@tempoxyz/patron 0.0.27 → 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-QQPBFRY6.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-QQPBFRY6.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/-PBBSDRMM.js");
15
+ const { run } = await import("./chunks/-OLVAZG3H.js");
16
16
  process.exitCode = await run(process.argv.slice(2));
17
17
  }
@@ -1,14 +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
- LeaderboardResponse,
7
- PatronInfo,
8
7
  STARTER_TOKENS,
9
8
  budgetTokens,
10
9
  buyTokens,
11
10
  codexHome,
11
+ codexNotifyCommand,
12
12
  codexPaths,
13
13
  hasPatronHandler,
14
14
  installCodex,
@@ -17,7 +17,7 @@ import {
17
17
  loadIssuer,
18
18
  needsRefill,
19
19
  parseUsd,
20
- profileMessage,
20
+ patronCommands,
21
21
  purchasableTokens,
22
22
  readJsonObject,
23
23
  redeem,
@@ -29,26 +29,20 @@ import {
29
29
  withPatronStatus,
30
30
  withoutPatronHandlers,
31
31
  writeJsonObject
32
- } from "./chunk-F5CY5O7O.js";
32
+ } from "./chunk-SAEZ54SS.js";
33
33
  import {
34
34
  startRefill
35
- } from "./chunk-TV5VLFTD.js";
35
+ } from "./chunk-3CYJJJR3.js";
36
36
  import {
37
- DEFAULT_MONTHLY_BUDGET_USD,
38
- MAX_TOKENS_PER_PAGE,
39
- MIN_TOKENS_PER_PAGE,
40
- PACK_SIZE,
41
- TOKEN_PRICE_UNITS,
42
- clampTokensPerPage,
43
- formatUsd,
44
- normalizePayee,
45
37
  resolveSettings,
46
- settingsFor,
47
- tokensToUnits
48
- } from "./chunk-WZRGCKEK.js";
38
+ settingsFor
39
+ } from "./chunk-OQXACACU.js";
49
40
  import {
50
41
  AGENT_IDS,
42
+ LeaderboardResponse,
43
+ PatronInfo,
51
44
  ensureHome,
45
+ profileMessage,
52
46
  readConfig,
53
47
  readJson,
54
48
  readUsage,
@@ -56,11 +50,20 @@ import {
56
50
  updateUsage,
57
51
  writeConfig,
58
52
  writePrivateFile
59
- } from "./chunk-MZJEM2SE.js";
53
+ } from "./chunk-ZOQJ4CZE.js";
60
54
  import {
55
+ DEFAULT_MONTHLY_BUDGET_USD,
56
+ MAX_TOKENS_PER_PAGE,
57
+ MIN_TOKENS_PER_PAGE,
58
+ PACK_SIZE,
59
+ TOKEN_PRICE_UNITS,
60
+ clampTokensPerPage,
61
+ formatUsd,
62
+ normalizePayee,
63
+ tokensToUnits,
61
64
  tryLock,
62
65
  withLock
63
- } from "./chunk-O64T22EK.js";
66
+ } from "./chunk-EISHX7Q3.js";
64
67
  import "./chunk-GTMLTZRN.js";
65
68
 
66
69
  // src/index.ts
@@ -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-OJP2JREM.js");
754
+ const { openLinkWallet } = await import("./wallet-LOLL66ID.js");
704
755
  return openLinkWallet(config, maxUnits);
705
756
  }
706
- const { openWallet } = await import("./wallet-OJP2JREM.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);
@@ -861,6 +940,19 @@ function defaults() {
861
940
  stdout: process.stdout
862
941
  };
863
942
  }
943
+ var REFRESHES_STABLE_COPY = /* @__PURE__ */ new Set([
944
+ "status",
945
+ "pause",
946
+ "resume",
947
+ "budget",
948
+ "per-page",
949
+ "tip",
950
+ "name"
951
+ ]);
952
+ async function refreshStableCli(deps) {
953
+ const config = await readConfig(deps.settings.home).catch(() => void 0);
954
+ if (config) await installStableCli(deps.settings.home).catch(() => void 0);
955
+ }
864
956
  async function run(argv, overrides = {}) {
865
957
  const base = { ...defaults(), ...overrides };
866
958
  let shared;
@@ -873,6 +965,8 @@ async function run(argv, overrides = {}) {
873
965
  };
874
966
  const [command, ...args] = argv;
875
967
  try {
968
+ if (command !== void 0 && REFRESHES_STABLE_COPY.has(command))
969
+ await refreshStableCli(deps);
876
970
  switch (command) {
877
971
  case void 0:
878
972
  await main(deps);
@@ -900,7 +994,7 @@ async function run(argv, overrides = {}) {
900
994
  await uninstall(deps);
901
995
  return 0;
902
996
  case "refill":
903
- 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);
904
998
  return 0;
905
999
  case "help":
906
1000
  case "--help":
@@ -1069,7 +1163,7 @@ async function tempoSignIn(deps, budgetUnits, issuer) {
1069
1163
  deps,
1070
1164
  deps.settings.privateKey ? "Using PATRON_PRIVATE_KEY" : "Signing in with Tempo Wallet"
1071
1165
  );
1072
- const { signIn } = await import("./wallet-OJP2JREM.js");
1166
+ const { signIn } = await import("./wallet-LOLL66ID.js");
1073
1167
  const account = await signIn(deps.settings, {
1074
1168
  budgetUnits,
1075
1169
  issuer,
@@ -1109,8 +1203,8 @@ async function askBudget(deps, fallback) {
1109
1203
  const preset = process.env.PATRON_BUDGET_USD;
1110
1204
  if (!deps.interactive) {
1111
1205
  const value = preset ? parseUsd(preset) : fallback;
1112
- if (value === void 0 || value <= 0)
1113
- 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.");
1114
1208
  return value;
1115
1209
  }
1116
1210
  const prompter = deps.prompter();
@@ -1181,9 +1275,9 @@ async function changeBudget(deps, input) {
1181
1275
  if (budget === void 0 || budget < 1e-3)
1182
1276
  throw new UsageError("Usage: patron budget <usd>, such as patron budget 10");
1183
1277
  } else budget = await askBudget(deps, config.monthlyBudgetUsd);
1184
- if (config.wallet === "tempo" && budget > config.monthlyBudgetUsd) {
1278
+ if (config.wallet === "tempo" && budget >= config.monthlyBudgetUsd) {
1185
1279
  step(deps, "Approve the new spending limit in Tempo Wallet");
1186
- const { authorizeBudget } = await import("./wallet-OJP2JREM.js");
1280
+ const { authorizeBudget } = await import("./wallet-LOLL66ID.js");
1187
1281
  const issuer = await loadIssuer(settings.home, settings.gatewayUrl, {
1188
1282
  fetch: deps.fetch
1189
1283
  });
@@ -1237,43 +1331,78 @@ async function tip(deps, url, amount = "0.10") {
1237
1331
  throw new Error(
1238
1332
  `This tip needs ${tokensUsd(missing)} more than this month's budget allows. Raise it with patron budget <usd>.`
1239
1333
  );
1240
- step(deps, `Adding ${tokensUsd(missing)} to your balance`);
1241
- await buyWithinBudget(settings, config, missing, deps.openWallet);
1334
+ const amount2 = Math.max(missing, purchasableTokens(config, usage, PACK_SIZE));
1335
+ step(deps, `Adding ${tokensUsd(amount2)} to your balance`);
1336
+ await buyWithinBudget(settings, config, amount2, deps.openWallet);
1242
1337
  }
1243
1338
  const issuer = await loadIssuer(settings.home, settings.gatewayUrl, {
1244
1339
  fetch: deps.fetch
1245
1340
  });
1246
- 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);
1247
1349
  let paid = 0;
1350
+ let sent = 0;
1248
1351
  let rejection;
1249
- const unsent = [];
1250
- for (let index = 0; index < tokens.length; index += 8) {
1251
- const batch = tokens.slice(index, index + 8);
1252
- const results = await Promise.all(
1253
- batch.map(
1254
- (token) => redeem({
1255
- domain,
1256
- fetch: deps.fetch,
1257
- issuer,
1258
- relayUrl: settings.relayUrl,
1259
- token
1260
- }).catch(() => {
1261
- unsent.push(token);
1262
- return void 0;
1263
- })
1264
- )
1265
- );
1266
- for (const result of results) {
1267
- if (result?.ok) paid += 1;
1268
- 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;
1269
1397
  }
1398
+ } finally {
1399
+ process.off("SIGINT", stop);
1400
+ process.off("SIGTERM", stop);
1270
1401
  }
1271
- await store.add(unsent);
1272
- if (paid > 0)
1273
- await updateUsage(settings.home, (usage) => {
1274
- usage.domains[domain] = (usage.domains[domain] ?? 0) + paid;
1275
- usage.tippedTokens += paid;
1276
- });
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
+ );
1277
1406
  if (paid === 0)
1278
1407
  throw new Error(
1279
1408
  rejection ? `Patron rejected the tip to ${domain}: ${rejection}.` : `Could not reach Patron to tip ${domain}; your balance was kept.`
@@ -1300,7 +1429,7 @@ async function setName(deps, name) {
1300
1429
  const issuer = await loadIssuer(settings.home, settings.gatewayUrl, {
1301
1430
  fetch: deps.fetch
1302
1431
  });
1303
- const open = deps.openWallet ?? (async (s, c, i, max) => (await import("./wallet-OJP2JREM.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));
1304
1433
  const wallet = await open(settings, config, issuer, 0n);
1305
1434
  await setProfile(settings.gatewayUrl, wallet, name, deps.fetch);
1306
1435
  await writeConfig(settings.home, { ...config, name });
@@ -1308,9 +1437,11 @@ async function setName(deps, name) {
1308
1437
  }
1309
1438
  async function uninstall(deps) {
1310
1439
  const config = await readConfig(deps.settings.home);
1311
- const results = (await removeAgents(deps.agents)).filter(
1312
- (result) => result.state !== "missing"
1313
- );
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);
1314
1445
  say(deps, styleText("Patron uninstall", "bold", deps.color));
1315
1446
  for (const line of formatAgentResults(results, deps.color)) say(deps, line);
1316
1447
  if (results.length === 0)
@@ -1342,6 +1473,10 @@ async function uninstall(deps) {
1342
1473
  deps.color
1343
1474
  )
1344
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
+ );
1345
1480
  }
1346
1481
  function readVersion() {
1347
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,