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.
Files changed (2) hide show
  1. package/dist/src/index.js +137 -26
  2. 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
- const { events } = await this.currentThread.runStreamed(message);
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 mkdir(historyDir, { recursive: true });
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",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "replicas-engine",
3
- "version": "0.1.20",
3
+ "version": "0.1.21",
4
4
  "description": "Lightweight API server for Replicas workspaces",
5
5
  "type": "module",
6
6
  "main": "dist/src/index.js",