@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.
- package/app/app/api/mcp/status/route.ts +30 -7
- package/app/components/MarkdownView.tsx +6 -3
- package/app/components/settings/KnowledgeTab.tsx +6 -3
- package/app/components/settings/McpServerStatus.tsx +207 -111
- package/app/components/settings/McpTab.tsx +2 -4
- package/app/components/settings/types.ts +2 -0
- package/app/components/setup/index.tsx +2 -1
- package/app/lib/clipboard.ts +29 -0
- package/app/lib/format.ts +6 -0
- package/app/lib/i18n-en.ts +9 -0
- package/app/lib/i18n-zh.ts +9 -0
- package/app/next-env.d.ts +1 -1
- package/package.json +1 -1
- package/scripts/release.sh +61 -0
|
@@ -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
|
-
|
|
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
|
|
10
|
-
const
|
|
11
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
13
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
token
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
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
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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 = "
|
|
91
|
+
lines.push(`Authorization = "${authVal}"`);
|
|
54
92
|
}
|
|
55
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
96
|
-
const snippetResult = useMemo(() =>
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
className="
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
{
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
|
46
|
-
<
|
|
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>
|
|
@@ -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
|
-
|
|
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
|
+
}
|
package/app/lib/i18n-en.ts
CHANGED
|
@@ -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',
|
package/app/lib/i18n-zh.ts
CHANGED
|
@@ -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
package/scripts/release.sh
CHANGED
|
@@ -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"
|