@geminilight/mindos 0.5.11 → 0.5.13

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 (42) hide show
  1. package/README.md +9 -9
  2. package/README_zh.md +9 -9
  3. package/app/README.md +2 -2
  4. package/app/app/api/ask/route.ts +191 -19
  5. package/app/app/api/mcp/install/route.ts +1 -1
  6. package/app/app/api/mcp/status/route.ts +11 -16
  7. package/app/app/api/settings/route.ts +3 -1
  8. package/app/app/api/setup/route.ts +7 -7
  9. package/app/app/api/sync/route.ts +18 -15
  10. package/app/components/AskModal.tsx +28 -32
  11. package/app/components/SettingsModal.tsx +7 -3
  12. package/app/components/ask/MessageList.tsx +65 -3
  13. package/app/components/ask/ThinkingBlock.tsx +55 -0
  14. package/app/components/ask/ToolCallBlock.tsx +97 -0
  15. package/app/components/settings/AiTab.tsx +76 -2
  16. package/app/components/settings/types.ts +8 -0
  17. package/app/components/setup/StepReview.tsx +31 -25
  18. package/app/components/setup/index.tsx +6 -3
  19. package/app/lib/agent/context.ts +317 -0
  20. package/app/lib/agent/index.ts +4 -0
  21. package/app/lib/agent/prompt.ts +46 -31
  22. package/app/lib/agent/stream-consumer.ts +212 -0
  23. package/app/lib/agent/tools.ts +159 -4
  24. package/app/lib/i18n.ts +28 -0
  25. package/app/lib/settings.ts +22 -0
  26. package/app/lib/types.ts +23 -0
  27. package/app/package.json +2 -3
  28. package/bin/cli.js +41 -21
  29. package/bin/lib/build.js +6 -2
  30. package/bin/lib/gateway.js +24 -3
  31. package/bin/lib/mcp-install.js +2 -2
  32. package/bin/lib/mcp-spawn.js +3 -3
  33. package/bin/lib/stop.js +1 -1
  34. package/bin/lib/sync.js +81 -40
  35. package/mcp/README.md +5 -5
  36. package/mcp/src/index.ts +2 -2
  37. package/package.json +3 -2
  38. package/scripts/setup.js +17 -12
  39. package/scripts/upgrade-prompt.md +6 -6
  40. package/skills/mindos/SKILL.md +47 -183
  41. package/skills/mindos-zh/SKILL.md +47 -183
  42. package/app/package-lock.json +0 -15615
package/README.md CHANGED
@@ -156,8 +156,8 @@ Or skip the wizard and edit `~/.mindos/config.json` manually (see Config Referen
156
156
  ```json
157
157
  {
158
158
  "mindRoot": "~/MindOS",
159
- "port": 3000,
160
- "mcpPort": 8787,
159
+ "port": 3456,
160
+ "mcpPort": 8781,
161
161
  "authToken": "",
162
162
  "webPassword": "",
163
163
  "startMode": "daemon",
@@ -182,8 +182,8 @@ Or skip the wizard and edit `~/.mindos/config.json` manually (see Config Referen
182
182
  | Field | Default | Description |
183
183
  | :--- | :--- | :--- |
184
184
  | `mindRoot` | `~/MindOS` | **Required**. Absolute path to the knowledge base root. |
185
- | `port` | `3000` | Optional. Web app port. |
186
- | `mcpPort` | `8787` | Optional. MCP server port. |
185
+ | `port` | `3456` | Optional. Web app port. |
186
+ | `mcpPort` | `8781` | Optional. MCP server port. |
187
187
  | `authToken` | — | Optional. Protects App `/api/*` and MCP `/mcp` with bearer token auth. For Agent / MCP clients. Recommended when exposed to a network. |
188
188
  | `webPassword` | — | Optional. Protects the web UI with a login page. For browser access. Independent from `authToken`. |
189
189
  | `startMode` | `start` | Start mode: `daemon` (background service, auto-starts on boot), `start` (foreground), or `dev`. |
@@ -257,15 +257,15 @@ mindos mcp install -g -y
257
257
  Use `http` transport — MindOS must be running (`mindos start`) on the remote machine:
258
258
 
259
259
  ```bash
260
- mindos mcp install--transport http --url http://<server-ip>:8787/mcp --token your-token -g
260
+ mindos mcp install--transport http --url http://<server-ip>:8781/mcp --token your-token -g
261
261
  ```
262
262
 
263
263
  > [!NOTE]
264
- > For remote access, ensure port `8787` is open in your firewall/security-group.
264
+ > For remote access, ensure port `8781` is open in your firewall/security-group.
265
265
 
266
266
  > Add `-g` to install globally — MCP config is shared across all projects instead of the current directory only.
267
267
 
268
- > The MCP port defaults to `8787`. To use a different port, run `mindos onboard` and set `mcpPort`.
268
+ > The MCP port defaults to `8781`. To use a different port, run `mindos onboard` and set `mcpPort`.
269
269
 
270
270
  <details>
271
271
  <summary>Manual config (JSON snippets)</summary>
@@ -291,7 +291,7 @@ mindos mcp install--transport http --url http://<server-ip>:8787/mcp --token you
291
291
  {
292
292
  "mcpServers": {
293
293
  "mindos": {
294
- "url": "http://localhost:8787/mcp",
294
+ "url": "http://localhost:8781/mcp",
295
295
  "headers": { "Authorization": "Bearer your-token" }
296
296
  }
297
297
  }
@@ -304,7 +304,7 @@ mindos mcp install--transport http --url http://<server-ip>:8787/mcp --token you
304
304
  {
305
305
  "mcpServers": {
306
306
  "mindos": {
307
- "url": "http://<server-ip>:8787/mcp",
307
+ "url": "http://<server-ip>:8781/mcp",
308
308
  "headers": { "Authorization": "Bearer your-token" }
309
309
  }
310
310
  }
package/README_zh.md CHANGED
@@ -158,8 +158,8 @@ mindos onboard --install-daemon
158
158
  ```json
159
159
  {
160
160
  "mindRoot": "~/MindOS",
161
- "port": 3000,
162
- "mcpPort": 8787,
161
+ "port": 3456,
162
+ "mcpPort": 8781,
163
163
  "authToken": "",
164
164
  "webPassword": "",
165
165
  "startMode": "daemon",
@@ -184,8 +184,8 @@ mindos onboard --install-daemon
184
184
  | 字段 | 默认值 | 说明 |
185
185
  | :--- | :--- | :--- |
186
186
  | `mindRoot` | `~/MindOS` | **必填**。知识库根目录的绝对路径 |
187
- | `port` | `3000` | 可选。Web 服务端口 |
188
- | `mcpPort` | `8787` | 可选。MCP 服务端口 |
187
+ | `port` | `3456` | 可选。Web 服务端口 |
188
+ | `mcpPort` | `8781` | 可选。MCP 服务端口 |
189
189
  | `authToken` | — | 可选。保护 App `/api/*` 和 MCP `/mcp` 的 Bearer Token 认证。供 Agent / MCP 客户端使用,暴露到网络时建议设置 |
190
190
  | `webPassword` | — | 可选。为 Web UI 添加登录密码保护。供浏览器访问,与 `authToken` 相互独立 |
191
191
  | `startMode` | `start` | 启动模式:`daemon`(后台服务,开机自启)、`start`(前台)或 `dev` |
@@ -259,15 +259,15 @@ mindos mcp install -g -y
259
259
  使用 `http` transport — 远程机器上需先运行 `mindos start`:
260
260
 
261
261
  ```bash
262
- mindos mcp install --transport http --url http://<服务器IP>:8787/mcp --token your-token -g
262
+ mindos mcp install --transport http --url http://<服务器IP>:8781/mcp --token your-token -g
263
263
  ```
264
264
 
265
265
  > [!NOTE]
266
- > 远程访问时,请确保端口 `8787` 已在防火墙/安全组中放行。
266
+ > 远程访问时,请确保端口 `8781` 已在防火墙/安全组中放行。
267
267
 
268
268
  > 加 `-g` 表示全局安装 — MCP 配置写入用户级配置文件,所有项目共享,而非仅当前目录。
269
269
 
270
- > MCP 端口默认为 `8787`。如需修改,运行 `mindos onboard` 设置 `mcpPort`。
270
+ > MCP 端口默认为 `8781`。如需修改,运行 `mindos onboard` 设置 `mcpPort`。
271
271
 
272
272
  <details>
273
273
  <summary>手动配置(JSON 片段)</summary>
@@ -293,7 +293,7 @@ mindos mcp install --transport http --url http://<服务器IP>:8787/mcp --token
293
293
  {
294
294
  "mcpServers": {
295
295
  "mindos": {
296
- "url": "http://localhost:8787/mcp",
296
+ "url": "http://localhost:8781/mcp",
297
297
  "headers": { "Authorization": "Bearer your-token" }
298
298
  }
299
299
  }
@@ -306,7 +306,7 @@ mindos mcp install --transport http --url http://<服务器IP>:8787/mcp --token
306
306
  {
307
307
  "mcpServers": {
308
308
  "mindos": {
309
- "url": "http://<服务器IP>:8787/mcp",
309
+ "url": "http://<服务器IP>:8781/mcp",
310
310
  "headers": { "Authorization": "Bearer your-token" }
311
311
  }
312
312
  }
package/app/README.md CHANGED
@@ -20,7 +20,7 @@ MIND_ROOT=~/MindOS ANTHROPIC_API_KEY=sk-ant-... npm run dev
20
20
  # Or copy .env.local.example to app/.env.local and fill in values
21
21
  ```
22
22
 
23
- Open [http://localhost:3000](http://localhost:3000).
23
+ Open [http://localhost:3456](http://localhost:3456).
24
24
 
25
25
  ## Features
26
26
 
@@ -47,7 +47,7 @@ Copy `.env.local.example` to `.env.local`:
47
47
  | Variable | Default | Description |
48
48
  |----------|---------|-------------|
49
49
  | `MIND_ROOT` | `./my-mind` | Path to your knowledge base directory |
50
- | `MINDOS_WEB_PORT` | `3000` | Dev/production server port |
50
+ | `MINDOS_WEB_PORT` | `3456` | Dev/production server port |
51
51
  | `AI_PROVIDER` | `anthropic` | `anthropic` or `openai` |
52
52
  | `ANTHROPIC_API_KEY` | — | Required when `AI_PROVIDER=anthropic` |
53
53
  | `ANTHROPIC_MODEL` | `claude-sonnet-4-6` | Anthropic model ID |
@@ -4,7 +4,88 @@ import { NextRequest, NextResponse } from 'next/server';
4
4
  import fs from 'fs';
5
5
  import path from 'path';
6
6
  import { getFileContent, getMindRoot } from '@/lib/fs';
7
- import { getModel, knowledgeBaseTools, truncate, AGENT_SYSTEM_PROMPT } from '@/lib/agent';
7
+ import { getModel, knowledgeBaseTools, truncate, AGENT_SYSTEM_PROMPT, estimateTokens, estimateStringTokens, getContextLimit, needsCompact, truncateToolOutputs, compactMessages, hardPrune } from '@/lib/agent';
8
+ import { effectiveAiConfig, readSettings } from '@/lib/settings';
9
+ import type { Message as FrontendMessage, ToolCallPart as FrontendToolCallPart } from '@/lib/types';
10
+
11
+ /**
12
+ * Convert frontend Message[] (with parts containing tool calls + results)
13
+ * into AI SDK ModelMessage[] that streamText expects.
14
+ *
15
+ * Frontend format:
16
+ * { role: 'assistant', content: '...', parts: [TextPart, ToolCallPart(with output/state)] }
17
+ *
18
+ * AI SDK format:
19
+ * { role: 'assistant', content: [TextPart, ToolCallPart(no output)] }
20
+ * { role: 'tool', content: [ToolResultPart] } // one per completed tool call
21
+ */
22
+ function convertToModelMessages(messages: FrontendMessage[]): ModelMessage[] {
23
+ const result: ModelMessage[] = [];
24
+
25
+ for (const msg of messages) {
26
+ if (msg.role === 'user') {
27
+ result.push({ role: 'user', content: msg.content });
28
+ continue;
29
+ }
30
+
31
+ // Skip error placeholder messages from frontend
32
+ if (msg.content.startsWith('__error__')) continue;
33
+
34
+ // Assistant message
35
+ if (!msg.parts || msg.parts.length === 0) {
36
+ // Plain text assistant message — no tool calls
37
+ if (msg.content) {
38
+ result.push({ role: 'assistant', content: msg.content });
39
+ }
40
+ continue;
41
+ }
42
+
43
+ // Build assistant message content array (text parts + tool call parts)
44
+ const assistantContent: Array<
45
+ { type: 'text'; text: string } |
46
+ { type: 'tool-call'; toolCallId: string; toolName: string; input: unknown }
47
+ > = [];
48
+ const completedToolCalls: FrontendToolCallPart[] = [];
49
+
50
+ for (const part of msg.parts) {
51
+ if (part.type === 'text') {
52
+ if (part.text) {
53
+ assistantContent.push({ type: 'text', text: part.text });
54
+ }
55
+ } else if (part.type === 'tool-call') {
56
+ assistantContent.push({
57
+ type: 'tool-call',
58
+ toolCallId: part.toolCallId,
59
+ toolName: part.toolName,
60
+ input: part.input,
61
+ });
62
+ if (part.state === 'done' || part.state === 'error') {
63
+ completedToolCalls.push(part);
64
+ }
65
+ }
66
+ // 'reasoning' parts are display-only; not sent back to model
67
+ }
68
+
69
+ if (assistantContent.length > 0) {
70
+ result.push({ role: 'assistant', content: assistantContent });
71
+ }
72
+
73
+ // Add tool result messages for completed tool calls
74
+ if (completedToolCalls.length > 0) {
75
+ result.push({
76
+ role: 'tool',
77
+ content: completedToolCalls.map(tc => ({
78
+ type: 'tool-result' as const,
79
+ toolCallId: tc.toolCallId,
80
+ toolName: tc.toolName,
81
+ output: { type: 'text' as const, value: tc.output ?? '' },
82
+ })),
83
+ });
84
+ }
85
+ }
86
+
87
+ return result;
88
+ }
8
89
 
9
90
  function readKnowledgeFile(filePath: string): { ok: boolean; content: string; error?: string } {
10
91
  try {
@@ -33,7 +114,7 @@ function dirnameOf(filePath?: string): string | null {
33
114
 
34
115
  export async function POST(req: NextRequest) {
35
116
  let body: {
36
- messages: ModelMessage[];
117
+ messages: FrontendMessage[];
37
118
  currentFile?: string;
38
119
  attachedFiles?: string[];
39
120
  uploadedFiles?: Array<{ name: string; content: string }>;
@@ -45,8 +126,19 @@ export async function POST(req: NextRequest) {
45
126
  return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 });
46
127
  }
47
128
 
48
- const { messages, currentFile, attachedFiles, uploadedFiles, maxSteps } = body;
49
- const stepLimit = Number.isFinite(maxSteps) ? Math.min(30, Math.max(1, Number(maxSteps))) : 20;
129
+ const { messages, currentFile, attachedFiles, uploadedFiles } = body;
130
+
131
+ // Read agent config from settings
132
+ // NOTE: readSettings() is also called inside getModel() → effectiveAiConfig().
133
+ // Acceptable duplication — both are sync fs reads with identical results.
134
+ const serverSettings = readSettings();
135
+ const agentConfig = serverSettings.agent ?? {};
136
+ const stepLimit = Number.isFinite(body.maxSteps)
137
+ ? Math.min(30, Math.max(1, Number(body.maxSteps)))
138
+ : Math.min(30, Math.max(1, agentConfig.maxSteps ?? 20));
139
+ const enableThinking = agentConfig.enableThinking ?? false;
140
+ const thinkingBudget = agentConfig.thinkingBudget ?? 5000;
141
+ const contextStrategy = agentConfig.contextStrategy ?? 'auto';
50
142
 
51
143
  // Auto-load skill + bootstrap context for each request.
52
144
  const skillPath = path.resolve(process.cwd(), 'data/skills/mindos/SKILL.md');
@@ -64,19 +156,21 @@ export async function POST(req: NextRequest) {
64
156
  target_config_md: targetDir ? readKnowledgeFile(`${targetDir}/CONFIG.md`) : null,
65
157
  };
66
158
 
67
- const initStatus = [
68
- `skill.mindos: ${skill.ok ? 'ok' : `failed (${skill.error})`} [${skillPath}]`,
69
- `bootstrap.instruction: ${bootstrap.instruction.ok ? 'ok' : `failed (${bootstrap.instruction.error})`}`,
70
- `bootstrap.index: ${bootstrap.index.ok ? 'ok' : `failed (${bootstrap.index.error})`}`,
71
- `bootstrap.config_json: ${bootstrap.config_json.ok ? 'ok' : `failed (${bootstrap.config_json.error})`}`,
72
- `bootstrap.config_md: ${bootstrap.config_md.ok ? 'ok' : `failed (${bootstrap.config_md.error})`}`,
73
- `bootstrap.target_dir: ${targetDir ?? '(none)'}`,
74
- `bootstrap.target_readme: ${bootstrap.target_readme ? (bootstrap.target_readme.ok ? 'ok' : `failed (${bootstrap.target_readme.error})`) : 'skipped'}`,
75
- `bootstrap.target_instruction: ${bootstrap.target_instruction ? (bootstrap.target_instruction.ok ? 'ok' : `failed (${bootstrap.target_instruction.error})`) : 'skipped'}`,
76
- `bootstrap.target_config_json: ${bootstrap.target_config_json ? (bootstrap.target_config_json.ok ? 'ok' : `failed (${bootstrap.target_config_json.error})`) : 'skipped'}`,
77
- `bootstrap.target_config_md: ${bootstrap.target_config_md ? (bootstrap.target_config_md.ok ? 'ok' : `failed (${bootstrap.target_config_md.error})`) : 'skipped'}`,
78
- `bootstrap.mind_root: ${getMindRoot()}`,
79
- ].join('\n');
159
+ // Only report failures — when everything loads fine, a single summary line suffices.
160
+ const initFailures: string[] = [];
161
+ if (!skill.ok) initFailures.push(`skill.mindos: failed (${skill.error})`);
162
+ if (!bootstrap.instruction.ok) initFailures.push(`bootstrap.instruction: failed (${bootstrap.instruction.error})`);
163
+ if (!bootstrap.index.ok) initFailures.push(`bootstrap.index: failed (${bootstrap.index.error})`);
164
+ if (!bootstrap.config_json.ok) initFailures.push(`bootstrap.config_json: failed (${bootstrap.config_json.error})`);
165
+ if (!bootstrap.config_md.ok) initFailures.push(`bootstrap.config_md: failed (${bootstrap.config_md.error})`);
166
+ if (bootstrap.target_readme && !bootstrap.target_readme.ok) initFailures.push(`bootstrap.target_readme: failed (${bootstrap.target_readme.error})`);
167
+ if (bootstrap.target_instruction && !bootstrap.target_instruction.ok) initFailures.push(`bootstrap.target_instruction: failed (${bootstrap.target_instruction.error})`);
168
+ if (bootstrap.target_config_json && !bootstrap.target_config_json.ok) initFailures.push(`bootstrap.target_config_json: failed (${bootstrap.target_config_json.error})`);
169
+ if (bootstrap.target_config_md && !bootstrap.target_config_md.ok) initFailures.push(`bootstrap.target_config_md: failed (${bootstrap.target_config_md.error})`);
170
+
171
+ const initStatus = initFailures.length === 0
172
+ ? `All initialization contexts loaded successfully. mind_root=${getMindRoot()}${targetDir ? `, target_dir=${targetDir}` : ''}`
173
+ : `Initialization issues:\n${initFailures.join('\n')}\nmind_root=${getMindRoot()}${targetDir ? `, target_dir=${targetDir}` : ''}`;
80
174
 
81
175
  const initContextBlocks: string[] = [];
82
176
  if (skill.ok) initContextBlocks.push(`## mindos_skill_md\n\n${skill.content}`);
@@ -148,18 +242,96 @@ export async function POST(req: NextRequest) {
148
242
 
149
243
  try {
150
244
  const model = getModel();
245
+ const cfg = effectiveAiConfig();
246
+ const modelName = cfg.provider === 'openai' ? cfg.openaiModel : cfg.anthropicModel;
247
+ let modelMessages = convertToModelMessages(messages);
248
+
249
+ // Phase 3: Context management pipeline
250
+ // 1. Truncate tool outputs in historical messages
251
+ modelMessages = truncateToolOutputs(modelMessages);
252
+
253
+ const preTokens = estimateTokens(modelMessages);
254
+ const sysTokens = estimateStringTokens(systemPrompt);
255
+ const ctxLimit = getContextLimit(modelName);
256
+ console.log(`[ask] Context: ~${preTokens + sysTokens} tokens (messages=${preTokens}, system=${sysTokens}), limit=${ctxLimit}`);
257
+
258
+ // 2. Compact if >70% context limit (skip if user disabled)
259
+ if (contextStrategy === 'auto' && needsCompact(modelMessages, systemPrompt, modelName)) {
260
+ console.log('[ask] Context >70% limit, compacting...');
261
+ const result = await compactMessages(modelMessages, model);
262
+ modelMessages = result.messages;
263
+ if (result.compacted) {
264
+ const postTokens = estimateTokens(modelMessages);
265
+ console.log(`[ask] After compact: ~${postTokens + sysTokens} tokens`);
266
+ } else {
267
+ console.log('[ask] Compact skipped (too few messages), hard prune will handle overflow if needed');
268
+ }
269
+ }
270
+
271
+ // 3. Hard prune if still >90% context limit
272
+ modelMessages = hardPrune(modelMessages, systemPrompt, modelName);
273
+
274
+ // Phase 2: Step monitoring + loop detection
275
+ const stepHistory: Array<{ tool: string; input: string }> = [];
276
+ let loopDetected = false;
277
+ let loopCooldown = 0; // skip detection for N steps after warning
278
+
151
279
  const result = streamText({
152
280
  model,
153
281
  system: systemPrompt,
154
- messages,
282
+ messages: modelMessages,
155
283
  tools: knowledgeBaseTools,
156
284
  stopWhen: stepCountIs(stepLimit),
285
+ ...(enableThinking && cfg.provider === 'anthropic' ? {
286
+ providerOptions: {
287
+ anthropic: {
288
+ thinking: { type: 'enabled', budgetTokens: thinkingBudget },
289
+ },
290
+ },
291
+ } : {}),
292
+
293
+ onStepFinish: ({ toolCalls, usage }) => {
294
+ if (toolCalls) {
295
+ for (const tc of toolCalls) {
296
+ stepHistory.push({ tool: tc.toolName, input: JSON.stringify(tc.input) });
297
+ }
298
+ }
299
+ // Loop detection: same tool + same args 3 times in a row
300
+ // Skip detection during cooldown to avoid repeated warnings
301
+ if (loopCooldown > 0) {
302
+ loopCooldown--;
303
+ } else if (stepHistory.length >= 3) {
304
+ const last3 = stepHistory.slice(-3);
305
+ if (last3.every(s => s.tool === last3[0].tool && s.input === last3[0].input)) {
306
+ loopDetected = true;
307
+ }
308
+ }
309
+ console.log(`[ask] Step ${stepHistory.length}/${stepLimit}, tokens=${usage?.totalTokens ?? '?'}`);
310
+ },
311
+
312
+ prepareStep: ({ messages: stepMessages }) => {
313
+ if (loopDetected) {
314
+ loopDetected = false;
315
+ loopCooldown = 3; // suppress re-detection for 3 steps
316
+ return {
317
+ messages: [
318
+ ...stepMessages,
319
+ {
320
+ role: 'user' as const,
321
+ content: '[SYSTEM WARNING] You have called the same tool with identical arguments 3 times in a row. This appears to be a loop. Try a completely different approach or ask the user for clarification.',
322
+ },
323
+ ],
324
+ };
325
+ }
326
+ return {}; // no modification
327
+ },
328
+
157
329
  onError: ({ error }) => {
158
330
  console.error('[ask] Stream error:', error);
159
331
  },
160
332
  });
161
333
 
162
- return result.toTextStreamResponse();
334
+ return result.toUIMessageStreamResponse();
163
335
  } catch (err) {
164
336
  console.error('[ask] Failed to initialize model:', err);
165
337
  return NextResponse.json(
@@ -21,7 +21,7 @@ function buildEntry(transport: string, url?: string, token?: string) {
21
21
  if (transport === 'stdio') {
22
22
  return { type: 'stdio', command: 'mindos', args: ['mcp'], env: { MCP_TRANSPORT: 'stdio' } };
23
23
  }
24
- const entry: Record<string, unknown> = { url: url || 'http://localhost:8787/mcp' };
24
+ const entry: Record<string, unknown> = { url: url || 'http://localhost:8781/mcp' };
25
25
  if (token) entry.headers = { Authorization: `Bearer ${token}` };
26
26
  return entry;
27
27
  }
@@ -5,29 +5,24 @@ import { readSettings } from '@/lib/settings';
5
5
  export async function GET() {
6
6
  try {
7
7
  const settings = readSettings();
8
- const port = settings.mcpPort ?? 8787;
9
- const endpoint = `http://127.0.0.1:${port}/mcp`;
8
+ const port = settings.mcpPort ?? 8781;
9
+ const baseUrl = `http://127.0.0.1:${port}`;
10
+ const endpoint = `${baseUrl}/mcp`;
10
11
  const authConfigured = !!settings.authToken;
11
12
 
12
- // Check if MCP server is running
13
13
  let running = false;
14
- let toolCount = 0;
14
+
15
15
  try {
16
+ // Use the health endpoint — avoids MCP handshake complexity
17
+ const healthUrl = `${baseUrl}/api/health`;
16
18
  const controller = new AbortController();
17
19
  const timeout = setTimeout(() => controller.abort(), 2000);
18
- const res = await fetch(endpoint, {
19
- method: 'POST',
20
- headers: { 'Content-Type': 'application/json' },
21
- body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/list', params: {} }),
22
- signal: controller.signal,
23
- });
20
+ const res = await fetch(healthUrl, { signal: controller.signal, cache: 'no-store' });
24
21
  clearTimeout(timeout);
22
+
25
23
  if (res.ok) {
26
- running = true;
27
- try {
28
- const data = await res.json();
29
- if (data?.result?.tools) toolCount = data.result.tools.length;
30
- } catch { /* non-JSON response — still running */ }
24
+ const data = await res.json() as { ok?: boolean; service?: string };
25
+ running = data.ok === true && data.service === 'mindos';
31
26
  }
32
27
  } catch {
33
28
  // Connection refused or timeout — not running
@@ -38,7 +33,7 @@ export async function GET() {
38
33
  transport: 'http',
39
34
  endpoint,
40
35
  port,
41
- toolCount,
36
+ toolCount: running ? 20 : 0,
42
37
  authConfigured,
43
38
  });
44
39
  } catch (err) {
@@ -48,7 +48,8 @@ export async function GET() {
48
48
  mindRoot: settings.mindRoot,
49
49
  webPassword: settings.webPassword ? '***set***' : '',
50
50
  authToken: maskToken(settings.authToken),
51
- mcpPort: settings.mcpPort ?? 8787,
51
+ mcpPort: settings.mcpPort ?? 8781,
52
+ agent: settings.agent ?? {},
52
53
  envOverrides: {
53
54
  AI_PROVIDER: !!process.env.AI_PROVIDER,
54
55
  ANTHROPIC_API_KEY: !!process.env.ANTHROPIC_API_KEY,
@@ -110,6 +111,7 @@ export async function POST(req: NextRequest) {
110
111
  },
111
112
  },
112
113
  mindRoot: body.mindRoot ?? current.mindRoot,
114
+ agent: body.agent ?? current.agent,
113
115
  webPassword: resolvedWebPassword,
114
116
  authToken: resolvedAuthToken,
115
117
  port: typeof body.port === 'number' ? body.port : current.port,
@@ -20,8 +20,8 @@ export async function GET() {
20
20
  mindRoot: defaultMindRoot,
21
21
  homeDir: home,
22
22
  platform: process.platform,
23
- port: s.port ?? 3000,
24
- mcpPort: s.mcpPort ?? 8787,
23
+ port: s.port ?? 3456,
24
+ mcpPort: s.mcpPort ?? 8781,
25
25
  authToken: s.authToken ?? '',
26
26
  webPassword: s.webPassword ?? '',
27
27
  provider: s.ai.provider,
@@ -58,8 +58,8 @@ export async function POST(req: NextRequest) {
58
58
  const resolvedRoot = expandHome(mindRoot.trim());
59
59
 
60
60
  // Validate ports
61
- const webPort = typeof port === 'number' ? port : 3000;
62
- const mcpPortNum = typeof mcpPort === 'number' ? mcpPort : 8787;
61
+ const webPort = typeof port === 'number' ? port : 3456;
62
+ const mcpPortNum = typeof mcpPort === 'number' ? mcpPort : 8781;
63
63
  if (webPort < 1024 || webPort > 65535) {
64
64
  return NextResponse.json({ error: `Invalid web port: ${webPort}` }, { status: 400 });
65
65
  }
@@ -78,7 +78,7 @@ export async function POST(req: NextRequest) {
78
78
 
79
79
  // Read current running port for portChanged detection
80
80
  const current = readSettings();
81
- const currentPort = current.port ?? 3000;
81
+ const currentPort = current.port ?? 3456;
82
82
 
83
83
  // Use the same resolved values that will actually be written to config
84
84
  const resolvedAuthToken = authToken ?? current.authToken ?? '';
@@ -87,8 +87,8 @@ export async function POST(req: NextRequest) {
87
87
  // Re-onboard only needs restart if port/path/auth/password actually changed.
88
88
  const isFirstTime = current.setupPending === true || !current.mindRoot;
89
89
  const needsRestart = isFirstTime || (
90
- webPort !== (current.port ?? 3000) ||
91
- mcpPortNum !== (current.mcpPort ?? 8787) ||
90
+ webPort !== (current.port ?? 3456) ||
91
+ mcpPortNum !== (current.mcpPort ?? 8781) ||
92
92
  resolvedRoot !== (current.mindRoot || '') ||
93
93
  resolvedAuthToken !== (current.authToken ?? '') ||
94
94
  resolvedWebPassword !== (current.webPassword ?? '')
@@ -37,6 +37,22 @@ function isGitRepo(dir: string) {
37
37
  return existsSync(join(dir, '.git'));
38
38
  }
39
39
 
40
+ /** Resolve path to bin/cli.js at runtime. */
41
+ function getCliPath() {
42
+ return resolve(process.cwd(), '..', 'bin', 'cli' + '.js');
43
+ }
44
+
45
+ /** Run CLI command via execFile — avoids shell injection by passing args as array */
46
+ function runCli(args: string[], timeoutMs = 30000): Promise<void> {
47
+ const cliPath = getCliPath();
48
+ return new Promise((res, rej) => {
49
+ execFile(process.execPath, [cliPath, ...args], { timeout: timeoutMs }, (err, _stdout, stderr) => {
50
+ if (err) rej(new Error(stderr?.trim() || err.message));
51
+ else res();
52
+ });
53
+ });
54
+ }
55
+
40
56
  export async function GET() {
41
57
  const config = loadConfig();
42
58
  const syncConfig = config.sync || {};
@@ -99,16 +115,9 @@ export async function POST(req: NextRequest) {
99
115
 
100
116
  // Call CLI's sync init — pass clean remote + token separately (never embed token in URL)
101
117
  try {
102
- const cliPath = resolve(process.cwd(), '..', 'bin', 'cli.js');
103
118
  const args = ['sync', 'init', '--non-interactive', '--remote', remote, '--branch', branch];
104
119
  if (body.token) args.push('--token', body.token);
105
-
106
- await new Promise<void>((res, rej) => {
107
- execFile('node', [cliPath, ...args], { timeout: 30000 }, (err, stdout, stderr) => {
108
- if (err) rej(new Error(stderr?.trim() || err.message));
109
- else res();
110
- });
111
- });
120
+ await runCli(args, 30000);
112
121
  return NextResponse.json({ success: true, message: 'Sync initialized' });
113
122
  } catch (err: unknown) {
114
123
  const errMsg = err instanceof Error ? err.message : String(err);
@@ -122,13 +131,7 @@ export async function POST(req: NextRequest) {
122
131
  }
123
132
  // Delegate to CLI for unified conflict handling
124
133
  try {
125
- const cliPath = resolve(process.cwd(), '..', 'bin', 'cli.js');
126
- await new Promise<void>((res, rej) => {
127
- execFile('node', [cliPath, 'sync', 'now'], { timeout: 60000 }, (err, stdout, stderr) => {
128
- if (err) rej(new Error(stderr?.trim() || err.message));
129
- else res();
130
- });
131
- });
134
+ await runCli(['sync', 'now'], 60000);
132
135
  return NextResponse.json({ ok: true });
133
136
  } catch (err: unknown) {
134
137
  const errMsg = err instanceof Error ? err.message : String(err);