ctxloom-pro 1.5.2 → 1.5.4

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/README.md CHANGED
@@ -47,7 +47,7 @@ The full first-run flow is **one install + one trial + one init per project.** E
47
47
  npm install -g ctxloom-pro
48
48
  ```
49
49
 
50
- > **For local trial / dev use the unpinned command above is fine.** For unattended CI usage, pin to the exact version (`ctxloom-pro@1.5.2`) so future CLI releases don't silently desync your agent-spec coverage — see the workflow example below.
50
+ > **For local trial / dev use the unpinned command above is fine.** For unattended CI usage, pin to the exact version (`ctxloom-pro@1.5.3`) so future CLI releases don't silently desync your agent-spec coverage — see the workflow example below.
51
51
 
52
52
  ### 2 — Start your free trial (once per email)
53
53
 
@@ -361,7 +361,7 @@ jobs:
361
361
  # Exact pin (not `@^1`) so future CLI releases that add/remove MCP
362
362
  # tools don't silently desync your reviewer-agent specs. Bump on
363
363
  # every release; see CHANGELOG.md for the live version table.
364
- - run: npm install -g ctxloom-pro@1.5.2
364
+ - run: npm install -g ctxloom-pro@1.5.3
365
365
  - run: ctxloom index
366
366
  - run: ctxloom rules check --json
367
367
  ```
@@ -676,6 +676,47 @@ var ASTParser = class {
676
676
  processedIds.add(node.id);
677
677
  return;
678
678
  }
679
+ // ─── CommonJS require() calls ────────────────────────────────────
680
+ // ES6 import_statement above handles `import X from './foo'`. But
681
+ // pure CommonJS projects — express, most pre-2020 Node libs — use
682
+ // `require('./foo')` instead and never emit an import_statement
683
+ // node. Without this case, those projects produce a dependency
684
+ // graph with 0 edges and the blast-radius tool collapses to
685
+ // "just the seed file" because there's nothing to traverse.
686
+ //
687
+ // Mirrors the Ruby require pattern below (see the `case 'call'`
688
+ // branch later in the walker). Only literal-string arguments are
689
+ // resolvable; dynamic `require(varName)` is statically invisible
690
+ // and intentionally skipped.
691
+ //
692
+ // Patterns matched:
693
+ // require('./path')
694
+ // require('../path')
695
+ // require('./path.json') — JSON files emit edges too
696
+ // Patterns NOT matched:
697
+ // require(varName) — dynamic, can't resolve
698
+ // require.resolve('./path') — returns path string, not a load
699
+ // import('./path') — dynamic ESM, separate concern
700
+ case "call_expression": {
701
+ const fnNode = node.childForFieldName?.("function") ?? node.children.find((c) => c?.type === "identifier");
702
+ if (fnNode?.text === "require") {
703
+ const argsNode = node.childForFieldName?.("arguments") ?? node.children.find((c) => c?.type === "arguments");
704
+ const firstArg = argsNode?.children.find((c) => c?.type === "string");
705
+ if (firstArg) {
706
+ const spec = firstArg.text.replace(/^['"`]|['"`]$/g, "");
707
+ if (spec.length > 0) {
708
+ nodes.push({
709
+ type: "import",
710
+ name: spec,
711
+ source: spec,
712
+ startLine: node.startPosition.row + 1,
713
+ endLine: node.endPosition.row + 1
714
+ });
715
+ }
716
+ }
717
+ }
718
+ break;
719
+ }
679
720
  // ─── Export statements (export function, export default) ────────
680
721
  case "export_statement": {
681
722
  const hasDefault = node.children.some((c) => c?.type === "default");
@@ -804,6 +845,8 @@ var ASTParser = class {
804
845
  startLine: declarator.startPosition.row + 1,
805
846
  endLine: declarator.endPosition.row + 1
806
847
  });
848
+ } else if (valueNode.type === "call_expression") {
849
+ walk(valueNode);
807
850
  }
808
851
  }
809
852
  }
@@ -1956,29 +1999,54 @@ function resolveImport(fromAbs, raw, rootDir) {
1956
1999
  }
1957
2000
  function extractPythonImports(content) {
1958
2001
  const results = [];
1959
- const relFrom = /^from\s+(\.+[\w.]*)\s+import/gm;
1960
2002
  let m;
2003
+ const relFrom = /^from\s+(\.+[\w.]*)\s+import/gm;
1961
2004
  while ((m = relFrom.exec(content)) !== null) {
1962
2005
  results.push({ specifier: m[1], isRelative: true });
1963
2006
  }
2007
+ const absFrom = /^from\s+([A-Za-z_][\w.]*)\s+import/gm;
2008
+ while ((m = absFrom.exec(content)) !== null) {
2009
+ results.push({ specifier: m[1], isRelative: false });
2010
+ }
2011
+ const directImport = /^import\s+([A-Za-z_][\w.]*)/gm;
2012
+ while ((m = directImport.exec(content)) !== null) {
2013
+ results.push({ specifier: m[1], isRelative: false });
2014
+ }
1964
2015
  return results;
1965
2016
  }
1966
2017
  function resolvePythonImport(fromAbs, fromDir, raw, rootDir) {
1967
2018
  const dotsMatch = raw.specifier.match(/^(\.+)/);
1968
- const dots = dotsMatch?.[1] ?? ".";
1969
- const modulePart = raw.specifier.slice(dots.length);
1970
- let baseDir = fromDir;
1971
- for (let i = 1; i < dots.length; i++) {
1972
- baseDir = path5.dirname(baseDir);
1973
- }
1974
- const modulePath = modulePart.replace(/\./g, path5.sep);
1975
- const candidates = modulePath ? [
1976
- path5.join(baseDir, modulePath + ".py"),
1977
- path5.join(baseDir, modulePath, "__init__.py")
1978
- ] : [
1979
- path5.join(fromDir, "__init__.py"),
1980
- fromAbs
1981
- // self — skip below
2019
+ if (dotsMatch) {
2020
+ const dots = dotsMatch[1];
2021
+ const modulePart = raw.specifier.slice(dots.length);
2022
+ let baseDir = fromDir;
2023
+ for (let i = 1; i < dots.length; i++) {
2024
+ baseDir = path5.dirname(baseDir);
2025
+ }
2026
+ const modulePath2 = modulePart.replace(/\./g, path5.sep);
2027
+ const candidates2 = modulePath2 ? [
2028
+ path5.join(baseDir, modulePath2 + ".py"),
2029
+ path5.join(baseDir, modulePath2, "__init__.py")
2030
+ ] : [
2031
+ path5.join(fromDir, "__init__.py"),
2032
+ fromAbs
2033
+ // self — skip below
2034
+ ];
2035
+ for (const c of candidates2) {
2036
+ if (c !== fromAbs && fs5.existsSync(c)) {
2037
+ return path5.relative(rootDir, c);
2038
+ }
2039
+ }
2040
+ return null;
2041
+ }
2042
+ const modulePath = raw.specifier.replace(/\./g, path5.sep);
2043
+ const candidates = [
2044
+ // <repoRoot>/<package>/foo.py or <repoRoot>/<package>/foo/__init__.py
2045
+ path5.join(rootDir, modulePath + ".py"),
2046
+ path5.join(rootDir, modulePath, "__init__.py"),
2047
+ // src/ layout (common for Python apps that follow PEP 518 src-layout)
2048
+ path5.join(rootDir, "src", modulePath + ".py"),
2049
+ path5.join(rootDir, "src", modulePath, "__init__.py")
1982
2050
  ];
1983
2051
  for (const c of candidates) {
1984
2052
  if (c !== fromAbs && fs5.existsSync(c)) {
@@ -2913,10 +2981,28 @@ var DependencyGraph = class {
2913
2981
  }
2914
2982
  resolveImport(fromAbs, specifier, rootDir) {
2915
2983
  const dir = path7.dirname(fromAbs);
2916
- for (const ext of ["", ".ts", ".tsx", ".js", ".jsx", "/index.ts", "/index.js"]) {
2984
+ const extensions = [
2985
+ "",
2986
+ ".ts",
2987
+ ".tsx",
2988
+ ".js",
2989
+ ".jsx",
2990
+ ".mjs",
2991
+ ".cjs",
2992
+ "/index.ts",
2993
+ "/index.tsx",
2994
+ "/index.js",
2995
+ "/index.jsx",
2996
+ "/index.mjs",
2997
+ "/index.cjs"
2998
+ ];
2999
+ for (const ext of extensions) {
2917
3000
  const candidate = path7.resolve(dir, specifier.replace(/\.js$/, "") + ext);
2918
- if (fs7.existsSync(candidate)) {
2919
- return path7.relative(rootDir, candidate);
3001
+ try {
3002
+ if (fs7.statSync(candidate).isFile()) {
3003
+ return path7.relative(rootDir, candidate);
3004
+ }
3005
+ } catch {
2920
3006
  }
2921
3007
  }
2922
3008
  return null;
@@ -11506,10 +11592,10 @@ function resolveTelemetryLevel() {
11506
11592
  }
11507
11593
  var TELEMETRY_LEVEL = resolveTelemetryLevel();
11508
11594
  var TELEMETRY_DISABLED = TELEMETRY_LEVEL === "off";
11509
- var CTXLOOM_VERSION = "1.5.2".length > 0 ? "1.5.2" : "dev";
11595
+ var CTXLOOM_VERSION = "1.5.4".length > 0 ? "1.5.4" : "dev";
11510
11596
  var POSTHOG_HOST = "https://eu.i.posthog.com";
11511
11597
  var POSTHOG_KEY = process.env["POSTHOG_API_KEY"] ?? (true ? "phc_CiDkmFLcZ2K6uCpcoSUQLmFrnnUvsyXGhSxopX5TVKE6" : "");
11512
- var SENTRY_DSN = process.env["SENTRY_DSN"] ?? (true ? "https://81c94a0f04a8e242dee493ac1e17f733@o4508531702497280.ingest.de.sentry.io/4511256875368528" : "");
11598
+ var SENTRY_DSN = process.env["SENTRY_DSN"] ?? (true ? "https://81c94a0f04a8e242dee493ac1e17f733@o4508531702497280.ingest.de.sentry.io/4511256875368528\u2028" : "");
11513
11599
  var cachedDistinctId = null;
11514
11600
  function resolveDistinctId() {
11515
11601
  if (cachedDistinctId) return cachedDistinctId;
@@ -522,6 +522,47 @@ var ASTParser = class {
522
522
  processedIds.add(node.id);
523
523
  return;
524
524
  }
525
+ // ─── CommonJS require() calls ────────────────────────────────────
526
+ // ES6 import_statement above handles `import X from './foo'`. But
527
+ // pure CommonJS projects — express, most pre-2020 Node libs — use
528
+ // `require('./foo')` instead and never emit an import_statement
529
+ // node. Without this case, those projects produce a dependency
530
+ // graph with 0 edges and the blast-radius tool collapses to
531
+ // "just the seed file" because there's nothing to traverse.
532
+ //
533
+ // Mirrors the Ruby require pattern below (see the `case 'call'`
534
+ // branch later in the walker). Only literal-string arguments are
535
+ // resolvable; dynamic `require(varName)` is statically invisible
536
+ // and intentionally skipped.
537
+ //
538
+ // Patterns matched:
539
+ // require('./path')
540
+ // require('../path')
541
+ // require('./path.json') — JSON files emit edges too
542
+ // Patterns NOT matched:
543
+ // require(varName) — dynamic, can't resolve
544
+ // require.resolve('./path') — returns path string, not a load
545
+ // import('./path') — dynamic ESM, separate concern
546
+ case "call_expression": {
547
+ const fnNode = node.childForFieldName?.("function") ?? node.children.find((c) => c?.type === "identifier");
548
+ if (fnNode?.text === "require") {
549
+ const argsNode = node.childForFieldName?.("arguments") ?? node.children.find((c) => c?.type === "arguments");
550
+ const firstArg = argsNode?.children.find((c) => c?.type === "string");
551
+ if (firstArg) {
552
+ const spec = firstArg.text.replace(/^['"`]|['"`]$/g, "");
553
+ if (spec.length > 0) {
554
+ nodes.push({
555
+ type: "import",
556
+ name: spec,
557
+ source: spec,
558
+ startLine: node.startPosition.row + 1,
559
+ endLine: node.endPosition.row + 1
560
+ });
561
+ }
562
+ }
563
+ }
564
+ break;
565
+ }
525
566
  // ─── Export statements (export function, export default) ────────
526
567
  case "export_statement": {
527
568
  const hasDefault = node.children.some((c) => c?.type === "default");
@@ -650,6 +691,8 @@ var ASTParser = class {
650
691
  startLine: declarator.startPosition.row + 1,
651
692
  endLine: declarator.endPosition.row + 1
652
693
  });
694
+ } else if (valueNode.type === "call_expression") {
695
+ walk(valueNode);
653
696
  }
654
697
  }
655
698
  }
@@ -1798,29 +1841,54 @@ function resolveImport(fromAbs, raw, rootDir) {
1798
1841
  }
1799
1842
  function extractPythonImports(content) {
1800
1843
  const results = [];
1801
- const relFrom = /^from\s+(\.+[\w.]*)\s+import/gm;
1802
1844
  let m;
1845
+ const relFrom = /^from\s+(\.+[\w.]*)\s+import/gm;
1803
1846
  while ((m = relFrom.exec(content)) !== null) {
1804
1847
  results.push({ specifier: m[1], isRelative: true });
1805
1848
  }
1849
+ const absFrom = /^from\s+([A-Za-z_][\w.]*)\s+import/gm;
1850
+ while ((m = absFrom.exec(content)) !== null) {
1851
+ results.push({ specifier: m[1], isRelative: false });
1852
+ }
1853
+ const directImport = /^import\s+([A-Za-z_][\w.]*)/gm;
1854
+ while ((m = directImport.exec(content)) !== null) {
1855
+ results.push({ specifier: m[1], isRelative: false });
1856
+ }
1806
1857
  return results;
1807
1858
  }
1808
1859
  function resolvePythonImport(fromAbs, fromDir, raw, rootDir) {
1809
1860
  const dotsMatch = raw.specifier.match(/^(\.+)/);
1810
- const dots = dotsMatch?.[1] ?? ".";
1811
- const modulePart = raw.specifier.slice(dots.length);
1812
- let baseDir = fromDir;
1813
- for (let i = 1; i < dots.length; i++) {
1814
- baseDir = path4.dirname(baseDir);
1815
- }
1816
- const modulePath = modulePart.replace(/\./g, path4.sep);
1817
- const candidates = modulePath ? [
1818
- path4.join(baseDir, modulePath + ".py"),
1819
- path4.join(baseDir, modulePath, "__init__.py")
1820
- ] : [
1821
- path4.join(fromDir, "__init__.py"),
1822
- fromAbs
1823
- // self — skip below
1861
+ if (dotsMatch) {
1862
+ const dots = dotsMatch[1];
1863
+ const modulePart = raw.specifier.slice(dots.length);
1864
+ let baseDir = fromDir;
1865
+ for (let i = 1; i < dots.length; i++) {
1866
+ baseDir = path4.dirname(baseDir);
1867
+ }
1868
+ const modulePath2 = modulePart.replace(/\./g, path4.sep);
1869
+ const candidates2 = modulePath2 ? [
1870
+ path4.join(baseDir, modulePath2 + ".py"),
1871
+ path4.join(baseDir, modulePath2, "__init__.py")
1872
+ ] : [
1873
+ path4.join(fromDir, "__init__.py"),
1874
+ fromAbs
1875
+ // self — skip below
1876
+ ];
1877
+ for (const c of candidates2) {
1878
+ if (c !== fromAbs && fs4.existsSync(c)) {
1879
+ return path4.relative(rootDir, c);
1880
+ }
1881
+ }
1882
+ return null;
1883
+ }
1884
+ const modulePath = raw.specifier.replace(/\./g, path4.sep);
1885
+ const candidates = [
1886
+ // <repoRoot>/<package>/foo.py or <repoRoot>/<package>/foo/__init__.py
1887
+ path4.join(rootDir, modulePath + ".py"),
1888
+ path4.join(rootDir, modulePath, "__init__.py"),
1889
+ // src/ layout (common for Python apps that follow PEP 518 src-layout)
1890
+ path4.join(rootDir, "src", modulePath + ".py"),
1891
+ path4.join(rootDir, "src", modulePath, "__init__.py")
1824
1892
  ];
1825
1893
  for (const c of candidates) {
1826
1894
  if (c !== fromAbs && fs4.existsSync(c)) {
@@ -2755,10 +2823,28 @@ var DependencyGraph = class {
2755
2823
  }
2756
2824
  resolveImport(fromAbs, specifier, rootDir) {
2757
2825
  const dir = path6.dirname(fromAbs);
2758
- for (const ext of ["", ".ts", ".tsx", ".js", ".jsx", "/index.ts", "/index.js"]) {
2826
+ const extensions = [
2827
+ "",
2828
+ ".ts",
2829
+ ".tsx",
2830
+ ".js",
2831
+ ".jsx",
2832
+ ".mjs",
2833
+ ".cjs",
2834
+ "/index.ts",
2835
+ "/index.tsx",
2836
+ "/index.js",
2837
+ "/index.jsx",
2838
+ "/index.mjs",
2839
+ "/index.cjs"
2840
+ ];
2841
+ for (const ext of extensions) {
2759
2842
  const candidate = path6.resolve(dir, specifier.replace(/\.js$/, "") + ext);
2760
- if (fs6.existsSync(candidate)) {
2761
- return path6.relative(rootDir, candidate);
2843
+ try {
2844
+ if (fs6.statSync(candidate).isFile()) {
2845
+ return path6.relative(rootDir, candidate);
2846
+ }
2847
+ } catch {
2762
2848
  }
2763
2849
  }
2764
2850
  return null;
@@ -10205,10 +10291,10 @@ var TELEMETRY_DISABLED = TELEMETRY_LEVEL === "off";
10205
10291
  function getTelemetryLevel() {
10206
10292
  return TELEMETRY_LEVEL;
10207
10293
  }
10208
- var CTXLOOM_VERSION = "1.5.2".length > 0 ? "1.5.2" : "dev";
10294
+ var CTXLOOM_VERSION = "1.5.4".length > 0 ? "1.5.4" : "dev";
10209
10295
  var POSTHOG_HOST = "https://eu.i.posthog.com";
10210
10296
  var POSTHOG_KEY = process.env["POSTHOG_API_KEY"] ?? (true ? "phc_CiDkmFLcZ2K6uCpcoSUQLmFrnnUvsyXGhSxopX5TVKE6" : "");
10211
- var SENTRY_DSN = process.env["SENTRY_DSN"] ?? (true ? "https://81c94a0f04a8e242dee493ac1e17f733@o4508531702497280.ingest.de.sentry.io/4511256875368528" : "");
10297
+ var SENTRY_DSN = process.env["SENTRY_DSN"] ?? (true ? "https://81c94a0f04a8e242dee493ac1e17f733@o4508531702497280.ingest.de.sentry.io/4511256875368528\u2028" : "");
10212
10298
  var cachedDistinctId = null;
10213
10299
  function resolveDistinctId() {
10214
10300
  if (cachedDistinctId) return cachedDistinctId;
@@ -11803,4 +11889,4 @@ export {
11803
11889
  skillFilePath,
11804
11890
  installHarness
11805
11891
  };
11806
- //# sourceMappingURL=chunk-SMI35T32.js.map
11892
+ //# sourceMappingURL=chunk-4ZXFJSDG.js.map
package/dist/index.js CHANGED
@@ -45,7 +45,7 @@ import {
45
45
  validateDefaultRoot,
46
46
  wrapWithIndexingEnvelope,
47
47
  writeCODEOWNERS
48
- } from "./chunk-SMI35T32.js";
48
+ } from "./chunk-4ZXFJSDG.js";
49
49
  import {
50
50
  addCtxloomToConfig,
51
51
  detectInstalledClients
@@ -1020,7 +1020,7 @@ try {
1020
1020
  } catch {
1021
1021
  }
1022
1022
  var args = process.argv.slice(2);
1023
- var ctxloomVersion = "1.5.2".length > 0 ? "1.5.2" : "dev";
1023
+ var ctxloomVersion = "1.5.4".length > 0 ? "1.5.4" : "dev";
1024
1024
  if (args.includes("--version") || args.includes("-v")) {
1025
1025
  process.stdout.write(`ctxloom ${ctxloomVersion}
1026
1026
  `);
@@ -1093,7 +1093,7 @@ async function checkLicense() {
1093
1093
  if (command !== void 0 && LICENSE_GATE_BYPASS_COMMANDS.has(command)) return;
1094
1094
  const ciKey = process.env["CTXLOOM_LICENSE_KEY"];
1095
1095
  if (ciKey) {
1096
- const { ApiClient } = await import("./src-AHIZ4UNS.js");
1096
+ const { ApiClient } = await import("./src-AN6JHJMC.js");
1097
1097
  const client = new ApiClient(process.env["CTXLOOM_API_BASE"]);
1098
1098
  try {
1099
1099
  const result = await client.validate(ciKey, "ci-ephemeral");
@@ -1471,7 +1471,7 @@ async function main() {
1471
1471
  }
1472
1472
  if (!skipHarness) {
1473
1473
  process.stdout.write("\n");
1474
- const { installHarness } = await import("./src-AHIZ4UNS.js");
1474
+ const { installHarness } = await import("./src-AN6JHJMC.js");
1475
1475
  const h = installHarness({ cwd: initRoot, dryRun, force, extraHosts });
1476
1476
  const harnessFiles = [
1477
1477
  h.claudeMd,
@@ -1534,7 +1534,7 @@ async function main() {
1534
1534
  process.exit(1);
1535
1535
  }
1536
1536
  if (alias !== void 0) {
1537
- const { validateAlias } = await import("./src-AHIZ4UNS.js");
1537
+ const { validateAlias } = await import("./src-AN6JHJMC.js");
1538
1538
  const v = validateAlias(alias);
1539
1539
  if (!v.ok) {
1540
1540
  console.error(`[ctxloom] Invalid alias: ${v.reason}`);
@@ -1798,7 +1798,7 @@ Suggested reviewers for ${files.length} file(s):`);
1798
1798
  process.stderr.write("[ctxloom] --limit must be a non-negative integer (0 for unlimited)\n");
1799
1799
  process.exit(2);
1800
1800
  }
1801
- const { loadRulesConfig, RulesChecker, formatText, formatJson, RulesConfigError } = await import("./src-AHIZ4UNS.js");
1801
+ const { loadRulesConfig, RulesChecker, formatText, formatJson, RulesConfigError } = await import("./src-AN6JHJMC.js");
1802
1802
  let config;
1803
1803
  try {
1804
1804
  config = await loadRulesConfig(root);
@@ -1822,7 +1822,7 @@ Suggested reviewers for ${files.length} file(s):`);
1822
1822
  }
1823
1823
  let graph;
1824
1824
  if (useSnapshot) {
1825
- const { DependencyGraph: DG } = await import("./src-AHIZ4UNS.js");
1825
+ const { DependencyGraph: DG } = await import("./src-AN6JHJMC.js");
1826
1826
  graph = new DG();
1827
1827
  const loaded = await graph.loadSnapshotOnly(root);
1828
1828
  if (!loaded) {
@@ -1831,7 +1831,7 @@ Suggested reviewers for ${files.length} file(s):`);
1831
1831
  }
1832
1832
  } else {
1833
1833
  process.stderr.write("[ctxloom] Building dependency graph...\n");
1834
- const { ASTParser: ASTParser2, DependencyGraph: DependencyGraph2 } = await import("./src-AHIZ4UNS.js");
1834
+ const { ASTParser: ASTParser2, DependencyGraph: DependencyGraph2 } = await import("./src-AN6JHJMC.js");
1835
1835
  let parser;
1836
1836
  try {
1837
1837
  parser = new ASTParser2();
@@ -129,7 +129,7 @@ import {
129
129
  wrapBlock,
130
130
  wrapWithIndexingEnvelope,
131
131
  writeCODEOWNERS
132
- } from "./chunk-SMI35T32.js";
132
+ } from "./chunk-4ZXFJSDG.js";
133
133
  import {
134
134
  VectorStore
135
135
  } from "./chunk-DVI2RWJR.js";
@@ -294,4 +294,4 @@ export {
294
294
  wrapWithIndexingEnvelope,
295
295
  writeCODEOWNERS
296
296
  };
297
- //# sourceMappingURL=src-AHIZ4UNS.js.map
297
+ //# sourceMappingURL=src-AN6JHJMC.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ctxloom-pro",
3
- "version": "1.5.2",
3
+ "version": "1.5.4",
4
4
  "description": "ctxloom — The Universal Code Context Engine. A local-first MCP server providing intelligent code context via hybrid Vector + AST + Graph search with Skeletonization (92% token reduction).",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -43,6 +43,8 @@
43
43
  "lint": "tsc --noEmit",
44
44
  "postinstall": "node dist/setup/postinstall.js || true",
45
45
  "bench:repos": "tsx benchmarks/benchmark-public-repos.ts",
46
+ "bench:spike": "tsx scripts/bench/eval.ts spike",
47
+ "bench:full": "tsx scripts/bench/eval.ts full",
46
48
  "e2e:corpus": "node e2e-corpus/run.mjs"
47
49
  },
48
50
  "keywords": [