@geminilight/mindos 0.5.6 → 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.
@@ -1,14 +1,16 @@
1
1
  export const dynamic = 'force-dynamic';
2
2
  import { NextResponse } from 'next/server';
3
- import { MCP_AGENTS, detectInstalled } from '@/lib/mcp-agents';
3
+ import { MCP_AGENTS, detectInstalled, detectAgentPresence } from '@/lib/mcp-agents';
4
4
 
5
5
  export async function GET() {
6
6
  try {
7
7
  const agents = Object.entries(MCP_AGENTS).map(([key, agent]) => {
8
8
  const status = detectInstalled(key);
9
+ const present = detectAgentPresence(key);
9
10
  return {
10
11
  key,
11
12
  name: agent.name,
13
+ present,
12
14
  installed: status.installed,
13
15
  scope: status.scope,
14
16
  transport: status.transport,
@@ -7,14 +7,38 @@ export async function POST() {
7
7
  try {
8
8
  // process.cwd() is the Next.js app directory; cli.js is one level up at project root/bin/
9
9
  const cliPath = resolve(process.cwd(), '../bin/cli.js');
10
- const child = spawn(process.execPath, [cliPath, 'start'], {
10
+ // Use 'restart' (stop all → wait for ports free → start) instead of bare
11
+ // 'start' which would fail assertPortFree because the current process and
12
+ // its MCP child are still holding the ports.
13
+ //
14
+ // IMPORTANT: Strip MINDOS_* env vars so the child's loadConfig() reads
15
+ // the *updated* config file instead of inheriting stale values from this
16
+ // process. Without this, changing ports in the GUI has no effect on the
17
+ // restarted server — it would start on the old ports.
18
+ //
19
+ // Pass the current (old) ports via MINDOS_OLD_* so the restart command
20
+ // can clean up processes still listening on the previous ports.
21
+ const childEnv = { ...process.env };
22
+ const oldWebPort = childEnv.MINDOS_WEB_PORT;
23
+ const oldMcpPort = childEnv.MINDOS_MCP_PORT;
24
+ delete childEnv.MINDOS_WEB_PORT;
25
+ delete childEnv.MINDOS_MCP_PORT;
26
+ delete childEnv.MIND_ROOT;
27
+ delete childEnv.AUTH_TOKEN;
28
+ delete childEnv.WEB_PASSWORD;
29
+ if (oldWebPort) childEnv.MINDOS_OLD_WEB_PORT = oldWebPort;
30
+ if (oldMcpPort) childEnv.MINDOS_OLD_MCP_PORT = oldMcpPort;
31
+ const child = spawn(process.execPath, [cliPath, 'restart'], {
11
32
  detached: true,
12
33
  stdio: 'ignore',
13
- env: process.env,
34
+ env: childEnv,
14
35
  });
15
36
  child.unref();
16
- // Give a brief moment for the response to be sent before exiting
17
- setTimeout(() => process.exit(0), 500);
37
+ // Give a brief moment for the response to be sent before exiting.
38
+ // The spawned 'restart' command will handle stopping this process via
39
+ // stopMindos() (kill by PID + port cleanup), so process.exit here is
40
+ // just a safety net in case the parent isn't killed cleanly.
41
+ setTimeout(() => process.exit(0), 1500);
18
42
  return NextResponse.json({ ok: true });
19
43
  } catch (err) {
20
44
  return NextResponse.json({ error: String(err) }, { status: 500 });
@@ -36,6 +36,7 @@ interface PortStatus {
36
36
  interface AgentEntry {
37
37
  key: string;
38
38
  name: string;
39
+ present: boolean;
39
40
  installed: boolean;
40
41
  hasProjectScope: boolean;
41
42
  hasGlobalScope: boolean;
@@ -511,7 +512,7 @@ function Step5({
511
512
  return agentTransport;
512
513
  };
513
514
 
514
- const getStatusBadge = (key: string, installed: boolean) => {
515
+ const getStatusBadge = (key: string, agent: AgentEntry) => {
515
516
  const st = agentStatuses[key];
516
517
  if (st) {
517
518
  if (st.state === 'installing') return (
@@ -533,16 +534,22 @@ function Step5({
533
534
  </span>
534
535
  );
535
536
  }
536
- if (installed) return (
537
+ if (agent.installed) return (
537
538
  <span className="text-[11px] px-1.5 py-0.5 rounded"
538
539
  style={{ background: 'rgba(34,197,94,0.12)', color: '#22c55e' }}>
539
540
  {settingsMcp.installed}
540
541
  </span>
541
542
  );
543
+ if (agent.present) return (
544
+ <span className="text-[11px] px-1.5 py-0.5 rounded"
545
+ style={{ background: 'rgba(245,158,11,0.12)', color: '#f59e0b' }}>
546
+ {s.agentDetected ?? 'detected'}
547
+ </span>
548
+ );
542
549
  return (
543
550
  <span className="text-[11px] px-1.5 py-0.5 rounded"
544
551
  style={{ background: 'rgba(100,100,120,0.1)', color: 'var(--muted-foreground)' }}>
545
- {s.agentNotInstalled}
552
+ {s.agentNotFound ?? s.agentNotInstalled}
546
553
  </span>
547
554
  );
548
555
  };
@@ -581,7 +588,7 @@ function Step5({
581
588
  style={{ background: 'rgba(100,100,120,0.08)', color: 'var(--muted-foreground)' }}>
582
589
  {getEffectiveTransport(agent)}
583
590
  </span>
584
- {getStatusBadge(agent.key, agent.installed)}
591
+ {getStatusBadge(agent.key, agent)}
585
592
  </label>
586
593
  ))}
587
594
  </div>
@@ -936,7 +943,7 @@ export default function SetupWizard() {
936
943
  if (data.agents) {
937
944
  setAgents(data.agents);
938
945
  setSelectedAgents(new Set(
939
- (data.agents as AgentEntry[]).filter(a => a.installed).map(a => a.key)
946
+ (data.agents as AgentEntry[]).filter(a => a.installed || a.present).map(a => a.key)
940
947
  ));
941
948
  }
942
949
  })
@@ -22,6 +22,7 @@ interface McpStatus {
22
22
  interface AgentInfo {
23
23
  key: string;
24
24
  name: string;
25
+ present: boolean;
25
26
  installed: boolean;
26
27
  scope?: string;
27
28
  transport?: string;
@@ -231,7 +232,9 @@ function AgentInstall({ agents, t, onRefresh }: { agents: AgentInfo[]; t: any; o
231
232
  <span className="text-[10px] text-muted-foreground">{agent.scope}</span>
232
233
  </>
233
234
  ) : (
234
- <span className="text-[10px] text-muted-foreground">{m?.notInstalled ?? 'Not installed'}</span>
235
+ <span className="text-[10px] text-muted-foreground">
236
+ {agent.present ? (m?.detected ?? 'Detected') : (m?.notFound ?? 'Not found')}
237
+ </span>
235
238
  )}
236
239
  {/* Scope selector */}
237
240
  {selected.has(agent.key) && agent.hasProjectScope && agent.hasGlobalScope && (
package/app/lib/i18n.ts CHANGED
@@ -197,6 +197,8 @@ export const messages = {
197
197
  global: 'Global',
198
198
  installed: 'Installed',
199
199
  notInstalled: 'Not installed',
200
+ detected: 'Detected',
201
+ notFound: 'Not found',
200
202
  transportStdio: 'stdio (recommended)',
201
203
  transportHttp: 'http',
202
204
  transportAuto: 'auto (recommended)',
@@ -325,6 +327,8 @@ export const messages = {
325
327
  agentNoneSelected: 'No agents selected — you can configure later in Settings → MCP.',
326
328
  agentSkipLater: 'Skip — configure later',
327
329
  agentNotInstalled: 'not installed',
330
+ agentDetected: 'detected',
331
+ agentNotFound: 'not found',
328
332
  agentStatusOk: 'configured',
329
333
  agentStatusError: 'failed',
330
334
  agentInstalling: 'Configuring…',
@@ -566,6 +570,8 @@ export const messages = {
566
570
  global: '全局',
567
571
  installed: '已安装',
568
572
  notInstalled: '未安装',
573
+ detected: '已检测',
574
+ notFound: '未找到',
569
575
  transportStdio: 'stdio(推荐)',
570
576
  transportHttp: 'http',
571
577
  transportAuto: '自动(推荐)',
@@ -694,6 +700,8 @@ export const messages = {
694
700
  agentNoneSelected: '未选择 agent — 可稍后在 设置 → MCP 中配置。',
695
701
  agentSkipLater: '跳过 — 稍后配置',
696
702
  agentNotInstalled: '未安装',
703
+ agentDetected: '已检测到',
704
+ agentNotFound: '未找到',
697
705
  agentStatusOk: '已配置',
698
706
  agentStatusError: '失败',
699
707
  agentInstalling: '配置中…',
@@ -1,6 +1,7 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import os from 'os';
4
+ import { execSync } from 'child_process';
4
5
 
5
6
  export function expandHome(p: string): string {
6
7
  return p.startsWith('~/') ? path.resolve(os.homedir(), p.slice(2)) : p;
@@ -12,20 +13,100 @@ export interface AgentDef {
12
13
  global: string;
13
14
  key: string;
14
15
  preferredTransport: 'stdio' | 'http';
16
+ /** CLI binary name for presence detection (e.g. 'claude'). Optional. */
17
+ presenceCli?: string;
18
+ /** Data directories for presence detection. Any one existing → present. */
19
+ presenceDirs?: string[];
15
20
  }
16
21
 
17
22
  export const MCP_AGENTS: Record<string, AgentDef> = {
18
- 'claude-code': { name: 'Claude Code', project: '.mcp.json', global: '~/.claude.json', key: 'mcpServers', preferredTransport: 'stdio' },
19
- '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' },
20
- 'cursor': { name: 'Cursor', project: '.cursor/mcp.json', global: '~/.cursor/mcp.json', key: 'mcpServers', preferredTransport: 'stdio' },
21
- 'windsurf': { name: 'Windsurf', project: null, global: '~/.codeium/windsurf/mcp_config.json', key: 'mcpServers', preferredTransport: 'stdio' },
22
- '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' },
23
- 'trae': { name: 'Trae', project: '.trae/mcp.json', global: '~/.trae/mcp.json', key: 'mcpServers', preferredTransport: 'stdio' },
24
- 'gemini-cli': { name: 'Gemini CLI', project: '.gemini/settings.json', global: '~/.gemini/settings.json', key: 'mcpServers', preferredTransport: 'stdio' },
25
- 'openclaw': { name: 'OpenClaw', project: null, global: '~/.openclaw/mcp.json', key: 'mcpServers', preferredTransport: 'stdio' },
26
- 'codebuddy': { name: 'CodeBuddy', project: null, global: '~/.claude-internal/.claude.json', key: 'mcpServers', preferredTransport: 'stdio' },
23
+ 'claude-code': {
24
+ name: 'Claude Code',
25
+ project: '.mcp.json',
26
+ global: '~/.claude.json',
27
+ key: 'mcpServers',
28
+ preferredTransport: 'stdio',
29
+ presenceCli: 'claude',
30
+ presenceDirs: ['~/.claude/'],
31
+ },
32
+ 'claude-desktop': {
33
+ name: 'Claude Desktop',
34
+ project: null,
35
+ global: process.platform === 'darwin'
36
+ ? '~/Library/Application Support/Claude/claude_desktop_config.json'
37
+ : '~/.config/Claude/claude_desktop_config.json',
38
+ key: 'mcpServers',
39
+ preferredTransport: 'http',
40
+ presenceDirs: ['~/Library/Application Support/Claude/', '~/.config/Claude/'],
41
+ },
42
+ 'cursor': {
43
+ name: 'Cursor',
44
+ project: '.cursor/mcp.json',
45
+ global: '~/.cursor/mcp.json',
46
+ key: 'mcpServers',
47
+ preferredTransport: 'stdio',
48
+ presenceDirs: ['~/.cursor/'],
49
+ },
50
+ 'windsurf': {
51
+ name: 'Windsurf',
52
+ project: null,
53
+ global: '~/.codeium/windsurf/mcp_config.json',
54
+ key: 'mcpServers',
55
+ preferredTransport: 'stdio',
56
+ presenceDirs: ['~/.codeium/windsurf/'],
57
+ },
58
+ 'cline': {
59
+ name: 'Cline',
60
+ project: null,
61
+ global: process.platform === 'darwin'
62
+ ? '~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json'
63
+ : '~/.config/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json',
64
+ key: 'mcpServers',
65
+ preferredTransport: 'stdio',
66
+ presenceDirs: [
67
+ '~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/',
68
+ '~/.config/Code/User/globalStorage/saoudrizwan.claude-dev/',
69
+ ],
70
+ },
71
+ 'trae': {
72
+ name: 'Trae',
73
+ project: '.trae/mcp.json',
74
+ global: '~/.trae/mcp.json',
75
+ key: 'mcpServers',
76
+ preferredTransport: 'stdio',
77
+ presenceDirs: ['~/.trae/'],
78
+ },
79
+ 'gemini-cli': {
80
+ name: 'Gemini CLI',
81
+ project: '.gemini/settings.json',
82
+ global: '~/.gemini/settings.json',
83
+ key: 'mcpServers',
84
+ preferredTransport: 'stdio',
85
+ presenceCli: 'gemini',
86
+ presenceDirs: ['~/.gemini/'],
87
+ },
88
+ 'openclaw': {
89
+ name: 'OpenClaw',
90
+ project: null,
91
+ global: '~/.openclaw/mcp.json',
92
+ key: 'mcpServers',
93
+ preferredTransport: 'stdio',
94
+ presenceCli: 'openclaw',
95
+ presenceDirs: ['~/.openclaw/'],
96
+ },
97
+ 'codebuddy': {
98
+ name: 'CodeBuddy',
99
+ project: null,
100
+ global: '~/.claude-internal/.claude.json',
101
+ key: 'mcpServers',
102
+ preferredTransport: 'stdio',
103
+ presenceCli: 'claude-internal',
104
+ presenceDirs: ['~/.claude-internal/'],
105
+ },
27
106
  };
28
107
 
108
+ /* ── MindOS MCP Install Detection ──────────────────────────────────────── */
109
+
29
110
  export function detectInstalled(agentKey: string): { installed: boolean; scope?: string; transport?: string; configPath?: string } {
30
111
  const agent = MCP_AGENTS[agentKey];
31
112
  if (!agent) return { installed: false };
@@ -47,3 +128,23 @@ export function detectInstalled(agentKey: string): { installed: boolean; scope?:
47
128
 
48
129
  return { installed: false };
49
130
  }
131
+
132
+ /* ── Agent Presence Detection ──────────────────────────────────────────── */
133
+
134
+ export function detectAgentPresence(agentKey: string): boolean {
135
+ const agent = MCP_AGENTS[agentKey];
136
+ if (!agent) return false;
137
+ // 1. CLI check
138
+ if (agent.presenceCli) {
139
+ try {
140
+ execSync(
141
+ process.platform === 'win32' ? `where ${agent.presenceCli}` : `which ${agent.presenceCli}`,
142
+ { stdio: 'pipe' },
143
+ );
144
+ return true;
145
+ } catch { /* not found */ }
146
+ }
147
+ // 2. Dir check
148
+ if (agent.presenceDirs?.some(d => fs.existsSync(expandHome(d)))) return true;
149
+ return false;
150
+ }
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
- if (pathname === '/api/auth') return NextResponse.next();
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
 
@@ -3,7 +3,7 @@ import path from 'path';
3
3
 
4
4
  export default defineConfig({
5
5
  test: {
6
- include: ['__tests__/**/*.test.ts'],
6
+ include: ['__tests__/**/*.test.ts', '../tests/unit/**/*.test.ts'],
7
7
  setupFiles: ['__tests__/setup.ts'],
8
8
  },
9
9
  resolve: {
Binary file
package/bin/cli.js CHANGED
@@ -317,16 +317,39 @@ const commands = {
317
317
  stop: () => stopMindos(),
318
318
 
319
319
  restart: async () => {
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
+
320
328
  loadConfig();
321
- const webPort = Number(process.env.MINDOS_WEB_PORT || '3000');
322
- const mcpPort = Number(process.env.MINDOS_MCP_PORT || '8787');
323
- stopMindos();
324
- // Wait until both ports are actually free (up to 15s)
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
+
325
346
  const deadline = Date.now() + 15_000;
326
347
  while (Date.now() < deadline) {
327
- const webBusy = await isPortInUse(webPort);
328
- const mcpBusy = await isPortInUse(mcpPort);
329
- if (!webBusy && !mcpBusy) break;
348
+ let anyBusy = false;
349
+ for (const p of allPorts) {
350
+ if (await isPortInUse(p)) { anyBusy = true; break; }
351
+ }
352
+ if (!anyBusy) break;
330
353
  await new Promise((r) => setTimeout(r, 500));
331
354
  }
332
355
  await commands[getStartMode()]();
@@ -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': { 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' },
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
@@ -66,7 +66,14 @@ function killTree(pid) {
66
66
  return false;
67
67
  }
68
68
 
69
- export function stopMindos() {
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 = {}) {
70
77
  // Read ports from config for port-based cleanup
71
78
  let webPort = '3000', mcpPort = '8787';
72
79
  try {
@@ -90,8 +97,14 @@ export function stopMindos() {
90
97
 
91
98
  // Always do port-based cleanup — Next.js spawns worker processes whose PIDs
92
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
+
93
106
  let portKilled = 0;
94
- for (const port of [webPort, mcpPort]) {
107
+ for (const port of portsToClean) {
95
108
  portKilled += killByPort(port);
96
109
  }
97
110
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geminilight/mindos",
3
- "version": "0.5.6",
3
+ "version": "0.5.7",
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",
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' ? '已安装' : 'installed') : (uiLang === 'zh' ? '未安装' : 'not installed'),
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