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.
Files changed (52) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/README.md +88 -141
  3. package/bin/opencode-antechamber +14 -4
  4. package/dist/cli.d.ts +1 -1
  5. package/dist/cli.js +1 -1
  6. package/dist/commands/context.d.ts +5 -2
  7. package/dist/commands/context.js +8 -4
  8. package/dist/commands/doctor.js +4 -3
  9. package/dist/commands/setup.js +9 -28
  10. package/dist/config.d.ts +6 -2
  11. package/dist/config.js +18 -4
  12. package/dist/latency.d.ts +10 -0
  13. package/dist/latency.js +115 -0
  14. package/dist/misc-routes.js +2 -10
  15. package/dist/route-services.d.ts +0 -2
  16. package/dist/routes-v1.d.ts +1 -1
  17. package/dist/routes-v1.js +29 -418
  18. package/dist/routes-v2.js +1 -1
  19. package/dist/self-update.d.ts +2 -1
  20. package/dist/self-update.js +5 -1
  21. package/dist/server.d.ts +1 -3
  22. package/dist/server.js +9 -12
  23. package/dist/session-store.d.ts +12 -10
  24. package/dist/session-store.js +238 -128
  25. package/dist/session-tree.js +2 -2
  26. package/dist/sse.d.ts +0 -2
  27. package/dist/sse.js +13 -46
  28. package/dist/tool-map.js +1 -3
  29. package/dist/work-status.js +1 -1
  30. package/extensions/antechamber/README.md +100 -0
  31. package/extensions/antechamber/bin/opencode-antechamber +42 -0
  32. package/extensions/{omo-dag/docs/DESIGN.md → antechamber/docs/GRAPH.md} +5 -5
  33. package/extensions/{omo-work/docs/DESIGN.md → antechamber/docs/WORK.md} +3 -3
  34. package/extensions/antechamber/package.json +60 -0
  35. package/extensions/{omo-dag → antechamber}/panel/index.html +8 -1
  36. package/extensions/antechamber/panel/main.js +261 -0
  37. package/extensions/antechamber/runtime/launcher.js +201 -0
  38. package/extensions/antechamber/service/main.js +884 -0
  39. package/package.json +75 -26
  40. package/extensions/omo-compat/README.md +0 -48
  41. package/extensions/omo-compat/icon.svg +0 -1
  42. package/extensions/omo-compat/package.json +0 -43
  43. package/extensions/omo-dag/README.md +0 -167
  44. package/extensions/omo-dag/package.json +0 -33
  45. package/extensions/omo-dag/panel/main.js +0 -176
  46. package/extensions/omo-dag/service/main.js +0 -457
  47. package/extensions/omo-work/README.md +0 -88
  48. package/extensions/omo-work/package.json +0 -30
  49. package/extensions/omo-work/service/main.js +0 -149
  50. /package/extensions/{omo-dag → antechamber}/icon.svg +0 -0
  51. /package/extensions/{omo-work → antechamber}/status/index.html +0 -0
  52. /package/extensions/{omo-work → antechamber}/status/main.js +0 -0
package/dist/routes-v1.js CHANGED
@@ -1,442 +1,53 @@
1
- /** OpenCode v1 routes, using the services composed by AdapterServer. */
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 { getAgentName } from "./agent-name.js";
8
- import { nativePromptInput, parsePromptParts } from "./prompt-parts.js";
9
- import { RequestBodyError } from "./http.js";
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 true;
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 POST /global/upgrade."));
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
- // --- sessions ---
73
- const misc = await miscRoute(method, path, url, directory);
74
- if (misc !== undefined) {
75
- json(res, misc.status, misc.body);
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
- const managed = services.sessions.get(sessionId);
103
- if (!managed) {
104
- json(res, 404, errorBody("NotFoundError", `session not found: ${sessionId}`));
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
- const messageMatch = sub?.match(/^\/message\/([^/]+)$/);
108
- if (messageMatch && method === "GET") {
109
- const messageID = decodeURIComponent(messageMatch[1]);
110
- const message = (services.sessions.messages(sessionId) ?? []).find((candidate) => candidate.info.id === messageID);
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
- // OmO monitors are a child session only for v1 clients; 2.x shows them in the OmO Work section.
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()));
@@ -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 the npm shipped beside the running node: service PATHs often lack the user's tool directories. */
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. */
@@ -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 the npm shipped beside the running node: service PATHs often lack the user's tool directories. */
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, flavor, directory, global) {
262
+ handleSSE(req, res, directory) {
260
263
  const lastEventID = req.headers["last-event-id"];
261
- this.sse.handleSSE(res, { flavor, global, ...(global ? {} : { directory }) }, Array.isArray(lastEventID) ? lastEventID[0] : lastEventID, this.writeSSE.bind(this));
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
- // SSE stream (v2 clients subscribe at /api/event; v1 at /event)
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, "v2", directory, directoryQuery === null && headerDirectory === undefined);
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),