@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.
- package/.claude-plugin/plugin.json +8 -3
- package/README.md +116 -108
- package/daemon-client.mjs +39 -85
- package/dassi-daemon.mjs +106 -156
- package/dassi-shared.mjs +35 -172
- package/dassi.mjs +99 -60
- 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 +2 -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
package/dassi.mjs
CHANGED
|
@@ -7,11 +7,12 @@
|
|
|
7
7
|
import * as fs from 'fs';
|
|
8
8
|
import * as path from 'path';
|
|
9
9
|
import * as child_process from 'child_process';
|
|
10
|
+
import { randomUUID } from 'node:crypto';
|
|
10
11
|
import { fileURLToPath, pathToFileURL } from 'url';
|
|
11
12
|
import { getSocketPath, validateSession, getAppDir, getLaunchesFile } from './dassi-shared.mjs';
|
|
12
|
-
import { ensureDaemonRunning, sendCommand, ensureDaemonReady } from './daemon-client.mjs';
|
|
13
|
+
import { ensureDaemonRunning, sendCommand, sendAndWait, ensureDaemonReady } from './daemon-client.mjs';
|
|
13
14
|
import { dispatchLaunch } from './launch.mjs';
|
|
14
|
-
import { parseToolCommand, requireTarget, parseStrictInt } from './tool-commands.mjs';
|
|
15
|
+
import { parseToolCommand, requireTarget, parseStrictInt, parseTarget, parseWait } from './tool-commands.mjs';
|
|
15
16
|
import { runWithGroupExpansion } from './group-expansion.mjs';
|
|
16
17
|
import { formatResponse } from './format-response.mjs';
|
|
17
18
|
import { HELP_TEXT } from './help-text.mjs';
|
|
@@ -19,22 +20,17 @@ import { HELP_TEXT } from './help-text.mjs';
|
|
|
19
20
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
20
21
|
const VERSION = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf8')).version;
|
|
21
22
|
|
|
22
|
-
// Reason: tests import isValidOptionsUrl from this module (it moved to
|
|
23
|
-
// daemon-client.mjs with the rest of the daemon-client unit) — re-export to
|
|
24
|
-
// keep the public surface and the tests unchanged.
|
|
25
|
-
export { isValidOptionsUrl } from './daemon-client.mjs';
|
|
26
|
-
|
|
27
23
|
// ─── Arg parsing ──────────────────────────────────────────────────────────────
|
|
28
24
|
|
|
29
25
|
/**
|
|
30
26
|
* Parses CLI arguments into a command descriptor.
|
|
31
|
-
* Supports
|
|
27
|
+
* Supports live tool discovery and generic browser calls,
|
|
32
28
|
* agent commands (run, list-tabs, status, raw), and top-level flags (--version, --help).
|
|
33
29
|
* Run `dassi --help` for the full command reference.
|
|
34
30
|
* @param {string[]} argv process.argv.slice(2)
|
|
35
31
|
* @returns {{ action: string; params: Record<string, unknown>; session: string; json: boolean; profile: string | null }}
|
|
36
32
|
*/
|
|
37
|
-
export function parseCliArgs(argv) {
|
|
33
|
+
export function parseCliArgs(argv, { interactive = false } = {}) {
|
|
38
34
|
const args = [...argv];
|
|
39
35
|
|
|
40
36
|
// Reason: hoist the `--` split to BEFORE global flag parsing so that Chrome
|
|
@@ -50,10 +46,10 @@ export function parseCliArgs(argv) {
|
|
|
50
46
|
// Reason: handle --version and --help before any flag parsing so they work
|
|
51
47
|
// even when other flags like --session are incomplete (e.g. `dassi --version --session`)
|
|
52
48
|
if (consumeFlag(args, '--version')) {
|
|
53
|
-
return { action: 'version', params: {}, session: 'default', json:
|
|
49
|
+
return { action: 'version', params: {}, session: 'default', json: args.includes('--json'), profile: null };
|
|
54
50
|
}
|
|
55
51
|
if (consumeFlag(args, '--help')) {
|
|
56
|
-
return { action: 'help', params: {}, session: 'default', json:
|
|
52
|
+
return { action: 'help', params: {}, session: 'default', json: args.includes('--json'), profile: null };
|
|
57
53
|
}
|
|
58
54
|
|
|
59
55
|
const session = validateSession(getFlag(args, '--session') ?? process.env.DASSI_SESSION ?? 'default');
|
|
@@ -62,10 +58,15 @@ export function parseCliArgs(argv) {
|
|
|
62
58
|
|
|
63
59
|
// Reason: centralise the return shape so every command branch includes profile
|
|
64
60
|
// without editing each return individually
|
|
65
|
-
const finish = (action, params) =>
|
|
61
|
+
const finish = (action, params) => {
|
|
62
|
+
if (args.length) throw new Error(`Unexpected argument: ${args[0]}. Run: dassi --help`);
|
|
63
|
+
const { profileTarget, ...rest } = params;
|
|
64
|
+
if (profile && profileTarget && profile !== profileTarget) throw new Error('Target includes a profile; omit --profile or use the same profile ID.');
|
|
65
|
+
return { action, params: rest, session, json, profile: profileTarget ?? profile };
|
|
66
|
+
};
|
|
66
67
|
|
|
67
|
-
const command = args.shift();
|
|
68
|
-
if (!command) throw new Error('No command specified. Run
|
|
68
|
+
const command = args.shift() ?? (interactive && argv.length === 0 ? 'setup' : undefined);
|
|
69
|
+
if (!command) throw new Error('No command specified. Run dassi setup to connect Chrome, or dassi --help for commands.');
|
|
69
70
|
|
|
70
71
|
// Reason: chromeArgs (tokens after `--`) are only meaningful for the `launch`
|
|
71
72
|
// command. Any other command that supplies `--` tokens would have them silently
|
|
@@ -74,18 +75,34 @@ export function parseCliArgs(argv) {
|
|
|
74
75
|
throw new Error("'--' is only valid with the launch command");
|
|
75
76
|
}
|
|
76
77
|
|
|
78
|
+
if (command === 'setup') {
|
|
79
|
+
const waitRaw = getFlag(args, '--wait');
|
|
80
|
+
return finish('setup', { waitMs: parseWait(waitRaw ?? '5m'), noOpen: consumeFlag(args, '--no-open') });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (command === 'skill') {
|
|
84
|
+
const pathOnly = consumeFlag(args, '--path');
|
|
85
|
+
const remove = consumeFlag(args, '--remove');
|
|
86
|
+
if (pathOnly && remove) throw new Error('Use either skill --path or skill --remove.');
|
|
87
|
+
return finish(remove ? 'remove_skill' : 'skill', remove ? {} : { pathOnly });
|
|
88
|
+
}
|
|
89
|
+
|
|
77
90
|
if (command === 'list-tabs') {
|
|
78
|
-
|
|
79
|
-
return finish('list_tabs',
|
|
91
|
+
consumeFlag(args, '--all');
|
|
92
|
+
return finish('list_tabs', { all: true });
|
|
80
93
|
}
|
|
81
94
|
|
|
82
95
|
if (command === 'list-groups') {
|
|
83
|
-
|
|
84
|
-
return finish('list_groups',
|
|
96
|
+
consumeFlag(args, '--all');
|
|
97
|
+
return finish('list_groups', { all: true });
|
|
85
98
|
}
|
|
86
99
|
|
|
87
|
-
if (command === 'status') {
|
|
88
|
-
|
|
100
|
+
if (command === 'status' || command === 'stop') {
|
|
101
|
+
const waitRaw = getFlag(args, '--wait');
|
|
102
|
+
const taskId = args[0] && !args[0].startsWith('-') ? args.shift() : undefined;
|
|
103
|
+
if (!taskId && (command === 'stop' || waitRaw !== undefined)) throw new Error(`${command} requires a task ID`);
|
|
104
|
+
return finish(taskId ? (command === 'stop' ? 'task_stop' : 'task_status') : 'status',
|
|
105
|
+
taskId ? { taskId, ...(waitRaw !== undefined ? { waitMs: parseWait(waitRaw) } : {}) } : {});
|
|
89
106
|
}
|
|
90
107
|
|
|
91
108
|
if (command === 'list-profiles') {
|
|
@@ -140,10 +157,13 @@ export function parseCliArgs(argv) {
|
|
|
140
157
|
if (command === 'run') {
|
|
141
158
|
const prompt = args.shift();
|
|
142
159
|
if (!prompt) throw new Error('run requires a prompt argument');
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const
|
|
160
|
+
const taskId = getFlag(args, '--task');
|
|
161
|
+
if (args.includes('--timeout')) throw new Error('run no longer accepts --timeout. Use --wait to wait locally, or stop <task-id> to cancel.');
|
|
162
|
+
if (taskId && ['--tab', '--group', '--group-title'].some(flag => args.includes(flag))) throw new Error('Use --task to continue its original browser context; omit tab/group selectors.');
|
|
163
|
+
const target = taskId ? { taskId } : requireTarget('run', args, getFlag);
|
|
164
|
+
const waitRaw = getFlag(args, '--wait');
|
|
165
|
+
const waitMs = waitRaw !== undefined ? parseWait(waitRaw) : 0;
|
|
166
|
+
const base = { prompt, ...(waitMs ? { waitMs } : {}) };
|
|
147
167
|
return finish('run', { ...base, ...target });
|
|
148
168
|
}
|
|
149
169
|
|
|
@@ -155,13 +175,13 @@ export function parseCliArgs(argv) {
|
|
|
155
175
|
if (command === 'panel-screenshot') {
|
|
156
176
|
const tabRaw = getFlag(args, '--tab');
|
|
157
177
|
if (!tabRaw) throw new Error('panel-screenshot requires --tab <tabId>');
|
|
158
|
-
const tabId =
|
|
178
|
+
const { id: tabId, ...target } = parseTarget(tabRaw, '--tab');
|
|
159
179
|
const output = getFlag(args, '-o') ?? getFlag(args, '--output') ?? null;
|
|
160
180
|
const widthRaw = getFlag(args, '--width');
|
|
161
181
|
const heightRaw = getFlag(args, '--height');
|
|
162
182
|
const width = widthRaw ? parseStrictInt(widthRaw, '--width') : undefined;
|
|
163
183
|
const height = heightRaw ? parseStrictInt(heightRaw, '--height') : undefined;
|
|
164
|
-
return finish('panel_screenshot', { tabId, ...(output ? { _output: output } : {}), ...(width ? { width } : {}), ...(height ? { height } : {}) });
|
|
184
|
+
return finish('panel_screenshot', { tabId, ...target, ...(output ? { _output: output } : {}), ...(width ? { width } : {}), ...(height ? { height } : {}) });
|
|
165
185
|
}
|
|
166
186
|
|
|
167
187
|
if (command === 'raw') {
|
|
@@ -173,14 +193,18 @@ export function parseCliArgs(argv) {
|
|
|
173
193
|
return finish('raw', cmd);
|
|
174
194
|
}
|
|
175
195
|
|
|
176
|
-
// Reason: Tool commands are extracted to tool-commands.mjs to keep parseCliArgs
|
|
177
|
-
// and dassi.mjs within CLAUDE.md size limits (40 lines / 500 lines).
|
|
178
196
|
const toolParams = parseToolCommand(command, args, getFlag, consumeFlag);
|
|
179
197
|
if (toolParams) {
|
|
198
|
+
// Reason: some tool commands resolve to dedicated bridge methods rather
|
|
199
|
+
// than tool_exec (open --window → open_tab).
|
|
200
|
+
if (toolParams.__bridgeAction) {
|
|
201
|
+
const { __bridgeAction, ...params } = toolParams;
|
|
202
|
+
return finish(__bridgeAction, params);
|
|
203
|
+
}
|
|
180
204
|
return finish('tool_exec', toolParams);
|
|
181
205
|
}
|
|
182
206
|
|
|
183
|
-
throw new Error(`Unknown command: "${command}". Run: dassi --help`);
|
|
207
|
+
throw new Error(`Unknown command: "${command}". Browser commands use dassi tools and dassi call. Run: dassi --help`);
|
|
184
208
|
}
|
|
185
209
|
|
|
186
210
|
/**
|
|
@@ -217,56 +241,74 @@ function consumeFlag(args, flag) {
|
|
|
217
241
|
|
|
218
242
|
// HELP_TEXT lives in ./help-text.mjs (extracted to keep this file within the size limit).
|
|
219
243
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
if (action === 'help') {
|
|
231
|
-
console.log(HELP_TEXT);
|
|
232
|
-
process.exit(0);
|
|
233
|
-
}
|
|
234
|
-
return false;
|
|
244
|
+
function handleImmediateAction({ action, params, json }) {
|
|
245
|
+
let text;
|
|
246
|
+
if (action === 'version') text = VERSION;
|
|
247
|
+
else if (action === 'help') text = HELP_TEXT;
|
|
248
|
+
else if (action === 'skill') {
|
|
249
|
+
const skillDir = path.join(__dirname, 'skills', 'dassi');
|
|
250
|
+
text = params.pathOnly ? skillDir : fs.readFileSync(path.join(skillDir, 'SKILL.md'), 'utf8');
|
|
251
|
+
} else return false;
|
|
252
|
+
console.log(json ? JSON.stringify({ success: true, data: text }) : text);
|
|
253
|
+
return true;
|
|
235
254
|
}
|
|
236
255
|
|
|
237
256
|
// ─── Entry point ──────────────────────────────────────────────────────────────
|
|
238
257
|
|
|
239
258
|
/**
|
|
240
|
-
*
|
|
241
|
-
* onboarding, sends command, prints response.
|
|
259
|
+
* Parses arguments, connects to the daemon, and prints the command result.
|
|
242
260
|
* @returns {Promise<void>}
|
|
243
261
|
*/
|
|
244
|
-
export async function run() {
|
|
262
|
+
export async function run(argv = process.argv.slice(2)) {
|
|
245
263
|
let parsed;
|
|
246
264
|
try {
|
|
247
|
-
parsed = parseCliArgs(process.
|
|
265
|
+
parsed = parseCliArgs(argv, { interactive: Boolean(process.stdin.isTTY && process.stdout.isTTY) });
|
|
266
|
+
await execute(parsed);
|
|
248
267
|
} catch (err) {
|
|
249
|
-
|
|
250
|
-
|
|
268
|
+
const args = argv.slice(0, argv.indexOf('--') === -1 ? argv.length : argv.indexOf('--'));
|
|
269
|
+
const json = parsed?.json ?? args.includes('--json');
|
|
270
|
+
const response = { success: false, error: err instanceof Error ? err.message : String(err), ...(err?.outcome === 'unknown' ? { outcome: 'unknown' } : {}) };
|
|
271
|
+
if (json) console.log(formatResponse('error', response, true));
|
|
272
|
+
else console.error(formatResponse('error', response, false));
|
|
273
|
+
process.exitCode = 1;
|
|
251
274
|
}
|
|
275
|
+
}
|
|
252
276
|
|
|
277
|
+
async function execute(parsed) {
|
|
253
278
|
const { action, params, session, json, profile } = parsed;
|
|
254
|
-
handleImmediateAction(
|
|
279
|
+
if (handleImmediateAction(parsed)) return;
|
|
280
|
+
if (action === 'remove_skill') {
|
|
281
|
+
const { unregisterSkills } = await import('./setup.mjs');
|
|
282
|
+
const skills = await unregisterSkills();
|
|
283
|
+
console.log(json ? JSON.stringify({ success: true, data: { skills } }) : [
|
|
284
|
+
...skills.map(skill => `${skill.status}: ${skill.path}`),
|
|
285
|
+
'To remove the CLI, run: npm uninstall -g @dassi_ai/cli',
|
|
286
|
+
].join('\n'));
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
if (action === 'setup') {
|
|
290
|
+
const { setup, formatSetup } = await import('./setup.mjs');
|
|
291
|
+
const data = await setup({ ...params, profile, session, interactive: !json && Boolean(process.stdin.isTTY) });
|
|
292
|
+
const response = { success: data.ready, data, ...(!data.ready ? { error: data.next } : {}) };
|
|
293
|
+
console.log(json ? JSON.stringify(response) : formatSetup(data));
|
|
294
|
+
if (!data.ready) process.exitCode = 1;
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
255
297
|
|
|
256
298
|
// Launch family (launch / --stop / __launch-hold) → launch.mjs.
|
|
257
299
|
if (await dispatchLaunch(action, params, { spawn: child_process.spawn, ensureDaemonRunning, getSocketPath, sendCommand, appDir: getAppDir(), launchesFile: getLaunchesFile() })) return;
|
|
258
300
|
|
|
259
301
|
const socketPath = await ensureDaemonReady(session);
|
|
260
302
|
|
|
261
|
-
const id = `cli_${
|
|
303
|
+
const id = `cli_${randomUUID()}`;
|
|
262
304
|
|
|
263
305
|
// Group expansion: if action=run or action=tool_exec with groupId/groupTitle, expand and fan out sequentially
|
|
264
306
|
if (
|
|
265
307
|
(action === 'run' || action === 'tool_exec') &&
|
|
266
308
|
(params.groupId !== undefined || params.groupTitle !== undefined)
|
|
267
309
|
) {
|
|
268
|
-
const allOk = await runWithGroupExpansion(socketPath, action, params, json,
|
|
269
|
-
if (!allOk) process.
|
|
310
|
+
const allOk = await runWithGroupExpansion(socketPath, action, params, json, sendAndWait, formatResponse, profile);
|
|
311
|
+
if (!allOk) process.exitCode = 1;
|
|
270
312
|
return;
|
|
271
313
|
}
|
|
272
314
|
|
|
@@ -275,10 +317,10 @@ export async function run() {
|
|
|
275
317
|
const command = action === 'raw'
|
|
276
318
|
? { id, ...params }
|
|
277
319
|
: { id, action, ...params, ...(profile ? { target: profile } : {}) };
|
|
278
|
-
const response = await
|
|
320
|
+
const response = await sendAndWait(socketPath, command);
|
|
279
321
|
|
|
280
322
|
console.log(formatResponse(action, response, json, params));
|
|
281
|
-
if (!response.success) process.
|
|
323
|
+
if (!response.success || (command.action !== 'task_stop' && ['failed', 'stopped'].includes(response.data?.status))) process.exitCode = 1;
|
|
282
324
|
}
|
|
283
325
|
|
|
284
326
|
// ── Entry point guard ─────────────────────────────────────────────────────────
|
|
@@ -309,8 +351,5 @@ export function isMainModule(metaUrl, argvPath) {
|
|
|
309
351
|
}
|
|
310
352
|
|
|
311
353
|
if (isMainModule(import.meta.url, process.argv[1])) {
|
|
312
|
-
run()
|
|
313
|
-
console.error(`❌ ${err.message}`);
|
|
314
|
-
process.exit(1);
|
|
315
|
-
});
|
|
354
|
+
await run();
|
|
316
355
|
}
|
package/format-response.mjs
CHANGED
|
@@ -8,13 +8,13 @@ import * as fs from 'fs';
|
|
|
8
8
|
/** @param {{ success: boolean; data?: unknown }} response */
|
|
9
9
|
function formatListTabs(response) {
|
|
10
10
|
const tabs = /** @type {Array<{tabId:number;title:string;url:string;active:boolean}>} */ (response.data ?? []);
|
|
11
|
-
if (tabs.length === 0) return '(no open tabs)';
|
|
12
|
-
const header = '
|
|
11
|
+
if (tabs.length === 0) return '(no open tabs — open Dassi in Chrome, then retry)';
|
|
12
|
+
const header = 'PROFILE TARGET TITLE / URL';
|
|
13
13
|
const rows = tabs.map((t) => {
|
|
14
14
|
const id = String(t.tabId).padEnd(7);
|
|
15
15
|
const prefix = t.active ? '* ' : ' ';
|
|
16
16
|
const title = (prefix + t.title).slice(0, 42).padEnd(42);
|
|
17
|
-
return `${id} ${title} ${t.url}`;
|
|
17
|
+
return `${(t.profile ?? '').padEnd(16)} ${(t.target ?? id).padEnd(48)} ${title} ${t.url}`;
|
|
18
18
|
});
|
|
19
19
|
return [header, ...rows].join('\n');
|
|
20
20
|
}
|
|
@@ -23,22 +23,27 @@ function formatListTabs(response) {
|
|
|
23
23
|
function formatListGroups(response) {
|
|
24
24
|
const groups = /** @type {Array<{id:number;title:string;color:string;windowId:number;tabCount:number}>} */ (response.data ?? []);
|
|
25
25
|
if (groups.length === 0) return '(no tab groups)';
|
|
26
|
-
const header = '
|
|
26
|
+
const header = 'PROFILE TARGET COLOR TABS WINDOW TITLE';
|
|
27
27
|
const rows = groups.map((g) => {
|
|
28
28
|
const id = String(g.id).padEnd(9);
|
|
29
29
|
const color = (g.color ?? '').padEnd(8);
|
|
30
30
|
const tabs = String(g.tabCount).padEnd(6);
|
|
31
31
|
const win = String(g.windowId).padEnd(8);
|
|
32
|
-
return `${id} ${color} ${tabs} ${win} ${g.title || '(untitled)'}`;
|
|
32
|
+
return `${(g.profile ?? '').padEnd(16)} ${(g.target ?? id).padEnd(48)} ${color} ${tabs} ${win} ${g.title || '(untitled)'}`;
|
|
33
33
|
});
|
|
34
34
|
return [header, ...rows].join('\n');
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
/** @param {{ success: boolean; data?: unknown }} response */
|
|
38
38
|
function formatRun(response) {
|
|
39
|
-
const d =
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
const d = response.data ?? {};
|
|
40
|
+
if (!d.taskId) return `${d.answer ?? ''}\n(${d.toolCalls ?? 0} tool calls, ${((d.durationMs ?? 0) / 1000).toFixed(1)}s)`;
|
|
41
|
+
const status = d.status[0].toUpperCase() + d.status.slice(1);
|
|
42
|
+
const lines = [`${status} · ${Math.floor((d.durationMs ?? 0) / 60000)} minutes · ${d.profile ?? ''}`, `Task: ${d.taskId}`];
|
|
43
|
+
if (d.answer) lines.push(d.answer);
|
|
44
|
+
if (d.reason) lines.push(d.reason);
|
|
45
|
+
if (d.status === 'running' || d.status === 'stopping') lines.push(`Check: dassi status '${d.taskId}'`, `Stop: dassi stop '${d.taskId}'`);
|
|
46
|
+
return lines.join('\n');
|
|
42
47
|
}
|
|
43
48
|
|
|
44
49
|
/**
|
|
@@ -99,13 +104,14 @@ function formatExportLogs(response, params) {
|
|
|
99
104
|
function formatListProfiles(response) {
|
|
100
105
|
const rows = /** @type {Array<{label:string;port:number}>} */ (response.data ?? []);
|
|
101
106
|
if (rows.length === 0) return '(no profiles connected)';
|
|
102
|
-
const header = 'PROFILE
|
|
103
|
-
const body = rows.map((r) => `${String(r.label).padEnd(
|
|
107
|
+
const header = 'PROFILE ID STATUS';
|
|
108
|
+
const body = rows.map((r) => `${String(r.label).padEnd(16)} ${String(r.id ?? r.port).padEnd(37)} ${r.error ?? (r.authenticated ? 'Signed in' : 'Sign in required')}`);
|
|
104
109
|
return [header, ...body].join('\n');
|
|
105
110
|
}
|
|
106
111
|
|
|
107
112
|
/** @param {{ success: boolean; data?: unknown }} response */
|
|
108
113
|
function formatStatus(response) {
|
|
114
|
+
if (Array.isArray(response.data)) return formatListProfiles(response);
|
|
109
115
|
const d = /** @type {{ authenticated?: boolean; email?: string | null }} */ (response.data ?? {});
|
|
110
116
|
if (d.authenticated) {
|
|
111
117
|
return `✓ Signed in as ${d.email}`;
|
|
@@ -119,6 +125,9 @@ const FORMATTERS = {
|
|
|
119
125
|
list_tabs: (r, _p, _a) => formatListTabs(r),
|
|
120
126
|
list_groups: (r, _p, _a) => formatListGroups(r),
|
|
121
127
|
run: (r, _p, _a) => formatRun(r),
|
|
128
|
+
task_status: (r) => formatRun(r),
|
|
129
|
+
task_stop: (r) => formatRun(r),
|
|
130
|
+
list_tools: (r) => Array.isArray(r.data) ? r.data.map(t => `${t.name} ${t.description}`).join('\n') : JSON.stringify(r.data, null, 2),
|
|
122
131
|
tool_exec: (r, p, a) => formatToolExec(r, p, a),
|
|
123
132
|
panel_screenshot: (r, p, a) => formatToolExec(r, p, a),
|
|
124
133
|
export_logs: (r, p, _a) => formatExportLogs(r, p),
|
|
@@ -138,7 +147,13 @@ export function formatResponse(action, response, rawJson, params) {
|
|
|
138
147
|
if (rawJson) return JSON.stringify(response);
|
|
139
148
|
if (!response.success) return `❌ Error: ${response.error ?? 'Unknown error'}`;
|
|
140
149
|
const formatter = FORMATTERS[action];
|
|
141
|
-
if (formatter)
|
|
150
|
+
if (formatter) {
|
|
151
|
+
const output = formatter(response, params, action);
|
|
152
|
+
const errors = ['list_tabs', 'list_groups'].includes(action)
|
|
153
|
+
? (response.profiles ?? []).flatMap(p => [p.error, p.authError].filter(Boolean).map(error => `${p.label} (${p.id}): ${error}`))
|
|
154
|
+
: [];
|
|
155
|
+
return [output, ...errors].join('\n');
|
|
156
|
+
}
|
|
142
157
|
// Default: pretty-print data
|
|
143
158
|
return JSON.stringify(response.data, null, 2);
|
|
144
159
|
}
|
package/group-expansion.mjs
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import * as path from 'path';
|
|
8
|
+
import { randomUUID } from 'node:crypto';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Inject `tabId` into a filename before the extension so multi-tab fan-out
|
|
@@ -52,8 +53,9 @@ export async function expandGroupToTabIds(socketPath, ref, sendFn, target = null
|
|
|
52
53
|
// `{ id, action, ...rest } = cmd` and forwards `rest` as JSON-RPC params,
|
|
53
54
|
// so nesting under `params` would arrive at the extension as
|
|
54
55
|
// `params.params.all` — and the filter would NOT bypass.
|
|
55
|
-
const groupsResp = await sendFn(socketPath, { id: `cli_lg_${
|
|
56
|
+
const groupsResp = await sendFn(socketPath, { id: `cli_lg_${randomUUID()}`, action: 'list_groups', all: true, ...targetField });
|
|
56
57
|
if (!groupsResp.success) throw new Error(`Failed to list groups: ${groupsResp.error ?? 'unknown'}`);
|
|
58
|
+
if (!target && groupsResp.profiles?.length > 1) throw new Error('Choose a profile with --profile <id>, or copy a group target from dassi list-groups.');
|
|
57
59
|
const groups = /** @type {Array<{id:number;title:string;windowId:number}>} */ (groupsResp.data ?? []);
|
|
58
60
|
|
|
59
61
|
let groupId;
|
|
@@ -74,7 +76,7 @@ export async function expandGroupToTabIds(socketPath, ref, sendFn, target = null
|
|
|
74
76
|
throw new Error('expandGroupToTabIds: pass groupId or groupTitle');
|
|
75
77
|
}
|
|
76
78
|
|
|
77
|
-
const tabsResp = await sendFn(socketPath, { id: `cli_lt_${
|
|
79
|
+
const tabsResp = await sendFn(socketPath, { id: `cli_lt_${randomUUID()}`, action: 'list_tabs', all: true, ...targetField });
|
|
78
80
|
if (!tabsResp.success) throw new Error(`Failed to list tabs: ${tabsResp.error ?? 'unknown'}`);
|
|
79
81
|
const tabs = /** @type {Array<{tabId:number;groupId:number}>} */ (tabsResp.data ?? []);
|
|
80
82
|
const memberIds = tabs.filter((t) => t.groupId === groupId).map((t) => t.tabId);
|
|
@@ -113,7 +115,7 @@ async function dispatchChildForTab(socketPath, action, childBase, tabId, multiTa
|
|
|
113
115
|
multiTab && childBase._output ? uniquifyOutputForTab(childBase._output, tabId) : childBase._output;
|
|
114
116
|
const childParams = { ...childBase, tabId, ...(perTabOutput !== childBase._output ? { _output: perTabOutput } : {}) };
|
|
115
117
|
// Reason: include target only when set so the fan-out routes to the chosen profile.
|
|
116
|
-
const response = await sendFn(socketPath, { id: `cli_${
|
|
118
|
+
const response = await sendFn(socketPath, { id: `cli_${randomUUID()}`, action, ...childParams, ...(target ? { target } : {}) });
|
|
117
119
|
return { tabId, response, childParams };
|
|
118
120
|
}
|
|
119
121
|
|
|
@@ -144,6 +146,13 @@ export async function runWithGroupExpansion(socketPath, action, params, json, se
|
|
|
144
146
|
sendFn,
|
|
145
147
|
target,
|
|
146
148
|
);
|
|
149
|
+
// One group is one conversation: submit an agent prompt once for the whole workspace.
|
|
150
|
+
if (action === 'run') {
|
|
151
|
+
const { groupId: _group, groupTitle: _title, ...base } = params;
|
|
152
|
+
const { response } = await dispatchChildForTab(socketPath, action, base, tabIds[0], false, sendFn, target);
|
|
153
|
+
console.log(formatFn(action, response, json, base));
|
|
154
|
+
return response.success && !['failed', 'stopped'].includes(response.data?.status);
|
|
155
|
+
}
|
|
147
156
|
// Reason: always on stderr so it doesn't pollute JSON output on stdout
|
|
148
157
|
console.error(`Running on ${tabIds.length} tab${tabIds.length === 1 ? '' : 's'}: ${tabIds.join(', ')}`);
|
|
149
158
|
// Strip group fields; substitute tabId per child call
|
package/help-text.mjs
CHANGED
|
@@ -1,48 +1,59 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
1
|
+
export const HELP_TEXT = `Usage: dassi [command] [options]
|
|
2
|
+
|
|
3
|
+
Start here:
|
|
4
|
+
dassi Start setup in an interactive terminal
|
|
5
|
+
setup Install agent skills and connect your Chrome profile
|
|
6
|
+
skill Read the local-agent workflow (no browser needed)
|
|
7
|
+
list-tabs Show tabs across connected profiles; copy a TARGET
|
|
8
|
+
run <prompt> --tab <target> Start a new task and return its ID
|
|
9
|
+
run <prompt> --task <task-id> Continue that conversation (queues a follow-up if busy)
|
|
10
|
+
status <task-id> Show progress or the finished result
|
|
11
|
+
stop <task-id> Stop that task
|
|
12
|
+
|
|
13
|
+
Options:
|
|
14
|
+
--wait <duration> Wait for a result (30s, 10m, 1h); never stops the task
|
|
15
|
+
--profile <id-or-name> Choose a profile for unscoped tab/group IDs
|
|
16
|
+
--json Machine-readable output
|
|
17
|
+
--help Show help
|
|
18
|
+
--version Show version
|
|
19
|
+
|
|
20
|
+
Agent setup:
|
|
21
|
+
setup [--profile id] Resume setup for the chosen profile
|
|
22
|
+
setup --wait 0 --json Check setup immediately; report any user action needed
|
|
23
|
+
setup --no-open Print browser instructions without opening Chrome
|
|
24
|
+
skill --path Print the bundled skill directory for installation
|
|
25
|
+
skill --remove Remove Dassi's registered agent skill links
|
|
26
|
+
|
|
27
|
+
Update / remove:
|
|
28
|
+
npm install -g @dassi_ai/cli@latest Update the CLI and its bundled agent skill
|
|
29
|
+
dassi skill --remove Unregister the agent skill
|
|
30
|
+
npm uninstall -g @dassi_ai/cli Remove the npm package
|
|
31
|
+
|
|
32
|
+
Browser tools:
|
|
33
|
+
tools [name] --tab <target> Discover current tools or read one tool's schema
|
|
34
|
+
call <name> --tab <target> Execute a discovered tool with --args '<json>'
|
|
35
|
+
call ... [-o file] Save returned images (omit --json to write files)
|
|
36
|
+
panel-screenshot [-o file] Capture Dassi (--tab, --width, --height)
|
|
37
|
+
open [url] --window <id> Create an ungrouped tab (--foreground to activate)
|
|
38
|
+
|
|
39
|
+
Other commands:
|
|
40
|
+
list-profiles Show profile IDs, labels, and sign-in status
|
|
41
|
+
list-groups Show groups with profile-bound targets
|
|
42
|
+
status Show connected profiles and their status
|
|
43
|
+
bug-report [-o file] Export diagnostic logs
|
|
44
|
+
raw <json> Send a bridge command
|
|
45
|
+
|
|
46
|
+
Groups:
|
|
47
|
+
run <prompt> --group <target> Start one task for the whole group
|
|
48
|
+
--group-title <name> Select a group by name (use --profile if needed)
|
|
49
|
+
call also accepts group targets and executes once per member tab.
|
|
50
|
+
|
|
51
|
+
Development:
|
|
52
|
+
launch [--label name] [--dist path] [--chrome path]
|
|
53
|
+
[--load-mode auto|pipe|flag] [-- <chrome args>]
|
|
54
|
+
launch --stop [label] | --stop-all
|
|
55
|
+
|
|
56
|
+
Browser aliases such as read-page and click have been replaced by tools and call.
|
|
57
|
+
run rejects the old --timeout flag. Use --wait for local waiting; stop to cancel.
|
|
58
|
+
A wait ending or terminal disconnecting does not cancel browser work.
|
|
59
|
+
`;
|
package/launch.mjs
CHANGED
|
@@ -255,7 +255,7 @@ export async function handleLaunch(opts, deps) {
|
|
|
255
255
|
if (pid === null) return exit(1);
|
|
256
256
|
|
|
257
257
|
recordLaunch(launchesFile, { label: opts.label, pid, profileDir, mode: useFlag ? 'flag' : 'pipe', startedAt: Date.now() });
|
|
258
|
-
log(`✅ launched "${opts.label}" (pid ${pid}) —
|
|
258
|
+
log(`✅ launched "${opts.label}" (pid ${pid}) — find targets with: dassi list-tabs --profile ${opts.label}`);
|
|
259
259
|
}
|
|
260
260
|
|
|
261
261
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dassi_ai/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "CLI for the Dassi Chrome extension \u2014 run browser automation from the terminal",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"group-expansion.mjs",
|
|
17
17
|
"launch.mjs",
|
|
18
18
|
"_launch-pipe.mjs",
|
|
19
|
+
"setup.mjs",
|
|
19
20
|
"help-text.mjs",
|
|
20
21
|
".claude-plugin/",
|
|
21
22
|
"skills/",
|
|
@@ -32,7 +33,6 @@
|
|
|
32
33
|
"email": "team@dassi.ai"
|
|
33
34
|
},
|
|
34
35
|
"dependencies": {
|
|
35
|
-
"open": "^10.1.0",
|
|
36
36
|
"ws": "^8.18.0"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|