@leoustc/ai-runner-codex 0.1.2 → 0.1.4

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/AGENTS.md CHANGED
@@ -97,10 +97,12 @@ Optional variables may also be added for the Codex app-server command,
97
97
  socket mode; this runner is intended to keep Codex hot through app-server
98
98
  WebSocket.
99
99
 
100
- If Codex app-server returns an auth/login error, start `CODEX_LOGIN_COMMAND`
101
- (`codex login --device-auth` by default), relay the device login URL/code back
102
- to the Hub as an agent message, wait for login completion, and retry the Codex
103
- request once.
100
+ If Codex app-server returns an auth/login error or generic app-server error,
101
+ start `CODEX_LOGIN_COMMAND` (`codex login --device-auth` by default), relay a
102
+ normal agent message with `codexLogin: true`, `status`, and any parsed device
103
+ login `url`, `code`, and `deviceId` back to the Hub, wait for login completion,
104
+ and retry the Codex request once. Spawn the login command as a local child
105
+ process and watch stdout/stderr for device login values.
104
106
 
105
107
 
106
108
  Do not commit real `.env` secrets. If examples are needed, use `.env.example`.
package/README.md CHANGED
@@ -84,9 +84,12 @@ WebSocket text frame.
84
84
  On startup, the runner checks that the `codex` CLI is installed and available
85
85
  on `PATH` before starting app-server.
86
86
 
87
- If Codex returns an auth/login error, the runner starts `CODEX_LOGIN_COMMAND`,
88
- posts the device login URL/code back to the Hub as an agent message, waits for
89
- login completion, then retries the Codex request once.
87
+ If Codex returns an auth/login error or a generic app-server error, the runner
88
+ starts `CODEX_LOGIN_COMMAND`, posts a normal agent message with
89
+ `codexLogin: true`, `status`, and any parsed device login `url`, `code`, and
90
+ `deviceId` back to the Hub, waits for login completion, then retries the Codex
91
+ request once. The login command is spawned as a local child process and the
92
+ runner watches stdout/stderr for the device login values.
90
93
 
91
94
  Hub WebSocket behavior:
92
95
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leoustc/ai-runner-codex",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "private": false,
5
5
  "description": "Codex app-server backed Agent Runner Hub CLI connector",
6
6
  "license": "MIT",
package/src/codex.js CHANGED
@@ -194,7 +194,7 @@ class CodexWsClient {
194
194
  this.pending.delete(String(message.id));
195
195
 
196
196
  if (message.error) {
197
- pending.reject(new Error(message.error.message || JSON.stringify(message.error)));
197
+ pending.reject(codexError(message.error, "Codex app-server request failed"));
198
198
  } else {
199
199
  pending.resolve(message.result);
200
200
  }
@@ -235,7 +235,7 @@ class CodexWsClient {
235
235
  this.turns.delete(threadId);
236
236
 
237
237
  if (message.params?.turn?.status === "failed") {
238
- state.reject?.(new Error(message.params.turn.error?.message || "Codex turn failed"));
238
+ state.reject?.(codexError(message.params.turn.error, "Codex turn failed"));
239
239
  } else {
240
240
  state.resolve?.(state.text || textFromTurn(message.params?.turn));
241
241
  }
@@ -243,7 +243,7 @@ class CodexWsClient {
243
243
  }
244
244
 
245
245
  if (message.method === "error") {
246
- const error = new Error(message.params?.message || "Codex app-server error");
246
+ const error = codexError(message.params, "Codex app-server error");
247
247
 
248
248
  for (const state of this.turns.values()) {
249
249
  state.reject?.(error);
@@ -379,6 +379,34 @@ function textFromTurn(turn) {
379
379
  .trim();
380
380
  }
381
381
 
382
+ function codexError(value, fallback) {
383
+ if (!value || typeof value !== "object") {
384
+ return new Error(fallback);
385
+ }
386
+
387
+ const fields = [];
388
+
389
+ for (const key of ["message", "code", "reason", "type", "status"]) {
390
+ if (typeof value[key] === "string" && value[key]) {
391
+ fields.push(`${key}: ${value[key]}`);
392
+ }
393
+ }
394
+
395
+ if (value.error && typeof value.error === "object") {
396
+ for (const key of ["message", "code", "type"]) {
397
+ if (typeof value.error[key] === "string" && value.error[key]) {
398
+ fields.push(`error.${key}: ${value.error[key]}`);
399
+ }
400
+ }
401
+ }
402
+
403
+ if (fields.length === 0) {
404
+ fields.push(JSON.stringify(value));
405
+ }
406
+
407
+ return new Error(`${fallback}: ${fields.join("; ")}`);
408
+ }
409
+
382
410
  function sandboxPolicy(mode, cwd) {
383
411
  if (mode === "danger-full-access") {
384
412
  return { type: "dangerFullAccess" };
package/src/hub.js CHANGED
@@ -117,11 +117,8 @@ export class HubClient {
117
117
  throw error;
118
118
  }
119
119
 
120
- await ensureCodexLogin(this.config, this.log, async (text) => {
121
- this.sendMessages(value, {
122
- codexLogin: true,
123
- text,
124
- });
120
+ await ensureCodexLogin(this.config, this.log, async (notice) => {
121
+ this.sendMessages(value, loginNoticeMessage(notice));
125
122
  });
126
123
 
127
124
  return await requestCodex(this.config, request);
@@ -257,3 +254,19 @@ function parseJson(data) {
257
254
  return null;
258
255
  }
259
256
  }
257
+
258
+ function loginNoticeMessage(notice) {
259
+ if (notice && typeof notice === "object") {
260
+ return {
261
+ ...notice,
262
+ codexLogin: true,
263
+ text: typeof notice.text === "string" ? notice.text : JSON.stringify(notice, null, 2),
264
+ };
265
+ }
266
+
267
+ return {
268
+ codexLogin: true,
269
+ status: "notice",
270
+ text: String(notice),
271
+ };
272
+ }
package/src/login.js CHANGED
@@ -5,12 +5,12 @@ let activeLogin = null;
5
5
  export function isCodexAuthError(error) {
6
6
  const message = error instanceof Error ? error.message : String(error);
7
7
 
8
- return /auth|login|sign[ -]?in|unauthori[sz]ed|forbidden|invalid_grant|refresh token|access token/i.test(message);
8
+ return /auth|login|sign[ -]?in|unauthori[sz]ed|forbidden|invalid_grant|refresh token|access token|codex app-server error/i.test(message);
9
9
  }
10
10
 
11
11
  export async function ensureCodexLogin(config, log, notify) {
12
12
  if (activeLogin) {
13
- await notify("Codex login is already in progress. Waiting for it to complete.");
13
+ await notify(loginStatusMessage("waiting", "Codex login is already in progress. Waiting for it to complete."));
14
14
  return await activeLogin;
15
15
  }
16
16
 
@@ -24,7 +24,7 @@ export async function ensureCodexLogin(config, log, notify) {
24
24
  async function runCodexLogin(config, log, notify) {
25
25
  const [program, ...args] = splitCommand(config.codexLoginCommand);
26
26
 
27
- await notify("Codex login is required. Starting device login.");
27
+ await notify(loginStatusMessage("starting", "Codex login is required. Starting device login process."));
28
28
  log.info(`starting codex login: ${config.codexLoginCommand}`);
29
29
 
30
30
  return await new Promise((resolve, reject) => {
@@ -62,7 +62,7 @@ async function runCodexLogin(config, log, notify) {
62
62
  clearTimeout(timer);
63
63
 
64
64
  if (code === 0) {
65
- void notify("Codex login completed. Retrying the request.");
65
+ void notify(loginStatusMessage("complete", "Codex login completed. Retrying the request."));
66
66
  resolve();
67
67
  } else {
68
68
  reject(new Error(`Codex login exited code=${code ?? "null"} signal=${signal ?? "null"}`));
@@ -78,31 +78,83 @@ async function sendLoginInfo(state, notify) {
78
78
 
79
79
  const info = parseLoginInfo(state.buffer);
80
80
 
81
- if (!info.url && !info.code) {
81
+ if (!info.url && !info.deviceId) {
82
82
  return;
83
83
  }
84
84
 
85
85
  state.sentLoginInfo = true;
86
- await notify([
87
- "Codex login required.",
88
- info.url ? `Open: ${info.url}` : "",
89
- info.code ? `Code: ${info.code}` : "",
90
- "After login completes, the runner will retry automatically.",
91
- ].filter(Boolean).join("\n"));
86
+ await notify({
87
+ codexLogin: true,
88
+ status: "login_required",
89
+ ...(info.url ? { url: info.url } : {}),
90
+ ...(info.deviceId ? { code: info.deviceId, deviceId: info.deviceId } : {}),
91
+ text: [
92
+ "Codex login required.",
93
+ info.url ? `Open: ${info.url}` : "",
94
+ info.deviceId ? `Code: ${info.deviceId}` : "",
95
+ "After login completes, the runner will retry automatically.",
96
+ ].filter(Boolean).join("\n"),
97
+ });
98
+ }
99
+
100
+ function loginStatusMessage(status, text) {
101
+ return {
102
+ codexLogin: true,
103
+ status,
104
+ text,
105
+ };
92
106
  }
93
107
 
94
108
  function parseLoginInfo(text) {
95
- const url = text.match(/https?:\/\/[^\s"'<>]+/)?.[0]?.replace(/[),.;]+$/, "") ?? "";
96
- const labelledCode =
97
- text.match(/(?:user|device|login|verification)?\s*code(?:\s+is)?\s*[:=]?\s*([A-Z0-9][A-Z0-9-]{4,})/i)?.[1] ?? "";
98
- const fallbackCode = text.match(/\b[A-Z0-9]{4}-[A-Z0-9]{4}\b/i)?.[0] ?? "";
109
+ const clean = stripAnsi(text);
110
+ const url = clean.match(/https?:\/\/[^\s"'<>]+/)?.[0]?.replace(/[),.;]+$/, "") ?? "";
111
+ const withoutUrls = clean.replace(/https?:\/\/[^\s"'<>]+/g, " ");
112
+ const labelledDeviceId =
113
+ firstDeviceCode([
114
+ ...withoutUrls.matchAll(
115
+ /(?:device|user|login|verification|authorization)\s*(?:id|code)(?:\s+(?:is|:)|\s*[:=])\s*([A-Z0-9][A-Z0-9-]{4,})/gi,
116
+ ),
117
+ ].map((match) => match[1])) ?? "";
118
+ const fallbackCode =
119
+ firstDeviceCode([
120
+ ...withoutUrls.matchAll(/\b[A-Z0-9]{4,}(?:-[A-Z0-9]{4,})+\b/g),
121
+ ...withoutUrls.matchAll(/\b[A-Z0-9]{8,}\b/g),
122
+ ].map((match) => match[0])) ?? "";
123
+ const deviceId = labelledDeviceId || fallbackCode;
99
124
 
100
125
  return {
101
- code: labelledCode || fallbackCode,
126
+ code: deviceId,
127
+ deviceId,
102
128
  url,
103
129
  };
104
130
  }
105
131
 
132
+ function stripAnsi(text) {
133
+ return String(text).replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, "");
134
+ }
135
+
136
+ function firstDeviceCode(candidates) {
137
+ return candidates
138
+ .map((candidate) => String(candidate || "").trim().replace(/[),.;]+$/, ""))
139
+ .find((candidate) => isDeviceCode(candidate));
140
+ }
141
+
142
+ function isDeviceCode(candidate) {
143
+ if (!candidate || candidate.length < 6) {
144
+ return false;
145
+ }
146
+
147
+ if (!/^[A-Z0-9-]+$/.test(candidate)) {
148
+ return false;
149
+ }
150
+
151
+ if (!/[0-9]/.test(candidate) && !candidate.includes("-")) {
152
+ return false;
153
+ }
154
+
155
+ return !/^(AUTHORIZATION|VERIFICATION|DEVICE|LOGIN|OPENAI|CODEX)$/i.test(candidate);
156
+ }
157
+
106
158
  function splitCommand(command) {
107
159
  const matches = command.match(/"[^"]*"|'[^']*'|\S+/g) ?? [];
108
160
  return matches.map((part) => {