@glubean/port 0.1.0 → 0.1.1

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 +1 @@
1
- {"version":3,"file":"codex.d.ts","sourceRoot":"","sources":["../../src/providers/codex.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAGV,cAAc,EAEd,aAAa,EAEb,kBAAkB,EAElB,SAAS,EACT,IAAI,EACJ,WAAW,EAGZ,MAAM,aAAa,CAAC;AAErB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAGpD,KAAK,gBAAgB,GAAG,kBAAkB,CAAC,OAAO,CAAC,CAAC;AAEpD,eAAO,MAAM,iBAAiB,EAAE,aAAa,CAAC,OAAO,CAwCpD,CAAC;AAEF,wBAAsB,eAAe,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAEvF;AAED,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC,CAIrG;AAiPD,wBAAgB,uBAAuB,CAAC,KAAK,EAAE;IAC7C,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;CACpC,GAAG,SAAS,CAYZ"}
1
+ {"version":3,"file":"codex.d.ts","sourceRoot":"","sources":["../../src/providers/codex.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAGV,cAAc,EAEd,aAAa,EAEb,kBAAkB,EAElB,SAAS,EACT,IAAI,EACJ,WAAW,EAGZ,MAAM,aAAa,CAAC;AAErB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAGpD,KAAK,gBAAgB,GAAG,kBAAkB,CAAC,OAAO,CAAC,CAAC;AAEpD,eAAO,MAAM,iBAAiB,EAAE,aAAa,CAAC,OAAO,CAwCpD,CAAC;AAEF,wBAAsB,eAAe,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAEvF;AAED,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC,CAIrG;AAuUD,wBAAgB,uBAAuB,CAAC,KAAK,EAAE;IAC7C,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;CACpC,GAAG,SAAS,CAYZ"}
@@ -55,6 +55,13 @@ export async function createCodexAdapter(options) {
55
55
  await runtime.start();
56
56
  return runtime.adapter;
57
57
  }
58
+ /**
59
+ * Cap stderr buffer to a bounded number of lines so a chatty codex
60
+ * process cannot grow memory unbounded over a long-lived session. The
61
+ * tail (most recent N lines) is what we surface in error messages —
62
+ * head lines from a healthy startup are rarely useful for diagnosis.
63
+ */
64
+ const STDERR_BUFFER_MAX_LINES = 200;
58
65
  class CodexRuntime {
59
66
  #options;
60
67
  #process;
@@ -62,6 +69,17 @@ class CodexRuntime {
62
69
  #normalizer = new CodexEventNormalizer();
63
70
  #events = new EventEmitter();
64
71
  #closed = false;
72
+ /**
73
+ * Bounded ring buffer of recent codex stderr lines, surfaced in
74
+ * augmented error messages so a 30ms "JSON-RPC input closed" failure
75
+ * carries the actual codex stderr (auth error, bad flag, missing
76
+ * config, etc.) instead of disappearing into the void.
77
+ */
78
+ #stderrBuffer = [];
79
+ /** Captured at process 'exit' so subsequent rpc.request rejections can cite it. */
80
+ #exitInfo = null;
81
+ /** Captured at process 'error' (e.g. spawn ENOENT) for the same reason. */
82
+ #spawnError = null;
65
83
  constructor(options) {
66
84
  this.#options = options;
67
85
  }
@@ -104,27 +122,84 @@ class CodexRuntime {
104
122
  shell: false,
105
123
  });
106
124
  this.#process.stderr.on("data", (chunk) => {
107
- const message = chunk.toString("utf8").trim();
108
- if (message)
109
- this.#emit({ type: "error", message });
125
+ const text = chunk.toString("utf8");
126
+ // Buffer stderr line-by-line so #augmentError can surface a
127
+ // bounded tail. We still emit the chunk as an error event so
128
+ // existing event-stream consumers aren't broken.
129
+ for (const line of text.split(/\r?\n/)) {
130
+ if (!line)
131
+ continue;
132
+ this.#stderrBuffer.push(line);
133
+ if (this.#stderrBuffer.length > STDERR_BUFFER_MAX_LINES) {
134
+ this.#stderrBuffer.splice(0, this.#stderrBuffer.length - STDERR_BUFFER_MAX_LINES);
135
+ }
136
+ }
137
+ const trimmed = text.trim();
138
+ if (trimmed)
139
+ this.#emit({ type: "error", message: trimmed });
110
140
  });
111
141
  this.#process.on("exit", (code, signal) => {
142
+ this.#exitInfo = { code, signal };
112
143
  if (!this.#closed) {
113
144
  this.#emit({ type: "error", message: `Codex app-server exited: code=${code ?? "null"} signal=${signal ?? "null"}` });
114
145
  }
115
146
  });
147
+ // 'error' fires on spawn failures (e.g. ENOENT when codex binary is
148
+ // missing from PATH). Without a listener Node would crash the
149
+ // process on the unhandled emit; instead capture and let the
150
+ // pending rpc.request rejection surface the spawn error.
151
+ this.#process.on("error", (err) => {
152
+ this.#spawnError = err;
153
+ if (!this.#closed) {
154
+ this.#emit({ type: "error", message: `Codex app-server spawn error: ${err.message}` });
155
+ }
156
+ });
116
157
  this.#rpc = new JsonRpcLineConnection(this.#process.stdout, this.#process.stdin);
117
158
  this.#rpc.onNotification((notification) => {
118
159
  for (const event of this.#normalizer.normalize(notification)) {
119
160
  this.#emit(event);
120
161
  }
121
162
  });
122
- await this.#rpc.request("initialize", {
123
- clientInfo: { name: "glubean-port", version: "0.0.0" },
124
- capabilities: { experimentalApi: true },
125
- });
163
+ try {
164
+ await this.#rpc.request("initialize", {
165
+ clientInfo: { name: "glubean-port", version: "0.0.0" },
166
+ capabilities: { experimentalApi: true },
167
+ });
168
+ }
169
+ catch (err) {
170
+ throw await this.#augmentError(err);
171
+ }
126
172
  this.#rpc.notify("initialized", {});
127
173
  }
174
+ /**
175
+ * Wrap an rpc rejection with codex process diagnostics so callers see
176
+ * (a) why the process closed if it did, (b) any spawn 'error' (e.g.
177
+ * ENOENT), and (c) the most recent stderr lines. The original error
178
+ * is preserved as `cause` for callers that introspect it.
179
+ *
180
+ * Awaits a microtask so any racing `process.on("exit")` fires before
181
+ * we read `#exitInfo` — JsonRpcLineConnection rejects synchronously
182
+ * from `input.on("close")`, but Node may fire stream 'close' before
183
+ * the matching process 'exit' on some versions.
184
+ */
185
+ async #augmentError(err) {
186
+ await new Promise((resolve) => setImmediate(resolve));
187
+ const original = err instanceof Error ? err : new Error(String(err));
188
+ const parts = [original.message];
189
+ if (this.#spawnError) {
190
+ parts.push(`(codex spawn error: ${this.#spawnError.message})`);
191
+ }
192
+ if (this.#exitInfo) {
193
+ parts.push(`(codex process exited: code=${this.#exitInfo.code ?? "null"} signal=${this.#exitInfo.signal ?? "null"})`);
194
+ }
195
+ if (this.#stderrBuffer.length > 0) {
196
+ const tail = this.#stderrBuffer.slice(-20).join("\n");
197
+ parts.push(`(codex stderr tail:\n${tail})`);
198
+ }
199
+ const augmented = new Error(parts.join(" "));
200
+ augmented.cause = original;
201
+ return augmented;
202
+ }
128
203
  async close() {
129
204
  this.#closed = true;
130
205
  this.#rpc?.close();
@@ -152,7 +227,14 @@ class CodexRuntime {
152
227
  });
153
228
  if (method === "thread/resume")
154
229
  params.threadId = input.id ?? "";
155
- const result = asRecord(await rpc.request(method, params));
230
+ let rawResult;
231
+ try {
232
+ rawResult = await rpc.request(method, params);
233
+ }
234
+ catch (err) {
235
+ throw await this.#augmentError(err);
236
+ }
237
+ const result = asRecord(rawResult);
156
238
  const thread = asRecord(result.thread);
157
239
  const id = asString(thread.id) ?? input.id;
158
240
  if (!id)
@@ -183,18 +265,25 @@ class CodexRuntime {
183
265
  const runtimeOptions = resolveRuntimeOptions(input, this.#options.options);
184
266
  const codexOptions = providerRuntimeOptions(runtimeOptions);
185
267
  const effort = toCodexReasoningEffort(codexOptions.effort);
186
- const result = asRecord(await rpc.request("turn/start", {
187
- threadId: input.sessionId,
188
- input: normalizeInput(input.input),
189
- ...(runtimeOptions.cwd ? { cwd: runtimeOptions.cwd } : {}),
190
- ...(runtimeOptions.model ? { model: runtimeOptions.model } : {}),
191
- ...(runtimeOptions.approvalPolicy ? { approvalPolicy: runtimeOptions.approvalPolicy } : {}),
192
- ...(runtimeOptions.sandbox ? { sandboxPolicy: toCodexSandboxPolicy(runtimeOptions.sandbox) } : {}),
193
- ...(effort ? { effort } : {}),
194
- ...(codexOptions?.reasoningSummary ? { summary: codexOptions.reasoningSummary } : {}),
195
- ...(codexOptions?.serviceTier ? { serviceTier: codexOptions.serviceTier } : {}),
196
- ...(input.outputSchema ? { outputSchema: input.outputSchema } : {}),
197
- }));
268
+ let rawResult;
269
+ try {
270
+ rawResult = await rpc.request("turn/start", {
271
+ threadId: input.sessionId,
272
+ input: normalizeInput(input.input),
273
+ ...(runtimeOptions.cwd ? { cwd: runtimeOptions.cwd } : {}),
274
+ ...(runtimeOptions.model ? { model: runtimeOptions.model } : {}),
275
+ ...(runtimeOptions.approvalPolicy ? { approvalPolicy: runtimeOptions.approvalPolicy } : {}),
276
+ ...(runtimeOptions.sandbox ? { sandboxPolicy: toCodexSandboxPolicy(runtimeOptions.sandbox) } : {}),
277
+ ...(effort ? { effort } : {}),
278
+ ...(codexOptions?.reasoningSummary ? { summary: codexOptions.reasoningSummary } : {}),
279
+ ...(codexOptions?.serviceTier ? { serviceTier: codexOptions.serviceTier } : {}),
280
+ ...(input.outputSchema ? { outputSchema: input.outputSchema } : {}),
281
+ });
282
+ }
283
+ catch (err) {
284
+ throw await this.#augmentError(err);
285
+ }
286
+ const result = asRecord(rawResult);
198
287
  expectedTurnId = readTurnId(result);
199
288
  for await (const event of queue) {
200
289
  yield event;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glubean/port",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Neutral runtime adapter for local coding agents.",
5
5
  "type": "module",
6
6
  "exports": {