antechamber 1.0.40 → 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 (70) hide show
  1. package/CHANGELOG.md +126 -0
  2. package/README.md +88 -141
  3. package/bin/opencode-antechamber +14 -4
  4. package/dist/approvals.d.ts +9 -1
  5. package/dist/approvals.js +10 -2
  6. package/dist/catalog.d.ts +13 -2
  7. package/dist/catalog.js +88 -52
  8. package/dist/cli.d.ts +1 -1
  9. package/dist/cli.js +1 -1
  10. package/dist/commands/context.d.ts +5 -2
  11. package/dist/commands/context.js +8 -4
  12. package/dist/commands/doctor.js +4 -3
  13. package/dist/commands/setup.js +9 -28
  14. package/dist/config.d.ts +6 -2
  15. package/dist/config.js +18 -4
  16. package/dist/file-watch.d.ts +35 -0
  17. package/dist/file-watch.js +163 -0
  18. package/dist/latency.d.ts +10 -0
  19. package/dist/latency.js +115 -0
  20. package/dist/misc-routes.js +14 -12
  21. package/dist/pi-event-translation.js +44 -2
  22. package/dist/route-services.d.ts +8 -3
  23. package/dist/routes-v1.d.ts +1 -1
  24. package/dist/routes-v1.js +29 -418
  25. package/dist/routes-v2.js +252 -34
  26. package/dist/self-update.d.ts +2 -1
  27. package/dist/self-update.js +5 -1
  28. package/dist/server.d.ts +17 -3
  29. package/dist/server.js +191 -18
  30. package/dist/session-commands.d.ts +3 -1
  31. package/dist/session-commands.js +13 -7
  32. package/dist/session-history.js +3 -0
  33. package/dist/session-store.d.ts +19 -10
  34. package/dist/session-store.js +266 -128
  35. package/dist/session-tree.d.ts +15 -1
  36. package/dist/session-tree.js +2 -2
  37. package/dist/sessions.d.ts +85 -1
  38. package/dist/sessions.js +238 -10
  39. package/dist/sse.d.ts +0 -2
  40. package/dist/sse.js +13 -46
  41. package/dist/text-generation.d.ts +20 -0
  42. package/dist/text-generation.js +93 -0
  43. package/dist/tool-map.js +1 -3
  44. package/dist/types.d.ts +60 -0
  45. package/dist/v2.d.ts +8 -1
  46. package/dist/v2.js +24 -4
  47. package/dist/work-status.js +1 -1
  48. package/extensions/antechamber/README.md +100 -0
  49. package/extensions/antechamber/bin/opencode-antechamber +42 -0
  50. package/extensions/{omo-dag/docs/DESIGN.md → antechamber/docs/GRAPH.md} +5 -5
  51. package/extensions/{omo-work/docs/DESIGN.md → antechamber/docs/WORK.md} +3 -3
  52. package/extensions/antechamber/package.json +60 -0
  53. package/extensions/{omo-dag → antechamber}/panel/index.html +8 -1
  54. package/extensions/antechamber/panel/main.js +261 -0
  55. package/extensions/antechamber/runtime/launcher.js +201 -0
  56. package/extensions/antechamber/service/main.js +884 -0
  57. package/package.json +75 -26
  58. package/extensions/omo-compat/README.md +0 -48
  59. package/extensions/omo-compat/icon.svg +0 -1
  60. package/extensions/omo-compat/package.json +0 -43
  61. package/extensions/omo-dag/README.md +0 -167
  62. package/extensions/omo-dag/package.json +0 -33
  63. package/extensions/omo-dag/panel/main.js +0 -176
  64. package/extensions/omo-dag/service/main.js +0 -457
  65. package/extensions/omo-work/README.md +0 -88
  66. package/extensions/omo-work/package.json +0 -30
  67. package/extensions/omo-work/service/main.js +0 -149
  68. /package/extensions/{omo-dag → antechamber}/icon.svg +0 -0
  69. /package/extensions/{omo-work → antechamber}/status/index.html +0 -0
  70. /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}`));