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
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import { loadInstructions } from "../instructions.js";
|
|
2
|
+
import { loadSession, saveSession, withSessionLock } from "../sessions.js";
|
|
3
|
+
import { buildUserContent } from "../agent.js";
|
|
4
|
+
import { lastUserText } from "../tui/hydrate.js";
|
|
5
|
+
import { compactMessages } from "../compaction.js";
|
|
6
|
+
import { resolveModel } from "../provider.js";
|
|
7
|
+
import { emptyTaskState } from "../tools/todo.js";
|
|
8
|
+
import { MAX_CONCURRENT_CHATS, sendJson, sendJsonError, readJsonBody, readSandboxTighten, normalizeMessages, chatRunOptions, executeChat, } from "./common.js";
|
|
9
|
+
/** Shared handler for /v1/chat and /v1/code (streaming + JSON). */
|
|
10
|
+
async function handleChatRequest(ctx, req, res) {
|
|
11
|
+
if (req.headers["content-type"]?.split(";")[0]?.trim() !== "application/json") {
|
|
12
|
+
sendJson(res, 415, { error: "unsupported_media_type", detail: "Use Content-Type: application/json" });
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
if (ctx.activeChats >= MAX_CONCURRENT_CHATS) {
|
|
16
|
+
sendJson(res, 429, {
|
|
17
|
+
error: "too_many_concurrent_requests",
|
|
18
|
+
detail: `Max ${MAX_CONCURRENT_CHATS} concurrent chats`,
|
|
19
|
+
});
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
ctx.activeChats++;
|
|
23
|
+
try {
|
|
24
|
+
const parsed = await readJsonBody(req);
|
|
25
|
+
if (!parsed.ok) {
|
|
26
|
+
sendJsonError(res, parsed);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const body = parsed.value;
|
|
30
|
+
const sandboxParsed = readSandboxTighten(body);
|
|
31
|
+
if (!sandboxParsed.ok) {
|
|
32
|
+
sendJson(res, 400, { error: sandboxParsed.error, detail: sandboxParsed.detail });
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const sandboxTighten = sandboxParsed.policy;
|
|
36
|
+
const modelId = typeof body.model === "string" ? body.model : undefined;
|
|
37
|
+
const stream = body.stream === true;
|
|
38
|
+
const sessionId = typeof body.session_id === "string" ? body.session_id : undefined;
|
|
39
|
+
let messages;
|
|
40
|
+
if (sessionId) {
|
|
41
|
+
await withSessionLock(sessionId, async () => {
|
|
42
|
+
const session = loadSession(sessionId);
|
|
43
|
+
if (!session) {
|
|
44
|
+
sendJson(res, 404, { error: "session_not_found", session_id: sessionId });
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (typeof body.message !== "string" || !body.message.trim()) {
|
|
48
|
+
sendJson(res, 400, {
|
|
49
|
+
error: "session_requires_message",
|
|
50
|
+
detail: "With `session_id`, send a non-empty `message` for the new user turn",
|
|
51
|
+
});
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const messages = [...session.messages];
|
|
55
|
+
const content = body.images && body.images.length > 0 ? await buildUserContent(body.message, body.images) : body.message;
|
|
56
|
+
messages.push({ role: "user", content });
|
|
57
|
+
await executeChat(ctx, {
|
|
58
|
+
res,
|
|
59
|
+
messages,
|
|
60
|
+
modelId,
|
|
61
|
+
stream,
|
|
62
|
+
sessionId,
|
|
63
|
+
runOptions: chatRunOptions(body),
|
|
64
|
+
autoApprove: body.auto_approve !== false,
|
|
65
|
+
taskState: session.taskState ?? emptyTaskState(),
|
|
66
|
+
sandboxTighten,
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
const norm = normalizeMessages(body);
|
|
73
|
+
if (!norm.ok) {
|
|
74
|
+
sendJson(res, 400, { error: "invalid_body", detail: norm.error });
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
messages = norm.messages;
|
|
78
|
+
if (body.images && body.images.length > 0) {
|
|
79
|
+
const last = messages[messages.length - 1];
|
|
80
|
+
if (!last || last.role !== "user" || typeof body.message !== "string") {
|
|
81
|
+
sendJson(res, 400, {
|
|
82
|
+
error: "images_require_message",
|
|
83
|
+
detail: "With `images`, send a top-level `message` string for the user turn",
|
|
84
|
+
});
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
messages[messages.length - 1] = {
|
|
88
|
+
role: "user",
|
|
89
|
+
content: await buildUserContent(body.message, body.images),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
await executeChat(ctx, {
|
|
94
|
+
res,
|
|
95
|
+
messages,
|
|
96
|
+
modelId,
|
|
97
|
+
stream,
|
|
98
|
+
sessionId,
|
|
99
|
+
runOptions: chatRunOptions(body),
|
|
100
|
+
autoApprove: body.auto_approve !== false,
|
|
101
|
+
sandboxTighten,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
ctx.activeChats--;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
export const handleChatRoutes = async (req, res, ctx, _url, pathname) => {
|
|
109
|
+
if (req.method === "POST" && pathname === "/v1/chat/reload-instructions") {
|
|
110
|
+
ctx.instructions = await loadInstructions();
|
|
111
|
+
sendJson(res, 200, { ok: true, instructions_chars: ctx.instructions.join("\n").length });
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
if (req.method === "POST" && (pathname === "/v1/chat" || pathname === "/v1/code")) {
|
|
115
|
+
await handleChatRequest(ctx, req, res);
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
if (req.method === "POST" && pathname === "/v1/chat/redo") {
|
|
119
|
+
const parsed = await readJsonBody(req);
|
|
120
|
+
if (!parsed.ok) {
|
|
121
|
+
sendJsonError(res, parsed);
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
const body = parsed.value;
|
|
125
|
+
const sessionId = typeof body.session_id === "string" ? body.session_id : undefined;
|
|
126
|
+
if (!sessionId) {
|
|
127
|
+
sendJson(res, 400, { error: "missing_session_id" });
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
const sandboxParsed = readSandboxTighten(body);
|
|
131
|
+
if (!sandboxParsed.ok) {
|
|
132
|
+
sendJson(res, 400, { error: sandboxParsed.error, detail: sandboxParsed.detail });
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
if (ctx.activeChats >= MAX_CONCURRENT_CHATS) {
|
|
136
|
+
sendJson(res, 429, {
|
|
137
|
+
error: "too_many_concurrent_requests",
|
|
138
|
+
detail: `Max ${MAX_CONCURRENT_CHATS} concurrent chats`,
|
|
139
|
+
});
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
ctx.activeChats++;
|
|
143
|
+
try {
|
|
144
|
+
await withSessionLock(sessionId, async () => {
|
|
145
|
+
const session = loadSession(sessionId);
|
|
146
|
+
if (!session) {
|
|
147
|
+
sendJson(res, 404, { error: "session_not_found", session_id: sessionId });
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const text = lastUserText(session.messages);
|
|
151
|
+
if (!text) {
|
|
152
|
+
sendJson(res, 400, { error: "no_user_message" });
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
await executeChat(ctx, {
|
|
156
|
+
res,
|
|
157
|
+
messages: [...session.messages, { role: "user", content: text }],
|
|
158
|
+
modelId: typeof body.model === "string" ? body.model : undefined,
|
|
159
|
+
stream: body.stream === true,
|
|
160
|
+
sessionId,
|
|
161
|
+
runOptions: chatRunOptions(body),
|
|
162
|
+
autoApprove: body.auto_approve !== false,
|
|
163
|
+
taskState: session.taskState ?? emptyTaskState(),
|
|
164
|
+
sandboxTighten: sandboxParsed.policy,
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
finally {
|
|
169
|
+
ctx.activeChats--;
|
|
170
|
+
}
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
if (req.method === "POST" && pathname === "/v1/approvals") {
|
|
174
|
+
const parsed = await readJsonBody(req);
|
|
175
|
+
if (!parsed.ok) {
|
|
176
|
+
sendJsonError(res, parsed);
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
const id = parsed.value.id?.trim();
|
|
180
|
+
if (!id) {
|
|
181
|
+
sendJson(res, 400, { error: "missing_id" });
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
if (typeof parsed.value.accepted !== "boolean") {
|
|
185
|
+
sendJson(res, 400, { error: "missing_accepted" });
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
if (!ctx.approvalGate.resolveConfirm(id, parsed.value.accepted)) {
|
|
189
|
+
sendJson(res, 404, { error: "unknown_approval_id", id });
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
sendJson(res, 200, { ok: true, id, accepted: parsed.value.accepted });
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
if (req.method === "POST" && pathname === "/v1/answers") {
|
|
196
|
+
const parsed = await readJsonBody(req);
|
|
197
|
+
if (!parsed.ok) {
|
|
198
|
+
sendJsonError(res, parsed);
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
const id = parsed.value.id?.trim();
|
|
202
|
+
if (!id) {
|
|
203
|
+
sendJson(res, 400, { error: "missing_id" });
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
const answer = parsed.value.answer === undefined ? null : parsed.value.answer;
|
|
207
|
+
if (answer !== null && typeof answer !== "string") {
|
|
208
|
+
sendJson(res, 400, { error: "invalid_answer" });
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
if (!ctx.approvalGate.resolveQuestion(id, answer)) {
|
|
212
|
+
sendJson(res, 404, { error: "unknown_answer_id", id });
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
sendJson(res, 200, { ok: true, id });
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
if (req.method === "POST" && pathname === "/v1/paste") {
|
|
219
|
+
if (req.headers["content-type"]?.split(";")[0]?.trim() !== "application/json") {
|
|
220
|
+
sendJson(res, 415, { error: "unsupported_media_type" });
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
223
|
+
if (ctx.activeChats >= MAX_CONCURRENT_CHATS) {
|
|
224
|
+
sendJson(res, 429, {
|
|
225
|
+
error: "too_many_concurrent_requests",
|
|
226
|
+
detail: `Max ${MAX_CONCURRENT_CHATS} concurrent chats`,
|
|
227
|
+
});
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
const parsed = await readJsonBody(req);
|
|
231
|
+
if (!parsed.ok) {
|
|
232
|
+
sendJsonError(res, parsed);
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
const body = parsed.value;
|
|
236
|
+
if (!body.image_base64) {
|
|
237
|
+
sendJson(res, 400, { error: "missing_image_base64" });
|
|
238
|
+
return true;
|
|
239
|
+
}
|
|
240
|
+
const sandboxParsed = readSandboxTighten(body);
|
|
241
|
+
if (!sandboxParsed.ok) {
|
|
242
|
+
sendJson(res, 400, { error: sandboxParsed.error, detail: sandboxParsed.detail });
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
const imageBuffer = Buffer.from(body.image_base64, "base64");
|
|
246
|
+
const mimeType = body.mime_type ?? "image/png";
|
|
247
|
+
const text = body.message ?? "What's in this image?";
|
|
248
|
+
const modelId = body.model;
|
|
249
|
+
const sessionId = body.session_id;
|
|
250
|
+
const stream = body.stream === true;
|
|
251
|
+
ctx.activeChats++;
|
|
252
|
+
try {
|
|
253
|
+
await withSessionLock(sessionId, async () => {
|
|
254
|
+
let messages = [];
|
|
255
|
+
let pasteTaskState;
|
|
256
|
+
if (sessionId) {
|
|
257
|
+
const session = loadSession(sessionId);
|
|
258
|
+
if (!session) {
|
|
259
|
+
sendJson(res, 404, { error: "session_not_found", session_id: sessionId });
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
messages = [...session.messages];
|
|
263
|
+
pasteTaskState = session.taskState;
|
|
264
|
+
}
|
|
265
|
+
messages.push({
|
|
266
|
+
role: "user",
|
|
267
|
+
content: [
|
|
268
|
+
{ type: "text", text },
|
|
269
|
+
{ type: "image", image: imageBuffer, mimeType },
|
|
270
|
+
],
|
|
271
|
+
});
|
|
272
|
+
await executeChat(ctx, {
|
|
273
|
+
res,
|
|
274
|
+
messages,
|
|
275
|
+
modelId,
|
|
276
|
+
stream,
|
|
277
|
+
sessionId,
|
|
278
|
+
runOptions: chatRunOptions(body),
|
|
279
|
+
autoApprove: body.auto_approve !== false,
|
|
280
|
+
taskState: pasteTaskState ?? emptyTaskState(),
|
|
281
|
+
sandboxTighten: sandboxParsed.policy,
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
finally {
|
|
286
|
+
ctx.activeChats--;
|
|
287
|
+
}
|
|
288
|
+
return true;
|
|
289
|
+
}
|
|
290
|
+
if (req.method === "POST" && pathname === "/v1/chat/compact") {
|
|
291
|
+
const parsed = await readJsonBody(req);
|
|
292
|
+
if (!parsed.ok) {
|
|
293
|
+
sendJsonError(res, parsed);
|
|
294
|
+
return true;
|
|
295
|
+
}
|
|
296
|
+
const body = parsed.value;
|
|
297
|
+
if (!body.session_id) {
|
|
298
|
+
sendJson(res, 400, { error: "missing_session_id" });
|
|
299
|
+
return true;
|
|
300
|
+
}
|
|
301
|
+
const session = loadSession(body.session_id);
|
|
302
|
+
if (!session) {
|
|
303
|
+
sendJson(res, 404, { error: "session_not_found" });
|
|
304
|
+
return true;
|
|
305
|
+
}
|
|
306
|
+
const model = resolveModel();
|
|
307
|
+
const result = await compactMessages(session.messages, model, { taskGoal: session.taskState?.goal });
|
|
308
|
+
if (result.compacted) {
|
|
309
|
+
saveSession(result.messages, body.session_id);
|
|
310
|
+
}
|
|
311
|
+
sendJson(res, 200, { ok: true, compacted: result.compacted, message_count: result.messages.length });
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
if (req.method === "POST" && pathname === "/v1/chat/undo") {
|
|
315
|
+
const parsed = await readJsonBody(req);
|
|
316
|
+
if (!parsed.ok) {
|
|
317
|
+
sendJsonError(res, parsed);
|
|
318
|
+
return true;
|
|
319
|
+
}
|
|
320
|
+
const body = parsed.value;
|
|
321
|
+
if (!body.session_id) {
|
|
322
|
+
sendJson(res, 400, { error: "missing_session_id" });
|
|
323
|
+
return true;
|
|
324
|
+
}
|
|
325
|
+
const session = loadSession(body.session_id);
|
|
326
|
+
if (!session) {
|
|
327
|
+
sendJson(res, 404, { error: "session_not_found" });
|
|
328
|
+
return true;
|
|
329
|
+
}
|
|
330
|
+
let userIdx = -1;
|
|
331
|
+
for (let i = session.messages.length - 1; i >= 0; i--) {
|
|
332
|
+
if (session.messages[i].role === "user") {
|
|
333
|
+
userIdx = i;
|
|
334
|
+
break;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
if (userIdx === -1) {
|
|
338
|
+
sendJson(res, 400, { error: "no_user_message" });
|
|
339
|
+
return true;
|
|
340
|
+
}
|
|
341
|
+
session.messages = session.messages.slice(0, userIdx + 1);
|
|
342
|
+
saveSession(session.messages, body.session_id);
|
|
343
|
+
sendJson(res, 200, { ok: true, message_count: session.messages.length, messages: session.messages });
|
|
344
|
+
return true;
|
|
345
|
+
}
|
|
346
|
+
return false;
|
|
347
|
+
};
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { getMcpStatus, syncMcpConnections, loadMcpConfigEntries, listMcpCatalog, readMcpResource, getMcpPrompt, } from "../mcp.js";
|
|
2
|
+
import { sendJson, sendJsonError, readJsonBody, decodePathSegment } from "./common.js";
|
|
3
|
+
export const handleMcpRoutes = async (req, res, _ctx, url, pathname) => {
|
|
4
|
+
if (req.method === "GET" && pathname === "/v1/mcp") {
|
|
5
|
+
const status = getMcpStatus();
|
|
6
|
+
sendJson(res, 200, { servers: status });
|
|
7
|
+
return true;
|
|
8
|
+
}
|
|
9
|
+
const mcpResources = pathname.match(/^\/v1\/mcp\/([^/]+)\/resources$/);
|
|
10
|
+
if (req.method === "GET" && mcpResources) {
|
|
11
|
+
const name = decodePathSegment(mcpResources[1]);
|
|
12
|
+
if (!name) {
|
|
13
|
+
sendJson(res, 400, { error: "missing_name" });
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
const status = getMcpStatus()[name];
|
|
17
|
+
if (!status) {
|
|
18
|
+
sendJson(res, 404, { error: "mcp_not_found", name });
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
if (!status.connected) {
|
|
22
|
+
sendJson(res, 404, { error: "mcp_not_connected", name });
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
const catalog = listMcpCatalog(name).servers[0];
|
|
26
|
+
sendJson(res, 200, { name, resources: catalog.resources, templates: catalog.templates });
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
const mcpPrompts = pathname.match(/^\/v1\/mcp\/([^/]+)\/prompts$/);
|
|
30
|
+
if (req.method === "GET" && mcpPrompts) {
|
|
31
|
+
const name = decodePathSegment(mcpPrompts[1]);
|
|
32
|
+
if (!name) {
|
|
33
|
+
sendJson(res, 400, { error: "missing_name" });
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
const status = getMcpStatus()[name];
|
|
37
|
+
if (!status) {
|
|
38
|
+
sendJson(res, 404, { error: "mcp_not_found", name });
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
if (!status.connected) {
|
|
42
|
+
sendJson(res, 404, { error: "mcp_not_connected", name });
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
const catalog = listMcpCatalog(name).servers[0];
|
|
46
|
+
sendJson(res, 200, { name, prompts: catalog.prompts });
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
const mcpRead = pathname.match(/^\/v1\/mcp\/([^/]+)\/resources\/read$/);
|
|
50
|
+
if (req.method === "POST" && mcpRead) {
|
|
51
|
+
const name = decodePathSegment(mcpRead[1]);
|
|
52
|
+
if (!name) {
|
|
53
|
+
sendJson(res, 400, { error: "missing_name" });
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
const parsed = await readJsonBody(req);
|
|
57
|
+
if (!parsed.ok) {
|
|
58
|
+
sendJsonError(res, parsed);
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
const uri = parsed.value.uri?.trim();
|
|
62
|
+
if (!uri) {
|
|
63
|
+
sendJson(res, 400, { error: "missing_uri" });
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
const status = getMcpStatus()[name];
|
|
67
|
+
if (!status) {
|
|
68
|
+
sendJson(res, 404, { error: "mcp_not_found", name });
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
if (!status.connected) {
|
|
72
|
+
sendJson(res, 404, { error: "mcp_not_connected", name });
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
const content = await readMcpResource(uri, name);
|
|
76
|
+
sendJson(res, 200, { ok: true, name, uri, content });
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
const mcpGetPrompt = pathname.match(/^\/v1\/mcp\/([^/]+)\/prompts\/get$/);
|
|
80
|
+
if (req.method === "POST" && mcpGetPrompt) {
|
|
81
|
+
const name = decodePathSegment(mcpGetPrompt[1]);
|
|
82
|
+
if (!name) {
|
|
83
|
+
sendJson(res, 400, { error: "missing_name" });
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
const parsed = await readJsonBody(req);
|
|
87
|
+
if (!parsed.ok) {
|
|
88
|
+
sendJsonError(res, parsed);
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
const prompt = parsed.value.prompt?.trim();
|
|
92
|
+
if (!prompt) {
|
|
93
|
+
sendJson(res, 400, { error: "missing_prompt" });
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
const status = getMcpStatus()[name];
|
|
97
|
+
if (!status) {
|
|
98
|
+
sendJson(res, 404, { error: "mcp_not_found", name });
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
if (!status.connected) {
|
|
102
|
+
sendJson(res, 404, { error: "mcp_not_connected", name });
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
const content = await getMcpPrompt(prompt, parsed.value.arguments, name);
|
|
106
|
+
sendJson(res, 200, { ok: true, name, prompt, content });
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
if (req.method === "POST" && pathname === "/v1/mcp") {
|
|
110
|
+
const parsed = await readJsonBody(req);
|
|
111
|
+
if (!parsed.ok) {
|
|
112
|
+
sendJsonError(res, parsed);
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
const body = parsed.value;
|
|
116
|
+
if (!body.name) {
|
|
117
|
+
sendJson(res, 400, { error: "missing_name" });
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
if (body.remoteTransport && !["streamable-http", "sse", "auto"].includes(body.remoteTransport)) {
|
|
121
|
+
sendJson(res, 400, { error: "invalid_remote_transport", allowed: ["streamable-http", "sse", "auto"] });
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
const scope = body.project ? "project" : "global";
|
|
125
|
+
const { loadMcpConfigFile, saveMcpConfig } = await import("../mcp.js");
|
|
126
|
+
const config = loadMcpConfigFile(scope);
|
|
127
|
+
const common = {
|
|
128
|
+
...(body.environment ? { environment: body.environment } : {}),
|
|
129
|
+
...(body.headers ? { headers: body.headers } : {}),
|
|
130
|
+
...(body.remoteTransport ? { remoteTransport: body.remoteTransport } : {}),
|
|
131
|
+
...(body.timeout !== undefined ? { timeout: body.timeout } : {}),
|
|
132
|
+
...(body.connectTimeout !== undefined ? { connectTimeout: body.connectTimeout } : {}),
|
|
133
|
+
...(body.callTimeout !== undefined ? { callTimeout: body.callTimeout } : {}),
|
|
134
|
+
enabled: body.enabled !== false,
|
|
135
|
+
};
|
|
136
|
+
if (body.url) {
|
|
137
|
+
config.mcpServers[body.name] = { url: body.url, ...(body.token ? { token: body.token } : {}), ...common };
|
|
138
|
+
}
|
|
139
|
+
else if (typeof body.command === "string" && body.command.length > 0) {
|
|
140
|
+
config.mcpServers[body.name] = { command: body.command, ...(body.args ? { args: body.args } : {}), ...common };
|
|
141
|
+
}
|
|
142
|
+
else if (Array.isArray(body.command) && body.command.length > 0) {
|
|
143
|
+
config.mcpServers[body.name] = { command: body.command, ...common };
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
sendJson(res, 400, { error: "provide command or url" });
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
saveMcpConfig(config, scope);
|
|
150
|
+
await syncMcpConnections();
|
|
151
|
+
const status = getMcpStatus()[body.name];
|
|
152
|
+
sendJson(res, 201, {
|
|
153
|
+
ok: true,
|
|
154
|
+
name: body.name,
|
|
155
|
+
scope,
|
|
156
|
+
connected: status?.connected ?? false,
|
|
157
|
+
...(status?.error ? { error: status.error } : {}),
|
|
158
|
+
...(status?.tools?.length ? { tools: status.tools } : {}),
|
|
159
|
+
});
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
if (req.method === "DELETE" && pathname.startsWith("/v1/mcp/")) {
|
|
163
|
+
const name = decodePathSegment(pathname.slice("/v1/mcp/".length));
|
|
164
|
+
if (!name) {
|
|
165
|
+
sendJson(res, 400, { error: "missing_name" });
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
const scope = url.searchParams.get("project") === "1" ? "project" : "global";
|
|
169
|
+
const { loadMcpConfigFile, saveMcpConfig } = await import("../mcp.js");
|
|
170
|
+
const config = loadMcpConfigFile(scope);
|
|
171
|
+
if (!config.mcpServers[name]) {
|
|
172
|
+
sendJson(res, 404, { error: "mcp_not_found", name, scope, available: Object.keys(config.mcpServers) });
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
delete config.mcpServers[name];
|
|
176
|
+
saveMcpConfig(config, scope);
|
|
177
|
+
await syncMcpConnections();
|
|
178
|
+
sendJson(res, 200, { ok: true, deleted: name, scope });
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
const mcpToggle = pathname.match(/^\/v1\/mcp\/([^/]+)\/(enable|disable)$/);
|
|
182
|
+
if (req.method === "POST" && mcpToggle) {
|
|
183
|
+
const name = decodePathSegment(mcpToggle[1]);
|
|
184
|
+
if (!name) {
|
|
185
|
+
sendJson(res, 400, { error: "missing_name" });
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
const enabled = mcpToggle[2] === "enable";
|
|
189
|
+
const parsed = await readJsonBody(req);
|
|
190
|
+
const scope = parsed.ok && parsed.value.scope === "project" ? "project" : "global";
|
|
191
|
+
const { loadMcpConfigFile, saveMcpConfig } = await import("../mcp.js");
|
|
192
|
+
const config = loadMcpConfigFile(scope);
|
|
193
|
+
if (!config.mcpServers[name]) {
|
|
194
|
+
const known = loadMcpConfigEntries().some((e) => e.name === name);
|
|
195
|
+
sendJson(res, 404, {
|
|
196
|
+
error: "mcp_not_found",
|
|
197
|
+
name,
|
|
198
|
+
scope,
|
|
199
|
+
...(known
|
|
200
|
+
? { hint: `server exists in another scope; pass the matching scope` }
|
|
201
|
+
: { available: Object.keys(config.mcpServers) }),
|
|
202
|
+
});
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
config.mcpServers[name].enabled = enabled;
|
|
206
|
+
saveMcpConfig(config, scope);
|
|
207
|
+
await syncMcpConnections();
|
|
208
|
+
sendJson(res, 200, { ok: true, name, enabled, scope, connected: getMcpStatus()[name]?.connected ?? false });
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
return false;
|
|
212
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { loadMemories, addMemory, deleteMemory, searchMemories, readMemoryScope } from "../memory.js";
|
|
2
|
+
import { sendJson, sendJsonError, readJsonBody } from "./common.js";
|
|
3
|
+
export const handleMemoryRoutes = async (req, res, _ctx, url, pathname) => {
|
|
4
|
+
if (req.method === "GET" && pathname === "/v1/memory") {
|
|
5
|
+
sendJson(res, 200, { memories: loadMemories("global"), project_memories: loadMemories("project") });
|
|
6
|
+
return true;
|
|
7
|
+
}
|
|
8
|
+
if (req.method === "POST" && pathname === "/v1/memory") {
|
|
9
|
+
const parsed = await readJsonBody(req);
|
|
10
|
+
if (!parsed.ok) {
|
|
11
|
+
sendJsonError(res, parsed);
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
const body = parsed.value;
|
|
15
|
+
if (!body.content) {
|
|
16
|
+
sendJson(res, 400, { error: "missing_content" });
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
const scopeParsed = readMemoryScope(body.scope);
|
|
20
|
+
if (!scopeParsed.ok) {
|
|
21
|
+
sendJson(res, 400, { error: "invalid_scope", allowed: ["global", "project"] });
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
const scope = scopeParsed.scope ?? "global";
|
|
25
|
+
const memory = addMemory(body.content, body.tags ?? [], scope);
|
|
26
|
+
sendJson(res, 201, { ok: true, memory, scope });
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
if (req.method === "GET" && pathname === "/v1/memory/search") {
|
|
30
|
+
const query = url.searchParams.get("q") ?? "";
|
|
31
|
+
if (!query) {
|
|
32
|
+
sendJson(res, 400, { error: "missing_query_param_q" });
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
const scopeParsed = readMemoryScope(url.searchParams.get("scope"));
|
|
36
|
+
if (!scopeParsed.ok) {
|
|
37
|
+
sendJson(res, 400, { error: "invalid_scope", allowed: ["global", "project"] });
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
const globalResults = scopeParsed.scope === "project" ? [] : searchMemories(query, "global");
|
|
41
|
+
const projectResults = scopeParsed.scope === "global" ? [] : searchMemories(query, "project");
|
|
42
|
+
sendJson(res, 200, { results: globalResults, project_results: projectResults });
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
if (req.method === "DELETE" && pathname.startsWith("/v1/memory/")) {
|
|
46
|
+
const idx = parseInt(pathname.slice("/v1/memory/".length), 10);
|
|
47
|
+
if (Number.isNaN(idx)) {
|
|
48
|
+
sendJson(res, 400, { error: "invalid_index" });
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
const scopeParsed = readMemoryScope(url.searchParams.get("scope"));
|
|
52
|
+
if (!scopeParsed.ok) {
|
|
53
|
+
sendJson(res, 400, { error: "invalid_scope", allowed: ["global", "project"] });
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
const scope = scopeParsed.scope ?? "global";
|
|
57
|
+
const ok = deleteMemory(idx - 1, scope);
|
|
58
|
+
if (!ok) {
|
|
59
|
+
sendJson(res, 404, { error: "memory_not_found", index: idx, scope });
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
sendJson(res, 200, { ok: true, deleted: idx, scope });
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
return false;
|
|
66
|
+
};
|