@jefuriiij/synthra 0.5.0 → 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 CHANGED
@@ -7,6 +7,20 @@ 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
+
10
24
  ## [0.5.0] — 2026-06-13
11
25
 
12
26
  ### 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.5.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");
@@ -5103,10 +5194,15 @@ ${fileNode.content}`);
5103
5194
 
5104
5195
  ---
5105
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}` : "";
5106
5202
  return textContent(
5107
5203
  `# ${fileNode.path}::${symbol.name} (L${symbol.start_line}-${symbol.end_line})
5108
5204
 
5109
- ${body}${editHint}`
5205
+ ${body}${depsBlock}${editHint}`
5110
5206
  );
5111
5207
  }
5112
5208
  var editedFiles = /* @__PURE__ */ new Set();
@@ -5353,32 +5449,6 @@ async function handleContextUpdate(req, ctx) {
5353
5449
  // src/server/routes/gate.ts
5354
5450
  import { appendFile as appendFile4, mkdir as mkdir12 } from "fs/promises";
5355
5451
  import { dirname as dirname13 } from "path";
5356
-
5357
- // src/shared/config.ts
5358
- function num(name, fallback) {
5359
- const v = process.env[name];
5360
- if (!v) return fallback;
5361
- const n = Number(v);
5362
- return Number.isFinite(n) ? n : fallback;
5363
- }
5364
- function str(name, fallback) {
5365
- return process.env[name] ?? fallback;
5366
- }
5367
- function loadConfig() {
5368
- return {
5369
- hardMaxReadChars: num("SYN_HARD_MAX_READ_CHARS", 4e3),
5370
- gateHintMaxChars: num("SYN_GATE_HINT_CHARS", 1200),
5371
- turnReadBudgetChars: num("SYN_TURN_READ_BUDGET_CHARS", 18e3),
5372
- fallbackMaxCallsPerTurn: num("SYN_FALLBACK_MAX_CALLS_PER_TURN", 1),
5373
- retrieveCacheTtlSec: num("SYN_RETRIEVE_CACHE_TTL_SEC", 900),
5374
- mcpPort: process.env.SYN_MCP_PORT ? num("SYN_MCP_PORT", 0) : null,
5375
- dashboardPort: num("SYN_DASHBOARD_PORT", 8901),
5376
- logLevel: str("SYN_LOG_LEVEL", "info"),
5377
- claudeBin: str("SYN_CLAUDE_BIN", "claude")
5378
- };
5379
- }
5380
-
5381
- // src/server/routes/gate.ts
5382
5452
  var BLOCKABLE_TOOLS = /* @__PURE__ */ new Set(["Grep", "Glob"]);
5383
5453
  var RECENT_ACTIVITY_WINDOW_MS = 5 * 60 * 1e3;
5384
5454
  function extractQuery(toolName, input) {