@geminilight/mindos 0.5.19 → 0.5.21

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.
Files changed (39) hide show
  1. package/app/app/api/ask/route.ts +308 -172
  2. package/app/app/api/file/route.ts +35 -11
  3. package/app/app/api/skills/route.ts +22 -3
  4. package/app/components/SettingsModal.tsx +52 -58
  5. package/app/components/Sidebar.tsx +21 -1
  6. package/app/components/settings/AiTab.tsx +4 -25
  7. package/app/components/settings/AppearanceTab.tsx +31 -13
  8. package/app/components/settings/KnowledgeTab.tsx +13 -28
  9. package/app/components/settings/McpAgentInstall.tsx +227 -0
  10. package/app/components/settings/McpServerStatus.tsx +172 -0
  11. package/app/components/settings/McpSkillsSection.tsx +583 -0
  12. package/app/components/settings/McpTab.tsx +16 -728
  13. package/app/components/settings/PluginsTab.tsx +4 -27
  14. package/app/components/settings/Primitives.tsx +69 -0
  15. package/app/components/settings/ShortcutsTab.tsx +2 -4
  16. package/app/components/settings/SyncTab.tsx +8 -24
  17. package/app/components/settings/types.ts +116 -2
  18. package/app/lib/agent/context.ts +151 -87
  19. package/app/lib/agent/index.ts +4 -3
  20. package/app/lib/agent/model.ts +76 -10
  21. package/app/lib/agent/stream-consumer.ts +73 -77
  22. package/app/lib/agent/to-agent-messages.ts +106 -0
  23. package/app/lib/agent/tools.ts +260 -266
  24. package/app/lib/i18n-en.ts +480 -0
  25. package/app/lib/i18n-zh.ts +505 -0
  26. package/app/lib/i18n.ts +4 -947
  27. package/app/next-env.d.ts +1 -1
  28. package/app/next.config.ts +7 -0
  29. package/app/package-lock.json +3258 -3093
  30. package/app/package.json +6 -3
  31. package/bin/cli.js +140 -5
  32. package/package.json +4 -1
  33. package/scripts/setup.js +13 -0
  34. package/skills/mindos/SKILL.md +10 -168
  35. package/skills/mindos-zh/SKILL.md +14 -172
  36. package/templates/skill-rules/en/skill-rules.md +222 -0
  37. package/templates/skill-rules/en/user-rules.md +20 -0
  38. package/templates/skill-rules/zh/skill-rules.md +222 -0
  39. package/templates/skill-rules/zh/user-rules.md +20 -0
@@ -0,0 +1,227 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import { CheckCircle2, AlertCircle, Loader2 } from 'lucide-react';
5
+ import { apiFetch } from '@/lib/api';
6
+ import type { AgentInfo, McpAgentInstallProps } from './types';
7
+
8
+ /* ── Agent Install ─────────────────────────────────────────────── */
9
+
10
+ export default function AgentInstall({ agents, t, onRefresh }: McpAgentInstallProps) {
11
+ const m = t.settings?.mcp;
12
+ const [selected, setSelected] = useState<Set<string>>(new Set());
13
+ const [transport, setTransport] = useState<'auto' | 'stdio' | 'http'>('auto');
14
+ const [httpUrl, setHttpUrl] = useState('http://localhost:8787/mcp');
15
+ const [httpToken, setHttpToken] = useState('');
16
+ const [scopes, setScopes] = useState<Record<string, 'project' | 'global'>>({});
17
+ const [installing, setInstalling] = useState(false);
18
+ const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
19
+
20
+ const getEffectiveTransport = (agent: AgentInfo) => {
21
+ if (transport === 'auto') return agent.preferredTransport;
22
+ return transport;
23
+ };
24
+
25
+ const toggle = (key: string) => {
26
+ setSelected(prev => {
27
+ const next = new Set(prev);
28
+ if (next.has(key)) next.delete(key); else next.add(key);
29
+ return next;
30
+ });
31
+ };
32
+
33
+ const handleInstall = async () => {
34
+ if (selected.size === 0) return;
35
+ setInstalling(true);
36
+ setMessage(null);
37
+ try {
38
+ const payload = {
39
+ agents: [...selected].map(key => {
40
+ const agent = agents.find(a => a.key === key);
41
+ const effectiveTransport = transport === 'auto'
42
+ ? (agent?.preferredTransport || 'stdio')
43
+ : transport;
44
+ return {
45
+ key,
46
+ scope: scopes[key] || (agent?.hasProjectScope ? 'project' : 'global'),
47
+ transport: effectiveTransport,
48
+ };
49
+ }),
50
+ transport,
51
+ ...(transport === 'http' ? { url: httpUrl, token: httpToken } : {}),
52
+ // For auto mode, pass http settings for agents that need it
53
+ ...(transport === 'auto' ? { url: httpUrl, token: httpToken } : {}),
54
+ };
55
+ const res = await apiFetch<{ results: Array<{ agent: string; status: string; message?: string }> }>('/api/mcp/install', {
56
+ method: 'POST',
57
+ headers: { 'Content-Type': 'application/json' },
58
+ body: JSON.stringify(payload),
59
+ });
60
+ const ok = res.results.filter(r => r.status === 'ok').length;
61
+ const fail = res.results.filter(r => r.status === 'error');
62
+ if (fail.length > 0) {
63
+ setMessage({ type: 'error', text: fail.map(f => `${f.agent}: ${f.message}`).join('; ') });
64
+ } else {
65
+ setMessage({ type: 'success', text: m?.installSuccess ? m.installSuccess(ok) : `${ok} agent(s) configured` });
66
+ }
67
+ setSelected(new Set());
68
+ onRefresh();
69
+ } catch {
70
+ setMessage({ type: 'error', text: m?.installFailed ?? 'Install failed' });
71
+ } finally {
72
+ setInstalling(false);
73
+ setTimeout(() => setMessage(null), 4000);
74
+ }
75
+ };
76
+
77
+ // Show http fields if transport is 'http', or 'auto' with any http-preferred agent selected
78
+ const showHttpFields = transport === 'http' || (transport === 'auto' && [...selected].some(key => {
79
+ const agent = agents.find(a => a.key === key);
80
+ return agent?.preferredTransport === 'http';
81
+ }));
82
+
83
+ return (
84
+ <div className="space-y-3 pt-2">
85
+ {/* Agent list */}
86
+ <div className="space-y-1">
87
+ {agents.map(agent => (
88
+ <div key={agent.key} className="flex items-center gap-3 py-1.5 text-sm">
89
+ <input
90
+ type="checkbox"
91
+ checked={selected.has(agent.key)}
92
+ onChange={() => toggle(agent.key)}
93
+ className="rounded border-border"
94
+ style={{ accentColor: 'var(--amber)' }}
95
+ />
96
+ <span className="w-28 shrink-0 text-xs">{agent.name}</span>
97
+ <span className="text-2xs px-1.5 py-0.5 rounded font-mono"
98
+ style={{ background: 'rgba(100,100,120,0.08)' }}>
99
+ {getEffectiveTransport(agent)}
100
+ </span>
101
+ {agent.installed ? (
102
+ <>
103
+ <span className="text-2xs px-1.5 py-0.5 rounded bg-success/15 text-success font-mono">
104
+ {agent.transport}
105
+ </span>
106
+ <span className="text-2xs text-muted-foreground">{agent.scope}</span>
107
+ </>
108
+ ) : (
109
+ <span className="text-2xs text-muted-foreground">
110
+ {agent.present ? (m?.detected ?? 'Detected') : (m?.notFound ?? 'Not found')}
111
+ </span>
112
+ )}
113
+ {/* Scope selector */}
114
+ {selected.has(agent.key) && agent.hasProjectScope && agent.hasGlobalScope && (
115
+ <select
116
+ value={scopes[agent.key] || 'project'}
117
+ onChange={e => setScopes({ ...scopes, [agent.key]: e.target.value as 'project' | 'global' })}
118
+ className="ml-auto text-2xs px-1.5 py-0.5 rounded border border-border bg-background text-foreground"
119
+ >
120
+ <option value="project">{m?.project ?? 'Project'}</option>
121
+ <option value="global">{m?.global ?? 'Global'}</option>
122
+ </select>
123
+ )}
124
+ </div>
125
+ ))}
126
+ </div>
127
+
128
+ {/* Select detected / Clear buttons */}
129
+ <div className="flex gap-2 text-xs pt-1">
130
+ <button type="button"
131
+ onClick={() => setSelected(new Set(
132
+ agents.filter(a => !a.installed && a.present).map(a => a.key)
133
+ ))}
134
+ className="px-2.5 py-1 rounded-md border transition-colors hover:bg-muted/50"
135
+ style={{ borderColor: 'var(--amber)', color: 'var(--amber)' }}>
136
+ {m?.selectDetected ?? 'Select Detected'}
137
+ </button>
138
+ <button type="button"
139
+ onClick={() => setSelected(new Set())}
140
+ className="px-2.5 py-1 rounded-md border border-border text-muted-foreground transition-colors hover:bg-muted/50 hover:text-foreground">
141
+ {m?.clearSelection ?? 'Clear'}
142
+ </button>
143
+ </div>
144
+
145
+ {/* Transport selector */}
146
+ <div className="flex items-center gap-4 text-xs pt-1">
147
+ <label className="flex items-center gap-1.5 cursor-pointer">
148
+ <input
149
+ type="radio"
150
+ name="transport"
151
+ checked={transport === 'auto'}
152
+ onChange={() => setTransport('auto')}
153
+ style={{ accentColor: 'var(--amber)' }}
154
+ />
155
+ {m?.transportAuto ?? 'auto (recommended)'}
156
+ </label>
157
+ <label className="flex items-center gap-1.5 cursor-pointer">
158
+ <input
159
+ type="radio"
160
+ name="transport"
161
+ checked={transport === 'stdio'}
162
+ onChange={() => setTransport('stdio')}
163
+ style={{ accentColor: 'var(--amber)' }}
164
+ />
165
+ {m?.transportStdio ?? 'stdio'}
166
+ </label>
167
+ <label className="flex items-center gap-1.5 cursor-pointer">
168
+ <input
169
+ type="radio"
170
+ name="transport"
171
+ checked={transport === 'http'}
172
+ onChange={() => setTransport('http')}
173
+ style={{ accentColor: 'var(--amber)' }}
174
+ />
175
+ {m?.transportHttp ?? 'http'}
176
+ </label>
177
+ </div>
178
+
179
+ {/* HTTP settings */}
180
+ {showHttpFields && (
181
+ <div className="space-y-2 pl-5 text-xs">
182
+ <div className="space-y-1">
183
+ <label className="text-muted-foreground">{m?.httpUrl ?? 'MCP URL'}</label>
184
+ <input
185
+ type="text"
186
+ value={httpUrl}
187
+ onChange={e => setHttpUrl(e.target.value)}
188
+ className="w-full px-2.5 py-1.5 text-xs rounded-md border border-border bg-background font-mono text-foreground outline-none focus-visible:ring-1 focus-visible:ring-ring"
189
+ />
190
+ </div>
191
+ <div className="space-y-1">
192
+ <label className="text-muted-foreground">{m?.httpToken ?? 'Auth Token'}</label>
193
+ <input
194
+ type="password"
195
+ value={httpToken}
196
+ onChange={e => setHttpToken(e.target.value)}
197
+ placeholder="Bearer token"
198
+ className="w-full px-2.5 py-1.5 text-xs rounded-md border border-border bg-background font-mono text-foreground outline-none focus-visible:ring-1 focus-visible:ring-ring"
199
+ />
200
+ </div>
201
+ </div>
202
+ )}
203
+
204
+ {/* Install button */}
205
+ <button
206
+ onClick={handleInstall}
207
+ disabled={selected.size === 0 || installing}
208
+ className="flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-lg transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
209
+ style={{ background: 'var(--amber)', color: 'var(--amber-foreground)' }}
210
+ >
211
+ {installing && <Loader2 size={12} className="animate-spin" />}
212
+ {installing ? (m?.installing ?? 'Installing...') : (m?.installSelected ?? 'Install Selected')}
213
+ </button>
214
+
215
+ {/* Message */}
216
+ {message && (
217
+ <div className="flex items-center gap-1.5 text-xs" role="status">
218
+ {message.type === 'success' ? (
219
+ <><CheckCircle2 size={12} className="text-success" /><span className="text-success">{message.text}</span></>
220
+ ) : (
221
+ <><AlertCircle size={12} className="text-destructive" /><span className="text-destructive">{message.text}</span></>
222
+ )}
223
+ </div>
224
+ )}
225
+ </div>
226
+ );
227
+ }
@@ -0,0 +1,172 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useMemo } from 'react';
4
+ import { Plug, Copy, Check, ChevronDown } from 'lucide-react';
5
+ import type { McpStatus, AgentInfo, McpServerStatusProps } from './types';
6
+
7
+ /* ── Helpers ───────────────────────────────────────────────────── */
8
+
9
+ function CopyButton({ text, label, copiedLabel }: { text: string; label: string; copiedLabel?: string }) {
10
+ const [copied, setCopied] = useState(false);
11
+ const handleCopy = async () => {
12
+ try {
13
+ await navigator.clipboard.writeText(text);
14
+ setCopied(true);
15
+ setTimeout(() => setCopied(false), 2000);
16
+ } catch { /* clipboard unavailable */ }
17
+ };
18
+ return (
19
+ <button
20
+ 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"
22
+ >
23
+ {copied ? <Check size={11} /> : <Copy size={11} />}
24
+ {copied ? (copiedLabel ?? 'Copied!') : label}
25
+ </button>
26
+ );
27
+ }
28
+
29
+ /* ── Config Snippet Generator ─────────────────────────────────── */
30
+
31
+ function generateConfigSnippet(
32
+ agent: AgentInfo,
33
+ status: McpStatus,
34
+ token?: string,
35
+ ): { snippet: string; path: string } {
36
+ const isRunning = status.running;
37
+
38
+ // Determine entry (stdio vs http)
39
+ 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
+
44
+ // TOML format (Codex)
45
+ 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) {
51
+ lines.push('');
52
+ lines.push(`[${agent.configKey}.mindos.headers]`);
53
+ lines.push(`Authorization = "Bearer ${token}"`);
54
+ }
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"`);
61
+ }
62
+ return { snippet: lines.join('\n'), path: agent.globalPath };
63
+ }
64
+
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
+ }
71
+
72
+ // Standard JSON
73
+ const snippet = JSON.stringify({ [agent.configKey]: { mindos: entry } }, null, 2);
74
+ return { snippet, path: agent.globalPath };
75
+ }
76
+
77
+ /* ── MCP Server Status ─────────────────────────────────────────── */
78
+
79
+ export default function ServerStatus({ status, agents, t }: McpServerStatusProps) {
80
+ const m = t.settings?.mcp;
81
+ const [expanded, setExpanded] = useState(false);
82
+ const [selectedAgent, setSelectedAgent] = useState<string>('');
83
+
84
+ // Auto-select first installed or first detected agent
85
+ useEffect(() => {
86
+ if (agents.length > 0 && !selectedAgent) {
87
+ const first = agents.find(a => a.installed) ?? agents.find(a => a.present) ?? agents[0];
88
+ if (first) setSelectedAgent(first.key);
89
+ }
90
+ }, [agents, selectedAgent]);
91
+
92
+ if (!status) return null;
93
+
94
+ 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]);
97
+
98
+ return (
99
+ <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>
135
+
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"
145
+ >
146
+ {agents.map(a => (
147
+ <option key={a.key} value={a.key}>
148
+ {a.name}{a.installed ? ' ✓' : a.present ? ' ·' : ''}
149
+ </option>
150
+ ))}
151
+ </select>
152
+ </div>
153
+
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>
163
+ <CopyButton text={snippetResult.snippet} label={m?.copyConfig ?? 'Copy Config'} copiedLabel={m?.copied} />
164
+ </>
165
+ )}
166
+ </div>
167
+ )}
168
+ </div>
169
+ )}
170
+ </div>
171
+ );
172
+ }