replicas-engine 0.1.61 → 0.1.63

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 CHANGED
@@ -1,10 +1,9 @@
1
1
  #!/usr/bin/env node
2
- import "./chunk-ZXMDA7VB.js";
3
2
 
4
3
  // src/index.ts
5
4
  import { serve } from "@hono/node-server";
6
5
  import { Hono as Hono2 } from "hono";
7
- import { readFile as readFile10 } from "fs/promises";
6
+ import { readFile as readFile13 } from "fs/promises";
8
7
  import { execSync } from "child_process";
9
8
  import { randomUUID as randomUUID5 } from "crypto";
10
9
 
@@ -800,15 +799,18 @@ import { format } from "util";
800
799
  import { randomBytes } from "crypto";
801
800
  var LOG_DIR = join4(homedir3(), ".replicas", "logs");
802
801
  var EngineLogger = class {
803
- sessionId = null;
802
+ _sessionId = null;
804
803
  filePath = null;
805
804
  writeChain = Promise.resolve();
806
805
  patched = false;
806
+ get sessionId() {
807
+ return this._sessionId;
808
+ }
807
809
  async initialize() {
808
810
  await mkdir2(LOG_DIR, { recursive: true });
809
- this.sessionId = this.createSessionId();
810
- this.filePath = join4(LOG_DIR, `${this.sessionId}.log`);
811
- await writeFile2(this.filePath, `=== Replicas Engine Session ${this.sessionId} ===
811
+ this._sessionId = this.createSessionId();
812
+ this.filePath = join4(LOG_DIR, `${this._sessionId}.log`);
813
+ await writeFile2(this.filePath, `=== Replicas Engine Session ${this._sessionId} ===
812
814
  `, "utf-8");
813
815
  this.patchConsole();
814
816
  this.log("INFO", `Engine logging initialized at ${this.filePath}`);
@@ -854,9 +856,9 @@ var EngineLogger = class {
854
856
  var engineLogger = new EngineLogger();
855
857
 
856
858
  // src/services/replicas-config-service.ts
857
- import { readFile as readFile3, appendFile as appendFile2, writeFile as writeFile4, mkdir as mkdir4 } from "fs/promises";
859
+ import { readFile as readFile4, appendFile as appendFile2, writeFile as writeFile5, mkdir as mkdir5 } from "fs/promises";
858
860
  import { existsSync as existsSync4 } from "fs";
859
- import { join as join6 } from "path";
861
+ import { join as join7 } from "path";
860
862
  import { homedir as homedir5 } from "os";
861
863
  import { exec } from "child_process";
862
864
  import { promisify as promisify2 } from "util";
@@ -1203,9 +1205,57 @@ var EnvironmentDetailsService = class {
1203
1205
  };
1204
1206
  var environmentDetailsService = new EnvironmentDetailsService();
1205
1207
 
1208
+ // src/services/start-hook-logs-service.ts
1209
+ import { createHash } from "crypto";
1210
+ import { mkdir as mkdir4, readFile as readFile3, writeFile as writeFile4, readdir as readdir2 } from "fs/promises";
1211
+ import { join as join6 } from "path";
1212
+ var LOGS_DIR = join6(SANDBOX_PATHS.REPLICAS_DIR, "start-hook-logs");
1213
+ function sanitizeFilename(name) {
1214
+ const safe = name.replace(/[^a-zA-Z0-9._-]/g, "_");
1215
+ const hash = createHash("sha256").update(name).digest("hex").slice(0, 8);
1216
+ return `${safe}-${hash}`;
1217
+ }
1218
+ var StartHookLogsService = class {
1219
+ async ensureDir() {
1220
+ await mkdir4(LOGS_DIR, { recursive: true });
1221
+ }
1222
+ async saveRepoLog(repoName, entry) {
1223
+ await this.ensureDir();
1224
+ const log = { repoName, ...entry };
1225
+ const filename = `repo-${sanitizeFilename(repoName)}.json`;
1226
+ await writeFile4(join6(LOGS_DIR, filename), `${JSON.stringify(log, null, 2)}
1227
+ `, "utf-8");
1228
+ }
1229
+ async getAllLogs() {
1230
+ let files;
1231
+ try {
1232
+ files = await readdir2(LOGS_DIR);
1233
+ } catch (err) {
1234
+ if (err.code === "ENOENT") {
1235
+ return [];
1236
+ }
1237
+ throw err;
1238
+ }
1239
+ const logs = [];
1240
+ for (const file of files) {
1241
+ if (!file.endsWith(".json")) {
1242
+ continue;
1243
+ }
1244
+ try {
1245
+ const raw = await readFile3(join6(LOGS_DIR, file), "utf-8");
1246
+ logs.push(JSON.parse(raw));
1247
+ } catch {
1248
+ }
1249
+ }
1250
+ logs.sort((a, b) => a.repoName.localeCompare(b.repoName));
1251
+ return logs;
1252
+ }
1253
+ };
1254
+ var startHookLogsService = new StartHookLogsService();
1255
+
1206
1256
  // src/services/replicas-config-service.ts
1207
1257
  var execAsync = promisify2(exec);
1208
- var START_HOOKS_LOG = join6(homedir5(), ".replicas", "startHooks.log");
1258
+ var START_HOOKS_LOG = join7(homedir5(), ".replicas", "startHooks.log");
1209
1259
  var START_HOOKS_RUNNING_PROMPT = `IMPORTANT - Start Hooks Running:
1210
1260
  Start hooks are shell commands/scripts set by repository owners that run on workspace startup.
1211
1261
  These hooks are currently executing in the background. You can:
@@ -1216,11 +1266,11 @@ The start hooks may install dependencies, build projects, or perform other setup
1216
1266
  If your task depends on setup being complete, check the log file before proceeding.`;
1217
1267
  async function readReplicasConfigFromDir(dirPath) {
1218
1268
  for (const filename of REPLICAS_CONFIG_FILENAMES) {
1219
- const configPath = join6(dirPath, filename);
1269
+ const configPath = join7(dirPath, filename);
1220
1270
  if (!existsSync4(configPath)) {
1221
1271
  continue;
1222
1272
  }
1223
- const data = await readFile3(configPath, "utf-8");
1273
+ const data = await readFile4(configPath, "utf-8");
1224
1274
  const config = parseReplicasConfigString(data, filename);
1225
1275
  return { config, filename };
1226
1276
  }
@@ -1280,7 +1330,7 @@ var ReplicasConfigService = class {
1280
1330
  const logLine = `[${timestamp}] ${message}
1281
1331
  `;
1282
1332
  try {
1283
- await mkdir4(join6(homedir5(), ".replicas"), { recursive: true });
1333
+ await mkdir5(join7(homedir5(), ".replicas"), { recursive: true });
1284
1334
  await appendFile2(START_HOOKS_LOG, logLine, "utf-8");
1285
1335
  } catch (error) {
1286
1336
  console.error("Failed to write to start hooks log:", error);
@@ -1304,8 +1354,8 @@ var ReplicasConfigService = class {
1304
1354
  this.hooksRunning = true;
1305
1355
  this.hooksCompleted = false;
1306
1356
  try {
1307
- await mkdir4(join6(homedir5(), ".replicas"), { recursive: true });
1308
- await writeFile4(
1357
+ await mkdir5(join7(homedir5(), ".replicas"), { recursive: true });
1358
+ await writeFile5(
1309
1359
  START_HOOKS_LOG,
1310
1360
  `=== Start Hooks Execution Log ===
1311
1361
  Started: ${(/* @__PURE__ */ new Date()).toISOString()}
@@ -1334,6 +1384,9 @@ Repositories: ${hookEntries.length}
1334
1384
  }
1335
1385
  const timeout = startHookConfig.timeout ?? 3e5;
1336
1386
  let repoFailed = false;
1387
+ let lastExitCode = 0;
1388
+ let repoTimedOut = false;
1389
+ const repoOutput = [];
1337
1390
  await this.logToFile(`[${entry.repoName}] Executing ${startHookConfig.commands.length} hook(s) with timeout ${timeout}ms`);
1338
1391
  for (const hook of startHookConfig.commands) {
1339
1392
  try {
@@ -1344,20 +1397,35 @@ Repositories: ${hookEntries.length}
1344
1397
  env: process.env
1345
1398
  });
1346
1399
  if (stdout) {
1400
+ repoOutput.push(stdout);
1347
1401
  await this.logToFile(`[${entry.repoName}] [stdout] ${stdout}`);
1348
1402
  }
1349
1403
  if (stderr) {
1404
+ repoOutput.push(stderr);
1350
1405
  await this.logToFile(`[${entry.repoName}] [stderr] ${stderr}`);
1351
1406
  }
1352
1407
  await this.logToFile(`[${entry.repoName}] --- Completed: ${hook} ---`);
1353
1408
  } catch (error) {
1354
1409
  const errorMessage = error instanceof Error ? error.message : "Unknown error";
1410
+ const execError = error;
1411
+ lastExitCode = execError.code ?? 1;
1412
+ if (execError.killed) {
1413
+ repoTimedOut = true;
1414
+ }
1355
1415
  this.hooksFailed = true;
1356
1416
  repoFailed = true;
1417
+ repoOutput.push(`[ERROR] ${hook} failed: ${errorMessage}`);
1357
1418
  await this.logToFile(`[${entry.repoName}] [ERROR] ${hook} failed: ${errorMessage}`);
1358
1419
  await environmentDetailsService.setRepositoryStartHook(entry.repoName, "no");
1359
1420
  }
1360
1421
  }
1422
+ await startHookLogsService.saveRepoLog(entry.repoName, {
1423
+ hookCommands: startHookConfig.commands,
1424
+ output: repoOutput.join("\n"),
1425
+ exitCode: lastExitCode,
1426
+ timedOut: repoTimedOut,
1427
+ executedAt: (/* @__PURE__ */ new Date()).toISOString()
1428
+ });
1361
1429
  const fallbackRepoState = persistedRepoState ?? {
1362
1430
  name: entry.repoName,
1363
1431
  path: entry.workingDirectory,
@@ -1426,17 +1494,17 @@ Repositories: ${hookEntries.length}
1426
1494
  var replicasConfigService = new ReplicasConfigService();
1427
1495
 
1428
1496
  // src/services/event-service.ts
1429
- import { appendFile as appendFile3, mkdir as mkdir5 } from "fs/promises";
1497
+ import { appendFile as appendFile3, mkdir as mkdir6 } from "fs/promises";
1430
1498
  import { homedir as homedir6 } from "os";
1431
- import { join as join7 } from "path";
1499
+ import { join as join8 } from "path";
1432
1500
  import { randomUUID } from "crypto";
1433
- var ENGINE_DIR = join7(homedir6(), ".replicas", "engine");
1434
- var EVENTS_FILE = join7(ENGINE_DIR, "events.jsonl");
1501
+ var ENGINE_DIR = join8(homedir6(), ".replicas", "engine");
1502
+ var EVENTS_FILE = join8(ENGINE_DIR, "events.jsonl");
1435
1503
  var EventService = class {
1436
1504
  subscribers = /* @__PURE__ */ new Map();
1437
1505
  writeChain = Promise.resolve();
1438
1506
  async initialize() {
1439
- await mkdir5(ENGINE_DIR, { recursive: true });
1507
+ await mkdir6(ENGINE_DIR, { recursive: true });
1440
1508
  }
1441
1509
  subscribe(subscriber) {
1442
1510
  const id = randomUUID();
@@ -1466,7 +1534,7 @@ var EventService = class {
1466
1534
  var eventService = new EventService();
1467
1535
 
1468
1536
  // src/services/preview-service.ts
1469
- import { mkdir as mkdir6, readFile as readFile4, writeFile as writeFile5 } from "fs/promises";
1537
+ import { mkdir as mkdir7, readFile as readFile5, writeFile as writeFile6 } from "fs/promises";
1470
1538
  import { existsSync as existsSync5 } from "fs";
1471
1539
  import { randomUUID as randomUUID2 } from "crypto";
1472
1540
  import { dirname } from "path";
@@ -1476,7 +1544,7 @@ async function readPreviewsFile() {
1476
1544
  if (!existsSync5(PREVIEW_PORTS_FILE)) {
1477
1545
  return { previews: [] };
1478
1546
  }
1479
- const raw = await readFile4(PREVIEW_PORTS_FILE, "utf-8");
1547
+ const raw = await readFile5(PREVIEW_PORTS_FILE, "utf-8");
1480
1548
  return JSON.parse(raw);
1481
1549
  } catch {
1482
1550
  return { previews: [] };
@@ -1484,8 +1552,8 @@ async function readPreviewsFile() {
1484
1552
  }
1485
1553
  async function writePreviewsFile(data) {
1486
1554
  const dir = dirname(PREVIEW_PORTS_FILE);
1487
- await mkdir6(dir, { recursive: true });
1488
- await writeFile5(PREVIEW_PORTS_FILE, `${JSON.stringify(data, null, 2)}
1555
+ await mkdir7(dir, { recursive: true });
1556
+ await writeFile6(PREVIEW_PORTS_FILE, `${JSON.stringify(data, null, 2)}
1489
1557
  `, "utf-8");
1490
1558
  }
1491
1559
  var PreviewService = class {
@@ -1521,21 +1589,21 @@ var PreviewService = class {
1521
1589
  var previewService = new PreviewService();
1522
1590
 
1523
1591
  // src/services/chat/chat-service.ts
1524
- import { mkdir as mkdir9, readFile as readFile7, rm, writeFile as writeFile7 } from "fs/promises";
1592
+ import { mkdir as mkdir10, readFile as readFile8, rm, writeFile as writeFile8 } from "fs/promises";
1525
1593
  import { homedir as homedir9 } from "os";
1526
- import { join as join10 } from "path";
1594
+ import { join as join11 } from "path";
1527
1595
  import { randomUUID as randomUUID4 } from "crypto";
1528
1596
 
1529
1597
  // src/managers/claude-manager.ts
1530
1598
  import {
1531
1599
  query
1532
1600
  } from "@anthropic-ai/claude-agent-sdk";
1533
- import { join as join8 } from "path";
1534
- import { mkdir as mkdir7, appendFile as appendFile4 } from "fs/promises";
1601
+ import { join as join9 } from "path";
1602
+ import { mkdir as mkdir8, appendFile as appendFile4 } from "fs/promises";
1535
1603
  import { homedir as homedir7 } from "os";
1536
1604
 
1537
1605
  // src/utils/jsonl-reader.ts
1538
- import { readFile as readFile5 } from "fs/promises";
1606
+ import { readFile as readFile6 } from "fs/promises";
1539
1607
  function isJsonlEvent(value) {
1540
1608
  if (!isRecord(value)) {
1541
1609
  return false;
@@ -1557,7 +1625,7 @@ function parseJsonlEvents(lines) {
1557
1625
  }
1558
1626
  async function readJSONL(filePath) {
1559
1627
  try {
1560
- const content = await readFile5(filePath, "utf-8");
1628
+ const content = await readFile6(filePath, "utf-8");
1561
1629
  const lines = content.split("\n").filter((line) => line.trim());
1562
1630
  return parseJsonlEvents(lines);
1563
1631
  } catch (error) {
@@ -2276,8 +2344,8 @@ var PromptStream = class {
2276
2344
  if (this.closed) {
2277
2345
  return Promise.resolve({ value: void 0, done: true });
2278
2346
  }
2279
- return new Promise((resolve) => {
2280
- this.waiters.push(resolve);
2347
+ return new Promise((resolve2) => {
2348
+ this.waiters.push(resolve2);
2281
2349
  });
2282
2350
  }
2283
2351
  };
@@ -2294,7 +2362,7 @@ var ClaudeManager = class _ClaudeManager extends CodingAgentManager {
2294
2362
  pendingInterrupt = false;
2295
2363
  constructor(options) {
2296
2364
  super(options);
2297
- this.historyFile = options.historyFilePath ?? join8(homedir7(), ".replicas", "claude", "history.jsonl");
2365
+ this.historyFile = options.historyFilePath ?? join9(homedir7(), ".replicas", "claude", "history.jsonl");
2298
2366
  this.initializeManager(this.processMessageInternal.bind(this));
2299
2367
  }
2300
2368
  async interruptActiveTurn() {
@@ -2463,8 +2531,8 @@ var ClaudeManager = class _ClaudeManager extends CodingAgentManager {
2463
2531
  };
2464
2532
  }
2465
2533
  async initialize() {
2466
- const historyDir = join8(homedir7(), ".replicas", "claude");
2467
- await mkdir7(historyDir, { recursive: true });
2534
+ const historyDir = join9(homedir7(), ".replicas", "claude");
2535
+ await mkdir8(historyDir, { recursive: true });
2468
2536
  if (this.initialSessionId) {
2469
2537
  this.sessionId = this.initialSessionId;
2470
2538
  console.log(`[ClaudeManager] Restored session ID from persisted state: ${this.sessionId}`);
@@ -2515,13 +2583,13 @@ var ClaudeManager = class _ClaudeManager extends CodingAgentManager {
2515
2583
  // src/managers/codex-manager.ts
2516
2584
  import { Codex } from "@openai/codex-sdk";
2517
2585
  import { randomUUID as randomUUID3 } from "crypto";
2518
- import { readdir as readdir2, stat as stat2, writeFile as writeFile6, mkdir as mkdir8, readFile as readFile6 } from "fs/promises";
2586
+ import { readdir as readdir3, stat as stat2, writeFile as writeFile7, mkdir as mkdir9, readFile as readFile7 } from "fs/promises";
2519
2587
  import { existsSync as existsSync6 } from "fs";
2520
- import { join as join9 } from "path";
2588
+ import { join as join10 } from "path";
2521
2589
  import { homedir as homedir8 } from "os";
2522
2590
  import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
2523
2591
  var DEFAULT_MODEL = "gpt-5.4";
2524
- var CODEX_CONFIG_PATH = join9(homedir8(), ".codex", "config.toml");
2592
+ var CODEX_CONFIG_PATH = join10(homedir8(), ".codex", "config.toml");
2525
2593
  function isLinearThoughtEvent2(event) {
2526
2594
  return event.content.type === "thought";
2527
2595
  }
@@ -2532,7 +2600,7 @@ function isJsonlEvent2(value) {
2532
2600
  return typeof value.timestamp === "string" && typeof value.type === "string" && isRecord(value.payload);
2533
2601
  }
2534
2602
  function sleep(ms) {
2535
- return new Promise((resolve) => setTimeout(resolve, ms));
2603
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
2536
2604
  }
2537
2605
  var CodexManager = class extends CodingAgentManager {
2538
2606
  codex;
@@ -2543,7 +2611,7 @@ var CodexManager = class extends CodingAgentManager {
2543
2611
  constructor(options) {
2544
2612
  super(options);
2545
2613
  this.codex = new Codex();
2546
- this.tempImageDir = join9(homedir8(), ".replicas", "codex", "temp-images");
2614
+ this.tempImageDir = join10(homedir8(), ".replicas", "codex", "temp-images");
2547
2615
  this.initializeManager(this.processMessageInternal.bind(this));
2548
2616
  }
2549
2617
  async initialize() {
@@ -2563,12 +2631,12 @@ var CodexManager = class extends CodingAgentManager {
2563
2631
  */
2564
2632
  async updateCodexConfig(developerInstructions) {
2565
2633
  try {
2566
- const codexDir = join9(homedir8(), ".codex");
2567
- await mkdir8(codexDir, { recursive: true });
2634
+ const codexDir = join10(homedir8(), ".codex");
2635
+ await mkdir9(codexDir, { recursive: true });
2568
2636
  let config = {};
2569
2637
  if (existsSync6(CODEX_CONFIG_PATH)) {
2570
2638
  try {
2571
- const existingContent = await readFile6(CODEX_CONFIG_PATH, "utf-8");
2639
+ const existingContent = await readFile7(CODEX_CONFIG_PATH, "utf-8");
2572
2640
  const parsed = parseToml(existingContent);
2573
2641
  if (isRecord(parsed)) {
2574
2642
  config = parsed;
@@ -2583,7 +2651,7 @@ var CodexManager = class extends CodingAgentManager {
2583
2651
  delete config.developer_instructions;
2584
2652
  }
2585
2653
  const tomlContent = stringifyToml(config);
2586
- await writeFile6(CODEX_CONFIG_PATH, tomlContent, "utf-8");
2654
+ await writeFile7(CODEX_CONFIG_PATH, tomlContent, "utf-8");
2587
2655
  console.log("[CodexManager] Updated config.toml with developer_instructions");
2588
2656
  } catch (error) {
2589
2657
  console.error("[CodexManager] Failed to update config.toml:", error);
@@ -2594,14 +2662,14 @@ var CodexManager = class extends CodingAgentManager {
2594
2662
  * @returns Array of temp file paths
2595
2663
  */
2596
2664
  async saveImagesToTempFiles(images) {
2597
- await mkdir8(this.tempImageDir, { recursive: true });
2665
+ await mkdir9(this.tempImageDir, { recursive: true });
2598
2666
  const tempPaths = [];
2599
2667
  for (const image of images) {
2600
2668
  const ext = image.source.media_type.split("/")[1] || "png";
2601
2669
  const filename = `img_${randomUUID3()}.${ext}`;
2602
- const filepath = join9(this.tempImageDir, filename);
2670
+ const filepath = join10(this.tempImageDir, filename);
2603
2671
  const buffer = Buffer.from(image.source.data, "base64");
2604
- await writeFile6(filepath, buffer);
2672
+ await writeFile7(filepath, buffer);
2605
2673
  tempPaths.push(filepath);
2606
2674
  }
2607
2675
  return tempPaths;
@@ -2731,13 +2799,13 @@ var CodexManager = class extends CodingAgentManager {
2731
2799
  }
2732
2800
  // Helper methods for finding session files
2733
2801
  async findSessionFile(threadId) {
2734
- const sessionsDir = join9(homedir8(), ".codex", "sessions");
2802
+ const sessionsDir = join10(homedir8(), ".codex", "sessions");
2735
2803
  try {
2736
2804
  const now = /* @__PURE__ */ new Date();
2737
2805
  const year = now.getFullYear();
2738
2806
  const month = String(now.getMonth() + 1).padStart(2, "0");
2739
2807
  const day = String(now.getDate()).padStart(2, "0");
2740
- const todayDir = join9(sessionsDir, String(year), month, day);
2808
+ const todayDir = join10(sessionsDir, String(year), month, day);
2741
2809
  const file = await this.findFileInDirectory(todayDir, threadId);
2742
2810
  if (file) return file;
2743
2811
  for (let daysAgo = 1; daysAgo <= 7; daysAgo++) {
@@ -2746,7 +2814,7 @@ var CodexManager = class extends CodingAgentManager {
2746
2814
  const searchYear = date.getFullYear();
2747
2815
  const searchMonth = String(date.getMonth() + 1).padStart(2, "0");
2748
2816
  const searchDay = String(date.getDate()).padStart(2, "0");
2749
- const searchDir = join9(sessionsDir, String(searchYear), searchMonth, searchDay);
2817
+ const searchDir = join10(sessionsDir, String(searchYear), searchMonth, searchDay);
2750
2818
  const file2 = await this.findFileInDirectory(searchDir, threadId);
2751
2819
  if (file2) return file2;
2752
2820
  }
@@ -2757,10 +2825,10 @@ var CodexManager = class extends CodingAgentManager {
2757
2825
  }
2758
2826
  async findFileInDirectory(directory, threadId) {
2759
2827
  try {
2760
- const files = await readdir2(directory);
2828
+ const files = await readdir3(directory);
2761
2829
  for (const file of files) {
2762
2830
  if (file.endsWith(".jsonl") && file.includes(threadId)) {
2763
- const fullPath = join9(directory, file);
2831
+ const fullPath = join10(directory, file);
2764
2832
  const stats = await stat2(fullPath);
2765
2833
  if (stats.isFile()) {
2766
2834
  return fullPath;
@@ -2793,7 +2861,7 @@ var CodexManager = class extends CodingAgentManager {
2793
2861
  const seenLines = /* @__PURE__ */ new Set();
2794
2862
  const seedSeenLines = async () => {
2795
2863
  try {
2796
- const content = await readFile6(sessionFile, "utf-8");
2864
+ const content = await readFile7(sessionFile, "utf-8");
2797
2865
  const lines = content.split("\n").map((line) => line.trim()).filter(Boolean);
2798
2866
  for (const line of lines) {
2799
2867
  seenLines.add(line);
@@ -2805,7 +2873,7 @@ var CodexManager = class extends CodingAgentManager {
2805
2873
  const pump = async () => {
2806
2874
  let emitted = 0;
2807
2875
  try {
2808
- const content = await readFile6(sessionFile, "utf-8");
2876
+ const content = await readFile7(sessionFile, "utf-8");
2809
2877
  const lines = content.split("\n");
2810
2878
  const completeLines = content.endsWith("\n") ? lines : lines.slice(0, -1);
2811
2879
  for (const line of completeLines) {
@@ -2876,9 +2944,9 @@ var DuplicateDefaultChatError = class extends Error {
2876
2944
  };
2877
2945
 
2878
2946
  // src/services/chat/chat-service.ts
2879
- var ENGINE_DIR2 = join10(homedir9(), ".replicas", "engine");
2880
- var CHATS_FILE = join10(ENGINE_DIR2, "chats.json");
2881
- var CLAUDE_HISTORY_DIR = join10(ENGINE_DIR2, "claude-histories");
2947
+ var ENGINE_DIR2 = join11(homedir9(), ".replicas", "engine");
2948
+ var CHATS_FILE = join11(ENGINE_DIR2, "chats.json");
2949
+ var CLAUDE_HISTORY_DIR = join11(ENGINE_DIR2, "claude-histories");
2882
2950
  function isPersistedChat(value) {
2883
2951
  if (!isRecord(value)) {
2884
2952
  return false;
@@ -2893,8 +2961,8 @@ var ChatService = class {
2893
2961
  chats = /* @__PURE__ */ new Map();
2894
2962
  writeChain = Promise.resolve();
2895
2963
  async initialize() {
2896
- await mkdir9(ENGINE_DIR2, { recursive: true });
2897
- await mkdir9(CLAUDE_HISTORY_DIR, { recursive: true });
2964
+ await mkdir10(ENGINE_DIR2, { recursive: true });
2965
+ await mkdir10(CLAUDE_HISTORY_DIR, { recursive: true });
2898
2966
  const persisted = await this.loadChats();
2899
2967
  for (const chat of persisted) {
2900
2968
  const runtime = this.createRuntimeChat(chat);
@@ -2990,7 +3058,7 @@ var ChatService = class {
2990
3058
  this.chats.delete(chatId);
2991
3059
  await this.persistAllChats();
2992
3060
  if (chat.persisted.provider === "claude") {
2993
- const historyFilePath = join10(CLAUDE_HISTORY_DIR, `${chatId}.jsonl`);
3061
+ const historyFilePath = join11(CLAUDE_HISTORY_DIR, `${chatId}.jsonl`);
2994
3062
  await rm(historyFilePath, { force: true });
2995
3063
  }
2996
3064
  await this.publish({
@@ -3033,7 +3101,7 @@ var ChatService = class {
3033
3101
  };
3034
3102
  const provider = persisted.provider === "claude" ? new ClaudeManager({
3035
3103
  workingDirectory: this.workingDirectory,
3036
- historyFilePath: join10(CLAUDE_HISTORY_DIR, `${persisted.id}.jsonl`),
3104
+ historyFilePath: join11(CLAUDE_HISTORY_DIR, `${persisted.id}.jsonl`),
3037
3105
  initialSessionId: persisted.providerSessionId,
3038
3106
  onSaveSessionId: saveSession,
3039
3107
  onTurnComplete: onProviderTurnComplete,
@@ -3129,7 +3197,7 @@ var ChatService = class {
3129
3197
  }
3130
3198
  async loadChats() {
3131
3199
  try {
3132
- const content = await readFile7(CHATS_FILE, "utf-8");
3200
+ const content = await readFile8(CHATS_FILE, "utf-8");
3133
3201
  const parsed = JSON.parse(content);
3134
3202
  if (!Array.isArray(parsed)) {
3135
3203
  return [];
@@ -3146,7 +3214,7 @@ var ChatService = class {
3146
3214
  this.writeChain = this.writeChain.catch(() => {
3147
3215
  }).then(async () => {
3148
3216
  const payload = Array.from(this.chats.values()).map((chat) => chat.persisted);
3149
- await writeFile7(CHATS_FILE, JSON.stringify(payload, null, 2), "utf-8");
3217
+ await writeFile8(CHATS_FILE, JSON.stringify(payload, null, 2), "utf-8");
3150
3218
  });
3151
3219
  await this.writeChain;
3152
3220
  }
@@ -3174,14 +3242,16 @@ var ChatService = class {
3174
3242
  // src/v1-routes.ts
3175
3243
  import { Hono } from "hono";
3176
3244
  import { z } from "zod";
3245
+ import { readdir as readdir6, stat as stat3, readFile as readFile12 } from "fs/promises";
3246
+ import { join as join15, resolve } from "path";
3177
3247
 
3178
3248
  // src/services/plan-service.ts
3179
- import { readdir as readdir3, readFile as readFile8 } from "fs/promises";
3249
+ import { readdir as readdir4, readFile as readFile9 } from "fs/promises";
3180
3250
  import { homedir as homedir10 } from "os";
3181
- import { basename, join as join11 } from "path";
3251
+ import { basename, join as join12 } from "path";
3182
3252
  var PLAN_DIRECTORIES = [
3183
- join11(homedir10(), ".claude", "plans"),
3184
- join11(homedir10(), ".replicas", "plans")
3253
+ join12(homedir10(), ".claude", "plans"),
3254
+ join12(homedir10(), ".replicas", "plans")
3185
3255
  ];
3186
3256
  function isMarkdownFile(filename) {
3187
3257
  return filename.toLowerCase().endsWith(".md");
@@ -3194,7 +3264,7 @@ var PlanService = class {
3194
3264
  const planNames = /* @__PURE__ */ new Set();
3195
3265
  for (const directory of PLAN_DIRECTORIES) {
3196
3266
  try {
3197
- const entries = await readdir3(directory, { withFileTypes: true });
3267
+ const entries = await readdir4(directory, { withFileTypes: true });
3198
3268
  for (const entry of entries) {
3199
3269
  if (!entry.isFile()) {
3200
3270
  continue;
@@ -3215,9 +3285,9 @@ var PlanService = class {
3215
3285
  return null;
3216
3286
  }
3217
3287
  for (const directory of PLAN_DIRECTORIES) {
3218
- const filePath = join11(directory, safeFilename);
3288
+ const filePath = join12(directory, safeFilename);
3219
3289
  try {
3220
- const content = await readFile8(filePath, "utf-8");
3290
+ const content = await readFile9(filePath, "utf-8");
3221
3291
  return { filename: safeFilename, content };
3222
3292
  } catch {
3223
3293
  }
@@ -3230,9 +3300,78 @@ var planService = new PlanService();
3230
3300
  // src/services/warm-hooks-service.ts
3231
3301
  import { execFile as execFile2 } from "child_process";
3232
3302
  import { promisify as promisify3 } from "util";
3233
- import { readFile as readFile9 } from "fs/promises";
3303
+ import { readFile as readFile11 } from "fs/promises";
3234
3304
  import { existsSync as existsSync7 } from "fs";
3235
- import { join as join12 } from "path";
3305
+ import { join as join14 } from "path";
3306
+
3307
+ // src/services/warm-hook-logs-service.ts
3308
+ import { createHash as createHash2 } from "crypto";
3309
+ import { mkdir as mkdir11, readFile as readFile10, writeFile as writeFile9, readdir as readdir5 } from "fs/promises";
3310
+ import { join as join13 } from "path";
3311
+ var LOGS_DIR2 = join13(SANDBOX_PATHS.REPLICAS_DIR, "warm-hook-logs");
3312
+ function sanitizeFilename2(name) {
3313
+ const safe = name.replace(/[^a-zA-Z0-9._-]/g, "_");
3314
+ const hash = createHash2("sha256").update(name).digest("hex").slice(0, 8);
3315
+ return `${safe}-${hash}`;
3316
+ }
3317
+ var WarmHookLogsService = class {
3318
+ async ensureDir() {
3319
+ await mkdir11(LOGS_DIR2, { recursive: true });
3320
+ }
3321
+ async saveGlobalHookLog(entry) {
3322
+ await this.ensureDir();
3323
+ const log = {
3324
+ hookType: "global",
3325
+ hookName: "organization",
3326
+ ...entry
3327
+ };
3328
+ await writeFile9(join13(LOGS_DIR2, "global.json"), `${JSON.stringify(log, null, 2)}
3329
+ `, "utf-8");
3330
+ }
3331
+ async saveRepoHookLog(repoName, entry) {
3332
+ await this.ensureDir();
3333
+ const log = {
3334
+ hookType: "repository",
3335
+ hookName: repoName,
3336
+ ...entry
3337
+ };
3338
+ const filename = `repo-${sanitizeFilename2(repoName)}.json`;
3339
+ await writeFile9(join13(LOGS_DIR2, filename), `${JSON.stringify(log, null, 2)}
3340
+ `, "utf-8");
3341
+ }
3342
+ async getAllLogs() {
3343
+ let files;
3344
+ try {
3345
+ files = await readdir5(LOGS_DIR2);
3346
+ } catch (err) {
3347
+ if (err.code === "ENOENT") {
3348
+ return [];
3349
+ }
3350
+ throw err;
3351
+ }
3352
+ const logs = [];
3353
+ for (const file of files) {
3354
+ if (!file.endsWith(".json")) {
3355
+ continue;
3356
+ }
3357
+ try {
3358
+ const raw = await readFile10(join13(LOGS_DIR2, file), "utf-8");
3359
+ logs.push(JSON.parse(raw));
3360
+ } catch {
3361
+ }
3362
+ }
3363
+ logs.sort((a, b) => {
3364
+ if (a.hookType !== b.hookType) {
3365
+ return a.hookType === "global" ? -1 : 1;
3366
+ }
3367
+ return a.hookName.localeCompare(b.hookName);
3368
+ });
3369
+ return logs;
3370
+ }
3371
+ };
3372
+ var warmHookLogsService = new WarmHookLogsService();
3373
+
3374
+ // src/services/warm-hooks-service.ts
3236
3375
  var execFileAsync2 = promisify3(execFile2);
3237
3376
  async function executeHookScript(params) {
3238
3377
  const timeout = clampWarmHookTimeoutMs(params.timeoutMs);
@@ -3263,12 +3402,12 @@ async function executeHookScript(params) {
3263
3402
  }
3264
3403
  async function readRepoWarmHook(repoPath) {
3265
3404
  for (const filename of REPLICAS_CONFIG_FILENAMES) {
3266
- const configPath = join12(repoPath, filename);
3405
+ const configPath = join14(repoPath, filename);
3267
3406
  if (!existsSync7(configPath)) {
3268
3407
  continue;
3269
3408
  }
3270
3409
  try {
3271
- const raw = await readFile9(configPath, "utf-8");
3410
+ const raw = await readFile11(configPath, "utf-8");
3272
3411
  const config = parseReplicasConfigString(raw, filename);
3273
3412
  if (!config.warmHook) {
3274
3413
  return null;
@@ -3314,6 +3453,13 @@ async function runWarmHooks(params) {
3314
3453
  });
3315
3454
  outputBlocks.push(orgResult.output);
3316
3455
  await environmentDetailsService.setGlobalWarmHook(orgResult.exitCode === 0 ? "yes" : "no", orgResult.output);
3456
+ await warmHookLogsService.saveGlobalHookLog({
3457
+ hookScript: orgHook,
3458
+ output: orgResult.output,
3459
+ exitCode: orgResult.exitCode,
3460
+ timedOut: orgResult.timedOut,
3461
+ executedAt: (/* @__PURE__ */ new Date()).toISOString()
3462
+ });
3317
3463
  if (orgResult.exitCode !== 0) {
3318
3464
  return {
3319
3465
  exitCode: orgResult.exitCode,
@@ -3334,6 +3480,11 @@ async function runWarmHooks(params) {
3334
3480
  }
3335
3481
  }
3336
3482
  for (const repoHook of repoHooks) {
3483
+ const combinedScript = repoHook.commands.join("\n");
3484
+ const repoOutputBlocks = [];
3485
+ let repoFailed = false;
3486
+ let repoTimedOut = false;
3487
+ let repoExitCode = 0;
3337
3488
  for (const command of repoHook.commands) {
3338
3489
  const repoResult = await executeHookScript({
3339
3490
  label: `repo-warm-hook:${repoHook.repoName}`,
@@ -3342,18 +3493,32 @@ async function runWarmHooks(params) {
3342
3493
  timeoutMs: repoHook.timeoutMs
3343
3494
  });
3344
3495
  outputBlocks.push(repoResult.output);
3496
+ repoOutputBlocks.push(repoResult.output);
3345
3497
  await environmentDetailsService.setRepositoryWarmHook(
3346
3498
  repoHook.repoName,
3347
3499
  repoResult.exitCode === 0 ? "yes" : "no"
3348
3500
  );
3349
3501
  if (repoResult.exitCode !== 0) {
3350
- return {
3351
- exitCode: repoResult.exitCode,
3352
- output: outputBlocks.join("\n\n"),
3353
- timedOut: repoResult.timedOut
3354
- };
3502
+ repoFailed = true;
3503
+ repoTimedOut = repoResult.timedOut;
3504
+ repoExitCode = repoResult.exitCode;
3505
+ break;
3355
3506
  }
3356
3507
  }
3508
+ await warmHookLogsService.saveRepoHookLog(repoHook.repoName, {
3509
+ hookScript: combinedScript,
3510
+ output: repoOutputBlocks.join("\n\n"),
3511
+ exitCode: repoFailed ? repoExitCode : 0,
3512
+ timedOut: repoTimedOut,
3513
+ executedAt: (/* @__PURE__ */ new Date()).toISOString()
3514
+ });
3515
+ if (repoFailed) {
3516
+ return {
3517
+ exitCode: repoExitCode,
3518
+ output: outputBlocks.join("\n\n"),
3519
+ timedOut: repoTimedOut
3520
+ };
3521
+ }
3357
3522
  }
3358
3523
  } else {
3359
3524
  const repos = await gitService.listRepositories();
@@ -3651,6 +3816,28 @@ function createV1Routes(deps) {
3651
3816
  );
3652
3817
  }
3653
3818
  });
3819
+ app2.get("/warm-hooks/logs", async (c) => {
3820
+ try {
3821
+ const logs = await warmHookLogsService.getAllLogs();
3822
+ return c.json({ logs });
3823
+ } catch (error) {
3824
+ return c.json(
3825
+ jsonError("Failed to read warm hook logs", error instanceof Error ? error.message : "Unknown error"),
3826
+ 500
3827
+ );
3828
+ }
3829
+ });
3830
+ app2.get("/start-hooks/logs", async (c) => {
3831
+ try {
3832
+ const logs = await startHookLogsService.getAllLogs();
3833
+ return c.json({ logs });
3834
+ } catch (error) {
3835
+ return c.json(
3836
+ jsonError("Failed to read start hook logs", error instanceof Error ? error.message : "Unknown error"),
3837
+ 500
3838
+ );
3839
+ }
3840
+ });
3654
3841
  app2.post("/workspace-name", async (c) => {
3655
3842
  try {
3656
3843
  const body = setWorkspaceNameSchema.parse(await c.req.json());
@@ -3688,6 +3875,75 @@ function createV1Routes(deps) {
3688
3875
  return c.json(jsonError("Failed to create preview", details), 400);
3689
3876
  }
3690
3877
  });
3878
+ app2.get("/logs", async (c) => {
3879
+ try {
3880
+ const files = await readdir6(LOG_DIR).catch(() => []);
3881
+ const logFiles = files.filter((f) => f.endsWith(".log"));
3882
+ const sessions = await Promise.all(
3883
+ logFiles.map(async (filename) => {
3884
+ const filePath = join15(LOG_DIR, filename);
3885
+ const fileStat = await stat3(filePath);
3886
+ const sessionId = filename.replace(/\.log$/, "");
3887
+ return {
3888
+ sessionId,
3889
+ filename,
3890
+ sizeBytes: fileStat.size,
3891
+ updatedAt: fileStat.mtime.toISOString()
3892
+ };
3893
+ })
3894
+ );
3895
+ sessions.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
3896
+ return c.json({
3897
+ currentSessionId: engineLogger.sessionId,
3898
+ sessions
3899
+ });
3900
+ } catch (error) {
3901
+ return c.json(
3902
+ jsonError("Failed to list logs", error instanceof Error ? error.message : "Unknown error"),
3903
+ 500
3904
+ );
3905
+ }
3906
+ });
3907
+ app2.get("/logs/:sessionId", async (c) => {
3908
+ try {
3909
+ const sessionId = c.req.param("sessionId");
3910
+ if (!sessionId || /[/\\]/.test(sessionId) || sessionId.includes("..")) {
3911
+ return c.json(jsonError("Invalid session ID"), 400);
3912
+ }
3913
+ const filePath = resolve(LOG_DIR, `${sessionId}.log`);
3914
+ if (!filePath.startsWith(resolve(LOG_DIR))) {
3915
+ return c.json(jsonError("Invalid session ID"), 400);
3916
+ }
3917
+ const offset = parseInt(c.req.query("offset") || "0", 10);
3918
+ const limit = Math.min(parseInt(c.req.query("limit") || "500", 10), 5e3);
3919
+ let content;
3920
+ try {
3921
+ content = await readFile12(filePath, "utf-8");
3922
+ } catch {
3923
+ return c.json(jsonError("Log session not found"), 404);
3924
+ }
3925
+ const allLines = content.split("\n");
3926
+ if (allLines.length > 0 && allLines[allLines.length - 1] === "") {
3927
+ allLines.pop();
3928
+ }
3929
+ const totalLines = allLines.length;
3930
+ const slicedLines = allLines.slice(offset, offset + limit);
3931
+ const hasMore = offset + limit < totalLines;
3932
+ return c.json({
3933
+ sessionId,
3934
+ totalLines,
3935
+ offset,
3936
+ limit,
3937
+ hasMore,
3938
+ lines: slicedLines
3939
+ });
3940
+ } catch (error) {
3941
+ return c.json(
3942
+ jsonError("Failed to read log", error instanceof Error ? error.message : "Unknown error"),
3943
+ 500
3944
+ );
3945
+ }
3946
+ });
3691
3947
  return app2;
3692
3948
  }
3693
3949
 
@@ -3728,7 +3984,7 @@ app.get("/health", async (c) => {
3728
3984
  return c.json({ status: "initializing", timestamp: (/* @__PURE__ */ new Date()).toISOString() }, 503);
3729
3985
  }
3730
3986
  try {
3731
- const logContent = await readFile10("/var/log/cloud-init-output.log", "utf-8");
3987
+ const logContent = await readFile13("/var/log/cloud-init-output.log", "utf-8");
3732
3988
  let status;
3733
3989
  if (logContent.includes(COMPLETION_MESSAGE)) {
3734
3990
  status = "active";