antechamber 1.0.40 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +126 -0
- package/README.md +88 -141
- package/bin/opencode-antechamber +14 -4
- package/dist/approvals.d.ts +9 -1
- package/dist/approvals.js +10 -2
- package/dist/catalog.d.ts +13 -2
- package/dist/catalog.js +88 -52
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +1 -1
- package/dist/commands/context.d.ts +5 -2
- package/dist/commands/context.js +8 -4
- package/dist/commands/doctor.js +4 -3
- package/dist/commands/setup.js +9 -28
- package/dist/config.d.ts +6 -2
- package/dist/config.js +18 -4
- package/dist/file-watch.d.ts +35 -0
- package/dist/file-watch.js +163 -0
- package/dist/latency.d.ts +10 -0
- package/dist/latency.js +115 -0
- package/dist/misc-routes.js +14 -12
- package/dist/pi-event-translation.js +44 -2
- package/dist/route-services.d.ts +8 -3
- package/dist/routes-v1.d.ts +1 -1
- package/dist/routes-v1.js +29 -418
- package/dist/routes-v2.js +252 -34
- package/dist/self-update.d.ts +2 -1
- package/dist/self-update.js +5 -1
- package/dist/server.d.ts +17 -3
- package/dist/server.js +191 -18
- package/dist/session-commands.d.ts +3 -1
- package/dist/session-commands.js +13 -7
- package/dist/session-history.js +3 -0
- package/dist/session-store.d.ts +19 -10
- package/dist/session-store.js +266 -128
- package/dist/session-tree.d.ts +15 -1
- package/dist/session-tree.js +2 -2
- package/dist/sessions.d.ts +85 -1
- package/dist/sessions.js +238 -10
- package/dist/sse.d.ts +0 -2
- package/dist/sse.js +13 -46
- package/dist/text-generation.d.ts +20 -0
- package/dist/text-generation.js +93 -0
- package/dist/tool-map.js +1 -3
- package/dist/types.d.ts +60 -0
- package/dist/v2.d.ts +8 -1
- package/dist/v2.js +24 -4
- package/dist/work-status.js +1 -1
- package/extensions/antechamber/README.md +100 -0
- package/extensions/antechamber/bin/opencode-antechamber +42 -0
- package/extensions/{omo-dag/docs/DESIGN.md → antechamber/docs/GRAPH.md} +5 -5
- package/extensions/{omo-work/docs/DESIGN.md → antechamber/docs/WORK.md} +3 -3
- package/extensions/antechamber/package.json +60 -0
- package/extensions/{omo-dag → antechamber}/panel/index.html +8 -1
- package/extensions/antechamber/panel/main.js +261 -0
- package/extensions/antechamber/runtime/launcher.js +201 -0
- package/extensions/antechamber/service/main.js +884 -0
- package/package.json +75 -26
- package/extensions/omo-compat/README.md +0 -48
- package/extensions/omo-compat/icon.svg +0 -1
- package/extensions/omo-compat/package.json +0 -43
- package/extensions/omo-dag/README.md +0 -167
- package/extensions/omo-dag/package.json +0 -33
- package/extensions/omo-dag/panel/main.js +0 -176
- package/extensions/omo-dag/service/main.js +0 -457
- package/extensions/omo-work/README.md +0 -88
- package/extensions/omo-work/package.json +0 -30
- package/extensions/omo-work/service/main.js +0 -149
- /package/extensions/{omo-dag → antechamber}/icon.svg +0 -0
- /package/extensions/{omo-work → antechamber}/status/index.html +0 -0
- /package/extensions/{omo-work → antechamber}/status/main.js +0 -0
package/dist/catalog.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Model/provider catalog: obtained from a throwaway `pi --mode rpc` process
|
|
3
3
|
* (`get_available_models` + `get_state`), translated into OpenCode v2
|
|
4
|
-
* Provider/Model shapes,
|
|
4
|
+
* Provider/Model shapes, refreshed from live agents or bounded cold reads.
|
|
5
5
|
*/
|
|
6
6
|
import { PiRpcClient } from "./pi-rpc.js";
|
|
7
7
|
import { thinkingVariants } from "./thinking-levels.js";
|
|
@@ -44,24 +44,95 @@ function toOCModel(m) {
|
|
|
44
44
|
...(variants !== undefined ? { variants } : {}),
|
|
45
45
|
};
|
|
46
46
|
}
|
|
47
|
-
|
|
48
|
-
const
|
|
47
|
+
function inventory(res) {
|
|
48
|
+
const providers = new Map();
|
|
49
|
+
if (res.success && res.data) {
|
|
50
|
+
for (const m of res.data.models) {
|
|
51
|
+
let p = providers.get(m.provider);
|
|
52
|
+
if (!p) {
|
|
53
|
+
p = { id: m.provider, name: m.provider, source: "config", env: [], options: {}, models: {} };
|
|
54
|
+
providers.set(m.provider, p);
|
|
55
|
+
}
|
|
56
|
+
p.models[m.id] = toOCModel(m);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return providers;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Apply the default to the inventory: list it even if get_available_models omitted it (the client drops a
|
|
63
|
+
* selection it cannot find), and put it first, because OpenChamber's picker defaults to the first listed
|
|
64
|
+
* provider/model. `active` is a model from a fresh agent's get_state, or the previous snapshot's default.
|
|
65
|
+
*/
|
|
66
|
+
function withDefault(providers, active, thinkingLevel) {
|
|
67
|
+
const defaults = {};
|
|
68
|
+
if (active !== null) {
|
|
69
|
+
defaults[active.providerID] = active.modelID;
|
|
70
|
+
let p = providers.get(active.providerID);
|
|
71
|
+
if (p === undefined) {
|
|
72
|
+
p = { id: active.providerID, name: active.providerID, source: "config", env: [], options: {}, models: {} };
|
|
73
|
+
providers.set(active.providerID, p);
|
|
74
|
+
}
|
|
75
|
+
if (p.models[active.modelID] === undefined)
|
|
76
|
+
p.models[active.modelID] = active.model;
|
|
77
|
+
}
|
|
78
|
+
const sorted = [...providers.values()].sort((a, b) => {
|
|
79
|
+
const aDef = defaults[a.id] !== undefined ? 0 : 1;
|
|
80
|
+
const bDef = defaults[b.id] !== undefined ? 0 : 1;
|
|
81
|
+
return aDef - bDef;
|
|
82
|
+
});
|
|
83
|
+
for (const p of sorted) {
|
|
84
|
+
const def = defaults[p.id];
|
|
85
|
+
if (!def)
|
|
86
|
+
continue;
|
|
87
|
+
const entries = Object.entries(p.models).sort(([a], [b]) => (a === def ? -1 : b === def ? 1 : 0));
|
|
88
|
+
p.models = Object.fromEntries(entries);
|
|
89
|
+
}
|
|
90
|
+
// exactOptionalPropertyTypes: omit `default` rather than set it undefined.
|
|
91
|
+
if (active === null)
|
|
92
|
+
return { providers: sorted, defaults };
|
|
93
|
+
const resolvedDefault = { providerID: active.providerID, modelID: active.modelID };
|
|
94
|
+
// Only a level the catalog offers as a variant is selectable in the picker.
|
|
95
|
+
const offered = sorted.find((p) => p.id === active.providerID)?.models[active.modelID]?.variants;
|
|
96
|
+
return thinkingLevel !== undefined && offered?.[thinkingLevel] !== undefined
|
|
97
|
+
? { providers: sorted, defaults, default: resolvedDefault, defaultThinkingLevel: thinkingLevel }
|
|
98
|
+
: { providers: sorted, defaults, default: resolvedDefault };
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* The configured default plus the provider/model inventory in a fixed order, so a reordering alone is not
|
|
102
|
+
* reported as a catalog change but a new default is.
|
|
103
|
+
*/
|
|
104
|
+
export function catalogFingerprint(catalog) {
|
|
105
|
+
return JSON.stringify([
|
|
106
|
+
catalog.default ?? null,
|
|
107
|
+
catalog.defaultThinkingLevel ?? null,
|
|
108
|
+
[...catalog.providers]
|
|
109
|
+
.sort((a, b) => a.id.localeCompare(b.id))
|
|
110
|
+
.map((p) => [
|
|
111
|
+
p.id,
|
|
112
|
+
Object.keys(p.models)
|
|
113
|
+
.sort()
|
|
114
|
+
.map((id) => [id, p.models[id]]),
|
|
115
|
+
]),
|
|
116
|
+
]);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* `configured` is the previous snapshot when `client` is a live session's agent: that agent's `get_state.model`
|
|
120
|
+
* is the session's own selection, not the configured default, so only the model inventory is re-read and the
|
|
121
|
+
* snapshot's default is kept. Only a fresh `--no-session` agent defines the default.
|
|
122
|
+
*/
|
|
123
|
+
export async function loadCatalog(directory, client, configured) {
|
|
124
|
+
const pi = client ?? new PiRpcClient({ cwd: directory, args: ["--no-session"] });
|
|
49
125
|
try {
|
|
126
|
+
if (configured !== undefined) {
|
|
127
|
+
const modelsRes = await pi.command({ type: "get_available_models" });
|
|
128
|
+
const def = configured.default;
|
|
129
|
+
const model = def && configured.providers.find((p) => p.id === def.providerID)?.models[def.modelID];
|
|
130
|
+
return withDefault(inventory(modelsRes), def && model ? { ...def, model } : null, configured.defaultThinkingLevel);
|
|
131
|
+
}
|
|
50
132
|
const [modelsRes, firstState] = await Promise.all([
|
|
51
133
|
pi.command({ type: "get_available_models" }),
|
|
52
134
|
pi.command({ type: "get_state" }),
|
|
53
135
|
]);
|
|
54
|
-
const providers = new Map();
|
|
55
|
-
if (modelsRes.success && modelsRes.data) {
|
|
56
|
-
for (const m of modelsRes.data.models) {
|
|
57
|
-
let p = providers.get(m.provider);
|
|
58
|
-
if (!p) {
|
|
59
|
-
p = { id: m.provider, name: m.provider, source: "config", env: [], options: {}, models: {} };
|
|
60
|
-
providers.set(m.provider, p);
|
|
61
|
-
}
|
|
62
|
-
p.models[m.id] = toOCModel(m);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
136
|
// get_state occasionally races pi's own startup. One retry beats resolving
|
|
66
137
|
// no active model, which is precisely what leaves the UI picker guessing.
|
|
67
138
|
let active = firstState.success ? (firstState.data?.model ?? null) : null;
|
|
@@ -74,45 +145,10 @@ export async function loadCatalog(directory) {
|
|
|
74
145
|
if (active === null) {
|
|
75
146
|
console.error("[catalog] warning: pi reported no active model — /api/model/default will be null and the UI picker falls back to its own default");
|
|
76
147
|
}
|
|
77
|
-
|
|
78
|
-
let resolvedDefault;
|
|
79
|
-
if (active !== null) {
|
|
80
|
-
defaults[active.provider] = active.id;
|
|
81
|
-
resolvedDefault = { providerID: active.provider, modelID: active.id };
|
|
82
|
-
// The client drops any selection it cannot find in this catalog and picks
|
|
83
|
-
// something else instead, so a model get_available_models omitted still
|
|
84
|
-
// has to be listed here — otherwise the active model is unselectable.
|
|
85
|
-
let p = providers.get(active.provider);
|
|
86
|
-
if (p === undefined) {
|
|
87
|
-
p = { id: active.provider, name: active.provider, source: "config", env: [], options: {}, models: {} };
|
|
88
|
-
providers.set(active.provider, p);
|
|
89
|
-
}
|
|
90
|
-
if (p.models[active.id] === undefined)
|
|
91
|
-
p.models[active.id] = toOCModel(active);
|
|
92
|
-
}
|
|
93
|
-
// OpenChamber's picker defaults to the first listed provider/model — put pi's active config there.
|
|
94
|
-
const sorted = [...providers.values()].sort((a, b) => {
|
|
95
|
-
const aDef = defaults[a.id] !== undefined ? 0 : 1;
|
|
96
|
-
const bDef = defaults[b.id] !== undefined ? 0 : 1;
|
|
97
|
-
return aDef - bDef;
|
|
98
|
-
});
|
|
99
|
-
for (const p of sorted) {
|
|
100
|
-
const def = defaults[p.id];
|
|
101
|
-
if (!def)
|
|
102
|
-
continue;
|
|
103
|
-
const entries = Object.entries(p.models).sort(([a], [b]) => (a === def ? -1 : b === def ? 1 : 0));
|
|
104
|
-
p.models = Object.fromEntries(entries);
|
|
105
|
-
}
|
|
106
|
-
// exactOptionalPropertyTypes: omit `default` rather than set it undefined.
|
|
107
|
-
if (resolvedDefault === undefined)
|
|
108
|
-
return { providers: sorted, defaults };
|
|
109
|
-
// Only a level the catalog offers as a variant is selectable in the picker.
|
|
110
|
-
const offered = sorted.find((p) => p.id === resolvedDefault.providerID)?.models[resolvedDefault.modelID]?.variants;
|
|
111
|
-
return thinkingLevel !== undefined && offered?.[thinkingLevel] !== undefined
|
|
112
|
-
? { providers: sorted, defaults, default: resolvedDefault, defaultThinkingLevel: thinkingLevel }
|
|
113
|
-
: { providers: sorted, defaults, default: resolvedDefault };
|
|
148
|
+
return withDefault(inventory(modelsRes), active === null ? null : { providerID: active.provider, modelID: active.id, model: toOCModel(active) }, thinkingLevel);
|
|
114
149
|
}
|
|
115
150
|
finally {
|
|
116
|
-
|
|
151
|
+
if (client === undefined)
|
|
152
|
+
await pi.dispose();
|
|
117
153
|
}
|
|
118
154
|
}
|
package/dist/cli.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
#!/usr/bin/env
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
2
|
export {};
|
package/dist/cli.js
CHANGED
|
@@ -7,7 +7,10 @@ export interface CommandContext {
|
|
|
7
7
|
readonly exec: (binary: string, args: readonly string[]) => string;
|
|
8
8
|
readonly now: () => Date;
|
|
9
9
|
readonly platform: NodeJS.Platform;
|
|
10
|
-
|
|
10
|
+
/** Bun's version, or undefined when this process is not running on Bun. */
|
|
11
|
+
readonly bunVersion: string | undefined;
|
|
12
|
+
/** The running Bun executable, or undefined when this process is not running on Bun. */
|
|
13
|
+
readonly bunBinary: string | undefined;
|
|
11
14
|
readonly output: (line: string) => void;
|
|
12
15
|
readonly cliPath: string;
|
|
13
16
|
readonly launcherPath: string;
|
|
@@ -24,6 +27,6 @@ export declare function readSettings(path: string, ctx: CommandContext): Record<
|
|
|
24
27
|
export declare function shellQuote(value: string): string;
|
|
25
28
|
/** OpenChamber launcher written by `setup --auto-update`, kept beside the session registry. */
|
|
26
29
|
export declare function autoUpdateLauncherPath(ctx: CommandContext): string;
|
|
27
|
-
export declare function autoUpdateLauncherText(binary: string): string;
|
|
30
|
+
export declare function autoUpdateLauncherText(binary: string, bunBinary?: string): string;
|
|
28
31
|
export declare function autoUpdateLauncherTarget(text: string): string | undefined;
|
|
29
32
|
export declare function isPackageBinary(binary: string, ctx: CommandContext): boolean;
|
package/dist/commands/context.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
|
-
import { delimiter, isAbsolute, join, resolve } from "node:path";
|
|
4
|
+
import { delimiter, dirname, isAbsolute, join, resolve } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { defaultStateDir, envSetting } from "../config.js";
|
|
7
7
|
export function createContext(overrides = {}) {
|
|
@@ -29,7 +29,8 @@ export function createContext(overrides = {}) {
|
|
|
29
29
|
},
|
|
30
30
|
now: () => new Date(),
|
|
31
31
|
platform: process.platform,
|
|
32
|
-
|
|
32
|
+
bunVersion: process.versions.bun,
|
|
33
|
+
bunBinary: process.versions.bun === undefined ? undefined : process.execPath,
|
|
33
34
|
output: console.log,
|
|
34
35
|
cliPath: packageCliPath,
|
|
35
36
|
launcherPath: fileURLToPath(new URL("../../bin/opencode-antechamber", import.meta.url)),
|
|
@@ -85,13 +86,16 @@ export function shellQuote(value) {
|
|
|
85
86
|
export function autoUpdateLauncherPath(ctx) {
|
|
86
87
|
return resolve(ctx.cwd, envSetting("STATE_DIR", ctx.env) ?? defaultStateDir(ctx.home, ctx.fs.existsSync), "opencode-antechamber");
|
|
87
88
|
}
|
|
88
|
-
export function autoUpdateLauncherText(binary) {
|
|
89
|
+
export function autoUpdateLauncherText(binary, bunBinary) {
|
|
90
|
+
const quote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
|
|
89
91
|
return [
|
|
90
92
|
"#!/bin/sh",
|
|
91
93
|
"# Written by `antechamber setup --auto-update`; rerun `antechamber setup` without it to stop auto-updates.",
|
|
92
94
|
"ANTECHAMBER_AUTO_UPDATE=1",
|
|
93
95
|
"export ANTECHAMBER_AUTO_UPDATE",
|
|
94
|
-
|
|
96
|
+
// The CLI's `env bun` shebang must find Bun even when OpenChamber's PATH lacks its directory.
|
|
97
|
+
...(bunBinary === undefined ? [] : [`PATH=${quote(dirname(bunBinary))}:"$PATH"`, "export PATH"]),
|
|
98
|
+
`exec ${quote(binary)} "$@"`,
|
|
95
99
|
"",
|
|
96
100
|
].join("\n");
|
|
97
101
|
}
|
package/dist/commands/doctor.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { dirname, resolve } from "node:path";
|
|
2
2
|
import { piAgentDir } from "../agent-dir.js";
|
|
3
3
|
import { detectAgentName } from "../agent-name.js";
|
|
4
|
-
import { agentBinary, defaultStateDir, envSetting, isPlainPi } from "../config.js";
|
|
4
|
+
import { agentBinary, defaultStateDir, envSetting, isPlainPi, managedByExtension } from "../config.js";
|
|
5
5
|
import { findPidfdPython } from "../pidfd.js";
|
|
6
6
|
import { readRunningServers } from "../running-servers.js";
|
|
7
7
|
import { isNewerVersion } from "../self-update.js";
|
|
@@ -23,7 +23,8 @@ export function doctor(options = {}, ctx = createContext(), latestVersion) {
|
|
|
23
23
|
const add = (name, status, message) => {
|
|
24
24
|
checks.push({ name, status, message });
|
|
25
25
|
};
|
|
26
|
-
|
|
26
|
+
const [bunMajor = 0, bunMinor = 0] = (ctx.bunVersion ?? "").split(".").map(Number);
|
|
27
|
+
add("bun", bunMajor > 1 || (bunMajor === 1 && bunMinor >= 4) ? "PASS" : "FAIL", ctx.bunVersion === undefined ? "not running on Bun; requires Bun >=1.4" : `Bun ${ctx.bunVersion}; requires >=1.4`);
|
|
27
28
|
const backend = agentBinary(ctx.env);
|
|
28
29
|
const binary = resolveBinary(backend, ctx);
|
|
29
30
|
if (binary === undefined)
|
|
@@ -59,7 +60,7 @@ export function doctor(options = {}, ctx = createContext(), latestVersion) {
|
|
|
59
60
|
if (latestVersion instanceof Error)
|
|
60
61
|
add("version", "INFO", `${ADAPTER_VERSION}; latest unknown (${latestVersion.message})`);
|
|
61
62
|
else if (latest !== undefined && isNewerVersion(latest, ADAPTER_VERSION))
|
|
62
|
-
add("version", "WARN", `${ADAPTER_VERSION} installed; ${latest} is released: run antechamber upgrade`);
|
|
63
|
+
add("version", "WARN", `${ADAPTER_VERSION} installed; ${latest} is released: ${managedByExtension(ctx.env) ? "update the antechamber extension in OpenChamber" : "run antechamber upgrade"}`);
|
|
63
64
|
else
|
|
64
65
|
add("version", "PASS", ADAPTER_VERSION);
|
|
65
66
|
// A released fix is live only once OpenChamber relaunches the adapter.
|
package/dist/commands/setup.js
CHANGED
|
@@ -12,23 +12,12 @@ function writeAtomically(path, content, ctx) {
|
|
|
12
12
|
ctx.fs.writeFileSync(temporary, content, { mode: 0o600, flag: "wx" });
|
|
13
13
|
ctx.fs.renameSync(temporary, path);
|
|
14
14
|
}
|
|
15
|
-
/** The bundled
|
|
16
|
-
function
|
|
17
|
-
const dir = resolve(dirname(ctx.cliPath), "..", "extensions", "
|
|
18
|
-
const built = ["package.json", "panel/main.js", "service/main.js"].every((file) => ctx.fs.existsSync(join(dir, file)));
|
|
15
|
+
/** The bundled antechamber OpenChamber extension folder, when this installation has its built files. */
|
|
16
|
+
function antechamberExtensionDir(ctx) {
|
|
17
|
+
const dir = resolve(dirname(ctx.cliPath), "..", "extensions", "antechamber");
|
|
18
|
+
const built = ["package.json", "panel/main.js", "status/main.js", "service/main.js", "runtime/launcher.js"].every((file) => ctx.fs.existsSync(join(dir, file)));
|
|
19
19
|
return built ? dir : undefined;
|
|
20
20
|
}
|
|
21
|
-
/** The bundled OmO Work status-section extension folder, when this installation has its built files. */
|
|
22
|
-
function omoWorkExtensionDir(ctx) {
|
|
23
|
-
const dir = resolve(dirname(ctx.cliPath), "..", "extensions", "omo-work");
|
|
24
|
-
const built = ["package.json", "status/main.js", "service/main.js"].every((file) => ctx.fs.existsSync(join(dir, file)));
|
|
25
|
-
return built ? dir : undefined;
|
|
26
|
-
}
|
|
27
|
-
/** The bundled OmO Compatibility extension folder; it is a manifest only, so it needs no build. */
|
|
28
|
-
function omoCompatExtensionDir(ctx) {
|
|
29
|
-
const dir = resolve(dirname(ctx.cliPath), "..", "extensions", "omo-compat");
|
|
30
|
-
return ctx.fs.existsSync(join(dir, "package.json")) ? dir : undefined;
|
|
31
|
-
}
|
|
32
21
|
/** Plan all inputs before applying changes; dependencies are injected for hermetic tests. */
|
|
33
22
|
export function setup(options, ctx = createContext()) {
|
|
34
23
|
const lines = [];
|
|
@@ -49,8 +38,8 @@ export function setup(options, ctx = createContext()) {
|
|
|
49
38
|
: ctx.cliPath;
|
|
50
39
|
if (binary === undefined)
|
|
51
40
|
throw new Error(`Cannot resolve executable --binary ${options.binary}`);
|
|
52
|
-
if (binary === ctx.cliPath && !ctx.fs.readFileSync(binary, "utf8").startsWith("#!/usr/bin/env
|
|
53
|
-
throw new Error(`Package CLI ${binary} is missing its
|
|
41
|
+
if (binary === ctx.cliPath && !ctx.fs.readFileSync(binary, "utf8").startsWith("#!/usr/bin/env bun\n")) {
|
|
42
|
+
throw new Error(`Package CLI ${binary} is missing its bun shebang; run npm run build`);
|
|
54
43
|
}
|
|
55
44
|
let launcher;
|
|
56
45
|
let launcherText;
|
|
@@ -63,7 +52,7 @@ export function setup(options, ctx = createContext()) {
|
|
|
63
52
|
realpath: (path) => ctx.fs.realpathSync(path),
|
|
64
53
|
}, () => ctx.exec(npmBinaryFor(), ["root", "-g"]));
|
|
65
54
|
launcher = autoUpdateLauncherPath(ctx);
|
|
66
|
-
launcherText = autoUpdateLauncherText(binary);
|
|
55
|
+
launcherText = autoUpdateLauncherText(binary, ctx.bunBinary);
|
|
67
56
|
if (ctx.fs.existsSync(launcher))
|
|
68
57
|
oldLauncher = ctx.fs.readFileSync(launcher, "utf8");
|
|
69
58
|
}
|
|
@@ -181,17 +170,9 @@ export function setup(options, ctx = createContext()) {
|
|
|
181
170
|
if (options.profile !== undefined && options.settings === undefined) {
|
|
182
171
|
report(`OPENCHAMBER_DATA_DIR=${shellQuote(resolve(ctx.cwd, options.profile))} openchamber serve`);
|
|
183
172
|
}
|
|
184
|
-
const extension =
|
|
173
|
+
const extension = antechamberExtensionDir(ctx);
|
|
185
174
|
if (extension !== undefined) {
|
|
186
|
-
report(`Optional OmO Graph
|
|
187
|
-
}
|
|
188
|
-
const work = omoWorkExtensionDir(ctx);
|
|
189
|
-
if (work !== undefined) {
|
|
190
|
-
report(`Optional OmO Work section (todo phases, monitors, eval cells; OpenChamber 2.x): Settings \u2192 Extensions \u2192 Add \u2192 Folder: ${work}`);
|
|
191
|
-
}
|
|
192
|
-
const compat = omoCompatExtensionDir(ctx);
|
|
193
|
-
if (compat !== undefined) {
|
|
194
|
-
report(`Optional OmO Compatibility extension (eval card headers): Settings \u2192 Extensions \u2192 Add \u2192 Folder: ${compat}`);
|
|
175
|
+
report(`Optional antechamber extension (OmO Graph, OmO Work, OmO tool headers; OpenChamber 2.3.2+): Settings \u2192 Extensions \u2192 Add \u2192 Folder: ${extension}`);
|
|
195
176
|
}
|
|
196
177
|
return lines;
|
|
197
178
|
}
|
package/dist/config.d.ts
CHANGED
|
@@ -12,7 +12,11 @@ export declare function envSetting(name: string, env?: NodeJS.ProcessEnv): strin
|
|
|
12
12
|
* mappings survive the upgrade.
|
|
13
13
|
*/
|
|
14
14
|
export declare function defaultStateDir(home?: string, exists?: typeof existsSync): string;
|
|
15
|
-
/**
|
|
16
|
-
export declare function
|
|
15
|
+
/** The OmO that antechamber's package.json pins, when it is installed beside antechamber. */
|
|
16
|
+
export declare function bundledOmo(): string | undefined;
|
|
17
|
+
/** Agent executable spawned per session: the pinned OmO by default, else `omo` on PATH, plain pi when configured. */
|
|
18
|
+
export declare function agentBinary(env?: NodeJS.ProcessEnv, bundled?: () => string | undefined): string;
|
|
19
|
+
/** Set by the OpenChamber extension's launcher: the extension, not npm, installs and updates antechamber. */
|
|
20
|
+
export declare function managedByExtension(env?: NodeJS.ProcessEnv): boolean;
|
|
17
21
|
/** True when the configured backend is upstream pi rather than OmO. */
|
|
18
22
|
export declare function isPlainPi(env?: NodeJS.ProcessEnv): boolean;
|
package/dist/config.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
2
3
|
import { homedir } from "node:os";
|
|
3
|
-
import { basename, join } from "node:path";
|
|
4
|
+
import { basename, dirname, join } from "node:path";
|
|
4
5
|
/** Environment variable prefix for all antechamber settings. */
|
|
5
6
|
export const ENV_PREFIX = "ANTECHAMBER_";
|
|
6
7
|
/** Prefix used before the project was renamed; still honoured as a fallback. */
|
|
@@ -30,9 +31,22 @@ export function defaultStateDir(home = homedir(), exists = existsSync) {
|
|
|
30
31
|
return legacy;
|
|
31
32
|
return current;
|
|
32
33
|
}
|
|
33
|
-
/**
|
|
34
|
-
export function
|
|
35
|
-
|
|
34
|
+
/** The OmO that antechamber's package.json pins, when it is installed beside antechamber. */
|
|
35
|
+
export function bundledOmo() {
|
|
36
|
+
try {
|
|
37
|
+
return join(dirname(createRequire(import.meta.url).resolve("omo-ai/package.json")), "bin", "omo.js");
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/** Agent executable spawned per session: the pinned OmO by default, else `omo` on PATH, plain pi when configured. */
|
|
44
|
+
export function agentBinary(env = process.env, bundled = bundledOmo) {
|
|
45
|
+
return envSetting("PI_BINARY", env) ?? bundled() ?? "omo";
|
|
46
|
+
}
|
|
47
|
+
/** Set by the OpenChamber extension's launcher: the extension, not npm, installs and updates antechamber. */
|
|
48
|
+
export function managedByExtension(env = process.env) {
|
|
49
|
+
return envSetting("MANAGED", env) === "openchamber-extension";
|
|
36
50
|
}
|
|
37
51
|
/** True when the configured backend is upstream pi rather than OmO. */
|
|
38
52
|
export function isPlainPi(env = process.env) {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { OCEvent } from "./types.js";
|
|
2
|
+
export declare class FileWatchError extends Error {
|
|
3
|
+
readonly status: number;
|
|
4
|
+
readonly kind: string;
|
|
5
|
+
constructor(status: number, kind: string, message: string);
|
|
6
|
+
}
|
|
7
|
+
export declare class FileWatchRegistry {
|
|
8
|
+
private emit;
|
|
9
|
+
private watches;
|
|
10
|
+
constructor(emit: (directory: string, event: OCEvent) => void);
|
|
11
|
+
file(directory: string, path: string, required: boolean): Promise<string>;
|
|
12
|
+
watch(directory: string, file: string, reportedFile: string): void;
|
|
13
|
+
/**
|
|
14
|
+
* Watch the file itself, not its directory: on macOS a directory watch goes through FSEvents, which starts
|
|
15
|
+
* asynchronously and under load missed a write made right after the 204, and missed a delete-and-recreate for
|
|
16
|
+
* seconds (reproduced: every run failed with eight test processes in parallel). A file watch (kqueue there,
|
|
17
|
+
* inotify on Linux) starts within a tick. An editor's atomic save replaces the inode and ends the watch with
|
|
18
|
+
* "rename"; the replacement is watched. A removed file keeps its lease, as OpenCode's does: a stat watch looks for
|
|
19
|
+
* it until it exists again, then the native watch resumes.
|
|
20
|
+
*/
|
|
21
|
+
private armFile;
|
|
22
|
+
/**
|
|
23
|
+
* Check for the missing file on a timer. Not fs.watchFile: it takes its baseline stat asynchronously and never
|
|
24
|
+
* reports a file that came back before that stat finished.
|
|
25
|
+
*/
|
|
26
|
+
private awaitFile;
|
|
27
|
+
/**
|
|
28
|
+
* Runs inside a watch listener, where a throw would end the process: fs.watch throws synchronously for a
|
|
29
|
+
* replacement it cannot open (EACCES, a file gone again, ENOSPC/EMFILE). Such a lease is released instead.
|
|
30
|
+
*/
|
|
31
|
+
private rearm;
|
|
32
|
+
private release;
|
|
33
|
+
unwatch(directory: string, file: string): void;
|
|
34
|
+
close(): void;
|
|
35
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { existsSync, watch } from "node:fs";
|
|
2
|
+
import { realpath, stat } from "node:fs/promises";
|
|
3
|
+
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
4
|
+
export class FileWatchError extends Error {
|
|
5
|
+
status;
|
|
6
|
+
kind;
|
|
7
|
+
constructor(status, kind, message) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.status = status;
|
|
10
|
+
this.kind = kind;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
const LEASE_MS = 24 * 60 * 60 * 1000;
|
|
14
|
+
const MAX_PER_DIRECTORY = 256;
|
|
15
|
+
/** How often a missing watched file is checked for (existsSync, nothing spawned). */
|
|
16
|
+
const MISSING_POLL_MS = 1000;
|
|
17
|
+
export class FileWatchRegistry {
|
|
18
|
+
emit;
|
|
19
|
+
watches = new Map();
|
|
20
|
+
constructor(emit) {
|
|
21
|
+
this.emit = emit;
|
|
22
|
+
}
|
|
23
|
+
async file(directory, path, required) {
|
|
24
|
+
const root = await realpath(directory);
|
|
25
|
+
const target = resolve(root, path);
|
|
26
|
+
const inside = relative(root, target);
|
|
27
|
+
if (inside === ".." || inside.startsWith(`..${sep}`) || isAbsolute(inside))
|
|
28
|
+
throw new FileWatchError(400, "BadRequestError", "Path must remain inside the selected directory");
|
|
29
|
+
// Removal uses the registered path even when the file or symlink no longer exists.
|
|
30
|
+
if (!required)
|
|
31
|
+
return resolve(directory, path);
|
|
32
|
+
try {
|
|
33
|
+
const canonical = await realpath(target);
|
|
34
|
+
const rel = relative(root, canonical);
|
|
35
|
+
if (rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel))
|
|
36
|
+
throw new FileWatchError(400, "BadRequestError", "Path must remain inside the selected directory");
|
|
37
|
+
if (!(await stat(canonical)).isFile())
|
|
38
|
+
throw new FileWatchError(404, "FileNotFoundError", `File not found: ${path}`);
|
|
39
|
+
return canonical;
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
43
|
+
throw new FileWatchError(404, "FileNotFoundError", `File not found: ${path}`);
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
watch(directory, file, reportedFile) {
|
|
48
|
+
const files = this.watches.get(directory) ?? new Map();
|
|
49
|
+
const existing = files.get(reportedFile);
|
|
50
|
+
if (existing !== undefined) {
|
|
51
|
+
existing.expires = Date.now() + LEASE_MS;
|
|
52
|
+
clearTimeout(existing.timer);
|
|
53
|
+
existing.timer = setTimeout(() => this.unwatch(directory, reportedFile), LEASE_MS);
|
|
54
|
+
existing.timer.unref();
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (files.size >= MAX_PER_DIRECTORY) {
|
|
58
|
+
const [soonest] = [...files].reduce((a, b) => (b[1].expires < a[1].expires ? b : a));
|
|
59
|
+
this.unwatch(directory, soonest);
|
|
60
|
+
}
|
|
61
|
+
const timer = setTimeout(() => this.unwatch(directory, reportedFile), LEASE_MS);
|
|
62
|
+
timer.unref();
|
|
63
|
+
const lease = { watcher: null, missing: null, expires: Date.now() + LEASE_MS, timer };
|
|
64
|
+
files.set(reportedFile, lease);
|
|
65
|
+
this.watches.set(directory, files);
|
|
66
|
+
lease.watcher = this.armFile(directory, file, reportedFile);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Watch the file itself, not its directory: on macOS a directory watch goes through FSEvents, which starts
|
|
70
|
+
* asynchronously and under load missed a write made right after the 204, and missed a delete-and-recreate for
|
|
71
|
+
* seconds (reproduced: every run failed with eight test processes in parallel). A file watch (kqueue there,
|
|
72
|
+
* inotify on Linux) starts within a tick. An editor's atomic save replaces the inode and ends the watch with
|
|
73
|
+
* "rename"; the replacement is watched. A removed file keeps its lease, as OpenCode's does: a stat watch looks for
|
|
74
|
+
* it until it exists again, then the native watch resumes.
|
|
75
|
+
*/
|
|
76
|
+
armFile(directory, file, reportedFile) {
|
|
77
|
+
const watcher = watch(file, (event) => {
|
|
78
|
+
const exists = existsSync(file);
|
|
79
|
+
this.emit(directory, {
|
|
80
|
+
type: "filesystem.changed",
|
|
81
|
+
properties: { file: reportedFile, event: exists ? "change" : "unlink" },
|
|
82
|
+
});
|
|
83
|
+
const lease = this.watches.get(directory)?.get(reportedFile);
|
|
84
|
+
// A rename replaced the inode; an absent file ended the watch, whatever the event (macOS reports a write and
|
|
85
|
+
// a delete arriving together as one "change").
|
|
86
|
+
if (lease?.watcher !== watcher || (exists && event !== "rename"))
|
|
87
|
+
return;
|
|
88
|
+
watcher.close();
|
|
89
|
+
lease.watcher = null;
|
|
90
|
+
if (exists)
|
|
91
|
+
this.rearm(directory, file, reportedFile, lease);
|
|
92
|
+
else
|
|
93
|
+
this.awaitFile(directory, file, reportedFile, lease);
|
|
94
|
+
});
|
|
95
|
+
watcher.on("error", () => {
|
|
96
|
+
if (this.watches.get(directory)?.get(reportedFile)?.watcher === watcher)
|
|
97
|
+
this.unwatch(directory, reportedFile);
|
|
98
|
+
else
|
|
99
|
+
watcher.close();
|
|
100
|
+
});
|
|
101
|
+
return watcher;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Check for the missing file on a timer. Not fs.watchFile: it takes its baseline stat asynchronously and never
|
|
105
|
+
* reports a file that came back before that stat finished.
|
|
106
|
+
*/
|
|
107
|
+
awaitFile(directory, file, reportedFile, lease) {
|
|
108
|
+
const check = () => {
|
|
109
|
+
if (!existsSync(file) || this.watches.get(directory)?.get(reportedFile) !== lease)
|
|
110
|
+
return;
|
|
111
|
+
if (lease.missing !== null)
|
|
112
|
+
clearInterval(lease.missing);
|
|
113
|
+
lease.missing = null;
|
|
114
|
+
this.emit(directory, { type: "filesystem.changed", properties: { file: reportedFile, event: "change" } });
|
|
115
|
+
this.rearm(directory, file, reportedFile, lease);
|
|
116
|
+
};
|
|
117
|
+
lease.missing = setInterval(check, MISSING_POLL_MS);
|
|
118
|
+
lease.missing.unref();
|
|
119
|
+
check();
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Runs inside a watch listener, where a throw would end the process: fs.watch throws synchronously for a
|
|
123
|
+
* replacement it cannot open (EACCES, a file gone again, ENOSPC/EMFILE). Such a lease is released instead.
|
|
124
|
+
*/
|
|
125
|
+
rearm(directory, file, reportedFile, lease) {
|
|
126
|
+
try {
|
|
127
|
+
lease.watcher = this.armFile(directory, file, reportedFile);
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
// Gone again between the check and the watch: keep the lease and wait for it once more.
|
|
131
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
132
|
+
this.awaitFile(directory, file, reportedFile, lease);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
console.error(`[file-watch] stopped watching ${reportedFile}:`, error);
|
|
136
|
+
this.unwatch(directory, reportedFile);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
release(lease) {
|
|
140
|
+
clearTimeout(lease.timer);
|
|
141
|
+
lease.watcher?.close();
|
|
142
|
+
if (lease.missing !== null)
|
|
143
|
+
clearInterval(lease.missing);
|
|
144
|
+
}
|
|
145
|
+
unwatch(directory, file) {
|
|
146
|
+
const files = this.watches.get(directory);
|
|
147
|
+
const entry = files?.get(file);
|
|
148
|
+
if (entry === undefined)
|
|
149
|
+
return;
|
|
150
|
+
this.release(entry);
|
|
151
|
+
files?.delete(file);
|
|
152
|
+
if (files?.size === 0)
|
|
153
|
+
this.watches.delete(directory);
|
|
154
|
+
}
|
|
155
|
+
close() {
|
|
156
|
+
for (const files of this.watches.values()) {
|
|
157
|
+
for (const entry of files.values()) {
|
|
158
|
+
this.release(entry);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
this.watches.clear();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Operator diagnostics. Route labels contain only literals and parameter names. */
|
|
2
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
3
|
+
export declare class LatencyMonitor {
|
|
4
|
+
private readonly lagThreshold;
|
|
5
|
+
private readonly requestThreshold;
|
|
6
|
+
private timer;
|
|
7
|
+
start(): void;
|
|
8
|
+
stop(): void;
|
|
9
|
+
request(req: IncomingMessage, res: ServerResponse, handle: () => Promise<void>): Promise<void>;
|
|
10
|
+
}
|