@tempoxyz/patron 0.0.21 → 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/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-FNSWFVFP.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-FNSWFVFP.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/-NGLRKGBT.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,7 +29,7 @@ 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
35
  } from "./chunk-HEQBEBKL.js";
@@ -57,12 +58,12 @@ 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 rm4 } from "node:fs/promises";
66
+ import { rm as rm5 } from "node:fs/promises";
66
67
  import { homedir as homedir2 } from "node:os";
67
68
 
68
69
  // src/agents.ts
@@ -680,9 +681,19 @@ ${styleText(title, "bold", color)}
680
681
  }
681
682
 
682
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
+ }
683
690
  var REFILL_LOCK_STALE_MS = 5 * 60 * 1e3;
684
691
  var defaultOpenWallet = async (settings, config, issuer, maxUnits) => {
685
- 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");
686
697
  return openWallet(settings, config.wallet, issuer, maxUnits);
687
698
  };
688
699
  async function buyWithinBudget(settings, config, wanted, openWallet = defaultOpenWallet) {
@@ -708,7 +719,7 @@ async function buyWithinBudget(settings, config, wanted, openWallet = defaultOpe
708
719
  });
709
720
  return tokens.length;
710
721
  }
711
- async function refill(base, openWallet = defaultOpenWallet) {
722
+ async function refill(base, openWallet = defaultOpenWallet, now = Date.now) {
712
723
  const config = await readConfig(base.home);
713
724
  if (!config || config.paused) return 0;
714
725
  const settings = settingsFor(base, config);
@@ -719,7 +730,19 @@ async function refill(base, openWallet = defaultOpenWallet) {
719
730
  if (!release) return 0;
720
731
  try {
721
732
  if (!needsRefill(await new FileTokenStore(settings.home).count())) return 0;
722
- 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
+ }
723
746
  } finally {
724
747
  await release();
725
748
  }
@@ -772,6 +795,18 @@ function formatStatus(view, color) {
772
795
  color
773
796
  )
774
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
+ );
775
810
  if (top.length > 0) lines.push(row("Top sites", top.join(" \xB7 "), color));
776
811
  return lines.join("\n");
777
812
  }
@@ -800,7 +835,8 @@ Usage: patron [command]
800
835
  hook Run by agents after each web read (reads the event JSON)
801
836
 
802
837
  Environment: PATRON_HOME, PATRON_GATEWAY_URL, PATRON_RELAY_URL, PATRON_NETWORK,
803
- 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).`;
804
840
  var UsageError = class extends Error {
805
841
  };
806
842
  function defaults() {
@@ -957,14 +993,12 @@ async function setup(deps) {
957
993
  });
958
994
  const budget = await askBudget(deps, DEFAULT_MONTHLY_BUDGET_USD);
959
995
  const budgetUnits = BigInt(budgetTokens(budget)) * TOKEN_PRICE_UNITS;
960
- step(
961
- deps,
962
- settings.privateKey ? "Using PATRON_PRIVATE_KEY" : "Signing in with Tempo Wallet"
963
- );
964
- const { signIn } = await import("./wallet-WNPT6R4M.js");
965
- const account = await signIn(settings, { budgetUnits, issuer, output: deps.stdout });
966
- 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);
967
1000
  const config = {
1001
+ ...account.linkPaymentMethodId ? { linkPaymentMethodId: account.linkPaymentMethodId } : {},
968
1002
  address: account.address,
969
1003
  agents: [],
970
1004
  gatewayUrl: settings.gatewayUrl,
@@ -976,9 +1010,10 @@ async function setup(deps) {
976
1010
  wallet: account.wallet
977
1011
  };
978
1012
  await writeConfig(settings.home, config);
979
- 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);
980
1015
  step(deps, `Buying ${first.toLocaleString("en-US")} seals (${tokensUsd(first)})`);
981
- const bought = await buyWithinBudget(settings, config, STARTER_TOKENS, deps.openWallet);
1016
+ const bought = await buyWithinBudget(settings, config, starter, deps.openWallet);
982
1017
  done(deps, `${bought.toLocaleString("en-US")} seals ready`);
983
1018
  deps.startRefill();
984
1019
  await installStableCli(settings.home).catch(() => void 0);
@@ -1005,6 +1040,64 @@ async function setup(deps) {
1005
1040
  styleText("\u2713 Patron on. Agents now pay the sites they read.", "green", deps.color)
1006
1041
  );
1007
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
+ }
1008
1101
  async function askBudget(deps, fallback) {
1009
1102
  const preset = process.env.PATRON_BUDGET_USD;
1010
1103
  if (!deps.interactive) {
@@ -1027,14 +1120,21 @@ async function askBudget(deps, fallback) {
1027
1120
  }
1028
1121
  async function status(deps, menu) {
1029
1122
  const { config, settings } = await loadSetup(deps);
1030
- const [tokensLeft, usage, standing] = await Promise.all([
1123
+ const [tokensLeft, usage, standing, refillFailedAt] = await Promise.all([
1031
1124
  new FileTokenStore(settings.home).count(),
1032
1125
  readUsage(settings.home),
1033
- fetchStanding(settings.gatewayUrl, config.address, config.name, deps.fetch).catch(
1126
+ config.address ? fetchStanding(settings.gatewayUrl, config.address, config.name, deps.fetch).catch(
1034
1127
  () => void 0
1035
- )
1128
+ ) : void 0,
1129
+ config.wallet === "link" ? linkRefillFailedAt(settings.home) : void 0
1036
1130
  ]);
1037
- 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
+ );
1038
1138
  if (!menu) return;
1039
1139
  const prompter = deps.prompter();
1040
1140
  let choice;
@@ -1076,7 +1176,7 @@ async function changeBudget(deps, input) {
1076
1176
  } else budget = await askBudget(deps, config.monthlyBudgetUsd);
1077
1177
  if (config.wallet === "tempo" && budget > config.monthlyBudgetUsd) {
1078
1178
  step(deps, "Approve the new spending limit in Tempo Wallet");
1079
- const { authorizeBudget } = await import("./wallet-WNPT6R4M.js");
1179
+ const { authorizeBudget } = await import("./wallet-R3UWBXVC.js");
1080
1180
  const issuer = await loadIssuer(settings.home, settings.gatewayUrl, {
1081
1181
  fetch: deps.fetch
1082
1182
  });
@@ -1185,10 +1285,14 @@ async function setName(deps, name) {
1185
1285
  if (!name || !/^[\p{L}\p{N}_.-]{1,32}$/u.test(name))
1186
1286
  throw new UsageError("Usage: patron name <handle> (1 to 32 letters, digits, _ . -)");
1187
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
+ );
1188
1292
  const issuer = await loadIssuer(settings.home, settings.gatewayUrl, {
1189
1293
  fetch: deps.fetch
1190
1294
  });
1191
- 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));
1192
1296
  const wallet = await open(settings, config, issuer, 0n);
1193
1297
  await setProfile(settings.gatewayUrl, wallet, name, deps.fetch);
1194
1298
  await writeConfig(settings.home, { ...config, name });
@@ -1219,7 +1323,7 @@ async function uninstall(deps) {
1219
1323
  }
1220
1324
  }
1221
1325
  if (purge) {
1222
- await rm4(deps.settings.home, { force: true, recursive: true });
1326
+ await rm5(deps.settings.home, { force: true, recursive: true });
1223
1327
  done(deps, "Local data deleted.");
1224
1328
  } else
1225
1329
  say(
@@ -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
@@ -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.21",
3
+ "version": "0.0.22",
4
4
  "description": "Pay the websites your AI agents read, privately",
5
5
  "type": "module",
6
6
  "bin": {