antechamber 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/CHANGELOG.md +143 -0
  2. package/LICENSE +22 -0
  3. package/README.md +316 -2
  4. package/bin/opencode-antechamber +18 -0
  5. package/bin/opencode-pi +3 -0
  6. package/dist/agent-dir.d.ts +6 -0
  7. package/dist/agent-dir.js +21 -0
  8. package/dist/agent-name.d.ts +24 -0
  9. package/dist/agent-name.js +110 -0
  10. package/dist/approvals.d.ts +86 -0
  11. package/dist/approvals.js +520 -0
  12. package/dist/catalog.d.ts +88 -0
  13. package/dist/catalog.js +118 -0
  14. package/dist/cli.d.ts +2 -0
  15. package/dist/cli.js +86 -0
  16. package/dist/commands/context.d.ts +26 -0
  17. package/dist/commands/context.js +89 -0
  18. package/dist/commands/doctor.d.ts +13 -0
  19. package/dist/commands/doctor.js +92 -0
  20. package/dist/commands/help.d.ts +1 -0
  21. package/dist/commands/help.js +15 -0
  22. package/dist/commands/serve.d.ts +8 -0
  23. package/dist/commands/serve.js +29 -0
  24. package/dist/commands/setup.d.ts +14 -0
  25. package/dist/commands/setup.js +137 -0
  26. package/dist/config.d.ts +18 -0
  27. package/dist/config.js +40 -0
  28. package/dist/http.d.ts +18 -0
  29. package/dist/http.js +168 -0
  30. package/dist/lifecycle.d.ts +43 -0
  31. package/dist/lifecycle.js +138 -0
  32. package/dist/misc-routes.d.ts +8 -0
  33. package/dist/misc-routes.js +119 -0
  34. package/dist/pi-event-translation.d.ts +65 -0
  35. package/dist/pi-event-translation.js +545 -0
  36. package/dist/pi-rpc.d.ts +68 -0
  37. package/dist/pi-rpc.js +285 -0
  38. package/dist/pidfd.d.ts +2 -0
  39. package/dist/pidfd.js +28 -0
  40. package/dist/question-forms.d.ts +30 -0
  41. package/dist/question-forms.js +46 -0
  42. package/dist/route-services.d.ts +26 -0
  43. package/dist/route-services.js +33 -0
  44. package/dist/routes-v1.d.ts +4 -0
  45. package/dist/routes-v1.js +346 -0
  46. package/dist/routes-v2.d.ts +4 -0
  47. package/dist/routes-v2.js +477 -0
  48. package/dist/server.d.ts +28 -0
  49. package/dist/server.js +113 -0
  50. package/dist/session-commands.d.ts +75 -0
  51. package/dist/session-commands.js +323 -0
  52. package/dist/session-diff.d.ts +14 -0
  53. package/dist/session-diff.js +152 -0
  54. package/dist/session-history.d.ts +35 -0
  55. package/dist/session-history.js +416 -0
  56. package/dist/session-hosts.d.ts +3 -0
  57. package/dist/session-hosts.js +188 -0
  58. package/dist/session-store.d.ts +103 -0
  59. package/dist/session-store.js +339 -0
  60. package/dist/session-tree.d.ts +59 -0
  61. package/dist/session-tree.js +324 -0
  62. package/dist/sessions.d.ts +174 -0
  63. package/dist/sessions.js +1141 -0
  64. package/dist/sse.d.ts +19 -0
  65. package/dist/sse.js +92 -0
  66. package/dist/subagents.d.ts +64 -0
  67. package/dist/subagents.js +621 -0
  68. package/dist/thinking-levels.d.ts +17 -0
  69. package/dist/thinking-levels.js +24 -0
  70. package/dist/todo-bridge.d.ts +26 -0
  71. package/dist/todo-bridge.js +100 -0
  72. package/dist/tool-map.d.ts +32 -0
  73. package/dist/tool-map.js +349 -0
  74. package/dist/types.d.ts +338 -0
  75. package/dist/types.js +6 -0
  76. package/dist/v2.d.ts +190 -0
  77. package/dist/v2.js +553 -0
  78. package/dist/version.d.ts +2 -0
  79. package/dist/version.js +13 -0
  80. package/extensions/omo-dag/README.md +159 -0
  81. package/extensions/omo-dag/docs/DESIGN.md +105 -0
  82. package/extensions/omo-dag/icon.svg +1 -0
  83. package/extensions/omo-dag/package.json +33 -0
  84. package/extensions/omo-dag/panel/index.html +179 -0
  85. package/extensions/omo-dag/panel/main.js +176 -0
  86. package/extensions/omo-dag/service/main.js +378 -0
  87. package/package.json +77 -4
@@ -0,0 +1,346 @@
1
+ /** OpenCode v1 routes, using the services composed by AdapterServer. */
2
+ import { execFile } from "node:child_process";
3
+ import { homedir } from "node:os";
4
+ import { errorBody, json } from "./http.js";
5
+ import { miscRoute } from "./misc-routes.js";
6
+ import { getAgentName, getExposedAgentNames } from "./agent-name.js";
7
+ import { agentDefaults } from "./catalog.js";
8
+ import { announceSessionError, compactSession, getCommands, parseShellCommand, parseSlashCommand, runShellCommand, runSlashCommand, unsupportedShare, } from "./session-commands.js";
9
+ import { modelRefFromBody, OPENCODE_COMPAT_VERSION, promptAsync } from "./route-services.js";
10
+ /**
11
+ * `archived` list filter per SDK v2 (`boolean | "true" | "false"`):
12
+ * "true" keeps only archived sessions, "false" only live ones, absent = all.
13
+ */
14
+ function applyArchivedFilter(sessions, archived) {
15
+ if (archived === "true")
16
+ return sessions.filter((s) => s.time.archived !== undefined);
17
+ if (archived === "false")
18
+ return sessions.filter((s) => s.time.archived === undefined);
19
+ return sessions;
20
+ }
21
+ export async function handleV1(services, req, res, url, path, method, directory) {
22
+ if (method === "GET" && path === "/global/health") {
23
+ json(res, 200, OPENCODE_COMPAT_VERSION === null ? { healthy: true } : { healthy: true, version: OPENCODE_COMPAT_VERSION });
24
+ return;
25
+ }
26
+ // There is no opencode install behind this endpoint to upgrade: the adapter
27
+ // speaks the OpenCode API itself. Answer instead of 404 so a direct upgrade
28
+ // call gets a clear reason rather than a bare HTTP status.
29
+ if ((method === "POST" || method === "GET") && path === "/global/upgrade") {
30
+ json(res, 409, errorBody("UnsupportedError", "This endpoint is served by antechamber (a pi adapter), not a real opencode install — there is nothing to upgrade."));
31
+ return;
32
+ }
33
+ // --- sessions ---
34
+ const misc = await miscRoute(method, path, url, directory);
35
+ if (misc !== undefined) {
36
+ json(res, misc.status, misc.body);
37
+ return;
38
+ }
39
+ if (path === "/session" && method === "POST") {
40
+ const body = (await services.body(req));
41
+ json(res, 200, await services.sessions.create(directory, body.title, body.parentID));
42
+ return;
43
+ }
44
+ if (path === "/session" && method === "GET") {
45
+ const dir = url.searchParams.get("directory") ?? undefined;
46
+ let sessions = services.sessions.list(dir);
47
+ sessions = applyArchivedFilter(sessions, url.searchParams.get("archived"));
48
+ json(res, 200, sessions);
49
+ return;
50
+ }
51
+ if (path === "/session/status" && method === "GET") {
52
+ json(res, 200, services.sessions.statuses());
53
+ return;
54
+ }
55
+ const sessionMatch = path.match(/^\/session\/([^/]+)(\/.*)?$/);
56
+ if (sessionMatch) {
57
+ const [, sessionId, sub] = sessionMatch;
58
+ if (sessionId === undefined) {
59
+ json(res, 404, errorBody("NotFoundError", `session not found: ${sessionId}`));
60
+ return;
61
+ }
62
+ const managed = services.sessions.get(sessionId);
63
+ if (!managed) {
64
+ json(res, 404, errorBody("NotFoundError", `session not found: ${sessionId}`));
65
+ return;
66
+ }
67
+ switch (`${method} ${sub ?? ""}`) {
68
+ case "GET /diff":
69
+ json(res, 200, await services.sessions.diff(sessionId));
70
+ return;
71
+ case "GET ":
72
+ json(res, 200, managed.info);
73
+ return;
74
+ case "PATCH ": {
75
+ const body = (await services.body(req));
76
+ json(res, 200, services.sessions.update(sessionId, { title: body.title, archived: body.time?.archived }));
77
+ return;
78
+ }
79
+ case "DELETE ":
80
+ json(res, 200, await services.sessions.remove(sessionId));
81
+ return;
82
+ case "GET /message":
83
+ json(res, 200, services.sessions.messages(sessionId));
84
+ return;
85
+ case "GET /todo":
86
+ json(res, 200, services.sessions.todos(sessionId));
87
+ return;
88
+ case "POST /prompt_async": {
89
+ const body = (await services.body(req));
90
+ const text = (body.parts ?? [])
91
+ .filter((p) => p.type === "text" && typeof p.text === "string")
92
+ .map((p) => p.text ?? "")
93
+ .join("\n");
94
+ const model = modelRefFromBody(body.model ?? (body.variant !== undefined ? managed.model : undefined), body.variant);
95
+ promptAsync(services, managed, text, model, body.noReply, body.messageID);
96
+ res.writeHead(204).end();
97
+ return;
98
+ }
99
+ case "POST /message": {
100
+ const body = (await services.body(req));
101
+ const text = (body.parts ?? [])
102
+ .filter((p) => p.type === "text" && typeof p.text === "string")
103
+ .map((p) => p.text ?? "")
104
+ .join("\n");
105
+ const model = modelRefFromBody(body.model ?? (body.variant !== undefined ? managed.model : undefined), body.variant);
106
+ await services.sessions.prompt(sessionId, text, model, body.noReply, body.messageID);
107
+ const msgs = services.sessions.messages(sessionId) ?? [];
108
+ json(res, 200, msgs[msgs.length - 1] ?? null);
109
+ return;
110
+ }
111
+ case "POST /abort":
112
+ json(res, 200, await services.sessions.abort(sessionId));
113
+ return;
114
+ case "POST /fork":
115
+ case "POST /revert":
116
+ case "POST /unrevert": {
117
+ const body = await services.body(req);
118
+ const messageID = body !== null && typeof body === "object" && "messageID" in body ? body.messageID : undefined;
119
+ if ((sub === "/revert" && (typeof messageID !== "string" || messageID === "")) ||
120
+ (sub === "/fork" && messageID !== undefined && (typeof messageID !== "string" || messageID === ""))) {
121
+ json(res, 400, errorBody("InvalidRequest", "messageID must be a nonempty string"));
122
+ return;
123
+ }
124
+ try {
125
+ const info = sub === "/fork"
126
+ ? await services.sessions.fork(sessionId, typeof messageID === "string" ? messageID : undefined)
127
+ : sub === "/revert"
128
+ ? await services.sessions.revert(sessionId, String(messageID))
129
+ : await services.sessions.unrevert(sessionId);
130
+ json(res, 200, info);
131
+ }
132
+ catch (error) {
133
+ json(res, 400, errorBody("SessionTreeError", error instanceof Error ? error.message : String(error)));
134
+ }
135
+ return;
136
+ }
137
+ case "POST /summarize": {
138
+ await services.body(req);
139
+ const result = await compactSession(services.sessions, sessionId);
140
+ if (result.kind === "error") {
141
+ announceSessionError({ sessions: services.sessions, session: managed, emit: services.broadcast }, "BadRequestError", result.message);
142
+ json(res, 400, errorBody("BadRequestError", result.message));
143
+ return;
144
+ }
145
+ json(res, 200, true);
146
+ return;
147
+ }
148
+ case "POST /shell": {
149
+ const command = parseShellCommand(await services.body(req));
150
+ if (command === undefined) {
151
+ json(res, 400, errorBody("BadRequestError", "command is required"));
152
+ return;
153
+ }
154
+ const managed = services.sessions.get(sessionId);
155
+ if (!managed) {
156
+ json(res, 404, errorBody("NotFoundError", `session not found: ${sessionId}`));
157
+ return;
158
+ }
159
+ if (managed.status.type !== "idle") {
160
+ json(res, 409, errorBody("SessionBusyError", "session is busy"));
161
+ return;
162
+ }
163
+ const result = await runShellCommand({
164
+ sessions: services.sessions,
165
+ session: managed,
166
+ command,
167
+ emit: services.broadcast,
168
+ });
169
+ if (result.kind === "error") {
170
+ json(res, 400, errorBody("BadRequestError", result.message));
171
+ return;
172
+ }
173
+ json(res, 200, result.message.info);
174
+ return;
175
+ }
176
+ case "POST /command": {
177
+ const input = parseSlashCommand(await services.body(req));
178
+ if (input === undefined) {
179
+ json(res, 400, errorBody("BadRequestError", "command is required"));
180
+ return;
181
+ }
182
+ const message = await runSlashCommand(services.sessions, sessionId, input);
183
+ json(res, 200, message === undefined ? { info: null, parts: [] } : { info: message.info, parts: message.parts });
184
+ return;
185
+ }
186
+ case "POST /share":
187
+ case "DELETE /share": {
188
+ const session = services.sessions.get(sessionId);
189
+ if (session)
190
+ announceSessionError({ sessions: services.sessions, session, emit: services.broadcast }, unsupportedShare.name, unsupportedShare.data.message);
191
+ json(res, 400, errorBody(unsupportedShare.name, unsupportedShare.data.message));
192
+ return;
193
+ }
194
+ default:
195
+ json(res, 501, errorBody("NotImplemented", `${method} ${path} not implemented (M1)`));
196
+ return;
197
+ }
198
+ }
199
+ // --- project / config ---
200
+ if (path === "/project/current" && method === "GET") {
201
+ json(res, 200, { id: directory, worktree: directory, time: { created: Date.now() } });
202
+ return;
203
+ }
204
+ if (path === "/project" && method === "GET") {
205
+ json(res, 200, [{ id: directory, worktree: directory, time: { created: Date.now() } }]);
206
+ return;
207
+ }
208
+ if ((path === "/config" || path === "/global/config") && method === "GET") {
209
+ json(res, 200, { default_agent: getAgentName() });
210
+ return;
211
+ }
212
+ // --- model catalog (from pi) ---
213
+ if (path === "/provider" && method === "GET") {
214
+ const cat = await services.catalog();
215
+ json(res, 200, { all: cat.providers, default: cat.defaults, connected: cat.providers.map((p) => p.id) });
216
+ return;
217
+ }
218
+ if (path === "/config/providers" && method === "GET") {
219
+ const cat = await services.catalog();
220
+ json(res, 200, { providers: cat.providers, default: cat.defaults });
221
+ return;
222
+ }
223
+ // --- bootstrap probes OpenChamber expects to exist ---
224
+ if (path === "/experimental/session" && method === "GET") {
225
+ // OpenChamber page-loads the sidebar through this route (not /session) with
226
+ // ?directory=…&archived=…&roots=…&limit=…. Honor directory/roots/archived:
227
+ // ignoring them left the sidebar with only live sessions, so restored
228
+ // history never appeared after a restart. archived=false excludes archived
229
+ // sessions; archived=true includes them so the UI can partition both lists.
230
+ const directoryParam = url.searchParams.get("directory") ?? req.headers["x-opencode-directory"];
231
+ const rootsOnly = url.searchParams.get("roots") === "true";
232
+ let sessions = directoryParam !== undefined && directoryParam !== ""
233
+ ? services.sessions.list(directoryParam)
234
+ : services.sessions.listAll();
235
+ if (rootsOnly)
236
+ sessions = sessions.filter((s) => s.parentID === undefined);
237
+ // Experimental lists use archived=true to INCLUDE archived sessions.
238
+ // The UI partitions this result itself; filtering to archived-only drops
239
+ // the current live session from the header's global metadata store.
240
+ if (url.searchParams.get("archived") === "false") {
241
+ sessions = sessions.filter((s) => s.time.archived === undefined);
242
+ }
243
+ json(res, 200, sessions);
244
+ return;
245
+ }
246
+ if (path === "/question" && method === "GET") {
247
+ json(res, 200, services.sessions.questions());
248
+ return;
249
+ }
250
+ if (path === "/permission" && method === "GET") {
251
+ json(res, 200, services.sessions.permissions());
252
+ return;
253
+ }
254
+ const questionReply = path.match(/^\/question\/([^/]+)\/reply$/);
255
+ if (questionReply && method === "POST") {
256
+ const requestID = decodeURIComponent(questionReply[1]);
257
+ const body = (await services.body(req));
258
+ const pending = services.sessions.questions().find((question) => question.id === requestID);
259
+ if (!pending) {
260
+ json(res, 404, errorBody("QuestionNotFoundError", `question not found: ${requestID}`));
261
+ return;
262
+ }
263
+ if (!(await services.sessions.replyQuestion(pending.sessionID, requestID, body.answers ?? [], body.comment))) {
264
+ json(res, 409, errorBody("QuestionReplyError", "native question response refused"));
265
+ return;
266
+ }
267
+ json(res, 200, true);
268
+ return;
269
+ }
270
+ const questionReject = path.match(/^\/question\/([^/]+)\/reject$/);
271
+ if (questionReject && method === "POST") {
272
+ const requestID = decodeURIComponent(questionReject[1]);
273
+ const pending = services.sessions.questions().find((question) => question.id === requestID);
274
+ if (!pending) {
275
+ json(res, 404, errorBody("QuestionNotFoundError", `question not found: ${requestID}`));
276
+ return;
277
+ }
278
+ if (!(await services.sessions.rejectQuestion(pending.sessionID, requestID))) {
279
+ json(res, 409, errorBody("QuestionReplyError", "native question cancellation refused"));
280
+ return;
281
+ }
282
+ json(res, 200, true);
283
+ return;
284
+ }
285
+ const permissionReply = path.match(/^\/permission\/([^/]+)\/reply$/);
286
+ if (permissionReply && method === "POST") {
287
+ const requestID = decodeURIComponent(permissionReply[1]);
288
+ const body = (await services.body(req));
289
+ const pending = services.sessions.permissions().find((permission) => permission.id === requestID);
290
+ if (!pending ||
291
+ !body.reply ||
292
+ !services.sessions.replyPermission(pending.sessionID, requestID, body.reply, body.message)) {
293
+ json(res, 404, errorBody("PermissionNotFoundError", `permission not found: ${requestID}`));
294
+ return;
295
+ }
296
+ json(res, 200, true);
297
+ return;
298
+ }
299
+ if (path === "/lsp" && method === "GET") {
300
+ json(res, 200, []);
301
+ return;
302
+ }
303
+ if (path === "/formatter" && method === "GET") {
304
+ json(res, 200, []);
305
+ return;
306
+ }
307
+ if (path === "/mcp" && method === "GET") {
308
+ json(res, 200, {});
309
+ return;
310
+ }
311
+ if (path === "/command" && method === "GET") {
312
+ json(res, 200, await getCommands(directory));
313
+ return;
314
+ }
315
+ if (path === "/path" && method === "GET") {
316
+ json(res, 200, { home: homedir(), state: "", config: "", worktree: directory, directory });
317
+ return;
318
+ }
319
+ if (path === "/vcs" && method === "GET") {
320
+ execFile("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd: directory, timeout: 3000 }, (err, stdout) => {
321
+ const branch = err ? undefined : stdout.trim() || undefined;
322
+ json(res, 200, branch === undefined ? {} : { branch });
323
+ });
324
+ return;
325
+ }
326
+ if (path === "/agent" && method === "GET") {
327
+ // pi has one agent; publish OpenChamber's conventional `build` primary
328
+ // agent next to pi's own name so the picker — and its default resolution,
329
+ // which looks for `build` — has a real option. Both names run the same
330
+ // pi agent (see getExposedAgentNames).
331
+ const piName = getAgentName();
332
+ const defaults = agentDefaults(await services.catalog());
333
+ json(res, 200, getExposedAgentNames().map((name) => ({
334
+ name,
335
+ description: name === piName
336
+ ? `${piName} (pi coding agent via antechamber)`
337
+ : `${name} — ${piName} (pi coding agent via antechamber)`,
338
+ mode: "primary",
339
+ hidden: name === "build" && piName !== "build",
340
+ options: {},
341
+ ...defaults,
342
+ })));
343
+ return;
344
+ }
345
+ json(res, 404, errorBody("NotFoundError", `no route: ${method} ${path}`));
346
+ }
@@ -0,0 +1,4 @@
1
+ /** OpenCode v2 (/api/*) routes and their response adapters. */
2
+ import type { IncomingMessage, ServerResponse } from "node:http";
3
+ import { type RouteServices } from "./route-services.js";
4
+ export declare function handleV2(services: RouteServices, req: IncomingMessage, res: ServerResponse, url: URL, path: string, method: string, directory: string): Promise<void>;