unbrowse 2.12.2 → 2.12.7

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.
Files changed (64) hide show
  1. package/README.md +8 -44
  2. package/dist/cli.js +514 -20723
  3. package/package.json +4 -10
  4. package/runtime-src/api/routes.ts +15 -801
  5. package/runtime-src/auth/index.ts +32 -142
  6. package/runtime-src/capture/index.ts +101 -436
  7. package/runtime-src/cli.ts +371 -956
  8. package/runtime-src/client/index.ts +29 -622
  9. package/runtime-src/execution/index.ts +85 -345
  10. package/runtime-src/graph/index.ts +10 -128
  11. package/runtime-src/intent-match.ts +27 -27
  12. package/runtime-src/kuri/client.ts +82 -543
  13. package/runtime-src/orchestrator/index.ts +462 -2246
  14. package/runtime-src/reverse-engineer/index.ts +22 -220
  15. package/runtime-src/runtime/local-server.ts +16 -149
  16. package/runtime-src/runtime/paths.ts +5 -9
  17. package/runtime-src/runtime/setup.ts +1 -52
  18. package/runtime-src/server.ts +11 -6
  19. package/runtime-src/transform/schema-hints.ts +358 -0
  20. package/runtime-src/types/skill.ts +2 -49
  21. package/runtime-src/verification/index.ts +0 -15
  22. package/runtime-src/version.ts +13 -13
  23. package/vendor/kuri/darwin-arm64/kuri +0 -0
  24. package/vendor/kuri/darwin-x64/kuri +0 -0
  25. package/vendor/kuri/linux-arm64/kuri +0 -0
  26. package/vendor/kuri/linux-x64/kuri +0 -0
  27. package/bin/unbrowse-wrapper.mjs +0 -39
  28. package/bin/unbrowse.js +0 -38
  29. package/runtime-src/analytics-session.ts +0 -33
  30. package/runtime-src/api/browse-index.ts +0 -254
  31. package/runtime-src/api/browse-session.ts +0 -179
  32. package/runtime-src/api/browse-submit.ts +0 -455
  33. package/runtime-src/auth/runtime.ts +0 -116
  34. package/runtime-src/browser/index.ts +0 -635
  35. package/runtime-src/browser/types.ts +0 -41
  36. package/runtime-src/capture/prefetch.ts +0 -122
  37. package/runtime-src/capture/rsc.ts +0 -45
  38. package/runtime-src/cli/shortcuts.ts +0 -273
  39. package/runtime-src/client/graph-client.ts +0 -99
  40. package/runtime-src/execution/robots.ts +0 -167
  41. package/runtime-src/execution/search-forms.ts +0 -188
  42. package/runtime-src/graph/planner.ts +0 -411
  43. package/runtime-src/graph/session.ts +0 -294
  44. package/runtime-src/graph/trace-store.ts +0 -136
  45. package/runtime-src/indexer/index.ts +0 -480
  46. package/runtime-src/orchestrator/browser-agent.ts +0 -374
  47. package/runtime-src/orchestrator/dag-advisor.ts +0 -59
  48. package/runtime-src/orchestrator/dag-feedback.ts +0 -256
  49. package/runtime-src/orchestrator/first-pass-action.ts +0 -362
  50. package/runtime-src/orchestrator/passive-publish.ts +0 -152
  51. package/runtime-src/orchestrator/timing-economics.ts +0 -80
  52. package/runtime-src/payments/cascade.ts +0 -137
  53. package/runtime-src/payments/index.ts +0 -268
  54. package/runtime-src/payments/wallet.ts +0 -33
  55. package/runtime-src/reverse-engineer/description-prompt.ts +0 -132
  56. package/runtime-src/router.ts +0 -17
  57. package/runtime-src/runtime/browser-access.ts +0 -11
  58. package/runtime-src/runtime/browser-host.ts +0 -48
  59. package/runtime-src/runtime/lifecycle.ts +0 -17
  60. package/runtime-src/runtime/supervisor.ts +0 -69
  61. package/runtime-src/single-binary.ts +0 -141
  62. package/runtime-src/telemetry.ts +0 -253
  63. package/runtime-src/verification/matrix.ts +0 -30
  64. package/scripts/postinstall.mjs +0 -81
@@ -8,19 +8,9 @@
8
8
  */
9
9
 
10
10
  import { config as loadEnv } from "dotenv";
11
- import {
12
- detectTelemetryHostType,
13
- ensureCliInstallTracked,
14
- ensureRegistered,
15
- getApiKey,
16
- recordFunnelTelemetryEvent,
17
- recordInstallTelemetryEvent,
18
- } from "./client/index.js";
19
- import { findSitePack, findTask, allSitePacks, buildDepsGraph, planExecution, buildDepsMetadata, type SitePack } from "./cli/shortcuts.js";
20
- import { ensureLocalServer, checkServerVersion, stopServer, restartServer } from "./runtime/local-server.js";
11
+ import { ensureRegistered, getApiKey } from "./client/index.js";
12
+ import { ensureLocalServer } from "./runtime/local-server.js";
21
13
  import { isMainModule } from "./runtime/paths.js";
22
- import { drainPendingIndexJobs } from "./indexer/index.js";
23
- import { drainPendingPassivePublishes } from "./orchestrator/passive-publish.js";
24
14
  import { runSetup, type SetupReport, type SetupScope } from "./runtime/setup.js";
25
15
 
26
16
  loadEnv({ quiet: true });
@@ -92,28 +82,6 @@ function info(msg: string): void {
92
82
  process.stderr.write(`[unbrowse] ${msg}\n`);
93
83
  }
94
84
 
95
- function resolveResultError(result: Record<string, unknown>): string | undefined {
96
- return (result.result as Record<string, unknown> | undefined)?.error as string | undefined
97
- ?? result.error as string | undefined;
98
- }
99
-
100
- function resolveLoginUrl(result: Record<string, unknown>, fallbackUrl?: string): string {
101
- return (result.result as Record<string, unknown> | undefined)?.login_url as string
102
- ?? fallbackUrl
103
- ?? "";
104
- }
105
-
106
- function hasIndexingFallback(result: Record<string, unknown>): boolean {
107
- return (result.result as Record<string, unknown> | undefined)?.indexing_fallback_available === true;
108
- }
109
-
110
- function isResolveSuccessResult(result: Record<string, unknown>): boolean {
111
- const resultObj = result.result as Record<string, unknown> | undefined;
112
- if (resolveResultError(result)) return false;
113
- if (resultObj?.status === "browse_session_open") return false;
114
- return !!result.result || Array.isArray(result.available_endpoints);
115
- }
116
-
117
85
  async function withPendingNotice<T>(promise: Promise<T>, message: string, delayMs = 3_000): Promise<T> {
118
86
  let done = false;
119
87
  const timer = setTimeout(() => {
@@ -135,9 +103,210 @@ function normalizeSetupScope(value: string | boolean | undefined): SetupScope {
135
103
  }
136
104
 
137
105
  // ---------------------------------------------------------------------------
138
- // Slim outputkeep only essential trace metadata + result
106
+ // Path resolutiondrill into nested structures with [] array expansion
139
107
  // ---------------------------------------------------------------------------
140
108
 
109
+ /** Build entityUrn → object index for normalized APIs (LinkedIn, Facebook, etc.) */
110
+ function buildEntityIndex(items: unknown[]): Map<string, unknown> {
111
+ const index = new Map<string, unknown>();
112
+ for (const item of items) {
113
+ if (item != null && typeof item === "object") {
114
+ const urn = (item as Record<string, unknown>).entityUrn;
115
+ if (typeof urn === "string") index.set(urn, item);
116
+ }
117
+ }
118
+ return index;
119
+ }
120
+
121
+ /** Detect if an object contains a normalized entity array and build the index.
122
+ * Searches all top-level and one-level-nested arrays for entityUrn-keyed items,
123
+ * picking the largest qualifying array. Works for any normalized API shape. */
124
+ function detectEntityIndex(data: unknown): Map<string, unknown> | null {
125
+ if (data == null || typeof data !== "object") return null;
126
+
127
+ let best: unknown[] | null = null;
128
+
129
+ const check = (arr: unknown[]) => {
130
+ if (arr.length < 2) return;
131
+ const sample = arr.slice(0, 10);
132
+ const withUrn = sample.filter(
133
+ (i) => i != null && typeof i === "object" && typeof (i as Record<string, unknown>).entityUrn === "string"
134
+ ).length;
135
+ if (withUrn >= sample.length * 0.5 && (!best || arr.length > best.length)) {
136
+ best = arr;
137
+ }
138
+ };
139
+
140
+ const obj = data as Record<string, unknown>;
141
+ for (const val of Object.values(obj)) {
142
+ if (Array.isArray(val)) {
143
+ check(val);
144
+ } else if (val != null && typeof val === "object" && !Array.isArray(val)) {
145
+ // One level deep: { data: { included: [...] } }, { response: { entities: [...] } }, etc.
146
+ for (const nested of Object.values(val as Record<string, unknown>)) {
147
+ if (Array.isArray(nested)) check(nested);
148
+ }
149
+ }
150
+ }
151
+
152
+ return best ? buildEntityIndex(best) : null;
153
+ }
154
+
155
+ /** Resolve a dot-path like "data.items[].name" against an object.
156
+ * When entityIndex is provided, transparently follows *-prefixed URN references. */
157
+ function resolvePath(obj: unknown, path: string, entityIndex?: Map<string, unknown> | null): unknown {
158
+ if (!path || obj == null) return obj;
159
+ const segments = path.split(".");
160
+ let cur: unknown = obj;
161
+ for (let i = 0; i < segments.length; i++) {
162
+ if (cur == null) return undefined;
163
+ const seg = segments[i];
164
+ if (seg.endsWith("[]")) {
165
+ const key = seg.slice(0, -2);
166
+ const arr = key ? (cur as Record<string, unknown>)[key] : cur;
167
+ if (!Array.isArray(arr)) return undefined;
168
+ const remaining = segments.slice(i + 1).join(".");
169
+ if (!remaining) return arr;
170
+ return arr.flatMap((item) => {
171
+ const v = resolvePath(item, remaining, entityIndex);
172
+ return v === undefined ? [] : Array.isArray(v) ? v : [v];
173
+ });
174
+ }
175
+ const rec = cur as Record<string, unknown>;
176
+ let val = rec[seg];
177
+
178
+ // URN reference resolution: if direct lookup fails (or is null), check for "*key" reference.
179
+ // Normalized APIs (LinkedIn Voyager, Facebook Graph) set inline fields to null when
180
+ // the value is stored as a URN reference: e.g. socialDetail: null + *socialDetail: "urn:li:..."
181
+ if (val == null && entityIndex) {
182
+ const ref = rec[`*${seg}`];
183
+ if (typeof ref === "string") {
184
+ val = entityIndex.get(ref);
185
+ }
186
+ }
187
+
188
+ cur = val;
189
+ }
190
+ return cur;
191
+ }
192
+
193
+ /** Apply --extract fields to data. Each field is "alias:deep.path" or just "field".
194
+ * When processing arrays, rows where ALL extracted fields are null/undefined/empty are dropped.
195
+ * This handles decorator-pattern APIs (e.g. LinkedIn included[]) where heterogeneous
196
+ * item types coexist and only some items match the requested fields. */
197
+ function extractFields(data: unknown, fields: string[], entityIndex?: Map<string, unknown> | null): unknown {
198
+ if (data == null) return data;
199
+
200
+ function mapItem(item: unknown): Record<string, unknown> {
201
+ const out: Record<string, unknown> = {};
202
+ for (const f of fields) {
203
+ const colonIdx = f.indexOf(":");
204
+ const alias = colonIdx >= 0 ? f.slice(0, colonIdx) : f.split(".").pop()!;
205
+ const path = colonIdx >= 0 ? f.slice(colonIdx + 1) : f;
206
+ const resolved = resolvePath(item, path, entityIndex ?? undefined) ?? [];
207
+ // Unwrap single-element arrays to scalar values
208
+ out[alias] = Array.isArray(resolved)
209
+ ? resolved.length === 0
210
+ ? null
211
+ : resolved.length === 1
212
+ ? resolved[0]
213
+ : resolved
214
+ : resolved;
215
+ }
216
+ return out;
217
+ }
218
+
219
+ /** Check if a value is "present" (non-null, non-empty) */
220
+ function hasValue(v: unknown): boolean {
221
+ if (v == null) return false;
222
+ if (Array.isArray(v)) return v.length > 0;
223
+ return true;
224
+ }
225
+
226
+ if (Array.isArray(data)) {
227
+ return data.map(mapItem).filter((row) => Object.values(row).some(hasValue));
228
+ }
229
+ return mapItem(data);
230
+ }
231
+
232
+ function hasMeaningfulValue(value: unknown): boolean {
233
+ if (value == null) return false;
234
+ if (typeof value === "string") return value.trim().length > 0;
235
+ if (typeof value === "number" || typeof value === "boolean") return true;
236
+ if (Array.isArray(value)) return value.some((item) => hasMeaningfulValue(item));
237
+ if (typeof value === "object") return Object.values(value as Record<string, unknown>).some((item) => hasMeaningfulValue(item));
238
+ return false;
239
+ }
240
+
241
+ function isPlainRecord(value: unknown): value is Record<string, unknown> {
242
+ return value != null && typeof value === "object" && !Array.isArray(value);
243
+ }
244
+
245
+ function isScalarLike(value: unknown): boolean {
246
+ if (value == null) return false;
247
+ if (typeof value === "string") return value.trim().length > 0;
248
+ if (typeof value === "number" || typeof value === "boolean") return true;
249
+ if (Array.isArray(value)) {
250
+ return value.length > 0 && value.every((item) => item == null || typeof item === "string" || typeof item === "number" || typeof item === "boolean");
251
+ }
252
+ return false;
253
+ }
254
+
255
+ function looksStructuredForDirectOutput(value: unknown): boolean {
256
+ if (Array.isArray(value)) {
257
+ const sample = value.filter(isPlainRecord).slice(0, 3);
258
+ if (sample.length === 0) return false;
259
+ const simpleRows = sample.filter((row) => {
260
+ const keys = Object.keys(row);
261
+ const scalarFields = Object.values(row).filter(isScalarLike).length;
262
+ return keys.length > 0 && keys.length <= 20 && scalarFields >= 2;
263
+ });
264
+ return simpleRows.length >= Math.ceil(sample.length / 2);
265
+ }
266
+
267
+ if (!isPlainRecord(value)) return false;
268
+ const keys = Object.keys(value);
269
+ if (keys.length === 0 || keys.length > 20) return false;
270
+ const scalarFields = Object.values(value).filter(isScalarLike).length;
271
+ return scalarFields >= 2;
272
+ }
273
+
274
+ /** Apply --path, --extract, --limit to a result object. */
275
+ function applyTransforms(result: unknown, flags: Record<string, string | boolean>): unknown {
276
+ let data = result;
277
+
278
+ // Build entity index from the full response before drilling into it
279
+ const entityIndex = detectEntityIndex(result);
280
+
281
+ // --path: drill into nested structure
282
+ const pathFlag = flags.path as string | undefined;
283
+ if (pathFlag) {
284
+ data = resolvePath(data, pathFlag, entityIndex);
285
+ if (data === undefined) {
286
+ // Path didn't match — warn so the user knows to fix it
287
+ process.stderr.write(`[unbrowse] warning: --path "${pathFlag}" resolved to undefined. Check path against response structure.\n`);
288
+ return [];
289
+ }
290
+ }
291
+
292
+ // --extract: pick specific fields (with entity index for URN resolution)
293
+ const extractFlag = flags.extract as string | undefined;
294
+ if (extractFlag) {
295
+ const fields = extractFlag.split(",").map((f) => f.trim());
296
+ data = extractFields(data, fields, entityIndex);
297
+ }
298
+
299
+ // --limit: cap array output
300
+ const limitFlag = flags.limit as string | undefined;
301
+ if (limitFlag && Array.isArray(data)) {
302
+ data = data.slice(0, Number(limitFlag));
303
+ }
304
+
305
+ return data;
306
+ }
307
+
308
+ /** Slim down output when transforms are applied — keep only essential trace metadata
309
+ * and drop the response_schema (it's noise once extraction is done). */
141
310
  function slimTrace(obj: Record<string, unknown>): Record<string, unknown> {
142
311
  const trace = obj.trace as Record<string, unknown> | undefined;
143
312
  const out: Record<string, unknown> = {
@@ -153,434 +322,207 @@ function slimTrace(obj: Record<string, unknown>): Record<string, unknown> {
153
322
  }
154
323
  : undefined,
155
324
  };
325
+ // Carry over result (even if empty array — don't silently drop it)
156
326
  if ("result" in obj) out.result = obj.result;
157
- if (obj.available_endpoints) out.available_endpoints = obj.available_endpoints;
158
- if (obj.source) out.source = obj.source;
159
- if (obj.skill) out.skill = obj.skill;
327
+ // Keep extraction_hints agents need them for --path/--extract guidance
328
+ if (obj.extraction_hints) out.extraction_hints = obj.extraction_hints;
160
329
  return out;
161
330
  }
162
331
 
332
+ /** When a response is large and has extraction_hints, replace the full result
333
+ * with a compact summary + the hints so agents know how to extract. */
334
+ function wrapWithHints(obj: Record<string, unknown>): Record<string, unknown> {
335
+ const hints = obj.extraction_hints as { path: string; fields: string[]; item_field_count: number; confidence: string; cli_args?: string; schema_tree?: Record<string, string> } | undefined;
336
+ if (!hints) return obj;
163
337
 
164
- async function cmdHealth(flags: Record<string, string | boolean>): Promise<void> {
165
- output(await api("GET", "/health"), !!flags.pretty);
166
- }
338
+ const resultStr = JSON.stringify(obj.result ?? "");
339
+ // Only wrap when response is large enough that raw output would overwhelm context
340
+ if (resultStr.length < 2000) return obj;
167
341
 
168
- async function cmdResolve(flags: Record<string, string | boolean>): Promise<void> {
169
- const intent = flags.intent as string;
170
- if (!intent) die("--intent is required");
171
- const hostType = detectTelemetryHostType();
172
- await ensureCliInstallTracked(hostType);
173
- await recordFunnelTelemetryEvent("cli_invoked", {
174
- source: "cli",
175
- hostType,
176
- properties: { command: "resolve" },
177
- });
178
- await recordFunnelTelemetryEvent("resolve_started", {
179
- source: "cli",
180
- hostType,
181
- properties: {
182
- command: "resolve",
183
- has_url: typeof flags.url === "string",
184
- has_domain: typeof flags.domain === "string",
185
- auto_execute: !!flags.execute,
186
- },
187
- });
342
+ const trace = obj.trace as Record<string, unknown> | undefined;
188
343
 
189
- try {
190
- const body: Record<string, unknown> = { intent };
191
- const url = flags.url as string | undefined;
192
- const domain = flags.domain as string | undefined;
193
- const explicitEndpointId = flags["endpoint-id"] as string | undefined;
194
- const autoExecute = !!flags.execute;
195
- const extraParams = flags.params ? JSON.parse(flags.params as string) : {};
196
-
197
- if (url) {
198
- body.params = { url };
199
- body.context = { url };
200
- }
201
- if (domain) {
202
- body.context = { ...(body.context as Record<string, unknown> ?? {}), domain };
203
- }
204
- if (explicitEndpointId) {
205
- body.params = { ...(body.params as Record<string, unknown> ?? {}), endpoint_id: explicitEndpointId };
206
- }
207
- if (flags.params) {
208
- body.params = { ...(body.params as Record<string, unknown> ?? {}), ...extraParams };
209
- }
210
- if (flags["dry-run"]) body.dry_run = true;
211
- if (flags["force-capture"]) body.force_capture = true;
212
- body.projection = { raw: true };
344
+ return {
345
+ trace: trace
346
+ ? {
347
+ trace_id: trace.trace_id,
348
+ skill_id: trace.skill_id,
349
+ endpoint_id: trace.endpoint_id,
350
+ success: trace.success,
351
+ status_code: trace.status_code,
352
+ }
353
+ : undefined,
354
+ _response_too_large: `${resultStr.length} bytes use extraction flags below to get structured data`,
355
+ extraction_hints: hints,
356
+ };
357
+ }
213
358
 
214
- function execBody(endpointId: string): Record<string, unknown> {
215
- return { params: { endpoint_id: endpointId, ...extraParams }, intent, projection: { raw: true } };
216
- }
359
+ /** When --schema is used, return only the schema tree + extraction hints */
360
+ function schemaOnly(obj: Record<string, unknown>): Record<string, unknown> {
361
+ const trace = obj.trace as Record<string, unknown> | undefined;
362
+ return {
363
+ trace: trace
364
+ ? { trace_id: trace.trace_id, skill_id: trace.skill_id, endpoint_id: trace.endpoint_id, success: trace.success }
365
+ : undefined,
366
+ extraction_hints: obj.extraction_hints ?? null,
367
+ response_schema: obj.response_schema ?? null,
368
+ };
369
+ }
217
370
 
218
- function resolveSkillId(): string | undefined {
219
- return (result.skill as Record<string, unknown>)?.skill_id as string
220
- ?? (result as Record<string, unknown>).skill_id as string;
221
- }
371
+ /** Auto-extract when hints have high confidence, otherwise wrap with hints.
372
+ * This is the "right first try" path — agents get clean data without a second call. */
373
+ function autoExtractOrWrap(obj: Record<string, unknown>): Record<string, unknown> {
374
+ const hints = obj.extraction_hints as { path: string; fields: string[]; confidence: string; cli_args?: string; schema_tree?: Record<string, string> } | undefined;
375
+ const resultStr = JSON.stringify(obj.result ?? "");
222
376
 
223
- const startedAt = Date.now();
224
- 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>> {
225
- return withPendingNotice(
226
- api("POST", "/v1/intent/resolve", body) as Promise<Record<string, unknown>>,
227
- message,
228
- );
229
- }
377
+ // Small responses: return as-is
378
+ if (resultStr.length < 2000) return obj;
230
379
 
231
- let result = await resolveOnce();
232
- let attemptedForceCapture = !!body.force_capture;
233
- let attemptedCookieImport = false;
234
- let attemptedInteractiveLogin = false;
235
-
236
- while (true) {
237
- const resultError = resolveResultError(result);
238
- if (resultError === "payment_required" && hasIndexingFallback(result) && url && !attemptedForceCapture) {
239
- attemptedForceCapture = true;
240
- body.force_capture = true;
241
- info("Marketplace search is paid here. Falling back to free live capture on the exact URL...");
242
- result = await resolveOnce("Running free live capture...");
243
- continue;
244
- }
380
+ // Server-side intent projection can already return clean rows while raw endpoint
381
+ // hints still describe the pre-projection payload. Preserve the structured rows.
382
+ if (looksStructuredForDirectOutput(obj.result)) {
383
+ return slimTrace({ ...obj, extraction_hints: undefined, response_schema: undefined });
384
+ }
245
385
 
246
- if (resultError === "auth_required") {
247
- const loginUrl = resolveLoginUrl(result, url);
248
- if (!loginUrl) break;
249
-
250
- if (!attemptedCookieImport) {
251
- attemptedCookieImport = true;
252
- info("Site requires authentication. Trying browser cookie import first...");
253
- const stealResult = await api("POST", "/v1/auth/steal", { url: loginUrl }) as Record<string, unknown>;
254
- const cookiesStored = typeof stealResult.cookies_stored === "number"
255
- ? stealResult.cookies_stored
256
- : Number(stealResult.cookies_stored ?? 0);
257
- if (stealResult.success === true && cookiesStored > 0) {
258
- info(`Imported ${cookiesStored} browser cookies. Retrying...`);
259
- result = await resolveOnce("Retrying after browser cookie import...");
260
- continue;
261
- }
262
- }
386
+ // No hints: can't auto-extract, return as-is (raw will be big but we have no better option)
387
+ if (!hints) return obj;
263
388
 
264
- if (!attemptedInteractiveLogin) {
265
- attemptedInteractiveLogin = true;
266
- info("Site requires authentication. Opening browser for login...");
267
- const loginResult = await api("POST", "/v1/auth/login", { url: loginUrl }) as Record<string, unknown>;
268
- if (loginResult.error || loginResult.success === false) {
269
- const message = typeof loginResult.error === "string"
270
- ? loginResult.error
271
- : "interactive login did not produce a reusable session";
272
- throw new Error(`Login failed: ${message}. Run: unbrowse login --url "${loginUrl}"`);
273
- }
274
- info("Login complete. Retrying...");
275
- result = await resolveOnce("Retrying after login...");
276
- continue;
277
- }
278
- }
389
+ // High confidence only: medium confidence still too error-prone for first-try correctness.
390
+ if (hints.confidence === "high") {
391
+ const syntheticFlags: Record<string, string | boolean> = {};
392
+ if (hints.path) syntheticFlags.path = hints.path;
393
+ if (hints.fields.length > 0) syntheticFlags.extract = hints.fields.join(",");
394
+ syntheticFlags.limit = "20";
279
395
 
280
- break;
281
- }
396
+ const extracted = applyTransforms(obj.result, syntheticFlags);
397
+ if (!hasMeaningfulValue(extracted)) return wrapWithHints(obj);
398
+ const slimmed = slimTrace({ ...obj, result: extracted });
282
399
 
283
- // When agent explicitly picked an endpoint but resolve deferred, execute it directly
284
- if (explicitEndpointId && result.available_endpoints) {
285
- const skillId = resolveSkillId();
286
- if (skillId) {
287
- result = await withPendingNotice(
288
- api("POST", `/v1/skills/${skillId}/execute`, execBody(explicitEndpointId)) as Promise<Record<string, unknown>>,
289
- "Executing selected endpoint...",
290
- );
291
- }
292
- }
400
+ // Include the hints so the agent knows what was auto-applied and can adjust
401
+ (slimmed as Record<string, unknown>)._auto_extracted = {
402
+ applied: hints.cli_args,
403
+ confidence: hints.confidence,
404
+ all_fields: hints.schema_tree,
405
+ note: "Auto-extracted using response_schema. Add/remove fields with --extract, change array with --path, or use --raw for full response.",
406
+ };
407
+ return slimmed;
408
+ }
293
409
 
294
- // --execute: auto-pick best endpoint and return data in one step
295
- if (autoExecute && result.available_endpoints && !result.result) {
296
- const endpoints = result.available_endpoints as Array<Record<string, unknown>>;
297
- const skillId = resolveSkillId();
298
- if (skillId && endpoints.length > 0) {
299
- const bestEndpoint = endpoints[0];
300
- info(`Auto-executing endpoint: ${bestEndpoint.description ?? bestEndpoint.endpoint_id}`);
301
- result = await withPendingNotice(
302
- api("POST", `/v1/skills/${skillId}/execute`, execBody(bestEndpoint.endpoint_id as string)) as Promise<Record<string, unknown>>,
303
- "Executing best endpoint...",
304
- );
305
- }
306
- }
410
+ // Low confidence: wrap with hints, let agent decide
411
+ return wrapWithHints(obj);
412
+ }
307
413
 
308
- // Browse session handoff
309
- const resultObj = result.result as Record<string, unknown> | undefined;
310
- if (resultObj?.status === "browse_session_open") {
311
- info(`No cached API. Browser session open on ${resultObj.domain ?? resultObj.url}.`);
312
- info(`Preferred flow: snap -> click/fill/eval -> submit -> sync -> close.`);
313
- info(`Use these commands to get your data:`);
314
- const commands = resultObj.commands as string[] ?? [
315
- "unbrowse snap --filter interactive",
316
- "unbrowse click <ref>",
317
- "unbrowse fill <ref> <value>",
318
- "unbrowse submit --wait-for \"/next-step\"",
319
- "unbrowse sync",
320
- "unbrowse close",
321
- ];
322
- for (const cmd of commands) info(` ${cmd}`);
323
- info(`For JS-heavy forms: prefer real date/time clicks first, inspect hidden inputs with eval when needed, then submit.`);
324
- info(`All traffic is being passively captured. Run "unbrowse close" when done.`);
325
- output(slimTrace(result), !!flags.pretty);
326
- return;
327
- }
414
+ // ---------------------------------------------------------------------------
415
+ // Server lifecycle
416
+ // ---------------------------------------------------------------------------
328
417
 
329
- if (Date.now() - startedAt > 3_000 && result.source === "live-capture") {
330
- info("Live capture finished. Future runs against this site should be much faster.");
331
- }
418
+ // ---------------------------------------------------------------------------
419
+ // Commands
420
+ // ---------------------------------------------------------------------------
332
421
 
333
- if (isResolveSuccessResult(result)) {
334
- await recordFunnelTelemetryEvent("resolve_completed", {
335
- source: "cli",
336
- hostType,
337
- properties: {
338
- command: "resolve",
339
- source: result.source,
340
- auto_execute: autoExecute,
341
- explicit_endpoint: explicitEndpointId ?? null,
342
- },
343
- });
344
- }
422
+ async function cmdHealth(flags: Record<string, string | boolean>): Promise<void> {
423
+ output(await api("GET", "/health"), !!flags.pretty);
424
+ }
345
425
 
346
- result = slimTrace(result);
426
+ async function cmdResolve(flags: Record<string, string | boolean>): Promise<void> {
427
+ const intent = flags.intent as string;
428
+ if (!intent) die("--intent is required");
347
429
 
348
- const skill = result.skill as Record<string, unknown> | undefined;
349
- const trace = result.trace as Record<string, unknown> | undefined;
350
- if (skill?.skill_id && trace) {
351
- (result as Record<string, unknown>)._feedback = `unbrowse feedback --skill ${skill.skill_id} --endpoint ${trace.endpoint_id || "?"} --rating <1-5>`;
352
- }
430
+ const body: Record<string, unknown> = { intent };
431
+ const url = flags.url as string | undefined;
432
+ const domain = flags.domain as string | undefined;
353
433
 
354
- output(result, !!flags.pretty);
355
- } catch (error) {
356
- const message = error instanceof Error ? error.message : String(error);
357
- await recordFunnelTelemetryEvent("resolve_failed", {
358
- source: "cli",
359
- hostType,
360
- properties: {
361
- command: "resolve",
362
- failure_stage: "resolve",
363
- failure_reason: message,
364
- },
365
- });
366
- throw error;
434
+ if (url) {
435
+ body.params = { url };
436
+ body.context = { url };
367
437
  }
368
- }
438
+ if (domain) {
439
+ body.context = { ...(body.context as Record<string, unknown> ?? {}), domain };
440
+ }
441
+ if (flags["endpoint-id"]) {
442
+ body.params = { ...(body.params as Record<string, unknown> ?? {}), endpoint_id: flags["endpoint-id"] };
443
+ }
444
+ if (flags.params) {
445
+ body.params = { ...(body.params as Record<string, unknown> ?? {}), ...JSON.parse(flags.params as string) };
446
+ }
447
+ if (flags["dry-run"]) body.dry_run = true;
448
+ if (flags["force-capture"]) body.force_capture = true;
449
+ // When explicit CLI transforms are present, get raw data for client-side extraction
450
+ const hasTransforms = !!(flags.path || flags.extract);
451
+ if (flags.raw || hasTransforms) body.projection = { raw: true };
369
452
 
370
- // ---------------------------------------------------------------------------
371
- // Post-processing helpers for --path, --extract, --limit, --schema
372
- // ---------------------------------------------------------------------------
453
+ const startedAt = Date.now();
454
+ let result = await withPendingNotice(
455
+ api("POST", "/v1/intent/resolve", body) as Promise<Record<string, unknown>>,
456
+ "Still working. First-time capture/indexing for a site can take 20-80s. Waiting is usually better than falling back.",
457
+ );
373
458
 
374
- /** Drill into a value using a dot-path like "data.items[].name".
375
- * `[]` flattens arrays at that level so nested arrays become flat collections. */
376
- function drillPath(data: unknown, path: string): unknown {
377
- const segments = path.split(/\./).flatMap((s) => {
378
- // "items[]" → ["items", "[]"]
379
- const m = s.match(/^(.+)\[\]$/);
380
- return m ? [m[1], "[]"] : [s];
381
- });
382
- // Work with an array of "current values" to handle multi-level flattening
383
- let values: unknown[] = [data];
384
- for (const seg of segments) {
385
- if (values.length === 0) return [];
386
- if (seg === "[]") {
387
- // Flatten: each value that is an array gets its elements spread out
388
- values = values.flatMap((v) => (Array.isArray(v) ? v : [v]));
389
- continue;
390
- }
391
- // Drill into each value
392
- values = values.flatMap((v) => {
393
- if (v == null) return [];
394
- if (Array.isArray(v)) {
395
- // Auto-flatten arrays even without explicit []
396
- return v.map((item) => (item as Record<string, unknown>)?.[seg]).filter((x) => x !== undefined);
397
- }
398
- if (typeof v === "object") {
399
- const val = (v as Record<string, unknown>)[seg];
400
- return val !== undefined ? [val] : [];
401
- }
402
- return [];
403
- });
459
+ // --schema: return only schema + extraction hints (no data)
460
+ if (flags.schema) {
461
+ output(schemaOnly(result), !!flags.pretty);
462
+ return;
404
463
  }
405
- return values;
406
- }
407
464
 
408
- /** Resolve a dot-path on a single object, e.g. "core.user_results.result.core.screen_name" */
409
- function resolveDotPath(obj: unknown, path: string): unknown {
410
- let cur = obj;
411
- for (const key of path.split(".")) {
412
- if (cur == null || typeof cur !== "object") return undefined;
413
- cur = (cur as Record<string, unknown>)[key];
465
+ // --path / --extract / --limit: transform .result in-place
466
+ if (hasTransforms && result.result != null) {
467
+ result = slimTrace({ ...result, result: applyTransforms(result.result, flags) });
468
+ } else if (!flags.raw && result.result != null) {
469
+ // No transforms requested try auto-extraction from hints
470
+ result = autoExtractOrWrap(result);
414
471
  }
415
- return cur;
416
- }
417
-
418
- /** Apply --extract field spec: "alias:deep.path,field2,alias2:path" */
419
- function applyExtract(items: unknown[], extractSpec: string): unknown[] {
420
- const fields = extractSpec.split(",").map((f) => {
421
- const colon = f.indexOf(":");
422
- if (colon > 0) return { alias: f.slice(0, colon), path: f.slice(colon + 1) };
423
- return { alias: f, path: f };
424
- });
425
- return items
426
- .map((item) => {
427
- const row: Record<string, unknown> = {};
428
- let hasValue = false;
429
- for (const { alias, path } of fields) {
430
- const val = resolveDotPath(item, path);
431
- row[alias] = val ?? null;
432
- if (val != null) hasValue = true;
433
- }
434
- return hasValue ? row : null;
435
- })
436
- .filter((row): row is Record<string, unknown> => row !== null);
437
- }
438
472
 
439
- /** Build a compact schema tree from a value (depth-limited). */
440
- function schemaOf(value: unknown, depth = 4): unknown {
441
- if (value == null) return "null";
442
- if (Array.isArray(value)) {
443
- if (value.length === 0) return ["unknown"];
444
- return [schemaOf(value[0], depth - 1)];
473
+ // Append CLI hint for feedback
474
+ const skill = result.skill as Record<string, unknown> | undefined;
475
+ const trace = result.trace as Record<string, unknown> | undefined;
476
+ if (skill?.skill_id && trace) {
477
+ (result as Record<string, unknown>)._feedback = `unbrowse feedback --skill ${skill.skill_id} --endpoint ${trace.endpoint_id || "?"} --rating <1-5>`;
445
478
  }
446
- if (typeof value === "object") {
447
- if (depth <= 0) return "object";
448
- const out: Record<string, unknown> = {};
449
- for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
450
- out[k] = schemaOf(v, depth - 1);
451
- }
452
- return out;
479
+
480
+ if (Date.now() - startedAt > 3_000 && result.source === "live-capture") {
481
+ info("Live capture finished. Future runs against this site should be much faster.");
453
482
  }
454
- return typeof value;
483
+
484
+ output(result, !!flags.pretty);
455
485
  }
456
486
 
457
487
  async function cmdExecute(flags: Record<string, string | boolean>): Promise<void> {
458
488
  const skillId = flags.skill as string;
459
489
  if (!skillId) die("--skill is required");
460
- const hostType = detectTelemetryHostType();
461
- await ensureCliInstallTracked(hostType);
462
- await recordFunnelTelemetryEvent("cli_invoked", {
463
- source: "cli",
464
- hostType,
465
- properties: { command: "execute" },
466
- });
467
- await recordFunnelTelemetryEvent("resolve_started", {
468
- source: "cli",
469
- hostType,
470
- properties: {
471
- command: "execute",
472
- skill_id: skillId,
473
- endpoint_id: typeof flags.endpoint === "string" ? flags.endpoint : null,
474
- },
475
- });
476
490
 
477
- try {
478
- const body: Record<string, unknown> = { params: {} };
479
- if (flags.endpoint) {
480
- (body.params as Record<string, unknown>).endpoint_id = flags.endpoint;
481
- }
482
- if (flags.params) {
483
- body.params = { ...(body.params as Record<string, unknown>), ...JSON.parse(flags.params as string) };
484
- }
485
- if (flags.url) {
486
- body.context_url = flags.url;
487
- (body.params as Record<string, unknown>).url = flags.url;
488
- }
489
- if (flags.intent) body.intent = flags.intent;
490
- if (flags["dry-run"]) body.dry_run = true;
491
- if (flags["confirm-unsafe"]) body.confirm_unsafe = true;
492
- body.projection = { raw: true };
493
-
494
- let result = await withPendingNotice(
495
- api("POST", `/v1/skills/${skillId}/execute`, body) as Promise<Record<string, unknown>>,
496
- "Still working. This endpoint may require browser replay or first-time auth/capture setup.",
497
- );
498
-
499
- if (isResolveSuccessResult(result)) {
500
- await recordFunnelTelemetryEvent("resolve_completed", {
501
- source: "cli",
502
- hostType,
503
- properties: {
504
- command: "execute",
505
- skill_id: skillId,
506
- endpoint_id: typeof flags.endpoint === "string" ? flags.endpoint : null,
507
- },
508
- });
509
- }
510
-
511
- // Strip metadata bloat
512
- result = slimTrace(result);
513
-
514
- const pathFlag = flags.path as string | undefined;
515
- const extractFlag = flags.extract as string | undefined;
516
- const limitFlag = flags.limit ? Number(flags.limit) : undefined;
517
- const schemaFlag = !!flags.schema;
518
- const rawFlag = !!flags.raw;
519
-
520
- // --schema: show response structure without data
521
- if (schemaFlag && !rawFlag) {
522
- const data = result.result;
523
- output({ trace: result.trace, schema: schemaOf(data) }, !!flags.pretty);
524
- return;
525
- }
526
-
527
- // Apply --path, --extract, --limit when not --raw
528
- if (!rawFlag && (pathFlag || extractFlag || limitFlag)) {
529
- const data = pathFlag ? drillPath(result.result, pathFlag) : result.result;
530
-
531
- // Ensure array for extract/limit
532
- const items = Array.isArray(data) ? data : data != null ? [data] : [];
533
-
534
- // Apply --extract
535
- const extracted = extractFlag ? applyExtract(items, extractFlag) : items;
536
-
537
- // Apply --limit
538
- const limited = limitFlag ? extracted.slice(0, limitFlag) : extracted;
539
-
540
- const trace = result.trace as Record<string, unknown> | undefined;
541
- const out: Record<string, unknown> = { trace: result.trace, data: limited, count: limited.length };
542
-
543
- // Prompt agent to review when this is likely a first-time execute
544
- if (trace?.skill_id && trace?.endpoint_id && limited.length > 0) {
545
- 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"}]'`;
546
- }
491
+ const body: Record<string, unknown> = { params: {} };
492
+ if (flags.endpoint) {
493
+ (body.params as Record<string, unknown>).endpoint_id = flags.endpoint;
494
+ }
495
+ if (flags.params) {
496
+ body.params = { ...(body.params as Record<string, unknown>), ...JSON.parse(flags.params as string) };
497
+ }
498
+ if (flags.url) body.context_url = flags.url;
499
+ if (flags.intent) body.intent = flags.intent;
500
+ if (flags["dry-run"]) body.dry_run = true;
501
+ if (flags["confirm-unsafe"]) body.confirm_unsafe = true;
502
+ // When explicit CLI transforms are present, get raw data for client-side extraction
503
+ const hasTransforms = !!(flags.path || flags.extract);
504
+ if (flags.raw || hasTransforms) body.projection = { raw: true };
547
505
 
548
- output(out, !!flags.pretty);
549
- return;
550
- }
506
+ let result = await withPendingNotice(
507
+ api("POST", `/v1/skills/${skillId}/execute`, body) as Promise<Record<string, unknown>>,
508
+ "Still working. This endpoint may require browser replay or first-time auth/capture setup.",
509
+ );
551
510
 
552
- // Auto-wrap large responses with extraction_hints when no flags given
553
- if (!rawFlag && !pathFlag && !extractFlag && !schemaFlag) {
554
- const raw = JSON.stringify(result.result);
555
- if (raw && raw.length > 2048) {
556
- const schema = schemaOf(result.result);
557
- output({
558
- trace: result.trace,
559
- extraction_hints: {
560
- message: "Response is large. Use --path/--extract/--limit to filter, or --schema to see structure, or --raw for full response.",
561
- schema_tree: schema,
562
- response_bytes: raw.length,
563
- },
564
- }, !!flags.pretty);
565
- return;
566
- }
567
- }
511
+ // --schema: return only schema + extraction hints (no data)
512
+ if (flags.schema) {
513
+ output(schemaOnly(result), !!flags.pretty);
514
+ return;
515
+ }
568
516
 
569
- output(result, !!flags.pretty);
570
- } catch (error) {
571
- const message = error instanceof Error ? error.message : String(error);
572
- await recordFunnelTelemetryEvent("resolve_failed", {
573
- source: "cli",
574
- hostType,
575
- properties: {
576
- command: "execute",
577
- skill_id: skillId,
578
- failure_stage: "execute",
579
- failure_reason: message,
580
- },
581
- });
582
- throw error;
517
+ // --path / --extract / --limit: transform .result in-place
518
+ if (hasTransforms && result.result != null) {
519
+ result = slimTrace({ ...result, result: applyTransforms(result.result, flags) });
520
+ } else if (!flags.raw && result.result != null) {
521
+ // No transforms requested — try auto-extraction from hints
522
+ result = autoExtractOrWrap(result);
583
523
  }
524
+
525
+ output(result, !!flags.pretty);
584
526
  }
585
527
 
586
528
  async function cmdFeedback(flags: Record<string, string | boolean>): Promise<void> {
@@ -600,31 +542,6 @@ async function cmdFeedback(flags: Record<string, string | boolean>): Promise<voi
600
542
  output(await api("POST", "/v1/feedback", body), !!flags.pretty);
601
543
  }
602
544
 
603
- async function cmdReview(flags: Record<string, string | boolean>): Promise<void> {
604
- const skillId = flags.skill as string;
605
- if (!skillId) die("--skill is required");
606
- const endpointsJson = flags.endpoints as string;
607
- if (!endpointsJson) die("--endpoints is required (JSON array of {endpoint_id, description?, action_kind?, resource_kind?})");
608
- const endpoints = JSON.parse(endpointsJson) as Array<Record<string, unknown>>;
609
- if (!Array.isArray(endpoints) || endpoints.length === 0) die("--endpoints must be a non-empty JSON array");
610
- output(await api("POST", `/v1/skills/${skillId}/review`, { endpoints }), !!flags.pretty);
611
- }
612
-
613
- async function cmdPublish(flags: Record<string, string | boolean>): Promise<void> {
614
- const skillId = flags.skill as string;
615
- if (!skillId) die("--skill is required");
616
- const endpointsJson = flags.endpoints as string | undefined;
617
- if (endpointsJson) {
618
- // Phase 2: merge descriptions + publish
619
- const endpoints = JSON.parse(endpointsJson) as Array<Record<string, unknown>>;
620
- if (!Array.isArray(endpoints) || endpoints.length === 0) die("--endpoints must be a non-empty JSON array");
621
- output(await api("POST", `/v1/skills/${skillId}/publish`, { endpoints }), !!flags.pretty);
622
- } else {
623
- // Phase 1: return endpoints needing descriptions
624
- output(await api("POST", `/v1/skills/${skillId}/publish`, {}), !!flags.pretty);
625
- }
626
- }
627
-
628
545
  async function cmdLogin(flags: Record<string, string | boolean>): Promise<void> {
629
546
  const url = flags.url as string;
630
547
  if (!url) die("--url is required");
@@ -659,13 +576,6 @@ async function cmdSessions(flags: Record<string, string | boolean>): Promise<voi
659
576
  }
660
577
 
661
578
  async function cmdSetup(flags: Record<string, string | boolean>): Promise<void> {
662
- const hostType = detectTelemetryHostType();
663
- await ensureCliInstallTracked(hostType);
664
- await recordFunnelTelemetryEvent("cli_invoked", {
665
- source: "setup",
666
- hostType,
667
- properties: { command: "setup" },
668
- });
669
579
  info("Running setup checks");
670
580
  const report = await runSetup({
671
581
  cwd: process.cwd(),
@@ -690,26 +600,6 @@ async function cmdSetup(flags: Record<string, string | boolean>): Promise<void>
690
600
  info(`Open Code command installed at ${report.opencode.command_file}`);
691
601
  }
692
602
 
693
- await recordInstallTelemetryEvent("setup", {
694
- hostType,
695
- status: report.browser_engine.action === "failed" ? "failed" : "installed",
696
- properties: {
697
- browser_engine_action: report.browser_engine.action,
698
- opencode_action: report.opencode.action,
699
- no_start: !!flags["no-start"],
700
- skip_browser: !!flags["skip-browser"],
701
- },
702
- });
703
- await recordFunnelTelemetryEvent("setup_completed", {
704
- source: "setup",
705
- hostType,
706
- properties: {
707
- browser_engine_action: report.browser_engine.action,
708
- opencode_action: report.opencode.action,
709
- no_start: !!flags["no-start"],
710
- },
711
- });
712
-
713
603
  if (flags["no-start"]) {
714
604
  report.server = { started: false, skipped: true, base_url: BASE_URL };
715
605
  output(report, true);
@@ -724,23 +614,8 @@ async function cmdSetup(flags: Record<string, string | boolean>): Promise<void>
724
614
  try {
725
615
  await ensureLocalServer(BASE_URL, false, import.meta.url);
726
616
  report.server = { started: true, base_url: BASE_URL };
727
- await recordFunnelTelemetryEvent("server_autostart_succeeded", {
728
- source: "setup",
729
- hostType,
730
- properties: {
731
- base_url: BASE_URL,
732
- },
733
- });
734
617
  } catch (error) {
735
618
  const message = error instanceof Error ? error.message : String(error);
736
- await recordFunnelTelemetryEvent("server_autostart_failed", {
737
- source: "setup",
738
- hostType,
739
- properties: {
740
- failure_stage: "server_autostart",
741
- failure_reason: message,
742
- },
743
- });
744
619
  report.server = { started: false, error: message, base_url: BASE_URL };
745
620
  output(report, true);
746
621
  process.exit(1);
@@ -758,34 +633,14 @@ export const CLI_REFERENCE = {
758
633
  commands: [
759
634
  { name: "health", usage: "", desc: "Server health check" },
760
635
  { name: "setup", usage: "[--opencode auto|global|project|off] [--no-start]", desc: "Bootstrap browser deps + Open Code command" },
761
- { name: "resolve", usage: '--intent "..." --url "..." [opts]', desc: "Resolve intent search/capture/execute" },
636
+ { name: "resolve", usage: '--intent "..." --url "..." [opts]', desc: "Resolve intent \u2192 search/capture/execute" },
762
637
  { name: "execute", usage: "--skill ID --endpoint ID [opts]", desc: "Execute a specific endpoint" },
763
638
  { name: "feedback", usage: "--skill ID --endpoint ID --rating N", desc: "Submit feedback (mandatory after resolve)" },
764
- { name: "review", usage: "--skill ID --endpoints '[...]'", desc: "Push reviewed descriptions/metadata back to skill" },
765
- { name: "publish", usage: "--skill ID [--endpoints '[...]']", desc: "Describe + publish skill to marketplace (two-phase)" },
766
639
  { name: "login", usage: '--url "..."', desc: "Interactive browser login" },
767
640
  { name: "skills", usage: "", desc: "List all skills" },
768
641
  { name: "skill", usage: "<id>", desc: "Get skill details" },
769
642
  { name: "search", usage: '--intent "..." [--domain "..."]', desc: "Search marketplace" },
770
643
  { name: "sessions", usage: '--domain "..." [--limit N]', desc: "Debug session logs" },
771
- { name: "go", usage: '<url>', desc: "Open a live Kuri browser tab for capture-first workflows" },
772
- { name: "submit", usage: "[--form-selector sel] [--submit-selector sel] [--wait-for hint]", desc: "Submit current form with DOM-first + same-origin rehydrate fallback for JS-heavy flows" },
773
- { name: "snap", usage: "[--filter interactive]", desc: "A11y snapshot with @eN refs" },
774
- { name: "click", usage: "<ref>", desc: "Click element by ref (e.g. e5)" },
775
- { name: "fill", usage: "<ref> <value>", desc: "Fill input by ref" },
776
- { name: "type", usage: "<text>", desc: "Type text with key events" },
777
- { name: "press", usage: "<key>", desc: "Press key (Enter, Tab, Escape)" },
778
- { name: "select", usage: "<ref> <value>", desc: "Select option by ref" },
779
- { name: "scroll", usage: "[up|down|left|right]", desc: "Scroll the page" },
780
- { name: "screenshot", usage: "", desc: "Capture screenshot (base64 PNG)" },
781
- { name: "text", usage: "", desc: "Get page text content" },
782
- { name: "markdown", usage: "", desc: "Get page as Markdown" },
783
- { name: "cookies", usage: "", desc: "Get page cookies" },
784
- { name: "eval", usage: "<expression>", desc: "Evaluate JavaScript" },
785
- { name: "back", usage: "", desc: "Navigate back" },
786
- { name: "forward", usage: "", desc: "Navigate forward" },
787
- { name: "sync", usage: "", desc: "Flush the current step's captured traffic into route cache without closing tab" },
788
- { name: "close", usage: "", desc: "Close browse session, flush + index traffic" },
789
644
  ],
790
645
  globalFlags: [
791
646
  { flag: "--pretty", desc: "Indented JSON output" },
@@ -806,19 +661,11 @@ export const CLI_REFERENCE = {
806
661
  ],
807
662
  examples: [
808
663
  "unbrowse setup",
809
- 'unbrowse resolve --intent "top stories" --url "https://news.ycombinator.com" --execute',
810
664
  'unbrowse resolve --intent "get timeline" --url "https://x.com"',
811
- 'unbrowse go "https://www.mandai.com/en/ticketing/admission-and-rides/parks-selection.html"',
812
- 'unbrowse snap --filter interactive',
813
- 'unbrowse submit --wait-for "/time-selection.html"',
814
- 'unbrowse sync',
815
665
  "unbrowse execute --skill abc --endpoint def --pretty",
816
- "unbrowse execute --skill abc --endpoint def --schema --pretty",
817
- 'unbrowse execute --skill abc --endpoint def --path "data.items[]" --extract "name,url" --limit 10 --pretty',
666
+ 'unbrowse execute --skill abc --endpoint def --extract "user,text,likes" --limit 10',
667
+ 'unbrowse execute --skill abc --endpoint def --path "data.included[]" --extract "name:actor.name,text:commentary.text" --limit 20',
818
668
  "unbrowse feedback --skill abc --endpoint def --rating 5",
819
- 'unbrowse review --skill abc --endpoints \'[{"endpoint_id":"def","description":"..."}]\'',
820
- "unbrowse publish --skill abc --pretty",
821
- 'unbrowse publish --skill abc --endpoints \'[{"endpoint_id":"def","description":"Search court judgments by keywords","action_kind":"search","resource_kind":"judgment"}]\'',
822
669
  ],
823
670
  };
824
671
 
@@ -854,387 +701,14 @@ function printHelp(): void {
854
701
  lines.push(` ${e}`);
855
702
  }
856
703
 
857
- lines.push(
858
- "",
859
- "Browser workflow:",
860
- " 1. go -> open the live tab you want to work in",
861
- " 2. snap -> inspect refs and confirm the page state",
862
- " 3. click/fill/eval -> set real page state",
863
- " 4. submit -> prefer DOM submit; auto-falls back to same-origin rehydrate",
864
- " 5. sync -> flush captured routes after a successful step",
865
- " 6. close -> finish capture + indexing",
866
- );
867
-
868
- lines.push(
869
- "",
870
- "JS-heavy forms:",
871
- " Prefer real calendar/time clicks before submit.",
872
- " If the UI is flaky, inspect hidden inputs/cookies with eval, then submit the real form.",
873
- );
874
-
875
704
  lines.push("");
876
705
  process.stderr.write(lines.join("\n"));
877
706
  }
878
707
 
879
- // ---------------------------------------------------------------------------
880
- // Server lifecycle commands
881
- // ---------------------------------------------------------------------------
882
-
883
- async function cmdStatus(flags: Record<string, string | boolean>): Promise<void> {
884
- const healthy = await fetch(`${BASE_URL}/health`, { signal: AbortSignal.timeout(2_000) })
885
- .then((r) => r.ok).catch(() => false);
886
- const versionInfo = checkServerVersion(BASE_URL, import.meta.url);
887
- output({
888
- server: healthy ? "running" : "stopped",
889
- url: BASE_URL,
890
- ...(versionInfo ?? {}),
891
- }, !!flags.pretty);
892
- }
893
-
894
- async function cmdRestart(flags: Record<string, string | boolean>): Promise<void> {
895
- info("Restarting server...");
896
- await restartServer(BASE_URL, import.meta.url);
897
- info("Server restarted.");
898
- await cmdStatus(flags);
899
- }
900
-
901
- function cmdStop(flags: Record<string, string | boolean>): void {
902
- const stopped = stopServer(BASE_URL);
903
- if (stopped) info("Server stopped.");
904
- else info("No server running.");
905
- }
906
-
907
- async function cmdUpgrade(flags: Record<string, string | boolean>): Promise<void> {
908
- info("Checking for updates...");
909
- const { execSync } = await import("node:child_process");
910
- try {
911
- const result = execSync("npm view unbrowse version", { encoding: "utf-8", timeout: 10_000 }).trim();
912
- const versionInfo = checkServerVersion(BASE_URL, import.meta.url);
913
- const installed = versionInfo?.installed ?? "unknown";
914
- if (result === installed) {
915
- info(`Already at latest version: ${installed}`);
916
- return;
917
- }
918
- info(`Update available: ${installed} -> ${result}`);
919
- info("Run: npm install -g unbrowse@latest");
920
- info("Then: unbrowse restart");
921
- } catch (err) {
922
- info(`Could not check for updates: ${(err as Error).message}`);
923
- }
924
- }
925
-
926
- // ---------------------------------------------------------------------------
927
- // Site/task shortcut commands
928
- // ---------------------------------------------------------------------------
929
-
930
- async function cmdSiteHelp(pack: SitePack, flags: Record<string, string | boolean>): Promise<void> {
931
- // --deps: return dependency graph as JSON
932
- if (flags.deps) {
933
- const graph = buildDepsGraph(pack);
934
- output({ site: pack.site, tasks: graph }, !!flags.pretty);
935
- return;
936
- }
937
- // --plan: return execution plan for a set of tasks
938
- if (flags.plan) {
939
- const taskNames = (flags.plan as string).split(",").map((s) => s.trim());
940
- const waves = planExecution(pack, taskNames);
941
- output({ site: pack.site, waves }, !!flags.pretty);
942
- return;
943
- }
944
- // Default: human-readable help
945
- const lines: string[] = [
946
- `unbrowse ${pack.site} — ${pack.description}`,
947
- "",
948
- "Tasks:",
949
- ];
950
- for (const t of pack.tasks) {
951
- const aliases = t.match.length > 1 ? ` (${t.match.slice(1).join(", ")})` : "";
952
- const auth = t.needs_auth ? " [auth]" : "";
953
- lines.push(` ${t.match[0]}${aliases}${auth} ${t.description}`);
954
- }
955
- lines.push(
956
- "",
957
- "Examples:",
958
- ` unbrowse ${pack.site} login`,
959
- ` unbrowse ${pack.site} ${pack.tasks.find((t) => t.match[0] !== "login")?.match[0] || "help"} --pretty`,
960
- ` unbrowse ${pack.site} --batch ${pack.tasks.filter((t) => t.parallel_safe).map((t) => t.match[0]).join(",")}`,
961
- ` unbrowse ${pack.site} help --deps`,
962
- ` unbrowse ${pack.site} help --plan feed,notifications`,
963
- "",
964
- "Flags: --pretty --raw --path --extract --limit --force-capture --dry-run --batch --deps --plan",
965
- );
966
- process.stderr.write(lines.join("\n") + "\n");
967
- }
968
-
969
- async function cmdSiteLogin(pack: SitePack, flags: Record<string, string | boolean>): Promise<void> {
970
- const result = await api("POST", "/v1/auth/login", { url: pack.login_url });
971
- const deps = buildDepsMetadata(pack, "login");
972
- output({ ...result as Record<string, unknown>, _deps: deps, _shortcut: `${pack.site} login` }, !!flags.pretty);
973
- }
974
-
975
- async function cmdSiteTask(pack: SitePack, taskName: string, flags: Record<string, string | boolean>): Promise<void> {
976
- const task = findTask(pack, taskName);
977
- if (!task) {
978
- info(`Unknown task "${taskName}" for ${pack.site}. Run: unbrowse ${pack.site} help`);
979
- process.exit(1);
980
- }
981
-
982
- // Compile to resolve call
983
- const body: Record<string, unknown> = {
984
- intent: task.intent,
985
- params: { url: task.url },
986
- context: { url: task.url },
987
- };
988
- if (flags.params) {
989
- body.params = { ...(body.params as Record<string, unknown>), ...JSON.parse(flags.params as string) };
990
- }
991
- if (flags["dry-run"]) body.dry_run = true;
992
- if (flags["force-capture"]) body.force_capture = true;
993
- body.projection = { raw: true };
994
-
995
- const startedAt = Date.now();
996
- let result = await withPendingNotice(
997
- api("POST", "/v1/intent/resolve", body) as Promise<Record<string, unknown>>,
998
- "Still working. First-time capture/indexing for a site can take 20-80s.",
999
- );
1000
-
1001
- // Check for auth-required response
1002
- if (result && typeof result === "object" && (result as Record<string, unknown>).error === "auth_required") {
1003
- info(`Authentication required. Run: unbrowse ${pack.site} login`);
1004
- const deps = buildDepsMetadata(pack, taskName);
1005
- output({ ...(result as Record<string, unknown>), _deps: { ...deps, requires: ["login"] }, _next: [`unbrowse ${pack.site} login`] }, !!flags.pretty);
1006
- process.exit(2);
1007
- }
1008
-
1009
- // Strip metadata bloat
1010
- result = slimTrace(result);
1011
-
1012
- const deps = buildDepsMetadata(pack, taskName);
1013
- (result as Record<string, unknown>)._deps = deps;
1014
- (result as Record<string, unknown>)._shortcut = `${pack.site} ${taskName}`;
1015
-
1016
- if (Date.now() - startedAt > 3_000 && result.source === "live-capture") {
1017
- info("Live capture finished. Future runs should be much faster.");
1018
- }
1019
-
1020
- output(result, !!flags.pretty);
1021
- }
1022
-
1023
- async function cmdSiteBatch(pack: SitePack, batchArg: string, flags: Record<string, string | boolean>): Promise<void> {
1024
- const taskNames = batchArg.split(",").map((s) => s.trim());
1025
- const waves = planExecution(pack, taskNames);
1026
- const results: Record<string, unknown> = { site: pack.site, waves: [], _deps: { parallel_safe: true } };
1027
- const waveResults: unknown[] = [];
1028
-
1029
- for (const wave of waves) {
1030
- const waveStart = Date.now();
1031
- const promises = wave.commands.map(async (cmd) => {
1032
- const parts = cmd.split(" ");
1033
- const task = parts[parts.length - 1];
1034
- const taskDef = findTask(pack, task);
1035
- if (!taskDef) return { task, error: "unknown task" };
1036
-
1037
- if (task === "login") {
1038
- return { task, result: await api("POST", "/v1/auth/login", { url: pack.login_url }) };
1039
- }
1040
- const body: Record<string, unknown> = {
1041
- intent: taskDef.intent,
1042
- params: { url: taskDef.url },
1043
- context: { url: taskDef.url },
1044
- };
1045
- if (flags["force-capture"]) body.force_capture = true;
1046
- body.projection = { raw: true };
1047
- const res = await api("POST", "/v1/intent/resolve", body) as Record<string, unknown>;
1048
- return { task, result: slimTrace(res) };
1049
- });
1050
-
1051
- const waveResult = await Promise.all(promises);
1052
- waveResults.push({
1053
- wave: wave.wave,
1054
- reason: wave.reason,
1055
- elapsed_ms: Date.now() - waveStart,
1056
- tasks: waveResult,
1057
- });
1058
- }
1059
-
1060
- (results as Record<string, unknown>).waves = waveResults;
1061
- output(results, !!flags.pretty);
1062
- }
1063
-
1064
708
  // ---------------------------------------------------------------------------
1065
709
  // Main
1066
710
  // ---------------------------------------------------------------------------
1067
711
 
1068
- // ---------------------------------------------------------------------------
1069
- // Browse commands — Kuri browser actions with passive indexing via server
1070
- // ---------------------------------------------------------------------------
1071
-
1072
- async function cmdGo(args: string[], flags: Record<string, string | boolean>): Promise<void> {
1073
- const url = args[0] ?? flags.url as string;
1074
- if (!url) die("Usage: unbrowse go <url>");
1075
- output(await api("POST", "/v1/browse/go", { url }), !!flags.pretty);
1076
- }
1077
-
1078
- async function cmdSubmit(flags: Record<string, string | boolean>): Promise<void> {
1079
- const body: Record<string, unknown> = {};
1080
- if (typeof flags["form-selector"] === "string") body.form_selector = flags["form-selector"];
1081
- if (typeof flags["submit-selector"] === "string") body.submit_selector = flags["submit-selector"];
1082
- if (typeof flags["wait-for"] === "string") body.wait_for = flags["wait-for"];
1083
- if (typeof flags["timeout-ms"] === "string") body.timeout_ms = Number(flags["timeout-ms"]);
1084
- if (flags["same-origin-fetch-fallback"] !== undefined) {
1085
- body.same_origin_fetch_fallback = flags["same-origin-fetch-fallback"] !== "false";
1086
- }
1087
- output(await api("POST", "/v1/browse/submit", body), !!flags.pretty);
1088
- }
1089
-
1090
- async function cmdSnap(flags: Record<string, string | boolean>): Promise<void> {
1091
- const filter = flags.filter as string | undefined;
1092
- const result = await api("POST", "/v1/browse/snap", { filter }) as { snapshot?: string };
1093
- if (result.snapshot && !flags.pretty) {
1094
- console.log(result.snapshot);
1095
- } else {
1096
- output(result, !!flags.pretty);
1097
- }
1098
- }
1099
-
1100
- async function cmdClick(args: string[]): Promise<void> {
1101
- const ref = args[0];
1102
- if (!ref) die("Usage: unbrowse click <ref>");
1103
- output(await api("POST", "/v1/browse/click", { ref }), false);
1104
- }
1105
-
1106
- async function cmdFill(args: string[]): Promise<void> {
1107
- const ref = args[0];
1108
- const value = args.slice(1).join(" ");
1109
- if (!ref || !value) die("Usage: unbrowse fill <ref> <value>");
1110
- output(await api("POST", "/v1/browse/fill", { ref, value }), false);
1111
- }
1112
-
1113
- async function cmdType(args: string[]): Promise<void> {
1114
- const text = args.join(" ");
1115
- if (!text) die("Usage: unbrowse type <text>");
1116
- output(await api("POST", "/v1/browse/type", { text }), false);
1117
- }
1118
-
1119
- async function cmdPress(args: string[]): Promise<void> {
1120
- const key = args[0];
1121
- if (!key) die("Usage: unbrowse press <key>");
1122
- output(await api("POST", "/v1/browse/press", { key }), false);
1123
- }
1124
-
1125
- async function cmdSelect(args: string[]): Promise<void> {
1126
- const ref = args[0];
1127
- const value = args.slice(1).join(" ");
1128
- if (!ref || !value) die("Usage: unbrowse select <ref> <value>");
1129
- output(await api("POST", "/v1/browse/select", { ref, value }), false);
1130
- }
1131
-
1132
- async function cmdScroll(args: string[]): Promise<void> {
1133
- const direction = args[0] ?? "down";
1134
- output(await api("POST", "/v1/browse/scroll", { direction }), false);
1135
- }
1136
-
1137
- async function cmdScreenshot(flags: Record<string, string | boolean>): Promise<void> {
1138
- output(await api("GET", "/v1/browse/screenshot"), !!flags.pretty);
1139
- }
1140
-
1141
- async function cmdText(flags: Record<string, string | boolean>): Promise<void> {
1142
- const result = await api("GET", "/v1/browse/text") as { text?: string };
1143
- if (result.text && !flags.pretty) {
1144
- console.log(result.text);
1145
- } else {
1146
- output(result, !!flags.pretty);
1147
- }
1148
- }
1149
-
1150
- async function cmdMarkdown(flags: Record<string, string | boolean>): Promise<void> {
1151
- const result = await api("GET", "/v1/browse/markdown") as { markdown?: string };
1152
- if (result.markdown && !flags.pretty) {
1153
- console.log(result.markdown);
1154
- } else {
1155
- output(result, !!flags.pretty);
1156
- }
1157
- }
1158
-
1159
- async function cmdCookies(flags: Record<string, string | boolean>): Promise<void> {
1160
- output(await api("GET", "/v1/browse/cookies"), !!flags.pretty);
1161
- }
1162
-
1163
- async function cmdEval(args: string[], flags: Record<string, string | boolean>): Promise<void> {
1164
- const expression = args.join(" ");
1165
- if (!expression) die("Usage: unbrowse eval <expression>");
1166
- output(await api("POST", "/v1/browse/eval", { expression }), !!flags.pretty);
1167
- }
1168
-
1169
- async function cmdBack(): Promise<void> {
1170
- output(await api("POST", "/v1/browse/back"), false);
1171
- }
1172
-
1173
- async function cmdForward(): Promise<void> {
1174
- output(await api("POST", "/v1/browse/forward"), false);
1175
- }
1176
-
1177
- async function cmdSync(flags: Record<string, string | boolean>): Promise<void> {
1178
- output(await api("POST", "/v1/browse/sync"), !!flags.pretty);
1179
- }
1180
-
1181
- async function cmdClose(): Promise<void> {
1182
- output(await api("POST", "/v1/browse/close"), false);
1183
- }
1184
-
1185
- async function cmdConnectChrome(): Promise<void> {
1186
- const { execSync, spawn: spawnProc } = require("child_process");
1187
-
1188
- // Check if Chrome already has CDP
1189
- try {
1190
- const res = await fetch("http://127.0.0.1:9222/json/version", { signal: AbortSignal.timeout(1000) });
1191
- if (res.ok) {
1192
- const data = await res.json() as { "User-Agent"?: string };
1193
- if (!data["User-Agent"]?.includes("Headless")) {
1194
- console.log("Your Chrome is already connected with CDP on port 9222.");
1195
- console.log("Browse commands will use your real browser with all your sessions.");
1196
- return;
1197
- }
1198
- }
1199
- } catch { /* not running */ }
1200
-
1201
- // Kill any Kuri-managed Chrome
1202
- try { execSync("pkill -f kuri/chrome-profile", { stdio: "ignore" }); } catch {}
1203
-
1204
- // Quit Chrome fully — can't add debugging port to running instance
1205
- console.log("Quitting Chrome to relaunch with remote debugging...");
1206
- if (process.platform === "darwin") {
1207
- try { execSync('osascript -e "quit app \\"Google Chrome\\""', { stdio: "ignore", timeout: 5000 }); } catch {}
1208
- } else {
1209
- try { execSync("pkill -f chrome", { stdio: "ignore" }); } catch {}
1210
- }
1211
- await new Promise(r => setTimeout(r, 2000));
1212
-
1213
- console.log("Launching Chrome with remote debugging on port 9222...");
1214
- if (process.platform === "darwin") {
1215
- spawnProc("/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
1216
- ["--remote-debugging-port=9222", "--no-first-run", "--no-default-browser-check"],
1217
- { stdio: "ignore", detached: true }).unref();
1218
- } else {
1219
- spawnProc("google-chrome", ["--remote-debugging-port=9222"], { stdio: "ignore", detached: true }).unref();
1220
- }
1221
-
1222
- // Wait for CDP
1223
- const deadline = Date.now() + 15000;
1224
- while (Date.now() < deadline) {
1225
- try {
1226
- const res = await fetch("http://127.0.0.1:9222/json/version", { signal: AbortSignal.timeout(500) });
1227
- if (res.ok) {
1228
- console.log("Connected. Your real Chrome is now available for browse commands.");
1229
- console.log("All your logged-in sessions (LinkedIn, X, etc.) will work.");
1230
- console.log('Run: unbrowse go "https://linkedin.com/feed/"');
1231
- return;
1232
- }
1233
- } catch {}
1234
- await new Promise(r => setTimeout(r, 500));
1235
- }
1236
- console.error("Could not connect to Chrome. Make sure all Chrome windows are closed and try again.");
1237
- }
1238
712
  async function main(): Promise<void> {
1239
713
  const { command, args, flags } = parseArgs(process.argv);
1240
714
  const noAutoStart = !!flags["no-auto-start"];
@@ -1249,42 +723,6 @@ async function main(): Promise<void> {
1249
723
  return;
1250
724
  }
1251
725
 
1252
- // Server lifecycle commands (don't need ensureLocalServer)
1253
- if (command === "status") return cmdStatus(flags);
1254
- if (command === "stop") { cmdStop(flags); return; }
1255
- if (command === "restart") return cmdRestart(flags);
1256
- if (command === "upgrade" || command === "update") return cmdUpgrade(flags);
1257
- if (command === "connect-chrome") return cmdConnectChrome();
1258
-
1259
- // --- Shortcut resolution: unbrowse <site> [task] [flags] ---
1260
- const KNOWN_COMMANDS = new Set([
1261
- "health", "setup", "resolve", "execute", "exec",
1262
- "feedback", "fb", "review", "publish", "login", "skills", "skill", "search", "sessions",
1263
- "status", "stop", "restart", "upgrade", "update",
1264
- "go", "submit", "snap", "click", "fill", "type", "press", "select", "scroll",
1265
- "screenshot", "text", "markdown", "cookies", "eval", "back", "forward", "sync", "close",
1266
- "connect-chrome",
1267
- ]);
1268
-
1269
- if (!KNOWN_COMMANDS.has(command)) {
1270
- const pack = findSitePack(command);
1271
- if (pack) {
1272
- await ensureLocalServer(BASE_URL, noAutoStart, import.meta.url);
1273
- const taskName = args[0];
1274
- if (!taskName || taskName === "help") {
1275
- return cmdSiteHelp(pack, flags);
1276
- }
1277
- if (taskName === "login") {
1278
- return cmdSiteLogin(pack, flags);
1279
- }
1280
- const batchArg = flags.batch as string | undefined;
1281
- if (batchArg) {
1282
- return cmdSiteBatch(pack, batchArg, flags);
1283
- }
1284
- return cmdSiteTask(pack, taskName, flags);
1285
- }
1286
- }
1287
-
1288
726
  await ensureLocalServer(BASE_URL, noAutoStart, import.meta.url);
1289
727
 
1290
728
  switch (command) {
@@ -1293,41 +731,18 @@ async function main(): Promise<void> {
1293
731
  case "resolve": return cmdResolve(flags);
1294
732
  case "execute": case "exec": return cmdExecute(flags);
1295
733
  case "feedback": case "fb": return cmdFeedback(flags);
1296
- case "review": return cmdReview(flags);
1297
- case "publish": return cmdPublish(flags);
1298
734
  case "login": return cmdLogin(flags);
1299
735
  case "skills": return cmdSkills(flags);
1300
736
  case "skill": return cmdSkill(args, flags);
1301
737
  case "search": return cmdSearch(flags);
1302
738
  case "sessions": return cmdSessions(flags);
1303
- // Browse commands — Kuri browser actions with passive indexing
1304
- case "go": return cmdGo(args, flags);
1305
- case "submit": return cmdSubmit(flags);
1306
- case "snap": return cmdSnap(flags);
1307
- case "click": return cmdClick(args);
1308
- case "fill": return cmdFill(args);
1309
- case "type": return cmdType(args);
1310
- case "press": return cmdPress(args);
1311
- case "select": return cmdSelect(args);
1312
- case "scroll": return cmdScroll(args);
1313
- case "screenshot": return cmdScreenshot(flags);
1314
- case "text": return cmdText(flags);
1315
- case "markdown": return cmdMarkdown(flags);
1316
- case "cookies": return cmdCookies(flags);
1317
- case "eval": return cmdEval(args, flags);
1318
- case "back": return cmdBack();
1319
- case "forward": return cmdForward();
1320
- case "sync": return cmdSync(flags);
1321
- case "close": return cmdClose();
1322
- case "connect-chrome": return cmdConnectChrome();
1323
739
  default: info(`Unknown command: ${command}`); printHelp(); process.exit(1);
1324
740
  }
1325
741
  }
1326
742
 
743
+ // Only run when this file is the entry point (not when imported by sync script etc.)
1327
744
  if (isMainModule(import.meta.url)) {
1328
- main()
1329
- .then(() => Promise.all([drainPendingIndexJobs(), drainPendingPassivePublishes()]))
1330
- .catch((err) => {
1331
- die((err as Error).message);
1332
- });
745
+ main().catch((err) => {
746
+ die((err as Error).message);
747
+ });
1333
748
  }