@danielblomma/cortex-mcp 2.2.5 → 2.4.0

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
@@ -143,6 +143,26 @@ Check context status:
143
143
  cortex status
144
144
  ```
145
145
 
146
+ ## Agent plugin (Claude Code + Codex)
147
+
148
+ The `plugins/cortex` directory is a dual-manifest agent plugin: five behavior
149
+ skills (`using-cortex`, `repo-research`, `change-impact`, `pattern-review`,
150
+ `context-review`), a SessionStart bootstrap that re-injects Cortex
151
+ instructions after new sessions, `/clear`, and compaction (Claude Code), and
152
+ an MCP config that follows the active workspace.
153
+
154
+ Claude Code:
155
+
156
+ ```bash
157
+ /plugin marketplace add DanielBlomma/cortex
158
+ /plugin install cortex@cortex
159
+ ```
160
+
161
+ Codex discovers the same skills through `.codex-plugin/plugin.json`; repos
162
+ initialized with `cortex init` also get an AGENTS.md bootstrap section as a
163
+ fallback when the plugin is not installed. The CLI remains the engine — the
164
+ plugin only adds the behavior layer, and `cortex connect` stays opt-in.
165
+
146
166
  ## Query From The CLI
147
167
 
148
168
  Use the CLI as the default local agent interface:
@@ -153,6 +173,7 @@ cortex related file:src/auth.ts --json
153
173
  cortex impact "payment service" --json
154
174
  cortex rules --json
155
175
  cortex explain "where retries are configured" --json
176
+ cortex pattern-evidence src/auth.ts --query "error handling" --json
156
177
  ```
157
178
 
158
179
  These commands read the same local graph, embeddings, and rules used by the MCP server, but they do not require an MCP client registration.
@@ -381,12 +402,39 @@ cortex related <entity-id> [--json]
381
402
  cortex impact <query|entity-id> [--json]
382
403
  cortex rules [--json]
383
404
  cortex explain <query|entity-id> [--json]
405
+ cortex pattern-evidence <file-path|entity-id> [--query <text>] [--top-k <n>] [--json]
384
406
  cortex status
385
407
  cortex dashboard [--interval <sec>]
386
408
  cortex watch [start|stop|status|run|once] [--interval <sec>] [--debounce <sec>] [--mode <auto|event|poll>]
387
409
  cortex help
388
410
  ```
389
411
 
412
+ ## Enterprise Pattern Review
413
+
414
+ Enterprise `context.review` includes bounded repo-local pattern context by
415
+ default. This evidence is advisory and does not change policy validator totals,
416
+ workflow approval, or review trust.
417
+
418
+ Optional MCP inputs:
419
+
420
+ - `include_pattern_evidence` — enable or disable pattern context (default `true`).
421
+ - `pattern_query` — shared pattern query; otherwise Cortex derives one per file.
422
+ - `pattern_top_k` — evidence items per locality tier, from 1 to 5 (default `2`).
423
+ - `pattern_limit` — analyzed review targets, from 1 to 25 (default `10`).
424
+
425
+ The response adds `pattern_review` with deterministic targets, the canonical
426
+ review question, cited evidence tiers, explicit repository fallback, unindexed
427
+ status, and omitted-file counts. Pattern evidence never constitutes automatic
428
+ code approval. Enterprise pattern review is lexical-only and never downloads an
429
+ embedding model or calls an external service.
430
+
431
+ Every target uses the same response fields. `status` is one of
432
+ `local_evidence`, `repo_fallback`, `no_evidence`, `not_indexed`, or `error`;
433
+ `local_pattern_found` and `fallback_used` are always explicit booleans, and
434
+ `evidence_order` plus all four `tiers` are always present. Unavailable evidence
435
+ uses empty tiers and sanitized messages rather than local paths or runtime
436
+ errors.
437
+
390
438
  ## Automated Release
391
439
 
392
440
  This repository includes two GitHub Actions workflows:
@@ -428,6 +476,12 @@ set it to `model` or `full` to force the full-model baseline even on tight
428
476
  machines. When several cortex instances share one machine, set
429
477
  `CORTEX_EMBED_THREADS` to give each its fair share of cores.
430
478
 
479
+ For experiments on very large repositories, `CORTEX_EMBED_TEXT_PROFILE=compact-files`
480
+ compacts only large file-level embedding records while keeping chunk-level
481
+ embedding text full. The default remains `full`; use the compact profile only
482
+ with a before/after semantic quality check because file-level compaction can
483
+ change retrieval behavior.
484
+
431
485
  ## Limitations
432
486
 
433
487
  - Requires repo initialization (`cortex init --bootstrap`).
package/bin/cortex.mjs CHANGED
@@ -81,6 +81,7 @@ function printHelp() {
81
81
  console.log(helpRow("impact <query|entity-id> [--json]", "Trace likely impact paths"));
82
82
  console.log(helpRow("rules [--json]", "List active context rules"));
83
83
  console.log(helpRow("explain <query|entity-id> [--json]", "Show search score evidence"));
84
+ console.log(helpRow("pattern-evidence <file|entity-id> [--query <text>] [--top-k <n>] [--json]", "Collect cited repo-local pattern evidence"));
84
85
  console.log(helpRow("dashboard [--interval <sec>]", "Live local dashboard"));
85
86
  console.log(helpRow("memory-compile [--dry-run] [--verbose]", "Compile memory artifacts"));
86
87
  console.log(helpRow("memory-lint [--verbose] [--json]", "Lint compiled memory"));
@@ -198,81 +199,7 @@ function ensureScaffoldExists() {
198
199
  // Files that should never be overwritten if they already exist in the target.
199
200
  // These contain user-specific configuration that would be lost on re-init.
200
201
  const PRESERVE_FILES = new Set(["config.yaml", "rules.yaml", "enterprise.yml", "enterprise.yaml", "CLAUDE.md", "AGENTS.md"]);
201
- const DEFAULT_SOURCE_PATHS = [
202
- "src",
203
- "docs",
204
- "design",
205
- ".context/notes",
206
- ".context/decisions",
207
- "README.md"
208
- ];
209
- const INIT_SKIP_DIRECTORIES = new Set([
210
- ".git",
211
- ".idea",
212
- ".vscode",
213
- "node_modules",
214
- "dist",
215
- "build",
216
- "coverage",
217
- ".next",
218
- ".cache",
219
- ".context",
220
- "scripts",
221
- ".githooks",
222
- "bin",
223
- "obj"
224
- ]);
225
- const INIT_SOURCE_EXTENSIONS = new Set([
226
- ".md",
227
- ".mdx",
228
- ".txt",
229
- ".adoc",
230
- ".rst",
231
- ".yaml",
232
- ".yml",
233
- ".json",
234
- ".toml",
235
- ".csv",
236
- ".ts",
237
- ".tsx",
238
- ".mts",
239
- ".cts",
240
- ".js",
241
- ".jsx",
242
- ".mjs",
243
- ".cjs",
244
- ".py",
245
- ".go",
246
- ".java",
247
- ".cs",
248
- ".vb",
249
- ".sln",
250
- ".vbproj",
251
- ".csproj",
252
- ".fsproj",
253
- ".props",
254
- ".targets",
255
- ".config",
256
- ".resx",
257
- ".settings",
258
- ".rb",
259
- ".rs",
260
- ".php",
261
- ".swift",
262
- ".kt",
263
- ".sql",
264
- ".sh",
265
- ".bash",
266
- ".zsh",
267
- ".ps1",
268
- ".c",
269
- ".h",
270
- ".cpp",
271
- ".hpp",
272
- ".cc",
273
- ".hh"
274
- ]);
275
- const ROOT_DOC_PATHS = new Set(["docs", "design"]);
202
+ const DEFAULT_SOURCE_PATHS = ["."];
276
203
 
277
204
  function copyDirectory(sourceDir, targetDir) {
278
205
  fs.mkdirSync(targetDir, { recursive: true });
@@ -318,92 +245,8 @@ function slugifyRepoId(value) {
318
245
  return dashed || "cortex";
319
246
  }
320
247
 
321
- function isInterestingSourceFile(fileName) {
322
- const base = fileName.toLowerCase();
323
- const ext = path.extname(fileName).toLowerCase();
324
- return INIT_SOURCE_EXTENSIONS.has(ext) || base === "readme" || base.startsWith("readme.");
325
- }
326
-
327
- function directoryContainsInterestingFiles(directoryPath) {
328
- const stack = [directoryPath];
329
- while (stack.length > 0) {
330
- const current = stack.pop();
331
- let entries = [];
332
- try {
333
- entries = fs.readdirSync(current, { withFileTypes: true });
334
- } catch {
335
- continue;
336
- }
337
-
338
- for (const entry of entries) {
339
- const absolutePath = path.join(current, entry.name);
340
- if (entry.isDirectory()) {
341
- if (INIT_SKIP_DIRECTORIES.has(entry.name)) {
342
- continue;
343
- }
344
- stack.push(absolutePath);
345
- continue;
346
- }
347
-
348
- if (entry.isFile() && isInterestingSourceFile(entry.name)) {
349
- return true;
350
- }
351
- }
352
- }
353
-
354
- return false;
355
- }
356
-
357
248
  function detectInitialSourcePaths(targetDir) {
358
- if (!fs.existsSync(targetDir)) {
359
- return [...DEFAULT_SOURCE_PATHS];
360
- }
361
-
362
- let entries = [];
363
- try {
364
- entries = fs.readdirSync(targetDir, { withFileTypes: true });
365
- } catch {
366
- return [...DEFAULT_SOURCE_PATHS];
367
- }
368
-
369
- const codeDirs = [];
370
- const docDirs = [];
371
- const rootFiles = [];
372
-
373
- for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
374
- const absolutePath = path.join(targetDir, entry.name);
375
-
376
- if (entry.isDirectory()) {
377
- if (INIT_SKIP_DIRECTORIES.has(entry.name)) {
378
- continue;
379
- }
380
- if (!directoryContainsInterestingFiles(absolutePath)) {
381
- continue;
382
- }
383
-
384
- const bucket = ROOT_DOC_PATHS.has(entry.name) ? docDirs : codeDirs;
385
- bucket.push(toPosixPath(entry.name));
386
- continue;
387
- }
388
-
389
- if (entry.isFile() && isInterestingSourceFile(entry.name)) {
390
- rootFiles.push(toPosixPath(entry.name));
391
- }
392
- }
393
-
394
- const readmeFiles = rootFiles.filter((filePath) => /^readme(\.|$)/i.test(path.basename(filePath)));
395
- const nonReadmeRootFiles = rootFiles.filter((filePath) => !readmeFiles.includes(filePath));
396
- const detected = [
397
- ...codeDirs,
398
- ...nonReadmeRootFiles,
399
- ...docDirs,
400
- ".context/notes",
401
- ".context/decisions",
402
- ...readmeFiles
403
- ];
404
- const uniqueDetected = [...new Set(detected)];
405
- const hasConcreteRepoContent = uniqueDetected.some((value) => !value.startsWith(".context/"));
406
- return hasConcreteRepoContent ? uniqueDetected : [...DEFAULT_SOURCE_PATHS];
249
+ return [...DEFAULT_SOURCE_PATHS];
407
250
  }
408
251
 
409
252
  function buildInitialConfig(targetDir) {
@@ -418,12 +261,10 @@ function buildInitialConfig(targetDir) {
418
261
  " - RULE",
419
262
  " - CODE",
420
263
  " - WIKI",
421
- "# graph weight is low because graph degree mostly measures how many rules",
422
- "# constrain an entity (docs are hubs, leaf code is not). Tuned as a pair with",
423
- "# the midrank-percentile graph_score.",
264
+ "# Tuned together with the midrank-percentile graph_score in searchResults.ts.",
424
265
  "ranking:",
425
- " semantic: 0.55",
426
- " graph: 0.10",
266
+ " semantic: 0.40",
267
+ " graph: 0.25",
427
268
  " trust: 0.20",
428
269
  " recency: 0.15",
429
270
  "runtime:",
@@ -645,6 +486,10 @@ function installCodexAgentsSection(targetDir) {
645
486
  const startMarker = "<!-- cortex:auto:start -->";
646
487
  const endMarker = "<!-- cortex:auto:end -->";
647
488
  const section = `## Cortex Auto Workflow
489
+ - Use the \`using-cortex\` skill if available; otherwise follow the commands below.
490
+ - Search before answering code questions: \`cortex search "<query>" --json\`; never answer from memory.
491
+ - Check \`cortex rules --json\` before suggesting changes and \`cortex impact "<query>" --json\` before refactors.
492
+ - Review changed files with \`cortex pattern-evidence <file> --json\` before finalizing.
648
493
  - Run \`cortex update\` before completing substantial code changes.
649
494
  - If background sync is enabled, check with \`cortex watch status\`.`;
650
495
  const changed = upsertSectionByMarkers(agentsPath, startMarker, endMarker, section);
@@ -956,12 +801,31 @@ async function ensureProjectInitializedForMcp(targetDir) {
956
801
  }
957
802
  }
958
803
 
804
+ // The session bootstrap hook caches index status for 10 minutes; drop the
805
+ // cache whenever a command may have changed the index so new sessions never
806
+ // see a stale "no index"/"index is old" verdict.
807
+ const INDEX_MUTATING_COMMANDS = new Set(["bootstrap", "update", "refresh", "ingest", "embed", "graph-load"]);
808
+
809
+ function invalidateSessionStatusCache(cwd) {
810
+ try {
811
+ fs.rmSync(path.join(cwd, ".context", "cache", "session-status.json"), { force: true });
812
+ } catch {
813
+ // best effort: a stale cache only delays the status refresh
814
+ }
815
+ }
816
+
959
817
  async function runContextCommand(cwd, contextArgs) {
960
818
  const contextScript = path.join(cwd, CONTEXT_SCRIPTS_REL, "context.sh");
961
819
  if (!fs.existsSync(contextScript)) {
962
820
  throw new Error(`Missing ${contextScript}. Run 'cortex init' first.`);
963
821
  }
964
- await runCommand("bash", [contextScript, ...contextArgs], cwd);
822
+ try {
823
+ await runCommand("bash", [contextScript, ...contextArgs], cwd);
824
+ } finally {
825
+ if (INDEX_MUTATING_COMMANDS.has(contextArgs[0])) {
826
+ invalidateSessionStatusCache(cwd);
827
+ }
828
+ }
965
829
  }
966
830
 
967
831
  async function run() {
@@ -1588,7 +1452,7 @@ async function runEnterpriseInstall(args) {
1588
1452
  }
1589
1453
 
1590
1454
  const RUN_CLIS = new Set(["claude", "codex", "copilot"]);
1591
- const QUERY_COMMANDS = new Set(["search", "related", "impact", "rules", "explain"]);
1455
+ const QUERY_COMMANDS = new Set(["search", "related", "impact", "rules", "explain", "pattern-evidence"]);
1592
1456
 
1593
1457
  async function runRunCommand(args) {
1594
1458
  const sub = args[0];
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@danielblomma/cortex-mcp",
3
3
  "mcpName": "io.github.DanielBlomma/cortex",
4
- "version": "2.2.5",
4
+ "version": "2.4.0",
5
5
  "description": "Local, repo-scoped context platform for coding assistants. Semantic search, graph relationships, and architectural rule context.",
6
6
  "type": "module",
7
7
  "author": "Daniel Blomma",
@@ -64,7 +64,7 @@
64
64
  ],
65
65
  "scripts": {
66
66
  "pretest": "test -d scaffold/scripts/parsers/node_modules || npm --prefix scaffold/scripts/parsers install --no-fund --no-update-notifier --silent",
67
- "test": "node tests/context-regressions.test.mjs && node --test tests/ingest-units.test.mjs tests/ingest-parallel.test.mjs tests/ingest-worker-crash.test.mjs tests/javascript-parser.test.mjs tests/markdown-parser.test.mjs tests/sql-parser.test.mjs tests/config-parser.test.mjs tests/resources-parser.test.mjs tests/vbnet-parser.test.mjs tests/cpp-parser.test.mjs tests/dashboard.test.mjs tests/init-config.test.mjs tests/init-agents.test.mjs tests/multi-level.test.mjs tests/no-legacy-paths.test.mjs tests/tree-sitter-error-reporting.test.mjs tests/tree-sitter-body-cap.test.mjs tests/tree-sitter-exported.test.mjs tests/tree-sitter-robustness.test.mjs tests/bootstrapbench-run.test.mjs tests/bootstrapbench-stats.test.mjs tests/bootstrapbench-aggregate.test.mjs tests/bootstrapbench-cleanup.test.mjs tests/query-cli-shim.test.mjs",
67
+ "test": "node tests/context-regressions.test.mjs && node --test tests/ingest-units.test.mjs tests/ingest-parallel.test.mjs tests/ingest-worker-crash.test.mjs tests/javascript-parser.test.mjs tests/markdown-parser.test.mjs tests/sql-parser.test.mjs tests/config-parser.test.mjs tests/resources-parser.test.mjs tests/vbnet-parser.test.mjs tests/cpp-parser.test.mjs tests/dashboard.test.mjs tests/init-config.test.mjs tests/init-agents.test.mjs tests/multi-level.test.mjs tests/no-legacy-paths.test.mjs tests/tree-sitter-error-reporting.test.mjs tests/tree-sitter-body-cap.test.mjs tests/tree-sitter-exported.test.mjs tests/tree-sitter-robustness.test.mjs tests/bootstrapbench-run.test.mjs tests/bootstrapbench-stats.test.mjs tests/bootstrapbench-aggregate.test.mjs tests/bootstrapbench-cleanup.test.mjs tests/bootstrapbench-query-packs.test.mjs tests/query-cli-shim.test.mjs tests/session-bootstrap.test.mjs tests/plugin-skills.test.mjs tests/plugin-manifests.test.mjs",
68
68
  "release:sync-version": "node scripts/sync-release-version.mjs",
69
69
  "release:check-version-sync": "node scripts/sync-release-version.mjs --check",
70
70
  "prepublishOnly": "echo 'Ready to publish to npm'"
@@ -742,9 +742,9 @@
742
742
  "license": "BSD-3-Clause"
743
743
  },
744
744
  "node_modules/@protobufjs/eventemitter": {
745
- "version": "1.1.0",
746
- "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
747
- "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==",
745
+ "version": "1.1.1",
746
+ "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz",
747
+ "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==",
748
748
  "license": "BSD-3-Clause"
749
749
  },
750
750
  "node_modules/@protobufjs/fetch": {
@@ -762,12 +762,6 @@
762
762
  "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
763
763
  "license": "BSD-3-Clause"
764
764
  },
765
- "node_modules/@protobufjs/inquire": {
766
- "version": "1.1.2",
767
- "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.2.tgz",
768
- "integrity": "sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw==",
769
- "license": "BSD-3-Clause"
770
- },
771
765
  "node_modules/@protobufjs/path": {
772
766
  "version": "1.1.2",
773
767
  "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
@@ -1865,9 +1859,19 @@
1865
1859
  }
1866
1860
  },
1867
1861
  "node_modules/js-yaml": {
1868
- "version": "4.1.1",
1869
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
1870
- "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
1862
+ "version": "4.2.0",
1863
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz",
1864
+ "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==",
1865
+ "funding": [
1866
+ {
1867
+ "type": "github",
1868
+ "url": "https://github.com/sponsors/puzrin"
1869
+ },
1870
+ {
1871
+ "type": "github",
1872
+ "url": "https://github.com/sponsors/nodeca"
1873
+ }
1874
+ ],
1871
1875
  "license": "MIT",
1872
1876
  "dependencies": {
1873
1877
  "argparse": "^2.0.1"
@@ -2128,24 +2132,23 @@
2128
2132
  "license": "MIT"
2129
2133
  },
2130
2134
  "node_modules/protobufjs": {
2131
- "version": "7.5.9",
2132
- "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.9.tgz",
2133
- "integrity": "sha512-Od4muIm3HW1AouyHF5lONOf1FWo3hY1NbFDoy191X9GzhpgW1clCoaFjfVs2rKJNFYpTNJbje4cbAIDBZJ63ZA==",
2135
+ "version": "7.6.4",
2136
+ "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.4.tgz",
2137
+ "integrity": "sha512-RJJPTTpvFfHcWLkIa2JFWK4XvtSzS0yEWDmunqHXli1h3JlkbcQZXDZdcWxv+JK3Xsl5/UFDPZ0iGm7DAengYw==",
2134
2138
  "hasInstallScript": true,
2135
2139
  "license": "BSD-3-Clause",
2136
2140
  "dependencies": {
2137
2141
  "@protobufjs/aspromise": "^1.1.2",
2138
2142
  "@protobufjs/base64": "^1.1.2",
2139
2143
  "@protobufjs/codegen": "^2.0.5",
2140
- "@protobufjs/eventemitter": "^1.1.0",
2144
+ "@protobufjs/eventemitter": "^1.1.1",
2141
2145
  "@protobufjs/fetch": "^1.1.1",
2142
2146
  "@protobufjs/float": "^1.0.2",
2143
- "@protobufjs/inquire": "^1.1.2",
2144
2147
  "@protobufjs/path": "^1.1.2",
2145
2148
  "@protobufjs/pool": "^1.1.0",
2146
2149
  "@protobufjs/utf8": "^1.1.1",
2147
2150
  "@types/node": ">=13.7.0",
2148
- "long": "^5.0.0"
2151
+ "long": "^5.3.2"
2149
2152
  },
2150
2153
  "engines": {
2151
2154
  "node": ">=12.0.0"
@@ -2539,9 +2542,9 @@
2539
2542
  }
2540
2543
  },
2541
2544
  "node_modules/tar": {
2542
- "version": "7.5.11",
2543
- "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.11.tgz",
2544
- "integrity": "sha512-ChjMH33/KetonMTAtpYdgUFr0tbz69Fp2v7zWxQfYZX4g5ZN2nOBXm1R2xyA+lMIKrLKIoKAwFj93jE/avX9cQ==",
2545
+ "version": "7.5.16",
2546
+ "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.16.tgz",
2547
+ "integrity": "sha512-56adEpPMouktRlBLXiaYFFzZ/3+JXa8P9n7WbR+ibIjtviN55mEaOkiysCnPnWm+7kkui1Dn8J9l+g6zV8731w==",
2545
2548
  "license": "BlueOak-1.0.0",
2546
2549
  "dependencies": {
2547
2550
  "@isaacs/fs-minipass": "^4.0.0",
@@ -1,7 +1,9 @@
1
1
  import { runContextRules } from "../rules.js";
2
+ import { runPatternEvidence } from "../patternEvidence.js";
2
3
  import { runContextImpact, runContextRelated, runContextSearch } from "../search.js";
3
4
  import type {
4
5
  ImpactParams,
6
+ PatternEvidenceParams,
5
7
  RelatedParams,
6
8
  RelationType,
7
9
  RulesParams,
@@ -29,7 +31,7 @@ type JsonEnvelope = {
29
31
  };
30
32
  };
31
33
 
32
- const QUERY_COMMANDS = new Set(["search", "related", "impact", "rules", "explain"]);
34
+ const QUERY_COMMANDS = new Set(["search", "related", "impact", "rules", "explain", "pattern-evidence"]);
33
35
 
34
36
  const ENTITY_ID_PREFIXES = [
35
37
  "file:",
@@ -66,6 +68,8 @@ export async function runQueryCommand(args: string[]): Promise<void> {
66
68
  return await runRules(rest);
67
69
  case "explain":
68
70
  return await runExplain(rest);
71
+ case "pattern-evidence":
72
+ return await runPatternEvidenceCommand(rest);
69
73
  default:
70
74
  throw new Error(`Unknown query command: ${command}`);
71
75
  }
@@ -93,6 +97,7 @@ function printHelp(): void {
93
97
  " cortex impact <query-or-entity-id> [--entity-id <id>] [--query <q>] [--depth <n>] [--top-k <n>] [--json]",
94
98
  " cortex rules [--scope <scope>] [--include-inactive] [--json]",
95
99
  " cortex explain <query-or-entity-id> [--top-k <n>] [--json]",
100
+ " cortex pattern-evidence <file-path|entity-id> [--query <text>] [--top-k <n>] [--json]",
96
101
  "",
97
102
  "These commands read the local Cortex graph and emit MCP-equivalent data with --json.",
98
103
  ];
@@ -157,7 +162,11 @@ function parsePositiveIntFlag(
157
162
  if (typeof raw !== "string" || raw.trim().length === 0) {
158
163
  throw new Error(`--${name} requires a numeric value`);
159
164
  }
160
- const parsed = Number.parseInt(raw, 10);
165
+ const normalized = raw.trim();
166
+ if (!/^[1-9]\d*$/.test(normalized)) {
167
+ throw new Error(`--${name} must be an integer from 1 to ${max}`);
168
+ }
169
+ const parsed = Number(normalized);
161
170
  if (!Number.isInteger(parsed) || parsed <= 0 || parsed > max) {
162
171
  throw new Error(`--${name} must be an integer from 1 to ${max}`);
163
172
  }
@@ -453,3 +462,28 @@ async function runExplain(args: string[]): Promise<void> {
453
462
  }
454
463
  printSummary("explain", data);
455
464
  }
465
+
466
+ async function runPatternEvidenceCommand(args: string[]): Promise<void> {
467
+ const { flags, rest } = parseArgs(args);
468
+ const target = optionalString(flags, "target") ?? positionalText(rest, "file-path-or-entity-id");
469
+ const input: PatternEvidenceParams = {
470
+ target,
471
+ query: optionalString(flags, "query"),
472
+ top_k: parsePositiveIntFlag(flags, "top-k", 3, 10),
473
+ include_deprecated: isFlagEnabled(flags, "include-deprecated"),
474
+ };
475
+ const data = await runPatternEvidence(input);
476
+ if (isFlagEnabled(flags, "json")) {
477
+ emitEnvelope("pattern-evidence", input, data);
478
+ return;
479
+ }
480
+
481
+ const tiers = Array.isArray(data.tiers) ? data.tiers : [];
482
+ process.stdout.write(`pattern-evidence: local_pattern_found=${String(data.local_pattern_found)}\n`);
483
+ for (const value of tiers) {
484
+ if (!value || typeof value !== "object") continue;
485
+ const tier = value as Record<string, unknown>;
486
+ const evidence = Array.isArray(tier.evidence) ? tier.evidence : [];
487
+ process.stdout.write(`- ${String(tier.name ?? "")}: ${evidence.length}\n`);
488
+ }
489
+ }