x402-bazaar 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.js +6 -1
- package/package.json +1 -1
- package/src/commands/config.js +54 -42
- package/src/commands/init.js +120 -380
- package/src/generators/env-file.js +15 -6
- package/src/generators/mcp-config.js +7 -29
- package/src/utils/prompt.js +107 -0
package/bin/cli.js
CHANGED
|
@@ -21,7 +21,7 @@ const program = new Command();
|
|
|
21
21
|
program
|
|
22
22
|
.name('x402-bazaar')
|
|
23
23
|
.description(chalk.hex('#FF9900')('x402 Bazaar') + ' — Connect your AI agent to the marketplace in one command')
|
|
24
|
-
.version('1.
|
|
24
|
+
.version('1.2.0');
|
|
25
25
|
|
|
26
26
|
program
|
|
27
27
|
.command('init')
|
|
@@ -29,6 +29,8 @@ program
|
|
|
29
29
|
.option('--env <environment>', 'Force environment (claude-desktop, cursor, claude-code, vscode-continue, generic)')
|
|
30
30
|
.option('--no-wallet', 'Skip wallet configuration (read-only mode)')
|
|
31
31
|
.option('--server-url <url>', 'Custom server URL', 'https://x402-api.onrender.com')
|
|
32
|
+
.option('--network <network>', 'Network: mainnet or testnet', 'mainnet')
|
|
33
|
+
.option('--budget <amount>', 'Max USDC budget per session', '1.00')
|
|
32
34
|
.action(initCommand);
|
|
33
35
|
|
|
34
36
|
program
|
|
@@ -36,6 +38,9 @@ program
|
|
|
36
38
|
.description('Generate MCP configuration file for your environment')
|
|
37
39
|
.option('--env <environment>', 'Target environment (claude-desktop, cursor, claude-code, vscode-continue, generic)')
|
|
38
40
|
.option('--output <path>', 'Output file path')
|
|
41
|
+
.option('--server-url <url>', 'Custom server URL', 'https://x402-api.onrender.com')
|
|
42
|
+
.option('--network <network>', 'Network: mainnet or testnet', 'mainnet')
|
|
43
|
+
.option('--budget <amount>', 'Max USDC budget per session', '1.00')
|
|
39
44
|
.action(configCommand);
|
|
40
45
|
|
|
41
46
|
program
|
package/package.json
CHANGED
package/src/commands/config.js
CHANGED
|
@@ -2,6 +2,7 @@ import inquirer from 'inquirer';
|
|
|
2
2
|
import { writeFileSync, existsSync, readFileSync } from 'fs';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import { log } from '../utils/logger.js';
|
|
5
|
+
import { isInteractive, promptOrDefault, printNonInteractiveHint } from '../utils/prompt.js';
|
|
5
6
|
import { detectEnvironment, getDefaultInstallDir } from '../detectors/environment.js';
|
|
6
7
|
import { generateMcpConfig } from '../generators/mcp-config.js';
|
|
7
8
|
|
|
@@ -17,6 +18,8 @@ export async function configCommand(options) {
|
|
|
17
18
|
const environments = detectEnvironment();
|
|
18
19
|
const detected = environments.filter(e => e.detected);
|
|
19
20
|
|
|
21
|
+
const defaultEnv = detected.length > 0 ? detected[0].name : 'claude-desktop';
|
|
22
|
+
|
|
20
23
|
const choices = [
|
|
21
24
|
...environments.map(e => ({
|
|
22
25
|
name: `${e.label}${e.detected ? chalk.hex('#34D399')(' (detected)') : ''}`,
|
|
@@ -25,20 +28,23 @@ export async function configCommand(options) {
|
|
|
25
28
|
{ name: 'Generic (print JSON to stdout)', value: 'generic' },
|
|
26
29
|
];
|
|
27
30
|
|
|
28
|
-
const { env } = await
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
},
|
|
36
|
-
]);
|
|
31
|
+
const { env } = await promptOrDefault([{
|
|
32
|
+
type: 'list',
|
|
33
|
+
name: 'env',
|
|
34
|
+
message: 'Which environment do you want to configure?',
|
|
35
|
+
choices,
|
|
36
|
+
default: defaultEnv,
|
|
37
|
+
}]);
|
|
37
38
|
targetEnv = env;
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
//
|
|
41
|
-
const
|
|
41
|
+
// Config values — use CLI flags as overrides
|
|
42
|
+
const configOverrides = {};
|
|
43
|
+
if (options.serverUrl) configOverrides.serverUrl = options.serverUrl;
|
|
44
|
+
if (options.budget) configOverrides.maxBudget = options.budget;
|
|
45
|
+
if (options.network) configOverrides.network = options.network;
|
|
46
|
+
|
|
47
|
+
const { serverUrl, maxBudget, network } = await promptOrDefault([
|
|
42
48
|
{
|
|
43
49
|
type: 'input',
|
|
44
50
|
name: 'serverUrl',
|
|
@@ -61,42 +67,46 @@ export async function configCommand(options) {
|
|
|
61
67
|
],
|
|
62
68
|
default: 'mainnet',
|
|
63
69
|
},
|
|
64
|
-
]);
|
|
70
|
+
], configOverrides);
|
|
65
71
|
|
|
66
|
-
//
|
|
67
|
-
const { walletMode } = await
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
]);
|
|
72
|
+
// Wallet config
|
|
73
|
+
const { walletMode } = await promptOrDefault([{
|
|
74
|
+
type: 'list',
|
|
75
|
+
name: 'walletMode',
|
|
76
|
+
message: 'Wallet configuration:',
|
|
77
|
+
choices: [
|
|
78
|
+
{ name: 'I have Coinbase API keys', value: 'existing' },
|
|
79
|
+
{ name: 'Read-only mode (browse only, no payments)', value: 'readonly' },
|
|
80
|
+
],
|
|
81
|
+
default: 'readonly',
|
|
82
|
+
}]);
|
|
78
83
|
|
|
79
84
|
let coinbaseApiKey = '';
|
|
80
85
|
let coinbaseApiSecret = '';
|
|
81
86
|
|
|
82
87
|
if (walletMode === 'existing') {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
88
|
+
if (!isInteractive()) {
|
|
89
|
+
log.warn('Cannot enter API credentials in non-interactive mode.');
|
|
90
|
+
log.info('Config will be generated in read-only mode.');
|
|
91
|
+
} else {
|
|
92
|
+
const walletAnswers = await promptOrDefault([
|
|
93
|
+
{
|
|
94
|
+
type: 'input',
|
|
95
|
+
name: 'coinbaseApiKey',
|
|
96
|
+
message: 'Coinbase API Key:',
|
|
97
|
+
validate: (v) => v.length > 0 || 'API key is required',
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
type: 'password',
|
|
101
|
+
name: 'coinbaseApiSecret',
|
|
102
|
+
message: 'Coinbase API Secret:',
|
|
103
|
+
mask: '*',
|
|
104
|
+
validate: (v) => v.length > 0 || 'API secret is required',
|
|
105
|
+
},
|
|
106
|
+
]);
|
|
107
|
+
coinbaseApiKey = walletAnswers.coinbaseApiKey;
|
|
108
|
+
coinbaseApiSecret = walletAnswers.coinbaseApiSecret;
|
|
109
|
+
}
|
|
100
110
|
}
|
|
101
111
|
|
|
102
112
|
const installDir = getDefaultInstallDir();
|
|
@@ -110,7 +120,7 @@ export async function configCommand(options) {
|
|
|
110
120
|
network,
|
|
111
121
|
coinbaseApiKey,
|
|
112
122
|
coinbaseApiSecret,
|
|
113
|
-
readOnly: walletMode === 'readonly',
|
|
123
|
+
readOnly: walletMode === 'readonly' || (!isInteractive() && walletMode === 'existing'),
|
|
114
124
|
});
|
|
115
125
|
|
|
116
126
|
log.separator();
|
|
@@ -134,4 +144,6 @@ export async function configCommand(options) {
|
|
|
134
144
|
}
|
|
135
145
|
|
|
136
146
|
console.log('');
|
|
147
|
+
|
|
148
|
+
printNonInteractiveHint('config');
|
|
137
149
|
}
|
package/src/commands/init.js
CHANGED
|
@@ -4,8 +4,10 @@ import chalk from 'chalk';
|
|
|
4
4
|
import { existsSync, mkdirSync, writeFileSync, readFileSync, copyFileSync } from 'fs';
|
|
5
5
|
import { join, dirname, resolve } from 'path';
|
|
6
6
|
import { execSync } from 'child_process';
|
|
7
|
+
import { randomBytes } from 'crypto';
|
|
7
8
|
import { fileURLToPath } from 'url';
|
|
8
9
|
import { log } from '../utils/logger.js';
|
|
10
|
+
import { isInteractive, promptOrDefault, printNonInteractiveHint } from '../utils/prompt.js';
|
|
9
11
|
import { detectEnvironment, getOsLabel, getDefaultInstallDir } from '../detectors/environment.js';
|
|
10
12
|
import { generateMcpConfig } from '../generators/mcp-config.js';
|
|
11
13
|
import { generateEnvContent } from '../generators/env-file.js';
|
|
@@ -39,6 +41,10 @@ export async function initCommand(options) {
|
|
|
39
41
|
|
|
40
42
|
log.banner();
|
|
41
43
|
|
|
44
|
+
if (!isInteractive()) {
|
|
45
|
+
log.info(chalk.yellow('Non-interactive terminal detected. Using smart defaults.'));
|
|
46
|
+
}
|
|
47
|
+
|
|
42
48
|
const osLabel = getOsLabel();
|
|
43
49
|
log.info(`OS: ${chalk.bold(osLabel)} | Node: ${chalk.bold(process.versions.node)}`);
|
|
44
50
|
log.separator();
|
|
@@ -74,21 +80,23 @@ export async function initCommand(options) {
|
|
|
74
80
|
log.warn('No AI client detected automatically.');
|
|
75
81
|
}
|
|
76
82
|
|
|
83
|
+
const defaultEnv = detected.length > 0 ? detected[0].name : 'claude-desktop';
|
|
84
|
+
|
|
77
85
|
const choices = [
|
|
78
86
|
...environments.map(e => ({
|
|
79
87
|
name: `${e.label}${e.detected ? chalk.hex('#34D399')(' (detected)') : ''}`,
|
|
80
88
|
value: e.name,
|
|
81
89
|
})),
|
|
82
|
-
new inquirer.Separator(),
|
|
90
|
+
...(isInteractive() ? [new inquirer.Separator()] : []),
|
|
83
91
|
{ name: 'Generic (I\'ll configure manually)', value: 'generic' },
|
|
84
92
|
];
|
|
85
93
|
|
|
86
|
-
const { env } = await
|
|
94
|
+
const { env } = await promptOrDefault([{
|
|
87
95
|
type: 'list',
|
|
88
96
|
name: 'env',
|
|
89
97
|
message: 'Which AI client are you using?',
|
|
90
98
|
choices,
|
|
91
|
-
default:
|
|
99
|
+
default: defaultEnv,
|
|
92
100
|
}]);
|
|
93
101
|
|
|
94
102
|
targetEnv = environments.find(e => e.name === env) || {
|
|
@@ -119,8 +127,18 @@ export async function initCommand(options) {
|
|
|
119
127
|
copyFileSync(mcpSource, mcpServerDest);
|
|
120
128
|
spinner.text = 'Copied mcp-server.mjs from local project...';
|
|
121
129
|
} else {
|
|
122
|
-
spinner.text = '
|
|
123
|
-
|
|
130
|
+
spinner.text = 'Downloading MCP server from GitHub...';
|
|
131
|
+
try {
|
|
132
|
+
const dlRes = await fetch('https://raw.githubusercontent.com/Wintyx57/x402-backend/main/mcp-server.mjs');
|
|
133
|
+
if (!dlRes.ok) throw new Error(`HTTP ${dlRes.status}`);
|
|
134
|
+
writeFileSync(mcpServerDest, await dlRes.text());
|
|
135
|
+
} catch (dlErr) {
|
|
136
|
+
spinner.fail(`Could not download MCP server: ${dlErr.message}`);
|
|
137
|
+
log.error('Download the file manually from:');
|
|
138
|
+
log.dim(' https://github.com/Wintyx57/x402-backend/blob/main/mcp-server.mjs');
|
|
139
|
+
log.dim(` Save it to: ${mcpServerDest}`);
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
124
142
|
}
|
|
125
143
|
|
|
126
144
|
// Create package.json for the MCP server runtime
|
|
@@ -128,14 +146,16 @@ export async function initCommand(options) {
|
|
|
128
146
|
if (!existsSync(pkgJsonPath)) {
|
|
129
147
|
writeFileSync(pkgJsonPath, JSON.stringify({
|
|
130
148
|
name: 'x402-bazaar-mcp',
|
|
131
|
-
version: '
|
|
149
|
+
version: '2.0.0',
|
|
132
150
|
type: 'module',
|
|
133
151
|
private: true,
|
|
134
152
|
dependencies: {
|
|
135
|
-
'
|
|
153
|
+
'viem': '^2.0.0',
|
|
136
154
|
'@modelcontextprotocol/sdk': '^1.26.0',
|
|
137
155
|
'dotenv': '^17.2.4',
|
|
138
156
|
'zod': '^4.3.6',
|
|
157
|
+
'ed2curve': '^0.3.0',
|
|
158
|
+
'@scure/bip32': '^1.6.0',
|
|
139
159
|
},
|
|
140
160
|
}, null, 2));
|
|
141
161
|
}
|
|
@@ -176,9 +196,9 @@ export async function initCommand(options) {
|
|
|
176
196
|
console.log('');
|
|
177
197
|
|
|
178
198
|
let walletMode = 'readonly';
|
|
199
|
+
let agentPrivateKey = '';
|
|
179
200
|
let coinbaseApiKey = '';
|
|
180
201
|
let coinbaseApiSecret = '';
|
|
181
|
-
let seedPath = '';
|
|
182
202
|
let maxBudget = '1.00';
|
|
183
203
|
let network = 'mainnet';
|
|
184
204
|
let serverUrl = options.serverUrl || 'https://x402-api.onrender.com';
|
|
@@ -187,83 +207,118 @@ export async function initCommand(options) {
|
|
|
187
207
|
log.info('Skipping wallet setup (--no-wallet)');
|
|
188
208
|
walletMode = 'readonly';
|
|
189
209
|
} else {
|
|
190
|
-
const { mode } = await
|
|
210
|
+
const { mode } = await promptOrDefault([{
|
|
191
211
|
type: 'list',
|
|
192
212
|
name: 'mode',
|
|
193
213
|
message: 'How do you want to configure payments?',
|
|
194
214
|
choices: [
|
|
195
215
|
{
|
|
196
|
-
name: `${chalk.bold('
|
|
197
|
-
value: '
|
|
216
|
+
name: `${chalk.bold('Generate a new wallet')} — Creates a fresh Ethereum wallet automatically (Recommended)`,
|
|
217
|
+
value: 'generate',
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
name: `${chalk.bold('Import private key')} — Use an existing Ethereum private key`,
|
|
221
|
+
value: 'import',
|
|
198
222
|
},
|
|
199
223
|
{
|
|
200
|
-
name: `${chalk.bold('
|
|
201
|
-
value: '
|
|
224
|
+
name: `${chalk.bold('Coinbase API keys')} — Legacy: use Coinbase CDP seed file`,
|
|
225
|
+
value: 'coinbase',
|
|
202
226
|
},
|
|
203
227
|
{
|
|
204
228
|
name: `${chalk.bold('Read-only mode')} — Browse marketplace for free (no payments)`,
|
|
205
229
|
value: 'readonly',
|
|
206
230
|
},
|
|
207
231
|
],
|
|
232
|
+
default: 'generate',
|
|
208
233
|
}]);
|
|
209
234
|
|
|
210
235
|
walletMode = mode;
|
|
211
236
|
|
|
212
|
-
if (mode === '
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
237
|
+
if (mode === 'generate') {
|
|
238
|
+
agentPrivateKey = '0x' + randomBytes(32).toString('hex');
|
|
239
|
+
log.success('New wallet generated!');
|
|
240
|
+
|
|
241
|
+
// Derive address using viem (installed in step 2)
|
|
242
|
+
try {
|
|
243
|
+
const addr = execSync(
|
|
244
|
+
`node --input-type=module -e "import{privateKeyToAccount}from'viem/accounts';console.log(privateKeyToAccount('${agentPrivateKey}').address)"`,
|
|
245
|
+
{ cwd: installDir, stdio: 'pipe', timeout: 15000 }
|
|
246
|
+
).toString().trim();
|
|
247
|
+
log.info(`Wallet address: ${chalk.bold(addr)}`);
|
|
248
|
+
console.log('');
|
|
249
|
+
log.dim(' Fund this address with USDC on Base to start paying for APIs.');
|
|
250
|
+
log.dim(` View on BaseScan: https://basescan.org/address/${addr}`);
|
|
251
|
+
} catch {
|
|
252
|
+
log.info('Wallet address will be shown when you first use the MCP server.');
|
|
253
|
+
log.dim(' Use the get_wallet_balance tool to see your address.');
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (mode === 'import') {
|
|
258
|
+
if (!isInteractive()) {
|
|
259
|
+
log.warn('Cannot enter private key in a non-interactive terminal.');
|
|
260
|
+
log.info('Set AGENT_PRIVATE_KEY in your .env file manually after setup.');
|
|
261
|
+
walletMode = 'readonly';
|
|
262
|
+
} else {
|
|
263
|
+
const { key } = await promptOrDefault([{
|
|
221
264
|
type: 'password',
|
|
222
|
-
name: '
|
|
223
|
-
message: '
|
|
265
|
+
name: 'key',
|
|
266
|
+
message: 'Ethereum private key (0x...):',
|
|
224
267
|
mask: '*',
|
|
225
|
-
validate: (v) =>
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
log.
|
|
235
|
-
log.dim(' Run: cd "' + installDir + '" && node -e "const{Coinbase,Wallet}=require(\'@coinbase/coinbase-sdk\');...');
|
|
236
|
-
log.dim(' Or copy your existing agent-seed.json to: ' + installDir);
|
|
268
|
+
validate: (v) => {
|
|
269
|
+
const trimmed = v.trim();
|
|
270
|
+
if (trimmed.length === 0) return 'Private key is required';
|
|
271
|
+
const hex = trimmed.startsWith('0x') ? trimmed.slice(2) : trimmed;
|
|
272
|
+
if (!/^[0-9a-fA-F]{64}$/.test(hex)) return 'Invalid private key (expected 64 hex characters)';
|
|
273
|
+
return true;
|
|
274
|
+
},
|
|
275
|
+
}]);
|
|
276
|
+
agentPrivateKey = key.trim().startsWith('0x') ? key.trim() : `0x${key.trim()}`;
|
|
277
|
+
log.success('Private key imported.');
|
|
237
278
|
}
|
|
238
279
|
}
|
|
239
280
|
|
|
240
|
-
if (mode === '
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
281
|
+
if (mode === 'coinbase') {
|
|
282
|
+
if (!isInteractive()) {
|
|
283
|
+
log.warn('Cannot enter API credentials in a non-interactive terminal.');
|
|
284
|
+
log.info('Falling back to read-only mode.');
|
|
285
|
+
walletMode = 'readonly';
|
|
286
|
+
} else {
|
|
287
|
+
log.dim(' Legacy mode: requires Coinbase CDP API keys + agent-seed.json');
|
|
288
|
+
console.log('');
|
|
289
|
+
const walletAnswers = await promptOrDefault([
|
|
290
|
+
{
|
|
291
|
+
type: 'input',
|
|
292
|
+
name: 'coinbaseApiKey',
|
|
293
|
+
message: 'Coinbase API Key (from portal.cdp.coinbase.com):',
|
|
294
|
+
validate: (v) => v.trim().length > 0 || 'API key is required',
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
type: 'password',
|
|
298
|
+
name: 'coinbaseApiSecret',
|
|
299
|
+
message: 'Coinbase API Secret:',
|
|
300
|
+
mask: '*',
|
|
301
|
+
validate: (v) => v.trim().length > 0 || 'API secret is required',
|
|
302
|
+
},
|
|
303
|
+
]);
|
|
304
|
+
coinbaseApiKey = walletAnswers.coinbaseApiKey.trim();
|
|
305
|
+
coinbaseApiSecret = walletAnswers.coinbaseApiSecret.trim();
|
|
306
|
+
|
|
307
|
+
const existingSeed = join(installDir, 'agent-seed.json');
|
|
308
|
+
if (!existsSync(existingSeed)) {
|
|
309
|
+
log.warn('No agent-seed.json found.');
|
|
310
|
+
log.dim(' Copy your existing agent-seed.json to: ' + installDir);
|
|
311
|
+
log.dim(' Or re-run init and choose "Generate a new wallet" instead.');
|
|
312
|
+
}
|
|
261
313
|
}
|
|
262
|
-
walletMode = 'readonly';
|
|
263
314
|
}
|
|
264
315
|
|
|
265
|
-
// Network & Budget
|
|
266
|
-
const
|
|
316
|
+
// Network & Budget — use CLI flags as overrides
|
|
317
|
+
const configOverrides = {};
|
|
318
|
+
if (options.network) configOverrides.network = options.network;
|
|
319
|
+
if (options.budget) configOverrides.maxBudget = options.budget;
|
|
320
|
+
|
|
321
|
+
const configAnswers = await promptOrDefault([
|
|
267
322
|
{
|
|
268
323
|
type: 'list',
|
|
269
324
|
name: 'network',
|
|
@@ -286,11 +341,10 @@ export async function initCommand(options) {
|
|
|
286
341
|
return true;
|
|
287
342
|
},
|
|
288
343
|
},
|
|
289
|
-
]);
|
|
344
|
+
], configOverrides);
|
|
290
345
|
|
|
291
346
|
network = configAnswers.network;
|
|
292
347
|
maxBudget = configAnswers.maxBudget;
|
|
293
|
-
seedPath = join(installDir, 'agent-seed.json');
|
|
294
348
|
}
|
|
295
349
|
|
|
296
350
|
console.log('');
|
|
@@ -305,9 +359,9 @@ export async function initCommand(options) {
|
|
|
305
359
|
serverUrl,
|
|
306
360
|
maxBudget,
|
|
307
361
|
network,
|
|
362
|
+
agentPrivateKey,
|
|
308
363
|
coinbaseApiKey,
|
|
309
364
|
coinbaseApiSecret,
|
|
310
|
-
seedPath,
|
|
311
365
|
readOnly: walletMode === 'readonly',
|
|
312
366
|
});
|
|
313
367
|
|
|
@@ -317,9 +371,9 @@ export async function initCommand(options) {
|
|
|
317
371
|
serverUrl,
|
|
318
372
|
maxBudget,
|
|
319
373
|
network,
|
|
374
|
+
agentPrivateKey,
|
|
320
375
|
coinbaseApiKey,
|
|
321
376
|
coinbaseApiSecret,
|
|
322
|
-
seedPath,
|
|
323
377
|
});
|
|
324
378
|
const envPath = join(installDir, '.env');
|
|
325
379
|
writeFileSync(envPath, envContent);
|
|
@@ -407,6 +461,8 @@ export async function initCommand(options) {
|
|
|
407
461
|
log.dim(' Dashboard: https://x402-api.onrender.com/dashboard');
|
|
408
462
|
log.dim(' Re-configure: npx x402-bazaar init');
|
|
409
463
|
console.log('');
|
|
464
|
+
|
|
465
|
+
printNonInteractiveHint('init');
|
|
410
466
|
}
|
|
411
467
|
|
|
412
468
|
/**
|
|
@@ -460,319 +516,3 @@ function writeConfig(envInfo, newConfig) {
|
|
|
460
516
|
}
|
|
461
517
|
}
|
|
462
518
|
|
|
463
|
-
/**
|
|
464
|
-
* Generate a standalone MCP server file.
|
|
465
|
-
* Used as fallback when the local x402-bazaar project is not found.
|
|
466
|
-
*/
|
|
467
|
-
function generateMcpServerFile() {
|
|
468
|
-
return `import 'dotenv/config';
|
|
469
|
-
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
470
|
-
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
471
|
-
import { z } from 'zod';
|
|
472
|
-
import { createRequire } from 'module';
|
|
473
|
-
|
|
474
|
-
const require = createRequire(import.meta.url);
|
|
475
|
-
const { Coinbase, Wallet } = require('@coinbase/coinbase-sdk');
|
|
476
|
-
|
|
477
|
-
// ─── Config ──────────────────────────────────────────────────────────
|
|
478
|
-
const SERVER_URL = process.env.X402_SERVER_URL || 'https://x402-api.onrender.com';
|
|
479
|
-
const MAX_BUDGET = parseFloat(process.env.MAX_BUDGET_USDC || '1.00');
|
|
480
|
-
const NETWORK = process.env.NETWORK || 'mainnet';
|
|
481
|
-
const explorerBase = NETWORK === 'testnet'
|
|
482
|
-
? 'https://sepolia.basescan.org'
|
|
483
|
-
: 'https://basescan.org';
|
|
484
|
-
const networkLabel = NETWORK === 'testnet' ? 'Base Sepolia' : 'Base Mainnet';
|
|
485
|
-
|
|
486
|
-
// ─── Budget Tracking ─────────────────────────────────────────────────
|
|
487
|
-
let sessionSpending = 0;
|
|
488
|
-
const sessionPayments = [];
|
|
489
|
-
|
|
490
|
-
// ─── Wallet ──────────────────────────────────────────────────────────
|
|
491
|
-
let wallet = null;
|
|
492
|
-
let walletReady = false;
|
|
493
|
-
|
|
494
|
-
async function initWallet() {
|
|
495
|
-
if (walletReady) return;
|
|
496
|
-
|
|
497
|
-
Coinbase.configure({
|
|
498
|
-
apiKeyName: process.env.COINBASE_API_KEY,
|
|
499
|
-
privateKey: process.env.COINBASE_API_SECRET,
|
|
500
|
-
});
|
|
501
|
-
|
|
502
|
-
const seedPath = process.env.AGENT_SEED_PATH || 'agent-seed.json';
|
|
503
|
-
const fs = await import('fs');
|
|
504
|
-
const seedData = JSON.parse(fs.readFileSync(seedPath, 'utf-8'));
|
|
505
|
-
const seedWalletId = Object.keys(seedData)[0];
|
|
506
|
-
|
|
507
|
-
wallet = await Wallet.fetch(seedWalletId);
|
|
508
|
-
await wallet.loadSeed(seedPath);
|
|
509
|
-
walletReady = true;
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
// ─── x402 Payment Flow ──────────────────────────────────────────────
|
|
513
|
-
async function payAndRequest(url, options = {}) {
|
|
514
|
-
const res = await fetch(url, options);
|
|
515
|
-
const body = await res.json();
|
|
516
|
-
|
|
517
|
-
if (res.status !== 402) {
|
|
518
|
-
return body;
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
// HTTP 402 — Payment Required
|
|
522
|
-
const details = body.payment_details;
|
|
523
|
-
const cost = parseFloat(details.amount);
|
|
524
|
-
|
|
525
|
-
// Budget check
|
|
526
|
-
if (sessionSpending + cost > MAX_BUDGET) {
|
|
527
|
-
throw new Error(
|
|
528
|
-
\`Budget limit reached. Spent: \${sessionSpending.toFixed(2)} USDC / \${MAX_BUDGET.toFixed(2)} USDC limit. \` +
|
|
529
|
-
\`This call costs \${cost} USDC. Increase MAX_BUDGET_USDC env var to allow more spending.\`
|
|
530
|
-
);
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
await initWallet();
|
|
534
|
-
|
|
535
|
-
const transfer = await wallet.createTransfer({
|
|
536
|
-
amount: details.amount,
|
|
537
|
-
assetId: Coinbase.assets.Usdc,
|
|
538
|
-
destination: details.recipient,
|
|
539
|
-
});
|
|
540
|
-
const confirmed = await transfer.wait({ timeoutSeconds: 120 });
|
|
541
|
-
const txHash = confirmed.getTransactionHash();
|
|
542
|
-
|
|
543
|
-
// Track spending
|
|
544
|
-
sessionSpending += cost;
|
|
545
|
-
sessionPayments.push({
|
|
546
|
-
amount: cost,
|
|
547
|
-
txHash,
|
|
548
|
-
timestamp: new Date().toISOString(),
|
|
549
|
-
endpoint: url.replace(SERVER_URL, ''),
|
|
550
|
-
});
|
|
551
|
-
|
|
552
|
-
// Retry with payment proof
|
|
553
|
-
const retryHeaders = { ...options.headers, 'X-Payment-TxHash': txHash };
|
|
554
|
-
const retryRes = await fetch(url, { ...options, headers: retryHeaders });
|
|
555
|
-
const result = await retryRes.json();
|
|
556
|
-
|
|
557
|
-
// Enrich result with payment info
|
|
558
|
-
result._payment = {
|
|
559
|
-
amount: details.amount,
|
|
560
|
-
currency: 'USDC',
|
|
561
|
-
txHash,
|
|
562
|
-
explorer: \`\${explorerBase}/tx/\${txHash}\`,
|
|
563
|
-
session_spent: sessionSpending.toFixed(2),
|
|
564
|
-
session_remaining: (MAX_BUDGET - sessionSpending).toFixed(2),
|
|
565
|
-
};
|
|
566
|
-
|
|
567
|
-
return result;
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
// ─── MCP Server ─────────────────────────────────────────────────────
|
|
571
|
-
const server = new McpServer({
|
|
572
|
-
name: 'x402-bazaar',
|
|
573
|
-
version: '1.1.0',
|
|
574
|
-
});
|
|
575
|
-
|
|
576
|
-
// --- Tool: discover_marketplace (FREE) ---
|
|
577
|
-
server.tool(
|
|
578
|
-
'discover_marketplace',
|
|
579
|
-
'Discover the x402 Bazaar marketplace. Returns available endpoints, total services, and protocol info. Free — no payment needed.',
|
|
580
|
-
{},
|
|
581
|
-
async () => {
|
|
582
|
-
try {
|
|
583
|
-
const res = await fetch(SERVER_URL);
|
|
584
|
-
const data = await res.json();
|
|
585
|
-
return {
|
|
586
|
-
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
|
|
587
|
-
};
|
|
588
|
-
} catch (err) {
|
|
589
|
-
return {
|
|
590
|
-
content: [{ type: 'text', text: \`Error: \${err.message}\` }],
|
|
591
|
-
isError: true,
|
|
592
|
-
};
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
);
|
|
596
|
-
|
|
597
|
-
// --- Tool: search_services (0.05 USDC) ---
|
|
598
|
-
server.tool(
|
|
599
|
-
'search_services',
|
|
600
|
-
\`Search for API services on x402 Bazaar by keyword. Costs 0.05 USDC (paid automatically). Budget: \${MAX_BUDGET.toFixed(2)} USDC per session. Check get_budget_status before calling if unsure about remaining budget.\`,
|
|
601
|
-
{ query: z.string().describe('Search keyword (e.g. "weather", "crypto", "ai")') },
|
|
602
|
-
async ({ query }) => {
|
|
603
|
-
try {
|
|
604
|
-
const result = await payAndRequest(
|
|
605
|
-
\`\${SERVER_URL}/search?q=\${encodeURIComponent(query)}\`
|
|
606
|
-
);
|
|
607
|
-
return {
|
|
608
|
-
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
609
|
-
};
|
|
610
|
-
} catch (err) {
|
|
611
|
-
return {
|
|
612
|
-
content: [{ type: 'text', text: \`Error: \${err.message}\` }],
|
|
613
|
-
isError: true,
|
|
614
|
-
};
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
);
|
|
618
|
-
|
|
619
|
-
// --- Tool: list_services (0.05 USDC) ---
|
|
620
|
-
server.tool(
|
|
621
|
-
'list_services',
|
|
622
|
-
\`List all API services available on x402 Bazaar. Costs 0.05 USDC (paid automatically). Budget: \${MAX_BUDGET.toFixed(2)} USDC per session.\`,
|
|
623
|
-
{},
|
|
624
|
-
async () => {
|
|
625
|
-
try {
|
|
626
|
-
const result = await payAndRequest(\`\${SERVER_URL}/services\`);
|
|
627
|
-
return {
|
|
628
|
-
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
629
|
-
};
|
|
630
|
-
} catch (err) {
|
|
631
|
-
return {
|
|
632
|
-
content: [{ type: 'text', text: \`Error: \${err.message}\` }],
|
|
633
|
-
isError: true,
|
|
634
|
-
};
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
);
|
|
638
|
-
|
|
639
|
-
// --- Tool: find_tool_for_task (0.05 USDC — smart service lookup) ---
|
|
640
|
-
server.tool(
|
|
641
|
-
'find_tool_for_task',
|
|
642
|
-
\`Describe what you need in plain English and get the best matching API service ready to call. Returns the single best match with name, URL, price, and usage instructions. Costs 0.05 USDC. Budget: \${MAX_BUDGET.toFixed(2)} USDC per session.\`,
|
|
643
|
-
{ task: z.string().describe('What you need, in natural language (e.g. "get current weather for a city", "translate text to French")') },
|
|
644
|
-
async ({ task }) => {
|
|
645
|
-
try {
|
|
646
|
-
const stopWords = new Set(['i', 'need', 'want', 'to', 'a', 'an', 'the', 'for', 'of', 'and', 'or', 'in', 'on', 'with', 'that', 'this', 'get', 'find', 'me', 'my', 'some', 'can', 'you', 'do', 'is', 'it', 'be', 'have', 'use', 'please', 'should', 'would', 'could']);
|
|
647
|
-
const keywords = task.toLowerCase()
|
|
648
|
-
.replace(/[^a-z0-9\\s]/g, '')
|
|
649
|
-
.split(/\\s+/)
|
|
650
|
-
.filter(w => w.length > 2 && !stopWords.has(w));
|
|
651
|
-
const query = keywords.slice(0, 3).join(' ') || task.slice(0, 30);
|
|
652
|
-
|
|
653
|
-
const result = await payAndRequest(
|
|
654
|
-
\`\${SERVER_URL}/search?q=\${encodeURIComponent(query)}\`
|
|
655
|
-
);
|
|
656
|
-
|
|
657
|
-
const services = result.data || result.services || [];
|
|
658
|
-
if (services.length === 0) {
|
|
659
|
-
return {
|
|
660
|
-
content: [{ type: 'text', text: JSON.stringify({
|
|
661
|
-
found: false,
|
|
662
|
-
query_used: query,
|
|
663
|
-
message: \`No services found matching "\${task}". Try rephrasing or use search_services with different keywords.\`,
|
|
664
|
-
_payment: result._payment,
|
|
665
|
-
}, null, 2) }],
|
|
666
|
-
};
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
const best = services[0];
|
|
670
|
-
return {
|
|
671
|
-
content: [{ type: 'text', text: JSON.stringify({
|
|
672
|
-
found: true,
|
|
673
|
-
query_used: query,
|
|
674
|
-
service: {
|
|
675
|
-
name: best.name,
|
|
676
|
-
description: best.description,
|
|
677
|
-
url: best.url,
|
|
678
|
-
price_usdc: best.price_usdc,
|
|
679
|
-
tags: best.tags,
|
|
680
|
-
},
|
|
681
|
-
action: \`Call this API using call_api("\${best.url}"). \${Number(best.price_usdc) === 0 ? 'This API is free.' : \`This API costs \${best.price_usdc} USDC per call.\`}\`,
|
|
682
|
-
alternatives_count: services.length - 1,
|
|
683
|
-
_payment: result._payment,
|
|
684
|
-
}, null, 2) }],
|
|
685
|
-
};
|
|
686
|
-
} catch (err) {
|
|
687
|
-
return {
|
|
688
|
-
content: [{ type: 'text', text: \`Error: \${err.message}\` }],
|
|
689
|
-
isError: true,
|
|
690
|
-
};
|
|
691
|
-
}
|
|
692
|
-
}
|
|
693
|
-
);
|
|
694
|
-
|
|
695
|
-
// --- Tool: call_api (FREE — calls external APIs) ---
|
|
696
|
-
server.tool(
|
|
697
|
-
'call_api',
|
|
698
|
-
'Call an external API URL and return the response. Use this to fetch real data from service URLs discovered on the marketplace. Free — no marketplace payment needed.',
|
|
699
|
-
{ url: z.string().url().describe('The full API URL to call') },
|
|
700
|
-
async ({ url }) => {
|
|
701
|
-
try {
|
|
702
|
-
const res = await fetch(url);
|
|
703
|
-
const text = await res.text();
|
|
704
|
-
let parsed;
|
|
705
|
-
try {
|
|
706
|
-
parsed = JSON.parse(text);
|
|
707
|
-
} catch {
|
|
708
|
-
parsed = { response: text.slice(0, 5000) };
|
|
709
|
-
}
|
|
710
|
-
return {
|
|
711
|
-
content: [{ type: 'text', text: JSON.stringify(parsed, null, 2) }],
|
|
712
|
-
};
|
|
713
|
-
} catch (err) {
|
|
714
|
-
return {
|
|
715
|
-
content: [{ type: 'text', text: \`Error: \${err.message}\` }],
|
|
716
|
-
isError: true,
|
|
717
|
-
};
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
);
|
|
721
|
-
|
|
722
|
-
// --- Tool: get_wallet_balance (FREE) ---
|
|
723
|
-
server.tool(
|
|
724
|
-
'get_wallet_balance',
|
|
725
|
-
'Check the USDC balance of the agent wallet on-chain. Free.',
|
|
726
|
-
{},
|
|
727
|
-
async () => {
|
|
728
|
-
try {
|
|
729
|
-
await initWallet();
|
|
730
|
-
const balance = await wallet.getBalance(Coinbase.assets.Usdc);
|
|
731
|
-
const address = (await wallet.getDefaultAddress()).getId();
|
|
732
|
-
return {
|
|
733
|
-
content: [{
|
|
734
|
-
type: 'text',
|
|
735
|
-
text: JSON.stringify({
|
|
736
|
-
address,
|
|
737
|
-
balance_usdc: balance.toString(),
|
|
738
|
-
network: networkLabel,
|
|
739
|
-
explorer: \`\${explorerBase}/address/\${address}\`,
|
|
740
|
-
}, null, 2),
|
|
741
|
-
}],
|
|
742
|
-
};
|
|
743
|
-
} catch (err) {
|
|
744
|
-
return {
|
|
745
|
-
content: [{ type: 'text', text: \`Error: \${err.message}\` }],
|
|
746
|
-
isError: true,
|
|
747
|
-
};
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
);
|
|
751
|
-
|
|
752
|
-
// --- Tool: get_budget_status (FREE) ---
|
|
753
|
-
server.tool(
|
|
754
|
-
'get_budget_status',
|
|
755
|
-
'Check the session spending budget. Shows how much USDC has been spent, remaining budget, and a list of all payments made this session. Free — call this before paid requests to verify budget.',
|
|
756
|
-
{},
|
|
757
|
-
async () => {
|
|
758
|
-
return {
|
|
759
|
-
content: [{
|
|
760
|
-
type: 'text',
|
|
761
|
-
text: JSON.stringify({
|
|
762
|
-
budget_limit: MAX_BUDGET.toFixed(2) + ' USDC',
|
|
763
|
-
spent: sessionSpending.toFixed(2) + ' USDC',
|
|
764
|
-
remaining: (MAX_BUDGET - sessionSpending).toFixed(2) + ' USDC',
|
|
765
|
-
payments_count: sessionPayments.length,
|
|
766
|
-
payments: sessionPayments,
|
|
767
|
-
network: networkLabel,
|
|
768
|
-
}, null, 2),
|
|
769
|
-
}],
|
|
770
|
-
};
|
|
771
|
-
}
|
|
772
|
-
);
|
|
773
|
-
|
|
774
|
-
// ─── Start ──────────────────────────────────────────────────────────
|
|
775
|
-
const transport = new StdioServerTransport();
|
|
776
|
-
await server.connect(transport);
|
|
777
|
-
`;
|
|
778
|
-
}
|
|
@@ -5,11 +5,11 @@ export function generateEnvContent({
|
|
|
5
5
|
serverUrl = 'https://x402-api.onrender.com',
|
|
6
6
|
maxBudget = '1.00',
|
|
7
7
|
network = 'mainnet',
|
|
8
|
+
agentPrivateKey = '',
|
|
8
9
|
coinbaseApiKey = '',
|
|
9
10
|
coinbaseApiSecret = '',
|
|
10
|
-
seedPath = 'agent-seed.json',
|
|
11
11
|
}) {
|
|
12
|
-
|
|
12
|
+
let content = `# x402 Bazaar MCP Server Configuration
|
|
13
13
|
# Generated by: npx x402-bazaar init
|
|
14
14
|
|
|
15
15
|
# Marketplace server URL
|
|
@@ -20,13 +20,22 @@ MAX_BUDGET_USDC=${maxBudget}
|
|
|
20
20
|
|
|
21
21
|
# Network: mainnet or testnet
|
|
22
22
|
NETWORK=${network}
|
|
23
|
+
`;
|
|
23
24
|
|
|
24
|
-
|
|
25
|
+
if (agentPrivateKey) {
|
|
26
|
+
content += `
|
|
27
|
+
# Agent wallet private key (Ethereum)
|
|
28
|
+
# WARNING: Keep this secret! Never commit to git.
|
|
29
|
+
AGENT_PRIVATE_KEY=${agentPrivateKey}
|
|
30
|
+
`;
|
|
31
|
+
} else if (coinbaseApiKey) {
|
|
32
|
+
content += `
|
|
33
|
+
# Coinbase Developer Platform API credentials (legacy mode)
|
|
25
34
|
# Get yours at: https://portal.cdp.coinbase.com/
|
|
26
35
|
COINBASE_API_KEY=${coinbaseApiKey}
|
|
27
36
|
COINBASE_API_SECRET=${coinbaseApiSecret}
|
|
28
|
-
|
|
29
|
-
# Path to the agent wallet seed file
|
|
30
|
-
AGENT_SEED_PATH=${seedPath}
|
|
31
37
|
`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return content;
|
|
32
41
|
}
|
|
@@ -10,16 +10,12 @@ export function generateMcpConfig({
|
|
|
10
10
|
serverUrl = 'https://x402-api.onrender.com',
|
|
11
11
|
maxBudget = '1.00',
|
|
12
12
|
network = 'mainnet',
|
|
13
|
+
agentPrivateKey = '',
|
|
13
14
|
coinbaseApiKey = '',
|
|
14
15
|
coinbaseApiSecret = '',
|
|
15
|
-
seedPath = '',
|
|
16
16
|
readOnly = false,
|
|
17
17
|
}) {
|
|
18
|
-
const os = platform();
|
|
19
|
-
const sep = os === 'win32' ? '\\' : '/';
|
|
20
|
-
|
|
21
18
|
const mcpServerPath = join(installDir, 'mcp-server.mjs');
|
|
22
|
-
const defaultSeedPath = seedPath || join(installDir, 'agent-seed.json');
|
|
23
19
|
|
|
24
20
|
const env = {
|
|
25
21
|
X402_SERVER_URL: serverUrl,
|
|
@@ -28,9 +24,12 @@ export function generateMcpConfig({
|
|
|
28
24
|
};
|
|
29
25
|
|
|
30
26
|
if (!readOnly) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
27
|
+
if (agentPrivateKey) {
|
|
28
|
+
env.AGENT_PRIVATE_KEY = agentPrivateKey;
|
|
29
|
+
} else if (coinbaseApiKey) {
|
|
30
|
+
env.COINBASE_API_KEY = coinbaseApiKey;
|
|
31
|
+
env.COINBASE_API_SECRET = coinbaseApiSecret;
|
|
32
|
+
}
|
|
34
33
|
}
|
|
35
34
|
|
|
36
35
|
const serverEntry = {
|
|
@@ -41,27 +40,6 @@ export function generateMcpConfig({
|
|
|
41
40
|
|
|
42
41
|
// Format for the target environment
|
|
43
42
|
switch (environment) {
|
|
44
|
-
case 'claude-desktop':
|
|
45
|
-
return {
|
|
46
|
-
mcpServers: {
|
|
47
|
-
'x402-bazaar': serverEntry,
|
|
48
|
-
},
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
case 'cursor':
|
|
52
|
-
return {
|
|
53
|
-
mcpServers: {
|
|
54
|
-
'x402-bazaar': serverEntry,
|
|
55
|
-
},
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
case 'claude-code':
|
|
59
|
-
return {
|
|
60
|
-
mcpServers: {
|
|
61
|
-
'x402-bazaar': serverEntry,
|
|
62
|
-
},
|
|
63
|
-
};
|
|
64
|
-
|
|
65
43
|
case 'vscode-continue':
|
|
66
44
|
return {
|
|
67
45
|
models: [],
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { log } from './logger.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Returns true if the terminal supports interactive prompts.
|
|
7
|
+
*/
|
|
8
|
+
export function isInteractive() {
|
|
9
|
+
if (process.env.CI === 'true' || process.env.CI === '1') return false;
|
|
10
|
+
return Boolean(process.stdin.isTTY);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Safe wrapper around inquirer.prompt().
|
|
15
|
+
*
|
|
16
|
+
* Interactive mode (TTY): behaves exactly like inquirer.prompt().
|
|
17
|
+
* Non-interactive mode: resolves each question with its default value
|
|
18
|
+
* and logs what was chosen.
|
|
19
|
+
*
|
|
20
|
+
* @param {Array} questions - inquirer question array
|
|
21
|
+
* @param {Object} overrides - optional { questionName: value } to force
|
|
22
|
+
* @returns {Promise<Object>} answers
|
|
23
|
+
*/
|
|
24
|
+
export async function promptOrDefault(questions, overrides = {}) {
|
|
25
|
+
const questionList = Array.isArray(questions) ? questions : [questions];
|
|
26
|
+
|
|
27
|
+
if (isInteractive()) {
|
|
28
|
+
const remaining = questionList.filter(q => !(q.name in overrides));
|
|
29
|
+
const answers = { ...overrides };
|
|
30
|
+
if (remaining.length > 0) {
|
|
31
|
+
const prompted = await inquirer.prompt(remaining);
|
|
32
|
+
Object.assign(answers, prompted);
|
|
33
|
+
}
|
|
34
|
+
return answers;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Non-interactive path
|
|
38
|
+
const answers = {};
|
|
39
|
+
|
|
40
|
+
for (const q of questionList) {
|
|
41
|
+
if (q.name in overrides) {
|
|
42
|
+
answers[q.name] = overrides[q.name];
|
|
43
|
+
log.info(`${q.message} ${chalk.bold(overrides[q.name])} ${chalk.dim('(flag)')}`);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let defaultVal = q.default;
|
|
48
|
+
|
|
49
|
+
// For list prompts without explicit default, use first choice
|
|
50
|
+
if (q.type === 'list' && q.choices && defaultVal === undefined) {
|
|
51
|
+
const firstChoice = q.choices.find(c => c.value !== undefined);
|
|
52
|
+
defaultVal = firstChoice ? firstChoice.value : undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// For confirm prompts, default to true
|
|
56
|
+
if (q.type === 'confirm' && defaultVal === undefined) {
|
|
57
|
+
defaultVal = true;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (defaultVal === undefined) {
|
|
61
|
+
answers[q.name] = '';
|
|
62
|
+
log.warn(`${q.message} ${chalk.dim('(skipped — use flags to provide)')}`);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
answers[q.name] = defaultVal;
|
|
67
|
+
|
|
68
|
+
// Build human-readable label for list choices
|
|
69
|
+
let displayVal = String(defaultVal);
|
|
70
|
+
if (q.type === 'list' && q.choices) {
|
|
71
|
+
const match = q.choices.find(c => c.value === defaultVal);
|
|
72
|
+
if (match && match.name) {
|
|
73
|
+
displayVal = match.name.replace(/\x1b\[[0-9;]*m/g, '');
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
log.info(`${q.message} ${chalk.bold(displayVal)} ${chalk.dim('(auto)')}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return answers;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Print a hint showing available flags for customization.
|
|
85
|
+
* Only prints in non-interactive mode.
|
|
86
|
+
*/
|
|
87
|
+
export function printNonInteractiveHint(command = 'init') {
|
|
88
|
+
if (isInteractive()) return;
|
|
89
|
+
|
|
90
|
+
console.log('');
|
|
91
|
+
log.info(chalk.yellow('Running in non-interactive mode (IDE terminal detected).'));
|
|
92
|
+
log.info('To customize, use flags:');
|
|
93
|
+
console.log('');
|
|
94
|
+
|
|
95
|
+
if (command === 'init') {
|
|
96
|
+
log.dim(' npx x402-bazaar init --env cursor');
|
|
97
|
+
log.dim(' npx x402-bazaar init --network testnet --budget 5.00');
|
|
98
|
+
log.dim(' npx x402-bazaar init --no-wallet');
|
|
99
|
+
} else if (command === 'config') {
|
|
100
|
+
log.dim(' npx x402-bazaar config --env cursor');
|
|
101
|
+
log.dim(' npx x402-bazaar config --network testnet --budget 5.00');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
console.log('');
|
|
105
|
+
log.dim(' For full interactive setup, run in a standalone terminal.');
|
|
106
|
+
console.log('');
|
|
107
|
+
}
|