@geminilight/mindos 0.5.26 → 0.5.27

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,20 +1,37 @@
1
1
  export const dynamic = 'force-dynamic';
2
- import { NextResponse } from 'next/server';
2
+ import { NextRequest, NextResponse } from 'next/server';
3
3
  import { readSettings } from '@/lib/settings';
4
+ import { maskToken } from '@/lib/format';
4
5
 
5
- export async function GET() {
6
+ /** Parse hostname from Host header, handling IPv6 brackets */
7
+ function parseHostname(host: string): string {
8
+ // IPv6: [::1]:3003 → [::1]
9
+ if (host.includes(']')) {
10
+ return host.slice(0, host.lastIndexOf(']') + 1);
11
+ }
12
+ // IPv4/hostname: 192.168.1.1:3003 → 192.168.1.1
13
+ const colonIdx = host.lastIndexOf(':');
14
+ return colonIdx > 0 ? host.slice(0, colonIdx) : host;
15
+ }
16
+
17
+ export async function GET(req: NextRequest) {
6
18
  try {
7
19
  const settings = readSettings();
8
20
  const port = settings.mcpPort ?? 8781;
9
- const baseUrl = `http://127.0.0.1:${port}`;
10
- const endpoint = `${baseUrl}/mcp`;
11
- const authConfigured = !!settings.authToken;
21
+ const token = settings.authToken ?? '';
22
+ const authConfigured = !!token;
23
+
24
+ // Derive endpoint from the request's host so remote users see the correct IP/hostname
25
+ const reqHost = req.headers.get('host') ?? `127.0.0.1:${port}`;
26
+ const hostname = parseHostname(reqHost);
27
+ const endpoint = `http://${hostname}:${port}/mcp`;
28
+
29
+ // Health check always goes to localhost (server-to-self)
30
+ const healthUrl = `http://127.0.0.1:${port}/api/health`;
12
31
 
13
32
  let running = false;
14
33
 
15
34
  try {
16
- // Use the health endpoint — avoids MCP handshake complexity
17
- const healthUrl = `${baseUrl}/api/health`;
18
35
  const controller = new AbortController();
19
36
  const timeout = setTimeout(() => controller.abort(), 2000);
20
37
  const res = await fetch(healthUrl, { signal: controller.signal, cache: 'no-store' });
@@ -35,6 +52,12 @@ export async function GET() {
35
52
  port,
36
53
  toolCount: running ? 20 : 0,
37
54
  authConfigured,
55
+ // Masked for display; full token only used server-side in snippet generation
56
+ maskedToken: authConfigured ? maskToken(token) : undefined,
57
+ // Full token for config snippet copy — this API is protected by proxy.ts middleware
58
+ // (same-origin or bearer token required). Consistent with /api/settings which also
59
+ // exposes the token to authenticated users.
60
+ authToken: authConfigured ? token : undefined,
38
61
  });
39
62
  } catch (err) {
40
63
  return NextResponse.json({ error: String(err) }, { status: 500 });
@@ -7,6 +7,7 @@ import rehypeRaw from 'rehype-raw';
7
7
  import rehypeSlug from 'rehype-slug';
8
8
  import { useState, useCallback } from 'react';
9
9
  import { Copy, Check } from 'lucide-react';
10
+ import { copyToClipboard } from '@/lib/clipboard';
10
11
  import type { Components } from 'react-markdown';
11
12
 
12
13
  interface MarkdownViewProps {
@@ -16,9 +17,11 @@ interface MarkdownViewProps {
16
17
  function CopyButton({ code }: { code: string }) {
17
18
  const [copied, setCopied] = useState(false);
18
19
  const handleCopy = useCallback(() => {
19
- navigator.clipboard.writeText(code).then(() => {
20
- setCopied(true);
21
- setTimeout(() => setCopied(false), 2000);
20
+ copyToClipboard(code).then((ok) => {
21
+ if (ok) {
22
+ setCopied(true);
23
+ setTimeout(() => setCopied(false), 2000);
24
+ }
22
25
  });
23
26
  }, [code]);
24
27
 
@@ -5,6 +5,7 @@ import { Copy, Check, RefreshCw, Trash2, Sparkles, ChevronDown, ChevronRight, Lo
5
5
  import type { KnowledgeTabProps } from './types';
6
6
  import { Field, Input, EnvBadge, SectionLabel, Toggle } from './Primitives';
7
7
  import { apiFetch } from '@/lib/api';
8
+ import { copyToClipboard } from '@/lib/clipboard';
8
9
  import { formatBytes, formatUptime } from '@/lib/format';
9
10
 
10
11
  export function KnowledgeTab({ data, setData, t }: KnowledgeTabProps) {
@@ -85,9 +86,11 @@ export function KnowledgeTab({ data, setData, t }: KnowledgeTabProps) {
85
86
  function handleCopy() {
86
87
  const text = revealedToken ?? data.authToken ?? '';
87
88
  if (!text) return;
88
- navigator.clipboard.writeText(text).then(() => {
89
- setCopied(true);
90
- setTimeout(() => setCopied(false), 2000);
89
+ copyToClipboard(text).then((ok) => {
90
+ if (ok) {
91
+ setCopied(true);
92
+ setTimeout(() => setCopied(false), 2000);
93
+ }
91
94
  });
92
95
  }
93
96
 
@@ -1,24 +1,26 @@
1
1
  'use client';
2
2
 
3
- import { useState, useEffect, useMemo } from 'react';
4
- import { Plug, Copy, Check, ChevronDown } from 'lucide-react';
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';
5
6
  import type { McpStatus, AgentInfo, McpServerStatusProps } from './types';
6
7
 
7
8
  /* ── Helpers ───────────────────────────────────────────────────── */
8
9
 
9
10
  function CopyButton({ text, label, copiedLabel }: { text: string; label: string; copiedLabel?: string }) {
10
11
  const [copied, setCopied] = useState(false);
11
- const handleCopy = async () => {
12
- try {
13
- await navigator.clipboard.writeText(text);
12
+ const handleCopy = useCallback(async () => {
13
+ const ok = await copyToClipboard(text);
14
+ if (ok) {
14
15
  setCopied(true);
15
16
  setTimeout(() => setCopied(false), 2000);
16
- } catch { /* clipboard unavailable */ }
17
- };
17
+ }
18
+ }, [text]);
18
19
  return (
19
20
  <button
21
+ type="button"
20
22
  onClick={handleCopy}
21
- className="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"
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"
22
24
  >
23
25
  {copied ? <Check size={11} /> : <Copy size={11} />}
24
26
  {copied ? (copiedLabel ?? 'Copied!') : label}
@@ -28,60 +30,91 @@ function CopyButton({ text, label, copiedLabel }: { text: string; label: string;
28
30
 
29
31
  /* ── Config Snippet Generator ─────────────────────────────────── */
30
32
 
31
- function generateConfigSnippet(
32
- agent: AgentInfo,
33
- status: McpStatus,
34
- token?: string,
35
- ): { snippet: string; path: string } {
36
- const isRunning = status.running;
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
+ }
37
40
 
38
- // Determine entry (stdio vs http)
41
+ function generateStdioSnippet(agent: AgentInfo): ConfigSnippet {
39
42
  const stdioEntry: Record<string, unknown> = { type: 'stdio', command: 'mindos', args: ['mcp'] };
40
- const httpEntry: Record<string, unknown> = { url: status.endpoint };
41
- if (token) httpEntry.headers = { Authorization: `Bearer ${token}` };
42
- const entry = isRunning ? httpEntry : stdioEntry;
43
43
 
44
- // TOML format (Codex)
45
44
  if (agent.format === 'toml') {
46
- const lines: string[] = [`[${agent.configKey}.mindos]`];
47
- if (isRunning) {
48
- lines.push(`type = "http"`);
49
- lines.push(`url = "${status.endpoint}"`);
50
- if (token) {
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) {
51
89
  lines.push('');
52
90
  lines.push(`[${agent.configKey}.mindos.headers]`);
53
- lines.push(`Authorization = "Bearer ${token}"`);
91
+ lines.push(`Authorization = "${authVal}"`);
54
92
  }
55
- } else {
56
- lines.push(`command = "mindos"`);
57
- lines.push(`args = ["mcp"]`);
58
- lines.push('');
59
- lines.push(`[${agent.configKey}.mindos.env]`);
60
- lines.push(`MCP_TRANSPORT = "stdio"`);
93
+ return lines.join('\n');
61
94
  }
62
- return { snippet: lines.join('\n'), path: agent.globalPath };
63
- }
64
95
 
65
- // JSON with globalNestedKey (VS Code project-level uses flat key)
66
- if (agent.globalNestedKey) {
67
- // project-level: flat key structure
68
- const projectSnippet = JSON.stringify({ [agent.configKey]: { mindos: entry } }, null, 2);
69
- return { snippet: projectSnippet, path: agent.projectPath ?? agent.globalPath };
70
- }
96
+ if (agent.globalNestedKey) {
97
+ return JSON.stringify({ [agent.configKey]: { mindos: entry } }, null, 2);
98
+ }
71
99
 
72
- // Standard JSON
73
- const snippet = JSON.stringify({ [agent.configKey]: { mindos: entry } }, null, 2);
74
- return { snippet, path: agent.globalPath };
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
+ };
75
108
  }
76
109
 
77
110
  /* ── MCP Server Status ─────────────────────────────────────────── */
78
111
 
79
112
  export default function ServerStatus({ status, agents, t }: McpServerStatusProps) {
80
113
  const m = t.settings?.mcp;
81
- const [expanded, setExpanded] = useState(false);
82
114
  const [selectedAgent, setSelectedAgent] = useState<string>('');
115
+ const [mode, setMode] = useState<'stdio' | 'http'>('stdio');
116
+ const [showSnippet, setShowSnippet] = useState(false);
83
117
 
84
- // Auto-select first installed or first detected agent
85
118
  useEffect(() => {
86
119
  if (agents.length > 0 && !selectedAgent) {
87
120
  const first = agents.find(a => a.installed) ?? agents.find(a => a.present) ?? agents[0];
@@ -89,84 +122,147 @@ export default function ServerStatus({ status, agents, t }: McpServerStatusProps
89
122
  }
90
123
  }, [agents, selectedAgent]);
91
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
+
92
131
  if (!status) return null;
93
132
 
94
133
  const currentAgent = agents.find(a => a.key === selectedAgent);
95
- // 🟡 MINOR #9: Memoize snippet generation to avoid recomputing on every render
96
- const snippetResult = useMemo(() => currentAgent ? generateConfigSnippet(currentAgent, status) : null, [currentAgent, status]);
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');
97
142
 
98
143
  return (
99
144
  <div>
100
- {/* Summary line always visible */}
101
- <button
102
- type="button"
103
- onClick={() => setExpanded(!expanded)}
104
- className="w-full flex items-center gap-2.5 text-xs"
105
- >
106
- <Plug size={14} className="text-muted-foreground shrink-0" />
107
- <span className={`inline-block w-1.5 h-1.5 rounded-full shrink-0 ${status.running ? 'bg-success' : 'bg-muted-foreground'}`} />
108
- <span className="text-foreground font-medium">
109
- {status.running ? (m?.running ?? 'Running') : (m?.stopped ?? 'Stopped')}
110
- </span>
111
- {status.running && (
112
- <>
113
- <span className="text-muted-foreground">·</span>
114
- <span className="font-mono text-muted-foreground">{status.transport.toUpperCase()}</span>
115
- <span className="text-muted-foreground">·</span>
116
- <span className="text-muted-foreground">{m?.toolsRegistered ? m.toolsRegistered(status.toolCount) : `${status.toolCount} tools`}</span>
117
- <span className="text-muted-foreground">·</span>
118
- <span className={status.authConfigured ? 'text-success' : 'text-muted-foreground'}>
119
- {status.authConfigured ? (m?.authSet ?? 'Token set') : (m?.authNotSet ?? 'No token')}
120
- </span>
121
- </>
122
- )}
123
- <ChevronDown size={12} className={`ml-auto text-muted-foreground transition-transform shrink-0 ${expanded ? 'rotate-180' : ''}`} />
124
- </button>
125
-
126
- {/* Expanded details */}
127
- {expanded && (
128
- <div className="pt-3 mt-3 border-t border-border space-y-3">
129
- {/* Endpoint + copy */}
130
- <div className="flex items-center gap-2 text-xs">
131
- <span className="text-muted-foreground shrink-0">{m?.endpoint ?? 'Endpoint'}</span>
132
- <span className="font-mono text-foreground truncate">{status.endpoint}</span>
133
- <CopyButton text={status.endpoint} label={m?.copyEndpoint ?? 'Copy'} copiedLabel={m?.copied} />
134
- </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>
135
193
 
136
- {/* Quick Setup */}
137
- {agents.length > 0 && (
138
- <div className="space-y-2.5">
139
- <div className="flex items-center gap-2 text-xs">
140
- <span className="text-muted-foreground shrink-0">{m?.configureFor ?? 'Configure for'}</span>
141
- <select
142
- value={selectedAgent}
143
- onChange={e => setSelectedAgent(e.target.value)}
144
- 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"
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'}
145
204
  >
146
- {agents.map(a => (
147
- <option key={a.key} value={a.key}>
148
- {a.name}{a.installed ? ' ✓' : a.present ? ' ·' : ''}
149
- </option>
150
- ))}
151
- </select>
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>
152
221
  </div>
222
+ </div>
153
223
 
154
- {snippetResult && (
155
- <>
156
- <div className="flex items-center gap-2 text-xs">
157
- <span className="text-muted-foreground shrink-0">{m?.configPath ?? 'Config path'}</span>
158
- <span className="font-mono text-foreground text-2xs">{snippetResult.path}</span>
159
- </div>
160
- <pre className="text-xs font-mono bg-muted/50 border border-border rounded-lg p-3 overflow-x-auto whitespace-pre">
161
- {snippetResult.snippet}
162
- </pre>
224
+ {/* Hint for remote mode */}
225
+ {mode === 'http' && (
226
+ <p className="text-[11px] text-muted-foreground leading-relaxed">
227
+ {isRemote
228
+ ? (m?.remoteDetectedHint ?? 'Uses your current remote IP. Ensure the MCP port is accessible from the target device.')
229
+ : (m?.remoteManualHint ?? 'Tip: access MindOS from a remote device to auto-detect the correct IP, or replace 127.0.0.1 with your server\'s LAN IP.')}
230
+ {!status.authConfigured && (
231
+ <span className="text-amber-500 ml-1">{m?.noAuthWarning ?? '⚠ No auth token set — configure one in Settings → General for secure remote access.'}</span>
232
+ )}
233
+ </p>
234
+ )}
235
+
236
+ {/* Copy config + show JSON toggle */}
237
+ {snippetResult && (
238
+ <div className="space-y-2">
239
+ <div className="flex items-center gap-2 text-xs flex-wrap">
240
+ {/* Copy button uses full token (snippetResult.snippet) */}
163
241
  <CopyButton text={snippetResult.snippet} label={m?.copyConfig ?? 'Copy Config'} copiedLabel={m?.copied} />
164
- </>
165
- )}
166
- </div>
167
- )}
168
- </div>
169
- )}
242
+ <span className="text-muted-foreground">→</span>
243
+ <span className="font-mono text-muted-foreground text-[11px] truncate">{snippetResult.path}</span>
244
+ <button
245
+ type="button"
246
+ onClick={() => setShowSnippet(!showSnippet)}
247
+ 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"
248
+ >
249
+ <Code size={10} />
250
+ {showSnippet ? (m?.hideJson ?? 'Hide JSON') : (m?.showJson ?? 'Show JSON')}
251
+ <ChevronDown size={10} className={`transition-transform ${showSnippet ? 'rotate-180' : ''}`} />
252
+ </button>
253
+ </div>
254
+
255
+ {/* Display snippet uses masked token */}
256
+ {showSnippet && (
257
+ <pre className="text-xs font-mono bg-muted/50 border border-border rounded-lg p-3 overflow-x-auto whitespace-pre select-all">
258
+ {snippetResult.displaySnippet}
259
+ </pre>
260
+ )}
261
+ </div>
262
+ )}
263
+ </div>
264
+ )}
265
+ </div>
170
266
  </div>
171
267
  );
172
268
  }
@@ -42,10 +42,8 @@ export function McpTab({ t }: McpTabProps) {
42
42
 
43
43
  return (
44
44
  <div className="space-y-6">
45
- {/* MCP Server Status — compact card */}
46
- <div className="rounded-xl border p-4" style={{ borderColor: 'var(--border)', background: 'var(--card)' }}>
47
- <ServerStatus status={mcpStatus} agents={agents} t={t} />
48
- </div>
45
+ {/* MCP Server Status */}
46
+ <ServerStatus status={mcpStatus} agents={agents} t={t} />
49
47
 
50
48
  {/* Skills */}
51
49
  <div>
@@ -58,6 +58,8 @@ export interface McpStatus {
58
58
  port: number;
59
59
  toolCount: number;
60
60
  authConfigured: boolean;
61
+ maskedToken?: string;
62
+ authToken?: string;
61
63
  }
62
64
 
63
65
  export interface AgentInfo {
@@ -3,6 +3,7 @@
3
3
  import { useState, useEffect, useCallback } from 'react';
4
4
  import { Sparkles, Loader2, ChevronLeft, ChevronRight } from 'lucide-react';
5
5
  import { useLocale } from '@/lib/LocaleContext';
6
+ import { copyToClipboard } from '@/lib/clipboard';
6
7
  import type { SetupState, PortStatus, AgentEntry, AgentInstallStatus } from './types';
7
8
  import { TOTAL_STEPS, STEP_KB, STEP_PORTS, STEP_AGENTS } from './constants';
8
9
  import StepKB from './StepKB';
@@ -229,7 +230,7 @@ export default function SetupWizard() {
229
230
  }, []);
230
231
 
231
232
  const copyToken = useCallback(() => {
232
- navigator.clipboard.writeText(state.authToken).catch(() => { /* clipboard unavailable in insecure context */ });
233
+ copyToClipboard(state.authToken).catch(() => {});
233
234
  setTokenCopied(true);
234
235
  setTimeout(() => setTokenCopied(false), 2000);
235
236
  }, [state.authToken]);
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Copy text to clipboard with fallback for non-HTTPS environments.
3
+ * Returns true if successful.
4
+ */
5
+ export async function copyToClipboard(text: string): Promise<boolean> {
6
+ // Modern Clipboard API (requires HTTPS or localhost)
7
+ if (navigator.clipboard?.writeText) {
8
+ try {
9
+ await navigator.clipboard.writeText(text);
10
+ return true;
11
+ } catch { /* falls through to fallback */ }
12
+ }
13
+ // Fallback: textarea + execCommand (works over HTTP).
14
+ // execCommand('copy') is deprecated but remains the only option for
15
+ // non-secure contexts. No replacement exists yet — monitor browser support.
16
+ try {
17
+ const textarea = document.createElement('textarea');
18
+ textarea.value = text;
19
+ textarea.style.position = 'fixed';
20
+ textarea.style.opacity = '0';
21
+ document.body.appendChild(textarea);
22
+ textarea.select();
23
+ const ok = document.execCommand('copy');
24
+ document.body.removeChild(textarea);
25
+ return ok;
26
+ } catch {
27
+ return false;
28
+ }
29
+ }
package/app/lib/format.ts CHANGED
@@ -17,3 +17,9 @@ export function formatUptime(ms: number): string {
17
17
  const d = Math.floor(h / 24);
18
18
  return `${d}d ${h % 24}h`;
19
19
  }
20
+
21
+ /** Mask a token for display: show first 4 and last 4 chars, middle replaced with bullets */
22
+ export function maskToken(token: string): string {
23
+ if (token.length <= 12) return '••••';
24
+ return `${token.slice(0, 4)}••••${token.slice(-4)}`;
25
+ }
@@ -261,6 +261,15 @@ export const en = {
261
261
  quickSetup: 'Quick Setup',
262
262
  configureFor: 'Configure for',
263
263
  configPath: 'Config path',
264
+ transportLocal: 'Local',
265
+ transportRemote: 'Remote',
266
+ transportLocalHint: 'Local — same machine as MindOS server',
267
+ transportRemoteHint: 'Remote — connect from another device via HTTP',
268
+ remoteDetectedHint: 'Uses your current remote IP. Ensure the MCP port is accessible from the target device.',
269
+ remoteManualHint: 'Tip: access MindOS from a remote device to auto-detect the correct IP, or replace 127.0.0.1 with your server\'s LAN IP.',
270
+ noAuthWarning: '⚠ No auth token set — configure one in Settings → General for secure remote access.',
271
+ showJson: 'Show JSON',
272
+ hideJson: 'Hide JSON',
264
273
  },
265
274
  monitoring: {
266
275
  system: 'System',
@@ -286,6 +286,15 @@ export const zh = {
286
286
  quickSetup: '快速配置',
287
287
  configureFor: '配置目标',
288
288
  configPath: '配置路径',
289
+ transportLocal: '本地',
290
+ transportRemote: '远程',
291
+ transportLocalHint: '本地 — 与 MindOS 服务在同一台机器上',
292
+ transportRemoteHint: '远程 — 从其他设备通过 HTTP 连接',
293
+ remoteDetectedHint: '使用当前远程 IP。请确保目标设备可以访问 MCP 端口。',
294
+ remoteManualHint: '提示:从远程设备访问 MindOS 可自动检测正确 IP,或手动将 127.0.0.1 替换为服务器的局域网 IP。',
295
+ noAuthWarning: '⚠ 未设置认证令牌 — 请在 设置 → 通用 中配置,以确保远程访问安全。',
296
+ showJson: '显示 JSON',
297
+ hideJson: '隐藏 JSON',
289
298
  },
290
299
  monitoring: {
291
300
  system: '系统',
package/app/next-env.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /// <reference types="next" />
2
2
  /// <reference types="next/image-types/global" />
3
- import "./.next/types/routes.d.ts";
3
+ import "./.next/dev/types/routes.d.ts";
4
4
 
5
5
  // NOTE: This file should not be edited
6
6
  // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geminilight/mindos",
3
- "version": "0.5.26",
3
+ "version": "0.5.27",
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",
@@ -18,6 +18,67 @@ echo "🧪 Running tests..."
18
18
  npm test
19
19
  echo ""
20
20
 
21
+ # 3. Smoke test: pack → install in temp dir → verify CLI works
22
+ echo "🔍 Smoke testing package..."
23
+ SMOKE_DIR=$(mktemp -d)
24
+ TARBALL=$(npm pack --pack-destination "$SMOKE_DIR" 2>/dev/null | tail -1)
25
+ TARBALL_PATH="$SMOKE_DIR/$TARBALL"
26
+
27
+ if [ ! -f "$TARBALL_PATH" ]; then
28
+ echo "❌ npm pack failed — tarball not found"
29
+ rm -rf "$SMOKE_DIR"
30
+ exit 1
31
+ fi
32
+
33
+ TARBALL_SIZE=$(du -sh "$TARBALL_PATH" | cut -f1)
34
+ echo " 📦 Tarball: $TARBALL ($TARBALL_SIZE)"
35
+
36
+ # Install from tarball in isolation (production deps only)
37
+ cd "$SMOKE_DIR"
38
+ npm init -y --silent >/dev/null 2>&1
39
+ npm install "$TARBALL_PATH" --ignore-scripts >/dev/null 2>&1
40
+
41
+ # Verify bin entry exists and is executable
42
+ if [ ! -f "$SMOKE_DIR/node_modules/.bin/mindos" ]; then
43
+ echo "❌ 'mindos' binary not found after install"
44
+ rm -rf "$SMOKE_DIR"
45
+ exit 1
46
+ fi
47
+
48
+ # Verify --version works
49
+ INSTALLED_VERSION=$("$SMOKE_DIR/node_modules/.bin/mindos" --version 2>&1 || true)
50
+ if [ -z "$INSTALLED_VERSION" ]; then
51
+ echo "❌ 'mindos --version' returned empty"
52
+ rm -rf "$SMOKE_DIR"
53
+ exit 1
54
+ fi
55
+ echo " ✅ mindos --version → $INSTALLED_VERSION"
56
+
57
+ # Verify --help works (exits 0, produces output)
58
+ HELP_OUTPUT=$("$SMOKE_DIR/node_modules/.bin/mindos" --help 2>&1 || true)
59
+ if ! echo "$HELP_OUTPUT" | grep -qi "mindos"; then
60
+ echo "❌ 'mindos --help' did not produce expected output"
61
+ rm -rf "$SMOKE_DIR"
62
+ exit 1
63
+ fi
64
+ echo " ✅ mindos --help works"
65
+
66
+ # Verify key files are present in the installed package
67
+ for f in bin/cli.js app/package.json app/next.config.ts skills/mindos/SKILL.md; do
68
+ if [ ! -f "$SMOKE_DIR/node_modules/@geminilight/mindos/$f" ]; then
69
+ echo "❌ Missing file in package: $f"
70
+ rm -rf "$SMOKE_DIR"
71
+ exit 1
72
+ fi
73
+ done
74
+ echo " ✅ Key files present"
75
+
76
+ # Cleanup
77
+ rm -rf "$SMOKE_DIR"
78
+ cd - >/dev/null
79
+ echo " 🟢 Smoke test passed"
80
+ echo ""
81
+
21
82
  # 3. Bump version (creates commit + tag automatically)
22
83
  echo "📦 Bumping version ($BUMP)..."
23
84
  npm version "$BUMP" -m "%s"