@geminilight/mindos 0.5.29 → 0.5.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,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
- }