@geminilight/mindos 0.5.18 → 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/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/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/settings.ts +29 -0
- package/app/next-env.d.ts +1 -1
- package/bin/cli.js +2 -8
- package/bin/lib/build.js +2 -7
- package/bin/lib/mcp-spawn.js +2 -13
- package/bin/lib/utils.js +23 -0
- 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
|
}
|
|
@@ -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/settings.ts
CHANGED
|
@@ -25,6 +25,15 @@ export interface AgentConfig {
|
|
|
25
25
|
contextStrategy?: 'auto' | 'off'; // default 'auto'
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
export interface GuideState {
|
|
29
|
+
active: boolean; // setup 完成时写入 true
|
|
30
|
+
dismissed: boolean; // 用户关闭 Guide Card 时写入 true
|
|
31
|
+
template: 'en' | 'zh' | 'empty'; // setup 时写入
|
|
32
|
+
step1Done: boolean; // 至少浏览过 1 个文件
|
|
33
|
+
askedAI: boolean; // 至少发过 1 条 AI 消息
|
|
34
|
+
nextStepIndex: number; // 0=C2, 1=C3, 2=C4, 3=全部完成
|
|
35
|
+
}
|
|
36
|
+
|
|
28
37
|
export interface ServerSettings {
|
|
29
38
|
ai: AiConfig;
|
|
30
39
|
agent?: AgentConfig;
|
|
@@ -36,6 +45,7 @@ export interface ServerSettings {
|
|
|
36
45
|
startMode?: 'dev' | 'start' | 'daemon';
|
|
37
46
|
setupPending?: boolean; // true → / redirects to /setup
|
|
38
47
|
disabledSkills?: string[];
|
|
48
|
+
guideState?: GuideState;
|
|
39
49
|
}
|
|
40
50
|
|
|
41
51
|
const DEFAULTS: ServerSettings = {
|
|
@@ -119,6 +129,23 @@ function parseAgent(raw: unknown): AgentConfig | undefined {
|
|
|
119
129
|
return Object.keys(result).length > 0 ? result : undefined;
|
|
120
130
|
}
|
|
121
131
|
|
|
132
|
+
/** Parse guideState from unknown input */
|
|
133
|
+
function parseGuideState(raw: unknown): GuideState | undefined {
|
|
134
|
+
if (!raw || typeof raw !== 'object') return undefined;
|
|
135
|
+
const obj = raw as Record<string, unknown>;
|
|
136
|
+
if (obj.active !== true) return undefined;
|
|
137
|
+
const template = obj.template === 'en' || obj.template === 'zh' || obj.template === 'empty'
|
|
138
|
+
? obj.template : 'en';
|
|
139
|
+
return {
|
|
140
|
+
active: true,
|
|
141
|
+
dismissed: obj.dismissed === true,
|
|
142
|
+
template,
|
|
143
|
+
step1Done: obj.step1Done === true,
|
|
144
|
+
askedAI: obj.askedAI === true,
|
|
145
|
+
nextStepIndex: typeof obj.nextStepIndex === 'number' ? obj.nextStepIndex : 0,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
122
149
|
export function readSettings(): ServerSettings {
|
|
123
150
|
try {
|
|
124
151
|
const raw = fs.readFileSync(SETTINGS_PATH, 'utf-8');
|
|
@@ -134,6 +161,7 @@ export function readSettings(): ServerSettings {
|
|
|
134
161
|
startMode: typeof parsed.startMode === 'string' ? parsed.startMode as ServerSettings['startMode'] : undefined,
|
|
135
162
|
setupPending: parsed.setupPending === true ? true : undefined,
|
|
136
163
|
disabledSkills: Array.isArray(parsed.disabledSkills) ? parsed.disabledSkills as string[] : undefined,
|
|
164
|
+
guideState: parseGuideState(parsed.guideState),
|
|
137
165
|
};
|
|
138
166
|
} catch {
|
|
139
167
|
return { ...DEFAULTS, ai: { ...DEFAULTS.ai, providers: { ...DEFAULTS.ai.providers } } };
|
|
@@ -154,6 +182,7 @@ export function writeSettings(settings: ServerSettings): void {
|
|
|
154
182
|
if (settings.mcpPort !== undefined) merged.mcpPort = settings.mcpPort;
|
|
155
183
|
if (settings.startMode !== undefined) merged.startMode = settings.startMode;
|
|
156
184
|
if (settings.disabledSkills !== undefined) merged.disabledSkills = settings.disabledSkills;
|
|
185
|
+
if (settings.guideState !== undefined) merged.guideState = settings.guideState;
|
|
157
186
|
// setupPending: false/undefined → remove the field (cleanup); true → set it
|
|
158
187
|
if ('setupPending' in settings) {
|
|
159
188
|
if (settings.setupPending) merged.setupPending = true;
|
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/bin/cli.js
CHANGED
|
@@ -45,7 +45,7 @@ import { homedir } from 'node:os';
|
|
|
45
45
|
|
|
46
46
|
import { ROOT, CONFIG_PATH, BUILD_STAMP, LOG_PATH, MINDOS_DIR } from './lib/constants.js';
|
|
47
47
|
import { bold, dim, cyan, green, red, yellow } from './lib/colors.js';
|
|
48
|
-
import { run } from './lib/utils.js';
|
|
48
|
+
import { run, npmInstall } from './lib/utils.js';
|
|
49
49
|
import { loadConfig, getStartMode, isDaemonMode } from './lib/config.js';
|
|
50
50
|
import { needsBuild, writeBuildStamp, clearBuildLock, cleanNextDir, ensureAppDeps } from './lib/build.js';
|
|
51
51
|
import { isPortInUse, assertPortFree } from './lib/port.js';
|
|
@@ -325,13 +325,7 @@ const commands = {
|
|
|
325
325
|
const mcpSdk = resolve(ROOT, 'mcp', 'node_modules', '@modelcontextprotocol', 'sdk', 'package.json');
|
|
326
326
|
if (!existsSync(mcpSdk)) {
|
|
327
327
|
console.log(yellow('Installing MCP dependencies (first run)...\n'));
|
|
328
|
-
|
|
329
|
-
try {
|
|
330
|
-
execSync('npm install --prefer-offline --no-workspaces', { cwd: mcpCwd, stdio: 'inherit' });
|
|
331
|
-
} catch {
|
|
332
|
-
console.log(yellow('Offline install failed, retrying online...\n'));
|
|
333
|
-
run('npm install --no-workspaces', mcpCwd);
|
|
334
|
-
}
|
|
328
|
+
npmInstall(resolve(ROOT, 'mcp'), '--no-workspaces');
|
|
335
329
|
}
|
|
336
330
|
// Map config env vars to what the MCP server expects
|
|
337
331
|
const mcpPort = process.env.MINDOS_MCP_PORT || '8781';
|
package/bin/lib/build.js
CHANGED
|
@@ -4,7 +4,7 @@ import { createHash } from 'node:crypto';
|
|
|
4
4
|
import { resolve } from 'node:path';
|
|
5
5
|
import { ROOT, BUILD_STAMP, DEPS_STAMP } from './constants.js';
|
|
6
6
|
import { red, dim, yellow } from './colors.js';
|
|
7
|
-
import { run } from './utils.js';
|
|
7
|
+
import { run, npmInstall } from './utils.js';
|
|
8
8
|
|
|
9
9
|
export function needsBuild() {
|
|
10
10
|
const nextDir = resolve(ROOT, 'app', '.next');
|
|
@@ -105,12 +105,7 @@ export function ensureAppDeps() {
|
|
|
105
105
|
? 'Updating app dependencies (package-lock.json changed)...\n'
|
|
106
106
|
: 'Installing app dependencies (first run)...\n';
|
|
107
107
|
console.log(yellow(label));
|
|
108
|
-
|
|
109
|
-
execSync('npm install --prefer-offline --no-workspaces', { cwd: resolve(ROOT, 'app'), stdio: 'inherit' });
|
|
110
|
-
} catch {
|
|
111
|
-
console.log(yellow('Offline install failed, retrying online...\n'));
|
|
112
|
-
run('npm install --no-workspaces', resolve(ROOT, 'app'));
|
|
113
|
-
}
|
|
108
|
+
npmInstall(resolve(ROOT, 'app'), '--no-workspaces');
|
|
114
109
|
|
|
115
110
|
// Verify critical deps — npm tar extraction can silently fail (ENOENT race)
|
|
116
111
|
if (!verifyDeps()) {
|
package/bin/lib/mcp-spawn.js
CHANGED
|
@@ -3,6 +3,7 @@ import { existsSync } from 'node:fs';
|
|
|
3
3
|
import { resolve } from 'node:path';
|
|
4
4
|
import { ROOT } from './constants.js';
|
|
5
5
|
import { bold, red, yellow } from './colors.js';
|
|
6
|
+
import { npmInstall } from './utils.js';
|
|
6
7
|
|
|
7
8
|
export function spawnMcp(verbose = false) {
|
|
8
9
|
const mcpPort = process.env.MINDOS_MCP_PORT || '8781';
|
|
@@ -11,19 +12,7 @@ export function spawnMcp(verbose = false) {
|
|
|
11
12
|
const mcpSdk = resolve(ROOT, 'mcp', 'node_modules', '@modelcontextprotocol', 'sdk', 'package.json');
|
|
12
13
|
if (!existsSync(mcpSdk)) {
|
|
13
14
|
console.log(yellow('Installing MCP dependencies (first run)...\n'));
|
|
14
|
-
|
|
15
|
-
try {
|
|
16
|
-
execSync('npm install --prefer-offline --no-workspaces', { cwd: mcpCwd, stdio: 'inherit' });
|
|
17
|
-
} catch {
|
|
18
|
-
console.log(yellow('Offline install failed, retrying online...\n'));
|
|
19
|
-
try {
|
|
20
|
-
execSync('npm install --no-workspaces', { cwd: mcpCwd, stdio: 'inherit' });
|
|
21
|
-
} catch (err) {
|
|
22
|
-
console.error(red('Failed to install MCP dependencies.'));
|
|
23
|
-
console.error(` Try manually: cd ${mcpCwd} && npm install\n`);
|
|
24
|
-
process.exit(1);
|
|
25
|
-
}
|
|
26
|
-
}
|
|
15
|
+
npmInstall(resolve(ROOT, 'mcp'), '--no-workspaces');
|
|
27
16
|
}
|
|
28
17
|
const env = {
|
|
29
18
|
...process.env,
|
package/bin/lib/utils.js
CHANGED
|
@@ -11,6 +11,29 @@ export function run(command, cwd = ROOT) {
|
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Run `npm install` with --prefer-offline for speed, auto-fallback to online
|
|
16
|
+
* if the local cache is stale or missing a required package version.
|
|
17
|
+
*
|
|
18
|
+
* @param {string} cwd - Directory to run in
|
|
19
|
+
* @param {string} [extraFlags=''] - Additional npm flags (e.g. '--no-workspaces')
|
|
20
|
+
*/
|
|
21
|
+
export function npmInstall(cwd, extraFlags = '') {
|
|
22
|
+
const base = `npm install ${extraFlags}`.trim();
|
|
23
|
+
try {
|
|
24
|
+
execSync(`${base} --prefer-offline`, { cwd, stdio: 'inherit', env: process.env });
|
|
25
|
+
} catch {
|
|
26
|
+
// Cache miss or stale packument — retry online
|
|
27
|
+
try {
|
|
28
|
+
execSync(base, { cwd, stdio: 'inherit', env: process.env });
|
|
29
|
+
} catch (err) {
|
|
30
|
+
console.error(`\nFailed to install dependencies in ${cwd}`);
|
|
31
|
+
console.error(` Try manually: cd ${cwd} && ${base}\n`);
|
|
32
|
+
process.exit(err.status || 1);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
14
37
|
export function expandHome(p) {
|
|
15
38
|
return p.startsWith('~/') ? resolve(homedir(), p.slice(2)) : p;
|
|
16
39
|
}
|
package/package.json
CHANGED