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.
- package/dist/cli.js +79 -33
- package/dist/index.js +2 -6
- package/dist/mcp.js +73 -22
- package/dist/server.js +25994 -0
- package/package.json +1 -2
- package/vendor/kuri/manifest.json +1 -1
- package/runtime-src/agent-outcome.ts +0 -166
- package/runtime-src/analytics-session.ts +0 -55
- package/runtime-src/api/browse-index.ts +0 -343
- package/runtime-src/api/browse-session.ts +0 -572
- package/runtime-src/api/browse-submit-prereqs.ts +0 -48
- package/runtime-src/api/browse-submit.ts +0 -1184
- package/runtime-src/api/routes.ts +0 -1833
- package/runtime-src/auth/browser-cookies.ts +0 -423
- package/runtime-src/auth/index.ts +0 -535
- package/runtime-src/auth/runtime.ts +0 -116
- package/runtime-src/browser/index.ts +0 -659
- package/runtime-src/browser/types.ts +0 -41
- package/runtime-src/build-info.generated.ts +0 -6
- package/runtime-src/capture/index.ts +0 -1929
- package/runtime-src/capture/prefetch.ts +0 -95
- package/runtime-src/capture/rsc.ts +0 -45
- package/runtime-src/cli/shortcuts.ts +0 -273
- package/runtime-src/cli.ts +0 -1734
- package/runtime-src/client/graph-client.ts +0 -100
- package/runtime-src/client/index.ts +0 -1425
- package/runtime-src/debug-trace.ts +0 -18
- package/runtime-src/domain.ts +0 -38
- package/runtime-src/execution/index.ts +0 -3407
- package/runtime-src/execution/retry.ts +0 -46
- package/runtime-src/execution/robots.ts +0 -167
- package/runtime-src/execution/search-forms.ts +0 -188
- package/runtime-src/execution/token-resolver.ts +0 -135
- package/runtime-src/extraction/index.ts +0 -1507
- package/runtime-src/foundry/publish-bundle.ts +0 -392
- package/runtime-src/graph/agent-augment.ts +0 -315
- package/runtime-src/graph/index.ts +0 -1532
- package/runtime-src/graph/local-fixtures.ts +0 -393
- package/runtime-src/graph/local-harness.ts +0 -646
- 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/impact-log.ts +0 -227
- package/runtime-src/index.ts +0 -24
- package/runtime-src/indexer/index.ts +0 -465
- package/runtime-src/intent-match.ts +0 -1515
- package/runtime-src/kuri/client.ts +0 -1839
- package/runtime-src/logger.ts +0 -30
- package/runtime-src/marketplace/index.ts +0 -111
- package/runtime-src/mcp.ts +0 -1911
- 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 -257
- package/runtime-src/orchestrator/first-pass-action.ts +0 -403
- package/runtime-src/orchestrator/index.ts +0 -4480
- package/runtime-src/orchestrator/passive-publish.ts +0 -187
- package/runtime-src/orchestrator/timing-economics.ts +0 -80
- package/runtime-src/payments/cascade.ts +0 -137
- package/runtime-src/payments/index.ts +0 -270
- package/runtime-src/payments/lobster-pay.ts +0 -182
- package/runtime-src/payments/wallet.ts +0 -98
- package/runtime-src/publish/review-context.ts +0 -93
- package/runtime-src/publish/sanitize.ts +0 -197
- package/runtime-src/publish/schema-review.ts +0 -192
- package/runtime-src/publish-admission.ts +0 -388
- package/runtime-src/ratelimit/index.ts +0 -23
- package/runtime-src/reverse-engineer/bundle-scanner.ts +0 -127
- package/runtime-src/reverse-engineer/description-prompt.ts +0 -213
- package/runtime-src/reverse-engineer/index.ts +0 -1551
- package/runtime-src/reverse-engineer/token-sources.ts +0 -379
- package/runtime-src/router.ts +0 -17
- package/runtime-src/routing-telemetry.ts +0 -395
- package/runtime-src/runtime/browser-access.ts +0 -11
- package/runtime-src/runtime/browser-auth.ts +0 -12
- package/runtime-src/runtime/browser-host.ts +0 -48
- package/runtime-src/runtime/lifecycle.ts +0 -17
- package/runtime-src/runtime/local-server.ts +0 -311
- package/runtime-src/runtime/paths.ts +0 -99
- package/runtime-src/runtime/setup.ts +0 -251
- package/runtime-src/runtime/supervisor.ts +0 -69
- package/runtime-src/runtime/update-hints.ts +0 -351
- package/runtime-src/server.ts +0 -100
- package/runtime-src/session-logs.ts +0 -142
- package/runtime-src/settings.ts +0 -221
- package/runtime-src/single-binary.ts +0 -143
- package/runtime-src/site-policy.ts +0 -54
- package/runtime-src/stale-cleanup-runner.ts +0 -144
- package/runtime-src/stale-cleanup.ts +0 -133
- package/runtime-src/telemetry-attribution.ts +0 -120
- package/runtime-src/telemetry.ts +0 -253
- package/runtime-src/template-params.ts +0 -141
- package/runtime-src/transform/drift.ts +0 -60
- package/runtime-src/transform/index.ts +0 -277
- package/runtime-src/types/index.ts +0 -1
- package/runtime-src/types/skill.ts +0 -931
- package/runtime-src/vault/index.ts +0 -196
- package/runtime-src/verification/auth-gate.ts +0 -8
- package/runtime-src/verification/candidates.ts +0 -27
- package/runtime-src/verification/index.ts +0 -120
- package/runtime-src/verification/matrix.ts +0 -30
- package/runtime-src/version.ts +0 -148
- package/runtime-src/workflow/artifact.ts +0 -161
- package/runtime-src/workflow/compile.ts +0 -808
- package/runtime-src/workflow/publish.ts +0 -225
- 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.
|
|
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": "
|
|
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
|
-
}
|