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.
- package/README.md +153 -37
- package/dist/ai/autoRefill.d.ts +27 -0
- package/dist/ai/autoRefill.d.ts.map +1 -0
- package/dist/ai/autoRefill.js +75 -0
- package/dist/ai/autoRefill.js.map +1 -0
- package/dist/ai/client.d.ts +73 -0
- package/dist/ai/client.d.ts.map +1 -0
- package/dist/ai/client.js +90 -0
- package/dist/ai/client.js.map +1 -0
- package/dist/ai/config.d.ts +7 -0
- package/dist/ai/config.d.ts.map +1 -0
- package/dist/ai/config.js +16 -0
- package/dist/ai/config.js.map +1 -0
- package/dist/ai/configure.d.ts +61 -0
- package/dist/ai/configure.d.ts.map +1 -0
- package/dist/ai/configure.js +452 -0
- package/dist/ai/configure.js.map +1 -0
- package/dist/ai/credits.d.ts +27 -0
- package/dist/ai/credits.d.ts.map +1 -0
- package/dist/ai/credits.js +99 -0
- package/dist/ai/credits.js.map +1 -0
- package/dist/ai/models.d.ts +13 -0
- package/dist/ai/models.d.ts.map +1 -0
- package/dist/ai/models.js +49 -0
- package/dist/ai/models.js.map +1 -0
- package/dist/ai/oauth.d.ts +45 -0
- package/dist/ai/oauth.d.ts.map +1 -0
- package/dist/ai/oauth.js +137 -0
- package/dist/ai/oauth.js.map +1 -0
- package/dist/ai/purchase.d.ts +30 -0
- package/dist/ai/purchase.d.ts.map +1 -0
- package/dist/ai/purchase.js +456 -0
- package/dist/ai/purchase.js.map +1 -0
- package/dist/commands/address.d.ts.map +1 -1
- package/dist/commands/address.js +88 -36
- package/dist/commands/address.js.map +1 -1
- package/dist/commands/ai.d.ts +9 -0
- package/dist/commands/ai.d.ts.map +1 -0
- package/dist/commands/ai.js +675 -0
- package/dist/commands/ai.js.map +1 -0
- package/dist/commands/balance.d.ts.map +1 -1
- package/dist/commands/balance.js +68 -129
- package/dist/commands/balance.js.map +1 -1
- package/dist/commands/history.d.ts.map +1 -1
- package/dist/commands/history.js +38 -78
- package/dist/commands/history.js.map +1 -1
- package/dist/commands/mcp.d.ts +14 -0
- package/dist/commands/mcp.d.ts.map +1 -0
- package/dist/commands/mcp.js +68 -0
- package/dist/commands/mcp.js.map +1 -0
- package/dist/commands/pay.js +1 -1
- package/dist/commands/pay.js.map +1 -1
- package/dist/commands/receive.d.ts +1 -8
- package/dist/commands/receive.d.ts.map +1 -1
- package/dist/commands/receive.js +51 -124
- package/dist/commands/receive.js.map +1 -1
- package/dist/commands/send.d.ts +0 -10
- package/dist/commands/send.d.ts.map +1 -1
- package/dist/commands/send.js +40 -50
- package/dist/commands/send.js.map +1 -1
- package/dist/commands/swap.js +1 -1
- package/dist/commands/swap.js.map +1 -1
- package/dist/commands/token.d.ts +1 -9
- package/dist/commands/token.d.ts.map +1 -1
- package/dist/commands/token.js +192 -289
- package/dist/commands/token.js.map +1 -1
- package/dist/core/context.d.ts +30 -0
- package/dist/core/context.d.ts.map +1 -0
- package/dist/core/context.js +45 -0
- package/dist/core/context.js.map +1 -0
- package/dist/core/wallet.d.ts +130 -0
- package/dist/core/wallet.d.ts.map +1 -0
- package/dist/core/wallet.js +276 -0
- package/dist/core/wallet.js.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.d.ts +8 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +24 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/tools.d.ts +5 -0
- package/dist/mcp/tools.d.ts.map +1 -0
- package/dist/mcp/tools.js +473 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/utils/format.d.ts +23 -0
- package/dist/utils/format.d.ts.map +1 -0
- package/dist/utils/format.js +62 -0
- package/dist/utils/format.js.map +1 -0
- package/dist/utils/x402.js +1 -1
- package/dist/utils/x402.js.map +1 -1
- package/dist/wallet/index.d.ts +13 -0
- package/dist/wallet/index.d.ts.map +1 -1
- package/dist/wallet/index.js +18 -0
- package/dist/wallet/index.js.map +1 -1
- package/dist/wallet/keys.d.ts.map +1 -1
- package/dist/wallet/keys.js +2 -0
- package/dist/wallet/keys.js.map +1 -1
- package/package.json +7 -3
- package/dist/commands/wallet.test.d.ts +0 -2
- package/dist/commands/wallet.test.d.ts.map +0 -1
- package/dist/commands/wallet.test.js +0 -68
- package/dist/commands/wallet.test.js.map +0 -1
- package/dist/nostr/chat.test.d.ts +0 -2
- package/dist/nostr/chat.test.d.ts.map +0 -1
- package/dist/nostr/chat.test.js +0 -121
- package/dist/nostr/chat.test.js.map +0 -1
- package/dist/nostr/store.test.d.ts +0 -2
- package/dist/nostr/store.test.d.ts.map +0 -1
- package/dist/nostr/store.test.js +0 -456
- package/dist/nostr/store.test.js.map +0 -1
- package/dist/utils/prices.test.d.ts +0 -5
- package/dist/utils/prices.test.d.ts.map +0 -1
- package/dist/utils/prices.test.js +0 -62
- package/dist/utils/prices.test.js.map +0 -1
- package/dist/utils/x402.test.d.ts +0 -2
- package/dist/utils/x402.test.d.ts.map +0 -1
- package/dist/utils/x402.test.js +0 -216
- package/dist/utils/x402.test.js.map +0 -1
- package/dist/wallet/cauldron/pools.test.d.ts +0 -3
- package/dist/wallet/cauldron/pools.test.d.ts.map +0 -1
- package/dist/wallet/cauldron/pools.test.js +0 -149
- package/dist/wallet/cauldron/pools.test.js.map +0 -1
- package/dist/wallet/cauldron/swap.test.d.ts +0 -2
- package/dist/wallet/cauldron/swap.test.d.ts.map +0 -1
- package/dist/wallet/cauldron/swap.test.js +0 -179
- package/dist/wallet/cauldron/swap.test.js.map +0 -1
- package/skills/README.md +0 -68
- package/skills/paytaca-send/SKILL.md +0 -159
- package/skills/paytaca-wallet/SKILL.md +0 -164
- 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
|