@geminilight/mindos 0.5.49 → 0.5.51

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 CHANGED
@@ -5,7 +5,7 @@
5
5
  <h1 align="center">MindOS</h1>
6
6
 
7
7
  <p align="center">
8
- <strong>Human Thinks Here, Agent Acts There.</strong>
8
+ <strong>Human Thinks Here, Agents Act There.</strong>
9
9
  </p>
10
10
 
11
11
  <p align="center">
@@ -20,7 +20,7 @@
20
20
  <a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-6366f1.svg?style=for-the-badge" alt="MIT License"></a>
21
21
  </p>
22
22
 
23
- MindOS is a **Human-AI Collaborative Mind System**—a local-first knowledge base that ensures your notes, workflows, and personal context are both human-readable and directly executable by Agents. **Share your brain with every AI — auditable, correctable, and more YOU with every use.**
23
+ MindOS is where you think, and where your AI agents act a local-first knowledge base shared between you and every AI you use. **Share your brain with every AI — every thought grows.**
24
24
 
25
25
  ---
26
26
 
package/README_zh.md CHANGED
@@ -20,7 +20,7 @@
20
20
  <a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-6366f1.svg?style=for-the-badge" alt="MIT License"></a>
21
21
  </p>
22
22
 
23
- MindOS 是一个**人机协同心智系统**——基于本地优先的协作知识库,让你的笔记、工作流、个人上下文既对人类阅读友好,也能直接被 Agent 调用和执行。**和所有 AI 共享你的大脑——可审计、可修正、越用越是你。**
23
+ MindOS 是你思考的地方,也是 AI Agent 行动的起点——一个你和所有 AI 共享的本地知识库。**每次思考,都在生长。**
24
24
 
25
25
  ---
26
26
 
@@ -4,6 +4,13 @@ import fs from 'fs';
4
4
  import path from 'path';
5
5
  import { MCP_AGENTS, expandHome } from '@/lib/mcp-agents';
6
6
 
7
+ /** Parse JSONC — strips single-line (//) and block comments before JSON.parse */
8
+ function parseJsonc(text: string): Record<string, unknown> {
9
+ let stripped = text.replace(/\\"|"(?:\\"|[^"])*"|(\/\/.*$)/gm, (m, g) => g ? '' : m);
10
+ stripped = stripped.replace(/\/\*[\s\S]*?\*\//g, '');
11
+ return JSON.parse(stripped);
12
+ }
13
+
7
14
  interface AgentInstallItem {
8
15
  key: string;
9
16
  scope: 'project' | 'global';
@@ -92,7 +99,7 @@ export async function POST(req: NextRequest) {
92
99
  // Read existing config
93
100
  let config: Record<string, unknown> = {};
94
101
  if (fs.existsSync(absPath)) {
95
- config = JSON.parse(fs.readFileSync(absPath, 'utf-8'));
102
+ config = parseJsonc(fs.readFileSync(absPath, 'utf-8'));
96
103
  }
97
104
 
98
105
  // Merge — only touch mcpServers.mindos
@@ -0,0 +1,88 @@
1
+ export const dynamic = 'force-dynamic';
2
+ import { NextResponse } from 'next/server';
3
+ import { execSync, spawn } from 'node:child_process';
4
+ import { existsSync, readFileSync } from 'node:fs';
5
+ import { resolve } from 'node:path';
6
+ import { homedir } from 'node:os';
7
+
8
+ const CONFIG_PATH = resolve(homedir(), '.mindos', 'config.json');
9
+
10
+ function readConfig(): Record<string, unknown> {
11
+ try { return JSON.parse(readFileSync(CONFIG_PATH, 'utf-8')); }
12
+ catch { return {}; }
13
+ }
14
+
15
+ /**
16
+ * Kill process(es) listening on the given port.
17
+ * Tries lsof first, falls back to ss + manual kill.
18
+ */
19
+ function killByPort(port: number) {
20
+ try {
21
+ execSync(`lsof -ti :${port} | xargs kill -9 2>/dev/null`, { stdio: 'ignore' });
22
+ return;
23
+ } catch { /* lsof not available */ }
24
+ try {
25
+ const output = execSync('ss -tlnp 2>/dev/null', { encoding: 'utf-8' });
26
+ const portRe = new RegExp(`:${port}(?!\\d)`);
27
+ for (const line of output.split('\n')) {
28
+ if (!portRe.test(line)) continue;
29
+ const pidMatch = line.match(/pid=(\d+)/g);
30
+ if (pidMatch) {
31
+ for (const m of pidMatch) {
32
+ const pid = Number(m.slice(4));
33
+ if (pid > 0) try { process.kill(pid, 'SIGKILL'); } catch {}
34
+ }
35
+ }
36
+ }
37
+ } catch { /* no process to kill */ }
38
+ }
39
+
40
+ /**
41
+ * POST /api/mcp/restart — kill the MCP server process and spawn a new one.
42
+ *
43
+ * Unlike /api/restart which restarts the entire MindOS (Web + MCP),
44
+ * this endpoint only restarts the MCP server. The Web UI stays up.
45
+ */
46
+ export async function POST() {
47
+ try {
48
+ const cfg = readConfig();
49
+ const mcpPort = (cfg.mcpPort as number) ?? 8781;
50
+ const webPort = process.env.MINDOS_WEB_PORT || '3456';
51
+ const authToken = cfg.authToken as string | undefined;
52
+
53
+ // Step 1: Kill process on MCP port
54
+ killByPort(mcpPort);
55
+
56
+ // Step 2: Wait briefly for port to free
57
+ await new Promise(r => setTimeout(r, 1000));
58
+
59
+ // Step 3: Spawn new MCP server
60
+ const root = resolve(process.cwd(), '..');
61
+ const mcpDir = resolve(root, 'mcp');
62
+
63
+ if (!existsSync(resolve(mcpDir, 'node_modules'))) {
64
+ return NextResponse.json({ error: 'MCP dependencies not installed' }, { status: 500 });
65
+ }
66
+
67
+ const env: NodeJS.ProcessEnv = {
68
+ ...process.env,
69
+ MCP_PORT: String(mcpPort),
70
+ MCP_HOST: process.env.MCP_HOST || '0.0.0.0',
71
+ MINDOS_URL: process.env.MINDOS_URL || `http://127.0.0.1:${webPort}`,
72
+ ...(authToken ? { AUTH_TOKEN: authToken } : {}),
73
+ };
74
+
75
+ const child = spawn('npx', ['tsx', 'src/index.ts'], {
76
+ cwd: mcpDir,
77
+ detached: true,
78
+ stdio: 'ignore',
79
+ env,
80
+ });
81
+ child.unref();
82
+
83
+ return NextResponse.json({ ok: true, pid: child.pid, port: mcpPort });
84
+ } catch (err) {
85
+ const message = err instanceof Error ? err.message : String(err);
86
+ return NextResponse.json({ error: message }, { status: 500 });
87
+ }
88
+ }
@@ -0,0 +1,12 @@
1
+ import { redirect } from 'next/navigation';
2
+ import { readSettings } from '@/lib/settings';
3
+ import HelpContent from '@/components/help/HelpContent';
4
+
5
+ export const dynamic = 'force-dynamic';
6
+
7
+ export default function HelpPage() {
8
+ const settings = readSettings();
9
+ if (settings.setupPending) redirect('/setup');
10
+
11
+ return <HelpContent />;
12
+ }
@@ -0,0 +1,229 @@
1
+ 'use client';
2
+
3
+ import { useState, useMemo, useCallback } from 'react';
4
+ import { BookOpen, Rocket, Brain, Keyboard, HelpCircle, Bot, ChevronDown, Copy, Check } from 'lucide-react';
5
+ import { useLocale } from '@/lib/LocaleContext';
6
+
7
+ /* ── Collapsible Section ── */
8
+ function Section({ icon, title, defaultOpen = false, children }: {
9
+ icon: React.ReactNode;
10
+ title: string;
11
+ defaultOpen?: boolean;
12
+ children: React.ReactNode;
13
+ }) {
14
+ const [open, setOpen] = useState(defaultOpen);
15
+
16
+ return (
17
+ <div className="bg-card border border-border rounded-lg overflow-hidden">
18
+ <button
19
+ onClick={() => setOpen(v => !v)}
20
+ className="w-full flex items-center gap-3 px-5 py-4 text-left hover:bg-muted/50 transition-colors focus-visible:ring-2 focus-visible:ring-ring"
21
+ aria-expanded={open}
22
+ >
23
+ <span className="text-[var(--amber)]">{icon}</span>
24
+ <span className="text-base font-medium font-display text-foreground flex-1">{title}</span>
25
+ <ChevronDown size={16} className={`text-muted-foreground transition-transform duration-200 ${open ? 'rotate-180' : ''}`} />
26
+ </button>
27
+ {open && (
28
+ <div className="px-5 pb-5 pt-0">
29
+ <div className="border-t border-border pt-4">
30
+ {children}
31
+ </div>
32
+ </div>
33
+ )}
34
+ </div>
35
+ );
36
+ }
37
+
38
+ /* ── Step Card ── */
39
+ function StepCard({ step, title, desc }: { step: number; title: string; desc: string }) {
40
+ return (
41
+ <div className="flex gap-4 items-start">
42
+ <div className="shrink-0 w-8 h-8 rounded-full flex items-center justify-center text-sm font-bold font-mono" style={{ background: 'var(--amber-dim)', color: 'var(--amber)' }}>
43
+ {step}
44
+ </div>
45
+ <div className="min-w-0">
46
+ <p className="text-sm font-medium text-foreground">{title}</p>
47
+ <p className="text-sm text-muted-foreground mt-0.5">{desc}</p>
48
+ </div>
49
+ </div>
50
+ );
51
+ }
52
+
53
+ /* ── Copyable Prompt Block ── */
54
+ function PromptBlock({ text, copyLabel }: { text: string; copyLabel: string }) {
55
+ const [copied, setCopied] = useState(false);
56
+
57
+ const handleCopy = useCallback(() => {
58
+ const clean = text.replace(/^[""]|[""]$/g, '');
59
+ navigator.clipboard.writeText(clean).then(() => {
60
+ setCopied(true);
61
+ setTimeout(() => setCopied(false), 1500);
62
+ });
63
+ }, [text]);
64
+
65
+ return (
66
+ <div className="group/prompt mt-2 flex items-start gap-2 bg-background border border-border rounded-md px-3 py-2">
67
+ <p className="flex-1 text-xs font-mono leading-relaxed" style={{ color: 'var(--amber)' }}>{text}</p>
68
+ <button
69
+ onClick={handleCopy}
70
+ className="shrink-0 p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors opacity-0 group-hover/prompt:opacity-100 focus-visible:opacity-100"
71
+ aria-label={copyLabel}
72
+ title={copyLabel}
73
+ >
74
+ {copied ? <Check size={13} className="text-success" /> : <Copy size={13} />}
75
+ </button>
76
+ </div>
77
+ );
78
+ }
79
+
80
+ /* ── FAQ Item ── */
81
+ function FaqItem({ q, a }: { q: string; a: string }) {
82
+ const [open, setOpen] = useState(false);
83
+
84
+ return (
85
+ <div className="border-b border-border last:border-b-0">
86
+ <button
87
+ onClick={() => setOpen(v => !v)}
88
+ className="w-full flex items-center justify-between py-3 text-left focus-visible:ring-2 focus-visible:ring-ring"
89
+ aria-expanded={open}
90
+ >
91
+ <span className="text-sm font-medium text-foreground pr-4">{q}</span>
92
+ <ChevronDown size={14} className={`shrink-0 text-muted-foreground transition-transform duration-200 ${open ? 'rotate-180' : ''}`} />
93
+ </button>
94
+ {open && (
95
+ <p className="text-sm text-muted-foreground pb-3 leading-relaxed">{a}</p>
96
+ )}
97
+ </div>
98
+ );
99
+ }
100
+
101
+ /* ── Shortcut Row ── */
102
+ function ShortcutRow({ keys, label }: { keys: string; label: string }) {
103
+ return (
104
+ <div className="flex items-center justify-between py-1.5">
105
+ <span className="text-sm text-foreground">{label}</span>
106
+ <div className="flex items-center gap-1">
107
+ {keys.split(' ').map((key, i) => (
108
+ <kbd
109
+ key={i}
110
+ className="px-1.5 py-0.5 text-xs rounded border border-border bg-muted text-muted-foreground font-mono min-w-[24px] text-center"
111
+ >
112
+ {key}
113
+ </kbd>
114
+ ))}
115
+ </div>
116
+ </div>
117
+ );
118
+ }
119
+
120
+ /* ── Main Component ── */
121
+ export default function HelpContent() {
122
+ const { t } = useLocale();
123
+ const h = t.help;
124
+
125
+ const isMac = typeof navigator !== 'undefined' && /Mac|iPhone|iPad/.test(navigator.userAgent);
126
+ const mod = isMac ? '⌘' : 'Ctrl';
127
+
128
+ const shortcuts = useMemo(() => [
129
+ { keys: `${mod} K`, label: h.shortcuts.search },
130
+ { keys: `${mod} /`, label: h.shortcuts.askAI },
131
+ { keys: `${mod} ,`, label: h.shortcuts.settings },
132
+ { keys: `${mod} ?`, label: h.shortcuts.shortcutPanel },
133
+ { keys: 'E', label: h.shortcuts.editFile },
134
+ { keys: `${mod} S`, label: h.shortcuts.save },
135
+ { keys: 'Esc', label: h.shortcuts.closePanel },
136
+ { keys: '@', label: h.shortcuts.attachFile },
137
+ ], [mod, h.shortcuts]);
138
+
139
+ return (
140
+ <div className="content-width px-4 md:px-6 py-8 md:py-12">
141
+ {/* ── Header ── */}
142
+ <div className="mb-8">
143
+ <div className="flex items-center gap-2 mb-1">
144
+ <div className="w-1 h-6 rounded-full" style={{ background: 'var(--amber)' }} />
145
+ <h1 className="text-2xl font-bold font-display text-foreground">{h.title}</h1>
146
+ </div>
147
+ <p className="text-muted-foreground text-sm ml-3 mt-1">{h.subtitle}</p>
148
+ </div>
149
+
150
+ {/* ── Sections ── */}
151
+ <div className="space-y-3">
152
+ {/* 1. What is MindOS */}
153
+ <Section icon={<BookOpen size={18} />} title={h.whatIs.title} defaultOpen>
154
+ <p className="text-sm text-muted-foreground leading-relaxed">{h.whatIs.body}</p>
155
+ </Section>
156
+
157
+ {/* 2. Core Concepts */}
158
+ <Section icon={<Brain size={18} />} title={h.concepts.title} defaultOpen>
159
+ <div className="space-y-4">
160
+ <div>
161
+ <p className="text-sm font-medium text-foreground">{h.concepts.spaceTitle}</p>
162
+ <p className="text-sm text-muted-foreground mt-0.5">{h.concepts.spaceDesc}</p>
163
+ </div>
164
+ <div>
165
+ <p className="text-sm font-medium text-foreground">{h.concepts.instructionTitle}</p>
166
+ <p className="text-sm text-muted-foreground mt-0.5">{h.concepts.instructionDesc}</p>
167
+ </div>
168
+ <div>
169
+ <p className="text-sm font-medium text-foreground">{h.concepts.skillTitle}</p>
170
+ <p className="text-sm text-muted-foreground mt-0.5">{h.concepts.skillDesc}</p>
171
+ </div>
172
+ </div>
173
+ </Section>
174
+
175
+ {/* 3. Quick Start */}
176
+ <Section icon={<Rocket size={18} />} title={h.quickStart.title} defaultOpen>
177
+ <div className="space-y-4">
178
+ <StepCard step={1} title={h.quickStart.step1Title} desc={h.quickStart.step1Desc} />
179
+ <StepCard step={2} title={h.quickStart.step2Title} desc={h.quickStart.step2Desc} />
180
+ <StepCard step={3} title={h.quickStart.step3Title} desc={h.quickStart.step3Desc} />
181
+ </div>
182
+ </Section>
183
+
184
+ {/* 4. Using MindOS with AI Agents */}
185
+ <Section icon={<Bot size={18} />} title={h.agentUsage.title} defaultOpen>
186
+ <p className="text-sm text-muted-foreground leading-relaxed mb-4">{h.agentUsage.intro}</p>
187
+
188
+ <div className="space-y-3">
189
+ {h.agentUsage.scenarios.map((sc, i) => {
190
+ const prompts = sc.prompt.split('\n');
191
+ return (
192
+ <div key={i} className="bg-muted/50 rounded-md px-4 py-3">
193
+ <div className="flex items-center gap-2">
194
+ <span className="text-base leading-none" role="img" suppressHydrationWarning>{sc.emoji}</span>
195
+ <p className="text-sm font-medium text-foreground">{sc.title}</p>
196
+ </div>
197
+ <p className="text-sm text-muted-foreground mt-1">{sc.desc}</p>
198
+ {prompts.map((p, j) => (
199
+ <PromptBlock key={j} text={p} copyLabel={h.agentUsage.copy} />
200
+ ))}
201
+ </div>
202
+ );
203
+ })}
204
+ </div>
205
+
206
+ <p className="text-xs text-muted-foreground mt-4">{h.agentUsage.hint}</p>
207
+ </Section>
208
+
209
+ {/* 5. Keyboard Shortcuts */}
210
+ <Section icon={<Keyboard size={18} />} title={h.shortcutsTitle}>
211
+ <div className="space-y-0">
212
+ {shortcuts.map((s) => (
213
+ <ShortcutRow key={s.keys} keys={s.keys} label={s.label} />
214
+ ))}
215
+ </div>
216
+ </Section>
217
+
218
+ {/* 6. FAQ */}
219
+ <Section icon={<HelpCircle size={18} />} title={h.faq.title}>
220
+ <div>
221
+ {h.faq.items.map((item, i) => (
222
+ <FaqItem key={i} q={item.q} a={item.a} />
223
+ ))}
224
+ </div>
225
+ </Section>
226
+ </div>
227
+ </div>
228
+ );
229
+ }
@@ -1,13 +1,11 @@
1
1
  'use client';
2
2
 
3
- import { useState, useCallback, useMemo } from 'react';
4
- import { Loader2, RefreshCw, ChevronDown, ChevronRight, CheckCircle2, AlertCircle, Copy, Check, Monitor, Globe, Settings } from 'lucide-react';
3
+ import { useState } from 'react';
4
+ import { Loader2, RefreshCw, ChevronDown, ChevronRight, CheckCircle2, AlertCircle, Settings } from 'lucide-react';
5
5
  import { useMcpData } from '@/hooks/useMcpData';
6
6
  import { useLocale } from '@/lib/LocaleContext';
7
- import { generateSnippet } from '@/lib/mcp-snippets';
8
- import { copyToClipboard } from '@/lib/clipboard';
9
7
  import { Toggle } from '../settings/Primitives';
10
- import type { AgentInfo, McpStatus, SkillInfo } from '../settings/types';
8
+ import type { AgentInfo, SkillInfo } from '../settings/types';
11
9
  import PanelHeader from './PanelHeader';
12
10
 
13
11
  interface AgentsPanelProps {
@@ -22,7 +20,6 @@ export default function AgentsPanel({ active, maximized, onMaximize }: AgentsPan
22
20
  const mcp = useMcpData();
23
21
  const [refreshing, setRefreshing] = useState(false);
24
22
  const [showNotDetected, setShowNotDetected] = useState(false);
25
- const [expandedAgent, setExpandedAgent] = useState<string | null>(null);
26
23
  const [showBuiltinSkills, setShowBuiltinSkills] = useState(false);
27
24
 
28
25
  const handleRefresh = async () => {
@@ -31,10 +28,6 @@ export default function AgentsPanel({ active, maximized, onMaximize }: AgentsPan
31
28
  setRefreshing(false);
32
29
  };
33
30
 
34
- const toggleAgent = (key: string) => {
35
- setExpandedAgent(prev => prev === key ? null : key);
36
- };
37
-
38
31
  const connected = mcp.agents.filter(a => a.present && a.installed);
39
32
  const detected = mcp.agents.filter(a => a.present && !a.installed);
40
33
  const notFound = mcp.agents.filter(a => !a.present);
@@ -102,9 +95,6 @@ export default function AgentsPanel({ active, maximized, onMaximize }: AgentsPan
102
95
  key={agent.key}
103
96
  agent={agent}
104
97
  agentStatus="connected"
105
- mcpStatus={mcp.status}
106
- expanded={expandedAgent === agent.key}
107
- onToggle={() => toggleAgent(agent.key)}
108
98
  onInstallAgent={mcp.installAgent}
109
99
  t={p}
110
100
  />
@@ -123,9 +113,6 @@ export default function AgentsPanel({ active, maximized, onMaximize }: AgentsPan
123
113
  key={agent.key}
124
114
  agent={agent}
125
115
  agentStatus="detected"
126
- mcpStatus={mcp.status}
127
- expanded={expandedAgent === agent.key}
128
- onToggle={() => toggleAgent(agent.key)}
129
116
  onInstallAgent={mcp.installAgent}
130
117
  t={p}
131
118
  />
@@ -149,9 +136,6 @@ export default function AgentsPanel({ active, maximized, onMaximize }: AgentsPan
149
136
  key={agent.key}
150
137
  agent={agent}
151
138
  agentStatus="notFound"
152
- mcpStatus={mcp.status}
153
- expanded={expandedAgent === agent.key}
154
- onToggle={() => toggleAgent(agent.key)}
155
139
  onInstallAgent={mcp.installAgent}
156
140
  t={p}
157
141
  />
@@ -239,129 +223,55 @@ function SkillRow({ skill, onToggle }: { skill: SkillInfo; onToggle: (name: stri
239
223
  );
240
224
  }
241
225
 
242
- /* ── Agent Card ── */
226
+ /* ── Agent Card (compact — no snippet, config viewing is in Settings) ── */
243
227
 
244
- function AgentCard({ agent, agentStatus, mcpStatus, expanded, onToggle, onInstallAgent, t }: {
228
+ function AgentCard({ agent, agentStatus, onInstallAgent, t }: {
245
229
  agent: AgentInfo;
246
230
  agentStatus: 'connected' | 'detected' | 'notFound';
247
- mcpStatus: McpStatus | null;
248
- expanded: boolean;
249
- onToggle: () => void;
250
- onInstallAgent: (key: string, opts?: { scope?: string; transport?: string }) => Promise<boolean>;
231
+ onInstallAgent: (key: string) => Promise<boolean>;
251
232
  t: Record<string, any>;
252
233
  }) {
253
- const [transport, setTransport] = useState<'stdio' | 'http'>('stdio');
254
- const [copied, setCopied] = useState(false);
255
234
  const [installing, setInstalling] = useState(false);
256
235
  const [result, setResult] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
257
236
 
258
237
  const dot = agentStatus === 'connected' ? 'bg-emerald-500' : agentStatus === 'detected' ? 'bg-amber-500' : 'bg-zinc-400';
259
238
 
260
- const snippet = useMemo(() => generateSnippet(agent, mcpStatus, transport), [agent, mcpStatus, transport]);
261
-
262
- const handleCopy = useCallback(async () => {
263
- const ok = await copyToClipboard(snippet.snippet);
264
- if (ok) {
265
- setCopied(true);
266
- setTimeout(() => setCopied(false), 2000);
267
- }
268
- }, [snippet.snippet]);
269
-
270
239
  const handleInstall = async () => {
271
240
  setInstalling(true);
272
241
  setResult(null);
273
242
  const ok = await onInstallAgent(agent.key);
274
- if (ok) {
275
- setResult({ type: 'success', text: `${agent.name} ${t.connected}` });
276
- } else {
277
- setResult({ type: 'error', text: 'Install failed' });
278
- }
243
+ setResult(ok
244
+ ? { type: 'success', text: `${agent.name} ${t.connected}` }
245
+ : { type: 'error', text: 'Install failed' });
279
246
  setInstalling(false);
280
247
  };
281
248
 
282
249
  return (
283
- <div className="rounded-lg border border-border/60 bg-card/30 overflow-hidden">
284
- {/* Header row always clickable to expand */}
285
- <button
286
- onClick={onToggle}
287
- className="w-full px-3 py-2 flex items-center justify-between gap-2 hover:bg-muted/30 transition-colors text-left"
288
- >
289
- <div className="flex items-center gap-2 min-w-0">
290
- <span className={`w-1.5 h-1.5 rounded-full shrink-0 ${dot}`} />
291
- <span className="text-xs font-medium text-foreground truncate">{agent.name}</span>
292
- {agentStatus === 'connected' && agent.transport && (
293
- <span className="text-2xs px-1 py-0.5 rounded bg-muted text-muted-foreground shrink-0">{agent.transport}</span>
294
- )}
295
- </div>
296
- {expanded ? <ChevronDown size={10} className="text-muted-foreground shrink-0" /> : <ChevronRight size={10} className="text-muted-foreground shrink-0" />}
297
- </button>
298
-
299
- {/* Expanded: snippet + actions */}
300
- {expanded && (
301
- <div className="px-3 pb-3 pt-1 border-t border-border/40 space-y-2.5">
302
- {/* Detected: Connect button */}
303
- {agentStatus === 'detected' && (
304
- <>
305
- <button onClick={handleInstall} disabled={installing}
306
- className="w-full flex items-center justify-center gap-1.5 px-2.5 py-1.5 text-2xs rounded-md font-medium text-white disabled:opacity-50 transition-colors"
307
- style={{ background: 'var(--amber)' }}>
308
- {installing ? <Loader2 size={11} className="animate-spin" /> : null}
309
- {installing ? t.installing : t.install(agent.name)}
310
- </button>
311
- {result && (
312
- <div className={`flex items-center gap-1.5 text-2xs ${result.type === 'success' ? 'text-emerald-600 dark:text-emerald-400' : 'text-destructive'}`}>
313
- {result.type === 'success' ? <CheckCircle2 size={11} /> : <AlertCircle size={11} />}
314
- {result.text}
315
- </div>
316
- )}
317
- </>
318
- )}
319
-
320
- {/* Transport toggle */}
321
- <div className="flex items-center rounded-md border border-border overflow-hidden w-fit">
322
- <button
323
- onClick={() => setTransport('stdio')}
324
- className={`flex items-center gap-1 px-2 py-1 text-2xs transition-colors ${
325
- transport === 'stdio' ? 'bg-muted text-foreground font-medium' : 'text-muted-foreground hover:text-foreground'
326
- }`}
327
- >
328
- <Monitor size={10} />
329
- {t.transportLocal}
330
- </button>
331
- <button
332
- onClick={() => setTransport('http')}
333
- className={`flex items-center gap-1 px-2 py-1 text-2xs transition-colors ${
334
- transport === 'http' ? 'bg-muted text-foreground font-medium' : 'text-muted-foreground hover:text-foreground'
335
- }`}
336
- >
337
- <Globe size={10} />
338
- {t.transportRemote}
339
- </button>
340
- </div>
341
-
342
- {/* No auth warning for HTTP */}
343
- {transport === 'http' && mcpStatus && !mcpStatus.authConfigured && (
344
- <p className="text-2xs" style={{ color: 'var(--amber)' }}>{t.noAuthWarning}</p>
345
- )}
250
+ <div className="rounded-lg border border-border/60 bg-card/30 px-3 py-2 flex items-center justify-between gap-2">
251
+ <div className="flex items-center gap-2 min-w-0">
252
+ <span className={`w-1.5 h-1.5 rounded-full shrink-0 ${dot}`} />
253
+ <span className="text-xs font-medium text-foreground truncate">{agent.name}</span>
254
+ {agentStatus === 'connected' && agent.transport && (
255
+ <span className="text-2xs px-1 py-0.5 rounded bg-muted text-muted-foreground shrink-0">{agent.transport}</span>
256
+ )}
257
+ </div>
346
258
 
347
- {/* Config snippet */}
348
- <pre className="text-[10px] font-mono bg-muted/50 border border-border rounded-lg p-2.5 overflow-x-auto whitespace-pre select-all max-h-[200px] overflow-y-auto">
349
- {snippet.displaySnippet}
350
- </pre>
259
+ {/* Detected: Install button */}
260
+ {agentStatus === 'detected' && (
261
+ <button onClick={handleInstall} disabled={installing}
262
+ className="flex items-center gap-1 px-2 py-1 text-2xs rounded-md font-medium text-white disabled:opacity-50 transition-colors shrink-0"
263
+ style={{ background: 'var(--amber)' }}>
264
+ {installing ? <Loader2 size={10} className="animate-spin" /> : null}
265
+ {installing ? t.installing : t.install(agent.name)}
266
+ </button>
267
+ )}
351
268
 
352
- {/* Copy + path */}
353
- <div className="flex items-center gap-2 text-2xs">
354
- <button
355
- onClick={handleCopy}
356
- className="inline-flex items-center gap-1 px-2 py-1 rounded-md border border-border text-muted-foreground hover:text-foreground hover:bg-muted transition-colors shrink-0"
357
- >
358
- {copied ? <Check size={10} /> : <Copy size={10} />}
359
- {copied ? t.copied : t.copyConfig}
360
- </button>
361
- <span className="text-muted-foreground">→</span>
362
- <span className="font-mono text-muted-foreground truncate">{snippet.path}</span>
363
- </div>
364
- </div>
269
+ {/* Install result */}
270
+ {result && (
271
+ <span className={`flex items-center gap-1 text-2xs shrink-0 ${result.type === 'success' ? 'text-emerald-600 dark:text-emerald-400' : 'text-destructive'}`}>
272
+ {result.type === 'success' ? <CheckCircle2 size={10} /> : <AlertCircle size={10} />}
273
+ {result.text}
274
+ </span>
365
275
  )}
366
276
  </div>
367
277
  );
@@ -1,6 +1,10 @@
1
- import { Loader2 } from 'lucide-react';
1
+ import { useState, useMemo, useRef, useEffect } from 'react';
2
+ import { Loader2, ChevronDown, Copy, Check, Monitor, Globe, AlertCircle, RotateCcw, RefreshCw } from 'lucide-react';
2
3
  import { useMcpDataOptional } from '@/hooks/useMcpData';
3
- import type { McpTabProps } from './types';
4
+ import { generateSnippet } from '@/lib/mcp-snippets';
5
+ import { copyToClipboard } from '@/lib/clipboard';
6
+ import { apiFetch } from '@/lib/api';
7
+ import type { McpTabProps, McpStatus, AgentInfo } from './types';
4
8
  import AgentInstall from './McpAgentInstall';
5
9
  import SkillsSection from './McpSkillsSection';
6
10
 
@@ -13,6 +17,15 @@ export function McpTab({ t }: McpTabProps) {
13
17
  const mcp = useMcpDataOptional();
14
18
  const m = t.settings?.mcp;
15
19
 
20
+ const [restarting, setRestarting] = useState(false);
21
+ const [selectedAgent, setSelectedAgent] = useState('');
22
+ const [transport, setTransport] = useState<'stdio' | 'http'>('stdio');
23
+ const [copied, setCopied] = useState(false);
24
+ const restartPollRef = useRef<ReturnType<typeof setInterval>>(undefined);
25
+
26
+ // Cleanup restart poll on unmount
27
+ useEffect(() => () => clearInterval(restartPollRef.current), []);
28
+
16
29
  if (!mcp || mcp.loading) {
17
30
  return (
18
31
  <div className="flex justify-center py-8">
@@ -21,29 +34,62 @@ export function McpTab({ t }: McpTabProps) {
21
34
  );
22
35
  }
23
36
 
37
+ const connectedAgents = mcp.agents.filter(a => a.present && a.installed);
38
+ const detectedAgents = mcp.agents.filter(a => a.present && !a.installed);
39
+ const notFoundAgents = mcp.agents.filter(a => !a.present);
40
+
41
+ // Auto-select first agent if none selected
42
+ const effectiveSelected = selectedAgent || (mcp.agents[0]?.key ?? '');
43
+ const currentAgent = mcp.agents.find(a => a.key === effectiveSelected);
44
+
24
45
  return (
25
46
  <div className="space-y-6">
26
- {/* Server status summary (minimal — full status is in sidebar AgentsPanel) */}
27
- {mcp.status && (
28
- <div className="rounded-xl border p-4 space-y-2" style={{ borderColor: 'var(--border)', background: 'var(--card)' }}>
29
- <div className="flex items-center gap-2.5 text-xs">
30
- <span className={`inline-block w-1.5 h-1.5 rounded-full shrink-0 ${mcp.status.running ? 'bg-success' : 'bg-muted-foreground'}`} />
31
- <span className="text-foreground font-medium">
32
- {mcp.status.running ? (m?.running ?? 'Running') : (m?.stopped ?? 'Stopped')}
33
- </span>
34
- {mcp.status.running && (
35
- <>
36
- <span className="text-muted-foreground">·</span>
37
- <span className="font-mono text-muted-foreground">{mcp.status.endpoint}</span>
38
- <span className="text-muted-foreground">·</span>
39
- <span className="text-muted-foreground">{mcp.status.toolCount} tools</span>
40
- </>
41
- )}
42
- </div>
47
+ {/* Server status with restart */}
48
+ <McpStatusCard
49
+ status={mcp.status}
50
+ restarting={restarting}
51
+ onRestart={async () => {
52
+ setRestarting(true);
53
+ try { await apiFetch('/api/mcp/restart', { method: 'POST' }); } catch {}
54
+ const deadline = Date.now() + 60_000;
55
+ clearInterval(restartPollRef.current);
56
+ restartPollRef.current = setInterval(async () => {
57
+ if (Date.now() > deadline) { clearInterval(restartPollRef.current); setRestarting(false); return; }
58
+ try {
59
+ const s = await apiFetch<McpStatus>('/api/mcp/status', { timeout: 3000 });
60
+ if (s.running) { clearInterval(restartPollRef.current); setRestarting(false); mcp.refresh(); }
61
+ } catch {}
62
+ }, 3000);
63
+ }}
64
+ onRefresh={mcp.refresh}
65
+ m={m}
66
+ />
67
+
68
+ {/* MCP Config Viewer */}
69
+ {mcp.agents.length > 0 && (
70
+ <div>
71
+ <h3 className="text-sm font-medium text-foreground mb-3">MCP</h3>
72
+ <AgentConfigViewer
73
+ connectedAgents={connectedAgents}
74
+ detectedAgents={detectedAgents}
75
+ notFoundAgents={notFoundAgents}
76
+ currentAgent={currentAgent ?? null}
77
+ mcpStatus={mcp.status}
78
+ selectedAgent={effectiveSelected}
79
+ onSelectAgent={(key) => setSelectedAgent(key)}
80
+ transport={transport}
81
+ onTransportChange={setTransport}
82
+ copied={copied}
83
+ onCopy={async (snippet) => {
84
+ const ok = await copyToClipboard(snippet);
85
+ if (ok) { setCopied(true); setTimeout(() => setCopied(false), 2000); }
86
+ }}
87
+ m={m}
88
+ />
43
89
  </div>
44
90
  )}
45
91
 
46
- {/* Skills (full CRUD — search, edit, delete, create, language switch) */}
92
+ {/* Skills */}
47
93
  <div>
48
94
  <h3 className="text-sm font-medium text-foreground mb-3">{m?.skillsTitle ?? 'Skills'}</h3>
49
95
  <SkillsSection t={t} />
@@ -57,3 +103,169 @@ export function McpTab({ t }: McpTabProps) {
57
103
  </div>
58
104
  );
59
105
  }
106
+
107
+ /* ── MCP Status Card ── */
108
+
109
+ function McpStatusCard({ status, restarting, onRestart, onRefresh, m }: {
110
+ status: McpStatus | null;
111
+ restarting: boolean;
112
+ onRestart: () => void;
113
+ onRefresh: () => void;
114
+ m: Record<string, any> | undefined;
115
+ }) {
116
+ if (!status) return null;
117
+ return (
118
+ <div className="rounded-xl border border-border bg-card p-4 flex items-center justify-between">
119
+ <div className="flex items-center gap-2.5 text-xs">
120
+ {restarting ? (
121
+ <>
122
+ <Loader2 size={12} className="animate-spin" style={{ color: 'var(--amber)' }} />
123
+ <span style={{ color: 'var(--amber)' }}>{m?.restarting ?? 'Restarting...'}</span>
124
+ </>
125
+ ) : (
126
+ <>
127
+ <span className={`inline-block w-1.5 h-1.5 rounded-full shrink-0 ${status.running ? 'bg-success' : 'bg-muted-foreground'}`} />
128
+ <span className="text-foreground font-medium">
129
+ {status.running ? (m?.running ?? 'Running') : (m?.stopped ?? 'Stopped')}
130
+ </span>
131
+ {status.running && (
132
+ <>
133
+ <span className="text-muted-foreground">·</span>
134
+ <span className="font-mono text-muted-foreground">{status.endpoint}</span>
135
+ <span className="text-muted-foreground">·</span>
136
+ <span className="text-muted-foreground">{status.toolCount} tools</span>
137
+ </>
138
+ )}
139
+ </>
140
+ )}
141
+ </div>
142
+ <div className="flex items-center gap-2">
143
+ {!status.running && !restarting && (
144
+ <button onClick={onRestart}
145
+ className="flex items-center gap-1.5 px-2.5 py-1 text-xs rounded-lg font-medium text-white transition-colors"
146
+ style={{ background: 'var(--amber)' }}>
147
+ <RotateCcw size={12} /> {m?.restart ?? 'Restart'}
148
+ </button>
149
+ )}
150
+ <button onClick={onRefresh}
151
+ className="p-1.5 rounded-lg border border-border text-muted-foreground hover:text-foreground hover:bg-muted transition-colors">
152
+ <RefreshCw size={12} />
153
+ </button>
154
+ </div>
155
+ </div>
156
+ );
157
+ }
158
+
159
+ /* ── Agent Config Viewer (dropdown + snippet) ── */
160
+
161
+ function AgentConfigViewer({ connectedAgents, detectedAgents, notFoundAgents, currentAgent, mcpStatus, selectedAgent, onSelectAgent, transport, onTransportChange, copied, onCopy, m }: {
162
+ connectedAgents: AgentInfo[];
163
+ detectedAgents: AgentInfo[];
164
+ notFoundAgents: AgentInfo[];
165
+ currentAgent: AgentInfo | null;
166
+ mcpStatus: McpStatus | null;
167
+ selectedAgent: string;
168
+ onSelectAgent: (key: string) => void;
169
+ transport: 'stdio' | 'http';
170
+ onTransportChange: (t: 'stdio' | 'http') => void;
171
+ copied: boolean;
172
+ onCopy: (snippet: string) => void;
173
+ m: Record<string, any> | undefined;
174
+ }) {
175
+ const snippet = useMemo(
176
+ () => currentAgent ? generateSnippet(currentAgent, mcpStatus, transport) : null,
177
+ [currentAgent, mcpStatus, transport]
178
+ );
179
+
180
+ return (
181
+ <div className="rounded-xl border border-border bg-card p-4 space-y-3">
182
+ {/* Agent selector */}
183
+ <div className="relative">
184
+ <select
185
+ value={selectedAgent}
186
+ onChange={(e) => onSelectAgent(e.target.value)}
187
+ className="w-full appearance-none px-3 py-2 pr-8 text-xs rounded-lg border border-border bg-background text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
188
+ >
189
+ {connectedAgents.length > 0 && (
190
+ <optgroup label={m?.connectedGroup ?? 'Connected'}>
191
+ {connectedAgents.map(a => (
192
+ <option key={a.key} value={a.key}>
193
+ ✓ {a.name} — {a.transport ?? 'stdio'} · {a.scope ?? 'global'}
194
+ </option>
195
+ ))}
196
+ </optgroup>
197
+ )}
198
+ {detectedAgents.length > 0 && (
199
+ <optgroup label={m?.detectedGroup ?? 'Detected (not configured)'}>
200
+ {detectedAgents.map(a => (
201
+ <option key={a.key} value={a.key}>
202
+ ○ {a.name} — {m?.notConfigured ?? 'not configured'}
203
+ </option>
204
+ ))}
205
+ </optgroup>
206
+ )}
207
+ {notFoundAgents.length > 0 && (
208
+ <optgroup label={m?.notFoundGroup ?? 'Not Installed'}>
209
+ {notFoundAgents.map(a => (
210
+ <option key={a.key} value={a.key}>
211
+ · {a.name}
212
+ </option>
213
+ ))}
214
+ </optgroup>
215
+ )}
216
+ </select>
217
+ <ChevronDown size={14} className="absolute right-2.5 top-1/2 -translate-y-1/2 text-muted-foreground pointer-events-none" />
218
+ </div>
219
+
220
+ {currentAgent && (
221
+ <>
222
+ {/* Transport toggle */}
223
+ <div className="flex items-center rounded-lg border border-border overflow-hidden w-fit">
224
+ <button
225
+ onClick={() => onTransportChange('stdio')}
226
+ className={`flex items-center gap-1.5 px-3 py-1.5 text-xs transition-colors ${
227
+ transport === 'stdio' ? 'bg-muted text-foreground font-medium' : 'text-muted-foreground hover:text-foreground'
228
+ }`}
229
+ >
230
+ <Monitor size={12} /> {m?.transportLocal ?? 'Local (stdio)'}
231
+ </button>
232
+ <button
233
+ onClick={() => onTransportChange('http')}
234
+ className={`flex items-center gap-1.5 px-3 py-1.5 text-xs transition-colors ${
235
+ transport === 'http' ? 'bg-muted text-foreground font-medium' : 'text-muted-foreground hover:text-foreground'
236
+ }`}
237
+ >
238
+ <Globe size={12} /> {m?.transportRemote ?? 'Remote (HTTP)'}
239
+ </button>
240
+ </div>
241
+
242
+ {/* Auth warning */}
243
+ {transport === 'http' && mcpStatus && !mcpStatus.authConfigured && (
244
+ <p className="flex items-center gap-1.5 text-xs" style={{ color: 'var(--amber)' }}>
245
+ <AlertCircle size={12} />
246
+ {m?.noAuthWarning ?? 'Auth not configured. Run `mindos token` to set up.'}
247
+ </p>
248
+ )}
249
+
250
+ {/* Snippet */}
251
+ {snippet && (
252
+ <>
253
+ <pre className="text-[11px] font-mono bg-muted/50 border border-border rounded-lg p-3 overflow-x-auto whitespace-pre select-all max-h-[240px] overflow-y-auto">
254
+ {snippet.displaySnippet}
255
+ </pre>
256
+ <div className="flex items-center gap-3 text-xs">
257
+ <button onClick={() => onCopy(snippet.snippet)}
258
+ className="inline-flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg border border-border text-muted-foreground hover:text-foreground hover:bg-muted transition-colors shrink-0">
259
+ {copied ? <Check size={12} /> : <Copy size={12} />}
260
+ {copied ? (m?.copied ?? 'Copied!') : (m?.copyConfig ?? 'Copy config')}
261
+ </button>
262
+ <span className="text-muted-foreground">→</span>
263
+ <span className="font-mono text-muted-foreground truncate text-2xs">{snippet.path}</span>
264
+ </div>
265
+ </>
266
+ )}
267
+ </>
268
+ )}
269
+ </div>
270
+ );
271
+ }
@@ -415,6 +415,8 @@ export const en = {
415
415
  mcpServer: 'MCP Server',
416
416
  running: 'Running',
417
417
  stopped: 'Not running',
418
+ restarting: 'Restarting...',
419
+ restart: 'Restart',
418
420
  onPort: (port: number) => `on :${port}`,
419
421
  refresh: 'Refresh',
420
422
  refreshing: 'Refreshing...',
@@ -440,6 +440,8 @@ export const zh = {
440
440
  mcpServer: 'MCP 服务器',
441
441
  running: '运行中',
442
442
  stopped: '未运行',
443
+ restarting: '重启中...',
444
+ restart: '重启',
443
445
  onPort: (port: number) => `端口 :${port}`,
444
446
  refresh: '刷新',
445
447
  refreshing: '刷新中...',
@@ -3,6 +3,16 @@ import path from 'path';
3
3
  import os from 'os';
4
4
  import { execSync } from 'child_process';
5
5
 
6
+ /** Parse JSONC — strips single-line (//) and block comments before JSON.parse */
7
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
8
+ function parseJsonc(text: string): any {
9
+ // Strip single-line comments (not inside strings)
10
+ let stripped = text.replace(/\\"|"(?:\\"|[^"])*"|(\/\/.*$)/gm, (m, g) => g ? '' : m);
11
+ // Strip block comments
12
+ stripped = stripped.replace(/\/\*[\s\S]*?\*\//g, '');
13
+ return JSON.parse(stripped);
14
+ }
15
+
6
16
  export function expandHome(p: string): string {
7
17
  return p.startsWith('~/') ? path.resolve(os.homedir(), p.slice(2)) : p;
8
18
  }
@@ -91,11 +101,11 @@ export const MCP_AGENTS: Record<string, AgentDef> = {
91
101
  'codebuddy': {
92
102
  name: 'CodeBuddy',
93
103
  project: null,
94
- global: '~/.claude-internal/.claude.json',
104
+ global: '~/.codebuddy/mcp.json',
95
105
  key: 'mcpServers',
96
106
  preferredTransport: 'stdio',
97
- presenceCli: 'claude-internal',
98
- presenceDirs: ['~/.claude-internal/'],
107
+ presenceCli: 'codebuddy',
108
+ presenceDirs: ['~/.codebuddy/'],
99
109
  },
100
110
  'iflow-cli': {
101
111
  name: 'iFlow CLI',
@@ -227,7 +237,7 @@ export function detectInstalled(agentKey: string): { installed: boolean; scope?:
227
237
  }
228
238
  } else {
229
239
  // JSON format (default)
230
- const config = JSON.parse(content);
240
+ const config = parseJsonc(content);
231
241
  const servers = config[agent.key];
232
242
  if (servers?.mindos) {
233
243
  const entry = servers.mindos;
@@ -2,7 +2,7 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
2
2
  import { resolve } from 'node:path';
3
3
  import { CONFIG_PATH } from './constants.js';
4
4
  import { bold, dim, cyan, green, red, yellow } from './colors.js';
5
- import { expandHome } from './utils.js';
5
+ import { expandHome, parseJsonc } from './utils.js';
6
6
  import { MCP_AGENTS, detectAgentPresence } from './mcp-agents.js';
7
7
 
8
8
  export { MCP_AGENTS };
@@ -194,7 +194,7 @@ export async function mcpInstall() {
194
194
  const abs = expandHome(cfgPath);
195
195
  if (!existsSync(abs)) continue;
196
196
  try {
197
- const config = JSON.parse(readFileSync(abs, 'utf-8'));
197
+ const config = parseJsonc(readFileSync(abs, 'utf-8'));
198
198
  if (config[agent.key]?.mindos) { installed = true; break; }
199
199
  } catch {}
200
200
  }
@@ -313,7 +313,7 @@ export async function mcpInstall() {
313
313
  const absPath = expandHome(configPath);
314
314
  let config = {};
315
315
  if (existsSync(absPath)) {
316
- try { config = JSON.parse(readFileSync(absPath, 'utf-8')); } catch {
316
+ try { config = parseJsonc(readFileSync(absPath, 'utf-8')); } catch {
317
317
  console.error(red(` Failed to parse existing config: ${absPath} — skipping.`));
318
318
  continue;
319
319
  }
package/bin/lib/utils.js CHANGED
@@ -37,3 +37,15 @@ export function npmInstall(cwd, extraFlags = '') {
37
37
  export function expandHome(p) {
38
38
  return p.startsWith('~/') ? resolve(homedir(), p.slice(2)) : p;
39
39
  }
40
+
41
+ /**
42
+ * Parse JSONC — strips single-line and block comments before JSON.parse.
43
+ * VS Code-based editors (Cursor, Windsurf, Cline) use JSONC for config files.
44
+ */
45
+ export function parseJsonc(text) {
46
+ // Strip single-line comments (not inside quoted strings)
47
+ let stripped = text.replace(/\\"|"(?:\\"|[^"])*"|(\/\/.*$)/gm, (m, g) => g ? '' : m);
48
+ // Strip block comments
49
+ stripped = stripped.replace(/\/\*[\s\S]*?\*\//g, '');
50
+ return JSON.parse(stripped);
51
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geminilight/mindos",
3
- "version": "0.5.49",
3
+ "version": "0.5.51",
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
@@ -598,6 +598,13 @@ function expandHomePath(p) {
598
598
  return p.startsWith('~/') ? resolve(homedir(), p.slice(2)) : p;
599
599
  }
600
600
 
601
+ /** Parse JSONC (JSON with // and /* */ comments) — for VS Code-based editor configs */
602
+ function parseJsonc(text) {
603
+ let stripped = text.replace(/\\"|"(?:\\"|[^"])*"|(\/\/.*$)/gm, (m, g) => g ? '' : m);
604
+ stripped = stripped.replace(/\/\*[\s\S]*?\*\//g, '');
605
+ return JSON.parse(stripped);
606
+ }
607
+
601
608
  /** Detect if an agent already has mindos configured (for pre-selection). */
602
609
  function isAgentInstalled(agentKey) {
603
610
  const agent = MCP_AGENTS[agentKey];
@@ -607,7 +614,7 @@ function isAgentInstalled(agentKey) {
607
614
  const abs = expandHomePath(cfgPath);
608
615
  if (!existsSync(abs)) continue;
609
616
  try {
610
- const config = JSON.parse(readFileSync(abs, 'utf-8'));
617
+ const config = parseJsonc(readFileSync(abs, 'utf-8'));
611
618
  if (config[agent.key]?.mindos) return true;
612
619
  } catch { /* ignore */ }
613
620
  }
@@ -720,7 +727,7 @@ async function runMcpInstallStep(mcpPort, authToken) {
720
727
  const abs = expandHomePath(cfgPath);
721
728
  try {
722
729
  let config = {};
723
- if (existsSync(abs)) config = JSON.parse(readFileSync(abs, 'utf-8'));
730
+ if (existsSync(abs)) config = parseJsonc(readFileSync(abs, 'utf-8'));
724
731
  if (!config[agent.key]) config[agent.key] = {};
725
732
  config[agent.key].mindos = entry;
726
733
  const dir = resolve(abs, '..');
@@ -740,7 +747,7 @@ async function runMcpInstallStep(mcpPort, authToken) {
740
747
  /* ── Skill auto-install ────────────────────────────────────────────────────── */
741
748
 
742
749
  const UNIVERSAL_AGENTS = new Set([
743
- 'amp', 'cline', 'codex', 'cursor', 'gemini-cli',
750
+ 'cline', 'codex', 'cursor', 'gemini-cli',
744
751
  'github-copilot', 'kimi-cli', 'opencode', 'warp',
745
752
  ]);
746
753
  const SKILL_UNSUPPORTED = new Set(['claude-desktop']);
@@ -1,240 +0,0 @@
1
- import { useState, useEffect, useCallback, useRef } from 'react';
2
- import { Loader2, RefreshCw, ChevronDown, ChevronRight, ExternalLink } from 'lucide-react';
3
- import { apiFetch } from '@/lib/api';
4
- import type { McpStatus, AgentInfo } from './types';
5
- import type { Messages } from '@/lib/i18n';
6
-
7
- interface AgentsTabProps {
8
- t: Messages;
9
- }
10
-
11
- export function AgentsTab({ t }: AgentsTabProps) {
12
- const [agents, setAgents] = useState<AgentInfo[]>([]);
13
- const [mcpStatus, setMcpStatus] = useState<McpStatus | null>(null);
14
- const [loading, setLoading] = useState(true);
15
- const [error, setError] = useState(false);
16
- const [refreshing, setRefreshing] = useState(false);
17
- const [showNotDetected, setShowNotDetected] = useState(false);
18
- const intervalRef = useRef<ReturnType<typeof setInterval>>(undefined);
19
-
20
- const a = t.settings?.agents as Record<string, unknown> | undefined;
21
-
22
- // i18n helpers with fallbacks
23
- const txt = (key: string, fallback: string) => (a?.[key] as string) ?? fallback;
24
- const txtFn = <T,>(key: string, fallback: (v: T) => string) =>
25
- (a?.[key] as ((v: T) => string) | undefined) ?? fallback;
26
-
27
- const fetchAll = useCallback(async (silent = false) => {
28
- if (!silent) setError(false);
29
- try {
30
- const [statusData, agentsData] = await Promise.all([
31
- apiFetch<McpStatus>('/api/mcp/status'),
32
- apiFetch<{ agents: AgentInfo[] }>('/api/mcp/agents'),
33
- ]);
34
- setMcpStatus(statusData);
35
- setAgents(agentsData.agents);
36
- setError(false);
37
- } catch {
38
- if (!silent) setError(true);
39
- }
40
- setLoading(false);
41
- setRefreshing(false);
42
- }, []);
43
-
44
- // Initial fetch + 30s auto-refresh
45
- useEffect(() => {
46
- fetchAll();
47
- intervalRef.current = setInterval(() => fetchAll(true), 30_000);
48
- return () => clearInterval(intervalRef.current);
49
- }, [fetchAll]);
50
-
51
- const handleRefresh = () => {
52
- setRefreshing(true);
53
- fetchAll();
54
- };
55
-
56
- if (loading) {
57
- return (
58
- <div className="flex justify-center py-8">
59
- <Loader2 size={18} className="animate-spin text-muted-foreground" />
60
- </div>
61
- );
62
- }
63
-
64
- if (error && agents.length === 0) {
65
- return (
66
- <div className="flex flex-col items-center gap-3 py-8 text-center">
67
- <p className="text-sm text-destructive">{txt('fetchError', 'Failed to load agent data')}</p>
68
- <button
69
- onClick={handleRefresh}
70
- className="flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-lg border border-border text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
71
- >
72
- <RefreshCw size={12} />
73
- {txt('refresh', 'Refresh')}
74
- </button>
75
- </div>
76
- );
77
- }
78
-
79
- // Group agents
80
- const connected = agents.filter(a => a.present && a.installed);
81
- const detected = agents.filter(a => a.present && !a.installed);
82
- const notFound = agents.filter(a => !a.present);
83
-
84
- return (
85
- <div className="space-y-5">
86
- {/* MCP Server Status */}
87
- <div className="rounded-xl border border-border bg-card p-4 flex items-center justify-between">
88
- <div className="flex items-center gap-3">
89
- <span className="text-sm font-medium text-foreground">{txt('mcpServer', 'MCP Server')}</span>
90
- {mcpStatus?.running ? (
91
- <span className="flex items-center gap-1.5 text-xs">
92
- <span className="w-2 h-2 rounded-full bg-emerald-500 inline-block" />
93
- <span className="text-emerald-600 dark:text-emerald-400">
94
- {txt('running', 'Running')} {txtFn<number>('onPort', (p) => `on :${p}`)(mcpStatus.port)}
95
- </span>
96
- </span>
97
- ) : (
98
- <span className="flex items-center gap-1.5 text-xs">
99
- <span className="w-2 h-2 rounded-full bg-zinc-400 inline-block" />
100
- <span className="text-muted-foreground">{txt('stopped', 'Not running')}</span>
101
- </span>
102
- )}
103
- </div>
104
- <button
105
- onClick={handleRefresh}
106
- disabled={refreshing}
107
- className="flex items-center gap-1.5 px-2.5 py-1 text-xs rounded-lg border border-border text-muted-foreground hover:text-foreground hover:bg-muted disabled:opacity-50 transition-colors"
108
- >
109
- <RefreshCw size={12} className={refreshing ? 'animate-spin' : ''} />
110
- {refreshing ? txt('refreshing', 'Refreshing...') : txt('refresh', 'Refresh')}
111
- </button>
112
- </div>
113
-
114
- {/* Connected Agents */}
115
- {connected.length > 0 && (
116
- <section>
117
- <h3 className="text-xs font-medium text-muted-foreground uppercase tracking-wider mb-3">
118
- {txtFn<number>('connectedCount', (n) => `Connected (${n})`)(connected.length)}
119
- </h3>
120
- <div className="space-y-2">
121
- {connected.map(agent => (
122
- <AgentCard key={agent.key} agent={agent} status="connected" />
123
- ))}
124
- </div>
125
- </section>
126
- )}
127
-
128
- {/* Detected but not configured */}
129
- {detected.length > 0 && (
130
- <section>
131
- <h3 className="text-xs font-medium text-muted-foreground uppercase tracking-wider mb-3">
132
- {txtFn<number>('detectedCount', (n) => `Detected but not configured (${n})`)(detected.length)}
133
- </h3>
134
- <div className="space-y-2">
135
- {detected.map(agent => (
136
- <AgentCard
137
- key={agent.key}
138
- agent={agent}
139
- status="detected"
140
- connectLabel={txt('connect', 'Connect')}
141
- />
142
- ))}
143
- </div>
144
- </section>
145
- )}
146
-
147
- {/* Not Detected */}
148
- {notFound.length > 0 && (
149
- <section>
150
- <button
151
- onClick={() => setShowNotDetected(!showNotDetected)}
152
- className="flex items-center gap-1.5 text-xs font-medium text-muted-foreground uppercase tracking-wider mb-3 hover:text-foreground transition-colors"
153
- >
154
- {showNotDetected ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
155
- {txtFn<number>('notDetectedCount', (n) => `Not Detected (${n})`)(notFound.length)}
156
- </button>
157
- {showNotDetected && (
158
- <div className="space-y-2">
159
- {notFound.map(agent => (
160
- <AgentCard key={agent.key} agent={agent} status="notFound" />
161
- ))}
162
- </div>
163
- )}
164
- </section>
165
- )}
166
-
167
- {/* Empty state */}
168
- {agents.length === 0 && (
169
- <p className="text-sm text-muted-foreground text-center py-4">
170
- {txt('noAgents', 'No agents detected on this machine.')}
171
- </p>
172
- )}
173
-
174
- {/* Auto-refresh hint */}
175
- <p className="text-[10px] text-muted-foreground/60 text-center">
176
- {txt('autoRefresh', 'Auto-refresh every 30s')}
177
- </p>
178
- </div>
179
- );
180
- }
181
-
182
- /* ── Agent Card ──────────────────────────────────────────────── */
183
-
184
- function AgentCard({
185
- agent,
186
- status,
187
- connectLabel,
188
- }: {
189
- agent: AgentInfo;
190
- status: 'connected' | 'detected' | 'notFound';
191
- connectLabel?: string;
192
- }) {
193
- const dot =
194
- status === 'connected' ? 'bg-emerald-500' :
195
- status === 'detected' ? 'bg-amber-500' :
196
- 'bg-zinc-400';
197
-
198
- return (
199
- <div className="rounded-lg border border-border bg-card/50 px-4 py-3 flex items-center justify-between gap-3">
200
- <div className="flex items-center gap-3 min-w-0">
201
- <span className={`w-2 h-2 rounded-full shrink-0 ${dot}`} />
202
- <span className="text-sm font-medium text-foreground truncate">{agent.name}</span>
203
- {status === 'connected' && (
204
- <div className="flex items-center gap-2 text-[11px] text-muted-foreground">
205
- <span className="px-1.5 py-0.5 rounded bg-muted">{agent.transport}</span>
206
- <span className="px-1.5 py-0.5 rounded bg-muted">{agent.scope}</span>
207
- {agent.configPath && (
208
- <span className="truncate max-w-[200px]" title={agent.configPath}>
209
- {agent.configPath.replace(/^.*[/\\]/, '')}
210
- </span>
211
- )}
212
- </div>
213
- )}
214
- </div>
215
- {status === 'detected' && (
216
- <a
217
- href="#"
218
- onClick={(e) => {
219
- e.preventDefault();
220
- // Navigate to MCP tab by dispatching a custom event
221
- const settingsModal = document.querySelector('[role="dialog"][aria-label="Settings"]');
222
- if (settingsModal) {
223
- const mcpBtn = settingsModal.querySelectorAll('button');
224
- for (const btn of mcpBtn) {
225
- if (btn.textContent?.trim() === 'MCP') {
226
- btn.click();
227
- break;
228
- }
229
- }
230
- }
231
- }}
232
- className="flex items-center gap-1 px-2.5 py-1 text-xs rounded-lg bg-amber-500/10 text-amber-600 dark:text-amber-400 hover:bg-amber-500/20 transition-colors shrink-0"
233
- >
234
- {connectLabel ?? 'Connect'}
235
- <ExternalLink size={11} />
236
- </a>
237
- )}
238
- </div>
239
- );
240
- }