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}