replicas-engine 0.1.11 → 0.1.12
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/dist/src/index.js +699 -0
- package/package.json +10 -6
- package/dist/index.js +0 -40
- package/dist/middleware/auth.js +0 -14
- package/dist/routes/codex.js +0 -112
- package/dist/routes/ping.js +0 -9
- package/dist/services/codex-manager.js +0 -210
- package/dist/utils/jsonl-reader.js +0 -135
|
@@ -0,0 +1,699 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import "dotenv/config";
|
|
5
|
+
import { serve } from "@hono/node-server";
|
|
6
|
+
import { Hono as Hono3 } from "hono";
|
|
7
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
8
|
+
|
|
9
|
+
// src/middleware/auth.ts
|
|
10
|
+
var authMiddleware = async (c, next) => {
|
|
11
|
+
const secret = c.req.header("X-Replicas-Engine-Secret");
|
|
12
|
+
const expectedSecret = process.env.REPLICAS_ENGINE_SECRET;
|
|
13
|
+
if (!expectedSecret) {
|
|
14
|
+
return c.json(
|
|
15
|
+
{ error: "Server configuration error: REPLICAS_ENGINE_SECRET not set" },
|
|
16
|
+
500
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
if (!secret) {
|
|
20
|
+
return c.json(
|
|
21
|
+
{ error: "Unauthorized: X-Replicas-Engine-Secret header required" },
|
|
22
|
+
401
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
if (secret !== expectedSecret) {
|
|
26
|
+
return c.json({ error: "Unauthorized: Invalid secret" }, 401);
|
|
27
|
+
}
|
|
28
|
+
await next();
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// src/routes/codex.ts
|
|
32
|
+
import { Hono } from "hono";
|
|
33
|
+
|
|
34
|
+
// src/services/codex-manager.ts
|
|
35
|
+
import { Codex } from "@openai/codex-sdk";
|
|
36
|
+
|
|
37
|
+
// src/utils/jsonl-reader.ts
|
|
38
|
+
import { readFile } from "fs/promises";
|
|
39
|
+
async function readJSONL(filePath) {
|
|
40
|
+
try {
|
|
41
|
+
const content = await readFile(filePath, "utf-8");
|
|
42
|
+
const lines = content.split("\n").filter((line) => line.trim());
|
|
43
|
+
const events = lines.map((line, index) => {
|
|
44
|
+
try {
|
|
45
|
+
return JSON.parse(line);
|
|
46
|
+
} catch (e) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}).filter((event) => event !== null);
|
|
50
|
+
return events;
|
|
51
|
+
} catch (error) {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// src/services/codex-manager.ts
|
|
57
|
+
import { readdir, stat } from "fs/promises";
|
|
58
|
+
import { join } from "path";
|
|
59
|
+
import { homedir } from "os";
|
|
60
|
+
var CodexManager = class {
|
|
61
|
+
codex;
|
|
62
|
+
currentThreadId = null;
|
|
63
|
+
currentThread = null;
|
|
64
|
+
workingDirectory;
|
|
65
|
+
processing = false;
|
|
66
|
+
constructor(workingDirectory) {
|
|
67
|
+
this.codex = new Codex();
|
|
68
|
+
if (workingDirectory) {
|
|
69
|
+
this.workingDirectory = workingDirectory;
|
|
70
|
+
} else {
|
|
71
|
+
const repoName = process.env.REPLICAS_REPO_NAME;
|
|
72
|
+
const workspaceHome = process.env.WORKSPACE_HOME || process.env.HOME || homedir();
|
|
73
|
+
if (repoName) {
|
|
74
|
+
this.workingDirectory = join(workspaceHome, "workspaces", repoName);
|
|
75
|
+
} else {
|
|
76
|
+
this.workingDirectory = workspaceHome;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
isProcessing() {
|
|
81
|
+
return this.processing;
|
|
82
|
+
}
|
|
83
|
+
async sendMessage(message, model, customInstructions) {
|
|
84
|
+
try {
|
|
85
|
+
this.processing = true;
|
|
86
|
+
if (!this.currentThread) {
|
|
87
|
+
if (this.currentThreadId) {
|
|
88
|
+
this.currentThread = this.codex.resumeThread(this.currentThreadId, {
|
|
89
|
+
workingDirectory: this.workingDirectory,
|
|
90
|
+
skipGitRepoCheck: true,
|
|
91
|
+
sandboxMode: "danger-full-access",
|
|
92
|
+
model: model || "gpt-5-codex"
|
|
93
|
+
});
|
|
94
|
+
} else {
|
|
95
|
+
this.currentThread = this.codex.startThread({
|
|
96
|
+
workingDirectory: this.workingDirectory,
|
|
97
|
+
skipGitRepoCheck: true,
|
|
98
|
+
sandboxMode: "danger-full-access",
|
|
99
|
+
model: model || "gpt-5-codex"
|
|
100
|
+
});
|
|
101
|
+
if (customInstructions) {
|
|
102
|
+
message = customInstructions + "\n" + message;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const { events } = await this.currentThread.runStreamed(message);
|
|
107
|
+
for await (const event of events) {
|
|
108
|
+
if (event.type === "thread.started") {
|
|
109
|
+
this.currentThreadId = event.thread_id;
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (!this.currentThreadId && this.currentThread.id) {
|
|
114
|
+
this.currentThreadId = this.currentThread.id;
|
|
115
|
+
}
|
|
116
|
+
} finally {
|
|
117
|
+
this.processing = false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
async getHistory() {
|
|
121
|
+
if (!this.currentThreadId) {
|
|
122
|
+
return {
|
|
123
|
+
thread_id: null,
|
|
124
|
+
events: []
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
const sessionFile = await this.findSessionFile(this.currentThreadId);
|
|
128
|
+
if (!sessionFile) {
|
|
129
|
+
return {
|
|
130
|
+
thread_id: this.currentThreadId,
|
|
131
|
+
events: []
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
const events = await readJSONL(sessionFile);
|
|
135
|
+
return {
|
|
136
|
+
thread_id: this.currentThreadId,
|
|
137
|
+
events
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
async getStatus() {
|
|
141
|
+
let sessionFile = null;
|
|
142
|
+
if (this.currentThreadId) {
|
|
143
|
+
sessionFile = await this.findSessionFile(this.currentThreadId);
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
has_active_thread: this.currentThreadId !== null,
|
|
147
|
+
thread_id: this.currentThreadId,
|
|
148
|
+
session_file: sessionFile,
|
|
149
|
+
working_directory: this.workingDirectory
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
async reset() {
|
|
153
|
+
this.currentThread = null;
|
|
154
|
+
this.currentThreadId = null;
|
|
155
|
+
this.processing = false;
|
|
156
|
+
}
|
|
157
|
+
getThreadId() {
|
|
158
|
+
return this.currentThreadId;
|
|
159
|
+
}
|
|
160
|
+
async getUpdates(since) {
|
|
161
|
+
if (!this.currentThreadId) {
|
|
162
|
+
return {
|
|
163
|
+
events: [],
|
|
164
|
+
isComplete: true
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
const sessionFile = await this.findSessionFile(this.currentThreadId);
|
|
168
|
+
if (!sessionFile) {
|
|
169
|
+
return {
|
|
170
|
+
events: [],
|
|
171
|
+
isComplete: true
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
const allEvents = await readJSONL(sessionFile);
|
|
175
|
+
const events = allEvents.filter((event) => event.timestamp > since);
|
|
176
|
+
const isComplete = allEvents.some((event) => event.type === "turn.completed");
|
|
177
|
+
return {
|
|
178
|
+
events,
|
|
179
|
+
isComplete
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
// Helper methods for finding session files
|
|
183
|
+
async findSessionFile(threadId) {
|
|
184
|
+
const sessionsDir = join(homedir(), ".codex", "sessions");
|
|
185
|
+
try {
|
|
186
|
+
const now = /* @__PURE__ */ new Date();
|
|
187
|
+
const year = now.getFullYear();
|
|
188
|
+
const month = String(now.getMonth() + 1).padStart(2, "0");
|
|
189
|
+
const day = String(now.getDate()).padStart(2, "0");
|
|
190
|
+
const todayDir = join(sessionsDir, String(year), month, day);
|
|
191
|
+
const file = await this.findFileInDirectory(todayDir, threadId);
|
|
192
|
+
if (file) return file;
|
|
193
|
+
for (let daysAgo = 1; daysAgo <= 7; daysAgo++) {
|
|
194
|
+
const date = new Date(now);
|
|
195
|
+
date.setDate(date.getDate() - daysAgo);
|
|
196
|
+
const searchYear = date.getFullYear();
|
|
197
|
+
const searchMonth = String(date.getMonth() + 1).padStart(2, "0");
|
|
198
|
+
const searchDay = String(date.getDate()).padStart(2, "0");
|
|
199
|
+
const searchDir = join(sessionsDir, String(searchYear), searchMonth, searchDay);
|
|
200
|
+
const file2 = await this.findFileInDirectory(searchDir, threadId);
|
|
201
|
+
if (file2) return file2;
|
|
202
|
+
}
|
|
203
|
+
return null;
|
|
204
|
+
} catch (error) {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
async findFileInDirectory(directory, threadId) {
|
|
209
|
+
try {
|
|
210
|
+
const files = await readdir(directory);
|
|
211
|
+
for (const file of files) {
|
|
212
|
+
if (file.endsWith(".jsonl") && file.includes(threadId)) {
|
|
213
|
+
const fullPath = join(directory, file);
|
|
214
|
+
const stats = await stat(fullPath);
|
|
215
|
+
if (stats.isFile()) {
|
|
216
|
+
return fullPath;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return null;
|
|
221
|
+
} catch (error) {
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
// src/routes/codex.ts
|
|
228
|
+
var codex = new Hono();
|
|
229
|
+
var codexManager = new CodexManager();
|
|
230
|
+
codex.post("/send", async (c) => {
|
|
231
|
+
try {
|
|
232
|
+
const body = await c.req.json();
|
|
233
|
+
const { message, model, customInstructions } = body;
|
|
234
|
+
if (!message || typeof message !== "string") {
|
|
235
|
+
return c.json({ error: "Message is required and must be a string" }, 400);
|
|
236
|
+
}
|
|
237
|
+
if (codexManager.isProcessing()) {
|
|
238
|
+
return c.json(
|
|
239
|
+
{
|
|
240
|
+
error: "A turn is already in progress. Please wait."
|
|
241
|
+
},
|
|
242
|
+
400
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
codexManager.sendMessage(message, model, customInstructions).catch((error) => {
|
|
246
|
+
console.error("[Codex Route] Error in background message processing:", error);
|
|
247
|
+
});
|
|
248
|
+
return c.json({
|
|
249
|
+
success: true,
|
|
250
|
+
message: "Message sent successfully"
|
|
251
|
+
});
|
|
252
|
+
} catch (error) {
|
|
253
|
+
console.error("Error in /codex/send:", error);
|
|
254
|
+
return c.json(
|
|
255
|
+
{
|
|
256
|
+
error: "Failed to process message",
|
|
257
|
+
details: error instanceof Error ? error.message : "Unknown error"
|
|
258
|
+
},
|
|
259
|
+
500
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
codex.get("/history", async (c) => {
|
|
264
|
+
try {
|
|
265
|
+
const history = await codexManager.getHistory();
|
|
266
|
+
if (!history.thread_id) {
|
|
267
|
+
return c.json({
|
|
268
|
+
message: "No active thread",
|
|
269
|
+
thread_id: null,
|
|
270
|
+
events: []
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
return c.json(history);
|
|
274
|
+
} catch (error) {
|
|
275
|
+
console.error("Error in /codex/history:", error);
|
|
276
|
+
return c.json(
|
|
277
|
+
{
|
|
278
|
+
error: "Failed to retrieve history",
|
|
279
|
+
details: error instanceof Error ? error.message : "Unknown error"
|
|
280
|
+
},
|
|
281
|
+
500
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
codex.get("/updates", async (c) => {
|
|
286
|
+
try {
|
|
287
|
+
const since = c.req.query("since") || "";
|
|
288
|
+
if (!since) {
|
|
289
|
+
return c.json({ error: 'Missing "since" query parameter' }, 400);
|
|
290
|
+
}
|
|
291
|
+
const updates = await codexManager.getUpdates(since);
|
|
292
|
+
return c.json(updates);
|
|
293
|
+
} catch (error) {
|
|
294
|
+
console.error("Error in /codex/updates:", error);
|
|
295
|
+
return c.json(
|
|
296
|
+
{
|
|
297
|
+
error: "Failed to retrieve updates",
|
|
298
|
+
details: error instanceof Error ? error.message : "Unknown error"
|
|
299
|
+
},
|
|
300
|
+
500
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
codex.get("/status", async (c) => {
|
|
305
|
+
try {
|
|
306
|
+
const status = await codexManager.getStatus();
|
|
307
|
+
return c.json(status);
|
|
308
|
+
} catch (error) {
|
|
309
|
+
console.error("Error in /codex/status:", error);
|
|
310
|
+
return c.json(
|
|
311
|
+
{
|
|
312
|
+
error: "Failed to retrieve status",
|
|
313
|
+
details: error instanceof Error ? error.message : "Unknown error"
|
|
314
|
+
},
|
|
315
|
+
500
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
codex.post("/reset", async (c) => {
|
|
320
|
+
try {
|
|
321
|
+
await codexManager.reset();
|
|
322
|
+
return c.json({
|
|
323
|
+
message: "Thread reset successfully",
|
|
324
|
+
success: true
|
|
325
|
+
});
|
|
326
|
+
} catch (error) {
|
|
327
|
+
console.error("Error in /codex/reset:", error);
|
|
328
|
+
return c.json(
|
|
329
|
+
{
|
|
330
|
+
error: "Failed to reset thread",
|
|
331
|
+
details: error instanceof Error ? error.message : "Unknown error"
|
|
332
|
+
},
|
|
333
|
+
500
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
var codex_default = codex;
|
|
338
|
+
|
|
339
|
+
// src/routes/claude.ts
|
|
340
|
+
import { Hono as Hono2 } from "hono";
|
|
341
|
+
|
|
342
|
+
// src/services/claude-manager.ts
|
|
343
|
+
import {
|
|
344
|
+
query
|
|
345
|
+
} from "@anthropic-ai/claude-agent-sdk";
|
|
346
|
+
import { join as join2 } from "path";
|
|
347
|
+
import { mkdir, appendFile, rm } from "fs/promises";
|
|
348
|
+
import { homedir as homedir2 } from "os";
|
|
349
|
+
var ClaudeManager = class {
|
|
350
|
+
workingDirectory;
|
|
351
|
+
historyFile;
|
|
352
|
+
sessionId = null;
|
|
353
|
+
initialized;
|
|
354
|
+
processing = false;
|
|
355
|
+
constructor(workingDirectory) {
|
|
356
|
+
if (workingDirectory) {
|
|
357
|
+
this.workingDirectory = workingDirectory;
|
|
358
|
+
} else {
|
|
359
|
+
const repoName = process.env.REPLICAS_REPO_NAME;
|
|
360
|
+
const workspaceHome = process.env.WORKSPACE_HOME || process.env.HOME || homedir2();
|
|
361
|
+
if (repoName) {
|
|
362
|
+
this.workingDirectory = join2(workspaceHome, "workspaces", repoName);
|
|
363
|
+
} else {
|
|
364
|
+
this.workingDirectory = workspaceHome;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
this.historyFile = join2(homedir2(), ".replicas", "claude", "history.jsonl");
|
|
368
|
+
this.initialized = this.initialize();
|
|
369
|
+
}
|
|
370
|
+
isProcessing() {
|
|
371
|
+
return this.processing;
|
|
372
|
+
}
|
|
373
|
+
async sendMessage(message, model, customInstructions) {
|
|
374
|
+
if (!message || !message.trim()) {
|
|
375
|
+
throw new Error("Message cannot be empty");
|
|
376
|
+
}
|
|
377
|
+
await this.initialized;
|
|
378
|
+
this.processing = true;
|
|
379
|
+
try {
|
|
380
|
+
const userMessage = {
|
|
381
|
+
type: "user",
|
|
382
|
+
message: {
|
|
383
|
+
role: "user",
|
|
384
|
+
content: [
|
|
385
|
+
{
|
|
386
|
+
type: "text",
|
|
387
|
+
text: message
|
|
388
|
+
}
|
|
389
|
+
]
|
|
390
|
+
},
|
|
391
|
+
parent_tool_use_id: null,
|
|
392
|
+
session_id: this.sessionId ?? ""
|
|
393
|
+
};
|
|
394
|
+
await this.recordEvent(userMessage);
|
|
395
|
+
const promptIterable = (async function* () {
|
|
396
|
+
yield userMessage;
|
|
397
|
+
})();
|
|
398
|
+
const response = query({
|
|
399
|
+
prompt: promptIterable,
|
|
400
|
+
options: {
|
|
401
|
+
resume: this.sessionId || void 0,
|
|
402
|
+
cwd: this.workingDirectory,
|
|
403
|
+
permissionMode: "bypassPermissions",
|
|
404
|
+
allowDangerouslySkipPermissions: true,
|
|
405
|
+
settingSources: ["user", "project", "local"],
|
|
406
|
+
systemPrompt: {
|
|
407
|
+
type: "preset",
|
|
408
|
+
preset: "claude_code",
|
|
409
|
+
append: customInstructions
|
|
410
|
+
},
|
|
411
|
+
env: process.env,
|
|
412
|
+
model: model || "sonnet"
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
for await (const msg of response) {
|
|
416
|
+
await this.handleMessage(msg);
|
|
417
|
+
}
|
|
418
|
+
} finally {
|
|
419
|
+
this.processing = false;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
async getHistory() {
|
|
423
|
+
await this.initialized;
|
|
424
|
+
const events = await readJSONL(this.historyFile);
|
|
425
|
+
return {
|
|
426
|
+
thread_id: this.sessionId,
|
|
427
|
+
events
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
async getStatus() {
|
|
431
|
+
await this.initialized;
|
|
432
|
+
const status = {
|
|
433
|
+
has_active_thread: this.processing,
|
|
434
|
+
thread_id: this.sessionId,
|
|
435
|
+
working_directory: this.workingDirectory
|
|
436
|
+
};
|
|
437
|
+
return status;
|
|
438
|
+
}
|
|
439
|
+
async getUpdates(since) {
|
|
440
|
+
await this.initialized;
|
|
441
|
+
const allEvents = await readJSONL(this.historyFile);
|
|
442
|
+
const events = allEvents.filter((event) => event.timestamp > since);
|
|
443
|
+
const isComplete = !this.processing;
|
|
444
|
+
return { events, isComplete };
|
|
445
|
+
}
|
|
446
|
+
async reset() {
|
|
447
|
+
await this.initialized;
|
|
448
|
+
this.sessionId = null;
|
|
449
|
+
this.processing = false;
|
|
450
|
+
try {
|
|
451
|
+
await rm(this.historyFile, { force: true });
|
|
452
|
+
} catch {
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
async initialize() {
|
|
456
|
+
const historyDir = join2(homedir2(), ".replicas", "claude");
|
|
457
|
+
await mkdir(historyDir, { recursive: true });
|
|
458
|
+
}
|
|
459
|
+
async handleMessage(message) {
|
|
460
|
+
if ("session_id" in message && message.session_id && !this.sessionId) {
|
|
461
|
+
this.sessionId = message.session_id;
|
|
462
|
+
}
|
|
463
|
+
await this.recordEvent(message);
|
|
464
|
+
}
|
|
465
|
+
async recordEvent(event) {
|
|
466
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
467
|
+
const jsonEvent = {
|
|
468
|
+
timestamp,
|
|
469
|
+
type: `claude-${event.type}`,
|
|
470
|
+
payload: event
|
|
471
|
+
};
|
|
472
|
+
await appendFile(this.historyFile, JSON.stringify(jsonEvent) + "\n", "utf-8");
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
// src/routes/claude.ts
|
|
477
|
+
var claude = new Hono2();
|
|
478
|
+
var claudeManager = new ClaudeManager();
|
|
479
|
+
claude.post("/send", async (c) => {
|
|
480
|
+
try {
|
|
481
|
+
const body = await c.req.json();
|
|
482
|
+
const { message, model, customInstructions } = body;
|
|
483
|
+
if (!message || typeof message !== "string") {
|
|
484
|
+
return c.json({ error: "Message is required and must be a string" }, 400);
|
|
485
|
+
}
|
|
486
|
+
if (claudeManager.isProcessing()) {
|
|
487
|
+
return c.json(
|
|
488
|
+
{
|
|
489
|
+
error: "A turn is already in progress. Please wait."
|
|
490
|
+
},
|
|
491
|
+
400
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
claudeManager.sendMessage(message, model, customInstructions).catch((error) => {
|
|
495
|
+
console.error("[Claude Route] Error in background message processing:", error);
|
|
496
|
+
});
|
|
497
|
+
return c.json({
|
|
498
|
+
success: true,
|
|
499
|
+
message: "Message sent successfully"
|
|
500
|
+
});
|
|
501
|
+
} catch (error) {
|
|
502
|
+
console.error("Error in /claude/send:", error);
|
|
503
|
+
return c.json(
|
|
504
|
+
{
|
|
505
|
+
error: "Failed to process message",
|
|
506
|
+
details: error instanceof Error ? error.message : "Unknown error"
|
|
507
|
+
},
|
|
508
|
+
500
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
claude.get("/history", async (c) => {
|
|
513
|
+
try {
|
|
514
|
+
const history = await claudeManager.getHistory();
|
|
515
|
+
if (!history.thread_id) {
|
|
516
|
+
return c.json({
|
|
517
|
+
message: "No active session",
|
|
518
|
+
thread_id: null,
|
|
519
|
+
events: []
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
return c.json(history);
|
|
523
|
+
} catch (error) {
|
|
524
|
+
console.error("Error in /claude/history:", error);
|
|
525
|
+
return c.json(
|
|
526
|
+
{
|
|
527
|
+
error: "Failed to retrieve history",
|
|
528
|
+
details: error instanceof Error ? error.message : "Unknown error"
|
|
529
|
+
},
|
|
530
|
+
500
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
});
|
|
534
|
+
claude.get("/updates", async (c) => {
|
|
535
|
+
try {
|
|
536
|
+
const since = c.req.query("since") || "";
|
|
537
|
+
if (!since) {
|
|
538
|
+
return c.json({ error: 'Missing "since" query parameter' }, 400);
|
|
539
|
+
}
|
|
540
|
+
const updates = await claudeManager.getUpdates(since);
|
|
541
|
+
return c.json(updates);
|
|
542
|
+
} catch (error) {
|
|
543
|
+
console.error("Error in /claude/updates:", error);
|
|
544
|
+
return c.json(
|
|
545
|
+
{
|
|
546
|
+
error: "Failed to retrieve updates",
|
|
547
|
+
details: error instanceof Error ? error.message : "Unknown error"
|
|
548
|
+
},
|
|
549
|
+
500
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
claude.get("/status", async (c) => {
|
|
554
|
+
try {
|
|
555
|
+
const status = await claudeManager.getStatus();
|
|
556
|
+
return c.json(status);
|
|
557
|
+
} catch (error) {
|
|
558
|
+
console.error("Error in /claude/status:", error);
|
|
559
|
+
return c.json(
|
|
560
|
+
{
|
|
561
|
+
error: "Failed to retrieve status",
|
|
562
|
+
details: error instanceof Error ? error.message : "Unknown error"
|
|
563
|
+
},
|
|
564
|
+
500
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
});
|
|
568
|
+
claude.post("/reset", async (c) => {
|
|
569
|
+
try {
|
|
570
|
+
await claudeManager.reset();
|
|
571
|
+
return c.json({
|
|
572
|
+
message: "Session reset successfully",
|
|
573
|
+
success: true
|
|
574
|
+
});
|
|
575
|
+
} catch (error) {
|
|
576
|
+
console.error("Error in /claude/reset:", error);
|
|
577
|
+
return c.json(
|
|
578
|
+
{
|
|
579
|
+
error: "Failed to reset session",
|
|
580
|
+
details: error instanceof Error ? error.message : "Unknown error"
|
|
581
|
+
},
|
|
582
|
+
500
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
});
|
|
586
|
+
var claude_default = claude;
|
|
587
|
+
|
|
588
|
+
// src/utils/git.ts
|
|
589
|
+
import { execSync } from "child_process";
|
|
590
|
+
function getCurrentBranch(cwd) {
|
|
591
|
+
try {
|
|
592
|
+
const branch = execSync("git rev-parse --abbrev-ref HEAD", {
|
|
593
|
+
cwd,
|
|
594
|
+
encoding: "utf-8",
|
|
595
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
596
|
+
}).trim();
|
|
597
|
+
return branch;
|
|
598
|
+
} catch (error) {
|
|
599
|
+
console.error("Error getting current branch:", error);
|
|
600
|
+
return null;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
function getGitDiff(cwd) {
|
|
604
|
+
try {
|
|
605
|
+
const shortstat = execSync("git diff --shortstat -M", {
|
|
606
|
+
cwd,
|
|
607
|
+
encoding: "utf-8",
|
|
608
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
609
|
+
}).trim();
|
|
610
|
+
let added = 0;
|
|
611
|
+
let removed = 0;
|
|
612
|
+
const addedMatch = shortstat.match(/(\d+) insertion/);
|
|
613
|
+
const removedMatch = shortstat.match(/(\d+) deletion/);
|
|
614
|
+
if (addedMatch) {
|
|
615
|
+
added = parseInt(addedMatch[1], 10);
|
|
616
|
+
}
|
|
617
|
+
if (removedMatch) {
|
|
618
|
+
removed = parseInt(removedMatch[1], 10);
|
|
619
|
+
}
|
|
620
|
+
const fullDiff = execSync("git diff -M -C", {
|
|
621
|
+
cwd,
|
|
622
|
+
encoding: "utf-8",
|
|
623
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
624
|
+
});
|
|
625
|
+
return {
|
|
626
|
+
added,
|
|
627
|
+
removed,
|
|
628
|
+
fullDiff
|
|
629
|
+
};
|
|
630
|
+
} catch (error) {
|
|
631
|
+
console.error("Error getting git diff:", error);
|
|
632
|
+
return null;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// src/index.ts
|
|
637
|
+
var READY_MESSAGE = "========= REPLICAS WORKSPACE READY ==========";
|
|
638
|
+
var COMPLETION_MESSAGE = "========= REPLICAS WORKSPACE INITIALIZATION COMPLETE ==========";
|
|
639
|
+
var app = new Hono3();
|
|
640
|
+
app.get("/health", async (c) => {
|
|
641
|
+
try {
|
|
642
|
+
const logContent = await readFile2("/var/log/cloud-init-output.log", "utf-8");
|
|
643
|
+
let status;
|
|
644
|
+
if (logContent.includes(COMPLETION_MESSAGE)) {
|
|
645
|
+
status = "active";
|
|
646
|
+
} else if (logContent.includes(READY_MESSAGE)) {
|
|
647
|
+
status = "ready";
|
|
648
|
+
} else {
|
|
649
|
+
status = "initializing";
|
|
650
|
+
}
|
|
651
|
+
return c.json({ status, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
652
|
+
} catch (error) {
|
|
653
|
+
return c.json({ status: "initializing", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
654
|
+
}
|
|
655
|
+
});
|
|
656
|
+
app.get("/status", async (c) => {
|
|
657
|
+
try {
|
|
658
|
+
const isCodexProcessing = codexManager.isProcessing();
|
|
659
|
+
const isClaudeProcessing = claudeManager.isProcessing();
|
|
660
|
+
const codexHistory = await codexManager.getHistory();
|
|
661
|
+
const claudeHistory = await claudeManager.getHistory();
|
|
662
|
+
const isCodexUsed = codexHistory.thread_id !== null;
|
|
663
|
+
const isClaudeUsed = claudeHistory.thread_id !== null;
|
|
664
|
+
const claudeStatus = await claudeManager.getStatus();
|
|
665
|
+
const workingDirectory = claudeStatus.working_directory;
|
|
666
|
+
const branch = getCurrentBranch(workingDirectory);
|
|
667
|
+
const gitDiff = getGitDiff(workingDirectory);
|
|
668
|
+
return c.json({
|
|
669
|
+
isCodexProcessing,
|
|
670
|
+
isClaudeProcessing,
|
|
671
|
+
isCodexUsed,
|
|
672
|
+
isClaudeUsed,
|
|
673
|
+
branch,
|
|
674
|
+
gitDiff
|
|
675
|
+
});
|
|
676
|
+
} catch (error) {
|
|
677
|
+
console.error("Error getting workspace status:", error);
|
|
678
|
+
return c.json(
|
|
679
|
+
{
|
|
680
|
+
error: "Failed to get workspace status",
|
|
681
|
+
details: error instanceof Error ? error.message : "Unknown error"
|
|
682
|
+
},
|
|
683
|
+
500
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
});
|
|
687
|
+
app.use("*", authMiddleware);
|
|
688
|
+
app.route("/codex", codex_default);
|
|
689
|
+
app.route("/claude", claude_default);
|
|
690
|
+
var port = Number(process.env.PORT) || 3737;
|
|
691
|
+
serve(
|
|
692
|
+
{
|
|
693
|
+
fetch: app.fetch,
|
|
694
|
+
port
|
|
695
|
+
},
|
|
696
|
+
(info) => {
|
|
697
|
+
console.log(`Replicas Engine running on port ${info.port}`);
|
|
698
|
+
}
|
|
699
|
+
);
|
package/package.json
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "replicas-engine",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.12",
|
|
4
4
|
"description": "Lightweight API server for Replicas workspaces",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"main": "dist/index.js",
|
|
6
|
+
"main": "dist/src/index.js",
|
|
7
7
|
"bin": {
|
|
8
|
-
"replicas-engine": "./dist/index.js"
|
|
8
|
+
"replicas-engine": "./dist/src/index.js"
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
"dist"
|
|
12
12
|
],
|
|
13
13
|
"scripts": {
|
|
14
14
|
"dev": "tsx watch src/index.ts",
|
|
15
|
-
"build": "
|
|
16
|
-
"start": "node dist/index.js",
|
|
15
|
+
"build": "tsup",
|
|
16
|
+
"start": "node dist/src/index.js",
|
|
17
|
+
"dev-sync": "scripts/dev-sync.sh",
|
|
17
18
|
"prepublishOnly": "yarn build"
|
|
18
19
|
},
|
|
19
20
|
"keywords": [
|
|
@@ -25,13 +26,16 @@
|
|
|
25
26
|
"author": "Replicas",
|
|
26
27
|
"license": "MIT",
|
|
27
28
|
"dependencies": {
|
|
29
|
+
"@anthropic-ai/claude-agent-sdk": "^0.1.30",
|
|
28
30
|
"@hono/node-server": "^1.19.5",
|
|
29
31
|
"@openai/codex-sdk": "^0.50.0",
|
|
30
32
|
"dotenv": "^17.2.3",
|
|
31
|
-
"hono": "^4.10.3"
|
|
33
|
+
"hono": "^4.10.3",
|
|
34
|
+
"zod": "3.24.1"
|
|
32
35
|
},
|
|
33
36
|
"devDependencies": {
|
|
34
37
|
"@types/node": "^20.11.17",
|
|
38
|
+
"tsup": "^8.5.0",
|
|
35
39
|
"tsx": "^4.7.1",
|
|
36
40
|
"typescript": "^5.8.3"
|
|
37
41
|
}
|
package/dist/index.js
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import 'dotenv/config';
|
|
3
|
-
import { serve } from '@hono/node-server';
|
|
4
|
-
import { Hono } from 'hono';
|
|
5
|
-
import { readFile } from 'fs/promises';
|
|
6
|
-
import { authMiddleware } from './middleware/auth.js';
|
|
7
|
-
import codex from './routes/codex.js';
|
|
8
|
-
// NOTE: These constants are duplicated in @replicas/shared/src/workspaces/WorkspaceInitializer.ts
|
|
9
|
-
// If you change these values, you MUST update both locations to keep them in sync.
|
|
10
|
-
const READY_MESSAGE = '========= REPLICAS WORKSPACE READY ==========';
|
|
11
|
-
const COMPLETION_MESSAGE = '========= REPLICAS WORKSPACE INITIALIZATION COMPLETE ==========';
|
|
12
|
-
const app = new Hono();
|
|
13
|
-
app.get('/health', async (c) => {
|
|
14
|
-
try {
|
|
15
|
-
const logContent = await readFile('/var/log/cloud-init-output.log', 'utf-8');
|
|
16
|
-
let status;
|
|
17
|
-
if (logContent.includes(COMPLETION_MESSAGE)) {
|
|
18
|
-
status = 'active';
|
|
19
|
-
}
|
|
20
|
-
else if (logContent.includes(READY_MESSAGE)) {
|
|
21
|
-
status = 'ready';
|
|
22
|
-
}
|
|
23
|
-
else {
|
|
24
|
-
status = 'initializing';
|
|
25
|
-
}
|
|
26
|
-
return c.json({ status, timestamp: new Date().toISOString() });
|
|
27
|
-
}
|
|
28
|
-
catch (error) {
|
|
29
|
-
return c.json({ status: 'initializing', timestamp: new Date().toISOString() });
|
|
30
|
-
}
|
|
31
|
-
});
|
|
32
|
-
app.use('*', authMiddleware);
|
|
33
|
-
app.route('/codex', codex);
|
|
34
|
-
const port = Number(process.env.PORT) || 3737;
|
|
35
|
-
serve({
|
|
36
|
-
fetch: app.fetch,
|
|
37
|
-
port,
|
|
38
|
-
}, (info) => {
|
|
39
|
-
console.log(`Replicas Engine running on port ${info.port}`);
|
|
40
|
-
});
|
package/dist/middleware/auth.js
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
export const authMiddleware = async (c, next) => {
|
|
2
|
-
const secret = c.req.header('X-Replicas-Engine-Secret');
|
|
3
|
-
const expectedSecret = process.env.REPLICAS_ENGINE_SECRET;
|
|
4
|
-
if (!expectedSecret) {
|
|
5
|
-
return c.json({ error: 'Server configuration error: REPLICAS_ENGINE_SECRET not set' }, 500);
|
|
6
|
-
}
|
|
7
|
-
if (!secret) {
|
|
8
|
-
return c.json({ error: 'Unauthorized: X-Replicas-Engine-Secret header required' }, 401);
|
|
9
|
-
}
|
|
10
|
-
if (secret !== expectedSecret) {
|
|
11
|
-
return c.json({ error: 'Unauthorized: Invalid secret' }, 401);
|
|
12
|
-
}
|
|
13
|
-
await next();
|
|
14
|
-
};
|
package/dist/routes/codex.js
DELETED
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
import { Hono } from 'hono';
|
|
2
|
-
import { CodexManager } from '../services/codex-manager.js';
|
|
3
|
-
const codex = new Hono();
|
|
4
|
-
const codexManager = new CodexManager();
|
|
5
|
-
/**
|
|
6
|
-
* POST /codex/send
|
|
7
|
-
* send a message to Codex (non-blocking, writes to JSONL automatically)
|
|
8
|
-
*/
|
|
9
|
-
codex.post('/send', async (c) => {
|
|
10
|
-
try {
|
|
11
|
-
const body = await c.req.json();
|
|
12
|
-
const { message } = body;
|
|
13
|
-
if (!message || typeof message !== 'string') {
|
|
14
|
-
return c.json({ error: 'Message is required and must be a string' }, 400);
|
|
15
|
-
}
|
|
16
|
-
await codexManager.sendMessage(message);
|
|
17
|
-
return c.json({
|
|
18
|
-
success: true,
|
|
19
|
-
message: 'Message sent successfully',
|
|
20
|
-
});
|
|
21
|
-
}
|
|
22
|
-
catch (error) {
|
|
23
|
-
console.error('Error in /codex/send:', error);
|
|
24
|
-
return c.json({
|
|
25
|
-
error: 'Failed to process message',
|
|
26
|
-
details: error instanceof Error ? error.message : 'Unknown error',
|
|
27
|
-
}, 500);
|
|
28
|
-
}
|
|
29
|
-
});
|
|
30
|
-
/**
|
|
31
|
-
* GET /codex/history
|
|
32
|
-
* get the conversation history from the JSONL session file
|
|
33
|
-
*/
|
|
34
|
-
codex.get('/history', async (c) => {
|
|
35
|
-
try {
|
|
36
|
-
const history = await codexManager.getHistory();
|
|
37
|
-
if (!history.thread_id) {
|
|
38
|
-
return c.json({
|
|
39
|
-
message: 'No active thread',
|
|
40
|
-
thread_id: null,
|
|
41
|
-
events: [],
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
return c.json(history);
|
|
45
|
-
}
|
|
46
|
-
catch (error) {
|
|
47
|
-
console.error('Error in /codex/history:', error);
|
|
48
|
-
return c.json({
|
|
49
|
-
error: 'Failed to retrieve history',
|
|
50
|
-
details: error instanceof Error ? error.message : 'Unknown error',
|
|
51
|
-
}, 500);
|
|
52
|
-
}
|
|
53
|
-
});
|
|
54
|
-
/**
|
|
55
|
-
* GET /codex/updates
|
|
56
|
-
* get new events since a given timestamp (for polling)
|
|
57
|
-
*/
|
|
58
|
-
codex.get('/updates', async (c) => {
|
|
59
|
-
try {
|
|
60
|
-
const since = c.req.query('since') || '';
|
|
61
|
-
if (!since) {
|
|
62
|
-
return c.json({ error: 'Missing "since" query parameter' }, 400);
|
|
63
|
-
}
|
|
64
|
-
const updates = await codexManager.getUpdates(since);
|
|
65
|
-
return c.json(updates);
|
|
66
|
-
}
|
|
67
|
-
catch (error) {
|
|
68
|
-
console.error('Error in /codex/updates:', error);
|
|
69
|
-
return c.json({
|
|
70
|
-
error: 'Failed to retrieve updates',
|
|
71
|
-
details: error instanceof Error ? error.message : 'Unknown error',
|
|
72
|
-
}, 500);
|
|
73
|
-
}
|
|
74
|
-
});
|
|
75
|
-
/**
|
|
76
|
-
* GET /codex/status
|
|
77
|
-
* get current thread status and information
|
|
78
|
-
*/
|
|
79
|
-
codex.get('/status', async (c) => {
|
|
80
|
-
try {
|
|
81
|
-
const status = await codexManager.getStatus();
|
|
82
|
-
return c.json(status);
|
|
83
|
-
}
|
|
84
|
-
catch (error) {
|
|
85
|
-
console.error('Error in /codex/status:', error);
|
|
86
|
-
return c.json({
|
|
87
|
-
error: 'Failed to retrieve status',
|
|
88
|
-
details: error instanceof Error ? error.message : 'Unknown error',
|
|
89
|
-
}, 500);
|
|
90
|
-
}
|
|
91
|
-
});
|
|
92
|
-
/**
|
|
93
|
-
* POST /codex/reset
|
|
94
|
-
* reset the current thread and start fresh
|
|
95
|
-
*/
|
|
96
|
-
codex.post('/reset', async (c) => {
|
|
97
|
-
try {
|
|
98
|
-
codexManager.reset();
|
|
99
|
-
return c.json({
|
|
100
|
-
message: 'Thread reset successfully',
|
|
101
|
-
success: true,
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
catch (error) {
|
|
105
|
-
console.error('Error in /codex/reset:', error);
|
|
106
|
-
return c.json({
|
|
107
|
-
error: 'Failed to reset thread',
|
|
108
|
-
details: error instanceof Error ? error.message : 'Unknown error',
|
|
109
|
-
}, 500);
|
|
110
|
-
}
|
|
111
|
-
});
|
|
112
|
-
export default codex;
|
package/dist/routes/ping.js
DELETED
|
@@ -1,210 +0,0 @@
|
|
|
1
|
-
import { Codex, Thread } from '@openai/codex-sdk';
|
|
2
|
-
import { randomUUID } from 'crypto';
|
|
3
|
-
import { findSessionFile, readJSONL } from '../utils/jsonl-reader.js';
|
|
4
|
-
export class CodexManager {
|
|
5
|
-
codex;
|
|
6
|
-
currentThreadId = null;
|
|
7
|
-
currentThread = null;
|
|
8
|
-
workingDirectory;
|
|
9
|
-
constructor(workingDirectory) {
|
|
10
|
-
this.codex = new Codex();
|
|
11
|
-
if (workingDirectory) {
|
|
12
|
-
this.workingDirectory = workingDirectory;
|
|
13
|
-
}
|
|
14
|
-
else {
|
|
15
|
-
const repoName = process.env.REPLICAS_REPO_NAME;
|
|
16
|
-
const workspaceHome = process.env.WORKSPACE_HOME || process.env.HOME || '/home/ubuntu';
|
|
17
|
-
if (repoName) {
|
|
18
|
-
this.workingDirectory = `${workspaceHome}/workspaces/${repoName}`;
|
|
19
|
-
console.log(`Using repository working directory: ${this.workingDirectory}`);
|
|
20
|
-
}
|
|
21
|
-
else {
|
|
22
|
-
this.workingDirectory = workspaceHome;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
async sendMessage(message) {
|
|
27
|
-
console.log(`[CodexManager] sendMessage called with message length: ${message.length}`);
|
|
28
|
-
if (!this.currentThread) {
|
|
29
|
-
if (this.currentThreadId) {
|
|
30
|
-
console.log(`[CodexManager] Resuming existing thread: ${this.currentThreadId}`);
|
|
31
|
-
this.currentThread = this.codex.resumeThread(this.currentThreadId, {
|
|
32
|
-
workingDirectory: this.workingDirectory,
|
|
33
|
-
skipGitRepoCheck: true,
|
|
34
|
-
sandboxMode: 'danger-full-access',
|
|
35
|
-
});
|
|
36
|
-
console.log(`[CodexManager] Thread resumed successfully`);
|
|
37
|
-
}
|
|
38
|
-
else {
|
|
39
|
-
console.log('[CodexManager] No existing thread, starting new thread');
|
|
40
|
-
console.log(`[CodexManager] Working directory: ${this.workingDirectory}`);
|
|
41
|
-
const thread = this.codex.startThread({
|
|
42
|
-
workingDirectory: this.workingDirectory,
|
|
43
|
-
skipGitRepoCheck: true,
|
|
44
|
-
sandboxMode: 'danger-full-access',
|
|
45
|
-
});
|
|
46
|
-
this.currentThread = thread;
|
|
47
|
-
console.log(`[CodexManager] Thread object created, ID at creation: ${thread.id || 'null'}`);
|
|
48
|
-
// prime the thread with system instructions if thread is new
|
|
49
|
-
const workspaceName = process.env.WORKSPACE_NAME || 'workspace';
|
|
50
|
-
console.log(`[CodexManager] Workspace name from env: ${workspaceName}`);
|
|
51
|
-
const workspaceBranchSegment = workspaceName
|
|
52
|
-
.trim()
|
|
53
|
-
.toLowerCase()
|
|
54
|
-
.replace(/[^a-z0-9]+/g, '-')
|
|
55
|
-
.replace(/^-+|-+$/g, '') || 'workspace';
|
|
56
|
-
const uuid = randomUUID().slice(0, 8);
|
|
57
|
-
const branchName = `replicas/${workspaceBranchSegment}-${uuid}`;
|
|
58
|
-
console.log(`[CodexManager] Generated branch name: ${branchName}`);
|
|
59
|
-
const systemMessage = `<replicas_important_instructions>When completing solutions, push your changes to branch ${branchName} and to origin. Greet the user.</replicas_important_instructions>`;
|
|
60
|
-
console.log('[CodexManager] Starting thread priming with system instructions');
|
|
61
|
-
const primingStartTime = Date.now();
|
|
62
|
-
// Use runStreamed to capture thread.started event with thread_id
|
|
63
|
-
const streamedTurn = await this.currentThread.runStreamed(systemMessage);
|
|
64
|
-
console.log('[CodexManager] Processing priming stream events...');
|
|
65
|
-
for await (const event of streamedTurn.events) {
|
|
66
|
-
console.log(`[CodexManager] Event: ${event.type}`);
|
|
67
|
-
if (event.type === 'thread.started') {
|
|
68
|
-
this.currentThreadId = event.thread_id;
|
|
69
|
-
console.log(`[CodexManager] ✓ Thread ID captured from thread.started event: ${this.currentThreadId}`);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
const primingDuration = Date.now() - primingStartTime;
|
|
73
|
-
console.log(`[CodexManager] Thread priming completed in ${primingDuration}ms`);
|
|
74
|
-
// Double-check thread ID is available
|
|
75
|
-
if (!this.currentThreadId && this.currentThread.id) {
|
|
76
|
-
this.currentThreadId = this.currentThread.id;
|
|
77
|
-
console.log(`[CodexManager] Thread ID fallback from thread.id property: ${this.currentThreadId}`);
|
|
78
|
-
}
|
|
79
|
-
if (!this.currentThreadId) {
|
|
80
|
-
console.error('[CodexManager] ERROR: Thread ID still not available after priming run');
|
|
81
|
-
console.error(`[CodexManager] thread.id = ${this.currentThread.id}`);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
else {
|
|
86
|
-
console.log(`[CodexManager] Using existing thread object for thread ID: ${this.currentThreadId}`);
|
|
87
|
-
}
|
|
88
|
-
console.log(`[CodexManager] Running user message on thread ${this.currentThreadId}`);
|
|
89
|
-
const messageStartTime = Date.now();
|
|
90
|
-
await this.currentThread.run(message);
|
|
91
|
-
const messageDuration = Date.now() - messageStartTime;
|
|
92
|
-
console.log(`[CodexManager] User message run completed in ${messageDuration}ms`);
|
|
93
|
-
}
|
|
94
|
-
async getHistory() {
|
|
95
|
-
console.log('[CodexManager] getHistory called');
|
|
96
|
-
if (!this.currentThreadId) {
|
|
97
|
-
console.log('[CodexManager] No active thread ID, returning empty history');
|
|
98
|
-
return {
|
|
99
|
-
thread_id: null,
|
|
100
|
-
events: [],
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
console.log(`[CodexManager] Looking for session file for thread: ${this.currentThreadId}`);
|
|
104
|
-
const sessionFile = await findSessionFile(this.currentThreadId);
|
|
105
|
-
if (!sessionFile) {
|
|
106
|
-
console.warn(`[CodexManager] WARNING: Session file not found for thread ${this.currentThreadId}`);
|
|
107
|
-
return {
|
|
108
|
-
thread_id: this.currentThreadId,
|
|
109
|
-
events: [],
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
console.log(`[CodexManager] Reading session file: ${sessionFile}`);
|
|
113
|
-
const events = await readJSONL(sessionFile);
|
|
114
|
-
console.log(`[CodexManager] Read ${events.length} events from session file`);
|
|
115
|
-
// Filter out priming instruction events
|
|
116
|
-
let filteredEvents = events.filter((event) => {
|
|
117
|
-
const eventStr = JSON.stringify(event);
|
|
118
|
-
return !eventStr.includes('<replicas_important_instructions>');
|
|
119
|
-
});
|
|
120
|
-
// Find the first real user message (after priming)
|
|
121
|
-
const firstUserMessageIndex = filteredEvents.findIndex((event) => {
|
|
122
|
-
return event.type === 'response_item' &&
|
|
123
|
-
event.payload?.type === 'message' &&
|
|
124
|
-
event.payload?.role === 'user';
|
|
125
|
-
});
|
|
126
|
-
// If we found a user message, remove all events before it (priming response)
|
|
127
|
-
if (firstUserMessageIndex > 0) {
|
|
128
|
-
const beforeCount = filteredEvents.length;
|
|
129
|
-
filteredEvents = filteredEvents.slice(firstUserMessageIndex);
|
|
130
|
-
console.log(`[CodexManager] Filtered out ${beforeCount - filteredEvents.length} events before first user message`);
|
|
131
|
-
}
|
|
132
|
-
const totalFilteredCount = events.length - filteredEvents.length;
|
|
133
|
-
if (totalFilteredCount > 0) {
|
|
134
|
-
console.log(`[CodexManager] Total filtered: ${totalFilteredCount} events (priming + response)`);
|
|
135
|
-
}
|
|
136
|
-
console.log(`[CodexManager] Returning ${filteredEvents.length} events`);
|
|
137
|
-
return {
|
|
138
|
-
thread_id: this.currentThreadId,
|
|
139
|
-
events: filteredEvents,
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
async getStatus() {
|
|
143
|
-
console.log('[CodexManager] getStatus called');
|
|
144
|
-
let sessionFile = null;
|
|
145
|
-
if (this.currentThreadId) {
|
|
146
|
-
console.log(`[CodexManager] Checking for session file for thread: ${this.currentThreadId}`);
|
|
147
|
-
sessionFile = await findSessionFile(this.currentThreadId);
|
|
148
|
-
if (sessionFile) {
|
|
149
|
-
console.log(`[CodexManager] Session file found: ${sessionFile}`);
|
|
150
|
-
}
|
|
151
|
-
else {
|
|
152
|
-
console.log('[CodexManager] Session file not found');
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
else {
|
|
156
|
-
console.log('[CodexManager] No active thread ID');
|
|
157
|
-
}
|
|
158
|
-
const status = {
|
|
159
|
-
has_active_thread: this.currentThreadId !== null,
|
|
160
|
-
thread_id: this.currentThreadId,
|
|
161
|
-
session_file: sessionFile,
|
|
162
|
-
working_directory: this.workingDirectory,
|
|
163
|
-
};
|
|
164
|
-
console.log(`[CodexManager] Status: ${JSON.stringify(status)}`);
|
|
165
|
-
return status;
|
|
166
|
-
}
|
|
167
|
-
reset() {
|
|
168
|
-
console.log(`[CodexManager] Resetting thread (was: ${this.currentThreadId})`);
|
|
169
|
-
this.currentThread = null;
|
|
170
|
-
this.currentThreadId = null;
|
|
171
|
-
console.log('[CodexManager] Thread reset complete');
|
|
172
|
-
}
|
|
173
|
-
getThreadId() {
|
|
174
|
-
return this.currentThreadId;
|
|
175
|
-
}
|
|
176
|
-
async getUpdates(since) {
|
|
177
|
-
console.log(`[CodexManager] getUpdates called with since: ${since}`);
|
|
178
|
-
if (!this.currentThreadId) {
|
|
179
|
-
console.log('[CodexManager] No active thread, returning empty updates');
|
|
180
|
-
return {
|
|
181
|
-
events: [],
|
|
182
|
-
isComplete: true,
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
const sessionFile = await findSessionFile(this.currentThreadId);
|
|
186
|
-
if (!sessionFile) {
|
|
187
|
-
console.log('[CodexManager] Session file not found, returning empty updates');
|
|
188
|
-
return {
|
|
189
|
-
events: [],
|
|
190
|
-
isComplete: true,
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
const allEvents = await readJSONL(sessionFile);
|
|
194
|
-
console.log(`[CodexManager] Read ${allEvents.length} total events`);
|
|
195
|
-
// Filter events that occurred after the 'since' timestamp
|
|
196
|
-
const filteredEvents = allEvents.filter((event) => {
|
|
197
|
-
return event.timestamp > since;
|
|
198
|
-
});
|
|
199
|
-
console.log(`[CodexManager] Found ${filteredEvents.length} events since ${since}`);
|
|
200
|
-
// Check if thread is complete by looking for turn.completed or error events
|
|
201
|
-
const isComplete = this.currentThread === null ||
|
|
202
|
-
allEvents.some((event) => event.type === 'event_msg' &&
|
|
203
|
-
(event.payload?.type === 'turn.completed' || event.payload?.type === 'error'));
|
|
204
|
-
console.log(`[CodexManager] Thread complete: ${isComplete}`);
|
|
205
|
-
return {
|
|
206
|
-
events: filteredEvents,
|
|
207
|
-
isComplete,
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
|
-
}
|
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
import { readFile, readdir, stat } from 'fs/promises';
|
|
2
|
-
import { join } from 'path';
|
|
3
|
-
import { homedir } from 'os';
|
|
4
|
-
export async function readJSONL(filePath) {
|
|
5
|
-
console.log(`[jsonl-reader] readJSONL called for: ${filePath}`);
|
|
6
|
-
try {
|
|
7
|
-
const content = await readFile(filePath, 'utf-8');
|
|
8
|
-
console.log(`[jsonl-reader] File read successfully, content length: ${content.length} bytes`);
|
|
9
|
-
const lines = content.split('\n').filter((line) => line.trim());
|
|
10
|
-
console.log(`[jsonl-reader] Found ${lines.length} non-empty lines`);
|
|
11
|
-
const events = lines
|
|
12
|
-
.map((line, index) => {
|
|
13
|
-
try {
|
|
14
|
-
return JSON.parse(line);
|
|
15
|
-
}
|
|
16
|
-
catch (e) {
|
|
17
|
-
console.error(`[jsonl-reader] Failed to parse line ${index + 1}: ${line.substring(0, 100)}...`, e);
|
|
18
|
-
return null;
|
|
19
|
-
}
|
|
20
|
-
})
|
|
21
|
-
.filter((event) => event !== null);
|
|
22
|
-
console.log(`[jsonl-reader] Successfully parsed ${events.length} events`);
|
|
23
|
-
return events;
|
|
24
|
-
}
|
|
25
|
-
catch (error) {
|
|
26
|
-
console.error(`[jsonl-reader] ERROR: Failed to read JSONL file ${filePath}:`, error);
|
|
27
|
-
return [];
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
// Sessions are stored in ~/.codex/sessions/YYYY/MM/DD/rollout-*-{threadId}.jsonl
|
|
31
|
-
export async function findSessionFile(threadId) {
|
|
32
|
-
console.log(`[jsonl-reader] findSessionFile called for threadId: ${threadId}`);
|
|
33
|
-
const sessionsDir = join(homedir(), '.codex', 'sessions');
|
|
34
|
-
console.log(`[jsonl-reader] Sessions directory: ${sessionsDir}`);
|
|
35
|
-
try {
|
|
36
|
-
// current date for searching
|
|
37
|
-
const now = new Date();
|
|
38
|
-
const year = now.getFullYear();
|
|
39
|
-
const month = String(now.getMonth() + 1).padStart(2, '0');
|
|
40
|
-
const day = String(now.getDate()).padStart(2, '0');
|
|
41
|
-
const todayDir = join(sessionsDir, String(year), month, day);
|
|
42
|
-
console.log(`[jsonl-reader] Searching today's directory: ${todayDir}`);
|
|
43
|
-
const file = await findFileInDirectory(todayDir, threadId);
|
|
44
|
-
if (file) {
|
|
45
|
-
console.log(`[jsonl-reader] Found file in today's directory: ${file}`);
|
|
46
|
-
return file;
|
|
47
|
-
}
|
|
48
|
-
console.log(`[jsonl-reader] Not found in today's directory, searching previous 7 days`);
|
|
49
|
-
for (let daysAgo = 1; daysAgo <= 7; daysAgo++) {
|
|
50
|
-
const date = new Date(now);
|
|
51
|
-
date.setDate(date.getDate() - daysAgo);
|
|
52
|
-
const searchYear = date.getFullYear();
|
|
53
|
-
const searchMonth = String(date.getMonth() + 1).padStart(2, '0');
|
|
54
|
-
const searchDay = String(date.getDate()).padStart(2, '0');
|
|
55
|
-
const searchDir = join(sessionsDir, String(searchYear), searchMonth, searchDay);
|
|
56
|
-
console.log(`[jsonl-reader] Searching ${daysAgo} day(s) ago: ${searchDir}`);
|
|
57
|
-
const file = await findFileInDirectory(searchDir, threadId);
|
|
58
|
-
if (file) {
|
|
59
|
-
console.log(`[jsonl-reader] Found file ${daysAgo} day(s) ago: ${file}`);
|
|
60
|
-
return file;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
console.log(`[jsonl-reader] Session file not found after searching 8 days`);
|
|
64
|
-
return null;
|
|
65
|
-
}
|
|
66
|
-
catch (error) {
|
|
67
|
-
console.error('[jsonl-reader] ERROR finding session file:', error);
|
|
68
|
-
return null;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
async function findFileInDirectory(directory, threadId) {
|
|
72
|
-
try {
|
|
73
|
-
console.log(`[jsonl-reader] Reading directory: ${directory}`);
|
|
74
|
-
const files = await readdir(directory);
|
|
75
|
-
console.log(`[jsonl-reader] Found ${files.length} files in directory`);
|
|
76
|
-
const jsonlFiles = files.filter(f => f.endsWith('.jsonl'));
|
|
77
|
-
console.log(`[jsonl-reader] ${jsonlFiles.length} JSONL files: ${jsonlFiles.join(', ')}`);
|
|
78
|
-
for (const file of files) {
|
|
79
|
-
if (file.endsWith('.jsonl') && file.includes(threadId)) {
|
|
80
|
-
const fullPath = join(directory, file);
|
|
81
|
-
console.log(`[jsonl-reader] Found matching file: ${file} -> ${fullPath}`);
|
|
82
|
-
const stats = await stat(fullPath);
|
|
83
|
-
if (stats.isFile()) {
|
|
84
|
-
console.log(`[jsonl-reader] Confirmed as regular file, returning: ${fullPath}`);
|
|
85
|
-
return fullPath;
|
|
86
|
-
}
|
|
87
|
-
else {
|
|
88
|
-
console.log(`[jsonl-reader] WARNING: Path exists but is not a regular file`);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
console.log(`[jsonl-reader] No matching file found in ${directory}`);
|
|
93
|
-
return null;
|
|
94
|
-
}
|
|
95
|
-
catch (error) {
|
|
96
|
-
console.log(`[jsonl-reader] Directory read failed for ${directory}:`, error instanceof Error ? error.message : error);
|
|
97
|
-
return null;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
export async function getMostRecentSessionFile() {
|
|
101
|
-
const sessionsDir = join(homedir(), '.codex', 'sessions');
|
|
102
|
-
try {
|
|
103
|
-
const now = new Date();
|
|
104
|
-
for (let daysAgo = 0; daysAgo <= 7; daysAgo++) {
|
|
105
|
-
const date = new Date(now);
|
|
106
|
-
date.setDate(date.getDate() - daysAgo);
|
|
107
|
-
const year = date.getFullYear();
|
|
108
|
-
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
109
|
-
const day = String(date.getDate()).padStart(2, '0');
|
|
110
|
-
const searchDir = join(sessionsDir, String(year), month, day);
|
|
111
|
-
try {
|
|
112
|
-
const files = await readdir(searchDir);
|
|
113
|
-
const jsonlFiles = files
|
|
114
|
-
.filter((f) => f.endsWith('.jsonl'))
|
|
115
|
-
.map((f) => join(searchDir, f));
|
|
116
|
-
if (jsonlFiles.length > 0) {
|
|
117
|
-
const stats = await Promise.all(jsonlFiles.map(async (f) => ({
|
|
118
|
-
path: f,
|
|
119
|
-
mtime: (await stat(f)).mtime,
|
|
120
|
-
})));
|
|
121
|
-
stats.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
|
|
122
|
-
return stats[0].path;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
catch {
|
|
126
|
-
continue;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
return null;
|
|
130
|
-
}
|
|
131
|
-
catch (error) {
|
|
132
|
-
console.error('Error finding most recent session file:', error);
|
|
133
|
-
return null;
|
|
134
|
-
}
|
|
135
|
-
}
|