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.
- package/CHANGELOG.md +143 -0
- package/LICENSE +22 -0
- package/README.md +316 -2
- package/bin/opencode-antechamber +18 -0
- package/bin/opencode-pi +3 -0
- package/dist/agent-dir.d.ts +6 -0
- package/dist/agent-dir.js +21 -0
- package/dist/agent-name.d.ts +24 -0
- package/dist/agent-name.js +110 -0
- package/dist/approvals.d.ts +86 -0
- package/dist/approvals.js +520 -0
- package/dist/catalog.d.ts +88 -0
- package/dist/catalog.js +118 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +86 -0
- package/dist/commands/context.d.ts +26 -0
- package/dist/commands/context.js +89 -0
- package/dist/commands/doctor.d.ts +13 -0
- package/dist/commands/doctor.js +92 -0
- package/dist/commands/help.d.ts +1 -0
- package/dist/commands/help.js +15 -0
- package/dist/commands/serve.d.ts +8 -0
- package/dist/commands/serve.js +29 -0
- package/dist/commands/setup.d.ts +14 -0
- package/dist/commands/setup.js +137 -0
- package/dist/config.d.ts +18 -0
- package/dist/config.js +40 -0
- package/dist/http.d.ts +18 -0
- package/dist/http.js +168 -0
- package/dist/lifecycle.d.ts +43 -0
- package/dist/lifecycle.js +138 -0
- package/dist/misc-routes.d.ts +8 -0
- package/dist/misc-routes.js +119 -0
- package/dist/pi-event-translation.d.ts +65 -0
- package/dist/pi-event-translation.js +545 -0
- package/dist/pi-rpc.d.ts +68 -0
- package/dist/pi-rpc.js +285 -0
- package/dist/pidfd.d.ts +2 -0
- package/dist/pidfd.js +28 -0
- package/dist/question-forms.d.ts +30 -0
- package/dist/question-forms.js +46 -0
- package/dist/route-services.d.ts +26 -0
- package/dist/route-services.js +33 -0
- package/dist/routes-v1.d.ts +4 -0
- package/dist/routes-v1.js +346 -0
- package/dist/routes-v2.d.ts +4 -0
- package/dist/routes-v2.js +477 -0
- package/dist/server.d.ts +28 -0
- package/dist/server.js +113 -0
- package/dist/session-commands.d.ts +75 -0
- package/dist/session-commands.js +323 -0
- package/dist/session-diff.d.ts +14 -0
- package/dist/session-diff.js +152 -0
- package/dist/session-history.d.ts +35 -0
- package/dist/session-history.js +416 -0
- package/dist/session-hosts.d.ts +3 -0
- package/dist/session-hosts.js +188 -0
- package/dist/session-store.d.ts +103 -0
- package/dist/session-store.js +339 -0
- package/dist/session-tree.d.ts +59 -0
- package/dist/session-tree.js +324 -0
- package/dist/sessions.d.ts +174 -0
- package/dist/sessions.js +1141 -0
- package/dist/sse.d.ts +19 -0
- package/dist/sse.js +92 -0
- package/dist/subagents.d.ts +64 -0
- package/dist/subagents.js +621 -0
- package/dist/thinking-levels.d.ts +17 -0
- package/dist/thinking-levels.js +24 -0
- package/dist/todo-bridge.d.ts +26 -0
- package/dist/todo-bridge.js +100 -0
- package/dist/tool-map.d.ts +32 -0
- package/dist/tool-map.js +349 -0
- package/dist/types.d.ts +338 -0
- package/dist/types.js +6 -0
- package/dist/v2.d.ts +190 -0
- package/dist/v2.js +553 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +13 -0
- package/extensions/omo-dag/README.md +159 -0
- package/extensions/omo-dag/docs/DESIGN.md +105 -0
- package/extensions/omo-dag/icon.svg +1 -0
- package/extensions/omo-dag/package.json +33 -0
- package/extensions/omo-dag/panel/index.html +179 -0
- package/extensions/omo-dag/panel/main.js +176 -0
- package/extensions/omo-dag/service/main.js +378 -0
- package/package.json +77 -4
package/dist/pi-rpc.js
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi RPC subprocess client.
|
|
3
|
+
*
|
|
4
|
+
* Speaks pi's headless JSONL protocol (`pi --mode rpc`): commands go to stdin,
|
|
5
|
+
* responses and events come back on stdout. Framing is strict LF-only JSONL —
|
|
6
|
+
* pi's docs warn that generic line readers (Node `readline`) also split on
|
|
7
|
+
* U+2028/U+2029, which are legal inside JSON strings, so we split on "\n"
|
|
8
|
+
* ourselves and only strip a single trailing "\r".
|
|
9
|
+
*/
|
|
10
|
+
import { spawn } from "node:child_process";
|
|
11
|
+
import { StringDecoder } from "node:string_decoder";
|
|
12
|
+
import { EventEmitter } from "node:events";
|
|
13
|
+
import { randomUUID } from "node:crypto";
|
|
14
|
+
import { systemClock } from "./lifecycle.js";
|
|
15
|
+
import { agentBinary, envSetting } from "./config.js";
|
|
16
|
+
export const RPC_COMMAND_TIMEOUT_MS = 60_000;
|
|
17
|
+
export const MAX_JSONL_FRAME_BYTES = 64 * 1024 * 1024;
|
|
18
|
+
const PROCESS_KILL_GRACE_MS = 5_000;
|
|
19
|
+
export class PiRpcClient extends EventEmitter {
|
|
20
|
+
options;
|
|
21
|
+
proc;
|
|
22
|
+
buffer = "";
|
|
23
|
+
bufferBytes = 0;
|
|
24
|
+
decoder = new StringDecoder("utf8");
|
|
25
|
+
pending = new Map();
|
|
26
|
+
closed = false;
|
|
27
|
+
stderrTail = "";
|
|
28
|
+
disposing;
|
|
29
|
+
get pid() {
|
|
30
|
+
return this.proc.pid;
|
|
31
|
+
}
|
|
32
|
+
get isBusy() {
|
|
33
|
+
return this.pending.size > 0;
|
|
34
|
+
}
|
|
35
|
+
onEvent(listener) {
|
|
36
|
+
return super.on("event", listener);
|
|
37
|
+
}
|
|
38
|
+
onExit(listener) {
|
|
39
|
+
return super.on("exit", listener);
|
|
40
|
+
}
|
|
41
|
+
onExtensionUI(listener) {
|
|
42
|
+
return super.on("extension_ui_request", listener);
|
|
43
|
+
}
|
|
44
|
+
constructor(options) {
|
|
45
|
+
super();
|
|
46
|
+
this.options = options;
|
|
47
|
+
const extraArgs = envSetting("PI_ARGS")?.trim().split(/\s+/).filter(Boolean) ?? [];
|
|
48
|
+
const args = ["--mode", "rpc", ...(options.args ?? []), ...extraArgs];
|
|
49
|
+
const capabilities = [
|
|
50
|
+
...new Set([
|
|
51
|
+
...(process.env.OMO_RPC_CLIENT_CAPABILITIES ?? "").split(","),
|
|
52
|
+
...(process.env.SENPI_RPC_CLIENT_CAPABILITIES ?? "").split(","),
|
|
53
|
+
"auto_title_sessions",
|
|
54
|
+
"question",
|
|
55
|
+
]
|
|
56
|
+
.map((value) => value.trim())
|
|
57
|
+
.filter(Boolean)),
|
|
58
|
+
].join(",");
|
|
59
|
+
this.proc = spawn(options.piBinary ?? agentBinary(), args, {
|
|
60
|
+
cwd: options.cwd,
|
|
61
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
62
|
+
env: {
|
|
63
|
+
...process.env,
|
|
64
|
+
OMO_RPC_CLIENT_CAPABILITIES: capabilities,
|
|
65
|
+
SENPI_RPC_CLIENT_CAPABILITIES: capabilities,
|
|
66
|
+
},
|
|
67
|
+
detached: true,
|
|
68
|
+
});
|
|
69
|
+
this.proc.stderr.on("data", (chunk) => {
|
|
70
|
+
this.stderrTail = (this.stderrTail + chunk.toString()).slice(-8192);
|
|
71
|
+
});
|
|
72
|
+
this.proc.stdout.on("data", (chunk) => {
|
|
73
|
+
const text = typeof chunk === "string" ? chunk : this.decoder.write(chunk);
|
|
74
|
+
this.buffer += text;
|
|
75
|
+
this.bufferBytes += Buffer.byteLength(text);
|
|
76
|
+
this.drain();
|
|
77
|
+
});
|
|
78
|
+
this.proc.stdout.on("end", () => {
|
|
79
|
+
const text = this.decoder.end();
|
|
80
|
+
this.buffer += text;
|
|
81
|
+
this.bufferBytes += Buffer.byteLength(text);
|
|
82
|
+
this.drain();
|
|
83
|
+
this.failAll(new Error("pi process closed stdout"));
|
|
84
|
+
});
|
|
85
|
+
this.proc.on("error", (err) => {
|
|
86
|
+
this.closed = true;
|
|
87
|
+
this.failAll(err);
|
|
88
|
+
});
|
|
89
|
+
this.proc.on("exit", (code, signal) => {
|
|
90
|
+
this.closed = true;
|
|
91
|
+
console.error(`[pi-rpc] process exited code=${code} signal=${signal}` +
|
|
92
|
+
(envSetting("DEBUG") && this.stderrTail ? `\n[pi-rpc] stderr tail:\n${this.stderrTail}` : ""));
|
|
93
|
+
this.failAll(new Error(`pi exited (code=${code} signal=${signal})`));
|
|
94
|
+
this.emit("exit", code, signal);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
drain() {
|
|
98
|
+
for (;;) {
|
|
99
|
+
const idx = this.buffer.indexOf("\n");
|
|
100
|
+
if (idx === -1) {
|
|
101
|
+
if (this.bufferBytes > MAX_JSONL_FRAME_BYTES)
|
|
102
|
+
this.frameOverflow();
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
let line = this.buffer.slice(0, idx);
|
|
106
|
+
const frameBytes = Buffer.byteLength(line);
|
|
107
|
+
if (frameBytes > MAX_JSONL_FRAME_BYTES) {
|
|
108
|
+
this.frameOverflow();
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
this.buffer = this.buffer.slice(idx + 1);
|
|
112
|
+
this.bufferBytes -= frameBytes + 1;
|
|
113
|
+
if (line.endsWith("\r"))
|
|
114
|
+
line = line.slice(0, -1);
|
|
115
|
+
if (line.length === 0)
|
|
116
|
+
continue;
|
|
117
|
+
this.handleLine(line);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
frameOverflow() {
|
|
121
|
+
const error = new Error(`pi JSONL frame exceeded ${MAX_JSONL_FRAME_BYTES} bytes`);
|
|
122
|
+
this.closed = true;
|
|
123
|
+
this.buffer = "";
|
|
124
|
+
this.bufferBytes = 0;
|
|
125
|
+
this.failAll(error);
|
|
126
|
+
this.proc.stdout?.destroy();
|
|
127
|
+
if (this.proc.pid === undefined)
|
|
128
|
+
this.proc.kill("SIGKILL");
|
|
129
|
+
else
|
|
130
|
+
process.kill(-this.proc.pid, "SIGKILL");
|
|
131
|
+
}
|
|
132
|
+
handleLine(line) {
|
|
133
|
+
let value;
|
|
134
|
+
try {
|
|
135
|
+
value = JSON.parse(line);
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
// Non-JSON noise on stdout; ignore but keep going.
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
142
|
+
return;
|
|
143
|
+
const msg = value;
|
|
144
|
+
if (msg.type === "response") {
|
|
145
|
+
const pending = typeof msg.id === "string" ? this.pending.get(msg.id) : undefined;
|
|
146
|
+
if (typeof msg.id !== "string" ||
|
|
147
|
+
typeof msg.success !== "boolean" ||
|
|
148
|
+
typeof msg.command !== "string" ||
|
|
149
|
+
(msg.error !== undefined && typeof msg.error !== "string")) {
|
|
150
|
+
const error = new Error("pi returned a malformed RPC response");
|
|
151
|
+
console.error(`[pi-rpc] ${error.message}`);
|
|
152
|
+
pending?.reject(error);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const response = msg.success
|
|
156
|
+
? { type: "response", id: msg.id, command: msg.command, success: true, data: msg.data }
|
|
157
|
+
: {
|
|
158
|
+
type: "response",
|
|
159
|
+
id: msg.id,
|
|
160
|
+
command: msg.command,
|
|
161
|
+
success: false,
|
|
162
|
+
...(msg.error !== undefined ? { error: msg.error } : {}),
|
|
163
|
+
};
|
|
164
|
+
pending?.resolve(response);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (msg.type === "extension_ui_request") {
|
|
168
|
+
this.handleExtensionUI(msg);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
this.emit("event", msg);
|
|
172
|
+
}
|
|
173
|
+
handleExtensionUI(msg) {
|
|
174
|
+
const method = msg.method;
|
|
175
|
+
const id = msg.id;
|
|
176
|
+
if (!id || !method)
|
|
177
|
+
return;
|
|
178
|
+
const blocking = new Set(["select", "confirm", "input", "editor"]);
|
|
179
|
+
if (!blocking.has(method) && method !== "notify" && method !== "question")
|
|
180
|
+
return;
|
|
181
|
+
this.emit("extension_ui_request", msg);
|
|
182
|
+
}
|
|
183
|
+
respondExtensionUI(response) {
|
|
184
|
+
this.write(response);
|
|
185
|
+
}
|
|
186
|
+
write(obj) {
|
|
187
|
+
if (this.closed)
|
|
188
|
+
return;
|
|
189
|
+
this.proc.stdin.write(JSON.stringify(obj) + "\n");
|
|
190
|
+
}
|
|
191
|
+
failAll(err) {
|
|
192
|
+
for (const p of this.pending.values())
|
|
193
|
+
p.reject(err);
|
|
194
|
+
this.pending.clear();
|
|
195
|
+
}
|
|
196
|
+
/** Send a command and wait for its correlated response. */
|
|
197
|
+
command(cmd, timeoutMs = RPC_COMMAND_TIMEOUT_MS, signal, onDispatch) {
|
|
198
|
+
if (this.closed)
|
|
199
|
+
return Promise.reject(new Error("pi process is not running"));
|
|
200
|
+
if (signal?.aborted)
|
|
201
|
+
return Promise.reject(signal.reason);
|
|
202
|
+
const id = randomUUID();
|
|
203
|
+
return new Promise((resolve, reject) => {
|
|
204
|
+
const cleanup = () => {
|
|
205
|
+
clearTimeout(timer);
|
|
206
|
+
signal?.removeEventListener("abort", onAbort);
|
|
207
|
+
this.pending.delete(id);
|
|
208
|
+
};
|
|
209
|
+
const onAbort = () => {
|
|
210
|
+
cleanup();
|
|
211
|
+
reject(signal?.reason);
|
|
212
|
+
};
|
|
213
|
+
const timer = setTimeout(() => {
|
|
214
|
+
cleanup();
|
|
215
|
+
reject(new Error(`pi command "${cmd.type}" timed out after ${timeoutMs}ms`));
|
|
216
|
+
}, timeoutMs);
|
|
217
|
+
this.pending.set(id, {
|
|
218
|
+
resolve: (r) => {
|
|
219
|
+
cleanup();
|
|
220
|
+
// The envelope is checked at the transport boundary; callers own data schemas.
|
|
221
|
+
resolve(r.success ? { ...r, data: r.data } : r);
|
|
222
|
+
},
|
|
223
|
+
reject: (e) => {
|
|
224
|
+
cleanup();
|
|
225
|
+
reject(e);
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
229
|
+
this.write({ ...cmd, id });
|
|
230
|
+
onDispatch?.();
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
/** Fire-and-forget command (no response correlation). */
|
|
234
|
+
notify(cmd) {
|
|
235
|
+
this.write(cmd);
|
|
236
|
+
}
|
|
237
|
+
async dispose() {
|
|
238
|
+
if (this.disposing !== undefined)
|
|
239
|
+
return this.disposing;
|
|
240
|
+
this.closed = true;
|
|
241
|
+
this.failAll(new Error("pi process disposed"));
|
|
242
|
+
const pid = this.proc.pid;
|
|
243
|
+
if (pid === undefined)
|
|
244
|
+
return;
|
|
245
|
+
const clock = this.options.clock ?? systemClock;
|
|
246
|
+
const signalGroup = (signal) => {
|
|
247
|
+
try {
|
|
248
|
+
process.kill(-pid, signal);
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
252
|
+
if (error instanceof Error && "code" in error && error.code === "ESRCH")
|
|
253
|
+
return false;
|
|
254
|
+
throw error;
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
this.disposing = new Promise((resolve) => {
|
|
258
|
+
let exited = this.proc.exitCode !== null || this.proc.signalCode !== null;
|
|
259
|
+
const finish = () => {
|
|
260
|
+
clock.clearTimeout(timer);
|
|
261
|
+
this.proc.off("exit", onExit);
|
|
262
|
+
resolve();
|
|
263
|
+
};
|
|
264
|
+
const onExit = () => {
|
|
265
|
+
exited = true;
|
|
266
|
+
// A grandchild may outlive pi. Keep the escalation armed for its group.
|
|
267
|
+
if (!signalGroup(0))
|
|
268
|
+
finish();
|
|
269
|
+
};
|
|
270
|
+
this.proc.once("exit", onExit);
|
|
271
|
+
const timer = clock.setTimeout(() => {
|
|
272
|
+
signalGroup("SIGKILL");
|
|
273
|
+
if (exited)
|
|
274
|
+
finish();
|
|
275
|
+
else
|
|
276
|
+
this.proc.once("exit", finish);
|
|
277
|
+
}, PROCESS_KILL_GRACE_MS);
|
|
278
|
+
const alive = signalGroup("SIGTERM");
|
|
279
|
+
this.proc.stdin?.end();
|
|
280
|
+
if (!alive && exited)
|
|
281
|
+
finish();
|
|
282
|
+
});
|
|
283
|
+
return this.disposing;
|
|
284
|
+
}
|
|
285
|
+
}
|
package/dist/pidfd.d.ts
ADDED
package/dist/pidfd.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { delimiter, resolve } from "node:path";
|
|
3
|
+
const pidfdProbe = String.raw `
|
|
4
|
+
import os, selectors, signal
|
|
5
|
+
fd = os.pidfd_open(os.getpid())
|
|
6
|
+
try:
|
|
7
|
+
signal.pidfd_send_signal(fd, 0)
|
|
8
|
+
with selectors.DefaultSelector() as selector:
|
|
9
|
+
selector.register(fd, selectors.EVENT_READ)
|
|
10
|
+
finally:
|
|
11
|
+
os.close(fd)
|
|
12
|
+
`;
|
|
13
|
+
/** Shared by private-host teardown and doctor; no probing at module import. */
|
|
14
|
+
export function findPidfdPython(env = process.env, probe = (binary, args) => {
|
|
15
|
+
const result = spawnSync(binary, args, { stdio: "ignore", timeout: 1_000, killSignal: "SIGKILL", env });
|
|
16
|
+
return !result.error && result.status === 0;
|
|
17
|
+
}) {
|
|
18
|
+
const candidates = new Set([
|
|
19
|
+
...(env.PATH ?? "").split(delimiter).map((directory) => resolve(directory, "python3")),
|
|
20
|
+
"/bin/python3",
|
|
21
|
+
"/usr/bin/python3",
|
|
22
|
+
]);
|
|
23
|
+
for (const binary of candidates) {
|
|
24
|
+
if (probe(binary, ["-I", "-c", pidfdProbe]))
|
|
25
|
+
return binary;
|
|
26
|
+
}
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type PendingQuestion } from "./approvals.js";
|
|
2
|
+
export declare function questionFormID(id: string): string;
|
|
3
|
+
export declare function toQuestionForm(card: PendingQuestion): {
|
|
4
|
+
id: string;
|
|
5
|
+
sessionID: string;
|
|
6
|
+
title: string;
|
|
7
|
+
metadata: Record<string, unknown>;
|
|
8
|
+
fields: ({
|
|
9
|
+
key: string;
|
|
10
|
+
type: string;
|
|
11
|
+
title: string;
|
|
12
|
+
description: string;
|
|
13
|
+
required: boolean;
|
|
14
|
+
options?: {
|
|
15
|
+
label: string;
|
|
16
|
+
description: string;
|
|
17
|
+
value: string;
|
|
18
|
+
}[];
|
|
19
|
+
custom?: boolean;
|
|
20
|
+
} | {
|
|
21
|
+
key: string;
|
|
22
|
+
type: string;
|
|
23
|
+
title: string;
|
|
24
|
+
required: boolean;
|
|
25
|
+
})[];
|
|
26
|
+
};
|
|
27
|
+
export declare function formQuestionAnswer(card: PendingQuestion, value: unknown): {
|
|
28
|
+
answers: string[][];
|
|
29
|
+
comment?: string;
|
|
30
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { ApprovalInputError } from "./approvals.js";
|
|
2
|
+
export function questionFormID(id) {
|
|
3
|
+
return `frm_${id}`;
|
|
4
|
+
}
|
|
5
|
+
export function toQuestionForm(card) {
|
|
6
|
+
return {
|
|
7
|
+
id: questionFormID(card.id),
|
|
8
|
+
sessionID: card.sessionID,
|
|
9
|
+
title: card.questions.map((q) => q.header).join(" / "),
|
|
10
|
+
metadata: card.metadata ?? {},
|
|
11
|
+
fields: [
|
|
12
|
+
...card.questions.map((q, index) => ({
|
|
13
|
+
key: card.questionIDs?.[index] ?? `q${index + 1}`,
|
|
14
|
+
type: q.multiple ? "multiselect" : "string",
|
|
15
|
+
title: q.header,
|
|
16
|
+
description: q.question,
|
|
17
|
+
required: false,
|
|
18
|
+
...(q.options.length
|
|
19
|
+
? { options: q.options.map((o) => ({ value: o.label, ...o })), custom: q.custom !== false }
|
|
20
|
+
: {}),
|
|
21
|
+
})),
|
|
22
|
+
...(card.questionIDs ? [{ key: "comment", type: "string", title: "Comment", required: false }] : []),
|
|
23
|
+
],
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export function formQuestionAnswer(card, value) {
|
|
27
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
28
|
+
throw new ApprovalInputError("answer must be an object");
|
|
29
|
+
const answer = Object.fromEntries(Object.entries(value));
|
|
30
|
+
const keys = card.questions.map((_, i) => card.questionIDs?.[i] ?? `q${i + 1}`);
|
|
31
|
+
if (Object.keys(answer).some((key) => !keys.includes(key) && !(card.questionIDs && key === "comment")))
|
|
32
|
+
throw new ApprovalInputError("unknown form field");
|
|
33
|
+
const answers = keys.map((key, i) => {
|
|
34
|
+
const v = answer[key];
|
|
35
|
+
if (v === undefined)
|
|
36
|
+
return [];
|
|
37
|
+
if (typeof v === "string")
|
|
38
|
+
return v === "" ? [] : [v];
|
|
39
|
+
if (card.questions[i]?.multiple && Array.isArray(v) && v.every((item) => typeof item === "string"))
|
|
40
|
+
return v;
|
|
41
|
+
throw new ApprovalInputError("form field answer must match its type");
|
|
42
|
+
});
|
|
43
|
+
if (answer.comment !== undefined && typeof answer.comment !== "string")
|
|
44
|
+
throw new ApprovalInputError("comment must be a string");
|
|
45
|
+
return { answers, ...(typeof answer.comment === "string" ? { comment: answer.comment } : {}) };
|
|
46
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** Shared services and prompt/model adaptation for both API versions. */
|
|
2
|
+
import type { IncomingMessage } from "node:http";
|
|
3
|
+
import type { SessionManager, ManagedSession } from "./sessions.js";
|
|
4
|
+
import type { Catalog } from "./catalog.js";
|
|
5
|
+
import type { ModelSelection } from "./thinking-levels.js";
|
|
6
|
+
import type { OCEvent } from "./types.js";
|
|
7
|
+
import type { TodoUpdated } from "./todo-bridge.js";
|
|
8
|
+
export interface RouteServices {
|
|
9
|
+
readonly sessions: SessionManager;
|
|
10
|
+
readonly defaultDirectory: string;
|
|
11
|
+
readonly url: string;
|
|
12
|
+
readonly body: (req: IncomingMessage) => Promise<unknown>;
|
|
13
|
+
readonly catalog: () => Promise<Catalog>;
|
|
14
|
+
readonly broadcast: (directory: string, event: OCEvent | TodoUpdated) => void;
|
|
15
|
+
}
|
|
16
|
+
export declare const OPENCODE_COMPAT_VERSION: string | null;
|
|
17
|
+
export declare function numberParam(value: string | null): number | undefined;
|
|
18
|
+
/** v2 prompt bodies carry `model:{providerID, id|modelID}`; adapt to OCModelRef. */
|
|
19
|
+
export declare function modelRefFromBody(model: {
|
|
20
|
+
providerID?: string;
|
|
21
|
+
id?: string;
|
|
22
|
+
modelID?: string;
|
|
23
|
+
variant?: string;
|
|
24
|
+
} | undefined, variant?: string): ModelSelection | undefined;
|
|
25
|
+
/** Both async APIs acknowledge admission; failures must reach SSE and history. */
|
|
26
|
+
export declare function promptAsync(services: RouteServices, session: ManagedSession, text: string, model?: ModelSelection, noReply?: boolean, messageID?: string, delivery?: "steer" | "queue"): void;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** Shared services and prompt/model adaptation for both API versions. */
|
|
2
|
+
import { announceSessionError } from "./session-commands.js";
|
|
3
|
+
import { envSetting } from "./config.js";
|
|
4
|
+
// This connector is not opencode — it only implements an OpenCode API subset.
|
|
5
|
+
// OpenChamber reads /global/health's `version` as the installed opencode version
|
|
6
|
+
// and offers an update when it trails the latest release. Advertising the SDK
|
|
7
|
+
// contract version (1.18.31) made every adapter profile look permanently
|
|
8
|
+
// outdated, and the resulting "update" then failed with "Unknown installation
|
|
9
|
+
// method". Report no version so upgrade-status returns available:null and the UI
|
|
10
|
+
// shows no prompt. Set ANTECHAMBER_OPENCODE_COMPAT_VERSION to advertise one on purpose.
|
|
11
|
+
export const OPENCODE_COMPAT_VERSION = envSetting("OPENCODE_COMPAT_VERSION")?.trim() || null;
|
|
12
|
+
export function numberParam(value) {
|
|
13
|
+
if (value === null || value === "")
|
|
14
|
+
return undefined;
|
|
15
|
+
const n = Number(value);
|
|
16
|
+
return Number.isFinite(n) && n >= 0 ? n : undefined;
|
|
17
|
+
}
|
|
18
|
+
/** v2 prompt bodies carry `model:{providerID, id|modelID}`; adapt to OCModelRef. */
|
|
19
|
+
export function modelRefFromBody(model, variant) {
|
|
20
|
+
if (!model || typeof model.providerID !== "string")
|
|
21
|
+
return undefined;
|
|
22
|
+
const modelID = typeof model.id === "string" ? model.id : typeof model.modelID === "string" ? model.modelID : undefined;
|
|
23
|
+
if (modelID === undefined)
|
|
24
|
+
return undefined;
|
|
25
|
+
const level = variant ?? model.variant;
|
|
26
|
+
return { providerID: model.providerID, modelID, ...(typeof level === "string" ? { variant: level } : {}) };
|
|
27
|
+
}
|
|
28
|
+
/** Both async APIs acknowledge admission; failures must reach SSE and history. */
|
|
29
|
+
export function promptAsync(services, session, text, model, noReply = false, messageID, delivery = "steer") {
|
|
30
|
+
void services.sessions.prompt(session.info.id, text, model, noReply, messageID, delivery).catch((error) => {
|
|
31
|
+
announceSessionError({ sessions: services.sessions, session, emit: services.broadcast }, error instanceof Error ? error.name : "UnknownError", error instanceof Error ? error.message : String(error));
|
|
32
|
+
});
|
|
33
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** OpenCode v1 routes, using the services composed by AdapterServer. */
|
|
2
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
3
|
+
import { type RouteServices } from "./route-services.js";
|
|
4
|
+
export declare function handleV1(services: RouteServices, req: IncomingMessage, res: ServerResponse, url: URL, path: string, method: string, directory: string): Promise<void>;
|