antechamber 0.0.0-stage → 0.1.0

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.
Files changed (87) hide show
  1. package/CHANGELOG.md +143 -0
  2. package/LICENSE +22 -0
  3. package/README.md +316 -2
  4. package/bin/opencode-antechamber +18 -0
  5. package/bin/opencode-pi +3 -0
  6. package/dist/agent-dir.d.ts +6 -0
  7. package/dist/agent-dir.js +21 -0
  8. package/dist/agent-name.d.ts +24 -0
  9. package/dist/agent-name.js +110 -0
  10. package/dist/approvals.d.ts +86 -0
  11. package/dist/approvals.js +520 -0
  12. package/dist/catalog.d.ts +88 -0
  13. package/dist/catalog.js +118 -0
  14. package/dist/cli.d.ts +2 -0
  15. package/dist/cli.js +86 -0
  16. package/dist/commands/context.d.ts +26 -0
  17. package/dist/commands/context.js +89 -0
  18. package/dist/commands/doctor.d.ts +13 -0
  19. package/dist/commands/doctor.js +92 -0
  20. package/dist/commands/help.d.ts +1 -0
  21. package/dist/commands/help.js +15 -0
  22. package/dist/commands/serve.d.ts +8 -0
  23. package/dist/commands/serve.js +29 -0
  24. package/dist/commands/setup.d.ts +14 -0
  25. package/dist/commands/setup.js +137 -0
  26. package/dist/config.d.ts +18 -0
  27. package/dist/config.js +40 -0
  28. package/dist/http.d.ts +18 -0
  29. package/dist/http.js +168 -0
  30. package/dist/lifecycle.d.ts +43 -0
  31. package/dist/lifecycle.js +138 -0
  32. package/dist/misc-routes.d.ts +8 -0
  33. package/dist/misc-routes.js +119 -0
  34. package/dist/pi-event-translation.d.ts +65 -0
  35. package/dist/pi-event-translation.js +545 -0
  36. package/dist/pi-rpc.d.ts +68 -0
  37. package/dist/pi-rpc.js +285 -0
  38. package/dist/pidfd.d.ts +2 -0
  39. package/dist/pidfd.js +28 -0
  40. package/dist/question-forms.d.ts +30 -0
  41. package/dist/question-forms.js +46 -0
  42. package/dist/route-services.d.ts +26 -0
  43. package/dist/route-services.js +33 -0
  44. package/dist/routes-v1.d.ts +4 -0
  45. package/dist/routes-v1.js +346 -0
  46. package/dist/routes-v2.d.ts +4 -0
  47. package/dist/routes-v2.js +477 -0
  48. package/dist/server.d.ts +28 -0
  49. package/dist/server.js +113 -0
  50. package/dist/session-commands.d.ts +75 -0
  51. package/dist/session-commands.js +323 -0
  52. package/dist/session-diff.d.ts +14 -0
  53. package/dist/session-diff.js +152 -0
  54. package/dist/session-history.d.ts +35 -0
  55. package/dist/session-history.js +416 -0
  56. package/dist/session-hosts.d.ts +3 -0
  57. package/dist/session-hosts.js +188 -0
  58. package/dist/session-store.d.ts +103 -0
  59. package/dist/session-store.js +339 -0
  60. package/dist/session-tree.d.ts +59 -0
  61. package/dist/session-tree.js +324 -0
  62. package/dist/sessions.d.ts +174 -0
  63. package/dist/sessions.js +1141 -0
  64. package/dist/sse.d.ts +19 -0
  65. package/dist/sse.js +92 -0
  66. package/dist/subagents.d.ts +64 -0
  67. package/dist/subagents.js +621 -0
  68. package/dist/thinking-levels.d.ts +17 -0
  69. package/dist/thinking-levels.js +24 -0
  70. package/dist/todo-bridge.d.ts +26 -0
  71. package/dist/todo-bridge.js +100 -0
  72. package/dist/tool-map.d.ts +32 -0
  73. package/dist/tool-map.js +349 -0
  74. package/dist/types.d.ts +338 -0
  75. package/dist/types.js +6 -0
  76. package/dist/v2.d.ts +190 -0
  77. package/dist/v2.js +553 -0
  78. package/dist/version.d.ts +2 -0
  79. package/dist/version.js +13 -0
  80. package/extensions/omo-dag/README.md +159 -0
  81. package/extensions/omo-dag/docs/DESIGN.md +105 -0
  82. package/extensions/omo-dag/icon.svg +1 -0
  83. package/extensions/omo-dag/package.json +33 -0
  84. package/extensions/omo-dag/panel/index.html +179 -0
  85. package/extensions/omo-dag/panel/main.js +176 -0
  86. package/extensions/omo-dag/service/main.js +378 -0
  87. package/package.json +77 -4
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Model/provider catalog: obtained from a throwaway `pi --mode rpc` process
3
+ * (`get_available_models` + `get_state`), translated into OpenCode v2
4
+ * Provider/Model shapes, cached for the adapter lifetime.
5
+ */
6
+ import { PiRpcClient } from "./pi-rpc.js";
7
+ import { thinkingVariants } from "./thinking-levels.js";
8
+ /**
9
+ * OpenChamber preselects an agent's `model` + `variant` for new sessions; publish
10
+ * pi's active model and its resolved thinking level so "Default" is pi's level.
11
+ */
12
+ export function agentDefaults(cat) {
13
+ if (cat.default === undefined)
14
+ return {};
15
+ return cat.defaultThinkingLevel !== undefined
16
+ ? { model: cat.default, variant: cat.defaultThinkingLevel }
17
+ : { model: cat.default };
18
+ }
19
+ function toOCModel(m) {
20
+ const inputs = new Set(m.input ?? ["text"]);
21
+ const modality = (name) => inputs.has(name);
22
+ const variants = thinkingVariants(m);
23
+ return {
24
+ id: m.id,
25
+ providerID: m.provider,
26
+ api: { id: m.id, url: m.baseUrl ?? "", npm: "@ai-sdk/openai-compatible" },
27
+ name: m.name ?? m.id,
28
+ capabilities: {
29
+ temperature: true,
30
+ reasoning: m.reasoning ?? false,
31
+ attachment: modality("image"),
32
+ toolcall: true,
33
+ input: { text: modality("text"), audio: false, image: modality("image"), video: false, pdf: false },
34
+ output: { text: true, audio: false, image: false, video: false, pdf: false },
35
+ interleaved: m.reasoning ? true : false,
36
+ },
37
+ cost: {
38
+ input: m.cost?.input ?? 0,
39
+ output: m.cost?.output ?? 0,
40
+ cache: { read: m.cost?.cacheRead ?? 0, write: m.cost?.cacheWrite ?? 0 },
41
+ },
42
+ limit: { context: m.contextWindow ?? 0, output: m.maxTokens ?? 0 },
43
+ options: {},
44
+ ...(variants !== undefined ? { variants } : {}),
45
+ };
46
+ }
47
+ export async function loadCatalog(directory) {
48
+ const pi = new PiRpcClient({ cwd: directory });
49
+ try {
50
+ const [modelsRes, firstState] = await Promise.all([
51
+ pi.command({ type: "get_available_models" }),
52
+ pi.command({ type: "get_state" }),
53
+ ]);
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
+ // get_state occasionally races pi's own startup. One retry beats resolving
66
+ // no active model, which is precisely what leaves the UI picker guessing.
67
+ let active = firstState.success ? (firstState.data?.model ?? null) : null;
68
+ let thinkingLevel = firstState.success ? firstState.data?.thinkingLevel : undefined;
69
+ if (active === null) {
70
+ const retry = await pi.command({ type: "get_state" });
71
+ active = retry.success ? (retry.data?.model ?? null) : null;
72
+ thinkingLevel = retry.success ? retry.data?.thinkingLevel : undefined;
73
+ }
74
+ if (active === null) {
75
+ 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
+ }
77
+ const defaults = {};
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 };
114
+ }
115
+ finally {
116
+ await pi.dispose();
117
+ }
118
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env node
2
+ /** CLI parsing and dispatch. Server code is loaded only for serve/default mode. */
3
+ import { parseArgs } from "node:util";
4
+ import { ADAPTER_VERSION, OPENCODE_API_VERSION } from "./version.js";
5
+ try {
6
+ const argv = process.argv.slice(2);
7
+ // Keep this probe byte-identical: OpenChamber expects an OpenCode 2.x version.
8
+ if (argv.includes("--version") || argv.includes("-v")) {
9
+ console.log(`opencode v${OPENCODE_API_VERSION}`);
10
+ }
11
+ else {
12
+ const { values, positionals } = parseArgs({
13
+ args: argv,
14
+ allowPositionals: true,
15
+ options: {
16
+ help: { type: "boolean", short: "h" },
17
+ port: { type: "string" },
18
+ host: { type: "string" },
19
+ hostname: { type: "string" },
20
+ directory: { type: "string" },
21
+ "insecure-no-auth": { type: "boolean" },
22
+ settings: { type: "string" },
23
+ profile: { type: "string" },
24
+ binary: { type: "string" },
25
+ "dry-run": { type: "boolean" },
26
+ systemd: { type: "boolean" },
27
+ "pi-binary": { type: "string" },
28
+ restart: { type: "boolean" },
29
+ json: { type: "boolean" },
30
+ },
31
+ });
32
+ const command = values.help ? "help" : (positionals[0] ?? "serve");
33
+ if (command === "help") {
34
+ const { help } = await import("./commands/help.js");
35
+ console.log(help);
36
+ }
37
+ else if (command === "version") {
38
+ console.log(`antechamber ${ADAPTER_VERSION} (OpenCode API v${OPENCODE_API_VERSION})`);
39
+ }
40
+ else if (command === "setup") {
41
+ if (positionals.length > (values.systemd ? 2 : 1))
42
+ throw new Error("Unexpected setup argument");
43
+ const { setup } = await import("./commands/setup.js");
44
+ setup({
45
+ ...(values.settings !== undefined ? { settings: values.settings } : {}),
46
+ ...(values.profile !== undefined ? { profile: values.profile } : {}),
47
+ ...(values.binary !== undefined ? { binary: values.binary } : {}),
48
+ ...(values["pi-binary"] !== undefined ? { piBinary: values["pi-binary"] } : {}),
49
+ ...(values.systemd ? { systemd: positionals[1] ?? "openchamber.service" } : {}),
50
+ dryRun: values["dry-run"] ?? false,
51
+ restart: values.restart ?? false,
52
+ });
53
+ }
54
+ else if (command === "doctor") {
55
+ const { doctor, formatDoctor } = await import("./commands/doctor.js");
56
+ const report = doctor({
57
+ ...(values.settings !== undefined ? { settings: values.settings } : {}),
58
+ ...(values.profile !== undefined ? { profile: values.profile } : {}),
59
+ });
60
+ console.log(formatDoctor(report, values.json ?? false));
61
+ process.exitCode = report.exitCode;
62
+ }
63
+ else if (command === "serve") {
64
+ if (positionals.length > 1)
65
+ throw new Error("Unexpected serve argument");
66
+ const { serve } = await import("./commands/serve.js");
67
+ const port = Number(values.port ?? "4096");
68
+ if (!Number.isInteger(port) || port < 0 || port > 65535)
69
+ throw new Error("Invalid --port: expected 0-65535");
70
+ await serve({
71
+ port,
72
+ host: values.host ?? values.hostname ?? "127.0.0.1",
73
+ directory: values.directory ?? process.cwd(),
74
+ insecureNoAuth: values["insecure-no-auth"] ?? false,
75
+ compatible: positionals[0] === "serve",
76
+ });
77
+ }
78
+ else {
79
+ throw new Error(`Unknown command: ${command}. Run antechamber help.`);
80
+ }
81
+ }
82
+ }
83
+ catch (error) {
84
+ console.error(`antechamber: ${error instanceof Error ? error.message : String(error)}`);
85
+ process.exitCode = 1;
86
+ }
@@ -0,0 +1,26 @@
1
+ import * as fs from "node:fs";
2
+ export interface CommandContext {
3
+ readonly fs: typeof fs;
4
+ readonly env: NodeJS.ProcessEnv;
5
+ readonly home: string;
6
+ readonly cwd: string;
7
+ readonly exec: (binary: string, args: readonly string[]) => string;
8
+ readonly now: () => Date;
9
+ readonly platform: NodeJS.Platform;
10
+ readonly nodeVersion: string;
11
+ readonly output: (line: string) => void;
12
+ readonly cliPath: string;
13
+ readonly launcherPath: string;
14
+ }
15
+ export declare function createContext(overrides?: Partial<CommandContext>): CommandContext;
16
+ export declare const packageCliPath: string;
17
+ /** Resolve executables without shell evaluation, respecting the supplied PATH. */
18
+ export declare function resolveBinary(binary: string, ctx: CommandContext): string | undefined;
19
+ export interface SettingsOptions {
20
+ readonly settings?: string;
21
+ readonly profile?: string;
22
+ }
23
+ export declare function settingsPath(options: SettingsOptions, ctx: CommandContext): string;
24
+ export declare function readSettings(path: string, ctx: CommandContext): Record<string, unknown>;
25
+ export declare function shellQuote(value: string): string;
26
+ export declare function isPackageBinary(binary: string, ctx: CommandContext): boolean;
@@ -0,0 +1,89 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import * as fs from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { delimiter, isAbsolute, join, resolve } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ export function createContext(overrides = {}) {
7
+ const env = overrides.env ?? process.env;
8
+ const cwd = overrides.cwd ?? process.cwd();
9
+ return {
10
+ fs,
11
+ env,
12
+ home: overrides.home ?? env.HOME ?? homedir(),
13
+ cwd,
14
+ exec: (binary, args) => {
15
+ const result = spawnSync(binary, args, {
16
+ env,
17
+ cwd,
18
+ encoding: "utf8",
19
+ timeout: 5000,
20
+ killSignal: "SIGKILL",
21
+ maxBuffer: 1024 * 1024,
22
+ });
23
+ if (result.error)
24
+ throw result.error;
25
+ if (result.status !== 0)
26
+ throw new Error(`${binary} exited ${result.status}: ${result.stderr.trim()}`);
27
+ return result.stdout.trim();
28
+ },
29
+ now: () => new Date(),
30
+ platform: process.platform,
31
+ nodeVersion: process.versions.node,
32
+ output: console.log,
33
+ cliPath: packageCliPath,
34
+ launcherPath: fileURLToPath(new URL("../../bin/opencode-antechamber", import.meta.url)),
35
+ ...overrides,
36
+ };
37
+ }
38
+ export const packageCliPath = fileURLToPath(new URL("../../dist/cli.js", import.meta.url));
39
+ /** Resolve executables without shell evaluation, respecting the supplied PATH. */
40
+ export function resolveBinary(binary, ctx) {
41
+ const candidates = isAbsolute(binary) || binary.includes("/") || binary.includes("\\")
42
+ ? [resolve(ctx.cwd, binary)]
43
+ : (ctx.env.PATH ?? "").split(delimiter).map((dir) => resolve(ctx.cwd, dir, binary));
44
+ for (const candidate of candidates) {
45
+ try {
46
+ ctx.fs.accessSync(candidate, ctx.fs.constants.X_OK);
47
+ if (ctx.fs.statSync(candidate).isFile())
48
+ return candidate;
49
+ }
50
+ catch (error) {
51
+ if (!(error instanceof Error) ||
52
+ !("code" in error) ||
53
+ !["ENOENT", "EACCES", "ENOTDIR"].includes(String(error.code)))
54
+ throw error;
55
+ }
56
+ }
57
+ return undefined;
58
+ }
59
+ export function settingsPath(options, ctx) {
60
+ if (options.settings !== undefined)
61
+ return resolve(ctx.cwd, options.settings);
62
+ const dir = options.profile ?? ctx.env.OPENCHAMBER_DATA_DIR ?? join(ctx.home, ".config", "openchamber");
63
+ return resolve(ctx.cwd, dir, "settings.json");
64
+ }
65
+ export function readSettings(path, ctx) {
66
+ let value;
67
+ try {
68
+ value = JSON.parse(ctx.fs.readFileSync(path, "utf8"));
69
+ }
70
+ catch (error) {
71
+ if (error instanceof SyntaxError)
72
+ throw new Error(`Invalid JSON in settings file ${path}: ${error.message}`);
73
+ throw error;
74
+ }
75
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
76
+ throw new Error(`Invalid settings file ${path}: expected a JSON object`);
77
+ }
78
+ return Object.fromEntries(Object.entries(value));
79
+ }
80
+ export function shellQuote(value) {
81
+ return /^[\w./:@=-]+$/.test(value) ? value : `'${value.replaceAll("'", "'\\''")}'`;
82
+ }
83
+ export function isPackageBinary(binary, ctx) {
84
+ const resolved = resolveBinary(binary, ctx);
85
+ if (resolved === undefined)
86
+ return false;
87
+ const target = ctx.fs.realpathSync(resolved);
88
+ return [ctx.cliPath, ctx.launcherPath].some((path) => ctx.fs.existsSync(path) && ctx.fs.realpathSync(path) === target);
89
+ }
@@ -0,0 +1,13 @@
1
+ import { type CommandContext, type SettingsOptions } from "./context.js";
2
+ export interface DoctorCheck {
3
+ readonly name: string;
4
+ readonly status: "PASS" | "WARN" | "FAIL" | "INFO";
5
+ readonly message: string;
6
+ }
7
+ export interface DoctorReport {
8
+ readonly checks: readonly DoctorCheck[];
9
+ readonly exitCode: 0 | 1;
10
+ }
11
+ /** Checks capabilities with access/stat only, never creating state or config files. */
12
+ export declare function doctor(options?: SettingsOptions, ctx?: CommandContext): DoctorReport;
13
+ export declare function formatDoctor(report: DoctorReport, json: boolean): string;
@@ -0,0 +1,92 @@
1
+ import { dirname, resolve } from "node:path";
2
+ import { piAgentDir } from "../agent-dir.js";
3
+ import { detectAgentName } from "../agent-name.js";
4
+ import { agentBinary, defaultStateDir, envSetting, isPlainPi } from "../config.js";
5
+ import { findPidfdPython } from "../pidfd.js";
6
+ import { createContext, isPackageBinary, readSettings, resolveBinary, settingsPath, } from "./context.js";
7
+ /** Checks capabilities with access/stat only, never creating state or config files. */
8
+ export function doctor(options = {}, ctx = createContext()) {
9
+ const checks = [];
10
+ const add = (name, status, message) => {
11
+ checks.push({ name, status, message });
12
+ };
13
+ add("node", Number(ctx.nodeVersion.split(".")[0]) >= 22 ? "PASS" : "FAIL", `Node ${ctx.nodeVersion}; requires >=22`);
14
+ const backend = agentBinary(ctx.env);
15
+ const binary = resolveBinary(backend, ctx);
16
+ if (binary === undefined)
17
+ add("backend", "FAIL", `${backend} is not an executable on PATH`);
18
+ else {
19
+ try {
20
+ add("backend", "PASS", `${binary}: ${ctx.exec(binary, ["--version"]) || "version command succeeded"}`);
21
+ }
22
+ catch (error) {
23
+ add("backend", "FAIL", `${binary}: ${error instanceof Error ? error.message : String(error)}`);
24
+ }
25
+ }
26
+ add("backend-kind", isPlainPi(ctx.env) ? "WARN" : "PASS", isPlainPi(ctx.env)
27
+ ? "plain pi backend: best effort; OmO features unavailable"
28
+ : `OmO-compatible backend (${backend})`);
29
+ const agentDir = resolve(ctx.cwd, piAgentDir(ctx.env, ctx.home));
30
+ add("agent-dir", ctx.fs.existsSync(agentDir) && ctx.fs.statSync(agentDir).isDirectory() ? "PASS" : "FAIL", agentDir);
31
+ add("agent-name", "PASS", detectAgentName(ctx.env, ctx.home, ctx.fs));
32
+ const stateDir = resolve(ctx.cwd, envSetting("STATE_DIR", ctx.env) ?? defaultStateDir(ctx.home, ctx.fs.existsSync));
33
+ try {
34
+ let ancestor = stateDir;
35
+ while (!ctx.fs.existsSync(ancestor) && dirname(ancestor) !== ancestor)
36
+ ancestor = dirname(ancestor);
37
+ if (!ctx.fs.statSync(ancestor).isDirectory())
38
+ throw new Error(`${ancestor} is not a directory`);
39
+ ctx.fs.accessSync(ancestor, ctx.fs.constants.W_OK | ctx.fs.constants.X_OK);
40
+ add("state-dir", "PASS", `${stateDir}${ancestor === stateDir ? " is writable" : ` can be created under writable ${ancestor}`}`);
41
+ }
42
+ catch (error) {
43
+ add("state-dir", "FAIL", `${stateDir}: ${error instanceof Error ? error.message : String(error)}`);
44
+ }
45
+ const python = ctx.platform === "linux"
46
+ ? findPidfdPython(ctx.env, (candidate, args) => {
47
+ try {
48
+ ctx.exec(candidate, args);
49
+ return true;
50
+ }
51
+ catch (error) {
52
+ if (!(error instanceof Error))
53
+ throw error;
54
+ return false;
55
+ }
56
+ })
57
+ : undefined;
58
+ add("private-host-teardown", python === undefined ? "WARN" : "PASS", python === undefined
59
+ ? "requires Linux and python3 with working pidfd support"
60
+ : `Linux pidfd support via ${python}`);
61
+ const openchamber = resolveBinary("openchamber", ctx);
62
+ if (openchamber === undefined)
63
+ add("openchamber", "WARN", "openchamber is not on PATH");
64
+ else {
65
+ try {
66
+ add("openchamber", "PASS", `${openchamber}: ${ctx.exec(openchamber, ["--version"]) || "version not reported"}`);
67
+ }
68
+ catch (error) {
69
+ add("openchamber", "WARN", `${openchamber}: version not determinable (${error instanceof Error ? error.message : String(error)})`);
70
+ }
71
+ }
72
+ const settings = settingsPath(options, ctx);
73
+ if (!ctx.fs.existsSync(settings))
74
+ add("openchamber-settings", "WARN", `${settings} does not exist; run antechamber setup`);
75
+ else {
76
+ try {
77
+ const configured = readSettings(settings, ctx).opencodeBinary;
78
+ const matches = typeof configured === "string" && isPackageBinary(configured, ctx);
79
+ add("openchamber-settings", matches ? "PASS" : "WARN", `${settings}: opencodeBinary=${JSON.stringify(configured ?? null)}${matches ? "" : "; does not point at this antechamber installation"}`);
80
+ }
81
+ catch (error) {
82
+ add("openchamber-settings", "WARN", error instanceof Error ? error.message : String(error));
83
+ }
84
+ }
85
+ add("auth", "INFO", `OPENCODE_SERVER_PASSWORD is ${ctx.env.OPENCODE_SERVER_PASSWORD ? "set" : "not set"}`);
86
+ return { checks, exitCode: checks.some((check) => check.status === "FAIL") ? 1 : 0 };
87
+ }
88
+ export function formatDoctor(report, json) {
89
+ return json
90
+ ? JSON.stringify(report, null, 2)
91
+ : report.checks.map((check) => `${check.status} ${check.name}: ${check.message}`).join("\n");
92
+ }
@@ -0,0 +1 @@
1
+ export declare const help = "Usage: antechamber [command] [options]\n\nCommands:\n serve Run the OpenCode-compatible HTTP/SSE server (also the default)\n setup Point OpenChamber at this installation; back up changed files\n doctor Read-only installation and backend checks (exit 1 on FAIL)\n version Print the npm package and OpenCode API versions\n help Show this help (--help, -h)\n\nServe: --port 4096 --host 127.0.0.1 (or --hostname) --directory <path>\n --insecure-no-auth\nSetup: --settings <file> | --profile <dir>\n --binary <path> --dry-run --systemd [unit] --pi-binary <path> --restart\nDoctor: --settings <file> | --profile <dir> --json\nProbe: --version, -v prints the OpenCode compatibility version for OpenChamber";
@@ -0,0 +1,15 @@
1
+ export const help = `Usage: antechamber [command] [options]
2
+
3
+ Commands:
4
+ serve Run the OpenCode-compatible HTTP/SSE server (also the default)
5
+ setup Point OpenChamber at this installation; back up changed files
6
+ doctor Read-only installation and backend checks (exit 1 on FAIL)
7
+ version Print the npm package and OpenCode API versions
8
+ help Show this help (--help, -h)
9
+
10
+ Serve: --port 4096 --host 127.0.0.1 (or --hostname) --directory <path>
11
+ --insecure-no-auth
12
+ Setup: --settings <file> | --profile <dir>
13
+ --binary <path> --dry-run --systemd [unit] --pi-binary <path> --restart
14
+ Doctor: --settings <file> | --profile <dir> --json
15
+ Probe: --version, -v prints the OpenCode compatibility version for OpenChamber`;
@@ -0,0 +1,8 @@
1
+ export interface ServeOptions {
2
+ readonly port: number;
3
+ readonly host: string;
4
+ readonly directory: string;
5
+ readonly insecureNoAuth: boolean;
6
+ readonly compatible: boolean;
7
+ }
8
+ export declare function serve(options: ServeOptions): Promise<void>;
@@ -0,0 +1,29 @@
1
+ import { envSetting } from "../config.js";
2
+ import { AdapterServer, isLoopbackHost, serverPassword } from "../server.js";
3
+ export async function serve(options) {
4
+ const { port, host, directory, insecureNoAuth, compatible } = options;
5
+ if (!isLoopbackHost(host) &&
6
+ serverPassword() === undefined &&
7
+ !insecureNoAuth &&
8
+ envSetting("ALLOW_INSECURE") !== "1") {
9
+ throw new Error(`refusing to bind ${host} without authentication. Set OPENCODE_SERVER_PASSWORD or ANTECHAMBER_SERVER_PASSWORD, or explicitly use --insecure-no-auth (ANTECHAMBER_ALLOW_INSECURE=1).`);
10
+ }
11
+ const server = new AdapterServer({ port, host, defaultDirectory: directory });
12
+ const shutdown = () => {
13
+ void server.stop().then(() => process.exit(0), (error) => {
14
+ console.error("antechamber: shutdown failed:", error);
15
+ process.exit(1);
16
+ });
17
+ };
18
+ // Install handlers before announcing readiness: a supervisor may signal as
19
+ // soon as it reads the listening line, before this process runs again.
20
+ process.on("SIGINT", shutdown);
21
+ process.on("SIGTERM", shutdown);
22
+ await server.start();
23
+ if (compatible)
24
+ console.log(`opencode server listening on ${server.url}`);
25
+ else {
26
+ console.log(`antechamber listening on ${server.url}`);
27
+ console.log(`default directory: ${directory}`);
28
+ }
29
+ }
@@ -0,0 +1,14 @@
1
+ import { type CommandContext, type SettingsOptions } from "./context.js";
2
+ export interface SetupOptions extends SettingsOptions {
3
+ readonly binary?: string;
4
+ readonly dryRun?: boolean;
5
+ readonly systemd?: string;
6
+ readonly piBinary?: string;
7
+ readonly restart?: boolean;
8
+ /** User-unit root, overridable without touching the real systemd configuration. */
9
+ readonly systemdDir?: string;
10
+ }
11
+ /** The bundled OmO Graph extension folder, when this installation has its built files. */
12
+ export declare function omoGraphExtensionDir(ctx: CommandContext): string | undefined;
13
+ /** Plan all inputs before applying changes; dependencies are injected for hermetic tests. */
14
+ export declare function setup(options: SetupOptions, ctx?: CommandContext): string[];
@@ -0,0 +1,137 @@
1
+ import { dirname, join, resolve } from "node:path";
2
+ import { createContext, isPackageBinary, readSettings, resolveBinary, settingsPath, shellQuote, } from "./context.js";
3
+ function systemdValue(value) {
4
+ if (/[\r\n\0]/.test(value))
5
+ throw new Error("systemd environment values cannot contain newlines or NUL");
6
+ return value.replaceAll("\\", "\\\\").replaceAll('"', '\\"').replaceAll("%", "%%");
7
+ }
8
+ /** The bundled OmO Graph extension folder, when this installation has its built files. */
9
+ export function omoGraphExtensionDir(ctx) {
10
+ const dir = resolve(dirname(ctx.cliPath), "..", "extensions", "omo-dag");
11
+ const built = ["package.json", "panel/main.js", "service/main.js"].every((file) => ctx.fs.existsSync(join(dir, file)));
12
+ return built ? dir : undefined;
13
+ }
14
+ /** Plan all inputs before applying changes; dependencies are injected for hermetic tests. */
15
+ export function setup(options, ctx = createContext()) {
16
+ const lines = [];
17
+ const report = (line) => {
18
+ lines.push(line);
19
+ ctx.output(line);
20
+ };
21
+ if (options.restart && options.systemd === undefined)
22
+ throw new Error("--restart requires --systemd");
23
+ const settings = settingsPath(options, ctx);
24
+ const existing = ctx.fs.existsSync(settings);
25
+ const data = existing ? readSettings(settings, ctx) : {};
26
+ const onPath = resolveBinary("antechamber", ctx);
27
+ const binary = options.binary !== undefined
28
+ ? resolveBinary(options.binary, ctx)
29
+ : onPath !== undefined && isPackageBinary(onPath, ctx)
30
+ ? onPath
31
+ : ctx.cliPath;
32
+ if (binary === undefined)
33
+ throw new Error(`Cannot resolve executable --binary ${options.binary}`);
34
+ if (binary === ctx.cliPath && !ctx.fs.readFileSync(binary, "utf8").startsWith("#!/usr/bin/env node\n")) {
35
+ throw new Error(`Package CLI ${binary} is missing its node shebang; run npm run build`);
36
+ }
37
+ const updated = `${JSON.stringify({ ...data, opencodeBinary: binary }, null, 2)}\n`;
38
+ const changed = data.opencodeBinary !== binary;
39
+ let dropIn;
40
+ let dropInText;
41
+ let oldDropIn;
42
+ if (options.systemd !== undefined) {
43
+ const unit = options.systemd;
44
+ if (!/^[a-zA-Z0-9_.@:-]+\.service$/.test(unit))
45
+ throw new Error(`Invalid systemd service unit: ${unit}`);
46
+ const backend = resolveBinary(options.piBinary ?? "omo", ctx);
47
+ if (backend === undefined)
48
+ throw new Error(`Cannot resolve backend ${options.piBinary ?? "omo"} for --systemd`);
49
+ const root = options.systemdDir ?? join(ctx.home, ".config", "systemd", "user");
50
+ const dir = resolve(ctx.cwd, root, `${unit}.d`);
51
+ dropIn = join(dir, "antechamber.conf");
52
+ dropInText = [
53
+ "[Service]",
54
+ `Environment="PATH=${systemdValue(ctx.env.PATH ?? "")}"`,
55
+ `Environment="ANTECHAMBER_PI_BINARY=${systemdValue(backend)}"`,
56
+ "KillMode=control-group",
57
+ "TimeoutStopSec=20",
58
+ "",
59
+ ].join("\n");
60
+ if (ctx.fs.existsSync(dropIn))
61
+ oldDropIn = ctx.fs.readFileSync(dropIn, "utf8");
62
+ if (ctx.fs.existsSync(dir)) {
63
+ for (const name of ctx.fs.readdirSync(dir).sort()) {
64
+ if (name === "antechamber.conf" || !name.endsWith(".conf"))
65
+ continue;
66
+ const content = ctx.fs.readFileSync(join(dir, name), "utf8");
67
+ if (content
68
+ .split("\n")
69
+ .some((line) => /^\s*Environment(?:File)?\s*=/.test(line) && /\b(?:ANTECHAMBER_|OCPI_)/.test(line))) {
70
+ report(`WARN ${join(dir, name)} sets ANTECHAMBER_*/OCPI_*; ${name > "antechamber.conf" ? "later files override this drop-in" : "review earlier settings and legacy fallbacks"}.`);
71
+ }
72
+ }
73
+ }
74
+ }
75
+ const stamp = ctx.now().toISOString().replace(/\D/g, "").slice(0, 14);
76
+ const backup = (path) => {
77
+ const target = `${path}.bak-antechamber-${stamp}`;
78
+ if (options.dryRun)
79
+ report(`Would back up ${path} to ${target}`);
80
+ else {
81
+ ctx.fs.copyFileSync(path, target, ctx.fs.constants.COPYFILE_EXCL);
82
+ report(`Backed up ${path} to ${target}`);
83
+ }
84
+ };
85
+ if (binary === ctx.cliPath && (ctx.fs.statSync(binary).mode & 0o111) === 0) {
86
+ if (options.dryRun)
87
+ report(`Would make ${binary} executable`);
88
+ else {
89
+ ctx.fs.chmodSync(binary, ctx.fs.statSync(binary).mode | 0o111);
90
+ report(`Made ${binary} executable`);
91
+ }
92
+ }
93
+ if (changed) {
94
+ report(`${options.dryRun ? "Would update" : "Updating"} ${settings}`);
95
+ report(`- opencodeBinary: ${JSON.stringify(data.opencodeBinary ?? null)}`);
96
+ report(`+ opencodeBinary: ${JSON.stringify(binary)}`);
97
+ if (existing)
98
+ backup(settings);
99
+ if (!options.dryRun) {
100
+ ctx.fs.mkdirSync(dirname(settings), { recursive: true });
101
+ ctx.fs.writeFileSync(settings, updated, { mode: 0o600 });
102
+ report(`Wrote ${settings}`);
103
+ }
104
+ }
105
+ else
106
+ report(`Already configured: ${settings} points at ${binary}`);
107
+ if (dropIn !== undefined && dropInText !== undefined) {
108
+ if (oldDropIn === dropInText)
109
+ report(`Already configured: ${dropIn}`);
110
+ else {
111
+ report(`${options.dryRun ? "Would write" : "Writing"} ${dropIn}:\n${dropInText}`);
112
+ if (oldDropIn !== undefined)
113
+ backup(dropIn);
114
+ if (!options.dryRun) {
115
+ ctx.fs.mkdirSync(dirname(dropIn), { recursive: true });
116
+ ctx.fs.writeFileSync(dropIn, dropInText, { mode: 0o600 });
117
+ report(`Wrote ${dropIn}`);
118
+ }
119
+ }
120
+ const unit = options.systemd ?? "openchamber.service";
121
+ report(`systemctl --user daemon-reload && systemctl --user restart ${shellQuote(unit)}`);
122
+ if (options.restart && !options.dryRun) {
123
+ ctx.exec("systemctl", ["--user", "daemon-reload"]);
124
+ report("Reloaded systemd user units");
125
+ ctx.exec("systemctl", ["--user", "restart", unit]);
126
+ report(`Restarted ${unit}`);
127
+ }
128
+ }
129
+ if (options.profile !== undefined && options.settings === undefined) {
130
+ report(`OPENCHAMBER_DATA_DIR=${shellQuote(resolve(ctx.cwd, options.profile))} openchamber serve`);
131
+ }
132
+ const extension = omoGraphExtensionDir(ctx);
133
+ if (extension !== undefined) {
134
+ report(`Optional OmO Graph extension: in OpenChamber, Settings \u2192 Extensions \u2192 Add \u2192 Folder: ${extension}`);
135
+ }
136
+ return lines;
137
+ }
@@ -0,0 +1,18 @@
1
+ import { existsSync } from "node:fs";
2
+ /** Environment variable prefix for all antechamber settings. */
3
+ export declare const ENV_PREFIX = "ANTECHAMBER_";
4
+ /**
5
+ * Reads `ANTECHAMBER_<name>`, falling back to the deprecated `OCPI_<name>`.
6
+ * Blank values count as unset.
7
+ */
8
+ export declare function envSetting(name: string, env?: NodeJS.ProcessEnv): string | undefined;
9
+ /**
10
+ * Default registry/state directory. Prefers `~/.config/antechamber`; when only
11
+ * the pre-rename directory exists it keeps using that so existing session
12
+ * mappings survive the upgrade.
13
+ */
14
+ export declare function defaultStateDir(home?: string, exists?: typeof existsSync): string;
15
+ /** Agent executable spawned per session: OmO by default, plain pi when configured. */
16
+ export declare function agentBinary(env?: NodeJS.ProcessEnv): string;
17
+ /** True when the configured backend is upstream pi rather than OmO. */
18
+ export declare function isPlainPi(env?: NodeJS.ProcessEnv): boolean;