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.
@@ -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)),
@@ -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 ((method === "POST" || method === "GET") && path === "/global/upgrade") {
28
- json(res, 409, errorBody("UnsupportedError", "This endpoint is served by antechamber (a pi adapter), not a real opencode install — there is nothing to upgrade."));
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
- if (path === "/api/integration" && method === "GET") {
196
- json(res, 200, withLocation([]));
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: OmO has no background shells,
230
- // web-search providers or MCP servers, so each is an honest empty list (mutations stay 400).
231
- if ((path === "/api/shell" || path === "/api/websearch/provider" || path === "/api/mcp") && method === "GET") {
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
+ }