antechamber 0.1.1 → 0.2.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 (56) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +17 -14
  3. package/bin/opencode-antechamber +10 -1
  4. package/bin/opencode-pi +11 -1
  5. package/dist/approvals.d.ts +4 -1
  6. package/dist/approvals.js +51 -33
  7. package/dist/catalog.d.ts +1 -1
  8. package/dist/catalog.js +1 -1
  9. package/dist/commands/context.d.ts +0 -1
  10. package/dist/commands/context.js +1 -1
  11. package/dist/commands/doctor.d.ts +3 -2
  12. package/dist/commands/serve.d.ts +2 -1
  13. package/dist/commands/setup.d.ts +2 -3
  14. package/dist/commands/setup.js +16 -4
  15. package/dist/http.d.ts +8 -1
  16. package/dist/http.js +42 -19
  17. package/dist/pi-event-translation.d.ts +9 -5
  18. package/dist/pi-event-translation.js +233 -79
  19. package/dist/pi-rpc.d.ts +20 -2
  20. package/dist/pi-rpc.js +96 -29
  21. package/dist/prompt-parts.d.ts +40 -0
  22. package/dist/prompt-parts.js +157 -0
  23. package/dist/route-services.d.ts +45 -2
  24. package/dist/route-services.js +70 -3
  25. package/dist/routes-v1.js +110 -65
  26. package/dist/routes-v2.js +376 -73
  27. package/dist/server.d.ts +9 -1
  28. package/dist/server.js +109 -9
  29. package/dist/session-commands.d.ts +12 -15
  30. package/dist/session-commands.js +52 -11
  31. package/dist/session-diff.d.ts +1 -1
  32. package/dist/session-diff.js +10 -2
  33. package/dist/session-history.js +108 -42
  34. package/dist/session-hosts.js +2 -2
  35. package/dist/session-store.d.ts +8 -4
  36. package/dist/session-store.js +183 -33
  37. package/dist/session-tree.d.ts +9 -1
  38. package/dist/session-tree.js +51 -26
  39. package/dist/sessions.d.ts +32 -5
  40. package/dist/sessions.js +330 -119
  41. package/dist/sse.d.ts +16 -6
  42. package/dist/sse.js +77 -21
  43. package/dist/subagents.d.ts +5 -0
  44. package/dist/subagents.js +84 -71
  45. package/dist/tool-map.d.ts +3 -2
  46. package/dist/types.d.ts +152 -1
  47. package/dist/v2.d.ts +18 -4
  48. package/dist/v2.js +128 -44
  49. package/dist/version.d.ts +1 -1
  50. package/dist/version.js +1 -1
  51. package/extensions/omo-dag/README.md +2 -0
  52. package/extensions/omo-dag/docs/DESIGN.md +10 -1
  53. package/extensions/omo-dag/panel/index.html +1 -1
  54. package/extensions/omo-dag/panel/main.js +60 -60
  55. package/extensions/omo-dag/service/main.js +72 -8
  56. package/package.json +1 -1
package/dist/routes-v1.js CHANGED
@@ -3,10 +3,11 @@ import { execFile } from "node:child_process";
3
3
  import { homedir } from "node:os";
4
4
  import { errorBody, json } from "./http.js";
5
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";
6
+ import { getAgentName } from "./agent-name.js";
7
+ import { nativePromptInput, parsePromptParts } from "./prompt-parts.js";
8
+ import { RequestBodyError } from "./http.js";
9
+ import { announceSessionError, compactSession, getCommands, unsupportedShare } from "./session-commands.js";
10
+ import { agentDescriptors, modelRefFromBody, numberParam, OPENCODE_COMPAT_VERSION, promptAsync, runSessionShell, runSessionSlash, sessionDirectory, sessionMetadata, } from "./route-services.js";
10
11
  /**
11
12
  * `archived` list filter per SDK v2 (`boolean | "true" | "false"`):
12
13
  * "true" keeps only archived sessions, "false" only live ones, absent = all.
@@ -18,18 +19,33 @@ function applyArchivedFilter(sessions, archived) {
18
19
  return sessions.filter((s) => s.time.archived === undefined);
19
20
  return sessions;
20
21
  }
21
- export async function handleV1(services, req, res, url, path, method, directory) {
22
+ function handleV1GlobalRoute(res, path, method) {
22
23
  if (method === "GET" && path === "/global/health") {
23
24
  json(res, 200, OPENCODE_COMPAT_VERSION === null ? { healthy: true } : { healthy: true, version: OPENCODE_COMPAT_VERSION });
24
- return;
25
+ return true;
25
26
  }
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
27
  if ((method === "POST" || method === "GET") && path === "/global/upgrade") {
30
28
  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;
29
+ return true;
32
30
  }
31
+ return false;
32
+ }
33
+ function parseV1Prompt(body, defaultModel) {
34
+ const parts = parsePromptParts(body.parts);
35
+ if (body.delivery !== undefined && body.delivery !== "steer" && body.delivery !== "queue")
36
+ throw new RequestBodyError(400, "delivery must be steer or queue");
37
+ const text = parts
38
+ .filter((part) => part.type === "text")
39
+ .map((part) => part.text ?? "")
40
+ .join("\n");
41
+ const model = modelRefFromBody(body.model ?? (body.variant !== undefined ? defaultModel : undefined), body.variant);
42
+ if (!body.noReply)
43
+ nativePromptInput(text, parts);
44
+ return { text, model, parts, delivery: body.delivery ?? "steer" };
45
+ }
46
+ export async function handleV1(services, req, res, url, path, method, directory) {
47
+ if (handleV1GlobalRoute(res, path, method))
48
+ return;
33
49
  // --- sessions ---
34
50
  const misc = await miscRoute(method, path, url, directory);
35
51
  if (misc !== undefined) {
@@ -38,7 +54,8 @@ export async function handleV1(services, req, res, url, path, method, directory)
38
54
  }
39
55
  if (path === "/session" && method === "POST") {
40
56
  const body = (await services.body(req));
41
- json(res, 200, await services.sessions.create(directory, body.title, body.parentID));
57
+ const metadata = sessionMetadata(body.metadata);
58
+ json(res, 200, await services.sessions.create(sessionDirectory(directory), body.title, body.parentID, metadata === undefined ? {} : { metadata }));
42
59
  return;
43
60
  }
44
61
  if (path === "/session" && method === "GET") {
@@ -64,46 +81,58 @@ export async function handleV1(services, req, res, url, path, method, directory)
64
81
  json(res, 404, errorBody("NotFoundError", `session not found: ${sessionId}`));
65
82
  return;
66
83
  }
84
+ const messageMatch = sub?.match(/^\/message\/([^/]+)$/);
85
+ if (messageMatch && method === "GET") {
86
+ const messageID = decodeURIComponent(messageMatch[1]);
87
+ const message = (services.sessions.messages(sessionId) ?? []).find((candidate) => candidate.info.id === messageID);
88
+ if (message === undefined)
89
+ json(res, 404, errorBody("NotFoundError", `message not found: ${messageID}`));
90
+ else
91
+ json(res, 200, message);
92
+ return;
93
+ }
67
94
  switch (`${method} ${sub ?? ""}`) {
68
95
  case "GET /diff":
69
96
  json(res, 200, await services.sessions.diff(sessionId));
70
97
  return;
71
98
  case "GET ":
72
- json(res, 200, managed.info);
99
+ json(res, 200, { ...managed.info, cost: managed.info.cost ?? 0 });
73
100
  return;
74
101
  case "PATCH ": {
75
102
  const body = (await services.body(req));
76
- json(res, 200, services.sessions.update(sessionId, { title: body.title, archived: body.time?.archived }));
103
+ json(res, 200, await services.sessions.update(sessionId, {
104
+ title: body.title,
105
+ archived: body.time?.archived,
106
+ metadata: sessionMetadata(body.metadata),
107
+ }));
77
108
  return;
78
109
  }
79
110
  case "DELETE ":
80
111
  json(res, 200, await services.sessions.remove(sessionId));
81
112
  return;
82
- case "GET /message":
83
- json(res, 200, services.sessions.messages(sessionId));
113
+ case "GET /message": {
114
+ const messages = services.sessions.messages(sessionId) ?? [];
115
+ const limit = numberParam(url.searchParams.get("limit"));
116
+ json(res, 200, limit === undefined ? messages : limit === 0 ? [] : messages.slice(-limit));
117
+ return;
118
+ }
119
+ case "GET /children":
120
+ json(res, 200, services.sessions.list(directory).filter((candidate) => candidate.parentID === sessionId));
84
121
  return;
85
122
  case "GET /todo":
86
123
  json(res, 200, services.sessions.todos(sessionId));
87
124
  return;
88
125
  case "POST /prompt_async": {
89
126
  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);
127
+ const { text, model, parts, delivery } = parseV1Prompt(body, managed.model);
128
+ promptAsync(services, managed, text, model, body.noReply, body.messageID, delivery, parts);
96
129
  res.writeHead(204).end();
97
130
  return;
98
131
  }
99
132
  case "POST /message": {
100
133
  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);
134
+ const { text, model, parts, delivery } = parseV1Prompt(body, managed.model);
135
+ await services.sessions.prompt(sessionId, text, model, body.noReply, body.messageID, delivery, parts);
107
136
  const msgs = services.sessions.messages(sessionId) ?? [];
108
137
  json(res, 200, msgs[msgs.length - 1] ?? null);
109
138
  return;
@@ -146,40 +175,34 @@ export async function handleV1(services, req, res, url, path, method, directory)
146
175
  return;
147
176
  }
148
177
  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
178
  const managed = services.sessions.get(sessionId);
155
179
  if (!managed) {
156
180
  json(res, 404, errorBody("NotFoundError", `session not found: ${sessionId}`));
157
181
  return;
158
182
  }
159
- if (managed.status.type !== "idle") {
183
+ const result = await runSessionShell(services, managed, await services.body(req));
184
+ if (result.kind === "input") {
185
+ json(res, 400, errorBody("BadRequestError", "command is required"));
186
+ return;
187
+ }
188
+ if (result.kind === "busy") {
160
189
  json(res, 409, errorBody("SessionBusyError", "session is busy"));
161
190
  return;
162
191
  }
163
- const result = await runShellCommand({
164
- sessions: services.sessions,
165
- session: managed,
166
- command,
167
- emit: services.broadcast,
168
- });
169
192
  if (result.kind === "error") {
170
193
  json(res, 400, errorBody("BadRequestError", result.message));
171
194
  return;
172
195
  }
173
- json(res, 200, result.message.info);
196
+ json(res, 200, result.message);
174
197
  return;
175
198
  }
176
199
  case "POST /command": {
177
- const input = parseSlashCommand(await services.body(req));
178
- if (input === undefined) {
200
+ const result = await runSessionSlash(services, sessionId, await services.body(req));
201
+ if (result.kind === "input") {
179
202
  json(res, 400, errorBody("BadRequestError", "command is required"));
180
203
  return;
181
204
  }
182
- const message = await runSlashCommand(services.sessions, sessionId, input);
205
+ const { message } = result;
183
206
  json(res, 200, message === undefined ? { info: null, parts: [] } : { info: message.info, parts: message.parts });
184
207
  return;
185
208
  }
@@ -198,15 +221,39 @@ export async function handleV1(services, req, res, url, path, method, directory)
198
221
  }
199
222
  // --- project / config ---
200
223
  if (path === "/project/current" && method === "GET") {
201
- json(res, 200, { id: directory, worktree: directory, time: { created: Date.now() } });
224
+ json(res, 200, {
225
+ id: directory,
226
+ worktree: directory,
227
+ vcs: "git",
228
+ time: { created: Date.now(), updated: Date.now() },
229
+ sandboxes: [],
230
+ });
202
231
  return;
203
232
  }
204
233
  if (path === "/project" && method === "GET") {
205
- json(res, 200, [{ id: directory, worktree: directory, time: { created: Date.now() } }]);
234
+ json(res, 200, [
235
+ {
236
+ id: directory,
237
+ worktree: directory,
238
+ vcs: "git",
239
+ time: { created: Date.now(), updated: Date.now() },
240
+ sandboxes: [],
241
+ },
242
+ ]);
206
243
  return;
207
244
  }
208
245
  if ((path === "/config" || path === "/global/config") && method === "GET") {
209
- json(res, 200, { default_agent: getAgentName() });
246
+ const cat = await services.catalog();
247
+ json(res, 200, {
248
+ $schema: "https://opencode.ai/config.json",
249
+ default_agent: getAgentName(),
250
+ plugin: [],
251
+ command: {},
252
+ mode: {},
253
+ agent: {},
254
+ username: process.env.OPENCODE_SERVER_USERNAME ?? "opencode",
255
+ ...(cat.default ? { model: `${cat.default.providerID}/${cat.default.modelID}` } : {}),
256
+ });
210
257
  return;
211
258
  }
212
259
  // --- model catalog (from pi) ---
@@ -240,7 +287,15 @@ export async function handleV1(services, req, res, url, path, method, directory)
240
287
  if (url.searchParams.get("archived") === "false") {
241
288
  sessions = sessions.filter((s) => s.time.archived === undefined);
242
289
  }
243
- json(res, 200, sessions);
290
+ sessions.sort((a, b) => b.time.updated - a.time.updated || a.id.localeCompare(b.id));
291
+ const cursor = numberParam(url.searchParams.get("cursor"));
292
+ if (cursor !== undefined)
293
+ sessions = sessions.filter((session) => session.time.updated < cursor);
294
+ const limit = numberParam(url.searchParams.get("limit"));
295
+ const page = limit === undefined ? sessions : sessions.slice(0, limit);
296
+ if (limit !== undefined && page.length < sessions.length && page.at(-1)?.time.updated !== undefined)
297
+ res.setHeader("x-next-cursor", String(page.at(-1).time.updated));
298
+ json(res, 200, page);
244
299
  return;
245
300
  }
246
301
  if (path === "/question" && method === "GET") {
@@ -309,7 +364,12 @@ export async function handleV1(services, req, res, url, path, method, directory)
309
364
  return;
310
365
  }
311
366
  if (path === "/command" && method === "GET") {
312
- json(res, 200, await getCommands(directory));
367
+ json(res, 200, (await getCommands(directory)).map((command) => ({
368
+ ...command,
369
+ source: "command",
370
+ hints: [],
371
+ subtask: false,
372
+ })));
313
373
  return;
314
374
  }
315
375
  if (path === "/path" && method === "GET") {
@@ -319,27 +379,12 @@ export async function handleV1(services, req, res, url, path, method, directory)
319
379
  if (path === "/vcs" && method === "GET") {
320
380
  execFile("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd: directory, timeout: 3000 }, (err, stdout) => {
321
381
  const branch = err ? undefined : stdout.trim() || undefined;
322
- json(res, 200, branch === undefined ? {} : { branch });
382
+ json(res, 200, { branch: branch ?? null, default_branch: null });
323
383
  });
324
384
  return;
325
385
  }
326
386
  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
- })));
387
+ json(res, 200, await agentDescriptors(services));
343
388
  return;
344
389
  }
345
390
  json(res, 404, errorBody("NotFoundError", `no route: ${method} ${path}`));