replicas-engine 0.1.118 → 0.1.120

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 +235 -128
  2. package/package.json +1 -1
package/dist/src/index.js CHANGED
@@ -42,6 +42,18 @@ function requireValidURL(value, name) {
42
42
  throw new Error(`Invalid engine environment: ${name} must be a valid URL`);
43
43
  }
44
44
  }
45
+ function parseClaudeAuthMethod(value) {
46
+ if (value === "oauth" || value === "api_key" || value === "bedrock") {
47
+ return value;
48
+ }
49
+ return void 0;
50
+ }
51
+ function parseCodexAuthMethod(value) {
52
+ if (value === "oauth" || value === "api_key") {
53
+ return value;
54
+ }
55
+ return void 0;
56
+ }
45
57
  var IS_WARMING_MODE = process.argv.includes("--warming");
46
58
  function loadEngineEnv() {
47
59
  const HOME_DIR = homedir();
@@ -66,7 +78,9 @@ function loadEngineEnv() {
66
78
  CLAUDE_CODE_USE_BEDROCK: readEnv("CLAUDE_CODE_USE_BEDROCK"),
67
79
  AWS_ACCESS_KEY_ID: readEnv("AWS_ACCESS_KEY_ID"),
68
80
  AWS_SECRET_ACCESS_KEY: readEnv("AWS_SECRET_ACCESS_KEY"),
69
- AWS_REGION: readEnv("AWS_REGION")
81
+ AWS_REGION: readEnv("AWS_REGION"),
82
+ REPLICAS_CLAUDE_AUTH_METHOD: parseClaudeAuthMethod(readEnv("REPLICAS_CLAUDE_AUTH_METHOD")),
83
+ REPLICAS_CODEX_AUTH_METHOD: parseCodexAuthMethod(readEnv("REPLICAS_CODEX_AUTH_METHOD"))
70
84
  };
71
85
  if (!IS_WARMING_MODE && !env.WORKSPACE_ID) {
72
86
  console.error("WORKSPACE_ID is not set \u2014 this is required in normal (non-warming) mode");
@@ -362,7 +376,7 @@ var codexTokenManager = new CodexTokenManager();
362
376
  import { readdir, stat } from "fs/promises";
363
377
  import { existsSync as existsSync2 } from "fs";
364
378
  import { execFileSync as execFileSync2 } from "child_process";
365
- import { join as join3 } from "path";
379
+ import { join as join4 } from "path";
366
380
 
367
381
  // src/utils/state.ts
368
382
  import { readFile, writeFile, mkdir, rename, unlink } from "fs/promises";
@@ -413,7 +427,12 @@ function coerceRepoState(value) {
413
427
  if (typeof value.path !== "string") return null;
414
428
  if (typeof value.defaultBranch !== "string") return null;
415
429
  if (typeof value.currentBranch !== "string") return null;
416
- if (!(value.prUrl === null || typeof value.prUrl === "string")) return null;
430
+ if (!Array.isArray(value.prUrls)) return null;
431
+ const prUrls = [];
432
+ for (const entry of value.prUrls) {
433
+ if (typeof entry !== "string") return null;
434
+ if (!prUrls.includes(entry)) prUrls.push(entry);
435
+ }
417
436
  if (!(value.gitDiff === null || isEngineRepoDiff(value.gitDiff))) return null;
418
437
  if (typeof value.startHooksCompleted !== "boolean") return null;
419
438
  return {
@@ -421,7 +440,7 @@ function coerceRepoState(value) {
421
440
  path: value.path,
422
441
  defaultBranch: value.defaultBranch,
423
442
  currentBranch: value.currentBranch,
424
- prUrl: value.prUrl,
443
+ prUrls,
425
444
  gitDiff: value.gitDiff,
426
445
  startHooksCompleted: value.startHooksCompleted
427
446
  };
@@ -482,6 +501,8 @@ async function saveRepoState(repoName, state, fallbackState) {
482
501
 
483
502
  // src/git/commands.ts
484
503
  import { execFileSync } from "child_process";
504
+ import { readFileSync } from "fs";
505
+ import { join as join3 } from "path";
485
506
  function runGitCommand(args, cwd) {
486
507
  return execFileSync("git", args, {
487
508
  cwd,
@@ -489,6 +510,15 @@ function runGitCommand(args, cwd) {
489
510
  stdio: ["pipe", "pipe", "pipe"]
490
511
  }).trim();
491
512
  }
513
+ function readRepoHeadBranch(repoPath) {
514
+ try {
515
+ const contents = readFileSync(join3(repoPath, ".git", "HEAD"), "utf-8").trim();
516
+ const match = contents.match(/^ref:\s+refs\/heads\/(.+)$/);
517
+ return match ? match[1] : null;
518
+ } catch {
519
+ return null;
520
+ }
521
+ }
492
522
  function branchExists(branchName, cwd) {
493
523
  try {
494
524
  runGitCommand(["rev-parse", "--verify", branchName], cwd);
@@ -507,6 +537,9 @@ function getCurrentBranch(cwd) {
507
537
  }
508
538
 
509
539
  // src/git/service.ts
540
+ function appendUniqueUrl(urls, url) {
541
+ return urls.includes(url) ? urls : [...urls, url];
542
+ }
510
543
  var GitService = class {
511
544
  defaultBranchCache = /* @__PURE__ */ new Map();
512
545
  cachedPrByRepo = /* @__PURE__ */ new Map();
@@ -522,13 +555,13 @@ var GitService = class {
522
555
  const entries = await readdir(root);
523
556
  const repos = [];
524
557
  for (const entry of entries) {
525
- const fullPath = join3(root, entry);
558
+ const fullPath = join4(root, entry);
526
559
  try {
527
560
  const entryStat = await stat(fullPath);
528
561
  if (!entryStat.isDirectory()) {
529
562
  continue;
530
563
  }
531
- const hasGit = Boolean(await this.safeStat(join3(fullPath, ".git")));
564
+ const hasGit = Boolean(await this.safeStat(join4(fullPath, ".git")));
532
565
  if (!hasGit) {
533
566
  continue;
534
567
  }
@@ -563,7 +596,7 @@ var GitService = class {
563
596
  path: repo.path,
564
597
  defaultBranch: repo.defaultBranch,
565
598
  currentBranch,
566
- prUrl: persistedMatchesCurrentBranch ? persistedState.prUrl : null,
599
+ prUrls: persistedState?.prUrls ?? [],
567
600
  gitDiff: persistedMatchesCurrentBranch ? persistedState.gitDiff : null,
568
601
  startHooksCompleted: persistedState?.startHooksCompleted ?? false
569
602
  });
@@ -572,7 +605,7 @@ var GitService = class {
572
605
  }
573
606
  return states;
574
607
  }
575
- async refreshRepos() {
608
+ async refreshRepos(observedBranchesByRepo) {
576
609
  const repos = await this.listRepositories();
577
610
  const states = [];
578
611
  for (const repo of repos) {
@@ -580,12 +613,29 @@ var GitService = class {
580
613
  const persistedState = await loadRepoState(repo.name);
581
614
  const currentBranch = getCurrentBranch(repo.path) ?? repo.defaultBranch;
582
615
  const startHooksCompleted = persistedState?.startHooksCompleted ?? false;
583
- states.push(await this.refreshRepoMetadata(repo, currentBranch, startHooksCompleted, persistedState));
616
+ const observed = observedBranchesByRepo?.get(repo.name);
617
+ states.push(
618
+ await this.refreshRepoMetadata(repo, currentBranch, startHooksCompleted, persistedState, observed)
619
+ );
584
620
  } catch {
585
621
  }
586
622
  }
587
623
  return states;
588
624
  }
625
+ // Fast branch snapshot for per-event observation during a turn. Reads
626
+ // .git/HEAD directly for each repo (no subprocess) so callers can safely
627
+ // invoke this on every agent event without measurable overhead.
628
+ async snapshotCurrentBranches() {
629
+ const repos = await this.listRepositories();
630
+ const branches = /* @__PURE__ */ new Map();
631
+ for (const repo of repos) {
632
+ const branch = readRepoHeadBranch(repo.path);
633
+ if (branch) {
634
+ branches.set(repo.name, branch);
635
+ }
636
+ }
637
+ return branches;
638
+ }
589
639
  async setBranchNameAndCheckout(name) {
590
640
  ENGINE_ENV.WORKSPACE_BRANCH_NAME = name;
591
641
  return this.initializeGitRepository();
@@ -618,7 +668,7 @@ var GitService = class {
618
668
  path: repo.path,
619
669
  defaultBranch: repo.defaultBranch,
620
670
  currentBranch: repo.defaultBranch,
621
- prUrl: null,
671
+ prUrls: [],
622
672
  gitDiff: null,
623
673
  startHooksCompleted: false
624
674
  };
@@ -645,7 +695,7 @@ var GitService = class {
645
695
  }
646
696
  const branchName = this.findAvailableBranchName(workspaceName, repo.path);
647
697
  runGitCommand(["checkout", "-b", branchName], repo.path);
648
- await saveRepoState(repo.name, { currentBranch: branchName, prUrl: null }, baselineState);
698
+ await saveRepoState(repo.name, { currentBranch: branchName }, baselineState);
649
699
  results.push({
650
700
  name: repo.name,
651
701
  success: true,
@@ -723,56 +773,50 @@ var GitService = class {
723
773
  return { status: "found", url: cachedPr.prUrl };
724
774
  }
725
775
  const persistedRepoState = persistedRepoStateArg ?? await loadRepoState(repoName);
726
- if (persistedRepoState?.prUrl && persistedRepoState.currentBranch === currentBranch) {
727
- this.cachedPrByRepo.set(repoName, {
728
- prUrl: persistedRepoState.prUrl,
729
- currentBranch
730
- });
731
- return { status: "found", url: persistedRepoState.prUrl };
732
- }
733
776
  this.cachedPrByRepo.delete(repoName);
734
- if (persistedRepoState?.prUrl && persistedRepoState.currentBranch !== currentBranch) {
735
- await saveRepoState(repoName, { prUrl: null }, persistedRepoState);
736
- }
737
- try {
738
- const remoteRef = execFileSync2("git", ["ls-remote", "--heads", "origin", currentBranch], {
739
- cwd: repoPath,
740
- encoding: "utf-8",
741
- stdio: ["pipe", "pipe", "pipe"]
742
- }).trim();
743
- if (!remoteRef) {
744
- return { status: "not_found" };
745
- }
746
- } catch {
747
- return { status: "error" };
748
- }
749
- try {
750
- const prInfo = execFileSync2("gh", ["pr", "view", "--json", "url", "--jq", ".url"], {
751
- cwd: repoPath,
752
- encoding: "utf-8",
753
- stdio: ["pipe", "pipe", "pipe"]
754
- }).trim();
755
- if (prInfo) {
756
- this.cachedPrByRepo.set(repoName, {
757
- prUrl: prInfo,
758
- currentBranch
759
- });
760
- if (persistedRepoState) {
761
- await saveRepoState(repoName, { prUrl: prInfo }, persistedRepoState);
762
- }
763
- return { status: "found", url: prInfo };
777
+ const result = this.lookupPrOnRemote(repoName, repoPath, currentBranch);
778
+ if (result.status === "found") {
779
+ this.cachedPrByRepo.set(repoName, { prUrl: result.url, currentBranch });
780
+ if (persistedRepoState && !persistedRepoState.prUrls.includes(result.url)) {
781
+ await saveRepoState(
782
+ repoName,
783
+ { prUrls: appendUniqueUrl(persistedRepoState.prUrls, result.url) },
784
+ persistedRepoState
785
+ );
764
786
  }
765
- } catch (error) {
766
- const message = error instanceof Error ? error.message : String(error);
767
- console.warn(`[GitService] gh pr view failed for ${repoName}: ${message}`);
768
- return { status: "error" };
769
787
  }
770
- return { status: "not_found" };
788
+ return result;
771
789
  } catch (error) {
772
790
  console.error("Error checking for pull request:", error);
773
791
  return { status: "error" };
774
792
  }
775
793
  }
794
+ lookupPrOnRemote(repoName, repoPath, branch) {
795
+ try {
796
+ const remoteRef = execFileSync2("git", ["ls-remote", "--heads", "origin", branch], {
797
+ cwd: repoPath,
798
+ encoding: "utf-8",
799
+ stdio: ["pipe", "pipe", "pipe"]
800
+ }).trim();
801
+ if (!remoteRef) {
802
+ return { status: "not_found" };
803
+ }
804
+ } catch {
805
+ return { status: "error" };
806
+ }
807
+ try {
808
+ const prInfo = execFileSync2("gh", ["pr", "view", branch, "--json", "url", "--jq", ".url"], {
809
+ cwd: repoPath,
810
+ encoding: "utf-8",
811
+ stdio: ["pipe", "pipe", "pipe"]
812
+ }).trim();
813
+ return prInfo ? { status: "found", url: prInfo } : { status: "not_found" };
814
+ } catch (error) {
815
+ const message = error instanceof Error ? error.message : String(error);
816
+ console.warn(`[GitService] gh pr view ${branch} failed for ${repoName}: ${message}`);
817
+ return { status: "error" };
818
+ }
819
+ }
776
820
  resolveDefaultBranch(repoPath) {
777
821
  const cached = this.defaultBranchCache.get(repoPath);
778
822
  if (cached) {
@@ -807,22 +851,27 @@ var GitService = class {
807
851
  const normalized = name.toLowerCase().replace(/[^a-z0-9._/-]+/g, "-").replace(/\/{2,}/g, "/").replace(/^-+|-+$/g, "");
808
852
  return normalized || "replicas";
809
853
  }
810
- async refreshRepoMetadata(repo, currentBranch, startHooksCompleted, persistedState) {
854
+ async refreshRepoMetadata(repo, currentBranch, startHooksCompleted, persistedState, observedBranches) {
811
855
  const prResult = await this.getPullRequestUrl(repo.name, repo.path, currentBranch, persistedState);
812
- let prUrl;
856
+ let prUrls = persistedState?.prUrls ?? [];
813
857
  if (prResult.status === "found") {
814
- prUrl = prResult.url;
815
- } else if (prResult.status === "not_found") {
816
- prUrl = null;
817
- } else {
818
- prUrl = persistedState?.prUrl ?? null;
858
+ prUrls = appendUniqueUrl(prUrls, prResult.url);
859
+ }
860
+ if (observedBranches) {
861
+ for (const branch of observedBranches) {
862
+ if (branch === currentBranch) continue;
863
+ const branchResult = this.lookupPrOnRemote(repo.name, repo.path, branch);
864
+ if (branchResult.status === "found") {
865
+ prUrls = appendUniqueUrl(prUrls, branchResult.url);
866
+ }
867
+ }
819
868
  }
820
869
  const state = {
821
870
  name: repo.name,
822
871
  path: repo.path,
823
872
  defaultBranch: repo.defaultBranch,
824
873
  currentBranch,
825
- prUrl,
874
+ prUrls,
826
875
  gitDiff: this.getGitDiffStats(repo.path, repo.defaultBranch),
827
876
  startHooksCompleted
828
877
  };
@@ -845,10 +894,10 @@ var gitService = new GitService();
845
894
  // src/utils/logger.ts
846
895
  import { appendFile, mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
847
896
  import { homedir as homedir3 } from "os";
848
- import { join as join4 } from "path";
897
+ import { join as join5 } from "path";
849
898
  import { format } from "util";
850
899
  import { randomBytes } from "crypto";
851
- var LOG_DIR = join4(homedir3(), ".replicas", "logs");
900
+ var LOG_DIR = join5(homedir3(), ".replicas", "logs");
852
901
  var EngineLogger = class {
853
902
  _sessionId = null;
854
903
  filePath = null;
@@ -860,7 +909,7 @@ var EngineLogger = class {
860
909
  async initialize() {
861
910
  await mkdir2(LOG_DIR, { recursive: true });
862
911
  this._sessionId = this.createSessionId();
863
- this.filePath = join4(LOG_DIR, `${this._sessionId}.log`);
912
+ this.filePath = join5(LOG_DIR, `${this._sessionId}.log`);
864
913
  await writeFile2(this.filePath, `=== Replicas Engine Session ${this._sessionId} ===
865
914
  `, "utf-8");
866
915
  this.patchConsole();
@@ -913,7 +962,7 @@ var engineLogger = new EngineLogger();
913
962
  // src/services/replicas-config-service.ts
914
963
  import { readFile as readFile4, appendFile as appendFile2, writeFile as writeFile5, mkdir as mkdir5 } from "fs/promises";
915
964
  import { existsSync as existsSync4 } from "fs";
916
- import { join as join7 } from "path";
965
+ import { join as join8 } from "path";
917
966
  import { homedir as homedir5 } from "os";
918
967
  import { exec } from "child_process";
919
968
  import { promisify as promisify2 } from "util";
@@ -1102,7 +1151,7 @@ function parseReplicasConfigString(content, filename) {
1102
1151
  }
1103
1152
 
1104
1153
  // ../shared/src/engine/environment.ts
1105
- var DAYTONA_SNAPSHOT_ID = "22-04-2026-islington-v1";
1154
+ var DAYTONA_SNAPSHOT_ID = "23-04-2026-islington-v1";
1106
1155
 
1107
1156
  // ../shared/src/engine/types.ts
1108
1157
  var DEFAULT_CHAT_TITLES = {
@@ -1120,14 +1169,14 @@ var WORKSPACE_FILE_CONTENT_MAX_SIZE_BYTES = 1 * 1024 * 1024;
1120
1169
  import { mkdir as mkdir3, readFile as readFile2, writeFile as writeFile3 } from "fs/promises";
1121
1170
  import { existsSync as existsSync3 } from "fs";
1122
1171
  import { homedir as homedir4 } from "os";
1123
- import { join as join5 } from "path";
1172
+ import { join as join6 } from "path";
1124
1173
  import { execFile } from "child_process";
1125
1174
  import { promisify } from "util";
1126
1175
  var execFileAsync = promisify(execFile);
1127
1176
  var REPLICAS_DIR = SANDBOX_PATHS.REPLICAS_DIR;
1128
- var DETAILS_FILE = join5(REPLICAS_DIR, "environment-details.json");
1129
- var CLAUDE_CREDENTIALS_PATH = join5(homedir4(), ".claude", ".credentials.json");
1130
- var CODEX_AUTH_PATH = join5(homedir4(), ".codex", "auth.json");
1177
+ var DETAILS_FILE = join6(REPLICAS_DIR, "environment-details.json");
1178
+ var CLAUDE_CREDENTIALS_PATH = join6(homedir4(), ".claude", ".credentials.json");
1179
+ var CODEX_AUTH_PATH = join6(homedir4(), ".codex", "auth.json");
1131
1180
  function detectClaudeAuthMethod() {
1132
1181
  if (existsSync3(CLAUDE_CREDENTIALS_PATH)) {
1133
1182
  return "oauth";
@@ -1273,8 +1322,8 @@ var environmentDetailsService = new EnvironmentDetailsService();
1273
1322
  // src/services/start-hook-logs-service.ts
1274
1323
  import { createHash } from "crypto";
1275
1324
  import { mkdir as mkdir4, readFile as readFile3, writeFile as writeFile4, readdir as readdir2 } from "fs/promises";
1276
- import { join as join6 } from "path";
1277
- var LOGS_DIR = join6(SANDBOX_PATHS.REPLICAS_DIR, "start-hook-logs");
1325
+ import { join as join7 } from "path";
1326
+ var LOGS_DIR = join7(SANDBOX_PATHS.REPLICAS_DIR, "start-hook-logs");
1278
1327
  function sanitizeFilename(name) {
1279
1328
  const safe = name.replace(/[^a-zA-Z0-9._-]/g, "_");
1280
1329
  const hash = createHash("sha256").update(name).digest("hex").slice(0, 8);
@@ -1294,7 +1343,7 @@ var StartHookLogsService = class {
1294
1343
  async saveRepoLog(repoName, entry) {
1295
1344
  await this.ensureDir();
1296
1345
  const log = { repoName, ...entry };
1297
- await writeFile4(join6(LOGS_DIR, repoFilename(repoName)), `${JSON.stringify(log, null, 2)}
1346
+ await writeFile4(join7(LOGS_DIR, repoFilename(repoName)), `${JSON.stringify(log, null, 2)}
1298
1347
  `, "utf-8");
1299
1348
  }
1300
1349
  async getAllLogs() {
@@ -1313,7 +1362,7 @@ var StartHookLogsService = class {
1313
1362
  continue;
1314
1363
  }
1315
1364
  try {
1316
- const raw = await readFile3(join6(LOGS_DIR, file), "utf-8");
1365
+ const raw = await readFile3(join7(LOGS_DIR, file), "utf-8");
1317
1366
  const stored = JSON.parse(raw);
1318
1367
  logs.push(withPreview(stored));
1319
1368
  } catch {
@@ -1324,7 +1373,7 @@ var StartHookLogsService = class {
1324
1373
  }
1325
1374
  async getFullOutput(repoName) {
1326
1375
  try {
1327
- const raw = await readFile3(join6(LOGS_DIR, repoFilename(repoName)), "utf-8");
1376
+ const raw = await readFile3(join7(LOGS_DIR, repoFilename(repoName)), "utf-8");
1328
1377
  const stored = JSON.parse(raw);
1329
1378
  if (stored.repoName !== repoName) {
1330
1379
  return null;
@@ -1342,7 +1391,7 @@ var startHookLogsService = new StartHookLogsService();
1342
1391
 
1343
1392
  // src/services/replicas-config-service.ts
1344
1393
  var execAsync = promisify2(exec);
1345
- var START_HOOKS_LOG = join7(homedir5(), ".replicas", "startHooks.log");
1394
+ var START_HOOKS_LOG = join8(homedir5(), ".replicas", "startHooks.log");
1346
1395
  var START_HOOKS_RUNNING_PROMPT = `IMPORTANT - Start Hooks Running:
1347
1396
  Start hooks are shell commands/scripts set by repository owners that run on workspace startup.
1348
1397
  These hooks are currently executing in the background. You can:
@@ -1353,7 +1402,7 @@ The start hooks may install dependencies, build projects, or perform other setup
1353
1402
  If your task depends on setup being complete, check the log file before proceeding.`;
1354
1403
  async function readReplicasConfigFromDir(dirPath) {
1355
1404
  for (const filename of REPLICAS_CONFIG_FILENAMES) {
1356
- const configPath = join7(dirPath, filename);
1405
+ const configPath = join8(dirPath, filename);
1357
1406
  if (!existsSync4(configPath)) {
1358
1407
  continue;
1359
1408
  }
@@ -1417,7 +1466,7 @@ var ReplicasConfigService = class {
1417
1466
  const logLine = `[${timestamp}] ${message}
1418
1467
  `;
1419
1468
  try {
1420
- await mkdir5(join7(homedir5(), ".replicas"), { recursive: true });
1469
+ await mkdir5(join8(homedir5(), ".replicas"), { recursive: true });
1421
1470
  await appendFile2(START_HOOKS_LOG, logLine, "utf-8");
1422
1471
  } catch (error) {
1423
1472
  console.error("Failed to write to start hooks log:", error);
@@ -1441,7 +1490,7 @@ var ReplicasConfigService = class {
1441
1490
  this.hooksRunning = true;
1442
1491
  this.hooksCompleted = false;
1443
1492
  try {
1444
- await mkdir5(join7(homedir5(), ".replicas"), { recursive: true });
1493
+ await mkdir5(join8(homedir5(), ".replicas"), { recursive: true });
1445
1494
  await writeFile5(
1446
1495
  START_HOOKS_LOG,
1447
1496
  `=== Start Hooks Execution Log ===
@@ -1528,7 +1577,7 @@ Repositories: ${hookEntries.length}
1528
1577
  path: entry.workingDirectory,
1529
1578
  defaultBranch: entry.defaultBranch,
1530
1579
  currentBranch: entry.defaultBranch,
1531
- prUrl: null,
1580
+ prUrls: [],
1532
1581
  gitDiff: null,
1533
1582
  startHooksCompleted: false
1534
1583
  };
@@ -1593,10 +1642,10 @@ var replicasConfigService = new ReplicasConfigService();
1593
1642
  // src/services/event-service.ts
1594
1643
  import { appendFile as appendFile3, mkdir as mkdir6 } from "fs/promises";
1595
1644
  import { homedir as homedir6 } from "os";
1596
- import { join as join8 } from "path";
1645
+ import { join as join9 } from "path";
1597
1646
  import { randomUUID } from "crypto";
1598
- var ENGINE_DIR = join8(homedir6(), ".replicas", "engine");
1599
- var EVENTS_FILE = join8(ENGINE_DIR, "events.jsonl");
1647
+ var ENGINE_DIR = join9(homedir6(), ".replicas", "engine");
1648
+ var EVENTS_FILE = join9(ENGINE_DIR, "events.jsonl");
1600
1649
  var EventService = class {
1601
1650
  subscribers = /* @__PURE__ */ new Map();
1602
1651
  writeChain = Promise.resolve();
@@ -1689,14 +1738,14 @@ var previewService = new PreviewService();
1689
1738
  import { existsSync as existsSync7 } from "fs";
1690
1739
  import { mkdir as mkdir10, readFile as readFile8, rm, writeFile as writeFile8 } from "fs/promises";
1691
1740
  import { homedir as homedir9 } from "os";
1692
- import { join as join11 } from "path";
1741
+ import { join as join12 } from "path";
1693
1742
  import { randomUUID as randomUUID4 } from "crypto";
1694
1743
 
1695
1744
  // src/managers/claude-manager.ts
1696
1745
  import {
1697
1746
  query
1698
1747
  } from "@anthropic-ai/claude-agent-sdk";
1699
- import { join as join9 } from "path";
1748
+ import { join as join10 } from "path";
1700
1749
  import { mkdir as mkdir8, appendFile as appendFile4 } from "fs/promises";
1701
1750
  import { homedir as homedir7 } from "os";
1702
1751
 
@@ -2412,6 +2461,43 @@ var CodingAgentManager = class {
2412
2461
  }
2413
2462
  };
2414
2463
 
2464
+ // src/utils/agent-env.ts
2465
+ function buildClaudeAgentEnv(overrides) {
2466
+ const env = { ...process.env };
2467
+ if (overrides) {
2468
+ Object.assign(env, overrides);
2469
+ }
2470
+ if (shouldStripAnthropicApiKey()) {
2471
+ env.ANTHROPIC_API_KEY = void 0;
2472
+ }
2473
+ return env;
2474
+ }
2475
+ function buildCodexAgentEnv() {
2476
+ const env = {};
2477
+ for (const [key, value] of Object.entries(process.env)) {
2478
+ if (typeof value === "string") {
2479
+ env[key] = value;
2480
+ }
2481
+ }
2482
+ if (shouldStripOpenAIApiKey()) {
2483
+ delete env.OPENAI_API_KEY;
2484
+ }
2485
+ return env;
2486
+ }
2487
+ function resolveCodexApiKey() {
2488
+ if (shouldStripOpenAIApiKey()) {
2489
+ return void 0;
2490
+ }
2491
+ return ENGINE_ENV.OPENAI_API_KEY;
2492
+ }
2493
+ function shouldStripAnthropicApiKey() {
2494
+ const method = ENGINE_ENV.REPLICAS_CLAUDE_AUTH_METHOD;
2495
+ return method === "oauth" || method === "bedrock";
2496
+ }
2497
+ function shouldStripOpenAIApiKey() {
2498
+ return ENGINE_ENV.REPLICAS_CODEX_AUTH_METHOD === "oauth";
2499
+ }
2500
+
2415
2501
  // src/managers/claude-manager.ts
2416
2502
  var PromptStream = class {
2417
2503
  queue = [];
@@ -2467,7 +2553,7 @@ var ClaudeManager = class _ClaudeManager extends CodingAgentManager {
2467
2553
  disallowedToolsOverride;
2468
2554
  constructor(options) {
2469
2555
  super(options);
2470
- this.historyFile = options.historyFilePath ?? join9(homedir7(), ".replicas", "claude", "history.jsonl");
2556
+ this.historyFile = options.historyFilePath ?? join10(homedir7(), ".replicas", "claude", "history.jsonl");
2471
2557
  this.systemPromptOverride = options.systemPromptOverride;
2472
2558
  this.toolsOverride = options.tools;
2473
2559
  this.mcpServersConfig = options.mcpServers;
@@ -2557,7 +2643,7 @@ var ClaudeManager = class _ClaudeManager extends CodingAgentManager {
2557
2643
  preset: "claude_code",
2558
2644
  append: combinedInstructions
2559
2645
  };
2560
- const queryEnv = this.envOverrides ? { ...process.env, ...this.envOverrides } : process.env;
2646
+ const queryEnv = buildClaudeAgentEnv(this.envOverrides);
2561
2647
  const response = query({
2562
2648
  prompt: promptStream,
2563
2649
  options: {
@@ -2650,7 +2736,7 @@ var ClaudeManager = class _ClaudeManager extends CodingAgentManager {
2650
2736
  };
2651
2737
  }
2652
2738
  async initialize() {
2653
- const historyDir = join9(homedir7(), ".replicas", "claude");
2739
+ const historyDir = join10(homedir7(), ".replicas", "claude");
2654
2740
  await mkdir8(historyDir, { recursive: true });
2655
2741
  if (this.initialSessionId) {
2656
2742
  this.sessionId = this.initialSessionId;
@@ -2704,11 +2790,11 @@ import { Codex } from "@openai/codex-sdk";
2704
2790
  import { randomUUID as randomUUID3 } from "crypto";
2705
2791
  import { readdir as readdir3, stat as stat2, writeFile as writeFile7, mkdir as mkdir9, readFile as readFile7 } from "fs/promises";
2706
2792
  import { existsSync as existsSync6 } from "fs";
2707
- import { join as join10 } from "path";
2793
+ import { join as join11 } from "path";
2708
2794
  import { homedir as homedir8 } from "os";
2709
2795
  import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
2710
2796
  var DEFAULT_MODEL = "gpt-5.4";
2711
- var CODEX_CONFIG_PATH = join10(homedir8(), ".codex", "config.toml");
2797
+ var CODEX_CONFIG_PATH = join11(homedir8(), ".codex", "config.toml");
2712
2798
  function isLinearThoughtEvent2(event) {
2713
2799
  return event.content.type === "thought";
2714
2800
  }
@@ -2729,10 +2815,12 @@ var CodexManager = class extends CodingAgentManager {
2729
2815
  activeAbortController = null;
2730
2816
  constructor(options) {
2731
2817
  super(options);
2732
- this.codex = new Codex(
2733
- ENGINE_ENV.OPENAI_API_KEY ? { apiKey: ENGINE_ENV.OPENAI_API_KEY } : void 0
2734
- );
2735
- this.tempImageDir = join10(homedir8(), ".replicas", "codex", "temp-images");
2818
+ const codexApiKey = resolveCodexApiKey();
2819
+ this.codex = new Codex({
2820
+ env: buildCodexAgentEnv(),
2821
+ ...codexApiKey ? { apiKey: codexApiKey } : {}
2822
+ });
2823
+ this.tempImageDir = join11(homedir8(), ".replicas", "codex", "temp-images");
2736
2824
  this.initializeManager(this.processMessageInternal.bind(this));
2737
2825
  }
2738
2826
  async initialize() {
@@ -2752,7 +2840,7 @@ var CodexManager = class extends CodingAgentManager {
2752
2840
  */
2753
2841
  async updateCodexConfig(developerInstructions) {
2754
2842
  try {
2755
- const codexDir = join10(homedir8(), ".codex");
2843
+ const codexDir = join11(homedir8(), ".codex");
2756
2844
  await mkdir9(codexDir, { recursive: true });
2757
2845
  let config = {};
2758
2846
  if (existsSync6(CODEX_CONFIG_PATH)) {
@@ -2788,7 +2876,7 @@ var CodexManager = class extends CodingAgentManager {
2788
2876
  for (const image of images) {
2789
2877
  const ext = image.source.media_type.split("/")[1] || "png";
2790
2878
  const filename = `img_${randomUUID3()}.${ext}`;
2791
- const filepath = join10(this.tempImageDir, filename);
2879
+ const filepath = join11(this.tempImageDir, filename);
2792
2880
  const buffer = Buffer.from(image.source.data, "base64");
2793
2881
  await writeFile7(filepath, buffer);
2794
2882
  tempPaths.push(filepath);
@@ -2927,13 +3015,13 @@ var CodexManager = class extends CodingAgentManager {
2927
3015
  }
2928
3016
  // Helper methods for finding session files
2929
3017
  async findSessionFile(threadId) {
2930
- const sessionsDir = join10(homedir8(), ".codex", "sessions");
3018
+ const sessionsDir = join11(homedir8(), ".codex", "sessions");
2931
3019
  try {
2932
3020
  const now = /* @__PURE__ */ new Date();
2933
3021
  const year = now.getFullYear();
2934
3022
  const month = String(now.getMonth() + 1).padStart(2, "0");
2935
3023
  const day = String(now.getDate()).padStart(2, "0");
2936
- const todayDir = join10(sessionsDir, String(year), month, day);
3024
+ const todayDir = join11(sessionsDir, String(year), month, day);
2937
3025
  const file = await this.findFileInDirectory(todayDir, threadId);
2938
3026
  if (file) return file;
2939
3027
  for (let daysAgo = 1; daysAgo <= 7; daysAgo++) {
@@ -2942,7 +3030,7 @@ var CodexManager = class extends CodingAgentManager {
2942
3030
  const searchYear = date.getFullYear();
2943
3031
  const searchMonth = String(date.getMonth() + 1).padStart(2, "0");
2944
3032
  const searchDay = String(date.getDate()).padStart(2, "0");
2945
- const searchDir = join10(sessionsDir, String(searchYear), searchMonth, searchDay);
3033
+ const searchDir = join11(sessionsDir, String(searchYear), searchMonth, searchDay);
2946
3034
  const file2 = await this.findFileInDirectory(searchDir, threadId);
2947
3035
  if (file2) return file2;
2948
3036
  }
@@ -2956,7 +3044,7 @@ var CodexManager = class extends CodingAgentManager {
2956
3044
  const files = await readdir3(directory);
2957
3045
  for (const file of files) {
2958
3046
  if (file.endsWith(".jsonl") && file.includes(threadId)) {
2959
- const fullPath = join10(directory, file);
3047
+ const fullPath = join11(directory, file);
2960
3048
  const stats = await stat2(fullPath);
2961
3049
  if (stats.isFile()) {
2962
3050
  return fullPath;
@@ -3495,7 +3583,7 @@ function getEnvironmentSection() {
3495
3583
  `Platform: ${process.platform}`,
3496
3584
  getShellInfoLine(),
3497
3585
  `OS Version: ${unameSR}`,
3498
- `The most recent Claude model family is Claude 4.5/4.6. Model IDs \u2014 Opus 4.6: 'claude-opus-4-6', Sonnet 4.6: 'claude-sonnet-4-6', Haiku 4.5: 'claude-haiku-4-5-20251001'. When building AI applications, default to the latest and most capable Claude models.`
3586
+ `The most recent Claude model family is Claude 4.5/4.6/4.7. Model IDs \u2014 Opus 4.7: 'claude-opus-4-7', Sonnet 4.6: 'claude-sonnet-4-6', Haiku 4.5: 'claude-haiku-4-5-20251001'. When building AI applications, default to the latest and most capable Claude models.`
3499
3587
  ];
3500
3588
  return [
3501
3589
  `# Environment`,
@@ -3639,11 +3727,11 @@ var DuplicateDefaultChatError = class extends Error {
3639
3727
  };
3640
3728
 
3641
3729
  // src/services/chat/chat-service.ts
3642
- var ENGINE_DIR2 = join11(homedir9(), ".replicas", "engine");
3643
- var CHATS_FILE = join11(ENGINE_DIR2, "chats.json");
3644
- var CLAUDE_HISTORY_DIR = join11(ENGINE_DIR2, "claude-histories");
3645
- var RELAY_HISTORY_DIR = join11(ENGINE_DIR2, "relay-histories");
3646
- var CODEX_AUTH_PATH2 = join11(homedir9(), ".codex", "auth.json");
3730
+ var ENGINE_DIR2 = join12(homedir9(), ".replicas", "engine");
3731
+ var CHATS_FILE = join12(ENGINE_DIR2, "chats.json");
3732
+ var CLAUDE_HISTORY_DIR = join12(ENGINE_DIR2, "claude-histories");
3733
+ var RELAY_HISTORY_DIR = join12(ENGINE_DIR2, "relay-histories");
3734
+ var CODEX_AUTH_PATH2 = join12(homedir9(), ".codex", "auth.json");
3647
3735
  function isCodexAvailable() {
3648
3736
  return existsSync7(CODEX_AUTH_PATH2) || Boolean(ENGINE_ENV.OPENAI_API_KEY);
3649
3737
  }
@@ -3801,7 +3889,7 @@ var ChatService = class {
3801
3889
  async deleteHistoryFile(persisted) {
3802
3890
  if (persisted.provider === "claude" || persisted.provider === "relay") {
3803
3891
  const dir = persisted.provider === "claude" ? CLAUDE_HISTORY_DIR : RELAY_HISTORY_DIR;
3804
- await rm(join11(dir, `${persisted.id}.jsonl`), { force: true });
3892
+ await rm(join12(dir, `${persisted.id}.jsonl`), { force: true });
3805
3893
  }
3806
3894
  }
3807
3895
  async getChatHistory(chatId) {
@@ -3844,7 +3932,7 @@ var ChatService = class {
3844
3932
  if (persisted.provider === "claude") {
3845
3933
  provider = new ClaudeManager({
3846
3934
  workingDirectory: this.workingDirectory,
3847
- historyFilePath: join11(CLAUDE_HISTORY_DIR, `${persisted.id}.jsonl`),
3935
+ historyFilePath: join12(CLAUDE_HISTORY_DIR, `${persisted.id}.jsonl`),
3848
3936
  initialSessionId: persisted.providerSessionId,
3849
3937
  onSaveSessionId: saveSession,
3850
3938
  onTurnComplete: onProviderTurnComplete,
@@ -3853,7 +3941,7 @@ var ChatService = class {
3853
3941
  } else if (persisted.provider === "relay") {
3854
3942
  provider = new RelayManager({
3855
3943
  workingDirectory: this.workingDirectory,
3856
- historyFilePath: join11(RELAY_HISTORY_DIR, `${persisted.id}.jsonl`),
3944
+ historyFilePath: join12(RELAY_HISTORY_DIR, `${persisted.id}.jsonl`),
3857
3945
  initialSessionId: persisted.providerSessionId,
3858
3946
  onSaveSessionId: saveSession,
3859
3947
  onTurnComplete: onProviderTurnComplete,
@@ -3874,7 +3962,8 @@ var ChatService = class {
3874
3962
  persisted,
3875
3963
  provider,
3876
3964
  pendingMessageIds: [],
3877
- hasActiveTurn: false
3965
+ hasActiveTurn: false,
3966
+ observedBranchesByRepo: /* @__PURE__ */ new Map()
3878
3967
  };
3879
3968
  }
3880
3969
  touch(chat) {
@@ -3922,6 +4011,8 @@ var ChatService = class {
3922
4011
  });
3923
4012
  }
3924
4013
  this.touch(chat);
4014
+ this.observeCurrentBranches(chat).catch(() => {
4015
+ });
3925
4016
  this.publish({
3926
4017
  type: "chat.turn.delta",
3927
4018
  payload: {
@@ -3937,7 +4028,7 @@ var ChatService = class {
3937
4028
  return;
3938
4029
  }
3939
4030
  if (!chat.hasActiveTurn) {
3940
- await this.publishAgentTurnCompleteWebhook();
4031
+ await this.publishAgentTurnCompleteWebhook(chat);
3941
4032
  return;
3942
4033
  }
3943
4034
  chat.hasActiveTurn = false;
@@ -3950,7 +4041,7 @@ var ChatService = class {
3950
4041
  }
3951
4042
  }).catch(() => {
3952
4043
  });
3953
- await this.publishAgentTurnCompleteWebhook();
4044
+ await this.publishAgentTurnCompleteWebhook(chat);
3954
4045
  }
3955
4046
  getRuntimeChat(chatId) {
3956
4047
  return this.chats.get(chatId) ?? null;
@@ -3986,15 +4077,31 @@ var ChatService = class {
3986
4077
  };
3987
4078
  await eventService.publish(event);
3988
4079
  }
3989
- async publishAgentTurnCompleteWebhook() {
4080
+ async observeCurrentBranches(chat) {
4081
+ try {
4082
+ const snapshot = await gitService.snapshotCurrentBranches();
4083
+ for (const [repoName, branch] of snapshot) {
4084
+ let observed = chat.observedBranchesByRepo.get(repoName);
4085
+ if (!observed) {
4086
+ observed = /* @__PURE__ */ new Set();
4087
+ chat.observedBranchesByRepo.set(repoName, observed);
4088
+ }
4089
+ observed.add(branch);
4090
+ }
4091
+ } catch {
4092
+ }
4093
+ }
4094
+ async publishAgentTurnCompleteWebhook(chat) {
3990
4095
  try {
3991
4096
  await githubTokenManager.refreshOnce();
3992
4097
  } catch {
3993
4098
  }
3994
4099
  const linearSessionId = ENGINE_ENV.LINEAR_SESSION_ID;
4100
+ const observedBranches = chat.observedBranchesByRepo;
4101
+ chat.observedBranchesByRepo = /* @__PURE__ */ new Map();
3995
4102
  let repoStatuses;
3996
4103
  try {
3997
- repoStatuses = await gitService.refreshRepos();
4104
+ repoStatuses = await gitService.refreshRepos(observedBranches);
3998
4105
  console.log(`Repository Statuses Refreshed: `, repoStatuses);
3999
4106
  } catch (error) {
4000
4107
  console.error("[ChatService] Failed to refresh repo statuses:", error);
@@ -4012,15 +4119,15 @@ var ChatService = class {
4012
4119
  import { Hono } from "hono";
4013
4120
  import { z as z2 } from "zod";
4014
4121
  import { readdir as readdir6, stat as stat3, readFile as readFile12 } from "fs/promises";
4015
- import { join as join15, resolve } from "path";
4122
+ import { join as join16, resolve } from "path";
4016
4123
 
4017
4124
  // src/services/plan-service.ts
4018
4125
  import { readdir as readdir4, readFile as readFile9 } from "fs/promises";
4019
4126
  import { homedir as homedir10 } from "os";
4020
- import { basename, join as join12 } from "path";
4127
+ import { basename, join as join13 } from "path";
4021
4128
  var PLAN_DIRECTORIES = [
4022
- join12(homedir10(), ".claude", "plans"),
4023
- join12(homedir10(), ".replicas", "plans")
4129
+ join13(homedir10(), ".claude", "plans"),
4130
+ join13(homedir10(), ".replicas", "plans")
4024
4131
  ];
4025
4132
  function isMarkdownFile(filename) {
4026
4133
  return filename.toLowerCase().endsWith(".md");
@@ -4054,7 +4161,7 @@ var PlanService = class {
4054
4161
  return null;
4055
4162
  }
4056
4163
  for (const directory of PLAN_DIRECTORIES) {
4057
- const filePath = join12(directory, safeFilename);
4164
+ const filePath = join13(directory, safeFilename);
4058
4165
  try {
4059
4166
  const content = await readFile9(filePath, "utf-8");
4060
4167
  return { filename: safeFilename, content };
@@ -4071,13 +4178,13 @@ import { execFile as execFile2 } from "child_process";
4071
4178
  import { promisify as promisify3 } from "util";
4072
4179
  import { readFile as readFile11 } from "fs/promises";
4073
4180
  import { existsSync as existsSync8 } from "fs";
4074
- import { join as join14 } from "path";
4181
+ import { join as join15 } from "path";
4075
4182
 
4076
4183
  // src/services/warm-hook-logs-service.ts
4077
4184
  import { createHash as createHash2 } from "crypto";
4078
4185
  import { mkdir as mkdir11, readFile as readFile10, writeFile as writeFile9, readdir as readdir5 } from "fs/promises";
4079
- import { join as join13 } from "path";
4080
- var LOGS_DIR2 = join13(SANDBOX_PATHS.REPLICAS_DIR, "warm-hook-logs");
4186
+ import { join as join14 } from "path";
4187
+ var LOGS_DIR2 = join14(SANDBOX_PATHS.REPLICAS_DIR, "warm-hook-logs");
4081
4188
  function sanitizeFilename2(name) {
4082
4189
  const safe = name.replace(/[^a-zA-Z0-9._-]/g, "_");
4083
4190
  const hash = createHash2("sha256").update(name).digest("hex").slice(0, 8);
@@ -4104,7 +4211,7 @@ var WarmHookLogsService = class {
4104
4211
  hookName: "organization",
4105
4212
  ...entry
4106
4213
  };
4107
- await writeFile9(join13(LOGS_DIR2, globalFilename()), `${JSON.stringify(log, null, 2)}
4214
+ await writeFile9(join14(LOGS_DIR2, globalFilename()), `${JSON.stringify(log, null, 2)}
4108
4215
  `, "utf-8");
4109
4216
  }
4110
4217
  async saveRepoHookLog(repoName, entry) {
@@ -4114,7 +4221,7 @@ var WarmHookLogsService = class {
4114
4221
  hookName: repoName,
4115
4222
  ...entry
4116
4223
  };
4117
- await writeFile9(join13(LOGS_DIR2, repoFilename2(repoName)), `${JSON.stringify(log, null, 2)}
4224
+ await writeFile9(join14(LOGS_DIR2, repoFilename2(repoName)), `${JSON.stringify(log, null, 2)}
4118
4225
  `, "utf-8");
4119
4226
  }
4120
4227
  async getAllLogs() {
@@ -4133,7 +4240,7 @@ var WarmHookLogsService = class {
4133
4240
  continue;
4134
4241
  }
4135
4242
  try {
4136
- const raw = await readFile10(join13(LOGS_DIR2, file), "utf-8");
4243
+ const raw = await readFile10(join14(LOGS_DIR2, file), "utf-8");
4137
4244
  const stored = JSON.parse(raw);
4138
4245
  logs.push(withPreview2(stored));
4139
4246
  } catch {
@@ -4150,7 +4257,7 @@ var WarmHookLogsService = class {
4150
4257
  async getFullOutput(hookType, hookName) {
4151
4258
  const filename = hookType === "global" ? globalFilename() : repoFilename2(hookName);
4152
4259
  try {
4153
- const raw = await readFile10(join13(LOGS_DIR2, filename), "utf-8");
4260
+ const raw = await readFile10(join14(LOGS_DIR2, filename), "utf-8");
4154
4261
  const stored = JSON.parse(raw);
4155
4262
  if (stored.hookType !== hookType || stored.hookName !== hookName) {
4156
4263
  return null;
@@ -4197,7 +4304,7 @@ async function executeHookScript(params) {
4197
4304
  }
4198
4305
  async function readRepoWarmHook(repoPath) {
4199
4306
  for (const filename of REPLICAS_CONFIG_FILENAMES) {
4200
- const configPath = join14(repoPath, filename);
4307
+ const configPath = join15(repoPath, filename);
4201
4308
  if (!existsSync8(configPath)) {
4202
4309
  continue;
4203
4310
  }
@@ -4724,7 +4831,7 @@ function createV1Routes(deps) {
4724
4831
  const logFiles = files.filter((f) => f.endsWith(".log"));
4725
4832
  const sessions = await Promise.all(
4726
4833
  logFiles.map(async (filename) => {
4727
- const filePath = join15(LOG_DIR, filename);
4834
+ const filePath = join16(LOG_DIR, filename);
4728
4835
  const fileStat = await stat3(filePath);
4729
4836
  const sessionId = filename.replace(/\.log$/, "");
4730
4837
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "replicas-engine",
3
- "version": "0.1.118",
3
+ "version": "0.1.120",
4
4
  "description": "Lightweight API server for Replicas workspaces",
5
5
  "type": "module",
6
6
  "main": "dist/src/index.js",