@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 +46 -0
- package/dist/cli/index.js +59 -35
- package/dist/cli/index.js.map +1 -1
- package/dist/dashboard/index.js +7 -2
- package/dist/dashboard/index.js.map +1 -1
- package/dist/server/index.js +25 -6
- package/dist/server/index.js.map +1 -1
- package/package.json +1 -1
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.
|
|
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
|
-
|
|
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:
|
|
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 =
|
|
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
|
|
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:
|
|
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
|
-
|
|
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) {
|