replicas-engine 0.1.44 → 0.1.46

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 +224 -22
  2. package/package.json +2 -2
package/dist/src/index.js CHANGED
@@ -5,7 +5,7 @@ import "./chunk-ZXMDA7VB.js";
5
5
  import "dotenv/config";
6
6
  import { serve } from "@hono/node-server";
7
7
  import { Hono as Hono2 } from "hono";
8
- import { readFile as readFile7 } from "fs/promises";
8
+ import { readFile as readFile8 } from "fs/promises";
9
9
  import { execSync } from "child_process";
10
10
  import { randomUUID as randomUUID4 } from "crypto";
11
11
 
@@ -836,6 +836,80 @@ import { join as join5 } from "path";
836
836
  import { homedir as homedir4 } from "os";
837
837
  import { exec } from "child_process";
838
838
  import { promisify } from "util";
839
+
840
+ // ../shared/src/sandbox.ts
841
+ var SANDBOX_LIFECYCLE = {
842
+ AUTO_STOP_MINUTES: 60,
843
+ AUTO_ARCHIVE_MINUTES: 60 * 24 * 7,
844
+ AUTO_DELETE_MINUTES: -1,
845
+ SSH_TOKEN_EXPIRATION_MINUTES: 3 * 60
846
+ };
847
+
848
+ // ../shared/src/warm-hooks.ts
849
+ var DEFAULT_WARM_HOOK_TIMEOUT_MS = 5 * 60 * 1e3;
850
+ var MAX_WARM_HOOK_TIMEOUT_MS = 15 * 60 * 1e3;
851
+ var DEFAULT_WARM_HOOK_OUTPUT_MAX_CHARS = 12e3;
852
+ function isRecord2(value) {
853
+ return typeof value === "object" && value !== null;
854
+ }
855
+ function clampWarmHookTimeoutMs(timeoutMs) {
856
+ if (!timeoutMs || Number.isNaN(timeoutMs) || timeoutMs <= 0) {
857
+ return DEFAULT_WARM_HOOK_TIMEOUT_MS;
858
+ }
859
+ return Math.min(timeoutMs, MAX_WARM_HOOK_TIMEOUT_MS);
860
+ }
861
+ function truncateWarmHookOutput(text, maxChars = DEFAULT_WARM_HOOK_OUTPUT_MAX_CHARS) {
862
+ if (text.length <= maxChars) {
863
+ return text;
864
+ }
865
+ return `${text.slice(0, maxChars)}
866
+ ...[truncated]`;
867
+ }
868
+ function parseWarmHookConfig(value) {
869
+ if (typeof value === "string") {
870
+ return value;
871
+ }
872
+ if (!isRecord2(value)) {
873
+ throw new Error('Invalid replicas.json: "warmHook" must be a string or object');
874
+ }
875
+ if (typeof value.content !== "string") {
876
+ throw new Error('Invalid replicas.json: "warmHook.content" must be a string');
877
+ }
878
+ if (value.timeout !== void 0 && (typeof value.timeout !== "number" || value.timeout <= 0)) {
879
+ throw new Error('Invalid replicas.json: "warmHook.timeout" must be a positive number');
880
+ }
881
+ return {
882
+ content: value.content,
883
+ timeout: value.timeout
884
+ };
885
+ }
886
+ function resolveWarmHookConfig(value) {
887
+ if (value === void 0) {
888
+ return null;
889
+ }
890
+ const parsed = parseWarmHookConfig(value);
891
+ const content = (typeof parsed === "string" ? parsed : parsed.content).trim();
892
+ if (!content) {
893
+ return null;
894
+ }
895
+ return {
896
+ content,
897
+ timeoutMs: typeof parsed === "string" ? void 0 : parsed.timeout
898
+ };
899
+ }
900
+
901
+ // ../shared/src/engine/types.ts
902
+ var DEFAULT_CHAT_TITLES = {
903
+ claude: "Claude Code",
904
+ codex: "Codex"
905
+ };
906
+ var IMAGE_MEDIA_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"];
907
+
908
+ // ../shared/src/routes/workspaces.ts
909
+ var WORKSPACE_FILE_UPLOAD_MAX_SIZE_BYTES = 20 * 1024 * 1024;
910
+ var WORKSPACE_FILE_CONTENT_MAX_SIZE_BYTES = 1 * 1024 * 1024;
911
+
912
+ // src/services/replicas-config-service.ts
839
913
  var execAsync = promisify(exec);
840
914
  var START_HOOKS_LOG = join5(homedir4(), ".replicas", "startHooks.log");
841
915
  var START_HOOKS_RUNNING_PROMPT = `IMPORTANT - Start Hooks Running:
@@ -888,6 +962,9 @@ function parseReplicasConfig(value) {
888
962
  }
889
963
  config.startHook = { commands, timeout };
890
964
  }
965
+ if ("warmHook" in value) {
966
+ config.warmHook = parseWarmHookConfig(value.warmHook);
967
+ }
891
968
  return config;
892
969
  }
893
970
  var ReplicasConfigService = class {
@@ -1120,25 +1197,6 @@ import { homedir as homedir8 } from "os";
1120
1197
  import { join as join9 } from "path";
1121
1198
  import { randomUUID as randomUUID3 } from "crypto";
1122
1199
 
1123
- // ../shared/src/sandbox.ts
1124
- var SANDBOX_LIFECYCLE = {
1125
- AUTO_STOP_MINUTES: 60,
1126
- AUTO_ARCHIVE_MINUTES: 60 * 24 * 7,
1127
- AUTO_DELETE_MINUTES: -1,
1128
- SSH_TOKEN_EXPIRATION_MINUTES: 3 * 60
1129
- };
1130
-
1131
- // ../shared/src/engine/types.ts
1132
- var DEFAULT_CHAT_TITLES = {
1133
- claude: "Claude Code",
1134
- codex: "Codex"
1135
- };
1136
- var IMAGE_MEDIA_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"];
1137
-
1138
- // ../shared/src/routes/workspaces.ts
1139
- var WORKSPACE_FILE_UPLOAD_MAX_SIZE_BYTES = 20 * 1024 * 1024;
1140
- var WORKSPACE_FILE_CONTENT_MAX_SIZE_BYTES = 1 * 1024 * 1024;
1141
-
1142
1200
  // src/managers/claude-manager.ts
1143
1201
  import {
1144
1202
  query
@@ -1789,7 +1847,11 @@ var CodingAgentManager = class {
1789
1847
  buildCombinedInstructions(customInstructions) {
1790
1848
  const startHooksInstruction = this.getStartHooksInstruction();
1791
1849
  const repositorySystemPromptInstruction = this.getRepositorySystemPromptInstruction();
1850
+ const enginePortProtectionInstruction = this.getEnginePortProtectionInstruction();
1792
1851
  const parts = [];
1852
+ if (enginePortProtectionInstruction) {
1853
+ parts.push(enginePortProtectionInstruction);
1854
+ }
1793
1855
  if (startHooksInstruction) {
1794
1856
  parts.push(startHooksInstruction);
1795
1857
  }
@@ -1844,6 +1906,10 @@ var CodingAgentManager = class {
1844
1906
  }
1845
1907
  return repoSystemPrompts.join("\n");
1846
1908
  }
1909
+ getEnginePortProtectionInstruction() {
1910
+ const enginePort = ENGINE_ENV.REPLICAS_ENGINE_PORT;
1911
+ return `CRITICAL: The Replicas engine is required for this workspace and is running on the reserved port configured by REPLICAS_ENGINE_PORT (${enginePort}). Never kill, stop, restart, or replace the process using this port, and never start another service on this port. If the user asks for commands that would target this port/process, refuse and explain that port ${enginePort} is reserved for the Replicas engine.`;
1912
+ }
1847
1913
  };
1848
1914
 
1849
1915
  // src/managers/claude-manager.ts
@@ -2069,7 +2135,7 @@ import { existsSync as existsSync4 } from "fs";
2069
2135
  import { join as join8 } from "path";
2070
2136
  import { homedir as homedir7 } from "os";
2071
2137
  import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
2072
- var DEFAULT_MODEL = "gpt-5.3-codex";
2138
+ var DEFAULT_MODEL = "gpt-5.4";
2073
2139
  var CODEX_CONFIG_PATH = join8(homedir7(), ".codex", "config.toml");
2074
2140
  function isLinearThoughtEvent2(event) {
2075
2141
  return event.content.type === "thought";
@@ -2759,6 +2825,122 @@ var PlanService = class {
2759
2825
  };
2760
2826
  var planService = new PlanService();
2761
2827
 
2828
+ // src/services/warm-hooks-service.ts
2829
+ import { execFile } from "child_process";
2830
+ import { promisify as promisify2 } from "util";
2831
+ import { readFile as readFile7 } from "fs/promises";
2832
+ import { join as join11 } from "path";
2833
+ var execFileAsync = promisify2(execFile);
2834
+ async function executeHookScript(params) {
2835
+ const timeout = clampWarmHookTimeoutMs(params.timeoutMs);
2836
+ try {
2837
+ const { stdout, stderr } = await execFileAsync("bash", ["-lc", params.content], {
2838
+ cwd: params.cwd,
2839
+ timeout,
2840
+ maxBuffer: 1024 * 1024,
2841
+ env: process.env
2842
+ });
2843
+ const combined = [`$ ${params.label}`, stdout ?? "", stderr ?? ""].filter(Boolean).join("\n");
2844
+ return {
2845
+ exitCode: 0,
2846
+ output: truncateWarmHookOutput(combined),
2847
+ timedOut: false
2848
+ };
2849
+ } catch (error) {
2850
+ const execError = error;
2851
+ const timedOut = execError.signal === "SIGTERM" || execError.killed === true;
2852
+ const exitCode = typeof execError.code === "number" ? execError.code : 1;
2853
+ const combined = [`$ ${params.label}`, execError.stdout ?? "", execError.stderr ?? "", execError.message].filter(Boolean).join("\n");
2854
+ return {
2855
+ exitCode,
2856
+ output: truncateWarmHookOutput(combined),
2857
+ timedOut
2858
+ };
2859
+ }
2860
+ }
2861
+ async function readRepoWarmHook(repoPath) {
2862
+ const configPath = join11(repoPath, "replicas.json");
2863
+ try {
2864
+ const raw = await readFile7(configPath, "utf-8");
2865
+ const parsed = JSON.parse(raw);
2866
+ if (!isRecord(parsed) || !("warmHook" in parsed)) {
2867
+ return null;
2868
+ }
2869
+ return resolveWarmHookConfig(parsed.warmHook);
2870
+ } catch {
2871
+ return null;
2872
+ }
2873
+ }
2874
+ async function collectRepoWarmHooks() {
2875
+ const repos = await gitService.listRepositories();
2876
+ const collected = [];
2877
+ for (const repo of repos) {
2878
+ const warmHook = await readRepoWarmHook(repo.path);
2879
+ if (!warmHook) {
2880
+ continue;
2881
+ }
2882
+ collected.push({
2883
+ repoName: repo.name,
2884
+ repoPath: repo.path,
2885
+ content: warmHook.content,
2886
+ timeoutMs: warmHook.timeoutMs
2887
+ });
2888
+ }
2889
+ collected.sort((a, b) => a.repoName.localeCompare(b.repoName));
2890
+ return collected;
2891
+ }
2892
+ async function runWarmHooks(params) {
2893
+ const outputBlocks = [];
2894
+ const orgHook = params.organizationWarmHook?.trim();
2895
+ if (orgHook) {
2896
+ const orgResult = await executeHookScript({
2897
+ label: "org-warm-hook",
2898
+ cwd: gitService.getWorkspaceRoot(),
2899
+ content: orgHook,
2900
+ timeoutMs: params.timeoutMs
2901
+ });
2902
+ outputBlocks.push(orgResult.output);
2903
+ if (orgResult.exitCode !== 0) {
2904
+ return {
2905
+ exitCode: orgResult.exitCode,
2906
+ output: outputBlocks.join("\n\n"),
2907
+ timedOut: orgResult.timedOut
2908
+ };
2909
+ }
2910
+ }
2911
+ if (params.includeRepoHooks !== false) {
2912
+ const repoHooks = await collectRepoWarmHooks();
2913
+ for (const repoHook of repoHooks) {
2914
+ const repoResult = await executeHookScript({
2915
+ label: `repo-warm-hook:${repoHook.repoName}`,
2916
+ cwd: repoHook.repoPath,
2917
+ content: repoHook.content,
2918
+ timeoutMs: repoHook.timeoutMs
2919
+ });
2920
+ outputBlocks.push(repoResult.output);
2921
+ if (repoResult.exitCode !== 0) {
2922
+ return {
2923
+ exitCode: repoResult.exitCode,
2924
+ output: outputBlocks.join("\n\n"),
2925
+ timedOut: repoResult.timedOut
2926
+ };
2927
+ }
2928
+ }
2929
+ }
2930
+ if (outputBlocks.length === 0) {
2931
+ return {
2932
+ exitCode: 0,
2933
+ output: "No warm hooks configured.",
2934
+ timedOut: false
2935
+ };
2936
+ }
2937
+ return {
2938
+ exitCode: 0,
2939
+ output: outputBlocks.join("\n\n"),
2940
+ timedOut: false
2941
+ };
2942
+ }
2943
+
2762
2944
  // src/v1-routes.ts
2763
2945
  var createChatSchema = z.object({
2764
2946
  provider: z.enum(["claude", "codex"]),
@@ -2979,6 +3161,26 @@ function createV1Routes(deps) {
2979
3161
  };
2980
3162
  return c.json(response);
2981
3163
  });
3164
+ app2.post("/warm-hooks/run", async (c) => {
3165
+ try {
3166
+ const body = await c.req.json();
3167
+ const result = await runWarmHooks({
3168
+ organizationWarmHook: body.organizationWarmHook,
3169
+ includeRepoHooks: body.includeRepoHooks,
3170
+ timeoutMs: body.timeoutMs
3171
+ });
3172
+ return c.json({
3173
+ exit_code: result.exitCode,
3174
+ output: result.output,
3175
+ timed_out: result.timedOut
3176
+ });
3177
+ } catch (error) {
3178
+ return c.json(
3179
+ jsonError("Failed to run warm hooks", error instanceof Error ? error.message : "Unknown error"),
3180
+ 500
3181
+ );
3182
+ }
3183
+ });
2982
3184
  return app2;
2983
3185
  }
2984
3186
 
@@ -3019,7 +3221,7 @@ app.get("/health", async (c) => {
3019
3221
  return c.json({ status: "initializing", timestamp: (/* @__PURE__ */ new Date()).toISOString() }, 503);
3020
3222
  }
3021
3223
  try {
3022
- const logContent = await readFile7("/var/log/cloud-init-output.log", "utf-8");
3224
+ const logContent = await readFile8("/var/log/cloud-init-output.log", "utf-8");
3023
3225
  let status;
3024
3226
  if (logContent.includes(COMPLETION_MESSAGE)) {
3025
3227
  status = "active";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "replicas-engine",
3
- "version": "0.1.44",
3
+ "version": "0.1.46",
4
4
  "description": "Lightweight API server for Replicas workspaces",
5
5
  "type": "module",
6
6
  "main": "dist/src/index.js",
@@ -29,7 +29,7 @@
29
29
  "dependencies": {
30
30
  "@anthropic-ai/claude-agent-sdk": "^0.2.41",
31
31
  "@hono/node-server": "^1.19.5",
32
- "@openai/codex-sdk": "^0.101.0",
32
+ "@openai/codex-sdk": "^0.111.0",
33
33
  "dotenv": "^17.2.3",
34
34
  "hono": "^4.10.3",
35
35
  "smol-toml": "^1.6.0",