@jefuriiij/synthra 0.1.22 → 0.1.23

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,52 @@ For older versions, see [GitHub Releases](https://github.com/jefuriiij/synthra/r
7
7
 
8
8
  ---
9
9
 
10
+ ## [0.1.23] — 2026-06-06
11
+
12
+ ### Added
13
+
14
+ - **Dashboard token-log dedupe can now be disabled via `SYN_DASHBOARD_DEDUPE=0`.**
15
+ By default, `delta.ts` deduplicates `token_log.jsonl` entries that share the
16
+ same project, usage totals, and second-rounded timestamp — collapsing the
17
+ duplicate writes that a co-installed AI tool's Stop hook may produce. Set
18
+ `SYN_DASHBOARD_DEDUPE=0` (also accepts `off` or `false`) to see every raw
19
+ entry. Useful when debugging multi-tool coexistence or auditing raw log data.
20
+
21
+ - **Graph schema-migration check on load.** A new `SCHEMA_VERSION` constant is
22
+ exported from `src/graph/types.ts` and stamped into `info_graph.json` by
23
+ `buildGraph`. On server start, `http.ts` compares the stored graph's
24
+ `schema_version` to the current constant; if they differ it triggers an
25
+ automatic one-time rescan instead of serving an incompatible graph. No
26
+ behavior change today — all graphs are v1 and schema_version matches — but
27
+ this is the forward-safety mechanism for future schema bumps so existing
28
+ graphs are never silently misread.
29
+
30
+ ### Fixed
31
+
32
+ - **JS/TS parser now captures member-assigned functions** (`exports.handler = fn`,
33
+ `module.exports.route = () => {}`, `this.x = () => {}`). Previously these
34
+ CommonJS/member-export patterns were invisible to the query, so modules that
35
+ exclusively use this style extracted zero symbols and degraded to whole-file
36
+ reads via `graph_read`. A member-assignment capture has been added to both
37
+ `JS_QUERY` and `TS_QUERY` in `src/scanner/parsers/typescript.ts`. Note: a
38
+ pure-wiring `server.js` whose only structure is anonymous inline-callback
39
+ arguments (e.g. `io.use(...)` / `socket.on(event, fn)`) is genuinely
40
+ symbol-less — that is correct, and the gate's symbol-hit guard already
41
+ prevents blocking such files.
42
+
43
+ ### Changed
44
+
45
+ - **Policy block v4 → v5.** Adds a "large file — pull the symbol, don't
46
+ chunk" nudge to address recurring dogfood friction: on large files Claude
47
+ was reading successive line-range chunks instead of fetching the specific
48
+ symbol via `graph_read("file::symbol")`. The v5 block now explicitly
49
+ instructs: when a file is large, use `graph_read("file/path.ts::SymbolName")`
50
+ to pull the symbol directly rather than reading successive line-range chunks.
51
+ `POLICY_VERSION` bumped `4 → 5`; existing v4 blocks auto-upgrade on the
52
+ next `syn .` run.
53
+
54
+ ---
55
+
10
56
  ## [0.1.22] — 2026-06-06
11
57
 
12
58
  ### Fixed
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.1.22",
21
+ version: "0.1.23",
22
22
  publishConfig: {
23
23
  access: "public"
24
24
  },
@@ -297,13 +297,18 @@ function summarize(p) {
297
297
  models
298
298
  };
299
299
  }
300
+ function dedupeEnabled() {
301
+ const v = process.env.SYN_DASHBOARD_DEDUPE;
302
+ return v !== "0" && v !== "off" && v !== "false";
303
+ }
300
304
  async function loadProjectFiles(path, name, lastSeen) {
301
305
  const paths = resolvePaths(path);
302
306
  const [rawTokens, gates] = await Promise.all([
303
307
  readJsonl(paths.tokenLog),
304
308
  readJsonl(paths.gateLog)
305
309
  ]);
306
- return { path, name, last_seen: lastSeen, tokens: dedupeTokens(rawTokens), gates };
310
+ const tokens = dedupeEnabled() ? dedupeTokens(rawTokens) : rawTokens;
311
+ return { path, name, last_seen: lastSeen, tokens, gates };
307
312
  }
308
313
  function dedupeTokens(entries) {
309
314
  const score2 = (model) => {
@@ -1401,33 +1406,33 @@ exit 0
1401
1406
  var pre_tool_use_default = '# PreToolUse hook \u2014 Windows PowerShell.\n# THE MOAT (improvement #1). Reads the tool call from stdin (JSON), POSTs it\n# to /gate, and if the server says "block" emits a JSON deny-decision to\n# stdout. Claude Code reads stdout JSON to enforce the decision.\n# Always exits 0; failure-to-reach-server leaves Claude untouched.\n\n$ErrorActionPreference = "SilentlyContinue"\n\n$raw = [Console]::In.ReadToEnd()\nif (-not $raw) { exit 0 }\n\ntry {\n $hookInput = $raw | ConvertFrom-Json -ErrorAction Stop\n} catch {\n exit 0\n}\n\n$portFile = Join-Path $PWD ".synthra-graph\\mcp_port"\nif (-not (Test-Path $portFile)) { exit 0 }\n$port = (Get-Content -Path $portFile -Raw).Trim()\nif (-not $port) { exit 0 }\n\n$payload = @{\n tool_name = $hookInput.tool_name\n tool_input = $hookInput.tool_input\n} | ConvertTo-Json -Depth 10 -Compress\n\ntry {\n $resp = Invoke-RestMethod -Uri "http://127.0.0.1:$port/gate" -Method POST `\n -Body $payload -ContentType "application/json" -TimeoutSec 3\n} catch {\n exit 0\n}\n\nif ($resp.decision -eq "block") {\n $denyJson = @{\n hookSpecificOutput = @{\n hookEventName = "PreToolUse"\n permissionDecision = "deny"\n permissionDecisionReason = $resp.reason\n }\n } | ConvertTo-Json -Depth 5 -Compress\n Write-Output $denyJson\n}\nexit 0\n';
1402
1407
 
1403
1408
  // src/hooks/scripts/pre-tool-use.sh
1404
- var pre_tool_use_default2 = `#!/usr/bin/env bash
1405
- # PreToolUse hook \u2014 bash. POSTs the tool call to /gate; if server returns
1406
- # "block", emits the deny-decision JSON to stdout for Claude Code to enforce.
1407
- # Always exits 0; server failures leave Claude untouched.
1408
-
1409
- set +e
1410
-
1411
- PORT_FILE="$PWD/.synthra-graph/mcp_port"
1412
- if [ ! -f "$PORT_FILE" ]; then exit 0; fi
1413
- PORT=$(cat "$PORT_FILE" 2>/dev/null | tr -d '[:space:]')
1414
- if [ -z "$PORT" ]; then exit 0; fi
1415
-
1416
- INPUT=$(cat 2>/dev/null)
1417
- if [ -z "$INPUT" ]; then exit 0; fi
1418
-
1419
- RESP=$(curl -sS --max-time 3 -X POST -H "Content-Type: application/json" \\
1420
- --data "$INPUT" "http://127.0.0.1:$PORT/gate" 2>/dev/null)
1421
-
1422
- case "$RESP" in
1423
- *'"decision":"block"'*)
1424
- REASON=$(printf '%s' "$RESP" | sed -n 's/.*"reason"[[:space:]]*:[[:space:]]*"\\(.*\\)".*/\\1/p')
1425
- cat <<EOF
1426
- {"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"\${REASON}"}}
1427
- EOF
1428
- ;;
1429
- esac
1430
- exit 0
1409
+ var pre_tool_use_default2 = `#!/usr/bin/env bash\r
1410
+ # PreToolUse hook \u2014 bash. POSTs the tool call to /gate; if server returns\r
1411
+ # "block", emits the deny-decision JSON to stdout for Claude Code to enforce.\r
1412
+ # Always exits 0; server failures leave Claude untouched.\r
1413
+ \r
1414
+ set +e\r
1415
+ \r
1416
+ PORT_FILE="$PWD/.synthra-graph/mcp_port"\r
1417
+ if [ ! -f "$PORT_FILE" ]; then exit 0; fi\r
1418
+ PORT=$(cat "$PORT_FILE" 2>/dev/null | tr -d '[:space:]')\r
1419
+ if [ -z "$PORT" ]; then exit 0; fi\r
1420
+ \r
1421
+ INPUT=$(cat 2>/dev/null)\r
1422
+ if [ -z "$INPUT" ]; then exit 0; fi\r
1423
+ \r
1424
+ RESP=$(curl -sS --max-time 3 -X POST -H "Content-Type: application/json" \\\r
1425
+ --data "$INPUT" "http://127.0.0.1:$PORT/gate" 2>/dev/null)\r
1426
+ \r
1427
+ case "$RESP" in\r
1428
+ *'"decision":"block"'*)\r
1429
+ REASON=$(printf '%s' "$RESP" | sed -n 's/.*"reason"[[:space:]]*:[[:space:]]*"\\(.*\\)".*/\\1/p')\r
1430
+ cat <<EOF\r
1431
+ {"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"\${REASON}"}}\r
1432
+ EOF\r
1433
+ ;;\r
1434
+ esac\r
1435
+ exit 0\r
1431
1436
  `;
1432
1437
 
1433
1438
  // src/hooks/scripts/prime.ps1
@@ -1958,6 +1963,9 @@ import { resolve } from "path";
1958
1963
  // src/scanner/extract.ts
1959
1964
  import { dirname as dirname4, join as join6, posix } from "path";
1960
1965
 
1966
+ // src/graph/types.ts
1967
+ var SCHEMA_VERSION2 = 1;
1968
+
1961
1969
  // src/scanner/hash.ts
1962
1970
  import { createHash } from "crypto";
1963
1971
  function fileHash(content) {
@@ -2294,7 +2302,7 @@ async function buildGraph(root, parsed) {
2294
2302
  nodes,
2295
2303
  edges,
2296
2304
  generated_at: (/* @__PURE__ */ new Date()).toISOString(),
2297
- schema_version: 1
2305
+ schema_version: SCHEMA_VERSION2
2298
2306
  };
2299
2307
  }
2300
2308
  function buildSymbolIndex(graph) {
@@ -2828,6 +2836,7 @@ var TS_QUERY = `
2828
2836
  (enum_declaration name: (identifier) @enum.name) @enum
2829
2837
  (method_definition name: (property_identifier) @method.name) @method
2830
2838
  (lexical_declaration (variable_declarator name: (identifier) @const-fn.name value: [(arrow_function) (function_expression)])) @const-fn
2839
+ (assignment_expression left: (member_expression property: (property_identifier) @member-fn.name) right: [(arrow_function) (function_expression)]) @member-fn
2831
2840
  (import_statement source: (string) @import)
2832
2841
  `;
2833
2842
  var JS_QUERY = `
@@ -2835,6 +2844,7 @@ var JS_QUERY = `
2835
2844
  (class_declaration name: (identifier) @class.name) @class
2836
2845
  (method_definition name: (property_identifier) @method.name) @method
2837
2846
  (lexical_declaration (variable_declarator name: (identifier) @const-fn.name value: [(arrow_function) (function_expression)])) @const-fn
2847
+ (assignment_expression left: (member_expression property: (property_identifier) @member-fn.name) right: [(arrow_function) (function_expression)]) @member-fn
2838
2848
  (import_statement source: (string) @import)
2839
2849
  (call_expression function: (identifier) @_require_fn arguments: (arguments . (string) @require_source))
2840
2850
  `;
@@ -2859,7 +2869,7 @@ function shapeFromCaptures(captures) {
2859
2869
  const name = captures.get(`${k}.name`);
2860
2870
  return decl && name ? { decl, name, kind: sk } : null;
2861
2871
  };
2862
- return findDecl("function", "function") ?? findDecl("class", "class") ?? findDecl("interface", "interface") ?? findDecl("type", "type") ?? findDecl("enum", "enum") ?? findDecl("method", "method") ?? findDecl("const-fn", "function");
2872
+ return findDecl("function", "function") ?? findDecl("class", "class") ?? findDecl("interface", "interface") ?? findDecl("type", "type") ?? findDecl("enum", "enum") ?? findDecl("method", "method") ?? findDecl("const-fn", "function") ?? findDecl("member-fn", "function");
2863
2873
  }
2864
2874
  async function parseTypeScript(f, source) {
2865
2875
  const grammar = grammarFor(f.ext);
@@ -3267,7 +3277,7 @@ import { basename as basename4 } from "path";
3267
3277
  // src/hooks/claude-md.ts
3268
3278
  import { readFile as readFile9, writeFile as writeFile4 } from "fs/promises";
3269
3279
  import { basename as basename3, dirname as dirname6 } from "path";
3270
- var POLICY_VERSION = 4;
3280
+ var POLICY_VERSION = 5;
3271
3281
  var POLICY_BEGIN = `<!-- synthra-policy v${POLICY_VERSION} BEGIN -->`;
3272
3282
  var POLICY_END = `<!-- synthra-policy v${POLICY_VERSION} END -->`;
3273
3283
  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;
@@ -3335,6 +3345,10 @@ function policyBlock() {
3335
3345
  " reads should be rare \u2014 only when you genuinely need the full file.",
3336
3346
  "- If `graph_continue`'s `Files` list contains a `::` entry, pass it",
3337
3347
  " verbatim to `graph_read`.",
3348
+ "- **Large file?** Don't read it in successive line-range chunks \u2014 call",
3349
+ ' `graph_continue` or `graph_read("file::symbol")` to pull the one symbol',
3350
+ " you need. Chunked whole-file Reads are exactly the cost `graph_read`",
3351
+ " exists to avoid.",
3338
3352
  "",
3339
3353
  "### Editing a file",
3340
3354
  "",
@@ -3892,7 +3906,7 @@ async function writeContextMd(path, ctx) {
3892
3906
  // src/memory/context-store.ts
3893
3907
  import { mkdir as mkdir7, readFile as readFile13, writeFile as writeFile7 } from "fs/promises";
3894
3908
  import { dirname as dirname8 } from "path";
3895
- var SCHEMA_VERSION2 = 1;
3909
+ var SCHEMA_VERSION3 = 1;
3896
3910
  async function readEntries(path) {
3897
3911
  try {
3898
3912
  const raw = await readFile13(path, "utf8");
@@ -3904,7 +3918,7 @@ async function readEntries(path) {
3904
3918
  }
3905
3919
  async function writeEntries(path, entries) {
3906
3920
  await mkdir7(dirname8(path), { recursive: true });
3907
- const store = { schema_version: SCHEMA_VERSION2, entries };
3921
+ const store = { schema_version: SCHEMA_VERSION3, entries };
3908
3922
  await writeFile7(path, JSON.stringify(store, null, 2) + "\n", "utf8");
3909
3923
  }
3910
3924
  async function appendEntry(path, entry) {
@@ -4804,10 +4818,20 @@ ${fileCount} files indexed, ${symbolCount} symbols. Prefer the graph_* MCP tools
4804
4818
  // src/server/http.ts
4805
4819
  async function loadContext(paths) {
4806
4820
  try {
4807
- const [graph, symbolIndex] = await Promise.all([
4821
+ let [graph, symbolIndex] = await Promise.all([
4808
4822
  readGraph(paths.infoGraph),
4809
4823
  readSymbolIndex(paths.symbolIndex)
4810
4824
  ]);
4825
+ if (graph.schema_version !== SCHEMA_VERSION2) {
4826
+ log.info(
4827
+ `graph schema v${graph.schema_version} \u2260 current v${SCHEMA_VERSION2} \u2014 rescanning\u2026`
4828
+ );
4829
+ await scanProject(paths.projectRoot, { silent: true });
4830
+ [graph, symbolIndex] = await Promise.all([
4831
+ readGraph(paths.infoGraph),
4832
+ readSymbolIndex(paths.symbolIndex)
4833
+ ]);
4834
+ }
4811
4835
  const activity = new ActivityStore(paths.activityLog);
4812
4836
  return { paths, graph, symbolIndex, activity };
4813
4837
  } catch (err2) {