@geminilight/mindos 0.5.5 → 0.5.6
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 +21 -8
- package/README_zh.md +20 -7
- package/app/app/api/mcp/install-skill/route.ts +70 -39
- package/app/app/view/[...path]/ViewPageClient.tsx +37 -7
- package/app/components/HomeContent.tsx +3 -1
- package/app/components/renderers/graph/manifest.ts +3 -2
- package/app/lib/renderers/index.ts +2 -2
- package/app/package-lock.json +311 -2
- package/bin/cli.js +11 -1
- package/bin/lib/stop.js +72 -32
- package/mcp/src/index.ts +5 -0
- package/package.json +1 -1
- package/scripts/gen-renderer-index.js +9 -2
- package/scripts/setup.js +28 -19
package/bin/cli.js
CHANGED
|
@@ -317,8 +317,18 @@ const commands = {
|
|
|
317
317
|
stop: () => stopMindos(),
|
|
318
318
|
|
|
319
319
|
restart: async () => {
|
|
320
|
+
loadConfig();
|
|
321
|
+
const webPort = Number(process.env.MINDOS_WEB_PORT || '3000');
|
|
322
|
+
const mcpPort = Number(process.env.MINDOS_MCP_PORT || '8787');
|
|
320
323
|
stopMindos();
|
|
321
|
-
|
|
324
|
+
// Wait until both ports are actually free (up to 15s)
|
|
325
|
+
const deadline = Date.now() + 15_000;
|
|
326
|
+
while (Date.now() < deadline) {
|
|
327
|
+
const webBusy = await isPortInUse(webPort);
|
|
328
|
+
const mcpBusy = await isPortInUse(mcpPort);
|
|
329
|
+
if (!webBusy && !mcpBusy) break;
|
|
330
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
331
|
+
}
|
|
322
332
|
await commands[getStartMode()]();
|
|
323
333
|
},
|
|
324
334
|
|
package/bin/lib/stop.js
CHANGED
|
@@ -6,60 +6,100 @@ import { CONFIG_PATH } from './constants.js';
|
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Kill processes listening on the given port.
|
|
9
|
+
* Tries lsof first, then falls back to parsing `ss` output.
|
|
9
10
|
* Returns number of processes killed.
|
|
10
11
|
*/
|
|
11
12
|
function killByPort(port) {
|
|
12
|
-
|
|
13
|
+
const pidsToKill = new Set();
|
|
14
|
+
|
|
15
|
+
// Method 1: lsof
|
|
13
16
|
try {
|
|
14
17
|
const output = execSync(`lsof -ti :${port} 2>/dev/null`, { encoding: 'utf-8' }).trim();
|
|
15
18
|
if (output) {
|
|
16
19
|
for (const p of output.split('\n')) {
|
|
17
20
|
const pid = Number(p);
|
|
18
|
-
if (pid > 0)
|
|
19
|
-
try { process.kill(pid, 'SIGTERM'); killed++; } catch {}
|
|
20
|
-
}
|
|
21
|
+
if (pid > 0) pidsToKill.add(pid);
|
|
21
22
|
}
|
|
22
23
|
}
|
|
23
24
|
} catch {
|
|
24
25
|
// lsof not available or no processes found
|
|
25
26
|
}
|
|
27
|
+
|
|
28
|
+
// Method 2: ss -tlnp (fallback — works when lsof can't see the process)
|
|
29
|
+
if (pidsToKill.size === 0) {
|
|
30
|
+
try {
|
|
31
|
+
const output = execSync(`ss -tlnp 2>/dev/null`, { encoding: 'utf-8' });
|
|
32
|
+
// Match lines like: LISTEN ... *:3003 ... users:(("next-server",pid=12345,fd=21))
|
|
33
|
+
// Match `:PORT` followed by a non-digit to avoid partial matches
|
|
34
|
+
// (e.g. port 80 must not match :8080)
|
|
35
|
+
const portRe = new RegExp(`:${port}(?!\\d)`);
|
|
36
|
+
for (const line of output.split('\n')) {
|
|
37
|
+
if (!portRe.test(line)) continue;
|
|
38
|
+
const pidMatch = line.match(/pid=(\d+)/g);
|
|
39
|
+
if (pidMatch) {
|
|
40
|
+
for (const m of pidMatch) {
|
|
41
|
+
const pid = Number(m.slice(4));
|
|
42
|
+
if (pid > 0) pidsToKill.add(pid);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
} catch {
|
|
47
|
+
// ss not available
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let killed = 0;
|
|
52
|
+
for (const pid of pidsToKill) {
|
|
53
|
+
try { process.kill(pid, 'SIGTERM'); killed++; } catch {}
|
|
54
|
+
}
|
|
26
55
|
return killed;
|
|
27
56
|
}
|
|
28
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Kill a process and all its children (process group).
|
|
60
|
+
*/
|
|
61
|
+
function killTree(pid) {
|
|
62
|
+
// Try to kill the entire process group first
|
|
63
|
+
try { process.kill(-pid, 'SIGTERM'); return true; } catch {}
|
|
64
|
+
// Fallback: kill individual process
|
|
65
|
+
try { process.kill(pid, 'SIGTERM'); return true; } catch {}
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
|
|
29
69
|
export function stopMindos() {
|
|
70
|
+
// Read ports from config for port-based cleanup
|
|
71
|
+
let webPort = '3000', mcpPort = '8787';
|
|
72
|
+
try {
|
|
73
|
+
const config = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8'));
|
|
74
|
+
if (config.port) webPort = String(config.port);
|
|
75
|
+
if (config.mcpPort) mcpPort = String(config.mcpPort);
|
|
76
|
+
} catch {}
|
|
77
|
+
|
|
30
78
|
const pids = loadPids();
|
|
31
79
|
if (!pids.length) {
|
|
32
80
|
console.log(yellow('No PID file found, trying port-based stop...'));
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
try {
|
|
36
|
-
const config = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8'));
|
|
37
|
-
if (config.port) webPort = String(config.port);
|
|
38
|
-
if (config.mcpPort) mcpPort = String(config.mcpPort);
|
|
39
|
-
} catch {}
|
|
81
|
+
} else {
|
|
82
|
+
// Kill saved PIDs (parent process + MCP) and their child processes
|
|
40
83
|
let stopped = 0;
|
|
41
|
-
for (const
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
if (stopped === 0) {
|
|
45
|
-
// Fallback: pkill pattern match (for envs without lsof)
|
|
46
|
-
try { execSync('pkill -f "next start|next dev" 2>/dev/null || true', { stdio: 'inherit' }); } catch {}
|
|
47
|
-
try { execSync('pkill -f "mcp/src/index" 2>/dev/null || true', { stdio: 'inherit' }); } catch {}
|
|
84
|
+
for (const pid of pids) {
|
|
85
|
+
if (killTree(pid)) stopped++;
|
|
48
86
|
}
|
|
49
|
-
|
|
50
|
-
|
|
87
|
+
clearPids();
|
|
88
|
+
if (stopped) console.log(green(`\u2714 Stopped ${stopped} process${stopped > 1 ? 'es' : ''}`));
|
|
51
89
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
// process already gone — ignore
|
|
59
|
-
}
|
|
90
|
+
|
|
91
|
+
// Always do port-based cleanup — Next.js spawns worker processes whose PIDs
|
|
92
|
+
// are not recorded in the PID file and would otherwise become orphaned.
|
|
93
|
+
let portKilled = 0;
|
|
94
|
+
for (const port of [webPort, mcpPort]) {
|
|
95
|
+
portKilled += killByPort(port);
|
|
60
96
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
97
|
+
|
|
98
|
+
if (!pids.length && portKilled === 0) {
|
|
99
|
+
// Last resort: pattern match (for envs without lsof)
|
|
100
|
+
try { execSync('pkill -f "next start|next dev" 2>/dev/null || true', { stdio: 'inherit' }); } catch {}
|
|
101
|
+
try { execSync('pkill -f "mcp/src/index" 2>/dev/null || true', { stdio: 'inherit' }); } catch {}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (!pids.length) console.log(green('\u2714 Done'));
|
|
65
105
|
}
|
package/mcp/src/index.ts
CHANGED
|
@@ -484,6 +484,11 @@ async function main() {
|
|
|
484
484
|
|
|
485
485
|
const expressApp = createMcpExpressApp({ host: MCP_HOST });
|
|
486
486
|
|
|
487
|
+
// Health endpoint — allows check-port to detect this is a MindOS MCP instance
|
|
488
|
+
expressApp.get("/api/health", (_req, res) => {
|
|
489
|
+
res.json({ ok: true, service: "mindos" });
|
|
490
|
+
});
|
|
491
|
+
|
|
487
492
|
// Auth middleware
|
|
488
493
|
if (AUTH_TOKEN) {
|
|
489
494
|
expressApp.use(MCP_ENDPOINT, (req, res, next) => {
|
package/package.json
CHANGED
|
@@ -20,6 +20,13 @@ const dirs = fs.readdirSync(renderersDir, { withFileTypes: true })
|
|
|
20
20
|
.map(d => d.name)
|
|
21
21
|
.sort();
|
|
22
22
|
|
|
23
|
+
// Opt-in-only renderers (match: () => false) go last for consistent ordering.
|
|
24
|
+
const LAST = new Set(['graph']);
|
|
25
|
+
const sorted = [
|
|
26
|
+
...dirs.filter(d => !LAST.has(d)),
|
|
27
|
+
...dirs.filter(d => LAST.has(d)),
|
|
28
|
+
];
|
|
29
|
+
|
|
23
30
|
if (dirs.length === 0) {
|
|
24
31
|
console.error('No manifest.ts files found in', renderersDir);
|
|
25
32
|
process.exit(1);
|
|
@@ -30,12 +37,12 @@ function toCamel(s) {
|
|
|
30
37
|
return s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
31
38
|
}
|
|
32
39
|
|
|
33
|
-
const imports =
|
|
40
|
+
const imports = sorted.map(dir => {
|
|
34
41
|
const varName = toCamel(dir);
|
|
35
42
|
return `import { manifest as ${varName} } from '@/components/renderers/${dir}/manifest';`;
|
|
36
43
|
}).join('\n');
|
|
37
44
|
|
|
38
|
-
const varNames =
|
|
45
|
+
const varNames = sorted.map(toCamel);
|
|
39
46
|
|
|
40
47
|
const code = `/**
|
|
41
48
|
* AUTO-GENERATED by scripts/gen-renderer-index.js — do not edit manually.
|
package/scripts/setup.js
CHANGED
|
@@ -746,34 +746,41 @@ function runSkillInstallStep(template, selectedAgents) {
|
|
|
746
746
|
}
|
|
747
747
|
|
|
748
748
|
const skillName = template === 'zh' ? 'mindos-zh' : 'mindos';
|
|
749
|
-
const
|
|
749
|
+
const localSource = resolve(ROOT, 'skills');
|
|
750
|
+
const githubSource = 'GeminiLight/MindOS';
|
|
750
751
|
|
|
751
752
|
// Filter to non-universal, skill-capable agents
|
|
752
753
|
const additionalAgents = selectedAgents
|
|
753
754
|
.filter(key => !UNIVERSAL_AGENTS.has(key) && !SKILL_UNSUPPORTED.has(key))
|
|
754
755
|
.map(key => AGENT_NAME_MAP[key] || key);
|
|
755
756
|
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
757
|
+
// Each agent needs its own -a flag (skills CLI does NOT accept comma-separated)
|
|
758
|
+
const agentFlags = additionalAgents.length > 0
|
|
759
|
+
? additionalAgents.map(a => `-a ${a}`).join(' ')
|
|
760
|
+
: '-a universal';
|
|
761
|
+
|
|
762
|
+
// Try GitHub source first, fall back to local path
|
|
763
|
+
const sources = [githubSource, localSource];
|
|
762
764
|
|
|
763
765
|
write(tf('skillInstalling', skillName) + '\n');
|
|
764
766
|
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
767
|
+
for (const source of sources) {
|
|
768
|
+
// Quote local paths for shell safety
|
|
769
|
+
const quotedSource = /[/\\]/.test(source) ? `"${source}"` : source;
|
|
770
|
+
const cmd = `npx skills add ${quotedSource} --skill ${skillName} ${agentFlags} -g -y`;
|
|
771
|
+
try {
|
|
772
|
+
execSync(cmd, {
|
|
773
|
+
encoding: 'utf-8',
|
|
774
|
+
timeout: 30_000,
|
|
775
|
+
env: { ...process.env, NODE_ENV: 'production' },
|
|
776
|
+
stdio: 'pipe',
|
|
777
|
+
});
|
|
778
|
+
write(tf('skillInstallOk', skillName) + '\n');
|
|
779
|
+
return;
|
|
780
|
+
} catch { /* try next source */ }
|
|
776
781
|
}
|
|
782
|
+
|
|
783
|
+
write(tf('skillInstallFail', skillName, 'All sources failed') + '\n');
|
|
777
784
|
}
|
|
778
785
|
|
|
779
786
|
// ── GUI Setup ─────────────────────────────────────────────────────────────────
|
|
@@ -1187,7 +1194,9 @@ async function finish(mindDir, startMode = 'start', mcpPort = 8787, authToken =
|
|
|
1187
1194
|
const doRestart = await askYesNoDefault('restartNow');
|
|
1188
1195
|
if (doRestart) {
|
|
1189
1196
|
const cliPath = resolve(__dirname, '../bin/cli.js');
|
|
1190
|
-
|
|
1197
|
+
// Use 'restart' (stop → start) instead of bare 'start' which would
|
|
1198
|
+
// fail assertPortFree because the old process is still running.
|
|
1199
|
+
execSync(`node "${cliPath}" restart`, { stdio: 'inherit' });
|
|
1191
1200
|
} else {
|
|
1192
1201
|
write(c.dim(t('restartManual') + '\n'));
|
|
1193
1202
|
}
|