@geminilight/mindos 0.5.17 → 0.5.19
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/ask/route.ts +5 -4
- package/app/app/api/mcp/agents/route.ts +6 -0
- package/app/app/api/setup/route.ts +64 -1
- package/app/app/globals.css +1 -0
- package/app/components/AskFab.tsx +49 -3
- package/app/components/AskModal.tsx +11 -2
- package/app/components/GuideCard.tsx +361 -0
- package/app/components/HomeContent.tsx +2 -2
- package/app/components/ask/ToolCallBlock.tsx +2 -1
- package/app/components/settings/KnowledgeTab.tsx +64 -2
- package/app/components/settings/McpTab.tsx +109 -10
- package/app/components/setup/StepAI.tsx +9 -1
- package/app/components/setup/index.tsx +4 -0
- package/app/components/setup/types.ts +2 -0
- package/app/hooks/useAskModal.ts +46 -0
- package/app/lib/agent/stream-consumer.ts +4 -2
- package/app/lib/agent/tools.ts +26 -12
- package/app/lib/fs.ts +9 -1
- package/app/lib/i18n.ts +106 -0
- package/app/lib/mcp-agents.ts +29 -0
- package/app/lib/settings.ts +29 -0
- package/app/next-env.d.ts +1 -1
- package/bin/cli.js +2 -2
- package/bin/lib/build.js +2 -2
- package/bin/lib/mcp-spawn.js +2 -1
- package/bin/lib/utils.js +23 -0
- package/mcp/package.json +1 -1
- package/package.json +1 -1
- package/skills/project-wiki/SKILL.md +80 -74
- package/skills/project-wiki/references/file-reference.md +6 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useState, useSyncExternalStore } from 'react';
|
|
4
|
-
import { Copy, Check, RefreshCw, Trash2 } from 'lucide-react';
|
|
3
|
+
import { useState, useEffect, useCallback, useSyncExternalStore } from 'react';
|
|
4
|
+
import { Copy, Check, RefreshCw, Trash2, Sparkles } from 'lucide-react';
|
|
5
5
|
import type { SettingsData } from './types';
|
|
6
6
|
import { Field, Input, EnvBadge, SectionLabel } from './Primitives';
|
|
7
7
|
import { apiFetch } from '@/lib/api';
|
|
@@ -16,6 +16,38 @@ export function KnowledgeTab({ data, setData, t }: KnowledgeTabProps) {
|
|
|
16
16
|
const env = data.envOverrides ?? {};
|
|
17
17
|
const k = t.settings.knowledge;
|
|
18
18
|
|
|
19
|
+
// Guide state toggle
|
|
20
|
+
const [guideActive, setGuideActive] = useState<boolean | null>(null);
|
|
21
|
+
const [guideDismissed, setGuideDismissed] = useState(false);
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
fetch('/api/setup')
|
|
25
|
+
.then(r => r.json())
|
|
26
|
+
.then(d => {
|
|
27
|
+
const gs = d.guideState;
|
|
28
|
+
if (gs) {
|
|
29
|
+
setGuideActive(gs.active);
|
|
30
|
+
setGuideDismissed(!!gs.dismissed);
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
.catch(() => {});
|
|
34
|
+
}, []);
|
|
35
|
+
|
|
36
|
+
const handleGuideToggle = useCallback(() => {
|
|
37
|
+
const newDismissed = !guideDismissed;
|
|
38
|
+
setGuideDismissed(newDismissed);
|
|
39
|
+
// If re-enabling, also ensure active is true
|
|
40
|
+
const patch: Record<string, boolean> = { dismissed: newDismissed };
|
|
41
|
+
if (!newDismissed) patch.active = true;
|
|
42
|
+
fetch('/api/setup', {
|
|
43
|
+
method: 'PATCH',
|
|
44
|
+
headers: { 'Content-Type': 'application/json' },
|
|
45
|
+
body: JSON.stringify({ guideState: patch }),
|
|
46
|
+
})
|
|
47
|
+
.then(() => window.dispatchEvent(new Event('guide-state-updated')))
|
|
48
|
+
.catch(() => setGuideDismissed(!newDismissed)); // rollback on failure
|
|
49
|
+
}, [guideDismissed]);
|
|
50
|
+
|
|
19
51
|
const origin = useSyncExternalStore(
|
|
20
52
|
() => () => {},
|
|
21
53
|
() => `${window.location.protocol}//${window.location.hostname}`,
|
|
@@ -158,6 +190,36 @@ export function KnowledgeTab({ data, setData, t }: KnowledgeTabProps) {
|
|
|
158
190
|
)}
|
|
159
191
|
</div>
|
|
160
192
|
</Field>
|
|
193
|
+
|
|
194
|
+
{/* Getting Started Guide toggle */}
|
|
195
|
+
{guideActive !== null && (
|
|
196
|
+
<div className="border-t border-border pt-5">
|
|
197
|
+
<SectionLabel>{t.guide?.title ?? 'Getting Started'}</SectionLabel>
|
|
198
|
+
<div className="flex items-center justify-between py-2">
|
|
199
|
+
<div className="flex items-center gap-2">
|
|
200
|
+
<Sparkles size={14} style={{ color: 'var(--amber)' }} />
|
|
201
|
+
<div>
|
|
202
|
+
<div className="text-sm text-foreground">{t.guide?.showGuide ?? 'Show getting started guide'}</div>
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
<button
|
|
206
|
+
type="button"
|
|
207
|
+
role="switch"
|
|
208
|
+
aria-checked={!guideDismissed}
|
|
209
|
+
onClick={handleGuideToggle}
|
|
210
|
+
className={`relative inline-flex h-5 w-9 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring ${
|
|
211
|
+
!guideDismissed ? 'bg-amber-500' : 'bg-muted'
|
|
212
|
+
}`}
|
|
213
|
+
>
|
|
214
|
+
<span
|
|
215
|
+
className={`pointer-events-none inline-block h-4 w-4 rounded-full bg-white shadow-sm transition-transform ${
|
|
216
|
+
!guideDismissed ? 'translate-x-4' : 'translate-x-0'
|
|
217
|
+
}`}
|
|
218
|
+
/>
|
|
219
|
+
</button>
|
|
220
|
+
</div>
|
|
221
|
+
</div>
|
|
222
|
+
)}
|
|
161
223
|
</div>
|
|
162
224
|
);
|
|
163
225
|
}
|
|
@@ -29,6 +29,12 @@ interface AgentInfo {
|
|
|
29
29
|
hasProjectScope: boolean;
|
|
30
30
|
hasGlobalScope: boolean;
|
|
31
31
|
preferredTransport: 'stdio' | 'http';
|
|
32
|
+
// Snippet generation fields
|
|
33
|
+
format: 'json' | 'toml';
|
|
34
|
+
configKey: string;
|
|
35
|
+
globalNestedKey?: string;
|
|
36
|
+
globalPath: string;
|
|
37
|
+
projectPath?: string | null;
|
|
32
38
|
}
|
|
33
39
|
|
|
34
40
|
interface SkillInfo {
|
|
@@ -66,19 +72,72 @@ function CopyButton({ text, label }: { text: string; label: string }) {
|
|
|
66
72
|
);
|
|
67
73
|
}
|
|
68
74
|
|
|
75
|
+
/* ── Config Snippet Generator ─────────────────────────────────── */
|
|
76
|
+
|
|
77
|
+
function generateConfigSnippet(
|
|
78
|
+
agent: AgentInfo,
|
|
79
|
+
status: McpStatus,
|
|
80
|
+
token?: string,
|
|
81
|
+
): { snippet: string; path: string } {
|
|
82
|
+
const isRunning = status.running;
|
|
83
|
+
|
|
84
|
+
// Determine entry (stdio vs http)
|
|
85
|
+
const stdioEntry: Record<string, unknown> = { type: 'stdio', command: 'mindos', args: ['mcp'] };
|
|
86
|
+
const httpEntry: Record<string, unknown> = { url: status.endpoint };
|
|
87
|
+
if (token) httpEntry.headers = { Authorization: `Bearer ${token}` };
|
|
88
|
+
const entry = isRunning ? httpEntry : stdioEntry;
|
|
89
|
+
|
|
90
|
+
// TOML format (Codex)
|
|
91
|
+
if (agent.format === 'toml') {
|
|
92
|
+
const lines: string[] = [`[${agent.configKey}.mindos]`];
|
|
93
|
+
if (isRunning) {
|
|
94
|
+
lines.push(`type = "http"`);
|
|
95
|
+
lines.push(`url = "${status.endpoint}"`);
|
|
96
|
+
if (token) {
|
|
97
|
+
lines.push('');
|
|
98
|
+
lines.push(`[${agent.configKey}.mindos.headers]`);
|
|
99
|
+
lines.push(`Authorization = "Bearer ${token}"`);
|
|
100
|
+
}
|
|
101
|
+
} else {
|
|
102
|
+
lines.push(`command = "mindos"`);
|
|
103
|
+
lines.push(`args = ["mcp"]`);
|
|
104
|
+
lines.push('');
|
|
105
|
+
lines.push(`[${agent.configKey}.mindos.env]`);
|
|
106
|
+
lines.push(`MCP_TRANSPORT = "stdio"`);
|
|
107
|
+
}
|
|
108
|
+
return { snippet: lines.join('\n'), path: agent.globalPath };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// JSON with globalNestedKey (VS Code project-level uses flat key)
|
|
112
|
+
if (agent.globalNestedKey) {
|
|
113
|
+
// project-level: flat key structure
|
|
114
|
+
const projectSnippet = JSON.stringify({ [agent.configKey]: { mindos: entry } }, null, 2);
|
|
115
|
+
return { snippet: projectSnippet, path: agent.projectPath ?? agent.globalPath };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Standard JSON
|
|
119
|
+
const snippet = JSON.stringify({ [agent.configKey]: { mindos: entry } }, null, 2);
|
|
120
|
+
return { snippet, path: agent.globalPath };
|
|
121
|
+
}
|
|
122
|
+
|
|
69
123
|
/* ── MCP Server Status ─────────────────────────────────────────── */
|
|
70
124
|
|
|
71
|
-
function ServerStatus({ status, t }: { status: McpStatus | null; t: any }) {
|
|
125
|
+
function ServerStatus({ status, agents, t }: { status: McpStatus | null; agents: AgentInfo[]; t: any }) {
|
|
72
126
|
const m = t.settings?.mcp;
|
|
127
|
+
const [selectedAgent, setSelectedAgent] = useState<string>('');
|
|
128
|
+
|
|
129
|
+
// Auto-select first installed or first detected agent
|
|
130
|
+
useEffect(() => {
|
|
131
|
+
if (agents.length > 0 && !selectedAgent) {
|
|
132
|
+
const first = agents.find(a => a.installed) ?? agents.find(a => a.present) ?? agents[0];
|
|
133
|
+
if (first) setSelectedAgent(first.key);
|
|
134
|
+
}
|
|
135
|
+
}, [agents, selectedAgent]);
|
|
136
|
+
|
|
73
137
|
if (!status) return null;
|
|
74
138
|
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
mindos: status.running
|
|
78
|
-
? { url: status.endpoint }
|
|
79
|
-
: { type: 'stdio', command: 'mindos', args: ['mcp'] },
|
|
80
|
-
},
|
|
81
|
-
}, null, 2);
|
|
139
|
+
const currentAgent = agents.find(a => a.key === selectedAgent);
|
|
140
|
+
const snippetResult = currentAgent ? generateConfigSnippet(currentAgent, status) : null;
|
|
82
141
|
|
|
83
142
|
return (
|
|
84
143
|
<div className="space-y-3">
|
|
@@ -123,8 +182,48 @@ function ServerStatus({ status, t }: { status: McpStatus | null; t: any }) {
|
|
|
123
182
|
|
|
124
183
|
<div className="flex items-center gap-2 pl-11">
|
|
125
184
|
<CopyButton text={status.endpoint} label={m?.copyEndpoint ?? 'Copy Endpoint'} />
|
|
126
|
-
<CopyButton text={configSnippet} label={m?.copyConfig ?? 'Copy Config'} />
|
|
127
185
|
</div>
|
|
186
|
+
|
|
187
|
+
{/* Quick Setup — agent-specific config snippet */}
|
|
188
|
+
{agents.length > 0 && (
|
|
189
|
+
<div className="pl-11 pt-2 space-y-2.5">
|
|
190
|
+
<div className="flex items-center gap-2">
|
|
191
|
+
<span className="text-xs text-muted-foreground font-medium">
|
|
192
|
+
── {m?.quickSetup ?? 'Quick Setup'} ──
|
|
193
|
+
</span>
|
|
194
|
+
</div>
|
|
195
|
+
|
|
196
|
+
<div className="flex items-center gap-2">
|
|
197
|
+
<span className="text-xs text-muted-foreground shrink-0">{m?.configureFor ?? 'Configure for'}</span>
|
|
198
|
+
<select
|
|
199
|
+
value={selectedAgent}
|
|
200
|
+
onChange={e => setSelectedAgent(e.target.value)}
|
|
201
|
+
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"
|
|
202
|
+
>
|
|
203
|
+
{agents.map(a => (
|
|
204
|
+
<option key={a.key} value={a.key}>
|
|
205
|
+
{a.name}{a.installed ? ` ✓` : a.present ? ` ·` : ''}
|
|
206
|
+
</option>
|
|
207
|
+
))}
|
|
208
|
+
</select>
|
|
209
|
+
</div>
|
|
210
|
+
|
|
211
|
+
{snippetResult && (
|
|
212
|
+
<>
|
|
213
|
+
<div className="flex items-center gap-2">
|
|
214
|
+
<span className="text-xs text-muted-foreground shrink-0">{m?.configPath ?? 'Config path'}</span>
|
|
215
|
+
<span className="text-xs font-mono text-foreground">{snippetResult.path}</span>
|
|
216
|
+
</div>
|
|
217
|
+
|
|
218
|
+
<pre className="text-xs font-mono bg-muted/50 border border-border rounded-lg p-3 overflow-x-auto whitespace-pre">
|
|
219
|
+
{snippetResult.snippet}
|
|
220
|
+
</pre>
|
|
221
|
+
|
|
222
|
+
<CopyButton text={snippetResult.snippet} label={m?.copyConfig ?? 'Copy Config'} />
|
|
223
|
+
</>
|
|
224
|
+
)}
|
|
225
|
+
</div>
|
|
226
|
+
)}
|
|
128
227
|
</div>
|
|
129
228
|
);
|
|
130
229
|
}
|
|
@@ -633,7 +732,7 @@ export function McpTab({ t }: McpTabProps) {
|
|
|
633
732
|
<div className="space-y-6">
|
|
634
733
|
{/* MCP Server Status — prominent card */}
|
|
635
734
|
<div className="rounded-xl border p-4" style={{ borderColor: 'var(--border)', background: 'var(--card)' }}>
|
|
636
|
-
<ServerStatus status={mcpStatus} t={t} />
|
|
735
|
+
<ServerStatus status={mcpStatus} agents={agents} t={t} />
|
|
637
736
|
</div>
|
|
638
737
|
|
|
639
738
|
{/* Agent Install — collapsible */}
|
|
@@ -45,8 +45,16 @@ export default function StepAI({ state, update, s }: StepAIProps) {
|
|
|
45
45
|
<ApiKeyInput
|
|
46
46
|
value={state.provider === 'anthropic' ? state.anthropicKey : state.openaiKey}
|
|
47
47
|
onChange={v => update(state.provider === 'anthropic' ? 'anthropicKey' : 'openaiKey', v)}
|
|
48
|
-
placeholder={
|
|
48
|
+
placeholder={
|
|
49
|
+
(state.provider === 'anthropic' ? state.anthropicKeyMask : state.openaiKeyMask)
|
|
50
|
+
|| (state.provider === 'anthropic' ? 'sk-ant-...' : 'sk-...')
|
|
51
|
+
}
|
|
49
52
|
/>
|
|
53
|
+
{(state.provider === 'anthropic' ? state.anthropicKeyMask : state.openaiKeyMask) && !(state.provider === 'anthropic' ? state.anthropicKey : state.openaiKey) && (
|
|
54
|
+
<p className="text-xs mt-1" style={{ color: 'var(--muted-foreground)' }}>
|
|
55
|
+
{s.apiKeyExisting ?? 'Existing key configured. Leave blank to keep it.'}
|
|
56
|
+
</p>
|
|
57
|
+
)}
|
|
50
58
|
</Field>
|
|
51
59
|
<Field label={s.model}>
|
|
52
60
|
<Input
|
|
@@ -129,9 +129,11 @@ export default function SetupWizard() {
|
|
|
129
129
|
provider: 'anthropic',
|
|
130
130
|
anthropicKey: '',
|
|
131
131
|
anthropicModel: 'claude-sonnet-4-6',
|
|
132
|
+
anthropicKeyMask: '',
|
|
132
133
|
openaiKey: '',
|
|
133
134
|
openaiModel: 'gpt-5.4',
|
|
134
135
|
openaiBaseUrl: '',
|
|
136
|
+
openaiKeyMask: '',
|
|
135
137
|
webPort: 3000,
|
|
136
138
|
mcpPort: 8787,
|
|
137
139
|
authToken: '',
|
|
@@ -172,8 +174,10 @@ export default function SetupWizard() {
|
|
|
172
174
|
webPassword: data.webPassword || prev.webPassword,
|
|
173
175
|
provider: (data.provider === 'anthropic' || data.provider === 'openai') ? data.provider : prev.provider,
|
|
174
176
|
anthropicModel: data.anthropicModel || prev.anthropicModel,
|
|
177
|
+
anthropicKeyMask: data.anthropicApiKey || '',
|
|
175
178
|
openaiModel: data.openaiModel || prev.openaiModel,
|
|
176
179
|
openaiBaseUrl: data.openaiBaseUrl ?? prev.openaiBaseUrl,
|
|
180
|
+
openaiKeyMask: data.openaiApiKey || '',
|
|
177
181
|
}));
|
|
178
182
|
// Generate a new token only if none exists yet
|
|
179
183
|
if (!data.authToken) {
|
|
@@ -14,9 +14,11 @@ export interface SetupState {
|
|
|
14
14
|
provider: 'anthropic' | 'openai' | 'skip';
|
|
15
15
|
anthropicKey: string;
|
|
16
16
|
anthropicModel: string;
|
|
17
|
+
anthropicKeyMask: string; // masked existing key from server (display only)
|
|
17
18
|
openaiKey: string;
|
|
18
19
|
openaiModel: string;
|
|
19
20
|
openaiBaseUrl: string;
|
|
21
|
+
openaiKeyMask: string; // masked existing key from server (display only)
|
|
20
22
|
webPort: number;
|
|
21
23
|
mcpPort: number;
|
|
22
24
|
authToken: string;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useSyncExternalStore, useCallback } from 'react';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Lightweight pub/sub store for cross-component AskModal control.
|
|
7
|
+
* Replaces KeyboardEvent dispatch pattern with typed, testable API.
|
|
8
|
+
* No external dependencies (no zustand needed).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
interface AskModalState {
|
|
12
|
+
open: boolean;
|
|
13
|
+
initialMessage: string;
|
|
14
|
+
source: 'user' | 'guide' | 'guide-next'; // who triggered the open
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let state: AskModalState = { open: false, initialMessage: '', source: 'user' };
|
|
18
|
+
const listeners = new Set<() => void>();
|
|
19
|
+
|
|
20
|
+
function emit() { listeners.forEach(l => l()); }
|
|
21
|
+
function subscribe(listener: () => void) {
|
|
22
|
+
listeners.add(listener);
|
|
23
|
+
return () => { listeners.delete(listener); };
|
|
24
|
+
}
|
|
25
|
+
function getSnapshot() { return state; }
|
|
26
|
+
|
|
27
|
+
export function openAskModal(message = '', source: AskModalState['source'] = 'user') {
|
|
28
|
+
state = { open: true, initialMessage: message, source };
|
|
29
|
+
emit();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function closeAskModal() {
|
|
33
|
+
state = { open: false, initialMessage: '', source: 'user' };
|
|
34
|
+
emit();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function useAskModal() {
|
|
38
|
+
const snap = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
39
|
+
return {
|
|
40
|
+
open: snap.open,
|
|
41
|
+
initialMessage: snap.initialMessage,
|
|
42
|
+
source: snap.source,
|
|
43
|
+
openWith: useCallback((message: string, source: AskModalState['source'] = 'user') => openAskModal(message, source), []),
|
|
44
|
+
close: useCallback(() => closeAskModal(), []),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -138,7 +138,9 @@ export async function consumeUIMessageStream(
|
|
|
138
138
|
case 'tool-output-available': {
|
|
139
139
|
const tc = toolCalls.get(chunk.toolCallId as string);
|
|
140
140
|
if (tc) {
|
|
141
|
-
tc.output =
|
|
141
|
+
tc.output = chunk.output != null
|
|
142
|
+
? (typeof chunk.output === 'string' ? chunk.output : JSON.stringify(chunk.output))
|
|
143
|
+
: '';
|
|
142
144
|
tc.state = 'done';
|
|
143
145
|
changed = true;
|
|
144
146
|
}
|
|
@@ -148,7 +150,7 @@ export async function consumeUIMessageStream(
|
|
|
148
150
|
case 'tool-input-error': {
|
|
149
151
|
const tc = toolCalls.get(chunk.toolCallId as string);
|
|
150
152
|
if (tc) {
|
|
151
|
-
tc.output = (chunk.errorText as string) ?? '
|
|
153
|
+
tc.output = (chunk.errorText as string) ?? (chunk.error as string) ?? 'Tool error';
|
|
152
154
|
tc.state = 'error';
|
|
153
155
|
changed = true;
|
|
154
156
|
}
|
package/app/lib/agent/tools.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
searchFiles, getFileContent, getFileTree, getRecentlyModified,
|
|
5
5
|
saveFileContent, createFile, appendToFile, insertAfterHeading, updateSection,
|
|
6
6
|
deleteFile, renameFile, moveFile, findBacklinks, gitLog, gitShowFile, appendCsvRow,
|
|
7
|
+
getMindRoot,
|
|
7
8
|
} from '@/lib/fs';
|
|
8
9
|
import { assertNotProtected } from '@/lib/core';
|
|
9
10
|
import { logAgentOp } from './log';
|
|
@@ -21,7 +22,13 @@ export function assertWritable(filePath: string): void {
|
|
|
21
22
|
assertNotProtected(filePath, 'modified by AI agent');
|
|
22
23
|
}
|
|
23
24
|
|
|
24
|
-
/**
|
|
25
|
+
/**
|
|
26
|
+
* Wrap a tool execute fn with agent-op logging.
|
|
27
|
+
* Catches ALL exceptions and returns an error string — never throws.
|
|
28
|
+
* This is critical: an unhandled throw from a tool execute function kills
|
|
29
|
+
* the AI SDK stream and corrupts the session message state, causing
|
|
30
|
+
* "Cannot read properties of undefined" on every subsequent request.
|
|
31
|
+
*/
|
|
25
32
|
function logged<P extends Record<string, unknown>>(
|
|
26
33
|
toolName: string,
|
|
27
34
|
fn: (params: P) => Promise<string>,
|
|
@@ -31,12 +38,12 @@ function logged<P extends Record<string, unknown>>(
|
|
|
31
38
|
try {
|
|
32
39
|
const result = await fn(params);
|
|
33
40
|
const isError = result.startsWith('Error:');
|
|
34
|
-
logAgentOp({ ts, tool: toolName, params, result: isError ? 'error' : 'ok', message: result.slice(0, 200) });
|
|
41
|
+
try { logAgentOp({ ts, tool: toolName, params, result: isError ? 'error' : 'ok', message: result.slice(0, 200) }); } catch { /* logging must never kill the stream */ }
|
|
35
42
|
return result;
|
|
36
43
|
} catch (e) {
|
|
37
44
|
const msg = e instanceof Error ? e.message : String(e);
|
|
38
|
-
logAgentOp({ ts, tool: toolName, params, result: 'error', message: msg.slice(0, 200) });
|
|
39
|
-
|
|
45
|
+
try { logAgentOp({ ts, tool: toolName, params, result: 'error', message: msg.slice(0, 200) }); } catch { /* swallow — logging must never kill the stream */ }
|
|
46
|
+
return `Error: ${msg}`;
|
|
40
47
|
}
|
|
41
48
|
};
|
|
42
49
|
}
|
|
@@ -47,12 +54,19 @@ export const knowledgeBaseTools = {
|
|
|
47
54
|
list_files: tool({
|
|
48
55
|
description: 'List files in the knowledge base as an indented tree. Directories beyond `depth` show "... (N items)". Pass `path` to list only a subdirectory, or `depth` to control how deep to expand (default 3).',
|
|
49
56
|
inputSchema: z.object({
|
|
50
|
-
path: z.string().
|
|
51
|
-
depth: z.number().min(1).max(10).
|
|
57
|
+
path: z.string().nullish().describe('Optional subdirectory to list (e.g. "Projects/Products"). Omit to list everything.'),
|
|
58
|
+
depth: z.number().min(1).max(10).nullish().describe('Max tree depth to expand (default 3). Directories deeper than this show item count only.'),
|
|
52
59
|
}),
|
|
53
60
|
execute: logged('list_files', async ({ path: subdir, depth: maxDepth }) => {
|
|
54
61
|
try {
|
|
55
62
|
const tree = getFileTree();
|
|
63
|
+
|
|
64
|
+
// Empty tree at root level → likely a misconfigured mindRoot
|
|
65
|
+
if (tree.length === 0 && !subdir) {
|
|
66
|
+
const root = getMindRoot();
|
|
67
|
+
return `(empty — no .md or .csv files found under mind_root: ${root})`;
|
|
68
|
+
}
|
|
69
|
+
|
|
56
70
|
const limit = maxDepth ?? 3;
|
|
57
71
|
const lines: string[] = [];
|
|
58
72
|
function walk(nodes: Array<{ name: string; type: string; children?: unknown[] }>, depth: number) {
|
|
@@ -114,9 +128,9 @@ export const knowledgeBaseTools = {
|
|
|
114
128
|
|
|
115
129
|
get_recent: tool({
|
|
116
130
|
description: 'Get the most recently modified files in the knowledge base.',
|
|
117
|
-
inputSchema: z.object({ limit: z.number().min(1).max(50).
|
|
131
|
+
inputSchema: z.object({ limit: z.number().min(1).max(50).nullish().describe('Number of files to return (default 10)') }),
|
|
118
132
|
execute: logged('get_recent', async ({ limit }) => {
|
|
119
|
-
const files = getRecentlyModified(limit);
|
|
133
|
+
const files = getRecentlyModified(limit ?? 10);
|
|
120
134
|
return files.map(f => `- ${f.path} (${new Date(f.mtime).toISOString()})`).join('\n');
|
|
121
135
|
}),
|
|
122
136
|
}),
|
|
@@ -142,12 +156,12 @@ export const knowledgeBaseTools = {
|
|
|
142
156
|
description: 'Create a new file. Only .md and .csv files are allowed. Parent directories are created automatically.',
|
|
143
157
|
inputSchema: z.object({
|
|
144
158
|
path: z.string().describe('Relative file path (must end in .md or .csv)'),
|
|
145
|
-
content: z.string().
|
|
159
|
+
content: z.string().nullish().describe('Initial file content'),
|
|
146
160
|
}),
|
|
147
161
|
execute: logged('create_file', async ({ path, content }) => {
|
|
148
162
|
try {
|
|
149
163
|
assertWritable(path);
|
|
150
|
-
createFile(path, content);
|
|
164
|
+
createFile(path, content ?? '');
|
|
151
165
|
return `File created: ${path}`;
|
|
152
166
|
} catch (e: unknown) {
|
|
153
167
|
return `Error: ${e instanceof Error ? e.message : String(e)}`;
|
|
@@ -283,11 +297,11 @@ export const knowledgeBaseTools = {
|
|
|
283
297
|
description: 'Get git commit history for a file. Shows recent commits that modified this file.',
|
|
284
298
|
inputSchema: z.object({
|
|
285
299
|
path: z.string().describe('Relative file path'),
|
|
286
|
-
limit: z.number().min(1).max(50).
|
|
300
|
+
limit: z.number().min(1).max(50).nullish().describe('Number of commits to return (default 10)'),
|
|
287
301
|
}),
|
|
288
302
|
execute: logged('get_history', async ({ path, limit }) => {
|
|
289
303
|
try {
|
|
290
|
-
const commits = gitLog(path, limit);
|
|
304
|
+
const commits = gitLog(path, limit ?? 10);
|
|
291
305
|
if (commits.length === 0) return `No git history found for: ${path}`;
|
|
292
306
|
return commits.map(c => `- \`${c.hash.slice(0, 7)}\` ${c.date} — ${c.message} (${c.author})`).join('\n');
|
|
293
307
|
} catch (e: unknown) {
|
package/app/lib/fs.ts
CHANGED
|
@@ -59,7 +59,15 @@ function ensureCache(): FileTreeCache {
|
|
|
59
59
|
if (isCacheValid()) return _cache!;
|
|
60
60
|
const root = getMindRoot();
|
|
61
61
|
const tree = buildFileTree(root);
|
|
62
|
-
|
|
62
|
+
// Extract all file paths from the tree to avoid a second full traversal.
|
|
63
|
+
const allFiles: string[] = [];
|
|
64
|
+
function collect(nodes: FileNode[]) {
|
|
65
|
+
for (const n of nodes) {
|
|
66
|
+
if (n.type === 'file') allFiles.push(n.path);
|
|
67
|
+
else if (n.children) collect(n.children);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
collect(tree);
|
|
63
71
|
_cache = { tree, allFiles, timestamp: Date.now() };
|
|
64
72
|
return _cache;
|
|
65
73
|
}
|
package/app/lib/i18n.ts
CHANGED
|
@@ -253,6 +253,9 @@ export const messages = {
|
|
|
253
253
|
skillLangZh: '中文',
|
|
254
254
|
selectDetected: 'Select Detected',
|
|
255
255
|
clearSelection: 'Clear',
|
|
256
|
+
quickSetup: 'Quick Setup',
|
|
257
|
+
configureFor: 'Configure for',
|
|
258
|
+
configPath: 'Config path',
|
|
256
259
|
},
|
|
257
260
|
save: 'Save',
|
|
258
261
|
saved: 'Saved',
|
|
@@ -320,6 +323,7 @@ export const messages = {
|
|
|
320
323
|
aiProviderHint: 'Choose your preferred AI service.',
|
|
321
324
|
aiSkip: 'Skip — configure later',
|
|
322
325
|
apiKey: 'API Key',
|
|
326
|
+
apiKeyExisting: 'Existing key configured. Leave blank to keep it.',
|
|
323
327
|
model: 'Model',
|
|
324
328
|
baseUrl: 'Base URL',
|
|
325
329
|
baseUrlHint: 'Optional. For proxies or OpenAI-compatible APIs.',
|
|
@@ -418,6 +422,55 @@ export const messages = {
|
|
|
418
422
|
welcomeLinkAskAI: 'Ask AI',
|
|
419
423
|
welcomeLinkMCP: 'MCP Settings',
|
|
420
424
|
},
|
|
425
|
+
guide: {
|
|
426
|
+
title: 'Get Started with MindOS',
|
|
427
|
+
showGuide: 'Show getting started guide',
|
|
428
|
+
close: 'Close',
|
|
429
|
+
skip: 'Skip',
|
|
430
|
+
kb: {
|
|
431
|
+
title: 'Explore your knowledge base',
|
|
432
|
+
cta: 'Start',
|
|
433
|
+
fullDesc: 'Your knowledge base has 6 areas — try clicking one:',
|
|
434
|
+
dirs: {
|
|
435
|
+
profile: 'Who you are, preferences, goals',
|
|
436
|
+
notes: 'Daily capture: ideas, meetings, todos',
|
|
437
|
+
connections: 'Your network of people',
|
|
438
|
+
workflows: 'Reusable process SOPs',
|
|
439
|
+
resources: 'Structured data: product lists, tool lists',
|
|
440
|
+
projects: 'Project plans and progress',
|
|
441
|
+
},
|
|
442
|
+
instructionHint: 'Click INSTRUCTION.md to see how AI agents behave.',
|
|
443
|
+
emptyDesc: 'Your knowledge base has 3 core files:',
|
|
444
|
+
emptyFiles: {
|
|
445
|
+
instruction: 'INSTRUCTION.md — Rules that all AI agents follow',
|
|
446
|
+
readme: 'README.md — Directory index and navigation',
|
|
447
|
+
config: 'CONFIG.json — Machine-readable preferences',
|
|
448
|
+
},
|
|
449
|
+
emptyHint: 'Create your own folder structure anytime.',
|
|
450
|
+
progress: (n: number) => `Browsed ${n}/1 file`,
|
|
451
|
+
done: 'Done',
|
|
452
|
+
},
|
|
453
|
+
ai: {
|
|
454
|
+
title: 'Chat with AI',
|
|
455
|
+
cta: 'Start',
|
|
456
|
+
prompt: 'Read my knowledge base and help me write a self-introduction into Profile.',
|
|
457
|
+
promptEmpty: 'Help me design a knowledge base folder structure that fits my needs',
|
|
458
|
+
},
|
|
459
|
+
sync: {
|
|
460
|
+
title: 'Set up sync',
|
|
461
|
+
optional: 'Optional',
|
|
462
|
+
cta: 'Configure',
|
|
463
|
+
},
|
|
464
|
+
done: {
|
|
465
|
+
title: "You're all set!",
|
|
466
|
+
titleFinal: "You've mastered MindOS essentials!",
|
|
467
|
+
steps: [
|
|
468
|
+
{ hint: 'Next: try saving an article →', prompt: 'Help me save the key points from this article into MindOS.' },
|
|
469
|
+
{ hint: 'Next: try using your KB in another Agent →', prompt: 'Help me start coding based on the plan in MindOS.' },
|
|
470
|
+
{ hint: 'Next: try turning experience into a reusable SOP →', prompt: 'Help me distill this conversation into a reusable workflow in MindOS.' },
|
|
471
|
+
],
|
|
472
|
+
},
|
|
473
|
+
},
|
|
421
474
|
},
|
|
422
475
|
zh: {
|
|
423
476
|
common: {
|
|
@@ -671,6 +724,9 @@ export const messages = {
|
|
|
671
724
|
skillLangZh: '中文',
|
|
672
725
|
selectDetected: '选择已检测',
|
|
673
726
|
clearSelection: '清除',
|
|
727
|
+
quickSetup: '快速配置',
|
|
728
|
+
configureFor: '配置目标',
|
|
729
|
+
configPath: '配置路径',
|
|
674
730
|
},
|
|
675
731
|
save: '保存',
|
|
676
732
|
saved: '已保存',
|
|
@@ -738,6 +794,7 @@ export const messages = {
|
|
|
738
794
|
aiProviderHint: '选择你偏好的 AI 服务。',
|
|
739
795
|
aiSkip: '跳过 — 稍后配置',
|
|
740
796
|
apiKey: 'API 密钥',
|
|
797
|
+
apiKeyExisting: '已配置密钥。留空即保持不变。',
|
|
741
798
|
model: '模型',
|
|
742
799
|
baseUrl: '接口地址',
|
|
743
800
|
baseUrlHint: '可选。用于代理或 OpenAI 兼容 API。',
|
|
@@ -837,6 +894,55 @@ export const messages = {
|
|
|
837
894
|
welcomeLinkAskAI: '问 AI',
|
|
838
895
|
welcomeLinkMCP: 'MCP 设置',
|
|
839
896
|
},
|
|
897
|
+
guide: {
|
|
898
|
+
title: '开始使用 MindOS',
|
|
899
|
+
showGuide: '显示新手引导',
|
|
900
|
+
close: '关闭',
|
|
901
|
+
skip: '跳过',
|
|
902
|
+
kb: {
|
|
903
|
+
title: '探索你的知识库',
|
|
904
|
+
cta: '开始',
|
|
905
|
+
fullDesc: '你的知识库有 6 个区域,试试点开看看:',
|
|
906
|
+
dirs: {
|
|
907
|
+
profile: '你是谁、偏好、目标',
|
|
908
|
+
notes: '日常捕捉:想法、会议、待办',
|
|
909
|
+
connections: '你的人脉关系网',
|
|
910
|
+
workflows: '可复用的工作流程 SOP',
|
|
911
|
+
resources: '结构化数据:产品库、工具库',
|
|
912
|
+
projects: '项目计划和进展',
|
|
913
|
+
},
|
|
914
|
+
instructionHint: '点击 INSTRUCTION.md 看看 AI 的行为规则。',
|
|
915
|
+
emptyDesc: '你的知识库有 3 个核心文件:',
|
|
916
|
+
emptyFiles: {
|
|
917
|
+
instruction: 'INSTRUCTION.md — 所有 AI Agent 遵循的规则',
|
|
918
|
+
readme: 'README.md — 目录索引和导航',
|
|
919
|
+
config: 'CONFIG.json — 机器可读的配置偏好',
|
|
920
|
+
},
|
|
921
|
+
emptyHint: '你可以随时创建自己的目录结构。',
|
|
922
|
+
progress: (n: number) => `已浏览 ${n}/1 个文件`,
|
|
923
|
+
done: '完成',
|
|
924
|
+
},
|
|
925
|
+
ai: {
|
|
926
|
+
title: '和 AI 对话',
|
|
927
|
+
cta: '开始',
|
|
928
|
+
prompt: '读一下我的知识库,帮我把自我介绍写进 Profile。',
|
|
929
|
+
promptEmpty: '帮我设计一个适合我的知识库目录结构',
|
|
930
|
+
},
|
|
931
|
+
sync: {
|
|
932
|
+
title: '配置同步',
|
|
933
|
+
optional: '可选',
|
|
934
|
+
cta: '设置',
|
|
935
|
+
},
|
|
936
|
+
done: {
|
|
937
|
+
title: '你已准备好使用 MindOS',
|
|
938
|
+
titleFinal: '你已掌握 MindOS 核心用法',
|
|
939
|
+
steps: [
|
|
940
|
+
{ hint: '下一步:试试把一篇文章存进来 →', prompt: '帮我把这篇文章的要点整理到 MindOS 里。' },
|
|
941
|
+
{ hint: '下一步:试试在另一个 Agent 里调用知识库 →', prompt: '帮我按 MindOS 里的方案开始写代码。' },
|
|
942
|
+
{ hint: '下一步:试试把经验沉淀为 SOP →', prompt: '帮我把这次对话的经验沉淀到 MindOS,形成可复用的工作流。' },
|
|
943
|
+
],
|
|
944
|
+
},
|
|
945
|
+
},
|
|
840
946
|
},
|
|
841
947
|
} as const;
|
|
842
948
|
|
package/app/lib/mcp-agents.ts
CHANGED
|
@@ -13,6 +13,10 @@ export interface AgentDef {
|
|
|
13
13
|
global: string;
|
|
14
14
|
key: string;
|
|
15
15
|
preferredTransport: 'stdio' | 'http';
|
|
16
|
+
/** Config file format: 'json' (default) or 'toml'. */
|
|
17
|
+
format?: 'json' | 'toml';
|
|
18
|
+
/** For agents whose global config nests under a parent key (e.g. VS Code: mcp.servers). */
|
|
19
|
+
globalNestedKey?: string;
|
|
16
20
|
/** CLI binary name for presence detection (e.g. 'claude'). Optional. */
|
|
17
21
|
presenceCli?: string;
|
|
18
22
|
/** Data directories for presence detection. Any one existing → present. */
|
|
@@ -174,6 +178,31 @@ export const MCP_AGENTS: Record<string, AgentDef> = {
|
|
|
174
178
|
'~/.config/Code/User/globalStorage/rooveterinaryinc.roo-cline/',
|
|
175
179
|
],
|
|
176
180
|
},
|
|
181
|
+
'vscode': {
|
|
182
|
+
name: 'VS Code',
|
|
183
|
+
project: '.vscode/mcp.json',
|
|
184
|
+
global: process.platform === 'darwin'
|
|
185
|
+
? '~/Library/Application Support/Code/User/settings.json'
|
|
186
|
+
: '~/.config/Code/User/settings.json',
|
|
187
|
+
key: 'servers',
|
|
188
|
+
globalNestedKey: 'mcp.servers',
|
|
189
|
+
preferredTransport: 'stdio',
|
|
190
|
+
presenceDirs: [
|
|
191
|
+
'~/Library/Application Support/Code/',
|
|
192
|
+
'~/.config/Code/',
|
|
193
|
+
],
|
|
194
|
+
presenceCli: 'code',
|
|
195
|
+
},
|
|
196
|
+
'codex': {
|
|
197
|
+
name: 'Codex',
|
|
198
|
+
project: null,
|
|
199
|
+
global: '~/.codex/config.toml',
|
|
200
|
+
key: 'mcp_servers',
|
|
201
|
+
format: 'toml',
|
|
202
|
+
preferredTransport: 'stdio',
|
|
203
|
+
presenceCli: 'codex',
|
|
204
|
+
presenceDirs: ['~/.codex/'],
|
|
205
|
+
},
|
|
177
206
|
};
|
|
178
207
|
|
|
179
208
|
/* ── MindOS MCP Install Detection ──────────────────────────────────────── */
|