@geminilight/mindos 0.5.5 → 0.5.7
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/agents/route.ts +3 -1
- package/app/app/api/mcp/install-skill/route.ts +70 -39
- package/app/app/api/restart/route.ts +28 -4
- package/app/app/view/[...path]/ViewPageClient.tsx +37 -7
- package/app/components/HomeContent.tsx +3 -1
- package/app/components/SetupWizard.tsx +12 -5
- package/app/components/renderers/graph/manifest.ts +3 -2
- package/app/components/settings/McpTab.tsx +4 -1
- package/app/lib/i18n.ts +8 -0
- package/app/lib/mcp-agents.ts +110 -9
- package/app/lib/renderers/index.ts +2 -2
- package/app/package-lock.json +311 -2
- package/app/proxy.ts +3 -2
- package/app/vitest.config.ts +1 -1
- package/assets/images/wechat-qr.png +0 -0
- package/bin/cli.js +35 -2
- package/bin/lib/mcp-agents.js +112 -9
- package/bin/lib/stop.js +86 -33
- package/mcp/src/index.ts +5 -0
- package/package.json +1 -1
- package/scripts/gen-renderer-index.js +9 -2
- package/scripts/setup.js +33 -23
package/app/proxy.ts
CHANGED
|
@@ -16,8 +16,9 @@ export async function proxy(req: NextRequest) {
|
|
|
16
16
|
|
|
17
17
|
// --- API protection (AUTH_TOKEN) ---
|
|
18
18
|
if (pathname.startsWith('/api/')) {
|
|
19
|
-
// /api/auth handles its own password validation — never block it
|
|
20
|
-
|
|
19
|
+
// /api/auth handles its own password validation — never block it.
|
|
20
|
+
// /api/health is unauthenticated so check-port can detect this MindOS instance.
|
|
21
|
+
if (pathname === '/api/auth' || pathname === '/api/health') return NextResponse.next();
|
|
21
22
|
|
|
22
23
|
if (!authToken) return NextResponse.next();
|
|
23
24
|
|
package/app/vitest.config.ts
CHANGED
|
Binary file
|
package/bin/cli.js
CHANGED
|
@@ -317,8 +317,41 @@ const commands = {
|
|
|
317
317
|
stop: () => stopMindos(),
|
|
318
318
|
|
|
319
319
|
restart: async () => {
|
|
320
|
-
|
|
321
|
-
|
|
320
|
+
// Capture old ports BEFORE loadConfig overwrites env vars, so we can
|
|
321
|
+
// clean up processes that are still listening on the previous ports
|
|
322
|
+
// (e.g. user changed ports in the GUI and config was already saved).
|
|
323
|
+
// Sources: (1) MINDOS_OLD_* set by /api/restart when it strips the
|
|
324
|
+
// current env, (2) current MINDOS_*_PORT env vars.
|
|
325
|
+
const oldWebPort = process.env.MINDOS_OLD_WEB_PORT || process.env.MINDOS_WEB_PORT;
|
|
326
|
+
const oldMcpPort = process.env.MINDOS_OLD_MCP_PORT || process.env.MINDOS_MCP_PORT;
|
|
327
|
+
|
|
328
|
+
loadConfig();
|
|
329
|
+
|
|
330
|
+
// After loadConfig, env vars reflect the NEW config (or old if unchanged).
|
|
331
|
+
const newWebPort = Number(process.env.MINDOS_WEB_PORT || '3000');
|
|
332
|
+
const newMcpPort = Number(process.env.MINDOS_MCP_PORT || '8787');
|
|
333
|
+
|
|
334
|
+
// Collect old ports that differ from new ones — processes may still be
|
|
335
|
+
// listening there even though config already points to the new ports.
|
|
336
|
+
const extraPorts = [];
|
|
337
|
+
if (oldWebPort && Number(oldWebPort) !== newWebPort) extraPorts.push(oldWebPort);
|
|
338
|
+
if (oldMcpPort && Number(oldMcpPort) !== newMcpPort) extraPorts.push(oldMcpPort);
|
|
339
|
+
|
|
340
|
+
stopMindos({ extraPorts });
|
|
341
|
+
|
|
342
|
+
// Wait until ALL ports (old + new) are actually free (up to 15s)
|
|
343
|
+
const allPorts = new Set([newWebPort, newMcpPort]);
|
|
344
|
+
for (const p of extraPorts) allPorts.add(Number(p));
|
|
345
|
+
|
|
346
|
+
const deadline = Date.now() + 15_000;
|
|
347
|
+
while (Date.now() < deadline) {
|
|
348
|
+
let anyBusy = false;
|
|
349
|
+
for (const p of allPorts) {
|
|
350
|
+
if (await isPortInUse(p)) { anyBusy = true; break; }
|
|
351
|
+
}
|
|
352
|
+
if (!anyBusy) break;
|
|
353
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
354
|
+
}
|
|
322
355
|
await commands[getStartMode()]();
|
|
323
356
|
},
|
|
324
357
|
|
package/bin/lib/mcp-agents.js
CHANGED
|
@@ -1,16 +1,119 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared MCP agent definitions for CLI tools.
|
|
3
3
|
* Mirrors app/lib/mcp-agents.ts — keep in sync manually.
|
|
4
|
+
*
|
|
5
|
+
* Each agent entry includes presenceCli / presenceDirs for detecting
|
|
6
|
+
* whether the agent is installed on the user's machine. To add a new
|
|
7
|
+
* agent, add a single entry here — no separate table needed.
|
|
4
8
|
*/
|
|
5
9
|
|
|
10
|
+
import { existsSync } from 'node:fs';
|
|
11
|
+
import { resolve } from 'node:path';
|
|
12
|
+
import { homedir } from 'node:os';
|
|
13
|
+
import { execSync } from 'node:child_process';
|
|
14
|
+
|
|
15
|
+
function expandHome(p) {
|
|
16
|
+
return p.startsWith('~/') ? resolve(homedir(), p.slice(2)) : p;
|
|
17
|
+
}
|
|
18
|
+
|
|
6
19
|
export const MCP_AGENTS = {
|
|
7
|
-
'claude-code':
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
20
|
+
'claude-code': {
|
|
21
|
+
name: 'Claude Code',
|
|
22
|
+
project: '.mcp.json',
|
|
23
|
+
global: '~/.claude.json',
|
|
24
|
+
key: 'mcpServers',
|
|
25
|
+
preferredTransport: 'stdio',
|
|
26
|
+
presenceCli: 'claude',
|
|
27
|
+
presenceDirs: ['~/.claude/'],
|
|
28
|
+
},
|
|
29
|
+
'claude-desktop': {
|
|
30
|
+
name: 'Claude Desktop',
|
|
31
|
+
project: null,
|
|
32
|
+
global: process.platform === 'darwin'
|
|
33
|
+
? '~/Library/Application Support/Claude/claude_desktop_config.json'
|
|
34
|
+
: '~/.config/Claude/claude_desktop_config.json',
|
|
35
|
+
key: 'mcpServers',
|
|
36
|
+
preferredTransport: 'http',
|
|
37
|
+
presenceDirs: ['~/Library/Application Support/Claude/', '~/.config/Claude/'],
|
|
38
|
+
},
|
|
39
|
+
'cursor': {
|
|
40
|
+
name: 'Cursor',
|
|
41
|
+
project: '.cursor/mcp.json',
|
|
42
|
+
global: '~/.cursor/mcp.json',
|
|
43
|
+
key: 'mcpServers',
|
|
44
|
+
preferredTransport: 'stdio',
|
|
45
|
+
presenceDirs: ['~/.cursor/'],
|
|
46
|
+
},
|
|
47
|
+
'windsurf': {
|
|
48
|
+
name: 'Windsurf',
|
|
49
|
+
project: null,
|
|
50
|
+
global: '~/.codeium/windsurf/mcp_config.json',
|
|
51
|
+
key: 'mcpServers',
|
|
52
|
+
preferredTransport: 'stdio',
|
|
53
|
+
presenceDirs: ['~/.codeium/windsurf/'],
|
|
54
|
+
},
|
|
55
|
+
'cline': {
|
|
56
|
+
name: 'Cline',
|
|
57
|
+
project: null,
|
|
58
|
+
global: process.platform === 'darwin'
|
|
59
|
+
? '~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json'
|
|
60
|
+
: '~/.config/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json',
|
|
61
|
+
key: 'mcpServers',
|
|
62
|
+
preferredTransport: 'stdio',
|
|
63
|
+
presenceDirs: [
|
|
64
|
+
'~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/',
|
|
65
|
+
'~/.config/Code/User/globalStorage/saoudrizwan.claude-dev/',
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
'trae': {
|
|
69
|
+
name: 'Trae',
|
|
70
|
+
project: '.trae/mcp.json',
|
|
71
|
+
global: '~/.trae/mcp.json',
|
|
72
|
+
key: 'mcpServers',
|
|
73
|
+
preferredTransport: 'stdio',
|
|
74
|
+
presenceDirs: ['~/.trae/'],
|
|
75
|
+
},
|
|
76
|
+
'gemini-cli': {
|
|
77
|
+
name: 'Gemini CLI',
|
|
78
|
+
project: '.gemini/settings.json',
|
|
79
|
+
global: '~/.gemini/settings.json',
|
|
80
|
+
key: 'mcpServers',
|
|
81
|
+
preferredTransport: 'stdio',
|
|
82
|
+
presenceCli: 'gemini',
|
|
83
|
+
presenceDirs: ['~/.gemini/'],
|
|
84
|
+
},
|
|
85
|
+
'openclaw': {
|
|
86
|
+
name: 'OpenClaw',
|
|
87
|
+
project: null,
|
|
88
|
+
global: '~/.openclaw/mcp.json',
|
|
89
|
+
key: 'mcpServers',
|
|
90
|
+
preferredTransport: 'stdio',
|
|
91
|
+
presenceCli: 'openclaw',
|
|
92
|
+
presenceDirs: ['~/.openclaw/'],
|
|
93
|
+
},
|
|
94
|
+
'codebuddy': {
|
|
95
|
+
name: 'CodeBuddy',
|
|
96
|
+
project: null,
|
|
97
|
+
global: '~/.claude-internal/.claude.json',
|
|
98
|
+
key: 'mcpServers',
|
|
99
|
+
preferredTransport: 'stdio',
|
|
100
|
+
presenceCli: 'claude-internal',
|
|
101
|
+
presenceDirs: ['~/.claude-internal/'],
|
|
102
|
+
},
|
|
16
103
|
};
|
|
104
|
+
|
|
105
|
+
export function detectAgentPresence(agentKey) {
|
|
106
|
+
const agent = MCP_AGENTS[agentKey];
|
|
107
|
+
if (!agent) return false;
|
|
108
|
+
if (agent.presenceCli) {
|
|
109
|
+
try {
|
|
110
|
+
execSync(
|
|
111
|
+
process.platform === 'win32' ? `where ${agent.presenceCli}` : `which ${agent.presenceCli}`,
|
|
112
|
+
{ stdio: 'pipe' },
|
|
113
|
+
);
|
|
114
|
+
return true;
|
|
115
|
+
} catch { /* not found */ }
|
|
116
|
+
}
|
|
117
|
+
if (agent.presenceDirs?.some(d => existsSync(expandHome(d)))) return true;
|
|
118
|
+
return false;
|
|
119
|
+
}
|
package/bin/lib/stop.js
CHANGED
|
@@ -6,60 +6,113 @@ 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
|
|
|
29
|
-
|
|
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
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Stop MindOS processes.
|
|
71
|
+
* @param {Object} [opts] - Optional overrides.
|
|
72
|
+
* @param {string[]} [opts.extraPorts] - Additional ports to clean up (e.g. old
|
|
73
|
+
* ports before a config change). These are cleaned in addition to the ports
|
|
74
|
+
* read from the current config file.
|
|
75
|
+
*/
|
|
76
|
+
export function stopMindos(opts = {}) {
|
|
77
|
+
// Read ports from config for port-based cleanup
|
|
78
|
+
let webPort = '3000', mcpPort = '8787';
|
|
79
|
+
try {
|
|
80
|
+
const config = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8'));
|
|
81
|
+
if (config.port) webPort = String(config.port);
|
|
82
|
+
if (config.mcpPort) mcpPort = String(config.mcpPort);
|
|
83
|
+
} catch {}
|
|
84
|
+
|
|
30
85
|
const pids = loadPids();
|
|
31
86
|
if (!pids.length) {
|
|
32
87
|
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 {}
|
|
88
|
+
} else {
|
|
89
|
+
// Kill saved PIDs (parent process + MCP) and their child processes
|
|
40
90
|
let stopped = 0;
|
|
41
|
-
for (const
|
|
42
|
-
|
|
91
|
+
for (const pid of pids) {
|
|
92
|
+
if (killTree(pid)) stopped++;
|
|
43
93
|
}
|
|
44
|
-
|
|
45
|
-
|
|
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 {}
|
|
48
|
-
}
|
|
49
|
-
console.log(green('\u2714 Done'));
|
|
50
|
-
return;
|
|
94
|
+
clearPids();
|
|
95
|
+
if (stopped) console.log(green(`\u2714 Stopped ${stopped} process${stopped > 1 ? 'es' : ''}`));
|
|
51
96
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
97
|
+
|
|
98
|
+
// Always do port-based cleanup — Next.js spawns worker processes whose PIDs
|
|
99
|
+
// are not recorded in the PID file and would otherwise become orphaned.
|
|
100
|
+
// Include any extra ports (e.g. old ports from before a config change).
|
|
101
|
+
const portsToClean = new Set([webPort, mcpPort]);
|
|
102
|
+
if (opts.extraPorts) {
|
|
103
|
+
for (const p of opts.extraPorts) portsToClean.add(String(p));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
let portKilled = 0;
|
|
107
|
+
for (const port of portsToClean) {
|
|
108
|
+
portKilled += killByPort(port);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!pids.length && portKilled === 0) {
|
|
112
|
+
// Last resort: pattern match (for envs without lsof)
|
|
113
|
+
try { execSync('pkill -f "next start|next dev" 2>/dev/null || true', { stdio: 'inherit' }); } catch {}
|
|
114
|
+
try { execSync('pkill -f "mcp/src/index" 2>/dev/null || true', { stdio: 'inherit' }); } catch {}
|
|
60
115
|
}
|
|
61
|
-
|
|
62
|
-
console.log(
|
|
63
|
-
? green(`\u2714 Stopped ${stopped} process${stopped > 1 ? 'es' : ''}`)
|
|
64
|
-
: dim('No running processes found'));
|
|
116
|
+
|
|
117
|
+
if (!pids.length) console.log(green('\u2714 Done'));
|
|
65
118
|
}
|
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
|
@@ -30,7 +30,7 @@ import { execSync, spawn } from 'node:child_process';
|
|
|
30
30
|
import { randomBytes, createHash } from 'node:crypto';
|
|
31
31
|
import { createConnection } from 'node:net';
|
|
32
32
|
import http from 'node:http';
|
|
33
|
-
import { MCP_AGENTS } from '../bin/lib/mcp-agents.js';
|
|
33
|
+
import { MCP_AGENTS, detectAgentPresence } from '../bin/lib/mcp-agents.js';
|
|
34
34
|
|
|
35
35
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
36
36
|
const ROOT = resolve(__dirname, '..');
|
|
@@ -616,14 +616,15 @@ function isAgentInstalled(agentKey) {
|
|
|
616
616
|
async function runMcpInstallStep(mcpPort, authToken) {
|
|
617
617
|
const keys = Object.keys(MCP_AGENTS);
|
|
618
618
|
|
|
619
|
-
// Build options with installed status shown as hint
|
|
619
|
+
// Build options with installed/detected status shown as hint
|
|
620
620
|
const options = keys.map(k => {
|
|
621
621
|
const installed = isAgentInstalled(k);
|
|
622
|
+
const present = detectAgentPresence(k);
|
|
622
623
|
return {
|
|
623
624
|
label: MCP_AGENTS[k].name,
|
|
624
|
-
hint: installed ? (uiLang === 'zh' ? '
|
|
625
|
+
hint: installed ? (uiLang === 'zh' ? '已配置' : 'configured') : present ? (uiLang === 'zh' ? '已检测' : 'detected') : (uiLang === 'zh' ? '未找到' : 'not found'),
|
|
625
626
|
value: k,
|
|
626
|
-
preselect: installed,
|
|
627
|
+
preselect: installed || present,
|
|
627
628
|
};
|
|
628
629
|
});
|
|
629
630
|
|
|
@@ -746,34 +747,41 @@ function runSkillInstallStep(template, selectedAgents) {
|
|
|
746
747
|
}
|
|
747
748
|
|
|
748
749
|
const skillName = template === 'zh' ? 'mindos-zh' : 'mindos';
|
|
749
|
-
const
|
|
750
|
+
const localSource = resolve(ROOT, 'skills');
|
|
751
|
+
const githubSource = 'GeminiLight/MindOS';
|
|
750
752
|
|
|
751
753
|
// Filter to non-universal, skill-capable agents
|
|
752
754
|
const additionalAgents = selectedAgents
|
|
753
755
|
.filter(key => !UNIVERSAL_AGENTS.has(key) && !SKILL_UNSUPPORTED.has(key))
|
|
754
756
|
.map(key => AGENT_NAME_MAP[key] || key);
|
|
755
757
|
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
758
|
+
// Each agent needs its own -a flag (skills CLI does NOT accept comma-separated)
|
|
759
|
+
const agentFlags = additionalAgents.length > 0
|
|
760
|
+
? additionalAgents.map(a => `-a ${a}`).join(' ')
|
|
761
|
+
: '-a universal';
|
|
762
|
+
|
|
763
|
+
// Try GitHub source first, fall back to local path
|
|
764
|
+
const sources = [githubSource, localSource];
|
|
762
765
|
|
|
763
766
|
write(tf('skillInstalling', skillName) + '\n');
|
|
764
767
|
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
768
|
+
for (const source of sources) {
|
|
769
|
+
// Quote local paths for shell safety
|
|
770
|
+
const quotedSource = /[/\\]/.test(source) ? `"${source}"` : source;
|
|
771
|
+
const cmd = `npx skills add ${quotedSource} --skill ${skillName} ${agentFlags} -g -y`;
|
|
772
|
+
try {
|
|
773
|
+
execSync(cmd, {
|
|
774
|
+
encoding: 'utf-8',
|
|
775
|
+
timeout: 30_000,
|
|
776
|
+
env: { ...process.env, NODE_ENV: 'production' },
|
|
777
|
+
stdio: 'pipe',
|
|
778
|
+
});
|
|
779
|
+
write(tf('skillInstallOk', skillName) + '\n');
|
|
780
|
+
return;
|
|
781
|
+
} catch { /* try next source */ }
|
|
776
782
|
}
|
|
783
|
+
|
|
784
|
+
write(tf('skillInstallFail', skillName, 'All sources failed') + '\n');
|
|
777
785
|
}
|
|
778
786
|
|
|
779
787
|
// ── GUI Setup ─────────────────────────────────────────────────────────────────
|
|
@@ -1187,7 +1195,9 @@ async function finish(mindDir, startMode = 'start', mcpPort = 8787, authToken =
|
|
|
1187
1195
|
const doRestart = await askYesNoDefault('restartNow');
|
|
1188
1196
|
if (doRestart) {
|
|
1189
1197
|
const cliPath = resolve(__dirname, '../bin/cli.js');
|
|
1190
|
-
|
|
1198
|
+
// Use 'restart' (stop → start) instead of bare 'start' which would
|
|
1199
|
+
// fail assertPortFree because the old process is still running.
|
|
1200
|
+
execSync(`node "${cliPath}" restart`, { stdio: 'inherit' });
|
|
1191
1201
|
} else {
|
|
1192
1202
|
write(c.dim(t('restartManual') + '\n'));
|
|
1193
1203
|
}
|