paytaca-cli 0.6.1 → 0.7.0

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.
Files changed (130) hide show
  1. package/README.md +153 -37
  2. package/dist/ai/autoRefill.d.ts +27 -0
  3. package/dist/ai/autoRefill.d.ts.map +1 -0
  4. package/dist/ai/autoRefill.js +75 -0
  5. package/dist/ai/autoRefill.js.map +1 -0
  6. package/dist/ai/client.d.ts +73 -0
  7. package/dist/ai/client.d.ts.map +1 -0
  8. package/dist/ai/client.js +90 -0
  9. package/dist/ai/client.js.map +1 -0
  10. package/dist/ai/config.d.ts +7 -0
  11. package/dist/ai/config.d.ts.map +1 -0
  12. package/dist/ai/config.js +16 -0
  13. package/dist/ai/config.js.map +1 -0
  14. package/dist/ai/configure.d.ts +61 -0
  15. package/dist/ai/configure.d.ts.map +1 -0
  16. package/dist/ai/configure.js +452 -0
  17. package/dist/ai/configure.js.map +1 -0
  18. package/dist/ai/credits.d.ts +27 -0
  19. package/dist/ai/credits.d.ts.map +1 -0
  20. package/dist/ai/credits.js +99 -0
  21. package/dist/ai/credits.js.map +1 -0
  22. package/dist/ai/models.d.ts +13 -0
  23. package/dist/ai/models.d.ts.map +1 -0
  24. package/dist/ai/models.js +49 -0
  25. package/dist/ai/models.js.map +1 -0
  26. package/dist/ai/oauth.d.ts +45 -0
  27. package/dist/ai/oauth.d.ts.map +1 -0
  28. package/dist/ai/oauth.js +137 -0
  29. package/dist/ai/oauth.js.map +1 -0
  30. package/dist/ai/purchase.d.ts +30 -0
  31. package/dist/ai/purchase.d.ts.map +1 -0
  32. package/dist/ai/purchase.js +456 -0
  33. package/dist/ai/purchase.js.map +1 -0
  34. package/dist/commands/address.d.ts.map +1 -1
  35. package/dist/commands/address.js +88 -36
  36. package/dist/commands/address.js.map +1 -1
  37. package/dist/commands/ai.d.ts +9 -0
  38. package/dist/commands/ai.d.ts.map +1 -0
  39. package/dist/commands/ai.js +675 -0
  40. package/dist/commands/ai.js.map +1 -0
  41. package/dist/commands/balance.d.ts.map +1 -1
  42. package/dist/commands/balance.js +68 -129
  43. package/dist/commands/balance.js.map +1 -1
  44. package/dist/commands/history.d.ts.map +1 -1
  45. package/dist/commands/history.js +38 -78
  46. package/dist/commands/history.js.map +1 -1
  47. package/dist/commands/mcp.d.ts +14 -0
  48. package/dist/commands/mcp.d.ts.map +1 -0
  49. package/dist/commands/mcp.js +68 -0
  50. package/dist/commands/mcp.js.map +1 -0
  51. package/dist/commands/pay.js +1 -1
  52. package/dist/commands/pay.js.map +1 -1
  53. package/dist/commands/receive.d.ts +1 -8
  54. package/dist/commands/receive.d.ts.map +1 -1
  55. package/dist/commands/receive.js +51 -124
  56. package/dist/commands/receive.js.map +1 -1
  57. package/dist/commands/send.d.ts +0 -10
  58. package/dist/commands/send.d.ts.map +1 -1
  59. package/dist/commands/send.js +40 -50
  60. package/dist/commands/send.js.map +1 -1
  61. package/dist/commands/swap.js +1 -1
  62. package/dist/commands/swap.js.map +1 -1
  63. package/dist/commands/token.d.ts +1 -9
  64. package/dist/commands/token.d.ts.map +1 -1
  65. package/dist/commands/token.js +192 -289
  66. package/dist/commands/token.js.map +1 -1
  67. package/dist/core/context.d.ts +30 -0
  68. package/dist/core/context.d.ts.map +1 -0
  69. package/dist/core/context.js +45 -0
  70. package/dist/core/context.js.map +1 -0
  71. package/dist/core/wallet.d.ts +130 -0
  72. package/dist/core/wallet.d.ts.map +1 -0
  73. package/dist/core/wallet.js +276 -0
  74. package/dist/core/wallet.js.map +1 -0
  75. package/dist/index.js +4 -0
  76. package/dist/index.js.map +1 -1
  77. package/dist/mcp/server.d.ts +8 -0
  78. package/dist/mcp/server.d.ts.map +1 -0
  79. package/dist/mcp/server.js +24 -0
  80. package/dist/mcp/server.js.map +1 -0
  81. package/dist/mcp/tools.d.ts +5 -0
  82. package/dist/mcp/tools.d.ts.map +1 -0
  83. package/dist/mcp/tools.js +473 -0
  84. package/dist/mcp/tools.js.map +1 -0
  85. package/dist/utils/format.d.ts +23 -0
  86. package/dist/utils/format.d.ts.map +1 -0
  87. package/dist/utils/format.js +62 -0
  88. package/dist/utils/format.js.map +1 -0
  89. package/dist/utils/x402.js +1 -1
  90. package/dist/utils/x402.js.map +1 -1
  91. package/dist/wallet/index.d.ts +13 -0
  92. package/dist/wallet/index.d.ts.map +1 -1
  93. package/dist/wallet/index.js +18 -0
  94. package/dist/wallet/index.js.map +1 -1
  95. package/dist/wallet/keys.d.ts.map +1 -1
  96. package/dist/wallet/keys.js +2 -0
  97. package/dist/wallet/keys.js.map +1 -1
  98. package/package.json +7 -3
  99. package/dist/commands/wallet.test.d.ts +0 -2
  100. package/dist/commands/wallet.test.d.ts.map +0 -1
  101. package/dist/commands/wallet.test.js +0 -68
  102. package/dist/commands/wallet.test.js.map +0 -1
  103. package/dist/nostr/chat.test.d.ts +0 -2
  104. package/dist/nostr/chat.test.d.ts.map +0 -1
  105. package/dist/nostr/chat.test.js +0 -121
  106. package/dist/nostr/chat.test.js.map +0 -1
  107. package/dist/nostr/store.test.d.ts +0 -2
  108. package/dist/nostr/store.test.d.ts.map +0 -1
  109. package/dist/nostr/store.test.js +0 -456
  110. package/dist/nostr/store.test.js.map +0 -1
  111. package/dist/utils/prices.test.d.ts +0 -5
  112. package/dist/utils/prices.test.d.ts.map +0 -1
  113. package/dist/utils/prices.test.js +0 -62
  114. package/dist/utils/prices.test.js.map +0 -1
  115. package/dist/utils/x402.test.d.ts +0 -2
  116. package/dist/utils/x402.test.d.ts.map +0 -1
  117. package/dist/utils/x402.test.js +0 -216
  118. package/dist/utils/x402.test.js.map +0 -1
  119. package/dist/wallet/cauldron/pools.test.d.ts +0 -3
  120. package/dist/wallet/cauldron/pools.test.d.ts.map +0 -1
  121. package/dist/wallet/cauldron/pools.test.js +0 -149
  122. package/dist/wallet/cauldron/pools.test.js.map +0 -1
  123. package/dist/wallet/cauldron/swap.test.d.ts +0 -2
  124. package/dist/wallet/cauldron/swap.test.d.ts.map +0 -1
  125. package/dist/wallet/cauldron/swap.test.js +0 -179
  126. package/dist/wallet/cauldron/swap.test.js.map +0 -1
  127. package/skills/README.md +0 -68
  128. package/skills/paytaca-send/SKILL.md +0 -159
  129. package/skills/paytaca-wallet/SKILL.md +0 -164
  130. package/skills/paytaca-x402/SKILL.md +0 -130
@@ -0,0 +1,675 @@
1
+ /**
2
+ * CLI commands: ai
3
+ *
4
+ * Paytaca AI integration: model catalogue, plan pricing, credits, plan
5
+ * purchase via x402 (BCH or LIFT), non-streaming chat, and auto-refill.
6
+ */
7
+ import chalk from 'chalk';
8
+ import readline from 'readline';
9
+ import { WalletNotConfiguredError } from '../core/context.js';
10
+ import { loadWalletRef } from '../wallet/index.js';
11
+ import { getBalanceView, getTokenBalances } from '../core/wallet.js';
12
+ import { formatSats } from '../utils/format.js';
13
+ import { formatUsd } from '../utils/prices.js';
14
+ import { getConfig, getWalletStatus, } from '../ai/client.js';
15
+ import { LIFT_TOKEN_ID } from '../ai/config.js';
16
+ import { listPlans, selectModel, selectTier, formatDuration, formatPriceUsd, } from '../ai/models.js';
17
+ import { summarizeCredits, summarizeAllCredits, getSessions, formatRemaining, } from '../ai/credits.js';
18
+ import { buyPlan } from '../ai/purchase.js';
19
+ import { provisionApiKey } from '../ai/oauth.js';
20
+ import { configureHarness } from '../ai/configure.js';
21
+ import { CLIENTS } from './mcp.js';
22
+ import { armAutoRefill, disarmAutoRefill, readAutoRefillState, remainingBudget, } from '../ai/autoRefill.js';
23
+ function outputJson(value) {
24
+ console.log(JSON.stringify(value, null, 2));
25
+ }
26
+ async function loadConfigOrExit(opts) {
27
+ try {
28
+ return await getConfig({ backendUrl: opts.backend });
29
+ }
30
+ catch (err) {
31
+ const message = err?.message || String(err);
32
+ if (opts.json)
33
+ outputJson({ error: message });
34
+ else
35
+ console.log(chalk.red(`\nError: ${message}\n`));
36
+ process.exitCode = 1;
37
+ return null;
38
+ }
39
+ }
40
+ async function promptConfirmation(message) {
41
+ const rl = readline.createInterface({
42
+ input: process.stdin,
43
+ output: process.stdout,
44
+ });
45
+ return new Promise((resolve) => {
46
+ rl.question(chalk.bold(`\n ${message} (y/N): `), (answer) => {
47
+ rl.close();
48
+ resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
49
+ });
50
+ });
51
+ }
52
+ function promptSecret(message) {
53
+ if (!process.stdin.isTTY) {
54
+ return Promise.reject(new Error('A terminal is required to enter the API key.'));
55
+ }
56
+ process.stdout.write(chalk.bold(`\n ${message}: `));
57
+ const stdin = process.stdin;
58
+ stdin.setRawMode?.(true);
59
+ stdin.resume();
60
+ stdin.setEncoding('utf8');
61
+ return new Promise((resolve) => {
62
+ let value = '';
63
+ const finish = (result) => {
64
+ stdin.removeListener('data', onData);
65
+ stdin.removeListener('end', onEnd);
66
+ stdin.setRawMode?.(false);
67
+ stdin.pause();
68
+ process.stdout.write('\n');
69
+ resolve(result);
70
+ };
71
+ const onData = (chunk) => {
72
+ for (const char of chunk) {
73
+ if (char === '\r' || char === '\n') {
74
+ finish(value);
75
+ return;
76
+ }
77
+ if (char === '\u0003') {
78
+ finish('');
79
+ process.exit(130);
80
+ }
81
+ if (char === '\u007f' || char === '\b') {
82
+ value = value.slice(0, -1);
83
+ continue;
84
+ }
85
+ value += char;
86
+ }
87
+ };
88
+ const onEnd = () => finish(value);
89
+ stdin.on('data', onData);
90
+ stdin.on('end', onEnd);
91
+ });
92
+ }
93
+ function printPlans(config, modelQuery) {
94
+ const models = listPlans(config, modelQuery);
95
+ if (models.length === 0) {
96
+ console.log(chalk.dim('\n No plans found.\n'));
97
+ return;
98
+ }
99
+ for (const model of models) {
100
+ console.log(` ${chalk.bold(model.displayName)} ${chalk.dim(`(${model.id})`)}`);
101
+ for (const plan of model.plans) {
102
+ const price = formatPriceUsd(plan.price_usd);
103
+ const sats = `${formatSats(Number(plan.price_sats) || 0)} sats`;
104
+ console.log(` ${formatDuration(plan.minutes).padEnd(8)} ${price.padEnd(8)} ${chalk.dim(sats)}`);
105
+ }
106
+ console.log();
107
+ }
108
+ }
109
+ export function registerAiCommands(program) {
110
+ const ai = program.command('ai').description('Paytaca AI: models, plans, credits, purchase');
111
+ // ── ai configure ────────────────────────────────────────────────────
112
+ ai.command('configure')
113
+ .description('Install the Paytaca MCP + AI provider into an AI harness')
114
+ .argument('[harness]', `Target harness: ${CLIENTS.join(', ')}`, 'opencode')
115
+ .option('--backend <url>', 'Override backend URL')
116
+ .option('--chipnet', 'Use chipnet (testnet) instead of mainnet')
117
+ .option('--path <path>', 'Override the target config file path')
118
+ .option('--api-key <key>', 'Use an existing Paytaca API key (required for read-only wallets)')
119
+ .option('-y, --yes', 'Buy a plan without prompting when no credits are active')
120
+ .option('--json', 'Output as JSON')
121
+ .action(async (harnessArg, opts) => {
122
+ const harness = String(harnessArg || 'opencode').toLowerCase();
123
+ const json = Boolean(opts.json);
124
+ const interactive = !json && Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY);
125
+ const requestApiKey = async () => {
126
+ console.log(chalk.yellow('\n This wallet is read-only; an API key is required.'));
127
+ console.log(chalk.dim(' Create one from your full wallet: paytaca ai api-key create'));
128
+ const entered = await promptSecret('Paste your Paytaca API key');
129
+ return entered.trim() || null;
130
+ };
131
+ let result;
132
+ try {
133
+ result = await configureHarness({
134
+ harness,
135
+ chipnet: Boolean(opts.chipnet),
136
+ backendUrl: opts.backend,
137
+ path: opts.path,
138
+ apiKey: opts.apiKey,
139
+ requestApiKey: interactive ? requestApiKey : undefined,
140
+ });
141
+ }
142
+ catch (err) {
143
+ if (json)
144
+ outputJson({ error: err.message });
145
+ else
146
+ console.log(chalk.red(`\nError: ${err.message}\n`));
147
+ process.exitCode = 1;
148
+ return;
149
+ }
150
+ let purchased = null;
151
+ if (!json && result.canSign && !result.creditsActive && result.planSuggestion) {
152
+ const plan = result.planSuggestion;
153
+ const proceed = Boolean(opts.yes) ||
154
+ (await promptConfirmation(`No active AI credits. Buy a ${plan.minutes}-minute plan for ${plan.displayName} (${formatPriceUsd(plan.priceUsd)})?`));
155
+ if (proceed) {
156
+ purchased = await buyPlan({
157
+ model: plan.modelId,
158
+ minutes: plan.minutes,
159
+ paymentMethod: 'bch',
160
+ isChipnet: Boolean(opts.chipnet),
161
+ backendUrl: opts.backend,
162
+ confirmed: true,
163
+ });
164
+ }
165
+ }
166
+ if (json) {
167
+ outputJson({ ...result, purchased });
168
+ if (!result.mcpInstalled || result.providerError)
169
+ process.exitCode = 1;
170
+ return;
171
+ }
172
+ console.log(chalk.bold(`\n Paytaca AI — ${result.harness}\n`));
173
+ if (result.path)
174
+ console.log(` Config: ${result.path}`);
175
+ console.log(` MCP: ${result.mcpInstalled ? chalk.green('configured') : chalk.red('failed')}`);
176
+ if (result.harness === 'opencode' || result.harness === 'pi' || result.harness === 'omp') {
177
+ if (result.providerInstalled) {
178
+ const how = result.apiKeyReused
179
+ ? 'existing API key'
180
+ : result.apiKeyProvided
181
+ ? 'provided API key'
182
+ : 'new API key';
183
+ const suffix = result.apiKeyPrefix ? `, ${result.apiKeyPrefix}…` : '';
184
+ console.log(` Provider: ${chalk.green('configured')} ${chalk.dim(`(${how}${suffix})`)}`);
185
+ if (result.models.length > 0) {
186
+ console.log(chalk.dim(` Models: ${result.models.length} available — use \`paytaca-ai/<model-id>\``));
187
+ }
188
+ }
189
+ else {
190
+ const detail = result.providerError ? chalk.dim(` (${result.providerError})`) : '';
191
+ console.log(` Provider: ${chalk.yellow('not configured')}${detail}`);
192
+ }
193
+ }
194
+ for (const warning of result.warnings || []) {
195
+ console.log(chalk.yellow(` Warning: ${warning}`));
196
+ }
197
+ if (result.creditsActive) {
198
+ console.log(chalk.green('\n AI credits active — ready to use.'));
199
+ }
200
+ else if (purchased?.paid) {
201
+ console.log(chalk.green(`\n Plan purchased: ${purchased.displayName || purchased.model} — ${purchased.minutes} minutes.`));
202
+ if (purchased.txid)
203
+ console.log(chalk.dim(` txid: ${purchased.txid}`));
204
+ }
205
+ else if (purchased && !purchased.success) {
206
+ console.log(chalk.red(`\n Purchase failed: ${purchased.error || 'unknown error'}`));
207
+ }
208
+ else if (!result.canSign) {
209
+ console.log(chalk.yellow('\n No active AI credits.'));
210
+ if (result.planSuggestion) {
211
+ console.log(chalk.dim(` Fund from your full wallet: paytaca ai purchase --model ${result.planSuggestion.modelId} --minutes ${result.planSuggestion.minutes}`));
212
+ }
213
+ }
214
+ else if (result.planSuggestion) {
215
+ console.log(chalk.yellow('\n No active AI credits.'));
216
+ console.log(chalk.dim(` Buy a plan: paytaca ai purchase --model ${result.planSuggestion.modelId} --minutes ${result.planSuggestion.minutes}`));
217
+ }
218
+ if (result.harness === 'pi') {
219
+ console.log(chalk.yellow.bold('\n Pi requires the pi-mcp-adapter extension.'));
220
+ console.log(chalk.yellow('\n Install it:'));
221
+ console.log(chalk.cyan.bold('\n pi install npm:pi-mcp-adapter\n'));
222
+ console.log(chalk.yellow(' Then restart Pi and verify it loaded with ') +
223
+ chalk.bold.cyan('pi list'));
224
+ }
225
+ else {
226
+ console.log(chalk.dim('\n Restart your AI harness to load the new configuration.\n'));
227
+ }
228
+ if (!result.mcpInstalled)
229
+ process.exitCode = 1;
230
+ });
231
+ // ── ai api-key ──────────────────────────────────────────────────────
232
+ const apiKey = ai.command('api-key').description('Manage Paytaca AI API keys');
233
+ apiKey
234
+ .command('create')
235
+ .description('Create an API key bound to your wallet (requires a full wallet)')
236
+ .option('--name <name>', 'Label for the key', 'paytaca-cli')
237
+ .option('--backend <url>', 'Override backend URL')
238
+ .option('--json', 'Output as JSON')
239
+ .action(async (opts) => {
240
+ const wallet = loadWalletRef();
241
+ if (!wallet) {
242
+ const message = 'No wallet found. Run `paytaca wallet create` or `paytaca wallet import` first.';
243
+ if (opts.json)
244
+ outputJson({ error: message });
245
+ else
246
+ console.log(chalk.red(`\nError: ${message}\n`));
247
+ process.exitCode = 1;
248
+ return;
249
+ }
250
+ if (!wallet.canSign || !wallet.mnemonic) {
251
+ const message = 'This wallet is read-only. Create an API key from your full wallet.';
252
+ if (opts.json)
253
+ outputJson({ error: message });
254
+ else
255
+ console.log(chalk.red(`\nError: ${message}\n`));
256
+ process.exitCode = 1;
257
+ return;
258
+ }
259
+ try {
260
+ const created = await provisionApiKey({
261
+ mnemonic: wallet.mnemonic,
262
+ walletHash: wallet.walletHash,
263
+ backendUrl: opts.backend,
264
+ name: opts.name,
265
+ });
266
+ if (opts.json) {
267
+ outputJson(created);
268
+ return;
269
+ }
270
+ console.log(chalk.bold('\n Paytaca AI API key created\n'));
271
+ console.log(` Name: ${created.name}`);
272
+ console.log(` Prefix: ${created.keyPrefix}`);
273
+ console.log(` API key: ${chalk.bold(created.key)}`);
274
+ console.log(chalk.yellow('\n Copy this key now — it will not be shown again.'));
275
+ console.log(chalk.dim(' Then configure a harness: paytaca ai configure opencode --api-key <key>\n'));
276
+ }
277
+ catch (err) {
278
+ if (opts.json)
279
+ outputJson({ error: err.message });
280
+ else
281
+ console.log(chalk.red(`\nError: ${err.message}\n`));
282
+ process.exitCode = 1;
283
+ }
284
+ });
285
+ // ── ai models ───────────────────────────────────────────────────────
286
+ ai.command('models')
287
+ .description('List available AI models')
288
+ .option('--backend <url>', 'Override backend URL')
289
+ .option('--json', 'Output as JSON')
290
+ .action(async (opts) => {
291
+ const config = await loadConfigOrExit(opts);
292
+ if (!config)
293
+ return;
294
+ const models = (config.models || []).map((m) => ({
295
+ id: m.id,
296
+ displayName: m.display_name,
297
+ plans: m.price_tiers || [],
298
+ }));
299
+ if (opts.json) {
300
+ outputJson({ defaultModel: config.default_model, models });
301
+ return;
302
+ }
303
+ console.log(chalk.bold('\n AI Models\n'));
304
+ for (const m of models) {
305
+ console.log(` ${chalk.bold(m.displayName)} ${chalk.dim(`(${m.id})`)}`);
306
+ if (m.plans.length > 0) {
307
+ const cheapest = m.plans.reduce((a, b) => a.price_usd !== undefined && (b.price_usd === undefined || a.price_usd <= b.price_usd) ? a : b);
308
+ console.log(chalk.dim(` from ${formatPriceUsd(cheapest.price_usd)} · ${m.plans.length} plans`));
309
+ }
310
+ }
311
+ console.log();
312
+ });
313
+ // ── ai plans ────────────────────────────────────────────────────────
314
+ ai.command('plans')
315
+ .description('Show plan pricing for AI models')
316
+ .argument('[model]', 'Filter to a single model id or display name')
317
+ .option('--backend <url>', 'Override backend URL')
318
+ .option('--json', 'Output as JSON')
319
+ .action(async (model, opts) => {
320
+ const config = await loadConfigOrExit(opts);
321
+ if (!config)
322
+ return;
323
+ const models = listPlans(config, model);
324
+ if (opts.json) {
325
+ outputJson({
326
+ discountPercent: config.lift_payment_discount_percent ?? 0,
327
+ models,
328
+ });
329
+ return;
330
+ }
331
+ console.log(chalk.bold(`\n AI Plans${model ? ` — ${model}` : ''}\n`));
332
+ printPlans(config, model);
333
+ const discount = config.lift_payment_discount_percent;
334
+ if (discount) {
335
+ console.log(chalk.green(` Pay with LIFT for a ${discount}% discount.\n`));
336
+ }
337
+ });
338
+ // ── ai credits ──────────────────────────────────────────────────────
339
+ ai.command('credits')
340
+ .description('Show remaining AI time credits')
341
+ .option('--model <id>', 'Filter to a single model')
342
+ .option('--backend <url>', 'Override backend URL')
343
+ .option('--chipnet', 'Use chipnet (testnet) instead of mainnet')
344
+ .option('--json', 'Output as JSON')
345
+ .action(async (opts) => {
346
+ const wallet = loadWalletRef();
347
+ if (!wallet) {
348
+ const err = new WalletNotConfiguredError();
349
+ if (opts.json)
350
+ outputJson({ error: err.message });
351
+ else
352
+ console.log(chalk.red(`\n${err.message}\n`));
353
+ process.exitCode = 1;
354
+ return;
355
+ }
356
+ const walletHash = wallet.walletHash;
357
+ try {
358
+ const status = await getWalletStatus(walletHash, {
359
+ backendUrl: opts.backend,
360
+ modelId: opts.model,
361
+ });
362
+ if (opts.json) {
363
+ if (opts.model) {
364
+ outputJson({ walletHash, ...summarizeCredits(status, opts.model) });
365
+ }
366
+ else {
367
+ outputJson({ walletHash, sessions: summarizeAllCredits(status) });
368
+ }
369
+ return;
370
+ }
371
+ const summaries = opts.model
372
+ ? [summarizeCredits(status, opts.model)]
373
+ : summarizeAllCredits(status);
374
+ console.log(chalk.bold('\n AI Credits\n'));
375
+ if (summaries.length === 0) {
376
+ console.log(chalk.dim(' No sessions found.\n'));
377
+ return;
378
+ }
379
+ for (const summary of summaries) {
380
+ console.log(` ${chalk.bold(summary.displayName || summary.modelId || 'unknown')}`);
381
+ console.log(` Model: ${summary.modelId || '(unknown)'}`);
382
+ console.log(` Remaining: ${formatRemaining(summary.timeRemainingSeconds)}`);
383
+ console.log(` Used: ${formatRemaining(summary.timeUsedSeconds)}`);
384
+ console.log(` Total: ${formatRemaining(summary.timeCreditsSeconds)}`);
385
+ console.log(` Status: ${summary.active ? chalk.green('active') : chalk.dim('inactive')}`);
386
+ console.log();
387
+ }
388
+ }
389
+ catch (err) {
390
+ if (opts.json)
391
+ outputJson({ error: err.message });
392
+ else
393
+ console.log(chalk.red(`\nError: ${err.message}\n`));
394
+ process.exitCode = 1;
395
+ }
396
+ });
397
+ // ── ai usage ────────────────────────────────────────────────────────
398
+ ai.command('usage')
399
+ .description('Show per-session AI usage')
400
+ .option('--backend <url>', 'Override backend URL')
401
+ .option('--chipnet', 'Use chipnet (testnet) instead of mainnet')
402
+ .option('--json', 'Output as JSON')
403
+ .action(async (opts) => {
404
+ const wallet = loadWalletRef();
405
+ if (!wallet) {
406
+ const err = new WalletNotConfiguredError();
407
+ if (opts.json)
408
+ outputJson({ error: err.message });
409
+ else
410
+ console.log(chalk.red(`\n${err.message}\n`));
411
+ process.exitCode = 1;
412
+ return;
413
+ }
414
+ const walletHash = wallet.walletHash;
415
+ try {
416
+ const status = await getWalletStatus(walletHash, { backendUrl: opts.backend });
417
+ const sessions = getSessions(status).map((s) => ({
418
+ model: s.model_id || s.ai_model || null,
419
+ displayName: s.display_name || null,
420
+ active: s.model_active === true || s.session_active === true,
421
+ remainingSeconds: s.time_remaining_seconds ?? 0,
422
+ usedSeconds: s.time_used_seconds ?? 0,
423
+ creditsSeconds: s.time_credits_seconds ?? 0,
424
+ }));
425
+ if (opts.json) {
426
+ outputJson({ walletHash, sessions });
427
+ return;
428
+ }
429
+ console.log(chalk.bold('\n AI Usage\n'));
430
+ if (sessions.length === 0) {
431
+ console.log(chalk.dim(' No sessions found.\n'));
432
+ return;
433
+ }
434
+ for (const s of sessions) {
435
+ console.log(` ${chalk.bold(s.displayName || s.model || 'unknown')}`);
436
+ console.log(chalk.dim(` used ${formatRemaining(s.usedSeconds)} · remaining ${formatRemaining(s.remainingSeconds)} of ${formatRemaining(s.creditsSeconds)}`));
437
+ console.log();
438
+ }
439
+ }
440
+ catch (err) {
441
+ if (opts.json)
442
+ outputJson({ error: err.message });
443
+ else
444
+ console.log(chalk.red(`\nError: ${err.message}\n`));
445
+ process.exitCode = 1;
446
+ }
447
+ });
448
+ // ── ai balance ──────────────────────────────────────────────────────
449
+ ai.command('balance')
450
+ .description('Show wallet balance available for AI plan purchases (BCH + LIFT)')
451
+ .option('--chipnet', 'Use chipnet (testnet) instead of mainnet')
452
+ .option('--json', 'Output as JSON')
453
+ .action(async (opts) => {
454
+ const isChipnet = Boolean(opts.chipnet);
455
+ try {
456
+ const balance = await getBalanceView(isChipnet);
457
+ const tokens = await getTokenBalances(isChipnet);
458
+ const lift = tokens.tokens.find((t) => t.category === LIFT_TOKEN_ID);
459
+ if (opts.json) {
460
+ outputJson({
461
+ network: balance.network,
462
+ bch: {
463
+ balance: balance.balanceBch,
464
+ spendable: balance.spendableBch,
465
+ sats: balance.spendableSats,
466
+ usd: balance.usd,
467
+ },
468
+ lift: lift
469
+ ? {
470
+ category: lift.category,
471
+ symbol: lift.symbol,
472
+ displayBalance: lift.displayBalance,
473
+ rawBalance: lift.rawBalance,
474
+ }
475
+ : null,
476
+ });
477
+ return;
478
+ }
479
+ console.log(chalk.bold(`\n Funds for AI Plans (${balance.network})\n`));
480
+ console.log(` BCH: ${balance.spendableBch} BCH ${chalk.dim(`(${formatSats(balance.spendableSats)} sats)`)}`);
481
+ if (balance.usd !== null) {
482
+ console.log(chalk.dim(` ≈ ${formatUsd(balance.usd)}`));
483
+ }
484
+ if (lift) {
485
+ console.log(` LIFT: ${lift.displayBalance} ${lift.symbol || 'LIFT'}`);
486
+ }
487
+ else {
488
+ console.log(chalk.dim(' LIFT: none'));
489
+ }
490
+ console.log();
491
+ }
492
+ catch (err) {
493
+ if (opts.json)
494
+ outputJson({ error: err.message });
495
+ else
496
+ console.log(chalk.red(`\nError: ${err.message}\n`));
497
+ process.exitCode = 1;
498
+ }
499
+ });
500
+ // ── ai purchase ─────────────────────────────────────────────────────
501
+ ai.command('purchase')
502
+ .description('Purchase an AI plan (x402 payment with BCH or LIFT)')
503
+ .requiredOption('--model <model>', 'Model id or display name')
504
+ .requiredOption('--minutes <minutes>', 'Plan duration in minutes')
505
+ .option('--lift', 'Pay with LIFT tokens (discount) instead of BCH')
506
+ .option('--chipnet', 'Use chipnet (testnet) instead of mainnet')
507
+ .option('--backend <url>', 'Override backend URL')
508
+ .option('-y, --yes', 'Skip confirmation prompt')
509
+ .option('--json', 'Output as JSON')
510
+ .action(async (opts) => {
511
+ const paymentMethod = opts.lift ? 'lift' : 'bch';
512
+ const minutes = Number(opts.minutes);
513
+ const json = Boolean(opts.json);
514
+ if (!Number.isFinite(minutes) || minutes <= 0) {
515
+ if (json)
516
+ outputJson({ error: 'Minutes must be a positive number.' });
517
+ else
518
+ console.log(chalk.red('\nError: Minutes must be a positive number.\n'));
519
+ process.exitCode = 1;
520
+ return;
521
+ }
522
+ const config = await loadConfigOrExit(opts);
523
+ if (!config)
524
+ return;
525
+ const model = selectModel(config.models || [], opts.model);
526
+ if (!model) {
527
+ const err = `Unknown model "${opts.model}".`;
528
+ if (json)
529
+ outputJson({ error: err });
530
+ else
531
+ console.log(chalk.red(`\nError: ${err}\n`));
532
+ process.exitCode = 1;
533
+ return;
534
+ }
535
+ const tier = selectTier(model, minutes);
536
+ if (!tier) {
537
+ const err = `No ${minutes}-minute plan for ${model.display_name || model.id}.`;
538
+ if (json)
539
+ outputJson({ error: err });
540
+ else
541
+ console.log(chalk.red(`\nError: ${err}\n`));
542
+ process.exitCode = 1;
543
+ return;
544
+ }
545
+ let discountPercent = 0;
546
+ if (paymentMethod === 'lift') {
547
+ discountPercent = config.lift_payment_discount_percent ?? 0;
548
+ }
549
+ const rawSats = Number(tier.price_sats) || 0;
550
+ const discountSats = Math.round(rawSats * (discountPercent / 100));
551
+ const effectiveSats = rawSats - discountSats;
552
+ const confirmed = Boolean(opts.yes) ||
553
+ (json
554
+ ? false
555
+ : await promptConfirmation(`Buy ${tier.minutes}-minute plan for ${model.display_name || model.id} for ${(effectiveSats / 1e8).toFixed(8)} BCH${paymentMethod === 'lift' ? ' (paid with LIFT)' : ''}?`));
556
+ if (json && !opts.yes) {
557
+ outputJson({
558
+ error: 'Payment not confirmed. Re-run with --yes to execute.',
559
+ model: model.id,
560
+ displayName: model.display_name,
561
+ minutes: tier.minutes,
562
+ priceSats: effectiveSats,
563
+ paymentMethod,
564
+ });
565
+ process.exitCode = 1;
566
+ return;
567
+ }
568
+ if (!confirmed) {
569
+ if (json)
570
+ outputJson({ error: 'Payment rejected by user.' });
571
+ else
572
+ console.log(chalk.dim('\n Purchase cancelled.\n'));
573
+ process.exitCode = 1;
574
+ return;
575
+ }
576
+ const result = await buyPlan({
577
+ model: model.id,
578
+ minutes: tier.minutes,
579
+ paymentMethod,
580
+ isChipnet: Boolean(opts.chipnet),
581
+ backendUrl: opts.backend,
582
+ confirmed: true,
583
+ });
584
+ if (json) {
585
+ outputJson(result);
586
+ if (!result.success)
587
+ process.exitCode = 1;
588
+ return;
589
+ }
590
+ if (!result.success) {
591
+ console.log(chalk.red(`\n Error: ${result.error || 'Purchase failed.'}\n`));
592
+ process.exitCode = 1;
593
+ return;
594
+ }
595
+ if (!result.paid) {
596
+ console.log(chalk.yellow(`\n ${result.error || 'No purchase made.'}\n`));
597
+ return;
598
+ }
599
+ console.log(chalk.green(`\n Plan purchased: ${result.displayName || result.model} — ${result.minutes} minutes.`));
600
+ if (result.paymentMethod === 'lift') {
601
+ console.log(chalk.dim(` Paid with LIFT${discountPercent ? ` (${discountPercent}% discount)` : ''}.`));
602
+ }
603
+ if (result.txid)
604
+ console.log(` txid: ${result.txid}`);
605
+ if (result.timeRemainingSeconds !== undefined) {
606
+ console.log(chalk.dim(` Credits remaining: ${formatRemaining(result.timeRemainingSeconds)}`));
607
+ }
608
+ console.log();
609
+ });
610
+ // ── ai auto-refill ──────────────────────────────────────────────────
611
+ ai.command('auto-refill')
612
+ .description('Arm, disarm, or inspect automatic plan refills')
613
+ .option('--enable', 'Arm auto-refill')
614
+ .option('--disable', 'Disarm auto-refill')
615
+ .option('--status', 'Show auto-refill status (default)')
616
+ .option('--model <id>', 'Model id or display name to auto-refill')
617
+ .option('--minutes <minutes>', 'Plan size in minutes per refill')
618
+ .option('--max-minutes <minutes>', 'Maximum total minutes to auto-buy')
619
+ .option('--lift', 'Pay refills with LIFT tokens')
620
+ .option('--json', 'Output as JSON')
621
+ .action((opts) => {
622
+ if (opts.enable) {
623
+ const minutes = opts.minutes !== undefined ? Number(opts.minutes) : undefined;
624
+ const maxMinutes = opts.maxMinutes !== undefined ? Number(opts.maxMinutes) : undefined;
625
+ const badMinutes = minutes !== undefined && (!Number.isFinite(minutes) || minutes <= 0);
626
+ const badMaxMinutes = maxMinutes !== undefined && (!Number.isFinite(maxMinutes) || maxMinutes <= 0);
627
+ if (badMinutes || badMaxMinutes) {
628
+ const message = '--minutes and --max-minutes must be positive numbers.';
629
+ if (opts.json)
630
+ outputJson({ error: message });
631
+ else
632
+ console.log(chalk.red(`\nError: ${message}\n`));
633
+ process.exitCode = 1;
634
+ return;
635
+ }
636
+ const state = armAutoRefill({
637
+ model: opts.model,
638
+ minutes,
639
+ maxMinutes,
640
+ paymentMethod: opts.lift ? 'lift' : 'bch',
641
+ });
642
+ if (opts.json)
643
+ outputJson(state);
644
+ else
645
+ console.log(chalk.green(`\n Auto-refill armed${state.model ? ` for ${state.model}` : ''}.\n`));
646
+ return;
647
+ }
648
+ if (opts.disable) {
649
+ const state = disarmAutoRefill();
650
+ if (opts.json)
651
+ outputJson(state);
652
+ else
653
+ console.log(chalk.dim('\n Auto-refill disarmed.\n'));
654
+ return;
655
+ }
656
+ const state = readAutoRefillState();
657
+ if (opts.json) {
658
+ outputJson({ state, remainingMinutes: remainingBudget(state) });
659
+ return;
660
+ }
661
+ console.log(chalk.bold('\n Auto-Refill\n'));
662
+ if (!state) {
663
+ console.log(chalk.dim(' Not configured.\n'));
664
+ return;
665
+ }
666
+ console.log(` Enabled: ${state.enabled ? chalk.green('yes') : chalk.dim('no')}`);
667
+ console.log(` Model: ${state.model || '(any)'}`);
668
+ console.log(` Plan: ${state.minutes ? formatDuration(state.minutes) : '(any)'}`);
669
+ console.log(` Payment: ${state.paymentMethod || 'bch'}`);
670
+ const budget = remainingBudget(state);
671
+ console.log(` Budget: ${budget === null ? '(unlimited)' : `${budget} min remaining`}`);
672
+ console.log();
673
+ });
674
+ }
675
+ //# sourceMappingURL=ai.js.map