@jefuriiij/synthra 0.12.0 → 0.13.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 +20 -0
- package/dist/cli/index.js +187 -6
- 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 +186 -5
- package/dist/server/index.js.map +1 -1
- package/package.json +1 -1
package/dist/server/index.js
CHANGED
|
@@ -3153,6 +3153,19 @@ var TOOLS = [
|
|
|
3153
3153
|
limit: { type: "number", description: "Cap on returned names. Default 30." }
|
|
3154
3154
|
}
|
|
3155
3155
|
}
|
|
3156
|
+
},
|
|
3157
|
+
{
|
|
3158
|
+
name: "call_path",
|
|
3159
|
+
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.",
|
|
3160
|
+
inputSchema: {
|
|
3161
|
+
type: "object",
|
|
3162
|
+
properties: {
|
|
3163
|
+
from: { type: "string", description: "Starting symbol ('file::symbol' or unique name)." },
|
|
3164
|
+
to: { type: "string", description: "Target symbol ('file::symbol' or unique name)." },
|
|
3165
|
+
depth: { type: "number", description: "Max call hops to search. Default 6." }
|
|
3166
|
+
},
|
|
3167
|
+
required: ["from", "to"]
|
|
3168
|
+
}
|
|
3156
3169
|
}
|
|
3157
3170
|
];
|
|
3158
3171
|
async function callTool(name, args, ctx) {
|
|
@@ -3179,6 +3192,8 @@ async function callTool(name, args, ctx) {
|
|
|
3179
3192
|
return findSymbol(args, ctx);
|
|
3180
3193
|
case "duplicate_symbols":
|
|
3181
3194
|
return duplicateSymbols(args, ctx);
|
|
3195
|
+
case "call_path":
|
|
3196
|
+
return callPath(args, ctx);
|
|
3182
3197
|
default:
|
|
3183
3198
|
return errorContent(`Unknown tool: ${name}`);
|
|
3184
3199
|
}
|
|
@@ -3335,6 +3350,97 @@ function testsCoveringLine(graph, filePaths) {
|
|
|
3335
3350
|
const omitted = tests.length - shown.length;
|
|
3336
3351
|
return `Tests covering the impact: ${shown.join(" \xB7 ")}${omitted > 0 ? ` \u2026+${omitted} more` : ""}`;
|
|
3337
3352
|
}
|
|
3353
|
+
function resolveSymbolArg(ctx, arg) {
|
|
3354
|
+
const a = arg.trim();
|
|
3355
|
+
if (a.includes("::")) {
|
|
3356
|
+
const [rawFile, rawSym] = a.split("::", 2);
|
|
3357
|
+
const resolved = resolveFileTarget(ctx.graph, (rawFile ?? "").trim());
|
|
3358
|
+
if (!("node" in resolved)) return null;
|
|
3359
|
+
const name = (rawSym ?? "").trim();
|
|
3360
|
+
return ctx.graph.nodes.find(
|
|
3361
|
+
(n) => n.kind === "symbol" && n.file === resolved.node.path && n.name === name
|
|
3362
|
+
) ?? null;
|
|
3363
|
+
}
|
|
3364
|
+
const matches = ctx.graph.nodes.filter(
|
|
3365
|
+
(n) => n.kind === "symbol" && n.name === a
|
|
3366
|
+
);
|
|
3367
|
+
return matches.length === 1 ? matches[0] : null;
|
|
3368
|
+
}
|
|
3369
|
+
function callPath(args, ctx) {
|
|
3370
|
+
const fromArg = typeof args?.from === "string" ? args.from : "";
|
|
3371
|
+
const toArg = typeof args?.to === "string" ? args.to : "";
|
|
3372
|
+
const maxDepth = typeof args?.depth === "number" && args.depth > 0 ? Math.floor(args.depth) : 6;
|
|
3373
|
+
if (!fromArg.trim() || !toArg.trim()) {
|
|
3374
|
+
return errorContent("call_path: 'from' and 'to' (strings) are required");
|
|
3375
|
+
}
|
|
3376
|
+
const from = resolveSymbolArg(ctx, fromArg);
|
|
3377
|
+
if (!from) {
|
|
3378
|
+
return errorContent(
|
|
3379
|
+
`call_path: could not resolve 'from': ${fromArg} (use file::symbol if the name is ambiguous)`
|
|
3380
|
+
);
|
|
3381
|
+
}
|
|
3382
|
+
const to = resolveSymbolArg(ctx, toArg);
|
|
3383
|
+
if (!to) {
|
|
3384
|
+
return errorContent(
|
|
3385
|
+
`call_path: could not resolve 'to': ${toArg} (use file::symbol if the name is ambiguous)`
|
|
3386
|
+
);
|
|
3387
|
+
}
|
|
3388
|
+
if (from.id === to.id) {
|
|
3389
|
+
return textContent(`# call_path
|
|
3390
|
+
|
|
3391
|
+
\`${from.name}\` and \`${to.name}\` are the same symbol.`);
|
|
3392
|
+
}
|
|
3393
|
+
const calleesBy = /* @__PURE__ */ new Map();
|
|
3394
|
+
for (const e of ctx.graph.edges) {
|
|
3395
|
+
if (e.kind !== "calls" || e.from === e.to) continue;
|
|
3396
|
+
(calleesBy.get(e.from) ?? calleesBy.set(e.from, []).get(e.from)).push(e.to);
|
|
3397
|
+
}
|
|
3398
|
+
const symById = /* @__PURE__ */ new Map();
|
|
3399
|
+
for (const n of ctx.graph.nodes) if (n.kind === "symbol") symById.set(n.id, n);
|
|
3400
|
+
const prevOf = /* @__PURE__ */ new Map();
|
|
3401
|
+
const visited = /* @__PURE__ */ new Set([from.id]);
|
|
3402
|
+
let frontier = [from.id];
|
|
3403
|
+
let found = false;
|
|
3404
|
+
for (let d = 0; d < maxDepth && !found && frontier.length > 0; d++) {
|
|
3405
|
+
const next = [];
|
|
3406
|
+
for (const cur2 of frontier) {
|
|
3407
|
+
for (const nb of calleesBy.get(cur2) ?? []) {
|
|
3408
|
+
if (visited.has(nb)) continue;
|
|
3409
|
+
visited.add(nb);
|
|
3410
|
+
prevOf.set(nb, cur2);
|
|
3411
|
+
if (nb === to.id) {
|
|
3412
|
+
found = true;
|
|
3413
|
+
break;
|
|
3414
|
+
}
|
|
3415
|
+
next.push(nb);
|
|
3416
|
+
}
|
|
3417
|
+
if (found) break;
|
|
3418
|
+
}
|
|
3419
|
+
frontier = next;
|
|
3420
|
+
}
|
|
3421
|
+
if (!found) {
|
|
3422
|
+
return textContent(
|
|
3423
|
+
`# call_path: ${from.name} \u2192 ${to.name}
|
|
3424
|
+
|
|
3425
|
+
_(no call path found within depth ${maxDepth})_`
|
|
3426
|
+
);
|
|
3427
|
+
}
|
|
3428
|
+
const chain = [];
|
|
3429
|
+
let cur = to.id;
|
|
3430
|
+
while (cur !== void 0) {
|
|
3431
|
+
chain.unshift(cur);
|
|
3432
|
+
if (cur === from.id) break;
|
|
3433
|
+
cur = prevOf.get(cur);
|
|
3434
|
+
}
|
|
3435
|
+
const syms = chain.map((id) => symById.get(id)).filter((s) => !!s);
|
|
3436
|
+
const hops = syms.length - 1;
|
|
3437
|
+
const rendered = syms.map((s) => `\`${s.name}\` (${s.file}:${s.start_line})`).join("\n \u2192 ");
|
|
3438
|
+
return textContent(
|
|
3439
|
+
`# call_path: ${from.name} \u2192 ${to.name} (${hops} hop${hops === 1 ? "" : "s"})
|
|
3440
|
+
|
|
3441
|
+
${rendered}`
|
|
3442
|
+
);
|
|
3443
|
+
}
|
|
3338
3444
|
var LIKELY_ENTRY_PATTERNS = [
|
|
3339
3445
|
/(?:^|\/)main\.[a-z0-9_]+$/i,
|
|
3340
3446
|
/(?:^|\/)index\.[a-z0-9_]+$/i,
|
|
@@ -3900,11 +4006,51 @@ async function getCommitsSince(projectRoot, sinceIso) {
|
|
|
3900
4006
|
return [];
|
|
3901
4007
|
}
|
|
3902
4008
|
}
|
|
4009
|
+
async function getHeadSha(projectRoot) {
|
|
4010
|
+
try {
|
|
4011
|
+
const { stdout } = await execFileAsync3("git", ["rev-parse", "HEAD"], { cwd: projectRoot });
|
|
4012
|
+
return stdout.trim();
|
|
4013
|
+
} catch {
|
|
4014
|
+
return "";
|
|
4015
|
+
}
|
|
4016
|
+
}
|
|
4017
|
+
function parseDiffHunks(stdout) {
|
|
4018
|
+
const out = /* @__PURE__ */ new Map();
|
|
4019
|
+
let current = null;
|
|
4020
|
+
for (const line of stdout.split("\n")) {
|
|
4021
|
+
if (line.startsWith("+++ ")) {
|
|
4022
|
+
const p = line.slice(4).trim();
|
|
4023
|
+
current = p === "/dev/null" ? null : p.replace(/^b\//, "");
|
|
4024
|
+
} else if (current && line.startsWith("@@")) {
|
|
4025
|
+
const m = /\+(\d+)(?:,(\d+))?/.exec(line);
|
|
4026
|
+
if (!m) continue;
|
|
4027
|
+
const start = Number(m[1]);
|
|
4028
|
+
const count = m[2] === void 0 ? 1 : Number(m[2]);
|
|
4029
|
+
const end = count === 0 ? start : start + count - 1;
|
|
4030
|
+
const list = out.get(current) ?? [];
|
|
4031
|
+
list.push([start, end]);
|
|
4032
|
+
out.set(current, list);
|
|
4033
|
+
}
|
|
4034
|
+
}
|
|
4035
|
+
return out;
|
|
4036
|
+
}
|
|
4037
|
+
async function getChangedLineRanges(projectRoot, sinceRef) {
|
|
4038
|
+
if (!sinceRef) return /* @__PURE__ */ new Map();
|
|
4039
|
+
try {
|
|
4040
|
+
const { stdout } = await execFileAsync3("git", ["diff", "-U0", "--no-color", sinceRef, "--"], {
|
|
4041
|
+
cwd: projectRoot,
|
|
4042
|
+
maxBuffer: 16 * 1024 * 1024
|
|
4043
|
+
});
|
|
4044
|
+
return parseDiffHunks(stdout);
|
|
4045
|
+
} catch {
|
|
4046
|
+
return /* @__PURE__ */ new Map();
|
|
4047
|
+
}
|
|
4048
|
+
}
|
|
3903
4049
|
|
|
3904
4050
|
// src/memory/session.ts
|
|
3905
4051
|
import { mkdir as mkdir9, readFile as readFile13, writeFile as writeFile8 } from "fs/promises";
|
|
3906
4052
|
import { dirname as dirname10 } from "path";
|
|
3907
|
-
var SESSION_SCHEMA_VERSION =
|
|
4053
|
+
var SESSION_SCHEMA_VERSION = 2;
|
|
3908
4054
|
async function readSession(path) {
|
|
3909
4055
|
try {
|
|
3910
4056
|
const raw = await readFile13(path, "utf8");
|
|
@@ -3933,6 +4079,7 @@ async function captureSnapshot(ctx, branchOverride) {
|
|
|
3933
4079
|
for (const p of ctx.activity.recentFilePaths(TOUCHED_WINDOW_MS)) touched.add(p);
|
|
3934
4080
|
const prev = await readSession(ctx.paths.sessionState);
|
|
3935
4081
|
const recentCommits = await getCommitsSince(ctx.paths.projectRoot, prev?.endedAt ?? "");
|
|
4082
|
+
const headSha = await getHeadSha(ctx.paths.projectRoot);
|
|
3936
4083
|
const snapshot = {
|
|
3937
4084
|
schema_version: SESSION_SCHEMA_VERSION,
|
|
3938
4085
|
endedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -3943,7 +4090,8 @@ async function captureSnapshot(ctx, branchOverride) {
|
|
|
3943
4090
|
tasks: tasks.entries.map((e) => e.content),
|
|
3944
4091
|
decisions: decisions.entries.map((e) => e.content),
|
|
3945
4092
|
next: next.entries.map((e) => e.content)
|
|
3946
|
-
}
|
|
4093
|
+
},
|
|
4094
|
+
headSha
|
|
3947
4095
|
};
|
|
3948
4096
|
await writeSession(ctx.paths.sessionState, snapshot);
|
|
3949
4097
|
}
|
|
@@ -4391,6 +4539,34 @@ var RESUME_PRIMER_MAX_CHARS = 2720;
|
|
|
4391
4539
|
var MAX_FILES = 15;
|
|
4392
4540
|
var MAX_COMMITS2 = 5;
|
|
4393
4541
|
var MAX_BULLETS2 = 3;
|
|
4542
|
+
var MAX_CHANGED_SYMBOLS = 20;
|
|
4543
|
+
var CHANGED_SIG_MAX = 100;
|
|
4544
|
+
function changedSymbols(ranges, graph) {
|
|
4545
|
+
if (ranges.size === 0) return [];
|
|
4546
|
+
const hits = [];
|
|
4547
|
+
for (const n of graph.nodes) {
|
|
4548
|
+
if (n.kind !== "symbol") continue;
|
|
4549
|
+
const rs = ranges.get(n.file);
|
|
4550
|
+
if (!rs) continue;
|
|
4551
|
+
if (rs.some(([a, b]) => a <= n.end_line && b >= n.start_line)) hits.push(n);
|
|
4552
|
+
}
|
|
4553
|
+
hits.sort((a, b) => a.file === b.file ? a.start_line - b.start_line : a.file < b.file ? -1 : 1);
|
|
4554
|
+
return hits;
|
|
4555
|
+
}
|
|
4556
|
+
function changedSymbolsSection(ranges, graph) {
|
|
4557
|
+
const hits = changedSymbols(ranges, graph);
|
|
4558
|
+
if (hits.length === 0) return [];
|
|
4559
|
+
const shown = hits.slice(0, MAX_CHANGED_SYMBOLS);
|
|
4560
|
+
const lines = ["", "### Changed symbols (since last session)"];
|
|
4561
|
+
for (const s of shown) {
|
|
4562
|
+
lines.push(
|
|
4563
|
+
`- \`${s.file}::${s.name}\` (${s.symbol_kind}) \u2014 ${s.signature.trim().slice(0, CHANGED_SIG_MAX)}`
|
|
4564
|
+
);
|
|
4565
|
+
}
|
|
4566
|
+
const more = hits.length - shown.length;
|
|
4567
|
+
if (more > 0) lines.push(`_(+${more} more)_`);
|
|
4568
|
+
return lines;
|
|
4569
|
+
}
|
|
4394
4570
|
function legacyPrimer(ctx) {
|
|
4395
4571
|
const g = ctx.graph;
|
|
4396
4572
|
return `Synthra context loaded for ${g.root}.
|
|
@@ -4401,7 +4577,7 @@ function hasContent(snap) {
|
|
|
4401
4577
|
snap.recentCommits.length || snap.filesTouched.length || snap.summary.tasks.length || snap.summary.next.length || snap.summary.decisions.length
|
|
4402
4578
|
);
|
|
4403
4579
|
}
|
|
4404
|
-
function buildResumeDigest(snap, branchNow) {
|
|
4580
|
+
function buildResumeDigest(snap, branchNow, changedSymbolLines = []) {
|
|
4405
4581
|
const plural = (n) => n === 1 ? "" : "s";
|
|
4406
4582
|
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
4583
|
const essential = [head];
|
|
@@ -4422,7 +4598,7 @@ function buildResumeDigest(snap, branchNow) {
|
|
|
4422
4598
|
essential.push("", "### Recent decisions");
|
|
4423
4599
|
for (const d of snap.summary.decisions.slice(0, MAX_BULLETS2)) essential.push(`- ${d}`);
|
|
4424
4600
|
}
|
|
4425
|
-
const extra = [];
|
|
4601
|
+
const extra = [...changedSymbolLines];
|
|
4426
4602
|
if (snap.recentCommits.length) {
|
|
4427
4603
|
extra.push("", "### Recent commits");
|
|
4428
4604
|
for (const c of snap.recentCommits.slice(0, MAX_COMMITS2)) {
|
|
@@ -4449,7 +4625,12 @@ async function handlePrime(ctx, port) {
|
|
|
4449
4625
|
return { primer: legacy, port };
|
|
4450
4626
|
}
|
|
4451
4627
|
const branchNow = await currentBranch(ctx.paths.projectRoot);
|
|
4452
|
-
|
|
4628
|
+
let changedSymbolLines = [];
|
|
4629
|
+
if (snap.headSha) {
|
|
4630
|
+
const ranges = await getChangedLineRanges(ctx.paths.projectRoot, snap.headSha);
|
|
4631
|
+
changedSymbolLines = changedSymbolsSection(ranges, ctx.graph);
|
|
4632
|
+
}
|
|
4633
|
+
const digest = buildResumeDigest(snap, branchNow, changedSymbolLines);
|
|
4453
4634
|
return { primer: `${digest}
|
|
4454
4635
|
|
|
4455
4636
|
---
|