@companyhelm/cli 0.0.7 → 0.0.9

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.
@@ -1 +1 @@
1
- 0.0.8
1
+ 0.0.9
package/dist/cli.js CHANGED
@@ -4,6 +4,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
4
4
  const node_fs_1 = require("node:fs");
5
5
  const node_path_1 = require("node:path");
6
6
  const commander_1 = require("commander");
7
+ const global_options_js_1 = require("./commands/global-options.js");
7
8
  const register_commands_js_1 = require("./commands/register-commands.js");
8
9
  function getVersion() {
9
10
  try {
@@ -20,7 +21,11 @@ program
20
21
  .name("companyhelm")
21
22
  .description("Run coding agents in fully isolated Docker sandboxes, locally.")
22
23
  .version(getVersion());
24
+ (0, global_options_js_1.addGlobalOptions)(program);
23
25
  (0, register_commands_js_1.registerCommands)(program);
26
+ program.hook("preAction", (_thisCommand, actionCommand) => {
27
+ (0, global_options_js_1.applyGlobalOptionEnvironment)(actionCommand);
28
+ });
24
29
  function formatCliError(error) {
25
30
  if (error instanceof commander_1.CommanderError) {
26
31
  return {
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CONFIG_PATH_OPTION_DESCRIPTION = void 0;
4
+ exports.addGlobalOptions = addGlobalOptions;
5
+ exports.applyGlobalOptionEnvironment = applyGlobalOptionEnvironment;
6
+ const config_js_1 = require("../config.js");
7
+ exports.CONFIG_PATH_OPTION_DESCRIPTION = `Config directory override (defaults to $${config_js_1.CONFIG_PATH_ENV} or ${config_js_1.DEFAULT_CONFIG_DIRECTORY}).`;
8
+ function addGlobalOptions(program) {
9
+ return program.option("--config-path <path>", exports.CONFIG_PATH_OPTION_DESCRIPTION);
10
+ }
11
+ function applyGlobalOptionEnvironment(command) {
12
+ const options = command.optsWithGlobals();
13
+ if (typeof options.configPath !== "string") {
14
+ return;
15
+ }
16
+ const configPath = options.configPath.trim();
17
+ if (configPath.length > 0) {
18
+ process.env[config_js_1.CONFIG_PATH_ENV] = configPath;
19
+ }
20
+ }
@@ -73,6 +73,7 @@ const db_js_1 = require("../state/db.js");
73
73
  const schema_js_1 = require("../state/schema.js");
74
74
  const daemon_js_1 = require("../utils/daemon.js");
75
75
  const logger_js_1 = require("../utils/logger.js");
76
+ const path_js_1 = require("../utils/path.js");
76
77
  const workspace_agents_js_1 = require("../service/workspace_agents.js");
77
78
  const COMMAND_CHANNEL_CONNECT_RETRY_DELAY_MS = 1000;
78
79
  const COMMAND_CHANNEL_OPEN_TIMEOUT_MS = 5000;
@@ -1467,10 +1468,11 @@ async function hasConfiguredSdks(cfg) {
1467
1468
  client.close();
1468
1469
  }
1469
1470
  }
1470
- async function clearSdkModels(cfg, sdkName) {
1471
+ async function countSdkModels(cfg, sdkName) {
1471
1472
  const { db, client } = await (0, db_js_1.initDb)(cfg.state_db_path);
1472
1473
  try {
1473
- await db.delete(schema_js_1.llmModels).where((0, drizzle_orm_1.eq)(schema_js_1.llmModels.sdkName, sdkName));
1474
+ const models = await db.select({ name: schema_js_1.llmModels.name }).from(schema_js_1.llmModels).where((0, drizzle_orm_1.eq)(schema_js_1.llmModels.sdkName, sdkName)).all();
1475
+ return models.length;
1474
1476
  }
1475
1477
  finally {
1476
1478
  client.close();
@@ -1483,9 +1485,12 @@ async function refreshCodexModelsForRegistration(cfg, logger) {
1483
1485
  logger.info(`Refreshed Codex models from container app-server (${modelCount} models).`);
1484
1486
  }
1485
1487
  catch (error) {
1486
- logger.warn(`Failed to refresh Codex models from container app-server: ${toErrorMessage(error)}. ` +
1487
- "Registering runner with an empty Codex model list.");
1488
- await clearSdkModels(cfg, "codex");
1488
+ const cachedModelCount = await countSdkModels(cfg, "codex");
1489
+ const failureMessage = (0, refresh_models_js_1.formatSdkModelRefreshFailure)("codex", error);
1490
+ if (cachedModelCount === 0) {
1491
+ throw new Error(`${failureMessage} Runner startup aborted because no cached Codex models are available; refusing to register zero models.`);
1492
+ }
1493
+ logger.warn(`${failureMessage} Using ${cachedModelCount} cached Codex model(s) from local state instead of registering zero models.`);
1489
1494
  }
1490
1495
  }
1491
1496
  async function resolveThreadAuthMode(cfg) {
@@ -2181,13 +2186,15 @@ function isInternalDaemonChildProcess() {
2181
2186
  function resolveEffectiveDaemonLogPath(cfg) {
2182
2187
  const envPath = process.env[daemon_js_1.DAEMON_LOG_PATH_ENV];
2183
2188
  if (envPath && envPath.trim().length > 0) {
2184
- return envPath;
2189
+ return (0, path_js_1.expandHome)(envPath);
2185
2190
  }
2186
2191
  return (0, daemon_js_1.resolveDaemonLogPath)(cfg.state_db_path);
2187
2192
  }
2188
2193
  async function runDetachedDaemonProcess(options) {
2189
2194
  const cfg = buildRootConfig(options);
2190
- const logPath = (0, daemon_js_1.resolveDaemonLogPath)(cfg.state_db_path);
2195
+ const logPath = options.logPath && options.logPath.trim().length > 0
2196
+ ? (0, path_js_1.expandHome)(options.logPath)
2197
+ : (0, daemon_js_1.resolveDaemonLogPath)(cfg.state_db_path);
2191
2198
  (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(logPath), { recursive: true });
2192
2199
  const logFd = (0, node_fs_1.openSync)(logPath, "a");
2193
2200
  try {
@@ -1,13 +1,15 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.addRunnerStartOptions = addRunnerStartOptions;
4
+ const global_options_js_1 = require("../global-options.js");
4
5
  function addRunnerStartOptions(command) {
5
6
  return command
6
- .option("--config-path <path>", "Config directory override (defaults to ~/.config/companyhelm).")
7
+ .option("--config-path <path>", global_options_js_1.CONFIG_PATH_OPTION_DESCRIPTION)
7
8
  .option("--server-url <url>", "CompanyHelm gRPC API URL override.")
8
9
  .option("--agent-api-url <url>", "Agent gRPC API URL for companyhelm-agent in runtime containers (localhost is rewritten to http://host.docker.internal).")
9
10
  .option("--secret <secret>", "Bearer secret used as gRPC Authorization header.")
10
- .option("--state-db-path <path>", "State database path override (defaults to ~/.local/share/companyhelm/state.db).")
11
+ .option("--state-db-path <path>", "State database path override (defaults to state.db under the active config directory).")
12
+ .option("--log-path <path>", "Daemon log file override.")
11
13
  .option("--use-host-docker-runtime", "Mount host Docker socket into runtime containers instead of creating DinD sidecars.")
12
14
  .option("--host-docker-path <path>", "Host Docker endpoint when --use-host-docker-runtime is enabled (unix:///<socket-path> or tcp://localhost:<port>).")
13
15
  .option("--thread-git-skills-directory <path>", "Container path where thread git skill repositories are cloned before linking into ~/.codex/skills.")
@@ -48,7 +48,7 @@ function registerRunnerStopCommand(runnerCommand) {
48
48
  runnerCommand
49
49
  .command("stop")
50
50
  .description("Stop the local CompanyHelm runner daemon.")
51
- .option("--state-db-path <path>", "State database path override (defaults to ~/.local/share/companyhelm/state.db).")
51
+ .option("--state-db-path <path>", "State database path override (defaults to state.db under the active config directory).")
52
52
  .action(async (options) => {
53
53
  await runRunnerStopCommand(options);
54
54
  });
@@ -0,0 +1,200 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.defaultUseDedicatedCodexAuthDependencies = exports.defaultSetCodexHostAuthDependencies = void 0;
4
+ exports.isErrnoException = isErrnoException;
5
+ exports.buildDockerMissingError = buildDockerMissingError;
6
+ exports.ensureDockerAvailable = ensureDockerAvailable;
7
+ exports.listCodexStartupAuthOptions = listCodexStartupAuthOptions;
8
+ exports.setCodexHostAuthInDb = setCodexHostAuthInDb;
9
+ exports.setCodexDedicatedAuthInDb = setCodexDedicatedAuthInDb;
10
+ exports.runSetCodexHostAuth = runSetCodexHostAuth;
11
+ exports.runDedicatedCodexAuth = runDedicatedCodexAuth;
12
+ exports.runUseDedicatedCodexAuth = runUseDedicatedCodexAuth;
13
+ const node_child_process_1 = require("node:child_process");
14
+ const node_fs_1 = require("node:fs");
15
+ const node_path_1 = require("node:path");
16
+ const drizzle_orm_1 = require("drizzle-orm");
17
+ const host_js_1 = require("../../../service/host.js");
18
+ const db_js_1 = require("../../../state/db.js");
19
+ const schema_js_1 = require("../../../state/schema.js");
20
+ const path_js_1 = require("../../../utils/path.js");
21
+ exports.defaultSetCodexHostAuthDependencies = {
22
+ getHostInfoFn: host_js_1.getHostInfo,
23
+ initDbFn: db_js_1.initDb,
24
+ };
25
+ exports.defaultUseDedicatedCodexAuthDependencies = {
26
+ initDbFn: db_js_1.initDb,
27
+ logInfo: console.log,
28
+ logSuccess: console.log,
29
+ spawnCommand: node_child_process_1.spawn,
30
+ spawnSyncCommand: node_child_process_1.spawnSync,
31
+ };
32
+ function isErrnoException(error) {
33
+ return error instanceof Error && "code" in error;
34
+ }
35
+ function buildDockerMissingError() {
36
+ return new Error("Docker is not installed or not available on PATH. Install Docker and retry.");
37
+ }
38
+ function ensureDockerAvailable(spawnSyncCommand) {
39
+ const result = spawnSyncCommand("docker", ["--version"], { stdio: "ignore" });
40
+ if (isErrnoException(result.error) && result.error.code === "ENOENT") {
41
+ throw buildDockerMissingError();
42
+ }
43
+ }
44
+ function listCodexStartupAuthOptions(cfg, getHostInfoFn = host_js_1.getHostInfo) {
45
+ const options = [
46
+ {
47
+ value: "dedicated",
48
+ label: "Dedicated",
49
+ hint: "recommended -- runs Codex login inside a container",
50
+ },
51
+ ];
52
+ const hostInfo = getHostInfoFn(cfg.codex.codex_auth_path);
53
+ if (hostInfo.codexAuthExists) {
54
+ options.push({
55
+ value: "host",
56
+ label: "Host",
57
+ hint: `reuse existing credentials from ${cfg.codex.codex_auth_path}`,
58
+ });
59
+ }
60
+ return options;
61
+ }
62
+ async function setCodexHostAuthInDb(db) {
63
+ const existingSdk = await db.select().from(schema_js_1.agentSdks).where((0, drizzle_orm_1.eq)(schema_js_1.agentSdks.name, "codex")).get();
64
+ if (existingSdk) {
65
+ await db
66
+ .update(schema_js_1.agentSdks)
67
+ .set({ authentication: "host" })
68
+ .where((0, drizzle_orm_1.eq)(schema_js_1.agentSdks.name, "codex"));
69
+ return;
70
+ }
71
+ await db.insert(schema_js_1.agentSdks).values({ name: "codex", authentication: "host" });
72
+ }
73
+ async function setCodexDedicatedAuthInDb(db) {
74
+ const existingSdk = await db.select().from(schema_js_1.agentSdks).where((0, drizzle_orm_1.eq)(schema_js_1.agentSdks.name, "codex")).get();
75
+ if (existingSdk) {
76
+ await db
77
+ .update(schema_js_1.agentSdks)
78
+ .set({ authentication: "dedicated" })
79
+ .where((0, drizzle_orm_1.eq)(schema_js_1.agentSdks.name, "codex"));
80
+ return;
81
+ }
82
+ await db.insert(schema_js_1.agentSdks).values({ name: "codex", authentication: "dedicated" });
83
+ }
84
+ async function runSetCodexHostAuth(cfg, overrides = {}) {
85
+ const deps = { ...exports.defaultSetCodexHostAuthDependencies, ...overrides };
86
+ const authPath = (0, path_js_1.expandHome)(cfg.codex.codex_auth_path);
87
+ const hostInfo = deps.getHostInfoFn(cfg.codex.codex_auth_path);
88
+ if (!hostInfo.codexAuthExists) {
89
+ throw new Error(`Codex host auth file not found at ${authPath}.`);
90
+ }
91
+ const { db, client } = await deps.initDbFn(cfg.state_db_path);
92
+ try {
93
+ await setCodexHostAuthInDb(db);
94
+ }
95
+ finally {
96
+ client.close();
97
+ }
98
+ return authPath;
99
+ }
100
+ async function runDedicatedCodexAuth(cfg, db, deps) {
101
+ ensureDockerAvailable(deps.spawnSyncCommand);
102
+ const port = cfg.codex.codex_auth_port;
103
+ const socatPort = port + 1;
104
+ const containerName = `companyhelm-codex-auth-${Date.now()}`;
105
+ deps.logInfo("Starting Codex login inside a container...");
106
+ deps.logInfo("A browser URL will appear -- open it to complete authentication.");
107
+ const configDir = (0, path_js_1.expandHome)(cfg.config_directory);
108
+ if (!(0, node_fs_1.existsSync)(configDir)) {
109
+ (0, node_fs_1.mkdirSync)(configDir, { recursive: true });
110
+ }
111
+ const destPath = (0, node_path_1.join)(configDir, cfg.codex.codex_auth_file_path);
112
+ let authCopied = false;
113
+ await new Promise((resolve, reject) => {
114
+ let settled = false;
115
+ let poll;
116
+ const rejectOnce = (error) => {
117
+ if (settled) {
118
+ return;
119
+ }
120
+ settled = true;
121
+ if (poll) {
122
+ clearInterval(poll);
123
+ }
124
+ deps.spawnSyncCommand("docker", ["rm", "-f", containerName], { stdio: "ignore" });
125
+ reject(error);
126
+ };
127
+ const resolveOnce = () => {
128
+ if (settled) {
129
+ return;
130
+ }
131
+ settled = true;
132
+ if (poll) {
133
+ clearInterval(poll);
134
+ }
135
+ resolve();
136
+ };
137
+ const child = deps.spawnCommand("docker", [
138
+ "run",
139
+ "-it",
140
+ "--name",
141
+ containerName,
142
+ "-p",
143
+ `${port}:${socatPort}`,
144
+ "--entrypoint",
145
+ "bash",
146
+ cfg.runtime_image,
147
+ "-c",
148
+ `source "$NVM_DIR/nvm.sh"; socat TCP-LISTEN:${socatPort},fork,bind=0.0.0.0,reuseaddr TCP:127.0.0.1:${port} 2>/dev/null & codex`,
149
+ ], { stdio: "inherit" });
150
+ child.on("error", (error) => {
151
+ if (isErrnoException(error) && error.code === "ENOENT") {
152
+ rejectOnce(buildDockerMissingError());
153
+ return;
154
+ }
155
+ rejectOnce(new Error(`Failed to start Codex login container: ${error.message}`));
156
+ });
157
+ poll = setInterval(() => {
158
+ if (settled) {
159
+ return;
160
+ }
161
+ const check = deps.spawnSyncCommand("docker", ["exec", containerName, "sh", "-c", `test -f ${cfg.codex.codex_auth_path}`], {
162
+ stdio: "ignore",
163
+ });
164
+ if (check.status === 0) {
165
+ const resolveResult = deps.spawnSyncCommand("docker", ["exec", containerName, "sh", "-c", `echo ${cfg.codex.codex_auth_path}`], {
166
+ encoding: "utf-8",
167
+ });
168
+ const containerAuthAbsPath = resolveResult.stdout.trim();
169
+ const cpResult = deps.spawnSyncCommand("docker", ["cp", `${containerName}:${containerAuthAbsPath}`, destPath], {
170
+ stdio: "ignore",
171
+ });
172
+ if (cpResult.status !== 0) {
173
+ rejectOnce(new Error("Failed to extract auth file from container."));
174
+ return;
175
+ }
176
+ authCopied = true;
177
+ deps.spawnSyncCommand("docker", ["rm", "-f", containerName], { stdio: "ignore" });
178
+ resolveOnce();
179
+ }
180
+ }, 1000);
181
+ child.on("exit", () => {
182
+ if (!authCopied) {
183
+ rejectOnce(new Error("Codex login failed or was cancelled."));
184
+ }
185
+ });
186
+ });
187
+ await setCodexDedicatedAuthInDb(db);
188
+ deps.logSuccess(`Codex auth saved to ${destPath}`);
189
+ return destPath;
190
+ }
191
+ async function runUseDedicatedCodexAuth(cfg, overrides = {}) {
192
+ const deps = { ...exports.defaultUseDedicatedCodexAuthDependencies, ...overrides };
193
+ const { db, client } = await deps.initDbFn(cfg.state_db_path);
194
+ try {
195
+ return await runDedicatedCodexAuth(cfg, db, deps);
196
+ }
197
+ finally {
198
+ client.close();
199
+ }
200
+ }
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerCodexSdkCommands = registerCodexSdkCommands;
4
+ const use_dedicated_auth_js_1 = require("./use-dedicated-auth.js");
5
+ const use_host_auth_js_1 = require("./use-host-auth.js");
6
+ function registerCodexSdkCommands(sdkCommand) {
7
+ const codexCommand = sdkCommand
8
+ .command("codex")
9
+ .description("Manage Codex SDK authentication.");
10
+ (0, use_host_auth_js_1.registerSdkCodexUseHostAuthCommand)(codexCommand);
11
+ (0, use_dedicated_auth_js_1.registerSdkCodexUseDedicatedAuthCommand)(codexCommand);
12
+ }
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runSdkCodexUseDedicatedAuthCommand = runSdkCodexUseDedicatedAuthCommand;
4
+ exports.registerSdkCodexUseDedicatedAuthCommand = registerSdkCodexUseDedicatedAuthCommand;
5
+ const config_js_1 = require("../../../config.js");
6
+ const auth_js_1 = require("./auth.js");
7
+ async function runSdkCodexUseDedicatedAuthCommand(cfg = config_js_1.config.parse({}), overrides = {}) {
8
+ const deps = { ...auth_js_1.defaultUseDedicatedCodexAuthDependencies, ...overrides };
9
+ await (0, auth_js_1.runUseDedicatedCodexAuth)(cfg, deps);
10
+ }
11
+ function registerSdkCodexUseDedicatedAuthCommand(codexCommand) {
12
+ codexCommand
13
+ .command("use-dedicated-auth")
14
+ .description("Configure the Codex SDK to use dedicated authentication.")
15
+ .action(async () => {
16
+ await runSdkCodexUseDedicatedAuthCommand();
17
+ });
18
+ }
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runSdkCodexUseHostAuthCommand = runSdkCodexUseHostAuthCommand;
4
+ exports.registerSdkCodexUseHostAuthCommand = registerSdkCodexUseHostAuthCommand;
5
+ const config_js_1 = require("../../../config.js");
6
+ const auth_js_1 = require("./auth.js");
7
+ async function runSdkCodexUseHostAuthCommand(cfg = config_js_1.config.parse({}), overrides = {}) {
8
+ const deps = { ...auth_js_1.defaultSetCodexHostAuthDependencies, ...overrides };
9
+ const authPath = await (0, auth_js_1.runSetCodexHostAuth)(cfg, deps);
10
+ console.log(`Codex SDK configured with host authentication using ${authPath}.`);
11
+ }
12
+ function registerSdkCodexUseHostAuthCommand(codexCommand) {
13
+ codexCommand
14
+ .command("use-host-auth")
15
+ .description("Configure the Codex SDK to use host authentication.")
16
+ .action(async () => {
17
+ await runSdkCodexUseHostAuthCommand();
18
+ });
19
+ }
@@ -4,7 +4,14 @@ exports.runSdkRefreshModelsCommand = runSdkRefreshModelsCommand;
4
4
  exports.registerSdkRefreshModelsCommand = registerSdkRefreshModelsCommand;
5
5
  const refresh_models_js_1 = require("../../service/sdk/refresh_models.js");
6
6
  async function runSdkRefreshModelsCommand(options) {
7
- const results = await (0, refresh_models_js_1.refreshSdkModels)({ sdk: options.sdk });
7
+ let results;
8
+ try {
9
+ results = await (0, refresh_models_js_1.refreshSdkModels)({ sdk: options.sdk });
10
+ }
11
+ catch (error) {
12
+ const sdkName = options.sdk ?? "configured";
13
+ throw new Error((0, refresh_models_js_1.formatSdkModelRefreshFailure)(sdkName, error));
14
+ }
8
15
  for (const result of results) {
9
16
  console.log(`Refreshed ${result.modelCount} models for SDK '${result.sdk}'.`);
10
17
  }
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.registerSdkCommands = registerSdkCommands;
4
+ const register_codex_sdk_commands_js_1 = require("./codex/register-codex-sdk-commands.js");
4
5
  const list_js_1 = require("./list.js");
5
6
  const refresh_models_js_1 = require("./refresh-models.js");
6
7
  function registerSdkCommands(program) {
@@ -9,4 +10,5 @@ function registerSdkCommands(program) {
9
10
  .description("Manage configured SDKs and their model capabilities.");
10
11
  (0, list_js_1.registerSdkListCommand)(sdkCommand);
11
12
  (0, refresh_models_js_1.registerSdkRefreshModelsCommand)(sdkCommand);
13
+ (0, register_codex_sdk_commands_js_1.registerCodexSdkCommands)(sdkCommand);
12
14
  }
@@ -37,42 +37,27 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.startup = startup;
40
- const node_fs_1 = require("node:fs");
41
- const node_path_1 = require("node:path");
42
40
  const node_child_process_1 = require("node:child_process");
43
41
  const p = __importStar(require("@clack/prompts"));
44
42
  const figlet_1 = __importDefault(require("figlet"));
45
43
  const config_js_1 = require("../config.js");
46
44
  const db_js_1 = require("../state/db.js");
47
45
  const schema_js_1 = require("../state/schema.js");
48
- const host_js_1 = require("../service/host.js");
49
46
  const refresh_models_js_1 = require("../service/sdk/refresh_models.js");
50
- const path_js_1 = require("../utils/path.js");
47
+ const auth_js_1 = require("./sdk/codex/auth.js");
51
48
  function banner() {
52
49
  console.log();
53
50
  console.log(figlet_1.default.textSync("CompanyHelm", { font: "Small" }));
54
51
  console.log();
55
52
  }
56
- function isErrnoException(error) {
57
- return error instanceof Error && "code" in error;
58
- }
59
- function buildDockerMissingError() {
60
- return new Error("Docker is not installed or not available on PATH. Install Docker and retry.");
61
- }
62
53
  const defaultStartupDependencies = {
63
- getHostInfoFn: host_js_1.getHostInfo,
54
+ getHostInfoFn: auth_js_1.defaultSetCodexHostAuthDependencies.getHostInfoFn,
64
55
  initDbFn: db_js_1.initDb,
65
56
  promptApi: p,
66
57
  refreshSdkModelsFn: refresh_models_js_1.refreshSdkModels,
67
58
  spawnCommand: node_child_process_1.spawn,
68
59
  spawnSyncCommand: node_child_process_1.spawnSync,
69
60
  };
70
- function ensureDockerAvailable(spawnSyncCommand) {
71
- const result = spawnSyncCommand("docker", ["--version"], { stdio: "ignore" });
72
- if (isErrnoException(result.error) && result.error.code === "ENOENT") {
73
- throw buildDockerMissingError();
74
- }
75
- }
76
61
  function restoreInteractiveTerminalState() {
77
62
  try {
78
63
  if (process.stdin.isTTY && typeof process.stdin.setRawMode === "function") {
@@ -96,7 +81,7 @@ function exitStartup(code) {
96
81
  process.exit(code);
97
82
  }
98
83
  async function refreshCodexModelsInStartup(deps) {
99
- ensureDockerAvailable(deps.spawnSyncCommand);
84
+ (0, auth_js_1.ensureDockerAvailable)(deps.spawnSyncCommand);
100
85
  const spinner = deps.promptApi.spinner();
101
86
  const seenStatusMessages = new Set();
102
87
  spinner.start("Preparing Codex runtime image and refreshing model catalog via app-server");
@@ -113,96 +98,6 @@ async function refreshCodexModelsInStartup(deps) {
113
98
  const count = results[0]?.modelCount ?? 0;
114
99
  spinner.stop(`Codex model catalog refreshed (${count} models).`);
115
100
  }
116
- async function dedicatedAuth(cfg, db, deps) {
117
- ensureDockerAvailable(deps.spawnSyncCommand);
118
- const port = cfg.codex.codex_auth_port;
119
- const socatPort = port + 1;
120
- const containerName = `companyhelm-codex-auth-${Date.now()}`;
121
- deps.promptApi.log.info("Starting Codex login inside a container...");
122
- deps.promptApi.log.info("A browser URL will appear -- open it to complete authentication.");
123
- const configDir = (0, path_js_1.expandHome)(cfg.config_directory);
124
- if (!(0, node_fs_1.existsSync)(configDir)) {
125
- (0, node_fs_1.mkdirSync)(configDir, { recursive: true });
126
- }
127
- const destPath = (0, node_path_1.join)(configDir, cfg.codex.codex_auth_file_path);
128
- let authCopied = false;
129
- await new Promise((resolve, reject) => {
130
- let settled = false;
131
- let poll;
132
- const rejectOnce = (error) => {
133
- if (settled) {
134
- return;
135
- }
136
- settled = true;
137
- if (poll) {
138
- clearInterval(poll);
139
- }
140
- deps.spawnSyncCommand("docker", ["rm", "-f", containerName], { stdio: "ignore" });
141
- reject(error);
142
- };
143
- const resolveOnce = () => {
144
- if (settled) {
145
- return;
146
- }
147
- settled = true;
148
- if (poll) {
149
- clearInterval(poll);
150
- }
151
- resolve();
152
- };
153
- const child = deps.spawnCommand("docker", [
154
- "run",
155
- "-it",
156
- "--name",
157
- containerName,
158
- "-p",
159
- `${port}:${socatPort}`,
160
- "--entrypoint",
161
- "bash",
162
- cfg.runtime_image,
163
- "-c",
164
- `source "$NVM_DIR/nvm.sh"; socat TCP-LISTEN:${socatPort},fork,bind=0.0.0.0,reuseaddr TCP:127.0.0.1:${port} 2>/dev/null & codex`,
165
- ], { stdio: "inherit" });
166
- child.on("error", (error) => {
167
- if (isErrnoException(error) && error.code === "ENOENT") {
168
- rejectOnce(buildDockerMissingError());
169
- return;
170
- }
171
- rejectOnce(new Error(`Failed to start Codex login container: ${error.message}`));
172
- });
173
- poll = setInterval(() => {
174
- if (settled) {
175
- return;
176
- }
177
- const check = deps.spawnSyncCommand("docker", ["exec", containerName, "sh", "-c", `test -f ${cfg.codex.codex_auth_path}`], {
178
- stdio: "ignore",
179
- });
180
- if (check.status === 0) {
181
- const resolveResult = deps.spawnSyncCommand("docker", ["exec", containerName, "sh", "-c", `echo ${cfg.codex.codex_auth_path}`], {
182
- encoding: "utf-8",
183
- });
184
- const containerAuthAbsPath = resolveResult.stdout.trim();
185
- const cpResult = deps.spawnSyncCommand("docker", ["cp", `${containerName}:${containerAuthAbsPath}`, destPath], {
186
- stdio: "ignore",
187
- });
188
- if (cpResult.status !== 0) {
189
- rejectOnce(new Error("Failed to extract auth file from container."));
190
- return;
191
- }
192
- authCopied = true;
193
- deps.spawnSyncCommand("docker", ["rm", "-f", containerName], { stdio: "ignore" });
194
- resolveOnce();
195
- }
196
- }, 1000);
197
- child.on("exit", () => {
198
- if (!authCopied) {
199
- rejectOnce(new Error("Codex login failed or was cancelled."));
200
- }
201
- });
202
- });
203
- await db.insert(schema_js_1.agentSdks).values({ name: "codex", authentication: "dedicated" });
204
- deps.promptApi.log.success(`Codex auth saved to ${destPath}`);
205
- }
206
101
  async function selectStartupAuthMode(options, deps) {
207
102
  if (options.length === 1) {
208
103
  return options[0].value;
@@ -230,30 +125,21 @@ async function startup(cfg = config_js_1.config.parse({}), overrides = {}) {
230
125
  return;
231
126
  }
232
127
  deps.promptApi.intro("No agent SDK configured. Let's set up Codex authentication.");
233
- const hostInfo = deps.getHostInfoFn(cfg.codex.codex_auth_path);
234
- const options = [
235
- {
236
- value: "dedicated",
237
- label: "Dedicated",
238
- hint: "recommended -- runs Codex login inside a container",
239
- },
240
- ];
241
- if (hostInfo.codexAuthExists) {
242
- options.push({
243
- value: "host",
244
- label: "Host",
245
- hint: `reuse existing credentials from ${cfg.codex.codex_auth_path}`,
246
- });
247
- }
128
+ const options = (0, auth_js_1.listCodexStartupAuthOptions)(cfg, deps.getHostInfoFn);
248
129
  try {
249
130
  const authMode = await selectStartupAuthMode(options, deps);
250
131
  if (authMode === "host") {
251
- await db.insert(schema_js_1.agentSdks).values({ name: "codex", authentication: "host" });
132
+ await (0, auth_js_1.setCodexHostAuthInDb)(db);
252
133
  await refreshCodexModelsInStartup(deps);
253
134
  deps.promptApi.outro("Codex SDK configured with host authentication.");
254
135
  return;
255
136
  }
256
- await dedicatedAuth(cfg, db, deps);
137
+ await (0, auth_js_1.runDedicatedCodexAuth)(cfg, db, {
138
+ logInfo: deps.promptApi.log.info,
139
+ logSuccess: deps.promptApi.log.success,
140
+ spawnCommand: deps.spawnCommand,
141
+ spawnSyncCommand: deps.spawnSyncCommand,
142
+ });
257
143
  await refreshCodexModelsInStartup(deps);
258
144
  deps.promptApi.outro("Codex login successful!");
259
145
  }
@@ -10,7 +10,7 @@ function registerStatusCommand(program) {
10
10
  program
11
11
  .command("status")
12
12
  .description("Show whether the local CompanyHelm daemon is running.")
13
- .option("--state-db-path <path>", "State database path override (defaults to ~/.local/share/companyhelm/state.db).")
13
+ .option("--state-db-path <path>", "State database path override (defaults to state.db under the active config directory).")
14
14
  .action(async (options) => {
15
15
  const cfg = config_js_1.config.parse({
16
16
  state_db_path: options.stateDbPath,
package/dist/config.js CHANGED
@@ -1,9 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.config = exports.codexConfig = void 0;
3
+ exports.config = exports.codexConfig = exports.DEFAULT_CONFIG_DIRECTORY = exports.CONFIG_PATH_ENV = void 0;
4
4
  const node_fs_1 = require("node:fs");
5
5
  const node_path_1 = require("node:path");
6
6
  const zod_1 = require("zod");
7
+ const path_js_1 = require("./utils/path.js");
8
+ exports.CONFIG_PATH_ENV = "COMPANYHELM_CONFIG_PATH";
9
+ exports.DEFAULT_CONFIG_DIRECTORY = "~/.config/companyhelm";
7
10
  const DEFAULT_RUNTIME_IMAGE_REPOSITORY = "companyhelm/runner";
8
11
  const FALLBACK_RUNTIME_IMAGE_VERSION = "latest";
9
12
  function loadRuntimeImageVersion() {
@@ -19,6 +22,19 @@ function loadRuntimeImageVersion() {
19
22
  return FALLBACK_RUNTIME_IMAGE_VERSION;
20
23
  }
21
24
  const DEFAULT_RUNTIME_IMAGE = `${DEFAULT_RUNTIME_IMAGE_REPOSITORY}:${loadRuntimeImageVersion()}`;
25
+ function resolveConfigRelativePath(configDirectory, pathValue) {
26
+ if ((0, node_path_1.isAbsolute)((0, path_js_1.expandHome)(pathValue))) {
27
+ return pathValue;
28
+ }
29
+ return (0, node_path_1.join)(configDirectory, pathValue);
30
+ }
31
+ function resolveConfigDirectoryDefault() {
32
+ const envValue = process.env[exports.CONFIG_PATH_ENV]?.trim();
33
+ if (envValue && envValue.length > 0) {
34
+ return envValue;
35
+ }
36
+ return exports.DEFAULT_CONFIG_DIRECTORY;
37
+ }
22
38
  exports.codexConfig = zod_1.z.object({
23
39
  codex_auth_file_path: zod_1.z.string()
24
40
  .describe("The path to the Codex authentication file on the host, relative to config_directory.")
@@ -36,13 +52,13 @@ exports.codexConfig = zod_1.z.object({
36
52
  exports.config = zod_1.z.object({
37
53
  config_directory: zod_1.z.string()
38
54
  .describe("The directory where the config files are stored.")
39
- .default("~/.config/companyhelm"),
55
+ .default(resolveConfigDirectoryDefault),
40
56
  workspaces_directory: zod_1.z.string()
41
57
  .describe("The directory where thread workspaces are stored, relative to config_directory when not absolute.")
42
58
  .default("workspaces"),
43
59
  state_db_path: zod_1.z.string()
44
- .describe("The path to the state database.")
45
- .default("~/.local/share/companyhelm/state.db"),
60
+ .describe("The path to the state database, relative to the config directory.")
61
+ .default("state.db"),
46
62
  companyhelm_api_url: zod_1.z.string()
47
63
  .describe("CompanyHelm control plane gRPC endpoint URL.")
48
64
  .default("https://api.companyhelm.com:50051"),
@@ -83,4 +99,7 @@ exports.config = zod_1.z.object({
83
99
  .describe("Default git author email used when runtime repositories are missing user.email.")
84
100
  .default("agent@companyhelm.com"),
85
101
  codex: exports.codexConfig.default(() => exports.codexConfig.parse({})),
86
- });
102
+ }).transform((value) => ({
103
+ ...value,
104
+ state_db_path: resolveConfigRelativePath(value.config_directory, value.state_db_path),
105
+ }));
@@ -56,12 +56,46 @@ class AppServerContainerService {
56
56
  this.child = null;
57
57
  this.containerName = null;
58
58
  this.running = false;
59
+ this.recentStderrLines = [];
60
+ this.lastExitCode = null;
61
+ this.lastExitSignal = null;
59
62
  this.docker = options?.docker ?? new dockerode_1.default();
60
63
  this.imageStatusReporter = options?.imageStatusReporter;
61
64
  }
62
65
  reportImageStatus(message) {
63
66
  this.imageStatusReporter?.(message);
64
67
  }
68
+ recordStderr(chunk) {
69
+ const lines = chunk
70
+ .split(/\r?\n/)
71
+ .map((line) => line.trim())
72
+ .filter((line) => line.length > 0);
73
+ if (lines.length === 0) {
74
+ return;
75
+ }
76
+ this.recentStderrLines.push(...lines);
77
+ if (this.recentStderrLines.length > 8) {
78
+ this.recentStderrLines.splice(0, this.recentStderrLines.length - 8);
79
+ }
80
+ }
81
+ buildContainerStoppedErrorMessage() {
82
+ const details = [];
83
+ if (this.containerName) {
84
+ details.push(`container ${this.containerName}`);
85
+ }
86
+ if (this.lastExitCode !== null) {
87
+ details.push(`exit code ${this.lastExitCode}`);
88
+ }
89
+ else if (this.lastExitSignal) {
90
+ details.push(`signal ${this.lastExitSignal}`);
91
+ }
92
+ if (this.recentStderrLines.length > 0) {
93
+ details.push(`stderr: ${this.recentStderrLines.join(" | ")}`);
94
+ }
95
+ return details.length > 0
96
+ ? `App server container is not running (${details.join(", ")})`
97
+ : "App server container is not running";
98
+ }
65
99
  static isImageNotFound(error) {
66
100
  if (typeof error !== "object" || error === null) {
67
101
  return false;
@@ -159,6 +193,9 @@ class AppServerContainerService {
159
193
  if (this.running) {
160
194
  throw new Error("App server container is already running");
161
195
  }
196
+ this.recentStderrLines = [];
197
+ this.lastExitCode = null;
198
+ this.lastExitSignal = null;
162
199
  const cfg = config_js_1.config.parse({});
163
200
  await this.ensureImageAvailable(cfg.runtime_image);
164
201
  const { db, client } = await (0, db_js_1.initDb)(cfg.state_db_path);
@@ -218,15 +255,27 @@ class AppServerContainerService {
218
255
  this.messageQueue.push({ type: "stdout", payload: chunk });
219
256
  });
220
257
  child.stderr.on("data", (chunk) => {
221
- this.messageQueue.push({ type: "stderr", payload: chunk.toString("utf8") });
258
+ const payload = chunk.toString("utf8");
259
+ this.recordStderr(payload);
260
+ this.messageQueue.push({ type: "stderr", payload });
222
261
  });
223
262
  child.on("error", (err) => {
263
+ this.recordStderr(err.message);
224
264
  this.messageQueue.push({ type: "error", reason: `docker process error: ${err.message}` });
225
265
  this.running = false;
226
266
  this.messageQueue.close();
227
267
  });
228
- child.on("exit", () => {
268
+ child.on("exit", (code, signal) => {
269
+ const wasRunning = this.running;
270
+ this.lastExitCode = code;
271
+ this.lastExitSignal = signal;
229
272
  this.running = false;
273
+ if (wasRunning) {
274
+ this.messageQueue.push({
275
+ type: "error",
276
+ reason: this.buildContainerStoppedErrorMessage(),
277
+ });
278
+ }
230
279
  this.messageQueue.close();
231
280
  });
232
281
  this.child = child;
@@ -253,7 +302,7 @@ class AppServerContainerService {
253
302
  }
254
303
  async sendRaw(payload) {
255
304
  if (!this.running || !this.child || !this.child.stdin) {
256
- throw new Error("App server container is not running");
305
+ throw new Error(this.buildContainerStoppedErrorMessage());
257
306
  }
258
307
  this.child.stdin.write(payload);
259
308
  }
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.formatSdkModelRefreshFailure = formatSdkModelRefreshFailure;
3
4
  exports.refreshSdkModels = refreshSdkModels;
4
5
  const drizzle_orm_1 = require("drizzle-orm");
5
6
  const config_js_1 = require("../../config.js");
@@ -7,6 +8,13 @@ const db_js_1 = require("../../state/db.js");
7
8
  const schema_js_1 = require("../../state/schema.js");
8
9
  const app_server_js_1 = require("../app_server.js");
9
10
  const app_server_container_js_1 = require("../docker/app_server_container.js");
11
+ function toErrorMessage(error) {
12
+ return error instanceof Error ? error.message : String(error);
13
+ }
14
+ function formatSdkModelRefreshFailure(sdk, error) {
15
+ return (`Failed to refresh ${sdk} models from the local Codex app-server: ${toErrorMessage(error)}. ` +
16
+ "Verify the runner image can start Codex app-server with valid auth, then retry.");
17
+ }
10
18
  async function fetchCodexModelsFromAppServer(clientName, logger, imageStatusReporter) {
11
19
  const transport = new app_server_container_js_1.AppServerContainerService({ imageStatusReporter });
12
20
  const appServer = new app_server_js_1.AppServerService(transport, clientName, logger);
@@ -6,6 +6,12 @@ AGENT_UID={{ agent_uid }}
6
6
  AGENT_GID={{ agent_gid }}
7
7
  CODEX_AUTH_PATH={{ codex_auth_path }}
8
8
  APP_SERVER_COMMAND={{ app_server_command }}
9
+ EXISTING_UID_USER=""
10
+
11
+ if getent passwd "$AGENT_UID" >/dev/null 2>&1; then
12
+ EXISTING_UID_USER="$(getent passwd "$AGENT_UID" | cut -d: -f1)"
13
+ AGENT_USER="$EXISTING_UID_USER"
14
+ fi
9
15
 
10
16
  AGENT_GROUP="$AGENT_USER"
11
17
  if getent group "$AGENT_GID" >/dev/null 2>&1; then
@@ -19,7 +25,7 @@ else
19
25
  fi
20
26
 
21
27
  if id -u "$AGENT_USER" >/dev/null 2>&1; then
22
- usermod -u "$AGENT_UID" -g "$AGENT_GROUP" -d "$AGENT_HOME" "$AGENT_USER" || true
28
+ usermod -u "$AGENT_UID" -g "$AGENT_GROUP" -d "$AGENT_HOME" -s /bin/bash "$AGENT_USER" || true
23
29
  else
24
30
  useradd -m -d "$AGENT_HOME" -u "$AGENT_UID" -g "$AGENT_GROUP" -s /bin/bash "$AGENT_USER"
25
31
  fi
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@companyhelm/cli",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
4
  "description": "Run coding agents in fully isolated Docker sandboxes, locally.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {