antechamber 1.0.40 → 2.0.1
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/CHANGELOG.md +126 -0
- package/README.md +88 -141
- package/bin/opencode-antechamber +14 -4
- package/dist/approvals.d.ts +9 -1
- package/dist/approvals.js +10 -2
- package/dist/catalog.d.ts +13 -2
- package/dist/catalog.js +88 -52
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +1 -1
- package/dist/commands/context.d.ts +5 -2
- package/dist/commands/context.js +8 -4
- package/dist/commands/doctor.js +4 -3
- package/dist/commands/setup.js +9 -28
- package/dist/config.d.ts +6 -2
- package/dist/config.js +18 -4
- package/dist/file-watch.d.ts +35 -0
- package/dist/file-watch.js +163 -0
- package/dist/latency.d.ts +10 -0
- package/dist/latency.js +115 -0
- package/dist/misc-routes.js +14 -12
- package/dist/pi-event-translation.js +44 -2
- package/dist/route-services.d.ts +8 -3
- package/dist/routes-v1.d.ts +1 -1
- package/dist/routes-v1.js +29 -418
- package/dist/routes-v2.js +252 -34
- package/dist/self-update.d.ts +2 -1
- package/dist/self-update.js +5 -1
- package/dist/server.d.ts +17 -3
- package/dist/server.js +191 -18
- package/dist/session-commands.d.ts +3 -1
- package/dist/session-commands.js +13 -7
- package/dist/session-history.js +3 -0
- package/dist/session-store.d.ts +19 -10
- package/dist/session-store.js +266 -128
- package/dist/session-tree.d.ts +15 -1
- package/dist/session-tree.js +2 -2
- package/dist/sessions.d.ts +85 -1
- package/dist/sessions.js +238 -10
- package/dist/sse.d.ts +0 -2
- package/dist/sse.js +13 -46
- package/dist/text-generation.d.ts +20 -0
- package/dist/text-generation.js +93 -0
- package/dist/tool-map.js +1 -3
- package/dist/types.d.ts +60 -0
- package/dist/v2.d.ts +8 -1
- package/dist/v2.js +24 -4
- package/dist/work-status.js +1 -1
- package/extensions/antechamber/README.md +100 -0
- package/extensions/antechamber/bin/opencode-antechamber +42 -0
- package/extensions/{omo-dag/docs/DESIGN.md → antechamber/docs/GRAPH.md} +5 -5
- package/extensions/{omo-work/docs/DESIGN.md → antechamber/docs/WORK.md} +3 -3
- package/extensions/antechamber/package.json +60 -0
- package/extensions/{omo-dag → antechamber}/panel/index.html +8 -1
- package/extensions/antechamber/panel/main.js +261 -0
- package/extensions/antechamber/runtime/launcher.js +201 -0
- package/extensions/antechamber/service/main.js +884 -0
- package/package.json +75 -26
- package/extensions/omo-compat/README.md +0 -48
- package/extensions/omo-compat/icon.svg +0 -1
- package/extensions/omo-compat/package.json +0 -43
- package/extensions/omo-dag/README.md +0 -167
- package/extensions/omo-dag/package.json +0 -33
- package/extensions/omo-dag/panel/main.js +0 -176
- package/extensions/omo-dag/service/main.js +0 -457
- package/extensions/omo-work/README.md +0 -88
- package/extensions/omo-work/package.json +0 -30
- package/extensions/omo-work/service/main.js +0 -149
- /package/extensions/{omo-dag → antechamber}/icon.svg +0 -0
- /package/extensions/{omo-work → antechamber}/status/index.html +0 -0
- /package/extensions/{omo-work → antechamber}/status/main.js +0 -0
package/dist/routes-v1.js
CHANGED
|
@@ -1,442 +1,53 @@
|
|
|
1
|
-
/**
|
|
2
|
-
import { execFile } from "node:child_process";
|
|
3
|
-
import { homedir } from "node:os";
|
|
1
|
+
/** Unprefixed compatibility routes retained for evidenced or uncertain callers. */
|
|
4
2
|
import { errorBody, json } from "./http.js";
|
|
5
|
-
import { miscRoute } from "./misc-routes.js";
|
|
6
3
|
import { mcpRoute } from "./mcp.js";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
import { SelfUpdateUnsupportedError } from "./self-update.js";
|
|
11
|
-
import { announceSessionError, compactSession, getCommands, unsupportedShare } from "./session-commands.js";
|
|
12
|
-
import { agentDescriptors, modelRefFromBody, numberParam, OPENCODE_COMPAT_VERSION, promptAsync, runSessionShell, runSessionSlash, sessionDirectory, sessionMetadata, } from "./route-services.js";
|
|
13
|
-
/**
|
|
14
|
-
* `archived` list filter per SDK v2 (`boolean | "true" | "false"`):
|
|
15
|
-
* "true" keeps only archived sessions, "false" only live ones, absent = all.
|
|
16
|
-
*/
|
|
17
|
-
function applyArchivedFilter(sessions, archived) {
|
|
18
|
-
if (archived === "true")
|
|
19
|
-
return sessions.filter((s) => s.time.archived !== undefined);
|
|
20
|
-
if (archived === "false")
|
|
21
|
-
return sessions.filter((s) => s.time.archived === undefined);
|
|
22
|
-
return sessions;
|
|
23
|
-
}
|
|
24
|
-
async function handleV1GlobalRoute(services, res, path, method) {
|
|
4
|
+
import { miscRoute } from "./misc-routes.js";
|
|
5
|
+
import { OPENCODE_COMPAT_VERSION } from "./route-services.js";
|
|
6
|
+
export async function handleV1(services, req, res, url, path, method, directory) {
|
|
25
7
|
if (method === "GET" && path === "/global/health") {
|
|
26
8
|
json(res, 200, OPENCODE_COMPAT_VERSION === null ? { healthy: true } : { healthy: true, version: OPENCODE_COMPAT_VERSION });
|
|
27
|
-
return
|
|
28
|
-
}
|
|
29
|
-
if (method === "POST" && path === "/global/upgrade") {
|
|
30
|
-
// The requested OpenCode `target` does not apply: this upgrades antechamber itself.
|
|
31
|
-
try {
|
|
32
|
-
const result = await services.selfUpdate();
|
|
33
|
-
json(res, 200, { success: true, version: result.to, updated: result.updated });
|
|
34
|
-
}
|
|
35
|
-
catch (error) {
|
|
36
|
-
if (!(error instanceof SelfUpdateUnsupportedError))
|
|
37
|
-
throw error;
|
|
38
|
-
json(res, 409, errorBody("UnsupportedError", error.message));
|
|
39
|
-
}
|
|
40
|
-
return true;
|
|
9
|
+
return;
|
|
41
10
|
}
|
|
42
11
|
if (method === "GET" && path === "/global/upgrade") {
|
|
43
|
-
json(res, 409, errorBody("UnsupportedError", "Upgrade antechamber with
|
|
44
|
-
return true;
|
|
45
|
-
}
|
|
46
|
-
return false;
|
|
47
|
-
}
|
|
48
|
-
function parseV1Prompt(body, defaultModel) {
|
|
49
|
-
if (!Array.isArray(body.parts))
|
|
50
|
-
throw new RequestBodyError(400, "parts must be an array");
|
|
51
|
-
const parts = parsePromptParts(body.parts);
|
|
52
|
-
if (body.delivery !== undefined && body.delivery !== "steer" && body.delivery !== "queue")
|
|
53
|
-
throw new RequestBodyError(400, "delivery must be steer or queue");
|
|
54
|
-
const text = parts
|
|
55
|
-
.filter((part) => part.type === "text")
|
|
56
|
-
.map((part) => part.text ?? "")
|
|
57
|
-
.join("\n");
|
|
58
|
-
const model = modelRefFromBody(body.model ?? (body.variant !== undefined ? defaultModel : undefined), body.variant);
|
|
59
|
-
const native = nativePromptInput(text, parts);
|
|
60
|
-
if (native.message.trim() === "" && !native.images?.length)
|
|
61
|
-
throw new RequestBodyError(400, "prompt requires non-whitespace text, file content, or an agent mention");
|
|
62
|
-
return { text, model, parts, delivery: body.delivery ?? "steer" };
|
|
63
|
-
}
|
|
64
|
-
export async function handleV1(services, req, res, url, path, method, directory) {
|
|
65
|
-
if (await handleV1GlobalRoute(services, res, path, method))
|
|
12
|
+
json(res, 409, errorBody("UnsupportedError", "Upgrade antechamber with the upgrade CLI command."));
|
|
66
13
|
return;
|
|
14
|
+
}
|
|
15
|
+
// MCP OAuth and the remaining unprefixed probes are deliberately retained
|
|
16
|
+
// until their complete caller chain is established; see .omo/v1-removal-map.md.
|
|
67
17
|
const mcp = await mcpRoute(services, method, path, directory, () => services.body(req));
|
|
68
18
|
if (mcp !== undefined) {
|
|
69
19
|
json(res, mcp.status, mcp.body);
|
|
70
20
|
return;
|
|
71
21
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
if (path === "/session" && method === "POST") {
|
|
79
|
-
const body = (await services.body(req));
|
|
80
|
-
const metadata = sessionMetadata(body.metadata);
|
|
81
|
-
json(res, 200, await services.sessions.create(sessionDirectory(directory), body.title, body.parentID, metadata === undefined ? {} : { metadata }));
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
if (path === "/session" && method === "GET") {
|
|
85
|
-
const dir = url.searchParams.get("directory") ?? undefined;
|
|
86
|
-
let sessions = services.sessions.list(dir);
|
|
87
|
-
sessions = applyArchivedFilter(sessions, url.searchParams.get("archived"));
|
|
88
|
-
json(res, 200, sessions);
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
if (path === "/session/status" && method === "GET") {
|
|
92
|
-
json(res, 200, services.sessions.statuses());
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
const sessionMatch = path.match(/^\/session\/([^/]+)(\/.*)?$/);
|
|
96
|
-
if (sessionMatch) {
|
|
97
|
-
const [, sessionId, sub] = sessionMatch;
|
|
98
|
-
if (sessionId === undefined) {
|
|
99
|
-
json(res, 404, errorBody("NotFoundError", `session not found: ${sessionId}`));
|
|
22
|
+
if ((method === "GET" && ["/skill", "/formatter", "/file"].includes(path)) || path.startsWith("/mcp/")) {
|
|
23
|
+
const misc = await miscRoute(method, path, url, directory);
|
|
24
|
+
if (misc !== undefined) {
|
|
25
|
+
json(res, misc.status, misc.body);
|
|
100
26
|
return;
|
|
101
27
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
28
|
+
}
|
|
29
|
+
const sessionResource = method === "GET" ? path.match(/^\/session\/([^/]+)\/(diff|children|message\/[^/]+)$/) : null;
|
|
30
|
+
if (sessionResource) {
|
|
31
|
+
const sessionID = sessionResource[1];
|
|
32
|
+
const resource = sessionResource[2];
|
|
33
|
+
if (!services.sessions.get(sessionID)) {
|
|
34
|
+
json(res, 404, errorBody("NotFoundError", `session not found: ${sessionID}`));
|
|
105
35
|
return;
|
|
106
36
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
37
|
+
if (resource === "diff") {
|
|
38
|
+
json(res, 200, await services.sessions.diff(sessionID));
|
|
39
|
+
}
|
|
40
|
+
else if (resource === "children") {
|
|
41
|
+
json(res, 200, services.sessions.list(directory).filter((candidate) => candidate.parentID === sessionID));
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
const messageID = decodeURIComponent(resource.slice("message/".length));
|
|
45
|
+
const message = services.sessions.messages(sessionID)?.find((candidate) => candidate.info.id === messageID);
|
|
111
46
|
if (message === undefined)
|
|
112
47
|
json(res, 404, errorBody("NotFoundError", `message not found: ${messageID}`));
|
|
113
48
|
else
|
|
114
49
|
json(res, 200, message);
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
switch (`${method} ${sub ?? ""}`) {
|
|
118
|
-
case "GET /diff":
|
|
119
|
-
json(res, 200, await services.sessions.diff(sessionId));
|
|
120
|
-
return;
|
|
121
|
-
case "GET ":
|
|
122
|
-
json(res, 200, { ...managed.info, cost: managed.info.cost ?? 0 });
|
|
123
|
-
return;
|
|
124
|
-
case "PATCH ": {
|
|
125
|
-
const body = (await services.body(req));
|
|
126
|
-
json(res, 200, await services.sessions.update(sessionId, {
|
|
127
|
-
title: body.title,
|
|
128
|
-
archived: body.time?.archived,
|
|
129
|
-
metadata: sessionMetadata(body.metadata),
|
|
130
|
-
}));
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
case "DELETE ":
|
|
134
|
-
json(res, 200, await services.sessions.remove(sessionId));
|
|
135
|
-
return;
|
|
136
|
-
case "GET /message": {
|
|
137
|
-
// OpenCode pages backwards from the newest message: `before` is the
|
|
138
|
-
// x-next-cursor of the previous page (here, its oldest message ID).
|
|
139
|
-
// Without the cursor, OpenChamber's loader treats a partial page as the
|
|
140
|
-
// whole history; mobile's 30-message first page then has no user
|
|
141
|
-
// message to anchor a turn on, and the transcript renders blank.
|
|
142
|
-
const messages = services.sessions.messages(sessionId) ?? [];
|
|
143
|
-
const limit = numberParam(url.searchParams.get("limit"));
|
|
144
|
-
const before = url.searchParams.get("before");
|
|
145
|
-
const end = before === null ? messages.length : messages.findIndex((message) => message.info.id === before);
|
|
146
|
-
if (end < 0 || limit === 0) {
|
|
147
|
-
json(res, 200, []);
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
let start = limit === undefined ? 0 : Math.max(0, end - limit);
|
|
151
|
-
const parents = new Set(messages
|
|
152
|
-
.slice(start, end)
|
|
153
|
-
.flatMap((message) => (message.info.role === "assistant" ? [message.info.parentID] : [])));
|
|
154
|
-
const parent = messages.findIndex((message, index) => index < start && message.info.role === "user" && parents.has(message.info.id));
|
|
155
|
-
if (parent >= 0)
|
|
156
|
-
start = parent;
|
|
157
|
-
// OpenChamber 1.24 widens a page with no user message only up to 120 messages
|
|
158
|
-
// (150 on desktop), then renders it with no turn to anchor on: blank. A long agent
|
|
159
|
-
// turn outgrows that, so retain its native prompt, not just a compaction display marker.
|
|
160
|
-
if (!messages
|
|
161
|
-
.slice(start, end)
|
|
162
|
-
.some((message) => message.info.role === "user" && !message.parts.some((part) => part.type === "compaction"))) {
|
|
163
|
-
const prompt = messages.findLastIndex((message, index) => index < start &&
|
|
164
|
-
message.info.role === "user" &&
|
|
165
|
-
!message.parts.some((part) => part.type === "compaction"));
|
|
166
|
-
if (prompt >= 0)
|
|
167
|
-
start = prompt;
|
|
168
|
-
}
|
|
169
|
-
const page = messages.slice(start, end);
|
|
170
|
-
if (start > 0 && page[0] !== undefined)
|
|
171
|
-
res.setHeader("x-next-cursor", page[0].info.id);
|
|
172
|
-
json(res, 200, page);
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
case "GET /children":
|
|
176
|
-
json(res, 200, services.sessions.list(directory).filter((candidate) => candidate.parentID === sessionId));
|
|
177
|
-
return;
|
|
178
|
-
case "GET /todo":
|
|
179
|
-
json(res, 200, services.sessions.todos(sessionId));
|
|
180
|
-
return;
|
|
181
|
-
case "POST /prompt_async": {
|
|
182
|
-
const body = (await services.body(req));
|
|
183
|
-
const { text, model, parts, delivery } = parseV1Prompt(body, managed.model);
|
|
184
|
-
promptAsync(services, managed, text, model, body.noReply, body.messageID, delivery, parts);
|
|
185
|
-
res.writeHead(204).end();
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
case "POST /message": {
|
|
189
|
-
const body = (await services.body(req));
|
|
190
|
-
const { text, model, parts, delivery } = parseV1Prompt(body, managed.model);
|
|
191
|
-
await services.sessions.prompt(sessionId, text, model, body.noReply, body.messageID, delivery, parts);
|
|
192
|
-
const msgs = services.sessions.messages(sessionId) ?? [];
|
|
193
|
-
json(res, 200, msgs[msgs.length - 1] ?? null);
|
|
194
|
-
return;
|
|
195
|
-
}
|
|
196
|
-
case "POST /abort":
|
|
197
|
-
json(res, 200, await services.sessions.abort(sessionId));
|
|
198
|
-
return;
|
|
199
|
-
case "POST /fork":
|
|
200
|
-
case "POST /revert":
|
|
201
|
-
case "POST /unrevert": {
|
|
202
|
-
const body = await services.body(req);
|
|
203
|
-
const messageID = body !== null && typeof body === "object" && "messageID" in body ? body.messageID : undefined;
|
|
204
|
-
if ((sub === "/revert" && (typeof messageID !== "string" || messageID === "")) ||
|
|
205
|
-
(sub === "/fork" && messageID !== undefined && (typeof messageID !== "string" || messageID === ""))) {
|
|
206
|
-
json(res, 400, errorBody("InvalidRequest", "messageID must be a nonempty string"));
|
|
207
|
-
return;
|
|
208
|
-
}
|
|
209
|
-
try {
|
|
210
|
-
const info = sub === "/fork"
|
|
211
|
-
? await services.sessions.fork(sessionId, typeof messageID === "string" ? messageID : undefined)
|
|
212
|
-
: sub === "/revert"
|
|
213
|
-
? await services.sessions.revert(sessionId, String(messageID))
|
|
214
|
-
: await services.sessions.unrevert(sessionId);
|
|
215
|
-
json(res, 200, info);
|
|
216
|
-
}
|
|
217
|
-
catch (error) {
|
|
218
|
-
json(res, 400, errorBody("SessionTreeError", error instanceof Error ? error.message : String(error)));
|
|
219
|
-
}
|
|
220
|
-
return;
|
|
221
|
-
}
|
|
222
|
-
case "POST /summarize": {
|
|
223
|
-
await services.body(req);
|
|
224
|
-
const result = await compactSession(services.sessions, sessionId);
|
|
225
|
-
if (result.kind === "error") {
|
|
226
|
-
announceSessionError({ sessions: services.sessions, session: managed, emit: services.broadcast }, "BadRequestError", result.message);
|
|
227
|
-
json(res, 400, errorBody("BadRequestError", result.message));
|
|
228
|
-
return;
|
|
229
|
-
}
|
|
230
|
-
json(res, 200, true);
|
|
231
|
-
return;
|
|
232
|
-
}
|
|
233
|
-
case "POST /shell": {
|
|
234
|
-
const managed = services.sessions.get(sessionId);
|
|
235
|
-
if (!managed) {
|
|
236
|
-
json(res, 404, errorBody("NotFoundError", `session not found: ${sessionId}`));
|
|
237
|
-
return;
|
|
238
|
-
}
|
|
239
|
-
const result = await runSessionShell(services, managed, await services.body(req));
|
|
240
|
-
if (result.kind === "input") {
|
|
241
|
-
json(res, 400, errorBody("BadRequestError", "command is required"));
|
|
242
|
-
return;
|
|
243
|
-
}
|
|
244
|
-
if (result.kind === "busy") {
|
|
245
|
-
json(res, 409, errorBody("SessionBusyError", "session is busy"));
|
|
246
|
-
return;
|
|
247
|
-
}
|
|
248
|
-
if (result.kind === "error") {
|
|
249
|
-
json(res, 400, errorBody("BadRequestError", result.message));
|
|
250
|
-
return;
|
|
251
|
-
}
|
|
252
|
-
json(res, 200, result.message);
|
|
253
|
-
return;
|
|
254
|
-
}
|
|
255
|
-
case "POST /command": {
|
|
256
|
-
const result = await runSessionSlash(services, sessionId, await services.body(req));
|
|
257
|
-
if (result.kind === "input") {
|
|
258
|
-
json(res, 400, errorBody("BadRequestError", "command is required"));
|
|
259
|
-
return;
|
|
260
|
-
}
|
|
261
|
-
const { message } = result;
|
|
262
|
-
json(res, 200, message === undefined ? { info: null, parts: [] } : { info: message.info, parts: message.parts });
|
|
263
|
-
return;
|
|
264
|
-
}
|
|
265
|
-
case "POST /share":
|
|
266
|
-
case "DELETE /share": {
|
|
267
|
-
const session = services.sessions.get(sessionId);
|
|
268
|
-
if (session)
|
|
269
|
-
announceSessionError({ sessions: services.sessions, session, emit: services.broadcast }, unsupportedShare.name, unsupportedShare.data.message);
|
|
270
|
-
json(res, 400, errorBody(unsupportedShare.name, unsupportedShare.data.message));
|
|
271
|
-
return;
|
|
272
|
-
}
|
|
273
|
-
default:
|
|
274
|
-
json(res, 501, errorBody("NotImplemented", `${method} ${path} not implemented (M1)`));
|
|
275
|
-
return;
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
// --- project / config ---
|
|
279
|
-
if (path === "/project/current" && method === "GET") {
|
|
280
|
-
json(res, 200, {
|
|
281
|
-
id: directory,
|
|
282
|
-
worktree: directory,
|
|
283
|
-
vcs: "git",
|
|
284
|
-
time: { created: Date.now(), updated: Date.now() },
|
|
285
|
-
sandboxes: [],
|
|
286
|
-
});
|
|
287
|
-
return;
|
|
288
|
-
}
|
|
289
|
-
if (path === "/project" && method === "GET") {
|
|
290
|
-
json(res, 200, [
|
|
291
|
-
{
|
|
292
|
-
id: directory,
|
|
293
|
-
worktree: directory,
|
|
294
|
-
vcs: "git",
|
|
295
|
-
time: { created: Date.now(), updated: Date.now() },
|
|
296
|
-
sandboxes: [],
|
|
297
|
-
},
|
|
298
|
-
]);
|
|
299
|
-
return;
|
|
300
|
-
}
|
|
301
|
-
if ((path === "/config" || path === "/global/config") && method === "GET") {
|
|
302
|
-
const cat = await services.catalog();
|
|
303
|
-
json(res, 200, {
|
|
304
|
-
$schema: "https://opencode.ai/config.json",
|
|
305
|
-
default_agent: getAgentName(),
|
|
306
|
-
plugin: [],
|
|
307
|
-
command: {},
|
|
308
|
-
mode: {},
|
|
309
|
-
agent: {},
|
|
310
|
-
username: process.env.OPENCODE_SERVER_USERNAME ?? "opencode",
|
|
311
|
-
...(cat.default ? { model: `${cat.default.providerID}/${cat.default.modelID}` } : {}),
|
|
312
|
-
});
|
|
313
|
-
return;
|
|
314
|
-
}
|
|
315
|
-
// --- model catalog (from pi) ---
|
|
316
|
-
if (path === "/provider" && method === "GET") {
|
|
317
|
-
const cat = await services.catalog();
|
|
318
|
-
json(res, 200, { all: cat.providers, default: cat.defaults, connected: cat.providers.map((p) => p.id) });
|
|
319
|
-
return;
|
|
320
|
-
}
|
|
321
|
-
if (path === "/config/providers" && method === "GET") {
|
|
322
|
-
const cat = await services.catalog();
|
|
323
|
-
json(res, 200, { providers: cat.providers, default: cat.defaults });
|
|
324
|
-
return;
|
|
325
|
-
}
|
|
326
|
-
// --- bootstrap probes OpenChamber expects to exist ---
|
|
327
|
-
if (path === "/experimental/session" && method === "GET") {
|
|
328
|
-
// OpenChamber page-loads the sidebar through this route (not /session) with
|
|
329
|
-
// ?directory=…&archived=…&roots=…&limit=…. Honor directory/roots/archived:
|
|
330
|
-
// ignoring them left the sidebar with only live sessions, so restored
|
|
331
|
-
// history never appeared after a restart. archived=false excludes archived
|
|
332
|
-
// sessions; archived=true includes them so the UI can partition both lists.
|
|
333
|
-
const directoryParam = url.searchParams.get("directory") ?? req.headers["x-opencode-directory"];
|
|
334
|
-
const rootsOnly = url.searchParams.get("roots") === "true";
|
|
335
|
-
let sessions = directoryParam !== undefined && directoryParam !== ""
|
|
336
|
-
? services.sessions.list(directoryParam)
|
|
337
|
-
: services.sessions.listAll();
|
|
338
|
-
if (rootsOnly)
|
|
339
|
-
sessions = sessions.filter((s) => s.parentID === undefined);
|
|
340
|
-
// Experimental lists use archived=true to INCLUDE archived sessions.
|
|
341
|
-
// The UI partitions this result itself; filtering to archived-only drops
|
|
342
|
-
// the current live session from the header's global metadata store.
|
|
343
|
-
if (url.searchParams.get("archived") === "false") {
|
|
344
|
-
sessions = sessions.filter((s) => s.time.archived === undefined);
|
|
345
|
-
}
|
|
346
|
-
sessions.sort((a, b) => b.time.updated - a.time.updated || a.id.localeCompare(b.id));
|
|
347
|
-
const cursor = numberParam(url.searchParams.get("cursor"));
|
|
348
|
-
if (cursor !== undefined)
|
|
349
|
-
sessions = sessions.filter((session) => session.time.updated < cursor);
|
|
350
|
-
const limit = numberParam(url.searchParams.get("limit"));
|
|
351
|
-
const page = limit === undefined ? sessions : sessions.slice(0, limit);
|
|
352
|
-
if (limit !== undefined && page.length < sessions.length && page.at(-1)?.time.updated !== undefined)
|
|
353
|
-
res.setHeader("x-next-cursor", String(page.at(-1).time.updated));
|
|
354
|
-
json(res, 200, page);
|
|
355
|
-
return;
|
|
356
|
-
}
|
|
357
|
-
if (path === "/question" && method === "GET") {
|
|
358
|
-
json(res, 200, services.sessions.questions());
|
|
359
|
-
return;
|
|
360
|
-
}
|
|
361
|
-
if (path === "/permission" && method === "GET") {
|
|
362
|
-
json(res, 200, services.sessions.permissions());
|
|
363
|
-
return;
|
|
364
|
-
}
|
|
365
|
-
const questionReply = path.match(/^\/question\/([^/]+)\/reply$/);
|
|
366
|
-
if (questionReply && method === "POST") {
|
|
367
|
-
const requestID = decodeURIComponent(questionReply[1]);
|
|
368
|
-
const body = (await services.body(req));
|
|
369
|
-
const pending = services.sessions.questions().find((question) => question.id === requestID);
|
|
370
|
-
if (!pending) {
|
|
371
|
-
json(res, 404, errorBody("QuestionNotFoundError", `question not found: ${requestID}`));
|
|
372
|
-
return;
|
|
373
50
|
}
|
|
374
|
-
if (!(await services.sessions.replyQuestion(pending.sessionID, requestID, body.answers ?? [], body.comment))) {
|
|
375
|
-
json(res, 409, errorBody("QuestionReplyError", "native question response refused"));
|
|
376
|
-
return;
|
|
377
|
-
}
|
|
378
|
-
json(res, 200, true);
|
|
379
|
-
return;
|
|
380
|
-
}
|
|
381
|
-
const questionReject = path.match(/^\/question\/([^/]+)\/reject$/);
|
|
382
|
-
if (questionReject && method === "POST") {
|
|
383
|
-
const requestID = decodeURIComponent(questionReject[1]);
|
|
384
|
-
const pending = services.sessions.questions().find((question) => question.id === requestID);
|
|
385
|
-
if (!pending) {
|
|
386
|
-
json(res, 404, errorBody("QuestionNotFoundError", `question not found: ${requestID}`));
|
|
387
|
-
return;
|
|
388
|
-
}
|
|
389
|
-
if (!(await services.sessions.rejectQuestion(pending.sessionID, requestID))) {
|
|
390
|
-
json(res, 409, errorBody("QuestionReplyError", "native question cancellation refused"));
|
|
391
|
-
return;
|
|
392
|
-
}
|
|
393
|
-
json(res, 200, true);
|
|
394
|
-
return;
|
|
395
|
-
}
|
|
396
|
-
const permissionReply = path.match(/^\/permission\/([^/]+)\/reply$/);
|
|
397
|
-
if (permissionReply && method === "POST") {
|
|
398
|
-
const requestID = decodeURIComponent(permissionReply[1]);
|
|
399
|
-
const body = (await services.body(req));
|
|
400
|
-
const pending = services.sessions.permissions().find((permission) => permission.id === requestID);
|
|
401
|
-
if (!pending ||
|
|
402
|
-
!body.reply ||
|
|
403
|
-
!services.sessions.replyPermission(pending.sessionID, requestID, body.reply, body.message)) {
|
|
404
|
-
json(res, 404, errorBody("PermissionNotFoundError", `permission not found: ${requestID}`));
|
|
405
|
-
return;
|
|
406
|
-
}
|
|
407
|
-
json(res, 200, true);
|
|
408
|
-
return;
|
|
409
|
-
}
|
|
410
|
-
if (path === "/lsp" && method === "GET") {
|
|
411
|
-
json(res, 200, []);
|
|
412
|
-
return;
|
|
413
|
-
}
|
|
414
|
-
if (path === "/formatter" && method === "GET") {
|
|
415
|
-
json(res, 200, []);
|
|
416
|
-
return;
|
|
417
|
-
}
|
|
418
|
-
if (path === "/command" && method === "GET") {
|
|
419
|
-
json(res, 200, (await getCommands(directory)).map((command) => ({
|
|
420
|
-
...command,
|
|
421
|
-
source: "command",
|
|
422
|
-
hints: [],
|
|
423
|
-
subtask: false,
|
|
424
|
-
})));
|
|
425
|
-
return;
|
|
426
|
-
}
|
|
427
|
-
if (path === "/path" && method === "GET") {
|
|
428
|
-
json(res, 200, { home: homedir(), state: "", config: "", worktree: directory, directory });
|
|
429
|
-
return;
|
|
430
|
-
}
|
|
431
|
-
if (path === "/vcs" && method === "GET") {
|
|
432
|
-
execFile("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd: directory, timeout: 3000 }, (err, stdout) => {
|
|
433
|
-
const branch = err ? undefined : stdout.trim() || undefined;
|
|
434
|
-
json(res, 200, { branch: branch ?? null, default_branch: null });
|
|
435
|
-
});
|
|
436
|
-
return;
|
|
437
|
-
}
|
|
438
|
-
if (path === "/agent" && method === "GET") {
|
|
439
|
-
json(res, 200, await agentDescriptors(services));
|
|
440
51
|
return;
|
|
441
52
|
}
|
|
442
53
|
json(res, 404, errorBody("NotFoundError", `no route: ${method} ${path}`));
|