@jefuriiij/synthra 0.3.1 → 0.4.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.
@@ -1810,7 +1810,7 @@ import { basename as basename2 } from "path";
1810
1810
  // src/hooks/claude-md.ts
1811
1811
  import { readFile as readFile7, writeFile as writeFile3 } from "fs/promises";
1812
1812
  import { basename, dirname as dirname5 } from "path";
1813
- var POLICY_VERSION = 6;
1813
+ var POLICY_VERSION = 7;
1814
1814
  var POLICY_BEGIN = `<!-- synthra-policy v${POLICY_VERSION} BEGIN -->`;
1815
1815
  var POLICY_END = `<!-- synthra-policy v${POLICY_VERSION} END -->`;
1816
1816
  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;
@@ -1826,9 +1826,12 @@ function policyBlock() {
1826
1826
  "",
1827
1827
  "> **Tool namespace.** Synthra's MCP tools are exposed as",
1828
1828
  "> `mcp__synthra__graph_continue`, `mcp__synthra__graph_read`, and",
1829
- "> `mcp__synthra__graph_register_edit`. Below they are referred to by",
1830
- "> their short names (`graph_continue` etc.) for readability \u2014 use the",
1831
- "> full namespaced form when actually invoking them.",
1829
+ "> `mcp__synthra__graph_register_edit`. **Short names will NOT resolve**",
1830
+ "> in ToolSearch or invocation \u2014 always use the full namespaced form.",
1831
+ "> If the tools are deferred, load their schemas with ToolSearch:",
1832
+ "> `select:mcp__synthra__graph_continue,mcp__synthra__graph_read,mcp__synthra__graph_register_edit`.",
1833
+ "> Below, short names (`graph_continue` etc.) appear in prose for",
1834
+ "> readability only.",
1832
1835
  "",
1833
1836
  "### Tools",
1834
1837
  "",
@@ -1857,8 +1860,8 @@ function policyBlock() {
1857
1860
  " test, docs, cleanup, commit)",
1858
1861
  "- The task is pure text (commit message, explanation, summary)",
1859
1862
  "",
1860
- 'If skipping, go directly to `graph_read("file.ts::symbol")` on what',
1861
- "you already know.",
1863
+ "If skipping, go directly to",
1864
+ '`mcp__synthra__graph_read("file.ts::symbol")` on what you already know.',
1862
1865
  "",
1863
1866
  "### Confidence caps",
1864
1867
  "",
@@ -1867,8 +1870,8 @@ function policyBlock() {
1867
1870
  "- **`Confidence: high`** \u2192 Stop. Do NOT Grep, Glob, or further explore",
1868
1871
  " for this query. The graph already has it.",
1869
1872
  "- **`Confidence: medium`** \u2192 Read the listed `Files` directly via",
1870
- ' `graph_read("file::symbol")` *before* trying Grep. The graph has',
1871
- " narrowed the search space \u2014 use it, don't bypass it.",
1873
+ ' `mcp__synthra__graph_read("file::symbol")` *before* trying Grep. The',
1874
+ " graph has narrowed the search space \u2014 use it, don't bypass it.",
1872
1875
  "- **`Confidence: low`** \u2192 You may use Grep / Glob, but the PreToolUse",
1873
1876
  " hook may still block redundant calls.",
1874
1877
  "",
@@ -1879,8 +1882,9 @@ function policyBlock() {
1879
1882
  "- If `graph_continue`'s `Files` list contains a `::` entry, pass it",
1880
1883
  " verbatim to `graph_read`.",
1881
1884
  "- **Large file?** Don't read it in successive line-range chunks \u2014 call",
1882
- ' `graph_continue` or `graph_read("file::symbol")` to pull the one symbol',
1883
- " you need. Chunked whole-file Reads are exactly the cost `graph_read`",
1885
+ " `mcp__synthra__graph_continue` or",
1886
+ ' `mcp__synthra__graph_read("file::symbol")` to pull the one symbol you',
1887
+ " need. Chunked whole-file Reads are exactly the cost `graph_read`",
1884
1888
  " exists to avoid.",
1885
1889
  "",
1886
1890
  "### Editing a file",
@@ -1908,9 +1912,10 @@ function policyBlock() {
1908
1912
  "decisions carried over from the previous session. **Trust it.** It is the",
1909
1913
  "cheapest possible orientation: do NOT re-run `graph_continue` or Grep just",
1910
1914
  'to rediscover "what were we doing / what changed" \u2014 that work is already',
1911
- 'done. For the concrete next steps, `context_recall({kind:"next"})` returns',
1912
- "them verbatim. Only reach for fresh retrieval when the task moves beyond",
1913
- "what the digest covers.",
1915
+ "done. For the concrete next steps,",
1916
+ '`mcp__synthra__context_recall({kind:"next"})` returns them verbatim. Only',
1917
+ "reach for fresh retrieval when the task moves beyond what the digest",
1918
+ "covers.",
1914
1919
  "",
1915
1920
  "### Session-end resume note",
1916
1921
  "",
@@ -3547,6 +3552,32 @@ async function handleContextUpdate(req, ctx) {
3547
3552
  // src/server/routes/gate.ts
3548
3553
  import { appendFile as appendFile4, mkdir as mkdir10 } from "fs/promises";
3549
3554
  import { dirname as dirname11 } from "path";
3555
+
3556
+ // src/shared/config.ts
3557
+ function num(name, fallback) {
3558
+ const v = process.env[name];
3559
+ if (!v) return fallback;
3560
+ const n = Number(v);
3561
+ return Number.isFinite(n) ? n : fallback;
3562
+ }
3563
+ function str(name, fallback) {
3564
+ return process.env[name] ?? fallback;
3565
+ }
3566
+ function loadConfig() {
3567
+ return {
3568
+ hardMaxReadChars: num("SYN_HARD_MAX_READ_CHARS", 4e3),
3569
+ gateHintMaxChars: num("SYN_GATE_HINT_CHARS", 1200),
3570
+ turnReadBudgetChars: num("SYN_TURN_READ_BUDGET_CHARS", 18e3),
3571
+ fallbackMaxCallsPerTurn: num("SYN_FALLBACK_MAX_CALLS_PER_TURN", 1),
3572
+ retrieveCacheTtlSec: num("SYN_RETRIEVE_CACHE_TTL_SEC", 900),
3573
+ mcpPort: process.env.SYN_MCP_PORT ? num("SYN_MCP_PORT", 0) : null,
3574
+ dashboardPort: num("SYN_DASHBOARD_PORT", 8901),
3575
+ logLevel: str("SYN_LOG_LEVEL", "info"),
3576
+ claudeBin: str("SYN_CLAUDE_BIN", "claude")
3577
+ };
3578
+ }
3579
+
3580
+ // src/server/routes/gate.ts
3550
3581
  var BLOCKABLE_TOOLS = /* @__PURE__ */ new Set(["Grep", "Glob"]);
3551
3582
  var RECENT_ACTIVITY_WINDOW_MS = 5 * 60 * 1e3;
3552
3583
  function extractQuery(toolName, input) {
@@ -3600,7 +3631,8 @@ function recentlyTouchedMatchesQuery(recentPaths, queryTokens, graph) {
3600
3631
  }
3601
3632
  return matches;
3602
3633
  }
3603
- async function logDecision(ctx, toolName, query, decision, reason) {
3634
+ var LOG_REASON_MAX_CHARS = 240;
3635
+ async function logDecision(ctx, toolName, query, decision, reason, hintChars) {
3604
3636
  try {
3605
3637
  await mkdir10(dirname11(ctx.paths.gateLog), { recursive: true });
3606
3638
  const entry = {
@@ -3608,12 +3640,70 @@ async function logDecision(ctx, toolName, query, decision, reason) {
3608
3640
  tool: toolName,
3609
3641
  decision,
3610
3642
  query,
3611
- reason
3643
+ reason: reason && reason.length > LOG_REASON_MAX_CHARS ? `${reason.slice(0, LOG_REASON_MAX_CHARS)}\u2026` : reason,
3644
+ ...hintChars === void 0 ? {} : { hint_chars: hintChars }
3612
3645
  };
3613
3646
  await appendFile4(ctx.paths.gateLog, JSON.stringify(entry) + "\n", "utf8");
3614
3647
  } catch {
3615
3648
  }
3616
3649
  }
3650
+ var SIG_LINE_MAX_CHARS = 140;
3651
+ function scoreSymbolName(name, qTokens) {
3652
+ const lower = name.toLowerCase();
3653
+ let score2 = 0;
3654
+ for (const t of qTokens) {
3655
+ if (t === lower) score2 += 3;
3656
+ else if (t.length >= 3 && lower.includes(t)) score2 += 1;
3657
+ }
3658
+ return score2;
3659
+ }
3660
+ function buildBlockHint(query, retrieval, graph, toolName, maxChars = loadConfig().gateHintMaxChars) {
3661
+ const topFiles = retrieval.files.slice(0, 3);
3662
+ const topPaths = new Set(topFiles.map((f) => f.path));
3663
+ const symsByFile = /* @__PURE__ */ new Map();
3664
+ for (const n of graph.nodes) {
3665
+ if (n.kind !== "symbol" || !topPaths.has(n.file)) continue;
3666
+ const list = symsByFile.get(n.file);
3667
+ if (list) list.push(n);
3668
+ else symsByFile.set(n.file, [n]);
3669
+ }
3670
+ const qTokens = tokenizeQuery(query);
3671
+ const entries = [];
3672
+ for (const f of topFiles) {
3673
+ const syms = (symsByFile.get(f.path) ?? []).slice().sort((a, b) => a.start_line - b.start_line);
3674
+ if (syms.length === 0) {
3675
+ entries.push(`\u2022 mcp__synthra__graph_read("${f.path}")`);
3676
+ continue;
3677
+ }
3678
+ const scored = syms.map((s) => ({ s, score: scoreSymbolName(s.name, qTokens) })).filter((x) => x.score > 0).sort((a, b) => b.score - a.score);
3679
+ const picks = scored.length > 0 ? scored.slice(0, 2).map((x) => x.s) : syms.slice(0, 1);
3680
+ for (const s of picks) {
3681
+ const sig = `L${s.start_line}: ${s.signature.trim()}`;
3682
+ const sigLine = sig.length > SIG_LINE_MAX_CHARS ? `${sig.slice(0, SIG_LINE_MAX_CHARS - 1)}\u2026` : sig;
3683
+ entries.push(`\u2022 mcp__synthra__graph_read("${f.path}::${s.name}")
3684
+ ${sigLine}`);
3685
+ }
3686
+ }
3687
+ const header = `Synthra blocked this ${toolName} \u2014 ${retrieval.confidence}-confidence context for "${query}" already exists.
3688
+ Read symbols directly (~50 tokens each) instead of whole files:
3689
+ `;
3690
+ const footer = `
3691
+ Full pack: mcp__synthra__graph_continue("${query}")`;
3692
+ const parts = [];
3693
+ let used = header.length + footer.length + 1;
3694
+ for (const e of entries) {
3695
+ if (used + e.length + 1 > maxChars) break;
3696
+ parts.push(e);
3697
+ used += e.length + 1;
3698
+ }
3699
+ if (parts.length === 0) {
3700
+ const top = topFiles.map((f) => f.path).join(", ");
3701
+ return `Synthra has ${retrieval.confidence}-confidence context for "${query}" (top files: ${top}). Use mcp__synthra__graph_continue("${query}") instead of ${toolName}, or read a specific file/symbol with mcp__synthra__graph_read.`;
3702
+ }
3703
+ return `${header}
3704
+ ${parts.join("\n")}
3705
+ ${footer}`;
3706
+ }
3617
3707
  async function handleGate(req, ctx) {
3618
3708
  if (!req?.tool_name || typeof req.tool_name !== "string") {
3619
3709
  return { decision: "allow", reason: "no tool_name" };
@@ -3664,12 +3754,9 @@ async function handleGate(req, ctx) {
3664
3754
  await logDecision(ctx, req.tool_name, query, res2.decision, res2.reason);
3665
3755
  return res2;
3666
3756
  }
3667
- const top = retrieval.files.slice(0, 3).map((f) => f.path).join(", ");
3668
- const res = {
3669
- decision: "block",
3670
- reason: `Synthra has ${retrieval.confidence}-confidence context for "${query}" (top files: ${top}). Use the \`graph_continue\` MCP tool with this query instead of ${req.tool_name}, or read a specific file/symbol with \`graph_read\`.`
3671
- };
3672
- await logDecision(ctx, req.tool_name, query, res.decision, res.reason);
3757
+ const hint = buildBlockHint(query, retrieval, ctx.graph, req.tool_name);
3758
+ const res = { decision: "block", reason: hint };
3759
+ await logDecision(ctx, req.tool_name, query, res.decision, hint, hint.length);
3673
3760
  return res;
3674
3761
  }
3675
3762