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.
- package/dist/src/index.js +321 -189
- 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
|
|
60
|
-
import { join as
|
|
61
|
-
import { homedir as
|
|
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 ||
|
|
950
|
+
const workspaceHome = process.env.WORKSPACE_HOME || process.env.HOME || homedir3();
|
|
741
951
|
if (repoName) {
|
|
742
|
-
this.workingDirectory =
|
|
952
|
+
this.workingDirectory = join3(workspaceHome, "workspaces", repoName);
|
|
743
953
|
} else {
|
|
744
954
|
this.workingDirectory = workspaceHome;
|
|
745
955
|
}
|
|
746
956
|
}
|
|
747
|
-
this.tempImageDir =
|
|
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
|
|
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 =
|
|
1030
|
+
const filepath = join3(this.tempImageDir, filename);
|
|
807
1031
|
const buffer = Buffer.from(image.source.data, "base64");
|
|
808
|
-
await
|
|
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
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
1130
|
-
import { mkdir as
|
|
1131
|
-
import { homedir as
|
|
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 ||
|
|
1387
|
+
const workspaceHome = process.env.WORKSPACE_HOME || process.env.HOME || homedir4();
|
|
1145
1388
|
if (repoName) {
|
|
1146
|
-
this.workingDirectory =
|
|
1389
|
+
this.workingDirectory = join4(workspaceHome, "workspaces", repoName);
|
|
1147
1390
|
} else {
|
|
1148
1391
|
this.workingDirectory = workspaceHome;
|
|
1149
1392
|
}
|
|
1150
1393
|
}
|
|
1151
|
-
this.historyFile =
|
|
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
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
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
|
-
|
|
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 =
|
|
1314
|
-
await
|
|
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
|
|
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
|
|
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 (!
|
|
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 (!
|
|
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);
|