@tempoxyz/patron 0.0.20 → 0.0.22

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 CHANGED
@@ -50,7 +50,9 @@ and the site's domain through an Oblivious HTTP relay (RFC 9458): the relay sees
50
50
  not the content; the gateway sees the content but not your IP.
51
51
 
52
52
  Local state lives in `~/.patron`: settings, seals encrypted with AES-256-GCM under a key
53
- file readable only by you, and this month's per-site counts, which never leave the machine.
53
+ file readable only by you, this month's per-site counts, which never leave the machine, and
54
+ a copy of the CLI in `~/.patron/cli` that agent hooks start with node, so a read never waits
55
+ on npx. Running `npx @tempoxyz/patron` updates the copy.
54
56
 
55
57
  ## Budget
56
58
 
package/dist/bin.js CHANGED
@@ -4,14 +4,14 @@ const require = __patronCreateRequire(import.meta.url)
4
4
 
5
5
  // src/bin.ts
6
6
  if (process.argv[2] === "hook") {
7
- const { runHookProcess } = await import("./chunks/hook-process-Y2W56T2X.js");
7
+ const { runHookProcess } = await import("./chunks/hook-process-D5ENJPRB.js");
8
8
  await runHookProcess().catch(() => void 0);
9
9
  process.exit(0);
10
10
  } else if (process.argv[2] === "pay") {
11
- const { runPayProcess } = await import("./chunks/hook-process-Y2W56T2X.js");
11
+ const { runPayProcess } = await import("./chunks/hook-process-D5ENJPRB.js");
12
12
  await runPayProcess().catch(() => void 0);
13
13
  process.exit(0);
14
14
  } else {
15
- const { run } = await import("./chunks/-RNUFDXSV.js");
15
+ const { run } = await import("./chunks/-PMO5HHO2.js");
16
16
  process.exitCode = await run(process.argv.slice(2));
17
17
  }
@@ -2,6 +2,7 @@ import { createRequire as __patronCreateRequire } from 'node:module'
2
2
  const require = __patronCreateRequire(import.meta.url)
3
3
  import {
4
4
  FileTokenStore,
5
+ LINK_TOKEN_STEP,
5
6
  LeaderboardResponse,
6
7
  PatronInfo,
7
8
  STARTER_TOKENS,
@@ -28,10 +29,10 @@ import {
28
29
  withPatronStatus,
29
30
  withoutPatronHandlers,
30
31
  writeJsonObject
31
- } from "./chunk-UFBFF3R6.js";
32
+ } from "./chunk-QKBTYUXG.js";
32
33
  import {
33
34
  startRefill
34
- } from "./chunk-CV2WYKM7.js";
35
+ } from "./chunk-HEQBEBKL.js";
35
36
  import {
36
37
  DEFAULT_MONTHLY_BUDGET_USD,
37
38
  MAX_TOKENS_PER_PAGE,
@@ -57,18 +58,19 @@ import {
57
58
  withLock,
58
59
  writeConfig,
59
60
  writePrivateFile
60
- } from "./chunk-RDEPJ6XD.js";
61
+ } from "./chunk-UWIBR62N.js";
61
62
  import "./chunk-GTMLTZRN.js";
62
63
 
63
64
  // src/index.ts
64
65
  import { readFileSync } from "node:fs";
65
- import { rm as rm3 } from "node:fs/promises";
66
- import { homedir } from "node:os";
66
+ import { rm as rm5 } from "node:fs/promises";
67
+ import { homedir as homedir2 } from "node:os";
67
68
 
68
69
  // src/agents.ts
69
70
  import { constants } from "node:fs";
70
- import { access, realpath } from "node:fs/promises";
71
- import { delimiter, join as join4 } from "node:path";
71
+ import { access as access2, realpath as realpath2 } from "node:fs/promises";
72
+ import { homedir } from "node:os";
73
+ import { delimiter, join as join5 } from "node:path";
72
74
 
73
75
  // src/amp.ts
74
76
  import { mkdir, readFile, rm } from "node:fs/promises";
@@ -322,6 +324,66 @@ function parse(text) {
322
324
  }
323
325
  }
324
326
 
327
+ // src/stable-install.ts
328
+ import {
329
+ access,
330
+ cp,
331
+ mkdir as mkdir2,
332
+ readdir,
333
+ readFile as readFile3,
334
+ readlink,
335
+ realpath,
336
+ rename,
337
+ rm as rm3,
338
+ symlink
339
+ } from "node:fs/promises";
340
+ import { basename, dirname as dirname3, join as join4 } from "node:path";
341
+ function stableBin(home) {
342
+ return join4(home, "cli", "current", "dist", "bin.js");
343
+ }
344
+ async function installStableCli(home, script = process.argv[1]) {
345
+ const resolved = script ? await realpath(script).catch(() => void 0) : void 0;
346
+ if (!resolved || !/[/\\]_npx[/\\]/.test(resolved) || basename(resolved) !== "bin.js")
347
+ return void 0;
348
+ const packageDir = dirname3(dirname3(resolved));
349
+ const { version } = JSON.parse(await readFile3(join4(packageDir, "package.json"), "utf8"));
350
+ if (typeof version !== "string" || !/^[\w.+-]+$/.test(version)) return void 0;
351
+ const root = join4(home, "cli");
352
+ const target = join4(root, version);
353
+ await mkdir2(root, { recursive: true, mode: 448 });
354
+ if (!await isFile(join4(target, "dist", "bin.js"))) {
355
+ const staging = `${target}.${process.pid}.tmp`;
356
+ await rm3(staging, { force: true, recursive: true });
357
+ await cp(join4(packageDir, "dist"), join4(staging, "dist"), { recursive: true });
358
+ await cp(join4(packageDir, "package.json"), join4(staging, "package.json"));
359
+ await rm3(target, { force: true, recursive: true });
360
+ await rename(staging, target);
361
+ }
362
+ const current = join4(root, "current");
363
+ const linked = await readlink(current).catch(() => void 0);
364
+ const previous = linked ? basename(linked) : void 0;
365
+ if (previous !== version) {
366
+ const link = `${current}.${process.pid}.tmp`;
367
+ await rm3(link, { force: true });
368
+ await symlink(process.platform === "win32" ? target : version, link, "junction");
369
+ await rename(link, current).catch(async () => {
370
+ await rm3(current, { force: true, recursive: true });
371
+ await rename(link, current);
372
+ });
373
+ }
374
+ for (const entry of await readdir(root)) {
375
+ if (entry === "current" || entry === version || entry === previous) continue;
376
+ await rm3(join4(root, entry), { force: true, recursive: true });
377
+ }
378
+ return stableBin(home);
379
+ }
380
+ async function isFile(path) {
381
+ return access(path).then(
382
+ () => true,
383
+ () => false
384
+ );
385
+ }
386
+
325
387
  // src/agents.ts
326
388
  var AGENT_LABELS = {
327
389
  amp: "Amp",
@@ -332,7 +394,7 @@ var AGENT_LABELS = {
332
394
  var ADAPTERS = {
333
395
  amp: {
334
396
  files: ({ env, home }) => [ampPluginPath(env, home)],
335
- detect: async ({ env, home }) => await exists(join4(env.XDG_CONFIG_HOME || join4(home, ".config"), "amp")) || await onPath(["amp"], env.PATH ?? ""),
397
+ detect: async ({ env, home }) => await exists(join5(env.XDG_CONFIG_HOME || join5(home, ".config"), "amp")) || await onPath(["amp"], env.PATH ?? ""),
336
398
  install: async (command, { env, home }) => ({
337
399
  state: await installAmpPlugin(ampPluginPath(env, home), command)
338
400
  }),
@@ -357,7 +419,7 @@ var ADAPTERS = {
357
419
  },
358
420
  cursor: {
359
421
  files: ({ home }) => [cursorHooksPath(home)],
360
- detect: async ({ env, home }) => await exists(join4(home, ".cursor")) || await onPath(["cursor-agent", "cursor"], env.PATH ?? ""),
422
+ detect: async ({ env, home }) => await exists(join5(home, ".cursor")) || await onPath(["cursor-agent", "cursor"], env.PATH ?? ""),
361
423
  install: async (command, { home }) => ({
362
424
  state: await installCursorHook(cursorHooksPath(home), command)
363
425
  }),
@@ -365,10 +427,10 @@ var ADAPTERS = {
365
427
  }
366
428
  };
367
429
  function claudeDirectory({ env, home }) {
368
- return env.CLAUDE_CONFIG_DIR || join4(home, ".claude");
430
+ return env.CLAUDE_CONFIG_DIR || join5(home, ".claude");
369
431
  }
370
432
  function claudeSettingsPath(environment) {
371
- return join4(claudeDirectory(environment), "settings.json");
433
+ return join5(claudeDirectory(environment), "settings.json");
372
434
  }
373
435
  async function detectAgents(environment) {
374
436
  const found = await Promise.all(
@@ -421,7 +483,7 @@ async function removeAgents(environment) {
421
483
  );
422
484
  }
423
485
  function originalsStore({ env, home }) {
424
- return originalsPath(env.PATRON_HOME || join4(home, ".patron"));
486
+ return originalsPath(env.PATRON_HOME || join5(home, ".patron"));
425
487
  }
426
488
  function failure(agent, error) {
427
489
  return {
@@ -431,18 +493,20 @@ function failure(agent, error) {
431
493
  state: "failed"
432
494
  };
433
495
  }
434
- async function hookCommand(env = process.env, script = process.argv[1], node = process.execPath) {
496
+ async function hookCommand(env = process.env, script = process.argv[1], node = process.execPath, patronHome = env.PATRON_HOME || join5(homedir(), ".patron")) {
435
497
  if (env.PATRON_HOOK_COMMAND) return env.PATRON_HOOK_COMMAND;
436
- const resolved = script ? await realpath(script).catch(() => void 0) : void 0;
498
+ const resolved = script ? await realpath2(script).catch(() => void 0) : void 0;
437
499
  if (resolved && /[/\\]bin\.js$/.test(resolved) && !/[/\\]_npx[/\\]/.test(resolved))
438
500
  return `${quote(node)} ${quote(resolved)} hook`;
501
+ if (await exists(stableBin(patronHome)))
502
+ return `${quote(node)} ${quote(stableBin(patronHome))} hook`;
439
503
  return "npx -y @tempoxyz/patron hook";
440
504
  }
441
505
  function quote(value) {
442
506
  return /^[\w./:@-]+$/.test(value) ? value : `'${value.replaceAll("'", "'\\''")}'`;
443
507
  }
444
508
  async function exists(path) {
445
- return access(path).then(
509
+ return access2(path).then(
446
510
  () => true,
447
511
  () => false
448
512
  );
@@ -451,7 +515,7 @@ async function onPath(commands, path) {
451
515
  for (const directory of path.split(delimiter)) {
452
516
  if (!directory) continue;
453
517
  for (const command of commands) {
454
- const found = await access(join4(directory, command), constants.X_OK).then(
518
+ const found = await access2(join5(directory, command), constants.X_OK).then(
455
519
  () => true,
456
520
  () => false
457
521
  );
@@ -617,9 +681,19 @@ ${styleText(title, "bold", color)}
617
681
  }
618
682
 
619
683
  // src/purchase.ts
684
+ import { rm as rm4 } from "node:fs/promises";
685
+ var LINK_REFILL_BACKOFF_MS = 60 * 60 * 1e3;
686
+ async function linkRefillFailedAt(home) {
687
+ const value = await readJson(statePaths(home).linkRefill).catch(() => void 0);
688
+ return typeof value?.failedAt === "number" ? value.failedAt : void 0;
689
+ }
620
690
  var REFILL_LOCK_STALE_MS = 5 * 60 * 1e3;
621
691
  var defaultOpenWallet = async (settings, config, issuer, maxUnits) => {
622
- const { openWallet } = await import("./wallet-WNPT6R4M.js");
692
+ if (config.wallet === "link") {
693
+ const { openLinkWallet } = await import("./wallet-R3UWBXVC.js");
694
+ return openLinkWallet(config, maxUnits);
695
+ }
696
+ const { openWallet } = await import("./wallet-R3UWBXVC.js");
623
697
  return openWallet(settings, config.wallet, issuer, maxUnits);
624
698
  };
625
699
  async function buyWithinBudget(settings, config, wanted, openWallet = defaultOpenWallet) {
@@ -645,7 +719,7 @@ async function buyWithinBudget(settings, config, wanted, openWallet = defaultOpe
645
719
  });
646
720
  return tokens.length;
647
721
  }
648
- async function refill(base, openWallet = defaultOpenWallet) {
722
+ async function refill(base, openWallet = defaultOpenWallet, now = Date.now) {
649
723
  const config = await readConfig(base.home);
650
724
  if (!config || config.paused) return 0;
651
725
  const settings = settingsFor(base, config);
@@ -656,7 +730,19 @@ async function refill(base, openWallet = defaultOpenWallet) {
656
730
  if (!release) return 0;
657
731
  try {
658
732
  if (!needsRefill(await new FileTokenStore(settings.home).count())) return 0;
659
- return await buyWithinBudget(settings, config, PACK_SIZE, openWallet);
733
+ if (config.wallet !== "link")
734
+ return await buyWithinBudget(settings, config, PACK_SIZE, openWallet);
735
+ const failedAt = await linkRefillFailedAt(settings.home);
736
+ if (failedAt !== void 0 && now() - failedAt < LINK_REFILL_BACKOFF_MS) return 0;
737
+ const path = statePaths(settings.home).linkRefill;
738
+ try {
739
+ const bought = await buyWithinBudget(settings, config, PACK_SIZE, openWallet);
740
+ await rm4(path, { force: true });
741
+ return bought;
742
+ } catch (error) {
743
+ await writePrivateFile(path, JSON.stringify({ failedAt: now() }));
744
+ throw error;
745
+ }
660
746
  } finally {
661
747
  await release();
662
748
  }
@@ -709,6 +795,18 @@ function formatStatus(view, color) {
709
795
  color
710
796
  )
711
797
  ];
798
+ if (view.linkRefillFailedAt !== void 0)
799
+ lines.push(
800
+ row(
801
+ "Refill",
802
+ styleText(
803
+ "last Link refill was not approved; approve the next one in the Link app (tries again after an hour)",
804
+ "yellow",
805
+ color
806
+ ),
807
+ color
808
+ )
809
+ );
712
810
  if (top.length > 0) lines.push(row("Top sites", top.join(" \xB7 "), color));
713
811
  return lines.join("\n");
714
812
  }
@@ -737,14 +835,15 @@ Usage: patron [command]
737
835
  hook Run by agents after each web read (reads the event JSON)
738
836
 
739
837
  Environment: PATRON_HOME, PATRON_GATEWAY_URL, PATRON_RELAY_URL, PATRON_NETWORK,
740
- PATRON_PRIVATE_KEY (testnet automation), PATRON_BUDGET_USD (non-interactive setup).`;
838
+ PATRON_PRIVATE_KEY (testnet automation), PATRON_BUDGET_USD (non-interactive setup),
839
+ PATRON_WALLET=link (pay with Link through link-cli), PATRON_LINK_CLI (its path).`;
741
840
  var UsageError = class extends Error {
742
841
  };
743
842
  function defaults() {
744
843
  const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
745
844
  const color = supportsColor(process.stdout);
746
845
  return {
747
- agents: { env: process.env, home: homedir() },
846
+ agents: { env: process.env, home: homedir2() },
748
847
  color,
749
848
  fetch: globalThis.fetch,
750
849
  interactive,
@@ -848,6 +947,18 @@ async function main(deps) {
848
947
  await status(deps, deps.interactive);
849
948
  }
850
949
  async function syncAgents(deps, config) {
950
+ await installStableCli(deps.settings.home).catch(() => void 0);
951
+ const command = await hookCommand(deps.agents.env);
952
+ const moved = (await installAgents(config.agents, command, deps.agents)).filter((result) => result.state === "installed").map((result) => result.agent);
953
+ if (moved.length > 0) {
954
+ say(
955
+ deps,
956
+ `Patron now starts with node for ${moved.map((a) => AGENT_LABELS[a]).join(", ")}.`
957
+ );
958
+ if (moved.includes("codex"))
959
+ say(deps, " Codex: trust the hook again in /hooks when it asks.");
960
+ say(deps);
961
+ }
851
962
  const upgraded = await upgradeAgents(config.agents, deps.agents);
852
963
  if (upgraded.length > 0) {
853
964
  const names = upgraded.map((agent) => AGENT_LABELS[agent]).join(", ");
@@ -860,11 +971,7 @@ async function syncAgents(deps, config) {
860
971
  (agent) => !config.agents.includes(agent)
861
972
  );
862
973
  if (added.length === 0) return;
863
- const results = await installAgents(
864
- added,
865
- await hookCommand(deps.agents.env),
866
- deps.agents
867
- );
974
+ const results = await installAgents(added, command, deps.agents);
868
975
  for (const line of formatAgentResults(results, deps.color)) say(deps, line);
869
976
  say(deps);
870
977
  const installed = results.filter((result) => result.state !== "failed").map((result) => result.agent);
@@ -886,14 +993,12 @@ async function setup(deps) {
886
993
  });
887
994
  const budget = await askBudget(deps, DEFAULT_MONTHLY_BUDGET_USD);
888
995
  const budgetUnits = BigInt(budgetTokens(budget)) * TOKEN_PRICE_UNITS;
889
- step(
890
- deps,
891
- settings.privateKey ? "Using PATRON_PRIVATE_KEY" : "Signing in with Tempo Wallet"
892
- );
893
- const { signIn } = await import("./wallet-WNPT6R4M.js");
894
- const account = await signIn(settings, { budgetUnits, issuer, output: deps.stdout });
895
- done(deps, `Signed in as ${account.address}`);
996
+ const payWithLink = await askPayWithLink(deps);
997
+ if (payWithLink && budgetTokens(budget) < LINK_TOKEN_STEP)
998
+ throw new UsageError("Paying with Link needs a monthly budget of at least $0.01.");
999
+ const account = payWithLink ? await linkSignIn(deps) : await tempoSignIn(deps, budgetUnits, issuer);
896
1000
  const config = {
1001
+ ...account.linkPaymentMethodId ? { linkPaymentMethodId: account.linkPaymentMethodId } : {},
897
1002
  address: account.address,
898
1003
  agents: [],
899
1004
  gatewayUrl: settings.gatewayUrl,
@@ -905,11 +1010,13 @@ async function setup(deps) {
905
1010
  wallet: account.wallet
906
1011
  };
907
1012
  await writeConfig(settings.home, config);
908
- const first = purchasableTokens(config, await readUsage(settings.home), STARTER_TOKENS);
1013
+ const starter = config.wallet === "link" ? PACK_SIZE : STARTER_TOKENS;
1014
+ const first = purchasableTokens(config, await readUsage(settings.home), starter);
909
1015
  step(deps, `Buying ${first.toLocaleString("en-US")} seals (${tokensUsd(first)})`);
910
- const bought = await buyWithinBudget(settings, config, STARTER_TOKENS, deps.openWallet);
1016
+ const bought = await buyWithinBudget(settings, config, starter, deps.openWallet);
911
1017
  done(deps, `${bought.toLocaleString("en-US")} seals ready`);
912
1018
  deps.startRefill();
1019
+ await installStableCli(settings.home).catch(() => void 0);
913
1020
  const agents = await detectAgents(deps.agents);
914
1021
  if (agents.length === 0) {
915
1022
  say(deps);
@@ -933,6 +1040,64 @@ async function setup(deps) {
933
1040
  styleText("\u2713 Patron on. Agents now pay the sites they read.", "green", deps.color)
934
1041
  );
935
1042
  }
1043
+ async function askPayWithLink(deps) {
1044
+ if (deps.settings.privateKey) return false;
1045
+ const preset = process.env.PATRON_WALLET;
1046
+ if (preset) return preset === "link";
1047
+ if (!deps.interactive) return false;
1048
+ const prompter = deps.prompter();
1049
+ try {
1050
+ const choice = await prompter.choose(
1051
+ "Pay with",
1052
+ ["Tempo Wallet", "Link (your Link balance or a card, via link-cli)"],
1053
+ 0
1054
+ );
1055
+ return choice === 1;
1056
+ } finally {
1057
+ prompter.close();
1058
+ }
1059
+ }
1060
+ async function tempoSignIn(deps, budgetUnits, issuer) {
1061
+ step(
1062
+ deps,
1063
+ deps.settings.privateKey ? "Using PATRON_PRIVATE_KEY" : "Signing in with Tempo Wallet"
1064
+ );
1065
+ const { signIn } = await import("./wallet-R3UWBXVC.js");
1066
+ const account = await signIn(deps.settings, {
1067
+ budgetUnits,
1068
+ issuer,
1069
+ output: deps.stdout
1070
+ });
1071
+ done(deps, `Signed in as ${account.address}`);
1072
+ return account;
1073
+ }
1074
+ async function linkSignIn(deps) {
1075
+ step(deps, "Checking your Link wallet (link-cli)");
1076
+ const { linkBalance, linkCli, linkPaymentMethods } = await import("./link-OU7W7IT6.js");
1077
+ const cli = deps.linkCli ?? linkCli();
1078
+ const methods = await linkPaymentMethods(cli).catch((error) => {
1079
+ throw new Error(
1080
+ `${error.message}
1081
+ Patron pays with Link through Stripe's Link CLI: npm i -g @stripe/link-cli, then link-cli auth login.`
1082
+ );
1083
+ });
1084
+ const balance = linkBalance(methods);
1085
+ if (balance) {
1086
+ const available = balance.balance_details?.available_balance;
1087
+ done(
1088
+ deps,
1089
+ `Paying with your Link balance${available ? ` ($${(available.amount / 100).toFixed(2)} available)` : ""}`
1090
+ );
1091
+ return { address: "", linkPaymentMethodId: balance.id, wallet: "link" };
1092
+ }
1093
+ const fallback = methods.find((method) => method.is_default) ?? methods[0];
1094
+ if (!fallback)
1095
+ throw new Error(
1096
+ "Your Link wallet has no payment method. Add one at https://app.link.com."
1097
+ );
1098
+ done(deps, `Paying with Link: ${fallback.name}`);
1099
+ return { address: "", linkPaymentMethodId: fallback.id, wallet: "link" };
1100
+ }
936
1101
  async function askBudget(deps, fallback) {
937
1102
  const preset = process.env.PATRON_BUDGET_USD;
938
1103
  if (!deps.interactive) {
@@ -955,14 +1120,21 @@ async function askBudget(deps, fallback) {
955
1120
  }
956
1121
  async function status(deps, menu) {
957
1122
  const { config, settings } = await loadSetup(deps);
958
- const [tokensLeft, usage, standing] = await Promise.all([
1123
+ const [tokensLeft, usage, standing, refillFailedAt] = await Promise.all([
959
1124
  new FileTokenStore(settings.home).count(),
960
1125
  readUsage(settings.home),
961
- fetchStanding(settings.gatewayUrl, config.address, config.name, deps.fetch).catch(
1126
+ config.address ? fetchStanding(settings.gatewayUrl, config.address, config.name, deps.fetch).catch(
962
1127
  () => void 0
963
- )
1128
+ ) : void 0,
1129
+ config.wallet === "link" ? linkRefillFailedAt(settings.home) : void 0
964
1130
  ]);
965
- say(deps, formatStatus({ config, standing, tokensLeft, usage }, deps.color));
1131
+ say(
1132
+ deps,
1133
+ formatStatus(
1134
+ { config, linkRefillFailedAt: refillFailedAt, standing, tokensLeft, usage },
1135
+ deps.color
1136
+ )
1137
+ );
966
1138
  if (!menu) return;
967
1139
  const prompter = deps.prompter();
968
1140
  let choice;
@@ -1004,7 +1176,7 @@ async function changeBudget(deps, input) {
1004
1176
  } else budget = await askBudget(deps, config.monthlyBudgetUsd);
1005
1177
  if (config.wallet === "tempo" && budget > config.monthlyBudgetUsd) {
1006
1178
  step(deps, "Approve the new spending limit in Tempo Wallet");
1007
- const { authorizeBudget } = await import("./wallet-WNPT6R4M.js");
1179
+ const { authorizeBudget } = await import("./wallet-R3UWBXVC.js");
1008
1180
  const issuer = await loadIssuer(settings.home, settings.gatewayUrl, {
1009
1181
  fetch: deps.fetch
1010
1182
  });
@@ -1113,10 +1285,14 @@ async function setName(deps, name) {
1113
1285
  if (!name || !/^[\p{L}\p{N}_.-]{1,32}$/u.test(name))
1114
1286
  throw new UsageError("Usage: patron name <handle> (1 to 32 letters, digits, _ . -)");
1115
1287
  const { config, settings } = await loadSetup(deps);
1288
+ if (config.wallet === "link")
1289
+ throw new UsageError(
1290
+ "Leaderboard names belong to Tempo addresses; Link purchases are not ranked."
1291
+ );
1116
1292
  const issuer = await loadIssuer(settings.home, settings.gatewayUrl, {
1117
1293
  fetch: deps.fetch
1118
1294
  });
1119
- const open = deps.openWallet ?? (async (s, c, i, max) => (await import("./wallet-WNPT6R4M.js")).openWallet(s, c.wallet, i, max));
1295
+ const open = deps.openWallet ?? (async (s, c, i, max) => (await import("./wallet-R3UWBXVC.js")).openWallet(s, c.wallet, i, max));
1120
1296
  const wallet = await open(settings, config, issuer, 0n);
1121
1297
  await setProfile(settings.gatewayUrl, wallet, name, deps.fetch);
1122
1298
  await writeConfig(settings.home, { ...config, name });
@@ -1147,7 +1323,7 @@ async function uninstall(deps) {
1147
1323
  }
1148
1324
  }
1149
1325
  if (purge) {
1150
- await rm3(deps.settings.home, { force: true, recursive: true });
1326
+ await rm5(deps.settings.home, { force: true, recursive: true });
1151
1327
  done(deps, "Local data deleted.");
1152
1328
  } else
1153
1329
  say(
@@ -3,10 +3,12 @@ const require = __patronCreateRequire(import.meta.url)
3
3
 
4
4
  // src/refill-process.ts
5
5
  import { spawn } from "node:child_process";
6
+ import { createRequire } from "node:module";
6
7
  function startRefill(script = process.argv[1]) {
7
8
  try {
8
9
  if (!script) return;
9
- const child = spawn(process.execPath, [...process.execArgv, script, "refill"], {
10
+ const [command, args] = refillCommand(script);
11
+ const child = spawn(command, args, {
10
12
  detached: true,
11
13
  env: process.env,
12
14
  stdio: "ignore"
@@ -16,6 +18,17 @@ function startRefill(script = process.argv[1]) {
16
18
  } catch {
17
19
  }
18
20
  }
21
+ function refillCommand(script) {
22
+ try {
23
+ createRequire(script).resolve("mppx");
24
+ return [process.execPath, [...process.execArgv, script, "refill"]];
25
+ } catch {
26
+ return [
27
+ process.platform === "win32" ? "npx.cmd" : "npx",
28
+ ["-y", "@tempoxyz/patron", "refill"]
29
+ ];
30
+ }
31
+ }
19
32
  function startPayment(payload, script = process.argv[1]) {
20
33
  return new Promise((resolve) => {
21
34
  try {
@@ -15,7 +15,7 @@ import {
15
15
  statePaths,
16
16
  withLock,
17
17
  writePrivateFile
18
- } from "./chunk-RDEPJ6XD.js";
18
+ } from "./chunk-UWIBR62N.js";
19
19
  import {
20
20
  __commonJS,
21
21
  __export,
@@ -34839,6 +34839,7 @@ async function redeem(options) {
34839
34839
  }
34840
34840
 
34841
34841
  // src/budget.ts
34842
+ var LINK_TOKEN_STEP = 10;
34842
34843
  var REFILL_THRESHOLD = 200;
34843
34844
  var STARTER_TOKENS = 100;
34844
34845
  function budgetTokens(monthlyBudgetUsd) {
@@ -34848,7 +34849,10 @@ function budgetTokens(monthlyBudgetUsd) {
34848
34849
  }
34849
34850
  function purchasableTokens(config2, usage, wanted) {
34850
34851
  const left = budgetTokens(config2.monthlyBudgetUsd) - usage.boughtTokens;
34851
- return Math.max(0, Math.min(Math.floor(wanted), left, MAX_TOKENS_PER_PURCHASE));
34852
+ const step = config2.wallet === "link" ? LINK_TOKEN_STEP : 1;
34853
+ const asked = Math.ceil(Math.floor(wanted) / step) * step;
34854
+ const count = Math.max(0, Math.min(asked, left, MAX_TOKENS_PER_PURCHASE));
34855
+ return count - count % step;
34852
34856
  }
34853
34857
  function refillSize(config2, usage) {
34854
34858
  return purchasableTokens(config2, usage, PACK_SIZE);
@@ -35210,6 +35214,7 @@ export {
35210
35214
  profileMessage,
35211
35215
  buyTokens,
35212
35216
  redeem,
35217
+ LINK_TOKEN_STEP,
35213
35218
  STARTER_TOKENS,
35214
35219
  budgetTokens,
35215
35220
  purchasableTokens,
@@ -68,6 +68,7 @@ function statePaths(home) {
68
68
  config: join(home, "config.json"),
69
69
  issuer: join(home, "issuer.json"),
70
70
  key: join(home, "key"),
71
+ linkRefill: join(home, "link-refill.json"),
71
72
  refillLock: join(home, "refill.lock"),
72
73
  stateLock: join(home, "state.lock"),
73
74
  tokens: join(home, "tokens.enc"),
@@ -139,7 +140,8 @@ async function readConfig(home) {
139
140
  paused: config.paused === true,
140
141
  relayUrl: String(config.relayUrl ?? ""),
141
142
  version: 1,
142
- wallet: config.wallet === "key" ? "key" : "tempo",
143
+ wallet: config.wallet === "key" || config.wallet === "link" ? config.wallet : "tempo",
144
+ ...typeof config.linkPaymentMethodId === "string" ? { linkPaymentMethodId: config.linkPaymentMethodId } : {},
143
145
  ...typeof config.name === "string" ? { name: config.name } : {},
144
146
  ...typeof config.tokensPerPage === "number" ? { tokensPerPage: config.tokensPerPage } : {}
145
147
  };
@@ -15,7 +15,7 @@ import {
15
15
  needsRefill,
16
16
  redeem,
17
17
  refillSize
18
- } from "./chunk-UFBFF3R6.js";
18
+ } from "./chunk-QKBTYUXG.js";
19
19
  import {
20
20
  clampTokensPerPage,
21
21
  settingsFor
@@ -26,7 +26,7 @@ import {
26
26
  updateUsage,
27
27
  withLock,
28
28
  writePrivateFile
29
- } from "./chunk-RDEPJ6XD.js";
29
+ } from "./chunk-UWIBR62N.js";
30
30
  import "./chunk-GTMLTZRN.js";
31
31
 
32
32
  // src/hook.ts
@@ -10,7 +10,7 @@ import {
10
10
  import {
11
11
  startPayment,
12
12
  startRefill
13
- } from "./chunk-CV2WYKM7.js";
13
+ } from "./chunk-HEQBEBKL.js";
14
14
  import {
15
15
  resolveSettings
16
16
  } from "./chunk-CF4LJZLC.js";
@@ -25,7 +25,7 @@ async function runHookProcess(argument = process.argv[3]) {
25
25
  if (isCursorPayload(payload)) process.stdout.write("{}\n");
26
26
  if (!turnEnd(payload) && hookDomains(payload).length === 0) return;
27
27
  if (await startPayment(raw)) return;
28
- const { runHook } = await import("./hook-IWRRBFKG.js");
28
+ const { runHook } = await import("./hook-QEFJTAKR.js");
29
29
  await runHook({
30
30
  readInput: async () => raw,
31
31
  settings: resolveSettings(),
@@ -33,7 +33,7 @@ async function runHookProcess(argument = process.argv[3]) {
33
33
  });
34
34
  }
35
35
  async function runPayProcess() {
36
- const { PAY_TIMEOUT_MS, runHook } = await import("./hook-IWRRBFKG.js");
36
+ const { PAY_TIMEOUT_MS, runHook } = await import("./hook-QEFJTAKR.js");
37
37
  setTimeout(() => process.exit(0), PAY_TIMEOUT_MS + 1e3).unref();
38
38
  const raw = await readStdin(HOOK_TIMEOUT_MS);
39
39
  await runHook(
@@ -0,0 +1,226 @@
1
+ import { createRequire as __patronCreateRequire } from 'node:module'
2
+ const require = __patronCreateRequire(import.meta.url)
3
+ import "./chunk-GTMLTZRN.js";
4
+
5
+ // src/link.ts
6
+ import { execFile } from "node:child_process";
7
+ import { Credential, Method } from "mppx";
8
+ import { Mppx } from "mppx/client";
9
+ import { Methods as StripeMethods } from "mppx/stripe";
10
+ function linkCliPath(env = process.env) {
11
+ return env.PATRON_LINK_CLI || "link-cli";
12
+ }
13
+ var LINK_CLI_TIMEOUT_MS = 3e4;
14
+ function quoteForCmd(arg) {
15
+ return `"${arg.replace(/"/g, '""').replace(/%/g, '"%"')}"`;
16
+ }
17
+ function linkCli(path = linkCliPath(), platform = process.platform) {
18
+ const windows = platform === "win32";
19
+ return (args, timeoutMs = LINK_CLI_TIMEOUT_MS) => new Promise((resolve, reject) => {
20
+ const argv = [...args, "--format", "json"];
21
+ execFile(
22
+ windows ? quoteForCmd(path) : path,
23
+ windows ? argv.map(quoteForCmd) : argv,
24
+ {
25
+ maxBuffer: 1024 * 1024,
26
+ timeout: timeoutMs,
27
+ ...windows ? { shell: true, windowsVerbatimArguments: true } : {}
28
+ },
29
+ (error, stdout, stderr) => {
30
+ const parsed = parseLastJson(stdout);
31
+ if (error) {
32
+ const code = error.code;
33
+ if (code === "ENOENT")
34
+ return reject(
35
+ new Error(
36
+ "Link CLI not found. Install it with npm i -g @stripe/link-cli, then run link-cli auth login."
37
+ )
38
+ );
39
+ const message = errorMessage(parsed) ?? (stderr.trim() || error.message);
40
+ return reject(new Error(`Link CLI: ${message}`));
41
+ }
42
+ if (parsed === void 0)
43
+ return reject(new Error("Link CLI returned no JSON result."));
44
+ resolve(parsed);
45
+ }
46
+ );
47
+ });
48
+ }
49
+ async function linkPaymentMethods(cli) {
50
+ const result = await cli(["payment-methods", "list"]);
51
+ const list = Array.isArray(result) ? result : result?.payment_details ?? [];
52
+ if (!Array.isArray(list)) throw new Error("Link CLI listed no payment methods.");
53
+ return list.filter(
54
+ (item) => typeof item === "object" && item !== null && typeof item.id === "string"
55
+ );
56
+ }
57
+ function linkBalance(methods) {
58
+ return methods.find(
59
+ (method) => method.balance_details?.available_balance !== void 0 || /balance/i.test(method.type)
60
+ );
61
+ }
62
+ var WAITING = /* @__PURE__ */ new Set(["created", "pending_approval", "requires_action"]);
63
+ var PURCHASE_TIMEOUT_MS = 4 * 60 * 1e3;
64
+ async function createLinkSpt(options) {
65
+ const sleep = options.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
66
+ const usd = (options.amount / 100).toFixed(2);
67
+ const deadline = Date.now() + (options.timeoutMs ?? PURCHASE_TIMEOUT_MS);
68
+ const call = (args) => {
69
+ const left = deadline - Date.now();
70
+ if (left <= 0) throw new Error("Link did not approve the purchase in time.");
71
+ return options.cli(args, Math.min(LINK_CLI_TIMEOUT_MS, left));
72
+ };
73
+ const retrieve = async (id) => asSpendRequest(
74
+ await call(["spend-request", "retrieve", id, "--include", "shared_payment_token"])
75
+ );
76
+ const created = asSpendRequest(
77
+ await call([
78
+ "spend-request",
79
+ "create",
80
+ "--credential-type",
81
+ "shared_payment_token",
82
+ "--network-id",
83
+ options.networkId,
84
+ "--amount",
85
+ String(options.amount),
86
+ "--currency",
87
+ "usd",
88
+ "--context",
89
+ `Patron: buying $${usd} of seals (1,000 per dollar) that pay the websites your AI agents and browser read, $0.001 per page, privately. Started by npx @tempoxyz/patron on this computer.`,
90
+ "--idempotency-key",
91
+ options.idempotencyKey,
92
+ "--request-approval",
93
+ ...options.paymentMethodId ? ["--payment-method-id", options.paymentMethodId] : []
94
+ ])
95
+ );
96
+ let request = created;
97
+ try {
98
+ if (WAITING.has(request.status)) {
99
+ const action = request.status_details?.requires_action?.next_action;
100
+ const url = action?.action_url ?? request.approval_url;
101
+ options.output.write(
102
+ `
103
+ Approve $${usd} for Patron in the Link app${url ? `:
104
+ ${url}` : "."}
105
+
106
+ `
107
+ );
108
+ while (WAITING.has(request.status)) {
109
+ assertResumable(request);
110
+ if (Date.now() > deadline)
111
+ throw new Error(`Link spend request ${request.id} was not approved in time.`);
112
+ await sleep(options.pollMs ?? 2e3);
113
+ request = await retrieve(request.id);
114
+ }
115
+ }
116
+ if (request.status !== "approved")
117
+ throw new Error(`Link spend request ${request.id} ended ${request.status}.`);
118
+ } catch (error) {
119
+ if (WAITING.has(request.status))
120
+ await options.cli(["spend-request", "cancel", request.id], 5e3).catch(() => void 0);
121
+ throw error;
122
+ }
123
+ for (let attempt = 0; attempt < 8; attempt++) {
124
+ const spt = request.shared_payment_token?.id;
125
+ if (spt) return spt;
126
+ await sleep(attempt === 0 ? 0 : 1e3);
127
+ request = await retrieve(request.id);
128
+ }
129
+ throw new Error(
130
+ `Link approved spend request ${request.id} but issued no payment token.`
131
+ );
132
+ }
133
+ function assertResumable(request) {
134
+ if (request.status !== "requires_action") return;
135
+ const action = request.status_details?.requires_action?.next_action;
136
+ if (action?.resolution === "auto_resume") return;
137
+ throw new Error(
138
+ `Link needs you to act before it can pay${action?.display_message ? `: ${action.display_message}` : "."}${action?.action_url ? ` ${action.action_url}` : ""} Patron will ask again on the next purchase.`
139
+ );
140
+ }
141
+ async function paymentMethodFor(cli, preferred, cents) {
142
+ if (!preferred) return void 0;
143
+ const methods = await linkPaymentMethods(cli).catch(() => void 0);
144
+ if (!methods) return preferred;
145
+ const method = methods.find((item) => item.id === preferred);
146
+ if (!method) return void 0;
147
+ const available = method.balance_details?.available_balance;
148
+ if (available && (available.currency.toLowerCase() !== "usd" || available.amount < cents))
149
+ return void 0;
150
+ return preferred;
151
+ }
152
+ function asSpendRequest(value) {
153
+ const request = Array.isArray(value) ? value.at(-1) : value;
154
+ if (!request || typeof request.id !== "string" || typeof request.status !== "string")
155
+ throw new Error("Link CLI returned an unexpected spend request.");
156
+ return request;
157
+ }
158
+ function linkFetch(options) {
159
+ const charge = Method.toClient(StripeMethods.charge, {
160
+ async createCredential({ challenge }) {
161
+ const { amount, currency, methodDetails } = challenge.request;
162
+ const cents = Number(amount);
163
+ if (currency.toLowerCase() !== "usd")
164
+ throw new Error(`Patron asked for ${currency}; Link pays only USD.`);
165
+ if (!Number.isSafeInteger(cents) || cents < 1 || cents > options.maxCents)
166
+ throw new Error(
167
+ `Patron asked for ${amount} cents, more than this purchase allows.`
168
+ );
169
+ if (!methodDetails.networkId) throw new Error("Patron sent no Stripe network ID.");
170
+ const paymentMethodId = await paymentMethodFor(
171
+ options.cli,
172
+ options.paymentMethodId,
173
+ cents
174
+ );
175
+ const spt = await createLinkSpt({
176
+ amount: cents,
177
+ cli: options.cli,
178
+ idempotencyKey: `patron-${challenge.id}`,
179
+ networkId: methodDetails.networkId,
180
+ output: options.output,
181
+ ...options.sleep ? { sleep: options.sleep } : {},
182
+ ...paymentMethodId ? { paymentMethodId } : {}
183
+ });
184
+ return Credential.serialize({ challenge, payload: { spt } });
185
+ }
186
+ });
187
+ return Mppx.create({
188
+ ...options.fetch ? { fetch: options.fetch } : {},
189
+ methods: [charge],
190
+ polyfill: false
191
+ }).fetch;
192
+ }
193
+ function parseLastJson(output) {
194
+ const text = output.trim();
195
+ if (!text) return void 0;
196
+ try {
197
+ return JSON.parse(text);
198
+ } catch {
199
+ const lines = text.split("\n").filter((line) => line.trim());
200
+ for (let i = lines.length - 1; i >= 0; i--) {
201
+ try {
202
+ return JSON.parse(lines.slice(i).join("\n"));
203
+ } catch {
204
+ }
205
+ }
206
+ return void 0;
207
+ }
208
+ }
209
+ function errorMessage(value) {
210
+ if (!value || typeof value !== "object") return void 0;
211
+ const record = value;
212
+ if (typeof record.message === "string") return record.message;
213
+ if (record.error && typeof record.error === "object") return errorMessage(record.error);
214
+ if (typeof record.error === "string") return record.error;
215
+ return void 0;
216
+ }
217
+ export {
218
+ createLinkSpt,
219
+ linkBalance,
220
+ linkCli,
221
+ linkCliPath,
222
+ linkFetch,
223
+ linkPaymentMethods,
224
+ paymentMethodFor,
225
+ quoteForCmd
226
+ };
@@ -2,7 +2,7 @@ import { createRequire as __patronCreateRequire } from 'node:module'
2
2
  const require = __patronCreateRequire(import.meta.url)
3
3
  import {
4
4
  statePaths
5
- } from "./chunk-RDEPJ6XD.js";
5
+ } from "./chunk-UWIBR62N.js";
6
6
  import "./chunk-GTMLTZRN.js";
7
7
 
8
8
  // src/wallet.ts
@@ -118,6 +118,22 @@ async function openWallet(settings, wallet, issuer, maxUnits, output = process.s
118
118
  signMessage: (message) => provider.request({ method: "personal_sign", params: [toHex(message), address] })
119
119
  };
120
120
  }
121
+ var UNITS_PER_CENT = 10000n;
122
+ async function openLinkWallet(config, maxUnits, output = process.stderr) {
123
+ const { linkCli, linkFetch } = await import("./link-OU7W7IT6.js");
124
+ return {
125
+ address: "0x0000000000000000000000000000000000000000",
126
+ fetch: linkFetch({
127
+ cli: linkCli(),
128
+ maxCents: Number(maxUnits / UNITS_PER_CENT),
129
+ output,
130
+ ...config.linkPaymentMethodId ? { paymentMethodId: config.linkPaymentMethodId } : {}
131
+ }),
132
+ signMessage: async () => {
133
+ throw new Error("A Link wallet has no Tempo address to sign with.");
134
+ }
135
+ };
136
+ }
121
137
  function budgetLimit(budgetUnits, currency) {
122
138
  return {
123
139
  limit: toHex(budgetUnits),
@@ -151,6 +167,7 @@ ${url}
151
167
  }
152
168
  export {
153
169
  authorizeBudget,
170
+ openLinkWallet,
154
171
  openWallet,
155
172
  signIn
156
173
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tempoxyz/patron",
3
- "version": "0.0.20",
3
+ "version": "0.0.22",
4
4
  "description": "Pay the websites your AI agents read, privately",
5
5
  "type": "module",
6
6
  "bin": {