@geminilight/mindos 0.5.50 → 0.5.52
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 +2 -2
- package/README_zh.md +1 -1
- package/app/app/api/mcp/install/route.ts +8 -1
- package/app/app/api/mcp/restart/route.ts +88 -0
- package/app/app/layout.tsx +2 -0
- package/app/components/UpdateOverlay.tsx +124 -0
- package/app/components/help/HelpContent.tsx +10 -7
- package/app/components/panels/AgentsPanel.tsx +32 -122
- package/app/components/settings/McpSkillsSection.tsx +88 -2
- package/app/components/settings/McpTab.tsx +258 -20
- package/app/components/settings/UpdateTab.tsx +65 -27
- package/app/lib/i18n-en.ts +4 -0
- package/app/lib/i18n-zh.ts +4 -0
- package/app/lib/mcp-agents.ts +14 -4
- package/app/next-env.d.ts +1 -1
- package/bin/lib/mcp-install.js +3 -3
- package/bin/lib/utils.js +12 -0
- package/package.json +1 -1
- package/scripts/setup.js +10 -3
- package/app/components/settings/AgentsTab.tsx +0 -240
package/bin/lib/mcp-install.js
CHANGED
|
@@ -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 =
|
|
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 =
|
|
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
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 =
|
|
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 =
|
|
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
|
-
'
|
|
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
|
-
}
|