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
|
@@ -0,0 +1,884 @@
|
|
|
1
|
+
// extensions/antechamber/src/service-main.ts
|
|
2
|
+
import { chmodSync, readFileSync as readFileSync4 } from "node:fs";
|
|
3
|
+
import { dirname, join as join5, resolve as resolve4 } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
// extensions/antechamber/src/runtime/runtime.ts
|
|
7
|
+
import { spawn } from "node:child_process";
|
|
8
|
+
import { createHash } from "node:crypto";
|
|
9
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
import { delimiter, isAbsolute, join, resolve } from "node:path";
|
|
12
|
+
var STALE_LOCK_MS = 15 * 6e4;
|
|
13
|
+
var INSTALL_TIMEOUT_MS = 10 * 6e4;
|
|
14
|
+
var READY = ".ready.json";
|
|
15
|
+
var ERROR_TAIL_CHARS = 2e3;
|
|
16
|
+
function defaultEnv() {
|
|
17
|
+
return { env: process.env, home: homedir() };
|
|
18
|
+
}
|
|
19
|
+
function runtimeRoot({ env: env2, home }) {
|
|
20
|
+
const configured = env2.ANTECHAMBER_RUNTIME_DIR?.trim();
|
|
21
|
+
return configured ? resolve(configured) : join(home, ".antechamber", "runtime");
|
|
22
|
+
}
|
|
23
|
+
function runtimePaths(version2, runtime) {
|
|
24
|
+
const root = runtimeRoot(runtime);
|
|
25
|
+
const override = runtime.env.ANTECHAMBER_RUNTIME_SPEC?.trim();
|
|
26
|
+
if (!override) return { root, spec: version2, key: version2, dir: join(root, version2) };
|
|
27
|
+
const spec = isAbsolute(override) ? override : resolve(override);
|
|
28
|
+
const stamp = existsSync(spec) ? `${statSync(spec).mtimeMs}:${statSync(spec).size}` : "";
|
|
29
|
+
const key = `local-${createHash("sha256").update(`${spec}
|
|
30
|
+
${stamp}`).digest("hex").slice(0, 12)}`;
|
|
31
|
+
return { root, spec: `file:${spec}`, key, dir: join(root, key) };
|
|
32
|
+
}
|
|
33
|
+
function findBun({ env: env2, home }, exists = existsSync) {
|
|
34
|
+
for (const dir of (env2.PATH ?? "").split(delimiter)) {
|
|
35
|
+
if (dir && exists(join(dir, "bun"))) return join(dir, "bun");
|
|
36
|
+
}
|
|
37
|
+
for (const candidate of [
|
|
38
|
+
join(env2.BUN_INSTALL ?? join(home, ".bun"), "bin", "bun"),
|
|
39
|
+
join(home, ".bun", "bin", "bun")
|
|
40
|
+
]) {
|
|
41
|
+
if (exists(candidate)) return candidate;
|
|
42
|
+
}
|
|
43
|
+
return void 0;
|
|
44
|
+
}
|
|
45
|
+
function readJson(path) {
|
|
46
|
+
try {
|
|
47
|
+
const value = JSON.parse(readFileSync(path, "utf8"));
|
|
48
|
+
return typeof value === "object" && value !== null ? value : void 0;
|
|
49
|
+
} catch {
|
|
50
|
+
return void 0;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function installed(dir) {
|
|
54
|
+
if (!existsSync(join(dir, READY))) return void 0;
|
|
55
|
+
const cli = join(dir, "node_modules", "antechamber", "dist", "cli.js");
|
|
56
|
+
const antechamber = readJson(join(dir, "node_modules", "antechamber", "package.json"))?.version;
|
|
57
|
+
if (!existsSync(cli) || typeof antechamber !== "string") return void 0;
|
|
58
|
+
const omo = readJson(join(dir, "node_modules", "omo-ai", "package.json"))?.version;
|
|
59
|
+
return { dir, cli, antechamber, omo: typeof omo === "string" ? omo : void 0 };
|
|
60
|
+
}
|
|
61
|
+
function lockHeld(lock, deps) {
|
|
62
|
+
let mtime;
|
|
63
|
+
try {
|
|
64
|
+
mtime = statSync(lock).mtimeMs;
|
|
65
|
+
} catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
const pid = Number.parseInt(readFileSync(join(lock, "pid"), { encoding: "utf8", flag: "a+" }), 10);
|
|
69
|
+
if (deps.now() - mtime > STALE_LOCK_MS) return false;
|
|
70
|
+
return !Number.isInteger(pid) || deps.pidAlive(pid);
|
|
71
|
+
}
|
|
72
|
+
function runtimeState(paths, deps = defaultDeps()) {
|
|
73
|
+
const runtime = installed(paths.dir);
|
|
74
|
+
if (runtime) return { state: "ready", runtime };
|
|
75
|
+
if (lockHeld(`${paths.dir}.lock`, deps)) return { state: "installing" };
|
|
76
|
+
const error = existsSync(`${paths.dir}.error`) ? readFileSync(`${paths.dir}.error`, "utf8") : void 0;
|
|
77
|
+
return error ? { state: "failed", error } : { state: "missing" };
|
|
78
|
+
}
|
|
79
|
+
function tryLock(lock, deps) {
|
|
80
|
+
try {
|
|
81
|
+
mkdirSync(lock);
|
|
82
|
+
} catch (error) {
|
|
83
|
+
if (error.code !== "EEXIST") throw error;
|
|
84
|
+
if (lockHeld(lock, deps)) return false;
|
|
85
|
+
rmSync(lock, { recursive: true, force: true });
|
|
86
|
+
return tryLock(lock, deps);
|
|
87
|
+
}
|
|
88
|
+
writeFileSync(join(lock, "pid"), String(process.pid));
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
function prune(paths) {
|
|
92
|
+
const others = readdirSync(paths.root, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name !== paths.key && !/\.(lock|tmp-.*)$/.test(entry.name)).map((entry) => join(paths.root, entry.name)).filter((dir) => existsSync(join(dir, READY)) && !existsSync(`${dir}.lock`)).map((dir) => ({ dir, at: Number(readJson(join(dir, READY))?.installedAt ?? 0) })).sort((a, b) => b.at - a.at);
|
|
93
|
+
for (const { dir } of others.slice(1)) rmSync(dir, { recursive: true, force: true });
|
|
94
|
+
}
|
|
95
|
+
async function installOnce(paths, bun, deps) {
|
|
96
|
+
const staging = `${paths.dir}.tmp-${process.pid}-${deps.now()}`;
|
|
97
|
+
mkdirSync(staging, { recursive: true });
|
|
98
|
+
try {
|
|
99
|
+
const manifest = {
|
|
100
|
+
name: "antechamber-runtime",
|
|
101
|
+
private: true,
|
|
102
|
+
dependencies: { antechamber: paths.spec },
|
|
103
|
+
// omo-ai's postinstall prepares its engine; Bun skips dependency scripts unless trusted.
|
|
104
|
+
trustedDependencies: ["omo-ai"]
|
|
105
|
+
};
|
|
106
|
+
writeFileSync(join(staging, "package.json"), `${JSON.stringify(manifest, null, 2)}
|
|
107
|
+
`);
|
|
108
|
+
await deps.install(bun, staging);
|
|
109
|
+
writeFileSync(join(staging, READY), `${JSON.stringify({ spec: paths.spec, installedAt: deps.now() })}
|
|
110
|
+
`);
|
|
111
|
+
if (!installed(staging)) throw new Error(`bun install did not produce node_modules/antechamber/dist/cli.js`);
|
|
112
|
+
rmSync(paths.dir, { recursive: true, force: true });
|
|
113
|
+
renameSync(staging, paths.dir);
|
|
114
|
+
} catch (error) {
|
|
115
|
+
rmSync(staging, { recursive: true, force: true });
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
const runtime = installed(paths.dir);
|
|
119
|
+
if (!runtime) throw new Error(`runtime vanished from ${paths.dir}`);
|
|
120
|
+
return runtime;
|
|
121
|
+
}
|
|
122
|
+
async function ensureRuntime(paths, bun, deps = defaultDeps()) {
|
|
123
|
+
const lock = `${paths.dir}.lock`;
|
|
124
|
+
for (; ; ) {
|
|
125
|
+
const ready = installed(paths.dir);
|
|
126
|
+
if (ready) return ready;
|
|
127
|
+
if (!bun) throw new Error("bun not found on PATH or in ~/.bun/bin; install it from https://bun.sh");
|
|
128
|
+
mkdirSync(paths.root, { recursive: true });
|
|
129
|
+
if (!tryLock(lock, deps)) {
|
|
130
|
+
await deps.pause();
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
const raced = installed(paths.dir);
|
|
135
|
+
if (raced) return raced;
|
|
136
|
+
const runtime = await installOnce(paths, bun, deps);
|
|
137
|
+
rmSync(`${paths.dir}.error`, { force: true });
|
|
138
|
+
prune(paths);
|
|
139
|
+
return runtime;
|
|
140
|
+
} catch (error) {
|
|
141
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
142
|
+
writeFileSync(`${paths.dir}.error`, message.slice(-ERROR_TAIL_CHARS));
|
|
143
|
+
throw error;
|
|
144
|
+
} finally {
|
|
145
|
+
rmSync(lock, { recursive: true, force: true });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function bunInstall(bun, cwd) {
|
|
150
|
+
return new Promise((resolvePromise, reject) => {
|
|
151
|
+
const child = spawn(bun, ["install", "--production", "--no-progress"], {
|
|
152
|
+
cwd,
|
|
153
|
+
env: process.env,
|
|
154
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
155
|
+
});
|
|
156
|
+
let output = "";
|
|
157
|
+
const keep = (chunk) => {
|
|
158
|
+
output = (output + chunk.toString("utf8")).slice(-ERROR_TAIL_CHARS);
|
|
159
|
+
};
|
|
160
|
+
child.stdout.on("data", keep);
|
|
161
|
+
child.stderr.on("data", keep);
|
|
162
|
+
const timer = setTimeout(() => child.kill("SIGTERM"), INSTALL_TIMEOUT_MS);
|
|
163
|
+
child.on("error", (error) => {
|
|
164
|
+
clearTimeout(timer);
|
|
165
|
+
reject(error);
|
|
166
|
+
});
|
|
167
|
+
child.on("close", (code, signal) => {
|
|
168
|
+
clearTimeout(timer);
|
|
169
|
+
if (code === 0) resolvePromise();
|
|
170
|
+
else reject(new Error(`bun install failed (${signal ?? `exit ${code}`}): ${output.trim()}`));
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
function defaultDeps() {
|
|
175
|
+
return {
|
|
176
|
+
install: bunInstall,
|
|
177
|
+
pause: () => new Promise((done) => setTimeout(done, 500)),
|
|
178
|
+
now: Date.now,
|
|
179
|
+
pidAlive: (pid) => {
|
|
180
|
+
try {
|
|
181
|
+
process.kill(pid, 0);
|
|
182
|
+
return true;
|
|
183
|
+
} catch (error) {
|
|
184
|
+
return error.code === "EPERM";
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// extensions/antechamber/src/service.ts
|
|
191
|
+
import { timingSafeEqual as timingSafeEqual2 } from "node:crypto";
|
|
192
|
+
import { createServer as createHttpServer } from "node:http";
|
|
193
|
+
import { join as join4 } from "node:path";
|
|
194
|
+
|
|
195
|
+
// extensions/antechamber/src/graph/routes.ts
|
|
196
|
+
import http from "node:http";
|
|
197
|
+
import { resolve as resolve2 } from "node:path";
|
|
198
|
+
|
|
199
|
+
// extensions/antechamber/src/graph/graph.ts
|
|
200
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
201
|
+
var TEXT_FIELDS = [
|
|
202
|
+
"status",
|
|
203
|
+
"parent_session_id",
|
|
204
|
+
"root_session_id",
|
|
205
|
+
"execution_mode",
|
|
206
|
+
"name",
|
|
207
|
+
"task_summary",
|
|
208
|
+
"description",
|
|
209
|
+
"agent_type",
|
|
210
|
+
"category",
|
|
211
|
+
"model",
|
|
212
|
+
"created_at",
|
|
213
|
+
"started_at",
|
|
214
|
+
"terminal_at",
|
|
215
|
+
"child_session_id",
|
|
216
|
+
"final_response",
|
|
217
|
+
"error_message"
|
|
218
|
+
];
|
|
219
|
+
var isObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
220
|
+
var text = (v) => typeof v === "string" ? v : void 0;
|
|
221
|
+
var num = (v) => typeof v === "number" && Number.isFinite(v) ? v : void 0;
|
|
222
|
+
function normalizeTask(raw) {
|
|
223
|
+
if (!isObject(raw) || typeof raw.task_id !== "string" || raw.task_id === "") return void 0;
|
|
224
|
+
const task = { task_id: raw.task_id };
|
|
225
|
+
for (const key of TEXT_FIELDS) {
|
|
226
|
+
const value = text(raw[key]);
|
|
227
|
+
if (value !== void 0) task[key] = value;
|
|
228
|
+
}
|
|
229
|
+
if (num(raw.depth) !== void 0) task.depth = num(raw.depth);
|
|
230
|
+
const host = isObject(raw.host_session) ? text(raw.host_session.session_path) : void 0;
|
|
231
|
+
if (host !== void 0) task.host_session = { session_path: host };
|
|
232
|
+
if (isObject(raw.spawn_spec))
|
|
233
|
+
task.spawn_spec = { prompt: text(raw.spawn_spec.prompt), instructions: text(raw.spawn_spec.instructions) };
|
|
234
|
+
if (isObject(raw.run_stats)) {
|
|
235
|
+
const s = raw.run_stats;
|
|
236
|
+
task.run_stats = {
|
|
237
|
+
total_tokens: num(s.total_tokens),
|
|
238
|
+
cost_usd: num(s.cost_usd),
|
|
239
|
+
tool_calls: num(s.tool_calls),
|
|
240
|
+
turns: num(s.turns)
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
return task;
|
|
244
|
+
}
|
|
245
|
+
function normalizeRegistry(raw) {
|
|
246
|
+
if (!isObject(raw)) return void 0;
|
|
247
|
+
const out = {};
|
|
248
|
+
for (const [id, value] of Object.entries(raw)) {
|
|
249
|
+
if (!isObject(value)) continue;
|
|
250
|
+
const entry = {};
|
|
251
|
+
if (value.deleted === true) entry.deleted = true;
|
|
252
|
+
if (text(value.piSessionId) !== void 0) entry.piSessionId = text(value.piSessionId);
|
|
253
|
+
if (text(value.parentID) !== void 0) entry.parentID = text(value.parentID);
|
|
254
|
+
if (text(value.title) !== void 0) entry.title = text(value.title);
|
|
255
|
+
if (isObject(value.task)) entry.task = { id: text(value.task.id), parentPiId: text(value.task.parentPiId) };
|
|
256
|
+
out[id] = entry;
|
|
257
|
+
}
|
|
258
|
+
return out;
|
|
259
|
+
}
|
|
260
|
+
function ocIdFor(piSessionId) {
|
|
261
|
+
return `ses_${createHash2("md5").update(piSessionId).digest("hex")}`;
|
|
262
|
+
}
|
|
263
|
+
function taskSessionId(task, tasks, registry) {
|
|
264
|
+
const registered = Object.entries(registry).find(
|
|
265
|
+
([, entry]) => entry.task?.id === task.task_id && entry.task.parentPiId === task.parent_session_id
|
|
266
|
+
);
|
|
267
|
+
if (registered) return registered[0];
|
|
268
|
+
const legacy = ocIdFor(task.task_id);
|
|
269
|
+
const parent = Object.entries(registry).find(([, entry]) => entry.piSessionId === task.parent_session_id)?.[0];
|
|
270
|
+
if (registry[legacy]?.deleted && registry[legacy]?.parentID === (parent ?? ocIdFor(task.parent_session_id ?? "")))
|
|
271
|
+
return legacy;
|
|
272
|
+
const qualified = ocIdFor(`${task.parent_session_id}:${task.task_id}`);
|
|
273
|
+
if (registry[legacy] !== void 0 || registry[qualified] !== void 0 || tasks.some((other) => other.task_id === task.task_id && other.parent_session_id !== task.parent_session_id))
|
|
274
|
+
return qualified;
|
|
275
|
+
return legacy;
|
|
276
|
+
}
|
|
277
|
+
var ACTIVE = /* @__PURE__ */ new Set(["running", "starting"]);
|
|
278
|
+
var WAITING = /* @__PURE__ */ new Set(["queued", "pending", "suspended"]);
|
|
279
|
+
var FAILED = /* @__PURE__ */ new Set(["failed", "error", "lost", "denied", "plan_error"]);
|
|
280
|
+
var CANCELLED = /* @__PURE__ */ new Set(["cancelled", "canceled", "killed"]);
|
|
281
|
+
function nodeState(status) {
|
|
282
|
+
const s = String(status ?? "");
|
|
283
|
+
if (ACTIVE.has(s)) return "running";
|
|
284
|
+
if (WAITING.has(s)) return "queued";
|
|
285
|
+
if (s === "completed") return "completed";
|
|
286
|
+
if (FAILED.has(s)) return "failed";
|
|
287
|
+
if (CANCELLED.has(s)) return "cancelled";
|
|
288
|
+
return "idle";
|
|
289
|
+
}
|
|
290
|
+
var finished = (state) => state === "completed" || state === "failed" || state === "cancelled";
|
|
291
|
+
function time(value) {
|
|
292
|
+
if (value === void 0) return void 0;
|
|
293
|
+
const t = Date.parse(value);
|
|
294
|
+
return Number.isFinite(t) ? t : void 0;
|
|
295
|
+
}
|
|
296
|
+
function firstLine(text2, max = 280) {
|
|
297
|
+
if (!text2) return void 0;
|
|
298
|
+
const clean = text2.replace(/\s*\[task_id:[^\]]*\]/g, "").replace(/\s+/g, " ").trim();
|
|
299
|
+
if (clean === "") return void 0;
|
|
300
|
+
return clean.length > max ? `${clean.slice(0, max - 1)}\u2026` : clean;
|
|
301
|
+
}
|
|
302
|
+
function ownSessionIds(task) {
|
|
303
|
+
const ids = [task.task_id];
|
|
304
|
+
if (task.child_session_id) ids.push(task.child_session_id);
|
|
305
|
+
const path = task.host_session?.session_path;
|
|
306
|
+
const match = path ? /_([0-9a-f-]{36})\.jsonl$/i.exec(path) : null;
|
|
307
|
+
if (match?.[1]) ids.push(match[1]);
|
|
308
|
+
return ids;
|
|
309
|
+
}
|
|
310
|
+
var TASK_REF = /(?:agent:\/\/)?\b(st_[0-9A-Za-z_-]{6,})\b/g;
|
|
311
|
+
function referencedTasks(task) {
|
|
312
|
+
const text2 = `${task.spawn_spec?.prompt ?? ""}
|
|
313
|
+
${task.spawn_spec?.instructions ?? ""}`;
|
|
314
|
+
const refs = /* @__PURE__ */ new Set();
|
|
315
|
+
for (const m of text2.matchAll(TASK_REF)) if (m[1] && m[1] !== task.task_id) refs.add(m[1]);
|
|
316
|
+
return refs;
|
|
317
|
+
}
|
|
318
|
+
function resolveSession(sessionId, tasks, registry) {
|
|
319
|
+
const entry = registry[sessionId];
|
|
320
|
+
const focusTask = tasks.find((t) => taskSessionId(t, tasks, registry) === sessionId);
|
|
321
|
+
let rootPi;
|
|
322
|
+
if (focusTask) rootPi = focusTask.root_session_id ?? focusTask.parent_session_id ?? entry?.task?.parentPiId;
|
|
323
|
+
else if (entry?.task?.parentPiId) rootPi = entry.task.parentPiId;
|
|
324
|
+
else if (entry?.piSessionId) rootPi = entry.piSessionId;
|
|
325
|
+
else {
|
|
326
|
+
const t = tasks.find(
|
|
327
|
+
(t2) => [t2.root_session_id, t2.parent_session_id].some((id) => id !== void 0 && ocIdFor(id) === sessionId)
|
|
328
|
+
);
|
|
329
|
+
rootPi = t ? t.root_session_id ?? t.parent_session_id : void 0;
|
|
330
|
+
}
|
|
331
|
+
if (rootPi === void 0) return void 0;
|
|
332
|
+
const rootOc = Object.entries(registry).find(([, v]) => v.piSessionId === rootPi && v.task === void 0)?.[0] ?? (focusTask || entry?.task ? entry?.parentID ?? ocIdFor(rootPi) : sessionId);
|
|
333
|
+
return { rootPi, rootOc, focusOc: sessionId };
|
|
334
|
+
}
|
|
335
|
+
function buildGraph(sessionId, tasks, registry) {
|
|
336
|
+
const resolved = resolveSession(sessionId, tasks, registry);
|
|
337
|
+
if (!resolved) {
|
|
338
|
+
return {
|
|
339
|
+
status: "unknown-session",
|
|
340
|
+
rootId: sessionId,
|
|
341
|
+
focusId: sessionId,
|
|
342
|
+
title: registry[sessionId]?.title ?? "Session",
|
|
343
|
+
nodes: [],
|
|
344
|
+
edges: [],
|
|
345
|
+
columns: [],
|
|
346
|
+
models: 0
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
const { rootPi, rootOc, focusOc } = resolved;
|
|
350
|
+
const members = tasks.filter((t) => (t.root_session_id ?? t.parent_session_id) === rootPi).sort((a, b) => (time(a.created_at) ?? 0) - (time(b.created_at) ?? 0) || a.task_id.localeCompare(b.task_id));
|
|
351
|
+
const owner = /* @__PURE__ */ new Map();
|
|
352
|
+
for (const t of members) for (const id of ownSessionIds(t)) owner.set(id, t.task_id);
|
|
353
|
+
const parentOf = (t) => (t.parent_session_id && t.parent_session_id !== rootPi ? owner.get(t.parent_session_id) : void 0) ?? rootOc;
|
|
354
|
+
const byId = new Map(members.map((t) => [t.task_id, t]));
|
|
355
|
+
const edges = [];
|
|
356
|
+
const uses = /* @__PURE__ */ new Map();
|
|
357
|
+
for (const t of members) {
|
|
358
|
+
const parent = parentOf(t);
|
|
359
|
+
edges.push({ id: `spawn:${parent}>${t.task_id}`, from: parent, to: t.task_id, kind: "spawn" });
|
|
360
|
+
for (const ref of referencedTasks(t)) {
|
|
361
|
+
const source = byId.get(ref);
|
|
362
|
+
if (!source || ref === parent) continue;
|
|
363
|
+
if ((time(source.created_at) ?? 0) > (time(t.created_at) ?? 0)) continue;
|
|
364
|
+
edges.push({ id: `uses:${ref}>${t.task_id}`, from: ref, to: t.task_id, kind: "uses" });
|
|
365
|
+
uses.set(t.task_id, [...uses.get(t.task_id) ?? [], ref]);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
const wave = /* @__PURE__ */ new Map();
|
|
369
|
+
const column = /* @__PURE__ */ new Map([[rootOc, 0]]);
|
|
370
|
+
const siblingsSoFar = /* @__PURE__ */ new Map();
|
|
371
|
+
for (const t of members) {
|
|
372
|
+
const parent = parentOf(t);
|
|
373
|
+
const created = time(t.created_at) ?? 0;
|
|
374
|
+
const earlier = siblingsSoFar.get(parent) ?? [];
|
|
375
|
+
let w = 0;
|
|
376
|
+
for (const s of earlier) {
|
|
377
|
+
const ended = time(s.terminal_at);
|
|
378
|
+
if (ended !== void 0 && ended <= created) w = Math.max(w, (wave.get(s.task_id) ?? 0) + 1);
|
|
379
|
+
}
|
|
380
|
+
for (const ref of uses.get(t.task_id) ?? []) {
|
|
381
|
+
if (parentOf(byId.get(ref)) === parent) w = Math.max(w, (wave.get(ref) ?? 0) + 1);
|
|
382
|
+
}
|
|
383
|
+
wave.set(t.task_id, w);
|
|
384
|
+
column.set(t.task_id, (column.get(parent) ?? 0) + 1 + w);
|
|
385
|
+
for (const ref of uses.get(t.task_id) ?? []) {
|
|
386
|
+
column.set(t.task_id, Math.max(column.get(t.task_id) ?? 0, (column.get(ref) ?? 0) + 1));
|
|
387
|
+
}
|
|
388
|
+
siblingsSoFar.set(parent, [...earlier, t]);
|
|
389
|
+
}
|
|
390
|
+
const taskNodes = members.map((t) => {
|
|
391
|
+
const state = nodeState(t.status);
|
|
392
|
+
return {
|
|
393
|
+
id: t.task_id,
|
|
394
|
+
kind: "task",
|
|
395
|
+
sessionId: taskSessionId(t, tasks, registry),
|
|
396
|
+
title: firstLine(t.task_summary ?? t.description ?? t.name, 90) ?? t.task_id,
|
|
397
|
+
state,
|
|
398
|
+
rawStatus: String(t.status ?? "unknown"),
|
|
399
|
+
agent: t.agent_type ?? t.category,
|
|
400
|
+
model: t.model,
|
|
401
|
+
createdAt: time(t.created_at),
|
|
402
|
+
startedAt: time(t.started_at) ?? time(t.created_at),
|
|
403
|
+
endedAt: finished(state) ? time(t.terminal_at) : void 0,
|
|
404
|
+
summary: firstLine(t.final_response),
|
|
405
|
+
error: firstLine(t.error_message),
|
|
406
|
+
tokens: t.run_stats?.total_tokens,
|
|
407
|
+
costUsd: t.run_stats?.cost_usd,
|
|
408
|
+
toolCalls: t.run_stats?.tool_calls,
|
|
409
|
+
parentId: parentOf(t),
|
|
410
|
+
column: column.get(t.task_id) ?? 1,
|
|
411
|
+
wave: wave.get(t.task_id) ?? 0
|
|
412
|
+
};
|
|
413
|
+
});
|
|
414
|
+
const anyActive = taskNodes.some((n) => n.state === "running" || n.state === "queued");
|
|
415
|
+
const root = {
|
|
416
|
+
id: rootOc,
|
|
417
|
+
kind: "root",
|
|
418
|
+
sessionId: rootOc,
|
|
419
|
+
title: firstLine(registry[rootOc]?.title, 90) ?? "Session",
|
|
420
|
+
state: anyActive ? "running" : "idle",
|
|
421
|
+
rawStatus: anyActive ? "orchestrating" : "orchestrator",
|
|
422
|
+
createdAt: taskNodes[0]?.createdAt,
|
|
423
|
+
column: 0,
|
|
424
|
+
wave: 0
|
|
425
|
+
};
|
|
426
|
+
const nodes = [root, ...taskNodes];
|
|
427
|
+
const columns = /* @__PURE__ */ new Map();
|
|
428
|
+
for (const n of taskNodes) {
|
|
429
|
+
const c = columns.get(n.column) ?? { column: n.column, total: 0, done: 0, running: 0 };
|
|
430
|
+
c.total++;
|
|
431
|
+
if (finished(n.state)) c.done++;
|
|
432
|
+
if (n.state === "running") c.running++;
|
|
433
|
+
columns.set(n.column, c);
|
|
434
|
+
}
|
|
435
|
+
const focusId = taskNodes.find((n) => n.sessionId === focusOc)?.id ?? rootOc;
|
|
436
|
+
return {
|
|
437
|
+
status: "ok",
|
|
438
|
+
rootId: rootOc,
|
|
439
|
+
focusId,
|
|
440
|
+
title: root.title,
|
|
441
|
+
nodes,
|
|
442
|
+
edges,
|
|
443
|
+
columns: [...columns.values()].sort((a, b) => a.column - b.column),
|
|
444
|
+
models: new Set(taskNodes.map((n) => n.model).filter(Boolean)).size
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// extensions/antechamber/src/graph/store.ts
|
|
449
|
+
import { lstatSync, readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync2 } from "node:fs";
|
|
450
|
+
import { homedir as homedir2 } from "node:os";
|
|
451
|
+
import { join as join2 } from "node:path";
|
|
452
|
+
import { DatabaseSync } from "node:sqlite";
|
|
453
|
+
function expandHome(path) {
|
|
454
|
+
if (path === "~") return homedir2();
|
|
455
|
+
return path.startsWith("~/") ? join2(homedir2(), path.slice(2)) : path;
|
|
456
|
+
}
|
|
457
|
+
function defaultRoots() {
|
|
458
|
+
const home = homedir2();
|
|
459
|
+
return {
|
|
460
|
+
agentDirs: [join2(home, ".omo", "agent"), join2(home, ".pi", "agent")],
|
|
461
|
+
stateDirs: [join2(home, ".config", "antechamber"), join2(home, ".config", "openchamber-pi-connector")]
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
function isMissing(error) {
|
|
465
|
+
return error instanceof Error && "code" in error && ["ENOENT", "ENOTDIR", "EACCES"].includes(String(error.code));
|
|
466
|
+
}
|
|
467
|
+
function list(dir) {
|
|
468
|
+
try {
|
|
469
|
+
return readdirSync2(dir);
|
|
470
|
+
} catch (error) {
|
|
471
|
+
if (isMissing(error)) return [];
|
|
472
|
+
throw error;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
function isRegularFile(path) {
|
|
476
|
+
try {
|
|
477
|
+
return lstatSync(path).isFile();
|
|
478
|
+
} catch (error) {
|
|
479
|
+
if (isMissing(error)) return false;
|
|
480
|
+
throw error;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
var JsonCache = class {
|
|
484
|
+
entries = /* @__PURE__ */ new Map();
|
|
485
|
+
read(path, accept) {
|
|
486
|
+
let stat;
|
|
487
|
+
try {
|
|
488
|
+
stat = statSync2(path);
|
|
489
|
+
} catch (error) {
|
|
490
|
+
if (isMissing(error)) {
|
|
491
|
+
this.entries.delete(path);
|
|
492
|
+
return void 0;
|
|
493
|
+
}
|
|
494
|
+
throw error;
|
|
495
|
+
}
|
|
496
|
+
const hit = this.entries.get(path);
|
|
497
|
+
if (hit && hit.mtimeMs === stat.mtimeMs && hit.size === stat.size) return hit.value;
|
|
498
|
+
let value;
|
|
499
|
+
try {
|
|
500
|
+
value = accept(JSON.parse(readFileSync2(path, "utf8")));
|
|
501
|
+
} catch (error) {
|
|
502
|
+
if (error instanceof SyntaxError || isMissing(error)) return hit?.value;
|
|
503
|
+
throw error;
|
|
504
|
+
}
|
|
505
|
+
this.entries.set(path, { mtimeMs: stat.mtimeMs, size: stat.size, value });
|
|
506
|
+
return value;
|
|
507
|
+
}
|
|
508
|
+
};
|
|
509
|
+
var isRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
510
|
+
var RegistryReader = class {
|
|
511
|
+
databases = /* @__PURE__ */ new Map();
|
|
512
|
+
files = new JsonCache();
|
|
513
|
+
read(dir) {
|
|
514
|
+
const path = join2(dir, "sessions.sqlite");
|
|
515
|
+
if (!isRegularFile(path)) {
|
|
516
|
+
this.databases.get(path)?.db.close();
|
|
517
|
+
this.databases.delete(path);
|
|
518
|
+
return this.files.read(join2(dir, "sessions.json"), normalizeRegistry);
|
|
519
|
+
}
|
|
520
|
+
let open = this.databases.get(path);
|
|
521
|
+
if (!open) {
|
|
522
|
+
open = { db: new DatabaseSync(path, { readOnly: true }), version: void 0, value: {} };
|
|
523
|
+
this.databases.set(path, open);
|
|
524
|
+
}
|
|
525
|
+
const version2 = open.db.prepare("PRAGMA data_version").get()?.data_version;
|
|
526
|
+
if (version2 !== open.version) {
|
|
527
|
+
const rows = {};
|
|
528
|
+
for (const row of open.db.prepare("SELECT id, payload FROM sessions").all())
|
|
529
|
+
rows[String(row.id)] = JSON.parse(String(row.payload));
|
|
530
|
+
open.value = normalizeRegistry(rows) ?? {};
|
|
531
|
+
open.version = version2;
|
|
532
|
+
}
|
|
533
|
+
return open.value;
|
|
534
|
+
}
|
|
535
|
+
};
|
|
536
|
+
var TaskStore = class {
|
|
537
|
+
constructor(roots) {
|
|
538
|
+
this.roots = roots;
|
|
539
|
+
}
|
|
540
|
+
roots;
|
|
541
|
+
tasks = new JsonCache();
|
|
542
|
+
json = new JsonCache();
|
|
543
|
+
registries = new RegistryReader();
|
|
544
|
+
storeDirs() {
|
|
545
|
+
const dirs = /* @__PURE__ */ new Set();
|
|
546
|
+
for (const agent of this.roots.agentDirs) {
|
|
547
|
+
const registry = this.json.read(join2(agent, "rpc", "task-stores.json"), (v) => isRecord(v) ? v : void 0);
|
|
548
|
+
if (isRecord(registry?.stores)) for (const dir of Object.keys(registry.stores)) dirs.add(dir);
|
|
549
|
+
for (const project of list(join2(agent, "projects"))) dirs.add(join2(agent, "projects", project, "senpi-task"));
|
|
550
|
+
}
|
|
551
|
+
return [...dirs];
|
|
552
|
+
}
|
|
553
|
+
loadTasks() {
|
|
554
|
+
const out = /* @__PURE__ */ new Map();
|
|
555
|
+
for (const dir of this.storeDirs()) {
|
|
556
|
+
for (const file of list(join2(dir, "tasks"))) {
|
|
557
|
+
if (!file.endsWith(".json")) continue;
|
|
558
|
+
const path = join2(dir, "tasks", file);
|
|
559
|
+
if (!isRegularFile(path)) continue;
|
|
560
|
+
const task = this.tasks.read(path, normalizeTask);
|
|
561
|
+
if (task) out.set(JSON.stringify([task.parent_session_id, task.task_id]), task);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
return [...out.values()];
|
|
565
|
+
}
|
|
566
|
+
loadRegistry() {
|
|
567
|
+
const merged = {};
|
|
568
|
+
for (const dir of [...this.roots.stateDirs].reverse()) {
|
|
569
|
+
const reg = this.registries.read(dir);
|
|
570
|
+
if (reg) Object.assign(merged, reg);
|
|
571
|
+
}
|
|
572
|
+
return merged;
|
|
573
|
+
}
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
// extensions/antechamber/src/graph/routes.ts
|
|
577
|
+
var MAX_TASK_STORES = 32;
|
|
578
|
+
var MAX_GRAPH_BODY_CHARS = 24e4;
|
|
579
|
+
function canonicalRoots(roots) {
|
|
580
|
+
return {
|
|
581
|
+
agentDirs: roots.agentDirs.map((path) => resolve2(expandHome(path))),
|
|
582
|
+
stateDirs: roots.stateDirs.map((path) => resolve2(expandHome(path)))
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
function graphWithTaskLimit(graph, taskCount) {
|
|
586
|
+
const root = graph.nodes.find((node) => node.kind === "root");
|
|
587
|
+
if (!root) return graph;
|
|
588
|
+
const tasks = graph.nodes.filter((node) => node.kind === "task");
|
|
589
|
+
const shown = [root, ...tasks.slice(0, taskCount)];
|
|
590
|
+
const shownIds = new Set(shown.map((node) => node.id));
|
|
591
|
+
const columns = /* @__PURE__ */ new Map();
|
|
592
|
+
for (const node of shown) {
|
|
593
|
+
if (node.kind === "root") continue;
|
|
594
|
+
const column = columns.get(node.column) ?? { column: node.column, total: 0, done: 0, running: 0 };
|
|
595
|
+
column.total++;
|
|
596
|
+
if (node.state === "completed" || node.state === "failed" || node.state === "cancelled") column.done++;
|
|
597
|
+
if (node.state === "running") column.running++;
|
|
598
|
+
columns.set(node.column, column);
|
|
599
|
+
}
|
|
600
|
+
return {
|
|
601
|
+
...graph,
|
|
602
|
+
nodes: shown,
|
|
603
|
+
edges: graph.edges.filter((edge) => shownIds.has(edge.from) && shownIds.has(edge.to)),
|
|
604
|
+
columns: [...columns.values()].sort((a, b) => a.column - b.column),
|
|
605
|
+
...taskCount < tasks.length ? { truncated: { omittedNodes: tasks.length - taskCount } } : {}
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
function boundedGraph(graph) {
|
|
609
|
+
if (JSON.stringify(graph).length <= MAX_GRAPH_BODY_CHARS) return graph;
|
|
610
|
+
const totalTasks = graph.nodes.filter((node) => node.kind === "task").length;
|
|
611
|
+
let low = 0;
|
|
612
|
+
let high = totalTasks;
|
|
613
|
+
while (low < high) {
|
|
614
|
+
const count = Math.ceil((low + high) / 2);
|
|
615
|
+
if (JSON.stringify(graphWithTaskLimit(graph, count)).length <= MAX_GRAPH_BODY_CHARS) low = count;
|
|
616
|
+
else high = count - 1;
|
|
617
|
+
}
|
|
618
|
+
return graphWithTaskLimit(graph, low);
|
|
619
|
+
}
|
|
620
|
+
function rootsFrom(query, fallback = defaultRoots()) {
|
|
621
|
+
const pick = (key, base) => {
|
|
622
|
+
const value = query.get(key)?.trim();
|
|
623
|
+
return value ? [expandHome(value)] : base;
|
|
624
|
+
};
|
|
625
|
+
return canonicalRoots({
|
|
626
|
+
agentDirs: pick("agentDir", fallback.agentDirs),
|
|
627
|
+
stateDirs: pick("stateDir", fallback.stateDirs)
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
function createServer(token2, makeStore = (r) => new TaskStore(r)) {
|
|
631
|
+
const stores = /* @__PURE__ */ new Map();
|
|
632
|
+
const storeFor = (roots) => {
|
|
633
|
+
const key = JSON.stringify(roots);
|
|
634
|
+
const cached = stores.get(key);
|
|
635
|
+
if (cached) {
|
|
636
|
+
stores.delete(key);
|
|
637
|
+
stores.set(key, cached);
|
|
638
|
+
return cached;
|
|
639
|
+
}
|
|
640
|
+
const store = makeStore(roots);
|
|
641
|
+
stores.set(key, store);
|
|
642
|
+
if (stores.size > MAX_TASK_STORES) {
|
|
643
|
+
const oldest = stores.keys().next().value;
|
|
644
|
+
if (oldest !== void 0) stores.delete(oldest);
|
|
645
|
+
}
|
|
646
|
+
return store;
|
|
647
|
+
};
|
|
648
|
+
const json = (res, status, body) => {
|
|
649
|
+
res.writeHead(status, { "Content-Type": "application/json", "Cache-Control": "no-store" });
|
|
650
|
+
res.end(JSON.stringify(body));
|
|
651
|
+
};
|
|
652
|
+
return http.createServer((req, res) => {
|
|
653
|
+
if (req.headers.authorization !== `Bearer ${token2}`) return json(res, 401, { error: "unauthorized" });
|
|
654
|
+
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
655
|
+
if (url.pathname === "/health") return json(res, 200, { ok: true });
|
|
656
|
+
if (url.pathname === "/graph" && req.method === "GET") {
|
|
657
|
+
const session = url.searchParams.get("session");
|
|
658
|
+
if (!session || !/^ses_[0-9A-Za-z]+$/.test(session))
|
|
659
|
+
return json(res, 400, { error: "session query must be an OpenChamber session id" });
|
|
660
|
+
try {
|
|
661
|
+
const store = storeFor(rootsFrom(url.searchParams));
|
|
662
|
+
return json(res, 200, boundedGraph(buildGraph(session, store.loadTasks(), store.loadRegistry())));
|
|
663
|
+
} catch (error) {
|
|
664
|
+
return json(res, 500, { error: error instanceof Error ? error.message : String(error) });
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
return json(res, 404, { error: "not-found" });
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// extensions/antechamber/src/work/routes.ts
|
|
672
|
+
import { timingSafeEqual } from "node:crypto";
|
|
673
|
+
import http2 from "node:http";
|
|
674
|
+
import { resolve as resolve3 } from "node:path";
|
|
675
|
+
|
|
676
|
+
// extensions/antechamber/src/work/discovery.ts
|
|
677
|
+
import { readdirSync as readdirSync3, readFileSync as readFileSync3 } from "node:fs";
|
|
678
|
+
import { homedir as homedir3 } from "node:os";
|
|
679
|
+
import { join as join3 } from "node:path";
|
|
680
|
+
function expandHome2(path) {
|
|
681
|
+
if (path === "~") return homedir3();
|
|
682
|
+
return path.startsWith("~/") ? join3(homedir3(), path.slice(2)) : path;
|
|
683
|
+
}
|
|
684
|
+
function defaultStateDirs(home = homedir3()) {
|
|
685
|
+
return [join3(home, ".config", "antechamber"), join3(home, ".config", "openchamber-pi-connector")];
|
|
686
|
+
}
|
|
687
|
+
function alive(pid) {
|
|
688
|
+
try {
|
|
689
|
+
process.kill(pid, 0);
|
|
690
|
+
return true;
|
|
691
|
+
} catch (error) {
|
|
692
|
+
return error instanceof Error && "code" in error && error.code === "EPERM";
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
function loopbackUrl(url) {
|
|
696
|
+
return url.replace("://0.0.0.0:", "://127.0.0.1:").replace("://[::]:", "://[::1]:");
|
|
697
|
+
}
|
|
698
|
+
function discoverServers(stateDirs, isAlive = alive) {
|
|
699
|
+
const servers = /* @__PURE__ */ new Map();
|
|
700
|
+
for (const dir of stateDirs) {
|
|
701
|
+
let names;
|
|
702
|
+
try {
|
|
703
|
+
names = readdirSync3(join3(dir, "run"));
|
|
704
|
+
} catch {
|
|
705
|
+
continue;
|
|
706
|
+
}
|
|
707
|
+
for (const name of names) {
|
|
708
|
+
if (!/^\d+\.json$/.test(name)) continue;
|
|
709
|
+
try {
|
|
710
|
+
const record = JSON.parse(readFileSync3(join3(dir, "run", name), "utf8"));
|
|
711
|
+
if (record === null || typeof record !== "object") continue;
|
|
712
|
+
const { pid, url, token: token2, started } = record;
|
|
713
|
+
if (typeof pid !== "number" || typeof url !== "string" || typeof token2 !== "string") continue;
|
|
714
|
+
if (typeof started !== "string" || servers.has(pid) || !isAlive(pid)) continue;
|
|
715
|
+
servers.set(pid, { pid, url: loopbackUrl(url), token: token2, started });
|
|
716
|
+
} catch {
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
return [...servers.values()].sort((a, b) => b.started.localeCompare(a.started));
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// extensions/antechamber/src/work/routes.ts
|
|
724
|
+
var SESSION_ID = /^ses_[0-9A-Za-z]+$/;
|
|
725
|
+
var STATUS_TIMEOUT_MS = 5e3;
|
|
726
|
+
var ACTION_TIMEOUT_MS = 15e3;
|
|
727
|
+
async function call(server, method, path, timeoutMs) {
|
|
728
|
+
const response = await fetch(`${server.url}/antechamber/work-status/${path}`, {
|
|
729
|
+
method,
|
|
730
|
+
headers: { Authorization: `Bearer ${server.token}` },
|
|
731
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
732
|
+
});
|
|
733
|
+
return { status: response.status, body: await response.text() };
|
|
734
|
+
}
|
|
735
|
+
function createServer2(options) {
|
|
736
|
+
const discover = options.discover ?? ((dirs) => discoverServers(dirs));
|
|
737
|
+
const defaultDirs = options.defaultDirs ?? (() => defaultStateDirs());
|
|
738
|
+
const owners = /* @__PURE__ */ new Map();
|
|
739
|
+
const send = (res, status, body) => {
|
|
740
|
+
res.writeHead(status, { "Content-Type": "application/json", "Cache-Control": "no-store" });
|
|
741
|
+
res.end(body);
|
|
742
|
+
};
|
|
743
|
+
const fail = (res, status, error, detail) => send(res, status, JSON.stringify({ error, ...detail !== void 0 ? { detail } : {} }));
|
|
744
|
+
const ordered = (session, stateDirs) => {
|
|
745
|
+
const servers = discover(stateDirs);
|
|
746
|
+
const owner = owners.get(session);
|
|
747
|
+
return owner === void 0 ? servers : [...servers].sort((a, b) => Number(b.pid === owner) - Number(a.pid === owner));
|
|
748
|
+
};
|
|
749
|
+
const relayStatus = async (res, session, stateDirs) => {
|
|
750
|
+
const servers = ordered(session, stateDirs);
|
|
751
|
+
if (servers.length === 0)
|
|
752
|
+
return fail(
|
|
753
|
+
res,
|
|
754
|
+
503,
|
|
755
|
+
"no-server",
|
|
756
|
+
`No running antechamber server with a work-status token in ${stateDirs.join(", ")}`
|
|
757
|
+
);
|
|
758
|
+
let fallback;
|
|
759
|
+
let lastError;
|
|
760
|
+
for (const server of servers) {
|
|
761
|
+
try {
|
|
762
|
+
const upstream = await call(server, "GET", encodeURIComponent(session), STATUS_TIMEOUT_MS);
|
|
763
|
+
if (upstream.status !== 200) {
|
|
764
|
+
lastError = `${server.url} answered HTTP ${upstream.status}`;
|
|
765
|
+
continue;
|
|
766
|
+
}
|
|
767
|
+
if (JSON.parse(upstream.body).live === true) {
|
|
768
|
+
owners.set(session, server.pid);
|
|
769
|
+
return send(res, 200, upstream.body);
|
|
770
|
+
}
|
|
771
|
+
fallback ??= { server, upstream };
|
|
772
|
+
} catch (error) {
|
|
773
|
+
lastError = `${server.url}: ${error instanceof Error ? error.message : String(error)}`;
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
if (fallback) {
|
|
777
|
+
owners.set(session, fallback.server.pid);
|
|
778
|
+
return send(res, 200, fallback.upstream.body);
|
|
779
|
+
}
|
|
780
|
+
return fail(res, 502, "unreachable", lastError);
|
|
781
|
+
};
|
|
782
|
+
const relayAction = async (res, session, stateDirs, method, path) => {
|
|
783
|
+
const server = ordered(session, stateDirs)[0];
|
|
784
|
+
if (server === void 0) return fail(res, 503, "no-server");
|
|
785
|
+
try {
|
|
786
|
+
const upstream = await call(server, method, `${encodeURIComponent(session)}/${path}`, ACTION_TIMEOUT_MS);
|
|
787
|
+
return send(res, upstream.status, upstream.body || "{}");
|
|
788
|
+
} catch (error) {
|
|
789
|
+
return fail(res, 502, "unreachable", error instanceof Error ? error.message : String(error));
|
|
790
|
+
}
|
|
791
|
+
};
|
|
792
|
+
const expected = Buffer.from(`Bearer ${options.token}`);
|
|
793
|
+
return http2.createServer((req, res) => {
|
|
794
|
+
const supplied = Buffer.from(req.headers.authorization ?? "");
|
|
795
|
+
if (supplied.length !== expected.length || !timingSafeEqual(supplied, expected))
|
|
796
|
+
return fail(res, 401, "unauthorized");
|
|
797
|
+
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
798
|
+
if (url.pathname === "/health") return send(res, 200, '{"ok":true}');
|
|
799
|
+
const session = url.searchParams.get("session") ?? "";
|
|
800
|
+
if (!SESSION_ID.test(session)) return fail(res, 400, "session query must be an OpenChamber session id");
|
|
801
|
+
const custom = url.searchParams.get("stateDir")?.trim();
|
|
802
|
+
const stateDirs = custom ? [resolve3(expandHome2(custom))] : defaultDirs();
|
|
803
|
+
const method = req.method ?? "GET";
|
|
804
|
+
req.resume();
|
|
805
|
+
if (url.pathname === "/status" && method === "GET") return void relayStatus(res, session, stateDirs);
|
|
806
|
+
if (url.pathname === "/goal/fire" && method === "POST")
|
|
807
|
+
return void relayAction(res, session, stateDirs, "POST", "goal/fire");
|
|
808
|
+
return fail(res, 404, "not-found");
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// extensions/antechamber/src/service.ts
|
|
813
|
+
function createService(options) {
|
|
814
|
+
const deps = options.deps ?? defaultDeps();
|
|
815
|
+
const graph = createServer(options.token);
|
|
816
|
+
const work = createServer2({ token: options.token });
|
|
817
|
+
let installing;
|
|
818
|
+
const install = () => {
|
|
819
|
+
installing ??= ensureRuntime(options.paths, options.bun, deps).catch(() => void 0).finally(() => {
|
|
820
|
+
installing = void 0;
|
|
821
|
+
});
|
|
822
|
+
return installing;
|
|
823
|
+
};
|
|
824
|
+
const status = () => {
|
|
825
|
+
const current = runtimeState(options.paths, deps);
|
|
826
|
+
const base = {
|
|
827
|
+
state: installing && current.state !== "ready" ? "installing" : current.state,
|
|
828
|
+
spec: options.paths.spec,
|
|
829
|
+
launcher: join4(options.extensionDir, "bin", "opencode-antechamber"),
|
|
830
|
+
bun: options.bun ?? null
|
|
831
|
+
};
|
|
832
|
+
if (current.state === "ready")
|
|
833
|
+
return {
|
|
834
|
+
...base,
|
|
835
|
+
antechamber: current.runtime.antechamber,
|
|
836
|
+
dir: current.runtime.dir,
|
|
837
|
+
...current.runtime.omo ? { omo: current.runtime.omo } : {}
|
|
838
|
+
};
|
|
839
|
+
if (current.state === "failed" && !installing) return { ...base, error: current.error };
|
|
840
|
+
if (!options.bun) return { ...base, state: "failed", error: "bun not found on PATH or in ~/.bun/bin" };
|
|
841
|
+
return base;
|
|
842
|
+
};
|
|
843
|
+
if (options.prewarm && options.bun && runtimeState(options.paths, deps).state === "missing") void install();
|
|
844
|
+
const expected = Buffer.from(`Bearer ${options.token}`);
|
|
845
|
+
const send = (res, code, body) => {
|
|
846
|
+
res.writeHead(code, { "Content-Type": "application/json", "Cache-Control": "no-store" });
|
|
847
|
+
res.end(JSON.stringify(body));
|
|
848
|
+
};
|
|
849
|
+
return createHttpServer((req, res) => {
|
|
850
|
+
const supplied = Buffer.from(req.headers.authorization ?? "");
|
|
851
|
+
if (supplied.length !== expected.length || !timingSafeEqual2(supplied, expected))
|
|
852
|
+
return send(res, 401, { error: "unauthorized" });
|
|
853
|
+
const { pathname } = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
854
|
+
if (pathname === "/health") return send(res, 200, { ok: true });
|
|
855
|
+
if (pathname === "/graph") return void graph.emit("request", req, res);
|
|
856
|
+
if (pathname === "/status" || pathname === "/goal/fire") return void work.emit("request", req, res);
|
|
857
|
+
if (pathname === "/runtime" && req.method === "GET") return send(res, 200, status());
|
|
858
|
+
if (pathname === "/runtime/install" && req.method === "POST") {
|
|
859
|
+
req.resume();
|
|
860
|
+
if (options.bun) void install();
|
|
861
|
+
return send(res, 202, status());
|
|
862
|
+
}
|
|
863
|
+
return send(res, 404, { error: "not-found" });
|
|
864
|
+
});
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
// extensions/antechamber/src/service-main.ts
|
|
868
|
+
var port = Number(process.env.OPENCHAMBER_SERVICE_PORT);
|
|
869
|
+
var token = process.env.OPENCHAMBER_SERVICE_TOKEN ?? "";
|
|
870
|
+
if (!port || !token) {
|
|
871
|
+
console.error("OPENCHAMBER_SERVICE_PORT and OPENCHAMBER_SERVICE_TOKEN are required");
|
|
872
|
+
process.exit(1);
|
|
873
|
+
}
|
|
874
|
+
var extensionDir = resolve4(dirname(fileURLToPath(import.meta.url)), "..");
|
|
875
|
+
var { version } = JSON.parse(readFileSync4(join5(extensionDir, "package.json"), "utf8"));
|
|
876
|
+
chmodSync(join5(extensionDir, "bin", "opencode-antechamber"), 493);
|
|
877
|
+
var env = defaultEnv();
|
|
878
|
+
createService({
|
|
879
|
+
token,
|
|
880
|
+
extensionDir,
|
|
881
|
+
paths: runtimePaths(version, env),
|
|
882
|
+
bun: findBun(env),
|
|
883
|
+
prewarm: true
|
|
884
|
+
}).listen(port, "127.0.0.1");
|