@dench.com/cli 2.1.1 → 2.1.2
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/browser.ts +507 -0
- package/crm.ts +209 -0
- package/dench.ts +124 -41
- package/fs-daemon.ts +201 -2
- package/lib/api-schemas.ts +1100 -0
- package/lib/command-registry.ts +1664 -0
- package/lib/organizationSelector.ts +58 -0
- package/linkedin.ts +210 -0
- package/package.json +6 -2
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 };
|