agentstore 1.0.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 ADDED
@@ -0,0 +1,1459 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import * as fs from 'fs';
4
+ import * as path from 'path';
5
+ import * as os from 'os';
6
+ import * as crypto from 'crypto';
7
+ import * as readline from 'readline';
8
+ import { fileURLToPath } from 'url';
9
+ import { generatePrivateKey, privateKeyToAccount, } from 'viem/accounts';
10
+ import { createPublicClient, createWalletClient, http, formatEther, parseEther, } from 'viem';
11
+ import { mainnet } from 'viem/chains';
12
+ import * as keytar from 'keytar';
13
+ const __filename = fileURLToPath(import.meta.url);
14
+ const __dirname = path.dirname(__filename);
15
+ const API_BASE = 'https://api.agentstore.dev';
16
+ const MEV_COMMIT_RPC = 'https://fastrpc.mev-commit.xyz';
17
+ const KEYCHAIN_SERVICE = 'agentstore-wallet';
18
+ const KEYCHAIN_ACCOUNT = 'encryption-key';
19
+ // File paths
20
+ const HOME_DIR = os.homedir();
21
+ const AGENTSTORE_DIR = path.join(HOME_DIR, '.agentstore');
22
+ const ROUTES_FILE = path.join(AGENTSTORE_DIR, 'routes.json');
23
+ const ENTITLEMENTS_FILE = path.join(AGENTSTORE_DIR, 'entitlements.json');
24
+ const CLAUDE_DIR = path.join(HOME_DIR, '.claude');
25
+ const SKILLS_DIR = path.join(CLAUDE_DIR, 'skills', 'agentstore');
26
+ const WALLET_FILE = path.join(AGENTSTORE_DIR, 'wallet.json');
27
+ const KEYSTORE_FILE = path.join(AGENTSTORE_DIR, 'wallet.keystore');
28
+ const TX_HISTORY_FILE = path.join(AGENTSTORE_DIR, 'tx_history.json');
29
+ // Create public client for reading blockchain state
30
+ const publicClient = createPublicClient({
31
+ chain: mainnet,
32
+ transport: http(MEV_COMMIT_RPC),
33
+ });
34
+ // Ensure directories exist
35
+ function ensureDirectories() {
36
+ if (!fs.existsSync(AGENTSTORE_DIR)) {
37
+ fs.mkdirSync(AGENTSTORE_DIR, { recursive: true, mode: 0o700 });
38
+ }
39
+ if (!fs.existsSync(SKILLS_DIR)) {
40
+ fs.mkdirSync(SKILLS_DIR, { recursive: true });
41
+ }
42
+ }
43
+ // Get price from pricing object (handles both amount and amount_usd)
44
+ function getPriceUsd(pricing) {
45
+ return pricing.amount ?? pricing.amount_usd ?? 0;
46
+ }
47
+ // Load/save routes
48
+ function loadRoutes() {
49
+ try {
50
+ if (fs.existsSync(ROUTES_FILE)) {
51
+ return JSON.parse(fs.readFileSync(ROUTES_FILE, 'utf-8'));
52
+ }
53
+ }
54
+ catch {
55
+ // ignore
56
+ }
57
+ return [];
58
+ }
59
+ function saveRoutes(routes) {
60
+ fs.writeFileSync(ROUTES_FILE, JSON.stringify(routes, null, 2), { mode: 0o600 });
61
+ }
62
+ // Load/save entitlements
63
+ function loadEntitlements() {
64
+ try {
65
+ if (fs.existsSync(ENTITLEMENTS_FILE)) {
66
+ return JSON.parse(fs.readFileSync(ENTITLEMENTS_FILE, 'utf-8'));
67
+ }
68
+ }
69
+ catch {
70
+ // ignore
71
+ }
72
+ return [];
73
+ }
74
+ function saveEntitlements(entitlements) {
75
+ fs.writeFileSync(ENTITLEMENTS_FILE, JSON.stringify(entitlements, null, 2), { mode: 0o600 });
76
+ }
77
+ // Load wallet config
78
+ function loadWalletConfig() {
79
+ try {
80
+ if (fs.existsSync(WALLET_FILE)) {
81
+ return JSON.parse(fs.readFileSync(WALLET_FILE, 'utf-8'));
82
+ }
83
+ }
84
+ catch {
85
+ // ignore
86
+ }
87
+ return null;
88
+ }
89
+ // Check if wallet exists
90
+ function walletExists() {
91
+ return fs.existsSync(WALLET_FILE) && fs.existsSync(KEYSTORE_FILE);
92
+ }
93
+ // Load transaction history
94
+ function loadTxHistory() {
95
+ try {
96
+ if (fs.existsSync(TX_HISTORY_FILE)) {
97
+ return JSON.parse(fs.readFileSync(TX_HISTORY_FILE, 'utf-8'));
98
+ }
99
+ }
100
+ catch {
101
+ // ignore
102
+ }
103
+ return [];
104
+ }
105
+ // Save transaction history
106
+ function saveTxHistory(history) {
107
+ fs.writeFileSync(TX_HISTORY_FILE, JSON.stringify(history, null, 2), { mode: 0o600 });
108
+ }
109
+ // Encrypt private key
110
+ function encryptKey(key, password) {
111
+ const salt = crypto.randomBytes(32);
112
+ const iv = crypto.randomBytes(16);
113
+ const derivedKey = crypto.pbkdf2Sync(password, salt, 100000, 32, 'sha256');
114
+ const cipher = crypto.createCipheriv('aes-256-gcm', derivedKey, iv);
115
+ let encrypted = cipher.update(key, 'utf8', 'hex');
116
+ encrypted += cipher.final('hex');
117
+ const authTag = cipher.getAuthTag();
118
+ return {
119
+ iv: iv.toString('hex'),
120
+ salt: salt.toString('hex'),
121
+ encryptedKey: encrypted + authTag.toString('hex'),
122
+ };
123
+ }
124
+ // Decrypt private key
125
+ function decryptKey(encrypted, password) {
126
+ const salt = Buffer.from(encrypted.salt, 'hex');
127
+ const iv = Buffer.from(encrypted.iv, 'hex');
128
+ const derivedKey = crypto.pbkdf2Sync(password, salt, 100000, 32, 'sha256');
129
+ const encryptedData = encrypted.encryptedKey.slice(0, -32);
130
+ const authTag = Buffer.from(encrypted.encryptedKey.slice(-32), 'hex');
131
+ const decipher = crypto.createDecipheriv('aes-256-gcm', derivedKey, iv);
132
+ decipher.setAuthTag(authTag);
133
+ let decrypted = decipher.update(encryptedData, 'hex', 'utf8');
134
+ decrypted += decipher.final('utf8');
135
+ return decrypted;
136
+ }
137
+ // Get or create password from keychain or file
138
+ async function getOrCreatePassword() {
139
+ // Priority 1: Environment variable
140
+ if (process.env.AGENTSTORE_WALLET_PASSWORD) {
141
+ return crypto.createHash('sha256').update(process.env.AGENTSTORE_WALLET_PASSWORD).digest('hex');
142
+ }
143
+ // Priority 2: OS keychain
144
+ try {
145
+ const keychainPassword = await keytar.getPassword(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT);
146
+ if (keychainPassword) {
147
+ return keychainPassword;
148
+ }
149
+ const newPassword = crypto.randomBytes(32).toString('hex');
150
+ await keytar.setPassword(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT, newPassword);
151
+ return newPassword;
152
+ }
153
+ catch {
154
+ console.warn('OS keychain unavailable, using file-based password');
155
+ }
156
+ // Priority 3: Password file
157
+ const passwordFile = path.join(AGENTSTORE_DIR, '.password');
158
+ if (fs.existsSync(passwordFile)) {
159
+ const password = fs.readFileSync(passwordFile, 'utf-8').trim();
160
+ return crypto.createHash('sha256').update(password).digest('hex');
161
+ }
162
+ // Priority 4: Generate new password file
163
+ const generatedPassword = crypto.randomBytes(32).toString('hex');
164
+ fs.writeFileSync(passwordFile, generatedPassword, { mode: 0o600 });
165
+ return crypto.createHash('sha256').update(generatedPassword).digest('hex');
166
+ }
167
+ // Load private key from keystore
168
+ async function loadPrivateKey() {
169
+ if (!fs.existsSync(KEYSTORE_FILE))
170
+ return null;
171
+ try {
172
+ const encrypted = JSON.parse(fs.readFileSync(KEYSTORE_FILE, 'utf-8'));
173
+ const password = await getOrCreatePassword();
174
+ return decryptKey(encrypted, password);
175
+ }
176
+ catch (error) {
177
+ console.error('Failed to decrypt wallet:', error instanceof Error ? error.message : error);
178
+ return null;
179
+ }
180
+ }
181
+ // Create new wallet
182
+ async function createNewWallet() {
183
+ ensureDirectories();
184
+ if (walletExists()) {
185
+ throw new Error('Wallet already exists. Delete ~/.agentstore/wallet.* files to create a new one.');
186
+ }
187
+ const privateKey = generatePrivateKey();
188
+ const account = privateKeyToAccount(privateKey);
189
+ const config = {
190
+ address: account.address,
191
+ createdAt: new Date().toISOString(),
192
+ network: 'mainnet',
193
+ rpcEndpoint: MEV_COMMIT_RPC,
194
+ spendLimits: {
195
+ perTransaction: 100,
196
+ daily: 500,
197
+ weekly: 2000,
198
+ },
199
+ allowedPublishers: [],
200
+ };
201
+ const password = await getOrCreatePassword();
202
+ const encrypted = encryptKey(privateKey, password);
203
+ fs.writeFileSync(KEYSTORE_FILE, JSON.stringify(encrypted), { mode: 0o600 });
204
+ fs.writeFileSync(WALLET_FILE, JSON.stringify(config, null, 2), { mode: 0o600 });
205
+ fs.writeFileSync(TX_HISTORY_FILE, JSON.stringify([]), { mode: 0o600 });
206
+ return { address: account.address };
207
+ }
208
+ // Create wallet silently (for lazy creation during install)
209
+ async function ensureWalletExists() {
210
+ if (walletExists()) {
211
+ const config = loadWalletConfig();
212
+ return { address: config.address, created: false };
213
+ }
214
+ // Create wallet silently
215
+ ensureDirectories();
216
+ const privateKey = generatePrivateKey();
217
+ const account = privateKeyToAccount(privateKey);
218
+ const config = {
219
+ address: account.address,
220
+ createdAt: new Date().toISOString(),
221
+ network: 'mainnet',
222
+ rpcEndpoint: MEV_COMMIT_RPC,
223
+ spendLimits: {
224
+ perTransaction: 100,
225
+ daily: 500,
226
+ weekly: 2000,
227
+ },
228
+ allowedPublishers: [],
229
+ };
230
+ const password = await getOrCreatePassword();
231
+ const encrypted = encryptKey(privateKey, password);
232
+ fs.writeFileSync(KEYSTORE_FILE, JSON.stringify(encrypted), { mode: 0o600 });
233
+ fs.writeFileSync(WALLET_FILE, JSON.stringify(config, null, 2), { mode: 0o600 });
234
+ fs.writeFileSync(TX_HISTORY_FILE, JSON.stringify([]), { mode: 0o600 });
235
+ return { address: account.address, created: true };
236
+ }
237
+ // Trigger funding flow and wait for funds
238
+ async function triggerFundingFlow(requiredUsd) {
239
+ const config = loadWalletConfig();
240
+ if (!config)
241
+ return false;
242
+ console.log('\nšŸ’³ Opening Coinbase to fund your wallet...\n');
243
+ console.log(` Wallet: ${config.address}`);
244
+ console.log(` Required: ~$${requiredUsd} USD\n`);
245
+ // Get initial balance
246
+ const initialBalance = await publicClient.getBalance({
247
+ address: config.address,
248
+ });
249
+ // Get onramp URL from API
250
+ const response = await fetch(`${API_BASE}/api/onramp/session`, {
251
+ method: 'POST',
252
+ headers: { 'Content-Type': 'application/json' },
253
+ body: JSON.stringify({
254
+ wallet_address: config.address,
255
+ amount_usd: Math.ceil(requiredUsd * 1.1), // Add 10% buffer for gas
256
+ }),
257
+ });
258
+ const result = await response.json();
259
+ if (!response.ok || !result.success) {
260
+ if (result.manual_instructions) {
261
+ console.log('āš ļø Coinbase Onramp not configured.\n');
262
+ console.log(' Manual funding instructions:');
263
+ console.log(` 1. ${result.manual_instructions.step1}`);
264
+ console.log(` 2. ${result.manual_instructions.step2}`);
265
+ console.log(` 3. ${result.manual_instructions.step3}`);
266
+ console.log(`\n Your wallet address: ${config.address}`);
267
+ console.log('\n After funding, run the install command again.');
268
+ }
269
+ return false;
270
+ }
271
+ // Open browser
272
+ const { exec } = await import('child_process');
273
+ const openCmd = process.platform === 'darwin'
274
+ ? `open "${result.onramp_url}"`
275
+ : process.platform === 'win32'
276
+ ? `start "${result.onramp_url}"`
277
+ : `xdg-open "${result.onramp_url}"`;
278
+ exec(openCmd);
279
+ console.log('🌐 Coinbase opened in your browser.\n');
280
+ console.log(' Complete the purchase, then wait for funds to arrive.');
281
+ console.log('ā³ Waiting for funds (Ctrl+C to cancel)...\n');
282
+ // Poll for balance
283
+ const startTime = Date.now();
284
+ const maxWaitTime = 10 * 60 * 1000; // 10 minutes
285
+ const pollInterval = 10 * 1000; // 10 seconds
286
+ while (Date.now() - startTime < maxWaitTime) {
287
+ await new Promise((resolve) => setTimeout(resolve, pollInterval));
288
+ try {
289
+ const currentBalance = await publicClient.getBalance({
290
+ address: config.address,
291
+ });
292
+ if (currentBalance > initialBalance) {
293
+ const added = currentBalance - initialBalance;
294
+ console.log(`\nāœ… Funds received! +${formatEther(added)} ETH\n`);
295
+ return true;
296
+ }
297
+ const elapsed = Math.floor((Date.now() - startTime) / 1000);
298
+ process.stdout.write(`\r Checking balance... (${elapsed}s elapsed)`);
299
+ }
300
+ catch {
301
+ // Ignore poll errors
302
+ }
303
+ }
304
+ console.log('\nāš ļø Timeout waiting for funds. Run install again after funding.');
305
+ return false;
306
+ }
307
+ // Get wallet balance
308
+ async function getWalletBalance() {
309
+ const config = loadWalletConfig();
310
+ if (!config)
311
+ throw new Error('Wallet not initialized');
312
+ const balanceWei = await publicClient.getBalance({
313
+ address: config.address,
314
+ });
315
+ const ethBalance = formatEther(balanceWei);
316
+ const ethPrice = await getEthPrice();
317
+ const usdBalance = parseFloat(ethBalance) * ethPrice;
318
+ return {
319
+ eth: ethBalance,
320
+ usd: Math.round(usdBalance * 100) / 100,
321
+ };
322
+ }
323
+ // Check spend limits
324
+ function checkSpendLimit(amountUsd, config, txHistory) {
325
+ // Per-transaction limit
326
+ if (amountUsd > config.spendLimits.perTransaction) {
327
+ return {
328
+ allowed: false,
329
+ reason: `Amount $${amountUsd} exceeds per-transaction limit of $${config.spendLimits.perTransaction}`,
330
+ };
331
+ }
332
+ // Daily limit
333
+ const oneDayAgo = Date.now() - 24 * 60 * 60 * 1000;
334
+ const dailySpent = txHistory
335
+ .filter((tx) => new Date(tx.timestamp).getTime() > oneDayAgo && tx.status === 'confirmed')
336
+ .reduce((sum, tx) => sum + tx.amountUsd, 0);
337
+ if (dailySpent + amountUsd > config.spendLimits.daily) {
338
+ return {
339
+ allowed: false,
340
+ reason: `Would exceed daily limit of $${config.spendLimits.daily} (spent: $${dailySpent.toFixed(2)})`,
341
+ };
342
+ }
343
+ // Weekly limit
344
+ const oneWeekAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
345
+ const weeklySpent = txHistory
346
+ .filter((tx) => new Date(tx.timestamp).getTime() > oneWeekAgo && tx.status === 'confirmed')
347
+ .reduce((sum, tx) => sum + tx.amountUsd, 0);
348
+ if (weeklySpent + amountUsd > config.spendLimits.weekly) {
349
+ return {
350
+ allowed: false,
351
+ reason: `Would exceed weekly limit of $${config.spendLimits.weekly} (spent: $${weeklySpent.toFixed(2)})`,
352
+ };
353
+ }
354
+ return { allowed: true };
355
+ }
356
+ // Send payment for agent
357
+ async function sendAgentPayment(params) {
358
+ const config = loadWalletConfig();
359
+ if (!config)
360
+ throw new Error('Wallet not initialized');
361
+ const privateKey = await loadPrivateKey();
362
+ if (!privateKey)
363
+ throw new Error('Could not load wallet private key');
364
+ const txHistory = loadTxHistory();
365
+ // Check spend limits
366
+ const limitCheck = checkSpendLimit(params.amountUsd, config, txHistory);
367
+ if (!limitCheck.allowed) {
368
+ throw new Error(limitCheck.reason);
369
+ }
370
+ // Check publisher allowlist
371
+ if (config.allowedPublishers.length > 0 && !config.allowedPublishers.includes(params.to.toLowerCase())) {
372
+ throw new Error(`Publisher ${params.to} is not in your allowed publishers list`);
373
+ }
374
+ // Get current ETH price
375
+ const ethPrice = await getEthPrice();
376
+ const amountEth = params.amountUsd / ethPrice;
377
+ const amountWei = parseEther(amountEth.toFixed(18));
378
+ // Check balance
379
+ const balance = await getWalletBalance();
380
+ if (parseFloat(balance.eth) < amountEth) {
381
+ throw new Error(`Insufficient balance: have ${balance.eth} ETH, need ${amountEth.toFixed(6)} ETH`);
382
+ }
383
+ // Create wallet client for signing
384
+ const account = privateKeyToAccount(privateKey);
385
+ const walletClient = createWalletClient({
386
+ account,
387
+ chain: mainnet,
388
+ transport: http(MEV_COMMIT_RPC),
389
+ });
390
+ console.log('Sending transaction...');
391
+ // Send transaction
392
+ const txHash = await walletClient.sendTransaction({
393
+ to: params.to,
394
+ value: amountWei,
395
+ });
396
+ // Record transaction
397
+ const txRecord = {
398
+ txHash,
399
+ to: params.to,
400
+ amountEth: amountEth.toFixed(6),
401
+ amountUsd: params.amountUsd,
402
+ agentId: params.agentId,
403
+ timestamp: new Date().toISOString(),
404
+ status: 'pending',
405
+ };
406
+ txHistory.push(txRecord);
407
+ saveTxHistory(txHistory);
408
+ console.log(`Transaction sent: ${txHash}`);
409
+ // Wait for confirmation
410
+ try {
411
+ console.log('Waiting for confirmation...');
412
+ const receipt = await publicClient.waitForTransactionReceipt({
413
+ hash: txHash,
414
+ confirmations: 2,
415
+ timeout: 120_000,
416
+ });
417
+ const txIndex = txHistory.findIndex((tx) => tx.txHash === txHash);
418
+ const txRecord = txHistory[txIndex];
419
+ if (txIndex !== -1 && txRecord) {
420
+ txRecord.status = receipt.status === 'success' ? 'confirmed' : 'failed';
421
+ saveTxHistory(txHistory);
422
+ }
423
+ if (receipt.status !== 'success') {
424
+ throw new Error('Transaction failed on chain');
425
+ }
426
+ console.log('āœ“ Transaction confirmed!');
427
+ }
428
+ catch (error) {
429
+ const txIndex = txHistory.findIndex((tx) => tx.txHash === txHash);
430
+ const txRecord = txHistory[txIndex];
431
+ if (txIndex !== -1 && txRecord) {
432
+ txRecord.status = 'failed';
433
+ saveTxHistory(txHistory);
434
+ }
435
+ throw error;
436
+ }
437
+ return { txHash, amountEth: amountEth.toFixed(6) };
438
+ }
439
+ // Get ETH price from CoinGecko
440
+ async function getEthPrice() {
441
+ try {
442
+ const response = await fetch('https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd');
443
+ const data = (await response.json());
444
+ return data.ethereum?.usd || 2000;
445
+ }
446
+ catch {
447
+ return 2000;
448
+ }
449
+ }
450
+ // Prompt user for confirmation
451
+ function prompt(question) {
452
+ const rl = readline.createInterface({
453
+ input: process.stdin,
454
+ output: process.stdout,
455
+ });
456
+ return new Promise((resolve) => {
457
+ rl.question(question, (answer) => {
458
+ rl.close();
459
+ resolve(answer.trim().toLowerCase());
460
+ });
461
+ });
462
+ }
463
+ // Purchase agent via API
464
+ async function purchaseAgent(agentId, walletAddress, txHash) {
465
+ try {
466
+ const response = await fetch(`${API_BASE}/api/purchase`, {
467
+ method: 'POST',
468
+ headers: { 'Content-Type': 'application/json' },
469
+ body: JSON.stringify({
470
+ agent_id: agentId,
471
+ wallet_address: walletAddress,
472
+ tx_hash: txHash,
473
+ }),
474
+ });
475
+ if (!response.ok) {
476
+ const error = (await response.json());
477
+ console.error(`Purchase failed: ${error.error || response.statusText}`);
478
+ return null;
479
+ }
480
+ return (await response.json());
481
+ }
482
+ catch (error) {
483
+ console.error(`Purchase error: ${error instanceof Error ? error.message : error}`);
484
+ return null;
485
+ }
486
+ }
487
+ // Fetch agent from API
488
+ async function fetchAgent(agentId) {
489
+ try {
490
+ const response = await fetch(`${API_BASE}/api/agents/${encodeURIComponent(agentId)}`);
491
+ if (!response.ok) {
492
+ if (response.status === 404) {
493
+ console.error(`Agent not found: ${agentId}`);
494
+ return null;
495
+ }
496
+ throw new Error(`API error: ${response.status}`);
497
+ }
498
+ return (await response.json());
499
+ }
500
+ catch (error) {
501
+ console.error(`Failed to fetch agent: ${error instanceof Error ? error.message : error}`);
502
+ return null;
503
+ }
504
+ }
505
+ // Create skill file for Claude
506
+ function createSkillFile(agent) {
507
+ // API returns flat structure - agent IS the manifest
508
+ const tools = agent.install.gateway_routes.flatMap((r) => r.tools.map((t) => `- \`${agent.agent_id}:${t.name}\` - ${t.description}`));
509
+ const hasTools = tools.length > 0;
510
+ const agentWrapper = agent.install.agent_wrapper;
511
+ let content;
512
+ if (hasTools) {
513
+ content = `---
514
+ description: ${agent.description}
515
+ ---
516
+
517
+ # ${agent.name}
518
+
519
+ **Publisher:** ${agent.publisher.display_name}
520
+ **Version:** ${agent.version}
521
+ **Type:** ${agent.type === 'open' ? 'Free' : 'Paid'}
522
+
523
+ ${agent.description}
524
+
525
+ ## Available Tools
526
+
527
+ ${tools.join('\n')}
528
+
529
+ ## Usage
530
+
531
+ These tools are available via the AgentStore gateway. Simply ask Claude to use them by name.
532
+
533
+ Example: "Use ${agent.agent_id}:${agent.install.gateway_routes[0]?.tools[0]?.name || 'tool_name'} to..."
534
+ `;
535
+ }
536
+ else {
537
+ // Simple agent without MCP tools - use agent_wrapper content or description
538
+ const wrapperContent = agentWrapper?.content || agent.description;
539
+ content = `---
540
+ description: ${agent.description}
541
+ ---
542
+
543
+ # ${agent.name}
544
+
545
+ **Publisher:** ${agent.publisher.display_name}
546
+ **Version:** ${agent.version}
547
+ **Type:** ${agent.type === 'open' ? 'Free' : 'Paid'}
548
+
549
+ ${wrapperContent}
550
+
551
+ ## Usage
552
+
553
+ This is a prompt-based agent. Reference it by asking Claude to follow the instructions from "${agent.name}".
554
+ `;
555
+ }
556
+ const skillFile = path.join(SKILLS_DIR, `${agent.agent_id.replace(/\./g, '-')}.md`);
557
+ fs.writeFileSync(skillFile, content);
558
+ console.log(` Created skill file: ${skillFile}`);
559
+ }
560
+ // Install command
561
+ async function installAgent(agentId, options) {
562
+ ensureDirectories();
563
+ console.log(`Fetching agent: ${agentId}...`);
564
+ const agent = await fetchAgent(agentId);
565
+ if (!agent) {
566
+ process.exit(1);
567
+ }
568
+ // API returns flat structure - agent IS the manifest
569
+ // Check if already installed
570
+ const routes = loadRoutes();
571
+ const existingRoute = routes.find((r) => r.agentId === agent.agent_id);
572
+ if (existingRoute) {
573
+ console.log(`Agent ${agent.agent_id} is already installed.`);
574
+ console.log('Use --yes to reinstall/update.');
575
+ if (!options.yes) {
576
+ process.exit(0);
577
+ }
578
+ }
579
+ // Display agent info
580
+ console.log('\nā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”');
581
+ console.log(`│ Installing: ${agent.name} v${agent.version}`.padEnd(50) + '│');
582
+ console.log('ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤');
583
+ console.log(`│ Publisher: ${agent.publisher.display_name}`.padEnd(50) + '│');
584
+ console.log(`│ Type: ${agent.type === 'open' ? 'Free (Open Source)' : 'Paid (Proprietary)'}`.padEnd(50) + '│');
585
+ console.log(`│ Price: ${agent.pricing.model === 'free' ? 'FREE' : `$${getPriceUsd(agent.pricing)}`}`.padEnd(50) + '│');
586
+ console.log('ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤');
587
+ console.log('│ Tools:'.padEnd(50) + '│');
588
+ for (const route of agent.install.gateway_routes) {
589
+ for (const tool of route.tools) {
590
+ const toolLine = `│ • ${tool.name}`;
591
+ console.log(toolLine.padEnd(50) + '│');
592
+ }
593
+ }
594
+ console.log('ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜');
595
+ // For paid agents, handle payment flow
596
+ let entitlementToken = null;
597
+ let expiresAt = null;
598
+ if (agent.type === 'proprietary' && agent.pricing.model !== 'free') {
599
+ // Lazy wallet creation - create if doesn't exist
600
+ const { address: walletAddress, created: walletCreated } = await ensureWalletExists();
601
+ if (walletCreated) {
602
+ console.log('\nšŸ” Wallet created automatically');
603
+ console.log(` Address: ${walletAddress}`);
604
+ }
605
+ const wallet = loadWalletConfig();
606
+ const ethPrice = await getEthPrice();
607
+ const priceEth = getPriceUsd(agent.pricing) / ethPrice;
608
+ console.log('\nšŸ’° Payment Required:');
609
+ console.log(` Price: $${getPriceUsd(agent.pricing)} (~${priceEth.toFixed(6)} ETH)`);
610
+ console.log(` Your wallet: ${wallet.address}`);
611
+ console.log(` ETH Price: $${ethPrice}`);
612
+ // Check if already purchased
613
+ const entitlements = loadEntitlements();
614
+ const existing = entitlements.find((e) => e.agentId === agent.agent_id);
615
+ if (existing) {
616
+ console.log('\nāœ“ Already purchased! Using existing entitlement.');
617
+ entitlementToken = existing.token;
618
+ expiresAt = existing.expiresAt;
619
+ }
620
+ else {
621
+ let txHash = options.txHash;
622
+ // Direct payment with --pay flag
623
+ if (options.pay && !txHash) {
624
+ const payoutAddress = agent.publisher.payout_address;
625
+ if (!payoutAddress) {
626
+ console.log('\nāŒ Publisher has no payout address configured.');
627
+ console.log(' Contact the publisher or use --tx-hash with manual payment.');
628
+ process.exit(1);
629
+ }
630
+ // Check balance and trigger funding if needed
631
+ try {
632
+ const balance = await getWalletBalance();
633
+ console.log(`\n Your balance: ${balance.eth} ETH ($${balance.usd})`);
634
+ // Auto-trigger funding flow if insufficient balance
635
+ if (balance.usd < getPriceUsd(agent.pricing)) {
636
+ console.log(`\nāš ļø Insufficient balance. Need $${getPriceUsd(agent.pricing)}, have $${balance.usd}`);
637
+ const funded = await triggerFundingFlow(getPriceUsd(agent.pricing));
638
+ if (!funded) {
639
+ process.exit(1);
640
+ }
641
+ // Re-check balance after funding
642
+ const newBalance = await getWalletBalance();
643
+ console.log(` New balance: ${newBalance.eth} ETH ($${newBalance.usd})`);
644
+ if (newBalance.usd < getPriceUsd(agent.pricing)) {
645
+ console.log(`\nāŒ Still insufficient. Need $${getPriceUsd(agent.pricing)}, have $${newBalance.usd}`);
646
+ process.exit(1);
647
+ }
648
+ }
649
+ }
650
+ catch (error) {
651
+ console.log(`\nāŒ Could not check balance: ${error instanceof Error ? error.message : error}`);
652
+ process.exit(1);
653
+ }
654
+ // Confirm payment
655
+ if (!options.yes) {
656
+ const confirm = await prompt(`\nPay $${getPriceUsd(agent.pricing)} (~${priceEth.toFixed(6)} ETH) to ${payoutAddress.slice(0, 10)}...? (y/n) `);
657
+ if (confirm !== 'y' && confirm !== 'yes') {
658
+ console.log('Payment cancelled.');
659
+ process.exit(0);
660
+ }
661
+ }
662
+ // Send payment
663
+ try {
664
+ console.log('\nšŸ”„ Processing payment...');
665
+ const payment = await sendAgentPayment({
666
+ to: payoutAddress,
667
+ amountUsd: getPriceUsd(agent.pricing),
668
+ agentId: agent.agent_id,
669
+ });
670
+ txHash = payment.txHash;
671
+ console.log(`āœ“ Payment sent: ${txHash}`);
672
+ }
673
+ catch (error) {
674
+ console.log(`\nāŒ Payment failed: ${error instanceof Error ? error.message : error}`);
675
+ process.exit(1);
676
+ }
677
+ }
678
+ // Verify payment with API
679
+ if (!txHash) {
680
+ console.log('\nšŸ’³ Payment options:');
681
+ console.log(' 1. Auto-pay: agentstore install ' + agent.agent_id + ' --pay');
682
+ console.log(' 2. Manual: Send ' + priceEth.toFixed(6) + ' ETH to ' + (agent.publisher.payout_address || '[publisher]'));
683
+ console.log(' Then: agentstore install ' + agent.agent_id + ' --tx-hash 0x...');
684
+ process.exit(1);
685
+ }
686
+ console.log('\nVerifying payment with marketplace...');
687
+ const purchase = await purchaseAgent(agent.agent_id, wallet.address, txHash);
688
+ if (!purchase) {
689
+ console.log('āŒ Payment verification failed.');
690
+ process.exit(1);
691
+ }
692
+ entitlementToken = purchase.entitlement_token;
693
+ expiresAt = purchase.expires_at;
694
+ console.log('āœ“ Payment verified!');
695
+ }
696
+ }
697
+ console.log('\nInstalling...');
698
+ // Remove existing route if updating
699
+ const updatedRoutes = routes.filter((r) => r.agentId !== agent.agent_id);
700
+ // Add new routes
701
+ for (const gatewayRoute of agent.install.gateway_routes) {
702
+ const route = {
703
+ agentId: agent.agent_id,
704
+ routeId: gatewayRoute.route_id,
705
+ mcpEndpoint: gatewayRoute.mcp_endpoint,
706
+ tools: gatewayRoute.tools,
707
+ authType: gatewayRoute.auth.type,
708
+ };
709
+ updatedRoutes.push(route);
710
+ }
711
+ saveRoutes(updatedRoutes);
712
+ console.log(` āœ“ Added ${agent.install.gateway_routes.length} route(s) to ${ROUTES_FILE}`);
713
+ // Save entitlement for paid agents
714
+ if (entitlementToken) {
715
+ const entitlements = loadEntitlements();
716
+ const existingIdx = entitlements.findIndex((e) => e.agentId === agent.agent_id);
717
+ const newEntitlement = {
718
+ agentId: agent.agent_id,
719
+ token: entitlementToken,
720
+ expiresAt: expiresAt,
721
+ };
722
+ if (existingIdx >= 0) {
723
+ entitlements[existingIdx] = newEntitlement;
724
+ }
725
+ else {
726
+ entitlements.push(newEntitlement);
727
+ }
728
+ saveEntitlements(entitlements);
729
+ console.log(` āœ“ Saved entitlement to ${ENTITLEMENTS_FILE}`);
730
+ }
731
+ // Create skill file
732
+ createSkillFile(agent);
733
+ // Count tools
734
+ const toolCount = agent.install.gateway_routes.reduce((sum, r) => sum + r.tools.length, 0);
735
+ console.log('\nāœ… Installation complete!');
736
+ console.log(`\n Agent: ${agent.agent_id}`);
737
+ if (toolCount > 0) {
738
+ console.log(` Tools: ${toolCount} available`);
739
+ console.log('\n To use, ask Claude to call the tools, e.g.:');
740
+ const firstTool = agent.install.gateway_routes[0]?.tools[0];
741
+ if (firstTool) {
742
+ console.log(` "Use ${agent.agent_id}:${firstTool.name}"`);
743
+ }
744
+ }
745
+ else {
746
+ console.log(` Type: Prompt-based agent (no MCP tools)`);
747
+ console.log(`\n To use, ask Claude to follow the "${agent.name}" instructions.`);
748
+ }
749
+ }
750
+ // List installed agents
751
+ function listAgents() {
752
+ const routes = loadRoutes();
753
+ if (routes.length === 0) {
754
+ console.log('No agents installed.');
755
+ console.log('\nRun: agentstore install <agent_id>');
756
+ return;
757
+ }
758
+ // Group by agentId
759
+ const agents = new Map();
760
+ for (const route of routes) {
761
+ const existing = agents.get(route.agentId) || [];
762
+ existing.push(route);
763
+ agents.set(route.agentId, existing);
764
+ }
765
+ console.log(`\nInstalled Agents (${agents.size}):\n`);
766
+ for (const [agentId, agentRoutes] of agents) {
767
+ const toolCount = agentRoutes.reduce((sum, r) => sum + r.tools.length, 0);
768
+ const authType = agentRoutes[0]?.authType || 'none';
769
+ console.log(` ${agentId}`);
770
+ console.log(` Tools: ${toolCount}`);
771
+ console.log(` Auth: ${authType}`);
772
+ for (const route of agentRoutes) {
773
+ for (const tool of route.tools) {
774
+ console.log(` • ${agentId}:${tool.name}`);
775
+ }
776
+ }
777
+ console.log();
778
+ }
779
+ }
780
+ // Uninstall agent
781
+ function uninstallAgent(agentId) {
782
+ const routes = loadRoutes();
783
+ const entitlements = loadEntitlements();
784
+ const routesBefore = routes.length;
785
+ const updatedRoutes = routes.filter((r) => r.agentId !== agentId);
786
+ if (updatedRoutes.length === routesBefore) {
787
+ console.log(`Agent not found: ${agentId}`);
788
+ process.exit(1);
789
+ }
790
+ saveRoutes(updatedRoutes);
791
+ console.log(` āœ“ Removed routes from ${ROUTES_FILE}`);
792
+ // Remove entitlement if exists
793
+ const updatedEntitlements = entitlements.filter((e) => e.agentId !== agentId);
794
+ if (updatedEntitlements.length < entitlements.length) {
795
+ saveEntitlements(updatedEntitlements);
796
+ console.log(` āœ“ Removed entitlement from ${ENTITLEMENTS_FILE}`);
797
+ }
798
+ // Remove skill file
799
+ const skillFile = path.join(SKILLS_DIR, `${agentId.replace(/\./g, '-')}.md`);
800
+ if (fs.existsSync(skillFile)) {
801
+ fs.unlinkSync(skillFile);
802
+ console.log(` āœ“ Removed skill file: ${skillFile}`);
803
+ }
804
+ console.log(`\nāœ… Uninstalled: ${agentId}`);
805
+ }
806
+ // Show config info
807
+ function showConfig() {
808
+ console.log('\nAgentStore Configuration:\n');
809
+ console.log(` Config directory: ${AGENTSTORE_DIR}`);
810
+ console.log(` Routes file: ${ROUTES_FILE}`);
811
+ console.log(` Entitlements file: ${ENTITLEMENTS_FILE}`);
812
+ console.log(` Skills directory: ${SKILLS_DIR}`);
813
+ const routes = loadRoutes();
814
+ const entitlements = loadEntitlements();
815
+ console.log(`\n Installed agents: ${new Set(routes.map((r) => r.agentId)).size}`);
816
+ console.log(` Total routes: ${routes.length}`);
817
+ console.log(` Entitlements: ${entitlements.length}`);
818
+ // Check if gateway is configured
819
+ const mcpConfigFile = path.join(CLAUDE_DIR, 'mcp.json');
820
+ if (fs.existsSync(mcpConfigFile)) {
821
+ try {
822
+ const mcpConfig = JSON.parse(fs.readFileSync(mcpConfigFile, 'utf-8'));
823
+ const hasGateway = mcpConfig.mcpServers?.['agentstore-gateway'];
824
+ console.log(`\n Gateway configured: ${hasGateway ? 'āœ“ Yes' : 'āœ— No'}`);
825
+ }
826
+ catch {
827
+ console.log('\n Gateway configured: ? (could not read mcp.json)');
828
+ }
829
+ }
830
+ else {
831
+ console.log('\n Gateway configured: āœ— No (mcp.json not found)');
832
+ }
833
+ }
834
+ // Setup gateway in Claude's MCP config
835
+ function setupGateway() {
836
+ const mcpConfigFile = path.join(CLAUDE_DIR, 'mcp.json');
837
+ let mcpConfig = { mcpServers: {} };
838
+ if (fs.existsSync(mcpConfigFile)) {
839
+ try {
840
+ mcpConfig = JSON.parse(fs.readFileSync(mcpConfigFile, 'utf-8'));
841
+ }
842
+ catch {
843
+ console.log('Warning: Could not parse existing mcp.json, creating new one');
844
+ }
845
+ }
846
+ if (!mcpConfig.mcpServers || typeof mcpConfig.mcpServers !== 'object') {
847
+ mcpConfig.mcpServers = {};
848
+ }
849
+ const servers = mcpConfig.mcpServers;
850
+ // Check if already configured
851
+ if (servers['agentstore-gateway']) {
852
+ console.log('Gateway already configured in mcp.json');
853
+ return;
854
+ }
855
+ // Find the gateway executable - try multiple locations
856
+ const possiblePaths = [
857
+ // Installed globally via npm
858
+ path.join(os.homedir(), '.npm-global', 'lib', 'node_modules', '@agentstore', 'gateway', 'dist', 'index.js'),
859
+ // Local development (relative to CLI dist)
860
+ path.join(__dirname, '..', '..', 'gateway', 'dist', 'index.js'),
861
+ // Relative to cwd
862
+ path.join(process.cwd(), 'packages', 'gateway', 'dist', 'index.js'),
863
+ ];
864
+ let gatewayPath = possiblePaths.find((p) => fs.existsSync(p));
865
+ if (!gatewayPath) {
866
+ // Default to the local dev relative path
867
+ gatewayPath = path.join(__dirname, '..', '..', 'gateway', 'dist', 'index.js');
868
+ console.log('āš ļø Gateway not found at expected paths.');
869
+ console.log(' Make sure to build the gateway: cd packages/gateway && npm run build');
870
+ }
871
+ servers['agentstore-gateway'] = {
872
+ command: 'node',
873
+ args: [gatewayPath],
874
+ };
875
+ console.log(` Gateway path: ${gatewayPath}`);
876
+ // Ensure .claude directory exists
877
+ if (!fs.existsSync(CLAUDE_DIR)) {
878
+ fs.mkdirSync(CLAUDE_DIR, { recursive: true });
879
+ }
880
+ fs.writeFileSync(mcpConfigFile, JSON.stringify(mcpConfig, null, 2));
881
+ console.log(`āœ“ Added agentstore-gateway to ${mcpConfigFile}`);
882
+ console.log('\nRestart Claude Code to activate the gateway.');
883
+ }
884
+ // Main CLI
885
+ const program = new Command();
886
+ program
887
+ .name('agentstore')
888
+ .description('AgentStore CLI - Install and manage marketplace agents')
889
+ .version('1.0.0');
890
+ program
891
+ .command('install <agent_id>')
892
+ .description('Install an agent from the marketplace')
893
+ .option('-y, --yes', 'Skip confirmation / force reinstall')
894
+ .option('--pay', 'Pay for agent directly from wallet')
895
+ .option('--tx-hash <hash>', 'Transaction hash for manual payment verification')
896
+ .action(installAgent);
897
+ program
898
+ .command('list')
899
+ .alias('ls')
900
+ .description('List installed agents')
901
+ .action(listAgents);
902
+ program
903
+ .command('uninstall <agent_id>')
904
+ .alias('rm')
905
+ .description('Uninstall an agent')
906
+ .action(uninstallAgent);
907
+ program
908
+ .command('config')
909
+ .description('Show configuration info')
910
+ .action(showConfig);
911
+ program
912
+ .command('gateway-setup')
913
+ .description('Configure gateway in Claude MCP settings')
914
+ .action(setupGateway);
915
+ program
916
+ .command('browse')
917
+ .description('Browse agents in the marketplace')
918
+ .option('-s, --search <query>', 'Search for agents')
919
+ .option('-t, --tag <tag>', 'Filter by tag')
920
+ .action(async (options) => {
921
+ try {
922
+ let url = `${API_BASE}/api/agents`;
923
+ const params = [];
924
+ if (options.search)
925
+ params.push(`search=${encodeURIComponent(options.search)}`);
926
+ if (options.tag)
927
+ params.push(`tag=${encodeURIComponent(options.tag)}`);
928
+ if (params.length > 0)
929
+ url += '?' + params.join('&');
930
+ const response = await fetch(url);
931
+ if (!response.ok) {
932
+ console.error('Failed to fetch agents');
933
+ process.exit(1);
934
+ }
935
+ const data = (await response.json());
936
+ const agents = data.agents || [];
937
+ if (agents.length === 0) {
938
+ console.log('No agents found.');
939
+ return;
940
+ }
941
+ console.log(`\nšŸ“¦ AgentStore Marketplace (${agents.length} agents)\n`);
942
+ for (const agent of agents) {
943
+ const priceAmount = agent.pricing ? getPriceUsd(agent.pricing) : 0;
944
+ const isFree = agent.pricing?.model === 'free' || priceAmount === 0;
945
+ const price = isFree ? 'FREE' : `$${priceAmount}`;
946
+ const priceEmoji = isFree ? 'šŸ†“' : 'šŸ’°';
947
+ const featured = agent.is_featured ? '⭐ ' : '';
948
+ console.log(` ${featured}${agent.name}`);
949
+ console.log(` ID: ${agent.agent_id}`);
950
+ console.log(` ${priceEmoji} ${price} | by ${agent.publisher?.display_name || 'Unknown'}`);
951
+ console.log(` ${(agent.description || '').slice(0, 70)}${(agent.description || '').length > 70 ? '...' : ''}`);
952
+ console.log();
953
+ }
954
+ console.log('Install with: agentstore install <agent_id>');
955
+ console.log('For paid agents: agentstore install <agent_id> --pay');
956
+ }
957
+ catch (error) {
958
+ console.error(`Error: ${error instanceof Error ? error.message : error}`);
959
+ process.exit(1);
960
+ }
961
+ });
962
+ // Wallet commands
963
+ const walletCmd = program
964
+ .command('wallet')
965
+ .description('Manage your AgentStore wallet');
966
+ walletCmd
967
+ .command('setup')
968
+ .description('Create a new wallet')
969
+ .action(async () => {
970
+ try {
971
+ if (walletExists()) {
972
+ const config = loadWalletConfig();
973
+ console.log('Wallet already exists.');
974
+ console.log(`Address: ${config?.address}`);
975
+ return;
976
+ }
977
+ console.log('Creating new wallet...');
978
+ const { address } = await createNewWallet();
979
+ console.log('\nāœ… Wallet created!');
980
+ console.log(`\nAddress: ${address}`);
981
+ console.log('\nāš ļø Fund this address with ETH to purchase paid agents.');
982
+ console.log(' Use any exchange or wallet to send ETH to this address.');
983
+ }
984
+ catch (error) {
985
+ console.error(`Error: ${error instanceof Error ? error.message : error}`);
986
+ process.exit(1);
987
+ }
988
+ });
989
+ walletCmd
990
+ .command('balance')
991
+ .description('Show wallet balance')
992
+ .action(async () => {
993
+ try {
994
+ if (!walletExists()) {
995
+ console.log('No wallet configured. Run: agentstore wallet setup');
996
+ process.exit(1);
997
+ }
998
+ const config = loadWalletConfig();
999
+ console.log(`\nAddress: ${config?.address}`);
1000
+ console.log('Fetching balance...');
1001
+ const balance = await getWalletBalance();
1002
+ console.log(`\nšŸ’° Balance: ${balance.eth} ETH (~$${balance.usd})`);
1003
+ // Show spending stats
1004
+ const txHistory = loadTxHistory();
1005
+ const oneDayAgo = Date.now() - 24 * 60 * 60 * 1000;
1006
+ const oneWeekAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
1007
+ const dailySpent = txHistory
1008
+ .filter((tx) => new Date(tx.timestamp).getTime() > oneDayAgo && tx.status === 'confirmed')
1009
+ .reduce((sum, tx) => sum + tx.amountUsd, 0);
1010
+ const weeklySpent = txHistory
1011
+ .filter((tx) => new Date(tx.timestamp).getTime() > oneWeekAgo && tx.status === 'confirmed')
1012
+ .reduce((sum, tx) => sum + tx.amountUsd, 0);
1013
+ if (config?.spendLimits) {
1014
+ console.log(`\nšŸ“Š Spending Limits:`);
1015
+ console.log(` Per transaction: $${config.spendLimits.perTransaction}`);
1016
+ console.log(` Daily: $${dailySpent.toFixed(2)} / $${config.spendLimits.daily}`);
1017
+ console.log(` Weekly: $${weeklySpent.toFixed(2)} / $${config.spendLimits.weekly}`);
1018
+ }
1019
+ }
1020
+ catch (error) {
1021
+ console.error(`Error: ${error instanceof Error ? error.message : error}`);
1022
+ process.exit(1);
1023
+ }
1024
+ });
1025
+ walletCmd
1026
+ .command('history')
1027
+ .description('Show transaction history')
1028
+ .action(() => {
1029
+ try {
1030
+ if (!walletExists()) {
1031
+ console.log('No wallet configured. Run: agentstore wallet setup');
1032
+ process.exit(1);
1033
+ }
1034
+ const txHistory = loadTxHistory();
1035
+ if (txHistory.length === 0) {
1036
+ console.log('No transactions yet.');
1037
+ return;
1038
+ }
1039
+ console.log(`\nšŸ“œ Transaction History (${txHistory.length} transactions)\n`);
1040
+ for (const tx of txHistory.slice(-10).reverse()) {
1041
+ const date = new Date(tx.timestamp).toLocaleDateString();
1042
+ const statusIcon = tx.status === 'confirmed' ? 'āœ“' : tx.status === 'pending' ? 'ā³' : 'āœ—';
1043
+ console.log(` ${statusIcon} ${date} | ${tx.agentId}`);
1044
+ console.log(` ${tx.amountEth} ETH ($${tx.amountUsd}) → ${tx.to.slice(0, 10)}...`);
1045
+ console.log(` ${tx.txHash.slice(0, 20)}...`);
1046
+ console.log();
1047
+ }
1048
+ }
1049
+ catch (error) {
1050
+ console.error(`Error: ${error instanceof Error ? error.message : error}`);
1051
+ process.exit(1);
1052
+ }
1053
+ });
1054
+ walletCmd
1055
+ .command('address')
1056
+ .description('Show wallet address')
1057
+ .action(() => {
1058
+ try {
1059
+ if (!walletExists()) {
1060
+ console.log('No wallet configured. Run: agentstore wallet setup');
1061
+ process.exit(1);
1062
+ }
1063
+ const config = loadWalletConfig();
1064
+ console.log(config?.address);
1065
+ }
1066
+ catch (error) {
1067
+ console.error(`Error: ${error instanceof Error ? error.message : error}`);
1068
+ process.exit(1);
1069
+ }
1070
+ });
1071
+ walletCmd
1072
+ .command('fund')
1073
+ .description('Fund your wallet with a credit card via Coinbase')
1074
+ .option('-a, --amount <usd>', 'Amount in USD to purchase', parseFloat)
1075
+ .option('--no-open', 'Print URL instead of opening browser')
1076
+ .option('--wait', 'Wait and poll for funds to arrive')
1077
+ .action(async (options) => {
1078
+ try {
1079
+ if (!walletExists()) {
1080
+ console.log('No wallet configured. Run: agentstore wallet setup');
1081
+ process.exit(1);
1082
+ }
1083
+ const config = loadWalletConfig();
1084
+ if (!config) {
1085
+ console.log('Failed to load wallet config');
1086
+ process.exit(1);
1087
+ }
1088
+ console.log('\nšŸ’³ Coinbase Onramp - Fund Your Wallet\n');
1089
+ console.log(` Wallet: ${config.address}`);
1090
+ // Get initial balance for comparison
1091
+ let initialBalance;
1092
+ if (options.wait) {
1093
+ try {
1094
+ initialBalance = await publicClient.getBalance({
1095
+ address: config.address,
1096
+ });
1097
+ console.log(` Current balance: ${formatEther(initialBalance)} ETH`);
1098
+ }
1099
+ catch {
1100
+ // Ignore balance fetch errors
1101
+ }
1102
+ }
1103
+ if (options.amount) {
1104
+ console.log(` Amount: $${options.amount} USD`);
1105
+ }
1106
+ console.log('\nšŸ”„ Generating secure onramp session...');
1107
+ // Call API to get onramp URL
1108
+ const response = await fetch(`${API_BASE}/api/onramp/session`, {
1109
+ method: 'POST',
1110
+ headers: { 'Content-Type': 'application/json' },
1111
+ body: JSON.stringify({
1112
+ wallet_address: config.address,
1113
+ amount_usd: options.amount,
1114
+ }),
1115
+ });
1116
+ const result = await response.json();
1117
+ if (!response.ok || !result.success) {
1118
+ // Handle fallback for when CDP credentials aren't configured
1119
+ if (result.manual_instructions) {
1120
+ console.log('\nāš ļø Coinbase Onramp not configured on server.\n');
1121
+ console.log(' Manual funding instructions:');
1122
+ console.log(` 1. ${result.manual_instructions.step1}`);
1123
+ console.log(` 2. ${result.manual_instructions.step2}`);
1124
+ console.log(` 3. ${result.manual_instructions.step3}`);
1125
+ console.log(`\n Your wallet address: ${config.address}`);
1126
+ }
1127
+ else {
1128
+ console.log(`\nāŒ Error: ${result.error || result.message || 'Unknown error'}`);
1129
+ if (result.fallback) {
1130
+ console.log(`\n ${result.fallback.message}`);
1131
+ console.log(` 1. ${result.fallback.step1}`);
1132
+ console.log(` 2. ${result.fallback.step2}`);
1133
+ }
1134
+ }
1135
+ process.exit(1);
1136
+ }
1137
+ const onrampUrl = result.onramp_url;
1138
+ if (options.open === false) {
1139
+ // Just print the URL
1140
+ console.log('\nāœ… Onramp URL generated:\n');
1141
+ console.log(` ${onrampUrl}\n`);
1142
+ console.log(' Open this URL in your browser to complete the purchase.');
1143
+ }
1144
+ else {
1145
+ // Open in default browser
1146
+ console.log('\n🌐 Opening Coinbase in your browser...\n');
1147
+ const { exec } = await import('child_process');
1148
+ const openCmd = process.platform === 'darwin'
1149
+ ? `open "${onrampUrl}"`
1150
+ : process.platform === 'win32'
1151
+ ? `start "${onrampUrl}"`
1152
+ : `xdg-open "${onrampUrl}"`;
1153
+ exec(openCmd, (error) => {
1154
+ if (error) {
1155
+ console.log('Could not open browser. Please open this URL manually:\n');
1156
+ console.log(` ${onrampUrl}\n`);
1157
+ }
1158
+ });
1159
+ console.log(' Complete the purchase in your browser.');
1160
+ console.log(' ETH will be sent to your wallet within a few minutes.\n');
1161
+ }
1162
+ // Poll for balance changes if --wait flag is set
1163
+ if (options.wait && initialBalance !== undefined) {
1164
+ console.log('ā³ Waiting for funds to arrive (Ctrl+C to cancel)...\n');
1165
+ const startTime = Date.now();
1166
+ const maxWaitTime = 10 * 60 * 1000; // 10 minutes
1167
+ const pollInterval = 15 * 1000; // 15 seconds
1168
+ while (Date.now() - startTime < maxWaitTime) {
1169
+ await new Promise((resolve) => setTimeout(resolve, pollInterval));
1170
+ try {
1171
+ const currentBalance = await publicClient.getBalance({
1172
+ address: config.address,
1173
+ });
1174
+ if (currentBalance > initialBalance) {
1175
+ const added = currentBalance - initialBalance;
1176
+ const ethPrice = await getEthPrice();
1177
+ const addedUsd = parseFloat(formatEther(added)) * ethPrice;
1178
+ console.log('āœ… Funds received!\n');
1179
+ console.log(` Added: ${formatEther(added)} ETH (~$${addedUsd.toFixed(2)})`);
1180
+ console.log(` New balance: ${formatEther(currentBalance)} ETH`);
1181
+ process.exit(0);
1182
+ }
1183
+ const elapsed = Math.floor((Date.now() - startTime) / 1000);
1184
+ process.stdout.write(`\r Checking... (${elapsed}s elapsed)`);
1185
+ }
1186
+ catch {
1187
+ // Ignore individual poll errors
1188
+ }
1189
+ }
1190
+ console.log('\n\nāš ļø Timed out waiting for funds.');
1191
+ console.log(' Funds may still arrive - check your balance later with:');
1192
+ console.log(' agentstore wallet balance\n');
1193
+ }
1194
+ }
1195
+ catch (error) {
1196
+ console.error(`Error: ${error instanceof Error ? error.message : error}`);
1197
+ process.exit(1);
1198
+ }
1199
+ });
1200
+ // Publisher commands
1201
+ const publisherCmd = program
1202
+ .command('publisher')
1203
+ .description('Manage your publisher account');
1204
+ publisherCmd
1205
+ .command('register')
1206
+ .description('Register as a publisher on AgentStore')
1207
+ .requiredOption('-n, --name <name>', 'Publisher name (lowercase, alphanumeric with hyphens, used as unique ID)')
1208
+ .requiredOption('-d, --display-name <display_name>', 'Display name for your publisher account')
1209
+ .option('-e, --email <email>', 'Contact email (optional)')
1210
+ .option('-u, --support-url <url>', 'Support URL for your agents')
1211
+ .action(async (options) => {
1212
+ try {
1213
+ if (!walletExists()) {
1214
+ console.log('No wallet configured. Run: agentstore wallet setup');
1215
+ process.exit(1);
1216
+ }
1217
+ const config = loadWalletConfig();
1218
+ if (!config) {
1219
+ console.log('Failed to load wallet config');
1220
+ process.exit(1);
1221
+ }
1222
+ // Validate name format
1223
+ if (!/^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$/.test(options.name) || options.name.length < 2) {
1224
+ console.log('Invalid publisher name. Must be:');
1225
+ console.log(' - At least 2 characters');
1226
+ console.log(' - Lowercase letters, numbers, and hyphens only');
1227
+ console.log(' - Start and end with a letter or number');
1228
+ process.exit(1);
1229
+ }
1230
+ console.log('\nšŸ“ Registering as publisher...');
1231
+ console.log(` Name: ${options.name}`);
1232
+ console.log(` Display Name: ${options.displayName}`);
1233
+ console.log(` Payout Address: ${config.address}`);
1234
+ if (options.email) {
1235
+ console.log(` Email: ${options.email}`);
1236
+ }
1237
+ if (options.supportUrl) {
1238
+ console.log(` Support URL: ${options.supportUrl}`);
1239
+ }
1240
+ // Submit registration
1241
+ console.log('\nšŸ“¤ Submitting registration...');
1242
+ const response = await fetch(`${API_BASE}/api/publishers`, {
1243
+ method: 'POST',
1244
+ headers: { 'Content-Type': 'application/json' },
1245
+ body: JSON.stringify({
1246
+ name: options.name,
1247
+ display_name: options.displayName,
1248
+ payout_address: config.address,
1249
+ email: options.email || undefined,
1250
+ support_url: options.supportUrl || undefined,
1251
+ }),
1252
+ });
1253
+ const result = await response.json();
1254
+ if (!response.ok) {
1255
+ console.log(`\nāŒ Registration failed: ${result.error || response.statusText}`);
1256
+ process.exit(1);
1257
+ }
1258
+ console.log('\nāœ… Publisher registered successfully!');
1259
+ console.log(`\n Your publisher name: ${result.publisher?.publisher_id || options.name}`);
1260
+ console.log('\n Next steps:');
1261
+ console.log(' 1. Create an agent manifest: agentstore publisher init');
1262
+ console.log(' 2. Submit your agent: agentstore publisher submit <manifest.json>');
1263
+ }
1264
+ catch (error) {
1265
+ console.error(`Error: ${error instanceof Error ? error.message : error}`);
1266
+ process.exit(1);
1267
+ }
1268
+ });
1269
+ publisherCmd
1270
+ .command('info')
1271
+ .description('Show your publisher info')
1272
+ .action(async () => {
1273
+ try {
1274
+ if (!walletExists()) {
1275
+ console.log('No wallet configured. Run: agentstore wallet setup');
1276
+ process.exit(1);
1277
+ }
1278
+ const config = loadWalletConfig();
1279
+ if (!config) {
1280
+ console.log('Failed to load wallet config');
1281
+ process.exit(1);
1282
+ }
1283
+ // Look up publisher by payout address
1284
+ const response = await fetch(`${API_BASE}/api/publishers`);
1285
+ if (!response.ok) {
1286
+ console.log('Failed to fetch publishers');
1287
+ process.exit(1);
1288
+ }
1289
+ const data = await response.json();
1290
+ // Note: We can't directly query by payout_address via the public API
1291
+ // For now, list all publishers owned by this wallet would require a new endpoint
1292
+ // Just show the wallet address and instruct to check dashboard
1293
+ console.log('\nšŸ“‹ Publisher Account');
1294
+ console.log(` Wallet: ${config.address}`);
1295
+ console.log('\n To see your published agents, visit the AgentStore dashboard');
1296
+ console.log(' or use: agentstore browse --search <your-publisher-id>');
1297
+ }
1298
+ catch (error) {
1299
+ console.error(`Error: ${error instanceof Error ? error.message : error}`);
1300
+ process.exit(1);
1301
+ }
1302
+ });
1303
+ publisherCmd
1304
+ .command('submit <manifest>')
1305
+ .description('Submit an agent to AgentStore')
1306
+ .option('--publish', 'Request immediate publication (requires approval)')
1307
+ .action(async (manifestPath, options) => {
1308
+ try {
1309
+ if (!walletExists()) {
1310
+ console.log('No wallet configured. Run: agentstore wallet setup');
1311
+ process.exit(1);
1312
+ }
1313
+ const config = loadWalletConfig();
1314
+ if (!config) {
1315
+ console.log('Failed to load wallet config');
1316
+ process.exit(1);
1317
+ }
1318
+ const privateKey = await loadPrivateKey();
1319
+ if (!privateKey) {
1320
+ console.log('Failed to load wallet private key');
1321
+ process.exit(1);
1322
+ }
1323
+ // Read and parse manifest file
1324
+ const fullPath = path.resolve(manifestPath);
1325
+ if (!fs.existsSync(fullPath)) {
1326
+ console.log(`Manifest file not found: ${fullPath}`);
1327
+ process.exit(1);
1328
+ }
1329
+ let manifest;
1330
+ try {
1331
+ manifest = JSON.parse(fs.readFileSync(fullPath, 'utf-8'));
1332
+ }
1333
+ catch (e) {
1334
+ console.log(`Invalid JSON in manifest: ${e instanceof Error ? e.message : e}`);
1335
+ process.exit(1);
1336
+ }
1337
+ // Validate required fields
1338
+ const required = ['agent_id', 'name', 'type', 'description', 'version', 'pricing', 'install'];
1339
+ for (const field of required) {
1340
+ if (!manifest[field]) {
1341
+ console.log(`Missing required field: ${field}`);
1342
+ process.exit(1);
1343
+ }
1344
+ }
1345
+ const agentId = manifest.agent_id;
1346
+ const version = manifest.version;
1347
+ console.log('\nšŸ“¦ Submitting Agent to AgentStore\n');
1348
+ console.log(` Agent ID: ${agentId}`);
1349
+ console.log(` Name: ${manifest.name}`);
1350
+ console.log(` Version: ${version}`);
1351
+ console.log(` Type: ${manifest.type}`);
1352
+ const pricing = manifest.pricing;
1353
+ console.log(` Pricing: ${pricing.model === 'free' ? 'Free' : `$${pricing.amount || 0}`}`);
1354
+ // Create and sign the submission message
1355
+ const message = `Submit agent to AgentStore: ${agentId} v${version}`;
1356
+ const account = privateKeyToAccount(privateKey);
1357
+ console.log('\nšŸ” Signing submission...');
1358
+ const signature = await account.signMessage({ message });
1359
+ // Submit to API
1360
+ console.log('šŸ“¤ Uploading to marketplace...');
1361
+ const response = await fetch(`${API_BASE}/api/publishers/agents`, {
1362
+ method: 'POST',
1363
+ headers: { 'Content-Type': 'application/json' },
1364
+ body: JSON.stringify({
1365
+ ...manifest,
1366
+ tags: manifest.tags || [],
1367
+ permissions: manifest.permissions || { requires_network: false, requires_filesystem: false },
1368
+ signature,
1369
+ message,
1370
+ }),
1371
+ });
1372
+ const result = await response.json();
1373
+ if (!response.ok) {
1374
+ console.log(`\nāŒ Submission failed: ${result.error || response.statusText}`);
1375
+ if (result.details) {
1376
+ console.log(' Details:', JSON.stringify(result.details, null, 2));
1377
+ }
1378
+ process.exit(1);
1379
+ }
1380
+ console.log(`\nāœ… Agent ${result.action === 'updated' ? 'updated' : 'published'} successfully!`);
1381
+ console.log(`\n Agent ID: ${result.agent?.agent_id || agentId}`);
1382
+ console.log(` Version: ${result.agent?.version || version}`);
1383
+ if (result.action === 'created') {
1384
+ console.log('\n šŸŽ‰ Your agent is now live in the marketplace!');
1385
+ console.log(' Users can install it with:');
1386
+ console.log(` agentstore install ${result.agent?.agent_id || agentId}`);
1387
+ }
1388
+ else {
1389
+ console.log('\n Your agent has been updated in the marketplace.');
1390
+ }
1391
+ }
1392
+ catch (error) {
1393
+ console.error(`Error: ${error instanceof Error ? error.message : error}`);
1394
+ process.exit(1);
1395
+ }
1396
+ });
1397
+ publisherCmd
1398
+ .command('init')
1399
+ .description('Create a sample agent manifest file')
1400
+ .option('-o, --output <file>', 'Output file path', 'agent-manifest.json')
1401
+ .action((options) => {
1402
+ const sampleManifest = {
1403
+ agent_id: 'your-publisher-name.your-agent-name',
1404
+ name: 'Your Agent Name',
1405
+ type: 'open',
1406
+ description: 'A brief description of what your agent does (10-1000 characters)',
1407
+ version: '1.0.0',
1408
+ pricing: {
1409
+ model: 'free',
1410
+ amount: 0,
1411
+ currency: 'USD',
1412
+ },
1413
+ install: {
1414
+ agent_wrapper: {
1415
+ format: 'markdown',
1416
+ entrypoint: 'agent.md',
1417
+ },
1418
+ gateway_routes: [
1419
+ {
1420
+ route_id: 'default',
1421
+ mcp_endpoint: 'https://your-mcp-server.com/endpoint',
1422
+ tools: [
1423
+ {
1424
+ name: 'example_tool',
1425
+ description: 'What this tool does',
1426
+ inputSchema: {
1427
+ type: 'object',
1428
+ properties: {
1429
+ param1: {
1430
+ type: 'string',
1431
+ description: 'Description of param1',
1432
+ },
1433
+ },
1434
+ required: ['param1'],
1435
+ },
1436
+ },
1437
+ ],
1438
+ auth: {
1439
+ type: 'none',
1440
+ },
1441
+ },
1442
+ ],
1443
+ },
1444
+ permissions: {
1445
+ requires_network: true,
1446
+ requires_filesystem: false,
1447
+ notes: 'Optional notes about permissions',
1448
+ },
1449
+ tags: ['Productivity', 'Data'],
1450
+ };
1451
+ const outputPath = path.resolve(options.output);
1452
+ fs.writeFileSync(outputPath, JSON.stringify(sampleManifest, null, 2));
1453
+ console.log(`\nāœ… Sample manifest created: ${outputPath}`);
1454
+ console.log('\nNext steps:');
1455
+ console.log('1. Edit the manifest with your agent details');
1456
+ console.log('2. Update agent_id to: your-publisher-name.agent-name');
1457
+ console.log('3. Submit with: agentstore publisher submit ' + options.output);
1458
+ });
1459
+ program.parse();