@dassi_ai/cli 0.4.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.
- package/.claude-plugin/plugin.json +8 -3
- package/README.md +116 -105
- package/daemon-client.mjs +181 -0
- package/dassi-daemon.mjs +106 -156
- package/dassi-shared.mjs +35 -172
- package/dassi.mjs +125 -270
- package/format-response.mjs +26 -11
- package/group-expansion.mjs +12 -3
- package/help-text.mjs +59 -48
- package/launch.mjs +21 -8
- package/package.json +4 -3
- 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,152 +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
|
-
|
|
84
|
-
|
|
85
|
-
# How the extension is loaded (auto-detected per Chrome binary):
|
|
86
|
-
# • Branded Google Chrome (137+) disabled the `--load-extension` flag, so launch
|
|
87
|
-
# installs the dist at runtime via the `Extensions.loadUnpacked` CDP command over
|
|
88
|
-
# `--remote-debugging-pipe`. Because such an extension lives only as long as the
|
|
89
|
-
# debugging pipe, launch spawns a detached `__launch-hold` helper that keeps the
|
|
90
|
-
# pipe open; `--stop` kills the helper, which closes the pipe and its Chrome.
|
|
91
|
-
# • Chrome for Testing / Chromium still honour `--load-extension` (persistent), so
|
|
92
|
-
# launch uses that directly there — no helper. Point at one with `--chrome <path>`.
|
|
93
|
-
# • The mode is auto-detected from the binary; override with `--load-mode auto|pipe|flag`
|
|
94
|
-
# (e.g. `dassi launch --chrome <cft> --load-mode pipe` to exercise the pipe path on CfT).
|
|
95
|
-
|
|
96
|
-
# Show version / help
|
|
97
|
-
dassi --version
|
|
98
|
-
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>'
|
|
99
100
|
```
|
|
100
101
|
|
|
101
|
-
|
|
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
|
+
```
|
|
102
108
|
|
|
103
|
-
|
|
104
|
-
# List all tab groups
|
|
105
|
-
dassi list-groups
|
|
109
|
+
Successful `stop` commands exit zero. `run` and task `status` exit nonzero for failed or stopped tasks.
|
|
106
110
|
|
|
107
|
-
|
|
108
|
-
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.
|
|
109
112
|
|
|
110
|
-
|
|
111
|
-
dassi run "extract prices" --group-title "Shopping"
|
|
113
|
+
## Multiple Chrome profiles
|
|
112
114
|
|
|
113
|
-
|
|
114
|
-
|
|
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>'
|
|
115
123
|
```
|
|
116
124
|
|
|
117
|
-
|
|
125
|
+
## Browser groups
|
|
118
126
|
|
|
119
|
-
|
|
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.
|
|
120
128
|
|
|
121
|
-
|
|
129
|
+
## Development and upgrades
|
|
122
130
|
|
|
123
|
-
|
|
124
|
-
|
|
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
|
+
```
|
|
125
138
|
|
|
126
|
-
|
|
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.
|
|
127
140
|
|
|
128
|
-
|
|
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.
|
|
129
142
|
|
|
130
|
-
|
|
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.
|
|
131
144
|
|
|
132
|
-
-
|
|
133
|
-
- Exposes a Unix socket (`~/.dassi/<session>.sock`) for CLI commands via NDJSON protocol
|
|
134
|
-
- Serializes concurrent `run` commands via a FIFO queue
|
|
135
|
-
- Handles first-run onboarding (extension install detection, login flow)
|
|
136
|
-
- 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.
|
|
137
146
|
|
|
138
|
-
|
|
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.
|
|
139
148
|
|
|
140
|
-
|
|
141
|
-
- [`open`](https://www.npmjs.com/package/open) — Opens browser for onboarding/install flow
|
|
149
|
+
## Output for scripts
|
|
142
150
|
|
|
143
|
-
|
|
151
|
+
Browser and task commands with `--json` write their response to stdout, including argument, setup, and connection failures:
|
|
144
152
|
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
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."}
|
|
148
155
|
```
|
|
149
156
|
|
|
150
|
-
|
|
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.
|
|
151
162
|
|
|
152
|
-
|
|
163
|
+
Run CLI tests with `pnpm --dir cli test`.
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/** CLI transport, daemon startup, and bounded observation of browser-owned tasks. */
|
|
2
|
+
|
|
3
|
+
import * as net from 'net';
|
|
4
|
+
import * as fs from 'fs';
|
|
5
|
+
import * as path from 'path';
|
|
6
|
+
import * as child_process from 'child_process';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
8
|
+
import {
|
|
9
|
+
getSocketPath,
|
|
10
|
+
CLI_PROTOCOL_VERSION,
|
|
11
|
+
getReadyFile,
|
|
12
|
+
isDaemonRunning,
|
|
13
|
+
parseReadyPayload,
|
|
14
|
+
CHROME_WEB_STORE_URL,
|
|
15
|
+
unknownToolOutcome,
|
|
16
|
+
} from './dassi-shared.mjs';
|
|
17
|
+
|
|
18
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
const DAEMON_SCRIPT = path.join(__dirname, 'dassi-daemon.mjs');
|
|
20
|
+
const READY_POLL_MS = 100;
|
|
21
|
+
const READY_TIMEOUT_MS = 30_000;
|
|
22
|
+
|
|
23
|
+
// ─── Daemon management ────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Ensures the daemon is running for the given session.
|
|
27
|
+
* Spawns it as a detached background process if the PID file is missing or stale.
|
|
28
|
+
* @param {string} session
|
|
29
|
+
*/
|
|
30
|
+
export function ensureDaemonRunning(session) {
|
|
31
|
+
if (isDaemonRunning(session)) return;
|
|
32
|
+
|
|
33
|
+
// Reason: detached + unref means the daemon outlives the CLI process
|
|
34
|
+
const daemon = child_process.spawn(process.execPath, [DAEMON_SCRIPT], {
|
|
35
|
+
detached: true,
|
|
36
|
+
stdio: 'ignore',
|
|
37
|
+
env: { ...process.env, DASSI_SESSION: session },
|
|
38
|
+
});
|
|
39
|
+
daemon.unref();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Polls for the ready file until it appears or the timeout elapses.
|
|
44
|
+
* Resolves with the parsed ready payload.
|
|
45
|
+
* @param {string} readyFile
|
|
46
|
+
* @param {number} [timeoutMs]
|
|
47
|
+
* @returns {Promise<{ status: string; [key: string]: unknown }>}
|
|
48
|
+
*/
|
|
49
|
+
export function waitForReady(readyFile, timeoutMs = READY_TIMEOUT_MS) {
|
|
50
|
+
return new Promise((resolve, reject) => {
|
|
51
|
+
const deadline = Date.now() + timeoutMs;
|
|
52
|
+
const check = () => {
|
|
53
|
+
if (fs.existsSync(readyFile)) {
|
|
54
|
+
try {
|
|
55
|
+
resolve(parseReadyPayload(fs.readFileSync(readyFile, 'utf8')));
|
|
56
|
+
return;
|
|
57
|
+
} catch { /* file may be partially written — keep polling */ }
|
|
58
|
+
}
|
|
59
|
+
if (Date.now() >= deadline) {
|
|
60
|
+
const seconds = Math.round(timeoutMs / 1000);
|
|
61
|
+
reject(new Error(`Timed out waiting for Dassi daemon to start (${seconds}s)`));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
setTimeout(check, READY_POLL_MS);
|
|
65
|
+
};
|
|
66
|
+
check();
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Connects to the daemon Unix socket, sends one NDJSON command, and reads the response.
|
|
72
|
+
* @param {string} socketPath - Unix socket path.
|
|
73
|
+
* @param {object} command - Command object to send (will be JSON-stringified).
|
|
74
|
+
* @returns {Promise<object>} Parsed response object.
|
|
75
|
+
*/
|
|
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
|
+
} = {}) {
|
|
82
|
+
return new Promise((resolve, reject) => {
|
|
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);
|
|
85
|
+
let buffer = '';
|
|
86
|
+
let settled = false;
|
|
87
|
+
let submitted = false;
|
|
88
|
+
|
|
89
|
+
// Reason: guard against double-settlement since 'close' fires after socket.destroy()
|
|
90
|
+
// in the normal data path, and we don't want the close handler to re-resolve
|
|
91
|
+
const settle = (fn, val) => {
|
|
92
|
+
if (settled) return;
|
|
93
|
+
settled = true;
|
|
94
|
+
clearTimeout(timer);
|
|
95
|
+
fn(fn === reject && submitted && command.action === 'tool_exec' ? unknownToolOutcome(val) : val);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
socket.on('connect', () => {
|
|
99
|
+
submitted = true;
|
|
100
|
+
socket.write(JSON.stringify(command) + '\n');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
socket.on('data', (chunk) => {
|
|
104
|
+
buffer += chunk.toString();
|
|
105
|
+
const nl = buffer.indexOf('\n');
|
|
106
|
+
if (nl !== -1) {
|
|
107
|
+
const line = buffer.slice(0, nl);
|
|
108
|
+
socket.destroy();
|
|
109
|
+
try { settle(resolve, JSON.parse(line)); }
|
|
110
|
+
catch { settle(reject, new Error(`Invalid JSON from daemon: ${line}`)); }
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
socket.on('error', (err) => settle(reject, err));
|
|
115
|
+
|
|
116
|
+
socket.on('close', () => {
|
|
117
|
+
if (settled) return;
|
|
118
|
+
// Reason: attempt to parse whatever arrived before the socket closed unexpectedly
|
|
119
|
+
if (buffer.trim()) {
|
|
120
|
+
try { settle(resolve, JSON.parse(buffer.trim())); }
|
|
121
|
+
catch { settle(reject, new Error(`Invalid JSON from daemon: ${buffer.trim()}`)); }
|
|
122
|
+
} else {
|
|
123
|
+
settle(reject, new Error('Daemon closed connection without a response'));
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ─── Daemon readiness ─────────────────────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Ensures the local command transport is ready.
|
|
133
|
+
* @param {string} session
|
|
134
|
+
* @returns {Promise<string>} The daemon's Unix socket path.
|
|
135
|
+
*/
|
|
136
|
+
export async function ensureDaemonReady(session) {
|
|
137
|
+
const socketPath = getSocketPath(session);
|
|
138
|
+
const readyFile = getReadyFile(session);
|
|
139
|
+
|
|
140
|
+
// Reason: delete any stale ready file from a previous run so the new daemon
|
|
141
|
+
// writes a fresh one. Skip if a daemon is already running to avoid racing.
|
|
142
|
+
if (!isDaemonRunning(session)) {
|
|
143
|
+
try { fs.unlinkSync(readyFile); } catch { /* ok if missing */ }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
ensureDaemonRunning(session);
|
|
147
|
+
|
|
148
|
+
const ready = await waitForReady(readyFile);
|
|
149
|
+
|
|
150
|
+
if (ready.status === 'extension_not_installed') {
|
|
151
|
+
throw new Error(
|
|
152
|
+
`Dassi extension not detected.\n\n` +
|
|
153
|
+
` Install it from:\n ${CHROME_WEB_STORE_URL}\n\n` +
|
|
154
|
+
` Then run this command again.`
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
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.');
|
|
160
|
+
|
|
161
|
+
return socketPath;
|
|
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
|
+
}
|