@jefuriiij/synthra 0.1.21 → 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 CHANGED
@@ -7,6 +7,41 @@ 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
+
10
45
  ## [0.1.21] — 2026-06-06
11
46
 
12
47
  ### Added
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",
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: "./bin/syn",
29
- synthra: "./bin/syn"
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));
@@ -4438,15 +4438,33 @@ Reason: ${retrieval.reason}
4438
4438
  return textContent(`${header}
4439
4439
  ${packed.text}`);
4440
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
+ }
4441
4451
  function graphRead(args, ctx) {
4442
4452
  const target = typeof args?.target === "string" ? args.target : "";
4443
4453
  if (!target) return errorContent("graph_read: 'target' (string) is required");
4444
4454
  const [rawFile, symbolName] = target.includes("::") ? target.split("::", 2) : [target, void 0];
4445
4455
  const filePath = (rawFile ?? "").trim();
4446
- const fileNode = ctx.graph.nodes.find(
4447
- (n) => n.kind === "file" && n.path === filePath
4448
- );
4449
- if (!fileNode) return errorContent(`graph_read: file not found in graph: ${filePath}`);
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;
4450
4468
  if (!symbolName) {
4451
4469
  return textContent(`# ${fileNode.path}
4452
4470
 
@@ -4454,10 +4472,10 @@ ${fileNode.content}`);
4454
4472
  }
4455
4473
  const cleanSym = symbolName.trim();
4456
4474
  const symbol = ctx.graph.nodes.find(
4457
- (n) => n.kind === "symbol" && n.file === filePath && n.name === cleanSym
4475
+ (n) => n.kind === "symbol" && n.file === fileNode.path && n.name === cleanSym
4458
4476
  );
4459
4477
  if (!symbol) {
4460
- return errorContent(`graph_read: symbol '${cleanSym}' not found in ${filePath}`);
4478
+ return errorContent(`graph_read: symbol '${cleanSym}' not found in ${fileNode.path}`);
4461
4479
  }
4462
4480
  const lines = fileNode.content.split(/\r?\n/);
4463
4481
  const body = lines.slice(symbol.start_line - 1, symbol.end_line).join("\n");
@@ -4623,16 +4641,32 @@ function looksLikeNonSymbolQuery(pattern) {
4623
4641
  }
4624
4642
  return false;
4625
4643
  }
4626
- 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
+ }
4627
4651
  const matches = [];
4628
4652
  for (const path of recentPaths) {
4629
4653
  const lower = path.toLowerCase();
4654
+ let matched = false;
4630
4655
  for (const t of queryTokens) {
4631
4656
  if (lower.includes(t)) {
4632
- matches.push(path);
4657
+ matched = true;
4633
4658
  break;
4634
4659
  }
4635
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);
4636
4670
  }
4637
4671
  return matches;
4638
4672
  }
@@ -4683,7 +4717,7 @@ async function handleGate(req, ctx) {
4683
4717
  }
4684
4718
  const qTokens = new Set(tokenizeQuery(query));
4685
4719
  const recentPaths = ctx.activity.recentFilePaths(RECENT_ACTIVITY_WINDOW_MS);
4686
- const overlap = recentlyTouchedMatchesQuery(recentPaths, qTokens);
4720
+ const overlap = recentlyTouchedMatchesQuery(recentPaths, qTokens, ctx.graph);
4687
4721
  if (overlap.length > 0) {
4688
4722
  const res2 = {
4689
4723
  decision: "allow",