replicas-engine 0.1.20 → 0.1.22
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 +278 -61
- package/package.json +1 -1
package/dist/src/index.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import "dotenv/config";
|
|
5
5
|
import { serve } from "@hono/node-server";
|
|
6
6
|
import { Hono as Hono3 } from "hono";
|
|
7
|
-
import { readFile as
|
|
7
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
8
8
|
import { execSync as execSync2 } from "child_process";
|
|
9
9
|
|
|
10
10
|
// src/middleware/auth.ts
|
|
@@ -34,6 +34,7 @@ import { Hono } from "hono";
|
|
|
34
34
|
|
|
35
35
|
// src/services/codex-manager.ts
|
|
36
36
|
import { Codex } from "@openai/codex-sdk";
|
|
37
|
+
import { randomUUID } from "crypto";
|
|
37
38
|
|
|
38
39
|
// src/utils/jsonl-reader.ts
|
|
39
40
|
import { readFile } from "fs/promises";
|
|
@@ -55,9 +56,9 @@ async function readJSONL(filePath) {
|
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
// src/services/codex-manager.ts
|
|
58
|
-
import { readdir, stat } from "fs/promises";
|
|
59
|
-
import { join } from "path";
|
|
60
|
-
import { homedir } from "os";
|
|
59
|
+
import { readdir, stat, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
|
|
60
|
+
import { join as join2 } from "path";
|
|
61
|
+
import { homedir as homedir2 } from "os";
|
|
61
62
|
|
|
62
63
|
// src/services/monolith-service.ts
|
|
63
64
|
var MonolithService = class {
|
|
@@ -384,6 +385,53 @@ function convertCodexEvent(event, linearSessionId) {
|
|
|
384
385
|
|
|
385
386
|
// src/utils/git.ts
|
|
386
387
|
import { execSync } from "child_process";
|
|
388
|
+
|
|
389
|
+
// src/services/engine-state.ts
|
|
390
|
+
import { readFile as readFile2, writeFile, mkdir } from "fs/promises";
|
|
391
|
+
import { existsSync } from "fs";
|
|
392
|
+
import { join } from "path";
|
|
393
|
+
import { homedir } from "os";
|
|
394
|
+
var STATE_DIR = join(homedir(), ".replicas");
|
|
395
|
+
var STATE_FILE = join(STATE_DIR, "engine-state.json");
|
|
396
|
+
var DEFAULT_STATE = {
|
|
397
|
+
branch: null,
|
|
398
|
+
prUrl: null,
|
|
399
|
+
claudeSessionId: null,
|
|
400
|
+
codexThreadId: null
|
|
401
|
+
};
|
|
402
|
+
async function loadEngineState() {
|
|
403
|
+
try {
|
|
404
|
+
if (!existsSync(STATE_FILE)) {
|
|
405
|
+
return { ...DEFAULT_STATE };
|
|
406
|
+
}
|
|
407
|
+
const content = await readFile2(STATE_FILE, "utf-8");
|
|
408
|
+
const state = JSON.parse(content);
|
|
409
|
+
return {
|
|
410
|
+
...DEFAULT_STATE,
|
|
411
|
+
...state
|
|
412
|
+
};
|
|
413
|
+
} catch (error) {
|
|
414
|
+
console.error("[EngineState] Failed to load state, using defaults:", error);
|
|
415
|
+
return { ...DEFAULT_STATE };
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
async function saveEngineState(state) {
|
|
419
|
+
try {
|
|
420
|
+
await mkdir(STATE_DIR, { recursive: true });
|
|
421
|
+
const currentState = await loadEngineState();
|
|
422
|
+
const newState = {
|
|
423
|
+
...currentState,
|
|
424
|
+
...state
|
|
425
|
+
};
|
|
426
|
+
await writeFile(STATE_FILE, JSON.stringify(newState, null, 2), "utf-8");
|
|
427
|
+
console.log("[EngineState] State saved:", newState);
|
|
428
|
+
} catch (error) {
|
|
429
|
+
console.error("[EngineState] Failed to save state:", error);
|
|
430
|
+
throw error;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// src/utils/git.ts
|
|
387
435
|
var cachedPr = null;
|
|
388
436
|
function runGitCommand(command, cwd) {
|
|
389
437
|
return execSync(command, {
|
|
@@ -442,7 +490,7 @@ function getGitDiff(cwd) {
|
|
|
442
490
|
return null;
|
|
443
491
|
}
|
|
444
492
|
}
|
|
445
|
-
function getPullRequestUrl(cwd) {
|
|
493
|
+
async function getPullRequestUrl(cwd) {
|
|
446
494
|
try {
|
|
447
495
|
const currentBranch = getCurrentBranch(cwd);
|
|
448
496
|
if (!currentBranch) {
|
|
@@ -451,6 +499,14 @@ function getPullRequestUrl(cwd) {
|
|
|
451
499
|
if (cachedPr && cachedPr.branch === currentBranch) {
|
|
452
500
|
return cachedPr.prUrl;
|
|
453
501
|
}
|
|
502
|
+
const persistedState = await loadEngineState();
|
|
503
|
+
if (persistedState.prUrl && persistedState.branch === currentBranch) {
|
|
504
|
+
cachedPr = {
|
|
505
|
+
prUrl: persistedState.prUrl,
|
|
506
|
+
branch: currentBranch
|
|
507
|
+
};
|
|
508
|
+
return cachedPr.prUrl;
|
|
509
|
+
}
|
|
454
510
|
cachedPr = null;
|
|
455
511
|
try {
|
|
456
512
|
const remoteRef = execSync(`git ls-remote --heads origin ${currentBranch}`, {
|
|
@@ -475,6 +531,7 @@ function getPullRequestUrl(cwd) {
|
|
|
475
531
|
prUrl: prInfo,
|
|
476
532
|
branch: currentBranch
|
|
477
533
|
};
|
|
534
|
+
await saveEngineState({ prUrl: prInfo });
|
|
478
535
|
return cachedPr.prUrl;
|
|
479
536
|
}
|
|
480
537
|
} catch {
|
|
@@ -486,11 +543,11 @@ function getPullRequestUrl(cwd) {
|
|
|
486
543
|
return null;
|
|
487
544
|
}
|
|
488
545
|
}
|
|
489
|
-
function getGitStatus(workingDirectory) {
|
|
546
|
+
async function getGitStatus(workingDirectory) {
|
|
490
547
|
return {
|
|
491
548
|
branch: getCurrentBranch(workingDirectory),
|
|
492
549
|
gitDiff: getGitDiff(workingDirectory),
|
|
493
|
-
prUrl: getPullRequestUrl(workingDirectory)
|
|
550
|
+
prUrl: await getPullRequestUrl(workingDirectory)
|
|
494
551
|
};
|
|
495
552
|
}
|
|
496
553
|
|
|
@@ -511,13 +568,14 @@ var MessageQueue = class {
|
|
|
511
568
|
* Add a message to the queue or start processing immediately if not busy
|
|
512
569
|
* @returns Object indicating whether the message was queued or started processing
|
|
513
570
|
*/
|
|
514
|
-
async enqueue(message, model, customInstructions) {
|
|
571
|
+
async enqueue(message, model, customInstructions, images) {
|
|
515
572
|
const messageId = this.generateMessageId();
|
|
516
573
|
const queuedMessage = {
|
|
517
574
|
id: messageId,
|
|
518
575
|
message,
|
|
519
576
|
model,
|
|
520
577
|
customInstructions,
|
|
578
|
+
images,
|
|
521
579
|
queuedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
522
580
|
};
|
|
523
581
|
if (this.processing) {
|
|
@@ -542,7 +600,8 @@ var MessageQueue = class {
|
|
|
542
600
|
await this.processMessage(
|
|
543
601
|
queuedMessage.message,
|
|
544
602
|
queuedMessage.model,
|
|
545
|
-
queuedMessage.customInstructions
|
|
603
|
+
queuedMessage.customInstructions,
|
|
604
|
+
queuedMessage.images
|
|
546
605
|
);
|
|
547
606
|
} catch (error) {
|
|
548
607
|
console.error("[MessageQueue] Error processing message:", error);
|
|
@@ -601,6 +660,66 @@ var MessageQueue = class {
|
|
|
601
660
|
}
|
|
602
661
|
};
|
|
603
662
|
|
|
663
|
+
// src/utils/image-utils.ts
|
|
664
|
+
function inferMediaType(url, contentType) {
|
|
665
|
+
if (contentType) {
|
|
666
|
+
const normalized = contentType.toLowerCase().split(";")[0].trim();
|
|
667
|
+
if (normalized === "image/png" || normalized === "image/jpeg" || normalized === "image/gif" || normalized === "image/webp") {
|
|
668
|
+
return normalized;
|
|
669
|
+
}
|
|
670
|
+
if (normalized === "image/jpg") {
|
|
671
|
+
return "image/jpeg";
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
const urlLower = url.toLowerCase();
|
|
675
|
+
if (urlLower.includes(".png")) return "image/png";
|
|
676
|
+
if (urlLower.includes(".jpg") || urlLower.includes(".jpeg")) return "image/jpeg";
|
|
677
|
+
if (urlLower.includes(".gif")) return "image/gif";
|
|
678
|
+
if (urlLower.includes(".webp")) return "image/webp";
|
|
679
|
+
return "image/jpeg";
|
|
680
|
+
}
|
|
681
|
+
async function fetchImageAsBase64(url) {
|
|
682
|
+
const headers = {};
|
|
683
|
+
if (new URL(url).hostname === "uploads.linear.app") {
|
|
684
|
+
const token = process.env.LINEAR_TOKEN;
|
|
685
|
+
if (token) {
|
|
686
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
const response = await fetch(url, { headers });
|
|
690
|
+
if (!response.ok) {
|
|
691
|
+
throw new Error(`Failed to fetch image from ${url}: ${response.status} ${response.statusText}`);
|
|
692
|
+
}
|
|
693
|
+
const contentType = response.headers.get("content-type");
|
|
694
|
+
const mediaType = inferMediaType(url, contentType);
|
|
695
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
696
|
+
const buffer = Buffer.from(arrayBuffer);
|
|
697
|
+
const data = buffer.toString("base64");
|
|
698
|
+
return {
|
|
699
|
+
type: "base64",
|
|
700
|
+
media_type: mediaType,
|
|
701
|
+
data
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
async function normalizeImages(images) {
|
|
705
|
+
const normalized = [];
|
|
706
|
+
for (const image of images) {
|
|
707
|
+
if (image.source.type === "base64") {
|
|
708
|
+
normalized.push({
|
|
709
|
+
type: "image",
|
|
710
|
+
source: image.source
|
|
711
|
+
});
|
|
712
|
+
} else if (image.source.type === "url") {
|
|
713
|
+
const base64Source = await fetchImageAsBase64(image.source.url);
|
|
714
|
+
normalized.push({
|
|
715
|
+
type: "image",
|
|
716
|
+
source: base64Source
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
return normalized;
|
|
721
|
+
}
|
|
722
|
+
|
|
604
723
|
// src/services/codex-manager.ts
|
|
605
724
|
var CodexManager = class {
|
|
606
725
|
codex;
|
|
@@ -609,20 +728,31 @@ var CodexManager = class {
|
|
|
609
728
|
workingDirectory;
|
|
610
729
|
messageQueue;
|
|
611
730
|
baseSystemPrompt;
|
|
731
|
+
tempImageDir;
|
|
732
|
+
initialized;
|
|
612
733
|
constructor(workingDirectory) {
|
|
613
734
|
this.codex = new Codex();
|
|
614
735
|
if (workingDirectory) {
|
|
615
736
|
this.workingDirectory = workingDirectory;
|
|
616
737
|
} else {
|
|
617
738
|
const repoName = process.env.REPLICAS_REPO_NAME;
|
|
618
|
-
const workspaceHome = process.env.WORKSPACE_HOME || process.env.HOME ||
|
|
739
|
+
const workspaceHome = process.env.WORKSPACE_HOME || process.env.HOME || homedir2();
|
|
619
740
|
if (repoName) {
|
|
620
|
-
this.workingDirectory =
|
|
741
|
+
this.workingDirectory = join2(workspaceHome, "workspaces", repoName);
|
|
621
742
|
} else {
|
|
622
743
|
this.workingDirectory = workspaceHome;
|
|
623
744
|
}
|
|
624
745
|
}
|
|
746
|
+
this.tempImageDir = join2(homedir2(), ".replicas", "codex", "temp-images");
|
|
625
747
|
this.messageQueue = new MessageQueue(this.processMessageInternal.bind(this));
|
|
748
|
+
this.initialized = this.initialize();
|
|
749
|
+
}
|
|
750
|
+
async initialize() {
|
|
751
|
+
const persistedState = await loadEngineState();
|
|
752
|
+
if (persistedState.codexThreadId) {
|
|
753
|
+
this.currentThreadId = persistedState.codexThreadId;
|
|
754
|
+
console.log(`[CodexManager] Restored thread ID from persisted state: ${this.currentThreadId}`);
|
|
755
|
+
}
|
|
626
756
|
}
|
|
627
757
|
isProcessing() {
|
|
628
758
|
return this.messageQueue.isProcessing();
|
|
@@ -632,8 +762,9 @@ var CodexManager = class {
|
|
|
632
762
|
* If already processing, adds to queue.
|
|
633
763
|
* @returns Object with queued status, messageId, and position in queue
|
|
634
764
|
*/
|
|
635
|
-
async enqueueMessage(message, model, customInstructions) {
|
|
636
|
-
|
|
765
|
+
async enqueueMessage(message, model, customInstructions, images) {
|
|
766
|
+
await this.initialized;
|
|
767
|
+
return this.messageQueue.enqueue(message, model, customInstructions, images);
|
|
637
768
|
}
|
|
638
769
|
/**
|
|
639
770
|
* Get the current queue status
|
|
@@ -658,15 +789,37 @@ var CodexManager = class {
|
|
|
658
789
|
* Legacy sendMessage method - now uses the queue internally
|
|
659
790
|
* @deprecated Use enqueueMessage for better control over queue status
|
|
660
791
|
*/
|
|
661
|
-
async sendMessage(message, model, customInstructions) {
|
|
662
|
-
await this.enqueueMessage(message, model, customInstructions);
|
|
792
|
+
async sendMessage(message, model, customInstructions, images) {
|
|
793
|
+
await this.enqueueMessage(message, model, customInstructions, images);
|
|
794
|
+
}
|
|
795
|
+
/**
|
|
796
|
+
* Helper method to save normalized images to temp files for Codex SDK
|
|
797
|
+
* @returns Array of temp file paths
|
|
798
|
+
*/
|
|
799
|
+
async saveImagesToTempFiles(images) {
|
|
800
|
+
await mkdir2(this.tempImageDir, { recursive: true });
|
|
801
|
+
const tempPaths = [];
|
|
802
|
+
for (const image of images) {
|
|
803
|
+
const ext = image.source.media_type.split("/")[1] || "png";
|
|
804
|
+
const filename = `img_${randomUUID()}.${ext}`;
|
|
805
|
+
const filepath = join2(this.tempImageDir, filename);
|
|
806
|
+
const buffer = Buffer.from(image.source.data, "base64");
|
|
807
|
+
await writeFile2(filepath, buffer);
|
|
808
|
+
tempPaths.push(filepath);
|
|
809
|
+
}
|
|
810
|
+
return tempPaths;
|
|
663
811
|
}
|
|
664
812
|
/**
|
|
665
813
|
* Internal method that actually processes the message
|
|
666
814
|
*/
|
|
667
|
-
async processMessageInternal(message, model, customInstructions) {
|
|
815
|
+
async processMessageInternal(message, model, customInstructions, images) {
|
|
668
816
|
const linearSessionId = process.env.LINEAR_SESSION_ID;
|
|
817
|
+
let tempImagePaths = [];
|
|
669
818
|
try {
|
|
819
|
+
if (images && images.length > 0) {
|
|
820
|
+
const normalizedImages = await normalizeImages(images);
|
|
821
|
+
tempImagePaths = await this.saveImagesToTempFiles(normalizedImages);
|
|
822
|
+
}
|
|
670
823
|
if (!this.currentThread) {
|
|
671
824
|
if (this.currentThreadId) {
|
|
672
825
|
this.currentThread = this.codex.resumeThread(this.currentThreadId, {
|
|
@@ -697,15 +850,29 @@ ${customInstructions}`;
|
|
|
697
850
|
for await (const event of events2) {
|
|
698
851
|
if (event.type === "thread.started") {
|
|
699
852
|
this.currentThreadId = event.thread_id;
|
|
853
|
+
await saveEngineState({ codexThreadId: this.currentThreadId });
|
|
854
|
+
console.log(`[CodexManager] Captured and persisted thread ID: ${this.currentThreadId}`);
|
|
700
855
|
break;
|
|
701
856
|
}
|
|
702
857
|
}
|
|
703
858
|
if (!this.currentThreadId && this.currentThread.id) {
|
|
704
859
|
this.currentThreadId = this.currentThread.id;
|
|
860
|
+
await saveEngineState({ codexThreadId: this.currentThreadId });
|
|
861
|
+
console.log(`[CodexManager] Captured and persisted thread ID from thread.id: ${this.currentThreadId}`);
|
|
705
862
|
}
|
|
706
863
|
}
|
|
707
864
|
}
|
|
708
|
-
|
|
865
|
+
let input;
|
|
866
|
+
if (tempImagePaths.length > 0) {
|
|
867
|
+
const inputItems = [
|
|
868
|
+
{ type: "text", text: message },
|
|
869
|
+
...tempImagePaths.map((path5) => ({ type: "local_image", path: path5 }))
|
|
870
|
+
];
|
|
871
|
+
input = inputItems;
|
|
872
|
+
} else {
|
|
873
|
+
input = message;
|
|
874
|
+
}
|
|
875
|
+
const { events } = await this.currentThread.runStreamed(input);
|
|
709
876
|
for await (const event of events) {
|
|
710
877
|
if (linearSessionId) {
|
|
711
878
|
const linearEvent = convertCodexEvent(event, linearSessionId);
|
|
@@ -717,7 +884,7 @@ ${customInstructions}`;
|
|
|
717
884
|
}
|
|
718
885
|
} finally {
|
|
719
886
|
if (linearSessionId) {
|
|
720
|
-
const status = getGitStatus(this.workingDirectory);
|
|
887
|
+
const status = await getGitStatus(this.workingDirectory);
|
|
721
888
|
monolithService.sendEvent({ type: "agent_turn_complete", payload: { linearSessionId, status } }).catch(() => {
|
|
722
889
|
});
|
|
723
890
|
}
|
|
@@ -759,6 +926,7 @@ ${customInstructions}`;
|
|
|
759
926
|
this.currentThread = null;
|
|
760
927
|
this.currentThreadId = null;
|
|
761
928
|
this.messageQueue.reset();
|
|
929
|
+
await saveEngineState({ codexThreadId: null });
|
|
762
930
|
}
|
|
763
931
|
getThreadId() {
|
|
764
932
|
return this.currentThreadId;
|
|
@@ -787,13 +955,13 @@ ${customInstructions}`;
|
|
|
787
955
|
}
|
|
788
956
|
// Helper methods for finding session files
|
|
789
957
|
async findSessionFile(threadId) {
|
|
790
|
-
const sessionsDir =
|
|
958
|
+
const sessionsDir = join2(homedir2(), ".codex", "sessions");
|
|
791
959
|
try {
|
|
792
960
|
const now = /* @__PURE__ */ new Date();
|
|
793
961
|
const year = now.getFullYear();
|
|
794
962
|
const month = String(now.getMonth() + 1).padStart(2, "0");
|
|
795
963
|
const day = String(now.getDate()).padStart(2, "0");
|
|
796
|
-
const todayDir =
|
|
964
|
+
const todayDir = join2(sessionsDir, String(year), month, day);
|
|
797
965
|
const file = await this.findFileInDirectory(todayDir, threadId);
|
|
798
966
|
if (file) return file;
|
|
799
967
|
for (let daysAgo = 1; daysAgo <= 7; daysAgo++) {
|
|
@@ -802,7 +970,7 @@ ${customInstructions}`;
|
|
|
802
970
|
const searchYear = date.getFullYear();
|
|
803
971
|
const searchMonth = String(date.getMonth() + 1).padStart(2, "0");
|
|
804
972
|
const searchDay = String(date.getDate()).padStart(2, "0");
|
|
805
|
-
const searchDir =
|
|
973
|
+
const searchDir = join2(sessionsDir, String(searchYear), searchMonth, searchDay);
|
|
806
974
|
const file2 = await this.findFileInDirectory(searchDir, threadId);
|
|
807
975
|
if (file2) return file2;
|
|
808
976
|
}
|
|
@@ -816,7 +984,7 @@ ${customInstructions}`;
|
|
|
816
984
|
const files = await readdir(directory);
|
|
817
985
|
for (const file of files) {
|
|
818
986
|
if (file.endsWith(".jsonl") && file.includes(threadId)) {
|
|
819
|
-
const fullPath =
|
|
987
|
+
const fullPath = join2(directory, file);
|
|
820
988
|
const stats = await stat(fullPath);
|
|
821
989
|
if (stats.isFile()) {
|
|
822
990
|
return fullPath;
|
|
@@ -836,11 +1004,11 @@ var codexManager = new CodexManager();
|
|
|
836
1004
|
codex.post("/send", async (c) => {
|
|
837
1005
|
try {
|
|
838
1006
|
const body = await c.req.json();
|
|
839
|
-
const { message, model, customInstructions } = body;
|
|
1007
|
+
const { message, model, customInstructions, images } = body;
|
|
840
1008
|
if (!message || typeof message !== "string") {
|
|
841
1009
|
return c.json({ error: "Message is required and must be a string" }, 400);
|
|
842
1010
|
}
|
|
843
|
-
const result = await codexManager.enqueueMessage(message, model, customInstructions);
|
|
1011
|
+
const result = await codexManager.enqueueMessage(message, model, customInstructions, images);
|
|
844
1012
|
const response = {
|
|
845
1013
|
success: true,
|
|
846
1014
|
message: result.queued ? `Message queued at position ${result.position}` : "Message sent successfully",
|
|
@@ -958,9 +1126,9 @@ import { Hono as Hono2 } from "hono";
|
|
|
958
1126
|
import {
|
|
959
1127
|
query
|
|
960
1128
|
} from "@anthropic-ai/claude-agent-sdk";
|
|
961
|
-
import { join as
|
|
962
|
-
import { mkdir, appendFile, rm } from "fs/promises";
|
|
963
|
-
import { homedir as
|
|
1129
|
+
import { join as join3 } from "path";
|
|
1130
|
+
import { mkdir as mkdir3, appendFile, rm } from "fs/promises";
|
|
1131
|
+
import { homedir as homedir3 } from "os";
|
|
964
1132
|
var ClaudeManager = class {
|
|
965
1133
|
workingDirectory;
|
|
966
1134
|
historyFile;
|
|
@@ -973,14 +1141,14 @@ var ClaudeManager = class {
|
|
|
973
1141
|
this.workingDirectory = workingDirectory;
|
|
974
1142
|
} else {
|
|
975
1143
|
const repoName = process.env.REPLICAS_REPO_NAME;
|
|
976
|
-
const workspaceHome = process.env.WORKSPACE_HOME || process.env.HOME ||
|
|
1144
|
+
const workspaceHome = process.env.WORKSPACE_HOME || process.env.HOME || homedir3();
|
|
977
1145
|
if (repoName) {
|
|
978
|
-
this.workingDirectory =
|
|
1146
|
+
this.workingDirectory = join3(workspaceHome, "workspaces", repoName);
|
|
979
1147
|
} else {
|
|
980
1148
|
this.workingDirectory = workspaceHome;
|
|
981
1149
|
}
|
|
982
1150
|
}
|
|
983
|
-
this.historyFile =
|
|
1151
|
+
this.historyFile = join3(homedir3(), ".replicas", "claude", "history.jsonl");
|
|
984
1152
|
this.initialized = this.initialize();
|
|
985
1153
|
this.messageQueue = new MessageQueue(this.processMessageInternal.bind(this));
|
|
986
1154
|
}
|
|
@@ -992,9 +1160,9 @@ var ClaudeManager = class {
|
|
|
992
1160
|
* If already processing, adds to queue.
|
|
993
1161
|
* @returns Object with queued status, messageId, and position in queue
|
|
994
1162
|
*/
|
|
995
|
-
async enqueueMessage(message, model, customInstructions) {
|
|
1163
|
+
async enqueueMessage(message, model, customInstructions, images) {
|
|
996
1164
|
await this.initialized;
|
|
997
|
-
return this.messageQueue.enqueue(message, model, customInstructions);
|
|
1165
|
+
return this.messageQueue.enqueue(message, model, customInstructions, images);
|
|
998
1166
|
}
|
|
999
1167
|
/**
|
|
1000
1168
|
* Get the current queue status
|
|
@@ -1019,29 +1187,43 @@ var ClaudeManager = class {
|
|
|
1019
1187
|
* Legacy sendMessage method - now uses the queue internally
|
|
1020
1188
|
* @deprecated Use enqueueMessage for better control over queue status
|
|
1021
1189
|
*/
|
|
1022
|
-
async sendMessage(message, model, customInstructions) {
|
|
1023
|
-
await this.enqueueMessage(message, model, customInstructions);
|
|
1190
|
+
async sendMessage(message, model, customInstructions, images) {
|
|
1191
|
+
await this.enqueueMessage(message, model, customInstructions, images);
|
|
1024
1192
|
}
|
|
1025
1193
|
/**
|
|
1026
1194
|
* Internal method that actually processes the message
|
|
1027
1195
|
*/
|
|
1028
|
-
async processMessageInternal(message, model, customInstructions) {
|
|
1196
|
+
async processMessageInternal(message, model, customInstructions, images) {
|
|
1029
1197
|
const linearSessionId = process.env.LINEAR_SESSION_ID;
|
|
1030
1198
|
if (!message || !message.trim()) {
|
|
1031
1199
|
throw new Error("Message cannot be empty");
|
|
1032
1200
|
}
|
|
1033
1201
|
await this.initialized;
|
|
1034
1202
|
try {
|
|
1203
|
+
const content = [
|
|
1204
|
+
{
|
|
1205
|
+
type: "text",
|
|
1206
|
+
text: message
|
|
1207
|
+
}
|
|
1208
|
+
];
|
|
1209
|
+
if (images && images.length > 0) {
|
|
1210
|
+
const normalizedImages = await normalizeImages(images);
|
|
1211
|
+
for (const image of normalizedImages) {
|
|
1212
|
+
content.push({
|
|
1213
|
+
type: "image",
|
|
1214
|
+
source: {
|
|
1215
|
+
type: "base64",
|
|
1216
|
+
media_type: image.source.media_type,
|
|
1217
|
+
data: image.source.data
|
|
1218
|
+
}
|
|
1219
|
+
});
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1035
1222
|
const userMessage = {
|
|
1036
1223
|
type: "user",
|
|
1037
1224
|
message: {
|
|
1038
1225
|
role: "user",
|
|
1039
|
-
content
|
|
1040
|
-
{
|
|
1041
|
-
type: "text",
|
|
1042
|
-
text: message
|
|
1043
|
-
}
|
|
1044
|
-
]
|
|
1226
|
+
content
|
|
1045
1227
|
},
|
|
1046
1228
|
parent_tool_use_id: null,
|
|
1047
1229
|
session_id: this.sessionId ?? ""
|
|
@@ -1087,7 +1269,7 @@ ${customInstructions}`;
|
|
|
1087
1269
|
}
|
|
1088
1270
|
} finally {
|
|
1089
1271
|
if (linearSessionId) {
|
|
1090
|
-
const status = getGitStatus(this.workingDirectory);
|
|
1272
|
+
const status = await getGitStatus(this.workingDirectory);
|
|
1091
1273
|
monolithService.sendEvent({ type: "agent_turn_complete", payload: { linearSessionId, status } }).catch(() => {
|
|
1092
1274
|
});
|
|
1093
1275
|
}
|
|
@@ -1121,18 +1303,26 @@ ${customInstructions}`;
|
|
|
1121
1303
|
await this.initialized;
|
|
1122
1304
|
this.sessionId = null;
|
|
1123
1305
|
this.messageQueue.reset();
|
|
1306
|
+
await saveEngineState({ claudeSessionId: null });
|
|
1124
1307
|
try {
|
|
1125
1308
|
await rm(this.historyFile, { force: true });
|
|
1126
1309
|
} catch {
|
|
1127
1310
|
}
|
|
1128
1311
|
}
|
|
1129
1312
|
async initialize() {
|
|
1130
|
-
const historyDir =
|
|
1131
|
-
await
|
|
1313
|
+
const historyDir = join3(homedir3(), ".replicas", "claude");
|
|
1314
|
+
await mkdir3(historyDir, { recursive: true });
|
|
1315
|
+
const persistedState = await loadEngineState();
|
|
1316
|
+
if (persistedState.claudeSessionId) {
|
|
1317
|
+
this.sessionId = persistedState.claudeSessionId;
|
|
1318
|
+
console.log(`[ClaudeManager] Restored session ID from persisted state: ${this.sessionId}`);
|
|
1319
|
+
}
|
|
1132
1320
|
}
|
|
1133
1321
|
async handleMessage(message) {
|
|
1134
1322
|
if ("session_id" in message && message.session_id && !this.sessionId) {
|
|
1135
1323
|
this.sessionId = message.session_id;
|
|
1324
|
+
await saveEngineState({ claudeSessionId: this.sessionId });
|
|
1325
|
+
console.log(`[ClaudeManager] Captured and persisted session ID: ${this.sessionId}`);
|
|
1136
1326
|
}
|
|
1137
1327
|
await this.recordEvent(message);
|
|
1138
1328
|
}
|
|
@@ -1153,11 +1343,11 @@ var claudeManager = new ClaudeManager();
|
|
|
1153
1343
|
claude.post("/send", async (c) => {
|
|
1154
1344
|
try {
|
|
1155
1345
|
const body = await c.req.json();
|
|
1156
|
-
const { message, model, customInstructions } = body;
|
|
1346
|
+
const { message, model, customInstructions, images } = body;
|
|
1157
1347
|
if (!message || typeof message !== "string") {
|
|
1158
1348
|
return c.json({ error: "Message is required and must be a string" }, 400);
|
|
1159
1349
|
}
|
|
1160
|
-
const result = await claudeManager.enqueueMessage(message, model, customInstructions);
|
|
1350
|
+
const result = await claudeManager.enqueueMessage(message, model, customInstructions, images);
|
|
1161
1351
|
const response = {
|
|
1162
1352
|
success: true,
|
|
1163
1353
|
message: result.queued ? `Message queued at position ${result.position}` : "Message sent successfully",
|
|
@@ -1526,7 +1716,7 @@ var CodexTokenManager = class {
|
|
|
1526
1716
|
var codexTokenManager = new CodexTokenManager();
|
|
1527
1717
|
|
|
1528
1718
|
// src/services/git-init.ts
|
|
1529
|
-
import { existsSync } from "fs";
|
|
1719
|
+
import { existsSync as existsSync2 } from "fs";
|
|
1530
1720
|
import path4 from "path";
|
|
1531
1721
|
var initializedBranch = null;
|
|
1532
1722
|
function findAvailableBranchName(baseName, cwd) {
|
|
@@ -1562,7 +1752,7 @@ async function initializeGitRepository() {
|
|
|
1562
1752
|
};
|
|
1563
1753
|
}
|
|
1564
1754
|
const repoPath = path4.join(workspaceHome, "workspaces", repoName);
|
|
1565
|
-
if (!
|
|
1755
|
+
if (!existsSync2(repoPath)) {
|
|
1566
1756
|
console.log(`[GitInit] Repository directory does not exist: ${repoPath}`);
|
|
1567
1757
|
console.log("[GitInit] Waiting for initializer to clone the repository...");
|
|
1568
1758
|
return {
|
|
@@ -1570,7 +1760,7 @@ async function initializeGitRepository() {
|
|
|
1570
1760
|
branch: null
|
|
1571
1761
|
};
|
|
1572
1762
|
}
|
|
1573
|
-
if (!
|
|
1763
|
+
if (!existsSync2(path4.join(repoPath, ".git"))) {
|
|
1574
1764
|
return {
|
|
1575
1765
|
success: false,
|
|
1576
1766
|
branch: null,
|
|
@@ -1579,8 +1769,32 @@ async function initializeGitRepository() {
|
|
|
1579
1769
|
}
|
|
1580
1770
|
console.log(`[GitInit] Initializing repository at ${repoPath}`);
|
|
1581
1771
|
try {
|
|
1772
|
+
const persistedState = await loadEngineState();
|
|
1773
|
+
const persistedBranch = persistedState.branch;
|
|
1582
1774
|
console.log("[GitInit] Fetching all remotes...");
|
|
1583
1775
|
runGitCommand("git fetch --all --prune", repoPath);
|
|
1776
|
+
if (persistedBranch && branchExists(persistedBranch, repoPath)) {
|
|
1777
|
+
console.log(`[GitInit] Found persisted branch: ${persistedBranch}`);
|
|
1778
|
+
const currentBranch = getCurrentBranch(repoPath);
|
|
1779
|
+
if (currentBranch === persistedBranch) {
|
|
1780
|
+
console.log(`[GitInit] Already on persisted branch: ${persistedBranch}`);
|
|
1781
|
+
initializedBranch = persistedBranch;
|
|
1782
|
+
return {
|
|
1783
|
+
success: true,
|
|
1784
|
+
branch: persistedBranch,
|
|
1785
|
+
resumed: true
|
|
1786
|
+
};
|
|
1787
|
+
}
|
|
1788
|
+
console.log(`[GitInit] Resuming on persisted branch: ${persistedBranch}`);
|
|
1789
|
+
runGitCommand(`git checkout ${persistedBranch}`, repoPath);
|
|
1790
|
+
initializedBranch = persistedBranch;
|
|
1791
|
+
console.log(`[GitInit] Successfully resumed on branch: ${persistedBranch}`);
|
|
1792
|
+
return {
|
|
1793
|
+
success: true,
|
|
1794
|
+
branch: persistedBranch,
|
|
1795
|
+
resumed: true
|
|
1796
|
+
};
|
|
1797
|
+
}
|
|
1584
1798
|
console.log(`[GitInit] Checking out default branch: ${defaultBranch}`);
|
|
1585
1799
|
runGitCommand(`git checkout ${defaultBranch}`, repoPath);
|
|
1586
1800
|
console.log("[GitInit] Pulling latest changes...");
|
|
@@ -1596,10 +1810,12 @@ async function initializeGitRepository() {
|
|
|
1596
1810
|
console.log(`[GitInit] Creating workspace branch: ${branchName}`);
|
|
1597
1811
|
runGitCommand(`git checkout -b ${branchName}`, repoPath);
|
|
1598
1812
|
initializedBranch = branchName;
|
|
1813
|
+
await saveEngineState({ branch: branchName });
|
|
1599
1814
|
console.log(`[GitInit] Successfully initialized on branch: ${branchName}`);
|
|
1600
1815
|
return {
|
|
1601
1816
|
success: true,
|
|
1602
|
-
branch: branchName
|
|
1817
|
+
branch: branchName,
|
|
1818
|
+
resumed: false
|
|
1603
1819
|
};
|
|
1604
1820
|
} catch (error) {
|
|
1605
1821
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
@@ -1613,10 +1829,10 @@ async function initializeGitRepository() {
|
|
|
1613
1829
|
}
|
|
1614
1830
|
|
|
1615
1831
|
// src/services/replicas-config.ts
|
|
1616
|
-
import { readFile as
|
|
1617
|
-
import { existsSync as
|
|
1618
|
-
import { join as
|
|
1619
|
-
import { homedir as
|
|
1832
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
1833
|
+
import { existsSync as existsSync3 } from "fs";
|
|
1834
|
+
import { join as join4 } from "path";
|
|
1835
|
+
import { homedir as homedir4 } from "os";
|
|
1620
1836
|
import { exec } from "child_process";
|
|
1621
1837
|
import { promisify } from "util";
|
|
1622
1838
|
var execAsync = promisify(exec);
|
|
@@ -1626,9 +1842,9 @@ var ReplicasConfigService = class {
|
|
|
1626
1842
|
hooksExecuted = false;
|
|
1627
1843
|
constructor() {
|
|
1628
1844
|
const repoName = process.env.REPLICAS_REPO_NAME;
|
|
1629
|
-
const workspaceHome = process.env.WORKSPACE_HOME || process.env.HOME ||
|
|
1845
|
+
const workspaceHome = process.env.WORKSPACE_HOME || process.env.HOME || homedir4();
|
|
1630
1846
|
if (repoName) {
|
|
1631
|
-
this.workingDirectory =
|
|
1847
|
+
this.workingDirectory = join4(workspaceHome, "workspaces", repoName);
|
|
1632
1848
|
} else {
|
|
1633
1849
|
this.workingDirectory = workspaceHome;
|
|
1634
1850
|
}
|
|
@@ -1644,14 +1860,14 @@ var ReplicasConfigService = class {
|
|
|
1644
1860
|
* Load and parse the replicas.json config file
|
|
1645
1861
|
*/
|
|
1646
1862
|
async loadConfig() {
|
|
1647
|
-
const configPath =
|
|
1648
|
-
if (!
|
|
1863
|
+
const configPath = join4(this.workingDirectory, "replicas.json");
|
|
1864
|
+
if (!existsSync3(configPath)) {
|
|
1649
1865
|
console.log("No replicas.json found in workspace directory");
|
|
1650
1866
|
this.config = null;
|
|
1651
1867
|
return;
|
|
1652
1868
|
}
|
|
1653
1869
|
try {
|
|
1654
|
-
const data = await
|
|
1870
|
+
const data = await readFile3(configPath, "utf-8");
|
|
1655
1871
|
const config = JSON.parse(data);
|
|
1656
1872
|
if (config.copy && !Array.isArray(config.copy)) {
|
|
1657
1873
|
throw new Error('Invalid replicas.json: "copy" must be an array of file paths');
|
|
@@ -1769,7 +1985,7 @@ function checkActiveSSHSessions() {
|
|
|
1769
1985
|
var app = new Hono3();
|
|
1770
1986
|
app.get("/health", async (c) => {
|
|
1771
1987
|
try {
|
|
1772
|
-
const logContent = await
|
|
1988
|
+
const logContent = await readFile4("/var/log/cloud-init-output.log", "utf-8");
|
|
1773
1989
|
let status;
|
|
1774
1990
|
if (logContent.includes(COMPLETION_MESSAGE)) {
|
|
1775
1991
|
status = "active";
|
|
@@ -1794,13 +2010,14 @@ app.get("/status", async (c) => {
|
|
|
1794
2010
|
const claudeStatus = await claudeManager.getStatus();
|
|
1795
2011
|
const workingDirectory = claudeStatus.working_directory;
|
|
1796
2012
|
const hasActiveSSHSessions = checkActiveSSHSessions();
|
|
2013
|
+
const gitStatus = await getGitStatus(workingDirectory);
|
|
1797
2014
|
return c.json({
|
|
1798
2015
|
isCodexProcessing,
|
|
1799
2016
|
isClaudeProcessing,
|
|
1800
2017
|
isCodexUsed,
|
|
1801
2018
|
isClaudeUsed,
|
|
1802
2019
|
hasActiveSSHSessions,
|
|
1803
|
-
...
|
|
2020
|
+
...gitStatus,
|
|
1804
2021
|
linearBetaEnabled: true
|
|
1805
2022
|
// TODO: delete
|
|
1806
2023
|
});
|