@unerr-ai/unerr 0.1.7 → 0.1.9

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 (3) hide show
  1. package/README.md +9 -2
  2. package/dist/cli.js +1467 -296
  3. package/package.json +9 -2
package/dist/cli.js CHANGED
@@ -245,16 +245,16 @@ async function getParser(language) {
245
245
  const parser = new TreeSitter();
246
246
  const langFile = `tree-sitter-${language}.wasm`;
247
247
  try {
248
- const { join: join83 } = await import("path");
248
+ const { join: join85 } = await import("path");
249
249
  const { existsSync: existsSync82 } = await import("fs");
250
250
  const possiblePaths = [
251
- join83(
251
+ join85(
252
252
  process.cwd(),
253
253
  "node_modules",
254
254
  `tree-sitter-${language}`,
255
255
  langFile
256
256
  ),
257
- join83(process.cwd(), "node_modules", "web-tree-sitter", langFile)
257
+ join85(process.cwd(), "node_modules", "web-tree-sitter", langFile)
258
258
  ];
259
259
  let wasmPath = null;
260
260
  for (const p of possiblePaths) {
@@ -5770,6 +5770,7 @@ var init_startup_log = __esm({
5770
5770
  // src/config/settings.ts
5771
5771
  var settings_exports = {};
5772
5772
  __export(settings_exports, {
5773
+ FetchUrlConfigSchema: () => FetchUrlConfigSchema,
5773
5774
  LocalLlmConfigSchema: () => LocalLlmConfigSchema,
5774
5775
  SettingsSchema: () => SettingsSchema,
5775
5776
  loadSettings: () => loadSettings,
@@ -5852,7 +5853,7 @@ function loadSettings(cwd) {
5852
5853
  };
5853
5854
  return SettingsSchema.parse(merged);
5854
5855
  }
5855
- var LlmProviderEnum, EndpointConfigSchema, LocalLlmConfigSchema, LlmConfigSchema, SettingsSchema, DEFAULTS;
5856
+ var LlmProviderEnum, EndpointConfigSchema, LocalLlmConfigSchema, LlmConfigSchema, FetchUrlConfigSchema, SettingsSchema, DEFAULTS;
5856
5857
  var init_settings = __esm({
5857
5858
  "src/config/settings.ts"() {
5858
5859
  "use strict";
@@ -5885,6 +5886,17 @@ var init_settings = __esm({
5885
5886
  apiKey: z.string().optional(),
5886
5887
  baseUrl: z.string().optional()
5887
5888
  });
5889
+ FetchUrlConfigSchema = z.object({
5890
+ playwright: z.object({
5891
+ enabled: z.boolean().default(false),
5892
+ timeoutMs: z.number().int().min(1e3).max(6e4).default(15e3),
5893
+ waitUntil: z.enum(["load", "domcontentloaded", "networkidle"]).default("networkidle")
5894
+ }).default(() => ({
5895
+ enabled: false,
5896
+ timeoutMs: 15e3,
5897
+ waitUntil: "networkidle"
5898
+ }))
5899
+ });
5888
5900
  SettingsSchema = z.object({
5889
5901
  /** Default Claude model for interactive sessions */
5890
5902
  model: z.string().default("claude-sonnet-4-20250514"),
@@ -5899,7 +5911,15 @@ var init_settings = __esm({
5899
5911
  /** AI SDK LLM configuration (Sprint D — unified multi-provider) */
5900
5912
  llm: LlmConfigSchema.optional(),
5901
5913
  /** BYO-LLM configuration for local LLM providers */
5902
- localLlm: LocalLlmConfigSchema.optional()
5914
+ localLlm: LocalLlmConfigSchema.optional(),
5915
+ /** fetch_url runtime config (Playwright SPA fallback, etc.) */
5916
+ fetchUrl: FetchUrlConfigSchema.default(() => ({
5917
+ playwright: {
5918
+ enabled: false,
5919
+ timeoutMs: 15e3,
5920
+ waitUntil: "networkidle"
5921
+ }
5922
+ }))
5903
5923
  });
5904
5924
  DEFAULTS = SettingsSchema.parse({});
5905
5925
  }
@@ -6551,6 +6571,19 @@ CREATE TABLE IF NOT EXISTS meta (
6551
6571
  key TEXT PRIMARY KEY,
6552
6572
  value TEXT NOT NULL
6553
6573
  );
6574
+
6575
+ CREATE TABLE IF NOT EXISTS fetch_cache (
6576
+ url TEXT PRIMARY KEY,
6577
+ content_hash TEXT NOT NULL,
6578
+ markdown TEXT NOT NULL,
6579
+ title TEXT NOT NULL,
6580
+ extractor TEXT NOT NULL,
6581
+ raw_bytes INTEGER NOT NULL,
6582
+ compressed_bytes INTEGER NOT NULL,
6583
+ fetched_at INTEGER NOT NULL,
6584
+ hit_count INTEGER NOT NULL DEFAULT 0
6585
+ );
6586
+ CREATE INDEX IF NOT EXISTS idx_fetch_cache_fetched ON fetch_cache(fetched_at);
6554
6587
  `;
6555
6588
  SCHEMA_VERSION = "1";
6556
6589
  MetricsStore = class {
@@ -6660,9 +6693,39 @@ CREATE TABLE IF NOT EXISTS meta (
6660
6693
  `),
6661
6694
  allSessionSummaries: this.db.prepare(`
6662
6695
  SELECT * FROM session_summaries ORDER BY ended_at DESC
6696
+ `),
6697
+ upsertFetchCache: this.db.prepare(`
6698
+ INSERT INTO fetch_cache
6699
+ (url, content_hash, markdown, title, extractor,
6700
+ raw_bytes, compressed_bytes, fetched_at, hit_count)
6701
+ VALUES (@url, @content_hash, @markdown, @title, @extractor,
6702
+ @raw_bytes, @compressed_bytes, @fetched_at, 0)
6703
+ ON CONFLICT(url) DO UPDATE SET
6704
+ content_hash = excluded.content_hash,
6705
+ markdown = excluded.markdown,
6706
+ title = excluded.title,
6707
+ extractor = excluded.extractor,
6708
+ raw_bytes = excluded.raw_bytes,
6709
+ compressed_bytes = excluded.compressed_bytes,
6710
+ fetched_at = excluded.fetched_at
6711
+ `),
6712
+ getFetchCache: this.db.prepare(`
6713
+ SELECT * FROM fetch_cache WHERE url = @url
6714
+ `),
6715
+ bumpFetchCacheHit: this.db.prepare(`
6716
+ UPDATE fetch_cache SET hit_count = hit_count + 1 WHERE url = @url
6663
6717
  `)
6664
6718
  };
6665
6719
  }
6720
+ upsertFetchCacheRow(row) {
6721
+ this.stmt.upsertFetchCache.run(row);
6722
+ }
6723
+ getFetchCacheRow(url) {
6724
+ return this.stmt.getFetchCache.get({ url }) ?? null;
6725
+ }
6726
+ bumpFetchCacheHitFor(url) {
6727
+ this.stmt.bumpFetchCacheHit.run({ url });
6728
+ }
6666
6729
  // ── Writes ──────────────────────────────────────────────────────────
6667
6730
  insertCompression(row) {
6668
6731
  return Number(this.stmt.insertCompression.run(row).lastInsertRowid);
@@ -10717,6 +10780,7 @@ var init_protocol = __esm({
10717
10780
  var registry_exports = {};
10718
10781
  __export(registry_exports, {
10719
10782
  addRepo: () => addRepo,
10783
+ clearNeedsInputKey: () => clearNeedsInputKey,
10720
10784
  deriveLabel: () => deriveLabel,
10721
10785
  detectChildConflicts: () => detectChildConflicts,
10722
10786
  detectParentConflict: () => detectParentConflict,
@@ -10887,6 +10951,13 @@ function writeNeedsInput(repoPath, signals) {
10887
10951
  `
10888
10952
  );
10889
10953
  }
10954
+ function clearNeedsInputKey(repoPath, key) {
10955
+ const existing = readNeedsInput(repoPath);
10956
+ if (existing.length === 0) return;
10957
+ const remaining = existing.filter((s) => s.key !== key);
10958
+ if (remaining.length === existing.length) return;
10959
+ writeNeedsInput(repoPath, remaining);
10960
+ }
10890
10961
  function buildSettings(partial) {
10891
10962
  const s = {};
10892
10963
  for (const [k, v] of Object.entries(partial)) {
@@ -10974,6 +11045,7 @@ async function enrichWithScip(files, projectRoot, existingEdges, entities, optio
10974
11045
  const outputDir = join29(projectRoot, ".unerr", "scip");
10975
11046
  const binaryPath = binaryInfo.path ?? binaryInfo.binaryName;
10976
11047
  let extraArgs;
11048
+ let javaPlan = null;
10977
11049
  if (language === "typescript") {
10978
11050
  if (!existsSync25(join29(projectRoot, "tsconfig.json")) && !existsSync25(join29(projectRoot, "jsconfig.json"))) {
10979
11051
  log6.info(
@@ -10983,9 +11055,12 @@ async function enrichWithScip(files, projectRoot, existingEdges, entities, optio
10983
11055
  }
10984
11056
  }
10985
11057
  if (language === "java") {
10986
- const buildToolResult = await resolveJavaBuildTool(projectRoot, options);
10987
- if (buildToolResult) {
10988
- extraArgs = buildToolResult.extraArgs;
11058
+ javaPlan = await resolveJavaBuildTool(projectRoot, options);
11059
+ if (!javaPlan) {
11060
+ log6.info(
11061
+ "SCIP skipping java: no recognized build tool (Maven/Gradle/Bazel/Sbt) detected"
11062
+ );
11063
+ continue;
10989
11064
  }
10990
11065
  }
10991
11066
  if (language === "cpp") {
@@ -11002,7 +11077,13 @@ async function enrichWithScip(files, projectRoot, existingEdges, entities, optio
11002
11077
  log6.info(
11003
11078
  `SCIP enrichment: ${language} (${binaryInfo.bundled ? "bundled" : "external"}: ${binaryInfo.binaryName})`
11004
11079
  );
11005
- const runResult = await runScipIndexer({
11080
+ const runResult = language === "java" && javaPlan ? await runJavaScipWithCascade({
11081
+ binaryPath,
11082
+ projectRoot,
11083
+ outputDir,
11084
+ timeoutMs: 3e4,
11085
+ plan: javaPlan
11086
+ }) : await runScipIndexer({
11006
11087
  language,
11007
11088
  binaryPath,
11008
11089
  projectRoot,
@@ -11163,29 +11244,106 @@ async function resolveJavaBuildTool(projectRoot, _options) {
11163
11244
  const stored = getStoredBuildTool(projectRoot);
11164
11245
  if (stored && detected.includes(stored)) {
11165
11246
  log6.info(`Java build tool: ${stored} (from config)`);
11166
- return { tool: stored, extraArgs: [`--build-tool=${stored}`] };
11247
+ return {
11248
+ primary: stored,
11249
+ alternatives: [],
11250
+ reason: "from config",
11251
+ ambiguous: false,
11252
+ fromConfig: true
11253
+ };
11167
11254
  }
11168
11255
  const choice = chooseBuildTool(detected, projectRoot);
11169
11256
  if (!choice) return null;
11170
- storeBuildTool(projectRoot, choice.tool);
11171
11257
  if (choice.ambiguous) {
11172
- const signal = {
11173
- type: "needs_input",
11174
- key: "javaBuildTool",
11175
- auto: choice.tool,
11176
- alternatives: choice.alternatives,
11177
- reason: choice.reason
11178
- };
11179
- writeNeedsInput(projectRoot, [signal]);
11180
11258
  log6.info(
11181
- `Java build tool: ${choice.tool} (auto: ${choice.reason}). Override: unerr daemon config . --java-build-tool=<tool>`
11259
+ `Java build tool: ${choice.tool} (auto: ${choice.reason}; fallbacks: ${choice.alternatives.join(", ")})`
11182
11260
  );
11183
11261
  } else {
11184
11262
  log6.info(`Java build tool: ${choice.tool} (${choice.reason})`);
11185
11263
  }
11186
11264
  return {
11187
- tool: choice.tool,
11188
- extraArgs: [`--build-tool=${choice.tool}`]
11265
+ primary: choice.tool,
11266
+ alternatives: choice.alternatives,
11267
+ reason: choice.reason,
11268
+ ambiguous: choice.ambiguous,
11269
+ fromConfig: false
11270
+ };
11271
+ }
11272
+ function parseScipJavaBuildToolMismatch(error) {
11273
+ if (!error) return [];
11274
+ const match = error.match(
11275
+ /Automatically detected the build tool\(s\) ([^.]+?) but none of them match/i
11276
+ );
11277
+ if (!match?.[1]) return [];
11278
+ const known = new Set(JAVA_BUILD_TOOLS);
11279
+ return match[1].split(/[,\s]+/).map((s) => s.trim()).filter((s) => known.has(s));
11280
+ }
11281
+ async function runJavaScipWithCascade(opts) {
11282
+ const candidates = [
11283
+ opts.plan.primary,
11284
+ ...opts.plan.alternatives
11285
+ ];
11286
+ const attempted = [];
11287
+ let lastResult = null;
11288
+ for (let i = 0; i < candidates.length; i++) {
11289
+ const tool = candidates[i];
11290
+ attempted.push(tool);
11291
+ if (i === 0) {
11292
+ log6.info(`scip-java attempting --build-tool=${tool}`);
11293
+ } else {
11294
+ log6.info(
11295
+ `scip-java retrying --build-tool=${tool} (fallback #${i}, after ${attempted.slice(0, -1).join(" \u2192 ")})`
11296
+ );
11297
+ }
11298
+ const result = await runScipIndexer({
11299
+ language: "java",
11300
+ binaryPath: opts.binaryPath,
11301
+ projectRoot: opts.projectRoot,
11302
+ outputDir: opts.outputDir,
11303
+ timeoutMs: opts.timeoutMs,
11304
+ extraArgs: [`--build-tool=${tool}`]
11305
+ });
11306
+ lastResult = result;
11307
+ if (result.success) {
11308
+ storeBuildTool(opts.projectRoot, tool);
11309
+ clearNeedsInputKey(opts.projectRoot, "javaBuildTool");
11310
+ if (attempted.length > 1) {
11311
+ log6.info(
11312
+ `scip-java cascade resolved: ${tool} (${attempted.join(" \u2192 ")})`
11313
+ );
11314
+ }
11315
+ return result;
11316
+ }
11317
+ const detectedByScip = parseScipJavaBuildToolMismatch(result.error);
11318
+ if (detectedByScip.length === 0) {
11319
+ return result;
11320
+ }
11321
+ const detectedSet = new Set(detectedByScip);
11322
+ const remaining = candidates.slice(i + 1).filter((t) => detectedSet.has(t) && !attempted.includes(t));
11323
+ if (remaining.length === 0) {
11324
+ log6.warn(
11325
+ `scip-java cascade exhausted: tried ${attempted.join(" \u2192 ")}; scip-java detected ${detectedByScip.join(", ")} but no viable fallback remains`
11326
+ );
11327
+ const signal = {
11328
+ type: "needs_input",
11329
+ key: "javaBuildTool",
11330
+ auto: opts.plan.primary,
11331
+ alternatives: opts.plan.alternatives,
11332
+ reason: `cascade exhausted (tried ${attempted.join(" \u2192 ")}); override with unerr pm config <repo> --java-build-tool=<tool>`
11333
+ };
11334
+ writeNeedsInput(opts.projectRoot, [signal]);
11335
+ return result;
11336
+ }
11337
+ candidates.splice(i + 1, candidates.length - i - 1, ...remaining);
11338
+ log6.warn(
11339
+ `scip-java rejected --build-tool=${tool} (scip-java detected: ${detectedByScip.join(", ")}); cascading to ${candidates[i + 1]}`
11340
+ );
11341
+ }
11342
+ return lastResult ?? {
11343
+ success: false,
11344
+ outputPath: null,
11345
+ durationMs: 0,
11346
+ error: "no Java build tool candidates available"
11189
11347
  };
11190
11348
  }
11191
11349
  function resolveCompileCommandsJson(projectRoot) {
@@ -11270,6 +11428,7 @@ var init_orchestrator = __esm({
11270
11428
  init_downloader();
11271
11429
  init_merger();
11272
11430
  init_runner();
11431
+ init_protocol();
11273
11432
  init_registry();
11274
11433
  log6 = createModuleLogger("scip-orchestrator");
11275
11434
  BUILD_TOOL_FILES = {
@@ -11813,6 +11972,7 @@ __export(local_indexer_exports, {
11813
11972
  discoverSourceFiles: () => discoverSourceFiles,
11814
11973
  indexLocalProject: () => indexLocalProject,
11815
11974
  reindexFile: () => reindexFile,
11975
+ removeOrphanedEntities: () => removeOrphanedEntities,
11816
11976
  resolveImportSourceToFile: () => resolveImportSourceToFile,
11817
11977
  runCommunityDetection: () => runCommunityDetection,
11818
11978
  runConventionDetection: () => runConventionDetection
@@ -12830,7 +12990,45 @@ async function removeOrphanedEntities(graphStore, liveKeys) {
12830
12990
  const orphanKeys = existingKeys.filter((k) => !liveKeys.has(k));
12831
12991
  if (orphanKeys.length === 0) return;
12832
12992
  log7.info(`Removing ${orphanKeys.length} orphaned entities from graph`);
12833
- for (const key of orphanKeys) {
12993
+ for (let i = 0; i < orphanKeys.length; i += ORPHAN_BATCH_SIZE) {
12994
+ const chunk = orphanKeys.slice(i, i + ORPHAN_BATCH_SIZE);
12995
+ const keyRows = chunk.map((k) => [k]);
12996
+ try {
12997
+ await db.run(
12998
+ `orphan[k] <- $keys
12999
+ ?[from_key, to_key, type] := orphan[from_key], *edges{from_key, to_key, type}
13000
+ :rm edges {from_key, to_key, type}`,
13001
+ { keys: keyRows }
13002
+ );
13003
+ await db.run(
13004
+ `orphan[k] <- $keys
13005
+ ?[from_key, to_key, type] := orphan[to_key], *edges{from_key, to_key, type}
13006
+ :rm edges {from_key, to_key, type}`,
13007
+ { keys: keyRows }
13008
+ );
13009
+ await db.run(
13010
+ `orphan[k] <- $keys
13011
+ ?[token, entity_key] := orphan[entity_key], *search_tokens[token, entity_key]
13012
+ :rm search_tokens {token, entity_key}`,
13013
+ { keys: keyRows }
13014
+ );
13015
+ await db.run(
13016
+ `orphan[k] <- $keys
13017
+ ?[file_path, entity_key] := orphan[entity_key], *file_index[file_path, entity_key]
13018
+ :rm file_index {file_path, entity_key}`,
13019
+ { keys: keyRows }
13020
+ );
13021
+ await db.run("?[key] <- $keys :rm entities {key}", { keys: keyRows });
13022
+ } catch (err) {
13023
+ log7.info(
13024
+ `Batched orphan removal failed for chunk of ${chunk.length} (${formatUnknownError(err)}); falling back to per-key`
13025
+ );
13026
+ await removeOrphansPerKey(db, chunk);
13027
+ }
13028
+ }
13029
+ }
13030
+ async function removeOrphansPerKey(db, keys) {
13031
+ for (const key of keys) {
12834
13032
  try {
12835
13033
  await db.run(
12836
13034
  "?[from_key, to_key, type] := *edges{from_key, to_key, type}, from_key = $key :rm edges {from_key, to_key, type}",
@@ -12987,7 +13185,7 @@ async function indexDocumentFiles(projectRoot, graphStore) {
12987
13185
  }
12988
13186
  return docKeys;
12989
13187
  }
12990
- var INDEXABLE_EXTENSIONS, EXCLUDED_DIRS2, EXCLUDED_PATH_PREFIXES, MAX_FILE_SIZE, DOCUMENT_EXTENSIONS, DOCUMENT_NAMES, CI_CD_DIRS, DOC_READ_LIMIT, log7, DOC_TOKEN_PATTERNS;
13188
+ var INDEXABLE_EXTENSIONS, EXCLUDED_DIRS2, EXCLUDED_PATH_PREFIXES, MAX_FILE_SIZE, DOCUMENT_EXTENSIONS, DOCUMENT_NAMES, CI_CD_DIRS, DOC_READ_LIMIT, log7, ORPHAN_BATCH_SIZE, DOC_TOKEN_PATTERNS;
12991
13189
  var init_local_indexer = __esm({
12992
13190
  "src/intelligence/local-indexer.ts"() {
12993
13191
  "use strict";
@@ -13136,6 +13334,7 @@ var init_local_indexer = __esm({
13136
13334
  `);
13137
13335
  }
13138
13336
  };
13337
+ ORPHAN_BATCH_SIZE = 1e3;
13139
13338
  DOC_TOKEN_PATTERNS = {
13140
13339
  ".md": /^#{1,6}\s+(.+)$/gm,
13141
13340
  ".mdx": /^#{1,6}\s+(.+)$/gm,
@@ -14771,7 +14970,7 @@ function installFileLogger(opts) {
14771
14970
  bytesWritten = statSync9(filePath).size;
14772
14971
  } catch {
14773
14972
  }
14774
- const linePrefix = usePrefix ? `[pid=${process.pid} sid=${getOrCreateSid()}] ` : "";
14973
+ const idSuffix = usePrefix ? ` pid=${process.pid} sid=${getOrCreateSid()}] ` : "";
14775
14974
  const original = process.stderr.write;
14776
14975
  const bound = original.bind(process.stderr);
14777
14976
  const wrapped = ((chunk, ...rest) => {
@@ -14779,6 +14978,7 @@ function installFileLogger(opts) {
14779
14978
  try {
14780
14979
  const text2 = typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf-8");
14781
14980
  const clean = stripAnsi2(text2);
14981
+ const linePrefix = usePrefix ? `[${(/* @__PURE__ */ new Date()).toISOString()}${idSuffix}` : "";
14782
14982
  const out = usePrefix ? prefixLines(clean, linePrefix) : clean;
14783
14983
  appendFileSync5(filePath, out);
14784
14984
  bytesWritten += out.length;
@@ -16881,9 +17081,9 @@ function createEmptyAllTime() {
16881
17081
  function loadStats() {
16882
17082
  const currentWeek = getWeekStart();
16883
17083
  try {
16884
- const { readFileSync: readFileSync70 } = __require("fs");
17084
+ const { readFileSync: readFileSync71 } = __require("fs");
16885
17085
  const raw = JSON.parse(
16886
- readFileSync70(getStatsPath(), "utf-8")
17086
+ readFileSync71(getStatsPath(), "utf-8")
16887
17087
  );
16888
17088
  if (raw.version !== 1) {
16889
17089
  return {
@@ -17601,9 +17801,9 @@ function getCumulativePath() {
17601
17801
  function loadCumulativeStats() {
17602
17802
  const currentWeek = getWeekStart2();
17603
17803
  try {
17604
- const { readFileSync: readFileSync70 } = __require("fs");
17804
+ const { readFileSync: readFileSync71 } = __require("fs");
17605
17805
  const raw = JSON.parse(
17606
- readFileSync70(getCumulativePath(), "utf-8")
17806
+ readFileSync71(getCumulativePath(), "utf-8")
17607
17807
  );
17608
17808
  if (raw.weekStart !== currentWeek) {
17609
17809
  return {
@@ -17664,9 +17864,9 @@ function createEmptyCumulativeLocal(weekStart) {
17664
17864
  function loadCumulativeLocalStats() {
17665
17865
  const currentWeek = getWeekStart2();
17666
17866
  try {
17667
- const { readFileSync: readFileSync70 } = __require("fs");
17867
+ const { readFileSync: readFileSync71 } = __require("fs");
17668
17868
  const raw = JSON.parse(
17669
- readFileSync70(getCumulativeLocalPath(), "utf-8")
17869
+ readFileSync71(getCumulativeLocalPath(), "utf-8")
17670
17870
  );
17671
17871
  if (raw.weekStartDate !== currentWeek) {
17672
17872
  return createEmptyCumulativeLocal(currentWeek);
@@ -19119,6 +19319,24 @@ var init_tool_clusters = __esm({
19119
19319
  "starting",
19120
19320
  "planning"
19121
19321
  ]
19322
+ },
19323
+ {
19324
+ id: "web",
19325
+ name: "Web Fetch",
19326
+ tools: ["fetch_url"],
19327
+ triggerKeywords: [
19328
+ "fetch",
19329
+ "url",
19330
+ "webpage",
19331
+ "web page",
19332
+ "http",
19333
+ "https",
19334
+ "scrape",
19335
+ "docs at",
19336
+ "documentation at",
19337
+ "blog",
19338
+ "article"
19339
+ ]
19122
19340
  }
19123
19341
  ];
19124
19342
  TOOL_TO_CLUSTER = /* @__PURE__ */ new Map();
@@ -19236,6 +19454,11 @@ var init_tool_descriptions = __esm({
19236
19454
  active: "Find callers or callees of an entity across the codebase. Pass direction:'callers' (default) or 'callees'. Catches indirect refs grep misses.",
19237
19455
  locked: "[tier 1 \u2014 always exposed]"
19238
19456
  },
19457
+ fetch_url: {
19458
+ tier: 1,
19459
+ active: "Fetch a web page and return DOM-extracted markdown passages. Strips chrome, converts to ATX-markdown, splits by heading, ranks by BM25 when prompt is set, caches by content hash. Use instead of built-in WebFetch \u2014 5\u201310\xD7 fewer tokens.",
19460
+ locked: "[tier 1 \u2014 always exposed]"
19461
+ },
19239
19462
  // ── Tier 2 — structural unlock ─────────────────────────────────────────
19240
19463
  get_critical_nodes: {
19241
19464
  tier: 2,
@@ -19407,6 +19630,36 @@ var init_tool_definitions = __esm({
19407
19630
  openWorldHint: false
19408
19631
  }
19409
19632
  },
19633
+ fetch_url: {
19634
+ inputSchema: {
19635
+ type: "object",
19636
+ properties: {
19637
+ url: {
19638
+ type: "string",
19639
+ description: "Absolute URL to fetch (http or https). Localhost is allowed without TLS upgrade."
19640
+ },
19641
+ prompt: {
19642
+ type: "string",
19643
+ description: "Optional. When set AND extracted markdown > 8 KB, passages are re-ranked by BM25 relevance to this prompt (default top 20)."
19644
+ },
19645
+ offset: {
19646
+ type: "integer",
19647
+ description: "Passage index to start at for pagination (default 0). Pair with limit to walk long pages without re-fetching."
19648
+ },
19649
+ limit: {
19650
+ type: "integer",
19651
+ description: "Maximum passages to return (default 30, max 300). Also acts as BM25 topK when prompt is set."
19652
+ },
19653
+ token_budget: TOKEN_BUDGET_PROP
19654
+ },
19655
+ required: ["url"]
19656
+ },
19657
+ annotations: {
19658
+ title: "Fetch Web Page (Markdown + BM25)",
19659
+ readOnlyHint: true,
19660
+ openWorldHint: true
19661
+ }
19662
+ },
19410
19663
  file_outline: {
19411
19664
  inputSchema: {
19412
19665
  type: "object",
@@ -23084,6 +23337,12 @@ var init_wire_cap = __esm({
23084
23337
  // statically. The caller should pick from the returned `entities[].name`
23085
23338
  // and re-call file_read with that exact value. A literal `entity:<name>`
23086
23339
  // hint trains the agent to paste the placeholder verbatim.
23340
+ },
23341
+ fetch_url: {
23342
+ arrayKey: "passages",
23343
+ defaultLimit: 30,
23344
+ maxLimit: 300,
23345
+ cursorArg: "limit"
23087
23346
  }
23088
23347
  };
23089
23348
  HARD_BYTE_CAP = 8192;
@@ -24309,6 +24568,785 @@ var init_file_read_protocol = __esm({
24309
24568
  }
24310
24569
  });
24311
24570
 
24571
+ // src/tools/web/bm25-rank.ts
24572
+ function tokenize2(text2) {
24573
+ return text2.toLowerCase().replace(/[^a-z0-9\s]+/g, " ").split(/\s+/).filter((t) => t.length > 1 && !STOPWORDS.has(t));
24574
+ }
24575
+ async function rankPassagesByPrompt(passages, opts) {
24576
+ const prompt = opts.prompt?.trim();
24577
+ if (!prompt || passages.length === 0) return passages;
24578
+ const promptTokens = tokenize2(prompt);
24579
+ if (promptTokens.length === 0) return passages;
24580
+ const mod = await import("wink-bm25-text-search");
24581
+ const engine = mod.default();
24582
+ engine.defineConfig({ fldWeights: { text: 1, heading: 2 } });
24583
+ engine.definePrepTasks([tokenize2]);
24584
+ for (const p of passages) {
24585
+ engine.addDoc(
24586
+ { text: p.text, heading: p.heading ?? "" },
24587
+ String(p.index)
24588
+ );
24589
+ }
24590
+ engine.consolidate();
24591
+ const hits = engine.search(prompt);
24592
+ if (hits.length === 0) return passages;
24593
+ const topK = Math.max(1, opts.topK ?? Math.min(20, passages.length));
24594
+ const ranked = [];
24595
+ const seen = /* @__PURE__ */ new Set();
24596
+ for (const [docId] of hits.slice(0, topK)) {
24597
+ const idx = Number(docId);
24598
+ const p = passages.find((x2) => x2.index === idx);
24599
+ if (p && !seen.has(idx)) {
24600
+ ranked.push(p);
24601
+ seen.add(idx);
24602
+ }
24603
+ }
24604
+ if (ranked.length === 0) return passages;
24605
+ return ranked;
24606
+ }
24607
+ var STOPWORDS;
24608
+ var init_bm25_rank = __esm({
24609
+ "src/tools/web/bm25-rank.ts"() {
24610
+ "use strict";
24611
+ STOPWORDS = /* @__PURE__ */ new Set([
24612
+ "a",
24613
+ "an",
24614
+ "the",
24615
+ "is",
24616
+ "are",
24617
+ "was",
24618
+ "were",
24619
+ "be",
24620
+ "been",
24621
+ "being",
24622
+ "of",
24623
+ "and",
24624
+ "or",
24625
+ "but",
24626
+ "in",
24627
+ "on",
24628
+ "at",
24629
+ "to",
24630
+ "for",
24631
+ "with",
24632
+ "by",
24633
+ "from",
24634
+ "up",
24635
+ "down",
24636
+ "out",
24637
+ "off",
24638
+ "over",
24639
+ "under",
24640
+ "as",
24641
+ "this",
24642
+ "that",
24643
+ "these",
24644
+ "those",
24645
+ "it",
24646
+ "its",
24647
+ "into",
24648
+ "if",
24649
+ "then",
24650
+ "do",
24651
+ "does",
24652
+ "did"
24653
+ ]);
24654
+ }
24655
+ });
24656
+
24657
+ // src/tools/web/diff-cache.ts
24658
+ import { createHash as createHash3 } from "crypto";
24659
+ import { join as join60 } from "path";
24660
+ function hashHtml(html) {
24661
+ return createHash3("sha256").update(html).digest("hex").slice(0, 32);
24662
+ }
24663
+ function lookupFetchCache(cwd, url) {
24664
+ try {
24665
+ const store = openMetricsStore(join60(cwd, ".unerr"));
24666
+ const row = store.getFetchCacheRow(url);
24667
+ if (!row) return { hit: false, prior: null };
24668
+ return { hit: true, prior: row };
24669
+ } catch {
24670
+ return { hit: false, prior: null };
24671
+ }
24672
+ }
24673
+ function storeFetchCache(cwd, row) {
24674
+ try {
24675
+ const store = openMetricsStore(join60(cwd, ".unerr"));
24676
+ store.upsertFetchCacheRow({ ...row, hit_count: 0 });
24677
+ } catch {
24678
+ }
24679
+ }
24680
+ function bumpCacheHit(cwd, url) {
24681
+ try {
24682
+ const store = openMetricsStore(join60(cwd, ".unerr"));
24683
+ store.bumpFetchCacheHitFor(url);
24684
+ } catch {
24685
+ }
24686
+ }
24687
+ function summarizeMarkdownDiff(oldMd, newMd) {
24688
+ if (oldMd === newMd) {
24689
+ return { unchanged: true, changedRegions: 0, addedLines: 0, removedLines: 0 };
24690
+ }
24691
+ const oldLines = new Set(oldMd.split("\n"));
24692
+ const newLines = newMd.split("\n");
24693
+ let added = 0;
24694
+ let regions = 0;
24695
+ let inRegion = false;
24696
+ for (const line of newLines) {
24697
+ if (!oldLines.has(line)) {
24698
+ added++;
24699
+ if (!inRegion) {
24700
+ regions++;
24701
+ inRegion = true;
24702
+ }
24703
+ } else {
24704
+ inRegion = false;
24705
+ }
24706
+ }
24707
+ const newLineSet = new Set(newLines);
24708
+ let removed = 0;
24709
+ for (const line of oldMd.split("\n")) {
24710
+ if (!newLineSet.has(line)) removed++;
24711
+ }
24712
+ return {
24713
+ unchanged: false,
24714
+ changedRegions: regions,
24715
+ addedLines: added,
24716
+ removedLines: removed
24717
+ };
24718
+ }
24719
+ var init_diff_cache = __esm({
24720
+ "src/tools/web/diff-cache.ts"() {
24721
+ "use strict";
24722
+ init_metrics_store();
24723
+ }
24724
+ });
24725
+
24726
+ // src/tools/web/extract.ts
24727
+ async function extractMainContent(html, baseUrl) {
24728
+ const { JSDOM } = await import("jsdom");
24729
+ const dom = new JSDOM(html, { url: baseUrl });
24730
+ const defuddled = await tryDefuddle(dom);
24731
+ if (defuddled && stripTags(defuddled.contentHtml).length >= MIN_USEFUL_CHARS) {
24732
+ return defuddled;
24733
+ }
24734
+ const readabilityResult = await tryReadability(dom);
24735
+ if (readabilityResult && stripTags(readabilityResult.contentHtml).length >= MIN_USEFUL_CHARS) {
24736
+ return readabilityResult;
24737
+ }
24738
+ return rawBodyFallback(dom);
24739
+ }
24740
+ async function tryDefuddle(dom) {
24741
+ try {
24742
+ const mod = await import("defuddle");
24743
+ const DefuddleCtor = mod.default;
24744
+ const doc = dom.window.document;
24745
+ const result = new DefuddleCtor(doc, { markdown: false }).parse();
24746
+ if (!result?.content) return null;
24747
+ return {
24748
+ title: result.title ?? "",
24749
+ contentHtml: result.content,
24750
+ wordCount: result.wordCount ?? wordCountOf(result.content),
24751
+ byline: result.author ?? void 0,
24752
+ excerpt: result.description ?? void 0,
24753
+ extractor: "defuddle"
24754
+ };
24755
+ } catch {
24756
+ return null;
24757
+ }
24758
+ }
24759
+ async function tryReadability(dom) {
24760
+ try {
24761
+ const mod = await import("@mozilla/readability");
24762
+ const doc = dom.window.document;
24763
+ if (mod.isProbablyReaderable && !mod.isProbablyReaderable(doc)) {
24764
+ return null;
24765
+ }
24766
+ const article = new mod.Readability(
24767
+ doc
24768
+ ).parse();
24769
+ if (!article?.content) return null;
24770
+ return {
24771
+ title: article.title ?? "",
24772
+ contentHtml: article.content,
24773
+ wordCount: wordCountOf(article.textContent ?? article.content),
24774
+ byline: article.byline ?? void 0,
24775
+ excerpt: article.excerpt ?? void 0,
24776
+ lang: article.lang ?? void 0,
24777
+ extractor: "readability"
24778
+ };
24779
+ } catch {
24780
+ return null;
24781
+ }
24782
+ }
24783
+ function rawBodyFallback(dom) {
24784
+ const doc = dom.window.document;
24785
+ for (const sel of ["script", "style", "noscript", "iframe", "svg"]) {
24786
+ for (const node of doc.querySelectorAll(sel)) node.remove();
24787
+ }
24788
+ const main = doc.querySelector("main") ?? doc.querySelector("article") ?? doc.body ?? doc.documentElement;
24789
+ const html = main?.innerHTML ?? "";
24790
+ return {
24791
+ title: doc.title ?? "",
24792
+ contentHtml: html,
24793
+ wordCount: wordCountOf(main?.textContent ?? ""),
24794
+ extractor: "raw-body"
24795
+ };
24796
+ }
24797
+ function stripTags(html) {
24798
+ return html.replace(/<[^>]+>/g, "").trim();
24799
+ }
24800
+ function wordCountOf(text2) {
24801
+ return text2.trim().split(/\s+/).filter(Boolean).length;
24802
+ }
24803
+ var MIN_USEFUL_CHARS;
24804
+ var init_extract = __esm({
24805
+ "src/tools/web/extract.ts"() {
24806
+ "use strict";
24807
+ MIN_USEFUL_CHARS = 200;
24808
+ }
24809
+ });
24810
+
24811
+ // src/tools/web/markdown.ts
24812
+ async function htmlToMarkdown(html) {
24813
+ if (!html.trim()) return "";
24814
+ const TurndownModule = await import("turndown");
24815
+ const Turndown = TurndownModule.default;
24816
+ const td = new Turndown({
24817
+ headingStyle: "atx",
24818
+ codeBlockStyle: "fenced",
24819
+ hr: "---",
24820
+ bulletListMarker: "-",
24821
+ emDelimiter: "_"
24822
+ });
24823
+ td.addRule("drop-anchor-only", {
24824
+ filter: (node) => {
24825
+ if (node.nodeName !== "A") return false;
24826
+ const href = node.getAttribute("href") ?? "";
24827
+ const text2 = (node.textContent ?? "").trim();
24828
+ return href.startsWith("#") || href === "" && text2 === "";
24829
+ },
24830
+ replacement: (content) => content
24831
+ });
24832
+ td.addRule("strip-tracking-params", {
24833
+ filter: (node) => node.nodeName === "A",
24834
+ replacement: (content, node) => {
24835
+ const rawHref = node.getAttribute("href") ?? "";
24836
+ const cleanHref = cleanUrl(rawHref);
24837
+ const title = node.getAttribute("title");
24838
+ if (!cleanHref) return content;
24839
+ const titleSuffix = title ? ` "${title}"` : "";
24840
+ return `[${content}](${cleanHref}${titleSuffix})`;
24841
+ }
24842
+ });
24843
+ return td.turndown(html);
24844
+ }
24845
+ function cleanUrl(href) {
24846
+ if (!href || href.startsWith("#") || href.startsWith("javascript:")) {
24847
+ return href;
24848
+ }
24849
+ try {
24850
+ const url = new URL(href, "https://example.invalid");
24851
+ const params = url.searchParams;
24852
+ const drop = [];
24853
+ for (const key of params.keys()) {
24854
+ if (TRACKING_PARAMS.test(key)) drop.push(key);
24855
+ }
24856
+ for (const key of drop) params.delete(key);
24857
+ return url.protocol === "https:" && url.hostname === "example.invalid" ? url.pathname + url.search + url.hash : url.toString();
24858
+ } catch {
24859
+ return href;
24860
+ }
24861
+ }
24862
+ var TRACKING_PARAMS;
24863
+ var init_markdown = __esm({
24864
+ "src/tools/web/markdown.ts"() {
24865
+ "use strict";
24866
+ TRACKING_PARAMS = /^(utm_|mc_|gclid$|fbclid$|yclid$|msclkid$|ref$|ref_)/i;
24867
+ }
24868
+ });
24869
+
24870
+ // src/tools/web/passage-split.ts
24871
+ function splitMarkdownIntoPassages(markdown) {
24872
+ const lines = markdown.split("\n");
24873
+ const passages = [];
24874
+ let currentHeading = null;
24875
+ let buffer = [];
24876
+ let bufferStart = 0;
24877
+ let inFence = false;
24878
+ let fenceMarker = "";
24879
+ const flush = () => {
24880
+ const text2 = buffer.join("\n").trim();
24881
+ if (text2) {
24882
+ passages.push({
24883
+ index: passages.length,
24884
+ heading: currentHeading,
24885
+ text: text2,
24886
+ startLine: bufferStart + 1
24887
+ });
24888
+ }
24889
+ buffer = [];
24890
+ };
24891
+ for (let i = 0; i < lines.length; i++) {
24892
+ const line = lines[i] ?? "";
24893
+ const fenceMatch = line.match(/^(```|~~~)/);
24894
+ if (fenceMatch) {
24895
+ if (!inFence) {
24896
+ inFence = true;
24897
+ fenceMarker = fenceMatch[1] ?? "```";
24898
+ if (buffer.length === 0) bufferStart = i;
24899
+ buffer.push(line);
24900
+ } else if (line.startsWith(fenceMarker)) {
24901
+ buffer.push(line);
24902
+ inFence = false;
24903
+ } else {
24904
+ buffer.push(line);
24905
+ }
24906
+ continue;
24907
+ }
24908
+ if (inFence) {
24909
+ buffer.push(line);
24910
+ continue;
24911
+ }
24912
+ const headingMatch = line.match(/^(#{1,6})\s+(.+?)\s*$/);
24913
+ if (headingMatch) {
24914
+ flush();
24915
+ currentHeading = headingMatch[2] ?? null;
24916
+ bufferStart = i;
24917
+ buffer.push(line);
24918
+ continue;
24919
+ }
24920
+ if (line.trim() === "") {
24921
+ if (buffer.length > 0) flush();
24922
+ continue;
24923
+ }
24924
+ if (buffer.length === 0) bufferStart = i;
24925
+ buffer.push(line);
24926
+ }
24927
+ flush();
24928
+ return passages;
24929
+ }
24930
+ var init_passage_split = __esm({
24931
+ "src/tools/web/passage-split.ts"() {
24932
+ "use strict";
24933
+ }
24934
+ });
24935
+
24936
+ // src/tools/web/post-process.ts
24937
+ import { readdirSync as readdirSync14, readFileSync as readFileSync51 } from "fs";
24938
+ import { join as join61 } from "path";
24939
+ import { fileURLToPath as fileURLToPath3 } from "url";
24940
+ function loadRules() {
24941
+ if (rulesCache) return rulesCache;
24942
+ const map = /* @__PURE__ */ new Map();
24943
+ if (!RULES_DIR) {
24944
+ rulesCache = map;
24945
+ return map;
24946
+ }
24947
+ let entries;
24948
+ try {
24949
+ entries = readdirSync14(RULES_DIR);
24950
+ } catch {
24951
+ rulesCache = map;
24952
+ return map;
24953
+ }
24954
+ for (const name of entries) {
24955
+ if (!name.endsWith(".json")) continue;
24956
+ try {
24957
+ const raw = readFileSync51(join61(RULES_DIR, name), "utf-8");
24958
+ const parsed = JSON.parse(raw);
24959
+ if (parsed.host) map.set(parsed.host.toLowerCase(), parsed);
24960
+ } catch {
24961
+ }
24962
+ }
24963
+ rulesCache = map;
24964
+ return map;
24965
+ }
24966
+ function postProcessMarkdown(markdown, opts = {}) {
24967
+ let out = universalCleanup(markdown);
24968
+ const hostRule = pickHostRule(opts);
24969
+ if (hostRule) out = applyHostRule(out, hostRule);
24970
+ out = universalCleanup(out);
24971
+ return out;
24972
+ }
24973
+ function universalCleanup(md) {
24974
+ return md.replace(/\r\n/g, "\n").replace(/[ \t]+$/gm, "").replace(/\n{3,}/g, "\n\n").replace(/^\[\s*\]\([^)]*\)\s*$/gm, "").trim();
24975
+ }
24976
+ function pickHostRule(opts) {
24977
+ if (opts.rules?.length) {
24978
+ return mergeRules(opts.rules);
24979
+ }
24980
+ if (!opts.url) return null;
24981
+ try {
24982
+ const host = new URL(opts.url).hostname.toLowerCase();
24983
+ const rules = loadRules();
24984
+ const exact = rules.get(host);
24985
+ if (exact) return exact;
24986
+ const parts = host.split(".");
24987
+ for (let i = 1; i < parts.length - 1; i++) {
24988
+ const suffix = parts.slice(i).join(".");
24989
+ const match = rules.get(suffix);
24990
+ if (match) return match;
24991
+ }
24992
+ return null;
24993
+ } catch {
24994
+ return null;
24995
+ }
24996
+ }
24997
+ function mergeRules(rules) {
24998
+ return {
24999
+ host: "merged",
25000
+ drop: rules.flatMap((r) => r.drop ?? []),
25001
+ replace: rules.flatMap((r) => r.replace ?? [])
25002
+ };
25003
+ }
25004
+ function applyHostRule(md, rule) {
25005
+ let out = md;
25006
+ if (rule.drop?.length) {
25007
+ for (const pat of rule.drop) {
25008
+ try {
25009
+ const re = new RegExp(pat, "gm");
25010
+ out = out.replace(re, "");
25011
+ } catch {
25012
+ }
25013
+ }
25014
+ }
25015
+ if (rule.replace?.length) {
25016
+ for (const { pattern, with: replacement } of rule.replace) {
25017
+ try {
25018
+ const re = new RegExp(pattern, "gm");
25019
+ out = out.replace(re, replacement);
25020
+ } catch {
25021
+ }
25022
+ }
25023
+ }
25024
+ return out;
25025
+ }
25026
+ var RULES_DIR, rulesCache;
25027
+ var init_post_process = __esm({
25028
+ "src/tools/web/post-process.ts"() {
25029
+ "use strict";
25030
+ RULES_DIR = (() => {
25031
+ try {
25032
+ return join61(fileURLToPath3(new URL(".", import.meta.url)), "rules");
25033
+ } catch {
25034
+ return "";
25035
+ }
25036
+ })();
25037
+ rulesCache = null;
25038
+ }
25039
+ });
25040
+
25041
+ // src/tools/web/spa-render.ts
25042
+ function shouldUsePlaywright(args) {
25043
+ if (!args.config?.playwright?.enabled) return false;
25044
+ if (args.extractor !== "raw-body") return false;
25045
+ return args.wordCount < SPA_EMPTY_WC;
25046
+ }
25047
+ async function renderWithPlaywright(url, config) {
25048
+ const pw = await loadPlaywright();
25049
+ if (!pw) return null;
25050
+ const browser = await pw.chromium.launch({ headless: true });
25051
+ try {
25052
+ const ctx = await browser.newContext({
25053
+ userAgent: "unerr-fetch-url/1.0 (+https://unerr.dev) Mozilla/5.0 compatible"
25054
+ });
25055
+ const page = await ctx.newPage();
25056
+ const response = await page.goto(url, {
25057
+ waitUntil: config.playwright.waitUntil,
25058
+ timeout: config.playwright.timeoutMs
25059
+ });
25060
+ const html = await page.content();
25061
+ return {
25062
+ html,
25063
+ finalUrl: page.url(),
25064
+ status: response?.status() ?? 200
25065
+ };
25066
+ } finally {
25067
+ await browser.close();
25068
+ }
25069
+ }
25070
+ async function loadPlaywright() {
25071
+ try {
25072
+ return await import("playwright");
25073
+ } catch {
25074
+ return null;
25075
+ }
25076
+ }
25077
+ var SPA_EMPTY_WC;
25078
+ var init_spa_render = __esm({
25079
+ "src/tools/web/spa-render.ts"() {
25080
+ "use strict";
25081
+ SPA_EMPTY_WC = 40;
25082
+ }
25083
+ });
25084
+
25085
+ // src/tools/web/telemetry.ts
25086
+ function recordFetchUrlTelemetry(cwd, ev) {
25087
+ const savedPct = ev.rawBytes > 0 ? Math.round((ev.rawBytes - ev.compressedBytes) / ev.rawBytes * 100) : 0;
25088
+ appendCompressionLog(cwd, {
25089
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
25090
+ command: `fetch_url ${ev.url}`,
25091
+ category: "fetch_url",
25092
+ confidence: 1,
25093
+ rawBytes: ev.rawBytes,
25094
+ compressedBytes: ev.compressedBytes,
25095
+ savedPct,
25096
+ omniFallback: ev.extractor === "raw-body",
25097
+ teeFile: ev.cacheHit ? "cache-hit" : void 0
25098
+ });
25099
+ }
25100
+ var init_telemetry = __esm({
25101
+ "src/tools/web/telemetry.ts"() {
25102
+ "use strict";
25103
+ init_shell_compression_log();
25104
+ }
25105
+ });
25106
+
25107
+ // src/tools/web/fetch-url-protocol.ts
25108
+ var fetch_url_protocol_exports = {};
25109
+ __export(fetch_url_protocol_exports, {
25110
+ runFetchUrl: () => runFetchUrl
25111
+ });
25112
+ function shouldRank(args, markdown) {
25113
+ if (!args.prompt || args.prompt.trim().length === 0) return false;
25114
+ return Buffer.byteLength(markdown, "utf-8") > BM25_GATE_BYTES;
25115
+ }
25116
+ async function runFetchUrl(args, ctx) {
25117
+ if (!args.url || typeof args.url !== "string") {
25118
+ throw new Error("fetch_url requires a string `url` argument");
25119
+ }
25120
+ const url = upgradeToHttps(args.url);
25121
+ const started = Date.now();
25122
+ const fetched = await fetchHtml(url, ctx.abortSignal);
25123
+ const contentHash = hashHtml(fetched.html);
25124
+ const cache = lookupFetchCache(ctx.cwd, fetched.finalUrl);
25125
+ let markdown;
25126
+ let title;
25127
+ let extractor;
25128
+ let wordCount;
25129
+ let cacheHit = false;
25130
+ let diff;
25131
+ if (cache.hit && cache.prior && cache.prior.content_hash === contentHash) {
25132
+ markdown = cache.prior.markdown;
25133
+ title = cache.prior.title;
25134
+ extractor = "cache";
25135
+ wordCount = markdown.trim().split(/\s+/).filter(Boolean).length;
25136
+ cacheHit = true;
25137
+ diff = {
25138
+ unchanged: true,
25139
+ changed_regions: 0,
25140
+ added_lines: 0,
25141
+ removed_lines: 0
25142
+ };
25143
+ bumpCacheHit(ctx.cwd, fetched.finalUrl);
25144
+ } else {
25145
+ let extracted = await extractMainContent(fetched.html, fetched.finalUrl);
25146
+ const settings = safeLoadSettings(ctx.cwd);
25147
+ if (shouldUsePlaywright({
25148
+ config: settings?.fetchUrl,
25149
+ extractor: extracted.extractor,
25150
+ wordCount: extracted.wordCount
25151
+ }) && settings) {
25152
+ const rendered = await renderWithPlaywright(
25153
+ fetched.finalUrl,
25154
+ settings.fetchUrl
25155
+ );
25156
+ if (rendered) {
25157
+ fetched.html = rendered.html;
25158
+ fetched.finalUrl = rendered.finalUrl;
25159
+ fetched.status = rendered.status;
25160
+ extracted = await extractMainContent(rendered.html, rendered.finalUrl);
25161
+ }
25162
+ }
25163
+ const rawMarkdown = await htmlToMarkdown(extracted.contentHtml);
25164
+ markdown = postProcessMarkdown(rawMarkdown, { url: fetched.finalUrl });
25165
+ title = extracted.title;
25166
+ extractor = extracted.extractor;
25167
+ wordCount = extracted.wordCount;
25168
+ if (cache.prior) {
25169
+ const summary = summarizeMarkdownDiff(cache.prior.markdown, markdown);
25170
+ diff = {
25171
+ unchanged: summary.unchanged,
25172
+ changed_regions: summary.changedRegions,
25173
+ added_lines: summary.addedLines,
25174
+ removed_lines: summary.removedLines
25175
+ };
25176
+ }
25177
+ storeFetchCache(ctx.cwd, {
25178
+ url: fetched.finalUrl,
25179
+ content_hash: contentHash,
25180
+ markdown,
25181
+ title,
25182
+ extractor,
25183
+ raw_bytes: Buffer.byteLength(fetched.html, "utf-8"),
25184
+ compressed_bytes: Buffer.byteLength(markdown, "utf-8"),
25185
+ fetched_at: Date.now()
25186
+ });
25187
+ }
25188
+ const allPassages = splitMarkdownIntoPassages(markdown);
25189
+ const ranked = shouldRank(args, markdown) ? await rankPassagesByPrompt(allPassages, {
25190
+ prompt: args.prompt ?? "",
25191
+ topK: args.limit ?? BM25_DEFAULT_TOPK
25192
+ }) : allPassages;
25193
+ const offset = Math.max(0, args.offset ?? 0);
25194
+ const sliced = offset > 0 ? ranked.slice(offset) : ranked;
25195
+ const rawBytes = byteLength(fetched.html);
25196
+ const compressedBytes = byteLength(markdown);
25197
+ recordFetchUrlTelemetry(ctx.cwd, {
25198
+ url: fetched.finalUrl,
25199
+ rawBytes,
25200
+ compressedBytes,
25201
+ durationMs: Date.now() - started,
25202
+ extractor: extractor === "cache" ? "raw-body" : extractor,
25203
+ cacheHit
25204
+ });
25205
+ return {
25206
+ url: args.url,
25207
+ final_url: fetched.finalUrl,
25208
+ status: fetched.status,
25209
+ title,
25210
+ extractor,
25211
+ word_count: wordCount,
25212
+ cache_hit: cacheHit,
25213
+ diff,
25214
+ raw_bytes: rawBytes,
25215
+ extracted_bytes: compressedBytes,
25216
+ compression_ratio: rawBytes > 0 ? Math.round((1 - compressedBytes / rawBytes) * 100) / 100 : 0,
25217
+ passages: sliced.map((p) => ({
25218
+ index: p.index,
25219
+ heading: p.heading,
25220
+ text: p.text,
25221
+ start_line: p.startLine
25222
+ })),
25223
+ total: allPassages.length
25224
+ };
25225
+ }
25226
+ function upgradeToHttps(url) {
25227
+ if (!url.startsWith("http://")) return url;
25228
+ try {
25229
+ const host = new URL(url).hostname;
25230
+ if (host === "localhost" || host === "127.0.0.1" || host === "::1") {
25231
+ return url;
25232
+ }
25233
+ } catch {
25234
+ return url;
25235
+ }
25236
+ return "https://" + url.slice(7);
25237
+ }
25238
+ async function fetchHtml(url, abortSignal) {
25239
+ const controller = new AbortController();
25240
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
25241
+ const signal = abortSignal ? composeSignals(controller.signal, abortSignal) : controller.signal;
25242
+ try {
25243
+ const res = await fetch(url, {
25244
+ signal,
25245
+ redirect: "follow",
25246
+ headers: {
25247
+ "user-agent": "unerr-fetch-url/1.0 (+https://unerr.dev) Mozilla/5.0 compatible",
25248
+ accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
25249
+ }
25250
+ });
25251
+ const contentType = res.headers.get("content-type") ?? "";
25252
+ if (!contentType.includes("html") && !contentType.includes("xml")) {
25253
+ throw new Error(
25254
+ `fetch_url expected HTML response, got content-type: ${contentType || "unknown"}`
25255
+ );
25256
+ }
25257
+ const reader = res.body?.getReader();
25258
+ if (!reader) {
25259
+ const text2 = await res.text();
25260
+ return capHtml({
25261
+ html: text2,
25262
+ finalUrl: res.url,
25263
+ status: res.status,
25264
+ contentType
25265
+ });
25266
+ }
25267
+ const chunks = [];
25268
+ let total = 0;
25269
+ for (; ; ) {
25270
+ const { done, value } = await reader.read();
25271
+ if (done) break;
25272
+ if (value) {
25273
+ total += value.byteLength;
25274
+ if (total > MAX_HTML_BYTES) {
25275
+ await reader.cancel();
25276
+ break;
25277
+ }
25278
+ chunks.push(value);
25279
+ }
25280
+ }
25281
+ const merged = concatBytes(chunks);
25282
+ const html = new TextDecoder("utf-8", { fatal: false }).decode(merged);
25283
+ return capHtml({
25284
+ html,
25285
+ finalUrl: res.url,
25286
+ status: res.status,
25287
+ contentType
25288
+ });
25289
+ } finally {
25290
+ clearTimeout(timer);
25291
+ }
25292
+ }
25293
+ function capHtml(fetched) {
25294
+ if (fetched.html.length > MAX_HTML_BYTES) {
25295
+ return { ...fetched, html: fetched.html.slice(0, MAX_HTML_BYTES) };
25296
+ }
25297
+ return fetched;
25298
+ }
25299
+ function composeSignals(a, b) {
25300
+ const controller = new AbortController();
25301
+ const onAbort = () => controller.abort();
25302
+ if (a.aborted || b.aborted) controller.abort();
25303
+ else {
25304
+ a.addEventListener("abort", onAbort, { once: true });
25305
+ b.addEventListener("abort", onAbort, { once: true });
25306
+ }
25307
+ return controller.signal;
25308
+ }
25309
+ function byteLength(s) {
25310
+ return Buffer.byteLength(s, "utf-8");
25311
+ }
25312
+ function safeLoadSettings(cwd) {
25313
+ try {
25314
+ return loadSettings(cwd);
25315
+ } catch {
25316
+ return null;
25317
+ }
25318
+ }
25319
+ function concatBytes(chunks) {
25320
+ let total = 0;
25321
+ for (const c of chunks) total += c.byteLength;
25322
+ const out = new Uint8Array(total);
25323
+ let offset = 0;
25324
+ for (const c of chunks) {
25325
+ out.set(c, offset);
25326
+ offset += c.byteLength;
25327
+ }
25328
+ return out;
25329
+ }
25330
+ var FETCH_TIMEOUT_MS, MAX_HTML_BYTES, BM25_GATE_BYTES, BM25_DEFAULT_TOPK;
25331
+ var init_fetch_url_protocol = __esm({
25332
+ "src/tools/web/fetch-url-protocol.ts"() {
25333
+ "use strict";
25334
+ init_settings();
25335
+ init_bm25_rank();
25336
+ init_diff_cache();
25337
+ init_extract();
25338
+ init_markdown();
25339
+ init_passage_split();
25340
+ init_post_process();
25341
+ init_spa_render();
25342
+ init_telemetry();
25343
+ FETCH_TIMEOUT_MS = 15e3;
25344
+ MAX_HTML_BYTES = 5 * 1024 * 1024;
25345
+ BM25_GATE_BYTES = 8 * 1024;
25346
+ BM25_DEFAULT_TOPK = 20;
25347
+ }
25348
+ });
25349
+
24312
25350
  // src/intelligence/query-router.ts
24313
25351
  var query_router_exports = {};
24314
25352
  __export(query_router_exports, {
@@ -24564,7 +25602,9 @@ var init_query_router = __esm({
24564
25602
  "get_test_coverage",
24565
25603
  // Sprint FE-B: file read protocol
24566
25604
  "file_outline",
24567
- "file_read"
25605
+ "file_read",
25606
+ // Sprint FU-1: web fetch
25607
+ "fetch_url"
24568
25608
  ]);
24569
25609
  ENRICHABLE_TOOLS = /* @__PURE__ */ new Set([
24570
25610
  // Agent-facing tools
@@ -25362,7 +26402,8 @@ var init_query_router = __esm({
25362
26402
  const saved = estimate.tokensWithout - estimate.tokensUsed;
25363
26403
  if (saved > 0) {
25364
26404
  const isFileNav = toolName === "file_read" || toolName === "file_outline" || toolName === "get_file";
25365
- const mechanism = isFileNav ? "file_read" : "graph_query";
26405
+ const isFetchUrl = toolName === "fetch_url";
26406
+ const mechanism = isFetchUrl ? "fetch_url" : isFileNav ? "file_read" : "graph_query";
25366
26407
  enrichTokensSaved = saved;
25367
26408
  enrichSavingsMechanism = mechanism;
25368
26409
  const dollarSavings = calculateDollarSavings(saved);
@@ -26280,11 +27321,11 @@ var init_query_router = __esm({
26280
27321
  const entity = await this.resolveEntityWithOverlay(key);
26281
27322
  if (entity?.file_path && entity.start_line > 0) {
26282
27323
  try {
26283
- const { readFileSync: readFileSync70 } = await import("fs");
27324
+ const { readFileSync: readFileSync71 } = await import("fs");
26284
27325
  const { resolve: resolve6 } = await import("path");
26285
27326
  const cwd = this.projectRoot ?? process.cwd();
26286
27327
  const abs = resolve6(cwd, entity.file_path);
26287
- const lines = readFileSync70(abs, "utf-8").split("\n");
27328
+ const lines = readFileSync71(abs, "utf-8").split("\n");
26288
27329
  const start = entity.start_line - 1;
26289
27330
  const end = entity.end_line ?? lines.length;
26290
27331
  const bodyLines = lines.slice(start, end);
@@ -26522,6 +27563,12 @@ var init_query_router = __esm({
26522
27563
  graph: this.localGraph
26523
27564
  });
26524
27565
  }
27566
+ case "fetch_url": {
27567
+ const { runFetchUrl: runFetchUrl2 } = await Promise.resolve().then(() => (init_fetch_url_protocol(), fetch_url_protocol_exports));
27568
+ return runFetchUrl2(args, {
27569
+ cwd: this.projectRoot ?? process.cwd()
27570
+ });
27571
+ }
26525
27572
  default:
26526
27573
  throw new Error(`Unknown local tool: ${toolName}`);
26527
27574
  }
@@ -27881,8 +28928,8 @@ __export(causal_bridge_exports, {
27881
28928
  assembleCausalChain: () => assembleCausalChain,
27882
28929
  computeDurability: () => computeDurability
27883
28930
  });
27884
- import { existsSync as existsSync59, readFileSync as readFileSync51 } from "fs";
27885
- import { join as join60 } from "path";
28931
+ import { existsSync as existsSync59, readFileSync as readFileSync52 } from "fs";
28932
+ import { join as join62 } from "path";
27886
28933
  function computeAggregateDurability(interactions) {
27887
28934
  if (interactions.length === 0) return 1;
27888
28935
  const survivedCount = interactions.filter((i) => i.survived).length;
@@ -28039,9 +29086,9 @@ var init_causal_bridge = __esm({
28039
29086
  return chains;
28040
29087
  }
28041
29088
  loadEntityLedgerEntries(entityKey2) {
28042
- const ledgerPath = join60(this.unerrDir, "ledger", "shadow.jsonl");
29089
+ const ledgerPath = join62(this.unerrDir, "ledger", "shadow.jsonl");
28043
29090
  if (!existsSync59(ledgerPath)) return [];
28044
- const content = readFileSync51(ledgerPath, "utf-8");
29091
+ const content = readFileSync52(ledgerPath, "utf-8");
28045
29092
  const lines = content.split("\n").filter((l) => l.trim().length > 0);
28046
29093
  const entries = [];
28047
29094
  for (const line of lines) {
@@ -28527,11 +29574,11 @@ var context_ledger_exports = {};
28527
29574
  __export(context_ledger_exports, {
28528
29575
  createContextLedger: () => createContextLedger
28529
29576
  });
28530
- import { existsSync as existsSync60, mkdirSync as mkdirSync32, readFileSync as readFileSync52, writeFileSync as writeFileSync27 } from "fs";
28531
- import { join as join61 } from "path";
29577
+ import { existsSync as existsSync60, mkdirSync as mkdirSync32, readFileSync as readFileSync53, writeFileSync as writeFileSync27 } from "fs";
29578
+ import { join as join63 } from "path";
28532
29579
  function createContextLedger(unerrDir2) {
28533
- const stateDir = join61(unerrDir2, "state");
28534
- const filePath = join61(stateDir, "context-ledger.json");
29580
+ const stateDir = join63(unerrDir2, "state");
29581
+ const filePath = join63(stateDir, "context-ledger.json");
28535
29582
  let records = [];
28536
29583
  let index = /* @__PURE__ */ new Map();
28537
29584
  function ensureDir() {
@@ -28550,7 +29597,7 @@ function createContextLedger(unerrDir2) {
28550
29597
  return /* @__PURE__ */ new Map();
28551
29598
  }
28552
29599
  try {
28553
- const raw = readFileSync52(filePath, "utf-8");
29600
+ const raw = readFileSync53(filePath, "utf-8");
28554
29601
  const parsed = JSON.parse(raw);
28555
29602
  records = Array.isArray(parsed) ? parsed : [];
28556
29603
  } catch {
@@ -29477,11 +30524,11 @@ __export(intent_correlator_exports, {
29477
30524
  import {
29478
30525
  existsSync as existsSync61,
29479
30526
  mkdirSync as mkdirSync33,
29480
- readFileSync as readFileSync53,
30527
+ readFileSync as readFileSync54,
29481
30528
  renameSync as renameSync2,
29482
30529
  writeFileSync as writeFileSync28
29483
30530
  } from "fs";
29484
- import { join as join62 } from "path";
30531
+ import { join as join64 } from "path";
29485
30532
  function extractFiles4(args) {
29486
30533
  const files = args.files;
29487
30534
  if (files && Array.isArray(files)) {
@@ -29529,8 +30576,8 @@ var init_intent_correlator = __esm({
29529
30576
  pendingPath;
29530
30577
  pending = [];
29531
30578
  constructor(unerrDir2) {
29532
- this.ledgerDir = join62(unerrDir2, "ledger");
29533
- this.pendingPath = join62(this.ledgerDir, "pending_correlations.json");
30579
+ this.ledgerDir = join64(unerrDir2, "ledger");
30580
+ this.pendingPath = join64(this.ledgerDir, "pending_correlations.json");
29534
30581
  if (!existsSync61(this.ledgerDir)) {
29535
30582
  mkdirSync33(this.ledgerDir, { recursive: true });
29536
30583
  }
@@ -29625,7 +30672,7 @@ var init_intent_correlator = __esm({
29625
30672
  load() {
29626
30673
  if (!existsSync61(this.pendingPath)) return;
29627
30674
  try {
29628
- const raw = readFileSync53(this.pendingPath, "utf-8");
30675
+ const raw = readFileSync54(this.pendingPath, "utf-8");
29629
30676
  const parsed = JSON.parse(raw);
29630
30677
  if (Array.isArray(parsed)) {
29631
30678
  this.pending = parsed;
@@ -30190,7 +31237,7 @@ var init_session_state = __esm({
30190
31237
 
30191
31238
  // src/proxy/tool-exposure-store.ts
30192
31239
  import { promises as fs6 } from "fs";
30193
- import { dirname as dirname16, join as join63 } from "path";
31240
+ import { dirname as dirname16, join as join65 } from "path";
30194
31241
  var ToolExposureStore;
30195
31242
  var init_tool_exposure_store = __esm({
30196
31243
  "src/proxy/tool-exposure-store.ts"() {
@@ -30200,7 +31247,7 @@ var init_tool_exposure_store = __esm({
30200
31247
  sessionId;
30201
31248
  ensuredDir = false;
30202
31249
  constructor(unerrDir2, sessionId) {
30203
- this.filePath = join63(unerrDir2, "router", "exposure-events.jsonl");
31250
+ this.filePath = join65(unerrDir2, "router", "exposure-events.jsonl");
30204
31251
  this.sessionId = sessionId;
30205
31252
  }
30206
31253
  /**
@@ -30462,11 +31509,11 @@ var init_router_gateway = __esm({
30462
31509
 
30463
31510
  // src/timeline/timeline-store.ts
30464
31511
  import { existsSync as existsSync62, mkdirSync as mkdirSync34 } from "fs";
30465
- import { join as join64 } from "path";
31512
+ import { join as join66 } from "path";
30466
31513
  async function openTimelineDb(projectRoot) {
30467
- const unerrDir2 = join64(projectRoot, ".unerr");
31514
+ const unerrDir2 = join66(projectRoot, ".unerr");
30468
31515
  mkdirSync34(unerrDir2, { recursive: true });
30469
- const dbPath = join64(unerrDir2, TIMELINE_DB_FILENAME);
31516
+ const dbPath = join66(unerrDir2, TIMELINE_DB_FILENAME);
30470
31517
  const isNew = !existsSync62(dbPath);
30471
31518
  const cozoModule = await import("cozo-node");
30472
31519
  const CozoDbConstructor = cozoModule.default ? cozoModule.default.CozoDb : cozoModule.CozoDb;
@@ -31510,23 +32557,23 @@ import {
31510
32557
  appendFileSync as appendFileSync8,
31511
32558
  existsSync as existsSync63,
31512
32559
  mkdirSync as mkdirSync35,
31513
- readFileSync as readFileSync54,
32560
+ readFileSync as readFileSync55,
31514
32561
  renameSync as renameSync3,
31515
32562
  writeFileSync as writeFileSync29
31516
32563
  } from "fs";
31517
- import { join as join65 } from "path";
32564
+ import { join as join67 } from "path";
31518
32565
  import { gzipSync } from "zlib";
31519
32566
  function archiveShadowLedger(unerrDir2, opts = {}) {
31520
- const ledgerDir = join65(unerrDir2, "ledger");
31521
- const filePath = join65(ledgerDir, "shadow.jsonl");
31522
- const archiveDir = join65(ledgerDir, "archive");
32567
+ const ledgerDir = join67(unerrDir2, "ledger");
32568
+ const filePath = join67(ledgerDir, "shadow.jsonl");
32569
+ const archiveDir = join67(ledgerDir, "archive");
31523
32570
  if (!existsSync63(filePath)) {
31524
32571
  return { archived: 0, kept: 0, archivePath: null };
31525
32572
  }
31526
32573
  const retainMs = opts.retainMs ?? DEFAULT_RETAIN_MS;
31527
32574
  const now = opts.nowMs ?? Date.now();
31528
32575
  const cutoff = now - retainMs;
31529
- const raw = readFileSync54(filePath, "utf-8");
32576
+ const raw = readFileSync55(filePath, "utf-8");
31530
32577
  const initialBytes = Buffer.byteLength(raw);
31531
32578
  const lines = raw.split("\n").filter((l) => l.trim().length > 0);
31532
32579
  const keep = [];
@@ -31551,7 +32598,7 @@ function archiveShadowLedger(unerrDir2, opts = {}) {
31551
32598
  }
31552
32599
  mkdirSync35(archiveDir, { recursive: true });
31553
32600
  const dateStamp = new Date(now).toISOString().slice(0, 10);
31554
- const archivePath = join65(archiveDir, `${dateStamp}.jsonl.gz`);
32601
+ const archivePath = join67(archiveDir, `${dateStamp}.jsonl.gz`);
31555
32602
  const payload = `${archive.join("\n")}
31556
32603
  `;
31557
32604
  const gz = gzipSync(Buffer.from(payload, "utf-8"));
@@ -31563,7 +32610,7 @@ function archiveShadowLedger(unerrDir2, opts = {}) {
31563
32610
  const tmpPath = `${filePath}.tmp-${Date.now()}-${Math.random().toString(36).slice(2)}`;
31564
32611
  const kept = `${keep.join("\n")}${keep.length > 0 ? "\n" : ""}`;
31565
32612
  writeFileSync29(tmpPath, kept, "utf-8");
31566
- const afterRaw = readFileSync54(filePath, "utf-8");
32613
+ const afterRaw = readFileSync55(filePath, "utf-8");
31567
32614
  const afterBytes = Buffer.byteLength(afterRaw);
31568
32615
  if (afterBytes > initialBytes) {
31569
32616
  const tail = afterRaw.slice(raw.length);
@@ -32072,12 +33119,12 @@ import { randomBytes as randomBytes5 } from "crypto";
32072
33119
  import {
32073
33120
  existsSync as existsSync64,
32074
33121
  mkdirSync as mkdirSync36,
32075
- readFileSync as readFileSync55,
32076
- readdirSync as readdirSync14,
33122
+ readFileSync as readFileSync56,
33123
+ readdirSync as readdirSync15,
32077
33124
  rmSync as rmSync2,
32078
33125
  writeFileSync as writeFileSync30
32079
33126
  } from "fs";
32080
- import { join as join66 } from "path";
33127
+ import { join as join68 } from "path";
32081
33128
  var _log2, MAX_SNAPSHOTS, AUTO_SNAPSHOT_COOLDOWN_MS, WorkingSnapshotStore;
32082
33129
  var init_working_snapshots = __esm({
32083
33130
  "src/tracking/working-snapshots.ts"() {
@@ -32095,7 +33142,7 @@ var init_working_snapshots = __esm({
32095
33142
  unerrDir;
32096
33143
  constructor(unerrDir2) {
32097
33144
  this.unerrDir = unerrDir2;
32098
- this.snapshotDir = join66(unerrDir2, "snapshots");
33145
+ this.snapshotDir = join68(unerrDir2, "snapshots");
32099
33146
  if (!existsSync64(this.snapshotDir)) {
32100
33147
  mkdirSync36(this.snapshotDir, { recursive: true });
32101
33148
  }
@@ -32116,7 +33163,7 @@ var init_working_snapshots = __esm({
32116
33163
  processed: false
32117
33164
  };
32118
33165
  writeFileSync30(
32119
- join66(this.snapshotDir, `${id}.json`),
33166
+ join68(this.snapshotDir, `${id}.json`),
32120
33167
  JSON.stringify(snapshot, null, 2),
32121
33168
  "utf-8"
32122
33169
  );
@@ -32130,10 +33177,10 @@ var init_working_snapshots = __esm({
32130
33177
  * Get a snapshot by ID.
32131
33178
  */
32132
33179
  get(snapshotId) {
32133
- const filePath = join66(this.snapshotDir, `${snapshotId}.json`);
33180
+ const filePath = join68(this.snapshotDir, `${snapshotId}.json`);
32134
33181
  if (!existsSync64(filePath)) return null;
32135
33182
  try {
32136
- return JSON.parse(readFileSync55(filePath, "utf-8"));
33183
+ return JSON.parse(readFileSync56(filePath, "utf-8"));
32137
33184
  } catch {
32138
33185
  return null;
32139
33186
  }
@@ -32143,13 +33190,13 @@ var init_working_snapshots = __esm({
32143
33190
  */
32144
33191
  list() {
32145
33192
  if (!existsSync64(this.snapshotDir)) return [];
32146
- const files = readdirSync14(this.snapshotDir).filter(
33193
+ const files = readdirSync15(this.snapshotDir).filter(
32147
33194
  (f) => f.endsWith(".json")
32148
33195
  );
32149
33196
  const snapshots = [];
32150
33197
  for (const file of files) {
32151
33198
  try {
32152
- const raw = readFileSync55(join66(this.snapshotDir, file), "utf-8");
33199
+ const raw = readFileSync56(join68(this.snapshotDir, file), "utf-8");
32153
33200
  snapshots.push(JSON.parse(raw));
32154
33201
  } catch {
32155
33202
  }
@@ -32190,7 +33237,7 @@ var init_working_snapshots = __esm({
32190
33237
  if (!snapshot) return;
32191
33238
  snapshot.processed = true;
32192
33239
  writeFileSync30(
32193
- join66(this.snapshotDir, `${snapshotId}.json`),
33240
+ join68(this.snapshotDir, `${snapshotId}.json`),
32194
33241
  JSON.stringify(snapshot, null, 2),
32195
33242
  "utf-8"
32196
33243
  );
@@ -32205,7 +33252,7 @@ var init_working_snapshots = __esm({
32205
33252
  * Delete a snapshot.
32206
33253
  */
32207
33254
  delete(snapshotId) {
32208
- const filePath = join66(this.snapshotDir, `${snapshotId}.json`);
33255
+ const filePath = join68(this.snapshotDir, `${snapshotId}.json`);
32209
33256
  if (!existsSync64(filePath)) return false;
32210
33257
  rmSync2(filePath);
32211
33258
  return true;
@@ -32214,10 +33261,10 @@ var init_working_snapshots = __esm({
32214
33261
  * Get the timeline branch counter from branch_context.json.
32215
33262
  */
32216
33263
  getTimelineBranch() {
32217
- const contextPath = join66(this.unerrDir, "branch_context.json");
33264
+ const contextPath = join68(this.unerrDir, "branch_context.json");
32218
33265
  if (!existsSync64(contextPath)) return 0;
32219
33266
  try {
32220
- const ctx = JSON.parse(readFileSync55(contextPath, "utf-8"));
33267
+ const ctx = JSON.parse(readFileSync56(contextPath, "utf-8"));
32221
33268
  return ctx.timelineBranch ?? 0;
32222
33269
  } catch {
32223
33270
  return 0;
@@ -32227,11 +33274,11 @@ var init_working_snapshots = __esm({
32227
33274
  * Increment the timeline branch counter. Returns the new value.
32228
33275
  */
32229
33276
  incrementTimelineBranch() {
32230
- const contextPath = join66(this.unerrDir, "branch_context.json");
33277
+ const contextPath = join68(this.unerrDir, "branch_context.json");
32231
33278
  let ctx = {};
32232
33279
  if (existsSync64(contextPath)) {
32233
33280
  try {
32234
- ctx = JSON.parse(readFileSync55(contextPath, "utf-8"));
33281
+ ctx = JSON.parse(readFileSync56(contextPath, "utf-8"));
32235
33282
  } catch {
32236
33283
  ctx = {};
32237
33284
  }
@@ -32404,8 +33451,8 @@ var quality_signals_exports = {};
32404
33451
  __export(quality_signals_exports, {
32405
33452
  QualitySignalTracker: () => QualitySignalTracker
32406
33453
  });
32407
- import { existsSync as existsSync65, readFileSync as readFileSync56, writeFileSync as writeFileSync31 } from "fs";
32408
- import { join as join67 } from "path";
33454
+ import { existsSync as existsSync65, readFileSync as readFileSync57, writeFileSync as writeFileSync31 } from "fs";
33455
+ import { join as join69 } from "path";
32409
33456
  function computeDurabilityFromAge(survivalMs) {
32410
33457
  if (survivalMs < FRAGILE_THRESHOLD_MS) {
32411
33458
  return 0.1 + survivalMs / FRAGILE_THRESHOLD_MS * 0.2;
@@ -32432,7 +33479,7 @@ var init_quality_signals = __esm({
32432
33479
  /** Maximum corrections to retain in memory/disk. */
32433
33480
  static MAX_CORRECTIONS = 200;
32434
33481
  constructor(unerrDir2) {
32435
- this.signalsPath = join67(unerrDir2, "state", "quality_signals.json");
33482
+ this.signalsPath = join69(unerrDir2, "state", "quality_signals.json");
32436
33483
  this.signals = this.load();
32437
33484
  }
32438
33485
  /**
@@ -32524,7 +33571,7 @@ var init_quality_signals = __esm({
32524
33571
  */
32525
33572
  save() {
32526
33573
  try {
32527
- const dir = join67(this.signalsPath, "..");
33574
+ const dir = join69(this.signalsPath, "..");
32528
33575
  if (!existsSync65(dir)) {
32529
33576
  const { mkdirSync: mkdirSync48 } = __require("fs");
32530
33577
  mkdirSync48(dir, { recursive: true });
@@ -32580,7 +33627,7 @@ var init_quality_signals = __esm({
32580
33627
  }
32581
33628
  try {
32582
33629
  return JSON.parse(
32583
- readFileSync56(this.signalsPath, "utf-8")
33630
+ readFileSync57(this.signalsPath, "utf-8")
32584
33631
  );
32585
33632
  } catch {
32586
33633
  return {
@@ -33599,8 +34646,8 @@ var incomplete_work_exports = {};
33599
34646
  __export(incomplete_work_exports, {
33600
34647
  IncompleteWorkDetector: () => IncompleteWorkDetector
33601
34648
  });
33602
- import { existsSync as existsSync66, mkdirSync as mkdirSync37, readFileSync as readFileSync57, writeFileSync as writeFileSync32 } from "fs";
33603
- import { join as join68 } from "path";
34649
+ import { existsSync as existsSync66, mkdirSync as mkdirSync37, readFileSync as readFileSync58, writeFileSync as writeFileSync32 } from "fs";
34650
+ import { join as join70 } from "path";
33604
34651
  function severityRank(severity) {
33605
34652
  switch (severity) {
33606
34653
  case "high":
@@ -33793,9 +34840,9 @@ var init_incomplete_work = __esm({
33793
34840
  persistItems(items) {
33794
34841
  if (!this.unerrDir) return false;
33795
34842
  try {
33796
- const stateDir = join68(this.unerrDir, "state");
34843
+ const stateDir = join70(this.unerrDir, "state");
33797
34844
  if (!existsSync66(stateDir)) mkdirSync37(stateDir, { recursive: true });
33798
- const filePath = join68(stateDir, PERSISTENCE_FILE);
34845
+ const filePath = join70(stateDir, PERSISTENCE_FILE);
33799
34846
  writeFileSync32(
33800
34847
  filePath,
33801
34848
  JSON.stringify({
@@ -33815,9 +34862,9 @@ var init_incomplete_work = __esm({
33815
34862
  */
33816
34863
  static readPersistedItems(unerrDir2) {
33817
34864
  try {
33818
- const filePath = join68(unerrDir2, "state", PERSISTENCE_FILE);
34865
+ const filePath = join70(unerrDir2, "state", PERSISTENCE_FILE);
33819
34866
  if (!existsSync66(filePath)) return [];
33820
- const data = JSON.parse(readFileSync57(filePath, "utf-8"));
34867
+ const data = JSON.parse(readFileSync58(filePath, "utf-8"));
33821
34868
  return data.items ?? [];
33822
34869
  } catch {
33823
34870
  return [];
@@ -35597,8 +36644,8 @@ __export(git_trailers_exports, {
35597
36644
  parseTrailersFromMessage: () => parseTrailersFromMessage,
35598
36645
  uninstallPrepareCommitMsgHook: () => uninstallPrepareCommitMsgHook
35599
36646
  });
35600
- import { existsSync as existsSync68, readFileSync as readFileSync58, writeFileSync as writeFileSync33 } from "fs";
35601
- import { join as join69 } from "path";
36647
+ import { existsSync as existsSync68, readFileSync as readFileSync59, writeFileSync as writeFileSync33 } from "fs";
36648
+ import { join as join71 } from "path";
35602
36649
  function getCommitTrailers(ledger, timelineBranch, branch) {
35603
36650
  const recent = ledger.getRecentEntries(1);
35604
36651
  if (recent.length === 0) return null;
@@ -35618,15 +36665,15 @@ function formatTrailers(trailers) {
35618
36665
  ].join("\n");
35619
36666
  }
35620
36667
  function installPrepareCommitMsgHook(projectRoot) {
35621
- const hooksDir = join69(projectRoot, ".git", "hooks");
36668
+ const hooksDir = join71(projectRoot, ".git", "hooks");
35622
36669
  if (!existsSync68(hooksDir)) {
35623
36670
  return false;
35624
36671
  }
35625
- const hookPath = join69(hooksDir, "prepare-commit-msg");
36672
+ const hookPath = join71(hooksDir, "prepare-commit-msg");
35626
36673
  const marker = "# unerr-trailer-injection";
35627
36674
  if (existsSync68(hookPath)) {
35628
36675
  try {
35629
- const existing = readFileSync58(hookPath, "utf-8");
36676
+ const existing = readFileSync59(hookPath, "utf-8");
35630
36677
  if (existing.includes(marker)) {
35631
36678
  return true;
35632
36679
  }
@@ -35655,10 +36702,10 @@ ${generateHookScript()}`;
35655
36702
  }
35656
36703
  }
35657
36704
  function uninstallPrepareCommitMsgHook(projectRoot) {
35658
- const hookPath = join69(projectRoot, ".git", "hooks", "prepare-commit-msg");
36705
+ const hookPath = join71(projectRoot, ".git", "hooks", "prepare-commit-msg");
35659
36706
  if (!existsSync68(hookPath)) return true;
35660
36707
  try {
35661
- const content = readFileSync58(hookPath, "utf-8");
36708
+ const content = readFileSync59(hookPath, "utf-8");
35662
36709
  const marker = "# unerr-trailer-injection";
35663
36710
  if (!content.includes(marker)) return true;
35664
36711
  const lines = content.split("\n");
@@ -35825,13 +36872,13 @@ var init_http_transport = __esm({
35825
36872
  import {
35826
36873
  existsSync as existsSync69,
35827
36874
  mkdirSync as mkdirSync39,
35828
- readFileSync as readFileSync59,
35829
- readdirSync as readdirSync15,
36875
+ readFileSync as readFileSync60,
36876
+ readdirSync as readdirSync16,
35830
36877
  rmSync as rmSync3,
35831
36878
  statSync as statSync11,
35832
36879
  writeFileSync as writeFileSync34
35833
36880
  } from "fs";
35834
- import { join as join70 } from "path";
36881
+ import { join as join72 } from "path";
35835
36882
  function sanitizeBranchName(branch) {
35836
36883
  return branch.replace(/\//g, "__").replace(/[^a-zA-Z0-9_.\-]/g, "_");
35837
36884
  }
@@ -35849,7 +36896,7 @@ var init_branch_snapshot = __esm({
35849
36896
  branchDir;
35850
36897
  projectRoot;
35851
36898
  constructor(unerrDir2, projectRoot) {
35852
- this.branchDir = join70(unerrDir2, "drift", "branches");
36899
+ this.branchDir = join72(unerrDir2, "drift", "branches");
35853
36900
  this.projectRoot = projectRoot;
35854
36901
  }
35855
36902
  /**
@@ -35863,7 +36910,7 @@ var init_branch_snapshot = __esm({
35863
36910
  log17.info(`No drift entities/edges to snapshot for branch ${branch}`);
35864
36911
  }
35865
36912
  const dirName = sanitizeBranchName(branch);
35866
- const snapshotDir = join70(this.branchDir, dirName);
36913
+ const snapshotDir = join72(this.branchDir, dirName);
35867
36914
  if (!existsSync69(snapshotDir)) {
35868
36915
  mkdirSync39(snapshotDir, { recursive: true });
35869
36916
  }
@@ -35875,12 +36922,12 @@ var init_branch_snapshot = __esm({
35875
36922
  savedAt: (/* @__PURE__ */ new Date()).toISOString()
35876
36923
  };
35877
36924
  writeFileSync34(
35878
- join70(snapshotDir, OVERLAY_FILE),
36925
+ join72(snapshotDir, OVERLAY_FILE),
35879
36926
  JSON.stringify(snapshot, null, 2),
35880
36927
  "utf-8"
35881
36928
  );
35882
36929
  writeFileSync34(
35883
- join70(snapshotDir, HASHES_FILE),
36930
+ join72(snapshotDir, HASHES_FILE),
35884
36931
  JSON.stringify(fileHashState, null, 2),
35885
36932
  "utf-8"
35886
36933
  );
@@ -35896,14 +36943,14 @@ var init_branch_snapshot = __esm({
35896
36943
  */
35897
36944
  async restoreSnapshot(branch, localGraph) {
35898
36945
  const dirName = sanitizeBranchName(branch);
35899
- const snapshotDir = join70(this.branchDir, dirName);
35900
- const overlayPath = join70(snapshotDir, OVERLAY_FILE);
36946
+ const snapshotDir = join72(this.branchDir, dirName);
36947
+ const overlayPath = join72(snapshotDir, OVERLAY_FILE);
35901
36948
  if (!existsSync69(overlayPath)) {
35902
36949
  log17.info(`No snapshot for branch ${branch} \u2014 first visit`);
35903
36950
  return null;
35904
36951
  }
35905
36952
  try {
35906
- const raw = readFileSync59(overlayPath, "utf-8");
36953
+ const raw = readFileSync60(overlayPath, "utf-8");
35907
36954
  const snapshot = JSON.parse(raw);
35908
36955
  for (const entity of snapshot.entities) {
35909
36956
  await localGraph.upsertDriftEntity(entity);
@@ -35915,7 +36962,7 @@ var init_branch_snapshot = __esm({
35915
36962
  }
35916
36963
  const now = /* @__PURE__ */ new Date();
35917
36964
  writeFileSync34(
35918
- join70(snapshotDir, ".last_access"),
36965
+ join72(snapshotDir, ".last_access"),
35919
36966
  now.toISOString(),
35920
36967
  "utf-8"
35921
36968
  );
@@ -35935,17 +36982,17 @@ var init_branch_snapshot = __esm({
35935
36982
  */
35936
36983
  hasSnapshot(branch) {
35937
36984
  const dirName = sanitizeBranchName(branch);
35938
- return existsSync69(join70(this.branchDir, dirName, OVERLAY_FILE));
36985
+ return existsSync69(join72(this.branchDir, dirName, OVERLAY_FILE));
35939
36986
  }
35940
36987
  /**
35941
36988
  * Get the file hash state from a branch snapshot.
35942
36989
  */
35943
36990
  getSnapshotFileHashes(branch) {
35944
36991
  const dirName = sanitizeBranchName(branch);
35945
- const hashesPath = join70(this.branchDir, dirName, HASHES_FILE);
36992
+ const hashesPath = join72(this.branchDir, dirName, HASHES_FILE);
35946
36993
  if (!existsSync69(hashesPath)) return null;
35947
36994
  try {
35948
- const raw = readFileSync59(hashesPath, "utf-8");
36995
+ const raw = readFileSync60(hashesPath, "utf-8");
35949
36996
  return JSON.parse(raw);
35950
36997
  } catch {
35951
36998
  return null;
@@ -35956,7 +37003,7 @@ var init_branch_snapshot = __esm({
35956
37003
  */
35957
37004
  deleteSnapshot(branch) {
35958
37005
  const dirName = sanitizeBranchName(branch);
35959
- const snapshotDir = join70(this.branchDir, dirName);
37006
+ const snapshotDir = join72(this.branchDir, dirName);
35960
37007
  if (!existsSync69(snapshotDir)) return false;
35961
37008
  rmSync3(snapshotDir, { recursive: true, force: true });
35962
37009
  log17.info(`Deleted branch snapshot: ${branch}`);
@@ -35973,7 +37020,7 @@ var init_branch_snapshot = __esm({
35973
37020
  let removed = 0;
35974
37021
  for (const snapshot of snapshots) {
35975
37022
  if (!gitBranches.has(snapshot.branch)) {
35976
- const snapshotDir = join70(this.branchDir, snapshot.id);
37023
+ const snapshotDir = join72(this.branchDir, snapshot.id);
35977
37024
  rmSync3(snapshotDir, { recursive: true, force: true });
35978
37025
  log17.info(`GC removed snapshot for deleted branch: ${snapshot.branch}`);
35979
37026
  removed++;
@@ -35987,16 +37034,16 @@ var init_branch_snapshot = __esm({
35987
37034
  listSnapshots() {
35988
37035
  if (!existsSync69(this.branchDir)) return [];
35989
37036
  try {
35990
- const entries = readdirSync15(this.branchDir, { withFileTypes: true });
37037
+ const entries = readdirSync16(this.branchDir, { withFileTypes: true });
35991
37038
  const snapshots = [];
35992
37039
  for (const entry of entries) {
35993
37040
  if (!entry.isDirectory()) continue;
35994
- const overlayPath = join70(this.branchDir, entry.name, OVERLAY_FILE);
37041
+ const overlayPath = join72(this.branchDir, entry.name, OVERLAY_FILE);
35995
37042
  if (!existsSync69(overlayPath)) continue;
35996
37043
  try {
35997
- const raw = readFileSync59(overlayPath, "utf-8");
37044
+ const raw = readFileSync60(overlayPath, "utf-8");
35998
37045
  const snapshot = JSON.parse(raw);
35999
- const accessPath = join70(this.branchDir, entry.name, ".last_access");
37046
+ const accessPath = join72(this.branchDir, entry.name, ".last_access");
36000
37047
  let accessedAt;
36001
37048
  if (existsSync69(accessPath)) {
36002
37049
  accessedAt = statSync11(accessPath).mtime;
@@ -36025,7 +37072,7 @@ var init_branch_snapshot = __esm({
36025
37072
  if (snapshots.length <= MAX_BRANCH_SNAPSHOTS) return;
36026
37073
  const toRemove = snapshots.slice(MAX_BRANCH_SNAPSHOTS);
36027
37074
  for (const snapshot of toRemove) {
36028
- const dir = join70(this.branchDir, snapshot.id);
37075
+ const dir = join72(this.branchDir, snapshot.id);
36029
37076
  rmSync3(dir, { recursive: true, force: true });
36030
37077
  log17.info(`LRU evicted branch snapshot: ${snapshot.branch}`);
36031
37078
  }
@@ -36040,17 +37087,17 @@ __export(file_hash_state_exports, {
36040
37087
  FileHashManager: () => FileHashManager,
36041
37088
  contentSha256: () => contentSha256
36042
37089
  });
36043
- import { createHash as createHash3 } from "crypto";
37090
+ import { createHash as createHash4 } from "crypto";
36044
37091
  import {
36045
37092
  existsSync as existsSync70,
36046
37093
  mkdirSync as mkdirSync40,
36047
- readFileSync as readFileSync60,
37094
+ readFileSync as readFileSync61,
36048
37095
  renameSync as renameSync4,
36049
37096
  writeFileSync as writeFileSync35
36050
37097
  } from "fs";
36051
- import { join as join71 } from "path";
37098
+ import { join as join73 } from "path";
36052
37099
  function contentSha256(content) {
36053
- return createHash3("sha256").update(content).digest("hex");
37100
+ return createHash4("sha256").update(content).digest("hex");
36054
37101
  }
36055
37102
  var STATE_FILE2, FileHashManager;
36056
37103
  var init_file_hash_state = __esm({
@@ -36062,8 +37109,8 @@ var init_file_hash_state = __esm({
36062
37109
  statePath;
36063
37110
  state;
36064
37111
  constructor(unerrDir2) {
36065
- this.stateDir = join71(unerrDir2, "state");
36066
- this.statePath = join71(this.stateDir, STATE_FILE2);
37112
+ this.stateDir = join73(unerrDir2, "state");
37113
+ this.statePath = join73(this.stateDir, STATE_FILE2);
36067
37114
  this.state = this.load();
36068
37115
  }
36069
37116
  /**
@@ -36139,7 +37186,7 @@ var init_file_hash_state = __esm({
36139
37186
  return { files: {} };
36140
37187
  }
36141
37188
  try {
36142
- const raw = readFileSync60(this.statePath, "utf-8");
37189
+ const raw = readFileSync61(this.statePath, "utf-8");
36143
37190
  return JSON.parse(raw);
36144
37191
  } catch {
36145
37192
  return { files: {} };
@@ -36153,13 +37200,13 @@ var init_file_hash_state = __esm({
36153
37200
  import {
36154
37201
  existsSync as existsSync71,
36155
37202
  mkdirSync as mkdirSync41,
36156
- readFileSync as readFileSync61,
36157
- readdirSync as readdirSync16,
37203
+ readFileSync as readFileSync62,
37204
+ readdirSync as readdirSync17,
36158
37205
  rmSync as rmSync4,
36159
37206
  statSync as statSync12,
36160
37207
  writeFileSync as writeFileSync36
36161
37208
  } from "fs";
36162
- import { join as join72 } from "path";
37209
+ import { join as join74 } from "path";
36163
37210
  var MAX_STASH_SNAPSHOTS, OVERLAY_FILE2, HASHES_FILE2, _log6, StashManager;
36164
37211
  var init_stash_manager = __esm({
36165
37212
  "src/tracking/stash-manager.ts"() {
@@ -36177,8 +37224,8 @@ var init_stash_manager = __esm({
36177
37224
  constructor(unerrDir2, projectRoot) {
36178
37225
  this.unerrDir = unerrDir2;
36179
37226
  this.projectRoot = projectRoot;
36180
- this.stashDir = join72(unerrDir2, "drift", "stash");
36181
- this.gitDir = join72(projectRoot, ".git");
37227
+ this.stashDir = join74(unerrDir2, "drift", "stash");
37228
+ this.gitDir = join74(projectRoot, ".git");
36182
37229
  this.previousStashRef = this.readStashRef();
36183
37230
  this.previousStashCount = this.getStashCount();
36184
37231
  }
@@ -36223,7 +37270,7 @@ var init_stash_manager = __esm({
36223
37270
  return null;
36224
37271
  }
36225
37272
  const snapshotId = stashRef.slice(0, 12);
36226
- const snapshotDir = join72(this.stashDir, snapshotId);
37273
+ const snapshotDir = join74(this.stashDir, snapshotId);
36227
37274
  if (!existsSync71(snapshotDir)) {
36228
37275
  mkdirSync41(snapshotDir, { recursive: true });
36229
37276
  }
@@ -36235,12 +37282,12 @@ var init_stash_manager = __esm({
36235
37282
  savedAt: (/* @__PURE__ */ new Date()).toISOString()
36236
37283
  };
36237
37284
  writeFileSync36(
36238
- join72(snapshotDir, OVERLAY_FILE2),
37285
+ join74(snapshotDir, OVERLAY_FILE2),
36239
37286
  JSON.stringify(snapshot, null, 2),
36240
37287
  "utf-8"
36241
37288
  );
36242
37289
  writeFileSync36(
36243
- join72(snapshotDir, HASHES_FILE2),
37290
+ join74(snapshotDir, HASHES_FILE2),
36244
37291
  JSON.stringify(fileHashState, null, 2),
36245
37292
  "utf-8"
36246
37293
  );
@@ -36261,14 +37308,14 @@ var init_stash_manager = __esm({
36261
37308
  return 0;
36262
37309
  }
36263
37310
  const latest = snapshots[0];
36264
- const snapshotDir = join72(this.stashDir, latest.id);
36265
- const overlayPath = join72(snapshotDir, OVERLAY_FILE2);
37311
+ const snapshotDir = join74(this.stashDir, latest.id);
37312
+ const overlayPath = join74(snapshotDir, OVERLAY_FILE2);
36266
37313
  if (!existsSync71(overlayPath)) {
36267
37314
  _log6.warn(`Snapshot ${latest.id} missing overlay file`);
36268
37315
  return 0;
36269
37316
  }
36270
37317
  try {
36271
- const raw = readFileSync61(overlayPath, "utf-8");
37318
+ const raw = readFileSync62(overlayPath, "utf-8");
36272
37319
  const snapshot = JSON.parse(raw);
36273
37320
  for (const entity of snapshot.entities) {
36274
37321
  await localGraph.upsertDriftEntity(entity);
@@ -36298,10 +37345,10 @@ var init_stash_manager = __esm({
36298
37345
  const snapshots = this.listSnapshots();
36299
37346
  if (snapshots.length === 0) return null;
36300
37347
  const latest = snapshots[0];
36301
- const hashesPath = join72(this.stashDir, latest.id, HASHES_FILE2);
37348
+ const hashesPath = join74(this.stashDir, latest.id, HASHES_FILE2);
36302
37349
  if (!existsSync71(hashesPath)) return null;
36303
37350
  try {
36304
- const raw = readFileSync61(hashesPath, "utf-8");
37351
+ const raw = readFileSync62(hashesPath, "utf-8");
36305
37352
  return JSON.parse(raw);
36306
37353
  } catch {
36307
37354
  return null;
@@ -36312,7 +37359,7 @@ var init_stash_manager = __esm({
36312
37359
  */
36313
37360
  dropSnapshot(stashRef) {
36314
37361
  const snapshotId = stashRef.slice(0, 12);
36315
- const snapshotDir = join72(this.stashDir, snapshotId);
37362
+ const snapshotDir = join74(this.stashDir, snapshotId);
36316
37363
  if (!existsSync71(snapshotDir)) return false;
36317
37364
  rmSync4(snapshotDir, { recursive: true, force: true });
36318
37365
  _log6.info(`Dropped stash snapshot: ${snapshotId}`);
@@ -36324,11 +37371,11 @@ var init_stash_manager = __esm({
36324
37371
  listSnapshots() {
36325
37372
  if (!existsSync71(this.stashDir)) return [];
36326
37373
  try {
36327
- const entries = readdirSync16(this.stashDir, { withFileTypes: true });
37374
+ const entries = readdirSync17(this.stashDir, { withFileTypes: true });
36328
37375
  const snapshots = [];
36329
37376
  for (const entry of entries) {
36330
37377
  if (!entry.isDirectory()) continue;
36331
- const overlayPath = join72(this.stashDir, entry.name, OVERLAY_FILE2);
37378
+ const overlayPath = join74(this.stashDir, entry.name, OVERLAY_FILE2);
36332
37379
  if (!existsSync71(overlayPath)) continue;
36333
37380
  try {
36334
37381
  const stat2 = statSync12(overlayPath);
@@ -36346,10 +37393,10 @@ var init_stash_manager = __esm({
36346
37393
  * Read the current stash ref SHA from `.git/refs/stash`.
36347
37394
  */
36348
37395
  readStashRef() {
36349
- const stashPath = join72(this.gitDir, "refs", "stash");
37396
+ const stashPath = join74(this.gitDir, "refs", "stash");
36350
37397
  if (!existsSync71(stashPath)) return null;
36351
37398
  try {
36352
- return readFileSync61(stashPath, "utf-8").trim() || null;
37399
+ return readFileSync62(stashPath, "utf-8").trim() || null;
36353
37400
  } catch {
36354
37401
  return null;
36355
37402
  }
@@ -36358,10 +37405,10 @@ var init_stash_manager = __esm({
36358
37405
  * Count current stash entries via `.git/logs/refs/stash`.
36359
37406
  */
36360
37407
  getStashCount() {
36361
- const logPath = join72(this.gitDir, "logs", "refs", "stash");
37408
+ const logPath = join74(this.gitDir, "logs", "refs", "stash");
36362
37409
  if (!existsSync71(logPath)) return 0;
36363
37410
  try {
36364
- const content = readFileSync61(logPath, "utf-8");
37411
+ const content = readFileSync62(logPath, "utf-8");
36365
37412
  return content.split("\n").filter((line) => line.trim().length > 0).length;
36366
37413
  } catch {
36367
37414
  return 0;
@@ -36375,7 +37422,7 @@ var init_stash_manager = __esm({
36375
37422
  if (snapshots.length <= MAX_STASH_SNAPSHOTS) return;
36376
37423
  const toRemove = snapshots.slice(MAX_STASH_SNAPSHOTS);
36377
37424
  for (const snapshot of toRemove) {
36378
- const dir = join72(this.stashDir, snapshot.id);
37425
+ const dir = join74(this.stashDir, snapshot.id);
36379
37426
  rmSync4(dir, { recursive: true, force: true });
36380
37427
  _log6.info(`LRU evicted stash snapshot: ${snapshot.id}`);
36381
37428
  }
@@ -36395,11 +37442,11 @@ __export(drift_tracker_exports, {
36395
37442
  import {
36396
37443
  existsSync as existsSync72,
36397
37444
  mkdirSync as mkdirSync42,
36398
- readFileSync as readFileSync62,
37445
+ readFileSync as readFileSync63,
36399
37446
  statSync as statSync13,
36400
37447
  writeFileSync as writeFileSync37
36401
37448
  } from "fs";
36402
- import { join as join73 } from "path";
37449
+ import { join as join75 } from "path";
36403
37450
  function determineOrigin(lastSyncTimestamp) {
36404
37451
  if (lastSyncTimestamp === 0) return "human";
36405
37452
  const elapsed = Date.now() - lastSyncTimestamp;
@@ -36610,7 +37657,7 @@ var init_drift_tracker = __esm({
36610
37657
  crossFileInvalidated: 0,
36611
37658
  edgesExtracted: 0
36612
37659
  };
36613
- const absPath = filePath.startsWith("/") ? filePath : join73(this.config.projectRoot, filePath);
37660
+ const absPath = filePath.startsWith("/") ? filePath : join75(this.config.projectRoot, filePath);
36614
37661
  const relPath = filePath.startsWith("/") ? filePath.slice(this.config.projectRoot.length + 1) : filePath;
36615
37662
  const language = detectLanguage2(relPath);
36616
37663
  if (!language) return result;
@@ -36656,7 +37703,7 @@ var init_drift_tracker = __esm({
36656
37703
  this.maybeEmitDrift(relPath, result);
36657
37704
  return result;
36658
37705
  }
36659
- const content = readFileSync62(absPath, "utf-8");
37706
+ const content = readFileSync63(absPath, "utf-8");
36660
37707
  const sha = contentSha256(content);
36661
37708
  const decision = this.fileHashManager.shouldProcess(relPath, sha, headSha);
36662
37709
  if (decision === "skip") {
@@ -36890,7 +37937,7 @@ var init_drift_tracker = __esm({
36890
37937
  return await this.stashManager.restoreSnapshot(this.localGraph);
36891
37938
  }
36892
37939
  async markFileDeleted(filePath, intentId) {
36893
- const absPath = filePath.startsWith("/") ? filePath : join73(this.config.projectRoot, filePath);
37940
+ const absPath = filePath.startsWith("/") ? filePath : join75(this.config.projectRoot, filePath);
36894
37941
  this.mtimeCache.evict(absPath);
36895
37942
  const baseEntities = await this.localGraph.getEntitiesByFile(filePath);
36896
37943
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -36975,13 +38022,13 @@ var init_drift_tracker = __esm({
36975
38022
  return invalidated;
36976
38023
  }
36977
38024
  async saveDriftSummary() {
36978
- const driftDir = join73(this.config.unerrDir, "drift");
38025
+ const driftDir = join75(this.config.unerrDir, "drift");
36979
38026
  if (!existsSync72(driftDir)) {
36980
38027
  mkdirSync42(driftDir, { recursive: true });
36981
38028
  }
36982
38029
  const summary = await this.getDriftSummary();
36983
38030
  writeFileSync37(
36984
- join73(driftDir, "drift_summary.json"),
38031
+ join75(driftDir, "drift_summary.json"),
36985
38032
  JSON.stringify(summary, null, 2),
36986
38033
  "utf-8"
36987
38034
  );
@@ -37392,8 +38439,8 @@ var incremental_indexer_exports = {};
37392
38439
  __export(incremental_indexer_exports, {
37393
38440
  indexFilesIncremental: () => indexFilesIncremental
37394
38441
  });
37395
- import { existsSync as existsSync73, readFileSync as readFileSync63 } from "fs";
37396
- import { join as join74, relative as relative5 } from "path";
38442
+ import { existsSync as existsSync73, readFileSync as readFileSync64 } from "fs";
38443
+ import { join as join76, relative as relative5 } from "path";
37397
38444
  async function indexFilesIncremental(projectRoot, changedFiles, graphStore, repoId) {
37398
38445
  const startMs = Date.now();
37399
38446
  const db = {
@@ -37411,7 +38458,7 @@ async function indexFilesIncremental(projectRoot, changedFiles, graphStore, repo
37411
38458
  const changedEntityKeys = /* @__PURE__ */ new Set();
37412
38459
  const deletedEntityKeys = /* @__PURE__ */ new Set();
37413
38460
  for (const filePath of changedFiles) {
37414
- const absPath = filePath.startsWith("/") ? filePath : join74(projectRoot, filePath);
38461
+ const absPath = filePath.startsWith("/") ? filePath : join76(projectRoot, filePath);
37415
38462
  const relPath = filePath.startsWith("/") ? relative5(projectRoot, filePath) : filePath;
37416
38463
  if (!existsSync73(absPath)) {
37417
38464
  const deleted2 = await deleteFileFromGraph(db, relPath);
@@ -37426,7 +38473,7 @@ async function indexFilesIncremental(projectRoot, changedFiles, graphStore, repo
37426
38473
  }
37427
38474
  let content;
37428
38475
  try {
37429
- content = readFileSync63(absPath, "utf-8");
38476
+ content = readFileSync64(absPath, "utf-8");
37430
38477
  } catch {
37431
38478
  continue;
37432
38479
  }
@@ -38604,8 +39651,8 @@ var workspace_manifest_exports = {};
38604
39651
  __export(workspace_manifest_exports, {
38605
39652
  WorkspaceManifest: () => WorkspaceManifest
38606
39653
  });
38607
- import { existsSync as existsSync74, mkdirSync as mkdirSync43, readFileSync as readFileSync64, writeFileSync as writeFileSync38 } from "fs";
38608
- import { join as join75 } from "path";
39654
+ import { existsSync as existsSync74, mkdirSync as mkdirSync43, readFileSync as readFileSync65, writeFileSync as writeFileSync38 } from "fs";
39655
+ import { join as join77 } from "path";
38609
39656
  var WorkspaceManifest;
38610
39657
  var init_workspace_manifest = __esm({
38611
39658
  "src/tracking/workspace-manifest.ts"() {
@@ -38615,7 +39662,7 @@ var init_workspace_manifest = __esm({
38615
39662
  this.unerrDir = unerrDir2;
38616
39663
  this.repoId = repoId;
38617
39664
  this.sessionId = sessionId;
38618
- this.manifestPath = join75(unerrDir2, "manifest.json");
39665
+ this.manifestPath = join77(unerrDir2, "manifest.json");
38619
39666
  this.data = this.load();
38620
39667
  }
38621
39668
  unerrDir;
@@ -38720,7 +39767,7 @@ var init_workspace_manifest = __esm({
38720
39767
  };
38721
39768
  }
38722
39769
  try {
38723
- const raw = readFileSync64(this.manifestPath, "utf-8");
39770
+ const raw = readFileSync65(this.manifestPath, "utf-8");
38724
39771
  const parsed = JSON.parse(raw);
38725
39772
  if (parsed.repoId !== this.repoId) {
38726
39773
  return {
@@ -38945,7 +39992,7 @@ import {
38945
39992
  statSync as statSync14,
38946
39993
  watch
38947
39994
  } from "fs";
38948
- import { join as join76 } from "path";
39995
+ import { join as join78 } from "path";
38949
39996
  function formatSize(bytes) {
38950
39997
  if (bytes >= 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
38951
39998
  if (bytes >= 1024) return `${(bytes / 1024).toFixed(1)}KB`;
@@ -39092,9 +40139,9 @@ function tokenFlowRowToEntry(r) {
39092
40139
  };
39093
40140
  }
39094
40141
  function startLogTailer(cwd, options) {
39095
- const unerrDir2 = join76(cwd, ".unerr");
39096
- const logsDir = join76(unerrDir2, "logs");
39097
- const generalPath = join76(logsDir, "events.jsonl");
40142
+ const unerrDir2 = join78(cwd, ".unerr");
40143
+ const logsDir = join78(unerrDir2, "logs");
40144
+ const generalPath = join78(logsDir, "events.jsonl");
39098
40145
  const pollIntervalMs = options?.pollIntervalMs ?? 500;
39099
40146
  const generalState = {
39100
40147
  path: generalPath,
@@ -39240,13 +40287,13 @@ var init_middleware = __esm({
39240
40287
  });
39241
40288
 
39242
40289
  // src/server/routes/drift.ts
39243
- import { existsSync as existsSync76, readFileSync as readFileSync65, readdirSync as readdirSync17 } from "fs";
39244
- import { join as join77 } from "path";
40290
+ import { existsSync as existsSync76, readFileSync as readFileSync66, readdirSync as readdirSync18 } from "fs";
40291
+ import { join as join79 } from "path";
39245
40292
  import { Hono as Hono2 } from "hono";
39246
40293
  function readFlags(path7) {
39247
40294
  try {
39248
40295
  if (!existsSync76(path7)) return null;
39249
- return JSON.parse(readFileSync65(path7, "utf8"));
40296
+ return JSON.parse(readFileSync66(path7, "utf8"));
39250
40297
  } catch {
39251
40298
  return null;
39252
40299
  }
@@ -39254,7 +40301,7 @@ function readFlags(path7) {
39254
40301
  function listSessionFiles(stateDir) {
39255
40302
  try {
39256
40303
  if (!existsSync76(stateDir)) return [];
39257
- return readdirSync17(stateDir).filter((f) => f.startsWith("nudge-") && f.endsWith(".flags")).map((f) => join77(stateDir, f));
40304
+ return readdirSync18(stateDir).filter((f) => f.startsWith("nudge-") && f.endsWith(".flags")).map((f) => join79(stateDir, f));
39258
40305
  } catch {
39259
40306
  return [];
39260
40307
  }
@@ -39274,7 +40321,7 @@ function rowFor(file) {
39274
40321
  }
39275
40322
  function createDriftRoutes(deps) {
39276
40323
  const app = new Hono2();
39277
- const stateDir = join77(deps.cwd, ".unerr", "state");
40324
+ const stateDir = join79(deps.cwd, ".unerr", "state");
39278
40325
  app.get("/sessions", (c) => {
39279
40326
  const rows = [];
39280
40327
  for (const file of listSessionFiles(stateDir)) {
@@ -39295,7 +40342,7 @@ function createDriftRoutes(deps) {
39295
40342
  });
39296
40343
  app.get("/current", (c) => {
39297
40344
  const id = process.env.UNERR_SESSION_ID ?? `pid-${process.pid}`;
39298
- const file = join77(stateDir, `nudge-${id}.flags`);
40345
+ const file = join79(stateDir, `nudge-${id}.flags`);
39299
40346
  const row = rowFor(file);
39300
40347
  return c.json({
39301
40348
  data: row ?? {
@@ -41458,13 +42505,13 @@ function createSystemRoutes(deps) {
41458
42505
  });
41459
42506
  app.get("/config", async (c) => {
41460
42507
  const start = performance.now();
41461
- const { existsSync: existsSync82, readFileSync: readFileSync70 } = await import("fs");
41462
- const { join: join83 } = await import("path");
42508
+ const { existsSync: existsSync82, readFileSync: readFileSync71 } = await import("fs");
42509
+ const { join: join85 } = await import("path");
41463
42510
  let config = {};
41464
- const configPath2 = join83(deps.cwd, ".unerr", "config.json");
42511
+ const configPath2 = join85(deps.cwd, ".unerr", "config.json");
41465
42512
  if (existsSync82(configPath2)) {
41466
42513
  try {
41467
- config = JSON.parse(readFileSync70(configPath2, "utf-8"));
42514
+ config = JSON.parse(readFileSync71(configPath2, "utf-8"));
41468
42515
  } catch {
41469
42516
  config = { error: "unreadable" };
41470
42517
  }
@@ -42757,10 +43804,10 @@ var http_exports = {};
42757
43804
  __export(http_exports, {
42758
43805
  startDashboardServer: () => startDashboardServer
42759
43806
  });
42760
- import { existsSync as existsSync77, readFileSync as readFileSync66, unlinkSync as unlinkSync14, writeFileSync as writeFileSync39 } from "fs";
43807
+ import { existsSync as existsSync77, readFileSync as readFileSync67, unlinkSync as unlinkSync14, writeFileSync as writeFileSync39 } from "fs";
42761
43808
  import { createServer as createServer5 } from "net";
42762
- import { dirname as dirname17, join as join78 } from "path";
42763
- import { fileURLToPath as fileURLToPath3 } from "url";
43809
+ import { dirname as dirname17, join as join80 } from "path";
43810
+ import { fileURLToPath as fileURLToPath4 } from "url";
42764
43811
  import { serve as serve2 } from "@hono/node-server";
42765
43812
  import { serveStatic as serveStatic2 } from "@hono/node-server/serve-static";
42766
43813
  import { Hono as Hono13 } from "hono";
@@ -42819,10 +43866,10 @@ async function startDashboardServer(opts) {
42819
43866
  app.route("/api/router", createRouterApiV2(opts.routerV2));
42820
43867
  }
42821
43868
  if (!opts.apiOnly) {
42822
- const distDir = join78(dirname17(fileURLToPath3(import.meta.url)), "ui");
42823
- const spaIndex = join78(distDir, "index.html");
43869
+ const distDir = join80(dirname17(fileURLToPath4(import.meta.url)), "ui");
43870
+ const spaIndex = join80(distDir, "index.html");
42824
43871
  if (existsSync77(spaIndex)) {
42825
- const spaHtml = readFileSync66(spaIndex, "utf-8");
43872
+ const spaHtml = readFileSync67(spaIndex, "utf-8");
42826
43873
  app.use("*", serveStatic2({ root: distDir }));
42827
43874
  app.get("*", (c) => {
42828
43875
  const path7 = c.req.path;
@@ -42854,7 +43901,7 @@ async function startDashboardServer(opts) {
42854
43901
  port,
42855
43902
  hostname: "127.0.0.1"
42856
43903
  });
42857
- const serverJsonPath = join78(opts.stateDir, "server.json");
43904
+ const serverJsonPath = join80(opts.stateDir, "server.json");
42858
43905
  const serverInfo = {
42859
43906
  port,
42860
43907
  pid: process.pid,
@@ -43284,15 +44331,15 @@ import {
43284
44331
  existsSync as existsSync78,
43285
44332
  writeFileSync as fsWriteFileSync,
43286
44333
  mkdirSync as mkdirSync44,
43287
- readFileSync as readFileSync67,
43288
- readdirSync as readdirSync18
44334
+ readFileSync as readFileSync68,
44335
+ readdirSync as readdirSync19
43289
44336
  } from "fs";
43290
- import { join as join79 } from "path";
44337
+ import { join as join81 } from "path";
43291
44338
  async function getProxyFactStore(unerrDir2) {
43292
44339
  if (proxyFactStore !== void 0) return proxyFactStore;
43293
44340
  try {
43294
44341
  const { TemporalFactStore: TemporalFactStore2 } = await Promise.resolve().then(() => (init_temporal_facts(), temporal_facts_exports));
43295
- const cwd = join79(unerrDir2, "..");
44342
+ const cwd = join81(unerrDir2, "..");
43296
44343
  proxyFactStore = await TemporalFactStore2.create(cwd);
43297
44344
  return proxyFactStore;
43298
44345
  } catch {
@@ -43436,9 +44483,9 @@ async function handleRecallFactsProxy(args, unerrDir2, effectiveness) {
43436
44483
  }
43437
44484
  function migrateAgentPermissions(cwd) {
43438
44485
  try {
43439
- const settingsPath = join79(cwd, ".claude", "settings.json");
44486
+ const settingsPath = join81(cwd, ".claude", "settings.json");
43440
44487
  if (!existsSync78(settingsPath)) return;
43441
- const raw = readFileSync67(settingsPath, "utf-8");
44488
+ const raw = readFileSync68(settingsPath, "utf-8");
43442
44489
  const settings = JSON.parse(raw);
43443
44490
  const deny = settings?.permissions?.deny;
43444
44491
  if (!Array.isArray(deny)) return;
@@ -43455,7 +44502,7 @@ function migrateAgentPermissions(cwd) {
43455
44502
  }
43456
44503
  async function startProxy(opts = {}) {
43457
44504
  installFileLogger({
43458
- filePath: join79(process.cwd(), ".unerr", "logs", "unerr.log"),
44505
+ filePath: join81(process.cwd(), ".unerr", "logs", "unerr.log"),
43459
44506
  maxBytes: 5e6,
43460
44507
  keep: 5
43461
44508
  });
@@ -43468,7 +44515,7 @@ async function startProxy(opts = {}) {
43468
44515
  const lifecycle = createLifecycleActor(process.cwd());
43469
44516
  lifecycle.send({ type: "START_DETECT" });
43470
44517
  startup.setLocalMode(true);
43471
- const stateDir = join79(process.cwd(), ".unerr", "state");
44518
+ const stateDir = join81(process.cwd(), ".unerr", "state");
43472
44519
  if (!existsSync78(stateDir)) {
43473
44520
  mkdirSync44(stateDir, { recursive: true });
43474
44521
  }
@@ -43487,7 +44534,7 @@ async function startProxy(opts = {}) {
43487
44534
  startupLog.step(
43488
44535
  `PID ${process.pid} ${startupLog.fmt.muted(`\xB7 health localhost:${lockResult.healthPort}`)}`
43489
44536
  );
43490
- const ledgerDir = join79(process.cwd(), ".unerr", "ledger");
44537
+ const ledgerDir = join81(process.cwd(), ".unerr", "ledger");
43491
44538
  const previousSession = detectSessionResume(stateDir, ledgerDir);
43492
44539
  if (previousSession) {
43493
44540
  stats.isResumedSession = true;
@@ -43502,21 +44549,21 @@ async function startProxy(opts = {}) {
43502
44549
  if (opts.repoId) {
43503
44550
  repoIds = [opts.repoId];
43504
44551
  } else {
43505
- const configPath2 = join79(process.cwd(), ".unerr", "config.json");
44552
+ const configPath2 = join81(process.cwd(), ".unerr", "config.json");
43506
44553
  if (existsSync78(configPath2)) {
43507
44554
  try {
43508
- const config = JSON.parse(readFileSync67(configPath2, "utf-8"));
44555
+ const config = JSON.parse(readFileSync68(configPath2, "utf-8"));
43509
44556
  if (config.repoId) repoIds = [config.repoId];
43510
44557
  } catch {
43511
44558
  }
43512
44559
  }
43513
- const manifestsDir = join79(process.cwd(), ".unerr", "manifests");
44560
+ const manifestsDir = join81(process.cwd(), ".unerr", "manifests");
43514
44561
  if (repoIds.length === 0 && existsSync78(manifestsDir)) {
43515
- repoIds = readdirSync18(manifestsDir).filter((f) => f.endsWith(".json")).map((f) => f.replace(".json", ""));
44562
+ repoIds = readdirSync19(manifestsDir).filter((f) => f.endsWith(".json")).map((f) => f.replace(".json", ""));
43516
44563
  }
43517
44564
  }
43518
44565
  if (repoIds.length === 0) {
43519
- const { createHash: createHash5 } = await import("crypto");
44566
+ const { createHash: createHash6 } = await import("crypto");
43520
44567
  let repoIdentifier = process.cwd();
43521
44568
  try {
43522
44569
  const { getRemoteUrl: getRemoteUrl2 } = await Promise.resolve().then(() => (init_git(), git_exports));
@@ -43524,7 +44571,7 @@ async function startProxy(opts = {}) {
43524
44571
  if (remote) repoIdentifier = remote;
43525
44572
  } catch {
43526
44573
  }
43527
- const localRepoId = createHash5("sha256").update(repoIdentifier).digest("hex").slice(0, 12);
44574
+ const localRepoId = createHash6("sha256").update(repoIdentifier).digest("hex").slice(0, 12);
43528
44575
  repoIds = [localRepoId];
43529
44576
  startupLog.done(
43530
44577
  `Repository ${startupLog.fmt.cyan(localRepoId)} ${startupLog.fmt.muted(`(from ${repoIdentifier === process.cwd() ? "directory" : "git remote"})`)}`
@@ -43636,7 +44683,7 @@ async function startProxy(opts = {}) {
43636
44683
  `Migrated snapshot to persistent graph ${startupLog.fmt.muted(`\u2192 ${dbPath}`)}`
43637
44684
  );
43638
44685
  try {
43639
- const migrationUnerrDir = join79(process.cwd(), ".unerr");
44686
+ const migrationUnerrDir = join81(process.cwd(), ".unerr");
43640
44687
  const factStoreForMigration = await getProxyFactStore(migrationUnerrDir);
43641
44688
  if (factStoreForMigration) {
43642
44689
  const { detectLocalConventions: detectLocalConventions2 } = await Promise.resolve().then(() => (init_local_convention_detector(), local_convention_detector_exports));
@@ -43735,7 +44782,7 @@ async function startProxy(opts = {}) {
43735
44782
  let efficiencyTracker = createEfficiencyTracker2();
43736
44783
  router.setTokenCounter(tokenCounter);
43737
44784
  router.setEfficiencyTracker(efficiencyTracker);
43738
- const proxyFactStore2 = await getProxyFactStore(join79(process.cwd(), ".unerr"));
44785
+ const proxyFactStore2 = await getProxyFactStore(join81(process.cwd(), ".unerr"));
43739
44786
  if (proxyFactStore2) {
43740
44787
  router.setFactStore(proxyFactStore2);
43741
44788
  }
@@ -43743,7 +44790,7 @@ async function startProxy(opts = {}) {
43743
44790
  try {
43744
44791
  const { generateSessionResume: generateSessionResume2 } = await Promise.resolve().then(() => (init_session_resume(), session_resume_exports));
43745
44792
  const { ShadowLedger: ResumeLedger } = await Promise.resolve().then(() => (init_shadow_ledger(), shadow_ledger_exports));
43746
- const resumeLedger = new ResumeLedger(join79(process.cwd(), ".unerr"));
44793
+ const resumeLedger = new ResumeLedger(join81(process.cwd(), ".unerr"));
43747
44794
  const ledgerEntries = resumeLedger.getRecentEntries(50);
43748
44795
  const resumeCtx = generateSessionResume2(ledgerEntries);
43749
44796
  if (resumeCtx) {
@@ -43764,7 +44811,7 @@ async function startProxy(opts = {}) {
43764
44811
  const { createDurabilityScorer: createDurabilityScorer2 } = await Promise.resolve().then(() => (init_durability_scorer(), durability_scorer_exports));
43765
44812
  const durabilityScorer = createDurabilityScorer2();
43766
44813
  const { ShadowLedger: DurLedger } = await Promise.resolve().then(() => (init_shadow_ledger(), shadow_ledger_exports));
43767
- const durLedger = new DurLedger(join79(process.cwd(), ".unerr"));
44814
+ const durLedger = new DurLedger(join81(process.cwd(), ".unerr"));
43768
44815
  const durEntries = durLedger.getRecentEntries(200);
43769
44816
  if (durEntries.length > 0) {
43770
44817
  durabilityScorer.computeScores(durEntries);
@@ -43784,7 +44831,7 @@ async function startProxy(opts = {}) {
43784
44831
  try {
43785
44832
  const { detectInstableEntities: detectInstableEntities2 } = await Promise.resolve().then(() => (init_negative_knowledge(), negative_knowledge_exports));
43786
44833
  const { ShadowLedger: NkLedger } = await Promise.resolve().then(() => (init_shadow_ledger(), shadow_ledger_exports));
43787
- const nkLedger = new NkLedger(join79(process.cwd(), ".unerr"));
44834
+ const nkLedger = new NkLedger(join81(process.cwd(), ".unerr"));
43788
44835
  const nkEntries = nkLedger.getRecentEntries(200);
43789
44836
  if (nkEntries.length > 0) {
43790
44837
  const antiPatterns = detectInstableEntities2(nkEntries);
@@ -43806,7 +44853,7 @@ async function startProxy(opts = {}) {
43806
44853
  try {
43807
44854
  const { CausalBridge: CausalBridge2 } = await Promise.resolve().then(() => (init_causal_bridge(), causal_bridge_exports));
43808
44855
  const causalBridge = new CausalBridge2(
43809
- join79(process.cwd(), ".unerr"),
44856
+ join81(process.cwd(), ".unerr"),
43810
44857
  process.cwd()
43811
44858
  );
43812
44859
  router.setCausalBridge(causalBridge);
@@ -43816,7 +44863,7 @@ async function startProxy(opts = {}) {
43816
44863
  try {
43817
44864
  const { learnConventions: learnConventions2 } = await Promise.resolve().then(() => (init_convention_learner(), convention_learner_exports));
43818
44865
  const { ShadowLedger: ConvLedger } = await Promise.resolve().then(() => (init_shadow_ledger(), shadow_ledger_exports));
43819
- const convLedger = new ConvLedger(join79(process.cwd(), ".unerr"));
44866
+ const convLedger = new ConvLedger(join81(process.cwd(), ".unerr"));
43820
44867
  const convEntries = convLedger.getRecentEntries(100);
43821
44868
  if (convEntries.length > 0) {
43822
44869
  const learned = learnConventions2(convEntries);
@@ -43839,7 +44886,7 @@ async function startProxy(opts = {}) {
43839
44886
  try {
43840
44887
  const { computePromptDurabilityProfiles: computePromptDurabilityProfiles2 } = await Promise.resolve().then(() => (init_prompt_durability(), prompt_durability_exports));
43841
44888
  const { ShadowLedger: DurProfLedger } = await Promise.resolve().then(() => (init_shadow_ledger(), shadow_ledger_exports));
43842
- const durProfLedger = new DurProfLedger(join79(process.cwd(), ".unerr"));
44889
+ const durProfLedger = new DurProfLedger(join81(process.cwd(), ".unerr"));
43843
44890
  const durProfEntries = durProfLedger.getRecentEntries(200);
43844
44891
  if (durProfEntries.length > 0) {
43845
44892
  const profiles = computePromptDurabilityProfiles2(durProfEntries);
@@ -43859,7 +44906,7 @@ async function startProxy(opts = {}) {
43859
44906
  }
43860
44907
  try {
43861
44908
  const { createContextLedger: createContextLedger2 } = await Promise.resolve().then(() => (init_context_ledger(), context_ledger_exports));
43862
- const contextLedger = createContextLedger2(join79(process.cwd(), ".unerr"));
44909
+ const contextLedger = createContextLedger2(join81(process.cwd(), ".unerr"));
43863
44910
  contextLedger.load();
43864
44911
  contextLedger.prune();
43865
44912
  router.setContextLedger(contextLedger);
@@ -43984,7 +45031,7 @@ async function startProxy(opts = {}) {
43984
45031
  });
43985
45032
  const { ShadowLedger: ShadowLedger2 } = await Promise.resolve().then(() => (init_shadow_ledger(), shadow_ledger_exports));
43986
45033
  const { IntentCorrelator: IntentCorrelator2 } = await Promise.resolve().then(() => (init_intent_correlator(), intent_correlator_exports));
43987
- const unerrDirForLedger = join79(process.cwd(), ".unerr");
45034
+ const unerrDirForLedger = join81(process.cwd(), ".unerr");
43988
45035
  const shadowLedger = new ShadowLedger2(unerrDirForLedger);
43989
45036
  const intentCorrelator = new IntentCorrelator2(unerrDirForLedger);
43990
45037
  log22.info(
@@ -44094,7 +45141,7 @@ async function startProxy(opts = {}) {
44094
45141
  try {
44095
45142
  const { writeFileSync: writeFileSync42 } = await import("fs");
44096
45143
  writeFileSync42(
44097
- join79(unerrDirForLedger, "state", "session.id"),
45144
+ join81(unerrDirForLedger, "state", "session.id"),
44098
45145
  shadowLedger.getSessionId(),
44099
45146
  "utf-8"
44100
45147
  );
@@ -44522,7 +45569,7 @@ ${signalFooter.trimEnd()}` : "";
44522
45569
  lifecycle.send({ type: "INDEX_COMPLETE" });
44523
45570
  lifecycle.send({ type: "MCP_READY" });
44524
45571
  const { TransportMux: TransportMux2 } = await Promise.resolve().then(() => (init_transport_mux(), transport_mux_exports));
44525
- const sockPath2 = join79(stateDir, "proxy.sock");
45572
+ const sockPath2 = join81(stateDir, "proxy.sock");
44526
45573
  const transportMux = new TransportMux2(sockPath2);
44527
45574
  const agentNameByClient = /* @__PURE__ */ new Map();
44528
45575
  transportMux.setCustomHttpHandler("/commit-context", (_url) => {
@@ -44807,7 +45854,7 @@ ${signalFooter2.trimEnd()}` : "";
44807
45854
  try {
44808
45855
  const { DriftTracker: DriftTracker2 } = await Promise.resolve().then(() => (init_drift_tracker(), drift_tracker_exports));
44809
45856
  const { FileHashManager: FileHashManager2 } = await Promise.resolve().then(() => (init_file_hash_state(), file_hash_state_exports));
44810
- const unerrDir2 = join79(process.cwd(), ".unerr");
45857
+ const unerrDir2 = join81(process.cwd(), ".unerr");
44811
45858
  const fileHashManager = new FileHashManager2(unerrDir2);
44812
45859
  _driftTracker = new DriftTracker2(
44813
45860
  { projectRoot: process.cwd(), repoId: repoIds[0], unerrDir: unerrDir2 },
@@ -45031,7 +46078,7 @@ ${signalFooter2.trimEnd()}` : "";
45031
46078
  }
45032
46079
  const { WorkspaceManifest: WorkspaceManifest2 } = await Promise.resolve().then(() => (init_workspace_manifest(), workspace_manifest_exports));
45033
46080
  const workspaceManifest = repoIds[0] ? new WorkspaceManifest2(
45034
- join79(process.cwd(), ".unerr"),
46081
+ join81(process.cwd(), ".unerr"),
45035
46082
  repoIds[0],
45036
46083
  shadowLedger.getSessionId()
45037
46084
  ) : null;
@@ -45116,7 +46163,7 @@ ${signalFooter2.trimEnd()}` : "";
45116
46163
  if (proxyMode === "parse" && parseIndex) {
45117
46164
  try {
45118
46165
  const { extractEntitiesFromSource: extractEntitiesFromSource2 } = await Promise.resolve().then(() => (init_auto_bootstrap(), auto_bootstrap_exports));
45119
- const allFiles = readdirSync18(process.cwd(), {
46166
+ const allFiles = readdirSync19(process.cwd(), {
45120
46167
  recursive: true,
45121
46168
  encoding: "utf-8"
45122
46169
  });
@@ -45130,7 +46177,7 @@ ${signalFooter2.trimEnd()}` : "";
45130
46177
  });
45131
46178
  for (const file of sourceFiles.slice(0, 500)) {
45132
46179
  try {
45133
- const content = readFileSync67(join79(process.cwd(), file), "utf-8");
46180
+ const content = readFileSync68(join81(process.cwd(), file), "utf-8");
45134
46181
  const entities = extractEntitiesFromSource2(file, content);
45135
46182
  parseIndex.addEntities(entities);
45136
46183
  } catch {
@@ -45194,7 +46241,7 @@ ${signalFooter2.trimEnd()}` : "";
45194
46241
  }
45195
46242
  const { writeFileSync: writeStatsFile } = await import("fs");
45196
46243
  const { computePercentiles: computePercentiles2 } = await Promise.resolve().then(() => (init_session_stats(), session_stats_exports));
45197
- const statsSnapshotPath = join79(stateDir, "session_stats.json");
46244
+ const statsSnapshotPath = join81(stateDir, "session_stats.json");
45198
46245
  const statsSnapshotInterval = setInterval(() => {
45199
46246
  try {
45200
46247
  const total = stats.toolCallsLocal;
@@ -45232,7 +46279,7 @@ ${signalFooter2.trimEnd()}` : "";
45232
46279
  const { startDashboardServer: startDashboardServer2 } = await Promise.resolve().then(() => (init_http(), http_exports));
45233
46280
  const { detectIde: detectIdeDashboard } = await Promise.resolve().then(() => (init_detect(), detect_exports));
45234
46281
  const ideType = await detectIdeDashboard(process.cwd());
45235
- const unerrDirForApi = join79(process.cwd(), ".unerr");
46282
+ const unerrDirForApi = join81(process.cwd(), ".unerr");
45236
46283
  dashboardHandle = await startDashboardServer2({
45237
46284
  system: {
45238
46285
  stats,
@@ -45300,16 +46347,16 @@ ${signalFooter2.trimEnd()}` : "";
45300
46347
  temporal: await (async () => {
45301
46348
  try {
45302
46349
  const { TemporalFactStore: TemporalFactStore2 } = await Promise.resolve().then(() => (init_temporal_facts(), temporal_facts_exports));
45303
- const { readdirSync: readdirSync19, readFileSync: readFileSync70 } = await import("fs");
46350
+ const { readdirSync: readdirSync20, readFileSync: readFileSync71 } = await import("fs");
45304
46351
  const factStore = await TemporalFactStore2.create(process.cwd());
45305
46352
  return {
45306
46353
  factStore,
45307
46354
  loadRecentSessions: (limit) => {
45308
46355
  try {
45309
- const sessDir = join79(unerrDirForApi, "sessions");
45310
- const files = readdirSync19(sessDir).filter((f) => f.endsWith(".jsonl")).sort().slice(-limit);
46356
+ const sessDir = join81(unerrDirForApi, "sessions");
46357
+ const files = readdirSync20(sessDir).filter((f) => f.endsWith(".jsonl")).sort().slice(-limit);
45311
46358
  return files.map((f) => {
45312
- const content = readFileSync70(join79(sessDir, f), "utf-8").trim().split("\n").pop();
46359
+ const content = readFileSync71(join81(sessDir, f), "utf-8").trim().split("\n").pop();
45313
46360
  return JSON.parse(content);
45314
46361
  });
45315
46362
  } catch {
@@ -45396,7 +46443,7 @@ ${signalFooter2.trimEnd()}` : "";
45396
46443
  const topMech = Object.entries(tokenFlowSummary.by_mechanism).sort(
45397
46444
  ([, a], [, b]) => b.tokens_saved - a.tokens_saved
45398
46445
  )[0];
45399
- appendSessionHistory2(join79(process.cwd(), ".unerr"), {
46446
+ appendSessionHistory2(join81(process.cwd(), ".unerr"), {
45400
46447
  sessionId: shadowLedger.getSessionId(),
45401
46448
  startedAt: new Date(stats.sessionStartedAt).toISOString(),
45402
46449
  endedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -45556,7 +46603,7 @@ ${signalFooter2.trimEnd()}` : "";
45556
46603
  try {
45557
46604
  const correctionModule = (init_correction_detector(), __toCommonJS(correction_detector_exports));
45558
46605
  const detectCorrections2 = correctionModule.detectCorrections;
45559
- const ledgerPath = join79(
46606
+ const ledgerPath = join81(
45560
46607
  process.cwd(),
45561
46608
  ".unerr",
45562
46609
  "ledger",
@@ -45920,19 +46967,19 @@ __export(setup_wizard_exports, {
45920
46967
  promptLocalOrExit: () => promptLocalOrExit,
45921
46968
  runSetup: () => runSetup
45922
46969
  });
45923
- import { createHash as createHash4 } from "crypto";
46970
+ import { createHash as createHash5 } from "crypto";
45924
46971
  import { existsSync as existsSync80, mkdirSync as mkdirSync46, writeFileSync as writeFileSync40 } from "fs";
45925
- import { join as join80 } from "path";
46972
+ import { join as join82 } from "path";
45926
46973
  import * as clack from "@clack/prompts";
45927
46974
  async function runSetup(cwd) {
45928
46975
  const projectDir = cwd ?? process.cwd();
45929
46976
  clack.intro("unerr");
45930
46977
  clack.log.step("Project Setup");
45931
46978
  const repoId = await generateRepoId(projectDir);
45932
- const configDir = join80(projectDir, ".unerr");
46979
+ const configDir = join82(projectDir, ".unerr");
45933
46980
  mkdirSync46(configDir, { recursive: true });
45934
- const configPath2 = join80(configDir, "config.json");
45935
- const settingsPath = join80(configDir, "settings.json");
46981
+ const configPath2 = join82(configDir, "config.json");
46982
+ const settingsPath = join82(configDir, "settings.json");
45936
46983
  writeFileSync40(configPath2, `${JSON.stringify({ repoId }, null, 2)}
45937
46984
  `);
45938
46985
  let existingSettings = {};
@@ -46003,7 +47050,7 @@ async function generateRepoId(cwd) {
46003
47050
  let repoIdentifier = cwd;
46004
47051
  const remote = await getRemoteUrl(cwd);
46005
47052
  if (remote) repoIdentifier = remote;
46006
- return createHash4("sha256").update(repoIdentifier).digest("hex").slice(0, 12);
47053
+ return createHash5("sha256").update(repoIdentifier).digest("hex").slice(0, 12);
46007
47054
  }
46008
47055
  function formatSize2(bytes) {
46009
47056
  if (bytes >= 1e9) return `${(bytes / 1e9).toFixed(1)}GB`;
@@ -46316,6 +47363,112 @@ var init_setup_wizard = __esm({
46316
47363
  }
46317
47364
  });
46318
47365
 
47366
+ // src/proxy/bridge-catalog.ts
47367
+ var bridge_catalog_exports = {};
47368
+ __export(bridge_catalog_exports, {
47369
+ PROTOCOL_VERSION: () => PROTOCOL_VERSION,
47370
+ SERVER_INFO: () => SERVER_INFO,
47371
+ StaticCatalogInterceptor: () => StaticCatalogInterceptor,
47372
+ buildInitializeResult: () => buildInitializeResult,
47373
+ buildToolsListResult: () => buildToolsListResult
47374
+ });
47375
+ function tryIntercept(line) {
47376
+ let msg;
47377
+ try {
47378
+ const parsed = JSON.parse(line);
47379
+ if (!parsed || typeof parsed !== "object") return null;
47380
+ msg = parsed;
47381
+ } catch {
47382
+ return null;
47383
+ }
47384
+ if (msg.method === "initialize") {
47385
+ return JSON.stringify({
47386
+ jsonrpc: "2.0",
47387
+ id: msg.id ?? null,
47388
+ result: {
47389
+ protocolVersion: PROTOCOL_VERSION,
47390
+ capabilities: { tools: {} },
47391
+ serverInfo: SERVER_INFO
47392
+ }
47393
+ });
47394
+ }
47395
+ if (msg.method === "tools/list") {
47396
+ return JSON.stringify({
47397
+ jsonrpc: "2.0",
47398
+ id: msg.id ?? null,
47399
+ result: { tools: TOOL_DEFINITIONS }
47400
+ });
47401
+ }
47402
+ return null;
47403
+ }
47404
+ function buildInitializeResult(id) {
47405
+ return {
47406
+ jsonrpc: "2.0",
47407
+ id,
47408
+ result: {
47409
+ protocolVersion: PROTOCOL_VERSION,
47410
+ capabilities: { tools: {} },
47411
+ serverInfo: SERVER_INFO
47412
+ }
47413
+ };
47414
+ }
47415
+ function buildToolsListResult(id) {
47416
+ return {
47417
+ jsonrpc: "2.0",
47418
+ id,
47419
+ result: { tools: TOOL_DEFINITIONS }
47420
+ };
47421
+ }
47422
+ var PROTOCOL_VERSION, SERVER_INFO, StaticCatalogInterceptor;
47423
+ var init_bridge_catalog = __esm({
47424
+ "src/proxy/bridge-catalog.ts"() {
47425
+ "use strict";
47426
+ init_tool_definitions();
47427
+ PROTOCOL_VERSION = "2024-11-05";
47428
+ SERVER_INFO = {
47429
+ name: "unerr-local",
47430
+ version: "0.1.7"
47431
+ };
47432
+ StaticCatalogInterceptor = class {
47433
+ partial = "";
47434
+ ingest(chunk) {
47435
+ const replies = [];
47436
+ const forward = [];
47437
+ this.partial += chunk.toString("utf8");
47438
+ const lastNewline = this.partial.lastIndexOf("\n");
47439
+ if (lastNewline < 0) {
47440
+ return { replies, forward };
47441
+ }
47442
+ const completeBlock = this.partial.slice(0, lastNewline + 1);
47443
+ this.partial = this.partial.slice(lastNewline + 1);
47444
+ for (const rawLine of completeBlock.split("\n")) {
47445
+ if (rawLine === "") continue;
47446
+ const reply = tryIntercept(rawLine);
47447
+ if (reply !== null) {
47448
+ replies.push(`${reply}
47449
+ `);
47450
+ } else {
47451
+ forward.push(Buffer.from(`${rawLine}
47452
+ `, "utf8"));
47453
+ }
47454
+ }
47455
+ return { replies, forward };
47456
+ }
47457
+ /**
47458
+ * Returns any unfinished line (no terminating newline yet). Call exactly
47459
+ * once when handing the pre-buffer off to the bridge so partial frames are
47460
+ * forwarded to the proxy without being dropped.
47461
+ */
47462
+ drainPartial() {
47463
+ if (this.partial.length === 0) return null;
47464
+ const buf = Buffer.from(this.partial, "utf8");
47465
+ this.partial = "";
47466
+ return buf;
47467
+ }
47468
+ };
47469
+ }
47470
+ });
47471
+
46319
47472
  // src/daemon/spawn-lock.ts
46320
47473
  var spawn_lock_exports = {};
46321
47474
  __export(spawn_lock_exports, {
@@ -46327,16 +47480,16 @@ import {
46327
47480
  closeSync as closeSync2,
46328
47481
  mkdirSync as mkdirSync47,
46329
47482
  openSync as openSync2,
46330
- readFileSync as readFileSync68,
47483
+ readFileSync as readFileSync69,
46331
47484
  unlinkSync as unlinkSync16,
46332
47485
  writeFileSync as writeFileSync41
46333
47486
  } from "fs";
46334
- import { join as join81 } from "path";
47487
+ import { join as join83 } from "path";
46335
47488
  function spawnLockPath() {
46336
- return join81(globalDir(), "state", "spawn.lock");
47489
+ return join83(globalDir(), "state", "spawn.lock");
46337
47490
  }
46338
47491
  function tryAcquireSpawnLock() {
46339
- mkdirSync47(join81(globalDir(), "state"), { recursive: true });
47492
+ mkdirSync47(join83(globalDir(), "state"), { recursive: true });
46340
47493
  const path7 = spawnLockPath();
46341
47494
  for (let attempt = 0; attempt < 2; attempt++) {
46342
47495
  try {
@@ -46364,7 +47517,7 @@ function releaseSpawnLock() {
46364
47517
  function reclaimIfStale(path7) {
46365
47518
  let body = null;
46366
47519
  try {
46367
- body = JSON.parse(readFileSync68(path7, "utf8"));
47520
+ body = JSON.parse(readFileSync69(path7, "utf8"));
46368
47521
  } catch {
46369
47522
  try {
46370
47523
  unlinkSync16(path7);
@@ -46536,8 +47689,8 @@ var init_bridge = __esm({
46536
47689
  });
46537
47690
 
46538
47691
  // src/entrypoints/cli.ts
46539
- import { existsSync as existsSync81, readFileSync as readFileSync69 } from "fs";
46540
- import { join as join82 } from "path";
47692
+ import { existsSync as existsSync81, readFileSync as readFileSync70 } from "fs";
47693
+ import { join as join84 } from "path";
46541
47694
  import { Command } from "commander";
46542
47695
 
46543
47696
  // src/commands/branches.ts
@@ -49573,12 +50726,12 @@ async function tryLoadGraphForShellBoost(cwd) {
49573
50726
  if (cached && Date.now() - cached.loadedAt < BOOST_TTL_MS) {
49574
50727
  return cached.kind === "ok" ? cached.graph : null;
49575
50728
  }
49576
- const { existsSync: existsSync82, readFileSync: readFileSync70 } = await import("fs");
49577
- const { join: join83 } = await import("path");
49578
- const snapshotsDir = join83(cwd, ".unerr", "snapshots");
49579
- let snapshotPath2 = join83(snapshotsDir, "graph.msgpack.gz");
50729
+ const { existsSync: existsSync82, readFileSync: readFileSync71 } = await import("fs");
50730
+ const { join: join85 } = await import("path");
50731
+ const snapshotsDir = join85(cwd, ".unerr", "snapshots");
50732
+ let snapshotPath2 = join85(snapshotsDir, "graph.msgpack.gz");
49580
50733
  if (!existsSync82(snapshotPath2)) {
49581
- snapshotPath2 = join83(snapshotsDir, "graph.msgpack");
50734
+ snapshotPath2 = join85(snapshotsDir, "graph.msgpack");
49582
50735
  }
49583
50736
  if (!existsSync82(snapshotPath2)) {
49584
50737
  const reason = `snapshot missing at ${snapshotsDir}`;
@@ -49605,7 +50758,7 @@ async function tryLoadGraphForShellBoost(cwd) {
49605
50758
  let buffer;
49606
50759
  try {
49607
50760
  const { gunzipSync: gunzipSync3 } = await import("zlib");
49608
- const raw = readFileSync70(snapshotPath2);
50761
+ const raw = readFileSync71(snapshotPath2);
49609
50762
  try {
49610
50763
  buffer = gunzipSync3(raw);
49611
50764
  } catch {
@@ -54765,9 +55918,9 @@ When your next action is Edit, use built-in Read with offset/limit on the target
54765
55918
 
54766
55919
  **Common failure mode:** Using \`file_read\` to understand a file, then attempting Edit without calling built-in Read first. The Edit tool WILL reject with "File has not been read yet". Always call built-in Read (with offset/limit) immediately before Edit.` : "";
54767
55920
  const summaryEditNote = isClaudeCode ? "\nNEVER use built-in Read/Grep/Glob for code navigation. EXCEPTION: built-in Read (with offset/limit) is REQUIRED immediately before Edit (file_read cannot substitute \u2014 Edit will fail without it)." : "\nNEVER use built-in Read/Grep/Glob for code navigation \u2014 use unerr MCP tools instead.";
54768
- return `## REQUIRED: Use unerr Graph Intelligence Tools (19 MCP tools)
55921
+ return `## REQUIRED: Use unerr Graph Intelligence Tools (20 MCP tools)
54769
55922
 
54770
- This project has unerr MCP tools installed. You MUST use these instead of built-in Read/Grep/Glob for code navigation. unerr tools are graph-backed, return results in <5ms, and include project context that built-in tools miss.
55923
+ This project has unerr MCP tools installed. You MUST use these instead of built-in Read/Grep/Glob for code navigation, and \`fetch_url\` instead of built-in WebFetch. unerr tools are graph-backed, return results in <5ms, and include project context that built-in tools miss.
54771
55924
 
54772
55925
  ### Tool Routing (MANDATORY \u2014 match your goal before calling any tool)
54773
55926
 
@@ -54781,6 +55934,7 @@ ${readForEditRow}
54781
55934
  | Get a specific function or class | \`get_entity\` or \`file_read\` with \`entity\` param | Reading entire file |
54782
55935
  | Trace imports/dependencies | \`get_imports\` or \`get_references\` (direction: callees) | Manual import scanning |
54783
55936
  | Find hotspots / high fan-in / blast-radius candidates | \`get_critical_nodes\` | \`get_entity\` (won't show ranked list), guessing |
55937
+ | Fetch a web page by URL | \`fetch_url\` (Defuddle/Readability \u2192 markdown passages \u2192 BM25 ranking when \`prompt\` supplied \u2192 diff-cache) | Built-in WebFetch |
54784
55938
  | Run a shell command | Automatic \u2014 routed through shell intelligence | N/A |
54785
55939
 
54786
55940
  ### FORBIDDEN Patterns (these waste tokens and miss context)
@@ -54791,7 +55945,8 @@ ${readForEditRow}
54791
55945
  - Reading multiple files to understand conventions -> use \`get_conventions\`
54792
55946
  - Guessing code style for new code -> use \`get_conventions\`
54793
55947
  - Guessing which entity has the highest fan-in / is the biggest hotspot -> use \`get_critical_nodes\`
54794
- - Reading a full file when you only need a section -> use \`file_read\` with \`entity\` param or offset/limit${twoStepSection}
55948
+ - Reading a full file when you only need a section -> use \`file_read\` with \`entity\` param or offset/limit
55949
+ - Using built-in WebFetch for a URL -> use \`fetch_url\` (DOM extraction + markdown + BM25 passage selection cuts 5\u201310\xD7 tokens; pass \`prompt\` to rank passages by relevance)${twoStepSection}
54795
55950
 
54796
55951
  ### Tool Reference
54797
55952
 
@@ -54829,6 +55984,14 @@ ${readForEditRow}
54829
55984
  - \`purpose:'explore'\` (default) \u2014 budget-capped, returns outline for large files. Use for browsing and pre-edit understanding.
54830
55985
  - \`purpose:'reference'\` \u2014 tight budget, entity/offset reads only. Use for quick lookups.
54831
55986
 
55987
+ #### Web Fetch (1 tool)
55988
+
55989
+ | Task | Tool | Replaces |
55990
+ |------|------|----------|
55991
+ | Fetch a web page by URL | \`fetch_url\` | Built-in WebFetch |
55992
+
55993
+ \`fetch_url\` strips chrome (nav, footer, ads), converts to markdown, splits into heading-bounded passages, optionally re-ranks passages with BM25 when you pass \`prompt\`, and caches by content hash so re-fetching an unchanged page costs near-zero tokens. Pass \`offset\`/\`limit\` to paginate large pages.
55994
+
54832
55995
  #### Shell Compression (automatic)
54833
55996
 
54834
55997
  All shell commands automatically route through unerr's compression layer. It strips ANSI codes, classifies output (diffs, test results, logs, errors), and returns compressed summaries \u2014 saving tokens without losing critical information.
@@ -55187,7 +56350,8 @@ function registerInstallCommand(program2) {
55187
56350
  }
55188
56351
  process.stderr.write("\n");
55189
56352
  process.stderr.write(
55190
- " \x1B[38;2;161;161;170mRun \x1B[0munerr\x1B[38;2;161;161;170m to start the intelligence engine.\x1B[0m\n"
56353
+ ` \x1B[38;2;161;161;170mRestart ${agentDef.name} to start using unerr.\x1B[0m
56354
+ `
55191
56355
  );
55192
56356
  process.stderr.write("\n");
55193
56357
  }
@@ -55243,29 +56407,13 @@ async function runInstall(cwd, ide, opts) {
55243
56407
  const { daemonSockPath: daemonSockPath2, probeDaemon: probeDaemon2, ensureRepo: ensureRepo2 } = await Promise.resolve().then(() => (init_client(), client_exports));
55244
56408
  const { addRepo: addRepo2, findRepo: findRepo2 } = await Promise.resolve().then(() => (init_registry(), registry_exports));
55245
56409
  const sock = daemonSockPath2();
55246
- const daemonRunning = await probeDaemon2(sock);
55247
- if (daemonRunning) {
56410
+ if (await probeDaemon2(sock)) {
55248
56411
  if (!findRepo2(cwd)) {
55249
56412
  addRepo2(cwd, {});
55250
56413
  repoRegistered = true;
55251
- process.stderr.write(
55252
- "\x1B[38;2;52;211;153m\u2713\x1B[0m Registered repo with unerrd.\n"
55253
- );
55254
- }
55255
- try {
55256
- await ensureRepo2(sock, cwd);
55257
- process.stderr.write(
55258
- "\x1B[38;2;52;211;153m\u2713\x1B[0m unerr process started via daemon.\n"
55259
- );
55260
- } catch {
55261
- process.stderr.write(
55262
- "\x1B[38;2;251;191;36m\u26A0\x1B[0m Repo registered but process did not start. It will start on next IDE connection.\n"
55263
- );
55264
56414
  }
55265
- } else {
55266
- process.stderr.write(
55267
- "\x1B[38;2;34;211;238m\u25B8\x1B[0m Daemon not running. To use daemon mode: \x1B[1munerr daemon initialize\x1B[0m\n For standalone mode: run \x1B[1munerr\x1B[0m in this directory.\n"
55268
- );
56415
+ await ensureRepo2(sock, cwd).catch(() => {
56416
+ });
55269
56417
  }
55270
56418
  } catch {
55271
56419
  }
@@ -55417,12 +56565,12 @@ function showSetupInstructions(agentName) {
55417
56565
  w(" After restarting your agent, verify unerr tools are available.\n");
55418
56566
  w(" You should see tools like: get_callers, search_code, file_read,\n");
55419
56567
  w(" file_outline, get_imports, get_callees.\n\n");
55420
- w(" \x1B[1mStep 4: Start unerr\x1B[0m\n");
55421
- w(" \x1B[2m\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\x1B[0m\n");
56568
+ w(" \x1B[1mStep 4: Restart your agent\x1B[0m\n");
56569
+ w(" \x1B[2m\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\x1B[0m\n");
55422
56570
  w(
55423
- " Run \x1B[1munerr\x1B[0m in your project root to start the intelligence engine.\n"
56571
+ " unerr starts automatically when your agent first connects to its MCP server.\n"
55424
56572
  );
55425
- w(" The MCP server will be available at \x1B[1munerr --mcp\x1B[0m.\n");
56573
+ w(" No background service to install \u2014 no boot-time setup needed.\n");
55426
56574
  }
55427
56575
  w("\n");
55428
56576
  }
@@ -55757,8 +56905,8 @@ async function persistPatterns(patterns, _unerrDir) {
55757
56905
  );
55758
56906
  return;
55759
56907
  }
55760
- const { readFileSync: readFileSync70 } = await import("fs");
55761
- const config = JSON.parse(readFileSync70(configPath2, "utf-8"));
56908
+ const { readFileSync: readFileSync71 } = await import("fs");
56909
+ const config = JSON.parse(readFileSync71(configPath2, "utf-8"));
55762
56910
  if (!config.repoId || !config.orgId) {
55763
56911
  warn("Repo not configured \u2014 cannot persist to CozoDB.");
55764
56912
  return;
@@ -55776,7 +56924,7 @@ async function persistPatterns(patterns, _unerrDir) {
55776
56924
  const { CozoGraphStore: CozoGraphStore2 } = await Promise.resolve().then(() => (init_local_graph(), local_graph_exports));
55777
56925
  const store = await CozoGraphStore2.create(db);
55778
56926
  const { unpack } = await import("msgpackr");
55779
- const raw = readFileSync70(snapshotPath2);
56927
+ const raw = readFileSync71(snapshotPath2);
55780
56928
  const buffer = gunzipSync2(raw);
55781
56929
  const envelope = unpack(buffer);
55782
56930
  await store.loadSnapshot(envelope);
@@ -56037,7 +57185,13 @@ function parseSettingsFlags(raw) {
56037
57185
  var write3 = (msg) => process.stderr.write(msg);
56038
57186
  function registerPmCommand(program2) {
56039
57187
  const pm = program2.command("pm").description("Manage the unerr process manager");
56040
- pm.command("start").description("Start the unerr process manager").option("--foreground", "Run in foreground (blocks terminal \u2014 useful for debugging)").option("--detached", "Run in-process as a detached supervisor (used by auto-spawn)").action(async (opts) => {
57188
+ pm.command("start").description("Start the unerr process manager").option(
57189
+ "--foreground",
57190
+ "Run in foreground (blocks terminal \u2014 useful for debugging)"
57191
+ ).option(
57192
+ "--detached",
57193
+ "Run in-process as a detached supervisor (used by auto-spawn)"
57194
+ ).action(async (opts) => {
56041
57195
  if (opts.foreground || opts.detached) {
56042
57196
  process.title = "unerrd";
56043
57197
  const { startDaemon: startDaemon2 } = await Promise.resolve().then(() => (init_daemon(), daemon_exports));
@@ -56049,7 +57203,12 @@ function registerPmCommand(program2) {
56049
57203
  const child = spawn(
56050
57204
  process.execPath,
56051
57205
  [unerrBin, "pm", "start", "--detached"],
56052
- { detached: true, stdio: "ignore", windowsHide: true, env: { ...process.env } }
57206
+ {
57207
+ detached: true,
57208
+ stdio: "ignore",
57209
+ windowsHide: true,
57210
+ env: { ...process.env }
57211
+ }
56053
57212
  );
56054
57213
  child.unref();
56055
57214
  const { daemonSockPath: daemonSockPath2, probeDaemon: probeDaemon2 } = await Promise.resolve().then(() => (init_client(), client_exports));
@@ -56066,12 +57225,12 @@ function registerPmCommand(program2) {
56066
57225
  }
56067
57226
  if (running) {
56068
57227
  const { homedir: homedir12 } = await import("os");
56069
- const { join: join83 } = await import("path");
56070
- const pidPath2 = join83(homedir12(), ".unerr", "unerrd.pid");
57228
+ const { join: join85 } = await import("path");
57229
+ const pidPath2 = join85(homedir12(), ".unerr", "unerrd.pid");
56071
57230
  let daemonPid = String(child.pid);
56072
57231
  try {
56073
- const { readFileSync: readFileSync70 } = await import("fs");
56074
- daemonPid = readFileSync70(pidPath2, "utf-8").trim();
57232
+ const { readFileSync: readFileSync71 } = await import("fs");
57233
+ daemonPid = readFileSync71(pidPath2, "utf-8").trim();
56075
57234
  } catch {
56076
57235
  }
56077
57236
  write3(
@@ -56088,8 +57247,8 @@ function registerPmCommand(program2) {
56088
57247
  pm.command("stop").description("Stop the unerrd supervisor").action(async () => {
56089
57248
  const { createConnection: createConnection3 } = await import("net");
56090
57249
  const { globalDir: globalDir2 } = await Promise.resolve().then(() => (init_registry(), registry_exports));
56091
- const { join: join83 } = await import("path");
56092
- const sock = join83(globalDir2(), "unerrd.sock");
57250
+ const { join: join85 } = await import("path");
57251
+ const sock = join85(globalDir2(), "unerrd.sock");
56093
57252
  const { existsSync: existsSync82 } = await import("fs");
56094
57253
  if (!existsSync82(sock)) {
56095
57254
  write3("unerrd is not running (no socket found).\n");
@@ -56214,7 +57373,7 @@ function registerPmCommand(program2) {
56214
57373
  pm.command("status").description("List all registered repos and their state").action(async () => {
56215
57374
  const repos = listRepos();
56216
57375
  if (repos.length === 0) {
56217
- write3("No repos registered. Use `unerr daemon add .` to register.\n");
57376
+ write3("No repos registered. Use `unerr pm add .` to register.\n");
56218
57377
  return;
56219
57378
  }
56220
57379
  let liveStatus = null;
@@ -56263,7 +57422,7 @@ function registerPmCommand(program2) {
56263
57422
  }
56264
57423
  write3(
56265
57424
  `
56266
- \x1B[1munerr daemon\x1B[0m \u2014 ${repos.length} repo${repos.length === 1 ? "" : "s"} registered
57425
+ \x1B[1munerr pm\x1B[0m \u2014 ${repos.length} repo${repos.length === 1 ? "" : "s"} registered
56267
57426
 
56268
57427
  `
56269
57428
  );
@@ -56296,7 +57455,7 @@ function registerPmCommand(program2) {
56296
57455
  write3(
56297
57456
  ` ${ni.key}: auto-selected \x1B[1m${ni.auto}\x1B[0m (${ni.reason})
56298
57457
  Alternatives: ${ni.alternatives.join(", ")}
56299
- Override: unerr daemon config ${repo.path} --${toKebab(ni.key)}=${ni.alternatives[0]}
57458
+ Override: unerr pm config ${repo.path} --${toKebab(ni.key)}=${ni.alternatives[0]}
56300
57459
  `
56301
57460
  );
56302
57461
  }
@@ -56327,7 +57486,7 @@ function registerPmCommand(program2) {
56327
57486
  if (!entry) {
56328
57487
  write3(
56329
57488
  `\x1B[38;2;248;113;113m\u2717\x1B[0m Not registered: ${targetPath}
56330
- Register first: unerr daemon add ${targetPath}
57489
+ Register first: unerr pm add ${targetPath}
56331
57490
  `
56332
57491
  );
56333
57492
  process.exitCode = 1;
@@ -56382,7 +57541,7 @@ function registerPmCommand(program2) {
56382
57541
  if (!updated) {
56383
57542
  write3(
56384
57543
  `\x1B[38;2;248;113;113m\u2717\x1B[0m Not registered: ${targetPath}
56385
- Register first: unerr daemon add ${targetPath}
57544
+ Register first: unerr pm add ${targetPath}
56386
57545
  `
56387
57546
  );
56388
57547
  process.exitCode = 1;
@@ -58025,8 +59184,8 @@ function registerStatusCommand(program2) {
58025
59184
  try {
58026
59185
  const logsDir = join53(unerrDir2, "logs");
58027
59186
  if (existsSync50(logsDir)) {
58028
- const { readdirSync: readdirSync19, statSync: statSync16 } = await import("fs");
58029
- const logs = readdirSync19(logsDir).filter((f) => f.startsWith("session-") && f.endsWith(".log")).map((f) => ({
59187
+ const { readdirSync: readdirSync20, statSync: statSync16 } = await import("fs");
59188
+ const logs = readdirSync20(logsDir).filter((f) => f.startsWith("session-") && f.endsWith(".log")).map((f) => ({
58030
59189
  name: f,
58031
59190
  mtime: statSync16(join53(logsDir, f)).mtimeMs
58032
59191
  })).sort((a, b) => b.mtime - a.mtime);
@@ -59034,7 +60193,7 @@ var SOURCE_DIRS = /* @__PURE__ */ new Set([
59034
60193
  ]);
59035
60194
  var DETECTION_THRESHOLD = 5;
59036
60195
  async function detectProjectRoot(cwd) {
59037
- const { readdirSync: readdirSync19 } = await import("fs");
60196
+ const { readdirSync: readdirSync20 } = await import("fs");
59038
60197
  let score = 0;
59039
60198
  const signals = [];
59040
60199
  let hasGit = false;
@@ -59052,7 +60211,7 @@ async function detectProjectRoot(cwd) {
59052
60211
  }
59053
60212
  let rootEntries = [];
59054
60213
  try {
59055
- rootEntries = readdirSync19(cwd, { withFileTypes: true }).map((e) => ({
60214
+ rootEntries = readdirSync20(cwd, { withFileTypes: true }).map((e) => ({
59056
60215
  name: e.name,
59057
60216
  isFile: e.isFile(),
59058
60217
  isDir: e.isDirectory()
@@ -59143,7 +60302,7 @@ async function detectProjectRoot(cwd) {
59143
60302
  }
59144
60303
  function dirHasCodeFile(dir) {
59145
60304
  try {
59146
- const entries = readdirSync19(dir, { withFileTypes: true });
60305
+ const entries = readdirSync20(dir, { withFileTypes: true });
59147
60306
  for (const entry of entries) {
59148
60307
  if (!entry.isFile()) continue;
59149
60308
  const dot = entry.name.lastIndexOf(".");
@@ -59167,7 +60326,7 @@ async function detectProjectRoot(cwd) {
59167
60326
  for (const dirEntry of rootEntries) {
59168
60327
  if (!dirEntry.isDir || dirEntry.name.startsWith(".")) continue;
59169
60328
  if (!SOURCE_DIRS.has(dirEntry.name.toLowerCase())) continue;
59170
- const sourceDir = join82(cwd, dirEntry.name);
60329
+ const sourceDir = join84(cwd, dirEntry.name);
59171
60330
  const srcCodeFile = dirHasCodeFile(sourceDir);
59172
60331
  if (srcCodeFile) {
59173
60332
  score += 6;
@@ -59175,11 +60334,11 @@ async function detectProjectRoot(cwd) {
59175
60334
  break;
59176
60335
  }
59177
60336
  try {
59178
- const subEntries = readdirSync19(sourceDir, { withFileTypes: true });
60337
+ const subEntries = readdirSync20(sourceDir, { withFileTypes: true });
59179
60338
  let found = false;
59180
60339
  for (const sub of subEntries) {
59181
60340
  if (!sub.isDirectory() || sub.name.startsWith(".")) continue;
59182
- const deepCode = dirHasCodeFile(join82(sourceDir, sub.name));
60341
+ const deepCode = dirHasCodeFile(join84(sourceDir, sub.name));
59183
60342
  if (deepCode) {
59184
60343
  score += 6;
59185
60344
  signals.push(`code-src: ${dirEntry.name}/${sub.name}/${deepCode}`);
@@ -59202,10 +60361,10 @@ async function detectProjectRoot(cwd) {
59202
60361
  };
59203
60362
  }
59204
60363
  function readLocalConfig(cwd) {
59205
- const configPath2 = join82(cwd, ".unerr", "config.json");
60364
+ const configPath2 = join84(cwd, ".unerr", "config.json");
59206
60365
  if (!existsSync81(configPath2)) return null;
59207
60366
  try {
59208
- return JSON.parse(readFileSync69(configPath2, "utf-8"));
60367
+ return JSON.parse(readFileSync70(configPath2, "utf-8"));
59209
60368
  } catch {
59210
60369
  return null;
59211
60370
  }
@@ -59328,8 +60487,8 @@ async function daemonChildBoot(cwd) {
59328
60487
  repoId: config.repoId,
59329
60488
  daemonChild: true
59330
60489
  });
59331
- const stateDir = join82(cwd, ".unerr", "state");
59332
- const sockPath2 = join82(stateDir, "proxy.sock");
60490
+ const stateDir = join84(cwd, ".unerr", "state");
60491
+ const sockPath2 = join84(stateDir, "proxy.sock");
59333
60492
  if (process.send) {
59334
60493
  process.send({ type: "ready", sock: sockPath2 });
59335
60494
  }
@@ -59383,8 +60542,18 @@ async function mcpBoot(cwd) {
59383
60542
  keep: 5
59384
60543
  });
59385
60544
  initFileLog(cwd);
60545
+ const { StaticCatalogInterceptor: StaticCatalogInterceptor2 } = await Promise.resolve().then(() => (init_bridge_catalog(), bridge_catalog_exports));
60546
+ const interceptor = new StaticCatalogInterceptor2();
59386
60547
  const stdinPreBuffer = [];
59387
- const preBufferHandler = (chunk) => stdinPreBuffer.push(chunk);
60548
+ const preBufferHandler = (chunk) => {
60549
+ const out = interceptor.ingest(chunk);
60550
+ for (const reply of out.replies) {
60551
+ process.stdout.write(reply);
60552
+ }
60553
+ for (const buf of out.forward) {
60554
+ stdinPreBuffer.push(buf);
60555
+ }
60556
+ };
59388
60557
  process.stdin.on("data", preBufferHandler);
59389
60558
  let stdinEndedEarly = false;
59390
60559
  const earlyEndHandler = () => {
@@ -59446,6 +60615,8 @@ ${antiSignals.length > 0 ? ` Negative signals: ${antiSignals.map((s) => s.repla
59446
60615
  if (process.stdin.listenerCount("data") > 0 && stdinPreBuffer.length >= 0) {
59447
60616
  process.stdin.removeListener("data", preBufferHandler);
59448
60617
  process.stdin.removeListener("end", earlyEndHandler);
60618
+ const partial = interceptor.drainPartial();
60619
+ if (partial) stdinPreBuffer.push(partial);
59449
60620
  bufferForBridge = stdinPreBuffer.slice();
59450
60621
  stdinPreBuffer.length = 0;
59451
60622
  }
@@ -59500,10 +60671,10 @@ async function discoverWithRetry(cwd, daemonSockPath2, probeDaemon2, ensureRepo2
59500
60671
  let attempt = 0;
59501
60672
  let spawnAttempted = false;
59502
60673
  for (; ; ) {
59503
- const repoSock = join82(cwd, ".unerr", "state", "proxy.sock");
60674
+ const repoSock = join84(cwd, ".unerr", "state", "proxy.sock");
59504
60675
  if (existsSync81(repoSock)) {
59505
60676
  const { PidLock: PidLock2 } = await Promise.resolve().then(() => (init_pid_lock(), pid_lock_exports));
59506
- const pidLock = new PidLock2(join82(cwd, ".unerr", "state"));
60677
+ const pidLock = new PidLock2(join84(cwd, ".unerr", "state"));
59507
60678
  const probeResult = await pidLock.probe();
59508
60679
  if (probeResult.alive) {
59509
60680
  return {