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.
- package/README.md +8 -44
- package/dist/cli.js +514 -20723
- package/package.json +4 -10
- package/runtime-src/api/routes.ts +15 -801
- package/runtime-src/auth/index.ts +32 -142
- package/runtime-src/capture/index.ts +101 -436
- package/runtime-src/cli.ts +371 -956
- package/runtime-src/client/index.ts +29 -622
- package/runtime-src/execution/index.ts +85 -345
- package/runtime-src/graph/index.ts +10 -128
- package/runtime-src/intent-match.ts +27 -27
- package/runtime-src/kuri/client.ts +82 -543
- package/runtime-src/orchestrator/index.ts +462 -2246
- package/runtime-src/reverse-engineer/index.ts +22 -220
- package/runtime-src/runtime/local-server.ts +16 -149
- package/runtime-src/runtime/paths.ts +5 -9
- package/runtime-src/runtime/setup.ts +1 -52
- package/runtime-src/server.ts +11 -6
- package/runtime-src/transform/schema-hints.ts +358 -0
- package/runtime-src/types/skill.ts +2 -49
- package/runtime-src/verification/index.ts +0 -15
- package/runtime-src/version.ts +13 -13
- package/vendor/kuri/darwin-arm64/kuri +0 -0
- package/vendor/kuri/darwin-x64/kuri +0 -0
- package/vendor/kuri/linux-arm64/kuri +0 -0
- package/vendor/kuri/linux-x64/kuri +0 -0
- package/bin/unbrowse-wrapper.mjs +0 -39
- package/bin/unbrowse.js +0 -38
- package/runtime-src/analytics-session.ts +0 -33
- package/runtime-src/api/browse-index.ts +0 -254
- package/runtime-src/api/browse-session.ts +0 -179
- package/runtime-src/api/browse-submit.ts +0 -455
- package/runtime-src/auth/runtime.ts +0 -116
- package/runtime-src/browser/index.ts +0 -635
- package/runtime-src/browser/types.ts +0 -41
- package/runtime-src/capture/prefetch.ts +0 -122
- package/runtime-src/capture/rsc.ts +0 -45
- package/runtime-src/cli/shortcuts.ts +0 -273
- package/runtime-src/client/graph-client.ts +0 -99
- package/runtime-src/execution/robots.ts +0 -167
- package/runtime-src/execution/search-forms.ts +0 -188
- package/runtime-src/graph/planner.ts +0 -411
- package/runtime-src/graph/session.ts +0 -294
- package/runtime-src/graph/trace-store.ts +0 -136
- package/runtime-src/indexer/index.ts +0 -480
- package/runtime-src/orchestrator/browser-agent.ts +0 -374
- package/runtime-src/orchestrator/dag-advisor.ts +0 -59
- package/runtime-src/orchestrator/dag-feedback.ts +0 -256
- package/runtime-src/orchestrator/first-pass-action.ts +0 -362
- package/runtime-src/orchestrator/passive-publish.ts +0 -152
- package/runtime-src/orchestrator/timing-economics.ts +0 -80
- package/runtime-src/payments/cascade.ts +0 -137
- package/runtime-src/payments/index.ts +0 -268
- package/runtime-src/payments/wallet.ts +0 -33
- package/runtime-src/reverse-engineer/description-prompt.ts +0 -132
- package/runtime-src/router.ts +0 -17
- package/runtime-src/runtime/browser-access.ts +0 -11
- package/runtime-src/runtime/browser-host.ts +0 -48
- package/runtime-src/runtime/lifecycle.ts +0 -17
- package/runtime-src/runtime/supervisor.ts +0 -69
- package/runtime-src/single-binary.ts +0 -141
- package/runtime-src/telemetry.ts +0 -253
- package/runtime-src/verification/matrix.ts +0 -30
- package/scripts/postinstall.mjs +0 -81
package/runtime-src/cli.ts
CHANGED
|
@@ -8,19 +8,9 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { config as loadEnv } from "dotenv";
|
|
11
|
-
import {
|
|
12
|
-
|
|
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
|
-
//
|
|
106
|
+
// Path resolution — drill 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
|
-
|
|
158
|
-
if (obj.
|
|
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
|
-
|
|
165
|
-
output
|
|
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
|
-
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
215
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
|
|
224
|
-
|
|
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
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
|
|
247
|
-
|
|
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
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
-
|
|
330
|
-
|
|
331
|
-
|
|
418
|
+
// ---------------------------------------------------------------------------
|
|
419
|
+
// Commands
|
|
420
|
+
// ---------------------------------------------------------------------------
|
|
332
421
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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
|
-
|
|
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
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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
|
-
|
|
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
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
if (
|
|
443
|
-
|
|
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
|
-
|
|
447
|
-
|
|
448
|
-
|
|
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
|
-
|
|
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
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
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
|
-
|
|
549
|
-
|
|
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
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
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
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
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
|
|
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
|
-
|
|
817
|
-
'unbrowse execute --skill abc --endpoint def --path "data.
|
|
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
|
-
|
|
1330
|
-
|
|
1331
|
-
die((err as Error).message);
|
|
1332
|
-
});
|
|
745
|
+
main().catch((err) => {
|
|
746
|
+
die((err as Error).message);
|
|
747
|
+
});
|
|
1333
748
|
}
|