replicas-engine 0.1.20 → 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 +137 -26
- package/package.json +1 -1
package/dist/src/index.js
CHANGED
|
@@ -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;
|
|
@@ -609,6 +672,7 @@ var CodexManager = class {
|
|
|
609
672
|
workingDirectory;
|
|
610
673
|
messageQueue;
|
|
611
674
|
baseSystemPrompt;
|
|
675
|
+
tempImageDir;
|
|
612
676
|
constructor(workingDirectory) {
|
|
613
677
|
this.codex = new Codex();
|
|
614
678
|
if (workingDirectory) {
|
|
@@ -622,6 +686,7 @@ var CodexManager = class {
|
|
|
622
686
|
this.workingDirectory = workspaceHome;
|
|
623
687
|
}
|
|
624
688
|
}
|
|
689
|
+
this.tempImageDir = join(homedir(), ".replicas", "codex", "temp-images");
|
|
625
690
|
this.messageQueue = new MessageQueue(this.processMessageInternal.bind(this));
|
|
626
691
|
}
|
|
627
692
|
isProcessing() {
|
|
@@ -632,8 +697,8 @@ var CodexManager = class {
|
|
|
632
697
|
* If already processing, adds to queue.
|
|
633
698
|
* @returns Object with queued status, messageId, and position in queue
|
|
634
699
|
*/
|
|
635
|
-
async enqueueMessage(message, model, customInstructions) {
|
|
636
|
-
return this.messageQueue.enqueue(message, model, customInstructions);
|
|
700
|
+
async enqueueMessage(message, model, customInstructions, images) {
|
|
701
|
+
return this.messageQueue.enqueue(message, model, customInstructions, images);
|
|
637
702
|
}
|
|
638
703
|
/**
|
|
639
704
|
* Get the current queue status
|
|
@@ -658,15 +723,37 @@ var CodexManager = class {
|
|
|
658
723
|
* Legacy sendMessage method - now uses the queue internally
|
|
659
724
|
* @deprecated Use enqueueMessage for better control over queue status
|
|
660
725
|
*/
|
|
661
|
-
async sendMessage(message, model, customInstructions) {
|
|
662
|
-
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;
|
|
663
745
|
}
|
|
664
746
|
/**
|
|
665
747
|
* Internal method that actually processes the message
|
|
666
748
|
*/
|
|
667
|
-
async processMessageInternal(message, model, customInstructions) {
|
|
749
|
+
async processMessageInternal(message, model, customInstructions, images) {
|
|
668
750
|
const linearSessionId = process.env.LINEAR_SESSION_ID;
|
|
751
|
+
let tempImagePaths = [];
|
|
669
752
|
try {
|
|
753
|
+
if (images && images.length > 0) {
|
|
754
|
+
const normalizedImages = await normalizeImages(images);
|
|
755
|
+
tempImagePaths = await this.saveImagesToTempFiles(normalizedImages);
|
|
756
|
+
}
|
|
670
757
|
if (!this.currentThread) {
|
|
671
758
|
if (this.currentThreadId) {
|
|
672
759
|
this.currentThread = this.codex.resumeThread(this.currentThreadId, {
|
|
@@ -705,7 +792,17 @@ ${customInstructions}`;
|
|
|
705
792
|
}
|
|
706
793
|
}
|
|
707
794
|
}
|
|
708
|
-
|
|
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);
|
|
709
806
|
for await (const event of events) {
|
|
710
807
|
if (linearSessionId) {
|
|
711
808
|
const linearEvent = convertCodexEvent(event, linearSessionId);
|
|
@@ -836,11 +933,11 @@ var codexManager = new CodexManager();
|
|
|
836
933
|
codex.post("/send", async (c) => {
|
|
837
934
|
try {
|
|
838
935
|
const body = await c.req.json();
|
|
839
|
-
const { message, model, customInstructions } = body;
|
|
936
|
+
const { message, model, customInstructions, images } = body;
|
|
840
937
|
if (!message || typeof message !== "string") {
|
|
841
938
|
return c.json({ error: "Message is required and must be a string" }, 400);
|
|
842
939
|
}
|
|
843
|
-
const result = await codexManager.enqueueMessage(message, model, customInstructions);
|
|
940
|
+
const result = await codexManager.enqueueMessage(message, model, customInstructions, images);
|
|
844
941
|
const response = {
|
|
845
942
|
success: true,
|
|
846
943
|
message: result.queued ? `Message queued at position ${result.position}` : "Message sent successfully",
|
|
@@ -959,7 +1056,7 @@ import {
|
|
|
959
1056
|
query
|
|
960
1057
|
} from "@anthropic-ai/claude-agent-sdk";
|
|
961
1058
|
import { join as join2 } from "path";
|
|
962
|
-
import { mkdir, appendFile, rm } from "fs/promises";
|
|
1059
|
+
import { mkdir as mkdir2, appendFile, rm } from "fs/promises";
|
|
963
1060
|
import { homedir as homedir2 } from "os";
|
|
964
1061
|
var ClaudeManager = class {
|
|
965
1062
|
workingDirectory;
|
|
@@ -992,9 +1089,9 @@ var ClaudeManager = class {
|
|
|
992
1089
|
* If already processing, adds to queue.
|
|
993
1090
|
* @returns Object with queued status, messageId, and position in queue
|
|
994
1091
|
*/
|
|
995
|
-
async enqueueMessage(message, model, customInstructions) {
|
|
1092
|
+
async enqueueMessage(message, model, customInstructions, images) {
|
|
996
1093
|
await this.initialized;
|
|
997
|
-
return this.messageQueue.enqueue(message, model, customInstructions);
|
|
1094
|
+
return this.messageQueue.enqueue(message, model, customInstructions, images);
|
|
998
1095
|
}
|
|
999
1096
|
/**
|
|
1000
1097
|
* Get the current queue status
|
|
@@ -1019,29 +1116,43 @@ var ClaudeManager = class {
|
|
|
1019
1116
|
* Legacy sendMessage method - now uses the queue internally
|
|
1020
1117
|
* @deprecated Use enqueueMessage for better control over queue status
|
|
1021
1118
|
*/
|
|
1022
|
-
async sendMessage(message, model, customInstructions) {
|
|
1023
|
-
await this.enqueueMessage(message, model, customInstructions);
|
|
1119
|
+
async sendMessage(message, model, customInstructions, images) {
|
|
1120
|
+
await this.enqueueMessage(message, model, customInstructions, images);
|
|
1024
1121
|
}
|
|
1025
1122
|
/**
|
|
1026
1123
|
* Internal method that actually processes the message
|
|
1027
1124
|
*/
|
|
1028
|
-
async processMessageInternal(message, model, customInstructions) {
|
|
1125
|
+
async processMessageInternal(message, model, customInstructions, images) {
|
|
1029
1126
|
const linearSessionId = process.env.LINEAR_SESSION_ID;
|
|
1030
1127
|
if (!message || !message.trim()) {
|
|
1031
1128
|
throw new Error("Message cannot be empty");
|
|
1032
1129
|
}
|
|
1033
1130
|
await this.initialized;
|
|
1034
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
|
+
}
|
|
1035
1151
|
const userMessage = {
|
|
1036
1152
|
type: "user",
|
|
1037
1153
|
message: {
|
|
1038
1154
|
role: "user",
|
|
1039
|
-
content
|
|
1040
|
-
{
|
|
1041
|
-
type: "text",
|
|
1042
|
-
text: message
|
|
1043
|
-
}
|
|
1044
|
-
]
|
|
1155
|
+
content
|
|
1045
1156
|
},
|
|
1046
1157
|
parent_tool_use_id: null,
|
|
1047
1158
|
session_id: this.sessionId ?? ""
|
|
@@ -1128,7 +1239,7 @@ ${customInstructions}`;
|
|
|
1128
1239
|
}
|
|
1129
1240
|
async initialize() {
|
|
1130
1241
|
const historyDir = join2(homedir2(), ".replicas", "claude");
|
|
1131
|
-
await
|
|
1242
|
+
await mkdir2(historyDir, { recursive: true });
|
|
1132
1243
|
}
|
|
1133
1244
|
async handleMessage(message) {
|
|
1134
1245
|
if ("session_id" in message && message.session_id && !this.sessionId) {
|
|
@@ -1153,11 +1264,11 @@ var claudeManager = new ClaudeManager();
|
|
|
1153
1264
|
claude.post("/send", async (c) => {
|
|
1154
1265
|
try {
|
|
1155
1266
|
const body = await c.req.json();
|
|
1156
|
-
const { message, model, customInstructions } = body;
|
|
1267
|
+
const { message, model, customInstructions, images } = body;
|
|
1157
1268
|
if (!message || typeof message !== "string") {
|
|
1158
1269
|
return c.json({ error: "Message is required and must be a string" }, 400);
|
|
1159
1270
|
}
|
|
1160
|
-
const result = await claudeManager.enqueueMessage(message, model, customInstructions);
|
|
1271
|
+
const result = await claudeManager.enqueueMessage(message, model, customInstructions, images);
|
|
1161
1272
|
const response = {
|
|
1162
1273
|
success: true,
|
|
1163
1274
|
message: result.queued ? `Message queued at position ${result.position}` : "Message sent successfully",
|