@engineeros/connector 0.2.0 → 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 +102 -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
|
@@ -8,9 +8,15 @@ import { deflateRaw } from "node:zlib";
|
|
|
8
8
|
|
|
9
9
|
const deflate = promisify(deflateRaw);
|
|
10
10
|
const MAX_ARCHIVE_BYTES = 25 * 1024 * 1024;
|
|
11
|
+
const MAX_SHAREABLE_FILE_BYTES = 5 * 1024 * 1024;
|
|
11
12
|
const MAX_WORKSPACE_FILES = 5_000;
|
|
12
13
|
const EXCLUDED_DIRECTORIES = new Set([
|
|
14
|
+
".agents",
|
|
15
|
+
".claude",
|
|
16
|
+
".codex",
|
|
13
17
|
".engineeros",
|
|
18
|
+
".forge",
|
|
19
|
+
".gemini",
|
|
14
20
|
".git",
|
|
15
21
|
".next",
|
|
16
22
|
".pytest_cache",
|
|
@@ -24,6 +30,29 @@ const EXCLUDED_DIRECTORIES = new Set([
|
|
|
24
30
|
"target",
|
|
25
31
|
"vendor",
|
|
26
32
|
]);
|
|
33
|
+
const EXCLUDED_PATH_PREFIXES = [".github/skills/"];
|
|
34
|
+
const EXCLUDED_FILE_EXTENSIONS = new Set([
|
|
35
|
+
".7z",
|
|
36
|
+
".a",
|
|
37
|
+
".bin",
|
|
38
|
+
".class",
|
|
39
|
+
".dll",
|
|
40
|
+
".dylib",
|
|
41
|
+
".exe",
|
|
42
|
+
".gz",
|
|
43
|
+
".jar",
|
|
44
|
+
".lib",
|
|
45
|
+
".o",
|
|
46
|
+
".obj",
|
|
47
|
+
".pyc",
|
|
48
|
+
".pyo",
|
|
49
|
+
".rar",
|
|
50
|
+
".so",
|
|
51
|
+
".tar",
|
|
52
|
+
".tgz",
|
|
53
|
+
".war",
|
|
54
|
+
".zip",
|
|
55
|
+
]);
|
|
27
56
|
const CODE_EXTENSIONS = new Set([
|
|
28
57
|
".c",
|
|
29
58
|
".cpp",
|
|
@@ -83,6 +112,52 @@ export async function executeAssignment(assignment, config, callbacks) {
|
|
|
83
112
|
};
|
|
84
113
|
}
|
|
85
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
|
+
|
|
86
161
|
export async function stopProcess(child) {
|
|
87
162
|
if (!child || child.exitCode !== null) return;
|
|
88
163
|
if (process.platform === "win32") {
|
|
@@ -123,6 +198,10 @@ export async function workspaceSnapshot(workspace) {
|
|
|
123
198
|
excludedFileCount += 1;
|
|
124
199
|
continue;
|
|
125
200
|
}
|
|
201
|
+
if (details.size > MAX_SHAREABLE_FILE_BYTES) {
|
|
202
|
+
excludedFileCount += 1;
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
126
205
|
totalBytes += details.size;
|
|
127
206
|
if (totalBytes > MAX_ARCHIVE_BYTES) {
|
|
128
207
|
throw new Error(
|
|
@@ -200,6 +279,11 @@ async function prepareRunWorkspace(workspace, runId, baseRevision) {
|
|
|
200
279
|
}
|
|
201
280
|
|
|
202
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) {
|
|
203
287
|
const command =
|
|
204
288
|
process.env.CODEX_BIN ||
|
|
205
289
|
(process.platform === "win32" ? "codex.cmd" : "codex");
|
|
@@ -207,7 +291,7 @@ function launchCodex(workspace, packet, callbacks) {
|
|
|
207
291
|
"exec",
|
|
208
292
|
"--json",
|
|
209
293
|
"--sandbox",
|
|
210
|
-
|
|
294
|
+
sandbox,
|
|
211
295
|
"-C",
|
|
212
296
|
workspace,
|
|
213
297
|
"-",
|
|
@@ -218,10 +302,10 @@ function launchCodex(workspace, packet, callbacks) {
|
|
|
218
302
|
shell: process.platform === "win32",
|
|
219
303
|
stdio: ["pipe", "pipe", "pipe"],
|
|
220
304
|
});
|
|
221
|
-
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`;
|
|
222
305
|
child.stdin.end(prompt);
|
|
223
306
|
let output = "";
|
|
224
307
|
let buffer = "";
|
|
308
|
+
let finalMessage = "";
|
|
225
309
|
child.stdout.setEncoding("utf8");
|
|
226
310
|
child.stdout.on("data", (chunk) => {
|
|
227
311
|
output += chunk;
|
|
@@ -232,6 +316,13 @@ function launchCodex(workspace, packet, callbacks) {
|
|
|
232
316
|
if (!line.trim()) continue;
|
|
233
317
|
try {
|
|
234
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
|
+
}
|
|
235
326
|
callbacks.onEvent?.(event);
|
|
236
327
|
} catch {
|
|
237
328
|
callbacks.onEvent?.({
|
|
@@ -252,7 +343,7 @@ function launchCodex(workspace, packet, callbacks) {
|
|
|
252
343
|
const completed = new Promise((resolve, reject) => {
|
|
253
344
|
child.once("error", reject);
|
|
254
345
|
child.once("close", (code) => {
|
|
255
|
-
if (code === 0) resolve(output.slice(-20_000));
|
|
346
|
+
if (code === 0) resolve({ output: output.slice(-20_000), finalMessage });
|
|
256
347
|
else
|
|
257
348
|
reject(
|
|
258
349
|
new Error(`Codex exited with code ${code}. ${output.slice(-1_000)}`),
|
|
@@ -363,7 +454,15 @@ function isShareablePath(relative) {
|
|
|
363
454
|
if (parts.some((part) => EXCLUDED_DIRECTORIES.has(part.toLowerCase()))) {
|
|
364
455
|
return false;
|
|
365
456
|
}
|
|
457
|
+
if (
|
|
458
|
+
EXCLUDED_PATH_PREFIXES.some((prefix) =>
|
|
459
|
+
normalized.toLowerCase().startsWith(prefix),
|
|
460
|
+
)
|
|
461
|
+
) {
|
|
462
|
+
return false;
|
|
463
|
+
}
|
|
366
464
|
const name = parts.at(-1)?.toLowerCase() ?? "";
|
|
465
|
+
if (EXCLUDED_FILE_EXTENSIONS.has(path.posix.extname(name))) return false;
|
|
367
466
|
if (name === ".env.example" || name === ".env.sample") return true;
|
|
368
467
|
if (
|
|
369
468
|
name === ".env" ||
|