@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 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.11.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 = 7;
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");