@tempoxyz/patron 0.0.0-stage → 0.0.1

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.
@@ -0,0 +1,1105 @@
1
+ import { createRequire as __patronCreateRequire } from 'node:module'
2
+ const require = __patronCreateRequire(import.meta.url)
3
+ import {
4
+ DEFAULT_MONTHLY_BUDGET_USD,
5
+ FileTokenStore,
6
+ LeaderboardResponse,
7
+ MAX_TOKENS_PER_PAGE,
8
+ MIN_TOKENS_PER_PAGE,
9
+ PACK_SIZE,
10
+ PatronInfo,
11
+ STARTER_TOKENS,
12
+ TOKEN_PRICE_UNITS,
13
+ budgetTokens,
14
+ buyTokens,
15
+ clampTokensPerPage,
16
+ codexHome,
17
+ codexPaths,
18
+ formatUsd,
19
+ hasPatronHandler,
20
+ installCodex,
21
+ isObject,
22
+ isPatronHandler,
23
+ loadIssuer,
24
+ needsRefill,
25
+ normalizeDomain,
26
+ parseUsd,
27
+ profileMessage,
28
+ purchasableTokens,
29
+ readJsonObject,
30
+ redeem,
31
+ removeCodex,
32
+ resolveSettings,
33
+ settingsFor,
34
+ startRefill,
35
+ tipTokens,
36
+ tokensToUnits,
37
+ withHooksKey,
38
+ withPatronGroup,
39
+ withoutPatronHandlers,
40
+ writeJsonObject
41
+ } from "./chunk-6Q7AFS33.js";
42
+ import {
43
+ AGENT_IDS,
44
+ ensureHome,
45
+ readConfig,
46
+ readJson,
47
+ readUsage,
48
+ statePaths,
49
+ tryLock,
50
+ updateUsage,
51
+ withLock,
52
+ writeConfig,
53
+ writePrivateFile
54
+ } from "./chunk-NLCZ2IHE.js";
55
+
56
+ // src/index.ts
57
+ import { readFileSync } from "node:fs";
58
+ import { rm as rm3 } from "node:fs/promises";
59
+ import { homedir } from "node:os";
60
+
61
+ // src/agents.ts
62
+ import { constants } from "node:fs";
63
+ import { access, realpath } from "node:fs/promises";
64
+ import { delimiter, join as join4 } from "node:path";
65
+
66
+ // src/amp.ts
67
+ import { mkdir, readFile, rm } from "node:fs/promises";
68
+ import { dirname, join } from "node:path";
69
+ var AMP_PLUGIN_MARKER = "// Patron for Amp. Generated by npx @tempoxyz/patron; npx @tempoxyz/patron uninstall removes it.";
70
+ var AMP_WEB_TOOLS = ["read_web_page", "web_search"];
71
+ function ampPluginPath(env, home) {
72
+ return join(env.XDG_CONFIG_HOME || join(home, ".config"), "amp", "plugins", "patron.ts");
73
+ }
74
+ function ampPluginSource(command) {
75
+ return `${AMP_PLUGIN_MARKER}
76
+ import { spawn } from 'node:child_process'
77
+ import type { PluginAPI } from '@ampcode/plugin'
78
+
79
+ export const description = 'Patron: pays $0.001 to each site Amp reads with read_web_page or web_search, privately.'
80
+
81
+ const COMMAND = ${JSON.stringify(command)}
82
+ const TOOLS = new Set(${JSON.stringify(AMP_WEB_TOOLS)})
83
+
84
+ export default function (amp: PluginAPI) {
85
+ amp.on('tool.result', (event) => {
86
+ if (event.status !== 'done' || !TOOLS.has(event.tool)) return
87
+ try {
88
+ const child = spawn(COMMAND, { detached: true, shell: true, stdio: ['pipe', 'ignore', 'ignore'] })
89
+ child.on('error', () => undefined)
90
+ child.stdin?.on('error', () => undefined)
91
+ child.stdin?.end(
92
+ JSON.stringify({
93
+ hook_event_name: 'tool.result',
94
+ tool_input: event.input,
95
+ tool_name: event.tool,
96
+ tool_response: event.tool === 'web_search' ? event.output : undefined,
97
+ }),
98
+ )
99
+ child.unref()
100
+ } catch {
101
+ // Paying must never affect the agent.
102
+ }
103
+ })
104
+ }
105
+ `;
106
+ }
107
+ async function installAmpPlugin(path, command) {
108
+ const current = await readText(path);
109
+ const source = ampPluginSource(command);
110
+ if (current === source) return "unchanged";
111
+ if (current !== void 0 && !current.startsWith(AMP_PLUGIN_MARKER))
112
+ throw new Error(`${path} exists and is not Patron's; left unchanged.`);
113
+ await mkdir(dirname(path), { recursive: true });
114
+ await writePrivateFile(path, source);
115
+ return "installed";
116
+ }
117
+ async function removeAmpPlugin(path) {
118
+ const current = await readText(path);
119
+ if (current === void 0 || !current.startsWith(AMP_PLUGIN_MARKER)) return "missing";
120
+ await rm(path, { force: true });
121
+ return "removed";
122
+ }
123
+ async function readText(path) {
124
+ try {
125
+ return await readFile(path, "utf8");
126
+ } catch (error) {
127
+ if (error.code === "ENOENT") return void 0;
128
+ throw error;
129
+ }
130
+ }
131
+
132
+ // src/claude-code.ts
133
+ var CLAUDE_MATCHER = "WebFetch|WebSearch";
134
+ var HOOK_TIMEOUT_SECONDS = 30;
135
+ var EVENT = "PostToolUse";
136
+ function withPatronHook(settings, command) {
137
+ return withPatronGroup(settings, EVENT, {
138
+ hooks: [{ async: true, command, timeout: HOOK_TIMEOUT_SECONDS, type: "command" }],
139
+ matcher: CLAUDE_MATCHER
140
+ });
141
+ }
142
+ function withoutPatronHook(settings) {
143
+ return withoutPatronHandlers(settings, EVENT);
144
+ }
145
+ function hasPatronHook(settings, command) {
146
+ return hasPatronHandler(settings, EVENT, {
147
+ matcher: CLAUDE_MATCHER,
148
+ ...command === void 0 ? {} : { command }
149
+ });
150
+ }
151
+ async function installClaudeHook(path, command) {
152
+ const settings = await readJsonObject(path);
153
+ if (hasPatronHook(settings, command)) return "unchanged";
154
+ await writeJsonObject(path, withPatronHook(settings, command));
155
+ return "installed";
156
+ }
157
+ async function removeClaudeHook(path) {
158
+ const settings = await readJsonObject(path);
159
+ if (!hasPatronHandler(settings, EVENT)) return "missing";
160
+ await writeJsonObject(path, withoutPatronHook(settings));
161
+ return "removed";
162
+ }
163
+
164
+ // src/cursor.ts
165
+ import { join as join2 } from "node:path";
166
+ var CURSOR_MATCHER = "WebFetch|WebSearch";
167
+ var HOOK_TIMEOUT_SECONDS2 = 10;
168
+ var EVENT2 = "postToolUse";
169
+ function cursorHooksPath(home) {
170
+ return join2(home, ".cursor", "hooks.json");
171
+ }
172
+ function withCursorHook(config, command) {
173
+ const stripped = withoutCursorHook(config);
174
+ const next = typeof stripped.version === "number" ? stripped : { version: 1, ...stripped };
175
+ const hooks = isObject(next.hooks) ? next.hooks : {};
176
+ const entries = Array.isArray(hooks[EVENT2]) ? hooks[EVENT2] : [];
177
+ return withHooksKey(next, EVENT2, [...entries, cursorEntry(command)]);
178
+ }
179
+ function withoutCursorHook(config) {
180
+ const entries = isObject(config.hooks) ? config.hooks[EVENT2] : void 0;
181
+ if (!Array.isArray(entries) || !entries.some(isPatronHandler)) return { ...config };
182
+ return withHooksKey(
183
+ config,
184
+ EVENT2,
185
+ entries.filter((entry) => !isPatronHandler(entry))
186
+ );
187
+ }
188
+ function cursorEntry(command) {
189
+ return { command, matcher: CURSOR_MATCHER, timeout: HOOK_TIMEOUT_SECONDS2 };
190
+ }
191
+ function hasCursorHook(config, command) {
192
+ const entries = isObject(config.hooks) ? config.hooks[EVENT2] : void 0;
193
+ if (!Array.isArray(entries)) return false;
194
+ if (command === void 0) return entries.some(isPatronHandler);
195
+ const wanted = JSON.stringify(cursorEntry(command));
196
+ return entries.some((entry) => JSON.stringify(entry) === wanted);
197
+ }
198
+ async function installCursorHook(path, command) {
199
+ const config = await readJsonObject(path);
200
+ if (hasCursorHook(config, command)) return "unchanged";
201
+ await writeJsonObject(path, withCursorHook(config, command));
202
+ return "installed";
203
+ }
204
+ async function removeCursorHook(path) {
205
+ const config = await readJsonObject(path);
206
+ if (!hasCursorHook(config)) return "missing";
207
+ await writeJsonObject(path, withoutCursorHook(config));
208
+ return "removed";
209
+ }
210
+
211
+ // src/originals.ts
212
+ import { chmod, readFile as readFile2, rm as rm2, stat, writeFile } from "node:fs/promises";
213
+ import { dirname as dirname2, join as join3 } from "node:path";
214
+ import { isDeepStrictEqual } from "node:util";
215
+ function originalsPath(patronHome) {
216
+ return join3(patronHome, "agent-originals.json");
217
+ }
218
+ async function rememberOriginals(store, paths) {
219
+ return update(store, async (originals) => {
220
+ const added = [];
221
+ for (const path of paths) {
222
+ if (path in originals) continue;
223
+ originals[path] = await snapshot(path);
224
+ added.push(path);
225
+ }
226
+ return added;
227
+ });
228
+ }
229
+ async function forgetOriginals(store, paths) {
230
+ if (paths.length === 0) return;
231
+ await update(store, async (originals) => {
232
+ for (const path of paths) delete originals[path];
233
+ });
234
+ }
235
+ async function restoreOriginals(store, paths) {
236
+ await update(store, async (originals) => {
237
+ for (const path of paths) {
238
+ if (!(path in originals)) continue;
239
+ const original = originals[path] ?? null;
240
+ delete originals[path];
241
+ const current = await snapshot(path);
242
+ if (!sameContent(path, current?.text, original)) continue;
243
+ if (original === null) await rm2(path, { force: true });
244
+ else if (current?.text !== original.text || current.mode !== original.mode) {
245
+ await writeFile(path, original.text);
246
+ await chmod(path, original.mode);
247
+ }
248
+ }
249
+ });
250
+ }
251
+ async function update(store, change) {
252
+ await ensureHome(dirname2(store));
253
+ return withLock(`${store}.lock`, async () => {
254
+ const saved = await readJson(store).catch(() => void 0);
255
+ const originals = saved && typeof saved === "object" && !Array.isArray(saved) ? { ...saved } : {};
256
+ const result = await change(originals);
257
+ await writePrivateFile(store, JSON.stringify(originals));
258
+ return result;
259
+ });
260
+ }
261
+ async function snapshot(path) {
262
+ try {
263
+ const [text, info] = await Promise.all([readFile2(path, "utf8"), stat(path)]);
264
+ return { mode: info.mode & 511, text };
265
+ } catch (error) {
266
+ if (error.code === "ENOENT") return null;
267
+ throw error;
268
+ }
269
+ }
270
+ function sameContent(path, current, original) {
271
+ const before = original?.text ?? "";
272
+ const after = current ?? "";
273
+ if (before === after) return true;
274
+ if (!path.endsWith(".json")) return false;
275
+ const left = parse(after);
276
+ if (original === null)
277
+ return isDeepStrictEqual(left, {}) || isDeepStrictEqual(left, { version: 1 });
278
+ const right = parse(before);
279
+ return left !== void 0 && right !== void 0 && isDeepStrictEqual(left, right);
280
+ }
281
+ function parse(text) {
282
+ if (text.trim() === "") return {};
283
+ try {
284
+ return JSON.parse(text);
285
+ } catch {
286
+ return void 0;
287
+ }
288
+ }
289
+
290
+ // src/agents.ts
291
+ var AGENT_LABELS = {
292
+ amp: "Amp",
293
+ "claude-code": "Claude Code",
294
+ codex: "Codex",
295
+ cursor: "Cursor"
296
+ };
297
+ var ADAPTERS = {
298
+ amp: {
299
+ files: ({ env, home }) => [ampPluginPath(env, home)],
300
+ detect: async ({ env, home }) => await exists(join4(env.XDG_CONFIG_HOME || join4(home, ".config"), "amp")) || await onPath(["amp"], env.PATH ?? ""),
301
+ install: async (command, { env, home }) => ({
302
+ state: await installAmpPlugin(ampPluginPath(env, home), command)
303
+ }),
304
+ remove: ({ env, home }) => removeAmpPlugin(ampPluginPath(env, home))
305
+ },
306
+ "claude-code": {
307
+ files: (environment) => [claudeSettingsPath(environment)],
308
+ detect: async (environment) => await exists(claudeDirectory(environment)) || await onPath(["claude"], environment.env.PATH ?? ""),
309
+ install: async (command, environment) => ({
310
+ state: await installClaudeHook(claudeSettingsPath(environment), command)
311
+ }),
312
+ remove: (environment) => removeClaudeHook(claudeSettingsPath(environment))
313
+ },
314
+ codex: {
315
+ files: ({ env, home }) => Object.values(codexPaths(env, home)),
316
+ detect: async ({ env, home }) => await exists(codexHome(env, home)) || await onPath(["codex"], env.PATH ?? ""),
317
+ install: (command, { env, home }) => installCodex(codexPaths(env, home), command),
318
+ remove: ({ env, home }) => removeCodex(codexPaths(env, home))
319
+ },
320
+ cursor: {
321
+ files: ({ home }) => [cursorHooksPath(home)],
322
+ detect: async ({ env, home }) => await exists(join4(home, ".cursor")) || await onPath(["cursor-agent", "cursor"], env.PATH ?? ""),
323
+ install: async (command, { home }) => ({
324
+ state: await installCursorHook(cursorHooksPath(home), command)
325
+ }),
326
+ remove: ({ home }) => removeCursorHook(cursorHooksPath(home))
327
+ }
328
+ };
329
+ function claudeDirectory({ env, home }) {
330
+ return env.CLAUDE_CONFIG_DIR || join4(home, ".claude");
331
+ }
332
+ function claudeSettingsPath(environment) {
333
+ return join4(claudeDirectory(environment), "settings.json");
334
+ }
335
+ async function detectAgents(environment) {
336
+ const found = await Promise.all(
337
+ AGENT_IDS.map(
338
+ async (agent) => await ADAPTERS[agent].detect(environment) ? agent : void 0
339
+ )
340
+ );
341
+ return found.filter((agent) => agent !== void 0);
342
+ }
343
+ async function installAgents(agents, command, environment) {
344
+ const store = originalsStore(environment);
345
+ return Promise.all(
346
+ agents.map(async (agent) => {
347
+ let recorded = [];
348
+ try {
349
+ recorded = await rememberOriginals(store, ADAPTERS[agent].files(environment));
350
+ const { note, state } = await ADAPTERS[agent].install(command, environment);
351
+ if (state !== "installed") await forgetOriginals(store, recorded);
352
+ return { agent, label: AGENT_LABELS[agent], state, ...note ? { note } : {} };
353
+ } catch (error) {
354
+ await forgetOriginals(store, recorded).catch(() => void 0);
355
+ return failure(agent, error);
356
+ }
357
+ })
358
+ );
359
+ }
360
+ async function removeAgents(environment) {
361
+ const store = originalsStore(environment);
362
+ return Promise.all(
363
+ AGENT_IDS.map(async (agent) => {
364
+ try {
365
+ const state = await ADAPTERS[agent].remove(environment);
366
+ if (state === "removed")
367
+ await restoreOriginals(store, ADAPTERS[agent].files(environment)).catch(
368
+ () => void 0
369
+ );
370
+ return { agent, label: AGENT_LABELS[agent], state };
371
+ } catch (error) {
372
+ return failure(agent, error);
373
+ }
374
+ })
375
+ );
376
+ }
377
+ function originalsStore({ env, home }) {
378
+ return originalsPath(env.PATRON_HOME || join4(home, ".patron"));
379
+ }
380
+ function failure(agent, error) {
381
+ return {
382
+ agent,
383
+ detail: error.message,
384
+ label: AGENT_LABELS[agent],
385
+ state: "failed"
386
+ };
387
+ }
388
+ async function hookCommand(env = process.env, script = process.argv[1], node = process.execPath) {
389
+ if (env.PATRON_HOOK_COMMAND) return env.PATRON_HOOK_COMMAND;
390
+ const resolved = script ? await realpath(script).catch(() => void 0) : void 0;
391
+ if (resolved && /[/\\]bin\.js$/.test(resolved) && !/[/\\]_npx[/\\]/.test(resolved))
392
+ return `${quote(node)} ${quote(resolved)} hook`;
393
+ return "npx -y @tempoxyz/patron hook";
394
+ }
395
+ function quote(value) {
396
+ return /^[\w./:@-]+$/.test(value) ? value : `'${value.replaceAll("'", "'\\''")}'`;
397
+ }
398
+ async function exists(path) {
399
+ return access(path).then(
400
+ () => true,
401
+ () => false
402
+ );
403
+ }
404
+ async function onPath(commands, path) {
405
+ for (const directory of path.split(delimiter)) {
406
+ if (!directory) continue;
407
+ for (const command of commands) {
408
+ const found = await access(join4(directory, command), constants.X_OK).then(
409
+ () => true,
410
+ () => false
411
+ );
412
+ if (found) return true;
413
+ }
414
+ }
415
+ return false;
416
+ }
417
+
418
+ // src/gateway.ts
419
+ var STANDING_TIMEOUT_MS = 3e3;
420
+ function shortAddress(address) {
421
+ const lower = address.toLowerCase();
422
+ return `${lower.slice(0, 6)}\u2026${lower.slice(-4)}`;
423
+ }
424
+ async function fetchStanding(gatewayUrl, address, name, fetchFn = fetch, timeoutMs = STANDING_TIMEOUT_MS) {
425
+ const signal = AbortSignal.timeout(timeoutMs);
426
+ const direct = await fetchFn(
427
+ new URL(`/v1/patrons/${address.toLowerCase()}`, gatewayUrl),
428
+ { signal }
429
+ ).then(
430
+ async (response) => response.ok ? await response.json() : void 0
431
+ ).catch(() => void 0);
432
+ const info = PatronInfo.safeParse(direct);
433
+ if (info.success)
434
+ return {
435
+ rank: info.data.rank,
436
+ tokens: info.data.tokens,
437
+ usd: info.data.usd,
438
+ ...info.data.name ? { name: info.data.name } : {}
439
+ };
440
+ const board = await fetchFn(new URL("/v1/leaderboard", gatewayUrl), { signal }).then(
441
+ async (response) => response.ok ? await response.json() : void 0
442
+ ).catch(() => void 0);
443
+ const parsed = LeaderboardResponse.safeParse(board);
444
+ if (!parsed.success) return void 0;
445
+ const names = /* @__PURE__ */ new Set([shortAddress(address), ...name ? [name] : []]);
446
+ const index = parsed.data.patrons.findIndex((patron) => names.has(patron.name));
447
+ const row2 = parsed.data.patrons[index];
448
+ if (!row2) return void 0;
449
+ return { name: row2.name, rank: index + 1, tokens: row2.tokens, usd: row2.usd };
450
+ }
451
+ async function setProfile(gatewayUrl, wallet, name, fetchFn = fetch) {
452
+ const signature = await wallet.signMessage(profileMessage(wallet.address, name));
453
+ const response = await fetchFn(new URL("/v1/profile", gatewayUrl), {
454
+ body: JSON.stringify({ address: wallet.address, name, signature }),
455
+ headers: { "content-type": "application/json" },
456
+ method: "POST"
457
+ });
458
+ if (!response.ok)
459
+ throw new Error(`Profile update failed: ${response.status} ${await response.text()}`);
460
+ }
461
+
462
+ // src/prompts.ts
463
+ import { createInterface } from "node:readline/promises";
464
+
465
+ // src/prompt-input.ts
466
+ import { createReadStream, openSync } from "node:fs";
467
+ import { isatty, ReadStream } from "node:tty";
468
+ function openPromptInput() {
469
+ if (!process.stdin.isTTY) return { close: () => void 0, input: process.stdin };
470
+ try {
471
+ const terminalPath = process.platform === "win32" ? "CONIN$" : "/dev/tty";
472
+ const fd = openSync(terminalPath, "r");
473
+ const input = isatty(fd) ? new ReadStream(fd) : createReadStream(terminalPath, { autoClose: true, fd });
474
+ return { close: () => input.destroy(), input };
475
+ } catch {
476
+ return { close: () => void 0, input: process.stdin };
477
+ }
478
+ }
479
+
480
+ // src/terminal-style.ts
481
+ var RESET = "\x1B[0m";
482
+ var ANSI = {
483
+ bold: "\x1B[1m",
484
+ cyan: "\x1B[36m",
485
+ dim: "\x1B[2m",
486
+ green: "\x1B[32m",
487
+ purple: "\x1B[35m",
488
+ red: "\x1B[31m",
489
+ yellow: "\x1B[33m"
490
+ };
491
+ function supportsColor(output, requested = true) {
492
+ return requested && process.env.NO_COLOR === void 0 && process.env.TERM !== "dumb" && Boolean(output.isTTY);
493
+ }
494
+ function styleText(value, style, enabled) {
495
+ return enabled ? `${ANSI[style]}${value}${RESET}` : value;
496
+ }
497
+
498
+ // src/prompts.ts
499
+ function createPrompter(output = process.stdout, color = false, input) {
500
+ const promptInput = input ? { close: () => void 0, input } : openPromptInput();
501
+ const readline = createInterface({ input: promptInput.input, output, terminal: false });
502
+ const lines = readline[Symbol.asyncIterator]();
503
+ const read = async (query) => {
504
+ output.write(query);
505
+ const next = await lines.next();
506
+ return next.done ? void 0 : String(next.value).trim();
507
+ };
508
+ const ask = async (question, fallback) => {
509
+ const answer = await read(
510
+ `${styleText("?", "cyan", color)} ${question} ${styleText(`[${fallback}]`, "dim", color)} `
511
+ );
512
+ if (answer === void 0) throw new Error("Cancelled.");
513
+ return answer === "" ? fallback : answer;
514
+ };
515
+ return {
516
+ ask,
517
+ choose: async (title, options, fallback) => {
518
+ output.write(`
519
+ ${styleText(title, "bold", color)}
520
+ `);
521
+ options.forEach((option, index) => {
522
+ output.write(` ${styleText(String(index + 1), "cyan", color)}. ${option}
523
+ `);
524
+ });
525
+ for (; ; ) {
526
+ const answer = await ask("Choose", String(fallback + 1));
527
+ const index = Number(answer) - 1;
528
+ if (Number.isInteger(index) && index >= 0 && index < options.length) return index;
529
+ output.write(`Choose 1 to ${options.length}.
530
+ `);
531
+ }
532
+ },
533
+ close: () => {
534
+ readline.close();
535
+ promptInput.close();
536
+ },
537
+ confirm: async (question, fallback) => {
538
+ const hint = styleText(fallback ? "[Y/n]" : "[y/N]", "dim", color);
539
+ for (; ; ) {
540
+ const answer = await read(`${styleText("?", "cyan", color)} ${question} ${hint} `);
541
+ if (answer === void 0) throw new Error("Cancelled.");
542
+ const normalized = answer.toLowerCase();
543
+ if (normalized === "") return fallback;
544
+ if (normalized === "y" || normalized === "yes") return true;
545
+ if (normalized === "n" || normalized === "no") return false;
546
+ output.write("Enter y or n.\n");
547
+ }
548
+ }
549
+ };
550
+ }
551
+
552
+ // src/purchase.ts
553
+ var REFILL_LOCK_STALE_MS = 5 * 60 * 1e3;
554
+ var defaultOpenWallet = async (settings, config, issuer, maxUnits) => {
555
+ const { openWallet } = await import("./wallet-3AFTEAOI.js");
556
+ return openWallet(settings, config.wallet, issuer, maxUnits);
557
+ };
558
+ async function buyWithinBudget(settings, config, wanted, openWallet = defaultOpenWallet) {
559
+ const usage = await readUsage(settings.home);
560
+ const count = purchasableTokens(config, usage, wanted);
561
+ if (count === 0) return 0;
562
+ const issuer = await loadIssuer(settings.home, settings.gatewayUrl);
563
+ const wallet = await openWallet(
564
+ settings,
565
+ config,
566
+ issuer,
567
+ BigInt(count) * TOKEN_PRICE_UNITS
568
+ );
569
+ const tokens = await buyTokens({
570
+ count,
571
+ fetch: wallet.fetch,
572
+ gatewayUrl: settings.gatewayUrl,
573
+ issuer
574
+ });
575
+ await new FileTokenStore(settings.home).add(tokens);
576
+ await updateUsage(settings.home, (current) => {
577
+ current.boughtTokens += tokens.length;
578
+ });
579
+ return tokens.length;
580
+ }
581
+ async function refill(base, openWallet = defaultOpenWallet) {
582
+ const config = await readConfig(base.home);
583
+ if (!config || config.paused) return 0;
584
+ const settings = settingsFor(base, config);
585
+ const release = await tryLock(
586
+ statePaths(settings.home).refillLock,
587
+ REFILL_LOCK_STALE_MS
588
+ );
589
+ if (!release) return 0;
590
+ try {
591
+ if (!needsRefill(await new FileTokenStore(settings.home).count())) return 0;
592
+ return await buyWithinBudget(settings, config, PACK_SIZE, openWallet);
593
+ } finally {
594
+ await release();
595
+ }
596
+ }
597
+
598
+ // src/render.ts
599
+ function tokensUsd(tokens) {
600
+ return `$${twoDecimals(formatUsd(tokensToUnits(tokens)))}`;
601
+ }
602
+ function twoDecimals(usd) {
603
+ const [whole = "0", fraction = ""] = usd.split(".");
604
+ return `${whole}.${fraction.padEnd(2, "0")}`;
605
+ }
606
+ function row(label, value, color) {
607
+ return ` ${styleText(label.padEnd(13), "dim", color)}${value}`;
608
+ }
609
+ function formatStatus(view, color) {
610
+ const { config, tokensLeft, usage, standing } = view;
611
+ const state = config.paused ? styleText("\u25CF paused", "yellow", color) : styleText("\u25CF on", "green", color);
612
+ const network = config.network === "testnet" ? styleText(" \xB7 testnet", "dim", color) : "";
613
+ const bought = tokensUsd(usage.boughtTokens);
614
+ const budget = `$${twoDecimals(String(config.monthlyBudgetUsd))}`;
615
+ const top = Object.entries(usage.domains).sort(([, left], [, right]) => right - left).slice(0, 3).map(([domain, count]) => `${domain} ${count}`);
616
+ const lines = [
617
+ `${styleText("Patron", "bold", color)} ${state}${network}`,
618
+ row(
619
+ "Tokens left",
620
+ `${tokensLeft.toLocaleString("en-US")} (${tokensUsd(tokensLeft)})`,
621
+ color
622
+ ),
623
+ row(
624
+ "Per page",
625
+ `$${(clampTokensPerPage(config.tokensPerPage) / 1e3).toFixed(3)}`,
626
+ color
627
+ ),
628
+ row("Pages paid", `${usage.paidPages.toLocaleString("en-US")} this month`, color),
629
+ row(
630
+ "Budget used",
631
+ `${bought} of ${budget} this month (${budgetTokens(config.monthlyBudgetUsd) > 0 ? Math.min(100, Math.round(usage.boughtTokens / budgetTokens(config.monthlyBudgetUsd) * 100)) : 0}%)`,
632
+ color
633
+ ),
634
+ row(
635
+ "Rank",
636
+ standing?.rank ? `#${standing.rank} \xB7 ${standing.tokens.toLocaleString("en-US")} tokens bought` : styleText("not ranked yet", "dim", color),
637
+ color
638
+ ),
639
+ row(
640
+ "Agents",
641
+ config.agents.length > 0 ? config.agents.map((agent) => AGENT_LABELS[agent]).join(", ") : styleText("none", "dim", color),
642
+ color
643
+ )
644
+ ];
645
+ if (top.length > 0) lines.push(row("Top sites", top.join(" \xB7 "), color));
646
+ return lines.join("\n");
647
+ }
648
+ function formatAgentResults(results, color) {
649
+ return results.map((result) => {
650
+ const [glyph, style, text] = result.state === "failed" ? ["\u2717", "red", `hook not installed: ${result.detail ?? "unknown error"}`] : result.state === "removed" ? ["\u2713", "green", "hook removed"] : result.state === "missing" ? ["\xB7", "dim", "no hook to remove"] : result.state === "unchanged" ? ["\u2713", "green", "hook already installed"] : ["\u2713", "green", "hook installed"];
651
+ const note = result.note ? styleText(` (${result.note})`, "dim", color) : "";
652
+ return ` ${styleText(glyph, style, color)} ${result.label} ${text}${note}`;
653
+ });
654
+ }
655
+
656
+ // src/index.ts
657
+ var VERSION = readVersion();
658
+ var HELP = `patron@${VERSION} \u2014 pay the websites your agents read, privately
659
+
660
+ Usage: patron [command]
661
+
662
+ (none) Set up, or show status and options
663
+ status Tokens left, pages paid, budget and rank
664
+ pause | resume Stop or restart paying
665
+ budget <usd> Change the monthly budget
666
+ per-page <usd> What each site gets per page read, $0.001 to $0.01
667
+ tip <url> [usd] Tip a site (default $0.10)
668
+ name <handle> Set your public leaderboard name
669
+ uninstall Remove agent hooks
670
+ hook Run by agents after each web read (reads the event JSON)
671
+
672
+ Environment: PATRON_HOME, PATRON_GATEWAY_URL, PATRON_RELAY_URL, PATRON_NETWORK,
673
+ PATRON_PRIVATE_KEY (testnet automation), PATRON_BUDGET_USD (non-interactive setup).`;
674
+ var UsageError = class extends Error {
675
+ };
676
+ function defaults() {
677
+ const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
678
+ const color = supportsColor(process.stdout);
679
+ return {
680
+ agents: { env: process.env, home: homedir() },
681
+ color,
682
+ fetch: globalThis.fetch,
683
+ interactive,
684
+ prompter: () => createPrompter(process.stdout, color),
685
+ settings: resolveSettings(),
686
+ stderr: process.stderr,
687
+ startRefill: () => startRefill(),
688
+ stdout: process.stdout
689
+ };
690
+ }
691
+ async function run(argv, overrides = {}) {
692
+ const base = { ...defaults(), ...overrides };
693
+ let shared;
694
+ const deps = {
695
+ ...base,
696
+ prompter: () => {
697
+ shared ??= base.prompter();
698
+ return { ...shared, close: () => void 0 };
699
+ }
700
+ };
701
+ const [command, ...args] = argv;
702
+ try {
703
+ switch (command) {
704
+ case void 0:
705
+ await main(deps);
706
+ return 0;
707
+ case "status":
708
+ await status(deps, false);
709
+ return 0;
710
+ case "pause":
711
+ case "resume":
712
+ await setPaused(deps, command === "pause");
713
+ return 0;
714
+ case "budget":
715
+ await changeBudget(deps, args[0]);
716
+ return 0;
717
+ case "per-page":
718
+ await changePerPage(deps, args[0]);
719
+ return 0;
720
+ case "tip":
721
+ await tip(deps, args[0], args[1]);
722
+ return 0;
723
+ case "name":
724
+ await setName(deps, args[0]);
725
+ return 0;
726
+ case "uninstall":
727
+ await uninstall(deps);
728
+ return 0;
729
+ case "refill":
730
+ await refill(deps.settings, deps.openWallet).catch(() => 0);
731
+ return 0;
732
+ case "help":
733
+ case "--help":
734
+ case "-h":
735
+ deps.stdout.write(`${HELP}
736
+ `);
737
+ return 0;
738
+ case "--version":
739
+ case "-v":
740
+ deps.stdout.write(`${VERSION}
741
+ `);
742
+ return 0;
743
+ default:
744
+ throw new UsageError(`Unknown command: ${command}
745
+
746
+ ${HELP}`);
747
+ }
748
+ } catch (error) {
749
+ deps.stderr.write(
750
+ `${styleText("\u2717", "red", deps.color)} ${error.message}
751
+ `
752
+ );
753
+ return error instanceof UsageError ? 2 : 1;
754
+ } finally {
755
+ shared?.close();
756
+ }
757
+ }
758
+ async function loadSetup(deps) {
759
+ const config = await readConfig(deps.settings.home);
760
+ if (!config) throw new Error("Patron is not set up. Run npx @tempoxyz/patron.");
761
+ return { config, settings: settingsFor(deps.settings, config) };
762
+ }
763
+ function say(deps, line = "") {
764
+ deps.stdout.write(`${line}
765
+ `);
766
+ }
767
+ function step(deps, message) {
768
+ say(deps, `${styleText("\u2192", "cyan", deps.color)} ${message}`);
769
+ }
770
+ function done(deps, message) {
771
+ say(deps, `${styleText("\u2713", "green", deps.color)} ${message}`);
772
+ }
773
+ async function main(deps) {
774
+ const config = await readConfig(deps.settings.home);
775
+ if (!config) return setup(deps);
776
+ await syncAgents(deps, config);
777
+ await status(deps, deps.interactive);
778
+ }
779
+ async function syncAgents(deps, config) {
780
+ const added = (await detectAgents(deps.agents)).filter(
781
+ (agent) => !config.agents.includes(agent)
782
+ );
783
+ if (added.length === 0) return;
784
+ const results = await installAgents(
785
+ added,
786
+ await hookCommand(deps.agents.env),
787
+ deps.agents
788
+ );
789
+ for (const line of formatAgentResults(results, deps.color)) say(deps, line);
790
+ say(deps);
791
+ const installed = results.filter((result) => result.state !== "failed").map((result) => result.agent);
792
+ if (installed.length > 0)
793
+ await writeConfig(deps.settings.home, {
794
+ ...config,
795
+ agents: [...config.agents, ...installed]
796
+ });
797
+ }
798
+ async function setup(deps) {
799
+ const settings = deps.settings;
800
+ say(deps, styleText(`Patron v${VERSION}`, "bold", deps.color));
801
+ say(
802
+ deps,
803
+ styleText(
804
+ "Pay every site your agents read, $0.001 a page, privately.",
805
+ "dim",
806
+ deps.color
807
+ )
808
+ );
809
+ say(deps);
810
+ const issuer = await loadIssuer(settings.home, settings.gatewayUrl, {
811
+ fetch: deps.fetch,
812
+ fresh: true
813
+ }).catch((error) => {
814
+ throw new Error(`Cannot reach Patron at ${settings.gatewayUrl}: ${error.message}`);
815
+ });
816
+ const budget = await askBudget(deps, DEFAULT_MONTHLY_BUDGET_USD);
817
+ const budgetUnits = BigInt(budgetTokens(budget)) * TOKEN_PRICE_UNITS;
818
+ step(
819
+ deps,
820
+ settings.privateKey ? "Using PATRON_PRIVATE_KEY" : "Signing in with Tempo Wallet"
821
+ );
822
+ const { signIn } = await import("./wallet-3AFTEAOI.js");
823
+ const account = await signIn(settings, { budgetUnits, issuer, output: deps.stdout });
824
+ done(deps, `Signed in as ${account.address}`);
825
+ const config = {
826
+ address: account.address,
827
+ agents: [],
828
+ gatewayUrl: settings.gatewayUrl,
829
+ monthlyBudgetUsd: budget,
830
+ network: settings.network,
831
+ paused: false,
832
+ relayUrl: settings.relayUrl,
833
+ version: 1,
834
+ wallet: account.wallet
835
+ };
836
+ await writeConfig(settings.home, config);
837
+ const first = purchasableTokens(config, await readUsage(settings.home), STARTER_TOKENS);
838
+ step(deps, `Buying ${first.toLocaleString("en-US")} tokens (${tokensUsd(first)})`);
839
+ const bought = await buyWithinBudget(settings, config, STARTER_TOKENS, deps.openWallet);
840
+ done(deps, `${bought.toLocaleString("en-US")} tokens ready`);
841
+ deps.startRefill();
842
+ const agents = await detectAgents(deps.agents);
843
+ if (agents.length === 0) {
844
+ say(deps);
845
+ say(
846
+ deps,
847
+ "No supported agent found. Install Claude Code, Codex, Cursor or Amp, then run npx @tempoxyz/patron again."
848
+ );
849
+ return;
850
+ }
851
+ const results = await installAgents(
852
+ agents,
853
+ await hookCommand(deps.agents.env),
854
+ deps.agents
855
+ );
856
+ for (const line of formatAgentResults(results, deps.color)) say(deps, line);
857
+ config.agents = results.filter((result) => result.state !== "failed").map((r) => r.agent);
858
+ await writeConfig(settings.home, config);
859
+ say(deps);
860
+ say(
861
+ deps,
862
+ styleText("\u2713 Patron on. Agents now pay the sites they read.", "green", deps.color)
863
+ );
864
+ }
865
+ async function askBudget(deps, fallback) {
866
+ const preset = process.env.PATRON_BUDGET_USD;
867
+ if (!deps.interactive) {
868
+ const value = preset ? parseUsd(preset) : fallback;
869
+ if (value === void 0 || value <= 0)
870
+ throw new UsageError("PATRON_BUDGET_USD must be a positive amount.");
871
+ return value;
872
+ }
873
+ const prompter = deps.prompter();
874
+ try {
875
+ for (; ; ) {
876
+ const answer = await prompter.ask("Monthly budget in USD", `$${fallback}`);
877
+ const value = parseUsd(answer);
878
+ if (value !== void 0 && value >= 1e-3) return value;
879
+ say(deps, "Enter an amount such as 5 or 2.50.");
880
+ }
881
+ } finally {
882
+ prompter.close();
883
+ }
884
+ }
885
+ async function status(deps, menu) {
886
+ const { config, settings } = await loadSetup(deps);
887
+ const [tokensLeft, usage, standing] = await Promise.all([
888
+ new FileTokenStore(settings.home).count(),
889
+ readUsage(settings.home),
890
+ fetchStanding(settings.gatewayUrl, config.address, config.name, deps.fetch).catch(
891
+ () => void 0
892
+ )
893
+ ]);
894
+ say(deps, formatStatus({ config, standing, tokensLeft, usage }, deps.color));
895
+ if (!menu) return;
896
+ const prompter = deps.prompter();
897
+ let choice;
898
+ try {
899
+ choice = await prompter.choose(
900
+ "Options",
901
+ [
902
+ config.paused ? "Resume" : "Pause",
903
+ "Change budget",
904
+ "Change per-page amount",
905
+ "Uninstall",
906
+ "Done"
907
+ ],
908
+ 4
909
+ );
910
+ } finally {
911
+ prompter.close();
912
+ }
913
+ if (choice === 0) await setPaused(deps, !config.paused);
914
+ if (choice === 1) await changeBudget(deps, void 0);
915
+ if (choice === 2) await changePerPage(deps, void 0);
916
+ if (choice === 3) await uninstall(deps);
917
+ }
918
+ async function setPaused(deps, paused) {
919
+ const { config, settings } = await loadSetup(deps);
920
+ await writeConfig(settings.home, { ...config, paused });
921
+ done(
922
+ deps,
923
+ paused ? "Patron paused. Agents read without paying." : "Patron on. Agents pay the sites they read."
924
+ );
925
+ }
926
+ async function changeBudget(deps, input) {
927
+ const { config, settings } = await loadSetup(deps);
928
+ let budget;
929
+ if (input !== void 0) {
930
+ budget = parseUsd(input);
931
+ if (budget === void 0 || budget < 1e-3)
932
+ throw new UsageError("Usage: patron budget <usd>, such as patron budget 10");
933
+ } else budget = await askBudget(deps, config.monthlyBudgetUsd);
934
+ if (config.wallet === "tempo" && budget > config.monthlyBudgetUsd) {
935
+ step(deps, "Approve the new spending limit in Tempo Wallet");
936
+ const { authorizeBudget } = await import("./wallet-3AFTEAOI.js");
937
+ const issuer = await loadIssuer(settings.home, settings.gatewayUrl, {
938
+ fetch: deps.fetch
939
+ });
940
+ await authorizeBudget(settings, {
941
+ budgetUnits: BigInt(budgetTokens(budget)) * TOKEN_PRICE_UNITS,
942
+ issuer,
943
+ output: deps.stdout
944
+ });
945
+ }
946
+ await writeConfig(settings.home, { ...config, monthlyBudgetUsd: budget });
947
+ done(deps, `Monthly budget set to $${twoDecimals(String(budget))}.`);
948
+ }
949
+ async function changePerPage(deps, input) {
950
+ const { config, settings } = await loadSetup(deps);
951
+ const min = (MIN_TOKENS_PER_PAGE / 1e3).toFixed(3);
952
+ const max = (MAX_TOKENS_PER_PAGE / 1e3).toFixed(3);
953
+ let answer = input;
954
+ if (answer === void 0) {
955
+ const prompter = deps.prompter();
956
+ try {
957
+ answer = await prompter.ask(
958
+ `Per page in USD ($${min} to $${max})`,
959
+ `$${(clampTokensPerPage(config.tokensPerPage) / 1e3).toFixed(3)}`
960
+ );
961
+ } finally {
962
+ prompter.close();
963
+ }
964
+ }
965
+ const usd = parseUsd(answer);
966
+ const tokens = usd === void 0 ? NaN : Math.round(usd * 1e3);
967
+ if (usd === void 0 || Math.abs(usd * 1e3 - tokens) > 1e-9 || tokens < MIN_TOKENS_PER_PAGE || tokens > MAX_TOKENS_PER_PAGE)
968
+ throw new UsageError(
969
+ `Usage: patron per-page <usd>, $${min} to $${max} in steps of $0.001, such as patron per-page 0.005`
970
+ );
971
+ await writeConfig(settings.home, { ...config, tokensPerPage: tokens });
972
+ done(deps, `Each site now gets $${(tokens / 1e3).toFixed(3)} per page.`);
973
+ }
974
+ async function tip(deps, url, amount = "0.10") {
975
+ const domain = url ? normalizeDomain(url) : null;
976
+ const usd = parseUsd(amount);
977
+ if (!domain || usd === void 0) throw new UsageError("Usage: patron tip <url> [usd]");
978
+ const count = tipTokens(usd);
979
+ if (count < 1) throw new UsageError("The smallest tip is $0.001.");
980
+ const { config, settings } = await loadSetup(deps);
981
+ const store = new FileTokenStore(settings.home);
982
+ const missing = count - await store.count();
983
+ if (missing > 0) {
984
+ const usage = await readUsage(settings.home);
985
+ if (purchasableTokens(config, usage, missing) < missing)
986
+ throw new Error(
987
+ `This tip needs ${tokensUsd(missing)} more than this month's budget allows. Raise it with patron budget <usd>.`
988
+ );
989
+ step(deps, `Buying ${missing.toLocaleString("en-US")} tokens (${tokensUsd(missing)})`);
990
+ await buyWithinBudget(settings, config, missing, deps.openWallet);
991
+ }
992
+ const issuer = await loadIssuer(settings.home, settings.gatewayUrl, {
993
+ fetch: deps.fetch
994
+ });
995
+ const tokens = await store.takeMany(count);
996
+ let paid = 0;
997
+ let rejection;
998
+ const unsent = [];
999
+ for (let index = 0; index < tokens.length; index += 8) {
1000
+ const batch = tokens.slice(index, index + 8);
1001
+ const results = await Promise.all(
1002
+ batch.map(
1003
+ (token) => redeem({
1004
+ domain,
1005
+ fetch: deps.fetch,
1006
+ issuer,
1007
+ relayUrl: settings.relayUrl,
1008
+ token
1009
+ }).catch(() => {
1010
+ unsent.push(token);
1011
+ return void 0;
1012
+ })
1013
+ )
1014
+ );
1015
+ for (const result of results) {
1016
+ if (result?.ok) paid += 1;
1017
+ else if (result) rejection = result.error ?? "rejected";
1018
+ }
1019
+ }
1020
+ await store.add(unsent);
1021
+ if (paid > 0)
1022
+ await updateUsage(settings.home, (usage) => {
1023
+ usage.domains[domain] = (usage.domains[domain] ?? 0) + paid;
1024
+ });
1025
+ if (paid === 0)
1026
+ throw new Error(
1027
+ rejection ? `Patron rejected the tip to ${domain}: ${rejection}.` : `Could not reach Patron to tip ${domain}; your tokens were kept.`
1028
+ );
1029
+ if (paid < count)
1030
+ say(
1031
+ deps,
1032
+ styleText(
1033
+ ` ${count - paid} of ${count} tokens did not go through.`,
1034
+ "yellow",
1035
+ deps.color
1036
+ )
1037
+ );
1038
+ done(deps, `Tipped ${domain} ${tokensUsd(paid)}.`);
1039
+ }
1040
+ async function setName(deps, name) {
1041
+ if (!name || !/^[\p{L}\p{N}_.-]{1,32}$/u.test(name))
1042
+ throw new UsageError("Usage: patron name <handle> (1 to 32 letters, digits, _ . -)");
1043
+ const { config, settings } = await loadSetup(deps);
1044
+ const issuer = await loadIssuer(settings.home, settings.gatewayUrl, {
1045
+ fetch: deps.fetch
1046
+ });
1047
+ const open = deps.openWallet ?? (async (s, c, i, max) => (await import("./wallet-3AFTEAOI.js")).openWallet(s, c.wallet, i, max));
1048
+ const wallet = await open(settings, config, issuer, 0n);
1049
+ await setProfile(settings.gatewayUrl, wallet, name, deps.fetch);
1050
+ await writeConfig(settings.home, { ...config, name });
1051
+ done(deps, `Leaderboard name set to ${name}.`);
1052
+ }
1053
+ async function uninstall(deps) {
1054
+ const config = await readConfig(deps.settings.home);
1055
+ const results = (await removeAgents(deps.agents)).filter(
1056
+ (result) => result.state !== "missing"
1057
+ );
1058
+ say(deps, styleText("Patron uninstall", "bold", deps.color));
1059
+ for (const line of formatAgentResults(results, deps.color)) say(deps, line);
1060
+ if (results.length === 0)
1061
+ say(deps, styleText(" \xB7 No agent hooks to remove", "dim", deps.color));
1062
+ if (!config) return;
1063
+ await writeConfig(deps.settings.home, { ...config, agents: [] });
1064
+ const tokensLeft = await new FileTokenStore(deps.settings.home).count().catch(() => 0);
1065
+ let purge = false;
1066
+ if (deps.interactive) {
1067
+ const prompter = deps.prompter();
1068
+ try {
1069
+ purge = await prompter.confirm(
1070
+ `Also delete ${deps.settings.home}${tokensLeft > 0 ? ` and ${tokensLeft} unspent tokens (${tokensUsd(tokensLeft)})` : ""}?`,
1071
+ false
1072
+ );
1073
+ } finally {
1074
+ prompter.close();
1075
+ }
1076
+ }
1077
+ if (purge) {
1078
+ await rm3(deps.settings.home, { force: true, recursive: true });
1079
+ done(deps, "Local data deleted.");
1080
+ } else
1081
+ say(
1082
+ deps,
1083
+ styleText(
1084
+ ` Kept ${deps.settings.home}; run npx @tempoxyz/patron to turn Patron back on.`,
1085
+ "dim",
1086
+ deps.color
1087
+ )
1088
+ );
1089
+ }
1090
+ function readVersion() {
1091
+ for (const path of ["../package.json", "../../package.json"]) {
1092
+ try {
1093
+ const metadata = JSON.parse(
1094
+ readFileSync(new URL(path, import.meta.url), "utf8")
1095
+ );
1096
+ if (metadata.name === "@tempoxyz/patron" && metadata.version)
1097
+ return metadata.version;
1098
+ } catch {
1099
+ }
1100
+ }
1101
+ return "0.0.0";
1102
+ }
1103
+ export {
1104
+ run
1105
+ };