@tempoxyz/patron 0.0.28 → 0.0.29

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-IMQMGXBT.js");
7
+ const { runHookProcess } = await import("./chunks/hook-process-6WSLYXC7.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-IMQMGXBT.js");
11
+ const { runPayProcess } = await import("./chunks/hook-process-6WSLYXC7.js");
12
12
  await runPayProcess().catch(() => void 0);
13
13
  process.exit(0);
14
14
  } else {
15
- const { run } = await import("./chunks/-5Q7V7W7W.js");
15
+ const { run } = await import("./chunks/-OLVAZG3H.js");
16
16
  process.exitCode = await run(process.argv.slice(2));
17
17
  }
@@ -1,12 +1,14 @@
1
1
  import { createRequire as __patronCreateRequire } from 'node:module'
2
2
  const require = __patronCreateRequire(import.meta.url)
3
3
  import {
4
+ CLAUDE_HOOK_STATUS,
4
5
  FileTokenStore,
5
6
  LINK_TOKEN_STEP,
6
7
  STARTER_TOKENS,
7
8
  budgetTokens,
8
9
  buyTokens,
9
10
  codexHome,
11
+ codexNotifyCommand,
10
12
  codexPaths,
11
13
  hasPatronHandler,
12
14
  installCodex,
@@ -15,6 +17,7 @@ import {
15
17
  loadIssuer,
16
18
  needsRefill,
17
19
  parseUsd,
20
+ patronCommands,
18
21
  purchasableTokens,
19
22
  readJsonObject,
20
23
  redeem,
@@ -26,10 +29,10 @@ import {
26
29
  withPatronStatus,
27
30
  withoutPatronHandlers,
28
31
  writeJsonObject
29
- } from "./chunk-EKFENOVR.js";
32
+ } from "./chunk-SAEZ54SS.js";
30
33
  import {
31
34
  startRefill
32
- } from "./chunk-TV5VLFTD.js";
35
+ } from "./chunk-3CYJJJR3.js";
33
36
  import {
34
37
  resolveSettings,
35
38
  settingsFor
@@ -47,7 +50,7 @@ import {
47
50
  updateUsage,
48
51
  writeConfig,
49
52
  writePrivateFile
50
- } from "./chunk-EV7OA6GK.js";
53
+ } from "./chunk-ZOQJ4CZE.js";
51
54
  import {
52
55
  DEFAULT_MONTHLY_BUDGET_USD,
53
56
  MAX_TOKENS_PER_PAGE,
@@ -70,9 +73,9 @@ import { homedir as homedir2 } from "node:os";
70
73
 
71
74
  // src/agents.ts
72
75
  import { constants } from "node:fs";
73
- import { access as access2, realpath as realpath2 } from "node:fs/promises";
76
+ import { access as access2, readFile as readFile4, realpath as realpath2 } from "node:fs/promises";
74
77
  import { homedir } from "node:os";
75
- import { delimiter, join as join5 } from "node:path";
78
+ import { delimiter, join as join5, resolve } from "node:path";
76
79
 
77
80
  // src/amp.ts
78
81
  import { mkdir, readFile, rm } from "node:fs/promises";
@@ -134,13 +137,17 @@ async function installAmpPlugin(path, command) {
134
137
  return "installed";
135
138
  }
136
139
  async function upgradeAmpPlugin(path) {
140
+ const command = await ampPluginCommand(path);
141
+ if (command === void 0) return false;
142
+ return await installAmpPlugin(path, command) === "installed";
143
+ }
144
+ async function ampPluginCommand(path) {
137
145
  const current = await readText(path);
138
- if (current === void 0 || !current.startsWith(AMP_PLUGIN_MARKER)) return false;
146
+ if (current === void 0 || !current.startsWith(AMP_PLUGIN_MARKER)) return void 0;
139
147
  const literal = /^const COMMAND = (".*")$/m.exec(current)?.[1];
140
- if (!literal) return false;
148
+ if (!literal) return void 0;
141
149
  const command = JSON.parse(literal);
142
- if (typeof command !== "string") return false;
143
- return await installAmpPlugin(path, command) === "installed";
150
+ return typeof command === "string" ? command : void 0;
144
151
  }
145
152
  async function removeAmpPlugin(path) {
146
153
  const current = await readText(path);
@@ -161,7 +168,6 @@ async function readText(path) {
161
168
  var CLAUDE_MATCHER = "WebFetch|WebSearch";
162
169
  var HOOK_TIMEOUT_SECONDS = 30;
163
170
  var EVENT = "PostToolUse";
164
- var CLAUDE_HOOK_STATUS = "Patron: pay the site this read";
165
171
  function patronGroup(command) {
166
172
  return {
167
173
  hooks: [
@@ -176,8 +182,8 @@ function patronGroup(command) {
176
182
  matcher: CLAUDE_MATCHER
177
183
  };
178
184
  }
179
- function withPatronHook(settings, command) {
180
- return withPatronGroup(settings, EVENT, patronGroup(command));
185
+ function withPatronHook(settings, command, known) {
186
+ return withPatronGroup(settings, EVENT, patronGroup(command), known);
181
187
  }
182
188
  async function upgradeClaudeHook(path) {
183
189
  const settings = await readJsonObject(path);
@@ -186,8 +192,8 @@ async function upgradeClaudeHook(path) {
186
192
  await writeJsonObject(path, next);
187
193
  return true;
188
194
  }
189
- function withoutPatronHook(settings) {
190
- return withoutPatronHandlers(settings, EVENT);
195
+ function withoutPatronHook(settings, known) {
196
+ return withoutPatronHandlers(settings, EVENT, known);
191
197
  }
192
198
  function hasPatronHook(settings, command) {
193
199
  return hasPatronHandler(settings, EVENT, {
@@ -195,16 +201,17 @@ function hasPatronHook(settings, command) {
195
201
  ...command === void 0 ? {} : { command }
196
202
  });
197
203
  }
198
- async function installClaudeHook(path, command) {
204
+ async function installClaudeHook(path, command, known) {
199
205
  const settings = await readJsonObject(path);
200
- if (hasPatronHook(settings, command)) return "unchanged";
201
- await writeJsonObject(path, withPatronHook(settings, command));
206
+ if (hasPatronHook(settings, command) && patronCommands(settings, EVENT).length === 1)
207
+ return "unchanged";
208
+ await writeJsonObject(path, withPatronHook(settings, command, known));
202
209
  return "installed";
203
210
  }
204
- async function removeClaudeHook(path) {
211
+ async function removeClaudeHook(path, known) {
205
212
  const settings = await readJsonObject(path);
206
- if (!hasPatronHandler(settings, EVENT)) return "missing";
207
- await writeJsonObject(path, withoutPatronHook(settings));
213
+ if (!hasPatronHandler(settings, EVENT, known ? { known } : {})) return "missing";
214
+ await writeJsonObject(path, withoutPatronHook(settings, known));
208
215
  return "removed";
209
216
  }
210
217
 
@@ -216,42 +223,46 @@ var EVENT2 = "postToolUse";
216
223
  function cursorHooksPath(home) {
217
224
  return join2(home, ".cursor", "hooks.json");
218
225
  }
219
- function withCursorHook(config, command) {
220
- const stripped = withoutCursorHook(config);
226
+ function withCursorHook(config, command, known) {
227
+ const stripped = withoutCursorHook(config, known);
221
228
  const next = typeof stripped.version === "number" ? stripped : { version: 1, ...stripped };
222
229
  const hooks = isObject(next.hooks) ? next.hooks : {};
223
230
  const entries = Array.isArray(hooks[EVENT2]) ? hooks[EVENT2] : [];
224
231
  return withHooksKey(next, EVENT2, [...entries, cursorEntry(command)]);
225
232
  }
226
- function withoutCursorHook(config) {
233
+ function withoutCursorHook(config, known) {
227
234
  const entries = isObject(config.hooks) ? config.hooks[EVENT2] : void 0;
228
- if (!Array.isArray(entries) || !entries.some(isPatronHandler)) return { ...config };
235
+ const patron = (entry) => isPatronHandler(entry, known);
236
+ if (!Array.isArray(entries) || !entries.some(patron)) return { ...config };
229
237
  return withHooksKey(
230
238
  config,
231
239
  EVENT2,
232
- entries.filter((entry) => !isPatronHandler(entry))
240
+ entries.filter((entry) => !patron(entry))
233
241
  );
234
242
  }
235
243
  function cursorEntry(command) {
236
244
  return { command, matcher: CURSOR_MATCHER, timeout: HOOK_TIMEOUT_SECONDS2 };
237
245
  }
238
- function hasCursorHook(config, command) {
246
+ function hasCursorHook(config, command, known) {
239
247
  const entries = isObject(config.hooks) ? config.hooks[EVENT2] : void 0;
240
248
  if (!Array.isArray(entries)) return false;
241
- if (command === void 0) return entries.some(isPatronHandler);
249
+ if (command === void 0) return entries.some((entry) => isPatronHandler(entry, known));
242
250
  const wanted = JSON.stringify(cursorEntry(command));
243
- return entries.some((entry) => JSON.stringify(entry) === wanted);
251
+ const patron = entries.filter(
252
+ (entry) => isPatronHandler(entry, known) || JSON.stringify(entry) === wanted
253
+ );
254
+ return patron.length === 1 && JSON.stringify(patron[0]) === wanted;
244
255
  }
245
- async function installCursorHook(path, command) {
256
+ async function installCursorHook(path, command, known) {
246
257
  const config = await readJsonObject(path);
247
- if (hasCursorHook(config, command)) return "unchanged";
248
- await writeJsonObject(path, withCursorHook(config, command));
258
+ if (hasCursorHook(config, command, known)) return "unchanged";
259
+ await writeJsonObject(path, withCursorHook(config, command, known));
249
260
  return "installed";
250
261
  }
251
- async function removeCursorHook(path) {
262
+ async function removeCursorHook(path, known) {
252
263
  const config = await readJsonObject(path);
253
- if (!hasCursorHook(config)) return "missing";
254
- await writeJsonObject(path, withoutCursorHook(config));
264
+ if (!hasCursorHook(config, void 0, known)) return "missing";
265
+ await writeJsonObject(path, withoutCursorHook(config, known));
255
266
  return "removed";
256
267
  }
257
268
 
@@ -408,32 +419,33 @@ var ADAPTERS = {
408
419
  install: async (command, { env, home }) => ({
409
420
  state: await installAmpPlugin(ampPluginPath(env, home), command)
410
421
  }),
422
+ // The plugin file starts with Patron's marker, so it needs no known commands.
411
423
  remove: ({ env, home }) => removeAmpPlugin(ampPluginPath(env, home)),
412
424
  upgrade: ({ env, home }) => upgradeAmpPlugin(ampPluginPath(env, home))
413
425
  },
414
426
  "claude-code": {
415
427
  files: (environment) => [claudeSettingsPath(environment)],
416
428
  detect: async (environment) => await exists(claudeDirectory(environment)) || await onPath(["claude"], environment.env.PATH ?? ""),
417
- install: async (command, environment) => ({
418
- state: await installClaudeHook(claudeSettingsPath(environment), command)
429
+ install: async (command, environment, known) => ({
430
+ state: await installClaudeHook(claudeSettingsPath(environment), command, known)
419
431
  }),
420
- remove: (environment) => removeClaudeHook(claudeSettingsPath(environment)),
432
+ remove: (environment, known) => removeClaudeHook(claudeSettingsPath(environment), known),
421
433
  upgrade: (environment) => upgradeClaudeHook(claudeSettingsPath(environment))
422
434
  },
423
435
  codex: {
424
436
  files: ({ env, home }) => Object.values(codexPaths(env, home)),
425
437
  detect: async ({ env, home }) => await exists(codexHome(env, home)) || await onPath(["codex"], env.PATH ?? ""),
426
- install: (command, { env, home }) => installCodex(codexPaths(env, home), command),
427
- remove: ({ env, home }) => removeCodex(codexPaths(env, home)),
438
+ install: (command, { env, home }, known) => installCodex(codexPaths(env, home), command, process.platform, known),
439
+ remove: ({ env, home }, known) => removeCodex(codexPaths(env, home), known),
428
440
  upgrade: ({ env, home }) => upgradeCodex(codexPaths(env, home))
429
441
  },
430
442
  cursor: {
431
443
  files: ({ home }) => [cursorHooksPath(home)],
432
444
  detect: async ({ env, home }) => await exists(join5(home, ".cursor")) || await onPath(["cursor-agent", "cursor"], env.PATH ?? ""),
433
- install: async (command, { home }) => ({
434
- state: await installCursorHook(cursorHooksPath(home), command)
445
+ install: async (command, { home }, known) => ({
446
+ state: await installCursorHook(cursorHooksPath(home), command, known)
435
447
  }),
436
- remove: ({ home }) => removeCursorHook(cursorHooksPath(home))
448
+ remove: ({ home }, known) => removeCursorHook(cursorHooksPath(home), known)
437
449
  }
438
450
  };
439
451
  function claudeDirectory({ env, home }) {
@@ -452,12 +464,13 @@ async function detectAgents(environment) {
452
464
  }
453
465
  async function installAgents(agents, command, environment) {
454
466
  const store = originalsStore(environment);
467
+ const known = await knownCommands(environment, [command]);
455
468
  return Promise.all(
456
469
  agents.map(async (agent) => {
457
470
  let recorded = [];
458
471
  try {
459
472
  recorded = await rememberOriginals(store, ADAPTERS[agent].files(environment));
460
- const { note, state } = await ADAPTERS[agent].install(command, environment);
473
+ const { note, state } = await ADAPTERS[agent].install(command, environment, known);
461
474
  if (state !== "installed") await forgetOriginals(store, recorded);
462
475
  return { agent, label: AGENT_LABELS[agent], state, ...note ? { note } : {} };
463
476
  } catch (error) {
@@ -477,10 +490,17 @@ async function upgradeAgents(agents, environment) {
477
490
  }
478
491
  async function removeAgents(environment) {
479
492
  const store = originalsStore(environment);
493
+ const patronHome = environment.env.PATRON_HOME || join5(environment.home, ".patron");
494
+ const stable = `${quote(process.execPath)} ${quote(stableBin(patronHome))} hook`;
495
+ const known = await knownCommands(environment, [
496
+ await hookCommand(environment.env, process.argv[1], process.execPath, patronHome),
497
+ stable,
498
+ withPatronHome(stable, environment.env.PATRON_HOME)
499
+ ]);
480
500
  return Promise.all(
481
501
  AGENT_IDS.map(async (agent) => {
482
502
  try {
483
- const state = await ADAPTERS[agent].remove(environment);
503
+ const state = await ADAPTERS[agent].remove(environment, known);
484
504
  if (state === "removed")
485
505
  await restoreOriginals(store, ADAPTERS[agent].files(environment)).catch(
486
506
  () => void 0
@@ -492,6 +512,32 @@ async function removeAgents(environment) {
492
512
  })
493
513
  );
494
514
  }
515
+ async function knownCommands({ env, home }, extra) {
516
+ const paths = codexPaths(env, home);
517
+ const found = await Promise.all([
518
+ readJsonObject(claudeSettingsPath({ env, home })).then(
519
+ (settings) => patronCommands(settings, "PostToolUse"),
520
+ () => []
521
+ ),
522
+ readJsonObject(paths.hooks).then(
523
+ (settings) => patronCommands(settings, "Stop"),
524
+ () => []
525
+ ),
526
+ readFile4(paths.config, "utf8").then(
527
+ (toml) => [codexNotifyCommand(toml)],
528
+ () => []
529
+ ),
530
+ ampPluginCommand(ampPluginPath(env, home)).then(
531
+ (command) => [command],
532
+ () => []
533
+ )
534
+ ]);
535
+ return new Set(
536
+ [...found.flat(), ...extra].filter(
537
+ (command) => typeof command === "string" && command !== ""
538
+ )
539
+ );
540
+ }
495
541
  function originalsStore({ env, home }) {
496
542
  return originalsPath(env.PATRON_HOME || join5(home, ".patron"));
497
543
  }
@@ -505,6 +551,9 @@ function failure(agent, error) {
505
551
  }
506
552
  async function hookCommand(env = process.env, script = process.argv[1], node = process.execPath, patronHome = env.PATRON_HOME || join5(homedir(), ".patron")) {
507
553
  if (env.PATRON_HOOK_COMMAND) return env.PATRON_HOOK_COMMAND;
554
+ return withPatronHome(await runCommand(script, node, patronHome), env.PATRON_HOME);
555
+ }
556
+ async function runCommand(script, node, patronHome) {
508
557
  const resolved = script ? await realpath2(script).catch(() => void 0) : void 0;
509
558
  if (resolved && /[/\\]bin\.js$/.test(resolved) && !/[/\\]_npx[/\\]/.test(resolved))
510
559
  return `${quote(node)} ${quote(resolved)} hook`;
@@ -512,6 +561,9 @@ async function hookCommand(env = process.env, script = process.argv[1], node = p
512
561
  return `${quote(node)} ${quote(stableBin(patronHome))} hook`;
513
562
  return "npx -y @tempoxyz/patron hook";
514
563
  }
564
+ function withPatronHome(command, patronHome) {
565
+ return patronHome ? `PATRON_HOME=${quote(resolve(patronHome))} ${command}` : command;
566
+ }
515
567
  function quote(value) {
516
568
  return /^[\w./:@-]+$/.test(value) ? value : `'${value.replaceAll("'", "'\\''")}'`;
517
569
  }
@@ -697,16 +749,24 @@ async function linkRefillFailedAt(home) {
697
749
  const value = await readJson(statePaths(home).linkRefill).catch(() => void 0);
698
750
  return typeof value?.failedAt === "number" ? value.failedAt : void 0;
699
751
  }
700
- var REFILL_LOCK_STALE_MS = 5 * 60 * 1e3;
701
752
  var defaultOpenWallet = async (settings, config, issuer, maxUnits) => {
702
753
  if (config.wallet === "link") {
703
- const { openLinkWallet } = await import("./wallet-6TDMXRWW.js");
754
+ const { openLinkWallet } = await import("./wallet-LOLL66ID.js");
704
755
  return openLinkWallet(config, maxUnits);
705
756
  }
706
- const { openWallet } = await import("./wallet-6TDMXRWW.js");
757
+ const { openWallet } = await import("./wallet-LOLL66ID.js");
707
758
  return openWallet(settings, config.wallet, issuer, maxUnits);
708
759
  };
760
+ var REFILL_LOCK_STALE_MS = 5 * 60 * 1e3;
709
761
  async function buyWithinBudget(settings, config, wanted, openWallet = defaultOpenWallet) {
762
+ await ensureHome(settings.home);
763
+ return withLock(
764
+ statePaths(settings.home).purchaseLock,
765
+ () => buyLocked(settings, config, wanted, openWallet),
766
+ { staleMs: REFILL_LOCK_STALE_MS, timeoutMs: REFILL_LOCK_STALE_MS }
767
+ );
768
+ }
769
+ async function buyLocked(settings, config, wanted, openWallet) {
710
770
  const usage = await readUsage(settings.home);
711
771
  const count = purchasableTokens(config, usage, wanted);
712
772
  if (count === 0) return 0;
@@ -729,10 +789,28 @@ async function buyWithinBudget(settings, config, wanted, openWallet = defaultOpe
729
789
  });
730
790
  return tokens.length;
731
791
  }
792
+ var REFILL_DELAY_MS = { max: 10 * 60 * 1e3, min: 60 * 1e3 };
793
+ async function delayedRefill(base, openWallet = defaultOpenWallet, sleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms)), random = Math.random) {
794
+ if (!await readConfig(base.home)) return 0;
795
+ const release = await tryLock(
796
+ statePaths(base.home).refillWait,
797
+ REFILL_DELAY_MS.max + REFILL_LOCK_STALE_MS
798
+ );
799
+ if (!release) return 0;
800
+ try {
801
+ await sleep(
802
+ REFILL_DELAY_MS.min + random() * (REFILL_DELAY_MS.max - REFILL_DELAY_MS.min)
803
+ );
804
+ } finally {
805
+ await release();
806
+ }
807
+ return refill(base, openWallet);
808
+ }
732
809
  async function refill(base, openWallet = defaultOpenWallet, now = Date.now) {
733
810
  const config = await readConfig(base.home);
734
811
  if (!config || config.paused) return 0;
735
812
  const settings = settingsFor(base, config);
813
+ await loadIssuer(settings.home, settings.gatewayUrl).catch(() => void 0);
736
814
  const release = await tryLock(
737
815
  statePaths(settings.home).refillLock,
738
816
  REFILL_LOCK_STALE_MS
@@ -846,6 +924,7 @@ PATRON_PRIVATE_KEY (testnet automation), PATRON_BUDGET_USD (non-interactive setu
846
924
  PATRON_WALLET=link (pay with Link through link-cli), PATRON_LINK_CLI (its path).`;
847
925
  var UsageError = class extends Error {
848
926
  };
927
+ var TIP_BATCH = 8;
849
928
  function defaults() {
850
929
  const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
851
930
  const color = supportsColor(process.stdout);
@@ -915,7 +994,7 @@ async function run(argv, overrides = {}) {
915
994
  await uninstall(deps);
916
995
  return 0;
917
996
  case "refill":
918
- await refill(deps.settings, deps.openWallet).catch(() => 0);
997
+ await (args[0] === "--delay" ? delayedRefill(deps.settings, deps.openWallet) : refill(deps.settings, deps.openWallet)).catch(() => 0);
919
998
  return 0;
920
999
  case "help":
921
1000
  case "--help":
@@ -1084,7 +1163,7 @@ async function tempoSignIn(deps, budgetUnits, issuer) {
1084
1163
  deps,
1085
1164
  deps.settings.privateKey ? "Using PATRON_PRIVATE_KEY" : "Signing in with Tempo Wallet"
1086
1165
  );
1087
- const { signIn } = await import("./wallet-6TDMXRWW.js");
1166
+ const { signIn } = await import("./wallet-LOLL66ID.js");
1088
1167
  const account = await signIn(deps.settings, {
1089
1168
  budgetUnits,
1090
1169
  issuer,
@@ -1124,8 +1203,8 @@ async function askBudget(deps, fallback) {
1124
1203
  const preset = process.env.PATRON_BUDGET_USD;
1125
1204
  if (!deps.interactive) {
1126
1205
  const value = preset ? parseUsd(preset) : fallback;
1127
- if (value === void 0 || value <= 0)
1128
- throw new UsageError("PATRON_BUDGET_USD must be a positive amount.");
1206
+ if (value === void 0 || value < 1e-3)
1207
+ throw new UsageError("PATRON_BUDGET_USD must be at least 0.001.");
1129
1208
  return value;
1130
1209
  }
1131
1210
  const prompter = deps.prompter();
@@ -1198,7 +1277,7 @@ async function changeBudget(deps, input) {
1198
1277
  } else budget = await askBudget(deps, config.monthlyBudgetUsd);
1199
1278
  if (config.wallet === "tempo" && budget >= config.monthlyBudgetUsd) {
1200
1279
  step(deps, "Approve the new spending limit in Tempo Wallet");
1201
- const { authorizeBudget } = await import("./wallet-6TDMXRWW.js");
1280
+ const { authorizeBudget } = await import("./wallet-LOLL66ID.js");
1202
1281
  const issuer = await loadIssuer(settings.home, settings.gatewayUrl, {
1203
1282
  fetch: deps.fetch
1204
1283
  });
@@ -1259,37 +1338,71 @@ async function tip(deps, url, amount = "0.10") {
1259
1338
  const issuer = await loadIssuer(settings.home, settings.gatewayUrl, {
1260
1339
  fetch: deps.fetch
1261
1340
  });
1262
- const tokens = await store.takeMany(count);
1341
+ let stopped = false;
1342
+ const stop = () => {
1343
+ stopped = true;
1344
+ process.off("SIGINT", stop);
1345
+ process.off("SIGTERM", stop);
1346
+ };
1347
+ process.once("SIGINT", stop);
1348
+ process.once("SIGTERM", stop);
1263
1349
  let paid = 0;
1350
+ let sent = 0;
1264
1351
  let rejection;
1265
- const unsent = [];
1266
- for (let index = 0; index < tokens.length; index += 8) {
1267
- const batch = tokens.slice(index, index + 8);
1268
- const results = await Promise.all(
1269
- batch.map(
1270
- (token) => redeem({
1271
- domain,
1272
- fetch: deps.fetch,
1273
- issuer,
1274
- relayUrl: settings.relayUrl,
1275
- token
1276
- }).catch(() => {
1277
- unsent.push(token);
1278
- return void 0;
1279
- })
1280
- )
1281
- );
1282
- for (const result of results) {
1283
- if (result?.ok) paid += 1;
1284
- else if (result) rejection = result.error ?? "rejected";
1352
+ try {
1353
+ while (sent < count && !stopped) {
1354
+ const batch = await store.takeMany(Math.min(TIP_BATCH, count - sent));
1355
+ if (batch.length === 0) break;
1356
+ sent += batch.length;
1357
+ const unsent = [];
1358
+ let rejected = 0;
1359
+ const results = await Promise.all(
1360
+ batch.map(
1361
+ (token) => redeem({
1362
+ domain,
1363
+ fetch: deps.fetch,
1364
+ issuer,
1365
+ relayUrl: settings.relayUrl,
1366
+ token
1367
+ }).catch(() => {
1368
+ unsent.push(token);
1369
+ return void 0;
1370
+ })
1371
+ )
1372
+ );
1373
+ const ok = results.filter((result) => result?.ok).length;
1374
+ for (const result of results)
1375
+ if (result && !result.ok) {
1376
+ rejected += 1;
1377
+ rejection = result.error ?? "rejected";
1378
+ }
1379
+ sent -= unsent.length;
1380
+ await store.add(unsent);
1381
+ if (ok > 0)
1382
+ await updateUsage(settings.home, (usage) => {
1383
+ usage.domains[domain] = (usage.domains[domain] ?? 0) + ok;
1384
+ usage.tippedTokens += ok;
1385
+ });
1386
+ paid += ok;
1387
+ if (count > TIP_BATCH && ok > 0)
1388
+ say(
1389
+ deps,
1390
+ styleText(
1391
+ ` ${tokensUsd(paid)} of ${tokensUsd(count)} sent`,
1392
+ "dim",
1393
+ deps.color
1394
+ )
1395
+ );
1396
+ if (ok === 0 && (rejected > 0 || unsent.length > 0)) break;
1285
1397
  }
1398
+ } finally {
1399
+ process.off("SIGINT", stop);
1400
+ process.off("SIGTERM", stop);
1286
1401
  }
1287
- await store.add(unsent);
1288
- if (paid > 0)
1289
- await updateUsage(settings.home, (usage) => {
1290
- usage.domains[domain] = (usage.domains[domain] ?? 0) + paid;
1291
- usage.tippedTokens += paid;
1292
- });
1402
+ if (stopped && sent < count)
1403
+ throw new Error(
1404
+ `Tip stopped: tipped ${domain} ${tokensUsd(paid)} of ${tokensUsd(count)}; the rest stays in your balance.`
1405
+ );
1293
1406
  if (paid === 0)
1294
1407
  throw new Error(
1295
1408
  rejection ? `Patron rejected the tip to ${domain}: ${rejection}.` : `Could not reach Patron to tip ${domain}; your balance was kept.`
@@ -1316,7 +1429,7 @@ async function setName(deps, name) {
1316
1429
  const issuer = await loadIssuer(settings.home, settings.gatewayUrl, {
1317
1430
  fetch: deps.fetch
1318
1431
  });
1319
- const open = deps.openWallet ?? (async (s, c, i, max) => (await import("./wallet-6TDMXRWW.js")).openWallet(s, c.wallet, i, max));
1432
+ const open = deps.openWallet ?? (async (s, c, i, max) => (await import("./wallet-LOLL66ID.js")).openWallet(s, c.wallet, i, max));
1320
1433
  const wallet = await open(settings, config, issuer, 0n);
1321
1434
  await setProfile(settings.gatewayUrl, wallet, name, deps.fetch);
1322
1435
  await writeConfig(settings.home, { ...config, name });
@@ -1324,9 +1437,11 @@ async function setName(deps, name) {
1324
1437
  }
1325
1438
  async function uninstall(deps) {
1326
1439
  const config = await readConfig(deps.settings.home);
1327
- const results = (await removeAgents(deps.agents)).filter(
1328
- (result) => result.state !== "missing"
1329
- );
1440
+ const removed = await removeAgents(deps.agents);
1441
+ const results = removed.filter((result) => result.state !== "missing");
1442
+ const notFound = removed.filter(
1443
+ (result) => result.state === "missing" && config?.agents.includes(result.agent)
1444
+ ).map((result) => result.label);
1330
1445
  say(deps, styleText("Patron uninstall", "bold", deps.color));
1331
1446
  for (const line of formatAgentResults(results, deps.color)) say(deps, line);
1332
1447
  if (results.length === 0)
@@ -1358,6 +1473,10 @@ async function uninstall(deps) {
1358
1473
  deps.color
1359
1474
  )
1360
1475
  );
1476
+ if (notFound.length > 0)
1477
+ throw new Error(
1478
+ `Found no Patron hook to remove for ${notFound.join(", ")}. If a hook running "patron hook" is still in its settings, delete it by hand.`
1479
+ );
1361
1480
  }
1362
1481
  function readVersion() {
1363
1482
  for (const path of ["../package.json", "../../package.json"]) {
@@ -4,10 +4,11 @@ const require = __patronCreateRequire(import.meta.url)
4
4
  // src/refill-process.ts
5
5
  import { spawn } from "node:child_process";
6
6
  import { createRequire } from "node:module";
7
- function startRefill(script = process.argv[1]) {
7
+ function startRefill(options = {}, script = process.argv[1]) {
8
8
  try {
9
9
  if (!script) return;
10
- const [command, args] = refillCommand(script);
10
+ const [command, refillArgs] = refillCommand(script);
11
+ const args = options.delayed ? [...refillArgs, "--delay"] : refillArgs;
11
12
  const child = spawn(command, args, {
12
13
  detached: true,
13
14
  env: process.env,
@@ -8,7 +8,7 @@ import {
8
8
  readJson,
9
9
  statePaths,
10
10
  writePrivateFile
11
- } from "./chunk-EV7OA6GK.js";
11
+ } from "./chunk-ZOQJ4CZE.js";
12
12
  import {
13
13
  ISSUER_NAME,
14
14
  MAX_TOKENS_PER_PURCHASE,
@@ -14990,7 +14990,7 @@ async function redeem(options) {
14990
14990
 
14991
14991
  // src/budget.ts
14992
14992
  var LINK_TOKEN_STEP = 10;
14993
- var REFILL_THRESHOLD = 200;
14993
+ var REFILL_THRESHOLD = 400;
14994
14994
  var STARTER_TOKENS = 100;
14995
14995
  function budgetTokens(monthlyBudgetUsd) {
14996
14996
  if (!Number.isFinite(monthlyBudgetUsd) || monthlyBudgetUsd <= 0) return 0;
@@ -15030,11 +15030,28 @@ import { dirname } from "node:path";
15030
15030
  function isObject(value) {
15031
15031
  return typeof value === "object" && value !== null && !Array.isArray(value);
15032
15032
  }
15033
- function isPatronHandler(handler) {
15033
+ var CLAUDE_HOOK_STATUS = "Patron: pay the site this read";
15034
+ var CODEX_HOOK_STATUS = "Patron: pay the sites this turn read";
15035
+ var PATRON_STATUSES = /* @__PURE__ */ new Set([
15036
+ CLAUDE_HOOK_STATUS,
15037
+ CODEX_HOOK_STATUS
15038
+ ]);
15039
+ var NO_COMMANDS = /* @__PURE__ */ new Set();
15040
+ function isPatronHandler(handler, known = NO_COMMANDS) {
15034
15041
  if (!isObject(handler) || typeof handler.command !== "string") return false;
15042
+ if (typeof handler.statusMessage === "string" && PATRON_STATUSES.has(handler.statusMessage))
15043
+ return true;
15044
+ if (known.has(handler.command)) return true;
15035
15045
  return /patron/i.test(handler.command) && /\shook\s*$/.test(handler.command);
15036
15046
  }
15037
- function withoutPatronHandlers(settings, event) {
15047
+ function patronCommands(settings, event) {
15048
+ const groups = isObject(settings.hooks) ? settings.hooks[event] : void 0;
15049
+ if (!Array.isArray(groups)) return [];
15050
+ return groups.flatMap(
15051
+ (group) => isObject(group) && Array.isArray(group.hooks) ? group.hooks.filter((handler) => isPatronHandler(handler)).map((handler) => handler.command) : []
15052
+ );
15053
+ }
15054
+ function withoutPatronHandlers(settings, event, known = NO_COMMANDS) {
15038
15055
  const groups = isObject(settings.hooks) ? settings.hooks[event] : void 0;
15039
15056
  if (!isObject(settings.hooks) || !Array.isArray(groups)) return { ...settings };
15040
15057
  let changed = false;
@@ -15044,7 +15061,7 @@ function withoutPatronHandlers(settings, event) {
15044
15061
  kept.push(group);
15045
15062
  continue;
15046
15063
  }
15047
- const handlers = group.hooks.filter((handler) => !isPatronHandler(handler));
15064
+ const handlers = group.hooks.filter((handler) => !isPatronHandler(handler, known));
15048
15065
  if (handlers.length === group.hooks.length) kept.push(group);
15049
15066
  else {
15050
15067
  changed = true;
@@ -15054,8 +15071,8 @@ function withoutPatronHandlers(settings, event) {
15054
15071
  if (!changed) return { ...settings };
15055
15072
  return withHooksKey(settings, event, kept);
15056
15073
  }
15057
- function withPatronGroup(settings, event, group) {
15058
- const next = withoutPatronHandlers(settings, event);
15074
+ function withPatronGroup(settings, event, group, known = NO_COMMANDS) {
15075
+ const next = withoutPatronHandlers(settings, event, known);
15059
15076
  const hooks = isObject(next.hooks) ? next.hooks : {};
15060
15077
  const groups = Array.isArray(hooks[event]) ? hooks[event] : [];
15061
15078
  return withHooksKey(next, event, [...groups, group]);
@@ -15065,7 +15082,7 @@ function hasPatronHandler(settings, event, options = {}) {
15065
15082
  if (!Array.isArray(groups)) return false;
15066
15083
  return groups.some(
15067
15084
  (group) => isObject(group) && (options.matcher === void 0 || group.matcher === options.matcher) && Array.isArray(group.hooks) && group.hooks.some(
15068
- (handler) => isPatronHandler(handler) && (options.command === void 0 || handler.command === options.command)
15085
+ (handler) => isPatronHandler(handler, options.known) && (options.command === void 0 || handler.command === options.command)
15069
15086
  )
15070
15087
  );
15071
15088
  }
@@ -15131,10 +15148,9 @@ function codexPaths(env, home) {
15131
15148
  const root = codexHome(env, home);
15132
15149
  return { config: join(root, "config.toml"), hooks: join(root, "hooks.json") };
15133
15150
  }
15134
- function withCodexStopHook(settings, command) {
15135
- return withPatronGroup(settings, EVENT, stopGroup(command));
15151
+ function withCodexStopHook(settings, command, known) {
15152
+ return withPatronGroup(settings, EVENT, stopGroup(command), known);
15136
15153
  }
15137
- var CODEX_HOOK_STATUS = "Patron: pay the sites this turn read";
15138
15154
  function stopGroup(command) {
15139
15155
  return {
15140
15156
  hooks: [
@@ -15150,7 +15166,7 @@ function stopGroup(command) {
15150
15166
  function hasStopGroup(settings, command) {
15151
15167
  const groups = isObject(settings.hooks) ? settings.hooks[EVENT] : void 0;
15152
15168
  const wanted = JSON.stringify(stopGroup(command));
15153
- return Array.isArray(groups) && groups.some((group) => JSON.stringify(group) === wanted);
15169
+ return Array.isArray(groups) && groups.some((group) => JSON.stringify(group) === wanted) && patronCommands(settings, EVENT).length === 1;
15154
15170
  }
15155
15171
  function codexNotifyLine(command) {
15156
15172
  const argv = ["sh", "-c", `${command} "$1"`, "patron"];
@@ -15174,6 +15190,21 @@ function withCodexNotify(toml, command) {
15174
15190
  text: lines.map((value, at) => at === index ? line : value).join("\n")
15175
15191
  };
15176
15192
  }
15193
+ function codexNotifyCommand(toml) {
15194
+ const lines = toml.split("\n");
15195
+ const line = lines[topLevelNotify(lines)];
15196
+ if (!line?.includes(CODEX_NOTIFY_MARKER)) return void 0;
15197
+ const array = /=\s*(\[.*\])/.exec(
15198
+ line.slice(0, line.lastIndexOf(CODEX_NOTIFY_MARKER))
15199
+ )?.[1];
15200
+ try {
15201
+ const argv = JSON.parse(array ?? "");
15202
+ const script = Array.isArray(argv) ? argv[2] : void 0;
15203
+ return typeof script === "string" && script.endsWith(' "$1"') ? script.slice(0, -' "$1"'.length) : void 0;
15204
+ } catch {
15205
+ return void 0;
15206
+ }
15207
+ }
15177
15208
  function withoutCodexNotify(toml) {
15178
15209
  const lines = toml.split("\n");
15179
15210
  const index = topLevelNotify(lines);
@@ -15187,11 +15218,11 @@ function topLevelNotify(lines) {
15187
15218
  }
15188
15219
  return -1;
15189
15220
  }
15190
- async function installCodex(paths, command, platform = process.platform) {
15221
+ async function installCodex(paths, command, platform = process.platform, known) {
15191
15222
  const settings = await readJsonObject(paths.hooks);
15192
15223
  const hookChanged = !hasStopGroup(settings, command);
15193
15224
  if (hookChanged)
15194
- await writeJsonObject(paths.hooks, withCodexStopHook(settings, command));
15225
+ await writeJsonObject(paths.hooks, withCodexStopHook(settings, command, known));
15195
15226
  let notify = "taken";
15196
15227
  if (platform !== "win32") {
15197
15228
  const toml = await readText(paths.config);
@@ -15209,11 +15240,11 @@ async function upgradeCodex(paths) {
15209
15240
  await writeJsonObject(paths.hooks, next);
15210
15241
  return true;
15211
15242
  }
15212
- async function removeCodex(paths) {
15243
+ async function removeCodex(paths, known) {
15213
15244
  let removed = false;
15214
15245
  const settings = await readJsonObject(paths.hooks);
15215
- if (hasPatronHandler(settings, EVENT)) {
15216
- await writeJsonObject(paths.hooks, withoutPatronHandlers(settings, EVENT));
15246
+ if (hasPatronHandler(settings, EVENT, known ? { known } : {})) {
15247
+ await writeJsonObject(paths.hooks, withoutPatronHandlers(settings, EVENT, known));
15217
15248
  removed = true;
15218
15249
  }
15219
15250
  const toml = await readText(paths.config);
@@ -15239,14 +15270,19 @@ async function writeText(path, text) {
15239
15270
 
15240
15271
  // src/issuer.ts
15241
15272
  var ISSUER_TTL_MS = 60 * 60 * 1e3;
15273
+ async function storedIssuer(home, gatewayUrl, now = Date.now()) {
15274
+ const cached = await readJson(statePaths(home).issuer).catch(() => void 0);
15275
+ const parsed = IssuerInfo.safeParse(cached?.issuer);
15276
+ if (!parsed.success || cached?.gatewayUrl !== gatewayUrl) return void 0;
15277
+ const at = typeof cached.at === "number" ? cached.at : Number.NEGATIVE_INFINITY;
15278
+ return { issuer: parsed.data, stale: !(now - at < ISSUER_TTL_MS) };
15279
+ }
15242
15280
  async function loadIssuer(home, gatewayUrl, options = {}) {
15243
15281
  const now = options.now ?? Date.now();
15244
15282
  const path = statePaths(home).issuer;
15245
15283
  if (!options.fresh) {
15246
- const cached = await readJson(path).catch(() => void 0);
15247
- const parsed = IssuerInfo.safeParse(cached?.issuer);
15248
- if (parsed.success && cached?.gatewayUrl === gatewayUrl && typeof cached.at === "number" && now - cached.at < ISSUER_TTL_MS)
15249
- return parsed.data;
15284
+ const cached = await storedIssuer(home, gatewayUrl, now);
15285
+ if (cached && !cached.stale) return cached.issuer;
15250
15286
  }
15251
15287
  const issuer = await getIssuer(gatewayUrl, options.fetch);
15252
15288
  await ensureHome(home);
@@ -15370,7 +15406,9 @@ export {
15370
15406
  parseUsd,
15371
15407
  tipTokens,
15372
15408
  isObject,
15409
+ CLAUDE_HOOK_STATUS,
15373
15410
  isPatronHandler,
15411
+ patronCommands,
15374
15412
  withoutPatronHandlers,
15375
15413
  withPatronGroup,
15376
15414
  hasPatronHandler,
@@ -15380,9 +15418,11 @@ export {
15380
15418
  withPatronStatus,
15381
15419
  codexHome,
15382
15420
  codexPaths,
15421
+ codexNotifyCommand,
15383
15422
  installCodex,
15384
15423
  upgradeCodex,
15385
15424
  removeCodex,
15425
+ storedIssuer,
15386
15426
  loadIssuer,
15387
15427
  FileTokenStore
15388
15428
  };
@@ -19881,7 +19881,9 @@ function statePaths(home) {
19881
19881
  issuer: join(home, "issuer.json"),
19882
19882
  key: join(home, "key"),
19883
19883
  linkRefill: join(home, "link-refill.json"),
19884
+ purchaseLock: join(home, "purchase.lock"),
19884
19885
  refillLock: join(home, "refill.lock"),
19886
+ refillWait: join(home, "refill-wait.lock"),
19885
19887
  stateLock: join(home, "state.lock"),
19886
19888
  tokens: join(home, "tokens.enc"),
19887
19889
  usage: join(home, "usage.json"),
@@ -12,11 +12,11 @@ import {
12
12
  import {
13
13
  FileTokenStore,
14
14
  codexHome,
15
- loadIssuer,
16
15
  needsRefill,
17
16
  redeem,
18
- refillSize
19
- } from "./chunk-EKFENOVR.js";
17
+ refillSize,
18
+ storedIssuer
19
+ } from "./chunk-SAEZ54SS.js";
20
20
  import {
21
21
  settingsFor
22
22
  } from "./chunk-OQXACACU.js";
@@ -25,7 +25,7 @@ import {
25
25
  readUsage,
26
26
  updateUsage,
27
27
  writePrivateFile
28
- } from "./chunk-EV7OA6GK.js";
28
+ } from "./chunk-ZOQJ4CZE.js";
29
29
  import {
30
30
  clampTokensPerPage,
31
31
  withLock
@@ -250,11 +250,15 @@ async function payReads(dependencies, deadline) {
250
250
  }
251
251
  if (config.paused) return { paid: [], refill: false, skipped: "paused" };
252
252
  if (domains.length === 0) return { paid: [], refill: false, skipped: "empty" };
253
+ const now = dependencies.now?.() ?? /* @__PURE__ */ new Date();
254
+ const stored = await storedIssuer(settings.home, settings.gatewayUrl, now.getTime());
255
+ if (!stored) {
256
+ dependencies.startRefill();
257
+ return { paid: [], refill: true, skipped: "no-issuer" };
258
+ }
259
+ const { issuer } = stored;
253
260
  const store = new FileTokenStore(settings.home);
254
261
  const boundedFetch = withDeadline(dependencies.fetch ?? fetch, deadline);
255
- const issuer = await loadIssuer(settings.home, settings.gatewayUrl, {
256
- fetch: boundedFetch
257
- });
258
262
  const perPage = clampTokensPerPage(config.tokensPerPage);
259
263
  const taken = await store.takeWithRemaining(domains.length * perPage, domains.length);
260
264
  const { empty, paid, unspent } = await redeemRounds(
@@ -269,7 +273,6 @@ async function payReads(dependencies, deadline) {
269
273
  }
270
274
  );
271
275
  await store.add(unspent);
272
- const now = dependencies.now?.() ?? /* @__PURE__ */ new Date();
273
276
  if (paid.length > 0)
274
277
  await updateUsage(
275
278
  settings.home,
@@ -280,7 +283,7 @@ async function payReads(dependencies, deadline) {
280
283
  },
281
284
  now
282
285
  );
283
- const refill = needsRefill(taken.remaining + unspent.length) && refillSize(config, await readUsage(settings.home, now)) > 0;
286
+ const refill = stored.stale || needsRefill(taken.remaining + unspent.length) && refillSize(config, await readUsage(settings.home, now)) > 0;
284
287
  if (refill) dependencies.startRefill();
285
288
  return {
286
289
  paid,
@@ -11,7 +11,7 @@ import {
11
11
  import {
12
12
  startDrain,
13
13
  startRefill
14
- } from "./chunk-TV5VLFTD.js";
14
+ } from "./chunk-3CYJJJR3.js";
15
15
  import {
16
16
  resolveSettings
17
17
  } from "./chunk-OQXACACU.js";
@@ -178,7 +178,7 @@ async function runHookProcess(argument = process.argv[3]) {
178
178
  await payEvent(
179
179
  event,
180
180
  settings,
181
- () => startRefill(),
181
+ () => startRefill({ delayed: true }),
182
182
  started + HOOK_TIMEOUT_MS - Date.now()
183
183
  );
184
184
  }
@@ -191,7 +191,7 @@ async function runPayProcess() {
191
191
  const refill = () => {
192
192
  if (Date.now() - refilledAt < REFILL_RETRY_MS) return;
193
193
  refilledAt = Date.now();
194
- startRefill();
194
+ startRefill({ delayed: true });
195
195
  };
196
196
  await drainQueue(settings.home, {
197
197
  deadline: Date.now() + DRAIN_MAX_MS,
@@ -202,7 +202,7 @@ async function runPayProcess() {
202
202
  }
203
203
  async function payEvent(event, settings, refill, timeoutMs) {
204
204
  if (timeoutMs <= 0) return;
205
- const { runHook } = await import("./hook-N2SVXVUR.js");
205
+ const { runHook } = await import("./hook-HYV4BSCN.js");
206
206
  await runHook(
207
207
  {
208
208
  readInput: async () => event.raw,
@@ -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-EV7OA6GK.js";
5
+ } from "./chunk-ZOQJ4CZE.js";
6
6
  import {
7
7
  accessKeyLimitUnits
8
8
  } from "./chunk-EISHX7Q3.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tempoxyz/patron",
3
- "version": "0.0.28",
3
+ "version": "0.0.29",
4
4
  "description": "Pay the websites your AI agents read, privately",
5
5
  "type": "module",
6
6
  "bin": {