replicas-engine 0.1.60 → 0.1.62

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 readFile12 } from "fs/promises";
8
7
  import { execSync } from "child_process";
9
8
  import { randomUUID as randomUUID5 } from "crypto";
10
9
 
@@ -78,7 +77,7 @@ var ENGINE_ENV = loadEngineEnv();
78
77
 
79
78
  // src/managers/base-refresh-manager.ts
80
79
  var BaseRefreshManager = class {
81
- constructor(managerName, intervalMs = 45 * 60 * 1e3) {
80
+ constructor(managerName, intervalMs = 15 * 60 * 1e3) {
82
81
  this.managerName = managerName;
83
82
  this.intervalMs = intervalMs;
84
83
  this.health = {
@@ -228,6 +227,28 @@ var ClaudeTokenManager = class extends BaseRefreshManager {
228
227
  await this.updateClaudeCredentials(data);
229
228
  console.log(`[ClaudeTokenManager] Credentials refreshed successfully, expires at ${data.expiresAt}`);
230
229
  }
230
+ /**
231
+ * Re-fetches credentials from the monolith after an auth failure is detected.
232
+ * With the 1-hour refresh threshold, the monolith will have already
233
+ * proactively refreshed the token, so this just pulls the latest.
234
+ *
235
+ * Returns true if credentials were successfully updated, false otherwise.
236
+ */
237
+ async fetchFreshCredentials() {
238
+ if (this.getSkipReason()) {
239
+ return false;
240
+ }
241
+ const config = this.getRuntimeConfig();
242
+ if (!config) return false;
243
+ try {
244
+ console.log("[ClaudeTokenManager] Fetching fresh credentials from monolith after auth failure...");
245
+ await this.doRefresh(config);
246
+ return true;
247
+ } catch (error) {
248
+ console.error("[ClaudeTokenManager] Failed to fetch fresh credentials:", error);
249
+ return false;
250
+ }
251
+ }
231
252
  async updateClaudeCredentials(credentials) {
232
253
  const workspaceHome = ENGINE_ENV.HOME_DIR;
233
254
  const claudeDir = path2.join(workspaceHome, ".claude");
@@ -832,13 +853,63 @@ var EngineLogger = class {
832
853
  var engineLogger = new EngineLogger();
833
854
 
834
855
  // src/services/replicas-config-service.ts
835
- import { readFile as readFile3, appendFile as appendFile2, writeFile as writeFile4, mkdir as mkdir4 } from "fs/promises";
856
+ import { readFile as readFile4, appendFile as appendFile2, writeFile as writeFile5, mkdir as mkdir5 } from "fs/promises";
836
857
  import { existsSync as existsSync4 } from "fs";
837
- import { join as join6 } from "path";
858
+ import { join as join7 } from "path";
838
859
  import { homedir as homedir5 } from "os";
839
860
  import { exec } from "child_process";
840
861
  import { promisify as promisify2 } from "util";
841
862
 
863
+ // ../shared/src/pricing.ts
864
+ var PLANS = {
865
+ hobby: {
866
+ id: "hobby",
867
+ name: "Hobby",
868
+ monthlyPrice: 0,
869
+ seatPriceCents: 0,
870
+ creditsIncluded: 20,
871
+ features: ["20 hours of usage"]
872
+ },
873
+ developer: {
874
+ id: "developer",
875
+ name: "Developer",
876
+ monthlyPrice: 30,
877
+ seatPriceCents: 3e3,
878
+ creditsIncluded: 0,
879
+ features: ["Unlimited usage", "API access ($1/hr)"]
880
+ },
881
+ team: {
882
+ id: "team",
883
+ name: "Team",
884
+ monthlyPrice: 120,
885
+ seatPriceCents: 12e3,
886
+ creditsIncluded: 0,
887
+ features: [
888
+ "Unlimited usage",
889
+ "API access",
890
+ "Higher API rate limits",
891
+ "Warm hooks and pool access",
892
+ "Optional add-ons for higher resources, warm pools, rate limits, and SOC 2"
893
+ ]
894
+ },
895
+ enterprise: {
896
+ id: "enterprise",
897
+ name: "Enterprise",
898
+ monthlyPrice: 0,
899
+ seatPriceCents: 0,
900
+ creditsIncluded: 0,
901
+ features: [
902
+ "Unlimited usage",
903
+ "Custom API rates",
904
+ "Custom rate limits",
905
+ "Custom warm hooks and pools",
906
+ "SOC 2"
907
+ ]
908
+ }
909
+ };
910
+ var TEAM_PLAN = PLANS.team;
911
+ var ENTERPRISE_PLAN = PLANS.enterprise;
912
+
842
913
  // ../shared/src/sandbox.ts
843
914
  var SANDBOX_LIFECYCLE = {
844
915
  AUTO_STOP_MINUTES: 60,
@@ -1131,9 +1202,57 @@ var EnvironmentDetailsService = class {
1131
1202
  };
1132
1203
  var environmentDetailsService = new EnvironmentDetailsService();
1133
1204
 
1205
+ // src/services/start-hook-logs-service.ts
1206
+ import { createHash } from "crypto";
1207
+ import { mkdir as mkdir4, readFile as readFile3, writeFile as writeFile4, readdir as readdir2 } from "fs/promises";
1208
+ import { join as join6 } from "path";
1209
+ var LOGS_DIR = join6(SANDBOX_PATHS.REPLICAS_DIR, "start-hook-logs");
1210
+ function sanitizeFilename(name) {
1211
+ const safe = name.replace(/[^a-zA-Z0-9._-]/g, "_");
1212
+ const hash = createHash("sha256").update(name).digest("hex").slice(0, 8);
1213
+ return `${safe}-${hash}`;
1214
+ }
1215
+ var StartHookLogsService = class {
1216
+ async ensureDir() {
1217
+ await mkdir4(LOGS_DIR, { recursive: true });
1218
+ }
1219
+ async saveRepoLog(repoName, entry) {
1220
+ await this.ensureDir();
1221
+ const log = { repoName, ...entry };
1222
+ const filename = `repo-${sanitizeFilename(repoName)}.json`;
1223
+ await writeFile4(join6(LOGS_DIR, filename), `${JSON.stringify(log, null, 2)}
1224
+ `, "utf-8");
1225
+ }
1226
+ async getAllLogs() {
1227
+ let files;
1228
+ try {
1229
+ files = await readdir2(LOGS_DIR);
1230
+ } catch (err) {
1231
+ if (err.code === "ENOENT") {
1232
+ return [];
1233
+ }
1234
+ throw err;
1235
+ }
1236
+ const logs = [];
1237
+ for (const file of files) {
1238
+ if (!file.endsWith(".json")) {
1239
+ continue;
1240
+ }
1241
+ try {
1242
+ const raw = await readFile3(join6(LOGS_DIR, file), "utf-8");
1243
+ logs.push(JSON.parse(raw));
1244
+ } catch {
1245
+ }
1246
+ }
1247
+ logs.sort((a, b) => a.repoName.localeCompare(b.repoName));
1248
+ return logs;
1249
+ }
1250
+ };
1251
+ var startHookLogsService = new StartHookLogsService();
1252
+
1134
1253
  // src/services/replicas-config-service.ts
1135
1254
  var execAsync = promisify2(exec);
1136
- var START_HOOKS_LOG = join6(homedir5(), ".replicas", "startHooks.log");
1255
+ var START_HOOKS_LOG = join7(homedir5(), ".replicas", "startHooks.log");
1137
1256
  var START_HOOKS_RUNNING_PROMPT = `IMPORTANT - Start Hooks Running:
1138
1257
  Start hooks are shell commands/scripts set by repository owners that run on workspace startup.
1139
1258
  These hooks are currently executing in the background. You can:
@@ -1144,11 +1263,11 @@ The start hooks may install dependencies, build projects, or perform other setup
1144
1263
  If your task depends on setup being complete, check the log file before proceeding.`;
1145
1264
  async function readReplicasConfigFromDir(dirPath) {
1146
1265
  for (const filename of REPLICAS_CONFIG_FILENAMES) {
1147
- const configPath = join6(dirPath, filename);
1266
+ const configPath = join7(dirPath, filename);
1148
1267
  if (!existsSync4(configPath)) {
1149
1268
  continue;
1150
1269
  }
1151
- const data = await readFile3(configPath, "utf-8");
1270
+ const data = await readFile4(configPath, "utf-8");
1152
1271
  const config = parseReplicasConfigString(data, filename);
1153
1272
  return { config, filename };
1154
1273
  }
@@ -1208,7 +1327,7 @@ var ReplicasConfigService = class {
1208
1327
  const logLine = `[${timestamp}] ${message}
1209
1328
  `;
1210
1329
  try {
1211
- await mkdir4(join6(homedir5(), ".replicas"), { recursive: true });
1330
+ await mkdir5(join7(homedir5(), ".replicas"), { recursive: true });
1212
1331
  await appendFile2(START_HOOKS_LOG, logLine, "utf-8");
1213
1332
  } catch (error) {
1214
1333
  console.error("Failed to write to start hooks log:", error);
@@ -1232,8 +1351,8 @@ var ReplicasConfigService = class {
1232
1351
  this.hooksRunning = true;
1233
1352
  this.hooksCompleted = false;
1234
1353
  try {
1235
- await mkdir4(join6(homedir5(), ".replicas"), { recursive: true });
1236
- await writeFile4(
1354
+ await mkdir5(join7(homedir5(), ".replicas"), { recursive: true });
1355
+ await writeFile5(
1237
1356
  START_HOOKS_LOG,
1238
1357
  `=== Start Hooks Execution Log ===
1239
1358
  Started: ${(/* @__PURE__ */ new Date()).toISOString()}
@@ -1262,6 +1381,9 @@ Repositories: ${hookEntries.length}
1262
1381
  }
1263
1382
  const timeout = startHookConfig.timeout ?? 3e5;
1264
1383
  let repoFailed = false;
1384
+ let lastExitCode = 0;
1385
+ let repoTimedOut = false;
1386
+ const repoOutput = [];
1265
1387
  await this.logToFile(`[${entry.repoName}] Executing ${startHookConfig.commands.length} hook(s) with timeout ${timeout}ms`);
1266
1388
  for (const hook of startHookConfig.commands) {
1267
1389
  try {
@@ -1272,20 +1394,35 @@ Repositories: ${hookEntries.length}
1272
1394
  env: process.env
1273
1395
  });
1274
1396
  if (stdout) {
1397
+ repoOutput.push(stdout);
1275
1398
  await this.logToFile(`[${entry.repoName}] [stdout] ${stdout}`);
1276
1399
  }
1277
1400
  if (stderr) {
1401
+ repoOutput.push(stderr);
1278
1402
  await this.logToFile(`[${entry.repoName}] [stderr] ${stderr}`);
1279
1403
  }
1280
1404
  await this.logToFile(`[${entry.repoName}] --- Completed: ${hook} ---`);
1281
1405
  } catch (error) {
1282
1406
  const errorMessage = error instanceof Error ? error.message : "Unknown error";
1407
+ const execError = error;
1408
+ lastExitCode = execError.code ?? 1;
1409
+ if (execError.killed) {
1410
+ repoTimedOut = true;
1411
+ }
1283
1412
  this.hooksFailed = true;
1284
1413
  repoFailed = true;
1414
+ repoOutput.push(`[ERROR] ${hook} failed: ${errorMessage}`);
1285
1415
  await this.logToFile(`[${entry.repoName}] [ERROR] ${hook} failed: ${errorMessage}`);
1286
1416
  await environmentDetailsService.setRepositoryStartHook(entry.repoName, "no");
1287
1417
  }
1288
1418
  }
1419
+ await startHookLogsService.saveRepoLog(entry.repoName, {
1420
+ hookCommands: startHookConfig.commands,
1421
+ output: repoOutput.join("\n"),
1422
+ exitCode: lastExitCode,
1423
+ timedOut: repoTimedOut,
1424
+ executedAt: (/* @__PURE__ */ new Date()).toISOString()
1425
+ });
1289
1426
  const fallbackRepoState = persistedRepoState ?? {
1290
1427
  name: entry.repoName,
1291
1428
  path: entry.workingDirectory,
@@ -1354,17 +1491,17 @@ Repositories: ${hookEntries.length}
1354
1491
  var replicasConfigService = new ReplicasConfigService();
1355
1492
 
1356
1493
  // src/services/event-service.ts
1357
- import { appendFile as appendFile3, mkdir as mkdir5 } from "fs/promises";
1494
+ import { appendFile as appendFile3, mkdir as mkdir6 } from "fs/promises";
1358
1495
  import { homedir as homedir6 } from "os";
1359
- import { join as join7 } from "path";
1496
+ import { join as join8 } from "path";
1360
1497
  import { randomUUID } from "crypto";
1361
- var ENGINE_DIR = join7(homedir6(), ".replicas", "engine");
1362
- var EVENTS_FILE = join7(ENGINE_DIR, "events.jsonl");
1498
+ var ENGINE_DIR = join8(homedir6(), ".replicas", "engine");
1499
+ var EVENTS_FILE = join8(ENGINE_DIR, "events.jsonl");
1363
1500
  var EventService = class {
1364
1501
  subscribers = /* @__PURE__ */ new Map();
1365
1502
  writeChain = Promise.resolve();
1366
1503
  async initialize() {
1367
- await mkdir5(ENGINE_DIR, { recursive: true });
1504
+ await mkdir6(ENGINE_DIR, { recursive: true });
1368
1505
  }
1369
1506
  subscribe(subscriber) {
1370
1507
  const id = randomUUID();
@@ -1394,7 +1531,7 @@ var EventService = class {
1394
1531
  var eventService = new EventService();
1395
1532
 
1396
1533
  // src/services/preview-service.ts
1397
- import { mkdir as mkdir6, readFile as readFile4, writeFile as writeFile5 } from "fs/promises";
1534
+ import { mkdir as mkdir7, readFile as readFile5, writeFile as writeFile6 } from "fs/promises";
1398
1535
  import { existsSync as existsSync5 } from "fs";
1399
1536
  import { randomUUID as randomUUID2 } from "crypto";
1400
1537
  import { dirname } from "path";
@@ -1404,7 +1541,7 @@ async function readPreviewsFile() {
1404
1541
  if (!existsSync5(PREVIEW_PORTS_FILE)) {
1405
1542
  return { previews: [] };
1406
1543
  }
1407
- const raw = await readFile4(PREVIEW_PORTS_FILE, "utf-8");
1544
+ const raw = await readFile5(PREVIEW_PORTS_FILE, "utf-8");
1408
1545
  return JSON.parse(raw);
1409
1546
  } catch {
1410
1547
  return { previews: [] };
@@ -1412,8 +1549,8 @@ async function readPreviewsFile() {
1412
1549
  }
1413
1550
  async function writePreviewsFile(data) {
1414
1551
  const dir = dirname(PREVIEW_PORTS_FILE);
1415
- await mkdir6(dir, { recursive: true });
1416
- await writeFile5(PREVIEW_PORTS_FILE, `${JSON.stringify(data, null, 2)}
1552
+ await mkdir7(dir, { recursive: true });
1553
+ await writeFile6(PREVIEW_PORTS_FILE, `${JSON.stringify(data, null, 2)}
1417
1554
  `, "utf-8");
1418
1555
  }
1419
1556
  var PreviewService = class {
@@ -1449,21 +1586,21 @@ var PreviewService = class {
1449
1586
  var previewService = new PreviewService();
1450
1587
 
1451
1588
  // src/services/chat/chat-service.ts
1452
- import { mkdir as mkdir9, readFile as readFile7, rm, writeFile as writeFile7 } from "fs/promises";
1589
+ import { mkdir as mkdir10, readFile as readFile8, rm, writeFile as writeFile8 } from "fs/promises";
1453
1590
  import { homedir as homedir9 } from "os";
1454
- import { join as join10 } from "path";
1591
+ import { join as join11 } from "path";
1455
1592
  import { randomUUID as randomUUID4 } from "crypto";
1456
1593
 
1457
1594
  // src/managers/claude-manager.ts
1458
1595
  import {
1459
1596
  query
1460
1597
  } from "@anthropic-ai/claude-agent-sdk";
1461
- import { join as join8 } from "path";
1462
- import { mkdir as mkdir7, appendFile as appendFile4 } from "fs/promises";
1598
+ import { join as join9 } from "path";
1599
+ import { mkdir as mkdir8, appendFile as appendFile4 } from "fs/promises";
1463
1600
  import { homedir as homedir7 } from "os";
1464
1601
 
1465
1602
  // src/utils/jsonl-reader.ts
1466
- import { readFile as readFile5 } from "fs/promises";
1603
+ import { readFile as readFile6 } from "fs/promises";
1467
1604
  function isJsonlEvent(value) {
1468
1605
  if (!isRecord(value)) {
1469
1606
  return false;
@@ -1485,7 +1622,7 @@ function parseJsonlEvents(lines) {
1485
1622
  }
1486
1623
  async function readJSONL(filePath) {
1487
1624
  try {
1488
- const content = await readFile5(filePath, "utf-8");
1625
+ const content = await readFile6(filePath, "utf-8");
1489
1626
  const lines = content.split("\n").filter((line) => line.trim());
1490
1627
  return parseJsonlEvents(lines);
1491
1628
  } catch (error) {
@@ -2214,7 +2351,7 @@ var PromptStream = class {
2214
2351
  function isLinearThoughtEvent(event) {
2215
2352
  return event.content.type === "thought";
2216
2353
  }
2217
- var ClaudeManager = class extends CodingAgentManager {
2354
+ var ClaudeManager = class _ClaudeManager extends CodingAgentManager {
2218
2355
  historyFile;
2219
2356
  sessionId = null;
2220
2357
  activeQuery = null;
@@ -2222,7 +2359,7 @@ var ClaudeManager = class extends CodingAgentManager {
2222
2359
  pendingInterrupt = false;
2223
2360
  constructor(options) {
2224
2361
  super(options);
2225
- this.historyFile = options.historyFilePath ?? join8(homedir7(), ".replicas", "claude", "history.jsonl");
2362
+ this.historyFile = options.historyFilePath ?? join9(homedir7(), ".replicas", "claude", "history.jsonl");
2226
2363
  this.initializeManager(this.processMessageInternal.bind(this));
2227
2364
  }
2228
2365
  async interruptActiveTurn() {
@@ -2233,9 +2370,28 @@ var ClaudeManager = class extends CodingAgentManager {
2233
2370
  }
2234
2371
  }
2235
2372
  /**
2236
- * Internal method that actually processes the message
2373
+ * Internal method that actually processes the message.
2374
+ * On auth failure, refreshes credentials and retries once.
2237
2375
  */
2238
2376
  async processMessageInternal(request) {
2377
+ try {
2378
+ await this.executeQuery(request);
2379
+ } catch (error) {
2380
+ if (_ClaudeManager.isAuthError(error)) {
2381
+ console.warn("[ClaudeManager] Auth failure detected, refreshing credentials and retrying...", error);
2382
+ const refreshed = await claudeTokenManager.fetchFreshCredentials();
2383
+ if (refreshed) {
2384
+ await this.executeQuery(request);
2385
+ return;
2386
+ }
2387
+ }
2388
+ throw error;
2389
+ }
2390
+ }
2391
+ /**
2392
+ * Executes a single query attempt against the Claude Agent SDK.
2393
+ */
2394
+ async executeQuery(request) {
2239
2395
  const {
2240
2396
  message,
2241
2397
  model,
@@ -2351,6 +2507,18 @@ var ClaudeManager = class extends CodingAgentManager {
2351
2507
  await this.onTurnComplete();
2352
2508
  }
2353
2509
  }
2510
+ /**
2511
+ * Determines if an error is an OAuth authentication failure from the Claude SDK.
2512
+ * Known patterns:
2513
+ * - "Not logged in · Please run /login"
2514
+ * - "authentication_failed"
2515
+ * - "unauthorized"
2516
+ */
2517
+ static isAuthError(error) {
2518
+ const msg = error instanceof Error ? error.message : String(error);
2519
+ const lower = msg.toLowerCase();
2520
+ return lower.includes("not logged in") || lower.includes("please run /login") || lower.includes("authentication_failed") || lower.includes("authentication failed");
2521
+ }
2354
2522
  async getHistory() {
2355
2523
  await this.initialized;
2356
2524
  const events = await readJSONL(this.historyFile);
@@ -2360,19 +2528,41 @@ var ClaudeManager = class extends CodingAgentManager {
2360
2528
  };
2361
2529
  }
2362
2530
  async initialize() {
2363
- const historyDir = join8(homedir7(), ".replicas", "claude");
2364
- await mkdir7(historyDir, { recursive: true });
2531
+ const historyDir = join9(homedir7(), ".replicas", "claude");
2532
+ await mkdir8(historyDir, { recursive: true });
2365
2533
  if (this.initialSessionId) {
2366
2534
  this.sessionId = this.initialSessionId;
2367
2535
  console.log(`[ClaudeManager] Restored session ID from persisted state: ${this.sessionId}`);
2368
2536
  }
2369
2537
  }
2538
+ /**
2539
+ * Checks if an SDK message indicates an OAuth authentication failure.
2540
+ * When detected, triggers a force-refresh of credentials from the monolith.
2541
+ */
2542
+ async checkForAuthFailure(message) {
2543
+ if (message.type === "assistant" && "error" in message && message.error === "authentication_failed") {
2544
+ console.warn("[ClaudeManager] Detected authentication_failed in assistant message, force-refreshing credentials...");
2545
+ await claudeTokenManager.fetchFreshCredentials();
2546
+ return;
2547
+ }
2548
+ if (message.type === "result" && "is_error" in message && message.is_error && "errors" in message) {
2549
+ const errors = message.errors ?? [];
2550
+ const hasAuthError = errors.some(
2551
+ (err) => err.toLowerCase().includes("authentication") || err.toLowerCase().includes("unauthorized") || err.toLowerCase().includes("not logged in") || err.toLowerCase().includes("login")
2552
+ );
2553
+ if (hasAuthError) {
2554
+ console.warn("[ClaudeManager] Detected auth-related error in result, force-refreshing credentials...");
2555
+ await claudeTokenManager.fetchFreshCredentials();
2556
+ }
2557
+ }
2558
+ }
2370
2559
  async handleMessage(message) {
2371
2560
  if ("session_id" in message && message.session_id && !this.sessionId) {
2372
2561
  this.sessionId = message.session_id;
2373
2562
  await this.onSaveSessionId(this.sessionId);
2374
2563
  console.log(`[ClaudeManager] Captured and persisted session ID: ${this.sessionId}`);
2375
2564
  }
2565
+ await this.checkForAuthFailure(message);
2376
2566
  await this.recordEvent(message);
2377
2567
  }
2378
2568
  async recordEvent(event) {
@@ -2390,13 +2580,13 @@ var ClaudeManager = class extends CodingAgentManager {
2390
2580
  // src/managers/codex-manager.ts
2391
2581
  import { Codex } from "@openai/codex-sdk";
2392
2582
  import { randomUUID as randomUUID3 } from "crypto";
2393
- import { readdir as readdir2, stat as stat2, writeFile as writeFile6, mkdir as mkdir8, readFile as readFile6 } from "fs/promises";
2583
+ import { readdir as readdir3, stat as stat2, writeFile as writeFile7, mkdir as mkdir9, readFile as readFile7 } from "fs/promises";
2394
2584
  import { existsSync as existsSync6 } from "fs";
2395
- import { join as join9 } from "path";
2585
+ import { join as join10 } from "path";
2396
2586
  import { homedir as homedir8 } from "os";
2397
2587
  import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
2398
2588
  var DEFAULT_MODEL = "gpt-5.4";
2399
- var CODEX_CONFIG_PATH = join9(homedir8(), ".codex", "config.toml");
2589
+ var CODEX_CONFIG_PATH = join10(homedir8(), ".codex", "config.toml");
2400
2590
  function isLinearThoughtEvent2(event) {
2401
2591
  return event.content.type === "thought";
2402
2592
  }
@@ -2418,7 +2608,7 @@ var CodexManager = class extends CodingAgentManager {
2418
2608
  constructor(options) {
2419
2609
  super(options);
2420
2610
  this.codex = new Codex();
2421
- this.tempImageDir = join9(homedir8(), ".replicas", "codex", "temp-images");
2611
+ this.tempImageDir = join10(homedir8(), ".replicas", "codex", "temp-images");
2422
2612
  this.initializeManager(this.processMessageInternal.bind(this));
2423
2613
  }
2424
2614
  async initialize() {
@@ -2438,12 +2628,12 @@ var CodexManager = class extends CodingAgentManager {
2438
2628
  */
2439
2629
  async updateCodexConfig(developerInstructions) {
2440
2630
  try {
2441
- const codexDir = join9(homedir8(), ".codex");
2442
- await mkdir8(codexDir, { recursive: true });
2631
+ const codexDir = join10(homedir8(), ".codex");
2632
+ await mkdir9(codexDir, { recursive: true });
2443
2633
  let config = {};
2444
2634
  if (existsSync6(CODEX_CONFIG_PATH)) {
2445
2635
  try {
2446
- const existingContent = await readFile6(CODEX_CONFIG_PATH, "utf-8");
2636
+ const existingContent = await readFile7(CODEX_CONFIG_PATH, "utf-8");
2447
2637
  const parsed = parseToml(existingContent);
2448
2638
  if (isRecord(parsed)) {
2449
2639
  config = parsed;
@@ -2458,7 +2648,7 @@ var CodexManager = class extends CodingAgentManager {
2458
2648
  delete config.developer_instructions;
2459
2649
  }
2460
2650
  const tomlContent = stringifyToml(config);
2461
- await writeFile6(CODEX_CONFIG_PATH, tomlContent, "utf-8");
2651
+ await writeFile7(CODEX_CONFIG_PATH, tomlContent, "utf-8");
2462
2652
  console.log("[CodexManager] Updated config.toml with developer_instructions");
2463
2653
  } catch (error) {
2464
2654
  console.error("[CodexManager] Failed to update config.toml:", error);
@@ -2469,14 +2659,14 @@ var CodexManager = class extends CodingAgentManager {
2469
2659
  * @returns Array of temp file paths
2470
2660
  */
2471
2661
  async saveImagesToTempFiles(images) {
2472
- await mkdir8(this.tempImageDir, { recursive: true });
2662
+ await mkdir9(this.tempImageDir, { recursive: true });
2473
2663
  const tempPaths = [];
2474
2664
  for (const image of images) {
2475
2665
  const ext = image.source.media_type.split("/")[1] || "png";
2476
2666
  const filename = `img_${randomUUID3()}.${ext}`;
2477
- const filepath = join9(this.tempImageDir, filename);
2667
+ const filepath = join10(this.tempImageDir, filename);
2478
2668
  const buffer = Buffer.from(image.source.data, "base64");
2479
- await writeFile6(filepath, buffer);
2669
+ await writeFile7(filepath, buffer);
2480
2670
  tempPaths.push(filepath);
2481
2671
  }
2482
2672
  return tempPaths;
@@ -2606,13 +2796,13 @@ var CodexManager = class extends CodingAgentManager {
2606
2796
  }
2607
2797
  // Helper methods for finding session files
2608
2798
  async findSessionFile(threadId) {
2609
- const sessionsDir = join9(homedir8(), ".codex", "sessions");
2799
+ const sessionsDir = join10(homedir8(), ".codex", "sessions");
2610
2800
  try {
2611
2801
  const now = /* @__PURE__ */ new Date();
2612
2802
  const year = now.getFullYear();
2613
2803
  const month = String(now.getMonth() + 1).padStart(2, "0");
2614
2804
  const day = String(now.getDate()).padStart(2, "0");
2615
- const todayDir = join9(sessionsDir, String(year), month, day);
2805
+ const todayDir = join10(sessionsDir, String(year), month, day);
2616
2806
  const file = await this.findFileInDirectory(todayDir, threadId);
2617
2807
  if (file) return file;
2618
2808
  for (let daysAgo = 1; daysAgo <= 7; daysAgo++) {
@@ -2621,7 +2811,7 @@ var CodexManager = class extends CodingAgentManager {
2621
2811
  const searchYear = date.getFullYear();
2622
2812
  const searchMonth = String(date.getMonth() + 1).padStart(2, "0");
2623
2813
  const searchDay = String(date.getDate()).padStart(2, "0");
2624
- const searchDir = join9(sessionsDir, String(searchYear), searchMonth, searchDay);
2814
+ const searchDir = join10(sessionsDir, String(searchYear), searchMonth, searchDay);
2625
2815
  const file2 = await this.findFileInDirectory(searchDir, threadId);
2626
2816
  if (file2) return file2;
2627
2817
  }
@@ -2632,10 +2822,10 @@ var CodexManager = class extends CodingAgentManager {
2632
2822
  }
2633
2823
  async findFileInDirectory(directory, threadId) {
2634
2824
  try {
2635
- const files = await readdir2(directory);
2825
+ const files = await readdir3(directory);
2636
2826
  for (const file of files) {
2637
2827
  if (file.endsWith(".jsonl") && file.includes(threadId)) {
2638
- const fullPath = join9(directory, file);
2828
+ const fullPath = join10(directory, file);
2639
2829
  const stats = await stat2(fullPath);
2640
2830
  if (stats.isFile()) {
2641
2831
  return fullPath;
@@ -2668,7 +2858,7 @@ var CodexManager = class extends CodingAgentManager {
2668
2858
  const seenLines = /* @__PURE__ */ new Set();
2669
2859
  const seedSeenLines = async () => {
2670
2860
  try {
2671
- const content = await readFile6(sessionFile, "utf-8");
2861
+ const content = await readFile7(sessionFile, "utf-8");
2672
2862
  const lines = content.split("\n").map((line) => line.trim()).filter(Boolean);
2673
2863
  for (const line of lines) {
2674
2864
  seenLines.add(line);
@@ -2680,7 +2870,7 @@ var CodexManager = class extends CodingAgentManager {
2680
2870
  const pump = async () => {
2681
2871
  let emitted = 0;
2682
2872
  try {
2683
- const content = await readFile6(sessionFile, "utf-8");
2873
+ const content = await readFile7(sessionFile, "utf-8");
2684
2874
  const lines = content.split("\n");
2685
2875
  const completeLines = content.endsWith("\n") ? lines : lines.slice(0, -1);
2686
2876
  for (const line of completeLines) {
@@ -2751,9 +2941,9 @@ var DuplicateDefaultChatError = class extends Error {
2751
2941
  };
2752
2942
 
2753
2943
  // src/services/chat/chat-service.ts
2754
- var ENGINE_DIR2 = join10(homedir9(), ".replicas", "engine");
2755
- var CHATS_FILE = join10(ENGINE_DIR2, "chats.json");
2756
- var CLAUDE_HISTORY_DIR = join10(ENGINE_DIR2, "claude-histories");
2944
+ var ENGINE_DIR2 = join11(homedir9(), ".replicas", "engine");
2945
+ var CHATS_FILE = join11(ENGINE_DIR2, "chats.json");
2946
+ var CLAUDE_HISTORY_DIR = join11(ENGINE_DIR2, "claude-histories");
2757
2947
  function isPersistedChat(value) {
2758
2948
  if (!isRecord(value)) {
2759
2949
  return false;
@@ -2768,8 +2958,8 @@ var ChatService = class {
2768
2958
  chats = /* @__PURE__ */ new Map();
2769
2959
  writeChain = Promise.resolve();
2770
2960
  async initialize() {
2771
- await mkdir9(ENGINE_DIR2, { recursive: true });
2772
- await mkdir9(CLAUDE_HISTORY_DIR, { recursive: true });
2961
+ await mkdir10(ENGINE_DIR2, { recursive: true });
2962
+ await mkdir10(CLAUDE_HISTORY_DIR, { recursive: true });
2773
2963
  const persisted = await this.loadChats();
2774
2964
  for (const chat of persisted) {
2775
2965
  const runtime = this.createRuntimeChat(chat);
@@ -2865,7 +3055,7 @@ var ChatService = class {
2865
3055
  this.chats.delete(chatId);
2866
3056
  await this.persistAllChats();
2867
3057
  if (chat.persisted.provider === "claude") {
2868
- const historyFilePath = join10(CLAUDE_HISTORY_DIR, `${chatId}.jsonl`);
3058
+ const historyFilePath = join11(CLAUDE_HISTORY_DIR, `${chatId}.jsonl`);
2869
3059
  await rm(historyFilePath, { force: true });
2870
3060
  }
2871
3061
  await this.publish({
@@ -2908,7 +3098,7 @@ var ChatService = class {
2908
3098
  };
2909
3099
  const provider = persisted.provider === "claude" ? new ClaudeManager({
2910
3100
  workingDirectory: this.workingDirectory,
2911
- historyFilePath: join10(CLAUDE_HISTORY_DIR, `${persisted.id}.jsonl`),
3101
+ historyFilePath: join11(CLAUDE_HISTORY_DIR, `${persisted.id}.jsonl`),
2912
3102
  initialSessionId: persisted.providerSessionId,
2913
3103
  onSaveSessionId: saveSession,
2914
3104
  onTurnComplete: onProviderTurnComplete,
@@ -3004,7 +3194,7 @@ var ChatService = class {
3004
3194
  }
3005
3195
  async loadChats() {
3006
3196
  try {
3007
- const content = await readFile7(CHATS_FILE, "utf-8");
3197
+ const content = await readFile8(CHATS_FILE, "utf-8");
3008
3198
  const parsed = JSON.parse(content);
3009
3199
  if (!Array.isArray(parsed)) {
3010
3200
  return [];
@@ -3021,7 +3211,7 @@ var ChatService = class {
3021
3211
  this.writeChain = this.writeChain.catch(() => {
3022
3212
  }).then(async () => {
3023
3213
  const payload = Array.from(this.chats.values()).map((chat) => chat.persisted);
3024
- await writeFile7(CHATS_FILE, JSON.stringify(payload, null, 2), "utf-8");
3214
+ await writeFile8(CHATS_FILE, JSON.stringify(payload, null, 2), "utf-8");
3025
3215
  });
3026
3216
  await this.writeChain;
3027
3217
  }
@@ -3051,12 +3241,12 @@ import { Hono } from "hono";
3051
3241
  import { z } from "zod";
3052
3242
 
3053
3243
  // src/services/plan-service.ts
3054
- import { readdir as readdir3, readFile as readFile8 } from "fs/promises";
3244
+ import { readdir as readdir4, readFile as readFile9 } from "fs/promises";
3055
3245
  import { homedir as homedir10 } from "os";
3056
- import { basename, join as join11 } from "path";
3246
+ import { basename, join as join12 } from "path";
3057
3247
  var PLAN_DIRECTORIES = [
3058
- join11(homedir10(), ".claude", "plans"),
3059
- join11(homedir10(), ".replicas", "plans")
3248
+ join12(homedir10(), ".claude", "plans"),
3249
+ join12(homedir10(), ".replicas", "plans")
3060
3250
  ];
3061
3251
  function isMarkdownFile(filename) {
3062
3252
  return filename.toLowerCase().endsWith(".md");
@@ -3069,7 +3259,7 @@ var PlanService = class {
3069
3259
  const planNames = /* @__PURE__ */ new Set();
3070
3260
  for (const directory of PLAN_DIRECTORIES) {
3071
3261
  try {
3072
- const entries = await readdir3(directory, { withFileTypes: true });
3262
+ const entries = await readdir4(directory, { withFileTypes: true });
3073
3263
  for (const entry of entries) {
3074
3264
  if (!entry.isFile()) {
3075
3265
  continue;
@@ -3090,9 +3280,9 @@ var PlanService = class {
3090
3280
  return null;
3091
3281
  }
3092
3282
  for (const directory of PLAN_DIRECTORIES) {
3093
- const filePath = join11(directory, safeFilename);
3283
+ const filePath = join12(directory, safeFilename);
3094
3284
  try {
3095
- const content = await readFile8(filePath, "utf-8");
3285
+ const content = await readFile9(filePath, "utf-8");
3096
3286
  return { filename: safeFilename, content };
3097
3287
  } catch {
3098
3288
  }
@@ -3105,9 +3295,78 @@ var planService = new PlanService();
3105
3295
  // src/services/warm-hooks-service.ts
3106
3296
  import { execFile as execFile2 } from "child_process";
3107
3297
  import { promisify as promisify3 } from "util";
3108
- import { readFile as readFile9 } from "fs/promises";
3298
+ import { readFile as readFile11 } from "fs/promises";
3109
3299
  import { existsSync as existsSync7 } from "fs";
3110
- import { join as join12 } from "path";
3300
+ import { join as join14 } from "path";
3301
+
3302
+ // src/services/warm-hook-logs-service.ts
3303
+ import { createHash as createHash2 } from "crypto";
3304
+ import { mkdir as mkdir11, readFile as readFile10, writeFile as writeFile9, readdir as readdir5 } from "fs/promises";
3305
+ import { join as join13 } from "path";
3306
+ var LOGS_DIR2 = join13(SANDBOX_PATHS.REPLICAS_DIR, "warm-hook-logs");
3307
+ function sanitizeFilename2(name) {
3308
+ const safe = name.replace(/[^a-zA-Z0-9._-]/g, "_");
3309
+ const hash = createHash2("sha256").update(name).digest("hex").slice(0, 8);
3310
+ return `${safe}-${hash}`;
3311
+ }
3312
+ var WarmHookLogsService = class {
3313
+ async ensureDir() {
3314
+ await mkdir11(LOGS_DIR2, { recursive: true });
3315
+ }
3316
+ async saveGlobalHookLog(entry) {
3317
+ await this.ensureDir();
3318
+ const log = {
3319
+ hookType: "global",
3320
+ hookName: "organization",
3321
+ ...entry
3322
+ };
3323
+ await writeFile9(join13(LOGS_DIR2, "global.json"), `${JSON.stringify(log, null, 2)}
3324
+ `, "utf-8");
3325
+ }
3326
+ async saveRepoHookLog(repoName, entry) {
3327
+ await this.ensureDir();
3328
+ const log = {
3329
+ hookType: "repository",
3330
+ hookName: repoName,
3331
+ ...entry
3332
+ };
3333
+ const filename = `repo-${sanitizeFilename2(repoName)}.json`;
3334
+ await writeFile9(join13(LOGS_DIR2, filename), `${JSON.stringify(log, null, 2)}
3335
+ `, "utf-8");
3336
+ }
3337
+ async getAllLogs() {
3338
+ let files;
3339
+ try {
3340
+ files = await readdir5(LOGS_DIR2);
3341
+ } catch (err) {
3342
+ if (err.code === "ENOENT") {
3343
+ return [];
3344
+ }
3345
+ throw err;
3346
+ }
3347
+ const logs = [];
3348
+ for (const file of files) {
3349
+ if (!file.endsWith(".json")) {
3350
+ continue;
3351
+ }
3352
+ try {
3353
+ const raw = await readFile10(join13(LOGS_DIR2, file), "utf-8");
3354
+ logs.push(JSON.parse(raw));
3355
+ } catch {
3356
+ }
3357
+ }
3358
+ logs.sort((a, b) => {
3359
+ if (a.hookType !== b.hookType) {
3360
+ return a.hookType === "global" ? -1 : 1;
3361
+ }
3362
+ return a.hookName.localeCompare(b.hookName);
3363
+ });
3364
+ return logs;
3365
+ }
3366
+ };
3367
+ var warmHookLogsService = new WarmHookLogsService();
3368
+
3369
+ // src/services/warm-hooks-service.ts
3111
3370
  var execFileAsync2 = promisify3(execFile2);
3112
3371
  async function executeHookScript(params) {
3113
3372
  const timeout = clampWarmHookTimeoutMs(params.timeoutMs);
@@ -3138,12 +3397,12 @@ async function executeHookScript(params) {
3138
3397
  }
3139
3398
  async function readRepoWarmHook(repoPath) {
3140
3399
  for (const filename of REPLICAS_CONFIG_FILENAMES) {
3141
- const configPath = join12(repoPath, filename);
3400
+ const configPath = join14(repoPath, filename);
3142
3401
  if (!existsSync7(configPath)) {
3143
3402
  continue;
3144
3403
  }
3145
3404
  try {
3146
- const raw = await readFile9(configPath, "utf-8");
3405
+ const raw = await readFile11(configPath, "utf-8");
3147
3406
  const config = parseReplicasConfigString(raw, filename);
3148
3407
  if (!config.warmHook) {
3149
3408
  return null;
@@ -3189,6 +3448,13 @@ async function runWarmHooks(params) {
3189
3448
  });
3190
3449
  outputBlocks.push(orgResult.output);
3191
3450
  await environmentDetailsService.setGlobalWarmHook(orgResult.exitCode === 0 ? "yes" : "no", orgResult.output);
3451
+ await warmHookLogsService.saveGlobalHookLog({
3452
+ hookScript: orgHook,
3453
+ output: orgResult.output,
3454
+ exitCode: orgResult.exitCode,
3455
+ timedOut: orgResult.timedOut,
3456
+ executedAt: (/* @__PURE__ */ new Date()).toISOString()
3457
+ });
3192
3458
  if (orgResult.exitCode !== 0) {
3193
3459
  return {
3194
3460
  exitCode: orgResult.exitCode,
@@ -3209,6 +3475,11 @@ async function runWarmHooks(params) {
3209
3475
  }
3210
3476
  }
3211
3477
  for (const repoHook of repoHooks) {
3478
+ const combinedScript = repoHook.commands.join("\n");
3479
+ const repoOutputBlocks = [];
3480
+ let repoFailed = false;
3481
+ let repoTimedOut = false;
3482
+ let repoExitCode = 0;
3212
3483
  for (const command of repoHook.commands) {
3213
3484
  const repoResult = await executeHookScript({
3214
3485
  label: `repo-warm-hook:${repoHook.repoName}`,
@@ -3217,18 +3488,32 @@ async function runWarmHooks(params) {
3217
3488
  timeoutMs: repoHook.timeoutMs
3218
3489
  });
3219
3490
  outputBlocks.push(repoResult.output);
3491
+ repoOutputBlocks.push(repoResult.output);
3220
3492
  await environmentDetailsService.setRepositoryWarmHook(
3221
3493
  repoHook.repoName,
3222
3494
  repoResult.exitCode === 0 ? "yes" : "no"
3223
3495
  );
3224
3496
  if (repoResult.exitCode !== 0) {
3225
- return {
3226
- exitCode: repoResult.exitCode,
3227
- output: outputBlocks.join("\n\n"),
3228
- timedOut: repoResult.timedOut
3229
- };
3497
+ repoFailed = true;
3498
+ repoTimedOut = repoResult.timedOut;
3499
+ repoExitCode = repoResult.exitCode;
3500
+ break;
3230
3501
  }
3231
3502
  }
3503
+ await warmHookLogsService.saveRepoHookLog(repoHook.repoName, {
3504
+ hookScript: combinedScript,
3505
+ output: repoOutputBlocks.join("\n\n"),
3506
+ exitCode: repoFailed ? repoExitCode : 0,
3507
+ timedOut: repoTimedOut,
3508
+ executedAt: (/* @__PURE__ */ new Date()).toISOString()
3509
+ });
3510
+ if (repoFailed) {
3511
+ return {
3512
+ exitCode: repoExitCode,
3513
+ output: outputBlocks.join("\n\n"),
3514
+ timedOut: repoTimedOut
3515
+ };
3516
+ }
3232
3517
  }
3233
3518
  } else {
3234
3519
  const repos = await gitService.listRepositories();
@@ -3526,6 +3811,28 @@ function createV1Routes(deps) {
3526
3811
  );
3527
3812
  }
3528
3813
  });
3814
+ app2.get("/warm-hooks/logs", async (c) => {
3815
+ try {
3816
+ const logs = await warmHookLogsService.getAllLogs();
3817
+ return c.json({ logs });
3818
+ } catch (error) {
3819
+ return c.json(
3820
+ jsonError("Failed to read warm hook logs", error instanceof Error ? error.message : "Unknown error"),
3821
+ 500
3822
+ );
3823
+ }
3824
+ });
3825
+ app2.get("/start-hooks/logs", async (c) => {
3826
+ try {
3827
+ const logs = await startHookLogsService.getAllLogs();
3828
+ return c.json({ logs });
3829
+ } catch (error) {
3830
+ return c.json(
3831
+ jsonError("Failed to read start hook logs", error instanceof Error ? error.message : "Unknown error"),
3832
+ 500
3833
+ );
3834
+ }
3835
+ });
3529
3836
  app2.post("/workspace-name", async (c) => {
3530
3837
  try {
3531
3838
  const body = setWorkspaceNameSchema.parse(await c.req.json());
@@ -3603,7 +3910,7 @@ app.get("/health", async (c) => {
3603
3910
  return c.json({ status: "initializing", timestamp: (/* @__PURE__ */ new Date()).toISOString() }, 503);
3604
3911
  }
3605
3912
  try {
3606
- const logContent = await readFile10("/var/log/cloud-init-output.log", "utf-8");
3913
+ const logContent = await readFile12("/var/log/cloud-init-output.log", "utf-8");
3607
3914
  let status;
3608
3915
  if (logContent.includes(COMPLETION_MESSAGE)) {
3609
3916
  status = "active";