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.
Files changed (160) hide show
  1. package/README.md +183 -34
  2. package/dist/ai/autoRefill.d.ts +85 -0
  3. package/dist/ai/autoRefill.d.ts.map +1 -0
  4. package/dist/ai/autoRefill.js +266 -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 +8 -0
  11. package/dist/ai/config.d.ts.map +1 -0
  12. package/dist/ai/config.js +17 -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 +108 -0
  21. package/dist/ai/credits.js.map +1 -0
  22. package/dist/ai/images.d.ts +128 -0
  23. package/dist/ai/images.d.ts.map +1 -0
  24. package/dist/ai/images.js +399 -0
  25. package/dist/ai/images.js.map +1 -0
  26. package/dist/ai/models.d.ts +13 -0
  27. package/dist/ai/models.d.ts.map +1 -0
  28. package/dist/ai/models.js +49 -0
  29. package/dist/ai/models.js.map +1 -0
  30. package/dist/ai/oauth.d.ts +45 -0
  31. package/dist/ai/oauth.d.ts.map +1 -0
  32. package/dist/ai/oauth.js +137 -0
  33. package/dist/ai/oauth.js.map +1 -0
  34. package/dist/ai/purchase.d.ts +30 -0
  35. package/dist/ai/purchase.d.ts.map +1 -0
  36. package/dist/ai/purchase.js +456 -0
  37. package/dist/ai/purchase.js.map +1 -0
  38. package/dist/commands/address.d.ts.map +1 -1
  39. package/dist/commands/address.js +86 -47
  40. package/dist/commands/address.js.map +1 -1
  41. package/dist/commands/ai.d.ts +9 -0
  42. package/dist/commands/ai.d.ts.map +1 -0
  43. package/dist/commands/ai.js +951 -0
  44. package/dist/commands/ai.js.map +1 -0
  45. package/dist/commands/balance.d.ts.map +1 -1
  46. package/dist/commands/balance.js +68 -129
  47. package/dist/commands/balance.js.map +1 -1
  48. package/dist/commands/history.d.ts.map +1 -1
  49. package/dist/commands/history.js +38 -78
  50. package/dist/commands/history.js.map +1 -1
  51. package/dist/commands/mcp.d.ts +14 -0
  52. package/dist/commands/mcp.d.ts.map +1 -0
  53. package/dist/commands/mcp.js +68 -0
  54. package/dist/commands/mcp.js.map +1 -0
  55. package/dist/commands/pay.js +1 -1
  56. package/dist/commands/pay.js.map +1 -1
  57. package/dist/commands/receive.d.ts +1 -8
  58. package/dist/commands/receive.d.ts.map +1 -1
  59. package/dist/commands/receive.js +51 -124
  60. package/dist/commands/receive.js.map +1 -1
  61. package/dist/commands/send.d.ts +0 -10
  62. package/dist/commands/send.d.ts.map +1 -1
  63. package/dist/commands/send.js +40 -50
  64. package/dist/commands/send.js.map +1 -1
  65. package/dist/commands/swap.js +1 -1
  66. package/dist/commands/swap.js.map +1 -1
  67. package/dist/commands/token.d.ts +1 -9
  68. package/dist/commands/token.d.ts.map +1 -1
  69. package/dist/commands/token.js +192 -289
  70. package/dist/commands/token.js.map +1 -1
  71. package/dist/commands/update.d.ts +32 -0
  72. package/dist/commands/update.d.ts.map +1 -0
  73. package/dist/commands/update.js +200 -0
  74. package/dist/commands/update.js.map +1 -0
  75. package/dist/commands/wallet.js +1 -1
  76. package/dist/commands/wallet.js.map +1 -1
  77. package/dist/commands/web.d.ts +3 -0
  78. package/dist/commands/web.d.ts.map +1 -0
  79. package/dist/commands/web.js +71 -0
  80. package/dist/commands/web.js.map +1 -0
  81. package/dist/core/context.d.ts +30 -0
  82. package/dist/core/context.d.ts.map +1 -0
  83. package/dist/core/context.js +45 -0
  84. package/dist/core/context.js.map +1 -0
  85. package/dist/core/wallet.d.ts +130 -0
  86. package/dist/core/wallet.d.ts.map +1 -0
  87. package/dist/core/wallet.js +276 -0
  88. package/dist/core/wallet.js.map +1 -0
  89. package/dist/index.js +8 -0
  90. package/dist/index.js.map +1 -1
  91. package/dist/mcp/server.d.ts +8 -0
  92. package/dist/mcp/server.d.ts.map +1 -0
  93. package/dist/mcp/server.js +52 -0
  94. package/dist/mcp/server.js.map +1 -0
  95. package/dist/mcp/tools.d.ts +7 -0
  96. package/dist/mcp/tools.d.ts.map +1 -0
  97. package/dist/mcp/tools.js +752 -0
  98. package/dist/mcp/tools.js.map +1 -0
  99. package/dist/utils/format.d.ts +23 -0
  100. package/dist/utils/format.d.ts.map +1 -0
  101. package/dist/utils/format.js +62 -0
  102. package/dist/utils/format.js.map +1 -0
  103. package/dist/utils/x402.js +1 -1
  104. package/dist/utils/x402.js.map +1 -1
  105. package/dist/wallet/cauldron/swap.d.ts +15 -0
  106. package/dist/wallet/cauldron/swap.d.ts.map +1 -1
  107. package/dist/wallet/cauldron/swap.js +31 -0
  108. package/dist/wallet/cauldron/swap.js.map +1 -1
  109. package/dist/wallet/index.d.ts +29 -1
  110. package/dist/wallet/index.d.ts.map +1 -1
  111. package/dist/wallet/index.js +92 -8
  112. package/dist/wallet/index.js.map +1 -1
  113. package/dist/wallet/keys.d.ts.map +1 -1
  114. package/dist/wallet/keys.js +2 -0
  115. package/dist/wallet/keys.js.map +1 -1
  116. package/dist/web/assets.d.ts +3 -0
  117. package/dist/web/assets.d.ts.map +1 -0
  118. package/dist/web/assets.js +3 -0
  119. package/dist/web/assets.js.map +1 -0
  120. package/dist/web/page.d.ts +2 -0
  121. package/dist/web/page.d.ts.map +1 -0
  122. package/dist/web/page.js +637 -0
  123. package/dist/web/page.js.map +1 -0
  124. package/dist/web/server.d.ts +79 -0
  125. package/dist/web/server.d.ts.map +1 -0
  126. package/dist/web/server.js +665 -0
  127. package/dist/web/server.js.map +1 -0
  128. package/package.json +8 -3
  129. package/dist/commands/wallet.test.d.ts +0 -2
  130. package/dist/commands/wallet.test.d.ts.map +0 -1
  131. package/dist/commands/wallet.test.js +0 -68
  132. package/dist/commands/wallet.test.js.map +0 -1
  133. package/dist/nostr/chat.test.d.ts +0 -2
  134. package/dist/nostr/chat.test.d.ts.map +0 -1
  135. package/dist/nostr/chat.test.js +0 -121
  136. package/dist/nostr/chat.test.js.map +0 -1
  137. package/dist/nostr/store.test.d.ts +0 -2
  138. package/dist/nostr/store.test.d.ts.map +0 -1
  139. package/dist/nostr/store.test.js +0 -456
  140. package/dist/nostr/store.test.js.map +0 -1
  141. package/dist/utils/prices.test.d.ts +0 -5
  142. package/dist/utils/prices.test.d.ts.map +0 -1
  143. package/dist/utils/prices.test.js +0 -62
  144. package/dist/utils/prices.test.js.map +0 -1
  145. package/dist/utils/x402.test.d.ts +0 -2
  146. package/dist/utils/x402.test.d.ts.map +0 -1
  147. package/dist/utils/x402.test.js +0 -216
  148. package/dist/utils/x402.test.js.map +0 -1
  149. package/dist/wallet/cauldron/pools.test.d.ts +0 -3
  150. package/dist/wallet/cauldron/pools.test.d.ts.map +0 -1
  151. package/dist/wallet/cauldron/pools.test.js +0 -149
  152. package/dist/wallet/cauldron/pools.test.js.map +0 -1
  153. package/dist/wallet/cauldron/swap.test.d.ts +0 -2
  154. package/dist/wallet/cauldron/swap.test.d.ts.map +0 -1
  155. package/dist/wallet/cauldron/swap.test.js +0 -179
  156. package/dist/wallet/cauldron/swap.test.js.map +0 -1
  157. package/skills/README.md +0 -68
  158. package/skills/paytaca-send/SKILL.md +0 -159
  159. package/skills/paytaca-wallet/SKILL.md +0 -164
  160. package/skills/paytaca-x402/SKILL.md +0 -130
@@ -0,0 +1,752 @@
1
+ import { z } from 'zod';
2
+ import { getBalanceView, getHistoryView, getReceiveAddressView, getTokenBalances, getTokenDetail, sendBch, sendToken, isValidBchAddress, } from '../core/wallet.js';
3
+ import { WalletNotConfiguredError } from '../core/context.js';
4
+ import { loadWalletRef } from '../wallet/index.js';
5
+ import { getConfig } from '../ai/client.js';
6
+ import { listModels, listPlans, selectModel, formatPriceUsd, formatDuration } from '../ai/models.js';
7
+ import { getWalletStatus, summarizeCredits, summarizeAllCredits, buildPurchaseHint, formatRemaining, hasActiveCredits, } from '../ai/credits.js';
8
+ import { buyPlan } from '../ai/purchase.js';
9
+ import { generateImage, getImageHistory, getImageOrderStatus, isStillProcessing, listImageModels, } from '../ai/images.js';
10
+ import { armAutoRefill, autoRefillTick, deleteAutoRefill, disarmAutoRefill, readAutoRefillState, remainingBudget, } from '../ai/autoRefill.js';
11
+ function text(value) {
12
+ return { content: [{ type: 'text', text: value }] };
13
+ }
14
+ function json(value) {
15
+ return text(JSON.stringify(value, null, 2));
16
+ }
17
+ export function createRefillTickDeps(isChipnet, backendUrl) {
18
+ return {
19
+ hasActiveCredits: async (model) => {
20
+ const wallet = loadWalletRef();
21
+ if (!wallet)
22
+ throw new WalletNotConfiguredError();
23
+ const status = await getWalletStatus(wallet.walletHash, {
24
+ modelId: model,
25
+ backendUrl,
26
+ });
27
+ return hasActiveCredits(status, model);
28
+ },
29
+ buy: async (opts) => {
30
+ const result = await buyPlan({
31
+ model: opts.model,
32
+ minutes: opts.minutes,
33
+ paymentMethod: opts.paymentMethod,
34
+ isChipnet,
35
+ backendUrl,
36
+ confirmed: true,
37
+ });
38
+ return {
39
+ success: result.success,
40
+ paid: result.paid,
41
+ txid: result.txid,
42
+ priceSats: result.priceSats,
43
+ error: result.error,
44
+ };
45
+ },
46
+ };
47
+ }
48
+ function tickSummary(tick) {
49
+ if (tick.action === 'idle')
50
+ return { action: 'idle' };
51
+ if (tick.action === 'skipped')
52
+ return { action: 'skipped', reason: tick.reason };
53
+ if (tick.action === 'refilled') {
54
+ return {
55
+ action: 'refilled',
56
+ model: tick.state.model ?? null,
57
+ minutes: tick.state.minutes ?? null,
58
+ txid: tick.state.lastRefillTxid ?? null,
59
+ };
60
+ }
61
+ return { action: 'disarmed', reason: tick.reason };
62
+ }
63
+ function autoRefillField(state) {
64
+ if (!state)
65
+ return null;
66
+ return {
67
+ armed: Boolean(state.enabled),
68
+ model: state.model ?? null,
69
+ minutes: state.minutes ?? null,
70
+ remainingMinutes: remainingBudget(state),
71
+ refillCount: state.refillCount ?? 0,
72
+ lastEvent: state.lastEvent ?? null,
73
+ };
74
+ }
75
+ function clientName(server) {
76
+ return server.server.getClientVersion()?.name ?? '';
77
+ }
78
+ function creditsResponse(server, sessions, hint, payload) {
79
+ const name = clientName(server);
80
+ if (name.startsWith('pi-mcp')) {
81
+ return {
82
+ content: [{ type: 'text', text: creditsText(sessions, hint) }],
83
+ structuredContent: payload,
84
+ };
85
+ }
86
+ if (name === 'opencode') {
87
+ return {
88
+ content: [{ type: 'text', text: creditsMarkdown(sessions, hint) }],
89
+ structuredContent: payload,
90
+ };
91
+ }
92
+ return json(payload);
93
+ }
94
+ function creditRows(sessions) {
95
+ return sessions.map((session) => ({
96
+ name: session.displayName || session.modelId || 'unknown',
97
+ status: session.active ? 'active' : 'inactive',
98
+ remaining: formatRemaining(session.timeRemainingSeconds),
99
+ used: formatRemaining(session.timeUsedSeconds),
100
+ limit: session.tokenLimit != null ? session.tokenLimit.toLocaleString('en-US') : '-',
101
+ }));
102
+ }
103
+ function creditsText(sessions, hint) {
104
+ const lines = ['Paytaca AI credits', ''];
105
+ if (sessions.length === 0) {
106
+ lines.push('No AI credit sessions found.');
107
+ }
108
+ else {
109
+ const rows = creditRows(sessions);
110
+ const width = (key, header) => Math.max(header.length, ...rows.map((row) => row[key].length));
111
+ const nameWidth = width('name', 'Model');
112
+ const statusWidth = width('status', 'Status');
113
+ const remainingWidth = width('remaining', 'Remaining');
114
+ const usedWidth = width('used', 'Used');
115
+ const limitWidth = width('limit', 'Token limit');
116
+ const row = (...cells) => cells.map((cell, i) => cell.padEnd([nameWidth, statusWidth, remainingWidth, usedWidth, limitWidth][i])).join(' ');
117
+ lines.push(row('Model', 'Status', 'Remaining', 'Used', 'Token limit'));
118
+ for (const r of rows) {
119
+ lines.push(row(r.name, r.status, r.remaining, r.used, r.limit));
120
+ }
121
+ }
122
+ if (hint) {
123
+ lines.push('', hint.message);
124
+ }
125
+ return lines.join('\n');
126
+ }
127
+ function creditsMarkdown(sessions, hint) {
128
+ const lines = ['## Paytaca AI credits', ''];
129
+ if (sessions.length === 0) {
130
+ lines.push('No AI credit sessions found.');
131
+ }
132
+ else {
133
+ lines.push('| Model | Status | Remaining | Used | Token limit |');
134
+ lines.push('| --- | --- | --- | --- | --- |');
135
+ for (const r of creditRows(sessions)) {
136
+ lines.push(`| ${r.name} | ${r.status} | ${r.remaining} | ${r.used} | ${r.limit} |`);
137
+ }
138
+ }
139
+ if (hint) {
140
+ lines.push('', hint.message);
141
+ }
142
+ return lines.join('\n');
143
+ }
144
+ function planPriceCell(tier) {
145
+ if (!tier)
146
+ return '—';
147
+ return formatPriceUsd(tier.price_usd);
148
+ }
149
+ function plansMarkdown(plans) {
150
+ const lines = ['## Paytaca AI plans', ''];
151
+ if (plans.length === 0) {
152
+ lines.push('No plans found.');
153
+ return lines.join('\n');
154
+ }
155
+ const durations = Array.from(new Set(plans.flatMap((model) => model.plans.map((plan) => Number(plan.minutes))))).sort((a, b) => a - b);
156
+ const header = ['Model', ...durations.map(formatDuration)];
157
+ lines.push(`| ${header.join(' | ')} |`);
158
+ lines.push(`| ${header.map(() => '---').join(' | ')} |`);
159
+ for (const model of plans) {
160
+ const byMinutes = new Map(model.plans.map((plan) => [Number(plan.minutes), plan]));
161
+ const cells = [
162
+ `${model.displayName} (\`${model.id}\`)`,
163
+ ...durations.map((minutes) => planPriceCell(byMinutes.get(minutes))),
164
+ ];
165
+ lines.push(`| ${cells.join(' | ')} |`);
166
+ }
167
+ return lines.join('\n');
168
+ }
169
+ function plansText(plans) {
170
+ const lines = ['Paytaca AI plans', ''];
171
+ if (plans.length === 0) {
172
+ lines.push('No plans found.');
173
+ return lines.join('\n');
174
+ }
175
+ const durations = Array.from(new Set(plans.flatMap((model) => model.plans.map((plan) => Number(plan.minutes))))).sort((a, b) => a - b);
176
+ const headers = ['Model', ...durations.map(formatDuration)];
177
+ const rows = plans.map((model) => {
178
+ const byMinutes = new Map(model.plans.map((plan) => [Number(plan.minutes), plan]));
179
+ return [
180
+ `${model.displayName} (${model.id})`,
181
+ ...durations.map((minutes) => planPriceCell(byMinutes.get(minutes))),
182
+ ];
183
+ });
184
+ const widths = headers.map((header, i) => Math.max(header.length, ...rows.map((row) => row[i].length)));
185
+ const render = (cells) => cells.map((cell, i) => cell.padEnd(widths[i])).join(' ');
186
+ lines.push(render(headers));
187
+ for (const row of rows) {
188
+ lines.push(render(row));
189
+ }
190
+ return lines.join('\n');
191
+ }
192
+ function plansResponse(server, plans) {
193
+ const name = clientName(server);
194
+ if (name.startsWith('pi-mcp')) {
195
+ return {
196
+ content: [{ type: 'text', text: plansText(plans) }],
197
+ structuredContent: { models: plans },
198
+ };
199
+ }
200
+ if (name === 'opencode') {
201
+ return {
202
+ content: [{ type: 'text', text: plansMarkdown(plans) }],
203
+ structuredContent: { models: plans },
204
+ };
205
+ }
206
+ return json(plans);
207
+ }
208
+ function fail(err) {
209
+ const message = err instanceof Error ? err.message : String(err);
210
+ return { content: [{ type: 'text', text: message }], isError: true };
211
+ }
212
+ async function resolvePurchaseHint(modelQuery, backend) {
213
+ try {
214
+ const config = await getConfig({ backendUrl: backend });
215
+ const model = modelQuery ? selectModel(listModels(config), modelQuery) : null;
216
+ return buildPurchaseHint(model);
217
+ }
218
+ catch {
219
+ return null;
220
+ }
221
+ }
222
+ const HELP = `Paytaca MCP tools
223
+
224
+ Wallet (read):
225
+ get_balance BCH balance (mainnet/chipnet)
226
+ get_transactions Paginated transaction history
227
+ get_receiving_address Derive a receiving address (+ BIP21/PayPro URI)
228
+ get_tokens List CashToken balances or inspect one category
229
+
230
+ Wallet (spend):
231
+ send Send BCH or a CashToken to an address
232
+
233
+ Paytaca AI:
234
+ get_models List AI models
235
+ get_plans Plan pricing per AI model
236
+ get_credits Remaining AI time credits
237
+ buy_plan Buy an AI plan with BCH or LIFT (spends funds)
238
+ auto_refill Arm/disarm/inspect automatic plan refills
239
+
240
+ Image generation:
241
+ generate_image Generate an image from a prompt (spends funds)
242
+ get_image_status Poll/resume a pending image generation order
243
+ get_image_models List image generation models
244
+ get_image_history Image generation order history
245
+
246
+ Notes:
247
+ - All tools accept an optional "chipnet" flag (default mainnet).
248
+ - send, buy_plan, and generate_image spend real funds from the active
249
+ wallet; the MCP host is responsible for asking the user for approval
250
+ before invoking them.
251
+ - When get_credits reports an inactive session it includes a purchaseHint.
252
+ Relay purchaseHint.message (it contains the exact copy-paste command to
253
+ buy more time); do not replace it with upsell or "what next" questions.
254
+ - Wallet is resolved from the OS keychain (run "paytaca wallet create" first).`;
255
+ export function registerTools(server, opts = {}) {
256
+ const cn = (value) => value ?? Boolean(opts.defaultChipnet);
257
+ server.registerTool('get_help', {
258
+ title: 'Paytaca help',
259
+ description: 'List the Paytaca MCP tools and usage notes.',
260
+ inputSchema: {},
261
+ annotations: { readOnlyHint: true, openWorldHint: false },
262
+ }, async () => text(HELP));
263
+ server.registerTool('get_balance', {
264
+ title: 'Get BCH balance',
265
+ description: 'Return the BCH balance (in BCH and sats) of the active wallet.',
266
+ inputSchema: { chipnet: z.boolean().optional() },
267
+ annotations: { readOnlyHint: true, openWorldHint: true },
268
+ }, async ({ chipnet }) => {
269
+ try {
270
+ return json(await getBalanceView(cn(chipnet)));
271
+ }
272
+ catch (err) {
273
+ return fail(err);
274
+ }
275
+ });
276
+ server.registerTool('get_transactions', {
277
+ title: 'Get transaction history',
278
+ description: 'Return paginated BCH/CashToken transaction history.',
279
+ inputSchema: {
280
+ chipnet: z.boolean().optional(),
281
+ type: z.enum(['all', 'incoming', 'outgoing']).optional(),
282
+ page: z.number().int().positive().optional(),
283
+ token_category: z.string().optional(),
284
+ },
285
+ annotations: { readOnlyHint: true, openWorldHint: true },
286
+ }, async ({ chipnet, type, page, token_category }) => {
287
+ try {
288
+ return json(await getHistoryView({
289
+ page: page ?? 1,
290
+ recordType: type ?? 'all',
291
+ tokenId: token_category || '',
292
+ }, cn(chipnet)));
293
+ }
294
+ catch (err) {
295
+ return fail(err);
296
+ }
297
+ });
298
+ server.registerTool('get_receiving_address', {
299
+ title: 'Get receiving address',
300
+ description: 'Derive a receiving address. With token_category, returns a token-aware (z-prefix) address and a PayPro URI. With amount, embeds the requested amount.',
301
+ inputSchema: {
302
+ chipnet: z.boolean().optional(),
303
+ index: z.number().int().nonnegative().optional(),
304
+ token: z.boolean().optional(),
305
+ token_category: z.string().optional(),
306
+ amount: z.number().positive().optional(),
307
+ },
308
+ annotations: { readOnlyHint: true, openWorldHint: true },
309
+ }, async ({ chipnet, index, token, token_category, amount }) => {
310
+ try {
311
+ return json(await getReceiveAddressView({
312
+ index: index ?? 0,
313
+ token: Boolean(token) || Boolean(token_category),
314
+ category: token_category,
315
+ amount,
316
+ }, cn(chipnet)));
317
+ }
318
+ catch (err) {
319
+ return fail(err);
320
+ }
321
+ });
322
+ server.registerTool('get_tokens', {
323
+ title: 'Get CashToken balances',
324
+ description: 'List CashToken holdings, or return details for one token when token_category is given.',
325
+ inputSchema: {
326
+ chipnet: z.boolean().optional(),
327
+ token_category: z.string().optional(),
328
+ include_zero: z.boolean().optional(),
329
+ },
330
+ annotations: { readOnlyHint: true, openWorldHint: true },
331
+ }, async ({ chipnet, token_category, include_zero }) => {
332
+ try {
333
+ if (token_category) {
334
+ return json(await getTokenDetail(token_category, cn(chipnet)));
335
+ }
336
+ return json(await getTokenBalances(cn(chipnet), {
337
+ includeZero: Boolean(include_zero),
338
+ }));
339
+ }
340
+ catch (err) {
341
+ return fail(err);
342
+ }
343
+ });
344
+ server.registerTool('send', {
345
+ title: 'Send funds',
346
+ description: 'Send BCH, or a CashToken when token_category is given. Token amounts are in base units. SPENDS REAL FUNDS.',
347
+ inputSchema: {
348
+ address: z.string(),
349
+ amount: z.string(),
350
+ unit: z.enum(['bch', 'sats']).optional(),
351
+ token_category: z.string().optional(),
352
+ chipnet: z.boolean().optional(),
353
+ },
354
+ annotations: {
355
+ readOnlyHint: false,
356
+ destructiveHint: true,
357
+ idempotentHint: false,
358
+ openWorldHint: true,
359
+ },
360
+ }, async ({ address, amount, unit, token_category, chipnet }) => {
361
+ const isChipnet = cn(chipnet);
362
+ try {
363
+ if (!isValidBchAddress(address, isChipnet)) {
364
+ return fail(new Error('Invalid BCH address.'));
365
+ }
366
+ if (token_category) {
367
+ let tokenAmount;
368
+ try {
369
+ tokenAmount = BigInt(amount);
370
+ }
371
+ catch {
372
+ return fail(new Error('Token amount must be an integer.'));
373
+ }
374
+ if (tokenAmount <= 0n) {
375
+ return fail(new Error('Token amount must be positive.'));
376
+ }
377
+ return json(await sendToken({ category: token_category, amount: tokenAmount, address }, isChipnet));
378
+ }
379
+ const parsed = Number(amount);
380
+ if (!isFinite(parsed) || parsed <= 0) {
381
+ return fail(new Error('Amount must be a positive number.'));
382
+ }
383
+ const amountBch = unit === 'sats' ? parsed / 1e8 : parsed;
384
+ return json(await sendBch({ address, amountBch }, isChipnet));
385
+ }
386
+ catch (err) {
387
+ return fail(err);
388
+ }
389
+ });
390
+ server.registerTool('get_models', {
391
+ title: 'List AI models',
392
+ description: 'List the Paytaca AI models available for purchase.',
393
+ inputSchema: { backend: z.string().optional() },
394
+ annotations: { readOnlyHint: true, openWorldHint: true },
395
+ }, async ({ backend }) => {
396
+ try {
397
+ const config = await getConfig({ backendUrl: backend });
398
+ return json(listModels(config));
399
+ }
400
+ catch (err) {
401
+ return fail(err);
402
+ }
403
+ });
404
+ server.registerTool('get_plans', {
405
+ title: 'Get AI plan pricing',
406
+ description: 'Return plan pricing per model. Pass model to filter a single model.',
407
+ inputSchema: {
408
+ model: z.string().optional(),
409
+ backend: z.string().optional(),
410
+ },
411
+ annotations: { readOnlyHint: true, openWorldHint: true },
412
+ }, async ({ model, backend }) => {
413
+ try {
414
+ const config = await getConfig({ backendUrl: backend });
415
+ return plansResponse(server, listPlans(config, model));
416
+ }
417
+ catch (err) {
418
+ return fail(err);
419
+ }
420
+ });
421
+ server.registerTool('get_credits', {
422
+ title: 'Get AI time credits',
423
+ description: 'Return remaining Paytaca AI time credits for all models, or for one model when "model" is given. When a session is inactive, includes a purchaseHint whose "message" is the exact text to relay to the user (a copy-paste top-up command) instead of upsell or follow-up questions. Armed auto-refill state is reported under "autoRefill"; refills themselves run in the background, never from this read-only call.',
424
+ inputSchema: {
425
+ model: z.string().optional(),
426
+ chipnet: z.boolean().optional(),
427
+ backend: z.string().optional(),
428
+ },
429
+ annotations: { readOnlyHint: true, openWorldHint: true },
430
+ }, async ({ model, chipnet, backend }) => {
431
+ try {
432
+ const wallet = loadWalletRef();
433
+ if (!wallet)
434
+ throw new WalletNotConfiguredError();
435
+ const status = await getWalletStatus(wallet.walletHash, {
436
+ modelId: model,
437
+ backendUrl: backend,
438
+ });
439
+ const auto = readAutoRefillState();
440
+ if (!model) {
441
+ const sessions = summarizeAllCredits(status);
442
+ const payload = { sessions };
443
+ const autoField = autoRefillField(auto);
444
+ if (autoField)
445
+ payload.autoRefill = autoField;
446
+ let hint = null;
447
+ if (!sessions.some((s) => s.active)) {
448
+ hint = await resolvePurchaseHint(sessions[0]?.modelId ?? undefined, backend);
449
+ if (hint)
450
+ payload.purchaseHint = hint;
451
+ }
452
+ return creditsResponse(server, sessions, hint, payload);
453
+ }
454
+ const summary = summarizeCredits(status, model);
455
+ const payload = { ...summary };
456
+ const autoField = autoRefillField(auto);
457
+ if (autoField)
458
+ payload.autoRefill = autoField;
459
+ let hint = null;
460
+ if (!summary.active) {
461
+ hint = await resolvePurchaseHint(model, backend);
462
+ if (hint)
463
+ payload.purchaseHint = hint;
464
+ }
465
+ return creditsResponse(server, [summary], hint, payload);
466
+ }
467
+ catch (err) {
468
+ return fail(err);
469
+ }
470
+ });
471
+ server.registerTool('buy_plan', {
472
+ title: 'Buy an AI plan',
473
+ description: 'Purchase an AI plan for a model and duration, paying with BCH or LIFT via x402. SPENDS REAL FUNDS. The MCP host must obtain user approval before calling this.',
474
+ inputSchema: {
475
+ model: z.string(),
476
+ minutes: z.number().int().positive(),
477
+ payment_method: z.enum(['bch', 'lift']).optional(),
478
+ chipnet: z.boolean().optional(),
479
+ backend: z.string().optional(),
480
+ },
481
+ annotations: {
482
+ readOnlyHint: false,
483
+ destructiveHint: true,
484
+ idempotentHint: false,
485
+ openWorldHint: true,
486
+ },
487
+ }, async ({ model, minutes, payment_method, chipnet, backend }) => {
488
+ try {
489
+ return json(await buyPlan({
490
+ model,
491
+ minutes,
492
+ paymentMethod: payment_method ?? 'bch',
493
+ isChipnet: cn(chipnet),
494
+ backendUrl: backend,
495
+ confirmed: true,
496
+ }));
497
+ }
498
+ catch (err) {
499
+ return fail(err);
500
+ }
501
+ });
502
+ server.registerTool('auto_refill', {
503
+ title: 'Manage auto-refill',
504
+ description: 'Arm, disarm, delete, or inspect automatic plan refills. Arming buys a plan silently when credits run out, up to max_minutes, and runs one immediate refill check (buys right away if the armed model has no active credits). Each execution is recorded in ~/.paytaca/auto-refill-events.jsonl and surfaced as lastEvent. delete=true removes the armed state (history is kept).',
505
+ inputSchema: {
506
+ enabled: z.boolean().optional(),
507
+ model: z.string().optional(),
508
+ minutes: z.number().int().positive().optional(),
509
+ max_minutes: z.number().int().positive().optional(),
510
+ payment_method: z.enum(['bch', 'lift']).optional(),
511
+ chipnet: z.boolean().optional(),
512
+ delete: z.boolean().optional(),
513
+ },
514
+ annotations: {
515
+ readOnlyHint: false,
516
+ destructiveHint: true,
517
+ idempotentHint: false,
518
+ openWorldHint: false,
519
+ },
520
+ }, async ({ enabled, model, minutes, max_minutes, payment_method, chipnet, delete: del }) => {
521
+ try {
522
+ if (del) {
523
+ const existed = deleteAutoRefill();
524
+ return json({ deleted: true, existed });
525
+ }
526
+ if (enabled === false) {
527
+ return json(disarmAutoRefill());
528
+ }
529
+ if (enabled === true) {
530
+ if (!model || !minutes || !max_minutes) {
531
+ return fail(new Error('Arming auto-refill requires model, minutes, and max_minutes.'));
532
+ }
533
+ const state = armAutoRefill({
534
+ model,
535
+ minutes,
536
+ maxMinutes: max_minutes,
537
+ paymentMethod: payment_method ?? 'bch',
538
+ });
539
+ let initialTick = null;
540
+ const wallet = loadWalletRef();
541
+ if (wallet?.canSign) {
542
+ try {
543
+ initialTick = tickSummary(await autoRefillTick(createRefillTickDeps(cn(chipnet))));
544
+ }
545
+ catch (err) {
546
+ initialTick = { action: 'skipped', reason: err?.message || String(err) };
547
+ }
548
+ }
549
+ return json({ ...state, initialTick });
550
+ }
551
+ const state = readAutoRefillState();
552
+ return json({
553
+ armed: Boolean(state?.enabled),
554
+ remainingMinutes: remainingBudget(state),
555
+ refillCount: state?.refillCount ?? 0,
556
+ lastEvent: state?.lastEvent ?? null,
557
+ state,
558
+ });
559
+ }
560
+ catch (err) {
561
+ return fail(err);
562
+ }
563
+ });
564
+ server.registerTool('get_image_models', {
565
+ title: 'List image models',
566
+ description: 'List the image generation models available through Paytaca AI.',
567
+ inputSchema: { backend: z.string().optional() },
568
+ annotations: { readOnlyHint: true, openWorldHint: true },
569
+ }, async ({ backend }) => {
570
+ try {
571
+ return json(await listImageModels({ backendUrl: backend }));
572
+ }
573
+ catch (err) {
574
+ return fail(err);
575
+ }
576
+ });
577
+ server.registerTool('get_image_history', {
578
+ title: 'Get image order history',
579
+ description: 'Return paginated Paytaca image generation order history.',
580
+ inputSchema: {
581
+ page: z.number().int().positive().optional(),
582
+ page_size: z.number().int().positive().optional(),
583
+ backend: z.string().optional(),
584
+ },
585
+ annotations: { readOnlyHint: true, openWorldHint: true },
586
+ }, async ({ page, page_size, backend }) => {
587
+ try {
588
+ return json(await getImageHistory({ page, pageSize: page_size, backendUrl: backend }));
589
+ }
590
+ catch (err) {
591
+ return fail(err);
592
+ }
593
+ });
594
+ server.registerTool('get_image_status', {
595
+ title: 'Poll image generation status',
596
+ description: 'Poll an existing image generation order by ID. If generation is complete, returns the image inline plus the saved file path. If still processing, returns the current status so you can retry later. Use this to resume after generate_image returns a "processing" status.',
597
+ inputSchema: {
598
+ order_id: z.string().min(1),
599
+ chipnet: z.boolean().optional(),
600
+ backend: z.string().optional(),
601
+ },
602
+ annotations: {
603
+ readOnlyHint: true,
604
+ destructiveHint: false,
605
+ idempotentHint: true,
606
+ openWorldHint: false,
607
+ },
608
+ }, async ({ order_id, chipnet, backend }) => {
609
+ try {
610
+ const result = await getImageOrderStatus(order_id, {
611
+ isChipnet: cn(chipnet),
612
+ backendUrl: backend,
613
+ });
614
+ if (result.success && result.base64) {
615
+ const mediaType = result.mediaType || 'image/png';
616
+ return {
617
+ content: [
618
+ { type: 'image', data: result.base64, mimeType: mediaType },
619
+ {
620
+ type: 'text',
621
+ text: [
622
+ `Image ready (order ${result.orderId}).`,
623
+ `Saved to: ${result.path}`,
624
+ `Model: ${result.model}`,
625
+ `Cost: ${result.amountSats} sats`,
626
+ `txid: ${result.txid}`,
627
+ ].join('\n'),
628
+ },
629
+ ],
630
+ structuredContent: {
631
+ orderId: result.orderId,
632
+ model: result.model,
633
+ status: result.status,
634
+ path: result.path,
635
+ mediaType: result.mediaType,
636
+ amountSats: result.amountSats,
637
+ amountUsd: result.amountUsd,
638
+ txid: result.txid,
639
+ },
640
+ };
641
+ }
642
+ if (isStillProcessing(result)) {
643
+ return {
644
+ content: [
645
+ {
646
+ type: 'text',
647
+ text: [
648
+ `Image generation is still processing (order ${result.orderId}).`,
649
+ `Try again in a few seconds with get_image_status.`,
650
+ ].join('\n'),
651
+ },
652
+ ],
653
+ structuredContent: {
654
+ orderId: result.orderId,
655
+ status: 'processing',
656
+ },
657
+ };
658
+ }
659
+ return fail(new Error(result.error || 'Image generation failed.'));
660
+ }
661
+ catch (err) {
662
+ return fail(err);
663
+ }
664
+ });
665
+ server.registerTool('generate_image', {
666
+ title: 'Generate an image',
667
+ description: 'Generate an image from a text prompt with a Paytaca AI image model. Pays BCH on-chain from the active wallet (SPENDS REAL FUNDS). Returns the image inline plus the file path where it was saved. The MCP host must obtain user approval before calling this.',
668
+ inputSchema: {
669
+ prompt: z.string().min(1),
670
+ model: z.string().optional(),
671
+ aspect_ratio: z.string().optional(),
672
+ quality: z.string().optional(),
673
+ resolution: z.string().optional(),
674
+ chipnet: z.boolean().optional(),
675
+ backend: z.string().optional(),
676
+ },
677
+ annotations: {
678
+ readOnlyHint: false,
679
+ destructiveHint: true,
680
+ idempotentHint: false,
681
+ openWorldHint: true,
682
+ },
683
+ }, async ({ prompt, model, aspect_ratio, quality, resolution, chipnet, backend }) => {
684
+ try {
685
+ const result = await generateImage({
686
+ prompt,
687
+ model,
688
+ aspectRatio: aspect_ratio,
689
+ quality,
690
+ resolution,
691
+ isChipnet: cn(chipnet),
692
+ backendUrl: backend,
693
+ });
694
+ if (isStillProcessing(result)) {
695
+ return {
696
+ content: [
697
+ {
698
+ type: 'text',
699
+ text: [
700
+ `Image generation is still processing (order ${result.orderId}).`,
701
+ `The generation is running server-side — call get_image_status with order_id "${result.orderId}" to retrieve the result.`,
702
+ `Model: ${result.model}`,
703
+ `Paid: ${result.amountSats} sats (txid: ${result.txid})`,
704
+ ].join('\n'),
705
+ },
706
+ ],
707
+ structuredContent: {
708
+ orderId: result.orderId,
709
+ model: result.model,
710
+ status: 'processing',
711
+ amountSats: result.amountSats,
712
+ amountUsd: result.amountUsd,
713
+ txid: result.txid,
714
+ },
715
+ };
716
+ }
717
+ if (!result.success || !result.base64) {
718
+ return fail(new Error(result.error || 'Image generation failed.'));
719
+ }
720
+ const mediaType = result.mediaType || 'image/png';
721
+ return {
722
+ content: [
723
+ { type: 'image', data: result.base64, mimeType: mediaType },
724
+ {
725
+ type: 'text',
726
+ text: [
727
+ `Image generated (order ${result.orderId}).`,
728
+ `Saved to: ${result.path}`,
729
+ `Model: ${result.model}`,
730
+ `Cost: ${result.amountSats} sats`,
731
+ `txid: ${result.txid}`,
732
+ ].join('\n'),
733
+ },
734
+ ],
735
+ structuredContent: {
736
+ orderId: result.orderId,
737
+ model: result.model,
738
+ status: result.status,
739
+ path: result.path,
740
+ mediaType: result.mediaType,
741
+ amountSats: result.amountSats,
742
+ amountUsd: result.amountUsd,
743
+ txid: result.txid,
744
+ },
745
+ };
746
+ }
747
+ catch (err) {
748
+ return fail(err);
749
+ }
750
+ });
751
+ }
752
+ //# sourceMappingURL=tools.js.map