replicas-engine 0.1.53 → 0.1.55

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 +222 -113
  2. package/package.json +2 -1
package/dist/src/index.js CHANGED
@@ -4,9 +4,9 @@ import "./chunk-ZXMDA7VB.js";
4
4
  // src/index.ts
5
5
  import { serve } from "@hono/node-server";
6
6
  import { Hono as Hono2 } from "hono";
7
- import { readFile as readFile9 } from "fs/promises";
7
+ import { readFile as readFile10 } from "fs/promises";
8
8
  import { execSync } from "child_process";
9
- import { randomUUID as randomUUID4 } from "crypto";
9
+ import { randomUUID as randomUUID5 } from "crypto";
10
10
 
11
11
  // src/managers/github-token-manager.ts
12
12
  import { promises as fs } from "fs";
@@ -846,9 +846,13 @@ var SANDBOX_PATHS = {
846
846
  REPLICAS_DIR: "/home/ubuntu/.replicas",
847
847
  REPLICAS_FILES_DIR: "/home/ubuntu/.replicas/files",
848
848
  REPLICAS_FILES_DISPLAY_DIR: "~/.replicas/files",
849
- REPLICAS_RUNTIME_ENV_FILE: "/home/ubuntu/.replicas/runtime-env.sh"
849
+ REPLICAS_RUNTIME_ENV_FILE: "/home/ubuntu/.replicas/runtime-env.sh",
850
+ REPLICAS_PREVIEW_PORTS_FILE: "/home/ubuntu/.replicas/preview-ports.json"
850
851
  };
851
852
 
853
+ // ../shared/src/replicas-config.ts
854
+ import { parse as parseYaml } from "yaml";
855
+
852
856
  // ../shared/src/warm-hooks.ts
853
857
  var DEFAULT_WARM_HOOK_TIMEOUT_MS = 5 * 60 * 1e3;
854
858
  var MAX_WARM_HOOK_TIMEOUT_MS = 15 * 60 * 1e3;
@@ -869,18 +873,18 @@ function truncateWarmHookOutput(text, maxChars = DEFAULT_WARM_HOOK_OUTPUT_MAX_CH
869
873
  return `${text.slice(0, maxChars)}
870
874
  ...[truncated]`;
871
875
  }
872
- function parseWarmHookConfig(value) {
876
+ function parseWarmHookConfig(value, filename = "replicas.json") {
873
877
  if (typeof value === "string") {
874
878
  return value;
875
879
  }
876
880
  if (!isRecord2(value)) {
877
- throw new Error('Invalid replicas.json: "warmHook" must be a string or object');
881
+ throw new Error(`Invalid ${filename}: "warmHook" must be a string or object`);
878
882
  }
879
883
  if (!Array.isArray(value.commands) || !value.commands.every((entry) => typeof entry === "string")) {
880
- throw new Error('Invalid replicas.json: "warmHook.commands" must be an array of shell commands');
884
+ throw new Error(`Invalid ${filename}: "warmHook.commands" must be an array of shell commands`);
881
885
  }
882
886
  if (value.timeout !== void 0 && (typeof value.timeout !== "number" || value.timeout <= 0)) {
883
- throw new Error('Invalid replicas.json: "warmHook.timeout" must be a positive number');
887
+ throw new Error(`Invalid ${filename}: "warmHook.timeout" must be a positive number`);
884
888
  }
885
889
  return {
886
890
  commands: value.commands,
@@ -902,6 +906,57 @@ function resolveWarmHookConfig(value) {
902
906
  };
903
907
  }
904
908
 
909
+ // ../shared/src/replicas-config.ts
910
+ var REPLICAS_CONFIG_FILENAMES = ["replicas.json", "replicas.yaml", "replicas.yml"];
911
+ function isRecord3(value) {
912
+ return typeof value === "object" && value !== null;
913
+ }
914
+ function parseReplicasConfig(value, filename = "replicas.json") {
915
+ if (!isRecord3(value)) {
916
+ throw new Error(`Invalid ${filename}: expected an object`);
917
+ }
918
+ const config = {};
919
+ if ("organizationId" in value) {
920
+ if (typeof value.organizationId !== "string") {
921
+ throw new Error(`Invalid ${filename}: "organizationId" must be a string`);
922
+ }
923
+ config.organizationId = value.organizationId;
924
+ }
925
+ if ("systemPrompt" in value) {
926
+ if (typeof value.systemPrompt !== "string") {
927
+ throw new Error(`Invalid ${filename}: "systemPrompt" must be a string`);
928
+ }
929
+ config.systemPrompt = value.systemPrompt;
930
+ }
931
+ if ("startHook" in value) {
932
+ if (!isRecord3(value.startHook)) {
933
+ throw new Error(`Invalid ${filename}: "startHook" must be an object with "commands" array`);
934
+ }
935
+ const { commands, timeout } = value.startHook;
936
+ if (!Array.isArray(commands) || !commands.every((entry) => typeof entry === "string")) {
937
+ throw new Error(`Invalid ${filename}: "startHook.commands" must be an array of shell commands`);
938
+ }
939
+ if (timeout !== void 0 && (typeof timeout !== "number" || timeout <= 0)) {
940
+ throw new Error(`Invalid ${filename}: "startHook.timeout" must be a positive number`);
941
+ }
942
+ config.startHook = { commands, timeout };
943
+ }
944
+ if ("warmHook" in value) {
945
+ config.warmHook = parseWarmHookConfig(value.warmHook, filename);
946
+ }
947
+ return config;
948
+ }
949
+ function parseReplicasConfigString(content, filename) {
950
+ const isYaml = filename.endsWith(".yaml") || filename.endsWith(".yml");
951
+ let parsed;
952
+ if (isYaml) {
953
+ parsed = parseYaml(content);
954
+ } else {
955
+ parsed = JSON.parse(content);
956
+ }
957
+ return parseReplicasConfig(parsed, filename);
958
+ }
959
+
905
960
  // ../shared/src/engine/environment.ts
906
961
  var REPLICAS_ENGINE_VERSION = "09-03-2026-kipling";
907
962
 
@@ -1081,52 +1136,17 @@ These hooks are currently executing in the background. You can:
1081
1136
 
1082
1137
  The start hooks may install dependencies, build projects, or perform other setup tasks.
1083
1138
  If your task depends on setup being complete, check the log file before proceeding.`;
1084
- function parseReplicasConfig(value) {
1085
- if (!isRecord(value)) {
1086
- throw new Error("Invalid replicas.json: expected an object");
1087
- }
1088
- const config = {};
1089
- if ("copy" in value) {
1090
- if (!Array.isArray(value.copy) || !value.copy.every((entry) => typeof entry === "string")) {
1091
- throw new Error('Invalid replicas.json: "copy" must be an array of file paths');
1092
- }
1093
- config.copy = value.copy;
1094
- }
1095
- if ("ports" in value) {
1096
- if (!Array.isArray(value.ports) || !value.ports.every((entry) => typeof entry === "number")) {
1097
- throw new Error('Invalid replicas.json: "ports" must be an array of port numbers');
1098
- }
1099
- config.ports = value.ports;
1100
- }
1101
- if ("organizationId" in value) {
1102
- if (typeof value.organizationId !== "string") {
1103
- throw new Error('Invalid replicas.json: "organizationId" must be a string');
1104
- }
1105
- config.organizationId = value.organizationId;
1106
- }
1107
- if ("systemPrompt" in value) {
1108
- if (typeof value.systemPrompt !== "string") {
1109
- throw new Error('Invalid replicas.json: "systemPrompt" must be a string');
1110
- }
1111
- config.systemPrompt = value.systemPrompt;
1112
- }
1113
- if ("startHook" in value) {
1114
- if (!isRecord(value.startHook)) {
1115
- throw new Error('Invalid replicas.json: "startHook" must be an object with "commands" array');
1116
- }
1117
- const { commands, timeout } = value.startHook;
1118
- if (!Array.isArray(commands) || !commands.every((entry) => typeof entry === "string")) {
1119
- throw new Error('Invalid replicas.json: "startHook.commands" must be an array of shell commands');
1120
- }
1121
- if (timeout !== void 0 && (typeof timeout !== "number" || timeout <= 0)) {
1122
- throw new Error('Invalid replicas.json: "startHook.timeout" must be a positive number');
1139
+ async function readReplicasConfigFromDir(dirPath) {
1140
+ for (const filename of REPLICAS_CONFIG_FILENAMES) {
1141
+ const configPath = join6(dirPath, filename);
1142
+ if (!existsSync4(configPath)) {
1143
+ continue;
1123
1144
  }
1124
- config.startHook = { commands, timeout };
1145
+ const data = await readFile3(configPath, "utf-8");
1146
+ const config = parseReplicasConfigString(data, filename);
1147
+ return { config, filename };
1125
1148
  }
1126
- if ("warmHook" in value) {
1127
- config.warmHook = parseWarmHookConfig(value.warmHook);
1128
- }
1129
- return config;
1149
+ return null;
1130
1150
  }
1131
1151
  var ReplicasConfigService = class {
1132
1152
  configs = [];
@@ -1134,7 +1154,7 @@ var ReplicasConfigService = class {
1134
1154
  hooksCompleted = false;
1135
1155
  hooksFailed = false;
1136
1156
  /**
1137
- * Initialize by reading all replicas.json files and running start hooks.
1157
+ * Initialize by reading all replicas config files and running start hooks.
1138
1158
  */
1139
1159
  async initialize() {
1140
1160
  await this.loadConfigs();
@@ -1146,30 +1166,29 @@ var ReplicasConfigService = class {
1146
1166
  });
1147
1167
  }
1148
1168
  /**
1149
- * Load and parse replicas.json from each discovered repository root.
1169
+ * Load and parse replicas config from each discovered repository root.
1170
+ * Checks replicas.json first, then falls back to replicas.yaml/replicas.yml.
1150
1171
  */
1151
1172
  async loadConfigs() {
1152
1173
  const repos = await gitService.listRepositories();
1153
1174
  const configs = [];
1154
1175
  for (const repo of repos) {
1155
- const configPath = join6(repo.path, "replicas.json");
1156
- if (!existsSync4(configPath)) {
1157
- continue;
1158
- }
1159
1176
  try {
1160
- const data = await readFile3(configPath, "utf-8");
1161
- const config = parseReplicasConfig(JSON.parse(data));
1177
+ const result = await readReplicasConfigFromDir(repo.path);
1178
+ if (!result) {
1179
+ continue;
1180
+ }
1162
1181
  configs.push({
1163
1182
  repoName: repo.name,
1164
1183
  workingDirectory: repo.path,
1165
1184
  defaultBranch: repo.defaultBranch,
1166
- config
1185
+ config: result.config
1167
1186
  });
1168
1187
  } catch (error) {
1169
1188
  if (error instanceof SyntaxError) {
1170
- console.error(`[ReplicasConfig] Failed to parse ${repo.name}/replicas.json:`, error.message);
1189
+ console.error(`[ReplicasConfig] Failed to parse ${repo.name} config:`, error.message);
1171
1190
  } else if (error instanceof Error) {
1172
- console.error(`[ReplicasConfig] Error loading ${repo.name}/replicas.json:`, error.message);
1191
+ console.error(`[ReplicasConfig] Error loading ${repo.name} config:`, error.message);
1173
1192
  }
1174
1193
  }
1175
1194
  }
@@ -1368,22 +1387,77 @@ var EventService = class {
1368
1387
  };
1369
1388
  var eventService = new EventService();
1370
1389
 
1390
+ // src/services/preview-service.ts
1391
+ import { mkdir as mkdir6, readFile as readFile4, writeFile as writeFile5 } from "fs/promises";
1392
+ import { existsSync as existsSync5 } from "fs";
1393
+ import { randomUUID as randomUUID2 } from "crypto";
1394
+ import { dirname } from "path";
1395
+ var PREVIEW_PORTS_FILE = SANDBOX_PATHS.REPLICAS_PREVIEW_PORTS_FILE;
1396
+ async function readPreviewsFile() {
1397
+ try {
1398
+ if (!existsSync5(PREVIEW_PORTS_FILE)) {
1399
+ return { previews: [] };
1400
+ }
1401
+ const raw = await readFile4(PREVIEW_PORTS_FILE, "utf-8");
1402
+ return JSON.parse(raw);
1403
+ } catch {
1404
+ return { previews: [] };
1405
+ }
1406
+ }
1407
+ async function writePreviewsFile(data) {
1408
+ const dir = dirname(PREVIEW_PORTS_FILE);
1409
+ await mkdir6(dir, { recursive: true });
1410
+ await writeFile5(PREVIEW_PORTS_FILE, `${JSON.stringify(data, null, 2)}
1411
+ `, "utf-8");
1412
+ }
1413
+ var PreviewService = class {
1414
+ async initialize() {
1415
+ await writePreviewsFile({ previews: [] });
1416
+ }
1417
+ async addPreview(port2, publicUrl) {
1418
+ const data = await readPreviewsFile();
1419
+ const existing = data.previews.find((p) => p.port === port2);
1420
+ if (existing) {
1421
+ throw new Error(`Preview already exists for port ${port2}`);
1422
+ }
1423
+ const preview = {
1424
+ port: port2,
1425
+ publicUrl,
1426
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1427
+ };
1428
+ data.previews.push(preview);
1429
+ await writePreviewsFile(data);
1430
+ eventService.publish({
1431
+ id: randomUUID2(),
1432
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
1433
+ type: "preview.changed",
1434
+ payload: { previews: data.previews }
1435
+ });
1436
+ return preview;
1437
+ }
1438
+ async listPreviews() {
1439
+ const data = await readPreviewsFile();
1440
+ return data.previews;
1441
+ }
1442
+ };
1443
+ var previewService = new PreviewService();
1444
+
1371
1445
  // src/services/chat/chat-service.ts
1372
- import { mkdir as mkdir8, readFile as readFile6, rm, writeFile as writeFile6 } from "fs/promises";
1446
+ import { mkdir as mkdir9, readFile as readFile7, rm, writeFile as writeFile7 } from "fs/promises";
1373
1447
  import { homedir as homedir9 } from "os";
1374
1448
  import { join as join10 } from "path";
1375
- import { randomUUID as randomUUID3 } from "crypto";
1449
+ import { randomUUID as randomUUID4 } from "crypto";
1376
1450
 
1377
1451
  // src/managers/claude-manager.ts
1378
1452
  import {
1379
1453
  query
1380
1454
  } from "@anthropic-ai/claude-agent-sdk";
1381
1455
  import { join as join8 } from "path";
1382
- import { mkdir as mkdir6, appendFile as appendFile4 } from "fs/promises";
1456
+ import { mkdir as mkdir7, appendFile as appendFile4 } from "fs/promises";
1383
1457
  import { homedir as homedir7 } from "os";
1384
1458
 
1385
1459
  // src/utils/jsonl-reader.ts
1386
- import { readFile as readFile4 } from "fs/promises";
1460
+ import { readFile as readFile5 } from "fs/promises";
1387
1461
  function isJsonlEvent(value) {
1388
1462
  if (!isRecord(value)) {
1389
1463
  return false;
@@ -1405,7 +1479,7 @@ function parseJsonlEvents(lines) {
1405
1479
  }
1406
1480
  async function readJSONL(filePath) {
1407
1481
  try {
1408
- const content = await readFile4(filePath, "utf-8");
1482
+ const content = await readFile5(filePath, "utf-8");
1409
1483
  const lines = content.split("\n").filter((line) => line.trim());
1410
1484
  return parseJsonlEvents(lines);
1411
1485
  } catch (error) {
@@ -2281,7 +2355,7 @@ var ClaudeManager = class extends CodingAgentManager {
2281
2355
  }
2282
2356
  async initialize() {
2283
2357
  const historyDir = join8(homedir7(), ".replicas", "claude");
2284
- await mkdir6(historyDir, { recursive: true });
2358
+ await mkdir7(historyDir, { recursive: true });
2285
2359
  if (this.initialSessionId) {
2286
2360
  this.sessionId = this.initialSessionId;
2287
2361
  console.log(`[ClaudeManager] Restored session ID from persisted state: ${this.sessionId}`);
@@ -2309,9 +2383,9 @@ var ClaudeManager = class extends CodingAgentManager {
2309
2383
 
2310
2384
  // src/managers/codex-manager.ts
2311
2385
  import { Codex } from "@openai/codex-sdk";
2312
- import { randomUUID as randomUUID2 } from "crypto";
2313
- import { readdir as readdir2, stat as stat2, writeFile as writeFile5, mkdir as mkdir7, readFile as readFile5 } from "fs/promises";
2314
- import { existsSync as existsSync5 } from "fs";
2386
+ import { randomUUID as randomUUID3 } from "crypto";
2387
+ import { readdir as readdir2, stat as stat2, writeFile as writeFile6, mkdir as mkdir8, readFile as readFile6 } from "fs/promises";
2388
+ import { existsSync as existsSync6 } from "fs";
2315
2389
  import { join as join9 } from "path";
2316
2390
  import { homedir as homedir8 } from "os";
2317
2391
  import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
@@ -2359,11 +2433,11 @@ var CodexManager = class extends CodingAgentManager {
2359
2433
  async updateCodexConfig(developerInstructions) {
2360
2434
  try {
2361
2435
  const codexDir = join9(homedir8(), ".codex");
2362
- await mkdir7(codexDir, { recursive: true });
2436
+ await mkdir8(codexDir, { recursive: true });
2363
2437
  let config = {};
2364
- if (existsSync5(CODEX_CONFIG_PATH)) {
2438
+ if (existsSync6(CODEX_CONFIG_PATH)) {
2365
2439
  try {
2366
- const existingContent = await readFile5(CODEX_CONFIG_PATH, "utf-8");
2440
+ const existingContent = await readFile6(CODEX_CONFIG_PATH, "utf-8");
2367
2441
  const parsed = parseToml(existingContent);
2368
2442
  if (isRecord(parsed)) {
2369
2443
  config = parsed;
@@ -2378,7 +2452,7 @@ var CodexManager = class extends CodingAgentManager {
2378
2452
  delete config.developer_instructions;
2379
2453
  }
2380
2454
  const tomlContent = stringifyToml(config);
2381
- await writeFile5(CODEX_CONFIG_PATH, tomlContent, "utf-8");
2455
+ await writeFile6(CODEX_CONFIG_PATH, tomlContent, "utf-8");
2382
2456
  console.log("[CodexManager] Updated config.toml with developer_instructions");
2383
2457
  } catch (error) {
2384
2458
  console.error("[CodexManager] Failed to update config.toml:", error);
@@ -2389,14 +2463,14 @@ var CodexManager = class extends CodingAgentManager {
2389
2463
  * @returns Array of temp file paths
2390
2464
  */
2391
2465
  async saveImagesToTempFiles(images) {
2392
- await mkdir7(this.tempImageDir, { recursive: true });
2466
+ await mkdir8(this.tempImageDir, { recursive: true });
2393
2467
  const tempPaths = [];
2394
2468
  for (const image of images) {
2395
2469
  const ext = image.source.media_type.split("/")[1] || "png";
2396
- const filename = `img_${randomUUID2()}.${ext}`;
2470
+ const filename = `img_${randomUUID3()}.${ext}`;
2397
2471
  const filepath = join9(this.tempImageDir, filename);
2398
2472
  const buffer = Buffer.from(image.source.data, "base64");
2399
- await writeFile5(filepath, buffer);
2473
+ await writeFile6(filepath, buffer);
2400
2474
  tempPaths.push(filepath);
2401
2475
  }
2402
2476
  return tempPaths;
@@ -2588,7 +2662,7 @@ var CodexManager = class extends CodingAgentManager {
2588
2662
  const seenLines = /* @__PURE__ */ new Set();
2589
2663
  const seedSeenLines = async () => {
2590
2664
  try {
2591
- const content = await readFile5(sessionFile, "utf-8");
2665
+ const content = await readFile6(sessionFile, "utf-8");
2592
2666
  const lines = content.split("\n").map((line) => line.trim()).filter(Boolean);
2593
2667
  for (const line of lines) {
2594
2668
  seenLines.add(line);
@@ -2600,7 +2674,7 @@ var CodexManager = class extends CodingAgentManager {
2600
2674
  const pump = async () => {
2601
2675
  let emitted = 0;
2602
2676
  try {
2603
- const content = await readFile5(sessionFile, "utf-8");
2677
+ const content = await readFile6(sessionFile, "utf-8");
2604
2678
  const lines = content.split("\n");
2605
2679
  const completeLines = content.endsWith("\n") ? lines : lines.slice(0, -1);
2606
2680
  for (const line of completeLines) {
@@ -2688,8 +2762,8 @@ var ChatService = class {
2688
2762
  chats = /* @__PURE__ */ new Map();
2689
2763
  writeChain = Promise.resolve();
2690
2764
  async initialize() {
2691
- await mkdir8(ENGINE_DIR2, { recursive: true });
2692
- await mkdir8(CLAUDE_HISTORY_DIR, { recursive: true });
2765
+ await mkdir9(ENGINE_DIR2, { recursive: true });
2766
+ await mkdir9(CLAUDE_HISTORY_DIR, { recursive: true });
2693
2767
  const persisted = await this.loadChats();
2694
2768
  for (const chat of persisted) {
2695
2769
  const runtime = this.createRuntimeChat(chat);
@@ -2710,7 +2784,7 @@ var ChatService = class {
2710
2784
  throw new DuplicateDefaultChatError(request.provider);
2711
2785
  }
2712
2786
  const persisted = {
2713
- id: randomUUID3(),
2787
+ id: randomUUID4(),
2714
2788
  provider: request.provider,
2715
2789
  title,
2716
2790
  createdAt: now,
@@ -2912,7 +2986,7 @@ var ChatService = class {
2912
2986
  }
2913
2987
  async loadChats() {
2914
2988
  try {
2915
- const content = await readFile6(CHATS_FILE, "utf-8");
2989
+ const content = await readFile7(CHATS_FILE, "utf-8");
2916
2990
  const parsed = JSON.parse(content);
2917
2991
  if (!Array.isArray(parsed)) {
2918
2992
  return [];
@@ -2929,13 +3003,13 @@ var ChatService = class {
2929
3003
  this.writeChain = this.writeChain.catch(() => {
2930
3004
  }).then(async () => {
2931
3005
  const payload = Array.from(this.chats.values()).map((chat) => chat.persisted);
2932
- await writeFile6(CHATS_FILE, JSON.stringify(payload, null, 2), "utf-8");
3006
+ await writeFile7(CHATS_FILE, JSON.stringify(payload, null, 2), "utf-8");
2933
3007
  });
2934
3008
  await this.writeChain;
2935
3009
  }
2936
3010
  async publish(input) {
2937
3011
  const event = {
2938
- id: randomUUID3(),
3012
+ id: randomUUID4(),
2939
3013
  ts: (/* @__PURE__ */ new Date()).toISOString(),
2940
3014
  ...input
2941
3015
  };
@@ -2954,7 +3028,7 @@ import { Hono } from "hono";
2954
3028
  import { z } from "zod";
2955
3029
 
2956
3030
  // src/services/plan-service.ts
2957
- import { readdir as readdir3, readFile as readFile7 } from "fs/promises";
3031
+ import { readdir as readdir3, readFile as readFile8 } from "fs/promises";
2958
3032
  import { homedir as homedir10 } from "os";
2959
3033
  import { basename, join as join11 } from "path";
2960
3034
  var PLAN_DIRECTORIES = [
@@ -2995,7 +3069,7 @@ var PlanService = class {
2995
3069
  for (const directory of PLAN_DIRECTORIES) {
2996
3070
  const filePath = join11(directory, safeFilename);
2997
3071
  try {
2998
- const content = await readFile7(filePath, "utf-8");
3072
+ const content = await readFile8(filePath, "utf-8");
2999
3073
  return { filename: safeFilename, content };
3000
3074
  } catch {
3001
3075
  }
@@ -3008,7 +3082,8 @@ var planService = new PlanService();
3008
3082
  // src/services/warm-hooks-service.ts
3009
3083
  import { execFile as execFile2 } from "child_process";
3010
3084
  import { promisify as promisify3 } from "util";
3011
- import { readFile as readFile8 } from "fs/promises";
3085
+ import { readFile as readFile9 } from "fs/promises";
3086
+ import { existsSync as existsSync7 } from "fs";
3012
3087
  import { join as join12 } from "path";
3013
3088
  var execFileAsync2 = promisify3(execFile2);
3014
3089
  async function executeHookScript(params) {
@@ -3039,21 +3114,27 @@ async function executeHookScript(params) {
3039
3114
  }
3040
3115
  }
3041
3116
  async function readRepoWarmHook(repoPath) {
3042
- const configPath = join12(repoPath, "replicas.json");
3043
- try {
3044
- const raw = await readFile8(configPath, "utf-8");
3045
- const parsed = JSON.parse(raw);
3046
- if (!isRecord(parsed) || !("warmHook" in parsed)) {
3047
- return null;
3117
+ for (const filename of REPLICAS_CONFIG_FILENAMES) {
3118
+ const configPath = join12(repoPath, filename);
3119
+ if (!existsSync7(configPath)) {
3120
+ continue;
3048
3121
  }
3049
- return resolveWarmHookConfig(parsed.warmHook);
3050
- } catch (error) {
3051
- const nodeError = error;
3052
- if (nodeError?.code === "ENOENT") {
3053
- return null;
3122
+ try {
3123
+ const raw = await readFile9(configPath, "utf-8");
3124
+ const config = parseReplicasConfigString(raw, filename);
3125
+ if (!config.warmHook) {
3126
+ return null;
3127
+ }
3128
+ return resolveWarmHookConfig(config.warmHook);
3129
+ } catch (error) {
3130
+ const nodeError = error;
3131
+ if (nodeError?.code === "ENOENT") {
3132
+ continue;
3133
+ }
3134
+ throw error;
3054
3135
  }
3055
- throw error;
3056
3136
  }
3137
+ return null;
3057
3138
  }
3058
3139
  async function collectRepoWarmHooks() {
3059
3140
  const repos = await gitService.listRepositories();
@@ -3152,6 +3233,10 @@ var createChatSchema = z.object({
3152
3233
  title: z.string().min(1).optional()
3153
3234
  });
3154
3235
  var imageMediaTypeSchema = z.enum(IMAGE_MEDIA_TYPES);
3236
+ var createPreviewSchema = z.object({
3237
+ port: z.number().int().min(1).max(65535),
3238
+ publicUrl: z.string().min(1)
3239
+ });
3155
3240
  var sendMessageSchema = z.object({
3156
3241
  message: z.string().min(1),
3157
3242
  model: z.string().optional(),
@@ -3399,7 +3484,6 @@ function createV1Routes(deps) {
3399
3484
  try {
3400
3485
  const body = await c.req.json();
3401
3486
  const result = await runWarmHooks({
3402
- skills: Array.isArray(body.skills) ? body.skills : [],
3403
3487
  organizationWarmHook: body.organizationWarmHook,
3404
3488
  includeRepoHooks: body.includeRepoHooks,
3405
3489
  timeoutMs: body.timeoutMs
@@ -3416,6 +3500,30 @@ function createV1Routes(deps) {
3416
3500
  );
3417
3501
  }
3418
3502
  });
3503
+ app2.get("/previews", async (c) => {
3504
+ try {
3505
+ const previews = await previewService.listPreviews();
3506
+ return c.json({ previews });
3507
+ } catch (error) {
3508
+ return c.json(
3509
+ jsonError("Failed to list previews", error instanceof Error ? error.message : "Unknown error"),
3510
+ 500
3511
+ );
3512
+ }
3513
+ });
3514
+ app2.post("/previews", async (c) => {
3515
+ try {
3516
+ const body = createPreviewSchema.parse(await c.req.json());
3517
+ const preview = await previewService.addPreview(body.port, body.publicUrl);
3518
+ return c.json({ preview }, 201);
3519
+ } catch (error) {
3520
+ const details = error instanceof Error ? error.message : "Unknown error";
3521
+ if (details.includes("already exists")) {
3522
+ return c.json(jsonError("Preview already exists", details), 409);
3523
+ }
3524
+ return c.json(jsonError("Failed to create preview", details), 400);
3525
+ }
3526
+ });
3419
3527
  return app2;
3420
3528
  }
3421
3529
 
@@ -3456,7 +3564,7 @@ app.get("/health", async (c) => {
3456
3564
  return c.json({ status: "initializing", timestamp: (/* @__PURE__ */ new Date()).toISOString() }, 503);
3457
3565
  }
3458
3566
  try {
3459
- const logContent = await readFile9("/var/log/cloud-init-output.log", "utf-8");
3567
+ const logContent = await readFile10("/var/log/cloud-init-output.log", "utf-8");
3460
3568
  let status;
3461
3569
  if (logContent.includes(COMPLETION_MESSAGE)) {
3462
3570
  status = "active";
@@ -3511,7 +3619,7 @@ function startStatusBroadcaster() {
3511
3619
  if (serialized !== previousRepoStatus) {
3512
3620
  previousRepoStatus = serialized;
3513
3621
  eventService.publish({
3514
- id: randomUUID4(),
3622
+ id: randomUUID5(),
3515
3623
  ts: (/* @__PURE__ */ new Date()).toISOString(),
3516
3624
  type: "repo.status.changed",
3517
3625
  payload: { repos }
@@ -3531,7 +3639,7 @@ function startStatusBroadcaster() {
3531
3639
  if (engineStatusJson !== previousEngineStatus) {
3532
3640
  previousEngineStatus = engineStatusJson;
3533
3641
  eventService.publish({
3534
- id: randomUUID4(),
3642
+ id: randomUUID5(),
3535
3643
  ts: (/* @__PURE__ */ new Date()).toISOString(),
3536
3644
  type: "engine.status.changed",
3537
3645
  payload: { status: engineStatus }
@@ -3552,7 +3660,7 @@ function startStatusBroadcaster() {
3552
3660
  previousHookStatus = hookSnapshot;
3553
3661
  if (!lastHooksRunning && hooksRunning) {
3554
3662
  eventService.publish({
3555
- id: randomUUID4(),
3663
+ id: randomUUID5(),
3556
3664
  ts: (/* @__PURE__ */ new Date()).toISOString(),
3557
3665
  type: "hooks.started",
3558
3666
  payload: { running: true, completed: false }
@@ -3561,7 +3669,7 @@ function startStatusBroadcaster() {
3561
3669
  }
3562
3670
  if (hooksRunning) {
3563
3671
  eventService.publish({
3564
- id: randomUUID4(),
3672
+ id: randomUUID5(),
3565
3673
  ts: (/* @__PURE__ */ new Date()).toISOString(),
3566
3674
  type: "hooks.progress",
3567
3675
  payload: { running: true, completed: false }
@@ -3570,7 +3678,7 @@ function startStatusBroadcaster() {
3570
3678
  }
3571
3679
  if (lastHooksRunning && !hooksRunning && hooksCompleted && !hooksFailed) {
3572
3680
  eventService.publish({
3573
- id: randomUUID4(),
3681
+ id: randomUUID5(),
3574
3682
  ts: (/* @__PURE__ */ new Date()).toISOString(),
3575
3683
  type: "hooks.completed",
3576
3684
  payload: { running: false, completed: true }
@@ -3579,7 +3687,7 @@ function startStatusBroadcaster() {
3579
3687
  }
3580
3688
  if (lastHooksRunning && !hooksRunning && hooksFailed) {
3581
3689
  eventService.publish({
3582
- id: randomUUID4(),
3690
+ id: randomUUID5(),
3583
3691
  ts: (/* @__PURE__ */ new Date()).toISOString(),
3584
3692
  type: "hooks.failed",
3585
3693
  payload: { running: false, completed: hooksCompleted }
@@ -3587,7 +3695,7 @@ function startStatusBroadcaster() {
3587
3695
  });
3588
3696
  }
3589
3697
  eventService.publish({
3590
- id: randomUUID4(),
3698
+ id: randomUUID5(),
3591
3699
  ts: (/* @__PURE__ */ new Date()).toISOString(),
3592
3700
  type: "hooks.status",
3593
3701
  payload: {
@@ -3617,6 +3725,7 @@ serve(
3617
3725
  }
3618
3726
  await replicasConfigService.initialize();
3619
3727
  await chatService.initialize();
3728
+ await previewService.initialize();
3620
3729
  engineReady = true;
3621
3730
  if (!IS_WARMING_MODE) {
3622
3731
  await githubTokenManager.start();
@@ -3625,20 +3734,20 @@ serve(
3625
3734
  }
3626
3735
  const repos = await gitService.listRepos();
3627
3736
  await eventService.publish({
3628
- id: randomUUID4(),
3737
+ id: randomUUID5(),
3629
3738
  ts: (/* @__PURE__ */ new Date()).toISOString(),
3630
3739
  type: "repo.discovered",
3631
3740
  payload: { repos }
3632
3741
  });
3633
3742
  const repoStatuses = await gitService.listRepos();
3634
3743
  await eventService.publish({
3635
- id: randomUUID4(),
3744
+ id: randomUUID5(),
3636
3745
  ts: (/* @__PURE__ */ new Date()).toISOString(),
3637
3746
  type: "repo.status.changed",
3638
3747
  payload: { repos: repoStatuses }
3639
3748
  });
3640
3749
  await eventService.publish({
3641
- id: randomUUID4(),
3750
+ id: randomUUID5(),
3642
3751
  ts: (/* @__PURE__ */ new Date()).toISOString(),
3643
3752
  type: "engine.ready",
3644
3753
  payload: { version: "v1" }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "replicas-engine",
3
- "version": "0.1.53",
3
+ "version": "0.1.55",
4
4
  "description": "Lightweight API server for Replicas workspaces",
5
5
  "type": "module",
6
6
  "main": "dist/src/index.js",
@@ -32,6 +32,7 @@
32
32
  "@openai/codex-sdk": "^0.111.0",
33
33
  "hono": "^4.10.3",
34
34
  "smol-toml": "^1.6.0",
35
+ "yaml": "^2.8.2",
35
36
  "zod": "^4.0.0"
36
37
  },
37
38
  "devDependencies": {