@dassi_ai/cli 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,12 +1,17 @@
1
1
  {
2
2
  "name": "dassi",
3
- "description": "Drive the Dassi Chrome extension from Claude Code. Pick tabs/groups, then run AI agent or browser tools against them.",
4
- "version": "0.1.3",
3
+ "description": "Use Dassi in Chrome: select a browser target, operate pages directly, or delegate a task and retrieve its result.",
4
+ "version": "0.4.0",
5
5
  "author": {
6
6
  "name": "Omnify Labs",
7
7
  "email": "team@dassi.ai"
8
8
  },
9
9
  "homepage": "https://dassi.ai",
10
10
  "license": "MIT",
11
- "keywords": ["browser", "automation", "chrome-extension", "agent"]
11
+ "keywords": [
12
+ "browser",
13
+ "automation",
14
+ "chrome-extension",
15
+ "agent"
16
+ ]
12
17
  }
package/README.md CHANGED
@@ -1,155 +1,163 @@
1
- # @dassi_ai/cli
1
+ # Dassi CLI
2
2
 
3
- Standalone CLI for the [Dassi](../extension/README.md) Chrome extension run browser automation from the terminal.
3
+ Control Dassi in Chrome from a terminal or a local agent such as Codex, Claude Code, or OpenCode. Requires Node ≥20.11 with npm, and Google Chrome on the same macOS or Linux machine.
4
4
 
5
- ## Quickstart
5
+ ## One-command setup
6
6
 
7
- ```bash
8
- # 1. Install the Dassi extension from the Chrome Web Store and sign in.
9
- # 2. Open the Dassi side panel once — this wakes the extension so the CLI can reach it.
10
- # 3. Run a command (no install needed):
11
- npx @dassi_ai/cli@latest list-tabs # find a TAB ID
12
- npx @dassi_ai/cli@latest run "summarize this page" --tab <id>
7
+ ```sh
8
+ npx --yes @dassi_ai/cli@latest setup
13
9
  ```
14
10
 
15
- Prefer a permanent `dassi` command? `npm install -g @dassi_ai/cli` (Node 20.11), then drop the `npx @dassi_ai/cli@latest` prefix.
11
+ Setup installs the CLI into npm's global location and registers its bundled skill in `~/.agents/skills/dassi` (Codex/OpenCode) and `~/.claude/skills/dassi` (Claude Code). It honors `CLAUDE_CONFIG_DIR` for Claude's location. The terminal command and agent launcher use that same npm package. An existing global install is reused; an npx invocation copies its package out of the temporary cache using npm's `--install-links`. Existing skills pointing elsewhere are preserved and reported for manual resolution.
16
12
 
17
- Two things that trip people up:
18
- - **`run` needs a target** — pass `--tab <id>` (or `--group <id>` / `--group-title <name>`). `--profile` only selects *which* connected Chrome, not the tab.
19
- - **Multiple Chrome windows/profiles connected?** Commands need `--profile <label>`; run `dassi list-profiles` to see the labels (one profile = no flag needed).
13
+ If you prefer installing the command first:
20
14
 
21
- ## Installation
15
+ ```sh
16
+ npm install -g @dassi_ai/cli@latest
17
+ dassi
18
+ ```
19
+
20
+ Running `dassi` without arguments in an interactive terminal starts the same setup flow. Scripts must request `setup` explicitly; bare noninteractive and JSON invocations return an actionable error. Setup requires a writable npm global location. If npm reports a permissions error, follow its [user-level installation guidance](https://docs.npmjs.com/resolving-eacces-permissions-errors-when-installing-packages-globally/), then rerun setup.
21
+
22
+ If Dassi is not connected, setup opens its Chrome Web Store listing. In the Chrome profile you want to use, click **Add to Chrome**, then **Add extension**. Chrome downloads and installs the extension. Setup waits for it to connect and verifies live tool discovery and tab access. With multiple connected profiles, an interactive terminal asks which one to use; scripts receive the available profile IDs instead of a guessed selection.
23
+
24
+ Browser tools work while signed out. Setup finishes after verifying the browser and does not open sign-in settings. Dassi sign-in is needed when you choose task delegation with `run`. JSON reports delegation separately; `data.delegationError` explains an unavailable sign-in check without blocking browser readiness. Setup retries temporary browser connection failures until its deadline. It does not run a model task as its connection check. The Web Store provides extension updates; a connection that lacks tool discovery receives update instructions.
22
25
 
23
- ```bash
24
- # Zero-install
25
- npx @dassi_ai/cli --help
26
+ Setup waits up to five minutes for Chrome by default. Rerun the same command to resume. For local agents and scripts:
26
27
 
27
- # Or install globally
28
- npm install -g @dassi_ai/cli
28
+ ```sh
29
+ npx --yes @dassi_ai/cli@latest setup --wait 0 --json
30
+ npx --yes @dassi_ai/cli@latest setup --profile '<profile-id>' --wait 30s --json
29
31
  ```
30
32
 
31
- Or, for local development from a clone of this repo:
33
+ Read `data.ready`, `data.browserTools`, and `data.delegation`. An incomplete setup exits nonzero and includes the next action; it preserves the installation already completed. `--no-open` prints instructions without opening Chrome. Chrome installation confirmation and account sign-in remain user actions. Setup's observer deadline does not stop browser work.
32
34
 
33
- ```bash
34
- cd cli
35
- npm install
36
- npm link # exposes `dassi` on PATH
35
+ When setup says **“You're ready,”** open your agent and ask **“Use Dassi to show my open browser tabs.”** Restart an already-running agent to load the skill. Its bundled launcher works without PATH configuration. Installation paths and delegation diagnostics remain available in `setup --json`.
36
+
37
+ ## Update and remove
38
+
39
+ Update the terminal command and its bundled agent skill together:
40
+
41
+ ```sh
42
+ npm install -g @dassi_ai/cli@latest
37
43
  ```
38
44
 
39
- ## Prerequisites
45
+ You can also rerun `npx --yes @dassi_ai/cli@latest setup` to update and check Chrome. Restart running agents to load updated skill instructions. If you change Node versions or npm prefixes, rerun setup; registrations pointing elsewhere are reported for manual resolution.
40
46
 
41
- - Node.js >= 20.11.1
42
- - Dassi Chrome extension installed and running
43
- - **External Bridge** enabled in Dassi Options (`options.html?dev`)
47
+ To remove Dassi from local agents and uninstall the CLI:
44
48
 
45
- ## Usage
49
+ ```sh
50
+ dassi skill --remove
51
+ npm uninstall -g @dassi_ai/cli
52
+ ```
46
53
 
47
- ```bash
48
- # List all open Chrome tabs
49
- dassi list-tabs
54
+ Skill removal deletes only symlinks to this npm installation, preserving custom skills and other installations. npm owns package removal. Chrome's extension, browser conversations, and runtime files are separate; remove the extension through Chrome if desired.
50
55
 
51
- # Run an agent prompt on a specific tab
52
- dassi run "summarize the top issues" --tab 456
56
+ ## Use from a local agent
53
57
 
54
- # Run with a timeout (ms)
55
- dassi run "check inbox" --tab 123 --timeout 60000
58
+ The shared skill includes `scripts/dassi.mjs`, a launcher for the matching CLI version:
56
59
 
57
- # Send a raw JSON command
58
- dassi raw '{"id":"1","action":"list_tabs"}'
60
+ ```sh
61
+ node '<installed-skill-directory>/scripts/dassi.mjs' list-tabs --json
62
+ ```
63
+
64
+ Examples below use `dassi` as shorthand for that invocation or the globally installed command. `dassi skill` prints the bundled workflow without requiring Chrome; `dassi skill --path` locates its directory. The skill covers profile selection, tool discovery, delegation, bounded polling and recovery without resubmitting accepted work.
65
+
66
+ Native registration follows the documented [Codex](https://learn.chatgpt.com/docs/build-skills), [Claude Code](https://code.claude.com/docs/en/skills), and [OpenCode](https://opencode.ai/docs/skills) discovery locations. Other shell agents can load the same skill and launcher. Shell and local browser connectivity remain required.
67
+
68
+ ## Discover and call browser tools
59
69
 
60
- # Output as JSON (for scripting)
70
+ The connected extension owns tool names, instructions and parameter schemas. The CLI has no browser-name translation table or cached catalog.
71
+
72
+ ```sh
61
73
  dassi list-tabs --json
74
+ # Copy a target from the intended profile.
75
+ dassi tools --tab '<target>' --json
76
+ dassi tools '<name-from-discovery>' --tab '<target>' --json
77
+ dassi call '<name-from-discovery>' --tab '<target>' --args '{"parameter":"value"}' --json
78
+ ```
62
79
 
63
- # Use a named session
64
- dassi list-tabs --session work
80
+ `tools` lists short descriptions; `tools <name>` returns the full description and JSON schema. Discovery does not group tabs or claim a conversation. You can also use `tools --profile '<profile-id>'` without a tab. It requires one identifiable profile and does not merge catalogs from different installations.
65
81
 
66
- # List connected profiles (multiple Chrome profiles can connect at once)
67
- dassi list-profiles
82
+ `call` forwards the tool name and JSON object unchanged. The extension applies defaults, validates the parameters, checks its existing direct-call restrictions and executes under the existing CLI ownership rules. It supplies the outer `--tab` target to tools whose schema declares `tabId`, overriding any different tab ID in the JSON. Other schemas receive no added argument. Tools that require agent-run, automation or human-reply context (including `ask_user`) are excluded from both discovery and direct calls.
68
83
 
69
- # Target a specific profile by label when more than one is connected
70
- dassi list-tabs --profile dev
71
- dassi run "summarize" --tab 456 --profile dev # alias: --label
72
-
73
- # Launch a fresh Chrome with a locally-built dev dist (for testing extension changes)
74
- pnpm build # build extension/dist first
75
- dassi launch # loads extension/dist as profile "dev"
76
- dassi launch --label qa --dist some/dist # custom label + dist
77
- # Drive the launched Chrome run still needs a tab/group target; --profile selects which Chrome:
78
- dassi list-tabs --profile dev # find a tab id in the launched profile
79
- dassi run "summarize this page" --tab <id> --profile dev
80
- dassi launch --stop # close the "dev" Chrome (or --stop-all)
81
- # Pass extra args to Chrome by appending `-- <args>` after all CLI flags
82
- # (forwarded verbatim in both load modes; rejected on non-launch commands):
83
- dassi launch -- --headless=new --remote-debugging-port=9222
84
- # Note: launch reuses the default daemon (port 18790). To launch on an isolated
85
- # port (DASSI_BRIDGE_PORT=18791 dassi launch), no default daemon may be running —
86
- # it errors clearly otherwise, since it can't confirm the running daemon's port.
87
- #
88
- # How the extension is loaded (auto-detected per Chrome binary):
89
- # • Branded Google Chrome (137+) disabled the `--load-extension` flag, so launch
90
- # installs the dist at runtime via the `Extensions.loadUnpacked` CDP command over
91
- # `--remote-debugging-pipe`. Because such an extension lives only as long as the
92
- # debugging pipe, launch spawns a detached `__launch-hold` helper that keeps the
93
- # pipe open; `--stop` kills the helper, which closes the pipe and its Chrome.
94
- # • Chrome for Testing / Chromium still honour `--load-extension` (persistent), so
95
- # launch uses that directly there — no helper. Point at one with `--chrome <path>`.
96
- # • The mode is auto-detected from the binary; override with `--load-mode auto|pipe|flag`
97
- # (e.g. `dassi launch --chrome <cft> --load-mode pipe` to exercise the pipe path on CfT).
98
-
99
- # Show version / help
100
- dassi --version
101
- dassi --help
84
+ Direct calls wait for the capability's result or error; the CLI and daemon impose no additional execution deadline. Use the timeout parameter from that tool's schema when available. Closing the CLI does not cancel a direct call. A lost connection after submission reports `outcome: "unknown"`; inspect the browser before retrying because the action may still be running. Actual tool errors retain their original meaning.
85
+
86
+ To save returned images, use `call ... -o /tmp/dassi-page.jpg` without `--json`. JSON returns raw image content. `panel-screenshot` still captures Dassi's own UI, and `open [url] --window <id>` creates an ungrouped background tab (`--foreground` activates it).
87
+
88
+ CLI 0.7 replaces the handwritten browser aliases (`read-page`, `get-text`, `click`, `fill`, `type`, `navigate`, `screenshot`, `eval`, `tabs`, `close`, and grouped `open`) with `tools` and `call`. Scripts must discover the current tool and use its schema. `list-tabs`, `run`, `status`, `stop`, and the raw bridge interface retain their roles.
89
+
90
+ ## Delegate a task
91
+
92
+ Copy the `TARGET` from the listing, then start a task:
93
+
94
+ ```sh
95
+ dassi run 'Review these applications' --tab '<target>'
96
+ # Returns a task ID after the browser accepts the work.
97
+ dassi status '<task-id>'
98
+ dassi run 'Now compare the strongest two' --task '<task-id>'
99
+ dassi stop '<task-id>'
102
100
  ```
103
101
 
104
- ### Tab groups
102
+ A task can run for more than 30 minutes. Closing the terminal, a disconnected CLI, or an expired wait does not cancel it. To wait for a result in the current command:
103
+
104
+ ```sh
105
+ dassi run 'Summarize this page' --tab '<target>' --wait 30s --json
106
+ dassi status '<task-id>' --wait 10m --json
107
+ ```
105
108
 
106
- ```bash
107
- # List all tab groups
108
- dassi list-groups
109
+ Successful `stop` commands exit zero. `run` and task `status` exit nonzero for failed or stopped tasks.
109
110
 
110
- # Run agent against every tab in a group (sequential)
111
- dassi run "summarize each page" --group 7
111
+ When the wait ends, inspect `data.status`: `running` or `stopping` means check the **same task** again. `completed`, `failed`, and `stopped` are final. Each task is one conversation. A fresh `run --tab` starts a new conversation, even on the same tab. `run --task` explicitly continues that conversation and returns the same ID; status shows its latest reply and run outcome. A follow-up submitted while running uses the native follow-up queue. It has no separate result ID. Status reports persisted messages, so an in-progress model turn may not yet appear. Keep the full opaque task ID; it includes its browser identity. Tasks and their transcripts remain available until their conversation history is deleted. Follow-ups use the original browser tab; if it has closed, start a new task on another tab.
112
112
 
113
- # Use a group title instead of id (errors if title is ambiguous)
114
- dassi run "extract prices" --group-title "Shopping"
113
+ ## Multiple Chrome profiles
115
114
 
116
- # Tool commands also accept --group / --group-title
117
- dassi screenshot --group-title "Research" -o shot.png
115
+ `list-tabs`, `list-groups`, `list-profiles`, and bare `status` discover all connected profiles. Listings include profile labels, stable installation IDs, and sign-in status. A target combines the installation ID and the Chrome tab/group ID. Always copy it; tab IDs alone do not identify a browser profile.
116
+
117
+ For an existing numeric tab ID, `--profile <id-or-unique-name>` remains available. Duplicate names produce an ambiguity error. Profile selection does not fall back to another installation after disconnect. Sign-in is checked in the profile receiving a task, and discovery works while signed out.
118
+
119
+ ```sh
120
+ dassi list-profiles
121
+ dassi list-tabs --profile '<profile-id>'
122
+ dassi run 'Summarize' --tab 42 --profile '<profile-id>'
118
123
  ```
119
124
 
120
- When `--group`/`--group-title` is used, the CLI expands to member tab ids and runs them sequentially in one daemon session. True parallel execution is not currently supported — the daemon binds a fixed WebSocket port (`18790` by default, override with `DASSI_BRIDGE_PORT`), so multiple daemon processes can't coexist regardless of `--session`. Sequential dispatch is the only supported pattern.
125
+ ## Browser groups
121
126
 
122
- ## Claude Code Plugin
127
+ `run --group <target>` submits **one prompt for the whole group**, using the group as its browser workspace. `call --group <target>` executes sequentially once per member tab; use a single tab for an action intended to happen once. A fresh `run` on a busy group is refused with the existing task ID. Wait, explicitly continue that task, stop it, or use another tab.
123
128
 
124
- This package also ships as a Claude Code plugin under the `dassi` namespace. After installation (via either `npm install -g @dassi_ai/cli` or `npm link` from this directory), Claude Code auto-discovers two skills:
129
+ ## Development and upgrades
125
130
 
126
- - **`dassi:pick-tabs`** — a reusable tab/group picker. Lists open tabs and Chrome tab groups, asks the user to pick, returns the selected Chrome tab IDs.
127
- - **`dassi:operate`** — main entry point. Translates natural-language browser asks ("summarize my Research group", "screenshot the active tab", etc.) into `dassi` CLI invocations.
131
+ ```sh
132
+ # From the repository root:
133
+ pnpm --dir extension build
134
+ dassi launch --label dev
135
+ dassi list-tabs --profile dev
136
+ dassi launch --stop dev
137
+ ```
128
138
 
129
- The plugin manifest lives at `.claude-plugin/plugin.json`; skill content is under `skills/<name>/SKILL.md`. No additional configuration is required installing the CLI is sufficient.
139
+ Launch supports `--dist`, `--chrome`, `--profile-dir`, `--load-mode auto|pipe|flag`, and Chrome arguments after `--`. Branded Chrome uses a detached helper holding a CDP pipe for the unpacked extension; Chromium/Chrome for Testing can use `--load-extension` directly.
130
140
 
131
- ## How It Works
141
+ CLI 0.7 requires the current extension with live tool discovery, and CLI protocol 3 for task submission. Incompatible clients, daemons, and extensions fail before a task is submitted, in either upgrade order. After upgrading the CLI, finish any old CLI calls and restart an older daemon if prompted (its PID is in `~/.dassi/default.pid`). Browser conversations survive a daemon restart. This package is not auto-published by a source change.
132
142
 
133
- The CLI (`dassi.mjs`) auto-spawns a background daemon (`dassi-daemon.mjs`) that:
143
+ `run --timeout` is rejected with migration instructions. Use `--wait` to observe and `stop` to cancel. Scripts must check task status and explicitly stop a task if they impose an execution budget. `--session`/`DASSI_SESSION` are retained for development compatibility; they select a daemon socket, not a browser or conversation. Normal use needs neither. `DASSI_BRIDGE_PORT` configures an isolated development bridge.
134
144
 
135
- - Hosts a WebSocket server on port 18790 for the Chrome extension to connect to
136
- - Exposes a Unix socket (`~/.dassi/<session>.sock`) for CLI commands via NDJSON protocol
137
- - Serializes concurrent `run` commands via a FIFO queue
138
- - Handles first-run onboarding (extension install detection, login flow)
139
- - Shuts down after 30 minutes of idle
145
+ The daemon routes short requests. Native runtime admission controls each conversation; the daemon does not serialize unrelated profiles or hold a connection for a task's lifetime. Startup requires no sign-in. The native conversation owns admission, cancellation, recovery, and transcript. Status reads its transcript and state in one storage transaction; a single last-run summary survives later compaction. There is no per-prompt task ledger.
140
146
 
141
- ## Dependencies
147
+ Before promoting a CLI release to npm `latest`, make its compatible extension available in the Chrome Web Store. Check the packed npm artifact in a fresh global prefix, then verify setup against that store installation: extension confirmation, signed-out browser access, profile choice, agent discovery, an initial tab listing, and resume after interruption. Local builds and injected browser responses do not establish that store-to-npm release path.
142
148
 
143
- - [`ws`](https://www.npmjs.com/package/ws) — WebSocket server for extension communication
144
- - [`open`](https://www.npmjs.com/package/open) — Opens browser for onboarding/install flow
149
+ ## Output for scripts
145
150
 
146
- ## Development
151
+ Browser and task commands with `--json` write their response to stdout, including argument, setup, and connection failures:
147
152
 
148
- ```bash
149
- # Run tests
150
- npm test
153
+ ```json
154
+ {"success": false, "error": "An older Dassi daemon is running. Finish active tasks, restart the daemon, then retry with this CLI."}
151
155
  ```
152
156
 
153
- ## License
157
+ Parse stdout even on a nonzero exit. Successful responses contain `data`; failures contain `error`. Direct group tools return an array of `{tabId, response}`. Task status, rather than exit zero alone, tells you whether delegated work finished. `--help`, `--version`, and `skill` also support JSON, with a string in `data`. Development `launch` commands use terminal output.
158
+
159
+ An error's top-level `taskId` means that submission may have been accepted: check it before retrying. `existingTaskId` refers to other work already using the browser workspace. Do not treat it as permission to cancel that work.
160
+
161
+ The shared skill ships in `skills/dassi`. It replaces the former `operate` and `pick-tabs` skills. The Claude Code plugin manifest remains at `.claude-plugin/plugin.json`; its skill command is now `/dassi:dassi` when loaded as a plugin. Installing the npm package alone does not register a plugin in an agent.
154
162
 
155
- MIT
163
+ Run CLI tests with `pnpm --dir cli test`.
package/daemon-client.mjs CHANGED
@@ -1,11 +1,4 @@
1
- /**
2
- * Daemon client — the CLI side of the daemon protocol. Extracted from
3
- * dassi.mjs to keep that file within the CLAUDE.md 500-line limit.
4
- *
5
- * Covers spawning the daemon, waiting for its ready file, the Unix-socket
6
- * NDJSON transport, and the interactive onboarding flow (install prompt +
7
- * sign-in poll loop).
8
- */
1
+ /** CLI transport, daemon startup, and bounded observation of browser-owned tasks. */
9
2
 
10
3
  import * as net from 'net';
11
4
  import * as fs from 'fs';
@@ -14,19 +7,18 @@ import * as child_process from 'child_process';
14
7
  import { fileURLToPath } from 'url';
15
8
  import {
16
9
  getSocketPath,
10
+ CLI_PROTOCOL_VERSION,
17
11
  getReadyFile,
18
12
  isDaemonRunning,
19
13
  parseReadyPayload,
20
- DASSI_EXTENSION_ID,
14
+ CHROME_WEB_STORE_URL,
15
+ unknownToolOutcome,
21
16
  } from './dassi-shared.mjs';
22
17
 
23
18
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
24
19
  const DAEMON_SCRIPT = path.join(__dirname, 'dassi-daemon.mjs');
25
20
  const READY_POLL_MS = 100;
26
21
  const READY_TIMEOUT_MS = 30_000;
27
- const LOGIN_POLL_MS = 2_000;
28
- const LOGIN_TIMEOUT_MS = 5 * 60_000;
29
- const CHROME_WEB_STORE_URL = `https://chromewebstore.google.com/detail/dassi-ai-browser-agent-fo/${DASSI_EXTENSION_ID}`;
30
22
 
31
23
  // ─── Daemon management ────────────────────────────────────────────────────────
32
24
 
@@ -81,21 +73,30 @@ export function waitForReady(readyFile, timeoutMs = READY_TIMEOUT_MS) {
81
73
  * @param {object} command - Command object to send (will be JSON-stringified).
82
74
  * @returns {Promise<object>} Parsed response object.
83
75
  */
84
- export function sendCommand(socketPath, command) {
76
+ export function sendCommand(socketPath, command, {
77
+ // Direct tools own their deadlines; a plugin need not expose a timeout parameter.
78
+ timeoutMs = command.action === 'tool_exec' ? null : command.action !== 'run' && Number.isFinite(command.timeoutMs) && command.timeoutMs > 0
79
+ ? Math.min(command.timeoutMs + 15_000, 2147483647)
80
+ : 70_000,
81
+ } = {}) {
85
82
  return new Promise((resolve, reject) => {
86
83
  const socket = net.createConnection(socketPath);
84
+ const timer = timeoutMs == null ? null : setTimeout(() => socket.destroy(new Error('Daemon did not respond. Check the existing task before retrying.')), timeoutMs);
87
85
  let buffer = '';
88
86
  let settled = false;
87
+ let submitted = false;
89
88
 
90
89
  // Reason: guard against double-settlement since 'close' fires after socket.destroy()
91
90
  // in the normal data path, and we don't want the close handler to re-resolve
92
91
  const settle = (fn, val) => {
93
92
  if (settled) return;
94
93
  settled = true;
95
- fn(val);
94
+ clearTimeout(timer);
95
+ fn(fn === reject && submitted && command.action === 'tool_exec' ? unknownToolOutcome(val) : val);
96
96
  };
97
97
 
98
98
  socket.on('connect', () => {
99
+ submitted = true;
99
100
  socket.write(JSON.stringify(command) + '\n');
100
101
  });
101
102
 
@@ -125,74 +126,10 @@ export function sendCommand(socketPath, command) {
125
126
  });
126
127
  }
127
128
 
128
- // ─── Login helpers ────────────────────────────────────────────────────────────
129
-
130
- /**
131
- * Defense-in-depth check for `optionsUrl` before handing it to the `open`
132
- * package. Legit URLs always come from `chrome.runtime.getURL('options.html')`
133
- * → `chrome-extension://<id>/options.html`. Anything else is unexpected and
134
- * we should not auto-launch it (the `open` package shells out to the OS URL
135
- * handler).
136
- * @param {unknown} optionsUrl
137
- * @returns {boolean}
138
- */
139
- export function isValidOptionsUrl(optionsUrl) {
140
- return typeof optionsUrl === 'string' && /^chrome-extension:\/\/[a-z]{32}\//.test(optionsUrl);
141
- }
142
-
143
- /**
144
- * Polls the daemon's status until the user signs in (extension reports
145
- * authenticated=true) or the LOGIN_TIMEOUT_MS deadline elapses. On entry,
146
- * best-effort auto-opens the extension's options page (subject to the
147
- * isValidOptionsUrl guard above).
148
- * @param {string} socketPath - Path to the daemon Unix socket.
149
- * @param {string | undefined} optionsUrl - URL of the Dassi options page.
150
- * @returns {Promise<void>} Resolves on successful login; throws on timeout.
151
- */
152
- export async function waitForLogin(socketPath, optionsUrl) {
153
- console.error(`⚠️ Dassi is installed but you're not signed in.\n Opening the Dassi settings page...\n`);
154
-
155
- // Auto-open the options page — best-effort (package may not be installed).
156
- // Reason: only open URLs the extension would legitimately produce
157
- // (chrome.runtime.getURL → `chrome-extension://<id>/options.html`).
158
- // Defense-in-depth: even though optionsUrl is sourced from the daemon's
159
- // ready file (which lives in our owner-only ~/.dassi/ dir), validating the
160
- // protocol prevents `open` from launching arbitrary URIs/shell-handlers if
161
- // the file is ever tampered with.
162
- if (isValidOptionsUrl(optionsUrl)) {
163
- try {
164
- const { default: open } = await import('open');
165
- await open(String(optionsUrl));
166
- } catch {
167
- console.error(` Please open: ${optionsUrl}`);
168
- }
169
- } else if (optionsUrl) {
170
- console.error(` Refusing to auto-open unexpected URL: ${optionsUrl}\n Please open the Dassi settings page manually.`);
171
- }
172
-
173
- // Poll the daemon socket every LOGIN_POLL_MS until authenticated
174
- console.error(' Waiting for sign-in... (Ctrl+C to cancel)');
175
- const deadline = Date.now() + LOGIN_TIMEOUT_MS;
176
- let pollIndex = 0;
177
- while (Date.now() < deadline) {
178
- await new Promise((r) => setTimeout(r, LOGIN_POLL_MS));
179
- try {
180
- // Reason: increment poll index so each status request has a unique id for tracing
181
- const resp = await sendCommand(socketPath, { id: `poll-auth-${pollIndex++}`, action: 'status' });
182
- if (resp.success && resp.data?.authenticated) {
183
- console.error(` ✓ Signed in as ${resp.data.email}\n ✓ Ready\n`);
184
- return;
185
- }
186
- } catch { /* daemon may not be socket-ready yet — keep polling */ }
187
- }
188
-
189
- throw new Error('Login timed out. Please sign in and try again.');
190
- }
191
-
192
129
  // ─── Daemon readiness ─────────────────────────────────────────────────────────
193
130
 
194
131
  /**
195
- * Ensures the daemon is running and ready, handling onboarding if needed.
132
+ * Ensures the local command transport is ready.
196
133
  * @param {string} session
197
134
  * @returns {Promise<string>} The daemon's Unix socket path.
198
135
  */
@@ -211,17 +148,34 @@ export async function ensureDaemonReady(session) {
211
148
  const ready = await waitForReady(readyFile);
212
149
 
213
150
  if (ready.status === 'extension_not_installed') {
214
- console.error(
215
- `❌ Dassi extension not detected.\n\n` +
151
+ throw new Error(
152
+ `Dassi extension not detected.\n\n` +
216
153
  ` Install it from:\n ${CHROME_WEB_STORE_URL}\n\n` +
217
154
  ` Then run this command again.`
218
155
  );
219
- process.exit(1);
220
156
  }
221
157
 
222
- if (ready.status === 'needs_login') {
223
- await waitForLogin(socketPath, ready.optionsUrl ? String(ready.optionsUrl) : undefined);
224
- }
158
+ if (ready.status === 'error') throw new Error(ready.error);
159
+ if (ready.protocolVersion !== CLI_PROTOCOL_VERSION) throw new Error('An older Dassi daemon is running. Finish active tasks, restart the daemon, then retry with this CLI.');
225
160
 
226
161
  return socketPath;
227
162
  }
163
+
164
+ /** Waiting expires locally; it never cancels the browser-owned task. */
165
+ export async function sendAndWait(socketPath, command, send = sendCommand, now = Date.now, sleep = ms => new Promise(resolve => setTimeout(resolve, ms))) {
166
+ const { waitMs = 0, ...wire } = command;
167
+ if (['run', 'task_status', 'task_stop'].includes(wire.action)) wire.protocolVersion = CLI_PROTOCOL_VERSION;
168
+ let response = await send(socketPath, wire);
169
+ const deadline = now() + waitMs;
170
+ while (response.success && ['running', 'stopping'].includes(response.data?.status) && now() < deadline) {
171
+ await sleep(Math.min(1000, deadline - now()));
172
+ if (now() >= deadline) break;
173
+ const taskId = response.data.taskId;
174
+ try {
175
+ const next = await send(socketPath, { id: `${wire.id}-status-${now()}`, action: 'task_status', taskId, protocolVersion: CLI_PROTOCOL_VERSION }, { timeoutMs: Math.max(1, deadline - now()) });
176
+ if (!next.success) return { ...next, taskId, error: `${next.error} Check task ${taskId}; do not resubmit.` };
177
+ response = next;
178
+ } catch (error) { if (now() >= deadline) return response; return { success: false, taskId, error: `${error.message} Check task ${taskId}; do not resubmit.` }; }
179
+ }
180
+ return response;
181
+ }