@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/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
- await new Promise((r) => setTimeout(r, 1500));
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
 
@@ -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
- let killed = 0;
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
- // Read ports from config
34
- let webPort = '3000', mcpPort = '8787';
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 port of [webPort, mcpPort]) {
42
- stopped += killByPort(port);
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
- console.log(green('\u2714 Done'));
50
- return;
87
+ clearPids();
88
+ if (stopped) console.log(green(`\u2714 Stopped ${stopped} process${stopped > 1 ? 'es' : ''}`));
51
89
  }
52
- let stopped = 0;
53
- for (const pid of pids) {
54
- try {
55
- process.kill(pid, 'SIGTERM');
56
- stopped++;
57
- } catch {
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
- clearPids();
62
- console.log(stopped
63
- ? green(`\u2714 Stopped ${stopped} process${stopped > 1 ? 'es' : ''}`)
64
- : dim('No running processes found'));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geminilight/mindos",
3
- "version": "0.5.3",
3
+ "version": "0.5.6",
4
4
  "description": "MindOS — Human-Agent Collaborative Mind System. Local-first knowledge base that syncs your mind to all AI Agents via MCP.",
5
5
  "keywords": [
6
6
  "mindos",
@@ -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 = dirs.map(dir => {
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 = dirs.map(toCamel);
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
- resolve(data.service === 'mindos');
445
- } catch { resolve(false); }
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
- // Find a free port
746
- const port = await findFreePort(3000);
747
- if (config.port === undefined) {
748
- config.port = port;
749
- writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + '\n');
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
- const tpl = await select('tplPrompt', 'tplOptions', 'tplValues');
981
+ selectedTemplate = await select('tplPrompt', 'tplOptions', 'tplValues');
879
982
  mkdirSync(mindDir, { recursive: true });
880
- await applyTemplate(tpl, mindDir);
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: mindDir,
1004
- port: webPort,
1005
- mcpPort: mcpPort,
1006
- authToken: authToken,
1007
- webPassword: webPassword || '',
1008
- startMode: 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
- execSync(`node "${cliPath}" start`, { stdio: 'inherit' });
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
  }