@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.
- package/README.md +10 -8
- package/README_zh.md +7 -5
- package/app/app/api/mcp/agents/route.ts +3 -1
- package/app/app/api/mcp/install-skill/route.ts +1 -1
- package/app/app/api/restart/route.ts +28 -4
- package/app/components/SetupWizard.tsx +12 -5
- package/app/components/settings/McpTab.tsx +4 -1
- package/app/lib/i18n.ts +8 -0
- package/app/lib/mcp-agents.ts +100 -9
- package/app/proxy.ts +3 -2
- package/assets/images/wechat-qr.png +0 -0
- package/bin/cli.js +30 -7
- package/bin/lib/build.js +25 -0
- package/bin/lib/mcp-agents.js +112 -9
- package/bin/lib/stop.js +15 -2
- package/package.json +11 -1
- package/scripts/setup.js +5 -4
- package/skills/project-wiki/SKILL.md +223 -0
- package/skills/project-wiki/assets/api-reference.tmpl.md +49 -0
- package/skills/project-wiki/assets/backlog.tmpl.md +15 -0
- package/skills/project-wiki/assets/changelog.tmpl.md +16 -0
- package/skills/project-wiki/assets/conventions.tmpl.md +29 -0
- package/skills/project-wiki/assets/design-exploration.tmpl.md +26 -0
- package/skills/project-wiki/assets/design-principle.tmpl.md +48 -0
- package/skills/project-wiki/assets/development-guide.tmpl.md +38 -0
- package/skills/project-wiki/assets/glossary.tmpl.md +9 -0
- package/skills/project-wiki/assets/known-pitfalls.tmpl.md +21 -0
- package/skills/project-wiki/assets/postmortem.tmpl.md +38 -0
- package/skills/project-wiki/assets/product-proposal.tmpl.md +41 -0
- package/skills/project-wiki/assets/project-roadmap.tmpl.md +23 -0
- package/skills/project-wiki/assets/stage-x.tmpl.md +78 -0
- package/skills/project-wiki/assets/system-architecture.tmpl.md +62 -0
- package/skills/project-wiki/references/file-reference.md +254 -0
- package/skills/project-wiki/references/writing-guide.md +28 -0
- package/app/data/pages/home-dark.png +0 -0
- package/app/data/pages/home-mobile-crop.png +0 -0
- package/app/data/pages/home-mobile.png +0 -0
- package/app/data/pages/home.png +0 -0
- package/app/data/pages/view-dir.png +0 -0
- package/app/data/pages/view-file-bot.png +0 -0
- package/app/data/pages/view-file-dark-crop.png +0 -0
- package/app/data/pages/view-file-dark.png +0 -0
- package/app/data/pages/view-file-mobile.png +0 -0
- package/app/data/pages/view-file-sm.png +0 -0
- package/app/data/pages/view-file-top.png +0 -0
- package/app/data/pages/view-file.png +0 -0
- package/app/eslint.config.mjs +0 -18
- package/app/vitest.config.ts +0 -14
- 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
|
-
## 🧠
|
|
51
|
+
## 🧠 Human-AI Shared Mind
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
> No more fragmented memory, no more black-box behavior, no more lost experience.
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
**1. Global Sync — Breaking Memory Silos**
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
Each Agent keeps its own memory — switching tools means manually hauling context. **MindOS lets all Agents share one knowledge base via MCP and Skills — record once, reuse everywhere.**
|
|
58
58
|
|
|
59
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
- **
|
|
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
|
-
|
|
57
|
+
多个 Agent 各记各的,切换工具靠人工搬运上下文。**MindOS 通过 MCP 和 Skill 让所有 Agent 共享同一份知识库——一处记录,全局复用。**
|
|
56
58
|
|
|
57
59
|
**2. 透明可控 — 消除记忆黑箱**
|
|
58
60
|
|
|
59
|
-
|
|
61
|
+
Agent 记了什么、记对没有,用户无从知晓。**MindOS 将每次读写沉淀为本地纯文本,人类可在 GUI 中审查、修正、删除。**
|
|
60
62
|
|
|
61
63
|
**3. 共生演进 — 经验回流为指令**
|
|
62
64
|
|
|
63
|
-
|
|
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([
|
|
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
|
-
|
|
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:
|
|
34
|
+
env: childEnv,
|
|
14
35
|
});
|
|
15
36
|
child.unref();
|
|
16
|
-
// Give a brief moment for the response to be sent before exiting
|
|
17
|
-
|
|
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,
|
|
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
|
|
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">
|
|
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: '配置中…',
|
package/app/lib/mcp-agents.ts
CHANGED
|
@@ -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':
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
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
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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
|
-
|
|
328
|
-
const
|
|
329
|
-
|
|
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
|
}
|
package/bin/lib/mcp-agents.js
CHANGED
|
@@ -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':
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
+
}
|