@geminilight/mindos 0.5.26 → 0.5.28
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 +213 -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 +10 -0
- package/app/lib/i18n-zh.ts +10 -0
- package/app/next.config.ts +1 -1
- package/bin/lib/mcp-spawn.js +13 -2
- package/mcp/src/index.ts +3 -2
- 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
|
-
|
|
69
|
-
return {
|
|
70
|
-
}
|
|
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
|
+
};
|
|
71
102
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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,153 @@ 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
|
-
<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>
|
|
135
146
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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'}
|
|
145
217
|
>
|
|
146
|
-
{
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
</option>
|
|
150
|
-
))}
|
|
151
|
-
</select>
|
|
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
|
+
<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) */}
|
|
163
247
|
<CopyButton text={snippetResult.snippet} label={m?.copyConfig ?? 'Copy Config'} copiedLabel={m?.copied} />
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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>
|
|
170
272
|
</div>
|
|
171
273
|
);
|
|
172
274
|
}
|
|
@@ -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,16 @@ 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: 'Using your current remote IP.',
|
|
269
|
+
remoteManualHint: 'Replace 127.0.0.1 with your server\'s public or LAN IP.',
|
|
270
|
+
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.',
|
|
271
|
+
noAuthWarning: '⚠ No auth token — set one in Settings → General before enabling remote access.',
|
|
272
|
+
showJson: 'Show JSON',
|
|
273
|
+
hideJson: 'Hide JSON',
|
|
264
274
|
},
|
|
265
275
|
monitoring: {
|
|
266
276
|
system: 'System',
|
package/app/lib/i18n-zh.ts
CHANGED
|
@@ -286,6 +286,16 @@ 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。',
|
|
294
|
+
remoteManualHint: '将 127.0.0.1 替换为服务器的公网或局域网 IP。',
|
|
295
|
+
remoteSteps: '从其他设备连接:① 在防火墙/安全组中开放端口 {port} ② 将下方配置粘贴到 Agent 中 ③ 公网环境建议使用 SSH 隧道加密传输。',
|
|
296
|
+
noAuthWarning: '⚠ 未设置认证令牌 — 请先在 设置 → 通用 中配置,再启用远程访问。',
|
|
297
|
+
showJson: '显示 JSON',
|
|
298
|
+
hideJson: '隐藏 JSON',
|
|
289
299
|
},
|
|
290
300
|
monitoring: {
|
|
291
301
|
system: '系统',
|
package/app/next.config.ts
CHANGED
|
@@ -3,7 +3,7 @@ import path from "path";
|
|
|
3
3
|
|
|
4
4
|
const nextConfig: NextConfig = {
|
|
5
5
|
transpilePackages: ['github-slugger'],
|
|
6
|
-
serverExternalPackages: ['
|
|
6
|
+
serverExternalPackages: ['chokidar', 'openai', '@mariozechner/pi-ai', '@mariozechner/pi-agent-core'],
|
|
7
7
|
outputFileTracingRoot: path.join(__dirname),
|
|
8
8
|
turbopack: {
|
|
9
9
|
root: path.join(__dirname),
|
package/bin/lib/mcp-spawn.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { execSync, spawn } from 'node:child_process';
|
|
2
|
-
import { existsSync } from 'node:fs';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
3
|
import { resolve } from 'node:path';
|
|
4
|
-
import { ROOT } from './constants.js';
|
|
4
|
+
import { ROOT, CONFIG_PATH } from './constants.js';
|
|
5
5
|
import { bold, red, yellow } from './colors.js';
|
|
6
6
|
import { npmInstall } from './utils.js';
|
|
7
7
|
|
|
@@ -14,10 +14,21 @@ export function spawnMcp(verbose = false) {
|
|
|
14
14
|
console.log(yellow('Installing MCP dependencies (first run)...\n'));
|
|
15
15
|
npmInstall(resolve(ROOT, 'mcp'), '--no-workspaces');
|
|
16
16
|
}
|
|
17
|
+
|
|
18
|
+
// Read AUTH_TOKEN directly from config to avoid stale system env overriding
|
|
19
|
+
// the user's configured token. Config is the source of truth for auth.
|
|
20
|
+
let configAuthToken;
|
|
21
|
+
try {
|
|
22
|
+
const cfg = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8'));
|
|
23
|
+
configAuthToken = cfg.authToken;
|
|
24
|
+
} catch { /* config may not exist yet */ }
|
|
25
|
+
|
|
17
26
|
const env = {
|
|
18
27
|
...process.env,
|
|
19
28
|
MCP_PORT: mcpPort,
|
|
29
|
+
MCP_HOST: process.env.MCP_HOST || '0.0.0.0',
|
|
20
30
|
MINDOS_URL: process.env.MINDOS_URL || `http://127.0.0.1:${webPort}`,
|
|
31
|
+
...(configAuthToken ? { AUTH_TOKEN: configAuthToken } : {}),
|
|
21
32
|
...(verbose ? { MCP_VERBOSE: '1' } : {}),
|
|
22
33
|
};
|
|
23
34
|
const child = spawn('npx', ['tsx', 'src/index.ts'], {
|
package/mcp/src/index.ts
CHANGED
|
@@ -25,7 +25,7 @@ import { z } from "zod";
|
|
|
25
25
|
const BASE_URL = process.env.MINDOS_URL ?? "http://localhost:3456";
|
|
26
26
|
const AUTH_TOKEN = process.env.AUTH_TOKEN;
|
|
27
27
|
const MCP_TRANSPORT = process.env.MCP_TRANSPORT ?? "http"; // "http" | "stdio"
|
|
28
|
-
const MCP_HOST = process.env.MCP_HOST ?? "
|
|
28
|
+
const MCP_HOST = process.env.MCP_HOST ?? "0.0.0.0";
|
|
29
29
|
const MCP_PORT = parseInt(process.env.MCP_PORT ?? "8781", 10);
|
|
30
30
|
const MCP_ENDPOINT = process.env.MCP_ENDPOINT ?? "/mcp";
|
|
31
31
|
const CHARACTER_LIMIT = 25_000;
|
|
@@ -510,7 +510,8 @@ async function main() {
|
|
|
510
510
|
|
|
511
511
|
const httpServer = createServer(expressApp as Parameters<typeof createServer>[1]);
|
|
512
512
|
httpServer.listen(MCP_PORT, MCP_HOST, () => {
|
|
513
|
-
|
|
513
|
+
const displayHost = MCP_HOST === '0.0.0.0' ? '127.0.0.1' : MCP_HOST;
|
|
514
|
+
console.error(`MindOS MCP server (HTTP) listening on http://${displayHost}:${MCP_PORT}${MCP_ENDPOINT}`);
|
|
514
515
|
console.error(`API backend: ${BASE_URL}`);
|
|
515
516
|
});
|
|
516
517
|
} else {
|
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"
|