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,520 @@
1
+ import { createHash } from "node:crypto";
2
+ export class ApprovalInputError extends Error {
3
+ }
4
+ function record(value) {
5
+ return typeof value === "object" && value !== null && !Array.isArray(value)
6
+ ? value
7
+ : undefined;
8
+ }
9
+ export function extensionUiRequestFrom(value) {
10
+ const { id, method, title, options, toolCallId, timeout } = value;
11
+ if (typeof id !== "string" || id === "")
12
+ return undefined;
13
+ if (method === "question") {
14
+ const requestId = typeof value.requestId === "string" ? value.requestId : toolCallId;
15
+ if (typeof requestId !== "string" || !requestId || !Array.isArray(value.questions) || !value.questions.length)
16
+ return undefined;
17
+ const questions = [];
18
+ for (const item of value.questions) {
19
+ const q = record(item);
20
+ if (!q ||
21
+ typeof q.id !== "string" ||
22
+ typeof q.question !== "string" ||
23
+ typeof q.header !== "string" ||
24
+ !Array.isArray(q.options))
25
+ return undefined;
26
+ const parsedOptions = [];
27
+ for (const option of q.options) {
28
+ const o = record(option);
29
+ if (!o || typeof o.label !== "string")
30
+ return undefined;
31
+ parsedOptions.push({ label: o.label, description: typeof o.description === "string" ? o.description : "" });
32
+ }
33
+ if (questions.some((question) => question.id === q.id) || q.id === "comment")
34
+ return undefined;
35
+ questions.push({
36
+ id: q.id,
37
+ question: q.question,
38
+ header: q.header,
39
+ options: parsedOptions,
40
+ multiple: q.multiSelect === true || q.multiple === true,
41
+ custom: true,
42
+ });
43
+ }
44
+ const draft = record(value.draft);
45
+ return {
46
+ id,
47
+ method,
48
+ title: "",
49
+ options: [],
50
+ requestId,
51
+ toolCallId: requestId,
52
+ questions,
53
+ waitForAnswer: value.waitForAnswer !== false,
54
+ ...(typeof value.deadlineAtMs === "number" ? { deadlineAtMs: value.deadlineAtMs } : {}),
55
+ ...(draft ? { draft } : {}),
56
+ };
57
+ }
58
+ if (method !== "select" && method !== "confirm" && method !== "input" && method !== "editor")
59
+ return undefined;
60
+ return {
61
+ id,
62
+ method,
63
+ title: typeof title === "string" ? title : "",
64
+ options: Array.isArray(options) ? options.filter((o) => typeof o === "string") : [],
65
+ ...(typeof toolCallId === "string" ? { toolCallId } : {}),
66
+ ...(typeof timeout === "number" && Number.isFinite(timeout) && timeout > 0 ? { timeout } : {}),
67
+ };
68
+ }
69
+ export function parsePermissionTitle(title) {
70
+ const lines = title.split(/\r?\n/);
71
+ const permission = (lines[0] ?? "").slice("Permission required:".length).trim();
72
+ const start = lines.findIndex((line) => line.trim() === "Patterns:");
73
+ const patterns = [];
74
+ if (start !== -1) {
75
+ for (const line of lines.slice(start + 1)) {
76
+ const match = line.match(/^\s*-\s+(.+?)\s*$/);
77
+ if (!match)
78
+ break;
79
+ if (match[1] !== undefined)
80
+ patterns.push(match[1]);
81
+ }
82
+ }
83
+ return { permission, patterns };
84
+ }
85
+ export function toV2Permission(card) {
86
+ return {
87
+ id: card.id,
88
+ sessionID: card.sessionID,
89
+ action: card.permission,
90
+ resources: card.patterns,
91
+ save: card.always,
92
+ metadata: card.metadata,
93
+ ...(card.tool ? { source: { type: "tool", ...card.tool } } : {}),
94
+ };
95
+ }
96
+ /** Session-scoped dialog lifecycle; RPC select replies are labels, never indices. */
97
+ export class ApprovalBridge {
98
+ sessionID;
99
+ send;
100
+ messageID;
101
+ acknowledge;
102
+ pending = new Map();
103
+ questionCalls = new Map();
104
+ followups = new Map();
105
+ emit;
106
+ settled = new Set();
107
+ constructor(sessionID, emit, send, messageID, acknowledge) {
108
+ this.sessionID = sessionID;
109
+ this.send = send;
110
+ this.messageID = messageID;
111
+ this.acknowledge = acknowledge;
112
+ this.emit = (_directory, event) => emit(event);
113
+ }
114
+ context() {
115
+ return { sessionID: this.sessionID, directory: "", messageID: this.messageID() ?? "", send: this.send };
116
+ }
117
+ handle(request) {
118
+ this.ask(this.context(), { ...request });
119
+ }
120
+ noteToolStart(toolCallId, toolName, args) {
121
+ this.observe(this.context(), { type: "tool_execution_start", toolCallId, toolName, args });
122
+ }
123
+ noteToolEnd(toolCallId) {
124
+ this.clear(this.sessionID, false, toolCallId);
125
+ }
126
+ finishTurn() {
127
+ this.clear(this.sessionID, false);
128
+ }
129
+ cancelAll() {
130
+ this.clear(this.sessionID, true);
131
+ }
132
+ permissions(sessionID, directory) {
133
+ return [...this.pending.values()]
134
+ .filter((p) => p.kind === "permission" &&
135
+ (sessionID === undefined || p.context.sessionID === sessionID) &&
136
+ (directory === undefined || p.context.directory === directory))
137
+ .flatMap((p) => (p.kind === "permission" ? [p.card] : []));
138
+ }
139
+ questionRequests(sessionID, directory) {
140
+ return [...this.pending.values()]
141
+ .filter((p) => p.kind === "question" &&
142
+ (sessionID === undefined || p.context.sessionID === sessionID) &&
143
+ (directory === undefined || p.context.directory === directory))
144
+ .flatMap((p) => (p.kind === "question" ? [p.card] : []));
145
+ }
146
+ questions() {
147
+ return this.questionRequests();
148
+ }
149
+ hasPending(sessionID) {
150
+ return ([...this.pending.values()].some((p) => p.context.sessionID === sessionID) ||
151
+ (this.followups.get(sessionID)?.size ?? 0) > 0);
152
+ }
153
+ /** Merge a snapshot only after live subscriptions have been installed. */
154
+ hydrate(questions) {
155
+ if (!Array.isArray(questions))
156
+ return;
157
+ for (const value of questions) {
158
+ const frame = record(value);
159
+ if (!frame)
160
+ continue;
161
+ const request = extensionUiRequestFrom({ ...frame, method: "question" });
162
+ if (request && ![...this.pending.values()].some((p) => p.request.requestId === request.requestId))
163
+ this.handle(request);
164
+ }
165
+ }
166
+ observeQuestion(ev) {
167
+ if (ev.type !== "question_updated" && ev.type !== "question_resolved")
168
+ return;
169
+ const p = [...this.pending.values()].find((p) => p.request.method === "question" &&
170
+ (p.request.id === ev.id || (typeof ev.requestId === "string" && p.request.requestId === ev.requestId)));
171
+ if (ev.type === "question_resolved" && typeof ev.requestId === "string")
172
+ this.settled.add(ev.requestId);
173
+ if (!p || p.kind !== "question")
174
+ return;
175
+ if (ev.type === "question_updated") {
176
+ if (typeof ev.deadlineAtMs === "number")
177
+ p.request.deadlineAtMs = ev.deadlineAtMs;
178
+ const draft = record(ev.draft) ?? (record(ev.answers) ? { answers: ev.answers, comment: ev.comment } : undefined);
179
+ if (draft)
180
+ p.request.draft = draft;
181
+ p.card.metadata = { ...p.card.metadata, deadlineAtMs: p.request.deadlineAtMs, draft: p.request.draft };
182
+ return;
183
+ }
184
+ this.settled.add(p.request.requestId ?? "");
185
+ if (ev.outcome !== "answered" && ev.outcome !== "comment-submitted") {
186
+ this.remove(p, true);
187
+ return;
188
+ }
189
+ const native = record(ev.answers) ?? {};
190
+ const formAnswer = {};
191
+ const answers = (p.request.questions ?? []).map((q) => {
192
+ const answer = record(native[q.id]);
193
+ const selected = Array.isArray(answer?.selected)
194
+ ? answer.selected.filter((v) => typeof v === "string")
195
+ : [];
196
+ const values = typeof answer?.text === "string" ? [...selected, answer.text] : selected;
197
+ if (values.length)
198
+ formAnswer[q.id] = q.multiple ? values : (values[0] ?? "");
199
+ return values;
200
+ });
201
+ if (typeof ev.comment === "string")
202
+ formAnswer.comment = ev.comment;
203
+ this.remove(p, false);
204
+ this.emit(p.context.directory, {
205
+ type: "question.replied",
206
+ properties: {
207
+ sessionID: this.sessionID,
208
+ requestID: p.card.id,
209
+ answers,
210
+ formAnswer,
211
+ ...(typeof ev.comment === "string" ? { comment: ev.comment } : {}),
212
+ },
213
+ });
214
+ }
215
+ observe(context, ev) {
216
+ const callID = typeof ev.toolCallId === "string" ? ev.toolCallId : undefined;
217
+ switch (ev.type) {
218
+ case "tool_execution_start": {
219
+ if (ev.toolName !== "ask_user_question" || callID === undefined)
220
+ return;
221
+ const args = record(ev.args);
222
+ const questions = Array.isArray(args?.questions) ? args.questions : [];
223
+ const parsed = [];
224
+ for (const value of questions) {
225
+ const q = record(value);
226
+ if (!q || typeof q.question !== "string")
227
+ continue;
228
+ parsed.push({
229
+ question: q.question,
230
+ header: typeof q.header === "string" ? q.header : q.question.slice(0, 30),
231
+ options: (Array.isArray(q.options) ? q.options : []).flatMap((value) => {
232
+ const o = record(value);
233
+ return o && typeof o.label === "string"
234
+ ? [{ label: o.label, description: typeof o.description === "string" ? o.description : "" }]
235
+ : [];
236
+ }),
237
+ multiple: false,
238
+ });
239
+ }
240
+ let calls = this.questionCalls.get(context.sessionID);
241
+ if (!calls)
242
+ this.questionCalls.set(context.sessionID, (calls = new Map()));
243
+ calls.set(callID, parsed);
244
+ return;
245
+ }
246
+ case "tool_execution_end":
247
+ if (callID !== undefined)
248
+ this.clear(context.sessionID, false, callID);
249
+ return;
250
+ case "turn_end":
251
+ this.clear(context.sessionID, false);
252
+ return;
253
+ case "agent_end":
254
+ if (ev.willRetry !== true)
255
+ this.clear(context.sessionID, false);
256
+ return;
257
+ case "extension_ui_request":
258
+ this.ask(context, ev);
259
+ return;
260
+ }
261
+ }
262
+ ask(context, ev) {
263
+ const request = extensionUiRequestFrom(ev);
264
+ if (!request)
265
+ return;
266
+ if (request.method === "question") {
267
+ if (this.settled.has(request.requestId ?? ""))
268
+ return;
269
+ const publicID = `que_${createHash("sha256")
270
+ .update(JSON.stringify([context.sessionID, request.requestId]))
271
+ .digest("hex")
272
+ .slice(0, 32)}`;
273
+ const existing = this.pending.get(publicID);
274
+ if (existing) {
275
+ existing.request = request;
276
+ if (existing.kind === "question")
277
+ existing.card.metadata = {
278
+ requestId: request.requestId,
279
+ waitForAnswer: request.waitForAnswer,
280
+ deadlineAtMs: request.deadlineAtMs,
281
+ draft: request.draft,
282
+ };
283
+ return;
284
+ }
285
+ const card = {
286
+ id: publicID,
287
+ sessionID: context.sessionID,
288
+ questions: (request.questions ?? []).map(({ id: _id, ...q }) => q),
289
+ questionIDs: (request.questions ?? []).map((q) => q.id),
290
+ metadata: {
291
+ requestId: request.requestId,
292
+ waitForAnswer: request.waitForAnswer,
293
+ deadlineAtMs: request.deadlineAtMs,
294
+ draft: request.draft,
295
+ },
296
+ tool: { messageID: context.messageID, callID: request.requestId ?? "" },
297
+ };
298
+ this.pending.set(publicID, { kind: "question", card, context, request });
299
+ this.emit(context.directory, { type: "question.asked", properties: card });
300
+ return;
301
+ }
302
+ if (this.pending.has(request.id))
303
+ return;
304
+ const calls = this.questionCalls.get(context.sessionID);
305
+ const matched = request.method === "select"
306
+ ? [...(calls ?? [])].flatMap(([callID, questions]) => questions.filter((q) => q.question === request.title).map((question) => ({ callID, question })))
307
+ : [];
308
+ const match = matched.length === 1 ? matched[0] : undefined;
309
+ const soleCall = calls?.size === 1 ? calls.keys().next().value : undefined;
310
+ let callID = request.toolCallId ??
311
+ match?.callID ??
312
+ (request.method === "input" || request.method === "editor" ? soleCall : undefined);
313
+ const followups = this.followups.get(context.sessionID);
314
+ if (callID === undefined && request.method === "input" && followups?.size === 1) {
315
+ const next = [...followups.entries()][0];
316
+ if (next?.[1].kind === "question")
317
+ callID = next[0];
318
+ }
319
+ if (callID !== undefined)
320
+ request.toolCallId = callID;
321
+ const followup = callID !== undefined ? followups?.get(callID) : undefined;
322
+ if (request.method === "input" && followup) {
323
+ if (callID !== undefined)
324
+ followups?.delete(callID);
325
+ context.send({ type: "extension_ui_response", id: request.id, value: followup.value });
326
+ return;
327
+ }
328
+ let pending;
329
+ if (request.method === "select" && request.title.startsWith("Permission required:")) {
330
+ const parsed = parsePermissionTitle(request.title);
331
+ const card = {
332
+ id: request.id,
333
+ sessionID: context.sessionID,
334
+ ...parsed,
335
+ always: [],
336
+ metadata: { title: request.title },
337
+ ...(callID !== undefined ? { tool: { messageID: context.messageID, callID } } : {}),
338
+ };
339
+ pending = { kind: "permission", card, context, request };
340
+ }
341
+ else {
342
+ const options = request.method === "confirm" ? ["Yes", "No"] : request.method === "select" ? request.options : [];
343
+ const question = match
344
+ ? {
345
+ ...match.question,
346
+ options: match.question.options.filter((o) => options.includes(o.label)),
347
+ custom: options.includes("Other (type an answer)"),
348
+ }
349
+ : {
350
+ question: request.title,
351
+ header: request.title.slice(0, 30),
352
+ options: options.map((label) => ({ label, description: "" })),
353
+ multiple: false,
354
+ custom: request.method === "input" || request.method === "editor" || options.includes("Other (type an answer)"),
355
+ };
356
+ const card = {
357
+ id: request.id,
358
+ sessionID: context.sessionID,
359
+ questions: [question],
360
+ ...(callID !== undefined ? { tool: { messageID: context.messageID, callID } } : {}),
361
+ };
362
+ pending = { kind: "question", card, context, request };
363
+ }
364
+ this.pending.set(request.id, pending);
365
+ if (request.timeout !== undefined) {
366
+ pending.timer = setTimeout(() => this.remove(pending, true), request.timeout);
367
+ pending.timer.unref();
368
+ }
369
+ this.emit(context.directory, pending.kind === "permission"
370
+ ? { type: "permission.asked", properties: pending.card }
371
+ : { type: "question.asked", properties: pending.card });
372
+ }
373
+ replyPermission(id, reply, message, sessionID) {
374
+ const p = this.pending.get(id);
375
+ if (!p || p.kind !== "permission" || (sessionID !== undefined && p.context.sessionID !== sessionID))
376
+ return false;
377
+ if (reply !== "once" && reply !== "always" && reply !== "reject")
378
+ throw new ApprovalInputError("invalid permission reply");
379
+ if (message !== undefined && typeof message !== "string")
380
+ throw new ApprovalInputError("message must be a string");
381
+ const label = reply === "once" ? "Allow once" : reply === "always" ? "Allow always" : message ? "Deny with feedback" : "Deny";
382
+ if (!p.request.options.includes(label))
383
+ throw new ApprovalInputError(`permission option unavailable: ${label}`);
384
+ if (message && reply === "reject" && p.request.toolCallId !== undefined) {
385
+ this.follow(p, message, "permission");
386
+ }
387
+ this.remove(p, false);
388
+ p.context.send({ type: "extension_ui_response", id, value: label });
389
+ this.emit(p.context.directory, {
390
+ type: "permission.replied",
391
+ properties: { sessionID: p.context.sessionID, requestID: id, reply },
392
+ });
393
+ return true;
394
+ }
395
+ replyQuestion(id, answers, sessionID, comment) {
396
+ const p = this.pending.get(id);
397
+ if (!p || p.kind !== "question" || (sessionID !== undefined && p.context.sessionID !== sessionID))
398
+ return false;
399
+ if (p.request.method === "question") {
400
+ if (!Array.isArray(answers) ||
401
+ answers.length > p.card.questions.length ||
402
+ answers.some((a) => !Array.isArray(a) || a.some((v) => typeof v !== "string")) ||
403
+ (comment !== undefined && typeof comment !== "string"))
404
+ throw new ApprovalInputError("invalid structured question answers");
405
+ const native = {};
406
+ (p.request.questions ?? []).forEach((q, index) => {
407
+ const values = answers[index] ?? [];
408
+ if (!q.multiple && values.length > 1)
409
+ throw new ApprovalInputError("single select question accepts one answer");
410
+ if (!values.length)
411
+ return;
412
+ const selected = values.filter((value) => q.options.some((o) => o.label === value));
413
+ const text = values.filter((value) => !q.options.some((o) => o.label === value)).join("\n");
414
+ native[q.id] = { selected, ...(text ? { text } : {}) };
415
+ });
416
+ return this.respondStructured(p, { answers: native, ...(comment !== undefined ? { comment } : {}) });
417
+ }
418
+ if (!Array.isArray(answers) ||
419
+ answers.length !== 1 ||
420
+ !Array.isArray(answers[0]) ||
421
+ answers[0].length !== 1 ||
422
+ typeof answers[0][0] !== "string") {
423
+ throw new ApprovalInputError("answers must contain one question with one string answer");
424
+ }
425
+ const answer = answers[0][0];
426
+ let value = answer;
427
+ const { method, options } = p.request;
428
+ if (method === "select" && !options.includes(answer)) {
429
+ if (!options.includes("Other (type an answer)")) {
430
+ throw new ApprovalInputError("answer is not an available option");
431
+ }
432
+ value = "Other (type an answer)";
433
+ p.request.toolCallId ??= p.request.id;
434
+ this.follow(p, answer, "question");
435
+ }
436
+ if (method === "confirm" && answer !== "Yes" && answer !== "No")
437
+ throw new ApprovalInputError("confirm answer must be Yes or No");
438
+ this.remove(p, false);
439
+ p.context.send(method === "confirm"
440
+ ? { type: "extension_ui_response", id, confirmed: answer === "Yes" }
441
+ : { type: "extension_ui_response", id, value });
442
+ this.emit(p.context.directory, {
443
+ type: "question.replied",
444
+ properties: { sessionID: p.context.sessionID, requestID: id, answers },
445
+ });
446
+ return true;
447
+ }
448
+ rejectQuestion(id, sessionID) {
449
+ const p = this.pending.get(id);
450
+ if (!p || p.kind !== "question" || (sessionID !== undefined && p.context.sessionID !== sessionID))
451
+ return false;
452
+ if (p.request.method === "question")
453
+ return this.respondStructured(p, { cancelled: true });
454
+ p.context.send({ type: "extension_ui_response", id, cancelled: true });
455
+ this.remove(p, true);
456
+ return true;
457
+ }
458
+ async respondStructured(p, fields) {
459
+ if (p.replying)
460
+ return false;
461
+ p.replying = true;
462
+ try {
463
+ const response = { type: "extension_ui_response", uiRequestId: p.request.id, ...fields };
464
+ if (this.acknowledge)
465
+ return await this.acknowledge(response);
466
+ p.context.send({ ...response, id: `reply_${createHash("sha256").update(p.request.id).digest("hex")}` });
467
+ return true;
468
+ }
469
+ finally {
470
+ p.replying = false;
471
+ }
472
+ }
473
+ follow(p, value, kind) {
474
+ const callID = p.request.toolCallId;
475
+ if (callID === undefined)
476
+ return;
477
+ let followups = this.followups.get(p.context.sessionID);
478
+ if (!followups)
479
+ this.followups.set(p.context.sessionID, (followups = new Map()));
480
+ followups.set(callID, { value, kind });
481
+ }
482
+ remove(p, rejected) {
483
+ if (!this.pending.delete(p.card.id))
484
+ return;
485
+ if (p.timer !== undefined)
486
+ clearTimeout(p.timer);
487
+ if (!rejected)
488
+ return;
489
+ this.emit(p.context.directory, p.kind === "permission"
490
+ ? {
491
+ type: "permission.replied",
492
+ properties: { sessionID: p.context.sessionID, requestID: p.card.id, reply: "reject" },
493
+ }
494
+ : {
495
+ type: "question.rejected",
496
+ properties: { sessionID: p.context.sessionID, requestID: p.card.id },
497
+ });
498
+ }
499
+ clear(sessionID, cancel, callID) {
500
+ for (const p of this.pending.values()) {
501
+ if (p.context.sessionID !== sessionID || (callID !== undefined && p.request.toolCallId !== callID))
502
+ continue;
503
+ if (p.request.method === "question" && !cancel)
504
+ continue;
505
+ if (cancel && p.request.requestId)
506
+ this.settled.add(p.request.requestId);
507
+ if (cancel)
508
+ p.context.send({ type: "extension_ui_response", id: p.request.id, cancelled: true });
509
+ this.remove(p, true);
510
+ }
511
+ if (callID !== undefined) {
512
+ this.questionCalls.get(sessionID)?.delete(callID);
513
+ this.followups.get(sessionID)?.delete(callID);
514
+ }
515
+ else {
516
+ this.questionCalls.delete(sessionID);
517
+ this.followups.delete(sessionID);
518
+ }
519
+ }
520
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Model/provider catalog: obtained from a throwaway `pi --mode rpc` process
3
+ * (`get_available_models` + `get_state`), translated into OpenCode v2
4
+ * Provider/Model shapes, cached for the adapter lifetime.
5
+ */
6
+ interface OCv2Model {
7
+ id: string;
8
+ providerID: string;
9
+ api: {
10
+ id: string;
11
+ url: string;
12
+ npm: string;
13
+ };
14
+ name: string;
15
+ capabilities: {
16
+ temperature: boolean;
17
+ reasoning: boolean;
18
+ attachment: boolean;
19
+ toolcall: boolean;
20
+ input: {
21
+ text: boolean;
22
+ audio: boolean;
23
+ image: boolean;
24
+ video: boolean;
25
+ pdf: boolean;
26
+ };
27
+ output: {
28
+ text: boolean;
29
+ audio: boolean;
30
+ image: boolean;
31
+ video: boolean;
32
+ pdf: boolean;
33
+ };
34
+ interleaved: boolean;
35
+ };
36
+ cost: {
37
+ input: number;
38
+ output: number;
39
+ cache: {
40
+ read: number;
41
+ write: number;
42
+ };
43
+ };
44
+ limit: {
45
+ context: number;
46
+ output: number;
47
+ };
48
+ options: Record<string, unknown>;
49
+ variants?: Record<string, Record<string, unknown>>;
50
+ }
51
+ export interface OCv2Provider {
52
+ id: string;
53
+ name: string;
54
+ source: "config";
55
+ env: string[];
56
+ options: Record<string, unknown>;
57
+ models: Record<string, OCv2Model>;
58
+ }
59
+ export interface Catalog {
60
+ providers: OCv2Provider[];
61
+ /** providerID → default modelID */
62
+ defaults: Record<string, string>;
63
+ /**
64
+ * The model pi reported as *active*. Separate from `defaults` because the
65
+ * client pins its picker to /api/model/default and then validates it against
66
+ * the catalog — an arbitrary pick here silently becomes the model the UI
67
+ * believes is running.
68
+ */
69
+ default?: {
70
+ providerID: string;
71
+ modelID: string;
72
+ };
73
+ /** pi's resolved thinking level for `default` (its per-model setting). */
74
+ defaultThinkingLevel?: string;
75
+ }
76
+ /**
77
+ * OpenChamber preselects an agent's `model` + `variant` for new sessions; publish
78
+ * pi's active model and its resolved thinking level so "Default" is pi's level.
79
+ */
80
+ export declare function agentDefaults(cat: Catalog): {
81
+ model?: {
82
+ providerID: string;
83
+ modelID: string;
84
+ };
85
+ variant?: string;
86
+ };
87
+ export declare function loadCatalog(directory: string): Promise<Catalog>;
88
+ export {};