@geminilight/mindos 0.1.8 → 0.1.9

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 CHANGED
@@ -30,666 +30,23 @@
30
30
  * mindos config validate — validate config file
31
31
  */
32
32
 
33
- import { execSync, spawn } from 'node:child_process';
34
- import { existsSync, readFileSync, writeFileSync, rmSync, mkdirSync } from 'node:fs';
35
- import { resolve, dirname } from 'node:path';
36
- import { homedir, networkInterfaces } from 'node:os';
37
- import { fileURLToPath } from 'node:url';
38
- import { createConnection } from 'node:net';
39
-
40
- const __dirname = dirname(fileURLToPath(import.meta.url));
41
- const ROOT = resolve(__dirname, '..');
42
- const CONFIG_PATH = resolve(homedir(), '.mindos', 'config.json');
43
- const PID_PATH = resolve(homedir(), '.mindos', 'mindos.pid');
44
- const BUILD_STAMP = resolve(ROOT, 'app', '.next', '.mindos-build-version');
45
-
46
- // ── Colors ────────────────────────────────────────────────────────────────────
47
-
48
- const isTTY = process.stdout.isTTY;
49
- const bold = (s) => isTTY ? `\x1b[1m${s}\x1b[0m` : s;
50
- const dim = (s) => isTTY ? `\x1b[2m${s}\x1b[0m` : s;
51
- const cyan = (s) => isTTY ? `\x1b[36m${s}\x1b[0m` : s;
52
- const green = (s) => isTTY ? `\x1b[32m${s}\x1b[0m` : s;
53
- const red = (s) => isTTY ? `\x1b[31m${s}\x1b[0m` : s;
54
- const yellow= (s) => isTTY ? `\x1b[33m${s}\x1b[0m` : s;
55
-
56
- // ── Config ────────────────────────────────────────────────────────────────────
57
-
58
- function loadConfig() {
59
- if (!existsSync(CONFIG_PATH)) return;
60
- let config;
61
- try {
62
- config = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8'));
63
- } catch {
64
- console.error(`Warning: failed to parse ${CONFIG_PATH}`);
65
- return;
66
- }
67
-
68
- const set = (key, val) => {
69
- if (val && !process.env[key]) process.env[key] = String(val);
70
- };
71
-
72
- set('MIND_ROOT', config.mindRoot);
73
- set('MINDOS_WEB_PORT', config.port);
74
- set('MINDOS_MCP_PORT', config.mcpPort);
75
- set('AUTH_TOKEN', config.authToken);
76
- set('WEB_PASSWORD', config.webPassword);
77
- set('AI_PROVIDER', config.ai?.provider);
78
-
79
- const providers = config.ai?.providers;
80
- if (providers) {
81
- set('ANTHROPIC_API_KEY', providers.anthropic?.apiKey);
82
- set('ANTHROPIC_MODEL', providers.anthropic?.model);
83
- set('OPENAI_API_KEY', providers.openai?.apiKey);
84
- set('OPENAI_MODEL', providers.openai?.model);
85
- set('OPENAI_BASE_URL', providers.openai?.baseUrl);
86
- } else {
87
- set('ANTHROPIC_API_KEY', config.ai?.anthropicApiKey);
88
- set('ANTHROPIC_MODEL', config.ai?.anthropicModel);
89
- set('OPENAI_API_KEY', config.ai?.openaiApiKey);
90
- set('OPENAI_MODEL', config.ai?.openaiModel);
91
- set('OPENAI_BASE_URL', config.ai?.openaiBaseUrl);
92
- }
93
- }
94
-
95
- function getStartMode() {
96
- try {
97
- return JSON.parse(readFileSync(CONFIG_PATH, 'utf-8')).startMode || 'start';
98
- } catch {
99
- return 'start';
100
- }
101
- }
102
-
103
- // ── Build helpers ─────────────────────────────────────────────────────────────
104
-
105
- function needsBuild() {
106
- const nextDir = resolve(ROOT, 'app', '.next');
107
- if (!existsSync(nextDir)) return true;
108
- try {
109
- const builtVersion = readFileSync(BUILD_STAMP, 'utf-8').trim();
110
- const currentVersion = JSON.parse(readFileSync(resolve(ROOT, 'package.json'), 'utf-8')).version;
111
- return builtVersion !== currentVersion;
112
- } catch {
113
- return true;
114
- }
115
- }
116
-
117
- function writeBuildStamp() {
118
- const version = JSON.parse(readFileSync(resolve(ROOT, 'package.json'), 'utf-8')).version;
119
- writeFileSync(BUILD_STAMP, version, 'utf-8');
120
- }
121
-
122
- function clearBuildLock() {
123
- const lockFile = resolve(ROOT, 'app', '.next', 'lock');
124
- if (existsSync(lockFile)) {
125
- rmSync(lockFile, { force: true });
126
- }
127
- }
128
-
129
- function ensureAppDeps() {
130
- // When installed as a global npm package, app/node_modules may not exist.
131
- // next (and other deps) must be resolvable from app/ for Turbopack to work.
132
- const appNext = resolve(ROOT, 'app', 'node_modules', 'next', 'package.json');
133
- if (!existsSync(appNext)) {
134
- // Check npm is accessible before trying to run it.
135
- try {
136
- execSync('npm --version', { stdio: 'pipe' });
137
- } catch {
138
- console.error(red('\n✘ npm not found in PATH.\n'));
139
- console.error(' MindOS needs npm to install its app dependencies on first run.');
140
- console.error(' This usually means Node.js is installed via a version manager (nvm, fnm, volta, etc.)');
141
- console.error(' that only loads in interactive shells, but not in /bin/sh.\n');
142
- console.error(' Fix: add your Node.js bin directory to a profile that /bin/sh reads (~/.profile).');
143
- console.error(' Example:');
144
- console.error(dim(' echo \'export PATH="$HOME/.nvm/versions/node/$(node --version)/bin:$PATH"\' >> ~/.profile'));
145
- console.error(dim(' source ~/.profile\n'));
146
- console.error(' Then run `mindos start` again.\n');
147
- process.exit(1);
148
- }
149
- console.log(yellow('Installing app dependencies (first run)...\n'));
150
- // --no-workspaces: prevent npm from hoisting deps to monorepo root.
151
- // When globally installed, deps must live in app/node_modules/ so that
152
- // Turbopack can resolve next/package.json from the app/ project directory.
153
- run('npm install --prefer-offline --no-workspaces', resolve(ROOT, 'app'));
154
- }
155
- }
156
-
157
- // ── Port check ────────────────────────────────────────────────────────────────
158
-
159
- function isPortInUse(port) {
160
- return new Promise((resolve) => {
161
- const sock = createConnection({ port, host: '127.0.0.1' });
162
- sock.once('connect', () => { sock.destroy(); resolve(true); });
163
- sock.once('error', () => { sock.destroy(); resolve(false); });
164
- });
165
- }
166
-
167
- async function assertPortFree(port, name) {
168
- if (await isPortInUse(port)) {
169
- console.error(`\n${red('✘')} ${bold(`Port ${port} is already in use`)} ${dim(`(${name})`)}`);
170
- console.error(`\n ${dim('Stop MindOS:')} mindos stop`);
171
- console.error(` ${dim('Find the process:')} lsof -i :${port}\n`);
172
- process.exit(1);
173
- }
174
- }
175
-
176
- // ── PID file ──────────────────────────────────────────────────────────────────
177
-
178
- function savePids(...pids) {
179
- writeFileSync(PID_PATH, pids.filter(Boolean).join('\n'), 'utf-8');
180
- }
181
-
182
- function loadPids() {
183
- if (!existsSync(PID_PATH)) return [];
184
- return readFileSync(PID_PATH, 'utf-8').split('\n').map(Number).filter(Boolean);
185
- }
186
-
187
- function clearPids() {
188
- if (existsSync(PID_PATH)) rmSync(PID_PATH);
189
- }
190
-
191
- // ── Stop ──────────────────────────────────────────────────────────────────────
192
-
193
- function stopMindos() {
194
- const pids = loadPids();
195
- if (!pids.length) {
196
- console.log(yellow('No PID file found, trying pattern-based stop...'));
197
- try { execSync('pkill -f "next start|next dev" 2>/dev/null || true', { stdio: 'inherit' }); } catch {}
198
- try { execSync('pkill -f "mcp/src/index" 2>/dev/null || true', { stdio: 'inherit' }); } catch {}
199
- console.log(green('✔ Done'));
200
- return;
201
- }
202
- let stopped = 0;
203
- for (const pid of pids) {
204
- try {
205
- process.kill(pid, 'SIGTERM');
206
- stopped++;
207
- } catch {
208
- // process already gone — ignore
209
- }
210
- }
211
- clearPids();
212
- console.log(stopped
213
- ? green(`✔ Stopped ${stopped} process${stopped > 1 ? 'es' : ''}`)
214
- : dim('No running processes found'));
215
- }
216
-
217
- // ── Daemon / gateway helpers ───────────────────────────────────────────────────
218
-
219
- const MINDOS_DIR = resolve(homedir(), '.mindos');
220
- const LOG_PATH = resolve(MINDOS_DIR, 'mindos.log');
221
- const CLI_PATH = resolve(__dirname, 'cli.js');
222
- const NODE_BIN = process.execPath;
223
-
224
- function getPlatform() {
225
- if (process.platform === 'darwin') return 'launchd';
226
- if (process.platform === 'linux') return 'systemd';
227
- return null;
228
- }
229
-
230
- function ensureMindosDir() {
231
- if (!existsSync(MINDOS_DIR)) mkdirSync(MINDOS_DIR, { recursive: true });
232
- }
233
-
234
- // ── systemd (Linux) ───────────────────────────────────────────────────────────
235
-
236
- const SYSTEMD_DIR = resolve(homedir(), '.config', 'systemd', 'user');
237
- const SYSTEMD_UNIT = resolve(SYSTEMD_DIR, 'mindos.service');
238
-
239
- const systemd = {
240
- install() {
241
- if (!existsSync(SYSTEMD_DIR)) mkdirSync(SYSTEMD_DIR, { recursive: true });
242
- ensureMindosDir();
243
- const currentPath = process.env.PATH ?? '/usr/local/bin:/usr/bin:/bin';
244
- const unit = [
245
- '[Unit]',
246
- 'Description=MindOS app + MCP server',
247
- 'After=network.target',
248
- '',
249
- '[Service]',
250
- 'Type=simple',
251
- `ExecStart=${NODE_BIN} ${CLI_PATH} start`,
252
- 'Restart=on-failure',
253
- 'RestartSec=3',
254
- `Environment=HOME=${homedir()}`,
255
- `Environment=PATH=${currentPath}`,
256
- `EnvironmentFile=-${resolve(MINDOS_DIR, 'env')}`,
257
- `StandardOutput=append:${LOG_PATH}`,
258
- `StandardError=append:${LOG_PATH}`,
259
- '',
260
- '[Install]',
261
- 'WantedBy=default.target',
262
- ].join('\n');
263
- writeFileSync(SYSTEMD_UNIT, unit, 'utf-8');
264
- console.log(green(`✔ Wrote ${SYSTEMD_UNIT}`));
265
- execSync('systemctl --user daemon-reload', { stdio: 'inherit' });
266
- execSync('systemctl --user enable mindos', { stdio: 'inherit' });
267
- console.log(green('✔ Service installed and enabled'));
268
- },
269
-
270
- async start() {
271
- execSync('systemctl --user start mindos', { stdio: 'inherit' });
272
- // Wait up to 10s for the service to become active
273
- const ok = await waitForService(() => {
274
- try {
275
- const out = execSync('systemctl --user is-active mindos', { encoding: 'utf-8' }).trim();
276
- return out === 'active';
277
- } catch { return false; }
278
- });
279
- if (!ok) {
280
- console.error(red('\n✘ Service failed to start. Last log output:'));
281
- try { execSync(`journalctl --user -u mindos -n 30 --no-pager`, { stdio: 'inherit' }); } catch {}
282
- process.exit(1);
283
- }
284
- console.log(green('✔ Service started'));
285
- },
286
-
287
- stop() {
288
- execSync('systemctl --user stop mindos', { stdio: 'inherit' });
289
- console.log(green('✔ Service stopped'));
290
- },
291
-
292
- status() {
293
- try {
294
- execSync('systemctl --user status mindos', { stdio: 'inherit' });
295
- } catch { /* status exits non-zero when stopped */ }
296
- },
297
-
298
- logs() {
299
- execSync(`journalctl --user -u mindos -f`, { stdio: 'inherit' });
300
- },
301
-
302
- uninstall() {
303
- try {
304
- execSync('systemctl --user disable --now mindos', { stdio: 'inherit' });
305
- } catch { /* may already be stopped */ }
306
- if (existsSync(SYSTEMD_UNIT)) {
307
- rmSync(SYSTEMD_UNIT);
308
- console.log(green(`✔ Removed ${SYSTEMD_UNIT}`));
309
- }
310
- execSync('systemctl --user daemon-reload', { stdio: 'inherit' });
311
- console.log(green('✔ Service uninstalled'));
312
- },
313
- };
314
-
315
- // ── launchd (macOS) ───────────────────────────────────────────────────────────
316
-
317
- const LAUNCHD_DIR = resolve(homedir(), 'Library', 'LaunchAgents');
318
- const LAUNCHD_PLIST = resolve(LAUNCHD_DIR, 'com.mindos.app.plist');
319
- const LAUNCHD_LABEL = 'com.mindos.app';
320
-
321
- function launchctlUid() {
322
- return execSync('id -u').toString().trim();
323
- }
324
-
325
- const launchd = {
326
- install() {
327
- if (!existsSync(LAUNCHD_DIR)) mkdirSync(LAUNCHD_DIR, { recursive: true });
328
- ensureMindosDir();
329
- // Capture current PATH so the daemon can find npm/node even when launched by
330
- // launchd (which only sets a minimal PATH and doesn't source shell profiles).
331
- const currentPath = process.env.PATH ?? '/usr/local/bin:/usr/bin:/bin';
332
- const plist = `<?xml version="1.0" encoding="UTF-8"?>
333
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
334
- <plist version="1.0">
335
- <dict>
336
- <key>Label</key><string>${LAUNCHD_LABEL}</string>
337
- <key>ProgramArguments</key>
338
- <array>
339
- <string>${NODE_BIN}</string>
340
- <string>${CLI_PATH}</string>
341
- <string>start</string>
342
- </array>
343
- <key>RunAtLoad</key><true/>
344
- <key>KeepAlive</key><true/>
345
- <key>StandardOutPath</key><string>${LOG_PATH}</string>
346
- <key>StandardErrorPath</key><string>${LOG_PATH}</string>
347
- <key>EnvironmentVariables</key>
348
- <dict>
349
- <key>HOME</key><string>${homedir()}</string>
350
- <key>PATH</key><string>${currentPath}</string>
351
- </dict>
352
- </dict>
353
- </plist>
354
- `;
355
- writeFileSync(LAUNCHD_PLIST, plist, 'utf-8');
356
- console.log(green(`✔ Wrote ${LAUNCHD_PLIST}`));
357
- // Bootout first to ensure the new plist (with updated PATH) takes effect.
358
- // Safe to ignore errors here — service may not be loaded yet.
359
- try { execSync(`launchctl bootout gui/${launchctlUid()}/${LAUNCHD_LABEL}`, { stdio: 'pipe' }); } catch {}
360
- try {
361
- execSync(`launchctl bootstrap gui/${launchctlUid()} ${LAUNCHD_PLIST}`, { stdio: 'pipe' });
362
- } catch (e) {
363
- const msg = (e.stderr?.toString() ?? e.message ?? '').trim();
364
- console.error(red(`\n✘ launchctl bootstrap failed: ${msg}`));
365
- console.error(dim(' Try running: launchctl bootout gui/$(id -u)/com.mindos.app then retry.\n'));
366
- process.exit(1);
367
- }
368
- console.log(green('✔ Service installed'));
369
- },
370
-
371
- async start() {
372
- execSync(`launchctl kickstart -k gui/${launchctlUid()}/${LAUNCHD_LABEL}`, { stdio: 'inherit' });
373
- // Wait up to 10s for the service to become active
374
- const ok = await waitForService(() => {
375
- try {
376
- const out = execSync(`launchctl print gui/${launchctlUid()}/${LAUNCHD_LABEL}`, { encoding: 'utf-8' });
377
- return out.includes('state = running');
378
- } catch { return false; }
379
- });
380
- if (!ok) {
381
- console.error(red('\n✘ Service failed to start. Last log output:'));
382
- try { execSync(`tail -n 30 ${LOG_PATH}`, { stdio: 'inherit' }); } catch {}
383
- process.exit(1);
384
- }
385
- console.log(green('✔ Service started'));
386
- },
387
-
388
- stop() {
389
- try {
390
- execSync(`launchctl bootout gui/${launchctlUid()} ${LAUNCHD_PLIST}`, { stdio: 'inherit' });
391
- } catch { /* may not be running */ }
392
- console.log(green('✔ Service stopped'));
393
- },
394
-
395
- status() {
396
- try {
397
- execSync(`launchctl print gui/${launchctlUid()}/${LAUNCHD_LABEL}`, { stdio: 'inherit' });
398
- } catch {
399
- console.log(dim('Service is not running'));
400
- }
401
- },
402
-
403
- logs() {
404
- execSync(`tail -f ${LOG_PATH}`, { stdio: 'inherit' });
405
- },
406
-
407
- uninstall() {
408
- try {
409
- execSync(`launchctl bootout gui/${launchctlUid()} ${LAUNCHD_PLIST}`, { stdio: 'inherit' });
410
- } catch { /* may not be running */ }
411
- if (existsSync(LAUNCHD_PLIST)) {
412
- rmSync(LAUNCHD_PLIST);
413
- console.log(green(`✔ Removed ${LAUNCHD_PLIST}`));
414
- }
415
- console.log(green('✔ Service uninstalled'));
416
- },
417
- };
418
-
419
- // ── gateway dispatcher ────────────────────────────────────────────────────────
420
-
421
- async function waitForService(check, { retries = 10, intervalMs = 1000 } = {}) {
422
- for (let i = 0; i < retries; i++) {
423
- if (check()) return true;
424
- await new Promise(r => setTimeout(r, intervalMs));
425
- }
426
- return check();
427
- }
428
-
429
- async function waitForHttp(port, { retries = 120, intervalMs = 2000, label = 'service' } = {}) {
430
- process.stdout.write(cyan(` Waiting for ${label} to be ready`));
431
- for (let i = 0; i < retries; i++) {
432
- try {
433
- const { request } = await import('node:http');
434
- const ok = await new Promise((resolve) => {
435
- const req = request({ hostname: '127.0.0.1', port, path: '/', method: 'HEAD', timeout: 1500 },
436
- (res) => { res.resume(); resolve(res.statusCode < 500); });
437
- req.on('error', () => resolve(false));
438
- req.on('timeout', () => { req.destroy(); resolve(false); });
439
- req.end();
440
- });
441
- if (ok) { process.stdout.write(` ${green('✔')}\n`); return true; }
442
- } catch { /* not ready yet */ }
443
- process.stdout.write('.');
444
- await new Promise(r => setTimeout(r, intervalMs));
445
- }
446
- process.stdout.write(` ${red('✘')}\n`);
447
- return false;
448
- }
449
-
450
- async function runGatewayCommand(sub) {
451
- const platform = getPlatform();
452
- if (!platform) {
453
- console.error(red('Daemon mode is not supported on this platform (requires Linux/systemd or macOS/launchd)'));
454
- process.exit(1);
455
- }
456
- const impl = platform === 'systemd' ? systemd : launchd;
457
- const fn = impl[sub];
458
- if (!fn) {
459
- console.error(red(`Unknown gateway subcommand: ${sub}`));
460
- console.error(dim('Available: install | uninstall | start | stop | status | logs'));
461
- process.exit(1);
462
- }
463
- await fn();
464
- }
465
-
466
- // ── Startup info ──────────────────────────────────────────────────────────────
467
-
468
- function getLocalIP() {
469
- try {
470
- for (const ifaces of Object.values(networkInterfaces())) {
471
- for (const iface of ifaces) {
472
- if (iface.family === 'IPv4' && !iface.internal) return iface.address;
473
- }
474
- }
475
- } catch { /* ignore */ }
476
- return null;
477
- }
478
-
479
- function printStartupInfo(webPort, mcpPort) {
480
- let config = {};
481
- try { config = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8')); } catch { /* ignore */ }
482
- const authToken = config.authToken || '';
483
- const localIP = getLocalIP();
484
-
485
- const auth = authToken
486
- ? `,\n "headers": { "Authorization": "Bearer ${authToken}" }`
487
- : '';
488
- const block = (host) =>
489
- ` {\n "mcpServers": {\n "mindos": {\n "url": "http://${host}:${mcpPort}/mcp"${auth}\n }\n }\n }`;
490
-
491
- console.log(`\n${'─'.repeat(53)}`);
492
- console.log(`${bold('🧠 MindOS is starting')}\n`);
493
- console.log(` ${green('●')} Web UI ${cyan(`http://localhost:${webPort}`)}`);
494
- if (localIP) console.log(` ${cyan(`http://${localIP}:${webPort}`)}`);
495
- console.log(` ${green('●')} MCP ${cyan(`http://localhost:${mcpPort}/mcp`)}`);
496
- if (localIP) console.log(` ${cyan(`http://${localIP}:${mcpPort}/mcp`)}`);
497
- if (localIP) console.log(dim(`\n 💡 Running on a remote server? Open the Network URL (${localIP}) in your browser,\n or use SSH port forwarding: ssh -L ${webPort}:localhost:${webPort} user@${localIP}`));
498
- console.log();
499
- console.log(bold('Configure MCP in your Agent:'));
500
- console.log(dim(' Local (same machine):'));
501
- console.log(block('localhost'));
502
- if (localIP) {
503
- console.log(dim('\n Remote (other device):'));
504
- console.log(block(localIP));
505
- }
506
- if (authToken) {
507
- console.log(`\n 🔑 ${bold('Auth token:')} ${cyan(authToken)}`);
508
- console.log(dim(' Run `mindos token` anytime to view it again'));
509
- }
510
- console.log(dim('\n Install Skills (optional):'));
511
- console.log(dim(' npx skills add https://github.com/GeminiLight/MindOS --skill mindos -g -y'));
512
- console.log(`${'─'.repeat(53)}\n`);
513
- }
514
-
515
- // ── MCP spawn ─────────────────────────────────────────────────────────────────
516
-
517
- function spawnMcp(verbose = false) {
518
- const mcpPort = process.env.MINDOS_MCP_PORT || '8787';
519
- const webPort = process.env.MINDOS_WEB_PORT || '3000';
520
- const env = {
521
- ...process.env,
522
- MCP_PORT: mcpPort,
523
- MINDOS_URL: `http://localhost:${webPort}`,
524
- ...(verbose ? { MCP_VERBOSE: '1' } : {}),
525
- };
526
- const child = spawn('npx', ['tsx', 'src/index.ts'], {
527
- cwd: resolve(ROOT, 'mcp'),
528
- stdio: 'inherit',
529
- env,
530
- });
531
- child.on('error', (err) => {
532
- if (err.message.includes('EADDRINUSE')) {
533
- console.error(`\n${red('✘')} ${bold(`MCP port ${mcpPort} is already in use`)}`);
534
- console.error(` ${dim('Run:')} mindos stop\n`);
535
- } else {
536
- console.error(red('MCP server error:'), err.message);
537
- }
538
- });
539
- return child;
540
- }
541
-
542
- // ── mcp install ───────────────────────────────────────────────────────────────
543
-
544
- const MCP_AGENTS = {
545
- 'claude-code': { name: 'Claude Code', project: '.mcp.json', global: '~/.claude.json', key: 'mcpServers' },
546
- 'claude-desktop': { name: 'Claude Desktop', project: null, global: process.platform === 'darwin' ? '~/Library/Application Support/Claude/claude_desktop_config.json' : '~/.config/Claude/claude_desktop_config.json', key: 'mcpServers' },
547
- 'cursor': { name: 'Cursor', project: '.cursor/mcp.json', global: '~/.cursor/mcp.json', key: 'mcpServers' },
548
- 'windsurf': { name: 'Windsurf', project: null, global: '~/.codeium/windsurf/mcp_config.json', key: 'mcpServers' },
549
- 'cline': { name: 'Cline', project: null, global: process.platform === 'darwin' ? '~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json' : '~/.config/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json', key: 'mcpServers' },
550
- 'trae': { name: 'Trae', project: '.trae/mcp.json', global: '~/.trae/mcp.json', key: 'mcpServers' },
551
- 'gemini-cli': { name: 'Gemini CLI', project: '.gemini/settings.json', global: '~/.gemini/settings.json', key: 'mcpServers' },
552
- 'openclaw': { name: 'OpenClaw', project: null, global: '~/.openclaw/mcp.json', key: 'mcpServers' },
553
- 'codebuddy': { name: 'CodeBuddy', project: null, global: '~/.claude-internal/.claude.json', key: 'mcpServers' },
554
- };
555
-
556
- function expandHome(p) {
557
- return p.startsWith('~/') ? resolve(homedir(), p.slice(2)) : p;
558
- }
559
-
560
- async function mcpInstall() {
561
- const args = process.argv.slice(4);
562
-
563
- // parse flags
564
- const hasGlobalFlag = args.includes('-g') || args.includes('--global');
565
- const hasYesFlag = args.includes('-y') || args.includes('--yes');
566
- const transportIdx = args.findIndex(a => a === '--transport');
567
- const urlIdx = args.findIndex(a => a === '--url');
568
- const tokenIdx = args.findIndex(a => a === '--token');
569
- const transportArg = transportIdx >= 0 ? args[transportIdx + 1] : null;
570
- const urlArg = urlIdx >= 0 ? args[urlIdx + 1] : null;
571
- const tokenArg = tokenIdx >= 0 ? args[tokenIdx + 1] : null;
572
-
573
- // agent positional arg: first non-flag arg after "mcp install"
574
- const agentArg = args.find(a => !a.startsWith('-')) ?? null;
575
-
576
- const readline = await import('node:readline');
577
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
578
- const ask = (q) => new Promise(r => rl.question(q, r));
579
- const choose = async (prompt, options, defaultIdx = 0) => {
580
- if (hasYesFlag) return options[defaultIdx];
581
- console.log(`\n${bold(prompt)}\n`);
582
- options.forEach((o, i) => console.log(` ${dim(`${i + 1}.`)} ${o.label} ${o.hint ? dim(`(${o.hint})`) : ''}`));
583
- const ans = await ask(`\n${bold(`Enter number`)} ${dim(`[${defaultIdx + 1}]:`)} `);
584
- const idx = ans.trim() === '' ? defaultIdx : parseInt(ans.trim(), 10) - 1;
585
- return options[idx >= 0 && idx < options.length ? idx : defaultIdx];
586
- };
587
-
588
- console.log(`\n${bold('🔌 MindOS MCP Install')}\n`);
589
-
590
- // ── 1. agent ────────────────────────────────────────────────────────────────
591
- let agentKey = agentArg;
592
- if (!agentKey) {
593
- const keys = Object.keys(MCP_AGENTS);
594
- const picked = await choose('Which Agent would you like to configure?',
595
- keys.map(k => ({ label: MCP_AGENTS[k].name, hint: k, value: k })));
596
- agentKey = picked.value;
597
- }
598
-
599
- const agent = MCP_AGENTS[agentKey];
600
- if (!agent) {
601
- rl.close();
602
- console.error(red(`\nUnknown agent: ${agentKey}`));
603
- console.error(dim(`Supported: ${Object.keys(MCP_AGENTS).join(', ')}`));
604
- process.exit(1);
605
- }
606
-
607
- // ── 2. scope (only ask if agent supports both) ───────────────────────────────
608
- let isGlobal = hasGlobalFlag;
609
- if (!hasGlobalFlag) {
610
- if (agent.project && agent.global) {
611
- const picked = await choose('Install scope?', [
612
- { label: 'Project', hint: agent.project, value: 'project' },
613
- { label: 'Global', hint: agent.global, value: 'global' },
614
- ]);
615
- isGlobal = picked.value === 'global';
616
- } else {
617
- // agent only supports one scope, no need to ask
618
- isGlobal = !agent.project;
619
- }
620
- }
621
-
622
- const configPath = isGlobal ? agent.global : agent.project;
623
- if (!configPath) {
624
- rl.close();
625
- console.error(red(`${agent.name} does not support ${isGlobal ? 'global' : 'project'} scope.`));
626
- process.exit(1);
627
- }
628
-
629
- // ── 3. transport ─────────────────────────────────────────────────────────────
630
- let transport = transportArg;
631
- if (!transport) {
632
- const picked = await choose('Transport type?', [
633
- { label: 'stdio', hint: 'local, no server process needed (recommended)' },
634
- { label: 'http', hint: 'URL-based, use when server is running separately or remotely' },
635
- ]);
636
- transport = picked.label;
637
- }
638
-
639
- // ── 4. url + token (only for http) ───────────────────────────────────────────
640
- let url = urlArg;
641
- let token = tokenArg;
642
-
643
- if (transport === 'http') {
644
- if (!url) {
645
- url = hasYesFlag ? 'http://localhost:8787/mcp' : (await ask(`${bold('MCP URL')} ${dim('[http://localhost:8787/mcp]:')} `)).trim() || 'http://localhost:8787/mcp';
646
- }
647
-
648
- if (!token) {
649
- // try reading from config.json first
650
- try { token = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8')).authToken || ''; } catch {}
651
- if (!token && !hasYesFlag) {
652
- token = (await ask(`${bold('Auth token')} ${dim('(leave blank to skip):')} `)).trim();
653
- } else if (token) {
654
- console.log(dim(` Using auth token from ~/.mindos/config.json`));
655
- }
656
- }
657
- }
658
-
659
- rl.close();
660
-
661
- // ── build entry ──────────────────────────────────────────────────────────────
662
- const entry = transport === 'stdio'
663
- ? { type: 'stdio', command: 'mindos', args: ['mcp'], env: { MCP_TRANSPORT: 'stdio' } }
664
- : token
665
- ? { url, headers: { Authorization: `Bearer ${token}` } }
666
- : { url };
667
-
668
- // ── read + merge existing config ─────────────────────────────────────────────
669
- const absPath = expandHome(configPath);
670
- let config = {};
671
- if (existsSync(absPath)) {
672
- try { config = JSON.parse(readFileSync(absPath, 'utf-8')); } catch {
673
- console.error(red(`\nFailed to parse existing config: ${absPath}`));
674
- process.exit(1);
675
- }
676
- }
677
-
678
- if (!config[agent.key]) config[agent.key] = {};
679
- const existed = !!config[agent.key].mindos;
680
- config[agent.key].mindos = entry;
681
-
682
- // ── preview + write ──────────────────────────────────────────────────────────
683
- console.log(`\n${bold('Preview:')} ${dim(absPath)}\n`);
684
- console.log(dim(JSON.stringify({ [agent.key]: { mindos: entry } }, null, 2)));
685
-
686
- const dir = resolve(absPath, '..');
687
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
688
- writeFileSync(absPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
689
-
690
- console.log(`\n${green('✔')} ${existed ? 'Updated' : 'Installed'} MindOS MCP for ${bold(agent.name)}`);
691
- console.log(dim(` Config: ${absPath}\n`));
692
- }
33
+ import { execSync } from 'node:child_process';
34
+ import { existsSync, readFileSync, writeFileSync, rmSync } from 'node:fs';
35
+ import { resolve } from 'node:path';
36
+ import { homedir } from 'node:os';
37
+
38
+ import { ROOT, CONFIG_PATH, BUILD_STAMP, LOG_PATH } from './lib/constants.js';
39
+ import { bold, dim, cyan, green, red, yellow } from './lib/colors.js';
40
+ import { run } from './lib/utils.js';
41
+ import { loadConfig, getStartMode } from './lib/config.js';
42
+ import { needsBuild, writeBuildStamp, clearBuildLock, cleanNextDir, ensureAppDeps } from './lib/build.js';
43
+ import { isPortInUse, assertPortFree } from './lib/port.js';
44
+ import { savePids, clearPids } from './lib/pid.js';
45
+ import { stopMindos } from './lib/stop.js';
46
+ import { getPlatform, ensureMindosDir, waitForHttp, runGatewayCommand } from './lib/gateway.js';
47
+ import { printStartupInfo } from './lib/startup.js';
48
+ import { spawnMcp } from './lib/mcp-spawn.js';
49
+ import { mcpInstall } from './lib/mcp-install.js';
693
50
 
694
51
  // ── Commands ──────────────────────────────────────────────────────────────────
695
52
 
@@ -777,7 +134,7 @@ const commands = {
777
134
  ensureAppDeps();
778
135
  if (needsBuild()) {
779
136
  console.log(yellow('Building MindOS (first run or new version detected)...\n'));
780
- clearBuildLock();
137
+ cleanNextDir();
781
138
  run('npx next build', resolve(ROOT, 'app'));
782
139
  writeBuildStamp();
783
140
  }
@@ -791,15 +148,27 @@ const commands = {
791
148
  // ── build ──────────────────────────────────────────────────────────────────
792
149
  build: () => {
793
150
  ensureAppDeps();
794
- clearBuildLock();
151
+ cleanNextDir();
795
152
  run(`npx next build ${extra}`, resolve(ROOT, 'app'));
796
153
  writeBuildStamp();
797
154
  },
798
155
 
799
156
  mcp: async () => {
800
157
  const sub = process.argv[3];
801
- if (sub === 'install') { await mcpInstall(); return; }
158
+ const restArgs = process.argv.slice(3);
159
+ const hasInstallFlags = restArgs.some(a => ['-g', '--global', '-y', '--yes'].includes(a));
160
+ if (sub === 'install' || hasInstallFlags) { await mcpInstall(); return; }
802
161
  loadConfig();
162
+ const mcpSdk = resolve(ROOT, 'mcp', 'node_modules', '@modelcontextprotocol', 'sdk', 'package.json');
163
+ if (!existsSync(mcpSdk)) {
164
+ console.log(yellow('Installing MCP dependencies (first run)...\n'));
165
+ run('npm install --prefer-offline --no-workspaces', resolve(ROOT, 'mcp'));
166
+ }
167
+ // Map config env vars to what the MCP server expects
168
+ const mcpPort = process.env.MINDOS_MCP_PORT || '8787';
169
+ const webPort = process.env.MINDOS_WEB_PORT || '3000';
170
+ process.env.MCP_PORT = mcpPort;
171
+ process.env.MINDOS_URL = `http://localhost:${webPort}`;
803
172
  run(`npx tsx src/index.ts`, resolve(ROOT, 'mcp'));
804
173
  },
805
174
 
@@ -948,7 +317,8 @@ ${dim('Shortcut: mindos start --daemon → install + start in one step')}
948
317
  }
949
318
  } else if (platform === 'launchd') {
950
319
  try {
951
- execSync(`launchctl print gui/${launchctlUid()}/com.mindos.app`, { stdio: 'pipe' });
320
+ const uid = execSync('id -u').toString().trim();
321
+ execSync(`launchctl print gui/${uid}/com.mindos.app`, { stdio: 'pipe' });
952
322
  ok('LaunchAgent com.mindos.app is loaded');
953
323
  } catch {
954
324
  warn('LaunchAgent com.mindos.app is not loaded (run `mindos gateway start` to start)');
@@ -973,7 +343,6 @@ ${dim('Shortcut: mindos start --daemon → install + start in one step')}
973
343
  console.error(red('Update failed. Try: npm install -g @geminilight/mindos@latest'));
974
344
  process.exit(1);
975
345
  }
976
- // Clear build stamp so next `mindos start` rebuilds if version changed
977
346
  if (existsSync(BUILD_STAMP)) rmSync(BUILD_STAMP);
978
347
  const newVersion = (() => {
979
348
  try { return JSON.parse(readFileSync(resolve(ROOT, 'package.json'), 'utf-8')).version; } catch { return '?'; }
@@ -985,19 +354,22 @@ ${dim('Shortcut: mindos start --daemon → install + start in one step')}
985
354
  return;
986
355
  }
987
356
 
988
- // If daemon is running, restart it so the new version takes effect immediately
989
- const platform = getPlatform();
357
+ const updatePlatform = getPlatform();
990
358
  let daemonRunning = false;
991
- if (platform === 'systemd') {
359
+ if (updatePlatform === 'systemd') {
992
360
  try { execSync('systemctl --user is-active mindos', { stdio: 'pipe' }); daemonRunning = true; } catch {}
993
- } else if (platform === 'launchd') {
994
- try { execSync(`launchctl print gui/${launchctlUid()}/com.mindos.app`, { stdio: 'pipe' }); daemonRunning = true; } catch {}
361
+ } else if (updatePlatform === 'launchd') {
362
+ try {
363
+ const uid = execSync('id -u').toString().trim();
364
+ execSync(`launchctl print gui/${uid}/com.mindos.app`, { stdio: 'pipe' });
365
+ daemonRunning = true;
366
+ } catch {}
995
367
  }
996
368
 
997
369
  if (daemonRunning) {
998
370
  console.log(cyan('\n Daemon is running — restarting to apply the new version...'));
999
371
  await runGatewayCommand('stop');
1000
- await runGatewayCommand('install'); // regenerate plist/unit with updated PATH and binary
372
+ await runGatewayCommand('install');
1001
373
  await runGatewayCommand('start');
1002
374
  const webPort = process.env.MINDOS_WEB_PORT || '3000';
1003
375
  console.log(dim(' (Waiting for Web UI to come back up...)'));
@@ -1049,7 +421,6 @@ ${dim('Shortcut: mindos start --daemon → install + start in one step')}
1049
421
  console.error(red('Failed to parse config file.'));
1050
422
  process.exit(1);
1051
423
  }
1052
- // Mask API keys for display
1053
424
  const display = JSON.parse(JSON.stringify(config));
1054
425
  if (display.ai?.providers?.anthropic?.apiKey)
1055
426
  display.ai.providers.anthropic.apiKey = maskKey(display.ai.providers.anthropic.apiKey);
@@ -1122,14 +493,12 @@ ${dim('Shortcut: mindos start --daemon → install + start in one step')}
1122
493
  console.error(red('Failed to parse config file.'));
1123
494
  process.exit(1);
1124
495
  }
1125
- // Support dot-notation for nested keys (e.g. ai.provider)
1126
496
  const parts = key.split('.');
1127
497
  let obj = config;
1128
498
  for (let i = 0; i < parts.length - 1; i++) {
1129
499
  if (typeof obj[parts[i]] !== 'object' || !obj[parts[i]]) obj[parts[i]] = {};
1130
500
  obj = obj[parts[i]];
1131
501
  }
1132
- // Coerce numbers
1133
502
  const coerced = isNaN(Number(val)) ? val : Number(val);
1134
503
  obj[parts[parts.length - 1]] = coerced;
1135
504
  writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), 'utf-8');
@@ -1188,13 +557,3 @@ ${row('mindos', 'Start using mode saved in ~/.mindos/
1188
557
  }
1189
558
 
1190
559
  commands[resolvedCmd]();
1191
-
1192
- // ── run helper ────────────────────────────────────────────────────────────────
1193
-
1194
- function run(command, cwd = ROOT) {
1195
- try {
1196
- execSync(command, { cwd, stdio: 'inherit', env: process.env });
1197
- } catch {
1198
- process.exit(1);
1199
- }
1200
- }