@geminilight/mindos 0.5.18 → 0.5.20
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/file/route.ts +35 -11
- package/app/app/api/setup/route.ts +64 -1
- package/app/app/api/skills/route.ts +22 -3
- 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/Sidebar.tsx +21 -1
- package/app/components/ask/ToolCallBlock.tsx +2 -1
- package/app/components/settings/KnowledgeTab.tsx +64 -2
- package/app/components/settings/McpTab.tsx +286 -56
- 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 +16 -0
- package/app/lib/settings.ts +29 -0
- package/app/next-env.d.ts +1 -1
- package/app/next.config.ts +7 -0
- package/bin/cli.js +135 -9
- 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/scripts/setup.js +13 -0
- package/skills/mindos/SKILL.md +10 -168
- package/skills/mindos-zh/SKILL.md +14 -172
- package/skills/project-wiki/SKILL.md +80 -74
- package/skills/project-wiki/references/file-reference.md +6 -2
- package/templates/skill-rules/en/skill-rules.md +222 -0
- package/templates/skill-rules/en/user-rules.md +20 -0
- package/templates/skill-rules/zh/skill-rules.md +222 -0
- package/templates/skill-rules/zh/user-rules.md +20 -0
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
|
@@ -251,6 +251,14 @@ export const messages = {
|
|
|
251
251
|
skillLanguage: 'Skill Language',
|
|
252
252
|
skillLangEn: 'English',
|
|
253
253
|
skillLangZh: '中文',
|
|
254
|
+
searchSkills: 'Search skills...',
|
|
255
|
+
customGroup: 'Custom',
|
|
256
|
+
builtinGroup: 'Built-in',
|
|
257
|
+
noSkillsMatch: (query: string) => `No skills match "${query}"`,
|
|
258
|
+
skillTemplate: 'Template',
|
|
259
|
+
skillTemplateGeneral: 'General',
|
|
260
|
+
skillTemplateToolUse: 'Tool-use',
|
|
261
|
+
skillTemplateWorkflow: 'Workflow',
|
|
254
262
|
selectDetected: 'Select Detected',
|
|
255
263
|
clearSelection: 'Clear',
|
|
256
264
|
quickSetup: 'Quick Setup',
|
|
@@ -722,6 +730,14 @@ export const messages = {
|
|
|
722
730
|
skillLanguage: 'Skill 语言',
|
|
723
731
|
skillLangEn: 'English',
|
|
724
732
|
skillLangZh: '中文',
|
|
733
|
+
searchSkills: '搜索 Skill...',
|
|
734
|
+
customGroup: '自定义',
|
|
735
|
+
builtinGroup: '内置',
|
|
736
|
+
noSkillsMatch: (query: string) => `没有匹配「${query}」的 Skill`,
|
|
737
|
+
skillTemplate: '模板',
|
|
738
|
+
skillTemplateGeneral: '通用',
|
|
739
|
+
skillTemplateToolUse: '工具调用',
|
|
740
|
+
skillTemplateWorkflow: '工作流',
|
|
725
741
|
selectDetected: '选择已检测',
|
|
726
742
|
clearSelection: '清除',
|
|
727
743
|
quickSetup: '快速配置',
|
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/app/next.config.ts
CHANGED
|
@@ -8,6 +8,13 @@ const nextConfig: NextConfig = {
|
|
|
8
8
|
turbopack: {
|
|
9
9
|
root: path.join(__dirname),
|
|
10
10
|
},
|
|
11
|
+
// Disable client-side router cache for dynamic layouts so that
|
|
12
|
+
// router.refresh() always fetches a fresh file tree from the server.
|
|
13
|
+
experimental: {
|
|
14
|
+
staleTimes: {
|
|
15
|
+
dynamic: 0,
|
|
16
|
+
},
|
|
17
|
+
},
|
|
11
18
|
};
|
|
12
19
|
|
|
13
20
|
export default nextConfig;
|
package/bin/cli.js
CHANGED
|
@@ -39,13 +39,13 @@
|
|
|
39
39
|
*/
|
|
40
40
|
|
|
41
41
|
import { execSync } from 'node:child_process';
|
|
42
|
-
import { existsSync, readFileSync, writeFileSync, rmSync } from 'node:fs';
|
|
42
|
+
import { existsSync, readFileSync, writeFileSync, rmSync, mkdirSync, cpSync } from 'node:fs';
|
|
43
43
|
import { resolve } from 'node:path';
|
|
44
44
|
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';
|
|
@@ -268,6 +268,41 @@ const commands = {
|
|
|
268
268
|
const webPort = process.env.MINDOS_WEB_PORT || '3456';
|
|
269
269
|
const mcpPort = process.env.MINDOS_MCP_PORT || '8781';
|
|
270
270
|
|
|
271
|
+
// ── Auto-migrate skill rules (v3 split → v4 merged) ──────────────────
|
|
272
|
+
try {
|
|
273
|
+
const cfg = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8'));
|
|
274
|
+
const mr = cfg.mindRoot;
|
|
275
|
+
if (mr && existsSync(mr)) {
|
|
276
|
+
const isZh = cfg.disabledSkills?.includes('mindos');
|
|
277
|
+
const sName = isZh ? 'mindos-zh' : 'mindos';
|
|
278
|
+
const lang = isZh ? 'zh' : 'en';
|
|
279
|
+
const sDir = resolve(mr, '.agents', 'skills', sName);
|
|
280
|
+
const merged = resolve(sDir, 'skill-rules.md');
|
|
281
|
+
const legacyRules = resolve(sDir, 'rules.md');
|
|
282
|
+
// Migrate if legacy exists but merged doesn't
|
|
283
|
+
if (existsSync(legacyRules) && !existsSync(merged)) {
|
|
284
|
+
const tpl = resolve(ROOT, 'templates', 'skill-rules', lang, 'skill-rules.md');
|
|
285
|
+
if (existsSync(tpl)) {
|
|
286
|
+
cpSync(tpl, merged);
|
|
287
|
+
for (const f of ['rules.md', 'patterns.md', 'proactive.md']) {
|
|
288
|
+
const p = resolve(sDir, f);
|
|
289
|
+
if (existsSync(p)) rmSync(p);
|
|
290
|
+
}
|
|
291
|
+
console.log(` ${green('✓')} ${dim('Skill rules migrated to skill-rules.md')}`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
// Init if .agents/skills/ doesn't exist at all
|
|
295
|
+
if (!existsSync(sDir)) {
|
|
296
|
+
const srcDir = resolve(ROOT, 'templates', 'skill-rules', lang);
|
|
297
|
+
if (existsSync(srcDir)) {
|
|
298
|
+
mkdirSync(sDir, { recursive: true });
|
|
299
|
+
cpSync(srcDir, sDir, { recursive: true });
|
|
300
|
+
console.log(` ${green('✓')} ${dim('Skill rules initialized')}`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
} catch { /* best-effort, don't block startup */ }
|
|
305
|
+
|
|
271
306
|
// When launched by a daemon manager (launchd/systemd), wait for ports to
|
|
272
307
|
// free instead of exiting immediately — the previous instance may still be
|
|
273
308
|
// shutting down after a restart/update.
|
|
@@ -325,13 +360,7 @@ const commands = {
|
|
|
325
360
|
const mcpSdk = resolve(ROOT, 'mcp', 'node_modules', '@modelcontextprotocol', 'sdk', 'package.json');
|
|
326
361
|
if (!existsSync(mcpSdk)) {
|
|
327
362
|
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
|
-
}
|
|
363
|
+
npmInstall(resolve(ROOT, 'mcp'), '--no-workspaces');
|
|
335
364
|
}
|
|
336
365
|
// Map config env vars to what the MCP server expects
|
|
337
366
|
const mcpPort = process.env.MINDOS_MCP_PORT || '8781';
|
|
@@ -406,6 +435,80 @@ ${dim('Shortcut: mindos start --daemon → install + start in one step')}
|
|
|
406
435
|
await runGatewayCommand(sub);
|
|
407
436
|
},
|
|
408
437
|
|
|
438
|
+
// ── init-skills ──────────────────────────────────────────────────────────
|
|
439
|
+
'init-skills': async () => {
|
|
440
|
+
const force = process.argv.includes('--force');
|
|
441
|
+
console.log(`\n${bold('📦 Initialize Skill Rules')}\n`);
|
|
442
|
+
|
|
443
|
+
if (!existsSync(CONFIG_PATH)) {
|
|
444
|
+
console.log(` ${red('✘')} Config not found. Run ${cyan('mindos onboard')} first.\n`);
|
|
445
|
+
process.exit(1);
|
|
446
|
+
}
|
|
447
|
+
let config;
|
|
448
|
+
try {
|
|
449
|
+
config = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8'));
|
|
450
|
+
} catch {
|
|
451
|
+
console.log(` ${red('✘')} Failed to parse config at ${dim(CONFIG_PATH)}\n`);
|
|
452
|
+
process.exit(1);
|
|
453
|
+
}
|
|
454
|
+
const mindRoot = config.mindRoot;
|
|
455
|
+
if (!mindRoot || !existsSync(mindRoot)) {
|
|
456
|
+
console.log(` ${red('✘')} Knowledge base not found: ${dim(mindRoot || '(not set)')}\n`);
|
|
457
|
+
process.exit(1);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const isZh = config.disabledSkills?.includes('mindos');
|
|
461
|
+
const skillName = isZh ? 'mindos-zh' : 'mindos';
|
|
462
|
+
const lang = isZh ? 'zh' : 'en';
|
|
463
|
+
const skillDir = resolve(mindRoot, '.agents', 'skills', skillName);
|
|
464
|
+
const sourceDir = resolve(ROOT, 'templates', 'skill-rules', lang);
|
|
465
|
+
|
|
466
|
+
if (!existsSync(sourceDir)) {
|
|
467
|
+
console.log(` ${red('✘')} Template not found: ${dim(sourceDir)}\n`);
|
|
468
|
+
process.exit(1);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const files = ['skill-rules.md', 'user-rules.md'];
|
|
472
|
+
mkdirSync(skillDir, { recursive: true });
|
|
473
|
+
|
|
474
|
+
let count = 0;
|
|
475
|
+
for (const file of files) {
|
|
476
|
+
const dest = resolve(skillDir, file);
|
|
477
|
+
const src = resolve(sourceDir, file);
|
|
478
|
+
if (!existsSync(src)) continue;
|
|
479
|
+
|
|
480
|
+
// Never overwrite user-rules.md even with --force
|
|
481
|
+
if (file === 'user-rules.md' && existsSync(dest)) {
|
|
482
|
+
console.log(` ${dim('skip')} ${file} (user rules preserved)`);
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
if (existsSync(dest) && !force) {
|
|
487
|
+
console.log(` ${dim('skip')} ${file} (already exists)`);
|
|
488
|
+
continue;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
cpSync(src, dest);
|
|
492
|
+
console.log(` ${green('✓')} ${file}${force ? ' (reset)' : ''}`);
|
|
493
|
+
count++;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Clean up legacy split files from v3
|
|
497
|
+
for (const legacy of ['rules.md', 'patterns.md', 'proactive.md']) {
|
|
498
|
+
const legacyPath = resolve(skillDir, legacy);
|
|
499
|
+
if (existsSync(legacyPath)) {
|
|
500
|
+
rmSync(legacyPath);
|
|
501
|
+
console.log(` ${dim('cleanup')} ${legacy} (merged into skill-rules.md)`);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
if (count === 0) {
|
|
506
|
+
console.log(`\n ${dim('All files already exist. Use --force to reset defaults (user-rules.md is always preserved).')}\n`);
|
|
507
|
+
} else {
|
|
508
|
+
console.log(`\n ${green('✔')} Skill rules initialized at ${dim(skillDir)}\n`);
|
|
509
|
+
}
|
|
510
|
+
},
|
|
511
|
+
|
|
409
512
|
// ── doctor ─────────────────────────────────────────────────────────────────
|
|
410
513
|
doctor: async () => {
|
|
411
514
|
const ok = (msg) => console.log(` ${green('✔')} ${msg}`);
|
|
@@ -640,6 +743,28 @@ ${dim('Shortcut: mindos start --daemon → install + start in one step')}
|
|
|
640
743
|
console.log(dim(' Run `mindos start` — it will rebuild automatically.'));
|
|
641
744
|
console.log(` ${dim('View changelog:')} ${cyan('https://github.com/GeminiLight/MindOS/releases')}\n`);
|
|
642
745
|
}
|
|
746
|
+
|
|
747
|
+
// ── Check if skill rules need updating ──────────────────────────────────
|
|
748
|
+
try {
|
|
749
|
+
const updateCfg = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8'));
|
|
750
|
+
const mindRoot = updateCfg.mindRoot;
|
|
751
|
+
if (mindRoot && existsSync(mindRoot)) {
|
|
752
|
+
const isZh = updateCfg.disabledSkills?.includes('mindos');
|
|
753
|
+
const sName = isZh ? 'mindos-zh' : 'mindos';
|
|
754
|
+
const lang = isZh ? 'zh' : 'en';
|
|
755
|
+
const installedRules = resolve(mindRoot, '.agents', 'skills', sName, 'skill-rules.md');
|
|
756
|
+
const tplRules = resolve(ROOT, 'templates', 'skill-rules', lang, 'skill-rules.md');
|
|
757
|
+
if (existsSync(installedRules) && existsSync(tplRules)) {
|
|
758
|
+
const uVer = readFileSync(installedRules, 'utf-8').match(/<!--\s*version:\s*(\S+)\s*-->/)?.[1];
|
|
759
|
+
const tVer = readFileSync(tplRules, 'utf-8').match(/<!--\s*version:\s*(\S+)\s*-->/)?.[1];
|
|
760
|
+
if (uVer && tVer && uVer !== tVer) {
|
|
761
|
+
console.log(` ${yellow('!')} Skill rules ${dim(uVer)} → ${bold(tVer)} available. Run ${cyan('mindos init-skills --force')} to update ${dim('(user-rules.md preserved)')}\n`);
|
|
762
|
+
}
|
|
763
|
+
} else if (!existsSync(resolve(mindRoot, '.agents', 'skills', sName))) {
|
|
764
|
+
console.log(` ${dim('Tip:')} Run ${cyan('mindos init-skills')} to enable skill rules in your knowledge base.\n`);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
} catch {}
|
|
643
768
|
},
|
|
644
769
|
|
|
645
770
|
// ── uninstall ──────────────────────────────────────────────────────────────
|
|
@@ -1084,6 +1209,7 @@ ${row('mindos gateway <subcommand>', 'Manage background service (install/u
|
|
|
1084
1209
|
${bold('Config & Diagnostics:')}
|
|
1085
1210
|
${row('mindos config <subcommand>', 'View/update config (show/validate/set/unset)')}
|
|
1086
1211
|
${row('mindos doctor', 'Health check (config, ports, build, daemon)')}
|
|
1212
|
+
${row('mindos init-skills [--force]', 'Initialize skill rules in knowledge base')}
|
|
1087
1213
|
${row('mindos update', 'Update MindOS to the latest version')}
|
|
1088
1214
|
${row('mindos uninstall', 'Fully uninstall MindOS (stop, remove daemon, npm uninstall)')}
|
|
1089
1215
|
${row('mindos logs', 'Tail service logs (~/.mindos/mindos.log)')}
|
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
package/scripts/setup.js
CHANGED
|
@@ -1205,6 +1205,19 @@ async function main() {
|
|
|
1205
1205
|
writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + '\n');
|
|
1206
1206
|
console.log(`\n${c.green(t('cfgSaved'))}: ${c.dim(CONFIG_PATH)}`);
|
|
1207
1207
|
|
|
1208
|
+
// ── Init skill rules in knowledge base ────────────────────────────────────
|
|
1209
|
+
const skillName = selectedTemplate === 'zh' ? 'mindos-zh' : 'mindos';
|
|
1210
|
+
const skillRulesDir = resolve(mindDir, '.agents', 'skills', skillName);
|
|
1211
|
+
if (!existsSync(skillRulesDir)) {
|
|
1212
|
+
const skillRulesLang = selectedTemplate === 'zh' ? 'zh' : 'en';
|
|
1213
|
+
const skillRulesSource = resolve(ROOT, 'templates', 'skill-rules', skillRulesLang);
|
|
1214
|
+
if (existsSync(skillRulesSource)) {
|
|
1215
|
+
mkdirSync(skillRulesDir, { recursive: true });
|
|
1216
|
+
cpSync(skillRulesSource, skillRulesDir, { recursive: true });
|
|
1217
|
+
console.log(`${c.green('✓')} ${c.dim(`Skill rules initialized: ${skillRulesDir}`)}`);
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1208
1221
|
// ── Step 7: MCP Agent Install ──────────────────────────────────────────────
|
|
1209
1222
|
write('\n');
|
|
1210
1223
|
stepHeader(7);
|