@swarmai/local-agent 0.1.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 +174 -0
- package/index.js +11 -0
- package/package.json +42 -0
- package/src/agenticChatClaudeMd.js +92 -0
- package/src/agenticChatHandler.js +566 -0
- package/src/aiProviderScanner.js +115 -0
- package/src/auth.js +180 -0
- package/src/cli.js +334 -0
- package/src/commands.js +1853 -0
- package/src/config.js +98 -0
- package/src/connection.js +470 -0
- package/src/mcpManager.js +276 -0
- package/src/startup.js +297 -0
- package/src/toolScanner.js +221 -0
- package/src/workspace.js +201 -0
package/src/auth.js
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth flow for Local Agent CLI
|
|
3
|
+
*
|
|
4
|
+
* 1. POST /api/local-agents/auth/init → get sessionId + authUrl
|
|
5
|
+
* 2. Open authUrl in browser
|
|
6
|
+
* 3. Poll GET /api/local-agents/auth/status/:sessionId until approved/denied/expired
|
|
7
|
+
* 4. Save API key to config
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const http = require('http');
|
|
11
|
+
const https = require('https');
|
|
12
|
+
const os = require('os');
|
|
13
|
+
const { loadConfig, saveConfig } = require('./config');
|
|
14
|
+
|
|
15
|
+
const POLL_INTERVAL_MS = 2000;
|
|
16
|
+
const MAX_POLL_ATTEMPTS = 150; // 5 minutes at 2s intervals
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Make an HTTP(S) request (minimal, no external deps needed)
|
|
20
|
+
*/
|
|
21
|
+
function request(url, options = {}) {
|
|
22
|
+
return new Promise((resolve, reject) => {
|
|
23
|
+
const urlObj = new URL(url);
|
|
24
|
+
const mod = urlObj.protocol === 'https:' ? https : http;
|
|
25
|
+
|
|
26
|
+
const reqOptions = {
|
|
27
|
+
method: options.method || 'GET',
|
|
28
|
+
hostname: urlObj.hostname,
|
|
29
|
+
port: urlObj.port,
|
|
30
|
+
path: urlObj.pathname + urlObj.search,
|
|
31
|
+
headers: {
|
|
32
|
+
'Content-Type': 'application/json',
|
|
33
|
+
...options.headers,
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const req = mod.request(reqOptions, (res) => {
|
|
38
|
+
let data = '';
|
|
39
|
+
res.on('data', chunk => { data += chunk; });
|
|
40
|
+
res.on('end', () => {
|
|
41
|
+
try {
|
|
42
|
+
resolve({ status: res.statusCode, data: JSON.parse(data) });
|
|
43
|
+
} catch {
|
|
44
|
+
resolve({ status: res.statusCode, data });
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
req.on('error', reject);
|
|
50
|
+
|
|
51
|
+
if (options.body) {
|
|
52
|
+
req.write(JSON.stringify(options.body));
|
|
53
|
+
}
|
|
54
|
+
req.end();
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Initiate auth flow
|
|
60
|
+
*/
|
|
61
|
+
async function initAuth(serverUrl, deviceName) {
|
|
62
|
+
const url = new URL('/api/local-agents/auth/init', serverUrl);
|
|
63
|
+
const response = await request(url.toString(), {
|
|
64
|
+
method: 'POST',
|
|
65
|
+
body: {
|
|
66
|
+
deviceName,
|
|
67
|
+
hostname: os.hostname(),
|
|
68
|
+
os: os.platform(),
|
|
69
|
+
osVersion: os.release(),
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
if (response.status !== 200) {
|
|
74
|
+
throw new Error(`Auth init failed: ${response.data?.error || 'Unknown error'}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return response.data;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Poll for auth status
|
|
82
|
+
*/
|
|
83
|
+
async function pollAuthStatus(serverUrl, sessionId, onPoll) {
|
|
84
|
+
// Validate sessionId to prevent URL injection (must be UUID format)
|
|
85
|
+
if (!sessionId || !/^[a-f0-9-]{36}$/i.test(sessionId)) {
|
|
86
|
+
throw new Error('Invalid session ID format');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let attempts = 0;
|
|
90
|
+
|
|
91
|
+
return new Promise((resolve, reject) => {
|
|
92
|
+
const poll = async () => {
|
|
93
|
+
attempts++;
|
|
94
|
+
|
|
95
|
+
if (attempts > MAX_POLL_ATTEMPTS) {
|
|
96
|
+
reject(new Error('Auth timed out'));
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
const url = new URL(`/api/local-agents/auth/status/${encodeURIComponent(sessionId)}`, serverUrl);
|
|
102
|
+
const response = await request(url.toString());
|
|
103
|
+
|
|
104
|
+
if (onPoll) onPoll(attempts);
|
|
105
|
+
|
|
106
|
+
if (response.status !== 200) {
|
|
107
|
+
reject(new Error(`Status check failed: ${response.data?.error || 'Unknown'}`));
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const { status, apiKey, agentId } = response.data;
|
|
112
|
+
|
|
113
|
+
if (status === 'approved') {
|
|
114
|
+
if (!apiKey) {
|
|
115
|
+
reject(new Error('API key already retrieved. Run login again to generate a new key.'));
|
|
116
|
+
} else {
|
|
117
|
+
resolve({ apiKey, agentId });
|
|
118
|
+
}
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (status === 'denied') {
|
|
123
|
+
reject(new Error('Authorization denied by user'));
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (status === 'expired') {
|
|
128
|
+
reject(new Error('Authorization request expired'));
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// status === 'pending' — schedule next poll
|
|
133
|
+
setTimeout(poll, POLL_INTERVAL_MS);
|
|
134
|
+
} catch (err) {
|
|
135
|
+
// Network error, retry
|
|
136
|
+
setTimeout(poll, POLL_INTERVAL_MS);
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
setTimeout(poll, POLL_INTERVAL_MS);
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Full login flow
|
|
146
|
+
*/
|
|
147
|
+
async function login(serverUrl, deviceName) {
|
|
148
|
+
// 1. Init auth challenge
|
|
149
|
+
const { sessionId, authUrl } = await initAuth(serverUrl, deviceName);
|
|
150
|
+
|
|
151
|
+
// 2. Open browser
|
|
152
|
+
let opened = false;
|
|
153
|
+
try {
|
|
154
|
+
const open = require('open');
|
|
155
|
+
await open(authUrl);
|
|
156
|
+
opened = true;
|
|
157
|
+
} catch {
|
|
158
|
+
// open may fail in headless environments
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// 3. Poll for result
|
|
162
|
+
const result = await pollAuthStatus(serverUrl, sessionId);
|
|
163
|
+
|
|
164
|
+
// 4. Save config
|
|
165
|
+
const config = loadConfig();
|
|
166
|
+
config.server = serverUrl;
|
|
167
|
+
config.apiKey = result.apiKey;
|
|
168
|
+
config.agentId = result.agentId;
|
|
169
|
+
config.deviceName = deviceName;
|
|
170
|
+
saveConfig(config);
|
|
171
|
+
|
|
172
|
+
return { ...result, authUrl, opened };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
module.exports = {
|
|
176
|
+
initAuth,
|
|
177
|
+
pollAuthStatus,
|
|
178
|
+
login,
|
|
179
|
+
request,
|
|
180
|
+
};
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI command definitions for SwarmAI Local Agent
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const { Command } = require('commander');
|
|
7
|
+
const chalk = require('chalk');
|
|
8
|
+
const { loadConfig, saveConfig, isConfigured, DEFAULT_SERVER, CONFIG_FILE } = require('./config');
|
|
9
|
+
const { login } = require('./auth');
|
|
10
|
+
const { AgentConnection } = require('./connection');
|
|
11
|
+
const { enableStartup, disableStartup, isStartupEnabled } = require('./startup');
|
|
12
|
+
|
|
13
|
+
function createCli() {
|
|
14
|
+
const program = new Command();
|
|
15
|
+
|
|
16
|
+
program
|
|
17
|
+
.name('swarmai-agent')
|
|
18
|
+
.description('SwarmAI Local Agent — Connect your device to SwarmAI')
|
|
19
|
+
.version('0.1.0');
|
|
20
|
+
|
|
21
|
+
// ==================
|
|
22
|
+
// LOGIN
|
|
23
|
+
// ==================
|
|
24
|
+
program
|
|
25
|
+
.command('login')
|
|
26
|
+
.description('Authenticate this device with SwarmAI server')
|
|
27
|
+
.option('-a, --api <url>', 'SwarmAI server URL', DEFAULT_SERVER)
|
|
28
|
+
.option('-s, --server <url>', 'SwarmAI server URL (alias for --api)')
|
|
29
|
+
.option('-n, --name <name>', 'Device name', os.hostname())
|
|
30
|
+
.action(async (opts) => {
|
|
31
|
+
// --api takes priority, --server is alias for backwards compat
|
|
32
|
+
const serverUrl = opts.api || opts.server || DEFAULT_SERVER;
|
|
33
|
+
opts.server = serverUrl;
|
|
34
|
+
|
|
35
|
+
console.log(chalk.cyan.bold('\n SwarmAI Local Agent\n'));
|
|
36
|
+
console.log(chalk.gray(` Server: ${serverUrl}`));
|
|
37
|
+
console.log(chalk.gray(` Device: ${opts.name}\n`));
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
console.log(chalk.yellow(' Initiating authorization...'));
|
|
41
|
+
|
|
42
|
+
const result = await login(serverUrl, opts.name);
|
|
43
|
+
|
|
44
|
+
if (result.opened) {
|
|
45
|
+
console.log(chalk.gray(' Browser opened for authorization.'));
|
|
46
|
+
} else {
|
|
47
|
+
console.log(chalk.yellow(' Could not open browser. Please visit:'));
|
|
48
|
+
console.log(chalk.cyan(` ${result.authUrl}\n`));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
console.log(chalk.green.bold('\n Device authorized successfully!\n'));
|
|
52
|
+
console.log(chalk.gray(` Agent ID: ${result.agentId}`));
|
|
53
|
+
console.log(chalk.gray(` Config saved to: ${CONFIG_FILE}\n`));
|
|
54
|
+
console.log(chalk.cyan(' Run `swarmai-agent start` to connect.\n'));
|
|
55
|
+
} catch (error) {
|
|
56
|
+
console.log(chalk.red(`\n Error: ${error.message}\n`));
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// ==================
|
|
62
|
+
// START
|
|
63
|
+
// ==================
|
|
64
|
+
program
|
|
65
|
+
.command('start')
|
|
66
|
+
.description('Connect to SwarmAI server and listen for commands')
|
|
67
|
+
.option('--runat-startup', 'Register to auto-start at system boot')
|
|
68
|
+
.option('--remove-startup', 'Remove auto-start at system boot')
|
|
69
|
+
.action(async (opts) => {
|
|
70
|
+
// Handle startup registration
|
|
71
|
+
if (opts.runatStartup) {
|
|
72
|
+
try {
|
|
73
|
+
const result = enableStartup();
|
|
74
|
+
console.log(chalk.green(`\n Auto-start enabled (${result.method})`));
|
|
75
|
+
console.log(chalk.gray(` Path: ${result.path}\n`));
|
|
76
|
+
console.log(chalk.gray(' SwarmAI Agent will now start automatically on boot.'));
|
|
77
|
+
console.log(chalk.gray(' Use --remove-startup to disable.\n'));
|
|
78
|
+
} catch (err) {
|
|
79
|
+
console.log(chalk.red(`\n Failed to enable auto-start: ${err.message}\n`));
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
// Continue to also start the agent
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (opts.removeStartup) {
|
|
86
|
+
const removed = disableStartup();
|
|
87
|
+
if (removed) {
|
|
88
|
+
console.log(chalk.green('\n Auto-start removed.\n'));
|
|
89
|
+
} else {
|
|
90
|
+
console.log(chalk.yellow('\n Auto-start was not enabled.\n'));
|
|
91
|
+
}
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!isConfigured()) {
|
|
96
|
+
console.log(chalk.red('\n Not configured. Run `swarmai-agent login` first.\n'));
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const config = loadConfig();
|
|
101
|
+
|
|
102
|
+
// Check for updates (non-blocking)
|
|
103
|
+
checkForUpdates(config.server || DEFAULT_SERVER).catch(() => {});
|
|
104
|
+
|
|
105
|
+
console.log(chalk.cyan.bold('\n SwarmAI Local Agent\n'));
|
|
106
|
+
console.log(chalk.gray(` Server: ${config.server}`));
|
|
107
|
+
console.log(chalk.gray(` Device: ${config.deviceName || 'Unknown'}`));
|
|
108
|
+
console.log(chalk.gray(` Agent ID: ${config.agentId}\n`));
|
|
109
|
+
|
|
110
|
+
const connection = new AgentConnection(config.server, config.apiKey, {
|
|
111
|
+
onStatusChange: (status) => {
|
|
112
|
+
if (status === 'connected') {
|
|
113
|
+
console.log(chalk.green(` [${timestamp()}] Connected`));
|
|
114
|
+
} else if (status === 'disconnected') {
|
|
115
|
+
console.log(chalk.yellow(` [${timestamp()}] Disconnected — will reconnect`));
|
|
116
|
+
} else if (status === 'error') {
|
|
117
|
+
console.log(chalk.red(` [${timestamp()}] Connection error`));
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
onLog: (msg) => {
|
|
121
|
+
console.log(chalk.gray(` [${timestamp()}] ${msg}`));
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Handle graceful shutdown
|
|
126
|
+
const shutdown = () => {
|
|
127
|
+
console.log(chalk.yellow('\n Disconnecting...'));
|
|
128
|
+
connection.disconnect();
|
|
129
|
+
process.exit(0);
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
process.on('SIGINT', shutdown);
|
|
133
|
+
process.on('SIGTERM', shutdown);
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
await connection.connect();
|
|
137
|
+
console.log(chalk.green('\n Listening for commands. Press Ctrl+C to stop.\n'));
|
|
138
|
+
|
|
139
|
+
// Keep process alive
|
|
140
|
+
await new Promise(() => {});
|
|
141
|
+
} catch (error) {
|
|
142
|
+
console.log(chalk.red(`\n Failed to connect: ${error.message}`));
|
|
143
|
+
console.log(chalk.gray(' Make sure the server is running and your API key is valid.\n'));
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// ==================
|
|
149
|
+
// STATUS
|
|
150
|
+
// ==================
|
|
151
|
+
program
|
|
152
|
+
.command('status')
|
|
153
|
+
.description('Show current configuration and connection status')
|
|
154
|
+
.action(() => {
|
|
155
|
+
const config = loadConfig();
|
|
156
|
+
console.log(chalk.cyan.bold('\n SwarmAI Local Agent Status\n'));
|
|
157
|
+
|
|
158
|
+
if (!isConfigured()) {
|
|
159
|
+
console.log(chalk.yellow(' Not configured. Run `swarmai-agent login` first.\n'));
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
console.log(chalk.white(` Server: ${config.server}`));
|
|
164
|
+
console.log(chalk.white(` Device: ${config.deviceName || 'Unknown'}`));
|
|
165
|
+
console.log(chalk.white(` Agent ID: ${config.agentId || 'N/A'}`));
|
|
166
|
+
console.log(chalk.white(` API Key: ${config.apiKey ? config.apiKey.substring(0, 12) + '...' : 'N/A'}`));
|
|
167
|
+
console.log(chalk.white(` Auto-start: ${isStartupEnabled() ? chalk.green('Enabled') : chalk.gray('Disabled')}`));
|
|
168
|
+
console.log(chalk.white(` Config file: ${CONFIG_FILE}`));
|
|
169
|
+
console.log();
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// ==================
|
|
173
|
+
// LOGOUT
|
|
174
|
+
// ==================
|
|
175
|
+
program
|
|
176
|
+
.command('logout')
|
|
177
|
+
.description('Remove local configuration')
|
|
178
|
+
.action(() => {
|
|
179
|
+
const config = loadConfig();
|
|
180
|
+
if (config.apiKey) {
|
|
181
|
+
saveConfig({});
|
|
182
|
+
console.log(chalk.green('\n Logged out. Config cleared.\n'));
|
|
183
|
+
} else {
|
|
184
|
+
console.log(chalk.gray('\n Not logged in.\n'));
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// ==================
|
|
189
|
+
// MCP
|
|
190
|
+
// ==================
|
|
191
|
+
const mcpCommand = program
|
|
192
|
+
.command('mcp')
|
|
193
|
+
.description('Manage MCP (Model Context Protocol) servers');
|
|
194
|
+
|
|
195
|
+
mcpCommand
|
|
196
|
+
.command('add <nameOrRecipe>')
|
|
197
|
+
.description('Add an MCP server. Use a recipe name (playwright, filesystem, sqlite, git, docker) or provide custom config with --command')
|
|
198
|
+
.option('-c, --command <cmd>', 'Server command (e.g., "npx")')
|
|
199
|
+
.option('-a, --args <args>', 'Server arguments (comma-separated)')
|
|
200
|
+
.option('-e, --env <env>', 'Environment variables (KEY=VAL,KEY2=VAL2)')
|
|
201
|
+
.action((nameOrRecipe, opts) => {
|
|
202
|
+
const config = loadConfig();
|
|
203
|
+
const mcpServers = config.mcpServers || {};
|
|
204
|
+
|
|
205
|
+
const { MCPManager } = require('./mcpManager');
|
|
206
|
+
const recipe = MCPManager.getRecipe(nameOrRecipe);
|
|
207
|
+
|
|
208
|
+
if (recipe && !opts.command) {
|
|
209
|
+
mcpServers[nameOrRecipe] = {
|
|
210
|
+
command: recipe.command,
|
|
211
|
+
args: recipe.args,
|
|
212
|
+
env: {},
|
|
213
|
+
};
|
|
214
|
+
console.log(chalk.green(`\n MCP server "${nameOrRecipe}" added (recipe: ${recipe.description}).\n`));
|
|
215
|
+
} else if (opts.command) {
|
|
216
|
+
mcpServers[nameOrRecipe] = {
|
|
217
|
+
command: opts.command,
|
|
218
|
+
args: opts.args ? opts.args.split(',').map(s => s.trim()) : [],
|
|
219
|
+
env: opts.env ? parseEnvString(opts.env) : {},
|
|
220
|
+
};
|
|
221
|
+
console.log(chalk.green(`\n MCP server "${nameOrRecipe}" added (custom).\n`));
|
|
222
|
+
} else {
|
|
223
|
+
console.log(chalk.red(`\n Unknown recipe "${nameOrRecipe}". Provide --command for custom server.`));
|
|
224
|
+
console.log(chalk.gray(` Available recipes: ${MCPManager.getRecipeNames().join(', ')}\n`));
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
config.mcpServers = mcpServers;
|
|
229
|
+
saveConfig(config);
|
|
230
|
+
console.log(chalk.gray(' Restart the agent to activate: swarmai-agent start\n'));
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
mcpCommand
|
|
234
|
+
.command('remove <name>')
|
|
235
|
+
.description('Remove an MCP server')
|
|
236
|
+
.action((name) => {
|
|
237
|
+
const config = loadConfig();
|
|
238
|
+
const mcpServers = config.mcpServers || {};
|
|
239
|
+
|
|
240
|
+
if (!mcpServers[name]) {
|
|
241
|
+
console.log(chalk.yellow(`\n MCP server "${name}" not found.\n`));
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
delete mcpServers[name];
|
|
246
|
+
config.mcpServers = mcpServers;
|
|
247
|
+
saveConfig(config);
|
|
248
|
+
console.log(chalk.green(`\n MCP server "${name}" removed.\n`));
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
mcpCommand
|
|
252
|
+
.command('list')
|
|
253
|
+
.description('List configured MCP servers and available recipes')
|
|
254
|
+
.action(() => {
|
|
255
|
+
const config = loadConfig();
|
|
256
|
+
const mcpServers = config.mcpServers || {};
|
|
257
|
+
const names = Object.keys(mcpServers);
|
|
258
|
+
|
|
259
|
+
console.log(chalk.cyan.bold('\n Configured MCP Servers\n'));
|
|
260
|
+
|
|
261
|
+
if (names.length === 0) {
|
|
262
|
+
console.log(chalk.gray(' None configured.\n'));
|
|
263
|
+
} else {
|
|
264
|
+
for (const name of names) {
|
|
265
|
+
const s = mcpServers[name];
|
|
266
|
+
console.log(chalk.white(` ${chalk.green('*')} ${name}`));
|
|
267
|
+
console.log(chalk.gray(` ${s.command} ${(s.args || []).join(' ')}`));
|
|
268
|
+
}
|
|
269
|
+
console.log();
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
console.log(chalk.cyan.bold(' Available Recipes\n'));
|
|
273
|
+
const { MCPManager } = require('./mcpManager');
|
|
274
|
+
for (const recipe of MCPManager.getRecipeList()) {
|
|
275
|
+
const installed = mcpServers[recipe.name] ? chalk.green(' (installed)') : '';
|
|
276
|
+
console.log(chalk.white(` ${recipe.name}${installed}`));
|
|
277
|
+
console.log(chalk.gray(` ${recipe.description}`));
|
|
278
|
+
}
|
|
279
|
+
console.log(chalk.gray(`\n Add with: swarmai-agent mcp add <recipe>\n`));
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
return program;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Parse environment variable string (KEY=VAL,KEY2=VAL2)
|
|
287
|
+
*/
|
|
288
|
+
function parseEnvString(envStr) {
|
|
289
|
+
const result = {};
|
|
290
|
+
for (const part of envStr.split(',')) {
|
|
291
|
+
const [key, ...valParts] = part.split('=');
|
|
292
|
+
if (key) result[key.trim()] = valParts.join('=').trim();
|
|
293
|
+
}
|
|
294
|
+
return result;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function timestamp() {
|
|
298
|
+
return new Date().toLocaleTimeString();
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Check for agent updates (non-blocking, best-effort)
|
|
303
|
+
*/
|
|
304
|
+
async function checkForUpdates(serverUrl) {
|
|
305
|
+
try {
|
|
306
|
+
const pkg = require('../package.json');
|
|
307
|
+
const currentVersion = pkg.version;
|
|
308
|
+
|
|
309
|
+
const http = serverUrl.startsWith('https') ? require('https') : require('http');
|
|
310
|
+
const url = new URL('/api/local-agents/version', serverUrl);
|
|
311
|
+
|
|
312
|
+
return new Promise((resolve) => {
|
|
313
|
+
const req = http.get(url, { timeout: 5000 }, (res) => {
|
|
314
|
+
let data = '';
|
|
315
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
316
|
+
res.on('end', () => {
|
|
317
|
+
try {
|
|
318
|
+
const { latestVersion, downloadUrl } = JSON.parse(data);
|
|
319
|
+
if (latestVersion && latestVersion !== currentVersion) {
|
|
320
|
+
console.log(chalk.yellow(`\n Update available: v${currentVersion} → v${latestVersion}`));
|
|
321
|
+
if (downloadUrl) console.log(chalk.gray(` Download: ${downloadUrl}`));
|
|
322
|
+
console.log();
|
|
323
|
+
}
|
|
324
|
+
} catch { /* ignore parse errors */ }
|
|
325
|
+
resolve();
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
req.on('error', () => resolve());
|
|
329
|
+
req.on('timeout', () => { req.destroy(); resolve(); });
|
|
330
|
+
});
|
|
331
|
+
} catch { /* non-critical */ }
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
module.exports = { createCli };
|