Aartiq
Aartiq™
Download
URL Protocol Reference

URL Protocol Reference

Three schemes are registered as protocol clients. All of them reach the app through a single open-url handler in main.js, which recognises 19 command names and forwards each to one IPC channel. Seven of those commands reach the app. Three type a request into the chat without carrying it out. Nine do nothing at all.

Delivery is macOS only

The schemes are registered on every platform, but the handler is an app.on('open-url') event, which Electron only emits on macOS. Windows and Linux deliver a protocol activation as an argv element on a second process launch, and this app registers neither requestSingleInstanceLock nor a second-instance handler, and never readsprocess.argv for a URL. On those platforms the link opens the app and is discarded.

URL Schemes

Registered Schemes

aartiq://

Command links, also used by Raycast and macOS Shortcuts

main.js:open-url accepts aartiq:// and comet://. comet:// is handled at runtime but is not in package.json, so nothing registers it.

aartiq-browser://

OAuth callback

The whole URL is forwarded to the renderer on the auth-callback channel, which preload.js subscribes to.

http:// and https://

Default-browser registration

Forwarded to add-new-tab, so a link opened through the protocol client loads as a browser tab.

Command Reference

aartiq:// Commands

aartiq://chat?message | prompt | queryReaches the app

Opens the AI sidebar and sends the text to ai-chat-input-text, which fills the chat composer. The text is not submitted.

aartiq://ask-ai?message | prompt | queryReaches the app

Identical to chat.

aartiq://voice-chatReaches the app

Opens the AI sidebar. Does not start voice input.

aartiq://navigate?urlReaches the app

Sends navigate-to-url. A bare host is prefixed with https://. This is one of the few sends that a renderer channel actually listens for.

aartiq://search?query | promptReaches the app

Opens a new tab on https://www.google.com/search?q=... The engine is hardcoded; there is no setting for it.

aartiq://volume?level (0-100)Reaches the app

Runs osascript to set the output volume. macOS only, and it bypasses the approval gate — it is a raw shell call in the main process.

aartiq://open-app?appNameReaches the app

Runs open -a with the name. macOS only, and also a raw shell call.

aartiq://create-pdf?title, contentTypes a request into chat, does not run it

Opens chat and types: Create a PDF titled "<title>" with this content: ... Returns success. No PDF is created and nothing is generated.

aartiq://create-doc?title, contentNothing happens

The string create-doc does not appear in the action handler at all. It is in the command map, pointing at the ai:create-pdf channel, but there is no branch for it, so the handler answers "Unknown or unsupported action: create-doc" and the channel send goes nowhere. It does not share a branch with create-pdf.

aartiq://run-command?commandTypes a request into chat, does not run it

Opens chat and types: Run this shell command: <command>. Returns success with the message "Prepared shell command request". No process is started. Note that the command text arrives as chat input, so it is not an execution path and is not subject to shell approval gating.

aartiq://schedule?task, cron | scheduleTypes a request into chat, does not run it

Opens chat and types: Schedule this task: <task>. Returns success. No automation is created and the cron string is never parsed.

aartiq://screenshotNothing happens

Routed to executeShortcutAction, which has no branch for it, so it returns "Unknown or unsupported action". The channel it is then sent to has no listener.

aartiq://set-modelNothing happens

Same as screenshot: no branch, no listener.

aartiq://browseNothing happens

Not in the action list at all, so the only thing that happens is a send on a channel with no listener.

aartiq://ocrNothing happens

Same as browse.

aartiq://pdfNothing happens

Same as browse.

aartiq://automationNothing happens

Same as browse.

aartiq://settingsNothing happens

Same as browse.

aartiq://indexNothing happens

Same as browse. It is also the fallback for an unrecognised command name.

Query Parameters

Parameters Actually Read

ParameterRead by
message / prompt / querychat, ask-ai, search
urlnavigate
content, titlecreate-pdf
commandrun-command
task, cron / scheduleschedule
levelvolume
appNameopen-app
speaksent on a channel with no listener — see below

Every other key in the query string is parsed into the params object and then ignored. There is no theme, language, settings-tab or file parameter, and adding one will not do anything.

Implementation

The Code

main.js — open-url

// main.js — the whole deep-link surface
app.on('open-url', async (event, url) => {
  event.preventDefault();
  let target = getTopWindow();
  if (!target) {
    await createWindow();
    target = mainWindow;
  }
  if (!target || target.isDestroyed()) return;

  const parsed = new URL(url);
  const pathname = parsed.pathname.replace(/^\/+/, '');
  const params = Object.fromEntries(parsed.searchParams);

  if (url.startsWith('aartiq-browser://')) {
    target.webContents.send('auth-callback', url);
  } else if (url.startsWith('http://') || url.startsWith('https://')) {
    target.webContents.send('add-new-tab', url);
  } else if (url.startsWith('aartiq://') || url.startsWith('comet://')) {
    const command = parsed.hostname || pathname || params.command || 'index';

    const commandMap = {
      'chat': 'open-ai-chat',
      'search': 'ai:search',
      'navigate': 'navigate-to-url',
      'create-pdf': 'ai:create-pdf',
      'create-doc': 'ai:create-pdf',
      'run-command': 'shell:execute',
      'open-app': 'system:open-app',
      'screenshot': 'system:screenshot',
      'volume': 'system:set-volume',
      'schedule': 'ai:schedule',
      'ask-ai': 'ai:ask-speaking',
      'voice-chat': 'ai:voice-chat',
      'set-model': 'ai:set-model',
      'browse': 'open-quick-browse',
      'ocr': 'trigger-screen-ocr',
      'pdf': 'open-pdf-creator',
      'automation': 'open-automation-panel',
      'settings': 'open-settings',
      'index': 'open-main',
    };

    const siriActions = ['chat', 'search', 'navigate', 'create-pdf', 'create-doc',
      'run-command', 'open-app', 'screenshot', 'volume', 'schedule', 'ask-ai',
      'voice-chat', 'set-model'];
    if (siriActions.includes(command)) {
      executeShortcutAction(command, params);
      if (params.speak === 'true') {
        target.webContents.once('did-finish-load', () => {
          target.webContents.send('ai:request-speak-response', params);
        });
      }
    }

    // Sent unconditionally. Of these nineteen channel names only
    // navigate-to-url has a renderer listener; the rest are discarded.
    target.webContents.send(commandMap[command] || command, params);
  }

  if (target.isMinimized()) target.restore();
  target.focus();
});

Reaches the app

chat, ask-ai, voice-chat, navigate, search, volume, open-app. chat, ask-ai, create-pdf, run-command and schedule go through the ai-chat-input-text channel, which the preload does subscribe to.

Returns success without doing the work

create-pdf, run-command and schedule. Each returns success: true while only having typed a sentence into the chat composer. A caller that checks the return value will believe a PDF was created or a task was scheduled.

Nothing happens

browse, ocr, pdf, automation, settings, index, screenshot, set-model and create-doc have no branch in the action handler — create-doc despite being pointed at the create-pdf channel, and screenshot and set-model despite both being in the action list. All nine end in a send on a channel with no listener.

Edge Cases

Worth Knowing

Unrecognised commands

The command is read from parsed.hostname || pathname, with index as the fallback. An unknown command falls back to index and then sends the raw command name as the channel, so the failure is silent.

aartiq://approve is handled elsewhere

High-risk MCP tool calls render a QR code containing aartiq://approve?id=…&pin=…. That URL is meant to be opened on the phone, and the Flutter app is what consumes it. On the desktop the same URL matches the command branch above and is discarded. The token and PIN are verified over HTTP by the MCP server, not by the deep-link handler.

volume and open-app skip the approval gate

Both call execPromise directly from the main process with an interpolated argument. They are not routed through the capability controller that gates execute-shell-command. They are also the only two commands that cannot work off macOS.

?speak=true does nothing

It queues a send on ai:request-speak-response behind did-finish-load. No listener for that channel exists anywhere in the codebase, so the reply is never spoken.

Claims removed from this page

  • —In-app route table. There is no /chat, /automation, /settings, /docs or /pdf-viewer routing target. The handler matches command names, not paths, and ignores anything it does not recognise.
  • —?tab=, ?theme=, ?lang= parameters. They are parsed and then never read.
  • —The Flutter example, which was JavaScript and Objective-C syntax presented as Dart, called a DeepLinkHandler class that does not exist, and used navigator.pushNamed on a context that was never in scope.
  • —Firebase Dynamic Links, a Google service retired in 2025 and not present in the codebase.
  • —apple-app-site-association and assetlinks.json for a domain. Nothing in the app fetches or verifies either file.
  • —An iOS Info.plist snippet. There is no iOS app; the mobile client is Flutter on Android and iOS, and its scheme handling is not this snippet.
  • —Windows Jump List and App Links. Neither is implemented.
  • —The web origin as a way to open in-app routes. A web link either loads a real page in a tab or, if the origin does not serve one, nothing.