@geminilight/mindos 0.1.7 → 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/README.md +62 -42
- package/README_zh.md +62 -42
- package/assets/demo-flow-zh.html +30 -30
- package/assets/images/demo-flow-dark.png +0 -0
- package/assets/images/demo-flow-light.png +0 -0
- package/assets/images/demo-flow-zh-dark.png +0 -0
- package/assets/images/demo-flow-zh-light.png +0 -0
- package/bin/cli.js +49 -532
- package/bin/lib/build.js +59 -0
- package/bin/lib/colors.js +7 -0
- package/bin/lib/config.js +47 -0
- package/bin/lib/constants.js +13 -0
- package/bin/lib/gateway.js +244 -0
- package/bin/lib/mcp-install.js +156 -0
- package/bin/lib/mcp-spawn.js +36 -0
- package/bin/lib/pid.js +15 -0
- package/bin/lib/port.js +19 -0
- package/bin/lib/startup.js +51 -0
- package/bin/lib/stop.js +27 -0
- package/bin/lib/utils.js +16 -0
- package/package.json +1 -1
package/bin/cli.js
CHANGED
|
@@ -30,514 +30,23 @@
|
|
|
30
30
|
* mindos config validate — validate config file
|
|
31
31
|
*/
|
|
32
32
|
|
|
33
|
-
import { execSync
|
|
34
|
-
import { existsSync, readFileSync, writeFileSync, rmSync
|
|
35
|
-
import { resolve
|
|
36
|
-
import { homedir
|
|
37
|
-
|
|
38
|
-
import {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
}
|
|
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';
|
|
541
50
|
|
|
542
51
|
// ── Commands ──────────────────────────────────────────────────────────────────
|
|
543
52
|
|
|
@@ -625,7 +134,7 @@ const commands = {
|
|
|
625
134
|
ensureAppDeps();
|
|
626
135
|
if (needsBuild()) {
|
|
627
136
|
console.log(yellow('Building MindOS (first run or new version detected)...\n'));
|
|
628
|
-
|
|
137
|
+
cleanNextDir();
|
|
629
138
|
run('npx next build', resolve(ROOT, 'app'));
|
|
630
139
|
writeBuildStamp();
|
|
631
140
|
}
|
|
@@ -639,12 +148,29 @@ const commands = {
|
|
|
639
148
|
// ── build ──────────────────────────────────────────────────────────────────
|
|
640
149
|
build: () => {
|
|
641
150
|
ensureAppDeps();
|
|
642
|
-
|
|
151
|
+
cleanNextDir();
|
|
643
152
|
run(`npx next build ${extra}`, resolve(ROOT, 'app'));
|
|
644
153
|
writeBuildStamp();
|
|
645
154
|
},
|
|
646
155
|
|
|
647
|
-
mcp: () => {
|
|
156
|
+
mcp: async () => {
|
|
157
|
+
const sub = process.argv[3];
|
|
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; }
|
|
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}`;
|
|
172
|
+
run(`npx tsx src/index.ts`, resolve(ROOT, 'mcp'));
|
|
173
|
+
},
|
|
648
174
|
|
|
649
175
|
// ── stop / restart ─────────────────────────────────────────────────────────
|
|
650
176
|
stop: () => stopMindos(),
|
|
@@ -791,7 +317,8 @@ ${dim('Shortcut: mindos start --daemon → install + start in one step')}
|
|
|
791
317
|
}
|
|
792
318
|
} else if (platform === 'launchd') {
|
|
793
319
|
try {
|
|
794
|
-
execSync(
|
|
320
|
+
const uid = execSync('id -u').toString().trim();
|
|
321
|
+
execSync(`launchctl print gui/${uid}/com.mindos.app`, { stdio: 'pipe' });
|
|
795
322
|
ok('LaunchAgent com.mindos.app is loaded');
|
|
796
323
|
} catch {
|
|
797
324
|
warn('LaunchAgent com.mindos.app is not loaded (run `mindos gateway start` to start)');
|
|
@@ -816,7 +343,6 @@ ${dim('Shortcut: mindos start --daemon → install + start in one step')}
|
|
|
816
343
|
console.error(red('Update failed. Try: npm install -g @geminilight/mindos@latest'));
|
|
817
344
|
process.exit(1);
|
|
818
345
|
}
|
|
819
|
-
// Clear build stamp so next `mindos start` rebuilds if version changed
|
|
820
346
|
if (existsSync(BUILD_STAMP)) rmSync(BUILD_STAMP);
|
|
821
347
|
const newVersion = (() => {
|
|
822
348
|
try { return JSON.parse(readFileSync(resolve(ROOT, 'package.json'), 'utf-8')).version; } catch { return '?'; }
|
|
@@ -828,19 +354,22 @@ ${dim('Shortcut: mindos start --daemon → install + start in one step')}
|
|
|
828
354
|
return;
|
|
829
355
|
}
|
|
830
356
|
|
|
831
|
-
|
|
832
|
-
const platform = getPlatform();
|
|
357
|
+
const updatePlatform = getPlatform();
|
|
833
358
|
let daemonRunning = false;
|
|
834
|
-
if (
|
|
359
|
+
if (updatePlatform === 'systemd') {
|
|
835
360
|
try { execSync('systemctl --user is-active mindos', { stdio: 'pipe' }); daemonRunning = true; } catch {}
|
|
836
|
-
} else if (
|
|
837
|
-
try {
|
|
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 {}
|
|
838
367
|
}
|
|
839
368
|
|
|
840
369
|
if (daemonRunning) {
|
|
841
370
|
console.log(cyan('\n Daemon is running — restarting to apply the new version...'));
|
|
842
371
|
await runGatewayCommand('stop');
|
|
843
|
-
await runGatewayCommand('install');
|
|
372
|
+
await runGatewayCommand('install');
|
|
844
373
|
await runGatewayCommand('start');
|
|
845
374
|
const webPort = process.env.MINDOS_WEB_PORT || '3000';
|
|
846
375
|
console.log(dim(' (Waiting for Web UI to come back up...)'));
|
|
@@ -892,7 +421,6 @@ ${dim('Shortcut: mindos start --daemon → install + start in one step')}
|
|
|
892
421
|
console.error(red('Failed to parse config file.'));
|
|
893
422
|
process.exit(1);
|
|
894
423
|
}
|
|
895
|
-
// Mask API keys for display
|
|
896
424
|
const display = JSON.parse(JSON.stringify(config));
|
|
897
425
|
if (display.ai?.providers?.anthropic?.apiKey)
|
|
898
426
|
display.ai.providers.anthropic.apiKey = maskKey(display.ai.providers.anthropic.apiKey);
|
|
@@ -965,14 +493,12 @@ ${dim('Shortcut: mindos start --daemon → install + start in one step')}
|
|
|
965
493
|
console.error(red('Failed to parse config file.'));
|
|
966
494
|
process.exit(1);
|
|
967
495
|
}
|
|
968
|
-
// Support dot-notation for nested keys (e.g. ai.provider)
|
|
969
496
|
const parts = key.split('.');
|
|
970
497
|
let obj = config;
|
|
971
498
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
972
499
|
if (typeof obj[parts[i]] !== 'object' || !obj[parts[i]]) obj[parts[i]] = {};
|
|
973
500
|
obj = obj[parts[i]];
|
|
974
501
|
}
|
|
975
|
-
// Coerce numbers
|
|
976
502
|
const coerced = isNaN(Number(val)) ? val : Number(val);
|
|
977
503
|
obj[parts[parts.length - 1]] = coerced;
|
|
978
504
|
writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), 'utf-8');
|
|
@@ -1018,6 +544,7 @@ ${row('mindos stop', 'Stop running MindOS processes')}
|
|
|
1018
544
|
${row('mindos restart', 'Stop then start again')}
|
|
1019
545
|
${row('mindos build', 'Build the app for production')}
|
|
1020
546
|
${row('mindos mcp', 'Start MCP server only')}
|
|
547
|
+
${row('mindos mcp install [agent]', 'Install MindOS MCP config into Agent (claude-code/cursor/windsurf/…) [-g]')}
|
|
1021
548
|
${row('mindos token', 'Show current auth token and MCP config snippet')}
|
|
1022
549
|
${row('mindos gateway <subcommand>', 'Manage background service (install/uninstall/start/stop/status/logs)')}
|
|
1023
550
|
${row('mindos doctor', 'Health check (config, ports, build, daemon)')}
|
|
@@ -1030,13 +557,3 @@ ${row('mindos', 'Start using mode saved in ~/.mindos/
|
|
|
1030
557
|
}
|
|
1031
558
|
|
|
1032
559
|
commands[resolvedCmd]();
|
|
1033
|
-
|
|
1034
|
-
// ── run helper ────────────────────────────────────────────────────────────────
|
|
1035
|
-
|
|
1036
|
-
function run(command, cwd = ROOT) {
|
|
1037
|
-
try {
|
|
1038
|
-
execSync(command, { cwd, stdio: 'inherit', env: process.env });
|
|
1039
|
-
} catch {
|
|
1040
|
-
process.exit(1);
|
|
1041
|
-
}
|
|
1042
|
-
}
|