@jefuriiij/synthra 0.14.1 → 0.15.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 +21 -0
- package/dist/cli/index.js +83 -9
- 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 +82 -8
- package/dist/server/index.js.map +1 -1
- package/package.json +1 -1
package/dist/server/index.js
CHANGED
|
@@ -2769,7 +2769,8 @@ async function rememberEntry(paths, input) {
|
|
|
2769
2769
|
content: input.text,
|
|
2770
2770
|
tags: input.tags ?? [],
|
|
2771
2771
|
files: input.files ?? [],
|
|
2772
|
-
date: (/* @__PURE__ */ new Date()).toISOString()
|
|
2772
|
+
date: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2773
|
+
...input.anchors && input.anchors.length > 0 ? { anchors: input.anchors } : {}
|
|
2773
2774
|
};
|
|
2774
2775
|
await appendEntry(active.paths.contextStore, entry);
|
|
2775
2776
|
const entries = await readEntries(active.paths.contextStore);
|
|
@@ -3071,7 +3072,7 @@ var TOOLS = [
|
|
|
3071
3072
|
files: {
|
|
3072
3073
|
type: "array",
|
|
3073
3074
|
items: { type: "string" },
|
|
3074
|
-
description: "Optional project-relative file paths this entry relates to."
|
|
3075
|
+
description: "Optional project-relative file paths this entry relates to. Linked files also anchor the entry: recall flags it 'possibly stale' if they change, and graph_read of those files surfaces it automatically."
|
|
3075
3076
|
}
|
|
3076
3077
|
},
|
|
3077
3078
|
required: ["text", "kind"]
|
|
@@ -3613,9 +3614,28 @@ async function graphContinue(args, ctx) {
|
|
|
3613
3614
|
Files: ${retrieval.files.map((f) => f.path).join(", ") || "(none)"}
|
|
3614
3615
|
Reason: ${retrieval.reason}
|
|
3615
3616
|
`;
|
|
3616
|
-
|
|
3617
|
+
const remembered = matchRememberedFacts(query, retrieval.files, await safeRecallAll(ctx), ctx);
|
|
3618
|
+
return textContent(`${header}${remembered}
|
|
3617
3619
|
${packed.text}`);
|
|
3618
3620
|
}
|
|
3621
|
+
function matchRememberedFacts(query, retrievedFiles, entries, ctx) {
|
|
3622
|
+
if (entries.length === 0) return "";
|
|
3623
|
+
const qTokens = new Set(tokenizeQuery(query));
|
|
3624
|
+
const retrievedPaths = new Set(retrievedFiles.map((f) => f.path));
|
|
3625
|
+
const scored = entries.map((e) => {
|
|
3626
|
+
let score2 = 0;
|
|
3627
|
+
for (const t of tokenizeQuery(`${e.content} ${e.tags.join(" ")}`)) {
|
|
3628
|
+
if (qTokens.has(t)) score2 += 1;
|
|
3629
|
+
}
|
|
3630
|
+
if (e.files.some((f) => retrievedPaths.has(f)) || e.anchors?.some((a) => retrievedPaths.has(a.path))) {
|
|
3631
|
+
score2 += 2;
|
|
3632
|
+
}
|
|
3633
|
+
return { e, score: score2 };
|
|
3634
|
+
}).filter((x) => x.score > 0).sort((a, b) => b.score - a.score).slice(0, PACK_FACTS_MAX);
|
|
3635
|
+
if (scored.length === 0) return "";
|
|
3636
|
+
return `${scored.map((x) => `Remembered: ${factLine(x.e, ctx.graph).slice(2)}`).join("\n")}
|
|
3637
|
+
`;
|
|
3638
|
+
}
|
|
3619
3639
|
function resolveFileTarget(graph, filePath) {
|
|
3620
3640
|
const files = graph.nodes.filter((n) => n.kind === "file");
|
|
3621
3641
|
const exact = files.find((n) => n.path === filePath);
|
|
@@ -3708,6 +3728,43 @@ function buildTestsFooter(symbol, graph) {
|
|
|
3708
3728
|
if (isLikelyEntry(symbol.file)) return "";
|
|
3709
3729
|
return "Tests: none linked to this file.";
|
|
3710
3730
|
}
|
|
3731
|
+
var FACTS_MAX = 3;
|
|
3732
|
+
var FACTS_CONTENT_MAX = 160;
|
|
3733
|
+
var PACK_FACTS_MAX = 2;
|
|
3734
|
+
function staleAnchorPaths(entry, graph) {
|
|
3735
|
+
if (!entry.anchors || entry.anchors.length === 0) return [];
|
|
3736
|
+
const hashByPath = /* @__PURE__ */ new Map();
|
|
3737
|
+
for (const n of graph.nodes) if (n.kind === "file") hashByPath.set(n.path, n.file_hash);
|
|
3738
|
+
return entry.anchors.filter((a) => hashByPath.get(a.path) !== a.hash).map((a) => a.path);
|
|
3739
|
+
}
|
|
3740
|
+
function factLine(entry, graph) {
|
|
3741
|
+
const content = entry.content.length > FACTS_CONTENT_MAX ? `${entry.content.slice(0, FACTS_CONTENT_MAX - 1)}\u2026` : entry.content;
|
|
3742
|
+
const date = entry.date ? ` (${entry.date.slice(0, 10)})` : "";
|
|
3743
|
+
const stale = staleAnchorPaths(entry, graph);
|
|
3744
|
+
const staleNote = stale.length ? ` \u26A0 possibly stale \u2014 ${stale[0]} changed since stored` : "";
|
|
3745
|
+
return `- [${entry.type}] ${content}${date}${staleNote}`;
|
|
3746
|
+
}
|
|
3747
|
+
function entryLinksFile(entry, filePath) {
|
|
3748
|
+
if (entry.anchors?.some((a) => a.path === filePath)) return true;
|
|
3749
|
+
return entry.files.some((f) => f === filePath || filePath.endsWith(`/${f}`));
|
|
3750
|
+
}
|
|
3751
|
+
function buildFactsFooter(filePath, entries, graph) {
|
|
3752
|
+
const linked = entries.filter((e) => entryLinksFile(e, filePath));
|
|
3753
|
+
if (linked.length === 0) return "";
|
|
3754
|
+
const newestFirst = linked.slice().reverse();
|
|
3755
|
+
const shown = newestFirst.slice(0, FACTS_MAX);
|
|
3756
|
+
const omitted = newestFirst.length - shown.length;
|
|
3757
|
+
const lines = ["\u{1F4CC} Remembered for this file:", ...shown.map((e) => factLine(e, graph))];
|
|
3758
|
+
if (omitted > 0) lines.push(`\u2026+${omitted} more \u2014 mcp__synthra__context_recall()`);
|
|
3759
|
+
return lines.join("\n");
|
|
3760
|
+
}
|
|
3761
|
+
async function safeRecallAll(ctx) {
|
|
3762
|
+
try {
|
|
3763
|
+
return (await recallEntries(ctx.paths, {})).entries;
|
|
3764
|
+
} catch {
|
|
3765
|
+
return [];
|
|
3766
|
+
}
|
|
3767
|
+
}
|
|
3711
3768
|
async function graphRead(args, ctx) {
|
|
3712
3769
|
const target = typeof args?.target === "string" ? args.target : "";
|
|
3713
3770
|
if (!target) return errorContent("graph_read: 'target' (string) is required");
|
|
@@ -3726,10 +3783,15 @@ async function graphRead(args, ctx) {
|
|
|
3726
3783
|
}
|
|
3727
3784
|
const fileNode = resolved.node;
|
|
3728
3785
|
await logAccess(ctx, { ts: nowIso(), path: fileNode.path, source: "read" });
|
|
3786
|
+
const facts = buildFactsFooter(fileNode.path, await safeRecallAll(ctx), ctx.graph);
|
|
3787
|
+
const factsBlock = facts ? `
|
|
3788
|
+
|
|
3789
|
+
---
|
|
3790
|
+
${facts}` : "";
|
|
3729
3791
|
if (!symbolName) {
|
|
3730
3792
|
return textContent(`# ${fileNode.path}
|
|
3731
3793
|
|
|
3732
|
-
${fileNode.content}`);
|
|
3794
|
+
${fileNode.content}${factsBlock}`);
|
|
3733
3795
|
}
|
|
3734
3796
|
const cleanSym = symbolName.trim();
|
|
3735
3797
|
const symbol = ctx.graph.nodes.find(
|
|
@@ -3759,7 +3821,7 @@ ${tests}` : "";
|
|
|
3759
3821
|
return textContent(
|
|
3760
3822
|
`# ${fileNode.path}::${symbol.name} (L${symbol.start_line}-${symbol.end_line})
|
|
3761
3823
|
|
|
3762
|
-
${body}${depsBlock}${testsBlock}${editHint}`
|
|
3824
|
+
${body}${depsBlock}${testsBlock}${factsBlock}${editHint}`
|
|
3763
3825
|
);
|
|
3764
3826
|
}
|
|
3765
3827
|
var editedFiles = /* @__PURE__ */ new Set();
|
|
@@ -3794,16 +3856,26 @@ async function contextRemember(args, ctx) {
|
|
|
3794
3856
|
}
|
|
3795
3857
|
const tags = Array.isArray(args?.tags) ? args.tags.filter((t) => typeof t === "string") : [];
|
|
3796
3858
|
const files = Array.isArray(args?.files) ? args.files.filter((f) => typeof f === "string") : [];
|
|
3859
|
+
const anchors = [];
|
|
3860
|
+
for (const f of files) {
|
|
3861
|
+
const resolved = resolveFileTarget(ctx.graph, f);
|
|
3862
|
+
if ("node" in resolved) {
|
|
3863
|
+
anchors.push({ path: resolved.node.path, hash: resolved.node.file_hash });
|
|
3864
|
+
}
|
|
3865
|
+
}
|
|
3797
3866
|
const result = await rememberEntry(ctx.paths, {
|
|
3798
3867
|
text,
|
|
3799
3868
|
kind: kindRaw,
|
|
3800
3869
|
tags,
|
|
3801
|
-
files
|
|
3870
|
+
files,
|
|
3871
|
+
anchors
|
|
3802
3872
|
});
|
|
3873
|
+
const anchorNote = anchors.length ? `
|
|
3874
|
+
Anchored to ${anchors.length} file(s) \u2014 recall will flag this entry if they change.` : "";
|
|
3803
3875
|
return textContent(
|
|
3804
3876
|
`Remembered ${result.entry.type} on branch '${result.branch}'.
|
|
3805
3877
|
Stored: ${result.storePath}
|
|
3806
|
-
CONTEXT.md refreshed: ${result.contextMdPath}`
|
|
3878
|
+
CONTEXT.md refreshed: ${result.contextMdPath}${anchorNote}`
|
|
3807
3879
|
);
|
|
3808
3880
|
}
|
|
3809
3881
|
var DEFAULT_RECENT_WINDOW_MS = 60 * 60 * 1e3;
|
|
@@ -3838,7 +3910,9 @@ async function contextRecall(args, ctx) {
|
|
|
3838
3910
|
const lines = [`# Context entries \u2014 branch: ${result.branch}`, ""];
|
|
3839
3911
|
for (const e of result.entries) {
|
|
3840
3912
|
const tags = e.tags.length ? ` [${e.tags.join(", ")}]` : "";
|
|
3841
|
-
|
|
3913
|
+
const stale = staleAnchorPaths(e, ctx.graph);
|
|
3914
|
+
const staleNote = stale.length ? ` \u26A0 possibly stale \u2014 ${stale.join(", ")} changed since stored` : "";
|
|
3915
|
+
lines.push(`- **${e.type}**${tags} (${e.date}): ${e.content}${staleNote}`);
|
|
3842
3916
|
if (e.files.length) lines.push(` files: ${e.files.join(", ")}`);
|
|
3843
3917
|
}
|
|
3844
3918
|
return textContent(lines.join("\n"));
|