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/README.md +96 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1459 -0
- package/package.json +62 -0
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();
|