@geminilight/mindos 0.5.29 → 0.5.32
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/components/HomeContent.tsx +20 -97
- package/app/components/SidebarLayout.tsx +3 -0
- package/app/components/panels/AgentsPanel.tsx +238 -92
- package/app/components/panels/PluginsPanel.tsx +79 -22
- package/app/components/settings/McpSkillsSection.tsx +12 -0
- package/app/components/settings/McpTab.tsx +28 -30
- package/app/hooks/useMcpData.tsx +166 -0
- package/app/lib/api.ts +21 -4
- package/app/lib/i18n-en.ts +18 -0
- package/app/lib/i18n-zh.ts +18 -0
- package/app/lib/mcp-snippets.ts +103 -0
- package/package.json +1 -1
- package/scripts/release.sh +15 -8
- package/scripts/setup.js +10 -6
- package/app/components/settings/McpServerStatus.tsx +0 -274
package/scripts/release.sh
CHANGED
|
@@ -92,26 +92,33 @@ git push origin main
|
|
|
92
92
|
git push origin "$VERSION"
|
|
93
93
|
echo ""
|
|
94
94
|
|
|
95
|
-
# 5. Wait for CI
|
|
95
|
+
# 5. Wait for CI
|
|
96
|
+
# Flow: tag push → sync-to-mindos (syncs code + tag to public repo) → public repo publish-npm
|
|
96
97
|
if command -v gh &>/dev/null; then
|
|
97
|
-
echo "⏳ Waiting for
|
|
98
|
+
echo "⏳ Waiting for sync → publish pipeline..."
|
|
99
|
+
echo " mindos-dev tag push → sync-to-mindos → GeminiLight/MindOS tag → npm publish"
|
|
98
100
|
TIMEOUT=120
|
|
99
101
|
ELAPSED=0
|
|
100
102
|
RUN_ID=""
|
|
101
103
|
|
|
102
|
-
#
|
|
104
|
+
# Watch the sync workflow on mindos-dev
|
|
103
105
|
while [ -z "$RUN_ID" ] && [ "$ELAPSED" -lt 30 ]; do
|
|
104
106
|
sleep 3
|
|
105
107
|
ELAPSED=$((ELAPSED + 3))
|
|
106
|
-
RUN_ID=$(gh run list --workflow=
|
|
108
|
+
RUN_ID=$(gh run list --workflow=sync-to-mindos.yml --limit=1 --json databaseId,headBranch --jq ".[0].databaseId" 2>/dev/null || true)
|
|
107
109
|
done
|
|
108
110
|
|
|
109
111
|
if [ -n "$RUN_ID" ]; then
|
|
110
|
-
gh run watch "$RUN_ID" --exit-status && echo "✅
|
|
112
|
+
gh run watch "$RUN_ID" --exit-status && echo "✅ Synced $VERSION to GeminiLight/MindOS" || echo "❌ Sync failed — check: gh run view $RUN_ID --log"
|
|
113
|
+
echo " npm publish will be triggered on GeminiLight/MindOS."
|
|
114
|
+
echo " Check: https://github.com/GeminiLight/MindOS/actions"
|
|
111
115
|
else
|
|
112
|
-
echo "⚠️ Could not find CI run. Check manually:
|
|
116
|
+
echo "⚠️ Could not find CI run. Check manually:"
|
|
117
|
+
echo " Sync: https://github.com/GeminiLight/mindos-dev/actions"
|
|
118
|
+
echo " Publish: https://github.com/GeminiLight/MindOS/actions"
|
|
113
119
|
fi
|
|
114
120
|
else
|
|
115
|
-
echo "💡
|
|
116
|
-
echo " Check
|
|
121
|
+
echo "💡 Release pipeline: mindos-dev → sync → GeminiLight/MindOS → npm publish"
|
|
122
|
+
echo " Check sync: https://github.com/GeminiLight/mindos-dev/actions"
|
|
123
|
+
echo " Check publish: https://github.com/GeminiLight/MindOS/actions"
|
|
117
124
|
fi
|
package/scripts/setup.js
CHANGED
|
@@ -186,14 +186,18 @@ function write(s) { process.stdout.write(s); }
|
|
|
186
186
|
// ── State ─────────────────────────────────────────────────────────────────────
|
|
187
187
|
|
|
188
188
|
function detectSystemLang() {
|
|
189
|
-
// Check env vars
|
|
190
|
-
|
|
191
|
-
|
|
189
|
+
// Check env vars by precedence (LC_ALL > LC_MESSAGES > LANG > LANGUAGE)
|
|
190
|
+
// For LANGUAGE, only use the first entry before ':' (it's a priority list)
|
|
191
|
+
const candidates = [
|
|
192
192
|
process.env.LC_ALL,
|
|
193
193
|
process.env.LC_MESSAGES,
|
|
194
|
-
process.env.
|
|
195
|
-
|
|
196
|
-
|
|
194
|
+
process.env.LANG,
|
|
195
|
+
...(process.env.LANGUAGE ? [process.env.LANGUAGE.split(':')[0]] : []),
|
|
196
|
+
];
|
|
197
|
+
const first = candidates.find(Boolean);
|
|
198
|
+
if (first) {
|
|
199
|
+
return first.toLowerCase().startsWith('zh') ? 'zh' : 'en';
|
|
200
|
+
}
|
|
197
201
|
|
|
198
202
|
// Fallback: Intl API (works on Windows where LANG is often unset)
|
|
199
203
|
try {
|
|
@@ -1,274 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { useState, useEffect, useMemo, useCallback } from 'react';
|
|
4
|
-
import { Plug, Copy, Check, ChevronDown, Monitor, Globe, Code } from 'lucide-react';
|
|
5
|
-
import { copyToClipboard } from '@/lib/clipboard';
|
|
6
|
-
import type { McpStatus, AgentInfo, McpServerStatusProps } from './types';
|
|
7
|
-
|
|
8
|
-
/* ── Helpers ───────────────────────────────────────────────────── */
|
|
9
|
-
|
|
10
|
-
function CopyButton({ text, label, copiedLabel }: { text: string; label: string; copiedLabel?: string }) {
|
|
11
|
-
const [copied, setCopied] = useState(false);
|
|
12
|
-
const handleCopy = useCallback(async () => {
|
|
13
|
-
const ok = await copyToClipboard(text);
|
|
14
|
-
if (ok) {
|
|
15
|
-
setCopied(true);
|
|
16
|
-
setTimeout(() => setCopied(false), 2000);
|
|
17
|
-
}
|
|
18
|
-
}, [text]);
|
|
19
|
-
return (
|
|
20
|
-
<button
|
|
21
|
-
type="button"
|
|
22
|
-
onClick={handleCopy}
|
|
23
|
-
className="inline-flex items-center gap-1 px-2.5 py-1 text-xs rounded-md border border-border text-muted-foreground hover:text-foreground hover:bg-muted transition-colors cursor-pointer shrink-0 relative z-10"
|
|
24
|
-
>
|
|
25
|
-
{copied ? <Check size={11} /> : <Copy size={11} />}
|
|
26
|
-
{copied ? (copiedLabel ?? 'Copied!') : label}
|
|
27
|
-
</button>
|
|
28
|
-
);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/* ── Config Snippet Generator ─────────────────────────────────── */
|
|
32
|
-
|
|
33
|
-
interface ConfigSnippet {
|
|
34
|
-
/** Snippet with full token — for clipboard copy */
|
|
35
|
-
snippet: string;
|
|
36
|
-
/** Snippet with masked token — for display in UI */
|
|
37
|
-
displaySnippet: string;
|
|
38
|
-
path: string;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function generateStdioSnippet(agent: AgentInfo): ConfigSnippet {
|
|
42
|
-
const stdioEntry: Record<string, unknown> = { type: 'stdio', command: 'mindos', args: ['mcp'] };
|
|
43
|
-
|
|
44
|
-
if (agent.format === 'toml') {
|
|
45
|
-
const lines = [
|
|
46
|
-
`[${agent.configKey}.mindos]`,
|
|
47
|
-
`command = "mindos"`,
|
|
48
|
-
`args = ["mcp"]`,
|
|
49
|
-
'',
|
|
50
|
-
`[${agent.configKey}.mindos.env]`,
|
|
51
|
-
`MCP_TRANSPORT = "stdio"`,
|
|
52
|
-
];
|
|
53
|
-
const s = lines.join('\n');
|
|
54
|
-
return { snippet: s, displaySnippet: s, path: agent.globalPath };
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (agent.globalNestedKey) {
|
|
58
|
-
const s = JSON.stringify({ [agent.configKey]: { mindos: stdioEntry } }, null, 2);
|
|
59
|
-
return { snippet: s, displaySnippet: s, path: agent.projectPath ?? agent.globalPath };
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const s = JSON.stringify({ [agent.configKey]: { mindos: stdioEntry } }, null, 2);
|
|
63
|
-
return { snippet: s, displaySnippet: s, path: agent.globalPath };
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function generateHttpSnippet(
|
|
67
|
-
agent: AgentInfo,
|
|
68
|
-
endpoint: string,
|
|
69
|
-
token?: string,
|
|
70
|
-
maskedToken?: string,
|
|
71
|
-
): ConfigSnippet {
|
|
72
|
-
// Full token for copy
|
|
73
|
-
const httpEntry: Record<string, unknown> = { url: endpoint };
|
|
74
|
-
if (token) httpEntry.headers = { Authorization: `Bearer ${token}` };
|
|
75
|
-
|
|
76
|
-
// Masked token for display
|
|
77
|
-
const displayEntry: Record<string, unknown> = { url: endpoint };
|
|
78
|
-
if (maskedToken) displayEntry.headers = { Authorization: `Bearer ${maskedToken}` };
|
|
79
|
-
|
|
80
|
-
const buildSnippet = (entry: Record<string, unknown>) => {
|
|
81
|
-
if (agent.format === 'toml') {
|
|
82
|
-
const lines = [
|
|
83
|
-
`[${agent.configKey}.mindos]`,
|
|
84
|
-
`type = "http"`,
|
|
85
|
-
`url = "${endpoint}"`,
|
|
86
|
-
];
|
|
87
|
-
const authVal = (entry.headers as Record<string, string>)?.Authorization;
|
|
88
|
-
if (authVal) {
|
|
89
|
-
lines.push('');
|
|
90
|
-
lines.push(`[${agent.configKey}.mindos.headers]`);
|
|
91
|
-
lines.push(`Authorization = "${authVal}"`);
|
|
92
|
-
}
|
|
93
|
-
return lines.join('\n');
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
if (agent.globalNestedKey) {
|
|
97
|
-
return JSON.stringify({ [agent.configKey]: { mindos: entry } }, null, 2);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
return JSON.stringify({ [agent.configKey]: { mindos: entry } }, null, 2);
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
return {
|
|
104
|
-
snippet: buildSnippet(httpEntry),
|
|
105
|
-
displaySnippet: buildSnippet(token ? displayEntry : httpEntry),
|
|
106
|
-
path: agent.format === 'toml' ? agent.globalPath : (agent.globalNestedKey ? (agent.projectPath ?? agent.globalPath) : agent.globalPath),
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/* ── MCP Server Status ─────────────────────────────────────────── */
|
|
111
|
-
|
|
112
|
-
export default function ServerStatus({ status, agents, t }: McpServerStatusProps) {
|
|
113
|
-
const m = t.settings?.mcp;
|
|
114
|
-
const [selectedAgent, setSelectedAgent] = useState<string>('');
|
|
115
|
-
const [mode, setMode] = useState<'stdio' | 'http'>('stdio');
|
|
116
|
-
const [showSnippet, setShowSnippet] = useState(false);
|
|
117
|
-
|
|
118
|
-
useEffect(() => {
|
|
119
|
-
if (agents.length > 0 && !selectedAgent) {
|
|
120
|
-
const first = agents.find(a => a.installed) ?? agents.find(a => a.present) ?? agents[0];
|
|
121
|
-
if (first) setSelectedAgent(first.key);
|
|
122
|
-
}
|
|
123
|
-
}, [agents, selectedAgent]);
|
|
124
|
-
|
|
125
|
-
useEffect(() => {
|
|
126
|
-
if (status?.endpoint && !status.endpoint.includes('127.0.0.1') && !status.endpoint.includes('localhost')) {
|
|
127
|
-
setMode('http');
|
|
128
|
-
}
|
|
129
|
-
}, [status?.endpoint]);
|
|
130
|
-
|
|
131
|
-
if (!status) return null;
|
|
132
|
-
|
|
133
|
-
const currentAgent = agents.find(a => a.key === selectedAgent);
|
|
134
|
-
|
|
135
|
-
const snippetResult = useMemo(() => {
|
|
136
|
-
if (!currentAgent) return null;
|
|
137
|
-
if (mode === 'stdio') return generateStdioSnippet(currentAgent);
|
|
138
|
-
return generateHttpSnippet(currentAgent, status.endpoint, status.authToken, status.maskedToken);
|
|
139
|
-
}, [currentAgent, status, mode]);
|
|
140
|
-
|
|
141
|
-
const isRemote = status.endpoint && !status.endpoint.includes('127.0.0.1') && !status.endpoint.includes('localhost');
|
|
142
|
-
|
|
143
|
-
return (
|
|
144
|
-
<div>
|
|
145
|
-
<h3 className="text-sm font-medium text-foreground mb-3">{m?.serverTitle ?? 'MCP Server'}</h3>
|
|
146
|
-
|
|
147
|
-
<div className="rounded-xl border p-4 space-y-3" style={{ borderColor: 'var(--border)', background: 'var(--card)' }}>
|
|
148
|
-
{/* Status line */}
|
|
149
|
-
<div className="flex items-center gap-2.5 text-xs">
|
|
150
|
-
<Plug size={14} className="text-muted-foreground shrink-0" />
|
|
151
|
-
<span className={`inline-block w-1.5 h-1.5 rounded-full shrink-0 ${status.running ? 'bg-success' : 'bg-muted-foreground'}`} />
|
|
152
|
-
<span className="text-foreground font-medium">
|
|
153
|
-
{status.running ? (m?.running ?? 'Running') : (m?.stopped ?? 'Stopped')}
|
|
154
|
-
</span>
|
|
155
|
-
{status.running && (
|
|
156
|
-
<>
|
|
157
|
-
<span className="text-muted-foreground">·</span>
|
|
158
|
-
<span className="font-mono text-muted-foreground">{status.transport.toUpperCase()}</span>
|
|
159
|
-
<span className="text-muted-foreground">·</span>
|
|
160
|
-
<span className="text-muted-foreground">{m?.toolsRegistered ? m.toolsRegistered(status.toolCount) : `${status.toolCount} tools`}</span>
|
|
161
|
-
<span className="text-muted-foreground">·</span>
|
|
162
|
-
<span className={status.authConfigured ? 'text-success' : 'text-muted-foreground'}>
|
|
163
|
-
{status.authConfigured ? (m?.authSet ?? 'Token set') : (m?.authNotSet ?? 'No token')}
|
|
164
|
-
</span>
|
|
165
|
-
</>
|
|
166
|
-
)}
|
|
167
|
-
</div>
|
|
168
|
-
|
|
169
|
-
{/* Endpoint + copy */}
|
|
170
|
-
<div className="flex items-center gap-2 text-xs">
|
|
171
|
-
<span className="text-muted-foreground shrink-0">{m?.endpoint ?? 'Endpoint'}</span>
|
|
172
|
-
<span className="font-mono text-foreground truncate">{status.endpoint}</span>
|
|
173
|
-
<CopyButton text={status.endpoint} label={m?.copyEndpoint ?? 'Copy'} copiedLabel={m?.copied} />
|
|
174
|
-
</div>
|
|
175
|
-
|
|
176
|
-
{/* Quick Setup */}
|
|
177
|
-
{agents.length > 0 && (
|
|
178
|
-
<div className="pt-2 border-t border-border space-y-2.5">
|
|
179
|
-
{/* Agent selector + transport mode toggle */}
|
|
180
|
-
<div className="flex items-center gap-2 text-xs flex-wrap">
|
|
181
|
-
<span className="text-muted-foreground shrink-0">{m?.configureFor ?? 'Configure for'}</span>
|
|
182
|
-
<select
|
|
183
|
-
value={selectedAgent}
|
|
184
|
-
onChange={e => setSelectedAgent(e.target.value)}
|
|
185
|
-
className="text-xs px-2 py-1 rounded-md border border-border bg-background text-foreground outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
|
186
|
-
>
|
|
187
|
-
{agents.map(a => (
|
|
188
|
-
<option key={a.key} value={a.key}>
|
|
189
|
-
{a.name}{a.installed ? ' ✓' : a.present ? ' ·' : ''}
|
|
190
|
-
</option>
|
|
191
|
-
))}
|
|
192
|
-
</select>
|
|
193
|
-
|
|
194
|
-
<div className="flex items-center rounded-md border border-border overflow-hidden ml-auto">
|
|
195
|
-
<button
|
|
196
|
-
type="button"
|
|
197
|
-
onClick={() => setMode('stdio')}
|
|
198
|
-
className={`flex items-center gap-1 px-2 py-1 text-xs transition-colors ${
|
|
199
|
-
mode === 'stdio'
|
|
200
|
-
? 'bg-muted text-foreground font-medium'
|
|
201
|
-
: 'text-muted-foreground hover:text-foreground'
|
|
202
|
-
}`}
|
|
203
|
-
title={m?.transportLocalHint ?? 'Local — same machine as MindOS server'}
|
|
204
|
-
>
|
|
205
|
-
<Monitor size={11} />
|
|
206
|
-
{m?.transportLocal ?? 'Local'}
|
|
207
|
-
</button>
|
|
208
|
-
<button
|
|
209
|
-
type="button"
|
|
210
|
-
onClick={() => setMode('http')}
|
|
211
|
-
className={`flex items-center gap-1 px-2 py-1 text-xs transition-colors ${
|
|
212
|
-
mode === 'http'
|
|
213
|
-
? 'bg-muted text-foreground font-medium'
|
|
214
|
-
: 'text-muted-foreground hover:text-foreground'
|
|
215
|
-
}`}
|
|
216
|
-
title={m?.transportRemoteHint ?? 'Remote — connect from another device via HTTP'}
|
|
217
|
-
>
|
|
218
|
-
<Globe size={11} />
|
|
219
|
-
{m?.transportRemote ?? 'Remote'}
|
|
220
|
-
</button>
|
|
221
|
-
</div>
|
|
222
|
-
</div>
|
|
223
|
-
|
|
224
|
-
{/* Hint for remote mode */}
|
|
225
|
-
{mode === 'http' && (
|
|
226
|
-
<div className="text-[11px] text-muted-foreground leading-relaxed space-y-1">
|
|
227
|
-
<p>
|
|
228
|
-
{isRemote
|
|
229
|
-
? (m?.remoteDetectedHint ?? 'Using your current remote IP.')
|
|
230
|
-
: (m?.remoteManualHint ?? 'Replace 127.0.0.1 with your server\'s public or LAN IP.')}
|
|
231
|
-
</p>
|
|
232
|
-
<p>
|
|
233
|
-
{(m?.remoteSteps ?? 'To connect from another device: ① Open port {port} in firewall/security group ② Use the config below in your Agent ③ For public networks, consider SSH tunnel for encryption.')
|
|
234
|
-
.replace('{port}', String(status.port))}
|
|
235
|
-
</p>
|
|
236
|
-
{!status.authConfigured && (
|
|
237
|
-
<p className="text-amber-500">{m?.noAuthWarning ?? '⚠ No auth token — set one in Settings → General before enabling remote access.'}</p>
|
|
238
|
-
)}
|
|
239
|
-
</div>
|
|
240
|
-
)}
|
|
241
|
-
|
|
242
|
-
{/* Copy config + show JSON toggle */}
|
|
243
|
-
{snippetResult && (
|
|
244
|
-
<div className="space-y-2">
|
|
245
|
-
<div className="flex items-center gap-2 text-xs flex-wrap">
|
|
246
|
-
{/* Copy button uses full token (snippetResult.snippet) */}
|
|
247
|
-
<CopyButton text={snippetResult.snippet} label={m?.copyConfig ?? 'Copy Config'} copiedLabel={m?.copied} />
|
|
248
|
-
<span className="text-muted-foreground">→</span>
|
|
249
|
-
<span className="font-mono text-muted-foreground text-[11px] truncate">{snippetResult.path}</span>
|
|
250
|
-
<button
|
|
251
|
-
type="button"
|
|
252
|
-
onClick={() => setShowSnippet(!showSnippet)}
|
|
253
|
-
className="inline-flex items-center gap-1 px-1.5 py-0.5 text-[11px] text-muted-foreground hover:text-foreground transition-colors ml-auto"
|
|
254
|
-
>
|
|
255
|
-
<Code size={10} />
|
|
256
|
-
{showSnippet ? (m?.hideJson ?? 'Hide JSON') : (m?.showJson ?? 'Show JSON')}
|
|
257
|
-
<ChevronDown size={10} className={`transition-transform ${showSnippet ? 'rotate-180' : ''}`} />
|
|
258
|
-
</button>
|
|
259
|
-
</div>
|
|
260
|
-
|
|
261
|
-
{/* Display snippet uses masked token */}
|
|
262
|
-
{showSnippet && (
|
|
263
|
-
<pre className="text-xs font-mono bg-muted/50 border border-border rounded-lg p-3 overflow-x-auto whitespace-pre select-all">
|
|
264
|
-
{snippetResult.displaySnippet}
|
|
265
|
-
</pre>
|
|
266
|
-
)}
|
|
267
|
-
</div>
|
|
268
|
-
)}
|
|
269
|
-
</div>
|
|
270
|
-
)}
|
|
271
|
-
</div>
|
|
272
|
-
</div>
|
|
273
|
-
);
|
|
274
|
-
}
|