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.
Files changed (2) hide show
  1. package/dist/src/index.js +278 -61
  2. 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 readFile3 } from "fs/promises";
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 || homedir();
739
+ const workspaceHome = process.env.WORKSPACE_HOME || process.env.HOME || homedir2();
619
740
  if (repoName) {
620
- this.workingDirectory = join(workspaceHome, "workspaces", repoName);
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
- return this.messageQueue.enqueue(message, model, customInstructions);
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
- const { events } = await this.currentThread.runStreamed(message);
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 = join(homedir(), ".codex", "sessions");
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 = join(sessionsDir, String(year), month, day);
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 = join(sessionsDir, String(searchYear), searchMonth, searchDay);
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 = join(directory, file);
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 join2 } from "path";
962
- import { mkdir, appendFile, rm } from "fs/promises";
963
- import { homedir as homedir2 } from "os";
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 || homedir2();
1144
+ const workspaceHome = process.env.WORKSPACE_HOME || process.env.HOME || homedir3();
977
1145
  if (repoName) {
978
- this.workingDirectory = join2(workspaceHome, "workspaces", repoName);
1146
+ this.workingDirectory = join3(workspaceHome, "workspaces", repoName);
979
1147
  } else {
980
1148
  this.workingDirectory = workspaceHome;
981
1149
  }
982
1150
  }
983
- this.historyFile = join2(homedir2(), ".replicas", "claude", "history.jsonl");
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 = join2(homedir2(), ".replicas", "claude");
1131
- await mkdir(historyDir, { recursive: true });
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 (!existsSync(repoPath)) {
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 (!existsSync(path4.join(repoPath, ".git"))) {
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 readFile2 } from "fs/promises";
1617
- import { existsSync as existsSync2 } from "fs";
1618
- import { join as join3 } from "path";
1619
- import { homedir as homedir3 } from "os";
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 || homedir3();
1845
+ const workspaceHome = process.env.WORKSPACE_HOME || process.env.HOME || homedir4();
1630
1846
  if (repoName) {
1631
- this.workingDirectory = join3(workspaceHome, "workspaces", repoName);
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 = join3(this.workingDirectory, "replicas.json");
1648
- if (!existsSync2(configPath)) {
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 readFile2(configPath, "utf-8");
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 readFile3("/var/log/cloud-init-output.log", "utf-8");
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
- ...getGitStatus(workingDirectory),
2020
+ ...gitStatus,
1804
2021
  linearBetaEnabled: true
1805
2022
  // TODO: delete
1806
2023
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "replicas-engine",
3
- "version": "0.1.20",
3
+ "version": "0.1.22",
4
4
  "description": "Lightweight API server for Replicas workspaces",
5
5
  "type": "module",
6
6
  "main": "dist/src/index.js",