unbrowse 2.10.2 → 2.12.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/README.md +97 -10
- package/bin/unbrowse-wrapper.mjs +14 -30
- package/dist/cli.js +2832 -766
- package/dist/mcp.js +20392 -0
- package/package.json +5 -2
- package/runtime-src/agent-outcome.ts +166 -0
- package/runtime-src/analytics-session.ts +27 -5
- package/runtime-src/api/browse-index.ts +254 -0
- package/runtime-src/api/browse-session.ts +177 -0
- package/runtime-src/api/browse-submit.ts +462 -0
- package/runtime-src/api/routes.ts +348 -281
- package/runtime-src/auth/index.ts +68 -23
- package/runtime-src/cli.ts +574 -165
- package/runtime-src/client/index.ts +171 -18
- package/runtime-src/execution/index.ts +60 -10
- package/runtime-src/execution/robots.ts +167 -0
- package/runtime-src/kuri/client.ts +281 -89
- package/runtime-src/mcp.ts +1065 -0
- package/runtime-src/orchestrator/index.ts +66 -38
- package/runtime-src/orchestrator/timing-economics.ts +80 -0
- package/runtime-src/reverse-engineer/index.ts +33 -3
- package/runtime-src/runtime/setup.ts +8 -8
- package/runtime-src/single-binary.ts +20 -2
- package/runtime-src/types/skill.ts +2 -0
package/runtime-src/cli.ts
CHANGED
|
@@ -8,10 +8,18 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { config as loadEnv } from "dotenv";
|
|
11
|
-
import {
|
|
11
|
+
import { spawn } from "node:child_process";
|
|
12
|
+
import {
|
|
13
|
+
detectTelemetryHostType,
|
|
14
|
+
ensureCliInstallTracked,
|
|
15
|
+
ensureRegistered,
|
|
16
|
+
getApiKey,
|
|
17
|
+
recordFunnelTelemetryEvent,
|
|
18
|
+
recordInstallTelemetryEvent,
|
|
19
|
+
} from "./client/index.js";
|
|
12
20
|
import { findSitePack, findTask, allSitePacks, buildDepsGraph, planExecution, buildDepsMetadata, type SitePack } from "./cli/shortcuts.js";
|
|
13
21
|
import { ensureLocalServer, checkServerVersion, stopServer, restartServer } from "./runtime/local-server.js";
|
|
14
|
-
import { isMainModule } from "./runtime/paths.js";
|
|
22
|
+
import { isMainModule, resolveSiblingEntrypoint, runtimeArgsForEntrypoint } from "./runtime/paths.js";
|
|
15
23
|
import { drainPendingIndexJobs } from "./indexer/index.js";
|
|
16
24
|
import { drainPendingPassivePublishes } from "./orchestrator/passive-publish.js";
|
|
17
25
|
import { runSetup, type SetupReport, type SetupScope } from "./runtime/setup.js";
|
|
@@ -85,6 +93,28 @@ function info(msg: string): void {
|
|
|
85
93
|
process.stderr.write(`[unbrowse] ${msg}\n`);
|
|
86
94
|
}
|
|
87
95
|
|
|
96
|
+
function resolveResultError(result: Record<string, unknown>): string | undefined {
|
|
97
|
+
return (result.result as Record<string, unknown> | undefined)?.error as string | undefined
|
|
98
|
+
?? result.error as string | undefined;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function resolveLoginUrl(result: Record<string, unknown>, fallbackUrl?: string): string {
|
|
102
|
+
return (result.result as Record<string, unknown> | undefined)?.login_url as string
|
|
103
|
+
?? fallbackUrl
|
|
104
|
+
?? "";
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function hasIndexingFallback(result: Record<string, unknown>): boolean {
|
|
108
|
+
return (result.result as Record<string, unknown> | undefined)?.indexing_fallback_available === true;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function isResolveSuccessResult(result: Record<string, unknown>): boolean {
|
|
112
|
+
const resultObj = result.result as Record<string, unknown> | undefined;
|
|
113
|
+
if (resolveResultError(result)) return false;
|
|
114
|
+
if (resultObj?.status === "browse_session_open") return false;
|
|
115
|
+
return !!result.result || Array.isArray(result.available_endpoints);
|
|
116
|
+
}
|
|
117
|
+
|
|
88
118
|
async function withPendingNotice<T>(promise: Promise<T>, message: string, delayMs = 3_000): Promise<T> {
|
|
89
119
|
let done = false;
|
|
90
120
|
const timer = setTimeout(() => {
|
|
@@ -126,130 +156,279 @@ function slimTrace(obj: Record<string, unknown>): Record<string, unknown> {
|
|
|
126
156
|
};
|
|
127
157
|
if ("result" in obj) out.result = obj.result;
|
|
128
158
|
if (obj.available_endpoints) out.available_endpoints = obj.available_endpoints;
|
|
159
|
+
if (obj.impact) out.impact = obj.impact;
|
|
160
|
+
if (obj.next_actions) out.next_actions = obj.next_actions;
|
|
161
|
+
if (obj.next_step) out.next_step = obj.next_step;
|
|
129
162
|
if (obj.source) out.source = obj.source;
|
|
130
163
|
if (obj.skill) out.skill = obj.skill;
|
|
131
164
|
return out;
|
|
132
165
|
}
|
|
133
166
|
|
|
167
|
+
function formatSavedDuration(ms: number): string {
|
|
168
|
+
if (ms >= 60_000) return `${(ms / 60_000).toFixed(1)}m`;
|
|
169
|
+
if (ms >= 10_000) return `${Math.round(ms / 1000)}s`;
|
|
170
|
+
if (ms >= 1000) return `${(ms / 1000).toFixed(1)}s`;
|
|
171
|
+
return `${ms}ms`;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function emitImpactSummary(result: Record<string, unknown>): void {
|
|
175
|
+
const impact = result.impact as Record<string, unknown> | undefined;
|
|
176
|
+
if (!impact) return;
|
|
177
|
+
|
|
178
|
+
const timeSavedMs = typeof impact.time_saved_ms === "number" ? impact.time_saved_ms : 0;
|
|
179
|
+
const tokensSaved = typeof impact.tokens_saved === "number" ? impact.tokens_saved : 0;
|
|
180
|
+
const timeSavedPct = typeof impact.time_saved_pct === "number" ? impact.time_saved_pct : 0;
|
|
181
|
+
const tokensSavedPct = typeof impact.tokens_saved_pct === "number" ? impact.tokens_saved_pct : 0;
|
|
182
|
+
const browserAvoided = impact.browser_avoided === true;
|
|
183
|
+
if (timeSavedMs <= 0 && tokensSaved <= 0 && !browserAvoided) return;
|
|
184
|
+
|
|
185
|
+
const parts: string[] = [];
|
|
186
|
+
if (timeSavedMs > 0) parts.push(`${formatSavedDuration(timeSavedMs)} saved (${timeSavedPct}% faster)`);
|
|
187
|
+
if (tokensSaved > 0) parts.push(`${tokensSaved.toLocaleString("en-US")} tokens saved (${tokensSavedPct}% less context)`);
|
|
188
|
+
if (browserAvoided) parts.push("browser avoided");
|
|
189
|
+
info(parts.join(" • "));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function emitNextActionSummary(result: Record<string, unknown>): void {
|
|
193
|
+
const nextActions = Array.isArray(result.next_actions)
|
|
194
|
+
? result.next_actions as Array<Record<string, unknown>>
|
|
195
|
+
: [];
|
|
196
|
+
if (nextActions.length === 0) return;
|
|
197
|
+
info("Likely next actions:");
|
|
198
|
+
for (const action of nextActions.slice(0, 3)) {
|
|
199
|
+
const command = typeof action.command === "string" ? action.command : "";
|
|
200
|
+
const title = typeof action.title === "string" ? action.title : (action.endpoint_id as string | undefined) ?? "next step";
|
|
201
|
+
const why = typeof action.why === "string" ? action.why : "";
|
|
202
|
+
info(` ${command || title}${why ? ` # ${why}` : ""}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
134
206
|
|
|
135
207
|
async function cmdHealth(flags: Record<string, string | boolean>): Promise<void> {
|
|
136
208
|
output(await api("GET", "/health"), !!flags.pretty);
|
|
137
209
|
}
|
|
138
210
|
|
|
211
|
+
function telemetryDomainFromInput(domain?: string, url?: string): string | null {
|
|
212
|
+
if (domain?.trim()) return domain.trim().replace(/^www\./, "");
|
|
213
|
+
if (!url?.trim()) return null;
|
|
214
|
+
try {
|
|
215
|
+
return new URL(url).hostname.replace(/^www\./, "");
|
|
216
|
+
} catch {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
139
221
|
async function cmdResolve(flags: Record<string, string | boolean>): Promise<void> {
|
|
140
222
|
const intent = flags.intent as string;
|
|
141
223
|
if (!intent) die("--intent is required");
|
|
224
|
+
const hostType = detectTelemetryHostType();
|
|
225
|
+
await ensureCliInstallTracked(hostType);
|
|
226
|
+
await recordFunnelTelemetryEvent("cli_invoked", {
|
|
227
|
+
source: "cli",
|
|
228
|
+
hostType,
|
|
229
|
+
properties: { command: "resolve" },
|
|
230
|
+
});
|
|
231
|
+
await recordFunnelTelemetryEvent("resolve_started", {
|
|
232
|
+
source: "cli",
|
|
233
|
+
hostType,
|
|
234
|
+
properties: {
|
|
235
|
+
command: "resolve",
|
|
236
|
+
intent,
|
|
237
|
+
domain: telemetryDomainFromInput(flags.domain as string | undefined, flags.url as string | undefined),
|
|
238
|
+
url: typeof flags.url === "string" ? flags.url : null,
|
|
239
|
+
has_url: typeof flags.url === "string",
|
|
240
|
+
has_domain: typeof flags.domain === "string",
|
|
241
|
+
auto_execute: !!flags.execute,
|
|
242
|
+
},
|
|
243
|
+
});
|
|
142
244
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
245
|
+
try {
|
|
246
|
+
const body: Record<string, unknown> = { intent };
|
|
247
|
+
const url = flags.url as string | undefined;
|
|
248
|
+
const domain = flags.domain as string | undefined;
|
|
249
|
+
const explicitEndpointId = flags["endpoint-id"] as string | undefined;
|
|
250
|
+
const autoExecute = !!flags.execute;
|
|
251
|
+
const extraParams = flags.params ? JSON.parse(flags.params as string) : {};
|
|
252
|
+
|
|
253
|
+
if (url) {
|
|
254
|
+
body.params = { url };
|
|
255
|
+
body.context = { url };
|
|
256
|
+
}
|
|
257
|
+
if (domain) {
|
|
258
|
+
body.context = { ...(body.context as Record<string, unknown> ?? {}), domain };
|
|
259
|
+
}
|
|
260
|
+
if (explicitEndpointId) {
|
|
261
|
+
body.params = { ...(body.params as Record<string, unknown> ?? {}), endpoint_id: explicitEndpointId };
|
|
262
|
+
}
|
|
263
|
+
if (flags.params) {
|
|
264
|
+
body.params = { ...(body.params as Record<string, unknown> ?? {}), ...extraParams };
|
|
265
|
+
}
|
|
266
|
+
if (flags["dry-run"]) body.dry_run = true;
|
|
267
|
+
if (flags["force-capture"]) body.force_capture = true;
|
|
268
|
+
body.projection = { raw: true };
|
|
149
269
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
154
|
-
if (domain) {
|
|
155
|
-
body.context = { ...(body.context as Record<string, unknown> ?? {}), domain };
|
|
156
|
-
}
|
|
157
|
-
if (explicitEndpointId) {
|
|
158
|
-
body.params = { ...(body.params as Record<string, unknown> ?? {}), endpoint_id: explicitEndpointId };
|
|
159
|
-
}
|
|
160
|
-
if (flags.params) {
|
|
161
|
-
body.params = { ...(body.params as Record<string, unknown> ?? {}), ...extraParams };
|
|
162
|
-
}
|
|
163
|
-
if (flags["dry-run"]) body.dry_run = true;
|
|
164
|
-
if (flags["force-capture"]) body.force_capture = true;
|
|
165
|
-
body.projection = { raw: true };
|
|
270
|
+
function execBody(endpointId: string): Record<string, unknown> {
|
|
271
|
+
return { params: { endpoint_id: endpointId, ...extraParams }, intent, projection: { raw: true } };
|
|
272
|
+
}
|
|
166
273
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
274
|
+
function resolveSkillId(): string | undefined {
|
|
275
|
+
return (result.skill as Record<string, unknown>)?.skill_id as string
|
|
276
|
+
?? (result as Record<string, unknown>).skill_id as string;
|
|
277
|
+
}
|
|
170
278
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
279
|
+
const startedAt = Date.now();
|
|
280
|
+
async function resolveOnce(message = "Still working. First-time capture/indexing for a site can take 20-80s. Waiting is usually better than falling back."): Promise<Record<string, unknown>> {
|
|
281
|
+
return withPendingNotice(
|
|
282
|
+
api("POST", "/v1/intent/resolve", body) as Promise<Record<string, unknown>>,
|
|
283
|
+
message,
|
|
284
|
+
);
|
|
285
|
+
}
|
|
175
286
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
287
|
+
let result = await resolveOnce();
|
|
288
|
+
let attemptedForceCapture = !!body.force_capture;
|
|
289
|
+
let attemptedCookieImport = false;
|
|
290
|
+
let attemptedInteractiveLogin = false;
|
|
291
|
+
|
|
292
|
+
while (true) {
|
|
293
|
+
const resultError = resolveResultError(result);
|
|
294
|
+
if (resultError === "payment_required" && hasIndexingFallback(result) && url && !attemptedForceCapture) {
|
|
295
|
+
attemptedForceCapture = true;
|
|
296
|
+
body.force_capture = true;
|
|
297
|
+
info("Marketplace search is paid here. Falling back to free live capture on the exact URL...");
|
|
298
|
+
result = await resolveOnce("Running free live capture...");
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (resultError === "auth_required") {
|
|
303
|
+
const loginUrl = resolveLoginUrl(result, url);
|
|
304
|
+
if (!loginUrl) break;
|
|
305
|
+
|
|
306
|
+
if (!attemptedCookieImport) {
|
|
307
|
+
attemptedCookieImport = true;
|
|
308
|
+
info("Site requires authentication. Trying browser cookie import first...");
|
|
309
|
+
const stealResult = await api("POST", "/v1/auth/steal", { url: loginUrl }) as Record<string, unknown>;
|
|
310
|
+
const cookiesStored = typeof stealResult.cookies_stored === "number"
|
|
311
|
+
? stealResult.cookies_stored
|
|
312
|
+
: Number(stealResult.cookies_stored ?? 0);
|
|
313
|
+
if (stealResult.success === true && cookiesStored > 0) {
|
|
314
|
+
info(`Imported ${cookiesStored} browser cookies. Retrying...`);
|
|
315
|
+
result = await resolveOnce("Retrying after browser cookie import...");
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (!attemptedInteractiveLogin) {
|
|
321
|
+
attemptedInteractiveLogin = true;
|
|
322
|
+
info("Site requires authentication. Opening browser for login...");
|
|
323
|
+
const loginResult = await api("POST", "/v1/auth/login", { url: loginUrl }) as Record<string, unknown>;
|
|
324
|
+
if (loginResult.error || loginResult.success === false) {
|
|
325
|
+
const message = typeof loginResult.error === "string"
|
|
326
|
+
? loginResult.error
|
|
327
|
+
: "interactive login did not produce a reusable session";
|
|
328
|
+
throw new Error(`Login failed: ${message}. Run: unbrowse login --url "${loginUrl}"`);
|
|
329
|
+
}
|
|
330
|
+
info("Login complete. Retrying...");
|
|
331
|
+
result = await resolveOnce("Retrying after login...");
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
181
338
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
const loginUrl = (result.result as Record<string, unknown>)?.login_url as string
|
|
187
|
-
?? url ?? "";
|
|
188
|
-
if (loginUrl) {
|
|
189
|
-
info("Site requires authentication. Opening browser for login...");
|
|
190
|
-
try {
|
|
191
|
-
await api("POST", "/v1/auth/login", { url: loginUrl });
|
|
192
|
-
info("Login complete. Retrying...");
|
|
339
|
+
// When agent explicitly picked an endpoint but resolve deferred, execute it directly
|
|
340
|
+
if (explicitEndpointId && result.available_endpoints) {
|
|
341
|
+
const skillId = resolveSkillId();
|
|
342
|
+
if (skillId) {
|
|
193
343
|
result = await withPendingNotice(
|
|
194
|
-
api("POST",
|
|
195
|
-
"
|
|
344
|
+
api("POST", `/v1/skills/${skillId}/execute`, execBody(explicitEndpointId)) as Promise<Record<string, unknown>>,
|
|
345
|
+
"Executing selected endpoint...",
|
|
196
346
|
);
|
|
197
|
-
} catch (err) {
|
|
198
|
-
die(`Login failed: ${(err as Error).message}. Run: unbrowse login --url "${loginUrl}"`);
|
|
199
347
|
}
|
|
200
348
|
}
|
|
201
|
-
}
|
|
202
349
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
350
|
+
// --execute: auto-pick best endpoint and return data in one step
|
|
351
|
+
if (autoExecute && result.available_endpoints && !result.result) {
|
|
352
|
+
const endpoints = result.available_endpoints as Array<Record<string, unknown>>;
|
|
353
|
+
const skillId = resolveSkillId();
|
|
354
|
+
if (skillId && endpoints.length > 0) {
|
|
355
|
+
const bestEndpoint = endpoints[0];
|
|
356
|
+
info(`Auto-executing endpoint: ${bestEndpoint.description ?? bestEndpoint.endpoint_id}`);
|
|
357
|
+
result = await withPendingNotice(
|
|
358
|
+
api("POST", `/v1/skills/${skillId}/execute`, execBody(bestEndpoint.endpoint_id as string)) as Promise<Record<string, unknown>>,
|
|
359
|
+
"Executing best endpoint...",
|
|
360
|
+
);
|
|
361
|
+
}
|
|
211
362
|
}
|
|
212
|
-
}
|
|
213
363
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
"
|
|
224
|
-
|
|
364
|
+
// Browse session handoff
|
|
365
|
+
const resultObj = result.result as Record<string, unknown> | undefined;
|
|
366
|
+
if (resultObj?.status === "browse_session_open") {
|
|
367
|
+
info(`No cached API. Browser session open on ${resultObj.domain ?? resultObj.url}.`);
|
|
368
|
+
info(`Preferred flow: snap -> click/fill/eval -> submit -> sync -> close.`);
|
|
369
|
+
info(`Use these commands to get your data:`);
|
|
370
|
+
const commands = resultObj.commands as string[] ?? [
|
|
371
|
+
"unbrowse snap --filter interactive",
|
|
372
|
+
"unbrowse click <ref>",
|
|
373
|
+
"unbrowse fill <ref> <value>",
|
|
374
|
+
"unbrowse submit --wait-for \"/next-step\"",
|
|
375
|
+
"unbrowse sync",
|
|
376
|
+
"unbrowse close",
|
|
377
|
+
];
|
|
378
|
+
for (const cmd of commands) info(` ${cmd}`);
|
|
379
|
+
info(`For JS-heavy forms: prefer real date/time clicks first, inspect hidden inputs with eval when needed, then submit.`);
|
|
380
|
+
info(`All traffic is being passively captured. Run "unbrowse close" when done.`);
|
|
381
|
+
output(slimTrace(result), !!flags.pretty);
|
|
382
|
+
return;
|
|
225
383
|
}
|
|
226
|
-
}
|
|
227
384
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
info(`No cached API. Browser session open on ${resultObj.domain ?? resultObj.url}.`);
|
|
232
|
-
info(`Use these commands to get your data:`);
|
|
233
|
-
const commands = resultObj.commands as string[] ?? ["unbrowse snap --filter interactive", "unbrowse click <ref>", "unbrowse close"];
|
|
234
|
-
for (const cmd of commands) info(` ${cmd}`);
|
|
235
|
-
info(`All traffic is being passively captured. Run "unbrowse close" when done.`);
|
|
236
|
-
output(slimTrace(result), !!flags.pretty);
|
|
237
|
-
return;
|
|
238
|
-
}
|
|
385
|
+
if (Date.now() - startedAt > 3_000 && result.source === "live-capture") {
|
|
386
|
+
info("Live capture finished. Future runs against this site should be much faster.");
|
|
387
|
+
}
|
|
239
388
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
389
|
+
if (isResolveSuccessResult(result)) {
|
|
390
|
+
await recordFunnelTelemetryEvent("resolve_completed", {
|
|
391
|
+
source: "cli",
|
|
392
|
+
hostType,
|
|
393
|
+
properties: {
|
|
394
|
+
command: "resolve",
|
|
395
|
+
intent,
|
|
396
|
+
domain: telemetryDomainFromInput(domain, url),
|
|
397
|
+
url: url ?? null,
|
|
398
|
+
source: result.source,
|
|
399
|
+
auto_execute: autoExecute,
|
|
400
|
+
explicit_endpoint: explicitEndpointId ?? null,
|
|
401
|
+
},
|
|
402
|
+
});
|
|
403
|
+
}
|
|
243
404
|
|
|
244
|
-
|
|
405
|
+
result = slimTrace(result);
|
|
406
|
+
emitImpactSummary(result);
|
|
407
|
+
emitNextActionSummary(result);
|
|
245
408
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
409
|
+
const skill = result.skill as Record<string, unknown> | undefined;
|
|
410
|
+
const trace = result.trace as Record<string, unknown> | undefined;
|
|
411
|
+
if (skill?.skill_id && trace) {
|
|
412
|
+
(result as Record<string, unknown>)._feedback = `unbrowse feedback --skill ${skill.skill_id} --endpoint ${trace.endpoint_id || "?"} --rating <1-5>`;
|
|
413
|
+
}
|
|
251
414
|
|
|
252
|
-
|
|
415
|
+
output(result, !!flags.pretty);
|
|
416
|
+
} catch (error) {
|
|
417
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
418
|
+
await recordFunnelTelemetryEvent("resolve_failed", {
|
|
419
|
+
source: "cli",
|
|
420
|
+
hostType,
|
|
421
|
+
properties: {
|
|
422
|
+
command: "resolve",
|
|
423
|
+
intent,
|
|
424
|
+
domain: telemetryDomainFromInput(flags.domain as string | undefined, flags.url as string | undefined),
|
|
425
|
+
url: typeof flags.url === "string" ? flags.url : null,
|
|
426
|
+
failure_stage: "resolve",
|
|
427
|
+
failure_reason: message,
|
|
428
|
+
},
|
|
429
|
+
});
|
|
430
|
+
throw error;
|
|
431
|
+
}
|
|
253
432
|
}
|
|
254
433
|
|
|
255
434
|
// ---------------------------------------------------------------------------
|
|
@@ -342,87 +521,154 @@ function schemaOf(value: unknown, depth = 4): unknown {
|
|
|
342
521
|
async function cmdExecute(flags: Record<string, string | boolean>): Promise<void> {
|
|
343
522
|
const skillId = flags.skill as string;
|
|
344
523
|
if (!skillId) die("--skill is required");
|
|
524
|
+
const hostType = detectTelemetryHostType();
|
|
525
|
+
await ensureCliInstallTracked(hostType);
|
|
526
|
+
await recordFunnelTelemetryEvent("cli_invoked", {
|
|
527
|
+
source: "cli",
|
|
528
|
+
hostType,
|
|
529
|
+
properties: { command: "execute" },
|
|
530
|
+
});
|
|
531
|
+
await recordFunnelTelemetryEvent("resolve_started", {
|
|
532
|
+
source: "cli",
|
|
533
|
+
hostType,
|
|
534
|
+
properties: {
|
|
535
|
+
command: "execute",
|
|
536
|
+
intent: typeof flags.intent === "string" ? flags.intent : null,
|
|
537
|
+
domain: telemetryDomainFromInput(undefined, flags.url as string | undefined),
|
|
538
|
+
url: typeof flags.url === "string" ? flags.url : null,
|
|
539
|
+
skill_id: skillId,
|
|
540
|
+
endpoint_id: typeof flags.endpoint === "string" ? flags.endpoint : null,
|
|
541
|
+
},
|
|
542
|
+
});
|
|
345
543
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
(
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
544
|
+
try {
|
|
545
|
+
const body: Record<string, unknown> = { params: {} };
|
|
546
|
+
if (flags.endpoint) {
|
|
547
|
+
(body.params as Record<string, unknown>).endpoint_id = flags.endpoint;
|
|
548
|
+
}
|
|
549
|
+
if (flags.params) {
|
|
550
|
+
body.params = { ...(body.params as Record<string, unknown>), ...JSON.parse(flags.params as string) };
|
|
551
|
+
}
|
|
552
|
+
if (flags.url) {
|
|
553
|
+
body.context_url = flags.url;
|
|
554
|
+
(body.params as Record<string, unknown>).url = flags.url;
|
|
555
|
+
}
|
|
556
|
+
if (flags.intent) body.intent = flags.intent;
|
|
557
|
+
if (flags["dry-run"]) body.dry_run = true;
|
|
558
|
+
if (flags["confirm-unsafe"]) body.confirm_unsafe = true;
|
|
559
|
+
body.projection = { raw: true };
|
|
560
|
+
|
|
561
|
+
let result = await withPendingNotice(
|
|
562
|
+
api("POST", `/v1/skills/${skillId}/execute`, body) as Promise<Record<string, unknown>>,
|
|
563
|
+
"Still working. This endpoint may require browser replay or first-time auth/capture setup.",
|
|
564
|
+
);
|
|
565
|
+
|
|
566
|
+
if (isResolveSuccessResult(result)) {
|
|
567
|
+
await recordFunnelTelemetryEvent("resolve_completed", {
|
|
568
|
+
source: "cli",
|
|
569
|
+
hostType,
|
|
570
|
+
properties: {
|
|
571
|
+
command: "execute",
|
|
572
|
+
intent: typeof flags.intent === "string" ? flags.intent : null,
|
|
573
|
+
domain: telemetryDomainFromInput(undefined, flags.url as string | undefined),
|
|
574
|
+
url: typeof flags.url === "string" ? flags.url : null,
|
|
575
|
+
skill_id: skillId,
|
|
576
|
+
endpoint_id: typeof flags.endpoint === "string" ? flags.endpoint : null,
|
|
577
|
+
},
|
|
578
|
+
});
|
|
579
|
+
}
|
|
361
580
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
581
|
+
// Strip metadata bloat
|
|
582
|
+
result = slimTrace(result);
|
|
583
|
+
emitImpactSummary(result);
|
|
584
|
+
emitNextActionSummary(result);
|
|
366
585
|
|
|
367
|
-
|
|
368
|
-
|
|
586
|
+
const pathFlag = flags.path as string | undefined;
|
|
587
|
+
const extractFlag = flags.extract as string | undefined;
|
|
588
|
+
const limitFlag = flags.limit ? Number(flags.limit) : undefined;
|
|
589
|
+
const schemaFlag = !!flags.schema;
|
|
590
|
+
const rawFlag = !!flags.raw;
|
|
369
591
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
592
|
+
// --schema: show response structure without data
|
|
593
|
+
if (schemaFlag && !rawFlag) {
|
|
594
|
+
const data = result.result;
|
|
595
|
+
output({
|
|
596
|
+
trace: result.trace,
|
|
597
|
+
schema: schemaOf(data),
|
|
598
|
+
...(result.impact ? { impact: result.impact } : {}),
|
|
599
|
+
...(result.next_actions ? { next_actions: result.next_actions } : {}),
|
|
600
|
+
}, !!flags.pretty);
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
375
603
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
output({ trace: result.trace, schema: schemaOf(data) }, !!flags.pretty);
|
|
380
|
-
return;
|
|
381
|
-
}
|
|
604
|
+
// Apply --path, --extract, --limit when not --raw
|
|
605
|
+
if (!rawFlag && (pathFlag || extractFlag || limitFlag)) {
|
|
606
|
+
const data = pathFlag ? drillPath(result.result, pathFlag) : result.result;
|
|
382
607
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
let data = pathFlag ? drillPath(result.result, pathFlag) : result.result;
|
|
608
|
+
// Ensure array for extract/limit
|
|
609
|
+
const items = Array.isArray(data) ? data : data != null ? [data] : [];
|
|
386
610
|
|
|
387
|
-
|
|
388
|
-
|
|
611
|
+
// Apply --extract
|
|
612
|
+
const extracted = extractFlag ? applyExtract(items, extractFlag) : items;
|
|
389
613
|
|
|
390
|
-
|
|
391
|
-
|
|
614
|
+
// Apply --limit
|
|
615
|
+
const limited = limitFlag ? extracted.slice(0, limitFlag) : extracted;
|
|
392
616
|
|
|
393
|
-
|
|
394
|
-
|
|
617
|
+
const trace = result.trace as Record<string, unknown> | undefined;
|
|
618
|
+
const out: Record<string, unknown> = {
|
|
619
|
+
trace: result.trace,
|
|
620
|
+
data: limited,
|
|
621
|
+
count: limited.length,
|
|
622
|
+
...(result.impact ? { impact: result.impact } : {}),
|
|
623
|
+
...(result.next_actions ? { next_actions: result.next_actions } : {}),
|
|
624
|
+
};
|
|
395
625
|
|
|
396
|
-
|
|
397
|
-
|
|
626
|
+
// Prompt agent to review when this is likely a first-time execute
|
|
627
|
+
if (trace?.skill_id && trace?.endpoint_id && limited.length > 0) {
|
|
628
|
+
out._review_hint = `After presenting results, improve this endpoint's description: unbrowse review --skill ${trace.skill_id} --endpoints '[{"endpoint_id":"${trace.endpoint_id}","description":"DESCRIBE WHAT THIS RETURNS","action_kind":"ACTION","resource_kind":"RESOURCE"}]'`;
|
|
629
|
+
}
|
|
398
630
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
out._review_hint = `After presenting results, improve this endpoint's description: unbrowse review --skill ${trace.skill_id} --endpoints '[{"endpoint_id":"${trace.endpoint_id}","description":"DESCRIBE WHAT THIS RETURNS","action_kind":"ACTION","resource_kind":"RESOURCE"}]'`;
|
|
631
|
+
output(out, !!flags.pretty);
|
|
632
|
+
return;
|
|
402
633
|
}
|
|
403
634
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
}
|
|
421
|
-
return;
|
|
635
|
+
// Auto-wrap large responses with extraction_hints when no flags given
|
|
636
|
+
if (!rawFlag && !pathFlag && !extractFlag && !schemaFlag) {
|
|
637
|
+
const raw = JSON.stringify(result.result);
|
|
638
|
+
if (raw && raw.length > 2048) {
|
|
639
|
+
const schema = schemaOf(result.result);
|
|
640
|
+
output({
|
|
641
|
+
trace: result.trace,
|
|
642
|
+
...(result.impact ? { impact: result.impact } : {}),
|
|
643
|
+
...(result.next_actions ? { next_actions: result.next_actions } : {}),
|
|
644
|
+
extraction_hints: {
|
|
645
|
+
message: "Response is large. Use --path/--extract/--limit to filter, or --schema to see structure, or --raw for full response.",
|
|
646
|
+
schema_tree: schema,
|
|
647
|
+
response_bytes: raw.length,
|
|
648
|
+
},
|
|
649
|
+
}, !!flags.pretty);
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
422
652
|
}
|
|
423
|
-
}
|
|
424
653
|
|
|
425
|
-
|
|
654
|
+
output(result, !!flags.pretty);
|
|
655
|
+
} catch (error) {
|
|
656
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
657
|
+
await recordFunnelTelemetryEvent("resolve_failed", {
|
|
658
|
+
source: "cli",
|
|
659
|
+
hostType,
|
|
660
|
+
properties: {
|
|
661
|
+
command: "execute",
|
|
662
|
+
intent: typeof flags.intent === "string" ? flags.intent : null,
|
|
663
|
+
domain: telemetryDomainFromInput(undefined, flags.url as string | undefined),
|
|
664
|
+
url: typeof flags.url === "string" ? flags.url : null,
|
|
665
|
+
skill_id: skillId,
|
|
666
|
+
failure_stage: "execute",
|
|
667
|
+
failure_reason: message,
|
|
668
|
+
},
|
|
669
|
+
});
|
|
670
|
+
throw error;
|
|
671
|
+
}
|
|
426
672
|
}
|
|
427
673
|
|
|
428
674
|
async function cmdFeedback(flags: Record<string, string | boolean>): Promise<void> {
|
|
@@ -490,7 +736,53 @@ async function cmdSearch(flags: Record<string, string | boolean>): Promise<void>
|
|
|
490
736
|
const path = domain ? "/v1/search/domain" : "/v1/search";
|
|
491
737
|
const body: Record<string, unknown> = { intent, k: Number(flags.k) || 5 };
|
|
492
738
|
if (domain) body.domain = domain;
|
|
493
|
-
|
|
739
|
+
const hostType = detectTelemetryHostType();
|
|
740
|
+
await ensureCliInstallTracked(hostType);
|
|
741
|
+
await recordFunnelTelemetryEvent("cli_invoked", {
|
|
742
|
+
source: "cli",
|
|
743
|
+
hostType,
|
|
744
|
+
properties: { command: "search" },
|
|
745
|
+
});
|
|
746
|
+
await recordFunnelTelemetryEvent("search_started", {
|
|
747
|
+
source: "cli",
|
|
748
|
+
hostType,
|
|
749
|
+
properties: {
|
|
750
|
+
command: "search",
|
|
751
|
+
intent,
|
|
752
|
+
domain: domain ?? null,
|
|
753
|
+
k: body.k,
|
|
754
|
+
},
|
|
755
|
+
});
|
|
756
|
+
try {
|
|
757
|
+
const result = await api("POST", path, body) as Record<string, unknown>;
|
|
758
|
+
const results = Array.isArray(result.results) ? result.results : [];
|
|
759
|
+
await recordFunnelTelemetryEvent("search_completed", {
|
|
760
|
+
source: "cli",
|
|
761
|
+
hostType,
|
|
762
|
+
properties: {
|
|
763
|
+
command: "search",
|
|
764
|
+
intent,
|
|
765
|
+
domain: domain ?? null,
|
|
766
|
+
k: body.k,
|
|
767
|
+
result_count: results.length,
|
|
768
|
+
},
|
|
769
|
+
});
|
|
770
|
+
output(result, !!flags.pretty);
|
|
771
|
+
} catch (error) {
|
|
772
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
773
|
+
await recordFunnelTelemetryEvent("search_failed", {
|
|
774
|
+
source: "cli",
|
|
775
|
+
hostType,
|
|
776
|
+
properties: {
|
|
777
|
+
command: "search",
|
|
778
|
+
intent,
|
|
779
|
+
domain: domain ?? null,
|
|
780
|
+
failure_stage: "search",
|
|
781
|
+
failure_reason: message,
|
|
782
|
+
},
|
|
783
|
+
});
|
|
784
|
+
throw error;
|
|
785
|
+
}
|
|
494
786
|
}
|
|
495
787
|
|
|
496
788
|
async function cmdSessions(flags: Record<string, string | boolean>): Promise<void> {
|
|
@@ -501,6 +793,13 @@ async function cmdSessions(flags: Record<string, string | boolean>): Promise<voi
|
|
|
501
793
|
}
|
|
502
794
|
|
|
503
795
|
async function cmdSetup(flags: Record<string, string | boolean>): Promise<void> {
|
|
796
|
+
const hostType = detectTelemetryHostType();
|
|
797
|
+
await ensureCliInstallTracked(hostType);
|
|
798
|
+
await recordFunnelTelemetryEvent("cli_invoked", {
|
|
799
|
+
source: "setup",
|
|
800
|
+
hostType,
|
|
801
|
+
properties: { command: "setup" },
|
|
802
|
+
});
|
|
504
803
|
info("Running setup checks");
|
|
505
804
|
const report = await runSetup({
|
|
506
805
|
cwd: process.cwd(),
|
|
@@ -525,6 +824,26 @@ async function cmdSetup(flags: Record<string, string | boolean>): Promise<void>
|
|
|
525
824
|
info(`Open Code command installed at ${report.opencode.command_file}`);
|
|
526
825
|
}
|
|
527
826
|
|
|
827
|
+
await recordInstallTelemetryEvent("setup", {
|
|
828
|
+
hostType,
|
|
829
|
+
status: report.browser_engine.action === "failed" ? "failed" : "installed",
|
|
830
|
+
properties: {
|
|
831
|
+
browser_engine_action: report.browser_engine.action,
|
|
832
|
+
opencode_action: report.opencode.action,
|
|
833
|
+
no_start: !!flags["no-start"],
|
|
834
|
+
skip_browser: !!flags["skip-browser"],
|
|
835
|
+
},
|
|
836
|
+
});
|
|
837
|
+
await recordFunnelTelemetryEvent("setup_completed", {
|
|
838
|
+
source: "setup",
|
|
839
|
+
hostType,
|
|
840
|
+
properties: {
|
|
841
|
+
browser_engine_action: report.browser_engine.action,
|
|
842
|
+
opencode_action: report.opencode.action,
|
|
843
|
+
no_start: !!flags["no-start"],
|
|
844
|
+
},
|
|
845
|
+
});
|
|
846
|
+
|
|
528
847
|
if (flags["no-start"]) {
|
|
529
848
|
report.server = { started: false, skipped: true, base_url: BASE_URL };
|
|
530
849
|
output(report, true);
|
|
@@ -539,8 +858,23 @@ async function cmdSetup(flags: Record<string, string | boolean>): Promise<void>
|
|
|
539
858
|
try {
|
|
540
859
|
await ensureLocalServer(BASE_URL, false, import.meta.url);
|
|
541
860
|
report.server = { started: true, base_url: BASE_URL };
|
|
861
|
+
await recordFunnelTelemetryEvent("server_autostart_succeeded", {
|
|
862
|
+
source: "setup",
|
|
863
|
+
hostType,
|
|
864
|
+
properties: {
|
|
865
|
+
base_url: BASE_URL,
|
|
866
|
+
},
|
|
867
|
+
});
|
|
542
868
|
} catch (error) {
|
|
543
869
|
const message = error instanceof Error ? error.message : String(error);
|
|
870
|
+
await recordFunnelTelemetryEvent("server_autostart_failed", {
|
|
871
|
+
source: "setup",
|
|
872
|
+
hostType,
|
|
873
|
+
properties: {
|
|
874
|
+
failure_stage: "server_autostart",
|
|
875
|
+
failure_reason: message,
|
|
876
|
+
},
|
|
877
|
+
});
|
|
544
878
|
report.server = { started: false, error: message, base_url: BASE_URL };
|
|
545
879
|
output(report, true);
|
|
546
880
|
process.exit(1);
|
|
@@ -557,6 +891,7 @@ async function cmdSetup(flags: Record<string, string | boolean>): Promise<void>
|
|
|
557
891
|
export const CLI_REFERENCE = {
|
|
558
892
|
commands: [
|
|
559
893
|
{ name: "health", usage: "", desc: "Server health check" },
|
|
894
|
+
{ name: "mcp", usage: "[--no-auto-start]", desc: "Run the stdio MCP server" },
|
|
560
895
|
{ name: "setup", usage: "[--opencode auto|global|project|off] [--no-start]", desc: "Bootstrap browser deps + Open Code command" },
|
|
561
896
|
{ name: "resolve", usage: '--intent "..." --url "..." [opts]', desc: "Resolve intent → search/capture/execute" },
|
|
562
897
|
{ name: "execute", usage: "--skill ID --endpoint ID [opts]", desc: "Execute a specific endpoint" },
|
|
@@ -568,7 +903,8 @@ export const CLI_REFERENCE = {
|
|
|
568
903
|
{ name: "skill", usage: "<id>", desc: "Get skill details" },
|
|
569
904
|
{ name: "search", usage: '--intent "..." [--domain "..."]', desc: "Search marketplace" },
|
|
570
905
|
{ name: "sessions", usage: '--domain "..." [--limit N]', desc: "Debug session logs" },
|
|
571
|
-
{ name: "go", usage: '<url>', desc: "
|
|
906
|
+
{ name: "go", usage: '<url>', desc: "Open a live Kuri browser tab for capture-first workflows" },
|
|
907
|
+
{ name: "submit", usage: "[--form-selector sel] [--submit-selector sel] [--wait-for hint]", desc: "Submit current form, auto-flush current capture, and fall back to same-origin rehydrate for JS-heavy flows" },
|
|
572
908
|
{ name: "snap", usage: "[--filter interactive]", desc: "A11y snapshot with @eN refs" },
|
|
573
909
|
{ name: "click", usage: "<ref>", desc: "Click element by ref (e.g. e5)" },
|
|
574
910
|
{ name: "fill", usage: "<ref> <value>", desc: "Fill input by ref" },
|
|
@@ -583,6 +919,7 @@ export const CLI_REFERENCE = {
|
|
|
583
919
|
{ name: "eval", usage: "<expression>", desc: "Evaluate JavaScript" },
|
|
584
920
|
{ name: "back", usage: "", desc: "Navigate back" },
|
|
585
921
|
{ name: "forward", usage: "", desc: "Navigate forward" },
|
|
922
|
+
{ name: "sync", usage: "", desc: "Flush the current step's captured traffic into route cache without closing tab" },
|
|
586
923
|
{ name: "close", usage: "", desc: "Close browse session, flush + index traffic" },
|
|
587
924
|
],
|
|
588
925
|
globalFlags: [
|
|
@@ -604,8 +941,13 @@ export const CLI_REFERENCE = {
|
|
|
604
941
|
],
|
|
605
942
|
examples: [
|
|
606
943
|
"unbrowse setup",
|
|
944
|
+
"unbrowse mcp",
|
|
607
945
|
'unbrowse resolve --intent "top stories" --url "https://news.ycombinator.com" --execute',
|
|
608
946
|
'unbrowse resolve --intent "get timeline" --url "https://x.com"',
|
|
947
|
+
'unbrowse go "https://www.mandai.com/en/ticketing/admission-and-rides/parks-selection.html"',
|
|
948
|
+
'unbrowse snap --filter interactive',
|
|
949
|
+
'unbrowse submit --wait-for "/time-selection.html"',
|
|
950
|
+
'unbrowse sync',
|
|
609
951
|
"unbrowse execute --skill abc --endpoint def --pretty",
|
|
610
952
|
"unbrowse execute --skill abc --endpoint def --schema --pretty",
|
|
611
953
|
'unbrowse execute --skill abc --endpoint def --path "data.items[]" --extract "name,url" --limit 10 --pretty',
|
|
@@ -648,6 +990,24 @@ function printHelp(): void {
|
|
|
648
990
|
lines.push(` ${e}`);
|
|
649
991
|
}
|
|
650
992
|
|
|
993
|
+
lines.push(
|
|
994
|
+
"",
|
|
995
|
+
"Browser workflow:",
|
|
996
|
+
" 1. go -> open the live tab you want to work in",
|
|
997
|
+
" 2. snap -> inspect refs and confirm the page state",
|
|
998
|
+
" 3. click/fill/eval -> set real page state",
|
|
999
|
+
" 4. submit -> prefer DOM submit; auto-flush current capture; fall back to same-origin rehydrate",
|
|
1000
|
+
" 5. sync -> flush any additional captured routes after a successful step",
|
|
1001
|
+
" 6. close -> finish capture + indexing",
|
|
1002
|
+
);
|
|
1003
|
+
|
|
1004
|
+
lines.push(
|
|
1005
|
+
"",
|
|
1006
|
+
"JS-heavy forms:",
|
|
1007
|
+
" Prefer real calendar/time clicks before submit.",
|
|
1008
|
+
" If the UI is flaky, inspect hidden inputs/cookies with eval, then submit the real form.",
|
|
1009
|
+
);
|
|
1010
|
+
|
|
651
1011
|
lines.push("");
|
|
652
1012
|
process.stderr.write(lines.join("\n"));
|
|
653
1013
|
}
|
|
@@ -699,6 +1059,35 @@ async function cmdUpgrade(flags: Record<string, string | boolean>): Promise<void
|
|
|
699
1059
|
}
|
|
700
1060
|
}
|
|
701
1061
|
|
|
1062
|
+
async function cmdMcp(flags: Record<string, string | boolean>): Promise<void> {
|
|
1063
|
+
const entrypoint = resolveSiblingEntrypoint(import.meta.url, "mcp");
|
|
1064
|
+
const child = spawn(
|
|
1065
|
+
process.execPath,
|
|
1066
|
+
[...runtimeArgsForEntrypoint(import.meta.url, entrypoint), ...(flags["no-auto-start"] ? ["--no-auto-start"] : [])],
|
|
1067
|
+
{
|
|
1068
|
+
cwd: process.cwd(),
|
|
1069
|
+
stdio: "inherit",
|
|
1070
|
+
env: {
|
|
1071
|
+
...process.env,
|
|
1072
|
+
MCP_SERVER_MODE: "1",
|
|
1073
|
+
},
|
|
1074
|
+
},
|
|
1075
|
+
);
|
|
1076
|
+
|
|
1077
|
+
const code = await new Promise<number>((resolve, reject) => {
|
|
1078
|
+
child.once("error", reject);
|
|
1079
|
+
child.once("exit", (exitCode, signal) => {
|
|
1080
|
+
if (signal) {
|
|
1081
|
+
process.kill(process.pid, signal);
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
resolve(exitCode ?? 1);
|
|
1085
|
+
});
|
|
1086
|
+
});
|
|
1087
|
+
|
|
1088
|
+
if (code !== 0) process.exit(code);
|
|
1089
|
+
}
|
|
1090
|
+
|
|
702
1091
|
// ---------------------------------------------------------------------------
|
|
703
1092
|
// Site/task shortcut commands
|
|
704
1093
|
// ---------------------------------------------------------------------------
|
|
@@ -851,6 +1240,18 @@ async function cmdGo(args: string[], flags: Record<string, string | boolean>): P
|
|
|
851
1240
|
output(await api("POST", "/v1/browse/go", { url }), !!flags.pretty);
|
|
852
1241
|
}
|
|
853
1242
|
|
|
1243
|
+
async function cmdSubmit(flags: Record<string, string | boolean>): Promise<void> {
|
|
1244
|
+
const body: Record<string, unknown> = {};
|
|
1245
|
+
if (typeof flags["form-selector"] === "string") body.form_selector = flags["form-selector"];
|
|
1246
|
+
if (typeof flags["submit-selector"] === "string") body.submit_selector = flags["submit-selector"];
|
|
1247
|
+
if (typeof flags["wait-for"] === "string") body.wait_for = flags["wait-for"];
|
|
1248
|
+
if (typeof flags["timeout-ms"] === "string") body.timeout_ms = Number(flags["timeout-ms"]);
|
|
1249
|
+
if (flags["same-origin-fetch-fallback"] !== undefined) {
|
|
1250
|
+
body.same_origin_fetch_fallback = flags["same-origin-fetch-fallback"] !== "false";
|
|
1251
|
+
}
|
|
1252
|
+
output(await api("POST", "/v1/browse/submit", body), !!flags.pretty);
|
|
1253
|
+
}
|
|
1254
|
+
|
|
854
1255
|
async function cmdSnap(flags: Record<string, string | boolean>): Promise<void> {
|
|
855
1256
|
const filter = flags.filter as string | undefined;
|
|
856
1257
|
const result = await api("POST", "/v1/browse/snap", { filter }) as { snapshot?: string };
|
|
@@ -938,6 +1339,10 @@ async function cmdForward(): Promise<void> {
|
|
|
938
1339
|
output(await api("POST", "/v1/browse/forward"), false);
|
|
939
1340
|
}
|
|
940
1341
|
|
|
1342
|
+
async function cmdSync(flags: Record<string, string | boolean>): Promise<void> {
|
|
1343
|
+
output(await api("POST", "/v1/browse/sync"), !!flags.pretty);
|
|
1344
|
+
}
|
|
1345
|
+
|
|
941
1346
|
async function cmdClose(): Promise<void> {
|
|
942
1347
|
output(await api("POST", "/v1/browse/close"), false);
|
|
943
1348
|
}
|
|
@@ -1010,6 +1415,7 @@ async function main(): Promise<void> {
|
|
|
1010
1415
|
}
|
|
1011
1416
|
|
|
1012
1417
|
// Server lifecycle commands (don't need ensureLocalServer)
|
|
1418
|
+
if (command === "mcp") return cmdMcp(flags);
|
|
1013
1419
|
if (command === "status") return cmdStatus(flags);
|
|
1014
1420
|
if (command === "stop") { cmdStop(flags); return; }
|
|
1015
1421
|
if (command === "restart") return cmdRestart(flags);
|
|
@@ -1018,11 +1424,11 @@ async function main(): Promise<void> {
|
|
|
1018
1424
|
|
|
1019
1425
|
// --- Shortcut resolution: unbrowse <site> [task] [flags] ---
|
|
1020
1426
|
const KNOWN_COMMANDS = new Set([
|
|
1021
|
-
"health", "setup", "resolve", "execute", "exec",
|
|
1427
|
+
"health", "mcp", "setup", "resolve", "execute", "exec",
|
|
1022
1428
|
"feedback", "fb", "review", "publish", "login", "skills", "skill", "search", "sessions",
|
|
1023
1429
|
"status", "stop", "restart", "upgrade", "update",
|
|
1024
|
-
"go", "snap", "click", "fill", "type", "press", "select", "scroll",
|
|
1025
|
-
"screenshot", "text", "markdown", "cookies", "eval", "back", "forward", "close",
|
|
1430
|
+
"go", "submit", "snap", "click", "fill", "type", "press", "select", "scroll",
|
|
1431
|
+
"screenshot", "text", "markdown", "cookies", "eval", "back", "forward", "sync", "close",
|
|
1026
1432
|
"connect-chrome",
|
|
1027
1433
|
]);
|
|
1028
1434
|
|
|
@@ -1049,6 +1455,7 @@ async function main(): Promise<void> {
|
|
|
1049
1455
|
|
|
1050
1456
|
switch (command) {
|
|
1051
1457
|
case "health": return cmdHealth(flags);
|
|
1458
|
+
case "mcp": return cmdMcp(flags);
|
|
1052
1459
|
case "setup": return cmdSetup(flags);
|
|
1053
1460
|
case "resolve": return cmdResolve(flags);
|
|
1054
1461
|
case "execute": case "exec": return cmdExecute(flags);
|
|
@@ -1062,6 +1469,7 @@ async function main(): Promise<void> {
|
|
|
1062
1469
|
case "sessions": return cmdSessions(flags);
|
|
1063
1470
|
// Browse commands — Kuri browser actions with passive indexing
|
|
1064
1471
|
case "go": return cmdGo(args, flags);
|
|
1472
|
+
case "submit": return cmdSubmit(flags);
|
|
1065
1473
|
case "snap": return cmdSnap(flags);
|
|
1066
1474
|
case "click": return cmdClick(args);
|
|
1067
1475
|
case "fill": return cmdFill(args);
|
|
@@ -1076,6 +1484,7 @@ async function main(): Promise<void> {
|
|
|
1076
1484
|
case "eval": return cmdEval(args, flags);
|
|
1077
1485
|
case "back": return cmdBack();
|
|
1078
1486
|
case "forward": return cmdForward();
|
|
1487
|
+
case "sync": return cmdSync(flags);
|
|
1079
1488
|
case "close": return cmdClose();
|
|
1080
1489
|
case "connect-chrome": return cmdConnectChrome();
|
|
1081
1490
|
default: info(`Unknown command: ${command}`); printHelp(); process.exit(1);
|