@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/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
 
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.5",
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
@@ -746,34 +746,41 @@ function runSkillInstallStep(template, selectedAgents) {
746
746
  }
747
747
 
748
748
  const skillName = template === 'zh' ? 'mindos-zh' : 'mindos';
749
- const source = resolve(ROOT, 'skills');
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
- let cmd;
757
- if (additionalAgents.length > 0) {
758
- cmd = `npx skills add "${source}" -s ${skillName} -a ${additionalAgents.join(',')} -g -y`;
759
- } else {
760
- cmd = `npx skills add "${source}" -s ${skillName} -a universal -g -y`;
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
- try {
766
- execSync(cmd, {
767
- encoding: 'utf-8',
768
- timeout: 30_000,
769
- env: { ...process.env, NODE_ENV: 'production' },
770
- stdio: 'pipe',
771
- });
772
- write(tf('skillInstallOk', skillName) + '\n');
773
- } catch (err) {
774
- const msg = err.stderr || err.message || 'Unknown error';
775
- write(tf('skillInstallFail', skillName, msg.split('\n')[0]) + '\n');
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
- 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' });
1191
1200
  } else {
1192
1201
  write(c.dim(t('restartManual') + '\n'));
1193
1202
  }