@asgcard/cli 0.1.1 → 0.2.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/dist/index.js CHANGED
@@ -5,8 +5,16 @@
5
5
  * Manage virtual cards for AI agents from your terminal.
6
6
  * Authenticates via Stellar wallet signature (no API keys needed).
7
7
  *
8
- * Usage:
9
- * asgcard login Set your Stellar private key
8
+ * Onboarding commands:
9
+ * asgcard install --client codex|claude|cursor Configure MCP for your AI client
10
+ * asgcard onboard [-y] — Full onboarding: wallet + MCP + skill + next step
11
+ * asgcard wallet create — Generate a new Stellar keypair
12
+ * asgcard wallet import — Import an existing Stellar secret key
13
+ * asgcard wallet info — Show wallet address, USDC balance, deposit info
14
+ * asgcard doctor — Diagnose your setup
15
+ *
16
+ * Card commands:
17
+ * asgcard login — Set your Stellar private key (legacy, use wallet import)
10
18
  * asgcard cards — List your cards
11
19
  * asgcard card <id> — Get card details
12
20
  * asgcard card:details <id> — Get sensitive card info (PAN, CVV)
@@ -22,12 +30,20 @@ import chalk from "chalk";
22
30
  import ora from "ora";
23
31
  import { ASGCardClient } from "@asgcard/sdk";
24
32
  import { WalletClient } from "./wallet-client.js";
25
- import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
26
- import { join } from "node:path";
33
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, cpSync } from "node:fs";
34
+ import { join, dirname } from "node:path";
27
35
  import { homedir } from "node:os";
28
- // ── Config persistence ──────────────────────────────────────
36
+ import { execSync } from "node:child_process";
37
+ import { fileURLToPath } from "node:url";
38
+ // ── Constants ───────────────────────────────────────────────
29
39
  const CONFIG_DIR = join(homedir(), ".asgcard");
30
40
  const CONFIG_FILE = join(CONFIG_DIR, "config.json");
41
+ const WALLET_FILE = join(CONFIG_DIR, "wallet.json");
42
+ const SKILL_DIR = join(homedir(), ".agents", "skills", "asgcard");
43
+ const VERSION = "0.2.0";
44
+ const USDC_ISSUER = "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN";
45
+ const HORIZON_URL = "https://horizon.stellar.org";
46
+ const MIN_CARD_COST_USDC = 17.20; // $10 tier total cost
31
47
  function loadConfig() {
32
48
  try {
33
49
  if (existsSync(CONFIG_FILE)) {
@@ -43,16 +59,48 @@ function saveConfig(config) {
43
59
  mkdirSync(CONFIG_DIR, { recursive: true });
44
60
  writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), { mode: 0o600 });
45
61
  }
46
- function requireKey() {
62
+ function loadWallet() {
63
+ try {
64
+ if (existsSync(WALLET_FILE)) {
65
+ return JSON.parse(readFileSync(WALLET_FILE, "utf-8"));
66
+ }
67
+ }
68
+ catch {
69
+ // ignore
70
+ }
71
+ return null;
72
+ }
73
+ function saveWallet(wallet) {
74
+ mkdirSync(CONFIG_DIR, { recursive: true });
75
+ writeFileSync(WALLET_FILE, JSON.stringify(wallet, null, 2), { mode: 0o600 });
76
+ }
77
+ function resolveKey() {
78
+ // Priority: env var > wallet.json > config.json (legacy)
79
+ // Must match mcp-server/src/index.ts resolvePrivateKey() order
80
+ if (process.env.STELLAR_PRIVATE_KEY)
81
+ return process.env.STELLAR_PRIVATE_KEY;
82
+ const wallet = loadWallet();
83
+ if (wallet?.secretKey)
84
+ return wallet.secretKey;
47
85
  const config = loadConfig();
48
- const key = process.env.STELLAR_PRIVATE_KEY || config.privateKey;
86
+ if (config.privateKey)
87
+ return config.privateKey;
88
+ return null;
89
+ }
90
+ function requireKey() {
91
+ const key = resolveKey();
49
92
  if (!key) {
50
- console.error(chalk.red("❌ No Stellar private key configured.\n") +
51
- chalk.dim("Run: ") +
52
- chalk.cyan("asgcard login") +
53
- chalk.dim(" or set ") +
93
+ console.error(chalk.red("❌ No Stellar private key configured.\n\n") +
94
+ chalk.bold("To fix this, do one of:\n\n") +
95
+ chalk.cyan(" asgcard wallet create") +
96
+ chalk.dim(" generate a new Stellar keypair\n") +
97
+ chalk.cyan(" asgcard wallet import") +
98
+ chalk.dim(" — import an existing key\n") +
99
+ chalk.cyan(" asgcard login <key>") +
100
+ chalk.dim(" — save a key directly\n") +
101
+ chalk.dim("\n Or set ") +
54
102
  chalk.cyan("STELLAR_PRIVATE_KEY") +
55
- chalk.dim(" env var."));
103
+ chalk.dim(" environment variable.\n"));
56
104
  process.exit(1);
57
105
  }
58
106
  return key;
@@ -63,6 +111,31 @@ function getApiUrl() {
63
111
  function getRpcUrl() {
64
112
  return process.env.STELLAR_RPC_URL || loadConfig().rpcUrl;
65
113
  }
114
+ // ── Stellar Horizon helpers ─────────────────────────────────
115
+ async function getUsdcBalance(publicKey) {
116
+ try {
117
+ const res = await fetch(`${HORIZON_URL}/accounts/${publicKey}`);
118
+ if (res.status === 404)
119
+ return 0; // Account not funded
120
+ if (!res.ok)
121
+ throw new Error(`Horizon error: ${res.status}`);
122
+ const data = await res.json();
123
+ const usdcBalance = data.balances.find((b) => b.asset_code === "USDC" && b.asset_issuer === USDC_ISSUER);
124
+ return usdcBalance ? parseFloat(usdcBalance.balance) : 0;
125
+ }
126
+ catch {
127
+ return -1; // -1 signals error
128
+ }
129
+ }
130
+ async function isAccountFunded(publicKey) {
131
+ try {
132
+ const res = await fetch(`${HORIZON_URL}/accounts/${publicKey}`);
133
+ return res.ok;
134
+ }
135
+ catch {
136
+ return false;
137
+ }
138
+ }
66
139
  // ── Formatters ──────────────────────────────────────────────
67
140
  function formatCard(card) {
68
141
  const status = card.status === "active"
@@ -78,13 +151,704 @@ function formatCard(card) {
78
151
  ` Created: ${card.createdAt || "—"}`,
79
152
  ].join("\n");
80
153
  }
154
+ function remediate(what, why, fix) {
155
+ console.error(chalk.red(`❌ ${what}\n`) +
156
+ chalk.dim(` Why: ${why}\n`) +
157
+ chalk.bold(` Fix: `) + chalk.cyan(fix) + "\n");
158
+ }
81
159
  // ── CLI ─────────────────────────────────────────────────────
82
160
  const program = new Command();
83
161
  program
84
162
  .name("asgcard")
85
- .description("ASG Card CLI — virtual cards for AI agents, powered by x402")
86
- .version("0.1.0");
87
- // ── login ───────────────────────────────────────────────────
163
+ .description("ASG Card CLI — virtual cards for AI agents, powered by x402 on Stellar")
164
+ .version(VERSION);
165
+ // ═══════════════════════════════════════════════════════════
166
+ // ONBOARDING COMMANDS
167
+ // ═══════════════════════════════════════════════════════════
168
+ // ── wallet ──────────────────────────────────────────────────
169
+ const walletCmd = program
170
+ .command("wallet")
171
+ .description("Manage your Stellar wallet (create, import, info)");
172
+ walletCmd
173
+ .command("create")
174
+ .description("Generate a new Stellar keypair and save locally")
175
+ .action(async () => {
176
+ const existing = loadWallet();
177
+ if (existing) {
178
+ console.log(chalk.yellow("⚠ A wallet already exists:\n") +
179
+ chalk.dim(" Address: ") + chalk.cyan(existing.publicKey) + "\n" +
180
+ chalk.dim(" File: ") + chalk.dim(WALLET_FILE) + "\n\n" +
181
+ chalk.dim(" To replace it, delete ") + chalk.cyan(WALLET_FILE) + chalk.dim(" first."));
182
+ return;
183
+ }
184
+ const { Keypair } = await import("@stellar/stellar-sdk");
185
+ const kp = Keypair.random();
186
+ const wallet = {
187
+ publicKey: kp.publicKey(),
188
+ secretKey: kp.secret(),
189
+ createdAt: new Date().toISOString(),
190
+ };
191
+ saveWallet(wallet);
192
+ // Also save to config for backward compatibility
193
+ const config = loadConfig();
194
+ config.privateKey = kp.secret();
195
+ saveConfig(config);
196
+ console.log(chalk.green("✅ Wallet created!\n"));
197
+ console.log(chalk.dim(" Address: ") + chalk.cyan(kp.publicKey()));
198
+ console.log(chalk.dim(" Secret: ") + chalk.yellow(kp.secret()));
199
+ console.log(chalk.dim(" Saved to: ") + chalk.dim(WALLET_FILE));
200
+ console.log();
201
+ console.log(chalk.bold("⚡ Next steps:\n"));
202
+ console.log(chalk.dim(" 1. Fund your wallet with at least ") + chalk.green(`$${MIN_CARD_COST_USDC} USDC`) + chalk.dim(" on Stellar"));
203
+ console.log(chalk.dim(" Send USDC to: ") + chalk.cyan(kp.publicKey()));
204
+ console.log(chalk.dim(" 2. Check your balance: ") + chalk.cyan("asgcard wallet info"));
205
+ console.log(chalk.dim(" 3. Create your first card: ") + chalk.cyan("asgcard card:create -a 10 -n \"AI Agent\" -e you@email.com"));
206
+ console.log();
207
+ console.log(chalk.yellow("⚠ Back up your secret key! It cannot be recovered if lost."));
208
+ });
209
+ walletCmd
210
+ .command("import")
211
+ .description("Import an existing Stellar secret key")
212
+ .argument("[key]", "Stellar secret key (S...). Omit to enter interactively")
213
+ .action(async (key) => {
214
+ let privateKey = key;
215
+ if (!privateKey) {
216
+ const readline = await import("node:readline");
217
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
218
+ privateKey = await new Promise((resolve) => {
219
+ rl.question(chalk.cyan("Enter Stellar secret key (S...): "), (answer) => {
220
+ rl.close();
221
+ resolve(answer.trim());
222
+ });
223
+ });
224
+ }
225
+ if (!privateKey?.startsWith("S") || privateKey.length !== 56) {
226
+ remediate("Invalid Stellar secret key", "Key must start with 'S' and be 56 characters (Stellar Ed25519 format)", "Get your key from your Stellar wallet or run: asgcard wallet create");
227
+ process.exit(1);
228
+ }
229
+ try {
230
+ const { Keypair } = await import("@stellar/stellar-sdk");
231
+ const kp = Keypair.fromSecret(privateKey);
232
+ const wallet = {
233
+ publicKey: kp.publicKey(),
234
+ secretKey: privateKey,
235
+ createdAt: new Date().toISOString(),
236
+ };
237
+ saveWallet(wallet);
238
+ const config = loadConfig();
239
+ config.privateKey = privateKey;
240
+ saveConfig(config);
241
+ console.log(chalk.green("✅ Wallet imported!\n"));
242
+ console.log(chalk.dim(" Address: ") + chalk.cyan(kp.publicKey()));
243
+ console.log(chalk.dim(" Saved to: ") + chalk.dim(WALLET_FILE));
244
+ console.log();
245
+ console.log(chalk.dim(" Check your balance: ") + chalk.cyan("asgcard wallet info"));
246
+ }
247
+ catch {
248
+ remediate("Invalid Stellar secret key", "Could not decode the provided key", "Make sure it's a valid Stellar secret key starting with 'S'");
249
+ process.exit(1);
250
+ }
251
+ });
252
+ walletCmd
253
+ .command("info")
254
+ .description("Show wallet address, USDC balance, and deposit instructions")
255
+ .action(async () => {
256
+ const key = resolveKey();
257
+ if (!key) {
258
+ remediate("No wallet configured", "No Stellar key found in config, wallet file, or environment", "asgcard wallet create or asgcard wallet import");
259
+ process.exit(1);
260
+ }
261
+ const spinner = ora("Checking wallet...").start();
262
+ try {
263
+ const { Keypair } = await import("@stellar/stellar-sdk");
264
+ const kp = Keypair.fromSecret(key);
265
+ const pubKey = kp.publicKey();
266
+ const funded = await isAccountFunded(pubKey);
267
+ const balance = funded ? await getUsdcBalance(pubKey) : 0;
268
+ spinner.stop();
269
+ console.log(chalk.bold("\n🔑 Wallet Status\n"));
270
+ console.log(chalk.dim(" Public Key: ") + chalk.cyan(pubKey));
271
+ console.log(chalk.dim(" Account Funded: ") + (funded ? chalk.green("Yes") : chalk.red("No")));
272
+ if (balance === -1) {
273
+ console.log(chalk.dim(" USDC Balance: ") + chalk.yellow("Could not fetch (Horizon API error)"));
274
+ }
275
+ else {
276
+ const balanceColor = balance >= MIN_CARD_COST_USDC ? chalk.green : chalk.red;
277
+ console.log(chalk.dim(" USDC Balance: ") + balanceColor(`$${balance.toFixed(2)}`));
278
+ }
279
+ console.log(chalk.dim(" Min Required: ") + chalk.dim(`$${MIN_CARD_COST_USDC} USDC (for $10 card tier)`));
280
+ console.log();
281
+ if (!funded) {
282
+ console.log(chalk.yellow("⚠ Your Stellar account is not funded yet.\n"));
283
+ console.log(chalk.dim(" To activate your account, send at least 1 XLM + USDC to:"));
284
+ console.log(chalk.cyan(` ${pubKey}`));
285
+ console.log(chalk.dim("\n Then add a USDC trustline and deposit USDC."));
286
+ }
287
+ else if (balance < MIN_CARD_COST_USDC) {
288
+ console.log(chalk.yellow("⚠ Insufficient USDC for card creation.\n"));
289
+ console.log(chalk.dim(" Deposit at least ") + chalk.green(`$${MIN_CARD_COST_USDC} USDC`) + chalk.dim(" to your wallet:"));
290
+ console.log(chalk.cyan(` ${pubKey}`));
291
+ console.log(chalk.dim("\n USDC on Stellar: ") + chalk.dim("asset code USDC, issuer " + USDC_ISSUER.slice(0, 8) + "..."));
292
+ }
293
+ else {
294
+ console.log(chalk.green("✅ Wallet is ready for card creation!"));
295
+ console.log(chalk.dim(" Create a card: ") + chalk.cyan("asgcard card:create -a 10 -n \"AI Agent\" -e you@email.com"));
296
+ }
297
+ console.log();
298
+ }
299
+ catch (error) {
300
+ spinner.fail(chalk.red("Failed to check wallet"));
301
+ remediate("Invalid private key", error instanceof Error ? error.message : "Could not decode key", "asgcard wallet create or asgcard wallet import");
302
+ process.exit(1);
303
+ }
304
+ });
305
+ // ── install ─────────────────────────────────────────────────
306
+ program
307
+ .command("install")
308
+ .description("Configure ASG Card MCP server for your AI client")
309
+ .requiredOption("-c, --client <client>", "AI client to configure (codex, claude, cursor)")
310
+ .action(async (options) => {
311
+ const client = options.client.toLowerCase();
312
+ const validClients = ["codex", "claude", "cursor"];
313
+ if (!validClients.includes(client)) {
314
+ remediate(`Unknown client: ${client}`, `Supported clients: ${validClients.join(", ")}`, `asgcard install --client codex`);
315
+ process.exit(1);
316
+ }
317
+ // NOTE: We do NOT embed STELLAR_PRIVATE_KEY in client configs.
318
+ // The MCP server reads the key from ~/.asgcard/wallet.json (or config.json)
319
+ // at startup. This keeps wallet lifecycle in the CLI/state layer.
320
+ const key = resolveKey();
321
+ switch (client) {
322
+ case "codex": {
323
+ const configPath = join(homedir(), ".codex", "config.toml");
324
+ const configDir = dirname(configPath);
325
+ mkdirSync(configDir, { recursive: true });
326
+ let existing = "";
327
+ try {
328
+ existing = readFileSync(configPath, "utf-8");
329
+ }
330
+ catch {
331
+ // file doesn't exist yet
332
+ }
333
+ if (existing.includes("[mcp_servers.asgcard]")) {
334
+ console.log(chalk.yellow("⚠ ASG Card MCP server is already configured in Codex."));
335
+ console.log(chalk.dim(" Config: ") + chalk.dim(configPath));
336
+ return;
337
+ }
338
+ const tomlBlock = `\n[mcp_servers.asgcard]\ncommand = "npx"\nargs = ["-y", "@asgcard/mcp-server"]\n`;
339
+ writeFileSync(configPath, existing + tomlBlock);
340
+ console.log(chalk.green("✅ ASG Card MCP server added to Codex!\n"));
341
+ console.log(chalk.dim(" Config: ") + chalk.dim(configPath));
342
+ console.log(chalk.dim(" Key source: ~/.asgcard/wallet.json (auto-resolved by MCP server)"));
343
+ if (!key) {
344
+ console.log(chalk.yellow("\n⚠ No wallet found yet. Run: ") + chalk.cyan("asgcard wallet create"));
345
+ }
346
+ break;
347
+ }
348
+ case "claude": {
349
+ // Use claude CLI to add MCP server (no env vars — key is read from wallet.json)
350
+ console.log(chalk.bold("Adding ASG Card MCP server to Claude Code...\n"));
351
+ const cmd = `claude mcp add asgcard -- npx -y @asgcard/mcp-server`;
352
+ try {
353
+ execSync(cmd, { stdio: "inherit" });
354
+ console.log(chalk.green("\n✅ ASG Card MCP server added to Claude Code!"));
355
+ }
356
+ catch {
357
+ // Fallback: write JSON config
358
+ const claudeConfigPath = join(homedir(), ".claude", "mcp.json");
359
+ const claudeConfigDir = dirname(claudeConfigPath);
360
+ mkdirSync(claudeConfigDir, { recursive: true });
361
+ let claudeConfig = {};
362
+ try {
363
+ claudeConfig = JSON.parse(readFileSync(claudeConfigPath, "utf-8"));
364
+ }
365
+ catch {
366
+ // file doesn't exist
367
+ }
368
+ const servers = (claudeConfig.mcpServers || {});
369
+ servers.asgcard = {
370
+ command: "npx",
371
+ args: ["-y", "@asgcard/mcp-server"],
372
+ };
373
+ claudeConfig.mcpServers = servers;
374
+ writeFileSync(claudeConfigPath, JSON.stringify(claudeConfig, null, 2));
375
+ console.log(chalk.green("✅ ASG Card MCP server added to Claude config!\n"));
376
+ console.log(chalk.dim(" Config: ") + chalk.dim(claudeConfigPath));
377
+ }
378
+ console.log(chalk.dim(" Key source: ~/.asgcard/wallet.json (auto-resolved by MCP server)"));
379
+ if (!key) {
380
+ console.log(chalk.yellow("\n⚠ No wallet found yet. Run: ") + chalk.cyan("asgcard wallet create"));
381
+ }
382
+ break;
383
+ }
384
+ case "cursor": {
385
+ const cursorConfigPath = join(homedir(), ".cursor", "mcp.json");
386
+ const cursorConfigDir = dirname(cursorConfigPath);
387
+ mkdirSync(cursorConfigDir, { recursive: true });
388
+ let cursorConfig = {};
389
+ try {
390
+ cursorConfig = JSON.parse(readFileSync(cursorConfigPath, "utf-8"));
391
+ }
392
+ catch {
393
+ // file doesn't exist
394
+ }
395
+ const servers = (cursorConfig.mcpServers || {});
396
+ if (servers.asgcard) {
397
+ console.log(chalk.yellow("⚠ ASG Card MCP server is already configured in Cursor."));
398
+ console.log(chalk.dim(" Config: ") + chalk.dim(cursorConfigPath));
399
+ return;
400
+ }
401
+ servers.asgcard = {
402
+ command: "npx",
403
+ args: ["-y", "@asgcard/mcp-server"],
404
+ };
405
+ cursorConfig.mcpServers = servers;
406
+ writeFileSync(cursorConfigPath, JSON.stringify(cursorConfig, null, 2));
407
+ console.log(chalk.green("✅ ASG Card MCP server added to Cursor!\n"));
408
+ console.log(chalk.dim(" Config: ") + chalk.dim(cursorConfigPath));
409
+ console.log(chalk.dim(" Key source: ~/.asgcard/wallet.json (auto-resolved by MCP server)"));
410
+ if (!key) {
411
+ console.log(chalk.yellow("\n⚠ No wallet found yet. Run: ") + chalk.cyan("asgcard wallet create"));
412
+ }
413
+ break;
414
+ }
415
+ }
416
+ console.log(chalk.dim("\n Verify setup: ") + chalk.cyan("asgcard doctor"));
417
+ });
418
+ // ── onboard ─────────────────────────────────────────────────
419
+ program
420
+ .command("onboard")
421
+ .description("Full onboarding: create/import wallet, install MCP, install skill, print next step")
422
+ .option("-y, --yes", "Non-interactive mode (auto-create wallet, skip prompts)")
423
+ .option("-c, --client <client>", "AI client to configure (codex, claude, cursor)")
424
+ .action(async (options) => {
425
+ console.log(chalk.bold("\n🚀 ASG Card Onboarding\n"));
426
+ // Step 1: Wallet
427
+ console.log(chalk.bold("Step 1/4: Wallet"));
428
+ let key = resolveKey();
429
+ if (key) {
430
+ const { Keypair } = await import("@stellar/stellar-sdk");
431
+ const kp = Keypair.fromSecret(key);
432
+ console.log(chalk.green(" ✅ Wallet found: ") + chalk.cyan(kp.publicKey()));
433
+ }
434
+ else if (options.yes) {
435
+ // Auto-create wallet
436
+ const { Keypair } = await import("@stellar/stellar-sdk");
437
+ const kp = Keypair.random();
438
+ const wallet = {
439
+ publicKey: kp.publicKey(),
440
+ secretKey: kp.secret(),
441
+ createdAt: new Date().toISOString(),
442
+ };
443
+ saveWallet(wallet);
444
+ const config = loadConfig();
445
+ config.privateKey = kp.secret();
446
+ saveConfig(config);
447
+ key = kp.secret();
448
+ console.log(chalk.green(" ✅ New wallet created: ") + chalk.cyan(kp.publicKey()));
449
+ console.log(chalk.dim(" Secret saved to: ") + chalk.dim(WALLET_FILE));
450
+ }
451
+ else {
452
+ // Interactive: ask to create or import
453
+ const readline = await import("node:readline");
454
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
455
+ const answer = await new Promise((resolve) => {
456
+ rl.question(chalk.cyan(" No wallet found. Create a new one? (Y/n): "), (a) => {
457
+ rl.close();
458
+ resolve(a.trim().toLowerCase());
459
+ });
460
+ });
461
+ if (answer === "" || answer === "y" || answer === "yes") {
462
+ const { Keypair } = await import("@stellar/stellar-sdk");
463
+ const kp = Keypair.random();
464
+ const wallet = {
465
+ publicKey: kp.publicKey(),
466
+ secretKey: kp.secret(),
467
+ createdAt: new Date().toISOString(),
468
+ };
469
+ saveWallet(wallet);
470
+ const config = loadConfig();
471
+ config.privateKey = kp.secret();
472
+ saveConfig(config);
473
+ key = kp.secret();
474
+ console.log(chalk.green(" ✅ Wallet created: ") + chalk.cyan(kp.publicKey()));
475
+ }
476
+ else {
477
+ console.log(chalk.dim(" Skipped. Run ") + chalk.cyan("asgcard wallet import") + chalk.dim(" later."));
478
+ }
479
+ }
480
+ console.log();
481
+ // Step 2: Install MCP for detected/specified clients
482
+ console.log(chalk.bold("Step 2/4: MCP Configuration"));
483
+ const clients = [];
484
+ if (options.client) {
485
+ clients.push(options.client.toLowerCase());
486
+ }
487
+ else {
488
+ // Auto-detect installed clients
489
+ if (existsSync(join(homedir(), ".codex")))
490
+ clients.push("codex");
491
+ if (existsSync(join(homedir(), ".claude")))
492
+ clients.push("claude");
493
+ if (existsSync(join(homedir(), ".cursor")))
494
+ clients.push("cursor");
495
+ }
496
+ if (clients.length === 0) {
497
+ console.log(chalk.dim(" No AI clients detected. Install manually: ") + chalk.cyan("asgcard install --client <client>"));
498
+ }
499
+ else {
500
+ // NOTE: No STELLAR_PRIVATE_KEY in configs — MCP server reads from ~/.asgcard/wallet.json
501
+ for (const client of clients) {
502
+ switch (client) {
503
+ case "codex": {
504
+ const configPath = join(homedir(), ".codex", "config.toml");
505
+ mkdirSync(dirname(configPath), { recursive: true });
506
+ let existing = "";
507
+ try {
508
+ existing = readFileSync(configPath, "utf-8");
509
+ }
510
+ catch { /* */ }
511
+ if (existing.includes("[mcp_servers.asgcard]")) {
512
+ console.log(chalk.green(" ✅ Codex: already configured"));
513
+ }
514
+ else {
515
+ const tomlBlock = `\n[mcp_servers.asgcard]\ncommand = "npx"\nargs = ["-y", "@asgcard/mcp-server"]\n`;
516
+ writeFileSync(configPath, existing + tomlBlock);
517
+ console.log(chalk.green(" ✅ Codex: MCP configured"));
518
+ }
519
+ break;
520
+ }
521
+ case "claude": {
522
+ const claudeConfigPath = join(homedir(), ".claude", "mcp.json");
523
+ mkdirSync(dirname(claudeConfigPath), { recursive: true });
524
+ let claudeConfig = {};
525
+ try {
526
+ claudeConfig = JSON.parse(readFileSync(claudeConfigPath, "utf-8"));
527
+ }
528
+ catch { /* */ }
529
+ const servers = (claudeConfig.mcpServers || {});
530
+ if (servers.asgcard) {
531
+ console.log(chalk.green(" ✅ Claude: already configured"));
532
+ }
533
+ else {
534
+ servers.asgcard = {
535
+ command: "npx",
536
+ args: ["-y", "@asgcard/mcp-server"],
537
+ };
538
+ claudeConfig.mcpServers = servers;
539
+ writeFileSync(claudeConfigPath, JSON.stringify(claudeConfig, null, 2));
540
+ console.log(chalk.green(" ✅ Claude: MCP configured"));
541
+ }
542
+ break;
543
+ }
544
+ case "cursor": {
545
+ const cursorConfigPath = join(homedir(), ".cursor", "mcp.json");
546
+ mkdirSync(dirname(cursorConfigPath), { recursive: true });
547
+ let cursorConfig = {};
548
+ try {
549
+ cursorConfig = JSON.parse(readFileSync(cursorConfigPath, "utf-8"));
550
+ }
551
+ catch { /* */ }
552
+ const cServers = (cursorConfig.mcpServers || {});
553
+ if (cServers.asgcard) {
554
+ console.log(chalk.green(" ✅ Cursor: already configured"));
555
+ }
556
+ else {
557
+ cServers.asgcard = {
558
+ command: "npx",
559
+ args: ["-y", "@asgcard/mcp-server"],
560
+ };
561
+ cursorConfig.mcpServers = cServers;
562
+ writeFileSync(cursorConfigPath, JSON.stringify(cursorConfig, null, 2));
563
+ console.log(chalk.green(" ✅ Cursor: MCP configured"));
564
+ }
565
+ break;
566
+ }
567
+ }
568
+ }
569
+ }
570
+ console.log();
571
+ // Step 3: Install product-owned skill
572
+ console.log(chalk.bold("Step 3/4: Agent Skill"));
573
+ try {
574
+ const __filename = fileURLToPath(import.meta.url);
575
+ const __dirname = dirname(__filename);
576
+ const bundledSkillDir = join(__dirname, "..", "skill");
577
+ if (existsSync(bundledSkillDir)) {
578
+ mkdirSync(SKILL_DIR, { recursive: true });
579
+ cpSync(bundledSkillDir, SKILL_DIR, { recursive: true });
580
+ console.log(chalk.green(" ✅ ASG Card skill installed: ") + chalk.dim(SKILL_DIR));
581
+ }
582
+ else {
583
+ // Create a minimal skill file
584
+ mkdirSync(SKILL_DIR, { recursive: true });
585
+ const skillContent = `---
586
+ name: asgcard
587
+ description: ASG Card — virtual MasterCard cards for AI agents, powered by x402 on Stellar
588
+ ---
589
+
590
+ # ASG Card Agent Skill
591
+
592
+ ## Canonical Flow
593
+
594
+ 1. **Check wallet status**: Use \`get_wallet_status\` MCP tool to verify wallet address and USDC balance
595
+ 2. **Check pricing**: Use \`get_pricing\` to see available card tiers and costs
596
+ 3. **Create a card**: Use \`create_card\` with amount, name, and email
597
+ 4. **Manage cards**: Use \`list_cards\`, \`get_card\`, \`get_card_details\`, \`freeze_card\`, \`unfreeze_card\`
598
+
599
+ ## Zero Balance Handling
600
+
601
+ If wallet has insufficient USDC:
602
+ - Tell the user their current balance and the minimum required ($17.20 for $10 tier)
603
+ - Provide their Stellar public key for deposits
604
+ - Explain: "Send USDC on Stellar to your wallet address, then retry"
605
+
606
+ ## MCP Tools Available
607
+
608
+ | Tool | Description |
609
+ |------|-------------|
610
+ | \`get_wallet_status\` | Check wallet address, USDC balance, and readiness |
611
+ | \`get_pricing\` | View tier pricing for card creation and funding |
612
+ | \`create_card\` | Create virtual MasterCard (pays USDC on-chain via x402) |
613
+ | \`fund_card\` | Top up existing card |
614
+ | \`list_cards\` | List all wallet cards |
615
+ | \`get_card\` | Get card summary |
616
+ | \`get_card_details\` | Get PAN, CVV, expiry (sensitive) |
617
+ | \`freeze_card\` | Temporarily freeze card |
618
+ | \`unfreeze_card\` | Re-enable frozen card |
619
+
620
+ ## Important Notes
621
+
622
+ - All payments are in USDC on Stellar via x402 protocol
623
+ - Card details are returned immediately on creation (agent-first model)
624
+ - Wallet uses Stellar Ed25519 keypair — private key must stay local
625
+ - Minimum card tier is $10 (total cost $17.20 USDC including fees)
626
+ `;
627
+ writeFileSync(join(SKILL_DIR, "SKILL.md"), skillContent);
628
+ console.log(chalk.green(" ✅ ASG Card skill installed: ") + chalk.dim(SKILL_DIR));
629
+ }
630
+ // Also install for claude and kiro if dirs exist
631
+ for (const altDir of [
632
+ join(homedir(), ".claude", "skills", "asgcard"),
633
+ join(homedir(), ".kiro", "skills", "asgcard"),
634
+ ]) {
635
+ if (existsSync(dirname(dirname(altDir)))) {
636
+ mkdirSync(altDir, { recursive: true });
637
+ cpSync(SKILL_DIR, altDir, { recursive: true });
638
+ }
639
+ }
640
+ }
641
+ catch (error) {
642
+ console.log(chalk.yellow(" ⚠ Could not install skill: ") + chalk.dim(error instanceof Error ? error.message : String(error)));
643
+ }
644
+ console.log();
645
+ // Step 4: Wallet balance check and next step
646
+ console.log(chalk.bold("Step 4/4: Status & Next Steps"));
647
+ if (key) {
648
+ const { Keypair } = await import("@stellar/stellar-sdk");
649
+ const kp = Keypair.fromSecret(key);
650
+ const balance = await getUsdcBalance(kp.publicKey());
651
+ if (balance === -1) {
652
+ console.log(chalk.yellow(" ⚠ Could not check balance (Horizon API error)"));
653
+ console.log(chalk.dim(" Check manually: ") + chalk.cyan("asgcard wallet info"));
654
+ }
655
+ else if (balance >= MIN_CARD_COST_USDC) {
656
+ console.log(chalk.green(" ✅ Wallet funded!") + chalk.dim(` Balance: $${balance.toFixed(2)} USDC`));
657
+ console.log(chalk.bold("\n 🎉 Ready! Create your first card:\n"));
658
+ console.log(chalk.cyan(" asgcard card:create -a 10 -n \"AI Agent\" -e you@email.com\n"));
659
+ }
660
+ else {
661
+ console.log(chalk.yellow(` ⚠ Balance: $${balance.toFixed(2)} USDC`) + chalk.dim(` (need $${MIN_CARD_COST_USDC} for $10 tier)`));
662
+ console.log(chalk.bold("\n 📥 Next step: Fund your wallet\n"));
663
+ console.log(chalk.dim(" Send USDC on Stellar to:"));
664
+ console.log(chalk.cyan(` ${kp.publicKey()}\n`));
665
+ console.log(chalk.dim(" Then check: ") + chalk.cyan("asgcard wallet info"));
666
+ }
667
+ }
668
+ else {
669
+ console.log(chalk.yellow(" ⚠ No wallet configured."));
670
+ console.log(chalk.dim(" Run: ") + chalk.cyan("asgcard wallet create") + chalk.dim(" or ") + chalk.cyan("asgcard wallet import"));
671
+ }
672
+ console.log();
673
+ // ── Telemetry beacon (fire-and-forget) ──────────────
674
+ try {
675
+ const apiUrl = getApiUrl();
676
+ fetch(`${apiUrl}/telemetry/install`, {
677
+ method: "POST",
678
+ headers: { "Content-Type": "application/json" },
679
+ body: JSON.stringify({
680
+ client: clients[0] ?? "manual",
681
+ version: VERSION,
682
+ os: process.platform,
683
+ }),
684
+ signal: AbortSignal.timeout(3000),
685
+ }).catch(() => { }); // swallow — never block
686
+ }
687
+ catch {
688
+ // fail-open
689
+ }
690
+ });
691
+ // ── doctor ──────────────────────────────────────────────────
692
+ program
693
+ .command("doctor")
694
+ .description("Diagnose your ASG Card setup — checks CLI, wallet, API, RPC, and balance")
695
+ .action(async () => {
696
+ console.log(chalk.bold("\n🩺 ASG Card Doctor\n"));
697
+ let allGood = true;
698
+ // 1. CLI version
699
+ console.log(chalk.dim(" CLI Version: ") + chalk.cyan(VERSION));
700
+ // 2. Config directory
701
+ const configExists = existsSync(CONFIG_DIR);
702
+ console.log(chalk.dim(" Config Dir: ") +
703
+ (configExists ? chalk.green(`✅ ${CONFIG_DIR}`) : chalk.red(`❌ ${CONFIG_DIR} — run: asgcard wallet create`)));
704
+ if (!configExists)
705
+ allGood = false;
706
+ // 3. Private key
707
+ const key = resolveKey();
708
+ if (key) {
709
+ try {
710
+ const { Keypair } = await import("@stellar/stellar-sdk");
711
+ const kp = Keypair.fromSecret(key);
712
+ console.log(chalk.dim(" Wallet Key: ") + chalk.green(`✅ ${kp.publicKey().slice(0, 8)}...${kp.publicKey().slice(-4)}`));
713
+ }
714
+ catch {
715
+ console.log(chalk.dim(" Wallet Key: ") + chalk.red("❌ Invalid key — run: asgcard wallet create"));
716
+ allGood = false;
717
+ }
718
+ }
719
+ else {
720
+ console.log(chalk.dim(" Wallet Key: ") + chalk.red("❌ Not configured — run: asgcard wallet create"));
721
+ allGood = false;
722
+ }
723
+ // 4. API health
724
+ const apiUrl = getApiUrl();
725
+ try {
726
+ const res = await fetch(`${apiUrl}/health`, { signal: AbortSignal.timeout(5000) });
727
+ if (res.ok) {
728
+ const data = await res.json();
729
+ console.log(chalk.dim(" API Health: ") + chalk.green(`✅ ${apiUrl} (v${data.version || "?"})`));
730
+ }
731
+ else {
732
+ console.log(chalk.dim(" API Health: ") + chalk.red(`❌ ${apiUrl} returned ${res.status}`));
733
+ allGood = false;
734
+ }
735
+ }
736
+ catch (error) {
737
+ console.log(chalk.dim(" API Health: ") + chalk.red(`❌ ${apiUrl} — unreachable`));
738
+ allGood = false;
739
+ }
740
+ // 5. Stellar Horizon
741
+ try {
742
+ const res = await fetch(`${HORIZON_URL}/`, { signal: AbortSignal.timeout(5000) });
743
+ if (res.ok) {
744
+ console.log(chalk.dim(" Stellar Horizon: ") + chalk.green(`✅ ${HORIZON_URL}`));
745
+ }
746
+ else {
747
+ console.log(chalk.dim(" Stellar Horizon: ") + chalk.red(`❌ ${HORIZON_URL} returned ${res.status}`));
748
+ allGood = false;
749
+ }
750
+ }
751
+ catch {
752
+ console.log(chalk.dim(" Stellar Horizon: ") + chalk.red(`❌ ${HORIZON_URL} — unreachable`));
753
+ allGood = false;
754
+ }
755
+ // 6. Soroban RPC
756
+ const rpcUrl = getRpcUrl() || "https://mainnet.sorobanrpc.com";
757
+ try {
758
+ const res = await fetch(rpcUrl, {
759
+ method: "POST",
760
+ headers: { "Content-Type": "application/json" },
761
+ body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "getHealth" }),
762
+ signal: AbortSignal.timeout(5000),
763
+ });
764
+ if (res.ok) {
765
+ const data = await res.json();
766
+ const status = data.result?.status || "ok";
767
+ console.log(chalk.dim(" Soroban RPC: ") + chalk.green(`✅ ${rpcUrl} (${status})`));
768
+ }
769
+ else {
770
+ console.log(chalk.dim(" Soroban RPC: ") + chalk.red(`❌ ${rpcUrl} returned ${res.status}`));
771
+ allGood = false;
772
+ }
773
+ }
774
+ catch {
775
+ console.log(chalk.dim(" Soroban RPC: ") + chalk.red(`❌ ${rpcUrl} — unreachable`));
776
+ allGood = false;
777
+ }
778
+ // 7. USDC Balance
779
+ if (key) {
780
+ try {
781
+ const { Keypair } = await import("@stellar/stellar-sdk");
782
+ const kp = Keypair.fromSecret(key);
783
+ const balance = await getUsdcBalance(kp.publicKey());
784
+ if (balance === -1) {
785
+ console.log(chalk.dim(" USDC Balance: ") + chalk.yellow("⚠ Could not fetch"));
786
+ }
787
+ else if (balance >= MIN_CARD_COST_USDC) {
788
+ console.log(chalk.dim(" USDC Balance: ") + chalk.green(`✅ $${balance.toFixed(2)}`));
789
+ }
790
+ else {
791
+ console.log(chalk.dim(" USDC Balance: ") + chalk.red(`❌ $${balance.toFixed(2)} (need $${MIN_CARD_COST_USDC} for $10 tier)`));
792
+ allGood = false;
793
+ }
794
+ }
795
+ catch {
796
+ console.log(chalk.dim(" USDC Balance: ") + chalk.yellow("⚠ Could not check (invalid key?)"));
797
+ }
798
+ }
799
+ else {
800
+ console.log(chalk.dim(" USDC Balance: ") + chalk.dim("— (no wallet)"));
801
+ }
802
+ // 8. Skill check
803
+ const skillExists = existsSync(join(SKILL_DIR, "SKILL.md"));
804
+ console.log(chalk.dim(" Agent Skill: ") +
805
+ (skillExists ? chalk.green(`✅ ${SKILL_DIR}`) : chalk.yellow("⚠ Not installed — run: asgcard onboard")));
806
+ // 9. MCP configs
807
+ const codexHas = existsSync(join(homedir(), ".codex", "config.toml")) &&
808
+ readFileSync(join(homedir(), ".codex", "config.toml"), "utf-8").includes("[mcp_servers.asgcard]");
809
+ const claudeHas = (() => {
810
+ try {
811
+ const c = JSON.parse(readFileSync(join(homedir(), ".claude", "mcp.json"), "utf-8"));
812
+ return !!(c.mcpServers?.asgcard);
813
+ }
814
+ catch {
815
+ return false;
816
+ }
817
+ })();
818
+ const cursorHas = (() => {
819
+ try {
820
+ const c = JSON.parse(readFileSync(join(homedir(), ".cursor", "mcp.json"), "utf-8"));
821
+ return !!(c.mcpServers?.asgcard);
822
+ }
823
+ catch {
824
+ return false;
825
+ }
826
+ })();
827
+ const mcpParts = [];
828
+ if (codexHas)
829
+ mcpParts.push("Codex");
830
+ if (claudeHas)
831
+ mcpParts.push("Claude");
832
+ if (cursorHas)
833
+ mcpParts.push("Cursor");
834
+ if (mcpParts.length > 0) {
835
+ console.log(chalk.dim(" MCP Configured: ") + chalk.green(`✅ ${mcpParts.join(", ")}`));
836
+ }
837
+ else {
838
+ console.log(chalk.dim(" MCP Configured: ") + chalk.yellow("⚠ None — run: asgcard install --client <client>"));
839
+ }
840
+ console.log();
841
+ if (allGood) {
842
+ console.log(chalk.green(" ✅ All checks passed! You're ready to create cards.\n"));
843
+ }
844
+ else {
845
+ console.log(chalk.yellow(" ⚠ Some checks failed. Fix the issues above and run ") + chalk.cyan("asgcard doctor") + chalk.yellow(" again.\n"));
846
+ }
847
+ });
848
+ // ═══════════════════════════════════════════════════════════
849
+ // EXISTING CARD COMMANDS (preserved with better error handling)
850
+ // ═══════════════════════════════════════════════════════════
851
+ // ── login (legacy, kept for backward compatibility) ─────────
88
852
  program
89
853
  .command("login")
90
854
  .description("Configure your Stellar private key for wallet authentication")
@@ -94,7 +858,6 @@ program
94
858
  .action(async (key, options) => {
95
859
  let privateKey = key;
96
860
  if (!privateKey) {
97
- // Read from stdin
98
861
  const readline = await import("node:readline");
99
862
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
100
863
  privateKey = await new Promise((resolve) => {
@@ -105,7 +868,7 @@ program
105
868
  });
106
869
  }
107
870
  if (!privateKey?.startsWith("S")) {
108
- console.error(chalk.red("Invalid key. Must start with S (Stellar secret key)."));
871
+ remediate("Invalid key format", "Stellar secret keys start with 'S' and are 56 characters", "asgcard wallet create (to generate a new keypair)");
109
872
  process.exit(1);
110
873
  }
111
874
  const config = {
@@ -115,7 +878,6 @@ program
115
878
  ...(options?.rpcUrl && { rpcUrl: options.rpcUrl }),
116
879
  };
117
880
  saveConfig(config);
118
- // Derive and display the public key
119
881
  const { Keypair } = await import("@stellar/stellar-sdk");
120
882
  const kp = Keypair.fromSecret(privateKey);
121
883
  console.log(chalk.green("✅ Key saved to ~/.asgcard/config.json"));
@@ -144,7 +906,17 @@ program
144
906
  }
145
907
  }
146
908
  catch (error) {
147
- spinner.fail(chalk.red(`Error: ${error instanceof Error ? error.message : error}`));
909
+ spinner.fail();
910
+ const msg = error instanceof Error ? error.message : String(error);
911
+ if (msg.includes("401") || msg.includes("403")) {
912
+ remediate("Authentication failed", "Your wallet signature was rejected by the API", "Check your key: asgcard doctor");
913
+ }
914
+ else if (msg.includes("fetch") || msg.includes("ECONNREFUSED")) {
915
+ remediate("API unreachable", msg, "Check connectivity: asgcard health");
916
+ }
917
+ else {
918
+ remediate("Failed to list cards", msg, "Run: asgcard doctor");
919
+ }
148
920
  process.exit(1);
149
921
  }
150
922
  });
@@ -163,7 +935,8 @@ program
163
935
  console.log(formatCard(result));
164
936
  }
165
937
  catch (error) {
166
- spinner.fail(chalk.red(`Error: ${error instanceof Error ? error.message : error}`));
938
+ spinner.fail();
939
+ remediate("Failed to fetch card", error instanceof Error ? error.message : String(error), "asgcard doctor");
167
940
  process.exit(1);
168
941
  }
169
942
  });
@@ -191,7 +964,14 @@ program
191
964
  console.log(chalk.dim("\n ⚠ Store securely. Rate-limited to 5/hour."));
192
965
  }
193
966
  catch (error) {
194
- spinner.fail(chalk.red(`Error: ${error instanceof Error ? error.message : error}`));
967
+ spinner.fail();
968
+ const msg = error instanceof Error ? error.message : String(error);
969
+ if (msg.includes("429")) {
970
+ remediate("Rate limited", "Card details access is limited to 5 times per hour", "Wait and try again later");
971
+ }
972
+ else {
973
+ remediate("Failed to fetch details", msg, "asgcard doctor");
974
+ }
195
975
  process.exit(1);
196
976
  }
197
977
  });
@@ -205,10 +985,23 @@ program
205
985
  .requiredOption("-e, --email <email>", "Email for notifications")
206
986
  .action(async (options) => {
207
987
  if (!VALID_AMOUNTS.includes(options.amount)) {
208
- console.error(chalk.red(`❌ Invalid amount. Choose from: ${VALID_AMOUNTS.join(", ")}`));
988
+ remediate(`Invalid amount: ${options.amount}`, `Available amounts: ${VALID_AMOUNTS.join(", ")}`, "asgcard pricing (to see all tiers and costs)");
209
989
  process.exit(1);
210
990
  }
211
991
  const key = requireKey();
992
+ // Pre-flight balance check
993
+ try {
994
+ const { Keypair } = await import("@stellar/stellar-sdk");
995
+ const kp = Keypair.fromSecret(key);
996
+ const balance = await getUsdcBalance(kp.publicKey());
997
+ if (balance === 0) {
998
+ remediate("Wallet has zero USDC balance", `You need USDC on Stellar to pay for card creation`, `Send USDC to: ${kp.publicKey()}\n Check balance: asgcard wallet info\n View pricing: asgcard pricing`);
999
+ process.exit(1);
1000
+ }
1001
+ }
1002
+ catch {
1003
+ // Non-critical pre-flight — continue and let SDK handle it
1004
+ }
212
1005
  const spinner = ora(`Creating $${options.amount} card...`).start();
213
1006
  try {
214
1007
  const client = new ASGCardClient({
@@ -232,7 +1025,19 @@ program
232
1025
  console.log(chalk.dim(`\n TX: ${result.payment.txHash}`));
233
1026
  }
234
1027
  catch (error) {
235
- spinner.fail(chalk.red(`Error: ${error instanceof Error ? error.message : error}`));
1028
+ spinner.fail();
1029
+ const msg = error instanceof Error ? error.message : String(error);
1030
+ if (msg.includes("Insufficient") || msg.includes("balance")) {
1031
+ const { Keypair } = await import("@stellar/stellar-sdk");
1032
+ const kp = Keypair.fromSecret(key);
1033
+ remediate("Insufficient USDC balance", msg, `Deposit USDC to: ${kp.publicKey()}\n Then retry: asgcard card:create -a ${options.amount} -n "${options.name}" -e ${options.email}`);
1034
+ }
1035
+ else if (msg.includes("simulation")) {
1036
+ remediate("Transaction simulation failed", msg, "Check: asgcard doctor (RPC connectivity + balance)");
1037
+ }
1038
+ else {
1039
+ remediate("Card creation failed", msg, "asgcard doctor");
1040
+ }
236
1041
  process.exit(1);
237
1042
  }
238
1043
  });
@@ -244,7 +1049,7 @@ program
244
1049
  .requiredOption("-a, --amount <amount>", `Fund amount (${VALID_AMOUNTS.join(", ")})`)
245
1050
  .action(async (id, options) => {
246
1051
  if (!VALID_AMOUNTS.includes(options.amount)) {
247
- console.error(chalk.red(`❌ Invalid amount. Choose from: ${VALID_AMOUNTS.join(", ")}`));
1052
+ remediate(`Invalid amount: ${options.amount}`, `Available amounts: ${VALID_AMOUNTS.join(", ")}`, "asgcard pricing (to see all tiers and costs)");
248
1053
  process.exit(1);
249
1054
  }
250
1055
  const key = requireKey();
@@ -265,7 +1070,14 @@ program
265
1070
  console.log(chalk.dim(` TX: ${result.payment.txHash}`));
266
1071
  }
267
1072
  catch (error) {
268
- spinner.fail(chalk.red(`Error: ${error instanceof Error ? error.message : error}`));
1073
+ spinner.fail();
1074
+ const msg = error instanceof Error ? error.message : String(error);
1075
+ if (msg.includes("Insufficient") || msg.includes("balance")) {
1076
+ remediate("Insufficient USDC balance", msg, "asgcard wallet info (check balance and deposit)");
1077
+ }
1078
+ else {
1079
+ remediate("Funding failed", msg, "asgcard doctor");
1080
+ }
269
1081
  process.exit(1);
270
1082
  }
271
1083
  });
@@ -283,7 +1095,8 @@ program
283
1095
  spinner.succeed(chalk.blue(`❄ Card ${id} frozen`));
284
1096
  }
285
1097
  catch (error) {
286
- spinner.fail(chalk.red(`Error: ${error instanceof Error ? error.message : error}`));
1098
+ spinner.fail();
1099
+ remediate("Failed to freeze card", error instanceof Error ? error.message : String(error), "asgcard doctor");
287
1100
  process.exit(1);
288
1101
  }
289
1102
  });
@@ -300,44 +1113,45 @@ program
300
1113
  spinner.succeed(chalk.green(`🔓 Card ${id} unfrozen`));
301
1114
  }
302
1115
  catch (error) {
303
- spinner.fail(chalk.red(`Error: ${error instanceof Error ? error.message : error}`));
1116
+ spinner.fail();
1117
+ remediate("Failed to unfreeze card", error instanceof Error ? error.message : String(error), "asgcard doctor");
304
1118
  process.exit(1);
305
1119
  }
306
1120
  });
307
- // ── pricing ─────────────────────────────────────────────────
1121
+ // ── pricing (FIXED: no private key required) ────────────────
308
1122
  program
309
1123
  .command("pricing")
310
- .description("View current pricing tiers")
1124
+ .description("View current pricing tiers (no authentication required)")
311
1125
  .action(async () => {
312
1126
  const spinner = ora("Fetching pricing...").start();
313
1127
  try {
314
- const client = new ASGCardClient({
315
- privateKey: "SAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVID", // dummy for public endpoint
316
- baseUrl: getApiUrl(),
317
- });
318
- const tiers = await client.getTiers();
1128
+ const res = await fetch(`${getApiUrl()}/cards/tiers`);
1129
+ if (!res.ok)
1130
+ throw new Error(`API returned ${res.status}`);
1131
+ const tiers = await res.json();
319
1132
  spinner.stop();
320
1133
  console.log(chalk.bold("\n💰 Card Creation Tiers:\n"));
321
1134
  console.log(chalk.dim(" Load Amount Total Cost Endpoint"));
322
1135
  for (const t of tiers.creation) {
323
- console.log(` ${chalk.green("$" + String(t.loadAmount || "?").padEnd(11))} ${chalk.cyan("$" + String(t.totalCost).padEnd(11))} ${chalk.dim(t.endpoint)}`);
1136
+ console.log(` ${chalk.green("$" + String(t.loadAmount || "?").padEnd(11))} ${chalk.cyan("$" + String(t.totalCost).padEnd(11))} ${chalk.dim(String(t.endpoint))}`);
324
1137
  }
325
1138
  console.log(chalk.bold("\n💰 Card Funding Tiers:\n"));
326
1139
  console.log(chalk.dim(" Fund Amount Total Cost Endpoint"));
327
1140
  for (const t of tiers.funding) {
328
- console.log(` ${chalk.green("$" + String(t.fundAmount || "?").padEnd(11))} ${chalk.cyan("$" + String(t.totalCost).padEnd(11))} ${chalk.dim(t.endpoint)}`);
1141
+ console.log(` ${chalk.green("$" + String(t.fundAmount || "?").padEnd(11))} ${chalk.cyan("$" + String(t.totalCost).padEnd(11))} ${chalk.dim(String(t.endpoint))}`);
329
1142
  }
330
1143
  console.log();
331
1144
  }
332
1145
  catch (error) {
333
- spinner.fail(chalk.red(`Error: ${error instanceof Error ? error.message : error}`));
1146
+ spinner.fail();
1147
+ remediate("Failed to fetch pricing", error instanceof Error ? error.message : String(error), "Check API status: asgcard health");
334
1148
  process.exit(1);
335
1149
  }
336
1150
  });
337
1151
  // ── health ──────────────────────────────────────────────────
338
1152
  program
339
1153
  .command("health")
340
- .description("Check API health")
1154
+ .description("Check API health (no authentication required)")
341
1155
  .action(async () => {
342
1156
  const spinner = ora("Checking API...").start();
343
1157
  try {
@@ -347,7 +1161,8 @@ program
347
1161
  chalk.dim(`v${data.version} — ${data.timestamp}`));
348
1162
  }
349
1163
  catch (error) {
350
- spinner.fail(chalk.red(`API unreachable: ${error instanceof Error ? error.message : error}`));
1164
+ spinner.fail();
1165
+ remediate("API unreachable", error instanceof Error ? error.message : String(error), "Check your internet connection and try again");
351
1166
  process.exit(1);
352
1167
  }
353
1168
  });
@@ -356,10 +1171,26 @@ program
356
1171
  .command("whoami")
357
1172
  .description("Show your configured wallet address")
358
1173
  .action(async () => {
359
- const key = requireKey();
1174
+ const key = resolveKey();
1175
+ if (!key) {
1176
+ remediate("No wallet configured", "No key in config, wallet file, or environment", "asgcard wallet create");
1177
+ process.exit(1);
1178
+ }
360
1179
  const { Keypair } = await import("@stellar/stellar-sdk");
361
1180
  const kp = Keypair.fromSecret(key);
362
1181
  console.log(chalk.cyan(kp.publicKey()));
363
1182
  });
1183
+ // ── Default action: no subcommand → onboard -y ─────────────
1184
+ // If the user runs `npx @asgcard/cli` without any subcommand,
1185
+ // default to the onboarding flow. Preserves --help, --version,
1186
+ // and all existing subcommands.
1187
+ const knownCommands = new Set(program.commands.map((c) => c.name()));
1188
+ const userArgs = process.argv.slice(2);
1189
+ const hasSubcommand = userArgs.some((a) => knownCommands.has(a));
1190
+ const hasHelpOrVersion = userArgs.some((a) => ["-h", "--help", "-V", "--version"].includes(a));
1191
+ if (!hasSubcommand && !hasHelpOrVersion && userArgs.length === 0) {
1192
+ console.log(chalk.dim("No command specified — starting onboarding flow...\n"));
1193
+ process.argv.push("onboard", "--yes");
1194
+ }
364
1195
  program.parse();
365
1196
  //# sourceMappingURL=index.js.map