@jefuriiij/synthra 0.8.1 → 0.10.0

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.
@@ -1785,6 +1785,7 @@ function resolvePaths(projectRoot) {
1785
1785
  activityLog: join5(graphDir, "activity.jsonl"),
1786
1786
  tokenLog: join5(graphDir, "token_log.jsonl"),
1787
1787
  gateLog: join5(graphDir, "gate_log.jsonl"),
1788
+ bashLog: join5(graphDir, "bash_log.jsonl"),
1788
1789
  toolLog: join5(graphDir, "tool_log.jsonl"),
1789
1790
  accessLog: join5(graphDir, "access_log.jsonl"),
1790
1791
  learnStore: join5(graphDir, "learn_store.json"),
@@ -1987,9 +1988,13 @@ async function patchClaudeMd(path, projectName) {
1987
1988
  return { created: true, updated: false, skipped: false };
1988
1989
  }
1989
1990
  const stripped = existing.replace(ANY_BLOCK_RE, "");
1990
- const hadBlock = stripped !== existing;
1991
- const desired = stripped.endsWith("\n") ? stripped + "\n" + block + "\n" : (stripped.length ? stripped + "\n\n" : "") + block + "\n";
1992
- if (hadBlock && desired === existing) {
1991
+ const base = stripped.replace(/\s+$/, "");
1992
+ const desired = base.length ? `${base}
1993
+
1994
+ ${block}
1995
+ ` : `${block}
1996
+ `;
1997
+ if (desired === existing) {
1993
1998
  return { created: false, updated: false, skipped: true };
1994
1999
  }
1995
2000
  await writeFile3(path, desired, "utf8");
@@ -2088,8 +2093,8 @@ async function scanProject(projectRootRaw, opts = {}) {
2088
2093
  const start = Date.now();
2089
2094
  const verbose = !opts.silent;
2090
2095
  if (verbose) log.info(`scanning ${projectRoot}`);
2091
- const boot = await bootstrap(paths);
2092
- if (verbose) {
2096
+ const boot = opts.skipBootstrap ? null : await bootstrap(paths);
2097
+ if (verbose && boot) {
2093
2098
  if (boot.graphCreated) log.info(" created .synthra-graph/");
2094
2099
  if (boot.contextCreated) log.info(" created .synthra/");
2095
2100
  if (boot.gitignoreUpdated) log.info(" updated .gitignore");
@@ -2314,6 +2319,40 @@ var LearnRuntime = class _LearnRuntime {
2314
2319
  }
2315
2320
  };
2316
2321
 
2322
+ // src/shared/config.ts
2323
+ function num(name, fallback) {
2324
+ const v = process.env[name];
2325
+ if (!v) return fallback;
2326
+ const n = Number(v);
2327
+ return Number.isFinite(n) ? n : fallback;
2328
+ }
2329
+ function str(name, fallback) {
2330
+ return process.env[name] ?? fallback;
2331
+ }
2332
+ function loadConfig() {
2333
+ return {
2334
+ hardMaxReadChars: num("SYN_HARD_MAX_READ_CHARS", 4e3),
2335
+ gateHintMaxChars: num("SYN_GATE_HINT_CHARS", 1200),
2336
+ readDepsMaxChars: num("SYN_READ_DEPS_CHARS", 900),
2337
+ turnReadBudgetChars: num("SYN_TURN_READ_BUDGET_CHARS", 18e3),
2338
+ fallbackMaxCallsPerTurn: num("SYN_FALLBACK_MAX_CALLS_PER_TURN", 1),
2339
+ retrieveCacheTtlSec: num("SYN_RETRIEVE_CACHE_TTL_SEC", 900),
2340
+ // Auto-reindex: re-run the incremental scan + swap the in-memory graph this
2341
+ // many ms after the last source-file change, so graph reads never go stale
2342
+ // mid-session. Set SYN_NO_AUTOREINDEX to disable entirely.
2343
+ reindexDebounceMs: num("SYN_REINDEX_DEBOUNCE_MS", 1e3),
2344
+ autoReindex: !process.env.SYN_NO_AUTOREINDEX,
2345
+ // Observe-only: log codebase-exploration Bash commands (grep/cat/find …) so
2346
+ // the terminal bypass of the Moat can be measured. Never blocks. Opt out
2347
+ // with SYN_NO_BASH_OBSERVE.
2348
+ bashObserve: !process.env.SYN_NO_BASH_OBSERVE,
2349
+ mcpPort: process.env.SYN_MCP_PORT ? num("SYN_MCP_PORT", 0) : null,
2350
+ dashboardPort: num("SYN_DASHBOARD_PORT", 8901),
2351
+ logLevel: str("SYN_LOG_LEVEL", "info"),
2352
+ claudeBin: str("SYN_CLAUDE_BIN", "claude")
2353
+ };
2354
+ }
2355
+
2317
2356
  // src/server/mcp.ts
2318
2357
  import { appendFile as appendFile3, mkdir as mkdir8 } from "fs/promises";
2319
2358
  import { dirname as dirname9 } from "path";
@@ -2935,31 +2974,6 @@ async function pack(files, opts) {
2935
2974
  };
2936
2975
  }
2937
2976
 
2938
- // src/shared/config.ts
2939
- function num(name, fallback) {
2940
- const v = process.env[name];
2941
- if (!v) return fallback;
2942
- const n = Number(v);
2943
- return Number.isFinite(n) ? n : fallback;
2944
- }
2945
- function str(name, fallback) {
2946
- return process.env[name] ?? fallback;
2947
- }
2948
- function loadConfig() {
2949
- return {
2950
- hardMaxReadChars: num("SYN_HARD_MAX_READ_CHARS", 4e3),
2951
- gateHintMaxChars: num("SYN_GATE_HINT_CHARS", 1200),
2952
- readDepsMaxChars: num("SYN_READ_DEPS_CHARS", 900),
2953
- turnReadBudgetChars: num("SYN_TURN_READ_BUDGET_CHARS", 18e3),
2954
- fallbackMaxCallsPerTurn: num("SYN_FALLBACK_MAX_CALLS_PER_TURN", 1),
2955
- retrieveCacheTtlSec: num("SYN_RETRIEVE_CACHE_TTL_SEC", 900),
2956
- mcpPort: process.env.SYN_MCP_PORT ? num("SYN_MCP_PORT", 0) : null,
2957
- dashboardPort: num("SYN_DASHBOARD_PORT", 8901),
2958
- logLevel: str("SYN_LOG_LEVEL", "info"),
2959
- claudeBin: str("SYN_CLAUDE_BIN", "claude")
2960
- };
2961
- }
2962
-
2963
2977
  // src/server/mcp.ts
2964
2978
  var PROTOCOL_VERSION = "2024-11-05";
2965
2979
  var SERVER_INFO = { name: "synthra", version: "0.0.1" };
@@ -3552,6 +3566,61 @@ function isFree(port) {
3552
3566
  });
3553
3567
  }
3554
3568
 
3569
+ // src/server/reindex.ts
3570
+ async function rescanAndSwap(ctx, paths, label) {
3571
+ try {
3572
+ await scanProject(paths.projectRoot, { silent: true, skipBootstrap: true });
3573
+ const [graph, symbolIndex] = await Promise.all([
3574
+ readGraph(paths.infoGraph),
3575
+ readSymbolIndex(paths.symbolIndex)
3576
+ ]);
3577
+ ctx.graph = graph;
3578
+ ctx.symbolIndex = symbolIndex;
3579
+ log.info(`reindexed (${label}) \u2014 ${graph.symbol_count} symbols, ${graph.edge_count} edges.`);
3580
+ } catch (err2) {
3581
+ log.warn(`reindex failed (${label}): ${err2.message}`);
3582
+ }
3583
+ }
3584
+ function createReindexer(ctx, paths, opts = {}) {
3585
+ const debounceMs = opts.debounceMs ?? 1e3;
3586
+ const rescan = opts.rescan ?? rescanAndSwap;
3587
+ let timer = null;
3588
+ let running = false;
3589
+ let pending = false;
3590
+ async function run() {
3591
+ if (running) {
3592
+ pending = true;
3593
+ return;
3594
+ }
3595
+ running = true;
3596
+ try {
3597
+ await rescan(ctx, paths, "edit");
3598
+ } finally {
3599
+ running = false;
3600
+ if (pending) {
3601
+ pending = false;
3602
+ void run();
3603
+ }
3604
+ }
3605
+ }
3606
+ return {
3607
+ schedule() {
3608
+ if (timer) clearTimeout(timer);
3609
+ timer = setTimeout(() => {
3610
+ timer = null;
3611
+ void run();
3612
+ }, debounceMs);
3613
+ timer.unref?.();
3614
+ },
3615
+ stop() {
3616
+ if (timer) {
3617
+ clearTimeout(timer);
3618
+ timer = null;
3619
+ }
3620
+ }
3621
+ };
3622
+ }
3623
+
3555
3624
  // src/server/routes/activity.ts
3556
3625
  async function handleActivity(sinceMs, ctx) {
3557
3626
  const events = ctx.activity.getEvents(sinceMs);
@@ -3652,22 +3721,14 @@ async function handleContextUpdate(req, ctx) {
3652
3721
  }
3653
3722
 
3654
3723
  // src/server/routes/gate.ts
3724
+ import { appendFile as appendFile5, mkdir as mkdir11 } from "fs/promises";
3725
+ import { dirname as dirname12 } from "path";
3726
+
3727
+ // src/server/routes/bash-observe.ts
3655
3728
  import { appendFile as appendFile4, mkdir as mkdir10 } from "fs/promises";
3656
3729
  import { dirname as dirname11 } from "path";
3657
- var BLOCKABLE_TOOLS = /* @__PURE__ */ new Set(["Grep", "Glob"]);
3658
- var RECENT_ACTIVITY_WINDOW_MS = 5 * 60 * 1e3;
3659
- function extractQuery(toolName, input) {
3660
- if (toolName === "Grep") {
3661
- const pattern = typeof input.pattern === "string" ? input.pattern : "";
3662
- const query = typeof input.query === "string" ? input.query : "";
3663
- return (pattern || query).trim() || null;
3664
- }
3665
- if (toolName === "Glob") {
3666
- const pattern = typeof input.pattern === "string" ? input.pattern : "";
3667
- return pattern.replace(/[*?/\\.]+/g, " ").trim() || null;
3668
- }
3669
- return null;
3670
- }
3730
+
3731
+ // src/server/routes/query-heuristics.ts
3671
3732
  function looksLikeNonSymbolQuery(pattern) {
3672
3733
  if (/<\/?[a-zA-Z]/.test(pattern)) return true;
3673
3734
  if (/[a-zA-Z][\w-]*-[\w-]*\s*=/.test(pattern)) return true;
@@ -3683,6 +3744,195 @@ function looksLikeNonSymbolQuery(pattern) {
3683
3744
  if (branches.length > 0 && branches.every(isKebab)) return true;
3684
3745
  return false;
3685
3746
  }
3747
+
3748
+ // src/server/routes/bash-observe.ts
3749
+ var SEARCH_TOOLS = /* @__PURE__ */ new Set(["grep", "egrep", "fgrep", "rg", "ripgrep", "ag", "ack"]);
3750
+ var READ_TOOLS = /* @__PURE__ */ new Set(["cat", "head", "tail", "less", "more", "bat", "tac"]);
3751
+ var LIST_TOOLS = /* @__PURE__ */ new Set(["find", "tree"]);
3752
+ var SOURCE_EXT = /\.(ts|tsx|js|jsx|cts|mts|cjs|mjs|py|pyi|svelte|vue|go|rs|java|kt|kts|php|rb|c|h|cpp|cc|cxx|hpp|cs|dart|json|md|css|html|hubl|sh|yml|yaml|toml)$/i;
3753
+ function tokenizeCommand(cmd) {
3754
+ const tokens = [];
3755
+ let cur = "";
3756
+ let quote = null;
3757
+ let hasContent2 = false;
3758
+ const flush = () => {
3759
+ if (hasContent2) tokens.push(cur);
3760
+ cur = "";
3761
+ hasContent2 = false;
3762
+ };
3763
+ for (let i = 0; i < cmd.length; i++) {
3764
+ const ch = cmd[i];
3765
+ if (quote) {
3766
+ if (ch === quote) quote = null;
3767
+ else cur += ch;
3768
+ hasContent2 = true;
3769
+ continue;
3770
+ }
3771
+ if (ch === '"' || ch === "'") {
3772
+ quote = ch;
3773
+ hasContent2 = true;
3774
+ continue;
3775
+ }
3776
+ if (ch === "|" || ch === "&" || ch === ";") {
3777
+ flush();
3778
+ let op = ch;
3779
+ if ((ch === "|" || ch === "&") && cmd[i + 1] === ch) {
3780
+ op += ch;
3781
+ i++;
3782
+ }
3783
+ tokens.push(op);
3784
+ continue;
3785
+ }
3786
+ if (/\s/.test(ch)) {
3787
+ flush();
3788
+ continue;
3789
+ }
3790
+ cur += ch;
3791
+ hasContent2 = true;
3792
+ }
3793
+ flush();
3794
+ return tokens;
3795
+ }
3796
+ function isOperator(t) {
3797
+ return t === "|" || t === "||" || t === "&&" || t === ";";
3798
+ }
3799
+ function splitSegments(tokens) {
3800
+ const segs = [];
3801
+ let cur = [];
3802
+ for (const t of tokens) {
3803
+ if (isOperator(t)) {
3804
+ if (cur.length) segs.push(cur);
3805
+ cur = [];
3806
+ } else {
3807
+ cur.push(t);
3808
+ }
3809
+ }
3810
+ if (cur.length) segs.push(cur);
3811
+ return segs;
3812
+ }
3813
+ function commandTokens(seg) {
3814
+ let i = 0;
3815
+ while (i < seg.length) {
3816
+ const t = seg[i];
3817
+ if (t === "env") {
3818
+ i++;
3819
+ continue;
3820
+ }
3821
+ if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(t)) {
3822
+ i++;
3823
+ continue;
3824
+ }
3825
+ break;
3826
+ }
3827
+ return seg.slice(i);
3828
+ }
3829
+ function baseCmd(tok) {
3830
+ return (tok.split(/[\\/]/).pop() ?? tok).toLowerCase();
3831
+ }
3832
+ function classifySegment(seg) {
3833
+ const toks = commandTokens(seg);
3834
+ if (toks.length === 0) return null;
3835
+ const cmd = baseCmd(toks[0]);
3836
+ const rest = toks.slice(1);
3837
+ const flags = rest.filter((a) => a.startsWith("-"));
3838
+ const nonFlags = rest.filter((a) => !a.startsWith("-"));
3839
+ if (SEARCH_TOOLS.has(cmd)) {
3840
+ const pattern = nonFlags[0];
3841
+ if (!pattern) return null;
3842
+ const recursive = flags.some(
3843
+ (f) => f === "-r" || f === "-R" || f === "--recursive" || /^-[a-zA-Z]*[rR]$/.test(f)
3844
+ );
3845
+ const rgLike = cmd === "rg" || cmd === "ripgrep" || cmd === "ag" || cmd === "ack";
3846
+ const hasPathArg = nonFlags.length > 1;
3847
+ if (!rgLike && !recursive && !hasPathArg) return null;
3848
+ return { kind: "search", tool: cmd, query: pattern };
3849
+ }
3850
+ if (READ_TOOLS.has(cmd)) {
3851
+ const file = nonFlags.find((a) => SOURCE_EXT.test(a));
3852
+ if (!file) return null;
3853
+ return { kind: "read", tool: cmd, query: file };
3854
+ }
3855
+ if (LIST_TOOLS.has(cmd)) {
3856
+ const nameIdx = rest.findIndex((a) => a === "-name" || a === "-iname" || a === "-path");
3857
+ const target = nameIdx >= 0 ? rest[nameIdx + 1] ?? null : nonFlags[0] ?? ".";
3858
+ return { kind: "list", tool: cmd, query: target };
3859
+ }
3860
+ return null;
3861
+ }
3862
+ function classifyBashCommand(command) {
3863
+ if (!command || typeof command !== "string") return null;
3864
+ const tokens = tokenizeCommand(command);
3865
+ if (tokens.length === 0) return null;
3866
+ if (tokens.some((t) => !isOperator(t) && t.includes(">"))) return null;
3867
+ const found = splitSegments(tokens).map(classifySegment).filter((x) => x !== null);
3868
+ if (found.length === 0) return null;
3869
+ const prio = { search: 0, read: 1, list: 2 };
3870
+ found.sort((a, b) => prio[a.kind] - prio[b.kind]);
3871
+ return found[0];
3872
+ }
3873
+ function graphHasFile(ctx, target) {
3874
+ const base = target.split(/[\\/]/).pop() ?? target;
3875
+ for (const n of ctx.graph.nodes) {
3876
+ if (n.kind !== "file") continue;
3877
+ if (n.path === target || n.path.endsWith(`/${target}`) || n.path.split("/").pop() === base) {
3878
+ return true;
3879
+ }
3880
+ }
3881
+ return false;
3882
+ }
3883
+ var Q_MAX = 200;
3884
+ var CMD_MAX = 300;
3885
+ var trunc = (s, max) => s.length > max ? `${s.slice(0, max)}\u2026` : s;
3886
+ async function logObservation(ctx, exp, confidence, avoidable, command) {
3887
+ try {
3888
+ await mkdir10(dirname11(ctx.paths.bashLog), { recursive: true });
3889
+ const entry = {
3890
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
3891
+ kind: exp.kind,
3892
+ tool: exp.tool,
3893
+ query: exp.query ? trunc(exp.query, Q_MAX) : null,
3894
+ confidence,
3895
+ avoidable,
3896
+ command: trunc(command, CMD_MAX)
3897
+ };
3898
+ await appendFile4(ctx.paths.bashLog, JSON.stringify(entry) + "\n", "utf8");
3899
+ } catch {
3900
+ }
3901
+ }
3902
+ async function observeBash(input, ctx) {
3903
+ if (!loadConfig().bashObserve) return;
3904
+ const command = typeof input.command === "string" ? input.command : "";
3905
+ const exp = classifyBashCommand(command);
3906
+ if (!exp) return;
3907
+ let confidence = null;
3908
+ let avoidable = false;
3909
+ if (exp.kind === "search" && exp.query) {
3910
+ if (!looksLikeNonSymbolQuery(exp.query)) {
3911
+ const r = await retrieve(ctx.graph, exp.query);
3912
+ confidence = r.confidence;
3913
+ avoidable = r.confidence !== "low" && r.symbolMatched;
3914
+ }
3915
+ } else if (exp.kind === "read" && exp.query) {
3916
+ avoidable = graphHasFile(ctx, exp.query);
3917
+ }
3918
+ await logObservation(ctx, exp, confidence, avoidable, command);
3919
+ }
3920
+
3921
+ // src/server/routes/gate.ts
3922
+ var BLOCKABLE_TOOLS = /* @__PURE__ */ new Set(["Grep", "Glob"]);
3923
+ var RECENT_ACTIVITY_WINDOW_MS = 5 * 60 * 1e3;
3924
+ function extractQuery(toolName, input) {
3925
+ if (toolName === "Grep") {
3926
+ const pattern = typeof input.pattern === "string" ? input.pattern : "";
3927
+ const query = typeof input.query === "string" ? input.query : "";
3928
+ return (pattern || query).trim() || null;
3929
+ }
3930
+ if (toolName === "Glob") {
3931
+ const pattern = typeof input.pattern === "string" ? input.pattern : "";
3932
+ return pattern.replace(/[*?/\\.]+/g, " ").trim() || null;
3933
+ }
3934
+ return null;
3935
+ }
3686
3936
  function recentlyTouchedMatchesQuery(recentPaths, queryTokens, graph) {
3687
3937
  if (recentPaths.length === 0) return [];
3688
3938
  const recent = new Set(recentPaths);
@@ -3715,7 +3965,7 @@ function recentlyTouchedMatchesQuery(recentPaths, queryTokens, graph) {
3715
3965
  var LOG_REASON_MAX_CHARS = 240;
3716
3966
  async function logDecision(ctx, toolName, query, decision, reason, hintChars) {
3717
3967
  try {
3718
- await mkdir10(dirname11(ctx.paths.gateLog), { recursive: true });
3968
+ await mkdir11(dirname12(ctx.paths.gateLog), { recursive: true });
3719
3969
  const entry = {
3720
3970
  ts: (/* @__PURE__ */ new Date()).toISOString(),
3721
3971
  tool: toolName,
@@ -3724,7 +3974,7 @@ async function logDecision(ctx, toolName, query, decision, reason, hintChars) {
3724
3974
  reason: reason && reason.length > LOG_REASON_MAX_CHARS ? `${reason.slice(0, LOG_REASON_MAX_CHARS)}\u2026` : reason,
3725
3975
  ...hintChars === void 0 ? {} : { hint_chars: hintChars }
3726
3976
  };
3727
- await appendFile4(ctx.paths.gateLog, JSON.stringify(entry) + "\n", "utf8");
3977
+ await appendFile5(ctx.paths.gateLog, JSON.stringify(entry) + "\n", "utf8");
3728
3978
  } catch {
3729
3979
  }
3730
3980
  }
@@ -3789,6 +4039,11 @@ async function handleGate(req, ctx) {
3789
4039
  if (!req?.tool_name || typeof req.tool_name !== "string") {
3790
4040
  return { decision: "allow", reason: "no tool_name" };
3791
4041
  }
4042
+ if (req.tool_name === "Bash") {
4043
+ const input2 = req.tool_input && typeof req.tool_input === "object" ? req.tool_input : {};
4044
+ await observeBash(input2, ctx);
4045
+ return { decision: "allow" };
4046
+ }
3792
4047
  if (!BLOCKABLE_TOOLS.has(req.tool_name)) {
3793
4048
  return { decision: "allow" };
3794
4049
  }
@@ -3842,16 +4097,16 @@ async function handleGate(req, ctx) {
3842
4097
  }
3843
4098
 
3844
4099
  // src/server/routes/log.ts
3845
- import { appendFile as appendFile5, mkdir as mkdir11 } from "fs/promises";
3846
- import { dirname as dirname12 } from "path";
4100
+ import { appendFile as appendFile6, mkdir as mkdir12 } from "fs/promises";
4101
+ import { dirname as dirname13 } from "path";
3847
4102
  async function handleLog(entry, ctx) {
3848
4103
  if (!entry || typeof entry.input_tokens !== "number" || typeof entry.output_tokens !== "number") {
3849
4104
  throw new Error("log: input_tokens and output_tokens (number) are required");
3850
4105
  }
3851
4106
  const written_at = (/* @__PURE__ */ new Date()).toISOString();
3852
4107
  const record = { ...entry, written_at };
3853
- await mkdir11(dirname12(ctx.paths.tokenLog), { recursive: true });
3854
- await appendFile5(ctx.paths.tokenLog, JSON.stringify(record) + "\n", "utf8");
4108
+ await mkdir12(dirname13(ctx.paths.tokenLog), { recursive: true });
4109
+ await appendFile6(ctx.paths.tokenLog, JSON.stringify(record) + "\n", "utf8");
3855
4110
  return { ok: true, written_at };
3856
4111
  }
3857
4112
 
@@ -4037,24 +4292,17 @@ async function startServer(paths, options = {}) {
4037
4292
  const app = buildApp(ctx, port);
4038
4293
  const nodeServer = serve({ fetch: app.fetch, port, hostname: "127.0.0.1" });
4039
4294
  await writeFile9(paths.mcpPort, String(port), "utf8");
4040
- const fileWatcher = createFileWatcher(paths.projectRoot, (e) => ctx.activity.add(e));
4295
+ const cfg = loadConfig();
4296
+ const reindexer = cfg.autoReindex ? createReindexer(ctx, paths, { debounceMs: cfg.reindexDebounceMs }) : null;
4297
+ const fileWatcher = createFileWatcher(paths.projectRoot, (e) => {
4298
+ void ctx.activity.add(e);
4299
+ reindexer?.schedule();
4300
+ });
4041
4301
  const gitWatcher = createGitWatcher(paths.projectRoot, async (e) => {
4042
4302
  await ctx.activity.add(e);
4043
4303
  if (e.kind === "branch-switch") {
4044
- try {
4045
- const to = e.details?.to ?? "unknown";
4046
- log.info(`branch switched to '${to}' \u2014 rebuilding graph\u2026`);
4047
- await scanProject(paths.projectRoot, { silent: true });
4048
- const [g, idx] = await Promise.all([
4049
- readGraph(paths.infoGraph),
4050
- readSymbolIndex(paths.symbolIndex)
4051
- ]);
4052
- ctx.graph = g;
4053
- ctx.symbolIndex = idx;
4054
- log.info(`graph rebuilt for '${to}' (${g.symbol_count} symbols).`);
4055
- } catch (err2) {
4056
- log.warn(`branch rescan failed: ${err2.message}`);
4057
- }
4304
+ const to = e.details?.to ?? "unknown";
4305
+ await rescanAndSwap(ctx, paths, `branch ${to}`);
4058
4306
  }
4059
4307
  });
4060
4308
  try {
@@ -4072,6 +4320,7 @@ async function startServer(paths, options = {}) {
4072
4320
  port,
4073
4321
  url,
4074
4322
  async stop() {
4323
+ reindexer?.stop();
4075
4324
  await fileWatcher.stop().catch(() => void 0);
4076
4325
  await gitWatcher.stop().catch(() => void 0);
4077
4326
  await ctx.learn?.flush().catch(() => void 0);