@engineeros/connector 0.2.1 → 0.4.0
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 +5 -1
- package/bin/engineeros-connector.mjs +166 -2
- package/package.json +1 -1
- package/src/config.mjs +7 -0
- package/src/runner.mjs +61 -3
package/README.md
CHANGED
|
@@ -10,7 +10,11 @@ Create a connection command from **Project steering -> Connect workspace**, then
|
|
|
10
10
|
npx --yes @engineeros/connector@latest pair PAIRING-CODE --url https://your-engineeros.example --workspace . --onboard
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
The connector uploads a bounded ZIP snapshot for
|
|
13
|
+
The connector uploads a bounded ZIP snapshot for a safe file inventory, then stays online for deep assessments, rescans, and Goal Runs. Inventory never executes repository code. It excludes known secrets, dependency directories, build output, compiled binaries, files larger than 5 MB, agent-tool caches, Git metadata, and connector state before upload.
|
|
14
|
+
|
|
15
|
+
From **Project steering -> Workspace**, choose **Assess with Codex** to run a read-only engineering assessment using the Codex CLI subscription already authenticated on that computer. EngineerOS stores the source-cited findings in System State and promotes the highest-return corrective action in Steering. Assessment cannot modify the workspace.
|
|
16
|
+
|
|
17
|
+
After onboarding, every project prompt is routed to this connection. Copilot, shaping, planning, architecture, and experience generation use the Codex CLI subscription and connected workspace context. Prompt runs are read-only; only an explicitly registered Goal Run receives workspace-write access. If the connector is offline, EngineerOS asks the user to reconnect instead of silently switching models.
|
|
14
18
|
|
|
15
19
|
An empty or document-only folder establishes a greenfield baseline. A code-bearing folder is assessed as brownfield. Use **Rescan** in Steering after the local workspace changes.
|
|
16
20
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import {
|
|
5
|
+
assessmentResultUrl,
|
|
5
6
|
loadConfig,
|
|
6
7
|
resultUrl,
|
|
7
8
|
saveConfig,
|
|
@@ -10,6 +11,8 @@ import {
|
|
|
10
11
|
} from "../src/config.mjs";
|
|
11
12
|
import {
|
|
12
13
|
executeAssignment,
|
|
14
|
+
executeConnectedPrompt,
|
|
15
|
+
executeWorkspaceAssessment,
|
|
13
16
|
stopProcess,
|
|
14
17
|
workspaceSnapshot,
|
|
15
18
|
} from "../src/runner.mjs";
|
|
@@ -73,6 +76,8 @@ if (command === "pair") {
|
|
|
73
76
|
let stopped = false;
|
|
74
77
|
let active = null;
|
|
75
78
|
const available = [];
|
|
79
|
+
const assessments = [];
|
|
80
|
+
const prompts = [];
|
|
76
81
|
let socket;
|
|
77
82
|
let pingTimer;
|
|
78
83
|
let reconnectDelay = 1_000;
|
|
@@ -126,6 +131,37 @@ async function connect() {
|
|
|
126
131
|
void submitWorkspaceSnapshot();
|
|
127
132
|
return;
|
|
128
133
|
}
|
|
134
|
+
if (message.type === "workspace.assessment") {
|
|
135
|
+
if (
|
|
136
|
+
active?.runId !== message.assessment_id &&
|
|
137
|
+
!assessments.some((candidate) => candidate.assessment_id === message.assessment_id)
|
|
138
|
+
) {
|
|
139
|
+
assessments.push(message);
|
|
140
|
+
}
|
|
141
|
+
pump();
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (message.type === "prompt.execute") {
|
|
145
|
+
if (
|
|
146
|
+
active?.runId !== message.prompt_id &&
|
|
147
|
+
!prompts.some((candidate) => candidate.prompt_id === message.prompt_id)
|
|
148
|
+
) {
|
|
149
|
+
prompts.push(message);
|
|
150
|
+
}
|
|
151
|
+
pump();
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (message.type === "prompt.cancel") {
|
|
155
|
+
const queued = prompts.findIndex(
|
|
156
|
+
(candidate) => candidate.prompt_id === message.prompt_id,
|
|
157
|
+
);
|
|
158
|
+
if (queued >= 0) prompts.splice(queued, 1);
|
|
159
|
+
if (active?.kind === "prompt" && active.runId === message.prompt_id) {
|
|
160
|
+
active.cancelled = true;
|
|
161
|
+
await stopProcess(active.child);
|
|
162
|
+
}
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
129
165
|
if (message.type === "run.available") {
|
|
130
166
|
if (!available.includes(message.run_id)) available.push(message.run_id);
|
|
131
167
|
pump();
|
|
@@ -162,6 +198,10 @@ async function connect() {
|
|
|
162
198
|
});
|
|
163
199
|
socket.addEventListener("close", () => {
|
|
164
200
|
clearInterval(pingTimer);
|
|
201
|
+
if (active?.kind === "prompt") {
|
|
202
|
+
active.cancelled = true;
|
|
203
|
+
void stopProcess(active.child);
|
|
204
|
+
}
|
|
165
205
|
if (connectionRejected) {
|
|
166
206
|
stopped = true;
|
|
167
207
|
console.error(
|
|
@@ -226,7 +266,7 @@ function startPings() {
|
|
|
226
266
|
socket.send(
|
|
227
267
|
JSON.stringify({
|
|
228
268
|
type: "ping",
|
|
229
|
-
active_run_id: active?.runId
|
|
269
|
+
active_run_id: active?.kind === "goal" ? active.runId : null,
|
|
230
270
|
}),
|
|
231
271
|
);
|
|
232
272
|
}
|
|
@@ -235,9 +275,30 @@ function startPings() {
|
|
|
235
275
|
|
|
236
276
|
function pump() {
|
|
237
277
|
if (active || socket.readyState !== WebSocket.OPEN) return;
|
|
278
|
+
const prompt = prompts.shift();
|
|
279
|
+
if (prompt) {
|
|
280
|
+
active = {
|
|
281
|
+
kind: "prompt",
|
|
282
|
+
runId: prompt.prompt_id,
|
|
283
|
+
child: null,
|
|
284
|
+
cancelled: false,
|
|
285
|
+
};
|
|
286
|
+
void executePrompt(prompt);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
const assessment = assessments.shift();
|
|
290
|
+
if (assessment) {
|
|
291
|
+
active = {
|
|
292
|
+
kind: "assessment",
|
|
293
|
+
runId: assessment.assessment_id,
|
|
294
|
+
child: null,
|
|
295
|
+
};
|
|
296
|
+
void executeAssessment(assessment);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
238
299
|
const runId = available.shift();
|
|
239
300
|
if (!runId) return;
|
|
240
|
-
active = { runId, child: null, cancelled: false };
|
|
301
|
+
active = { kind: "goal", runId, child: null, cancelled: false };
|
|
241
302
|
socket.send(
|
|
242
303
|
JSON.stringify({
|
|
243
304
|
type: "run.claim",
|
|
@@ -251,6 +312,109 @@ function pump() {
|
|
|
251
312
|
);
|
|
252
313
|
}
|
|
253
314
|
|
|
315
|
+
async function executePrompt(assignment) {
|
|
316
|
+
const promptId = assignment.prompt_id;
|
|
317
|
+
console.log(`Answering ${assignment.purpose || "project"} prompt with Codex CLI.`);
|
|
318
|
+
try {
|
|
319
|
+
const result = await executeConnectedPrompt(assignment, config, {
|
|
320
|
+
onProcess: (child) => {
|
|
321
|
+
if (active?.runId === promptId) active.child = child;
|
|
322
|
+
},
|
|
323
|
+
});
|
|
324
|
+
if (active?.cancelled || socket.readyState !== WebSocket.OPEN) return;
|
|
325
|
+
socket.send(
|
|
326
|
+
JSON.stringify({
|
|
327
|
+
type: "prompt.completed",
|
|
328
|
+
prompt_id: promptId,
|
|
329
|
+
content: result.content,
|
|
330
|
+
model: result.model,
|
|
331
|
+
}),
|
|
332
|
+
);
|
|
333
|
+
} catch (error) {
|
|
334
|
+
if (!active?.cancelled && socket.readyState === WebSocket.OPEN) {
|
|
335
|
+
socket.send(
|
|
336
|
+
JSON.stringify({
|
|
337
|
+
type: "prompt.failed",
|
|
338
|
+
prompt_id: promptId,
|
|
339
|
+
message: error instanceof Error ? error.message : String(error),
|
|
340
|
+
}),
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
if (!active?.cancelled) {
|
|
344
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
345
|
+
}
|
|
346
|
+
} finally {
|
|
347
|
+
active = null;
|
|
348
|
+
pump();
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
async function executeAssessment(assignment) {
|
|
353
|
+
const assessmentId = assignment.assessment_id;
|
|
354
|
+
console.log(`Assessing workspace with Codex CLI (${assessmentId}).`);
|
|
355
|
+
let progress = 10;
|
|
356
|
+
const reportProgress = (message) => {
|
|
357
|
+
if (socket.readyState !== WebSocket.OPEN || active?.runId !== assessmentId) return;
|
|
358
|
+
socket.send(
|
|
359
|
+
JSON.stringify({
|
|
360
|
+
type: "workspace.assessment.progress",
|
|
361
|
+
assessment_id: assessmentId,
|
|
362
|
+
progress_percent: progress,
|
|
363
|
+
message: String(message).slice(0, 500),
|
|
364
|
+
}),
|
|
365
|
+
);
|
|
366
|
+
};
|
|
367
|
+
reportProgress("Codex is inspecting the workspace");
|
|
368
|
+
const heartbeat = setInterval(() => {
|
|
369
|
+
progress = Math.min(90, progress + 5);
|
|
370
|
+
reportProgress("Codex is building the evidence-backed assessment");
|
|
371
|
+
}, 15_000);
|
|
372
|
+
try {
|
|
373
|
+
const result = await executeWorkspaceAssessment(assignment, config, {
|
|
374
|
+
onProcess: (child) => {
|
|
375
|
+
if (active?.runId === assessmentId) active.child = child;
|
|
376
|
+
},
|
|
377
|
+
onEvent: (event) => {
|
|
378
|
+
const message =
|
|
379
|
+
event.message || event.item?.text || event.type || "Codex is assessing the workspace";
|
|
380
|
+
reportProgress(message);
|
|
381
|
+
},
|
|
382
|
+
});
|
|
383
|
+
const response = await fetch(
|
|
384
|
+
assessmentResultUrl(config.server_url, config.connector_id, assessmentId),
|
|
385
|
+
{
|
|
386
|
+
method: "POST",
|
|
387
|
+
headers: {
|
|
388
|
+
"Content-Type": "application/json",
|
|
389
|
+
Authorization: `Bearer ${config.token}`,
|
|
390
|
+
},
|
|
391
|
+
body: JSON.stringify(result),
|
|
392
|
+
},
|
|
393
|
+
);
|
|
394
|
+
if (!response.ok) {
|
|
395
|
+
throw new Error(
|
|
396
|
+
`EngineerOS rejected the assessment (${response.status}): ${await response.text()}`,
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
console.log("Codex workspace assessment is current in EngineerOS.");
|
|
400
|
+
} catch (error) {
|
|
401
|
+
if (socket.readyState === WebSocket.OPEN) {
|
|
402
|
+
socket.send(
|
|
403
|
+
JSON.stringify({
|
|
404
|
+
type: "workspace.assessment.failed",
|
|
405
|
+
assessment_id: assessmentId,
|
|
406
|
+
message: error instanceof Error ? error.message : String(error),
|
|
407
|
+
}),
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
411
|
+
} finally {
|
|
412
|
+
clearInterval(heartbeat);
|
|
413
|
+
active = null;
|
|
414
|
+
pump();
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
254
418
|
async function execute(assignment) {
|
|
255
419
|
const runId = assignment.run_id;
|
|
256
420
|
console.log(`Running Goal ${runId} with Codex CLI.`);
|
package/package.json
CHANGED
package/src/config.mjs
CHANGED
|
@@ -38,6 +38,13 @@ export function workspaceUrl(websocketUrl, connectorId) {
|
|
|
38
38
|
return url.toString();
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
export function assessmentResultUrl(websocketUrl, connectorId, assessmentId) {
|
|
42
|
+
const url = new URL(websocketUrl);
|
|
43
|
+
url.protocol = url.protocol === "wss:" ? "https:" : "http:";
|
|
44
|
+
url.pathname = `/api/v1/agent-connectors/${connectorId}/assessment/${assessmentId}/result`;
|
|
45
|
+
return url.toString();
|
|
46
|
+
}
|
|
47
|
+
|
|
41
48
|
export async function loadConfig(workspace = process.cwd()) {
|
|
42
49
|
try {
|
|
43
50
|
return JSON.parse(await readFile(configPath(workspace), "utf8"));
|
package/src/runner.mjs
CHANGED
|
@@ -112,6 +112,52 @@ export async function executeAssignment(assignment, config, callbacks) {
|
|
|
112
112
|
};
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
export async function executeWorkspaceAssessment(assignment, config, callbacks) {
|
|
116
|
+
const revision = await run(
|
|
117
|
+
"git",
|
|
118
|
+
["rev-parse", "HEAD"],
|
|
119
|
+
config.workspace,
|
|
120
|
+
{ allowFailure: true },
|
|
121
|
+
);
|
|
122
|
+
const instruction = `${assignment.prompt_markdown}\n\n## EngineerOS execution instruction\n\nAssess the current workspace at the requested inventory revision ${assignment.source_revision}. Remain read-only. Return only the required structured Markdown assessment as your final answer.\n`;
|
|
123
|
+
const controller = launchCodexProcess(
|
|
124
|
+
config.workspace,
|
|
125
|
+
instruction,
|
|
126
|
+
"read-only",
|
|
127
|
+
callbacks,
|
|
128
|
+
);
|
|
129
|
+
callbacks.onProcess?.(controller.child);
|
|
130
|
+
const completed = await controller.completed;
|
|
131
|
+
const report = completed.finalMessage.trim();
|
|
132
|
+
if (!report.startsWith("# Workspace Assessment")) {
|
|
133
|
+
throw new Error(
|
|
134
|
+
"Codex completed without returning the required structured Workspace Assessment.",
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
report_markdown: report,
|
|
139
|
+
observed_head_revision:
|
|
140
|
+
revision.code === 0 ? revision.stdout.trim().slice(0, 128) : null,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export async function executeConnectedPrompt(assignment, config, callbacks) {
|
|
145
|
+
const instruction = `${assignment.prompt_markdown}\n\n## EngineerOS connector instruction\n\nAnswer this project prompt from the connected workspace. Remain read-only and return only the requested response as your final answer.\n`;
|
|
146
|
+
const controller = launchCodexProcess(
|
|
147
|
+
config.workspace,
|
|
148
|
+
instruction,
|
|
149
|
+
"read-only",
|
|
150
|
+
callbacks,
|
|
151
|
+
);
|
|
152
|
+
callbacks.onProcess?.(controller.child);
|
|
153
|
+
const completed = await controller.completed;
|
|
154
|
+
const content = completed.finalMessage.trim();
|
|
155
|
+
if (!content) {
|
|
156
|
+
throw new Error("Codex completed without returning a response.");
|
|
157
|
+
}
|
|
158
|
+
return { content, model: "codex-cli" };
|
|
159
|
+
}
|
|
160
|
+
|
|
115
161
|
export async function stopProcess(child) {
|
|
116
162
|
if (!child || child.exitCode !== null) return;
|
|
117
163
|
if (process.platform === "win32") {
|
|
@@ -233,6 +279,11 @@ async function prepareRunWorkspace(workspace, runId, baseRevision) {
|
|
|
233
279
|
}
|
|
234
280
|
|
|
235
281
|
function launchCodex(workspace, packet, callbacks) {
|
|
282
|
+
const prompt = `${packet}\n\n## EngineerOS execution instruction\n\nImplement this frozen Goal completely in the current run workspace. Run the required verification. Do not commit, push, or modify files outside this workspace. End with a concise result and verification summary.\n`;
|
|
283
|
+
return launchCodexProcess(workspace, prompt, "workspace-write", callbacks);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function launchCodexProcess(workspace, prompt, sandbox, callbacks) {
|
|
236
287
|
const command =
|
|
237
288
|
process.env.CODEX_BIN ||
|
|
238
289
|
(process.platform === "win32" ? "codex.cmd" : "codex");
|
|
@@ -240,7 +291,7 @@ function launchCodex(workspace, packet, callbacks) {
|
|
|
240
291
|
"exec",
|
|
241
292
|
"--json",
|
|
242
293
|
"--sandbox",
|
|
243
|
-
|
|
294
|
+
sandbox,
|
|
244
295
|
"-C",
|
|
245
296
|
workspace,
|
|
246
297
|
"-",
|
|
@@ -251,10 +302,10 @@ function launchCodex(workspace, packet, callbacks) {
|
|
|
251
302
|
shell: process.platform === "win32",
|
|
252
303
|
stdio: ["pipe", "pipe", "pipe"],
|
|
253
304
|
});
|
|
254
|
-
const prompt = `${packet}\n\n## EngineerOS execution instruction\n\nImplement this frozen Goal completely in the current run workspace. Run the required verification. Do not commit, push, or modify files outside this workspace. End with a concise result and verification summary.\n`;
|
|
255
305
|
child.stdin.end(prompt);
|
|
256
306
|
let output = "";
|
|
257
307
|
let buffer = "";
|
|
308
|
+
let finalMessage = "";
|
|
258
309
|
child.stdout.setEncoding("utf8");
|
|
259
310
|
child.stdout.on("data", (chunk) => {
|
|
260
311
|
output += chunk;
|
|
@@ -265,6 +316,13 @@ function launchCodex(workspace, packet, callbacks) {
|
|
|
265
316
|
if (!line.trim()) continue;
|
|
266
317
|
try {
|
|
267
318
|
const event = JSON.parse(line);
|
|
319
|
+
if (
|
|
320
|
+
event.type === "item.completed" &&
|
|
321
|
+
event.item?.type === "agent_message" &&
|
|
322
|
+
typeof event.item.text === "string"
|
|
323
|
+
) {
|
|
324
|
+
finalMessage = event.item.text;
|
|
325
|
+
}
|
|
268
326
|
callbacks.onEvent?.(event);
|
|
269
327
|
} catch {
|
|
270
328
|
callbacks.onEvent?.({
|
|
@@ -285,7 +343,7 @@ function launchCodex(workspace, packet, callbacks) {
|
|
|
285
343
|
const completed = new Promise((resolve, reject) => {
|
|
286
344
|
child.once("error", reject);
|
|
287
345
|
child.once("close", (code) => {
|
|
288
|
-
if (code === 0) resolve(output.slice(-20_000));
|
|
346
|
+
if (code === 0) resolve({ output: output.slice(-20_000), finalMessage });
|
|
289
347
|
else
|
|
290
348
|
reject(
|
|
291
349
|
new Error(`Codex exited with code ${code}. ${output.slice(-1_000)}`),
|