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