@xyz-credit/agent-cli 1.1.2 → 1.2.1

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.
@@ -0,0 +1,295 @@
1
+ /**
2
+ * Setup Wizard — Automated NPX onboarding
3
+ *
4
+ * Chains inquirer prompts for:
5
+ * 1. Platform URL
6
+ * 2. LLM provider selection + API key validation
7
+ * 3. Custom MCP server (optional)
8
+ * 4. Auto-generate .env + config.json
9
+ * 5. Trigger first boot-up
10
+ */
11
+ const inquirer = require('inquirer');
12
+ const chalk = require('chalk');
13
+ const ora = require('ora');
14
+ const fetch = require('node-fetch');
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+ const boxen = require('boxen');
18
+ const { config, saveCredentials, isAuthenticated, getCredentials } = require('../config');
19
+
20
+ const LLM_PROVIDERS = [
21
+ { name: 'OpenAI (GPT-4o, GPT-5.2)', value: 'openai' },
22
+ { name: 'Anthropic (Claude Sonnet 4.5)', value: 'anthropic' },
23
+ { name: 'Google Gemini', value: 'gemini' },
24
+ { name: 'Mistral (Local via Ollama)', value: 'ollama' },
25
+ { name: 'Groq', value: 'groq' },
26
+ ];
27
+
28
+ async function validateLLMKey(provider, apiKey, platformUrl) {
29
+ if (provider === 'ollama') return { valid: true };
30
+
31
+ const spinner = ora(`Validating ${provider} API key...`).start();
32
+ try {
33
+ const res = await fetch(`${platformUrl}/api/cli/validate-llm`, {
34
+ method: 'POST',
35
+ headers: { 'Content-Type': 'application/json' },
36
+ body: JSON.stringify({ provider, api_key: apiKey }),
37
+ timeout: 15000,
38
+ });
39
+ if (res.ok) {
40
+ const data = await res.json();
41
+ if (data.valid) {
42
+ spinner.succeed(`${provider} API key validated`);
43
+ return { valid: true };
44
+ }
45
+ spinner.fail(`${provider} key invalid: ${data.error || 'authentication failed'}`);
46
+ return { valid: false, error: data.error };
47
+ }
48
+ spinner.fail('Validation request failed');
49
+ return { valid: false };
50
+ } catch (e) {
51
+ // Fallback: try direct validation
52
+ try {
53
+ let valid = false;
54
+ if (provider === 'openai') {
55
+ const r = await fetch('https://api.openai.com/v1/models', {
56
+ headers: { Authorization: `Bearer ${apiKey}` }, timeout: 8000,
57
+ });
58
+ valid = r.status === 200;
59
+ } else if (provider === 'anthropic') {
60
+ const r = await fetch('https://api.anthropic.com/v1/messages', {
61
+ method: 'POST',
62
+ headers: { 'x-api-key': apiKey, 'anthropic-version': '2023-06-01', 'content-type': 'application/json' },
63
+ body: JSON.stringify({ model: 'claude-3-haiku-20240307', max_tokens: 1, messages: [{ role: 'user', content: 'hi' }] }),
64
+ timeout: 8000,
65
+ });
66
+ valid = r.status === 200 || r.status === 201;
67
+ } else if (provider === 'gemini') {
68
+ const r = await fetch(`https://generativelanguage.googleapis.com/v1/models?key=${apiKey}`, { timeout: 8000 });
69
+ valid = r.status === 200;
70
+ } else if (provider === 'groq') {
71
+ const r = await fetch('https://api.groq.com/openai/v1/models', {
72
+ headers: { Authorization: `Bearer ${apiKey}` }, timeout: 8000,
73
+ });
74
+ valid = r.status === 200;
75
+ }
76
+ if (valid) {
77
+ spinner.succeed(`${provider} API key validated (direct)`);
78
+ return { valid: true };
79
+ }
80
+ spinner.fail(`${provider} key invalid`);
81
+ return { valid: false };
82
+ } catch {
83
+ spinner.warn(`Could not validate (network issue) — saving key anyway`);
84
+ return { valid: true, warning: 'offline_validation' };
85
+ }
86
+ }
87
+ }
88
+
89
+ async function setupCommand() {
90
+ console.log('');
91
+ console.log(boxen(
92
+ chalk.bold.cyan(' xyz.credit Agent Setup Wizard\n\n') +
93
+ chalk.dim(' Automated onboarding — configure identity, LLM, and MCP\n') +
94
+ chalk.dim(' in one seamless flow. No manual file editing needed.'),
95
+ { padding: 1, margin: 1, borderColor: 'cyan', borderStyle: 'round', title: 'Setup', titleAlignment: 'center' }
96
+ ));
97
+
98
+ // Check existing setup
99
+ if (isAuthenticated()) {
100
+ const creds = getCredentials();
101
+ console.log(chalk.yellow(` Existing agent found: ${creds.agentName} (${creds.agentId.slice(0, 12)}...)`));
102
+ const { reconfigure } = await inquirer.prompt([{
103
+ type: 'confirm', name: 'reconfigure',
104
+ message: 'Reconfigure from scratch?', default: false,
105
+ }]);
106
+ if (!reconfigure) {
107
+ console.log(chalk.green(' Using existing configuration.\n'));
108
+ return;
109
+ }
110
+ }
111
+
112
+ // ── Step 1: Platform URL ──
113
+ console.log(chalk.bold('\n Step 1/4: Platform Connection\n'));
114
+ const { platformUrl } = await inquirer.prompt([{
115
+ type: 'input', name: 'platformUrl',
116
+ message: 'Platform URL:',
117
+ default: config.get('platformUrl') || 'https://agent-messaging.preview.emergentagent.com',
118
+ validate: (v) => v.startsWith('http') ? true : 'Must start with http(s)://',
119
+ }]);
120
+
121
+ const pingSpinner = ora('Testing platform connection...').start();
122
+ try {
123
+ const r = await fetch(`${platformUrl}/api/health`, { timeout: 8000 });
124
+ if (r.ok) pingSpinner.succeed('Platform connected');
125
+ else pingSpinner.warn('Platform responded but may have issues');
126
+ } catch {
127
+ pingSpinner.warn('Cannot reach platform — continuing offline');
128
+ }
129
+
130
+ config.set('platformUrl', platformUrl);
131
+
132
+ // ── Step 2: LLM Provider ──
133
+ console.log(chalk.bold('\n Step 2/4: LLM Configuration\n'));
134
+ const { llmProvider } = await inquirer.prompt([{
135
+ type: 'list', name: 'llmProvider',
136
+ message: 'Select your LLM provider:',
137
+ choices: LLM_PROVIDERS,
138
+ }]);
139
+
140
+ let llmApiKey = '';
141
+ let llmValidated = false;
142
+
143
+ if (llmProvider === 'ollama') {
144
+ console.log(chalk.dim(' Ollama runs locally — no API key needed.'));
145
+ const { ollamaUrl } = await inquirer.prompt([{
146
+ type: 'input', name: 'ollamaUrl',
147
+ message: 'Ollama server URL:',
148
+ default: 'http://localhost:11434',
149
+ }]);
150
+ config.set('llmProvider', 'ollama');
151
+ config.set('llmEndpoint', ollamaUrl);
152
+ llmValidated = true;
153
+ } else {
154
+ let attempts = 0;
155
+ while (attempts < 3 && !llmValidated) {
156
+ const { apiKey } = await inquirer.prompt([{
157
+ type: 'password', name: 'apiKey', mask: '*',
158
+ message: `${llmProvider} API Key:`,
159
+ validate: (v) => v.length > 10 ? true : 'API key seems too short',
160
+ }]);
161
+
162
+ const result = await validateLLMKey(llmProvider, apiKey, platformUrl);
163
+ if (result.valid) {
164
+ llmApiKey = apiKey;
165
+ llmValidated = true;
166
+ } else {
167
+ attempts++;
168
+ if (attempts < 3) console.log(chalk.yellow(` Try again (${3 - attempts} attempts left)`));
169
+ }
170
+ }
171
+
172
+ if (!llmValidated) {
173
+ console.log(chalk.red(' LLM validation failed. You can update the key later with `xyz-agent config`.'));
174
+ }
175
+
176
+ config.set('llmProvider', llmProvider);
177
+ config.set('llmApiKey', llmApiKey);
178
+ }
179
+
180
+ // ── Step 3: Custom MCP Server ──
181
+ console.log(chalk.bold('\n Step 3/4: MCP Server (Optional)\n'));
182
+ const { hasMcp } = await inquirer.prompt([{
183
+ type: 'confirm', name: 'hasMcp',
184
+ message: 'Do you have a custom MCP server for your own products?',
185
+ default: false,
186
+ }]);
187
+
188
+ let mcpUrl = '';
189
+ if (hasMcp) {
190
+ const answer = await inquirer.prompt([{
191
+ type: 'input', name: 'mcpUrl',
192
+ message: 'MCP server URL:',
193
+ default: 'http://localhost:3001/mcp',
194
+ validate: (v) => v.startsWith('http') ? true : 'Must be a valid URL',
195
+ }]);
196
+ mcpUrl = answer.mcpUrl;
197
+ config.set('localMcpUrl', mcpUrl);
198
+
199
+ // Try connecting
200
+ const mcpSpinner = ora('Testing MCP connection...').start();
201
+ try {
202
+ const r = await fetch(mcpUrl.replace('/sse', ''), {
203
+ method: 'POST', timeout: 5000,
204
+ headers: { 'Content-Type': 'application/json' },
205
+ body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'initialize', params: { protocolVersion: '2025-03-26', capabilities: {}, clientInfo: { name: 'xyz-agent-cli', version: '2.0.0' } } }),
206
+ });
207
+ if (r.ok) mcpSpinner.succeed('MCP server connected');
208
+ else mcpSpinner.warn('MCP server responded with non-200');
209
+ } catch {
210
+ mcpSpinner.warn('MCP not reachable now — will retry on start');
211
+ }
212
+ }
213
+
214
+ // ── Step 4: Finalization ──
215
+ console.log(chalk.bold('\n Step 4/4: Generating Configuration\n'));
216
+
217
+ const outputDir = process.cwd();
218
+ const envPath = path.join(outputDir, '.env');
219
+ const configJsonPath = path.join(outputDir, 'config.json');
220
+
221
+ // Generate .env
222
+ const envContent = [
223
+ `# xyz.credit Agent Configuration`,
224
+ `# Generated by setup wizard at ${new Date().toISOString()}`,
225
+ ``,
226
+ `PLATFORM_URL=${platformUrl}`,
227
+ `LLM_PROVIDER=${llmProvider}`,
228
+ llmApiKey ? `LLM_API_KEY=${llmApiKey}` : `# LLM_API_KEY=`,
229
+ llmProvider === 'ollama' ? `OLLAMA_URL=${config.get('llmEndpoint') || 'http://localhost:11434'}` : '',
230
+ mcpUrl ? `LOCAL_MCP_SOURCE=${mcpUrl}` : `# LOCAL_MCP_SOURCE=`,
231
+ ``,
232
+ `# Agent identity (populated after auth)`,
233
+ `# AGENT_ID=`,
234
+ `# API_KEY=`,
235
+ ``,
236
+ `# Heartbeat interval (seconds)`,
237
+ `HEARTBEAT_INTERVAL=1800`,
238
+ ].filter(Boolean).join('\n') + '\n';
239
+
240
+ // Generate config.json
241
+ const configJson = {
242
+ platform_url: platformUrl,
243
+ llm: {
244
+ provider: llmProvider,
245
+ validated: llmValidated,
246
+ ...(llmProvider === 'ollama' ? { endpoint: config.get('llmEndpoint') } : {}),
247
+ },
248
+ mcp: mcpUrl ? { url: mcpUrl, connected: false } : null,
249
+ heartbeat: { interval_seconds: 1800, enabled: true },
250
+ agent: { id: config.get('agentId') || null, name: config.get('agentName') || null },
251
+ created_at: new Date().toISOString(),
252
+ };
253
+
254
+ const genSpinner = ora('Writing configuration files...').start();
255
+
256
+ fs.writeFileSync(envPath, envContent);
257
+ fs.writeFileSync(configJsonPath, JSON.stringify(configJson, null, 2) + '\n');
258
+
259
+ genSpinner.succeed('Configuration files generated');
260
+
261
+ console.log(chalk.dim(` .env → ${envPath}`));
262
+ console.log(chalk.dim(` config.json→ ${configJsonPath}`));
263
+
264
+ // ── Auto boot-up ──
265
+ console.log('');
266
+ console.log(boxen(
267
+ chalk.green.bold(' Setup Complete!\n\n') +
268
+ chalk.white(` Platform: ${chalk.cyan(platformUrl)}\n`) +
269
+ chalk.white(` LLM: ${chalk.cyan(llmProvider)} ${llmValidated ? chalk.green('(validated)') : chalk.yellow('(unverified)')}\n`) +
270
+ chalk.white(` MCP: ${mcpUrl ? chalk.cyan(mcpUrl) : chalk.dim('Not configured')}\n\n`) +
271
+ chalk.dim(' Next steps:\n') +
272
+ chalk.white(` 1. ${chalk.cyan('xyz-agent auth')} Authenticate your agent\n`) +
273
+ chalk.white(` 2. ${chalk.cyan('xyz-agent connect')} Bridge your MCP tools\n`) +
274
+ chalk.white(` 3. ${chalk.cyan('xyz-agent register-service')} Publish to marketplace\n`) +
275
+ chalk.white(` 4. ${chalk.cyan('xyz-agent start')} Start the agent with dashboard`),
276
+ { padding: 1, margin: 1, borderColor: 'green', borderStyle: 'round', title: 'Ready', titleAlignment: 'center' }
277
+ ));
278
+
279
+ // If already authenticated, offer to start immediately
280
+ if (isAuthenticated()) {
281
+ const { autoStart } = await inquirer.prompt([{
282
+ type: 'confirm', name: 'autoStart',
283
+ message: 'Agent is already authenticated. Start now?',
284
+ default: true,
285
+ }]);
286
+ if (autoStart) {
287
+ console.log(chalk.dim('\n Launching agent...\n'));
288
+ const { startCommand } = require('./start');
289
+ await startCommand({});
290
+ }
291
+ }
292
+ console.log('');
293
+ }
294
+
295
+ module.exports = { setupCommand };