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