@jefuriiij/synthra 0.12.0 → 0.13.1
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 +34 -0
- package/dist/cli/index.js +198 -7
- 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 +197 -6
- package/dist/server/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,40 @@ For older versions, see [GitHub Releases](https://github.com/jefuriiij/synthra/r
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## [0.13.1] — 2026-06-24
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **Minified/bundle files are no longer indexed.** Committed vendored plugin JS
|
|
15
|
+
(`*.min.js`, `*.bundle.js`, `*.min.css`, …) has no readable symbols, so indexing
|
|
16
|
+
it only polluted retrieval and caused **useless Moat blocks** on markup-heavy
|
|
17
|
+
projects — a Grep for CSS classes like `nav|menu|toggle` would spuriously match a
|
|
18
|
+
symbol *inside* the minified library and get blocked, only for `graph_continue` to
|
|
19
|
+
then find nothing. The scanner now skips these files (cleaner retrieval, smaller
|
|
20
|
+
graph, no behavior change for real source).
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## [0.13.0] — 2026-06-24
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- **The resume digest now lists the symbols that changed since your last session.**
|
|
29
|
+
The SessionStart "Since you were last here" primer showed *files* touched; it now
|
|
30
|
+
leads its supporting context with the actual **symbols/signatures** that changed —
|
|
31
|
+
e.g. `src/auth.ts::login (function) — function login(creds: Creds): Promise<...>`.
|
|
32
|
+
Computed from a git diff against the previous session's HEAD (committed **and**
|
|
33
|
+
uncommitted changes), overlapped with the current graph. Best-effort: silently
|
|
34
|
+
omitted in non-git projects.
|
|
35
|
+
- **`call_path(from, to)` — trace control flow.** Returns the shortest chain of
|
|
36
|
+
calls from one symbol to another (`handler → service → repo`), so you can see how
|
|
37
|
+
one symbol reaches another. The forward complement to `blast_radius` (callers).
|
|
38
|
+
Each of `from`/`to` is a `file::symbol` target or a bare symbol name when unique.
|
|
39
|
+
|
|
40
|
+
Both reuse the existing call graph + git — no graph schema change, no new dependencies.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
10
44
|
## [0.12.0] — 2026-06-24
|
|
11
45
|
|
|
12
46
|
### Added
|
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.13.1",
|
|
22
22
|
publishConfig: {
|
|
23
23
|
access: "public"
|
|
24
24
|
},
|
|
@@ -2890,7 +2890,17 @@ var DEFAULT_IGNORE = [
|
|
|
2890
2890
|
".mypy_cache/",
|
|
2891
2891
|
".ruff_cache/",
|
|
2892
2892
|
// .NET
|
|
2893
|
-
"obj/"
|
|
2893
|
+
"obj/",
|
|
2894
|
+
// Generated / minified bundles — no readable symbols, so indexing them only
|
|
2895
|
+
// pollutes retrieval (a markup query like `nav|menu|toggle` spuriously matches
|
|
2896
|
+
// a symbol inside vendored plugin JS → a useless Moat block) and bloats the
|
|
2897
|
+
// graph. Committed bootstrap/swiper-style plugin JS is the common offender.
|
|
2898
|
+
"*.min.js",
|
|
2899
|
+
"*.min.cjs",
|
|
2900
|
+
"*.min.mjs",
|
|
2901
|
+
"*.min.css",
|
|
2902
|
+
"*.bundle.js",
|
|
2903
|
+
"*-min.js"
|
|
2894
2904
|
];
|
|
2895
2905
|
var BINARY_EXTS = /* @__PURE__ */ new Set([
|
|
2896
2906
|
".png",
|
|
@@ -4244,6 +4254,19 @@ var TOOLS = [
|
|
|
4244
4254
|
limit: { type: "number", description: "Cap on returned names. Default 30." }
|
|
4245
4255
|
}
|
|
4246
4256
|
}
|
|
4257
|
+
},
|
|
4258
|
+
{
|
|
4259
|
+
name: "call_path",
|
|
4260
|
+
description: "Trace how one symbol reaches another through the call graph \u2014 the shortest chain of calls from 'from' to 'to'. Use to understand control flow ('how does this handler end up hitting the DB layer?'). Each of 'from'/'to' is a 'file::symbol' target or a bare symbol name when unique.",
|
|
4261
|
+
inputSchema: {
|
|
4262
|
+
type: "object",
|
|
4263
|
+
properties: {
|
|
4264
|
+
from: { type: "string", description: "Starting symbol ('file::symbol' or unique name)." },
|
|
4265
|
+
to: { type: "string", description: "Target symbol ('file::symbol' or unique name)." },
|
|
4266
|
+
depth: { type: "number", description: "Max call hops to search. Default 6." }
|
|
4267
|
+
},
|
|
4268
|
+
required: ["from", "to"]
|
|
4269
|
+
}
|
|
4247
4270
|
}
|
|
4248
4271
|
];
|
|
4249
4272
|
async function callTool(name, args, ctx) {
|
|
@@ -4270,6 +4293,8 @@ async function callTool(name, args, ctx) {
|
|
|
4270
4293
|
return findSymbol(args, ctx);
|
|
4271
4294
|
case "duplicate_symbols":
|
|
4272
4295
|
return duplicateSymbols(args, ctx);
|
|
4296
|
+
case "call_path":
|
|
4297
|
+
return callPath(args, ctx);
|
|
4273
4298
|
default:
|
|
4274
4299
|
return errorContent(`Unknown tool: ${name}`);
|
|
4275
4300
|
}
|
|
@@ -4426,6 +4451,97 @@ function testsCoveringLine(graph, filePaths) {
|
|
|
4426
4451
|
const omitted = tests.length - shown.length;
|
|
4427
4452
|
return `Tests covering the impact: ${shown.join(" \xB7 ")}${omitted > 0 ? ` \u2026+${omitted} more` : ""}`;
|
|
4428
4453
|
}
|
|
4454
|
+
function resolveSymbolArg(ctx, arg) {
|
|
4455
|
+
const a = arg.trim();
|
|
4456
|
+
if (a.includes("::")) {
|
|
4457
|
+
const [rawFile, rawSym] = a.split("::", 2);
|
|
4458
|
+
const resolved = resolveFileTarget(ctx.graph, (rawFile ?? "").trim());
|
|
4459
|
+
if (!("node" in resolved)) return null;
|
|
4460
|
+
const name = (rawSym ?? "").trim();
|
|
4461
|
+
return ctx.graph.nodes.find(
|
|
4462
|
+
(n) => n.kind === "symbol" && n.file === resolved.node.path && n.name === name
|
|
4463
|
+
) ?? null;
|
|
4464
|
+
}
|
|
4465
|
+
const matches = ctx.graph.nodes.filter(
|
|
4466
|
+
(n) => n.kind === "symbol" && n.name === a
|
|
4467
|
+
);
|
|
4468
|
+
return matches.length === 1 ? matches[0] : null;
|
|
4469
|
+
}
|
|
4470
|
+
function callPath(args, ctx) {
|
|
4471
|
+
const fromArg = typeof args?.from === "string" ? args.from : "";
|
|
4472
|
+
const toArg = typeof args?.to === "string" ? args.to : "";
|
|
4473
|
+
const maxDepth = typeof args?.depth === "number" && args.depth > 0 ? Math.floor(args.depth) : 6;
|
|
4474
|
+
if (!fromArg.trim() || !toArg.trim()) {
|
|
4475
|
+
return errorContent("call_path: 'from' and 'to' (strings) are required");
|
|
4476
|
+
}
|
|
4477
|
+
const from = resolveSymbolArg(ctx, fromArg);
|
|
4478
|
+
if (!from) {
|
|
4479
|
+
return errorContent(
|
|
4480
|
+
`call_path: could not resolve 'from': ${fromArg} (use file::symbol if the name is ambiguous)`
|
|
4481
|
+
);
|
|
4482
|
+
}
|
|
4483
|
+
const to = resolveSymbolArg(ctx, toArg);
|
|
4484
|
+
if (!to) {
|
|
4485
|
+
return errorContent(
|
|
4486
|
+
`call_path: could not resolve 'to': ${toArg} (use file::symbol if the name is ambiguous)`
|
|
4487
|
+
);
|
|
4488
|
+
}
|
|
4489
|
+
if (from.id === to.id) {
|
|
4490
|
+
return textContent(`# call_path
|
|
4491
|
+
|
|
4492
|
+
\`${from.name}\` and \`${to.name}\` are the same symbol.`);
|
|
4493
|
+
}
|
|
4494
|
+
const calleesBy = /* @__PURE__ */ new Map();
|
|
4495
|
+
for (const e of ctx.graph.edges) {
|
|
4496
|
+
if (e.kind !== "calls" || e.from === e.to) continue;
|
|
4497
|
+
(calleesBy.get(e.from) ?? calleesBy.set(e.from, []).get(e.from)).push(e.to);
|
|
4498
|
+
}
|
|
4499
|
+
const symById = /* @__PURE__ */ new Map();
|
|
4500
|
+
for (const n of ctx.graph.nodes) if (n.kind === "symbol") symById.set(n.id, n);
|
|
4501
|
+
const prevOf = /* @__PURE__ */ new Map();
|
|
4502
|
+
const visited = /* @__PURE__ */ new Set([from.id]);
|
|
4503
|
+
let frontier = [from.id];
|
|
4504
|
+
let found = false;
|
|
4505
|
+
for (let d = 0; d < maxDepth && !found && frontier.length > 0; d++) {
|
|
4506
|
+
const next = [];
|
|
4507
|
+
for (const cur2 of frontier) {
|
|
4508
|
+
for (const nb of calleesBy.get(cur2) ?? []) {
|
|
4509
|
+
if (visited.has(nb)) continue;
|
|
4510
|
+
visited.add(nb);
|
|
4511
|
+
prevOf.set(nb, cur2);
|
|
4512
|
+
if (nb === to.id) {
|
|
4513
|
+
found = true;
|
|
4514
|
+
break;
|
|
4515
|
+
}
|
|
4516
|
+
next.push(nb);
|
|
4517
|
+
}
|
|
4518
|
+
if (found) break;
|
|
4519
|
+
}
|
|
4520
|
+
frontier = next;
|
|
4521
|
+
}
|
|
4522
|
+
if (!found) {
|
|
4523
|
+
return textContent(
|
|
4524
|
+
`# call_path: ${from.name} \u2192 ${to.name}
|
|
4525
|
+
|
|
4526
|
+
_(no call path found within depth ${maxDepth})_`
|
|
4527
|
+
);
|
|
4528
|
+
}
|
|
4529
|
+
const chain = [];
|
|
4530
|
+
let cur = to.id;
|
|
4531
|
+
while (cur !== void 0) {
|
|
4532
|
+
chain.unshift(cur);
|
|
4533
|
+
if (cur === from.id) break;
|
|
4534
|
+
cur = prevOf.get(cur);
|
|
4535
|
+
}
|
|
4536
|
+
const syms = chain.map((id) => symById.get(id)).filter((s) => !!s);
|
|
4537
|
+
const hops = syms.length - 1;
|
|
4538
|
+
const rendered = syms.map((s) => `\`${s.name}\` (${s.file}:${s.start_line})`).join("\n \u2192 ");
|
|
4539
|
+
return textContent(
|
|
4540
|
+
`# call_path: ${from.name} \u2192 ${to.name} (${hops} hop${hops === 1 ? "" : "s"})
|
|
4541
|
+
|
|
4542
|
+
${rendered}`
|
|
4543
|
+
);
|
|
4544
|
+
}
|
|
4429
4545
|
var LIKELY_ENTRY_PATTERNS = [
|
|
4430
4546
|
/(?:^|\/)main\.[a-z0-9_]+$/i,
|
|
4431
4547
|
/(?:^|\/)index\.[a-z0-9_]+$/i,
|
|
@@ -4972,11 +5088,51 @@ async function getCommitsSince(projectRoot, sinceIso) {
|
|
|
4972
5088
|
return [];
|
|
4973
5089
|
}
|
|
4974
5090
|
}
|
|
5091
|
+
async function getHeadSha(projectRoot) {
|
|
5092
|
+
try {
|
|
5093
|
+
const { stdout } = await execFileAsync3("git", ["rev-parse", "HEAD"], { cwd: projectRoot });
|
|
5094
|
+
return stdout.trim();
|
|
5095
|
+
} catch {
|
|
5096
|
+
return "";
|
|
5097
|
+
}
|
|
5098
|
+
}
|
|
5099
|
+
function parseDiffHunks(stdout) {
|
|
5100
|
+
const out = /* @__PURE__ */ new Map();
|
|
5101
|
+
let current = null;
|
|
5102
|
+
for (const line of stdout.split("\n")) {
|
|
5103
|
+
if (line.startsWith("+++ ")) {
|
|
5104
|
+
const p = line.slice(4).trim();
|
|
5105
|
+
current = p === "/dev/null" ? null : p.replace(/^b\//, "");
|
|
5106
|
+
} else if (current && line.startsWith("@@")) {
|
|
5107
|
+
const m = /\+(\d+)(?:,(\d+))?/.exec(line);
|
|
5108
|
+
if (!m) continue;
|
|
5109
|
+
const start = Number(m[1]);
|
|
5110
|
+
const count = m[2] === void 0 ? 1 : Number(m[2]);
|
|
5111
|
+
const end = count === 0 ? start : start + count - 1;
|
|
5112
|
+
const list = out.get(current) ?? [];
|
|
5113
|
+
list.push([start, end]);
|
|
5114
|
+
out.set(current, list);
|
|
5115
|
+
}
|
|
5116
|
+
}
|
|
5117
|
+
return out;
|
|
5118
|
+
}
|
|
5119
|
+
async function getChangedLineRanges(projectRoot, sinceRef) {
|
|
5120
|
+
if (!sinceRef) return /* @__PURE__ */ new Map();
|
|
5121
|
+
try {
|
|
5122
|
+
const { stdout } = await execFileAsync3("git", ["diff", "-U0", "--no-color", sinceRef, "--"], {
|
|
5123
|
+
cwd: projectRoot,
|
|
5124
|
+
maxBuffer: 16 * 1024 * 1024
|
|
5125
|
+
});
|
|
5126
|
+
return parseDiffHunks(stdout);
|
|
5127
|
+
} catch {
|
|
5128
|
+
return /* @__PURE__ */ new Map();
|
|
5129
|
+
}
|
|
5130
|
+
}
|
|
4975
5131
|
|
|
4976
5132
|
// src/memory/session.ts
|
|
4977
5133
|
import { mkdir as mkdir11, readFile as readFile17, writeFile as writeFile10 } from "fs/promises";
|
|
4978
5134
|
import { dirname as dirname13 } from "path";
|
|
4979
|
-
var SESSION_SCHEMA_VERSION =
|
|
5135
|
+
var SESSION_SCHEMA_VERSION = 2;
|
|
4980
5136
|
async function readSession(path) {
|
|
4981
5137
|
try {
|
|
4982
5138
|
const raw = await readFile17(path, "utf8");
|
|
@@ -5005,6 +5161,7 @@ async function captureSnapshot(ctx, branchOverride) {
|
|
|
5005
5161
|
for (const p of ctx.activity.recentFilePaths(TOUCHED_WINDOW_MS)) touched.add(p);
|
|
5006
5162
|
const prev = await readSession(ctx.paths.sessionState);
|
|
5007
5163
|
const recentCommits = await getCommitsSince(ctx.paths.projectRoot, prev?.endedAt ?? "");
|
|
5164
|
+
const headSha = await getHeadSha(ctx.paths.projectRoot);
|
|
5008
5165
|
const snapshot = {
|
|
5009
5166
|
schema_version: SESSION_SCHEMA_VERSION,
|
|
5010
5167
|
endedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -5015,7 +5172,8 @@ async function captureSnapshot(ctx, branchOverride) {
|
|
|
5015
5172
|
tasks: tasks.entries.map((e) => e.content),
|
|
5016
5173
|
decisions: decisions.entries.map((e) => e.content),
|
|
5017
5174
|
next: next.entries.map((e) => e.content)
|
|
5018
|
-
}
|
|
5175
|
+
},
|
|
5176
|
+
headSha
|
|
5019
5177
|
};
|
|
5020
5178
|
await writeSession(ctx.paths.sessionState, snapshot);
|
|
5021
5179
|
}
|
|
@@ -5463,6 +5621,34 @@ var RESUME_PRIMER_MAX_CHARS = 2720;
|
|
|
5463
5621
|
var MAX_FILES = 15;
|
|
5464
5622
|
var MAX_COMMITS2 = 5;
|
|
5465
5623
|
var MAX_BULLETS2 = 3;
|
|
5624
|
+
var MAX_CHANGED_SYMBOLS = 20;
|
|
5625
|
+
var CHANGED_SIG_MAX = 100;
|
|
5626
|
+
function changedSymbols(ranges, graph) {
|
|
5627
|
+
if (ranges.size === 0) return [];
|
|
5628
|
+
const hits = [];
|
|
5629
|
+
for (const n of graph.nodes) {
|
|
5630
|
+
if (n.kind !== "symbol") continue;
|
|
5631
|
+
const rs = ranges.get(n.file);
|
|
5632
|
+
if (!rs) continue;
|
|
5633
|
+
if (rs.some(([a, b]) => a <= n.end_line && b >= n.start_line)) hits.push(n);
|
|
5634
|
+
}
|
|
5635
|
+
hits.sort((a, b) => a.file === b.file ? a.start_line - b.start_line : a.file < b.file ? -1 : 1);
|
|
5636
|
+
return hits;
|
|
5637
|
+
}
|
|
5638
|
+
function changedSymbolsSection(ranges, graph) {
|
|
5639
|
+
const hits = changedSymbols(ranges, graph);
|
|
5640
|
+
if (hits.length === 0) return [];
|
|
5641
|
+
const shown = hits.slice(0, MAX_CHANGED_SYMBOLS);
|
|
5642
|
+
const lines = ["", "### Changed symbols (since last session)"];
|
|
5643
|
+
for (const s of shown) {
|
|
5644
|
+
lines.push(
|
|
5645
|
+
`- \`${s.file}::${s.name}\` (${s.symbol_kind}) \u2014 ${s.signature.trim().slice(0, CHANGED_SIG_MAX)}`
|
|
5646
|
+
);
|
|
5647
|
+
}
|
|
5648
|
+
const more = hits.length - shown.length;
|
|
5649
|
+
if (more > 0) lines.push(`_(+${more} more)_`);
|
|
5650
|
+
return lines;
|
|
5651
|
+
}
|
|
5466
5652
|
function legacyPrimer(ctx) {
|
|
5467
5653
|
const g = ctx.graph;
|
|
5468
5654
|
return `Synthra context loaded for ${g.root}.
|
|
@@ -5473,7 +5659,7 @@ function hasContent(snap) {
|
|
|
5473
5659
|
snap.recentCommits.length || snap.filesTouched.length || snap.summary.tasks.length || snap.summary.next.length || snap.summary.decisions.length
|
|
5474
5660
|
);
|
|
5475
5661
|
}
|
|
5476
|
-
function buildResumeDigest(snap, branchNow) {
|
|
5662
|
+
function buildResumeDigest(snap, branchNow, changedSymbolLines = []) {
|
|
5477
5663
|
const plural = (n) => n === 1 ? "" : "s";
|
|
5478
5664
|
const head = `## Since you were last here \u2014 ${snap.branch} (${snap.recentCommits.length} commit${plural(snap.recentCommits.length)}, ${snap.filesTouched.length} file${plural(snap.filesTouched.length)} touched)`;
|
|
5479
5665
|
const essential = [head];
|
|
@@ -5494,7 +5680,7 @@ function buildResumeDigest(snap, branchNow) {
|
|
|
5494
5680
|
essential.push("", "### Recent decisions");
|
|
5495
5681
|
for (const d of snap.summary.decisions.slice(0, MAX_BULLETS2)) essential.push(`- ${d}`);
|
|
5496
5682
|
}
|
|
5497
|
-
const extra = [];
|
|
5683
|
+
const extra = [...changedSymbolLines];
|
|
5498
5684
|
if (snap.recentCommits.length) {
|
|
5499
5685
|
extra.push("", "### Recent commits");
|
|
5500
5686
|
for (const c of snap.recentCommits.slice(0, MAX_COMMITS2)) {
|
|
@@ -5521,7 +5707,12 @@ async function handlePrime(ctx, port) {
|
|
|
5521
5707
|
return { primer: legacy, port };
|
|
5522
5708
|
}
|
|
5523
5709
|
const branchNow = await currentBranch(ctx.paths.projectRoot);
|
|
5524
|
-
|
|
5710
|
+
let changedSymbolLines = [];
|
|
5711
|
+
if (snap.headSha) {
|
|
5712
|
+
const ranges = await getChangedLineRanges(ctx.paths.projectRoot, snap.headSha);
|
|
5713
|
+
changedSymbolLines = changedSymbolsSection(ranges, ctx.graph);
|
|
5714
|
+
}
|
|
5715
|
+
const digest = buildResumeDigest(snap, branchNow, changedSymbolLines);
|
|
5525
5716
|
return { primer: `${digest}
|
|
5526
5717
|
|
|
5527
5718
|
---
|