@engineeros/connector 0.4.1 → 0.4.4

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.
package/README.md CHANGED
@@ -36,3 +36,14 @@ Credentials are stored per workspace under `~/.engineeros/connectors` with owner
36
36
  Keep the connector online to receive Goals assigned from EngineerOS. Each Goal runs in an isolated worktree below `~/.engineeros/runs`. Cancellation stops Codex. The connector returns changed paths, a bounded diff, and the exact repository ZIP; a human still performs independent attestation.
37
37
 
38
38
  Requirements: Node.js 22 or newer, Git, and an authenticated Codex CLI (`codex login`).
39
+
40
+ ## Codex CLI compatibility
41
+
42
+ The connector prints the exact Codex CLI version it will use before connecting. If the
43
+ configured model requires a newer CLI, update Codex and restart the connector:
44
+
45
+ ```sh
46
+ npm install -g @openai/codex@latest
47
+ codex --version
48
+ npx @engineeros/connector start --workspace .
49
+ ```
@@ -10,9 +10,11 @@ import {
10
10
  workspaceUrl,
11
11
  } from "../src/config.mjs";
12
12
  import {
13
+ assessmentProgressMessage,
13
14
  executeAssignment,
14
15
  executeConnectedPrompt,
15
16
  executeWorkspaceAssessment,
17
+ inspectCodexCli,
16
18
  stopProcess,
17
19
  workspaceSnapshot,
18
20
  } from "../src/runner.mjs";
@@ -73,6 +75,17 @@ if (command === "pair") {
73
75
  fail("Use `engineeros-connector pair`, `start`, or `status`.");
74
76
  }
75
77
 
78
+ let codexCli;
79
+ try {
80
+ codexCli = await inspectCodexCli(config.workspace);
81
+ } catch (error) {
82
+ fail(error instanceof Error ? error.message : String(error));
83
+ }
84
+ console.log(`Using ${codexCli.version}.`);
85
+ if (firstMessage.type === "pair") {
86
+ firstMessage.capabilities.codex_cli_version = codexCli.version;
87
+ }
88
+
76
89
  let stopped = false;
77
90
  let active = null;
78
91
  const available = [];
@@ -375,9 +388,8 @@ async function executeAssessment(assignment) {
375
388
  if (active?.runId === assessmentId) active.child = child;
376
389
  },
377
390
  onEvent: (event) => {
378
- const message =
379
- event.message || event.item?.text || event.type || "Codex is assessing the workspace";
380
- reportProgress(message);
391
+ const message = assessmentProgressMessage(event);
392
+ if (message) reportProgress(message);
381
393
  },
382
394
  });
383
395
  const response = await fetch(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@engineeros/connector",
3
- "version": "0.4.1",
3
+ "version": "0.4.4",
4
4
  "description": "Connect a local Codex CLI workspace to EngineerOS over an outbound WebSocket.",
5
5
  "private": false,
6
6
  "type": "module",
package/src/runner.mjs CHANGED
@@ -1,6 +1,13 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { spawn } from "node:child_process";
3
- import { lstat, mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
3
+ import {
4
+ lstat,
5
+ mkdir,
6
+ readFile,
7
+ readdir,
8
+ stat,
9
+ writeFile,
10
+ } from "node:fs/promises";
4
11
  import os from "node:os";
5
12
  import path from "node:path";
6
13
  import { promisify } from "node:util";
@@ -114,14 +121,30 @@ export async function executeAssignment(assignment, config, callbacks) {
114
121
  };
115
122
  }
116
123
 
117
- export async function executeWorkspaceAssessment(assignment, config, callbacks) {
124
+ export async function executeWorkspaceAssessment(
125
+ assignment,
126
+ config,
127
+ callbacks,
128
+ ) {
118
129
  const execution = connectorExecution(assignment);
119
- const revision = await run(
130
+ const startingRevision = await run(
120
131
  "git",
121
132
  ["rev-parse", "HEAD"],
122
133
  config.workspace,
123
134
  { allowFailure: true },
124
135
  );
136
+ const startingHead =
137
+ startingRevision.code === 0
138
+ ? startingRevision.stdout.trim().slice(0, 128)
139
+ : null;
140
+ if (
141
+ assignment.target_head_revision &&
142
+ startingHead !== assignment.target_head_revision
143
+ ) {
144
+ throw new Error(
145
+ "This workspace is no longer at the commit inventoried by EngineerOS. Refresh the workspace inventory before assessing it.",
146
+ );
147
+ }
125
148
  const controller = launchCodexProcess(
126
149
  config.workspace,
127
150
  execution.prompt,
@@ -132,10 +155,24 @@ export async function executeWorkspaceAssessment(assignment, config, callbacks)
132
155
  const completed = await controller.completed;
133
156
  const report = completed.finalMessage.trim();
134
157
  if (!report) throw new Error("Codex completed without returning a response.");
158
+ const endingRevision = await run(
159
+ "git",
160
+ ["rev-parse", "HEAD"],
161
+ config.workspace,
162
+ { allowFailure: true },
163
+ );
164
+ const endingHead =
165
+ endingRevision.code === 0
166
+ ? endingRevision.stdout.trim().slice(0, 128)
167
+ : null;
168
+ if (startingHead !== endingHead) {
169
+ throw new Error(
170
+ "The Git commit changed during assessment. Refresh the workspace inventory and assess the new commit.",
171
+ );
172
+ }
135
173
  return {
136
174
  report_markdown: report,
137
- observed_head_revision:
138
- revision.code === 0 ? revision.stdout.trim().slice(0, 128) : null,
175
+ observed_head_revision: endingHead,
139
176
  };
140
177
  }
141
178
 
@@ -156,6 +193,70 @@ export async function executeConnectedPrompt(assignment, config, callbacks) {
156
193
  return { content, model: "codex-cli" };
157
194
  }
158
195
 
196
+ export async function inspectCodexCli(workspace = process.cwd()) {
197
+ const command =
198
+ process.env.CODEX_BIN ||
199
+ (process.platform === "win32" ? "codex.cmd" : "codex");
200
+ const result = await runCodexCommand(command, ["--version"], workspace);
201
+ if (result.code !== 0) {
202
+ throw new Error(
203
+ "Codex CLI is unavailable. Install it with `npm install -g @openai/codex@latest`, run `codex login`, then restart this connector.",
204
+ );
205
+ }
206
+ const version = result.stdout.trim().slice(0, 100);
207
+ if (!version) {
208
+ throw new Error(
209
+ "Codex CLI returned no version. Reinstall @openai/codex, then restart this connector.",
210
+ );
211
+ }
212
+ return { command, version };
213
+ }
214
+
215
+ export function codexFailureMessage(output, code) {
216
+ if (/requires a newer version of Codex/i.test(output)) {
217
+ return (
218
+ "The configured model requires a newer Codex CLI. " +
219
+ "Run `npm install -g @openai/codex@latest`, verify with `codex --version`, " +
220
+ "then restart the EngineerOS connector and retry the assessment."
221
+ );
222
+ }
223
+ if (/not logged in|login required|authentication required/i.test(output)) {
224
+ return "Codex CLI is not authenticated. Run `codex login`, then restart the EngineerOS connector.";
225
+ }
226
+ return `Codex exited with code ${code}. ${output.slice(-1_000)}`;
227
+ }
228
+
229
+ export function assessmentProgressMessage(event) {
230
+ if (!event || typeof event !== "object") return null;
231
+ if (event.type === "turn.started")
232
+ return "Codex started analyzing repository evidence";
233
+ if (event.type === "item.started") {
234
+ if (event.item?.type === "command_execution") {
235
+ const command = compactCommand(event.item.command);
236
+ return command
237
+ ? `Inspecting with ${command}`
238
+ : "Inspecting repository files";
239
+ }
240
+ if (event.item?.type === "mcp_tool_call")
241
+ return "Consulting a connected analysis tool";
242
+ if (event.item?.type === "web_search")
243
+ return "Checking an external technical reference";
244
+ return null;
245
+ }
246
+ if (event.type === "item.completed" && event.item?.type === "agent_message") {
247
+ return "Assessment report prepared";
248
+ }
249
+ return null;
250
+ }
251
+
252
+ function compactCommand(command) {
253
+ const value = Array.isArray(command)
254
+ ? command.join(" ")
255
+ : String(command || "");
256
+ const compact = value.replace(/\s+/g, " ").trim();
257
+ return compact.length > 96 ? `${compact.slice(0, 93)}...` : compact;
258
+ }
259
+
159
260
  export function connectorExecution(assignment) {
160
261
  const prompt = assignment?.prompt_markdown;
161
262
  if (typeof prompt !== "string" || !prompt.trim()) {
@@ -292,15 +393,7 @@ function launchCodexProcess(workspace, prompt, sandbox, callbacks) {
292
393
  const command =
293
394
  process.env.CODEX_BIN ||
294
395
  (process.platform === "win32" ? "codex.cmd" : "codex");
295
- const args = [
296
- "exec",
297
- "--json",
298
- "--sandbox",
299
- sandbox,
300
- "-C",
301
- workspace,
302
- "-",
303
- ];
396
+ const args = ["exec", "--json", "--sandbox", sandbox, "-C", workspace, "-"];
304
397
  const child = spawn(command, args, {
305
398
  cwd: workspace,
306
399
  env: process.env,
@@ -349,15 +442,31 @@ function launchCodexProcess(workspace, prompt, sandbox, callbacks) {
349
442
  child.once("error", reject);
350
443
  child.once("close", (code) => {
351
444
  if (code === 0) resolve({ output: output.slice(-20_000), finalMessage });
352
- else
353
- reject(
354
- new Error(`Codex exited with code ${code}. ${output.slice(-1_000)}`),
355
- );
445
+ else reject(new Error(codexFailureMessage(output, code)));
356
446
  });
357
447
  });
358
448
  return { child, completed };
359
449
  }
360
450
 
451
+ function runCodexCommand(command, args, cwd) {
452
+ return new Promise((resolve, reject) => {
453
+ const child = spawn(command, args, {
454
+ cwd,
455
+ env: process.env,
456
+ shell: process.platform === "win32",
457
+ windowsHide: true,
458
+ });
459
+ let stdout = "";
460
+ let stderr = "";
461
+ child.stdout.setEncoding("utf8");
462
+ child.stderr.setEncoding("utf8");
463
+ child.stdout.on("data", (chunk) => (stdout += chunk));
464
+ child.stderr.on("data", (chunk) => (stderr += chunk));
465
+ child.once("error", reject);
466
+ child.once("close", (code) => resolve({ code: code ?? 1, stdout, stderr }));
467
+ });
468
+ }
469
+
361
470
  async function changedFilePaths(workspace) {
362
471
  const tracked = await run(
363
472
  "git",
@@ -452,7 +561,12 @@ async function workspaceFiles(root, current = root) {
452
561
 
453
562
  function isShareablePath(relative) {
454
563
  const normalized = normalizePath(relative);
455
- if (!normalized || normalized === ".." || normalized.startsWith("../") || path.isAbsolute(normalized)) {
564
+ if (
565
+ !normalized ||
566
+ normalized === ".." ||
567
+ normalized.startsWith("../") ||
568
+ path.isAbsolute(normalized)
569
+ ) {
456
570
  return false;
457
571
  }
458
572
  const parts = normalized.split("/");
@@ -482,7 +596,9 @@ function isShareablePath(relative) {
482
596
 
483
597
  function isCodeBearing(relative) {
484
598
  const name = path.posix.basename(relative).toLowerCase();
485
- return CODE_MARKERS.has(name) || CODE_EXTENSIONS.has(path.posix.extname(name));
599
+ return (
600
+ CODE_MARKERS.has(name) || CODE_EXTENSIONS.has(path.posix.extname(name))
601
+ );
486
602
  }
487
603
 
488
604
  async function worktreeBase(workspace, requested) {