@leoustc/ai-runner-codex 0.1.2 → 0.1.3
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 +6 -4
- package/README.md +6 -3
- package/package.json +1 -1
- package/src/codex.js +31 -3
- package/src/hub.js +18 -5
- package/src/login.js +30 -12
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
|
|
101
|
-
(`codex login --device-auth` by default), relay
|
|
102
|
-
|
|
103
|
-
|
|
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
|
|
88
|
-
|
|
89
|
-
|
|
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
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(
|
|
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?.(
|
|
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 =
|
|
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 (
|
|
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,27 +78,45 @@ async function sendLoginInfo(state, notify) {
|
|
|
78
78
|
|
|
79
79
|
const info = parseLoginInfo(state.buffer);
|
|
80
80
|
|
|
81
|
-
if (!info.url && !info.
|
|
81
|
+
if (!info.url && !info.deviceId) {
|
|
82
82
|
return;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
state.sentLoginInfo = true;
|
|
86
|
-
await notify(
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
info.
|
|
90
|
-
|
|
91
|
-
|
|
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
109
|
const url = text.match(/https?:\/\/[^\s"'<>]+/)?.[0]?.replace(/[),.;]+$/, "") ?? "";
|
|
110
|
+
const labelledDeviceId =
|
|
111
|
+
text.match(/(?:device|user|login|verification)?\s*(?:id|code)(?:\s+is)?\s*[:=]?\s*([A-Z0-9][A-Z0-9-]{4,})/i)?.[1] ?? "";
|
|
96
112
|
const labelledCode =
|
|
97
113
|
text.match(/(?:user|device|login|verification)?\s*code(?:\s+is)?\s*[:=]?\s*([A-Z0-9][A-Z0-9-]{4,})/i)?.[1] ?? "";
|
|
98
114
|
const fallbackCode = text.match(/\b[A-Z0-9]{4}-[A-Z0-9]{4}\b/i)?.[0] ?? "";
|
|
115
|
+
const deviceId = labelledDeviceId || labelledCode || fallbackCode;
|
|
99
116
|
|
|
100
117
|
return {
|
|
101
|
-
code:
|
|
118
|
+
code: deviceId,
|
|
119
|
+
deviceId,
|
|
102
120
|
url,
|
|
103
121
|
};
|
|
104
122
|
}
|