replicas-engine 0.1.19 → 0.1.21
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 +335 -31
- 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 readFile3 } 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,7 +56,7 @@ async function readJSONL(filePath) {
|
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
// src/services/codex-manager.ts
|
|
58
|
-
import { readdir, stat } from "fs/promises";
|
|
59
|
+
import { readdir, stat, writeFile, mkdir } from "fs/promises";
|
|
59
60
|
import { join } from "path";
|
|
60
61
|
import { homedir } from "os";
|
|
61
62
|
|
|
@@ -511,13 +512,14 @@ var MessageQueue = class {
|
|
|
511
512
|
* Add a message to the queue or start processing immediately if not busy
|
|
512
513
|
* @returns Object indicating whether the message was queued or started processing
|
|
513
514
|
*/
|
|
514
|
-
async enqueue(message, model, customInstructions) {
|
|
515
|
+
async enqueue(message, model, customInstructions, images) {
|
|
515
516
|
const messageId = this.generateMessageId();
|
|
516
517
|
const queuedMessage = {
|
|
517
518
|
id: messageId,
|
|
518
519
|
message,
|
|
519
520
|
model,
|
|
520
521
|
customInstructions,
|
|
522
|
+
images,
|
|
521
523
|
queuedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
522
524
|
};
|
|
523
525
|
if (this.processing) {
|
|
@@ -542,7 +544,8 @@ var MessageQueue = class {
|
|
|
542
544
|
await this.processMessage(
|
|
543
545
|
queuedMessage.message,
|
|
544
546
|
queuedMessage.model,
|
|
545
|
-
queuedMessage.customInstructions
|
|
547
|
+
queuedMessage.customInstructions,
|
|
548
|
+
queuedMessage.images
|
|
546
549
|
);
|
|
547
550
|
} catch (error) {
|
|
548
551
|
console.error("[MessageQueue] Error processing message:", error);
|
|
@@ -601,6 +604,66 @@ var MessageQueue = class {
|
|
|
601
604
|
}
|
|
602
605
|
};
|
|
603
606
|
|
|
607
|
+
// src/utils/image-utils.ts
|
|
608
|
+
function inferMediaType(url, contentType) {
|
|
609
|
+
if (contentType) {
|
|
610
|
+
const normalized = contentType.toLowerCase().split(";")[0].trim();
|
|
611
|
+
if (normalized === "image/png" || normalized === "image/jpeg" || normalized === "image/gif" || normalized === "image/webp") {
|
|
612
|
+
return normalized;
|
|
613
|
+
}
|
|
614
|
+
if (normalized === "image/jpg") {
|
|
615
|
+
return "image/jpeg";
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
const urlLower = url.toLowerCase();
|
|
619
|
+
if (urlLower.includes(".png")) return "image/png";
|
|
620
|
+
if (urlLower.includes(".jpg") || urlLower.includes(".jpeg")) return "image/jpeg";
|
|
621
|
+
if (urlLower.includes(".gif")) return "image/gif";
|
|
622
|
+
if (urlLower.includes(".webp")) return "image/webp";
|
|
623
|
+
return "image/jpeg";
|
|
624
|
+
}
|
|
625
|
+
async function fetchImageAsBase64(url) {
|
|
626
|
+
const headers = {};
|
|
627
|
+
if (new URL(url).hostname === "uploads.linear.app") {
|
|
628
|
+
const token = process.env.LINEAR_TOKEN;
|
|
629
|
+
if (token) {
|
|
630
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
const response = await fetch(url, { headers });
|
|
634
|
+
if (!response.ok) {
|
|
635
|
+
throw new Error(`Failed to fetch image from ${url}: ${response.status} ${response.statusText}`);
|
|
636
|
+
}
|
|
637
|
+
const contentType = response.headers.get("content-type");
|
|
638
|
+
const mediaType = inferMediaType(url, contentType);
|
|
639
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
640
|
+
const buffer = Buffer.from(arrayBuffer);
|
|
641
|
+
const data = buffer.toString("base64");
|
|
642
|
+
return {
|
|
643
|
+
type: "base64",
|
|
644
|
+
media_type: mediaType,
|
|
645
|
+
data
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
async function normalizeImages(images) {
|
|
649
|
+
const normalized = [];
|
|
650
|
+
for (const image of images) {
|
|
651
|
+
if (image.source.type === "base64") {
|
|
652
|
+
normalized.push({
|
|
653
|
+
type: "image",
|
|
654
|
+
source: image.source
|
|
655
|
+
});
|
|
656
|
+
} else if (image.source.type === "url") {
|
|
657
|
+
const base64Source = await fetchImageAsBase64(image.source.url);
|
|
658
|
+
normalized.push({
|
|
659
|
+
type: "image",
|
|
660
|
+
source: base64Source
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
return normalized;
|
|
665
|
+
}
|
|
666
|
+
|
|
604
667
|
// src/services/codex-manager.ts
|
|
605
668
|
var CodexManager = class {
|
|
606
669
|
codex;
|
|
@@ -608,6 +671,8 @@ var CodexManager = class {
|
|
|
608
671
|
currentThread = null;
|
|
609
672
|
workingDirectory;
|
|
610
673
|
messageQueue;
|
|
674
|
+
baseSystemPrompt;
|
|
675
|
+
tempImageDir;
|
|
611
676
|
constructor(workingDirectory) {
|
|
612
677
|
this.codex = new Codex();
|
|
613
678
|
if (workingDirectory) {
|
|
@@ -621,6 +686,7 @@ var CodexManager = class {
|
|
|
621
686
|
this.workingDirectory = workspaceHome;
|
|
622
687
|
}
|
|
623
688
|
}
|
|
689
|
+
this.tempImageDir = join(homedir(), ".replicas", "codex", "temp-images");
|
|
624
690
|
this.messageQueue = new MessageQueue(this.processMessageInternal.bind(this));
|
|
625
691
|
}
|
|
626
692
|
isProcessing() {
|
|
@@ -631,8 +697,8 @@ var CodexManager = class {
|
|
|
631
697
|
* If already processing, adds to queue.
|
|
632
698
|
* @returns Object with queued status, messageId, and position in queue
|
|
633
699
|
*/
|
|
634
|
-
async enqueueMessage(message, model, customInstructions) {
|
|
635
|
-
return this.messageQueue.enqueue(message, model, customInstructions);
|
|
700
|
+
async enqueueMessage(message, model, customInstructions, images) {
|
|
701
|
+
return this.messageQueue.enqueue(message, model, customInstructions, images);
|
|
636
702
|
}
|
|
637
703
|
/**
|
|
638
704
|
* Get the current queue status
|
|
@@ -640,19 +706,54 @@ var CodexManager = class {
|
|
|
640
706
|
getQueueStatus() {
|
|
641
707
|
return this.messageQueue.getStatus();
|
|
642
708
|
}
|
|
709
|
+
/**
|
|
710
|
+
* Set the base system prompt from replicas.json
|
|
711
|
+
* This will be combined with any custom instructions passed to individual messages
|
|
712
|
+
*/
|
|
713
|
+
setBaseSystemPrompt(prompt) {
|
|
714
|
+
this.baseSystemPrompt = prompt;
|
|
715
|
+
}
|
|
716
|
+
/**
|
|
717
|
+
* Get the current base system prompt
|
|
718
|
+
*/
|
|
719
|
+
getBaseSystemPrompt() {
|
|
720
|
+
return this.baseSystemPrompt;
|
|
721
|
+
}
|
|
643
722
|
/**
|
|
644
723
|
* Legacy sendMessage method - now uses the queue internally
|
|
645
724
|
* @deprecated Use enqueueMessage for better control over queue status
|
|
646
725
|
*/
|
|
647
|
-
async sendMessage(message, model, customInstructions) {
|
|
648
|
-
await this.enqueueMessage(message, model, customInstructions);
|
|
726
|
+
async sendMessage(message, model, customInstructions, images) {
|
|
727
|
+
await this.enqueueMessage(message, model, customInstructions, images);
|
|
728
|
+
}
|
|
729
|
+
/**
|
|
730
|
+
* Helper method to save normalized images to temp files for Codex SDK
|
|
731
|
+
* @returns Array of temp file paths
|
|
732
|
+
*/
|
|
733
|
+
async saveImagesToTempFiles(images) {
|
|
734
|
+
await mkdir(this.tempImageDir, { recursive: true });
|
|
735
|
+
const tempPaths = [];
|
|
736
|
+
for (const image of images) {
|
|
737
|
+
const ext = image.source.media_type.split("/")[1] || "png";
|
|
738
|
+
const filename = `img_${randomUUID()}.${ext}`;
|
|
739
|
+
const filepath = join(this.tempImageDir, filename);
|
|
740
|
+
const buffer = Buffer.from(image.source.data, "base64");
|
|
741
|
+
await writeFile(filepath, buffer);
|
|
742
|
+
tempPaths.push(filepath);
|
|
743
|
+
}
|
|
744
|
+
return tempPaths;
|
|
649
745
|
}
|
|
650
746
|
/**
|
|
651
747
|
* Internal method that actually processes the message
|
|
652
748
|
*/
|
|
653
|
-
async processMessageInternal(message, model, customInstructions) {
|
|
749
|
+
async processMessageInternal(message, model, customInstructions, images) {
|
|
654
750
|
const linearSessionId = process.env.LINEAR_SESSION_ID;
|
|
751
|
+
let tempImagePaths = [];
|
|
655
752
|
try {
|
|
753
|
+
if (images && images.length > 0) {
|
|
754
|
+
const normalizedImages = await normalizeImages(images);
|
|
755
|
+
tempImagePaths = await this.saveImagesToTempFiles(normalizedImages);
|
|
756
|
+
}
|
|
656
757
|
if (!this.currentThread) {
|
|
657
758
|
if (this.currentThreadId) {
|
|
658
759
|
this.currentThread = this.codex.resumeThread(this.currentThreadId, {
|
|
@@ -668,8 +769,16 @@ var CodexManager = class {
|
|
|
668
769
|
sandboxMode: "danger-full-access",
|
|
669
770
|
model: model || "gpt-5.1-codex"
|
|
670
771
|
});
|
|
671
|
-
|
|
672
|
-
|
|
772
|
+
let combinedInstructions;
|
|
773
|
+
if (this.baseSystemPrompt && customInstructions) {
|
|
774
|
+
combinedInstructions = `${this.baseSystemPrompt}
|
|
775
|
+
|
|
776
|
+
${customInstructions}`;
|
|
777
|
+
} else {
|
|
778
|
+
combinedInstructions = this.baseSystemPrompt || customInstructions;
|
|
779
|
+
}
|
|
780
|
+
if (combinedInstructions) {
|
|
781
|
+
message = combinedInstructions + "\n" + message;
|
|
673
782
|
}
|
|
674
783
|
const { events: events2 } = await this.currentThread.runStreamed("Hello");
|
|
675
784
|
for await (const event of events2) {
|
|
@@ -683,7 +792,17 @@ var CodexManager = class {
|
|
|
683
792
|
}
|
|
684
793
|
}
|
|
685
794
|
}
|
|
686
|
-
|
|
795
|
+
let input;
|
|
796
|
+
if (tempImagePaths.length > 0) {
|
|
797
|
+
const inputItems = [
|
|
798
|
+
{ type: "text", text: message },
|
|
799
|
+
...tempImagePaths.map((path5) => ({ type: "local_image", path: path5 }))
|
|
800
|
+
];
|
|
801
|
+
input = inputItems;
|
|
802
|
+
} else {
|
|
803
|
+
input = message;
|
|
804
|
+
}
|
|
805
|
+
const { events } = await this.currentThread.runStreamed(input);
|
|
687
806
|
for await (const event of events) {
|
|
688
807
|
if (linearSessionId) {
|
|
689
808
|
const linearEvent = convertCodexEvent(event, linearSessionId);
|
|
@@ -814,11 +933,11 @@ var codexManager = new CodexManager();
|
|
|
814
933
|
codex.post("/send", async (c) => {
|
|
815
934
|
try {
|
|
816
935
|
const body = await c.req.json();
|
|
817
|
-
const { message, model, customInstructions } = body;
|
|
936
|
+
const { message, model, customInstructions, images } = body;
|
|
818
937
|
if (!message || typeof message !== "string") {
|
|
819
938
|
return c.json({ error: "Message is required and must be a string" }, 400);
|
|
820
939
|
}
|
|
821
|
-
const result = await codexManager.enqueueMessage(message, model, customInstructions);
|
|
940
|
+
const result = await codexManager.enqueueMessage(message, model, customInstructions, images);
|
|
822
941
|
const response = {
|
|
823
942
|
success: true,
|
|
824
943
|
message: result.queued ? `Message queued at position ${result.position}` : "Message sent successfully",
|
|
@@ -937,7 +1056,7 @@ import {
|
|
|
937
1056
|
query
|
|
938
1057
|
} from "@anthropic-ai/claude-agent-sdk";
|
|
939
1058
|
import { join as join2 } from "path";
|
|
940
|
-
import { mkdir, appendFile, rm } from "fs/promises";
|
|
1059
|
+
import { mkdir as mkdir2, appendFile, rm } from "fs/promises";
|
|
941
1060
|
import { homedir as homedir2 } from "os";
|
|
942
1061
|
var ClaudeManager = class {
|
|
943
1062
|
workingDirectory;
|
|
@@ -945,6 +1064,7 @@ var ClaudeManager = class {
|
|
|
945
1064
|
sessionId = null;
|
|
946
1065
|
initialized;
|
|
947
1066
|
messageQueue;
|
|
1067
|
+
baseSystemPrompt;
|
|
948
1068
|
constructor(workingDirectory) {
|
|
949
1069
|
if (workingDirectory) {
|
|
950
1070
|
this.workingDirectory = workingDirectory;
|
|
@@ -969,9 +1089,9 @@ var ClaudeManager = class {
|
|
|
969
1089
|
* If already processing, adds to queue.
|
|
970
1090
|
* @returns Object with queued status, messageId, and position in queue
|
|
971
1091
|
*/
|
|
972
|
-
async enqueueMessage(message, model, customInstructions) {
|
|
1092
|
+
async enqueueMessage(message, model, customInstructions, images) {
|
|
973
1093
|
await this.initialized;
|
|
974
|
-
return this.messageQueue.enqueue(message, model, customInstructions);
|
|
1094
|
+
return this.messageQueue.enqueue(message, model, customInstructions, images);
|
|
975
1095
|
}
|
|
976
1096
|
/**
|
|
977
1097
|
* Get the current queue status
|
|
@@ -979,33 +1099,60 @@ var ClaudeManager = class {
|
|
|
979
1099
|
getQueueStatus() {
|
|
980
1100
|
return this.messageQueue.getStatus();
|
|
981
1101
|
}
|
|
1102
|
+
/**
|
|
1103
|
+
* Set the base system prompt from replicas.json
|
|
1104
|
+
* This will be combined with any custom instructions passed to individual messages
|
|
1105
|
+
*/
|
|
1106
|
+
setBaseSystemPrompt(prompt) {
|
|
1107
|
+
this.baseSystemPrompt = prompt;
|
|
1108
|
+
}
|
|
1109
|
+
/**
|
|
1110
|
+
* Get the current base system prompt
|
|
1111
|
+
*/
|
|
1112
|
+
getBaseSystemPrompt() {
|
|
1113
|
+
return this.baseSystemPrompt;
|
|
1114
|
+
}
|
|
982
1115
|
/**
|
|
983
1116
|
* Legacy sendMessage method - now uses the queue internally
|
|
984
1117
|
* @deprecated Use enqueueMessage for better control over queue status
|
|
985
1118
|
*/
|
|
986
|
-
async sendMessage(message, model, customInstructions) {
|
|
987
|
-
await this.enqueueMessage(message, model, customInstructions);
|
|
1119
|
+
async sendMessage(message, model, customInstructions, images) {
|
|
1120
|
+
await this.enqueueMessage(message, model, customInstructions, images);
|
|
988
1121
|
}
|
|
989
1122
|
/**
|
|
990
1123
|
* Internal method that actually processes the message
|
|
991
1124
|
*/
|
|
992
|
-
async processMessageInternal(message, model, customInstructions) {
|
|
1125
|
+
async processMessageInternal(message, model, customInstructions, images) {
|
|
993
1126
|
const linearSessionId = process.env.LINEAR_SESSION_ID;
|
|
994
1127
|
if (!message || !message.trim()) {
|
|
995
1128
|
throw new Error("Message cannot be empty");
|
|
996
1129
|
}
|
|
997
1130
|
await this.initialized;
|
|
998
1131
|
try {
|
|
1132
|
+
const content = [
|
|
1133
|
+
{
|
|
1134
|
+
type: "text",
|
|
1135
|
+
text: message
|
|
1136
|
+
}
|
|
1137
|
+
];
|
|
1138
|
+
if (images && images.length > 0) {
|
|
1139
|
+
const normalizedImages = await normalizeImages(images);
|
|
1140
|
+
for (const image of normalizedImages) {
|
|
1141
|
+
content.push({
|
|
1142
|
+
type: "image",
|
|
1143
|
+
source: {
|
|
1144
|
+
type: "base64",
|
|
1145
|
+
media_type: image.source.media_type,
|
|
1146
|
+
data: image.source.data
|
|
1147
|
+
}
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
999
1151
|
const userMessage = {
|
|
1000
1152
|
type: "user",
|
|
1001
1153
|
message: {
|
|
1002
1154
|
role: "user",
|
|
1003
|
-
content
|
|
1004
|
-
{
|
|
1005
|
-
type: "text",
|
|
1006
|
-
text: message
|
|
1007
|
-
}
|
|
1008
|
-
]
|
|
1155
|
+
content
|
|
1009
1156
|
},
|
|
1010
1157
|
parent_tool_use_id: null,
|
|
1011
1158
|
session_id: this.sessionId ?? ""
|
|
@@ -1014,6 +1161,14 @@ var ClaudeManager = class {
|
|
|
1014
1161
|
const promptIterable = (async function* () {
|
|
1015
1162
|
yield userMessage;
|
|
1016
1163
|
})();
|
|
1164
|
+
let combinedInstructions;
|
|
1165
|
+
if (this.baseSystemPrompt && customInstructions) {
|
|
1166
|
+
combinedInstructions = `${this.baseSystemPrompt}
|
|
1167
|
+
|
|
1168
|
+
${customInstructions}`;
|
|
1169
|
+
} else {
|
|
1170
|
+
combinedInstructions = this.baseSystemPrompt || customInstructions;
|
|
1171
|
+
}
|
|
1017
1172
|
const response = query({
|
|
1018
1173
|
prompt: promptIterable,
|
|
1019
1174
|
options: {
|
|
@@ -1025,7 +1180,7 @@ var ClaudeManager = class {
|
|
|
1025
1180
|
systemPrompt: {
|
|
1026
1181
|
type: "preset",
|
|
1027
1182
|
preset: "claude_code",
|
|
1028
|
-
append:
|
|
1183
|
+
append: combinedInstructions
|
|
1029
1184
|
},
|
|
1030
1185
|
env: process.env,
|
|
1031
1186
|
model: model || "opus"
|
|
@@ -1084,7 +1239,7 @@ var ClaudeManager = class {
|
|
|
1084
1239
|
}
|
|
1085
1240
|
async initialize() {
|
|
1086
1241
|
const historyDir = join2(homedir2(), ".replicas", "claude");
|
|
1087
|
-
await
|
|
1242
|
+
await mkdir2(historyDir, { recursive: true });
|
|
1088
1243
|
}
|
|
1089
1244
|
async handleMessage(message) {
|
|
1090
1245
|
if ("session_id" in message && message.session_id && !this.sessionId) {
|
|
@@ -1109,11 +1264,11 @@ var claudeManager = new ClaudeManager();
|
|
|
1109
1264
|
claude.post("/send", async (c) => {
|
|
1110
1265
|
try {
|
|
1111
1266
|
const body = await c.req.json();
|
|
1112
|
-
const { message, model, customInstructions } = body;
|
|
1267
|
+
const { message, model, customInstructions, images } = body;
|
|
1113
1268
|
if (!message || typeof message !== "string") {
|
|
1114
1269
|
return c.json({ error: "Message is required and must be a string" }, 400);
|
|
1115
1270
|
}
|
|
1116
|
-
const result = await claudeManager.enqueueMessage(message, model, customInstructions);
|
|
1271
|
+
const result = await claudeManager.enqueueMessage(message, model, customInstructions, images);
|
|
1117
1272
|
const response = {
|
|
1118
1273
|
success: true,
|
|
1119
1274
|
message: result.queued ? `Message queued at position ${result.position}` : "Message sent successfully",
|
|
@@ -1568,6 +1723,148 @@ async function initializeGitRepository() {
|
|
|
1568
1723
|
}
|
|
1569
1724
|
}
|
|
1570
1725
|
|
|
1726
|
+
// src/services/replicas-config.ts
|
|
1727
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
1728
|
+
import { existsSync as existsSync2 } from "fs";
|
|
1729
|
+
import { join as join3 } from "path";
|
|
1730
|
+
import { homedir as homedir3 } from "os";
|
|
1731
|
+
import { exec } from "child_process";
|
|
1732
|
+
import { promisify } from "util";
|
|
1733
|
+
var execAsync = promisify(exec);
|
|
1734
|
+
var ReplicasConfigService = class {
|
|
1735
|
+
config = null;
|
|
1736
|
+
workingDirectory;
|
|
1737
|
+
hooksExecuted = false;
|
|
1738
|
+
constructor() {
|
|
1739
|
+
const repoName = process.env.REPLICAS_REPO_NAME;
|
|
1740
|
+
const workspaceHome = process.env.WORKSPACE_HOME || process.env.HOME || homedir3();
|
|
1741
|
+
if (repoName) {
|
|
1742
|
+
this.workingDirectory = join3(workspaceHome, "workspaces", repoName);
|
|
1743
|
+
} else {
|
|
1744
|
+
this.workingDirectory = workspaceHome;
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
/**
|
|
1748
|
+
* Initialize the service by reading replicas.json and executing start hooks
|
|
1749
|
+
*/
|
|
1750
|
+
async initialize() {
|
|
1751
|
+
await this.loadConfig();
|
|
1752
|
+
await this.executeStartHooks();
|
|
1753
|
+
}
|
|
1754
|
+
/**
|
|
1755
|
+
* Load and parse the replicas.json config file
|
|
1756
|
+
*/
|
|
1757
|
+
async loadConfig() {
|
|
1758
|
+
const configPath = join3(this.workingDirectory, "replicas.json");
|
|
1759
|
+
if (!existsSync2(configPath)) {
|
|
1760
|
+
console.log("No replicas.json found in workspace directory");
|
|
1761
|
+
this.config = null;
|
|
1762
|
+
return;
|
|
1763
|
+
}
|
|
1764
|
+
try {
|
|
1765
|
+
const data = await readFile2(configPath, "utf-8");
|
|
1766
|
+
const config = JSON.parse(data);
|
|
1767
|
+
if (config.copy && !Array.isArray(config.copy)) {
|
|
1768
|
+
throw new Error('Invalid replicas.json: "copy" must be an array of file paths');
|
|
1769
|
+
}
|
|
1770
|
+
if (config.ports && !Array.isArray(config.ports)) {
|
|
1771
|
+
throw new Error('Invalid replicas.json: "ports" must be an array of port numbers');
|
|
1772
|
+
}
|
|
1773
|
+
if (config.ports && !config.ports.every((p) => typeof p === "number")) {
|
|
1774
|
+
throw new Error("Invalid replicas.json: all ports must be numbers");
|
|
1775
|
+
}
|
|
1776
|
+
if (config.systemPrompt && typeof config.systemPrompt !== "string") {
|
|
1777
|
+
throw new Error('Invalid replicas.json: "systemPrompt" must be a string');
|
|
1778
|
+
}
|
|
1779
|
+
if (config.startHook) {
|
|
1780
|
+
if (typeof config.startHook !== "object" || Array.isArray(config.startHook)) {
|
|
1781
|
+
throw new Error('Invalid replicas.json: "startHook" must be an object with "commands" array');
|
|
1782
|
+
}
|
|
1783
|
+
if (!Array.isArray(config.startHook.commands)) {
|
|
1784
|
+
throw new Error('Invalid replicas.json: "startHook.commands" must be an array of shell commands');
|
|
1785
|
+
}
|
|
1786
|
+
if (!config.startHook.commands.every((cmd) => typeof cmd === "string")) {
|
|
1787
|
+
throw new Error("Invalid replicas.json: all startHook.commands entries must be strings");
|
|
1788
|
+
}
|
|
1789
|
+
if (config.startHook.timeout !== void 0 && (typeof config.startHook.timeout !== "number" || config.startHook.timeout <= 0)) {
|
|
1790
|
+
throw new Error('Invalid replicas.json: "startHook.timeout" must be a positive number');
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
this.config = config;
|
|
1794
|
+
console.log("Loaded replicas.json config:", {
|
|
1795
|
+
hasSystemPrompt: !!config.systemPrompt,
|
|
1796
|
+
startHookCount: config.startHook?.commands.length ?? 0
|
|
1797
|
+
});
|
|
1798
|
+
} catch (error) {
|
|
1799
|
+
if (error instanceof SyntaxError) {
|
|
1800
|
+
console.error("Failed to parse replicas.json:", error.message);
|
|
1801
|
+
} else if (error instanceof Error) {
|
|
1802
|
+
console.error("Error loading replicas.json:", error.message);
|
|
1803
|
+
}
|
|
1804
|
+
this.config = null;
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
/**
|
|
1808
|
+
* Execute all start hooks defined in replicas.json
|
|
1809
|
+
*/
|
|
1810
|
+
async executeStartHooks() {
|
|
1811
|
+
if (this.hooksExecuted) {
|
|
1812
|
+
console.log("Start hooks already executed, skipping");
|
|
1813
|
+
return;
|
|
1814
|
+
}
|
|
1815
|
+
const startHookConfig = this.config?.startHook;
|
|
1816
|
+
if (!startHookConfig || startHookConfig.commands.length === 0) {
|
|
1817
|
+
this.hooksExecuted = true;
|
|
1818
|
+
return;
|
|
1819
|
+
}
|
|
1820
|
+
const timeout = startHookConfig.timeout ?? 3e5;
|
|
1821
|
+
const hooks = startHookConfig.commands;
|
|
1822
|
+
console.log(`Executing ${hooks.length} start hook(s) with timeout ${timeout}ms...`);
|
|
1823
|
+
for (const hook of hooks) {
|
|
1824
|
+
try {
|
|
1825
|
+
console.log(`Running start hook: ${hook}`);
|
|
1826
|
+
const { stdout, stderr } = await execAsync(hook, {
|
|
1827
|
+
cwd: this.workingDirectory,
|
|
1828
|
+
timeout,
|
|
1829
|
+
env: process.env
|
|
1830
|
+
});
|
|
1831
|
+
if (stdout) {
|
|
1832
|
+
console.log(`[${hook}] stdout:`, stdout);
|
|
1833
|
+
}
|
|
1834
|
+
if (stderr) {
|
|
1835
|
+
console.warn(`[${hook}] stderr:`, stderr);
|
|
1836
|
+
}
|
|
1837
|
+
console.log(`Start hook completed: ${hook}`);
|
|
1838
|
+
} catch (error) {
|
|
1839
|
+
if (error instanceof Error) {
|
|
1840
|
+
console.error(`Start hook failed: ${hook}`, error.message);
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
this.hooksExecuted = true;
|
|
1845
|
+
console.log("All start hooks completed");
|
|
1846
|
+
}
|
|
1847
|
+
/**
|
|
1848
|
+
* Get the system prompt from replicas.json
|
|
1849
|
+
*/
|
|
1850
|
+
getSystemPrompt() {
|
|
1851
|
+
return this.config?.systemPrompt;
|
|
1852
|
+
}
|
|
1853
|
+
/**
|
|
1854
|
+
* Get the full config object
|
|
1855
|
+
*/
|
|
1856
|
+
getConfig() {
|
|
1857
|
+
return this.config;
|
|
1858
|
+
}
|
|
1859
|
+
/**
|
|
1860
|
+
* Check if start hooks have been executed
|
|
1861
|
+
*/
|
|
1862
|
+
hasExecutedHooks() {
|
|
1863
|
+
return this.hooksExecuted;
|
|
1864
|
+
}
|
|
1865
|
+
};
|
|
1866
|
+
var replicasConfigService = new ReplicasConfigService();
|
|
1867
|
+
|
|
1571
1868
|
// src/index.ts
|
|
1572
1869
|
var READY_MESSAGE = "========= REPLICAS WORKSPACE READY ==========";
|
|
1573
1870
|
var COMPLETION_MESSAGE = "========= REPLICAS WORKSPACE INITIALIZATION COMPLETE ==========";
|
|
@@ -1583,7 +1880,7 @@ function checkActiveSSHSessions() {
|
|
|
1583
1880
|
var app = new Hono3();
|
|
1584
1881
|
app.get("/health", async (c) => {
|
|
1585
1882
|
try {
|
|
1586
|
-
const logContent = await
|
|
1883
|
+
const logContent = await readFile3("/var/log/cloud-init-output.log", "utf-8");
|
|
1587
1884
|
let status;
|
|
1588
1885
|
if (logContent.includes(COMPLETION_MESSAGE)) {
|
|
1589
1886
|
status = "active";
|
|
@@ -1648,6 +1945,13 @@ serve(
|
|
|
1648
1945
|
} else {
|
|
1649
1946
|
console.warn(`Git initialization warning: ${gitResult.error}`);
|
|
1650
1947
|
}
|
|
1948
|
+
await replicasConfigService.initialize();
|
|
1949
|
+
const systemPrompt = replicasConfigService.getSystemPrompt();
|
|
1950
|
+
if (systemPrompt) {
|
|
1951
|
+
claudeManager.setBaseSystemPrompt(systemPrompt);
|
|
1952
|
+
codexManager.setBaseSystemPrompt(systemPrompt);
|
|
1953
|
+
console.log("Applied system prompt from replicas.json to Claude and Codex managers");
|
|
1954
|
+
}
|
|
1651
1955
|
await githubTokenManager.start();
|
|
1652
1956
|
await claudeTokenManager.start();
|
|
1653
1957
|
await codexTokenManager.start();
|