@geminilight/mindos 0.5.8 → 0.5.9

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 (34) hide show
  1. package/README.md +8 -9
  2. package/README_zh.md +8 -9
  3. package/app/app/api/mcp/agents/route.ts +7 -0
  4. package/app/app/api/mcp/install-skill/route.ts +6 -0
  5. package/app/app/api/setup/check-port/route.ts +27 -3
  6. package/app/app/api/setup/route.ts +2 -9
  7. package/app/app/globals.css +18 -2
  8. package/app/app/login/page.tsx +1 -1
  9. package/app/app/view/[...path]/ViewPageClient.tsx +9 -9
  10. package/app/components/AskModal.tsx +1 -1
  11. package/app/components/FileTree.tsx +5 -5
  12. package/app/components/HomeContent.tsx +1 -1
  13. package/app/components/SetupWizard.tsx +283 -141
  14. package/app/components/SyncStatusBar.tsx +3 -3
  15. package/app/components/ask/MessageList.tsx +2 -2
  16. package/app/components/ask/SessionHistory.tsx +1 -1
  17. package/app/components/renderers/agent-inspector/AgentInspectorRenderer.tsx +5 -5
  18. package/app/components/renderers/config/ConfigRenderer.tsx +3 -3
  19. package/app/components/renderers/csv/types.ts +1 -1
  20. package/app/components/renderers/diff/DiffRenderer.tsx +9 -9
  21. package/app/components/renderers/timeline/TimelineRenderer.tsx +1 -1
  22. package/app/components/renderers/workflow/WorkflowRenderer.tsx +2 -2
  23. package/app/components/settings/McpTab.tsx +66 -24
  24. package/app/components/settings/Primitives.tsx +3 -3
  25. package/app/components/settings/SyncTab.tsx +5 -5
  26. package/app/lib/i18n.ts +48 -4
  27. package/app/lib/mcp-agents.ts +81 -0
  28. package/bin/lib/gateway.js +44 -4
  29. package/bin/lib/mcp-agents.js +81 -0
  30. package/bin/lib/mcp-install.js +34 -4
  31. package/package.json +3 -1
  32. package/scripts/setup.js +43 -6
  33. package/app/public/landing/index.html +0 -353
  34. package/app/public/landing/style.css +0 -216
@@ -93,6 +93,87 @@ export const MCP_AGENTS: Record<string, AgentDef> = {
93
93
  presenceCli: 'claude-internal',
94
94
  presenceDirs: ['~/.claude-internal/'],
95
95
  },
96
+ 'iflow-cli': {
97
+ name: 'iFlow CLI',
98
+ project: '.iflow/settings.json',
99
+ global: '~/.iflow/settings.json',
100
+ key: 'mcpServers',
101
+ preferredTransport: 'stdio',
102
+ presenceCli: 'iflow',
103
+ presenceDirs: ['~/.iflow/'],
104
+ },
105
+ 'kimi-cli': {
106
+ name: 'Kimi Code',
107
+ project: '.kimi/mcp.json',
108
+ global: '~/.kimi/mcp.json',
109
+ key: 'mcpServers',
110
+ preferredTransport: 'stdio',
111
+ presenceCli: 'kimi',
112
+ presenceDirs: ['~/.kimi/'],
113
+ },
114
+ 'opencode': {
115
+ name: 'OpenCode',
116
+ project: null,
117
+ global: '~/.config/opencode/config.json',
118
+ key: 'mcpServers',
119
+ preferredTransport: 'stdio',
120
+ presenceCli: 'opencode',
121
+ presenceDirs: ['~/.config/opencode/'],
122
+ },
123
+ 'pi': {
124
+ name: 'Pi',
125
+ project: '.pi/settings.json',
126
+ global: '~/.pi/agent/mcp.json',
127
+ key: 'mcpServers',
128
+ preferredTransport: 'stdio',
129
+ presenceCli: 'pi',
130
+ presenceDirs: ['~/.pi/'],
131
+ },
132
+ 'augment': {
133
+ name: 'Augment',
134
+ project: '.augment/settings.json',
135
+ global: '~/.augment/settings.json',
136
+ key: 'mcpServers',
137
+ preferredTransport: 'stdio',
138
+ presenceCli: 'auggie',
139
+ presenceDirs: ['~/.augment/'],
140
+ },
141
+ 'qwen-code': {
142
+ name: 'Qwen Code',
143
+ project: '.qwen/settings.json',
144
+ global: '~/.qwen/settings.json',
145
+ key: 'mcpServers',
146
+ preferredTransport: 'stdio',
147
+ presenceCli: 'qwen',
148
+ presenceDirs: ['~/.qwen/'],
149
+ },
150
+ 'trae-cn': {
151
+ name: 'Trae CN',
152
+ project: '.trae/mcp.json',
153
+ global: process.platform === 'darwin'
154
+ ? '~/Library/Application Support/Trae CN/User/mcp.json'
155
+ : '~/.config/Trae CN/User/mcp.json',
156
+ key: 'mcpServers',
157
+ preferredTransport: 'stdio',
158
+ presenceCli: 'trae-cli',
159
+ presenceDirs: [
160
+ '~/Library/Application Support/Trae CN/',
161
+ '~/.config/Trae CN/',
162
+ ],
163
+ },
164
+ 'roo': {
165
+ name: 'Roo Code',
166
+ project: '.roo/mcp.json',
167
+ global: process.platform === 'darwin'
168
+ ? '~/Library/Application Support/Code/User/globalStorage/rooveterinaryinc.roo-cline/settings/mcp_settings.json'
169
+ : '~/.config/Code/User/globalStorage/rooveterinaryinc.roo-cline/settings/mcp_settings.json',
170
+ key: 'mcpServers',
171
+ preferredTransport: 'stdio',
172
+ presenceDirs: [
173
+ '~/Library/Application Support/Code/User/globalStorage/rooveterinaryinc.roo-cline/',
174
+ '~/.config/Code/User/globalStorage/rooveterinaryinc.roo-cline/',
175
+ ],
176
+ },
96
177
  };
97
178
 
98
179
  /* ── MindOS MCP Install Detection ──────────────────────────────────────── */
@@ -1,9 +1,10 @@
1
1
  import { execSync } from 'node:child_process';
2
- import { existsSync, writeFileSync, rmSync, mkdirSync } from 'node:fs';
2
+ import { existsSync, readFileSync, writeFileSync, rmSync, mkdirSync } from 'node:fs';
3
3
  import { resolve } from 'node:path';
4
4
  import { homedir } from 'node:os';
5
- import { MINDOS_DIR, LOG_PATH, CLI_PATH, NODE_BIN } from './constants.js';
6
- import { green, red, dim, cyan } from './colors.js';
5
+ import { MINDOS_DIR, LOG_PATH, CLI_PATH, NODE_BIN, CONFIG_PATH } from './constants.js';
6
+ import { green, red, dim, cyan, yellow } from './colors.js';
7
+ import { isPortInUse } from './port.js';
7
8
 
8
9
  // ── Helpers ──────────────────────────────────────────────────────────────────
9
10
 
@@ -25,6 +26,18 @@ export async function waitForService(check, { retries = 10, intervalMs = 1000 }
25
26
  return check();
26
27
  }
27
28
 
29
+ /**
30
+ * Wait until a port is free (no process listening).
31
+ * Returns true if port is free, false on timeout.
32
+ */
33
+ export async function waitForPortFree(port, { retries = 30, intervalMs = 500 } = {}) {
34
+ for (let i = 0; i < retries; i++) {
35
+ if (!(await isPortInUse(port))) return true;
36
+ await new Promise(r => setTimeout(r, intervalMs));
37
+ }
38
+ return !(await isPortInUse(port));
39
+ }
40
+
28
41
  export async function waitForHttp(port, { retries = 120, intervalMs = 2000, label = 'service' } = {}) {
29
42
  process.stdout.write(cyan(` Waiting for ${label} to be ready`));
30
43
  for (let i = 0; i < retries; i++) {
@@ -194,10 +207,37 @@ const launchd = {
194
207
  console.log(green('\u2714 Service started'));
195
208
  },
196
209
 
197
- stop() {
210
+ async stop() {
211
+ // Read ports before bootout so we can wait for them to be freed
212
+ let webPort = 3000, mcpPort = 8787;
213
+ try {
214
+ const config = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8'));
215
+ if (config.port) webPort = Number(config.port);
216
+ if (config.mcpPort) mcpPort = Number(config.mcpPort);
217
+ } catch {}
218
+
198
219
  try {
199
220
  execSync(`launchctl bootout gui/${launchctlUid()} ${LAUNCHD_PLIST}`, { stdio: 'inherit' });
200
221
  } catch { /* may not be running */ }
222
+
223
+ // launchctl bootout is async — wait for ports to actually be freed
224
+ let [webFree, mcpFree] = await Promise.all([
225
+ waitForPortFree(webPort),
226
+ waitForPortFree(mcpPort),
227
+ ]);
228
+ if (!webFree || !mcpFree) {
229
+ console.log(yellow('Ports still in use after bootout, force-killing...'));
230
+ const { stopMindos } = await import('./stop.js');
231
+ stopMindos();
232
+ // stopMindos() sends SIGTERM synchronously — wait for processes to exit
233
+ [webFree, mcpFree] = await Promise.all([
234
+ waitForPortFree(webPort),
235
+ waitForPortFree(mcpPort),
236
+ ]);
237
+ if (!webFree || !mcpFree) {
238
+ console.error(red('Warning: ports still in use after force-kill. Continuing anyway.'));
239
+ }
240
+ }
201
241
  console.log(green('\u2714 Service stopped'));
202
242
  },
203
243
 
@@ -100,6 +100,87 @@ export const MCP_AGENTS = {
100
100
  presenceCli: 'claude-internal',
101
101
  presenceDirs: ['~/.claude-internal/'],
102
102
  },
103
+ 'iflow-cli': {
104
+ name: 'iFlow CLI',
105
+ project: '.iflow/settings.json',
106
+ global: '~/.iflow/settings.json',
107
+ key: 'mcpServers',
108
+ preferredTransport: 'stdio',
109
+ presenceCli: 'iflow',
110
+ presenceDirs: ['~/.iflow/'],
111
+ },
112
+ 'kimi-cli': {
113
+ name: 'Kimi Code',
114
+ project: '.kimi/mcp.json',
115
+ global: '~/.kimi/mcp.json',
116
+ key: 'mcpServers',
117
+ preferredTransport: 'stdio',
118
+ presenceCli: 'kimi',
119
+ presenceDirs: ['~/.kimi/'],
120
+ },
121
+ 'opencode': {
122
+ name: 'OpenCode',
123
+ project: null,
124
+ global: '~/.config/opencode/config.json',
125
+ key: 'mcpServers',
126
+ preferredTransport: 'stdio',
127
+ presenceCli: 'opencode',
128
+ presenceDirs: ['~/.config/opencode/'],
129
+ },
130
+ 'pi': {
131
+ name: 'Pi',
132
+ project: '.pi/settings.json',
133
+ global: '~/.pi/agent/mcp.json',
134
+ key: 'mcpServers',
135
+ preferredTransport: 'stdio',
136
+ presenceCli: 'pi',
137
+ presenceDirs: ['~/.pi/'],
138
+ },
139
+ 'augment': {
140
+ name: 'Augment',
141
+ project: '.augment/settings.json',
142
+ global: '~/.augment/settings.json',
143
+ key: 'mcpServers',
144
+ preferredTransport: 'stdio',
145
+ presenceCli: 'auggie',
146
+ presenceDirs: ['~/.augment/'],
147
+ },
148
+ 'qwen-code': {
149
+ name: 'Qwen Code',
150
+ project: '.qwen/settings.json',
151
+ global: '~/.qwen/settings.json',
152
+ key: 'mcpServers',
153
+ preferredTransport: 'stdio',
154
+ presenceCli: 'qwen',
155
+ presenceDirs: ['~/.qwen/'],
156
+ },
157
+ 'trae-cn': {
158
+ name: 'Trae CN',
159
+ project: '.trae/mcp.json',
160
+ global: process.platform === 'darwin'
161
+ ? '~/Library/Application Support/Trae CN/User/mcp.json'
162
+ : '~/.config/Trae CN/User/mcp.json',
163
+ key: 'mcpServers',
164
+ preferredTransport: 'stdio',
165
+ presenceCli: 'trae-cli',
166
+ presenceDirs: [
167
+ '~/Library/Application Support/Trae CN/',
168
+ '~/.config/Trae CN/',
169
+ ],
170
+ },
171
+ 'roo': {
172
+ name: 'Roo Code',
173
+ project: '.roo/mcp.json',
174
+ global: process.platform === 'darwin'
175
+ ? '~/Library/Application Support/Code/User/globalStorage/rooveterinaryinc.roo-cline/settings/mcp_settings.json'
176
+ : '~/.config/Code/User/globalStorage/rooveterinaryinc.roo-cline/settings/mcp_settings.json',
177
+ key: 'mcpServers',
178
+ preferredTransport: 'stdio',
179
+ presenceDirs: [
180
+ '~/Library/Application Support/Code/User/globalStorage/rooveterinaryinc.roo-cline/',
181
+ '~/.config/Code/User/globalStorage/rooveterinaryinc.roo-cline/',
182
+ ],
183
+ },
103
184
  };
104
185
 
105
186
  export function detectAgentPresence(agentKey) {
@@ -3,7 +3,7 @@ import { resolve } from 'node:path';
3
3
  import { CONFIG_PATH } from './constants.js';
4
4
  import { bold, dim, cyan, green, red, yellow } from './colors.js';
5
5
  import { expandHome } from './utils.js';
6
- import { MCP_AGENTS } from './mcp-agents.js';
6
+ import { MCP_AGENTS, detectAgentPresence } from './mcp-agents.js';
7
7
 
8
8
  export { MCP_AGENTS };
9
9
 
@@ -76,7 +76,7 @@ async function interactiveSelect(title, options) {
76
76
  async function interactiveMultiSelect(title, options) {
77
77
  return new Promise((resolve) => {
78
78
  let cursor = 0;
79
- const selected = new Set();
79
+ const selected = new Set(options.map((o, i) => o.preselect ? i : -1).filter(i => i >= 0));
80
80
  const { stdin, stdout } = process;
81
81
 
82
82
  function render() {
@@ -85,7 +85,7 @@ async function interactiveMultiSelect(title, options) {
85
85
  }
86
86
 
87
87
  function draw() {
88
- stdout.write(`${bold(title)} ${dim('(↑↓ move, Space select, A all, Enter confirm)')}\n`);
88
+ stdout.write(`${bold(title)} ${dim('(↑↓ move, Space select, D detected, A all, Enter confirm)')}\n`);
89
89
  for (let i = 0; i < options.length; i++) {
90
90
  const o = options[i];
91
91
  const check = selected.has(i) ? green('✔') : dim('○');
@@ -120,6 +120,10 @@ async function interactiveMultiSelect(title, options) {
120
120
  if (selected.size === options.length) selected.clear();
121
121
  else options.forEach((_, i) => selected.add(i));
122
122
  render();
123
+ } else if (key === 'd' || key === 'D') { // select detected only
124
+ selected.clear();
125
+ options.forEach((o, i) => { if (o.preselect) selected.add(i); });
126
+ render();
123
127
  } else if (key === '\r' || key === '\n') { // enter
124
128
  cleanup();
125
129
  const result = [...selected].sort().map(i => options[i]);
@@ -178,9 +182,35 @@ export async function mcpInstall() {
178
182
  agentKeys = keys;
179
183
  } else {
180
184
  rl.close(); // close readline so raw mode works
185
+
186
+ // Build options with detected status and preselect
187
+ const agentOptions = keys.map(k => {
188
+ const agent = MCP_AGENTS[k];
189
+ const present = detectAgentPresence(k);
190
+ // Check if already configured
191
+ let installed = false;
192
+ for (const cfgPath of [agent.global, agent.project]) {
193
+ if (!cfgPath) continue;
194
+ const abs = expandHome(cfgPath);
195
+ if (!existsSync(abs)) continue;
196
+ try {
197
+ const config = JSON.parse(readFileSync(abs, 'utf-8'));
198
+ if (config[agent.key]?.mindos) { installed = true; break; }
199
+ } catch {}
200
+ }
201
+ const hint = installed ? 'configured' : present ? 'detected' : 'not found';
202
+ return { label: agent.name, hint, value: k, preselect: installed || present };
203
+ });
204
+
205
+ // Sort: configured > detected > not found
206
+ agentOptions.sort((a, b) => {
207
+ const rank = (o) => o.hint === 'configured' ? 0 : o.preselect ? 1 : 2;
208
+ return rank(a) - rank(b);
209
+ });
210
+
181
211
  const picked = await interactiveMultiSelect(
182
212
  'Which Agents to configure?',
183
- keys.map(k => ({ label: MCP_AGENTS[k].name, hint: k, value: k })),
213
+ agentOptions,
184
214
  );
185
215
  if (picked.length === 0) {
186
216
  console.log(dim('\nNo agents selected. Exiting.\n'));
package/package.json CHANGED
@@ -1,13 +1,15 @@
1
1
  {
2
2
  "name": "@geminilight/mindos",
3
- "version": "0.5.8",
3
+ "version": "0.5.9",
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",
7
7
  "mcp",
8
8
  "knowledge-base",
9
+ "knowledge-management",
9
10
  "ai-agent",
10
11
  "local-first",
12
+ "markdown",
11
13
  "second-brain"
12
14
  ],
13
15
  "type": "module",
package/scripts/setup.js CHANGED
@@ -429,7 +429,8 @@ function isPortInUse(port) {
429
429
  return new Promise((resolve) => {
430
430
  const sock = createConnection({ port, host: '127.0.0.1' });
431
431
  const cleanup = (result) => { sock.destroy(); resolve(result); };
432
- sock.setTimeout(500, () => cleanup(true));
432
+ // On localhost, timeout means no response — treat as free (same as bin/lib/port.js)
433
+ sock.setTimeout(500, () => cleanup(false));
433
434
  sock.once('connect', () => cleanup(true));
434
435
  sock.once('error', (err) => {
435
436
  // ECONNREFUSED = nothing listening → free; other errors = treat as in-use
@@ -628,6 +629,12 @@ async function runMcpInstallStep(mcpPort, authToken) {
628
629
  };
629
630
  });
630
631
 
632
+ // Sort: configured > detected > not found (stable within each group)
633
+ options.sort((a, b) => {
634
+ const rank = (o) => o.hint.includes('configured') || o.hint.includes('已配置') ? 0 : o.preselect ? 1 : 2;
635
+ return rank(a) - rank(b);
636
+ });
637
+
631
638
  // Multi-select using raw mode
632
639
  const selected = await (async () => {
633
640
  return new Promise((resolveSelected) => {
@@ -636,7 +643,7 @@ async function runMcpInstallStep(mcpPort, authToken) {
636
643
 
637
644
  const render = (first = false) => {
638
645
  if (!first) write(`\x1b[${options.length + 2}A\x1b[J`);
639
- write(`${c.bold(uiLang === 'zh' ? '选择 Agent:' : 'Select agents:')} ${c.dim(uiLang === 'zh' ? '(↑↓ 移动 空格 切换 A 全选 Enter 确认)' : '(↑↓ move Space toggle A all Enter confirm)')}\n`);
646
+ write(`${c.bold(uiLang === 'zh' ? '选择 Agent:' : 'Select agents:')} ${c.dim(uiLang === 'zh' ? '(↑↓ 移动 空格 切换 D 已检测 A 全选 Enter 确认)' : '(↑↓ move Space toggle D detected A all Enter confirm)')}\n`);
640
647
  for (let i = 0; i < options.length; i++) {
641
648
  const o = options[i];
642
649
  const check = chosen.has(i) ? c.green('✔') : c.dim('○');
@@ -665,6 +672,11 @@ async function runMcpInstallStep(mcpPort, authToken) {
665
672
  if (chosen.size === options.length) chosen.clear();
666
673
  else options.forEach((_, i) => chosen.add(i));
667
674
  render();
675
+ } else if (key === 'd' || key === 'D') {
676
+ // Select only detected/configured agents
677
+ chosen.clear();
678
+ options.forEach((o, i) => { if (o.preselect) chosen.add(i); });
679
+ render();
668
680
  } else if (key === '\r' || key === '\n') {
669
681
  cleanup();
670
682
  resolveSelected([...chosen].sort().map(i => options[i].value));
@@ -733,6 +745,12 @@ const AGENT_NAME_MAP = {
733
745
  'trae': 'trae',
734
746
  'openclaw': 'openclaw',
735
747
  'codebuddy': 'codebuddy',
748
+ 'iflow-cli': 'iflow-cli',
749
+ 'pi': 'pi',
750
+ 'augment': 'augment',
751
+ 'qwen-code': 'qwen-code',
752
+ 'trae-cn': 'trae-cn',
753
+ 'roo': 'roo',
736
754
  };
737
755
 
738
756
  /**
@@ -840,19 +858,38 @@ async function startGuiSetup() {
840
858
  process.exit(0);
841
859
  }
842
860
  // Service not running — start on existing port
843
- usePort = await isPortInUse(existingPort)
844
- ? await findFreePort(9100) // existing port occupied by another process
845
- : existingPort;
861
+ if (await isPortInUse(existingPort)) {
862
+ // Port occupied try stopping leftover MindOS processes first
863
+ try {
864
+ const { stopMindos } = await import('../bin/lib/stop.js');
865
+ stopMindos();
866
+ // stopMindos() sends SIGTERM synchronously — wait for both web and mcp
867
+ // ports to free, since `start` will assertPortFree on both.
868
+ const { waitForPortFree } = await import('../bin/lib/gateway.js');
869
+ const mcpPort = config.mcpPort || 8787;
870
+ const [webFreed, mcpFreed] = await Promise.all([
871
+ waitForPortFree(existingPort),
872
+ waitForPortFree(mcpPort),
873
+ ]);
874
+ usePort = webFreed ? existingPort : await findFreePort(9100);
875
+ } catch {
876
+ usePort = await findFreePort(9100);
877
+ }
878
+ } else {
879
+ usePort = existingPort;
880
+ }
846
881
  }
847
882
 
848
883
  write(c.yellow(t('guiStarting') + '\n'));
849
884
 
850
885
  // Start the server in the background
886
+ // Pass MINDOS_WEB_PORT (not PORT) so loadConfig() won't override with the
887
+ // config file port — this is critical when we need a temporary port.
851
888
  const cliPath = resolve(__dirname, '../bin/cli.js');
852
889
  const child = spawn(process.execPath, [cliPath, 'start'], {
853
890
  detached: true,
854
891
  stdio: 'ignore',
855
- env: { ...process.env, PORT: String(usePort) },
892
+ env: { ...process.env, MINDOS_WEB_PORT: String(usePort) },
856
893
  });
857
894
  child.unref();
858
895