@leoustc/ai-runner-codex 0.1.3 → 0.1.5
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 +3 -0
- package/README.md +4 -1
- package/package.json +1 -1
- package/src/login.js +115 -27
package/AGENTS.md
CHANGED
|
@@ -103,6 +103,9 @@ normal agent message with `codexLogin: true`, `status`, and any parsed device
|
|
|
103
103
|
login `url`, `code`, and `deviceId` back to the Hub, wait for login completion,
|
|
104
104
|
and retry the Codex request once. Spawn the login command as a local child
|
|
105
105
|
process and watch stdout/stderr for device login values.
|
|
106
|
+
While login is in progress, stream cleaned login output to every waiting Hub
|
|
107
|
+
request, and replay recent output plus the latest parsed URL/code/deviceId to
|
|
108
|
+
requests that arrive mid-login.
|
|
106
109
|
|
|
107
110
|
|
|
108
111
|
Do not commit real `.env` secrets. If examples are needed, use `.env.example`.
|
package/README.md
CHANGED
|
@@ -89,7 +89,10 @@ starts `CODEX_LOGIN_COMMAND`, posts a normal agent message with
|
|
|
89
89
|
`codexLogin: true`, `status`, and any parsed device login `url`, `code`, and
|
|
90
90
|
`deviceId` back to the Hub, waits for login completion, then retries the Codex
|
|
91
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.
|
|
92
|
+
runner watches stdout/stderr for the device login values. While login is in
|
|
93
|
+
progress, cleaned login output is streamed to every waiting Hub request, and a
|
|
94
|
+
new request that arrives mid-login receives the recent output plus the latest
|
|
95
|
+
parsed URL/code/deviceId.
|
|
93
96
|
|
|
94
97
|
Hub WebSocket behavior:
|
|
95
98
|
|
package/package.json
CHANGED
package/src/login.js
CHANGED
|
@@ -10,21 +10,37 @@ export function isCodexAuthError(error) {
|
|
|
10
10
|
|
|
11
11
|
export async function ensureCodexLogin(config, log, notify) {
|
|
12
12
|
if (activeLogin) {
|
|
13
|
+
const session = activeLogin;
|
|
14
|
+
session.notifiers.add(notify);
|
|
13
15
|
await notify(loginStatusMessage("waiting", "Codex login is already in progress. Waiting for it to complete."));
|
|
14
|
-
|
|
16
|
+
await replayLoginState(session, notify);
|
|
17
|
+
return await session.promise.finally(() => {
|
|
18
|
+
session.notifiers.delete(notify);
|
|
19
|
+
});
|
|
15
20
|
}
|
|
16
21
|
|
|
17
|
-
|
|
22
|
+
const session = {
|
|
23
|
+
buffer: "",
|
|
24
|
+
lastInfoKey: "",
|
|
25
|
+
notifiers: new Set([notify]),
|
|
26
|
+
outputTail: [],
|
|
27
|
+
promise: null,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
session.promise = runCodexLogin(config, log, session).finally(() => {
|
|
18
31
|
activeLogin = null;
|
|
19
32
|
});
|
|
33
|
+
activeLogin = session;
|
|
20
34
|
|
|
21
|
-
return await
|
|
35
|
+
return await session.promise.finally(() => {
|
|
36
|
+
session.notifiers.delete(notify);
|
|
37
|
+
});
|
|
22
38
|
}
|
|
23
39
|
|
|
24
|
-
async function runCodexLogin(config, log,
|
|
40
|
+
async function runCodexLogin(config, log, session) {
|
|
25
41
|
const [program, ...args] = splitCommand(config.codexLoginCommand);
|
|
26
42
|
|
|
27
|
-
await
|
|
43
|
+
await broadcastLogin(session, loginStatusMessage("starting", "Codex login is required. Starting device login process."));
|
|
28
44
|
log.info(`starting codex login: ${config.codexLoginCommand}`);
|
|
29
45
|
|
|
30
46
|
return await new Promise((resolve, reject) => {
|
|
@@ -36,20 +52,30 @@ async function runCodexLogin(config, log, notify) {
|
|
|
36
52
|
},
|
|
37
53
|
stdio: ["ignore", "pipe", "pipe"],
|
|
38
54
|
});
|
|
39
|
-
const state = {
|
|
40
|
-
buffer: "",
|
|
41
|
-
sentLoginInfo: false,
|
|
42
|
-
};
|
|
43
55
|
const timer = setTimeout(() => {
|
|
44
56
|
child.kill("SIGTERM");
|
|
45
57
|
reject(new Error(`Codex login timed out after ${config.codexLoginTimeoutMs}ms`));
|
|
46
58
|
}, config.codexLoginTimeoutMs);
|
|
47
59
|
|
|
48
60
|
const onData = (data) => {
|
|
49
|
-
const text = String(data);
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
61
|
+
const text = stripAnsi(String(data));
|
|
62
|
+
const trimmed = text.trimEnd();
|
|
63
|
+
log.debug(`[codex login] ${trimmed}`);
|
|
64
|
+
session.buffer = `${session.buffer}\n${text}`.slice(-8000);
|
|
65
|
+
|
|
66
|
+
if (trimmed) {
|
|
67
|
+
const notice = {
|
|
68
|
+
codexLogin: true,
|
|
69
|
+
status: "login_output",
|
|
70
|
+
output: trimmed,
|
|
71
|
+
text: trimmed,
|
|
72
|
+
};
|
|
73
|
+
session.outputTail.push(notice);
|
|
74
|
+
session.outputTail = session.outputTail.slice(-8);
|
|
75
|
+
void broadcastLogin(session, notice);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
void sendLoginInfo(session);
|
|
53
79
|
};
|
|
54
80
|
|
|
55
81
|
child.stdout.on("data", onData);
|
|
@@ -62,7 +88,7 @@ async function runCodexLogin(config, log, notify) {
|
|
|
62
88
|
clearTimeout(timer);
|
|
63
89
|
|
|
64
90
|
if (code === 0) {
|
|
65
|
-
void
|
|
91
|
+
void broadcastLogin(session, loginStatusMessage("complete", "Codex login completed. Retrying the request."));
|
|
66
92
|
resolve();
|
|
67
93
|
} else {
|
|
68
94
|
reject(new Error(`Codex login exited code=${code ?? "null"} signal=${signal ?? "null"}`));
|
|
@@ -71,19 +97,20 @@ async function runCodexLogin(config, log, notify) {
|
|
|
71
97
|
});
|
|
72
98
|
}
|
|
73
99
|
|
|
74
|
-
async function sendLoginInfo(
|
|
75
|
-
|
|
100
|
+
async function sendLoginInfo(session) {
|
|
101
|
+
const info = parseLoginInfo(session.buffer);
|
|
102
|
+
|
|
103
|
+
if (!info.url && !info.deviceId) {
|
|
76
104
|
return;
|
|
77
105
|
}
|
|
78
106
|
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
if (!info.url && !info.deviceId) {
|
|
107
|
+
const infoKey = `${info.url}|${info.deviceId}`;
|
|
108
|
+
if (infoKey === session.lastInfoKey) {
|
|
82
109
|
return;
|
|
83
110
|
}
|
|
84
111
|
|
|
85
|
-
|
|
86
|
-
await
|
|
112
|
+
session.lastInfoKey = infoKey;
|
|
113
|
+
await broadcastLogin(session, {
|
|
87
114
|
codexLogin: true,
|
|
88
115
|
status: "login_required",
|
|
89
116
|
...(info.url ? { url: info.url } : {}),
|
|
@@ -97,6 +124,33 @@ async function sendLoginInfo(state, notify) {
|
|
|
97
124
|
});
|
|
98
125
|
}
|
|
99
126
|
|
|
127
|
+
async function replayLoginState(session, notify) {
|
|
128
|
+
for (const notice of session.outputTail) {
|
|
129
|
+
await notify(notice);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const info = parseLoginInfo(session.buffer);
|
|
133
|
+
|
|
134
|
+
if (info.url || info.deviceId) {
|
|
135
|
+
await notify({
|
|
136
|
+
codexLogin: true,
|
|
137
|
+
status: "login_required",
|
|
138
|
+
...(info.url ? { url: info.url } : {}),
|
|
139
|
+
...(info.deviceId ? { code: info.deviceId, deviceId: info.deviceId } : {}),
|
|
140
|
+
text: [
|
|
141
|
+
"Codex login required.",
|
|
142
|
+
info.url ? `Open: ${info.url}` : "",
|
|
143
|
+
info.deviceId ? `Code: ${info.deviceId}` : "",
|
|
144
|
+
"After login completes, the runner will retry automatically.",
|
|
145
|
+
].filter(Boolean).join("\n"),
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function broadcastLogin(session, notice) {
|
|
151
|
+
await Promise.all([...session.notifiers].map((notify) => notify(notice).catch(() => undefined)));
|
|
152
|
+
}
|
|
153
|
+
|
|
100
154
|
function loginStatusMessage(status, text) {
|
|
101
155
|
return {
|
|
102
156
|
codexLogin: true,
|
|
@@ -106,13 +160,21 @@ function loginStatusMessage(status, text) {
|
|
|
106
160
|
}
|
|
107
161
|
|
|
108
162
|
function parseLoginInfo(text) {
|
|
109
|
-
const
|
|
163
|
+
const clean = stripAnsi(text);
|
|
164
|
+
const url = clean.match(/https?:\/\/[^\s"'<>]+/)?.[0]?.replace(/[),.;]+$/, "") ?? "";
|
|
165
|
+
const withoutUrls = clean.replace(/https?:\/\/[^\s"'<>]+/g, " ");
|
|
110
166
|
const labelledDeviceId =
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
167
|
+
firstDeviceCode([
|
|
168
|
+
...withoutUrls.matchAll(
|
|
169
|
+
/(?:device|user|login|verification|authorization)\s*(?:id|code)(?:\s+(?:is|:)|\s*[:=])\s*([A-Z0-9][A-Z0-9-]{4,})/gi,
|
|
170
|
+
),
|
|
171
|
+
].map((match) => match[1])) ?? "";
|
|
172
|
+
const fallbackCode =
|
|
173
|
+
firstDeviceCode([
|
|
174
|
+
...withoutUrls.matchAll(/\b[A-Z0-9]{4,}(?:-[A-Z0-9]{4,})+\b/g),
|
|
175
|
+
...withoutUrls.matchAll(/\b[A-Z0-9]{8,}\b/g),
|
|
176
|
+
].map((match) => match[0])) ?? "";
|
|
177
|
+
const deviceId = labelledDeviceId || fallbackCode;
|
|
116
178
|
|
|
117
179
|
return {
|
|
118
180
|
code: deviceId,
|
|
@@ -121,6 +183,32 @@ function parseLoginInfo(text) {
|
|
|
121
183
|
};
|
|
122
184
|
}
|
|
123
185
|
|
|
186
|
+
function stripAnsi(text) {
|
|
187
|
+
return String(text).replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, "");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function firstDeviceCode(candidates) {
|
|
191
|
+
return candidates
|
|
192
|
+
.map((candidate) => String(candidate || "").trim().replace(/[),.;]+$/, ""))
|
|
193
|
+
.find((candidate) => isDeviceCode(candidate));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function isDeviceCode(candidate) {
|
|
197
|
+
if (!candidate || candidate.length < 6) {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (!/^[A-Z0-9-]+$/.test(candidate)) {
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (!/[0-9]/.test(candidate) && !candidate.includes("-")) {
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return !/^(AUTHORIZATION|VERIFICATION|DEVICE|LOGIN|OPENAI|CODEX)$/i.test(candidate);
|
|
210
|
+
}
|
|
211
|
+
|
|
124
212
|
function splitCommand(command) {
|
|
125
213
|
const matches = command.match(/"[^"]*"|'[^']*'|\S+/g) ?? [];
|
|
126
214
|
return matches.map((part) => {
|