min-agent 0.3.0 → 0.4.0
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 +111 -28
- package/dist/agent.js +1119 -256
- package/dist/cli/commands/chat.js +10 -0
- package/dist/cli/commands/exec.js +32 -0
- package/dist/cli/commands/history.js +58 -0
- package/dist/cli/commands/index.js +224 -0
- package/dist/cli/commands/init.js +18 -0
- package/dist/cli/commands/mcp.js +173 -0
- package/dist/cli/commands/memory.js +69 -0
- package/dist/cli/commands/models.js +21 -0
- package/dist/cli/commands/permission.js +12 -0
- package/dist/cli/commands/rules.js +33 -0
- package/dist/cli/commands/sandbox.js +13 -0
- package/dist/cli/commands/serve.js +9 -0
- package/dist/cli/commands/setup.js +4 -0
- package/dist/cli/commands/shared.js +16 -0
- package/dist/cli/commands/skills.js +119 -0
- package/dist/cli/commands/update.js +7 -0
- package/dist/cli/commands/write-config.js +30 -0
- package/dist/cli/errors.js +36 -0
- package/dist/cli/exec-prompt.js +26 -0
- package/dist/cli/option-helpers.js +53 -0
- package/dist/cli/program.js +180 -0
- package/dist/cli.js +5 -888
- package/dist/code-mode.js +32 -14
- package/dist/compaction.js +347 -160
- package/dist/config.js +119 -10
- package/dist/confirm.js +56 -9
- package/dist/context-window.js +107 -39
- package/dist/doom-loop.js +264 -29
- package/dist/fetch-timeout.js +152 -0
- package/dist/http-approvals.js +60 -0
- package/dist/instructions.js +21 -0
- package/dist/logger.js +33 -4
- package/dist/markdown.js +37 -11
- package/dist/mcp.js +328 -30
- package/dist/memory.js +97 -56
- package/dist/output.js +7 -5
- package/dist/permission-cli.js +43 -0
- package/dist/plugins.js +46 -8
- package/dist/pricing.js +4 -4
- package/dist/provider.js +23 -6
- package/dist/question-format.js +60 -0
- package/dist/sandbox-cli.js +82 -0
- package/dist/sandbox.js +403 -0
- package/dist/save-throttle.js +45 -0
- package/dist/serve/common.js +404 -0
- package/dist/serve/routes-chat.js +347 -0
- package/dist/serve/routes-mcp.js +212 -0
- package/dist/serve/routes-memory.js +66 -0
- package/dist/serve/routes-meta.js +205 -0
- package/dist/serve/routes-sessions.js +61 -0
- package/dist/serve/routes-skills.js +70 -0
- package/dist/serve.js +33 -883
- package/dist/sessions.js +53 -9
- package/dist/skills.js +82 -18
- package/dist/title-gen.js +8 -2
- package/dist/token-display.js +36 -0
- package/dist/tool-display.js +5 -0
- package/dist/tool-output.js +1 -3
- package/dist/tools/apply_patch.js +85 -11
- package/dist/tools/atomic-file.js +35 -0
- package/dist/tools/backend.js +2 -2
- package/dist/tools/bash.js +57 -19
- package/dist/tools/code_search.js +7 -1
- package/dist/tools/edit.js +11 -10
- package/dist/tools/explore.js +74 -14
- package/dist/tools/glob.js +4 -0
- package/dist/tools/grep.js +17 -10
- package/dist/tools/index.js +6 -21
- package/dist/tools/question.js +28 -9
- package/dist/tools/read.js +6 -4
- package/dist/tools/search-searxng.js +223 -0
- package/dist/tools/search-serper.js +189 -0
- package/dist/tools/task.js +84 -30
- package/dist/tools/todo.js +120 -19
- package/dist/tools/web_fetch.js +11 -3
- package/dist/tools/web_search.js +66 -556
- package/dist/tools/write.js +23 -6
- package/dist/tui/App.js +63 -14
- package/dist/tui/ConfirmBar.js +45 -13
- package/dist/tui/InputBar.js +150 -35
- package/dist/tui/MessageList.js +266 -125
- package/dist/tui/ModelPicker.js +8 -3
- package/dist/tui/QuestionBar.js +51 -19
- package/dist/tui/SessionPicker.js +79 -0
- package/dist/tui/StatusBar.js +8 -14
- package/dist/tui/agent-runner.js +142 -22
- package/dist/tui/caret-pos.js +48 -5
- package/dist/tui/caret.js +1 -1
- package/dist/tui/click-count.js +13 -0
- package/dist/tui/drag-state.js +8 -3
- package/dist/tui/hydrate.js +129 -0
- package/dist/tui/index.js +42 -13
- package/dist/tui/input-history.js +92 -11
- package/dist/tui/layout.js +75 -4
- package/dist/tui/prompt-queue.js +24 -0
- package/dist/tui/selection.js +113 -21
- package/dist/tui/session-switch.js +28 -0
- package/dist/tui/slash-commands.js +22 -6
- package/dist/tui/slash-handler.js +233 -58
- package/dist/tui/text-width.js +38 -16
- package/dist/tui/token-info.js +7 -0
- package/dist/tui/tool-children.js +19 -0
- package/dist/tui/undo-stack.js +1 -1
- package/dist/tui/use-sgr-mouse.js +3 -1
- package/dist/tui-chat.js +276 -40
- package/dist/updater.js +88 -29
- package/dist/xml-search.js +194 -0
- package/docs/API.md +257 -25
- package/docs/superpowers/plans/2026-08-20-tui-completeness.md +873 -0
- package/docs/superpowers/plans/2026-08-20-unified-tui-default.md +631 -0
- package/docs/superpowers/specs/2026-08-20-config-http-alignment-design.md +47 -0
- package/docs/superpowers/specs/2026-08-20-mcp-plugins-alignment-design.md +37 -0
- package/docs/superpowers/specs/2026-08-20-sandbox-permissions-design.md +68 -0
- package/docs/superpowers/specs/2026-08-20-tui-completeness-design.md +273 -0
- package/docs/superpowers/specs/2026-08-20-unified-tui-default-design.md +165 -0
- package/package.json +6 -1
- package/skills/self-config/SKILL.md +90 -0
- package/skills/self-config/reference.md +149 -0
package/dist/serve.js
CHANGED
|
@@ -3,51 +3,20 @@
|
|
|
3
3
|
* Run: min-agent serve [--host 127.0.0.1] [--port 8787]
|
|
4
4
|
*/
|
|
5
5
|
import { createServer } from "http";
|
|
6
|
-
import { readFileSync } from "fs";
|
|
7
|
-
import path from "path";
|
|
8
|
-
import { fileURLToPath } from "url";
|
|
9
6
|
import { timingSafeEqual } from "crypto";
|
|
10
|
-
import { initMcp, shutdownMcp
|
|
11
|
-
import { discoverSkills
|
|
7
|
+
import { initMcp, shutdownMcp } from "./mcp.js";
|
|
8
|
+
import { discoverSkills } from "./skills.js";
|
|
12
9
|
import { loadInstructions } from "./instructions.js";
|
|
13
|
-
import {
|
|
10
|
+
import { isConfigured } from "./config.js";
|
|
14
11
|
import { setAutoApprove } from "./confirm.js";
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
const MAX_BODY_BYTES = 2 * 1024 * 1024;
|
|
24
|
-
const BODY_TIMEOUT_MS = 30_000;
|
|
25
|
-
const MAX_TOOL_RESULT_SSE_CHARS = 48_000;
|
|
26
|
-
const MAX_CONCURRENT_CHATS = 8;
|
|
27
|
-
let activeChats = 0;
|
|
28
|
-
function packageVersion() {
|
|
29
|
-
try {
|
|
30
|
-
const pkgPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../package.json");
|
|
31
|
-
const j = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
32
|
-
return j.version ?? "0.0.0";
|
|
33
|
-
}
|
|
34
|
-
catch { }
|
|
35
|
-
return "0.0.0";
|
|
36
|
-
}
|
|
37
|
-
function corsEnabled() {
|
|
38
|
-
const v = process.env.MIN_AGENT_SERVE_CORS;
|
|
39
|
-
return v === "1" || v === "true";
|
|
40
|
-
}
|
|
41
|
-
function corsHeaders() {
|
|
42
|
-
if (corsEnabled()) {
|
|
43
|
-
return {
|
|
44
|
-
"Access-Control-Allow-Origin": "*",
|
|
45
|
-
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
46
|
-
"Access-Control-Allow-Headers": "Content-Type, Authorization",
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
return {};
|
|
50
|
-
}
|
|
12
|
+
import { createApprovalGate } from "./http-approvals.js";
|
|
13
|
+
import { packageVersion, corsEnabled, corsHeaders, sendJson, } from "./serve/common.js";
|
|
14
|
+
import { handleChatRoutes } from "./serve/routes-chat.js";
|
|
15
|
+
import { handleSessionRoutes } from "./serve/routes-sessions.js";
|
|
16
|
+
import { handleMemoryRoutes } from "./serve/routes-memory.js";
|
|
17
|
+
import { handleMcpRoutes } from "./serve/routes-mcp.js";
|
|
18
|
+
import { handleSkillRoutes } from "./serve/routes-skills.js";
|
|
19
|
+
import { handleMetaRoutes } from "./serve/routes-meta.js";
|
|
51
20
|
/**
|
|
52
21
|
* With CORS enabled and no auth token, arbitrary web pages could drive the
|
|
53
22
|
* agent. Allow only same-machine origins (localhost) in that configuration;
|
|
@@ -80,192 +49,6 @@ function authOk(req) {
|
|
|
80
49
|
const expected = Buffer.from(token);
|
|
81
50
|
return provided.length === expected.length && timingSafeEqual(provided, expected);
|
|
82
51
|
}
|
|
83
|
-
function skillPayload(s) {
|
|
84
|
-
return {
|
|
85
|
-
name: s.name,
|
|
86
|
-
description: s.description,
|
|
87
|
-
location: s.location,
|
|
88
|
-
enabled: s.enabled,
|
|
89
|
-
...(s.disabledScope ? { disabled_scope: s.disabledScope } : {}),
|
|
90
|
-
...(s.version ? { version: s.version } : {}),
|
|
91
|
-
...(s.allowedTools.length ? { allowed_tools: s.allowedTools } : {}),
|
|
92
|
-
...(s.requiredBins.length ? { requires_bins: s.requiredBins } : {}),
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
function sendJson(res, status, body) {
|
|
96
|
-
const headers = {
|
|
97
|
-
"Content-Type": "application/json; charset=utf-8",
|
|
98
|
-
...corsHeaders(),
|
|
99
|
-
};
|
|
100
|
-
res.writeHead(status, headers);
|
|
101
|
-
res.end(JSON.stringify(body));
|
|
102
|
-
}
|
|
103
|
-
/** Decode a URL path segment without letting malformed escapes become a 500. */
|
|
104
|
-
function decodePathSegment(raw) {
|
|
105
|
-
try {
|
|
106
|
-
return decodeURIComponent(raw);
|
|
107
|
-
}
|
|
108
|
-
catch {
|
|
109
|
-
return null;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
function readBody(req) {
|
|
113
|
-
return new Promise((resolve, reject) => {
|
|
114
|
-
const chunks = [];
|
|
115
|
-
let total = 0;
|
|
116
|
-
const timer = setTimeout(() => {
|
|
117
|
-
req.destroy();
|
|
118
|
-
reject(new Error("body_timeout"));
|
|
119
|
-
}, BODY_TIMEOUT_MS);
|
|
120
|
-
req.on("data", (c) => {
|
|
121
|
-
total += c.length;
|
|
122
|
-
if (total > MAX_BODY_BYTES) {
|
|
123
|
-
clearTimeout(timer);
|
|
124
|
-
req.removeAllListeners("data");
|
|
125
|
-
req.pause();
|
|
126
|
-
reject(new Error("body_too_large"));
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
chunks.push(c);
|
|
130
|
-
});
|
|
131
|
-
req.on("end", () => {
|
|
132
|
-
clearTimeout(timer);
|
|
133
|
-
resolve(Buffer.concat(chunks).toString("utf-8"));
|
|
134
|
-
});
|
|
135
|
-
req.on("error", (e) => {
|
|
136
|
-
clearTimeout(timer);
|
|
137
|
-
reject(e);
|
|
138
|
-
});
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
async function readJsonBody(req) {
|
|
142
|
-
let raw;
|
|
143
|
-
try {
|
|
144
|
-
raw = await readBody(req);
|
|
145
|
-
}
|
|
146
|
-
catch (e) {
|
|
147
|
-
if (e?.message === "body_too_large") {
|
|
148
|
-
return { ok: false, status: 413, error: "payload_too_large" };
|
|
149
|
-
}
|
|
150
|
-
if (e?.message === "body_timeout") {
|
|
151
|
-
return { ok: false, status: 408, error: "payload_timeout" };
|
|
152
|
-
}
|
|
153
|
-
throw e;
|
|
154
|
-
}
|
|
155
|
-
try {
|
|
156
|
-
return { ok: true, value: JSON.parse(raw) };
|
|
157
|
-
}
|
|
158
|
-
catch {
|
|
159
|
-
return { ok: false, status: 400, error: "invalid_json" };
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
function sendJsonError(res, e) {
|
|
163
|
-
if (e.status === 413) {
|
|
164
|
-
// The request body was abandoned mid-stream; drop keep-alive so the
|
|
165
|
-
// connection cannot be reused for a second request.
|
|
166
|
-
res.shouldKeepAlive = false;
|
|
167
|
-
}
|
|
168
|
-
sendJson(res, e.status, e.status === 413 ? { error: e.error, max_bytes: MAX_BODY_BYTES } : { error: e.error });
|
|
169
|
-
}
|
|
170
|
-
function sseWrite(res, obj) {
|
|
171
|
-
try {
|
|
172
|
-
if (res.writableEnded)
|
|
173
|
-
return;
|
|
174
|
-
res.write(`data: ${JSON.stringify(obj)}\n\n`);
|
|
175
|
-
}
|
|
176
|
-
catch {
|
|
177
|
-
/* client gone */
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
function truncateForJson(v, max) {
|
|
181
|
-
if (typeof v === "string" && v.length > max)
|
|
182
|
-
return v.slice(0, max) + `\n… [truncated ${v.length - max} chars]`;
|
|
183
|
-
return v;
|
|
184
|
-
}
|
|
185
|
-
function sseHeaders(cors) {
|
|
186
|
-
return {
|
|
187
|
-
"Content-Type": "text/event-stream; charset=utf-8",
|
|
188
|
-
"Cache-Control": "no-cache, no-transform",
|
|
189
|
-
Connection: "keep-alive",
|
|
190
|
-
"X-Accel-Buffering": "no",
|
|
191
|
-
...cors,
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
function createSseCallbacks(res, messages, opts = {}) {
|
|
195
|
-
return {
|
|
196
|
-
onAssistantDisplayDelta(delta) {
|
|
197
|
-
sseWrite(res, { type: "assistant", text: delta });
|
|
198
|
-
},
|
|
199
|
-
onThinkingDelta(delta) {
|
|
200
|
-
sseWrite(res, { type: "thinking", text: delta });
|
|
201
|
-
},
|
|
202
|
-
onToolCall(name, input) {
|
|
203
|
-
sseWrite(res, { type: "tool_call", name, input: truncateForJson(input, MAX_TOOL_RESULT_SSE_CHARS) });
|
|
204
|
-
},
|
|
205
|
-
onToolResult(name, output) {
|
|
206
|
-
sseWrite(res, { type: "tool_result", name, output: truncateForJson(output, MAX_TOOL_RESULT_SSE_CHARS) });
|
|
207
|
-
},
|
|
208
|
-
onCompaction(line) {
|
|
209
|
-
sseWrite(res, { type: "compaction", line });
|
|
210
|
-
},
|
|
211
|
-
onStreamError(message) {
|
|
212
|
-
sseWrite(res, { type: "error", message });
|
|
213
|
-
},
|
|
214
|
-
onRunFinish(info) {
|
|
215
|
-
let saved;
|
|
216
|
-
if (opts.sessionId && messages.length > 0) {
|
|
217
|
-
try {
|
|
218
|
-
saved = saveSession(messages, opts.sessionId);
|
|
219
|
-
}
|
|
220
|
-
catch {
|
|
221
|
-
saved = undefined;
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
sseWrite(res, {
|
|
225
|
-
type: "done",
|
|
226
|
-
step_count: info.stepCount,
|
|
227
|
-
usage: info.usage,
|
|
228
|
-
has_error: info.hasError,
|
|
229
|
-
aborted: info.aborted,
|
|
230
|
-
max_steps_reached: info.maxStepsReached,
|
|
231
|
-
budget_exceeded: info.budgetExceeded ?? false,
|
|
232
|
-
session_id: saved,
|
|
233
|
-
messages: sanitizeForStorage(messages),
|
|
234
|
-
...(opts.codeMode ? { mode: "code", project: opts.project } : {}),
|
|
235
|
-
});
|
|
236
|
-
if (!res.writableEnded)
|
|
237
|
-
res.end();
|
|
238
|
-
},
|
|
239
|
-
};
|
|
240
|
-
}
|
|
241
|
-
function createJsonCallbacks() {
|
|
242
|
-
const toolCalls = [];
|
|
243
|
-
const toolResults = [];
|
|
244
|
-
let finishInfo = null;
|
|
245
|
-
const callbacks = {
|
|
246
|
-
onToolCall(name, input) {
|
|
247
|
-
toolCalls.push({ name, input: truncateForJson(input, MAX_TOOL_RESULT_SSE_CHARS) });
|
|
248
|
-
},
|
|
249
|
-
onToolResult(name, output) {
|
|
250
|
-
toolResults.push({ name, output: truncateForJson(output, MAX_TOOL_RESULT_SSE_CHARS) });
|
|
251
|
-
},
|
|
252
|
-
onRunFinish(info) {
|
|
253
|
-
finishInfo = info;
|
|
254
|
-
},
|
|
255
|
-
};
|
|
256
|
-
return { callbacks, toolCalls, toolResults, finishInfo: () => finishInfo };
|
|
257
|
-
}
|
|
258
|
-
function normalizeMessages(body) {
|
|
259
|
-
if (body.messages && Array.isArray(body.messages)) {
|
|
260
|
-
if (body.messages.length === 0)
|
|
261
|
-
return { ok: false, error: "messages must be non-empty" };
|
|
262
|
-
return { ok: true, messages: [...body.messages] };
|
|
263
|
-
}
|
|
264
|
-
if (typeof body.message === "string" && body.message.length > 0) {
|
|
265
|
-
return { ok: true, messages: [{ role: "user", content: body.message }] };
|
|
266
|
-
}
|
|
267
|
-
return { ok: false, error: "Provide `message` (string) or non-empty `messages` array" };
|
|
268
|
-
}
|
|
269
52
|
export async function runServe(opts = {}) {
|
|
270
53
|
if (!isConfigured()) {
|
|
271
54
|
console.error("Not configured. Run: min-agent setup");
|
|
@@ -279,139 +62,26 @@ export async function runServe(opts = {}) {
|
|
|
279
62
|
"Set the token (Authorization: Bearer <token>) or bind to 127.0.0.1.");
|
|
280
63
|
process.exit(1);
|
|
281
64
|
}
|
|
282
|
-
console.error("\x1b[33m⚠ min-agent serve: confirmations are auto-approved
|
|
65
|
+
console.error("\x1b[33m⚠ min-agent serve: confirmations are auto-approved by default (same as -y). Set auto_approve: false with stream: true to require callbacks.\x1b[0m");
|
|
283
66
|
setAutoApprove(true);
|
|
284
67
|
console.error("\x1b[90m⟳ Initializing MCP, skills, instructions…\x1b[0m");
|
|
285
68
|
await initMcp();
|
|
286
69
|
discoverSkills();
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
const parsed = await readJsonBody(req);
|
|
303
|
-
if (!parsed.ok) {
|
|
304
|
-
sendJsonError(res, parsed);
|
|
305
|
-
return;
|
|
306
|
-
}
|
|
307
|
-
const body = parsed.value;
|
|
308
|
-
const modelId = typeof body.model === "string" ? body.model : undefined;
|
|
309
|
-
const stream = body.stream === true;
|
|
310
|
-
const sessionId = typeof body.session_id === "string" ? body.session_id : undefined;
|
|
311
|
-
let messages;
|
|
312
|
-
if (sessionId) {
|
|
313
|
-
const session = loadSession(sessionId);
|
|
314
|
-
if (!session) {
|
|
315
|
-
sendJson(res, 404, { error: "session_not_found", session_id: sessionId });
|
|
316
|
-
return;
|
|
317
|
-
}
|
|
318
|
-
if (typeof body.message !== "string" || !body.message.trim()) {
|
|
319
|
-
sendJson(res, 400, {
|
|
320
|
-
error: "session_requires_message",
|
|
321
|
-
detail: "With `session_id`, send a non-empty `message` for the new user turn",
|
|
322
|
-
});
|
|
323
|
-
return;
|
|
324
|
-
}
|
|
325
|
-
messages = [...session.messages];
|
|
326
|
-
const content = body.images && body.images.length > 0 ? await buildUserContent(body.message, body.images) : body.message;
|
|
327
|
-
messages.push({ role: "user", content });
|
|
328
|
-
}
|
|
329
|
-
else {
|
|
330
|
-
const norm = normalizeMessages(body);
|
|
331
|
-
if (!norm.ok) {
|
|
332
|
-
sendJson(res, 400, { error: "invalid_body", detail: norm.error });
|
|
333
|
-
return;
|
|
334
|
-
}
|
|
335
|
-
messages = norm.messages;
|
|
336
|
-
if (body.images && body.images.length > 0) {
|
|
337
|
-
const last = messages[messages.length - 1];
|
|
338
|
-
if (!last || last.role !== "user" || typeof body.message !== "string") {
|
|
339
|
-
sendJson(res, 400, {
|
|
340
|
-
error: "images_require_message",
|
|
341
|
-
detail: "With `images`, send a top-level `message` string for the user turn",
|
|
342
|
-
});
|
|
343
|
-
return;
|
|
344
|
-
}
|
|
345
|
-
messages[messages.length - 1] = {
|
|
346
|
-
role: "user",
|
|
347
|
-
content: await buildUserContent(body.message, body.images),
|
|
348
|
-
};
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
const abort = new AbortController();
|
|
352
|
-
res.on("close", () => abort.abort());
|
|
353
|
-
const tracker = new TokenTracker();
|
|
354
|
-
const runOptions = {
|
|
355
|
-
temperature: typeof body.temperature === "number" ? body.temperature : undefined,
|
|
356
|
-
maxTokens: typeof body.maxTokens === "number" ? body.maxTokens : undefined,
|
|
357
|
-
topP: typeof body.topP === "number" ? body.topP : undefined,
|
|
358
|
-
providerName: typeof body.provider === "string" ? body.provider : undefined,
|
|
359
|
-
};
|
|
360
|
-
let codeSystemPrompt;
|
|
361
|
-
let project;
|
|
362
|
-
if (codeMode) {
|
|
363
|
-
project = scanProject();
|
|
364
|
-
codeSystemPrompt = buildCodeSystemPrompt(project, instructions);
|
|
365
|
-
}
|
|
366
|
-
const run = (callbacks) => codeMode
|
|
367
|
-
? runOnceWithSystem(messages, codeSystemPrompt, modelId, abort.signal, callbacks, tracker, runOptions)
|
|
368
|
-
: runOnce(messages, instructions, modelId, abort.signal, callbacks, tracker, runOptions);
|
|
369
|
-
if (stream) {
|
|
370
|
-
res.writeHead(200, sseHeaders(c));
|
|
371
|
-
res.flushHeaders?.();
|
|
372
|
-
const callbacks = createSseCallbacks(res, messages, { sessionId, codeMode, project });
|
|
373
|
-
try {
|
|
374
|
-
await run(callbacks);
|
|
375
|
-
}
|
|
376
|
-
catch (err) {
|
|
377
|
-
if (!res.writableEnded) {
|
|
378
|
-
sseWrite(res, { type: "fatal", message: err?.message ?? String(err) });
|
|
379
|
-
res.end();
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
return;
|
|
383
|
-
}
|
|
384
|
-
const { callbacks, toolCalls, toolResults, finishInfo } = createJsonCallbacks();
|
|
385
|
-
await run(callbacks);
|
|
386
|
-
const lastAssistant = [...messages].reverse().find((m) => m.role === "assistant");
|
|
387
|
-
let savedSession;
|
|
388
|
-
if (sessionId && messages.length > 0) {
|
|
389
|
-
try {
|
|
390
|
-
savedSession = saveSession(messages, sessionId);
|
|
391
|
-
}
|
|
392
|
-
catch {
|
|
393
|
-
savedSession = undefined;
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
const fi = finishInfo();
|
|
397
|
-
sendJson(res, 200, {
|
|
398
|
-
messages: sanitizeForStorage(messages),
|
|
399
|
-
assistant: lastAssistant ?? null,
|
|
400
|
-
tool_calls: toolCalls,
|
|
401
|
-
tool_results: toolResults,
|
|
402
|
-
session_id: savedSession,
|
|
403
|
-
step_count: fi?.stepCount ?? 0,
|
|
404
|
-
usage: fi?.usage ?? null,
|
|
405
|
-
has_error: fi?.hasError ?? false,
|
|
406
|
-
aborted: fi?.aborted ?? false,
|
|
407
|
-
max_steps_reached: fi?.maxStepsReached ?? false,
|
|
408
|
-
...(codeMode ? { mode: "code", project } : {}),
|
|
409
|
-
});
|
|
410
|
-
}
|
|
411
|
-
finally {
|
|
412
|
-
activeChats--;
|
|
413
|
-
}
|
|
414
|
-
}
|
|
70
|
+
const ctx = {
|
|
71
|
+
host,
|
|
72
|
+
version: packageVersion(),
|
|
73
|
+
instructions: await loadInstructions(),
|
|
74
|
+
approvalGate: createApprovalGate(),
|
|
75
|
+
activeChats: 0,
|
|
76
|
+
};
|
|
77
|
+
const routes = [
|
|
78
|
+
handleMetaRoutes,
|
|
79
|
+
handleChatRoutes,
|
|
80
|
+
handleSessionRoutes,
|
|
81
|
+
handleMemoryRoutes,
|
|
82
|
+
handleMcpRoutes,
|
|
83
|
+
handleSkillRoutes,
|
|
84
|
+
];
|
|
415
85
|
const server = createServer(async (req, res) => {
|
|
416
86
|
const c = corsHeaders();
|
|
417
87
|
if (req.method === "OPTIONS") {
|
|
@@ -420,7 +90,10 @@ export async function runServe(opts = {}) {
|
|
|
420
90
|
return;
|
|
421
91
|
}
|
|
422
92
|
if (!authOk(req)) {
|
|
423
|
-
sendJson(res, 401, {
|
|
93
|
+
sendJson(res, 401, {
|
|
94
|
+
error: "unauthorized",
|
|
95
|
+
detail: "Set Authorization: Bearer <MIN_AGENT_SERVE_TOKEN> when MIN_AGENT_SERVE_TOKEN is set",
|
|
96
|
+
});
|
|
424
97
|
return;
|
|
425
98
|
}
|
|
426
99
|
if (corsEnabled() && !originAllowed(req)) {
|
|
@@ -433,532 +106,9 @@ export async function runServe(opts = {}) {
|
|
|
433
106
|
const url = new URL(req.url ?? "/", `http://${host}`);
|
|
434
107
|
const pathname = url.pathname.replace(/\/$/, "") || "/";
|
|
435
108
|
try {
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
return;
|
|
439
|
-
}
|
|
440
|
-
if (req.method === "GET" && pathname === "/v1/meta") {
|
|
441
|
-
sendJson(res, 200, {
|
|
442
|
-
version,
|
|
443
|
-
cwd: process.cwd(),
|
|
444
|
-
instructions_chars: instructions.join("\n").length,
|
|
445
|
-
});
|
|
446
|
-
return;
|
|
447
|
-
}
|
|
448
|
-
if (req.method === "GET" && pathname === "/v1/models") {
|
|
449
|
-
const config = loadConfig();
|
|
450
|
-
const provider = getActiveProvider(config);
|
|
451
|
-
const base = provider?.baseURL;
|
|
452
|
-
const key = provider?.apiKey;
|
|
453
|
-
if (!base || !key) {
|
|
454
|
-
sendJson(res, 500, { error: "provider_not_configured" });
|
|
455
|
-
return;
|
|
456
|
-
}
|
|
457
|
-
const models = await fetchModels(base, key);
|
|
458
|
-
sendJson(res, 200, {
|
|
459
|
-
default_model: provider?.defaultModel ?? null,
|
|
460
|
-
models,
|
|
461
|
-
});
|
|
462
|
-
return;
|
|
463
|
-
}
|
|
464
|
-
if (req.method === "POST" && pathname === "/v1/chat/reload-instructions") {
|
|
465
|
-
instructions = await loadInstructions();
|
|
466
|
-
sendJson(res, 200, { ok: true, instructions_chars: instructions.join("\n").length });
|
|
467
|
-
return;
|
|
468
|
-
}
|
|
469
|
-
if (req.method === "POST" && (pathname === "/v1/chat" || pathname === "/v1/code")) {
|
|
470
|
-
await handleChatRequest(req, res, pathname === "/v1/code");
|
|
471
|
-
return;
|
|
472
|
-
}
|
|
473
|
-
// ─── Sessions ──────────────────────────────────────────────────────
|
|
474
|
-
if (req.method === "GET" && pathname === "/v1/sessions") {
|
|
475
|
-
const sessions = listSessions();
|
|
476
|
-
sendJson(res, 200, { sessions });
|
|
477
|
-
return;
|
|
478
|
-
}
|
|
479
|
-
if (req.method === "DELETE" && pathname.startsWith("/v1/sessions/")) {
|
|
480
|
-
const id = decodePathSegment(pathname.slice("/v1/sessions/".length));
|
|
481
|
-
if (!id) {
|
|
482
|
-
sendJson(res, 400, { error: "missing_session_id" });
|
|
483
|
-
return;
|
|
484
|
-
}
|
|
485
|
-
const ok = deleteSession(id);
|
|
486
|
-
if (!ok) {
|
|
487
|
-
sendJson(res, 404, { error: "session_not_found", session_id: id });
|
|
488
|
-
return;
|
|
489
|
-
}
|
|
490
|
-
sendJson(res, 200, { ok: true, deleted: id });
|
|
491
|
-
return;
|
|
492
|
-
}
|
|
493
|
-
if (req.method === "GET" && pathname.startsWith("/v1/sessions/")) {
|
|
494
|
-
const id = decodePathSegment(pathname.slice("/v1/sessions/".length));
|
|
495
|
-
if (!id) {
|
|
496
|
-
sendJson(res, 400, { error: "missing_session_id" });
|
|
497
|
-
return;
|
|
498
|
-
}
|
|
499
|
-
const session = loadSession(id);
|
|
500
|
-
if (!session) {
|
|
501
|
-
sendJson(res, 404, { error: "session_not_found", session_id: id });
|
|
502
|
-
return;
|
|
503
|
-
}
|
|
504
|
-
sendJson(res, 200, { session });
|
|
505
|
-
return;
|
|
506
|
-
}
|
|
507
|
-
if (req.method === "POST" && pathname.startsWith("/v1/sessions/") && pathname.endsWith("/rename")) {
|
|
508
|
-
const id = decodePathSegment(pathname.slice("/v1/sessions/".length, -"/rename".length));
|
|
509
|
-
if (!id) {
|
|
510
|
-
sendJson(res, 400, { error: "missing_session_id" });
|
|
511
|
-
return;
|
|
512
|
-
}
|
|
513
|
-
const parsed = await readJsonBody(req);
|
|
514
|
-
if (!parsed.ok) {
|
|
515
|
-
sendJsonError(res, parsed);
|
|
516
|
-
return;
|
|
517
|
-
}
|
|
518
|
-
const title = parsed.value.title?.trim();
|
|
519
|
-
if (!title) {
|
|
520
|
-
sendJson(res, 400, { error: "missing_title" });
|
|
521
|
-
return;
|
|
522
|
-
}
|
|
523
|
-
if (!renameSession(id, title)) {
|
|
524
|
-
sendJson(res, 404, { error: "session_not_found", session_id: id });
|
|
525
|
-
return;
|
|
526
|
-
}
|
|
527
|
-
sendJson(res, 200, { ok: true, session_id: id, title });
|
|
528
|
-
return;
|
|
529
|
-
}
|
|
530
|
-
// ─── Memory ────────────────────────────────────────────────────────
|
|
531
|
-
if (req.method === "GET" && pathname === "/v1/memory") {
|
|
532
|
-
const memories = loadMemories();
|
|
533
|
-
sendJson(res, 200, { memories });
|
|
534
|
-
return;
|
|
535
|
-
}
|
|
536
|
-
if (req.method === "POST" && pathname === "/v1/memory") {
|
|
537
|
-
const parsed = await readJsonBody(req);
|
|
538
|
-
if (!parsed.ok) {
|
|
539
|
-
sendJsonError(res, parsed);
|
|
540
|
-
return;
|
|
541
|
-
}
|
|
542
|
-
const body = parsed.value;
|
|
543
|
-
if (!body.content) {
|
|
544
|
-
sendJson(res, 400, { error: "missing_content" });
|
|
545
|
-
return;
|
|
546
|
-
}
|
|
547
|
-
const memory = addMemory(body.content, body.tags ?? []);
|
|
548
|
-
sendJson(res, 201, { ok: true, memory });
|
|
549
|
-
return;
|
|
550
|
-
}
|
|
551
|
-
if (req.method === "GET" && pathname === "/v1/memory/search") {
|
|
552
|
-
const query = url.searchParams.get("q") ?? "";
|
|
553
|
-
if (!query) {
|
|
554
|
-
sendJson(res, 400, { error: "missing_query_param_q" });
|
|
555
|
-
return;
|
|
556
|
-
}
|
|
557
|
-
const results = searchMemories(query);
|
|
558
|
-
sendJson(res, 200, { results });
|
|
559
|
-
return;
|
|
560
|
-
}
|
|
561
|
-
if (req.method === "DELETE" && pathname.startsWith("/v1/memory/")) {
|
|
562
|
-
const idx = parseInt(pathname.slice("/v1/memory/".length));
|
|
563
|
-
if (isNaN(idx)) {
|
|
564
|
-
sendJson(res, 400, { error: "invalid_index" });
|
|
565
|
-
return;
|
|
566
|
-
}
|
|
567
|
-
const ok = deleteMemory(idx - 1);
|
|
568
|
-
if (!ok) {
|
|
569
|
-
sendJson(res, 404, { error: "memory_not_found", index: idx });
|
|
570
|
-
return;
|
|
571
|
-
}
|
|
572
|
-
sendJson(res, 200, { ok: true, deleted: idx });
|
|
573
|
-
return;
|
|
574
|
-
}
|
|
575
|
-
// ─── MCP ───────────────────────────────────────────────────────────
|
|
576
|
-
if (req.method === "GET" && pathname === "/v1/mcp") {
|
|
577
|
-
const status = getMcpStatus();
|
|
578
|
-
sendJson(res, 200, { servers: status });
|
|
579
|
-
return;
|
|
580
|
-
}
|
|
581
|
-
// ─── Skills ────────────────────────────────────────────────────────
|
|
582
|
-
if (req.method === "GET" && pathname === "/v1/skills") {
|
|
583
|
-
const all = getAllSkills();
|
|
584
|
-
sendJson(res, 200, {
|
|
585
|
-
count: all.length,
|
|
586
|
-
enabled_count: all.filter((s) => s.enabled).length,
|
|
587
|
-
stale_disabled: getStaleDisabledSkills(),
|
|
588
|
-
skills: all.map(skillPayload),
|
|
589
|
-
});
|
|
590
|
-
return;
|
|
591
|
-
}
|
|
592
|
-
if (req.method === "POST" && pathname === "/v1/skills/reload") {
|
|
593
|
-
discoverSkills({ silent: true });
|
|
594
|
-
const all = getAllSkills();
|
|
595
|
-
sendJson(res, 200, {
|
|
596
|
-
ok: true,
|
|
597
|
-
count: all.length,
|
|
598
|
-
enabled_count: all.filter((s) => s.enabled).length,
|
|
599
|
-
stale_disabled: getStaleDisabledSkills(),
|
|
600
|
-
skills: all.map(skillPayload),
|
|
601
|
-
});
|
|
602
|
-
return;
|
|
603
|
-
}
|
|
604
|
-
const skillDetail = pathname.match(/^\/v1\/skills\/([^/]+)$/);
|
|
605
|
-
if (req.method === "GET" && skillDetail) {
|
|
606
|
-
const name = decodePathSegment(skillDetail[1]);
|
|
607
|
-
if (!name) {
|
|
608
|
-
sendJson(res, 400, { error: "invalid_skill_name" });
|
|
609
|
-
return;
|
|
610
|
-
}
|
|
611
|
-
const skill = getSkill(name);
|
|
612
|
-
if (!skill) {
|
|
613
|
-
sendJson(res, 404, { error: "skill_not_found", name, available: getAllSkills().map((s) => s.name) });
|
|
614
|
-
return;
|
|
615
|
-
}
|
|
616
|
-
sendJson(res, 200, { skill: { ...skillPayload(skill), content: skill.content } });
|
|
617
|
-
return;
|
|
618
|
-
}
|
|
619
|
-
const skillToggle = pathname.match(/^\/v1\/skills\/([^/]+)\/(enable|disable)$/);
|
|
620
|
-
if (req.method === "POST" && skillToggle) {
|
|
621
|
-
const name = decodePathSegment(skillToggle[1]);
|
|
622
|
-
if (!name) {
|
|
623
|
-
sendJson(res, 400, { error: "invalid_skill_name" });
|
|
624
|
-
return;
|
|
625
|
-
}
|
|
626
|
-
const enabled = skillToggle[2] === "enable";
|
|
627
|
-
if (!getSkill(name)) {
|
|
628
|
-
sendJson(res, 404, { error: "skill_not_found", name, available: getAllSkills().map((s) => s.name) });
|
|
629
|
-
return;
|
|
630
|
-
}
|
|
631
|
-
const parsed = await readJsonBody(req);
|
|
632
|
-
const scope = parsed.ok ? parsed.value.scope : undefined;
|
|
633
|
-
if (scope !== undefined && scope !== "global" && scope !== "project") {
|
|
634
|
-
sendJson(res, 400, { error: "invalid_scope", allowed: ["global", "project"] });
|
|
635
|
-
return;
|
|
636
|
-
}
|
|
637
|
-
const [result] = setSkillEnabled([name], enabled, scope ?? "global");
|
|
638
|
-
sendJson(res, 200, {
|
|
639
|
-
ok: true,
|
|
640
|
-
name,
|
|
641
|
-
enabled: result.enabled,
|
|
642
|
-
scope: scope ?? "global",
|
|
643
|
-
...(result.blockedBy ? { blocked_by: result.blockedBy } : {}),
|
|
644
|
-
});
|
|
645
|
-
return;
|
|
646
|
-
}
|
|
647
|
-
// ─── Rules ─────────────────────────────────────────────────────────
|
|
648
|
-
if (req.method === "GET" && pathname === "/v1/rules") {
|
|
649
|
-
sendJson(res, 200, {
|
|
650
|
-
count: instructions.length,
|
|
651
|
-
rules: instructions.map((inst) => {
|
|
652
|
-
const firstLine = inst.split("\n")[0] ?? "";
|
|
653
|
-
return { source: firstLine.replace("Instructions from: ", ""), chars: inst.length };
|
|
654
|
-
}),
|
|
655
|
-
});
|
|
656
|
-
return;
|
|
657
|
-
}
|
|
658
|
-
// ─── Project (code mode scan) ──────────────────────────────────────
|
|
659
|
-
if (req.method === "GET" && pathname === "/v1/project") {
|
|
660
|
-
const project = scanProject();
|
|
661
|
-
sendJson(res, 200, { project });
|
|
662
|
-
return;
|
|
663
|
-
}
|
|
664
|
-
// ─── Code mode chat ────────────────────────────────────────────────
|
|
665
|
-
// ─── MCP management ────────────────────────────────────────────────
|
|
666
|
-
if (req.method === "POST" && pathname === "/v1/paste") {
|
|
667
|
-
if (req.headers["content-type"]?.split(";")[0]?.trim() !== "application/json") {
|
|
668
|
-
sendJson(res, 415, { error: "unsupported_media_type" });
|
|
669
|
-
return;
|
|
670
|
-
}
|
|
671
|
-
if (activeChats >= MAX_CONCURRENT_CHATS) {
|
|
672
|
-
sendJson(res, 429, { error: "too_many_concurrent_requests", detail: `Max ${MAX_CONCURRENT_CHATS} concurrent chats` });
|
|
673
|
-
return;
|
|
674
|
-
}
|
|
675
|
-
const parsed = await readJsonBody(req);
|
|
676
|
-
if (!parsed.ok) {
|
|
677
|
-
sendJsonError(res, parsed);
|
|
678
|
-
return;
|
|
679
|
-
}
|
|
680
|
-
const body = parsed.value;
|
|
681
|
-
if (!body.image_base64) {
|
|
682
|
-
sendJson(res, 400, { error: "missing_image_base64" });
|
|
683
|
-
return;
|
|
684
|
-
}
|
|
685
|
-
const imageBuffer = Buffer.from(body.image_base64, "base64");
|
|
686
|
-
const mimeType = body.mime_type ?? "image/png";
|
|
687
|
-
const text = body.message ?? "What's in this image?";
|
|
688
|
-
const modelId = body.model;
|
|
689
|
-
const sessionId = body.session_id;
|
|
690
|
-
const stream = body.stream === true;
|
|
691
|
-
const codeMode = body.code === true;
|
|
692
|
-
let messages = [];
|
|
693
|
-
if (sessionId) {
|
|
694
|
-
const session = loadSession(sessionId);
|
|
695
|
-
if (!session) {
|
|
696
|
-
sendJson(res, 404, { error: "session_not_found", session_id: sessionId });
|
|
697
|
-
return;
|
|
698
|
-
}
|
|
699
|
-
messages = [...session.messages];
|
|
700
|
-
}
|
|
701
|
-
activeChats++;
|
|
702
|
-
try {
|
|
703
|
-
const content = [
|
|
704
|
-
{ type: "text", text },
|
|
705
|
-
{ type: "image", image: imageBuffer, mimeType },
|
|
706
|
-
];
|
|
707
|
-
messages.push({ role: "user", content });
|
|
708
|
-
const abort = new AbortController();
|
|
709
|
-
res.on("close", () => abort.abort());
|
|
710
|
-
const tracker = new TokenTracker();
|
|
711
|
-
const runOptions = {
|
|
712
|
-
temperature: typeof body.temperature === "number" ? body.temperature : undefined,
|
|
713
|
-
maxTokens: typeof body.maxTokens === "number" ? body.maxTokens : undefined,
|
|
714
|
-
topP: typeof body.topP === "number" ? body.topP : undefined,
|
|
715
|
-
providerName: typeof body.provider === "string" ? body.provider : undefined,
|
|
716
|
-
};
|
|
717
|
-
if (stream) {
|
|
718
|
-
res.writeHead(200, sseHeaders(c));
|
|
719
|
-
res.flushHeaders?.();
|
|
720
|
-
const callbacks = createSseCallbacks(res, messages, { sessionId });
|
|
721
|
-
try {
|
|
722
|
-
if (codeMode) {
|
|
723
|
-
const project = scanProject();
|
|
724
|
-
const system = buildCodeSystemPrompt(project, instructions);
|
|
725
|
-
await runOnceWithSystem(messages, system, modelId, abort.signal, callbacks, tracker, runOptions);
|
|
726
|
-
}
|
|
727
|
-
else {
|
|
728
|
-
await runOnce(messages, instructions, modelId, abort.signal, callbacks, tracker, runOptions);
|
|
729
|
-
}
|
|
730
|
-
}
|
|
731
|
-
catch (err) {
|
|
732
|
-
if (!res.writableEnded) {
|
|
733
|
-
sseWrite(res, { type: "fatal", message: err?.message ?? String(err) });
|
|
734
|
-
res.end();
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
|
-
}
|
|
738
|
-
else {
|
|
739
|
-
const { callbacks, toolCalls, toolResults, finishInfo } = createJsonCallbacks();
|
|
740
|
-
let project;
|
|
741
|
-
if (codeMode) {
|
|
742
|
-
project = scanProject();
|
|
743
|
-
const system = buildCodeSystemPrompt(project, instructions);
|
|
744
|
-
await runOnceWithSystem(messages, system, modelId, abort.signal, callbacks, tracker, runOptions);
|
|
745
|
-
}
|
|
746
|
-
else {
|
|
747
|
-
await runOnce(messages, instructions, modelId, abort.signal, callbacks, tracker, runOptions);
|
|
748
|
-
}
|
|
749
|
-
const lastAssistant = [...messages].reverse().find((m) => m.role === "assistant");
|
|
750
|
-
let saved;
|
|
751
|
-
if (sessionId) {
|
|
752
|
-
try {
|
|
753
|
-
saved = saveSession(messages, sessionId);
|
|
754
|
-
}
|
|
755
|
-
catch { }
|
|
756
|
-
}
|
|
757
|
-
const fi = finishInfo();
|
|
758
|
-
sendJson(res, 200, {
|
|
759
|
-
messages: sanitizeForStorage(messages),
|
|
760
|
-
assistant: lastAssistant ?? null,
|
|
761
|
-
tool_calls: toolCalls,
|
|
762
|
-
tool_results: toolResults,
|
|
763
|
-
session_id: saved,
|
|
764
|
-
step_count: fi?.stepCount ?? 0,
|
|
765
|
-
usage: fi?.usage ?? null,
|
|
766
|
-
has_error: fi?.hasError ?? false,
|
|
767
|
-
aborted: fi?.aborted ?? false,
|
|
768
|
-
max_steps_reached: fi?.maxStepsReached ?? false,
|
|
769
|
-
...(codeMode ? { mode: "code", project } : {}),
|
|
770
|
-
});
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
finally {
|
|
774
|
-
activeChats--;
|
|
775
|
-
}
|
|
776
|
-
return;
|
|
777
|
-
}
|
|
778
|
-
// ─── MCP management (continued) ───────────────────────────────────
|
|
779
|
-
if (req.method === "POST" && pathname === "/v1/mcp") {
|
|
780
|
-
const parsed = await readJsonBody(req);
|
|
781
|
-
if (!parsed.ok) {
|
|
782
|
-
sendJsonError(res, parsed);
|
|
783
|
-
return;
|
|
784
|
-
}
|
|
785
|
-
const body = parsed.value;
|
|
786
|
-
if (!body.name) {
|
|
787
|
-
sendJson(res, 400, { error: "missing_name" });
|
|
788
|
-
return;
|
|
789
|
-
}
|
|
790
|
-
if (body.remoteTransport && !["streamable-http", "sse", "auto"].includes(body.remoteTransport)) {
|
|
791
|
-
sendJson(res, 400, { error: "invalid_remote_transport", allowed: ["streamable-http", "sse", "auto"] });
|
|
792
|
-
return;
|
|
793
|
-
}
|
|
794
|
-
const scope = body.project ? "project" : "global";
|
|
795
|
-
const { loadMcpConfigFile, saveMcpConfig } = await import("./mcp.js");
|
|
796
|
-
const config = loadMcpConfigFile(scope);
|
|
797
|
-
const common = {
|
|
798
|
-
...(body.environment ? { environment: body.environment } : {}),
|
|
799
|
-
...(body.headers ? { headers: body.headers } : {}),
|
|
800
|
-
...(body.remoteTransport ? { remoteTransport: body.remoteTransport } : {}),
|
|
801
|
-
...(body.timeout !== undefined ? { timeout: body.timeout } : {}),
|
|
802
|
-
...(body.connectTimeout !== undefined ? { connectTimeout: body.connectTimeout } : {}),
|
|
803
|
-
...(body.callTimeout !== undefined ? { callTimeout: body.callTimeout } : {}),
|
|
804
|
-
enabled: body.enabled !== false,
|
|
805
|
-
};
|
|
806
|
-
if (body.url) {
|
|
807
|
-
config.mcpServers[body.name] = { url: body.url, ...(body.token ? { token: body.token } : {}), ...common };
|
|
808
|
-
}
|
|
809
|
-
else if (typeof body.command === "string" && body.command.length > 0) {
|
|
810
|
-
config.mcpServers[body.name] = { command: body.command, ...(body.args ? { args: body.args } : {}), ...common };
|
|
811
|
-
}
|
|
812
|
-
else if (Array.isArray(body.command) && body.command.length > 0) {
|
|
813
|
-
config.mcpServers[body.name] = { command: body.command, ...common };
|
|
814
|
-
}
|
|
815
|
-
else {
|
|
816
|
-
sendJson(res, 400, { error: "provide command or url" });
|
|
817
|
-
return;
|
|
818
|
-
}
|
|
819
|
-
saveMcpConfig(config, scope);
|
|
820
|
-
await syncMcpConnections();
|
|
821
|
-
const status = getMcpStatus()[body.name];
|
|
822
|
-
sendJson(res, 201, {
|
|
823
|
-
ok: true,
|
|
824
|
-
name: body.name,
|
|
825
|
-
scope,
|
|
826
|
-
connected: status?.connected ?? false,
|
|
827
|
-
...(status?.error ? { error: status.error } : {}),
|
|
828
|
-
...(status?.tools?.length ? { tools: status.tools } : {}),
|
|
829
|
-
});
|
|
830
|
-
return;
|
|
831
|
-
}
|
|
832
|
-
if (req.method === "DELETE" && pathname.startsWith("/v1/mcp/")) {
|
|
833
|
-
const name = decodePathSegment(pathname.slice("/v1/mcp/".length));
|
|
834
|
-
if (!name) {
|
|
835
|
-
sendJson(res, 400, { error: "missing_name" });
|
|
836
|
-
return;
|
|
837
|
-
}
|
|
838
|
-
const scope = url.searchParams.get("project") === "1" ? "project" : "global";
|
|
839
|
-
const { loadMcpConfigFile, saveMcpConfig } = await import("./mcp.js");
|
|
840
|
-
const config = loadMcpConfigFile(scope);
|
|
841
|
-
if (!config.mcpServers[name]) {
|
|
842
|
-
sendJson(res, 404, { error: "mcp_not_found", name, scope, available: Object.keys(config.mcpServers) });
|
|
843
|
-
return;
|
|
844
|
-
}
|
|
845
|
-
delete config.mcpServers[name];
|
|
846
|
-
saveMcpConfig(config, scope);
|
|
847
|
-
await syncMcpConnections();
|
|
848
|
-
sendJson(res, 200, { ok: true, deleted: name, scope });
|
|
849
|
-
return;
|
|
850
|
-
}
|
|
851
|
-
const mcpToggle = pathname.match(/^\/v1\/mcp\/([^/]+)\/(enable|disable)$/);
|
|
852
|
-
if (req.method === "POST" && mcpToggle) {
|
|
853
|
-
const name = decodePathSegment(mcpToggle[1]);
|
|
854
|
-
if (!name) {
|
|
855
|
-
sendJson(res, 400, { error: "missing_name" });
|
|
856
|
-
return;
|
|
857
|
-
}
|
|
858
|
-
const enabled = mcpToggle[2] === "enable";
|
|
859
|
-
const parsed = await readJsonBody(req);
|
|
860
|
-
const scope = parsed.ok && parsed.value.scope === "project" ? "project" : "global";
|
|
861
|
-
const { loadMcpConfigFile, saveMcpConfig } = await import("./mcp.js");
|
|
862
|
-
const config = loadMcpConfigFile(scope);
|
|
863
|
-
if (!config.mcpServers[name]) {
|
|
864
|
-
const known = loadMcpConfigEntries().some((e) => e.name === name);
|
|
865
|
-
sendJson(res, 404, {
|
|
866
|
-
error: "mcp_not_found",
|
|
867
|
-
name,
|
|
868
|
-
scope,
|
|
869
|
-
...(known ? { hint: `server exists in another scope; pass the matching scope` } : { available: Object.keys(config.mcpServers) }),
|
|
870
|
-
});
|
|
871
|
-
return;
|
|
872
|
-
}
|
|
873
|
-
config.mcpServers[name].enabled = enabled;
|
|
874
|
-
saveMcpConfig(config, scope);
|
|
875
|
-
await syncMcpConnections();
|
|
876
|
-
sendJson(res, 200, { ok: true, name, enabled, scope, connected: getMcpStatus()[name]?.connected ?? false });
|
|
877
|
-
return;
|
|
878
|
-
}
|
|
879
|
-
// ─── Context / Compact ─────────────────────────────────────────────
|
|
880
|
-
if (req.method === "GET" && pathname === "/v1/context") {
|
|
881
|
-
const config = loadConfig();
|
|
882
|
-
const provider = getActiveProvider(config);
|
|
883
|
-
const ctxWindow = await getContextWindow(provider?.defaultModel);
|
|
884
|
-
sendJson(res, 200, {
|
|
885
|
-
context_window: ctxWindow,
|
|
886
|
-
model: provider?.defaultModel ?? null,
|
|
887
|
-
});
|
|
888
|
-
return;
|
|
889
|
-
}
|
|
890
|
-
if (req.method === "GET" && pathname === "/v1/cost") {
|
|
891
|
-
const config = loadConfig();
|
|
892
|
-
const provider = getActiveProvider(config);
|
|
893
|
-
const modelId = url.searchParams.get("model") ?? provider?.defaultModel ?? null;
|
|
894
|
-
const inputRaw = url.searchParams.get("input");
|
|
895
|
-
const outputRaw = url.searchParams.get("output");
|
|
896
|
-
const input = inputRaw ? parseInt(inputRaw, 10) : 0;
|
|
897
|
-
const output = outputRaw ? parseInt(outputRaw, 10) : 0;
|
|
898
|
-
if (Number.isNaN(input) || Number.isNaN(output) || input < 0 || output < 0) {
|
|
899
|
-
sendJson(res, 400, { error: "invalid_tokens", detail: "input/output must be non-negative integers" });
|
|
900
|
-
return;
|
|
901
|
-
}
|
|
902
|
-
const price = modelId ? await getModelPrice(modelId) : null;
|
|
903
|
-
const cost = price ? estimateCost({ inputTokens: input, outputTokens: output }, price) : null;
|
|
904
|
-
sendJson(res, 200, { model: modelId, price, cost_usd: cost });
|
|
905
|
-
return;
|
|
906
|
-
}
|
|
907
|
-
if (req.method === "POST" && pathname === "/v1/chat/compact") {
|
|
908
|
-
const parsed = await readJsonBody(req);
|
|
909
|
-
if (!parsed.ok) {
|
|
910
|
-
sendJsonError(res, parsed);
|
|
911
|
-
return;
|
|
912
|
-
}
|
|
913
|
-
const body = parsed.value;
|
|
914
|
-
if (!body.session_id) {
|
|
915
|
-
sendJson(res, 400, { error: "missing_session_id" });
|
|
916
|
-
return;
|
|
917
|
-
}
|
|
918
|
-
const session = loadSession(body.session_id);
|
|
919
|
-
if (!session) {
|
|
920
|
-
sendJson(res, 404, { error: "session_not_found" });
|
|
921
|
-
return;
|
|
922
|
-
}
|
|
923
|
-
const model = resolveModel();
|
|
924
|
-
const result = await compactMessages(session.messages, model, { autoContinue: false });
|
|
925
|
-
if (result.compacted) {
|
|
926
|
-
saveSession(result.messages, body.session_id);
|
|
927
|
-
}
|
|
928
|
-
sendJson(res, 200, { ok: true, compacted: result.compacted, message_count: result.messages.length });
|
|
929
|
-
return;
|
|
930
|
-
}
|
|
931
|
-
if (req.method === "POST" && pathname === "/v1/chat/undo") {
|
|
932
|
-
const parsed = await readJsonBody(req);
|
|
933
|
-
if (!parsed.ok) {
|
|
934
|
-
sendJsonError(res, parsed);
|
|
935
|
-
return;
|
|
936
|
-
}
|
|
937
|
-
const body = parsed.value;
|
|
938
|
-
if (!body.session_id) {
|
|
939
|
-
sendJson(res, 400, { error: "missing_session_id" });
|
|
940
|
-
return;
|
|
941
|
-
}
|
|
942
|
-
const session = loadSession(body.session_id);
|
|
943
|
-
if (!session) {
|
|
944
|
-
sendJson(res, 404, { error: "session_not_found" });
|
|
945
|
-
return;
|
|
946
|
-
}
|
|
947
|
-
let userIdx = -1;
|
|
948
|
-
for (let i = session.messages.length - 1; i >= 0; i--) {
|
|
949
|
-
if (session.messages[i].role === "user") {
|
|
950
|
-
userIdx = i;
|
|
951
|
-
break;
|
|
952
|
-
}
|
|
953
|
-
}
|
|
954
|
-
if (userIdx === -1) {
|
|
955
|
-
sendJson(res, 400, { error: "no_user_message" });
|
|
109
|
+
for (const route of routes) {
|
|
110
|
+
if (await route(req, res, ctx, url, pathname))
|
|
956
111
|
return;
|
|
957
|
-
}
|
|
958
|
-
session.messages = session.messages.slice(0, userIdx + 1);
|
|
959
|
-
saveSession(session.messages, body.session_id);
|
|
960
|
-
sendJson(res, 200, { ok: true, message_count: session.messages.length, messages: session.messages });
|
|
961
|
-
return;
|
|
962
112
|
}
|
|
963
113
|
sendJson(res, 404, { error: "not_found", path: pathname });
|
|
964
114
|
}
|