antechamber 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -0
- package/README.md +17 -14
- package/bin/opencode-antechamber +10 -1
- package/bin/opencode-pi +11 -1
- package/dist/approvals.d.ts +4 -1
- package/dist/approvals.js +51 -33
- package/dist/catalog.d.ts +1 -1
- package/dist/catalog.js +1 -1
- package/dist/commands/context.d.ts +0 -1
- package/dist/commands/context.js +1 -1
- package/dist/commands/doctor.d.ts +3 -2
- package/dist/commands/serve.d.ts +2 -1
- package/dist/commands/setup.d.ts +2 -3
- package/dist/commands/setup.js +16 -4
- package/dist/http.d.ts +8 -1
- package/dist/http.js +42 -19
- package/dist/pi-event-translation.d.ts +9 -5
- package/dist/pi-event-translation.js +233 -79
- package/dist/pi-rpc.d.ts +20 -2
- package/dist/pi-rpc.js +96 -29
- package/dist/prompt-parts.d.ts +40 -0
- package/dist/prompt-parts.js +157 -0
- package/dist/route-services.d.ts +45 -2
- package/dist/route-services.js +70 -3
- package/dist/routes-v1.js +110 -65
- package/dist/routes-v2.js +376 -73
- package/dist/server.d.ts +9 -1
- package/dist/server.js +109 -9
- package/dist/session-commands.d.ts +12 -15
- package/dist/session-commands.js +52 -11
- package/dist/session-diff.d.ts +1 -1
- package/dist/session-diff.js +10 -2
- package/dist/session-history.js +108 -42
- package/dist/session-hosts.js +2 -2
- package/dist/session-store.d.ts +8 -4
- package/dist/session-store.js +183 -33
- package/dist/session-tree.d.ts +9 -1
- package/dist/session-tree.js +51 -26
- package/dist/sessions.d.ts +32 -5
- package/dist/sessions.js +330 -119
- package/dist/sse.d.ts +16 -6
- package/dist/sse.js +77 -21
- package/dist/subagents.d.ts +5 -0
- package/dist/subagents.js +84 -71
- package/dist/tool-map.d.ts +3 -2
- package/dist/types.d.ts +152 -1
- package/dist/v2.d.ts +18 -4
- package/dist/v2.js +128 -44
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/extensions/omo-dag/README.md +2 -0
- package/extensions/omo-dag/docs/DESIGN.md +10 -1
- package/extensions/omo-dag/panel/index.html +1 -1
- package/extensions/omo-dag/panel/main.js +60 -60
- package/extensions/omo-dag/service/main.js +72 -8
- package/package.json +1 -1
package/dist/routes-v2.js
CHANGED
|
@@ -1,34 +1,97 @@
|
|
|
1
1
|
/** OpenCode v2 (/api/*) routes and their response adapters. */
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
2
3
|
import { randomUUID } from "node:crypto";
|
|
3
4
|
import { tmpdir } from "node:os";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
import { errorBody, json, RequestBodyError } from "./http.js";
|
|
7
|
+
import { nativePromptInput, parsePromptParts, v2PromptAttachments, v2PromptParts } from "./prompt-parts.js";
|
|
8
|
+
import { debugLog, miscRoute } from "./misc-routes.js";
|
|
6
9
|
import { OPENCODE_API_VERSION } from "./version.js";
|
|
7
10
|
import { toV2Permission } from "./approvals.js";
|
|
8
11
|
import { formQuestionAnswer, questionFormID, toQuestionForm } from "./question-forms.js";
|
|
9
|
-
import { getAgentName
|
|
10
|
-
import {
|
|
11
|
-
import { announceSessionError, compactSession, getCommands, parseShellCommand, parseSlashCommand, runShellCommand, runSlashCommand, unsupportedShare, } from "./session-commands.js";
|
|
12
|
+
import { getAgentName } from "./agent-name.js";
|
|
13
|
+
import { announceSessionError, compactSession, getCommands, unsupportedShare } from "./session-commands.js";
|
|
12
14
|
import { defaultV2Model, flattenModels, toV2Message, toV2Session } from "./v2.js";
|
|
13
|
-
import { modelRefFromBody, numberParam, OPENCODE_COMPAT_VERSION, promptAsync, } from "./route-services.js";
|
|
15
|
+
import { agentDescriptors, modelRefFromBody, numberParam, OPENCODE_COMPAT_VERSION, promptAsync, runSessionShell, runSessionSlash, sessionDirectory, sessionMetadata, } from "./route-services.js";
|
|
14
16
|
async function v2Agents(services) {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
:
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
options: {},
|
|
26
|
-
...defaults,
|
|
17
|
+
return (await agentDescriptors(services)).map((agent) => ({
|
|
18
|
+
id: agent.name,
|
|
19
|
+
...agent,
|
|
20
|
+
request: { settings: {}, headers: {}, body: {} },
|
|
21
|
+
permissions: agent.permission.map((rule) => ({
|
|
22
|
+
action: rule.permission,
|
|
23
|
+
resource: rule.pattern,
|
|
24
|
+
effect: rule.action,
|
|
25
|
+
})),
|
|
26
|
+
system: agent.prompt,
|
|
27
27
|
}));
|
|
28
28
|
}
|
|
29
29
|
function v2Session(services, info) {
|
|
30
30
|
const managed = services.sessions.get(info.id);
|
|
31
|
-
return toV2Session(info, { model: managed?.model, agent: getAgentName() });
|
|
31
|
+
return toV2Session(info, { model: managed?.model, agent: info.agent ?? getAgentName() });
|
|
32
|
+
}
|
|
33
|
+
const execFileAsync = promisify(execFile);
|
|
34
|
+
const credentialsByDirectory = new Map();
|
|
35
|
+
function credentials(directory) {
|
|
36
|
+
let stored = credentialsByDirectory.get(directory);
|
|
37
|
+
if (stored === undefined) {
|
|
38
|
+
stored = new Map();
|
|
39
|
+
credentialsByDirectory.set(directory, stored);
|
|
40
|
+
}
|
|
41
|
+
return stored;
|
|
42
|
+
}
|
|
43
|
+
function encodeSessionCursor(session) {
|
|
44
|
+
return Buffer.from(JSON.stringify({ updated: session.time.updated, id: session.id })).toString("base64url");
|
|
45
|
+
}
|
|
46
|
+
function decodeSessionCursor(value) {
|
|
47
|
+
if (value === null || value === "")
|
|
48
|
+
return undefined;
|
|
49
|
+
try {
|
|
50
|
+
const parsed = JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
|
|
51
|
+
if (typeof parsed === "object" &&
|
|
52
|
+
parsed !== null &&
|
|
53
|
+
"updated" in parsed &&
|
|
54
|
+
"id" in parsed &&
|
|
55
|
+
typeof parsed.updated === "number" &&
|
|
56
|
+
Number.isFinite(parsed.updated) &&
|
|
57
|
+
typeof parsed.id === "string") {
|
|
58
|
+
return { updated: parsed.updated, id: parsed.id };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// A malformed continuation cursor starts a fresh listing, as OpenCode's
|
|
63
|
+
// clients may retain a cursor across a restarted local server.
|
|
64
|
+
}
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
function compareSessions(left, right, order) {
|
|
68
|
+
const comparison = left.time.updated - right.time.updated || left.id.localeCompare(right.id);
|
|
69
|
+
return order === "asc" ? comparison : -comparison;
|
|
70
|
+
}
|
|
71
|
+
function followsCursor(session, cursor, order) {
|
|
72
|
+
const comparison = session.time.updated - cursor.updated || session.id.localeCompare(cursor.id);
|
|
73
|
+
return order === "asc" ? comparison > 0 : comparison < 0;
|
|
74
|
+
}
|
|
75
|
+
async function currentGitBranch(directory) {
|
|
76
|
+
try {
|
|
77
|
+
const { stdout } = await execFileAsync("git", ["symbolic-ref", "--short", "HEAD"], {
|
|
78
|
+
cwd: directory,
|
|
79
|
+
timeout: 3000,
|
|
80
|
+
});
|
|
81
|
+
return stdout.trim() || undefined;
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
async function initializeGit(directory) {
|
|
88
|
+
try {
|
|
89
|
+
await execFileAsync("git", ["init", "-q"], { cwd: directory, timeout: 3000 });
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
32
95
|
}
|
|
33
96
|
/** v2 `/api/project` list: the adapter has no project registry, so expose the
|
|
34
97
|
* directories it knows about (the default plus every restorable session). */
|
|
@@ -40,14 +103,13 @@ function v2Projects(services) {
|
|
|
40
103
|
return [...dirs].map((dir) => ({
|
|
41
104
|
id: dir,
|
|
42
105
|
canonical: dir,
|
|
43
|
-
time: { created: now, updated: now },
|
|
106
|
+
time: { created: now, updated: now, active: now },
|
|
44
107
|
sandboxes: [],
|
|
108
|
+
vcs: "git",
|
|
45
109
|
}));
|
|
46
110
|
}
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
const withLocation = (data) => ({ location, data });
|
|
50
|
-
// --- bootstrap ---
|
|
111
|
+
async function handleV2Bootstrap(services, res, path, method, directory) {
|
|
112
|
+
const withLocation = (data) => ({ location: { directory }, data });
|
|
51
113
|
if (path === "/api/info" && method === "GET") {
|
|
52
114
|
json(res, 200, {
|
|
53
115
|
version: OPENCODE_COMPAT_VERSION ?? OPENCODE_API_VERSION,
|
|
@@ -55,55 +117,146 @@ export async function handleV2(services, req, res, url, path, method, directory)
|
|
|
55
117
|
urls: [services.url],
|
|
56
118
|
paths: { tmp: tmpdir() },
|
|
57
119
|
});
|
|
58
|
-
return;
|
|
120
|
+
return true;
|
|
59
121
|
}
|
|
60
122
|
if (path === "/api/location" && method === "GET") {
|
|
61
123
|
json(res, 200, { directory, project: { id: directory, directory, canonical: directory } });
|
|
62
|
-
return;
|
|
124
|
+
return true;
|
|
63
125
|
}
|
|
64
126
|
if (path === "/api/config" && method === "GET") {
|
|
65
|
-
|
|
66
|
-
|
|
127
|
+
// OpenCode 2.x returns a bare ConfigEntry[]; OpenChamber folds each document's `info` into one config.
|
|
128
|
+
json(res, 200, [{ type: "document", path: "antechamber", info: { default_agent: getAgentName() } }]);
|
|
129
|
+
return true;
|
|
67
130
|
}
|
|
68
131
|
if (path === "/api/provider" && method === "GET") {
|
|
69
132
|
const cat = await services.catalog();
|
|
70
133
|
json(res, 200, withLocation(cat.providers.map((p) => ({ ...p, activation: "enabled" }))));
|
|
71
|
-
return;
|
|
134
|
+
return true;
|
|
72
135
|
}
|
|
73
136
|
if (path === "/api/model" && method === "GET") {
|
|
74
137
|
const cat = await services.catalog();
|
|
75
138
|
json(res, 200, withLocation(flattenModels(cat)));
|
|
76
|
-
return;
|
|
139
|
+
return true;
|
|
77
140
|
}
|
|
78
141
|
if (path === "/api/model/default" && method === "GET") {
|
|
79
142
|
const cat = await services.catalog();
|
|
80
143
|
json(res, 200, withLocation(defaultV2Model(cat) ?? null));
|
|
81
|
-
return;
|
|
144
|
+
return true;
|
|
82
145
|
}
|
|
83
146
|
if (path === "/api/agent" && method === "GET") {
|
|
84
147
|
json(res, 200, withLocation(await v2Agents(services)));
|
|
85
|
-
return;
|
|
148
|
+
return true;
|
|
86
149
|
}
|
|
87
150
|
const agentMatch = path.match(/^\/api\/agent\/([^/]+)$/);
|
|
88
151
|
if (agentMatch && method === "GET") {
|
|
89
152
|
const agentId = decodeURIComponent(agentMatch[1]);
|
|
90
153
|
const agent = (await v2Agents(services)).find((a) => a.id === agentId);
|
|
91
|
-
if (!agent)
|
|
154
|
+
if (!agent)
|
|
92
155
|
json(res, 404, errorBody("NotFoundError", `agent not found: ${agentId}`));
|
|
156
|
+
else
|
|
157
|
+
json(res, 200, withLocation(agent));
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
if (path === "/api/command" && method === "GET") {
|
|
161
|
+
json(res, 200, withLocation((await getCommands(directory)).map((command) => ({
|
|
162
|
+
...command,
|
|
163
|
+
source: "command",
|
|
164
|
+
hints: [],
|
|
165
|
+
subtask: false,
|
|
166
|
+
}))));
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
if (path === "/api/skill" && method === "GET") {
|
|
170
|
+
json(res, 200, withLocation([]));
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
export async function handleV2(services, req, res, url, path, method, directory) {
|
|
176
|
+
const location = { directory };
|
|
177
|
+
const withLocation = (data) => ({ location, data });
|
|
178
|
+
// --- bootstrap ---
|
|
179
|
+
if (await handleV2Bootstrap(services, res, path, method, directory))
|
|
180
|
+
return;
|
|
181
|
+
if (path === "/api/debug/location" && method === "DELETE") {
|
|
182
|
+
json(res, 400, errorBody("UnsupportedError", "Location eviction is not available with OmO"));
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
// --- project / vcs / permission / form (2.0.4 bootstrap) ---
|
|
186
|
+
if (path === "/api/credential" && method === "GET") {
|
|
187
|
+
json(res, 200, withLocation([...credentials(directory).values()]));
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (path === "/api/credential" && method === "POST") {
|
|
191
|
+
const body = (await services.body(req));
|
|
192
|
+
if (typeof body.id !== "string" ||
|
|
193
|
+
body.id === "" ||
|
|
194
|
+
typeof body.integrationID !== "string" ||
|
|
195
|
+
body.integrationID === "" ||
|
|
196
|
+
typeof body.label !== "string") {
|
|
197
|
+
json(res, 400, errorBody("BadRequestError", "credential id, integrationID, and label are required"));
|
|
93
198
|
return;
|
|
94
199
|
}
|
|
95
|
-
|
|
200
|
+
const stored = credentials(directory);
|
|
201
|
+
if (body.activate === true) {
|
|
202
|
+
for (const credential of stored.values()) {
|
|
203
|
+
if (credential.integrationID === body.integrationID)
|
|
204
|
+
credential.active = false;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
const credential = {
|
|
208
|
+
id: body.id,
|
|
209
|
+
integrationID: body.integrationID,
|
|
210
|
+
label: body.label,
|
|
211
|
+
value: body.value,
|
|
212
|
+
active: body.activate === true,
|
|
213
|
+
};
|
|
214
|
+
stored.set(credential.id, credential);
|
|
215
|
+
json(res, 200, withLocation(credential));
|
|
96
216
|
return;
|
|
97
217
|
}
|
|
98
|
-
|
|
99
|
-
|
|
218
|
+
const credentialMatch = path.match(/^\/api\/credential\/([^/]+)$/);
|
|
219
|
+
if (credentialMatch && method === "DELETE") {
|
|
220
|
+
credentials(directory).delete(decodeURIComponent(credentialMatch[1]));
|
|
221
|
+
res.writeHead(204).end();
|
|
100
222
|
return;
|
|
101
223
|
}
|
|
102
|
-
if (path === "/api/
|
|
224
|
+
if (path === "/api/integration" && method === "GET") {
|
|
103
225
|
json(res, 200, withLocation([]));
|
|
104
226
|
return;
|
|
105
227
|
}
|
|
106
|
-
//
|
|
228
|
+
// Read-only inventories OpenChamber loads at bootstrap: OmO has no background shells,
|
|
229
|
+
// web-search providers or MCP servers, so each is an honest empty list (mutations stay 400).
|
|
230
|
+
if ((path === "/api/shell" || path === "/api/websearch/provider" || path === "/api/mcp") && method === "GET") {
|
|
231
|
+
json(res, 200, withLocation([]));
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (path === "/api/experimental/session/stats" && method === "GET") {
|
|
235
|
+
json(res, 400, errorBody("UnsupportedError", "Session statistics are not available with OmO"));
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
if (path === "/api/mcp" || path.startsWith("/api/mcp/") || path.startsWith("/api/experimental/mcp/")) {
|
|
239
|
+
json(res, 400, errorBody("UnsupportedError", "MCP is not available with OmO"));
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
if (path === "/api/fs/find" && method === "GET") {
|
|
243
|
+
const result = await miscRoute("GET", "/find/file", url, directory);
|
|
244
|
+
if (result === undefined) {
|
|
245
|
+
json(res, 500, errorBody("UnknownError", "file search route is unavailable"));
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
if (result.status !== 200) {
|
|
249
|
+
json(res, result.status, result.body);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
const files = Array.isArray(result.body)
|
|
253
|
+
? result.body
|
|
254
|
+
.filter((entry) => typeof entry === "string")
|
|
255
|
+
.map((path) => ({ path, type: "file" }))
|
|
256
|
+
: [];
|
|
257
|
+
json(res, 200, withLocation(files));
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
107
260
|
if (path === "/api/project" && method === "GET") {
|
|
108
261
|
json(res, 200, v2Projects(services));
|
|
109
262
|
return;
|
|
@@ -121,7 +274,10 @@ export async function handleV2(services, req, res, url, path, method, directory)
|
|
|
121
274
|
return;
|
|
122
275
|
}
|
|
123
276
|
if (path === "/api/permission/request" && method === "GET") {
|
|
124
|
-
json(res, 200, withLocation(services.sessions
|
|
277
|
+
json(res, 200, withLocation(services.sessions
|
|
278
|
+
.permissions()
|
|
279
|
+
.filter((permission) => services.sessions.get(permission.sessionID)?.info.directory === directory)
|
|
280
|
+
.map((permission) => ({ ...toV2Permission(permission), ...permission }))));
|
|
125
281
|
return;
|
|
126
282
|
}
|
|
127
283
|
if (path === "/api/question/request" && method === "GET") {
|
|
@@ -140,8 +296,17 @@ export async function handleV2(services, req, res, url, path, method, directory)
|
|
|
140
296
|
json(res, 200, withLocation(services.sessions.questions().map(toQuestionForm)));
|
|
141
297
|
return;
|
|
142
298
|
}
|
|
299
|
+
if (path === "/api/vcs/init" && method === "POST") {
|
|
300
|
+
if (!(await initializeGit(directory))) {
|
|
301
|
+
json(res, 400, errorBody("BadRequestError", "Git initialization failed"));
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
res.writeHead(204).end();
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
143
307
|
if (path === "/api/vcs" && method === "GET") {
|
|
144
|
-
|
|
308
|
+
const current = await currentGitBranch(directory);
|
|
309
|
+
json(res, 200, withLocation({ provider: "git", branch: current === undefined ? {} : { current } }));
|
|
145
310
|
return;
|
|
146
311
|
}
|
|
147
312
|
if (path === "/api/vcs/base" && method === "GET") {
|
|
@@ -164,22 +329,48 @@ export async function handleV2(services, req, res, url, path, method, directory)
|
|
|
164
329
|
if (path === "/api/session" && method === "GET") {
|
|
165
330
|
const dir = url.searchParams.get("directory") ?? url.searchParams.get("location");
|
|
166
331
|
const search = url.searchParams.get("search");
|
|
332
|
+
const parentID = url.searchParams.get("parentID");
|
|
333
|
+
const order = url.searchParams.get("order") === "asc" ? "asc" : "desc";
|
|
167
334
|
const limit = numberParam(url.searchParams.get("limit"));
|
|
168
335
|
let infos = dir !== null && dir !== "" ? services.sessions.list(dir) : services.sessions.listAll();
|
|
169
336
|
if (search)
|
|
170
337
|
infos = infos.filter((s) => s.title.toLowerCase().includes(search.toLowerCase()));
|
|
171
|
-
|
|
172
|
-
|
|
338
|
+
if (parentID !== null)
|
|
339
|
+
infos = infos.filter((session) => session.parentID === parentID);
|
|
340
|
+
let sessions = infos.map((info) => v2Session(services, info));
|
|
341
|
+
sessions.sort((left, right) => compareSessions(left, right, order));
|
|
342
|
+
const cursor = decodeSessionCursor(url.searchParams.get("cursor"));
|
|
343
|
+
if (cursor !== undefined)
|
|
344
|
+
sessions = sessions.filter((session) => followsCursor(session, cursor, order));
|
|
173
345
|
const page = limit !== undefined ? sessions.slice(0, limit) : sessions;
|
|
174
346
|
// The 2.0.4 UI reads `cursor.previous` unconditionally — a list body
|
|
175
347
|
// without a cursor throws "Cannot read properties of undefined".
|
|
176
|
-
|
|
348
|
+
const next = limit !== undefined && page.length < sessions.length && page.at(-1) !== undefined
|
|
349
|
+
? encodeSessionCursor(page.at(-1))
|
|
350
|
+
: null;
|
|
351
|
+
json(res, 200, { data: page, cursor: { previous: null, next } });
|
|
177
352
|
return;
|
|
178
353
|
}
|
|
179
354
|
if (path === "/api/session" && method === "POST") {
|
|
180
355
|
const body = (await services.body(req));
|
|
181
|
-
const
|
|
182
|
-
|
|
356
|
+
for (const field of ["id", "agent", "title", "parentID"]) {
|
|
357
|
+
if (body[field] !== undefined && typeof body[field] !== "string")
|
|
358
|
+
throw new RequestBodyError(400, `${field} must be a string`);
|
|
359
|
+
}
|
|
360
|
+
if (body.location !== undefined &&
|
|
361
|
+
(typeof body.location !== "object" || body.location === null || Array.isArray(body.location)))
|
|
362
|
+
throw new RequestBodyError(400, "location must be an object");
|
|
363
|
+
const dir = sessionDirectory(body.location?.directory ?? directory);
|
|
364
|
+
const metadata = sessionMetadata(body.metadata);
|
|
365
|
+
const model = modelRefFromBody(body.model);
|
|
366
|
+
if (body.model !== undefined && model === undefined)
|
|
367
|
+
throw new RequestBodyError(400, "model requires providerID and id or modelID");
|
|
368
|
+
const info = await services.sessions.create(dir, body.title, body.parentID, {
|
|
369
|
+
...(body.id !== undefined ? { id: body.id } : {}),
|
|
370
|
+
...(body.agent !== undefined ? { agent: body.agent } : {}),
|
|
371
|
+
...(model !== undefined ? { model } : {}),
|
|
372
|
+
...(metadata !== undefined ? { metadata } : {}),
|
|
373
|
+
});
|
|
183
374
|
json(res, 200, { data: v2Session(services, info) });
|
|
184
375
|
return;
|
|
185
376
|
}
|
|
@@ -285,7 +476,8 @@ export async function handleV2(services, req, res, url, path, method, directory)
|
|
|
285
476
|
const requestId = decodeURIComponent(permissionMatch[1]);
|
|
286
477
|
if (permissionMatch[2] === "/reply" && method === "POST") {
|
|
287
478
|
const body = (await services.body(req));
|
|
288
|
-
|
|
479
|
+
const decision = body.decision ?? body.reply;
|
|
480
|
+
if (!decision || !services.sessions.replyPermission(sessionId, requestId, decision, body.message)) {
|
|
289
481
|
json(res, 404, errorBody("PermissionNotFoundError", `permission not found: ${requestId}`));
|
|
290
482
|
return;
|
|
291
483
|
}
|
|
@@ -311,7 +503,11 @@ export async function handleV2(services, req, res, url, path, method, directory)
|
|
|
311
503
|
}
|
|
312
504
|
case "PATCH ": {
|
|
313
505
|
const body = (await services.body(req));
|
|
314
|
-
services.sessions.update(sessionId, {
|
|
506
|
+
await services.sessions.update(sessionId, {
|
|
507
|
+
title: body.title,
|
|
508
|
+
archived: body.time?.archived,
|
|
509
|
+
metadata: sessionMetadata(body.metadata),
|
|
510
|
+
});
|
|
315
511
|
res.writeHead(204).end();
|
|
316
512
|
return;
|
|
317
513
|
}
|
|
@@ -326,11 +522,21 @@ export async function handleV2(services, req, res, url, path, method, directory)
|
|
|
326
522
|
let messages = (services.sessions.messages(sessionId) ?? []).map((m) => toV2Message(m));
|
|
327
523
|
if (order !== "asc")
|
|
328
524
|
messages = messages.reverse(); // v2 default is newest-first
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
525
|
+
const cursor = url.searchParams.get("cursor");
|
|
526
|
+
if (cursor !== null) {
|
|
527
|
+
const boundary = messages.findIndex((message) => message.id === cursor);
|
|
528
|
+
if (boundary >= 0)
|
|
529
|
+
messages = messages.slice(boundary + 1);
|
|
530
|
+
}
|
|
531
|
+
const page = limit === undefined ? messages : messages.slice(0, limit);
|
|
532
|
+
const last = page.at(-1);
|
|
533
|
+
json(res, 200, {
|
|
534
|
+
data: page,
|
|
535
|
+
cursor: { previous: null, next: last && page.length < messages.length ? last.id : null },
|
|
536
|
+
});
|
|
332
537
|
return;
|
|
333
538
|
}
|
|
539
|
+
case "POST /compact":
|
|
334
540
|
case "POST /summarize": {
|
|
335
541
|
await services.body(req);
|
|
336
542
|
const result = await compactSession(services.sessions, sessionId);
|
|
@@ -343,38 +549,34 @@ export async function handleV2(services, req, res, url, path, method, directory)
|
|
|
343
549
|
return;
|
|
344
550
|
}
|
|
345
551
|
case "POST /shell": {
|
|
346
|
-
const
|
|
347
|
-
if (
|
|
552
|
+
const result = await runSessionShell(services, managed, await services.body(req));
|
|
553
|
+
if (result.kind === "input") {
|
|
348
554
|
json(res, 400, errorBody("BadRequestError", "command is required"));
|
|
349
555
|
return;
|
|
350
556
|
}
|
|
351
|
-
if (
|
|
557
|
+
if (result.kind === "busy") {
|
|
352
558
|
json(res, 409, errorBody("SessionBusyError", "session is busy"));
|
|
353
559
|
return;
|
|
354
560
|
}
|
|
355
|
-
const result = await runShellCommand({
|
|
356
|
-
sessions: services.sessions,
|
|
357
|
-
session: managed,
|
|
358
|
-
command,
|
|
359
|
-
emit: services.broadcast,
|
|
360
|
-
});
|
|
361
561
|
if (result.kind === "error") {
|
|
362
562
|
json(res, 400, errorBody("BadRequestError", result.message));
|
|
363
563
|
return;
|
|
364
564
|
}
|
|
365
|
-
|
|
565
|
+
res.writeHead(204).end();
|
|
366
566
|
return;
|
|
367
567
|
}
|
|
368
568
|
case "POST /command": {
|
|
369
|
-
const
|
|
370
|
-
|
|
569
|
+
const body = (await services.body(req));
|
|
570
|
+
const result = await runSessionSlash(services, sessionId, {
|
|
571
|
+
command: body.name,
|
|
572
|
+
arguments: body.text ?? "",
|
|
573
|
+
messageID: body.id,
|
|
574
|
+
});
|
|
575
|
+
if (result.kind === "input") {
|
|
371
576
|
json(res, 400, errorBody("BadRequestError", "command is required"));
|
|
372
577
|
return;
|
|
373
578
|
}
|
|
374
|
-
|
|
375
|
-
json(res, 200, {
|
|
376
|
-
data: message === undefined ? { info: null, parts: [] } : { info: toV2Message(message), parts: message.parts },
|
|
377
|
-
});
|
|
579
|
+
res.writeHead(204).end();
|
|
378
580
|
return;
|
|
379
581
|
}
|
|
380
582
|
case "POST /share":
|
|
@@ -384,7 +586,28 @@ export async function handleV2(services, req, res, url, path, method, directory)
|
|
|
384
586
|
return;
|
|
385
587
|
case "POST /prompt": {
|
|
386
588
|
const body = (await services.body(req));
|
|
387
|
-
const
|
|
589
|
+
const explicit = parsePromptParts(body.parts);
|
|
590
|
+
const firstText = explicit.find((part) => part.type === "text");
|
|
591
|
+
const text = typeof body.text === "string" ? body.text : firstText?.type === "text" ? firstText.text : "";
|
|
592
|
+
const parts = explicit.length > 0
|
|
593
|
+
? [...explicit, ...v2PromptParts("", body.files, body.agents)]
|
|
594
|
+
: v2PromptParts(text, body.files, body.agents);
|
|
595
|
+
const metadata = sessionMetadata(body.metadata);
|
|
596
|
+
if (body.skills !== undefined) {
|
|
597
|
+
if (!Array.isArray(body.skills))
|
|
598
|
+
throw new RequestBodyError(400, "skills must be an array");
|
|
599
|
+
const skill = body.skills[0];
|
|
600
|
+
if (skill !== undefined) {
|
|
601
|
+
if (typeof skill !== "object" || skill === null || !("id" in skill) || typeof skill.id !== "string")
|
|
602
|
+
throw new RequestBodyError(400, "skills must contain {id} references");
|
|
603
|
+
// GET /api/skill lists none, so any reference is unknown. OpenChamber retries without
|
|
604
|
+
// the attachment on exactly this tagged OpenCode error.
|
|
605
|
+
json(res, 400, { _tag: "InvalidRequestError", message: `Skill not found: ${skill.id}` });
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
// Reject an attachment the native RPC cannot carry before acknowledging the prompt.
|
|
610
|
+
nativePromptInput(text, parts);
|
|
388
611
|
const messageID = typeof body.id === "string" && body.id !== "" ? body.id : `msg_${randomUUID().replaceAll("-", "")}`;
|
|
389
612
|
const model = modelRefFromBody(body.model ?? (body.variant !== undefined ? managed.model : undefined), body.variant);
|
|
390
613
|
if (body.delivery !== undefined && body.delivery !== "steer" && body.delivery !== "queue") {
|
|
@@ -392,7 +615,7 @@ export async function handleV2(services, req, res, url, path, method, directory)
|
|
|
392
615
|
return;
|
|
393
616
|
}
|
|
394
617
|
const delivery = body.delivery ?? "steer";
|
|
395
|
-
promptAsync(services, managed, text, model, false, messageID, delivery);
|
|
618
|
+
promptAsync(services, managed, text, model, false, messageID, delivery, parts.length > 0 ? parts : undefined, metadata);
|
|
396
619
|
json(res, 200, {
|
|
397
620
|
data: {
|
|
398
621
|
id: messageID,
|
|
@@ -400,19 +623,93 @@ export async function handleV2(services, req, res, url, path, method, directory)
|
|
|
400
623
|
time: { created: Date.now() },
|
|
401
624
|
type: "user",
|
|
402
625
|
text,
|
|
403
|
-
payload: { text },
|
|
626
|
+
payload: { text, ...v2PromptAttachments(parts) },
|
|
404
627
|
delivery,
|
|
628
|
+
...(metadata !== undefined ? { metadata } : {}),
|
|
629
|
+
},
|
|
630
|
+
});
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
case "POST /synthetic":
|
|
634
|
+
case "POST /context": {
|
|
635
|
+
const body = (await services.body(req));
|
|
636
|
+
if (body.delivery !== undefined && body.delivery !== "steer" && body.delivery !== "queue") {
|
|
637
|
+
json(res, 400, errorBody("BadRequestError", 'delivery must be "steer" or "queue"'));
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
const metadata = sessionMetadata(body.metadata);
|
|
641
|
+
const messageID = typeof body.id === "string" && body.id !== "" ? body.id : `msg_${randomUUID().replaceAll("-", "")}`;
|
|
642
|
+
const text = typeof body.text === "string" ? body.text : "";
|
|
643
|
+
promptAsync(services, managed, text, undefined, true, messageID, body.delivery ?? "steer", undefined, metadata);
|
|
644
|
+
// OpenChamber 2.3.2 treats anything but 200 + the admitted record as a failed send (captured OpenCode shape).
|
|
645
|
+
json(res, 200, {
|
|
646
|
+
data: {
|
|
647
|
+
id: messageID,
|
|
648
|
+
sessionID: sessionId,
|
|
649
|
+
time: { created: Date.now() },
|
|
650
|
+
type: "synthetic",
|
|
651
|
+
payload: { text },
|
|
405
652
|
},
|
|
406
653
|
});
|
|
407
654
|
return;
|
|
408
655
|
}
|
|
409
656
|
case "POST /interrupt": {
|
|
410
|
-
await services.sessions.abort(sessionId);
|
|
411
|
-
|
|
657
|
+
const interrupted = await services.sessions.abort(sessionId);
|
|
658
|
+
// Bare body, as captured from OpenCode 2.x: OpenChamber reads `result.interrupted` directly.
|
|
659
|
+
json(res, 200, { interrupted });
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
case "POST /fork":
|
|
663
|
+
case "POST /revert":
|
|
664
|
+
case "POST /revert/stage":
|
|
665
|
+
case "POST /revert/commit":
|
|
666
|
+
case "DELETE /revert": {
|
|
667
|
+
const body = method === "DELETE" ? {} : await services.body(req);
|
|
668
|
+
const target = typeof body === "object" && body !== null
|
|
669
|
+
? "before" in body
|
|
670
|
+
? body.before
|
|
671
|
+
: "messageID" in body
|
|
672
|
+
? body.messageID
|
|
673
|
+
: undefined
|
|
674
|
+
: undefined;
|
|
675
|
+
if ((sub === "/revert" || sub === "/revert/stage") &&
|
|
676
|
+
method === "POST" &&
|
|
677
|
+
(typeof target !== "string" || target === "")) {
|
|
678
|
+
json(res, 400, errorBody("InvalidRequestError", "messageID is required"));
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
try {
|
|
682
|
+
if (sub === "/fork") {
|
|
683
|
+
if (target !== undefined && (typeof target !== "string" || target === ""))
|
|
684
|
+
throw new Error("before must be a nonempty message id");
|
|
685
|
+
const info = await services.sessions.fork(sessionId, typeof target === "string" ? target : undefined);
|
|
686
|
+
json(res, 200, { data: v2Session(services, info) });
|
|
687
|
+
}
|
|
688
|
+
else if (method === "DELETE") {
|
|
689
|
+
await services.sessions.unrevert(sessionId);
|
|
690
|
+
res.writeHead(204).end();
|
|
691
|
+
}
|
|
692
|
+
else if (sub === "/revert/commit") {
|
|
693
|
+
await services.sessions.commitRevert(sessionId);
|
|
694
|
+
res.writeHead(204).end();
|
|
695
|
+
}
|
|
696
|
+
else {
|
|
697
|
+
const info = await services.sessions.revert(sessionId, String(target));
|
|
698
|
+
json(res, 200, { data: info.revert });
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
catch (error) {
|
|
702
|
+
json(res, 400, errorBody("SessionTreeError", error instanceof Error ? error.message : String(error)));
|
|
703
|
+
}
|
|
412
704
|
return;
|
|
413
705
|
}
|
|
414
706
|
case "POST /agent": {
|
|
415
|
-
await services.body(req);
|
|
707
|
+
const body = (await services.body(req));
|
|
708
|
+
if (typeof body.agent !== "string") {
|
|
709
|
+
json(res, 400, errorBody("InvalidRequestError", "agent must be a string"));
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
await services.sessions.update(sessionId, { agent: body.agent });
|
|
416
713
|
res.writeHead(204).end();
|
|
417
714
|
return;
|
|
418
715
|
}
|
|
@@ -462,7 +759,13 @@ export async function handleV2(services, req, res, url, path, method, directory)
|
|
|
462
759
|
return;
|
|
463
760
|
}
|
|
464
761
|
case "GET /diff":
|
|
465
|
-
json(res, 200, {
|
|
762
|
+
json(res, 200, {
|
|
763
|
+
data: await services.sessions.diff(sessionId, {
|
|
764
|
+
from: url.searchParams.get("from") ?? undefined,
|
|
765
|
+
to: url.searchParams.get("to") ?? undefined,
|
|
766
|
+
context: numberParam(url.searchParams.get("context")),
|
|
767
|
+
}),
|
|
768
|
+
});
|
|
466
769
|
return;
|
|
467
770
|
case "GET /context": {
|
|
468
771
|
json(res, 501, errorBody("NotImplemented", `${method} ${path} is not implemented by this connector`));
|
package/dist/server.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** HTTP/SSE composition root for the OpenCode API backed by pi. */
|
|
2
2
|
export { isLoopbackHost, serverPassword } from "./http.js";
|
|
3
|
-
|
|
3
|
+
interface AdapterServerOptions {
|
|
4
4
|
port: number;
|
|
5
5
|
host: string;
|
|
6
6
|
/** Fallback directory when requests carry no ?directory= param. */
|
|
@@ -12,6 +12,9 @@ export declare class AdapterServer {
|
|
|
12
12
|
private sessions;
|
|
13
13
|
private sse;
|
|
14
14
|
private sseClients;
|
|
15
|
+
private webSocketClients;
|
|
16
|
+
private webSocketEvents;
|
|
17
|
+
private nextWebSocketEventID;
|
|
15
18
|
private catalogPromise;
|
|
16
19
|
private actualPort;
|
|
17
20
|
private readonly http;
|
|
@@ -23,6 +26,11 @@ export declare class AdapterServer {
|
|
|
23
26
|
private broadcast;
|
|
24
27
|
private writeSSE;
|
|
25
28
|
private handleSSE;
|
|
29
|
+
private handleUpgrade;
|
|
30
|
+
private rejectUpgrade;
|
|
31
|
+
private broadcastWebSocket;
|
|
32
|
+
private writeWebSocketEvent;
|
|
33
|
+
private writeWebSocket;
|
|
26
34
|
private route;
|
|
27
35
|
private handle;
|
|
28
36
|
}
|