@dassi_ai/cli 0.5.0 → 0.7.1
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.
- package/.claude-plugin/plugin.json +8 -3
- package/README.md +116 -108
- package/daemon-client.mjs +41 -85
- package/dassi-daemon.mjs +109 -156
- package/dassi-shared.mjs +40 -172
- package/dassi.mjs +100 -62
- package/format-response.mjs +26 -11
- package/group-expansion.mjs +12 -3
- package/help-text.mjs +59 -48
- package/launch.mjs +1 -1
- package/package.json +5 -2
- package/setup.mjs +200 -0
- package/skills/dassi/SKILL.md +69 -0
- package/skills/dassi/scripts/dassi.mjs +3 -0
- package/tool-commands.mjs +48 -89
- package/skills/operate/SKILL.md +0 -124
- package/skills/operate/command-reference.md +0 -65
- package/skills/pick-tabs/SKILL.md +0 -93
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dassi",
|
|
3
|
-
"description": "
|
|
4
|
-
"version": "0.
|
|
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": [
|
|
11
|
+
"keywords": [
|
|
12
|
+
"browser",
|
|
13
|
+
"automation",
|
|
14
|
+
"chrome-extension",
|
|
15
|
+
"agent"
|
|
16
|
+
]
|
|
12
17
|
}
|
package/README.md
CHANGED
|
@@ -1,155 +1,163 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Dassi CLI
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
##
|
|
5
|
+
## One-command setup
|
|
6
6
|
|
|
7
|
-
```
|
|
8
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
28
|
-
|
|
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
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
49
|
+
```sh
|
|
50
|
+
dassi skill --remove
|
|
51
|
+
npm uninstall -g @dassi_ai/cli
|
|
52
|
+
```
|
|
46
53
|
|
|
47
|
-
|
|
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
|
-
|
|
52
|
-
dassi run "summarize the top issues" --tab 456
|
|
56
|
+
## Use from a local agent
|
|
53
57
|
|
|
54
|
-
|
|
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
|
-
|
|
58
|
-
dassi
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
dassi
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
dassi
|
|
81
|
-
#
|
|
82
|
-
|
|
83
|
-
dassi
|
|
84
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
114
|
-
dassi run "extract prices" --group-title "Shopping"
|
|
113
|
+
## Multiple Chrome profiles
|
|
115
114
|
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
125
|
+
## Browser groups
|
|
121
126
|
|
|
122
|
-
|
|
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
|
-
|
|
129
|
+
## Development and upgrades
|
|
125
130
|
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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. Merging a `cli/package.json` version bump to `main` publishes this package (`.github/workflows/publish-cli.yml`, npm trusted publishing); an extension release is blocked until the CLI on npm speaks the protocol it requires.
|
|
132
142
|
|
|
133
|
-
|
|
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
|
-
-
|
|
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
|
-
|
|
147
|
+
A protocol bump ships CLI first: the CLI publishes on merge, and the extension release refuses to tag until npm `latest` declares that protocol (`scripts/check-cli-on-npm.sh` reads `dassi.protocol` from `cli/package.json` via `npm view`). While the matching extension is still in Chrome Web Store review, a freshly installed CLI reports that the extension is older and must be updated; CLIs already installed keep working against the store build. 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
|
-
|
|
144
|
-
- [`open`](https://www.npmjs.com/package/open) — Opens browser for onboarding/install flow
|
|
149
|
+
## Output for scripts
|
|
145
150
|
|
|
146
|
-
|
|
151
|
+
Browser and task commands with `--json` write their response to stdout, including argument, setup, and connection failures:
|
|
147
152
|
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
npm test
|
|
153
|
+
```json
|
|
154
|
+
{"success": false, "error": "An older Dassi daemon is running. Finish active tasks, stop it (kill the PID in ~/.dassi/default.pid), then retry with this CLI."}
|
|
151
155
|
```
|
|
152
156
|
|
|
153
|
-
|
|
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
|
-
|
|
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,19 @@ 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,
|
|
12
|
+
getPidFile,
|
|
18
13
|
isDaemonRunning,
|
|
19
14
|
parseReadyPayload,
|
|
20
|
-
|
|
15
|
+
CHROME_WEB_STORE_URL,
|
|
16
|
+
unknownToolOutcome,
|
|
21
17
|
} from './dassi-shared.mjs';
|
|
22
18
|
|
|
23
19
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
24
20
|
const DAEMON_SCRIPT = path.join(__dirname, 'dassi-daemon.mjs');
|
|
25
21
|
const READY_POLL_MS = 100;
|
|
26
22
|
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
23
|
|
|
31
24
|
// ─── Daemon management ────────────────────────────────────────────────────────
|
|
32
25
|
|
|
@@ -81,21 +74,30 @@ export function waitForReady(readyFile, timeoutMs = READY_TIMEOUT_MS) {
|
|
|
81
74
|
* @param {object} command - Command object to send (will be JSON-stringified).
|
|
82
75
|
* @returns {Promise<object>} Parsed response object.
|
|
83
76
|
*/
|
|
84
|
-
export function sendCommand(socketPath, command
|
|
77
|
+
export function sendCommand(socketPath, command, {
|
|
78
|
+
// Direct tools own their deadlines; a plugin need not expose a timeout parameter.
|
|
79
|
+
timeoutMs = command.action === 'tool_exec' ? null : command.action !== 'run' && Number.isFinite(command.timeoutMs) && command.timeoutMs > 0
|
|
80
|
+
? Math.min(command.timeoutMs + 15_000, 2147483647)
|
|
81
|
+
: 70_000,
|
|
82
|
+
} = {}) {
|
|
85
83
|
return new Promise((resolve, reject) => {
|
|
86
84
|
const socket = net.createConnection(socketPath);
|
|
85
|
+
const timer = timeoutMs == null ? null : setTimeout(() => socket.destroy(new Error('Daemon did not respond. Check the existing task before retrying.')), timeoutMs);
|
|
87
86
|
let buffer = '';
|
|
88
87
|
let settled = false;
|
|
88
|
+
let submitted = false;
|
|
89
89
|
|
|
90
90
|
// Reason: guard against double-settlement since 'close' fires after socket.destroy()
|
|
91
91
|
// in the normal data path, and we don't want the close handler to re-resolve
|
|
92
92
|
const settle = (fn, val) => {
|
|
93
93
|
if (settled) return;
|
|
94
94
|
settled = true;
|
|
95
|
-
|
|
95
|
+
clearTimeout(timer);
|
|
96
|
+
fn(fn === reject && submitted && command.action === 'tool_exec' ? unknownToolOutcome(val) : val);
|
|
96
97
|
};
|
|
97
98
|
|
|
98
99
|
socket.on('connect', () => {
|
|
100
|
+
submitted = true;
|
|
99
101
|
socket.write(JSON.stringify(command) + '\n');
|
|
100
102
|
});
|
|
101
103
|
|
|
@@ -125,74 +127,10 @@ export function sendCommand(socketPath, command) {
|
|
|
125
127
|
});
|
|
126
128
|
}
|
|
127
129
|
|
|
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
130
|
// ─── Daemon readiness ─────────────────────────────────────────────────────────
|
|
193
131
|
|
|
194
132
|
/**
|
|
195
|
-
* Ensures the
|
|
133
|
+
* Ensures the local command transport is ready.
|
|
196
134
|
* @param {string} session
|
|
197
135
|
* @returns {Promise<string>} The daemon's Unix socket path.
|
|
198
136
|
*/
|
|
@@ -211,17 +149,35 @@ export async function ensureDaemonReady(session) {
|
|
|
211
149
|
const ready = await waitForReady(readyFile);
|
|
212
150
|
|
|
213
151
|
if (ready.status === 'extension_not_installed') {
|
|
214
|
-
|
|
215
|
-
|
|
152
|
+
throw new Error(
|
|
153
|
+
`Dassi extension not detected.\n\n` +
|
|
216
154
|
` Install it from:\n ${CHROME_WEB_STORE_URL}\n\n` +
|
|
217
155
|
` Then run this command again.`
|
|
218
156
|
);
|
|
219
|
-
process.exit(1);
|
|
220
157
|
}
|
|
221
158
|
|
|
222
|
-
if (ready.status === '
|
|
223
|
-
|
|
224
|
-
}
|
|
159
|
+
if (ready.status === 'error') throw new Error(ready.error);
|
|
160
|
+
if (ready.protocolVersion > CLI_PROTOCOL_VERSION) throw new Error(`A newer Dassi daemon is running (protocol ${ready.protocolVersion}, this CLI speaks ${CLI_PROTOCOL_VERSION}). Run \`npm install -g @dassi_ai/cli@latest\` and retry.`);
|
|
161
|
+
if (ready.protocolVersion !== CLI_PROTOCOL_VERSION) throw new Error(`An older Dassi daemon is running. Finish active tasks, stop it (kill the PID in ${getPidFile(session)}), then retry with this CLI.`);
|
|
225
162
|
|
|
226
163
|
return socketPath;
|
|
227
164
|
}
|
|
165
|
+
|
|
166
|
+
/** Waiting expires locally; it never cancels the browser-owned task. */
|
|
167
|
+
export async function sendAndWait(socketPath, command, send = sendCommand, now = Date.now, sleep = ms => new Promise(resolve => setTimeout(resolve, ms))) {
|
|
168
|
+
const { waitMs = 0, ...wire } = command;
|
|
169
|
+
if (['run', 'task_status', 'task_stop'].includes(wire.action)) wire.protocolVersion = CLI_PROTOCOL_VERSION;
|
|
170
|
+
let response = await send(socketPath, wire);
|
|
171
|
+
const deadline = now() + waitMs;
|
|
172
|
+
while (response.success && ['running', 'stopping'].includes(response.data?.status) && now() < deadline) {
|
|
173
|
+
await sleep(Math.min(1000, deadline - now()));
|
|
174
|
+
if (now() >= deadline) break;
|
|
175
|
+
const taskId = response.data.taskId;
|
|
176
|
+
try {
|
|
177
|
+
const next = await send(socketPath, { id: `${wire.id}-status-${now()}`, action: 'task_status', taskId, protocolVersion: CLI_PROTOCOL_VERSION }, { timeoutMs: Math.max(1, deadline - now()) });
|
|
178
|
+
if (!next.success) return { ...next, taskId, error: `${next.error} Check task ${taskId}; do not resubmit.` };
|
|
179
|
+
response = next;
|
|
180
|
+
} catch (error) { if (now() >= deadline) return response; return { success: false, taskId, error: `${error.message} Check task ${taskId}; do not resubmit.` }; }
|
|
181
|
+
}
|
|
182
|
+
return response;
|
|
183
|
+
}
|