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
@@ -1,191 +1,23 @@
1
1
  import type { FastifyInstance } from "fastify";
2
- import * as kuri from "../kuri/client.js";
3
- import type { KuriHarEntry } from "../kuri/client.js";
4
- import { extractEndpoints, extractAuthHeaders } from "../reverse-engineer/index.js";
5
- import { INTERCEPTOR_SCRIPT, collectInterceptedRequests, injectInterceptor, type RawRequest } from "../capture/index.js";
6
- import { queueBackgroundIndex } from "../indexer/index.js";
7
- import { nanoid } from "nanoid";
8
- import type { ExecutionTrace, OrchestrationTiming, ProjectionOptions, SkillManifest } from "../types/index.js";
9
- import { extractBrowserCookies } from "../auth/browser-cookies.js";
10
- import { mergeEndpoints } from "../marketplace/index.js";
11
- import { buildSkillOperationGraph } from "../graph/index.js";
12
- import { augmentEndpointsWithAgent } from "../graph/agent-augment.js";
13
- import { findExistingSkillForDomain, cachePublishedSkill } from "../client/index.js";
14
- import { storeCredential } from "../vault/index.js";
15
- import { generateLocalDescription, writeSkillSnapshot, buildResolveCacheKey, getDomainReuseKey, domainSkillCache, persistDomainCache, scopedCacheKey, snapshotPathForCacheKey, invalidateRouteCacheForDomain, summarizeSchema, extractSampleValues } from "../orchestrator/index.js";
16
2
  import { TRACE_VERSION, CODE_HASH, GIT_SHA } from "../version.js";
17
- import { promoteExplicitExecution, resolveAndExecute, type OrchestratorResult } from "../orchestrator/index.js";
3
+ import { promoteExplicitExecution, resolveAndExecute } from "../orchestrator/index.js";
18
4
  import { getSkill } from "../marketplace/index.js";
19
- import { executeSkill, rankEndpoints } from "../execution/index.js";
5
+ import { executeSkill } from "../execution/index.js";
6
+ import { storeCredential } from "../vault/index.js";
20
7
  import { interactiveLogin, extractBrowserAuth } from "../auth/index.js";
21
8
  import { publishSkill } from "../marketplace/index.js";
22
- import { recordFeedback, recordDiagnostics, recordExecution, getApiKey, getRecentLocalSkill, recordAnalyticsSession, waitForBackgroundRegistration, type AnalyticsSessionPayload } from "../client/index.js";
9
+ import { recordFeedback, recordDiagnostics, getApiKey, getRecentLocalSkill } from "../client/index.js";
23
10
  import { ROUTE_LIMITS } from "../ratelimit/index.js";
11
+ import type { ProjectionOptions } from "../types/index.js";
24
12
  import { getSkillChunk, toAgentSkillChunkView } from "../graph/index.js";
25
13
  import { listRecentSessionsForDomain } from "../session-logs.js";
26
- import { mergeAgentReview } from "../indexer/index.js";
27
14
  import { writeFileSync, existsSync, mkdirSync } from "fs";
28
15
  import { join } from "path";
29
- import { type BrowseSession, getOrCreateBrowseSession, isRecoverableBrowseFailure, withRecoveredBrowseSession } from "./browse-session.js";
30
- import { cacheBrowseRequests, harEntriesToRawRequests, mergeBrowseRequests } from "./browse-index.js";
31
- import { submitBrowseForm } from "./browse-submit.js";
32
16
 
33
17
  const BETA_API_URL = process.env.UNBROWSE_BACKEND_URL || "https://beta-api.unbrowse.ai";
34
18
 
35
19
  const TRACES_DIR = process.env.TRACES_DIR ?? join(process.cwd(), "traces");
36
20
 
37
- type AnalyticsSessionResult = {
38
- trace: Pick<ExecutionTrace, "trace_id" | "started_at" | "completed_at" | "endpoint_id" | "trace_version">;
39
- timing?: Pick<OrchestrationTiming, "source">;
40
- source?: OrchestratorResult["source"];
41
- };
42
-
43
- export function buildAnalyticsSessionPayload(
44
- result: AnalyticsSessionResult,
45
- opts: {
46
- browser_mode: AnalyticsSessionPayload["browser_mode"];
47
- discovery_queries: number;
48
- cached_skill_calls?: number;
49
- fresh_index_calls?: number;
50
- },
51
- ): AnalyticsSessionPayload {
52
- const source = result.timing?.source ?? result.source;
53
- const apiCalls = result.trace.endpoint_id ? 1 : 0;
54
- const cachedSkillCalls = opts.cached_skill_calls ?? (
55
- apiCalls > 0 && source !== "live-capture" && source !== "first-pass" ? 1 : 0
56
- );
57
- const freshIndexCalls = opts.fresh_index_calls ?? (
58
- apiCalls > 0 && (source === "live-capture" || source === "first-pass") ? 1 : 0
59
- );
60
-
61
- return {
62
- session_id: result.trace.trace_id,
63
- started_at: result.trace.started_at,
64
- completed_at: result.trace.completed_at,
65
- trace_version: result.trace.trace_version ?? TRACE_VERSION,
66
- api_calls: apiCalls,
67
- discovery_queries: opts.discovery_queries,
68
- cached_skill_calls: cachedSkillCalls,
69
- fresh_index_calls: freshIndexCalls,
70
- browser_mode: opts.browser_mode ?? "unknown",
71
- };
72
- }
73
-
74
-
75
- /** Process HAR entries into routes and queue for background indexing */
76
- /** Full passive indexing pipeline — same enrichment as explicit capture */
77
- function passiveIndexFromRequests(requests: RawRequest[], pageUrl: string): void {
78
- if (requests.length === 0) return;
79
-
80
- let domain: string;
81
- try { domain = new URL(pageUrl).hostname; } catch { return; }
82
- const intent = `browse ${domain}`;
83
-
84
- // Fire-and-forget — full pipeline runs async
85
- void (async () => {
86
- try {
87
- // 1. Extract endpoints from captured traffic
88
- const rawEndpoints = extractEndpoints(requests, undefined, { pageUrl, finalUrl: pageUrl });
89
- if (rawEndpoints.length === 0) {
90
- console.log(`[passive-index] ${domain}: 0 endpoints from ${requests.length} requests`);
91
- return;
92
- }
93
-
94
- // 2. Extract and store auth credentials (cookies + sensitive headers)
95
- const capturedAuthHeaders = extractAuthHeaders(requests);
96
- if (Object.keys(capturedAuthHeaders).length > 0) {
97
- const authKey = `${domain}-session`;
98
- await storeCredential(authKey, JSON.stringify({ headers: capturedAuthHeaders }));
99
- }
100
-
101
- // 3. Merge with existing skill for this domain (never reduce endpoint count)
102
- const existingSkill = findExistingSkillForDomain(domain, intent);
103
- const mergedEndpoints = existingSkill
104
- ? mergeEndpoints(existingSkill.endpoints, rawEndpoints)
105
- : rawEndpoints;
106
- // Guard: if passive capture found fewer endpoints than what exists, keep the richer set
107
- if (existingSkill && mergedEndpoints.length < existingSkill.endpoints.length) {
108
- console.log(`[passive-index] ${domain}: skipping — would reduce ${existingSkill.endpoints.length} → ${mergedEndpoints.length} endpoints`);
109
- return;
110
- }
111
-
112
- // 4. Generate descriptions for endpoints without them (enables BM25 ranking)
113
- for (const ep of mergedEndpoints) {
114
- if (!ep.description) {
115
- ep.description = generateLocalDescription(ep);
116
- }
117
- }
118
-
119
- // 5. Skip LLM-based augmentation — the calling agent IS the LLM.
120
- // Endpoint descriptions come from generateLocalDescription (heuristic).
121
- // The agent reviews endpoints in the deferral response and picks the right one.
122
- const enrichedEndpoints = mergedEndpoints;
123
-
124
- // 6. Build operation dependency graph
125
- const operationGraph = buildSkillOperationGraph(enrichedEndpoints);
126
-
127
- // 7. Assemble full skill manifest
128
- const skill: SkillManifest = {
129
- skill_id: existingSkill?.skill_id ?? nanoid(),
130
- version: "1.0.0",
131
- schema_version: "1",
132
- lifecycle: "active" as const,
133
- execution_type: "http" as const,
134
- created_at: existingSkill?.created_at ?? new Date().toISOString(),
135
- updated_at: new Date().toISOString(),
136
- name: domain,
137
- intent_signature: intent,
138
- domain,
139
- description: `API skill for ${domain}`,
140
- owner_type: "agent" as const,
141
- endpoints: enrichedEndpoints,
142
- operation_graph: operationGraph,
143
- intents: Array.from(new Set([...(existingSkill?.intents ?? []), intent])),
144
- };
145
-
146
- // 8. Cache locally for immediate reuse — write to BOTH the published skill cache
147
- // AND the domain skill snapshot so resolveAndExecute finds it on next call
148
- try { cachePublishedSkill(skill); } catch { /* best-effort */ }
149
-
150
- // Write domain skill snapshot (keyed by resolve cache key)
151
- const bgCacheKey = buildResolveCacheKey(domain, intent, pageUrl);
152
- const bgScopedKey = scopedCacheKey("global", bgCacheKey);
153
- writeSkillSnapshot(bgScopedKey, skill);
154
-
155
- // Update domain-level reuse cache
156
- const bgDomainKey = getDomainReuseKey(pageUrl ?? domain);
157
- if (bgDomainKey) {
158
- domainSkillCache.set(bgDomainKey, {
159
- skillId: skill.skill_id,
160
- localSkillPath: snapshotPathForCacheKey(bgScopedKey),
161
- ts: Date.now(),
162
- });
163
- persistDomainCache();
164
- }
165
-
166
- // 9. Queue background index (graph building, validation, marketplace publish)
167
- const cacheKey = `passive:${domain}:${Date.now()}`;
168
- queueBackgroundIndex({ skill, domain, intent, contextUrl: pageUrl, cacheKey });
169
-
170
- console.log(`[passive-index] ${domain}: ${enrichedEndpoints.length} endpoints indexed from ${requests.length} requests`);
171
- } catch (err) {
172
- console.error(`[passive-index] ${domain} failed: ${err instanceof Error ? err.message : err}`);
173
- }
174
- })();
175
- }
176
-
177
- /** Convenience wrapper: convert HAR entries and run passive indexing */
178
- function passiveIndexHar(entries: KuriHarEntry[], pageUrl: string): void {
179
- passiveIndexFromRequests(harEntriesToRawRequests(entries), pageUrl);
180
- }
181
- // ── Browse session state (module-level so orchestrator can register sessions) ──
182
- const browseSessions = new Map<string, BrowseSession>();
183
-
184
- /** Register a browse session from the orchestrator (Phase 4 handoff) */
185
- export function registerBrowseSession(tabId: string, url: string, domain: string): void {
186
- browseSessions.set("default", { tabId, url, harActive: true, domain });
187
- }
188
-
189
21
  // ── /v1/stats cache ──────────────────────────────────────────────────
190
22
  let statsCache: { data: unknown; ts: number } | null = null;
191
23
  const STATS_CACHE_TTL = 5 * 60 * 1000; // 5 minutes
@@ -205,11 +37,11 @@ async function fetchStats() {
205
37
 
206
38
  const externalCalls: Promise<unknown>[] = [
207
39
  npmPoint("unbrowse", "last-month"),
208
- npmPoint("unbrowse-openclaw", "last-month"),
40
+ npmPoint("@getfoundry/unbrowse-openclaw", "last-month"),
209
41
  npmPoint("unbrowse", "1970-01-01:2099-12-31"),
210
- npmPoint("unbrowse-openclaw", "1970-01-01:2099-12-31"),
42
+ npmPoint("@getfoundry/unbrowse-openclaw", "1970-01-01:2099-12-31"),
211
43
  npmRange("unbrowse"),
212
- npmRange("unbrowse-openclaw"),
44
+ npmRange("@getfoundry/unbrowse-openclaw"),
213
45
  fetch("https://api.github.com/repos/anthropic-ai/unbrowse", {
214
46
  headers: { "User-Agent": "unbrowse-stats" },
215
47
  }).then(r => r.json() as Promise<Record<string, unknown>>),
@@ -315,11 +147,7 @@ export async function registerRoutes(app: FastifyInstance) {
315
147
  app.addHook("onRequest", async (req, reply) => {
316
148
  if (req.url === "/health" || req.url === "/v1/stats") return;
317
149
 
318
- let key = getApiKey();
319
- if (!key) {
320
- await waitForBackgroundRegistration(15_000);
321
- key = getApiKey();
322
- }
150
+ const key = getApiKey();
323
151
  if (!key) {
324
152
  return reply.code(401).send({
325
153
  error: "api_key_required",
@@ -358,11 +186,6 @@ export async function registerRoutes(app: FastifyInstance) {
358
186
  res.available_endpoints = innerResult.available_endpoints;
359
187
  }
360
188
 
361
- await recordAnalyticsSession(buildAnalyticsSessionPayload(result, {
362
- browser_mode: "replaced",
363
- discovery_queries: 1,
364
- })).catch(() => {});
365
-
366
189
  return reply.send(result);
367
190
  } catch (err) {
368
191
  return reply.code(500).send({ error: (err as Error).message });
@@ -373,173 +196,11 @@ export async function registerRoutes(app: FastifyInstance) {
373
196
  app.get("/v1/skills/:skill_id", async (req, reply) => {
374
197
  const clientScope = clientScopeFor(req);
375
198
  const { skill_id } = req.params as { skill_id: string };
376
- // Check local caches: recent skills domain snapshots → marketplace
377
- let skill = getRecentLocalSkill(skill_id, clientScope);
378
- if (!skill) {
379
- for (const [, entry] of domainSkillCache) {
380
- if (entry.skillId === skill_id && entry.localSkillPath) {
381
- try { skill = JSON.parse(require("fs").readFileSync(entry.localSkillPath, "utf-8")); } catch {}
382
- break;
383
- }
384
- }
385
- }
386
- if (!skill) skill = await getSkill(skill_id, clientScope);
199
+ const skill = getRecentLocalSkill(skill_id, clientScope) ?? await getSkill(skill_id, clientScope);
387
200
  if (!skill) return reply.code(404).send({ error: "Skill not found" });
388
201
  return reply.send(skill);
389
202
  });
390
203
 
391
- // POST /v1/skills/:skill_id/review — agent submits reviewed descriptions + synthetic examples
392
- app.post("/v1/skills/:skill_id/review", async (req, reply) => {
393
- const clientScope = clientScopeFor(req);
394
- const { skill_id } = req.params as { skill_id: string };
395
- const { endpoints: reviews } = req.body as {
396
- endpoints: Array<{
397
- endpoint_id: string;
398
- description?: string;
399
- action_kind?: string;
400
- resource_kind?: string;
401
- example_request?: unknown;
402
- example_response?: unknown;
403
- }>;
404
- };
405
- if (!reviews?.length) return reply.code(400).send({ error: "endpoints[] required" });
406
-
407
- let skill = getRecentLocalSkill(skill_id, clientScope);
408
- if (!skill) {
409
- for (const [, entry] of domainSkillCache) {
410
- if (entry.skillId === skill_id && entry.localSkillPath) {
411
- try { skill = JSON.parse(require("fs").readFileSync(entry.localSkillPath, "utf-8")); } catch {}
412
- break;
413
- }
414
- }
415
- }
416
- if (!skill) skill = await getSkill(skill_id, clientScope);
417
- if (!skill) return reply.code(404).send({ error: "Skill not found" });
418
-
419
- const updated = mergeAgentReview(skill.endpoints, reviews);
420
- skill.endpoints = updated;
421
- skill.updated_at = new Date().toISOString();
422
-
423
- // Update local caches so the next resolve sees reviewed metadata immediately
424
- try { cachePublishedSkill(skill); } catch { /* best-effort */ }
425
- const domain = skill.domain;
426
- if (domain) {
427
- const revCacheKey = buildResolveCacheKey(domain, skill.intent_signature ?? `browse ${domain}`, undefined);
428
- const revScopedKey = scopedCacheKey(clientScope, revCacheKey);
429
- writeSkillSnapshot(revScopedKey, skill);
430
- const revDomainKey = getDomainReuseKey(domain);
431
- if (revDomainKey) {
432
- domainSkillCache.set(revDomainKey, {
433
- skillId: skill.skill_id,
434
- localSkillPath: snapshotPathForCacheKey(revScopedKey),
435
- ts: Date.now(),
436
- });
437
- persistDomainCache();
438
- }
439
- }
440
-
441
- // Also publish to marketplace so all agents benefit — then re-cache
442
- // locally since publishSkill merges backend fields that may overwrite
443
- try { await publishSkill(skill); } catch {}
444
- try { cachePublishedSkill(skill); } catch {}
445
- return reply.send({ ok: true, endpoints_updated: reviews.length });
446
- });
447
-
448
- // POST /v1/skills/:skill_id/publish — two-phase agent-driven publish
449
- // Phase 1 (no endpoints body): return endpoints needing descriptions
450
- // Phase 2 (with endpoints): merge descriptions, update caches, publish to marketplace
451
- app.post("/v1/skills/:skill_id/publish", async (req, reply) => {
452
- const clientScope = clientScopeFor(req);
453
- const { skill_id } = req.params as { skill_id: string };
454
- const { endpoints: reviews } = (req.body as {
455
- endpoints?: Array<{
456
- endpoint_id: string;
457
- description?: string;
458
- action_kind?: string;
459
- resource_kind?: string;
460
- }>;
461
- }) ?? {};
462
-
463
- // Load skill from local caches → marketplace
464
- let skill = getRecentLocalSkill(skill_id, clientScope);
465
- if (!skill) {
466
- for (const [, entry] of domainSkillCache) {
467
- if (entry.skillId === skill_id && entry.localSkillPath) {
468
- try { skill = JSON.parse(require("fs").readFileSync(entry.localSkillPath, "utf-8")); } catch {}
469
- break;
470
- }
471
- }
472
- }
473
- if (!skill) skill = await getSkill(skill_id, clientScope);
474
- if (!skill) return reply.code(404).send({ error: "Skill not found" });
475
-
476
- // Phase 2: merge descriptions + publish
477
- if (reviews?.length) {
478
- const updated = mergeAgentReview(skill.endpoints, reviews);
479
- skill.endpoints = updated;
480
- skill.updated_at = new Date().toISOString();
481
-
482
- // Update local caches
483
- try { cachePublishedSkill(skill); } catch {}
484
- const domain = skill.domain;
485
- if (domain) {
486
- const ck = buildResolveCacheKey(domain, skill.intent_signature ?? `browse ${domain}`, undefined);
487
- const sk = scopedCacheKey(clientScope, ck);
488
- writeSkillSnapshot(sk, skill);
489
- const dk = getDomainReuseKey(domain);
490
- if (dk) {
491
- domainSkillCache.set(dk, {
492
- skillId: skill.skill_id,
493
- localSkillPath: snapshotPathForCacheKey(sk),
494
- ts: Date.now(),
495
- });
496
- persistDomainCache();
497
- }
498
- }
499
-
500
- // Publish to marketplace — then re-cache locally since publishSkill
501
- // merges backend fields that may overwrite our updated endpoints
502
- try { await publishSkill(skill); } catch {}
503
- try { cachePublishedSkill(skill); } catch {}
504
- return reply.send({
505
- ok: true,
506
- skill_id: skill.skill_id,
507
- endpoints_updated: reviews.length,
508
- published: true,
509
- });
510
- }
511
-
512
- // Phase 1: return endpoints needing descriptions
513
- const ranked = rankEndpoints(skill.endpoints, skill.intent_signature, skill.domain);
514
- const endpoints_to_describe = ranked.map((r) => ({
515
- endpoint_id: r.endpoint.endpoint_id,
516
- method: r.endpoint.method,
517
- url: r.endpoint.url_template.length > 120
518
- ? r.endpoint.url_template.slice(0, 120) + "..."
519
- : r.endpoint.url_template,
520
- current_description: r.endpoint.description ?? "",
521
- schema_summary: r.endpoint.response_schema
522
- ? summarizeSchema(r.endpoint.response_schema)
523
- : null,
524
- sample_values: extractSampleValues(r.endpoint.semantic?.example_response_compact),
525
- input_params: r.endpoint.semantic?.requires?.map((b) => ({
526
- key: b.key,
527
- type: b.type ?? b.semantic_type,
528
- required: b.required ?? false,
529
- example: b.example_value,
530
- })) ?? [],
531
- dom_extraction: !!r.endpoint.dom_extraction,
532
- _fill_description: "DESCRIBE THIS ENDPOINT — what it returns, key params, action type",
533
- }));
534
-
535
- return reply.send({
536
- skill_id: skill.skill_id,
537
- domain: skill.domain,
538
- endpoint_count: skill.endpoints.length,
539
- endpoints_to_describe,
540
- _next_step: `Fill each endpoint's description, then call: unbrowse publish --skill ${skill.skill_id} --endpoints '[{endpoint_id, description, action_kind, resource_kind}]'`,
541
- });
542
- });
543
204
  // POST /v1/skills/:skill_id/chunk — dynamic subgraph load for the current intent/bindings
544
205
  app.post("/v1/skills/:skill_id/chunk", async (req, reply) => {
545
206
  const clientScope = clientScopeFor(req);
@@ -572,37 +233,16 @@ export async function registerRoutes(app: FastifyInstance) {
572
233
  intent?: string;
573
234
  context_url?: string;
574
235
  };
575
- // Check local caches first: recent skills domain snapshots → marketplace
576
- let skill = getRecentLocalSkill(skill_id, clientScope);
577
- if (!skill) {
578
- // Check domain snapshot cache — passively indexed skills live here
579
- const { findExistingSkillForDomain: findLocal } = await import("../client/index.js");
580
- for (const [, entry] of domainSkillCache) {
581
- if (entry.skillId === skill_id && entry.localSkillPath) {
582
- try {
583
- skill = JSON.parse(require("fs").readFileSync(entry.localSkillPath, "utf-8"));
584
- } catch { /* snapshot read failed */ }
585
- break;
586
- }
587
- }
588
- }
589
- if (!skill) skill = await getSkill(skill_id, clientScope);
236
+ const skill = getRecentLocalSkill(skill_id, clientScope) ?? await getSkill(skill_id, clientScope);
590
237
  if (!skill) return reply.code(404).send({ error: "Skill not found" });
591
- const execParams = {
592
- ...(params ?? {}),
593
- ...(context_url && typeof params?.url !== "string" ? { url: context_url } : {}),
594
- };
595
238
  try {
596
- const execResult = await executeSkill(skill, execParams, projection, { confirm_unsafe, dry_run, intent, contextUrl: context_url, client_scope: clientScope });
239
+ const execResult = await executeSkill(skill, params ?? {}, projection, { confirm_unsafe, dry_run, intent, contextUrl: context_url, client_scope: clientScope });
597
240
  saveTrace(execResult.trace);
598
- if (execResult.trace.endpoint_id) {
599
- recordExecution(skill.skill_id, execResult.trace.endpoint_id, execResult.trace, skill).catch(() => {});
600
- }
601
241
  if (execResult.trace.success) {
602
242
  promoteExplicitExecution(
603
243
  clientScope,
604
244
  intent || skill.intent_signature,
605
- context_url || (typeof execParams.url === "string" ? execParams.url : undefined),
245
+ context_url || (typeof params?.url === "string" ? params.url : undefined),
606
246
  skill,
607
247
  execResult.trace.endpoint_id,
608
248
  execResult.result,
@@ -619,24 +259,17 @@ export async function registerRoutes(app: FastifyInstance) {
619
259
  try {
620
260
  const recoveryUrl =
621
261
  context_url ||
622
- (typeof execParams.url === "string" && execParams.url) ||
262
+ (typeof params?.url === "string" && params.url) ||
623
263
  skill.endpoints.find((endpoint) => typeof endpoint.trigger_url === "string" && endpoint.trigger_url)?.trigger_url ||
624
264
  `https://${skill.domain}`;
625
265
  const freshResult = await resolveAndExecute(
626
266
  intent || skill.intent_signature,
627
- { ...execParams, url: recoveryUrl },
267
+ { ...(params ?? {}), url: recoveryUrl },
628
268
  { url: recoveryUrl },
629
269
  projection,
630
270
  { confirm_unsafe, dry_run, intent: intent || skill.intent_signature, client_scope: clientScope }
631
271
  );
632
272
  saveTrace(freshResult.trace);
633
- if (freshResult.trace?.skill_id && freshResult.trace?.endpoint_id) {
634
- recordExecution(freshResult.trace.skill_id, freshResult.trace.endpoint_id, freshResult.trace, skill).catch(() => {});
635
- }
636
- await recordAnalyticsSession(buildAnalyticsSessionPayload(freshResult, {
637
- browser_mode: "manual",
638
- discovery_queries: 1,
639
- })).catch(() => {});
640
273
  return reply.send({
641
274
  ...freshResult,
642
275
  _recovery: {
@@ -650,13 +283,6 @@ export async function registerRoutes(app: FastifyInstance) {
650
283
  }
651
284
  }
652
285
 
653
- await recordAnalyticsSession(buildAnalyticsSessionPayload(execResult, {
654
- browser_mode: "manual",
655
- discovery_queries: 0,
656
- cached_skill_calls: execResult.trace.endpoint_id ? 1 : 0,
657
- fresh_index_calls: 0,
658
- })).catch(() => {});
659
-
660
286
  return reply.send(execResult);
661
287
  } catch (err) {
662
288
  return reply.code(500).send({ error: (err as Error).message });
@@ -840,418 +466,6 @@ export async function registerRoutes(app: FastifyInstance) {
840
466
  return reply.code(502).send({ error: `Proxy to beta-api failed: ${(err as Error).message}` });
841
467
  }
842
468
  });
843
-
844
- // ── Browse session management ─────────────────────────────────────────
845
- // Kuri browser actions with passive HAR indexing. The server manages a
846
- // per-session tab + HAR state so every action the agent takes through
847
- // the CLI is passively captured and indexed.
848
-
849
- // browseSessions is module-level (shared with orchestrator via registerBrowseSession)
850
-
851
- /** Extract registrable domain for auth profile naming */
852
- function profileName(url: string): string {
853
- try { return new URL(url).hostname.replace(/^www\./, ""); } catch { return "unknown"; }
854
- }
855
-
856
- async function restartBrowseCapture(session: BrowseSession): Promise<void> {
857
- await kuri.networkEnable(session.tabId).catch(() => {});
858
- await kuri.harStart(session.tabId).catch(() => {});
859
- await kuri.scriptInject(session.tabId, INTERCEPTOR_SCRIPT).catch(() => {});
860
- session.harActive = true;
861
- await injectInterceptor(session.tabId).catch(() => {});
862
- }
863
-
864
- // POST /v1/browse/go — navigate to URL
865
- app.post("/v1/browse/go", async (req, reply) => {
866
- const { url } = req.body as { url: string };
867
- if (!url) return reply.code(400).send({ error: "url required" });
868
- const { session, result } = await withRecoveredBrowseSession(
869
- browseSessions,
870
- kuri,
871
- injectInterceptor,
872
- async (session) => {
873
- const newDomain = profileName(url);
874
-
875
- // Flush prior HAR entries before navigating
876
- if (session.harActive && session.url !== "about:blank") {
877
- try {
878
- const { entries } = await kuri.harStop(session.tabId);
879
- passiveIndexHar(entries, session.url);
880
- } catch { /* non-fatal */ }
881
- session.harActive = false;
882
- }
883
-
884
- // Auto-save auth profile for the old domain before leaving
885
- if (session.domain && session.domain !== newDomain) {
886
- await kuri.authProfileSave(session.tabId, session.domain).catch(() => {});
887
- }
888
-
889
- // Inject cookies: try Kuri auth profile first, fall back to Chrome SQLite extraction
890
- let cookiesInjected = 0;
891
- if (newDomain && newDomain !== session.domain) {
892
- await kuri.authProfileLoad(session.tabId, newDomain).catch(() => {});
893
-
894
- // Also inject cookies from the user's real Chrome/Firefox browser
895
- try {
896
- const { cookies: browserCookies } = extractBrowserCookies(newDomain);
897
- if (browserCookies.length > 0) {
898
- for (const c of browserCookies) {
899
- await kuri.setCookie(session.tabId, c).catch(() => {});
900
- }
901
- cookiesInjected = browserCookies.length;
902
- }
903
- } catch { /* non-fatal */ }
904
- }
905
-
906
- // Start capture BEFORE navigation so all initial API calls are recorded
907
- await restartBrowseCapture(session);
908
-
909
- await kuri.navigate(session.tabId, url);
910
- const finalUrl = await kuri.getCurrentUrl(session.tabId).catch(() => url);
911
- session.url = typeof finalUrl === "string" && finalUrl.startsWith("http") ? finalUrl : url;
912
- session.domain = profileName(session.url);
913
-
914
- await injectInterceptor(session.tabId);
915
-
916
- return { cookiesInjected };
917
- },
918
- (result) => isRecoverableBrowseFailure(result),
919
- );
920
-
921
- return reply.send({
922
- ok: true,
923
- url: session.url,
924
- tab_id: session.tabId,
925
- auth_profile: session.domain,
926
- ...(result.cookiesInjected > 0 ? { cookies_injected: result.cookiesInjected } : {}),
927
- });
928
- });
929
-
930
- // POST /v1/browse/submit — submit active form, fall back to same-origin fetch+rehydrate
931
- app.post("/v1/browse/submit", async (req, reply) => {
932
- const {
933
- form_selector: formSelector,
934
- submit_selector: submitSelector,
935
- wait_for: waitFor,
936
- same_origin_fetch_fallback: sameOriginFetchFallback,
937
- timeout_ms: timeoutMs,
938
- } = (req.body as {
939
- form_selector?: string;
940
- submit_selector?: string;
941
- wait_for?: string;
942
- same_origin_fetch_fallback?: boolean;
943
- timeout_ms?: number;
944
- }) ?? {};
945
-
946
- const { session, result, recovered } = await withRecoveredBrowseSession(
947
- browseSessions,
948
- kuri,
949
- injectInterceptor,
950
- async (session) => submitBrowseForm(
951
- {
952
- client: kuri,
953
- session,
954
- restartCapture: restartBrowseCapture,
955
- rehydratePlugins: kuri.bestEffortRehydratePlugins,
956
- },
957
- {
958
- formSelector,
959
- submitSelector,
960
- waitFor,
961
- sameOriginFetchFallback,
962
- timeoutMs,
963
- },
964
- ),
965
- (result) => !result.ok && result.recoverable === true,
966
- );
967
-
968
- session.url = result.url || await kuri.getCurrentUrl(session.tabId).catch(() => session.url);
969
- session.domain = profileName(session.url);
970
-
971
- const statusCode = result.ok ? 200 : (result.recoverable ? 502 : 400);
972
- return reply.code(statusCode).send({
973
- ...result,
974
- recovered,
975
- tab_id: session.tabId,
976
- url: session.url,
977
- });
978
- });
979
-
980
- // POST /v1/browse/snap — a11y snapshot
981
- app.post("/v1/browse/snap", async (req, reply) => {
982
- const { filter } = (req.body as { filter?: string }) ?? {};
983
- const { session, result: snapshot } = await withRecoveredBrowseSession(
984
- browseSessions,
985
- kuri,
986
- injectInterceptor,
987
- async (session) => kuri.snapshot(session.tabId, filter),
988
- (snapshot) => typeof snapshot !== "string" || snapshot.trim().length === 0,
989
- );
990
- return reply.send({ snapshot, tab_id: session.tabId });
991
- });
992
-
993
- // POST /v1/browse/click — click by ref
994
- app.post("/v1/browse/click", async (req, reply) => {
995
- const { ref } = req.body as { ref: string };
996
- if (!ref) return reply.code(400).send({ error: "ref required" });
997
- await withRecoveredBrowseSession(browseSessions, kuri, injectInterceptor, async (session) => {
998
- await kuri.click(session.tabId, ref);
999
- return true;
1000
- });
1001
- return reply.send({ ok: true });
1002
- });
1003
-
1004
- // POST /v1/browse/fill — fill input by ref
1005
- app.post("/v1/browse/fill", async (req, reply) => {
1006
- const { ref, value } = req.body as { ref: string; value: string };
1007
- if (!ref || value === undefined) return reply.code(400).send({ error: "ref and value required" });
1008
- await withRecoveredBrowseSession(browseSessions, kuri, injectInterceptor, async (session) => {
1009
- await kuri.fill(session.tabId, ref, value);
1010
- return true;
1011
- });
1012
- return reply.send({ ok: true });
1013
- });
1014
-
1015
- // POST /v1/browse/type — keyboard type
1016
- app.post("/v1/browse/type", async (req, reply) => {
1017
- const { text } = req.body as { text: string };
1018
- if (!text) return reply.code(400).send({ error: "text required" });
1019
- await withRecoveredBrowseSession(browseSessions, kuri, injectInterceptor, async (session) => {
1020
- await kuri.keyboardType(session.tabId, text);
1021
- return true;
1022
- });
1023
- return reply.send({ ok: true });
1024
- });
1025
-
1026
- // POST /v1/browse/press — press key
1027
- app.post("/v1/browse/press", async (req, reply) => {
1028
- const { key } = req.body as { key: string };
1029
- if (!key) return reply.code(400).send({ error: "key required" });
1030
- await withRecoveredBrowseSession(browseSessions, kuri, injectInterceptor, async (session) => {
1031
- await kuri.press(session.tabId, key);
1032
- return true;
1033
- });
1034
- return reply.send({ ok: true });
1035
- });
1036
-
1037
- // POST /v1/browse/select — select option by ref
1038
- app.post("/v1/browse/select", async (req, reply) => {
1039
- const { ref, value } = req.body as { ref: string; value: string };
1040
- if (!ref || value === undefined) return reply.code(400).send({ error: "ref and value required" });
1041
- await withRecoveredBrowseSession(browseSessions, kuri, injectInterceptor, async (session) => {
1042
- await kuri.select(session.tabId, ref, value);
1043
- return true;
1044
- });
1045
- return reply.send({ ok: true });
1046
- });
1047
-
1048
- // POST /v1/browse/scroll — scroll
1049
- app.post("/v1/browse/scroll", async (req, reply) => {
1050
- const { direction, amount } = (req.body as { direction?: string; amount?: number }) ?? {};
1051
- await withRecoveredBrowseSession(browseSessions, kuri, injectInterceptor, async (session) => {
1052
- await kuri.scroll(session.tabId, (direction as any) ?? "down", amount);
1053
- return true;
1054
- });
1055
- return reply.send({ ok: true });
1056
- });
1057
-
1058
- // GET /v1/browse/screenshot — capture screenshot
1059
- app.get("/v1/browse/screenshot", async (_req, reply) => {
1060
- const { session, result: data } = await withRecoveredBrowseSession(
1061
- browseSessions,
1062
- kuri,
1063
- injectInterceptor,
1064
- async (session) => kuri.screenshot(session.tabId),
1065
- (data) => typeof data !== "string" || data.trim().length === 0,
1066
- );
1067
- return reply.send({ screenshot: data, tab_id: session.tabId });
1068
- });
1069
-
1070
- // GET /v1/browse/text — page text
1071
- app.get("/v1/browse/text", async (_req, reply) => {
1072
- const { result: text } = await withRecoveredBrowseSession(
1073
- browseSessions,
1074
- kuri,
1075
- injectInterceptor,
1076
- async (session) => kuri.getText(session.tabId),
1077
- (text) => typeof text !== "string",
1078
- );
1079
- return reply.send({ text });
1080
- });
1081
-
1082
- // GET /v1/browse/markdown — page as markdown
1083
- app.get("/v1/browse/markdown", async (_req, reply) => {
1084
- const { result: markdown } = await withRecoveredBrowseSession(
1085
- browseSessions,
1086
- kuri,
1087
- injectInterceptor,
1088
- async (session) => kuri.getMarkdown(session.tabId),
1089
- (markdown) => typeof markdown !== "string",
1090
- );
1091
- return reply.send({ markdown });
1092
- });
1093
-
1094
- // GET /v1/browse/cookies — page cookies
1095
- app.get("/v1/browse/cookies", async (_req, reply) => {
1096
- const { result: cookies } = await withRecoveredBrowseSession(
1097
- browseSessions,
1098
- kuri,
1099
- injectInterceptor,
1100
- async (session) => kuri.getCookies(session.tabId),
1101
- );
1102
- return reply.send({ cookies });
1103
- });
1104
-
1105
- // POST /v1/browse/eval — evaluate JS
1106
- app.post("/v1/browse/eval", async (req, reply) => {
1107
- const { expression } = req.body as { expression: string };
1108
- if (!expression) return reply.code(400).send({ error: "expression required" });
1109
- const { result } = await withRecoveredBrowseSession(
1110
- browseSessions,
1111
- kuri,
1112
- injectInterceptor,
1113
- async (session) => kuri.evaluate(session.tabId, expression),
1114
- (result) => isRecoverableBrowseFailure(result),
1115
- );
1116
- return reply.send({ result });
1117
- });
1118
-
1119
- // POST /v1/browse/back — navigate back
1120
- app.post("/v1/browse/back", async (_req, reply) => {
1121
- await withRecoveredBrowseSession(browseSessions, kuri, injectInterceptor, async (session) => {
1122
- await kuri.goBack(session.tabId);
1123
- return true;
1124
- });
1125
- return reply.send({ ok: true });
1126
- });
1127
-
1128
- // POST /v1/browse/forward — navigate forward
1129
- app.post("/v1/browse/forward", async (_req, reply) => {
1130
- await withRecoveredBrowseSession(browseSessions, kuri, injectInterceptor, async (session) => {
1131
- await kuri.goForward(session.tabId);
1132
- return true;
1133
- });
1134
- return reply.send({ ok: true });
1135
- });
1136
-
1137
- // POST /v1/browse/sync — flush captured traffic into local skill cache without closing tab
1138
- app.post("/v1/browse/sync", async (_req, reply) => {
1139
- const session = browseSessions.get("default");
1140
- if (!session) return reply.send({ ok: false, error: "no active session" });
1141
-
1142
- let intercepted: RawRequest[] = [];
1143
- try {
1144
- const raw = await collectInterceptedRequests(session.tabId);
1145
- intercepted = raw.map((request) => ({
1146
- url: request.url,
1147
- method: request.method,
1148
- request_headers: request.request_headers ?? {},
1149
- request_body: request.request_body,
1150
- response_status: request.response_status,
1151
- response_headers: request.response_headers ?? {},
1152
- response_body: request.response_body,
1153
- timestamp: request.timestamp,
1154
- }));
1155
- } catch { /* non-fatal */ }
1156
-
1157
- let harEntries: KuriHarEntry[] = [];
1158
- if (session.harActive) {
1159
- try {
1160
- const { entries } = await kuri.harStop(session.tabId);
1161
- harEntries = entries;
1162
- } catch { /* non-fatal */ }
1163
- }
1164
- session.harActive = false;
1165
-
1166
- const allRequests = mergeBrowseRequests(intercepted, harEntries, session.url);
1167
- const syncResult = await cacheBrowseRequests({
1168
- sessionUrl: session.url,
1169
- sessionDomain: session.domain,
1170
- requests: allRequests,
1171
- getPageHtml: () => kuri.getPageHtml(session.tabId),
1172
- });
1173
-
1174
- await kuri.networkEnable(session.tabId).catch(() => {});
1175
- await kuri.harStart(session.tabId).catch(() => {});
1176
- await kuri.scriptInject(session.tabId, INTERCEPTOR_SCRIPT).catch(() => {});
1177
- session.harActive = true;
1178
- await injectInterceptor(session.tabId).catch(() => {});
1179
-
1180
- return reply.send({
1181
- ok: true,
1182
- tab_id: session.tabId,
1183
- indexed: syncResult.indexed,
1184
- mode: syncResult.mode,
1185
- domain: syncResult.domain,
1186
- skill_id: syncResult.skill?.skill_id ?? null,
1187
- endpoint_count: syncResult.skill?.endpoints.length ?? 0,
1188
- endpoints: (syncResult.skill?.endpoints ?? []).map((endpoint) => ({
1189
- endpoint_id: endpoint.endpoint_id,
1190
- method: endpoint.method,
1191
- url_template: endpoint.url_template,
1192
- description: endpoint.description,
1193
- trigger_url: endpoint.trigger_url,
1194
- action_kind: endpoint.semantic?.action_kind,
1195
- resource_kind: endpoint.semantic?.resource_kind,
1196
- })),
1197
- });
1198
- });
1199
-
1200
- // POST /v1/browse/close — close session, flush HAR, index, save auth
1201
- app.post("/v1/browse/close", async (_req, reply) => {
1202
- const session = browseSessions.get("default");
1203
- if (!session) return reply.send({ ok: true, message: "no active session" });
1204
-
1205
- // Save auth profile for the current domain before closing
1206
- if (session.domain) {
1207
- await kuri.authProfileSave(session.tabId, session.domain).catch(() => {});
1208
- }
1209
-
1210
- // Collect intercepted fetch/XHR requests (has response bodies HAR misses)
1211
- let intercepted: RawRequest[] = [];
1212
- try {
1213
- const raw = await collectInterceptedRequests(session.tabId);
1214
- intercepted = raw.map(r => ({
1215
- url: r.url,
1216
- method: r.method,
1217
- request_headers: r.request_headers ?? {},
1218
- request_body: r.request_body,
1219
- response_status: r.response_status,
1220
- response_headers: r.response_headers ?? {},
1221
- response_body: r.response_body,
1222
- timestamp: r.timestamp,
1223
- }));
1224
- } catch { /* non-fatal */ }
1225
-
1226
- // Also collect HAR entries
1227
- let harEntries: KuriHarEntry[] = [];
1228
- if (session.harActive) {
1229
- try {
1230
- const { entries } = await kuri.harStop(session.tabId);
1231
- harEntries = entries;
1232
- } catch { /* non-fatal */ }
1233
- }
1234
-
1235
- const allRequests = mergeBrowseRequests(intercepted, harEntries, session.url);
1236
- const syncResult = await cacheBrowseRequests({
1237
- sessionUrl: session.url,
1238
- sessionDomain: session.domain,
1239
- requests: allRequests,
1240
- getPageHtml: () => kuri.getPageHtml(session.tabId),
1241
- });
1242
-
1243
- // Run full async enrichment pipeline (agent augmentation, graph, marketplace publish)
1244
- passiveIndexFromRequests(allRequests, session.url);
1245
- await kuri.closeTab(session.tabId).catch(() => {});
1246
- browseSessions.delete("default");
1247
- return reply.send({
1248
- ok: true,
1249
- indexed: syncResult.indexed,
1250
- mode: syncResult.mode,
1251
- endpoint_count: syncResult.skill?.endpoints.length ?? 0,
1252
- auth_saved: session.domain || null,
1253
- });
1254
- });
1255
469
  }
1256
470
 
1257
471
  function saveTrace(trace: unknown) {