codexair 0.1.0
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/README.md +34 -0
- package/bin/codexair +3 -0
- package/dist/index.js +1312 -0
- package/package.json +33 -0
package/README.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# CodexAir CLI
|
|
2
|
+
|
|
3
|
+
Local CodexAir agent CLI. It connects Relay `/ws/agent` to `codex app-server`.
|
|
4
|
+
|
|
5
|
+
Install:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g codexair
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Run:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
codexair
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Required runtime dependencies:
|
|
18
|
+
|
|
19
|
+
1. Bun `>=1.3.0`
|
|
20
|
+
2. `codex` available on `PATH`
|
|
21
|
+
3. A saved CodexAir agent identity from Desktop Setup, or Relay env vars
|
|
22
|
+
|
|
23
|
+
Useful environment variables:
|
|
24
|
+
|
|
25
|
+
```text
|
|
26
|
+
REPO_PATH=/path/to/project
|
|
27
|
+
CODEX_CMD=codex
|
|
28
|
+
CODEX_MODEL=gpt-5.4
|
|
29
|
+
RELAY_URL=http://127.0.0.1:8788
|
|
30
|
+
RELAY_AGENT_TOKEN=...
|
|
31
|
+
RELAY_AGENT_NAME=...
|
|
32
|
+
CODEXAIR_AGENT_IDENTITY_PATH=~/.codexair/agent.json
|
|
33
|
+
```
|
|
34
|
+
|
package/bin/codexair
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,1312 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/taskSession.ts
|
|
3
|
+
function enterStarting(task, goal) {
|
|
4
|
+
task.status = "starting";
|
|
5
|
+
task.goal = goal;
|
|
6
|
+
task.lastText = undefined;
|
|
7
|
+
task.error = undefined;
|
|
8
|
+
task.threadId = undefined;
|
|
9
|
+
task.turnId = undefined;
|
|
10
|
+
task.updatedAt = Date.now();
|
|
11
|
+
}
|
|
12
|
+
function enterRunning(task, threadId, turnId) {
|
|
13
|
+
task.status = "running";
|
|
14
|
+
task.threadId = threadId;
|
|
15
|
+
if (turnId)
|
|
16
|
+
task.turnId = turnId;
|
|
17
|
+
task.updatedAt = Date.now();
|
|
18
|
+
}
|
|
19
|
+
function enterWaitingApproval(task, approval) {
|
|
20
|
+
task.status = "waiting_approval";
|
|
21
|
+
task.updatedAt = Date.now();
|
|
22
|
+
}
|
|
23
|
+
function returnToRunning(task) {
|
|
24
|
+
task.status = "running";
|
|
25
|
+
task.updatedAt = Date.now();
|
|
26
|
+
}
|
|
27
|
+
function enterDone(task, lastText) {
|
|
28
|
+
task.status = "done";
|
|
29
|
+
task.lastText = lastText;
|
|
30
|
+
task.error = undefined;
|
|
31
|
+
task.updatedAt = Date.now();
|
|
32
|
+
}
|
|
33
|
+
function enterFailed(task, error, lastText) {
|
|
34
|
+
task.status = "failed";
|
|
35
|
+
task.error = error;
|
|
36
|
+
if (lastText)
|
|
37
|
+
task.lastText = lastText;
|
|
38
|
+
task.updatedAt = Date.now();
|
|
39
|
+
}
|
|
40
|
+
function enterCancelled(task) {
|
|
41
|
+
task.status = "cancelled";
|
|
42
|
+
task.updatedAt = Date.now();
|
|
43
|
+
}
|
|
44
|
+
function isCancellable(status) {
|
|
45
|
+
return status === "running" || status === "waiting_approval";
|
|
46
|
+
}
|
|
47
|
+
function isTerminal(status) {
|
|
48
|
+
return status === "done" || status === "failed" || status === "cancelled";
|
|
49
|
+
}
|
|
50
|
+
function isActive(status) {
|
|
51
|
+
return status === "starting" || status === "running" || status === "waiting_approval";
|
|
52
|
+
}
|
|
53
|
+
function canStartNew(currentTask) {
|
|
54
|
+
return currentTask === null || isTerminal(currentTask.status);
|
|
55
|
+
}
|
|
56
|
+
function isSteerable(status) {
|
|
57
|
+
return status === "running";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// src/spokenSummary.ts
|
|
61
|
+
function clean(s = "", maxLen = 180) {
|
|
62
|
+
return s.replace(/\x1B\[[0-9;]*m/g, "").replace(/\s+/g, " ").trim().slice(0, maxLen) || "\u6709\u65B0\u7684\u4EFB\u52A1\u72B6\u6001\u3002";
|
|
63
|
+
}
|
|
64
|
+
function summarizeForLog(value, max = 800) {
|
|
65
|
+
if (value == null)
|
|
66
|
+
return "";
|
|
67
|
+
const raw = typeof value === "string" ? value : JSON.stringify(value, (_key, nestedValue) => typeof nestedValue === "bigint" ? nestedValue.toString() : nestedValue);
|
|
68
|
+
return clean(raw, max);
|
|
69
|
+
}
|
|
70
|
+
function summaryTaskStarted(goal) {
|
|
71
|
+
return "\u6536\u5230\u3002\u6211\u4F1A\u5F00\u59CB\u5904\u7406\uFF0C\u53EA\u5728\u5173\u952E\u8282\u70B9\u901A\u77E5\u4F60\u3002";
|
|
72
|
+
}
|
|
73
|
+
function summaryTaskDone(lastText) {
|
|
74
|
+
return clean(lastText || "\u4EFB\u52A1\u5B8C\u6210\u3002");
|
|
75
|
+
}
|
|
76
|
+
function summaryTaskFailed(error) {
|
|
77
|
+
return clean(error || "\u4EFB\u52A1\u5931\u8D25\u3002");
|
|
78
|
+
}
|
|
79
|
+
function summaryTaskCancelled() {
|
|
80
|
+
return "\u4EFB\u52A1\u5DF2\u53D6\u6D88\u3002";
|
|
81
|
+
}
|
|
82
|
+
function summaryStatusNone() {
|
|
83
|
+
return "\u5F53\u524D\u6CA1\u6709\u4EFB\u52A1\u3002";
|
|
84
|
+
}
|
|
85
|
+
function summaryStatusActive(goal, status) {
|
|
86
|
+
if (status === "waiting_approval") {
|
|
87
|
+
return clean(`\u4EFB\u52A1\u7B49\u5F85\u5BA1\u6279\uFF1A${goal}`);
|
|
88
|
+
}
|
|
89
|
+
return clean(`\u4EFB\u52A1\u6B63\u5728\u8FD0\u884C\uFF1A${goal}`);
|
|
90
|
+
}
|
|
91
|
+
function summaryStatusFinished(status, lastText, error) {
|
|
92
|
+
if (status === "cancelled")
|
|
93
|
+
return "\u4EFB\u52A1\u5DF2\u53D6\u6D88\u3002";
|
|
94
|
+
return clean(error || lastText || `\u4EFB\u52A1\u72B6\u6001\uFF1A${status}`);
|
|
95
|
+
}
|
|
96
|
+
function summaryApprovalReason(text) {
|
|
97
|
+
return clean(text || "Codex \u9700\u8981\u4F60\u7684\u786E\u8BA4\u624D\u80FD\u7EE7\u7EED\u3002");
|
|
98
|
+
}
|
|
99
|
+
function summarySteerAck() {
|
|
100
|
+
return "\u6536\u5230\uFF0C\u5DF2\u8865\u5145\u7ED9\u5F53\u524D\u4EFB\u52A1\u3002";
|
|
101
|
+
}
|
|
102
|
+
function summaryApprovalAccepted() {
|
|
103
|
+
return "\u5DF2\u786E\u8BA4\uFF0C\u7EE7\u7EED\u5904\u7406\u3002";
|
|
104
|
+
}
|
|
105
|
+
function summaryApprovalDeclined() {
|
|
106
|
+
return "\u5DF2\u62D2\u7EDD\uFF0C\u505C\u6B62\u8BE5\u64CD\u4F5C\u3002";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// src/codexRuntime.ts
|
|
110
|
+
function buildTurnInterruptParams(task) {
|
|
111
|
+
if (!task.threadId || !task.turnId)
|
|
112
|
+
return null;
|
|
113
|
+
return {
|
|
114
|
+
threadId: task.threadId,
|
|
115
|
+
turnId: task.turnId
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// src/agentName.ts
|
|
120
|
+
import { hostname } from "os";
|
|
121
|
+
var defaultAgentName = (host = hostname()) => {
|
|
122
|
+
const trimmed = host.trim();
|
|
123
|
+
return trimmed || "local-agent";
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
// src/relayClient.ts
|
|
127
|
+
var createRelayAgentConfig = (env) => {
|
|
128
|
+
const relayUrl = env.RELAY_URL?.trim();
|
|
129
|
+
const agentToken = env.RELAY_AGENT_TOKEN?.trim();
|
|
130
|
+
if (!relayUrl || !agentToken)
|
|
131
|
+
return null;
|
|
132
|
+
return {
|
|
133
|
+
relayUrl,
|
|
134
|
+
agentToken,
|
|
135
|
+
agentName: env.RELAY_AGENT_NAME?.trim() || defaultAgentName(),
|
|
136
|
+
reconnectMs: Number(env.RELAY_RECONNECT_MS ?? 3000)
|
|
137
|
+
};
|
|
138
|
+
};
|
|
139
|
+
var buildRelayAgentWsUrl = (relayUrl) => {
|
|
140
|
+
const url = new URL(relayUrl);
|
|
141
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
142
|
+
url.pathname = "/ws/agent";
|
|
143
|
+
url.search = "";
|
|
144
|
+
url.hash = "";
|
|
145
|
+
return url.toString();
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
class RelayAgentClient {
|
|
149
|
+
config;
|
|
150
|
+
deps;
|
|
151
|
+
ws = null;
|
|
152
|
+
closed = false;
|
|
153
|
+
constructor(config, deps) {
|
|
154
|
+
this.config = config;
|
|
155
|
+
this.deps = deps;
|
|
156
|
+
}
|
|
157
|
+
connect() {
|
|
158
|
+
this.closed = false;
|
|
159
|
+
const WebSocketImpl = this.deps.WebSocketImpl ?? globalThis.WebSocket;
|
|
160
|
+
const ws = new WebSocketImpl(buildRelayAgentWsUrl(this.config.relayUrl), {
|
|
161
|
+
headers: {
|
|
162
|
+
Authorization: `Bearer ${this.config.agentToken}`,
|
|
163
|
+
"X-Agent-Name": this.config.agentName
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
this.ws = ws;
|
|
167
|
+
ws.onopen = () => {
|
|
168
|
+
this.deps.onStatus?.("connected");
|
|
169
|
+
};
|
|
170
|
+
ws.onmessage = (event) => {
|
|
171
|
+
try {
|
|
172
|
+
const message = JSON.parse(String(event.data));
|
|
173
|
+
this.deps.onMessage(message);
|
|
174
|
+
} catch (error) {
|
|
175
|
+
this.deps.onStatus?.("error", error instanceof Error ? error.message : "Invalid relay message");
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
ws.onerror = (event) => {
|
|
179
|
+
this.deps.onStatus?.("error", String(event));
|
|
180
|
+
};
|
|
181
|
+
ws.onclose = () => {
|
|
182
|
+
this.ws = null;
|
|
183
|
+
this.deps.onStatus?.("disconnected");
|
|
184
|
+
if (this.closed)
|
|
185
|
+
return;
|
|
186
|
+
const setTimeoutImpl = this.deps.setTimeoutImpl ?? setTimeout;
|
|
187
|
+
setTimeoutImpl(() => this.connect(), this.config.reconnectMs);
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
send(type, payload = {}) {
|
|
191
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN)
|
|
192
|
+
return false;
|
|
193
|
+
this.ws.send(JSON.stringify({ type, payload }));
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
close() {
|
|
197
|
+
this.closed = true;
|
|
198
|
+
this.ws?.close();
|
|
199
|
+
this.ws = null;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// src/codexHistory.ts
|
|
204
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "fs";
|
|
205
|
+
import { homedir } from "os";
|
|
206
|
+
import { basename, join } from "path";
|
|
207
|
+
var DEFAULT_LIMIT = 80;
|
|
208
|
+
var DUPLICATE_HISTORY_MESSAGE_WINDOW_MS = 1e4;
|
|
209
|
+
var shortText = (value, max = 80) => {
|
|
210
|
+
const normalized = value.trim().replace(/\s+/g, " ");
|
|
211
|
+
if (normalized.length <= max)
|
|
212
|
+
return normalized;
|
|
213
|
+
return normalized.slice(0, max - 1) + "\u2026";
|
|
214
|
+
};
|
|
215
|
+
var messageSignature = (message) => `${message.role}|${message.text.trim().replace(/\s+/g, " ")}`;
|
|
216
|
+
var projectNameFromPath = (path) => basename(path.replace(/\/$/, "")) || path;
|
|
217
|
+
var collectJsonlFiles = (root) => {
|
|
218
|
+
if (!existsSync(root))
|
|
219
|
+
return [];
|
|
220
|
+
const entries = [];
|
|
221
|
+
const walk = (dir) => {
|
|
222
|
+
for (const name of readdirSync(dir)) {
|
|
223
|
+
const fullPath = join(dir, name);
|
|
224
|
+
const info = statSync(fullPath);
|
|
225
|
+
if (info.isDirectory()) {
|
|
226
|
+
walk(fullPath);
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
if (name.endsWith(".jsonl")) {
|
|
230
|
+
entries.push(fullPath);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
walk(root);
|
|
235
|
+
return entries;
|
|
236
|
+
};
|
|
237
|
+
var timestampToMs = (value) => {
|
|
238
|
+
if (typeof value !== "string")
|
|
239
|
+
return 0;
|
|
240
|
+
const ms = Date.parse(value);
|
|
241
|
+
return Number.isFinite(ms) ? ms : 0;
|
|
242
|
+
};
|
|
243
|
+
var readStringContent = (content) => {
|
|
244
|
+
if (typeof content === "string")
|
|
245
|
+
return content;
|
|
246
|
+
if (!Array.isArray(content))
|
|
247
|
+
return "";
|
|
248
|
+
return content.map((item) => {
|
|
249
|
+
if (!item || typeof item !== "object")
|
|
250
|
+
return "";
|
|
251
|
+
const record = item;
|
|
252
|
+
return typeof record.text === "string" ? record.text : "";
|
|
253
|
+
}).filter(Boolean).join("");
|
|
254
|
+
};
|
|
255
|
+
var isBootstrapContext = (text) => {
|
|
256
|
+
const normalized = text.trim();
|
|
257
|
+
return normalized.startsWith("# AGENTS.md instructions") || normalized.startsWith("<environment_context>") || normalized.includes("<INSTRUCTIONS>") || normalized.includes("You are Codex, a coding agent");
|
|
258
|
+
};
|
|
259
|
+
var isAutomationSession = (text) => {
|
|
260
|
+
const normalized = text.trim();
|
|
261
|
+
return normalized.startsWith("Automation:") || normalized.includes("Automation ID:") || normalized.includes("$CODEX_HOME/automations/");
|
|
262
|
+
};
|
|
263
|
+
var isInternalPromptSession = (text) => {
|
|
264
|
+
const normalized = text.trim();
|
|
265
|
+
return normalized.startsWith("You are the coding execution layer behind CodexAir") || normalized.startsWith("User goal:") || normalized.includes(`
|
|
266
|
+
User goal:
|
|
267
|
+
`);
|
|
268
|
+
};
|
|
269
|
+
var parseArchivedSession = (filePath) => {
|
|
270
|
+
let id = basename(filePath).replace(/\.jsonl$/, "");
|
|
271
|
+
let cwd;
|
|
272
|
+
let firstUserMessage = "";
|
|
273
|
+
let updatedAt = 0;
|
|
274
|
+
for (const line of readFileSync(filePath, "utf8").split(`
|
|
275
|
+
`)) {
|
|
276
|
+
if (!line.trim())
|
|
277
|
+
continue;
|
|
278
|
+
let obj;
|
|
279
|
+
try {
|
|
280
|
+
obj = JSON.parse(line);
|
|
281
|
+
} catch {
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
updatedAt = Math.max(updatedAt, timestampToMs(obj.timestamp));
|
|
285
|
+
const type = obj.type;
|
|
286
|
+
const payload = obj.payload;
|
|
287
|
+
if (!payload)
|
|
288
|
+
continue;
|
|
289
|
+
if (type === "session_meta") {
|
|
290
|
+
if (typeof payload.id === "string" && payload.id.trim())
|
|
291
|
+
id = payload.id;
|
|
292
|
+
if (typeof payload.cwd === "string" && payload.cwd.trim())
|
|
293
|
+
cwd = payload.cwd;
|
|
294
|
+
updatedAt = Math.max(updatedAt, timestampToMs(payload.timestamp));
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
if (type === "event_msg" && payload.type === "user_message") {
|
|
298
|
+
if (typeof payload.message === "string")
|
|
299
|
+
firstUserMessage = payload.message;
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
if (!firstUserMessage && type === "response_item") {
|
|
303
|
+
if (payload.role === "user") {
|
|
304
|
+
const content = readStringContent(payload.content);
|
|
305
|
+
if (content && !isBootstrapContext(content)) {
|
|
306
|
+
firstUserMessage = content;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (!cwd && !firstUserMessage)
|
|
312
|
+
return null;
|
|
313
|
+
const text = shortText(firstUserMessage || "Codex \u4F1A\u8BDD");
|
|
314
|
+
if (isAutomationSession(text) || isInternalPromptSession(text))
|
|
315
|
+
return null;
|
|
316
|
+
return {
|
|
317
|
+
id,
|
|
318
|
+
cwd,
|
|
319
|
+
text,
|
|
320
|
+
updatedAt: updatedAt || statSync(filePath).mtimeMs,
|
|
321
|
+
filePath
|
|
322
|
+
};
|
|
323
|
+
};
|
|
324
|
+
var collectSessionFiles = (options = {}) => {
|
|
325
|
+
const archiveDir = options.archiveDir ?? join(homedir(), ".codex", "archived_sessions");
|
|
326
|
+
const sessionsDir = options.sessionsDir ?? join(homedir(), ".codex", "sessions");
|
|
327
|
+
const limit = options.limit ?? DEFAULT_LIMIT;
|
|
328
|
+
return [...collectJsonlFiles(archiveDir), ...collectJsonlFiles(sessionsDir)].sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs).slice(0, limit);
|
|
329
|
+
};
|
|
330
|
+
var loadCodexHistory = (options = {}) => {
|
|
331
|
+
const sessionFiles = collectSessionFiles(options);
|
|
332
|
+
if (sessionFiles.length === 0) {
|
|
333
|
+
return { projects: [], sessions: [] };
|
|
334
|
+
}
|
|
335
|
+
const parsed = sessionFiles.map(parseArchivedSession).filter((item) => item !== null).sort((a, b) => b.updatedAt - a.updatedAt);
|
|
336
|
+
const sessions = parsed.map((session) => {
|
|
337
|
+
const projectId = session.cwd;
|
|
338
|
+
const projectName = projectId ? projectNameFromPath(projectId) : undefined;
|
|
339
|
+
return {
|
|
340
|
+
id: session.id,
|
|
341
|
+
title: session.text,
|
|
342
|
+
project_id: projectId,
|
|
343
|
+
project_name: projectName,
|
|
344
|
+
status: "history",
|
|
345
|
+
thread_id: session.id,
|
|
346
|
+
text: session.text,
|
|
347
|
+
last_summary: session.text,
|
|
348
|
+
waiting_approval: false,
|
|
349
|
+
updated_at: session.updatedAt
|
|
350
|
+
};
|
|
351
|
+
});
|
|
352
|
+
const dedupedSessions = [];
|
|
353
|
+
const seenSessionIds = new Set;
|
|
354
|
+
for (const session of sessions) {
|
|
355
|
+
if (seenSessionIds.has(session.id))
|
|
356
|
+
continue;
|
|
357
|
+
seenSessionIds.add(session.id);
|
|
358
|
+
dedupedSessions.push(session);
|
|
359
|
+
}
|
|
360
|
+
const projectMap = new Map;
|
|
361
|
+
for (const session of dedupedSessions) {
|
|
362
|
+
const projectId = session.project_id ?? "unknown";
|
|
363
|
+
const existing = projectMap.get(projectId);
|
|
364
|
+
if (!existing) {
|
|
365
|
+
projectMap.set(projectId, {
|
|
366
|
+
id: projectId,
|
|
367
|
+
name: session.project_name ?? "\u672A\u77E5\u9879\u76EE",
|
|
368
|
+
repo_path: session.project_id,
|
|
369
|
+
conversation_count: 1,
|
|
370
|
+
last_summary: session.last_summary,
|
|
371
|
+
updated_at: session.updated_at
|
|
372
|
+
});
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
existing.conversation_count += 1;
|
|
376
|
+
if (session.updated_at > existing.updated_at) {
|
|
377
|
+
existing.updated_at = session.updated_at;
|
|
378
|
+
existing.last_summary = session.last_summary;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return {
|
|
382
|
+
projects: [...projectMap.values()].sort((a, b) => b.updated_at - a.updated_at),
|
|
383
|
+
sessions: dedupedSessions
|
|
384
|
+
};
|
|
385
|
+
};
|
|
386
|
+
var loadCodexSessionMessages = (sessionId, options = {}) => {
|
|
387
|
+
const parsed = collectSessionFiles(options).map(parseArchivedSession).filter((item) => item !== null);
|
|
388
|
+
const session = parsed.find((item) => item.id === sessionId);
|
|
389
|
+
if (!session)
|
|
390
|
+
return [];
|
|
391
|
+
const messages = [];
|
|
392
|
+
let nextId = 0;
|
|
393
|
+
for (const line of readFileSync(session.filePath, "utf8").split(`
|
|
394
|
+
`)) {
|
|
395
|
+
if (!line.trim())
|
|
396
|
+
continue;
|
|
397
|
+
let obj;
|
|
398
|
+
try {
|
|
399
|
+
obj = JSON.parse(line);
|
|
400
|
+
} catch {
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
const timestamp = timestampToMs(obj.timestamp) || session.updatedAt;
|
|
404
|
+
const type = obj.type;
|
|
405
|
+
const payload = obj.payload;
|
|
406
|
+
if (!payload)
|
|
407
|
+
continue;
|
|
408
|
+
if (type === "event_msg" && payload.type === "user_message" && typeof payload.message === "string") {
|
|
409
|
+
const text = payload.message.trim();
|
|
410
|
+
if (text && !isBootstrapContext(text) && !isAutomationSession(text)) {
|
|
411
|
+
messages.push({
|
|
412
|
+
id: `${sessionId}-m-${nextId++}`,
|
|
413
|
+
role: "user",
|
|
414
|
+
text,
|
|
415
|
+
created_at: timestamp
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
if (type === "response_item" && payload.type === "message") {
|
|
421
|
+
const role = payload.role;
|
|
422
|
+
if (role !== "user" && role !== "assistant")
|
|
423
|
+
continue;
|
|
424
|
+
const text = readStringContent(payload.content).trim();
|
|
425
|
+
if (!text || isBootstrapContext(text))
|
|
426
|
+
continue;
|
|
427
|
+
messages.push({
|
|
428
|
+
id: `${sessionId}-m-${nextId++}`,
|
|
429
|
+
role,
|
|
430
|
+
text,
|
|
431
|
+
created_at: timestamp
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
const seen = new Map;
|
|
436
|
+
const deduped = [];
|
|
437
|
+
for (const message of messages.sort((a, b) => a.created_at - b.created_at)) {
|
|
438
|
+
const signature = messageSignature(message);
|
|
439
|
+
const lastSeenAt = seen.get(signature);
|
|
440
|
+
if (lastSeenAt !== undefined && Math.abs(message.created_at - lastSeenAt) < DUPLICATE_HISTORY_MESSAGE_WINDOW_MS) {
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
seen.set(signature, message.created_at);
|
|
444
|
+
deduped.push(message);
|
|
445
|
+
}
|
|
446
|
+
return deduped.slice(-80);
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
// src/agentIdentity.ts
|
|
450
|
+
import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
451
|
+
import { homedir as homedir2 } from "os";
|
|
452
|
+
import { dirname, join as join2 } from "path";
|
|
453
|
+
var normalizeRelayUrl = (value) => {
|
|
454
|
+
const trimmed = value.trim().replace(/\/$/, "");
|
|
455
|
+
if (!trimmed)
|
|
456
|
+
return "";
|
|
457
|
+
if (trimmed.startsWith("http://") || trimmed.startsWith("https://"))
|
|
458
|
+
return trimmed;
|
|
459
|
+
if (trimmed.startsWith("ws://"))
|
|
460
|
+
return trimmed.replace(/^ws:\/\//, "http://");
|
|
461
|
+
if (trimmed.startsWith("wss://"))
|
|
462
|
+
return trimmed.replace(/^wss:\/\//, "https://");
|
|
463
|
+
return `http://${trimmed}`;
|
|
464
|
+
};
|
|
465
|
+
var defaultAgentIdentityPath = (home = homedir2()) => join2(home, ".codexair", "agent.json");
|
|
466
|
+
var loadSavedAgentIdentity = (identityPath) => {
|
|
467
|
+
if (!existsSync2(identityPath))
|
|
468
|
+
return null;
|
|
469
|
+
try {
|
|
470
|
+
const parsed = JSON.parse(readFileSync2(identityPath, "utf8"));
|
|
471
|
+
if (typeof parsed.relayUrl !== "string" || typeof parsed.agentId !== "string" || typeof parsed.agentToken !== "string" || typeof parsed.agentName !== "string" || !parsed.relayUrl.trim() || !parsed.agentId.trim() || !parsed.agentToken.trim()) {
|
|
472
|
+
return null;
|
|
473
|
+
}
|
|
474
|
+
return {
|
|
475
|
+
relayUrl: normalizeRelayUrl(parsed.relayUrl),
|
|
476
|
+
agentId: parsed.agentId,
|
|
477
|
+
agentToken: parsed.agentToken,
|
|
478
|
+
agentName: parsed.agentName || defaultAgentName()
|
|
479
|
+
};
|
|
480
|
+
} catch {
|
|
481
|
+
return null;
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
// src/builtinReply.ts
|
|
486
|
+
var BUILTIN_REPLIES = new Map([
|
|
487
|
+
["say_hello", "\u4F60\u597D\uFF0C\u6211\u5728\u3002\u9700\u8981\u6211\u505A\u4EC0\u4E48\uFF1F"],
|
|
488
|
+
["say hello", "\u4F60\u597D\uFF0C\u6211\u5728\u3002\u9700\u8981\u6211\u505A\u4EC0\u4E48\uFF1F"],
|
|
489
|
+
["testchat", "\u804A\u5929\u94FE\u8DEF\u6B63\u5E38\u3002\u4F60\u53EF\u4EE5\u7EE7\u7EED\u53D1\u4EFB\u52A1\u3002"],
|
|
490
|
+
["test chat", "\u804A\u5929\u94FE\u8DEF\u6B63\u5E38\u3002\u4F60\u53EF\u4EE5\u7EE7\u7EED\u53D1\u4EFB\u52A1\u3002"],
|
|
491
|
+
["test_chat", "\u804A\u5929\u94FE\u8DEF\u6B63\u5E38\u3002\u4F60\u53EF\u4EE5\u7EE7\u7EED\u53D1\u4EFB\u52A1\u3002"]
|
|
492
|
+
]);
|
|
493
|
+
var normalizeBuiltinInput = (text) => text.trim().toLowerCase().replace(/\s+/g, " ");
|
|
494
|
+
var resolveBuiltinReply = (text) => {
|
|
495
|
+
return BUILTIN_REPLIES.get(normalizeBuiltinInput(text)) ?? null;
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
// src/codexAppServerConfig.ts
|
|
499
|
+
var CONFIG_ERROR = "CODEX_APP_SERVER_CONFIG must be a JSON string array or newline-delimited key=value lines.";
|
|
500
|
+
var configKeyFromEntry = (entry) => {
|
|
501
|
+
const index = entry.indexOf("=");
|
|
502
|
+
return index >= 0 ? entry.slice(0, index).trim() : entry.trim();
|
|
503
|
+
};
|
|
504
|
+
var normalizeConfigEntry = (entry) => {
|
|
505
|
+
const trimmed = entry.trim();
|
|
506
|
+
const index = trimmed.indexOf("=");
|
|
507
|
+
if (index <= 0 || index === trimmed.length - 1) {
|
|
508
|
+
throw new Error(`${CONFIG_ERROR} Invalid entry: ${trimmed || "(empty)"}`);
|
|
509
|
+
}
|
|
510
|
+
const key = trimmed.slice(0, index).trim();
|
|
511
|
+
const value = trimmed.slice(index + 1).trim();
|
|
512
|
+
if (!key || !value) {
|
|
513
|
+
throw new Error(`${CONFIG_ERROR} Invalid entry: ${trimmed || "(empty)"}`);
|
|
514
|
+
}
|
|
515
|
+
return `${key}=${value}`;
|
|
516
|
+
};
|
|
517
|
+
var parseCodexAppServerConfig = (raw) => {
|
|
518
|
+
if (!raw?.trim())
|
|
519
|
+
return [];
|
|
520
|
+
const trimmed = raw.trim();
|
|
521
|
+
let entries;
|
|
522
|
+
if (trimmed.startsWith("[")) {
|
|
523
|
+
let parsed;
|
|
524
|
+
try {
|
|
525
|
+
parsed = JSON.parse(trimmed);
|
|
526
|
+
} catch {
|
|
527
|
+
throw new Error(CONFIG_ERROR);
|
|
528
|
+
}
|
|
529
|
+
if (!Array.isArray(parsed) || parsed.some((entry) => typeof entry !== "string")) {
|
|
530
|
+
throw new Error(CONFIG_ERROR);
|
|
531
|
+
}
|
|
532
|
+
entries = parsed;
|
|
533
|
+
} else {
|
|
534
|
+
entries = trimmed.split(/\r?\n/);
|
|
535
|
+
}
|
|
536
|
+
return entries.map((entry) => entry.trim()).filter((entry) => entry && !entry.startsWith("#")).map(normalizeConfigEntry);
|
|
537
|
+
};
|
|
538
|
+
var cloneEnv = (env) => Object.fromEntries(Object.entries(env).filter((entry) => typeof entry[1] === "string"));
|
|
539
|
+
var buildCodexAppServerLaunch = (codexCmd, env) => {
|
|
540
|
+
const configEntries = parseCodexAppServerConfig(env.CODEX_APP_SERVER_CONFIG);
|
|
541
|
+
const cmd = [codexCmd, "app-server"];
|
|
542
|
+
for (const entry of configEntries) {
|
|
543
|
+
cmd.push("-c", entry);
|
|
544
|
+
}
|
|
545
|
+
cmd.push("--listen", "stdio://");
|
|
546
|
+
return {
|
|
547
|
+
cmd,
|
|
548
|
+
env: cloneEnv(env),
|
|
549
|
+
configEntries,
|
|
550
|
+
configKeys: configEntries.map(configKeyFromEntry)
|
|
551
|
+
};
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
// index.ts
|
|
555
|
+
var REPO_PATH = Bun.env.REPO_PATH ?? process.cwd();
|
|
556
|
+
var CODEX_CMD = Bun.env.CODEX_CMD ?? "codex";
|
|
557
|
+
var CODEX_MODEL = Bun.env.CODEX_MODEL ?? "gpt-5.4";
|
|
558
|
+
var CODEX_LOG_MAX_CHARS = Number(Bun.env.CODEX_LOG_MAX_CHARS ?? 800);
|
|
559
|
+
var AGENT_IDENTITY_PATH = Bun.env.CODEXAIR_AGENT_IDENTITY_PATH?.trim() || defaultAgentIdentityPath();
|
|
560
|
+
var relayConfigFromEnv = createRelayAgentConfig(Bun.env);
|
|
561
|
+
var codexLaunch = buildCodexAppServerLaunch(CODEX_CMD, Bun.env);
|
|
562
|
+
var TELEMETRY_MAX = 1000;
|
|
563
|
+
var telemetryEvents = [];
|
|
564
|
+
var trace = (type, data = {}) => {
|
|
565
|
+
const ev = { ts: Date.now(), type, task_id: task?.id, data };
|
|
566
|
+
telemetryEvents.push(ev);
|
|
567
|
+
if (telemetryEvents.length > TELEMETRY_MAX)
|
|
568
|
+
telemetryEvents.shift();
|
|
569
|
+
};
|
|
570
|
+
var rpcLatencies = [];
|
|
571
|
+
var MAX_LATENCIES = 500;
|
|
572
|
+
var recordLatency = (method, ms, ok) => {
|
|
573
|
+
rpcLatencies.push({ method, ms, ok });
|
|
574
|
+
if (rpcLatencies.length > MAX_LATENCIES)
|
|
575
|
+
rpcLatencies.shift();
|
|
576
|
+
};
|
|
577
|
+
var task = null;
|
|
578
|
+
var pendingApproval = null;
|
|
579
|
+
var lastProgressAt = 0;
|
|
580
|
+
var activeRequestId;
|
|
581
|
+
var taskRequestIds = new Map;
|
|
582
|
+
var sessionHistory = [];
|
|
583
|
+
var relayClient = null;
|
|
584
|
+
var resolveRelayConfig = async () => {
|
|
585
|
+
if (relayConfigFromEnv)
|
|
586
|
+
return relayConfigFromEnv;
|
|
587
|
+
const savedIdentity = loadSavedAgentIdentity(AGENT_IDENTITY_PATH);
|
|
588
|
+
if (savedIdentity) {
|
|
589
|
+
return {
|
|
590
|
+
relayUrl: savedIdentity.relayUrl,
|
|
591
|
+
agentToken: savedIdentity.agentToken,
|
|
592
|
+
agentName: savedIdentity.agentName,
|
|
593
|
+
reconnectMs: Number(Bun.env.RELAY_RECONNECT_MS ?? 3000)
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
const relayUrl = Bun.env.RELAY_URL?.trim();
|
|
597
|
+
if (!relayUrl)
|
|
598
|
+
return null;
|
|
599
|
+
console.warn("No agent identity found.");
|
|
600
|
+
console.warn("Open Desktop Setup, sign in as owner, and register this computer as an agent.");
|
|
601
|
+
return null;
|
|
602
|
+
};
|
|
603
|
+
var emit = (type, payload = {}) => {
|
|
604
|
+
const taskId = typeof payload.task_id === "string" ? payload.task_id : undefined;
|
|
605
|
+
const requestId = activeRequestId ?? (taskId ? taskRequestIds.get(taskId) : undefined);
|
|
606
|
+
if (requestId && !payload.request_id) {
|
|
607
|
+
payload.request_id = requestId;
|
|
608
|
+
}
|
|
609
|
+
relayClient?.send(type, payload);
|
|
610
|
+
trace("ws_out:" + type, payload);
|
|
611
|
+
};
|
|
612
|
+
var speak = (text, priority = "normal", extra = {}) => emit("speak", { text: clean(text), priority, ...extra });
|
|
613
|
+
var upsertSessionHistory = (snapshot) => {
|
|
614
|
+
const index = sessionHistory.findIndex((item) => item.id == snapshot.id);
|
|
615
|
+
if (index >= 0) {
|
|
616
|
+
sessionHistory[index] = snapshot;
|
|
617
|
+
} else {
|
|
618
|
+
sessionHistory.unshift(snapshot);
|
|
619
|
+
}
|
|
620
|
+
if (sessionHistory.length > 12)
|
|
621
|
+
sessionHistory.length = 12;
|
|
622
|
+
};
|
|
623
|
+
var progress = (text) => {
|
|
624
|
+
const now = Date.now();
|
|
625
|
+
if (now - lastProgressAt < 30000)
|
|
626
|
+
return;
|
|
627
|
+
lastProgressAt = now;
|
|
628
|
+
emit("progress", { task_id: task?.id, thread_id: task?.threadId, text: clean(text) });
|
|
629
|
+
};
|
|
630
|
+
var promptFor = (goal) => `
|
|
631
|
+
You are the coding execution layer behind CodexAir, a remote-first async coding agent.
|
|
632
|
+
|
|
633
|
+
User goal:
|
|
634
|
+
${goal}
|
|
635
|
+
|
|
636
|
+
Rules:
|
|
637
|
+
- Work inside the current repository.
|
|
638
|
+
- Do not push, publish, deploy, or perform destructive operations.
|
|
639
|
+
- Ask for approval before risky actions.
|
|
640
|
+
- Prefer small, safe changes.
|
|
641
|
+
- Run relevant tests when possible.
|
|
642
|
+
- Final answer must be concise and suitable for a mobile chat update.
|
|
643
|
+
|
|
644
|
+
End with:
|
|
645
|
+
1. What changed.
|
|
646
|
+
2. Whether tests passed.
|
|
647
|
+
3. Whether user approval is needed.
|
|
648
|
+
`.trim();
|
|
649
|
+
|
|
650
|
+
class Codex {
|
|
651
|
+
proc = Bun.spawn({
|
|
652
|
+
cmd: codexLaunch.cmd,
|
|
653
|
+
stdin: "pipe",
|
|
654
|
+
stdout: "pipe",
|
|
655
|
+
stderr: "pipe",
|
|
656
|
+
env: codexLaunch.env,
|
|
657
|
+
onExit(proc, exitCode, signalCode, error) {
|
|
658
|
+
const reason = signalCode ? `signal ${signalCode}` : `exit code ${exitCode}`;
|
|
659
|
+
console.error(`[codex] process exited: ${reason}${error ? ` (${error.message})` : ""}`);
|
|
660
|
+
}
|
|
661
|
+
});
|
|
662
|
+
nextId = 1;
|
|
663
|
+
ready = false;
|
|
664
|
+
waiters = new Map;
|
|
665
|
+
constructor() {
|
|
666
|
+
this.readLines(this.proc.stdout, (line) => this.onMessage(JSON.parse(line)));
|
|
667
|
+
this.readLines(this.proc.stderr, (line) => console.error("[codex]", line));
|
|
668
|
+
}
|
|
669
|
+
async init() {
|
|
670
|
+
if (this.ready)
|
|
671
|
+
return;
|
|
672
|
+
await this.call("initialize", {
|
|
673
|
+
clientInfo: { name: "codexair", title: "CodexAir", version: "0.1.0" },
|
|
674
|
+
capabilities: { experimentalApi: true }
|
|
675
|
+
});
|
|
676
|
+
this.notify("initialized");
|
|
677
|
+
this.ready = true;
|
|
678
|
+
}
|
|
679
|
+
async start(goal) {
|
|
680
|
+
await this.init();
|
|
681
|
+
const thread = await this.call("thread/start", { model: CODEX_MODEL });
|
|
682
|
+
const threadId = thread.thread?.id ?? thread.threadId;
|
|
683
|
+
await this.call("turn/start", {
|
|
684
|
+
threadId,
|
|
685
|
+
cwd: REPO_PATH,
|
|
686
|
+
approvalPolicy: "on-request",
|
|
687
|
+
sandbox: "workspaceWrite",
|
|
688
|
+
input: [{ type: "text", text: promptFor(goal) }]
|
|
689
|
+
});
|
|
690
|
+
return threadId;
|
|
691
|
+
}
|
|
692
|
+
async resumeThread(threadId) {
|
|
693
|
+
await this.init();
|
|
694
|
+
const resumed = await this.call("thread/resume", { threadId });
|
|
695
|
+
return resumed.thread?.id ?? threadId;
|
|
696
|
+
}
|
|
697
|
+
async continueThread(threadId, text) {
|
|
698
|
+
await this.call("turn/start", {
|
|
699
|
+
threadId,
|
|
700
|
+
input: [{ type: "text", text }]
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
async steer(text) {
|
|
704
|
+
if (!task?.threadId)
|
|
705
|
+
throw new Error("No active thread");
|
|
706
|
+
await this.call("turn/steer", {
|
|
707
|
+
threadId: task.threadId,
|
|
708
|
+
input: [{ type: "text", text }]
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
async interrupt() {
|
|
712
|
+
if (!task)
|
|
713
|
+
return;
|
|
714
|
+
const params = buildTurnInterruptParams(task);
|
|
715
|
+
if (!params)
|
|
716
|
+
return;
|
|
717
|
+
await this.call("turn/interrupt", params);
|
|
718
|
+
}
|
|
719
|
+
answer(id, result) {
|
|
720
|
+
this.write({ id, result });
|
|
721
|
+
}
|
|
722
|
+
call(method, params = {}) {
|
|
723
|
+
const id = this.nextId++;
|
|
724
|
+
const start = Date.now();
|
|
725
|
+
this.write({ id, method, params });
|
|
726
|
+
return new Promise((ok, fail) => {
|
|
727
|
+
this.waiters.set(id, {
|
|
728
|
+
ok: (v) => {
|
|
729
|
+
recordLatency(method, Date.now() - start, true);
|
|
730
|
+
trace("rpc_latency", { method, ms: Date.now() - start, ok: true });
|
|
731
|
+
ok(v);
|
|
732
|
+
},
|
|
733
|
+
fail: (e) => {
|
|
734
|
+
recordLatency(method, Date.now() - start, false);
|
|
735
|
+
trace("rpc_latency", { method, ms: Date.now() - start, ok: false });
|
|
736
|
+
fail(e);
|
|
737
|
+
}
|
|
738
|
+
});
|
|
739
|
+
setTimeout(() => {
|
|
740
|
+
if (!this.waiters.delete(id))
|
|
741
|
+
return;
|
|
742
|
+
recordLatency(method, 120000, false);
|
|
743
|
+
fail(new Error(`Codex timeout: ${method}`));
|
|
744
|
+
}, 120000);
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
notify(method, params = {}) {
|
|
748
|
+
this.write({ method, params });
|
|
749
|
+
}
|
|
750
|
+
write(msg) {
|
|
751
|
+
this.proc.stdin.write(`${JSON.stringify(msg)}
|
|
752
|
+
`);
|
|
753
|
+
}
|
|
754
|
+
async readLines(stream, onLine) {
|
|
755
|
+
const reader = stream.getReader();
|
|
756
|
+
const decoder = new TextDecoder;
|
|
757
|
+
let buf = "";
|
|
758
|
+
while (true) {
|
|
759
|
+
const { value, done } = await reader.read();
|
|
760
|
+
if (done)
|
|
761
|
+
break;
|
|
762
|
+
buf += decoder.decode(value, { stream: true });
|
|
763
|
+
for (;; ) {
|
|
764
|
+
const i = buf.indexOf(`
|
|
765
|
+
`);
|
|
766
|
+
if (i < 0)
|
|
767
|
+
break;
|
|
768
|
+
const line = buf.slice(0, i).trim();
|
|
769
|
+
buf = buf.slice(i + 1);
|
|
770
|
+
if (line)
|
|
771
|
+
onLine(line);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
onMessage(msg) {
|
|
776
|
+
this.logMessage(msg);
|
|
777
|
+
if (msg.id != null && !msg.method)
|
|
778
|
+
return this.onResponse(msg);
|
|
779
|
+
if (msg.id != null && msg.method)
|
|
780
|
+
return this.onServerRequest(msg);
|
|
781
|
+
if (msg.method)
|
|
782
|
+
return this.onEvent(msg.method, msg.params ?? {});
|
|
783
|
+
}
|
|
784
|
+
logMessage(msg) {
|
|
785
|
+
if (msg.id != null && !msg.method) {
|
|
786
|
+
if (msg.error) {
|
|
787
|
+
console.log("[codex:response:error]", {
|
|
788
|
+
id: msg.id,
|
|
789
|
+
code: msg.error.code,
|
|
790
|
+
message: msg.error.message,
|
|
791
|
+
data: summarizeForLog(msg.error.data)
|
|
792
|
+
});
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
console.log("[codex:response]", {
|
|
796
|
+
id: msg.id,
|
|
797
|
+
result: summarizeForLog(msg.result)
|
|
798
|
+
});
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
if (msg.id != null && msg.method) {
|
|
802
|
+
console.log("[codex:request]", {
|
|
803
|
+
id: msg.id,
|
|
804
|
+
method: msg.method,
|
|
805
|
+
params: summarizeForLog(msg.params)
|
|
806
|
+
});
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
if (!msg.method)
|
|
810
|
+
return;
|
|
811
|
+
const delta = msg.params?.delta ?? msg.params?.text;
|
|
812
|
+
if (msg.method === "item/agentMessage/delta" && typeof delta === "string") {
|
|
813
|
+
console.log("[codex:text]", clean(delta, CODEX_LOG_MAX_CHARS));
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
console.log("[codex:event]", {
|
|
817
|
+
method: msg.method,
|
|
818
|
+
params: summarizeForLog(msg.params)
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
onResponse(msg) {
|
|
822
|
+
const waiter = this.waiters.get(msg.id);
|
|
823
|
+
if (!waiter)
|
|
824
|
+
return;
|
|
825
|
+
this.waiters.delete(msg.id);
|
|
826
|
+
msg.error ? waiter.fail(new Error(msg.error.message)) : waiter.ok(msg.result);
|
|
827
|
+
}
|
|
828
|
+
onServerRequest(msg) {
|
|
829
|
+
pendingApproval = {
|
|
830
|
+
id: msg.id,
|
|
831
|
+
method: msg.method,
|
|
832
|
+
params: msg.params ?? {}
|
|
833
|
+
};
|
|
834
|
+
if (task) {
|
|
835
|
+
enterWaitingApproval(task, pendingApproval);
|
|
836
|
+
}
|
|
837
|
+
trace("approval_requested", { method: msg.method });
|
|
838
|
+
emit("needs_approval", {
|
|
839
|
+
task_id: task?.id,
|
|
840
|
+
thread_id: task?.threadId,
|
|
841
|
+
request_id: msg.id,
|
|
842
|
+
text: summaryApprovalReason(msg.params?.reason || msg.params?.message),
|
|
843
|
+
options: ["\u7EE7\u7EED", "\u53D6\u6D88", "\u62D2\u7EDD"]
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
onEvent(method, p) {
|
|
847
|
+
if (!task)
|
|
848
|
+
return;
|
|
849
|
+
if (isTerminal(task.status))
|
|
850
|
+
return;
|
|
851
|
+
task.updatedAt = Date.now();
|
|
852
|
+
if (method === "turn/started") {
|
|
853
|
+
enterRunning(task, task.threadId, p.turn?.id);
|
|
854
|
+
progress("Codex \u5DF2\u5F00\u59CB\u5904\u7406\u3002");
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
if (method === "item/started") {
|
|
858
|
+
const kind = p.item?.type ?? p.type ?? "step";
|
|
859
|
+
if (["commandExecution", "command", "shell"].includes(kind))
|
|
860
|
+
progress("\u6B63\u5728\u6267\u884C\u547D\u4EE4\u3002");
|
|
861
|
+
else
|
|
862
|
+
progress("\u6B63\u5728\u7EE7\u7EED\u5904\u7406\u3002");
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
if (method === "item/agentMessage/delta") {
|
|
866
|
+
const d = p.delta ?? p.text ?? "";
|
|
867
|
+
if (typeof d === "string") {
|
|
868
|
+
task.lastText = (task.lastText ?? "") + d;
|
|
869
|
+
}
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
if (method === "item/completed") {
|
|
873
|
+
const item = p.item ?? {};
|
|
874
|
+
const text = item.text ?? item.summary ?? item.message ?? item.content;
|
|
875
|
+
if (typeof text === "string" && text.trim())
|
|
876
|
+
task.lastText = text;
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
if (method === "turn/completed") {
|
|
880
|
+
const status = p.turn?.status ?? p.status ?? "completed";
|
|
881
|
+
if (task.status === "cancelled")
|
|
882
|
+
return;
|
|
883
|
+
if (["completed", "success"].includes(status)) {
|
|
884
|
+
enterDone(task, task.lastText);
|
|
885
|
+
trace("task_done", { lastText: clean(task.lastText || "\u4EFB\u52A1\u5B8C\u6210\u3002") });
|
|
886
|
+
emit("task_done", {
|
|
887
|
+
task_id: task.id,
|
|
888
|
+
thread_id: task.threadId,
|
|
889
|
+
text: summaryTaskDone(task.lastText)
|
|
890
|
+
});
|
|
891
|
+
} else {
|
|
892
|
+
const errorMsg = task.lastText || `\u4EFB\u52A1\u7ED3\u675F\uFF0C\u72B6\u6001\u4E3A ${status}\u3002`;
|
|
893
|
+
enterFailed(task, errorMsg, task.lastText);
|
|
894
|
+
trace("task_failed", { error: task.error });
|
|
895
|
+
emit("task_failed", {
|
|
896
|
+
task_id: task.id,
|
|
897
|
+
thread_id: task.threadId,
|
|
898
|
+
text: summaryTaskFailed(task.error)
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
if (method === "error") {
|
|
904
|
+
const text = p.error?.message ?? p.message ?? "\u4EFB\u52A1\u5931\u8D25\u3002";
|
|
905
|
+
enterFailed(task, text);
|
|
906
|
+
trace("codex_error", { message: text });
|
|
907
|
+
emit("task_failed", {
|
|
908
|
+
task_id: task.id,
|
|
909
|
+
thread_id: task.threadId,
|
|
910
|
+
text: summaryTaskFailed(text)
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
var codex = new Codex;
|
|
916
|
+
function clearPendingApprovalIfTaskInactive() {
|
|
917
|
+
if (task && !isActive(task.status) && pendingApproval) {
|
|
918
|
+
pendingApproval = null;
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
async function startTask(text) {
|
|
922
|
+
if (task && !canStartNew(task)) {
|
|
923
|
+
speak(clean("\u5F53\u524D\u4EFB\u52A1\u8FD8\u5728\u8FDB\u884C\u4E2D\uFF0C\u8BF7\u5148\u5B8C\u6210\u6216\u53D6\u6D88\u540E\u518D\u5F00\u59CB\u65B0\u7684\u4EFB\u52A1\u3002"), "normal", {
|
|
924
|
+
task_id: task.id
|
|
925
|
+
});
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
task = {
|
|
929
|
+
id: crypto.randomUUID(),
|
|
930
|
+
goal: "",
|
|
931
|
+
status: "starting",
|
|
932
|
+
updatedAt: Date.now()
|
|
933
|
+
};
|
|
934
|
+
if (activeRequestId)
|
|
935
|
+
taskRequestIds.set(task.id, activeRequestId);
|
|
936
|
+
enterStarting(task, text);
|
|
937
|
+
trace("task_started", { goal: text });
|
|
938
|
+
emit("task_started", { task_id: task.id, text: summaryTaskStarted(text) });
|
|
939
|
+
speak(summaryTaskStarted(text), "normal", {
|
|
940
|
+
task_id: task.id
|
|
941
|
+
});
|
|
942
|
+
try {
|
|
943
|
+
task.threadId = await codex.start(text);
|
|
944
|
+
enterRunning(task, task.threadId);
|
|
945
|
+
upsertSessionHistory(buildSessionSnapshotPayload());
|
|
946
|
+
} catch (e) {
|
|
947
|
+
enterFailed(task, e instanceof Error ? e.message : "\u542F\u52A8 Codex \u5931\u8D25\u3002");
|
|
948
|
+
trace("codex_start_failed", { error: task.error });
|
|
949
|
+
speak(summaryTaskFailed(task.error), "high", { task_id: task.id });
|
|
950
|
+
upsertSessionHistory(buildSessionSnapshotPayload());
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
async function continueHistoricalThread(text, threadId, taskId) {
|
|
954
|
+
if (task && !canStartNew(task)) {
|
|
955
|
+
speak(clean("\u5F53\u524D\u4EFB\u52A1\u8FD8\u5728\u8FDB\u884C\u4E2D\uFF0C\u8BF7\u5148\u5B8C\u6210\u6216\u53D6\u6D88\u540E\u518D\u5F00\u59CB\u65B0\u7684\u4EFB\u52A1\u3002"), "normal", {
|
|
956
|
+
task_id: task.id
|
|
957
|
+
});
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
task = {
|
|
961
|
+
id: crypto.randomUUID(),
|
|
962
|
+
goal: text,
|
|
963
|
+
status: "starting",
|
|
964
|
+
updatedAt: Date.now(),
|
|
965
|
+
threadId
|
|
966
|
+
};
|
|
967
|
+
if (activeRequestId)
|
|
968
|
+
taskRequestIds.set(task.id, activeRequestId);
|
|
969
|
+
try {
|
|
970
|
+
const resumedThreadId = await codex.resumeThread(threadId);
|
|
971
|
+
task.threadId = resumedThreadId;
|
|
972
|
+
trace("task_started", { goal: text, thread_id: resumedThreadId, resumed_task_id: taskId });
|
|
973
|
+
emit("task_started", {
|
|
974
|
+
task_id: task.id,
|
|
975
|
+
thread_id: resumedThreadId,
|
|
976
|
+
text: summaryTaskStarted(text)
|
|
977
|
+
});
|
|
978
|
+
speak(summaryTaskStarted(text), "normal", {
|
|
979
|
+
task_id: task.id,
|
|
980
|
+
thread_id: resumedThreadId
|
|
981
|
+
});
|
|
982
|
+
await codex.continueThread(resumedThreadId, text);
|
|
983
|
+
enterRunning(task, resumedThreadId);
|
|
984
|
+
upsertSessionHistory(buildSessionSnapshotPayload());
|
|
985
|
+
} catch (e) {
|
|
986
|
+
enterFailed(task, e instanceof Error ? e.message : "\u7EED\u5199 Codex \u4F1A\u8BDD\u5931\u8D25\u3002");
|
|
987
|
+
trace("codex_resume_failed", { error: task.error, thread_id: task.threadId });
|
|
988
|
+
speak(summaryTaskFailed(task.error), "high", {
|
|
989
|
+
task_id: task.id,
|
|
990
|
+
thread_id: task.threadId
|
|
991
|
+
});
|
|
992
|
+
upsertSessionHistory(buildSessionSnapshotPayload());
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
async function answerApproval(text) {
|
|
996
|
+
if (!pendingApproval)
|
|
997
|
+
return false;
|
|
998
|
+
const req = pendingApproval;
|
|
999
|
+
pendingApproval = null;
|
|
1000
|
+
const negative = /\u53D6\u6D88|\u62D2\u7EDD|\u4E0D\u8981|decline|cancel|no/i.test(text);
|
|
1001
|
+
const positive = !negative;
|
|
1002
|
+
let result;
|
|
1003
|
+
if (req.method.includes("permissions")) {
|
|
1004
|
+
result = positive ? { scope: "session", permissions: req.params.permissions ?? {} } : { scope: "turn", permissions: {} };
|
|
1005
|
+
} else if (req.method.includes("elicitation")) {
|
|
1006
|
+
result = positive ? { action: "accept", content: text } : { action: "decline", content: null };
|
|
1007
|
+
} else if (req.method.includes("requestUserInput")) {
|
|
1008
|
+
result = { input: text };
|
|
1009
|
+
} else {
|
|
1010
|
+
result = { decision: positive ? "accept" : "decline" };
|
|
1011
|
+
}
|
|
1012
|
+
codex.answer(req.id, result);
|
|
1013
|
+
if (task) {
|
|
1014
|
+
returnToRunning(task);
|
|
1015
|
+
}
|
|
1016
|
+
clearPendingApprovalIfTaskInactive();
|
|
1017
|
+
speak(positive ? summaryApprovalAccepted() : summaryApprovalDeclined(), "normal", {
|
|
1018
|
+
task_id: task?.id
|
|
1019
|
+
});
|
|
1020
|
+
return true;
|
|
1021
|
+
}
|
|
1022
|
+
async function onUserInput(text, target) {
|
|
1023
|
+
trace("user_input", { text, task_id: target?.taskId, thread_id: target?.threadId });
|
|
1024
|
+
if (await answerApproval(text))
|
|
1025
|
+
return;
|
|
1026
|
+
if (task && !isActive(task.status)) {
|
|
1027
|
+
clearPendingApprovalIfTaskInactive();
|
|
1028
|
+
}
|
|
1029
|
+
const matchesCurrentTask = Boolean(target?.threadId && task?.threadId && target.threadId === task.threadId) || !target?.taskId || target.taskId === "current" || target.taskId === task?.id;
|
|
1030
|
+
const matchesCurrentThread = !target?.threadId || target.threadId === task?.threadId;
|
|
1031
|
+
const canContinueCurrentTask = task ? isActive(task.status) : false;
|
|
1032
|
+
const wantsHistoricalThread = Boolean(target?.threadId && !canContinueCurrentTask);
|
|
1033
|
+
const targetsCurrentThread = Boolean(target?.threadId && task?.threadId && target.threadId === task.threadId);
|
|
1034
|
+
if (target?.threadId && task?.threadId && canContinueCurrentTask && target.threadId !== task.threadId) {
|
|
1035
|
+
speak(clean("\u5F53\u524D\u8FD8\u4E0D\u80FD\u76F4\u63A5\u7EED\u5199\u5386\u53F2\u5BF9\u8BDD\uFF0C\u8BF7\u5148\u6253\u5F00\u5BF9\u5E94\u4EFB\u52A1\u540E\u518D\u53D1\u9001\u3002"), "normal", {
|
|
1036
|
+
task_id: target.taskId,
|
|
1037
|
+
thread_id: target.threadId
|
|
1038
|
+
});
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
if (wantsHistoricalThread && target?.threadId) {
|
|
1042
|
+
await continueHistoricalThread(text, target.threadId, target.taskId);
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
1045
|
+
if (task && isSteerable(task.status) && matchesCurrentTask && matchesCurrentThread) {
|
|
1046
|
+
task.lastText = null;
|
|
1047
|
+
await codex.steer(text);
|
|
1048
|
+
speak(summarySteerAck(), "normal", { task_id: task.id, thread_id: task.threadId });
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
1051
|
+
if (target?.taskId && target.taskId !== "current" && task?.id && canContinueCurrentTask && target.taskId !== task.id && !targetsCurrentThread) {
|
|
1052
|
+
speak(clean("\u5F53\u524D\u9009\u4E2D\u7684\u4E0D\u662F\u6B63\u5728\u8FD0\u884C\u7684\u4EFB\u52A1\uFF0C\u8BF7\u5148\u5207\u56DE\u5F53\u524D\u4EFB\u52A1\u6216\u65B0\u5EFA\u5BF9\u8BDD\u3002"), "normal", {
|
|
1053
|
+
task_id: target.taskId,
|
|
1054
|
+
thread_id: target.threadId
|
|
1055
|
+
});
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1058
|
+
const builtinReply = resolveBuiltinReply(text);
|
|
1059
|
+
if (builtinReply) {
|
|
1060
|
+
if (task && !canStartNew(task)) {
|
|
1061
|
+
speak(clean("\u5F53\u524D\u4EFB\u52A1\u8FD8\u5728\u8FDB\u884C\u4E2D\uFF0C\u8BF7\u5148\u5B8C\u6210\u6216\u53D6\u6D88\u540E\u518D\u5F00\u59CB\u65B0\u7684\u4EFB\u52A1\u3002"), "normal", {
|
|
1062
|
+
task_id: task.id
|
|
1063
|
+
});
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
task = {
|
|
1067
|
+
id: crypto.randomUUID(),
|
|
1068
|
+
goal: text,
|
|
1069
|
+
status: "starting",
|
|
1070
|
+
updatedAt: Date.now(),
|
|
1071
|
+
lastText: builtinReply
|
|
1072
|
+
};
|
|
1073
|
+
if (activeRequestId)
|
|
1074
|
+
taskRequestIds.set(task.id, activeRequestId);
|
|
1075
|
+
enterDone(task, task.lastText);
|
|
1076
|
+
trace("task_done", { lastText: task.lastText });
|
|
1077
|
+
const reply = summaryTaskDone(task.lastText);
|
|
1078
|
+
emit("task_done", { task_id: task.id, text: reply });
|
|
1079
|
+
speak(reply, "normal", { task_id: task.id });
|
|
1080
|
+
upsertSessionHistory(buildSessionSnapshotPayload());
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
await startTask(text);
|
|
1084
|
+
}
|
|
1085
|
+
async function onControl(action) {
|
|
1086
|
+
if (!task)
|
|
1087
|
+
return speak(summaryStatusNone());
|
|
1088
|
+
if (action === "cancel") {
|
|
1089
|
+
if (task.status === "cancelled") {
|
|
1090
|
+
return speak(summaryTaskCancelled(), "normal", { task_id: task.id, thread_id: task.threadId });
|
|
1091
|
+
}
|
|
1092
|
+
if (!isCancellable(task.status)) {
|
|
1093
|
+
return speak(clean("\u5F53\u524D\u4EFB\u52A1\u65E0\u6CD5\u53D6\u6D88\u3002"), "normal", { task_id: task.id, thread_id: task.threadId });
|
|
1094
|
+
}
|
|
1095
|
+
enterCancelled(task);
|
|
1096
|
+
pendingApproval = null;
|
|
1097
|
+
trace("task_cancelled");
|
|
1098
|
+
emit("task_cancelled", {
|
|
1099
|
+
task_id: task.id,
|
|
1100
|
+
thread_id: task.threadId,
|
|
1101
|
+
text: summaryTaskCancelled()
|
|
1102
|
+
});
|
|
1103
|
+
await codex.interrupt();
|
|
1104
|
+
speak(summaryTaskCancelled(), "high", { task_id: task.id, thread_id: task.threadId });
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
speak(clean(`\u6682\u4E0D\u652F\u6301 ${action}\u3002\u76EE\u524D\u53EA\u652F\u6301\u53D6\u6D88\u3002`), "normal", {
|
|
1108
|
+
task_id: task.id,
|
|
1109
|
+
thread_id: task.threadId
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1112
|
+
function buildStatusPayload() {
|
|
1113
|
+
if (!task) {
|
|
1114
|
+
return { text: summaryStatusNone() };
|
|
1115
|
+
}
|
|
1116
|
+
let text;
|
|
1117
|
+
if (task.status === "starting") {
|
|
1118
|
+
text = clean(`\u4EFB\u52A1\u6B63\u5728\u542F\u52A8\uFF1A${task.goal}`);
|
|
1119
|
+
} else if (task.status === "running" || task.status === "waiting_approval") {
|
|
1120
|
+
text = summaryStatusActive(task.goal, task.status);
|
|
1121
|
+
} else if (task.status === "cancelled") {
|
|
1122
|
+
text = summaryTaskCancelled();
|
|
1123
|
+
} else {
|
|
1124
|
+
text = summaryStatusFinished(task.status, task.lastText, task.error);
|
|
1125
|
+
}
|
|
1126
|
+
return {
|
|
1127
|
+
task_id: task.id,
|
|
1128
|
+
status: task.status,
|
|
1129
|
+
thread_id: task.threadId,
|
|
1130
|
+
turn_id: task.turnId,
|
|
1131
|
+
text
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
function buildSessionSnapshotPayload() {
|
|
1135
|
+
const defaultProjectName = REPO_PATH.split("/").filter(Boolean).at(-1) || "current";
|
|
1136
|
+
const history = loadCodexHistory();
|
|
1137
|
+
const projectMap = new Map(history.projects.map((item) => [item.id, { ...item }]));
|
|
1138
|
+
const sessions = history.sessions.map((item) => ({ ...item }));
|
|
1139
|
+
const seenSessionIds = new Set(sessions.map((item) => item.id));
|
|
1140
|
+
if (!task) {
|
|
1141
|
+
for (const session of sessionHistory) {
|
|
1142
|
+
const sessionId = session.threadId || session.id;
|
|
1143
|
+
if (seenSessionIds.has(sessionId))
|
|
1144
|
+
continue;
|
|
1145
|
+
seenSessionIds.add(sessionId);
|
|
1146
|
+
sessions.unshift({
|
|
1147
|
+
id: sessionId,
|
|
1148
|
+
title: session.title,
|
|
1149
|
+
project_id: REPO_PATH,
|
|
1150
|
+
project_name: defaultProjectName,
|
|
1151
|
+
status: session.status,
|
|
1152
|
+
task_id: session.taskId,
|
|
1153
|
+
thread_id: session.threadId,
|
|
1154
|
+
turn_id: session.turnId,
|
|
1155
|
+
text: session.text,
|
|
1156
|
+
last_summary: session.lastSummary ?? session.text,
|
|
1157
|
+
waiting_approval: session.waitingApproval,
|
|
1158
|
+
updated_at: session.updatedAt
|
|
1159
|
+
});
|
|
1160
|
+
}
|
|
1161
|
+
if (!projectMap.has(REPO_PATH)) {
|
|
1162
|
+
projectMap.set(REPO_PATH, {
|
|
1163
|
+
id: REPO_PATH,
|
|
1164
|
+
name: defaultProjectName,
|
|
1165
|
+
repo_path: REPO_PATH,
|
|
1166
|
+
conversation_count: sessions.filter((item) => item.project_id === REPO_PATH).length,
|
|
1167
|
+
last_summary: sessions.find((item) => item.project_id === REPO_PATH)?.last_summary,
|
|
1168
|
+
updated_at: sessions.find((item) => item.project_id === REPO_PATH)?.updated_at ?? Date.now()
|
|
1169
|
+
});
|
|
1170
|
+
}
|
|
1171
|
+
return {
|
|
1172
|
+
status: "idle",
|
|
1173
|
+
text: summaryStatusNone(),
|
|
1174
|
+
waiting_approval: false,
|
|
1175
|
+
projects: [...projectMap.values()].sort((a, b) => b.updated_at - a.updated_at),
|
|
1176
|
+
sessions: sessions.sort((a, b) => b.updated_at - a.updated_at)
|
|
1177
|
+
};
|
|
1178
|
+
}
|
|
1179
|
+
let text;
|
|
1180
|
+
if (task.status === "starting") {
|
|
1181
|
+
text = clean(`\u4EFB\u52A1\u6B63\u5728\u542F\u52A8\uFF1A${task.goal}`);
|
|
1182
|
+
} else if (task.status === "running" || task.status === "waiting_approval") {
|
|
1183
|
+
text = summaryStatusActive(task.goal, task.status);
|
|
1184
|
+
} else if (task.status === "cancelled") {
|
|
1185
|
+
text = summaryTaskCancelled();
|
|
1186
|
+
} else {
|
|
1187
|
+
text = summaryStatusFinished(task.status, task.lastText, task.error);
|
|
1188
|
+
}
|
|
1189
|
+
const snapshot = {
|
|
1190
|
+
id: task.id,
|
|
1191
|
+
title: task.goal,
|
|
1192
|
+
project_id: REPO_PATH,
|
|
1193
|
+
project_name: defaultProjectName,
|
|
1194
|
+
status: task.status,
|
|
1195
|
+
task_id: task.id,
|
|
1196
|
+
thread_id: task.threadId,
|
|
1197
|
+
turn_id: task.turnId,
|
|
1198
|
+
goal: task.goal,
|
|
1199
|
+
text,
|
|
1200
|
+
last_summary: task.lastText,
|
|
1201
|
+
waiting_approval: task.status === "waiting_approval",
|
|
1202
|
+
updated_at: task.updatedAt
|
|
1203
|
+
};
|
|
1204
|
+
upsertSessionHistory({
|
|
1205
|
+
id: snapshot.id,
|
|
1206
|
+
title: snapshot.title,
|
|
1207
|
+
status: snapshot.status,
|
|
1208
|
+
taskId: snapshot.task_id,
|
|
1209
|
+
threadId: snapshot.thread_id,
|
|
1210
|
+
turnId: snapshot.turn_id,
|
|
1211
|
+
text: snapshot.text,
|
|
1212
|
+
lastSummary: snapshot.last_summary ?? undefined,
|
|
1213
|
+
waitingApproval: snapshot.waiting_approval,
|
|
1214
|
+
updatedAt: snapshot.updated_at
|
|
1215
|
+
});
|
|
1216
|
+
const mergedSessions = [
|
|
1217
|
+
snapshot,
|
|
1218
|
+
...sessions.filter((item) => item.id !== snapshot.id && item.thread_id !== snapshot.thread_id)
|
|
1219
|
+
].sort((a, b) => b.updated_at - a.updated_at);
|
|
1220
|
+
projectMap.set(REPO_PATH, {
|
|
1221
|
+
id: REPO_PATH,
|
|
1222
|
+
name: defaultProjectName,
|
|
1223
|
+
repo_path: REPO_PATH,
|
|
1224
|
+
conversation_count: mergedSessions.filter((item) => item.project_id === REPO_PATH).length,
|
|
1225
|
+
last_summary: snapshot.last_summary ?? snapshot.text,
|
|
1226
|
+
updated_at: snapshot.updated_at
|
|
1227
|
+
});
|
|
1228
|
+
return {
|
|
1229
|
+
...snapshot,
|
|
1230
|
+
projects: [...projectMap.values()].sort((a, b) => b.updated_at - a.updated_at),
|
|
1231
|
+
sessions: mergedSessions
|
|
1232
|
+
};
|
|
1233
|
+
}
|
|
1234
|
+
async function onRelayMessage(message) {
|
|
1235
|
+
if (message.type === "ping")
|
|
1236
|
+
return;
|
|
1237
|
+
const requestId = message.payload && typeof message.payload === "object" && "request_id" in message.payload && typeof message.payload.request_id === "string" ? message.payload.request_id : undefined;
|
|
1238
|
+
activeRequestId = requestId;
|
|
1239
|
+
if (message.type === "status_query") {
|
|
1240
|
+
relayClient?.send("status", {
|
|
1241
|
+
...buildStatusPayload(),
|
|
1242
|
+
...activeRequestId ? { request_id: activeRequestId } : {}
|
|
1243
|
+
});
|
|
1244
|
+
activeRequestId = undefined;
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1247
|
+
if (message.type === "session_query") {
|
|
1248
|
+
const threadId = message.payload && "thread_id" in message.payload && typeof message.payload.thread_id === "string" ? message.payload.thread_id : undefined;
|
|
1249
|
+
if (threadId) {
|
|
1250
|
+
relayClient?.send("session_messages", {
|
|
1251
|
+
thread_id: threadId,
|
|
1252
|
+
messages: loadCodexSessionMessages(threadId),
|
|
1253
|
+
...activeRequestId ? { request_id: activeRequestId } : {}
|
|
1254
|
+
});
|
|
1255
|
+
activeRequestId = undefined;
|
|
1256
|
+
return;
|
|
1257
|
+
}
|
|
1258
|
+
relayClient?.send("session_snapshot", {
|
|
1259
|
+
...buildSessionSnapshotPayload(),
|
|
1260
|
+
...activeRequestId ? { request_id: activeRequestId } : {}
|
|
1261
|
+
});
|
|
1262
|
+
activeRequestId = undefined;
|
|
1263
|
+
return;
|
|
1264
|
+
}
|
|
1265
|
+
if (message.type === "control") {
|
|
1266
|
+
await onControl(message.payload.action);
|
|
1267
|
+
activeRequestId = undefined;
|
|
1268
|
+
return;
|
|
1269
|
+
}
|
|
1270
|
+
if (message.type === "user_input") {
|
|
1271
|
+
await onUserInput(message.payload.text, {
|
|
1272
|
+
taskId: message.payload.task_id,
|
|
1273
|
+
threadId: message.payload.thread_id
|
|
1274
|
+
});
|
|
1275
|
+
activeRequestId = undefined;
|
|
1276
|
+
return;
|
|
1277
|
+
}
|
|
1278
|
+
activeRequestId = undefined;
|
|
1279
|
+
}
|
|
1280
|
+
console.log(`Repo: ${REPO_PATH}`);
|
|
1281
|
+
console.log(`Agent identity: ${AGENT_IDENTITY_PATH}`);
|
|
1282
|
+
console.log("CodexAir CLI runs in Relay-only mode; no local HTTP/WebSocket debug server is exposed.");
|
|
1283
|
+
if (codexLaunch.configKeys.length > 0) {
|
|
1284
|
+
console.log(`[codex] app-server config keys: ${codexLaunch.configKeys.join(", ")}`);
|
|
1285
|
+
}
|
|
1286
|
+
var boot = async () => {
|
|
1287
|
+
const relayConfig = await resolveRelayConfig();
|
|
1288
|
+
if (relayConfig) {
|
|
1289
|
+
relayClient = new RelayAgentClient(relayConfig, {
|
|
1290
|
+
onMessage: onRelayMessage,
|
|
1291
|
+
onStatus(status, detail) {
|
|
1292
|
+
if (status === "connected") {
|
|
1293
|
+
console.log(`[relay] connected: ${relayConfig.relayUrl}`);
|
|
1294
|
+
return;
|
|
1295
|
+
}
|
|
1296
|
+
if (status === "disconnected") {
|
|
1297
|
+
console.warn("[relay] disconnected, will retry");
|
|
1298
|
+
return;
|
|
1299
|
+
}
|
|
1300
|
+
console.warn(`[relay] error: ${detail ?? "unknown"}`);
|
|
1301
|
+
}
|
|
1302
|
+
});
|
|
1303
|
+
relayClient.connect();
|
|
1304
|
+
console.log(`[relay] enabled: ${relayConfig.relayUrl}`);
|
|
1305
|
+
}
|
|
1306
|
+
await codex.init();
|
|
1307
|
+
console.log("[codex] initialized");
|
|
1308
|
+
};
|
|
1309
|
+
boot().catch((e) => {
|
|
1310
|
+
console.error("[boot] failed:", e instanceof Error ? e.message : String(e));
|
|
1311
|
+
process.exit(1);
|
|
1312
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "codexair",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CodexAir local agent CLI for connecting Relay to codex app-server.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"codexair": "bin/codexair"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/",
|
|
11
|
+
"dist/",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "bun build --target=bun --outfile dist/index.js index.ts",
|
|
16
|
+
"prepack": "bun run build",
|
|
17
|
+
"start": "bun run index.ts"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"codexair",
|
|
21
|
+
"codex",
|
|
22
|
+
"agent",
|
|
23
|
+
"relay"
|
|
24
|
+
],
|
|
25
|
+
"license": "UNLICENSED",
|
|
26
|
+
"engines": {
|
|
27
|
+
"bun": ">=1.3.0"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/bun": "latest"
|
|
32
|
+
}
|
|
33
|
+
}
|