@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.
- package/README.md +9 -9
- package/README_zh.md +9 -9
- package/app/README.md +2 -2
- package/app/app/api/ask/route.ts +191 -19
- package/app/app/api/mcp/install/route.ts +1 -1
- package/app/app/api/mcp/status/route.ts +11 -16
- package/app/app/api/settings/route.ts +3 -1
- package/app/app/api/setup/route.ts +7 -7
- package/app/app/api/sync/route.ts +18 -15
- package/app/components/AskModal.tsx +28 -32
- package/app/components/SettingsModal.tsx +7 -3
- package/app/components/ask/MessageList.tsx +65 -3
- package/app/components/ask/ThinkingBlock.tsx +55 -0
- package/app/components/ask/ToolCallBlock.tsx +97 -0
- package/app/components/settings/AiTab.tsx +76 -2
- package/app/components/settings/types.ts +8 -0
- package/app/components/setup/StepReview.tsx +31 -25
- package/app/components/setup/index.tsx +6 -3
- package/app/lib/agent/context.ts +317 -0
- package/app/lib/agent/index.ts +4 -0
- package/app/lib/agent/prompt.ts +46 -31
- package/app/lib/agent/stream-consumer.ts +212 -0
- package/app/lib/agent/tools.ts +159 -4
- package/app/lib/i18n.ts +28 -0
- package/app/lib/settings.ts +22 -0
- package/app/lib/types.ts +23 -0
- package/app/package.json +2 -3
- package/bin/cli.js +41 -21
- package/bin/lib/build.js +6 -2
- package/bin/lib/gateway.js +24 -3
- package/bin/lib/mcp-install.js +2 -2
- package/bin/lib/mcp-spawn.js +3 -3
- package/bin/lib/stop.js +1 -1
- package/bin/lib/sync.js +81 -40
- package/mcp/README.md +5 -5
- package/mcp/src/index.ts +2 -2
- package/package.json +3 -2
- package/scripts/setup.js +17 -12
- package/scripts/upgrade-prompt.md +6 -6
- package/skills/mindos/SKILL.md +47 -183
- package/skills/mindos-zh/SKILL.md +47 -183
- 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":
|
|
160
|
-
"mcpPort":
|
|
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` | `
|
|
186
|
-
| `mcpPort` | `
|
|
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>:
|
|
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 `
|
|
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 `
|
|
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:
|
|
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>:
|
|
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":
|
|
162
|
-
"mcpPort":
|
|
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` | `
|
|
188
|
-
| `mcpPort` | `
|
|
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>:
|
|
262
|
+
mindos mcp install --transport http --url http://<服务器IP>:8781/mcp --token your-token -g
|
|
263
263
|
```
|
|
264
264
|
|
|
265
265
|
> [!NOTE]
|
|
266
|
-
> 远程访问时,请确保端口 `
|
|
266
|
+
> 远程访问时,请确保端口 `8781` 已在防火墙/安全组中放行。
|
|
267
267
|
|
|
268
268
|
> 加 `-g` 表示全局安装 — MCP 配置写入用户级配置文件,所有项目共享,而非仅当前目录。
|
|
269
269
|
|
|
270
|
-
> MCP 端口默认为 `
|
|
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:
|
|
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>:
|
|
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:
|
|
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` | `
|
|
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 |
|
package/app/app/api/ask/route.ts
CHANGED
|
@@ -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:
|
|
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
|
|
49
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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.
|
|
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:
|
|
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 ??
|
|
9
|
-
const
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
27
|
-
|
|
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 ??
|
|
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 ??
|
|
24
|
-
mcpPort: s.mcpPort ??
|
|
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 :
|
|
62
|
-
const mcpPortNum = typeof mcpPort === 'number' ? mcpPort :
|
|
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 ??
|
|
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 ??
|
|
91
|
-
mcpPortNum !== (current.mcpPort ??
|
|
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
|
-
|
|
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);
|