@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.
Files changed (38) hide show
  1. package/app/app/api/ask/route.ts +5 -4
  2. package/app/app/api/file/route.ts +35 -11
  3. package/app/app/api/setup/route.ts +64 -1
  4. package/app/app/api/skills/route.ts +22 -3
  5. package/app/app/globals.css +1 -0
  6. package/app/components/AskFab.tsx +49 -3
  7. package/app/components/AskModal.tsx +11 -2
  8. package/app/components/GuideCard.tsx +361 -0
  9. package/app/components/HomeContent.tsx +2 -2
  10. package/app/components/Sidebar.tsx +21 -1
  11. package/app/components/ask/ToolCallBlock.tsx +2 -1
  12. package/app/components/settings/KnowledgeTab.tsx +64 -2
  13. package/app/components/settings/McpTab.tsx +286 -56
  14. package/app/components/setup/StepAI.tsx +9 -1
  15. package/app/components/setup/index.tsx +4 -0
  16. package/app/components/setup/types.ts +2 -0
  17. package/app/hooks/useAskModal.ts +46 -0
  18. package/app/lib/agent/stream-consumer.ts +4 -2
  19. package/app/lib/agent/tools.ts +26 -12
  20. package/app/lib/fs.ts +9 -1
  21. package/app/lib/i18n.ts +16 -0
  22. package/app/lib/settings.ts +29 -0
  23. package/app/next-env.d.ts +1 -1
  24. package/app/next.config.ts +7 -0
  25. package/bin/cli.js +135 -9
  26. package/bin/lib/build.js +2 -7
  27. package/bin/lib/mcp-spawn.js +2 -13
  28. package/bin/lib/utils.js +23 -0
  29. package/package.json +1 -1
  30. package/scripts/setup.js +13 -0
  31. package/skills/mindos/SKILL.md +10 -168
  32. package/skills/mindos-zh/SKILL.md +14 -172
  33. package/skills/project-wiki/SKILL.md +80 -74
  34. package/skills/project-wiki/references/file-reference.md +6 -2
  35. package/templates/skill-rules/en/skill-rules.md +222 -0
  36. package/templates/skill-rules/en/user-rules.md +20 -0
  37. package/templates/skill-rules/zh/skill-rules.md +222 -0
  38. package/templates/skill-rules/zh/user-rules.md +20 -0
@@ -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
- /** Helper: wrap a tool execute fn with agent-op logging */
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
- throw e;
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().optional().describe('Optional subdirectory to list (e.g. "Projects/Products"). Omit to list everything.'),
51
- depth: z.number().min(1).max(10).optional().describe('Max tree depth to expand (default 3). Directories deeper than this show item count only.'),
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).default(10).describe('Number of files to return') }),
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().default('').describe('Initial file content'),
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).default(10).describe('Number of commits to return'),
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
- const allFiles = buildAllFiles(root);
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: '快速配置',
@@ -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.
@@ -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
- const mcpCwd = resolve(ROOT, 'mcp');
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
- try {
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()) {
@@ -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
- const mcpCwd = resolve(ROOT, 'mcp');
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geminilight/mindos",
3
- "version": "0.5.18",
3
+ "version": "0.5.20",
4
4
  "description": "MindOS — Human-Agent Collaborative Mind System. Local-first knowledge base that syncs your mind to all AI Agents via MCP.",
5
5
  "keywords": [
6
6
  "mindos",
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);