@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.
- package/app/app/api/mcp/agents/route.ts +3 -1
- package/app/app/api/restart/route.ts +28 -4
- package/app/components/SetupWizard.tsx +12 -5
- 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/proxy.ts +3 -2
- package/app/vitest.config.ts +1 -1
- package/assets/images/wechat-qr.png +0 -0
- package/bin/cli.js +30 -7
- package/bin/lib/mcp-agents.js +112 -9
- package/bin/lib/stop.js +15 -2
- package/package.json +1 -1
- package/scripts/setup.js +5 -4
|
@@ -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
|
-
|
|
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:
|
|
34
|
+
env: childEnv,
|
|
14
35
|
});
|
|
15
36
|
child.unref();
|
|
16
|
-
// Give a brief moment for the response to be sent before exiting
|
|
17
|
-
|
|
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,
|
|
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
|
|
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">
|
|
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: '配置中…',
|
package/app/lib/mcp-agents.ts
CHANGED
|
@@ -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':
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
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,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
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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
|
-
|
|
328
|
-
const
|
|
329
|
-
|
|
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()]();
|
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
|
@@ -66,7 +66,14 @@ function killTree(pid) {
|
|
|
66
66
|
return false;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
|
|
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
|
|
107
|
+
for (const port of portsToClean) {
|
|
95
108
|
portKilled += killByPort(port);
|
|
96
109
|
}
|
|
97
110
|
|
package/package.json
CHANGED
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
|
|