@geminilight/mindos 0.1.8 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -11
- package/README_zh.md +3 -6
- package/app/app/api/sync/route.ts +124 -0
- package/app/components/SettingsModal.tsx +3 -0
- package/app/components/settings/SyncTab.tsx +219 -0
- package/app/components/settings/types.ts +1 -1
- package/app/lib/i18n.ts +2 -2
- package/app/lib/settings.ts +1 -1
- 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 +234 -692
- package/bin/lib/build.js +59 -0
- package/bin/lib/colors.js +7 -0
- package/bin/lib/config.js +58 -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/sync.js +367 -0
- package/bin/lib/utils.js +16 -0
- package/package.json +6 -2
- package/scripts/release.sh +56 -0
- package/scripts/setup.js +23 -5
package/bin/cli.js
CHANGED
|
@@ -15,7 +15,13 @@
|
|
|
15
15
|
* mindos mcp — start MCP server only
|
|
16
16
|
* mindos stop — stop running MindOS processes
|
|
17
17
|
* mindos restart — stop then start
|
|
18
|
+
* mindos open — open Web UI in the default browser
|
|
18
19
|
* mindos token — show current auth token and MCP config snippet
|
|
20
|
+
* mindos sync — show sync status
|
|
21
|
+
* mindos sync init — configure remote git repo for sync
|
|
22
|
+
* mindos sync now — manual trigger sync
|
|
23
|
+
* mindos sync conflicts — list conflict files
|
|
24
|
+
* mindos sync on|off — enable/disable auto-sync
|
|
19
25
|
* mindos gateway install — install background service (systemd/launchd)
|
|
20
26
|
* mindos gateway uninstall — remove background service
|
|
21
27
|
* mindos gateway start — start the background service
|
|
@@ -30,671 +36,29 @@
|
|
|
30
36
|
* mindos config validate — validate config file
|
|
31
37
|
*/
|
|
32
38
|
|
|
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
|
-
|
|
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
|
-
}
|
|
39
|
+
import { execSync } from 'node:child_process';
|
|
40
|
+
import { existsSync, readFileSync, writeFileSync, rmSync } from 'node:fs';
|
|
41
|
+
import { resolve } from 'node:path';
|
|
42
|
+
import { homedir } from 'node:os';
|
|
43
|
+
|
|
44
|
+
import { ROOT, CONFIG_PATH, BUILD_STAMP, LOG_PATH } from './lib/constants.js';
|
|
45
|
+
import { bold, dim, cyan, green, red, yellow } from './lib/colors.js';
|
|
46
|
+
import { run } from './lib/utils.js';
|
|
47
|
+
import { loadConfig, getStartMode, isDaemonMode } from './lib/config.js';
|
|
48
|
+
import { needsBuild, writeBuildStamp, clearBuildLock, cleanNextDir, ensureAppDeps } from './lib/build.js';
|
|
49
|
+
import { isPortInUse, assertPortFree } from './lib/port.js';
|
|
50
|
+
import { savePids, clearPids } from './lib/pid.js';
|
|
51
|
+
import { stopMindos } from './lib/stop.js';
|
|
52
|
+
import { getPlatform, ensureMindosDir, waitForHttp, runGatewayCommand } from './lib/gateway.js';
|
|
53
|
+
import { printStartupInfo, getLocalIP } from './lib/startup.js';
|
|
54
|
+
import { spawnMcp } from './lib/mcp-spawn.js';
|
|
55
|
+
import { mcpInstall } from './lib/mcp-install.js';
|
|
56
|
+
import { initSync, startSyncDaemon, stopSyncDaemon, getSyncStatus, manualSync, listConflicts, setSyncEnabled } from './lib/sync.js';
|
|
693
57
|
|
|
694
58
|
// ── Commands ──────────────────────────────────────────────────────────────────
|
|
695
59
|
|
|
696
60
|
const cmd = process.argv[2];
|
|
697
|
-
const isDaemon = process.argv.includes('--daemon');
|
|
61
|
+
const isDaemon = process.argv.includes('--daemon') || (!cmd && isDaemonMode());
|
|
698
62
|
const isVerbose = process.argv.includes('--verbose');
|
|
699
63
|
const extra = process.argv.slice(3).filter(a => a !== '--daemon' && a !== '--verbose').join(' ');
|
|
700
64
|
|
|
@@ -707,22 +71,117 @@ const commands = {
|
|
|
707
71
|
init: () => run(`node ${resolve(ROOT, 'scripts/setup.js')}`),
|
|
708
72
|
setup: () => run(`node ${resolve(ROOT, 'scripts/setup.js')}`),
|
|
709
73
|
|
|
74
|
+
// ── open ───────────────────────────────────────────────────────────────────
|
|
75
|
+
open: () => {
|
|
76
|
+
loadConfig();
|
|
77
|
+
const webPort = process.env.MINDOS_WEB_PORT || '3000';
|
|
78
|
+
const url = `http://localhost:${webPort}`;
|
|
79
|
+
let cmd;
|
|
80
|
+
if (process.platform === 'darwin') {
|
|
81
|
+
cmd = 'open';
|
|
82
|
+
} else if (process.platform === 'linux') {
|
|
83
|
+
// WSL detection
|
|
84
|
+
try {
|
|
85
|
+
const uname = execSync('uname -r', { encoding: 'utf-8' });
|
|
86
|
+
cmd = uname.toLowerCase().includes('microsoft') ? 'wslview' : 'xdg-open';
|
|
87
|
+
} catch {
|
|
88
|
+
cmd = 'xdg-open';
|
|
89
|
+
}
|
|
90
|
+
} else {
|
|
91
|
+
cmd = 'start';
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
execSync(`${cmd} ${url}`, { stdio: 'ignore' });
|
|
95
|
+
console.log(`${green('✔')} Opening ${cyan(url)}`);
|
|
96
|
+
} catch {
|
|
97
|
+
console.log(dim(`Could not open browser automatically. Visit: ${cyan(url)}`));
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
|
|
710
101
|
// ── token ──────────────────────────────────────────────────────────────────
|
|
711
102
|
token: () => {
|
|
712
103
|
if (!existsSync(CONFIG_PATH)) {
|
|
713
104
|
console.error(red('No config found. Run `mindos onboard` first.'));
|
|
714
105
|
process.exit(1);
|
|
715
106
|
}
|
|
716
|
-
let
|
|
717
|
-
try {
|
|
107
|
+
let config = {};
|
|
108
|
+
try { config = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8')); } catch {}
|
|
109
|
+
const token = config.authToken || '';
|
|
718
110
|
if (!token) {
|
|
719
111
|
console.log(dim('No auth token set. Run `mindos onboard` to configure one.'));
|
|
720
112
|
process.exit(0);
|
|
721
113
|
}
|
|
114
|
+
const mcpPort = config.mcpPort || 8787;
|
|
115
|
+
const localIP = getLocalIP();
|
|
116
|
+
|
|
117
|
+
const localUrl = `http://localhost:${mcpPort}/mcp`;
|
|
118
|
+
const sep = '━'.repeat(40);
|
|
119
|
+
|
|
722
120
|
console.log(`\n${bold('🔑 Auth token:')} ${cyan(token)}\n`);
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
console.log(
|
|
121
|
+
|
|
122
|
+
// Claude Code
|
|
123
|
+
console.log(`${sep}`);
|
|
124
|
+
console.log(`${bold('Claude Code')}`);
|
|
125
|
+
console.log(`${sep}`);
|
|
126
|
+
console.log(dim('一键安装:') + ` mindos mcp install claude-code -g -y`);
|
|
127
|
+
console.log(dim('\n手动配置 (~/.claude.json):'));
|
|
128
|
+
console.log(JSON.stringify({
|
|
129
|
+
mcpServers: {
|
|
130
|
+
mindos: {
|
|
131
|
+
url: localUrl,
|
|
132
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
}, null, 2));
|
|
136
|
+
|
|
137
|
+
// CodeBuddy (Claude Code Internal)
|
|
138
|
+
console.log(`\n${sep}`);
|
|
139
|
+
console.log(`${bold('CodeBuddy (Claude Code Internal)')}`);
|
|
140
|
+
console.log(`${sep}`);
|
|
141
|
+
console.log(dim('一键安装:') + ` mindos mcp install codebuddy -g -y`);
|
|
142
|
+
console.log(dim('\n手动配置 (~/.claude-internal/.claude.json):'));
|
|
143
|
+
console.log(JSON.stringify({
|
|
144
|
+
mcpServers: {
|
|
145
|
+
mindos: {
|
|
146
|
+
url: localUrl,
|
|
147
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
}, null, 2));
|
|
151
|
+
|
|
152
|
+
// Cursor
|
|
153
|
+
console.log(`\n${sep}`);
|
|
154
|
+
console.log(`${bold('Cursor')}`);
|
|
155
|
+
console.log(`${sep}`);
|
|
156
|
+
console.log(dim('一键安装:') + ` mindos mcp install cursor -g -y`);
|
|
157
|
+
console.log(dim('\n手动配置 (~/.cursor/mcp.json):'));
|
|
158
|
+
console.log(JSON.stringify({
|
|
159
|
+
mcpServers: {
|
|
160
|
+
mindos: {
|
|
161
|
+
url: localUrl,
|
|
162
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
}, null, 2));
|
|
166
|
+
|
|
167
|
+
// Remote
|
|
168
|
+
if (localIP) {
|
|
169
|
+
const remoteUrl = `http://${localIP}:${mcpPort}/mcp`;
|
|
170
|
+
console.log(`\n${sep}`);
|
|
171
|
+
console.log(`${bold('Remote (其他设备)')}`);
|
|
172
|
+
console.log(`${sep}`);
|
|
173
|
+
console.log(`URL: ${cyan(remoteUrl)}`);
|
|
174
|
+
console.log(JSON.stringify({
|
|
175
|
+
mcpServers: {
|
|
176
|
+
mindos: {
|
|
177
|
+
url: remoteUrl,
|
|
178
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
}, null, 2));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
console.log(dim('\nRun `mindos onboard` to regenerate.\n'));
|
|
726
185
|
},
|
|
727
186
|
|
|
728
187
|
// ── dev ────────────────────────────────────────────────────────────────────
|
|
@@ -735,7 +194,12 @@ const commands = {
|
|
|
735
194
|
ensureAppDeps();
|
|
736
195
|
const mcp = spawnMcp(isVerbose);
|
|
737
196
|
savePids(process.pid, mcp.pid);
|
|
738
|
-
process.on('exit', clearPids);
|
|
197
|
+
process.on('exit', () => { stopSyncDaemon(); clearPids(); });
|
|
198
|
+
// Start sync daemon if enabled
|
|
199
|
+
const devMindRoot = process.env.MIND_ROOT;
|
|
200
|
+
if (devMindRoot) {
|
|
201
|
+
startSyncDaemon(devMindRoot).catch(() => {});
|
|
202
|
+
}
|
|
739
203
|
printStartupInfo(webPort, mcpPort);
|
|
740
204
|
run(`npx next dev -p ${webPort} ${extra}`, resolve(ROOT, 'app'));
|
|
741
205
|
},
|
|
@@ -762,6 +226,14 @@ const commands = {
|
|
|
762
226
|
process.exit(1);
|
|
763
227
|
}
|
|
764
228
|
printStartupInfo(webPort, mcpPort);
|
|
229
|
+
// System notification
|
|
230
|
+
try {
|
|
231
|
+
if (process.platform === 'darwin') {
|
|
232
|
+
execSync(`osascript -e 'display notification "http://localhost:${webPort}" with title "MindOS 已就绪"'`, { stdio: 'ignore' });
|
|
233
|
+
} else if (process.platform === 'linux') {
|
|
234
|
+
execSync(`notify-send "MindOS 已就绪" "http://localhost:${webPort}"`, { stdio: 'ignore' });
|
|
235
|
+
}
|
|
236
|
+
} catch { /* notification is best-effort */ }
|
|
765
237
|
console.log(`${green('✔ MindOS is running as a background service')}`);
|
|
766
238
|
console.log(dim(' View logs: mindos logs'));
|
|
767
239
|
console.log(dim(' Stop: mindos gateway stop'));
|
|
@@ -777,13 +249,18 @@ const commands = {
|
|
|
777
249
|
ensureAppDeps();
|
|
778
250
|
if (needsBuild()) {
|
|
779
251
|
console.log(yellow('Building MindOS (first run or new version detected)...\n'));
|
|
780
|
-
|
|
252
|
+
cleanNextDir();
|
|
781
253
|
run('npx next build', resolve(ROOT, 'app'));
|
|
782
254
|
writeBuildStamp();
|
|
783
255
|
}
|
|
784
256
|
const mcp = spawnMcp(isVerbose);
|
|
785
257
|
savePids(process.pid, mcp.pid);
|
|
786
|
-
process.on('exit', clearPids);
|
|
258
|
+
process.on('exit', () => { stopSyncDaemon(); clearPids(); });
|
|
259
|
+
// Start sync daemon if enabled
|
|
260
|
+
const mindRoot = process.env.MIND_ROOT;
|
|
261
|
+
if (mindRoot) {
|
|
262
|
+
startSyncDaemon(mindRoot).catch(() => {});
|
|
263
|
+
}
|
|
787
264
|
printStartupInfo(webPort, mcpPort);
|
|
788
265
|
run(`npx next start -p ${webPort} ${extra}`, resolve(ROOT, 'app'));
|
|
789
266
|
},
|
|
@@ -791,15 +268,27 @@ const commands = {
|
|
|
791
268
|
// ── build ──────────────────────────────────────────────────────────────────
|
|
792
269
|
build: () => {
|
|
793
270
|
ensureAppDeps();
|
|
794
|
-
|
|
271
|
+
cleanNextDir();
|
|
795
272
|
run(`npx next build ${extra}`, resolve(ROOT, 'app'));
|
|
796
273
|
writeBuildStamp();
|
|
797
274
|
},
|
|
798
275
|
|
|
799
276
|
mcp: async () => {
|
|
800
277
|
const sub = process.argv[3];
|
|
801
|
-
|
|
278
|
+
const restArgs = process.argv.slice(3);
|
|
279
|
+
const hasInstallFlags = restArgs.some(a => ['-g', '--global', '-y', '--yes'].includes(a));
|
|
280
|
+
if (sub === 'install' || hasInstallFlags) { await mcpInstall(); return; }
|
|
802
281
|
loadConfig();
|
|
282
|
+
const mcpSdk = resolve(ROOT, 'mcp', 'node_modules', '@modelcontextprotocol', 'sdk', 'package.json');
|
|
283
|
+
if (!existsSync(mcpSdk)) {
|
|
284
|
+
console.log(yellow('Installing MCP dependencies (first run)...\n'));
|
|
285
|
+
run('npm install --prefer-offline --no-workspaces', resolve(ROOT, 'mcp'));
|
|
286
|
+
}
|
|
287
|
+
// Map config env vars to what the MCP server expects
|
|
288
|
+
const mcpPort = process.env.MINDOS_MCP_PORT || '8787';
|
|
289
|
+
const webPort = process.env.MINDOS_WEB_PORT || '3000';
|
|
290
|
+
process.env.MCP_PORT = mcpPort;
|
|
291
|
+
process.env.MINDOS_URL = `http://localhost:${webPort}`;
|
|
803
292
|
run(`npx tsx src/index.ts`, resolve(ROOT, 'mcp'));
|
|
804
293
|
},
|
|
805
294
|
|
|
@@ -948,7 +437,8 @@ ${dim('Shortcut: mindos start --daemon → install + start in one step')}
|
|
|
948
437
|
}
|
|
949
438
|
} else if (platform === 'launchd') {
|
|
950
439
|
try {
|
|
951
|
-
execSync(
|
|
440
|
+
const uid = execSync('id -u').toString().trim();
|
|
441
|
+
execSync(`launchctl print gui/${uid}/com.mindos.app`, { stdio: 'pipe' });
|
|
952
442
|
ok('LaunchAgent com.mindos.app is loaded');
|
|
953
443
|
} catch {
|
|
954
444
|
warn('LaunchAgent com.mindos.app is not loaded (run `mindos gateway start` to start)');
|
|
@@ -973,7 +463,6 @@ ${dim('Shortcut: mindos start --daemon → install + start in one step')}
|
|
|
973
463
|
console.error(red('Update failed. Try: npm install -g @geminilight/mindos@latest'));
|
|
974
464
|
process.exit(1);
|
|
975
465
|
}
|
|
976
|
-
// Clear build stamp so next `mindos start` rebuilds if version changed
|
|
977
466
|
if (existsSync(BUILD_STAMP)) rmSync(BUILD_STAMP);
|
|
978
467
|
const newVersion = (() => {
|
|
979
468
|
try { return JSON.parse(readFileSync(resolve(ROOT, 'package.json'), 'utf-8')).version; } catch { return '?'; }
|
|
@@ -985,19 +474,22 @@ ${dim('Shortcut: mindos start --daemon → install + start in one step')}
|
|
|
985
474
|
return;
|
|
986
475
|
}
|
|
987
476
|
|
|
988
|
-
|
|
989
|
-
const platform = getPlatform();
|
|
477
|
+
const updatePlatform = getPlatform();
|
|
990
478
|
let daemonRunning = false;
|
|
991
|
-
if (
|
|
479
|
+
if (updatePlatform === 'systemd') {
|
|
992
480
|
try { execSync('systemctl --user is-active mindos', { stdio: 'pipe' }); daemonRunning = true; } catch {}
|
|
993
|
-
} else if (
|
|
994
|
-
try {
|
|
481
|
+
} else if (updatePlatform === 'launchd') {
|
|
482
|
+
try {
|
|
483
|
+
const uid = execSync('id -u').toString().trim();
|
|
484
|
+
execSync(`launchctl print gui/${uid}/com.mindos.app`, { stdio: 'pipe' });
|
|
485
|
+
daemonRunning = true;
|
|
486
|
+
} catch {}
|
|
995
487
|
}
|
|
996
488
|
|
|
997
489
|
if (daemonRunning) {
|
|
998
490
|
console.log(cyan('\n Daemon is running — restarting to apply the new version...'));
|
|
999
491
|
await runGatewayCommand('stop');
|
|
1000
|
-
await runGatewayCommand('install');
|
|
492
|
+
await runGatewayCommand('install');
|
|
1001
493
|
await runGatewayCommand('start');
|
|
1002
494
|
const webPort = process.env.MINDOS_WEB_PORT || '3000';
|
|
1003
495
|
console.log(dim(' (Waiting for Web UI to come back up...)'));
|
|
@@ -1049,7 +541,6 @@ ${dim('Shortcut: mindos start --daemon → install + start in one step')}
|
|
|
1049
541
|
console.error(red('Failed to parse config file.'));
|
|
1050
542
|
process.exit(1);
|
|
1051
543
|
}
|
|
1052
|
-
// Mask API keys for display
|
|
1053
544
|
const display = JSON.parse(JSON.stringify(config));
|
|
1054
545
|
if (display.ai?.providers?.anthropic?.apiKey)
|
|
1055
546
|
display.ai.providers.anthropic.apiKey = maskKey(display.ai.providers.anthropic.apiKey);
|
|
@@ -1122,14 +613,12 @@ ${dim('Shortcut: mindos start --daemon → install + start in one step')}
|
|
|
1122
613
|
console.error(red('Failed to parse config file.'));
|
|
1123
614
|
process.exit(1);
|
|
1124
615
|
}
|
|
1125
|
-
// Support dot-notation for nested keys (e.g. ai.provider)
|
|
1126
616
|
const parts = key.split('.');
|
|
1127
617
|
let obj = config;
|
|
1128
618
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
1129
619
|
if (typeof obj[parts[i]] !== 'object' || !obj[parts[i]]) obj[parts[i]] = {};
|
|
1130
620
|
obj = obj[parts[i]];
|
|
1131
621
|
}
|
|
1132
|
-
// Coerce numbers
|
|
1133
622
|
const coerced = isNaN(Number(val)) ? val : Number(val);
|
|
1134
623
|
obj[parts[parts.length - 1]] = coerced;
|
|
1135
624
|
writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), 'utf-8');
|
|
@@ -1152,6 +641,67 @@ ${bold('Examples:')}
|
|
|
1152
641
|
${dim('mindos config set ai.provider openai')}
|
|
1153
642
|
`);
|
|
1154
643
|
},
|
|
644
|
+
|
|
645
|
+
// ── sync ──────────────────────────────────────────────────────────────────
|
|
646
|
+
sync: async () => {
|
|
647
|
+
const sub = process.argv[3];
|
|
648
|
+
loadConfig();
|
|
649
|
+
const mindRoot = process.env.MIND_ROOT;
|
|
650
|
+
|
|
651
|
+
if (sub === 'init') {
|
|
652
|
+
await initSync(mindRoot);
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
if (sub === 'now') {
|
|
657
|
+
manualSync(mindRoot);
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
if (sub === 'conflicts') {
|
|
662
|
+
listConflicts(mindRoot);
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
if (sub === 'on') {
|
|
667
|
+
setSyncEnabled(true);
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
if (sub === 'off') {
|
|
672
|
+
setSyncEnabled(false);
|
|
673
|
+
stopSyncDaemon();
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// default: sync status
|
|
678
|
+
const status = getSyncStatus(mindRoot);
|
|
679
|
+
if (!status.enabled) {
|
|
680
|
+
console.log(`\n${bold('🔄 Sync Status')}`);
|
|
681
|
+
console.log(dim(' Not configured. Run `mindos sync init` to set up.\n'));
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
const ago = status.lastSync
|
|
685
|
+
? (() => {
|
|
686
|
+
const diff = Date.now() - new Date(status.lastSync).getTime();
|
|
687
|
+
if (diff < 60000) return 'just now';
|
|
688
|
+
if (diff < 3600000) return `${Math.floor(diff / 60000)} minutes ago`;
|
|
689
|
+
return `${Math.floor(diff / 3600000)} hours ago`;
|
|
690
|
+
})()
|
|
691
|
+
: 'never';
|
|
692
|
+
|
|
693
|
+
console.log(`\n${bold('🔄 Sync Status')}`);
|
|
694
|
+
console.log(` ${dim('Provider:')} ${cyan(`${status.provider} (${status.remote})`)}`);
|
|
695
|
+
console.log(` ${dim('Branch:')} ${cyan(status.branch)}`);
|
|
696
|
+
console.log(` ${dim('Last sync:')} ${ago}`);
|
|
697
|
+
console.log(` ${dim('Unpushed:')} ${status.unpushed} commits`);
|
|
698
|
+
console.log(` ${dim('Conflicts:')} ${status.conflicts.length ? yellow(`${status.conflicts.length} file(s)`) : green('none')}`);
|
|
699
|
+
console.log(` ${dim('Auto-sync:')} ${green('● enabled')} ${dim(`(commit: ${status.autoCommitInterval}s, pull: ${status.autoPullInterval / 60}min)`)}`);
|
|
700
|
+
if (status.lastError) {
|
|
701
|
+
console.log(` ${dim('Last error:')} ${red(status.lastError)}`);
|
|
702
|
+
}
|
|
703
|
+
console.log();
|
|
704
|
+
},
|
|
1155
705
|
};
|
|
1156
706
|
|
|
1157
707
|
// ── Entry ─────────────────────────────────────────────────────────────────────
|
|
@@ -1176,7 +726,9 @@ ${row('mindos restart', 'Stop then start again')}
|
|
|
1176
726
|
${row('mindos build', 'Build the app for production')}
|
|
1177
727
|
${row('mindos mcp', 'Start MCP server only')}
|
|
1178
728
|
${row('mindos mcp install [agent]', 'Install MindOS MCP config into Agent (claude-code/cursor/windsurf/…) [-g]')}
|
|
729
|
+
${row('mindos open', 'Open Web UI in the default browser')}
|
|
1179
730
|
${row('mindos token', 'Show current auth token and MCP config snippet')}
|
|
731
|
+
${row('mindos sync', 'Show sync status (init/now/conflicts/on/off)')}
|
|
1180
732
|
${row('mindos gateway <subcommand>', 'Manage background service (install/uninstall/start/stop/status/logs)')}
|
|
1181
733
|
${row('mindos doctor', 'Health check (config, ports, build, daemon)')}
|
|
1182
734
|
${row('mindos update', 'Update MindOS to the latest version')}
|
|
@@ -1188,13 +740,3 @@ ${row('mindos', 'Start using mode saved in ~/.mindos/
|
|
|
1188
740
|
}
|
|
1189
741
|
|
|
1190
742
|
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
|
-
}
|