@geminilight/mindos 0.5.3 → 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/agents/route.ts +1 -0
- package/app/app/api/mcp/install/route.ts +62 -7
- package/app/app/api/mcp/install-skill/route.ts +127 -0
- package/app/app/api/setup/route.ts +5 -2
- package/app/app/view/[...path]/ViewPageClient.tsx +41 -27
- package/app/components/HomeContent.tsx +3 -1
- package/app/components/SetupWizard.tsx +135 -14
- package/app/components/renderers/config/manifest.ts +1 -0
- package/app/components/renderers/csv/CsvRenderer.tsx +6 -12
- package/app/components/renderers/csv/manifest.ts +1 -0
- package/app/components/renderers/graph/manifest.ts +3 -2
- package/app/components/renderers/todo/manifest.ts +1 -0
- package/app/components/settings/McpTab.tsx +85 -7
- package/app/components/settings/PluginsTab.tsx +31 -16
- package/app/lib/i18n.ts +36 -6
- package/app/lib/mcp-agents.ts +10 -9
- package/app/lib/renderers/index.ts +2 -2
- package/app/lib/renderers/registry.ts +7 -0
- package/app/lib/renderers/useRendererState.ts +114 -0
- package/app/package-lock.json +311 -2
- package/bin/cli.js +11 -1
- package/bin/lib/mcp-agents.js +9 -9
- 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 +131 -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/mcp-agents.js
CHANGED
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
export const MCP_AGENTS = {
|
|
7
|
-
'claude-code': { name: 'Claude Code', project: '.mcp.json', global: '~/.claude.json', key: 'mcpServers' },
|
|
8
|
-
'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' },
|
|
9
|
-
'cursor': { name: 'Cursor', project: '.cursor/mcp.json', global: '~/.cursor/mcp.json', key: 'mcpServers' },
|
|
10
|
-
'windsurf': { name: 'Windsurf', project: null, global: '~/.codeium/windsurf/mcp_config.json', key: 'mcpServers' },
|
|
11
|
-
'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' },
|
|
12
|
-
'trae': { name: 'Trae', project: '.trae/mcp.json', global: '~/.trae/mcp.json', key: 'mcpServers' },
|
|
13
|
-
'gemini-cli': { name: 'Gemini CLI', project: '.gemini/settings.json', global: '~/.gemini/settings.json', key: 'mcpServers' },
|
|
14
|
-
'openclaw': { name: 'OpenClaw', project: null, global: '~/.openclaw/mcp.json', key: 'mcpServers' },
|
|
15
|
-
'codebuddy': { name: 'CodeBuddy', project: null, global: '~/.claude-internal/.claude.json', key: 'mcpServers' },
|
|
7
|
+
'claude-code': { name: 'Claude Code', project: '.mcp.json', global: '~/.claude.json', key: 'mcpServers', preferredTransport: 'stdio' },
|
|
8
|
+
'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', preferredTransport: 'http' },
|
|
9
|
+
'cursor': { name: 'Cursor', project: '.cursor/mcp.json', global: '~/.cursor/mcp.json', key: 'mcpServers', preferredTransport: 'stdio' },
|
|
10
|
+
'windsurf': { name: 'Windsurf', project: null, global: '~/.codeium/windsurf/mcp_config.json', key: 'mcpServers', preferredTransport: 'stdio' },
|
|
11
|
+
'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', preferredTransport: 'stdio' },
|
|
12
|
+
'trae': { name: 'Trae', project: '.trae/mcp.json', global: '~/.trae/mcp.json', key: 'mcpServers', preferredTransport: 'stdio' },
|
|
13
|
+
'gemini-cli': { name: 'Gemini CLI', project: '.gemini/settings.json', global: '~/.gemini/settings.json', key: 'mcpServers', preferredTransport: 'stdio' },
|
|
14
|
+
'openclaw': { name: 'OpenClaw', project: null, global: '~/.openclaw/mcp.json', key: 'mcpServers', preferredTransport: 'stdio' },
|
|
15
|
+
'codebuddy': { name: 'CodeBuddy', project: null, global: '~/.claude-internal/.claude.json', key: 'mcpServers', preferredTransport: 'stdio' },
|
|
16
16
|
};
|
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
|
@@ -133,6 +133,12 @@ const T = {
|
|
|
133
133
|
mcpInstallDone: { en: (n) => `✔ ${n} agent(s) configured`, zh: (n) => `✔ 已配置 ${n} 个 Agent` },
|
|
134
134
|
mcpSkipped: { en: ' → Skipped. Run `mindos mcp install` anytime to configure agents.', zh: ' → 已跳过。随时运行 `mindos mcp install` 配置 Agent。' },
|
|
135
135
|
|
|
136
|
+
// skill install step
|
|
137
|
+
skillInstalling: { en: (name) => `⏳ Installing Skill "${name}"...`, zh: (name) => `⏳ 正在安装 Skill "${name}"...` },
|
|
138
|
+
skillInstallOk: { en: (name) => ` ${c.green('✔')} Skill "${name}" installed`, zh: (name) => ` ${c.green('✔')} Skill "${name}" 已安装` },
|
|
139
|
+
skillInstallFail: { en: (name, msg) => ` ${c.red('✘')} Skill "${name}" failed: ${msg}`, zh: (name, msg) => ` ${c.red('✘')} Skill "${name}" 安装失败:${msg}` },
|
|
140
|
+
skillSkipped: { en: ' → No agents selected, skill install skipped.', zh: ' → 未选择 Agent,跳过 Skill 安装。' },
|
|
141
|
+
|
|
136
142
|
// restart prompts (re-onboard with config changes)
|
|
137
143
|
restartRequired: { en: 'Config changed. Service restart required.', zh: '配置已变更,需要重启服务。' },
|
|
138
144
|
restartNow: { en: 'Restart now?', zh: '立即重启?' },
|
|
@@ -441,8 +447,13 @@ async function isSelfPort(port) {
|
|
|
441
447
|
res.on('end', () => {
|
|
442
448
|
try {
|
|
443
449
|
const data = JSON.parse(body);
|
|
444
|
-
|
|
445
|
-
|
|
450
|
+
// 200 with service=mindos → definitely us.
|
|
451
|
+
// 401 Unauthorized → also us (webPassword is set).
|
|
452
|
+
resolve(data.service === 'mindos' || res.statusCode === 401);
|
|
453
|
+
} catch {
|
|
454
|
+
// Non-JSON but got a response on /api/health → likely us
|
|
455
|
+
resolve(res.statusCode === 401 || res.statusCode === 200);
|
|
456
|
+
}
|
|
446
457
|
});
|
|
447
458
|
});
|
|
448
459
|
req.on('error', () => resolve(false));
|
|
@@ -671,7 +682,7 @@ async function runMcpInstallStep(mcpPort, authToken) {
|
|
|
671
682
|
|
|
672
683
|
if (selected.length === 0) {
|
|
673
684
|
write(c.dim(t('mcpSkipped') + '\n'));
|
|
674
|
-
return;
|
|
685
|
+
return [];
|
|
675
686
|
}
|
|
676
687
|
|
|
677
688
|
write('\n' + c.dim(tf('mcpInstalling', selected.length) + '\n'));
|
|
@@ -705,6 +716,71 @@ async function runMcpInstallStep(mcpPort, authToken) {
|
|
|
705
716
|
}
|
|
706
717
|
|
|
707
718
|
console.log(`\n${c.green(tf('mcpInstallDone', okCount))}`);
|
|
719
|
+
return selected;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/* ── Skill auto-install ────────────────────────────────────────────────────── */
|
|
723
|
+
|
|
724
|
+
const UNIVERSAL_AGENTS = new Set([
|
|
725
|
+
'amp', 'cline', 'codex', 'cursor', 'gemini-cli',
|
|
726
|
+
'github-copilot', 'kimi-cli', 'opencode', 'warp',
|
|
727
|
+
]);
|
|
728
|
+
const SKILL_UNSUPPORTED = new Set(['claude-desktop']);
|
|
729
|
+
const AGENT_NAME_MAP = {
|
|
730
|
+
'claude-code': 'claude-code',
|
|
731
|
+
'windsurf': 'windsurf',
|
|
732
|
+
'trae': 'trae',
|
|
733
|
+
'openclaw': 'openclaw',
|
|
734
|
+
'codebuddy': 'codebuddy',
|
|
735
|
+
};
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* Install the appropriate MindOS Skill to selected agents via `npx skills add`.
|
|
739
|
+
* @param {string} template - 'en' | 'zh' | 'empty' | 'custom'
|
|
740
|
+
* @param {string[]} selectedAgents - MCP agent keys from the multi-select step
|
|
741
|
+
*/
|
|
742
|
+
function runSkillInstallStep(template, selectedAgents) {
|
|
743
|
+
if (!selectedAgents || selectedAgents.length === 0) {
|
|
744
|
+
write(c.dim(t('skillSkipped') + '\n'));
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
const skillName = template === 'zh' ? 'mindos-zh' : 'mindos';
|
|
749
|
+
const localSource = resolve(ROOT, 'skills');
|
|
750
|
+
const githubSource = 'GeminiLight/MindOS';
|
|
751
|
+
|
|
752
|
+
// Filter to non-universal, skill-capable agents
|
|
753
|
+
const additionalAgents = selectedAgents
|
|
754
|
+
.filter(key => !UNIVERSAL_AGENTS.has(key) && !SKILL_UNSUPPORTED.has(key))
|
|
755
|
+
.map(key => AGENT_NAME_MAP[key] || key);
|
|
756
|
+
|
|
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];
|
|
764
|
+
|
|
765
|
+
write(tf('skillInstalling', skillName) + '\n');
|
|
766
|
+
|
|
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 */ }
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
write(tf('skillInstallFail', skillName, 'All sources failed') + '\n');
|
|
708
784
|
}
|
|
709
785
|
|
|
710
786
|
// ── GUI Setup ─────────────────────────────────────────────────────────────────
|
|
@@ -739,16 +815,34 @@ async function startGuiSetup() {
|
|
|
739
815
|
// Read or create config, set setupPending
|
|
740
816
|
let config = {};
|
|
741
817
|
try { config = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8')); } catch { /* ignore */ }
|
|
818
|
+
|
|
819
|
+
const isFirstTime = !config.mindRoot;
|
|
742
820
|
config.setupPending = true;
|
|
743
821
|
writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + '\n');
|
|
744
822
|
|
|
745
|
-
//
|
|
746
|
-
|
|
747
|
-
if (
|
|
748
|
-
|
|
749
|
-
|
|
823
|
+
// Determine which port to use for the setup wizard
|
|
824
|
+
let usePort;
|
|
825
|
+
if (isFirstTime) {
|
|
826
|
+
// First-time onboard: use a temporary port (scan from 9100) so the user's
|
|
827
|
+
// chosen port in Step 3 can differ without a mid-setup restart.
|
|
828
|
+
// 9100 is chosen to avoid conflicts with common services (5000=AirPlay, 3000/8080=dev).
|
|
829
|
+
usePort = await findFreePort(9100);
|
|
830
|
+
} else {
|
|
831
|
+
// Re-onboard: service is already running on config.port — reuse it.
|
|
832
|
+
const existingPort = config.port || 3000;
|
|
833
|
+
if (await isSelfPort(existingPort)) {
|
|
834
|
+
// Service already running — just open the setup page, no need to spawn.
|
|
835
|
+
const url = `http://localhost:${existingPort}/setup`;
|
|
836
|
+
console.log(`\n${c.green(tf('guiReady', url))}\n`);
|
|
837
|
+
const opened = openBrowser(url);
|
|
838
|
+
if (!opened) console.log(c.dim(tf('guiOpenFailed', url)));
|
|
839
|
+
process.exit(0);
|
|
840
|
+
}
|
|
841
|
+
// Service not running — start on existing port
|
|
842
|
+
usePort = await isPortInUse(existingPort)
|
|
843
|
+
? await findFreePort(9100) // existing port occupied by another process
|
|
844
|
+
: existingPort;
|
|
750
845
|
}
|
|
751
|
-
const usePort = config.port || port;
|
|
752
846
|
|
|
753
847
|
write(c.yellow(t('guiStarting') + '\n'));
|
|
754
848
|
|
|
@@ -841,6 +935,15 @@ async function main() {
|
|
|
841
935
|
|
|
842
936
|
const { readdirSync } = await import('node:fs');
|
|
843
937
|
let mindDir;
|
|
938
|
+
let selectedTemplate = 'en'; // hoisted — set by template selection or inferred from existing config
|
|
939
|
+
// Infer template from existing config's disabledSkills or UI language
|
|
940
|
+
if (resumeCfg.disabledSkills?.includes('mindos')) {
|
|
941
|
+
selectedTemplate = 'zh';
|
|
942
|
+
} else if (resumeCfg.disabledSkills?.includes('mindos-zh')) {
|
|
943
|
+
selectedTemplate = 'en';
|
|
944
|
+
} else {
|
|
945
|
+
selectedTemplate = uiLang; // fallback to UI language for first-time existing KB
|
|
946
|
+
}
|
|
844
947
|
|
|
845
948
|
// Default KB path: existing mindRoot if set, otherwise ~/MindOS (same as GUI default)
|
|
846
949
|
const HOME = homedir();
|
|
@@ -875,9 +978,9 @@ async function main() {
|
|
|
875
978
|
} else {
|
|
876
979
|
// ── Template selection (part of Step 1) ─────────────────────────────
|
|
877
980
|
write('\n');
|
|
878
|
-
|
|
981
|
+
selectedTemplate = await select('tplPrompt', 'tplOptions', 'tplValues');
|
|
879
982
|
mkdirSync(mindDir, { recursive: true });
|
|
880
|
-
await applyTemplate(
|
|
983
|
+
await applyTemplate(selectedTemplate, mindDir);
|
|
881
984
|
break;
|
|
882
985
|
}
|
|
883
986
|
}
|
|
@@ -1000,12 +1103,13 @@ async function main() {
|
|
|
1000
1103
|
}
|
|
1001
1104
|
|
|
1002
1105
|
const config = {
|
|
1003
|
-
mindRoot:
|
|
1004
|
-
port:
|
|
1005
|
-
mcpPort:
|
|
1006
|
-
authToken:
|
|
1007
|
-
webPassword:
|
|
1008
|
-
startMode:
|
|
1106
|
+
mindRoot: mindDir,
|
|
1107
|
+
port: webPort,
|
|
1108
|
+
mcpPort: mcpPort,
|
|
1109
|
+
authToken: authToken,
|
|
1110
|
+
webPassword: webPassword || '',
|
|
1111
|
+
startMode: startMode,
|
|
1112
|
+
disabledSkills: selectedTemplate === 'zh' ? ['mindos'] : ['mindos-zh'],
|
|
1009
1113
|
ai: {
|
|
1010
1114
|
provider: isSkip ? existingAiProvider : (isAnthropic ? 'anthropic' : 'openai'),
|
|
1011
1115
|
providers: existingProviders,
|
|
@@ -1052,7 +1156,13 @@ async function main() {
|
|
|
1052
1156
|
stepHeader(7);
|
|
1053
1157
|
write(c.dim(tf('mcpStepHint') + '\n\n'));
|
|
1054
1158
|
|
|
1055
|
-
await runMcpInstallStep(mcpPort, authToken);
|
|
1159
|
+
const selectedAgents = await runMcpInstallStep(mcpPort, authToken);
|
|
1160
|
+
|
|
1161
|
+
// ── Skill auto-install ────────────────────────────────────────────────────
|
|
1162
|
+
if (selectedAgents && selectedAgents.length > 0) {
|
|
1163
|
+
write('\n');
|
|
1164
|
+
runSkillInstallStep(selectedTemplate, selectedAgents);
|
|
1165
|
+
}
|
|
1056
1166
|
|
|
1057
1167
|
// ── Sync setup (optional) ──────────────────────────────────────────────────
|
|
1058
1168
|
const wantSync = await askYesNo('syncSetup');
|
|
@@ -1084,7 +1194,9 @@ async function finish(mindDir, startMode = 'start', mcpPort = 8787, authToken =
|
|
|
1084
1194
|
const doRestart = await askYesNoDefault('restartNow');
|
|
1085
1195
|
if (doRestart) {
|
|
1086
1196
|
const cliPath = resolve(__dirname, '../bin/cli.js');
|
|
1087
|
-
|
|
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' });
|
|
1088
1200
|
} else {
|
|
1089
1201
|
write(c.dim(t('restartManual') + '\n'));
|
|
1090
1202
|
}
|