@jefuriiij/synthra 0.11.0 → 0.12.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 +133 -3
- 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 +132 -2
- package/dist/server/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,26 @@ For older versions, see [GitHub Releases](https://github.com/jefuriiij/synthra/r
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## [0.12.0] — 2026-06-24
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`find_symbol(name)` — reuse before you re-implement.** Before writing a new
|
|
15
|
+
helper, ask Synthra whether one already exists: `find_symbol` returns every
|
|
16
|
+
exact-name definition (with signatures + ready `graph_read` targets), or — if
|
|
17
|
+
there's no exact match — similarly-named symbols to reuse or extend. "No symbol
|
|
18
|
+
matching … — safe to create" is the green light that it's genuinely new. The
|
|
19
|
+
injected policy now nudges the agent to check first.
|
|
20
|
+
- **`duplicate_symbols` — consolidation candidates.** Lists symbol names defined
|
|
21
|
+
in more than one file (functions/classes/types; methods excluded, since shared
|
|
22
|
+
method names are normal). Advisory — duplicates can be intentional; it never
|
|
23
|
+
says "delete."
|
|
24
|
+
|
|
25
|
+
Both are built on the symbol index (exact name lookup) — no false-positive risk,
|
|
26
|
+
no new dependencies.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
10
30
|
## [0.11.0] — 2026-06-24
|
|
11
31
|
|
|
12
32
|
### 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.12.0",
|
|
22
22
|
publishConfig: {
|
|
23
23
|
access: "public"
|
|
24
24
|
},
|
|
@@ -3017,7 +3017,7 @@ import { basename as basename5 } from "path";
|
|
|
3017
3017
|
// src/hooks/claude-md.ts
|
|
3018
3018
|
import { readFile as readFile12, writeFile as writeFile6 } from "fs/promises";
|
|
3019
3019
|
import { basename as basename4, dirname as dirname9 } from "path";
|
|
3020
|
-
var POLICY_VERSION =
|
|
3020
|
+
var POLICY_VERSION = 8;
|
|
3021
3021
|
var POLICY_BEGIN = `<!-- synthra-policy v${POLICY_VERSION} BEGIN -->`;
|
|
3022
3022
|
var POLICY_END = `<!-- synthra-policy v${POLICY_VERSION} END -->`;
|
|
3023
3023
|
var ANY_BLOCK_RE = /<!--\s*synthra-policy\s+v\d+\s+BEGIN\s*-->[\s\S]*?<!--\s*synthra-policy\s+v\d+\s+END\s*-->\s*/g;
|
|
@@ -3036,7 +3036,7 @@ function policyBlock() {
|
|
|
3036
3036
|
"> `mcp__synthra__graph_register_edit`. **Short names will NOT resolve**",
|
|
3037
3037
|
"> in ToolSearch or invocation \u2014 always use the full namespaced form.",
|
|
3038
3038
|
"> If the tools are deferred, load their schemas with ToolSearch:",
|
|
3039
|
-
"> `select:mcp__synthra__graph_continue,mcp__synthra__graph_read,mcp__synthra__graph_register_edit`.",
|
|
3039
|
+
"> `select:mcp__synthra__graph_continue,mcp__synthra__graph_read,mcp__synthra__graph_register_edit,mcp__synthra__find_symbol`.",
|
|
3040
3040
|
"> Below, short names (`graph_continue` etc.) appear in prose for",
|
|
3041
3041
|
"> readability only.",
|
|
3042
3042
|
"",
|
|
@@ -3050,6 +3050,10 @@ function policyBlock() {
|
|
|
3050
3050
|
" symbol is ~50 tokens, reading a whole file is thousands.",
|
|
3051
3051
|
"- **`graph_register_edit(files)`** \u2014 after you edit files, call this so",
|
|
3052
3052
|
" subsequent turns weight your changes and avoid stale snapshots.",
|
|
3053
|
+
"- **`find_symbol(name)`** \u2014 **reuse-first**: before writing a new helper,",
|
|
3054
|
+
" util, or function, call this to check whether one already exists. If it",
|
|
3055
|
+
" returns matches, reuse or extend them instead of re-implementing; only",
|
|
3056
|
+
' "no match \u2014 safe to create" means it is genuinely new.',
|
|
3053
3057
|
"",
|
|
3054
3058
|
"### When to call `graph_continue` \u2014 and when to skip",
|
|
3055
3059
|
"",
|
|
@@ -4219,6 +4223,27 @@ var TOOLS = [
|
|
|
4219
4223
|
limit: { type: "number", description: "Cap on returned files. Default 50." }
|
|
4220
4224
|
}
|
|
4221
4225
|
}
|
|
4226
|
+
},
|
|
4227
|
+
{
|
|
4228
|
+
name: "find_symbol",
|
|
4229
|
+
description: "Find existing symbols by name BEFORE writing a new one \u2014 reuse beats re-implementing. Returns exact-name definitions (signatures + graph_read targets) or, if none, similarly-named symbols. 'No symbol matching \u2026 \u2014 safe to create' means it's genuinely new.",
|
|
4230
|
+
inputSchema: {
|
|
4231
|
+
type: "object",
|
|
4232
|
+
properties: {
|
|
4233
|
+
name: { type: "string", description: "Symbol name (or near-name) to look for." }
|
|
4234
|
+
},
|
|
4235
|
+
required: ["name"]
|
|
4236
|
+
}
|
|
4237
|
+
},
|
|
4238
|
+
{
|
|
4239
|
+
name: "duplicate_symbols",
|
|
4240
|
+
description: "List symbol names defined in more than one file (functions/classes/types; methods excluded) \u2014 consolidation candidates for review. Advisory: duplicates may be intentional.",
|
|
4241
|
+
inputSchema: {
|
|
4242
|
+
type: "object",
|
|
4243
|
+
properties: {
|
|
4244
|
+
limit: { type: "number", description: "Cap on returned names. Default 30." }
|
|
4245
|
+
}
|
|
4246
|
+
}
|
|
4222
4247
|
}
|
|
4223
4248
|
];
|
|
4224
4249
|
async function callTool(name, args, ctx) {
|
|
@@ -4241,6 +4266,10 @@ async function callTool(name, args, ctx) {
|
|
|
4241
4266
|
return blastRadius(args, ctx);
|
|
4242
4267
|
case "dead_code":
|
|
4243
4268
|
return deadCode(args, ctx);
|
|
4269
|
+
case "find_symbol":
|
|
4270
|
+
return findSymbol(args, ctx);
|
|
4271
|
+
case "duplicate_symbols":
|
|
4272
|
+
return duplicateSymbols(args, ctx);
|
|
4244
4273
|
default:
|
|
4245
4274
|
return errorContent(`Unknown tool: ${name}`);
|
|
4246
4275
|
}
|
|
@@ -4444,6 +4473,107 @@ _(no file is unreferenced \u2014 every file is either imported by another, has a
|
|
|
4444
4473
|
);
|
|
4445
4474
|
return textContent(lines.join("\n"));
|
|
4446
4475
|
}
|
|
4476
|
+
var FIND_MAX = 12;
|
|
4477
|
+
var FIND_SIG_MAX = 140;
|
|
4478
|
+
function symbolEntry(s) {
|
|
4479
|
+
const sig = s.signature.trim().slice(0, FIND_SIG_MAX);
|
|
4480
|
+
return `\u2022 ${sig} \u2192 mcp__synthra__graph_read("${s.file}::${s.name}") [${s.symbol_kind}, L${s.start_line}]`;
|
|
4481
|
+
}
|
|
4482
|
+
var byFileLine = (a, b) => a.file === b.file ? a.start_line - b.start_line : a.file < b.file ? -1 : 1;
|
|
4483
|
+
function findSymbol(args, ctx) {
|
|
4484
|
+
const name = typeof args?.name === "string" ? args.name.trim() : "";
|
|
4485
|
+
if (!name) return errorContent("find_symbol: 'name' (string) is required");
|
|
4486
|
+
const symbols = ctx.graph.nodes.filter((n) => n.kind === "symbol");
|
|
4487
|
+
const lower = name.toLowerCase();
|
|
4488
|
+
const exact = symbols.filter((s) => s.name === name);
|
|
4489
|
+
const exactHits = exact.length > 0 ? exact : symbols.filter((s) => s.name.toLowerCase() === lower);
|
|
4490
|
+
if (exactHits.length > 0) {
|
|
4491
|
+
const sorted = exactHits.slice().sort(byFileLine);
|
|
4492
|
+
const shown2 = sorted.slice(0, FIND_MAX);
|
|
4493
|
+
const omitted2 = sorted.length - shown2.length;
|
|
4494
|
+
const lines2 = [
|
|
4495
|
+
`# find_symbol: "${name}"`,
|
|
4496
|
+
"",
|
|
4497
|
+
`Exact matches (${sorted.length}) \u2014 reuse one of these instead of writing a new one:`,
|
|
4498
|
+
...shown2.map(symbolEntry)
|
|
4499
|
+
];
|
|
4500
|
+
if (omitted2 > 0) lines2.push(`\u2026+${omitted2} more`);
|
|
4501
|
+
return textContent(lines2.join("\n"));
|
|
4502
|
+
}
|
|
4503
|
+
const tokens = new Set(tokenizeQuery(name));
|
|
4504
|
+
const scored = symbols.map((s) => {
|
|
4505
|
+
const n = s.name.toLowerCase();
|
|
4506
|
+
let score2 = 0;
|
|
4507
|
+
if (n.includes(lower) || lower.includes(n)) score2 += 2;
|
|
4508
|
+
for (const t of tokens) if (n.includes(t)) score2 += 1;
|
|
4509
|
+
return { s, score: score2 };
|
|
4510
|
+
}).filter((x) => x.score > 0).sort((a, b) => b.score - a.score || byFileLine(a.s, b.s));
|
|
4511
|
+
if (scored.length === 0) {
|
|
4512
|
+
return textContent(
|
|
4513
|
+
`# find_symbol: "${name}"
|
|
4514
|
+
|
|
4515
|
+
No symbol matching "${name}" \u2014 safe to create.`
|
|
4516
|
+
);
|
|
4517
|
+
}
|
|
4518
|
+
const shown = scored.slice(0, FIND_MAX);
|
|
4519
|
+
const omitted = scored.length - shown.length;
|
|
4520
|
+
const lines = [
|
|
4521
|
+
`# find_symbol: "${name}"`,
|
|
4522
|
+
"",
|
|
4523
|
+
`No exact match. Similar names (${scored.length}) \u2014 reuse or extend one before writing new:`,
|
|
4524
|
+
...shown.map((x) => symbolEntry(x.s))
|
|
4525
|
+
];
|
|
4526
|
+
if (omitted > 0) lines.push(`\u2026+${omitted} more`);
|
|
4527
|
+
return textContent(lines.join("\n"));
|
|
4528
|
+
}
|
|
4529
|
+
var DUP_INCLUDE = /* @__PURE__ */ new Set([
|
|
4530
|
+
"function",
|
|
4531
|
+
"class",
|
|
4532
|
+
"interface",
|
|
4533
|
+
"type",
|
|
4534
|
+
"enum",
|
|
4535
|
+
"const",
|
|
4536
|
+
"component"
|
|
4537
|
+
]);
|
|
4538
|
+
function duplicateSymbols(args, ctx) {
|
|
4539
|
+
const limit = typeof args?.limit === "number" && args.limit > 0 ? Math.floor(args.limit) : 30;
|
|
4540
|
+
const defsByName = /* @__PURE__ */ new Map();
|
|
4541
|
+
const filesByName = /* @__PURE__ */ new Map();
|
|
4542
|
+
for (const n of ctx.graph.nodes) {
|
|
4543
|
+
if (n.kind !== "symbol" || !DUP_INCLUDE.has(n.symbol_kind)) continue;
|
|
4544
|
+
(defsByName.get(n.name) ?? defsByName.set(n.name, []).get(n.name)).push({
|
|
4545
|
+
file: n.file,
|
|
4546
|
+
line: n.start_line
|
|
4547
|
+
});
|
|
4548
|
+
(filesByName.get(n.name) ?? filesByName.set(n.name, /* @__PURE__ */ new Set()).get(n.name)).add(n.file);
|
|
4549
|
+
}
|
|
4550
|
+
const dups = [...defsByName.entries()].filter(([name]) => (filesByName.get(name)?.size ?? 0) >= 2).map(([name, defs]) => ({
|
|
4551
|
+
name,
|
|
4552
|
+
defs: defs.slice().sort((a, b) => a.file === b.file ? a.line - b.line : a.file < b.file ? -1 : 1)
|
|
4553
|
+
})).sort((a, b) => b.defs.length - a.defs.length || a.name.localeCompare(b.name));
|
|
4554
|
+
if (dups.length === 0) {
|
|
4555
|
+
return textContent(
|
|
4556
|
+
"# Duplicate symbols\n\n_(no top-level symbol name is defined in more than one file)_"
|
|
4557
|
+
);
|
|
4558
|
+
}
|
|
4559
|
+
const shown = dups.slice(0, limit);
|
|
4560
|
+
const lines = [
|
|
4561
|
+
"# Duplicate symbols (consolidation candidates)",
|
|
4562
|
+
"",
|
|
4563
|
+
`${shown.length} of ${dups.length} name(s) defined in multiple files (functions/classes/types; methods excluded):`,
|
|
4564
|
+
""
|
|
4565
|
+
];
|
|
4566
|
+
for (const d of shown) {
|
|
4567
|
+
lines.push(
|
|
4568
|
+
`- \`${d.name}\` (${d.defs.length}): ${d.defs.map((x) => `${x.file}:${x.line}`).join(" \xB7 ")}`
|
|
4569
|
+
);
|
|
4570
|
+
}
|
|
4571
|
+
lines.push("");
|
|
4572
|
+
lines.push(
|
|
4573
|
+
"_advisory: the same name in multiple files may be intentional \u2014 verify before consolidating._"
|
|
4574
|
+
);
|
|
4575
|
+
return textContent(lines.join("\n"));
|
|
4576
|
+
}
|
|
4447
4577
|
async function graphContinue(args, ctx) {
|
|
4448
4578
|
const query = typeof args?.query === "string" ? args.query : "";
|
|
4449
4579
|
if (!query) return errorContent("graph_continue: 'query' (string) is required");
|