replicas-engine 0.1.21 → 0.1.23

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 +151 -45
  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
@@ -56,9 +56,9 @@ async function readJSONL(filePath) {
56
56
  }
57
57
 
58
58
  // src/services/codex-manager.ts
59
- import { readdir, stat, writeFile, mkdir } from "fs/promises";
60
- import { join } from "path";
61
- 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";
62
62
 
63
63
  // src/services/monolith-service.ts
64
64
  var MonolithService = class {
@@ -385,6 +385,53 @@ function convertCodexEvent(event, linearSessionId) {
385
385
 
386
386
  // src/utils/git.ts
387
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
388
435
  var cachedPr = null;
389
436
  function runGitCommand(command, cwd) {
390
437
  return execSync(command, {
@@ -443,7 +490,7 @@ function getGitDiff(cwd) {
443
490
  return null;
444
491
  }
445
492
  }
446
- function getPullRequestUrl(cwd) {
493
+ async function getPullRequestUrl(cwd) {
447
494
  try {
448
495
  const currentBranch = getCurrentBranch(cwd);
449
496
  if (!currentBranch) {
@@ -452,6 +499,14 @@ function getPullRequestUrl(cwd) {
452
499
  if (cachedPr && cachedPr.branch === currentBranch) {
453
500
  return cachedPr.prUrl;
454
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
+ }
455
510
  cachedPr = null;
456
511
  try {
457
512
  const remoteRef = execSync(`git ls-remote --heads origin ${currentBranch}`, {
@@ -476,6 +531,7 @@ function getPullRequestUrl(cwd) {
476
531
  prUrl: prInfo,
477
532
  branch: currentBranch
478
533
  };
534
+ await saveEngineState({ prUrl: prInfo });
479
535
  return cachedPr.prUrl;
480
536
  }
481
537
  } catch {
@@ -487,11 +543,11 @@ function getPullRequestUrl(cwd) {
487
543
  return null;
488
544
  }
489
545
  }
490
- function getGitStatus(workingDirectory) {
546
+ async function getGitStatus(workingDirectory) {
491
547
  return {
492
548
  branch: getCurrentBranch(workingDirectory),
493
549
  gitDiff: getGitDiff(workingDirectory),
494
- prUrl: getPullRequestUrl(workingDirectory)
550
+ prUrl: await getPullRequestUrl(workingDirectory)
495
551
  };
496
552
  }
497
553
 
@@ -665,6 +721,7 @@ async function normalizeImages(images) {
665
721
  }
666
722
 
667
723
  // src/services/codex-manager.ts
724
+ var DEFAULT_MODEL = "gpt-5.1-codex";
668
725
  var CodexManager = class {
669
726
  codex;
670
727
  currentThreadId = null;
@@ -673,21 +730,30 @@ var CodexManager = class {
673
730
  messageQueue;
674
731
  baseSystemPrompt;
675
732
  tempImageDir;
733
+ initialized;
676
734
  constructor(workingDirectory) {
677
735
  this.codex = new Codex();
678
736
  if (workingDirectory) {
679
737
  this.workingDirectory = workingDirectory;
680
738
  } else {
681
739
  const repoName = process.env.REPLICAS_REPO_NAME;
682
- const workspaceHome = process.env.WORKSPACE_HOME || process.env.HOME || homedir();
740
+ const workspaceHome = process.env.WORKSPACE_HOME || process.env.HOME || homedir2();
683
741
  if (repoName) {
684
- this.workingDirectory = join(workspaceHome, "workspaces", repoName);
742
+ this.workingDirectory = join2(workspaceHome, "workspaces", repoName);
685
743
  } else {
686
744
  this.workingDirectory = workspaceHome;
687
745
  }
688
746
  }
689
- this.tempImageDir = join(homedir(), ".replicas", "codex", "temp-images");
747
+ this.tempImageDir = join2(homedir2(), ".replicas", "codex", "temp-images");
690
748
  this.messageQueue = new MessageQueue(this.processMessageInternal.bind(this));
749
+ this.initialized = this.initialize();
750
+ }
751
+ async initialize() {
752
+ const persistedState = await loadEngineState();
753
+ if (persistedState.codexThreadId) {
754
+ this.currentThreadId = persistedState.codexThreadId;
755
+ console.log(`[CodexManager] Restored thread ID from persisted state: ${this.currentThreadId}`);
756
+ }
691
757
  }
692
758
  isProcessing() {
693
759
  return this.messageQueue.isProcessing();
@@ -698,6 +764,7 @@ var CodexManager = class {
698
764
  * @returns Object with queued status, messageId, and position in queue
699
765
  */
700
766
  async enqueueMessage(message, model, customInstructions, images) {
767
+ await this.initialized;
701
768
  return this.messageQueue.enqueue(message, model, customInstructions, images);
702
769
  }
703
770
  /**
@@ -731,14 +798,14 @@ var CodexManager = class {
731
798
  * @returns Array of temp file paths
732
799
  */
733
800
  async saveImagesToTempFiles(images) {
734
- await mkdir(this.tempImageDir, { recursive: true });
801
+ await mkdir2(this.tempImageDir, { recursive: true });
735
802
  const tempPaths = [];
736
803
  for (const image of images) {
737
804
  const ext = image.source.media_type.split("/")[1] || "png";
738
805
  const filename = `img_${randomUUID()}.${ext}`;
739
- const filepath = join(this.tempImageDir, filename);
806
+ const filepath = join2(this.tempImageDir, filename);
740
807
  const buffer = Buffer.from(image.source.data, "base64");
741
- await writeFile(filepath, buffer);
808
+ await writeFile2(filepath, buffer);
742
809
  tempPaths.push(filepath);
743
810
  }
744
811
  return tempPaths;
@@ -760,14 +827,14 @@ var CodexManager = class {
760
827
  workingDirectory: this.workingDirectory,
761
828
  skipGitRepoCheck: true,
762
829
  sandboxMode: "danger-full-access",
763
- model: model || "gpt-5.1-codex"
830
+ model: model || DEFAULT_MODEL
764
831
  });
765
832
  } else {
766
833
  this.currentThread = this.codex.startThread({
767
834
  workingDirectory: this.workingDirectory,
768
835
  skipGitRepoCheck: true,
769
836
  sandboxMode: "danger-full-access",
770
- model: model || "gpt-5.1-codex"
837
+ model: model || DEFAULT_MODEL
771
838
  });
772
839
  let combinedInstructions;
773
840
  if (this.baseSystemPrompt && customInstructions) {
@@ -784,11 +851,14 @@ ${customInstructions}`;
784
851
  for await (const event of events2) {
785
852
  if (event.type === "thread.started") {
786
853
  this.currentThreadId = event.thread_id;
787
- break;
854
+ await saveEngineState({ codexThreadId: this.currentThreadId });
855
+ console.log(`[CodexManager] Captured and persisted thread ID: ${this.currentThreadId}`);
788
856
  }
789
857
  }
790
858
  if (!this.currentThreadId && this.currentThread.id) {
791
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}`);
792
862
  }
793
863
  }
794
864
  }
@@ -814,7 +884,7 @@ ${customInstructions}`;
814
884
  }
815
885
  } finally {
816
886
  if (linearSessionId) {
817
- const status = getGitStatus(this.workingDirectory);
887
+ const status = await getGitStatus(this.workingDirectory);
818
888
  monolithService.sendEvent({ type: "agent_turn_complete", payload: { linearSessionId, status } }).catch(() => {
819
889
  });
820
890
  }
@@ -856,6 +926,7 @@ ${customInstructions}`;
856
926
  this.currentThread = null;
857
927
  this.currentThreadId = null;
858
928
  this.messageQueue.reset();
929
+ await saveEngineState({ codexThreadId: null });
859
930
  }
860
931
  getThreadId() {
861
932
  return this.currentThreadId;
@@ -884,13 +955,13 @@ ${customInstructions}`;
884
955
  }
885
956
  // Helper methods for finding session files
886
957
  async findSessionFile(threadId) {
887
- const sessionsDir = join(homedir(), ".codex", "sessions");
958
+ const sessionsDir = join2(homedir2(), ".codex", "sessions");
888
959
  try {
889
960
  const now = /* @__PURE__ */ new Date();
890
961
  const year = now.getFullYear();
891
962
  const month = String(now.getMonth() + 1).padStart(2, "0");
892
963
  const day = String(now.getDate()).padStart(2, "0");
893
- const todayDir = join(sessionsDir, String(year), month, day);
964
+ const todayDir = join2(sessionsDir, String(year), month, day);
894
965
  const file = await this.findFileInDirectory(todayDir, threadId);
895
966
  if (file) return file;
896
967
  for (let daysAgo = 1; daysAgo <= 7; daysAgo++) {
@@ -899,7 +970,7 @@ ${customInstructions}`;
899
970
  const searchYear = date.getFullYear();
900
971
  const searchMonth = String(date.getMonth() + 1).padStart(2, "0");
901
972
  const searchDay = String(date.getDate()).padStart(2, "0");
902
- const searchDir = join(sessionsDir, String(searchYear), searchMonth, searchDay);
973
+ const searchDir = join2(sessionsDir, String(searchYear), searchMonth, searchDay);
903
974
  const file2 = await this.findFileInDirectory(searchDir, threadId);
904
975
  if (file2) return file2;
905
976
  }
@@ -913,7 +984,7 @@ ${customInstructions}`;
913
984
  const files = await readdir(directory);
914
985
  for (const file of files) {
915
986
  if (file.endsWith(".jsonl") && file.includes(threadId)) {
916
- const fullPath = join(directory, file);
987
+ const fullPath = join2(directory, file);
917
988
  const stats = await stat(fullPath);
918
989
  if (stats.isFile()) {
919
990
  return fullPath;
@@ -1055,9 +1126,9 @@ import { Hono as Hono2 } from "hono";
1055
1126
  import {
1056
1127
  query
1057
1128
  } from "@anthropic-ai/claude-agent-sdk";
1058
- import { join as join2 } from "path";
1059
- import { mkdir as mkdir2, appendFile, rm } from "fs/promises";
1060
- 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";
1061
1132
  var ClaudeManager = class {
1062
1133
  workingDirectory;
1063
1134
  historyFile;
@@ -1070,14 +1141,14 @@ var ClaudeManager = class {
1070
1141
  this.workingDirectory = workingDirectory;
1071
1142
  } else {
1072
1143
  const repoName = process.env.REPLICAS_REPO_NAME;
1073
- const workspaceHome = process.env.WORKSPACE_HOME || process.env.HOME || homedir2();
1144
+ const workspaceHome = process.env.WORKSPACE_HOME || process.env.HOME || homedir3();
1074
1145
  if (repoName) {
1075
- this.workingDirectory = join2(workspaceHome, "workspaces", repoName);
1146
+ this.workingDirectory = join3(workspaceHome, "workspaces", repoName);
1076
1147
  } else {
1077
1148
  this.workingDirectory = workspaceHome;
1078
1149
  }
1079
1150
  }
1080
- this.historyFile = join2(homedir2(), ".replicas", "claude", "history.jsonl");
1151
+ this.historyFile = join3(homedir3(), ".replicas", "claude", "history.jsonl");
1081
1152
  this.initialized = this.initialize();
1082
1153
  this.messageQueue = new MessageQueue(this.processMessageInternal.bind(this));
1083
1154
  }
@@ -1198,7 +1269,7 @@ ${customInstructions}`;
1198
1269
  }
1199
1270
  } finally {
1200
1271
  if (linearSessionId) {
1201
- const status = getGitStatus(this.workingDirectory);
1272
+ const status = await getGitStatus(this.workingDirectory);
1202
1273
  monolithService.sendEvent({ type: "agent_turn_complete", payload: { linearSessionId, status } }).catch(() => {
1203
1274
  });
1204
1275
  }
@@ -1232,18 +1303,26 @@ ${customInstructions}`;
1232
1303
  await this.initialized;
1233
1304
  this.sessionId = null;
1234
1305
  this.messageQueue.reset();
1306
+ await saveEngineState({ claudeSessionId: null });
1235
1307
  try {
1236
1308
  await rm(this.historyFile, { force: true });
1237
1309
  } catch {
1238
1310
  }
1239
1311
  }
1240
1312
  async initialize() {
1241
- const historyDir = join2(homedir2(), ".replicas", "claude");
1242
- await mkdir2(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
+ }
1243
1320
  }
1244
1321
  async handleMessage(message) {
1245
1322
  if ("session_id" in message && message.session_id && !this.sessionId) {
1246
1323
  this.sessionId = message.session_id;
1324
+ await saveEngineState({ claudeSessionId: this.sessionId });
1325
+ console.log(`[ClaudeManager] Captured and persisted session ID: ${this.sessionId}`);
1247
1326
  }
1248
1327
  await this.recordEvent(message);
1249
1328
  }
@@ -1637,7 +1716,7 @@ var CodexTokenManager = class {
1637
1716
  var codexTokenManager = new CodexTokenManager();
1638
1717
 
1639
1718
  // src/services/git-init.ts
1640
- import { existsSync } from "fs";
1719
+ import { existsSync as existsSync2 } from "fs";
1641
1720
  import path4 from "path";
1642
1721
  var initializedBranch = null;
1643
1722
  function findAvailableBranchName(baseName, cwd) {
@@ -1673,7 +1752,7 @@ async function initializeGitRepository() {
1673
1752
  };
1674
1753
  }
1675
1754
  const repoPath = path4.join(workspaceHome, "workspaces", repoName);
1676
- if (!existsSync(repoPath)) {
1755
+ if (!existsSync2(repoPath)) {
1677
1756
  console.log(`[GitInit] Repository directory does not exist: ${repoPath}`);
1678
1757
  console.log("[GitInit] Waiting for initializer to clone the repository...");
1679
1758
  return {
@@ -1681,7 +1760,7 @@ async function initializeGitRepository() {
1681
1760
  branch: null
1682
1761
  };
1683
1762
  }
1684
- if (!existsSync(path4.join(repoPath, ".git"))) {
1763
+ if (!existsSync2(path4.join(repoPath, ".git"))) {
1685
1764
  return {
1686
1765
  success: false,
1687
1766
  branch: null,
@@ -1690,8 +1769,32 @@ async function initializeGitRepository() {
1690
1769
  }
1691
1770
  console.log(`[GitInit] Initializing repository at ${repoPath}`);
1692
1771
  try {
1772
+ const persistedState = await loadEngineState();
1773
+ const persistedBranch = persistedState.branch;
1693
1774
  console.log("[GitInit] Fetching all remotes...");
1694
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
+ }
1695
1798
  console.log(`[GitInit] Checking out default branch: ${defaultBranch}`);
1696
1799
  runGitCommand(`git checkout ${defaultBranch}`, repoPath);
1697
1800
  console.log("[GitInit] Pulling latest changes...");
@@ -1707,10 +1810,12 @@ async function initializeGitRepository() {
1707
1810
  console.log(`[GitInit] Creating workspace branch: ${branchName}`);
1708
1811
  runGitCommand(`git checkout -b ${branchName}`, repoPath);
1709
1812
  initializedBranch = branchName;
1813
+ await saveEngineState({ branch: branchName });
1710
1814
  console.log(`[GitInit] Successfully initialized on branch: ${branchName}`);
1711
1815
  return {
1712
1816
  success: true,
1713
- branch: branchName
1817
+ branch: branchName,
1818
+ resumed: false
1714
1819
  };
1715
1820
  } catch (error) {
1716
1821
  const errorMessage = error instanceof Error ? error.message : String(error);
@@ -1724,10 +1829,10 @@ async function initializeGitRepository() {
1724
1829
  }
1725
1830
 
1726
1831
  // 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";
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";
1731
1836
  import { exec } from "child_process";
1732
1837
  import { promisify } from "util";
1733
1838
  var execAsync = promisify(exec);
@@ -1737,9 +1842,9 @@ var ReplicasConfigService = class {
1737
1842
  hooksExecuted = false;
1738
1843
  constructor() {
1739
1844
  const repoName = process.env.REPLICAS_REPO_NAME;
1740
- const workspaceHome = process.env.WORKSPACE_HOME || process.env.HOME || homedir3();
1845
+ const workspaceHome = process.env.WORKSPACE_HOME || process.env.HOME || homedir4();
1741
1846
  if (repoName) {
1742
- this.workingDirectory = join3(workspaceHome, "workspaces", repoName);
1847
+ this.workingDirectory = join4(workspaceHome, "workspaces", repoName);
1743
1848
  } else {
1744
1849
  this.workingDirectory = workspaceHome;
1745
1850
  }
@@ -1755,14 +1860,14 @@ var ReplicasConfigService = class {
1755
1860
  * Load and parse the replicas.json config file
1756
1861
  */
1757
1862
  async loadConfig() {
1758
- const configPath = join3(this.workingDirectory, "replicas.json");
1759
- if (!existsSync2(configPath)) {
1863
+ const configPath = join4(this.workingDirectory, "replicas.json");
1864
+ if (!existsSync3(configPath)) {
1760
1865
  console.log("No replicas.json found in workspace directory");
1761
1866
  this.config = null;
1762
1867
  return;
1763
1868
  }
1764
1869
  try {
1765
- const data = await readFile2(configPath, "utf-8");
1870
+ const data = await readFile3(configPath, "utf-8");
1766
1871
  const config = JSON.parse(data);
1767
1872
  if (config.copy && !Array.isArray(config.copy)) {
1768
1873
  throw new Error('Invalid replicas.json: "copy" must be an array of file paths');
@@ -1880,7 +1985,7 @@ function checkActiveSSHSessions() {
1880
1985
  var app = new Hono3();
1881
1986
  app.get("/health", async (c) => {
1882
1987
  try {
1883
- 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");
1884
1989
  let status;
1885
1990
  if (logContent.includes(COMPLETION_MESSAGE)) {
1886
1991
  status = "active";
@@ -1905,13 +2010,14 @@ app.get("/status", async (c) => {
1905
2010
  const claudeStatus = await claudeManager.getStatus();
1906
2011
  const workingDirectory = claudeStatus.working_directory;
1907
2012
  const hasActiveSSHSessions = checkActiveSSHSessions();
2013
+ const gitStatus = await getGitStatus(workingDirectory);
1908
2014
  return c.json({
1909
2015
  isCodexProcessing,
1910
2016
  isClaudeProcessing,
1911
2017
  isCodexUsed,
1912
2018
  isClaudeUsed,
1913
2019
  hasActiveSSHSessions,
1914
- ...getGitStatus(workingDirectory),
2020
+ ...gitStatus,
1915
2021
  linearBetaEnabled: true
1916
2022
  // TODO: delete
1917
2023
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "replicas-engine",
3
- "version": "0.1.21",
3
+ "version": "0.1.23",
4
4
  "description": "Lightweight API server for Replicas workspaces",
5
5
  "type": "module",
6
6
  "main": "dist/src/index.js",