@tempoxyz/patron 0.0.28 → 0.0.30
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 +11 -3
- package/dist/chunks/{-5Q7V7W7W.js → -WRUP4P4C.js} +341 -116
- package/dist/chunks/{chunk-HQ2ZLUTT.js → chunk-23MZSDN6.js} +1 -1
- package/dist/chunks/{chunk-EKFENOVR.js → chunk-4SXIRXUL.js} +68 -28
- package/dist/chunks/{chunk-EISHX7Q3.js → chunk-AKOXBXV3.js} +207 -48
- package/dist/chunks/{chunk-OQXACACU.js → chunk-NDBTADXZ.js} +9 -2
- package/dist/chunks/{chunk-EV7OA6GK.js → chunk-ROVOCDKE.js} +7 -163
- package/dist/chunks/chunk-TC2WEQ25.js +179 -0
- package/dist/chunks/{hook-N2SVXVUR.js → hook-IE7INCPX.js} +17 -15
- package/dist/chunks/{hook-process-IMQMGXBT.js → hook-process-ETJ3UZZT.js} +62 -16
- package/dist/chunks/{wallet-6TDMXRWW.js → wallet-OTFEEKIG.js} +3 -4
- package/package.json +1 -1
- package/dist/chunks/chunk-TV5VLFTD.js +0 -55
|
@@ -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,10 +17,13 @@ import {
|
|
|
15
17
|
loadIssuer,
|
|
16
18
|
needsRefill,
|
|
17
19
|
parseUsd,
|
|
20
|
+
patronCommands,
|
|
18
21
|
purchasableTokens,
|
|
19
22
|
readJsonObject,
|
|
20
23
|
redeem,
|
|
24
|
+
refillSize,
|
|
21
25
|
removeCodex,
|
|
26
|
+
storedIssuer,
|
|
22
27
|
tipTokens,
|
|
23
28
|
upgradeCodex,
|
|
24
29
|
withHooksKey,
|
|
@@ -26,41 +31,44 @@ import {
|
|
|
26
31
|
withPatronStatus,
|
|
27
32
|
withoutPatronHandlers,
|
|
28
33
|
writeJsonObject
|
|
29
|
-
} from "./chunk-
|
|
34
|
+
} from "./chunk-4SXIRXUL.js";
|
|
30
35
|
import {
|
|
36
|
+
awaitRedemptions,
|
|
37
|
+
gatewayTurn,
|
|
31
38
|
startRefill
|
|
32
|
-
} from "./chunk-
|
|
39
|
+
} from "./chunk-TC2WEQ25.js";
|
|
33
40
|
import {
|
|
41
|
+
inferHome,
|
|
34
42
|
resolveSettings,
|
|
35
43
|
settingsFor
|
|
36
|
-
} from "./chunk-
|
|
44
|
+
} from "./chunk-NDBTADXZ.js";
|
|
37
45
|
import {
|
|
38
|
-
AGENT_IDS,
|
|
39
46
|
LeaderboardResponse,
|
|
40
47
|
PatronInfo,
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
readConfig,
|
|
44
|
-
readJson,
|
|
45
|
-
readUsage,
|
|
46
|
-
statePaths,
|
|
47
|
-
updateUsage,
|
|
48
|
-
writeConfig,
|
|
49
|
-
writePrivateFile
|
|
50
|
-
} from "./chunk-EV7OA6GK.js";
|
|
48
|
+
profileMessage
|
|
49
|
+
} from "./chunk-ROVOCDKE.js";
|
|
51
50
|
import {
|
|
51
|
+
AGENT_IDS,
|
|
52
52
|
DEFAULT_MONTHLY_BUDGET_USD,
|
|
53
53
|
MAX_TOKENS_PER_PAGE,
|
|
54
54
|
MIN_TOKENS_PER_PAGE,
|
|
55
55
|
PACK_SIZE,
|
|
56
56
|
TOKEN_PRICE_UNITS,
|
|
57
57
|
clampTokensPerPage,
|
|
58
|
+
ensureHome,
|
|
58
59
|
formatUsd,
|
|
59
60
|
normalizePayee,
|
|
61
|
+
readConfig,
|
|
62
|
+
readJson,
|
|
63
|
+
readUsage,
|
|
64
|
+
statePaths,
|
|
60
65
|
tokensToUnits,
|
|
61
66
|
tryLock,
|
|
62
|
-
|
|
63
|
-
|
|
67
|
+
updateUsage,
|
|
68
|
+
withLock,
|
|
69
|
+
writeConfig,
|
|
70
|
+
writePrivateFile
|
|
71
|
+
} from "./chunk-AKOXBXV3.js";
|
|
64
72
|
import "./chunk-GTMLTZRN.js";
|
|
65
73
|
|
|
66
74
|
// src/index.ts
|
|
@@ -70,9 +78,9 @@ import { homedir as homedir2 } from "node:os";
|
|
|
70
78
|
|
|
71
79
|
// src/agents.ts
|
|
72
80
|
import { constants } from "node:fs";
|
|
73
|
-
import { access as access2, realpath as realpath2 } from "node:fs/promises";
|
|
81
|
+
import { access as access2, readFile as readFile4, realpath as realpath2 } from "node:fs/promises";
|
|
74
82
|
import { homedir } from "node:os";
|
|
75
|
-
import { delimiter, join as join5 } from "node:path";
|
|
83
|
+
import { delimiter, join as join5, resolve } from "node:path";
|
|
76
84
|
|
|
77
85
|
// src/amp.ts
|
|
78
86
|
import { mkdir, readFile, rm } from "node:fs/promises";
|
|
@@ -134,13 +142,17 @@ async function installAmpPlugin(path, command) {
|
|
|
134
142
|
return "installed";
|
|
135
143
|
}
|
|
136
144
|
async function upgradeAmpPlugin(path) {
|
|
145
|
+
const command = await ampPluginCommand(path);
|
|
146
|
+
if (command === void 0) return false;
|
|
147
|
+
return await installAmpPlugin(path, command) === "installed";
|
|
148
|
+
}
|
|
149
|
+
async function ampPluginCommand(path) {
|
|
137
150
|
const current = await readText(path);
|
|
138
|
-
if (current === void 0 || !current.startsWith(AMP_PLUGIN_MARKER)) return
|
|
151
|
+
if (current === void 0 || !current.startsWith(AMP_PLUGIN_MARKER)) return void 0;
|
|
139
152
|
const literal = /^const COMMAND = (".*")$/m.exec(current)?.[1];
|
|
140
|
-
if (!literal) return
|
|
153
|
+
if (!literal) return void 0;
|
|
141
154
|
const command = JSON.parse(literal);
|
|
142
|
-
|
|
143
|
-
return await installAmpPlugin(path, command) === "installed";
|
|
155
|
+
return typeof command === "string" ? command : void 0;
|
|
144
156
|
}
|
|
145
157
|
async function removeAmpPlugin(path) {
|
|
146
158
|
const current = await readText(path);
|
|
@@ -161,7 +173,6 @@ async function readText(path) {
|
|
|
161
173
|
var CLAUDE_MATCHER = "WebFetch|WebSearch";
|
|
162
174
|
var HOOK_TIMEOUT_SECONDS = 30;
|
|
163
175
|
var EVENT = "PostToolUse";
|
|
164
|
-
var CLAUDE_HOOK_STATUS = "Patron: pay the site this read";
|
|
165
176
|
function patronGroup(command) {
|
|
166
177
|
return {
|
|
167
178
|
hooks: [
|
|
@@ -176,8 +187,8 @@ function patronGroup(command) {
|
|
|
176
187
|
matcher: CLAUDE_MATCHER
|
|
177
188
|
};
|
|
178
189
|
}
|
|
179
|
-
function withPatronHook(settings, command) {
|
|
180
|
-
return withPatronGroup(settings, EVENT, patronGroup(command));
|
|
190
|
+
function withPatronHook(settings, command, known) {
|
|
191
|
+
return withPatronGroup(settings, EVENT, patronGroup(command), known);
|
|
181
192
|
}
|
|
182
193
|
async function upgradeClaudeHook(path) {
|
|
183
194
|
const settings = await readJsonObject(path);
|
|
@@ -186,8 +197,8 @@ async function upgradeClaudeHook(path) {
|
|
|
186
197
|
await writeJsonObject(path, next);
|
|
187
198
|
return true;
|
|
188
199
|
}
|
|
189
|
-
function withoutPatronHook(settings) {
|
|
190
|
-
return withoutPatronHandlers(settings, EVENT);
|
|
200
|
+
function withoutPatronHook(settings, known) {
|
|
201
|
+
return withoutPatronHandlers(settings, EVENT, known);
|
|
191
202
|
}
|
|
192
203
|
function hasPatronHook(settings, command) {
|
|
193
204
|
return hasPatronHandler(settings, EVENT, {
|
|
@@ -195,16 +206,17 @@ function hasPatronHook(settings, command) {
|
|
|
195
206
|
...command === void 0 ? {} : { command }
|
|
196
207
|
});
|
|
197
208
|
}
|
|
198
|
-
async function installClaudeHook(path, command) {
|
|
209
|
+
async function installClaudeHook(path, command, known) {
|
|
199
210
|
const settings = await readJsonObject(path);
|
|
200
|
-
if (hasPatronHook(settings, command))
|
|
201
|
-
|
|
211
|
+
if (hasPatronHook(settings, command) && patronCommands(settings, EVENT).length === 1)
|
|
212
|
+
return "unchanged";
|
|
213
|
+
await writeJsonObject(path, withPatronHook(settings, command, known));
|
|
202
214
|
return "installed";
|
|
203
215
|
}
|
|
204
|
-
async function removeClaudeHook(path) {
|
|
216
|
+
async function removeClaudeHook(path, known) {
|
|
205
217
|
const settings = await readJsonObject(path);
|
|
206
|
-
if (!hasPatronHandler(settings, EVENT)) return "missing";
|
|
207
|
-
await writeJsonObject(path, withoutPatronHook(settings));
|
|
218
|
+
if (!hasPatronHandler(settings, EVENT, known ? { known } : {})) return "missing";
|
|
219
|
+
await writeJsonObject(path, withoutPatronHook(settings, known));
|
|
208
220
|
return "removed";
|
|
209
221
|
}
|
|
210
222
|
|
|
@@ -216,42 +228,46 @@ var EVENT2 = "postToolUse";
|
|
|
216
228
|
function cursorHooksPath(home) {
|
|
217
229
|
return join2(home, ".cursor", "hooks.json");
|
|
218
230
|
}
|
|
219
|
-
function withCursorHook(config, command) {
|
|
220
|
-
const stripped = withoutCursorHook(config);
|
|
231
|
+
function withCursorHook(config, command, known) {
|
|
232
|
+
const stripped = withoutCursorHook(config, known);
|
|
221
233
|
const next = typeof stripped.version === "number" ? stripped : { version: 1, ...stripped };
|
|
222
234
|
const hooks = isObject(next.hooks) ? next.hooks : {};
|
|
223
235
|
const entries = Array.isArray(hooks[EVENT2]) ? hooks[EVENT2] : [];
|
|
224
236
|
return withHooksKey(next, EVENT2, [...entries, cursorEntry(command)]);
|
|
225
237
|
}
|
|
226
|
-
function withoutCursorHook(config) {
|
|
238
|
+
function withoutCursorHook(config, known) {
|
|
227
239
|
const entries = isObject(config.hooks) ? config.hooks[EVENT2] : void 0;
|
|
228
|
-
|
|
240
|
+
const patron = (entry) => isPatronHandler(entry, known);
|
|
241
|
+
if (!Array.isArray(entries) || !entries.some(patron)) return { ...config };
|
|
229
242
|
return withHooksKey(
|
|
230
243
|
config,
|
|
231
244
|
EVENT2,
|
|
232
|
-
entries.filter((entry) => !
|
|
245
|
+
entries.filter((entry) => !patron(entry))
|
|
233
246
|
);
|
|
234
247
|
}
|
|
235
248
|
function cursorEntry(command) {
|
|
236
249
|
return { command, matcher: CURSOR_MATCHER, timeout: HOOK_TIMEOUT_SECONDS2 };
|
|
237
250
|
}
|
|
238
|
-
function hasCursorHook(config, command) {
|
|
251
|
+
function hasCursorHook(config, command, known) {
|
|
239
252
|
const entries = isObject(config.hooks) ? config.hooks[EVENT2] : void 0;
|
|
240
253
|
if (!Array.isArray(entries)) return false;
|
|
241
|
-
if (command === void 0) return entries.some(isPatronHandler);
|
|
254
|
+
if (command === void 0) return entries.some((entry) => isPatronHandler(entry, known));
|
|
242
255
|
const wanted = JSON.stringify(cursorEntry(command));
|
|
243
|
-
|
|
256
|
+
const patron = entries.filter(
|
|
257
|
+
(entry) => isPatronHandler(entry, known) || JSON.stringify(entry) === wanted
|
|
258
|
+
);
|
|
259
|
+
return patron.length === 1 && JSON.stringify(patron[0]) === wanted;
|
|
244
260
|
}
|
|
245
|
-
async function installCursorHook(path, command) {
|
|
261
|
+
async function installCursorHook(path, command, known) {
|
|
246
262
|
const config = await readJsonObject(path);
|
|
247
|
-
if (hasCursorHook(config, command)) return "unchanged";
|
|
248
|
-
await writeJsonObject(path, withCursorHook(config, command));
|
|
263
|
+
if (hasCursorHook(config, command, known)) return "unchanged";
|
|
264
|
+
await writeJsonObject(path, withCursorHook(config, command, known));
|
|
249
265
|
return "installed";
|
|
250
266
|
}
|
|
251
|
-
async function removeCursorHook(path) {
|
|
267
|
+
async function removeCursorHook(path, known) {
|
|
252
268
|
const config = await readJsonObject(path);
|
|
253
|
-
if (!hasCursorHook(config)) return "missing";
|
|
254
|
-
await writeJsonObject(path, withoutCursorHook(config));
|
|
269
|
+
if (!hasCursorHook(config, void 0, known)) return "missing";
|
|
270
|
+
await writeJsonObject(path, withoutCursorHook(config, known));
|
|
255
271
|
return "removed";
|
|
256
272
|
}
|
|
257
273
|
|
|
@@ -351,9 +367,9 @@ import { basename, dirname as dirname3, join as join4 } from "node:path";
|
|
|
351
367
|
function stableBin(home) {
|
|
352
368
|
return join4(home, "cli", "current", "dist", "bin.js");
|
|
353
369
|
}
|
|
354
|
-
async function installStableCli(home, script = process.argv[1]) {
|
|
370
|
+
async function installStableCli(home, script = process.argv[1], anySource = process.platform === "win32" && Boolean(process.env.PATRON_HOME)) {
|
|
355
371
|
const resolved = script ? await realpath(script).catch(() => void 0) : void 0;
|
|
356
|
-
if (!resolved || !/[/\\]_npx[/\\]/.test(resolved) || basename(resolved) !== "bin.js")
|
|
372
|
+
if (!resolved || !anySource && !/[/\\]_npx[/\\]/.test(resolved) || basename(resolved) !== "bin.js")
|
|
357
373
|
return void 0;
|
|
358
374
|
const packageDir = dirname3(dirname3(resolved));
|
|
359
375
|
const { version } = JSON.parse(await readFile3(join4(packageDir, "package.json"), "utf8"));
|
|
@@ -408,32 +424,33 @@ var ADAPTERS = {
|
|
|
408
424
|
install: async (command, { env, home }) => ({
|
|
409
425
|
state: await installAmpPlugin(ampPluginPath(env, home), command)
|
|
410
426
|
}),
|
|
427
|
+
// The plugin file starts with Patron's marker, so it needs no known commands.
|
|
411
428
|
remove: ({ env, home }) => removeAmpPlugin(ampPluginPath(env, home)),
|
|
412
429
|
upgrade: ({ env, home }) => upgradeAmpPlugin(ampPluginPath(env, home))
|
|
413
430
|
},
|
|
414
431
|
"claude-code": {
|
|
415
432
|
files: (environment) => [claudeSettingsPath(environment)],
|
|
416
433
|
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)
|
|
434
|
+
install: async (command, environment, known) => ({
|
|
435
|
+
state: await installClaudeHook(claudeSettingsPath(environment), command, known)
|
|
419
436
|
}),
|
|
420
|
-
remove: (environment) => removeClaudeHook(claudeSettingsPath(environment)),
|
|
437
|
+
remove: (environment, known) => removeClaudeHook(claudeSettingsPath(environment), known),
|
|
421
438
|
upgrade: (environment) => upgradeClaudeHook(claudeSettingsPath(environment))
|
|
422
439
|
},
|
|
423
440
|
codex: {
|
|
424
441
|
files: ({ env, home }) => Object.values(codexPaths(env, home)),
|
|
425
442
|
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)),
|
|
443
|
+
install: (command, { env, home }, known) => installCodex(codexPaths(env, home), command, process.platform, known),
|
|
444
|
+
remove: ({ env, home }, known) => removeCodex(codexPaths(env, home), known),
|
|
428
445
|
upgrade: ({ env, home }) => upgradeCodex(codexPaths(env, home))
|
|
429
446
|
},
|
|
430
447
|
cursor: {
|
|
431
448
|
files: ({ home }) => [cursorHooksPath(home)],
|
|
432
449
|
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)
|
|
450
|
+
install: async (command, { home }, known) => ({
|
|
451
|
+
state: await installCursorHook(cursorHooksPath(home), command, known)
|
|
435
452
|
}),
|
|
436
|
-
remove: ({ home }) => removeCursorHook(cursorHooksPath(home))
|
|
453
|
+
remove: ({ home }, known) => removeCursorHook(cursorHooksPath(home), known)
|
|
437
454
|
}
|
|
438
455
|
};
|
|
439
456
|
function claudeDirectory({ env, home }) {
|
|
@@ -452,12 +469,13 @@ async function detectAgents(environment) {
|
|
|
452
469
|
}
|
|
453
470
|
async function installAgents(agents, command, environment) {
|
|
454
471
|
const store = originalsStore(environment);
|
|
472
|
+
const known = await knownCommands(environment, [command]);
|
|
455
473
|
return Promise.all(
|
|
456
474
|
agents.map(async (agent) => {
|
|
457
475
|
let recorded = [];
|
|
458
476
|
try {
|
|
459
477
|
recorded = await rememberOriginals(store, ADAPTERS[agent].files(environment));
|
|
460
|
-
const { note, state } = await ADAPTERS[agent].install(command, environment);
|
|
478
|
+
const { note, state } = await ADAPTERS[agent].install(command, environment, known);
|
|
461
479
|
if (state !== "installed") await forgetOriginals(store, recorded);
|
|
462
480
|
return { agent, label: AGENT_LABELS[agent], state, ...note ? { note } : {} };
|
|
463
481
|
} catch (error) {
|
|
@@ -477,10 +495,17 @@ async function upgradeAgents(agents, environment) {
|
|
|
477
495
|
}
|
|
478
496
|
async function removeAgents(environment) {
|
|
479
497
|
const store = originalsStore(environment);
|
|
498
|
+
const patronHome = environment.env.PATRON_HOME || join5(environment.home, ".patron");
|
|
499
|
+
const stable = `${quote(process.execPath)} ${quote(stableBin(patronHome))} hook`;
|
|
500
|
+
const known = await knownCommands(environment, [
|
|
501
|
+
await hookCommand(environment.env, process.argv[1], process.execPath, patronHome),
|
|
502
|
+
stable,
|
|
503
|
+
withPatronHome(stable, environment.env.PATRON_HOME)
|
|
504
|
+
]);
|
|
480
505
|
return Promise.all(
|
|
481
506
|
AGENT_IDS.map(async (agent) => {
|
|
482
507
|
try {
|
|
483
|
-
const state = await ADAPTERS[agent].remove(environment);
|
|
508
|
+
const state = await ADAPTERS[agent].remove(environment, known);
|
|
484
509
|
if (state === "removed")
|
|
485
510
|
await restoreOriginals(store, ADAPTERS[agent].files(environment)).catch(
|
|
486
511
|
() => void 0
|
|
@@ -492,6 +517,32 @@ async function removeAgents(environment) {
|
|
|
492
517
|
})
|
|
493
518
|
);
|
|
494
519
|
}
|
|
520
|
+
async function knownCommands({ env, home }, extra) {
|
|
521
|
+
const paths = codexPaths(env, home);
|
|
522
|
+
const found = await Promise.all([
|
|
523
|
+
readJsonObject(claudeSettingsPath({ env, home })).then(
|
|
524
|
+
(settings) => patronCommands(settings, "PostToolUse"),
|
|
525
|
+
() => []
|
|
526
|
+
),
|
|
527
|
+
readJsonObject(paths.hooks).then(
|
|
528
|
+
(settings) => patronCommands(settings, "Stop"),
|
|
529
|
+
() => []
|
|
530
|
+
),
|
|
531
|
+
readFile4(paths.config, "utf8").then(
|
|
532
|
+
(toml) => [codexNotifyCommand(toml)],
|
|
533
|
+
() => []
|
|
534
|
+
),
|
|
535
|
+
ampPluginCommand(ampPluginPath(env, home)).then(
|
|
536
|
+
(command) => [command],
|
|
537
|
+
() => []
|
|
538
|
+
)
|
|
539
|
+
]);
|
|
540
|
+
return new Set(
|
|
541
|
+
[...found.flat(), ...extra].filter(
|
|
542
|
+
(command) => typeof command === "string" && command !== ""
|
|
543
|
+
)
|
|
544
|
+
);
|
|
545
|
+
}
|
|
495
546
|
function originalsStore({ env, home }) {
|
|
496
547
|
return originalsPath(env.PATRON_HOME || join5(home, ".patron"));
|
|
497
548
|
}
|
|
@@ -503,17 +554,45 @@ function failure(agent, error) {
|
|
|
503
554
|
state: "failed"
|
|
504
555
|
};
|
|
505
556
|
}
|
|
506
|
-
async function hookCommand(env = process.env, script = process.argv[1], node = process.execPath, patronHome = env.PATRON_HOME || join5(homedir(), ".patron")) {
|
|
557
|
+
async function hookCommand(env = process.env, script = process.argv[1], node = process.execPath, patronHome = env.PATRON_HOME || join5(homedir(), ".patron"), platform = process.platform) {
|
|
507
558
|
if (env.PATRON_HOOK_COMMAND) return env.PATRON_HOOK_COMMAND;
|
|
559
|
+
const windows = platform === "win32";
|
|
560
|
+
const run2 = await runCommand(
|
|
561
|
+
script,
|
|
562
|
+
node,
|
|
563
|
+
patronHome,
|
|
564
|
+
windows && Boolean(env.PATRON_HOME),
|
|
565
|
+
platform
|
|
566
|
+
);
|
|
567
|
+
if (!env.PATRON_HOME || windows || await runsFromHome(run2.bin, patronHome))
|
|
568
|
+
return run2.command;
|
|
569
|
+
return withPatronHome(run2.command, env.PATRON_HOME);
|
|
570
|
+
}
|
|
571
|
+
async function runCommand(script, node, patronHome, preferCopy, platform) {
|
|
572
|
+
const run2 = (bin) => ({
|
|
573
|
+
bin,
|
|
574
|
+
command: `${quote(node, platform)} ${quote(bin, platform)} hook`
|
|
575
|
+
});
|
|
576
|
+
const stable = stableBin(patronHome);
|
|
577
|
+
if (preferCopy && await exists(stable)) return run2(stable);
|
|
508
578
|
const resolved = script ? await realpath2(script).catch(() => void 0) : void 0;
|
|
509
579
|
if (resolved && /[/\\]bin\.js$/.test(resolved) && !/[/\\]_npx[/\\]/.test(resolved))
|
|
510
|
-
return
|
|
511
|
-
if (await exists(
|
|
512
|
-
|
|
513
|
-
return "npx -y @tempoxyz/patron hook";
|
|
580
|
+
return run2(resolved);
|
|
581
|
+
if (await exists(stable)) return run2(stable);
|
|
582
|
+
return { command: "npx -y @tempoxyz/patron hook" };
|
|
514
583
|
}
|
|
515
|
-
function
|
|
516
|
-
|
|
584
|
+
async function runsFromHome(bin, patronHome) {
|
|
585
|
+
const inferred = inferHome(bin);
|
|
586
|
+
if (!inferred) return false;
|
|
587
|
+
const real = (path) => realpath2(path).catch(() => resolve(path));
|
|
588
|
+
return await real(inferred) === await real(patronHome);
|
|
589
|
+
}
|
|
590
|
+
function withPatronHome(command, patronHome) {
|
|
591
|
+
return patronHome ? `PATRON_HOME=${quote(resolve(patronHome))} ${command}` : command;
|
|
592
|
+
}
|
|
593
|
+
function quote(value, platform = process.platform) {
|
|
594
|
+
if (/^[\w./:@-]+$/.test(value)) return value;
|
|
595
|
+
return platform === "win32" ? `"${value}"` : `'${value.replaceAll("'", "'\\''")}'`;
|
|
517
596
|
}
|
|
518
597
|
async function exists(path) {
|
|
519
598
|
return access2(path).then(
|
|
@@ -697,16 +776,24 @@ async function linkRefillFailedAt(home) {
|
|
|
697
776
|
const value = await readJson(statePaths(home).linkRefill).catch(() => void 0);
|
|
698
777
|
return typeof value?.failedAt === "number" ? value.failedAt : void 0;
|
|
699
778
|
}
|
|
700
|
-
var REFILL_LOCK_STALE_MS = 5 * 60 * 1e3;
|
|
701
779
|
var defaultOpenWallet = async (settings, config, issuer, maxUnits) => {
|
|
702
780
|
if (config.wallet === "link") {
|
|
703
|
-
const { openLinkWallet } = await import("./wallet-
|
|
781
|
+
const { openLinkWallet } = await import("./wallet-OTFEEKIG.js");
|
|
704
782
|
return openLinkWallet(config, maxUnits);
|
|
705
783
|
}
|
|
706
|
-
const { openWallet } = await import("./wallet-
|
|
784
|
+
const { openWallet } = await import("./wallet-OTFEEKIG.js");
|
|
707
785
|
return openWallet(settings, config.wallet, issuer, maxUnits);
|
|
708
786
|
};
|
|
787
|
+
var REFILL_LOCK_STALE_MS = 5 * 60 * 1e3;
|
|
709
788
|
async function buyWithinBudget(settings, config, wanted, openWallet = defaultOpenWallet) {
|
|
789
|
+
await ensureHome(settings.home);
|
|
790
|
+
return withLock(
|
|
791
|
+
statePaths(settings.home).purchaseLock,
|
|
792
|
+
() => buyLocked(settings, config, wanted, openWallet),
|
|
793
|
+
{ staleMs: REFILL_LOCK_STALE_MS, timeoutMs: REFILL_LOCK_STALE_MS }
|
|
794
|
+
);
|
|
795
|
+
}
|
|
796
|
+
async function buyLocked(settings, config, wanted, openWallet) {
|
|
710
797
|
const usage = await readUsage(settings.home);
|
|
711
798
|
const count = purchasableTokens(config, usage, wanted);
|
|
712
799
|
if (count === 0) return 0;
|
|
@@ -729,10 +816,46 @@ async function buyWithinBudget(settings, config, wanted, openWallet = defaultOpe
|
|
|
729
816
|
});
|
|
730
817
|
return tokens.length;
|
|
731
818
|
}
|
|
732
|
-
|
|
819
|
+
var REFILL_DELAY_MS = { max: 10 * 60 * 1e3, min: 60 * 1e3 };
|
|
820
|
+
async function delayedRefill(base, openWallet = defaultOpenWallet, sleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms)), random = Math.random) {
|
|
821
|
+
if (!await readConfig(base.home)) return 0;
|
|
822
|
+
const release = await tryLock(
|
|
823
|
+
statePaths(base.home).refillWait,
|
|
824
|
+
REFILL_DELAY_MS.max + REFILL_GATEWAY_WAIT_MS + REFILL_LOCK_STALE_MS
|
|
825
|
+
);
|
|
826
|
+
if (!release) return 0;
|
|
827
|
+
try {
|
|
828
|
+
await sleep(
|
|
829
|
+
REFILL_DELAY_MS.min + random() * (REFILL_DELAY_MS.max - REFILL_DELAY_MS.min)
|
|
830
|
+
);
|
|
831
|
+
return await refill(base, openWallet, Date.now, sleep);
|
|
832
|
+
} finally {
|
|
833
|
+
await release();
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
var REFILL_GATEWAY_WAIT_MS = 15 * 60 * 1e3;
|
|
837
|
+
async function refill(base, openWallet = defaultOpenWallet, now = Date.now, sleep) {
|
|
733
838
|
const config = await readConfig(base.home);
|
|
734
839
|
if (!config || config.paused) return 0;
|
|
735
840
|
const settings = settingsFor(base, config);
|
|
841
|
+
const stale = (await storedIssuer(settings.home, settings.gatewayUrl))?.stale ?? true;
|
|
842
|
+
const low = needsRefill(await new FileTokenStore(settings.home).count()) && await linkBackedOff(settings.home, config, now()) === false && await readUsage(settings.home).then(
|
|
843
|
+
(usage) => refillSize(config, usage) > 0,
|
|
844
|
+
() => true
|
|
845
|
+
);
|
|
846
|
+
if (!stale && !low) return 0;
|
|
847
|
+
const bought = await gatewayTurn(
|
|
848
|
+
settings.home,
|
|
849
|
+
async () => {
|
|
850
|
+
if (stale)
|
|
851
|
+
await loadIssuer(settings.home, settings.gatewayUrl).catch(() => void 0);
|
|
852
|
+
return low ? refillLocked(settings, config, openWallet, now) : 0;
|
|
853
|
+
},
|
|
854
|
+
{ maxWaitMs: REFILL_GATEWAY_WAIT_MS, ...sleep ? { sleep } : {} }
|
|
855
|
+
);
|
|
856
|
+
return bought ?? 0;
|
|
857
|
+
}
|
|
858
|
+
async function refillLocked(settings, config, openWallet, now) {
|
|
736
859
|
const release = await tryLock(
|
|
737
860
|
statePaths(settings.home).refillLock,
|
|
738
861
|
REFILL_LOCK_STALE_MS
|
|
@@ -742,8 +865,7 @@ async function refill(base, openWallet = defaultOpenWallet, now = Date.now) {
|
|
|
742
865
|
if (!needsRefill(await new FileTokenStore(settings.home).count())) return 0;
|
|
743
866
|
if (config.wallet !== "link")
|
|
744
867
|
return await buyWithinBudget(settings, config, PACK_SIZE, openWallet);
|
|
745
|
-
|
|
746
|
-
if (failedAt !== void 0 && now() - failedAt < LINK_REFILL_BACKOFF_MS) return 0;
|
|
868
|
+
if (await linkBackedOff(settings.home, config, now())) return 0;
|
|
747
869
|
const path = statePaths(settings.home).linkRefill;
|
|
748
870
|
try {
|
|
749
871
|
const bought = await buyWithinBudget(settings, config, PACK_SIZE, openWallet);
|
|
@@ -757,6 +879,11 @@ async function refill(base, openWallet = defaultOpenWallet, now = Date.now) {
|
|
|
757
879
|
await release();
|
|
758
880
|
}
|
|
759
881
|
}
|
|
882
|
+
async function linkBackedOff(home, config, now) {
|
|
883
|
+
if (config.wallet !== "link") return false;
|
|
884
|
+
const failedAt = await linkRefillFailedAt(home);
|
|
885
|
+
return failedAt !== void 0 && now - failedAt < LINK_REFILL_BACKOFF_MS;
|
|
886
|
+
}
|
|
760
887
|
|
|
761
888
|
// src/render.ts
|
|
762
889
|
function tokensUsd(tokens) {
|
|
@@ -846,6 +973,34 @@ PATRON_PRIVATE_KEY (testnet automation), PATRON_BUDGET_USD (non-interactive setu
|
|
|
846
973
|
PATRON_WALLET=link (pay with Link through link-cli), PATRON_LINK_CLI (its path).`;
|
|
847
974
|
var UsageError = class extends Error {
|
|
848
975
|
};
|
|
976
|
+
var TIP_BATCH = 8;
|
|
977
|
+
var TIP_REDEEM_TIMEOUT_MS = 3e4;
|
|
978
|
+
var TIP_BATCH_LEASE_MS = 6e4;
|
|
979
|
+
async function inGatewayTurn(deps, fn) {
|
|
980
|
+
const result = await gatewayTurn(
|
|
981
|
+
deps.settings.home,
|
|
982
|
+
async () => ({ value: await fn() }),
|
|
983
|
+
{
|
|
984
|
+
...waiting(deps),
|
|
985
|
+
onWait: (ms) => say(
|
|
986
|
+
deps,
|
|
987
|
+
styleText(
|
|
988
|
+
` Waiting ${minutes(ms)} after your agents' last payment, so Patron can't link the two.`,
|
|
989
|
+
"dim",
|
|
990
|
+
deps.color
|
|
991
|
+
)
|
|
992
|
+
)
|
|
993
|
+
}
|
|
994
|
+
);
|
|
995
|
+
return result.value;
|
|
996
|
+
}
|
|
997
|
+
function waiting(deps) {
|
|
998
|
+
return deps.sleep ? { sleep: deps.sleep } : {};
|
|
999
|
+
}
|
|
1000
|
+
function minutes(ms) {
|
|
1001
|
+
const count = Math.max(1, Math.ceil(ms / 6e4));
|
|
1002
|
+
return count === 1 ? "about a minute" : `about ${count} minutes`;
|
|
1003
|
+
}
|
|
849
1004
|
function defaults() {
|
|
850
1005
|
const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
851
1006
|
const color = supportsColor(process.stdout);
|
|
@@ -915,7 +1070,7 @@ async function run(argv, overrides = {}) {
|
|
|
915
1070
|
await uninstall(deps);
|
|
916
1071
|
return 0;
|
|
917
1072
|
case "refill":
|
|
918
|
-
await refill(deps.settings, deps.openWallet).catch(() => 0);
|
|
1073
|
+
await (args[0] === "--delay" ? delayedRefill(deps.settings, deps.openWallet) : refill(deps.settings, deps.openWallet)).catch(() => 0);
|
|
919
1074
|
return 0;
|
|
920
1075
|
case "help":
|
|
921
1076
|
case "--help":
|
|
@@ -1007,10 +1162,13 @@ async function setup(deps) {
|
|
|
1007
1162
|
const settings = deps.settings;
|
|
1008
1163
|
say(deps, scrollBanner(VERSION, deps.color));
|
|
1009
1164
|
say(deps);
|
|
1010
|
-
const issuer = await
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1165
|
+
const issuer = await inGatewayTurn(
|
|
1166
|
+
deps,
|
|
1167
|
+
() => loadIssuer(settings.home, settings.gatewayUrl, {
|
|
1168
|
+
fetch: deps.fetch,
|
|
1169
|
+
fresh: true
|
|
1170
|
+
})
|
|
1171
|
+
).catch((error) => {
|
|
1014
1172
|
throw new Error(`Cannot reach Patron at ${settings.gatewayUrl}: ${error.message}`);
|
|
1015
1173
|
});
|
|
1016
1174
|
const budget = await askBudget(deps, DEFAULT_MONTHLY_BUDGET_USD);
|
|
@@ -1035,7 +1193,10 @@ async function setup(deps) {
|
|
|
1035
1193
|
const starter = config.wallet === "link" ? PACK_SIZE : STARTER_TOKENS;
|
|
1036
1194
|
const first = purchasableTokens(config, await readUsage(settings.home), starter);
|
|
1037
1195
|
step(deps, `Adding ${tokensUsd(first)} to your balance`);
|
|
1038
|
-
const bought = await
|
|
1196
|
+
const bought = await inGatewayTurn(
|
|
1197
|
+
deps,
|
|
1198
|
+
() => buyWithinBudget(settings, config, starter, deps.openWallet)
|
|
1199
|
+
);
|
|
1039
1200
|
done(deps, `${tokensUsd(bought)} ready to pay sites`);
|
|
1040
1201
|
deps.startRefill();
|
|
1041
1202
|
await installStableCli(settings.home).catch(() => void 0);
|
|
@@ -1084,7 +1245,7 @@ async function tempoSignIn(deps, budgetUnits, issuer) {
|
|
|
1084
1245
|
deps,
|
|
1085
1246
|
deps.settings.privateKey ? "Using PATRON_PRIVATE_KEY" : "Signing in with Tempo Wallet"
|
|
1086
1247
|
);
|
|
1087
|
-
const { signIn } = await import("./wallet-
|
|
1248
|
+
const { signIn } = await import("./wallet-OTFEEKIG.js");
|
|
1088
1249
|
const account = await signIn(deps.settings, {
|
|
1089
1250
|
budgetUnits,
|
|
1090
1251
|
issuer,
|
|
@@ -1124,8 +1285,8 @@ async function askBudget(deps, fallback) {
|
|
|
1124
1285
|
const preset = process.env.PATRON_BUDGET_USD;
|
|
1125
1286
|
if (!deps.interactive) {
|
|
1126
1287
|
const value = preset ? parseUsd(preset) : fallback;
|
|
1127
|
-
if (value === void 0 || value
|
|
1128
|
-
throw new UsageError("PATRON_BUDGET_USD must be
|
|
1288
|
+
if (value === void 0 || value < 1e-3)
|
|
1289
|
+
throw new UsageError("PATRON_BUDGET_USD must be at least 0.001.");
|
|
1129
1290
|
return value;
|
|
1130
1291
|
}
|
|
1131
1292
|
const prompter = deps.prompter();
|
|
@@ -1198,7 +1359,7 @@ async function changeBudget(deps, input) {
|
|
|
1198
1359
|
} else budget = await askBudget(deps, config.monthlyBudgetUsd);
|
|
1199
1360
|
if (config.wallet === "tempo" && budget >= config.monthlyBudgetUsd) {
|
|
1200
1361
|
step(deps, "Approve the new spending limit in Tempo Wallet");
|
|
1201
|
-
const { authorizeBudget } = await import("./wallet-
|
|
1362
|
+
const { authorizeBudget } = await import("./wallet-OTFEEKIG.js");
|
|
1202
1363
|
const issuer = await loadIssuer(settings.home, settings.gatewayUrl, {
|
|
1203
1364
|
fetch: deps.fetch
|
|
1204
1365
|
});
|
|
@@ -1254,42 +1415,100 @@ async function tip(deps, url, amount = "0.10") {
|
|
|
1254
1415
|
);
|
|
1255
1416
|
const amount2 = Math.max(missing, purchasableTokens(config, usage, PACK_SIZE));
|
|
1256
1417
|
step(deps, `Adding ${tokensUsd(amount2)} to your balance`);
|
|
1257
|
-
await
|
|
1418
|
+
await inGatewayTurn(
|
|
1419
|
+
deps,
|
|
1420
|
+
() => buyWithinBudget(settings, config, amount2, deps.openWallet)
|
|
1421
|
+
);
|
|
1258
1422
|
}
|
|
1259
|
-
const issuer = await
|
|
1260
|
-
|
|
1423
|
+
const issuer = (await storedIssuer(settings.home, settings.gatewayUrl))?.issuer ?? await inGatewayTurn(
|
|
1424
|
+
deps,
|
|
1425
|
+
() => loadIssuer(settings.home, settings.gatewayUrl, { fetch: deps.fetch, fresh: true })
|
|
1426
|
+
);
|
|
1427
|
+
const redeemFetch = (input, init) => deps.fetch(input, { ...init, signal: AbortSignal.timeout(TIP_REDEEM_TIMEOUT_MS) });
|
|
1428
|
+
let told = false;
|
|
1429
|
+
const claimBatch = (cancelled) => awaitRedemptions(settings.home, TIP_BATCH_LEASE_MS, {
|
|
1430
|
+
...waiting(deps),
|
|
1431
|
+
...cancelled ? { cancelled } : {},
|
|
1432
|
+
onWait: (ms) => {
|
|
1433
|
+
if (told) return;
|
|
1434
|
+
told = true;
|
|
1435
|
+
say(
|
|
1436
|
+
deps,
|
|
1437
|
+
styleText(
|
|
1438
|
+
` Waiting ${minutes(ms)} after a recent purchase, so Patron can't link it to this tip. Ctrl-C keeps your balance.`,
|
|
1439
|
+
"dim",
|
|
1440
|
+
deps.color
|
|
1441
|
+
)
|
|
1442
|
+
);
|
|
1443
|
+
}
|
|
1261
1444
|
});
|
|
1262
|
-
|
|
1445
|
+
await claimBatch();
|
|
1446
|
+
let stopped = false;
|
|
1447
|
+
const stop = () => {
|
|
1448
|
+
stopped = true;
|
|
1449
|
+
process.off("SIGINT", stop);
|
|
1450
|
+
process.off("SIGTERM", stop);
|
|
1451
|
+
};
|
|
1452
|
+
process.once("SIGINT", stop);
|
|
1453
|
+
process.once("SIGTERM", stop);
|
|
1263
1454
|
let paid = 0;
|
|
1455
|
+
let sent = 0;
|
|
1264
1456
|
let rejection;
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
batch.
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
token
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1457
|
+
try {
|
|
1458
|
+
while (sent < count && !stopped) {
|
|
1459
|
+
if (sent > 0 && !await claimBatch(() => stopped)) break;
|
|
1460
|
+
const batch = await store.takeMany(Math.min(TIP_BATCH, count - sent));
|
|
1461
|
+
if (batch.length === 0) break;
|
|
1462
|
+
sent += batch.length;
|
|
1463
|
+
const unsent = [];
|
|
1464
|
+
let rejected = 0;
|
|
1465
|
+
const results = await Promise.all(
|
|
1466
|
+
batch.map(
|
|
1467
|
+
(token) => redeem({
|
|
1468
|
+
domain,
|
|
1469
|
+
fetch: redeemFetch,
|
|
1470
|
+
issuer,
|
|
1471
|
+
relayUrl: settings.relayUrl,
|
|
1472
|
+
token
|
|
1473
|
+
}).catch(() => {
|
|
1474
|
+
unsent.push(token);
|
|
1475
|
+
return void 0;
|
|
1476
|
+
})
|
|
1477
|
+
)
|
|
1478
|
+
);
|
|
1479
|
+
const ok = results.filter((result) => result?.ok).length;
|
|
1480
|
+
for (const result of results)
|
|
1481
|
+
if (result && !result.ok) {
|
|
1482
|
+
rejected += 1;
|
|
1483
|
+
rejection = result.error ?? "rejected";
|
|
1484
|
+
}
|
|
1485
|
+
sent -= unsent.length;
|
|
1486
|
+
await store.add(unsent);
|
|
1487
|
+
if (ok > 0)
|
|
1488
|
+
await updateUsage(settings.home, (usage) => {
|
|
1489
|
+
usage.domains[domain] = (usage.domains[domain] ?? 0) + ok;
|
|
1490
|
+
usage.tippedTokens += ok;
|
|
1491
|
+
});
|
|
1492
|
+
paid += ok;
|
|
1493
|
+
if (count > TIP_BATCH && ok > 0)
|
|
1494
|
+
say(
|
|
1495
|
+
deps,
|
|
1496
|
+
styleText(
|
|
1497
|
+
` ${tokensUsd(paid)} of ${tokensUsd(count)} sent`,
|
|
1498
|
+
"dim",
|
|
1499
|
+
deps.color
|
|
1500
|
+
)
|
|
1501
|
+
);
|
|
1502
|
+
if (ok === 0 && (rejected > 0 || unsent.length > 0)) break;
|
|
1285
1503
|
}
|
|
1504
|
+
} finally {
|
|
1505
|
+
process.off("SIGINT", stop);
|
|
1506
|
+
process.off("SIGTERM", stop);
|
|
1286
1507
|
}
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
usage.tippedTokens += paid;
|
|
1292
|
-
});
|
|
1508
|
+
if (stopped && sent < count)
|
|
1509
|
+
throw new Error(
|
|
1510
|
+
`Tip stopped: tipped ${domain} ${tokensUsd(paid)} of ${tokensUsd(count)}; the rest stays in your balance.`
|
|
1511
|
+
);
|
|
1293
1512
|
if (paid === 0)
|
|
1294
1513
|
throw new Error(
|
|
1295
1514
|
rejection ? `Patron rejected the tip to ${domain}: ${rejection}.` : `Could not reach Patron to tip ${domain}; your balance was kept.`
|
|
@@ -1316,7 +1535,7 @@ async function setName(deps, name) {
|
|
|
1316
1535
|
const issuer = await loadIssuer(settings.home, settings.gatewayUrl, {
|
|
1317
1536
|
fetch: deps.fetch
|
|
1318
1537
|
});
|
|
1319
|
-
const open = deps.openWallet ?? (async (s, c, i, max) => (await import("./wallet-
|
|
1538
|
+
const open = deps.openWallet ?? (async (s, c, i, max) => (await import("./wallet-OTFEEKIG.js")).openWallet(s, c.wallet, i, max));
|
|
1320
1539
|
const wallet = await open(settings, config, issuer, 0n);
|
|
1321
1540
|
await setProfile(settings.gatewayUrl, wallet, name, deps.fetch);
|
|
1322
1541
|
await writeConfig(settings.home, { ...config, name });
|
|
@@ -1324,9 +1543,11 @@ async function setName(deps, name) {
|
|
|
1324
1543
|
}
|
|
1325
1544
|
async function uninstall(deps) {
|
|
1326
1545
|
const config = await readConfig(deps.settings.home);
|
|
1327
|
-
const
|
|
1328
|
-
|
|
1329
|
-
|
|
1546
|
+
const removed = await removeAgents(deps.agents);
|
|
1547
|
+
const results = removed.filter((result) => result.state !== "missing");
|
|
1548
|
+
const notFound = removed.filter(
|
|
1549
|
+
(result) => result.state === "missing" && config?.agents.includes(result.agent)
|
|
1550
|
+
).map((result) => result.label);
|
|
1330
1551
|
say(deps, styleText("Patron uninstall", "bold", deps.color));
|
|
1331
1552
|
for (const line of formatAgentResults(results, deps.color)) say(deps, line);
|
|
1332
1553
|
if (results.length === 0)
|
|
@@ -1358,6 +1579,10 @@ async function uninstall(deps) {
|
|
|
1358
1579
|
deps.color
|
|
1359
1580
|
)
|
|
1360
1581
|
);
|
|
1582
|
+
if (notFound.length > 0)
|
|
1583
|
+
throw new Error(
|
|
1584
|
+
`Found no Patron hook to remove for ${notFound.join(", ")}. If a hook running "patron hook" is still in its settings, delete it by hand.`
|
|
1585
|
+
);
|
|
1361
1586
|
}
|
|
1362
1587
|
function readVersion() {
|
|
1363
1588
|
for (const path of ["../package.json", "../../package.json"]) {
|