@geminilight/mindos 0.5.1 → 0.5.2

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/scripts/setup.js CHANGED
@@ -6,7 +6,7 @@
6
6
  * Usage: npm run setup OR mindos onboard
7
7
  *
8
8
  * Steps:
9
- * 1. Choose knowledge base path → default ~/MindOS
9
+ * 1. Choose knowledge base path → default ~/MindOS/mind (same as GUI)
10
10
  * 2. Choose template (en / zh / empty / custom) → copy to knowledge base path
11
11
  * 3. Choose ports (web + mcp) — checked for conflicts upfront
12
12
  * 4. Auth token (auto-generated or passphrase-seeded)
@@ -29,6 +29,8 @@ import { pipeline } from 'node:stream/promises';
29
29
  import { execSync, spawn } from 'node:child_process';
30
30
  import { randomBytes, createHash } from 'node:crypto';
31
31
  import { createConnection } from 'node:net';
32
+ import http from 'node:http';
33
+ import { MCP_AGENTS } from '../bin/lib/mcp-agents.js';
32
34
 
33
35
  const __dirname = dirname(fileURLToPath(import.meta.url));
34
36
  const ROOT = resolve(__dirname, '..');
@@ -39,21 +41,21 @@ const CONFIG_PATH = resolve(MINDOS_DIR, 'config.json');
39
41
 
40
42
  const T = {
41
43
  title: { en: '🧠 MindOS Setup', zh: '🧠 MindOS 初始化' },
42
- langHint: { en: ' ← → switch language / 切换语言 ↑ ↓ navigate Enter confirm', zh: ' ← → switch language / 切换语言 ↑ ↓ 上下切换 Enter 确认' },
44
+ langHint: { en: ' ← → 切换中文 ↑ ↓ navigate Enter confirm', zh: ' ← → switch to English ↑ ↓ 上下切换 Enter 确认' },
43
45
 
44
46
  // mode selection
45
- modePrompt: { en: 'Setup mode', zh: '配置方式' },
46
- modeOpts: { en: ['CLI terminal wizard', 'GUI browser wizard (recommended)'], zh: ['CLI — 终端向导', 'GUI — 浏览器向导(推荐)'] },
47
+ modePrompt: { en: 'How would you like to set up?', zh: '选择配置方式' },
48
+ modeOpts: { en: ['Continue here in terminal (CLI)', 'Open browser to set up (recommended)'], zh: ['在终端继续配置(CLI', '打开浏览器配置(推荐)'] },
47
49
  modeVals: ['cli', 'gui'],
48
- guiStarting: { en: '⏳ Starting server for GUI setup...', zh: '⏳ 正在启动服务...' },
49
- guiReady: { en: (url) => `🌐 Complete setup in browser: ${url}`, zh: (url) => `🌐 在浏览器中完成配置: ${url}` },
50
+ guiStarting: { en: '⏳ Starting MindOS, please wait...', zh: '⏳ 正在启动 MindOS,请稍候...' },
51
+ guiReady: { en: (url) => `🌐 Opening setup in browser: ${url}`, zh: (url) => `🌐 在浏览器中打开配置页面: ${url}` },
50
52
  guiOpenFailed: { en: (url) => ` Could not open browser automatically. Open this URL manually:\n ${url}`, zh: (url) => ` 无法自动打开浏览器,请手动访问:\n ${url}` },
51
53
 
52
54
  // step labels
53
55
  step: { en: (n, total) => `Step ${n}/${total}`, zh: (n, total) => `步骤 ${n}/${total}` },
54
56
  stepTitles: {
55
- en: ['Knowledge Base', 'Template', 'Ports', 'Auth Token', 'Web Password', 'AI Provider', 'Start Mode', 'Agent Tools'],
56
- zh: ['知识库', '模板', '端口', 'Auth Token', 'Web 密码', 'AI 服务商', '启动方式', 'Agent 工具'],
57
+ en: ['Knowledge Base', 'AI Provider', 'Ports', 'Auth Token', 'Web Password', 'Start Mode', 'Agent Tools'],
58
+ zh: ['知识库', 'AI 服务商', '端口', 'Auth Token', 'Web 密码', '启动方式', 'Agent 工具'],
57
59
  },
58
60
 
59
61
  // path
@@ -131,6 +133,11 @@ const T = {
131
133
  mcpInstallDone: { en: (n) => `✔ ${n} agent(s) configured`, zh: (n) => `✔ 已配置 ${n} 个 Agent` },
132
134
  mcpSkipped: { en: ' → Skipped. Run `mindos mcp install` anytime to configure agents.', zh: ' → 已跳过。随时运行 `mindos mcp install` 配置 Agent。' },
133
135
 
136
+ // restart prompts (re-onboard with config changes)
137
+ restartRequired: { en: 'Config changed. Service restart required.', zh: '配置已变更,需要重启服务。' },
138
+ restartNow: { en: 'Restart now?', zh: '立即重启?' },
139
+ changesOnNextStart: { en: 'Changes will take effect on next start.', zh: '变更将在下次启动时生效。' },
140
+
134
141
  // next steps (onboard — keep it minimal, details shown on `mindos start`)
135
142
  nextSteps: {
136
143
  en: (cmd) => [
@@ -171,7 +178,26 @@ function write(s) { process.stdout.write(s); }
171
178
 
172
179
  // ── State ─────────────────────────────────────────────────────────────────────
173
180
 
174
- let uiLang = 'en';
181
+ function detectSystemLang() {
182
+ // Check env vars first (Linux / macOS / WSL)
183
+ const vars = [
184
+ process.env.LANG,
185
+ process.env.LC_ALL,
186
+ process.env.LC_MESSAGES,
187
+ process.env.LANGUAGE,
188
+ ].filter(Boolean).join(' ').toLowerCase();
189
+ if (vars.includes('zh')) return 'zh';
190
+
191
+ // Fallback: Intl API (works on Windows where LANG is often unset)
192
+ try {
193
+ const locale = Intl.DateTimeFormat().resolvedOptions().locale;
194
+ if (locale.toLowerCase().startsWith('zh')) return 'zh';
195
+ } catch {}
196
+
197
+ return 'en';
198
+ }
199
+
200
+ let uiLang = detectSystemLang();
175
201
  const t = (key) => T[key]?.[uiLang] ?? T[key]?.en ?? '';
176
202
  const tf = (key, ...args) => {
177
203
  const v = T[key]?.[uiLang] ?? T[key]?.en;
@@ -180,7 +206,7 @@ const tf = (key, ...args) => {
180
206
 
181
207
  // ── Step header ───────────────────────────────────────────────────────────────
182
208
 
183
- const TOTAL_STEPS = 8;
209
+ const TOTAL_STEPS = 7;
184
210
  function stepHeader(n) {
185
211
  const title = T.stepTitles[uiLang][n - 1] ?? T.stepTitles.en[n - 1];
186
212
  const stepLabel = tf('step', n, TOTAL_STEPS);
@@ -397,7 +423,7 @@ function isPortInUse(port) {
397
423
  return new Promise((resolve) => {
398
424
  const sock = createConnection({ port, host: '127.0.0.1' });
399
425
  const cleanup = (result) => { sock.destroy(); resolve(result); };
400
- sock.setTimeout(500, () => cleanup(false));
426
+ sock.setTimeout(500, () => cleanup(true));
401
427
  sock.once('connect', () => cleanup(true));
402
428
  sock.once('error', (err) => {
403
429
  // ECONNREFUSED = nothing listening → free; other errors = treat as in-use
@@ -406,6 +432,25 @@ function isPortInUse(port) {
406
432
  });
407
433
  }
408
434
 
435
+ async function isSelfPort(port) {
436
+ try {
437
+ return await new Promise((resolve) => {
438
+ const req = http.get(`http://127.0.0.1:${port}/api/health`, { timeout: 800 }, (res) => {
439
+ let body = '';
440
+ res.on('data', chunk => { body += chunk; });
441
+ res.on('end', () => {
442
+ try {
443
+ const data = JSON.parse(body);
444
+ resolve(data.service === 'mindos');
445
+ } catch { resolve(false); }
446
+ });
447
+ });
448
+ req.on('error', () => resolve(false));
449
+ req.on('timeout', () => { req.destroy(); resolve(false); });
450
+ });
451
+ } catch { return false; }
452
+ }
453
+
409
454
  async function findFreePort(from) {
410
455
  for (let p = from; p <= 65535; p++) {
411
456
  if (!await isPortInUse(p)) return p;
@@ -414,7 +459,15 @@ async function findFreePort(from) {
414
459
  }
415
460
 
416
461
  async function askPort(labelKey, defaultPort) {
417
- let port = await findFreePort(defaultPort);
462
+ let port = defaultPort;
463
+ // If the default port is in use, check if it's our own service (self = ok to keep)
464
+ if (await isPortInUse(port)) {
465
+ if (await isSelfPort(port)) {
466
+ // Already running on this port — keep it as default
467
+ } else {
468
+ port = await findFreePort(port + 1);
469
+ }
470
+ }
418
471
  while (true) {
419
472
  const input = (await askText(labelKey, String(port))).trim();
420
473
  const parsed = parseInt(input, 10);
@@ -423,6 +476,10 @@ async function askPort(labelKey, defaultPort) {
423
476
  continue;
424
477
  }
425
478
  if (await isPortInUse(parsed)) {
479
+ // Check if it's our own service — acceptable to keep
480
+ if (await isSelfPort(parsed)) {
481
+ return parsed;
482
+ }
426
483
  const next = await findFreePort(parsed + 1);
427
484
  write(c.yellow(tf('portInUse', parsed) + '\n'));
428
485
  port = next;
@@ -518,19 +575,7 @@ async function applyTemplate(tpl, mindDir) {
518
575
  }
519
576
  }
520
577
 
521
- // ── MCP Agent definitions (mirrors bin/lib/mcp-install.js) ───────────────────
522
-
523
- const MCP_AGENTS_SETUP = {
524
- 'claude-code': { name: 'Claude Code', project: '.mcp.json', global: '~/.claude.json', key: 'mcpServers' },
525
- 'claude-desktop': { name: 'Claude Desktop', project: null, global: process.platform === 'darwin' ? '~/Library/Application Support/Claude/claude_desktop_config.json' : '~/.config/Claude/claude_desktop_config.json', key: 'mcpServers' },
526
- 'cursor': { name: 'Cursor', project: '.cursor/mcp.json', global: '~/.cursor/mcp.json', key: 'mcpServers' },
527
- 'windsurf': { name: 'Windsurf', project: null, global: '~/.codeium/windsurf/mcp_config.json', key: 'mcpServers' },
528
- 'cline': { name: 'Cline', project: null, global: process.platform === 'darwin' ? '~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json' : '~/.config/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json', key: 'mcpServers' },
529
- 'trae': { name: 'Trae', project: '.trae/mcp.json', global: '~/.trae/mcp.json', key: 'mcpServers' },
530
- 'gemini-cli': { name: 'Gemini CLI', project: '.gemini/settings.json', global: '~/.gemini/settings.json', key: 'mcpServers' },
531
- 'openclaw': { name: 'OpenClaw', project: null, global: '~/.openclaw/mcp.json', key: 'mcpServers' },
532
- 'codebuddy': { name: 'CodeBuddy', project: null, global: '~/.claude-internal/.claude.json', key: 'mcpServers' },
533
- };
578
+ // MCP_AGENTS imported from bin/lib/mcp-agents.js
534
579
 
535
580
  function expandHomePath(p) {
536
581
  return p.startsWith('~/') ? resolve(homedir(), p.slice(2)) : p;
@@ -538,7 +583,7 @@ function expandHomePath(p) {
538
583
 
539
584
  /** Detect if an agent already has mindos configured (for pre-selection). */
540
585
  function isAgentInstalled(agentKey) {
541
- const agent = MCP_AGENTS_SETUP[agentKey];
586
+ const agent = MCP_AGENTS[agentKey];
542
587
  if (!agent) return false;
543
588
  for (const cfgPath of [agent.global, agent.project]) {
544
589
  if (!cfgPath) continue;
@@ -558,13 +603,13 @@ function isAgentInstalled(agentKey) {
558
603
  * because this script uses its own raw-mode helpers).
559
604
  */
560
605
  async function runMcpInstallStep(mcpPort, authToken) {
561
- const keys = Object.keys(MCP_AGENTS_SETUP);
606
+ const keys = Object.keys(MCP_AGENTS);
562
607
 
563
608
  // Build options with installed status shown as hint
564
609
  const options = keys.map(k => {
565
610
  const installed = isAgentInstalled(k);
566
611
  return {
567
- label: MCP_AGENTS_SETUP[k].name,
612
+ label: MCP_AGENTS[k].name,
568
613
  hint: installed ? (uiLang === 'zh' ? '已安装' : 'installed') : (uiLang === 'zh' ? '未安装' : 'not installed'),
569
614
  value: k,
570
615
  preselect: installed,
@@ -636,7 +681,7 @@ async function runMcpInstallStep(mcpPort, authToken) {
636
681
  let okCount = 0;
637
682
 
638
683
  for (const agentKey of selected) {
639
- const agent = MCP_AGENTS_SETUP[agentKey];
684
+ const agent = MCP_AGENTS[agentKey];
640
685
  // prefer global scope; fall back to project
641
686
  const cfgPath = agent.global || agent.project;
642
687
  if (!cfgPath) {
@@ -778,7 +823,7 @@ async function main() {
778
823
  const existingMode = existing.startMode || 'start';
779
824
  const existingMcpPort = existing.mcpPort || 8787;
780
825
  const existingAuth = existing.authToken || '';
781
- const existingMindRoot = existing.mindRoot || resolve(MINDOS_DIR, 'my-mind');
826
+ const existingMindRoot = existing.mindRoot || resolve(homedir(), 'MindOS', 'mind');
782
827
  console.log(`\n${c.green(t('cfgKept'))} ${c.dim(CONFIG_PATH)}`);
783
828
  write(c.dim(t('cfgKeptNote') + '\n'));
784
829
  const installDaemon = process.argv.includes('--install-daemon');
@@ -790,12 +835,23 @@ async function main() {
790
835
  // ── Step 1: Knowledge base path ───────────────────────────────────────────
791
836
  stepHeader(1);
792
837
 
838
+ // Resume: read existing config to offer current values as defaults
839
+ let resumeCfg = {};
840
+ try { resumeCfg = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8')); } catch { /* first run */ }
841
+
793
842
  const { readdirSync } = await import('node:fs');
794
843
  let mindDir;
795
844
 
845
+ // Default KB path: existing mindRoot if set, otherwise ~/MindOS (same as GUI default)
846
+ const HOME = homedir();
847
+ const kbDefault = resumeCfg.mindRoot || resolve(HOME, 'MindOS', 'mind');
848
+
796
849
  while (true) {
797
- const input = (await askText('pathPrompt', 'my-mind', 'pathHintInline', MINDOS_DIR)).trim();
798
- const resolved = input.startsWith('/') ? input : resolve(MINDOS_DIR, input);
850
+ const input = (await askText('pathPrompt', kbDefault)).trim();
851
+ // If absolute path entered, use as-is; if relative (no leading /), resolve from home
852
+ const resolved = input.startsWith('/') || input.startsWith('~/')
853
+ ? (input.startsWith('~/') ? resolve(HOME, input.slice(2)) : input)
854
+ : resolve(HOME, input);
799
855
  write(tf('pathResolved', resolved) + '\n');
800
856
  mindDir = resolved;
801
857
 
@@ -817,9 +873,8 @@ async function main() {
817
873
  if (choice === 'reselect') { write('\n'); continue; }
818
874
  break;
819
875
  } else {
820
- // ── Step 2: Template ────────────────────────────────────────────────
876
+ // ── Template selection (part of Step 1) ─────────────────────────────
821
877
  write('\n');
822
- stepHeader(2);
823
878
  const tpl = await select('tplPrompt', 'tplOptions', 'tplValues');
824
879
  mkdirSync(mindDir, { recursive: true });
825
880
  await applyTemplate(tpl, mindDir);
@@ -827,60 +882,27 @@ async function main() {
827
882
  }
828
883
  }
829
884
 
830
- // ── Step 3: Ports ─────────────────────────────────────────────────────────
885
+ // ── Step 2: AI Provider + API Key ─────────────────────────────────────────
831
886
  write('\n');
832
- stepHeader(3);
833
- let webPort, mcpPort;
834
- while (true) {
835
- webPort = await askPort('webPortPrompt', 3000);
836
- mcpPort = await askPort('mcpPortPrompt', webPort === 8787 ? 8788 : 8787);
837
- if (webPort !== mcpPort) break;
838
- write(c.yellow(` ⚠ ${uiLang === 'zh' ? 'Web 端口和 MCP 端口不能相同,请重新选择' : 'Web port and MCP port must be different — please choose again'}\n`));
839
- }
840
-
841
- // ── Step 4: Auth token ────────────────────────────────────────────────────
842
- write('\n');
843
- stepHeader(4);
844
- const authSeed = await askText('authPrompt');
845
- const authToken = generateToken(authSeed);
846
- console.log(`${c.green(t('tokenGenerated'))}: ${c.cyan(authToken)}`);
847
-
848
- // ── Step 5: Web UI password ───────────────────────────────────────────────
849
- write('\n');
850
- stepHeader(5);
851
- let webPassword = '';
852
- while (true) {
853
- webPassword = await askText('webPassPrompt');
854
- if (webPassword) break;
855
- write(c.yellow(t('webPassWarn') + '\n'));
856
- const confirmed = await askYesNo('webPassSkip');
857
- if (confirmed) break;
858
- }
859
-
860
- // ── Step 6: AI Provider + API Key ─────────────────────────────────────────
861
- write('\n');
862
- stepHeader(6);
887
+ stepHeader(2);
863
888
 
864
889
  const provider = await select('providerPrompt', 'providerOpts', 'providerVals');
865
890
  const isSkip = provider === 'skip';
866
891
  const isAnthropic = provider === 'anthropic';
867
892
 
868
- // preserve existing provider configs
893
+ // preserve existing provider configs (use resumeCfg already read at top of main)
869
894
  let existingProviders = {
870
895
  anthropic: { apiKey: '', model: 'claude-sonnet-4-6' },
871
896
  openai: { apiKey: '', model: 'gpt-5.4', baseUrl: '' },
872
897
  };
873
898
  let existingAiProvider = 'anthropic';
874
- try {
875
- const existing = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8'));
876
- if (existing.ai?.providers) {
877
- existingProviders = { ...existingProviders, ...existing.ai.providers };
878
- } else if (existing.ai?.anthropicApiKey) {
879
- existingProviders.anthropic = { apiKey: existing.ai.anthropicApiKey || '', model: existing.ai.anthropicModel || 'claude-sonnet-4-6' };
880
- existingProviders.openai = { apiKey: existing.ai.openaiApiKey || '', model: existing.ai.openaiModel || 'gpt-5.4', baseUrl: existing.ai.openaiBaseUrl || '' };
881
- }
882
- if (existing.ai?.provider) existingAiProvider = existing.ai.provider;
883
- } catch { /* ignore */ }
899
+ if (resumeCfg.ai?.providers) {
900
+ existingProviders = { ...existingProviders, ...resumeCfg.ai.providers };
901
+ } else if (resumeCfg.ai?.anthropicApiKey) {
902
+ existingProviders.anthropic = { apiKey: resumeCfg.ai.anthropicApiKey || '', model: resumeCfg.ai.anthropicModel || 'claude-sonnet-4-6' };
903
+ existingProviders.openai = { apiKey: resumeCfg.ai.openaiApiKey || '', model: resumeCfg.ai.openaiModel || 'gpt-5.4', baseUrl: resumeCfg.ai.openaiBaseUrl || '' };
904
+ }
905
+ if (resumeCfg.ai?.provider) existingAiProvider = resumeCfg.ai.provider;
884
906
 
885
907
  if (isSkip) {
886
908
  write(c.dim(t('providerSkip') + '\n'));
@@ -905,9 +927,69 @@ async function main() {
905
927
  }
906
928
  }
907
929
 
908
- // ── Step 7: Start Mode ──────────────────────────────────────────────────
930
+ // ── Step 3: Ports ─────────────────────────────────────────────────────────
909
931
  write('\n');
910
- stepHeader(7);
932
+ stepHeader(3);
933
+ const existingCfg = resumeCfg;
934
+ const defaultWebPort = typeof existingCfg.port === 'number' ? existingCfg.port : 3000;
935
+ const defaultMcpPort = typeof existingCfg.mcpPort === 'number' ? existingCfg.mcpPort : (defaultWebPort === 8787 ? 8788 : 8787);
936
+ let webPort, mcpPort;
937
+ while (true) {
938
+ webPort = await askPort('webPortPrompt', defaultWebPort);
939
+ mcpPort = await askPort('mcpPortPrompt', defaultMcpPort);
940
+ if (webPort !== mcpPort) break;
941
+ write(c.yellow(` ⚠ ${uiLang === 'zh' ? 'Web 端口和 MCP 端口不能相同,请重新选择' : 'Web port and MCP port must be different — please choose again'}\n`));
942
+ }
943
+
944
+ // ── Step 4: Auth token ────────────────────────────────────────────────────
945
+ write('\n');
946
+ stepHeader(4);
947
+ // Resume: if config already has a token, offer it as the default (Enter = keep)
948
+ const existingToken = existingCfg.authToken || '';
949
+ let authToken;
950
+ if (existingToken) {
951
+ const masked = existingToken.length > 8 ? existingToken.slice(0, 8) + '····' : existingToken;
952
+ write(c.dim(` ${uiLang === 'zh' ? '现有 token:' : 'Current token:'} ${c.cyan(masked)}\n`));
953
+ const keepToken = await askYesNoDefault('cfgConfirm');
954
+ if (keepToken) {
955
+ authToken = existingToken;
956
+ console.log(`${c.green(t('tokenGenerated'))}: ${c.cyan(existingToken.slice(0, 8) + '····')}`);
957
+ } else {
958
+ const authSeed = await askText('authPrompt');
959
+ authToken = generateToken(authSeed);
960
+ console.log(`${c.green(t('tokenGenerated'))}: ${c.cyan(authToken)}`);
961
+ }
962
+ } else {
963
+ const authSeed = await askText('authPrompt');
964
+ authToken = generateToken(authSeed);
965
+ console.log(`${c.green(t('tokenGenerated'))}: ${c.cyan(authToken)}`);
966
+ }
967
+
968
+ // ── Step 5: Web UI password ───────────────────────────────────────────────
969
+ write('\n');
970
+ stepHeader(5);
971
+ let webPassword = '';
972
+ const existingPassword = existingCfg.webPassword || '';
973
+ if (existingPassword) {
974
+ write(c.dim(` ${uiLang === 'zh' ? '已设置密码(Enter 保留)' : 'Password is set (Enter to keep)'}\n`));
975
+ const keepPass = await askYesNoDefault('cfgConfirm');
976
+ if (keepPass) {
977
+ webPassword = existingPassword;
978
+ }
979
+ }
980
+ if (!webPassword) {
981
+ while (true) {
982
+ webPassword = await askText('webPassPrompt');
983
+ if (webPassword) break;
984
+ write(c.yellow(t('webPassWarn') + '\n'));
985
+ const confirmed = await askYesNo('webPassSkip');
986
+ if (confirmed) break;
987
+ }
988
+ }
989
+
990
+ // ── Step 6: Start Mode ──────────────────────────────────────────────────
991
+ write('\n');
992
+ stepHeader(6);
911
993
 
912
994
  let startMode = 'start';
913
995
  const daemonPlatform = process.platform === 'darwin' || process.platform === 'linux';
@@ -952,13 +1034,22 @@ async function main() {
952
1034
  process.exit(0);
953
1035
  }
954
1036
 
1037
+ const isResuming = Object.keys(resumeCfg).length > 0;
1038
+ const needsRestart = isResuming && (
1039
+ config.port !== (resumeCfg.port ?? 3000) ||
1040
+ config.mcpPort !== (resumeCfg.mcpPort ?? 8787) ||
1041
+ config.mindRoot !== (resumeCfg.mindRoot ?? '') ||
1042
+ config.authToken !== (resumeCfg.authToken ?? '') ||
1043
+ config.webPassword !== (resumeCfg.webPassword ?? '')
1044
+ );
1045
+
955
1046
  mkdirSync(MINDOS_DIR, { recursive: true });
956
1047
  writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + '\n');
957
1048
  console.log(`\n${c.green(t('cfgSaved'))}: ${c.dim(CONFIG_PATH)}`);
958
1049
 
959
- // ── Step 8: MCP Agent Install ──────────────────────────────────────────────
1050
+ // ── Step 7: MCP Agent Install ──────────────────────────────────────────────
960
1051
  write('\n');
961
- stepHeader(8);
1052
+ stepHeader(7);
962
1053
  write(c.dim(tf('mcpStepHint') + '\n\n'));
963
1054
 
964
1055
  await runMcpInstallStep(mcpPort, authToken);
@@ -973,7 +1064,7 @@ async function main() {
973
1064
  }
974
1065
 
975
1066
  const installDaemon = startMode === 'daemon' || process.argv.includes('--install-daemon');
976
- finish(mindDir, config.startMode, config.mcpPort, config.authToken, installDaemon);
1067
+ finish(mindDir, config.startMode, config.mcpPort, config.authToken, installDaemon, needsRestart, resumeCfg.port ?? 3000);
977
1068
  }
978
1069
 
979
1070
  function getLocalIP() {
@@ -985,7 +1076,25 @@ function getLocalIP() {
985
1076
  return null;
986
1077
  }
987
1078
 
988
- async function finish(mindDir, startMode = 'start', mcpPort = 8787, authToken = '', installDaemon = false) {
1079
+ async function finish(mindDir, startMode = 'start', mcpPort = 8787, authToken = '', installDaemon = false, needsRestart = false, oldPort = 3000) {
1080
+ if (needsRestart) {
1081
+ const isRunning = await isSelfPort(oldPort);
1082
+ if (isRunning) {
1083
+ write(c.yellow(t('restartRequired') + '\n'));
1084
+ const doRestart = await askYesNoDefault('restartNow');
1085
+ if (doRestart) {
1086
+ const cliPath = resolve(__dirname, '../bin/cli.js');
1087
+ execSync(`node "${cliPath}" start`, { stdio: 'inherit' });
1088
+ } else {
1089
+ write(c.dim(t('restartManual') + '\n'));
1090
+ }
1091
+ return;
1092
+ } else {
1093
+ write(c.dim(t('changesOnNextStart') + '\n'));
1094
+ // fall through to normal Start now? prompt
1095
+ }
1096
+ }
1097
+
989
1098
  const startCmd = installDaemon ? 'mindos start --daemon' : (startMode === 'dev' ? 'mindos dev' : 'mindos start');
990
1099
  const lines = T.nextSteps[uiLang](startCmd);
991
1100
  console.log('');
@@ -993,13 +1102,13 @@ async function finish(mindDir, startMode = 'start', mcpPort = 8787, authToken =
993
1102
 
994
1103
  const doStart = await askYesNoDefault('startNow');
995
1104
  if (doStart) {
996
- const { execSync } = await import('node:child_process');
1105
+ const { execSync: exec } = await import('node:child_process');
997
1106
  const cliPath = resolve(__dirname, '../bin/cli.js');
998
1107
  if (installDaemon) {
999
1108
  // Install and start as background service — returns immediately
1000
- execSync(`node "${cliPath}" start --daemon`, { stdio: 'inherit' });
1109
+ exec(`node "${cliPath}" start --daemon`, { stdio: 'inherit' });
1001
1110
  } else {
1002
- execSync(`node "${cliPath}" ${startMode}`, { stdio: 'inherit' });
1111
+ exec(`node "${cliPath}" ${startMode}`, { stdio: 'inherit' });
1003
1112
  }
1004
1113
  }
1005
1114
  }
@@ -0,0 +1,143 @@
1
+ ---
2
+ name: human-insights
3
+ description: "捕捉、记录和提炼人机协作过程中产生的隐性知识,存入项目 human-insights/ 目录。当用户说「记录下这个发现」「这个方法很有效,存一下」「回顾下我们的协作」「总结下 SOP」「这个 AI 总是犯这个错」「把这个变成规则」「整理下 insights」「提炼下规律」「升华到 CLAUDE.md」时使用此 Skill。对话中出现值得沉淀的协作模式、认知升级、反模式时也应触发。"
4
+ ---
5
+
6
+ # Human Insights Skill
7
+
8
+ 捕捉人机协作中产生的隐性知识,让它不随对话消失,并定期提炼为可复用的 SOP。
9
+
10
+ 核心设计原则:**零摩擦**。记录的成本越低,记录的频率越高,积累的价值越大。所有让用户需要思考"怎么配合"的设计都是反模式。
11
+
12
+ ---
13
+
14
+ ## 核心理念
15
+
16
+ insights 不是终点,是 CLAUDE.md 的原材料。生命周期:
17
+
18
+ ```
19
+ 对话碎片 → 捕捉 → human-insights/ → 定期回顾提炼 → CLAUDE.md / Skill
20
+ ```
21
+
22
+ 已升华进 CLAUDE.md 的 insight 标记为 `[已采纳]`,避免重复处理。
23
+
24
+ ---
25
+
26
+ ## 三种模式
27
+
28
+ ### 模式一:捕捉(用户主动触发)
29
+
30
+ 用户说"记录下这个"、"这个很有用"时触发。
31
+
32
+ 1. 理解内容 — 读懂要记录什么,必要时追问一句
33
+ 2. **AI 判断分类**,用户不需要选(见下方分类体系)
34
+ 3. 检查目标文件,避免重复记录
35
+ 4. 写入,自动填写时间,来源一句话
36
+ 5. 告知用户记录到了哪里
37
+
38
+ ### 模式二:主动提议(AI 识别信号)
39
+
40
+ 对话中出现以下信号时,AI 在回答完后顺带问一句"这个发现值得记进 human-insights,要我记一下吗?"
41
+
42
+ **值得提议的信号(需同时满足:有新意 + 可复用):**
43
+ - 用户明确纠正了 AI 的错误认知或行为模式
44
+ - 用户发现某个做法比之前的方式明显更高效,且说清楚了为什么
45
+ - 对话中归纳出了一条可以跨场景复用的规律或原则
46
+
47
+ **不需要提议的情况:**
48
+ - 只是某个任务执行得顺利(不代表有可复用的模式)
49
+ - 用户已经在说"记一下"(模式一已覆盖)
50
+ - 同一对话中已经提议过一次(避免反复打断)
51
+
52
+ 用户说"好"/"记一下"即执行,说"不用"则跳过。不打断主流程,轻量提议。
53
+
54
+ ### 模式三:回顾提炼(定期整理)
55
+
56
+ 用户说"回顾下协作"、"总结 SOP"、"升华到 CLAUDE.md" 时触发。
57
+
58
+ 1. 读取 `human-insights/` 下所有文件,统计各类别条目数和"待提炼"数量
59
+ 2. 识别模式 — 找出:① 反复出现的主题 ② 相互印证的发现 ③ 已有足够证据支撑的规律
60
+ 3. 起草提炼结果,每条产出明确其形态:
61
+ - **规则**:适合写进 CLAUDE.md 的行为约束("做 X 之前先做 Y")
62
+ - **流程**:可复用的步骤序列(适合写成 Skill 或 CLAUDE.md 流程节)
63
+ - **Prompt 模板**:可直接复用的指令结构
64
+ 4. 展示给用户确认,用户可修改或拒绝某条
65
+ 5. 写入 CLAUDE.md 对应章节,原 insight 标记 `[已采纳 → CLAUDE.md #章节名]`
66
+ 6. 更新 `README.md` 各文件摘要
67
+
68
+ ---
69
+
70
+ ## 分类体系(AI 判断,用户无感)
71
+
72
+ | 类别 | 文件 | 记录什么 |
73
+ |------|------|---------|
74
+ | Prompt 模式 | `prompt-patterns.md` | 有效的提问方式、指令结构、上下文给法 |
75
+ | 工作流发现 | `workflow.md` | 有效的协作流程、任务拆分方式、节奏把握 |
76
+ | AI 行为规律 | `ai-behavior.md` | AI 在特定场景下的规律性表现、容易犯的错 |
77
+ | 认知升级 | `mindset.md` | 对某个概念/方法的新理解,之前认知被纠正 |
78
+ | 反模式 | `anti-patterns.md` | 踩过的坑、低效做法、应该避免的模式 |
79
+
80
+ 一条 insight 可以属于多个类别时,以最主要的类别为准写入一个文件,不重复写入。
81
+
82
+ ---
83
+
84
+ ## 记录格式
85
+
86
+ 轻量为主,自动填时间,来源一句话够了:
87
+
88
+ ```markdown
89
+ ## <标题>
90
+
91
+ **时间:** YYYY-MM-DD **来源:** <一句话场景,如"MindOS CLAUDE.md 整理中">
92
+
93
+ <发现了什么、为什么有价值、怎么用。1-3 句话。>
94
+
95
+ **状态:** 待提炼 / [已采纳 → CLAUDE.md #章节名]
96
+ ```
97
+
98
+ ---
99
+
100
+ ## 目录结构
101
+
102
+ ```
103
+ human-insights/
104
+ ├── README.md # 索引 + 各文件一句话摘要(只列已创建的文件)
105
+ ├── prompt-patterns.md
106
+ ├── workflow.md
107
+ ├── ai-behavior.md
108
+ ├── mindset.md
109
+ └── anti-patterns.md
110
+ ```
111
+
112
+ 首次使用时只创建 `README.md` 和当前需要写入的文件,其余按需创建。
113
+
114
+ ---
115
+
116
+ ## 初始化
117
+
118
+ `human-insights/` 不存在时自动创建目录和 `README.md`:
119
+
120
+ ```markdown
121
+ # Human Insights
122
+
123
+ 记录人机协作过程中产生的隐性知识,定期提炼为可复用的 SOP。
124
+
125
+ ## 文件索引
126
+
127
+ (按需添加,创建新文件时同步更新此表)
128
+
129
+ | 文件 | 摘要 |
130
+ |------|------|
131
+
132
+ ## 升华路径
133
+
134
+ 积累 → 回顾提炼 → 写入 CLAUDE.md → 标记 [已采纳]
135
+ ```
136
+
137
+ ---
138
+
139
+ ## 边界说明
140
+
141
+ - vs `wiki/80-known-pitfalls.md`:pitfalls 记客观技术坑(别人也会踩),insights 记主观协作发现(你个人的认知)
142
+ - vs `wiki/06-conventions.md`:conventions 是已确定的规范,insights 是还在验证中的发现
143
+ - vs `CLAUDE.md`:CLAUDE.md 是已固化的规则,insights 是原材料
@@ -1,14 +1,15 @@
1
1
  ---
2
2
  name: mindos
3
3
  description: >
4
- MindOS knowledge base operation guide for agent tasks on local markdown/csv knowledge bases.
5
- Use proactively whenever work touches note files, SOP/workflow docs, profile/context docs,
6
- CSV tables, knowledge-base organization, cross-agent handoff, or decision sync through MindOS MCP tools.
7
- Trigger on requests like "update notes", "search knowledge base", "organize files", "execute SOP",
4
+ MindOS knowledge base operation guide, only for agent tasks on files inside the MindOS knowledge base (mindRoot path).
5
+ Trigger only when the target files are inside the MindOS knowledge base directory.
6
+ Typical requests: "update notes", "search knowledge base", "organize files", "execute SOP",
8
7
  "review with our standards", "handoff to another agent", "sync decisions", "append CSV",
9
8
  "retrospective", "distill this conversation", "capture key learnings", "update related docs adaptively",
10
- "route this to the right files", "update everything related", "sync this across my knowledge base",
11
- and generally any local knowledge-maintenance workflow even if the user does not explicitly mention MindOS.
9
+ "route this to the right files", "update everything related", "sync this across my knowledge base".
10
+ Do NOT trigger when: the target is a local code repository file (e.g. /code/xxx/wiki/*.md),
11
+ the user provides an absolute path that is not under MindOS mindRoot,
12
+ or the task involves modifying project source code or project documentation.
12
13
  ---
13
14
 
14
15
  # MindOS Knowledge Base Operation Guide