@jefuriiij/synthra 0.4.1 → 0.6.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 +37 -0
- package/dist/cli/index.js +110 -29
- 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 +109 -28
- package/dist/server/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,43 @@ For older versions, see [GitHub Releases](https://github.com/jefuriiij/synthra/r
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## [0.6.0] — 2026-06-13
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`graph_read` now delivers a symbol's dependency surface.** Reading a symbol
|
|
15
|
+
appends a footer built from the call graph: **Depends on** — the symbols it
|
|
16
|
+
calls, each with its full one-line signature and a `graph_read` target, so you
|
|
17
|
+
can edit against real signatures instead of guessing parameter shapes or
|
|
18
|
+
re-reading the callee files; and **Used by** — the names of the symbols that
|
|
19
|
+
call it, so a change's blast radius is visible at a glance. Budgeted via
|
|
20
|
+
`SYN_READ_DEPS_CHARS` (default 900); leaf symbols with no calls add nothing.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## [0.5.0] — 2026-06-13
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- **`graph_read` hands you the cheap edit recipe.** Reading a symbol slice now
|
|
29
|
+
ends with the exact targeted `Read(path, offset, limit)` (covering the symbol
|
|
30
|
+
plus a little headroom) that satisfies Claude Code's Edit read-gate, plus a
|
|
31
|
+
"do not re-read the whole file" nudge. A `graph_read` slice doesn't satisfy
|
|
32
|
+
the gate on its own, so editing a symbol used to force a whole-file Read —
|
|
33
|
+
and the same large file would get re-read many times across a session.
|
|
34
|
+
Delivering the recipe at the point of use (not just once in the session
|
|
35
|
+
primer) keeps edits cheap.
|
|
36
|
+
|
|
37
|
+
### Changed
|
|
38
|
+
|
|
39
|
+
- **The Moat stops wasting blocks on styling searches.** Grep/Glob patterns for
|
|
40
|
+
CSS custom properties (`var(--brand)`, `--sidebar`), hex color literals
|
|
41
|
+
(`#fff`), and all-kebab class names (`cw-code-chip`) now pass through instead
|
|
42
|
+
of being blocked and redirected to a graph the symbol index can't answer.
|
|
43
|
+
Mixed queries that also name a real symbol still block.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
10
47
|
## [0.4.1] — 2026-06-10
|
|
11
48
|
|
|
12
49
|
### 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.6.0",
|
|
22
22
|
publishConfig: {
|
|
23
23
|
access: "public"
|
|
24
24
|
},
|
|
@@ -4749,6 +4749,31 @@ async function pack(files, opts) {
|
|
|
4749
4749
|
};
|
|
4750
4750
|
}
|
|
4751
4751
|
|
|
4752
|
+
// src/shared/config.ts
|
|
4753
|
+
function num(name, fallback) {
|
|
4754
|
+
const v = process.env[name];
|
|
4755
|
+
if (!v) return fallback;
|
|
4756
|
+
const n = Number(v);
|
|
4757
|
+
return Number.isFinite(n) ? n : fallback;
|
|
4758
|
+
}
|
|
4759
|
+
function str(name, fallback) {
|
|
4760
|
+
return process.env[name] ?? fallback;
|
|
4761
|
+
}
|
|
4762
|
+
function loadConfig() {
|
|
4763
|
+
return {
|
|
4764
|
+
hardMaxReadChars: num("SYN_HARD_MAX_READ_CHARS", 4e3),
|
|
4765
|
+
gateHintMaxChars: num("SYN_GATE_HINT_CHARS", 1200),
|
|
4766
|
+
readDepsMaxChars: num("SYN_READ_DEPS_CHARS", 900),
|
|
4767
|
+
turnReadBudgetChars: num("SYN_TURN_READ_BUDGET_CHARS", 18e3),
|
|
4768
|
+
fallbackMaxCallsPerTurn: num("SYN_FALLBACK_MAX_CALLS_PER_TURN", 1),
|
|
4769
|
+
retrieveCacheTtlSec: num("SYN_RETRIEVE_CACHE_TTL_SEC", 900),
|
|
4770
|
+
mcpPort: process.env.SYN_MCP_PORT ? num("SYN_MCP_PORT", 0) : null,
|
|
4771
|
+
dashboardPort: num("SYN_DASHBOARD_PORT", 8901),
|
|
4772
|
+
logLevel: str("SYN_LOG_LEVEL", "info"),
|
|
4773
|
+
claudeBin: str("SYN_CLAUDE_BIN", "claude")
|
|
4774
|
+
};
|
|
4775
|
+
}
|
|
4776
|
+
|
|
4752
4777
|
// src/server/mcp.ts
|
|
4753
4778
|
var PROTOCOL_VERSION = "2024-11-05";
|
|
4754
4779
|
var SERVER_INFO = { name: "synthra", version: "0.0.1" };
|
|
@@ -4788,7 +4813,7 @@ var TOOLS = [
|
|
|
4788
4813
|
},
|
|
4789
4814
|
{
|
|
4790
4815
|
name: "graph_read",
|
|
4791
|
-
description: "Return the source code for a specific file or symbol. Target is either a project-relative file path (e.g. 'src/auth.ts') or 'file::symbol' (e.g. 'src/auth.ts::AuthService').",
|
|
4816
|
+
description: "Return the source code for a specific file or symbol. Target is either a project-relative file path (e.g. 'src/auth.ts') or 'file::symbol' (e.g. 'src/auth.ts::AuthService'). A symbol read also returns its dependency surface \u2014 the signatures of the symbols it calls (edit against these instead of guessing or re-reading their files) and the names of the symbols that call it.",
|
|
4792
4817
|
inputSchema: {
|
|
4793
4818
|
type: "object",
|
|
4794
4819
|
properties: {
|
|
@@ -5065,6 +5090,72 @@ function resolveFileTarget(graph, filePath) {
|
|
|
5065
5090
|
if (matches.length > 1) return { ambiguous: matches.map((n) => n.path) };
|
|
5066
5091
|
return { none: true };
|
|
5067
5092
|
}
|
|
5093
|
+
var DEPS_SIG_MAX = 140;
|
|
5094
|
+
var DEPS_MAX_CALLEES = 10;
|
|
5095
|
+
var DEPS_MAX_CALLERS = 12;
|
|
5096
|
+
function buildDepsFooter(symbol, graph, maxChars = loadConfig().readDepsMaxChars) {
|
|
5097
|
+
const symById = /* @__PURE__ */ new Map();
|
|
5098
|
+
for (const n of graph.nodes) if (n.kind === "symbol") symById.set(n.id, n);
|
|
5099
|
+
const calleeIds = [];
|
|
5100
|
+
const callerIds = [];
|
|
5101
|
+
const seenCallee = /* @__PURE__ */ new Set();
|
|
5102
|
+
const seenCaller = /* @__PURE__ */ new Set();
|
|
5103
|
+
for (const e of graph.edges) {
|
|
5104
|
+
if (e.kind !== "calls") continue;
|
|
5105
|
+
if (e.from === symbol.id && e.to !== symbol.id && !seenCallee.has(e.to)) {
|
|
5106
|
+
seenCallee.add(e.to);
|
|
5107
|
+
calleeIds.push(e.to);
|
|
5108
|
+
} else if (e.to === symbol.id && e.from !== symbol.id && !seenCaller.has(e.from)) {
|
|
5109
|
+
seenCaller.add(e.from);
|
|
5110
|
+
callerIds.push(e.from);
|
|
5111
|
+
}
|
|
5112
|
+
}
|
|
5113
|
+
const resolve6 = (ids) => ids.map((id) => symById.get(id)).filter((n) => !!n);
|
|
5114
|
+
const callees = resolve6(calleeIds).sort(
|
|
5115
|
+
(a, b) => a.file === b.file ? a.start_line - b.start_line : a.file < b.file ? -1 : 1
|
|
5116
|
+
);
|
|
5117
|
+
const callers = resolve6(callerIds);
|
|
5118
|
+
if (callees.length === 0 && callers.length === 0) return "";
|
|
5119
|
+
const lines = [];
|
|
5120
|
+
let used = 0;
|
|
5121
|
+
if (callees.length > 0) {
|
|
5122
|
+
const head = "Depends on (signatures \u2014 don't guess these):";
|
|
5123
|
+
lines.push(head);
|
|
5124
|
+
used += head.length + 1;
|
|
5125
|
+
let shown = 0;
|
|
5126
|
+
for (const c of callees.slice(0, DEPS_MAX_CALLEES)) {
|
|
5127
|
+
const sig = c.signature.trim().slice(0, DEPS_SIG_MAX);
|
|
5128
|
+
const entry = `\u2022 ${sig} \u2192 mcp__synthra__graph_read("${c.file}::${c.name}")`;
|
|
5129
|
+
if (used + entry.length + 1 > maxChars) break;
|
|
5130
|
+
lines.push(entry);
|
|
5131
|
+
used += entry.length + 1;
|
|
5132
|
+
shown += 1;
|
|
5133
|
+
}
|
|
5134
|
+
const omitted = callees.length - shown;
|
|
5135
|
+
if (omitted > 0) lines.push(`\u2026+${omitted} more`);
|
|
5136
|
+
}
|
|
5137
|
+
if (callers.length > 0) {
|
|
5138
|
+
const sep3 = lines.length > 0 ? 1 : 0;
|
|
5139
|
+
const head = `Used by (${callers.length}): `;
|
|
5140
|
+
const shown = [];
|
|
5141
|
+
let cUsed = used + sep3 + head.length;
|
|
5142
|
+
for (const c of callers.slice(0, DEPS_MAX_CALLERS)) {
|
|
5143
|
+
const part = `${c.name} \u2192 ${c.file}`;
|
|
5144
|
+
const join12 = shown.length > 0 ? 3 : 0;
|
|
5145
|
+
if (cUsed + join12 + part.length > maxChars) break;
|
|
5146
|
+
shown.push(part);
|
|
5147
|
+
cUsed += join12 + part.length;
|
|
5148
|
+
}
|
|
5149
|
+
if (lines.length > 0) lines.push("");
|
|
5150
|
+
if (shown.length > 0) {
|
|
5151
|
+
const omitted = callers.length - shown.length;
|
|
5152
|
+
lines.push(head + shown.join(" \xB7 ") + (omitted > 0 ? ` \u2026+${omitted} more` : ""));
|
|
5153
|
+
} else {
|
|
5154
|
+
lines.push(`Used by (${callers.length} callers)`);
|
|
5155
|
+
}
|
|
5156
|
+
}
|
|
5157
|
+
return lines.join("\n");
|
|
5158
|
+
}
|
|
5068
5159
|
async function graphRead(args, ctx) {
|
|
5069
5160
|
const target = typeof args?.target === "string" ? args.target : "";
|
|
5070
5161
|
if (!target) return errorContent("graph_read: 'target' (string) is required");
|
|
@@ -5097,10 +5188,21 @@ ${fileNode.content}`);
|
|
|
5097
5188
|
}
|
|
5098
5189
|
const lines = fileNode.content.split(/\r?\n/);
|
|
5099
5190
|
const body = lines.slice(symbol.start_line - 1, symbol.end_line).join("\n");
|
|
5191
|
+
const offset = Math.max(1, symbol.start_line - 2);
|
|
5192
|
+
const limit = symbol.end_line - symbol.start_line + 1 + 4;
|
|
5193
|
+
const editHint = `
|
|
5194
|
+
|
|
5195
|
+
---
|
|
5196
|
+
\u270E To edit this symbol: Read("${fileNode.path}", offset=${offset}, limit=${limit}) then Edit \u2014 that satisfies Claude Code's read-gate at ~${limit} lines; do NOT re-read the whole file.`;
|
|
5197
|
+
const deps = buildDepsFooter(symbol, ctx.graph);
|
|
5198
|
+
const depsBlock = deps ? `
|
|
5199
|
+
|
|
5200
|
+
---
|
|
5201
|
+
${deps}` : "";
|
|
5100
5202
|
return textContent(
|
|
5101
5203
|
`# ${fileNode.path}::${symbol.name} (L${symbol.start_line}-${symbol.end_line})
|
|
5102
5204
|
|
|
5103
|
-
${body}`
|
|
5205
|
+
${body}${depsBlock}${editHint}`
|
|
5104
5206
|
);
|
|
5105
5207
|
}
|
|
5106
5208
|
var editedFiles = /* @__PURE__ */ new Set();
|
|
@@ -5347,32 +5449,6 @@ async function handleContextUpdate(req, ctx) {
|
|
|
5347
5449
|
// src/server/routes/gate.ts
|
|
5348
5450
|
import { appendFile as appendFile4, mkdir as mkdir12 } from "fs/promises";
|
|
5349
5451
|
import { dirname as dirname13 } from "path";
|
|
5350
|
-
|
|
5351
|
-
// src/shared/config.ts
|
|
5352
|
-
function num(name, fallback) {
|
|
5353
|
-
const v = process.env[name];
|
|
5354
|
-
if (!v) return fallback;
|
|
5355
|
-
const n = Number(v);
|
|
5356
|
-
return Number.isFinite(n) ? n : fallback;
|
|
5357
|
-
}
|
|
5358
|
-
function str(name, fallback) {
|
|
5359
|
-
return process.env[name] ?? fallback;
|
|
5360
|
-
}
|
|
5361
|
-
function loadConfig() {
|
|
5362
|
-
return {
|
|
5363
|
-
hardMaxReadChars: num("SYN_HARD_MAX_READ_CHARS", 4e3),
|
|
5364
|
-
gateHintMaxChars: num("SYN_GATE_HINT_CHARS", 1200),
|
|
5365
|
-
turnReadBudgetChars: num("SYN_TURN_READ_BUDGET_CHARS", 18e3),
|
|
5366
|
-
fallbackMaxCallsPerTurn: num("SYN_FALLBACK_MAX_CALLS_PER_TURN", 1),
|
|
5367
|
-
retrieveCacheTtlSec: num("SYN_RETRIEVE_CACHE_TTL_SEC", 900),
|
|
5368
|
-
mcpPort: process.env.SYN_MCP_PORT ? num("SYN_MCP_PORT", 0) : null,
|
|
5369
|
-
dashboardPort: num("SYN_DASHBOARD_PORT", 8901),
|
|
5370
|
-
logLevel: str("SYN_LOG_LEVEL", "info"),
|
|
5371
|
-
claudeBin: str("SYN_CLAUDE_BIN", "claude")
|
|
5372
|
-
};
|
|
5373
|
-
}
|
|
5374
|
-
|
|
5375
|
-
// src/server/routes/gate.ts
|
|
5376
5452
|
var BLOCKABLE_TOOLS = /* @__PURE__ */ new Set(["Grep", "Glob"]);
|
|
5377
5453
|
var RECENT_ACTIVITY_WINDOW_MS = 5 * 60 * 1e3;
|
|
5378
5454
|
function extractQuery(toolName, input) {
|
|
@@ -5395,6 +5471,11 @@ function looksLikeNonSymbolQuery(pattern) {
|
|
|
5395
5471
|
if (/:\s*\d/.test(pattern) || /\d(?:px|rem|em|vh|vw)\b/.test(pattern) || /\d%/.test(pattern)) {
|
|
5396
5472
|
return true;
|
|
5397
5473
|
}
|
|
5474
|
+
if (/--[a-zA-Z]/.test(pattern)) return true;
|
|
5475
|
+
if (/#[0-9a-fA-F]{3,8}\b/.test(pattern)) return true;
|
|
5476
|
+
const branches = pattern.replace(/\[[^\]]*\]/g, "").split("|").map((b) => b.trim()).filter(Boolean);
|
|
5477
|
+
const isKebab = (b) => /^[a-z][a-z0-9]*(?:-[a-z0-9]+)+$/i.test(b);
|
|
5478
|
+
if (branches.length > 0 && branches.every(isKebab)) return true;
|
|
5398
5479
|
return false;
|
|
5399
5480
|
}
|
|
5400
5481
|
function recentlyTouchedMatchesQuery(recentPaths, queryTokens, graph) {
|