@agentwonderland/mcp 0.1.22 → 0.1.24

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 (74) hide show
  1. package/dist/core/__tests__/card-setup.test.d.ts +1 -0
  2. package/dist/core/__tests__/card-setup.test.js +99 -0
  3. package/dist/core/__tests__/formatters.test.d.ts +1 -0
  4. package/dist/core/__tests__/formatters.test.js +15 -0
  5. package/dist/core/__tests__/passes.test.d.ts +1 -0
  6. package/dist/core/__tests__/passes.test.js +82 -0
  7. package/dist/core/__tests__/payments.test.d.ts +1 -0
  8. package/dist/core/__tests__/payments.test.js +52 -0
  9. package/dist/core/__tests__/principal.test.d.ts +1 -0
  10. package/dist/core/__tests__/principal.test.js +67 -0
  11. package/dist/core/api-client.d.ts +9 -4
  12. package/dist/core/api-client.js +52 -22
  13. package/dist/core/card-setup.d.ts +20 -13
  14. package/dist/core/card-setup.js +87 -30
  15. package/dist/core/config.d.ts +4 -0
  16. package/dist/core/config.js +28 -1
  17. package/dist/core/formatters.d.ts +2 -0
  18. package/dist/core/formatters.js +5 -1
  19. package/dist/core/index.d.ts +2 -0
  20. package/dist/core/index.js +2 -0
  21. package/dist/core/ows-adapter.d.ts +10 -2
  22. package/dist/core/ows-adapter.js +54 -10
  23. package/dist/core/passes.d.ts +40 -0
  24. package/dist/core/passes.js +32 -0
  25. package/dist/core/payments.d.ts +8 -0
  26. package/dist/core/payments.js +121 -16
  27. package/dist/core/principal.d.ts +2 -0
  28. package/dist/core/principal.js +109 -0
  29. package/dist/core/solana-charge.d.ts +9 -0
  30. package/dist/core/solana-charge.js +95 -0
  31. package/dist/core/types.d.ts +10 -0
  32. package/dist/index.js +13 -4
  33. package/dist/prompts/index.js +1 -1
  34. package/dist/resources/wallet.js +8 -1
  35. package/dist/tools/__tests__/_payment-confirmation.test.d.ts +1 -0
  36. package/dist/tools/__tests__/_payment-confirmation.test.js +30 -0
  37. package/dist/tools/_payment-confirmation.d.ts +6 -0
  38. package/dist/tools/_payment-confirmation.js +28 -0
  39. package/dist/tools/agent-info.js +14 -0
  40. package/dist/tools/index.d.ts +1 -0
  41. package/dist/tools/index.js +1 -0
  42. package/dist/tools/passes.d.ts +2 -0
  43. package/dist/tools/passes.js +157 -0
  44. package/dist/tools/run.js +116 -49
  45. package/dist/tools/solve.js +102 -44
  46. package/dist/tools/wallet.js +85 -50
  47. package/package.json +3 -1
  48. package/src/core/__tests__/card-setup.test.ts +118 -0
  49. package/src/core/__tests__/formatters.test.ts +17 -0
  50. package/src/core/__tests__/passes.test.ts +94 -0
  51. package/src/core/__tests__/payments.test.ts +60 -0
  52. package/src/core/__tests__/principal.test.ts +87 -0
  53. package/src/core/api-client.ts +70 -23
  54. package/src/core/card-setup.ts +112 -35
  55. package/src/core/config.ts +33 -2
  56. package/src/core/formatters.ts +7 -1
  57. package/src/core/index.ts +2 -0
  58. package/src/core/ows-adapter.ts +74 -8
  59. package/src/core/passes.ts +74 -0
  60. package/src/core/payments.ts +140 -15
  61. package/src/core/principal.ts +128 -0
  62. package/src/core/solana-charge.ts +149 -0
  63. package/src/core/types.ts +10 -0
  64. package/src/index.ts +13 -4
  65. package/src/prompts/index.ts +1 -1
  66. package/src/resources/wallet.ts +8 -1
  67. package/src/tools/__tests__/_payment-confirmation.test.ts +45 -0
  68. package/src/tools/_payment-confirmation.ts +52 -0
  69. package/src/tools/agent-info.ts +23 -0
  70. package/src/tools/index.ts +1 -0
  71. package/src/tools/passes.ts +234 -0
  72. package/src/tools/run.ts +174 -53
  73. package/src/tools/solve.ts +149 -56
  74. package/src/tools/wallet.ts +102 -52
@@ -1,51 +1,78 @@
1
- import QRCode from "qrcode";
2
- import { apiPost, apiGet } from "./api-client.js";
3
- import { getApiUrl, getCardConfig, setCardConfig } from "./config.js";
1
+ import { apiPost, apiGet, ApiError } from "./api-client.js";
2
+ import {
3
+ getApiUrl,
4
+ setCardConfig,
5
+ getPendingCardSetupToken,
6
+ setPendingCardSetupToken,
7
+ } from "./config.js";
8
+
9
+ export interface CardSetupResult {
10
+ last4: string;
11
+ brand: string;
12
+ consumerToken: string;
13
+ }
14
+
15
+ export interface CardCapabilities {
16
+ spt_status: "enabled" | "unavailable" | "unknown";
17
+ message?: string;
18
+ }
19
+
20
+ let cachedCapabilities:
21
+ | { value: CardCapabilities; expiresAt: number; apiUrl: string }
22
+ | null = null;
23
+
24
+ function buildCardSetupUrl(token: string): string {
25
+ return `${getApiUrl()}/card/handoff/${token}`;
26
+ }
4
27
 
5
28
  /**
6
- * Initiate card setup creates a Stripe Checkout session and returns
7
- * QR code + short URL for the user to scan.
29
+ * Create a new card setup session or resume the pending one from config.
8
30
  */
9
- export async function initiateCardSetup(): Promise<{
10
- qr: string;
31
+ export async function getOrCreatePendingCardSetup(): Promise<{
11
32
  url: string;
12
33
  token: string;
34
+ isNew: boolean;
13
35
  }> {
14
- const { url: _checkoutUrl, token } = await apiPost<{ url: string; token: string }>("/card/setup", {});
15
-
16
- // Short redirect URL keeps the QR code small and scannable
17
- const apiUrl = getApiUrl();
18
- const shortUrl = `${apiUrl}/card/link/${token}`;
19
-
20
- const rawQr = await QRCode.toString(shortUrl, {
21
- type: "utf8",
22
- errorCorrectionLevel: "L",
23
- margin: 2,
24
- });
36
+ const pendingToken = getPendingCardSetupToken();
37
+ if (pendingToken) {
38
+ const url = buildCardSetupUrl(pendingToken);
39
+ return {
40
+ url,
41
+ token: pendingToken,
42
+ isNew: false,
43
+ };
44
+ }
25
45
 
26
- // Pad every line with consistent leading spaces so the QR aligns
27
- // properly in terminal renderers that add prefixes to the first line
28
- const qr = rawQr.split("\n").map((line) => " " + line).join("\n");
46
+ const { token } = await apiPost<{ url: string; token: string }>("/card/setup", {});
47
+ setPendingCardSetupToken(token);
48
+ const url = buildCardSetupUrl(token);
29
49
 
30
- return { qr, url: shortUrl, token };
50
+ return {
51
+ url,
52
+ token,
53
+ isNew: true,
54
+ };
31
55
  }
32
56
 
33
57
  /**
34
- * Format the card setup prompt as multiple content blocks.
35
- * Block 1: QR code (may be collapsed in some clients)
36
- * Block 2: URL + instructions (short, always visible)
58
+ * Format the card setup prompt as a single text block.
59
+ *
60
+ * Some MCP clients only surface one text chunk or collapse multi-part tool
61
+ * output, which can hide the QR code entirely. Keeping everything in one
62
+ * message makes the browser handoff much more reliable.
37
63
  */
38
- export function formatCardSetupBlocks(qr: string, url: string): string[] {
64
+ export function formatCardSetupBlocks(url: string): string[] {
39
65
  return [
40
- "\n" + qr.trim(),
41
66
  [
42
- `IMPORTANT: You MUST present this link to the user so they can connect a payment card:`,
67
+ "Open this setup page to connect your card:",
43
68
  "",
44
69
  url,
45
70
  "",
46
- `Tell the user: "Scan the QR code above or open this link to connect your card. Let me know when you're done."`,
71
+ "The setup page will take you to Stripe to securely save the card.",
72
+ "",
73
+ `Tell the user: "Open this setup page to connect your card. Let me know when you're done."`,
47
74
  `After they confirm, call wallet_setup({ action: "add-card" }) to complete setup.`,
48
- `Crypto wallets (Base/Tempo USDC) are also available via wallet_setup({ action: "create" }).`,
75
+ `Crypto wallets (Tempo, Base, or Solana USDC) are also available via wallet_setup({ action: "create" }).`,
49
76
  ].join("\n"),
50
77
  ];
51
78
  }
@@ -56,39 +83,89 @@ export function formatCardSetupBlocks(qr: string, url: string): string[] {
56
83
  export async function pollCardSetup(
57
84
  token: string,
58
85
  timeoutMs = 120_000,
59
- ): Promise<{ last4: string; brand: string; consumerToken: string } | null> {
86
+ ): Promise<CardSetupResult | null> {
60
87
  const deadline = Date.now() + timeoutMs;
61
88
 
62
89
  while (Date.now() < deadline) {
63
- await new Promise((r) => setTimeout(r, 3000));
64
90
  try {
65
91
  const status = await apiGet<{
66
92
  status: string;
67
93
  card_last4?: string;
68
94
  card_brand?: string;
69
95
  consumer_token?: string;
96
+ payment_method_id?: string;
70
97
  }>(`/card/status?token=${token}`);
71
98
 
72
99
  if (status.status === "complete" && status.consumer_token) {
73
- const card = {
100
+ const card: CardSetupResult = {
74
101
  last4: status.card_last4 ?? "????",
75
102
  brand: status.card_brand ?? "card",
76
103
  consumerToken: status.consumer_token,
77
104
  };
78
105
 
79
- // Persist to config
106
+ // Persist to config (including paymentMethodId for mppx stripe charge)
80
107
  setCardConfig({
81
108
  consumerToken: card.consumerToken,
109
+ paymentMethodId: status.payment_method_id,
82
110
  last4: card.last4,
83
111
  brand: card.brand,
84
112
  });
113
+ setPendingCardSetupToken(null);
85
114
 
86
115
  return card;
87
116
  }
88
- } catch {
117
+ } catch (err) {
118
+ if (err instanceof ApiError && err.status === 404) {
119
+ setPendingCardSetupToken(null);
120
+ return null;
121
+ }
89
122
  // Keep polling
90
123
  }
124
+
125
+ const remainingMs = deadline - Date.now();
126
+ if (remainingMs <= 0) break;
127
+ await new Promise((r) => setTimeout(r, Math.min(3000, remainingMs)));
91
128
  }
92
129
 
93
130
  return null;
94
131
  }
132
+
133
+ export async function getCardCapabilities(): Promise<CardCapabilities> {
134
+ const apiUrl = getApiUrl();
135
+ if (cachedCapabilities && cachedCapabilities.apiUrl === apiUrl && cachedCapabilities.expiresAt > Date.now()) {
136
+ return cachedCapabilities.value;
137
+ }
138
+
139
+ try {
140
+ const capabilities = await apiGet<CardCapabilities>("/card/capabilities");
141
+ cachedCapabilities = {
142
+ value: capabilities,
143
+ apiUrl,
144
+ expiresAt: Date.now() + 30_000,
145
+ };
146
+ return capabilities;
147
+ } catch (err) {
148
+ if (err instanceof ApiError) {
149
+ const fallback = {
150
+ spt_status: "unknown",
151
+ message: err.message,
152
+ } satisfies CardCapabilities;
153
+ cachedCapabilities = {
154
+ value: fallback,
155
+ apiUrl,
156
+ expiresAt: Date.now() + 10_000,
157
+ };
158
+ return fallback;
159
+ }
160
+ const fallback = {
161
+ spt_status: "unknown",
162
+ message: "Could not determine card payment availability.",
163
+ } satisfies CardCapabilities;
164
+ cachedCapabilities = {
165
+ value: fallback,
166
+ apiUrl,
167
+ expiresAt: Date.now() + 10_000,
168
+ };
169
+ return fallback;
170
+ }
171
+ }
@@ -9,7 +9,7 @@ export interface WalletEntry {
9
9
  keyType: "evm" | "ows"; // key format: raw EVM key or OWS-managed wallet
10
10
  key?: string; // private key (legacy / raw EVM only)
11
11
  owsWalletId?: string; // OWS wallet reference (when keyType === "ows")
12
- chains: string[]; // enabled chains: "tempo", "base"
12
+ chains: string[]; // enabled chains: "tempo", "base", "solana"
13
13
  defaultChain?: string; // which chain to use by default for this wallet
14
14
  label?: string; // user-friendly name
15
15
  }
@@ -27,7 +27,9 @@ export interface Config {
27
27
  userId: string | null;
28
28
  wallets: WalletEntry[];
29
29
  defaultWallet: string | null;
30
+ defaultPaymentMethod?: string;
30
31
  card: CardConfig | null;
32
+ pendingCardSetupToken?: string | null;
31
33
  favorites: string[];
32
34
  /** Require user confirmation before spending. Default: true. Set false for headless/automated use. */
33
35
  confirmBeforeSpend: boolean;
@@ -79,6 +81,7 @@ interface LegacyConfig {
79
81
  wallets?: WalletEntry[];
80
82
  defaultWallet?: string | null;
81
83
  card?: CardConfig | null;
84
+ pendingCardSetupToken?: string | null;
82
85
  }
83
86
 
84
87
  /**
@@ -95,7 +98,9 @@ function migrateIfNeeded(raw: LegacyConfig): Config {
95
98
  userId: raw.userId ?? null,
96
99
  wallets: raw.wallets,
97
100
  defaultWallet: raw.defaultWallet ?? null,
101
+ defaultPaymentMethod: raw.defaultPaymentMethod ?? undefined,
98
102
  card: raw.card ?? null,
103
+ pendingCardSetupToken: (r.pendingCardSetupToken as string | null | undefined) ?? null,
99
104
  favorites: r.favorites as string[] ?? [],
100
105
  confirmBeforeSpend: r.confirmBeforeSpend !== false,
101
106
  defaultTipAmount: typeof r.defaultTipAmount === "number" ? r.defaultTipAmount : 0,
@@ -167,7 +172,9 @@ function migrateIfNeeded(raw: LegacyConfig): Config {
167
172
  userId: raw.userId ?? null,
168
173
  wallets,
169
174
  defaultWallet,
175
+ defaultPaymentMethod: raw.defaultPaymentMethod ?? undefined,
170
176
  card,
177
+ pendingCardSetupToken: null,
171
178
  favorites: [],
172
179
  confirmBeforeSpend: true,
173
180
  defaultTipAmount: 0,
@@ -192,6 +199,7 @@ export function getConfig(): Config {
192
199
  wallets: [],
193
200
  defaultWallet: null,
194
201
  card: null,
202
+ pendingCardSetupToken: null,
195
203
  favorites: [],
196
204
  confirmBeforeSpend: true,
197
205
  defaultTipAmount: 0,
@@ -350,7 +358,30 @@ export function getCardConfig(): CardConfig | null {
350
358
  * Save card configuration after setup.
351
359
  */
352
360
  export function setCardConfig(card: CardConfig | null): void {
353
- saveConfig({ card });
361
+ const current = getConfig();
362
+ if (card) {
363
+ saveConfig({
364
+ card,
365
+ defaultPaymentMethod: "card",
366
+ pendingCardSetupToken: null,
367
+ });
368
+ } else {
369
+ saveConfig({
370
+ card,
371
+ pendingCardSetupToken: null,
372
+ defaultPaymentMethod: current.defaultPaymentMethod === "card"
373
+ ? undefined
374
+ : current.defaultPaymentMethod,
375
+ });
376
+ }
377
+ }
378
+
379
+ export function getPendingCardSetupToken(): string | null {
380
+ return getConfig().pendingCardSetupToken ?? null;
381
+ }
382
+
383
+ export function setPendingCardSetupToken(token: string | null): void {
384
+ saveConfig({ pendingCardSetupToken: token });
354
385
  }
355
386
 
356
387
  /**
@@ -118,6 +118,8 @@ interface RunResultLike {
118
118
  estimated_cost?: number;
119
119
  input_tokens?: number;
120
120
  tags?: string[];
121
+ consumption_mode?: string;
122
+ credit_pack_id?: string;
121
123
  }
122
124
 
123
125
  export function formatRunResult(result: RunResultLike, opts?: { paymentMethod?: string }): string {
@@ -147,6 +149,7 @@ export function formatRunResult(result: RunResultLike, opts?: { paymentMethod?:
147
149
 
148
150
  // Summary line
149
151
  const cost = result.cost ?? result.estimated_cost;
152
+ const usedCreditPack = result.consumption_mode === "credit_pack";
150
153
  const status = result.status === "success" || result.status === "completed" ? "✓" : "✗";
151
154
  const agent = result.agent_name ?? result.agent_id ?? "";
152
155
  const costStr = cost != null ? `$${cost.toFixed(cost < 0.01 ? 6 : 2)}` : "";
@@ -157,7 +160,10 @@ export function formatRunResult(result: RunResultLike, opts?: { paymentMethod?:
157
160
  if (result.error_code) {
158
161
  lines.push(`Error: ${result.error_code}`);
159
162
  }
160
- if (costStr) {
163
+ if (usedCreditPack) {
164
+ lines.push(`Covered by credit pack${result.credit_pack_id ? ` (${result.credit_pack_id})` : ""}`);
165
+ }
166
+ if (costStr && !usedCreditPack) {
161
167
  lines.push(`Paid: ${costStr}${method ? ` via ${method}` : ""}`);
162
168
  }
163
169
  if (result.job_id) {
package/src/core/index.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  export * from "./api-client.js";
2
2
  export * from "./config.js";
3
3
  export * from "./payments.js";
4
+ export * from "./principal.js";
5
+ export * from "./passes.js";
4
6
  export * from "./ows-adapter.js";
5
7
  export * from "./formatters.js";
6
8
  export * from "./types.js";
@@ -10,6 +10,7 @@
10
10
 
11
11
  import type { LocalAccount } from "viem/accounts";
12
12
  import type { Hex } from "viem";
13
+ import type { Keypair } from "@solana/web3.js";
13
14
 
14
15
  // Note: OWS provides encrypted key storage at rest (~/.ows/, AES-256-GCM).
15
16
  // For EVM signing, we export the secp256k1 key and use viem's native
@@ -127,6 +128,33 @@ function findEvmAccount(
127
128
  return evm;
128
129
  }
129
130
 
131
+ function findSolanaAccount(
132
+ wallet: OwsWalletInfo,
133
+ ): OwsAccountInfo {
134
+ const solana = wallet.accounts.find((a) => a.chainId.startsWith("solana"));
135
+ if (!solana) {
136
+ throw new Error(
137
+ `Wallet "${wallet.name}" (${wallet.id}) has no Solana account.`,
138
+ );
139
+ }
140
+ return solana;
141
+ }
142
+
143
+ function ed25519HexToBytes(privateKeyHex: string): Uint8Array {
144
+ return Uint8Array.from(Buffer.from(privateKeyHex, "hex"));
145
+ }
146
+
147
+ function keypairFromEd25519Hex(privateKeyHex: string, KeypairCtor: typeof import("@solana/web3.js").Keypair): Keypair {
148
+ const bytes = ed25519HexToBytes(privateKeyHex);
149
+ if (bytes.length === 32) {
150
+ return KeypairCtor.fromSeed(bytes);
151
+ }
152
+ if (bytes.length === 64) {
153
+ return KeypairCtor.fromSecretKey(bytes);
154
+ }
155
+ throw new Error(`Unsupported ed25519 key length: ${bytes.length} bytes.`);
156
+ }
157
+
130
158
  // ── Public API ───────────────────────────────────────────────────
131
159
 
132
160
  /**
@@ -169,16 +197,45 @@ export async function owsAccountFromWalletId(
169
197
  return privateKeyToAccount(`0x${keys.secp256k1}` as Hex);
170
198
  }
171
199
 
200
+ export async function owsSolanaKeypairFromWalletId(
201
+ walletId: string,
202
+ ): Promise<Keypair> {
203
+ const ows = await loadOws();
204
+ const wallet = ows.getWallet(walletId);
205
+ findSolanaAccount(wallet);
206
+
207
+ const exported = ows.exportWallet(walletId);
208
+ const keys = JSON.parse(exported) as { secp256k1?: string; ed25519?: string };
209
+ if (!keys.ed25519) {
210
+ throw new Error(`Wallet "${wallet.name}" has no ed25519 key for Solana signing.`);
211
+ }
212
+
213
+ const { Keypair } = await import("@solana/web3.js");
214
+ return keypairFromEd25519Hex(keys.ed25519, Keypair);
215
+ }
216
+
217
+ export async function getOwsWalletAddress(
218
+ walletId: string,
219
+ chain: "evm" | "solana" = "evm",
220
+ ): Promise<string> {
221
+ const ows = await loadOws();
222
+ const wallet = ows.getWallet(walletId);
223
+ return chain === "solana"
224
+ ? findSolanaAccount(wallet).address
225
+ : findEvmAccount(wallet).address;
226
+ }
227
+
172
228
  /**
173
229
  * Create a new OWS wallet and return its ID + EVM address.
174
230
  */
175
231
  export async function createOwsWallet(
176
232
  name: string,
233
+ chain: "evm" | "solana" = "evm",
177
234
  ): Promise<{ walletId: string; address: string }> {
178
235
  const ows = await loadOws();
179
236
  const wallet = ows.createWallet(name);
180
- const evmAccount = findEvmAccount(wallet);
181
- return { walletId: wallet.id, address: evmAccount.address };
237
+ const account = chain === "solana" ? findSolanaAccount(wallet) : findEvmAccount(wallet);
238
+ return { walletId: wallet.id, address: account.address };
182
239
  }
183
240
 
184
241
  /**
@@ -187,6 +244,7 @@ export async function createOwsWallet(
187
244
  export async function importKeyToOws(
188
245
  privateKey: string,
189
246
  name: string,
247
+ chain: "evm" | "solana" = "evm",
190
248
  ): Promise<{ walletId: string; address: string }> {
191
249
  const ows = await loadOws();
192
250
  const normalizedKey = privateKey.startsWith("0x")
@@ -197,10 +255,10 @@ export async function importKeyToOws(
197
255
  normalizedKey,
198
256
  null,
199
257
  null,
200
- "evm",
258
+ chain,
201
259
  );
202
- const evmAccount = findEvmAccount(wallet);
203
- return { walletId: wallet.id, address: evmAccount.address };
260
+ const account = chain === "solana" ? findSolanaAccount(wallet) : findEvmAccount(wallet);
261
+ return { walletId: wallet.id, address: account.address };
204
262
  }
205
263
 
206
264
  /**
@@ -209,14 +267,22 @@ export async function importKeyToOws(
209
267
  export async function listOwsWallets(): Promise<
210
268
  Array<{ id: string; name: string; address: string }>
211
269
  > {
270
+ return listOwsWalletsByChain("evm");
271
+ }
272
+
273
+ export async function listOwsWalletsByChain(
274
+ chain: "evm" | "solana" = "evm",
275
+ ): Promise<Array<{ id: string; name: string; address: string }>> {
212
276
  const ows = await loadOws();
213
277
  const wallets = ows.listWallets();
214
278
  const result: Array<{ id: string; name: string; address: string }> = [];
215
279
 
216
280
  for (const w of wallets) {
217
- const evm = w.accounts.find((a) => a.chainId.startsWith("eip155") || a.chainId.startsWith("evm"));
218
- if (evm) {
219
- result.push({ id: w.id, name: w.name, address: evm.address });
281
+ const account = chain === "solana"
282
+ ? w.accounts.find((a) => a.chainId.startsWith("solana"))
283
+ : w.accounts.find((a) => a.chainId.startsWith("eip155") || a.chainId.startsWith("evm"));
284
+ if (account) {
285
+ result.push({ id: w.id, name: w.name, address: account.address });
220
286
  }
221
287
  }
222
288
 
@@ -0,0 +1,74 @@
1
+ import { apiGet } from "./api-client.js";
2
+ import type { AgentRecord } from "./types.js";
3
+
4
+ export interface CreditPackOffer {
5
+ pack_id: string;
6
+ label: string;
7
+ included_units: number;
8
+ price_usd: string;
9
+ effective_price_per_unit_usd?: string;
10
+ }
11
+
12
+ export interface CreditPackRecord {
13
+ id: string;
14
+ status: string;
15
+ unit_type: string;
16
+ included_units: number;
17
+ remaining_units: number;
18
+ price_usd: string;
19
+ purchased_at: string;
20
+ pack?: {
21
+ name?: string | null;
22
+ key?: string | null;
23
+ };
24
+ }
25
+
26
+ export interface CreditPackProgramInfo {
27
+ unit_type?: string;
28
+ packs?: Array<{
29
+ key?: string;
30
+ name?: string;
31
+ included_units?: number;
32
+ price_usd?: string;
33
+ }>;
34
+ }
35
+
36
+ export interface CreditPackInventory {
37
+ consumer_principal: string | null;
38
+ offers: CreditPackOffer[];
39
+ balances: CreditPackRecord[];
40
+ }
41
+
42
+ export function getCreditPackProgram(agent: AgentRecord): CreditPackProgramInfo | null {
43
+ const payment = (agent.payment ?? {}) as Record<string, unknown>;
44
+ const creditPacks = payment.credit_packs as CreditPackProgramInfo | null | undefined;
45
+ return creditPacks ?? null;
46
+ }
47
+
48
+ export async function getCreditPackInventory(agentId: string): Promise<CreditPackInventory | null> {
49
+ try {
50
+ return await apiGet<CreditPackInventory>(`/agents/${agentId}/credit-packs`, { ensureConsumerPrincipal: true });
51
+ } catch {
52
+ return null;
53
+ }
54
+ }
55
+
56
+ export function getActiveCreditPack(inventory: CreditPackInventory | null): CreditPackRecord | null {
57
+ if (!inventory) return null;
58
+ return inventory.balances
59
+ .filter((pack) => pack.status === "active" && pack.remaining_units > 0)
60
+ .sort((a, b) => new Date(a.purchased_at).getTime() - new Date(b.purchased_at).getTime())[0] ?? null;
61
+ }
62
+
63
+ export function formatCreditPackOffer(offer: CreditPackOffer): string {
64
+ const unitPrice = offer.effective_price_per_unit_usd
65
+ ? ` ($${Number(offer.effective_price_per_unit_usd).toFixed(2)}/unit)`
66
+ : "";
67
+ return `${offer.label} — ${offer.included_units} units for $${offer.price_usd}${unitPrice}`;
68
+ }
69
+
70
+ export function formatCreditPack(pack: CreditPackRecord): string {
71
+ const label = pack.pack?.name ?? pack.pack?.key ?? "Credit Pack";
72
+ const statusPrefix = pack.status === "active" ? "" : `${pack.status} • `;
73
+ return `${label}: ${statusPrefix}${pack.remaining_units}/${pack.included_units} ${pack.unit_type}s remaining`;
74
+ }