@companyhelm/cli 0.0.2 → 0.0.6

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 (47) hide show
  1. package/README.md +24 -62
  2. package/RUNTIME_IMAGE_VERSION +1 -1
  3. package/dist/cli.js +29 -1
  4. package/dist/commands/register-commands.js +2 -0
  5. package/dist/commands/root.js +341 -20
  6. package/dist/commands/startup.js +138 -55
  7. package/dist/commands/status.js +32 -0
  8. package/dist/service/app_server.js +23 -9
  9. package/dist/service/docker/app_server_container.js +3 -1
  10. package/dist/service/thread_lifecycle.js +4 -1
  11. package/dist/state/daemon_state.js +83 -0
  12. package/dist/state/schema.js +9 -1
  13. package/dist/templates/app_server_bootstrap.sh.j2 +46 -0
  14. package/dist/templates/runtime_agents.md.j2 +50 -0
  15. package/dist/templates/runtime_bashrc.j2 +19 -0
  16. package/dist/utils/daemon.js +15 -0
  17. package/dist/utils/process.js +22 -0
  18. package/drizzle/0011_actual_lucky.sql +7 -0
  19. package/drizzle/meta/_journal.json +8 -1
  20. package/package.json +7 -3
  21. package/dist/commands/agent/index.js +0 -10
  22. package/dist/commands/agent/list.js +0 -31
  23. package/dist/commands/agent/register-agent-commands.js +0 -10
  24. package/dist/commands/index.js +0 -15
  25. package/dist/commands/sdk/index.js +0 -12
  26. package/dist/commands/thread/index.js +0 -12
  27. package/dist/config/local.js +0 -1
  28. package/dist/config/schema.js +0 -7
  29. package/dist/model.js +0 -22
  30. package/dist/schema.js +0 -47
  31. package/dist/service/docker/docker_provider.js +0 -1
  32. package/dist/service/docker/runtime_container.js +0 -1
  33. package/dist/service/docker/runtime_image.js +0 -40
  34. package/dist/startup.js +0 -166
  35. package/dist/state/service/app_server.js +0 -392
  36. package/dist/state/service/buffered_client_message_sender.js +0 -73
  37. package/dist/state/service/companyhelm_api_client.js +0 -316
  38. package/dist/state/service/docker/app_server_container.js +0 -165
  39. package/dist/state/service/docker/dind.js +0 -114
  40. package/dist/state/service/docker/runtime_app_server_exec.js +0 -95
  41. package/dist/state/service/host.js +0 -15
  42. package/dist/state/service/runtime_shell.js +0 -23
  43. package/dist/state/service/sdk/refresh_models.js +0 -83
  44. package/dist/state/service/thread_lifecycle.js +0 -327
  45. package/dist/state/service/thread_runtime.js +0 -11
  46. package/dist/state/service/thread_turn_state.js +0 -45
  47. package/dist/state/service/workspace_agents.js +0 -115
@@ -33,6 +33,9 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.isRetryableApiConnectionError = isRetryableApiConnectionError;
37
+ exports.formatApiConnectionFailureMessage = formatApiConnectionFailureMessage;
38
+ exports.formatApiConnectionFailureDiagnostics = formatApiConnectionFailureDiagnostics;
36
39
  exports.shouldUseTurnSteer = shouldUseTurnSteer;
37
40
  exports.isNoActiveTurnSteerError = isNoActiveTurnSteerError;
38
41
  exports.isNoRunningTurnInterruptError = isNoRunningTurnInterruptError;
@@ -40,11 +43,13 @@ exports.normalizeThreadAgentApiUrlForRuntime = normalizeThreadAgentApiUrlForRunt
40
43
  exports.extractThreadNameUpdateFromNotification = extractThreadNameUpdateFromNotification;
41
44
  exports.extractServerMessageRequestId = extractServerMessageRequestId;
42
45
  exports.runRootCommand = runRootCommand;
46
+ exports.buildRootConfig = buildRootConfig;
43
47
  exports.registerRootCommand = registerRootCommand;
44
48
  const protobuf_1 = require("@bufbuild/protobuf");
45
49
  const protos_1 = require("@companyhelm/protos");
46
50
  const drizzle_orm_1 = require("drizzle-orm");
47
51
  const grpc = __importStar(require("@grpc/grpc-js"));
52
+ const node_child_process_1 = require("node:child_process");
48
53
  const node_crypto_1 = require("node:crypto");
49
54
  const node_fs_1 = require("node:fs");
50
55
  const node_path_1 = require("node:path");
@@ -60,8 +65,10 @@ const thread_runtime_js_1 = require("../service/thread_runtime.js");
60
65
  const thread_turn_state_js_1 = require("../service/thread_turn_state.js");
61
66
  const thread_user_message_request_store_js_1 = require("../service/thread_user_message_request_store.js");
62
67
  const thread_lifecycle_js_1 = require("../service/thread_lifecycle.js");
68
+ const daemon_state_js_1 = require("../state/daemon_state.js");
63
69
  const db_js_1 = require("../state/db.js");
64
70
  const schema_js_1 = require("../state/schema.js");
71
+ const daemon_js_1 = require("../utils/daemon.js");
65
72
  const logger_js_1 = require("../utils/logger.js");
66
73
  const workspace_agents_js_1 = require("../service/workspace_agents.js");
67
74
  const COMMAND_CHANNEL_CONNECT_RETRY_DELAY_MS = 1000;
@@ -83,6 +90,7 @@ const YOLO_SANDBOX_MODE = "danger-full-access";
83
90
  const YOLO_SANDBOX_POLICY = { type: "dangerFullAccess" };
84
91
  const DOCKER_INTERNAL_HOSTNAME = "host.docker.internal";
85
92
  const LOCALHOST_HOSTNAMES = new Set(["localhost", "127.0.0.1", "0.0.0.0", "::1"]);
93
+ const DAEMON_STARTUP_TIMEOUT_MS = 15000;
86
94
  const threadAppServerSessions = new Map();
87
95
  const threadRolloutPaths = new Map();
88
96
  function rememberThreadRolloutPath(threadId, rolloutPath) {
@@ -193,6 +201,81 @@ function normalizeReasoningLevels(value) {
193
201
  function toErrorMessage(error) {
194
202
  return error instanceof Error ? error.message : String(error);
195
203
  }
204
+ function getGrpcStatusCode(error) {
205
+ if (!error || typeof error !== "object" || !("code" in error)) {
206
+ return undefined;
207
+ }
208
+ const { code } = error;
209
+ return typeof code === "number" ? code : undefined;
210
+ }
211
+ function getGrpcStatusName(error) {
212
+ const code = getGrpcStatusCode(error);
213
+ if (code === undefined) {
214
+ return undefined;
215
+ }
216
+ const statusName = grpc.status[code];
217
+ return typeof statusName === "string" ? statusName : undefined;
218
+ }
219
+ function isRetryableApiConnectionError(error) {
220
+ return getGrpcStatusCode(error) !== grpc.status.UNAUTHENTICATED;
221
+ }
222
+ function formatGrpcMetadataForLog(metadata) {
223
+ if (!metadata) {
224
+ return undefined;
225
+ }
226
+ const rawEntries = metadata.getMap();
227
+ const entries = Object.entries(rawEntries);
228
+ if (entries.length === 0) {
229
+ return undefined;
230
+ }
231
+ const normalizedEntries = Object.fromEntries(entries.map(([key, value]) => [
232
+ key,
233
+ Buffer.isBuffer(value) ? value.toString("base64") : String(value),
234
+ ]));
235
+ return JSON.stringify(normalizedEntries);
236
+ }
237
+ function formatApiConnectionFailureMessage(error, apiUrl, secret) {
238
+ const statusCode = getGrpcStatusCode(error);
239
+ const statusName = getGrpcStatusName(error);
240
+ const serviceError = isGrpcServiceError(error) ? error : undefined;
241
+ const baseMessage = serviceError && typeof serviceError.details === "string" && serviceError.details.trim().length > 0
242
+ ? serviceError.details.trim()
243
+ : toErrorMessage(error);
244
+ let message = baseMessage;
245
+ if (statusCode !== undefined) {
246
+ message = `gRPC ${statusName ?? "UNKNOWN"} (${statusCode}): ${baseMessage}`;
247
+ }
248
+ message += ` [endpoint=${apiUrl}]`;
249
+ if (statusCode === grpc.status.UNAUTHENTICATED && (!secret || secret.trim().length === 0)) {
250
+ message += " Provide --secret <secret> to authenticate.";
251
+ }
252
+ return message;
253
+ }
254
+ function formatApiConnectionFailureDiagnostics(error) {
255
+ if (!isGrpcServiceError(error)) {
256
+ return error instanceof Error && typeof error.stack === "string" ? error.stack : undefined;
257
+ }
258
+ const diagnostics = [];
259
+ const statusCode = getGrpcStatusCode(error);
260
+ const statusName = getGrpcStatusName(error);
261
+ if (statusCode !== undefined) {
262
+ diagnostics.push(`code=${statusCode}`);
263
+ }
264
+ if (statusName) {
265
+ diagnostics.push(`status=${statusName}`);
266
+ }
267
+ if (typeof error.details === "string" && error.details.trim().length > 0) {
268
+ diagnostics.push(`details=${JSON.stringify(error.details.trim())}`);
269
+ }
270
+ const metadata = formatGrpcMetadataForLog(error.metadata);
271
+ if (metadata) {
272
+ diagnostics.push(`metadata=${metadata}`);
273
+ }
274
+ if (typeof error.stack === "string" && error.stack.trim().length > 0) {
275
+ diagnostics.push(`stack=${JSON.stringify(error.stack)}`);
276
+ }
277
+ return diagnostics.length > 0 ? diagnostics.join(" ") : undefined;
278
+ }
196
279
  function shouldUseTurnSteer(allowSteer, startedFromIdle) {
197
280
  return allowSteer && !startedFromIdle;
198
281
  }
@@ -202,6 +285,9 @@ function isNoActiveTurnSteerError(error) {
202
285
  function isNoRunningTurnInterruptError(error) {
203
286
  return /no running turn to interrupt/i.test(toErrorMessage(error));
204
287
  }
288
+ function isTurnCompletionTimeoutError(error) {
289
+ return /timed out waiting for completion of turn/i.test(toErrorMessage(error));
290
+ }
205
291
  function isRecord(value) {
206
292
  return typeof value === "object" && value !== null;
207
293
  }
@@ -1280,8 +1366,9 @@ async function sendRequestError(commandChannel, errorMessage, requestId) {
1280
1366
  });
1281
1367
  await commandChannel.send(message);
1282
1368
  }
1283
- async function sendThreadUpdate(commandChannel, threadId, status) {
1369
+ async function sendThreadUpdate(commandChannel, threadId, status, requestId) {
1284
1370
  const message = (0, protobuf_1.create)(protos_1.ClientMessageSchema, {
1371
+ requestId,
1285
1372
  payload: {
1286
1373
  case: "threadUpdate",
1287
1374
  value: {
@@ -1416,11 +1503,18 @@ async function resolveThreadAuthMode(cfg) {
1416
1503
  }
1417
1504
  async function handleCreateThreadRequest(cfg, commandChannel, request, requestId, apiClient, apiCallOptions, logger) {
1418
1505
  const threadId = (request.threadId ?? "").trim();
1506
+ const modelName = (request.model ?? "").trim();
1507
+ const requestedReasoningLevel = (request.reasoningLevel ?? "").trim();
1419
1508
  if (!threadId) {
1420
1509
  logger.warn("Rejecting createThreadRequest: threadId is required.");
1421
1510
  await sendRequestError(commandChannel, "Thread id is required.", requestId);
1422
1511
  return;
1423
1512
  }
1513
+ if (!modelName) {
1514
+ logger.warn("Rejecting createThreadRequest: model is required.");
1515
+ await sendRequestError(commandChannel, "Model is required.", requestId);
1516
+ return;
1517
+ }
1424
1518
  const { db, client } = await (0, db_js_1.initDb)(cfg.state_db_path);
1425
1519
  const threadDirectory = (0, thread_lifecycle_js_1.resolveThreadDirectory)(cfg.config_directory, cfg.workspaces_directory, threadId);
1426
1520
  const containerNames = (0, thread_lifecycle_js_1.buildThreadContainerNames)(threadId);
@@ -1429,16 +1523,40 @@ async function handleCreateThreadRequest(cfg, commandChannel, request, requestId
1429
1523
  const threadGitSkillPackages = normalizeThreadGitSkillPackagesForThreadConfig(request.gitSkillPackages, logger);
1430
1524
  const threadMcpServers = normalizeThreadMcpServersForThreadConfig(request.mcpServers, logger);
1431
1525
  const cliSecret = String(request.cliSecret ?? "").trim();
1432
- logger.debug(`Received createThreadRequest for thread '${threadId}' (model '${request.model}', reasoning '${request.reasoningLevel ?? ""}', additional instructions length '${normalizedAdditionalModelInstructions?.length ?? 0}', git skill packages '${threadGitSkillPackages.length}', MCP servers '${threadMcpServers.length}').`);
1526
+ logger.debug(`Received createThreadRequest for thread '${threadId}' (model '${modelName}', reasoning '${requestedReasoningLevel}', additional instructions length '${normalizedAdditionalModelInstructions?.length ?? 0}', git skill packages '${threadGitSkillPackages.length}', MCP servers '${threadMcpServers.length}').`);
1433
1527
  let authMode;
1434
1528
  try {
1435
1529
  authMode = await resolveThreadAuthMode(cfg);
1530
+ const modelConfig = await db
1531
+ .select({
1532
+ name: schema_js_1.llmModels.name,
1533
+ reasoningLevels: schema_js_1.llmModels.reasoningLevels,
1534
+ })
1535
+ .from(schema_js_1.llmModels)
1536
+ .where((0, drizzle_orm_1.eq)(schema_js_1.llmModels.name, modelName))
1537
+ .get();
1538
+ const configuredModelSample = await db
1539
+ .select({ name: schema_js_1.llmModels.name })
1540
+ .from(schema_js_1.llmModels)
1541
+ .limit(1)
1542
+ .all();
1543
+ if (configuredModelSample.length > 0) {
1544
+ if (!modelConfig) {
1545
+ throw new Error(`Model '${modelName}' is not configured.`);
1546
+ }
1547
+ if (requestedReasoningLevel.length > 0) {
1548
+ const supportedReasoningLevels = normalizeReasoningLevels(modelConfig.reasoningLevels);
1549
+ if (supportedReasoningLevels.length > 0 && !supportedReasoningLevels.includes(requestedReasoningLevel)) {
1550
+ throw new Error(`Reasoning level '${requestedReasoningLevel}' is not configured for model '${modelName}'.`);
1551
+ }
1552
+ }
1553
+ }
1436
1554
  await db.insert(schema_js_1.threads).values({
1437
1555
  id: threadId,
1438
1556
  sdkThreadId: null,
1439
1557
  cliSecret: cliSecret.length > 0 ? cliSecret : null,
1440
- model: request.model,
1441
- reasoningLevel: request.reasoningLevel ?? "",
1558
+ model: modelName,
1559
+ reasoningLevel: requestedReasoningLevel,
1442
1560
  additionalModelInstructions: normalizedAdditionalModelInstructions,
1443
1561
  status: "pending",
1444
1562
  currentSdkTurnId: null,
@@ -1508,9 +1626,73 @@ async function handleCreateThreadRequest(cfg, commandChannel, request, requestId
1508
1626
  await sendRequestError(commandChannel, `Failed to create containers for thread '${threadId}': ${toErrorMessage(error)}`, requestId);
1509
1627
  return;
1510
1628
  }
1629
+ let readyRequestId;
1511
1630
  const { db: updateDb, client: updateClient } = await (0, db_js_1.initDb)(cfg.state_db_path);
1512
1631
  try {
1513
- await updateDb.update(schema_js_1.threads).set({ status: "ready" }).where((0, drizzle_orm_1.eq)(schema_js_1.threads.id, threadId));
1632
+ const threadState = await (0, thread_turn_state_js_1.loadThreadMessageExecutionState)(cfg.state_db_path, threadId);
1633
+ if (!threadState) {
1634
+ throw new Error(`Thread '${threadId}' disappeared before SDK bootstrap.`);
1635
+ }
1636
+ const threadMcpSetup = buildThreadCodexMcpSetup(readWorkspaceThreadMcpConfig(threadState.workspace, logger));
1637
+ const threadAgentCliConfig = readWorkspaceThreadAgentCliConfig(threadState.workspace, logger);
1638
+ const appServerSession = await getOrCreateThreadAppServerSession(threadId, threadState.runtimeContainer, threadMcpSetup.appServerEnv, cfg.codex.app_server_client_name, logger);
1639
+ const runtimeUser = {
1640
+ uid: threadState.uid,
1641
+ gid: threadState.gid,
1642
+ agentUser: cfg.agent_user,
1643
+ agentHomeDirectory: threadState.homeDirectory,
1644
+ };
1645
+ await (0, thread_runtime_js_1.ensureThreadRuntimeReady)({
1646
+ dindContainer: threadState.dindContainer,
1647
+ runtimeContainer: threadState.runtimeContainer,
1648
+ containerService,
1649
+ gitUserName: cfg.git_user_name,
1650
+ gitUserEmail: cfg.git_user_email,
1651
+ user: runtimeUser,
1652
+ });
1653
+ await ensureThreadGitSkillsInRuntime(cfg, threadState, containerService, logger);
1654
+ if (threadAgentCliConfig) {
1655
+ await containerService.ensureRuntimeContainerAgentCliConfig(threadState.runtimeContainer, runtimeUser, threadAgentCliConfig);
1656
+ }
1657
+ if (!appServerSession.started) {
1658
+ await containerService.ensureRuntimeContainerCodexConfig(threadState.runtimeContainer, runtimeUser, threadMcpSetup.configToml);
1659
+ }
1660
+ await ensureThreadAppServerSessionStarted(appServerSession);
1661
+ const developerInstructions = buildThreadDeveloperInstructions(threadState.additionalModelInstructions);
1662
+ logger.debug(`Starting app-server thread '${threadId}' with developer instructions: ${JSON.stringify(developerInstructions)}.`);
1663
+ const threadStartResponse = await appServerSession.appServer.startThreadWithResponse({
1664
+ model: threadState.model,
1665
+ modelProvider: null,
1666
+ cwd: "/workspace",
1667
+ approvalPolicy: YOLO_APPROVAL_POLICY,
1668
+ sandbox: YOLO_SANDBOX_MODE,
1669
+ config: null,
1670
+ baseInstructions: null,
1671
+ developerInstructions,
1672
+ personality: null,
1673
+ ephemeral: null,
1674
+ experimentalRawEvents: false,
1675
+ persistExtendedHistory: true,
1676
+ }, requestId);
1677
+ if (requestId && threadStartResponse.id !== requestId) {
1678
+ throw new Error(`App-server thread/start response id '${String(threadStartResponse.id)}' did not match runner request id '${requestId}'.`);
1679
+ }
1680
+ if (!threadStartResponse.result.thread?.id) {
1681
+ throw new Error(`App-server thread/start did not return an SDK thread id for thread '${threadId}'.`);
1682
+ }
1683
+ readyRequestId = typeof threadStartResponse.id === "string" && threadStartResponse.id.length > 0
1684
+ ? threadStartResponse.id
1685
+ : undefined;
1686
+ appServerSession.sdkThreadId = threadStartResponse.result.thread.id;
1687
+ appServerSession.rolloutPath = threadStartResponse.result.thread.path;
1688
+ rememberThreadRolloutPath(threadId, threadStartResponse.result.thread.path);
1689
+ await updateDb
1690
+ .update(schema_js_1.threads)
1691
+ .set({
1692
+ status: "ready",
1693
+ sdkThreadId: threadStartResponse.result.thread.id,
1694
+ })
1695
+ .where((0, drizzle_orm_1.eq)(schema_js_1.threads.id, threadId));
1514
1696
  }
1515
1697
  catch (error) {
1516
1698
  logger.warn(`Failed to mark thread '${threadId}' as ready: ${toErrorMessage(error)}`);
@@ -1527,7 +1709,7 @@ async function handleCreateThreadRequest(cfg, commandChannel, request, requestId
1527
1709
  updateClient.close();
1528
1710
  }
1529
1711
  logger.info(`Thread '${threadId}' created and ready.`);
1530
- await sendThreadUpdate(commandChannel, threadId, protos_1.ThreadStatus.READY);
1712
+ await sendThreadUpdate(commandChannel, threadId, protos_1.ThreadStatus.READY, readyRequestId);
1531
1713
  }
1532
1714
  async function deleteThreadWithCleanup(cfg, request) {
1533
1715
  const { db, client } = await (0, db_js_1.initDb)(cfg.state_db_path);
@@ -1742,6 +1924,7 @@ async function executeCreateUserMessageRequest(cfg, commandChannel, request, req
1742
1924
  let keepRuntimeWarm = false;
1743
1925
  let shouldTrackTurnCompletion = trackTurnCompletion;
1744
1926
  let enqueuedRequestTurnId = null;
1927
+ let turnCompletionWaitStarted = false;
1745
1928
  try {
1746
1929
  await (0, thread_runtime_js_1.ensureThreadRuntimeReady)({
1747
1930
  dindContainer: threadState.dindContainer,
@@ -1827,14 +2010,18 @@ async function executeCreateUserMessageRequest(cfg, commandChannel, request, req
1827
2010
  if (!threadState.currentSdkTurnId) {
1828
2011
  throw new Error(`Thread '${request.threadId}' is marked running but has no current SDK turn id.`);
1829
2012
  }
2013
+ const activeSdkTurnId = threadState.currentSdkTurnId;
1830
2014
  const steerParams = {
1831
2015
  threadId: resolvedSdkThreadId,
1832
2016
  input,
1833
- expectedTurnId: threadState.currentSdkTurnId,
2017
+ expectedTurnId: activeSdkTurnId,
1834
2018
  };
1835
2019
  try {
1836
2020
  const turnSteerResult = await appServer.steerTurn(steerParams);
1837
- sdkTurnId = turnSteerResult.turnId;
2021
+ if (turnSteerResult.turnId && turnSteerResult.turnId !== activeSdkTurnId) {
2022
+ logger.debug(`turn/steer returned turn '${turnSteerResult.turnId}' for thread '${request.threadId}', preserving active turn '${activeSdkTurnId}' as the canonical turn id.`);
2023
+ }
2024
+ sdkTurnId = activeSdkTurnId;
1838
2025
  }
1839
2026
  catch (error) {
1840
2027
  if (!isNoActiveTurnSteerError(error)) {
@@ -1864,6 +2051,7 @@ async function executeCreateUserMessageRequest(cfg, commandChannel, request, req
1864
2051
  keepRuntimeWarm = true;
1865
2052
  return;
1866
2053
  }
2054
+ turnCompletionWaitStarted = true;
1867
2055
  const terminalStatus = await waitForThreadTurnCompletion(cfg.state_db_path, appServer, commandChannel, request.threadId, sdkThreadId, sdkTurnId, logger, requestId);
1868
2056
  await updateThreadTurnState(cfg, request.threadId, {
1869
2057
  currentSdkTurnId: sdkTurnId,
@@ -1886,7 +2074,12 @@ async function executeCreateUserMessageRequest(cfg, commandChannel, request, req
1886
2074
  if (enqueuedRequestTurnId && requestId) {
1887
2075
  await (0, thread_user_message_request_store_js_1.removePendingUserMessageRequestIdForTurn)(cfg.state_db_path, request.threadId, enqueuedRequestTurnId, requestId);
1888
2076
  }
1889
- if (startedFromIdle && !turnAccepted) {
2077
+ if (turnCompletionWaitStarted && !isTurnCompletionTimeoutError(error)) {
2078
+ await updateThreadTurnState(cfg, request.threadId, {
2079
+ isCurrentTurnRunning: false,
2080
+ }).catch(() => undefined);
2081
+ }
2082
+ else if (startedFromIdle && !turnAccepted) {
1890
2083
  await updateThreadTurnState(cfg, request.threadId, {
1891
2084
  isCurrentTurnRunning: false,
1892
2085
  }).catch(() => undefined);
@@ -1979,25 +2172,109 @@ function buildGrpcAuthCallOptions(secret) {
1979
2172
  metadata.set("authorization", `Bearer ${secret}`);
1980
2173
  return { metadata };
1981
2174
  }
1982
- async function runRootCommand(options) {
2175
+ function isInternalDaemonChildProcess() {
2176
+ return process.env[daemon_js_1.DAEMON_CHILD_ENV] === "1";
2177
+ }
2178
+ function resolveEffectiveDaemonLogPath(cfg) {
2179
+ const envPath = process.env[daemon_js_1.DAEMON_LOG_PATH_ENV];
2180
+ if (envPath && envPath.trim().length > 0) {
2181
+ return envPath;
2182
+ }
2183
+ return (0, daemon_js_1.resolveDaemonLogPath)(cfg.state_db_path);
2184
+ }
2185
+ async function runDetachedDaemonProcess(options) {
2186
+ const cfg = buildRootConfig(options);
2187
+ const logPath = (0, daemon_js_1.resolveDaemonLogPath)(cfg.state_db_path);
2188
+ (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(logPath), { recursive: true });
2189
+ const logFd = (0, node_fs_1.openSync)(logPath, "a");
2190
+ try {
2191
+ await new Promise((resolve, reject) => {
2192
+ const child = (0, node_child_process_1.spawn)(process.execPath, process.argv.slice(1), {
2193
+ cwd: process.cwd(),
2194
+ detached: true,
2195
+ env: {
2196
+ ...process.env,
2197
+ [daemon_js_1.DAEMON_CHILD_ENV]: "1",
2198
+ [daemon_js_1.DAEMON_LOG_PATH_ENV]: logPath,
2199
+ },
2200
+ stdio: ["ignore", logFd, logFd, "ipc"],
2201
+ windowsHide: true,
2202
+ });
2203
+ let settled = false;
2204
+ const timeout = setTimeout(() => {
2205
+ if (settled) {
2206
+ return;
2207
+ }
2208
+ settled = true;
2209
+ child.kill();
2210
+ reject(new Error(`Timed out waiting for daemon startup confirmation. See ${logPath}.`));
2211
+ }, DAEMON_STARTUP_TIMEOUT_MS);
2212
+ const finish = (callback) => {
2213
+ if (settled) {
2214
+ return;
2215
+ }
2216
+ settled = true;
2217
+ clearTimeout(timeout);
2218
+ callback();
2219
+ };
2220
+ child.once("error", (error) => {
2221
+ finish(() => reject(error));
2222
+ });
2223
+ child.once("exit", (code, signal) => {
2224
+ finish(() => {
2225
+ reject(new Error(`Daemon exited before startup completed (code=${code ?? "null"}, signal=${signal ?? "null"}). See ${logPath}.`));
2226
+ });
2227
+ });
2228
+ child.on("message", (message) => {
2229
+ if (!message || typeof message !== "object" || !("type" in message)) {
2230
+ return;
2231
+ }
2232
+ const type = message.type;
2233
+ if (type === "daemon-ready") {
2234
+ finish(() => {
2235
+ if (child.connected) {
2236
+ child.disconnect();
2237
+ }
2238
+ child.unref();
2239
+ console.log(`CompanyHelm daemon started (pid ${child.pid}). Logs: ${logPath}`);
2240
+ resolve();
2241
+ });
2242
+ return;
2243
+ }
2244
+ if (type === "daemon-error") {
2245
+ const messageValue = message.message;
2246
+ const daemonErrorMessage = typeof messageValue === "string" ? messageValue : `Daemon startup failed. See ${logPath}.`;
2247
+ finish(() => reject(new Error(daemonErrorMessage)));
2248
+ }
2249
+ });
2250
+ });
2251
+ }
2252
+ finally {
2253
+ (0, node_fs_1.closeSync)(logFd);
2254
+ }
2255
+ }
2256
+ function sendDaemonParentMessage(message) {
2257
+ if (typeof process.send === "function") {
2258
+ process.send(message);
2259
+ }
2260
+ }
2261
+ async function runRootCommand(options, runtimeOptions) {
1983
2262
  const logger = (0, logger_js_1.createLogger)(options.logLevel ?? "INFO", { daemonMode: options.daemon ?? false });
1984
- const cfg = config_js_1.config.parse({
1985
- companyhelm_api_url: options.serverUrl,
1986
- agent_api_url: options.agentApiUrl,
1987
- use_host_docker_runtime: options.useHostDockerRuntime,
1988
- host_docker_path: options.hostDockerPath,
1989
- thread_git_skills_directory: options.threadGitSkillsDirectory,
1990
- });
2263
+ const cfg = buildRootConfig(options);
1991
2264
  const configuredSdks = await hasConfiguredSdks(cfg);
1992
2265
  if (!configuredSdks && options.daemon) {
1993
2266
  throw new Error("No SDKs configured. Daemon mode requires at least one configured SDK.");
1994
2267
  }
1995
2268
  if (!configuredSdks) {
1996
- await (0, startup_js_1.startup)();
2269
+ await (0, startup_js_1.startup)(cfg);
1997
2270
  }
1998
2271
  await refreshCodexModelsForRegistration(cfg, logger);
1999
2272
  const registerRequest = await buildRegisterRunnerRequest(cfg);
2000
2273
  const apiCallOptions = buildGrpcAuthCallOptions(options.secret);
2274
+ if (options.daemon) {
2275
+ await (0, daemon_state_js_1.claimCurrentDaemonState)(cfg.state_db_path, process.pid, resolveEffectiveDaemonLogPath(cfg));
2276
+ runtimeOptions?.onDaemonReady?.();
2277
+ }
2001
2278
  const commandMessageSink = new buffered_client_message_sender_js_1.BufferedClientMessageSender({
2002
2279
  maxBufferedEvents: cfg.client_message_buffer_limit,
2003
2280
  logger,
@@ -2032,7 +2309,14 @@ async function runRootCommand(options) {
2032
2309
  logger.warn("CompanyHelm API command channel closed. Reconnecting...");
2033
2310
  }
2034
2311
  catch (error) {
2035
- const failureMessage = toErrorMessage(error);
2312
+ const failureMessage = formatApiConnectionFailureMessage(error, cfg.companyhelm_api_url, options.secret);
2313
+ const diagnostics = formatApiConnectionFailureDiagnostics(error);
2314
+ if (diagnostics) {
2315
+ logger.debug(`CompanyHelm API failure diagnostics: ${diagnostics}`);
2316
+ }
2317
+ if (!isRetryableApiConnectionError(error)) {
2318
+ throw new Error(failureMessage);
2319
+ }
2036
2320
  logger.warn(`CompanyHelm API connection attempt ${reconnectAttempt} failed: ${failureMessage}. ` +
2037
2321
  "Retrying...");
2038
2322
  }
@@ -2057,21 +2341,58 @@ async function runRootCommand(options) {
2057
2341
  if (droppedMessages > 0) {
2058
2342
  logger.warn(`Dropped ${droppedMessages} outbound client message(s) while command channel was disconnected.`);
2059
2343
  }
2344
+ if (options.daemon) {
2345
+ await (0, daemon_state_js_1.clearCurrentDaemonState)(cfg.state_db_path, process.pid).catch((error) => {
2346
+ logger.warn(`Failed to clear daemon state: ${toErrorMessage(error)}`);
2347
+ });
2348
+ }
2060
2349
  await stopAllThreadAppServerSessions();
2061
2350
  await stopAllThreadContainers(cfg, logger);
2062
2351
  }
2063
2352
  }
2353
+ function buildRootConfig(options) {
2354
+ return config_js_1.config.parse({
2355
+ config_directory: options.configPath,
2356
+ state_db_path: options.stateDbPath,
2357
+ companyhelm_api_url: options.serverUrl,
2358
+ agent_api_url: options.agentApiUrl,
2359
+ use_host_docker_runtime: options.useHostDockerRuntime,
2360
+ host_docker_path: options.hostDockerPath,
2361
+ thread_git_skills_directory: options.threadGitSkillsDirectory,
2362
+ });
2363
+ }
2064
2364
  function registerRootCommand(program) {
2065
2365
  program
2366
+ .option("--config-path <path>", "Config directory override (defaults to ~/.config/companyhelm).")
2066
2367
  .option("--server-url <url>", "CompanyHelm gRPC API URL override.")
2067
2368
  .option("--agent-api-url <url>", "Agent gRPC API URL for companyhelm-agent in runtime containers (localhost is rewritten to http://host.docker.internal).")
2068
2369
  .option("--secret <secret>", "Bearer secret used as gRPC Authorization header.")
2370
+ .option("--state-db-path <path>", "State database path override (defaults to ~/.local/share/companyhelm/state.db).")
2069
2371
  .option("--use-host-docker-runtime", "Mount host Docker socket into runtime containers instead of creating DinD sidecars.")
2070
2372
  .option("--host-docker-path <path>", "Host Docker endpoint when --use-host-docker-runtime is enabled (unix:///<socket-path> or tcp://localhost:<port>).")
2071
2373
  .option("--thread-git-skills-directory <path>", "Container path where thread git skill repositories are cloned before linking into ~/.codex/skills.")
2072
2374
  .option("-d, --daemon", "Run in daemon mode and fail fast when no SDK is configured.")
2073
2375
  .option("--log-level <level>", "Log level (DEBUG, INFO, WARN, ERROR).", "INFO")
2074
2376
  .action(async () => {
2075
- await runRootCommand(program.opts());
2377
+ const options = program.opts();
2378
+ if (options.daemon && !isInternalDaemonChildProcess()) {
2379
+ await runDetachedDaemonProcess(options);
2380
+ return;
2381
+ }
2382
+ try {
2383
+ await runRootCommand(options, isInternalDaemonChildProcess()
2384
+ ? {
2385
+ onDaemonReady: () => {
2386
+ sendDaemonParentMessage({ type: "daemon-ready" });
2387
+ },
2388
+ }
2389
+ : undefined);
2390
+ }
2391
+ catch (error) {
2392
+ if (isInternalDaemonChildProcess()) {
2393
+ sendDaemonParentMessage({ type: "daemon-error", message: toErrorMessage(error) });
2394
+ }
2395
+ throw error;
2396
+ }
2076
2397
  });
2077
2398
  }