@jefuriiij/synthra 0.1.20 → 0.1.22
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 +58 -0
- package/dist/cli/index.js +102 -14
- package/dist/cli/index.js.map +1 -1
- package/dist/dashboard/index.js +4 -4
- package/dist/dashboard/index.js.map +1 -1
- package/dist/server/index.js +98 -10
- package/dist/server/index.js.map +1 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,64 @@ For older versions, see [GitHub Releases](https://github.com/jefuriiij/synthra/r
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## [0.1.22] — 2026-06-06
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **`graph_read` now resolves shortened file paths (path-suffix fallback).** Previously
|
|
15
|
+
`graph_read` performed an exact `path === target` match only. Passing a shortened path
|
|
16
|
+
like `appsettings.json` returned "file not found" even when
|
|
17
|
+
`connectwarev2/.../appsettings.json` was indexed. A new `resolveFileTarget` helper (now
|
|
18
|
+
exported) tries an exact match first; on a miss it looks for a unique path-suffix match
|
|
19
|
+
and serves that file; if multiple files share the suffix it reports them as ambiguous with
|
|
20
|
+
candidate paths rather than guessing. Symbol lookups use the resolved path. No API or
|
|
21
|
+
protocol change. Roadmap item #11.
|
|
22
|
+
|
|
23
|
+
- **Gate content-keyword relaxation now intersects file contents, not just file paths.**
|
|
24
|
+
The Moat's recent-activity relaxation previously matched query tokens against the paths of
|
|
25
|
+
recently-touched files only. A query like `Grep "login"` would not relax on a recent save
|
|
26
|
+
of `auth.ts` unless the word "login" appeared in the path. Now the relaxation also checks
|
|
27
|
+
the recently-touched file's graph-node keywords (its indexed content), so a recent save
|
|
28
|
+
relaxes a Grep whenever the file *contains* the queried term — not just when the path
|
|
29
|
+
matches it. Completes roadmap item #3.
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
|
|
33
|
+
- **Dashboard Projects card shows a first-run hint in the empty state.** When no projects
|
|
34
|
+
have run `syn .` yet, the Projects card now displays "No projects yet — run `syn .` in a
|
|
35
|
+
project to start" instead of a blank card. The Recent-turns card already carried this
|
|
36
|
+
hint; Projects now matches it. Roadmap item #10.
|
|
37
|
+
|
|
38
|
+
- **`bin` path normalization (chore).** Ran `npm pkg fix` to normalize `bin` entries from
|
|
39
|
+
`./bin/syn` to `bin/syn`. Silences the cosmetic publish warnings
|
|
40
|
+
(`"bin[syn]" script name was cleaned`). `syn` and `synthra` still resolve to the same
|
|
41
|
+
entry point. Roadmap item #4.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## [0.1.21] — 2026-06-06
|
|
46
|
+
|
|
47
|
+
### Added
|
|
48
|
+
|
|
49
|
+
- **HubL (HubSpot CMS) symbol extraction for `.html` and `.hubl` files.**
|
|
50
|
+
Previously `.html` files were content-indexed only — keyword search and
|
|
51
|
+
whole-file reads, no symbol-level granularity. On HubSpot projects this
|
|
52
|
+
meant the graph contributed nothing: zero `graph_continue`/`graph_read`
|
|
53
|
+
calls resolved to symbol slices all session. Now `.html` and `.hubl` files
|
|
54
|
+
run through a new **regex-based** parser (`parsers/hubl.ts`; there is no
|
|
55
|
+
tree-sitter grammar for HubL):
|
|
56
|
+
- `{% macro name(args) %}` → extracted as a `function` symbol
|
|
57
|
+
- `{% block name %}` → extracted as a `component` symbol
|
|
58
|
+
- `{% include / extends / import / from "path" %}` → import edges (relative
|
|
59
|
+
paths resolve to local templates; `.html`/`.hubl` added to the resolver's
|
|
60
|
+
extension list)
|
|
61
|
+
|
|
62
|
+
Plain HTML with no HubL tags is unaffected — the parser yields zero symbols
|
|
63
|
+
and zero imports, identical to before. No API, protocol, or policy-block
|
|
64
|
+
change. Roadmap item #12.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
10
68
|
## [0.1.20] — 2026-06-06
|
|
11
69
|
|
|
12
70
|
### Fixed
|
package/dist/cli/index.js
CHANGED
|
@@ -18,15 +18,15 @@ 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.22",
|
|
22
22
|
publishConfig: {
|
|
23
23
|
access: "public"
|
|
24
24
|
},
|
|
25
25
|
description: "Local context engine for AI coding assistants \u2014 graph-based context, branch-aware memory, real-time human-activity awareness, deterministic Grep/Glob gating, and a live token dashboard.",
|
|
26
26
|
type: "module",
|
|
27
27
|
bin: {
|
|
28
|
-
syn: "
|
|
29
|
-
synthra: "
|
|
28
|
+
syn: "bin/syn",
|
|
29
|
+
synthra: "bin/syn"
|
|
30
30
|
},
|
|
31
31
|
scripts: {
|
|
32
32
|
build: "tsup",
|
|
@@ -1075,7 +1075,7 @@ var public_default = `<!doctype html>
|
|
|
1075
1075
|
const el = $('#proj-chart');
|
|
1076
1076
|
el.innerHTML = '';
|
|
1077
1077
|
if (!projects.length) {
|
|
1078
|
-
el.innerHTML = '<div class="empty">No projects yet.</div>';
|
|
1078
|
+
el.innerHTML = '<div class="empty">No projects yet \u2014 run <code>syn .</code> in a project to start.</div>';
|
|
1079
1079
|
return;
|
|
1080
1080
|
}
|
|
1081
1081
|
const ranked = [...projects].sort((a, b) => (b.total_turns || 0) - (a.total_turns || 0));
|
|
@@ -2170,7 +2170,7 @@ function extractKeywords(content, _ext) {
|
|
|
2170
2170
|
}
|
|
2171
2171
|
|
|
2172
2172
|
// src/scanner/extract.ts
|
|
2173
|
-
var RESOLVE_EXTS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py", ".svelte", ".vue", ".dart"];
|
|
2173
|
+
var RESOLVE_EXTS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py", ".svelte", ".vue", ".dart", ".html", ".hubl"];
|
|
2174
2174
|
var INDEX_FILES = ["index.ts", "index.tsx", "index.js", "index.jsx", "__init__.py"];
|
|
2175
2175
|
function fileId(relPath) {
|
|
2176
2176
|
return `file:${relPath}`;
|
|
@@ -2574,6 +2574,57 @@ async function parseGo(f, source) {
|
|
|
2574
2574
|
);
|
|
2575
2575
|
}
|
|
2576
2576
|
|
|
2577
|
+
// src/scanner/parsers/hubl.ts
|
|
2578
|
+
var MACRO_RE = /\{%-?\s*macro\s+([A-Za-z_]\w*)\s*\(([^)]*)\)/g;
|
|
2579
|
+
var ENDMACRO_RE = /\{%-?\s*endmacro\b/g;
|
|
2580
|
+
var BLOCK_RE = /\{%-?\s*block\s+([A-Za-z_]\w*)/g;
|
|
2581
|
+
var ENDBLOCK_RE = /\{%-?\s*endblock\b/g;
|
|
2582
|
+
var IMPORT_RE = /\{%-?\s*(?:include|extends|import|from)\s+["']([^"']+)["']/g;
|
|
2583
|
+
function lineAt(source, index) {
|
|
2584
|
+
return source.slice(0, index).split(/\r?\n/).length;
|
|
2585
|
+
}
|
|
2586
|
+
function endLineAfter(source, fromIndex, endRe, startLine) {
|
|
2587
|
+
endRe.lastIndex = fromIndex;
|
|
2588
|
+
const m = endRe.exec(source);
|
|
2589
|
+
return m ? lineAt(source, m.index) : startLine;
|
|
2590
|
+
}
|
|
2591
|
+
function parseHubL(f, source) {
|
|
2592
|
+
const symbols = [];
|
|
2593
|
+
const imports = [];
|
|
2594
|
+
for (const m of source.matchAll(MACRO_RE)) {
|
|
2595
|
+
const name = m[1];
|
|
2596
|
+
if (!name) continue;
|
|
2597
|
+
const args = (m[2] ?? "").trim();
|
|
2598
|
+
const start = m.index ?? 0;
|
|
2599
|
+
const startLine = lineAt(source, start);
|
|
2600
|
+
symbols.push({
|
|
2601
|
+
name,
|
|
2602
|
+
kind: "function",
|
|
2603
|
+
startLine,
|
|
2604
|
+
endLine: endLineAfter(source, start + m[0].length, ENDMACRO_RE, startLine),
|
|
2605
|
+
signature: `macro ${name}(${args})`
|
|
2606
|
+
});
|
|
2607
|
+
}
|
|
2608
|
+
for (const m of source.matchAll(BLOCK_RE)) {
|
|
2609
|
+
const name = m[1];
|
|
2610
|
+
if (!name) continue;
|
|
2611
|
+
const start = m.index ?? 0;
|
|
2612
|
+
const startLine = lineAt(source, start);
|
|
2613
|
+
symbols.push({
|
|
2614
|
+
name,
|
|
2615
|
+
kind: "component",
|
|
2616
|
+
startLine,
|
|
2617
|
+
endLine: endLineAfter(source, start + m[0].length, ENDBLOCK_RE, startLine),
|
|
2618
|
+
signature: `block ${name}`
|
|
2619
|
+
});
|
|
2620
|
+
}
|
|
2621
|
+
for (const m of source.matchAll(IMPORT_RE)) {
|
|
2622
|
+
const spec = m[1];
|
|
2623
|
+
if (spec) imports.push(spec);
|
|
2624
|
+
}
|
|
2625
|
+
return { file: f, source, symbols, imports: Array.from(new Set(imports)), calls: [] };
|
|
2626
|
+
}
|
|
2627
|
+
|
|
2577
2628
|
// src/scanner/parsers/java.ts
|
|
2578
2629
|
var QUERY6 = `
|
|
2579
2630
|
(class_declaration name: (identifier) @class.name) @class
|
|
@@ -3012,6 +3063,9 @@ async function parseFile(f) {
|
|
|
3012
3063
|
return parseSvelte(f, source);
|
|
3013
3064
|
case ".vue":
|
|
3014
3065
|
return parseVue(f, source);
|
|
3066
|
+
case ".html":
|
|
3067
|
+
case ".hubl":
|
|
3068
|
+
return parseHubL(f, source);
|
|
3015
3069
|
case ".go":
|
|
3016
3070
|
return parseGo(f, source);
|
|
3017
3071
|
case ".rs":
|
|
@@ -4384,15 +4438,33 @@ Reason: ${retrieval.reason}
|
|
|
4384
4438
|
return textContent(`${header}
|
|
4385
4439
|
${packed.text}`);
|
|
4386
4440
|
}
|
|
4441
|
+
function resolveFileTarget(graph, filePath) {
|
|
4442
|
+
const files = graph.nodes.filter((n) => n.kind === "file");
|
|
4443
|
+
const exact = files.find((n) => n.path === filePath);
|
|
4444
|
+
if (exact) return { node: exact };
|
|
4445
|
+
const suffix = "/" + filePath;
|
|
4446
|
+
const matches = files.filter((n) => n.path.endsWith(suffix));
|
|
4447
|
+
if (matches.length === 1) return { node: matches[0] };
|
|
4448
|
+
if (matches.length > 1) return { ambiguous: matches.map((n) => n.path) };
|
|
4449
|
+
return { none: true };
|
|
4450
|
+
}
|
|
4387
4451
|
function graphRead(args, ctx) {
|
|
4388
4452
|
const target = typeof args?.target === "string" ? args.target : "";
|
|
4389
4453
|
if (!target) return errorContent("graph_read: 'target' (string) is required");
|
|
4390
4454
|
const [rawFile, symbolName] = target.includes("::") ? target.split("::", 2) : [target, void 0];
|
|
4391
4455
|
const filePath = (rawFile ?? "").trim();
|
|
4392
|
-
const
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
|
|
4456
|
+
const resolved = resolveFileTarget(ctx.graph, filePath);
|
|
4457
|
+
if ("ambiguous" in resolved) {
|
|
4458
|
+
const shown = resolved.ambiguous.slice(0, 5).join(", ");
|
|
4459
|
+
const more = resolved.ambiguous.length > 5 ? ", \u2026" : "";
|
|
4460
|
+
return errorContent(
|
|
4461
|
+
`graph_read: '${filePath}' matches multiple files (${shown}${more}). Pass a longer path.`
|
|
4462
|
+
);
|
|
4463
|
+
}
|
|
4464
|
+
if ("none" in resolved) {
|
|
4465
|
+
return errorContent(`graph_read: file not found in graph: ${filePath}`);
|
|
4466
|
+
}
|
|
4467
|
+
const fileNode = resolved.node;
|
|
4396
4468
|
if (!symbolName) {
|
|
4397
4469
|
return textContent(`# ${fileNode.path}
|
|
4398
4470
|
|
|
@@ -4400,10 +4472,10 @@ ${fileNode.content}`);
|
|
|
4400
4472
|
}
|
|
4401
4473
|
const cleanSym = symbolName.trim();
|
|
4402
4474
|
const symbol = ctx.graph.nodes.find(
|
|
4403
|
-
(n) => n.kind === "symbol" && n.file ===
|
|
4475
|
+
(n) => n.kind === "symbol" && n.file === fileNode.path && n.name === cleanSym
|
|
4404
4476
|
);
|
|
4405
4477
|
if (!symbol) {
|
|
4406
|
-
return errorContent(`graph_read: symbol '${cleanSym}' not found in ${
|
|
4478
|
+
return errorContent(`graph_read: symbol '${cleanSym}' not found in ${fileNode.path}`);
|
|
4407
4479
|
}
|
|
4408
4480
|
const lines = fileNode.content.split(/\r?\n/);
|
|
4409
4481
|
const body = lines.slice(symbol.start_line - 1, symbol.end_line).join("\n");
|
|
@@ -4569,16 +4641,32 @@ function looksLikeNonSymbolQuery(pattern) {
|
|
|
4569
4641
|
}
|
|
4570
4642
|
return false;
|
|
4571
4643
|
}
|
|
4572
|
-
function recentlyTouchedMatchesQuery(recentPaths, queryTokens) {
|
|
4644
|
+
function recentlyTouchedMatchesQuery(recentPaths, queryTokens, graph) {
|
|
4645
|
+
if (recentPaths.length === 0) return [];
|
|
4646
|
+
const recent = new Set(recentPaths);
|
|
4647
|
+
const keywordsByPath = /* @__PURE__ */ new Map();
|
|
4648
|
+
for (const n of graph.nodes) {
|
|
4649
|
+
if (n.kind === "file" && recent.has(n.path)) keywordsByPath.set(n.path, n.keywords);
|
|
4650
|
+
}
|
|
4573
4651
|
const matches = [];
|
|
4574
4652
|
for (const path of recentPaths) {
|
|
4575
4653
|
const lower = path.toLowerCase();
|
|
4654
|
+
let matched = false;
|
|
4576
4655
|
for (const t of queryTokens) {
|
|
4577
4656
|
if (lower.includes(t)) {
|
|
4578
|
-
|
|
4657
|
+
matched = true;
|
|
4579
4658
|
break;
|
|
4580
4659
|
}
|
|
4581
4660
|
}
|
|
4661
|
+
if (!matched) {
|
|
4662
|
+
for (const kw of keywordsByPath.get(path) ?? []) {
|
|
4663
|
+
if (queryTokens.has(kw)) {
|
|
4664
|
+
matched = true;
|
|
4665
|
+
break;
|
|
4666
|
+
}
|
|
4667
|
+
}
|
|
4668
|
+
}
|
|
4669
|
+
if (matched) matches.push(path);
|
|
4582
4670
|
}
|
|
4583
4671
|
return matches;
|
|
4584
4672
|
}
|
|
@@ -4629,7 +4717,7 @@ async function handleGate(req, ctx) {
|
|
|
4629
4717
|
}
|
|
4630
4718
|
const qTokens = new Set(tokenizeQuery(query));
|
|
4631
4719
|
const recentPaths = ctx.activity.recentFilePaths(RECENT_ACTIVITY_WINDOW_MS);
|
|
4632
|
-
const overlap = recentlyTouchedMatchesQuery(recentPaths, qTokens);
|
|
4720
|
+
const overlap = recentlyTouchedMatchesQuery(recentPaths, qTokens, ctx.graph);
|
|
4633
4721
|
if (overlap.length > 0) {
|
|
4634
4722
|
const res2 = {
|
|
4635
4723
|
decision: "allow",
|