@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
@@ -53,97 +53,184 @@ function banner() {
53
53
  console.log(figlet_1.default.textSync("CompanyHelm", { font: "Small" }));
54
54
  console.log();
55
55
  }
56
- async function refreshCodexModelsInStartup() {
57
- const spinner = p.spinner();
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
+ const defaultStartupDependencies = {
63
+ getHostInfoFn: host_js_1.getHostInfo,
64
+ initDbFn: db_js_1.initDb,
65
+ promptApi: p,
66
+ refreshSdkModelsFn: refresh_models_js_1.refreshSdkModels,
67
+ spawnCommand: node_child_process_1.spawn,
68
+ spawnSyncCommand: node_child_process_1.spawnSync,
69
+ };
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
+ function restoreInteractiveTerminalState() {
77
+ try {
78
+ if (process.stdin.isTTY && typeof process.stdin.setRawMode === "function") {
79
+ process.stdin.setRawMode(false);
80
+ }
81
+ }
82
+ catch {
83
+ // Best-effort prompt cleanup.
84
+ }
85
+ try {
86
+ if (process.stdout.isTTY) {
87
+ process.stdout.write("\u001B[?25h");
88
+ }
89
+ }
90
+ catch {
91
+ // Best-effort prompt cleanup.
92
+ }
93
+ }
94
+ function exitStartup(code) {
95
+ restoreInteractiveTerminalState();
96
+ process.exit(code);
97
+ }
98
+ async function refreshCodexModelsInStartup(deps) {
99
+ ensureDockerAvailable(deps.spawnSyncCommand);
100
+ const spinner = deps.promptApi.spinner();
58
101
  const seenStatusMessages = new Set();
59
102
  spinner.start("Preparing Codex runtime image and refreshing model catalog via app-server");
60
- const results = await (0, refresh_models_js_1.refreshSdkModels)({
103
+ const results = await deps.refreshSdkModelsFn({
61
104
  sdk: "codex",
62
105
  imageStatusReporter: (message) => {
63
106
  if (seenStatusMessages.has(message)) {
64
107
  return;
65
108
  }
66
109
  seenStatusMessages.add(message);
67
- p.log.info(message);
110
+ deps.promptApi.log.info(message);
68
111
  },
69
112
  });
70
113
  const count = results[0]?.modelCount ?? 0;
71
114
  spinner.stop(`Codex model catalog refreshed (${count} models).`);
72
115
  }
73
- async function dedicatedAuth(cfg, db) {
116
+ async function dedicatedAuth(cfg, db, deps) {
117
+ ensureDockerAvailable(deps.spawnSyncCommand);
74
118
  const port = cfg.codex.codex_auth_port;
75
119
  const socatPort = port + 1;
76
120
  const containerName = `companyhelm-codex-auth-${Date.now()}`;
77
- p.log.info("Starting Codex login inside a container...");
78
- p.log.info("A browser URL will appear -- open it to complete authentication.");
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.");
79
123
  const configDir = (0, path_js_1.expandHome)(cfg.config_directory);
80
124
  if (!(0, node_fs_1.existsSync)(configDir)) {
81
125
  (0, node_fs_1.mkdirSync)(configDir, { recursive: true });
82
126
  }
83
127
  const destPath = (0, node_path_1.join)(configDir, cfg.codex.codex_auth_file_path);
84
- const child = (0, node_child_process_1.spawn)("docker", [
85
- "run",
86
- "-it",
87
- "--name",
88
- containerName,
89
- "-p",
90
- `${port}:${socatPort}`,
91
- "--entrypoint",
92
- "bash",
93
- cfg.runtime_image,
94
- "-c",
95
- `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`,
96
- ], { stdio: "inherit" });
97
128
  let authCopied = false;
98
129
  await new Promise((resolve, reject) => {
99
- const poll = setInterval(() => {
100
- const check = (0, node_child_process_1.spawnSync)("docker", ["exec", containerName, "sh", "-c", `test -f ${cfg.codex.codex_auth_path}`], {
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}`], {
101
178
  stdio: "ignore",
102
179
  });
103
180
  if (check.status === 0) {
104
- clearInterval(poll);
105
- const resolveResult = (0, node_child_process_1.spawnSync)("docker", ["exec", containerName, "sh", "-c", `echo ${cfg.codex.codex_auth_path}`], {
181
+ const resolveResult = deps.spawnSyncCommand("docker", ["exec", containerName, "sh", "-c", `echo ${cfg.codex.codex_auth_path}`], {
106
182
  encoding: "utf-8",
107
183
  });
108
184
  const containerAuthAbsPath = resolveResult.stdout.trim();
109
- const cpResult = (0, node_child_process_1.spawnSync)("docker", ["cp", `${containerName}:${containerAuthAbsPath}`, destPath], {
185
+ const cpResult = deps.spawnSyncCommand("docker", ["cp", `${containerName}:${containerAuthAbsPath}`, destPath], {
110
186
  stdio: "ignore",
111
187
  });
112
188
  if (cpResult.status !== 0) {
113
- (0, node_child_process_1.spawnSync)("docker", ["rm", "-f", containerName], { stdio: "ignore" });
114
- reject(new Error("Failed to extract auth file from container."));
189
+ rejectOnce(new Error("Failed to extract auth file from container."));
115
190
  return;
116
191
  }
117
192
  authCopied = true;
118
- (0, node_child_process_1.spawnSync)("docker", ["rm", "-f", containerName], { stdio: "ignore" });
119
- resolve();
193
+ deps.spawnSyncCommand("docker", ["rm", "-f", containerName], { stdio: "ignore" });
194
+ resolveOnce();
120
195
  }
121
196
  }, 1000);
122
197
  child.on("exit", () => {
123
- clearInterval(poll);
124
198
  if (!authCopied) {
125
- (0, node_child_process_1.spawnSync)("docker", ["rm", "-f", containerName], { stdio: "ignore" });
126
- reject(new Error("Codex login failed or was cancelled."));
199
+ rejectOnce(new Error("Codex login failed or was cancelled."));
127
200
  }
128
201
  });
129
202
  });
130
203
  await db.insert(schema_js_1.agentSdks).values({ name: "codex", authentication: "dedicated" });
131
- p.log.success(`Codex auth saved to ${destPath}`);
204
+ deps.promptApi.log.success(`Codex auth saved to ${destPath}`);
132
205
  }
133
- async function startup() {
206
+ async function selectStartupAuthMode(options, deps) {
207
+ if (options.length === 1) {
208
+ return options[0].value;
209
+ }
210
+ const authMode = await deps.promptApi.select({
211
+ message: "How would you like to authenticate Codex?",
212
+ options,
213
+ });
214
+ if (deps.promptApi.isCancel(authMode)) {
215
+ deps.promptApi.cancel("Setup cancelled.");
216
+ exitStartup(0);
217
+ }
218
+ return authMode;
219
+ }
220
+ async function startup(cfg = config_js_1.config.parse({}), overrides = {}) {
221
+ const deps = { ...defaultStartupDependencies, ...overrides };
134
222
  banner();
135
- const cfg = config_js_1.config.parse({});
136
- const s = p.spinner();
223
+ const s = deps.promptApi.spinner();
137
224
  s.start("Initializing state database");
138
- const { db } = await (0, db_js_1.initDb)(cfg.state_db_path);
225
+ const { db } = await deps.initDbFn(cfg.state_db_path);
139
226
  s.stop("State database ready.");
140
227
  const sdks = await db.select().from(schema_js_1.agentSdks).all();
141
228
  if (sdks.length > 0) {
142
- p.log.success(`Agent SDK configured: ${sdks.map((sdk) => sdk.name).join(", ")}`);
229
+ deps.promptApi.log.success(`Agent SDK configured: ${sdks.map((sdk) => sdk.name).join(", ")}`);
143
230
  return;
144
231
  }
145
- p.intro("No agent SDK configured. Let's set up Codex authentication.");
146
- const hostInfo = (0, host_js_1.getHostInfo)(cfg.codex.codex_auth_path);
232
+ deps.promptApi.intro("No agent SDK configured. Let's set up Codex authentication.");
233
+ const hostInfo = deps.getHostInfoFn(cfg.codex.codex_auth_path);
147
234
  const options = [
148
235
  {
149
236
  value: "dedicated",
@@ -158,28 +245,24 @@ async function startup() {
158
245
  hint: `reuse existing credentials from ${cfg.codex.codex_auth_path}`,
159
246
  });
160
247
  }
161
- const authMode = await p.select({
162
- message: "How would you like to authenticate Codex?",
163
- options,
164
- });
165
- if (p.isCancel(authMode)) {
166
- p.cancel("Setup cancelled.");
167
- process.exit(0);
168
- }
169
248
  try {
249
+ const authMode = await selectStartupAuthMode(options, deps);
170
250
  if (authMode === "host") {
171
251
  await db.insert(schema_js_1.agentSdks).values({ name: "codex", authentication: "host" });
172
- await refreshCodexModelsInStartup();
173
- p.outro("Codex SDK configured with host authentication.");
252
+ await refreshCodexModelsInStartup(deps);
253
+ deps.promptApi.outro("Codex SDK configured with host authentication.");
174
254
  return;
175
255
  }
176
- await dedicatedAuth(cfg, db);
177
- await refreshCodexModelsInStartup();
178
- p.outro("Codex login successful!");
256
+ await dedicatedAuth(cfg, db, deps);
257
+ await refreshCodexModelsInStartup(deps);
258
+ deps.promptApi.outro("Codex login successful!");
179
259
  }
180
260
  catch (error) {
181
261
  const message = error instanceof Error ? error.message : "Codex setup failed.";
182
- p.cancel(message);
183
- process.exit(1);
262
+ deps.promptApi.cancel(message);
263
+ exitStartup(1);
264
+ }
265
+ finally {
266
+ restoreInteractiveTerminalState();
184
267
  }
185
268
  }
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerStatusCommand = registerStatusCommand;
4
+ const node_path_1 = require("node:path");
5
+ const config_js_1 = require("../config.js");
6
+ const daemon_state_js_1 = require("../state/daemon_state.js");
7
+ const daemon_js_1 = require("../utils/daemon.js");
8
+ const process_js_1 = require("../utils/process.js");
9
+ function registerStatusCommand(program) {
10
+ program
11
+ .command("status")
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).")
14
+ .action(async (options) => {
15
+ const cfg = config_js_1.config.parse({
16
+ state_db_path: options.stateDbPath,
17
+ });
18
+ const state = await (0, daemon_state_js_1.readCurrentDaemonState)(cfg.state_db_path);
19
+ const running = state?.pid != null && (0, process_js_1.isProcessRunning)(state.pid);
20
+ const logPath = state?.logPath ?? (0, daemon_js_1.resolveDaemonLogPath)(cfg.state_db_path);
21
+ const logDirectory = state?.logPath ? (0, node_path_1.dirname)(state.logPath) : (0, daemon_js_1.resolveDaemonLogDirectory)(cfg.state_db_path);
22
+ console.log(`Daemon: ${running ? "running" : "not running"}`);
23
+ if (state?.pid != null) {
24
+ console.log(`PID: ${running ? state.pid : `${state.pid} (stale)`}`);
25
+ }
26
+ else {
27
+ console.log("PID: none");
28
+ }
29
+ console.log(`Log directory: ${logDirectory}`);
30
+ console.log(`Log file: ${logPath}`);
31
+ });
32
+ }
@@ -104,6 +104,9 @@ class AppServerService {
104
104
  async startThread(params) {
105
105
  return this.request("thread/start", params, 15000);
106
106
  }
107
+ async startThreadWithResponse(params, requestId) {
108
+ return this.requestWithResponse("thread/start", params, 15000, requestId);
109
+ }
107
110
  async resumeThread(params) {
108
111
  return this.request("thread/resume", params, 15000);
109
112
  }
@@ -160,6 +163,9 @@ class AppServerService {
160
163
  if (message.method === "error" &&
161
164
  message.params.threadId === threadId &&
162
165
  message.params.turnId === turnId) {
166
+ if (message.params.willRetry) {
167
+ continue;
168
+ }
163
169
  throw new Error(message.params.error.message);
164
170
  }
165
171
  if (message.method === "turn/completed" &&
@@ -206,14 +212,22 @@ class AppServerService {
206
212
  }
207
213
  }
208
214
  async request(method, params, timeoutMs) {
209
- const requestId = this.nextRequestId++;
215
+ const response = await this.requestWithResponse(method, params, timeoutMs);
216
+ return response.result;
217
+ }
218
+ async requestWithResponse(method, params, timeoutMs, requestId) {
219
+ const resolvedRequestId = requestId ?? this.nextRequestId++;
210
220
  const request = {
211
221
  method,
212
- id: requestId,
222
+ id: resolvedRequestId,
213
223
  params,
214
224
  };
215
225
  await this.sendMessage(request);
216
- return this.waitForResponseResult(requestId, timeoutMs);
226
+ const response = await this.waitForResponseMessage(resolvedRequestId, timeoutMs);
227
+ return {
228
+ id: response.id,
229
+ result: response.result,
230
+ };
217
231
  }
218
232
  async sendMessage(message) {
219
233
  const payload = JSON.stringify(message);
@@ -369,7 +383,7 @@ class AppServerService {
369
383
  pendingRequest.reject(new Error(`app-server returned an error for request ${String(message.id)}: ${formatUnknownError(message.error)}`));
370
384
  return;
371
385
  }
372
- pendingRequest.resolve(message.result);
386
+ pendingRequest.resolve(message);
373
387
  }
374
388
  rejectAllPendingRequests(error) {
375
389
  for (const [requestId, pendingRequest] of this.pendingRequests.entries()) {
@@ -378,14 +392,14 @@ class AppServerService {
378
392
  pendingRequest.reject(new Error(`request ${String(requestId)} failed: ${error.message}`));
379
393
  }
380
394
  }
381
- async waitForResponseResult(requestId, timeoutMs) {
395
+ async waitForResponseMessage(requestId, timeoutMs) {
382
396
  const immediateResponse = this.pendingResponses.get(requestId);
383
397
  if (immediateResponse) {
384
398
  this.pendingResponses.delete(requestId);
385
399
  if (immediateResponse.error !== undefined) {
386
400
  throw new Error(`app-server returned an error for request ${String(requestId)}: ${formatUnknownError(immediateResponse.error)}`);
387
401
  }
388
- return immediateResponse.result;
402
+ return immediateResponse;
389
403
  }
390
404
  return new Promise((resolve, reject) => {
391
405
  const timeout = setTimeout(() => {
@@ -394,8 +408,8 @@ class AppServerService {
394
408
  }, timeoutMs);
395
409
  const pendingRequest = {
396
410
  timeout,
397
- resolve: (result) => {
398
- resolve(result);
411
+ resolve: (message) => {
412
+ resolve(message);
399
413
  },
400
414
  reject: (error) => {
401
415
  reject(error);
@@ -413,7 +427,7 @@ class AppServerService {
413
427
  reject(new Error(`app-server returned an error for request ${String(requestId)}: ${formatUnknownError(bufferedResponse.error)}`));
414
428
  return;
415
429
  }
416
- resolve(bufferedResponse.result);
430
+ resolve(bufferedResponse);
417
431
  });
418
432
  }
419
433
  formatDebugContext() {
@@ -151,7 +151,7 @@ class AppServerContainerService {
151
151
  throw error;
152
152
  }
153
153
  }
154
- this.reportImageStatus(`Docker image '${image}' not found locally. Downloading now.`);
154
+ this.reportImageStatus(`Docker image '${image}' not found locally. Pulling remotely.`);
155
155
  await this.pullImage(image);
156
156
  this.reportImageStatus(`Docker image '${image}' is ready.`);
157
157
  }
@@ -210,6 +210,7 @@ class AppServerContainerService {
210
210
  "-lc",
211
211
  bootstrapScript,
212
212
  ];
213
+ this.reportImageStatus(`Launching Docker container from image '${cfg.runtime_image}'.`);
213
214
  const child = (0, node_child_process_1.spawn)("docker", args, {
214
215
  stdio: ["pipe", "pipe", "pipe"],
215
216
  });
@@ -230,6 +231,7 @@ class AppServerContainerService {
230
231
  });
231
232
  this.child = child;
232
233
  this.running = true;
234
+ this.reportImageStatus(`Waiting for app-server to initialize in Docker container '${this.containerName}'.`);
233
235
  }
234
236
  async stop() {
235
237
  this.running = false;
@@ -527,7 +527,7 @@ class ThreadContainerService {
527
527
  throw error;
528
528
  }
529
529
  }
530
- imageStatusReporter?.(`Docker image '${image}' not found locally. Downloading now.`);
530
+ imageStatusReporter?.(`Docker image '${image}' not found locally. Pulling remotely.`);
531
531
  await this.pullImage(image, imageStatusReporter);
532
532
  imageStatusReporter?.(`Docker image '${image}' is ready.`);
533
533
  }
@@ -539,6 +539,7 @@ class ThreadContainerService {
539
539
  }
540
540
  await this.ensureImageAvailable(options.runtimeImage, options.imageStatusReporter);
541
541
  try {
542
+ options.imageStatusReporter?.(`Creating Docker container '${options.names.runtime}' from image '${options.runtimeImage}'.`);
542
543
  await this.docker.createContainer(buildRuntimeContainerOptions(options));
543
544
  }
544
545
  catch (error) {
@@ -552,8 +553,10 @@ class ThreadContainerService {
552
553
  if (options.runtimeImage !== options.dindImage) {
553
554
  await this.ensureImageAvailable(options.runtimeImage, options.imageStatusReporter);
554
555
  }
556
+ options.imageStatusReporter?.(`Creating Docker container '${options.names.dind}' from image '${options.dindImage}'.`);
555
557
  await this.docker.createContainer(buildDindContainerOptions(options));
556
558
  try {
559
+ options.imageStatusReporter?.(`Creating Docker container '${options.names.runtime}' from image '${options.runtimeImage}'.`);
557
560
  await this.docker.createContainer(buildRuntimeContainerOptions(options));
558
561
  }
559
562
  catch (error) {
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RUNNER_DAEMON_STATE_ID = void 0;
4
+ exports.readCurrentDaemonState = readCurrentDaemonState;
5
+ exports.claimCurrentDaemonState = claimCurrentDaemonState;
6
+ exports.clearCurrentDaemonState = clearCurrentDaemonState;
7
+ const drizzle_orm_1 = require("drizzle-orm");
8
+ const db_js_1 = require("./db.js");
9
+ const schema_js_1 = require("./schema.js");
10
+ const process_js_1 = require("../utils/process.js");
11
+ exports.RUNNER_DAEMON_STATE_ID = "runner";
12
+ async function readCurrentDaemonState(stateDbPath) {
13
+ const { db, client } = await (0, db_js_1.initDb)(stateDbPath);
14
+ try {
15
+ const existing = await db.select().from(schema_js_1.daemonState).where((0, drizzle_orm_1.eq)(schema_js_1.daemonState.id, exports.RUNNER_DAEMON_STATE_ID)).all();
16
+ const row = existing[0];
17
+ if (!row) {
18
+ return null;
19
+ }
20
+ return {
21
+ id: row.id,
22
+ pid: row.pid ?? null,
23
+ logPath: row.logPath ?? null,
24
+ startedAt: row.startedAt,
25
+ updatedAt: row.updatedAt,
26
+ };
27
+ }
28
+ finally {
29
+ client.close();
30
+ }
31
+ }
32
+ async function claimCurrentDaemonState(stateDbPath, pid, logPath) {
33
+ const now = new Date().toISOString();
34
+ const { client } = await (0, db_js_1.initDb)(stateDbPath);
35
+ try {
36
+ await client.execute("BEGIN IMMEDIATE");
37
+ try {
38
+ const existing = await client.execute({
39
+ sql: "SELECT pid FROM daemon_state WHERE id = ?",
40
+ args: [exports.RUNNER_DAEMON_STATE_ID],
41
+ });
42
+ const row = existing.rows[0];
43
+ const currentPid = typeof row?.pid === "number" ? row.pid : row?.pid == null ? null : Number(row.pid);
44
+ if (currentPid && currentPid !== pid && (0, process_js_1.isProcessRunning)(currentPid)) {
45
+ throw new Error(`Another companyhelm daemon is already running with pid ${currentPid}.`);
46
+ }
47
+ await client.execute({
48
+ sql: "INSERT INTO daemon_state (id, pid, log_path, started_at, updated_at) VALUES (?, ?, ?, ?, ?) " +
49
+ "ON CONFLICT(id) DO UPDATE SET pid = excluded.pid, log_path = excluded.log_path, started_at = excluded.started_at, updated_at = excluded.updated_at",
50
+ args: [exports.RUNNER_DAEMON_STATE_ID, pid, logPath, now, now],
51
+ });
52
+ await client.execute("COMMIT");
53
+ }
54
+ catch (error) {
55
+ await client.execute("ROLLBACK");
56
+ throw error;
57
+ }
58
+ }
59
+ finally {
60
+ client.close();
61
+ }
62
+ }
63
+ async function clearCurrentDaemonState(stateDbPath, pid) {
64
+ const now = new Date().toISOString();
65
+ const { db, client } = await (0, db_js_1.initDb)(stateDbPath);
66
+ try {
67
+ const existing = await db.select().from(schema_js_1.daemonState).where((0, drizzle_orm_1.eq)(schema_js_1.daemonState.id, exports.RUNNER_DAEMON_STATE_ID)).all();
68
+ const current = existing[0];
69
+ if (!current || current.pid !== pid) {
70
+ return;
71
+ }
72
+ await db
73
+ .update(schema_js_1.daemonState)
74
+ .set({
75
+ pid: null,
76
+ updatedAt: now,
77
+ })
78
+ .where((0, drizzle_orm_1.eq)(schema_js_1.daemonState.id, exports.RUNNER_DAEMON_STATE_ID));
79
+ }
80
+ finally {
81
+ client.close();
82
+ }
83
+ }
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.threadUserMessageRequestStore = exports.threads = exports.llmModels = exports.agentSdks = void 0;
3
+ exports.daemonState = exports.threadUserMessageRequestStore = exports.threads = exports.llmModels = exports.agentSdks = void 0;
4
4
  const sqlite_core_1 = require("drizzle-orm/sqlite-core");
5
5
  // ── agent_sdks ──────────────────────────────────────────────────────────────
6
6
  exports.agentSdks = (0, sqlite_core_1.sqliteTable)("agent_sdks", {
@@ -49,3 +49,11 @@ exports.threadUserMessageRequestStore = (0, sqlite_core_1.sqliteTable)("thread_u
49
49
  requestId: (0, sqlite_core_1.text)("request_id").notNull(),
50
50
  sdkItemId: (0, sqlite_core_1.text)("sdk_item_id"),
51
51
  });
52
+ // -- daemon_state ------------------------------------------------------------
53
+ exports.daemonState = (0, sqlite_core_1.sqliteTable)("daemon_state", {
54
+ id: (0, sqlite_core_1.text)("id").primaryKey(),
55
+ pid: (0, sqlite_core_1.integer)("pid"),
56
+ logPath: (0, sqlite_core_1.text)("log_path"),
57
+ startedAt: (0, sqlite_core_1.text)("started_at").notNull(),
58
+ updatedAt: (0, sqlite_core_1.text)("updated_at").notNull(),
59
+ });
@@ -0,0 +1,46 @@
1
+ set -euo pipefail
2
+
3
+ AGENT_USER={{ agent_user }}
4
+ AGENT_HOME={{ agent_home }}
5
+ AGENT_UID={{ agent_uid }}
6
+ AGENT_GID={{ agent_gid }}
7
+ CODEX_AUTH_PATH={{ codex_auth_path }}
8
+ APP_SERVER_COMMAND={{ app_server_command }}
9
+
10
+ AGENT_GROUP="$AGENT_USER"
11
+ if getent group "$AGENT_GID" >/dev/null 2>&1; then
12
+ AGENT_GROUP="$(getent group "$AGENT_GID" | cut -d: -f1)"
13
+ elif getent group "$AGENT_USER" >/dev/null 2>&1; then
14
+ groupmod -g "$AGENT_GID" "$AGENT_USER"
15
+ AGENT_GROUP="$AGENT_USER"
16
+ else
17
+ groupadd -g "$AGENT_GID" "$AGENT_USER"
18
+ AGENT_GROUP="$AGENT_USER"
19
+ fi
20
+
21
+ if id -u "$AGENT_USER" >/dev/null 2>&1; then
22
+ usermod -u "$AGENT_UID" -g "$AGENT_GROUP" -d "$AGENT_HOME" "$AGENT_USER" || true
23
+ else
24
+ useradd -m -d "$AGENT_HOME" -u "$AGENT_UID" -g "$AGENT_GROUP" -s /bin/bash "$AGENT_USER"
25
+ fi
26
+
27
+ SUDOERS_FILE="/etc/sudoers.d/90-companyhelm-agent"
28
+ if command -v sudo >/dev/null 2>&1; then
29
+ install -d -m 0750 /etc/sudoers.d
30
+ printf '%s\n' "$AGENT_USER ALL=(ALL) NOPASSWD:ALL" > "$SUDOERS_FILE"
31
+ chmod 0440 "$SUDOERS_FILE"
32
+ if command -v visudo >/dev/null 2>&1; then
33
+ visudo -cf "$SUDOERS_FILE" >/dev/null
34
+ fi
35
+ fi
36
+
37
+ mkdir -p "$AGENT_HOME"
38
+ chown "$AGENT_UID:$AGENT_GID" "$AGENT_HOME" || true
39
+
40
+ if [ -n "${CODEX_AUTH_PATH:-}" ]; then
41
+ mkdir -p "$(dirname "$CODEX_AUTH_PATH")"
42
+ chown -R "$AGENT_UID:$AGENT_GID" "$(dirname "$CODEX_AUTH_PATH")" || true
43
+ fi
44
+
45
+ export HOME="$AGENT_HOME"
46
+ exec sudo -n -E -H -u "$AGENT_USER" bash -lc "$APP_SERVER_COMMAND"
@@ -0,0 +1,50 @@
1
+ # Agent Instructions
2
+
3
+ ## Workspace Structure
4
+
5
+ - You are running in a thread-specific container and workspace
6
+ - This workspace is not initialized as a Git repository by default. Repositories should live in a subdirectory of the workspace.
7
+
8
+ ## Docker
9
+
10
+ Docker is available, the docker host runs in a separate container. The network is shared with container.
11
+ - only the `/workspace` is shared with the docker host
12
+ - `{{home_directory}}/.codex/auth.json` is also shared with the docker host
13
+ - Nested DinD is not supported in this environment because the outer runtime already uses rootless DinD.
14
+ - If you need to use Docker in Docker you can mount the docker socket in the container and use this environment DinD to run containers within containers.
15
+
16
+ ## GitHub Installations
17
+
18
+ - Synced GitHub installation credentials are written to `/workspace/.companyhelm/installations.json`.
19
+ - Use `list-installations` to inspect installation IDs, repository scopes, tokens, and expiration timestamps.
20
+ - Use `gh-use-installation <installation-id>` to configure `gh` authentication for a specific installation.
21
+
22
+ ```bash
23
+ # Inspect synced installation credentials
24
+ list-installations
25
+
26
+ # Configure gh to use installation 112331765
27
+ gh-use-installation 112331765
28
+
29
+ # Verify gh is authenticated for github.com
30
+ gh auth status --hostname github.com
31
+ ```
32
+
33
+ ## Available CLI Tools
34
+
35
+ - `list-installations`: list synced GitHub installations with repositories, access tokens, and expirations.
36
+ - `gh-use-installation <installation-id>`: configure `gh` authentication for a selected GitHub installation token.
37
+ - `aws`: AWS CLI is pre-installed and available in `PATH`.
38
+ - For scripted PR creation/updates, always use `gh pr create --body-file <path>` and `gh pr edit --body-file <path>` instead of inline `--body` to avoid shell interpolation of markdown backticks.
39
+ - DO NOT INSTALL PLAYWRIGHT IN THE RUNTIME IMAGE. Playwright CLI is already installed and available for browser automation tasks with Chromium pre-installed: `playwright open --browser=chromium ...`
40
+
41
+ ## CompanyHelm Agent CLI
42
+
43
+ - `companyhelm-agent` is pre-installed in the runtime image.
44
+ - Thread bootstrap writes `{{home_directory}}/.config/companyhelm-agent-cli/config.json` with:
45
+ - `agent_api_url`: localhost targets are rewritten to `host.docker.internal` (for example `http://host.docker.internal:<port>`) for Docker-to-host access.
46
+ - `token`: sourced from the thread secret.
47
+ - Example commands:
48
+ - `companyhelm-agent task get --task-id <id>`
49
+ - `companyhelm-agent task dependencies --task-id <id>`
50
+ - `companyhelm-agent task update-status --task-id <id> --status <draft|pending|in_progress|completed>`
@@ -0,0 +1,19 @@
1
+ # CompanyHelm runtime shell initialization.
2
+ # This file is generated by companyhelm-cli when a thread runtime starts.
3
+
4
+ if [ -z "${NVM_DIR:-}" ] || [ ! -s "$NVM_DIR/nvm.sh" ]; then
5
+ for candidate in "/usr/local/nvm" "$HOME/.nvm" "/opt/nvm" "/root/.nvm"; do
6
+ if [ -s "$candidate/nvm.sh" ]; then
7
+ export NVM_DIR="$candidate"
8
+ break
9
+ fi
10
+ done
11
+ fi
12
+
13
+ if [ -n "${NVM_DIR:-}" ] && [ -s "$NVM_DIR/nvm.sh" ]; then
14
+ . "$NVM_DIR/nvm.sh"
15
+ nvm use --silent default >/dev/null 2>&1 || true
16
+ if [ -s "$NVM_DIR/bash_completion" ]; then
17
+ . "$NVM_DIR/bash_completion"
18
+ fi
19
+ fi