@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.
- package/CHANGELOG.md +23 -0
- package/dist/cli/index.js +109 -45
- 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 +108 -21
- package/dist/server/index.js.map +1 -1
- package/package.json +1 -1
package/dist/server/index.js
CHANGED
|
@@ -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 =
|
|
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`.
|
|
1830
|
-
">
|
|
1831
|
-
">
|
|
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
|
-
|
|
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
|
-
' `
|
|
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
|
-
|
|
1883
|
-
"
|
|
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
|
-
|
|
1912
|
-
"them verbatim. Only
|
|
1913
|
-
"what the digest
|
|
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
|
-
|
|
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
|
|
3668
|
-
const res = {
|
|
3669
|
-
|
|
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
|
|