@geminilight/mindos 0.4.0 → 0.5.1

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/lib/i18n.ts CHANGED
@@ -271,7 +271,7 @@ export const messages = {
271
271
  releaseNotes: 'view release notes',
272
272
  },
273
273
  setup: {
274
- stepTitles: ['Knowledge Base', 'AI Provider', 'Ports', 'Security', 'Review'],
274
+ stepTitles: ['Knowledge Base', 'AI Provider', 'Ports', 'Security', 'Agent Tools', 'Review'],
275
275
  // Step 1
276
276
  kbPath: 'Knowledge base path',
277
277
  kbPathHint: 'Absolute path to your notes directory.',
@@ -291,6 +291,11 @@ export const messages = {
291
291
  mcpPort: 'MCP server port',
292
292
  portHint: 'Valid range: 1024–65535',
293
293
  portRestartWarning: 'Port changes take effect after server restart.',
294
+ portInUse: (p: number) => `Port ${p} is already in use.`,
295
+ portSuggest: (p: number) => `Use ${p}`,
296
+ portChecking: 'Checking…',
297
+ portConflict: 'Web UI and MCP ports must be different.',
298
+ portVerifyHint: 'Click outside each field to verify, or wait for auto-check.',
294
299
  // Step 4
295
300
  authToken: 'Auth Token',
296
301
  authTokenHint: 'Bearer token for MCP / API clients. Auto-generated.',
@@ -301,16 +306,30 @@ export const messages = {
301
306
  copiedToken: 'Copied!',
302
307
  webPassword: 'Web UI Password',
303
308
  webPasswordHint: 'Optional. Protect browser access with a password.',
304
- // Step 5
305
- reviewTitle: 'Review Configuration',
309
+ // Step 5 — Agent Tools
310
+ agentToolsTitle: 'Agent Tools',
311
+ agentToolsHint: 'Select AI agents to configure with MindOS MCP. Agents marked "not installed" can be configured now — they will work once you install the app.',
312
+ agentTransport: 'Transport',
313
+ agentScope: 'Scope',
314
+ agentToolsLoading: 'Loading agents…',
315
+ agentToolsEmpty: 'No supported agents detected.',
316
+ agentNoneSelected: 'No agents selected — you can configure later in Settings → MCP.',
317
+ agentNotInstalled: 'not installed',
318
+ agentStatusOk: 'configured',
319
+ agentStatusError: 'failed',
320
+ agentInstalling: 'Configuring…',
321
+ // Step 2 — AI skip card
322
+ aiSkipTitle: 'Skip for now',
323
+ aiSkipDesc: 'You can add an API key later in Settings → AI.',
324
+ // Step 6 — Review
306
325
  reviewHint: 'Verify your settings before completing setup.',
307
- keyMasked: (key: string) => key.slice(0, 6) + '•••' + key.slice(-3),
326
+ reviewInstallResults: 'Agent configuration results:',
327
+ portAvailable: 'Available',
308
328
  portChanged: 'Port changed — please restart the server for it to take effect.',
309
329
  // Buttons
310
330
  back: 'Back',
311
331
  next: 'Next',
312
332
  complete: 'Complete Setup',
313
- skip: 'Skip',
314
333
  // Status
315
334
  completing: 'Saving...',
316
335
  completeDone: 'Setup complete!',
@@ -587,7 +606,7 @@ export const messages = {
587
606
  releaseNotes: '查看更新说明',
588
607
  },
589
608
  setup: {
590
- stepTitles: ['知识库', 'AI 服务商', '端口', '安全', '确认'],
609
+ stepTitles: ['知识库', 'AI 服务商', '端口', '安全', 'Agent 工具', '确认'],
591
610
  // Step 1
592
611
  kbPath: '知识库路径',
593
612
  kbPathHint: '笔记目录的绝对路径。',
@@ -607,6 +626,11 @@ export const messages = {
607
626
  mcpPort: 'MCP 服务端口',
608
627
  portHint: '有效范围:1024–65535',
609
628
  portRestartWarning: '端口修改需重启服务后生效。',
629
+ portInUse: (p: number) => `端口 ${p} 已被占用。`,
630
+ portSuggest: (p: number) => `使用 ${p}`,
631
+ portChecking: '检测中…',
632
+ portConflict: 'Web UI 端口和 MCP 端口不能相同。',
633
+ portVerifyHint: '点击输入框外部验证,或等待自动检测。',
610
634
  // Step 4
611
635
  authToken: 'Auth Token',
612
636
  authTokenHint: 'MCP / API 客户端使用的 Bearer Token,自动生成。',
@@ -617,10 +641,25 @@ export const messages = {
617
641
  copiedToken: '已复制!',
618
642
  webPassword: '网页访问密码',
619
643
  webPasswordHint: '可选。设置后浏览器访问需要登录。',
620
- // Step 5
621
- reviewTitle: '确认配置',
644
+ // Step 5 — Agent Tools
645
+ agentToolsTitle: 'Agent 工具',
646
+ agentToolsHint: '选择要与 MindOS MCP 配置的 AI Agent。标注「未安装」的 agent 可以先行配置,安装应用后即可生效。',
647
+ agentTransport: '传输方式',
648
+ agentScope: '范围',
649
+ agentToolsLoading: '正在加载 Agent…',
650
+ agentToolsEmpty: '未检测到受支持的 Agent。',
651
+ agentNoneSelected: '未选择 agent — 可稍后在 设置 → MCP 中配置。',
652
+ agentNotInstalled: '未安装',
653
+ agentStatusOk: '已配置',
654
+ agentStatusError: '失败',
655
+ agentInstalling: '配置中…',
656
+ // Step 2 — AI skip card
657
+ aiSkipTitle: '暂时跳过',
658
+ aiSkipDesc: '稍后可在 设置 → AI 中添加 API 密钥。',
659
+ // Step 6 — Review
622
660
  reviewHint: '完成设置前请确认以下信息。',
623
- keyMasked: (key: string) => key.slice(0, 6) + '•••' + key.slice(-3),
661
+ reviewInstallResults: 'Agent 配置结果:',
662
+ portAvailable: '可用',
624
663
  portChanged: '端口已变更 — 请重启服务以使其生效。',
625
664
  // Buttons
626
665
  back: '上一步',
@@ -232,7 +232,10 @@ export async function mcpInstall() {
232
232
  const ask2 = (q) => new Promise(r => rl2.question(q, r));
233
233
 
234
234
  if (!url) {
235
- url = hasYesFlag ? 'http://localhost:8787/mcp' : (await ask2(`${bold('MCP URL')} ${dim('[http://localhost:8787/mcp]:')} `)).trim() || 'http://localhost:8787/mcp';
235
+ let mcpPort = 8787;
236
+ try { mcpPort = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8')).mcpPort || 8787; } catch {}
237
+ const defaultUrl = `http://localhost:${mcpPort}/mcp`;
238
+ url = hasYesFlag ? defaultUrl : (await ask2(`${bold('MCP URL')} ${dim(`[${defaultUrl}]:`)} `)).trim() || defaultUrl;
236
239
  }
237
240
 
238
241
  if (!token) {
package/bin/lib/port.js CHANGED
@@ -4,8 +4,14 @@ import { bold, dim, red } from './colors.js';
4
4
  export function isPortInUse(port) {
5
5
  return new Promise((resolve) => {
6
6
  const sock = createConnection({ port, host: '127.0.0.1' });
7
- sock.once('connect', () => { sock.destroy(); resolve(true); });
8
- sock.once('error', () => { sock.destroy(); resolve(false); });
7
+ const cleanup = (result) => { sock.destroy(); resolve(result); };
8
+ sock.setTimeout(500, () => cleanup(false));
9
+ sock.once('connect', () => cleanup(true));
10
+ sock.once('error', (err) => {
11
+ // ECONNREFUSED = nothing listening → port is free
12
+ // EACCES / ENETUNREACH / etc. = treat as unavailable (can't bind either)
13
+ cleanup(err.code !== 'ECONNREFUSED');
14
+ });
9
15
  });
10
16
  }
11
17
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geminilight/mindos",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
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
@@ -52,8 +52,8 @@ const T = {
52
52
  // step labels
53
53
  step: { en: (n, total) => `Step ${n}/${total}`, zh: (n, total) => `步骤 ${n}/${total}` },
54
54
  stepTitles: {
55
- en: ['Knowledge Base', 'Template', 'Ports', 'Auth Token', 'Web Password', 'AI Provider', 'Start Mode'],
56
- zh: ['知识库', '模板', '端口', 'Auth Token', 'Web 密码', 'AI 服务商', '启动方式'],
55
+ en: ['Knowledge Base', 'Template', 'Ports', 'Auth Token', 'Web Password', 'AI Provider', 'Start Mode', 'Agent Tools'],
56
+ zh: ['知识库', '模板', '端口', 'Auth Token', 'Web 密码', 'AI 服务商', '启动方式', 'Agent 工具'],
57
57
  },
58
58
 
59
59
  // path
@@ -122,6 +122,15 @@ const T = {
122
122
  syncSetup: { en: 'Set up cross-device sync via Git?', zh: '是否配置 Git 跨设备同步?' },
123
123
  syncLater: { en: ' → Run `mindos sync init` anytime to set up sync later.', zh: ' → 随时运行 `mindos sync init` 配置同步。' },
124
124
 
125
+ // mcp install step
126
+ mcpStepTitle: { en: 'Agent Tools (MCP)', zh: 'Agent 工具 (MCP)' },
127
+ mcpStepHint: { en: 'Select AI agents to configure with MindOS MCP (Space to toggle, A for all, Enter to confirm).\nAgents not yet installed can be pre-configured — they will work once you install the app.', zh: '选择要配置 MindOS MCP 的 AI Agent(空格切换,A 全选,Enter 确认)。\n未安装的 Agent 可以预先配置,安装应用后即可生效。' },
128
+ mcpInstalling: { en: (n) => `⏳ Configuring ${n} agent(s)...`, zh: (n) => `⏳ 正在配置 ${n} 个 Agent...` },
129
+ mcpInstallOk: { en: (name, path) => ` ${c.green('✔')} ${name} ${c.dim('→ ' + path)}`, zh: (name, path) => ` ${c.green('✔')} ${name} ${c.dim('→ ' + path)}` },
130
+ mcpInstallFail: { en: (name, msg) => ` ${c.red('✘')} ${name} ${c.dim(msg)}`, zh: (name, msg) => ` ${c.red('✘')} ${name} ${c.dim(msg)}` },
131
+ mcpInstallDone: { en: (n) => `✔ ${n} agent(s) configured`, zh: (n) => `✔ 已配置 ${n} 个 Agent` },
132
+ mcpSkipped: { en: ' → Skipped. Run `mindos mcp install` anytime to configure agents.', zh: ' → 已跳过。随时运行 `mindos mcp install` 配置 Agent。' },
133
+
125
134
  // next steps (onboard — keep it minimal, details shown on `mindos start`)
126
135
  nextSteps: {
127
136
  en: (cmd) => [
@@ -171,7 +180,7 @@ const tf = (key, ...args) => {
171
180
 
172
181
  // ── Step header ───────────────────────────────────────────────────────────────
173
182
 
174
- const TOTAL_STEPS = 7;
183
+ const TOTAL_STEPS = 8;
175
184
  function stepHeader(n) {
176
185
  const title = T.stepTitles[uiLang][n - 1] ?? T.stepTitles.en[n - 1];
177
186
  const stepLabel = tf('step', n, TOTAL_STEPS);
@@ -387,15 +396,21 @@ const askYesNoDefault = (labelKey, arg = '') => askYesNo(labelKey, arg, true);
387
396
  function isPortInUse(port) {
388
397
  return new Promise((resolve) => {
389
398
  const sock = createConnection({ port, host: '127.0.0.1' });
390
- sock.once('connect', () => { sock.destroy(); resolve(true); });
391
- sock.once('error', () => { sock.destroy(); resolve(false); });
399
+ const cleanup = (result) => { sock.destroy(); resolve(result); };
400
+ sock.setTimeout(500, () => cleanup(false));
401
+ sock.once('connect', () => cleanup(true));
402
+ sock.once('error', (err) => {
403
+ // ECONNREFUSED = nothing listening → free; other errors = treat as in-use
404
+ cleanup(err.code !== 'ECONNREFUSED');
405
+ });
392
406
  });
393
407
  }
394
408
 
395
409
  async function findFreePort(from) {
396
- let p = from;
397
- while (p <= 65535 && await isPortInUse(p)) p++;
398
- return p;
410
+ for (let p = from; p <= 65535; p++) {
411
+ if (!await isPortInUse(p)) return p;
412
+ }
413
+ return from; // fallback (extremely unlikely)
399
414
  }
400
415
 
401
416
  async function askPort(labelKey, defaultPort) {
@@ -503,6 +518,150 @@ async function applyTemplate(tpl, mindDir) {
503
518
  }
504
519
  }
505
520
 
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
+ };
534
+
535
+ function expandHomePath(p) {
536
+ return p.startsWith('~/') ? resolve(homedir(), p.slice(2)) : p;
537
+ }
538
+
539
+ /** Detect if an agent already has mindos configured (for pre-selection). */
540
+ function isAgentInstalled(agentKey) {
541
+ const agent = MCP_AGENTS_SETUP[agentKey];
542
+ if (!agent) return false;
543
+ for (const cfgPath of [agent.global, agent.project]) {
544
+ if (!cfgPath) continue;
545
+ const abs = expandHomePath(cfgPath);
546
+ if (!existsSync(abs)) continue;
547
+ try {
548
+ const config = JSON.parse(readFileSync(abs, 'utf-8'));
549
+ if (config[agent.key]?.mindos) return true;
550
+ } catch { /* ignore */ }
551
+ }
552
+ return false;
553
+ }
554
+
555
+ /**
556
+ * Step 8: interactive multi-select of agents to configure, then install.
557
+ * Uses the same interactiveMultiSelect as mcp-install.js (re-implemented inline
558
+ * because this script uses its own raw-mode helpers).
559
+ */
560
+ async function runMcpInstallStep(mcpPort, authToken) {
561
+ const keys = Object.keys(MCP_AGENTS_SETUP);
562
+
563
+ // Build options with installed status shown as hint
564
+ const options = keys.map(k => {
565
+ const installed = isAgentInstalled(k);
566
+ return {
567
+ label: MCP_AGENTS_SETUP[k].name,
568
+ hint: installed ? (uiLang === 'zh' ? '已安装' : 'installed') : (uiLang === 'zh' ? '未安装' : 'not installed'),
569
+ value: k,
570
+ preselect: installed,
571
+ };
572
+ });
573
+
574
+ // Multi-select using raw mode
575
+ const selected = await (async () => {
576
+ return new Promise((resolveSelected) => {
577
+ let cursor = 0;
578
+ const chosen = new Set(options.map((o, i) => o.preselect ? i : -1).filter(i => i >= 0));
579
+
580
+ const render = (first = false) => {
581
+ if (!first) write(`\x1b[${options.length + 2}A\x1b[J`);
582
+ write(`${c.bold(uiLang === 'zh' ? '选择 Agent:' : 'Select agents:')} ${c.dim(uiLang === 'zh' ? '(↑↓ 移动 空格 切换 A 全选 Enter 确认)' : '(↑↓ move Space toggle A all Enter confirm)')}\n`);
583
+ for (let i = 0; i < options.length; i++) {
584
+ const o = options[i];
585
+ const check = chosen.has(i) ? c.green('✔') : c.dim('○');
586
+ const pointer = i === cursor ? c.cyan('❯') : ' ';
587
+ const label = i === cursor ? (chosen.has(i) ? c.green(o.label) : c.cyan(o.label)) : (chosen.has(i) ? c.green(o.label) : o.label);
588
+ write(` ${pointer} ${check} ${label} ${c.dim('(' + o.hint + ')')}\n`);
589
+ }
590
+ write(c.dim(` ${chosen.size} ${uiLang === 'zh' ? '已选' : 'selected'}\n`));
591
+ };
592
+
593
+ write('\n');
594
+ render(true);
595
+
596
+ process.stdin.setRawMode(true);
597
+ process.stdin.resume();
598
+ process.stdin.setEncoding('utf8');
599
+
600
+ const onKey = (key) => {
601
+ if (key === '\x03') { cleanup(); process.exit(1); }
602
+ if (key === `${ESC}[A`) { cursor = (cursor - 1 + options.length) % options.length; render(); }
603
+ else if (key === `${ESC}[B`) { cursor = (cursor + 1) % options.length; render(); }
604
+ else if (key === ' ') {
605
+ if (chosen.has(cursor)) chosen.delete(cursor); else chosen.add(cursor);
606
+ render();
607
+ } else if (key === 'a' || key === 'A') {
608
+ if (chosen.size === options.length) chosen.clear();
609
+ else options.forEach((_, i) => chosen.add(i));
610
+ render();
611
+ } else if (key === '\r' || key === '\n') {
612
+ cleanup();
613
+ resolveSelected([...chosen].sort().map(i => options[i].value));
614
+ }
615
+ };
616
+
617
+ const cleanup = () => {
618
+ process.stdin.removeListener('data', onKey);
619
+ process.stdin.setRawMode(false);
620
+ process.stdin.pause();
621
+ };
622
+
623
+ process.stdin.on('data', onKey);
624
+ });
625
+ })();
626
+
627
+ if (selected.length === 0) {
628
+ write(c.dim(t('mcpSkipped') + '\n'));
629
+ return;
630
+ }
631
+
632
+ write('\n' + c.dim(tf('mcpInstalling', selected.length) + '\n'));
633
+
634
+ // stdio entry (same as mcp-install.js)
635
+ const entry = { type: 'stdio', command: 'mindos', args: ['mcp'], env: { MCP_TRANSPORT: 'stdio' } };
636
+ let okCount = 0;
637
+
638
+ for (const agentKey of selected) {
639
+ const agent = MCP_AGENTS_SETUP[agentKey];
640
+ // prefer global scope; fall back to project
641
+ const cfgPath = agent.global || agent.project;
642
+ if (!cfgPath) {
643
+ write(tf('mcpInstallFail', agent.name, 'no config path') + '\n');
644
+ continue;
645
+ }
646
+ const abs = expandHomePath(cfgPath);
647
+ try {
648
+ let config = {};
649
+ if (existsSync(abs)) config = JSON.parse(readFileSync(abs, 'utf-8'));
650
+ if (!config[agent.key]) config[agent.key] = {};
651
+ config[agent.key].mindos = entry;
652
+ const dir = resolve(abs, '..');
653
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
654
+ writeFileSync(abs, JSON.stringify(config, null, 2) + '\n', 'utf-8');
655
+ write(tf('mcpInstallOk', agent.name, cfgPath) + '\n');
656
+ okCount++;
657
+ } catch (err) {
658
+ write(tf('mcpInstallFail', agent.name, String(err)) + '\n');
659
+ }
660
+ }
661
+
662
+ console.log(`\n${c.green(tf('mcpInstallDone', okCount))}`);
663
+ }
664
+
506
665
  // ── GUI Setup ─────────────────────────────────────────────────────────────────
507
666
 
508
667
  function openBrowser(url) {
@@ -671,8 +830,13 @@ async function main() {
671
830
  // ── Step 3: Ports ─────────────────────────────────────────────────────────
672
831
  write('\n');
673
832
  stepHeader(3);
674
- const webPort = await askPort('webPortPrompt', 3000);
675
- const mcpPort = await askPort('mcpPortPrompt', 8787);
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
+ }
676
840
 
677
841
  // ── Step 4: Auth token ────────────────────────────────────────────────────
678
842
  write('\n');
@@ -792,6 +956,13 @@ async function main() {
792
956
  writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + '\n');
793
957
  console.log(`\n${c.green(t('cfgSaved'))}: ${c.dim(CONFIG_PATH)}`);
794
958
 
959
+ // ── Step 8: MCP Agent Install ──────────────────────────────────────────────
960
+ write('\n');
961
+ stepHeader(8);
962
+ write(c.dim(tf('mcpStepHint') + '\n\n'));
963
+
964
+ await runMcpInstallStep(mcpPort, authToken);
965
+
795
966
  // ── Sync setup (optional) ──────────────────────────────────────────────────
796
967
  const wantSync = await askYesNo('syncSetup');
797
968
  if (wantSync) {