@engineeros/connector 0.16.1 → 0.17.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 +4 -13
- package/bin/engineeros-connector.mjs +132 -1133
- package/package.json +24 -41
- package/src/acp-client.mjs +0 -468
- package/src/agent-harness.mjs +0 -264
- package/src/agent-registry.mjs +0 -643
- package/src/assessment-spool.mjs +0 -458
- package/src/capabilities.mjs +0 -24
- package/src/cli-args.mjs +0 -18
- package/src/codex-app-server.mjs +0 -250
- package/src/config.mjs +0 -109
- package/src/connection.mjs +0 -80
- package/src/mcp-server.mjs +0 -256
- package/src/runner.mjs +0 -2159
- package/src/skills/change-planning/SKILL.md +0 -12
- package/src/skills/change-verification/SKILL.md +0 -12
- package/src/skills/codebase-research/SKILL.md +0 -12
- package/src/skills/goal-execution/SKILL.md +0 -12
package/src/codex-app-server.mjs
DELETED
|
@@ -1,250 +0,0 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
|
-
import readline from "node:readline";
|
|
3
|
-
import { normalizeTokenUsage } from "./acp-client.mjs";
|
|
4
|
-
|
|
5
|
-
export async function inspectCodexExecutionProfiles({
|
|
6
|
-
workspace,
|
|
7
|
-
command = process.env.CODEX_BIN || (process.platform === "win32" ? "codex.cmd" : "codex"),
|
|
8
|
-
spawnProcess = spawn,
|
|
9
|
-
timeoutMs = 15_000,
|
|
10
|
-
}) {
|
|
11
|
-
const child = spawnProcess(command, ["app-server", "--stdio"], {
|
|
12
|
-
cwd: workspace,
|
|
13
|
-
env: process.env,
|
|
14
|
-
shell: process.platform === "win32",
|
|
15
|
-
windowsHide: true,
|
|
16
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
17
|
-
});
|
|
18
|
-
const pending = new Map();
|
|
19
|
-
let requestId = 0;
|
|
20
|
-
let stderr = "";
|
|
21
|
-
const request = (method, params) => new Promise((resolve, reject) => {
|
|
22
|
-
const id = ++requestId;
|
|
23
|
-
pending.set(id, { resolve, reject });
|
|
24
|
-
child.stdin.write(`${JSON.stringify({ id, method, params })}\n`);
|
|
25
|
-
});
|
|
26
|
-
const lines = readline.createInterface({ input: child.stdout });
|
|
27
|
-
lines.on("line", (line) => {
|
|
28
|
-
try {
|
|
29
|
-
const message = JSON.parse(line);
|
|
30
|
-
const waiter = pending.get(message.id);
|
|
31
|
-
if (!waiter) return;
|
|
32
|
-
pending.delete(message.id);
|
|
33
|
-
if (message.error) waiter.reject(new Error(message.error.message || "Codex model discovery failed."));
|
|
34
|
-
else waiter.resolve(message.result);
|
|
35
|
-
} catch {
|
|
36
|
-
// Ignore non-protocol output.
|
|
37
|
-
}
|
|
38
|
-
});
|
|
39
|
-
child.stderr.setEncoding("utf8");
|
|
40
|
-
child.stderr.on("data", (chunk) => { stderr = `${stderr}${chunk}`.slice(-4_000); });
|
|
41
|
-
const rejectPending = (message) => {
|
|
42
|
-
for (const waiter of pending.values()) waiter.reject(new Error(message));
|
|
43
|
-
pending.clear();
|
|
44
|
-
};
|
|
45
|
-
child.once("error", (error) => rejectPending(error.message));
|
|
46
|
-
child.once("close", (code) => {
|
|
47
|
-
if (pending.size) rejectPending(`Codex model discovery stopped with code ${code ?? 1}. ${stderr}`.trim());
|
|
48
|
-
});
|
|
49
|
-
const timeout = setTimeout(() => {
|
|
50
|
-
rejectPending("Codex model discovery timed out.");
|
|
51
|
-
child.kill();
|
|
52
|
-
}, timeoutMs);
|
|
53
|
-
try {
|
|
54
|
-
await request("initialize", {
|
|
55
|
-
clientInfo: { name: "engineeros-connector", title: "EngineerOS Connector", version: "0.11.0" },
|
|
56
|
-
});
|
|
57
|
-
child.stdin.write(`${JSON.stringify({ method: "initialized" })}\n`);
|
|
58
|
-
const models = [];
|
|
59
|
-
let cursor = null;
|
|
60
|
-
do {
|
|
61
|
-
const result = await request("model/list", { cursor, includeHidden: false });
|
|
62
|
-
models.push(...(Array.isArray(result?.data) ? result.data : []));
|
|
63
|
-
cursor = result?.nextCursor || null;
|
|
64
|
-
} while (cursor);
|
|
65
|
-
return models.map((item) => ({
|
|
66
|
-
id: item.model || item.id,
|
|
67
|
-
name: item.displayName || item.model || item.id,
|
|
68
|
-
description: item.description || "",
|
|
69
|
-
is_default: item.isDefault === true,
|
|
70
|
-
default_reasoning_effort: item.defaultReasoningEffort || null,
|
|
71
|
-
reasoning_efforts: (item.supportedReasoningEfforts || [])
|
|
72
|
-
.map((option) => option.reasoningEffort)
|
|
73
|
-
.filter(Boolean),
|
|
74
|
-
})).filter((item) => item.id);
|
|
75
|
-
} catch (error) {
|
|
76
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
77
|
-
throw new Error(`${detail}${stderr ? ` ${stderr}` : ""}`.trim());
|
|
78
|
-
} finally {
|
|
79
|
-
clearTimeout(timeout);
|
|
80
|
-
child.kill();
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export function launchCodexAppServer({
|
|
85
|
-
workspace,
|
|
86
|
-
prompt,
|
|
87
|
-
sandbox,
|
|
88
|
-
profile = {},
|
|
89
|
-
previousSessionId,
|
|
90
|
-
callbacks = {},
|
|
91
|
-
command = process.env.CODEX_BIN || (process.platform === "win32" ? "codex.cmd" : "codex"),
|
|
92
|
-
spawnProcess = spawn,
|
|
93
|
-
}) {
|
|
94
|
-
const child = spawnProcess(command, ["app-server", "--stdio"], {
|
|
95
|
-
cwd: workspace,
|
|
96
|
-
env: process.env,
|
|
97
|
-
shell: process.platform === "win32",
|
|
98
|
-
windowsHide: true,
|
|
99
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
100
|
-
});
|
|
101
|
-
const pending = new Map();
|
|
102
|
-
let requestId = 0;
|
|
103
|
-
let threadId = previousSessionId || "";
|
|
104
|
-
let turnId = "";
|
|
105
|
-
let finalMessage = "";
|
|
106
|
-
let stderr = "";
|
|
107
|
-
let usage = null;
|
|
108
|
-
let settled = false;
|
|
109
|
-
let completionTimer = null;
|
|
110
|
-
|
|
111
|
-
const request = (method, params) =>
|
|
112
|
-
new Promise((resolve, reject) => {
|
|
113
|
-
const id = ++requestId;
|
|
114
|
-
pending.set(id, { resolve, reject });
|
|
115
|
-
child.stdin.write(`${JSON.stringify({ id, method, params })}\n`);
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
const completed = new Promise((resolve, reject) => {
|
|
119
|
-
const finish = (error) => {
|
|
120
|
-
if (settled) return;
|
|
121
|
-
settled = true;
|
|
122
|
-
if (completionTimer) clearTimeout(completionTimer);
|
|
123
|
-
for (const waiter of pending.values()) waiter.reject(error || new Error("Codex app-server stopped."));
|
|
124
|
-
pending.clear();
|
|
125
|
-
if (error) reject(error);
|
|
126
|
-
else resolve({
|
|
127
|
-
finalMessage,
|
|
128
|
-
output: stderr.slice(-20_000),
|
|
129
|
-
model: profile.model || "codex-app-server",
|
|
130
|
-
sessionId: threadId,
|
|
131
|
-
usage,
|
|
132
|
-
});
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
child.once("error", finish);
|
|
136
|
-
child.once("close", (code) => {
|
|
137
|
-
if (!settled) finish(new Error(`Codex app-server exited before completing the turn (code ${code ?? 1}). ${stderr}`));
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
const lines = readline.createInterface({ input: child.stdout });
|
|
141
|
-
lines.on("line", (line) => {
|
|
142
|
-
let message;
|
|
143
|
-
try {
|
|
144
|
-
message = JSON.parse(line);
|
|
145
|
-
} catch {
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
if (message.id !== undefined) {
|
|
149
|
-
const waiter = pending.get(message.id);
|
|
150
|
-
if (!waiter) return;
|
|
151
|
-
pending.delete(message.id);
|
|
152
|
-
if (message.error) waiter.reject(new Error(message.error.message || "Codex app-server request failed."));
|
|
153
|
-
else waiter.resolve(message.result);
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
const params = message.params || {};
|
|
157
|
-
if (message.method === "item/agentMessage/delta" && typeof params.delta === "string") {
|
|
158
|
-
finalMessage += params.delta;
|
|
159
|
-
callbacks.onEvent?.({ type: "codex.agent_message_delta", delta: params.delta });
|
|
160
|
-
} else if (message.method === "item/started" || message.method === "item/completed") {
|
|
161
|
-
callbacks.onEvent?.({ type: `codex.${message.method}`, item: params.item });
|
|
162
|
-
} else if (message.method === "thread/tokenUsage/updated") {
|
|
163
|
-
const tokenUsage = params.tokenUsage || params.usage || params;
|
|
164
|
-
usage = normalizeTokenUsage(
|
|
165
|
-
previousSessionId
|
|
166
|
-
? tokenUsage.last || params.last || tokenUsage
|
|
167
|
-
: tokenUsage.total || params.total || tokenUsage.last || params.last || tokenUsage,
|
|
168
|
-
);
|
|
169
|
-
callbacks.onEvent?.({ type: "codex.usage", update: params });
|
|
170
|
-
if (usage && completionTimer) {
|
|
171
|
-
finish();
|
|
172
|
-
child.kill();
|
|
173
|
-
}
|
|
174
|
-
} else if (message.method === "turn/completed" && params.threadId === threadId) {
|
|
175
|
-
const status = params.turn?.status;
|
|
176
|
-
if (status === "failed") {
|
|
177
|
-
finish(new Error(params.turn?.error?.message || "Codex turn failed."));
|
|
178
|
-
child.kill();
|
|
179
|
-
} else if (!finalMessage.trim()) {
|
|
180
|
-
finish(new Error("Codex completed without returning a response."));
|
|
181
|
-
child.kill();
|
|
182
|
-
} else if (usage) {
|
|
183
|
-
finish();
|
|
184
|
-
child.kill();
|
|
185
|
-
} else if (!completionTimer) {
|
|
186
|
-
completionTimer = setTimeout(() => {
|
|
187
|
-
finish();
|
|
188
|
-
child.kill();
|
|
189
|
-
}, 250);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
child.stderr.setEncoding("utf8");
|
|
195
|
-
child.stderr.on("data", (chunk) => {
|
|
196
|
-
stderr = `${stderr}${chunk}`.slice(-20_000);
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
void (async () => {
|
|
200
|
-
try {
|
|
201
|
-
await request("initialize", {
|
|
202
|
-
clientInfo: { name: "engineeros-connector", title: "EngineerOS Connector", version: "0.11.0" },
|
|
203
|
-
});
|
|
204
|
-
child.stdin.write(`${JSON.stringify({ method: "initialized" })}\n`);
|
|
205
|
-
const threadResult = previousSessionId
|
|
206
|
-
? await request("thread/resume", {
|
|
207
|
-
threadId: previousSessionId,
|
|
208
|
-
cwd: workspace,
|
|
209
|
-
sandbox,
|
|
210
|
-
approvalPolicy: "never",
|
|
211
|
-
model: profile.model || null,
|
|
212
|
-
})
|
|
213
|
-
: await request("thread/start", {
|
|
214
|
-
cwd: workspace,
|
|
215
|
-
sandbox,
|
|
216
|
-
approvalPolicy: "never",
|
|
217
|
-
model: profile.model || null,
|
|
218
|
-
ephemeral: false,
|
|
219
|
-
});
|
|
220
|
-
threadId = threadResult.thread.id;
|
|
221
|
-
await callbacks.onSession?.(threadId);
|
|
222
|
-
callbacks.onEvent?.({ type: "thread.started", thread_id: threadId });
|
|
223
|
-
const turnResult = await request("turn/start", {
|
|
224
|
-
threadId,
|
|
225
|
-
input: [{ type: "text", text: prompt }],
|
|
226
|
-
effort: profile.reasoning_effort || null,
|
|
227
|
-
});
|
|
228
|
-
turnId = turnResult.turn.id;
|
|
229
|
-
} catch (error) {
|
|
230
|
-
child.kill();
|
|
231
|
-
finish(error instanceof Error ? error : new Error(String(error)));
|
|
232
|
-
}
|
|
233
|
-
})();
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
return {
|
|
237
|
-
child,
|
|
238
|
-
completed,
|
|
239
|
-
cancel: async () => {
|
|
240
|
-
if (threadId && turnId && !settled) {
|
|
241
|
-
try {
|
|
242
|
-
await request("turn/interrupt", { threadId, turnId });
|
|
243
|
-
} catch {
|
|
244
|
-
// Process termination below is the final cancellation boundary.
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
child.kill();
|
|
248
|
-
},
|
|
249
|
-
};
|
|
250
|
-
}
|
package/src/config.mjs
DELETED
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
-
import os from "node:os";
|
|
4
|
-
import path from "node:path";
|
|
5
|
-
|
|
6
|
-
export function configPath(workspace = process.cwd()) {
|
|
7
|
-
const key = createHash("sha256")
|
|
8
|
-
.update(path.resolve(workspace))
|
|
9
|
-
.digest("hex")
|
|
10
|
-
.slice(0, 24);
|
|
11
|
-
return path.join(os.homedir(), ".engineeros", "connectors", `${key}.json`);
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function socketUrl(value) {
|
|
15
|
-
const url = new URL(value);
|
|
16
|
-
if (url.protocol === "http:") url.protocol = "ws:";
|
|
17
|
-
if (url.protocol === "https:") url.protocol = "wss:";
|
|
18
|
-
if (!["ws:", "wss:"].includes(url.protocol)) {
|
|
19
|
-
throw new Error("EngineerOS URL must use http, https, ws, or wss.");
|
|
20
|
-
}
|
|
21
|
-
url.pathname = "/api/v1/agent-connectors/ws";
|
|
22
|
-
url.search = "";
|
|
23
|
-
url.hash = "";
|
|
24
|
-
return url.toString();
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function connectorResumeCredentials(config, serverUrl) {
|
|
28
|
-
if (
|
|
29
|
-
!config?.connector_id ||
|
|
30
|
-
!config.token ||
|
|
31
|
-
config.server_url !== serverUrl
|
|
32
|
-
) {
|
|
33
|
-
return {};
|
|
34
|
-
}
|
|
35
|
-
return {
|
|
36
|
-
existing_connector_id: config.connector_id,
|
|
37
|
-
existing_token: config.token,
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export function mergePairedConfig(
|
|
42
|
-
requestedConfig,
|
|
43
|
-
existingConfig,
|
|
44
|
-
connectorId,
|
|
45
|
-
token,
|
|
46
|
-
) {
|
|
47
|
-
const preserved =
|
|
48
|
-
existingConfig?.connector_id === connectorId ? existingConfig : {};
|
|
49
|
-
const sameAgent =
|
|
50
|
-
preserved.agent_protocol === requestedConfig.agent_protocol &&
|
|
51
|
-
(preserved.agent_id || preserved.agent_command || null) ===
|
|
52
|
-
(requestedConfig.agent_id || requestedConfig.agent_command || null) &&
|
|
53
|
-
(preserved.agent_version || null) ===
|
|
54
|
-
(requestedConfig.agent_version || null);
|
|
55
|
-
return {
|
|
56
|
-
...preserved,
|
|
57
|
-
...requestedConfig,
|
|
58
|
-
connector_id: connectorId,
|
|
59
|
-
token,
|
|
60
|
-
sessions: sameAgent ? preserved.sessions || {} : {},
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export function resultUrl(websocketUrl, connectorId, runId) {
|
|
65
|
-
const url = new URL(websocketUrl);
|
|
66
|
-
url.protocol = url.protocol === "wss:" ? "https:" : "http:";
|
|
67
|
-
url.pathname = `/api/v1/agent-connectors/${connectorId}/runs/${runId}/result`;
|
|
68
|
-
return url.toString();
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export function workspaceUrl(websocketUrl, connectorId) {
|
|
72
|
-
const url = new URL(websocketUrl);
|
|
73
|
-
url.protocol = url.protocol === "wss:" ? "https:" : "http:";
|
|
74
|
-
url.pathname = `/api/v1/agent-connectors/${connectorId}/workspace`;
|
|
75
|
-
return url.toString();
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export function assessmentResultUrl(websocketUrl, connectorId, assessmentId) {
|
|
79
|
-
const url = new URL(websocketUrl);
|
|
80
|
-
url.protocol = url.protocol === "wss:" ? "https:" : "http:";
|
|
81
|
-
url.pathname = `/api/v1/agent-connectors/${connectorId}/assessment/${assessmentId}/result`;
|
|
82
|
-
return url.toString();
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export function artifactToolUrl(websocketUrl, connectorId) {
|
|
86
|
-
const url = new URL(websocketUrl);
|
|
87
|
-
url.protocol = url.protocol === "wss:" ? "https:" : "http:";
|
|
88
|
-
url.pathname = `/api/v1/agent-connectors/${connectorId}/artifact-tools`;
|
|
89
|
-
return url.toString();
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
export async function loadConfig(workspace = process.cwd()) {
|
|
93
|
-
try {
|
|
94
|
-
return JSON.parse(await readFile(configPath(workspace), "utf8"));
|
|
95
|
-
} catch (error) {
|
|
96
|
-
if (error?.code === "ENOENT") return null;
|
|
97
|
-
throw error;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export async function saveConfig(config) {
|
|
102
|
-
const target = configPath(config.workspace);
|
|
103
|
-
await mkdir(path.dirname(target), { recursive: true });
|
|
104
|
-
await writeFile(target, `${JSON.stringify(config, null, 2)}\n`, {
|
|
105
|
-
encoding: "utf8",
|
|
106
|
-
mode: 0o600,
|
|
107
|
-
});
|
|
108
|
-
if (process.platform !== "win32") await chmod(target, 0o600);
|
|
109
|
-
}
|
package/src/connection.mjs
DELETED
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
const PROTOCOL_FAILURE_MESSAGE_MAX_LENGTH = 2_000;
|
|
2
|
-
const TRUNCATED_FAILURE_SUFFIX = "\n… [truncated by EngineerOS connector]";
|
|
3
|
-
|
|
4
|
-
export function describeWebSocketError(event) {
|
|
5
|
-
return (
|
|
6
|
-
event?.error?.message ||
|
|
7
|
-
event?.message ||
|
|
8
|
-
"The WebSocket connection failed without providing an error detail."
|
|
9
|
-
);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function sendConnectionMessage(activeSocket, connection, message) {
|
|
13
|
-
if (activeSocket !== connection || connection.readyState !== 1) return false;
|
|
14
|
-
connection.send(JSON.stringify(message));
|
|
15
|
-
return true;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function protocolFailureMessage(error) {
|
|
19
|
-
const raw = error instanceof Error ? error.message : String(error ?? "");
|
|
20
|
-
const message =
|
|
21
|
-
raw.trim() || "The connector failed without providing an error detail.";
|
|
22
|
-
const characters = [...message];
|
|
23
|
-
if (characters.length <= PROTOCOL_FAILURE_MESSAGE_MAX_LENGTH) return message;
|
|
24
|
-
const suffix = [...TRUNCATED_FAILURE_SUFFIX];
|
|
25
|
-
return `${characters
|
|
26
|
-
.slice(0, PROTOCOL_FAILURE_MESSAGE_MAX_LENGTH - suffix.length)
|
|
27
|
-
.join("")}${TRUNCATED_FAILURE_SUFFIX}`;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export async function describeRejectedResponse(response, subject) {
|
|
31
|
-
const responseBody = await response.text();
|
|
32
|
-
const detail = responseDetail(responseBody);
|
|
33
|
-
return protocolFailureMessage(
|
|
34
|
-
`EngineerOS rejected ${subject} (${response.status})${detail ? `: ${detail}` : "."}`,
|
|
35
|
-
);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function startConnectionWatchdog(
|
|
39
|
-
socket,
|
|
40
|
-
url,
|
|
41
|
-
{ timeoutMs = 15_000, onTimeout = console.error } = {},
|
|
42
|
-
) {
|
|
43
|
-
const timer = setTimeout(() => {
|
|
44
|
-
onTimeout(
|
|
45
|
-
`EngineerOS did not complete the WebSocket handshake at ${url} within ${Math.round(timeoutMs / 1_000)} seconds. Check that the backend is running and the URL is reachable from this machine.`,
|
|
46
|
-
);
|
|
47
|
-
try {
|
|
48
|
-
socket.close();
|
|
49
|
-
} catch {
|
|
50
|
-
// Reconnect scheduling is owned by the caller.
|
|
51
|
-
}
|
|
52
|
-
}, timeoutMs);
|
|
53
|
-
|
|
54
|
-
return () => clearTimeout(timer);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function responseDetail(responseBody) {
|
|
58
|
-
const body = String(responseBody || "").trim();
|
|
59
|
-
if (!body) return "";
|
|
60
|
-
try {
|
|
61
|
-
const parsed = JSON.parse(body);
|
|
62
|
-
if (typeof parsed?.detail === "string") return parsed.detail;
|
|
63
|
-
if (Array.isArray(parsed?.detail)) {
|
|
64
|
-
return parsed.detail.map(validationIssue).filter(Boolean).join("; ");
|
|
65
|
-
}
|
|
66
|
-
} catch {
|
|
67
|
-
// Plain-text server errors are already useful to the operator.
|
|
68
|
-
}
|
|
69
|
-
return body;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function validationIssue(issue) {
|
|
73
|
-
if (typeof issue === "string") return issue;
|
|
74
|
-
if (!issue || typeof issue !== "object") return "";
|
|
75
|
-
const location = Array.isArray(issue.loc) ? issue.loc.join(".") : "";
|
|
76
|
-
const message = typeof issue.msg === "string" ? issue.msg : "";
|
|
77
|
-
const type = typeof issue.type === "string" ? ` (${issue.type})` : "";
|
|
78
|
-
if (!location && !message) return "";
|
|
79
|
-
return `${location ? `${location}: ` : ""}${message}${type}`;
|
|
80
|
-
}
|
package/src/mcp-server.mjs
DELETED
|
@@ -1,256 +0,0 @@
|
|
|
1
|
-
import readline from "node:readline";
|
|
2
|
-
|
|
3
|
-
import { artifactToolUrl } from "./config.mjs";
|
|
4
|
-
|
|
5
|
-
const PROTOCOL_VERSION = "2025-06-18";
|
|
6
|
-
|
|
7
|
-
const ARTIFACT_TYPES = [
|
|
8
|
-
"vision",
|
|
9
|
-
"problem",
|
|
10
|
-
"capability",
|
|
11
|
-
"epic",
|
|
12
|
-
"feature",
|
|
13
|
-
"story",
|
|
14
|
-
"architecture",
|
|
15
|
-
"service",
|
|
16
|
-
"api",
|
|
17
|
-
"data_model",
|
|
18
|
-
"event",
|
|
19
|
-
"task",
|
|
20
|
-
"release",
|
|
21
|
-
"risk",
|
|
22
|
-
"pattern",
|
|
23
|
-
"technology",
|
|
24
|
-
"agent",
|
|
25
|
-
"workflow",
|
|
26
|
-
"review",
|
|
27
|
-
"comment",
|
|
28
|
-
];
|
|
29
|
-
|
|
30
|
-
const artifactTypeSchema = { type: "string", enum: ARTIFACT_TYPES };
|
|
31
|
-
const artifactIdSchema = {
|
|
32
|
-
type: "string",
|
|
33
|
-
format: "uuid",
|
|
34
|
-
description: "The saved EngineerOS artifact ID.",
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
export const artifactTools = [
|
|
38
|
-
{
|
|
39
|
-
name: "engineeros_list_artifacts",
|
|
40
|
-
description: "List saved artifacts in the current EngineerOS project.",
|
|
41
|
-
inputSchema: {
|
|
42
|
-
type: "object",
|
|
43
|
-
properties: {
|
|
44
|
-
artifact_type: artifactTypeSchema,
|
|
45
|
-
status: { type: "string", enum: ["draft", "active", "archived"] },
|
|
46
|
-
page: { type: "integer", minimum: 1 },
|
|
47
|
-
page_size: { type: "integer", minimum: 1, maximum: 50 },
|
|
48
|
-
},
|
|
49
|
-
additionalProperties: false,
|
|
50
|
-
},
|
|
51
|
-
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
52
|
-
},
|
|
53
|
-
{
|
|
54
|
-
name: "engineeros_get_artifact",
|
|
55
|
-
description: "Load one saved artifact from the current EngineerOS project.",
|
|
56
|
-
inputSchema: {
|
|
57
|
-
type: "object",
|
|
58
|
-
properties: { artifact_id: artifactIdSchema },
|
|
59
|
-
required: ["artifact_id"],
|
|
60
|
-
additionalProperties: false,
|
|
61
|
-
},
|
|
62
|
-
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
63
|
-
},
|
|
64
|
-
{
|
|
65
|
-
name: "engineeros_create_artifact",
|
|
66
|
-
description:
|
|
67
|
-
"Create a saved EngineerOS artifact during the currently approved writable Goal.",
|
|
68
|
-
inputSchema: {
|
|
69
|
-
type: "object",
|
|
70
|
-
properties: {
|
|
71
|
-
artifact_type: artifactTypeSchema,
|
|
72
|
-
name: { type: "string", minLength: 1, maxLength: 500 },
|
|
73
|
-
content: {
|
|
74
|
-
type: "string",
|
|
75
|
-
minLength: 1,
|
|
76
|
-
description: "Complete artifact content as structured Markdown.",
|
|
77
|
-
},
|
|
78
|
-
metadata_info: { type: "object" },
|
|
79
|
-
},
|
|
80
|
-
required: ["artifact_type", "name", "content"],
|
|
81
|
-
additionalProperties: false,
|
|
82
|
-
},
|
|
83
|
-
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
84
|
-
},
|
|
85
|
-
{
|
|
86
|
-
name: "engineeros_update_artifact",
|
|
87
|
-
description:
|
|
88
|
-
"Update a saved EngineerOS artifact during the currently approved writable Goal.",
|
|
89
|
-
inputSchema: {
|
|
90
|
-
type: "object",
|
|
91
|
-
properties: {
|
|
92
|
-
artifact_id: artifactIdSchema,
|
|
93
|
-
artifact_type: artifactTypeSchema,
|
|
94
|
-
name: { type: "string", minLength: 1, maxLength: 500 },
|
|
95
|
-
content: {
|
|
96
|
-
type: "string",
|
|
97
|
-
minLength: 1,
|
|
98
|
-
description: "Complete replacement content as structured Markdown.",
|
|
99
|
-
},
|
|
100
|
-
metadata_info: { type: "object" },
|
|
101
|
-
},
|
|
102
|
-
required: ["artifact_id"],
|
|
103
|
-
additionalProperties: false,
|
|
104
|
-
},
|
|
105
|
-
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
106
|
-
},
|
|
107
|
-
{
|
|
108
|
-
name: "engineeros_reclassify_artifact",
|
|
109
|
-
description:
|
|
110
|
-
"Move a saved artifact to another EngineerOS classification during the currently approved writable Goal.",
|
|
111
|
-
inputSchema: {
|
|
112
|
-
type: "object",
|
|
113
|
-
properties: {
|
|
114
|
-
artifact_id: artifactIdSchema,
|
|
115
|
-
target_artifact_type: artifactTypeSchema,
|
|
116
|
-
},
|
|
117
|
-
required: ["artifact_id", "target_artifact_type"],
|
|
118
|
-
additionalProperties: false,
|
|
119
|
-
},
|
|
120
|
-
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
121
|
-
},
|
|
122
|
-
{
|
|
123
|
-
name: "engineeros_materialize_product_artifacts",
|
|
124
|
-
description:
|
|
125
|
-
"Materialize structured Product documents into the canonical EngineerOS Vision, Portfolio capabilities, and project graph during the currently approved writable Goal.",
|
|
126
|
-
inputSchema: {
|
|
127
|
-
type: "object",
|
|
128
|
-
properties: {},
|
|
129
|
-
additionalProperties: false,
|
|
130
|
-
},
|
|
131
|
-
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
132
|
-
},
|
|
133
|
-
{
|
|
134
|
-
name: "engineeros_delete_artifact",
|
|
135
|
-
description:
|
|
136
|
-
"Soft-delete a saved EngineerOS artifact during the currently approved writable Goal.",
|
|
137
|
-
inputSchema: {
|
|
138
|
-
type: "object",
|
|
139
|
-
properties: { artifact_id: artifactIdSchema },
|
|
140
|
-
required: ["artifact_id"],
|
|
141
|
-
additionalProperties: false,
|
|
142
|
-
},
|
|
143
|
-
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
144
|
-
},
|
|
145
|
-
];
|
|
146
|
-
|
|
147
|
-
const toolNames = new Map(
|
|
148
|
-
artifactTools.map((tool) => [tool.name, tool.name.replace("engineeros_", "")]),
|
|
149
|
-
);
|
|
150
|
-
|
|
151
|
-
export async function handleMcpRequest(message, context) {
|
|
152
|
-
if (message.method === "notifications/initialized") return null;
|
|
153
|
-
if (message.method === "initialize") {
|
|
154
|
-
return success(message.id, {
|
|
155
|
-
protocolVersion: message.params?.protocolVersion || PROTOCOL_VERSION,
|
|
156
|
-
capabilities: { tools: { listChanged: false } },
|
|
157
|
-
serverInfo: { name: "engineeros-connector", version: context.version },
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
if (message.method === "ping") return success(message.id, {});
|
|
161
|
-
if (message.method === "tools/list") {
|
|
162
|
-
return success(message.id, { tools: artifactTools });
|
|
163
|
-
}
|
|
164
|
-
if (message.method === "tools/call") {
|
|
165
|
-
const tool = toolNames.get(message.params?.name);
|
|
166
|
-
if (!tool) return failure(message.id, -32602, "Unknown EngineerOS artifact tool.");
|
|
167
|
-
try {
|
|
168
|
-
const response = await callArtifactTool(context, tool, message.params?.arguments || {});
|
|
169
|
-
return success(message.id, {
|
|
170
|
-
content: [{ type: "text", text: renderArtifactToolResult(response) }],
|
|
171
|
-
isError: false,
|
|
172
|
-
});
|
|
173
|
-
} catch (error) {
|
|
174
|
-
return success(message.id, {
|
|
175
|
-
content: [
|
|
176
|
-
{
|
|
177
|
-
type: "text",
|
|
178
|
-
text: `# EngineerOS artifact tool failed\n\n${error instanceof Error ? error.message : String(error)}`,
|
|
179
|
-
},
|
|
180
|
-
],
|
|
181
|
-
isError: true,
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
return failure(message.id, -32601, `Unsupported MCP method: ${message.method}`);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
export async function runMcpServer({ config, runId, version, input, output, fetchImpl = fetch }) {
|
|
189
|
-
const context = { config, runId, version, fetchImpl };
|
|
190
|
-
const lines = readline.createInterface({ input, crlfDelay: Infinity });
|
|
191
|
-
for await (const line of lines) {
|
|
192
|
-
if (!line.trim()) continue;
|
|
193
|
-
let response;
|
|
194
|
-
try {
|
|
195
|
-
response = await handleMcpRequest(JSON.parse(line), context);
|
|
196
|
-
} catch (error) {
|
|
197
|
-
response = failure(null, -32700, error instanceof Error ? error.message : String(error));
|
|
198
|
-
}
|
|
199
|
-
if (response) output.write(`${JSON.stringify(response)}\n`);
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
async function callArtifactTool(context, tool, arguments_) {
|
|
204
|
-
const response = await context.fetchImpl(
|
|
205
|
-
artifactToolUrl(context.config.server_url, context.config.connector_id),
|
|
206
|
-
{
|
|
207
|
-
method: "POST",
|
|
208
|
-
headers: {
|
|
209
|
-
"Content-Type": "application/json",
|
|
210
|
-
Authorization: `Bearer ${context.config.token}`,
|
|
211
|
-
},
|
|
212
|
-
body: JSON.stringify({ run_id: context.runId, tool, arguments: arguments_ }),
|
|
213
|
-
},
|
|
214
|
-
);
|
|
215
|
-
if (!response.ok) {
|
|
216
|
-
const detail = await response.text();
|
|
217
|
-
throw new Error(`EngineerOS rejected ${tool} (${response.status}): ${detail}`);
|
|
218
|
-
}
|
|
219
|
-
return response.json();
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
function renderArtifactToolResult(result) {
|
|
223
|
-
const lines = ["# EngineerOS artifact tool result", "", result.message || "Completed."];
|
|
224
|
-
if (result.vision_id) lines.push("", `- Vision ID: \`${result.vision_id}\``);
|
|
225
|
-
if (Array.isArray(result.capability_ids) && result.capability_ids.length) {
|
|
226
|
-
lines.push(`- Capability IDs: ${result.capability_ids.map((id) => `\`${id}\``).join(", ")}`);
|
|
227
|
-
}
|
|
228
|
-
if (result.artifact) lines.push("", ...artifactMarkdown(result.artifact));
|
|
229
|
-
if (Array.isArray(result.artifacts)) {
|
|
230
|
-
lines.push("", `## Artifacts (${result.total ?? result.artifacts.length})`, "");
|
|
231
|
-
for (const artifact of result.artifacts) {
|
|
232
|
-
lines.push(`- **${artifact.name}** — \`${artifact.artifact_type}\` — \`${artifact.id}\``);
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
return lines.join("\n");
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
function artifactMarkdown(artifact) {
|
|
239
|
-
return [
|
|
240
|
-
`## ${artifact.name}`,
|
|
241
|
-
"",
|
|
242
|
-
`- ID: \`${artifact.id}\``,
|
|
243
|
-
`- Type: \`${artifact.artifact_type}\``,
|
|
244
|
-
`- Status: \`${artifact.status}\``,
|
|
245
|
-
"",
|
|
246
|
-
artifact.content || "",
|
|
247
|
-
];
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
function success(id, result) {
|
|
251
|
-
return { jsonrpc: "2.0", id, result };
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
function failure(id, code, message) {
|
|
255
|
-
return { jsonrpc: "2.0", id, error: { code, message } };
|
|
256
|
-
}
|