min-agent 0.2.1 → 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 +242 -31
- package/dist/agent.js +1233 -485
- package/dist/assistant-stream.js +11 -7
- 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 +7 -632
- package/dist/clipboard.js +59 -23
- package/dist/code-mode.js +35 -17
- package/dist/compaction.js +457 -169
- package/dist/config.js +298 -38
- package/dist/confirm.js +105 -9
- package/dist/context-window.js +156 -75
- package/dist/doom-loop.js +268 -26
- package/dist/fetch-timeout.js +152 -0
- package/dist/http-approvals.js +60 -0
- package/dist/http.js +119 -0
- package/dist/instructions.js +72 -33
- package/dist/logger.js +95 -0
- package/dist/markdown.js +35 -50
- package/dist/mcp.js +847 -102
- package/dist/memory.js +128 -45
- package/dist/output.js +42 -31
- package/dist/paste-handler.js +3 -3
- package/dist/permission-cli.js +43 -0
- package/dist/plugins.js +76 -11
- package/dist/pricing.js +119 -0
- package/dist/provider.js +34 -15
- 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 +74 -635
- package/dist/sessions.js +197 -15
- package/dist/skills.js +531 -77
- package/dist/synthetic.js +7 -0
- package/dist/title-gen.js +9 -2
- package/dist/token-display.js +36 -0
- package/dist/tool-display.js +178 -0
- package/dist/tool-output.js +53 -46
- package/dist/tools/apply_patch.js +265 -0
- package/dist/tools/atomic-file.js +35 -0
- package/dist/tools/backend.js +61 -0
- package/dist/tools/bash.js +186 -71
- package/dist/tools/code_search.js +13 -6
- package/dist/tools/edit.js +26 -9
- package/dist/tools/explore.js +144 -16
- package/dist/tools/glob.js +7 -3
- package/dist/tools/grep.js +153 -14
- package/dist/tools/index.js +9 -24
- package/dist/tools/question.js +31 -30
- package/dist/tools/read.js +77 -15
- package/dist/tools/search-searxng.js +223 -0
- package/dist/tools/search-serper.js +189 -0
- package/dist/tools/task.js +100 -33
- package/dist/tools/todo.js +178 -67
- package/dist/tools/web_fetch.js +158 -46
- package/dist/tools/web_search.js +217 -29
- package/dist/tools/write.js +34 -11
- package/dist/tui/App.js +89 -6
- package/dist/tui/ConfirmBar.js +57 -4
- package/dist/tui/InputBar.js +504 -44
- package/dist/tui/MessageList.js +674 -20
- package/dist/tui/ModelPicker.js +113 -0
- package/dist/tui/QuestionBar.js +136 -0
- package/dist/tui/SessionPicker.js +79 -0
- package/dist/tui/StatusBar.js +14 -12
- package/dist/tui/agent-runner.js +223 -0
- package/dist/tui/caret-pos.js +177 -0
- package/dist/tui/caret.js +69 -0
- package/dist/tui/click-count.js +13 -0
- package/dist/tui/diff-view.js +61 -0
- package/dist/tui/drag-state.js +49 -0
- package/dist/tui/hydrate.js +129 -0
- package/dist/tui/index.js +189 -31
- package/dist/tui/input-history.js +125 -0
- package/dist/tui/layout.js +88 -0
- package/dist/tui/mouse.js +46 -0
- package/dist/tui/prompt-queue.js +24 -0
- package/dist/tui/selection.js +226 -0
- package/dist/tui/session-switch.js +28 -0
- package/dist/tui/slash-commands.js +106 -0
- package/dist/tui/slash-handler.js +545 -0
- package/dist/tui/text-width.js +113 -0
- package/dist/tui/theme.js +12 -0
- package/dist/tui/token-info.js +7 -0
- package/dist/tui/tool-children.js +19 -0
- package/dist/tui/undo-stack.js +14 -0
- package/dist/tui/use-sgr-mouse.js +29 -0
- package/dist/tui-chat.js +346 -330
- package/dist/updater.js +116 -0
- package/dist/xml-search.js +194 -0
- package/docs/API.md +410 -32
- package/docs/superpowers/plans/2026-08-16-batch1-tui-improvements.md +1510 -0
- package/docs/superpowers/plans/2026-08-16-batch2-cli-tools-api.md +2105 -0
- package/docs/superpowers/plans/2026-08-16-batch3-config-engineering.md +1595 -0
- package/docs/superpowers/plans/2026-08-16-input-caret.md +782 -0
- 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-16-batch1-tui-improvements-design.md +183 -0
- package/docs/superpowers/specs/2026-08-16-batch2-cli-tools-api-design.md +220 -0
- package/docs/superpowers/specs/2026-08-16-batch3-config-engineering-design.md +196 -0
- package/docs/superpowers/specs/2026-08-16-input-caret-design.md +63 -0
- package/docs/superpowers/specs/2026-08-17-mouse-selection-design.md +116 -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 +12 -8
- package/skills/self-config/SKILL.md +90 -0
- package/skills/self-config/reference.md +149 -0
package/dist/serve.js
CHANGED
|
@@ -3,42 +3,40 @@
|
|
|
3
3
|
* Run: min-agent serve [--host 127.0.0.1] [--port 8787]
|
|
4
4
|
*/
|
|
5
5
|
import { createServer } from "http";
|
|
6
|
-
import {
|
|
7
|
-
import
|
|
8
|
-
import {
|
|
9
|
-
import { discoverSkills, getSkills } from "./skills.js";
|
|
6
|
+
import { timingSafeEqual } from "crypto";
|
|
7
|
+
import { initMcp, shutdownMcp } from "./mcp.js";
|
|
8
|
+
import { discoverSkills } from "./skills.js";
|
|
10
9
|
import { loadInstructions } from "./instructions.js";
|
|
11
|
-
import {
|
|
10
|
+
import { isConfigured } from "./config.js";
|
|
12
11
|
import { setAutoApprove } from "./confirm.js";
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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";
|
|
20
|
+
/**
|
|
21
|
+
* With CORS enabled and no auth token, arbitrary web pages could drive the
|
|
22
|
+
* agent. Allow only same-machine origins (localhost) in that configuration;
|
|
23
|
+
* a token bypasses the origin check entirely.
|
|
24
|
+
*/
|
|
25
|
+
function originAllowed(req) {
|
|
26
|
+
if (process.env.MIN_AGENT_SERVE_TOKEN?.trim())
|
|
27
|
+
return true;
|
|
28
|
+
const origin = req.headers.origin?.trim();
|
|
29
|
+
if (!origin || origin === "null")
|
|
30
|
+
return true;
|
|
23
31
|
try {
|
|
24
|
-
const
|
|
25
|
-
if (
|
|
26
|
-
|
|
27
|
-
return j.version ?? "0.0.0";
|
|
28
|
-
}
|
|
32
|
+
const host = new URL(origin).hostname;
|
|
33
|
+
if (host === "localhost" || host === "127.0.0.1" || host === "[::1]")
|
|
34
|
+
return true;
|
|
29
35
|
}
|
|
30
|
-
catch {
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
function corsHeaders() {
|
|
34
|
-
if (process.env.MIN_AGENT_SERVE_CORS === "1" || process.env.MIN_AGENT_SERVE_CORS === "true") {
|
|
35
|
-
return {
|
|
36
|
-
"Access-Control-Allow-Origin": "*",
|
|
37
|
-
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
38
|
-
"Access-Control-Allow-Headers": "Content-Type, Authorization",
|
|
39
|
-
};
|
|
36
|
+
catch {
|
|
37
|
+
return false;
|
|
40
38
|
}
|
|
41
|
-
return
|
|
39
|
+
return false;
|
|
42
40
|
}
|
|
43
41
|
function authOk(req) {
|
|
44
42
|
const token = process.env.MIN_AGENT_SERVE_TOKEN?.trim();
|
|
@@ -47,57 +45,9 @@ function authOk(req) {
|
|
|
47
45
|
const h = req.headers.authorization?.trim();
|
|
48
46
|
if (!h?.startsWith("Bearer "))
|
|
49
47
|
return false;
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const headers = {
|
|
54
|
-
"Content-Type": "application/json; charset=utf-8",
|
|
55
|
-
...corsHeaders(),
|
|
56
|
-
};
|
|
57
|
-
res.writeHead(status, headers);
|
|
58
|
-
res.end(JSON.stringify(body));
|
|
59
|
-
}
|
|
60
|
-
function readBody(req) {
|
|
61
|
-
return new Promise((resolve, reject) => {
|
|
62
|
-
const chunks = [];
|
|
63
|
-
let total = 0;
|
|
64
|
-
req.on("data", (c) => {
|
|
65
|
-
total += c.length;
|
|
66
|
-
if (total > MAX_BODY_BYTES) {
|
|
67
|
-
reject(new Error("body_too_large"));
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
chunks.push(c);
|
|
71
|
-
});
|
|
72
|
-
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
|
|
73
|
-
req.on("error", reject);
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
function sseWrite(res, obj) {
|
|
77
|
-
try {
|
|
78
|
-
if (res.writableEnded)
|
|
79
|
-
return;
|
|
80
|
-
res.write(`data: ${JSON.stringify(obj)}\n\n`);
|
|
81
|
-
}
|
|
82
|
-
catch {
|
|
83
|
-
/* client gone */
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
function truncateForJson(v, max) {
|
|
87
|
-
if (typeof v === "string" && v.length > max)
|
|
88
|
-
return v.slice(0, max) + `\n… [truncated ${v.length - max} chars]`;
|
|
89
|
-
return v;
|
|
90
|
-
}
|
|
91
|
-
function normalizeMessages(body) {
|
|
92
|
-
if (body.messages && Array.isArray(body.messages)) {
|
|
93
|
-
if (body.messages.length === 0)
|
|
94
|
-
return { ok: false, error: "messages must be non-empty" };
|
|
95
|
-
return { ok: true, messages: [...body.messages] };
|
|
96
|
-
}
|
|
97
|
-
if (typeof body.message === "string" && body.message.length > 0) {
|
|
98
|
-
return { ok: true, messages: [{ role: "user", content: body.message }] };
|
|
99
|
-
}
|
|
100
|
-
return { ok: false, error: "Provide `message` (string) or non-empty `messages` array" };
|
|
48
|
+
const provided = Buffer.from(h.slice(7));
|
|
49
|
+
const expected = Buffer.from(token);
|
|
50
|
+
return provided.length === expected.length && timingSafeEqual(provided, expected);
|
|
101
51
|
}
|
|
102
52
|
export async function runServe(opts = {}) {
|
|
103
53
|
if (!isConfigured()) {
|
|
@@ -106,13 +56,32 @@ export async function runServe(opts = {}) {
|
|
|
106
56
|
}
|
|
107
57
|
const host = opts.host ?? process.env.MIN_AGENT_SERVE_HOST ?? "127.0.0.1";
|
|
108
58
|
const port = opts.port ?? parseInt(process.env.MIN_AGENT_SERVE_PORT ?? "8787", 10);
|
|
109
|
-
|
|
59
|
+
const isLoopbackHost = host === "127.0.0.1" || host === "::1" || host === "localhost";
|
|
60
|
+
if (!isLoopbackHost && !process.env.MIN_AGENT_SERVE_TOKEN?.trim()) {
|
|
61
|
+
console.error("Refusing to serve on a non-loopback address without MIN_AGENT_SERVE_TOKEN. " +
|
|
62
|
+
"Set the token (Authorization: Bearer <token>) or bind to 127.0.0.1.");
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
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");
|
|
110
66
|
setAutoApprove(true);
|
|
111
67
|
console.error("\x1b[90m⟳ Initializing MCP, skills, instructions…\x1b[0m");
|
|
112
68
|
await initMcp();
|
|
113
69
|
discoverSkills();
|
|
114
|
-
|
|
115
|
-
|
|
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
|
+
];
|
|
116
85
|
const server = createServer(async (req, res) => {
|
|
117
86
|
const c = corsHeaders();
|
|
118
87
|
if (req.method === "OPTIONS") {
|
|
@@ -121,560 +90,25 @@ export async function runServe(opts = {}) {
|
|
|
121
90
|
return;
|
|
122
91
|
}
|
|
123
92
|
if (!authOk(req)) {
|
|
124
|
-
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
|
+
});
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (corsEnabled() && !originAllowed(req)) {
|
|
100
|
+
sendJson(res, 403, {
|
|
101
|
+
error: "origin_not_allowed",
|
|
102
|
+
detail: "CORS is enabled without a token; only localhost origins are allowed. Set MIN_AGENT_SERVE_TOKEN or disable CORS.",
|
|
103
|
+
});
|
|
125
104
|
return;
|
|
126
105
|
}
|
|
127
106
|
const url = new URL(req.url ?? "/", `http://${host}`);
|
|
128
107
|
const pathname = url.pathname.replace(/\/$/, "") || "/";
|
|
129
108
|
try {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
if (req.method === "GET" && pathname === "/v1/meta") {
|
|
135
|
-
sendJson(res, 200, {
|
|
136
|
-
version,
|
|
137
|
-
cwd: process.cwd(),
|
|
138
|
-
instructions_chars: instructions.join("\n").length,
|
|
139
|
-
});
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
if (req.method === "GET" && pathname === "/v1/models") {
|
|
143
|
-
const config = loadConfig();
|
|
144
|
-
const base = config.provider?.baseURL;
|
|
145
|
-
const key = config.provider?.apiKey;
|
|
146
|
-
if (!base || !key) {
|
|
147
|
-
sendJson(res, 500, { error: "provider_not_configured" });
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
const models = await fetchModels(base, key);
|
|
151
|
-
sendJson(res, 200, {
|
|
152
|
-
default_model: config.provider?.defaultModel ?? null,
|
|
153
|
-
models,
|
|
154
|
-
});
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
if (req.method === "POST" && pathname === "/v1/chat/reload-instructions") {
|
|
158
|
-
instructions = await loadInstructions();
|
|
159
|
-
sendJson(res, 200, { ok: true, instructions_chars: instructions.join("\n").length });
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
162
|
-
if (req.method === "POST" && pathname === "/v1/chat") {
|
|
163
|
-
if (req.headers["content-type"]?.split(";")[0]?.trim() !== "application/json") {
|
|
164
|
-
sendJson(res, 415, { error: "unsupported_media_type", detail: "Use Content-Type: application/json" });
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
let raw;
|
|
168
|
-
try {
|
|
169
|
-
raw = await readBody(req);
|
|
170
|
-
}
|
|
171
|
-
catch (e) {
|
|
172
|
-
if (e?.message === "body_too_large") {
|
|
173
|
-
sendJson(res, 413, { error: "payload_too_large", max_bytes: MAX_BODY_BYTES });
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
throw e;
|
|
177
|
-
}
|
|
178
|
-
let body;
|
|
179
|
-
try {
|
|
180
|
-
body = JSON.parse(raw);
|
|
181
|
-
}
|
|
182
|
-
catch {
|
|
183
|
-
sendJson(res, 400, { error: "invalid_json" });
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
186
|
-
const modelId = typeof body.model === "string" ? body.model : undefined;
|
|
187
|
-
const stream = body.stream === true;
|
|
188
|
-
const sessionId = typeof body.session_id === "string" ? body.session_id : undefined;
|
|
189
|
-
let messages;
|
|
190
|
-
if (sessionId) {
|
|
191
|
-
const session = loadSession(sessionId);
|
|
192
|
-
if (!session) {
|
|
193
|
-
sendJson(res, 404, { error: "session_not_found", session_id: sessionId });
|
|
194
|
-
return;
|
|
195
|
-
}
|
|
196
|
-
if (typeof body.message !== "string" || !body.message.trim()) {
|
|
197
|
-
sendJson(res, 400, {
|
|
198
|
-
error: "session_requires_message",
|
|
199
|
-
detail: "With `session_id`, send a non-empty `message` for the new user turn",
|
|
200
|
-
});
|
|
201
|
-
return;
|
|
202
|
-
}
|
|
203
|
-
messages = [...session.messages];
|
|
204
|
-
const content = body.images && body.images.length > 0 ? await buildUserContent(body.message, body.images) : body.message;
|
|
205
|
-
messages.push({ role: "user", content });
|
|
206
|
-
}
|
|
207
|
-
else {
|
|
208
|
-
const norm = normalizeMessages(body);
|
|
209
|
-
if (!norm.ok) {
|
|
210
|
-
sendJson(res, 400, { error: "invalid_body", detail: norm.error });
|
|
211
|
-
return;
|
|
212
|
-
}
|
|
213
|
-
messages = norm.messages;
|
|
214
|
-
if (body.images && body.images.length > 0) {
|
|
215
|
-
const last = messages[messages.length - 1];
|
|
216
|
-
if (!last || last.role !== "user" || typeof body.message !== "string") {
|
|
217
|
-
sendJson(res, 400, {
|
|
218
|
-
error: "images_require_message",
|
|
219
|
-
detail: "With `images`, send a top-level `message` string for the user turn",
|
|
220
|
-
});
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
messages[messages.length - 1] = {
|
|
224
|
-
role: "user",
|
|
225
|
-
content: await buildUserContent(body.message, body.images),
|
|
226
|
-
};
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
const abort = new AbortController();
|
|
230
|
-
req.on("close", () => abort.abort());
|
|
231
|
-
if (stream) {
|
|
232
|
-
res.writeHead(200, {
|
|
233
|
-
"Content-Type": "text/event-stream; charset=utf-8",
|
|
234
|
-
"Cache-Control": "no-cache, no-transform",
|
|
235
|
-
Connection: "keep-alive",
|
|
236
|
-
"X-Accel-Buffering": "no",
|
|
237
|
-
...c,
|
|
238
|
-
});
|
|
239
|
-
res.flushHeaders?.();
|
|
240
|
-
const toolCalls = [];
|
|
241
|
-
const toolResults = [];
|
|
242
|
-
const callbacks = {
|
|
243
|
-
onAssistantDisplayDelta(delta) {
|
|
244
|
-
sseWrite(res, { type: "assistant", text: delta });
|
|
245
|
-
},
|
|
246
|
-
onThinkingDelta(delta) {
|
|
247
|
-
sseWrite(res, { type: "thinking", text: delta });
|
|
248
|
-
},
|
|
249
|
-
onToolCall(name, input) {
|
|
250
|
-
toolCalls.push({ name, input });
|
|
251
|
-
sseWrite(res, { type: "tool_call", name, input });
|
|
252
|
-
},
|
|
253
|
-
onToolResult(name, output) {
|
|
254
|
-
const out = truncateForJson(output, MAX_TOOL_RESULT_SSE_CHARS);
|
|
255
|
-
toolResults.push({ name, output: out });
|
|
256
|
-
sseWrite(res, { type: "tool_result", name, output: out });
|
|
257
|
-
},
|
|
258
|
-
onCompaction(line) {
|
|
259
|
-
sseWrite(res, { type: "compaction", line });
|
|
260
|
-
},
|
|
261
|
-
onStreamError(message) {
|
|
262
|
-
sseWrite(res, { type: "error", message });
|
|
263
|
-
},
|
|
264
|
-
onRunFinish(info) {
|
|
265
|
-
let saved;
|
|
266
|
-
if (sessionId && messages.length > 0) {
|
|
267
|
-
try {
|
|
268
|
-
saved = saveSession(messages, sessionId);
|
|
269
|
-
}
|
|
270
|
-
catch {
|
|
271
|
-
saved = undefined;
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
sseWrite(res, {
|
|
275
|
-
type: "done",
|
|
276
|
-
step_count: info.stepCount,
|
|
277
|
-
usage: info.usage,
|
|
278
|
-
has_error: info.hasError,
|
|
279
|
-
aborted: info.aborted,
|
|
280
|
-
session_id: saved,
|
|
281
|
-
messages,
|
|
282
|
-
});
|
|
283
|
-
if (!res.writableEnded)
|
|
284
|
-
res.end();
|
|
285
|
-
},
|
|
286
|
-
};
|
|
287
|
-
try {
|
|
288
|
-
await runOnce(messages, instructions, modelId, abort.signal, callbacks);
|
|
289
|
-
}
|
|
290
|
-
catch (err) {
|
|
291
|
-
if (!res.writableEnded) {
|
|
292
|
-
sseWrite(res, { type: "fatal", message: err?.message ?? String(err) });
|
|
293
|
-
res.end();
|
|
294
|
-
}
|
|
295
|
-
}
|
|
109
|
+
for (const route of routes) {
|
|
110
|
+
if (await route(req, res, ctx, url, pathname))
|
|
296
111
|
return;
|
|
297
|
-
}
|
|
298
|
-
const toolCalls = [];
|
|
299
|
-
const toolResults = [];
|
|
300
|
-
const finishBox = { info: null };
|
|
301
|
-
await runOnce(messages, instructions, modelId, abort.signal, {
|
|
302
|
-
onToolCall(name, input) {
|
|
303
|
-
toolCalls.push({ name, input });
|
|
304
|
-
},
|
|
305
|
-
onToolResult(name, output) {
|
|
306
|
-
toolResults.push({ name, output: truncateForJson(output, MAX_TOOL_RESULT_SSE_CHARS) });
|
|
307
|
-
},
|
|
308
|
-
onRunFinish(info) {
|
|
309
|
-
finishBox.info = info;
|
|
310
|
-
},
|
|
311
|
-
});
|
|
312
|
-
const lastAssistant = [...messages].reverse().find((m) => m.role === "assistant");
|
|
313
|
-
let savedSession;
|
|
314
|
-
if (sessionId && messages.length > 0) {
|
|
315
|
-
try {
|
|
316
|
-
savedSession = saveSession(messages, sessionId);
|
|
317
|
-
}
|
|
318
|
-
catch {
|
|
319
|
-
savedSession = undefined;
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
const fi = finishBox.info;
|
|
323
|
-
sendJson(res, 200, {
|
|
324
|
-
messages,
|
|
325
|
-
assistant: lastAssistant ?? null,
|
|
326
|
-
tool_calls: toolCalls,
|
|
327
|
-
tool_results: toolResults,
|
|
328
|
-
session_id: savedSession,
|
|
329
|
-
step_count: fi?.stepCount ?? 0,
|
|
330
|
-
usage: fi?.usage ?? null,
|
|
331
|
-
has_error: fi?.hasError ?? false,
|
|
332
|
-
aborted: fi?.aborted ?? false,
|
|
333
|
-
});
|
|
334
|
-
return;
|
|
335
|
-
}
|
|
336
|
-
// ─── Sessions ──────────────────────────────────────────────────────
|
|
337
|
-
if (req.method === "GET" && pathname === "/v1/sessions") {
|
|
338
|
-
const sessions = listSessions();
|
|
339
|
-
sendJson(res, 200, { sessions });
|
|
340
|
-
return;
|
|
341
|
-
}
|
|
342
|
-
if (req.method === "DELETE" && pathname.startsWith("/v1/sessions/")) {
|
|
343
|
-
const id = pathname.slice("/v1/sessions/".length);
|
|
344
|
-
if (!id) {
|
|
345
|
-
sendJson(res, 400, { error: "missing_session_id" });
|
|
346
|
-
return;
|
|
347
|
-
}
|
|
348
|
-
const ok = deleteSession(id);
|
|
349
|
-
if (!ok) {
|
|
350
|
-
sendJson(res, 404, { error: "session_not_found", session_id: id });
|
|
351
|
-
return;
|
|
352
|
-
}
|
|
353
|
-
sendJson(res, 200, { ok: true, deleted: id });
|
|
354
|
-
return;
|
|
355
|
-
}
|
|
356
|
-
// ─── Memory ────────────────────────────────────────────────────────
|
|
357
|
-
if (req.method === "GET" && pathname === "/v1/memory") {
|
|
358
|
-
const memories = loadMemories();
|
|
359
|
-
sendJson(res, 200, { memories });
|
|
360
|
-
return;
|
|
361
|
-
}
|
|
362
|
-
if (req.method === "POST" && pathname === "/v1/memory") {
|
|
363
|
-
const raw = await readBody(req);
|
|
364
|
-
const body = JSON.parse(raw);
|
|
365
|
-
if (!body.content) {
|
|
366
|
-
sendJson(res, 400, { error: "missing_content" });
|
|
367
|
-
return;
|
|
368
|
-
}
|
|
369
|
-
const memory = addMemory(body.content, body.tags ?? []);
|
|
370
|
-
sendJson(res, 201, { ok: true, memory });
|
|
371
|
-
return;
|
|
372
|
-
}
|
|
373
|
-
if (req.method === "GET" && pathname === "/v1/memory/search") {
|
|
374
|
-
const query = url.searchParams.get("q") ?? "";
|
|
375
|
-
if (!query) {
|
|
376
|
-
sendJson(res, 400, { error: "missing_query_param_q" });
|
|
377
|
-
return;
|
|
378
|
-
}
|
|
379
|
-
const results = searchMemories(query);
|
|
380
|
-
sendJson(res, 200, { results });
|
|
381
|
-
return;
|
|
382
|
-
}
|
|
383
|
-
if (req.method === "DELETE" && pathname.startsWith("/v1/memory/")) {
|
|
384
|
-
const idx = parseInt(pathname.slice("/v1/memory/".length));
|
|
385
|
-
if (isNaN(idx)) {
|
|
386
|
-
sendJson(res, 400, { error: "invalid_index" });
|
|
387
|
-
return;
|
|
388
|
-
}
|
|
389
|
-
const ok = deleteMemory(idx - 1);
|
|
390
|
-
if (!ok) {
|
|
391
|
-
sendJson(res, 404, { error: "memory_not_found", index: idx });
|
|
392
|
-
return;
|
|
393
|
-
}
|
|
394
|
-
sendJson(res, 200, { ok: true, deleted: idx });
|
|
395
|
-
return;
|
|
396
|
-
}
|
|
397
|
-
// ─── MCP ───────────────────────────────────────────────────────────
|
|
398
|
-
if (req.method === "GET" && pathname === "/v1/mcp") {
|
|
399
|
-
const status = getMcpStatus();
|
|
400
|
-
sendJson(res, 200, { servers: status });
|
|
401
|
-
return;
|
|
402
|
-
}
|
|
403
|
-
// ─── Skills ────────────────────────────────────────────────────────
|
|
404
|
-
if (req.method === "GET" && pathname === "/v1/skills") {
|
|
405
|
-
const skills = getSkills();
|
|
406
|
-
sendJson(res, 200, {
|
|
407
|
-
skills: skills.map((s) => ({ name: s.name, description: s.description, location: s.location })),
|
|
408
|
-
});
|
|
409
|
-
return;
|
|
410
|
-
}
|
|
411
|
-
// ─── Rules ─────────────────────────────────────────────────────────
|
|
412
|
-
if (req.method === "GET" && pathname === "/v1/rules") {
|
|
413
|
-
sendJson(res, 200, {
|
|
414
|
-
count: instructions.length,
|
|
415
|
-
rules: instructions.map((inst) => {
|
|
416
|
-
const firstLine = inst.split("\n")[0] ?? "";
|
|
417
|
-
return { source: firstLine.replace("Instructions from: ", ""), chars: inst.length };
|
|
418
|
-
}),
|
|
419
|
-
});
|
|
420
|
-
return;
|
|
421
|
-
}
|
|
422
|
-
// ─── Project (code mode scan) ──────────────────────────────────────
|
|
423
|
-
if (req.method === "GET" && pathname === "/v1/project") {
|
|
424
|
-
const project = scanProject();
|
|
425
|
-
sendJson(res, 200, { project });
|
|
426
|
-
return;
|
|
427
|
-
}
|
|
428
|
-
// ─── Code mode chat ────────────────────────────────────────────────
|
|
429
|
-
if (req.method === "POST" && pathname === "/v1/code") {
|
|
430
|
-
if (req.headers["content-type"]?.split(";")[0]?.trim() !== "application/json") {
|
|
431
|
-
sendJson(res, 415, { error: "unsupported_media_type" });
|
|
432
|
-
return;
|
|
433
|
-
}
|
|
434
|
-
let raw;
|
|
435
|
-
try {
|
|
436
|
-
raw = await readBody(req);
|
|
437
|
-
}
|
|
438
|
-
catch {
|
|
439
|
-
sendJson(res, 413, { error: "payload_too_large" });
|
|
440
|
-
return;
|
|
441
|
-
}
|
|
442
|
-
let body;
|
|
443
|
-
try {
|
|
444
|
-
body = JSON.parse(raw);
|
|
445
|
-
}
|
|
446
|
-
catch {
|
|
447
|
-
sendJson(res, 400, { error: "invalid_json" });
|
|
448
|
-
return;
|
|
449
|
-
}
|
|
450
|
-
const modelId = typeof body.model === "string" ? body.model : undefined;
|
|
451
|
-
const stream = body.stream === true;
|
|
452
|
-
const sessionId = typeof body.session_id === "string" ? body.session_id : undefined;
|
|
453
|
-
// Build code-mode system prompt
|
|
454
|
-
const { buildCodeSystemPrompt } = await import("./code-mode.js");
|
|
455
|
-
const project = scanProject();
|
|
456
|
-
const codeSystemPrompt = buildCodeSystemPrompt(project, instructions);
|
|
457
|
-
let messages;
|
|
458
|
-
if (sessionId) {
|
|
459
|
-
const session = loadSession(sessionId);
|
|
460
|
-
if (!session) {
|
|
461
|
-
sendJson(res, 404, { error: "session_not_found", session_id: sessionId });
|
|
462
|
-
return;
|
|
463
|
-
}
|
|
464
|
-
if (!body.message) {
|
|
465
|
-
sendJson(res, 400, { error: "session_requires_message" });
|
|
466
|
-
return;
|
|
467
|
-
}
|
|
468
|
-
messages = [...session.messages];
|
|
469
|
-
const content = body.images?.length ? await buildUserContent(body.message, body.images) : body.message;
|
|
470
|
-
messages.push({ role: "user", content });
|
|
471
|
-
}
|
|
472
|
-
else {
|
|
473
|
-
const norm = normalizeMessages(body);
|
|
474
|
-
if (!norm.ok) {
|
|
475
|
-
sendJson(res, 400, { error: "invalid_body", detail: norm.error });
|
|
476
|
-
return;
|
|
477
|
-
}
|
|
478
|
-
messages = norm.messages;
|
|
479
|
-
}
|
|
480
|
-
const abort = new AbortController();
|
|
481
|
-
req.on("close", () => abort.abort());
|
|
482
|
-
// Use runOnce with code system prompt override
|
|
483
|
-
const { runOnceWithSystem } = await import("./agent.js");
|
|
484
|
-
if (stream) {
|
|
485
|
-
res.writeHead(200, { "Content-Type": "text/event-stream; charset=utf-8", "Cache-Control": "no-cache", Connection: "keep-alive", ...c });
|
|
486
|
-
res.flushHeaders?.();
|
|
487
|
-
const callbacks = {
|
|
488
|
-
onAssistantDisplayDelta(delta) { sseWrite(res, { type: "assistant", text: delta }); },
|
|
489
|
-
onThinkingDelta(delta) { sseWrite(res, { type: "thinking", text: delta }); },
|
|
490
|
-
onToolCall(name, input) { sseWrite(res, { type: "tool_call", name, input }); },
|
|
491
|
-
onToolResult(name, output) { sseWrite(res, { type: "tool_result", name, output: truncateForJson(output, MAX_TOOL_RESULT_SSE_CHARS) }); },
|
|
492
|
-
onCompaction(line) { sseWrite(res, { type: "compaction", line }); },
|
|
493
|
-
onStreamError(message) { sseWrite(res, { type: "error", message }); },
|
|
494
|
-
onRunFinish(info) {
|
|
495
|
-
let saved;
|
|
496
|
-
if (sessionId && messages.length > 0) {
|
|
497
|
-
try {
|
|
498
|
-
saved = saveSession(messages, sessionId);
|
|
499
|
-
}
|
|
500
|
-
catch { }
|
|
501
|
-
}
|
|
502
|
-
sseWrite(res, { type: "done", step_count: info.stepCount, usage: info.usage, has_error: info.hasError, session_id: saved, messages, mode: "code", project });
|
|
503
|
-
if (!res.writableEnded)
|
|
504
|
-
res.end();
|
|
505
|
-
},
|
|
506
|
-
};
|
|
507
|
-
try {
|
|
508
|
-
await runOnceWithSystem(messages, codeSystemPrompt, modelId, abort.signal, callbacks);
|
|
509
|
-
}
|
|
510
|
-
catch (err) {
|
|
511
|
-
if (!res.writableEnded) {
|
|
512
|
-
sseWrite(res, { type: "fatal", message: err?.message ?? String(err) });
|
|
513
|
-
res.end();
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
else {
|
|
518
|
-
const toolCalls = [];
|
|
519
|
-
const toolResults = [];
|
|
520
|
-
const finishBox = { info: null };
|
|
521
|
-
await runOnceWithSystem(messages, codeSystemPrompt, modelId, abort.signal, {
|
|
522
|
-
onToolCall(name, input) { toolCalls.push({ name, input }); },
|
|
523
|
-
onToolResult(name, output) { toolResults.push({ name, output: truncateForJson(output, MAX_TOOL_RESULT_SSE_CHARS) }); },
|
|
524
|
-
onRunFinish(info) { finishBox.info = info; },
|
|
525
|
-
});
|
|
526
|
-
const lastAssistant = [...messages].reverse().find((m) => m.role === "assistant");
|
|
527
|
-
let saved;
|
|
528
|
-
if (sessionId && messages.length > 0) {
|
|
529
|
-
try {
|
|
530
|
-
saved = saveSession(messages, sessionId);
|
|
531
|
-
}
|
|
532
|
-
catch { }
|
|
533
|
-
}
|
|
534
|
-
const fi = finishBox.info;
|
|
535
|
-
sendJson(res, 200, { mode: "code", project, messages, assistant: lastAssistant ?? null, tool_calls: toolCalls, tool_results: toolResults, session_id: saved, step_count: fi?.stepCount ?? 0, usage: fi?.usage ?? null, has_error: fi?.hasError ?? false });
|
|
536
|
-
}
|
|
537
|
-
return;
|
|
538
|
-
}
|
|
539
|
-
// ─── MCP management ────────────────────────────────────────────────
|
|
540
|
-
if (req.method === "POST" && pathname === "/v1/paste") {
|
|
541
|
-
if (req.headers["content-type"]?.split(";")[0]?.trim() !== "application/json") {
|
|
542
|
-
sendJson(res, 415, { error: "unsupported_media_type" });
|
|
543
|
-
return;
|
|
544
|
-
}
|
|
545
|
-
const raw = await readBody(req);
|
|
546
|
-
const body = JSON.parse(raw);
|
|
547
|
-
if (!body.image_base64) {
|
|
548
|
-
sendJson(res, 400, { error: "missing_image_base64" });
|
|
549
|
-
return;
|
|
550
|
-
}
|
|
551
|
-
const imageBuffer = Buffer.from(body.image_base64, "base64");
|
|
552
|
-
const mimeType = body.mime_type ?? "image/png";
|
|
553
|
-
const text = body.message ?? "What's in this image?";
|
|
554
|
-
const modelId = body.model;
|
|
555
|
-
const sessionId = body.session_id;
|
|
556
|
-
const stream = body.stream === true;
|
|
557
|
-
let messages = [];
|
|
558
|
-
if (sessionId) {
|
|
559
|
-
const session = loadSession(sessionId);
|
|
560
|
-
if (session)
|
|
561
|
-
messages = [...session.messages];
|
|
562
|
-
}
|
|
563
|
-
const content = [
|
|
564
|
-
{ type: "text", text },
|
|
565
|
-
{ type: "image", image: imageBuffer, mimeType },
|
|
566
|
-
];
|
|
567
|
-
messages.push({ role: "user", content });
|
|
568
|
-
const abort = new AbortController();
|
|
569
|
-
req.on("close", () => abort.abort());
|
|
570
|
-
if (stream) {
|
|
571
|
-
res.writeHead(200, { "Content-Type": "text/event-stream; charset=utf-8", "Cache-Control": "no-cache", Connection: "keep-alive", ...c });
|
|
572
|
-
res.flushHeaders?.();
|
|
573
|
-
const callbacks = {
|
|
574
|
-
onAssistantDisplayDelta(delta) { sseWrite(res, { type: "assistant", text: delta }); },
|
|
575
|
-
onThinkingDelta(delta) { sseWrite(res, { type: "thinking", text: delta }); },
|
|
576
|
-
onToolCall(name, input) { sseWrite(res, { type: "tool_call", name, input }); },
|
|
577
|
-
onToolResult(name, output) { sseWrite(res, { type: "tool_result", name, output: truncateForJson(output, MAX_TOOL_RESULT_SSE_CHARS) }); },
|
|
578
|
-
onStreamError(message) { sseWrite(res, { type: "error", message }); },
|
|
579
|
-
onRunFinish(info) {
|
|
580
|
-
let saved;
|
|
581
|
-
if (sessionId) {
|
|
582
|
-
try {
|
|
583
|
-
saved = saveSession(messages, sessionId);
|
|
584
|
-
}
|
|
585
|
-
catch { }
|
|
586
|
-
}
|
|
587
|
-
sseWrite(res, { type: "done", step_count: info.stepCount, usage: info.usage, session_id: saved });
|
|
588
|
-
if (!res.writableEnded)
|
|
589
|
-
res.end();
|
|
590
|
-
},
|
|
591
|
-
};
|
|
592
|
-
await runOnce(messages, instructions, modelId, abort.signal, callbacks);
|
|
593
|
-
}
|
|
594
|
-
else {
|
|
595
|
-
await runOnce(messages, instructions, modelId, abort.signal);
|
|
596
|
-
const lastAssistant = [...messages].reverse().find((m) => m.role === "assistant");
|
|
597
|
-
let saved;
|
|
598
|
-
if (sessionId) {
|
|
599
|
-
try {
|
|
600
|
-
saved = saveSession(messages, sessionId);
|
|
601
|
-
}
|
|
602
|
-
catch { }
|
|
603
|
-
}
|
|
604
|
-
sendJson(res, 200, { messages, assistant: lastAssistant ?? null, session_id: saved });
|
|
605
|
-
}
|
|
606
|
-
return;
|
|
607
|
-
}
|
|
608
|
-
// ─── MCP management (continued) ───────────────────────────────────
|
|
609
|
-
if (req.method === "POST" && pathname === "/v1/mcp") {
|
|
610
|
-
const raw = await readBody(req);
|
|
611
|
-
const body = JSON.parse(raw);
|
|
612
|
-
if (!body.name) {
|
|
613
|
-
sendJson(res, 400, { error: "missing_name" });
|
|
614
|
-
return;
|
|
615
|
-
}
|
|
616
|
-
const { loadMcpConfig, saveMcpConfig } = await import("./mcp.js");
|
|
617
|
-
const config = loadMcpConfig();
|
|
618
|
-
if (body.url) {
|
|
619
|
-
config.mcpServers[body.name] = { url: body.url, token: body.token, enabled: body.enabled !== false };
|
|
620
|
-
}
|
|
621
|
-
else if (body.command && body.command.length > 0) {
|
|
622
|
-
config.mcpServers[body.name] = { command: body.command, enabled: body.enabled !== false };
|
|
623
|
-
}
|
|
624
|
-
else {
|
|
625
|
-
sendJson(res, 400, { error: "provide command[] or url" });
|
|
626
|
-
return;
|
|
627
|
-
}
|
|
628
|
-
saveMcpConfig(config);
|
|
629
|
-
sendJson(res, 201, { ok: true, name: body.name });
|
|
630
|
-
return;
|
|
631
|
-
}
|
|
632
|
-
if (req.method === "DELETE" && pathname.startsWith("/v1/mcp/")) {
|
|
633
|
-
const name = decodeURIComponent(pathname.slice("/v1/mcp/".length));
|
|
634
|
-
if (!name) {
|
|
635
|
-
sendJson(res, 400, { error: "missing_name" });
|
|
636
|
-
return;
|
|
637
|
-
}
|
|
638
|
-
const { loadMcpConfig, saveMcpConfig } = await import("./mcp.js");
|
|
639
|
-
const config = loadMcpConfig();
|
|
640
|
-
if (!config.mcpServers[name]) {
|
|
641
|
-
sendJson(res, 404, { error: "mcp_not_found", name });
|
|
642
|
-
return;
|
|
643
|
-
}
|
|
644
|
-
delete config.mcpServers[name];
|
|
645
|
-
saveMcpConfig(config);
|
|
646
|
-
sendJson(res, 200, { ok: true, deleted: name });
|
|
647
|
-
return;
|
|
648
|
-
}
|
|
649
|
-
// ─── Context / Compact ─────────────────────────────────────────────
|
|
650
|
-
if (req.method === "GET" && pathname === "/v1/context") {
|
|
651
|
-
const config = loadConfig();
|
|
652
|
-
const ctxWindow = await getContextWindow(config.provider?.defaultModel);
|
|
653
|
-
sendJson(res, 200, {
|
|
654
|
-
context_window: ctxWindow,
|
|
655
|
-
model: config.provider?.defaultModel ?? null,
|
|
656
|
-
});
|
|
657
|
-
return;
|
|
658
|
-
}
|
|
659
|
-
if (req.method === "POST" && pathname === "/v1/chat/compact") {
|
|
660
|
-
const raw = await readBody(req);
|
|
661
|
-
const body = JSON.parse(raw);
|
|
662
|
-
if (!body.session_id) {
|
|
663
|
-
sendJson(res, 400, { error: "missing_session_id" });
|
|
664
|
-
return;
|
|
665
|
-
}
|
|
666
|
-
const session = loadSession(body.session_id);
|
|
667
|
-
if (!session) {
|
|
668
|
-
sendJson(res, 404, { error: "session_not_found" });
|
|
669
|
-
return;
|
|
670
|
-
}
|
|
671
|
-
const model = resolveModel();
|
|
672
|
-
const result = await compactMessages(session.messages, model, { autoContinue: false });
|
|
673
|
-
if (result.compacted) {
|
|
674
|
-
saveSession(result.messages, body.session_id);
|
|
675
|
-
}
|
|
676
|
-
sendJson(res, 200, { ok: true, compacted: result.compacted, message_count: result.messages.length });
|
|
677
|
-
return;
|
|
678
112
|
}
|
|
679
113
|
sendJson(res, 404, { error: "not_found", path: pathname });
|
|
680
114
|
}
|
|
@@ -689,11 +123,16 @@ export async function runServe(opts = {}) {
|
|
|
689
123
|
resolve();
|
|
690
124
|
});
|
|
691
125
|
});
|
|
692
|
-
const
|
|
126
|
+
const closeServer = async () => {
|
|
693
127
|
await shutdownMcp();
|
|
694
|
-
server.
|
|
128
|
+
server.closeAllConnections?.();
|
|
129
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
130
|
+
};
|
|
131
|
+
const shutdown = async () => {
|
|
132
|
+
await closeServer();
|
|
695
133
|
process.exit(0);
|
|
696
134
|
};
|
|
697
135
|
process.on("SIGINT", shutdown);
|
|
698
136
|
process.on("SIGTERM", shutdown);
|
|
137
|
+
return { close: closeServer };
|
|
699
138
|
}
|