@danielblomma/cortex-mcp 2.4.0 → 2.4.1

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.
Files changed (173) hide show
  1. package/CHANGELOG.md +201 -0
  2. package/README.md +19 -3
  3. package/bin/cortex.mjs +308 -75
  4. package/bin/daemon-control.mjs +162 -0
  5. package/package.json +7 -3
  6. package/scaffold/mcp/dist/.cortex-build-hash +1 -0
  7. package/scaffold/mcp/dist/cli/enterprise-setup.js +134 -0
  8. package/scaffold/mcp/dist/cli/govern.js +937 -0
  9. package/scaffold/mcp/dist/cli/query.js +409 -0
  10. package/scaffold/mcp/dist/cli/run.js +295 -0
  11. package/scaffold/mcp/dist/cli/stage.js +308 -0
  12. package/scaffold/mcp/dist/cli/telemetry-test.js +141 -0
  13. package/scaffold/mcp/dist/cli/ungoverned-detector.js +133 -0
  14. package/scaffold/mcp/dist/contextEntities.js +282 -0
  15. package/scaffold/mcp/dist/core/audit/query.js +72 -0
  16. package/scaffold/mcp/dist/core/audit/writer.js +28 -0
  17. package/scaffold/mcp/dist/core/config.js +235 -0
  18. package/scaffold/mcp/dist/core/enterprise-host-identity.js +193 -0
  19. package/scaffold/mcp/dist/core/enterprise-identity.js +18 -0
  20. package/scaffold/mcp/dist/core/enterprise-rotation.js +158 -0
  21. package/scaffold/mcp/dist/core/govern-paths.js +21 -0
  22. package/scaffold/mcp/dist/core/index.js +14 -0
  23. package/scaffold/mcp/dist/core/license.js +278 -0
  24. package/scaffold/mcp/dist/core/policy/enforce.js +66 -0
  25. package/scaffold/mcp/dist/core/policy/injection.js +172 -0
  26. package/scaffold/mcp/dist/core/policy/store.js +179 -0
  27. package/scaffold/mcp/dist/core/rbac/check.js +30 -0
  28. package/scaffold/mcp/dist/core/secure-endpoint.js +25 -0
  29. package/scaffold/mcp/dist/core/telemetry/collector.js +285 -0
  30. package/scaffold/mcp/dist/core/telemetry/state-dir.js +31 -0
  31. package/scaffold/mcp/dist/core/validators/builtins.js +632 -0
  32. package/scaffold/mcp/dist/core/validators/config.js +44 -0
  33. package/scaffold/mcp/dist/core/validators/engine.js +120 -0
  34. package/scaffold/mcp/dist/core/validators/evaluators/code_comments.js +236 -0
  35. package/scaffold/mcp/dist/core/validators/evaluators/regex.js +116 -0
  36. package/scaffold/mcp/dist/core/workflow/artifact-io.js +103 -0
  37. package/scaffold/mcp/dist/core/workflow/capabilities.js +88 -0
  38. package/scaffold/mcp/dist/core/workflow/default-workflows.js +102 -0
  39. package/scaffold/mcp/dist/core/workflow/enforcement.js +164 -0
  40. package/scaffold/mcp/dist/core/workflow/envelope.js +132 -0
  41. package/scaffold/mcp/dist/core/workflow/index.js +11 -0
  42. package/scaffold/mcp/dist/core/workflow/mcp-tools.js +175 -0
  43. package/scaffold/mcp/dist/core/workflow/resolution.js +69 -0
  44. package/scaffold/mcp/dist/core/workflow/run-lifecycle.js +123 -0
  45. package/scaffold/mcp/dist/core/workflow/schemas.js +141 -0
  46. package/scaffold/mcp/dist/core/workflow/synced-capability-registry.js +60 -0
  47. package/scaffold/mcp/dist/core/workflow/synced-registry.js +60 -0
  48. package/scaffold/mcp/dist/daemon/capability-sync-checker.js +242 -0
  49. package/scaffold/mcp/dist/daemon/client.js +130 -0
  50. package/scaffold/mcp/dist/daemon/egress-proxy.js +288 -0
  51. package/scaffold/mcp/dist/daemon/global-host-events.js +222 -0
  52. package/scaffold/mcp/dist/daemon/heartbeat-pusher.js +116 -0
  53. package/scaffold/mcp/dist/daemon/heartbeat-tracker.js +165 -0
  54. package/scaffold/mcp/dist/daemon/host-events-pusher.js +249 -0
  55. package/scaffold/mcp/dist/daemon/main.js +449 -0
  56. package/scaffold/mcp/dist/daemon/paths.js +36 -0
  57. package/scaffold/mcp/dist/daemon/project-service-registry.js +78 -0
  58. package/scaffold/mcp/dist/daemon/protocol.js +8 -0
  59. package/scaffold/mcp/dist/daemon/server.js +180 -0
  60. package/scaffold/mcp/dist/daemon/skill-sync-checker.js +557 -0
  61. package/scaffold/mcp/dist/daemon/sync-checker.js +165 -0
  62. package/scaffold/mcp/dist/daemon/ungoverned-scanner.js +136 -0
  63. package/scaffold/mcp/dist/daemon/workflow-sync-checker.js +249 -0
  64. package/scaffold/mcp/dist/defaults.js +6 -0
  65. package/scaffold/mcp/dist/embed.js +627 -0
  66. package/scaffold/mcp/dist/embedScheduler.js +479 -0
  67. package/scaffold/mcp/dist/embeddings.js +167 -0
  68. package/scaffold/mcp/dist/enterprise/audit/push.js +66 -0
  69. package/scaffold/mcp/dist/enterprise/index.js +327 -0
  70. package/scaffold/mcp/dist/enterprise/model/deploy.js +27 -0
  71. package/scaffold/mcp/dist/enterprise/policy/sync.js +129 -0
  72. package/scaffold/mcp/dist/enterprise/privacy/boundary.js +184 -0
  73. package/scaffold/mcp/dist/enterprise/reviews/changed-files.js +33 -0
  74. package/scaffold/mcp/dist/enterprise/reviews/pattern-context.js +202 -0
  75. package/scaffold/mcp/dist/enterprise/reviews/policy-selection.js +46 -0
  76. package/scaffold/mcp/dist/enterprise/reviews/push.js +102 -0
  77. package/scaffold/mcp/dist/enterprise/reviews/trust-state.js +186 -0
  78. package/scaffold/mcp/dist/enterprise/telemetry/sync.js +57 -0
  79. package/scaffold/mcp/dist/enterprise/tools/enterprise.js +826 -0
  80. package/scaffold/mcp/dist/enterprise/tools/harness.js +40 -0
  81. package/scaffold/mcp/dist/enterprise/tools/walk.js +73 -0
  82. package/scaffold/mcp/dist/enterprise/violations/push.js +77 -0
  83. package/scaffold/mcp/dist/enterprise/workflow/push.js +44 -0
  84. package/scaffold/mcp/dist/enterprise/workflow/state.js +329 -0
  85. package/scaffold/mcp/dist/frontmatter.js +33 -0
  86. package/scaffold/mcp/dist/graph.js +769 -0
  87. package/scaffold/mcp/dist/graphCsv.js +55 -0
  88. package/scaffold/mcp/dist/graphMetrics.js +8 -0
  89. package/scaffold/mcp/dist/hooks/permission-request.js +89 -0
  90. package/scaffold/mcp/dist/hooks/post-tool-use.js +105 -0
  91. package/scaffold/mcp/dist/hooks/pre-compact.js +29 -0
  92. package/scaffold/mcp/dist/hooks/pre-tool-use.js +78 -0
  93. package/scaffold/mcp/dist/hooks/session-end.js +43 -0
  94. package/scaffold/mcp/dist/hooks/session-start.js +41 -0
  95. package/scaffold/mcp/dist/hooks/shared.js +194 -0
  96. package/scaffold/mcp/dist/hooks/stop.js +28 -0
  97. package/scaffold/mcp/dist/hooks/user-prompt-submit.js +33 -0
  98. package/scaffold/mcp/dist/impactPresentation.js +137 -0
  99. package/scaffold/mcp/dist/impactRanking.js +191 -0
  100. package/scaffold/mcp/dist/impactResponse.js +30 -0
  101. package/scaffold/mcp/dist/impactResults.js +105 -0
  102. package/scaffold/mcp/dist/impactSeed.js +20 -0
  103. package/scaffold/mcp/dist/impactTraversal.js +64 -0
  104. package/scaffold/mcp/dist/jsonl.js +77 -0
  105. package/scaffold/mcp/dist/loadGraph.js +759 -0
  106. package/scaffold/mcp/dist/lruCache.js +38 -0
  107. package/scaffold/mcp/dist/paths.js +97 -0
  108. package/scaffold/mcp/dist/patternEvidence.js +272 -0
  109. package/scaffold/mcp/dist/plugin.js +81 -0
  110. package/scaffold/mcp/dist/presets.js +78 -0
  111. package/scaffold/mcp/dist/relatedResponse.js +18 -0
  112. package/scaffold/mcp/dist/relatedTraversal.js +78 -0
  113. package/scaffold/mcp/dist/rules.js +23 -0
  114. package/scaffold/mcp/dist/search.js +212 -0
  115. package/scaffold/mcp/dist/searchCore.js +457 -0
  116. package/scaffold/mcp/dist/searchResults.js +230 -0
  117. package/scaffold/mcp/dist/server.js +317 -0
  118. package/scaffold/mcp/dist/types.js +1 -0
  119. package/scaffold/mcp/package-lock.json +321 -200
  120. package/scaffold/mcp/package.json +15 -6
  121. package/scaffold/mcp/src/cli/enterprise-setup.ts +82 -8
  122. package/scaffold/mcp/src/cli/govern.ts +137 -52
  123. package/scaffold/mcp/src/cli/run.ts +53 -19
  124. package/scaffold/mcp/src/cli/stage.ts +8 -3
  125. package/scaffold/mcp/src/core/config.ts +9 -2
  126. package/scaffold/mcp/src/core/enterprise-host-identity.ts +259 -0
  127. package/scaffold/mcp/src/core/enterprise-identity.ts +20 -0
  128. package/scaffold/mcp/src/core/enterprise-rotation.ts +185 -0
  129. package/scaffold/mcp/src/core/govern-paths.ts +30 -0
  130. package/scaffold/mcp/src/core/license.ts +186 -17
  131. package/scaffold/mcp/src/core/secure-endpoint.ts +23 -0
  132. package/scaffold/mcp/src/core/workflow/enforcement.ts +8 -1
  133. package/scaffold/mcp/src/core/workflow/mcp-tools.ts +3 -0
  134. package/scaffold/mcp/src/core/workflow/resolution.ts +9 -1
  135. package/scaffold/mcp/src/core/workflow/synced-capability-registry.ts +15 -0
  136. package/scaffold/mcp/src/core/workflow/synced-registry.ts +15 -0
  137. package/scaffold/mcp/src/daemon/capability-sync-checker.ts +38 -2
  138. package/scaffold/mcp/src/daemon/egress-proxy.ts +14 -8
  139. package/scaffold/mcp/src/daemon/global-host-events.ts +288 -0
  140. package/scaffold/mcp/src/daemon/heartbeat-pusher.ts +4 -0
  141. package/scaffold/mcp/src/daemon/heartbeat-tracker.ts +2 -0
  142. package/scaffold/mcp/src/daemon/host-events-pusher.ts +5 -0
  143. package/scaffold/mcp/src/daemon/main.ts +99 -25
  144. package/scaffold/mcp/src/daemon/project-service-registry.ts +107 -0
  145. package/scaffold/mcp/src/daemon/skill-sync-checker.ts +368 -31
  146. package/scaffold/mcp/src/daemon/sync-checker.ts +4 -0
  147. package/scaffold/mcp/src/daemon/ungoverned-scanner.ts +57 -25
  148. package/scaffold/mcp/src/daemon/workflow-sync-checker.ts +41 -2
  149. package/scaffold/mcp/src/enterprise/audit/push.ts +8 -0
  150. package/scaffold/mcp/src/enterprise/policy/sync.ts +12 -0
  151. package/scaffold/mcp/src/enterprise/reviews/push.ts +9 -0
  152. package/scaffold/mcp/src/enterprise/reviews/trust-state.ts +3 -1
  153. package/scaffold/mcp/src/enterprise/telemetry/sync.ts +9 -0
  154. package/scaffold/mcp/src/enterprise/violations/push.ts +9 -0
  155. package/scaffold/mcp/src/enterprise/workflow/push.ts +7 -0
  156. package/scaffold/mcp/src/plugin.ts +20 -0
  157. package/scaffold/mcp/tests/copilot-shim.test.mjs +19 -1
  158. package/scaffold/mcp/tests/egress-proxy.test.mjs +37 -0
  159. package/scaffold/mcp/tests/enterprise-identity-sync.test.mjs +401 -0
  160. package/scaffold/mcp/tests/enterprise-setup.test.mjs +189 -0
  161. package/scaffold/mcp/tests/global-host-events.test.mjs +81 -0
  162. package/scaffold/mcp/tests/govern-install.test.mjs +95 -2
  163. package/scaffold/mcp/tests/govern-repair.test.mjs +20 -3
  164. package/scaffold/mcp/tests/heartbeat-tracker.test.mjs +26 -0
  165. package/scaffold/mcp/tests/license.test.mjs +367 -0
  166. package/scaffold/mcp/tests/project-service-registry.test.mjs +172 -0
  167. package/scaffold/mcp/tests/review-trust-contract.test.mjs +18 -0
  168. package/scaffold/mcp/tests/secure-enterprise-endpoint.test.mjs +46 -0
  169. package/scaffold/mcp/tests/skill-sync-checker.test.mjs +446 -2
  170. package/scaffold/mcp/tests/ungoverned-scanner.test.mjs +104 -22
  171. package/scaffold/mcp/tests/workflow-cli.test.mjs +14 -1
  172. package/scaffold/mcp/tests/workflow-synced-capabilities.test.mjs +37 -0
  173. package/scaffold/mcp/tests/workflow-synced-registry.test.mjs +40 -1
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Minimal LRU cache on Map insertion order: reads refresh recency, writes
3
+ * evict the least-recently-used entry once capacity is reached.
4
+ */
5
+ export class LruCache {
6
+ capacity;
7
+ entries = new Map();
8
+ constructor(capacity) {
9
+ if (!Number.isInteger(capacity) || capacity < 1) {
10
+ throw new Error(`LruCache capacity must be a positive integer, got ${capacity}`);
11
+ }
12
+ this.capacity = capacity;
13
+ }
14
+ get(key) {
15
+ if (!this.entries.has(key)) {
16
+ return undefined;
17
+ }
18
+ const value = this.entries.get(key);
19
+ this.entries.delete(key);
20
+ this.entries.set(key, value);
21
+ return value;
22
+ }
23
+ set(key, value) {
24
+ if (this.entries.has(key)) {
25
+ this.entries.delete(key);
26
+ }
27
+ else if (this.entries.size >= this.capacity) {
28
+ const oldest = this.entries.keys().next();
29
+ if (!oldest.done) {
30
+ this.entries.delete(oldest.value);
31
+ }
32
+ }
33
+ this.entries.set(key, value);
34
+ }
35
+ get size() {
36
+ return this.entries.size;
37
+ }
38
+ }
@@ -0,0 +1,97 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ const __filename = fileURLToPath(import.meta.url);
5
+ const __dirname = path.dirname(__filename);
6
+ function normalizeForWsl(rawPath) {
7
+ const winMatch = rawPath.match(/^([A-Za-z]):[/\\](.*)/);
8
+ if (!winMatch)
9
+ return rawPath;
10
+ try {
11
+ const version = fs.readFileSync("/proc/version", "utf8");
12
+ if (!/microsoft|wsl/i.test(version))
13
+ return rawPath;
14
+ }
15
+ catch {
16
+ return rawPath;
17
+ }
18
+ const drive = winMatch[1].toLowerCase();
19
+ const rest = winMatch[2].replace(/\\/g, "/").replace(/\/+$/, "");
20
+ return `/mnt/${drive}/${rest}`;
21
+ }
22
+ const PROJECT_ROOT_OVERRIDE = process.env.CORTEX_PROJECT_ROOT?.trim();
23
+ function hasContextConfig(candidate) {
24
+ return fs.existsSync(path.join(candidate, ".context", "config.yaml"));
25
+ }
26
+ function resolveFrom(startDir) {
27
+ let current = path.resolve(startDir);
28
+ while (true) {
29
+ if (hasContextConfig(current)) {
30
+ return current;
31
+ }
32
+ const parent = path.dirname(current);
33
+ if (parent === current) {
34
+ return null;
35
+ }
36
+ current = parent;
37
+ }
38
+ }
39
+ function resolveRepoRoot() {
40
+ const candidates = [
41
+ PROJECT_ROOT_OVERRIDE ? path.resolve(normalizeForWsl(PROJECT_ROOT_OVERRIDE)) : null,
42
+ process.cwd(),
43
+ __dirname,
44
+ process.env.INIT_CWD?.trim() ? path.resolve(normalizeForWsl(process.env.INIT_CWD.trim())) : null
45
+ ].filter((value) => Boolean(value));
46
+ for (const candidate of candidates) {
47
+ const resolved = resolveFrom(candidate);
48
+ if (resolved) {
49
+ return resolved;
50
+ }
51
+ }
52
+ return path.resolve(__dirname, "../../..");
53
+ }
54
+ export const REPO_ROOT = resolveRepoRoot();
55
+ export const CONTEXT_DIR = path.join(REPO_ROOT, ".context");
56
+ export const CACHE_DIR = path.join(CONTEXT_DIR, "cache");
57
+ export const DB_PATH = path.join(CONTEXT_DIR, "db", "graph.ryu");
58
+ export const PATHS = {
59
+ config: path.join(CONTEXT_DIR, "config.yaml"),
60
+ rulesYaml: path.join(CONTEXT_DIR, "rules.yaml"),
61
+ graphManifest: path.join(CACHE_DIR, "graph-manifest.json"),
62
+ embeddingsManifest: path.join(CONTEXT_DIR, "embeddings", "manifest.json"),
63
+ embeddingsEntities: path.join(CONTEXT_DIR, "embeddings", "entities.jsonl"),
64
+ embeddingsModelCache: path.join(CONTEXT_DIR, "embeddings", "models"),
65
+ documents: path.join(CACHE_DIR, "documents.jsonl"),
66
+ adrEntities: path.join(CACHE_DIR, "entities.adr.jsonl"),
67
+ ruleEntities: path.join(CACHE_DIR, "entities.rule.jsonl"),
68
+ chunkEntities: path.join(CACHE_DIR, "entities.chunk.jsonl"),
69
+ projectEntities: path.join(CACHE_DIR, "entities.project.jsonl"),
70
+ constrainsRelations: path.join(CACHE_DIR, "relations.constrains.jsonl"),
71
+ implementsRelations: path.join(CACHE_DIR, "relations.implements.jsonl"),
72
+ supersedesRelations: path.join(CACHE_DIR, "relations.supersedes.jsonl"),
73
+ callsRelations: path.join(CACHE_DIR, "relations.calls.jsonl"),
74
+ callsSqlRelations: path.join(CACHE_DIR, "relations.calls_sql.jsonl"),
75
+ usesConfigKeyRelations: path.join(CACHE_DIR, "relations.uses_config_key.jsonl"),
76
+ usesResourceKeyRelations: path.join(CACHE_DIR, "relations.uses_resource_key.jsonl"),
77
+ usesSettingKeyRelations: path.join(CACHE_DIR, "relations.uses_setting_key.jsonl"),
78
+ definesRelations: path.join(CACHE_DIR, "relations.defines.jsonl"),
79
+ importsRelations: path.join(CACHE_DIR, "relations.imports.jsonl"),
80
+ moduleEntities: path.join(CACHE_DIR, "entities.module.jsonl"),
81
+ containsRelations: path.join(CACHE_DIR, "relations.contains.jsonl"),
82
+ containsModuleRelations: path.join(CACHE_DIR, "relations.contains_module.jsonl"),
83
+ exportsRelations: path.join(CACHE_DIR, "relations.exports.jsonl"),
84
+ includesFileRelations: path.join(CACHE_DIR, "relations.includes_file.jsonl"),
85
+ referencesProjectRelations: path.join(CACHE_DIR, "relations.references_project.jsonl"),
86
+ usesResourceRelations: path.join(CACHE_DIR, "relations.uses_resource.jsonl"),
87
+ usesSettingRelations: path.join(CACHE_DIR, "relations.uses_setting.jsonl"),
88
+ usesConfigRelations: path.join(CACHE_DIR, "relations.uses_config.jsonl"),
89
+ transformsConfigRelations: path.join(CACHE_DIR, "relations.transforms_config.jsonl")
90
+ };
91
+ // Tuned together with the midrank-percentile graph_score in searchResults.ts.
92
+ export const DEFAULT_RANKING = {
93
+ semantic: 0.4,
94
+ graph: 0.25,
95
+ trust: 0.2,
96
+ recency: 0.15
97
+ };
@@ -0,0 +1,272 @@
1
+ import { loadContextData } from "./graph.js";
2
+ import { iterateSearchEntities } from "./contextEntities.js";
3
+ import { embedQuery, loadEmbeddingIndex } from "./embeddings.js";
4
+ import { runContextSearch } from "./search.js";
5
+ const EVIDENCE_TIERS = [
6
+ { name: "same_file", scope: "Same file as the review target." },
7
+ { name: "same_module", scope: "Same directory or module as the review target." },
8
+ { name: "same_feature_area", scope: "Same parent feature area as the review target." },
9
+ { name: "repo_wide", scope: "Repository-wide fallback evidence." },
10
+ ];
11
+ export function normalizeRepoPath(value) {
12
+ const normalized = value.trim().replace(/\\/g, "/").replace(/^\.\//, "");
13
+ return normalized.replace(/\/{2,}/g, "/").replace(/\/$/, "");
14
+ }
15
+ function dirnameRepoPath(value) {
16
+ const normalized = normalizeRepoPath(value);
17
+ const separator = normalized.lastIndexOf("/");
18
+ return separator === -1 ? "." : normalized.slice(0, separator) || ".";
19
+ }
20
+ function parentRepoPath(value) {
21
+ const normalized = normalizeRepoPath(value);
22
+ if (!normalized || normalized === ".") {
23
+ return null;
24
+ }
25
+ const separator = normalized.lastIndexOf("/");
26
+ return separator === -1 ? "." : normalized.slice(0, separator) || ".";
27
+ }
28
+ function isWithinPath(candidate, directory) {
29
+ if (!directory || directory === ".") {
30
+ return false;
31
+ }
32
+ return candidate === directory || candidate.startsWith(`${directory}/`);
33
+ }
34
+ function resolvePatternTarget(data, input) {
35
+ const normalizedInput = normalizeRepoPath(input);
36
+ const document = data.documents.find((entry) => entry.id === input || normalizeRepoPath(entry.path) === normalizedInput);
37
+ if (document) {
38
+ return {
39
+ input,
40
+ entity_id: document.id,
41
+ entity_type: document.kind === "ADR" ? "ADR" : "File",
42
+ path: normalizeRepoPath(document.path),
43
+ };
44
+ }
45
+ const chunk = data.chunks.find((entry) => entry.id === input);
46
+ if (chunk) {
47
+ const owner = data.documents.find((entry) => entry.id === chunk.file_id);
48
+ if (!owner) {
49
+ throw new Error(`Pattern target chunk has no indexed owner file: ${input}`);
50
+ }
51
+ return {
52
+ input,
53
+ entity_id: chunk.id,
54
+ entity_type: "Chunk",
55
+ path: normalizeRepoPath(owner.path),
56
+ };
57
+ }
58
+ const adr = data.adrs.find((entry) => entry.id === input);
59
+ if (adr?.path) {
60
+ return {
61
+ input,
62
+ entity_id: adr.id,
63
+ entity_type: "ADR",
64
+ path: normalizeRepoPath(adr.path),
65
+ };
66
+ }
67
+ const knownEntity = [
68
+ ...data.rules.map((entry) => entry.id),
69
+ ...data.modules.map((entry) => entry.id),
70
+ ...data.projects.map((entry) => entry.id),
71
+ ].includes(input);
72
+ if (knownEntity) {
73
+ throw new Error(`Pattern target is not file-backed: ${input}`);
74
+ }
75
+ throw new Error(`Pattern target was not found in indexed context: ${input}`);
76
+ }
77
+ function derivePatternQuery(data, target) {
78
+ if (target.entity_type === "Chunk") {
79
+ const chunk = data.chunks.find((entry) => entry.id === target.entity_id);
80
+ if (chunk) {
81
+ return [chunk.name, chunk.kind, chunk.signature].filter(Boolean).join(" ");
82
+ }
83
+ }
84
+ const document = data.documents.find((entry) => entry.id === target.entity_id);
85
+ const chunkSignals = data.chunks
86
+ .filter((entry) => entry.file_id === target.entity_id && !entry.id.includes(":window:"))
87
+ .slice(0, 12)
88
+ .flatMap((entry) => [entry.name, entry.kind])
89
+ .filter(Boolean);
90
+ const basename = target.path.split("/").at(-1)?.replace(/\.[^.]+$/, "") ?? target.path;
91
+ return [basename, ...chunkSignals, document?.excerpt ?? ""].filter(Boolean).join(" ").slice(0, 1000);
92
+ }
93
+ function tierForPath(targetPath, candidatePath) {
94
+ const normalizedTarget = normalizeRepoPath(targetPath);
95
+ const normalizedCandidate = normalizeRepoPath(candidatePath);
96
+ if (normalizedCandidate === normalizedTarget) {
97
+ return "same_file";
98
+ }
99
+ const targetModule = dirnameRepoPath(normalizedTarget);
100
+ if (dirnameRepoPath(normalizedCandidate) === targetModule) {
101
+ return "same_module";
102
+ }
103
+ const featureArea = parentRepoPath(targetModule);
104
+ if (isWithinPath(normalizedCandidate, featureArea)) {
105
+ return "same_feature_area";
106
+ }
107
+ return "repo_wide";
108
+ }
109
+ function toPatternEvidence(result, chunksById) {
110
+ const id = typeof result.id === "string" ? result.id : "";
111
+ const candidatePath = typeof result.path === "string" ? normalizeRepoPath(result.path) : "";
112
+ if (!id || !candidatePath) {
113
+ return null;
114
+ }
115
+ const chunk = chunksById.get(id);
116
+ const entityType = typeof result.entity_type === "string" ? result.entity_type : "";
117
+ if (entityType === "Chunk" && (!chunk || chunk.start_line <= 0 || chunk.end_line < chunk.start_line)) {
118
+ return null;
119
+ }
120
+ const evidence = {
121
+ id,
122
+ entity_type: entityType,
123
+ kind: typeof result.kind === "string" ? result.kind : "",
124
+ title: typeof result.title === "string" ? result.title : id,
125
+ path: candidatePath,
126
+ excerpt: typeof result.excerpt === "string" ? result.excerpt : "",
127
+ };
128
+ if (chunk) {
129
+ evidence.start_line = chunk.start_line;
130
+ evidence.end_line = chunk.end_line;
131
+ }
132
+ if (typeof result.score === "number") {
133
+ evidence.score = result.score;
134
+ }
135
+ if (Array.isArray(result.matched_rules)) {
136
+ evidence.matched_rules = [...new Set(result.matched_rules)];
137
+ }
138
+ return evidence;
139
+ }
140
+ const referenceTimeCache = new WeakMap();
141
+ export function contextReferenceTimeMs(data) {
142
+ const cached = referenceTimeCache.get(data);
143
+ if (cached !== undefined) {
144
+ return cached;
145
+ }
146
+ let latest = 0;
147
+ const consider = (value) => {
148
+ const timestamp = Date.parse(value);
149
+ if (Number.isFinite(timestamp) && timestamp > latest) {
150
+ latest = timestamp;
151
+ }
152
+ };
153
+ for (const entry of data.documents)
154
+ consider(entry.updated_at);
155
+ for (const entry of data.rules)
156
+ consider(entry.updated_at);
157
+ for (const entry of data.adrs)
158
+ consider(entry.decision_date);
159
+ for (const entry of data.chunks)
160
+ consider(entry.updated_at);
161
+ for (const entry of data.modules)
162
+ consider(entry.updated_at);
163
+ for (const entry of data.projects)
164
+ consider(entry.updated_at);
165
+ referenceTimeCache.set(data, latest);
166
+ return latest;
167
+ }
168
+ export function classifyPatternEvidence(input) {
169
+ const chunksById = new Map(input.chunks.map((chunk) => [chunk.id, chunk]));
170
+ const evidenceByTier = new Map(EVIDENCE_TIERS.map((tier) => [tier.name, []]));
171
+ const seen = new Set();
172
+ for (const result of input.results) {
173
+ if (result.id === input.target.entity_id) {
174
+ continue;
175
+ }
176
+ const evidence = toPatternEvidence(result, chunksById);
177
+ if (!evidence || seen.has(evidence.id)) {
178
+ continue;
179
+ }
180
+ seen.add(evidence.id);
181
+ const tierName = tierForPath(input.target.path, evidence.path);
182
+ const tierEvidence = evidenceByTier.get(tierName);
183
+ if (tierEvidence && tierEvidence.length < input.topK) {
184
+ tierEvidence.push(evidence);
185
+ }
186
+ }
187
+ const tiers = EVIDENCE_TIERS.map((tier) => ({
188
+ ...tier,
189
+ evidence: evidenceByTier.get(tier.name) ?? [],
190
+ }));
191
+ const localPatternFound = tiers.slice(0, 3).some((tier) => tier.evidence.length > 0);
192
+ const fallbackUsed = !localPatternFound && tiers[3].evidence.length > 0;
193
+ return { tiers, localPatternFound, fallbackUsed };
194
+ }
195
+ export async function runPatternEvidence(parsed, options = {}) {
196
+ const data = options.data ?? await loadContextData();
197
+ const target = resolvePatternTarget(data, parsed.target);
198
+ const explicitQuery = parsed.query?.trim();
199
+ const query = explicitQuery || derivePatternQuery(data, target);
200
+ if (!query) {
201
+ throw new Error(`Could not derive a pattern query for target: ${parsed.target}`);
202
+ }
203
+ const referenceTimeMs = contextReferenceTimeMs(data);
204
+ const embeddingIndex = options.use_embeddings === false
205
+ ? { model: null, vectors: new Map() }
206
+ : loadEmbeddingIndex();
207
+ const queryVector = embeddingIndex.model && embeddingIndex.vectors.size > 0
208
+ ? await embedQuery(query, embeddingIndex.model)
209
+ : null;
210
+ const tierByEntityId = new Map();
211
+ for (const entity of iterateSearchEntities(data, false)) {
212
+ if (entity.id !== target.entity_id &&
213
+ entity.path &&
214
+ (entity.entity_type === "File" || entity.entity_type === "Chunk" || entity.entity_type === "ADR")) {
215
+ tierByEntityId.set(entity.id, tierForPath(target.path, entity.path));
216
+ }
217
+ }
218
+ const searchResults = [];
219
+ const warningParts = [];
220
+ let contextSource = data.source;
221
+ let semanticEngine;
222
+ for (const tier of EVIDENCE_TIERS) {
223
+ const search = await runContextSearch({
224
+ query,
225
+ top_k: parsed.top_k,
226
+ include_deprecated: parsed.include_deprecated ?? false,
227
+ response_preset: "full",
228
+ include_scores: true,
229
+ include_matched_rules: true,
230
+ }, {
231
+ data,
232
+ reference_time_ms: referenceTimeMs,
233
+ embedding_index: embeddingIndex,
234
+ query_vector: queryVector,
235
+ candidate_filter: (entity) => tierByEntityId.get(entity.id) === tier.name,
236
+ });
237
+ if (Array.isArray(search.results)) {
238
+ searchResults.push(...search.results);
239
+ }
240
+ if (typeof search.warning === "string" && !warningParts.includes(search.warning)) {
241
+ warningParts.push(search.warning);
242
+ }
243
+ contextSource = search.context_source ?? contextSource;
244
+ semanticEngine = search.semantic_engine ?? semanticEngine;
245
+ }
246
+ const classified = classifyPatternEvidence({
247
+ target,
248
+ results: searchResults,
249
+ chunks: data.chunks,
250
+ topK: parsed.top_k,
251
+ });
252
+ if (!classified.localPatternFound) {
253
+ warningParts.push("No applicable file-local, module-local, or feature-local pattern evidence was found.");
254
+ }
255
+ return {
256
+ target,
257
+ query,
258
+ query_source: explicitQuery ? "explicit" : "derived_from_target",
259
+ evidence_order: EVIDENCE_TIERS.map((tier) => tier.name),
260
+ top_k_per_tier: parsed.top_k,
261
+ ranking_reference_time: referenceTimeMs > 0 ? new Date(referenceTimeMs).toISOString() : null,
262
+ local_pattern_found: classified.localPatternFound,
263
+ fallback_used: classified.fallbackUsed,
264
+ tiers: classified.tiers,
265
+ context_source: contextSource,
266
+ semantic_engine: semanticEngine,
267
+ warning: warningParts.length > 0 ? warningParts.join(" | ") : undefined,
268
+ };
269
+ }
270
+ export async function runLocalPatternEvidence(parsed, options = {}) {
271
+ return runPatternEvidence(parsed, { ...options, use_embeddings: false });
272
+ }
@@ -0,0 +1,81 @@
1
+ let loadedEdition = { edition: "community" };
2
+ let toolCallHook = null;
3
+ let sessionEndHook = null;
4
+ let toolEventHook = null;
5
+ let sessionEventHook = null;
6
+ export function getEdition() {
7
+ return loadedEdition;
8
+ }
9
+ export function getToolCallHook() {
10
+ return toolCallHook;
11
+ }
12
+ export function getSessionEndHook() {
13
+ return sessionEndHook;
14
+ }
15
+ export function getToolEventHook() {
16
+ return toolEventHook;
17
+ }
18
+ export function getSessionEventHook() {
19
+ return sessionEventHook;
20
+ }
21
+ export async function loadPlugins(server) {
22
+ // v2.0.0: enterprise is now in-process. We still gate registration on
23
+ // whether enterprise activation succeeds (license + config), so community
24
+ // users get a no-op when no api_key is present.
25
+ try {
26
+ const enterprise = await import("./enterprise/index.js");
27
+ const { resolveEnterpriseActivation, loadEnterpriseConfig } = await import("./core/config.js");
28
+ const path = await import("node:path");
29
+ const projectRoot = process.env.CORTEX_PROJECT_ROOT?.trim() || process.cwd();
30
+ const contextDir = path.join(projectRoot, ".context");
31
+ const config = loadEnterpriseConfig(contextDir);
32
+ const activation = resolveEnterpriseActivation(config);
33
+ if (!activation.active) {
34
+ // Community mode: no api_key or invalid config. Skip registration.
35
+ return;
36
+ }
37
+ // v2.0.0: validate license against cortex-web before activating.
38
+ // The verifier uses 24h cache + 7d grace period so transient endpoint
39
+ // outages don't degrade enterprise users immediately.
40
+ const { verifyLicense } = await import("./core/license.js");
41
+ const license = await verifyLicense(contextDir, config.enterprise.endpoint, config.enterprise.api_key, {
42
+ client_version: process.env.CORTEX_VERSION,
43
+ });
44
+ if (!license.valid) {
45
+ process.stderr.write(`[cortex] Enterprise inactive: license check failed (${license.reason}, source=${license.source})\n`);
46
+ return;
47
+ }
48
+ const [{ matchesEnterpriseHostIdentity }, { enterpriseCredentialId },] = await Promise.all([
49
+ import("./core/enterprise-host-identity.js"),
50
+ import("./core/license.js"),
51
+ ]);
52
+ if (!matchesEnterpriseHostIdentity(enterpriseCredentialId(config.enterprise.base_url || config.enterprise.endpoint, config.enterprise.api_key))) {
53
+ process.stderr.write("[cortex] Enterprise inactive: this user profile is enrolled to another endpoint or API key\n");
54
+ return;
55
+ }
56
+ if (typeof enterprise.register === "function") {
57
+ await enterprise.register(server);
58
+ loadedEdition = {
59
+ edition: "enterprise",
60
+ name: enterprise.name ?? "enterprise",
61
+ version: enterprise.version ?? "unknown",
62
+ };
63
+ if (typeof enterprise.onToolCall === "function") {
64
+ toolCallHook = enterprise.onToolCall;
65
+ }
66
+ if (typeof enterprise.onSessionEnd === "function") {
67
+ sessionEndHook = enterprise.onSessionEnd;
68
+ }
69
+ if (typeof enterprise.onToolEvent === "function") {
70
+ toolEventHook = enterprise.onToolEvent;
71
+ }
72
+ if (typeof enterprise.onSessionEvent === "function") {
73
+ sessionEventHook = enterprise.onSessionEvent;
74
+ }
75
+ process.stderr.write(`[cortex] Enterprise loaded: ${loadedEdition.version}\n`);
76
+ }
77
+ }
78
+ catch (error) {
79
+ process.stderr.write(`[cortex] Enterprise activation failed: ${error instanceof Error ? error.message : "unknown error"}\n`);
80
+ }
81
+ }
@@ -0,0 +1,78 @@
1
+ function resolvePresetOptions(responsePreset, presetDefaultsByMode, explicitOverrides) {
2
+ const resolvedPreset = responsePreset ?? "full";
3
+ const presetDefaults = presetDefaultsByMode[resolvedPreset];
4
+ return {
5
+ responsePreset: resolvedPreset,
6
+ ...presetDefaults,
7
+ ...Object.fromEntries(Object.entries(explicitOverrides).filter(([, value]) => value !== undefined))
8
+ };
9
+ }
10
+ export function resolveImpactResponsePreset(parsed) {
11
+ return resolvePresetOptions(parsed.response_preset, {
12
+ full: {
13
+ includeScores: true,
14
+ includeReasons: true,
15
+ verbosePaths: true,
16
+ maxPathHopsShown: 2
17
+ },
18
+ compact: {
19
+ includeScores: false,
20
+ includeReasons: true,
21
+ verbosePaths: false,
22
+ maxPathHopsShown: 2
23
+ },
24
+ minimal: {
25
+ includeScores: false,
26
+ includeReasons: false,
27
+ verbosePaths: false,
28
+ maxPathHopsShown: 1
29
+ }
30
+ }, {
31
+ includeScores: parsed.include_scores,
32
+ includeReasons: parsed.include_reasons,
33
+ verbosePaths: parsed.verbose_paths,
34
+ maxPathHopsShown: parsed.max_path_hops_shown
35
+ });
36
+ }
37
+ export function resolveSearchResponsePreset(parsed) {
38
+ return resolvePresetOptions(parsed.response_preset, {
39
+ full: {
40
+ includeScores: true,
41
+ includeMatchedRules: true,
42
+ includeContent: false
43
+ },
44
+ compact: {
45
+ includeScores: false,
46
+ includeMatchedRules: true,
47
+ includeContent: false
48
+ },
49
+ minimal: {
50
+ includeScores: false,
51
+ includeMatchedRules: false,
52
+ includeContent: false
53
+ }
54
+ }, {
55
+ includeScores: parsed.include_scores,
56
+ includeMatchedRules: parsed.include_matched_rules,
57
+ includeContent: parsed.include_content
58
+ });
59
+ }
60
+ export function resolveRelatedResponsePreset(parsed) {
61
+ return resolvePresetOptions(parsed.response_preset, {
62
+ full: {
63
+ includeEdges: true,
64
+ includeEntityMetadata: true
65
+ },
66
+ compact: {
67
+ includeEdges: false,
68
+ includeEntityMetadata: true
69
+ },
70
+ minimal: {
71
+ includeEdges: false,
72
+ includeEntityMetadata: false
73
+ }
74
+ }, {
75
+ includeEdges: parsed.include_edges,
76
+ includeEntityMetadata: parsed.include_entity_metadata
77
+ });
78
+ }
@@ -0,0 +1,18 @@
1
+ export function buildRelatedResponseMeta(params) {
2
+ return {
3
+ entity_id: params.parsed.entity_id,
4
+ depth: params.parsed.depth,
5
+ response_preset: params.responsePreset,
6
+ include_edges: params.includeEdges,
7
+ include_entity_metadata: params.includeEntityMetadata,
8
+ context_source: params.contextSource
9
+ };
10
+ }
11
+ export function buildEmptyRelatedResponse(params) {
12
+ return {
13
+ ...params.meta,
14
+ related: [],
15
+ edges: [],
16
+ warning: params.warning
17
+ };
18
+ }
@@ -0,0 +1,78 @@
1
+ export function traverseRelatedGraph(params) {
2
+ const outgoing = new Map();
3
+ const incoming = new Map();
4
+ for (const relation of params.relations) {
5
+ const outList = outgoing.get(relation.from) ?? [];
6
+ outList.push(relation);
7
+ outgoing.set(relation.from, outList);
8
+ const inList = incoming.get(relation.to) ?? [];
9
+ inList.push(relation);
10
+ incoming.set(relation.to, inList);
11
+ }
12
+ const seen = new Set([params.entityId]);
13
+ const queue = [{ id: params.entityId, hop: 0 }];
14
+ const related = [];
15
+ const traversedEdges = [];
16
+ const traversedEdgeKeys = new Set();
17
+ while (queue.length > 0) {
18
+ const current = queue.shift();
19
+ if (current.hop >= params.depth) {
20
+ continue;
21
+ }
22
+ const neighbors = [
23
+ ...(outgoing.get(current.id) ?? []).map((edge) => ({
24
+ edge,
25
+ next: edge.to,
26
+ direction: "outgoing"
27
+ })),
28
+ ...(incoming.get(current.id) ?? []).map((edge) => ({
29
+ edge,
30
+ next: edge.from,
31
+ direction: "incoming"
32
+ }))
33
+ ];
34
+ for (const neighbor of neighbors) {
35
+ const target = neighbor.next;
36
+ if (!seen.has(target)) {
37
+ seen.add(target);
38
+ queue.push({ id: target, hop: current.hop + 1 });
39
+ const entity = params.catalog.get(target) ?? {
40
+ id: target,
41
+ type: "Unknown",
42
+ label: target,
43
+ status: "unknown",
44
+ source_of_truth: false
45
+ };
46
+ related.push(params.includeEntityMetadata
47
+ ? {
48
+ ...entity,
49
+ hops: current.hop + 1,
50
+ via_relation: neighbor.edge.relation,
51
+ direction: neighbor.direction
52
+ }
53
+ : {
54
+ id: entity.id,
55
+ type: entity.type,
56
+ label: entity.label,
57
+ hops: current.hop + 1,
58
+ via_relation: neighbor.edge.relation,
59
+ direction: neighbor.direction
60
+ });
61
+ }
62
+ const edgeKey = `${neighbor.edge.from}|${neighbor.edge.relation}|${neighbor.edge.to}|${neighbor.edge.note}`;
63
+ if (!traversedEdgeKeys.has(edgeKey)) {
64
+ traversedEdgeKeys.add(edgeKey);
65
+ traversedEdges.push({
66
+ from: neighbor.edge.from,
67
+ to: neighbor.edge.to,
68
+ relation: neighbor.edge.relation,
69
+ note: neighbor.edge.note
70
+ });
71
+ }
72
+ }
73
+ }
74
+ return {
75
+ related,
76
+ traversedEdges
77
+ };
78
+ }
@@ -0,0 +1,23 @@
1
+ import { loadContextData } from "./graph.js";
2
+ export async function runContextRules(parsed) {
3
+ const data = await loadContextData();
4
+ const rules = data.rules
5
+ .filter((rule) => parsed.include_inactive || rule.status === "active")
6
+ .filter((rule) => !parsed.scope || rule.scope === parsed.scope || rule.scope === "global")
7
+ .sort((a, b) => b.priority - a.priority)
8
+ .map((rule) => ({
9
+ id: rule.id,
10
+ title: rule.title,
11
+ description: rule.body,
12
+ priority: rule.priority,
13
+ scope: rule.scope,
14
+ status: rule.status
15
+ }));
16
+ return {
17
+ scope: parsed.scope ?? "global",
18
+ count: rules.length,
19
+ context_source: data.source,
20
+ warning: data.warning,
21
+ rules
22
+ };
23
+ }