@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/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,27 @@ For older versions, see [GitHub Releases](https://github.com/jefuriiij/synthra/r
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## [0.15.0] — 2026-07-02
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Your remembered context now talks back.** Synthra's second brain was
|
|
15
|
+
write-only — decisions and gotchas went in via `context_remember` and never
|
|
16
|
+
resurfaced. Now they appear exactly where they're relevant:
|
|
17
|
+
- `graph_read` of a file (or a symbol in it) shows a `📌 Remembered for this
|
|
18
|
+
file` block with the entries linked to that file.
|
|
19
|
+
- `graph_continue` packs include `Remembered:` lines for entries matching the
|
|
20
|
+
query.
|
|
21
|
+
- **Memories know when they might be wrong.** Entries linked to files are now
|
|
22
|
+
*anchored* to those files' content hashes at capture time. When the code
|
|
23
|
+
changes afterwards (tracked live by auto-reindex), every surfacing of that
|
|
24
|
+
entry — graph_read, graph_continue, context_recall — carries
|
|
25
|
+
`⚠ possibly stale — <file> changed since stored`. Old entries without anchors
|
|
26
|
+
keep working and are never flagged; the shared context-store format is
|
|
27
|
+
unchanged (additive optional field).
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
10
31
|
## [0.14.1] — 2026-07-02
|
|
11
32
|
|
|
12
33
|
### 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.15.0",
|
|
22
22
|
publishConfig: {
|
|
23
23
|
access: "public"
|
|
24
24
|
},
|
|
@@ -3882,7 +3882,8 @@ async function rememberEntry(paths, input) {
|
|
|
3882
3882
|
content: input.text,
|
|
3883
3883
|
tags: input.tags ?? [],
|
|
3884
3884
|
files: input.files ?? [],
|
|
3885
|
-
date: (/* @__PURE__ */ new Date()).toISOString()
|
|
3885
|
+
date: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3886
|
+
...input.anchors && input.anchors.length > 0 ? { anchors: input.anchors } : {}
|
|
3886
3887
|
};
|
|
3887
3888
|
await appendEntry(active.paths.contextStore, entry);
|
|
3888
3889
|
const entries = await readEntries(active.paths.contextStore);
|
|
@@ -4184,7 +4185,7 @@ var TOOLS = [
|
|
|
4184
4185
|
files: {
|
|
4185
4186
|
type: "array",
|
|
4186
4187
|
items: { type: "string" },
|
|
4187
|
-
description: "Optional project-relative file paths this entry relates to."
|
|
4188
|
+
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."
|
|
4188
4189
|
}
|
|
4189
4190
|
},
|
|
4190
4191
|
required: ["text", "kind"]
|
|
@@ -4726,9 +4727,28 @@ async function graphContinue(args, ctx) {
|
|
|
4726
4727
|
Files: ${retrieval.files.map((f) => f.path).join(", ") || "(none)"}
|
|
4727
4728
|
Reason: ${retrieval.reason}
|
|
4728
4729
|
`;
|
|
4729
|
-
|
|
4730
|
+
const remembered = matchRememberedFacts(query, retrieval.files, await safeRecallAll(ctx), ctx);
|
|
4731
|
+
return textContent(`${header}${remembered}
|
|
4730
4732
|
${packed.text}`);
|
|
4731
4733
|
}
|
|
4734
|
+
function matchRememberedFacts(query, retrievedFiles, entries, ctx) {
|
|
4735
|
+
if (entries.length === 0) return "";
|
|
4736
|
+
const qTokens = new Set(tokenizeQuery(query));
|
|
4737
|
+
const retrievedPaths = new Set(retrievedFiles.map((f) => f.path));
|
|
4738
|
+
const scored = entries.map((e) => {
|
|
4739
|
+
let score2 = 0;
|
|
4740
|
+
for (const t of tokenizeQuery(`${e.content} ${e.tags.join(" ")}`)) {
|
|
4741
|
+
if (qTokens.has(t)) score2 += 1;
|
|
4742
|
+
}
|
|
4743
|
+
if (e.files.some((f) => retrievedPaths.has(f)) || e.anchors?.some((a) => retrievedPaths.has(a.path))) {
|
|
4744
|
+
score2 += 2;
|
|
4745
|
+
}
|
|
4746
|
+
return { e, score: score2 };
|
|
4747
|
+
}).filter((x) => x.score > 0).sort((a, b) => b.score - a.score).slice(0, PACK_FACTS_MAX);
|
|
4748
|
+
if (scored.length === 0) return "";
|
|
4749
|
+
return `${scored.map((x) => `Remembered: ${factLine(x.e, ctx.graph).slice(2)}`).join("\n")}
|
|
4750
|
+
`;
|
|
4751
|
+
}
|
|
4732
4752
|
function resolveFileTarget(graph, filePath) {
|
|
4733
4753
|
const files = graph.nodes.filter((n) => n.kind === "file");
|
|
4734
4754
|
const exact = files.find((n) => n.path === filePath);
|
|
@@ -4821,6 +4841,43 @@ function buildTestsFooter(symbol, graph) {
|
|
|
4821
4841
|
if (isLikelyEntry(symbol.file)) return "";
|
|
4822
4842
|
return "Tests: none linked to this file.";
|
|
4823
4843
|
}
|
|
4844
|
+
var FACTS_MAX = 3;
|
|
4845
|
+
var FACTS_CONTENT_MAX = 160;
|
|
4846
|
+
var PACK_FACTS_MAX = 2;
|
|
4847
|
+
function staleAnchorPaths(entry, graph) {
|
|
4848
|
+
if (!entry.anchors || entry.anchors.length === 0) return [];
|
|
4849
|
+
const hashByPath = /* @__PURE__ */ new Map();
|
|
4850
|
+
for (const n of graph.nodes) if (n.kind === "file") hashByPath.set(n.path, n.file_hash);
|
|
4851
|
+
return entry.anchors.filter((a) => hashByPath.get(a.path) !== a.hash).map((a) => a.path);
|
|
4852
|
+
}
|
|
4853
|
+
function factLine(entry, graph) {
|
|
4854
|
+
const content = entry.content.length > FACTS_CONTENT_MAX ? `${entry.content.slice(0, FACTS_CONTENT_MAX - 1)}\u2026` : entry.content;
|
|
4855
|
+
const date = entry.date ? ` (${entry.date.slice(0, 10)})` : "";
|
|
4856
|
+
const stale = staleAnchorPaths(entry, graph);
|
|
4857
|
+
const staleNote = stale.length ? ` \u26A0 possibly stale \u2014 ${stale[0]} changed since stored` : "";
|
|
4858
|
+
return `- [${entry.type}] ${content}${date}${staleNote}`;
|
|
4859
|
+
}
|
|
4860
|
+
function entryLinksFile(entry, filePath) {
|
|
4861
|
+
if (entry.anchors?.some((a) => a.path === filePath)) return true;
|
|
4862
|
+
return entry.files.some((f) => f === filePath || filePath.endsWith(`/${f}`));
|
|
4863
|
+
}
|
|
4864
|
+
function buildFactsFooter(filePath, entries, graph) {
|
|
4865
|
+
const linked = entries.filter((e) => entryLinksFile(e, filePath));
|
|
4866
|
+
if (linked.length === 0) return "";
|
|
4867
|
+
const newestFirst = linked.slice().reverse();
|
|
4868
|
+
const shown = newestFirst.slice(0, FACTS_MAX);
|
|
4869
|
+
const omitted = newestFirst.length - shown.length;
|
|
4870
|
+
const lines = ["\u{1F4CC} Remembered for this file:", ...shown.map((e) => factLine(e, graph))];
|
|
4871
|
+
if (omitted > 0) lines.push(`\u2026+${omitted} more \u2014 mcp__synthra__context_recall()`);
|
|
4872
|
+
return lines.join("\n");
|
|
4873
|
+
}
|
|
4874
|
+
async function safeRecallAll(ctx) {
|
|
4875
|
+
try {
|
|
4876
|
+
return (await recallEntries(ctx.paths, {})).entries;
|
|
4877
|
+
} catch {
|
|
4878
|
+
return [];
|
|
4879
|
+
}
|
|
4880
|
+
}
|
|
4824
4881
|
async function graphRead(args, ctx) {
|
|
4825
4882
|
const target = typeof args?.target === "string" ? args.target : "";
|
|
4826
4883
|
if (!target) return errorContent("graph_read: 'target' (string) is required");
|
|
@@ -4839,10 +4896,15 @@ async function graphRead(args, ctx) {
|
|
|
4839
4896
|
}
|
|
4840
4897
|
const fileNode = resolved.node;
|
|
4841
4898
|
await logAccess(ctx, { ts: nowIso(), path: fileNode.path, source: "read" });
|
|
4899
|
+
const facts = buildFactsFooter(fileNode.path, await safeRecallAll(ctx), ctx.graph);
|
|
4900
|
+
const factsBlock = facts ? `
|
|
4901
|
+
|
|
4902
|
+
---
|
|
4903
|
+
${facts}` : "";
|
|
4842
4904
|
if (!symbolName) {
|
|
4843
4905
|
return textContent(`# ${fileNode.path}
|
|
4844
4906
|
|
|
4845
|
-
${fileNode.content}`);
|
|
4907
|
+
${fileNode.content}${factsBlock}`);
|
|
4846
4908
|
}
|
|
4847
4909
|
const cleanSym = symbolName.trim();
|
|
4848
4910
|
const symbol = ctx.graph.nodes.find(
|
|
@@ -4872,7 +4934,7 @@ ${tests}` : "";
|
|
|
4872
4934
|
return textContent(
|
|
4873
4935
|
`# ${fileNode.path}::${symbol.name} (L${symbol.start_line}-${symbol.end_line})
|
|
4874
4936
|
|
|
4875
|
-
${body}${depsBlock}${testsBlock}${editHint}`
|
|
4937
|
+
${body}${depsBlock}${testsBlock}${factsBlock}${editHint}`
|
|
4876
4938
|
);
|
|
4877
4939
|
}
|
|
4878
4940
|
var editedFiles = /* @__PURE__ */ new Set();
|
|
@@ -4907,16 +4969,26 @@ async function contextRemember(args, ctx) {
|
|
|
4907
4969
|
}
|
|
4908
4970
|
const tags = Array.isArray(args?.tags) ? args.tags.filter((t) => typeof t === "string") : [];
|
|
4909
4971
|
const files = Array.isArray(args?.files) ? args.files.filter((f) => typeof f === "string") : [];
|
|
4972
|
+
const anchors = [];
|
|
4973
|
+
for (const f of files) {
|
|
4974
|
+
const resolved = resolveFileTarget(ctx.graph, f);
|
|
4975
|
+
if ("node" in resolved) {
|
|
4976
|
+
anchors.push({ path: resolved.node.path, hash: resolved.node.file_hash });
|
|
4977
|
+
}
|
|
4978
|
+
}
|
|
4910
4979
|
const result = await rememberEntry(ctx.paths, {
|
|
4911
4980
|
text,
|
|
4912
4981
|
kind: kindRaw,
|
|
4913
4982
|
tags,
|
|
4914
|
-
files
|
|
4983
|
+
files,
|
|
4984
|
+
anchors
|
|
4915
4985
|
});
|
|
4986
|
+
const anchorNote = anchors.length ? `
|
|
4987
|
+
Anchored to ${anchors.length} file(s) \u2014 recall will flag this entry if they change.` : "";
|
|
4916
4988
|
return textContent(
|
|
4917
4989
|
`Remembered ${result.entry.type} on branch '${result.branch}'.
|
|
4918
4990
|
Stored: ${result.storePath}
|
|
4919
|
-
CONTEXT.md refreshed: ${result.contextMdPath}`
|
|
4991
|
+
CONTEXT.md refreshed: ${result.contextMdPath}${anchorNote}`
|
|
4920
4992
|
);
|
|
4921
4993
|
}
|
|
4922
4994
|
var DEFAULT_RECENT_WINDOW_MS = 60 * 60 * 1e3;
|
|
@@ -4951,7 +5023,9 @@ async function contextRecall(args, ctx) {
|
|
|
4951
5023
|
const lines = [`# Context entries \u2014 branch: ${result.branch}`, ""];
|
|
4952
5024
|
for (const e of result.entries) {
|
|
4953
5025
|
const tags = e.tags.length ? ` [${e.tags.join(", ")}]` : "";
|
|
4954
|
-
|
|
5026
|
+
const stale = staleAnchorPaths(e, ctx.graph);
|
|
5027
|
+
const staleNote = stale.length ? ` \u26A0 possibly stale \u2014 ${stale.join(", ")} changed since stored` : "";
|
|
5028
|
+
lines.push(`- **${e.type}**${tags} (${e.date}): ${e.content}${staleNote}`);
|
|
4955
5029
|
if (e.files.length) lines.push(` files: ${e.files.join(", ")}`);
|
|
4956
5030
|
}
|
|
4957
5031
|
return textContent(lines.join("\n"));
|