@jefuriiij/synthra 0.8.0 → 0.9.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.
@@ -1987,9 +1987,13 @@ async function patchClaudeMd(path, projectName) {
1987
1987
  return { created: true, updated: false, skipped: false };
1988
1988
  }
1989
1989
  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) {
1990
+ const base = stripped.replace(/\s+$/, "");
1991
+ const desired = base.length ? `${base}
1992
+
1993
+ ${block}
1994
+ ` : `${block}
1995
+ `;
1996
+ if (desired === existing) {
1993
1997
  return { created: false, updated: false, skipped: true };
1994
1998
  }
1995
1999
  await writeFile3(path, desired, "utf8");
@@ -2088,8 +2092,8 @@ async function scanProject(projectRootRaw, opts = {}) {
2088
2092
  const start = Date.now();
2089
2093
  const verbose = !opts.silent;
2090
2094
  if (verbose) log.info(`scanning ${projectRoot}`);
2091
- const boot = await bootstrap(paths);
2092
- if (verbose) {
2095
+ const boot = opts.skipBootstrap ? null : await bootstrap(paths);
2096
+ if (verbose && boot) {
2093
2097
  if (boot.graphCreated) log.info(" created .synthra-graph/");
2094
2098
  if (boot.contextCreated) log.info(" created .synthra/");
2095
2099
  if (boot.gitignoreUpdated) log.info(" updated .gitignore");
@@ -2314,6 +2318,36 @@ var LearnRuntime = class _LearnRuntime {
2314
2318
  }
2315
2319
  };
2316
2320
 
2321
+ // src/shared/config.ts
2322
+ function num(name, fallback) {
2323
+ const v = process.env[name];
2324
+ if (!v) return fallback;
2325
+ const n = Number(v);
2326
+ return Number.isFinite(n) ? n : fallback;
2327
+ }
2328
+ function str(name, fallback) {
2329
+ return process.env[name] ?? fallback;
2330
+ }
2331
+ function loadConfig() {
2332
+ return {
2333
+ hardMaxReadChars: num("SYN_HARD_MAX_READ_CHARS", 4e3),
2334
+ gateHintMaxChars: num("SYN_GATE_HINT_CHARS", 1200),
2335
+ readDepsMaxChars: num("SYN_READ_DEPS_CHARS", 900),
2336
+ turnReadBudgetChars: num("SYN_TURN_READ_BUDGET_CHARS", 18e3),
2337
+ fallbackMaxCallsPerTurn: num("SYN_FALLBACK_MAX_CALLS_PER_TURN", 1),
2338
+ retrieveCacheTtlSec: num("SYN_RETRIEVE_CACHE_TTL_SEC", 900),
2339
+ // Auto-reindex: re-run the incremental scan + swap the in-memory graph this
2340
+ // many ms after the last source-file change, so graph reads never go stale
2341
+ // mid-session. Set SYN_NO_AUTOREINDEX to disable entirely.
2342
+ reindexDebounceMs: num("SYN_REINDEX_DEBOUNCE_MS", 1e3),
2343
+ autoReindex: !process.env.SYN_NO_AUTOREINDEX,
2344
+ mcpPort: process.env.SYN_MCP_PORT ? num("SYN_MCP_PORT", 0) : null,
2345
+ dashboardPort: num("SYN_DASHBOARD_PORT", 8901),
2346
+ logLevel: str("SYN_LOG_LEVEL", "info"),
2347
+ claudeBin: str("SYN_CLAUDE_BIN", "claude")
2348
+ };
2349
+ }
2350
+
2317
2351
  // src/server/mcp.ts
2318
2352
  import { appendFile as appendFile3, mkdir as mkdir8 } from "fs/promises";
2319
2353
  import { dirname as dirname9 } from "path";
@@ -2935,31 +2969,6 @@ async function pack(files, opts) {
2935
2969
  };
2936
2970
  }
2937
2971
 
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
2972
  // src/server/mcp.ts
2964
2973
  var PROTOCOL_VERSION = "2024-11-05";
2965
2974
  var SERVER_INFO = { name: "synthra", version: "0.0.1" };
@@ -3552,6 +3561,61 @@ function isFree(port) {
3552
3561
  });
3553
3562
  }
3554
3563
 
3564
+ // src/server/reindex.ts
3565
+ async function rescanAndSwap(ctx, paths, label) {
3566
+ try {
3567
+ await scanProject(paths.projectRoot, { silent: true, skipBootstrap: true });
3568
+ const [graph, symbolIndex] = await Promise.all([
3569
+ readGraph(paths.infoGraph),
3570
+ readSymbolIndex(paths.symbolIndex)
3571
+ ]);
3572
+ ctx.graph = graph;
3573
+ ctx.symbolIndex = symbolIndex;
3574
+ log.info(`reindexed (${label}) \u2014 ${graph.symbol_count} symbols, ${graph.edge_count} edges.`);
3575
+ } catch (err2) {
3576
+ log.warn(`reindex failed (${label}): ${err2.message}`);
3577
+ }
3578
+ }
3579
+ function createReindexer(ctx, paths, opts = {}) {
3580
+ const debounceMs = opts.debounceMs ?? 1e3;
3581
+ const rescan = opts.rescan ?? rescanAndSwap;
3582
+ let timer = null;
3583
+ let running = false;
3584
+ let pending = false;
3585
+ async function run() {
3586
+ if (running) {
3587
+ pending = true;
3588
+ return;
3589
+ }
3590
+ running = true;
3591
+ try {
3592
+ await rescan(ctx, paths, "edit");
3593
+ } finally {
3594
+ running = false;
3595
+ if (pending) {
3596
+ pending = false;
3597
+ void run();
3598
+ }
3599
+ }
3600
+ }
3601
+ return {
3602
+ schedule() {
3603
+ if (timer) clearTimeout(timer);
3604
+ timer = setTimeout(() => {
3605
+ timer = null;
3606
+ void run();
3607
+ }, debounceMs);
3608
+ timer.unref?.();
3609
+ },
3610
+ stop() {
3611
+ if (timer) {
3612
+ clearTimeout(timer);
3613
+ timer = null;
3614
+ }
3615
+ }
3616
+ };
3617
+ }
3618
+
3555
3619
  // src/server/routes/activity.ts
3556
3620
  async function handleActivity(sinceMs, ctx) {
3557
3621
  const events = ctx.activity.getEvents(sinceMs);
@@ -4037,24 +4101,17 @@ async function startServer(paths, options = {}) {
4037
4101
  const app = buildApp(ctx, port);
4038
4102
  const nodeServer = serve({ fetch: app.fetch, port, hostname: "127.0.0.1" });
4039
4103
  await writeFile9(paths.mcpPort, String(port), "utf8");
4040
- const fileWatcher = createFileWatcher(paths.projectRoot, (e) => ctx.activity.add(e));
4104
+ const cfg = loadConfig();
4105
+ const reindexer = cfg.autoReindex ? createReindexer(ctx, paths, { debounceMs: cfg.reindexDebounceMs }) : null;
4106
+ const fileWatcher = createFileWatcher(paths.projectRoot, (e) => {
4107
+ void ctx.activity.add(e);
4108
+ reindexer?.schedule();
4109
+ });
4041
4110
  const gitWatcher = createGitWatcher(paths.projectRoot, async (e) => {
4042
4111
  await ctx.activity.add(e);
4043
4112
  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
- }
4113
+ const to = e.details?.to ?? "unknown";
4114
+ await rescanAndSwap(ctx, paths, `branch ${to}`);
4058
4115
  }
4059
4116
  });
4060
4117
  try {
@@ -4072,6 +4129,7 @@ async function startServer(paths, options = {}) {
4072
4129
  port,
4073
4130
  url,
4074
4131
  async stop() {
4132
+ reindexer?.stop();
4075
4133
  await fileWatcher.stop().catch(() => void 0);
4076
4134
  await gitWatcher.stop().catch(() => void 0);
4077
4135
  await ctx.learn?.flush().catch(() => void 0);