replicas-engine 0.1.23 → 0.1.25

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 +321 -189
  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 {
@@ -91,6 +91,15 @@ var MonolithService = class {
91
91
  var monolithService = new MonolithService();
92
92
 
93
93
  // src/services/linear-event-converter.ts
94
+ function linearThoughtToResponse(thought) {
95
+ return {
96
+ linearSessionId: thought.linearSessionId,
97
+ content: {
98
+ type: "response",
99
+ body: thought.content.body
100
+ }
101
+ };
102
+ }
94
103
  function summarizeInput(input) {
95
104
  if (!input) return "";
96
105
  if (typeof input === "string") return input;
@@ -397,7 +406,8 @@ var DEFAULT_STATE = {
397
406
  branch: null,
398
407
  prUrl: null,
399
408
  claudeSessionId: null,
400
- codexThreadId: null
409
+ codexThreadId: null,
410
+ startHooksCompleted: false
401
411
  };
402
412
  async function loadEngineState() {
403
413
  try {
@@ -720,6 +730,206 @@ async function normalizeImages(images) {
720
730
  return normalized;
721
731
  }
722
732
 
733
+ // src/services/replicas-config.ts
734
+ import { readFile as readFile3, appendFile, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
735
+ import { existsSync as existsSync2 } from "fs";
736
+ import { join as join2 } from "path";
737
+ import { homedir as homedir2 } from "os";
738
+ import { exec } from "child_process";
739
+ import { promisify } from "util";
740
+ var execAsync = promisify(exec);
741
+ var START_HOOKS_LOG = join2(homedir2(), ".replicas", "startHooks.log");
742
+ var START_HOOKS_RUNNING_PROMPT = `IMPORTANT - Start Hooks Running:
743
+ Start hooks are shell commands/scripts set by the repository owners that run on workspace startup.
744
+ These hooks are currently executing in the background. You can:
745
+ - Check the status and output by reading ~/.replicas/startHooks.log
746
+ - View the hook commands in replicas.json in the repository root (under the "startHook" field)
747
+
748
+ The start hooks may install dependencies, build projects, or perform other setup tasks.
749
+ If your task depends on setup being complete, check the log file before proceeding.`;
750
+ var ReplicasConfigService = class {
751
+ config = null;
752
+ workingDirectory;
753
+ hooksRunning = false;
754
+ hooksCompleted = false;
755
+ startHooksPromise = null;
756
+ constructor() {
757
+ const repoName = process.env.REPLICAS_REPO_NAME;
758
+ const workspaceHome = process.env.WORKSPACE_HOME || process.env.HOME || homedir2();
759
+ if (repoName) {
760
+ this.workingDirectory = join2(workspaceHome, "workspaces", repoName);
761
+ } else {
762
+ this.workingDirectory = workspaceHome;
763
+ }
764
+ }
765
+ /**
766
+ * Initialize the service by reading replicas.json and starting start hooks asynchronously
767
+ * Start hooks run in the background and don't block engine startup
768
+ */
769
+ async initialize() {
770
+ await this.loadConfig();
771
+ this.startHooksPromise = this.executeStartHooks();
772
+ }
773
+ /**
774
+ * Load and parse the replicas.json config file
775
+ */
776
+ async loadConfig() {
777
+ const configPath = join2(this.workingDirectory, "replicas.json");
778
+ if (!existsSync2(configPath)) {
779
+ console.log("No replicas.json found in workspace directory");
780
+ this.config = null;
781
+ return;
782
+ }
783
+ try {
784
+ const data = await readFile3(configPath, "utf-8");
785
+ const config = JSON.parse(data);
786
+ if (config.copy && !Array.isArray(config.copy)) {
787
+ throw new Error('Invalid replicas.json: "copy" must be an array of file paths');
788
+ }
789
+ if (config.ports && !Array.isArray(config.ports)) {
790
+ throw new Error('Invalid replicas.json: "ports" must be an array of port numbers');
791
+ }
792
+ if (config.ports && !config.ports.every((p) => typeof p === "number")) {
793
+ throw new Error("Invalid replicas.json: all ports must be numbers");
794
+ }
795
+ if (config.organizationId && typeof config.organizationId !== "string") {
796
+ throw new Error('Invalid replicas.json: "organizationId" must be a string');
797
+ }
798
+ if (config.systemPrompt && typeof config.systemPrompt !== "string") {
799
+ throw new Error('Invalid replicas.json: "systemPrompt" must be a string');
800
+ }
801
+ if (config.startHook) {
802
+ if (typeof config.startHook !== "object" || Array.isArray(config.startHook)) {
803
+ throw new Error('Invalid replicas.json: "startHook" must be an object with "commands" array');
804
+ }
805
+ if (!Array.isArray(config.startHook.commands)) {
806
+ throw new Error('Invalid replicas.json: "startHook.commands" must be an array of shell commands');
807
+ }
808
+ if (!config.startHook.commands.every((cmd) => typeof cmd === "string")) {
809
+ throw new Error("Invalid replicas.json: all startHook.commands entries must be strings");
810
+ }
811
+ if (config.startHook.timeout !== void 0 && (typeof config.startHook.timeout !== "number" || config.startHook.timeout <= 0)) {
812
+ throw new Error('Invalid replicas.json: "startHook.timeout" must be a positive number');
813
+ }
814
+ }
815
+ this.config = config;
816
+ console.log("Loaded replicas.json config:", {
817
+ hasSystemPrompt: !!config.systemPrompt,
818
+ startHookCount: config.startHook?.commands.length ?? 0
819
+ });
820
+ } catch (error) {
821
+ if (error instanceof SyntaxError) {
822
+ console.error("Failed to parse replicas.json:", error.message);
823
+ } else if (error instanceof Error) {
824
+ console.error("Error loading replicas.json:", error.message);
825
+ }
826
+ this.config = null;
827
+ }
828
+ }
829
+ /**
830
+ * Write a message to the start hooks log file
831
+ */
832
+ async logToFile(message) {
833
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
834
+ const logLine = `[${timestamp}] ${message}
835
+ `;
836
+ try {
837
+ await mkdir2(join2(homedir2(), ".replicas"), { recursive: true });
838
+ await appendFile(START_HOOKS_LOG, logLine, "utf-8");
839
+ } catch (error) {
840
+ console.error("Failed to write to start hooks log:", error);
841
+ }
842
+ }
843
+ /**
844
+ * Execute all start hooks defined in replicas.json
845
+ * Writes output to ~/.replicas/startHooks.log
846
+ * Only runs once per workspace lifecycle (persisted across sleep/wake cycles)
847
+ */
848
+ async executeStartHooks() {
849
+ const persistedState = await loadEngineState();
850
+ if (persistedState.startHooksCompleted) {
851
+ console.log("Start hooks already completed in previous session, skipping");
852
+ this.hooksCompleted = true;
853
+ await this.logToFile("Start hooks already completed in previous session, skipping");
854
+ return;
855
+ }
856
+ const startHookConfig = this.config?.startHook;
857
+ if (!startHookConfig || startHookConfig.commands.length === 0) {
858
+ this.hooksCompleted = true;
859
+ await saveEngineState({ startHooksCompleted: true });
860
+ return;
861
+ }
862
+ const timeout = startHookConfig.timeout ?? 3e5;
863
+ const hooks = startHookConfig.commands;
864
+ this.hooksRunning = true;
865
+ await mkdir2(join2(homedir2(), ".replicas"), { recursive: true });
866
+ await writeFile2(START_HOOKS_LOG, `=== Start Hooks Execution Log ===
867
+ Started: ${(/* @__PURE__ */ new Date()).toISOString()}
868
+ Commands: ${hooks.length}
869
+
870
+ `, "utf-8");
871
+ console.log(`Executing ${hooks.length} start hook(s) with timeout ${timeout}ms...`);
872
+ await this.logToFile(`Executing ${hooks.length} start hook(s) with timeout ${timeout}ms...`);
873
+ for (const hook of hooks) {
874
+ try {
875
+ console.log(`Running start hook: ${hook}`);
876
+ await this.logToFile(`
877
+ --- Running: ${hook} ---`);
878
+ const { stdout, stderr } = await execAsync(hook, {
879
+ cwd: this.workingDirectory,
880
+ timeout,
881
+ env: process.env
882
+ });
883
+ if (stdout) {
884
+ console.log(`[${hook}] stdout:`, stdout);
885
+ await this.logToFile(`[stdout] ${stdout}`);
886
+ }
887
+ if (stderr) {
888
+ console.warn(`[${hook}] stderr:`, stderr);
889
+ await this.logToFile(`[stderr] ${stderr}`);
890
+ }
891
+ console.log(`Start hook completed: ${hook}`);
892
+ await this.logToFile(`--- Completed: ${hook} ---`);
893
+ } catch (error) {
894
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
895
+ console.error(`Start hook failed: ${hook}`, errorMessage);
896
+ await this.logToFile(`[ERROR] ${hook} failed: ${errorMessage}`);
897
+ }
898
+ }
899
+ this.hooksRunning = false;
900
+ this.hooksCompleted = true;
901
+ await saveEngineState({ startHooksCompleted: true });
902
+ console.log("All start hooks completed");
903
+ await this.logToFile(`
904
+ === All start hooks completed at ${(/* @__PURE__ */ new Date()).toISOString()} ===`);
905
+ }
906
+ /**
907
+ * Get the system prompt from replicas.json
908
+ */
909
+ getSystemPrompt() {
910
+ return this.config?.systemPrompt;
911
+ }
912
+ /**
913
+ * Get the full config object
914
+ */
915
+ getConfig() {
916
+ return this.config;
917
+ }
918
+ /**
919
+ * Check if start hooks are currently running
920
+ */
921
+ areHooksRunning() {
922
+ return this.hooksRunning;
923
+ }
924
+ /**
925
+ * Get start hook configuration (for system prompt)
926
+ */
927
+ getStartHookConfig() {
928
+ return this.config?.startHook;
929
+ }
930
+ };
931
+ var replicasConfigService = new ReplicasConfigService();
932
+
723
933
  // src/services/codex-manager.ts
724
934
  var DEFAULT_MODEL = "gpt-5.1-codex";
725
935
  var CodexManager = class {
@@ -737,14 +947,14 @@ var CodexManager = class {
737
947
  this.workingDirectory = workingDirectory;
738
948
  } else {
739
949
  const repoName = process.env.REPLICAS_REPO_NAME;
740
- const workspaceHome = process.env.WORKSPACE_HOME || process.env.HOME || homedir2();
950
+ const workspaceHome = process.env.WORKSPACE_HOME || process.env.HOME || homedir3();
741
951
  if (repoName) {
742
- this.workingDirectory = join2(workspaceHome, "workspaces", repoName);
952
+ this.workingDirectory = join3(workspaceHome, "workspaces", repoName);
743
953
  } else {
744
954
  this.workingDirectory = workspaceHome;
745
955
  }
746
956
  }
747
- this.tempImageDir = join2(homedir2(), ".replicas", "codex", "temp-images");
957
+ this.tempImageDir = join3(homedir3(), ".replicas", "codex", "temp-images");
748
958
  this.messageQueue = new MessageQueue(this.processMessageInternal.bind(this));
749
959
  this.initialized = this.initialize();
750
960
  }
@@ -786,6 +996,20 @@ var CodexManager = class {
786
996
  getBaseSystemPrompt() {
787
997
  return this.baseSystemPrompt;
788
998
  }
999
+ /**
1000
+ * Generate start hooks instruction for the system prompt
1001
+ * Returns a message explaining start hooks status if they are running
1002
+ */
1003
+ getStartHooksInstruction() {
1004
+ if (!replicasConfigService.areHooksRunning()) {
1005
+ return void 0;
1006
+ }
1007
+ const startHookConfig = replicasConfigService.getStartHookConfig();
1008
+ if (!startHookConfig || startHookConfig.commands.length === 0) {
1009
+ return void 0;
1010
+ }
1011
+ return START_HOOKS_RUNNING_PROMPT;
1012
+ }
789
1013
  /**
790
1014
  * Legacy sendMessage method - now uses the queue internally
791
1015
  * @deprecated Use enqueueMessage for better control over queue status
@@ -798,14 +1022,14 @@ var CodexManager = class {
798
1022
  * @returns Array of temp file paths
799
1023
  */
800
1024
  async saveImagesToTempFiles(images) {
801
- await mkdir2(this.tempImageDir, { recursive: true });
1025
+ await mkdir3(this.tempImageDir, { recursive: true });
802
1026
  const tempPaths = [];
803
1027
  for (const image of images) {
804
1028
  const ext = image.source.media_type.split("/")[1] || "png";
805
1029
  const filename = `img_${randomUUID()}.${ext}`;
806
- const filepath = join2(this.tempImageDir, filename);
1030
+ const filepath = join3(this.tempImageDir, filename);
807
1031
  const buffer = Buffer.from(image.source.data, "base64");
808
- await writeFile2(filepath, buffer);
1032
+ await writeFile3(filepath, buffer);
809
1033
  tempPaths.push(filepath);
810
1034
  }
811
1035
  return tempPaths;
@@ -836,14 +1060,18 @@ var CodexManager = class {
836
1060
  sandboxMode: "danger-full-access",
837
1061
  model: model || DEFAULT_MODEL
838
1062
  });
839
- let combinedInstructions;
840
- if (this.baseSystemPrompt && customInstructions) {
841
- combinedInstructions = `${this.baseSystemPrompt}
842
-
843
- ${customInstructions}`;
844
- } else {
845
- combinedInstructions = this.baseSystemPrompt || customInstructions;
1063
+ const startHooksInstruction = this.getStartHooksInstruction();
1064
+ const parts = [];
1065
+ if (this.baseSystemPrompt) {
1066
+ parts.push(this.baseSystemPrompt);
1067
+ }
1068
+ if (startHooksInstruction) {
1069
+ parts.push(startHooksInstruction);
1070
+ }
1071
+ if (customInstructions) {
1072
+ parts.push(customInstructions);
846
1073
  }
1074
+ const combinedInstructions = parts.length > 0 ? parts.join("\n\n") : void 0;
847
1075
  if (combinedInstructions) {
848
1076
  message = combinedInstructions + "\n" + message;
849
1077
  }
@@ -873,15 +1101,30 @@ ${customInstructions}`;
873
1101
  input = message;
874
1102
  }
875
1103
  const { events } = await this.currentThread.runStreamed(input);
1104
+ let latestThoughtEvent = null;
876
1105
  for await (const event of events) {
877
1106
  if (linearSessionId) {
878
1107
  const linearEvent = convertCodexEvent(event, linearSessionId);
879
1108
  if (linearEvent) {
880
- monolithService.sendEvent({ type: "agent_update", payload: linearEvent }).catch(() => {
881
- });
1109
+ if (latestThoughtEvent) {
1110
+ monolithService.sendEvent({ type: "agent_update", payload: latestThoughtEvent }).catch(() => {
1111
+ });
1112
+ }
1113
+ if (linearEvent.content.type === "thought") {
1114
+ latestThoughtEvent = linearEvent;
1115
+ } else {
1116
+ latestThoughtEvent = null;
1117
+ monolithService.sendEvent({ type: "agent_update", payload: linearEvent }).catch(() => {
1118
+ });
1119
+ }
882
1120
  }
883
1121
  }
884
1122
  }
1123
+ if (linearSessionId && latestThoughtEvent) {
1124
+ const responseEvent = linearThoughtToResponse(latestThoughtEvent);
1125
+ monolithService.sendEvent({ type: "agent_update", payload: responseEvent }).catch(() => {
1126
+ });
1127
+ }
885
1128
  } finally {
886
1129
  if (linearSessionId) {
887
1130
  const status = await getGitStatus(this.workingDirectory);
@@ -955,13 +1198,13 @@ ${customInstructions}`;
955
1198
  }
956
1199
  // Helper methods for finding session files
957
1200
  async findSessionFile(threadId) {
958
- const sessionsDir = join2(homedir2(), ".codex", "sessions");
1201
+ const sessionsDir = join3(homedir3(), ".codex", "sessions");
959
1202
  try {
960
1203
  const now = /* @__PURE__ */ new Date();
961
1204
  const year = now.getFullYear();
962
1205
  const month = String(now.getMonth() + 1).padStart(2, "0");
963
1206
  const day = String(now.getDate()).padStart(2, "0");
964
- const todayDir = join2(sessionsDir, String(year), month, day);
1207
+ const todayDir = join3(sessionsDir, String(year), month, day);
965
1208
  const file = await this.findFileInDirectory(todayDir, threadId);
966
1209
  if (file) return file;
967
1210
  for (let daysAgo = 1; daysAgo <= 7; daysAgo++) {
@@ -970,7 +1213,7 @@ ${customInstructions}`;
970
1213
  const searchYear = date.getFullYear();
971
1214
  const searchMonth = String(date.getMonth() + 1).padStart(2, "0");
972
1215
  const searchDay = String(date.getDate()).padStart(2, "0");
973
- const searchDir = join2(sessionsDir, String(searchYear), searchMonth, searchDay);
1216
+ const searchDir = join3(sessionsDir, String(searchYear), searchMonth, searchDay);
974
1217
  const file2 = await this.findFileInDirectory(searchDir, threadId);
975
1218
  if (file2) return file2;
976
1219
  }
@@ -984,7 +1227,7 @@ ${customInstructions}`;
984
1227
  const files = await readdir(directory);
985
1228
  for (const file of files) {
986
1229
  if (file.endsWith(".jsonl") && file.includes(threadId)) {
987
- const fullPath = join2(directory, file);
1230
+ const fullPath = join3(directory, file);
988
1231
  const stats = await stat(fullPath);
989
1232
  if (stats.isFile()) {
990
1233
  return fullPath;
@@ -1126,9 +1369,9 @@ import { Hono as Hono2 } from "hono";
1126
1369
  import {
1127
1370
  query
1128
1371
  } 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";
1372
+ import { join as join4 } from "path";
1373
+ import { mkdir as mkdir4, appendFile as appendFile2, rm } from "fs/promises";
1374
+ import { homedir as homedir4 } from "os";
1132
1375
  var ClaudeManager = class {
1133
1376
  workingDirectory;
1134
1377
  historyFile;
@@ -1141,14 +1384,14 @@ var ClaudeManager = class {
1141
1384
  this.workingDirectory = workingDirectory;
1142
1385
  } else {
1143
1386
  const repoName = process.env.REPLICAS_REPO_NAME;
1144
- const workspaceHome = process.env.WORKSPACE_HOME || process.env.HOME || homedir3();
1387
+ const workspaceHome = process.env.WORKSPACE_HOME || process.env.HOME || homedir4();
1145
1388
  if (repoName) {
1146
- this.workingDirectory = join3(workspaceHome, "workspaces", repoName);
1389
+ this.workingDirectory = join4(workspaceHome, "workspaces", repoName);
1147
1390
  } else {
1148
1391
  this.workingDirectory = workspaceHome;
1149
1392
  }
1150
1393
  }
1151
- this.historyFile = join3(homedir3(), ".replicas", "claude", "history.jsonl");
1394
+ this.historyFile = join4(homedir4(), ".replicas", "claude", "history.jsonl");
1152
1395
  this.initialized = this.initialize();
1153
1396
  this.messageQueue = new MessageQueue(this.processMessageInternal.bind(this));
1154
1397
  }
@@ -1183,6 +1426,20 @@ var ClaudeManager = class {
1183
1426
  getBaseSystemPrompt() {
1184
1427
  return this.baseSystemPrompt;
1185
1428
  }
1429
+ /**
1430
+ * Generate start hooks instruction for the system prompt
1431
+ * Returns a message explaining start hooks status if they are running
1432
+ */
1433
+ getStartHooksInstruction() {
1434
+ if (!replicasConfigService.areHooksRunning()) {
1435
+ return void 0;
1436
+ }
1437
+ const startHookConfig = replicasConfigService.getStartHookConfig();
1438
+ if (!startHookConfig || startHookConfig.commands.length === 0) {
1439
+ return void 0;
1440
+ }
1441
+ return START_HOOKS_RUNNING_PROMPT;
1442
+ }
1186
1443
  /**
1187
1444
  * Legacy sendMessage method - now uses the queue internally
1188
1445
  * @deprecated Use enqueueMessage for better control over queue status
@@ -1232,14 +1489,18 @@ var ClaudeManager = class {
1232
1489
  const promptIterable = (async function* () {
1233
1490
  yield userMessage;
1234
1491
  })();
1235
- let combinedInstructions;
1236
- if (this.baseSystemPrompt && customInstructions) {
1237
- combinedInstructions = `${this.baseSystemPrompt}
1238
-
1239
- ${customInstructions}`;
1240
- } else {
1241
- combinedInstructions = this.baseSystemPrompt || customInstructions;
1492
+ const startHooksInstruction = this.getStartHooksInstruction();
1493
+ const parts = [];
1494
+ if (this.baseSystemPrompt) {
1495
+ parts.push(this.baseSystemPrompt);
1496
+ }
1497
+ if (startHooksInstruction) {
1498
+ parts.push(startHooksInstruction);
1499
+ }
1500
+ if (customInstructions) {
1501
+ parts.push(customInstructions);
1242
1502
  }
1503
+ const combinedInstructions = parts.length > 0 ? parts.join("\n\n") : void 0;
1243
1504
  const response = query({
1244
1505
  prompt: promptIterable,
1245
1506
  options: {
@@ -1257,16 +1518,31 @@ ${customInstructions}`;
1257
1518
  model: model || "opus"
1258
1519
  }
1259
1520
  });
1521
+ let latestThoughtEvent = null;
1260
1522
  for await (const msg of response) {
1261
1523
  await this.handleMessage(msg);
1262
1524
  if (linearSessionId) {
1263
1525
  const linearEvent = convertClaudeEvent(msg, linearSessionId);
1264
1526
  if (linearEvent) {
1265
- monolithService.sendEvent({ type: "agent_update", payload: linearEvent }).catch(() => {
1266
- });
1527
+ if (latestThoughtEvent) {
1528
+ monolithService.sendEvent({ type: "agent_update", payload: latestThoughtEvent }).catch(() => {
1529
+ });
1530
+ }
1531
+ if (linearEvent.content.type === "thought") {
1532
+ latestThoughtEvent = linearEvent;
1533
+ } else {
1534
+ latestThoughtEvent = null;
1535
+ monolithService.sendEvent({ type: "agent_update", payload: linearEvent }).catch(() => {
1536
+ });
1537
+ }
1267
1538
  }
1268
1539
  }
1269
1540
  }
1541
+ if (linearSessionId && latestThoughtEvent) {
1542
+ const responseEvent = linearThoughtToResponse(latestThoughtEvent);
1543
+ monolithService.sendEvent({ type: "agent_update", payload: responseEvent }).catch(() => {
1544
+ });
1545
+ }
1270
1546
  } finally {
1271
1547
  if (linearSessionId) {
1272
1548
  const status = await getGitStatus(this.workingDirectory);
@@ -1310,8 +1586,8 @@ ${customInstructions}`;
1310
1586
  }
1311
1587
  }
1312
1588
  async initialize() {
1313
- const historyDir = join3(homedir3(), ".replicas", "claude");
1314
- await mkdir3(historyDir, { recursive: true });
1589
+ const historyDir = join4(homedir4(), ".replicas", "claude");
1590
+ await mkdir4(historyDir, { recursive: true });
1315
1591
  const persistedState = await loadEngineState();
1316
1592
  if (persistedState.claudeSessionId) {
1317
1593
  this.sessionId = persistedState.claudeSessionId;
@@ -1333,7 +1609,7 @@ ${customInstructions}`;
1333
1609
  type: `claude-${event.type}`,
1334
1610
  payload: event
1335
1611
  };
1336
- await appendFile(this.historyFile, JSON.stringify(jsonEvent) + "\n", "utf-8");
1612
+ await appendFile2(this.historyFile, JSON.stringify(jsonEvent) + "\n", "utf-8");
1337
1613
  }
1338
1614
  };
1339
1615
 
@@ -1716,7 +1992,7 @@ var CodexTokenManager = class {
1716
1992
  var codexTokenManager = new CodexTokenManager();
1717
1993
 
1718
1994
  // src/services/git-init.ts
1719
- import { existsSync as existsSync2 } from "fs";
1995
+ import { existsSync as existsSync3 } from "fs";
1720
1996
  import path4 from "path";
1721
1997
  var initializedBranch = null;
1722
1998
  function findAvailableBranchName(baseName, cwd) {
@@ -1752,7 +2028,7 @@ async function initializeGitRepository() {
1752
2028
  };
1753
2029
  }
1754
2030
  const repoPath = path4.join(workspaceHome, "workspaces", repoName);
1755
- if (!existsSync2(repoPath)) {
2031
+ if (!existsSync3(repoPath)) {
1756
2032
  console.log(`[GitInit] Repository directory does not exist: ${repoPath}`);
1757
2033
  console.log("[GitInit] Waiting for initializer to clone the repository...");
1758
2034
  return {
@@ -1760,7 +2036,7 @@ async function initializeGitRepository() {
1760
2036
  branch: null
1761
2037
  };
1762
2038
  }
1763
- if (!existsSync2(path4.join(repoPath, ".git"))) {
2039
+ if (!existsSync3(path4.join(repoPath, ".git"))) {
1764
2040
  return {
1765
2041
  success: false,
1766
2042
  branch: null,
@@ -1828,148 +2104,6 @@ async function initializeGitRepository() {
1828
2104
  }
1829
2105
  }
1830
2106
 
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
2107
  // src/index.ts
1974
2108
  var READY_MESSAGE = "========= REPLICAS WORKSPACE READY ==========";
1975
2109
  var COMPLETION_MESSAGE = "========= REPLICAS WORKSPACE INITIALIZATION COMPLETE ==========";
@@ -2017,9 +2151,7 @@ app.get("/status", async (c) => {
2017
2151
  isCodexUsed,
2018
2152
  isClaudeUsed,
2019
2153
  hasActiveSSHSessions,
2020
- ...gitStatus,
2021
- linearBetaEnabled: true
2022
- // TODO: delete
2154
+ ...gitStatus
2023
2155
  });
2024
2156
  } catch (error) {
2025
2157
  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.23",
3
+ "version": "0.1.25",
4
4
  "description": "Lightweight API server for Replicas workspaces",
5
5
  "type": "module",
6
6
  "main": "dist/src/index.js",