@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/dist/server/index.js
CHANGED
|
@@ -1649,7 +1649,17 @@ var DEFAULT_IGNORE = [
|
|
|
1649
1649
|
".mypy_cache/",
|
|
1650
1650
|
".ruff_cache/",
|
|
1651
1651
|
// .NET
|
|
1652
|
-
"obj/"
|
|
1652
|
+
"obj/",
|
|
1653
|
+
// Generated / minified bundles — no readable symbols, so indexing them only
|
|
1654
|
+
// pollutes retrieval (a markup query like `nav|menu|toggle` spuriously matches
|
|
1655
|
+
// a symbol inside vendored plugin JS → a useless Moat block) and bloats the
|
|
1656
|
+
// graph. Committed bootstrap/swiper-style plugin JS is the common offender.
|
|
1657
|
+
"*.min.js",
|
|
1658
|
+
"*.min.cjs",
|
|
1659
|
+
"*.min.mjs",
|
|
1660
|
+
"*.min.css",
|
|
1661
|
+
"*.bundle.js",
|
|
1662
|
+
"*-min.js"
|
|
1653
1663
|
];
|
|
1654
1664
|
var BINARY_EXTS = /* @__PURE__ */ new Set([
|
|
1655
1665
|
".png",
|
|
@@ -3153,6 +3163,19 @@ var TOOLS = [
|
|
|
3153
3163
|
limit: { type: "number", description: "Cap on returned names. Default 30." }
|
|
3154
3164
|
}
|
|
3155
3165
|
}
|
|
3166
|
+
},
|
|
3167
|
+
{
|
|
3168
|
+
name: "call_path",
|
|
3169
|
+
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.",
|
|
3170
|
+
inputSchema: {
|
|
3171
|
+
type: "object",
|
|
3172
|
+
properties: {
|
|
3173
|
+
from: { type: "string", description: "Starting symbol ('file::symbol' or unique name)." },
|
|
3174
|
+
to: { type: "string", description: "Target symbol ('file::symbol' or unique name)." },
|
|
3175
|
+
depth: { type: "number", description: "Max call hops to search. Default 6." }
|
|
3176
|
+
},
|
|
3177
|
+
required: ["from", "to"]
|
|
3178
|
+
}
|
|
3156
3179
|
}
|
|
3157
3180
|
];
|
|
3158
3181
|
async function callTool(name, args, ctx) {
|
|
@@ -3179,6 +3202,8 @@ async function callTool(name, args, ctx) {
|
|
|
3179
3202
|
return findSymbol(args, ctx);
|
|
3180
3203
|
case "duplicate_symbols":
|
|
3181
3204
|
return duplicateSymbols(args, ctx);
|
|
3205
|
+
case "call_path":
|
|
3206
|
+
return callPath(args, ctx);
|
|
3182
3207
|
default:
|
|
3183
3208
|
return errorContent(`Unknown tool: ${name}`);
|
|
3184
3209
|
}
|
|
@@ -3335,6 +3360,97 @@ function testsCoveringLine(graph, filePaths) {
|
|
|
3335
3360
|
const omitted = tests.length - shown.length;
|
|
3336
3361
|
return `Tests covering the impact: ${shown.join(" \xB7 ")}${omitted > 0 ? ` \u2026+${omitted} more` : ""}`;
|
|
3337
3362
|
}
|
|
3363
|
+
function resolveSymbolArg(ctx, arg) {
|
|
3364
|
+
const a = arg.trim();
|
|
3365
|
+
if (a.includes("::")) {
|
|
3366
|
+
const [rawFile, rawSym] = a.split("::", 2);
|
|
3367
|
+
const resolved = resolveFileTarget(ctx.graph, (rawFile ?? "").trim());
|
|
3368
|
+
if (!("node" in resolved)) return null;
|
|
3369
|
+
const name = (rawSym ?? "").trim();
|
|
3370
|
+
return ctx.graph.nodes.find(
|
|
3371
|
+
(n) => n.kind === "symbol" && n.file === resolved.node.path && n.name === name
|
|
3372
|
+
) ?? null;
|
|
3373
|
+
}
|
|
3374
|
+
const matches = ctx.graph.nodes.filter(
|
|
3375
|
+
(n) => n.kind === "symbol" && n.name === a
|
|
3376
|
+
);
|
|
3377
|
+
return matches.length === 1 ? matches[0] : null;
|
|
3378
|
+
}
|
|
3379
|
+
function callPath(args, ctx) {
|
|
3380
|
+
const fromArg = typeof args?.from === "string" ? args.from : "";
|
|
3381
|
+
const toArg = typeof args?.to === "string" ? args.to : "";
|
|
3382
|
+
const maxDepth = typeof args?.depth === "number" && args.depth > 0 ? Math.floor(args.depth) : 6;
|
|
3383
|
+
if (!fromArg.trim() || !toArg.trim()) {
|
|
3384
|
+
return errorContent("call_path: 'from' and 'to' (strings) are required");
|
|
3385
|
+
}
|
|
3386
|
+
const from = resolveSymbolArg(ctx, fromArg);
|
|
3387
|
+
if (!from) {
|
|
3388
|
+
return errorContent(
|
|
3389
|
+
`call_path: could not resolve 'from': ${fromArg} (use file::symbol if the name is ambiguous)`
|
|
3390
|
+
);
|
|
3391
|
+
}
|
|
3392
|
+
const to = resolveSymbolArg(ctx, toArg);
|
|
3393
|
+
if (!to) {
|
|
3394
|
+
return errorContent(
|
|
3395
|
+
`call_path: could not resolve 'to': ${toArg} (use file::symbol if the name is ambiguous)`
|
|
3396
|
+
);
|
|
3397
|
+
}
|
|
3398
|
+
if (from.id === to.id) {
|
|
3399
|
+
return textContent(`# call_path
|
|
3400
|
+
|
|
3401
|
+
\`${from.name}\` and \`${to.name}\` are the same symbol.`);
|
|
3402
|
+
}
|
|
3403
|
+
const calleesBy = /* @__PURE__ */ new Map();
|
|
3404
|
+
for (const e of ctx.graph.edges) {
|
|
3405
|
+
if (e.kind !== "calls" || e.from === e.to) continue;
|
|
3406
|
+
(calleesBy.get(e.from) ?? calleesBy.set(e.from, []).get(e.from)).push(e.to);
|
|
3407
|
+
}
|
|
3408
|
+
const symById = /* @__PURE__ */ new Map();
|
|
3409
|
+
for (const n of ctx.graph.nodes) if (n.kind === "symbol") symById.set(n.id, n);
|
|
3410
|
+
const prevOf = /* @__PURE__ */ new Map();
|
|
3411
|
+
const visited = /* @__PURE__ */ new Set([from.id]);
|
|
3412
|
+
let frontier = [from.id];
|
|
3413
|
+
let found = false;
|
|
3414
|
+
for (let d = 0; d < maxDepth && !found && frontier.length > 0; d++) {
|
|
3415
|
+
const next = [];
|
|
3416
|
+
for (const cur2 of frontier) {
|
|
3417
|
+
for (const nb of calleesBy.get(cur2) ?? []) {
|
|
3418
|
+
if (visited.has(nb)) continue;
|
|
3419
|
+
visited.add(nb);
|
|
3420
|
+
prevOf.set(nb, cur2);
|
|
3421
|
+
if (nb === to.id) {
|
|
3422
|
+
found = true;
|
|
3423
|
+
break;
|
|
3424
|
+
}
|
|
3425
|
+
next.push(nb);
|
|
3426
|
+
}
|
|
3427
|
+
if (found) break;
|
|
3428
|
+
}
|
|
3429
|
+
frontier = next;
|
|
3430
|
+
}
|
|
3431
|
+
if (!found) {
|
|
3432
|
+
return textContent(
|
|
3433
|
+
`# call_path: ${from.name} \u2192 ${to.name}
|
|
3434
|
+
|
|
3435
|
+
_(no call path found within depth ${maxDepth})_`
|
|
3436
|
+
);
|
|
3437
|
+
}
|
|
3438
|
+
const chain = [];
|
|
3439
|
+
let cur = to.id;
|
|
3440
|
+
while (cur !== void 0) {
|
|
3441
|
+
chain.unshift(cur);
|
|
3442
|
+
if (cur === from.id) break;
|
|
3443
|
+
cur = prevOf.get(cur);
|
|
3444
|
+
}
|
|
3445
|
+
const syms = chain.map((id) => symById.get(id)).filter((s) => !!s);
|
|
3446
|
+
const hops = syms.length - 1;
|
|
3447
|
+
const rendered = syms.map((s) => `\`${s.name}\` (${s.file}:${s.start_line})`).join("\n \u2192 ");
|
|
3448
|
+
return textContent(
|
|
3449
|
+
`# call_path: ${from.name} \u2192 ${to.name} (${hops} hop${hops === 1 ? "" : "s"})
|
|
3450
|
+
|
|
3451
|
+
${rendered}`
|
|
3452
|
+
);
|
|
3453
|
+
}
|
|
3338
3454
|
var LIKELY_ENTRY_PATTERNS = [
|
|
3339
3455
|
/(?:^|\/)main\.[a-z0-9_]+$/i,
|
|
3340
3456
|
/(?:^|\/)index\.[a-z0-9_]+$/i,
|
|
@@ -3900,11 +4016,51 @@ async function getCommitsSince(projectRoot, sinceIso) {
|
|
|
3900
4016
|
return [];
|
|
3901
4017
|
}
|
|
3902
4018
|
}
|
|
4019
|
+
async function getHeadSha(projectRoot) {
|
|
4020
|
+
try {
|
|
4021
|
+
const { stdout } = await execFileAsync3("git", ["rev-parse", "HEAD"], { cwd: projectRoot });
|
|
4022
|
+
return stdout.trim();
|
|
4023
|
+
} catch {
|
|
4024
|
+
return "";
|
|
4025
|
+
}
|
|
4026
|
+
}
|
|
4027
|
+
function parseDiffHunks(stdout) {
|
|
4028
|
+
const out = /* @__PURE__ */ new Map();
|
|
4029
|
+
let current = null;
|
|
4030
|
+
for (const line of stdout.split("\n")) {
|
|
4031
|
+
if (line.startsWith("+++ ")) {
|
|
4032
|
+
const p = line.slice(4).trim();
|
|
4033
|
+
current = p === "/dev/null" ? null : p.replace(/^b\//, "");
|
|
4034
|
+
} else if (current && line.startsWith("@@")) {
|
|
4035
|
+
const m = /\+(\d+)(?:,(\d+))?/.exec(line);
|
|
4036
|
+
if (!m) continue;
|
|
4037
|
+
const start = Number(m[1]);
|
|
4038
|
+
const count = m[2] === void 0 ? 1 : Number(m[2]);
|
|
4039
|
+
const end = count === 0 ? start : start + count - 1;
|
|
4040
|
+
const list = out.get(current) ?? [];
|
|
4041
|
+
list.push([start, end]);
|
|
4042
|
+
out.set(current, list);
|
|
4043
|
+
}
|
|
4044
|
+
}
|
|
4045
|
+
return out;
|
|
4046
|
+
}
|
|
4047
|
+
async function getChangedLineRanges(projectRoot, sinceRef) {
|
|
4048
|
+
if (!sinceRef) return /* @__PURE__ */ new Map();
|
|
4049
|
+
try {
|
|
4050
|
+
const { stdout } = await execFileAsync3("git", ["diff", "-U0", "--no-color", sinceRef, "--"], {
|
|
4051
|
+
cwd: projectRoot,
|
|
4052
|
+
maxBuffer: 16 * 1024 * 1024
|
|
4053
|
+
});
|
|
4054
|
+
return parseDiffHunks(stdout);
|
|
4055
|
+
} catch {
|
|
4056
|
+
return /* @__PURE__ */ new Map();
|
|
4057
|
+
}
|
|
4058
|
+
}
|
|
3903
4059
|
|
|
3904
4060
|
// src/memory/session.ts
|
|
3905
4061
|
import { mkdir as mkdir9, readFile as readFile13, writeFile as writeFile8 } from "fs/promises";
|
|
3906
4062
|
import { dirname as dirname10 } from "path";
|
|
3907
|
-
var SESSION_SCHEMA_VERSION =
|
|
4063
|
+
var SESSION_SCHEMA_VERSION = 2;
|
|
3908
4064
|
async function readSession(path) {
|
|
3909
4065
|
try {
|
|
3910
4066
|
const raw = await readFile13(path, "utf8");
|
|
@@ -3933,6 +4089,7 @@ async function captureSnapshot(ctx, branchOverride) {
|
|
|
3933
4089
|
for (const p of ctx.activity.recentFilePaths(TOUCHED_WINDOW_MS)) touched.add(p);
|
|
3934
4090
|
const prev = await readSession(ctx.paths.sessionState);
|
|
3935
4091
|
const recentCommits = await getCommitsSince(ctx.paths.projectRoot, prev?.endedAt ?? "");
|
|
4092
|
+
const headSha = await getHeadSha(ctx.paths.projectRoot);
|
|
3936
4093
|
const snapshot = {
|
|
3937
4094
|
schema_version: SESSION_SCHEMA_VERSION,
|
|
3938
4095
|
endedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -3943,7 +4100,8 @@ async function captureSnapshot(ctx, branchOverride) {
|
|
|
3943
4100
|
tasks: tasks.entries.map((e) => e.content),
|
|
3944
4101
|
decisions: decisions.entries.map((e) => e.content),
|
|
3945
4102
|
next: next.entries.map((e) => e.content)
|
|
3946
|
-
}
|
|
4103
|
+
},
|
|
4104
|
+
headSha
|
|
3947
4105
|
};
|
|
3948
4106
|
await writeSession(ctx.paths.sessionState, snapshot);
|
|
3949
4107
|
}
|
|
@@ -4391,6 +4549,34 @@ var RESUME_PRIMER_MAX_CHARS = 2720;
|
|
|
4391
4549
|
var MAX_FILES = 15;
|
|
4392
4550
|
var MAX_COMMITS2 = 5;
|
|
4393
4551
|
var MAX_BULLETS2 = 3;
|
|
4552
|
+
var MAX_CHANGED_SYMBOLS = 20;
|
|
4553
|
+
var CHANGED_SIG_MAX = 100;
|
|
4554
|
+
function changedSymbols(ranges, graph) {
|
|
4555
|
+
if (ranges.size === 0) return [];
|
|
4556
|
+
const hits = [];
|
|
4557
|
+
for (const n of graph.nodes) {
|
|
4558
|
+
if (n.kind !== "symbol") continue;
|
|
4559
|
+
const rs = ranges.get(n.file);
|
|
4560
|
+
if (!rs) continue;
|
|
4561
|
+
if (rs.some(([a, b]) => a <= n.end_line && b >= n.start_line)) hits.push(n);
|
|
4562
|
+
}
|
|
4563
|
+
hits.sort((a, b) => a.file === b.file ? a.start_line - b.start_line : a.file < b.file ? -1 : 1);
|
|
4564
|
+
return hits;
|
|
4565
|
+
}
|
|
4566
|
+
function changedSymbolsSection(ranges, graph) {
|
|
4567
|
+
const hits = changedSymbols(ranges, graph);
|
|
4568
|
+
if (hits.length === 0) return [];
|
|
4569
|
+
const shown = hits.slice(0, MAX_CHANGED_SYMBOLS);
|
|
4570
|
+
const lines = ["", "### Changed symbols (since last session)"];
|
|
4571
|
+
for (const s of shown) {
|
|
4572
|
+
lines.push(
|
|
4573
|
+
`- \`${s.file}::${s.name}\` (${s.symbol_kind}) \u2014 ${s.signature.trim().slice(0, CHANGED_SIG_MAX)}`
|
|
4574
|
+
);
|
|
4575
|
+
}
|
|
4576
|
+
const more = hits.length - shown.length;
|
|
4577
|
+
if (more > 0) lines.push(`_(+${more} more)_`);
|
|
4578
|
+
return lines;
|
|
4579
|
+
}
|
|
4394
4580
|
function legacyPrimer(ctx) {
|
|
4395
4581
|
const g = ctx.graph;
|
|
4396
4582
|
return `Synthra context loaded for ${g.root}.
|
|
@@ -4401,7 +4587,7 @@ function hasContent(snap) {
|
|
|
4401
4587
|
snap.recentCommits.length || snap.filesTouched.length || snap.summary.tasks.length || snap.summary.next.length || snap.summary.decisions.length
|
|
4402
4588
|
);
|
|
4403
4589
|
}
|
|
4404
|
-
function buildResumeDigest(snap, branchNow) {
|
|
4590
|
+
function buildResumeDigest(snap, branchNow, changedSymbolLines = []) {
|
|
4405
4591
|
const plural = (n) => n === 1 ? "" : "s";
|
|
4406
4592
|
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)`;
|
|
4407
4593
|
const essential = [head];
|
|
@@ -4422,7 +4608,7 @@ function buildResumeDigest(snap, branchNow) {
|
|
|
4422
4608
|
essential.push("", "### Recent decisions");
|
|
4423
4609
|
for (const d of snap.summary.decisions.slice(0, MAX_BULLETS2)) essential.push(`- ${d}`);
|
|
4424
4610
|
}
|
|
4425
|
-
const extra = [];
|
|
4611
|
+
const extra = [...changedSymbolLines];
|
|
4426
4612
|
if (snap.recentCommits.length) {
|
|
4427
4613
|
extra.push("", "### Recent commits");
|
|
4428
4614
|
for (const c of snap.recentCommits.slice(0, MAX_COMMITS2)) {
|
|
@@ -4449,7 +4635,12 @@ async function handlePrime(ctx, port) {
|
|
|
4449
4635
|
return { primer: legacy, port };
|
|
4450
4636
|
}
|
|
4451
4637
|
const branchNow = await currentBranch(ctx.paths.projectRoot);
|
|
4452
|
-
|
|
4638
|
+
let changedSymbolLines = [];
|
|
4639
|
+
if (snap.headSha) {
|
|
4640
|
+
const ranges = await getChangedLineRanges(ctx.paths.projectRoot, snap.headSha);
|
|
4641
|
+
changedSymbolLines = changedSymbolsSection(ranges, ctx.graph);
|
|
4642
|
+
}
|
|
4643
|
+
const digest = buildResumeDigest(snap, branchNow, changedSymbolLines);
|
|
4453
4644
|
return { primer: `${digest}
|
|
4454
4645
|
|
|
4455
4646
|
---
|