antechamber 0.2.3 → 0.2.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/CHANGELOG.md +32 -0
- package/README.md +25 -10
- package/bin/opencode-antechamber +4 -6
- package/dist/cli.js +8 -0
- package/dist/commands/context.d.ts +4 -0
- package/dist/commands/context.js +19 -0
- package/dist/commands/doctor.js +8 -3
- package/dist/commands/help.d.ts +1 -1
- package/dist/commands/help.js +2 -0
- package/dist/commands/serve.js +14 -0
- package/dist/commands/setup.d.ts +1 -0
- package/dist/commands/setup.js +34 -5
- package/dist/commands/upgrade.d.ts +2 -0
- package/dist/commands/upgrade.js +7 -0
- package/dist/lifecycle.d.ts +1 -0
- package/dist/lifecycle.js +4 -0
- package/dist/mcp.d.ts +70 -0
- package/dist/mcp.js +494 -0
- package/dist/misc-routes.js +2 -3
- package/dist/monitor-sessions.d.ts +48 -0
- package/dist/monitor-sessions.js +295 -0
- package/dist/pi-event-translation.d.ts +2 -0
- package/dist/pi-event-translation.js +3 -0
- package/dist/pi-rpc.js +2 -0
- package/dist/route-services.d.ts +2 -0
- package/dist/routes-v1.js +24 -8
- package/dist/routes-v2.js +11 -5
- package/dist/self-update.d.ts +32 -0
- package/dist/self-update.js +188 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.js +2 -0
- package/dist/sessions.d.ts +6 -0
- package/dist/sessions.js +19 -1
- package/dist/types.d.ts +5 -0
- package/dist/v2.js +2 -0
- package/extensions/omo-dag/package.json +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OmO terminal monitors as read-only child sessions. OpenChamber has no surface for
|
|
3
|
+
* background watches, but it already lists child sessions under Subagents and gives each
|
|
4
|
+
* a live dot, so a watch is busy while OmO reports it and idle once it ends.
|
|
5
|
+
*/
|
|
6
|
+
import { getAgentName } from "./agent-name.js";
|
|
7
|
+
import { ocIdFor } from "./session-store.js";
|
|
8
|
+
/** Names OmO's terminal extension passes to `pi.rpc.emit` (core/extensions/builtin/monitor-state-event.js). */
|
|
9
|
+
export const MONITOR_STATE_EVENT = "terminal_monitor_state";
|
|
10
|
+
export const MONITOR_ENDED_EVENT = "terminal_monitor_ended";
|
|
11
|
+
function record(value) {
|
|
12
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
13
|
+
}
|
|
14
|
+
function finite(value) {
|
|
15
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
16
|
+
}
|
|
17
|
+
function text(value) {
|
|
18
|
+
return typeof value === "string" && value !== "" ? value : null;
|
|
19
|
+
}
|
|
20
|
+
export function parseMonitor(value) {
|
|
21
|
+
if (!record(value) || typeof value.id !== "string" || value.id === "")
|
|
22
|
+
return undefined;
|
|
23
|
+
const startedAtMs = finite(value.startedAtMs);
|
|
24
|
+
if (startedAtMs === null)
|
|
25
|
+
return undefined;
|
|
26
|
+
return {
|
|
27
|
+
id: value.id,
|
|
28
|
+
description: text(value.description) ?? value.id,
|
|
29
|
+
command: text(value.command),
|
|
30
|
+
filter: text(value.filter),
|
|
31
|
+
paused: value.paused === true,
|
|
32
|
+
persistent: value.persistent === true,
|
|
33
|
+
startedAtMs,
|
|
34
|
+
deadlineMs: finite(value.deadlineMs),
|
|
35
|
+
fireCount: finite(value.fireCount) ?? 0,
|
|
36
|
+
lastFiredAtMs: finite(value.lastFiredAtMs),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function endingLabel(ending) {
|
|
40
|
+
switch (ending.reason) {
|
|
41
|
+
case "exit":
|
|
42
|
+
return ending.exitCode === null ? "command exited" : `command exited with code ${ending.exitCode}`;
|
|
43
|
+
case "timeout":
|
|
44
|
+
return "deadline reached";
|
|
45
|
+
case "killed":
|
|
46
|
+
return "stopped";
|
|
47
|
+
case "disposed":
|
|
48
|
+
return "suspended by an OmO shutdown or reload";
|
|
49
|
+
case "agent-exit":
|
|
50
|
+
return "OmO process exited";
|
|
51
|
+
default:
|
|
52
|
+
return "ended";
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function iso(ms) {
|
|
56
|
+
return ms === null ? undefined : new Date(ms).toISOString();
|
|
57
|
+
}
|
|
58
|
+
export function monitorSummary(monitor, ending) {
|
|
59
|
+
const deadline = monitor.persistent ? "none (persistent)" : iso(monitor.deadlineMs);
|
|
60
|
+
return [
|
|
61
|
+
`Status: ${ending ? endingLabel(ending) : monitor.paused ? "paused" : "watching"}`,
|
|
62
|
+
monitor.filter !== null ? `Filter: ${monitor.filter}` : undefined,
|
|
63
|
+
`Events: ${monitor.fireCount}`,
|
|
64
|
+
monitor.lastFiredAtMs !== null ? `Last event: ${iso(monitor.lastFiredAtMs)}` : undefined,
|
|
65
|
+
`Started: ${iso(monitor.startedAtMs)}`,
|
|
66
|
+
ending ? `Ended: ${iso(ending.endedAtMs)}` : deadline !== undefined ? `Deadline: ${deadline}` : undefined,
|
|
67
|
+
]
|
|
68
|
+
.filter((line) => line !== undefined)
|
|
69
|
+
.join("\n");
|
|
70
|
+
}
|
|
71
|
+
export class MonitorSessions {
|
|
72
|
+
sessions;
|
|
73
|
+
emit;
|
|
74
|
+
watches = new Map();
|
|
75
|
+
/** Children the user deleted; a later snapshot of the same watch must not resurrect them. */
|
|
76
|
+
dismissed = new Set();
|
|
77
|
+
constructor(sessions, emit) {
|
|
78
|
+
this.sessions = sessions;
|
|
79
|
+
this.emit = emit;
|
|
80
|
+
}
|
|
81
|
+
wire(parent) {
|
|
82
|
+
parent.hasBackgroundWork = () => this.running(parent.info.id);
|
|
83
|
+
}
|
|
84
|
+
owns(id) {
|
|
85
|
+
return this.watches.has(id);
|
|
86
|
+
}
|
|
87
|
+
messages(id) {
|
|
88
|
+
return this.watches.get(id)?.session.messages ?? [];
|
|
89
|
+
}
|
|
90
|
+
observe(parent, ev) {
|
|
91
|
+
if (!record(ev.data))
|
|
92
|
+
return;
|
|
93
|
+
if (ev.name === MONITOR_STATE_EVENT && Array.isArray(ev.data.monitors))
|
|
94
|
+
this.applyState(parent, ev.data.monitors);
|
|
95
|
+
else if (ev.name === MONITOR_ENDED_EVENT)
|
|
96
|
+
this.applyEnded(parent, ev.data);
|
|
97
|
+
}
|
|
98
|
+
/** The agent process is gone: OmO's in-process watches died with it. */
|
|
99
|
+
stopParent(parentID) {
|
|
100
|
+
for (const watch of this.watches.values()) {
|
|
101
|
+
if (watch.session.info.parentID !== parentID || watch.ending !== undefined)
|
|
102
|
+
continue;
|
|
103
|
+
this.end(watch, { reason: "agent-exit", exitCode: null, endedAtMs: Date.now() });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/** Forget a deleted child, or every child of a deleted parent. */
|
|
107
|
+
remove(id) {
|
|
108
|
+
for (const [childID, watch] of this.watches) {
|
|
109
|
+
if (childID !== id && watch.session.info.parentID !== id)
|
|
110
|
+
continue;
|
|
111
|
+
this.watches.delete(childID);
|
|
112
|
+
this.dismissed.add(childID);
|
|
113
|
+
if (this.sessions.get(childID) !== watch.session)
|
|
114
|
+
continue;
|
|
115
|
+
this.sessions.delete(childID);
|
|
116
|
+
this.emit(watch.session.info.directory, { type: "session.deleted", properties: { info: watch.session.info } });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
running(parentID) {
|
|
120
|
+
for (const watch of this.watches.values()) {
|
|
121
|
+
if (watch.session.info.parentID === parentID && watch.ending === undefined)
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
childID(parent, monitorID) {
|
|
127
|
+
return ocIdFor(`${parent.info.id}/monitor/${monitorID}`);
|
|
128
|
+
}
|
|
129
|
+
applyState(parent, entries) {
|
|
130
|
+
const live = new Set();
|
|
131
|
+
for (const entry of entries) {
|
|
132
|
+
const monitor = parseMonitor(entry);
|
|
133
|
+
if (!monitor)
|
|
134
|
+
continue;
|
|
135
|
+
const id = this.childID(parent, monitor.id);
|
|
136
|
+
live.add(id);
|
|
137
|
+
if (this.dismissed.has(id))
|
|
138
|
+
continue;
|
|
139
|
+
const watch = this.watches.get(id);
|
|
140
|
+
if (watch === undefined)
|
|
141
|
+
this.create(parent, id, monitor);
|
|
142
|
+
else {
|
|
143
|
+
watch.monitor = monitor;
|
|
144
|
+
delete watch.ending;
|
|
145
|
+
this.publish(watch);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// A snapshot lists every live watch; one that is missing has ended. Its reason may follow.
|
|
149
|
+
for (const [id, watch] of this.watches) {
|
|
150
|
+
if (watch.session.info.parentID !== parent.info.id || watch.ending !== undefined || live.has(id))
|
|
151
|
+
continue;
|
|
152
|
+
this.end(watch, { reason: "ended", exitCode: null, endedAtMs: Date.now() });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
applyEnded(parent, data) {
|
|
156
|
+
if (typeof data.id !== "string")
|
|
157
|
+
return;
|
|
158
|
+
const watch = this.watches.get(this.childID(parent, data.id));
|
|
159
|
+
if (!watch)
|
|
160
|
+
return;
|
|
161
|
+
const fireCount = finite(data.fireCount);
|
|
162
|
+
if (fireCount !== null)
|
|
163
|
+
watch.monitor.fireCount = fireCount;
|
|
164
|
+
this.end(watch, {
|
|
165
|
+
reason: typeof data.reason === "string" ? data.reason : "ended",
|
|
166
|
+
exitCode: finite(data.exitCode),
|
|
167
|
+
endedAtMs: finite(data.endedAtMs) ?? Date.now(),
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
end(watch, ending) {
|
|
171
|
+
watch.ending = ending;
|
|
172
|
+
this.publish(watch);
|
|
173
|
+
}
|
|
174
|
+
create(parent, id, monitor) {
|
|
175
|
+
const now = Date.now();
|
|
176
|
+
const session = {
|
|
177
|
+
info: {
|
|
178
|
+
id,
|
|
179
|
+
parentID: parent.info.id,
|
|
180
|
+
projectID: parent.info.projectID,
|
|
181
|
+
directory: parent.info.directory,
|
|
182
|
+
title: `Monitor: ${monitor.description}`,
|
|
183
|
+
version: parent.info.version,
|
|
184
|
+
time: { created: monitor.startedAtMs, updated: now },
|
|
185
|
+
},
|
|
186
|
+
pi: undefined,
|
|
187
|
+
messages: [],
|
|
188
|
+
model: parent.model,
|
|
189
|
+
streaming: undefined,
|
|
190
|
+
status: { type: "idle" },
|
|
191
|
+
toolByCallId: new Map(),
|
|
192
|
+
tree: { messageEntries: {} },
|
|
193
|
+
hasPendingInteraction: () => false,
|
|
194
|
+
hasRunningChildren: () => false,
|
|
195
|
+
};
|
|
196
|
+
const watch = { session, monitor };
|
|
197
|
+
this.watches.set(id, watch);
|
|
198
|
+
this.sessions.set(id, session);
|
|
199
|
+
this.emit(session.info.directory, { type: "session.created", properties: { info: session.info } });
|
|
200
|
+
this.publish(watch);
|
|
201
|
+
}
|
|
202
|
+
publish(watch) {
|
|
203
|
+
const { session, monitor, ending } = watch;
|
|
204
|
+
const directory = session.info.directory;
|
|
205
|
+
const sessionID = session.info.id;
|
|
206
|
+
const base = sessionID.replace(/^ses_/, "");
|
|
207
|
+
const model = session.model ?? { providerID: "omo", modelID: "monitor" };
|
|
208
|
+
const started = monitor.startedAtMs;
|
|
209
|
+
const command = monitor.command ?? monitor.description;
|
|
210
|
+
const input = { command, description: monitor.description };
|
|
211
|
+
const output = monitorSummary(monitor, ending);
|
|
212
|
+
const tool = {
|
|
213
|
+
id: `prt_${base}_tool`,
|
|
214
|
+
sessionID,
|
|
215
|
+
messageID: `msg_${base}_1`,
|
|
216
|
+
type: "tool",
|
|
217
|
+
callID: `call_${base}`,
|
|
218
|
+
tool: "bash",
|
|
219
|
+
state: ending
|
|
220
|
+
? {
|
|
221
|
+
status: "completed",
|
|
222
|
+
input,
|
|
223
|
+
output,
|
|
224
|
+
title: monitor.description,
|
|
225
|
+
metadata: { monitor, ending },
|
|
226
|
+
time: { start: started, end: ending.endedAtMs },
|
|
227
|
+
}
|
|
228
|
+
: {
|
|
229
|
+
status: "running",
|
|
230
|
+
input,
|
|
231
|
+
title: monitor.description,
|
|
232
|
+
metadata: { monitor, output },
|
|
233
|
+
time: { start: started },
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
const messages = [
|
|
237
|
+
{
|
|
238
|
+
info: {
|
|
239
|
+
id: `msg_${base}_0`,
|
|
240
|
+
sessionID,
|
|
241
|
+
role: "user",
|
|
242
|
+
time: { created: started },
|
|
243
|
+
agent: getAgentName(),
|
|
244
|
+
model,
|
|
245
|
+
},
|
|
246
|
+
parts: [
|
|
247
|
+
{
|
|
248
|
+
id: `prt_${base}_request`,
|
|
249
|
+
sessionID,
|
|
250
|
+
messageID: `msg_${base}_0`,
|
|
251
|
+
type: "text",
|
|
252
|
+
text: "The agent started a background monitor",
|
|
253
|
+
synthetic: true,
|
|
254
|
+
time: { start: started, end: started },
|
|
255
|
+
},
|
|
256
|
+
],
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
info: {
|
|
260
|
+
id: `msg_${base}_1`,
|
|
261
|
+
sessionID,
|
|
262
|
+
role: "assistant",
|
|
263
|
+
time: { created: started, ...(ending ? { completed: ending.endedAtMs } : {}) },
|
|
264
|
+
parentID: `msg_${base}_0`,
|
|
265
|
+
modelID: model.modelID,
|
|
266
|
+
providerID: model.providerID,
|
|
267
|
+
mode: getAgentName(),
|
|
268
|
+
path: { cwd: directory, root: directory },
|
|
269
|
+
cost: 0,
|
|
270
|
+
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
|
271
|
+
...(ending ? { finish: "stop" } : {}),
|
|
272
|
+
},
|
|
273
|
+
parts: [tool],
|
|
274
|
+
},
|
|
275
|
+
];
|
|
276
|
+
const before = new Map((session.messages ?? []).map((message) => [message.info.id, JSON.stringify(message)]));
|
|
277
|
+
session.messages = messages;
|
|
278
|
+
for (const message of messages) {
|
|
279
|
+
if (before.get(message.info.id) === JSON.stringify(message))
|
|
280
|
+
continue;
|
|
281
|
+
this.emit(directory, { type: "message.updated", properties: { info: message.info } });
|
|
282
|
+
for (const part of message.parts)
|
|
283
|
+
this.emit(directory, { type: "message.part.updated", properties: { part } });
|
|
284
|
+
}
|
|
285
|
+
const status = ending ? "idle" : "busy";
|
|
286
|
+
if (session.status.type === status)
|
|
287
|
+
return;
|
|
288
|
+
session.status = { type: status };
|
|
289
|
+
session.info.time.updated = Date.now();
|
|
290
|
+
this.emit(directory, { type: "session.updated", properties: { info: session.info } });
|
|
291
|
+
this.emit(directory, { type: "session.status", properties: { sessionID, status: session.status } });
|
|
292
|
+
if (status === "idle")
|
|
293
|
+
this.emit(directory, { type: "session.idle", properties: { sessionID } });
|
|
294
|
+
}
|
|
295
|
+
}
|
|
@@ -3,6 +3,7 @@ import type { OCAssistantMessage, OCMessageWithParts, OCModelRef, OCEvent, OCSes
|
|
|
3
3
|
import type { ManagedSession } from "./sessions.js";
|
|
4
4
|
import type { SessionStore } from "./session-store.js";
|
|
5
5
|
import type { SubagentBridge } from "./subagents.js";
|
|
6
|
+
import type { MonitorSessions } from "./monitor-sessions.js";
|
|
6
7
|
import { type TodoUpdated } from "./todo-bridge.js";
|
|
7
8
|
export interface PiUsage {
|
|
8
9
|
input?: number;
|
|
@@ -38,6 +39,7 @@ export declare function compactionMessages(s: ManagedSession, key: string, summa
|
|
|
38
39
|
interface PiEventServices {
|
|
39
40
|
readonly emit: (directory: string, event: OCEvent | TodoUpdated) => void;
|
|
40
41
|
readonly subagents: SubagentBridge;
|
|
42
|
+
readonly monitors: MonitorSessions;
|
|
41
43
|
readonly store: Pick<SessionStore, "update">;
|
|
42
44
|
readonly dirtyDiffs: Set<string>;
|
|
43
45
|
readonly ensureMessages: (s: ManagedSession) => OCMessageWithParts[];
|
|
@@ -284,6 +284,9 @@ export class PiEventTranslator {
|
|
|
284
284
|
case "compaction_end":
|
|
285
285
|
this.onCompactionEvent(s, ev);
|
|
286
286
|
break;
|
|
287
|
+
case "extension_event":
|
|
288
|
+
this.services.monitors.observe(s, ev);
|
|
289
|
+
break;
|
|
287
290
|
default:
|
|
288
291
|
break; // turn_start, queue_update — future milestones
|
|
289
292
|
}
|
package/dist/pi-rpc.js
CHANGED
|
@@ -93,6 +93,8 @@ export class PiRpcClient extends EventEmitter {
|
|
|
93
93
|
...(process.env.SENPI_RPC_CLIENT_CAPABILITIES ?? "").split(","),
|
|
94
94
|
"auto_title_sessions",
|
|
95
95
|
"question",
|
|
96
|
+
// Extension `pi.rpc.emit` frames, e.g. the terminal extension's monitor state.
|
|
97
|
+
"extension_events",
|
|
96
98
|
]
|
|
97
99
|
.map((value) => value.trim())
|
|
98
100
|
.filter(Boolean)),
|
package/dist/route-services.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type { Catalog } from "./catalog.js";
|
|
|
5
5
|
import type { ModelSelection } from "./thinking-levels.js";
|
|
6
6
|
import type { OCEvent, OCMessageWithParts, OCPromptPart } from "./types.js";
|
|
7
7
|
import type { TodoUpdated } from "./todo-bridge.js";
|
|
8
|
+
import type { SelfUpdateResult } from "./self-update.js";
|
|
8
9
|
export interface RouteServices {
|
|
9
10
|
readonly sessions: SessionManager;
|
|
10
11
|
readonly defaultDirectory: string;
|
|
@@ -12,6 +13,7 @@ export interface RouteServices {
|
|
|
12
13
|
readonly body: (req: IncomingMessage) => Promise<unknown>;
|
|
13
14
|
readonly catalog: () => Promise<Catalog>;
|
|
14
15
|
readonly broadcast: (directory: string, event: OCEvent | TodoUpdated) => void;
|
|
16
|
+
readonly selfUpdate: () => Promise<SelfUpdateResult>;
|
|
15
17
|
}
|
|
16
18
|
export declare const OPENCODE_COMPAT_VERSION: string | null;
|
|
17
19
|
export declare function numberParam(value: string | null): number | undefined;
|
package/dist/routes-v1.js
CHANGED
|
@@ -3,9 +3,11 @@ import { execFile } from "node:child_process";
|
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { errorBody, json } from "./http.js";
|
|
5
5
|
import { miscRoute } from "./misc-routes.js";
|
|
6
|
+
import { mcpRoute } from "./mcp.js";
|
|
6
7
|
import { getAgentName } from "./agent-name.js";
|
|
7
8
|
import { nativePromptInput, parsePromptParts } from "./prompt-parts.js";
|
|
8
9
|
import { RequestBodyError } from "./http.js";
|
|
10
|
+
import { SelfUpdateUnsupportedError } from "./self-update.js";
|
|
9
11
|
import { announceSessionError, compactSession, getCommands, unsupportedShare } from "./session-commands.js";
|
|
10
12
|
import { agentDescriptors, modelRefFromBody, numberParam, OPENCODE_COMPAT_VERSION, promptAsync, runSessionShell, runSessionSlash, sessionDirectory, sessionMetadata, } from "./route-services.js";
|
|
11
13
|
/**
|
|
@@ -19,13 +21,26 @@ function applyArchivedFilter(sessions, archived) {
|
|
|
19
21
|
return sessions.filter((s) => s.time.archived === undefined);
|
|
20
22
|
return sessions;
|
|
21
23
|
}
|
|
22
|
-
function handleV1GlobalRoute(res, path, method) {
|
|
24
|
+
async function handleV1GlobalRoute(services, res, path, method) {
|
|
23
25
|
if (method === "GET" && path === "/global/health") {
|
|
24
26
|
json(res, 200, OPENCODE_COMPAT_VERSION === null ? { healthy: true } : { healthy: true, version: OPENCODE_COMPAT_VERSION });
|
|
25
27
|
return true;
|
|
26
28
|
}
|
|
27
|
-
if (
|
|
28
|
-
|
|
29
|
+
if (method === "POST" && path === "/global/upgrade") {
|
|
30
|
+
// The requested OpenCode `target` does not apply: this upgrades antechamber itself.
|
|
31
|
+
try {
|
|
32
|
+
const result = await services.selfUpdate();
|
|
33
|
+
json(res, 200, { success: true, version: result.to, updated: result.updated });
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
if (!(error instanceof SelfUpdateUnsupportedError))
|
|
37
|
+
throw error;
|
|
38
|
+
json(res, 409, errorBody("UnsupportedError", error.message));
|
|
39
|
+
}
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
if (method === "GET" && path === "/global/upgrade") {
|
|
43
|
+
json(res, 409, errorBody("UnsupportedError", "Upgrade antechamber with POST /global/upgrade."));
|
|
29
44
|
return true;
|
|
30
45
|
}
|
|
31
46
|
return false;
|
|
@@ -47,8 +62,13 @@ function parseV1Prompt(body, defaultModel) {
|
|
|
47
62
|
return { text, model, parts, delivery: body.delivery ?? "steer" };
|
|
48
63
|
}
|
|
49
64
|
export async function handleV1(services, req, res, url, path, method, directory) {
|
|
50
|
-
if (handleV1GlobalRoute(res, path, method))
|
|
65
|
+
if (await handleV1GlobalRoute(services, res, path, method))
|
|
51
66
|
return;
|
|
67
|
+
const mcp = await mcpRoute(services, method, path, directory, () => services.body(req));
|
|
68
|
+
if (mcp !== undefined) {
|
|
69
|
+
json(res, mcp.status, mcp.body);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
52
72
|
// --- sessions ---
|
|
53
73
|
const misc = await miscRoute(method, path, url, directory);
|
|
54
74
|
if (misc !== undefined) {
|
|
@@ -362,10 +382,6 @@ export async function handleV1(services, req, res, url, path, method, directory)
|
|
|
362
382
|
json(res, 200, []);
|
|
363
383
|
return;
|
|
364
384
|
}
|
|
365
|
-
if (path === "/mcp" && method === "GET") {
|
|
366
|
-
json(res, 200, {});
|
|
367
|
-
return;
|
|
368
|
-
}
|
|
369
385
|
if (path === "/command" && method === "GET") {
|
|
370
386
|
json(res, 200, (await getCommands(directory)).map((command) => ({
|
|
371
387
|
...command,
|
package/dist/routes-v2.js
CHANGED
|
@@ -6,6 +6,7 @@ import { promisify } from "node:util";
|
|
|
6
6
|
import { errorBody, json, RequestBodyError } from "./http.js";
|
|
7
7
|
import { nativePromptInput, parsePromptParts, v2PromptAttachments, v2PromptParts } from "./prompt-parts.js";
|
|
8
8
|
import { debugLog, miscRoute } from "./misc-routes.js";
|
|
9
|
+
import { mcpRouteV2 } from "./mcp.js";
|
|
9
10
|
import { OPENCODE_API_VERSION } from "./version.js";
|
|
10
11
|
import { toV2Permission } from "./approvals.js";
|
|
11
12
|
import { formQuestionAnswer, questionFormID, toQuestionForm } from "./question-forms.js";
|
|
@@ -192,8 +193,12 @@ export async function handleV2(services, req, res, url, path, method, directory)
|
|
|
192
193
|
res.writeHead(204).end();
|
|
193
194
|
return;
|
|
194
195
|
}
|
|
195
|
-
|
|
196
|
-
|
|
196
|
+
const mcp = await mcpRouteV2(services, method, path, directory, () => services.body(req));
|
|
197
|
+
if (mcp !== undefined) {
|
|
198
|
+
if (mcp.status === 204)
|
|
199
|
+
res.writeHead(204).end();
|
|
200
|
+
else
|
|
201
|
+
json(res, mcp.status, mcp.body);
|
|
197
202
|
return;
|
|
198
203
|
}
|
|
199
204
|
const integrationMatch = path.match(/^\/api\/integration\/([^/]+)$/);
|
|
@@ -226,9 +231,10 @@ export async function handleV2(services, req, res, url, path, method, directory)
|
|
|
226
231
|
});
|
|
227
232
|
return;
|
|
228
233
|
}
|
|
229
|
-
// Read-only inventories OpenChamber loads at bootstrap
|
|
230
|
-
// web-search providers
|
|
231
|
-
|
|
234
|
+
// Read-only inventories OpenChamber loads at bootstrap. OmO has no OpenCode-style shells or
|
|
235
|
+
// web-search providers, so each is an honest empty list (mutations stay 400);
|
|
236
|
+
// OmO monitors appear as child sessions instead (monitor-sessions.ts).
|
|
237
|
+
if ((path === "/api/shell" || path === "/api/websearch/provider") && method === "GET") {
|
|
232
238
|
json(res, 200, withLocation([]));
|
|
233
239
|
return;
|
|
234
240
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export type InstallMethod = "npm" | "bun";
|
|
2
|
+
export declare class SelfUpdateUnsupportedError extends Error {
|
|
3
|
+
readonly name = "SelfUpdateUnsupportedError";
|
|
4
|
+
}
|
|
5
|
+
export interface InstallLocation {
|
|
6
|
+
readonly packageRoot: string;
|
|
7
|
+
readonly env: NodeJS.ProcessEnv;
|
|
8
|
+
readonly home: string;
|
|
9
|
+
readonly realpath: (path: string) => string;
|
|
10
|
+
}
|
|
11
|
+
export interface SelfUpdateDeps extends InstallLocation {
|
|
12
|
+
readonly npmBinary: string;
|
|
13
|
+
readonly bunBinary: string;
|
|
14
|
+
readonly npmRoot: () => Promise<string>;
|
|
15
|
+
readonly currentVersion: string;
|
|
16
|
+
readonly latestVersion: () => Promise<string>;
|
|
17
|
+
readonly install: (binary: string, args: readonly string[]) => Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
export interface SelfUpdateResult {
|
|
20
|
+
readonly updated: boolean;
|
|
21
|
+
readonly from: string;
|
|
22
|
+
readonly to: string;
|
|
23
|
+
readonly method: InstallMethod;
|
|
24
|
+
}
|
|
25
|
+
export declare function installMethod(location: InstallLocation, npmRoot: () => string): InstallMethod;
|
|
26
|
+
export declare function isNewerVersion(a: string, b: string): boolean;
|
|
27
|
+
export declare function fetchLatestVersion(env?: NodeJS.ProcessEnv): Promise<string>;
|
|
28
|
+
/** Prefers the npm shipped beside the running node: service PATHs often lack the user's tool directories. */
|
|
29
|
+
export declare function npmBinaryFor(execPath?: string): string;
|
|
30
|
+
export declare function defaultSelfUpdateDeps(env?: NodeJS.ProcessEnv, home?: string): SelfUpdateDeps;
|
|
31
|
+
/** Installs the latest release when it is newer; concurrent callers in this process share one attempt. */
|
|
32
|
+
export declare function selfUpdate(deps?: SelfUpdateDeps): Promise<SelfUpdateResult>;
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/** Upgrades a global npm or bun install of antechamber to the latest published release. */
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { basename, dirname, join } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { ADAPTER_VERSION } from "./version.js";
|
|
8
|
+
export class SelfUpdateUnsupportedError extends Error {
|
|
9
|
+
name = "SelfUpdateUnsupportedError";
|
|
10
|
+
}
|
|
11
|
+
const INSTALL_TIMEOUT_MS = 5 * 60_000;
|
|
12
|
+
const VERSION_PATTERN = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/;
|
|
13
|
+
function tryRealpath(path, location) {
|
|
14
|
+
try {
|
|
15
|
+
return location.realpath(path);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function bunInstallDir(location) {
|
|
22
|
+
return location.env.BUN_INSTALL ?? join(location.home, ".bun");
|
|
23
|
+
}
|
|
24
|
+
function locate(location) {
|
|
25
|
+
const root = tryRealpath(location.packageRoot, location) ?? location.packageRoot;
|
|
26
|
+
if (basename(root) !== "antechamber" || basename(dirname(root)) !== "node_modules")
|
|
27
|
+
throw notGlobal(root);
|
|
28
|
+
const bunPackage = join(bunInstallDir(location), "install", "global", "node_modules", "antechamber");
|
|
29
|
+
return { root, bun: tryRealpath(bunPackage, location) === root };
|
|
30
|
+
}
|
|
31
|
+
function notGlobal(root) {
|
|
32
|
+
return new SelfUpdateUnsupportedError(`${root} is not a global npm or bun install of antechamber; update it the way it was installed (source checkout: git pull && npm ci && npm run build)`);
|
|
33
|
+
}
|
|
34
|
+
function npmOwns(location, root, npmGlobal) {
|
|
35
|
+
const global = npmGlobal.trim();
|
|
36
|
+
if (global !== "" && tryRealpath(join(global, "antechamber"), location) === root)
|
|
37
|
+
return "npm";
|
|
38
|
+
throw notGlobal(root);
|
|
39
|
+
}
|
|
40
|
+
function npmUnavailable(error) {
|
|
41
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
42
|
+
return new SelfUpdateUnsupportedError(`cannot run npm root -g to confirm a global npm install (${reason})`);
|
|
43
|
+
}
|
|
44
|
+
export function installMethod(location, npmRoot) {
|
|
45
|
+
const { root, bun } = locate(location);
|
|
46
|
+
if (bun)
|
|
47
|
+
return "bun";
|
|
48
|
+
let global;
|
|
49
|
+
try {
|
|
50
|
+
global = npmRoot();
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
throw npmUnavailable(error);
|
|
54
|
+
}
|
|
55
|
+
return npmOwns(location, root, global);
|
|
56
|
+
}
|
|
57
|
+
async function installMethodAsync(deps) {
|
|
58
|
+
const { root, bun } = locate(deps);
|
|
59
|
+
if (bun)
|
|
60
|
+
return "bun";
|
|
61
|
+
let global;
|
|
62
|
+
try {
|
|
63
|
+
global = await deps.npmRoot();
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
throw npmUnavailable(error);
|
|
67
|
+
}
|
|
68
|
+
return npmOwns(deps, root, global);
|
|
69
|
+
}
|
|
70
|
+
export function isNewerVersion(a, b) {
|
|
71
|
+
const left = VERSION_PATTERN.exec(a);
|
|
72
|
+
const right = VERSION_PATTERN.exec(b);
|
|
73
|
+
if (left === null || right === null)
|
|
74
|
+
return false;
|
|
75
|
+
for (let index = 1; index <= 3; index += 1) {
|
|
76
|
+
const diff = Number(left[index]) - Number(right[index]);
|
|
77
|
+
if (diff !== 0)
|
|
78
|
+
return diff > 0;
|
|
79
|
+
}
|
|
80
|
+
if (left[4] === right[4])
|
|
81
|
+
return false;
|
|
82
|
+
if (left[4] === undefined)
|
|
83
|
+
return true;
|
|
84
|
+
if (right[4] === undefined)
|
|
85
|
+
return false;
|
|
86
|
+
const leftParts = left[4].split(".");
|
|
87
|
+
const rightParts = right[4].split(".");
|
|
88
|
+
for (let index = 0; index < Math.max(leftParts.length, rightParts.length); index += 1) {
|
|
89
|
+
const l = leftParts[index];
|
|
90
|
+
const r = rightParts[index];
|
|
91
|
+
if (l === undefined)
|
|
92
|
+
return false;
|
|
93
|
+
if (r === undefined)
|
|
94
|
+
return true;
|
|
95
|
+
if (l === r)
|
|
96
|
+
continue;
|
|
97
|
+
const ln = /^\d+$/.test(l) ? Number(l) : undefined;
|
|
98
|
+
const rn = /^\d+$/.test(r) ? Number(r) : undefined;
|
|
99
|
+
if (ln !== undefined && rn !== undefined)
|
|
100
|
+
return ln > rn;
|
|
101
|
+
if (ln !== undefined)
|
|
102
|
+
return false;
|
|
103
|
+
if (rn !== undefined)
|
|
104
|
+
return true;
|
|
105
|
+
return l > r;
|
|
106
|
+
}
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
export async function fetchLatestVersion(env = process.env) {
|
|
110
|
+
const url = new URL(`${(env.npm_config_registry ?? "https://registry.npmjs.org").replace(/\/+$/, "")}/antechamber/latest`);
|
|
111
|
+
// Registry URLs can embed a token; fetch rejects them and its error would echo the URL.
|
|
112
|
+
url.username = "";
|
|
113
|
+
url.password = "";
|
|
114
|
+
let response;
|
|
115
|
+
try {
|
|
116
|
+
response = await fetch(url, { headers: { Accept: "application/json" }, signal: AbortSignal.timeout(10_000) });
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
throw new Error(`cannot reach the npm registry at ${url.host}`);
|
|
120
|
+
}
|
|
121
|
+
if (!response.ok)
|
|
122
|
+
throw new Error(`npm registry at ${url.host} answered ${response.status} for antechamber`);
|
|
123
|
+
const payload = await response.json().catch(() => undefined);
|
|
124
|
+
const version = typeof payload === "object" && payload !== null && "version" in payload ? String(payload.version) : "";
|
|
125
|
+
if (!VERSION_PATTERN.test(version))
|
|
126
|
+
throw new Error("npm registry returned no valid antechamber version");
|
|
127
|
+
return version;
|
|
128
|
+
}
|
|
129
|
+
function run(binary, args, env, timeout) {
|
|
130
|
+
return new Promise((resolve, reject) => {
|
|
131
|
+
const child = execFile(binary, [...args], { env, timeout, maxBuffer: 4 * 1024 * 1024 }, (error, stdout) => {
|
|
132
|
+
// Installer output can contain registry credentials, so it is never echoed.
|
|
133
|
+
if (error)
|
|
134
|
+
reject(new Error(`${basename(binary)} ${args.join(" ")} failed; run it in a terminal for details`));
|
|
135
|
+
else
|
|
136
|
+
resolve(stdout);
|
|
137
|
+
});
|
|
138
|
+
// Package managers must see EOF instead of waiting for interactive input.
|
|
139
|
+
child.stdin?.end();
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
/** Prefers the npm shipped beside the running node: service PATHs often lack the user's tool directories. */
|
|
143
|
+
export function npmBinaryFor(execPath = process.execPath) {
|
|
144
|
+
const sibling = join(dirname(execPath), process.platform === "win32" ? "npm.cmd" : "npm");
|
|
145
|
+
return existsSync(sibling) ? sibling : "npm";
|
|
146
|
+
}
|
|
147
|
+
function bunBinaryFor(env, home) {
|
|
148
|
+
const installed = join(bunInstallDir({ env, home }), "bin", "bun");
|
|
149
|
+
return existsSync(installed) ? installed : "bun";
|
|
150
|
+
}
|
|
151
|
+
export function defaultSelfUpdateDeps(env = process.env, home = homedir()) {
|
|
152
|
+
const npmBinary = npmBinaryFor();
|
|
153
|
+
return {
|
|
154
|
+
// src/self-update.ts (tsx) and dist/self-update.js both sit one level below the package root.
|
|
155
|
+
packageRoot: fileURLToPath(new URL("..", import.meta.url)),
|
|
156
|
+
env,
|
|
157
|
+
home,
|
|
158
|
+
realpath: realpathSync,
|
|
159
|
+
npmBinary,
|
|
160
|
+
bunBinary: bunBinaryFor(env, home),
|
|
161
|
+
npmRoot: () => run(npmBinary, ["root", "-g"], env, 10_000),
|
|
162
|
+
currentVersion: ADAPTER_VERSION,
|
|
163
|
+
latestVersion: () => fetchLatestVersion(env),
|
|
164
|
+
install: async (binary, args) => {
|
|
165
|
+
await run(binary, args, env, INSTALL_TIMEOUT_MS);
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
let inFlight;
|
|
170
|
+
/** Installs the latest release when it is newer; concurrent callers in this process share one attempt. */
|
|
171
|
+
export function selfUpdate(deps = defaultSelfUpdateDeps()) {
|
|
172
|
+
inFlight ??= (async () => {
|
|
173
|
+
const method = await installMethodAsync(deps);
|
|
174
|
+
const latest = await deps.latestVersion();
|
|
175
|
+
const from = deps.currentVersion;
|
|
176
|
+
if (!isNewerVersion(latest, from))
|
|
177
|
+
return { updated: false, from, to: from, method };
|
|
178
|
+
const spec = `antechamber@${latest}`;
|
|
179
|
+
if (method === "npm")
|
|
180
|
+
await deps.install(deps.npmBinary, ["install", "-g", spec]);
|
|
181
|
+
else
|
|
182
|
+
await deps.install(deps.bunBinary, ["add", "-g", spec]);
|
|
183
|
+
return { updated: true, from, to: latest, method };
|
|
184
|
+
})().finally(() => {
|
|
185
|
+
inFlight = undefined;
|
|
186
|
+
});
|
|
187
|
+
return inFlight;
|
|
188
|
+
}
|