unbrowse 3.0.4 → 3.1.0

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 CHANGED
@@ -17,12 +17,21 @@ var __toESM = (mod, isNodeMode, target) => {
17
17
  });
18
18
  return to;
19
19
  };
20
+ var __export = (target, all) => {
21
+ for (var name in all)
22
+ __defProp(target, name, {
23
+ get: all[name],
24
+ enumerable: true,
25
+ configurable: true,
26
+ set: (newValue) => all[name] = () => newValue
27
+ });
28
+ };
20
29
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
21
30
  var __promiseAll = (args) => Promise.all(args);
22
31
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
23
32
 
24
33
  // ../../src/build-info.generated.ts
25
- var BUILD_RELEASE_VERSION = "3.0.4", BUILD_GIT_SHA = "44d2baa5a0e0", BUILD_CODE_HASH = "1488fc1d92b7", BUILD_RELEASE_MANIFEST_BASE64 = "eyJzY2hlbWFfdmVyc2lvbiI6MSwicmVsZWFzZV92ZXJzaW9uIjoiMy4wLjQiLCJnaXRfc2hhIjoiNDRkMmJhYTVhMGUwIiwiY29kZV9oYXNoIjoiMTQ4OGZjMWQ5MmI3IiwidHJhY2VfdmVyc2lvbiI6IjE0ODhmYzFkOTJiN0A0NGQyYmFhNWEwZTAiLCJpc3N1ZWRfYXQiOiIyMDI2LTA0LTA1VDA0OjIyOjE2LjM2M1oifQ", BUILD_RELEASE_MANIFEST_SIGNATURE = "zLW181YNH-1BLhH-HL-OvhuGJAzfy-HrZnIo7xm0aVo", BUILD_DEFAULT_BACKEND_URL = "https://beta-api.unbrowse.ai";
34
+ var BUILD_RELEASE_VERSION = "3.1.0", BUILD_GIT_SHA = "c9f9ce0e5cb1", BUILD_CODE_HASH = "1488fc1d92b7", BUILD_RELEASE_MANIFEST_BASE64 = "eyJzY2hlbWFfdmVyc2lvbiI6MSwicmVsZWFzZV92ZXJzaW9uIjoiMy4xLjAiLCJnaXRfc2hhIjoiYzlmOWNlMGU1Y2IxIiwiY29kZV9oYXNoIjoiMTQ4OGZjMWQ5MmI3IiwidHJhY2VfdmVyc2lvbiI6IjE0ODhmYzFkOTJiN0BjOWY5Y2UwZTVjYjEiLCJpc3N1ZWRfYXQiOiIyMDI2LTA0LTA1VDA2OjUwOjA4LjQzNVoifQ", BUILD_RELEASE_MANIFEST_SIGNATURE = "_2pdxyoIEYkKUHO0nXaXTubP5SrZCa-8CqStcPBsXxw", BUILD_DEFAULT_BACKEND_URL = "https://beta-api.unbrowse.ai";
26
35
 
27
36
  // ../../src/version.ts
28
37
  import { createHash } from "crypto";
@@ -284,8 +293,114 @@ var init_telemetry_attribution = __esm(() => {
284
293
  ];
285
294
  });
286
295
 
296
+ // ../../src/payments/lobster-pay.ts
297
+ var exports_lobster_pay = {};
298
+ __export(exports_lobster_pay, {
299
+ payAndRetry: () => payAndRetry,
300
+ lobsterX402Fetch: () => lobsterX402Fetch,
301
+ isLobsterAvailable: () => isLobsterAvailable
302
+ });
303
+ import { execFile, execFileSync } from "node:child_process";
304
+ import { existsSync as existsSync3 } from "node:fs";
305
+ import { homedir as homedir2 } from "node:os";
306
+ import { join as join3 } from "node:path";
307
+ function getLobsterCommand() {
308
+ try {
309
+ execFileSync("lobstercash", ["--version"], { stdio: "ignore", timeout: 3000 });
310
+ return { cmd: "lobstercash", prefix: [] };
311
+ } catch (_e) {}
312
+ try {
313
+ const npmPrefix = execFileSync("npm", ["config", "get", "prefix"], { encoding: "utf8", timeout: 5000 }).trim();
314
+ const lobsterPath = join3(npmPrefix, "bin", "lobstercash");
315
+ if (existsSync3(lobsterPath)) {
316
+ execFileSync(lobsterPath, ["--version"], { stdio: "ignore", timeout: 3000 });
317
+ return { cmd: lobsterPath, prefix: [] };
318
+ }
319
+ } catch (_e) {}
320
+ return null;
321
+ }
322
+ function lobsterCmd() {
323
+ if (cachedCommand === undefined)
324
+ cachedCommand = getLobsterCommand();
325
+ return cachedCommand;
326
+ }
327
+ function isLobsterAvailable() {
328
+ const agentsPath = join3(process.env.HOME || homedir2(), ".lobster", "agents.json");
329
+ return existsSync3(agentsPath);
330
+ }
331
+ function lobsterX402Fetch(url, options) {
332
+ return new Promise((resolve) => {
333
+ const resolved = lobsterCmd();
334
+ if (!resolved) {
335
+ resolve({ success: false, body: "", error: "lobstercash CLI not in PATH" });
336
+ return;
337
+ }
338
+ const { cmd, prefix } = resolved;
339
+ const args = [...prefix, "x402", "fetch", url, "--debug"];
340
+ if (options?.jsonBody) {
341
+ args.push("--json", options.jsonBody);
342
+ }
343
+ if (options?.headers) {
344
+ for (const [key, value] of Object.entries(options.headers)) {
345
+ args.push("--header", `${key}:${value}`);
346
+ }
347
+ }
348
+ const timeout = options?.timeoutMs ?? LOBSTER_PAY_TIMEOUT_MS;
349
+ args.push("--timeout", String(timeout));
350
+ execFile(cmd, args, { timeout: timeout + 5000, maxBuffer: 1024 * 1024 }, (err, stdout, stderr) => {
351
+ if (err) {
352
+ const msg = stderr?.trim() || err.message;
353
+ console.warn(`[lobster-pay] x402 fetch failed: ${msg}`);
354
+ resolve({ success: false, body: "", error: msg });
355
+ return;
356
+ }
357
+ if (stderr) {
358
+ for (const line of stderr.split(`
359
+ `).filter(Boolean)) {
360
+ console.log(`[lobster-pay] ${line}`);
361
+ }
362
+ }
363
+ const statusMatch = stdout.match(/^Status:\s*(\d+)/m);
364
+ const statusCode = statusMatch ? parseInt(statusMatch[1], 10) : undefined;
365
+ if (statusCode && statusCode >= 400) {
366
+ resolve({ success: false, body: stdout, statusCode, error: `HTTP ${statusCode}` });
367
+ return;
368
+ }
369
+ resolve({ success: true, body: stdout, statusCode });
370
+ });
371
+ });
372
+ }
373
+ async function payAndRetry(fullUrl, options) {
374
+ if (!isLobsterAvailable()) {
375
+ console.log("[lobster-pay] lobster.cash not configured — skipping payment");
376
+ return null;
377
+ }
378
+ console.log(`[lobster-pay] attempting x402 payment for ${fullUrl}`);
379
+ const result = await lobsterX402Fetch(fullUrl, {
380
+ jsonBody: options?.body ? JSON.stringify(options.body) : undefined,
381
+ headers: options?.headers
382
+ });
383
+ if (!result.success) {
384
+ console.warn(`[lobster-pay] payment failed: ${result.error}`);
385
+ return null;
386
+ }
387
+ try {
388
+ const raw = result.body;
389
+ const jsonStart = Math.min(...[raw.indexOf("{"), raw.indexOf("[")].filter((i) => i >= 0));
390
+ const jsonStr = jsonStart >= 0 ? raw.slice(jsonStart) : raw;
391
+ const data = JSON.parse(jsonStr);
392
+ console.log("[lobster-pay] payment successful — got paid response");
393
+ return { data, paid: true };
394
+ } catch (_e) {
395
+ console.warn("[lobster-pay] paid response was not valid JSON");
396
+ return null;
397
+ }
398
+ }
399
+ var LOBSTER_PAY_TIMEOUT_MS = 30000, cachedCommand = undefined;
400
+ var init_lobster_pay = () => {};
401
+
287
402
  // ../../src/runtime/paths.ts
288
- import { existsSync as existsSync4, mkdirSync as mkdirSync2, realpathSync } from "node:fs";
403
+ import { existsSync as existsSync5, mkdirSync as mkdirSync2, realpathSync } from "node:fs";
289
404
  import os from "node:os";
290
405
  import path from "node:path";
291
406
  import { createRequire as createRequire2 } from "node:module";
@@ -299,7 +414,7 @@ function getPackageRoot(metaUrl) {
299
414
  let dir = getModuleDir(metaUrl);
300
415
  const root = path.parse(dir).root;
301
416
  while (dir !== root) {
302
- if (existsSync4(path.join(dir, "package.json")))
417
+ if (existsSync5(path.join(dir, "package.json")))
303
418
  return dir;
304
419
  dir = path.dirname(dir);
305
420
  }
@@ -318,7 +433,7 @@ function runtimeArgsForEntrypoint(metaUrl, entrypoint) {
318
433
  const req = createRequire2(metaUrl);
319
434
  const tsxPkg = req.resolve("tsx/package.json");
320
435
  const tsxLoader = path.join(path.dirname(tsxPkg), "dist", "loader.mjs");
321
- if (existsSync4(tsxLoader))
436
+ if (existsSync5(tsxLoader))
322
437
  return ["--import", pathToFileURL(tsxLoader).href, entrypoint];
323
438
  } catch {}
324
439
  return ["--import", "tsx", entrypoint];
@@ -327,7 +442,7 @@ function getUnbrowseHome() {
327
442
  return path.join(os.homedir(), ".unbrowse");
328
443
  }
329
444
  function ensureDir(dir) {
330
- if (!existsSync4(dir))
445
+ if (!existsSync5(dir))
331
446
  mkdirSync2(dir, { recursive: true });
332
447
  return dir;
333
448
  }
@@ -470,8 +585,8 @@ var init_logger = __esm(() => {
470
585
  });
471
586
 
472
587
  // ../../src/kuri/client.ts
473
- import { execFileSync, spawn as spawn2 } from "node:child_process";
474
- import { existsSync as existsSync6 } from "node:fs";
588
+ import { execFileSync as execFileSync2, spawn as spawn2 } from "node:child_process";
589
+ import { existsSync as existsSync7 } from "node:fs";
475
590
  import path5 from "node:path";
476
591
  function createBrokerState(port = KURI_DEFAULT_PORT) {
477
592
  return {
@@ -496,12 +611,14 @@ function currentBundledKuriTarget() {
496
611
  return "linux-arm64";
497
612
  if (process.platform === "linux" && process.arch === "x64")
498
613
  return "linux-x64";
614
+ if (process.platform === "win32" && process.arch === "x64")
615
+ return "win-x64";
499
616
  return null;
500
617
  }
501
618
  function resolveBinaryOnPath(name) {
502
619
  const checker = process.platform === "win32" ? "where" : "which";
503
620
  try {
504
- const output = execFileSync(checker, [name], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
621
+ const output = execFileSync2(checker, [name], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
505
622
  const match = output.split(/\r?\n/).map((line) => line.trim()).find(Boolean);
506
623
  return match || null;
507
624
  } catch {
@@ -544,7 +661,7 @@ function findKuriBinary() {
544
661
  if (process.env.KURI_BIN)
545
662
  return process.env.KURI_BIN;
546
663
  const candidates = getKuriBinaryCandidates();
547
- return candidates.find((candidate) => existsSync6(candidate)) ?? candidates[0] ?? kuriBinaryName();
664
+ return candidates.find((candidate) => existsSync7(candidate)) ?? candidates[0] ?? kuriBinaryName();
548
665
  }
549
666
  var KURI_DEFAULT_PORT = 7700, defaultBrokerState, brokerClients;
550
667
  var init_client2 = __esm(() => {
@@ -584,11 +701,11 @@ var init_transform = __esm(() => {
584
701
  });
585
702
 
586
703
  // ../../src/debug-trace.ts
587
- import { join as join4 } from "node:path";
704
+ import { join as join5 } from "node:path";
588
705
  import { nanoid as nanoid3 } from "nanoid";
589
706
  var TRACE_DIR;
590
707
  var init_debug_trace = __esm(() => {
591
- TRACE_DIR = process.env.TRACES_DIR ?? join4(process.cwd(), "traces");
708
+ TRACE_DIR = process.env.TRACES_DIR ?? join5(process.cwd(), "traces");
592
709
  });
593
710
 
594
711
  // ../../src/publish/sanitize.ts
@@ -678,8 +795,8 @@ var init_bundle_scanner = __esm(() => {
678
795
  });
679
796
 
680
797
  // ../../src/vault/index.ts
681
- import { join as join5 } from "path";
682
- import { homedir as homedir3 } from "os";
798
+ import { join as join6 } from "path";
799
+ import { homedir as homedir4 } from "os";
683
800
  function normalizeKeytarModule(mod) {
684
801
  let candidate = mod;
685
802
  for (let depth = 0;depth < 3; depth++) {
@@ -701,9 +818,9 @@ var init_vault = __esm(async () => {
701
818
  try {
702
819
  keytar = normalizeKeytarModule(await import("keytar"));
703
820
  } catch {}
704
- VAULT_DIR = join5(homedir3(), ".unbrowse", "vault");
705
- VAULT_FILE = join5(VAULT_DIR, "credentials.enc");
706
- KEY_FILE = join5(VAULT_DIR, ".key");
821
+ VAULT_DIR = join6(homedir4(), ".unbrowse", "vault");
822
+ VAULT_FILE = join6(VAULT_DIR, "credentials.enc");
823
+ KEY_FILE = join6(VAULT_DIR, ".key");
707
824
  vaultLock = Promise.resolve();
708
825
  });
709
826
 
@@ -854,7 +971,7 @@ var init_schema_review = __esm(() => {
854
971
  });
855
972
 
856
973
  // ../../src/indexer/index.ts
857
- import { join as join6 } from "node:path";
974
+ import { join as join7 } from "node:path";
858
975
  var SKILL_SNAPSHOT_DIR, indexInFlight, pendingIndexJobs;
859
976
  var init_indexer = __esm(async () => {
860
977
  init_graph();
@@ -869,7 +986,7 @@ var init_indexer = __esm(async () => {
869
986
  init_graph();
870
987
  init_schema_review();
871
988
  await init_orchestrator();
872
- SKILL_SNAPSHOT_DIR = process.env.UNBROWSE_SKILL_SNAPSHOT_DIR ?? join6(process.env.HOME ?? "/tmp", ".unbrowse", "skill-snapshots");
989
+ SKILL_SNAPSHOT_DIR = process.env.UNBROWSE_SKILL_SNAPSHOT_DIR ?? join7(process.env.HOME ?? "/tmp", ".unbrowse", "skill-snapshots");
873
990
  indexInFlight = new Map;
874
991
  pendingIndexJobs = new Map;
875
992
  });
@@ -1046,8 +1163,8 @@ var init_routing_telemetry = __esm(() => {
1046
1163
  });
1047
1164
  // ../../src/orchestrator/index.ts
1048
1165
  import { nanoid as nanoid9 } from "nanoid";
1049
- import { existsSync as existsSync7, writeFileSync as writeFileSync3, readFileSync as readFileSync5, mkdirSync as mkdirSync4, readdirSync as readdirSync3 } from "node:fs";
1050
- import { dirname as dirname2, join as join7 } from "node:path";
1166
+ import { existsSync as existsSync8, writeFileSync as writeFileSync3, readFileSync as readFileSync5, mkdirSync as mkdirSync4, readdirSync as readdirSync3 } from "node:fs";
1167
+ import { dirname as dirname2, join as join8 } from "node:path";
1051
1168
  var LIVE_CAPTURE_TIMEOUT_MS, capturedDomainCache, captureInFlight, captureDomainLocks, skillRouteCache, ROUTE_CACHE_FILE, SKILL_SNAPSHOT_DIR2, domainSkillCache, DOMAIN_CACHE_FILE, _routeCacheDirty = false, routeCacheFlushTimer, routeResultCache, ROUTE_CACHE_TTL, MARKETPLACE_HYDRATE_LIMIT, MARKETPLACE_GET_SKILL_TIMEOUT_MS, MARKETPLACE_DOMAIN_SEARCH_K, MARKETPLACE_GLOBAL_SEARCH_K, SEARCH_INTENT_STOPWORDS;
1052
1169
  var init_orchestrator = __esm(async () => {
1053
1170
  init_client();
@@ -1077,12 +1194,12 @@ var init_orchestrator = __esm(async () => {
1077
1194
  captureInFlight = new Map;
1078
1195
  captureDomainLocks = new Map;
1079
1196
  skillRouteCache = new Map;
1080
- ROUTE_CACHE_FILE = join7(process.env.HOME ?? "/tmp", ".unbrowse", "route-cache.json");
1081
- SKILL_SNAPSHOT_DIR2 = process.env.UNBROWSE_SKILL_SNAPSHOT_DIR ?? join7(process.env.HOME ?? "/tmp", ".unbrowse", "skill-snapshots");
1197
+ ROUTE_CACHE_FILE = join8(process.env.HOME ?? "/tmp", ".unbrowse", "route-cache.json");
1198
+ SKILL_SNAPSHOT_DIR2 = process.env.UNBROWSE_SKILL_SNAPSHOT_DIR ?? join8(process.env.HOME ?? "/tmp", ".unbrowse", "skill-snapshots");
1082
1199
  domainSkillCache = new Map;
1083
- DOMAIN_CACHE_FILE = join7(process.env.HOME ?? "/tmp", ".unbrowse", "domain-skill-cache.json");
1200
+ DOMAIN_CACHE_FILE = join8(process.env.HOME ?? "/tmp", ".unbrowse", "domain-skill-cache.json");
1084
1201
  try {
1085
- if (existsSync7(DOMAIN_CACHE_FILE)) {
1202
+ if (existsSync8(DOMAIN_CACHE_FILE)) {
1086
1203
  const data = JSON.parse(readFileSync5(DOMAIN_CACHE_FILE, "utf-8"));
1087
1204
  for (const [k, v] of Object.entries(data)) {
1088
1205
  const entry = v;
@@ -1099,7 +1216,7 @@ var init_orchestrator = __esm(async () => {
1099
1216
  _routeCacheDirty = false;
1100
1217
  try {
1101
1218
  const dir = dirname2(ROUTE_CACHE_FILE);
1102
- if (!existsSync7(dir))
1219
+ if (!existsSync8(dir))
1103
1220
  mkdirSync4(dir, { recursive: true });
1104
1221
  const entries = Object.fromEntries(skillRouteCache);
1105
1222
  writeFileSync3(ROUTE_CACHE_FILE, JSON.stringify(entries), "utf-8");
@@ -1107,7 +1224,7 @@ var init_orchestrator = __esm(async () => {
1107
1224
  }, 5000);
1108
1225
  routeCacheFlushTimer.unref?.();
1109
1226
  try {
1110
- if (existsSync7(ROUTE_CACHE_FILE)) {
1227
+ if (existsSync8(ROUTE_CACHE_FILE)) {
1111
1228
  const data = JSON.parse(readFileSync5(ROUTE_CACHE_FILE, "utf-8"));
1112
1229
  for (const [k, v] of Object.entries(data)) {
1113
1230
  const entry = v;
@@ -1198,6 +1315,60 @@ var init_orchestrator = __esm(async () => {
1198
1315
  ]);
1199
1316
  });
1200
1317
 
1318
+ // ../../src/payments/wallet.ts
1319
+ var exports_wallet = {};
1320
+ __export(exports_wallet, {
1321
+ getWalletContext: () => getWalletContext2,
1322
+ checkWalletConfigured: () => checkWalletConfigured2
1323
+ });
1324
+ import { existsSync as existsSync12, readFileSync as readFileSync8 } from "node:fs";
1325
+ import { homedir as homedir5 } from "node:os";
1326
+ import { join as join10 } from "node:path";
1327
+ function asNonEmptyString2(value) {
1328
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
1329
+ }
1330
+ function getLobsterWalletFromLocalConfig2() {
1331
+ const agentsPath = join10(process.env.HOME || homedir5(), ".lobster", "agents.json");
1332
+ if (!existsSync12(agentsPath))
1333
+ return;
1334
+ try {
1335
+ const raw = JSON.parse(readFileSync8(agentsPath, "utf8"));
1336
+ const activeAgentId = asNonEmptyString2(raw.activeAgentId);
1337
+ const activeAgent = Array.isArray(raw.agents) ? raw.agents.find((agent) => asNonEmptyString2(agent.id) === activeAgentId) : activeAgentId ? raw.agents?.[activeAgentId] : undefined;
1338
+ return asNonEmptyString2(activeAgent?.authorizedWallets?.solana) ?? asNonEmptyString2(activeAgent?.walletAddress) ?? asNonEmptyString2(activeAgent?.wallet_address);
1339
+ } catch {
1340
+ return;
1341
+ }
1342
+ }
1343
+ function getWalletContext2() {
1344
+ const lobsterWallet = asNonEmptyString2(process.env.LOBSTER_WALLET_ADDRESS);
1345
+ if (lobsterWallet) {
1346
+ return { wallet_address: lobsterWallet, wallet_provider: "lobster.cash" };
1347
+ }
1348
+ const genericWallet = asNonEmptyString2(process.env.AGENT_WALLET_ADDRESS);
1349
+ if (genericWallet) {
1350
+ return {
1351
+ wallet_address: genericWallet,
1352
+ wallet_provider: asNonEmptyString2(process.env.AGENT_WALLET_PROVIDER)
1353
+ };
1354
+ }
1355
+ const localLobsterWallet = getLobsterWalletFromLocalConfig2();
1356
+ if (localLobsterWallet) {
1357
+ return { wallet_address: localLobsterWallet, wallet_provider: "lobster.cash" };
1358
+ }
1359
+ return {};
1360
+ }
1361
+ function checkWalletConfigured2() {
1362
+ const wallet = getWalletContext2();
1363
+ if (!wallet.wallet_address)
1364
+ return { configured: false };
1365
+ return {
1366
+ configured: true,
1367
+ provider: wallet.wallet_provider ?? "unknown"
1368
+ };
1369
+ }
1370
+ var init_wallet2 = () => {};
1371
+
1201
1372
  // ../../src/cli.ts
1202
1373
  import { config as loadEnv } from "dotenv";
1203
1374
  import { spawn as spawn3 } from "child_process";
@@ -1207,9 +1378,9 @@ init_version();
1207
1378
  init_cascade();
1208
1379
  init_wallet();
1209
1380
  init_telemetry_attribution();
1210
- import { readFileSync as readFileSync3, writeFileSync, existsSync as existsSync3, mkdirSync, readdirSync as readdirSync2 } from "fs";
1211
- import { join as join3 } from "path";
1212
- import { homedir as homedir2, hostname } from "os";
1381
+ import { readFileSync as readFileSync3, writeFileSync, existsSync as existsSync4, mkdirSync, readdirSync as readdirSync2 } from "fs";
1382
+ import { join as join4 } from "path";
1383
+ import { homedir as homedir3, hostname } from "os";
1213
1384
  import { randomBytes, createHash as createHash2 } from "crypto";
1214
1385
  import { createInterface } from "readline";
1215
1386
  var API_URL = process.env.UNBROWSE_BACKEND_URL || DEFAULT_BACKEND_URL;
@@ -1244,13 +1415,13 @@ function decodeBase64Json(value) {
1244
1415
  function getConfigDir() {
1245
1416
  if (process.env.UNBROWSE_CONFIG_DIR)
1246
1417
  return process.env.UNBROWSE_CONFIG_DIR;
1247
- return PROFILE_NAME ? join3(homedir2(), ".unbrowse", "profiles", PROFILE_NAME) : join3(homedir2(), ".unbrowse");
1418
+ return PROFILE_NAME ? join4(homedir3(), ".unbrowse", "profiles", PROFILE_NAME) : join4(homedir3(), ".unbrowse");
1248
1419
  }
1249
1420
  function getConfigPath() {
1250
- return join3(getConfigDir(), "config.json");
1421
+ return join4(getConfigDir(), "config.json");
1251
1422
  }
1252
1423
  function getInstallTelemetryPath() {
1253
- return join3(getConfigDir(), "install-state.json");
1424
+ return join4(getConfigDir(), "install-state.json");
1254
1425
  }
1255
1426
  function getLandingToken() {
1256
1427
  const token = process.env.UNBROWSE_LANDING_TOKEN?.trim();
@@ -1265,7 +1436,7 @@ function getActiveProfile() {
1265
1436
  function loadConfig() {
1266
1437
  try {
1267
1438
  const configPath = getConfigPath();
1268
- if (existsSync3(configPath)) {
1439
+ if (existsSync4(configPath)) {
1269
1440
  return JSON.parse(readFileSync3(configPath, "utf-8"));
1270
1441
  }
1271
1442
  } catch {}
@@ -1274,14 +1445,14 @@ function loadConfig() {
1274
1445
  function saveConfig(config) {
1275
1446
  const configDir = getConfigDir();
1276
1447
  const configPath = getConfigPath();
1277
- if (!existsSync3(configDir))
1448
+ if (!existsSync4(configDir))
1278
1449
  mkdirSync(configDir, { recursive: true });
1279
1450
  writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
1280
1451
  }
1281
1452
  function loadInstallTelemetryState() {
1282
1453
  try {
1283
1454
  const statePath = getInstallTelemetryPath();
1284
- if (existsSync3(statePath)) {
1455
+ if (existsSync4(statePath)) {
1285
1456
  return JSON.parse(readFileSync3(statePath, "utf-8"));
1286
1457
  }
1287
1458
  } catch {}
@@ -1290,7 +1461,7 @@ function loadInstallTelemetryState() {
1290
1461
  function saveInstallTelemetryState(state) {
1291
1462
  const configDir = getConfigDir();
1292
1463
  const statePath = getInstallTelemetryPath();
1293
- if (!existsSync3(configDir))
1464
+ if (!existsSync4(configDir))
1294
1465
  mkdirSync(configDir, { recursive: true });
1295
1466
  writeFileSync(statePath, JSON.stringify(state, null, 2), { mode: 384 });
1296
1467
  }
@@ -1547,6 +1718,26 @@ async function apiRequest(method, path, body, opts) {
1547
1718
  const paymentRequired = res.headers.get("PAYMENT-REQUIRED");
1548
1719
  const legacyPaymentTerms = res.headers.get("X-Payment-Required");
1549
1720
  const terms = paymentRequired ? decodeBase64Json(paymentRequired) : legacyPaymentTerms ? JSON.parse(legacyPaymentTerms) : data.terms;
1721
+ try {
1722
+ const { isLobsterAvailable: isLobsterAvailable2, payAndRetry: payAndRetry2 } = await Promise.resolve().then(() => (init_lobster_pay(), exports_lobster_pay));
1723
+ if (isLobsterAvailable2()) {
1724
+ const fullUrl = `${API_URL}${path}`;
1725
+ const paidResult = await payAndRetry2(fullUrl, {
1726
+ body,
1727
+ headers: {
1728
+ "Content-Type": "application/json",
1729
+ "Accept-Encoding": "gzip, deflate",
1730
+ ...releaseAttestationHeaders,
1731
+ ...key ? { Authorization: `Bearer ${key}` } : {}
1732
+ }
1733
+ });
1734
+ if (paidResult) {
1735
+ return { data: paidResult.data, headers: new Headers };
1736
+ }
1737
+ }
1738
+ } catch (payErr) {
1739
+ console.warn(`[x402] lobster pay-and-retry failed: ${payErr.message}`);
1740
+ }
1550
1741
  const err = new Error(`Payment required: ${data.error ?? "This skill requires payment"}`);
1551
1742
  err.x402 = true;
1552
1743
  err.terms = terms;
@@ -2150,7 +2341,7 @@ async function restartServer(baseUrl, metaUrl) {
2150
2341
  }
2151
2342
 
2152
2343
  // ../../src/runtime/paths.ts
2153
- import { existsSync as existsSync5, mkdirSync as mkdirSync3, realpathSync as realpathSync2 } from "node:fs";
2344
+ import { existsSync as existsSync6, mkdirSync as mkdirSync3, realpathSync as realpathSync2 } from "node:fs";
2154
2345
  import path3 from "node:path";
2155
2346
  import { createRequire as createRequire3 } from "node:module";
2156
2347
  import { fileURLToPath as fileURLToPath3, pathToFileURL as pathToFileURL2 } from "node:url";
@@ -2170,7 +2361,7 @@ function runtimeArgsForEntrypoint2(metaUrl, entrypoint) {
2170
2361
  const req = createRequire3(metaUrl);
2171
2362
  const tsxPkg = req.resolve("tsx/package.json");
2172
2363
  const tsxLoader = path3.join(path3.dirname(tsxPkg), "dist", "loader.mjs");
2173
- if (existsSync5(tsxLoader))
2364
+ if (existsSync6(tsxLoader))
2174
2365
  return ["--import", pathToFileURL2(tsxLoader).href, entrypoint];
2175
2366
  } catch {}
2176
2367
  return ["--import", "tsx", entrypoint];
@@ -2200,8 +2391,8 @@ init_publish();
2200
2391
  init_settings();
2201
2392
  init_graph();
2202
2393
  init_schema_review();
2203
- import { join as join8 } from "node:path";
2204
- var SKILL_SNAPSHOT_DIR3 = process.env.UNBROWSE_SKILL_SNAPSHOT_DIR ?? join8(process.env.HOME ?? "/tmp", ".unbrowse", "skill-snapshots");
2394
+ import { join as join9 } from "node:path";
2395
+ var SKILL_SNAPSHOT_DIR3 = process.env.UNBROWSE_SKILL_SNAPSHOT_DIR ?? join9(process.env.HOME ?? "/tmp", ".unbrowse", "skill-snapshots");
2205
2396
  var indexInFlight2 = new Map;
2206
2397
  var pendingIndexJobs2 = new Map;
2207
2398
  async function drainPendingIndexJobs() {
@@ -2238,14 +2429,14 @@ init_paths();
2238
2429
  init_client2();
2239
2430
  init_logger();
2240
2431
  init_wallet();
2241
- import { execFileSync as execFileSync2 } from "node:child_process";
2242
- import { existsSync as existsSync9, mkdirSync as mkdirSync6, writeFileSync as writeFileSync5 } from "node:fs";
2432
+ import { execFileSync as execFileSync3 } from "node:child_process";
2433
+ import { existsSync as existsSync10, mkdirSync as mkdirSync6, writeFileSync as writeFileSync5 } from "node:fs";
2243
2434
  import os4 from "node:os";
2244
2435
  import path7 from "node:path";
2245
2436
 
2246
2437
  // ../../src/runtime/update-hints.ts
2247
2438
  init_paths();
2248
- import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "node:fs";
2439
+ import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "node:fs";
2249
2440
  import os3 from "node:os";
2250
2441
  import path6 from "node:path";
2251
2442
  var DEFAULT_INTERVAL_MS = 12 * 60 * 60 * 1000;
@@ -2259,7 +2450,7 @@ function getConfigDir2() {
2259
2450
  return path6.join(getHomeDir(), ".unbrowse");
2260
2451
  }
2261
2452
  function ensureDir2(dir) {
2262
- if (!existsSync8(dir))
2453
+ if (!existsSync9(dir))
2263
2454
  mkdirSync5(dir, { recursive: true });
2264
2455
  return dir;
2265
2456
  }
@@ -2282,7 +2473,7 @@ function detectRepoRoot(start2) {
2282
2473
  let dir = path6.resolve(start2);
2283
2474
  const root = path6.parse(dir).root;
2284
2475
  while (dir !== root) {
2285
- if (existsSync8(path6.join(dir, ".git")))
2476
+ if (existsSync9(path6.join(dir, ".git")))
2286
2477
  return dir;
2287
2478
  dir = path6.dirname(dir);
2288
2479
  }
@@ -2361,12 +2552,12 @@ codex_hooks = true
2361
2552
  }
2362
2553
  function writeCodexHook(metaUrl) {
2363
2554
  const configPath = getCodexConfigPath();
2364
- if (!existsSync8(path6.dirname(configPath))) {
2555
+ if (!existsSync9(path6.dirname(configPath))) {
2365
2556
  return { host: "codex", action: "not-detected", config_file: configPath };
2366
2557
  }
2367
2558
  try {
2368
2559
  const hookScript = getHookScriptPath(metaUrl).replace(/\\/g, "/");
2369
- const fileExistsBefore = existsSync8(configPath);
2560
+ const fileExistsBefore = existsSync9(configPath);
2370
2561
  let content = fileExistsBefore ? readFileSync6(configPath, "utf8") : "";
2371
2562
  const previous = content;
2372
2563
  content = ensureCodexHooksFeature(content);
@@ -2401,13 +2592,13 @@ command = ${JSON.stringify(command)}
2401
2592
  }
2402
2593
  function writeClaudeHook(metaUrl) {
2403
2594
  const settingsPath = getClaudeSettingsPath();
2404
- if (!existsSync8(path6.dirname(settingsPath))) {
2595
+ if (!existsSync9(path6.dirname(settingsPath))) {
2405
2596
  return { host: "claude", action: "not-detected", config_file: settingsPath };
2406
2597
  }
2407
2598
  try {
2408
2599
  const hookScript = getHookScriptPath(metaUrl).replace(/\\/g, "/");
2409
2600
  const command = `node "${hookScript}"`;
2410
- const fileExistsBefore = existsSync8(settingsPath);
2601
+ const fileExistsBefore = existsSync9(settingsPath);
2411
2602
  const settings = readJsonFile(settingsPath) ?? {};
2412
2603
  settings.hooks ??= {};
2413
2604
  settings.hooks.SessionStart ??= [];
@@ -2445,7 +2636,7 @@ function configureUpdateHintHooks(metaUrl, install) {
2445
2636
  function hasBinary(name) {
2446
2637
  const checker = process.platform === "win32" ? "where" : "which";
2447
2638
  try {
2448
- execFileSync2(checker, [name], { stdio: "ignore" });
2639
+ execFileSync3(checker, [name], { stdio: "ignore" });
2449
2640
  return true;
2450
2641
  } catch {
2451
2642
  return false;
@@ -2472,7 +2663,7 @@ function getOpenCodeProjectCommandsDir(cwd) {
2472
2663
  return path7.join(cwd, ".opencode", "commands");
2473
2664
  }
2474
2665
  function detectOpenCode(cwd) {
2475
- return hasBinary("opencode") || existsSync9(path7.join(resolveConfigHome(), "opencode")) || existsSync9(path7.join(cwd, ".opencode"));
2666
+ return hasBinary("opencode") || existsSync10(path7.join(resolveConfigHome(), "opencode")) || existsSync10(path7.join(cwd, ".opencode"));
2476
2667
  }
2477
2668
  function renderOpenCodeCommand() {
2478
2669
  return `---
@@ -2500,11 +2691,11 @@ function writeOpenCodeCommand(scope, cwd) {
2500
2691
  if (scope === "auto" && !detected) {
2501
2692
  return { detected: false, action: "not-detected", scope: "off" };
2502
2693
  }
2503
- const resolvedScope = scope === "project" ? "project" : scope === "global" ? "global" : existsSync9(path7.join(cwd, ".opencode")) ? "project" : "global";
2694
+ const resolvedScope = scope === "project" ? "project" : scope === "global" ? "global" : existsSync10(path7.join(cwd, ".opencode")) ? "project" : "global";
2504
2695
  const commandsDir = resolvedScope === "project" ? getOpenCodeProjectCommandsDir(cwd) : getOpenCodeGlobalCommandsDir();
2505
2696
  const commandFile = path7.join(ensureDir(commandsDir), "unbrowse.md");
2506
2697
  const content = renderOpenCodeCommand();
2507
- const action2 = existsSync9(commandFile) ? "updated" : "installed";
2698
+ const action2 = existsSync10(commandFile) ? "updated" : "installed";
2508
2699
  mkdirSync6(path7.dirname(commandFile), { recursive: true });
2509
2700
  writeFileSync5(commandFile, content);
2510
2701
  return {
@@ -2516,10 +2707,10 @@ function writeOpenCodeCommand(scope, cwd) {
2516
2707
  }
2517
2708
  async function ensureBrowserEngineInstalled() {
2518
2709
  const binary = findKuriBinary();
2519
- if (existsSync9(binary)) {
2710
+ if (existsSync10(binary)) {
2520
2711
  return { installed: true, action: "already-installed" };
2521
2712
  }
2522
- const sourceDir = getKuriSourceCandidates().find((candidate) => existsSync9(path7.join(candidate, "build.zig")));
2713
+ const sourceDir = getKuriSourceCandidates().find((candidate) => existsSync10(path7.join(candidate, "build.zig")));
2523
2714
  if (!sourceDir) {
2524
2715
  return {
2525
2716
  installed: false,
@@ -2535,13 +2726,13 @@ async function ensureBrowserEngineInstalled() {
2535
2726
  };
2536
2727
  }
2537
2728
  try {
2538
- execFileSync2("zig", ["build", "-Doptimize=ReleaseFast"], {
2729
+ execFileSync3("zig", ["build", "-Doptimize=ReleaseFast"], {
2539
2730
  cwd: sourceDir,
2540
2731
  stdio: "inherit",
2541
2732
  timeout: 300000
2542
2733
  });
2543
2734
  const builtBinary = findKuriBinary();
2544
- if (existsSync9(builtBinary)) {
2735
+ if (existsSync10(builtBinary)) {
2545
2736
  return {
2546
2737
  installed: true,
2547
2738
  action: "installed",
@@ -2566,11 +2757,11 @@ async function runSetup(options) {
2566
2757
  const browser = options?.installBrowser === false ? { installed: false, action: "skipped" } : await ensureBrowserEngineInstalled();
2567
2758
  const walletCheck = checkWalletConfigured();
2568
2759
  const skipWalletSetup = process.env.UNBROWSE_SKIP_WALLET_SETUP === "1";
2569
- const lobsterInstalled = hasBinary("lobstercash") || existsSync9(path7.join(os4.homedir(), ".agents", "skills", "lobstercash", "SKILL.md"));
2760
+ const lobsterInstalled = hasBinary("lobstercash") || existsSync10(path7.join(os4.homedir(), ".agents", "skills", "lobstercash", "SKILL.md"));
2570
2761
  if (!skipWalletSetup && !walletCheck.configured && lobsterInstalled) {
2571
2762
  console.log("[unbrowse] Crossmint lobster.cash detected but wallet not configured — running wallet setup...");
2572
2763
  try {
2573
- execFileSync2("npx", ["@crossmint/lobster-cli", "setup"], {
2764
+ execFileSync3("npx", ["@crossmint/lobster-cli", "setup"], {
2574
2765
  stdio: "inherit",
2575
2766
  timeout: 60000
2576
2767
  });
@@ -2606,7 +2797,7 @@ async function runSetup(options) {
2606
2797
 
2607
2798
  // ../../src/runtime/update-hints.ts
2608
2799
  init_paths();
2609
- import { existsSync as existsSync10, mkdirSync as mkdirSync7, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "node:fs";
2800
+ import { existsSync as existsSync11, mkdirSync as mkdirSync7, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "node:fs";
2610
2801
  import os5 from "node:os";
2611
2802
  import path8 from "node:path";
2612
2803
  var INSTALL_SCRIPT_URL = "https://unbrowse.ai/install.sh";
@@ -2620,7 +2811,7 @@ function getConfigDir3() {
2620
2811
  return path8.join(getHomeDir2(), ".unbrowse");
2621
2812
  }
2622
2813
  function ensureDir3(dir) {
2623
- if (!existsSync10(dir))
2814
+ if (!existsSync11(dir))
2624
2815
  mkdirSync7(dir, { recursive: true });
2625
2816
  return dir;
2626
2817
  }
@@ -2646,7 +2837,7 @@ function detectRepoRoot2(start2) {
2646
2837
  let dir = path8.resolve(start2);
2647
2838
  const root = path8.parse(dir).root;
2648
2839
  while (dir !== root) {
2649
- if (existsSync10(path8.join(dir, ".git")))
2840
+ if (existsSync11(path8.join(dir, ".git")))
2650
2841
  return dir;
2651
2842
  dir = path8.dirname(dir);
2652
2843
  }
@@ -2780,6 +2971,7 @@ loadEnv({ quiet: true });
2780
2971
  loadEnv({ path: ".env.runtime", quiet: true });
2781
2972
  var BASE_URL = process.env.UNBROWSE_URL || "http://localhost:6969";
2782
2973
  var CLI_CLIENT_ID = process.env.UNBROWSE_CLIENT_ID || `cli-${process.ppid || process.pid}`;
2974
+ var walletNudgeShown = false;
2783
2975
  function parseArgs(argv) {
2784
2976
  const raw = argv.slice(2);
2785
2977
  const command = raw[0] && !raw[0].startsWith("--") ? raw[0] : "help";
@@ -3089,6 +3281,16 @@ async function cmdResolve(flags) {
3089
3281
  emitNextActionSummary(result);
3090
3282
  const skill = result.skill;
3091
3283
  const trace = result.trace;
3284
+ if (trace?.success && !walletNudgeShown) {
3285
+ try {
3286
+ const { checkWalletConfigured: checkWalletConfigured3 } = await Promise.resolve().then(() => (init_wallet2(), exports_wallet));
3287
+ const wallet = checkWalletConfigured3();
3288
+ if (!wallet.configured) {
3289
+ info("You're indexing routes but have no payout wallet. Run: npx @crossmint/lobster-cli setup");
3290
+ walletNudgeShown = true;
3291
+ }
3292
+ } catch (_e) {}
3293
+ }
3092
3294
  if (skill?.skill_id && trace) {
3093
3295
  result._feedback = `unbrowse feedback --skill ${skill.skill_id} --endpoint ${trace.endpoint_id || "?"} --rating <1-5>`;
3094
3296
  }
@@ -3472,6 +3674,16 @@ async function cmdSetup(flags) {
3472
3674
  info(`${hook.host} update hint hook ${hook.action} at ${hook.config_file}`);
3473
3675
  }
3474
3676
  }
3677
+ if (report.wallet.configured) {
3678
+ info(`Wallet configured (${report.wallet.provider}): ${report.wallet.wallet_address ?? "linked"}`);
3679
+ } else if (report.wallet.lobster_installed) {
3680
+ info("Wallet not paired \u2014 your indexed routes won't earn payouts.");
3681
+ info("Run: npx @crossmint/lobster-cli setup");
3682
+ } else {
3683
+ info("No wallet configured \u2014 you're indexing routes for free.");
3684
+ info("Set up a wallet so you earn when agents use your routes:");
3685
+ info(" npx @crossmint/lobster-cli setup");
3686
+ }
3475
3687
  await recordInstallTelemetryEvent("setup", {
3476
3688
  hostType,
3477
3689
  status: report.browser_engine.action === "failed" ? "failed" : "installed",
package/dist/mcp.js CHANGED
@@ -108,11 +108,11 @@ import { dirname, join, parse } from "path";
108
108
  import { fileURLToPath as fileURLToPath2 } from "url";
109
109
 
110
110
  // ../../src/build-info.generated.ts
111
- var BUILD_RELEASE_VERSION = "3.0.4";
112
- var BUILD_GIT_SHA = "44d2baa5a0e0";
111
+ var BUILD_RELEASE_VERSION = "3.1.0";
112
+ var BUILD_GIT_SHA = "c9f9ce0e5cb1";
113
113
  var BUILD_CODE_HASH = "1488fc1d92b7";
114
- var BUILD_RELEASE_MANIFEST_BASE64 = "eyJzY2hlbWFfdmVyc2lvbiI6MSwicmVsZWFzZV92ZXJzaW9uIjoiMy4wLjQiLCJnaXRfc2hhIjoiNDRkMmJhYTVhMGUwIiwiY29kZV9oYXNoIjoiMTQ4OGZjMWQ5MmI3IiwidHJhY2VfdmVyc2lvbiI6IjE0ODhmYzFkOTJiN0A0NGQyYmFhNWEwZTAiLCJpc3N1ZWRfYXQiOiIyMDI2LTA0LTA1VDA0OjIyOjE2LjM2M1oifQ";
115
- var BUILD_RELEASE_MANIFEST_SIGNATURE = "zLW181YNH-1BLhH-HL-OvhuGJAzfy-HrZnIo7xm0aVo";
114
+ var BUILD_RELEASE_MANIFEST_BASE64 = "eyJzY2hlbWFfdmVyc2lvbiI6MSwicmVsZWFzZV92ZXJzaW9uIjoiMy4xLjAiLCJnaXRfc2hhIjoiYzlmOWNlMGU1Y2IxIiwiY29kZV9oYXNoIjoiMTQ4OGZjMWQ5MmI3IiwidHJhY2VfdmVyc2lvbiI6IjE0ODhmYzFkOTJiN0BjOWY5Y2UwZTVjYjEiLCJpc3N1ZWRfYXQiOiIyMDI2LTA0LTA1VDA2OjUwOjA4LjQzNVoifQ";
115
+ var BUILD_RELEASE_MANIFEST_SIGNATURE = "_2pdxyoIEYkKUHO0nXaXTubP5SrZCa-8CqStcPBsXxw";
116
116
  var BUILD_DEFAULT_BACKEND_URL = "https://beta-api.unbrowse.ai";
117
117
 
118
118
  // ../../src/version.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unbrowse",
3
- "version": "3.0.4",
3
+ "version": "3.1.0",
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": {
@@ -1,6 +1,6 @@
1
- export const BUILD_RELEASE_VERSION = "3.0.4";
2
- export const BUILD_GIT_SHA = "44d2baa5a0e0";
1
+ export const BUILD_RELEASE_VERSION = "3.1.0";
2
+ export const BUILD_GIT_SHA = "c9f9ce0e5cb1";
3
3
  export const BUILD_CODE_HASH = "1488fc1d92b7";
4
- export const BUILD_RELEASE_MANIFEST_BASE64 = "eyJzY2hlbWFfdmVyc2lvbiI6MSwicmVsZWFzZV92ZXJzaW9uIjoiMy4wLjQiLCJnaXRfc2hhIjoiNDRkMmJhYTVhMGUwIiwiY29kZV9oYXNoIjoiMTQ4OGZjMWQ5MmI3IiwidHJhY2VfdmVyc2lvbiI6IjE0ODhmYzFkOTJiN0A0NGQyYmFhNWEwZTAiLCJpc3N1ZWRfYXQiOiIyMDI2LTA0LTA1VDA0OjIyOjE2LjM2M1oifQ";
5
- export const BUILD_RELEASE_MANIFEST_SIGNATURE = "zLW181YNH-1BLhH-HL-OvhuGJAzfy-HrZnIo7xm0aVo";
4
+ export const BUILD_RELEASE_MANIFEST_BASE64 = "eyJzY2hlbWFfdmVyc2lvbiI6MSwicmVsZWFzZV92ZXJzaW9uIjoiMy4xLjAiLCJnaXRfc2hhIjoiYzlmOWNlMGU1Y2IxIiwiY29kZV9oYXNoIjoiMTQ4OGZjMWQ5MmI3IiwidHJhY2VfdmVyc2lvbiI6IjE0ODhmYzFkOTJiN0BjOWY5Y2UwZTVjYjEiLCJpc3N1ZWRfYXQiOiIyMDI2LTA0LTA1VDA2OjUwOjA4LjQzNVoifQ";
5
+ export const BUILD_RELEASE_MANIFEST_SIGNATURE = "_2pdxyoIEYkKUHO0nXaXTubP5SrZCa-8CqStcPBsXxw";
6
6
  export const BUILD_DEFAULT_BACKEND_URL = "https://beta-api.unbrowse.ai";
@@ -30,6 +30,7 @@ loadEnv({ path: ".env.runtime", quiet: true });
30
30
 
31
31
  const BASE_URL = process.env.UNBROWSE_URL || "http://localhost:6969";
32
32
  const CLI_CLIENT_ID = process.env.UNBROWSE_CLIENT_ID || `cli-${process.ppid || process.pid}`;
33
+ let walletNudgeShown = false;
33
34
 
34
35
  // ---------------------------------------------------------------------------
35
36
  // Arg parser
@@ -374,6 +375,19 @@ async function cmdResolve(flags: Record<string, string | boolean>): Promise<void
374
375
 
375
376
  const skill = result.skill as Record<string, unknown> | undefined;
376
377
  const trace = result.trace as Record<string, unknown> | undefined;
378
+
379
+ // Nudge wallet setup after successful resolve that indexed routes
380
+ if (trace?.success && !walletNudgeShown) {
381
+ try {
382
+ const { checkWalletConfigured } = await import("./payments/wallet.js");
383
+ const wallet = checkWalletConfigured();
384
+ if (!wallet.configured) {
385
+ info("You're indexing routes but have no payout wallet. Run: npx @crossmint/lobster-cli setup");
386
+ walletNudgeShown = true;
387
+ }
388
+ } catch (_e) { /* non-fatal */ }
389
+ }
390
+
377
391
  if (skill?.skill_id && trace) {
378
392
  (result as Record<string, unknown>)._feedback = `unbrowse feedback --skill ${skill.skill_id} --endpoint ${trace.endpoint_id || "?"} --rating <1-5>`;
379
393
  }
@@ -812,6 +826,18 @@ async function cmdSetup(flags: Record<string, string | boolean>): Promise<void>
812
826
  }
813
827
  }
814
828
 
829
+ // Wallet status — tell the user if they're missing payout config
830
+ if (report.wallet.configured) {
831
+ info(`Wallet configured (${report.wallet.provider}): ${(report.wallet as Record<string, unknown>).wallet_address ?? "linked"}`);
832
+ } else if ((report.wallet as Record<string, unknown>).lobster_installed) {
833
+ info("Wallet not paired — your indexed routes won't earn payouts.");
834
+ info("Run: npx @crossmint/lobster-cli setup");
835
+ } else {
836
+ info("No wallet configured — you're indexing routes for free.");
837
+ info("Set up a wallet so you earn when agents use your routes:");
838
+ info(" npx @crossmint/lobster-cli setup");
839
+ }
840
+
815
841
  await recordInstallTelemetryEvent("setup", {
816
842
  hostType,
817
843
  status: report.browser_engine.action === "failed" ? "failed" : "installed",
@@ -497,7 +497,8 @@ async function apiRequest<T = unknown>(
497
497
  throw new Error("ToS update required. Restart unbrowse to accept new terms.");
498
498
  }
499
499
 
500
- // Handle x402 payment required — surface payment terms to the caller
500
+
501
+ // Handle x402 payment required — attempt lobster pay-and-retry before surfacing
501
502
  if (res.status === 402) {
502
503
  const paymentRequired = res.headers.get("PAYMENT-REQUIRED");
503
504
  const legacyPaymentTerms = res.headers.get("X-Payment-Required");
@@ -506,6 +507,29 @@ async function apiRequest<T = unknown>(
506
507
  : legacyPaymentTerms
507
508
  ? JSON.parse(legacyPaymentTerms)
508
509
  : (data as Record<string, unknown>).terms;
510
+
511
+ // Try lobster.cash automatic payment before throwing
512
+ try {
513
+ const { isLobsterAvailable, payAndRetry } = await import("../payments/lobster-pay.js");
514
+ if (isLobsterAvailable()) {
515
+ const fullUrl = `${API_URL}${path}`;
516
+ const paidResult = await payAndRetry<T>(fullUrl, {
517
+ body,
518
+ headers: {
519
+ "Content-Type": "application/json",
520
+ "Accept-Encoding": "gzip, deflate",
521
+ ...releaseAttestationHeaders,
522
+ ...(key ? { Authorization: `Bearer ${key}` } : {}),
523
+ },
524
+ });
525
+ if (paidResult) {
526
+ return { data: paidResult.data, headers: new Headers() };
527
+ }
528
+ }
529
+ } catch (payErr) {
530
+ console.warn(`[x402] lobster pay-and-retry failed: ${(payErr as Error).message}`);
531
+ }
532
+
509
533
  const err = new Error(`Payment required: ${(data as Record<string, unknown>).error ?? "This skill requires payment"}`);
510
534
  (err as Error & { x402: boolean; terms: unknown; status: number }).x402 = true;
511
535
  (err as Error & { terms: unknown }).terms = terms;
@@ -1882,28 +1882,40 @@ export async function executeEndpoint(
1882
1882
  wallet_configured: !!wallet.wallet_address,
1883
1883
  });
1884
1884
  if (gate.status === "payment_required" || gate.status === "wallet_not_configured" || gate.status === "insufficient_balance") {
1885
- const trace: ExecutionTrace = stampTrace({
1886
- trace_id: nanoid(),
1887
- skill_id: skill.skill_id,
1888
- endpoint_id: endpoint.endpoint_id,
1889
- started_at: new Date().toISOString(),
1890
- completed_at: new Date().toISOString(),
1891
- success: false,
1892
- status_code: 402,
1893
- error: "payment_required",
1894
- });
1895
- return {
1896
- trace,
1897
- result: {
1885
+ // If lobster wallet is available, let execution proceed —
1886
+ // the client-level apiRequest will handle 402 pay-and-retry automatically.
1887
+ let lobsterAvailable = false;
1888
+ try {
1889
+ const { isLobsterAvailable } = await import("../payments/lobster-pay.js");
1890
+ lobsterAvailable = isLobsterAvailable();
1891
+ } catch {}
1892
+
1893
+ if (lobsterAvailable && gate.status === "payment_required") {
1894
+ console.log(`[payment] ${skill.skill_id}: lobster available — proceeding with auto-pay`);
1895
+ } else {
1896
+ const trace: ExecutionTrace = stampTrace({
1897
+ trace_id: nanoid(),
1898
+ skill_id: skill.skill_id,
1899
+ endpoint_id: endpoint.endpoint_id,
1900
+ started_at: new Date().toISOString(),
1901
+ completed_at: new Date().toISOString(),
1902
+ success: false,
1903
+ status_code: 402,
1898
1904
  error: "payment_required",
1899
- price_usd: gate.requirement?.amount,
1900
- payment_status: gate.status,
1901
- message: gate.message,
1902
- wallet_provider: wallet.wallet_provider ?? "lobster.cash",
1903
- wallet_address: wallet.wallet_address,
1904
- indexing_fallback_available: true,
1905
- },
1906
- };
1905
+ });
1906
+ return {
1907
+ trace,
1908
+ result: {
1909
+ error: "payment_required",
1910
+ price_usd: gate.requirement?.amount,
1911
+ payment_status: gate.status,
1912
+ message: gate.message,
1913
+ wallet_provider: wallet.wallet_provider ?? "lobster.cash",
1914
+ wallet_address: wallet.wallet_address,
1915
+ indexing_fallback_available: true,
1916
+ },
1917
+ };
1918
+ }
1907
1919
  }
1908
1920
  }
1909
1921
 
@@ -216,7 +216,10 @@ function falseyEnv(value: string | undefined): boolean {
216
216
  }
217
217
 
218
218
  export function resolveKuriLaunchConfig(env: NodeJS.ProcessEnv = process.env): KuriLaunchConfig {
219
- const headless = envFlag(env.KURI_HEADLESS ?? env.HEADLESS);
219
+ const explicitHeadless = env.KURI_HEADLESS ?? env.HEADLESS;
220
+ const headless = explicitHeadless !== undefined
221
+ ? envFlag(explicitHeadless)
222
+ : (process.platform === "linux" && !env.DISPLAY); // auto-headless when no display on Linux
220
223
  const cleanRoom = envFlag(env.UNBROWSE_LOCAL_ONLY) || envFlag(env.KURI_CLEAN_ROOM);
221
224
  const browserCookieOptOut = falseyEnv(env.UNBROWSE_IMPORT_BROWSER_COOKIES);
222
225
  const explicitAttach = envFlag(env.KURI_ATTACH_EXISTING_CHROME ?? env.UNBROWSE_ATTACH_EXISTING_CHROME);
@@ -240,6 +243,7 @@ function currentBundledKuriTarget(): string | null {
240
243
  if (process.platform === "darwin" && process.arch === "x64") return "darwin-x64";
241
244
  if (process.platform === "linux" && process.arch === "arm64") return "linux-arm64";
242
245
  if (process.platform === "linux" && process.arch === "x64") return "linux-x64";
246
+ if (process.platform === "win32" && process.arch === "x64") return "win-x64";
243
247
  return null;
244
248
  }
245
249
 
@@ -3015,7 +3015,11 @@ export async function resolveAndExecute(
3015
3015
  console.log(`[prefetch] error: ${(prefetchErr as Error).message}`);
3016
3016
  }
3017
3017
  // --- Payment gate: only for marketplace-sourced paid skills ---
3018
- if (source === "marketplace" && skill.base_price_usd && skill.base_price_usd > 0) {
3018
+ const dynamicPrice = source === "marketplace"
3019
+ ? (skill.base_price_usd ?? await (await import("../payments/index.js")).fetchDynamicPrice(skill.skill_id))
3020
+ : null;
3021
+ const effectivePrice = typeof dynamicPrice === "string" ? parseFloat(dynamicPrice) : (dynamicPrice ?? 0);
3022
+ if (source === "marketplace" && effectivePrice > 0) {
3019
3023
  try {
3020
3024
  const walletCheck = checkWalletConfigured();
3021
3025
  const wallet = getLocalWalletContext();
@@ -3023,7 +3027,7 @@ export async function resolveAndExecute(
3023
3027
  skill.skill_id,
3024
3028
  candidate.endpoint.endpoint_id,
3025
3029
  {
3026
- price_usd: String(skill.base_price_usd),
3030
+ price_usd: String(effectivePrice),
3027
3031
  wallet_configured: walletCheck.configured,
3028
3032
  },
3029
3033
  );
@@ -3038,7 +3042,7 @@ export async function resolveAndExecute(
3038
3042
  return {
3039
3043
  result: {
3040
3044
  error: "payment_required",
3041
- price_usd: skill.base_price_usd,
3045
+ price_usd: effectivePrice,
3042
3046
  payment_status: paymentResult.status,
3043
3047
  message: paymentResult.message,
3044
3048
  next_step: paymentResult.next_step,
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Lobster.cash x402 payment bridge.
3
+ *
4
+ * Handles the pay-and-retry cycle:
5
+ * 1. Receives x402 payment terms from a 402 response
6
+ * 2. Calls `lobstercash x402 fetch <url>` to pay + retry
7
+ * 3. Returns the paid response body
8
+ *
9
+ * Delegation boundary:
10
+ * - Unbrowse owns: detecting 402, passing the URL, using the result
11
+ * - Lobster owns: wallet signing, transaction broadcast, proof construction
12
+ */
13
+
14
+ import { execFile, execFileSync } from "node:child_process";
15
+ import { existsSync } from "node:fs";
16
+ import { homedir } from "node:os";
17
+ import { join } from "node:path";
18
+
19
+ const LOBSTER_PAY_TIMEOUT_MS = 30_000;
20
+
21
+ /**
22
+ * Resolve the lobster CLI command. Prefers the direct binary if
23
+ * installed globally, otherwise falls back to npx.
24
+ */
25
+ function getLobsterCommand(): { cmd: string; prefix: string[] } | null {
26
+ // 1. Direct binary in PATH
27
+ try {
28
+ execFileSync("lobstercash", ["--version"], { stdio: "ignore", timeout: 3_000 });
29
+ return { cmd: "lobstercash", prefix: [] };
30
+ } catch (_e) { /* not in PATH */ }
31
+
32
+ // 2. Check npm global bin (often not in agent PATH)
33
+ try {
34
+ const npmPrefix = execFileSync("npm", ["config", "get", "prefix"], { encoding: "utf8", timeout: 5_000 }).trim();
35
+ const lobsterPath = join(npmPrefix, "bin", "lobstercash");
36
+ if (existsSync(lobsterPath)) {
37
+ execFileSync(lobsterPath, ["--version"], { stdio: "ignore", timeout: 3_000 });
38
+ return { cmd: lobsterPath, prefix: [] };
39
+ }
40
+ } catch (_e) { /* npm not available */ }
41
+
42
+ return null;
43
+ }
44
+
45
+ let cachedCommand: { cmd: string; prefix: string[] } | null | undefined = undefined;
46
+ function lobsterCmd(): { cmd: string; prefix: string[] } | null {
47
+ if (cachedCommand === undefined) cachedCommand = getLobsterCommand();
48
+ return cachedCommand;
49
+ }
50
+
51
+ export interface LobsterPayResult {
52
+ success: boolean;
53
+ body: string;
54
+ statusCode?: number;
55
+ error?: string;
56
+ }
57
+
58
+ /**
59
+ * Check if the lobster CLI is available and wallet is configured.
60
+ */
61
+ export function isLobsterAvailable(): boolean {
62
+ const agentsPath = join(process.env.HOME || homedir(), ".lobster", "agents.json");
63
+ return existsSync(agentsPath);
64
+ }
65
+
66
+ /**
67
+ * Pay for an x402-gated URL via lobster.cash and return the response.
68
+ *
69
+ * Shells out to `lobstercash x402 fetch <url>` which handles:
70
+ * - Reading 402 + payment terms
71
+ * - Signing + broadcasting the USDC transfer
72
+ * - Retrying with PAYMENT-SIGNATURE header
73
+ * - Returning the paid response
74
+ */
75
+ export function lobsterX402Fetch(
76
+ url: string,
77
+ options?: {
78
+ jsonBody?: string;
79
+ headers?: Record<string, string>;
80
+ timeoutMs?: number;
81
+ },
82
+ ): Promise<LobsterPayResult> {
83
+ return new Promise((resolve) => {
84
+ const resolved = lobsterCmd();
85
+ if (!resolved) {
86
+ resolve({ success: false, body: "", error: "lobstercash CLI not in PATH" });
87
+ return;
88
+ }
89
+ const { cmd, prefix } = resolved;
90
+ const args = [...prefix, "x402", "fetch", url, "--debug"];
91
+
92
+ if (options?.jsonBody) {
93
+ args.push("--json", options.jsonBody);
94
+ }
95
+
96
+ if (options?.headers) {
97
+ for (const [key, value] of Object.entries(options.headers)) {
98
+ args.push("--header", `${key}:${value}`);
99
+ }
100
+ }
101
+
102
+ const timeout = options?.timeoutMs ?? LOBSTER_PAY_TIMEOUT_MS;
103
+ args.push("--timeout", String(timeout));
104
+
105
+ execFile(cmd, args, { timeout: timeout + 5_000, maxBuffer: 1024 * 1024 }, (err, stdout, stderr) => {
106
+ if (err) {
107
+ const msg = stderr?.trim() || err.message;
108
+ console.warn(`[lobster-pay] x402 fetch failed: ${msg}`);
109
+ resolve({ success: false, body: "", error: msg });
110
+ return;
111
+ }
112
+
113
+ if (stderr) {
114
+ for (const line of stderr.split("\n").filter(Boolean)) {
115
+ console.log(`[lobster-pay] ${line}`);
116
+ }
117
+ }
118
+
119
+ // Parse status from stdout header line: "Status: 200"
120
+ const statusMatch = stdout.match(/^Status:\s*(\d+)/m);
121
+ const statusCode = statusMatch ? parseInt(statusMatch[1], 10) : undefined;
122
+
123
+ if (statusCode && statusCode >= 400) {
124
+ resolve({ success: false, body: stdout, statusCode, error: `HTTP ${statusCode}` });
125
+ return;
126
+ }
127
+
128
+ resolve({ success: true, body: stdout, statusCode });
129
+ });
130
+ });
131
+ }
132
+
133
+ /**
134
+ * Pay for an x402-gated API request and return the parsed JSON response.
135
+ *
136
+ * This is the high-level entry point for the pay-and-retry loop.
137
+ * Returns null if payment fails or lobster is unavailable.
138
+ */
139
+ export async function payAndRetry<T = unknown>(
140
+ fullUrl: string,
141
+ options?: {
142
+ body?: unknown;
143
+ headers?: Record<string, string>;
144
+ },
145
+ ): Promise<{ data: T; paid: true } | null> {
146
+ if (!isLobsterAvailable()) {
147
+ console.log("[lobster-pay] lobster.cash not configured — skipping payment");
148
+ return null;
149
+ }
150
+
151
+ console.log(`[lobster-pay] attempting x402 payment for ${fullUrl}`);
152
+
153
+ const result = await lobsterX402Fetch(fullUrl, {
154
+ jsonBody: options?.body ? JSON.stringify(options.body) : undefined,
155
+ headers: options?.headers,
156
+ });
157
+
158
+ if (!result.success) {
159
+ console.warn(`[lobster-pay] payment failed: ${result.error}`);
160
+ return null;
161
+ }
162
+ try {
163
+ // lobstercash x402 fetch outputs header lines before the JSON body:
164
+ // x402 FETCH <url>
165
+ // Status: 200
166
+ // Content-Type: application/json
167
+ // <blank line>
168
+ // {"actual":"json",...}
169
+ // Extract the JSON portion by finding the first '{' or '['
170
+ const raw = result.body;
171
+ const jsonStart = Math.min(
172
+ ...[raw.indexOf("{"), raw.indexOf("[")].filter((i) => i >= 0),
173
+ );
174
+ const jsonStr = jsonStart >= 0 ? raw.slice(jsonStart) : raw;
175
+ const data = JSON.parse(jsonStr) as T;
176
+ console.log("[lobster-pay] payment successful — got paid response");
177
+ return { data, paid: true };
178
+ } catch (_e) {
179
+ console.warn("[lobster-pay] paid response was not valid JSON");
180
+ return null;
181
+ }
182
+ }
Binary file
Binary file
Binary file
Binary file
@@ -1,24 +1,28 @@
1
1
  {
2
2
  "repo_url": "https://github.com/justrach/kuri.git",
3
3
  "branch": "adding-extensions",
4
- "source_sha": "005ce5cdf41347b683df99d307868a11439be972",
5
- "built_at": "2026-04-04T11:57:45.378Z",
4
+ "source_sha": "08eecbe3740f046a46f656eed7ebfc66c1bad9bb",
5
+ "built_at": "2026-04-05T06:43:57.212Z",
6
6
  "binaries": {
7
7
  "darwin-arm64": {
8
8
  "zig_target": "aarch64-macos",
9
- "sha256": "44d5e985e075ba57c7cc8e3ab51a39186b4bf62ece4d0c2e57797cbd4d0408d2"
9
+ "sha256": "1553633e722d18059dedffa8a52d55ed6c052e4961fd2753ee0b62be60b241bf"
10
10
  },
11
11
  "darwin-x64": {
12
12
  "zig_target": "x86_64-macos",
13
- "sha256": "09dc2104449506f2ba8a73edc884bb7399c349c12fc6ec6b3d5f7f652918742f"
13
+ "sha256": "b5eb07e631c6ddad64019c8d0c86c32cb76a74ff0791ac5611a3aa3550767ec8"
14
14
  },
15
15
  "linux-arm64": {
16
16
  "zig_target": "aarch64-linux",
17
- "sha256": "98c18cec5a588f44746cd465b0c8fd17cec311ca03e1292dab173841db8bfcf1"
17
+ "sha256": "ea88a26f7b335d5842b0c1d83bfa4066bed0a119284560f6bd3833f1d240cce2"
18
18
  },
19
19
  "linux-x64": {
20
20
  "zig_target": "x86_64-linux",
21
- "sha256": "6d081f758ee76e44bbf1bb9c633057212f9c1641d97060e391dadf35015fbbe2"
21
+ "sha256": "175a7c59e458e952a26974f0fb5c2ce374e56f2c4c352903b481b5aa5a16978f"
22
+ },
23
+ "win-x64": {
24
+ "zig_target": "x86_64-windows",
25
+ "sha256": "176291ad9827a183ba7322ddb56cc1fa5edc7c214a264ecdf8a1d5d18366d686"
22
26
  }
23
27
  }
24
28
  }
Binary file