@dench.com/cli 2.1.1 → 2.1.3
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/agent-config.ts +3 -126
- package/browser.ts +507 -0
- package/chat.ts +1 -1
- package/crm.ts +209 -0
- package/dench.ts +173 -49
- package/fs-daemon.ts +201 -2
- package/lib/api-schemas.ts +1095 -0
- package/lib/command-registry.ts +1816 -0
- package/lib/enrichment-gateway.ts +23 -12
- package/lib/organizationSelector.ts +58 -0
- package/linkedin.ts +210 -0
- package/package.json +6 -2
package/agent-config.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `dench identity / organisation / user /
|
|
3
|
-
*
|
|
2
|
+
* `dench identity / organisation / user / bootstrap / model / daily /
|
|
3
|
+
* mem aggregate` — CLI surface for the OpenClaw-
|
|
4
4
|
* parity self-updating harness.
|
|
5
5
|
*
|
|
6
6
|
* Mirrors the auth pattern in `cli/cron.ts`:
|
|
@@ -17,14 +17,12 @@
|
|
|
17
17
|
* user show | edit
|
|
18
18
|
* tools show | notes-edit
|
|
19
19
|
* memory show | append <text> | set (MEMORY.md aggregate)
|
|
20
|
-
* heartbeat status | enable | disable | interval <duration> |
|
|
21
|
-
* instructions
|
|
22
20
|
* bootstrap status | complete | reopen | template
|
|
23
21
|
* model default <id> | model default clear |
|
|
24
22
|
* model thread <threadId> <id> | model thread <threadId> clear
|
|
25
23
|
* daily today | daily show <YYYY-MM-DD> | daily append <text>
|
|
26
24
|
*
|
|
27
|
-
* `edit` / `notes-edit` / `
|
|
25
|
+
* `edit` / `notes-edit` / `template` open the
|
|
28
26
|
* user's `$EDITOR` (default `vi`) to capture the new body. The
|
|
29
27
|
* stdin path is also supported via `--stdin`, useful in scripts.
|
|
30
28
|
*/
|
|
@@ -60,9 +58,6 @@ const api = {
|
|
|
60
58
|
updateUserProfile: makeFunctionReference<"mutation">(
|
|
61
59
|
"functions/agentConfig:updateUserProfile",
|
|
62
60
|
),
|
|
63
|
-
updateHeartbeat: makeFunctionReference<"mutation">(
|
|
64
|
-
"functions/agentConfig:updateHeartbeat",
|
|
65
|
-
),
|
|
66
61
|
updateBootstrapTemplate: makeFunctionReference<"mutation">(
|
|
67
62
|
"functions/agentConfig:updateBootstrapTemplate",
|
|
68
63
|
),
|
|
@@ -182,12 +177,6 @@ async function loadAgentConfig(ctx: CliCtx): Promise<{
|
|
|
182
177
|
completedAt?: number;
|
|
183
178
|
template?: string;
|
|
184
179
|
};
|
|
185
|
-
heartbeat: {
|
|
186
|
-
enabled: boolean;
|
|
187
|
-
intervalMs: number;
|
|
188
|
-
instructions: string;
|
|
189
|
-
lastFiredAt?: number;
|
|
190
|
-
};
|
|
191
180
|
};
|
|
192
181
|
userProfile: { content: string; updatedAt: number } | null;
|
|
193
182
|
organizationId: string;
|
|
@@ -206,12 +195,6 @@ async function loadAgentConfig(ctx: CliCtx): Promise<{
|
|
|
206
195
|
completedAt?: number;
|
|
207
196
|
template?: string;
|
|
208
197
|
};
|
|
209
|
-
heartbeat: {
|
|
210
|
-
enabled: boolean;
|
|
211
|
-
intervalMs: number;
|
|
212
|
-
instructions: string;
|
|
213
|
-
lastFiredAt?: number;
|
|
214
|
-
};
|
|
215
198
|
};
|
|
216
199
|
userProfile: { content: string; updatedAt: number } | null;
|
|
217
200
|
organizationId: string;
|
|
@@ -306,106 +289,6 @@ async function handleSimpleMagicFile(
|
|
|
306
289
|
);
|
|
307
290
|
}
|
|
308
291
|
|
|
309
|
-
function formatInterval(ms: number): string {
|
|
310
|
-
if (ms < 60_000) return `${Math.round(ms / 1000)}s`;
|
|
311
|
-
if (ms < 60 * 60_000) return `${Math.round(ms / 60_000)}m`;
|
|
312
|
-
if (ms < 24 * 60 * 60_000) {
|
|
313
|
-
const hours = ms / (60 * 60_000);
|
|
314
|
-
return Number.isInteger(hours) ? `${hours}h` : `${hours.toFixed(1)}h`;
|
|
315
|
-
}
|
|
316
|
-
return `${(ms / (24 * 60 * 60_000)).toFixed(1)}d`;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
const DURATION_RE = /^(\d+)\s*(ms|s|m|h|d)$/i;
|
|
320
|
-
function parseDurationFlagToMs(input: string): number | null {
|
|
321
|
-
const m = DURATION_RE.exec(input.trim());
|
|
322
|
-
if (!m) return null;
|
|
323
|
-
const n = parseInt(m[1], 10);
|
|
324
|
-
if (!Number.isFinite(n) || n <= 0) return null;
|
|
325
|
-
switch (m[2].toLowerCase()) {
|
|
326
|
-
case "ms":
|
|
327
|
-
return n;
|
|
328
|
-
case "s":
|
|
329
|
-
return n * 1000;
|
|
330
|
-
case "m":
|
|
331
|
-
return n * 60_000;
|
|
332
|
-
case "h":
|
|
333
|
-
return n * 3_600_000;
|
|
334
|
-
case "d":
|
|
335
|
-
return n * 86_400_000;
|
|
336
|
-
}
|
|
337
|
-
return null;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
async function handleHeartbeat(ctx: CliCtx, args: string[]): Promise<void> {
|
|
341
|
-
const sub = args[0];
|
|
342
|
-
if (!sub || sub === "status" || sub === "show") {
|
|
343
|
-
const config = await loadAgentConfig(ctx);
|
|
344
|
-
const hb = config.agentConfig.heartbeat;
|
|
345
|
-
if (ctx.jsonOutput) {
|
|
346
|
-
out(ctx, hb);
|
|
347
|
-
return;
|
|
348
|
-
}
|
|
349
|
-
out(
|
|
350
|
-
ctx,
|
|
351
|
-
`enabled=${hb.enabled} interval=${formatInterval(hb.intervalMs)}\n\n${hb.instructions}`,
|
|
352
|
-
);
|
|
353
|
-
return;
|
|
354
|
-
}
|
|
355
|
-
if (sub === "enable") {
|
|
356
|
-
await runMutation(ctx, api.functions.agentConfig.updateHeartbeat, {
|
|
357
|
-
enabled: true,
|
|
358
|
-
});
|
|
359
|
-
out(ctx, { ok: true, enabled: true });
|
|
360
|
-
return;
|
|
361
|
-
}
|
|
362
|
-
if (sub === "disable") {
|
|
363
|
-
await runMutation(ctx, api.functions.agentConfig.updateHeartbeat, {
|
|
364
|
-
enabled: false,
|
|
365
|
-
});
|
|
366
|
-
out(ctx, { ok: true, enabled: false });
|
|
367
|
-
return;
|
|
368
|
-
}
|
|
369
|
-
if (sub === "interval") {
|
|
370
|
-
const raw = args[1];
|
|
371
|
-
if (!raw) {
|
|
372
|
-
throw new AgentConfigCliError(
|
|
373
|
-
"Usage: dench heartbeat interval <duration> (e.g. 30m, 2h, 1d)",
|
|
374
|
-
);
|
|
375
|
-
}
|
|
376
|
-
const ms = parseDurationFlagToMs(raw);
|
|
377
|
-
if (!ms) throw new AgentConfigCliError(`Invalid duration: ${raw}`);
|
|
378
|
-
await runMutation(ctx, api.functions.agentConfig.updateHeartbeat, {
|
|
379
|
-
intervalMs: ms,
|
|
380
|
-
});
|
|
381
|
-
out(ctx, { ok: true, intervalMs: ms, label: formatInterval(ms) });
|
|
382
|
-
return;
|
|
383
|
-
}
|
|
384
|
-
if (sub === "instructions" || sub === "edit") {
|
|
385
|
-
let content: string;
|
|
386
|
-
if (hasFlag(args, "--stdin") || getFlag(args, "--content")) {
|
|
387
|
-
content = await resolveContent(args);
|
|
388
|
-
} else {
|
|
389
|
-
const config = await loadAgentConfig(ctx);
|
|
390
|
-
content = openEditor(
|
|
391
|
-
config.agentConfig.heartbeat.instructions,
|
|
392
|
-
"heartbeat",
|
|
393
|
-
);
|
|
394
|
-
}
|
|
395
|
-
if (!content.trim().length) {
|
|
396
|
-
throw new AgentConfigCliError("heartbeat instructions cannot be empty");
|
|
397
|
-
}
|
|
398
|
-
await runMutation(ctx, api.functions.agentConfig.updateHeartbeat, {
|
|
399
|
-
instructions: content,
|
|
400
|
-
});
|
|
401
|
-
out(ctx, { ok: true, kind: "heartbeat" });
|
|
402
|
-
return;
|
|
403
|
-
}
|
|
404
|
-
throw new AgentConfigCliError(
|
|
405
|
-
`Unknown heartbeat subcommand: "${sub}". Try "status", "enable", "disable", "interval <duration>", "instructions".`,
|
|
406
|
-
);
|
|
407
|
-
}
|
|
408
|
-
|
|
409
292
|
async function handleBootstrap(ctx: CliCtx, args: string[]): Promise<void> {
|
|
410
293
|
const sub = args[0];
|
|
411
294
|
if (!sub || sub === "status" || sub === "show") {
|
|
@@ -598,10 +481,6 @@ export async function runMemoryAggregateCommand(ctx: CliCtx): Promise<void> {
|
|
|
598
481
|
);
|
|
599
482
|
}
|
|
600
483
|
|
|
601
|
-
export async function runHeartbeatCommand(ctx: CliCtx): Promise<void> {
|
|
602
|
-
await handleHeartbeat(ctx, ctx.args);
|
|
603
|
-
}
|
|
604
|
-
|
|
605
484
|
export async function runBootstrapCommand(ctx: CliCtx): Promise<void> {
|
|
606
485
|
await handleBootstrap(ctx, ctx.args);
|
|
607
486
|
}
|
|
@@ -666,8 +545,6 @@ export function printAgentConfigCliHelp(): void {
|
|
|
666
545
|
dench user show | edit USER.md (per-member)
|
|
667
546
|
dench tools show | notes-edit TOOLS.md notes section
|
|
668
547
|
dench mem show | append <text> | set MEMORY.md aggregate
|
|
669
|
-
dench heartbeat status | enable | disable Heartbeat cron
|
|
670
|
-
| interval <30m|1h|…> | instructions
|
|
671
548
|
dench bootstrap status | complete | reopen | template
|
|
672
549
|
dench model default <id|clear> | model thread <threadId> <id|clear>
|
|
673
550
|
dench daily today | show <YYYY-MM-DD> Activity log paths
|
package/browser.ts
ADDED
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `dench browser` — CLI control for the cloud Browserbase browser.
|
|
3
|
+
*
|
|
4
|
+
* The CLI does not launch Chrome locally and never receives Browserbase
|
|
5
|
+
* credentials. It sends a bearer-authenticated JSON command to
|
|
6
|
+
* /api/browser/cli, where Dench Cloud drives Browserbase server-side.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { createHash } from "node:crypto";
|
|
10
|
+
import { mkdir, readFile, unlink, writeFile } from "node:fs/promises";
|
|
11
|
+
import { homedir } from "node:os";
|
|
12
|
+
import { dirname, join } from "node:path";
|
|
13
|
+
import { hasFlag } from "./lib/cli-args";
|
|
14
|
+
|
|
15
|
+
type RuntimeBundle = {
|
|
16
|
+
host: string;
|
|
17
|
+
bearerToken: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
type BrowserCliContext = {
|
|
21
|
+
runtime: RuntimeBundle;
|
|
22
|
+
args: string[];
|
|
23
|
+
jsonOutput: boolean;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type BrowserState = {
|
|
27
|
+
sessions?: Record<
|
|
28
|
+
string,
|
|
29
|
+
{
|
|
30
|
+
threadId: string;
|
|
31
|
+
updatedAt: number;
|
|
32
|
+
}
|
|
33
|
+
>;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
type BrowserResponse = Record<string, unknown> & {
|
|
37
|
+
action?: string;
|
|
38
|
+
currentUrl?: string | null;
|
|
39
|
+
error?: string;
|
|
40
|
+
hint?: string;
|
|
41
|
+
ok?: boolean;
|
|
42
|
+
pageTitle?: string | null;
|
|
43
|
+
screenshot?: { data?: string; mimeType?: string };
|
|
44
|
+
text?: string | null;
|
|
45
|
+
threadId?: string;
|
|
46
|
+
threadUrl?: string;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
class BrowserCliError extends Error {}
|
|
50
|
+
|
|
51
|
+
const ACTIONS = new Set([
|
|
52
|
+
"open",
|
|
53
|
+
"state",
|
|
54
|
+
"inspect",
|
|
55
|
+
"navigate",
|
|
56
|
+
"click",
|
|
57
|
+
"clickAt",
|
|
58
|
+
"fill",
|
|
59
|
+
"type",
|
|
60
|
+
"key",
|
|
61
|
+
"scroll",
|
|
62
|
+
"wait",
|
|
63
|
+
"evaluate",
|
|
64
|
+
"tabs",
|
|
65
|
+
"newTab",
|
|
66
|
+
"activateTab",
|
|
67
|
+
"closeTab",
|
|
68
|
+
"screenshot",
|
|
69
|
+
"stop",
|
|
70
|
+
]);
|
|
71
|
+
|
|
72
|
+
function browserHelp(): void {
|
|
73
|
+
console.log(`Usage: dench browser <action> [options]
|
|
74
|
+
|
|
75
|
+
Control the cloud Browserbase browser without opening dench.com locally.
|
|
76
|
+
Commands authenticate with the active dench signin session or DENCH_API_KEY.
|
|
77
|
+
|
|
78
|
+
Common:
|
|
79
|
+
dench browser open [url] [--json]
|
|
80
|
+
dench browser inspect [--json]
|
|
81
|
+
dench browser navigate <url>
|
|
82
|
+
dench browser click --selector <css>
|
|
83
|
+
dench browser click --text "Button text"
|
|
84
|
+
dench browser click-at <x> <y>
|
|
85
|
+
dench browser fill --label "Email" --value "me@example.com"
|
|
86
|
+
dench browser type "text for focused field"
|
|
87
|
+
dench browser key Enter
|
|
88
|
+
dench browser scroll [up|down] [--pixels N]
|
|
89
|
+
dench browser wait --text "Done" [--timeout-ms N]
|
|
90
|
+
dench browser evaluate "return document.title"
|
|
91
|
+
dench browser screenshot [--output page.jpg] [--full-page]
|
|
92
|
+
dench browser tabs
|
|
93
|
+
dench browser new-tab <url>
|
|
94
|
+
dench browser activate-tab <targetId>
|
|
95
|
+
dench browser close-tab <targetId>
|
|
96
|
+
dench browser stop
|
|
97
|
+
|
|
98
|
+
State:
|
|
99
|
+
The CLI remembers the latest browser thread per Dench host/session.
|
|
100
|
+
Use --thread <threadId> to control a specific thread, or
|
|
101
|
+
dench browser reset to forget the local remembered thread.
|
|
102
|
+
`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function consumeFlagValue(args: string[], name: string): string | undefined {
|
|
106
|
+
const idx = args.indexOf(name);
|
|
107
|
+
if (idx === -1) return undefined;
|
|
108
|
+
const value = args[idx + 1];
|
|
109
|
+
args.splice(idx, 2);
|
|
110
|
+
return value;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function consumeFlag(args: string[], name: string): boolean {
|
|
114
|
+
const idx = args.indexOf(name);
|
|
115
|
+
if (idx === -1) return false;
|
|
116
|
+
args.splice(idx, 1);
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function parseNumberFlag(args: string[], name: string): number | undefined {
|
|
121
|
+
const raw = consumeFlagValue(args, name);
|
|
122
|
+
if (raw === undefined) return undefined;
|
|
123
|
+
const value = Number(raw);
|
|
124
|
+
if (!Number.isFinite(value)) {
|
|
125
|
+
throw new BrowserCliError(`Invalid ${name} value: ${raw}`);
|
|
126
|
+
}
|
|
127
|
+
return value;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function statePath(): string {
|
|
131
|
+
return (
|
|
132
|
+
process.env.DENCH_BROWSER_STATE_PATH?.trim() ||
|
|
133
|
+
join(homedir(), ".dench", "browser-state.json")
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function stateKey(runtime: RuntimeBundle): string {
|
|
138
|
+
const tokenHash = createHash("sha256")
|
|
139
|
+
.update(runtime.bearerToken)
|
|
140
|
+
.digest("hex")
|
|
141
|
+
.slice(0, 24);
|
|
142
|
+
return `${runtime.host.replace(/\/+$/, "")}#${tokenHash}`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function readState(): Promise<BrowserState> {
|
|
146
|
+
try {
|
|
147
|
+
const raw = await readFile(statePath(), "utf8");
|
|
148
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
149
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
150
|
+
return {};
|
|
151
|
+
}
|
|
152
|
+
return parsed as BrowserState;
|
|
153
|
+
} catch {
|
|
154
|
+
return {};
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function writeState(state: BrowserState): Promise<void> {
|
|
159
|
+
const target = statePath();
|
|
160
|
+
await mkdir(dirname(target), { recursive: true });
|
|
161
|
+
await writeFile(target, JSON.stringify(state, null, 2), "utf8");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function readRememberedThreadId(
|
|
165
|
+
runtime: RuntimeBundle,
|
|
166
|
+
): Promise<string | undefined> {
|
|
167
|
+
const state = await readState();
|
|
168
|
+
const session = state.sessions?.[stateKey(runtime)];
|
|
169
|
+
return session?.threadId;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function rememberThreadId(
|
|
173
|
+
runtime: RuntimeBundle,
|
|
174
|
+
threadId: string,
|
|
175
|
+
): Promise<void> {
|
|
176
|
+
const state = await readState();
|
|
177
|
+
await writeState({
|
|
178
|
+
...state,
|
|
179
|
+
sessions: {
|
|
180
|
+
...(state.sessions ?? {}),
|
|
181
|
+
[stateKey(runtime)]: { threadId, updatedAt: Date.now() },
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function forgetThreadId(runtime: RuntimeBundle): Promise<boolean> {
|
|
187
|
+
const state = await readState();
|
|
188
|
+
const sessions = { ...(state.sessions ?? {}) };
|
|
189
|
+
const key = stateKey(runtime);
|
|
190
|
+
const existed = Boolean(sessions[key]);
|
|
191
|
+
delete sessions[key];
|
|
192
|
+
if (Object.keys(sessions).length === 0) {
|
|
193
|
+
try {
|
|
194
|
+
await unlink(statePath());
|
|
195
|
+
} catch {
|
|
196
|
+
// Ignore a concurrent/manual delete.
|
|
197
|
+
}
|
|
198
|
+
return existed;
|
|
199
|
+
}
|
|
200
|
+
await writeState({ ...state, sessions });
|
|
201
|
+
return existed;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function normalizeBrowserUrl(input: string | undefined): string | undefined {
|
|
205
|
+
const value = input?.trim();
|
|
206
|
+
if (!value) return undefined;
|
|
207
|
+
if (/^https?:\/\//i.test(value)) return value;
|
|
208
|
+
if (!/\s/.test(value) && value.includes(".")) return `https://${value}`;
|
|
209
|
+
return `https://www.google.com/search?q=${encodeURIComponent(value)}`;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function buildCliBrowserUrl(host: string): string {
|
|
213
|
+
return `${host.replace(/\/+$/, "")}/api/browser/cli`;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function postJson(
|
|
217
|
+
url: string,
|
|
218
|
+
bearer: string,
|
|
219
|
+
body: unknown,
|
|
220
|
+
): Promise<{ ok: boolean; payload: unknown; status: number }> {
|
|
221
|
+
const response = await fetch(url, {
|
|
222
|
+
method: "POST",
|
|
223
|
+
headers: {
|
|
224
|
+
accept: "application/json",
|
|
225
|
+
authorization: `Bearer ${bearer}`,
|
|
226
|
+
"content-type": "application/json",
|
|
227
|
+
},
|
|
228
|
+
body: JSON.stringify(body),
|
|
229
|
+
});
|
|
230
|
+
const text = await response.text();
|
|
231
|
+
let payload: unknown;
|
|
232
|
+
try {
|
|
233
|
+
payload = text ? JSON.parse(text) : null;
|
|
234
|
+
} catch {
|
|
235
|
+
payload = { raw: text };
|
|
236
|
+
}
|
|
237
|
+
return { ok: response.ok, payload, status: response.status };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function payloadError(payload: unknown): string {
|
|
241
|
+
if (payload && typeof payload === "object" && !Array.isArray(payload)) {
|
|
242
|
+
const error = (payload as { error?: unknown }).error;
|
|
243
|
+
if (typeof error === "string" && error.trim()) return error;
|
|
244
|
+
}
|
|
245
|
+
return "Cloud browser request failed";
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async function maybeWriteScreenshot(
|
|
249
|
+
response: BrowserResponse,
|
|
250
|
+
outputPath: string | undefined,
|
|
251
|
+
): Promise<string | undefined> {
|
|
252
|
+
const data = response.screenshot?.data;
|
|
253
|
+
if (!outputPath || typeof data !== "string") return undefined;
|
|
254
|
+
await writeFile(outputPath, Buffer.from(data, "base64"));
|
|
255
|
+
return outputPath;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function formatInspectElements(value: unknown): string[] {
|
|
259
|
+
if (!Array.isArray(value)) return [];
|
|
260
|
+
return value.slice(0, 10).flatMap((item, index) => {
|
|
261
|
+
if (!item || typeof item !== "object") return [];
|
|
262
|
+
const record = item as {
|
|
263
|
+
selector?: unknown;
|
|
264
|
+
tagName?: unknown;
|
|
265
|
+
text?: unknown;
|
|
266
|
+
};
|
|
267
|
+
const label =
|
|
268
|
+
typeof record.text === "string" && record.text.trim()
|
|
269
|
+
? ` ${record.text.trim()}`
|
|
270
|
+
: "";
|
|
271
|
+
const selector =
|
|
272
|
+
typeof record.selector === "string" ? ` ${record.selector}` : "";
|
|
273
|
+
const tag = typeof record.tagName === "string" ? record.tagName : "el";
|
|
274
|
+
return [`${index + 1}. ${tag}${label}${selector}`];
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function formatHuman(response: BrowserResponse, screenshotPath?: string) {
|
|
279
|
+
const lines: string[] = [];
|
|
280
|
+
const action = response.action ?? "browser";
|
|
281
|
+
if (response.ok === false) {
|
|
282
|
+
lines.push(
|
|
283
|
+
`Browser ${action} failed: ${response.error ?? "unknown error"}`,
|
|
284
|
+
);
|
|
285
|
+
} else {
|
|
286
|
+
lines.push(`Browser ${action} ok.`);
|
|
287
|
+
}
|
|
288
|
+
if (response.threadId) lines.push(`threadId: ${response.threadId}`);
|
|
289
|
+
if (response.currentUrl) lines.push(`url: ${response.currentUrl}`);
|
|
290
|
+
if (response.pageTitle) lines.push(`title: ${response.pageTitle}`);
|
|
291
|
+
if (screenshotPath) lines.push(`screenshot: ${screenshotPath}`);
|
|
292
|
+
if (response.screenshot?.data && !screenshotPath) {
|
|
293
|
+
lines.push("screenshot captured; pass --output <file> to save it.");
|
|
294
|
+
}
|
|
295
|
+
if (typeof response.text === "string" && response.text.trim()) {
|
|
296
|
+
const text = response.text.trim();
|
|
297
|
+
lines.push("");
|
|
298
|
+
lines.push(text.length > 2000 ? `${text.slice(0, 2000)}...` : text);
|
|
299
|
+
}
|
|
300
|
+
const elements = formatInspectElements(response.elements);
|
|
301
|
+
if (elements.length > 0) {
|
|
302
|
+
lines.push("");
|
|
303
|
+
lines.push("Elements:");
|
|
304
|
+
lines.push(...elements);
|
|
305
|
+
}
|
|
306
|
+
if (response.hint) {
|
|
307
|
+
lines.push("");
|
|
308
|
+
lines.push(String(response.hint));
|
|
309
|
+
}
|
|
310
|
+
if (response.threadUrl) {
|
|
311
|
+
lines.push("");
|
|
312
|
+
lines.push(`Open in Dench: ${response.threadUrl}`);
|
|
313
|
+
}
|
|
314
|
+
return lines.join("\n");
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function output(ctx: BrowserCliContext, value: unknown): void {
|
|
318
|
+
if (ctx.jsonOutput) {
|
|
319
|
+
console.log(JSON.stringify(value, null, 2));
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
console.log(String(value));
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function parseActionAndBody(ctx: BrowserCliContext): {
|
|
326
|
+
body: Record<string, unknown>;
|
|
327
|
+
outputPath?: string;
|
|
328
|
+
} {
|
|
329
|
+
const actionArg = ctx.args.shift();
|
|
330
|
+
const action =
|
|
331
|
+
actionArg === "new-tab"
|
|
332
|
+
? "newTab"
|
|
333
|
+
: actionArg === "activate-tab"
|
|
334
|
+
? "activateTab"
|
|
335
|
+
: actionArg === "close-tab"
|
|
336
|
+
? "closeTab"
|
|
337
|
+
: actionArg === "click-at"
|
|
338
|
+
? "clickAt"
|
|
339
|
+
: (actionArg ?? "open");
|
|
340
|
+
if (!ACTIONS.has(action)) {
|
|
341
|
+
throw new BrowserCliError(`Unknown browser action: ${actionArg}`);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const threadId = consumeFlagValue(ctx.args, "--thread");
|
|
345
|
+
const selector = consumeFlagValue(ctx.args, "--selector");
|
|
346
|
+
const text = consumeFlagValue(ctx.args, "--text");
|
|
347
|
+
const label = consumeFlagValue(ctx.args, "--label");
|
|
348
|
+
const value = consumeFlagValue(ctx.args, "--value");
|
|
349
|
+
const key = consumeFlagValue(ctx.args, "--key");
|
|
350
|
+
const targetId =
|
|
351
|
+
consumeFlagValue(ctx.args, "--target-id") ||
|
|
352
|
+
consumeFlagValue(ctx.args, "--targetId");
|
|
353
|
+
const pixels = parseNumberFlag(ctx.args, "--pixels");
|
|
354
|
+
const timeoutMs =
|
|
355
|
+
parseNumberFlag(ctx.args, "--timeout-ms") ??
|
|
356
|
+
parseNumberFlag(ctx.args, "--timeoutMs");
|
|
357
|
+
const x = parseNumberFlag(ctx.args, "--x");
|
|
358
|
+
const y = parseNumberFlag(ctx.args, "--y");
|
|
359
|
+
const outputPath = consumeFlagValue(ctx.args, "--output");
|
|
360
|
+
const fullPage = consumeFlag(ctx.args, "--full-page");
|
|
361
|
+
|
|
362
|
+
if (action === "open" || action === "navigate" || action === "newTab") {
|
|
363
|
+
const url = normalizeBrowserUrl(
|
|
364
|
+
consumeFlagValue(ctx.args, "--url") ?? ctx.args.join(" "),
|
|
365
|
+
);
|
|
366
|
+
ctx.args.length = 0;
|
|
367
|
+
return {
|
|
368
|
+
body: { action, threadId, url },
|
|
369
|
+
outputPath,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (action === "type") {
|
|
374
|
+
return {
|
|
375
|
+
body: { action, threadId, value: value ?? ctx.args.join(" ") },
|
|
376
|
+
outputPath,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (action === "key") {
|
|
381
|
+
return {
|
|
382
|
+
body: { action, threadId, key: key ?? ctx.args.join(" ") },
|
|
383
|
+
outputPath,
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (action === "scroll") {
|
|
388
|
+
const direction = ctx.args[0] === "up" ? "up" : "down";
|
|
389
|
+
return {
|
|
390
|
+
body: { action, direction, pixels, threadId },
|
|
391
|
+
outputPath,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (action === "evaluate") {
|
|
396
|
+
const script = consumeFlagValue(ctx.args, "--script") ?? ctx.args.join(" ");
|
|
397
|
+
return {
|
|
398
|
+
body: { action, script, threadId },
|
|
399
|
+
outputPath,
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (action === "clickAt") {
|
|
404
|
+
const positionalX = ctx.args[0] !== undefined ? Number(ctx.args[0]) : x;
|
|
405
|
+
const positionalY = ctx.args[1] !== undefined ? Number(ctx.args[1]) : y;
|
|
406
|
+
return {
|
|
407
|
+
body: { action, threadId, x: positionalX, y: positionalY },
|
|
408
|
+
outputPath,
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (action === "activateTab" || action === "closeTab") {
|
|
413
|
+
return {
|
|
414
|
+
body: { action, targetId: targetId ?? ctx.args[0], threadId },
|
|
415
|
+
outputPath,
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
return {
|
|
420
|
+
body: {
|
|
421
|
+
action,
|
|
422
|
+
fullPage,
|
|
423
|
+
label,
|
|
424
|
+
pixels,
|
|
425
|
+
selector,
|
|
426
|
+
targetId,
|
|
427
|
+
text,
|
|
428
|
+
threadId,
|
|
429
|
+
timeoutMs,
|
|
430
|
+
value,
|
|
431
|
+
},
|
|
432
|
+
outputPath,
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
export async function runBrowserCommand(opts: {
|
|
437
|
+
args: string[];
|
|
438
|
+
runtime: RuntimeBundle;
|
|
439
|
+
}): Promise<void> {
|
|
440
|
+
const args = [...opts.args];
|
|
441
|
+
const jsonOutput = hasFlag(args, "--json");
|
|
442
|
+
const ctx: BrowserCliContext = {
|
|
443
|
+
args,
|
|
444
|
+
jsonOutput,
|
|
445
|
+
runtime: opts.runtime,
|
|
446
|
+
};
|
|
447
|
+
const first = args[0];
|
|
448
|
+
if (!first || first === "help" || first === "--help" || first === "-h") {
|
|
449
|
+
browserHelp();
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (first === "reset") {
|
|
454
|
+
await forgetThreadId(opts.runtime);
|
|
455
|
+
output(
|
|
456
|
+
ctx,
|
|
457
|
+
jsonOutput ? { ok: true, reset: true } : "Forgot CLI browser thread.",
|
|
458
|
+
);
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const { body, outputPath } = parseActionAndBody(ctx);
|
|
463
|
+
if (!body.threadId) {
|
|
464
|
+
body.threadId = await readRememberedThreadId(opts.runtime);
|
|
465
|
+
}
|
|
466
|
+
if (body.action === "stop" && !body.threadId) {
|
|
467
|
+
output(
|
|
468
|
+
ctx,
|
|
469
|
+
ctx.jsonOutput
|
|
470
|
+
? { ok: true, action: "stop", stopped: false, reason: "no_thread" }
|
|
471
|
+
: "No remembered CLI browser thread. Pass --thread <threadId> to stop a specific one.",
|
|
472
|
+
);
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const result = await postJson(
|
|
477
|
+
buildCliBrowserUrl(opts.runtime.host),
|
|
478
|
+
opts.runtime.bearerToken,
|
|
479
|
+
body,
|
|
480
|
+
);
|
|
481
|
+
if (!result.ok) {
|
|
482
|
+
throw new BrowserCliError(
|
|
483
|
+
`${payloadError(result.payload)} (${result.status})`,
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const response =
|
|
488
|
+
result.payload && typeof result.payload === "object"
|
|
489
|
+
? (result.payload as BrowserResponse)
|
|
490
|
+
: ({} as BrowserResponse);
|
|
491
|
+
if (typeof response.threadId === "string" && response.threadId.trim()) {
|
|
492
|
+
await rememberThreadId(opts.runtime, response.threadId.trim());
|
|
493
|
+
}
|
|
494
|
+
const screenshotPath = await maybeWriteScreenshot(response, outputPath);
|
|
495
|
+
if (ctx.jsonOutput) {
|
|
496
|
+
output(ctx, screenshotPath ? { ...response, screenshotPath } : response);
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const printable =
|
|
501
|
+
screenshotPath && response.screenshot
|
|
502
|
+
? { ...response, screenshot: undefined }
|
|
503
|
+
: response;
|
|
504
|
+
output(ctx, formatHuman(printable, screenshotPath));
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
export { BrowserCliError };
|
package/chat.ts
CHANGED
|
@@ -433,7 +433,7 @@ async function runChatDeleteCommand(ctx: ChatCliContext): Promise<void> {
|
|
|
433
433
|
);
|
|
434
434
|
}
|
|
435
435
|
const automatedSuffix = includeAutomated
|
|
436
|
-
? " (INCLUDING ROUTINES
|
|
436
|
+
? " (INCLUDING AUTOMATED ROUTINES — automated thread protection bypassed)"
|
|
437
437
|
: "";
|
|
438
438
|
const ok = await promptYesNo(
|
|
439
439
|
`PERMANENTLY queue delete of ${threadIds.length} chat${
|