@engineeros/connector 0.11.2 → 0.12.2

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.
@@ -1,218 +1,246 @@
1
- import { spawn } from "node:child_process";
2
- import readline from "node:readline";
3
-
4
- export async function inspectCodexExecutionProfiles({
5
- workspace,
6
- command = process.env.CODEX_BIN || (process.platform === "win32" ? "codex.cmd" : "codex"),
7
- spawnProcess = spawn,
8
- timeoutMs = 15_000,
9
- }) {
10
- const child = spawnProcess(command, ["app-server", "--stdio"], {
11
- cwd: workspace,
12
- env: process.env,
13
- shell: process.platform === "win32",
14
- windowsHide: true,
15
- stdio: ["pipe", "pipe", "pipe"],
16
- });
17
- const pending = new Map();
18
- let requestId = 0;
19
- let stderr = "";
20
- const request = (method, params) => new Promise((resolve, reject) => {
21
- const id = ++requestId;
22
- pending.set(id, { resolve, reject });
23
- child.stdin.write(`${JSON.stringify({ id, method, params })}\n`);
24
- });
25
- const lines = readline.createInterface({ input: child.stdout });
26
- lines.on("line", (line) => {
27
- try {
28
- const message = JSON.parse(line);
29
- const waiter = pending.get(message.id);
30
- if (!waiter) return;
31
- pending.delete(message.id);
32
- if (message.error) waiter.reject(new Error(message.error.message || "Codex model discovery failed."));
33
- else waiter.resolve(message.result);
34
- } catch {
35
- // Ignore non-protocol output.
36
- }
37
- });
38
- child.stderr.setEncoding("utf8");
39
- child.stderr.on("data", (chunk) => { stderr = `${stderr}${chunk}`.slice(-4_000); });
40
- const rejectPending = (message) => {
41
- for (const waiter of pending.values()) waiter.reject(new Error(message));
42
- pending.clear();
43
- };
44
- child.once("error", (error) => rejectPending(error.message));
45
- child.once("close", (code) => {
46
- if (pending.size) rejectPending(`Codex model discovery stopped with code ${code ?? 1}. ${stderr}`.trim());
47
- });
48
- const timeout = setTimeout(() => {
49
- rejectPending("Codex model discovery timed out.");
50
- child.kill();
51
- }, timeoutMs);
52
- try {
53
- await request("initialize", {
54
- clientInfo: { name: "engineeros-connector", title: "EngineerOS Connector", version: "0.11.0" },
55
- });
56
- child.stdin.write(`${JSON.stringify({ method: "initialized" })}\n`);
57
- const models = [];
58
- let cursor = null;
59
- do {
60
- const result = await request("model/list", { cursor, includeHidden: false });
61
- models.push(...(Array.isArray(result?.data) ? result.data : []));
62
- cursor = result?.nextCursor || null;
63
- } while (cursor);
64
- return models.map((item) => ({
65
- id: item.model || item.id,
66
- name: item.displayName || item.model || item.id,
67
- description: item.description || "",
68
- is_default: item.isDefault === true,
69
- default_reasoning_effort: item.defaultReasoningEffort || null,
70
- reasoning_efforts: (item.supportedReasoningEfforts || [])
71
- .map((option) => option.reasoningEffort)
72
- .filter(Boolean),
73
- })).filter((item) => item.id);
74
- } catch (error) {
75
- const detail = error instanceof Error ? error.message : String(error);
76
- throw new Error(`${detail}${stderr ? ` ${stderr}` : ""}`.trim());
77
- } finally {
78
- clearTimeout(timeout);
79
- child.kill();
80
- }
81
- }
82
-
83
- export function launchCodexAppServer({
84
- workspace,
85
- prompt,
86
- sandbox,
87
- profile = {},
88
- previousSessionId,
89
- callbacks = {},
90
- command = process.env.CODEX_BIN || (process.platform === "win32" ? "codex.cmd" : "codex"),
91
- spawnProcess = spawn,
92
- }) {
93
- const child = spawnProcess(command, ["app-server", "--stdio"], {
94
- cwd: workspace,
95
- env: process.env,
96
- shell: process.platform === "win32",
97
- windowsHide: true,
98
- stdio: ["pipe", "pipe", "pipe"],
99
- });
100
- const pending = new Map();
101
- let requestId = 0;
102
- let threadId = previousSessionId || "";
103
- let turnId = "";
104
- let finalMessage = "";
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 = "";
105
106
  let stderr = "";
107
+ let usage = null;
106
108
  let settled = false;
107
-
108
- const request = (method, params) =>
109
- new Promise((resolve, reject) => {
110
- const id = ++requestId;
111
- pending.set(id, { resolve, reject });
112
- child.stdin.write(`${JSON.stringify({ id, method, params })}\n`);
113
- });
114
-
115
- const completed = new Promise((resolve, reject) => {
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) => {
116
119
  const finish = (error) => {
117
120
  if (settled) return;
118
121
  settled = true;
119
- for (const waiter of pending.values()) waiter.reject(error || new Error("Codex app-server stopped."));
120
- pending.clear();
121
- if (error) reject(error);
122
- else resolve({ finalMessage, output: stderr.slice(-20_000), sessionId: threadId });
123
- };
124
-
125
- child.once("error", finish);
126
- child.once("close", (code) => {
127
- if (!settled) finish(new Error(`Codex app-server exited before completing the turn (code ${code ?? 1}). ${stderr}`));
128
- });
129
-
130
- const lines = readline.createInterface({ input: child.stdout });
131
- lines.on("line", (line) => {
132
- let message;
133
- try {
134
- message = JSON.parse(line);
135
- } catch {
136
- return;
137
- }
138
- if (message.id !== undefined) {
139
- const waiter = pending.get(message.id);
140
- if (!waiter) return;
141
- pending.delete(message.id);
142
- if (message.error) waiter.reject(new Error(message.error.message || "Codex app-server request failed."));
143
- else waiter.resolve(message.result);
144
- return;
145
- }
146
- const params = message.params || {};
147
- if (message.method === "item/agentMessage/delta" && typeof params.delta === "string") {
148
- finalMessage += params.delta;
149
- callbacks.onEvent?.({ type: "codex.agent_message_delta", delta: params.delta });
150
- } else if (message.method === "item/started" || message.method === "item/completed") {
151
- callbacks.onEvent?.({ type: `codex.${message.method}`, item: params.item });
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 });
152
162
  } else if (message.method === "thread/tokenUsage/updated") {
163
+ usage = normalizeTokenUsage(
164
+ params.last || params.usage || params.tokenUsage?.last || params.tokenUsage || params,
165
+ );
153
166
  callbacks.onEvent?.({ type: "codex.usage", update: params });
167
+ if (usage && completionTimer) {
168
+ finish();
169
+ child.kill();
170
+ }
154
171
  } else if (message.method === "turn/completed" && params.threadId === threadId) {
155
172
  const status = params.turn?.status;
156
- if (status === "failed") finish(new Error(params.turn?.error?.message || "Codex turn failed."));
157
- else if (!finalMessage.trim()) finish(new Error("Codex completed without returning a response."));
158
- else finish();
159
- child.kill();
160
- }
161
- });
162
-
163
- child.stderr.setEncoding("utf8");
164
- child.stderr.on("data", (chunk) => {
165
- stderr = `${stderr}${chunk}`.slice(-20_000);
166
- });
167
-
168
- void (async () => {
169
- try {
170
- await request("initialize", {
171
- clientInfo: { name: "engineeros-connector", title: "EngineerOS Connector", version: "0.11.0" },
172
- });
173
- child.stdin.write(`${JSON.stringify({ method: "initialized" })}\n`);
174
- const threadResult = previousSessionId
175
- ? await request("thread/resume", {
176
- threadId: previousSessionId,
177
- cwd: workspace,
178
- sandbox,
179
- approvalPolicy: "never",
180
- model: profile.model || null,
181
- })
182
- : await request("thread/start", {
183
- cwd: workspace,
184
- sandbox,
185
- approvalPolicy: "never",
186
- model: profile.model || null,
187
- ephemeral: false,
188
- });
189
- threadId = threadResult.thread.id;
190
- callbacks.onEvent?.({ type: "thread.started", thread_id: threadId });
191
- const turnResult = await request("turn/start", {
192
- threadId,
193
- input: [{ type: "text", text: prompt }],
194
- effort: profile.reasoning_effort || null,
195
- });
196
- turnId = turnResult.turn.id;
197
- } catch (error) {
198
- child.kill();
199
- finish(error instanceof Error ? error : new Error(String(error)));
200
- }
201
- })();
202
- });
203
-
204
- return {
205
- child,
206
- completed,
207
- cancel: async () => {
208
- if (threadId && turnId && !settled) {
209
- try {
210
- await request("turn/interrupt", { threadId, turnId });
211
- } catch {
212
- // Process termination below is the final cancellation boundary.
173
+ if (status === "failed") {
174
+ finish(new Error(params.turn?.error?.message || "Codex turn failed."));
175
+ child.kill();
176
+ } else if (!finalMessage.trim()) {
177
+ finish(new Error("Codex completed without returning a response."));
178
+ child.kill();
179
+ } else if (usage) {
180
+ finish();
181
+ child.kill();
182
+ } else if (!completionTimer) {
183
+ completionTimer = setTimeout(() => {
184
+ finish();
185
+ child.kill();
186
+ }, 250);
213
187
  }
214
188
  }
215
- child.kill();
216
- },
217
- };
218
- }
189
+ });
190
+
191
+ child.stderr.setEncoding("utf8");
192
+ child.stderr.on("data", (chunk) => {
193
+ stderr = `${stderr}${chunk}`.slice(-20_000);
194
+ });
195
+
196
+ void (async () => {
197
+ try {
198
+ await request("initialize", {
199
+ clientInfo: { name: "engineeros-connector", title: "EngineerOS Connector", version: "0.11.0" },
200
+ });
201
+ child.stdin.write(`${JSON.stringify({ method: "initialized" })}\n`);
202
+ const threadResult = previousSessionId
203
+ ? await request("thread/resume", {
204
+ threadId: previousSessionId,
205
+ cwd: workspace,
206
+ sandbox,
207
+ approvalPolicy: "never",
208
+ model: profile.model || null,
209
+ })
210
+ : await request("thread/start", {
211
+ cwd: workspace,
212
+ sandbox,
213
+ approvalPolicy: "never",
214
+ model: profile.model || null,
215
+ ephemeral: false,
216
+ });
217
+ threadId = threadResult.thread.id;
218
+ callbacks.onEvent?.({ type: "thread.started", thread_id: threadId });
219
+ const turnResult = await request("turn/start", {
220
+ threadId,
221
+ input: [{ type: "text", text: prompt }],
222
+ effort: profile.reasoning_effort || null,
223
+ });
224
+ turnId = turnResult.turn.id;
225
+ } catch (error) {
226
+ child.kill();
227
+ finish(error instanceof Error ? error : new Error(String(error)));
228
+ }
229
+ })();
230
+ });
231
+
232
+ return {
233
+ child,
234
+ completed,
235
+ cancel: async () => {
236
+ if (threadId && turnId && !settled) {
237
+ try {
238
+ await request("turn/interrupt", { threadId, turnId });
239
+ } catch {
240
+ // Process termination below is the final cancellation boundary.
241
+ }
242
+ }
243
+ child.kill();
244
+ },
245
+ };
246
+ }