@asgcard/cli 0.1.2 → 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/README.md +70 -67
- package/dist/index.d.ts +10 -2
- package/dist/index.js +871 -40
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/skill/SKILL.md +53 -0
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
|
-
*
|
|
9
|
-
* asgcard
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
52
|
-
chalk.cyan("asgcard
|
|
53
|
-
chalk.dim("
|
|
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("
|
|
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(
|
|
87
|
-
//
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
315
|
-
|
|
316
|
-
|
|
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(
|
|
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(
|
|
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 =
|
|
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
|