@jefuriiij/synthra 0.8.1 → 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.
- package/CHANGELOG.md +28 -0
- package/dist/cli/index.js +104 -46
- package/dist/cli/index.js.map +1 -1
- package/dist/dashboard/index.js +1 -1
- package/dist/dashboard/index.js.map +1 -1
- package/dist/server/index.js +103 -45
- package/dist/server/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,34 @@ For older versions, see [GitHub Releases](https://github.com/jefuriiij/synthra/r
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## [0.9.0] — 2026-06-20
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **The graph auto-reindexes edited files mid-session — it never goes stale.**
|
|
15
|
+
Previously the in-memory graph was a snapshot from the last `syn .`: edit a
|
|
16
|
+
file and `graph_read` / `blast_radius` / the dependency footer would keep
|
|
17
|
+
serving the *old* signature, body, and line ranges until the next manual scan.
|
|
18
|
+
Now the running server watches for source changes and, ~1s after edits settle,
|
|
19
|
+
re-runs the incremental scan and hot-swaps the graph in place — so reads always
|
|
20
|
+
reflect what's on disk. The rescan is incremental (only the changed file hits
|
|
21
|
+
tree-sitter; everything else reuses the content-hash parse cache) and debounced
|
|
22
|
+
so a burst of saves coalesces into one rebuild. Tune with
|
|
23
|
+
`SYN_REINDEX_DEBOUNCE_MS` (default `1000`); disable with `SYN_NO_AUTOREINDEX`.
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- **In-session rescans (auto-reindex and branch-switch) no longer rewrite your
|
|
28
|
+
`CLAUDE.md` / `.gitignore`.** A rescan now skips the bootstrap step — it only
|
|
29
|
+
rebuilds the graph. This also closes a feedback loop the new auto-reindex would
|
|
30
|
+
otherwise hit (rewriting the watched `CLAUDE.md` on every rescan would retrigger
|
|
31
|
+
the watcher endlessly).
|
|
32
|
+
- **`CLAUDE.md` no longer accumulates a blank line on every `syn .`.** The policy
|
|
33
|
+
block patcher is now idempotent — re-running with nothing to change is a true
|
|
34
|
+
no-op instead of appending an empty line above the managed block each time.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
10
38
|
## [0.8.1] — 2026-06-16
|
|
11
39
|
|
|
12
40
|
### Changed
|
package/dist/cli/index.js
CHANGED
|
@@ -18,7 +18,7 @@ var init_package = __esm({
|
|
|
18
18
|
"package.json"() {
|
|
19
19
|
package_default = {
|
|
20
20
|
name: "@jefuriiij/synthra",
|
|
21
|
-
version: "0.
|
|
21
|
+
version: "0.9.0",
|
|
22
22
|
publishConfig: {
|
|
23
23
|
access: "public"
|
|
24
24
|
},
|
|
@@ -3167,9 +3167,13 @@ async function patchClaudeMd(path, projectName) {
|
|
|
3167
3167
|
return { created: true, updated: false, skipped: false };
|
|
3168
3168
|
}
|
|
3169
3169
|
const stripped = existing.replace(ANY_BLOCK_RE, "");
|
|
3170
|
-
const
|
|
3171
|
-
const desired =
|
|
3172
|
-
|
|
3170
|
+
const base = stripped.replace(/\s+$/, "");
|
|
3171
|
+
const desired = base.length ? `${base}
|
|
3172
|
+
|
|
3173
|
+
${block}
|
|
3174
|
+
` : `${block}
|
|
3175
|
+
`;
|
|
3176
|
+
if (desired === existing) {
|
|
3173
3177
|
return { created: false, updated: false, skipped: true };
|
|
3174
3178
|
}
|
|
3175
3179
|
await writeFile6(path, desired, "utf8");
|
|
@@ -3268,8 +3272,8 @@ async function scanProject(projectRootRaw, opts = {}) {
|
|
|
3268
3272
|
const start = Date.now();
|
|
3269
3273
|
const verbose = !opts.silent;
|
|
3270
3274
|
if (verbose) log.info(`scanning ${projectRoot}`);
|
|
3271
|
-
const boot = await bootstrap(paths);
|
|
3272
|
-
if (verbose) {
|
|
3275
|
+
const boot = opts.skipBootstrap ? null : await bootstrap(paths);
|
|
3276
|
+
if (verbose && boot) {
|
|
3273
3277
|
if (boot.graphCreated) log.info(" created .synthra-graph/");
|
|
3274
3278
|
if (boot.contextCreated) log.info(" created .synthra/");
|
|
3275
3279
|
if (boot.gitignoreUpdated) log.info(" updated .gitignore");
|
|
@@ -3379,6 +3383,36 @@ var LearnRuntime = class _LearnRuntime {
|
|
|
3379
3383
|
}
|
|
3380
3384
|
};
|
|
3381
3385
|
|
|
3386
|
+
// src/shared/config.ts
|
|
3387
|
+
function num(name, fallback) {
|
|
3388
|
+
const v = process.env[name];
|
|
3389
|
+
if (!v) return fallback;
|
|
3390
|
+
const n = Number(v);
|
|
3391
|
+
return Number.isFinite(n) ? n : fallback;
|
|
3392
|
+
}
|
|
3393
|
+
function str(name, fallback) {
|
|
3394
|
+
return process.env[name] ?? fallback;
|
|
3395
|
+
}
|
|
3396
|
+
function loadConfig() {
|
|
3397
|
+
return {
|
|
3398
|
+
hardMaxReadChars: num("SYN_HARD_MAX_READ_CHARS", 4e3),
|
|
3399
|
+
gateHintMaxChars: num("SYN_GATE_HINT_CHARS", 1200),
|
|
3400
|
+
readDepsMaxChars: num("SYN_READ_DEPS_CHARS", 900),
|
|
3401
|
+
turnReadBudgetChars: num("SYN_TURN_READ_BUDGET_CHARS", 18e3),
|
|
3402
|
+
fallbackMaxCallsPerTurn: num("SYN_FALLBACK_MAX_CALLS_PER_TURN", 1),
|
|
3403
|
+
retrieveCacheTtlSec: num("SYN_RETRIEVE_CACHE_TTL_SEC", 900),
|
|
3404
|
+
// Auto-reindex: re-run the incremental scan + swap the in-memory graph this
|
|
3405
|
+
// many ms after the last source-file change, so graph reads never go stale
|
|
3406
|
+
// mid-session. Set SYN_NO_AUTOREINDEX to disable entirely.
|
|
3407
|
+
reindexDebounceMs: num("SYN_REINDEX_DEBOUNCE_MS", 1e3),
|
|
3408
|
+
autoReindex: !process.env.SYN_NO_AUTOREINDEX,
|
|
3409
|
+
mcpPort: process.env.SYN_MCP_PORT ? num("SYN_MCP_PORT", 0) : null,
|
|
3410
|
+
dashboardPort: num("SYN_DASHBOARD_PORT", 8901),
|
|
3411
|
+
logLevel: str("SYN_LOG_LEVEL", "info"),
|
|
3412
|
+
claudeBin: str("SYN_CLAUDE_BIN", "claude")
|
|
3413
|
+
};
|
|
3414
|
+
}
|
|
3415
|
+
|
|
3382
3416
|
// src/server/mcp.ts
|
|
3383
3417
|
import { appendFile as appendFile3, mkdir as mkdir10 } from "fs/promises";
|
|
3384
3418
|
import { dirname as dirname12 } from "path";
|
|
@@ -4000,31 +4034,6 @@ async function pack(files, opts) {
|
|
|
4000
4034
|
};
|
|
4001
4035
|
}
|
|
4002
4036
|
|
|
4003
|
-
// src/shared/config.ts
|
|
4004
|
-
function num(name, fallback) {
|
|
4005
|
-
const v = process.env[name];
|
|
4006
|
-
if (!v) return fallback;
|
|
4007
|
-
const n = Number(v);
|
|
4008
|
-
return Number.isFinite(n) ? n : fallback;
|
|
4009
|
-
}
|
|
4010
|
-
function str(name, fallback) {
|
|
4011
|
-
return process.env[name] ?? fallback;
|
|
4012
|
-
}
|
|
4013
|
-
function loadConfig() {
|
|
4014
|
-
return {
|
|
4015
|
-
hardMaxReadChars: num("SYN_HARD_MAX_READ_CHARS", 4e3),
|
|
4016
|
-
gateHintMaxChars: num("SYN_GATE_HINT_CHARS", 1200),
|
|
4017
|
-
readDepsMaxChars: num("SYN_READ_DEPS_CHARS", 900),
|
|
4018
|
-
turnReadBudgetChars: num("SYN_TURN_READ_BUDGET_CHARS", 18e3),
|
|
4019
|
-
fallbackMaxCallsPerTurn: num("SYN_FALLBACK_MAX_CALLS_PER_TURN", 1),
|
|
4020
|
-
retrieveCacheTtlSec: num("SYN_RETRIEVE_CACHE_TTL_SEC", 900),
|
|
4021
|
-
mcpPort: process.env.SYN_MCP_PORT ? num("SYN_MCP_PORT", 0) : null,
|
|
4022
|
-
dashboardPort: num("SYN_DASHBOARD_PORT", 8901),
|
|
4023
|
-
logLevel: str("SYN_LOG_LEVEL", "info"),
|
|
4024
|
-
claudeBin: str("SYN_CLAUDE_BIN", "claude")
|
|
4025
|
-
};
|
|
4026
|
-
}
|
|
4027
|
-
|
|
4028
4037
|
// src/server/mcp.ts
|
|
4029
4038
|
var PROTOCOL_VERSION = "2024-11-05";
|
|
4030
4039
|
var SERVER_INFO = { name: "synthra", version: "0.0.1" };
|
|
@@ -4598,6 +4607,61 @@ async function handleMcpRequest(body, ctx) {
|
|
|
4598
4607
|
}
|
|
4599
4608
|
}
|
|
4600
4609
|
|
|
4610
|
+
// src/server/reindex.ts
|
|
4611
|
+
async function rescanAndSwap(ctx, paths, label) {
|
|
4612
|
+
try {
|
|
4613
|
+
await scanProject(paths.projectRoot, { silent: true, skipBootstrap: true });
|
|
4614
|
+
const [graph, symbolIndex] = await Promise.all([
|
|
4615
|
+
readGraph(paths.infoGraph),
|
|
4616
|
+
readSymbolIndex(paths.symbolIndex)
|
|
4617
|
+
]);
|
|
4618
|
+
ctx.graph = graph;
|
|
4619
|
+
ctx.symbolIndex = symbolIndex;
|
|
4620
|
+
log.info(`reindexed (${label}) \u2014 ${graph.symbol_count} symbols, ${graph.edge_count} edges.`);
|
|
4621
|
+
} catch (err2) {
|
|
4622
|
+
log.warn(`reindex failed (${label}): ${err2.message}`);
|
|
4623
|
+
}
|
|
4624
|
+
}
|
|
4625
|
+
function createReindexer(ctx, paths, opts = {}) {
|
|
4626
|
+
const debounceMs = opts.debounceMs ?? 1e3;
|
|
4627
|
+
const rescan = opts.rescan ?? rescanAndSwap;
|
|
4628
|
+
let timer = null;
|
|
4629
|
+
let running = false;
|
|
4630
|
+
let pending = false;
|
|
4631
|
+
async function run() {
|
|
4632
|
+
if (running) {
|
|
4633
|
+
pending = true;
|
|
4634
|
+
return;
|
|
4635
|
+
}
|
|
4636
|
+
running = true;
|
|
4637
|
+
try {
|
|
4638
|
+
await rescan(ctx, paths, "edit");
|
|
4639
|
+
} finally {
|
|
4640
|
+
running = false;
|
|
4641
|
+
if (pending) {
|
|
4642
|
+
pending = false;
|
|
4643
|
+
void run();
|
|
4644
|
+
}
|
|
4645
|
+
}
|
|
4646
|
+
}
|
|
4647
|
+
return {
|
|
4648
|
+
schedule() {
|
|
4649
|
+
if (timer) clearTimeout(timer);
|
|
4650
|
+
timer = setTimeout(() => {
|
|
4651
|
+
timer = null;
|
|
4652
|
+
void run();
|
|
4653
|
+
}, debounceMs);
|
|
4654
|
+
timer.unref?.();
|
|
4655
|
+
},
|
|
4656
|
+
stop() {
|
|
4657
|
+
if (timer) {
|
|
4658
|
+
clearTimeout(timer);
|
|
4659
|
+
timer = null;
|
|
4660
|
+
}
|
|
4661
|
+
}
|
|
4662
|
+
};
|
|
4663
|
+
}
|
|
4664
|
+
|
|
4601
4665
|
// src/server/routes/activity.ts
|
|
4602
4666
|
async function handleActivity(sinceMs, ctx) {
|
|
4603
4667
|
const events = ctx.activity.getEvents(sinceMs);
|
|
@@ -5083,24 +5147,17 @@ async function startServer(paths, options = {}) {
|
|
|
5083
5147
|
const app = buildApp(ctx, port);
|
|
5084
5148
|
const nodeServer = serve2({ fetch: app.fetch, port, hostname: "127.0.0.1" });
|
|
5085
5149
|
await writeFile11(paths.mcpPort, String(port), "utf8");
|
|
5086
|
-
const
|
|
5150
|
+
const cfg = loadConfig();
|
|
5151
|
+
const reindexer = cfg.autoReindex ? createReindexer(ctx, paths, { debounceMs: cfg.reindexDebounceMs }) : null;
|
|
5152
|
+
const fileWatcher = createFileWatcher(paths.projectRoot, (e) => {
|
|
5153
|
+
void ctx.activity.add(e);
|
|
5154
|
+
reindexer?.schedule();
|
|
5155
|
+
});
|
|
5087
5156
|
const gitWatcher = createGitWatcher(paths.projectRoot, async (e) => {
|
|
5088
5157
|
await ctx.activity.add(e);
|
|
5089
5158
|
if (e.kind === "branch-switch") {
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
log.info(`branch switched to '${to}' \u2014 rebuilding graph\u2026`);
|
|
5093
|
-
await scanProject(paths.projectRoot, { silent: true });
|
|
5094
|
-
const [g, idx] = await Promise.all([
|
|
5095
|
-
readGraph(paths.infoGraph),
|
|
5096
|
-
readSymbolIndex(paths.symbolIndex)
|
|
5097
|
-
]);
|
|
5098
|
-
ctx.graph = g;
|
|
5099
|
-
ctx.symbolIndex = idx;
|
|
5100
|
-
log.info(`graph rebuilt for '${to}' (${g.symbol_count} symbols).`);
|
|
5101
|
-
} catch (err2) {
|
|
5102
|
-
log.warn(`branch rescan failed: ${err2.message}`);
|
|
5103
|
-
}
|
|
5159
|
+
const to = e.details?.to ?? "unknown";
|
|
5160
|
+
await rescanAndSwap(ctx, paths, `branch ${to}`);
|
|
5104
5161
|
}
|
|
5105
5162
|
});
|
|
5106
5163
|
try {
|
|
@@ -5118,6 +5175,7 @@ async function startServer(paths, options = {}) {
|
|
|
5118
5175
|
port,
|
|
5119
5176
|
url,
|
|
5120
5177
|
async stop() {
|
|
5178
|
+
reindexer?.stop();
|
|
5121
5179
|
await fileWatcher.stop().catch(() => void 0);
|
|
5122
5180
|
await gitWatcher.stop().catch(() => void 0);
|
|
5123
5181
|
await ctx.learn?.flush().catch(() => void 0);
|