unbrowse 3.1.0-experiments.995f8bb → 3.1.0-experiments.9cbcb13

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 (105) hide show
  1. package/dist/cli.js +79 -33
  2. package/dist/index.js +2 -6
  3. package/dist/mcp.js +73 -22
  4. package/dist/server.js +25994 -0
  5. package/package.json +1 -2
  6. package/vendor/kuri/manifest.json +1 -1
  7. package/runtime-src/agent-outcome.ts +0 -166
  8. package/runtime-src/analytics-session.ts +0 -55
  9. package/runtime-src/api/browse-index.ts +0 -343
  10. package/runtime-src/api/browse-session.ts +0 -572
  11. package/runtime-src/api/browse-submit-prereqs.ts +0 -48
  12. package/runtime-src/api/browse-submit.ts +0 -1184
  13. package/runtime-src/api/routes.ts +0 -1833
  14. package/runtime-src/auth/browser-cookies.ts +0 -423
  15. package/runtime-src/auth/index.ts +0 -535
  16. package/runtime-src/auth/runtime.ts +0 -116
  17. package/runtime-src/browser/index.ts +0 -659
  18. package/runtime-src/browser/types.ts +0 -41
  19. package/runtime-src/build-info.generated.ts +0 -6
  20. package/runtime-src/capture/index.ts +0 -1929
  21. package/runtime-src/capture/prefetch.ts +0 -95
  22. package/runtime-src/capture/rsc.ts +0 -45
  23. package/runtime-src/cli/shortcuts.ts +0 -273
  24. package/runtime-src/cli.ts +0 -1734
  25. package/runtime-src/client/graph-client.ts +0 -100
  26. package/runtime-src/client/index.ts +0 -1425
  27. package/runtime-src/debug-trace.ts +0 -18
  28. package/runtime-src/domain.ts +0 -38
  29. package/runtime-src/execution/index.ts +0 -3407
  30. package/runtime-src/execution/retry.ts +0 -46
  31. package/runtime-src/execution/robots.ts +0 -167
  32. package/runtime-src/execution/search-forms.ts +0 -188
  33. package/runtime-src/execution/token-resolver.ts +0 -135
  34. package/runtime-src/extraction/index.ts +0 -1507
  35. package/runtime-src/foundry/publish-bundle.ts +0 -392
  36. package/runtime-src/graph/agent-augment.ts +0 -315
  37. package/runtime-src/graph/index.ts +0 -1532
  38. package/runtime-src/graph/local-fixtures.ts +0 -393
  39. package/runtime-src/graph/local-harness.ts +0 -646
  40. package/runtime-src/graph/planner.ts +0 -411
  41. package/runtime-src/graph/session.ts +0 -294
  42. package/runtime-src/graph/trace-store.ts +0 -136
  43. package/runtime-src/impact-log.ts +0 -227
  44. package/runtime-src/index.ts +0 -24
  45. package/runtime-src/indexer/index.ts +0 -465
  46. package/runtime-src/intent-match.ts +0 -1515
  47. package/runtime-src/kuri/client.ts +0 -1839
  48. package/runtime-src/logger.ts +0 -30
  49. package/runtime-src/marketplace/index.ts +0 -111
  50. package/runtime-src/mcp.ts +0 -1911
  51. package/runtime-src/orchestrator/browser-agent.ts +0 -374
  52. package/runtime-src/orchestrator/dag-advisor.ts +0 -59
  53. package/runtime-src/orchestrator/dag-feedback.ts +0 -257
  54. package/runtime-src/orchestrator/first-pass-action.ts +0 -403
  55. package/runtime-src/orchestrator/index.ts +0 -4480
  56. package/runtime-src/orchestrator/passive-publish.ts +0 -187
  57. package/runtime-src/orchestrator/timing-economics.ts +0 -80
  58. package/runtime-src/payments/cascade.ts +0 -137
  59. package/runtime-src/payments/index.ts +0 -270
  60. package/runtime-src/payments/lobster-pay.ts +0 -182
  61. package/runtime-src/payments/wallet.ts +0 -98
  62. package/runtime-src/publish/review-context.ts +0 -93
  63. package/runtime-src/publish/sanitize.ts +0 -197
  64. package/runtime-src/publish/schema-review.ts +0 -192
  65. package/runtime-src/publish-admission.ts +0 -388
  66. package/runtime-src/ratelimit/index.ts +0 -23
  67. package/runtime-src/reverse-engineer/bundle-scanner.ts +0 -127
  68. package/runtime-src/reverse-engineer/description-prompt.ts +0 -213
  69. package/runtime-src/reverse-engineer/index.ts +0 -1551
  70. package/runtime-src/reverse-engineer/token-sources.ts +0 -379
  71. package/runtime-src/router.ts +0 -17
  72. package/runtime-src/routing-telemetry.ts +0 -395
  73. package/runtime-src/runtime/browser-access.ts +0 -11
  74. package/runtime-src/runtime/browser-auth.ts +0 -12
  75. package/runtime-src/runtime/browser-host.ts +0 -48
  76. package/runtime-src/runtime/lifecycle.ts +0 -17
  77. package/runtime-src/runtime/local-server.ts +0 -311
  78. package/runtime-src/runtime/paths.ts +0 -99
  79. package/runtime-src/runtime/setup.ts +0 -251
  80. package/runtime-src/runtime/supervisor.ts +0 -69
  81. package/runtime-src/runtime/update-hints.ts +0 -351
  82. package/runtime-src/server.ts +0 -100
  83. package/runtime-src/session-logs.ts +0 -142
  84. package/runtime-src/settings.ts +0 -221
  85. package/runtime-src/single-binary.ts +0 -143
  86. package/runtime-src/site-policy.ts +0 -54
  87. package/runtime-src/stale-cleanup-runner.ts +0 -144
  88. package/runtime-src/stale-cleanup.ts +0 -133
  89. package/runtime-src/telemetry-attribution.ts +0 -120
  90. package/runtime-src/telemetry.ts +0 -253
  91. package/runtime-src/template-params.ts +0 -141
  92. package/runtime-src/transform/drift.ts +0 -60
  93. package/runtime-src/transform/index.ts +0 -277
  94. package/runtime-src/types/index.ts +0 -1
  95. package/runtime-src/types/skill.ts +0 -931
  96. package/runtime-src/vault/index.ts +0 -196
  97. package/runtime-src/verification/auth-gate.ts +0 -8
  98. package/runtime-src/verification/candidates.ts +0 -27
  99. package/runtime-src/verification/index.ts +0 -120
  100. package/runtime-src/verification/matrix.ts +0 -30
  101. package/runtime-src/version.ts +0 -148
  102. package/runtime-src/workflow/artifact.ts +0 -161
  103. package/runtime-src/workflow/compile.ts +0 -808
  104. package/runtime-src/workflow/publish.ts +0 -225
  105. package/runtime-src/workflow/runtime.ts +0 -213
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unbrowse",
3
- "version": "3.1.0-experiments.995f8bb",
3
+ "version": "3.1.0-experiments.9cbcb13",
4
4
  "description": "Reverse-engineer any website into reusable API skills. Zero-dep single binary with embedded browser engine.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -9,7 +9,6 @@
9
9
  "files": [
10
10
  "bin",
11
11
  "dist",
12
- "runtime-src",
13
12
  "vendor/kuri",
14
13
  "scripts/release-assets.mjs",
15
14
  "scripts/postinstall.mjs",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "repo_url": "https://github.com/justrach/kuri.git",
3
3
  "branch": "adding-extensions",
4
- "source_sha": "08eecbe3740f046a46f656eed7ebfc66c1bad9bb",
4
+ "source_sha": "eadfaa5f921f7152e1762aed5ed64b3a4fbefbf3",
5
5
  "built_at": "2026-04-05T06:43:57.212Z",
6
6
  "binaries": {
7
7
  "darwin-arm64": {
@@ -1,166 +0,0 @@
1
- import type { OrchestrationTiming, SkillManifest } from "./types/index.js";
2
-
3
- export interface AgentImpact {
4
- source: string;
5
- cache_hit: boolean;
6
- browser_avoided: boolean;
7
- baseline_total_ms?: number;
8
- actual_total_ms?: number;
9
- time_saved_ms?: number;
10
- time_saved_pct: number;
11
- tokens_saved: number;
12
- tokens_saved_pct: number;
13
- baseline_cost_uc?: number;
14
- actual_cost_uc?: number;
15
- cost_saved_uc?: number;
16
- }
17
-
18
- export interface AgentNextAction {
19
- endpoint_id: string;
20
- operation_id: string;
21
- title: string;
22
- why: string;
23
- command: string;
24
- }
25
-
26
- const BROWSER_SOURCES = new Set(["live-capture", "first-pass", "browser-action"]);
27
-
28
- function edgePriority(kind: string): number {
29
- switch (kind) {
30
- case "parent_child":
31
- return 4;
32
- case "pagination":
33
- return 3;
34
- case "dependency":
35
- return 2;
36
- case "hint":
37
- return 1;
38
- case "auth":
39
- return 0;
40
- default:
41
- return -1;
42
- }
43
- }
44
-
45
- function nextActionWhy(kind: string, bindingKey: string, title: string): string {
46
- switch (kind) {
47
- case "parent_child":
48
- return `Likely next detail step after this result. Exposes ${title}.`;
49
- case "pagination":
50
- return `Likely next page or continuation step. Carries ${bindingKey || "cursor"} forward.`;
51
- case "dependency":
52
- return `Unlocks the next dependent call using ${bindingKey || "known bindings"}.`;
53
- case "auth":
54
- return "Useful once authentication is in place.";
55
- case "hint":
56
- return "Common follow-up action from the current result.";
57
- default:
58
- return "Likely follow-up action.";
59
- }
60
- }
61
-
62
- function operationTitle(operation: NonNullable<SkillManifest["operation_graph"]>["operations"][number]): string {
63
- const semantic = [operation.action_kind, operation.resource_kind]
64
- .filter(Boolean)
65
- .join(" ")
66
- .replace(/_/g, " ")
67
- .trim();
68
- return operation.description_out || semantic || operation.endpoint_id;
69
- }
70
-
71
- export function buildAgentImpact(
72
- timing?: Partial<OrchestrationTiming> | null,
73
- ): AgentImpact | undefined {
74
- if (!timing?.source) return undefined;
75
- return {
76
- source: timing.source,
77
- cache_hit: timing.cache_hit === true,
78
- browser_avoided: !BROWSER_SOURCES.has(timing.source),
79
- baseline_total_ms: timing.baseline_total_ms,
80
- actual_total_ms: timing.actual_total_ms,
81
- time_saved_ms: timing.time_saved_ms,
82
- time_saved_pct: timing.time_saved_pct ?? 0,
83
- tokens_saved: timing.tokens_saved ?? 0,
84
- tokens_saved_pct: timing.tokens_saved_pct ?? 0,
85
- baseline_cost_uc: timing.baseline_cost_uc,
86
- actual_cost_uc: timing.actual_cost_uc,
87
- cost_saved_uc: timing.cost_saved_uc,
88
- };
89
- }
90
-
91
- export function buildNextActions(
92
- skill: SkillManifest | undefined,
93
- endpointId: string | undefined,
94
- maxActions = 3,
95
- ): AgentNextAction[] {
96
- if (!skill?.operation_graph || !endpointId) return [];
97
- const graph = skill.operation_graph;
98
- const current = graph.operations.find((operation) => operation.endpoint_id === endpointId);
99
- if (!current) return [];
100
-
101
- const byOperationId = new Map(graph.operations.map((operation) => [operation.operation_id, operation]));
102
- const scored = new Map<string, {
103
- operation_id: string;
104
- endpoint_id: string;
105
- title: string;
106
- why: string;
107
- score: number;
108
- }>();
109
-
110
- for (const edge of graph.edges) {
111
- if (edge.from_operation_id !== current.operation_id) continue;
112
- const target = byOperationId.get(edge.to_operation_id);
113
- if (!target) continue;
114
-
115
- const candidate = {
116
- operation_id: target.operation_id,
117
- endpoint_id: target.endpoint_id,
118
- title: operationTitle(target),
119
- why: nextActionWhy(edge.kind, edge.binding_key, operationTitle(target)),
120
- score: (edgePriority(edge.kind) * 10) + Math.round(edge.confidence * 10),
121
- };
122
- const existing = scored.get(target.operation_id);
123
- if (!existing || candidate.score > existing.score) {
124
- scored.set(target.operation_id, candidate);
125
- }
126
- }
127
-
128
- return [...scored.values()]
129
- .sort((a, b) => b.score - a.score || a.title.localeCompare(b.title))
130
- .slice(0, maxActions)
131
- .map((candidate) => ({
132
- endpoint_id: candidate.endpoint_id,
133
- operation_id: candidate.operation_id,
134
- title: candidate.title,
135
- why: candidate.why,
136
- command: `unbrowse execute --skill ${skill.skill_id} --endpoint ${candidate.endpoint_id}`,
137
- }));
138
- }
139
-
140
- export function attachAgentOutcomeHints<T extends Record<string, unknown>>(
141
- payload: T,
142
- opts?: {
143
- skill?: SkillManifest;
144
- endpointId?: string;
145
- timing?: Partial<OrchestrationTiming> | null;
146
- },
147
- ): T & {
148
- impact?: AgentImpact;
149
- next_actions?: AgentNextAction[];
150
- } {
151
- const target = payload as Record<string, unknown>;
152
- const impact = buildAgentImpact(opts?.timing);
153
- if (impact) {
154
- target.impact = impact;
155
- }
156
-
157
- const nextActions = buildNextActions(opts?.skill, opts?.endpointId);
158
- if (nextActions.length > 0) {
159
- target.next_actions = nextActions;
160
- }
161
-
162
- return target as T & {
163
- impact?: AgentImpact;
164
- next_actions?: AgentNextAction[];
165
- };
166
- }
@@ -1,55 +0,0 @@
1
- import type { ExecutionTrace, OrchestrationTiming } from "./types/index.js";
2
-
3
- export interface AnalyticsSessionPayload {
4
- session_id: string;
5
- started_at: string;
6
- completed_at?: string;
7
- trace_version?: string;
8
- api_calls: number;
9
- discovery_queries: number;
10
- cached_skill_calls: number;
11
- fresh_index_calls: number;
12
- browser_mode: "default" | "replaced" | "manual" | "unknown";
13
- success?: boolean;
14
- source?: string;
15
- time_saved_ms?: number;
16
- time_saved_pct?: number;
17
- tokens_saved?: number;
18
- tokens_saved_pct?: number;
19
- cost_saved_uc?: number;
20
- }
21
-
22
- export function buildAnalyticsSessionPayload(
23
- sessionId: string,
24
- startedAt: string,
25
- source: OrchestrationTiming["source"] | "first-pass",
26
- trace: Pick<ExecutionTrace, "completed_at" | "trace_version" | "success" | "tokens_saved" | "tokens_saved_pct" | "api_call_count"> & {
27
- network_events?: unknown[];
28
- },
29
- timing?: Pick<OrchestrationTiming, "time_saved_ms" | "time_saved_pct" | "cost_saved_uc">,
30
- ): AnalyticsSessionPayload {
31
- const cacheLike = source === "marketplace" || source === "route-cache";
32
- const browserMode = source === "live-capture" || source === "browser-action"
33
- ? "default"
34
- : source === "first-pass"
35
- ? "default"
36
- : "replaced";
37
- return {
38
- session_id: sessionId,
39
- started_at: startedAt,
40
- completed_at: trace.completed_at,
41
- trace_version: trace.trace_version,
42
- api_calls: trace.api_call_count ?? Math.max(1, trace.network_events?.length ?? 0),
43
- discovery_queries: cacheLike ? 1 : 0,
44
- cached_skill_calls: cacheLike ? 1 : 0,
45
- fresh_index_calls: source === "live-capture" || source === "first-pass" || source === "browser-action" ? 1 : 0,
46
- browser_mode: browserMode,
47
- success: trace.success ?? true,
48
- source,
49
- time_saved_ms: timing?.time_saved_ms,
50
- time_saved_pct: timing?.time_saved_pct,
51
- tokens_saved: trace.tokens_saved,
52
- tokens_saved_pct: trace.tokens_saved_pct,
53
- cost_saved_uc: timing?.cost_saved_uc,
54
- };
55
- }
@@ -1,343 +0,0 @@
1
- import { nanoid } from "nanoid";
2
- import { readFileSync } from "node:fs";
3
- import { log } from "../logger.js";
4
- import { extractEndpoints, extractAuthHeaders } from "../reverse-engineer/index.js";
5
- import { enrichEndpointsWithTokenSources } from "../reverse-engineer/token-sources.js";
6
- import { buildSkillOperationGraph, inferEndpointSemantic } from "../graph/index.js";
7
- import { validateExtractionQuality } from "../execution/index.js";
8
- import { assessIntentResult } from "../intent-match.js";
9
- import type { KuriHarEntry } from "../kuri/client.js";
10
- import type { EndpointDescriptor, SkillManifest } from "../types/index.js";
11
- import type { RawRequest } from "../capture/index.js";
12
- import { cachePublishedSkill, findExistingSkillForDomain } from "../client/index.js";
13
- import { mergeEndpoints } from "../marketplace/index.js";
14
- import { upsertDagEdgesFromOperationGraph } from "../orchestrator/dag-feedback.js";
15
- import { storeCredential } from "../vault/index.js";
16
- import { getRegistrableDomain } from "../domain.js";
17
- import {
18
- buildResolveCacheKey,
19
- domainSkillCache,
20
- generateLocalDescription,
21
- getDomainReuseKey,
22
- invalidateRouteCacheForDomain,
23
- persistDomainCache,
24
- scopedCacheKey,
25
- snapshotPathForCacheKey,
26
- writeSkillSnapshot,
27
- } from "../orchestrator/index.js";
28
-
29
- function normalizeBrowseUrl(url: string, baseUrl?: string): string {
30
- if (!url) return url;
31
- try {
32
- return new URL(url).toString();
33
- } catch {
34
- if (!baseUrl) return url;
35
- try {
36
- return new URL(url, baseUrl).toString();
37
- } catch {
38
- return url;
39
- }
40
- }
41
- }
42
-
43
- export function harEntriesToRawRequests(entries: KuriHarEntry[], baseUrl?: string): RawRequest[] {
44
- return entries
45
- .filter((entry) => entry.request && entry.response)
46
- .map((entry) => ({
47
- url: normalizeBrowseUrl(entry.request.url, baseUrl),
48
- method: entry.request.method,
49
- request_headers: Object.fromEntries((entry.request.headers ?? []).map((header) => [header.name.toLowerCase(), header.value])),
50
- request_body: entry.request.postData?.text,
51
- response_status: entry.response.status,
52
- response_headers: Object.fromEntries((entry.response.headers ?? []).map((header) => [header.name.toLowerCase(), header.value])),
53
- response_body: entry.response.content?.text,
54
- timestamp: entry.startedDateTime ?? new Date().toISOString(),
55
- }));
56
- }
57
-
58
- export function buildBrowseRequestKey(request: RawRequest): string {
59
- return [
60
- request.method,
61
- request.url,
62
- typeof request.request_body === "string" ? request.request_body : JSON.stringify(request.request_body ?? null),
63
- ].join(":");
64
- }
65
-
66
- export function mergeBrowseRequests(intercepted: RawRequest[], harEntries: KuriHarEntry[], baseUrl?: string): RawRequest[] {
67
- const normalizedIntercepted = intercepted.map((request) => ({
68
- ...request,
69
- url: normalizeBrowseUrl(request.url, baseUrl),
70
- }));
71
- const harRequests = harEntriesToRawRequests(harEntries, baseUrl);
72
- const seen = new Set<string>();
73
- const allRequests: RawRequest[] = [];
74
-
75
- for (const request of normalizedIntercepted) {
76
- const key = buildBrowseRequestKey(request);
77
- if (!seen.has(key)) {
78
- seen.add(key);
79
- allRequests.push(request);
80
- }
81
- }
82
-
83
- for (const request of harRequests) {
84
- const key = buildBrowseRequestKey(request);
85
- if (!seen.has(key)) {
86
- seen.add(key);
87
- allRequests.push(request);
88
- }
89
- }
90
-
91
- return allRequests;
92
- }
93
-
94
- export interface BrowseIndexResult {
95
- domain: string;
96
- indexed: boolean;
97
- mode: "http" | "dom" | "none";
98
- skill: SkillManifest | null;
99
- }
100
-
101
- export function shouldIndexDomBrowseFallback(params: {
102
- requestCount: number;
103
- intent: string;
104
- extractedData: unknown;
105
- extractedConfidence: number;
106
- hasStructuredForm: boolean;
107
- }): {
108
- allow: boolean;
109
- reason?: string;
110
- intentLooksSearch: boolean;
111
- } {
112
- const { requestCount, intent, extractedData, extractedConfidence, hasStructuredForm } = params;
113
- const intentLooksSearch = /\b(search|find|lookup|filter)\b/i.test(intent);
114
-
115
- if (!extractedData) {
116
- if (hasStructuredForm && requestCount > 0 && intentLooksSearch) {
117
- return { allow: true, intentLooksSearch };
118
- }
119
- return {
120
- allow: false,
121
- reason: hasStructuredForm ? "form_only_without_network_evidence" : "no_dom_data",
122
- intentLooksSearch,
123
- };
124
- }
125
-
126
- const quality = validateExtractionQuality(extractedData, extractedConfidence, intent);
127
- if (!quality.valid) {
128
- if (hasStructuredForm && requestCount > 0 && intentLooksSearch) {
129
- return { allow: true, intentLooksSearch };
130
- }
131
- return {
132
- allow: false,
133
- reason: quality.quality_note ?? "low_quality_dom_extraction",
134
- intentLooksSearch,
135
- };
136
- }
137
-
138
- const semanticAssessment = assessIntentResult(extractedData, intent);
139
- if (semanticAssessment.verdict === "fail") {
140
- if (hasStructuredForm && requestCount > 0 && intentLooksSearch) {
141
- return { allow: true, intentLooksSearch };
142
- }
143
- return {
144
- allow: false,
145
- reason: semanticAssessment.reason ?? "dom_extraction_did_not_match_intent",
146
- intentLooksSearch,
147
- };
148
- }
149
-
150
- return { allow: true, intentLooksSearch };
151
- }
152
-
153
- export async function cacheBrowseRequests(params: {
154
- sessionUrl: string;
155
- sessionDomain: string;
156
- requests: RawRequest[];
157
- getPageHtml?: () => Promise<string>;
158
- jsBundles?: Map<string, string>;
159
- intent?: string;
160
- }): Promise<BrowseIndexResult> {
161
- const { sessionUrl, sessionDomain, requests, getPageHtml, jsBundles } = params;
162
- let domain: string;
163
- try { domain = new URL(sessionUrl).hostname; } catch { domain = sessionDomain; }
164
- const intent = params.intent ?? `browse ${domain}`;
165
-
166
- const rawEndpoints = extractEndpoints(requests, undefined, { pageUrl: sessionUrl, finalUrl: sessionUrl });
167
-
168
- // Extract and persist auth headers (authorization, csrf, bearer tokens)
169
- // so serverFetch can replay them. Use registrable domain for vault key
170
- // so ads.x.com and ads-api.x.com share the same session.
171
- const capturedAuthHeaders = extractAuthHeaders(requests);
172
- if (Object.keys(capturedAuthHeaders).length > 0) {
173
- const sessionKey = `${getRegistrableDomain(domain)}-session`;
174
- await storeCredential(sessionKey, JSON.stringify({ headers: capturedAuthHeaders })).catch(() => {});
175
- }
176
-
177
- if (rawEndpoints.length > 0) {
178
- const existingSkill = findExistingSkillForDomain(domain);
179
- let allExisting = existingSkill?.endpoints ?? [];
180
-
181
- const domainKey = getDomainReuseKey(sessionUrl ?? domain);
182
- if (domainKey) {
183
- const cached = domainSkillCache.get(domainKey);
184
- if (cached?.localSkillPath) {
185
- try {
186
- const snapshot = JSON.parse(readFileSync(cached.localSkillPath, "utf-8"));
187
- if (snapshot?.endpoints?.length > 0) {
188
- allExisting = mergeEndpoints(allExisting, snapshot.endpoints);
189
- }
190
- } catch {
191
- // ignore stale snapshot
192
- }
193
- }
194
- }
195
-
196
- const mergedEndpoints = allExisting.length > 0 ? mergeEndpoints(allExisting, rawEndpoints) : rawEndpoints;
197
- if (!existingSkill || mergedEndpoints.length >= existingSkill.endpoints.length) {
198
- for (const endpoint of mergedEndpoints) {
199
- if (!endpoint.description) endpoint.description = generateLocalDescription(endpoint);
200
- if (!endpoint.semantic) endpoint.semantic = inferEndpointSemantic(endpoint);
201
- }
202
- const quickSkill: SkillManifest = {
203
- skill_id: existingSkill?.skill_id ?? nanoid(),
204
- version: "1.0.0",
205
- schema_version: "1",
206
- lifecycle: "active",
207
- execution_type: "http",
208
- created_at: existingSkill?.created_at ?? new Date().toISOString(),
209
- updated_at: new Date().toISOString(),
210
- name: domain,
211
- intent_signature: intent,
212
- domain,
213
- description: `API skill for ${domain}`,
214
- owner_type: "agent",
215
- endpoints: mergedEndpoints,
216
- operation_graph: buildSkillOperationGraph(mergedEndpoints),
217
- intents: Array.from(new Set([...(existingSkill?.intents ?? []), intent])),
218
- };
219
-
220
- // Token source discovery: scan live HTML for tokens used in captured
221
- // request headers and attach AuthTokenBinding entries so serverFetch
222
- // can rescrape fresh tokens on replay.
223
- try {
224
- const html = getPageHtml ? await getPageHtml() : undefined;
225
- if (html && html.startsWith("<")) {
226
- const preCheck = requests.filter(r => r.request_headers["authorization"] || r.request_headers["x-csrf-token"]).length;
227
- const enriched = enrichEndpointsWithTokenSources(quickSkill.endpoints, requests, html, jsBundles);
228
- log("browse-index", `token enrichment: ${enriched} bindings, ${preCheck} auth-reqs pre-call, ${quickSkill.endpoints.length} eps`);
229
- }
230
- } catch (e) { log("browse-index", `token enrichment failed: ${e}`); }
231
-
232
- const cacheKey = buildResolveCacheKey(domain, intent, sessionUrl);
233
- const scopedKey = scopedCacheKey("global", cacheKey);
234
- writeSkillSnapshot(scopedKey, quickSkill);
235
- if (domainKey) {
236
- domainSkillCache.set(domainKey, {
237
- skillId: quickSkill.skill_id,
238
- localSkillPath: snapshotPathForCacheKey(scopedKey),
239
- ts: Date.now(),
240
- });
241
- persistDomainCache();
242
- }
243
- try { cachePublishedSkill(quickSkill); } catch {}
244
- upsertDagEdgesFromOperationGraph(quickSkill);
245
- invalidateRouteCacheForDomain(domain);
246
- return { domain, indexed: true, mode: "http", skill: quickSkill };
247
- }
248
-
249
- return { domain, indexed: false, mode: "http", skill: existingSkill ?? null };
250
- }
251
-
252
- if (!getPageHtml) return { domain, indexed: false, mode: "none", skill: null };
253
-
254
- try {
255
- const html = await getPageHtml();
256
- if (!html || !html.startsWith("<")) return { domain, indexed: false, mode: "none", skill: null };
257
-
258
- const { extractFromDOM } = await import("../extraction/index.js");
259
- const { detectSearchForms, isStructuredSearchForm } = await import("../execution/search-forms.js");
260
- const { inferSchema } = await import("../transform/index.js");
261
- const { templatizeQueryParams } = await import("../execution/index.js");
262
-
263
- const extracted = extractFromDOM(html, intent);
264
- const searchForms = detectSearchForms(html);
265
- const validForm = searchForms.find((form: { form_selector: string; fields: unknown[] }) => isStructuredSearchForm(form));
266
- const domDecision = shouldIndexDomBrowseFallback({
267
- requestCount: requests.length,
268
- intent,
269
- extractedData: extracted.data,
270
- extractedConfidence: extracted.confidence,
271
- hasStructuredForm: !!validForm,
272
- });
273
-
274
- if (!domDecision.allow || !extracted.data) return { domain, indexed: false, mode: "none", skill: null };
275
-
276
- const urlTemplate = templatizeQueryParams(sessionUrl);
277
- const endpoint: EndpointDescriptor = {
278
- endpoint_id: nanoid(),
279
- method: "GET",
280
- url_template: urlTemplate,
281
- idempotency: "safe",
282
- verification_status: "verified",
283
- reliability_score: extracted.confidence ?? 0.7,
284
- description: validForm && domDecision.intentLooksSearch ? `Search form for ${domain}` : `Page content from ${domain}`,
285
- response_schema: inferSchema([extracted.data]),
286
- dom_extraction: {
287
- extraction_method: extracted.extraction_method ?? "repeated-elements",
288
- confidence: extracted.confidence ?? 0.7,
289
- ...(extracted.selector ? { selector: extracted.selector } : {}),
290
- },
291
- trigger_url: sessionUrl,
292
- ...(validForm && domDecision.intentLooksSearch ? { search_form: validForm } : {}),
293
- };
294
-
295
- endpoint.semantic = inferEndpointSemantic(endpoint, {
296
- sampleResponse: extracted.data,
297
- observedAt: new Date().toISOString(),
298
- sampleRequestUrl: sessionUrl,
299
- });
300
-
301
- const existing = findExistingSkillForDomain(domain);
302
- const allEndpoints = existing ? mergeEndpoints(existing.endpoints, [endpoint]) : [endpoint];
303
- for (const candidate of allEndpoints) {
304
- if (!candidate.description) candidate.description = generateLocalDescription(candidate);
305
- }
306
-
307
- const skill: SkillManifest = {
308
- skill_id: existing?.skill_id ?? nanoid(),
309
- version: "1.0.0",
310
- schema_version: "1",
311
- lifecycle: "active",
312
- execution_type: "http",
313
- created_at: existing?.created_at ?? new Date().toISOString(),
314
- updated_at: new Date().toISOString(),
315
- name: domain,
316
- intent_signature: intent,
317
- domain,
318
- description: `DOM skill for ${domain}`,
319
- owner_type: "agent",
320
- endpoints: allEndpoints,
321
- operation_graph: buildSkillOperationGraph(allEndpoints),
322
- intents: [...new Set([...(existing?.intents ?? []), intent])],
323
- };
324
- const cacheKey = buildResolveCacheKey(domain, intent, sessionUrl);
325
- const scopedKey = scopedCacheKey("global", cacheKey);
326
- writeSkillSnapshot(scopedKey, skill);
327
- const domainReuseKey = getDomainReuseKey(sessionUrl ?? domain);
328
- if (domainReuseKey) {
329
- domainSkillCache.set(domainReuseKey, {
330
- skillId: skill.skill_id,
331
- localSkillPath: snapshotPathForCacheKey(scopedKey),
332
- ts: Date.now(),
333
- });
334
- persistDomainCache();
335
- }
336
- try { cachePublishedSkill(skill); } catch {}
337
- upsertDagEdgesFromOperationGraph(skill);
338
- invalidateRouteCacheForDomain(domain);
339
- return { domain, indexed: true, mode: "dom", skill };
340
- } catch {
341
- return { domain, indexed: false, mode: "none", skill: null };
342
- }
343
- }