unbrowse 2.9.1 → 2.10.2
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 +275 -34
- package/package.json +1 -1
- package/runtime-src/analytics-session.ts +33 -0
- package/runtime-src/api/routes.ts +59 -8
- package/runtime-src/auth/index.ts +44 -6
- package/runtime-src/client/index.ts +143 -9
- package/runtime-src/execution/index.ts +2 -1
- package/runtime-src/orchestrator/index.ts +15 -1
- package/runtime-src/payments/cascade.ts +137 -0
- package/runtime-src/types/skill.ts +18 -0
|
@@ -0,0 +1,33 @@
|
|
|
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
|
+
}
|
|
14
|
+
|
|
15
|
+
export function buildAnalyticsSessionPayload(
|
|
16
|
+
sessionId: string,
|
|
17
|
+
startedAt: string,
|
|
18
|
+
source: OrchestrationTiming["source"],
|
|
19
|
+
trace: Pick<ExecutionTrace, "completed_at" | "network_events" | "trace_version">,
|
|
20
|
+
): AnalyticsSessionPayload {
|
|
21
|
+
const cacheLike = source === "marketplace" || source === "route-cache" || source === "first-pass";
|
|
22
|
+
return {
|
|
23
|
+
session_id: sessionId,
|
|
24
|
+
started_at: startedAt,
|
|
25
|
+
completed_at: trace.completed_at,
|
|
26
|
+
trace_version: trace.trace_version,
|
|
27
|
+
api_calls: Math.max(1, trace.network_events?.length ?? 0),
|
|
28
|
+
discovery_queries: cacheLike ? 1 : 0,
|
|
29
|
+
cached_skill_calls: cacheLike ? 1 : 0,
|
|
30
|
+
fresh_index_calls: source === "live-capture" ? 1 : 0,
|
|
31
|
+
browser_mode: "unknown",
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -5,7 +5,7 @@ import { extractEndpoints, extractAuthHeaders } from "../reverse-engineer/index.
|
|
|
5
5
|
import { INTERCEPTOR_SCRIPT, collectInterceptedRequests, injectInterceptor, type RawRequest } from "../capture/index.js";
|
|
6
6
|
import { queueBackgroundIndex } from "../indexer/index.js";
|
|
7
7
|
import { nanoid } from "nanoid";
|
|
8
|
-
import type { SkillManifest } from "../types/index.js";
|
|
8
|
+
import type { ExecutionTrace, OrchestrationTiming, ProjectionOptions, SkillManifest } from "../types/index.js";
|
|
9
9
|
import { extractBrowserCookies } from "../auth/browser-cookies.js";
|
|
10
10
|
import { mergeEndpoints } from "../marketplace/index.js";
|
|
11
11
|
import { buildSkillOperationGraph } from "../graph/index.js";
|
|
@@ -14,15 +14,13 @@ import { findExistingSkillForDomain, cachePublishedSkill } from "../client/index
|
|
|
14
14
|
import { storeCredential } from "../vault/index.js";
|
|
15
15
|
import { generateLocalDescription, writeSkillSnapshot, buildResolveCacheKey, getDomainReuseKey, domainSkillCache, persistDomainCache, scopedCacheKey, snapshotPathForCacheKey, invalidateRouteCacheForDomain, summarizeSchema, extractSampleValues } from "../orchestrator/index.js";
|
|
16
16
|
import { TRACE_VERSION, CODE_HASH, GIT_SHA } from "../version.js";
|
|
17
|
-
import { promoteExplicitExecution, resolveAndExecute } from "../orchestrator/index.js";
|
|
17
|
+
import { promoteExplicitExecution, resolveAndExecute, type OrchestratorResult } from "../orchestrator/index.js";
|
|
18
18
|
import { getSkill } from "../marketplace/index.js";
|
|
19
19
|
import { executeSkill, rankEndpoints } from "../execution/index.js";
|
|
20
|
-
import { storeCredential } from "../vault/index.js";
|
|
21
20
|
import { interactiveLogin, extractBrowserAuth } from "../auth/index.js";
|
|
22
21
|
import { publishSkill } from "../marketplace/index.js";
|
|
23
|
-
import { recordFeedback, recordDiagnostics, recordExecution, getApiKey, getRecentLocalSkill } from "../client/index.js";
|
|
22
|
+
import { recordFeedback, recordDiagnostics, recordExecution, getApiKey, getRecentLocalSkill, recordAnalyticsSession, type AnalyticsSessionPayload } from "../client/index.js";
|
|
24
23
|
import { ROUTE_LIMITS } from "../ratelimit/index.js";
|
|
25
|
-
import type { ProjectionOptions } from "../types/index.js";
|
|
26
24
|
import { getSkillChunk, toAgentSkillChunkView } from "../graph/index.js";
|
|
27
25
|
import { listRecentSessionsForDomain } from "../session-logs.js";
|
|
28
26
|
import { mergeAgentReview } from "../indexer/index.js";
|
|
@@ -33,6 +31,43 @@ const BETA_API_URL = process.env.UNBROWSE_BACKEND_URL || "https://beta-api.unbro
|
|
|
33
31
|
|
|
34
32
|
const TRACES_DIR = process.env.TRACES_DIR ?? join(process.cwd(), "traces");
|
|
35
33
|
|
|
34
|
+
type AnalyticsSessionResult = {
|
|
35
|
+
trace: Pick<ExecutionTrace, "trace_id" | "started_at" | "completed_at" | "endpoint_id" | "trace_version">;
|
|
36
|
+
timing?: Pick<OrchestrationTiming, "source">;
|
|
37
|
+
source?: OrchestratorResult["source"];
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export function buildAnalyticsSessionPayload(
|
|
41
|
+
result: AnalyticsSessionResult,
|
|
42
|
+
opts: {
|
|
43
|
+
browser_mode: AnalyticsSessionPayload["browser_mode"];
|
|
44
|
+
discovery_queries: number;
|
|
45
|
+
cached_skill_calls?: number;
|
|
46
|
+
fresh_index_calls?: number;
|
|
47
|
+
},
|
|
48
|
+
): AnalyticsSessionPayload {
|
|
49
|
+
const source = result.timing?.source ?? result.source;
|
|
50
|
+
const apiCalls = result.trace.endpoint_id ? 1 : 0;
|
|
51
|
+
const cachedSkillCalls = opts.cached_skill_calls ?? (
|
|
52
|
+
apiCalls > 0 && source !== "live-capture" && source !== "first-pass" ? 1 : 0
|
|
53
|
+
);
|
|
54
|
+
const freshIndexCalls = opts.fresh_index_calls ?? (
|
|
55
|
+
apiCalls > 0 && (source === "live-capture" || source === "first-pass") ? 1 : 0
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
session_id: result.trace.trace_id,
|
|
60
|
+
started_at: result.trace.started_at,
|
|
61
|
+
completed_at: result.trace.completed_at,
|
|
62
|
+
trace_version: result.trace.trace_version ?? TRACE_VERSION,
|
|
63
|
+
api_calls: apiCalls,
|
|
64
|
+
discovery_queries: opts.discovery_queries,
|
|
65
|
+
cached_skill_calls: cachedSkillCalls,
|
|
66
|
+
fresh_index_calls: freshIndexCalls,
|
|
67
|
+
browser_mode: opts.browser_mode ?? "unknown",
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
36
71
|
|
|
37
72
|
/** Convert Kuri HAR entries to RawRequest format */
|
|
38
73
|
function harEntriesToRawRequests(entries: KuriHarEntry[]): RawRequest[] {
|
|
@@ -183,11 +218,11 @@ async function fetchStats() {
|
|
|
183
218
|
|
|
184
219
|
const externalCalls: Promise<unknown>[] = [
|
|
185
220
|
npmPoint("unbrowse", "last-month"),
|
|
186
|
-
npmPoint("
|
|
221
|
+
npmPoint("unbrowse-openclaw", "last-month"),
|
|
187
222
|
npmPoint("unbrowse", "1970-01-01:2099-12-31"),
|
|
188
|
-
npmPoint("
|
|
223
|
+
npmPoint("unbrowse-openclaw", "1970-01-01:2099-12-31"),
|
|
189
224
|
npmRange("unbrowse"),
|
|
190
|
-
npmRange("
|
|
225
|
+
npmRange("unbrowse-openclaw"),
|
|
191
226
|
fetch("https://api.github.com/repos/anthropic-ai/unbrowse", {
|
|
192
227
|
headers: { "User-Agent": "unbrowse-stats" },
|
|
193
228
|
}).then(r => r.json() as Promise<Record<string, unknown>>),
|
|
@@ -332,6 +367,11 @@ export async function registerRoutes(app: FastifyInstance) {
|
|
|
332
367
|
res.available_endpoints = innerResult.available_endpoints;
|
|
333
368
|
}
|
|
334
369
|
|
|
370
|
+
await recordAnalyticsSession(buildAnalyticsSessionPayload(result, {
|
|
371
|
+
browser_mode: "replaced",
|
|
372
|
+
discovery_queries: 1,
|
|
373
|
+
})).catch(() => {});
|
|
374
|
+
|
|
335
375
|
return reply.send(result);
|
|
336
376
|
} catch (err) {
|
|
337
377
|
return reply.code(500).send({ error: (err as Error).message });
|
|
@@ -602,6 +642,10 @@ export async function registerRoutes(app: FastifyInstance) {
|
|
|
602
642
|
if (freshResult.trace?.skill_id && freshResult.trace?.endpoint_id) {
|
|
603
643
|
recordExecution(freshResult.trace.skill_id, freshResult.trace.endpoint_id, freshResult.trace, skill).catch(() => {});
|
|
604
644
|
}
|
|
645
|
+
await recordAnalyticsSession(buildAnalyticsSessionPayload(freshResult, {
|
|
646
|
+
browser_mode: "manual",
|
|
647
|
+
discovery_queries: 1,
|
|
648
|
+
})).catch(() => {});
|
|
605
649
|
return reply.send({
|
|
606
650
|
...freshResult,
|
|
607
651
|
_recovery: {
|
|
@@ -615,6 +659,13 @@ export async function registerRoutes(app: FastifyInstance) {
|
|
|
615
659
|
}
|
|
616
660
|
}
|
|
617
661
|
|
|
662
|
+
await recordAnalyticsSession(buildAnalyticsSessionPayload(execResult, {
|
|
663
|
+
browser_mode: "manual",
|
|
664
|
+
discovery_queries: 0,
|
|
665
|
+
cached_skill_calls: execResult.trace.endpoint_id ? 1 : 0,
|
|
666
|
+
fresh_index_calls: 0,
|
|
667
|
+
})).catch(() => {});
|
|
668
|
+
|
|
618
669
|
return reply.send(execResult);
|
|
619
670
|
} catch (err) {
|
|
620
671
|
return reply.code(500).send({ error: (err as Error).message });
|
|
@@ -27,6 +27,29 @@ export interface LoginResult {
|
|
|
27
27
|
error?: string;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
export interface BrowserAuthSourceMeta {
|
|
31
|
+
family?: string;
|
|
32
|
+
userDataDir?: string;
|
|
33
|
+
cookieDbPath?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface StoredAuthBundle {
|
|
37
|
+
cookies: AuthCookie[];
|
|
38
|
+
headers: Record<string, string>;
|
|
39
|
+
source_keys: string[];
|
|
40
|
+
source_meta?: BrowserAuthSourceMeta | null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function storedAuthNeedsBrowserRefresh(bundle: StoredAuthBundle | null | undefined): boolean {
|
|
44
|
+
if (!bundle) return true;
|
|
45
|
+
if (bundle.cookies.length === 0 && Object.keys(bundle.headers).length === 0) return true;
|
|
46
|
+
const sourceMeta = bundle.source_meta;
|
|
47
|
+
if (!sourceMeta) return true;
|
|
48
|
+
if (sourceMeta.family === "chromium" && !sourceMeta.userDataDir && !sourceMeta.cookieDbPath) {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
30
53
|
/**
|
|
31
54
|
* Open a visible browser for the user to complete login.
|
|
32
55
|
* Uses Kuri to manage the browser tab, polls for login completion via cookies.
|
|
@@ -225,6 +248,17 @@ function filterExpired(cookies: AuthCookie[]): AuthCookie[] {
|
|
|
225
248
|
export async function getStoredAuth(
|
|
226
249
|
domain: string
|
|
227
250
|
): Promise<AuthCookie[] | null> {
|
|
251
|
+
const bundle = await getStoredAuthBundle(domain);
|
|
252
|
+
return bundle?.cookies?.length ? bundle.cookies : null;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Retrieve the stored auth bundle for a domain from the vault.
|
|
257
|
+
* Preserves headers/source metadata while filtering expired cookies.
|
|
258
|
+
*/
|
|
259
|
+
export async function getStoredAuthBundle(
|
|
260
|
+
domain: string
|
|
261
|
+
): Promise<StoredAuthBundle | null> {
|
|
228
262
|
const regDomain = getRegistrableDomain(domain);
|
|
229
263
|
const keysToTry = [`auth:${regDomain}`];
|
|
230
264
|
if (domain !== regDomain) keysToTry.push(`auth:${domain}`);
|
|
@@ -233,12 +267,10 @@ export async function getStoredAuth(
|
|
|
233
267
|
const stored = await getCredential(key);
|
|
234
268
|
if (!stored) continue;
|
|
235
269
|
try {
|
|
236
|
-
const parsed = JSON.parse(stored) as { cookies?: AuthCookie[] };
|
|
237
|
-
const cookies = parsed.cookies;
|
|
238
|
-
if (!cookies || cookies.length === 0) continue;
|
|
239
|
-
|
|
270
|
+
const parsed = JSON.parse(stored) as Partial<StoredAuthBundle> & { cookies?: AuthCookie[] };
|
|
271
|
+
const cookies = parsed.cookies ?? [];
|
|
240
272
|
const valid = filterExpired(cookies);
|
|
241
|
-
if (valid.length === 0) {
|
|
273
|
+
if (cookies.length > 0 && valid.length === 0 && Object.keys(parsed.headers ?? {}).length === 0) {
|
|
242
274
|
log("auth", `all ${cookies.length} cookies for ${domain} (key: ${key}) are expired — deleting`);
|
|
243
275
|
await deleteCredential(key);
|
|
244
276
|
continue;
|
|
@@ -246,11 +278,17 @@ export async function getStoredAuth(
|
|
|
246
278
|
if (valid.length < cookies.length) {
|
|
247
279
|
log("auth", `filtered ${cookies.length - valid.length} expired cookies for ${domain}`);
|
|
248
280
|
}
|
|
249
|
-
return
|
|
281
|
+
return {
|
|
282
|
+
cookies: valid,
|
|
283
|
+
headers: parsed.headers ?? {},
|
|
284
|
+
source_keys: parsed.source_keys ?? [],
|
|
285
|
+
source_meta: parsed.source_meta ?? null,
|
|
286
|
+
};
|
|
250
287
|
} catch {
|
|
251
288
|
continue;
|
|
252
289
|
}
|
|
253
290
|
}
|
|
291
|
+
|
|
254
292
|
return null;
|
|
255
293
|
}
|
|
256
294
|
|
|
@@ -4,6 +4,7 @@ import { homedir, hostname } from "os";
|
|
|
4
4
|
import { randomBytes, createHash } from "crypto";
|
|
5
5
|
import { createInterface } from "readline";
|
|
6
6
|
import type { AgentSkillChunkView, EndpointStats, ExecutionTrace, OrchestrationTiming, SkillManifest, ValidationResult } from "../types/index.js";
|
|
7
|
+
import { ensureCascadeSplitForSkill } from "../payments/cascade.js";
|
|
7
8
|
import { attributeLifecycle } from "../runtime/lifecycle.js";
|
|
8
9
|
import type { LifecycleEvent } from "../runtime/lifecycle.js";
|
|
9
10
|
|
|
@@ -70,6 +71,8 @@ interface UnbrowseConfig {
|
|
|
70
71
|
registered_at: string;
|
|
71
72
|
tos_accepted_version: string | null;
|
|
72
73
|
tos_accepted_at: string | null;
|
|
74
|
+
wallet_address?: string;
|
|
75
|
+
wallet_provider?: string;
|
|
73
76
|
}
|
|
74
77
|
|
|
75
78
|
type ApiKeySource = "env" | "config";
|
|
@@ -116,6 +119,23 @@ export function resolveAgentName(preferredEmail: string | undefined, fallbackNam
|
|
|
116
119
|
return isValidAgentEmail(normalized) ? normalized : fallbackName;
|
|
117
120
|
}
|
|
118
121
|
|
|
122
|
+
function getLocalWalletContext(): { wallet_address?: string; wallet_provider?: string } {
|
|
123
|
+
const lobsterWallet = process.env.LOBSTER_WALLET_ADDRESS?.trim();
|
|
124
|
+
if (lobsterWallet) {
|
|
125
|
+
return { wallet_address: lobsterWallet, wallet_provider: "lobster.cash" };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const genericWallet = process.env.AGENT_WALLET_ADDRESS?.trim();
|
|
129
|
+
if (genericWallet) {
|
|
130
|
+
return {
|
|
131
|
+
wallet_address: genericWallet,
|
|
132
|
+
wallet_provider: process.env.AGENT_WALLET_PROVIDER?.trim() || undefined,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return {};
|
|
137
|
+
}
|
|
138
|
+
|
|
119
139
|
export function getApiKey(): string {
|
|
120
140
|
if (LOCAL_ONLY) return "local-only";
|
|
121
141
|
// Env var takes priority, then cached config
|
|
@@ -211,7 +231,12 @@ async function findUsableApiKey(): Promise<{ key: string; source: ApiKeySource }
|
|
|
211
231
|
return null;
|
|
212
232
|
}
|
|
213
233
|
|
|
214
|
-
async function
|
|
234
|
+
async function apiRequest<T = unknown>(
|
|
235
|
+
method: string,
|
|
236
|
+
path: string,
|
|
237
|
+
body?: unknown,
|
|
238
|
+
opts?: { noAuth?: boolean; timeoutMs?: number },
|
|
239
|
+
): Promise<{ data: T; headers: Headers }> {
|
|
215
240
|
const key = opts?.noAuth ? "" : getApiKey();
|
|
216
241
|
const controller = new AbortController();
|
|
217
242
|
const timer = setTimeout(() => controller.abort(), opts?.timeoutMs ?? API_TIMEOUT_MS);
|
|
@@ -268,6 +293,11 @@ async function api<T = unknown>(method: string, path: string, body?: unknown, op
|
|
|
268
293
|
const msg = errData.details?.length ? `${errData.error}: ${errData.details.join("; ")}` : errData.error ?? `API HTTP ${res.status}`;
|
|
269
294
|
throw new Error(msg);
|
|
270
295
|
}
|
|
296
|
+
return { data: data as T, headers: res.headers };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async function api<T = unknown>(method: string, path: string, body?: unknown, opts?: { noAuth?: boolean; timeoutMs?: number }): Promise<T> {
|
|
300
|
+
const { data } = await apiRequest<T>(method, path, body, opts);
|
|
271
301
|
return data;
|
|
272
302
|
}
|
|
273
303
|
|
|
@@ -382,6 +412,13 @@ export async function ensureRegistered(options?: { promptForEmail?: boolean }):
|
|
|
382
412
|
console.log("[unbrowse] Restored saved registration.");
|
|
383
413
|
}
|
|
384
414
|
await checkTosStatus();
|
|
415
|
+
try {
|
|
416
|
+
const profile = await getMyProfile();
|
|
417
|
+
const wallet = getLocalWalletContext();
|
|
418
|
+
if (wallet.wallet_address && profile.wallet_address !== wallet.wallet_address) {
|
|
419
|
+
await syncAgentWallet(wallet);
|
|
420
|
+
}
|
|
421
|
+
} catch { /* non-fatal */ }
|
|
385
422
|
return;
|
|
386
423
|
}
|
|
387
424
|
|
|
@@ -408,8 +445,9 @@ export async function ensureRegistered(options?: { promptForEmail?: boolean }):
|
|
|
408
445
|
console.log(`Registering as "${name}"...`);
|
|
409
446
|
|
|
410
447
|
try {
|
|
448
|
+
const wallet = getLocalWalletContext();
|
|
411
449
|
const { agent_id, api_key } = await api<{ agent_id: string; api_key: string }>(
|
|
412
|
-
"POST", "/v1/agents/register", { name, tos_version: tosInfo.version }
|
|
450
|
+
"POST", "/v1/agents/register", { name, tos_version: tosInfo.version, ...wallet }
|
|
413
451
|
);
|
|
414
452
|
|
|
415
453
|
process.env.UNBROWSE_API_KEY = api_key;
|
|
@@ -420,6 +458,7 @@ export async function ensureRegistered(options?: { promptForEmail?: boolean }):
|
|
|
420
458
|
registered_at: new Date().toISOString(),
|
|
421
459
|
tos_accepted_version: tosInfo.version,
|
|
422
460
|
tos_accepted_at: new Date().toISOString(),
|
|
461
|
+
...wallet,
|
|
423
462
|
});
|
|
424
463
|
|
|
425
464
|
console.log(`Registered as ${name}. API key saved to ~/.unbrowse/config.json`);
|
|
@@ -596,7 +635,29 @@ export async function publishSkill(
|
|
|
596
635
|
} as SkillManifest & { warnings: string[] };
|
|
597
636
|
}
|
|
598
637
|
if (LOCAL_ONLY) throw new Error("local-only mode");
|
|
599
|
-
|
|
638
|
+
const wallet = getLocalWalletContext();
|
|
639
|
+
const published = await api<SkillManifest & { warnings: string[] }>("POST", "/v1/skills", {
|
|
640
|
+
...draft,
|
|
641
|
+
...(wallet.wallet_address ? wallet : {}),
|
|
642
|
+
}, { timeoutMs: PUBLISH_TIMEOUT_MS });
|
|
643
|
+
|
|
644
|
+
const cascade = await ensureCascadeSplitForSkill(published).catch((err) => ({
|
|
645
|
+
warning: `cascade_split_failed:${(err as Error).message}`,
|
|
646
|
+
}));
|
|
647
|
+
const warnings = [...(published.warnings ?? [])];
|
|
648
|
+
if (cascade.warning) warnings.push(cascade.warning);
|
|
649
|
+
|
|
650
|
+
if (cascade.split_config && cascade.split_config !== published.split_config) {
|
|
651
|
+
const updated = await api<SkillManifest>("PATCH", `/v1/skills/${published.skill_id}`, {
|
|
652
|
+
split_config: cascade.split_config,
|
|
653
|
+
});
|
|
654
|
+
return {
|
|
655
|
+
...updated,
|
|
656
|
+
warnings,
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
return { ...published, warnings };
|
|
600
661
|
}
|
|
601
662
|
|
|
602
663
|
export async function deprecateSkill(skillId: string): Promise<void> {
|
|
@@ -669,10 +730,11 @@ export async function searchIntentResolve(
|
|
|
669
730
|
domain_results: Array<{ id: number; score: number; metadata: Record<string, unknown> }>;
|
|
670
731
|
global_results: Array<{ id: number; score: number; metadata: Record<string, unknown> }>;
|
|
671
732
|
skipped_global: boolean;
|
|
733
|
+
actual_cost_uc?: number;
|
|
672
734
|
}> {
|
|
673
735
|
if (LOCAL_ONLY) return { domain_results: [], global_results: [], skipped_global: false };
|
|
674
736
|
try {
|
|
675
|
-
|
|
737
|
+
const { data, headers } = await apiRequest<{
|
|
676
738
|
domain_results: Array<{ id: number; score: number; metadata: Record<string, unknown> }>;
|
|
677
739
|
global_results: Array<{ id: number; score: number; metadata: Record<string, unknown> }>;
|
|
678
740
|
skipped_global: boolean;
|
|
@@ -682,6 +744,11 @@ export async function searchIntentResolve(
|
|
|
682
744
|
domain_k: domainK,
|
|
683
745
|
global_k: globalK,
|
|
684
746
|
});
|
|
747
|
+
const actualCostHeader = headers.get("X-Unbrowse-Cost-Uc");
|
|
748
|
+
const actualCostUc = actualCostHeader && /^\d+$/.test(actualCostHeader)
|
|
749
|
+
? Number(actualCostHeader)
|
|
750
|
+
: undefined;
|
|
751
|
+
return actualCostUc != null ? { ...data, actual_cost_uc: actualCostUc } : data;
|
|
685
752
|
} catch (err) {
|
|
686
753
|
if (isX402Error(err)) throw err;
|
|
687
754
|
const [domain_results, global_results] = await Promise.all([
|
|
@@ -710,6 +777,18 @@ export interface ExecutionPayload {
|
|
|
710
777
|
indexer_id?: string;
|
|
711
778
|
}
|
|
712
779
|
|
|
780
|
+
export interface AnalyticsSessionPayload {
|
|
781
|
+
session_id: string;
|
|
782
|
+
started_at: string;
|
|
783
|
+
completed_at?: string;
|
|
784
|
+
trace_version?: string;
|
|
785
|
+
api_calls: number;
|
|
786
|
+
discovery_queries?: number;
|
|
787
|
+
cached_skill_calls?: number;
|
|
788
|
+
fresh_index_calls?: number;
|
|
789
|
+
browser_mode?: "default" | "replaced" | "manual" | "unknown";
|
|
790
|
+
}
|
|
791
|
+
|
|
713
792
|
/**
|
|
714
793
|
* Build the POST body for /v1/stats/execution.
|
|
715
794
|
* Pure function — no I/O, fully testable.
|
|
@@ -747,11 +826,16 @@ export async function recordExecution(
|
|
|
747
826
|
await api("POST", "/v1/stats/execution", payload);
|
|
748
827
|
}
|
|
749
828
|
|
|
829
|
+
export async function recordAnalyticsSession(payload: AnalyticsSessionPayload): Promise<void> {
|
|
830
|
+
if (LOCAL_ONLY) return;
|
|
831
|
+
await api("POST", "/v1/analytics/sessions", payload);
|
|
832
|
+
}
|
|
833
|
+
|
|
750
834
|
/** Record a payment transaction for a paid skill execution. Fire-and-forget. */
|
|
751
835
|
export async function recordTransaction(params: {
|
|
752
836
|
transaction_id: string;
|
|
753
837
|
consumer_id: string;
|
|
754
|
-
creator_id
|
|
838
|
+
creator_id?: string;
|
|
755
839
|
skill_id: string;
|
|
756
840
|
endpoint_id?: string;
|
|
757
841
|
price_usd: number;
|
|
@@ -812,6 +896,23 @@ export async function recordOrchestrationPerf(timing: OrchestrationTiming): Prom
|
|
|
812
896
|
await api("POST", "/v1/stats/perf", { ...timing, phase_totals_ms: phaseTotals });
|
|
813
897
|
}
|
|
814
898
|
|
|
899
|
+
// --- Analytics Sessions ---
|
|
900
|
+
|
|
901
|
+
export async function recordAnalyticsSession(session: {
|
|
902
|
+
session_id: string;
|
|
903
|
+
started_at: string;
|
|
904
|
+
completed_at?: string;
|
|
905
|
+
trace_version?: string;
|
|
906
|
+
api_calls: number;
|
|
907
|
+
discovery_queries?: number;
|
|
908
|
+
cached_skill_calls?: number;
|
|
909
|
+
fresh_index_calls?: number;
|
|
910
|
+
browser_mode?: "default" | "replaced" | "manual" | "unknown";
|
|
911
|
+
}): Promise<void> {
|
|
912
|
+
if (LOCAL_ONLY) return;
|
|
913
|
+
await api("POST", "/v1/analytics/sessions", session);
|
|
914
|
+
}
|
|
915
|
+
|
|
815
916
|
// --- Validation ---
|
|
816
917
|
|
|
817
918
|
export async function validateManifest(manifest: unknown): Promise<ValidationResult> {
|
|
@@ -917,11 +1018,23 @@ export async function verifyMarketplaceDiscovery(
|
|
|
917
1018
|
|
|
918
1019
|
// --- Agent Registration ---
|
|
919
1020
|
|
|
920
|
-
export async function registerAgent(
|
|
921
|
-
|
|
1021
|
+
export async function registerAgent(
|
|
1022
|
+
name: string,
|
|
1023
|
+
wallet: { wallet_address?: string; wallet_provider?: string } = getLocalWalletContext(),
|
|
1024
|
+
): Promise<{ agent_id: string; api_key: string }> {
|
|
1025
|
+
return api<{ agent_id: string; api_key: string }>("POST", "/v1/agents/register", { name, ...wallet });
|
|
922
1026
|
}
|
|
923
1027
|
|
|
924
|
-
export async function getAgent(agentId: string): Promise<{
|
|
1028
|
+
export async function getAgent(agentId: string): Promise<{
|
|
1029
|
+
agent_id: string;
|
|
1030
|
+
name: string;
|
|
1031
|
+
created_at: string;
|
|
1032
|
+
wallet_address?: string | null;
|
|
1033
|
+
wallet_provider?: string | null;
|
|
1034
|
+
skills_discovered: string[];
|
|
1035
|
+
total_executions: number;
|
|
1036
|
+
total_feedback_given: number;
|
|
1037
|
+
} | null> {
|
|
925
1038
|
try {
|
|
926
1039
|
return await api("GET", `/v1/agents/${agentId}`);
|
|
927
1040
|
} catch {
|
|
@@ -929,10 +1042,27 @@ export async function getAgent(agentId: string): Promise<{ agent_id: string; nam
|
|
|
929
1042
|
}
|
|
930
1043
|
}
|
|
931
1044
|
|
|
932
|
-
export async function getMyProfile(): Promise<{
|
|
1045
|
+
export async function getMyProfile(): Promise<{
|
|
1046
|
+
agent_id: string;
|
|
1047
|
+
name: string;
|
|
1048
|
+
created_at: string;
|
|
1049
|
+
wallet_address?: string | null;
|
|
1050
|
+
wallet_provider?: string | null;
|
|
1051
|
+
skills_discovered: string[];
|
|
1052
|
+
total_executions: number;
|
|
1053
|
+
total_feedback_given: number;
|
|
1054
|
+
}> {
|
|
933
1055
|
return api("GET", "/v1/agents/me", undefined);
|
|
934
1056
|
}
|
|
935
1057
|
|
|
1058
|
+
export async function syncAgentWallet(wallet = getLocalWalletContext()): Promise<void> {
|
|
1059
|
+
if (!wallet.wallet_address) return;
|
|
1060
|
+
await api("POST", "/v1/agents/wallet", wallet);
|
|
1061
|
+
const config = loadConfig();
|
|
1062
|
+
if (!config) return;
|
|
1063
|
+
saveConfig({ ...config, ...wallet });
|
|
1064
|
+
}
|
|
1065
|
+
|
|
936
1066
|
|
|
937
1067
|
// --- Transaction Visibility ---
|
|
938
1068
|
|
|
@@ -989,3 +1119,7 @@ export async function getCreatorEarnings(agentId: string): Promise<{
|
|
|
989
1119
|
export async function setSkillPrice(skillId: string, priceUsd: number): Promise<unknown> {
|
|
990
1120
|
return api("PATCH", `/v1/skills/${skillId}`, { base_price_usd: priceUsd });
|
|
991
1121
|
}
|
|
1122
|
+
|
|
1123
|
+
export async function setSkillSplitConfig(skillId: string, splitConfig: string | null): Promise<unknown> {
|
|
1124
|
+
return api("PATCH", `/v1/skills/${skillId}`, { split_config: splitConfig });
|
|
1125
|
+
}
|
|
@@ -6,6 +6,7 @@ import { publishSkill, mergeEndpoints } from "../marketplace/index.js";
|
|
|
6
6
|
import { updateEndpointScore } from "../marketplace/index.js";
|
|
7
7
|
import { getCredential, storeCredential, deleteCredential } from "../vault/index.js";
|
|
8
8
|
import { getStoredAuth, getAuthCookies, refreshAuthFromBrowser } from "../auth/index.js";
|
|
9
|
+
import { resolvePreExecutionAuth } from "../auth/dependency-runtime.js";
|
|
9
10
|
import { authRuntime } from "../auth/runtime.js";
|
|
10
11
|
import { applyProjection, inferSchema } from "../transform/index.js";
|
|
11
12
|
import { detectSchemaDrift } from "../transform/drift.js";
|
|
@@ -2284,7 +2285,7 @@ export async function executeEndpoint(
|
|
|
2284
2285
|
recordExecution(skill.skill_id, endpoint.endpoint_id, trace, skill).catch(() => {});
|
|
2285
2286
|
|
|
2286
2287
|
// Record transaction if this was a paid execution (fire-and-forget)
|
|
2287
|
-
if (trace.success &&
|
|
2288
|
+
if (trace.success && options?.payment_verified === true && skill.base_price_usd && skill.base_price_usd > 0) {
|
|
2288
2289
|
const consumerConfig = (() => {
|
|
2289
2290
|
try { return JSON.parse(require("fs").readFileSync(require("os").homedir() + "/.unbrowse/config.json", "utf-8")); }
|
|
2290
2291
|
catch { return {}; }
|
|
@@ -1737,6 +1737,7 @@ export async function resolveAndExecute(
|
|
|
1737
1737
|
response_bytes: 0,
|
|
1738
1738
|
time_saved_pct: 0,
|
|
1739
1739
|
tokens_saved_pct: 0,
|
|
1740
|
+
actual_total_ms: 0,
|
|
1740
1741
|
trace_version: TRACE_VERSION,
|
|
1741
1742
|
};
|
|
1742
1743
|
const decisionTrace: Record<string, unknown> = {
|
|
@@ -1781,6 +1782,7 @@ export async function resolveAndExecute(
|
|
|
1781
1782
|
trace?: ExecutionTrace,
|
|
1782
1783
|
): OrchestrationTiming {
|
|
1783
1784
|
timing.total_ms = Date.now() - t0;
|
|
1785
|
+
timing.actual_total_ms = timing.total_ms;
|
|
1784
1786
|
timing.source = source;
|
|
1785
1787
|
timing.skill_id = skillId;
|
|
1786
1788
|
|
|
@@ -1793,6 +1795,10 @@ export async function resolveAndExecute(
|
|
|
1793
1795
|
const cost = skill?.discovery_cost;
|
|
1794
1796
|
const baselineTokens = cost?.capture_tokens ?? DEFAULT_CAPTURE_TOKENS;
|
|
1795
1797
|
const baselineMs = cost?.capture_ms ?? DEFAULT_CAPTURE_MS;
|
|
1798
|
+
const paidSearchUc = timing.paid_search_uc ?? 0;
|
|
1799
|
+
const paidExecutionUc = timing.paid_execution_uc ?? 0;
|
|
1800
|
+
const totalActualCostUc = paidSearchUc + paidExecutionUc;
|
|
1801
|
+
if (totalActualCostUc > 0) timing.actual_cost_uc = totalActualCostUc;
|
|
1796
1802
|
|
|
1797
1803
|
// Token savings: marketplace/cache returns structured data, skipping full-page browsing
|
|
1798
1804
|
if (source === "marketplace" || source === "route-cache" || source === "first-pass") {
|
|
@@ -1804,6 +1810,10 @@ export async function resolveAndExecute(
|
|
|
1804
1810
|
? Math.round((Math.max(0, baselineMs - timing.total_ms) / baselineMs) * 100)
|
|
1805
1811
|
: 0;
|
|
1806
1812
|
}
|
|
1813
|
+
if (cost?.capture_ms != null) {
|
|
1814
|
+
timing.baseline_total_ms = cost.capture_ms;
|
|
1815
|
+
timing.time_saved_ms = Math.max(0, cost.capture_ms - timing.total_ms);
|
|
1816
|
+
}
|
|
1807
1817
|
|
|
1808
1818
|
// Stamp trace with token metrics so they persist in trace files
|
|
1809
1819
|
if (trace) {
|
|
@@ -2935,6 +2945,7 @@ export async function resolveAndExecute(
|
|
|
2935
2945
|
domain_results: SearchResult[];
|
|
2936
2946
|
global_results: SearchResult[];
|
|
2937
2947
|
skipped_global: boolean;
|
|
2948
|
+
actual_cost_uc?: number;
|
|
2938
2949
|
};
|
|
2939
2950
|
try {
|
|
2940
2951
|
searchResponse = await Promise.race([
|
|
@@ -2944,7 +2955,7 @@ export async function resolveAndExecute(
|
|
|
2944
2955
|
MARKETPLACE_DOMAIN_SEARCH_K,
|
|
2945
2956
|
MARKETPLACE_GLOBAL_SEARCH_K,
|
|
2946
2957
|
),
|
|
2947
|
-
new Promise<{ domain_results: SearchResult[]; global_results: SearchResult[]; skipped_global: boolean }>((resolve) =>
|
|
2958
|
+
new Promise<{ domain_results: SearchResult[]; global_results: SearchResult[]; skipped_global: boolean; actual_cost_uc?: number }>((resolve) =>
|
|
2948
2959
|
setTimeout(() => {
|
|
2949
2960
|
console.log(`[marketplace] timeout after ${MARKETPLACE_TIMEOUT_MS}ms — falling through to browser`);
|
|
2950
2961
|
resolve({ domain_results: [], global_results: [], skipped_global: true });
|
|
@@ -2986,6 +2997,9 @@ export async function resolveAndExecute(
|
|
|
2986
2997
|
skipped_global: false,
|
|
2987
2998
|
};
|
|
2988
2999
|
}
|
|
3000
|
+
if (typeof searchResponse.actual_cost_uc === "number" && searchResponse.actual_cost_uc > 0) {
|
|
3001
|
+
timing.paid_search_uc = searchResponse.actual_cost_uc;
|
|
3002
|
+
}
|
|
2989
3003
|
const { domain_results: domainResults, global_results: globalResults } = searchResponse;
|
|
2990
3004
|
timing.search_ms = Date.now() - ts0;
|
|
2991
3005
|
console.log(`[marketplace] search: ${domainResults.length} domain + ${globalResults.length} global results (${timing.search_ms}ms)`);
|