alchemylab-opencode 1.0.3 → 1.0.5
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/build.mjs +163 -0
- package/dist/chunk-ANDTK76Q.js +319 -0
- package/dist/chunk-ANDTK76Q.js.map +7 -0
- package/dist/chunk-B3FN2B2N.js +2280 -0
- package/dist/chunk-B3FN2B2N.js.map +7 -0
- package/dist/chunk-BI5OUHTQ.js +31 -0
- package/dist/chunk-BI5OUHTQ.js.map +7 -0
- package/dist/chunk-CR6IREZA.js +127 -0
- package/dist/chunk-CR6IREZA.js.map +7 -0
- package/dist/chunk-CYXMV3J3.js +6535 -0
- package/dist/chunk-CYXMV3J3.js.map +7 -0
- package/dist/chunk-DSPC5QIG.js +23731 -0
- package/dist/chunk-DSPC5QIG.js.map +7 -0
- package/dist/chunk-HORZKFCR.js +88 -0
- package/dist/chunk-HORZKFCR.js.map +7 -0
- package/dist/chunk-MCWTARFP.js +728 -0
- package/dist/chunk-MCWTARFP.js.map +7 -0
- package/dist/chunk-UWZSRZHA.js +5025 -0
- package/dist/chunk-UWZSRZHA.js.map +7 -0
- package/dist/mcp-VAU4RMWH.js +22 -0
- package/dist/mcp-VAU4RMWH.js.map +7 -0
- package/dist/pty.node-RHOAUGH6.js +40 -0
- package/dist/pty.node-RHOAUGH6.js.map +7 -0
- package/dist/registry-PYMN6L3G.js +23 -0
- package/dist/registry-PYMN6L3G.js.map +7 -0
- package/dist/run-YFVCQJQM.js +4791 -0
- package/dist/run-YFVCQJQM.js.map +7 -0
- package/dist/run-service-LHEVFBIU.js +22 -0
- package/dist/run-service-LHEVFBIU.js.map +7 -0
- package/dist/run.js +22 -0
- package/dist/run.js.map +7 -0
- package/dist/schema-32EBFLEO.js +24 -0
- package/dist/schema-32EBFLEO.js.map +7 -0
- package/dist/serve.js +41 -0
- package/dist/serve.js.map +7 -0
- package/dist/server-K3UGWASP.js +26 -0
- package/dist/server-K3UGWASP.js.map +7 -0
- package/dist/session-HJFWTM7C.js +23 -0
- package/dist/session-HJFWTM7C.js.map +7 -0
- package/dist/share-next-55VCIMUH.js +24 -0
- package/dist/share-next-55VCIMUH.js.map +7 -0
- package/dist/worktree-6GGDLADY.js +63 -0
- package/dist/worktree-6GGDLADY.js.map +7 -0
- package/package.json +10 -3
|
@@ -0,0 +1,5025 @@
|
|
|
1
|
+
// Built by esbuild from opencode-server source
|
|
2
|
+
if (typeof globalThis.Bun === 'undefined') {
|
|
3
|
+
globalThis.Bun = {
|
|
4
|
+
version: '0.0.0-node-compat',
|
|
5
|
+
stdin: { async text() { const chunks = []; for await (const c of process.stdin) chunks.push(c); return Buffer.concat(chunks).toString(); } },
|
|
6
|
+
which(cmd) { try { return require('child_process').execFileSync('which', [cmd], { encoding: 'utf8' }).trim(); } catch { return null; } },
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
if (typeof globalThis.__name === 'undefined') { globalThis.__name = (t, n) => { Object.defineProperty(t, 'name', { value: n, configurable: true }); return t; }; }
|
|
10
|
+
if (typeof globalThis.__export === 'undefined') { globalThis.__export = (t, a) => { for (const k in a) Object.defineProperty(t, k, { get: a[k], enumerable: true }); }; }
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
InstanceBootstrap,
|
|
14
|
+
Vcs,
|
|
15
|
+
WorkspaceInfo,
|
|
16
|
+
Worktree
|
|
17
|
+
} from "./chunk-MCWTARFP.js";
|
|
18
|
+
import {
|
|
19
|
+
Agent,
|
|
20
|
+
Command,
|
|
21
|
+
File,
|
|
22
|
+
Format,
|
|
23
|
+
LSP,
|
|
24
|
+
MessageV2,
|
|
25
|
+
ModelID,
|
|
26
|
+
Permission,
|
|
27
|
+
PermissionID,
|
|
28
|
+
Plugin,
|
|
29
|
+
Provider,
|
|
30
|
+
ProviderID,
|
|
31
|
+
Question,
|
|
32
|
+
QuestionID,
|
|
33
|
+
Ripgrep,
|
|
34
|
+
Session,
|
|
35
|
+
SessionCompaction,
|
|
36
|
+
SessionPrompt,
|
|
37
|
+
SessionRevert,
|
|
38
|
+
SessionStatus,
|
|
39
|
+
SessionSummary,
|
|
40
|
+
Shell,
|
|
41
|
+
Skill,
|
|
42
|
+
Snapshot,
|
|
43
|
+
SyncEvent,
|
|
44
|
+
Todo,
|
|
45
|
+
ToolRegistry,
|
|
46
|
+
WorkspaceID,
|
|
47
|
+
fn
|
|
48
|
+
} from "./chunk-DSPC5QIG.js";
|
|
49
|
+
import {
|
|
50
|
+
Account,
|
|
51
|
+
AccountID,
|
|
52
|
+
Auth,
|
|
53
|
+
Bus,
|
|
54
|
+
Config,
|
|
55
|
+
Installation,
|
|
56
|
+
MCP,
|
|
57
|
+
ModelsDev,
|
|
58
|
+
OrgID,
|
|
59
|
+
TuiEvent
|
|
60
|
+
} from "./chunk-CYXMV3J3.js";
|
|
61
|
+
import {
|
|
62
|
+
BusEvent,
|
|
63
|
+
Database,
|
|
64
|
+
Filesystem,
|
|
65
|
+
Flag,
|
|
66
|
+
Global,
|
|
67
|
+
GlobalBus,
|
|
68
|
+
Instance,
|
|
69
|
+
InstanceState,
|
|
70
|
+
Log,
|
|
71
|
+
MessageTable,
|
|
72
|
+
NamedError,
|
|
73
|
+
NotFoundError,
|
|
74
|
+
PartTable,
|
|
75
|
+
Project,
|
|
76
|
+
ProjectID,
|
|
77
|
+
ProjectTable,
|
|
78
|
+
SessionTable,
|
|
79
|
+
db_exports,
|
|
80
|
+
lazy,
|
|
81
|
+
makeRuntime
|
|
82
|
+
} from "./chunk-B3FN2B2N.js";
|
|
83
|
+
import {
|
|
84
|
+
Identifier,
|
|
85
|
+
MessageID,
|
|
86
|
+
PartID,
|
|
87
|
+
SessionID
|
|
88
|
+
} from "./chunk-CR6IREZA.js";
|
|
89
|
+
import {
|
|
90
|
+
withStatics
|
|
91
|
+
} from "./chunk-BI5OUHTQ.js";
|
|
92
|
+
import {
|
|
93
|
+
__callDispose,
|
|
94
|
+
__using
|
|
95
|
+
} from "./chunk-HORZKFCR.js";
|
|
96
|
+
|
|
97
|
+
// packages/opencode/src/server/server.ts
|
|
98
|
+
import { describeRoute as describeRoute16, generateSpecs, validator as validator15, resolver as resolver17, openAPIRouteHandler } from "hono-openapi";
|
|
99
|
+
import { Hono as Hono17 } from "hono";
|
|
100
|
+
import { compress } from "hono/compress";
|
|
101
|
+
import { cors } from "hono/cors";
|
|
102
|
+
import { basicAuth } from "hono/basic-auth";
|
|
103
|
+
import z21 from "zod";
|
|
104
|
+
import { createAdaptorServer } from "@hono/node-server";
|
|
105
|
+
import { createNodeWebSocket as createNodeWebSocket2 } from "@hono/node-ws";
|
|
106
|
+
|
|
107
|
+
// packages/opencode/src/control-plane/adaptors/index.ts
|
|
108
|
+
var ADAPTORS = {
|
|
109
|
+
worktree: lazy(async () => (await import("./worktree-6GGDLADY.js")).WorktreeAdaptor)
|
|
110
|
+
};
|
|
111
|
+
function getAdaptor(type) {
|
|
112
|
+
return ADAPTORS[type]();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// packages/opencode/src/control-plane/workspace.ts
|
|
116
|
+
import z from "zod";
|
|
117
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
118
|
+
|
|
119
|
+
// packages/opencode/src/control-plane/workspace.sql.ts
|
|
120
|
+
import { sqliteTable, text } from "drizzle-orm/sqlite-core";
|
|
121
|
+
var WorkspaceTable = sqliteTable("workspace", {
|
|
122
|
+
id: text().$type().primaryKey(),
|
|
123
|
+
type: text().notNull(),
|
|
124
|
+
branch: text(),
|
|
125
|
+
name: text(),
|
|
126
|
+
directory: text(),
|
|
127
|
+
extra: text({ mode: "json" }),
|
|
128
|
+
project_id: text().$type().notNull().references(() => ProjectTable.id, { onDelete: "cascade" })
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// packages/opencode/src/control-plane/sse.ts
|
|
132
|
+
async function parseSSE(body, signal, onEvent) {
|
|
133
|
+
const reader = body.getReader();
|
|
134
|
+
const decoder = new TextDecoder();
|
|
135
|
+
let buf = "";
|
|
136
|
+
let last = "";
|
|
137
|
+
let retry = 1e3;
|
|
138
|
+
const abort = () => {
|
|
139
|
+
void reader.cancel().catch(() => void 0);
|
|
140
|
+
};
|
|
141
|
+
signal.addEventListener("abort", abort);
|
|
142
|
+
try {
|
|
143
|
+
while (!signal.aborted) {
|
|
144
|
+
const chunk = await reader.read().catch(() => ({ done: true, value: void 0 }));
|
|
145
|
+
if (chunk.done) break;
|
|
146
|
+
buf += decoder.decode(chunk.value, { stream: true });
|
|
147
|
+
buf = buf.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
148
|
+
const chunks = buf.split("\n\n");
|
|
149
|
+
buf = chunks.pop() ?? "";
|
|
150
|
+
chunks.forEach((chunk2) => {
|
|
151
|
+
const data = [];
|
|
152
|
+
chunk2.split("\n").forEach((line) => {
|
|
153
|
+
if (line.startsWith("data:")) {
|
|
154
|
+
data.push(line.replace(/^data:\s*/, ""));
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (line.startsWith("id:")) {
|
|
158
|
+
last = line.replace(/^id:\s*/, "");
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (line.startsWith("retry:")) {
|
|
162
|
+
const parsed = Number.parseInt(line.replace(/^retry:\s*/, ""), 10);
|
|
163
|
+
if (!Number.isNaN(parsed)) retry = parsed;
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
if (!data.length) return;
|
|
167
|
+
const raw = data.join("\n");
|
|
168
|
+
try {
|
|
169
|
+
onEvent(JSON.parse(raw));
|
|
170
|
+
} catch {
|
|
171
|
+
onEvent({
|
|
172
|
+
type: "sse.message",
|
|
173
|
+
properties: {
|
|
174
|
+
data: raw,
|
|
175
|
+
id: last || void 0,
|
|
176
|
+
retry
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
} finally {
|
|
183
|
+
signal.removeEventListener("abort", abort);
|
|
184
|
+
reader.releaseLock();
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// packages/opencode/src/control-plane/workspace.ts
|
|
189
|
+
var Workspace;
|
|
190
|
+
((Workspace2) => {
|
|
191
|
+
Workspace2.Event = {
|
|
192
|
+
Ready: BusEvent.define(
|
|
193
|
+
"workspace.ready",
|
|
194
|
+
z.object({
|
|
195
|
+
name: z.string()
|
|
196
|
+
})
|
|
197
|
+
),
|
|
198
|
+
Failed: BusEvent.define(
|
|
199
|
+
"workspace.failed",
|
|
200
|
+
z.object({
|
|
201
|
+
message: z.string()
|
|
202
|
+
})
|
|
203
|
+
)
|
|
204
|
+
};
|
|
205
|
+
Workspace2.Info = WorkspaceInfo.meta({
|
|
206
|
+
ref: "Workspace"
|
|
207
|
+
});
|
|
208
|
+
function fromRow(row) {
|
|
209
|
+
return {
|
|
210
|
+
id: row.id,
|
|
211
|
+
type: row.type,
|
|
212
|
+
branch: row.branch,
|
|
213
|
+
name: row.name,
|
|
214
|
+
directory: row.directory,
|
|
215
|
+
extra: row.extra,
|
|
216
|
+
projectID: row.project_id
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
const CreateInput = z.object({
|
|
220
|
+
id: WorkspaceID.zod.optional(),
|
|
221
|
+
type: Workspace2.Info.shape.type,
|
|
222
|
+
branch: Workspace2.Info.shape.branch,
|
|
223
|
+
projectID: ProjectID.zod,
|
|
224
|
+
extra: Workspace2.Info.shape.extra
|
|
225
|
+
});
|
|
226
|
+
Workspace2.create = fn(CreateInput, async (input) => {
|
|
227
|
+
const id = WorkspaceID.ascending(input.id);
|
|
228
|
+
const adaptor = await getAdaptor(input.type);
|
|
229
|
+
const config = await adaptor.configure({ ...input, id, name: null, directory: null });
|
|
230
|
+
const info = {
|
|
231
|
+
id,
|
|
232
|
+
type: config.type,
|
|
233
|
+
branch: config.branch ?? null,
|
|
234
|
+
name: config.name ?? null,
|
|
235
|
+
directory: config.directory ?? null,
|
|
236
|
+
extra: config.extra ?? null,
|
|
237
|
+
projectID: input.projectID
|
|
238
|
+
};
|
|
239
|
+
Database.use((db) => {
|
|
240
|
+
db.insert(WorkspaceTable).values({
|
|
241
|
+
id: info.id,
|
|
242
|
+
type: info.type,
|
|
243
|
+
branch: info.branch,
|
|
244
|
+
name: info.name,
|
|
245
|
+
directory: info.directory,
|
|
246
|
+
extra: info.extra,
|
|
247
|
+
project_id: info.projectID
|
|
248
|
+
}).run();
|
|
249
|
+
});
|
|
250
|
+
await adaptor.create(config);
|
|
251
|
+
return info;
|
|
252
|
+
});
|
|
253
|
+
function list(project) {
|
|
254
|
+
const rows = Database.use(
|
|
255
|
+
(db) => db.select().from(WorkspaceTable).where((0, db_exports.eq)(WorkspaceTable.project_id, project.id)).all()
|
|
256
|
+
);
|
|
257
|
+
return rows.map(fromRow).sort((a, b) => a.id.localeCompare(b.id));
|
|
258
|
+
}
|
|
259
|
+
Workspace2.list = list;
|
|
260
|
+
Workspace2.get = fn(WorkspaceID.zod, async (id) => {
|
|
261
|
+
const row = Database.use((db) => db.select().from(WorkspaceTable).where((0, db_exports.eq)(WorkspaceTable.id, id)).get());
|
|
262
|
+
if (!row) return;
|
|
263
|
+
return fromRow(row);
|
|
264
|
+
});
|
|
265
|
+
Workspace2.remove = fn(WorkspaceID.zod, async (id) => {
|
|
266
|
+
const row = Database.use((db) => db.select().from(WorkspaceTable).where((0, db_exports.eq)(WorkspaceTable.id, id)).get());
|
|
267
|
+
if (row) {
|
|
268
|
+
const info = fromRow(row);
|
|
269
|
+
const adaptor = await getAdaptor(row.type);
|
|
270
|
+
adaptor.remove(info);
|
|
271
|
+
Database.use((db) => db.delete(WorkspaceTable).where((0, db_exports.eq)(WorkspaceTable.id, id)).run());
|
|
272
|
+
return info;
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
const log9 = Log.create({ service: "workspace-sync" });
|
|
276
|
+
async function workspaceEventLoop(space, stop) {
|
|
277
|
+
while (!stop.aborted) {
|
|
278
|
+
const adaptor = await getAdaptor(space.type);
|
|
279
|
+
const target = await Promise.resolve(adaptor.target(space));
|
|
280
|
+
if (target.type === "local") {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
const baseURL = String(target.url).replace(/\/?$/, "/");
|
|
284
|
+
const res = await fetch(new URL(baseURL + "/event"), {
|
|
285
|
+
method: "GET",
|
|
286
|
+
signal: stop
|
|
287
|
+
});
|
|
288
|
+
if (!res.ok || !res.body) {
|
|
289
|
+
await sleep(1e3);
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
await parseSSE(res.body, stop, (event) => {
|
|
293
|
+
GlobalBus.emit("event", {
|
|
294
|
+
directory: space.id,
|
|
295
|
+
payload: event
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
await sleep(250);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
function startSyncing(project) {
|
|
302
|
+
const stop = new AbortController();
|
|
303
|
+
const spaces = list(project).filter((space) => space.type !== "worktree");
|
|
304
|
+
spaces.forEach((space) => {
|
|
305
|
+
void workspaceEventLoop(space, stop.signal).catch((error) => {
|
|
306
|
+
log9.warn("workspace sync listener failed", {
|
|
307
|
+
workspaceID: space.id,
|
|
308
|
+
error
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
return {
|
|
313
|
+
async stop() {
|
|
314
|
+
stop.abort();
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
Workspace2.startSyncing = startSyncing;
|
|
319
|
+
})(Workspace || (Workspace = {}));
|
|
320
|
+
|
|
321
|
+
// packages/opencode/src/server/proxy.ts
|
|
322
|
+
import { Hono } from "hono";
|
|
323
|
+
import { createNodeWebSocket } from "@hono/node-ws";
|
|
324
|
+
var { upgradeWebSocket } = createNodeWebSocket({ app: new Hono() });
|
|
325
|
+
var hop = /* @__PURE__ */ new Set([
|
|
326
|
+
"connection",
|
|
327
|
+
"keep-alive",
|
|
328
|
+
"proxy-authenticate",
|
|
329
|
+
"proxy-authorization",
|
|
330
|
+
"proxy-connection",
|
|
331
|
+
"te",
|
|
332
|
+
"trailer",
|
|
333
|
+
"transfer-encoding",
|
|
334
|
+
"upgrade",
|
|
335
|
+
"host"
|
|
336
|
+
]);
|
|
337
|
+
function headers(req, extra) {
|
|
338
|
+
const out = new Headers(req.headers);
|
|
339
|
+
for (const key of hop) out.delete(key);
|
|
340
|
+
out.delete("x-opencode-directory");
|
|
341
|
+
out.delete("x-opencode-workspace");
|
|
342
|
+
if (!extra) return out;
|
|
343
|
+
for (const [key, value] of new Headers(extra).entries()) {
|
|
344
|
+
out.set(key, value);
|
|
345
|
+
}
|
|
346
|
+
return out;
|
|
347
|
+
}
|
|
348
|
+
function protocols(req) {
|
|
349
|
+
const value = req.headers.get("sec-websocket-protocol");
|
|
350
|
+
if (!value) return [];
|
|
351
|
+
return value.split(",").map((item) => item.trim()).filter(Boolean);
|
|
352
|
+
}
|
|
353
|
+
function socket(url) {
|
|
354
|
+
const next = new URL(url);
|
|
355
|
+
if (next.protocol === "http:") next.protocol = "ws:";
|
|
356
|
+
if (next.protocol === "https:") next.protocol = "wss:";
|
|
357
|
+
return next.toString();
|
|
358
|
+
}
|
|
359
|
+
function send(ws, data) {
|
|
360
|
+
if (data instanceof Blob) {
|
|
361
|
+
return data.arrayBuffer().then((x) => ws.send(x));
|
|
362
|
+
}
|
|
363
|
+
return ws.send(data);
|
|
364
|
+
}
|
|
365
|
+
var app = lazy(
|
|
366
|
+
() => new Hono().get(
|
|
367
|
+
"/__workspace_ws",
|
|
368
|
+
upgradeWebSocket((c) => {
|
|
369
|
+
const url = c.req.header("x-opencode-proxy-url");
|
|
370
|
+
const queue = [];
|
|
371
|
+
let remote;
|
|
372
|
+
return {
|
|
373
|
+
onOpen(_, ws) {
|
|
374
|
+
if (!url) {
|
|
375
|
+
ws.close(1011, "missing proxy target");
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
remote = new WebSocket(url, protocols(c.req.raw));
|
|
379
|
+
remote.binaryType = "arraybuffer";
|
|
380
|
+
remote.onopen = () => {
|
|
381
|
+
for (const item of queue) remote?.send(item);
|
|
382
|
+
queue.length = 0;
|
|
383
|
+
};
|
|
384
|
+
remote.onmessage = (event) => {
|
|
385
|
+
send(ws, event.data);
|
|
386
|
+
};
|
|
387
|
+
remote.onerror = () => {
|
|
388
|
+
ws.close(1011, "proxy error");
|
|
389
|
+
};
|
|
390
|
+
remote.onclose = (event) => {
|
|
391
|
+
ws.close(event.code, event.reason);
|
|
392
|
+
};
|
|
393
|
+
},
|
|
394
|
+
onMessage(event) {
|
|
395
|
+
const data = event.data;
|
|
396
|
+
if (typeof data !== "string" && !(data instanceof Uint8Array) && !(data instanceof ArrayBuffer)) return;
|
|
397
|
+
if (remote?.readyState === WebSocket.OPEN) {
|
|
398
|
+
remote.send(data);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
queue.push(data);
|
|
402
|
+
},
|
|
403
|
+
onClose(event) {
|
|
404
|
+
remote?.close(event.code, event.reason);
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
})
|
|
408
|
+
)
|
|
409
|
+
);
|
|
410
|
+
var ServerProxy;
|
|
411
|
+
((ServerProxy2) => {
|
|
412
|
+
function http(target, req) {
|
|
413
|
+
return fetch(
|
|
414
|
+
new Request(target.url, {
|
|
415
|
+
method: req.method,
|
|
416
|
+
headers: headers(req, target.headers),
|
|
417
|
+
body: req.method === "GET" || req.method === "HEAD" ? void 0 : req.body,
|
|
418
|
+
redirect: "manual",
|
|
419
|
+
signal: req.signal
|
|
420
|
+
})
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
ServerProxy2.http = http;
|
|
424
|
+
function websocket(target, req, env) {
|
|
425
|
+
const url = new URL(req.url);
|
|
426
|
+
url.pathname = "/__workspace_ws";
|
|
427
|
+
url.search = "";
|
|
428
|
+
const next = new Headers(req.headers);
|
|
429
|
+
next.set("x-opencode-proxy-url", socket(target.url));
|
|
430
|
+
return app().fetch(
|
|
431
|
+
new Request(url, {
|
|
432
|
+
method: req.method,
|
|
433
|
+
headers: next,
|
|
434
|
+
signal: req.signal
|
|
435
|
+
}),
|
|
436
|
+
env
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
ServerProxy2.websocket = websocket;
|
|
440
|
+
})(ServerProxy || (ServerProxy = {}));
|
|
441
|
+
|
|
442
|
+
// packages/opencode/src/server/instance.ts
|
|
443
|
+
import { describeRoute as describeRoute14, resolver as resolver15, validator as validator13 } from "hono-openapi";
|
|
444
|
+
import { Hono as Hono15 } from "hono";
|
|
445
|
+
import { proxy } from "hono/proxy";
|
|
446
|
+
import z19 from "zod";
|
|
447
|
+
import { createHash } from "node:crypto";
|
|
448
|
+
|
|
449
|
+
// packages/opencode/src/server/routes/tui.ts
|
|
450
|
+
import { Hono as Hono2 } from "hono";
|
|
451
|
+
import { describeRoute, validator, resolver as resolver2 } from "hono-openapi";
|
|
452
|
+
import z3 from "zod";
|
|
453
|
+
|
|
454
|
+
// packages/opencode/src/util/queue.ts
|
|
455
|
+
var AsyncQueue = class {
|
|
456
|
+
queue = [];
|
|
457
|
+
resolvers = [];
|
|
458
|
+
push(item) {
|
|
459
|
+
const resolve = this.resolvers.shift();
|
|
460
|
+
if (resolve) resolve(item);
|
|
461
|
+
else this.queue.push(item);
|
|
462
|
+
}
|
|
463
|
+
async next() {
|
|
464
|
+
if (this.queue.length > 0) return this.queue.shift();
|
|
465
|
+
return new Promise((resolve) => this.resolvers.push(resolve));
|
|
466
|
+
}
|
|
467
|
+
async *[Symbol.asyncIterator]() {
|
|
468
|
+
while (true) yield await this.next();
|
|
469
|
+
}
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
// packages/opencode/src/server/error.ts
|
|
473
|
+
import { resolver } from "hono-openapi";
|
|
474
|
+
import z2 from "zod";
|
|
475
|
+
var ERRORS = {
|
|
476
|
+
400: {
|
|
477
|
+
description: "Bad request",
|
|
478
|
+
content: {
|
|
479
|
+
"application/json": {
|
|
480
|
+
schema: resolver(
|
|
481
|
+
z2.object({
|
|
482
|
+
data: z2.any(),
|
|
483
|
+
errors: z2.array(z2.record(z2.string(), z2.any())),
|
|
484
|
+
success: z2.literal(false)
|
|
485
|
+
}).meta({
|
|
486
|
+
ref: "BadRequestError"
|
|
487
|
+
})
|
|
488
|
+
)
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
},
|
|
492
|
+
404: {
|
|
493
|
+
description: "Not found",
|
|
494
|
+
content: {
|
|
495
|
+
"application/json": {
|
|
496
|
+
schema: resolver(NotFoundError.Schema)
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
function errors(...codes) {
|
|
502
|
+
return Object.fromEntries(codes.map((code) => [code, ERRORS[code]]));
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// packages/opencode/src/server/routes/tui.ts
|
|
506
|
+
var TuiRequest = z3.object({
|
|
507
|
+
path: z3.string(),
|
|
508
|
+
body: z3.any()
|
|
509
|
+
});
|
|
510
|
+
var request = new AsyncQueue();
|
|
511
|
+
var response = new AsyncQueue();
|
|
512
|
+
var TuiControlRoutes = new Hono2().get(
|
|
513
|
+
"/next",
|
|
514
|
+
describeRoute({
|
|
515
|
+
summary: "Get next TUI request",
|
|
516
|
+
description: "Retrieve the next TUI (Terminal User Interface) request from the queue for processing.",
|
|
517
|
+
operationId: "tui.control.next",
|
|
518
|
+
responses: {
|
|
519
|
+
200: {
|
|
520
|
+
description: "Next TUI request",
|
|
521
|
+
content: {
|
|
522
|
+
"application/json": {
|
|
523
|
+
schema: resolver2(TuiRequest)
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}),
|
|
529
|
+
async (c) => {
|
|
530
|
+
const req = await request.next();
|
|
531
|
+
return c.json(req);
|
|
532
|
+
}
|
|
533
|
+
).post(
|
|
534
|
+
"/response",
|
|
535
|
+
describeRoute({
|
|
536
|
+
summary: "Submit TUI response",
|
|
537
|
+
description: "Submit a response to the TUI request queue to complete a pending request.",
|
|
538
|
+
operationId: "tui.control.response",
|
|
539
|
+
responses: {
|
|
540
|
+
200: {
|
|
541
|
+
description: "Response submitted successfully",
|
|
542
|
+
content: {
|
|
543
|
+
"application/json": {
|
|
544
|
+
schema: resolver2(z3.boolean())
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}),
|
|
550
|
+
validator("json", z3.any()),
|
|
551
|
+
async (c) => {
|
|
552
|
+
const body = c.req.valid("json");
|
|
553
|
+
response.push(body);
|
|
554
|
+
return c.json(true);
|
|
555
|
+
}
|
|
556
|
+
);
|
|
557
|
+
var TuiRoutes = lazy(
|
|
558
|
+
() => new Hono2().post(
|
|
559
|
+
"/append-prompt",
|
|
560
|
+
describeRoute({
|
|
561
|
+
summary: "Append TUI prompt",
|
|
562
|
+
description: "Append prompt to the TUI",
|
|
563
|
+
operationId: "tui.appendPrompt",
|
|
564
|
+
responses: {
|
|
565
|
+
200: {
|
|
566
|
+
description: "Prompt processed successfully",
|
|
567
|
+
content: {
|
|
568
|
+
"application/json": {
|
|
569
|
+
schema: resolver2(z3.boolean())
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
},
|
|
573
|
+
...errors(400)
|
|
574
|
+
}
|
|
575
|
+
}),
|
|
576
|
+
validator("json", TuiEvent.PromptAppend.properties),
|
|
577
|
+
async (c) => {
|
|
578
|
+
await Bus.publish(TuiEvent.PromptAppend, c.req.valid("json"));
|
|
579
|
+
return c.json(true);
|
|
580
|
+
}
|
|
581
|
+
).post(
|
|
582
|
+
"/open-help",
|
|
583
|
+
describeRoute({
|
|
584
|
+
summary: "Open help dialog",
|
|
585
|
+
description: "Open the help dialog in the TUI to display user assistance information.",
|
|
586
|
+
operationId: "tui.openHelp",
|
|
587
|
+
responses: {
|
|
588
|
+
200: {
|
|
589
|
+
description: "Help dialog opened successfully",
|
|
590
|
+
content: {
|
|
591
|
+
"application/json": {
|
|
592
|
+
schema: resolver2(z3.boolean())
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}),
|
|
598
|
+
async (c) => {
|
|
599
|
+
await Bus.publish(TuiEvent.CommandExecute, {
|
|
600
|
+
command: "help.show"
|
|
601
|
+
});
|
|
602
|
+
return c.json(true);
|
|
603
|
+
}
|
|
604
|
+
).post(
|
|
605
|
+
"/open-sessions",
|
|
606
|
+
describeRoute({
|
|
607
|
+
summary: "Open sessions dialog",
|
|
608
|
+
description: "Open the session dialog",
|
|
609
|
+
operationId: "tui.openSessions",
|
|
610
|
+
responses: {
|
|
611
|
+
200: {
|
|
612
|
+
description: "Session dialog opened successfully",
|
|
613
|
+
content: {
|
|
614
|
+
"application/json": {
|
|
615
|
+
schema: resolver2(z3.boolean())
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}),
|
|
621
|
+
async (c) => {
|
|
622
|
+
await Bus.publish(TuiEvent.CommandExecute, {
|
|
623
|
+
command: "session.list"
|
|
624
|
+
});
|
|
625
|
+
return c.json(true);
|
|
626
|
+
}
|
|
627
|
+
).post(
|
|
628
|
+
"/open-themes",
|
|
629
|
+
describeRoute({
|
|
630
|
+
summary: "Open themes dialog",
|
|
631
|
+
description: "Open the theme dialog",
|
|
632
|
+
operationId: "tui.openThemes",
|
|
633
|
+
responses: {
|
|
634
|
+
200: {
|
|
635
|
+
description: "Theme dialog opened successfully",
|
|
636
|
+
content: {
|
|
637
|
+
"application/json": {
|
|
638
|
+
schema: resolver2(z3.boolean())
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}),
|
|
644
|
+
async (c) => {
|
|
645
|
+
await Bus.publish(TuiEvent.CommandExecute, {
|
|
646
|
+
command: "session.list"
|
|
647
|
+
});
|
|
648
|
+
return c.json(true);
|
|
649
|
+
}
|
|
650
|
+
).post(
|
|
651
|
+
"/open-models",
|
|
652
|
+
describeRoute({
|
|
653
|
+
summary: "Open models dialog",
|
|
654
|
+
description: "Open the model dialog",
|
|
655
|
+
operationId: "tui.openModels",
|
|
656
|
+
responses: {
|
|
657
|
+
200: {
|
|
658
|
+
description: "Model dialog opened successfully",
|
|
659
|
+
content: {
|
|
660
|
+
"application/json": {
|
|
661
|
+
schema: resolver2(z3.boolean())
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}),
|
|
667
|
+
async (c) => {
|
|
668
|
+
await Bus.publish(TuiEvent.CommandExecute, {
|
|
669
|
+
command: "model.list"
|
|
670
|
+
});
|
|
671
|
+
return c.json(true);
|
|
672
|
+
}
|
|
673
|
+
).post(
|
|
674
|
+
"/submit-prompt",
|
|
675
|
+
describeRoute({
|
|
676
|
+
summary: "Submit TUI prompt",
|
|
677
|
+
description: "Submit the prompt",
|
|
678
|
+
operationId: "tui.submitPrompt",
|
|
679
|
+
responses: {
|
|
680
|
+
200: {
|
|
681
|
+
description: "Prompt submitted successfully",
|
|
682
|
+
content: {
|
|
683
|
+
"application/json": {
|
|
684
|
+
schema: resolver2(z3.boolean())
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}),
|
|
690
|
+
async (c) => {
|
|
691
|
+
await Bus.publish(TuiEvent.CommandExecute, {
|
|
692
|
+
command: "prompt.submit"
|
|
693
|
+
});
|
|
694
|
+
return c.json(true);
|
|
695
|
+
}
|
|
696
|
+
).post(
|
|
697
|
+
"/clear-prompt",
|
|
698
|
+
describeRoute({
|
|
699
|
+
summary: "Clear TUI prompt",
|
|
700
|
+
description: "Clear the prompt",
|
|
701
|
+
operationId: "tui.clearPrompt",
|
|
702
|
+
responses: {
|
|
703
|
+
200: {
|
|
704
|
+
description: "Prompt cleared successfully",
|
|
705
|
+
content: {
|
|
706
|
+
"application/json": {
|
|
707
|
+
schema: resolver2(z3.boolean())
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
}),
|
|
713
|
+
async (c) => {
|
|
714
|
+
await Bus.publish(TuiEvent.CommandExecute, {
|
|
715
|
+
command: "prompt.clear"
|
|
716
|
+
});
|
|
717
|
+
return c.json(true);
|
|
718
|
+
}
|
|
719
|
+
).post(
|
|
720
|
+
"/execute-command",
|
|
721
|
+
describeRoute({
|
|
722
|
+
summary: "Execute TUI command",
|
|
723
|
+
description: "Execute a TUI command (e.g. agent_cycle)",
|
|
724
|
+
operationId: "tui.executeCommand",
|
|
725
|
+
responses: {
|
|
726
|
+
200: {
|
|
727
|
+
description: "Command executed successfully",
|
|
728
|
+
content: {
|
|
729
|
+
"application/json": {
|
|
730
|
+
schema: resolver2(z3.boolean())
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
},
|
|
734
|
+
...errors(400)
|
|
735
|
+
}
|
|
736
|
+
}),
|
|
737
|
+
validator("json", z3.object({ command: z3.string() })),
|
|
738
|
+
async (c) => {
|
|
739
|
+
const command = c.req.valid("json").command;
|
|
740
|
+
await Bus.publish(TuiEvent.CommandExecute, {
|
|
741
|
+
// @ts-expect-error
|
|
742
|
+
command: {
|
|
743
|
+
session_new: "session.new",
|
|
744
|
+
session_share: "session.share",
|
|
745
|
+
session_interrupt: "session.interrupt",
|
|
746
|
+
session_compact: "session.compact",
|
|
747
|
+
messages_page_up: "session.page.up",
|
|
748
|
+
messages_page_down: "session.page.down",
|
|
749
|
+
messages_line_up: "session.line.up",
|
|
750
|
+
messages_line_down: "session.line.down",
|
|
751
|
+
messages_half_page_up: "session.half.page.up",
|
|
752
|
+
messages_half_page_down: "session.half.page.down",
|
|
753
|
+
messages_first: "session.first",
|
|
754
|
+
messages_last: "session.last",
|
|
755
|
+
agent_cycle: "agent.cycle"
|
|
756
|
+
}[command]
|
|
757
|
+
});
|
|
758
|
+
return c.json(true);
|
|
759
|
+
}
|
|
760
|
+
).post(
|
|
761
|
+
"/show-toast",
|
|
762
|
+
describeRoute({
|
|
763
|
+
summary: "Show TUI toast",
|
|
764
|
+
description: "Show a toast notification in the TUI",
|
|
765
|
+
operationId: "tui.showToast",
|
|
766
|
+
responses: {
|
|
767
|
+
200: {
|
|
768
|
+
description: "Toast notification shown successfully",
|
|
769
|
+
content: {
|
|
770
|
+
"application/json": {
|
|
771
|
+
schema: resolver2(z3.boolean())
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
}),
|
|
777
|
+
validator("json", TuiEvent.ToastShow.properties),
|
|
778
|
+
async (c) => {
|
|
779
|
+
await Bus.publish(TuiEvent.ToastShow, c.req.valid("json"));
|
|
780
|
+
return c.json(true);
|
|
781
|
+
}
|
|
782
|
+
).post(
|
|
783
|
+
"/publish",
|
|
784
|
+
describeRoute({
|
|
785
|
+
summary: "Publish TUI event",
|
|
786
|
+
description: "Publish a TUI event",
|
|
787
|
+
operationId: "tui.publish",
|
|
788
|
+
responses: {
|
|
789
|
+
200: {
|
|
790
|
+
description: "Event published successfully",
|
|
791
|
+
content: {
|
|
792
|
+
"application/json": {
|
|
793
|
+
schema: resolver2(z3.boolean())
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
},
|
|
797
|
+
...errors(400)
|
|
798
|
+
}
|
|
799
|
+
}),
|
|
800
|
+
validator(
|
|
801
|
+
"json",
|
|
802
|
+
z3.union(
|
|
803
|
+
Object.values(TuiEvent).map((def) => {
|
|
804
|
+
return z3.object({
|
|
805
|
+
type: z3.literal(def.type),
|
|
806
|
+
properties: def.properties
|
|
807
|
+
}).meta({
|
|
808
|
+
ref: "Event." + def.type
|
|
809
|
+
});
|
|
810
|
+
})
|
|
811
|
+
)
|
|
812
|
+
),
|
|
813
|
+
async (c) => {
|
|
814
|
+
const evt = c.req.valid("json");
|
|
815
|
+
await Bus.publish(Object.values(TuiEvent).find((def) => def.type === evt.type), evt.properties);
|
|
816
|
+
return c.json(true);
|
|
817
|
+
}
|
|
818
|
+
).post(
|
|
819
|
+
"/select-session",
|
|
820
|
+
describeRoute({
|
|
821
|
+
summary: "Select session",
|
|
822
|
+
description: "Navigate the TUI to display the specified session.",
|
|
823
|
+
operationId: "tui.selectSession",
|
|
824
|
+
responses: {
|
|
825
|
+
200: {
|
|
826
|
+
description: "Session selected successfully",
|
|
827
|
+
content: {
|
|
828
|
+
"application/json": {
|
|
829
|
+
schema: resolver2(z3.boolean())
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
},
|
|
833
|
+
...errors(400, 404)
|
|
834
|
+
}
|
|
835
|
+
}),
|
|
836
|
+
validator("json", TuiEvent.SessionSelect.properties),
|
|
837
|
+
async (c) => {
|
|
838
|
+
const { sessionID } = c.req.valid("json");
|
|
839
|
+
await Session.get(sessionID);
|
|
840
|
+
await Bus.publish(TuiEvent.SessionSelect, { sessionID });
|
|
841
|
+
return c.json(true);
|
|
842
|
+
}
|
|
843
|
+
).route("/control", TuiControlRoutes)
|
|
844
|
+
);
|
|
845
|
+
|
|
846
|
+
// packages/opencode/src/server/routes/question.ts
|
|
847
|
+
import { Hono as Hono3 } from "hono";
|
|
848
|
+
import { describeRoute as describeRoute2, validator as validator2 } from "hono-openapi";
|
|
849
|
+
import { resolver as resolver3 } from "hono-openapi";
|
|
850
|
+
import z4 from "zod";
|
|
851
|
+
var QuestionRoutes = lazy(
|
|
852
|
+
() => new Hono3().get(
|
|
853
|
+
"/",
|
|
854
|
+
describeRoute2({
|
|
855
|
+
summary: "List pending questions",
|
|
856
|
+
description: "Get all pending question requests across all sessions.",
|
|
857
|
+
operationId: "question.list",
|
|
858
|
+
responses: {
|
|
859
|
+
200: {
|
|
860
|
+
description: "List of pending questions",
|
|
861
|
+
content: {
|
|
862
|
+
"application/json": {
|
|
863
|
+
schema: resolver3(Question.Request.array())
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
}),
|
|
869
|
+
async (c) => {
|
|
870
|
+
const questions = await Question.list();
|
|
871
|
+
return c.json(questions);
|
|
872
|
+
}
|
|
873
|
+
).post(
|
|
874
|
+
"/:requestID/reply",
|
|
875
|
+
describeRoute2({
|
|
876
|
+
summary: "Reply to question request",
|
|
877
|
+
description: "Provide answers to a question request from the AI assistant.",
|
|
878
|
+
operationId: "question.reply",
|
|
879
|
+
responses: {
|
|
880
|
+
200: {
|
|
881
|
+
description: "Question answered successfully",
|
|
882
|
+
content: {
|
|
883
|
+
"application/json": {
|
|
884
|
+
schema: resolver3(z4.boolean())
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
},
|
|
888
|
+
...errors(400, 404)
|
|
889
|
+
}
|
|
890
|
+
}),
|
|
891
|
+
validator2(
|
|
892
|
+
"param",
|
|
893
|
+
z4.object({
|
|
894
|
+
requestID: QuestionID.zod
|
|
895
|
+
})
|
|
896
|
+
),
|
|
897
|
+
validator2("json", Question.Reply),
|
|
898
|
+
async (c) => {
|
|
899
|
+
const params = c.req.valid("param");
|
|
900
|
+
const json = c.req.valid("json");
|
|
901
|
+
await Question.reply({
|
|
902
|
+
requestID: params.requestID,
|
|
903
|
+
answers: json.answers
|
|
904
|
+
});
|
|
905
|
+
return c.json(true);
|
|
906
|
+
}
|
|
907
|
+
).post(
|
|
908
|
+
"/:requestID/reject",
|
|
909
|
+
describeRoute2({
|
|
910
|
+
summary: "Reject question request",
|
|
911
|
+
description: "Reject a question request from the AI assistant.",
|
|
912
|
+
operationId: "question.reject",
|
|
913
|
+
responses: {
|
|
914
|
+
200: {
|
|
915
|
+
description: "Question rejected successfully",
|
|
916
|
+
content: {
|
|
917
|
+
"application/json": {
|
|
918
|
+
schema: resolver3(z4.boolean())
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
},
|
|
922
|
+
...errors(400, 404)
|
|
923
|
+
}
|
|
924
|
+
}),
|
|
925
|
+
validator2(
|
|
926
|
+
"param",
|
|
927
|
+
z4.object({
|
|
928
|
+
requestID: QuestionID.zod
|
|
929
|
+
})
|
|
930
|
+
),
|
|
931
|
+
async (c) => {
|
|
932
|
+
const params = c.req.valid("param");
|
|
933
|
+
await Question.reject(params.requestID);
|
|
934
|
+
return c.json(true);
|
|
935
|
+
}
|
|
936
|
+
)
|
|
937
|
+
);
|
|
938
|
+
|
|
939
|
+
// packages/opencode/src/server/routes/permission.ts
|
|
940
|
+
import { Hono as Hono4 } from "hono";
|
|
941
|
+
import { describeRoute as describeRoute3, validator as validator3, resolver as resolver4 } from "hono-openapi";
|
|
942
|
+
import z5 from "zod";
|
|
943
|
+
var PermissionRoutes = lazy(
|
|
944
|
+
() => new Hono4().post(
|
|
945
|
+
"/:requestID/reply",
|
|
946
|
+
describeRoute3({
|
|
947
|
+
summary: "Respond to permission request",
|
|
948
|
+
description: "Approve or deny a permission request from the AI assistant.",
|
|
949
|
+
operationId: "permission.reply",
|
|
950
|
+
responses: {
|
|
951
|
+
200: {
|
|
952
|
+
description: "Permission processed successfully",
|
|
953
|
+
content: {
|
|
954
|
+
"application/json": {
|
|
955
|
+
schema: resolver4(z5.boolean())
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
},
|
|
959
|
+
...errors(400, 404)
|
|
960
|
+
}
|
|
961
|
+
}),
|
|
962
|
+
validator3(
|
|
963
|
+
"param",
|
|
964
|
+
z5.object({
|
|
965
|
+
requestID: PermissionID.zod
|
|
966
|
+
})
|
|
967
|
+
),
|
|
968
|
+
validator3("json", z5.object({ reply: Permission.Reply, message: z5.string().optional() })),
|
|
969
|
+
async (c) => {
|
|
970
|
+
const params = c.req.valid("param");
|
|
971
|
+
const json = c.req.valid("json");
|
|
972
|
+
await Permission.reply({
|
|
973
|
+
requestID: params.requestID,
|
|
974
|
+
reply: json.reply,
|
|
975
|
+
message: json.message
|
|
976
|
+
});
|
|
977
|
+
return c.json(true);
|
|
978
|
+
}
|
|
979
|
+
).get(
|
|
980
|
+
"/",
|
|
981
|
+
describeRoute3({
|
|
982
|
+
summary: "List pending permissions",
|
|
983
|
+
description: "Get all pending permission requests across all sessions.",
|
|
984
|
+
operationId: "permission.list",
|
|
985
|
+
responses: {
|
|
986
|
+
200: {
|
|
987
|
+
description: "List of pending permissions",
|
|
988
|
+
content: {
|
|
989
|
+
"application/json": {
|
|
990
|
+
schema: resolver4(Permission.Request.array())
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
}),
|
|
996
|
+
async (c) => {
|
|
997
|
+
const permissions = await Permission.list();
|
|
998
|
+
return c.json(permissions);
|
|
999
|
+
}
|
|
1000
|
+
)
|
|
1001
|
+
);
|
|
1002
|
+
|
|
1003
|
+
// packages/opencode/src/server/routes/project.ts
|
|
1004
|
+
import { Hono as Hono5 } from "hono";
|
|
1005
|
+
import { describeRoute as describeRoute4, validator as validator4 } from "hono-openapi";
|
|
1006
|
+
import { resolver as resolver5 } from "hono-openapi";
|
|
1007
|
+
import z6 from "zod";
|
|
1008
|
+
var ProjectRoutes = lazy(
|
|
1009
|
+
() => new Hono5().get(
|
|
1010
|
+
"/",
|
|
1011
|
+
describeRoute4({
|
|
1012
|
+
summary: "List all projects",
|
|
1013
|
+
description: "Get a list of projects that have been opened with OpenCode.",
|
|
1014
|
+
operationId: "project.list",
|
|
1015
|
+
responses: {
|
|
1016
|
+
200: {
|
|
1017
|
+
description: "List of projects",
|
|
1018
|
+
content: {
|
|
1019
|
+
"application/json": {
|
|
1020
|
+
schema: resolver5(Project.Info.array())
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
}),
|
|
1026
|
+
async (c) => {
|
|
1027
|
+
const projects = Project.list();
|
|
1028
|
+
return c.json(projects);
|
|
1029
|
+
}
|
|
1030
|
+
).get(
|
|
1031
|
+
"/current",
|
|
1032
|
+
describeRoute4({
|
|
1033
|
+
summary: "Get current project",
|
|
1034
|
+
description: "Retrieve the currently active project that OpenCode is working with.",
|
|
1035
|
+
operationId: "project.current",
|
|
1036
|
+
responses: {
|
|
1037
|
+
200: {
|
|
1038
|
+
description: "Current project information",
|
|
1039
|
+
content: {
|
|
1040
|
+
"application/json": {
|
|
1041
|
+
schema: resolver5(Project.Info)
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
}),
|
|
1047
|
+
async (c) => {
|
|
1048
|
+
return c.json(Instance.project);
|
|
1049
|
+
}
|
|
1050
|
+
).post(
|
|
1051
|
+
"/git/init",
|
|
1052
|
+
describeRoute4({
|
|
1053
|
+
summary: "Initialize git repository",
|
|
1054
|
+
description: "Create a git repository for the current project and return the refreshed project info.",
|
|
1055
|
+
operationId: "project.initGit",
|
|
1056
|
+
responses: {
|
|
1057
|
+
200: {
|
|
1058
|
+
description: "Project information after git initialization",
|
|
1059
|
+
content: {
|
|
1060
|
+
"application/json": {
|
|
1061
|
+
schema: resolver5(Project.Info)
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
}),
|
|
1067
|
+
async (c) => {
|
|
1068
|
+
const dir = Instance.directory;
|
|
1069
|
+
const prev = Instance.project;
|
|
1070
|
+
const next = await Project.initGit({
|
|
1071
|
+
directory: dir,
|
|
1072
|
+
project: prev
|
|
1073
|
+
});
|
|
1074
|
+
if (next.id === prev.id && next.vcs === prev.vcs && next.worktree === prev.worktree) return c.json(next);
|
|
1075
|
+
await Instance.reload({
|
|
1076
|
+
directory: dir,
|
|
1077
|
+
worktree: dir,
|
|
1078
|
+
project: next,
|
|
1079
|
+
init: InstanceBootstrap
|
|
1080
|
+
});
|
|
1081
|
+
return c.json(next);
|
|
1082
|
+
}
|
|
1083
|
+
).patch(
|
|
1084
|
+
"/:projectID",
|
|
1085
|
+
describeRoute4({
|
|
1086
|
+
summary: "Update project",
|
|
1087
|
+
description: "Update project properties such as name, icon, and commands.",
|
|
1088
|
+
operationId: "project.update",
|
|
1089
|
+
responses: {
|
|
1090
|
+
200: {
|
|
1091
|
+
description: "Updated project information",
|
|
1092
|
+
content: {
|
|
1093
|
+
"application/json": {
|
|
1094
|
+
schema: resolver5(Project.Info)
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
},
|
|
1098
|
+
...errors(400, 404)
|
|
1099
|
+
}
|
|
1100
|
+
}),
|
|
1101
|
+
validator4("param", z6.object({ projectID: ProjectID.zod })),
|
|
1102
|
+
validator4("json", Project.UpdateInput.omit({ projectID: true })),
|
|
1103
|
+
async (c) => {
|
|
1104
|
+
const projectID = c.req.valid("param").projectID;
|
|
1105
|
+
const body = c.req.valid("json");
|
|
1106
|
+
const project = await Project.update({ ...body, projectID });
|
|
1107
|
+
return c.json(project);
|
|
1108
|
+
}
|
|
1109
|
+
)
|
|
1110
|
+
);
|
|
1111
|
+
|
|
1112
|
+
// packages/opencode/src/server/routes/session.ts
|
|
1113
|
+
import { Hono as Hono6 } from "hono";
|
|
1114
|
+
import { stream } from "hono/streaming";
|
|
1115
|
+
import { describeRoute as describeRoute5, validator as validator5, resolver as resolver6 } from "hono-openapi";
|
|
1116
|
+
import z7 from "zod";
|
|
1117
|
+
var log = Log.create({ service: "server" });
|
|
1118
|
+
var SessionRoutes = lazy(
|
|
1119
|
+
() => new Hono6().get(
|
|
1120
|
+
"/",
|
|
1121
|
+
describeRoute5({
|
|
1122
|
+
summary: "List sessions",
|
|
1123
|
+
description: "Get a list of all OpenCode sessions, sorted by most recently updated.",
|
|
1124
|
+
operationId: "session.list",
|
|
1125
|
+
responses: {
|
|
1126
|
+
200: {
|
|
1127
|
+
description: "List of sessions",
|
|
1128
|
+
content: {
|
|
1129
|
+
"application/json": {
|
|
1130
|
+
schema: resolver6(Session.Info.array())
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
}),
|
|
1136
|
+
validator5(
|
|
1137
|
+
"query",
|
|
1138
|
+
z7.object({
|
|
1139
|
+
directory: z7.string().optional().meta({ description: "Filter sessions by project directory" }),
|
|
1140
|
+
roots: z7.coerce.boolean().optional().meta({ description: "Only return root sessions (no parentID)" }),
|
|
1141
|
+
start: z7.coerce.number().optional().meta({ description: "Filter sessions updated on or after this timestamp (milliseconds since epoch)" }),
|
|
1142
|
+
search: z7.string().optional().meta({ description: "Filter sessions by title (case-insensitive)" }),
|
|
1143
|
+
limit: z7.coerce.number().optional().meta({ description: "Maximum number of sessions to return" })
|
|
1144
|
+
})
|
|
1145
|
+
),
|
|
1146
|
+
async (c) => {
|
|
1147
|
+
const query = c.req.valid("query");
|
|
1148
|
+
const sessions = [];
|
|
1149
|
+
for await (const session of Session.list({
|
|
1150
|
+
directory: query.directory,
|
|
1151
|
+
roots: query.roots,
|
|
1152
|
+
start: query.start,
|
|
1153
|
+
search: query.search,
|
|
1154
|
+
limit: query.limit
|
|
1155
|
+
})) {
|
|
1156
|
+
sessions.push(session);
|
|
1157
|
+
}
|
|
1158
|
+
return c.json(sessions);
|
|
1159
|
+
}
|
|
1160
|
+
).get(
|
|
1161
|
+
"/status",
|
|
1162
|
+
describeRoute5({
|
|
1163
|
+
summary: "Get session status",
|
|
1164
|
+
description: "Retrieve the current status of all sessions, including active, idle, and completed states.",
|
|
1165
|
+
operationId: "session.status",
|
|
1166
|
+
responses: {
|
|
1167
|
+
200: {
|
|
1168
|
+
description: "Get session status",
|
|
1169
|
+
content: {
|
|
1170
|
+
"application/json": {
|
|
1171
|
+
schema: resolver6(z7.record(z7.string(), SessionStatus.Info))
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
},
|
|
1175
|
+
...errors(400)
|
|
1176
|
+
}
|
|
1177
|
+
}),
|
|
1178
|
+
async (c) => {
|
|
1179
|
+
const result = await SessionStatus.list();
|
|
1180
|
+
return c.json(Object.fromEntries(result));
|
|
1181
|
+
}
|
|
1182
|
+
).get(
|
|
1183
|
+
"/:sessionID",
|
|
1184
|
+
describeRoute5({
|
|
1185
|
+
summary: "Get session",
|
|
1186
|
+
description: "Retrieve detailed information about a specific OpenCode session.",
|
|
1187
|
+
tags: ["Session"],
|
|
1188
|
+
operationId: "session.get",
|
|
1189
|
+
responses: {
|
|
1190
|
+
200: {
|
|
1191
|
+
description: "Get session",
|
|
1192
|
+
content: {
|
|
1193
|
+
"application/json": {
|
|
1194
|
+
schema: resolver6(Session.Info)
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
},
|
|
1198
|
+
...errors(400, 404)
|
|
1199
|
+
}
|
|
1200
|
+
}),
|
|
1201
|
+
validator5(
|
|
1202
|
+
"param",
|
|
1203
|
+
z7.object({
|
|
1204
|
+
sessionID: Session.get.schema
|
|
1205
|
+
})
|
|
1206
|
+
),
|
|
1207
|
+
async (c) => {
|
|
1208
|
+
const sessionID = c.req.valid("param").sessionID;
|
|
1209
|
+
log.info("SEARCH", { url: c.req.url });
|
|
1210
|
+
const session = await Session.get(sessionID);
|
|
1211
|
+
return c.json(session);
|
|
1212
|
+
}
|
|
1213
|
+
).get(
|
|
1214
|
+
"/:sessionID/children",
|
|
1215
|
+
describeRoute5({
|
|
1216
|
+
summary: "Get session children",
|
|
1217
|
+
tags: ["Session"],
|
|
1218
|
+
description: "Retrieve all child sessions that were forked from the specified parent session.",
|
|
1219
|
+
operationId: "session.children",
|
|
1220
|
+
responses: {
|
|
1221
|
+
200: {
|
|
1222
|
+
description: "List of children",
|
|
1223
|
+
content: {
|
|
1224
|
+
"application/json": {
|
|
1225
|
+
schema: resolver6(Session.Info.array())
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
},
|
|
1229
|
+
...errors(400, 404)
|
|
1230
|
+
}
|
|
1231
|
+
}),
|
|
1232
|
+
validator5(
|
|
1233
|
+
"param",
|
|
1234
|
+
z7.object({
|
|
1235
|
+
sessionID: Session.children.schema
|
|
1236
|
+
})
|
|
1237
|
+
),
|
|
1238
|
+
async (c) => {
|
|
1239
|
+
const sessionID = c.req.valid("param").sessionID;
|
|
1240
|
+
const session = await Session.children(sessionID);
|
|
1241
|
+
return c.json(session);
|
|
1242
|
+
}
|
|
1243
|
+
).get(
|
|
1244
|
+
"/:sessionID/todo",
|
|
1245
|
+
describeRoute5({
|
|
1246
|
+
summary: "Get session todos",
|
|
1247
|
+
description: "Retrieve the todo list associated with a specific session, showing tasks and action items.",
|
|
1248
|
+
operationId: "session.todo",
|
|
1249
|
+
responses: {
|
|
1250
|
+
200: {
|
|
1251
|
+
description: "Todo list",
|
|
1252
|
+
content: {
|
|
1253
|
+
"application/json": {
|
|
1254
|
+
schema: resolver6(Todo.Info.array())
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
},
|
|
1258
|
+
...errors(400, 404)
|
|
1259
|
+
}
|
|
1260
|
+
}),
|
|
1261
|
+
validator5(
|
|
1262
|
+
"param",
|
|
1263
|
+
z7.object({
|
|
1264
|
+
sessionID: SessionID.zod
|
|
1265
|
+
})
|
|
1266
|
+
),
|
|
1267
|
+
async (c) => {
|
|
1268
|
+
const sessionID = c.req.valid("param").sessionID;
|
|
1269
|
+
const todos = await Todo.get(sessionID);
|
|
1270
|
+
return c.json(todos);
|
|
1271
|
+
}
|
|
1272
|
+
).post(
|
|
1273
|
+
"/",
|
|
1274
|
+
describeRoute5({
|
|
1275
|
+
summary: "Create session",
|
|
1276
|
+
description: "Create a new OpenCode session for interacting with AI assistants and managing conversations.",
|
|
1277
|
+
operationId: "session.create",
|
|
1278
|
+
responses: {
|
|
1279
|
+
...errors(400),
|
|
1280
|
+
200: {
|
|
1281
|
+
description: "Successfully created session",
|
|
1282
|
+
content: {
|
|
1283
|
+
"application/json": {
|
|
1284
|
+
schema: resolver6(Session.Info)
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
}),
|
|
1290
|
+
validator5("json", Session.create.schema.optional()),
|
|
1291
|
+
async (c) => {
|
|
1292
|
+
const body = c.req.valid("json") ?? {};
|
|
1293
|
+
const session = await Session.create(body);
|
|
1294
|
+
return c.json(session);
|
|
1295
|
+
}
|
|
1296
|
+
).delete(
|
|
1297
|
+
"/:sessionID",
|
|
1298
|
+
describeRoute5({
|
|
1299
|
+
summary: "Delete session",
|
|
1300
|
+
description: "Delete a session and permanently remove all associated data, including messages and history.",
|
|
1301
|
+
operationId: "session.delete",
|
|
1302
|
+
responses: {
|
|
1303
|
+
200: {
|
|
1304
|
+
description: "Successfully deleted session",
|
|
1305
|
+
content: {
|
|
1306
|
+
"application/json": {
|
|
1307
|
+
schema: resolver6(z7.boolean())
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
},
|
|
1311
|
+
...errors(400, 404)
|
|
1312
|
+
}
|
|
1313
|
+
}),
|
|
1314
|
+
validator5(
|
|
1315
|
+
"param",
|
|
1316
|
+
z7.object({
|
|
1317
|
+
sessionID: Session.remove.schema
|
|
1318
|
+
})
|
|
1319
|
+
),
|
|
1320
|
+
async (c) => {
|
|
1321
|
+
const sessionID = c.req.valid("param").sessionID;
|
|
1322
|
+
await Session.remove(sessionID);
|
|
1323
|
+
return c.json(true);
|
|
1324
|
+
}
|
|
1325
|
+
).patch(
|
|
1326
|
+
"/:sessionID",
|
|
1327
|
+
describeRoute5({
|
|
1328
|
+
summary: "Update session",
|
|
1329
|
+
description: "Update properties of an existing session, such as title or other metadata.",
|
|
1330
|
+
operationId: "session.update",
|
|
1331
|
+
responses: {
|
|
1332
|
+
200: {
|
|
1333
|
+
description: "Successfully updated session",
|
|
1334
|
+
content: {
|
|
1335
|
+
"application/json": {
|
|
1336
|
+
schema: resolver6(Session.Info)
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
},
|
|
1340
|
+
...errors(400, 404)
|
|
1341
|
+
}
|
|
1342
|
+
}),
|
|
1343
|
+
validator5(
|
|
1344
|
+
"param",
|
|
1345
|
+
z7.object({
|
|
1346
|
+
sessionID: SessionID.zod
|
|
1347
|
+
})
|
|
1348
|
+
),
|
|
1349
|
+
validator5(
|
|
1350
|
+
"json",
|
|
1351
|
+
z7.object({
|
|
1352
|
+
title: z7.string().optional(),
|
|
1353
|
+
time: z7.object({
|
|
1354
|
+
archived: z7.number().optional()
|
|
1355
|
+
}).optional()
|
|
1356
|
+
})
|
|
1357
|
+
),
|
|
1358
|
+
async (c) => {
|
|
1359
|
+
const sessionID = c.req.valid("param").sessionID;
|
|
1360
|
+
const updates = c.req.valid("json");
|
|
1361
|
+
if (updates.title !== void 0) {
|
|
1362
|
+
await Session.setTitle({ sessionID, title: updates.title });
|
|
1363
|
+
}
|
|
1364
|
+
if (updates.time?.archived !== void 0) {
|
|
1365
|
+
await Session.setArchived({ sessionID, time: updates.time.archived });
|
|
1366
|
+
}
|
|
1367
|
+
const session = await Session.get(sessionID);
|
|
1368
|
+
return c.json(session);
|
|
1369
|
+
}
|
|
1370
|
+
).post(
|
|
1371
|
+
"/:sessionID/init",
|
|
1372
|
+
describeRoute5({
|
|
1373
|
+
summary: "Initialize session",
|
|
1374
|
+
description: "Analyze the current application and create an AGENTS.md file with project-specific agent configurations.",
|
|
1375
|
+
operationId: "session.init",
|
|
1376
|
+
responses: {
|
|
1377
|
+
200: {
|
|
1378
|
+
description: "200",
|
|
1379
|
+
content: {
|
|
1380
|
+
"application/json": {
|
|
1381
|
+
schema: resolver6(z7.boolean())
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
},
|
|
1385
|
+
...errors(400, 404)
|
|
1386
|
+
}
|
|
1387
|
+
}),
|
|
1388
|
+
validator5(
|
|
1389
|
+
"param",
|
|
1390
|
+
z7.object({
|
|
1391
|
+
sessionID: SessionID.zod
|
|
1392
|
+
})
|
|
1393
|
+
),
|
|
1394
|
+
validator5("json", Session.initialize.schema.omit({ sessionID: true })),
|
|
1395
|
+
async (c) => {
|
|
1396
|
+
const sessionID = c.req.valid("param").sessionID;
|
|
1397
|
+
const body = c.req.valid("json");
|
|
1398
|
+
await Session.initialize({ ...body, sessionID });
|
|
1399
|
+
return c.json(true);
|
|
1400
|
+
}
|
|
1401
|
+
).post(
|
|
1402
|
+
"/:sessionID/fork",
|
|
1403
|
+
describeRoute5({
|
|
1404
|
+
summary: "Fork session",
|
|
1405
|
+
description: "Create a new session by forking an existing session at a specific message point.",
|
|
1406
|
+
operationId: "session.fork",
|
|
1407
|
+
responses: {
|
|
1408
|
+
200: {
|
|
1409
|
+
description: "200",
|
|
1410
|
+
content: {
|
|
1411
|
+
"application/json": {
|
|
1412
|
+
schema: resolver6(Session.Info)
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
}),
|
|
1418
|
+
validator5(
|
|
1419
|
+
"param",
|
|
1420
|
+
z7.object({
|
|
1421
|
+
sessionID: Session.fork.schema.shape.sessionID
|
|
1422
|
+
})
|
|
1423
|
+
),
|
|
1424
|
+
validator5("json", Session.fork.schema.omit({ sessionID: true })),
|
|
1425
|
+
async (c) => {
|
|
1426
|
+
const sessionID = c.req.valid("param").sessionID;
|
|
1427
|
+
const body = c.req.valid("json");
|
|
1428
|
+
const result = await Session.fork({ ...body, sessionID });
|
|
1429
|
+
return c.json(result);
|
|
1430
|
+
}
|
|
1431
|
+
).post(
|
|
1432
|
+
"/:sessionID/abort",
|
|
1433
|
+
describeRoute5({
|
|
1434
|
+
summary: "Abort session",
|
|
1435
|
+
description: "Abort an active session and stop any ongoing AI processing or command execution.",
|
|
1436
|
+
operationId: "session.abort",
|
|
1437
|
+
responses: {
|
|
1438
|
+
200: {
|
|
1439
|
+
description: "Aborted session",
|
|
1440
|
+
content: {
|
|
1441
|
+
"application/json": {
|
|
1442
|
+
schema: resolver6(z7.boolean())
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
},
|
|
1446
|
+
...errors(400, 404)
|
|
1447
|
+
}
|
|
1448
|
+
}),
|
|
1449
|
+
validator5(
|
|
1450
|
+
"param",
|
|
1451
|
+
z7.object({
|
|
1452
|
+
sessionID: SessionID.zod
|
|
1453
|
+
})
|
|
1454
|
+
),
|
|
1455
|
+
async (c) => {
|
|
1456
|
+
await SessionPrompt.cancel(c.req.valid("param").sessionID);
|
|
1457
|
+
return c.json(true);
|
|
1458
|
+
}
|
|
1459
|
+
).post(
|
|
1460
|
+
"/:sessionID/share",
|
|
1461
|
+
describeRoute5({
|
|
1462
|
+
summary: "Share session",
|
|
1463
|
+
description: "Create a shareable link for a session, allowing others to view the conversation.",
|
|
1464
|
+
operationId: "session.share",
|
|
1465
|
+
responses: {
|
|
1466
|
+
200: {
|
|
1467
|
+
description: "Successfully shared session",
|
|
1468
|
+
content: {
|
|
1469
|
+
"application/json": {
|
|
1470
|
+
schema: resolver6(Session.Info)
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
},
|
|
1474
|
+
...errors(400, 404)
|
|
1475
|
+
}
|
|
1476
|
+
}),
|
|
1477
|
+
validator5(
|
|
1478
|
+
"param",
|
|
1479
|
+
z7.object({
|
|
1480
|
+
sessionID: SessionID.zod
|
|
1481
|
+
})
|
|
1482
|
+
),
|
|
1483
|
+
async (c) => {
|
|
1484
|
+
const sessionID = c.req.valid("param").sessionID;
|
|
1485
|
+
await Session.share(sessionID);
|
|
1486
|
+
const session = await Session.get(sessionID);
|
|
1487
|
+
return c.json(session);
|
|
1488
|
+
}
|
|
1489
|
+
).get(
|
|
1490
|
+
"/:sessionID/diff",
|
|
1491
|
+
describeRoute5({
|
|
1492
|
+
summary: "Get message diff",
|
|
1493
|
+
description: "Get the file changes (diff) that resulted from a specific user message in the session.",
|
|
1494
|
+
operationId: "session.diff",
|
|
1495
|
+
responses: {
|
|
1496
|
+
200: {
|
|
1497
|
+
description: "Successfully retrieved diff",
|
|
1498
|
+
content: {
|
|
1499
|
+
"application/json": {
|
|
1500
|
+
schema: resolver6(Snapshot.FileDiff.array())
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
}),
|
|
1506
|
+
validator5(
|
|
1507
|
+
"param",
|
|
1508
|
+
z7.object({
|
|
1509
|
+
sessionID: SessionSummary.DiffInput.shape.sessionID
|
|
1510
|
+
})
|
|
1511
|
+
),
|
|
1512
|
+
validator5(
|
|
1513
|
+
"query",
|
|
1514
|
+
z7.object({
|
|
1515
|
+
messageID: SessionSummary.DiffInput.shape.messageID
|
|
1516
|
+
})
|
|
1517
|
+
),
|
|
1518
|
+
async (c) => {
|
|
1519
|
+
const query = c.req.valid("query");
|
|
1520
|
+
const params = c.req.valid("param");
|
|
1521
|
+
const result = await SessionSummary.diff({
|
|
1522
|
+
sessionID: params.sessionID,
|
|
1523
|
+
messageID: query.messageID
|
|
1524
|
+
});
|
|
1525
|
+
return c.json(result);
|
|
1526
|
+
}
|
|
1527
|
+
).delete(
|
|
1528
|
+
"/:sessionID/share",
|
|
1529
|
+
describeRoute5({
|
|
1530
|
+
summary: "Unshare session",
|
|
1531
|
+
description: "Remove the shareable link for a session, making it private again.",
|
|
1532
|
+
operationId: "session.unshare",
|
|
1533
|
+
responses: {
|
|
1534
|
+
200: {
|
|
1535
|
+
description: "Successfully unshared session",
|
|
1536
|
+
content: {
|
|
1537
|
+
"application/json": {
|
|
1538
|
+
schema: resolver6(Session.Info)
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
},
|
|
1542
|
+
...errors(400, 404)
|
|
1543
|
+
}
|
|
1544
|
+
}),
|
|
1545
|
+
validator5(
|
|
1546
|
+
"param",
|
|
1547
|
+
z7.object({
|
|
1548
|
+
sessionID: Session.unshare.schema
|
|
1549
|
+
})
|
|
1550
|
+
),
|
|
1551
|
+
async (c) => {
|
|
1552
|
+
const sessionID = c.req.valid("param").sessionID;
|
|
1553
|
+
await Session.unshare(sessionID);
|
|
1554
|
+
const session = await Session.get(sessionID);
|
|
1555
|
+
return c.json(session);
|
|
1556
|
+
}
|
|
1557
|
+
).post(
|
|
1558
|
+
"/:sessionID/summarize",
|
|
1559
|
+
describeRoute5({
|
|
1560
|
+
summary: "Summarize session",
|
|
1561
|
+
description: "Generate a concise summary of the session using AI compaction to preserve key information.",
|
|
1562
|
+
operationId: "session.summarize",
|
|
1563
|
+
responses: {
|
|
1564
|
+
200: {
|
|
1565
|
+
description: "Summarized session",
|
|
1566
|
+
content: {
|
|
1567
|
+
"application/json": {
|
|
1568
|
+
schema: resolver6(z7.boolean())
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
},
|
|
1572
|
+
...errors(400, 404)
|
|
1573
|
+
}
|
|
1574
|
+
}),
|
|
1575
|
+
validator5(
|
|
1576
|
+
"param",
|
|
1577
|
+
z7.object({
|
|
1578
|
+
sessionID: SessionID.zod
|
|
1579
|
+
})
|
|
1580
|
+
),
|
|
1581
|
+
validator5(
|
|
1582
|
+
"json",
|
|
1583
|
+
z7.object({
|
|
1584
|
+
providerID: ProviderID.zod,
|
|
1585
|
+
modelID: ModelID.zod,
|
|
1586
|
+
auto: z7.boolean().optional().default(false)
|
|
1587
|
+
})
|
|
1588
|
+
),
|
|
1589
|
+
async (c) => {
|
|
1590
|
+
const sessionID = c.req.valid("param").sessionID;
|
|
1591
|
+
const body = c.req.valid("json");
|
|
1592
|
+
const session = await Session.get(sessionID);
|
|
1593
|
+
await SessionRevert.cleanup(session);
|
|
1594
|
+
const msgs = await Session.messages({ sessionID });
|
|
1595
|
+
let currentAgent = await Agent.defaultAgent();
|
|
1596
|
+
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
1597
|
+
const info = msgs[i].info;
|
|
1598
|
+
if (info.role === "user") {
|
|
1599
|
+
currentAgent = info.agent || await Agent.defaultAgent();
|
|
1600
|
+
break;
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
await SessionCompaction.create({
|
|
1604
|
+
sessionID,
|
|
1605
|
+
agent: currentAgent,
|
|
1606
|
+
model: {
|
|
1607
|
+
providerID: body.providerID,
|
|
1608
|
+
modelID: body.modelID
|
|
1609
|
+
},
|
|
1610
|
+
auto: body.auto
|
|
1611
|
+
});
|
|
1612
|
+
await SessionPrompt.loop({ sessionID });
|
|
1613
|
+
return c.json(true);
|
|
1614
|
+
}
|
|
1615
|
+
).get(
|
|
1616
|
+
"/:sessionID/message",
|
|
1617
|
+
describeRoute5({
|
|
1618
|
+
summary: "Get session messages",
|
|
1619
|
+
description: "Retrieve all messages in a session, including user prompts and AI responses.",
|
|
1620
|
+
operationId: "session.messages",
|
|
1621
|
+
responses: {
|
|
1622
|
+
200: {
|
|
1623
|
+
description: "List of messages",
|
|
1624
|
+
content: {
|
|
1625
|
+
"application/json": {
|
|
1626
|
+
schema: resolver6(MessageV2.WithParts.array())
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
},
|
|
1630
|
+
...errors(400, 404)
|
|
1631
|
+
}
|
|
1632
|
+
}),
|
|
1633
|
+
validator5(
|
|
1634
|
+
"param",
|
|
1635
|
+
z7.object({
|
|
1636
|
+
sessionID: SessionID.zod
|
|
1637
|
+
})
|
|
1638
|
+
),
|
|
1639
|
+
validator5(
|
|
1640
|
+
"query",
|
|
1641
|
+
z7.object({
|
|
1642
|
+
limit: z7.coerce.number().int().min(0).optional().meta({ description: "Maximum number of messages to return" }),
|
|
1643
|
+
before: z7.string().optional().meta({ description: "Opaque cursor for loading older messages" }).refine(
|
|
1644
|
+
(value) => {
|
|
1645
|
+
if (!value) return true;
|
|
1646
|
+
try {
|
|
1647
|
+
MessageV2.cursor.decode(value);
|
|
1648
|
+
return true;
|
|
1649
|
+
} catch {
|
|
1650
|
+
return false;
|
|
1651
|
+
}
|
|
1652
|
+
},
|
|
1653
|
+
{ message: "Invalid cursor" }
|
|
1654
|
+
)
|
|
1655
|
+
}).refine((value) => !value.before || value.limit !== void 0, {
|
|
1656
|
+
message: "before requires limit",
|
|
1657
|
+
path: ["before"]
|
|
1658
|
+
})
|
|
1659
|
+
),
|
|
1660
|
+
async (c) => {
|
|
1661
|
+
const query = c.req.valid("query");
|
|
1662
|
+
const sessionID = c.req.valid("param").sessionID;
|
|
1663
|
+
if (query.limit === void 0) {
|
|
1664
|
+
await Session.get(sessionID);
|
|
1665
|
+
const messages = await Session.messages({ sessionID });
|
|
1666
|
+
return c.json(messages);
|
|
1667
|
+
}
|
|
1668
|
+
if (query.limit === 0) {
|
|
1669
|
+
await Session.get(sessionID);
|
|
1670
|
+
const messages = await Session.messages({ sessionID });
|
|
1671
|
+
return c.json(messages);
|
|
1672
|
+
}
|
|
1673
|
+
const page = await MessageV2.page({
|
|
1674
|
+
sessionID,
|
|
1675
|
+
limit: query.limit,
|
|
1676
|
+
before: query.before
|
|
1677
|
+
});
|
|
1678
|
+
if (page.cursor) {
|
|
1679
|
+
const url = new URL(c.req.url);
|
|
1680
|
+
url.searchParams.set("limit", query.limit.toString());
|
|
1681
|
+
url.searchParams.set("before", page.cursor);
|
|
1682
|
+
c.header("Access-Control-Expose-Headers", "Link, X-Next-Cursor");
|
|
1683
|
+
c.header("Link", `<${url.toString()}>; rel="next"`);
|
|
1684
|
+
c.header("X-Next-Cursor", page.cursor);
|
|
1685
|
+
}
|
|
1686
|
+
return c.json(page.items);
|
|
1687
|
+
}
|
|
1688
|
+
).get(
|
|
1689
|
+
"/:sessionID/message/:messageID",
|
|
1690
|
+
describeRoute5({
|
|
1691
|
+
summary: "Get message",
|
|
1692
|
+
description: "Retrieve a specific message from a session by its message ID.",
|
|
1693
|
+
operationId: "session.message",
|
|
1694
|
+
responses: {
|
|
1695
|
+
200: {
|
|
1696
|
+
description: "Message",
|
|
1697
|
+
content: {
|
|
1698
|
+
"application/json": {
|
|
1699
|
+
schema: resolver6(
|
|
1700
|
+
z7.object({
|
|
1701
|
+
info: MessageV2.Info,
|
|
1702
|
+
parts: MessageV2.Part.array()
|
|
1703
|
+
})
|
|
1704
|
+
)
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
},
|
|
1708
|
+
...errors(400, 404)
|
|
1709
|
+
}
|
|
1710
|
+
}),
|
|
1711
|
+
validator5(
|
|
1712
|
+
"param",
|
|
1713
|
+
z7.object({
|
|
1714
|
+
sessionID: SessionID.zod,
|
|
1715
|
+
messageID: MessageID.zod
|
|
1716
|
+
})
|
|
1717
|
+
),
|
|
1718
|
+
async (c) => {
|
|
1719
|
+
const params = c.req.valid("param");
|
|
1720
|
+
const message = await MessageV2.get({
|
|
1721
|
+
sessionID: params.sessionID,
|
|
1722
|
+
messageID: params.messageID
|
|
1723
|
+
});
|
|
1724
|
+
return c.json(message);
|
|
1725
|
+
}
|
|
1726
|
+
).delete(
|
|
1727
|
+
"/:sessionID/message/:messageID",
|
|
1728
|
+
describeRoute5({
|
|
1729
|
+
summary: "Delete message",
|
|
1730
|
+
description: "Permanently delete a specific message (and all of its parts) from a session. This does not revert any file changes that may have been made while processing the message.",
|
|
1731
|
+
operationId: "session.deleteMessage",
|
|
1732
|
+
responses: {
|
|
1733
|
+
200: {
|
|
1734
|
+
description: "Successfully deleted message",
|
|
1735
|
+
content: {
|
|
1736
|
+
"application/json": {
|
|
1737
|
+
schema: resolver6(z7.boolean())
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
},
|
|
1741
|
+
...errors(400, 404)
|
|
1742
|
+
}
|
|
1743
|
+
}),
|
|
1744
|
+
validator5(
|
|
1745
|
+
"param",
|
|
1746
|
+
z7.object({
|
|
1747
|
+
sessionID: SessionID.zod,
|
|
1748
|
+
messageID: MessageID.zod
|
|
1749
|
+
})
|
|
1750
|
+
),
|
|
1751
|
+
async (c) => {
|
|
1752
|
+
const params = c.req.valid("param");
|
|
1753
|
+
await SessionPrompt.assertNotBusy(params.sessionID);
|
|
1754
|
+
await Session.removeMessage({
|
|
1755
|
+
sessionID: params.sessionID,
|
|
1756
|
+
messageID: params.messageID
|
|
1757
|
+
});
|
|
1758
|
+
return c.json(true);
|
|
1759
|
+
}
|
|
1760
|
+
).delete(
|
|
1761
|
+
"/:sessionID/message/:messageID/part/:partID",
|
|
1762
|
+
describeRoute5({
|
|
1763
|
+
description: "Delete a part from a message",
|
|
1764
|
+
operationId: "part.delete",
|
|
1765
|
+
responses: {
|
|
1766
|
+
200: {
|
|
1767
|
+
description: "Successfully deleted part",
|
|
1768
|
+
content: {
|
|
1769
|
+
"application/json": {
|
|
1770
|
+
schema: resolver6(z7.boolean())
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
},
|
|
1774
|
+
...errors(400, 404)
|
|
1775
|
+
}
|
|
1776
|
+
}),
|
|
1777
|
+
validator5(
|
|
1778
|
+
"param",
|
|
1779
|
+
z7.object({
|
|
1780
|
+
sessionID: SessionID.zod,
|
|
1781
|
+
messageID: MessageID.zod,
|
|
1782
|
+
partID: PartID.zod
|
|
1783
|
+
})
|
|
1784
|
+
),
|
|
1785
|
+
async (c) => {
|
|
1786
|
+
const params = c.req.valid("param");
|
|
1787
|
+
await Session.removePart({
|
|
1788
|
+
sessionID: params.sessionID,
|
|
1789
|
+
messageID: params.messageID,
|
|
1790
|
+
partID: params.partID
|
|
1791
|
+
});
|
|
1792
|
+
return c.json(true);
|
|
1793
|
+
}
|
|
1794
|
+
).patch(
|
|
1795
|
+
"/:sessionID/message/:messageID/part/:partID",
|
|
1796
|
+
describeRoute5({
|
|
1797
|
+
description: "Update a part in a message",
|
|
1798
|
+
operationId: "part.update",
|
|
1799
|
+
responses: {
|
|
1800
|
+
200: {
|
|
1801
|
+
description: "Successfully updated part",
|
|
1802
|
+
content: {
|
|
1803
|
+
"application/json": {
|
|
1804
|
+
schema: resolver6(MessageV2.Part)
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
},
|
|
1808
|
+
...errors(400, 404)
|
|
1809
|
+
}
|
|
1810
|
+
}),
|
|
1811
|
+
validator5(
|
|
1812
|
+
"param",
|
|
1813
|
+
z7.object({
|
|
1814
|
+
sessionID: SessionID.zod,
|
|
1815
|
+
messageID: MessageID.zod,
|
|
1816
|
+
partID: PartID.zod
|
|
1817
|
+
})
|
|
1818
|
+
),
|
|
1819
|
+
validator5("json", MessageV2.Part),
|
|
1820
|
+
async (c) => {
|
|
1821
|
+
const params = c.req.valid("param");
|
|
1822
|
+
const body = c.req.valid("json");
|
|
1823
|
+
if (body.id !== params.partID || body.messageID !== params.messageID || body.sessionID !== params.sessionID) {
|
|
1824
|
+
throw new Error(
|
|
1825
|
+
`Part mismatch: body.id='${body.id}' vs partID='${params.partID}', body.messageID='${body.messageID}' vs messageID='${params.messageID}', body.sessionID='${body.sessionID}' vs sessionID='${params.sessionID}'`
|
|
1826
|
+
);
|
|
1827
|
+
}
|
|
1828
|
+
const part = await Session.updatePart(body);
|
|
1829
|
+
return c.json(part);
|
|
1830
|
+
}
|
|
1831
|
+
).post(
|
|
1832
|
+
"/:sessionID/message",
|
|
1833
|
+
describeRoute5({
|
|
1834
|
+
summary: "Send message",
|
|
1835
|
+
description: "Create and send a new message to a session, streaming the AI response.",
|
|
1836
|
+
operationId: "session.prompt",
|
|
1837
|
+
responses: {
|
|
1838
|
+
200: {
|
|
1839
|
+
description: "Created message",
|
|
1840
|
+
content: {
|
|
1841
|
+
"application/json": {
|
|
1842
|
+
schema: resolver6(
|
|
1843
|
+
z7.object({
|
|
1844
|
+
info: MessageV2.Assistant,
|
|
1845
|
+
parts: MessageV2.Part.array()
|
|
1846
|
+
})
|
|
1847
|
+
)
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
},
|
|
1851
|
+
...errors(400, 404)
|
|
1852
|
+
}
|
|
1853
|
+
}),
|
|
1854
|
+
validator5(
|
|
1855
|
+
"param",
|
|
1856
|
+
z7.object({
|
|
1857
|
+
sessionID: SessionID.zod
|
|
1858
|
+
})
|
|
1859
|
+
),
|
|
1860
|
+
validator5("json", SessionPrompt.PromptInput.omit({ sessionID: true })),
|
|
1861
|
+
async (c) => {
|
|
1862
|
+
c.status(200);
|
|
1863
|
+
c.header("Content-Type", "application/json");
|
|
1864
|
+
return stream(c, async (stream2) => {
|
|
1865
|
+
const sessionID = c.req.valid("param").sessionID;
|
|
1866
|
+
const body = c.req.valid("json");
|
|
1867
|
+
const msg = await SessionPrompt.prompt({ ...body, sessionID });
|
|
1868
|
+
stream2.write(JSON.stringify(msg));
|
|
1869
|
+
});
|
|
1870
|
+
}
|
|
1871
|
+
).post(
|
|
1872
|
+
"/:sessionID/prompt_async",
|
|
1873
|
+
describeRoute5({
|
|
1874
|
+
summary: "Send async message",
|
|
1875
|
+
description: "Create and send a new message to a session asynchronously, starting the session if needed and returning immediately.",
|
|
1876
|
+
operationId: "session.prompt_async",
|
|
1877
|
+
responses: {
|
|
1878
|
+
202: {
|
|
1879
|
+
description: "Prompt accepted"
|
|
1880
|
+
},
|
|
1881
|
+
...errors(400, 404)
|
|
1882
|
+
}
|
|
1883
|
+
}),
|
|
1884
|
+
validator5(
|
|
1885
|
+
"param",
|
|
1886
|
+
z7.object({
|
|
1887
|
+
sessionID: SessionID.zod
|
|
1888
|
+
})
|
|
1889
|
+
),
|
|
1890
|
+
validator5("json", SessionPrompt.PromptInput.omit({ sessionID: true })),
|
|
1891
|
+
async (c) => {
|
|
1892
|
+
c.status(202);
|
|
1893
|
+
return stream(c, async () => {
|
|
1894
|
+
const sessionID = c.req.valid("param").sessionID;
|
|
1895
|
+
const body = c.req.valid("json");
|
|
1896
|
+
SessionPrompt.prompt({ ...body, sessionID }).catch((err) => {
|
|
1897
|
+
log.error("prompt_async failed", { sessionID, error: err });
|
|
1898
|
+
Bus.publish(Session.Event.Error, {
|
|
1899
|
+
sessionID,
|
|
1900
|
+
error: new NamedError.Unknown({ message: err instanceof Error ? err.message : String(err) }).toObject()
|
|
1901
|
+
});
|
|
1902
|
+
});
|
|
1903
|
+
});
|
|
1904
|
+
}
|
|
1905
|
+
).post(
|
|
1906
|
+
"/:sessionID/command",
|
|
1907
|
+
describeRoute5({
|
|
1908
|
+
summary: "Send command",
|
|
1909
|
+
description: "Send a new command to a session for execution by the AI assistant.",
|
|
1910
|
+
operationId: "session.command",
|
|
1911
|
+
responses: {
|
|
1912
|
+
200: {
|
|
1913
|
+
description: "Created message",
|
|
1914
|
+
content: {
|
|
1915
|
+
"application/json": {
|
|
1916
|
+
schema: resolver6(
|
|
1917
|
+
z7.object({
|
|
1918
|
+
info: MessageV2.Assistant,
|
|
1919
|
+
parts: MessageV2.Part.array()
|
|
1920
|
+
})
|
|
1921
|
+
)
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
},
|
|
1925
|
+
...errors(400, 404)
|
|
1926
|
+
}
|
|
1927
|
+
}),
|
|
1928
|
+
validator5(
|
|
1929
|
+
"param",
|
|
1930
|
+
z7.object({
|
|
1931
|
+
sessionID: SessionID.zod
|
|
1932
|
+
})
|
|
1933
|
+
),
|
|
1934
|
+
validator5("json", SessionPrompt.CommandInput.omit({ sessionID: true })),
|
|
1935
|
+
async (c) => {
|
|
1936
|
+
const sessionID = c.req.valid("param").sessionID;
|
|
1937
|
+
const body = c.req.valid("json");
|
|
1938
|
+
const msg = await SessionPrompt.command({ ...body, sessionID });
|
|
1939
|
+
return c.json(msg);
|
|
1940
|
+
}
|
|
1941
|
+
).post(
|
|
1942
|
+
"/:sessionID/shell",
|
|
1943
|
+
describeRoute5({
|
|
1944
|
+
summary: "Run shell command",
|
|
1945
|
+
description: "Execute a shell command within the session context and return the AI's response.",
|
|
1946
|
+
operationId: "session.shell",
|
|
1947
|
+
responses: {
|
|
1948
|
+
200: {
|
|
1949
|
+
description: "Created message",
|
|
1950
|
+
content: {
|
|
1951
|
+
"application/json": {
|
|
1952
|
+
schema: resolver6(MessageV2.Assistant)
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
},
|
|
1956
|
+
...errors(400, 404)
|
|
1957
|
+
}
|
|
1958
|
+
}),
|
|
1959
|
+
validator5(
|
|
1960
|
+
"param",
|
|
1961
|
+
z7.object({
|
|
1962
|
+
sessionID: SessionID.zod
|
|
1963
|
+
})
|
|
1964
|
+
),
|
|
1965
|
+
validator5("json", SessionPrompt.ShellInput.omit({ sessionID: true })),
|
|
1966
|
+
async (c) => {
|
|
1967
|
+
const sessionID = c.req.valid("param").sessionID;
|
|
1968
|
+
const body = c.req.valid("json");
|
|
1969
|
+
const msg = await SessionPrompt.shell({ ...body, sessionID });
|
|
1970
|
+
return c.json(msg);
|
|
1971
|
+
}
|
|
1972
|
+
).post(
|
|
1973
|
+
"/:sessionID/revert",
|
|
1974
|
+
describeRoute5({
|
|
1975
|
+
summary: "Revert message",
|
|
1976
|
+
description: "Revert a specific message in a session, undoing its effects and restoring the previous state.",
|
|
1977
|
+
operationId: "session.revert",
|
|
1978
|
+
responses: {
|
|
1979
|
+
200: {
|
|
1980
|
+
description: "Updated session",
|
|
1981
|
+
content: {
|
|
1982
|
+
"application/json": {
|
|
1983
|
+
schema: resolver6(Session.Info)
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
},
|
|
1987
|
+
...errors(400, 404)
|
|
1988
|
+
}
|
|
1989
|
+
}),
|
|
1990
|
+
validator5(
|
|
1991
|
+
"param",
|
|
1992
|
+
z7.object({
|
|
1993
|
+
sessionID: SessionID.zod
|
|
1994
|
+
})
|
|
1995
|
+
),
|
|
1996
|
+
validator5("json", SessionRevert.RevertInput.omit({ sessionID: true })),
|
|
1997
|
+
async (c) => {
|
|
1998
|
+
const sessionID = c.req.valid("param").sessionID;
|
|
1999
|
+
log.info("revert", c.req.valid("json"));
|
|
2000
|
+
const session = await SessionRevert.revert({
|
|
2001
|
+
sessionID,
|
|
2002
|
+
...c.req.valid("json")
|
|
2003
|
+
});
|
|
2004
|
+
return c.json(session);
|
|
2005
|
+
}
|
|
2006
|
+
).post(
|
|
2007
|
+
"/:sessionID/unrevert",
|
|
2008
|
+
describeRoute5({
|
|
2009
|
+
summary: "Restore reverted messages",
|
|
2010
|
+
description: "Restore all previously reverted messages in a session.",
|
|
2011
|
+
operationId: "session.unrevert",
|
|
2012
|
+
responses: {
|
|
2013
|
+
200: {
|
|
2014
|
+
description: "Updated session",
|
|
2015
|
+
content: {
|
|
2016
|
+
"application/json": {
|
|
2017
|
+
schema: resolver6(Session.Info)
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
},
|
|
2021
|
+
...errors(400, 404)
|
|
2022
|
+
}
|
|
2023
|
+
}),
|
|
2024
|
+
validator5(
|
|
2025
|
+
"param",
|
|
2026
|
+
z7.object({
|
|
2027
|
+
sessionID: SessionID.zod
|
|
2028
|
+
})
|
|
2029
|
+
),
|
|
2030
|
+
async (c) => {
|
|
2031
|
+
const sessionID = c.req.valid("param").sessionID;
|
|
2032
|
+
const session = await SessionRevert.unrevert({ sessionID });
|
|
2033
|
+
return c.json(session);
|
|
2034
|
+
}
|
|
2035
|
+
).post(
|
|
2036
|
+
"/:sessionID/permissions/:permissionID",
|
|
2037
|
+
describeRoute5({
|
|
2038
|
+
summary: "Respond to permission",
|
|
2039
|
+
deprecated: true,
|
|
2040
|
+
description: "Approve or deny a permission request from the AI assistant.",
|
|
2041
|
+
operationId: "permission.respond",
|
|
2042
|
+
responses: {
|
|
2043
|
+
200: {
|
|
2044
|
+
description: "Permission processed successfully",
|
|
2045
|
+
content: {
|
|
2046
|
+
"application/json": {
|
|
2047
|
+
schema: resolver6(z7.boolean())
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
},
|
|
2051
|
+
...errors(400, 404)
|
|
2052
|
+
}
|
|
2053
|
+
}),
|
|
2054
|
+
validator5(
|
|
2055
|
+
"param",
|
|
2056
|
+
z7.object({
|
|
2057
|
+
sessionID: SessionID.zod,
|
|
2058
|
+
permissionID: PermissionID.zod
|
|
2059
|
+
})
|
|
2060
|
+
),
|
|
2061
|
+
validator5("json", z7.object({ response: Permission.Reply })),
|
|
2062
|
+
async (c) => {
|
|
2063
|
+
const params = c.req.valid("param");
|
|
2064
|
+
Permission.reply({
|
|
2065
|
+
requestID: params.permissionID,
|
|
2066
|
+
reply: c.req.valid("json").response
|
|
2067
|
+
});
|
|
2068
|
+
return c.json(true);
|
|
2069
|
+
}
|
|
2070
|
+
)
|
|
2071
|
+
);
|
|
2072
|
+
|
|
2073
|
+
// packages/opencode/src/server/routes/pty.ts
|
|
2074
|
+
import { Hono as Hono7 } from "hono";
|
|
2075
|
+
import { describeRoute as describeRoute6, validator as validator6, resolver as resolver7 } from "hono-openapi";
|
|
2076
|
+
import z10 from "zod";
|
|
2077
|
+
|
|
2078
|
+
// packages/opencode/src/pty/index.ts
|
|
2079
|
+
import z9 from "zod";
|
|
2080
|
+
|
|
2081
|
+
// packages/util/src/lazy.ts
|
|
2082
|
+
function lazy2(fn2) {
|
|
2083
|
+
let value;
|
|
2084
|
+
let loaded = false;
|
|
2085
|
+
return () => {
|
|
2086
|
+
if (loaded) return value;
|
|
2087
|
+
loaded = true;
|
|
2088
|
+
value = fn2();
|
|
2089
|
+
return value;
|
|
2090
|
+
};
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
// packages/opencode/src/pty/schema.ts
|
|
2094
|
+
import { Schema } from "effect";
|
|
2095
|
+
import z8 from "zod";
|
|
2096
|
+
var ptyIdSchema = Schema.String.pipe(Schema.brand("PtyID"));
|
|
2097
|
+
var PtyID = ptyIdSchema.pipe(
|
|
2098
|
+
withStatics((schema) => ({
|
|
2099
|
+
make: (id) => schema.makeUnsafe(id),
|
|
2100
|
+
ascending: (id) => schema.makeUnsafe(Identifier.ascending("pty", id)),
|
|
2101
|
+
zod: Identifier.schema("pty").pipe(z8.custom())
|
|
2102
|
+
}))
|
|
2103
|
+
);
|
|
2104
|
+
|
|
2105
|
+
// packages/opencode/src/pty/index.ts
|
|
2106
|
+
import { Effect, Layer, ServiceMap } from "effect";
|
|
2107
|
+
var Pty;
|
|
2108
|
+
((Pty2) => {
|
|
2109
|
+
const log9 = Log.create({ service: "pty" });
|
|
2110
|
+
const BUFFER_LIMIT = 1024 * 1024 * 2;
|
|
2111
|
+
const BUFFER_CHUNK = 64 * 1024;
|
|
2112
|
+
const encoder = new TextEncoder();
|
|
2113
|
+
const sock = (ws) => ws.data && typeof ws.data === "object" ? ws.data : ws;
|
|
2114
|
+
const meta = (cursor) => {
|
|
2115
|
+
const json = JSON.stringify({ cursor });
|
|
2116
|
+
const bytes = encoder.encode(json);
|
|
2117
|
+
const out = new Uint8Array(bytes.length + 1);
|
|
2118
|
+
out[0] = 0;
|
|
2119
|
+
out.set(bytes, 1);
|
|
2120
|
+
return out;
|
|
2121
|
+
};
|
|
2122
|
+
const pty = lazy2(() => import("./pty.node-RHOAUGH6.js"));
|
|
2123
|
+
Pty2.Info = z9.object({
|
|
2124
|
+
id: PtyID.zod,
|
|
2125
|
+
title: z9.string(),
|
|
2126
|
+
command: z9.string(),
|
|
2127
|
+
args: z9.array(z9.string()),
|
|
2128
|
+
cwd: z9.string(),
|
|
2129
|
+
status: z9.enum(["running", "exited"]),
|
|
2130
|
+
pid: z9.number()
|
|
2131
|
+
}).meta({ ref: "Pty" });
|
|
2132
|
+
Pty2.CreateInput = z9.object({
|
|
2133
|
+
command: z9.string().optional(),
|
|
2134
|
+
args: z9.array(z9.string()).optional(),
|
|
2135
|
+
cwd: z9.string().optional(),
|
|
2136
|
+
title: z9.string().optional(),
|
|
2137
|
+
env: z9.record(z9.string(), z9.string()).optional()
|
|
2138
|
+
});
|
|
2139
|
+
Pty2.UpdateInput = z9.object({
|
|
2140
|
+
title: z9.string().optional(),
|
|
2141
|
+
size: z9.object({
|
|
2142
|
+
rows: z9.number(),
|
|
2143
|
+
cols: z9.number()
|
|
2144
|
+
}).optional()
|
|
2145
|
+
});
|
|
2146
|
+
Pty2.Event = {
|
|
2147
|
+
Created: BusEvent.define("pty.created", z9.object({ info: Pty2.Info })),
|
|
2148
|
+
Updated: BusEvent.define("pty.updated", z9.object({ info: Pty2.Info })),
|
|
2149
|
+
Exited: BusEvent.define("pty.exited", z9.object({ id: PtyID.zod, exitCode: z9.number() })),
|
|
2150
|
+
Deleted: BusEvent.define("pty.deleted", z9.object({ id: PtyID.zod }))
|
|
2151
|
+
};
|
|
2152
|
+
class Service extends ServiceMap.Service()("@opencode/Pty") {
|
|
2153
|
+
}
|
|
2154
|
+
Pty2.Service = Service;
|
|
2155
|
+
Pty2.layer = Layer.effect(
|
|
2156
|
+
Service,
|
|
2157
|
+
Effect.gen(function* () {
|
|
2158
|
+
const bus = yield* Bus.Service;
|
|
2159
|
+
const plugin = yield* Plugin.Service;
|
|
2160
|
+
function teardown(session) {
|
|
2161
|
+
try {
|
|
2162
|
+
session.process.kill();
|
|
2163
|
+
} catch {
|
|
2164
|
+
}
|
|
2165
|
+
for (const [sub, ws] of session.subscribers.entries()) {
|
|
2166
|
+
try {
|
|
2167
|
+
if (sock(ws) === sub) ws.close();
|
|
2168
|
+
} catch {
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
session.subscribers.clear();
|
|
2172
|
+
}
|
|
2173
|
+
const state = yield* InstanceState.make(
|
|
2174
|
+
Effect.fn("Pty.state")(function* (ctx) {
|
|
2175
|
+
const state2 = {
|
|
2176
|
+
dir: ctx.directory,
|
|
2177
|
+
sessions: /* @__PURE__ */ new Map()
|
|
2178
|
+
};
|
|
2179
|
+
yield* Effect.addFinalizer(
|
|
2180
|
+
() => Effect.sync(() => {
|
|
2181
|
+
for (const session of state2.sessions.values()) {
|
|
2182
|
+
teardown(session);
|
|
2183
|
+
}
|
|
2184
|
+
state2.sessions.clear();
|
|
2185
|
+
})
|
|
2186
|
+
);
|
|
2187
|
+
return state2;
|
|
2188
|
+
})
|
|
2189
|
+
);
|
|
2190
|
+
const remove2 = Effect.fn("Pty.remove")(function* (id) {
|
|
2191
|
+
const s = yield* InstanceState.get(state);
|
|
2192
|
+
const session = s.sessions.get(id);
|
|
2193
|
+
if (!session) return;
|
|
2194
|
+
s.sessions.delete(id);
|
|
2195
|
+
log9.info("removing session", { id });
|
|
2196
|
+
teardown(session);
|
|
2197
|
+
yield* bus.publish(Pty2.Event.Deleted, { id: session.info.id });
|
|
2198
|
+
});
|
|
2199
|
+
const list2 = Effect.fn("Pty.list")(function* () {
|
|
2200
|
+
const s = yield* InstanceState.get(state);
|
|
2201
|
+
return Array.from(s.sessions.values()).map((session) => session.info);
|
|
2202
|
+
});
|
|
2203
|
+
const get2 = Effect.fn("Pty.get")(function* (id) {
|
|
2204
|
+
const s = yield* InstanceState.get(state);
|
|
2205
|
+
return s.sessions.get(id)?.info;
|
|
2206
|
+
});
|
|
2207
|
+
const create2 = Effect.fn("Pty.create")(function* (input) {
|
|
2208
|
+
const s = yield* InstanceState.get(state);
|
|
2209
|
+
const id = PtyID.ascending();
|
|
2210
|
+
const command = input.command || Shell.preferred();
|
|
2211
|
+
const args = input.args || [];
|
|
2212
|
+
if (Shell.login(command)) {
|
|
2213
|
+
args.push("-l");
|
|
2214
|
+
}
|
|
2215
|
+
const cwd = input.cwd || s.dir;
|
|
2216
|
+
const shell = yield* plugin.trigger("shell.env", { cwd }, { env: {} });
|
|
2217
|
+
const env = {
|
|
2218
|
+
...process.env,
|
|
2219
|
+
...input.env,
|
|
2220
|
+
...shell.env,
|
|
2221
|
+
TERM: "xterm-256color",
|
|
2222
|
+
OPENCODE_TERMINAL: "1"
|
|
2223
|
+
};
|
|
2224
|
+
if (process.platform === "win32") {
|
|
2225
|
+
env.LC_ALL = "C.UTF-8";
|
|
2226
|
+
env.LC_CTYPE = "C.UTF-8";
|
|
2227
|
+
env.LANG = "C.UTF-8";
|
|
2228
|
+
}
|
|
2229
|
+
log9.info("creating session", { id, cmd: command, args, cwd });
|
|
2230
|
+
const { spawn } = yield* Effect.promise(() => pty());
|
|
2231
|
+
const proc = yield* Effect.sync(
|
|
2232
|
+
() => spawn(command, args, {
|
|
2233
|
+
name: "xterm-256color",
|
|
2234
|
+
cwd,
|
|
2235
|
+
env
|
|
2236
|
+
})
|
|
2237
|
+
);
|
|
2238
|
+
const info = {
|
|
2239
|
+
id,
|
|
2240
|
+
title: input.title || `Terminal ${id.slice(-4)}`,
|
|
2241
|
+
command,
|
|
2242
|
+
args,
|
|
2243
|
+
cwd,
|
|
2244
|
+
status: "running",
|
|
2245
|
+
pid: proc.pid
|
|
2246
|
+
};
|
|
2247
|
+
const session = {
|
|
2248
|
+
info,
|
|
2249
|
+
process: proc,
|
|
2250
|
+
buffer: "",
|
|
2251
|
+
bufferCursor: 0,
|
|
2252
|
+
cursor: 0,
|
|
2253
|
+
subscribers: /* @__PURE__ */ new Map()
|
|
2254
|
+
};
|
|
2255
|
+
s.sessions.set(id, session);
|
|
2256
|
+
proc.onData(
|
|
2257
|
+
Instance.bind((chunk) => {
|
|
2258
|
+
session.cursor += chunk.length;
|
|
2259
|
+
for (const [key, ws] of session.subscribers.entries()) {
|
|
2260
|
+
if (ws.readyState !== 1) {
|
|
2261
|
+
session.subscribers.delete(key);
|
|
2262
|
+
continue;
|
|
2263
|
+
}
|
|
2264
|
+
if (sock(ws) !== key) {
|
|
2265
|
+
session.subscribers.delete(key);
|
|
2266
|
+
continue;
|
|
2267
|
+
}
|
|
2268
|
+
try {
|
|
2269
|
+
ws.send(chunk);
|
|
2270
|
+
} catch {
|
|
2271
|
+
session.subscribers.delete(key);
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
session.buffer += chunk;
|
|
2275
|
+
if (session.buffer.length <= BUFFER_LIMIT) return;
|
|
2276
|
+
const excess = session.buffer.length - BUFFER_LIMIT;
|
|
2277
|
+
session.buffer = session.buffer.slice(excess);
|
|
2278
|
+
session.bufferCursor += excess;
|
|
2279
|
+
})
|
|
2280
|
+
);
|
|
2281
|
+
proc.onExit(
|
|
2282
|
+
Instance.bind(({ exitCode }) => {
|
|
2283
|
+
if (session.info.status === "exited") return;
|
|
2284
|
+
log9.info("session exited", { id, exitCode });
|
|
2285
|
+
session.info.status = "exited";
|
|
2286
|
+
Effect.runFork(bus.publish(Pty2.Event.Exited, { id, exitCode }));
|
|
2287
|
+
Effect.runFork(remove2(id));
|
|
2288
|
+
})
|
|
2289
|
+
);
|
|
2290
|
+
yield* bus.publish(Pty2.Event.Created, { info });
|
|
2291
|
+
return info;
|
|
2292
|
+
});
|
|
2293
|
+
const update2 = Effect.fn("Pty.update")(function* (id, input) {
|
|
2294
|
+
const s = yield* InstanceState.get(state);
|
|
2295
|
+
const session = s.sessions.get(id);
|
|
2296
|
+
if (!session) return;
|
|
2297
|
+
if (input.title) {
|
|
2298
|
+
session.info.title = input.title;
|
|
2299
|
+
}
|
|
2300
|
+
if (input.size) {
|
|
2301
|
+
session.process.resize(input.size.cols, input.size.rows);
|
|
2302
|
+
}
|
|
2303
|
+
yield* bus.publish(Pty2.Event.Updated, { info: session.info });
|
|
2304
|
+
return session.info;
|
|
2305
|
+
});
|
|
2306
|
+
const resize2 = Effect.fn("Pty.resize")(function* (id, cols, rows) {
|
|
2307
|
+
const s = yield* InstanceState.get(state);
|
|
2308
|
+
const session = s.sessions.get(id);
|
|
2309
|
+
if (session && session.info.status === "running") {
|
|
2310
|
+
session.process.resize(cols, rows);
|
|
2311
|
+
}
|
|
2312
|
+
});
|
|
2313
|
+
const write2 = Effect.fn("Pty.write")(function* (id, data) {
|
|
2314
|
+
const s = yield* InstanceState.get(state);
|
|
2315
|
+
const session = s.sessions.get(id);
|
|
2316
|
+
if (session && session.info.status === "running") {
|
|
2317
|
+
session.process.write(data);
|
|
2318
|
+
}
|
|
2319
|
+
});
|
|
2320
|
+
const connect2 = Effect.fn("Pty.connect")(function* (id, ws, cursor) {
|
|
2321
|
+
const s = yield* InstanceState.get(state);
|
|
2322
|
+
const session = s.sessions.get(id);
|
|
2323
|
+
if (!session) {
|
|
2324
|
+
ws.close();
|
|
2325
|
+
return;
|
|
2326
|
+
}
|
|
2327
|
+
log9.info("client connected to session", { id });
|
|
2328
|
+
const sub = sock(ws);
|
|
2329
|
+
session.subscribers.delete(sub);
|
|
2330
|
+
session.subscribers.set(sub, ws);
|
|
2331
|
+
const cleanup = () => {
|
|
2332
|
+
session.subscribers.delete(sub);
|
|
2333
|
+
};
|
|
2334
|
+
const start = session.bufferCursor;
|
|
2335
|
+
const end = session.cursor;
|
|
2336
|
+
const from = cursor === -1 ? end : typeof cursor === "number" && Number.isSafeInteger(cursor) ? Math.max(0, cursor) : 0;
|
|
2337
|
+
const data = (() => {
|
|
2338
|
+
if (!session.buffer) return "";
|
|
2339
|
+
if (from >= end) return "";
|
|
2340
|
+
const offset = Math.max(0, from - start);
|
|
2341
|
+
if (offset >= session.buffer.length) return "";
|
|
2342
|
+
return session.buffer.slice(offset);
|
|
2343
|
+
})();
|
|
2344
|
+
if (data) {
|
|
2345
|
+
try {
|
|
2346
|
+
for (let i = 0; i < data.length; i += BUFFER_CHUNK) {
|
|
2347
|
+
ws.send(data.slice(i, i + BUFFER_CHUNK));
|
|
2348
|
+
}
|
|
2349
|
+
} catch {
|
|
2350
|
+
cleanup();
|
|
2351
|
+
ws.close();
|
|
2352
|
+
return;
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
try {
|
|
2356
|
+
ws.send(meta(end));
|
|
2357
|
+
} catch {
|
|
2358
|
+
cleanup();
|
|
2359
|
+
ws.close();
|
|
2360
|
+
return;
|
|
2361
|
+
}
|
|
2362
|
+
return {
|
|
2363
|
+
onMessage: (message) => {
|
|
2364
|
+
session.process.write(String(message));
|
|
2365
|
+
},
|
|
2366
|
+
onClose: () => {
|
|
2367
|
+
log9.info("client disconnected from session", { id });
|
|
2368
|
+
cleanup();
|
|
2369
|
+
}
|
|
2370
|
+
};
|
|
2371
|
+
});
|
|
2372
|
+
return Service.of({ list: list2, get: get2, create: create2, update: update2, remove: remove2, resize: resize2, write: write2, connect: connect2 });
|
|
2373
|
+
})
|
|
2374
|
+
);
|
|
2375
|
+
const defaultLayer = Pty2.layer.pipe(Layer.provide(Bus.layer), Layer.provide(Plugin.defaultLayer));
|
|
2376
|
+
const { runPromise } = makeRuntime(Service, defaultLayer);
|
|
2377
|
+
async function list() {
|
|
2378
|
+
return runPromise((svc) => svc.list());
|
|
2379
|
+
}
|
|
2380
|
+
Pty2.list = list;
|
|
2381
|
+
async function get(id) {
|
|
2382
|
+
return runPromise((svc) => svc.get(id));
|
|
2383
|
+
}
|
|
2384
|
+
Pty2.get = get;
|
|
2385
|
+
async function resize(id, cols, rows) {
|
|
2386
|
+
return runPromise((svc) => svc.resize(id, cols, rows));
|
|
2387
|
+
}
|
|
2388
|
+
Pty2.resize = resize;
|
|
2389
|
+
async function write(id, data) {
|
|
2390
|
+
return runPromise((svc) => svc.write(id, data));
|
|
2391
|
+
}
|
|
2392
|
+
Pty2.write = write;
|
|
2393
|
+
async function connect(id, ws, cursor) {
|
|
2394
|
+
return runPromise((svc) => svc.connect(id, ws, cursor));
|
|
2395
|
+
}
|
|
2396
|
+
Pty2.connect = connect;
|
|
2397
|
+
async function create(input) {
|
|
2398
|
+
return runPromise((svc) => svc.create(input));
|
|
2399
|
+
}
|
|
2400
|
+
Pty2.create = create;
|
|
2401
|
+
async function update(id, input) {
|
|
2402
|
+
return runPromise((svc) => svc.update(id, input));
|
|
2403
|
+
}
|
|
2404
|
+
Pty2.update = update;
|
|
2405
|
+
async function remove(id) {
|
|
2406
|
+
return runPromise((svc) => svc.remove(id));
|
|
2407
|
+
}
|
|
2408
|
+
Pty2.remove = remove;
|
|
2409
|
+
})(Pty || (Pty = {}));
|
|
2410
|
+
|
|
2411
|
+
// packages/opencode/src/server/routes/pty.ts
|
|
2412
|
+
function PtyRoutes(upgradeWebSocket2) {
|
|
2413
|
+
return new Hono7().get(
|
|
2414
|
+
"/",
|
|
2415
|
+
describeRoute6({
|
|
2416
|
+
summary: "List PTY sessions",
|
|
2417
|
+
description: "Get a list of all active pseudo-terminal (PTY) sessions managed by OpenCode.",
|
|
2418
|
+
operationId: "pty.list",
|
|
2419
|
+
responses: {
|
|
2420
|
+
200: {
|
|
2421
|
+
description: "List of sessions",
|
|
2422
|
+
content: {
|
|
2423
|
+
"application/json": {
|
|
2424
|
+
schema: resolver7(Pty.Info.array())
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
}),
|
|
2430
|
+
async (c) => {
|
|
2431
|
+
return c.json(await Pty.list());
|
|
2432
|
+
}
|
|
2433
|
+
).post(
|
|
2434
|
+
"/",
|
|
2435
|
+
describeRoute6({
|
|
2436
|
+
summary: "Create PTY session",
|
|
2437
|
+
description: "Create a new pseudo-terminal (PTY) session for running shell commands and processes.",
|
|
2438
|
+
operationId: "pty.create",
|
|
2439
|
+
responses: {
|
|
2440
|
+
200: {
|
|
2441
|
+
description: "Created session",
|
|
2442
|
+
content: {
|
|
2443
|
+
"application/json": {
|
|
2444
|
+
schema: resolver7(Pty.Info)
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2447
|
+
},
|
|
2448
|
+
...errors(400)
|
|
2449
|
+
}
|
|
2450
|
+
}),
|
|
2451
|
+
validator6("json", Pty.CreateInput),
|
|
2452
|
+
async (c) => {
|
|
2453
|
+
const info = await Pty.create(c.req.valid("json"));
|
|
2454
|
+
return c.json(info);
|
|
2455
|
+
}
|
|
2456
|
+
).get(
|
|
2457
|
+
"/:ptyID",
|
|
2458
|
+
describeRoute6({
|
|
2459
|
+
summary: "Get PTY session",
|
|
2460
|
+
description: "Retrieve detailed information about a specific pseudo-terminal (PTY) session.",
|
|
2461
|
+
operationId: "pty.get",
|
|
2462
|
+
responses: {
|
|
2463
|
+
200: {
|
|
2464
|
+
description: "Session info",
|
|
2465
|
+
content: {
|
|
2466
|
+
"application/json": {
|
|
2467
|
+
schema: resolver7(Pty.Info)
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2470
|
+
},
|
|
2471
|
+
...errors(404)
|
|
2472
|
+
}
|
|
2473
|
+
}),
|
|
2474
|
+
validator6("param", z10.object({ ptyID: PtyID.zod })),
|
|
2475
|
+
async (c) => {
|
|
2476
|
+
const info = await Pty.get(c.req.valid("param").ptyID);
|
|
2477
|
+
if (!info) {
|
|
2478
|
+
throw new NotFoundError({ message: "Session not found" });
|
|
2479
|
+
}
|
|
2480
|
+
return c.json(info);
|
|
2481
|
+
}
|
|
2482
|
+
).put(
|
|
2483
|
+
"/:ptyID",
|
|
2484
|
+
describeRoute6({
|
|
2485
|
+
summary: "Update PTY session",
|
|
2486
|
+
description: "Update properties of an existing pseudo-terminal (PTY) session.",
|
|
2487
|
+
operationId: "pty.update",
|
|
2488
|
+
responses: {
|
|
2489
|
+
200: {
|
|
2490
|
+
description: "Updated session",
|
|
2491
|
+
content: {
|
|
2492
|
+
"application/json": {
|
|
2493
|
+
schema: resolver7(Pty.Info)
|
|
2494
|
+
}
|
|
2495
|
+
}
|
|
2496
|
+
},
|
|
2497
|
+
...errors(400)
|
|
2498
|
+
}
|
|
2499
|
+
}),
|
|
2500
|
+
validator6("param", z10.object({ ptyID: PtyID.zod })),
|
|
2501
|
+
validator6("json", Pty.UpdateInput),
|
|
2502
|
+
async (c) => {
|
|
2503
|
+
const info = await Pty.update(c.req.valid("param").ptyID, c.req.valid("json"));
|
|
2504
|
+
return c.json(info);
|
|
2505
|
+
}
|
|
2506
|
+
).delete(
|
|
2507
|
+
"/:ptyID",
|
|
2508
|
+
describeRoute6({
|
|
2509
|
+
summary: "Remove PTY session",
|
|
2510
|
+
description: "Remove and terminate a specific pseudo-terminal (PTY) session.",
|
|
2511
|
+
operationId: "pty.remove",
|
|
2512
|
+
responses: {
|
|
2513
|
+
200: {
|
|
2514
|
+
description: "Session removed",
|
|
2515
|
+
content: {
|
|
2516
|
+
"application/json": {
|
|
2517
|
+
schema: resolver7(z10.boolean())
|
|
2518
|
+
}
|
|
2519
|
+
}
|
|
2520
|
+
},
|
|
2521
|
+
...errors(404)
|
|
2522
|
+
}
|
|
2523
|
+
}),
|
|
2524
|
+
validator6("param", z10.object({ ptyID: PtyID.zod })),
|
|
2525
|
+
async (c) => {
|
|
2526
|
+
await Pty.remove(c.req.valid("param").ptyID);
|
|
2527
|
+
return c.json(true);
|
|
2528
|
+
}
|
|
2529
|
+
).get(
|
|
2530
|
+
"/:ptyID/connect",
|
|
2531
|
+
describeRoute6({
|
|
2532
|
+
summary: "Connect to PTY session",
|
|
2533
|
+
description: "Establish a WebSocket connection to interact with a pseudo-terminal (PTY) session in real-time.",
|
|
2534
|
+
operationId: "pty.connect",
|
|
2535
|
+
responses: {
|
|
2536
|
+
200: {
|
|
2537
|
+
description: "Connected session",
|
|
2538
|
+
content: {
|
|
2539
|
+
"application/json": {
|
|
2540
|
+
schema: resolver7(z10.boolean())
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2543
|
+
},
|
|
2544
|
+
...errors(404)
|
|
2545
|
+
}
|
|
2546
|
+
}),
|
|
2547
|
+
validator6("param", z10.object({ ptyID: PtyID.zod })),
|
|
2548
|
+
upgradeWebSocket2(async (c) => {
|
|
2549
|
+
const id = PtyID.zod.parse(c.req.param("ptyID"));
|
|
2550
|
+
const cursor = (() => {
|
|
2551
|
+
const value = c.req.query("cursor");
|
|
2552
|
+
if (!value) return;
|
|
2553
|
+
const parsed = Number(value);
|
|
2554
|
+
if (!Number.isSafeInteger(parsed) || parsed < -1) return;
|
|
2555
|
+
return parsed;
|
|
2556
|
+
})();
|
|
2557
|
+
let handler;
|
|
2558
|
+
if (!await Pty.get(id)) throw new Error("Session not found");
|
|
2559
|
+
const isSocket = (value) => {
|
|
2560
|
+
if (!value || typeof value !== "object") return false;
|
|
2561
|
+
if (!("readyState" in value)) return false;
|
|
2562
|
+
if (!("send" in value) || typeof value.send !== "function") return false;
|
|
2563
|
+
if (!("close" in value) || typeof value.close !== "function") return false;
|
|
2564
|
+
return typeof value.readyState === "number";
|
|
2565
|
+
};
|
|
2566
|
+
const pending = [];
|
|
2567
|
+
let ready = false;
|
|
2568
|
+
return {
|
|
2569
|
+
async onOpen(_event, ws) {
|
|
2570
|
+
const socket2 = ws.raw;
|
|
2571
|
+
if (!isSocket(socket2)) {
|
|
2572
|
+
ws.close();
|
|
2573
|
+
return;
|
|
2574
|
+
}
|
|
2575
|
+
handler = await Pty.connect(id, socket2, cursor);
|
|
2576
|
+
ready = true;
|
|
2577
|
+
for (const msg of pending) handler?.onMessage(msg);
|
|
2578
|
+
pending.length = 0;
|
|
2579
|
+
},
|
|
2580
|
+
onMessage(event) {
|
|
2581
|
+
if (typeof event.data !== "string") return;
|
|
2582
|
+
if (!ready) {
|
|
2583
|
+
pending.push(event.data);
|
|
2584
|
+
return;
|
|
2585
|
+
}
|
|
2586
|
+
handler?.onMessage(event.data);
|
|
2587
|
+
},
|
|
2588
|
+
onClose() {
|
|
2589
|
+
handler?.onClose();
|
|
2590
|
+
},
|
|
2591
|
+
onError() {
|
|
2592
|
+
handler?.onClose();
|
|
2593
|
+
}
|
|
2594
|
+
};
|
|
2595
|
+
})
|
|
2596
|
+
);
|
|
2597
|
+
}
|
|
2598
|
+
|
|
2599
|
+
// packages/opencode/src/server/routes/mcp.ts
|
|
2600
|
+
import { Hono as Hono8 } from "hono";
|
|
2601
|
+
import { describeRoute as describeRoute7, validator as validator7, resolver as resolver8 } from "hono-openapi";
|
|
2602
|
+
import z11 from "zod";
|
|
2603
|
+
var McpRoutes = lazy(
|
|
2604
|
+
() => new Hono8().get(
|
|
2605
|
+
"/",
|
|
2606
|
+
describeRoute7({
|
|
2607
|
+
summary: "Get MCP status",
|
|
2608
|
+
description: "Get the status of all Model Context Protocol (MCP) servers.",
|
|
2609
|
+
operationId: "mcp.status",
|
|
2610
|
+
responses: {
|
|
2611
|
+
200: {
|
|
2612
|
+
description: "MCP server status",
|
|
2613
|
+
content: {
|
|
2614
|
+
"application/json": {
|
|
2615
|
+
schema: resolver8(z11.record(z11.string(), MCP.Status))
|
|
2616
|
+
}
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
2619
|
+
}
|
|
2620
|
+
}),
|
|
2621
|
+
async (c) => {
|
|
2622
|
+
return c.json(await MCP.status());
|
|
2623
|
+
}
|
|
2624
|
+
).post(
|
|
2625
|
+
"/",
|
|
2626
|
+
describeRoute7({
|
|
2627
|
+
summary: "Add MCP server",
|
|
2628
|
+
description: "Dynamically add a new Model Context Protocol (MCP) server to the system.",
|
|
2629
|
+
operationId: "mcp.add",
|
|
2630
|
+
responses: {
|
|
2631
|
+
200: {
|
|
2632
|
+
description: "MCP server added successfully",
|
|
2633
|
+
content: {
|
|
2634
|
+
"application/json": {
|
|
2635
|
+
schema: resolver8(z11.record(z11.string(), MCP.Status))
|
|
2636
|
+
}
|
|
2637
|
+
}
|
|
2638
|
+
},
|
|
2639
|
+
...errors(400)
|
|
2640
|
+
}
|
|
2641
|
+
}),
|
|
2642
|
+
validator7(
|
|
2643
|
+
"json",
|
|
2644
|
+
z11.object({
|
|
2645
|
+
name: z11.string(),
|
|
2646
|
+
config: Config.Mcp
|
|
2647
|
+
})
|
|
2648
|
+
),
|
|
2649
|
+
async (c) => {
|
|
2650
|
+
const { name, config } = c.req.valid("json");
|
|
2651
|
+
const result = await MCP.add(name, config);
|
|
2652
|
+
return c.json(result.status);
|
|
2653
|
+
}
|
|
2654
|
+
).post(
|
|
2655
|
+
"/:name/auth",
|
|
2656
|
+
describeRoute7({
|
|
2657
|
+
summary: "Start MCP OAuth",
|
|
2658
|
+
description: "Start OAuth authentication flow for a Model Context Protocol (MCP) server.",
|
|
2659
|
+
operationId: "mcp.auth.start",
|
|
2660
|
+
responses: {
|
|
2661
|
+
200: {
|
|
2662
|
+
description: "OAuth flow started",
|
|
2663
|
+
content: {
|
|
2664
|
+
"application/json": {
|
|
2665
|
+
schema: resolver8(
|
|
2666
|
+
z11.object({
|
|
2667
|
+
authorizationUrl: z11.string().describe("URL to open in browser for authorization")
|
|
2668
|
+
})
|
|
2669
|
+
)
|
|
2670
|
+
}
|
|
2671
|
+
}
|
|
2672
|
+
},
|
|
2673
|
+
...errors(400, 404)
|
|
2674
|
+
}
|
|
2675
|
+
}),
|
|
2676
|
+
async (c) => {
|
|
2677
|
+
const name = c.req.param("name");
|
|
2678
|
+
const supportsOAuth = await MCP.supportsOAuth(name);
|
|
2679
|
+
if (!supportsOAuth) {
|
|
2680
|
+
return c.json({ error: `MCP server ${name} does not support OAuth` }, 400);
|
|
2681
|
+
}
|
|
2682
|
+
const result = await MCP.startAuth(name);
|
|
2683
|
+
return c.json(result);
|
|
2684
|
+
}
|
|
2685
|
+
).post(
|
|
2686
|
+
"/:name/auth/callback",
|
|
2687
|
+
describeRoute7({
|
|
2688
|
+
summary: "Complete MCP OAuth",
|
|
2689
|
+
description: "Complete OAuth authentication for a Model Context Protocol (MCP) server using the authorization code.",
|
|
2690
|
+
operationId: "mcp.auth.callback",
|
|
2691
|
+
responses: {
|
|
2692
|
+
200: {
|
|
2693
|
+
description: "OAuth authentication completed",
|
|
2694
|
+
content: {
|
|
2695
|
+
"application/json": {
|
|
2696
|
+
schema: resolver8(MCP.Status)
|
|
2697
|
+
}
|
|
2698
|
+
}
|
|
2699
|
+
},
|
|
2700
|
+
...errors(400, 404)
|
|
2701
|
+
}
|
|
2702
|
+
}),
|
|
2703
|
+
validator7(
|
|
2704
|
+
"json",
|
|
2705
|
+
z11.object({
|
|
2706
|
+
code: z11.string().describe("Authorization code from OAuth callback")
|
|
2707
|
+
})
|
|
2708
|
+
),
|
|
2709
|
+
async (c) => {
|
|
2710
|
+
const name = c.req.param("name");
|
|
2711
|
+
const { code } = c.req.valid("json");
|
|
2712
|
+
const status = await MCP.finishAuth(name, code);
|
|
2713
|
+
return c.json(status);
|
|
2714
|
+
}
|
|
2715
|
+
).post(
|
|
2716
|
+
"/:name/auth/authenticate",
|
|
2717
|
+
describeRoute7({
|
|
2718
|
+
summary: "Authenticate MCP OAuth",
|
|
2719
|
+
description: "Start OAuth flow and wait for callback (opens browser)",
|
|
2720
|
+
operationId: "mcp.auth.authenticate",
|
|
2721
|
+
responses: {
|
|
2722
|
+
200: {
|
|
2723
|
+
description: "OAuth authentication completed",
|
|
2724
|
+
content: {
|
|
2725
|
+
"application/json": {
|
|
2726
|
+
schema: resolver8(MCP.Status)
|
|
2727
|
+
}
|
|
2728
|
+
}
|
|
2729
|
+
},
|
|
2730
|
+
...errors(400, 404)
|
|
2731
|
+
}
|
|
2732
|
+
}),
|
|
2733
|
+
async (c) => {
|
|
2734
|
+
const name = c.req.param("name");
|
|
2735
|
+
const supportsOAuth = await MCP.supportsOAuth(name);
|
|
2736
|
+
if (!supportsOAuth) {
|
|
2737
|
+
return c.json({ error: `MCP server ${name} does not support OAuth` }, 400);
|
|
2738
|
+
}
|
|
2739
|
+
const status = await MCP.authenticate(name);
|
|
2740
|
+
return c.json(status);
|
|
2741
|
+
}
|
|
2742
|
+
).delete(
|
|
2743
|
+
"/:name/auth",
|
|
2744
|
+
describeRoute7({
|
|
2745
|
+
summary: "Remove MCP OAuth",
|
|
2746
|
+
description: "Remove OAuth credentials for an MCP server",
|
|
2747
|
+
operationId: "mcp.auth.remove",
|
|
2748
|
+
responses: {
|
|
2749
|
+
200: {
|
|
2750
|
+
description: "OAuth credentials removed",
|
|
2751
|
+
content: {
|
|
2752
|
+
"application/json": {
|
|
2753
|
+
schema: resolver8(z11.object({ success: z11.literal(true) }))
|
|
2754
|
+
}
|
|
2755
|
+
}
|
|
2756
|
+
},
|
|
2757
|
+
...errors(404)
|
|
2758
|
+
}
|
|
2759
|
+
}),
|
|
2760
|
+
async (c) => {
|
|
2761
|
+
const name = c.req.param("name");
|
|
2762
|
+
await MCP.removeAuth(name);
|
|
2763
|
+
return c.json({ success: true });
|
|
2764
|
+
}
|
|
2765
|
+
).post(
|
|
2766
|
+
"/:name/connect",
|
|
2767
|
+
describeRoute7({
|
|
2768
|
+
description: "Connect an MCP server",
|
|
2769
|
+
operationId: "mcp.connect",
|
|
2770
|
+
responses: {
|
|
2771
|
+
200: {
|
|
2772
|
+
description: "MCP server connected successfully",
|
|
2773
|
+
content: {
|
|
2774
|
+
"application/json": {
|
|
2775
|
+
schema: resolver8(z11.boolean())
|
|
2776
|
+
}
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
}
|
|
2780
|
+
}),
|
|
2781
|
+
validator7("param", z11.object({ name: z11.string() })),
|
|
2782
|
+
async (c) => {
|
|
2783
|
+
const { name } = c.req.valid("param");
|
|
2784
|
+
await MCP.connect(name);
|
|
2785
|
+
return c.json(true);
|
|
2786
|
+
}
|
|
2787
|
+
).post(
|
|
2788
|
+
"/:name/disconnect",
|
|
2789
|
+
describeRoute7({
|
|
2790
|
+
description: "Disconnect an MCP server",
|
|
2791
|
+
operationId: "mcp.disconnect",
|
|
2792
|
+
responses: {
|
|
2793
|
+
200: {
|
|
2794
|
+
description: "MCP server disconnected successfully",
|
|
2795
|
+
content: {
|
|
2796
|
+
"application/json": {
|
|
2797
|
+
schema: resolver8(z11.boolean())
|
|
2798
|
+
}
|
|
2799
|
+
}
|
|
2800
|
+
}
|
|
2801
|
+
}
|
|
2802
|
+
}),
|
|
2803
|
+
validator7("param", z11.object({ name: z11.string() })),
|
|
2804
|
+
async (c) => {
|
|
2805
|
+
const { name } = c.req.valid("param");
|
|
2806
|
+
await MCP.disconnect(name);
|
|
2807
|
+
return c.json(true);
|
|
2808
|
+
}
|
|
2809
|
+
)
|
|
2810
|
+
);
|
|
2811
|
+
|
|
2812
|
+
// packages/opencode/src/server/routes/file.ts
|
|
2813
|
+
import { Hono as Hono9 } from "hono";
|
|
2814
|
+
import { describeRoute as describeRoute8, validator as validator8, resolver as resolver9 } from "hono-openapi";
|
|
2815
|
+
import z12 from "zod";
|
|
2816
|
+
var FileRoutes = lazy(
|
|
2817
|
+
() => new Hono9().get(
|
|
2818
|
+
"/find",
|
|
2819
|
+
describeRoute8({
|
|
2820
|
+
summary: "Find text",
|
|
2821
|
+
description: "Search for text patterns across files in the project using ripgrep.",
|
|
2822
|
+
operationId: "find.text",
|
|
2823
|
+
responses: {
|
|
2824
|
+
200: {
|
|
2825
|
+
description: "Matches",
|
|
2826
|
+
content: {
|
|
2827
|
+
"application/json": {
|
|
2828
|
+
schema: resolver9(Ripgrep.Match.shape.data.array())
|
|
2829
|
+
}
|
|
2830
|
+
}
|
|
2831
|
+
}
|
|
2832
|
+
}
|
|
2833
|
+
}),
|
|
2834
|
+
validator8(
|
|
2835
|
+
"query",
|
|
2836
|
+
z12.object({
|
|
2837
|
+
pattern: z12.string()
|
|
2838
|
+
})
|
|
2839
|
+
),
|
|
2840
|
+
async (c) => {
|
|
2841
|
+
const pattern = c.req.valid("query").pattern;
|
|
2842
|
+
const result = await Ripgrep.search({
|
|
2843
|
+
cwd: Instance.directory,
|
|
2844
|
+
pattern,
|
|
2845
|
+
limit: 10
|
|
2846
|
+
});
|
|
2847
|
+
return c.json(result);
|
|
2848
|
+
}
|
|
2849
|
+
).get(
|
|
2850
|
+
"/find/file",
|
|
2851
|
+
describeRoute8({
|
|
2852
|
+
summary: "Find files",
|
|
2853
|
+
description: "Search for files or directories by name or pattern in the project directory.",
|
|
2854
|
+
operationId: "find.files",
|
|
2855
|
+
responses: {
|
|
2856
|
+
200: {
|
|
2857
|
+
description: "File paths",
|
|
2858
|
+
content: {
|
|
2859
|
+
"application/json": {
|
|
2860
|
+
schema: resolver9(z12.string().array())
|
|
2861
|
+
}
|
|
2862
|
+
}
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
2865
|
+
}),
|
|
2866
|
+
validator8(
|
|
2867
|
+
"query",
|
|
2868
|
+
z12.object({
|
|
2869
|
+
query: z12.string(),
|
|
2870
|
+
dirs: z12.enum(["true", "false"]).optional(),
|
|
2871
|
+
type: z12.enum(["file", "directory"]).optional(),
|
|
2872
|
+
limit: z12.coerce.number().int().min(1).max(200).optional()
|
|
2873
|
+
})
|
|
2874
|
+
),
|
|
2875
|
+
async (c) => {
|
|
2876
|
+
const query = c.req.valid("query").query;
|
|
2877
|
+
const dirs = c.req.valid("query").dirs;
|
|
2878
|
+
const type = c.req.valid("query").type;
|
|
2879
|
+
const limit = c.req.valid("query").limit;
|
|
2880
|
+
const results = await File.search({
|
|
2881
|
+
query,
|
|
2882
|
+
limit: limit ?? 10,
|
|
2883
|
+
dirs: dirs !== "false",
|
|
2884
|
+
type
|
|
2885
|
+
});
|
|
2886
|
+
return c.json(results);
|
|
2887
|
+
}
|
|
2888
|
+
).get(
|
|
2889
|
+
"/find/symbol",
|
|
2890
|
+
describeRoute8({
|
|
2891
|
+
summary: "Find symbols",
|
|
2892
|
+
description: "Search for workspace symbols like functions, classes, and variables using LSP.",
|
|
2893
|
+
operationId: "find.symbols",
|
|
2894
|
+
responses: {
|
|
2895
|
+
200: {
|
|
2896
|
+
description: "Symbols",
|
|
2897
|
+
content: {
|
|
2898
|
+
"application/json": {
|
|
2899
|
+
schema: resolver9(LSP.Symbol.array())
|
|
2900
|
+
}
|
|
2901
|
+
}
|
|
2902
|
+
}
|
|
2903
|
+
}
|
|
2904
|
+
}),
|
|
2905
|
+
validator8(
|
|
2906
|
+
"query",
|
|
2907
|
+
z12.object({
|
|
2908
|
+
query: z12.string()
|
|
2909
|
+
})
|
|
2910
|
+
),
|
|
2911
|
+
async (c) => {
|
|
2912
|
+
return c.json([]);
|
|
2913
|
+
}
|
|
2914
|
+
).get(
|
|
2915
|
+
"/file",
|
|
2916
|
+
describeRoute8({
|
|
2917
|
+
summary: "List files",
|
|
2918
|
+
description: "List files and directories in a specified path.",
|
|
2919
|
+
operationId: "file.list",
|
|
2920
|
+
responses: {
|
|
2921
|
+
200: {
|
|
2922
|
+
description: "Files and directories",
|
|
2923
|
+
content: {
|
|
2924
|
+
"application/json": {
|
|
2925
|
+
schema: resolver9(File.Node.array())
|
|
2926
|
+
}
|
|
2927
|
+
}
|
|
2928
|
+
}
|
|
2929
|
+
}
|
|
2930
|
+
}),
|
|
2931
|
+
validator8(
|
|
2932
|
+
"query",
|
|
2933
|
+
z12.object({
|
|
2934
|
+
path: z12.string()
|
|
2935
|
+
})
|
|
2936
|
+
),
|
|
2937
|
+
async (c) => {
|
|
2938
|
+
const path = c.req.valid("query").path;
|
|
2939
|
+
const content = await File.list(path);
|
|
2940
|
+
return c.json(content);
|
|
2941
|
+
}
|
|
2942
|
+
).get(
|
|
2943
|
+
"/file/content",
|
|
2944
|
+
describeRoute8({
|
|
2945
|
+
summary: "Read file",
|
|
2946
|
+
description: "Read the content of a specified file.",
|
|
2947
|
+
operationId: "file.read",
|
|
2948
|
+
responses: {
|
|
2949
|
+
200: {
|
|
2950
|
+
description: "File content",
|
|
2951
|
+
content: {
|
|
2952
|
+
"application/json": {
|
|
2953
|
+
schema: resolver9(File.Content)
|
|
2954
|
+
}
|
|
2955
|
+
}
|
|
2956
|
+
}
|
|
2957
|
+
}
|
|
2958
|
+
}),
|
|
2959
|
+
validator8(
|
|
2960
|
+
"query",
|
|
2961
|
+
z12.object({
|
|
2962
|
+
path: z12.string()
|
|
2963
|
+
})
|
|
2964
|
+
),
|
|
2965
|
+
async (c) => {
|
|
2966
|
+
const path = c.req.valid("query").path;
|
|
2967
|
+
const content = await File.read(path);
|
|
2968
|
+
return c.json(content);
|
|
2969
|
+
}
|
|
2970
|
+
).get(
|
|
2971
|
+
"/file/status",
|
|
2972
|
+
describeRoute8({
|
|
2973
|
+
summary: "Get file status",
|
|
2974
|
+
description: "Get the git status of all files in the project.",
|
|
2975
|
+
operationId: "file.status",
|
|
2976
|
+
responses: {
|
|
2977
|
+
200: {
|
|
2978
|
+
description: "File status",
|
|
2979
|
+
content: {
|
|
2980
|
+
"application/json": {
|
|
2981
|
+
schema: resolver9(File.Info.array())
|
|
2982
|
+
}
|
|
2983
|
+
}
|
|
2984
|
+
}
|
|
2985
|
+
}
|
|
2986
|
+
}),
|
|
2987
|
+
async (c) => {
|
|
2988
|
+
const content = await File.status();
|
|
2989
|
+
return c.json(content);
|
|
2990
|
+
}
|
|
2991
|
+
)
|
|
2992
|
+
);
|
|
2993
|
+
|
|
2994
|
+
// packages/opencode/src/server/routes/config.ts
|
|
2995
|
+
import { Hono as Hono10 } from "hono";
|
|
2996
|
+
import { describeRoute as describeRoute9, validator as validator9, resolver as resolver10 } from "hono-openapi";
|
|
2997
|
+
import z13 from "zod";
|
|
2998
|
+
import { mapValues } from "remeda";
|
|
2999
|
+
var log2 = Log.create({ service: "server" });
|
|
3000
|
+
var ConfigRoutes = lazy(
|
|
3001
|
+
() => new Hono10().get(
|
|
3002
|
+
"/",
|
|
3003
|
+
describeRoute9({
|
|
3004
|
+
summary: "Get configuration",
|
|
3005
|
+
description: "Retrieve the current OpenCode configuration settings and preferences.",
|
|
3006
|
+
operationId: "config.get",
|
|
3007
|
+
responses: {
|
|
3008
|
+
200: {
|
|
3009
|
+
description: "Get config info",
|
|
3010
|
+
content: {
|
|
3011
|
+
"application/json": {
|
|
3012
|
+
schema: resolver10(Config.Info)
|
|
3013
|
+
}
|
|
3014
|
+
}
|
|
3015
|
+
}
|
|
3016
|
+
}
|
|
3017
|
+
}),
|
|
3018
|
+
async (c) => {
|
|
3019
|
+
return c.json(await Config.get());
|
|
3020
|
+
}
|
|
3021
|
+
).patch(
|
|
3022
|
+
"/",
|
|
3023
|
+
describeRoute9({
|
|
3024
|
+
summary: "Update configuration",
|
|
3025
|
+
description: "Update OpenCode configuration settings and preferences.",
|
|
3026
|
+
operationId: "config.update",
|
|
3027
|
+
responses: {
|
|
3028
|
+
200: {
|
|
3029
|
+
description: "Successfully updated config",
|
|
3030
|
+
content: {
|
|
3031
|
+
"application/json": {
|
|
3032
|
+
schema: resolver10(Config.Info)
|
|
3033
|
+
}
|
|
3034
|
+
}
|
|
3035
|
+
},
|
|
3036
|
+
...errors(400)
|
|
3037
|
+
}
|
|
3038
|
+
}),
|
|
3039
|
+
validator9("json", Config.Info),
|
|
3040
|
+
async (c) => {
|
|
3041
|
+
const config = c.req.valid("json");
|
|
3042
|
+
await Config.update(config);
|
|
3043
|
+
return c.json(config);
|
|
3044
|
+
}
|
|
3045
|
+
).get(
|
|
3046
|
+
"/providers",
|
|
3047
|
+
describeRoute9({
|
|
3048
|
+
summary: "List config providers",
|
|
3049
|
+
description: "Get a list of all configured AI providers and their default models.",
|
|
3050
|
+
operationId: "config.providers",
|
|
3051
|
+
responses: {
|
|
3052
|
+
200: {
|
|
3053
|
+
description: "List of providers",
|
|
3054
|
+
content: {
|
|
3055
|
+
"application/json": {
|
|
3056
|
+
schema: resolver10(
|
|
3057
|
+
z13.object({
|
|
3058
|
+
providers: Provider.Info.array(),
|
|
3059
|
+
default: z13.record(z13.string(), z13.string())
|
|
3060
|
+
})
|
|
3061
|
+
)
|
|
3062
|
+
}
|
|
3063
|
+
}
|
|
3064
|
+
}
|
|
3065
|
+
}
|
|
3066
|
+
}),
|
|
3067
|
+
async (c) => {
|
|
3068
|
+
var _stack = [];
|
|
3069
|
+
try {
|
|
3070
|
+
const _ = __using(_stack, log2.time("providers"));
|
|
3071
|
+
const providers = await Provider.list().then((x) => mapValues(x, (item) => item));
|
|
3072
|
+
return c.json({
|
|
3073
|
+
providers: Object.values(providers),
|
|
3074
|
+
default: mapValues(providers, (item) => Provider.sort(Object.values(item.models))[0].id)
|
|
3075
|
+
});
|
|
3076
|
+
} catch (_2) {
|
|
3077
|
+
var _error = _2, _hasError = true;
|
|
3078
|
+
} finally {
|
|
3079
|
+
__callDispose(_stack, _error, _hasError);
|
|
3080
|
+
}
|
|
3081
|
+
}
|
|
3082
|
+
)
|
|
3083
|
+
);
|
|
3084
|
+
|
|
3085
|
+
// packages/opencode/src/server/routes/experimental.ts
|
|
3086
|
+
import { Hono as Hono12 } from "hono";
|
|
3087
|
+
import { describeRoute as describeRoute11, validator as validator11, resolver as resolver12 } from "hono-openapi";
|
|
3088
|
+
import z16 from "zod";
|
|
3089
|
+
|
|
3090
|
+
// packages/opencode/src/config/console-state.ts
|
|
3091
|
+
import z14 from "zod";
|
|
3092
|
+
var ConsoleState = z14.object({
|
|
3093
|
+
consoleManagedProviders: z14.array(z14.string()),
|
|
3094
|
+
activeOrgName: z14.string().optional(),
|
|
3095
|
+
switchableOrgCount: z14.number().int().nonnegative()
|
|
3096
|
+
});
|
|
3097
|
+
|
|
3098
|
+
// packages/opencode/src/server/routes/experimental.ts
|
|
3099
|
+
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
3100
|
+
|
|
3101
|
+
// packages/opencode/src/server/routes/workspace.ts
|
|
3102
|
+
import { Hono as Hono11 } from "hono";
|
|
3103
|
+
import { describeRoute as describeRoute10, resolver as resolver11, validator as validator10 } from "hono-openapi";
|
|
3104
|
+
import z15 from "zod";
|
|
3105
|
+
var WorkspaceRoutes = lazy(
|
|
3106
|
+
() => new Hono11().post(
|
|
3107
|
+
"/",
|
|
3108
|
+
describeRoute10({
|
|
3109
|
+
summary: "Create workspace",
|
|
3110
|
+
description: "Create a workspace for the current project.",
|
|
3111
|
+
operationId: "experimental.workspace.create",
|
|
3112
|
+
responses: {
|
|
3113
|
+
200: {
|
|
3114
|
+
description: "Workspace created",
|
|
3115
|
+
content: {
|
|
3116
|
+
"application/json": {
|
|
3117
|
+
schema: resolver11(Workspace.Info)
|
|
3118
|
+
}
|
|
3119
|
+
}
|
|
3120
|
+
},
|
|
3121
|
+
...errors(400)
|
|
3122
|
+
}
|
|
3123
|
+
}),
|
|
3124
|
+
validator10(
|
|
3125
|
+
"json",
|
|
3126
|
+
Workspace.create.schema.omit({
|
|
3127
|
+
projectID: true
|
|
3128
|
+
})
|
|
3129
|
+
),
|
|
3130
|
+
async (c) => {
|
|
3131
|
+
const body = c.req.valid("json");
|
|
3132
|
+
const workspace = await Workspace.create({
|
|
3133
|
+
projectID: Instance.project.id,
|
|
3134
|
+
...body
|
|
3135
|
+
});
|
|
3136
|
+
return c.json(workspace);
|
|
3137
|
+
}
|
|
3138
|
+
).get(
|
|
3139
|
+
"/",
|
|
3140
|
+
describeRoute10({
|
|
3141
|
+
summary: "List workspaces",
|
|
3142
|
+
description: "List all workspaces.",
|
|
3143
|
+
operationId: "experimental.workspace.list",
|
|
3144
|
+
responses: {
|
|
3145
|
+
200: {
|
|
3146
|
+
description: "Workspaces",
|
|
3147
|
+
content: {
|
|
3148
|
+
"application/json": {
|
|
3149
|
+
schema: resolver11(z15.array(Workspace.Info))
|
|
3150
|
+
}
|
|
3151
|
+
}
|
|
3152
|
+
}
|
|
3153
|
+
}
|
|
3154
|
+
}),
|
|
3155
|
+
async (c) => {
|
|
3156
|
+
return c.json(Workspace.list(Instance.project));
|
|
3157
|
+
}
|
|
3158
|
+
).delete(
|
|
3159
|
+
"/:id",
|
|
3160
|
+
describeRoute10({
|
|
3161
|
+
summary: "Remove workspace",
|
|
3162
|
+
description: "Remove an existing workspace.",
|
|
3163
|
+
operationId: "experimental.workspace.remove",
|
|
3164
|
+
responses: {
|
|
3165
|
+
200: {
|
|
3166
|
+
description: "Workspace removed",
|
|
3167
|
+
content: {
|
|
3168
|
+
"application/json": {
|
|
3169
|
+
schema: resolver11(Workspace.Info.optional())
|
|
3170
|
+
}
|
|
3171
|
+
}
|
|
3172
|
+
},
|
|
3173
|
+
...errors(400)
|
|
3174
|
+
}
|
|
3175
|
+
}),
|
|
3176
|
+
validator10(
|
|
3177
|
+
"param",
|
|
3178
|
+
z15.object({
|
|
3179
|
+
id: Workspace.Info.shape.id
|
|
3180
|
+
})
|
|
3181
|
+
),
|
|
3182
|
+
async (c) => {
|
|
3183
|
+
const { id } = c.req.valid("param");
|
|
3184
|
+
return c.json(await Workspace.remove(id));
|
|
3185
|
+
}
|
|
3186
|
+
)
|
|
3187
|
+
);
|
|
3188
|
+
|
|
3189
|
+
// packages/opencode/src/server/routes/experimental.ts
|
|
3190
|
+
var ConsoleOrgOption = z16.object({
|
|
3191
|
+
accountID: z16.string(),
|
|
3192
|
+
accountEmail: z16.string(),
|
|
3193
|
+
accountUrl: z16.string(),
|
|
3194
|
+
orgID: z16.string(),
|
|
3195
|
+
orgName: z16.string(),
|
|
3196
|
+
active: z16.boolean()
|
|
3197
|
+
});
|
|
3198
|
+
var ConsoleOrgList = z16.object({
|
|
3199
|
+
orgs: z16.array(ConsoleOrgOption)
|
|
3200
|
+
});
|
|
3201
|
+
var ConsoleSwitchBody = z16.object({
|
|
3202
|
+
accountID: z16.string(),
|
|
3203
|
+
orgID: z16.string()
|
|
3204
|
+
});
|
|
3205
|
+
var ExperimentalRoutes = lazy(
|
|
3206
|
+
() => new Hono12().get(
|
|
3207
|
+
"/console",
|
|
3208
|
+
describeRoute11({
|
|
3209
|
+
summary: "Get active Console provider metadata",
|
|
3210
|
+
description: "Get the active Console org name and the set of provider IDs managed by that Console org.",
|
|
3211
|
+
operationId: "experimental.console.get",
|
|
3212
|
+
responses: {
|
|
3213
|
+
200: {
|
|
3214
|
+
description: "Active Console provider metadata",
|
|
3215
|
+
content: {
|
|
3216
|
+
"application/json": {
|
|
3217
|
+
schema: resolver12(ConsoleState)
|
|
3218
|
+
}
|
|
3219
|
+
}
|
|
3220
|
+
}
|
|
3221
|
+
}
|
|
3222
|
+
}),
|
|
3223
|
+
async (c) => {
|
|
3224
|
+
const [consoleState, groups] = await Promise.all([Config.getConsoleState(), Account.orgsByAccount()]);
|
|
3225
|
+
return c.json({
|
|
3226
|
+
...consoleState,
|
|
3227
|
+
switchableOrgCount: groups.reduce((count, group) => count + group.orgs.length, 0)
|
|
3228
|
+
});
|
|
3229
|
+
}
|
|
3230
|
+
).get(
|
|
3231
|
+
"/console/orgs",
|
|
3232
|
+
describeRoute11({
|
|
3233
|
+
summary: "List switchable Console orgs",
|
|
3234
|
+
description: "Get the available Console orgs across logged-in accounts, including the current active org.",
|
|
3235
|
+
operationId: "experimental.console.listOrgs",
|
|
3236
|
+
responses: {
|
|
3237
|
+
200: {
|
|
3238
|
+
description: "Switchable Console orgs",
|
|
3239
|
+
content: {
|
|
3240
|
+
"application/json": {
|
|
3241
|
+
schema: resolver12(ConsoleOrgList)
|
|
3242
|
+
}
|
|
3243
|
+
}
|
|
3244
|
+
}
|
|
3245
|
+
}
|
|
3246
|
+
}),
|
|
3247
|
+
async (c) => {
|
|
3248
|
+
const [groups, active] = await Promise.all([Account.orgsByAccount(), Account.active()]);
|
|
3249
|
+
const orgs = groups.flatMap(
|
|
3250
|
+
(group) => group.orgs.map((org) => ({
|
|
3251
|
+
accountID: group.account.id,
|
|
3252
|
+
accountEmail: group.account.email,
|
|
3253
|
+
accountUrl: group.account.url,
|
|
3254
|
+
orgID: org.id,
|
|
3255
|
+
orgName: org.name,
|
|
3256
|
+
active: !!active && active.id === group.account.id && active.active_org_id === org.id
|
|
3257
|
+
}))
|
|
3258
|
+
);
|
|
3259
|
+
return c.json({ orgs });
|
|
3260
|
+
}
|
|
3261
|
+
).post(
|
|
3262
|
+
"/console/switch",
|
|
3263
|
+
describeRoute11({
|
|
3264
|
+
summary: "Switch active Console org",
|
|
3265
|
+
description: "Persist a new active Console account/org selection for the current local OpenCode state.",
|
|
3266
|
+
operationId: "experimental.console.switchOrg",
|
|
3267
|
+
responses: {
|
|
3268
|
+
200: {
|
|
3269
|
+
description: "Switch success",
|
|
3270
|
+
content: {
|
|
3271
|
+
"application/json": {
|
|
3272
|
+
schema: resolver12(z16.boolean())
|
|
3273
|
+
}
|
|
3274
|
+
}
|
|
3275
|
+
}
|
|
3276
|
+
}
|
|
3277
|
+
}),
|
|
3278
|
+
validator11("json", ConsoleSwitchBody),
|
|
3279
|
+
async (c) => {
|
|
3280
|
+
const body = c.req.valid("json");
|
|
3281
|
+
await Account.switchOrg(AccountID.make(body.accountID), OrgID.make(body.orgID));
|
|
3282
|
+
return c.json(true);
|
|
3283
|
+
}
|
|
3284
|
+
).get(
|
|
3285
|
+
"/tool/ids",
|
|
3286
|
+
describeRoute11({
|
|
3287
|
+
summary: "List tool IDs",
|
|
3288
|
+
description: "Get a list of all available tool IDs, including both built-in tools and dynamically registered tools.",
|
|
3289
|
+
operationId: "tool.ids",
|
|
3290
|
+
responses: {
|
|
3291
|
+
200: {
|
|
3292
|
+
description: "Tool IDs",
|
|
3293
|
+
content: {
|
|
3294
|
+
"application/json": {
|
|
3295
|
+
schema: resolver12(z16.array(z16.string()).meta({ ref: "ToolIDs" }))
|
|
3296
|
+
}
|
|
3297
|
+
}
|
|
3298
|
+
},
|
|
3299
|
+
...errors(400)
|
|
3300
|
+
}
|
|
3301
|
+
}),
|
|
3302
|
+
async (c) => {
|
|
3303
|
+
return c.json(await ToolRegistry.ids());
|
|
3304
|
+
}
|
|
3305
|
+
).get(
|
|
3306
|
+
"/tool",
|
|
3307
|
+
describeRoute11({
|
|
3308
|
+
summary: "List tools",
|
|
3309
|
+
description: "Get a list of available tools with their JSON schema parameters for a specific provider and model combination.",
|
|
3310
|
+
operationId: "tool.list",
|
|
3311
|
+
responses: {
|
|
3312
|
+
200: {
|
|
3313
|
+
description: "Tools",
|
|
3314
|
+
content: {
|
|
3315
|
+
"application/json": {
|
|
3316
|
+
schema: resolver12(
|
|
3317
|
+
z16.array(
|
|
3318
|
+
z16.object({
|
|
3319
|
+
id: z16.string(),
|
|
3320
|
+
description: z16.string(),
|
|
3321
|
+
parameters: z16.any()
|
|
3322
|
+
}).meta({ ref: "ToolListItem" })
|
|
3323
|
+
).meta({ ref: "ToolList" })
|
|
3324
|
+
)
|
|
3325
|
+
}
|
|
3326
|
+
}
|
|
3327
|
+
},
|
|
3328
|
+
...errors(400)
|
|
3329
|
+
}
|
|
3330
|
+
}),
|
|
3331
|
+
validator11(
|
|
3332
|
+
"query",
|
|
3333
|
+
z16.object({
|
|
3334
|
+
provider: z16.string(),
|
|
3335
|
+
model: z16.string()
|
|
3336
|
+
})
|
|
3337
|
+
),
|
|
3338
|
+
async (c) => {
|
|
3339
|
+
const { provider, model } = c.req.valid("query");
|
|
3340
|
+
const tools = await ToolRegistry.tools({ providerID: ProviderID.make(provider), modelID: ModelID.make(model) });
|
|
3341
|
+
return c.json(
|
|
3342
|
+
tools.map((t) => ({
|
|
3343
|
+
id: t.id,
|
|
3344
|
+
description: t.description,
|
|
3345
|
+
// Handle both Zod schemas and plain JSON schemas
|
|
3346
|
+
parameters: t.parameters?._def ? zodToJsonSchema(t.parameters) : t.parameters
|
|
3347
|
+
}))
|
|
3348
|
+
);
|
|
3349
|
+
}
|
|
3350
|
+
).route("/workspace", WorkspaceRoutes()).post(
|
|
3351
|
+
"/worktree",
|
|
3352
|
+
describeRoute11({
|
|
3353
|
+
summary: "Create worktree",
|
|
3354
|
+
description: "Create a new git worktree for the current project and run any configured startup scripts.",
|
|
3355
|
+
operationId: "worktree.create",
|
|
3356
|
+
responses: {
|
|
3357
|
+
200: {
|
|
3358
|
+
description: "Worktree created",
|
|
3359
|
+
content: {
|
|
3360
|
+
"application/json": {
|
|
3361
|
+
schema: resolver12(Worktree.Info)
|
|
3362
|
+
}
|
|
3363
|
+
}
|
|
3364
|
+
},
|
|
3365
|
+
...errors(400)
|
|
3366
|
+
}
|
|
3367
|
+
}),
|
|
3368
|
+
validator11("json", Worktree.CreateInput.optional()),
|
|
3369
|
+
async (c) => {
|
|
3370
|
+
const body = c.req.valid("json");
|
|
3371
|
+
const worktree = await Worktree.create(body);
|
|
3372
|
+
return c.json(worktree);
|
|
3373
|
+
}
|
|
3374
|
+
).get(
|
|
3375
|
+
"/worktree",
|
|
3376
|
+
describeRoute11({
|
|
3377
|
+
summary: "List worktrees",
|
|
3378
|
+
description: "List all sandbox worktrees for the current project.",
|
|
3379
|
+
operationId: "worktree.list",
|
|
3380
|
+
responses: {
|
|
3381
|
+
200: {
|
|
3382
|
+
description: "List of worktree directories",
|
|
3383
|
+
content: {
|
|
3384
|
+
"application/json": {
|
|
3385
|
+
schema: resolver12(z16.array(z16.string()))
|
|
3386
|
+
}
|
|
3387
|
+
}
|
|
3388
|
+
}
|
|
3389
|
+
}
|
|
3390
|
+
}),
|
|
3391
|
+
async (c) => {
|
|
3392
|
+
const sandboxes = await Project.sandboxes(Instance.project.id);
|
|
3393
|
+
return c.json(sandboxes);
|
|
3394
|
+
}
|
|
3395
|
+
).delete(
|
|
3396
|
+
"/worktree",
|
|
3397
|
+
describeRoute11({
|
|
3398
|
+
summary: "Remove worktree",
|
|
3399
|
+
description: "Remove a git worktree and delete its branch.",
|
|
3400
|
+
operationId: "worktree.remove",
|
|
3401
|
+
responses: {
|
|
3402
|
+
200: {
|
|
3403
|
+
description: "Worktree removed",
|
|
3404
|
+
content: {
|
|
3405
|
+
"application/json": {
|
|
3406
|
+
schema: resolver12(z16.boolean())
|
|
3407
|
+
}
|
|
3408
|
+
}
|
|
3409
|
+
},
|
|
3410
|
+
...errors(400)
|
|
3411
|
+
}
|
|
3412
|
+
}),
|
|
3413
|
+
validator11("json", Worktree.RemoveInput),
|
|
3414
|
+
async (c) => {
|
|
3415
|
+
const body = c.req.valid("json");
|
|
3416
|
+
await Worktree.remove(body);
|
|
3417
|
+
await Project.removeSandbox(Instance.project.id, body.directory);
|
|
3418
|
+
return c.json(true);
|
|
3419
|
+
}
|
|
3420
|
+
).post(
|
|
3421
|
+
"/worktree/reset",
|
|
3422
|
+
describeRoute11({
|
|
3423
|
+
summary: "Reset worktree",
|
|
3424
|
+
description: "Reset a worktree branch to the primary default branch.",
|
|
3425
|
+
operationId: "worktree.reset",
|
|
3426
|
+
responses: {
|
|
3427
|
+
200: {
|
|
3428
|
+
description: "Worktree reset",
|
|
3429
|
+
content: {
|
|
3430
|
+
"application/json": {
|
|
3431
|
+
schema: resolver12(z16.boolean())
|
|
3432
|
+
}
|
|
3433
|
+
}
|
|
3434
|
+
},
|
|
3435
|
+
...errors(400)
|
|
3436
|
+
}
|
|
3437
|
+
}),
|
|
3438
|
+
validator11("json", Worktree.ResetInput),
|
|
3439
|
+
async (c) => {
|
|
3440
|
+
const body = c.req.valid("json");
|
|
3441
|
+
await Worktree.reset(body);
|
|
3442
|
+
return c.json(true);
|
|
3443
|
+
}
|
|
3444
|
+
).get(
|
|
3445
|
+
"/session",
|
|
3446
|
+
describeRoute11({
|
|
3447
|
+
summary: "List sessions",
|
|
3448
|
+
description: "Get a list of all OpenCode sessions across projects, sorted by most recently updated. Archived sessions are excluded by default.",
|
|
3449
|
+
operationId: "experimental.session.list",
|
|
3450
|
+
responses: {
|
|
3451
|
+
200: {
|
|
3452
|
+
description: "List of sessions",
|
|
3453
|
+
content: {
|
|
3454
|
+
"application/json": {
|
|
3455
|
+
schema: resolver12(Session.GlobalInfo.array())
|
|
3456
|
+
}
|
|
3457
|
+
}
|
|
3458
|
+
}
|
|
3459
|
+
}
|
|
3460
|
+
}),
|
|
3461
|
+
validator11(
|
|
3462
|
+
"query",
|
|
3463
|
+
z16.object({
|
|
3464
|
+
directory: z16.string().optional().meta({ description: "Filter sessions by project directory" }),
|
|
3465
|
+
roots: z16.coerce.boolean().optional().meta({ description: "Only return root sessions (no parentID)" }),
|
|
3466
|
+
start: z16.coerce.number().optional().meta({ description: "Filter sessions updated on or after this timestamp (milliseconds since epoch)" }),
|
|
3467
|
+
cursor: z16.coerce.number().optional().meta({ description: "Return sessions updated before this timestamp (milliseconds since epoch)" }),
|
|
3468
|
+
search: z16.string().optional().meta({ description: "Filter sessions by title (case-insensitive)" }),
|
|
3469
|
+
limit: z16.coerce.number().optional().meta({ description: "Maximum number of sessions to return" }),
|
|
3470
|
+
archived: z16.coerce.boolean().optional().meta({ description: "Include archived sessions (default false)" })
|
|
3471
|
+
})
|
|
3472
|
+
),
|
|
3473
|
+
async (c) => {
|
|
3474
|
+
const query = c.req.valid("query");
|
|
3475
|
+
const limit = query.limit ?? 100;
|
|
3476
|
+
const sessions = [];
|
|
3477
|
+
for await (const session of Session.listGlobal({
|
|
3478
|
+
directory: query.directory,
|
|
3479
|
+
roots: query.roots,
|
|
3480
|
+
start: query.start,
|
|
3481
|
+
cursor: query.cursor,
|
|
3482
|
+
search: query.search,
|
|
3483
|
+
limit: limit + 1,
|
|
3484
|
+
archived: query.archived
|
|
3485
|
+
})) {
|
|
3486
|
+
sessions.push(session);
|
|
3487
|
+
}
|
|
3488
|
+
const hasMore = sessions.length > limit;
|
|
3489
|
+
const list = hasMore ? sessions.slice(0, limit) : sessions;
|
|
3490
|
+
if (hasMore && list.length > 0) {
|
|
3491
|
+
c.header("x-next-cursor", String(list[list.length - 1].time.updated));
|
|
3492
|
+
}
|
|
3493
|
+
return c.json(list);
|
|
3494
|
+
}
|
|
3495
|
+
).get(
|
|
3496
|
+
"/resource",
|
|
3497
|
+
describeRoute11({
|
|
3498
|
+
summary: "Get MCP resources",
|
|
3499
|
+
description: "Get all available MCP resources from connected servers. Optionally filter by name.",
|
|
3500
|
+
operationId: "experimental.resource.list",
|
|
3501
|
+
responses: {
|
|
3502
|
+
200: {
|
|
3503
|
+
description: "MCP resources",
|
|
3504
|
+
content: {
|
|
3505
|
+
"application/json": {
|
|
3506
|
+
schema: resolver12(z16.record(z16.string(), MCP.Resource))
|
|
3507
|
+
}
|
|
3508
|
+
}
|
|
3509
|
+
}
|
|
3510
|
+
}
|
|
3511
|
+
}),
|
|
3512
|
+
async (c) => {
|
|
3513
|
+
return c.json(await MCP.resources());
|
|
3514
|
+
}
|
|
3515
|
+
)
|
|
3516
|
+
);
|
|
3517
|
+
|
|
3518
|
+
// packages/opencode/src/server/routes/provider.ts
|
|
3519
|
+
import { Hono as Hono13 } from "hono";
|
|
3520
|
+
import { describeRoute as describeRoute12, validator as validator12, resolver as resolver13 } from "hono-openapi";
|
|
3521
|
+
import z18 from "zod";
|
|
3522
|
+
|
|
3523
|
+
// packages/opencode/src/provider/auth.ts
|
|
3524
|
+
import { Array as Arr, Effect as Effect2, Layer as Layer2, Record, Result, ServiceMap as ServiceMap2 } from "effect";
|
|
3525
|
+
import z17 from "zod";
|
|
3526
|
+
var ProviderAuth;
|
|
3527
|
+
((ProviderAuth2) => {
|
|
3528
|
+
ProviderAuth2.Method = z17.object({
|
|
3529
|
+
type: z17.union([z17.literal("oauth"), z17.literal("api")]),
|
|
3530
|
+
label: z17.string(),
|
|
3531
|
+
prompts: z17.array(
|
|
3532
|
+
z17.union([
|
|
3533
|
+
z17.object({
|
|
3534
|
+
type: z17.literal("text"),
|
|
3535
|
+
key: z17.string(),
|
|
3536
|
+
message: z17.string(),
|
|
3537
|
+
placeholder: z17.string().optional(),
|
|
3538
|
+
when: z17.object({
|
|
3539
|
+
key: z17.string(),
|
|
3540
|
+
op: z17.union([z17.literal("eq"), z17.literal("neq")]),
|
|
3541
|
+
value: z17.string()
|
|
3542
|
+
}).optional()
|
|
3543
|
+
}),
|
|
3544
|
+
z17.object({
|
|
3545
|
+
type: z17.literal("select"),
|
|
3546
|
+
key: z17.string(),
|
|
3547
|
+
message: z17.string(),
|
|
3548
|
+
options: z17.array(
|
|
3549
|
+
z17.object({
|
|
3550
|
+
label: z17.string(),
|
|
3551
|
+
value: z17.string(),
|
|
3552
|
+
hint: z17.string().optional()
|
|
3553
|
+
})
|
|
3554
|
+
),
|
|
3555
|
+
when: z17.object({
|
|
3556
|
+
key: z17.string(),
|
|
3557
|
+
op: z17.union([z17.literal("eq"), z17.literal("neq")]),
|
|
3558
|
+
value: z17.string()
|
|
3559
|
+
}).optional()
|
|
3560
|
+
})
|
|
3561
|
+
])
|
|
3562
|
+
).optional()
|
|
3563
|
+
}).meta({
|
|
3564
|
+
ref: "ProviderAuthMethod"
|
|
3565
|
+
});
|
|
3566
|
+
ProviderAuth2.Authorization = z17.object({
|
|
3567
|
+
url: z17.string(),
|
|
3568
|
+
method: z17.union([z17.literal("auto"), z17.literal("code")]),
|
|
3569
|
+
instructions: z17.string()
|
|
3570
|
+
}).meta({
|
|
3571
|
+
ref: "ProviderAuthAuthorization"
|
|
3572
|
+
});
|
|
3573
|
+
ProviderAuth2.OauthMissing = NamedError.create("ProviderAuthOauthMissing", z17.object({ providerID: ProviderID.zod }));
|
|
3574
|
+
ProviderAuth2.OauthCodeMissing = NamedError.create(
|
|
3575
|
+
"ProviderAuthOauthCodeMissing",
|
|
3576
|
+
z17.object({ providerID: ProviderID.zod })
|
|
3577
|
+
);
|
|
3578
|
+
ProviderAuth2.OauthCallbackFailed = NamedError.create("ProviderAuthOauthCallbackFailed", z17.object({}));
|
|
3579
|
+
ProviderAuth2.ValidationFailed = NamedError.create(
|
|
3580
|
+
"ProviderAuthValidationFailed",
|
|
3581
|
+
z17.object({
|
|
3582
|
+
field: z17.string(),
|
|
3583
|
+
message: z17.string()
|
|
3584
|
+
})
|
|
3585
|
+
);
|
|
3586
|
+
class Service extends ServiceMap2.Service()("@opencode/ProviderAuth") {
|
|
3587
|
+
}
|
|
3588
|
+
ProviderAuth2.Service = Service;
|
|
3589
|
+
ProviderAuth2.layer = Layer2.effect(
|
|
3590
|
+
Service,
|
|
3591
|
+
Effect2.gen(function* () {
|
|
3592
|
+
const auth = yield* Auth.Service;
|
|
3593
|
+
const plugin = yield* Plugin.Service;
|
|
3594
|
+
const state = yield* InstanceState.make(
|
|
3595
|
+
Effect2.fn("ProviderAuth.state")(function* () {
|
|
3596
|
+
const plugins = yield* plugin.list();
|
|
3597
|
+
return {
|
|
3598
|
+
hooks: Record.fromEntries(
|
|
3599
|
+
Arr.filterMap(
|
|
3600
|
+
plugins,
|
|
3601
|
+
(x) => x.auth?.provider !== void 0 ? Result.succeed([ProviderID.make(x.auth.provider), x.auth]) : Result.failVoid
|
|
3602
|
+
)
|
|
3603
|
+
),
|
|
3604
|
+
pending: /* @__PURE__ */ new Map()
|
|
3605
|
+
};
|
|
3606
|
+
})
|
|
3607
|
+
);
|
|
3608
|
+
const methods2 = Effect2.fn("ProviderAuth.methods")(function* () {
|
|
3609
|
+
const hooks = (yield* InstanceState.get(state)).hooks;
|
|
3610
|
+
return Record.map(
|
|
3611
|
+
hooks,
|
|
3612
|
+
(item) => item.methods.map(
|
|
3613
|
+
(method) => ({
|
|
3614
|
+
type: method.type,
|
|
3615
|
+
label: method.label,
|
|
3616
|
+
prompts: method.prompts?.map((prompt) => {
|
|
3617
|
+
if (prompt.type === "select") {
|
|
3618
|
+
return {
|
|
3619
|
+
type: "select",
|
|
3620
|
+
key: prompt.key,
|
|
3621
|
+
message: prompt.message,
|
|
3622
|
+
options: prompt.options,
|
|
3623
|
+
when: prompt.when
|
|
3624
|
+
};
|
|
3625
|
+
}
|
|
3626
|
+
return {
|
|
3627
|
+
type: "text",
|
|
3628
|
+
key: prompt.key,
|
|
3629
|
+
message: prompt.message,
|
|
3630
|
+
placeholder: prompt.placeholder,
|
|
3631
|
+
when: prompt.when
|
|
3632
|
+
};
|
|
3633
|
+
})
|
|
3634
|
+
})
|
|
3635
|
+
)
|
|
3636
|
+
);
|
|
3637
|
+
});
|
|
3638
|
+
const authorize2 = Effect2.fn("ProviderAuth.authorize")(function* (input) {
|
|
3639
|
+
const { hooks, pending } = yield* InstanceState.get(state);
|
|
3640
|
+
const method = hooks[input.providerID].methods[input.method];
|
|
3641
|
+
if (method.type !== "oauth") return;
|
|
3642
|
+
if (method.prompts && input.inputs) {
|
|
3643
|
+
for (const prompt of method.prompts) {
|
|
3644
|
+
if (prompt.type === "text" && prompt.validate && input.inputs[prompt.key] !== void 0) {
|
|
3645
|
+
const error = prompt.validate(input.inputs[prompt.key]);
|
|
3646
|
+
if (error) return yield* Effect2.fail(new ProviderAuth2.ValidationFailed({ field: prompt.key, message: error }));
|
|
3647
|
+
}
|
|
3648
|
+
}
|
|
3649
|
+
}
|
|
3650
|
+
const result = yield* Effect2.promise(() => method.authorize(input.inputs));
|
|
3651
|
+
pending.set(input.providerID, result);
|
|
3652
|
+
return {
|
|
3653
|
+
url: result.url,
|
|
3654
|
+
method: result.method,
|
|
3655
|
+
instructions: result.instructions
|
|
3656
|
+
};
|
|
3657
|
+
});
|
|
3658
|
+
const callback2 = Effect2.fn("ProviderAuth.callback")(function* (input) {
|
|
3659
|
+
const pending = (yield* InstanceState.get(state)).pending;
|
|
3660
|
+
const match = pending.get(input.providerID);
|
|
3661
|
+
if (!match) return yield* Effect2.fail(new ProviderAuth2.OauthMissing({ providerID: input.providerID }));
|
|
3662
|
+
if (match.method === "code" && !input.code) {
|
|
3663
|
+
return yield* Effect2.fail(new ProviderAuth2.OauthCodeMissing({ providerID: input.providerID }));
|
|
3664
|
+
}
|
|
3665
|
+
const result = yield* Effect2.promise(
|
|
3666
|
+
() => match.method === "code" ? match.callback(input.code) : match.callback()
|
|
3667
|
+
);
|
|
3668
|
+
if (!result || result.type !== "success") return yield* Effect2.fail(new ProviderAuth2.OauthCallbackFailed({}));
|
|
3669
|
+
if ("key" in result) {
|
|
3670
|
+
yield* auth.set(input.providerID, {
|
|
3671
|
+
type: "api",
|
|
3672
|
+
key: result.key
|
|
3673
|
+
});
|
|
3674
|
+
}
|
|
3675
|
+
if ("refresh" in result) {
|
|
3676
|
+
const { type: _, provider: __, refresh, access, expires, ...extra } = result;
|
|
3677
|
+
yield* auth.set(input.providerID, {
|
|
3678
|
+
type: "oauth",
|
|
3679
|
+
access,
|
|
3680
|
+
refresh,
|
|
3681
|
+
expires,
|
|
3682
|
+
...extra
|
|
3683
|
+
});
|
|
3684
|
+
}
|
|
3685
|
+
});
|
|
3686
|
+
return Service.of({ methods: methods2, authorize: authorize2, callback: callback2 });
|
|
3687
|
+
})
|
|
3688
|
+
);
|
|
3689
|
+
ProviderAuth2.defaultLayer = Layer2.suspend(
|
|
3690
|
+
() => ProviderAuth2.layer.pipe(Layer2.provide(Auth.defaultLayer), Layer2.provide(Plugin.defaultLayer))
|
|
3691
|
+
);
|
|
3692
|
+
const { runPromise } = makeRuntime(Service, ProviderAuth2.defaultLayer);
|
|
3693
|
+
async function methods() {
|
|
3694
|
+
return runPromise((svc) => svc.methods());
|
|
3695
|
+
}
|
|
3696
|
+
ProviderAuth2.methods = methods;
|
|
3697
|
+
async function authorize(input) {
|
|
3698
|
+
return runPromise((svc) => svc.authorize(input));
|
|
3699
|
+
}
|
|
3700
|
+
ProviderAuth2.authorize = authorize;
|
|
3701
|
+
async function callback(input) {
|
|
3702
|
+
return runPromise((svc) => svc.callback(input));
|
|
3703
|
+
}
|
|
3704
|
+
ProviderAuth2.callback = callback;
|
|
3705
|
+
})(ProviderAuth || (ProviderAuth = {}));
|
|
3706
|
+
|
|
3707
|
+
// packages/opencode/src/server/routes/provider.ts
|
|
3708
|
+
import { mapValues as mapValues2 } from "remeda";
|
|
3709
|
+
var log3 = Log.create({ service: "server" });
|
|
3710
|
+
var ProviderRoutes = lazy(
|
|
3711
|
+
() => new Hono13().get(
|
|
3712
|
+
"/",
|
|
3713
|
+
describeRoute12({
|
|
3714
|
+
summary: "List providers",
|
|
3715
|
+
description: "Get a list of all available AI providers, including both available and connected ones.",
|
|
3716
|
+
operationId: "provider.list",
|
|
3717
|
+
responses: {
|
|
3718
|
+
200: {
|
|
3719
|
+
description: "List of providers",
|
|
3720
|
+
content: {
|
|
3721
|
+
"application/json": {
|
|
3722
|
+
schema: resolver13(
|
|
3723
|
+
z18.object({
|
|
3724
|
+
all: ModelsDev.Provider.array(),
|
|
3725
|
+
default: z18.record(z18.string(), z18.string()),
|
|
3726
|
+
connected: z18.array(z18.string())
|
|
3727
|
+
})
|
|
3728
|
+
)
|
|
3729
|
+
}
|
|
3730
|
+
}
|
|
3731
|
+
}
|
|
3732
|
+
}
|
|
3733
|
+
}),
|
|
3734
|
+
async (c) => {
|
|
3735
|
+
const config = await Config.get();
|
|
3736
|
+
const disabled = new Set(config.disabled_providers ?? []);
|
|
3737
|
+
const enabled = config.enabled_providers ? new Set(config.enabled_providers) : void 0;
|
|
3738
|
+
const allProviders = await ModelsDev.get();
|
|
3739
|
+
const filteredProviders = {};
|
|
3740
|
+
for (const [key, value] of Object.entries(allProviders)) {
|
|
3741
|
+
if ((enabled ? enabled.has(key) : true) && !disabled.has(key)) {
|
|
3742
|
+
filteredProviders[key] = value;
|
|
3743
|
+
}
|
|
3744
|
+
}
|
|
3745
|
+
const connected = await Provider.list();
|
|
3746
|
+
const providers = Object.assign(
|
|
3747
|
+
mapValues2(filteredProviders, (x) => Provider.fromModelsDevProvider(x)),
|
|
3748
|
+
connected
|
|
3749
|
+
);
|
|
3750
|
+
return c.json({
|
|
3751
|
+
all: Object.values(providers),
|
|
3752
|
+
default: mapValues2(providers, (item) => Provider.sort(Object.values(item.models))[0].id),
|
|
3753
|
+
connected: Object.keys(connected)
|
|
3754
|
+
});
|
|
3755
|
+
}
|
|
3756
|
+
).get(
|
|
3757
|
+
"/auth",
|
|
3758
|
+
describeRoute12({
|
|
3759
|
+
summary: "Get provider auth methods",
|
|
3760
|
+
description: "Retrieve available authentication methods for all AI providers.",
|
|
3761
|
+
operationId: "provider.auth",
|
|
3762
|
+
responses: {
|
|
3763
|
+
200: {
|
|
3764
|
+
description: "Provider auth methods",
|
|
3765
|
+
content: {
|
|
3766
|
+
"application/json": {
|
|
3767
|
+
schema: resolver13(z18.record(z18.string(), z18.array(ProviderAuth.Method)))
|
|
3768
|
+
}
|
|
3769
|
+
}
|
|
3770
|
+
}
|
|
3771
|
+
}
|
|
3772
|
+
}),
|
|
3773
|
+
async (c) => {
|
|
3774
|
+
return c.json(await ProviderAuth.methods());
|
|
3775
|
+
}
|
|
3776
|
+
).post(
|
|
3777
|
+
"/:providerID/oauth/authorize",
|
|
3778
|
+
describeRoute12({
|
|
3779
|
+
summary: "OAuth authorize",
|
|
3780
|
+
description: "Initiate OAuth authorization for a specific AI provider to get an authorization URL.",
|
|
3781
|
+
operationId: "provider.oauth.authorize",
|
|
3782
|
+
responses: {
|
|
3783
|
+
200: {
|
|
3784
|
+
description: "Authorization URL and method",
|
|
3785
|
+
content: {
|
|
3786
|
+
"application/json": {
|
|
3787
|
+
schema: resolver13(ProviderAuth.Authorization.optional())
|
|
3788
|
+
}
|
|
3789
|
+
}
|
|
3790
|
+
},
|
|
3791
|
+
...errors(400)
|
|
3792
|
+
}
|
|
3793
|
+
}),
|
|
3794
|
+
validator12(
|
|
3795
|
+
"param",
|
|
3796
|
+
z18.object({
|
|
3797
|
+
providerID: ProviderID.zod.meta({ description: "Provider ID" })
|
|
3798
|
+
})
|
|
3799
|
+
),
|
|
3800
|
+
validator12(
|
|
3801
|
+
"json",
|
|
3802
|
+
z18.object({
|
|
3803
|
+
method: z18.number().meta({ description: "Auth method index" }),
|
|
3804
|
+
inputs: z18.record(z18.string(), z18.string()).optional().meta({ description: "Prompt inputs" })
|
|
3805
|
+
})
|
|
3806
|
+
),
|
|
3807
|
+
async (c) => {
|
|
3808
|
+
const providerID = c.req.valid("param").providerID;
|
|
3809
|
+
const { method, inputs } = c.req.valid("json");
|
|
3810
|
+
const result = await ProviderAuth.authorize({
|
|
3811
|
+
providerID,
|
|
3812
|
+
method,
|
|
3813
|
+
inputs
|
|
3814
|
+
});
|
|
3815
|
+
return c.json(result);
|
|
3816
|
+
}
|
|
3817
|
+
).post(
|
|
3818
|
+
"/:providerID/oauth/callback",
|
|
3819
|
+
describeRoute12({
|
|
3820
|
+
summary: "OAuth callback",
|
|
3821
|
+
description: "Handle the OAuth callback from a provider after user authorization.",
|
|
3822
|
+
operationId: "provider.oauth.callback",
|
|
3823
|
+
responses: {
|
|
3824
|
+
200: {
|
|
3825
|
+
description: "OAuth callback processed successfully",
|
|
3826
|
+
content: {
|
|
3827
|
+
"application/json": {
|
|
3828
|
+
schema: resolver13(z18.boolean())
|
|
3829
|
+
}
|
|
3830
|
+
}
|
|
3831
|
+
},
|
|
3832
|
+
...errors(400)
|
|
3833
|
+
}
|
|
3834
|
+
}),
|
|
3835
|
+
validator12(
|
|
3836
|
+
"param",
|
|
3837
|
+
z18.object({
|
|
3838
|
+
providerID: ProviderID.zod.meta({ description: "Provider ID" })
|
|
3839
|
+
})
|
|
3840
|
+
),
|
|
3841
|
+
validator12(
|
|
3842
|
+
"json",
|
|
3843
|
+
z18.object({
|
|
3844
|
+
method: z18.number().meta({ description: "Auth method index" }),
|
|
3845
|
+
code: z18.string().optional().meta({ description: "OAuth authorization code" })
|
|
3846
|
+
})
|
|
3847
|
+
),
|
|
3848
|
+
async (c) => {
|
|
3849
|
+
const providerID = c.req.valid("param").providerID;
|
|
3850
|
+
const { method, code } = c.req.valid("json");
|
|
3851
|
+
await ProviderAuth.callback({
|
|
3852
|
+
providerID,
|
|
3853
|
+
method,
|
|
3854
|
+
code
|
|
3855
|
+
});
|
|
3856
|
+
return c.json(true);
|
|
3857
|
+
}
|
|
3858
|
+
)
|
|
3859
|
+
);
|
|
3860
|
+
|
|
3861
|
+
// packages/opencode/src/server/routes/event.ts
|
|
3862
|
+
import { Hono as Hono14 } from "hono";
|
|
3863
|
+
import { describeRoute as describeRoute13, resolver as resolver14 } from "hono-openapi";
|
|
3864
|
+
import { streamSSE } from "hono/streaming";
|
|
3865
|
+
var log4 = Log.create({ service: "server" });
|
|
3866
|
+
var EventRoutes = () => new Hono14().get(
|
|
3867
|
+
"/event",
|
|
3868
|
+
describeRoute13({
|
|
3869
|
+
summary: "Subscribe to events",
|
|
3870
|
+
description: "Get events",
|
|
3871
|
+
operationId: "event.subscribe",
|
|
3872
|
+
responses: {
|
|
3873
|
+
200: {
|
|
3874
|
+
description: "Event stream",
|
|
3875
|
+
content: {
|
|
3876
|
+
"text/event-stream": {
|
|
3877
|
+
schema: resolver14(BusEvent.payloads())
|
|
3878
|
+
}
|
|
3879
|
+
}
|
|
3880
|
+
}
|
|
3881
|
+
}
|
|
3882
|
+
}),
|
|
3883
|
+
async (c) => {
|
|
3884
|
+
log4.info("event connected");
|
|
3885
|
+
c.header("Cache-Control", "no-cache, no-transform");
|
|
3886
|
+
c.header("X-Accel-Buffering", "no");
|
|
3887
|
+
c.header("X-Content-Type-Options", "nosniff");
|
|
3888
|
+
return streamSSE(c, async (stream2) => {
|
|
3889
|
+
const q = new AsyncQueue();
|
|
3890
|
+
let done = false;
|
|
3891
|
+
q.push(
|
|
3892
|
+
JSON.stringify({
|
|
3893
|
+
type: "server.connected",
|
|
3894
|
+
properties: {}
|
|
3895
|
+
})
|
|
3896
|
+
);
|
|
3897
|
+
const heartbeat = setInterval(() => {
|
|
3898
|
+
q.push(
|
|
3899
|
+
JSON.stringify({
|
|
3900
|
+
type: "server.heartbeat",
|
|
3901
|
+
properties: {}
|
|
3902
|
+
})
|
|
3903
|
+
);
|
|
3904
|
+
}, 1e4);
|
|
3905
|
+
const stop = () => {
|
|
3906
|
+
if (done) return;
|
|
3907
|
+
done = true;
|
|
3908
|
+
clearInterval(heartbeat);
|
|
3909
|
+
unsub();
|
|
3910
|
+
q.push(null);
|
|
3911
|
+
log4.info("event disconnected");
|
|
3912
|
+
};
|
|
3913
|
+
const unsub = Bus.subscribeAll((event) => {
|
|
3914
|
+
q.push(JSON.stringify(event));
|
|
3915
|
+
if (event.type === Bus.InstanceDisposed.type) {
|
|
3916
|
+
stop();
|
|
3917
|
+
}
|
|
3918
|
+
});
|
|
3919
|
+
stream2.onAbort(stop);
|
|
3920
|
+
try {
|
|
3921
|
+
for await (const data of q) {
|
|
3922
|
+
if (data === null) return;
|
|
3923
|
+
await stream2.writeSSE({ data });
|
|
3924
|
+
}
|
|
3925
|
+
} finally {
|
|
3926
|
+
stop();
|
|
3927
|
+
}
|
|
3928
|
+
});
|
|
3929
|
+
}
|
|
3930
|
+
);
|
|
3931
|
+
|
|
3932
|
+
// packages/opencode/src/server/middleware.ts
|
|
3933
|
+
import { HTTPException } from "hono/http-exception";
|
|
3934
|
+
function errorHandler(log9) {
|
|
3935
|
+
return (err, c) => {
|
|
3936
|
+
log9.error("failed", {
|
|
3937
|
+
error: err
|
|
3938
|
+
});
|
|
3939
|
+
if (err instanceof NamedError) {
|
|
3940
|
+
let status;
|
|
3941
|
+
if (err instanceof NotFoundError) status = 404;
|
|
3942
|
+
else if (err instanceof Provider.ModelNotFoundError) status = 400;
|
|
3943
|
+
else if (err.name === "ProviderAuthValidationFailed") status = 400;
|
|
3944
|
+
else if (err.name.startsWith("Worktree")) status = 400;
|
|
3945
|
+
else status = 500;
|
|
3946
|
+
return c.json(err.toObject(), { status });
|
|
3947
|
+
}
|
|
3948
|
+
if (err instanceof Session.BusyError) {
|
|
3949
|
+
return c.json(new NamedError.Unknown({ message: err.message }).toObject(), { status: 400 });
|
|
3950
|
+
}
|
|
3951
|
+
if (err instanceof HTTPException) return err.getResponse();
|
|
3952
|
+
const message = err instanceof Error && err.stack ? err.stack : err.toString();
|
|
3953
|
+
return c.json(new NamedError.Unknown({ message }).toObject(), {
|
|
3954
|
+
status: 500
|
|
3955
|
+
});
|
|
3956
|
+
};
|
|
3957
|
+
}
|
|
3958
|
+
|
|
3959
|
+
// packages/opencode/src/server/instance.ts
|
|
3960
|
+
var log5 = Log.create({ service: "server" });
|
|
3961
|
+
var embeddedUIPromise = Flag.OPENCODE_DISABLE_EMBEDDED_WEB_UI ? Promise.resolve(null) : (
|
|
3962
|
+
// @ts-expect-error - generated file at build time
|
|
3963
|
+
import("opencode-web-ui.gen.ts").then((module) => module.default).catch(() => null)
|
|
3964
|
+
);
|
|
3965
|
+
var DEFAULT_CSP = "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; media-src 'self' data:; connect-src 'self' data:";
|
|
3966
|
+
var csp = (hash = "") => `default-src 'self'; script-src 'self' 'wasm-unsafe-eval'${hash ? ` 'sha256-${hash}'` : ""}; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; media-src 'self' data:; connect-src 'self' data:`;
|
|
3967
|
+
var InstanceRoutes = (upgrade, app2 = new Hono15()) => app2.onError(errorHandler(log5)).route("/project", ProjectRoutes()).route("/pty", PtyRoutes(upgrade)).route("/config", ConfigRoutes()).route("/experimental", ExperimentalRoutes()).route("/session", SessionRoutes()).route("/permission", PermissionRoutes()).route("/question", QuestionRoutes()).route("/provider", ProviderRoutes()).route("/", FileRoutes()).route("/", EventRoutes()).route("/mcp", McpRoutes()).route("/tui", TuiRoutes()).post(
|
|
3968
|
+
"/instance/dispose",
|
|
3969
|
+
describeRoute14({
|
|
3970
|
+
summary: "Dispose instance",
|
|
3971
|
+
description: "Clean up and dispose the current OpenCode instance, releasing all resources.",
|
|
3972
|
+
operationId: "instance.dispose",
|
|
3973
|
+
responses: {
|
|
3974
|
+
200: {
|
|
3975
|
+
description: "Instance disposed",
|
|
3976
|
+
content: {
|
|
3977
|
+
"application/json": {
|
|
3978
|
+
schema: resolver15(z19.boolean())
|
|
3979
|
+
}
|
|
3980
|
+
}
|
|
3981
|
+
}
|
|
3982
|
+
}
|
|
3983
|
+
}),
|
|
3984
|
+
async (c) => {
|
|
3985
|
+
await Instance.dispose();
|
|
3986
|
+
return c.json(true);
|
|
3987
|
+
}
|
|
3988
|
+
).get(
|
|
3989
|
+
"/path",
|
|
3990
|
+
describeRoute14({
|
|
3991
|
+
summary: "Get paths",
|
|
3992
|
+
description: "Retrieve the current working directory and related path information for the OpenCode instance.",
|
|
3993
|
+
operationId: "path.get",
|
|
3994
|
+
responses: {
|
|
3995
|
+
200: {
|
|
3996
|
+
description: "Path",
|
|
3997
|
+
content: {
|
|
3998
|
+
"application/json": {
|
|
3999
|
+
schema: resolver15(
|
|
4000
|
+
z19.object({
|
|
4001
|
+
home: z19.string(),
|
|
4002
|
+
state: z19.string(),
|
|
4003
|
+
config: z19.string(),
|
|
4004
|
+
worktree: z19.string(),
|
|
4005
|
+
directory: z19.string()
|
|
4006
|
+
}).meta({
|
|
4007
|
+
ref: "Path"
|
|
4008
|
+
})
|
|
4009
|
+
)
|
|
4010
|
+
}
|
|
4011
|
+
}
|
|
4012
|
+
}
|
|
4013
|
+
}
|
|
4014
|
+
}),
|
|
4015
|
+
async (c) => {
|
|
4016
|
+
return c.json({
|
|
4017
|
+
home: Global.Path.home,
|
|
4018
|
+
state: Global.Path.state,
|
|
4019
|
+
config: Global.Path.config,
|
|
4020
|
+
worktree: Instance.worktree,
|
|
4021
|
+
directory: Instance.directory
|
|
4022
|
+
});
|
|
4023
|
+
}
|
|
4024
|
+
).get(
|
|
4025
|
+
"/vcs",
|
|
4026
|
+
describeRoute14({
|
|
4027
|
+
summary: "Get VCS info",
|
|
4028
|
+
description: "Retrieve version control system (VCS) information for the current project, such as git branch.",
|
|
4029
|
+
operationId: "vcs.get",
|
|
4030
|
+
responses: {
|
|
4031
|
+
200: {
|
|
4032
|
+
description: "VCS info",
|
|
4033
|
+
content: {
|
|
4034
|
+
"application/json": {
|
|
4035
|
+
schema: resolver15(Vcs.Info)
|
|
4036
|
+
}
|
|
4037
|
+
}
|
|
4038
|
+
}
|
|
4039
|
+
}
|
|
4040
|
+
}),
|
|
4041
|
+
async (c) => {
|
|
4042
|
+
const [branch, default_branch] = await Promise.all([Vcs.branch(), Vcs.defaultBranch()]);
|
|
4043
|
+
return c.json({
|
|
4044
|
+
branch,
|
|
4045
|
+
default_branch
|
|
4046
|
+
});
|
|
4047
|
+
}
|
|
4048
|
+
).get(
|
|
4049
|
+
"/vcs/diff",
|
|
4050
|
+
describeRoute14({
|
|
4051
|
+
summary: "Get VCS diff",
|
|
4052
|
+
description: "Retrieve the current git diff for the working tree or against the default branch.",
|
|
4053
|
+
operationId: "vcs.diff",
|
|
4054
|
+
responses: {
|
|
4055
|
+
200: {
|
|
4056
|
+
description: "VCS diff",
|
|
4057
|
+
content: {
|
|
4058
|
+
"application/json": {
|
|
4059
|
+
schema: resolver15(Snapshot.FileDiff.array())
|
|
4060
|
+
}
|
|
4061
|
+
}
|
|
4062
|
+
}
|
|
4063
|
+
}
|
|
4064
|
+
}),
|
|
4065
|
+
validator13(
|
|
4066
|
+
"query",
|
|
4067
|
+
z19.object({
|
|
4068
|
+
mode: Vcs.Mode
|
|
4069
|
+
})
|
|
4070
|
+
),
|
|
4071
|
+
async (c) => {
|
|
4072
|
+
return c.json(await Vcs.diff(c.req.valid("query").mode));
|
|
4073
|
+
}
|
|
4074
|
+
).get(
|
|
4075
|
+
"/command",
|
|
4076
|
+
describeRoute14({
|
|
4077
|
+
summary: "List commands",
|
|
4078
|
+
description: "Get a list of all available commands in the OpenCode system.",
|
|
4079
|
+
operationId: "command.list",
|
|
4080
|
+
responses: {
|
|
4081
|
+
200: {
|
|
4082
|
+
description: "List of commands",
|
|
4083
|
+
content: {
|
|
4084
|
+
"application/json": {
|
|
4085
|
+
schema: resolver15(Command.Info.array())
|
|
4086
|
+
}
|
|
4087
|
+
}
|
|
4088
|
+
}
|
|
4089
|
+
}
|
|
4090
|
+
}),
|
|
4091
|
+
async (c) => {
|
|
4092
|
+
const commands = await Command.list();
|
|
4093
|
+
return c.json(commands);
|
|
4094
|
+
}
|
|
4095
|
+
).get(
|
|
4096
|
+
"/agent",
|
|
4097
|
+
describeRoute14({
|
|
4098
|
+
summary: "List agents",
|
|
4099
|
+
description: "Get a list of all available AI agents in the OpenCode system.",
|
|
4100
|
+
operationId: "app.agents",
|
|
4101
|
+
responses: {
|
|
4102
|
+
200: {
|
|
4103
|
+
description: "List of agents",
|
|
4104
|
+
content: {
|
|
4105
|
+
"application/json": {
|
|
4106
|
+
schema: resolver15(Agent.Info.array())
|
|
4107
|
+
}
|
|
4108
|
+
}
|
|
4109
|
+
}
|
|
4110
|
+
}
|
|
4111
|
+
}),
|
|
4112
|
+
async (c) => {
|
|
4113
|
+
const modes = await Agent.list();
|
|
4114
|
+
return c.json(modes);
|
|
4115
|
+
}
|
|
4116
|
+
).get(
|
|
4117
|
+
"/skill",
|
|
4118
|
+
describeRoute14({
|
|
4119
|
+
summary: "List skills",
|
|
4120
|
+
description: "Get a list of all available skills in the OpenCode system.",
|
|
4121
|
+
operationId: "app.skills",
|
|
4122
|
+
responses: {
|
|
4123
|
+
200: {
|
|
4124
|
+
description: "List of skills",
|
|
4125
|
+
content: {
|
|
4126
|
+
"application/json": {
|
|
4127
|
+
schema: resolver15(Skill.Info.array())
|
|
4128
|
+
}
|
|
4129
|
+
}
|
|
4130
|
+
}
|
|
4131
|
+
}
|
|
4132
|
+
}),
|
|
4133
|
+
async (c) => {
|
|
4134
|
+
const skills = await Skill.all();
|
|
4135
|
+
return c.json(skills);
|
|
4136
|
+
}
|
|
4137
|
+
).get(
|
|
4138
|
+
"/lsp",
|
|
4139
|
+
describeRoute14({
|
|
4140
|
+
summary: "Get LSP status",
|
|
4141
|
+
description: "Get LSP server status",
|
|
4142
|
+
operationId: "lsp.status",
|
|
4143
|
+
responses: {
|
|
4144
|
+
200: {
|
|
4145
|
+
description: "LSP server status",
|
|
4146
|
+
content: {
|
|
4147
|
+
"application/json": {
|
|
4148
|
+
schema: resolver15(LSP.Status.array())
|
|
4149
|
+
}
|
|
4150
|
+
}
|
|
4151
|
+
}
|
|
4152
|
+
}
|
|
4153
|
+
}),
|
|
4154
|
+
async (c) => {
|
|
4155
|
+
return c.json(await LSP.status());
|
|
4156
|
+
}
|
|
4157
|
+
).get(
|
|
4158
|
+
"/formatter",
|
|
4159
|
+
describeRoute14({
|
|
4160
|
+
summary: "Get formatter status",
|
|
4161
|
+
description: "Get formatter status",
|
|
4162
|
+
operationId: "formatter.status",
|
|
4163
|
+
responses: {
|
|
4164
|
+
200: {
|
|
4165
|
+
description: "Formatter status",
|
|
4166
|
+
content: {
|
|
4167
|
+
"application/json": {
|
|
4168
|
+
schema: resolver15(Format.Status.array())
|
|
4169
|
+
}
|
|
4170
|
+
}
|
|
4171
|
+
}
|
|
4172
|
+
}
|
|
4173
|
+
}),
|
|
4174
|
+
async (c) => {
|
|
4175
|
+
return c.json(await Format.status());
|
|
4176
|
+
}
|
|
4177
|
+
).all("/*", async (c) => {
|
|
4178
|
+
const embeddedWebUI = await embeddedUIPromise;
|
|
4179
|
+
const path = c.req.path;
|
|
4180
|
+
if (embeddedWebUI) {
|
|
4181
|
+
const match = embeddedWebUI[path.replace(/^\//, "")] ?? embeddedWebUI["index.html"] ?? null;
|
|
4182
|
+
if (!match) return c.json({ error: "Not Found" }, 404);
|
|
4183
|
+
const fs = await import("fs");
|
|
4184
|
+
const pathMod = await import("path");
|
|
4185
|
+
try {
|
|
4186
|
+
const content = fs.readFileSync(match);
|
|
4187
|
+
const ext = pathMod.extname(match).toLowerCase();
|
|
4188
|
+
const mimeTypes = {
|
|
4189
|
+
".html": "text/html",
|
|
4190
|
+
".js": "application/javascript",
|
|
4191
|
+
".css": "text/css",
|
|
4192
|
+
".json": "application/json",
|
|
4193
|
+
".png": "image/png",
|
|
4194
|
+
".svg": "image/svg+xml",
|
|
4195
|
+
".ico": "image/x-icon",
|
|
4196
|
+
".woff": "font/woff",
|
|
4197
|
+
".woff2": "font/woff2"
|
|
4198
|
+
};
|
|
4199
|
+
const contentType = mimeTypes[ext] || "application/octet-stream";
|
|
4200
|
+
c.header("Content-Type", contentType);
|
|
4201
|
+
if (contentType.startsWith("text/html")) {
|
|
4202
|
+
c.header("Content-Security-Policy", DEFAULT_CSP);
|
|
4203
|
+
}
|
|
4204
|
+
return c.body(content);
|
|
4205
|
+
} catch {
|
|
4206
|
+
return c.json({ error: "Not Found" }, 404);
|
|
4207
|
+
}
|
|
4208
|
+
} else {
|
|
4209
|
+
const response2 = await proxy(`https://app.opencode.ai${path}`, {
|
|
4210
|
+
...c.req,
|
|
4211
|
+
headers: {
|
|
4212
|
+
...c.req.raw.headers,
|
|
4213
|
+
host: "app.opencode.ai"
|
|
4214
|
+
}
|
|
4215
|
+
});
|
|
4216
|
+
const match = response2.headers.get("content-type")?.includes("text/html") ? (await response2.clone().text()).match(
|
|
4217
|
+
/<script\b(?![^>]*\bsrc\s*=)[^>]*\bid=(['"])oc-theme-preload-script\1[^>]*>([\s\S]*?)<\/script>/i
|
|
4218
|
+
) : void 0;
|
|
4219
|
+
const hash = match ? createHash("sha256").update(match[2]).digest("base64") : "";
|
|
4220
|
+
response2.headers.set("Content-Security-Policy", csp(hash));
|
|
4221
|
+
return response2;
|
|
4222
|
+
}
|
|
4223
|
+
});
|
|
4224
|
+
|
|
4225
|
+
// packages/opencode/src/server/router.ts
|
|
4226
|
+
var RULES = [
|
|
4227
|
+
{ path: "/session/status", action: "forward" },
|
|
4228
|
+
{ method: "GET", path: "/session", action: "local" }
|
|
4229
|
+
];
|
|
4230
|
+
function local(method, path) {
|
|
4231
|
+
for (const rule of RULES) {
|
|
4232
|
+
if (rule.method && rule.method !== method) continue;
|
|
4233
|
+
const match = rule.exact ? path === rule.path : path === rule.path || path.startsWith(rule.path + "/");
|
|
4234
|
+
if (match) return rule.action === "local";
|
|
4235
|
+
}
|
|
4236
|
+
return false;
|
|
4237
|
+
}
|
|
4238
|
+
function WorkspaceRouterMiddleware(upgrade) {
|
|
4239
|
+
const routes = lazy(() => InstanceRoutes(upgrade));
|
|
4240
|
+
return async (c) => {
|
|
4241
|
+
const raw = c.req.query("directory") || c.req.header("x-opencode-directory") || process.cwd();
|
|
4242
|
+
const directory = Filesystem.resolve(
|
|
4243
|
+
(() => {
|
|
4244
|
+
try {
|
|
4245
|
+
return decodeURIComponent(raw);
|
|
4246
|
+
} catch {
|
|
4247
|
+
return raw;
|
|
4248
|
+
}
|
|
4249
|
+
})()
|
|
4250
|
+
);
|
|
4251
|
+
const url = new URL(c.req.url);
|
|
4252
|
+
const workspaceParam = url.searchParams.get("workspace") || c.req.header("x-opencode-workspace");
|
|
4253
|
+
if (!workspaceParam) {
|
|
4254
|
+
return Instance.provide({
|
|
4255
|
+
directory,
|
|
4256
|
+
init: InstanceBootstrap,
|
|
4257
|
+
async fn() {
|
|
4258
|
+
return routes().fetch(c.req.raw, c.env);
|
|
4259
|
+
}
|
|
4260
|
+
});
|
|
4261
|
+
}
|
|
4262
|
+
const workspaceID = WorkspaceID.make(workspaceParam);
|
|
4263
|
+
const workspace = await Workspace.get(workspaceID);
|
|
4264
|
+
if (!workspace) {
|
|
4265
|
+
return new Response(`Workspace not found: ${workspaceID}`, {
|
|
4266
|
+
status: 500,
|
|
4267
|
+
headers: {
|
|
4268
|
+
"content-type": "text/plain; charset=utf-8"
|
|
4269
|
+
}
|
|
4270
|
+
});
|
|
4271
|
+
}
|
|
4272
|
+
const adaptor = await getAdaptor(workspace.type);
|
|
4273
|
+
const target = await adaptor.target(workspace);
|
|
4274
|
+
if (target.type === "local") {
|
|
4275
|
+
return Instance.provide({
|
|
4276
|
+
directory: target.directory,
|
|
4277
|
+
init: InstanceBootstrap,
|
|
4278
|
+
async fn() {
|
|
4279
|
+
return routes().fetch(c.req.raw, c.env);
|
|
4280
|
+
}
|
|
4281
|
+
});
|
|
4282
|
+
}
|
|
4283
|
+
if (local(c.req.method, url.pathname)) {
|
|
4284
|
+
return routes().fetch(c.req.raw, c.env);
|
|
4285
|
+
}
|
|
4286
|
+
if (c.req.header("upgrade")?.toLowerCase() === "websocket") {
|
|
4287
|
+
return ServerProxy.websocket(target, c.req.raw, c.env);
|
|
4288
|
+
}
|
|
4289
|
+
const headers2 = new Headers(c.req.raw.headers);
|
|
4290
|
+
headers2.delete("x-opencode-workspace");
|
|
4291
|
+
return ServerProxy.http(
|
|
4292
|
+
target,
|
|
4293
|
+
new Request(c.req.raw, {
|
|
4294
|
+
headers: headers2
|
|
4295
|
+
})
|
|
4296
|
+
);
|
|
4297
|
+
};
|
|
4298
|
+
}
|
|
4299
|
+
|
|
4300
|
+
// packages/opencode/src/server/routes/global.ts
|
|
4301
|
+
import { Hono as Hono16 } from "hono";
|
|
4302
|
+
import { describeRoute as describeRoute15, resolver as resolver16, validator as validator14 } from "hono-openapi";
|
|
4303
|
+
import { streamSSE as streamSSE2 } from "hono/streaming";
|
|
4304
|
+
import z20 from "zod";
|
|
4305
|
+
var log6 = Log.create({ service: "server" });
|
|
4306
|
+
var GlobalDisposedEvent = BusEvent.define("global.disposed", z20.object({}));
|
|
4307
|
+
async function streamEvents(c, subscribe) {
|
|
4308
|
+
return streamSSE2(c, async (stream2) => {
|
|
4309
|
+
const q = new AsyncQueue();
|
|
4310
|
+
let done = false;
|
|
4311
|
+
q.push(
|
|
4312
|
+
JSON.stringify({
|
|
4313
|
+
payload: {
|
|
4314
|
+
type: "server.connected",
|
|
4315
|
+
properties: {}
|
|
4316
|
+
}
|
|
4317
|
+
})
|
|
4318
|
+
);
|
|
4319
|
+
const heartbeat = setInterval(() => {
|
|
4320
|
+
q.push(
|
|
4321
|
+
JSON.stringify({
|
|
4322
|
+
payload: {
|
|
4323
|
+
type: "server.heartbeat",
|
|
4324
|
+
properties: {}
|
|
4325
|
+
}
|
|
4326
|
+
})
|
|
4327
|
+
);
|
|
4328
|
+
}, 1e4);
|
|
4329
|
+
const stop = () => {
|
|
4330
|
+
if (done) return;
|
|
4331
|
+
done = true;
|
|
4332
|
+
clearInterval(heartbeat);
|
|
4333
|
+
unsub();
|
|
4334
|
+
q.push(null);
|
|
4335
|
+
log6.info("global event disconnected");
|
|
4336
|
+
};
|
|
4337
|
+
const unsub = subscribe(q);
|
|
4338
|
+
stream2.onAbort(stop);
|
|
4339
|
+
try {
|
|
4340
|
+
for await (const data of q) {
|
|
4341
|
+
if (data === null) return;
|
|
4342
|
+
await stream2.writeSSE({ data });
|
|
4343
|
+
}
|
|
4344
|
+
} finally {
|
|
4345
|
+
stop();
|
|
4346
|
+
}
|
|
4347
|
+
});
|
|
4348
|
+
}
|
|
4349
|
+
var GlobalRoutes = lazy(
|
|
4350
|
+
() => new Hono16().get(
|
|
4351
|
+
"/health",
|
|
4352
|
+
describeRoute15({
|
|
4353
|
+
summary: "Get health",
|
|
4354
|
+
description: "Get health information about the OpenCode server.",
|
|
4355
|
+
operationId: "global.health",
|
|
4356
|
+
responses: {
|
|
4357
|
+
200: {
|
|
4358
|
+
description: "Health information",
|
|
4359
|
+
content: {
|
|
4360
|
+
"application/json": {
|
|
4361
|
+
schema: resolver16(z20.object({ healthy: z20.literal(true), version: z20.string() }))
|
|
4362
|
+
}
|
|
4363
|
+
}
|
|
4364
|
+
}
|
|
4365
|
+
}
|
|
4366
|
+
}),
|
|
4367
|
+
async (c) => {
|
|
4368
|
+
return c.json({ healthy: true, version: Installation.VERSION });
|
|
4369
|
+
}
|
|
4370
|
+
).get(
|
|
4371
|
+
"/event",
|
|
4372
|
+
describeRoute15({
|
|
4373
|
+
summary: "Get global events",
|
|
4374
|
+
description: "Subscribe to global events from the OpenCode system using server-sent events.",
|
|
4375
|
+
operationId: "global.event",
|
|
4376
|
+
responses: {
|
|
4377
|
+
200: {
|
|
4378
|
+
description: "Event stream",
|
|
4379
|
+
content: {
|
|
4380
|
+
"text/event-stream": {
|
|
4381
|
+
schema: resolver16(
|
|
4382
|
+
z20.object({
|
|
4383
|
+
directory: z20.string(),
|
|
4384
|
+
payload: BusEvent.payloads()
|
|
4385
|
+
}).meta({
|
|
4386
|
+
ref: "GlobalEvent"
|
|
4387
|
+
})
|
|
4388
|
+
)
|
|
4389
|
+
}
|
|
4390
|
+
}
|
|
4391
|
+
}
|
|
4392
|
+
}
|
|
4393
|
+
}),
|
|
4394
|
+
async (c) => {
|
|
4395
|
+
log6.info("global event connected");
|
|
4396
|
+
c.header("Cache-Control", "no-cache, no-transform");
|
|
4397
|
+
c.header("X-Accel-Buffering", "no");
|
|
4398
|
+
c.header("X-Content-Type-Options", "nosniff");
|
|
4399
|
+
return streamEvents(c, (q) => {
|
|
4400
|
+
async function handler(event) {
|
|
4401
|
+
q.push(JSON.stringify(event));
|
|
4402
|
+
}
|
|
4403
|
+
GlobalBus.on("event", handler);
|
|
4404
|
+
return () => GlobalBus.off("event", handler);
|
|
4405
|
+
});
|
|
4406
|
+
}
|
|
4407
|
+
).get(
|
|
4408
|
+
"/sync-event",
|
|
4409
|
+
describeRoute15({
|
|
4410
|
+
summary: "Subscribe to global sync events",
|
|
4411
|
+
description: "Get global sync events",
|
|
4412
|
+
operationId: "global.sync-event.subscribe",
|
|
4413
|
+
responses: {
|
|
4414
|
+
200: {
|
|
4415
|
+
description: "Event stream",
|
|
4416
|
+
content: {
|
|
4417
|
+
"text/event-stream": {
|
|
4418
|
+
schema: resolver16(
|
|
4419
|
+
z20.object({
|
|
4420
|
+
payload: SyncEvent.payloads()
|
|
4421
|
+
}).meta({
|
|
4422
|
+
ref: "SyncEvent"
|
|
4423
|
+
})
|
|
4424
|
+
)
|
|
4425
|
+
}
|
|
4426
|
+
}
|
|
4427
|
+
}
|
|
4428
|
+
}
|
|
4429
|
+
}),
|
|
4430
|
+
async (c) => {
|
|
4431
|
+
log6.info("global sync event connected");
|
|
4432
|
+
c.header("Cache-Control", "no-cache, no-transform");
|
|
4433
|
+
c.header("X-Accel-Buffering", "no");
|
|
4434
|
+
c.header("X-Content-Type-Options", "nosniff");
|
|
4435
|
+
return streamEvents(c, (q) => {
|
|
4436
|
+
return SyncEvent.subscribeAll(({ def, event }) => {
|
|
4437
|
+
q.push(
|
|
4438
|
+
JSON.stringify({
|
|
4439
|
+
payload: {
|
|
4440
|
+
...event,
|
|
4441
|
+
type: SyncEvent.versionedType(def.type, def.version)
|
|
4442
|
+
}
|
|
4443
|
+
})
|
|
4444
|
+
);
|
|
4445
|
+
});
|
|
4446
|
+
});
|
|
4447
|
+
}
|
|
4448
|
+
).get(
|
|
4449
|
+
"/config",
|
|
4450
|
+
describeRoute15({
|
|
4451
|
+
summary: "Get global configuration",
|
|
4452
|
+
description: "Retrieve the current global OpenCode configuration settings and preferences.",
|
|
4453
|
+
operationId: "global.config.get",
|
|
4454
|
+
responses: {
|
|
4455
|
+
200: {
|
|
4456
|
+
description: "Get global config info",
|
|
4457
|
+
content: {
|
|
4458
|
+
"application/json": {
|
|
4459
|
+
schema: resolver16(Config.Info)
|
|
4460
|
+
}
|
|
4461
|
+
}
|
|
4462
|
+
}
|
|
4463
|
+
}
|
|
4464
|
+
}),
|
|
4465
|
+
async (c) => {
|
|
4466
|
+
return c.json(await Config.getGlobal());
|
|
4467
|
+
}
|
|
4468
|
+
).patch(
|
|
4469
|
+
"/config",
|
|
4470
|
+
describeRoute15({
|
|
4471
|
+
summary: "Update global configuration",
|
|
4472
|
+
description: "Update global OpenCode configuration settings and preferences.",
|
|
4473
|
+
operationId: "global.config.update",
|
|
4474
|
+
responses: {
|
|
4475
|
+
200: {
|
|
4476
|
+
description: "Successfully updated global config",
|
|
4477
|
+
content: {
|
|
4478
|
+
"application/json": {
|
|
4479
|
+
schema: resolver16(Config.Info)
|
|
4480
|
+
}
|
|
4481
|
+
}
|
|
4482
|
+
},
|
|
4483
|
+
...errors(400)
|
|
4484
|
+
}
|
|
4485
|
+
}),
|
|
4486
|
+
validator14("json", Config.Info),
|
|
4487
|
+
async (c) => {
|
|
4488
|
+
const config = c.req.valid("json");
|
|
4489
|
+
const next = await Config.updateGlobal(config);
|
|
4490
|
+
return c.json(next);
|
|
4491
|
+
}
|
|
4492
|
+
).post(
|
|
4493
|
+
"/dispose",
|
|
4494
|
+
describeRoute15({
|
|
4495
|
+
summary: "Dispose instance",
|
|
4496
|
+
description: "Clean up and dispose all OpenCode instances, releasing all resources.",
|
|
4497
|
+
operationId: "global.dispose",
|
|
4498
|
+
responses: {
|
|
4499
|
+
200: {
|
|
4500
|
+
description: "Global disposed",
|
|
4501
|
+
content: {
|
|
4502
|
+
"application/json": {
|
|
4503
|
+
schema: resolver16(z20.boolean())
|
|
4504
|
+
}
|
|
4505
|
+
}
|
|
4506
|
+
}
|
|
4507
|
+
}
|
|
4508
|
+
}),
|
|
4509
|
+
async (c) => {
|
|
4510
|
+
await Instance.disposeAll();
|
|
4511
|
+
GlobalBus.emit("event", {
|
|
4512
|
+
directory: "global",
|
|
4513
|
+
payload: {
|
|
4514
|
+
type: GlobalDisposedEvent.type,
|
|
4515
|
+
properties: {}
|
|
4516
|
+
}
|
|
4517
|
+
});
|
|
4518
|
+
return c.json(true);
|
|
4519
|
+
}
|
|
4520
|
+
).post(
|
|
4521
|
+
"/upgrade",
|
|
4522
|
+
describeRoute15({
|
|
4523
|
+
summary: "Upgrade opencode",
|
|
4524
|
+
description: "Upgrade opencode to the specified version or latest if not specified.",
|
|
4525
|
+
operationId: "global.upgrade",
|
|
4526
|
+
responses: {
|
|
4527
|
+
200: {
|
|
4528
|
+
description: "Upgrade result",
|
|
4529
|
+
content: {
|
|
4530
|
+
"application/json": {
|
|
4531
|
+
schema: resolver16(
|
|
4532
|
+
z20.union([
|
|
4533
|
+
z20.object({
|
|
4534
|
+
success: z20.literal(true),
|
|
4535
|
+
version: z20.string()
|
|
4536
|
+
}),
|
|
4537
|
+
z20.object({
|
|
4538
|
+
success: z20.literal(false),
|
|
4539
|
+
error: z20.string()
|
|
4540
|
+
})
|
|
4541
|
+
])
|
|
4542
|
+
)
|
|
4543
|
+
}
|
|
4544
|
+
}
|
|
4545
|
+
},
|
|
4546
|
+
...errors(400)
|
|
4547
|
+
}
|
|
4548
|
+
}),
|
|
4549
|
+
validator14(
|
|
4550
|
+
"json",
|
|
4551
|
+
z20.object({
|
|
4552
|
+
target: z20.string().optional()
|
|
4553
|
+
})
|
|
4554
|
+
),
|
|
4555
|
+
async (c) => {
|
|
4556
|
+
const method = await Installation.method();
|
|
4557
|
+
if (method === "unknown") {
|
|
4558
|
+
return c.json({ success: false, error: "Unknown installation method" }, 400);
|
|
4559
|
+
}
|
|
4560
|
+
const target = c.req.valid("json").target || await Installation.latest(method);
|
|
4561
|
+
const result = await Installation.upgrade(method, target).then(() => ({ success: true, version: target })).catch((e) => ({ success: false, error: e instanceof Error ? e.message : String(e) }));
|
|
4562
|
+
if (result.success) {
|
|
4563
|
+
GlobalBus.emit("event", {
|
|
4564
|
+
directory: "global",
|
|
4565
|
+
payload: {
|
|
4566
|
+
type: Installation.Event.Updated.type,
|
|
4567
|
+
properties: { version: target }
|
|
4568
|
+
}
|
|
4569
|
+
});
|
|
4570
|
+
return c.json(result);
|
|
4571
|
+
}
|
|
4572
|
+
return c.json(result, 500);
|
|
4573
|
+
}
|
|
4574
|
+
)
|
|
4575
|
+
);
|
|
4576
|
+
|
|
4577
|
+
// packages/opencode/src/server/mdns.ts
|
|
4578
|
+
import { Bonjour } from "bonjour-service";
|
|
4579
|
+
var log7 = Log.create({ service: "mdns" });
|
|
4580
|
+
var MDNS;
|
|
4581
|
+
((MDNS2) => {
|
|
4582
|
+
let bonjour;
|
|
4583
|
+
let currentPort;
|
|
4584
|
+
function publish(port, domain) {
|
|
4585
|
+
if (currentPort === port) return;
|
|
4586
|
+
if (bonjour) unpublish();
|
|
4587
|
+
try {
|
|
4588
|
+
const host = domain ?? "opencode.local";
|
|
4589
|
+
const name = `opencode-${port}`;
|
|
4590
|
+
bonjour = new Bonjour();
|
|
4591
|
+
const service = bonjour.publish({
|
|
4592
|
+
name,
|
|
4593
|
+
type: "http",
|
|
4594
|
+
host,
|
|
4595
|
+
port,
|
|
4596
|
+
txt: { path: "/" }
|
|
4597
|
+
});
|
|
4598
|
+
service.on("up", () => {
|
|
4599
|
+
log7.info("mDNS service published", { name, port });
|
|
4600
|
+
});
|
|
4601
|
+
service.on("error", (err) => {
|
|
4602
|
+
log7.error("mDNS service error", { error: err });
|
|
4603
|
+
});
|
|
4604
|
+
currentPort = port;
|
|
4605
|
+
} catch (err) {
|
|
4606
|
+
log7.error("mDNS publish failed", { error: err });
|
|
4607
|
+
if (bonjour) {
|
|
4608
|
+
try {
|
|
4609
|
+
bonjour.destroy();
|
|
4610
|
+
} catch {
|
|
4611
|
+
}
|
|
4612
|
+
}
|
|
4613
|
+
bonjour = void 0;
|
|
4614
|
+
currentPort = void 0;
|
|
4615
|
+
}
|
|
4616
|
+
}
|
|
4617
|
+
MDNS2.publish = publish;
|
|
4618
|
+
function unpublish() {
|
|
4619
|
+
if (bonjour) {
|
|
4620
|
+
try {
|
|
4621
|
+
bonjour.unpublishAll();
|
|
4622
|
+
bonjour.destroy();
|
|
4623
|
+
} catch (err) {
|
|
4624
|
+
log7.error("mDNS unpublish failed", { error: err });
|
|
4625
|
+
}
|
|
4626
|
+
bonjour = void 0;
|
|
4627
|
+
currentPort = void 0;
|
|
4628
|
+
log7.info("mDNS service unpublished");
|
|
4629
|
+
}
|
|
4630
|
+
}
|
|
4631
|
+
MDNS2.unpublish = unpublish;
|
|
4632
|
+
})(MDNS || (MDNS = {}));
|
|
4633
|
+
|
|
4634
|
+
// packages/opencode/src/session/projectors.ts
|
|
4635
|
+
var log8 = Log.create({ service: "session.projector" });
|
|
4636
|
+
function foreign(err) {
|
|
4637
|
+
if (typeof err !== "object" || err === null) return false;
|
|
4638
|
+
if ("code" in err && err.code === "SQLITE_CONSTRAINT_FOREIGNKEY") return true;
|
|
4639
|
+
return "message" in err && typeof err.message === "string" && err.message.includes("FOREIGN KEY constraint failed");
|
|
4640
|
+
}
|
|
4641
|
+
function grab(obj, field1, cb) {
|
|
4642
|
+
if (obj == void 0 || !(field1 in obj)) return void 0;
|
|
4643
|
+
const val = obj[field1];
|
|
4644
|
+
if (val && typeof val === "object" && cb) {
|
|
4645
|
+
return cb(val);
|
|
4646
|
+
}
|
|
4647
|
+
if (val === void 0) {
|
|
4648
|
+
throw new Error(
|
|
4649
|
+
"Session update failure: pass `null` to clear a field instead of `undefined`: " + JSON.stringify(obj)
|
|
4650
|
+
);
|
|
4651
|
+
}
|
|
4652
|
+
return val;
|
|
4653
|
+
}
|
|
4654
|
+
function toPartialRow(info) {
|
|
4655
|
+
const obj = {
|
|
4656
|
+
id: grab(info, "id"),
|
|
4657
|
+
project_id: grab(info, "projectID"),
|
|
4658
|
+
workspace_id: grab(info, "workspaceID"),
|
|
4659
|
+
parent_id: grab(info, "parentID"),
|
|
4660
|
+
slug: grab(info, "slug"),
|
|
4661
|
+
directory: grab(info, "directory"),
|
|
4662
|
+
title: grab(info, "title"),
|
|
4663
|
+
version: grab(info, "version"),
|
|
4664
|
+
share_url: grab(info, "share", (v) => grab(v, "url")),
|
|
4665
|
+
summary_additions: grab(info, "summary", (v) => grab(v, "additions")),
|
|
4666
|
+
summary_deletions: grab(info, "summary", (v) => grab(v, "deletions")),
|
|
4667
|
+
summary_files: grab(info, "summary", (v) => grab(v, "files")),
|
|
4668
|
+
summary_diffs: grab(info, "summary", (v) => grab(v, "diffs")),
|
|
4669
|
+
revert: grab(info, "revert"),
|
|
4670
|
+
permission: grab(info, "permission"),
|
|
4671
|
+
time_created: grab(info, "time", (v) => grab(v, "created")),
|
|
4672
|
+
time_updated: grab(info, "time", (v) => grab(v, "updated")),
|
|
4673
|
+
time_compacting: grab(info, "time", (v) => grab(v, "compacting")),
|
|
4674
|
+
time_archived: grab(info, "time", (v) => grab(v, "archived"))
|
|
4675
|
+
};
|
|
4676
|
+
return Object.fromEntries(Object.entries(obj).filter(([_, val]) => val !== void 0));
|
|
4677
|
+
}
|
|
4678
|
+
var projectors_default = [
|
|
4679
|
+
SyncEvent.project(Session.Event.Created, (db, data) => {
|
|
4680
|
+
db.insert(SessionTable).values(Session.toRow(data.info)).run();
|
|
4681
|
+
}),
|
|
4682
|
+
SyncEvent.project(Session.Event.Updated, (db, data) => {
|
|
4683
|
+
const info = data.info;
|
|
4684
|
+
const row = db.update(SessionTable).set(toPartialRow(info)).where((0, db_exports.eq)(SessionTable.id, data.sessionID)).returning().get();
|
|
4685
|
+
if (!row) throw new NotFoundError({ message: `Session not found: ${data.sessionID}` });
|
|
4686
|
+
}),
|
|
4687
|
+
SyncEvent.project(Session.Event.Deleted, (db, data) => {
|
|
4688
|
+
db.delete(SessionTable).where((0, db_exports.eq)(SessionTable.id, data.sessionID)).run();
|
|
4689
|
+
}),
|
|
4690
|
+
SyncEvent.project(MessageV2.Event.Updated, (db, data) => {
|
|
4691
|
+
const time_created = data.info.time.created;
|
|
4692
|
+
const { id, sessionID, ...rest } = data.info;
|
|
4693
|
+
try {
|
|
4694
|
+
db.insert(MessageTable).values({
|
|
4695
|
+
id,
|
|
4696
|
+
session_id: sessionID,
|
|
4697
|
+
time_created,
|
|
4698
|
+
data: rest
|
|
4699
|
+
}).onConflictDoUpdate({ target: MessageTable.id, set: { data: rest } }).run();
|
|
4700
|
+
} catch (err) {
|
|
4701
|
+
if (!foreign(err)) throw err;
|
|
4702
|
+
log8.warn("ignored late message update", { messageID: id, sessionID });
|
|
4703
|
+
}
|
|
4704
|
+
}),
|
|
4705
|
+
SyncEvent.project(MessageV2.Event.Removed, (db, data) => {
|
|
4706
|
+
db.delete(MessageTable).where((0, db_exports.and)((0, db_exports.eq)(MessageTable.id, data.messageID), (0, db_exports.eq)(MessageTable.session_id, data.sessionID))).run();
|
|
4707
|
+
}),
|
|
4708
|
+
SyncEvent.project(MessageV2.Event.PartRemoved, (db, data) => {
|
|
4709
|
+
db.delete(PartTable).where((0, db_exports.and)((0, db_exports.eq)(PartTable.id, data.partID), (0, db_exports.eq)(PartTable.session_id, data.sessionID))).run();
|
|
4710
|
+
}),
|
|
4711
|
+
SyncEvent.project(MessageV2.Event.PartUpdated, (db, data) => {
|
|
4712
|
+
const { id, messageID, sessionID, ...rest } = data.part;
|
|
4713
|
+
try {
|
|
4714
|
+
db.insert(PartTable).values({
|
|
4715
|
+
id,
|
|
4716
|
+
message_id: messageID,
|
|
4717
|
+
session_id: sessionID,
|
|
4718
|
+
time_created: data.time,
|
|
4719
|
+
data: rest
|
|
4720
|
+
}).onConflictDoUpdate({ target: PartTable.id, set: { data: rest } }).run();
|
|
4721
|
+
} catch (err) {
|
|
4722
|
+
if (!foreign(err)) throw err;
|
|
4723
|
+
log8.warn("ignored late part update", { partID: id, messageID, sessionID });
|
|
4724
|
+
}
|
|
4725
|
+
})
|
|
4726
|
+
];
|
|
4727
|
+
|
|
4728
|
+
// packages/opencode/src/server/projectors.ts
|
|
4729
|
+
function initProjectors() {
|
|
4730
|
+
SyncEvent.init({
|
|
4731
|
+
projectors: projectors_default,
|
|
4732
|
+
convertEvent: (type, data) => {
|
|
4733
|
+
if (type === "session.updated") {
|
|
4734
|
+
const id = data.sessionID;
|
|
4735
|
+
const row = Database.use((db) => db.select().from(SessionTable).where((0, db_exports.eq)(SessionTable.id, id)).get());
|
|
4736
|
+
if (!row) return data;
|
|
4737
|
+
return {
|
|
4738
|
+
sessionID: id,
|
|
4739
|
+
info: Session.fromRow(row)
|
|
4740
|
+
};
|
|
4741
|
+
}
|
|
4742
|
+
return data;
|
|
4743
|
+
}
|
|
4744
|
+
});
|
|
4745
|
+
}
|
|
4746
|
+
initProjectors();
|
|
4747
|
+
|
|
4748
|
+
// packages/opencode/src/server/server.ts
|
|
4749
|
+
globalThis.AI_SDK_LOG_WARNINGS = false;
|
|
4750
|
+
initProjectors();
|
|
4751
|
+
var Server;
|
|
4752
|
+
((Server2) => {
|
|
4753
|
+
const log9 = Log.create({ service: "server" });
|
|
4754
|
+
const zipped = compress();
|
|
4755
|
+
const skipCompress = (path, method) => {
|
|
4756
|
+
if (path === "/event" || path === "/global/event" || path === "/global/sync-event") return true;
|
|
4757
|
+
if (method === "POST" && /\/session\/[^/]+\/(message|prompt_async)$/.test(path)) return true;
|
|
4758
|
+
return false;
|
|
4759
|
+
};
|
|
4760
|
+
Server2.Default = lazy(() => create({}).app);
|
|
4761
|
+
function ControlPlaneRoutes(upgrade, app2 = new Hono17(), opts) {
|
|
4762
|
+
return app2.onError(errorHandler(log9)).use((c, next) => {
|
|
4763
|
+
if (c.req.method === "OPTIONS") return next();
|
|
4764
|
+
const password = Flag.OPENCODE_SERVER_PASSWORD;
|
|
4765
|
+
if (!password) return next();
|
|
4766
|
+
const username = Flag.OPENCODE_SERVER_USERNAME ?? "opencode";
|
|
4767
|
+
return basicAuth({ username, password })(c, next);
|
|
4768
|
+
}).use(async (c, next) => {
|
|
4769
|
+
const skip = c.req.path === "/log";
|
|
4770
|
+
if (!skip) {
|
|
4771
|
+
log9.info("request", {
|
|
4772
|
+
method: c.req.method,
|
|
4773
|
+
path: c.req.path
|
|
4774
|
+
});
|
|
4775
|
+
}
|
|
4776
|
+
const timer = log9.time("request", {
|
|
4777
|
+
method: c.req.method,
|
|
4778
|
+
path: c.req.path
|
|
4779
|
+
});
|
|
4780
|
+
await next();
|
|
4781
|
+
if (!skip) timer.stop();
|
|
4782
|
+
}).use(
|
|
4783
|
+
cors({
|
|
4784
|
+
maxAge: 86400,
|
|
4785
|
+
origin(input) {
|
|
4786
|
+
if (!input) return;
|
|
4787
|
+
if (input.startsWith("http://localhost:")) return input;
|
|
4788
|
+
if (input.startsWith("http://127.0.0.1:")) return input;
|
|
4789
|
+
if (input === "tauri://localhost" || input === "http://tauri.localhost" || input === "https://tauri.localhost")
|
|
4790
|
+
return input;
|
|
4791
|
+
if (/^https:\/\/([a-z0-9-]+\.)*opencode\.ai$/.test(input)) return input;
|
|
4792
|
+
if (opts?.cors?.includes(input)) return input;
|
|
4793
|
+
}
|
|
4794
|
+
})
|
|
4795
|
+
).use((c, next) => {
|
|
4796
|
+
if (skipCompress(c.req.path, c.req.method)) return next();
|
|
4797
|
+
return zipped(c, next);
|
|
4798
|
+
}).route("/global", GlobalRoutes()).put(
|
|
4799
|
+
"/auth/:providerID",
|
|
4800
|
+
describeRoute16({
|
|
4801
|
+
summary: "Set auth credentials",
|
|
4802
|
+
description: "Set authentication credentials",
|
|
4803
|
+
operationId: "auth.set",
|
|
4804
|
+
responses: {
|
|
4805
|
+
200: {
|
|
4806
|
+
description: "Successfully set authentication credentials",
|
|
4807
|
+
content: {
|
|
4808
|
+
"application/json": {
|
|
4809
|
+
schema: resolver17(z21.boolean())
|
|
4810
|
+
}
|
|
4811
|
+
}
|
|
4812
|
+
},
|
|
4813
|
+
...errors(400)
|
|
4814
|
+
}
|
|
4815
|
+
}),
|
|
4816
|
+
validator15(
|
|
4817
|
+
"param",
|
|
4818
|
+
z21.object({
|
|
4819
|
+
providerID: ProviderID.zod
|
|
4820
|
+
})
|
|
4821
|
+
),
|
|
4822
|
+
validator15("json", Auth.Info.zod),
|
|
4823
|
+
async (c) => {
|
|
4824
|
+
const providerID = c.req.valid("param").providerID;
|
|
4825
|
+
const info = c.req.valid("json");
|
|
4826
|
+
await Auth.set(providerID, info);
|
|
4827
|
+
return c.json(true);
|
|
4828
|
+
}
|
|
4829
|
+
).delete(
|
|
4830
|
+
"/auth/:providerID",
|
|
4831
|
+
describeRoute16({
|
|
4832
|
+
summary: "Remove auth credentials",
|
|
4833
|
+
description: "Remove authentication credentials",
|
|
4834
|
+
operationId: "auth.remove",
|
|
4835
|
+
responses: {
|
|
4836
|
+
200: {
|
|
4837
|
+
description: "Successfully removed authentication credentials",
|
|
4838
|
+
content: {
|
|
4839
|
+
"application/json": {
|
|
4840
|
+
schema: resolver17(z21.boolean())
|
|
4841
|
+
}
|
|
4842
|
+
}
|
|
4843
|
+
},
|
|
4844
|
+
...errors(400)
|
|
4845
|
+
}
|
|
4846
|
+
}),
|
|
4847
|
+
validator15(
|
|
4848
|
+
"param",
|
|
4849
|
+
z21.object({
|
|
4850
|
+
providerID: ProviderID.zod
|
|
4851
|
+
})
|
|
4852
|
+
),
|
|
4853
|
+
async (c) => {
|
|
4854
|
+
const providerID = c.req.valid("param").providerID;
|
|
4855
|
+
await Auth.remove(providerID);
|
|
4856
|
+
return c.json(true);
|
|
4857
|
+
}
|
|
4858
|
+
).get(
|
|
4859
|
+
"/doc",
|
|
4860
|
+
openAPIRouteHandler(app2, {
|
|
4861
|
+
documentation: {
|
|
4862
|
+
info: {
|
|
4863
|
+
title: "opencode",
|
|
4864
|
+
version: "0.0.3",
|
|
4865
|
+
description: "opencode api"
|
|
4866
|
+
},
|
|
4867
|
+
openapi: "3.1.1"
|
|
4868
|
+
}
|
|
4869
|
+
})
|
|
4870
|
+
).use(
|
|
4871
|
+
validator15(
|
|
4872
|
+
"query",
|
|
4873
|
+
z21.object({
|
|
4874
|
+
directory: z21.string().optional(),
|
|
4875
|
+
workspace: z21.string().optional()
|
|
4876
|
+
})
|
|
4877
|
+
)
|
|
4878
|
+
).post(
|
|
4879
|
+
"/log",
|
|
4880
|
+
describeRoute16({
|
|
4881
|
+
summary: "Write log",
|
|
4882
|
+
description: "Write a log entry to the server logs with specified level and metadata.",
|
|
4883
|
+
operationId: "app.log",
|
|
4884
|
+
responses: {
|
|
4885
|
+
200: {
|
|
4886
|
+
description: "Log entry written successfully",
|
|
4887
|
+
content: {
|
|
4888
|
+
"application/json": {
|
|
4889
|
+
schema: resolver17(z21.boolean())
|
|
4890
|
+
}
|
|
4891
|
+
}
|
|
4892
|
+
},
|
|
4893
|
+
...errors(400)
|
|
4894
|
+
}
|
|
4895
|
+
}),
|
|
4896
|
+
validator15(
|
|
4897
|
+
"json",
|
|
4898
|
+
z21.object({
|
|
4899
|
+
service: z21.string().meta({ description: "Service name for the log entry" }),
|
|
4900
|
+
level: z21.enum(["debug", "info", "error", "warn"]).meta({ description: "Log level" }),
|
|
4901
|
+
message: z21.string().meta({ description: "Log message" }),
|
|
4902
|
+
extra: z21.record(z21.string(), z21.any()).optional().meta({ description: "Additional metadata for the log entry" })
|
|
4903
|
+
})
|
|
4904
|
+
),
|
|
4905
|
+
async (c) => {
|
|
4906
|
+
const { service, level, message, extra } = c.req.valid("json");
|
|
4907
|
+
const logger = Log.create({ service });
|
|
4908
|
+
switch (level) {
|
|
4909
|
+
case "debug":
|
|
4910
|
+
logger.debug(message, extra);
|
|
4911
|
+
break;
|
|
4912
|
+
case "info":
|
|
4913
|
+
logger.info(message, extra);
|
|
4914
|
+
break;
|
|
4915
|
+
case "error":
|
|
4916
|
+
logger.error(message, extra);
|
|
4917
|
+
break;
|
|
4918
|
+
case "warn":
|
|
4919
|
+
logger.warn(message, extra);
|
|
4920
|
+
break;
|
|
4921
|
+
}
|
|
4922
|
+
return c.json(true);
|
|
4923
|
+
}
|
|
4924
|
+
).use(WorkspaceRouterMiddleware(upgrade));
|
|
4925
|
+
}
|
|
4926
|
+
Server2.ControlPlaneRoutes = ControlPlaneRoutes;
|
|
4927
|
+
function create(opts) {
|
|
4928
|
+
const app2 = new Hono17();
|
|
4929
|
+
const ws = createNodeWebSocket2({ app: app2 });
|
|
4930
|
+
return {
|
|
4931
|
+
app: ControlPlaneRoutes(ws.upgradeWebSocket, app2, opts),
|
|
4932
|
+
ws
|
|
4933
|
+
};
|
|
4934
|
+
}
|
|
4935
|
+
function createApp(opts) {
|
|
4936
|
+
return create(opts).app;
|
|
4937
|
+
}
|
|
4938
|
+
Server2.createApp = createApp;
|
|
4939
|
+
async function openapi() {
|
|
4940
|
+
const { app: app2, ws } = create({});
|
|
4941
|
+
InstanceRoutes(ws.upgradeWebSocket, app2);
|
|
4942
|
+
const result = await generateSpecs(app2, {
|
|
4943
|
+
documentation: {
|
|
4944
|
+
info: {
|
|
4945
|
+
title: "opencode",
|
|
4946
|
+
version: "1.0.0",
|
|
4947
|
+
description: "opencode api"
|
|
4948
|
+
},
|
|
4949
|
+
openapi: "3.1.1"
|
|
4950
|
+
}
|
|
4951
|
+
});
|
|
4952
|
+
return result;
|
|
4953
|
+
}
|
|
4954
|
+
Server2.openapi = openapi;
|
|
4955
|
+
async function listen(opts) {
|
|
4956
|
+
const built = create(opts);
|
|
4957
|
+
const start = (port) => new Promise((resolve, reject) => {
|
|
4958
|
+
const server2 = createAdaptorServer({ fetch: built.app.fetch });
|
|
4959
|
+
built.ws.injectWebSocket(server2);
|
|
4960
|
+
const fail = (err) => {
|
|
4961
|
+
cleanup();
|
|
4962
|
+
reject(err);
|
|
4963
|
+
};
|
|
4964
|
+
const ready = () => {
|
|
4965
|
+
cleanup();
|
|
4966
|
+
resolve(server2);
|
|
4967
|
+
};
|
|
4968
|
+
const cleanup = () => {
|
|
4969
|
+
server2.off("error", fail);
|
|
4970
|
+
server2.off("listening", ready);
|
|
4971
|
+
};
|
|
4972
|
+
server2.once("error", fail);
|
|
4973
|
+
server2.once("listening", ready);
|
|
4974
|
+
server2.listen(port, opts.hostname);
|
|
4975
|
+
});
|
|
4976
|
+
const server = opts.port === 0 ? await start(4096).catch(() => start(0)) : await start(opts.port);
|
|
4977
|
+
const addr = server.address();
|
|
4978
|
+
if (!addr || typeof addr === "string") {
|
|
4979
|
+
throw new Error(`Failed to resolve server address for port ${opts.port}`);
|
|
4980
|
+
}
|
|
4981
|
+
const next = new URL("http://localhost");
|
|
4982
|
+
next.hostname = opts.hostname;
|
|
4983
|
+
next.port = String(addr.port);
|
|
4984
|
+
Server2.url = next;
|
|
4985
|
+
const mdns = opts.mdns && addr.port && opts.hostname !== "127.0.0.1" && opts.hostname !== "localhost" && opts.hostname !== "::1";
|
|
4986
|
+
if (mdns) {
|
|
4987
|
+
MDNS.publish(addr.port, opts.mdnsDomain);
|
|
4988
|
+
} else if (opts.mdns) {
|
|
4989
|
+
log9.warn("mDNS enabled but hostname is loopback; skipping mDNS publish");
|
|
4990
|
+
}
|
|
4991
|
+
let closing;
|
|
4992
|
+
return {
|
|
4993
|
+
hostname: opts.hostname,
|
|
4994
|
+
port: addr.port,
|
|
4995
|
+
url: next,
|
|
4996
|
+
stop(close) {
|
|
4997
|
+
closing ??= new Promise((resolve, reject) => {
|
|
4998
|
+
if (mdns) MDNS.unpublish();
|
|
4999
|
+
server.close((err) => {
|
|
5000
|
+
if (err) {
|
|
5001
|
+
reject(err);
|
|
5002
|
+
return;
|
|
5003
|
+
}
|
|
5004
|
+
resolve();
|
|
5005
|
+
});
|
|
5006
|
+
if (close) {
|
|
5007
|
+
if ("closeAllConnections" in server && typeof server.closeAllConnections === "function") {
|
|
5008
|
+
server.closeAllConnections();
|
|
5009
|
+
}
|
|
5010
|
+
if ("closeIdleConnections" in server && typeof server.closeIdleConnections === "function") {
|
|
5011
|
+
server.closeIdleConnections();
|
|
5012
|
+
}
|
|
5013
|
+
}
|
|
5014
|
+
});
|
|
5015
|
+
return closing;
|
|
5016
|
+
}
|
|
5017
|
+
};
|
|
5018
|
+
}
|
|
5019
|
+
Server2.listen = listen;
|
|
5020
|
+
})(Server || (Server = {}));
|
|
5021
|
+
|
|
5022
|
+
export {
|
|
5023
|
+
Server
|
|
5024
|
+
};
|
|
5025
|
+
//# sourceMappingURL=chunk-UWZSRZHA.js.map
|