@geminilight/mindos 0.5.6 → 0.5.8

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 (49) hide show
  1. package/README.md +10 -8
  2. package/README_zh.md +7 -5
  3. package/app/app/api/mcp/agents/route.ts +3 -1
  4. package/app/app/api/mcp/install-skill/route.ts +1 -1
  5. package/app/app/api/restart/route.ts +28 -4
  6. package/app/components/SetupWizard.tsx +12 -5
  7. package/app/components/settings/McpTab.tsx +4 -1
  8. package/app/lib/i18n.ts +8 -0
  9. package/app/lib/mcp-agents.ts +100 -9
  10. package/app/proxy.ts +3 -2
  11. package/assets/images/wechat-qr.png +0 -0
  12. package/bin/cli.js +30 -7
  13. package/bin/lib/build.js +25 -0
  14. package/bin/lib/mcp-agents.js +112 -9
  15. package/bin/lib/stop.js +15 -2
  16. package/package.json +11 -1
  17. package/scripts/setup.js +5 -4
  18. package/skills/project-wiki/SKILL.md +223 -0
  19. package/skills/project-wiki/assets/api-reference.tmpl.md +49 -0
  20. package/skills/project-wiki/assets/backlog.tmpl.md +15 -0
  21. package/skills/project-wiki/assets/changelog.tmpl.md +16 -0
  22. package/skills/project-wiki/assets/conventions.tmpl.md +29 -0
  23. package/skills/project-wiki/assets/design-exploration.tmpl.md +26 -0
  24. package/skills/project-wiki/assets/design-principle.tmpl.md +48 -0
  25. package/skills/project-wiki/assets/development-guide.tmpl.md +38 -0
  26. package/skills/project-wiki/assets/glossary.tmpl.md +9 -0
  27. package/skills/project-wiki/assets/known-pitfalls.tmpl.md +21 -0
  28. package/skills/project-wiki/assets/postmortem.tmpl.md +38 -0
  29. package/skills/project-wiki/assets/product-proposal.tmpl.md +41 -0
  30. package/skills/project-wiki/assets/project-roadmap.tmpl.md +23 -0
  31. package/skills/project-wiki/assets/stage-x.tmpl.md +78 -0
  32. package/skills/project-wiki/assets/system-architecture.tmpl.md +62 -0
  33. package/skills/project-wiki/references/file-reference.md +254 -0
  34. package/skills/project-wiki/references/writing-guide.md +28 -0
  35. package/app/data/pages/home-dark.png +0 -0
  36. package/app/data/pages/home-mobile-crop.png +0 -0
  37. package/app/data/pages/home-mobile.png +0 -0
  38. package/app/data/pages/home.png +0 -0
  39. package/app/data/pages/view-dir.png +0 -0
  40. package/app/data/pages/view-file-bot.png +0 -0
  41. package/app/data/pages/view-file-dark-crop.png +0 -0
  42. package/app/data/pages/view-file-dark.png +0 -0
  43. package/app/data/pages/view-file-mobile.png +0 -0
  44. package/app/data/pages/view-file-sm.png +0 -0
  45. package/app/data/pages/view-file-top.png +0 -0
  46. package/app/data/pages/view-file.png +0 -0
  47. package/app/eslint.config.mjs +0 -18
  48. package/app/vitest.config.ts +0 -14
  49. package/assets/demo-flow-zh.html +0 -622
package/README.md CHANGED
@@ -48,21 +48,23 @@ MindOS is a **Human-AI Collaborative Mind System**—a local-first knowledge bas
48
48
  > Help me execute the XXX SOP from MindOS.
49
49
  > ```
50
50
 
51
- ## 🧠 Core Value: Human-AI Shared Mind
51
+ ## 🧠 Human-AI Shared Mind
52
52
 
53
- **1. Global Sync Break Memory Silos**
53
+ > No more fragmented memory, no more black-box behavior, no more lost experience.
54
54
 
55
- Traditional notes are scattered across tools and APIs, so agents miss your real context when it matters. MindOS turns your local knowledge into one MCP-ready source, so every agent can sync your Profile, SOPs, and live working memory.
55
+ **1. Global Sync Breaking Memory Silos**
56
56
 
57
- **2. Transparent and ControllableEliminate Memory Black Boxes**
57
+ Each Agent keeps its own memory — switching tools means manually hauling context. **MindOS lets all Agents share one knowledge base via MCP and Skillsrecord once, reuse everywhere.**
58
58
 
59
- Most assistant memory lives in black boxes, leaving humans unable to inspect or correct how decisions are made. MindOS writes retrieval and execution traces into local plain text, so you can audit, intervene, and improve continuously.
59
+ **2. Transparent & Controllable No More Black Boxes**
60
+
61
+ What did your Agent remember? Is it even correct? You have no way to know. **MindOS saves every read/write as local plain text — humans can audit, correct, and delete in the GUI.**
60
62
 
61
63
  **3. Symbiotic Evolution — Experience Flows Back as Instructions**
62
64
 
63
- Static documents are hard to synchronize and weak as execution systems in real human-agent collaboration. MindOS makes notes agent-ready and reference-linked, so daily writing naturally becomes executable workflows that evolve with you.
65
+ All that experience from your conversations gone the moment you close the window. **MindOS auto-distills conversation experience into Skills/SOPs. Notes are instructions. The knowledge base gets better with use.**
64
66
 
65
- > **Foundation:** Local-first by default - all data stays in local plain text for privacy, ownership, and speed.
67
+ > **Foundation:** Local-first by default all data stays in local plain text for privacy, ownership, and speed.
66
68
 
67
69
  ## ✨ Features
68
70
 
@@ -80,7 +82,7 @@ Static documents are hard to synchronize and weak as execution systems in real h
80
82
 
81
83
  **Infrastructure**
82
84
 
83
- - **Reference Sync**: keep cross-file status and context aligned via links/backlinks.
85
+ - **Security**: Bearer Token auth, path sandboxing, INSTRUCTION.md write-protection, atomic writes.
84
86
  - **Knowledge Graph**: visualize relationships and dependencies across notes.
85
87
  - **Git Time Machine**: track every edit, audit history, and roll back safely.
86
88
  - **Cross-Device Sync**: auto-commit, push, and pull via Git — edits on one device appear on all others within minutes.
package/README_zh.md CHANGED
@@ -48,19 +48,21 @@ MindOS 是一个**人机协同心智系统**——基于本地优先的协作知
48
48
  > 帮我执行 MindOS 里的 XXX 工作流。
49
49
  > ```
50
50
 
51
- ## 🧠 核心价值:人机共享心智
51
+ ## 🧠 人机共享心智
52
+
53
+ > 记忆不再割裂,行为不再黑箱,经验不再断流。
52
54
 
53
55
  **1. 全局同步 — 打破记忆割裂**
54
56
 
55
- 传统笔记分散在不同工具和接口中,Agent 在关键时刻拿不到你的真实上下文。MindOS 把本地知识统一为 MCP 可读的单一来源,让所有 Agent 同步你的 Profile、SOP 与实时记忆。
57
+ 多个 Agent 各记各的,切换工具靠人工搬运上下文。**MindOS 通过 MCP Skill 让所有 Agent 共享同一份知识库——一处记录,全局复用。**
56
58
 
57
59
  **2. 透明可控 — 消除记忆黑箱**
58
60
 
59
- 多数助手记忆封闭在黑箱里,人类难以审查和纠正决策过程。MindOS 将检索与执行轨迹沉淀为本地纯文本,让你可以持续审计、干预与优化。
61
+ Agent 记了什么、记对没有,用户无从知晓。**MindOS 将每次读写沉淀为本地纯文本,人类可在 GUI 中审查、修正、删除。**
60
62
 
61
63
  **3. 共生演进 — 经验回流为指令**
62
64
 
63
- 静态文档难同步,也难在真实人机协作中承担执行系统角色。MindOS 让笔记天然成为 Agent 可执行的指令,通过引用链接组织知识,日常记录自然变成可执行工作流并持续进化。
65
+ 对话里攒下的经验,关掉窗口就散了。**MindOS 自动将对话经验沉淀为 Skill/SOP,笔记即指令,知识库越用越好。**
64
66
 
65
67
  > **底层原则:** 默认本地优先,全部数据以本地纯文本保存,兼顾隐私、主权与性能。
66
68
 
@@ -80,7 +82,7 @@ MindOS 是一个**人机协同心智系统**——基于本地优先的协作知
80
82
 
81
83
  **基础设施**
82
84
 
83
- - **引用同步**:通过引用与反向链接保持跨文件状态一致。
85
+ - **安全防线**:Bearer Token 认证、路径沙箱、INSTRUCTION.md 写保护、原子写入。
84
86
  - **知识图谱**:可视化笔记间关系与依赖。
85
87
  - **Git 时光机**:记录修改历史,支持审计与安全回滚。
86
88
  - **跨设备同步**:通过 Git 自动 commit、push、pull —— 一台设备的编辑几分钟内同步到所有设备。
@@ -1,14 +1,16 @@
1
1
  export const dynamic = 'force-dynamic';
2
2
  import { NextResponse } from 'next/server';
3
- import { MCP_AGENTS, detectInstalled } from '@/lib/mcp-agents';
3
+ import { MCP_AGENTS, detectInstalled, detectAgentPresence } from '@/lib/mcp-agents';
4
4
 
5
5
  export async function GET() {
6
6
  try {
7
7
  const agents = Object.entries(MCP_AGENTS).map(([key, agent]) => {
8
8
  const status = detectInstalled(key);
9
+ const present = detectAgentPresence(key);
9
10
  return {
10
11
  key,
11
12
  name: agent.name,
13
+ present,
12
14
  installed: status.installed,
13
15
  scope: status.scope,
14
16
  transport: status.transport,
@@ -15,7 +15,7 @@ const UNIVERSAL_AGENTS = new Set([
15
15
  ]);
16
16
 
17
17
  // Agents that do NOT support Skills at all
18
- const SKILL_UNSUPPORTED = new Set(['claude-desktop']);
18
+ const SKILL_UNSUPPORTED = new Set<string>([]);
19
19
 
20
20
  // MCP agent key → npx skills agent name (for non-universal agents)
21
21
  const AGENT_NAME_MAP: Record<string, string> = {
@@ -7,14 +7,38 @@ export async function POST() {
7
7
  try {
8
8
  // process.cwd() is the Next.js app directory; cli.js is one level up at project root/bin/
9
9
  const cliPath = resolve(process.cwd(), '../bin/cli.js');
10
- const child = spawn(process.execPath, [cliPath, 'start'], {
10
+ // Use 'restart' (stop all → wait for ports free → start) instead of bare
11
+ // 'start' which would fail assertPortFree because the current process and
12
+ // its MCP child are still holding the ports.
13
+ //
14
+ // IMPORTANT: Strip MINDOS_* env vars so the child's loadConfig() reads
15
+ // the *updated* config file instead of inheriting stale values from this
16
+ // process. Without this, changing ports in the GUI has no effect on the
17
+ // restarted server — it would start on the old ports.
18
+ //
19
+ // Pass the current (old) ports via MINDOS_OLD_* so the restart command
20
+ // can clean up processes still listening on the previous ports.
21
+ const childEnv = { ...process.env };
22
+ const oldWebPort = childEnv.MINDOS_WEB_PORT;
23
+ const oldMcpPort = childEnv.MINDOS_MCP_PORT;
24
+ delete childEnv.MINDOS_WEB_PORT;
25
+ delete childEnv.MINDOS_MCP_PORT;
26
+ delete childEnv.MIND_ROOT;
27
+ delete childEnv.AUTH_TOKEN;
28
+ delete childEnv.WEB_PASSWORD;
29
+ if (oldWebPort) childEnv.MINDOS_OLD_WEB_PORT = oldWebPort;
30
+ if (oldMcpPort) childEnv.MINDOS_OLD_MCP_PORT = oldMcpPort;
31
+ const child = spawn(process.execPath, [cliPath, 'restart'], {
11
32
  detached: true,
12
33
  stdio: 'ignore',
13
- env: process.env,
34
+ env: childEnv,
14
35
  });
15
36
  child.unref();
16
- // Give a brief moment for the response to be sent before exiting
17
- setTimeout(() => process.exit(0), 500);
37
+ // Give a brief moment for the response to be sent before exiting.
38
+ // The spawned 'restart' command will handle stopping this process via
39
+ // stopMindos() (kill by PID + port cleanup), so process.exit here is
40
+ // just a safety net in case the parent isn't killed cleanly.
41
+ setTimeout(() => process.exit(0), 1500);
18
42
  return NextResponse.json({ ok: true });
19
43
  } catch (err) {
20
44
  return NextResponse.json({ error: String(err) }, { status: 500 });
@@ -36,6 +36,7 @@ interface PortStatus {
36
36
  interface AgentEntry {
37
37
  key: string;
38
38
  name: string;
39
+ present: boolean;
39
40
  installed: boolean;
40
41
  hasProjectScope: boolean;
41
42
  hasGlobalScope: boolean;
@@ -511,7 +512,7 @@ function Step5({
511
512
  return agentTransport;
512
513
  };
513
514
 
514
- const getStatusBadge = (key: string, installed: boolean) => {
515
+ const getStatusBadge = (key: string, agent: AgentEntry) => {
515
516
  const st = agentStatuses[key];
516
517
  if (st) {
517
518
  if (st.state === 'installing') return (
@@ -533,16 +534,22 @@ function Step5({
533
534
  </span>
534
535
  );
535
536
  }
536
- if (installed) return (
537
+ if (agent.installed) return (
537
538
  <span className="text-[11px] px-1.5 py-0.5 rounded"
538
539
  style={{ background: 'rgba(34,197,94,0.12)', color: '#22c55e' }}>
539
540
  {settingsMcp.installed}
540
541
  </span>
541
542
  );
543
+ if (agent.present) return (
544
+ <span className="text-[11px] px-1.5 py-0.5 rounded"
545
+ style={{ background: 'rgba(245,158,11,0.12)', color: '#f59e0b' }}>
546
+ {s.agentDetected ?? 'detected'}
547
+ </span>
548
+ );
542
549
  return (
543
550
  <span className="text-[11px] px-1.5 py-0.5 rounded"
544
551
  style={{ background: 'rgba(100,100,120,0.1)', color: 'var(--muted-foreground)' }}>
545
- {s.agentNotInstalled}
552
+ {s.agentNotFound ?? s.agentNotInstalled}
546
553
  </span>
547
554
  );
548
555
  };
@@ -581,7 +588,7 @@ function Step5({
581
588
  style={{ background: 'rgba(100,100,120,0.08)', color: 'var(--muted-foreground)' }}>
582
589
  {getEffectiveTransport(agent)}
583
590
  </span>
584
- {getStatusBadge(agent.key, agent.installed)}
591
+ {getStatusBadge(agent.key, agent)}
585
592
  </label>
586
593
  ))}
587
594
  </div>
@@ -936,7 +943,7 @@ export default function SetupWizard() {
936
943
  if (data.agents) {
937
944
  setAgents(data.agents);
938
945
  setSelectedAgents(new Set(
939
- (data.agents as AgentEntry[]).filter(a => a.installed).map(a => a.key)
946
+ (data.agents as AgentEntry[]).filter(a => a.installed || a.present).map(a => a.key)
940
947
  ));
941
948
  }
942
949
  })
@@ -22,6 +22,7 @@ interface McpStatus {
22
22
  interface AgentInfo {
23
23
  key: string;
24
24
  name: string;
25
+ present: boolean;
25
26
  installed: boolean;
26
27
  scope?: string;
27
28
  transport?: string;
@@ -231,7 +232,9 @@ function AgentInstall({ agents, t, onRefresh }: { agents: AgentInfo[]; t: any; o
231
232
  <span className="text-[10px] text-muted-foreground">{agent.scope}</span>
232
233
  </>
233
234
  ) : (
234
- <span className="text-[10px] text-muted-foreground">{m?.notInstalled ?? 'Not installed'}</span>
235
+ <span className="text-[10px] text-muted-foreground">
236
+ {agent.present ? (m?.detected ?? 'Detected') : (m?.notFound ?? 'Not found')}
237
+ </span>
235
238
  )}
236
239
  {/* Scope selector */}
237
240
  {selected.has(agent.key) && agent.hasProjectScope && agent.hasGlobalScope && (
package/app/lib/i18n.ts CHANGED
@@ -197,6 +197,8 @@ export const messages = {
197
197
  global: 'Global',
198
198
  installed: 'Installed',
199
199
  notInstalled: 'Not installed',
200
+ detected: 'Detected',
201
+ notFound: 'Not found',
200
202
  transportStdio: 'stdio (recommended)',
201
203
  transportHttp: 'http',
202
204
  transportAuto: 'auto (recommended)',
@@ -325,6 +327,8 @@ export const messages = {
325
327
  agentNoneSelected: 'No agents selected — you can configure later in Settings → MCP.',
326
328
  agentSkipLater: 'Skip — configure later',
327
329
  agentNotInstalled: 'not installed',
330
+ agentDetected: 'detected',
331
+ agentNotFound: 'not found',
328
332
  agentStatusOk: 'configured',
329
333
  agentStatusError: 'failed',
330
334
  agentInstalling: 'Configuring…',
@@ -566,6 +570,8 @@ export const messages = {
566
570
  global: '全局',
567
571
  installed: '已安装',
568
572
  notInstalled: '未安装',
573
+ detected: '已检测',
574
+ notFound: '未找到',
569
575
  transportStdio: 'stdio(推荐)',
570
576
  transportHttp: 'http',
571
577
  transportAuto: '自动(推荐)',
@@ -694,6 +700,8 @@ export const messages = {
694
700
  agentNoneSelected: '未选择 agent — 可稍后在 设置 → MCP 中配置。',
695
701
  agentSkipLater: '跳过 — 稍后配置',
696
702
  agentNotInstalled: '未安装',
703
+ agentDetected: '已检测到',
704
+ agentNotFound: '未找到',
697
705
  agentStatusOk: '已配置',
698
706
  agentStatusError: '失败',
699
707
  agentInstalling: '配置中…',
@@ -1,6 +1,7 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import os from 'os';
4
+ import { execSync } from 'child_process';
4
5
 
5
6
  export function expandHome(p: string): string {
6
7
  return p.startsWith('~/') ? path.resolve(os.homedir(), p.slice(2)) : p;
@@ -12,20 +13,90 @@ export interface AgentDef {
12
13
  global: string;
13
14
  key: string;
14
15
  preferredTransport: 'stdio' | 'http';
16
+ /** CLI binary name for presence detection (e.g. 'claude'). Optional. */
17
+ presenceCli?: string;
18
+ /** Data directories for presence detection. Any one existing → present. */
19
+ presenceDirs?: string[];
15
20
  }
16
21
 
17
22
  export const MCP_AGENTS: Record<string, AgentDef> = {
18
- 'claude-code': { name: 'Claude Code', project: '.mcp.json', global: '~/.claude.json', key: 'mcpServers', preferredTransport: 'stdio' },
19
- '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', preferredTransport: 'http' },
20
- 'cursor': { name: 'Cursor', project: '.cursor/mcp.json', global: '~/.cursor/mcp.json', key: 'mcpServers', preferredTransport: 'stdio' },
21
- 'windsurf': { name: 'Windsurf', project: null, global: '~/.codeium/windsurf/mcp_config.json', key: 'mcpServers', preferredTransport: 'stdio' },
22
- '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', preferredTransport: 'stdio' },
23
- 'trae': { name: 'Trae', project: '.trae/mcp.json', global: '~/.trae/mcp.json', key: 'mcpServers', preferredTransport: 'stdio' },
24
- 'gemini-cli': { name: 'Gemini CLI', project: '.gemini/settings.json', global: '~/.gemini/settings.json', key: 'mcpServers', preferredTransport: 'stdio' },
25
- 'openclaw': { name: 'OpenClaw', project: null, global: '~/.openclaw/mcp.json', key: 'mcpServers', preferredTransport: 'stdio' },
26
- 'codebuddy': { name: 'CodeBuddy', project: null, global: '~/.claude-internal/.claude.json', key: 'mcpServers', preferredTransport: 'stdio' },
23
+ 'claude-code': {
24
+ name: 'Claude Code',
25
+ project: '.mcp.json',
26
+ global: '~/.claude.json',
27
+ key: 'mcpServers',
28
+ preferredTransport: 'stdio',
29
+ presenceCli: 'claude',
30
+ presenceDirs: ['~/.claude/'],
31
+ },
32
+ 'cursor': {
33
+ name: 'Cursor',
34
+ project: '.cursor/mcp.json',
35
+ global: '~/.cursor/mcp.json',
36
+ key: 'mcpServers',
37
+ preferredTransport: 'stdio',
38
+ presenceDirs: ['~/.cursor/'],
39
+ },
40
+ 'windsurf': {
41
+ name: 'Windsurf',
42
+ project: null,
43
+ global: '~/.codeium/windsurf/mcp_config.json',
44
+ key: 'mcpServers',
45
+ preferredTransport: 'stdio',
46
+ presenceDirs: ['~/.codeium/windsurf/'],
47
+ },
48
+ 'cline': {
49
+ name: 'Cline',
50
+ project: null,
51
+ global: process.platform === 'darwin'
52
+ ? '~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json'
53
+ : '~/.config/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json',
54
+ key: 'mcpServers',
55
+ preferredTransport: 'stdio',
56
+ presenceDirs: [
57
+ '~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/',
58
+ '~/.config/Code/User/globalStorage/saoudrizwan.claude-dev/',
59
+ ],
60
+ },
61
+ 'trae': {
62
+ name: 'Trae',
63
+ project: '.trae/mcp.json',
64
+ global: '~/.trae/mcp.json',
65
+ key: 'mcpServers',
66
+ preferredTransport: 'stdio',
67
+ presenceDirs: ['~/.trae/'],
68
+ },
69
+ 'gemini-cli': {
70
+ name: 'Gemini CLI',
71
+ project: '.gemini/settings.json',
72
+ global: '~/.gemini/settings.json',
73
+ key: 'mcpServers',
74
+ preferredTransport: 'stdio',
75
+ presenceCli: 'gemini',
76
+ presenceDirs: ['~/.gemini/'],
77
+ },
78
+ 'openclaw': {
79
+ name: 'OpenClaw',
80
+ project: null,
81
+ global: '~/.openclaw/mcp.json',
82
+ key: 'mcpServers',
83
+ preferredTransport: 'stdio',
84
+ presenceCli: 'openclaw',
85
+ presenceDirs: ['~/.openclaw/'],
86
+ },
87
+ 'codebuddy': {
88
+ name: 'CodeBuddy',
89
+ project: null,
90
+ global: '~/.claude-internal/.claude.json',
91
+ key: 'mcpServers',
92
+ preferredTransport: 'stdio',
93
+ presenceCli: 'claude-internal',
94
+ presenceDirs: ['~/.claude-internal/'],
95
+ },
27
96
  };
28
97
 
98
+ /* ── MindOS MCP Install Detection ──────────────────────────────────────── */
99
+
29
100
  export function detectInstalled(agentKey: string): { installed: boolean; scope?: string; transport?: string; configPath?: string } {
30
101
  const agent = MCP_AGENTS[agentKey];
31
102
  if (!agent) return { installed: false };
@@ -47,3 +118,23 @@ export function detectInstalled(agentKey: string): { installed: boolean; scope?:
47
118
 
48
119
  return { installed: false };
49
120
  }
121
+
122
+ /* ── Agent Presence Detection ──────────────────────────────────────────── */
123
+
124
+ export function detectAgentPresence(agentKey: string): boolean {
125
+ const agent = MCP_AGENTS[agentKey];
126
+ if (!agent) return false;
127
+ // 1. CLI check
128
+ if (agent.presenceCli) {
129
+ try {
130
+ execSync(
131
+ process.platform === 'win32' ? `where ${agent.presenceCli}` : `which ${agent.presenceCli}`,
132
+ { stdio: 'pipe' },
133
+ );
134
+ return true;
135
+ } catch { /* not found */ }
136
+ }
137
+ // 2. Dir check
138
+ if (agent.presenceDirs?.some(d => fs.existsSync(expandHome(d)))) return true;
139
+ return false;
140
+ }
package/app/proxy.ts CHANGED
@@ -16,8 +16,9 @@ export async function proxy(req: NextRequest) {
16
16
 
17
17
  // --- API protection (AUTH_TOKEN) ---
18
18
  if (pathname.startsWith('/api/')) {
19
- // /api/auth handles its own password validation — never block it
20
- if (pathname === '/api/auth') return NextResponse.next();
19
+ // /api/auth handles its own password validation — never block it.
20
+ // /api/health is unauthenticated so check-port can detect this MindOS instance.
21
+ if (pathname === '/api/auth' || pathname === '/api/health') return NextResponse.next();
21
22
 
22
23
  if (!authToken) return NextResponse.next();
23
24
 
Binary file
package/bin/cli.js CHANGED
@@ -317,16 +317,39 @@ const commands = {
317
317
  stop: () => stopMindos(),
318
318
 
319
319
  restart: async () => {
320
+ // Capture old ports BEFORE loadConfig overwrites env vars, so we can
321
+ // clean up processes that are still listening on the previous ports
322
+ // (e.g. user changed ports in the GUI and config was already saved).
323
+ // Sources: (1) MINDOS_OLD_* set by /api/restart when it strips the
324
+ // current env, (2) current MINDOS_*_PORT env vars.
325
+ const oldWebPort = process.env.MINDOS_OLD_WEB_PORT || process.env.MINDOS_WEB_PORT;
326
+ const oldMcpPort = process.env.MINDOS_OLD_MCP_PORT || process.env.MINDOS_MCP_PORT;
327
+
320
328
  loadConfig();
321
- const webPort = Number(process.env.MINDOS_WEB_PORT || '3000');
322
- const mcpPort = Number(process.env.MINDOS_MCP_PORT || '8787');
323
- stopMindos();
324
- // Wait until both ports are actually free (up to 15s)
329
+
330
+ // After loadConfig, env vars reflect the NEW config (or old if unchanged).
331
+ const newWebPort = Number(process.env.MINDOS_WEB_PORT || '3000');
332
+ const newMcpPort = Number(process.env.MINDOS_MCP_PORT || '8787');
333
+
334
+ // Collect old ports that differ from new ones — processes may still be
335
+ // listening there even though config already points to the new ports.
336
+ const extraPorts = [];
337
+ if (oldWebPort && Number(oldWebPort) !== newWebPort) extraPorts.push(oldWebPort);
338
+ if (oldMcpPort && Number(oldMcpPort) !== newMcpPort) extraPorts.push(oldMcpPort);
339
+
340
+ stopMindos({ extraPorts });
341
+
342
+ // Wait until ALL ports (old + new) are actually free (up to 15s)
343
+ const allPorts = new Set([newWebPort, newMcpPort]);
344
+ for (const p of extraPorts) allPorts.add(Number(p));
345
+
325
346
  const deadline = Date.now() + 15_000;
326
347
  while (Date.now() < deadline) {
327
- const webBusy = await isPortInUse(webPort);
328
- const mcpBusy = await isPortInUse(mcpPort);
329
- if (!webBusy && !mcpBusy) break;
348
+ let anyBusy = false;
349
+ for (const p of allPorts) {
350
+ if (await isPortInUse(p)) { anyBusy = true; break; }
351
+ }
352
+ if (!anyBusy) break;
330
353
  await new Promise((r) => setTimeout(r, 500));
331
354
  }
332
355
  await commands[getStartMode()]();
package/bin/lib/build.js CHANGED
@@ -65,6 +65,17 @@ function writeDepsStamp() {
65
65
  }
66
66
  }
67
67
 
68
+ /** Critical packages that must exist after npm install for the app to work. */
69
+ const CRITICAL_DEPS = ['next', '@next/env', 'react', 'react-dom'];
70
+
71
+ function verifyDeps() {
72
+ const nm = resolve(ROOT, 'app', 'node_modules');
73
+ for (const dep of CRITICAL_DEPS) {
74
+ if (!existsSync(resolve(nm, dep, 'package.json'))) return false;
75
+ }
76
+ return true;
77
+ }
78
+
68
79
  export function ensureAppDeps() {
69
80
  const appNext = resolve(ROOT, 'app', 'node_modules', 'next', 'package.json');
70
81
  const needsInstall = !existsSync(appNext) || depsChanged();
@@ -90,5 +101,19 @@ export function ensureAppDeps() {
90
101
  : 'Installing app dependencies (first run)...\n';
91
102
  console.log(yellow(label));
92
103
  run('npm install --prefer-offline --no-workspaces', resolve(ROOT, 'app'));
104
+
105
+ // Verify critical deps — npm tar extraction can silently fail (ENOENT race)
106
+ if (!verifyDeps()) {
107
+ console.log(yellow('Some dependencies are incomplete, retrying with clean install...\n'));
108
+ const nm = resolve(ROOT, 'app', 'node_modules');
109
+ rmSync(nm, { recursive: true, force: true });
110
+ run('npm install --no-workspaces', resolve(ROOT, 'app'));
111
+ if (!verifyDeps()) {
112
+ console.error(red('\n✘ Failed to install dependencies after retry.\n'));
113
+ console.error(' Try manually: cd ' + resolve(ROOT, 'app') + ' && rm -rf node_modules && npm install');
114
+ process.exit(1);
115
+ }
116
+ }
117
+
93
118
  writeDepsStamp();
94
119
  }
@@ -1,16 +1,119 @@
1
1
  /**
2
2
  * Shared MCP agent definitions for CLI tools.
3
3
  * Mirrors app/lib/mcp-agents.ts — keep in sync manually.
4
+ *
5
+ * Each agent entry includes presenceCli / presenceDirs for detecting
6
+ * whether the agent is installed on the user's machine. To add a new
7
+ * agent, add a single entry here — no separate table needed.
4
8
  */
5
9
 
10
+ import { existsSync } from 'node:fs';
11
+ import { resolve } from 'node:path';
12
+ import { homedir } from 'node:os';
13
+ import { execSync } from 'node:child_process';
14
+
15
+ function expandHome(p) {
16
+ return p.startsWith('~/') ? resolve(homedir(), p.slice(2)) : p;
17
+ }
18
+
6
19
  export const MCP_AGENTS = {
7
- 'claude-code': { name: 'Claude Code', project: '.mcp.json', global: '~/.claude.json', key: 'mcpServers', preferredTransport: 'stdio' },
8
- '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', preferredTransport: 'http' },
9
- 'cursor': { name: 'Cursor', project: '.cursor/mcp.json', global: '~/.cursor/mcp.json', key: 'mcpServers', preferredTransport: 'stdio' },
10
- 'windsurf': { name: 'Windsurf', project: null, global: '~/.codeium/windsurf/mcp_config.json', key: 'mcpServers', preferredTransport: 'stdio' },
11
- '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', preferredTransport: 'stdio' },
12
- 'trae': { name: 'Trae', project: '.trae/mcp.json', global: '~/.trae/mcp.json', key: 'mcpServers', preferredTransport: 'stdio' },
13
- 'gemini-cli': { name: 'Gemini CLI', project: '.gemini/settings.json', global: '~/.gemini/settings.json', key: 'mcpServers', preferredTransport: 'stdio' },
14
- 'openclaw': { name: 'OpenClaw', project: null, global: '~/.openclaw/mcp.json', key: 'mcpServers', preferredTransport: 'stdio' },
15
- 'codebuddy': { name: 'CodeBuddy', project: null, global: '~/.claude-internal/.claude.json', key: 'mcpServers', preferredTransport: 'stdio' },
20
+ 'claude-code': {
21
+ name: 'Claude Code',
22
+ project: '.mcp.json',
23
+ global: '~/.claude.json',
24
+ key: 'mcpServers',
25
+ preferredTransport: 'stdio',
26
+ presenceCli: 'claude',
27
+ presenceDirs: ['~/.claude/'],
28
+ },
29
+ 'claude-desktop': {
30
+ name: 'Claude Desktop',
31
+ project: null,
32
+ global: process.platform === 'darwin'
33
+ ? '~/Library/Application Support/Claude/claude_desktop_config.json'
34
+ : '~/.config/Claude/claude_desktop_config.json',
35
+ key: 'mcpServers',
36
+ preferredTransport: 'http',
37
+ presenceDirs: ['~/Library/Application Support/Claude/', '~/.config/Claude/'],
38
+ },
39
+ 'cursor': {
40
+ name: 'Cursor',
41
+ project: '.cursor/mcp.json',
42
+ global: '~/.cursor/mcp.json',
43
+ key: 'mcpServers',
44
+ preferredTransport: 'stdio',
45
+ presenceDirs: ['~/.cursor/'],
46
+ },
47
+ 'windsurf': {
48
+ name: 'Windsurf',
49
+ project: null,
50
+ global: '~/.codeium/windsurf/mcp_config.json',
51
+ key: 'mcpServers',
52
+ preferredTransport: 'stdio',
53
+ presenceDirs: ['~/.codeium/windsurf/'],
54
+ },
55
+ 'cline': {
56
+ name: 'Cline',
57
+ project: null,
58
+ global: process.platform === 'darwin'
59
+ ? '~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json'
60
+ : '~/.config/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json',
61
+ key: 'mcpServers',
62
+ preferredTransport: 'stdio',
63
+ presenceDirs: [
64
+ '~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/',
65
+ '~/.config/Code/User/globalStorage/saoudrizwan.claude-dev/',
66
+ ],
67
+ },
68
+ 'trae': {
69
+ name: 'Trae',
70
+ project: '.trae/mcp.json',
71
+ global: '~/.trae/mcp.json',
72
+ key: 'mcpServers',
73
+ preferredTransport: 'stdio',
74
+ presenceDirs: ['~/.trae/'],
75
+ },
76
+ 'gemini-cli': {
77
+ name: 'Gemini CLI',
78
+ project: '.gemini/settings.json',
79
+ global: '~/.gemini/settings.json',
80
+ key: 'mcpServers',
81
+ preferredTransport: 'stdio',
82
+ presenceCli: 'gemini',
83
+ presenceDirs: ['~/.gemini/'],
84
+ },
85
+ 'openclaw': {
86
+ name: 'OpenClaw',
87
+ project: null,
88
+ global: '~/.openclaw/mcp.json',
89
+ key: 'mcpServers',
90
+ preferredTransport: 'stdio',
91
+ presenceCli: 'openclaw',
92
+ presenceDirs: ['~/.openclaw/'],
93
+ },
94
+ 'codebuddy': {
95
+ name: 'CodeBuddy',
96
+ project: null,
97
+ global: '~/.claude-internal/.claude.json',
98
+ key: 'mcpServers',
99
+ preferredTransport: 'stdio',
100
+ presenceCli: 'claude-internal',
101
+ presenceDirs: ['~/.claude-internal/'],
102
+ },
16
103
  };
104
+
105
+ export function detectAgentPresence(agentKey) {
106
+ const agent = MCP_AGENTS[agentKey];
107
+ if (!agent) return false;
108
+ if (agent.presenceCli) {
109
+ try {
110
+ execSync(
111
+ process.platform === 'win32' ? `where ${agent.presenceCli}` : `which ${agent.presenceCli}`,
112
+ { stdio: 'pipe' },
113
+ );
114
+ return true;
115
+ } catch { /* not found */ }
116
+ }
117
+ if (agent.presenceDirs?.some(d => existsSync(expandHome(d)))) return true;
118
+ return false;
119
+ }