unbrowse 2.12.2 → 2.12.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -44
- package/dist/cli.js +514 -20723
- package/package.json +4 -10
- package/runtime-src/api/routes.ts +15 -801
- package/runtime-src/auth/index.ts +32 -142
- package/runtime-src/capture/index.ts +101 -436
- package/runtime-src/cli.ts +371 -956
- package/runtime-src/client/index.ts +29 -622
- package/runtime-src/execution/index.ts +85 -345
- package/runtime-src/graph/index.ts +10 -128
- package/runtime-src/intent-match.ts +27 -27
- package/runtime-src/kuri/client.ts +82 -543
- package/runtime-src/orchestrator/index.ts +462 -2246
- package/runtime-src/reverse-engineer/index.ts +22 -220
- package/runtime-src/runtime/local-server.ts +16 -149
- package/runtime-src/runtime/paths.ts +5 -9
- package/runtime-src/runtime/setup.ts +1 -52
- package/runtime-src/server.ts +11 -6
- package/runtime-src/transform/schema-hints.ts +358 -0
- package/runtime-src/types/skill.ts +2 -49
- package/runtime-src/verification/index.ts +0 -15
- package/runtime-src/version.ts +13 -13
- package/vendor/kuri/darwin-arm64/kuri +0 -0
- package/vendor/kuri/darwin-x64/kuri +0 -0
- package/vendor/kuri/linux-arm64/kuri +0 -0
- package/vendor/kuri/linux-x64/kuri +0 -0
- package/bin/unbrowse-wrapper.mjs +0 -39
- package/bin/unbrowse.js +0 -38
- package/runtime-src/analytics-session.ts +0 -33
- package/runtime-src/api/browse-index.ts +0 -254
- package/runtime-src/api/browse-session.ts +0 -179
- package/runtime-src/api/browse-submit.ts +0 -455
- package/runtime-src/auth/runtime.ts +0 -116
- package/runtime-src/browser/index.ts +0 -635
- package/runtime-src/browser/types.ts +0 -41
- package/runtime-src/capture/prefetch.ts +0 -122
- package/runtime-src/capture/rsc.ts +0 -45
- package/runtime-src/cli/shortcuts.ts +0 -273
- package/runtime-src/client/graph-client.ts +0 -99
- package/runtime-src/execution/robots.ts +0 -167
- package/runtime-src/execution/search-forms.ts +0 -188
- package/runtime-src/graph/planner.ts +0 -411
- package/runtime-src/graph/session.ts +0 -294
- package/runtime-src/graph/trace-store.ts +0 -136
- package/runtime-src/indexer/index.ts +0 -480
- package/runtime-src/orchestrator/browser-agent.ts +0 -374
- package/runtime-src/orchestrator/dag-advisor.ts +0 -59
- package/runtime-src/orchestrator/dag-feedback.ts +0 -256
- package/runtime-src/orchestrator/first-pass-action.ts +0 -362
- package/runtime-src/orchestrator/passive-publish.ts +0 -152
- package/runtime-src/orchestrator/timing-economics.ts +0 -80
- package/runtime-src/payments/cascade.ts +0 -137
- package/runtime-src/payments/index.ts +0 -268
- package/runtime-src/payments/wallet.ts +0 -33
- package/runtime-src/reverse-engineer/description-prompt.ts +0 -132
- package/runtime-src/router.ts +0 -17
- package/runtime-src/runtime/browser-access.ts +0 -11
- package/runtime-src/runtime/browser-host.ts +0 -48
- package/runtime-src/runtime/lifecycle.ts +0 -17
- package/runtime-src/runtime/supervisor.ts +0 -69
- package/runtime-src/single-binary.ts +0 -141
- package/runtime-src/telemetry.ts +0 -253
- package/runtime-src/verification/matrix.ts +0 -30
- package/scripts/postinstall.mjs +0 -81
|
@@ -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
|
|
3
|
+
import { promoteExplicitExecution, resolveAndExecute } from "../orchestrator/index.js";
|
|
18
4
|
import { getSkill } from "../marketplace/index.js";
|
|
19
|
-
import { executeSkill
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
|
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
|
-
{ ...
|
|
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) {
|