antechamber 1.0.41 → 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 +97 -0
- package/README.md +88 -141
- package/bin/opencode-antechamber +14 -4
- 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/latency.d.ts +10 -0
- package/dist/latency.js +115 -0
- package/dist/misc-routes.js +2 -10
- package/dist/route-services.d.ts +0 -2
- package/dist/routes-v1.d.ts +1 -1
- package/dist/routes-v1.js +29 -418
- package/dist/routes-v2.js +1 -1
- package/dist/self-update.d.ts +2 -1
- package/dist/self-update.js +5 -1
- package/dist/server.d.ts +1 -3
- package/dist/server.js +9 -12
- package/dist/session-store.d.ts +12 -10
- package/dist/session-store.js +238 -128
- package/dist/session-tree.js +2 -2
- package/dist/sse.d.ts +0 -2
- package/dist/sse.js +13 -46
- package/dist/tool-map.js +1 -3
- 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}`));
|
package/dist/routes-v2.js
CHANGED
|
@@ -453,7 +453,7 @@ export async function handleV2(services, req, res, url, path, method, directory)
|
|
|
453
453
|
const order = url.searchParams.get("order") === "asc" ? "asc" : "desc";
|
|
454
454
|
const limit = numberParam(url.searchParams.get("limit"));
|
|
455
455
|
let infos = (dir !== null && dir !== "" ? services.sessions.list(dir) : services.sessions.listAll()).filter(
|
|
456
|
-
//
|
|
456
|
+
// Synthetic monitor sessions stay internal; 2.x shows them in the OmO Work section.
|
|
457
457
|
(session) => !services.sessions.monitors.owns(session.id));
|
|
458
458
|
if (search)
|
|
459
459
|
infos = infos.filter((s) => s.title.toLowerCase().includes(search.toLowerCase()));
|
package/dist/self-update.d.ts
CHANGED
|
@@ -22,10 +22,11 @@ export interface SelfUpdateResult {
|
|
|
22
22
|
readonly to: string;
|
|
23
23
|
readonly method: InstallMethod;
|
|
24
24
|
}
|
|
25
|
+
export declare const MANAGED_UPDATE_HINT = "antechamber is managed by its OpenChamber extension: update the extension in Settings -> Extensions";
|
|
25
26
|
export declare function installMethod(location: InstallLocation, npmRoot: () => string): InstallMethod;
|
|
26
27
|
export declare function isNewerVersion(a: string, b: string): boolean;
|
|
27
28
|
export declare function fetchLatestVersion(env?: NodeJS.ProcessEnv): Promise<string>;
|
|
28
|
-
/** Prefers
|
|
29
|
+
/** Prefers an npm beside the running runtime (bun or node): service PATHs often lack the user's tool directories. */
|
|
29
30
|
export declare function npmBinaryFor(execPath?: string): string;
|
|
30
31
|
export declare function defaultSelfUpdateDeps(env?: NodeJS.ProcessEnv, home?: string): SelfUpdateDeps;
|
|
31
32
|
/** Installs the latest release when it is newer; concurrent callers in this process share one attempt. */
|
package/dist/self-update.js
CHANGED
|
@@ -4,6 +4,7 @@ import { existsSync, realpathSync } from "node:fs";
|
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { basename, dirname, join } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { managedByExtension } from "./config.js";
|
|
7
8
|
import { ADAPTER_VERSION } from "./version.js";
|
|
8
9
|
export class SelfUpdateUnsupportedError extends Error {
|
|
9
10
|
name = "SelfUpdateUnsupportedError";
|
|
@@ -21,7 +22,10 @@ function tryRealpath(path, location) {
|
|
|
21
22
|
function bunInstallDir(location) {
|
|
22
23
|
return location.env.BUN_INSTALL ?? join(location.home, ".bun");
|
|
23
24
|
}
|
|
25
|
+
export const MANAGED_UPDATE_HINT = "antechamber is managed by its OpenChamber extension: update the extension in Settings -> Extensions";
|
|
24
26
|
function locate(location) {
|
|
27
|
+
if (managedByExtension(location.env))
|
|
28
|
+
throw new SelfUpdateUnsupportedError(MANAGED_UPDATE_HINT);
|
|
25
29
|
const root = tryRealpath(location.packageRoot, location) ?? location.packageRoot;
|
|
26
30
|
if (basename(root) !== "antechamber" || basename(dirname(root)) !== "node_modules")
|
|
27
31
|
throw notGlobal(root);
|
|
@@ -139,7 +143,7 @@ function run(binary, args, env, timeout) {
|
|
|
139
143
|
child.stdin?.end();
|
|
140
144
|
});
|
|
141
145
|
}
|
|
142
|
-
/** Prefers
|
|
146
|
+
/** Prefers an npm beside the running runtime (bun or node): service PATHs often lack the user's tool directories. */
|
|
143
147
|
export function npmBinaryFor(execPath = process.execPath) {
|
|
144
148
|
const sibling = join(dirname(execPath), process.platform === "win32" ? "npm.cmd" : "npm");
|
|
145
149
|
return existsSync(sibling) ? sibling : "npm";
|
package/dist/server.d.ts
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
/** HTTP/SSE composition root for the OpenCode API backed by pi. */
|
|
2
|
-
import { type SelfUpdateResult } from "./self-update.js";
|
|
3
2
|
export { isLoopbackHost, serverPassword } from "./http.js";
|
|
4
3
|
interface AdapterServerOptions {
|
|
5
4
|
port: number;
|
|
6
5
|
host: string;
|
|
7
6
|
/** Fallback directory when requests carry no ?directory= param. */
|
|
8
7
|
defaultDirectory: string;
|
|
9
|
-
/** Replaces the npm self-update behind POST /global/upgrade (tests). */
|
|
10
|
-
selfUpdate?: () => Promise<SelfUpdateResult>;
|
|
11
8
|
}
|
|
12
9
|
export declare class AdapterServer {
|
|
13
10
|
private options;
|
|
@@ -30,6 +27,7 @@ export declare class AdapterServer {
|
|
|
30
27
|
private readonly fileWatches;
|
|
31
28
|
private readonly pendingRefreshes;
|
|
32
29
|
private readonly textGeneration;
|
|
30
|
+
private readonly latency;
|
|
33
31
|
private stopping;
|
|
34
32
|
private actualPort;
|
|
35
33
|
/** Bearer token for the `/antechamber/work-status` routes, published in the run record for the OmO Work extension. */
|
package/dist/server.js
CHANGED
|
@@ -13,11 +13,11 @@ import { Http } from "./http.js";
|
|
|
13
13
|
import { SSE } from "./sse.js";
|
|
14
14
|
import { handleV1 } from "./routes-v1.js";
|
|
15
15
|
import { handleV2 } from "./routes-v2.js";
|
|
16
|
-
import { selfUpdate } from "./self-update.js";
|
|
17
16
|
import { handleWorkStatus } from "./work-status.js";
|
|
18
17
|
import { FileWatchRegistry } from "./file-watch.js";
|
|
19
18
|
import { getCommands } from "./session-commands.js";
|
|
20
19
|
import { TextGeneration } from "./text-generation.js";
|
|
20
|
+
import { LatencyMonitor } from "./latency.js";
|
|
21
21
|
export { isLoopbackHost, serverPassword } from "./http.js";
|
|
22
22
|
export class AdapterServer {
|
|
23
23
|
options;
|
|
@@ -40,6 +40,7 @@ export class AdapterServer {
|
|
|
40
40
|
fileWatches = new FileWatchRegistry((directory, event) => this.broadcast(directory, event));
|
|
41
41
|
pendingRefreshes = new Set();
|
|
42
42
|
textGeneration = new TextGeneration();
|
|
43
|
+
latency = new LatencyMonitor();
|
|
43
44
|
stopping = false;
|
|
44
45
|
actualPort;
|
|
45
46
|
/** Bearer token for the `/antechamber/work-status` routes, published in the run record for the OmO Work extension. */
|
|
@@ -179,6 +180,7 @@ export class AdapterServer {
|
|
|
179
180
|
});
|
|
180
181
|
const addr = this.server.address();
|
|
181
182
|
this.actualPort = typeof addr === "object" && addr !== null ? addr.port : this.options.port;
|
|
183
|
+
this.latency.start();
|
|
182
184
|
}
|
|
183
185
|
get url() {
|
|
184
186
|
const port = this.actualPort ?? this.options.port;
|
|
@@ -189,6 +191,7 @@ export class AdapterServer {
|
|
|
189
191
|
}
|
|
190
192
|
async stop() {
|
|
191
193
|
this.stopping = true;
|
|
194
|
+
this.latency.stop();
|
|
192
195
|
await this.textGeneration.dispose();
|
|
193
196
|
await Promise.all([...this.pendingRefreshes]);
|
|
194
197
|
const branchWatchers = [...this.branchWatchers.values()];
|
|
@@ -256,9 +259,9 @@ export class AdapterServer {
|
|
|
256
259
|
writeSSE(res, frame) {
|
|
257
260
|
return this.sse.writeSSE(res, frame);
|
|
258
261
|
}
|
|
259
|
-
handleSSE(req, res,
|
|
262
|
+
handleSSE(req, res, directory) {
|
|
260
263
|
const lastEventID = req.headers["last-event-id"];
|
|
261
|
-
this.sse.handleSSE(res, {
|
|
264
|
+
this.sse.handleSSE(res, { ...(directory !== undefined ? { directory } : {}) }, Array.isArray(lastEventID) ? lastEventID[0] : lastEventID, this.writeSSE.bind(this));
|
|
262
265
|
}
|
|
263
266
|
handleUpgrade(req, socket, _head) {
|
|
264
267
|
const url = new URL(req.url ?? "/", this.url);
|
|
@@ -342,7 +345,7 @@ export class AdapterServer {
|
|
|
342
345
|
// ---------------------------------------------------------------
|
|
343
346
|
async route(req, res) {
|
|
344
347
|
debugLog(`[http] ${req.method} ${(req.url ?? "/").split("?")[0]}`);
|
|
345
|
-
await this.http.route(req, res, () => this.handle(req, res));
|
|
348
|
+
await this.latency.request(req, res, () => this.http.route(req, res, () => this.handle(req, res)));
|
|
346
349
|
}
|
|
347
350
|
async handle(req, res) {
|
|
348
351
|
const url = new URL(req.url ?? "/", this.url);
|
|
@@ -363,15 +366,10 @@ export class AdapterServer {
|
|
|
363
366
|
headerDirectory = decodeURIComponent(encodedDirectory);
|
|
364
367
|
}
|
|
365
368
|
const directory = directoryQuery ?? headerDirectory ?? this.options.defaultDirectory;
|
|
366
|
-
|
|
367
|
-
if (method === "GET" && (path === "/event" || path === "/global/event")) {
|
|
368
|
-
this.handleSSE(req, res, "v1", directory, path === "/global/event");
|
|
369
|
-
return;
|
|
370
|
-
}
|
|
371
|
-
if (method === "GET" && path === "/api/event") {
|
|
369
|
+
if (method === "GET" && (path === "/api/event" || path === "/api/global/event")) {
|
|
372
370
|
// OpenChamber 2.3.2 opens one unscoped /api/event as its global hub (captured);
|
|
373
371
|
// only an explicit directory selector scopes the stream.
|
|
374
|
-
this.handleSSE(req, res,
|
|
372
|
+
this.handleSSE(req, res, directoryQuery === null && headerDirectory === undefined ? undefined : directory);
|
|
375
373
|
return;
|
|
376
374
|
}
|
|
377
375
|
if (path.startsWith("/antechamber/work-status/")) {
|
|
@@ -385,7 +383,6 @@ export class AdapterServer {
|
|
|
385
383
|
body: (request) => this.http.body(request),
|
|
386
384
|
catalog: (dir, refresh) => this.catalog(dir, undefined, refresh === true ? "read" : "cached"),
|
|
387
385
|
broadcast: (dir, event) => this.broadcast(dir, event),
|
|
388
|
-
selfUpdate: this.options.selfUpdate ?? (() => selfUpdate()),
|
|
389
386
|
fileWatches: this.fileWatches,
|
|
390
387
|
commands: (dir) => this.commands(dir),
|
|
391
388
|
vcsBranch: (dir) => this.vcsBranch(dir),
|