replicas-engine 0.1.22 → 0.1.24

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 +281 -188
  2. package/package.json +1 -1
package/dist/src/index.js CHANGED
@@ -56,9 +56,9 @@ async function readJSONL(filePath) {
56
56
  }
57
57
 
58
58
  // src/services/codex-manager.ts
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";
59
+ import { readdir, stat, writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
60
+ import { join as join3 } from "path";
61
+ import { homedir as homedir3 } from "os";
62
62
 
63
63
  // src/services/monolith-service.ts
64
64
  var MonolithService = class {
@@ -397,7 +397,8 @@ var DEFAULT_STATE = {
397
397
  branch: null,
398
398
  prUrl: null,
399
399
  claudeSessionId: null,
400
- codexThreadId: null
400
+ codexThreadId: null,
401
+ startHooksCompleted: false
401
402
  };
402
403
  async function loadEngineState() {
403
404
  try {
@@ -720,7 +721,208 @@ async function normalizeImages(images) {
720
721
  return normalized;
721
722
  }
722
723
 
724
+ // src/services/replicas-config.ts
725
+ import { readFile as readFile3, appendFile, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
726
+ import { existsSync as existsSync2 } from "fs";
727
+ import { join as join2 } from "path";
728
+ import { homedir as homedir2 } from "os";
729
+ import { exec } from "child_process";
730
+ import { promisify } from "util";
731
+ var execAsync = promisify(exec);
732
+ var START_HOOKS_LOG = join2(homedir2(), ".replicas", "startHooks.log");
733
+ var START_HOOKS_RUNNING_PROMPT = `IMPORTANT - Start Hooks Running:
734
+ Start hooks are shell commands/scripts set by the repository owners that run on workspace startup.
735
+ These hooks are currently executing in the background. You can:
736
+ - Check the status and output by reading ~/.replicas/startHooks.log
737
+ - View the hook commands in replicas.json in the repository root (under the "startHook" field)
738
+
739
+ The start hooks may install dependencies, build projects, or perform other setup tasks.
740
+ If your task depends on setup being complete, check the log file before proceeding.`;
741
+ var ReplicasConfigService = class {
742
+ config = null;
743
+ workingDirectory;
744
+ hooksRunning = false;
745
+ hooksCompleted = false;
746
+ startHooksPromise = null;
747
+ constructor() {
748
+ const repoName = process.env.REPLICAS_REPO_NAME;
749
+ const workspaceHome = process.env.WORKSPACE_HOME || process.env.HOME || homedir2();
750
+ if (repoName) {
751
+ this.workingDirectory = join2(workspaceHome, "workspaces", repoName);
752
+ } else {
753
+ this.workingDirectory = workspaceHome;
754
+ }
755
+ }
756
+ /**
757
+ * Initialize the service by reading replicas.json and starting start hooks asynchronously
758
+ * Start hooks run in the background and don't block engine startup
759
+ */
760
+ async initialize() {
761
+ await this.loadConfig();
762
+ this.startHooksPromise = this.executeStartHooks();
763
+ }
764
+ /**
765
+ * Load and parse the replicas.json config file
766
+ */
767
+ async loadConfig() {
768
+ const configPath = join2(this.workingDirectory, "replicas.json");
769
+ if (!existsSync2(configPath)) {
770
+ console.log("No replicas.json found in workspace directory");
771
+ this.config = null;
772
+ return;
773
+ }
774
+ try {
775
+ const data = await readFile3(configPath, "utf-8");
776
+ const config = JSON.parse(data);
777
+ if (config.copy && !Array.isArray(config.copy)) {
778
+ throw new Error('Invalid replicas.json: "copy" must be an array of file paths');
779
+ }
780
+ if (config.ports && !Array.isArray(config.ports)) {
781
+ throw new Error('Invalid replicas.json: "ports" must be an array of port numbers');
782
+ }
783
+ if (config.ports && !config.ports.every((p) => typeof p === "number")) {
784
+ throw new Error("Invalid replicas.json: all ports must be numbers");
785
+ }
786
+ if (config.organizationId && typeof config.organizationId !== "string") {
787
+ throw new Error('Invalid replicas.json: "organizationId" must be a string');
788
+ }
789
+ if (config.systemPrompt && typeof config.systemPrompt !== "string") {
790
+ throw new Error('Invalid replicas.json: "systemPrompt" must be a string');
791
+ }
792
+ if (config.startHook) {
793
+ if (typeof config.startHook !== "object" || Array.isArray(config.startHook)) {
794
+ throw new Error('Invalid replicas.json: "startHook" must be an object with "commands" array');
795
+ }
796
+ if (!Array.isArray(config.startHook.commands)) {
797
+ throw new Error('Invalid replicas.json: "startHook.commands" must be an array of shell commands');
798
+ }
799
+ if (!config.startHook.commands.every((cmd) => typeof cmd === "string")) {
800
+ throw new Error("Invalid replicas.json: all startHook.commands entries must be strings");
801
+ }
802
+ if (config.startHook.timeout !== void 0 && (typeof config.startHook.timeout !== "number" || config.startHook.timeout <= 0)) {
803
+ throw new Error('Invalid replicas.json: "startHook.timeout" must be a positive number');
804
+ }
805
+ }
806
+ this.config = config;
807
+ console.log("Loaded replicas.json config:", {
808
+ hasSystemPrompt: !!config.systemPrompt,
809
+ startHookCount: config.startHook?.commands.length ?? 0
810
+ });
811
+ } catch (error) {
812
+ if (error instanceof SyntaxError) {
813
+ console.error("Failed to parse replicas.json:", error.message);
814
+ } else if (error instanceof Error) {
815
+ console.error("Error loading replicas.json:", error.message);
816
+ }
817
+ this.config = null;
818
+ }
819
+ }
820
+ /**
821
+ * Write a message to the start hooks log file
822
+ */
823
+ async logToFile(message) {
824
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
825
+ const logLine = `[${timestamp}] ${message}
826
+ `;
827
+ try {
828
+ await mkdir2(join2(homedir2(), ".replicas"), { recursive: true });
829
+ await appendFile(START_HOOKS_LOG, logLine, "utf-8");
830
+ } catch (error) {
831
+ console.error("Failed to write to start hooks log:", error);
832
+ }
833
+ }
834
+ /**
835
+ * Execute all start hooks defined in replicas.json
836
+ * Writes output to ~/.replicas/startHooks.log
837
+ * Only runs once per workspace lifecycle (persisted across sleep/wake cycles)
838
+ */
839
+ async executeStartHooks() {
840
+ const persistedState = await loadEngineState();
841
+ if (persistedState.startHooksCompleted) {
842
+ console.log("Start hooks already completed in previous session, skipping");
843
+ this.hooksCompleted = true;
844
+ await this.logToFile("Start hooks already completed in previous session, skipping");
845
+ return;
846
+ }
847
+ const startHookConfig = this.config?.startHook;
848
+ if (!startHookConfig || startHookConfig.commands.length === 0) {
849
+ this.hooksCompleted = true;
850
+ await saveEngineState({ startHooksCompleted: true });
851
+ return;
852
+ }
853
+ const timeout = startHookConfig.timeout ?? 3e5;
854
+ const hooks = startHookConfig.commands;
855
+ this.hooksRunning = true;
856
+ await mkdir2(join2(homedir2(), ".replicas"), { recursive: true });
857
+ await writeFile2(START_HOOKS_LOG, `=== Start Hooks Execution Log ===
858
+ Started: ${(/* @__PURE__ */ new Date()).toISOString()}
859
+ Commands: ${hooks.length}
860
+
861
+ `, "utf-8");
862
+ console.log(`Executing ${hooks.length} start hook(s) with timeout ${timeout}ms...`);
863
+ await this.logToFile(`Executing ${hooks.length} start hook(s) with timeout ${timeout}ms...`);
864
+ for (const hook of hooks) {
865
+ try {
866
+ console.log(`Running start hook: ${hook}`);
867
+ await this.logToFile(`
868
+ --- Running: ${hook} ---`);
869
+ const { stdout, stderr } = await execAsync(hook, {
870
+ cwd: this.workingDirectory,
871
+ timeout,
872
+ env: process.env
873
+ });
874
+ if (stdout) {
875
+ console.log(`[${hook}] stdout:`, stdout);
876
+ await this.logToFile(`[stdout] ${stdout}`);
877
+ }
878
+ if (stderr) {
879
+ console.warn(`[${hook}] stderr:`, stderr);
880
+ await this.logToFile(`[stderr] ${stderr}`);
881
+ }
882
+ console.log(`Start hook completed: ${hook}`);
883
+ await this.logToFile(`--- Completed: ${hook} ---`);
884
+ } catch (error) {
885
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
886
+ console.error(`Start hook failed: ${hook}`, errorMessage);
887
+ await this.logToFile(`[ERROR] ${hook} failed: ${errorMessage}`);
888
+ }
889
+ }
890
+ this.hooksRunning = false;
891
+ this.hooksCompleted = true;
892
+ await saveEngineState({ startHooksCompleted: true });
893
+ console.log("All start hooks completed");
894
+ await this.logToFile(`
895
+ === All start hooks completed at ${(/* @__PURE__ */ new Date()).toISOString()} ===`);
896
+ }
897
+ /**
898
+ * Get the system prompt from replicas.json
899
+ */
900
+ getSystemPrompt() {
901
+ return this.config?.systemPrompt;
902
+ }
903
+ /**
904
+ * Get the full config object
905
+ */
906
+ getConfig() {
907
+ return this.config;
908
+ }
909
+ /**
910
+ * Check if start hooks are currently running
911
+ */
912
+ areHooksRunning() {
913
+ return this.hooksRunning;
914
+ }
915
+ /**
916
+ * Get start hook configuration (for system prompt)
917
+ */
918
+ getStartHookConfig() {
919
+ return this.config?.startHook;
920
+ }
921
+ };
922
+ var replicasConfigService = new ReplicasConfigService();
923
+
723
924
  // src/services/codex-manager.ts
925
+ var DEFAULT_MODEL = "gpt-5.1-codex";
724
926
  var CodexManager = class {
725
927
  codex;
726
928
  currentThreadId = null;
@@ -736,14 +938,14 @@ var CodexManager = class {
736
938
  this.workingDirectory = workingDirectory;
737
939
  } else {
738
940
  const repoName = process.env.REPLICAS_REPO_NAME;
739
- const workspaceHome = process.env.WORKSPACE_HOME || process.env.HOME || homedir2();
941
+ const workspaceHome = process.env.WORKSPACE_HOME || process.env.HOME || homedir3();
740
942
  if (repoName) {
741
- this.workingDirectory = join2(workspaceHome, "workspaces", repoName);
943
+ this.workingDirectory = join3(workspaceHome, "workspaces", repoName);
742
944
  } else {
743
945
  this.workingDirectory = workspaceHome;
744
946
  }
745
947
  }
746
- this.tempImageDir = join2(homedir2(), ".replicas", "codex", "temp-images");
948
+ this.tempImageDir = join3(homedir3(), ".replicas", "codex", "temp-images");
747
949
  this.messageQueue = new MessageQueue(this.processMessageInternal.bind(this));
748
950
  this.initialized = this.initialize();
749
951
  }
@@ -785,6 +987,20 @@ var CodexManager = class {
785
987
  getBaseSystemPrompt() {
786
988
  return this.baseSystemPrompt;
787
989
  }
990
+ /**
991
+ * Generate start hooks instruction for the system prompt
992
+ * Returns a message explaining start hooks status if they are running
993
+ */
994
+ getStartHooksInstruction() {
995
+ if (!replicasConfigService.areHooksRunning()) {
996
+ return void 0;
997
+ }
998
+ const startHookConfig = replicasConfigService.getStartHookConfig();
999
+ if (!startHookConfig || startHookConfig.commands.length === 0) {
1000
+ return void 0;
1001
+ }
1002
+ return START_HOOKS_RUNNING_PROMPT;
1003
+ }
788
1004
  /**
789
1005
  * Legacy sendMessage method - now uses the queue internally
790
1006
  * @deprecated Use enqueueMessage for better control over queue status
@@ -797,14 +1013,14 @@ var CodexManager = class {
797
1013
  * @returns Array of temp file paths
798
1014
  */
799
1015
  async saveImagesToTempFiles(images) {
800
- await mkdir2(this.tempImageDir, { recursive: true });
1016
+ await mkdir3(this.tempImageDir, { recursive: true });
801
1017
  const tempPaths = [];
802
1018
  for (const image of images) {
803
1019
  const ext = image.source.media_type.split("/")[1] || "png";
804
1020
  const filename = `img_${randomUUID()}.${ext}`;
805
- const filepath = join2(this.tempImageDir, filename);
1021
+ const filepath = join3(this.tempImageDir, filename);
806
1022
  const buffer = Buffer.from(image.source.data, "base64");
807
- await writeFile2(filepath, buffer);
1023
+ await writeFile3(filepath, buffer);
808
1024
  tempPaths.push(filepath);
809
1025
  }
810
1026
  return tempPaths;
@@ -826,23 +1042,27 @@ var CodexManager = class {
826
1042
  workingDirectory: this.workingDirectory,
827
1043
  skipGitRepoCheck: true,
828
1044
  sandboxMode: "danger-full-access",
829
- model: model || "gpt-5.1-codex"
1045
+ model: model || DEFAULT_MODEL
830
1046
  });
831
1047
  } else {
832
1048
  this.currentThread = this.codex.startThread({
833
1049
  workingDirectory: this.workingDirectory,
834
1050
  skipGitRepoCheck: true,
835
1051
  sandboxMode: "danger-full-access",
836
- model: model || "gpt-5.1-codex"
1052
+ model: model || DEFAULT_MODEL
837
1053
  });
838
- let combinedInstructions;
839
- if (this.baseSystemPrompt && customInstructions) {
840
- combinedInstructions = `${this.baseSystemPrompt}
841
-
842
- ${customInstructions}`;
843
- } else {
844
- combinedInstructions = this.baseSystemPrompt || customInstructions;
1054
+ const startHooksInstruction = this.getStartHooksInstruction();
1055
+ const parts = [];
1056
+ if (this.baseSystemPrompt) {
1057
+ parts.push(this.baseSystemPrompt);
1058
+ }
1059
+ if (startHooksInstruction) {
1060
+ parts.push(startHooksInstruction);
1061
+ }
1062
+ if (customInstructions) {
1063
+ parts.push(customInstructions);
845
1064
  }
1065
+ const combinedInstructions = parts.length > 0 ? parts.join("\n\n") : void 0;
846
1066
  if (combinedInstructions) {
847
1067
  message = combinedInstructions + "\n" + message;
848
1068
  }
@@ -852,7 +1072,6 @@ ${customInstructions}`;
852
1072
  this.currentThreadId = event.thread_id;
853
1073
  await saveEngineState({ codexThreadId: this.currentThreadId });
854
1074
  console.log(`[CodexManager] Captured and persisted thread ID: ${this.currentThreadId}`);
855
- break;
856
1075
  }
857
1076
  }
858
1077
  if (!this.currentThreadId && this.currentThread.id) {
@@ -955,13 +1174,13 @@ ${customInstructions}`;
955
1174
  }
956
1175
  // Helper methods for finding session files
957
1176
  async findSessionFile(threadId) {
958
- const sessionsDir = join2(homedir2(), ".codex", "sessions");
1177
+ const sessionsDir = join3(homedir3(), ".codex", "sessions");
959
1178
  try {
960
1179
  const now = /* @__PURE__ */ new Date();
961
1180
  const year = now.getFullYear();
962
1181
  const month = String(now.getMonth() + 1).padStart(2, "0");
963
1182
  const day = String(now.getDate()).padStart(2, "0");
964
- const todayDir = join2(sessionsDir, String(year), month, day);
1183
+ const todayDir = join3(sessionsDir, String(year), month, day);
965
1184
  const file = await this.findFileInDirectory(todayDir, threadId);
966
1185
  if (file) return file;
967
1186
  for (let daysAgo = 1; daysAgo <= 7; daysAgo++) {
@@ -970,7 +1189,7 @@ ${customInstructions}`;
970
1189
  const searchYear = date.getFullYear();
971
1190
  const searchMonth = String(date.getMonth() + 1).padStart(2, "0");
972
1191
  const searchDay = String(date.getDate()).padStart(2, "0");
973
- const searchDir = join2(sessionsDir, String(searchYear), searchMonth, searchDay);
1192
+ const searchDir = join3(sessionsDir, String(searchYear), searchMonth, searchDay);
974
1193
  const file2 = await this.findFileInDirectory(searchDir, threadId);
975
1194
  if (file2) return file2;
976
1195
  }
@@ -984,7 +1203,7 @@ ${customInstructions}`;
984
1203
  const files = await readdir(directory);
985
1204
  for (const file of files) {
986
1205
  if (file.endsWith(".jsonl") && file.includes(threadId)) {
987
- const fullPath = join2(directory, file);
1206
+ const fullPath = join3(directory, file);
988
1207
  const stats = await stat(fullPath);
989
1208
  if (stats.isFile()) {
990
1209
  return fullPath;
@@ -1126,9 +1345,9 @@ import { Hono as Hono2 } from "hono";
1126
1345
  import {
1127
1346
  query
1128
1347
  } from "@anthropic-ai/claude-agent-sdk";
1129
- import { join as join3 } from "path";
1130
- import { mkdir as mkdir3, appendFile, rm } from "fs/promises";
1131
- import { homedir as homedir3 } from "os";
1348
+ import { join as join4 } from "path";
1349
+ import { mkdir as mkdir4, appendFile as appendFile2, rm } from "fs/promises";
1350
+ import { homedir as homedir4 } from "os";
1132
1351
  var ClaudeManager = class {
1133
1352
  workingDirectory;
1134
1353
  historyFile;
@@ -1141,14 +1360,14 @@ var ClaudeManager = class {
1141
1360
  this.workingDirectory = workingDirectory;
1142
1361
  } else {
1143
1362
  const repoName = process.env.REPLICAS_REPO_NAME;
1144
- const workspaceHome = process.env.WORKSPACE_HOME || process.env.HOME || homedir3();
1363
+ const workspaceHome = process.env.WORKSPACE_HOME || process.env.HOME || homedir4();
1145
1364
  if (repoName) {
1146
- this.workingDirectory = join3(workspaceHome, "workspaces", repoName);
1365
+ this.workingDirectory = join4(workspaceHome, "workspaces", repoName);
1147
1366
  } else {
1148
1367
  this.workingDirectory = workspaceHome;
1149
1368
  }
1150
1369
  }
1151
- this.historyFile = join3(homedir3(), ".replicas", "claude", "history.jsonl");
1370
+ this.historyFile = join4(homedir4(), ".replicas", "claude", "history.jsonl");
1152
1371
  this.initialized = this.initialize();
1153
1372
  this.messageQueue = new MessageQueue(this.processMessageInternal.bind(this));
1154
1373
  }
@@ -1183,6 +1402,20 @@ var ClaudeManager = class {
1183
1402
  getBaseSystemPrompt() {
1184
1403
  return this.baseSystemPrompt;
1185
1404
  }
1405
+ /**
1406
+ * Generate start hooks instruction for the system prompt
1407
+ * Returns a message explaining start hooks status if they are running
1408
+ */
1409
+ getStartHooksInstruction() {
1410
+ if (!replicasConfigService.areHooksRunning()) {
1411
+ return void 0;
1412
+ }
1413
+ const startHookConfig = replicasConfigService.getStartHookConfig();
1414
+ if (!startHookConfig || startHookConfig.commands.length === 0) {
1415
+ return void 0;
1416
+ }
1417
+ return START_HOOKS_RUNNING_PROMPT;
1418
+ }
1186
1419
  /**
1187
1420
  * Legacy sendMessage method - now uses the queue internally
1188
1421
  * @deprecated Use enqueueMessage for better control over queue status
@@ -1232,14 +1465,18 @@ var ClaudeManager = class {
1232
1465
  const promptIterable = (async function* () {
1233
1466
  yield userMessage;
1234
1467
  })();
1235
- let combinedInstructions;
1236
- if (this.baseSystemPrompt && customInstructions) {
1237
- combinedInstructions = `${this.baseSystemPrompt}
1238
-
1239
- ${customInstructions}`;
1240
- } else {
1241
- combinedInstructions = this.baseSystemPrompt || customInstructions;
1468
+ const startHooksInstruction = this.getStartHooksInstruction();
1469
+ const parts = [];
1470
+ if (this.baseSystemPrompt) {
1471
+ parts.push(this.baseSystemPrompt);
1242
1472
  }
1473
+ if (startHooksInstruction) {
1474
+ parts.push(startHooksInstruction);
1475
+ }
1476
+ if (customInstructions) {
1477
+ parts.push(customInstructions);
1478
+ }
1479
+ const combinedInstructions = parts.length > 0 ? parts.join("\n\n") : void 0;
1243
1480
  const response = query({
1244
1481
  prompt: promptIterable,
1245
1482
  options: {
@@ -1310,8 +1547,8 @@ ${customInstructions}`;
1310
1547
  }
1311
1548
  }
1312
1549
  async initialize() {
1313
- const historyDir = join3(homedir3(), ".replicas", "claude");
1314
- await mkdir3(historyDir, { recursive: true });
1550
+ const historyDir = join4(homedir4(), ".replicas", "claude");
1551
+ await mkdir4(historyDir, { recursive: true });
1315
1552
  const persistedState = await loadEngineState();
1316
1553
  if (persistedState.claudeSessionId) {
1317
1554
  this.sessionId = persistedState.claudeSessionId;
@@ -1333,7 +1570,7 @@ ${customInstructions}`;
1333
1570
  type: `claude-${event.type}`,
1334
1571
  payload: event
1335
1572
  };
1336
- await appendFile(this.historyFile, JSON.stringify(jsonEvent) + "\n", "utf-8");
1573
+ await appendFile2(this.historyFile, JSON.stringify(jsonEvent) + "\n", "utf-8");
1337
1574
  }
1338
1575
  };
1339
1576
 
@@ -1716,7 +1953,7 @@ var CodexTokenManager = class {
1716
1953
  var codexTokenManager = new CodexTokenManager();
1717
1954
 
1718
1955
  // src/services/git-init.ts
1719
- import { existsSync as existsSync2 } from "fs";
1956
+ import { existsSync as existsSync3 } from "fs";
1720
1957
  import path4 from "path";
1721
1958
  var initializedBranch = null;
1722
1959
  function findAvailableBranchName(baseName, cwd) {
@@ -1752,7 +1989,7 @@ async function initializeGitRepository() {
1752
1989
  };
1753
1990
  }
1754
1991
  const repoPath = path4.join(workspaceHome, "workspaces", repoName);
1755
- if (!existsSync2(repoPath)) {
1992
+ if (!existsSync3(repoPath)) {
1756
1993
  console.log(`[GitInit] Repository directory does not exist: ${repoPath}`);
1757
1994
  console.log("[GitInit] Waiting for initializer to clone the repository...");
1758
1995
  return {
@@ -1760,7 +1997,7 @@ async function initializeGitRepository() {
1760
1997
  branch: null
1761
1998
  };
1762
1999
  }
1763
- if (!existsSync2(path4.join(repoPath, ".git"))) {
2000
+ if (!existsSync3(path4.join(repoPath, ".git"))) {
1764
2001
  return {
1765
2002
  success: false,
1766
2003
  branch: null,
@@ -1828,148 +2065,6 @@ async function initializeGitRepository() {
1828
2065
  }
1829
2066
  }
1830
2067
 
1831
- // src/services/replicas-config.ts
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";
1836
- import { exec } from "child_process";
1837
- import { promisify } from "util";
1838
- var execAsync = promisify(exec);
1839
- var ReplicasConfigService = class {
1840
- config = null;
1841
- workingDirectory;
1842
- hooksExecuted = false;
1843
- constructor() {
1844
- const repoName = process.env.REPLICAS_REPO_NAME;
1845
- const workspaceHome = process.env.WORKSPACE_HOME || process.env.HOME || homedir4();
1846
- if (repoName) {
1847
- this.workingDirectory = join4(workspaceHome, "workspaces", repoName);
1848
- } else {
1849
- this.workingDirectory = workspaceHome;
1850
- }
1851
- }
1852
- /**
1853
- * Initialize the service by reading replicas.json and executing start hooks
1854
- */
1855
- async initialize() {
1856
- await this.loadConfig();
1857
- await this.executeStartHooks();
1858
- }
1859
- /**
1860
- * Load and parse the replicas.json config file
1861
- */
1862
- async loadConfig() {
1863
- const configPath = join4(this.workingDirectory, "replicas.json");
1864
- if (!existsSync3(configPath)) {
1865
- console.log("No replicas.json found in workspace directory");
1866
- this.config = null;
1867
- return;
1868
- }
1869
- try {
1870
- const data = await readFile3(configPath, "utf-8");
1871
- const config = JSON.parse(data);
1872
- if (config.copy && !Array.isArray(config.copy)) {
1873
- throw new Error('Invalid replicas.json: "copy" must be an array of file paths');
1874
- }
1875
- if (config.ports && !Array.isArray(config.ports)) {
1876
- throw new Error('Invalid replicas.json: "ports" must be an array of port numbers');
1877
- }
1878
- if (config.ports && !config.ports.every((p) => typeof p === "number")) {
1879
- throw new Error("Invalid replicas.json: all ports must be numbers");
1880
- }
1881
- if (config.systemPrompt && typeof config.systemPrompt !== "string") {
1882
- throw new Error('Invalid replicas.json: "systemPrompt" must be a string');
1883
- }
1884
- if (config.startHook) {
1885
- if (typeof config.startHook !== "object" || Array.isArray(config.startHook)) {
1886
- throw new Error('Invalid replicas.json: "startHook" must be an object with "commands" array');
1887
- }
1888
- if (!Array.isArray(config.startHook.commands)) {
1889
- throw new Error('Invalid replicas.json: "startHook.commands" must be an array of shell commands');
1890
- }
1891
- if (!config.startHook.commands.every((cmd) => typeof cmd === "string")) {
1892
- throw new Error("Invalid replicas.json: all startHook.commands entries must be strings");
1893
- }
1894
- if (config.startHook.timeout !== void 0 && (typeof config.startHook.timeout !== "number" || config.startHook.timeout <= 0)) {
1895
- throw new Error('Invalid replicas.json: "startHook.timeout" must be a positive number');
1896
- }
1897
- }
1898
- this.config = config;
1899
- console.log("Loaded replicas.json config:", {
1900
- hasSystemPrompt: !!config.systemPrompt,
1901
- startHookCount: config.startHook?.commands.length ?? 0
1902
- });
1903
- } catch (error) {
1904
- if (error instanceof SyntaxError) {
1905
- console.error("Failed to parse replicas.json:", error.message);
1906
- } else if (error instanceof Error) {
1907
- console.error("Error loading replicas.json:", error.message);
1908
- }
1909
- this.config = null;
1910
- }
1911
- }
1912
- /**
1913
- * Execute all start hooks defined in replicas.json
1914
- */
1915
- async executeStartHooks() {
1916
- if (this.hooksExecuted) {
1917
- console.log("Start hooks already executed, skipping");
1918
- return;
1919
- }
1920
- const startHookConfig = this.config?.startHook;
1921
- if (!startHookConfig || startHookConfig.commands.length === 0) {
1922
- this.hooksExecuted = true;
1923
- return;
1924
- }
1925
- const timeout = startHookConfig.timeout ?? 3e5;
1926
- const hooks = startHookConfig.commands;
1927
- console.log(`Executing ${hooks.length} start hook(s) with timeout ${timeout}ms...`);
1928
- for (const hook of hooks) {
1929
- try {
1930
- console.log(`Running start hook: ${hook}`);
1931
- const { stdout, stderr } = await execAsync(hook, {
1932
- cwd: this.workingDirectory,
1933
- timeout,
1934
- env: process.env
1935
- });
1936
- if (stdout) {
1937
- console.log(`[${hook}] stdout:`, stdout);
1938
- }
1939
- if (stderr) {
1940
- console.warn(`[${hook}] stderr:`, stderr);
1941
- }
1942
- console.log(`Start hook completed: ${hook}`);
1943
- } catch (error) {
1944
- if (error instanceof Error) {
1945
- console.error(`Start hook failed: ${hook}`, error.message);
1946
- }
1947
- }
1948
- }
1949
- this.hooksExecuted = true;
1950
- console.log("All start hooks completed");
1951
- }
1952
- /**
1953
- * Get the system prompt from replicas.json
1954
- */
1955
- getSystemPrompt() {
1956
- return this.config?.systemPrompt;
1957
- }
1958
- /**
1959
- * Get the full config object
1960
- */
1961
- getConfig() {
1962
- return this.config;
1963
- }
1964
- /**
1965
- * Check if start hooks have been executed
1966
- */
1967
- hasExecutedHooks() {
1968
- return this.hooksExecuted;
1969
- }
1970
- };
1971
- var replicasConfigService = new ReplicasConfigService();
1972
-
1973
2068
  // src/index.ts
1974
2069
  var READY_MESSAGE = "========= REPLICAS WORKSPACE READY ==========";
1975
2070
  var COMPLETION_MESSAGE = "========= REPLICAS WORKSPACE INITIALIZATION COMPLETE ==========";
@@ -2017,9 +2112,7 @@ app.get("/status", async (c) => {
2017
2112
  isCodexUsed,
2018
2113
  isClaudeUsed,
2019
2114
  hasActiveSSHSessions,
2020
- ...gitStatus,
2021
- linearBetaEnabled: true
2022
- // TODO: delete
2115
+ ...gitStatus
2023
2116
  });
2024
2117
  } catch (error) {
2025
2118
  console.error("Error getting workspace status:", error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "replicas-engine",
3
- "version": "0.1.22",
3
+ "version": "0.1.24",
4
4
  "description": "Lightweight API server for Replicas workspaces",
5
5
  "type": "module",
6
6
  "main": "dist/src/index.js",