replicas-engine 0.1.166 → 0.1.168

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 +514 -38
  2. package/package.json +1 -1
package/dist/src/index.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // src/index.ts
4
4
  import { serve } from "@hono/node-server";
5
5
  import { Hono as Hono2 } from "hono";
6
- import { readFile as readFile13 } from "fs/promises";
6
+ import { readFile as readFile14 } from "fs/promises";
7
7
  import { execSync as execSync2 } from "child_process";
8
8
  import { randomUUID as randomUUID5 } from "crypto";
9
9
 
@@ -20,6 +20,96 @@ import { readFileSync } from "fs";
20
20
 
21
21
  // ../shared/src/event.ts
22
22
  var CODEX_QUOTA_STATUS_EVENT_TYPE = "codex-quota-status";
23
+ var CONTEXT_USAGE_EVENT_TYPE = "context-usage";
24
+
25
+ // ../shared/src/languages.ts
26
+ var EXT_TO_LANGUAGE = {
27
+ ".ts": "typescript",
28
+ ".tsx": "tsx",
29
+ ".js": "javascript",
30
+ ".jsx": "jsx",
31
+ ".mjs": "javascript",
32
+ ".cjs": "javascript",
33
+ ".py": "python",
34
+ ".pyw": "python",
35
+ ".pyi": "python",
36
+ ".rs": "rust",
37
+ ".go": "go",
38
+ ".rb": "ruby",
39
+ ".rake": "ruby",
40
+ ".java": "java",
41
+ ".kt": "kotlin",
42
+ ".kts": "kotlin",
43
+ ".scala": "scala",
44
+ ".swift": "swift",
45
+ ".m": "objectivec",
46
+ ".mm": "objectivec",
47
+ ".c": "c",
48
+ ".cpp": "cpp",
49
+ ".cc": "cpp",
50
+ ".cxx": "cpp",
51
+ ".h": "c",
52
+ ".hpp": "cpp",
53
+ ".hh": "cpp",
54
+ ".hxx": "cpp",
55
+ ".cs": "csharp",
56
+ ".vb": "vbnet",
57
+ ".fs": "fsharp",
58
+ ".php": "php",
59
+ ".phtml": "php",
60
+ ".css": "css",
61
+ ".scss": "scss",
62
+ ".sass": "sass",
63
+ ".less": "less",
64
+ ".html": "html",
65
+ ".htm": "html",
66
+ ".vue": "vue",
67
+ ".svelte": "svelte",
68
+ ".xml": "xml",
69
+ ".svg": "xml",
70
+ ".json": "json",
71
+ ".json5": "json5",
72
+ ".yaml": "yaml",
73
+ ".yml": "yaml",
74
+ ".toml": "toml",
75
+ ".ini": "ini",
76
+ ".md": "markdown",
77
+ ".markdown": "markdown",
78
+ ".sql": "sql",
79
+ ".sh": "bash",
80
+ ".bash": "bash",
81
+ ".zsh": "bash",
82
+ ".fish": "bash",
83
+ ".dockerfile": "dockerfile",
84
+ ".graphql": "graphql",
85
+ ".proto": "protobuf",
86
+ ".r": "r",
87
+ ".lua": "lua",
88
+ ".dart": "dart",
89
+ ".zig": "zig",
90
+ ".ex": "elixir",
91
+ ".exs": "elixir",
92
+ ".erl": "erlang",
93
+ ".hs": "haskell",
94
+ ".ml": "ocaml",
95
+ ".clj": "clojure",
96
+ ".cljs": "clojure",
97
+ ".pl": "perl",
98
+ ".perl": "perl",
99
+ ".elm": "elm",
100
+ ".lisp": "lisp"
101
+ };
102
+ function detectLanguageByPath(filePath) {
103
+ const dot = filePath.lastIndexOf(".");
104
+ if (dot === -1) {
105
+ const basename2 = filePath.split("/").pop() ?? "";
106
+ if (basename2 === "Dockerfile") return "dockerfile";
107
+ if (basename2 === "Makefile") return "makefile";
108
+ return null;
109
+ }
110
+ const ext = filePath.slice(dot).toLowerCase();
111
+ return EXT_TO_LANGUAGE[ext] ?? null;
112
+ }
23
113
 
24
114
  // ../shared/src/pricing.ts
25
115
  var PLANS = {
@@ -285,7 +375,7 @@ function parseReplicasConfigString(content, filename) {
285
375
  }
286
376
 
287
377
  // ../shared/src/engine/environment.ts
288
- var DAYTONA_SNAPSHOT_ID = "13-05-2026-royal-york-v12";
378
+ var DAYTONA_SNAPSHOT_ID = "14-05-2026-royal-york-v13";
289
379
 
290
380
  // ../shared/src/engine/types.ts
291
381
  var DEFAULT_CHAT_TITLES = {
@@ -453,7 +543,7 @@ var BaseRefreshManager = class {
453
543
  this.health.lastErrorMessage = message;
454
544
  if (attempt < 3) {
455
545
  console.warn(`[${this.managerName}] Initial refresh attempt ${attempt} failed, retrying in 2s...`);
456
- await new Promise((resolve2) => setTimeout(resolve2, 2e3));
546
+ await new Promise((resolve3) => setTimeout(resolve3, 2e3));
457
547
  } else {
458
548
  console.error(`[${this.managerName}] Initial refresh failed after 3 attempts:`, error);
459
549
  }
@@ -694,7 +784,7 @@ var codexTokenManager = new CodexTokenManager();
694
784
  // src/git/service.ts
695
785
  import { readdir, stat } from "fs/promises";
696
786
  import { existsSync as existsSync2 } from "fs";
697
- import { execFileSync as execFileSync2 } from "child_process";
787
+ import { execFileSync as execFileSync2, spawnSync } from "child_process";
698
788
  import { join as join4 } from "path";
699
789
 
700
790
  // src/utils/state.ts
@@ -1052,6 +1142,7 @@ var GitService = class {
1052
1142
  if (removedMatch) {
1053
1143
  removed = parseInt(removedMatch[1], 10);
1054
1144
  }
1145
+ added += this.countUntrackedAddedLines(repoPath);
1055
1146
  return {
1056
1147
  added,
1057
1148
  removed
@@ -1061,18 +1152,60 @@ var GitService = class {
1061
1152
  return null;
1062
1153
  }
1063
1154
  }
1155
+ getUntrackedDiff(repoPath, extraFlags = []) {
1156
+ const listing = execFileSync2("git", ["ls-files", "--others", "--exclude-standard", "-z"], {
1157
+ cwd: repoPath,
1158
+ encoding: "utf-8",
1159
+ stdio: ["pipe", "pipe", "pipe"]
1160
+ });
1161
+ const paths = listing.split("\0").filter(Boolean);
1162
+ if (paths.length === 0) return "";
1163
+ try {
1164
+ execFileSync2("git", ["add", "--intent-to-add", "--", ...paths], { cwd: repoPath, stdio: ["pipe", "pipe", "pipe"] });
1165
+ const result = spawnSync("git", ["diff", ...extraFlags, "--", ...paths], {
1166
+ cwd: repoPath,
1167
+ encoding: "utf-8",
1168
+ maxBuffer: 50 * 1024 * 1024
1169
+ });
1170
+ return result.status === 0 || result.status === 1 ? result.stdout : "";
1171
+ } finally {
1172
+ try {
1173
+ execFileSync2("git", ["reset", "--", ...paths], { cwd: repoPath, stdio: ["pipe", "pipe", "pipe"] });
1174
+ } catch {
1175
+ }
1176
+ }
1177
+ }
1178
+ countUntrackedAddedLines(repoPath) {
1179
+ try {
1180
+ const output = this.getUntrackedDiff(repoPath, ["--shortstat"]);
1181
+ const match = output.match(/(\d+) insertion/);
1182
+ return match ? parseInt(match[1], 10) : 0;
1183
+ } catch (error) {
1184
+ console.error("Error counting untracked added lines:", error);
1185
+ return 0;
1186
+ }
1187
+ }
1064
1188
  getFullGitDiff(repoPath, defaultBranch) {
1065
1189
  try {
1066
1190
  const diffBase = this.getDiffBase(repoPath, defaultBranch);
1067
- return execFileSync2("git", ["diff", diffBase, "-M", "-C"], {
1191
+ const trackedDiff = execFileSync2("git", ["diff", diffBase, "-M", "-C"], {
1068
1192
  cwd: repoPath,
1069
1193
  encoding: "utf-8",
1070
1194
  stdio: ["pipe", "pipe", "pipe"]
1071
1195
  });
1196
+ return trackedDiff + this.getUntrackedAsDiff(repoPath);
1072
1197
  } catch {
1073
1198
  return "";
1074
1199
  }
1075
1200
  }
1201
+ getUntrackedAsDiff(repoPath) {
1202
+ try {
1203
+ return this.getUntrackedDiff(repoPath);
1204
+ } catch (error) {
1205
+ console.error("Error building untracked diff:", error);
1206
+ return "";
1207
+ }
1208
+ }
1076
1209
  getDiffBase(repoPath, defaultBranch) {
1077
1210
  const baseBranch = `origin/${defaultBranch}`;
1078
1211
  try {
@@ -2616,6 +2749,15 @@ var CodingAgentManager = class {
2616
2749
  await this.initialized;
2617
2750
  return this.messageQueue.enqueue(request);
2618
2751
  }
2752
+ emitContextUsage(payload) {
2753
+ const event = {
2754
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2755
+ type: CONTEXT_USAGE_EVENT_TYPE,
2756
+ payload: { ...payload }
2757
+ };
2758
+ this.onEvent(event);
2759
+ return event;
2760
+ }
2619
2761
  buildCombinedInstructions(customInstructions) {
2620
2762
  const startHooksInstruction = this.getStartHooksInstruction();
2621
2763
  const repositorySystemPromptInstruction = this.getRepositorySystemPromptInstruction();
@@ -2761,8 +2903,8 @@ var PromptStream = class {
2761
2903
  if (this.closed) {
2762
2904
  return Promise.resolve({ value: void 0, done: true });
2763
2905
  }
2764
- return new Promise((resolve2) => {
2765
- this.waiters.push(resolve2);
2906
+ return new Promise((resolve3) => {
2907
+ this.waiters.push(resolve3);
2766
2908
  });
2767
2909
  }
2768
2910
  };
@@ -2926,6 +3068,7 @@ var ClaudeManager = class _ClaudeManager extends CodingAgentManager {
2926
3068
  }
2927
3069
  }
2928
3070
  if (msg.type === "result") {
3071
+ await this.recordContextUsage(response);
2929
3072
  this.activePromptStream?.close();
2930
3073
  break;
2931
3074
  }
@@ -2947,6 +3090,45 @@ var ClaudeManager = class _ClaudeManager extends CodingAgentManager {
2947
3090
  }
2948
3091
  }
2949
3092
  }
3093
+ getContextUsageProvider() {
3094
+ return this.systemPromptOverride ? "relay" : "claude";
3095
+ }
3096
+ buildContextUsagePayload(usage) {
3097
+ const maxTokens = Number.isFinite(usage.maxTokens) ? usage.maxTokens : null;
3098
+ const percentage = Number.isFinite(usage.percentage) ? usage.percentage : maxTokens && maxTokens > 0 ? usage.totalTokens / maxTokens * 100 : 0;
3099
+ return {
3100
+ provider: this.getContextUsageProvider(),
3101
+ source: "claude_context",
3102
+ model: usage.model,
3103
+ totalTokens: usage.totalTokens,
3104
+ maxTokens,
3105
+ rawMaxTokens: Number.isFinite(usage.rawMaxTokens) ? usage.rawMaxTokens : maxTokens,
3106
+ percentage,
3107
+ categories: usage.categories.map((category) => ({
3108
+ name: category.name,
3109
+ tokens: category.tokens,
3110
+ percentage: maxTokens && maxTokens > 0 ? category.tokens / maxTokens * 100 : 0,
3111
+ color: category.color,
3112
+ ...category.isDeferred !== void 0 ? { isDeferred: category.isDeferred } : {}
3113
+ })),
3114
+ apiUsage: usage.apiUsage ? {
3115
+ inputTokens: usage.apiUsage.input_tokens,
3116
+ outputTokens: usage.apiUsage.output_tokens,
3117
+ cacheCreationInputTokens: usage.apiUsage.cache_creation_input_tokens,
3118
+ cacheReadInputTokens: usage.apiUsage.cache_read_input_tokens
3119
+ } : null,
3120
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
3121
+ };
3122
+ }
3123
+ async recordContextUsage(response) {
3124
+ try {
3125
+ const usage = await response.getContextUsage();
3126
+ const event = this.emitContextUsage(this.buildContextUsagePayload(usage));
3127
+ await appendFile4(this.historyFile, JSON.stringify(event) + "\n", "utf-8");
3128
+ } catch (error) {
3129
+ console.warn("[ClaudeManager] Failed to record context usage:", error instanceof Error ? error.message : error);
3130
+ }
3131
+ }
2950
3132
  /**
2951
3133
  * Determines if an error is an OAuth authentication failure from the Claude SDK.
2952
3134
  * Known patterns:
@@ -3037,7 +3219,7 @@ function isJsonlEvent2(value) {
3037
3219
  return typeof value.timestamp === "string" && typeof value.type === "string" && isRecord3(value.payload);
3038
3220
  }
3039
3221
  function sleep(ms) {
3040
- return new Promise((resolve2) => setTimeout(resolve2, ms));
3222
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
3041
3223
  }
3042
3224
  function extractRateLimitsSnapshot(parsed) {
3043
3225
  if (!isRecord3(parsed)) return null;
@@ -4567,19 +4749,290 @@ var ChatService = class {
4567
4749
  }
4568
4750
  };
4569
4751
 
4752
+ // src/services/repo-file-service.ts
4753
+ import { execFile as execFile2 } from "child_process";
4754
+ import { readFile as readFile9, realpath, stat as stat3 } from "fs/promises";
4755
+ import { join as join13, resolve, extname } from "path";
4756
+ var CACHE_TTL_MS = 3e4;
4757
+ var SEARCH_TIMEOUT_MS = 15e3;
4758
+ var MAX_CONTENT_BYTES = 256 * 1024;
4759
+ var DEFAULT_LIMIT = 50;
4760
+ var MAX_LIMIT = 5e3;
4761
+ var EXCLUDED_DIRS = /* @__PURE__ */ new Set([".git", "node_modules", ".next", "dist", "build", "out", "vendor", ".turbo", "__pycache__"]);
4762
+ function isBinaryExtension(filePath) {
4763
+ const ext = extname(filePath).toLowerCase();
4764
+ const binaryExts = /* @__PURE__ */ new Set([
4765
+ ".png",
4766
+ ".jpg",
4767
+ ".jpeg",
4768
+ ".gif",
4769
+ ".bmp",
4770
+ ".ico",
4771
+ ".webp",
4772
+ ".svg",
4773
+ ".woff",
4774
+ ".woff2",
4775
+ ".ttf",
4776
+ ".otf",
4777
+ ".eot",
4778
+ ".zip",
4779
+ ".tar",
4780
+ ".gz",
4781
+ ".bz2",
4782
+ ".7z",
4783
+ ".rar",
4784
+ ".pdf",
4785
+ ".doc",
4786
+ ".docx",
4787
+ ".xls",
4788
+ ".xlsx",
4789
+ ".exe",
4790
+ ".dll",
4791
+ ".so",
4792
+ ".dylib",
4793
+ ".o",
4794
+ ".a",
4795
+ ".mp3",
4796
+ ".mp4",
4797
+ ".avi",
4798
+ ".mov",
4799
+ ".wav",
4800
+ ".ogg",
4801
+ ".sqlite",
4802
+ ".db",
4803
+ ".lock"
4804
+ ]);
4805
+ return binaryExts.has(ext);
4806
+ }
4807
+ function isExcludedPath(filePath) {
4808
+ const segments = filePath.split("/");
4809
+ return segments.some((seg) => EXCLUDED_DIRS.has(seg));
4810
+ }
4811
+ function scoreMatch(query2, filePath) {
4812
+ const lowerQuery = query2.toLowerCase();
4813
+ const lowerPath = filePath.toLowerCase();
4814
+ const segments = lowerPath.split("/");
4815
+ const basename2 = segments[segments.length - 1] ?? "";
4816
+ if (basename2 === lowerQuery) return 100;
4817
+ if (basename2.startsWith(lowerQuery)) return 90;
4818
+ if (segments.some((seg) => seg === lowerQuery)) return 80;
4819
+ if (basename2.includes(lowerQuery)) return 70;
4820
+ if (segments.some((seg) => seg.includes(lowerQuery))) return 60;
4821
+ if (lowerPath.includes(lowerQuery)) return 50;
4822
+ return 0;
4823
+ }
4824
+ function execGitAsync(args, cwd, timeoutMs) {
4825
+ return new Promise((resolve3, reject) => {
4826
+ const child = execFile2("git", args, {
4827
+ cwd,
4828
+ encoding: "utf-8",
4829
+ timeout: timeoutMs,
4830
+ maxBuffer: 10 * 1024 * 1024
4831
+ }, (error, stdout) => {
4832
+ if (error) {
4833
+ reject(error);
4834
+ } else {
4835
+ resolve3(stdout);
4836
+ }
4837
+ });
4838
+ child.stdin?.end();
4839
+ });
4840
+ }
4841
+ var RepoFileService = class {
4842
+ cache = /* @__PURE__ */ new Map();
4843
+ gitService;
4844
+ indexInflight = null;
4845
+ constructor(gitService2) {
4846
+ this.gitService = gitService2;
4847
+ }
4848
+ async search(params) {
4849
+ const { q = "", prefix = "", refresh = false } = params;
4850
+ const limit = Math.min(Math.max(params.limit ?? DEFAULT_LIMIT, 1), MAX_LIMIT);
4851
+ const parsedOffset = params.cursor ? parseInt(Buffer.from(params.cursor, "base64").toString(), 10) : 0;
4852
+ const offset = Number.isNaN(parsedOffset) ? 0 : parsedOffset;
4853
+ let stale = false;
4854
+ let indexedAt = "";
4855
+ try {
4856
+ if (!this.indexInflight || refresh) {
4857
+ const inflight = this.indexAll(refresh);
4858
+ const wrapped = inflight.finally(() => {
4859
+ if (this.indexInflight === wrapped) this.indexInflight = null;
4860
+ });
4861
+ this.indexInflight = wrapped;
4862
+ }
4863
+ await this.indexInflight;
4864
+ } catch {
4865
+ stale = true;
4866
+ }
4867
+ const repos = await this.gitService.listRepositories();
4868
+ const multiRepo = repos.length > 1;
4869
+ const allResults = [];
4870
+ for (const repo of repos) {
4871
+ const cached = this.cache.get(repo.name);
4872
+ if (!cached) continue;
4873
+ if (!indexedAt || cached.timestamp > new Date(indexedAt).getTime()) {
4874
+ indexedAt = new Date(cached.timestamp).toISOString();
4875
+ }
4876
+ const isStale = Date.now() - cached.timestamp > CACHE_TTL_MS;
4877
+ if (isStale) stale = true;
4878
+ for (const filePath of cached.files) {
4879
+ if (prefix && !filePath.startsWith(prefix)) continue;
4880
+ let score = 0;
4881
+ if (q) {
4882
+ score = scoreMatch(q, filePath);
4883
+ if (score === 0) continue;
4884
+ }
4885
+ allResults.push({
4886
+ repoName: repo.name,
4887
+ path: filePath,
4888
+ displayPath: multiRepo ? `${repo.name}/${filePath}` : filePath,
4889
+ status: cached.changedFiles.has(filePath) ? "changed" : "untouched",
4890
+ score,
4891
+ indexedAt: new Date(cached.timestamp).toISOString(),
4892
+ stale: isStale
4893
+ });
4894
+ }
4895
+ }
4896
+ allResults.sort((a, b) => q ? b.score - a.score || a.displayPath.localeCompare(b.displayPath) : a.displayPath.localeCompare(b.displayPath));
4897
+ const paged = allResults.slice(offset, offset + limit);
4898
+ const nextOffset = offset + limit;
4899
+ const nextCursor = nextOffset < allResults.length ? Buffer.from(String(nextOffset)).toString("base64") : null;
4900
+ return {
4901
+ files: paged,
4902
+ nextCursor,
4903
+ indexedAt: indexedAt || (/* @__PURE__ */ new Date()).toISOString(),
4904
+ stale
4905
+ };
4906
+ }
4907
+ async readFile(repoName, filePath) {
4908
+ if (filePath.includes("..") || filePath.startsWith("/")) {
4909
+ return null;
4910
+ }
4911
+ const repos = await this.gitService.listRepositories();
4912
+ const repo = repos.find((r) => r.name === repoName);
4913
+ if (!repo) return null;
4914
+ try {
4915
+ const fullPath = await realpath(resolve(join13(repo.path, filePath)));
4916
+ const repoRoot = await realpath(repo.path);
4917
+ const repoPrefix = repoRoot.endsWith("/") ? repoRoot : repoRoot + "/";
4918
+ if (!fullPath.startsWith(repoPrefix) && fullPath !== repoRoot) return null;
4919
+ const fileStat = await stat3(fullPath);
4920
+ if (!fileStat.isFile()) return null;
4921
+ const sizeBytes = fileStat.size;
4922
+ if (isBinaryExtension(filePath)) {
4923
+ return {
4924
+ repoName,
4925
+ path: filePath,
4926
+ content: null,
4927
+ language: null,
4928
+ sizeBytes,
4929
+ binary: true,
4930
+ tooLarge: false
4931
+ };
4932
+ }
4933
+ if (sizeBytes > MAX_CONTENT_BYTES) {
4934
+ return {
4935
+ repoName,
4936
+ path: filePath,
4937
+ content: null,
4938
+ language: detectLanguageByPath(filePath),
4939
+ sizeBytes,
4940
+ binary: false,
4941
+ tooLarge: true
4942
+ };
4943
+ }
4944
+ const content = await readFile9(fullPath, "utf-8");
4945
+ return {
4946
+ repoName,
4947
+ path: filePath,
4948
+ content,
4949
+ language: detectLanguageByPath(filePath),
4950
+ sizeBytes,
4951
+ binary: false,
4952
+ tooLarge: false
4953
+ };
4954
+ } catch {
4955
+ return null;
4956
+ }
4957
+ }
4958
+ async indexAll(refresh) {
4959
+ const repos = await this.gitService.listRepositories();
4960
+ for (const repo of repos) {
4961
+ const cached = this.cache.get(repo.name);
4962
+ const isFresh = cached && Date.now() - cached.timestamp < CACHE_TTL_MS;
4963
+ if (isFresh && !refresh) continue;
4964
+ try {
4965
+ const [files, changedFiles] = await Promise.all([
4966
+ this.listRepoFiles(repo.path),
4967
+ this.listChangedFiles(repo.path, repo.defaultBranch)
4968
+ ]);
4969
+ this.cache.set(repo.name, {
4970
+ files,
4971
+ changedFiles,
4972
+ timestamp: Date.now()
4973
+ });
4974
+ } catch {
4975
+ }
4976
+ }
4977
+ }
4978
+ async listRepoFiles(repoPath) {
4979
+ const output = await execGitAsync(
4980
+ ["ls-files", "-z", "--cached", "--others", "--exclude-standard"],
4981
+ repoPath,
4982
+ SEARCH_TIMEOUT_MS
4983
+ );
4984
+ return output.split("\0").filter((f) => f.length > 0 && !isExcludedPath(f));
4985
+ }
4986
+ async listChangedFiles(repoPath, defaultBranch) {
4987
+ const changedFiles = /* @__PURE__ */ new Set();
4988
+ try {
4989
+ const mergeBase = await execGitAsync(
4990
+ ["merge-base", `origin/${defaultBranch}`, "HEAD"],
4991
+ repoPath,
4992
+ SEARCH_TIMEOUT_MS
4993
+ );
4994
+ const diffOutput = await execGitAsync(
4995
+ ["diff", "--name-only", "-M", mergeBase.trim()],
4996
+ repoPath,
4997
+ SEARCH_TIMEOUT_MS
4998
+ );
4999
+ for (const file of diffOutput.split("\n")) {
5000
+ if (file.length > 0) {
5001
+ changedFiles.add(file);
5002
+ }
5003
+ }
5004
+ } catch {
5005
+ }
5006
+ try {
5007
+ const untrackedOutput = await execGitAsync(
5008
+ ["ls-files", "--others", "--exclude-standard"],
5009
+ repoPath,
5010
+ SEARCH_TIMEOUT_MS
5011
+ );
5012
+ for (const file of untrackedOutput.split("\n")) {
5013
+ if (file.length > 0) {
5014
+ changedFiles.add(file);
5015
+ }
5016
+ }
5017
+ } catch {
5018
+ }
5019
+ return changedFiles;
5020
+ }
5021
+ };
5022
+
4570
5023
  // src/v1-routes.ts
4571
5024
  import { Hono } from "hono";
4572
5025
  import { z as z2 } from "zod";
4573
- import { readdir as readdir6, stat as stat3, readFile as readFile12 } from "fs/promises";
4574
- import { join as join16, resolve } from "path";
5026
+ import { readdir as readdir6, stat as stat4, readFile as readFile13 } from "fs/promises";
5027
+ import { join as join17, resolve as resolve2 } from "path";
4575
5028
 
4576
5029
  // src/services/plan-service.ts
4577
- import { readdir as readdir4, readFile as readFile9 } from "fs/promises";
5030
+ import { readdir as readdir4, readFile as readFile10 } from "fs/promises";
4578
5031
  import { homedir as homedir10 } from "os";
4579
- import { basename, join as join13 } from "path";
5032
+ import { basename, join as join14 } from "path";
4580
5033
  var PLAN_DIRECTORIES = [
4581
- join13(homedir10(), ".claude", "plans"),
4582
- join13(homedir10(), ".replicas", "plans")
5034
+ join14(homedir10(), ".claude", "plans"),
5035
+ join14(homedir10(), ".replicas", "plans")
4583
5036
  ];
4584
5037
  function isMarkdownFile(filename) {
4585
5038
  return filename.toLowerCase().endsWith(".md");
@@ -4613,9 +5066,9 @@ var PlanService = class {
4613
5066
  return null;
4614
5067
  }
4615
5068
  for (const directory of PLAN_DIRECTORIES) {
4616
- const filePath = join13(directory, safeFilename);
5069
+ const filePath = join14(directory, safeFilename);
4617
5070
  try {
4618
- const content = await readFile9(filePath, "utf-8");
5071
+ const content = await readFile10(filePath, "utf-8");
4619
5072
  return { filename: safeFilename, content };
4620
5073
  } catch {
4621
5074
  }
@@ -4627,15 +5080,15 @@ var planService = new PlanService();
4627
5080
 
4628
5081
  // src/services/warm-hooks-service.ts
4629
5082
  import { spawn } from "child_process";
4630
- import { readFile as readFile11 } from "fs/promises";
5083
+ import { readFile as readFile12 } from "fs/promises";
4631
5084
  import { existsSync as existsSync8 } from "fs";
4632
- import { join as join15 } from "path";
5085
+ import { join as join16 } from "path";
4633
5086
 
4634
5087
  // src/services/warm-hook-logs-service.ts
4635
5088
  import { createHash as createHash2 } from "crypto";
4636
- import { mkdir as mkdir11, readFile as readFile10, writeFile as writeFile9, readdir as readdir5 } from "fs/promises";
4637
- import { join as join14 } from "path";
4638
- var LOGS_DIR2 = join14(SANDBOX_PATHS.REPLICAS_DIR, "warm-hook-logs");
5089
+ import { mkdir as mkdir11, readFile as readFile11, writeFile as writeFile9, readdir as readdir5 } from "fs/promises";
5090
+ import { join as join15 } from "path";
5091
+ var LOGS_DIR2 = join15(SANDBOX_PATHS.REPLICAS_DIR, "warm-hook-logs");
4639
5092
  function sanitizeFilename2(name) {
4640
5093
  const safe = name.replace(/[^a-zA-Z0-9._-]/g, "_");
4641
5094
  const hash = createHash2("sha256").update(name).digest("hex").slice(0, 8);
@@ -4665,7 +5118,7 @@ var WarmHookLogsService = class {
4665
5118
  hookName: "organization",
4666
5119
  ...entry
4667
5120
  };
4668
- await writeFile9(join14(LOGS_DIR2, globalFilename()), `${JSON.stringify(log, null, 2)}
5121
+ await writeFile9(join15(LOGS_DIR2, globalFilename()), `${JSON.stringify(log, null, 2)}
4669
5122
  `, "utf-8");
4670
5123
  }
4671
5124
  async saveEnvironmentHookLog(entry) {
@@ -4675,7 +5128,7 @@ var WarmHookLogsService = class {
4675
5128
  hookName: "environment",
4676
5129
  ...entry
4677
5130
  };
4678
- await writeFile9(join14(LOGS_DIR2, environmentFilename()), `${JSON.stringify(log, null, 2)}
5131
+ await writeFile9(join15(LOGS_DIR2, environmentFilename()), `${JSON.stringify(log, null, 2)}
4679
5132
  `, "utf-8");
4680
5133
  }
4681
5134
  async saveRepoHookLog(repoName, entry) {
@@ -4685,7 +5138,7 @@ var WarmHookLogsService = class {
4685
5138
  hookName: repoName,
4686
5139
  ...entry
4687
5140
  };
4688
- await writeFile9(join14(LOGS_DIR2, repoFilename2(repoName)), `${JSON.stringify(log, null, 2)}
5141
+ await writeFile9(join15(LOGS_DIR2, repoFilename2(repoName)), `${JSON.stringify(log, null, 2)}
4689
5142
  `, "utf-8");
4690
5143
  }
4691
5144
  async getAllLogs() {
@@ -4704,7 +5157,7 @@ var WarmHookLogsService = class {
4704
5157
  continue;
4705
5158
  }
4706
5159
  try {
4707
- const raw = await readFile10(join14(LOGS_DIR2, file), "utf-8");
5160
+ const raw = await readFile11(join15(LOGS_DIR2, file), "utf-8");
4708
5161
  const stored = JSON.parse(raw);
4709
5162
  logs.push(withPreview2(stored));
4710
5163
  } catch {
@@ -4722,7 +5175,7 @@ var WarmHookLogsService = class {
4722
5175
  async getFullOutput(hookType, hookName) {
4723
5176
  const filename = hookType === "global" ? globalFilename() : hookType === "environment" ? environmentFilename() : repoFilename2(hookName);
4724
5177
  try {
4725
- const raw = await readFile10(join14(LOGS_DIR2, filename), "utf-8");
5178
+ const raw = await readFile11(join15(LOGS_DIR2, filename), "utf-8");
4726
5179
  const stored = JSON.parse(raw);
4727
5180
  if (stored.hookType !== hookType || stored.hookName !== hookName) {
4728
5181
  return null;
@@ -4741,12 +5194,12 @@ var warmHookLogsService = new WarmHookLogsService();
4741
5194
  // src/services/warm-hooks-service.ts
4742
5195
  async function readRepoWarmHook(repoPath) {
4743
5196
  for (const filename of REPLICAS_CONFIG_FILENAMES) {
4744
- const configPath = join15(repoPath, filename);
5197
+ const configPath = join16(repoPath, filename);
4745
5198
  if (!existsSync8(configPath)) {
4746
5199
  continue;
4747
5200
  }
4748
5201
  try {
4749
- const raw = await readFile11(configPath, "utf-8");
5202
+ const raw = await readFile12(configPath, "utf-8");
4750
5203
  const config = parseReplicasConfigString(raw, filename);
4751
5204
  if (!config.warmHook) {
4752
5205
  return null;
@@ -4792,7 +5245,7 @@ async function executeHookScriptStreaming(params) {
4792
5245
  let bufferExceeded = false;
4793
5246
  params.onChunk(`$ ${params.label}
4794
5247
  `);
4795
- return new Promise((resolve2) => {
5248
+ return new Promise((resolve3) => {
4796
5249
  const proc = spawn("bash", ["-lc", params.content], {
4797
5250
  cwd: params.cwd,
4798
5251
  env: process.env,
@@ -4825,7 +5278,7 @@ async function executeHookScriptStreaming(params) {
4825
5278
  proc.on("close", (code) => {
4826
5279
  clearTimeout(timer);
4827
5280
  const output = [`$ ${params.label}`, ...outputParts].join("");
4828
- resolve2({
5281
+ resolve3({
4829
5282
  exitCode: bufferExceeded ? 1 : code ?? 1,
4830
5283
  output,
4831
5284
  timedOut: killed && !bufferExceeded
@@ -4835,7 +5288,7 @@ async function executeHookScriptStreaming(params) {
4835
5288
  clearTimeout(timer);
4836
5289
  const output = [`$ ${params.label}
4837
5290
  `, ...outputParts, err.message].join("");
4838
- resolve2({
5291
+ resolve3({
4839
5292
  exitCode: 1,
4840
5293
  output,
4841
5294
  timedOut: false
@@ -5236,6 +5689,28 @@ function createV1Routes(deps) {
5236
5689
  };
5237
5690
  return c.json(response);
5238
5691
  });
5692
+ app2.get("/repo-files/search", async (c) => {
5693
+ const q = c.req.query("q") ?? "";
5694
+ const prefix = c.req.query("prefix") ?? "";
5695
+ const rawLimit = c.req.query("limit") ? parseInt(c.req.query("limit"), 10) : void 0;
5696
+ const limit = rawLimit !== void 0 && !Number.isNaN(rawLimit) ? rawLimit : void 0;
5697
+ const cursor = c.req.query("cursor") ?? void 0;
5698
+ const refresh = c.req.query("refresh") === "true";
5699
+ const result = await deps.repoFileService.search({ q, prefix, limit, cursor, refresh });
5700
+ return c.json(result);
5701
+ });
5702
+ app2.get("/repo-files/content", async (c) => {
5703
+ const repoName = c.req.query("repoName");
5704
+ const path4 = c.req.query("path");
5705
+ if (!repoName || !path4) {
5706
+ return c.json(jsonError("repoName and path are required"), 400);
5707
+ }
5708
+ const result = await deps.repoFileService.readFile(repoName, path4);
5709
+ if (!result) {
5710
+ return c.json(jsonError("File not found"), 404);
5711
+ }
5712
+ return c.json(result);
5713
+ });
5239
5714
  app2.get("/plans", async (c) => {
5240
5715
  try {
5241
5716
  const plans = await planService.listPlans();
@@ -5500,8 +5975,8 @@ function createV1Routes(deps) {
5500
5975
  const logFiles = files.filter((f) => f.endsWith(".log"));
5501
5976
  const sessions = await Promise.all(
5502
5977
  logFiles.map(async (filename) => {
5503
- const filePath = join16(LOG_DIR, filename);
5504
- const fileStat = await stat3(filePath);
5978
+ const filePath = join17(LOG_DIR, filename);
5979
+ const fileStat = await stat4(filePath);
5505
5980
  const sessionId = filename.replace(/\.log$/, "");
5506
5981
  return {
5507
5982
  sessionId,
@@ -5529,15 +6004,15 @@ function createV1Routes(deps) {
5529
6004
  if (!sessionId || /[/\\]/.test(sessionId) || sessionId.includes("..")) {
5530
6005
  return c.json(jsonError("Invalid session ID"), 400);
5531
6006
  }
5532
- const filePath = resolve(LOG_DIR, `${sessionId}.log`);
5533
- if (!filePath.startsWith(resolve(LOG_DIR))) {
6007
+ const filePath = resolve2(LOG_DIR, `${sessionId}.log`);
6008
+ if (!filePath.startsWith(resolve2(LOG_DIR))) {
5534
6009
  return c.json(jsonError("Invalid session ID"), 400);
5535
6010
  }
5536
6011
  const offset = parseInt(c.req.query("offset") || "0", 10);
5537
6012
  const limit = Math.min(parseInt(c.req.query("limit") || "500", 10), 5e3);
5538
6013
  let content;
5539
6014
  try {
5540
- content = await readFile12(filePath, "utf-8");
6015
+ content = await readFile13(filePath, "utf-8");
5541
6016
  } catch {
5542
6017
  return c.json(jsonError("Log session not found"), 404);
5543
6018
  }
@@ -5611,7 +6086,7 @@ app.get("/health", async (c) => {
5611
6086
  return c.json({ status: "initializing", timestamp: (/* @__PURE__ */ new Date()).toISOString() }, 503);
5612
6087
  }
5613
6088
  try {
5614
- const logContent = await readFile13("/var/log/cloud-init-output.log", "utf-8");
6089
+ const logContent = await readFile14("/var/log/cloud-init-output.log", "utf-8");
5615
6090
  let status;
5616
6091
  if (logContent.includes(COMPLETION_MESSAGE)) {
5617
6092
  status = "active";
@@ -5653,7 +6128,8 @@ app.get("/token-refresh/health", async (c) => {
5653
6128
  codex: codexTokenManager.getHealthStatus()
5654
6129
  });
5655
6130
  });
5656
- app.route("/", createV1Routes({ chatService }));
6131
+ var repoFileService = new RepoFileService(gitService);
6132
+ app.route("/", createV1Routes({ chatService, repoFileService }));
5657
6133
  var port = ENGINE_ENV.REPLICAS_ENGINE_PORT;
5658
6134
  function startStatusBroadcaster() {
5659
6135
  let previousRepoStatus = "";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "replicas-engine",
3
- "version": "0.1.166",
3
+ "version": "0.1.168",
4
4
  "description": "Lightweight API server for Replicas workspaces",
5
5
  "type": "module",
6
6
  "main": "dist/src/index.js",