baa-conductor


baa-conductor / apps / claude-coded / src
im_wower  ·  2026-03-28

config.ts

  1import { resolve } from "node:path";
  2
  3import type {
  4  ClaudeCodedCliAction,
  5  ClaudeCodedEnvironment,
  6  ClaudeCodedResolvedConfig,
  7  ClaudeCodedRuntimePaths
  8} from "./contracts.js";
  9
 10export interface ClaudeCodedConfigInput {
 11  childCommand?: string;
 12  childCwd?: string;
 13  eventCacheSize?: number;
 14  extraArgs?: string[];
 15  localApiBase?: string;
 16  logsDir?: string;
 17  model?: string | null;
 18  nodeId?: string;
 19  repoRoot?: string;
 20  stateDir?: string;
 21  turnTimeoutMs?: number;
 22  version?: string | null;
 23}
 24
 25export type ClaudeCodedCliRequest =
 26  | {
 27      action: "help";
 28    }
 29  | {
 30      action: Exclude<ClaudeCodedCliAction, "help" | "start">;
 31      config: ClaudeCodedResolvedConfig;
 32      printJson: boolean;
 33    }
 34  | {
 35      action: "start";
 36      config: ClaudeCodedResolvedConfig;
 37      printJson: boolean;
 38    };
 39
 40const DEFAULT_EVENT_CACHE_SIZE = 50;
 41const DEFAULT_LOCAL_API_BASE = "http://127.0.0.1:4320";
 42const DEFAULT_NODE_ID = "mini-main";
 43const DEFAULT_CHILD_COMMAND = "claude";
 44const DEFAULT_CHILD_ARGS = [
 45  "-p",
 46  "--input-format", "stream-json",
 47  "--output-format", "stream-json",
 48  "--verbose",
 49  "--replay-user-messages",
 50  "--permission-mode", "bypassPermissions"
 51];
 52const DEFAULT_TURN_TIMEOUT_MS = 300_000;
 53
 54export function resolveClaudeCodedConfig(input: ClaudeCodedConfigInput = {}): ClaudeCodedResolvedConfig {
 55  const repoRoot = resolve(input.repoRoot ?? getDefaultRepoRoot());
 56  const logsRootDir = resolve(getOptionalString(input.logsDir) ?? resolve(repoRoot, "logs"));
 57  const stateRootDir = resolve(getOptionalString(input.stateDir) ?? resolve(repoRoot, "state"));
 58  const paths = resolveRuntimePaths(repoRoot, logsRootDir, stateRootDir);
 59
 60  const model = getOptionalString(input.model);
 61  const extraArgs = input.extraArgs ?? [];
 62  const childArgs = [...DEFAULT_CHILD_ARGS];
 63  if (model != null) {
 64    childArgs.push("--model", model);
 65  }
 66  childArgs.push(...extraArgs);
 67
 68  return {
 69    nodeId: getOptionalString(input.nodeId) ?? DEFAULT_NODE_ID,
 70    version: getOptionalString(input.version),
 71    eventCacheSize: normalizePositiveInteger(input.eventCacheSize, DEFAULT_EVENT_CACHE_SIZE, "event cache"),
 72    turnTimeoutMs: normalizePositiveInteger(input.turnTimeoutMs, DEFAULT_TURN_TIMEOUT_MS, "turn timeout"),
 73    paths,
 74    service: {
 75      localApiBase: getOptionalString(input.localApiBase) ?? DEFAULT_LOCAL_API_BASE
 76    },
 77    child: {
 78      command: getOptionalString(input.childCommand) ?? DEFAULT_CHILD_COMMAND,
 79      args: childArgs,
 80      cwd: resolve(getOptionalString(input.childCwd) ?? repoRoot),
 81      model,
 82      extraArgs
 83    }
 84  };
 85}
 86
 87export function parseClaudeCodedCliRequest(
 88  argv: readonly string[],
 89  env: ClaudeCodedEnvironment = {}
 90): ClaudeCodedCliRequest {
 91  const tokens = argv.slice(2);
 92  let action: ClaudeCodedCliAction = "start";
 93  let actionSet = false;
 94  let printJson = false;
 95  let childCommand = env.BAA_CLAUDE_CODED_CHILD_COMMAND;
 96  let childCwd = env.BAA_CLAUDE_CODED_CHILD_CWD;
 97  let eventCacheSize = parseOptionalInteger(env.BAA_CLAUDE_CODED_EVENT_CACHE_SIZE);
 98  let localApiBase = env.BAA_CLAUDE_CODED_LOCAL_API_BASE;
 99  let logsDir = env.BAA_CLAUDE_CODED_LOGS_DIR ?? env.BAA_LOGS_DIR;
100  let model: string | undefined = env.BAA_CLAUDE_CODED_MODEL;
101  let nodeId = env.BAA_NODE_ID;
102  let repoRoot = env.BAA_CLAUDE_CODED_REPO_ROOT;
103  let stateDir = env.BAA_CLAUDE_CODED_STATE_DIR ?? env.BAA_STATE_DIR;
104  let turnTimeoutMs = parseOptionalInteger(env.BAA_CLAUDE_CODED_TURN_TIMEOUT_MS);
105  let version = env.BAA_CLAUDE_CODED_VERSION ?? null;
106  let extraArgs: string[] = [];
107
108  for (let index = 0; index < tokens.length; index += 1) {
109    const token = tokens[index];
110
111    if (token == null) {
112      continue;
113    }
114
115    if (token === "--help" || token === "-h" || token === "help") {
116      return { action: "help" };
117    }
118
119    if (token === "--json") {
120      printJson = true;
121      continue;
122    }
123
124    if (token === "--repo-root") {
125      repoRoot = readCliValue(tokens, index, "--repo-root");
126      index += 1;
127      continue;
128    }
129
130    if (token === "--node-id") {
131      nodeId = readCliValue(tokens, index, "--node-id");
132      index += 1;
133      continue;
134    }
135
136    if (token === "--logs-dir") {
137      logsDir = readCliValue(tokens, index, "--logs-dir");
138      index += 1;
139      continue;
140    }
141
142    if (token === "--local-api-base") {
143      localApiBase = readCliValue(tokens, index, "--local-api-base");
144      index += 1;
145      continue;
146    }
147
148    if (token === "--state-dir") {
149      stateDir = readCliValue(tokens, index, "--state-dir");
150      index += 1;
151      continue;
152    }
153
154    if (token === "--child-command") {
155      childCommand = readCliValue(tokens, index, "--child-command");
156      index += 1;
157      continue;
158    }
159
160    if (token === "--child-cwd") {
161      childCwd = readCliValue(tokens, index, "--child-cwd");
162      index += 1;
163      continue;
164    }
165
166    if (token === "--model") {
167      model = readCliValue(tokens, index, "--model");
168      index += 1;
169      continue;
170    }
171
172    if (token === "--extra-arg") {
173      extraArgs.push(readCliValue(tokens, index, "--extra-arg"));
174      index += 1;
175      continue;
176    }
177
178    if (token === "--event-cache-size") {
179      eventCacheSize = parseStrictInteger(readCliValue(tokens, index, "--event-cache-size"), "--event-cache-size");
180      index += 1;
181      continue;
182    }
183
184    if (token === "--turn-timeout-ms") {
185      turnTimeoutMs = parseStrictInteger(readCliValue(tokens, index, "--turn-timeout-ms"), "--turn-timeout-ms");
186      index += 1;
187      continue;
188    }
189
190    if (token === "--version") {
191      version = readCliValue(tokens, index, "--version");
192      index += 1;
193      continue;
194    }
195
196    if (token.startsWith("--")) {
197      throw new Error(`Unknown claude-coded flag "${token}".`);
198    }
199
200    if (actionSet) {
201      throw new Error(`Unexpected extra claude-coded argument "${token}".`);
202    }
203
204    if (!isClaudeCodedCliAction(token)) {
205      throw new Error(`Unknown claude-coded action "${token}".`);
206    }
207
208    action = token;
209    actionSet = true;
210  }
211
212  const config = resolveClaudeCodedConfig({
213    childCommand,
214    childCwd,
215    eventCacheSize,
216    extraArgs: extraArgs.length > 0 ? extraArgs : undefined,
217    localApiBase,
218    logsDir,
219    model,
220    nodeId,
221    repoRoot,
222    stateDir,
223    turnTimeoutMs,
224    version
225  });
226
227  return {
228    action,
229    config,
230    printJson
231  };
232}
233
234export function formatClaudeCodedConfigText(config: ClaudeCodedResolvedConfig): string {
235  return [
236    `node_id: ${config.nodeId}`,
237    `version: ${config.version ?? "not-set"}`,
238    `child_command: ${config.child.command}`,
239    `child_args: ${config.child.args.join(" ") || "(none)"}`,
240    `child_cwd: ${config.child.cwd}`,
241    `model: ${config.child.model ?? "default"}`,
242    `local_api_base: ${config.service.localApiBase}`,
243    `logs_dir: ${config.paths.logsDir}`,
244    `state_dir: ${config.paths.stateDir}`,
245    `event_cache_size: ${config.eventCacheSize}`,
246    `turn_timeout_ms: ${config.turnTimeoutMs}`
247  ].join("\n");
248}
249
250export function getClaudeCodedUsageText(): string {
251  return [
252    "Usage:",
253    "  node apps/claude-coded/dist/index.js [start] [options]",
254    "  node apps/claude-coded/dist/index.js status [--json]",
255    "  node apps/claude-coded/dist/index.js config [--json]",
256    "  node apps/claude-coded/dist/index.js help",
257    "",
258    "Options:",
259    "  --repo-root <path>",
260    "  --node-id <id>",
261    "  --logs-dir <path>",
262    "  --local-api-base <http://127.0.0.1:4320>",
263    "  --state-dir <path>",
264    "  --child-command <command>",
265    "  --child-cwd <path>",
266    "  --model <model>",
267    "  --extra-arg <arg>",
268    "  --event-cache-size <integer>",
269    "  --turn-timeout-ms <integer>",
270    "  --version <string>",
271    "  --json",
272    "  --help",
273    "",
274    "Environment:",
275    "  BAA_NODE_ID",
276    "  BAA_LOGS_DIR",
277    "  BAA_STATE_DIR",
278    "  BAA_CLAUDE_CODED_REPO_ROOT",
279    "  BAA_CLAUDE_CODED_LOGS_DIR",
280    "  BAA_CLAUDE_CODED_STATE_DIR",
281    "  BAA_CLAUDE_CODED_LOCAL_API_BASE",
282    "  BAA_CLAUDE_CODED_CHILD_COMMAND",
283    "  BAA_CLAUDE_CODED_CHILD_CWD",
284    "  BAA_CLAUDE_CODED_MODEL",
285    "  BAA_CLAUDE_CODED_EVENT_CACHE_SIZE",
286    "  BAA_CLAUDE_CODED_TURN_TIMEOUT_MS",
287    "  BAA_CLAUDE_CODED_VERSION"
288  ].join("\n");
289}
290
291function getDefaultRepoRoot(): string {
292  if (typeof process !== "undefined" && typeof process.cwd === "function") {
293    return process.cwd();
294  }
295
296  return ".";
297}
298
299function resolveRuntimePaths(
300  repoRoot: string,
301  logsRootDir: string,
302  stateRootDir: string
303): ClaudeCodedRuntimePaths {
304  const logsDir = resolve(logsRootDir, "claude-coded");
305  const stateDir = resolve(stateRootDir, "claude-coded");
306
307  return {
308    repoRoot,
309    logsRootDir,
310    stateRootDir,
311    logsDir,
312    stateDir,
313    structuredEventLogPath: resolve(logsDir, "events.jsonl"),
314    stdoutLogPath: resolve(logsDir, "stdout.log"),
315    stderrLogPath: resolve(logsDir, "stderr.log"),
316    identityPath: resolve(stateDir, "identity.json"),
317    daemonStatePath: resolve(stateDir, "daemon-state.json")
318  };
319}
320
321function isClaudeCodedCliAction(value: string): value is Exclude<ClaudeCodedCliAction, "help"> {
322  return value === "config" || value === "start" || value === "status";
323}
324
325function parseOptionalInteger(value: string | undefined): number | undefined {
326  if (value == null || value.trim() === "") {
327    return undefined;
328  }
329
330  return parseStrictInteger(value, "integer value");
331}
332
333function parseStrictInteger(value: string, label: string): number {
334  const parsed = Number(value);
335
336  if (!Number.isInteger(parsed)) {
337    throw new Error(`Invalid ${label} "${value}".`);
338  }
339
340  return parsed;
341}
342
343function normalizePositiveInteger(value: number | undefined, fallback: number, label: string): number {
344  const candidate = value ?? fallback;
345
346  if (!Number.isInteger(candidate) || candidate < 0) {
347    throw new Error(`Invalid ${label} value "${String(value)}".`);
348  }
349
350  return candidate;
351}
352
353function getOptionalString(value: string | null | undefined): string | null {
354  if (value == null) {
355    return null;
356  }
357
358  const trimmed = value.trim();
359  return trimmed === "" ? null : trimmed;
360}
361
362function readCliValue(tokens: readonly string[], index: number, flag: string): string {
363  const value = tokens[index + 1];
364
365  if (value == null || value.startsWith("--")) {
366    throw new Error(`Missing value for ${flag}.`);
367  }
368
369  return value;
370}