@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.
@@ -1811,7 +1811,7 @@ import { basename as basename2 } from "path";
1811
1811
  // src/hooks/claude-md.ts
1812
1812
  import { readFile as readFile7, writeFile as writeFile3 } from "fs/promises";
1813
1813
  import { basename, dirname as dirname5 } from "path";
1814
- var POLICY_VERSION = 7;
1814
+ var POLICY_VERSION = 8;
1815
1815
  var POLICY_BEGIN = `<!-- synthra-policy v${POLICY_VERSION} BEGIN -->`;
1816
1816
  var POLICY_END = `<!-- synthra-policy v${POLICY_VERSION} END -->`;
1817
1817
  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;
@@ -1830,7 +1830,7 @@ function policyBlock() {
1830
1830
  "> `mcp__synthra__graph_register_edit`. **Short names will NOT resolve**",
1831
1831
  "> in ToolSearch or invocation \u2014 always use the full namespaced form.",
1832
1832
  "> If the tools are deferred, load their schemas with ToolSearch:",
1833
- "> `select:mcp__synthra__graph_continue,mcp__synthra__graph_read,mcp__synthra__graph_register_edit`.",
1833
+ "> `select:mcp__synthra__graph_continue,mcp__synthra__graph_read,mcp__synthra__graph_register_edit,mcp__synthra__find_symbol`.",
1834
1834
  "> Below, short names (`graph_continue` etc.) appear in prose for",
1835
1835
  "> readability only.",
1836
1836
  "",
@@ -1844,6 +1844,10 @@ function policyBlock() {
1844
1844
  " symbol is ~50 tokens, reading a whole file is thousands.",
1845
1845
  "- **`graph_register_edit(files)`** \u2014 after you edit files, call this so",
1846
1846
  " subsequent turns weight your changes and avoid stale snapshots.",
1847
+ "- **`find_symbol(name)`** \u2014 **reuse-first**: before writing a new helper,",
1848
+ " util, or function, call this to check whether one already exists. If it",
1849
+ " returns matches, reuse or extend them instead of re-implementing; only",
1850
+ ' "no match \u2014 safe to create" means it is genuinely new.',
1847
1851
  "",
1848
1852
  "### When to call `graph_continue` \u2014 and when to skip",
1849
1853
  "",
@@ -3128,6 +3132,27 @@ var TOOLS = [
3128
3132
  limit: { type: "number", description: "Cap on returned files. Default 50." }
3129
3133
  }
3130
3134
  }
3135
+ },
3136
+ {
3137
+ name: "find_symbol",
3138
+ 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.",
3139
+ inputSchema: {
3140
+ type: "object",
3141
+ properties: {
3142
+ name: { type: "string", description: "Symbol name (or near-name) to look for." }
3143
+ },
3144
+ required: ["name"]
3145
+ }
3146
+ },
3147
+ {
3148
+ name: "duplicate_symbols",
3149
+ 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.",
3150
+ inputSchema: {
3151
+ type: "object",
3152
+ properties: {
3153
+ limit: { type: "number", description: "Cap on returned names. Default 30." }
3154
+ }
3155
+ }
3131
3156
  }
3132
3157
  ];
3133
3158
  async function callTool(name, args, ctx) {
@@ -3150,6 +3175,10 @@ async function callTool(name, args, ctx) {
3150
3175
  return blastRadius(args, ctx);
3151
3176
  case "dead_code":
3152
3177
  return deadCode(args, ctx);
3178
+ case "find_symbol":
3179
+ return findSymbol(args, ctx);
3180
+ case "duplicate_symbols":
3181
+ return duplicateSymbols(args, ctx);
3153
3182
  default:
3154
3183
  return errorContent(`Unknown tool: ${name}`);
3155
3184
  }
@@ -3353,6 +3382,107 @@ _(no file is unreferenced \u2014 every file is either imported by another, has a
3353
3382
  );
3354
3383
  return textContent(lines.join("\n"));
3355
3384
  }
3385
+ var FIND_MAX = 12;
3386
+ var FIND_SIG_MAX = 140;
3387
+ function symbolEntry(s) {
3388
+ const sig = s.signature.trim().slice(0, FIND_SIG_MAX);
3389
+ return `\u2022 ${sig} \u2192 mcp__synthra__graph_read("${s.file}::${s.name}") [${s.symbol_kind}, L${s.start_line}]`;
3390
+ }
3391
+ var byFileLine = (a, b) => a.file === b.file ? a.start_line - b.start_line : a.file < b.file ? -1 : 1;
3392
+ function findSymbol(args, ctx) {
3393
+ const name = typeof args?.name === "string" ? args.name.trim() : "";
3394
+ if (!name) return errorContent("find_symbol: 'name' (string) is required");
3395
+ const symbols = ctx.graph.nodes.filter((n) => n.kind === "symbol");
3396
+ const lower = name.toLowerCase();
3397
+ const exact = symbols.filter((s) => s.name === name);
3398
+ const exactHits = exact.length > 0 ? exact : symbols.filter((s) => s.name.toLowerCase() === lower);
3399
+ if (exactHits.length > 0) {
3400
+ const sorted = exactHits.slice().sort(byFileLine);
3401
+ const shown2 = sorted.slice(0, FIND_MAX);
3402
+ const omitted2 = sorted.length - shown2.length;
3403
+ const lines2 = [
3404
+ `# find_symbol: "${name}"`,
3405
+ "",
3406
+ `Exact matches (${sorted.length}) \u2014 reuse one of these instead of writing a new one:`,
3407
+ ...shown2.map(symbolEntry)
3408
+ ];
3409
+ if (omitted2 > 0) lines2.push(`\u2026+${omitted2} more`);
3410
+ return textContent(lines2.join("\n"));
3411
+ }
3412
+ const tokens = new Set(tokenizeQuery(name));
3413
+ const scored = symbols.map((s) => {
3414
+ const n = s.name.toLowerCase();
3415
+ let score2 = 0;
3416
+ if (n.includes(lower) || lower.includes(n)) score2 += 2;
3417
+ for (const t of tokens) if (n.includes(t)) score2 += 1;
3418
+ return { s, score: score2 };
3419
+ }).filter((x) => x.score > 0).sort((a, b) => b.score - a.score || byFileLine(a.s, b.s));
3420
+ if (scored.length === 0) {
3421
+ return textContent(
3422
+ `# find_symbol: "${name}"
3423
+
3424
+ No symbol matching "${name}" \u2014 safe to create.`
3425
+ );
3426
+ }
3427
+ const shown = scored.slice(0, FIND_MAX);
3428
+ const omitted = scored.length - shown.length;
3429
+ const lines = [
3430
+ `# find_symbol: "${name}"`,
3431
+ "",
3432
+ `No exact match. Similar names (${scored.length}) \u2014 reuse or extend one before writing new:`,
3433
+ ...shown.map((x) => symbolEntry(x.s))
3434
+ ];
3435
+ if (omitted > 0) lines.push(`\u2026+${omitted} more`);
3436
+ return textContent(lines.join("\n"));
3437
+ }
3438
+ var DUP_INCLUDE = /* @__PURE__ */ new Set([
3439
+ "function",
3440
+ "class",
3441
+ "interface",
3442
+ "type",
3443
+ "enum",
3444
+ "const",
3445
+ "component"
3446
+ ]);
3447
+ function duplicateSymbols(args, ctx) {
3448
+ const limit = typeof args?.limit === "number" && args.limit > 0 ? Math.floor(args.limit) : 30;
3449
+ const defsByName = /* @__PURE__ */ new Map();
3450
+ const filesByName = /* @__PURE__ */ new Map();
3451
+ for (const n of ctx.graph.nodes) {
3452
+ if (n.kind !== "symbol" || !DUP_INCLUDE.has(n.symbol_kind)) continue;
3453
+ (defsByName.get(n.name) ?? defsByName.set(n.name, []).get(n.name)).push({
3454
+ file: n.file,
3455
+ line: n.start_line
3456
+ });
3457
+ (filesByName.get(n.name) ?? filesByName.set(n.name, /* @__PURE__ */ new Set()).get(n.name)).add(n.file);
3458
+ }
3459
+ const dups = [...defsByName.entries()].filter(([name]) => (filesByName.get(name)?.size ?? 0) >= 2).map(([name, defs]) => ({
3460
+ name,
3461
+ defs: defs.slice().sort((a, b) => a.file === b.file ? a.line - b.line : a.file < b.file ? -1 : 1)
3462
+ })).sort((a, b) => b.defs.length - a.defs.length || a.name.localeCompare(b.name));
3463
+ if (dups.length === 0) {
3464
+ return textContent(
3465
+ "# Duplicate symbols\n\n_(no top-level symbol name is defined in more than one file)_"
3466
+ );
3467
+ }
3468
+ const shown = dups.slice(0, limit);
3469
+ const lines = [
3470
+ "# Duplicate symbols (consolidation candidates)",
3471
+ "",
3472
+ `${shown.length} of ${dups.length} name(s) defined in multiple files (functions/classes/types; methods excluded):`,
3473
+ ""
3474
+ ];
3475
+ for (const d of shown) {
3476
+ lines.push(
3477
+ `- \`${d.name}\` (${d.defs.length}): ${d.defs.map((x) => `${x.file}:${x.line}`).join(" \xB7 ")}`
3478
+ );
3479
+ }
3480
+ lines.push("");
3481
+ lines.push(
3482
+ "_advisory: the same name in multiple files may be intentional \u2014 verify before consolidating._"
3483
+ );
3484
+ return textContent(lines.join("\n"));
3485
+ }
3356
3486
  async function graphContinue(args, ctx) {
3357
3487
  const query = typeof args?.query === "string" ? args.query : "";
3358
3488
  if (!query) return errorContent("graph_continue: 'query' (string) is required");