paytaca-cli 0.6.2 → 0.8.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 +183 -34
- package/dist/ai/autoRefill.d.ts +85 -0
- package/dist/ai/autoRefill.d.ts.map +1 -0
- package/dist/ai/autoRefill.js +266 -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 +8 -0
- package/dist/ai/config.d.ts.map +1 -0
- package/dist/ai/config.js +17 -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 +108 -0
- package/dist/ai/credits.js.map +1 -0
- package/dist/ai/images.d.ts +128 -0
- package/dist/ai/images.d.ts.map +1 -0
- package/dist/ai/images.js +399 -0
- package/dist/ai/images.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 +86 -47
- 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 +951 -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/commands/update.d.ts +32 -0
- package/dist/commands/update.d.ts.map +1 -0
- package/dist/commands/update.js +200 -0
- package/dist/commands/update.js.map +1 -0
- package/dist/commands/wallet.js +1 -1
- package/dist/commands/wallet.js.map +1 -1
- package/dist/commands/web.d.ts +3 -0
- package/dist/commands/web.d.ts.map +1 -0
- package/dist/commands/web.js +71 -0
- package/dist/commands/web.js.map +1 -0
- 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 +8 -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 +52 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/tools.d.ts +7 -0
- package/dist/mcp/tools.d.ts.map +1 -0
- package/dist/mcp/tools.js +752 -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/cauldron/swap.d.ts +15 -0
- package/dist/wallet/cauldron/swap.d.ts.map +1 -1
- package/dist/wallet/cauldron/swap.js +31 -0
- package/dist/wallet/cauldron/swap.js.map +1 -1
- package/dist/wallet/index.d.ts +29 -1
- package/dist/wallet/index.d.ts.map +1 -1
- package/dist/wallet/index.js +92 -8
- 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/dist/web/assets.d.ts +3 -0
- package/dist/web/assets.d.ts.map +1 -0
- package/dist/web/assets.js +3 -0
- package/dist/web/assets.js.map +1 -0
- package/dist/web/page.d.ts +2 -0
- package/dist/web/page.d.ts.map +1 -0
- package/dist/web/page.js +637 -0
- package/dist/web/page.js.map +1 -0
- package/dist/web/server.d.ts +79 -0
- package/dist/web/server.d.ts.map +1 -0
- package/dist/web/server.js +665 -0
- package/dist/web/server.js.map +1 -0
- package/package.json +8 -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,951 @@
|
|
|
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 { listImageModels, getImageHistory, getImageOrderStatus, createImageOrder, fulfillImageOrder, } from '../ai/images.js';
|
|
20
|
+
import { provisionApiKey } from '../ai/oauth.js';
|
|
21
|
+
import { configureHarness } from '../ai/configure.js';
|
|
22
|
+
import { CLIENTS } from './mcp.js';
|
|
23
|
+
import { armAutoRefill, disarmAutoRefill, deleteAutoRefill, readAutoRefillState, remainingBudget, autoRefillTick, } from '../ai/autoRefill.js';
|
|
24
|
+
import { createRefillTickDeps } from '../mcp/tools.js';
|
|
25
|
+
function formatTickResult(tick) {
|
|
26
|
+
switch (tick.action) {
|
|
27
|
+
case 'refilled':
|
|
28
|
+
return `Refilled now (${tick.state.refillCount ?? 0} total).`;
|
|
29
|
+
case 'disarmed':
|
|
30
|
+
return `Auto-refill disarmed: ${tick.reason}`;
|
|
31
|
+
case 'skipped':
|
|
32
|
+
return `No refill: ${tick.reason}`;
|
|
33
|
+
default:
|
|
34
|
+
return 'No refill needed.';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function outputJson(value) {
|
|
38
|
+
console.log(JSON.stringify(value, null, 2));
|
|
39
|
+
}
|
|
40
|
+
async function loadConfigOrExit(opts) {
|
|
41
|
+
try {
|
|
42
|
+
return await getConfig({ backendUrl: opts.backend });
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
const message = err?.message || String(err);
|
|
46
|
+
if (opts.json)
|
|
47
|
+
outputJson({ error: message });
|
|
48
|
+
else
|
|
49
|
+
console.log(chalk.red(`\nError: ${message}\n`));
|
|
50
|
+
process.exitCode = 1;
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async function promptConfirmation(message) {
|
|
55
|
+
const rl = readline.createInterface({
|
|
56
|
+
input: process.stdin,
|
|
57
|
+
output: process.stdout,
|
|
58
|
+
});
|
|
59
|
+
return new Promise((resolve) => {
|
|
60
|
+
rl.question(chalk.bold(`\n ${message} (y/N): `), (answer) => {
|
|
61
|
+
rl.close();
|
|
62
|
+
resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
function promptSecret(message) {
|
|
67
|
+
if (!process.stdin.isTTY) {
|
|
68
|
+
return Promise.reject(new Error('A terminal is required to enter the API key.'));
|
|
69
|
+
}
|
|
70
|
+
process.stdout.write(chalk.bold(`\n ${message}: `));
|
|
71
|
+
const stdin = process.stdin;
|
|
72
|
+
stdin.setRawMode?.(true);
|
|
73
|
+
stdin.resume();
|
|
74
|
+
stdin.setEncoding('utf8');
|
|
75
|
+
return new Promise((resolve) => {
|
|
76
|
+
let value = '';
|
|
77
|
+
const finish = (result) => {
|
|
78
|
+
stdin.removeListener('data', onData);
|
|
79
|
+
stdin.removeListener('end', onEnd);
|
|
80
|
+
stdin.setRawMode?.(false);
|
|
81
|
+
stdin.pause();
|
|
82
|
+
process.stdout.write('\n');
|
|
83
|
+
resolve(result);
|
|
84
|
+
};
|
|
85
|
+
const onData = (chunk) => {
|
|
86
|
+
for (const char of chunk) {
|
|
87
|
+
if (char === '\r' || char === '\n') {
|
|
88
|
+
finish(value);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (char === '\u0003') {
|
|
92
|
+
finish('');
|
|
93
|
+
process.exit(130);
|
|
94
|
+
}
|
|
95
|
+
if (char === '\u007f' || char === '\b') {
|
|
96
|
+
value = value.slice(0, -1);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
value += char;
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
const onEnd = () => finish(value);
|
|
103
|
+
stdin.on('data', onData);
|
|
104
|
+
stdin.on('end', onEnd);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
function printPlans(config, modelQuery) {
|
|
108
|
+
const models = listPlans(config, modelQuery);
|
|
109
|
+
if (models.length === 0) {
|
|
110
|
+
console.log(chalk.dim('\n No plans found.\n'));
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
for (const model of models) {
|
|
114
|
+
console.log(` ${chalk.bold(model.displayName)} ${chalk.dim(`(${model.id})`)}`);
|
|
115
|
+
for (const plan of model.plans) {
|
|
116
|
+
const price = formatPriceUsd(plan.price_usd);
|
|
117
|
+
const sats = `${formatSats(Number(plan.price_sats) || 0)} sats`;
|
|
118
|
+
console.log(` ${formatDuration(plan.minutes).padEnd(8)} ${price.padEnd(8)} ${chalk.dim(sats)}`);
|
|
119
|
+
}
|
|
120
|
+
console.log();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
export function registerAiCommands(program) {
|
|
124
|
+
const ai = program.command('ai').description('Paytaca AI: models, plans, credits, purchase');
|
|
125
|
+
// ── ai configure ────────────────────────────────────────────────────
|
|
126
|
+
ai.command('configure')
|
|
127
|
+
.description('Install the Paytaca MCP + AI provider into an AI harness')
|
|
128
|
+
.argument('[harness]', `Target harness: ${CLIENTS.join(', ')}`, 'opencode')
|
|
129
|
+
.option('--backend <url>', 'Override backend URL')
|
|
130
|
+
.option('--chipnet', 'Use chipnet (testnet) instead of mainnet')
|
|
131
|
+
.option('--path <path>', 'Override the target config file path')
|
|
132
|
+
.option('--api-key <key>', 'Use an existing Paytaca API key (required for read-only wallets)')
|
|
133
|
+
.option('-y, --yes', 'Buy a plan without prompting when no credits are active')
|
|
134
|
+
.option('--json', 'Output as JSON')
|
|
135
|
+
.action(async (harnessArg, opts) => {
|
|
136
|
+
const harness = String(harnessArg || 'opencode').toLowerCase();
|
|
137
|
+
const json = Boolean(opts.json);
|
|
138
|
+
const interactive = !json && Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY);
|
|
139
|
+
const requestApiKey = async () => {
|
|
140
|
+
console.log(chalk.yellow('\n This wallet is read-only; an API key is required.'));
|
|
141
|
+
console.log(chalk.dim(' Create one from your full wallet: paytaca ai api-key create'));
|
|
142
|
+
const entered = await promptSecret('Paste your Paytaca API key');
|
|
143
|
+
return entered.trim() || null;
|
|
144
|
+
};
|
|
145
|
+
let result;
|
|
146
|
+
try {
|
|
147
|
+
result = await configureHarness({
|
|
148
|
+
harness,
|
|
149
|
+
chipnet: Boolean(opts.chipnet),
|
|
150
|
+
backendUrl: opts.backend,
|
|
151
|
+
path: opts.path,
|
|
152
|
+
apiKey: opts.apiKey,
|
|
153
|
+
requestApiKey: interactive ? requestApiKey : undefined,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
catch (err) {
|
|
157
|
+
if (json)
|
|
158
|
+
outputJson({ error: err.message });
|
|
159
|
+
else
|
|
160
|
+
console.log(chalk.red(`\nError: ${err.message}\n`));
|
|
161
|
+
process.exitCode = 1;
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
let purchased = null;
|
|
165
|
+
if (!json && result.canSign && !result.creditsActive && result.planSuggestion) {
|
|
166
|
+
const plan = result.planSuggestion;
|
|
167
|
+
const proceed = Boolean(opts.yes) ||
|
|
168
|
+
(await promptConfirmation(`No active AI credits. Buy a ${plan.minutes}-minute plan for ${plan.displayName} (${formatPriceUsd(plan.priceUsd)})?`));
|
|
169
|
+
if (proceed) {
|
|
170
|
+
purchased = await buyPlan({
|
|
171
|
+
model: plan.modelId,
|
|
172
|
+
minutes: plan.minutes,
|
|
173
|
+
paymentMethod: 'bch',
|
|
174
|
+
isChipnet: Boolean(opts.chipnet),
|
|
175
|
+
backendUrl: opts.backend,
|
|
176
|
+
confirmed: true,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (json) {
|
|
181
|
+
outputJson({ ...result, purchased });
|
|
182
|
+
if (!result.mcpInstalled || result.providerError)
|
|
183
|
+
process.exitCode = 1;
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
console.log(chalk.bold(`\n Paytaca AI — ${result.harness}\n`));
|
|
187
|
+
if (result.path)
|
|
188
|
+
console.log(` Config: ${result.path}`);
|
|
189
|
+
console.log(` MCP: ${result.mcpInstalled ? chalk.green('configured') : chalk.red('failed')}`);
|
|
190
|
+
if (result.harness === 'opencode' || result.harness === 'pi' || result.harness === 'omp') {
|
|
191
|
+
if (result.providerInstalled) {
|
|
192
|
+
const how = result.apiKeyReused
|
|
193
|
+
? 'existing API key'
|
|
194
|
+
: result.apiKeyProvided
|
|
195
|
+
? 'provided API key'
|
|
196
|
+
: 'new API key';
|
|
197
|
+
const suffix = result.apiKeyPrefix ? `, ${result.apiKeyPrefix}…` : '';
|
|
198
|
+
console.log(` Provider: ${chalk.green('configured')} ${chalk.dim(`(${how}${suffix})`)}`);
|
|
199
|
+
if (result.models.length > 0) {
|
|
200
|
+
console.log(chalk.dim(` Models: ${result.models.length} available — use \`paytaca-ai/<model-id>\``));
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
const detail = result.providerError ? chalk.dim(` (${result.providerError})`) : '';
|
|
205
|
+
console.log(` Provider: ${chalk.yellow('not configured')}${detail}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
for (const warning of result.warnings || []) {
|
|
209
|
+
console.log(chalk.yellow(` Warning: ${warning}`));
|
|
210
|
+
}
|
|
211
|
+
if (result.creditsActive) {
|
|
212
|
+
console.log(chalk.green('\n AI credits active — ready to use.'));
|
|
213
|
+
}
|
|
214
|
+
else if (purchased?.paid) {
|
|
215
|
+
console.log(chalk.green(`\n Plan purchased: ${purchased.displayName || purchased.model} — ${purchased.minutes} minutes.`));
|
|
216
|
+
if (purchased.txid)
|
|
217
|
+
console.log(chalk.dim(` txid: ${purchased.txid}`));
|
|
218
|
+
}
|
|
219
|
+
else if (purchased && !purchased.success) {
|
|
220
|
+
console.log(chalk.red(`\n Purchase failed: ${purchased.error || 'unknown error'}`));
|
|
221
|
+
}
|
|
222
|
+
else if (!result.canSign) {
|
|
223
|
+
console.log(chalk.yellow('\n No active AI credits.'));
|
|
224
|
+
if (result.planSuggestion) {
|
|
225
|
+
console.log(chalk.dim(` Fund from your full wallet: paytaca ai purchase --model ${result.planSuggestion.modelId} --minutes ${result.planSuggestion.minutes}`));
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
else if (result.planSuggestion) {
|
|
229
|
+
console.log(chalk.yellow('\n No active AI credits.'));
|
|
230
|
+
console.log(chalk.dim(` Buy a plan: paytaca ai purchase --model ${result.planSuggestion.modelId} --minutes ${result.planSuggestion.minutes}`));
|
|
231
|
+
}
|
|
232
|
+
if (result.harness === 'pi') {
|
|
233
|
+
console.log(chalk.yellow.bold('\n Pi requires the pi-mcp-adapter extension.'));
|
|
234
|
+
console.log(chalk.yellow('\n Install it:'));
|
|
235
|
+
console.log(chalk.cyan.bold('\n pi install npm:pi-mcp-adapter\n'));
|
|
236
|
+
console.log(chalk.yellow(' Then restart Pi and verify it loaded with ') +
|
|
237
|
+
chalk.bold.cyan('pi list'));
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
console.log(chalk.dim('\n Restart your AI harness to load the new configuration.\n'));
|
|
241
|
+
}
|
|
242
|
+
if (!result.mcpInstalled)
|
|
243
|
+
process.exitCode = 1;
|
|
244
|
+
});
|
|
245
|
+
// ── ai api-key ──────────────────────────────────────────────────────
|
|
246
|
+
const apiKey = ai.command('api-key').description('Manage Paytaca AI API keys');
|
|
247
|
+
apiKey
|
|
248
|
+
.command('create')
|
|
249
|
+
.description('Create an API key bound to your wallet (requires a full wallet)')
|
|
250
|
+
.option('--name <name>', 'Label for the key', 'paytaca-cli')
|
|
251
|
+
.option('--backend <url>', 'Override backend URL')
|
|
252
|
+
.option('--json', 'Output as JSON')
|
|
253
|
+
.action(async (opts) => {
|
|
254
|
+
const wallet = loadWalletRef();
|
|
255
|
+
if (!wallet) {
|
|
256
|
+
const message = 'No wallet found. Run `paytaca wallet create` or `paytaca wallet import` first.';
|
|
257
|
+
if (opts.json)
|
|
258
|
+
outputJson({ error: message });
|
|
259
|
+
else
|
|
260
|
+
console.log(chalk.red(`\nError: ${message}\n`));
|
|
261
|
+
process.exitCode = 1;
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
if (!wallet.canSign || !wallet.mnemonic) {
|
|
265
|
+
const message = 'This wallet is read-only. Create an API key from your full wallet.';
|
|
266
|
+
if (opts.json)
|
|
267
|
+
outputJson({ error: message });
|
|
268
|
+
else
|
|
269
|
+
console.log(chalk.red(`\nError: ${message}\n`));
|
|
270
|
+
process.exitCode = 1;
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
try {
|
|
274
|
+
const created = await provisionApiKey({
|
|
275
|
+
mnemonic: wallet.mnemonic,
|
|
276
|
+
walletHash: wallet.walletHash,
|
|
277
|
+
backendUrl: opts.backend,
|
|
278
|
+
name: opts.name,
|
|
279
|
+
});
|
|
280
|
+
if (opts.json) {
|
|
281
|
+
outputJson(created);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
console.log(chalk.bold('\n Paytaca AI API key created\n'));
|
|
285
|
+
console.log(` Name: ${created.name}`);
|
|
286
|
+
console.log(` Prefix: ${created.keyPrefix}`);
|
|
287
|
+
console.log(` API key: ${chalk.bold(created.key)}`);
|
|
288
|
+
console.log(chalk.yellow('\n Copy this key now — it will not be shown again.'));
|
|
289
|
+
console.log(chalk.dim(' Then configure a harness: paytaca ai configure opencode --api-key <key>\n'));
|
|
290
|
+
}
|
|
291
|
+
catch (err) {
|
|
292
|
+
if (opts.json)
|
|
293
|
+
outputJson({ error: err.message });
|
|
294
|
+
else
|
|
295
|
+
console.log(chalk.red(`\nError: ${err.message}\n`));
|
|
296
|
+
process.exitCode = 1;
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
// ── ai models ───────────────────────────────────────────────────────
|
|
300
|
+
ai.command('models')
|
|
301
|
+
.description('List available AI models')
|
|
302
|
+
.option('--backend <url>', 'Override backend URL')
|
|
303
|
+
.option('--json', 'Output as JSON')
|
|
304
|
+
.action(async (opts) => {
|
|
305
|
+
const config = await loadConfigOrExit(opts);
|
|
306
|
+
if (!config)
|
|
307
|
+
return;
|
|
308
|
+
const models = (config.models || []).map((m) => ({
|
|
309
|
+
id: m.id,
|
|
310
|
+
displayName: m.display_name,
|
|
311
|
+
plans: m.price_tiers || [],
|
|
312
|
+
}));
|
|
313
|
+
if (opts.json) {
|
|
314
|
+
outputJson({ defaultModel: config.default_model, models });
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
console.log(chalk.bold('\n AI Models\n'));
|
|
318
|
+
for (const m of models) {
|
|
319
|
+
console.log(` ${chalk.bold(m.displayName)} ${chalk.dim(`(${m.id})`)}`);
|
|
320
|
+
if (m.plans.length > 0) {
|
|
321
|
+
const cheapest = m.plans.reduce((a, b) => a.price_usd !== undefined && (b.price_usd === undefined || a.price_usd <= b.price_usd) ? a : b);
|
|
322
|
+
console.log(chalk.dim(` from ${formatPriceUsd(cheapest.price_usd)} · ${m.plans.length} plans`));
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
console.log();
|
|
326
|
+
});
|
|
327
|
+
// ── ai plans ────────────────────────────────────────────────────────
|
|
328
|
+
ai.command('plans')
|
|
329
|
+
.description('Show plan pricing for AI models')
|
|
330
|
+
.argument('[model]', 'Filter to a single model id or display name')
|
|
331
|
+
.option('--backend <url>', 'Override backend URL')
|
|
332
|
+
.option('--json', 'Output as JSON')
|
|
333
|
+
.action(async (model, opts) => {
|
|
334
|
+
const config = await loadConfigOrExit(opts);
|
|
335
|
+
if (!config)
|
|
336
|
+
return;
|
|
337
|
+
const models = listPlans(config, model);
|
|
338
|
+
if (opts.json) {
|
|
339
|
+
outputJson({
|
|
340
|
+
discountPercent: config.lift_payment_discount_percent ?? 0,
|
|
341
|
+
models,
|
|
342
|
+
});
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
console.log(chalk.bold(`\n AI Plans${model ? ` — ${model}` : ''}\n`));
|
|
346
|
+
printPlans(config, model);
|
|
347
|
+
const discount = config.lift_payment_discount_percent;
|
|
348
|
+
if (discount) {
|
|
349
|
+
console.log(chalk.green(` Pay with LIFT for a ${discount}% discount.\n`));
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
// ── ai credits ──────────────────────────────────────────────────────
|
|
353
|
+
ai.command('credits')
|
|
354
|
+
.description('Show remaining AI time credits')
|
|
355
|
+
.option('--model <id>', 'Filter to a single model')
|
|
356
|
+
.option('--backend <url>', 'Override backend URL')
|
|
357
|
+
.option('--chipnet', 'Use chipnet (testnet) instead of mainnet')
|
|
358
|
+
.option('--json', 'Output as JSON')
|
|
359
|
+
.action(async (opts) => {
|
|
360
|
+
const wallet = loadWalletRef();
|
|
361
|
+
if (!wallet) {
|
|
362
|
+
const err = new WalletNotConfiguredError();
|
|
363
|
+
if (opts.json)
|
|
364
|
+
outputJson({ error: err.message });
|
|
365
|
+
else
|
|
366
|
+
console.log(chalk.red(`\n${err.message}\n`));
|
|
367
|
+
process.exitCode = 1;
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
const walletHash = wallet.walletHash;
|
|
371
|
+
try {
|
|
372
|
+
const status = await getWalletStatus(walletHash, {
|
|
373
|
+
backendUrl: opts.backend,
|
|
374
|
+
modelId: opts.model,
|
|
375
|
+
});
|
|
376
|
+
if (opts.json) {
|
|
377
|
+
if (opts.model) {
|
|
378
|
+
outputJson({ walletHash, ...summarizeCredits(status, opts.model) });
|
|
379
|
+
}
|
|
380
|
+
else {
|
|
381
|
+
outputJson({ walletHash, sessions: summarizeAllCredits(status) });
|
|
382
|
+
}
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
const summaries = opts.model
|
|
386
|
+
? [summarizeCredits(status, opts.model)]
|
|
387
|
+
: summarizeAllCredits(status);
|
|
388
|
+
console.log(chalk.bold('\n AI Credits\n'));
|
|
389
|
+
if (summaries.length === 0) {
|
|
390
|
+
console.log(chalk.dim(' No sessions found.\n'));
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
for (const summary of summaries) {
|
|
394
|
+
console.log(` ${chalk.bold(summary.displayName || summary.modelId || 'unknown')}`);
|
|
395
|
+
console.log(` Model: ${summary.modelId || '(unknown)'}`);
|
|
396
|
+
console.log(` Remaining: ${formatRemaining(summary.timeRemainingSeconds)}`);
|
|
397
|
+
console.log(` Used: ${formatRemaining(summary.timeUsedSeconds)}`);
|
|
398
|
+
console.log(` Total: ${formatRemaining(summary.timeCreditsSeconds)}`);
|
|
399
|
+
console.log(` Status: ${summary.active ? chalk.green('active') : chalk.dim('inactive')}`);
|
|
400
|
+
console.log();
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
catch (err) {
|
|
404
|
+
if (opts.json)
|
|
405
|
+
outputJson({ error: err.message });
|
|
406
|
+
else
|
|
407
|
+
console.log(chalk.red(`\nError: ${err.message}\n`));
|
|
408
|
+
process.exitCode = 1;
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
// ── ai usage ────────────────────────────────────────────────────────
|
|
412
|
+
ai.command('usage')
|
|
413
|
+
.description('Show per-session AI usage')
|
|
414
|
+
.option('--backend <url>', 'Override backend URL')
|
|
415
|
+
.option('--chipnet', 'Use chipnet (testnet) instead of mainnet')
|
|
416
|
+
.option('--json', 'Output as JSON')
|
|
417
|
+
.action(async (opts) => {
|
|
418
|
+
const wallet = loadWalletRef();
|
|
419
|
+
if (!wallet) {
|
|
420
|
+
const err = new WalletNotConfiguredError();
|
|
421
|
+
if (opts.json)
|
|
422
|
+
outputJson({ error: err.message });
|
|
423
|
+
else
|
|
424
|
+
console.log(chalk.red(`\n${err.message}\n`));
|
|
425
|
+
process.exitCode = 1;
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
const walletHash = wallet.walletHash;
|
|
429
|
+
try {
|
|
430
|
+
const status = await getWalletStatus(walletHash, { backendUrl: opts.backend });
|
|
431
|
+
const sessions = getSessions(status).map((s) => ({
|
|
432
|
+
model: s.model_id || s.ai_model || null,
|
|
433
|
+
displayName: s.display_name || null,
|
|
434
|
+
active: s.model_active === true || s.session_active === true,
|
|
435
|
+
remainingSeconds: s.time_remaining_seconds ?? 0,
|
|
436
|
+
usedSeconds: s.time_used_seconds ?? 0,
|
|
437
|
+
creditsSeconds: s.time_credits_seconds ?? 0,
|
|
438
|
+
}));
|
|
439
|
+
if (opts.json) {
|
|
440
|
+
outputJson({ walletHash, sessions });
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
console.log(chalk.bold('\n AI Usage\n'));
|
|
444
|
+
if (sessions.length === 0) {
|
|
445
|
+
console.log(chalk.dim(' No sessions found.\n'));
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
for (const s of sessions) {
|
|
449
|
+
console.log(` ${chalk.bold(s.displayName || s.model || 'unknown')}`);
|
|
450
|
+
console.log(chalk.dim(` used ${formatRemaining(s.usedSeconds)} · remaining ${formatRemaining(s.remainingSeconds)} of ${formatRemaining(s.creditsSeconds)}`));
|
|
451
|
+
console.log();
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
catch (err) {
|
|
455
|
+
if (opts.json)
|
|
456
|
+
outputJson({ error: err.message });
|
|
457
|
+
else
|
|
458
|
+
console.log(chalk.red(`\nError: ${err.message}\n`));
|
|
459
|
+
process.exitCode = 1;
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
// ── ai balance ──────────────────────────────────────────────────────
|
|
463
|
+
ai.command('balance')
|
|
464
|
+
.description('Show wallet balance available for AI plan purchases (BCH + LIFT)')
|
|
465
|
+
.option('--chipnet', 'Use chipnet (testnet) instead of mainnet')
|
|
466
|
+
.option('--json', 'Output as JSON')
|
|
467
|
+
.action(async (opts) => {
|
|
468
|
+
const isChipnet = Boolean(opts.chipnet);
|
|
469
|
+
try {
|
|
470
|
+
const balance = await getBalanceView(isChipnet);
|
|
471
|
+
const tokens = await getTokenBalances(isChipnet);
|
|
472
|
+
const lift = tokens.tokens.find((t) => t.category === LIFT_TOKEN_ID);
|
|
473
|
+
if (opts.json) {
|
|
474
|
+
outputJson({
|
|
475
|
+
network: balance.network,
|
|
476
|
+
bch: {
|
|
477
|
+
balance: balance.balanceBch,
|
|
478
|
+
spendable: balance.spendableBch,
|
|
479
|
+
sats: balance.spendableSats,
|
|
480
|
+
usd: balance.usd,
|
|
481
|
+
},
|
|
482
|
+
lift: lift
|
|
483
|
+
? {
|
|
484
|
+
category: lift.category,
|
|
485
|
+
symbol: lift.symbol,
|
|
486
|
+
displayBalance: lift.displayBalance,
|
|
487
|
+
rawBalance: lift.rawBalance,
|
|
488
|
+
}
|
|
489
|
+
: null,
|
|
490
|
+
});
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
console.log(chalk.bold(`\n Funds for AI Plans (${balance.network})\n`));
|
|
494
|
+
console.log(` BCH: ${balance.spendableBch} BCH ${chalk.dim(`(${formatSats(balance.spendableSats)} sats)`)}`);
|
|
495
|
+
if (balance.usd !== null) {
|
|
496
|
+
console.log(chalk.dim(` ≈ ${formatUsd(balance.usd)}`));
|
|
497
|
+
}
|
|
498
|
+
if (lift) {
|
|
499
|
+
console.log(` LIFT: ${lift.displayBalance} ${lift.symbol || 'LIFT'}`);
|
|
500
|
+
}
|
|
501
|
+
else {
|
|
502
|
+
console.log(chalk.dim(' LIFT: none'));
|
|
503
|
+
}
|
|
504
|
+
console.log();
|
|
505
|
+
}
|
|
506
|
+
catch (err) {
|
|
507
|
+
if (opts.json)
|
|
508
|
+
outputJson({ error: err.message });
|
|
509
|
+
else
|
|
510
|
+
console.log(chalk.red(`\nError: ${err.message}\n`));
|
|
511
|
+
process.exitCode = 1;
|
|
512
|
+
}
|
|
513
|
+
});
|
|
514
|
+
// ── ai purchase ─────────────────────────────────────────────────────
|
|
515
|
+
ai.command('purchase')
|
|
516
|
+
.description('Purchase an AI plan (x402 payment with BCH or LIFT)')
|
|
517
|
+
.requiredOption('--model <model>', 'Model id or display name')
|
|
518
|
+
.requiredOption('--minutes <minutes>', 'Plan duration in minutes')
|
|
519
|
+
.option('--lift', 'Pay with LIFT tokens (discount) instead of BCH')
|
|
520
|
+
.option('--chipnet', 'Use chipnet (testnet) instead of mainnet')
|
|
521
|
+
.option('--backend <url>', 'Override backend URL')
|
|
522
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
523
|
+
.option('--json', 'Output as JSON')
|
|
524
|
+
.action(async (opts) => {
|
|
525
|
+
const paymentMethod = opts.lift ? 'lift' : 'bch';
|
|
526
|
+
const minutes = Number(opts.minutes);
|
|
527
|
+
const json = Boolean(opts.json);
|
|
528
|
+
if (!Number.isFinite(minutes) || minutes <= 0) {
|
|
529
|
+
if (json)
|
|
530
|
+
outputJson({ error: 'Minutes must be a positive number.' });
|
|
531
|
+
else
|
|
532
|
+
console.log(chalk.red('\nError: Minutes must be a positive number.\n'));
|
|
533
|
+
process.exitCode = 1;
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
const config = await loadConfigOrExit(opts);
|
|
537
|
+
if (!config)
|
|
538
|
+
return;
|
|
539
|
+
const model = selectModel(config.models || [], opts.model);
|
|
540
|
+
if (!model) {
|
|
541
|
+
const err = `Unknown model "${opts.model}".`;
|
|
542
|
+
if (json)
|
|
543
|
+
outputJson({ error: err });
|
|
544
|
+
else
|
|
545
|
+
console.log(chalk.red(`\nError: ${err}\n`));
|
|
546
|
+
process.exitCode = 1;
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
const tier = selectTier(model, minutes);
|
|
550
|
+
if (!tier) {
|
|
551
|
+
const err = `No ${minutes}-minute plan for ${model.display_name || model.id}.`;
|
|
552
|
+
if (json)
|
|
553
|
+
outputJson({ error: err });
|
|
554
|
+
else
|
|
555
|
+
console.log(chalk.red(`\nError: ${err}\n`));
|
|
556
|
+
process.exitCode = 1;
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
let discountPercent = 0;
|
|
560
|
+
if (paymentMethod === 'lift') {
|
|
561
|
+
discountPercent = config.lift_payment_discount_percent ?? 0;
|
|
562
|
+
}
|
|
563
|
+
const rawSats = Number(tier.price_sats) || 0;
|
|
564
|
+
const discountSats = Math.round(rawSats * (discountPercent / 100));
|
|
565
|
+
const effectiveSats = rawSats - discountSats;
|
|
566
|
+
const confirmed = Boolean(opts.yes) ||
|
|
567
|
+
(json
|
|
568
|
+
? false
|
|
569
|
+
: await promptConfirmation(`Buy ${tier.minutes}-minute plan for ${model.display_name || model.id} for ${(effectiveSats / 1e8).toFixed(8)} BCH${paymentMethod === 'lift' ? ' (paid with LIFT)' : ''}?`));
|
|
570
|
+
if (json && !opts.yes) {
|
|
571
|
+
outputJson({
|
|
572
|
+
error: 'Payment not confirmed. Re-run with --yes to execute.',
|
|
573
|
+
model: model.id,
|
|
574
|
+
displayName: model.display_name,
|
|
575
|
+
minutes: tier.minutes,
|
|
576
|
+
priceSats: effectiveSats,
|
|
577
|
+
paymentMethod,
|
|
578
|
+
});
|
|
579
|
+
process.exitCode = 1;
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
if (!confirmed) {
|
|
583
|
+
if (json)
|
|
584
|
+
outputJson({ error: 'Payment rejected by user.' });
|
|
585
|
+
else
|
|
586
|
+
console.log(chalk.dim('\n Purchase cancelled.\n'));
|
|
587
|
+
process.exitCode = 1;
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
const result = await buyPlan({
|
|
591
|
+
model: model.id,
|
|
592
|
+
minutes: tier.minutes,
|
|
593
|
+
paymentMethod,
|
|
594
|
+
isChipnet: Boolean(opts.chipnet),
|
|
595
|
+
backendUrl: opts.backend,
|
|
596
|
+
confirmed: true,
|
|
597
|
+
});
|
|
598
|
+
if (json) {
|
|
599
|
+
outputJson(result);
|
|
600
|
+
if (!result.success)
|
|
601
|
+
process.exitCode = 1;
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
if (!result.success) {
|
|
605
|
+
console.log(chalk.red(`\n Error: ${result.error || 'Purchase failed.'}\n`));
|
|
606
|
+
process.exitCode = 1;
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
if (!result.paid) {
|
|
610
|
+
console.log(chalk.yellow(`\n ${result.error || 'No purchase made.'}\n`));
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
console.log(chalk.green(`\n Plan purchased: ${result.displayName || result.model} — ${result.minutes} minutes.`));
|
|
614
|
+
if (result.paymentMethod === 'lift') {
|
|
615
|
+
console.log(chalk.dim(` Paid with LIFT${discountPercent ? ` (${discountPercent}% discount)` : ''}.`));
|
|
616
|
+
}
|
|
617
|
+
if (result.txid)
|
|
618
|
+
console.log(` txid: ${result.txid}`);
|
|
619
|
+
if (result.timeRemainingSeconds !== undefined) {
|
|
620
|
+
console.log(chalk.dim(` Credits remaining: ${formatRemaining(result.timeRemainingSeconds)}`));
|
|
621
|
+
}
|
|
622
|
+
console.log();
|
|
623
|
+
});
|
|
624
|
+
// ── ai auto-refill ──────────────────────────────────────────────────
|
|
625
|
+
ai.command('auto-refill')
|
|
626
|
+
.description('Arm, disarm, or inspect automatic plan refills')
|
|
627
|
+
.option('--enable', 'Arm auto-refill')
|
|
628
|
+
.option('--disable', 'Disarm auto-refill')
|
|
629
|
+
.option('--delete', 'Delete the auto-refill configuration')
|
|
630
|
+
.option('--status', 'Show auto-refill status (default)')
|
|
631
|
+
.option('--model <id>', 'Model id or display name to auto-refill')
|
|
632
|
+
.option('--minutes <minutes>', 'Plan size in minutes per refill')
|
|
633
|
+
.option('--max-minutes <minutes>', 'Maximum total minutes to auto-buy (>= 2x --minutes, multiple of --minutes)')
|
|
634
|
+
.option('--lift', 'Pay refills with LIFT tokens')
|
|
635
|
+
.option('--chipnet', 'Use chipnet (testnet) instead of mainnet')
|
|
636
|
+
.option('--json', 'Output as JSON')
|
|
637
|
+
.action(async (opts) => {
|
|
638
|
+
if (opts.delete) {
|
|
639
|
+
const existed = deleteAutoRefill();
|
|
640
|
+
if (opts.json)
|
|
641
|
+
outputJson({ deleted: true, existed });
|
|
642
|
+
else if (existed)
|
|
643
|
+
console.log(chalk.dim('\n Auto-refill configuration deleted.\n'));
|
|
644
|
+
else
|
|
645
|
+
console.log(chalk.dim('\n No auto-refill configuration to delete.\n'));
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
if (opts.enable) {
|
|
649
|
+
const minutes = opts.minutes !== undefined ? Number(opts.minutes) : undefined;
|
|
650
|
+
const maxMinutes = opts.maxMinutes !== undefined ? Number(opts.maxMinutes) : undefined;
|
|
651
|
+
const badMinutes = minutes !== undefined && (!Number.isFinite(minutes) || minutes <= 0);
|
|
652
|
+
const badMaxMinutes = maxMinutes !== undefined && (!Number.isFinite(maxMinutes) || maxMinutes <= 0);
|
|
653
|
+
if (badMinutes || badMaxMinutes) {
|
|
654
|
+
const message = '--minutes and --max-minutes must be positive numbers.';
|
|
655
|
+
if (opts.json)
|
|
656
|
+
outputJson({ error: message });
|
|
657
|
+
else
|
|
658
|
+
console.log(chalk.red(`\nError: ${message}\n`));
|
|
659
|
+
process.exitCode = 1;
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
const existing = readAutoRefillState();
|
|
663
|
+
const effMinutes = minutes ?? existing?.minutes;
|
|
664
|
+
const effMaxMinutes = maxMinutes ?? existing?.maxMinutes;
|
|
665
|
+
let capError = null;
|
|
666
|
+
if (effMaxMinutes !== undefined) {
|
|
667
|
+
if (effMinutes === undefined) {
|
|
668
|
+
capError = '--max-minutes requires --minutes (a top-up size).';
|
|
669
|
+
}
|
|
670
|
+
else if (effMaxMinutes < effMinutes * 2) {
|
|
671
|
+
capError = `--max-minutes must be at least twice --minutes (${effMinutes * 2}).`;
|
|
672
|
+
}
|
|
673
|
+
else if (effMaxMinutes % effMinutes !== 0) {
|
|
674
|
+
capError = `--max-minutes must be a multiple of --minutes (${effMinutes}).`;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
if (capError) {
|
|
678
|
+
if (opts.json)
|
|
679
|
+
outputJson({ error: capError });
|
|
680
|
+
else
|
|
681
|
+
console.log(chalk.red(`\nError: ${capError}\n`));
|
|
682
|
+
process.exitCode = 1;
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
const state = armAutoRefill({
|
|
686
|
+
model: opts.model,
|
|
687
|
+
minutes,
|
|
688
|
+
maxMinutes,
|
|
689
|
+
paymentMethod: opts.lift ? 'lift' : 'bch',
|
|
690
|
+
});
|
|
691
|
+
let tick = null;
|
|
692
|
+
const wallet = loadWalletRef();
|
|
693
|
+
if (wallet?.canSign) {
|
|
694
|
+
try {
|
|
695
|
+
tick = await autoRefillTick(createRefillTickDeps(Boolean(opts.chipnet)));
|
|
696
|
+
}
|
|
697
|
+
catch (err) {
|
|
698
|
+
tick = { action: 'skipped', reason: err.message };
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
if (opts.json) {
|
|
702
|
+
outputJson(tick ? { ...state, initialTick: tick } : state);
|
|
703
|
+
}
|
|
704
|
+
else {
|
|
705
|
+
console.log(chalk.green(`\n Auto-refill armed${state.model ? ` for ${state.model}` : ''}.`));
|
|
706
|
+
if (tick)
|
|
707
|
+
console.log(chalk.dim(` ${formatTickResult(tick)}`));
|
|
708
|
+
console.log();
|
|
709
|
+
}
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
if (opts.disable) {
|
|
713
|
+
const state = disarmAutoRefill();
|
|
714
|
+
if (opts.json)
|
|
715
|
+
outputJson(state);
|
|
716
|
+
else
|
|
717
|
+
console.log(chalk.dim('\n Auto-refill disarmed.\n'));
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
const state = readAutoRefillState();
|
|
721
|
+
if (opts.json) {
|
|
722
|
+
outputJson({ state, remainingMinutes: remainingBudget(state) });
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
console.log(chalk.bold('\n Auto-Refill\n'));
|
|
726
|
+
if (!state) {
|
|
727
|
+
console.log(chalk.dim(' Not configured.\n'));
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
console.log(` Enabled: ${state.enabled ? chalk.green('yes') : chalk.dim('no')}`);
|
|
731
|
+
console.log(` Model: ${state.model || '(any)'}`);
|
|
732
|
+
console.log(` Plan: ${state.minutes ? formatDuration(state.minutes) : '(any)'}`);
|
|
733
|
+
console.log(` Payment: ${state.paymentMethod || 'bch'}`);
|
|
734
|
+
const budget = remainingBudget(state);
|
|
735
|
+
console.log(` Budget: ${budget === null ? '(unlimited)' : `${budget} min remaining`}`);
|
|
736
|
+
console.log(` Refills: ${state.refillCount ?? 0}`);
|
|
737
|
+
if (state.lastRefillAt) {
|
|
738
|
+
const tx = state.lastRefillTxid ? ` (${state.lastRefillTxid.slice(0, 12)}…)` : '';
|
|
739
|
+
console.log(` Last: ${new Date(state.lastRefillAt).toLocaleString()}${tx}`);
|
|
740
|
+
}
|
|
741
|
+
if (state.lastEvent) {
|
|
742
|
+
const ev = state.lastEvent;
|
|
743
|
+
const reason = ev.reason ? ` — ${ev.reason}` : '';
|
|
744
|
+
console.log(` Event: ${ev.action}/${ev.status}${reason} @ ${new Date(ev.at).toLocaleString()}`);
|
|
745
|
+
}
|
|
746
|
+
console.log();
|
|
747
|
+
});
|
|
748
|
+
// ── ai image ────────────────────────────────────────────────────────
|
|
749
|
+
const image = ai.command('image').description('Paytaca AI image generation');
|
|
750
|
+
image.command('models')
|
|
751
|
+
.description('List available image generation models')
|
|
752
|
+
.option('--backend <url>', 'Override backend URL')
|
|
753
|
+
.option('--json', 'Output as JSON')
|
|
754
|
+
.action(async (opts) => {
|
|
755
|
+
try {
|
|
756
|
+
const models = await listImageModels({ backendUrl: opts.backend });
|
|
757
|
+
if (opts.json) {
|
|
758
|
+
outputJson({ models });
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
console.log(chalk.bold('\n Image Models\n'));
|
|
762
|
+
if (models.length === 0) {
|
|
763
|
+
console.log(chalk.dim(' No image models found.\n'));
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
const unitLabel = {
|
|
767
|
+
image: 'per image',
|
|
768
|
+
megapixel: 'per megapixel',
|
|
769
|
+
token: 'per token',
|
|
770
|
+
};
|
|
771
|
+
for (const m of models) {
|
|
772
|
+
console.log(` ${chalk.bold(m.display_name || m.id)} ${chalk.dim(`(${m.id})`)}`);
|
|
773
|
+
const unit = unitLabel[m.pricing_unit ?? ''] ?? `per ${m.pricing_unit ?? 'image'}`;
|
|
774
|
+
const price = m.cost_per_unit_usd !== undefined ? `$${m.cost_per_unit_usd.toFixed(6)}` : '';
|
|
775
|
+
console.log(chalk.dim(` ${price} ${unit}`));
|
|
776
|
+
}
|
|
777
|
+
console.log();
|
|
778
|
+
}
|
|
779
|
+
catch (err) {
|
|
780
|
+
if (opts.json)
|
|
781
|
+
outputJson({ error: err.message });
|
|
782
|
+
else
|
|
783
|
+
console.log(chalk.red(`\nError: ${err.message}\n`));
|
|
784
|
+
process.exitCode = 1;
|
|
785
|
+
}
|
|
786
|
+
});
|
|
787
|
+
image.command('generate')
|
|
788
|
+
.description('Generate an image from a prompt (BCH payment)')
|
|
789
|
+
.argument('<prompt>', 'Image prompt')
|
|
790
|
+
.option('--model <model>', 'Image model id (defaults to the cheapest)')
|
|
791
|
+
.option('--aspect-ratio <ratio>', 'Aspect ratio, e.g. 1:1, 16:9', '1:1')
|
|
792
|
+
.option('--quality <quality>', 'Image quality: low, medium, high, auto', 'auto')
|
|
793
|
+
.option('--resolution <resolution>', 'Resolution: 512, 1K, 2K, 4K', '1K')
|
|
794
|
+
.option('--chipnet', 'Use chipnet (testnet) instead of mainnet')
|
|
795
|
+
.option('--backend <url>', 'Override backend URL')
|
|
796
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
797
|
+
.option('--json', 'Output as JSON')
|
|
798
|
+
.action(async (prompt, opts) => {
|
|
799
|
+
const json = Boolean(opts.json);
|
|
800
|
+
const orderOpts = {
|
|
801
|
+
prompt,
|
|
802
|
+
model: opts.model,
|
|
803
|
+
aspectRatio: opts.aspectRatio,
|
|
804
|
+
quality: opts.quality,
|
|
805
|
+
resolution: opts.resolution,
|
|
806
|
+
isChipnet: Boolean(opts.chipnet),
|
|
807
|
+
backendUrl: opts.backend,
|
|
808
|
+
};
|
|
809
|
+
let quote;
|
|
810
|
+
try {
|
|
811
|
+
quote = await createImageOrder(orderOpts);
|
|
812
|
+
}
|
|
813
|
+
catch (err) {
|
|
814
|
+
if (json)
|
|
815
|
+
outputJson({ error: err.message });
|
|
816
|
+
else
|
|
817
|
+
console.log(chalk.red(`\nError: ${err.message}\n`));
|
|
818
|
+
process.exitCode = 1;
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
if (!opts.yes) {
|
|
822
|
+
if (json) {
|
|
823
|
+
outputJson({
|
|
824
|
+
error: 'Payment not confirmed. Re-run with --yes to execute.',
|
|
825
|
+
orderId: quote.orderId,
|
|
826
|
+
model: quote.model,
|
|
827
|
+
amountSats: quote.amountSats,
|
|
828
|
+
amountUsd: quote.amountUsd,
|
|
829
|
+
});
|
|
830
|
+
process.exitCode = 1;
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
const proceed = await promptConfirmation(`Generate an image with ${quote.model || 'the default model'} for ${quote.amountSats} sats${quote.amountUsd !== undefined ? ` (~$${quote.amountUsd})` : ''}?`);
|
|
834
|
+
if (!proceed) {
|
|
835
|
+
console.log(chalk.dim('\n Cancelled.\n'));
|
|
836
|
+
process.exitCode = 1;
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
const result = await fulfillImageOrder(quote, orderOpts);
|
|
841
|
+
if (json) {
|
|
842
|
+
outputJson({ ...result, base64: undefined });
|
|
843
|
+
if (!result.success)
|
|
844
|
+
process.exitCode = 1;
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
if (!result.success) {
|
|
848
|
+
console.log(chalk.red(`\n Error: ${result.error || 'Generation failed.'}\n`));
|
|
849
|
+
process.exitCode = 1;
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
console.log(chalk.green(`\n Image generated — ${result.path}`));
|
|
853
|
+
console.log(` Order: ${result.orderId}`);
|
|
854
|
+
console.log(` Model: ${result.model}`);
|
|
855
|
+
console.log(` Cost: ${result.amountSats} sats${result.amountUsd !== undefined ? ` (~$${result.amountUsd})` : ''}`);
|
|
856
|
+
if (result.txid)
|
|
857
|
+
console.log(chalk.dim(` txid: ${result.txid}`));
|
|
858
|
+
console.log();
|
|
859
|
+
});
|
|
860
|
+
image.command('history')
|
|
861
|
+
.description('Show image generation order history')
|
|
862
|
+
.option('--page <page>', 'Page number', '1')
|
|
863
|
+
.option('--backend <url>', 'Override backend URL')
|
|
864
|
+
.option('--json', 'Output as JSON')
|
|
865
|
+
.action(async (opts) => {
|
|
866
|
+
const page = Number(opts.page);
|
|
867
|
+
if (!Number.isFinite(page) || page < 1) {
|
|
868
|
+
const message = 'Page must be a positive number.';
|
|
869
|
+
if (opts.json)
|
|
870
|
+
outputJson({ error: message });
|
|
871
|
+
else
|
|
872
|
+
console.log(chalk.red(`\nError: ${message}\n`));
|
|
873
|
+
process.exitCode = 1;
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
try {
|
|
877
|
+
const history = await getImageHistory({
|
|
878
|
+
page,
|
|
879
|
+
backendUrl: opts.backend,
|
|
880
|
+
});
|
|
881
|
+
if (opts.json) {
|
|
882
|
+
outputJson(history);
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
console.log(chalk.bold('\n Image Order History\n'));
|
|
886
|
+
const rows = history.data ?? [];
|
|
887
|
+
if (rows.length === 0) {
|
|
888
|
+
console.log(chalk.dim(' No orders found.\n'));
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
for (const row of rows) {
|
|
892
|
+
const date = row.created_at ? new Date(row.created_at).toLocaleString() : '';
|
|
893
|
+
console.log(` ${chalk.bold(row.id)}`);
|
|
894
|
+
console.log(chalk.dim(` ${row.status ?? 'unknown'} · ${row.model_display_name || row.model || ''} · ${row.price_sats ?? 0} sats${date ? ` · ${date}` : ''}`));
|
|
895
|
+
if (row.prompt)
|
|
896
|
+
console.log(chalk.dim(` "${row.prompt}"`));
|
|
897
|
+
console.log();
|
|
898
|
+
}
|
|
899
|
+
if (history.count !== undefined) {
|
|
900
|
+
console.log(chalk.dim(` ${history.count} total orders\n`));
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
catch (err) {
|
|
904
|
+
if (opts.json)
|
|
905
|
+
outputJson({ error: err.message });
|
|
906
|
+
else
|
|
907
|
+
console.log(chalk.red(`\nError: ${err.message}\n`));
|
|
908
|
+
process.exitCode = 1;
|
|
909
|
+
}
|
|
910
|
+
});
|
|
911
|
+
image.command('status')
|
|
912
|
+
.description('Poll a pending image generation order')
|
|
913
|
+
.argument('<order-id>', 'Image order ID to check')
|
|
914
|
+
.option('--chipnet', 'Use chipnet')
|
|
915
|
+
.option('--backend <url>', 'Override backend URL')
|
|
916
|
+
.option('--json', 'Output as JSON')
|
|
917
|
+
.action(async (orderId, opts) => {
|
|
918
|
+
try {
|
|
919
|
+
const result = await getImageOrderStatus(orderId, {
|
|
920
|
+
isChipnet: Boolean(opts.chipnet),
|
|
921
|
+
backendUrl: opts.backend,
|
|
922
|
+
});
|
|
923
|
+
if (opts.json) {
|
|
924
|
+
outputJson({ ...result, base64: undefined });
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
if (result.success && result.path) {
|
|
928
|
+
console.log(chalk.green(`\n Image ready (order ${result.orderId}).`));
|
|
929
|
+
console.log(` Saved to: ${result.path}`);
|
|
930
|
+
console.log(chalk.dim(` Model: ${result.model}`));
|
|
931
|
+
console.log(chalk.dim(` Cost: ${result.amountSats} sats`));
|
|
932
|
+
console.log(chalk.dim(` txid: ${result.txid}\n`));
|
|
933
|
+
}
|
|
934
|
+
else if (result.paid && !result.success) {
|
|
935
|
+
console.log(chalk.yellow(`\n Image generation is still processing (order ${result.orderId}).`));
|
|
936
|
+
console.log(chalk.dim(' Try again in a few seconds.\n'));
|
|
937
|
+
}
|
|
938
|
+
else {
|
|
939
|
+
console.log(chalk.red(`\n Error: ${result.error}\n`));
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
catch (err) {
|
|
943
|
+
if (opts.json)
|
|
944
|
+
outputJson({ error: err.message });
|
|
945
|
+
else
|
|
946
|
+
console.log(chalk.red(`\nError: ${err.message}\n`));
|
|
947
|
+
process.exitCode = 1;
|
|
948
|
+
}
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
//# sourceMappingURL=ai.js.map
|