@danielblomma/cortex-mcp 2.4.0 → 2.4.2

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 (214) hide show
  1. package/CHANGELOG.md +226 -0
  2. package/README.md +24 -3
  3. package/bin/cli/arguments.mjs +60 -0
  4. package/bin/cli/context-passthrough.mjs +129 -0
  5. package/bin/cli/daemon.mjs +157 -0
  6. package/bin/cli/enterprise.mjs +516 -0
  7. package/bin/cli/help.mjs +88 -0
  8. package/bin/cli/hooks.mjs +199 -0
  9. package/bin/cli/mcp-command.mjs +87 -0
  10. package/bin/cli/paths.mjs +14 -0
  11. package/bin/cli/process.mjs +49 -0
  12. package/bin/cli/project-commands.mjs +237 -0
  13. package/bin/cli/project-runtime.mjs +34 -0
  14. package/bin/cli/query-command.mjs +18 -0
  15. package/bin/cli/router.mjs +66 -0
  16. package/bin/cli/run-command.mjs +44 -0
  17. package/bin/cli/scaffold-ownership.mjs +936 -0
  18. package/bin/cli/scaffold.mjs +687 -0
  19. package/bin/cli/stage-command.mjs +9 -0
  20. package/bin/cli/telemetry-command.mjs +18 -0
  21. package/bin/cli/trusted-runtime.mjs +18 -0
  22. package/bin/cortex.mjs +12 -1607
  23. package/bin/daemon-control.mjs +162 -0
  24. package/mcp-registry-submission.json +1 -1
  25. package/package.json +8 -3
  26. package/scaffold/mcp/dist/.cortex-build-hash +1 -0
  27. package/scaffold/mcp/dist/cli/enterprise-setup.js +134 -0
  28. package/scaffold/mcp/dist/cli/govern.js +937 -0
  29. package/scaffold/mcp/dist/cli/query.js +409 -0
  30. package/scaffold/mcp/dist/cli/run.js +295 -0
  31. package/scaffold/mcp/dist/cli/stage.js +308 -0
  32. package/scaffold/mcp/dist/cli/telemetry-test.js +141 -0
  33. package/scaffold/mcp/dist/cli/ungoverned-detector.js +133 -0
  34. package/scaffold/mcp/dist/contextEntities.js +282 -0
  35. package/scaffold/mcp/dist/core/audit/query.js +72 -0
  36. package/scaffold/mcp/dist/core/audit/writer.js +28 -0
  37. package/scaffold/mcp/dist/core/config.js +235 -0
  38. package/scaffold/mcp/dist/core/enterprise-host-identity.js +193 -0
  39. package/scaffold/mcp/dist/core/enterprise-identity.js +18 -0
  40. package/scaffold/mcp/dist/core/enterprise-rotation.js +158 -0
  41. package/scaffold/mcp/dist/core/govern-paths.js +21 -0
  42. package/scaffold/mcp/dist/core/index.js +14 -0
  43. package/scaffold/mcp/dist/core/license.js +278 -0
  44. package/scaffold/mcp/dist/core/policy/enforce.js +66 -0
  45. package/scaffold/mcp/dist/core/policy/injection.js +172 -0
  46. package/scaffold/mcp/dist/core/policy/store.js +179 -0
  47. package/scaffold/mcp/dist/core/rbac/check.js +30 -0
  48. package/scaffold/mcp/dist/core/secure-endpoint.js +25 -0
  49. package/scaffold/mcp/dist/core/telemetry/collector.js +285 -0
  50. package/scaffold/mcp/dist/core/telemetry/state-dir.js +31 -0
  51. package/scaffold/mcp/dist/core/validators/builtins.js +632 -0
  52. package/scaffold/mcp/dist/core/validators/config.js +44 -0
  53. package/scaffold/mcp/dist/core/validators/engine.js +120 -0
  54. package/scaffold/mcp/dist/core/validators/evaluators/code_comments.js +236 -0
  55. package/scaffold/mcp/dist/core/validators/evaluators/regex.js +116 -0
  56. package/scaffold/mcp/dist/core/workflow/artifact-io.js +103 -0
  57. package/scaffold/mcp/dist/core/workflow/capabilities.js +88 -0
  58. package/scaffold/mcp/dist/core/workflow/default-workflows.js +102 -0
  59. package/scaffold/mcp/dist/core/workflow/enforcement.js +164 -0
  60. package/scaffold/mcp/dist/core/workflow/envelope.js +132 -0
  61. package/scaffold/mcp/dist/core/workflow/index.js +11 -0
  62. package/scaffold/mcp/dist/core/workflow/mcp-tools.js +175 -0
  63. package/scaffold/mcp/dist/core/workflow/resolution.js +69 -0
  64. package/scaffold/mcp/dist/core/workflow/run-lifecycle.js +123 -0
  65. package/scaffold/mcp/dist/core/workflow/schemas.js +141 -0
  66. package/scaffold/mcp/dist/core/workflow/synced-capability-registry.js +60 -0
  67. package/scaffold/mcp/dist/core/workflow/synced-registry.js +60 -0
  68. package/scaffold/mcp/dist/daemon/capability-sync-checker.js +242 -0
  69. package/scaffold/mcp/dist/daemon/client.js +130 -0
  70. package/scaffold/mcp/dist/daemon/egress-proxy.js +288 -0
  71. package/scaffold/mcp/dist/daemon/global-host-events.js +222 -0
  72. package/scaffold/mcp/dist/daemon/heartbeat-pusher.js +116 -0
  73. package/scaffold/mcp/dist/daemon/heartbeat-tracker.js +165 -0
  74. package/scaffold/mcp/dist/daemon/host-events-pusher.js +249 -0
  75. package/scaffold/mcp/dist/daemon/main.js +449 -0
  76. package/scaffold/mcp/dist/daemon/paths.js +36 -0
  77. package/scaffold/mcp/dist/daemon/project-service-registry.js +78 -0
  78. package/scaffold/mcp/dist/daemon/protocol.js +8 -0
  79. package/scaffold/mcp/dist/daemon/server.js +180 -0
  80. package/scaffold/mcp/dist/daemon/skill-sync-checker.js +557 -0
  81. package/scaffold/mcp/dist/daemon/sync-checker.js +165 -0
  82. package/scaffold/mcp/dist/daemon/ungoverned-scanner.js +136 -0
  83. package/scaffold/mcp/dist/daemon/workflow-sync-checker.js +249 -0
  84. package/scaffold/mcp/dist/defaults.js +6 -0
  85. package/scaffold/mcp/dist/embed.js +627 -0
  86. package/scaffold/mcp/dist/embedScheduler.js +479 -0
  87. package/scaffold/mcp/dist/embeddings.js +167 -0
  88. package/scaffold/mcp/dist/enterprise/audit/push.js +66 -0
  89. package/scaffold/mcp/dist/enterprise/index.js +327 -0
  90. package/scaffold/mcp/dist/enterprise/model/deploy.js +27 -0
  91. package/scaffold/mcp/dist/enterprise/policy/sync.js +129 -0
  92. package/scaffold/mcp/dist/enterprise/privacy/boundary.js +184 -0
  93. package/scaffold/mcp/dist/enterprise/reviews/changed-files.js +33 -0
  94. package/scaffold/mcp/dist/enterprise/reviews/pattern-context.js +202 -0
  95. package/scaffold/mcp/dist/enterprise/reviews/policy-selection.js +46 -0
  96. package/scaffold/mcp/dist/enterprise/reviews/push.js +102 -0
  97. package/scaffold/mcp/dist/enterprise/reviews/trust-state.js +186 -0
  98. package/scaffold/mcp/dist/enterprise/telemetry/sync.js +57 -0
  99. package/scaffold/mcp/dist/enterprise/tools/enterprise.js +826 -0
  100. package/scaffold/mcp/dist/enterprise/tools/harness.js +40 -0
  101. package/scaffold/mcp/dist/enterprise/tools/walk.js +73 -0
  102. package/scaffold/mcp/dist/enterprise/violations/push.js +77 -0
  103. package/scaffold/mcp/dist/enterprise/workflow/push.js +44 -0
  104. package/scaffold/mcp/dist/enterprise/workflow/state.js +329 -0
  105. package/scaffold/mcp/dist/frontmatter.js +33 -0
  106. package/scaffold/mcp/dist/graph.js +769 -0
  107. package/scaffold/mcp/dist/graphCsv.js +55 -0
  108. package/scaffold/mcp/dist/graphMetrics.js +8 -0
  109. package/scaffold/mcp/dist/hooks/permission-request.js +89 -0
  110. package/scaffold/mcp/dist/hooks/post-tool-use.js +105 -0
  111. package/scaffold/mcp/dist/hooks/pre-compact.js +29 -0
  112. package/scaffold/mcp/dist/hooks/pre-tool-use.js +78 -0
  113. package/scaffold/mcp/dist/hooks/session-end.js +43 -0
  114. package/scaffold/mcp/dist/hooks/session-start.js +41 -0
  115. package/scaffold/mcp/dist/hooks/shared.js +194 -0
  116. package/scaffold/mcp/dist/hooks/stop.js +28 -0
  117. package/scaffold/mcp/dist/hooks/user-prompt-submit.js +33 -0
  118. package/scaffold/mcp/dist/impactPresentation.js +137 -0
  119. package/scaffold/mcp/dist/impactRanking.js +191 -0
  120. package/scaffold/mcp/dist/impactResponse.js +30 -0
  121. package/scaffold/mcp/dist/impactResults.js +105 -0
  122. package/scaffold/mcp/dist/impactSeed.js +20 -0
  123. package/scaffold/mcp/dist/impactTraversal.js +64 -0
  124. package/scaffold/mcp/dist/jsonl.js +77 -0
  125. package/scaffold/mcp/dist/loadGraph.js +759 -0
  126. package/scaffold/mcp/dist/lruCache.js +38 -0
  127. package/scaffold/mcp/dist/paths.js +97 -0
  128. package/scaffold/mcp/dist/patternEvidence.js +272 -0
  129. package/scaffold/mcp/dist/plugin.js +81 -0
  130. package/scaffold/mcp/dist/presets.js +78 -0
  131. package/scaffold/mcp/dist/relatedResponse.js +18 -0
  132. package/scaffold/mcp/dist/relatedTraversal.js +78 -0
  133. package/scaffold/mcp/dist/rules.js +23 -0
  134. package/scaffold/mcp/dist/search.js +212 -0
  135. package/scaffold/mcp/dist/searchCore.js +457 -0
  136. package/scaffold/mcp/dist/searchResults.js +230 -0
  137. package/scaffold/mcp/dist/server.js +317 -0
  138. package/scaffold/mcp/dist/types.js +1 -0
  139. package/scaffold/mcp/package-lock.json +321 -200
  140. package/scaffold/mcp/package.json +15 -6
  141. package/scaffold/mcp/src/cli/enterprise-setup.ts +82 -8
  142. package/scaffold/mcp/src/cli/govern.ts +137 -52
  143. package/scaffold/mcp/src/cli/run.ts +53 -19
  144. package/scaffold/mcp/src/cli/stage.ts +8 -3
  145. package/scaffold/mcp/src/core/config.ts +9 -2
  146. package/scaffold/mcp/src/core/enterprise-host-identity.ts +259 -0
  147. package/scaffold/mcp/src/core/enterprise-identity.ts +20 -0
  148. package/scaffold/mcp/src/core/enterprise-rotation.ts +185 -0
  149. package/scaffold/mcp/src/core/govern-paths.ts +30 -0
  150. package/scaffold/mcp/src/core/license.ts +186 -17
  151. package/scaffold/mcp/src/core/secure-endpoint.ts +23 -0
  152. package/scaffold/mcp/src/core/workflow/enforcement.ts +8 -1
  153. package/scaffold/mcp/src/core/workflow/mcp-tools.ts +3 -0
  154. package/scaffold/mcp/src/core/workflow/resolution.ts +9 -1
  155. package/scaffold/mcp/src/core/workflow/synced-capability-registry.ts +15 -0
  156. package/scaffold/mcp/src/core/workflow/synced-registry.ts +15 -0
  157. package/scaffold/mcp/src/daemon/capability-sync-checker.ts +38 -2
  158. package/scaffold/mcp/src/daemon/egress-proxy.ts +14 -8
  159. package/scaffold/mcp/src/daemon/global-host-events.ts +288 -0
  160. package/scaffold/mcp/src/daemon/heartbeat-pusher.ts +4 -0
  161. package/scaffold/mcp/src/daemon/heartbeat-tracker.ts +2 -0
  162. package/scaffold/mcp/src/daemon/host-events-pusher.ts +5 -0
  163. package/scaffold/mcp/src/daemon/main.ts +99 -25
  164. package/scaffold/mcp/src/daemon/project-service-registry.ts +107 -0
  165. package/scaffold/mcp/src/daemon/skill-sync-checker.ts +368 -31
  166. package/scaffold/mcp/src/daemon/sync-checker.ts +4 -0
  167. package/scaffold/mcp/src/daemon/ungoverned-scanner.ts +57 -25
  168. package/scaffold/mcp/src/daemon/workflow-sync-checker.ts +41 -2
  169. package/scaffold/mcp/src/enterprise/audit/push.ts +8 -0
  170. package/scaffold/mcp/src/enterprise/policy/sync.ts +12 -0
  171. package/scaffold/mcp/src/enterprise/reviews/push.ts +9 -0
  172. package/scaffold/mcp/src/enterprise/reviews/trust-state.ts +3 -1
  173. package/scaffold/mcp/src/enterprise/telemetry/sync.ts +9 -0
  174. package/scaffold/mcp/src/enterprise/violations/push.ts +9 -0
  175. package/scaffold/mcp/src/enterprise/workflow/push.ts +7 -0
  176. package/scaffold/mcp/src/plugin.ts +20 -0
  177. package/scaffold/mcp/tests/copilot-shim.test.mjs +19 -1
  178. package/scaffold/mcp/tests/egress-proxy.test.mjs +37 -0
  179. package/scaffold/mcp/tests/enterprise-identity-sync.test.mjs +401 -0
  180. package/scaffold/mcp/tests/enterprise-setup.test.mjs +189 -0
  181. package/scaffold/mcp/tests/global-host-events.test.mjs +81 -0
  182. package/scaffold/mcp/tests/govern-install.test.mjs +95 -2
  183. package/scaffold/mcp/tests/govern-repair.test.mjs +20 -3
  184. package/scaffold/mcp/tests/heartbeat-tracker.test.mjs +26 -0
  185. package/scaffold/mcp/tests/license.test.mjs +367 -0
  186. package/scaffold/mcp/tests/project-service-registry.test.mjs +172 -0
  187. package/scaffold/mcp/tests/review-trust-contract.test.mjs +18 -0
  188. package/scaffold/mcp/tests/secure-enterprise-endpoint.test.mjs +46 -0
  189. package/scaffold/mcp/tests/skill-sync-checker.test.mjs +446 -2
  190. package/scaffold/mcp/tests/ungoverned-scanner.test.mjs +104 -22
  191. package/scaffold/mcp/tests/workflow-cli.test.mjs +14 -1
  192. package/scaffold/mcp/tests/workflow-synced-capabilities.test.mjs +37 -0
  193. package/scaffold/mcp/tests/workflow-synced-registry.test.mjs +40 -1
  194. package/scaffold/ownership/baseline-v2.4.1.json +22 -0
  195. package/scaffold/ownership/current.json +4 -0
  196. package/scaffold/ownership/v1.json +456 -0
  197. package/scaffold/scripts/ingest-parsers.mjs +1 -387
  198. package/scaffold/scripts/ingest-worker.mjs +4 -1
  199. package/scaffold/scripts/ingest.mjs +5 -3899
  200. package/scaffold/scripts/lib/ingest/arguments.mjs +78 -0
  201. package/scaffold/scripts/lib/ingest/chunks.mjs +212 -0
  202. package/scaffold/scripts/lib/ingest/config.mjs +120 -0
  203. package/scaffold/scripts/lib/ingest/constants.mjs +222 -0
  204. package/scaffold/scripts/lib/ingest/files.mjs +387 -0
  205. package/scaffold/scripts/lib/ingest/incremental-state.mjs +131 -0
  206. package/scaffold/scripts/lib/ingest/io.mjs +78 -0
  207. package/scaffold/scripts/lib/ingest/main.mjs +76 -0
  208. package/scaffold/scripts/lib/ingest/parser-composition.mjs +140 -0
  209. package/scaffold/scripts/lib/ingest/parser-registry.mjs +387 -0
  210. package/scaffold/scripts/lib/ingest/pipeline-stages.mjs +1625 -0
  211. package/scaffold/scripts/lib/ingest/projects.mjs +272 -0
  212. package/scaffold/scripts/lib/ingest/relations.mjs +860 -0
  213. package/scaffold/scripts/lib/ingest/runtime-paths.mjs +11 -0
  214. package/scaffold/scripts/lib/ingest/workers.mjs +264 -0
@@ -0,0 +1,409 @@
1
+ import { runContextRules } from "../rules.js";
2
+ import { runPatternEvidence } from "../patternEvidence.js";
3
+ import { runContextImpact, runContextRelated, runContextSearch } from "../search.js";
4
+ const QUERY_COMMANDS = new Set(["search", "related", "impact", "rules", "explain", "pattern-evidence"]);
5
+ const ENTITY_ID_PREFIXES = [
6
+ "file:",
7
+ "chunk:",
8
+ "rule:",
9
+ "adr:",
10
+ "module:",
11
+ "project:",
12
+ ];
13
+ export async function runQueryCommand(args) {
14
+ const command = args[0] ?? "help";
15
+ const rest = args.slice(1);
16
+ if (command === "help" || command === "--help" || command === "-h") {
17
+ printHelp();
18
+ return;
19
+ }
20
+ if (!QUERY_COMMANDS.has(command)) {
21
+ throw new Error(`Unknown query command: ${command}`);
22
+ }
23
+ const json = wantsJson(rest);
24
+ try {
25
+ switch (command) {
26
+ case "search":
27
+ return await runSearch(rest);
28
+ case "related":
29
+ return await runRelated(rest);
30
+ case "impact":
31
+ return await runImpact(rest);
32
+ case "rules":
33
+ return await runRules(rest);
34
+ case "explain":
35
+ return await runExplain(rest);
36
+ case "pattern-evidence":
37
+ return await runPatternEvidenceCommand(rest);
38
+ default:
39
+ throw new Error(`Unknown query command: ${command}`);
40
+ }
41
+ }
42
+ catch (error) {
43
+ if (!json) {
44
+ throw error;
45
+ }
46
+ emitJson({
47
+ ok: false,
48
+ command,
49
+ error: {
50
+ code: "INVALID_ARGS",
51
+ message: error instanceof Error ? error.message : String(error),
52
+ },
53
+ });
54
+ process.exitCode = 1;
55
+ }
56
+ }
57
+ function printHelp() {
58
+ const lines = [
59
+ "Usage:",
60
+ " cortex search <query> [--top-k <n>] [--preset <full|compact|minimal>] [--include-content] [--json]",
61
+ " cortex related <entity-id> [--depth <n>] [--edges] [--metadata] [--json]",
62
+ " cortex impact <query-or-entity-id> [--entity-id <id>] [--query <q>] [--depth <n>] [--top-k <n>] [--json]",
63
+ " cortex rules [--scope <scope>] [--include-inactive] [--json]",
64
+ " cortex explain <query-or-entity-id> [--top-k <n>] [--json]",
65
+ " cortex pattern-evidence <file-path|entity-id> [--query <text>] [--top-k <n>] [--json]",
66
+ "",
67
+ "These commands read the local Cortex graph and emit MCP-equivalent data with --json.",
68
+ ];
69
+ process.stdout.write(lines.join("\n") + "\n");
70
+ }
71
+ function parseArgs(args) {
72
+ const flags = {};
73
+ const rest = [];
74
+ let i = 0;
75
+ while (i < args.length) {
76
+ const arg = args[i];
77
+ if (arg === "--") {
78
+ rest.push(...args.slice(i + 1));
79
+ break;
80
+ }
81
+ if (arg.startsWith("--")) {
82
+ const name = arg.slice(2);
83
+ const next = args[i + 1];
84
+ if (next === undefined || next.startsWith("--")) {
85
+ flags[name] = true;
86
+ i += 1;
87
+ continue;
88
+ }
89
+ flags[name] = next;
90
+ i += 2;
91
+ continue;
92
+ }
93
+ rest.push(arg);
94
+ i += 1;
95
+ }
96
+ return { flags, rest };
97
+ }
98
+ function wantsJson(args) {
99
+ return args.includes("--json");
100
+ }
101
+ function isFlagEnabled(flags, name) {
102
+ return flags[name] === true;
103
+ }
104
+ function optionalString(flags, name) {
105
+ const value = flags[name];
106
+ return typeof value === "string" && value.length > 0 ? value : undefined;
107
+ }
108
+ function parsePositiveIntFlag(flags, name, defaultValue, max) {
109
+ const raw = flags[name];
110
+ if (raw === undefined) {
111
+ return defaultValue;
112
+ }
113
+ if (typeof raw !== "string" || raw.trim().length === 0) {
114
+ throw new Error(`--${name} requires a numeric value`);
115
+ }
116
+ const normalized = raw.trim();
117
+ if (!/^[1-9]\d*$/.test(normalized)) {
118
+ throw new Error(`--${name} must be an integer from 1 to ${max}`);
119
+ }
120
+ const parsed = Number(normalized);
121
+ if (!Number.isInteger(parsed) || parsed <= 0 || parsed > max) {
122
+ throw new Error(`--${name} must be an integer from 1 to ${max}`);
123
+ }
124
+ return parsed;
125
+ }
126
+ function parsePreset(flags) {
127
+ const value = optionalString(flags, "preset") ?? optionalString(flags, "response-preset");
128
+ if (value === undefined) {
129
+ return undefined;
130
+ }
131
+ if (value === "full" || value === "compact" || value === "minimal") {
132
+ return value;
133
+ }
134
+ throw new Error("--preset must be one of full, compact, minimal");
135
+ }
136
+ function parseCsvFlag(flags, name, allowed) {
137
+ const raw = optionalString(flags, name);
138
+ if (!raw) {
139
+ return undefined;
140
+ }
141
+ const allowedSet = new Set(allowed);
142
+ const parsed = raw.split(",").map((value) => value.trim()).filter(Boolean);
143
+ for (const value of parsed) {
144
+ if (!allowedSet.has(value)) {
145
+ throw new Error(`--${name} contains unsupported value: ${value}`);
146
+ }
147
+ }
148
+ return parsed;
149
+ }
150
+ function positionalText(rest, label) {
151
+ const value = rest.join(" ").trim();
152
+ if (!value) {
153
+ throw new Error(`${label} is required`);
154
+ }
155
+ return value;
156
+ }
157
+ function emitJson(value) {
158
+ process.stdout.write(JSON.stringify(value, null, 2) + "\n");
159
+ }
160
+ function emitEnvelope(command, input, data) {
161
+ emitJson({
162
+ ok: true,
163
+ command,
164
+ input,
165
+ context_source: data.context_source,
166
+ warning: data.warning,
167
+ data,
168
+ });
169
+ }
170
+ function printSummary(command, data) {
171
+ const source = data.context_source ? ` context_source=${String(data.context_source)}` : "";
172
+ const warning = data.warning ? ` warning=${String(data.warning)}` : "";
173
+ if (Array.isArray(data.results)) {
174
+ process.stdout.write(`${command}: ${data.results.length} results${source}${warning}\n`);
175
+ for (const result of data.results.slice(0, 10)) {
176
+ printEntityLine(result);
177
+ }
178
+ return;
179
+ }
180
+ if (Array.isArray(data.related)) {
181
+ process.stdout.write(`${command}: ${data.related.length} related${source}${warning}\n`);
182
+ for (const result of data.related.slice(0, 10)) {
183
+ printEntityLine(result);
184
+ }
185
+ return;
186
+ }
187
+ if (Array.isArray(data.rules)) {
188
+ process.stdout.write(`${command}: ${data.rules.length} rules${source}${warning}\n`);
189
+ for (const rule of data.rules.slice(0, 10)) {
190
+ if (rule && typeof rule === "object") {
191
+ const row = rule;
192
+ process.stdout.write(`- ${String(row.id ?? "")} priority=${String(row.priority ?? "")} scope=${String(row.scope ?? "")}\n`);
193
+ }
194
+ }
195
+ return;
196
+ }
197
+ process.stdout.write(`${command}: ok${source}${warning}\n`);
198
+ }
199
+ function printEntityLine(value) {
200
+ if (!value || typeof value !== "object") {
201
+ return;
202
+ }
203
+ const row = value;
204
+ const title = row.title ?? row.label ?? row.id ?? "";
205
+ const path = row.path ? ` ${String(row.path)}` : "";
206
+ process.stdout.write(`- ${String(title)}${path}\n`);
207
+ }
208
+ async function runSearch(args) {
209
+ const { flags, rest } = parseArgs(args);
210
+ const query = optionalString(flags, "query") ?? positionalText(rest, "query");
211
+ const input = {
212
+ query,
213
+ top_k: parsePositiveIntFlag(flags, "top-k", 5, 20),
214
+ include_deprecated: isFlagEnabled(flags, "include-deprecated"),
215
+ response_preset: parsePreset(flags),
216
+ include_scores: isFlagEnabled(flags, "scores") || isFlagEnabled(flags, "include-scores") || undefined,
217
+ include_matched_rules: isFlagEnabled(flags, "matched-rules") || isFlagEnabled(flags, "include-matched-rules") || undefined,
218
+ include_content: isFlagEnabled(flags, "include-content") || undefined,
219
+ };
220
+ const data = await runContextSearch(input);
221
+ if (isFlagEnabled(flags, "json")) {
222
+ emitEnvelope("search", input, data);
223
+ return;
224
+ }
225
+ printSummary("search", data);
226
+ }
227
+ async function runRelated(args) {
228
+ const { flags, rest } = parseArgs(args);
229
+ const entityId = optionalString(flags, "entity-id") ?? positionalText(rest, "entity-id");
230
+ const input = {
231
+ entity_id: entityId,
232
+ depth: parsePositiveIntFlag(flags, "depth", 1, 3),
233
+ include_edges: isFlagEnabled(flags, "edges") || isFlagEnabled(flags, "include-edges") || undefined,
234
+ response_preset: parsePreset(flags),
235
+ include_entity_metadata: isFlagEnabled(flags, "metadata") || isFlagEnabled(flags, "include-entity-metadata") || undefined,
236
+ };
237
+ const data = await runContextRelated(input);
238
+ if (isFlagEnabled(flags, "json")) {
239
+ emitEnvelope("related", input, data);
240
+ return;
241
+ }
242
+ printSummary("related", data);
243
+ }
244
+ async function runImpact(args) {
245
+ const { flags, rest } = parseArgs(args);
246
+ const explicitEntityId = optionalString(flags, "entity-id");
247
+ const explicitQuery = optionalString(flags, "query");
248
+ const positional = rest.length > 0 ? positionalText(rest, "query-or-entity-id") : undefined;
249
+ const seed = explicitEntityId ?? explicitQuery ?? positional;
250
+ if (!seed) {
251
+ throw new Error("Either --entity-id, --query, or a positional seed is required");
252
+ }
253
+ const relationTypes = parseCsvFlag(flags, "relation-types", [
254
+ "CALLS",
255
+ "CALLS_SQL",
256
+ "IMPORTS",
257
+ "USES_CONFIG_KEY",
258
+ "USES_RESOURCE_KEY",
259
+ "USES_SETTING_KEY",
260
+ "USES_CONFIG",
261
+ "TRANSFORMS_CONFIG",
262
+ "PART_OF",
263
+ ]);
264
+ const input = {
265
+ depth: parsePositiveIntFlag(flags, "depth", 2, 4),
266
+ top_k: parsePositiveIntFlag(flags, "top-k", 8, 20),
267
+ include_edges: !isFlagEnabled(flags, "no-edges"),
268
+ response_preset: parsePreset(flags),
269
+ include_scores: isFlagEnabled(flags, "scores") || isFlagEnabled(flags, "include-scores") || undefined,
270
+ include_reasons: isFlagEnabled(flags, "reasons") || isFlagEnabled(flags, "include-reasons") || undefined,
271
+ verbose_paths: isFlagEnabled(flags, "verbose-paths") || undefined,
272
+ max_path_hops_shown: parsePositiveIntFlag(flags, "max-path-hops-shown", 3, 8),
273
+ profile: parseProfile(flags),
274
+ sort_by: parseSortBy(flags),
275
+ relation_types: relationTypes,
276
+ path_must_include: parseCsvFlag(flags, "path-must-include", relationTypesAllowed()),
277
+ path_must_exclude: parseCsvFlag(flags, "path-must-exclude", relationTypesAllowed()),
278
+ result_domains: parseCsvFlag(flags, "result-domains", [
279
+ "code",
280
+ "config",
281
+ "resource",
282
+ "settings",
283
+ "sql",
284
+ "project",
285
+ ]),
286
+ result_entity_types: parseCsvFlag(flags, "result-entity-types", [
287
+ "File",
288
+ "Chunk",
289
+ "Module",
290
+ "Project",
291
+ "ADR",
292
+ "Rule",
293
+ ]),
294
+ };
295
+ if (explicitEntityId || (!explicitQuery && seedLooksLikeEntityId(seed))) {
296
+ input.entity_id = seed;
297
+ }
298
+ else {
299
+ input.query = seed;
300
+ }
301
+ const data = await runContextImpact(input);
302
+ if (isFlagEnabled(flags, "json")) {
303
+ emitEnvelope("impact", input, data);
304
+ return;
305
+ }
306
+ printSummary("impact", data);
307
+ }
308
+ function relationTypesAllowed() {
309
+ return [
310
+ "CALLS",
311
+ "CALLS_SQL",
312
+ "IMPORTS",
313
+ "USES_CONFIG_KEY",
314
+ "USES_RESOURCE_KEY",
315
+ "USES_SETTING_KEY",
316
+ "USES_CONFIG",
317
+ "TRANSFORMS_CONFIG",
318
+ "PART_OF",
319
+ ];
320
+ }
321
+ function seedLooksLikeEntityId(value) {
322
+ return ENTITY_ID_PREFIXES.some((prefix) => value.startsWith(prefix));
323
+ }
324
+ function parseProfile(flags) {
325
+ const value = optionalString(flags, "profile");
326
+ if (!value) {
327
+ return "all";
328
+ }
329
+ if (value === "all" || value === "config_only" || value === "config_to_sql" || value === "code_only" || value === "sql_only") {
330
+ return value;
331
+ }
332
+ throw new Error("--profile must be one of all, config_only, config_to_sql, code_only, sql_only");
333
+ }
334
+ function parseSortBy(flags) {
335
+ const value = optionalString(flags, "sort-by");
336
+ if (!value) {
337
+ return "impact_score";
338
+ }
339
+ if (value === "impact_score" || value === "shortest_path" || value === "semantic_score" || value === "graph_score" || value === "trust_score") {
340
+ return value;
341
+ }
342
+ throw new Error("--sort-by must be one of impact_score, shortest_path, semantic_score, graph_score, trust_score");
343
+ }
344
+ async function runRules(args) {
345
+ const { flags } = parseArgs(args);
346
+ const input = {
347
+ scope: optionalString(flags, "scope"),
348
+ include_inactive: isFlagEnabled(flags, "include-inactive"),
349
+ };
350
+ const data = await runContextRules(input);
351
+ if (isFlagEnabled(flags, "json")) {
352
+ emitEnvelope("rules", input, data);
353
+ return;
354
+ }
355
+ printSummary("rules", data);
356
+ }
357
+ async function runExplain(args) {
358
+ const { flags, rest } = parseArgs(args);
359
+ const target = optionalString(flags, "id") ?? optionalString(flags, "query") ?? positionalText(rest, "query-or-entity-id");
360
+ const input = {
361
+ query: target,
362
+ top_k: parsePositiveIntFlag(flags, "top-k", 3, 20),
363
+ include_deprecated: isFlagEnabled(flags, "include-deprecated"),
364
+ response_preset: parsePreset(flags) ?? "full",
365
+ include_scores: true,
366
+ include_matched_rules: true,
367
+ include_content: isFlagEnabled(flags, "include-content") || undefined,
368
+ };
369
+ const search = await runContextSearch(input);
370
+ const data = {
371
+ query: target,
372
+ entity_id: seedLooksLikeEntityId(target) ? target : undefined,
373
+ context_source: search.context_source,
374
+ warning: search.warning,
375
+ semantic_engine: search.semantic_engine,
376
+ ranking: search.ranking,
377
+ results: search.results,
378
+ explanation: "Scores and matched_rules come from the same local search ranking used by context.search.",
379
+ };
380
+ if (isFlagEnabled(flags, "json")) {
381
+ emitEnvelope("explain", input, data);
382
+ return;
383
+ }
384
+ printSummary("explain", data);
385
+ }
386
+ async function runPatternEvidenceCommand(args) {
387
+ const { flags, rest } = parseArgs(args);
388
+ const target = optionalString(flags, "target") ?? positionalText(rest, "file-path-or-entity-id");
389
+ const input = {
390
+ target,
391
+ query: optionalString(flags, "query"),
392
+ top_k: parsePositiveIntFlag(flags, "top-k", 3, 10),
393
+ include_deprecated: isFlagEnabled(flags, "include-deprecated"),
394
+ };
395
+ const data = await runPatternEvidence(input);
396
+ if (isFlagEnabled(flags, "json")) {
397
+ emitEnvelope("pattern-evidence", input, data);
398
+ return;
399
+ }
400
+ const tiers = Array.isArray(data.tiers) ? data.tiers : [];
401
+ process.stdout.write(`pattern-evidence: local_pattern_found=${String(data.local_pattern_found)}\n`);
402
+ for (const value of tiers) {
403
+ if (!value || typeof value !== "object")
404
+ continue;
405
+ const tier = value;
406
+ const evidence = Array.isArray(tier.evidence) ? tier.evidence : [];
407
+ process.stdout.write(`- ${String(tier.name ?? "")}: ${evidence.length}\n`);
408
+ }
409
+ }
@@ -0,0 +1,295 @@
1
+ import { spawn } from "node:child_process";
2
+ import { existsSync, closeSync, fchmodSync, lstatSync, openSync, readFileSync, renameSync, writeFileSync, unlinkSync, mkdirSync, } from "node:fs";
3
+ import { join, dirname } from "node:path";
4
+ import { platform, tmpdir } from "node:os";
5
+ import { randomUUID } from "node:crypto";
6
+ import { getGovernManagedPath } from "../core/govern-paths.js";
7
+ export const RUN_CLIS = ["claude", "codex", "copilot"];
8
+ // Inline ANSI helper. Kept here (rather than imported from bin/style.mjs) so
9
+ // the scaffold MCP stays self-contained — projects bundle scaffold/ on init
10
+ // but do not pull bin/ into their tree. Style stays brand-aligned: cyan accent
11
+ // (xterm-256 code 81), graceful degrade when the stream is not a TTY or when
12
+ // NO_COLOR / CORTEX_NO_COLOR are set.
13
+ function runHeaderSupportsColor() {
14
+ if (process.env.NO_COLOR)
15
+ return false;
16
+ if (process.env.CORTEX_NO_COLOR)
17
+ return false;
18
+ if (process.env.TERM === "dumb")
19
+ return false;
20
+ return Boolean(process.stderr.isTTY);
21
+ }
22
+ function runHeaderUnicodeOk() {
23
+ if (process.env.CORTEX_NO_UNICODE)
24
+ return false;
25
+ const locale = process.env.LC_ALL || process.env.LC_CTYPE || process.env.LANG || "";
26
+ if (!locale)
27
+ return process.platform !== "win32";
28
+ return /UTF-?8/i.test(locale);
29
+ }
30
+ export function buildRunHeader(label) {
31
+ const glyph = runHeaderUnicodeOk() ? "▸" : ">";
32
+ if (!runHeaderSupportsColor()) {
33
+ return `${glyph} ${label}`;
34
+ }
35
+ return `\x1b[38;5;81m${glyph}\x1b[0m ${label}`;
36
+ }
37
+ export const SHIM_MARKER = "# cortex-shim-v1";
38
+ export function isCortexShim(filePath) {
39
+ try {
40
+ return readFileSync(filePath, "utf8").includes(SHIM_MARKER);
41
+ }
42
+ catch {
43
+ return false;
44
+ }
45
+ }
46
+ export function findRealBinary(name, excludePaths = []) {
47
+ const exclusions = new Set(excludePaths);
48
+ const pathDirs = (process.env.PATH ?? "").split(":");
49
+ for (const dir of pathDirs) {
50
+ if (!dir)
51
+ continue;
52
+ const candidate = join(dir, name);
53
+ if (exclusions.has(candidate))
54
+ continue;
55
+ if (!existsSync(candidate))
56
+ continue;
57
+ if (isCortexShim(candidate))
58
+ continue;
59
+ return candidate;
60
+ }
61
+ return null;
62
+ }
63
+ export function buildDarwinSandboxProfile(homeDir) {
64
+ return [
65
+ "(version 1)",
66
+ "(allow default)",
67
+ "; Cortex Tier 2 (wrap) profile for Copilot CLI.",
68
+ "; Deny writes to Copilot's mutable config locations so AI cannot",
69
+ "; reconfigure itself out of governance.",
70
+ `(deny file-write* (subpath "${homeDir}/.copilot"))`,
71
+ `(deny file-write* (subpath "${homeDir}/.copilot.local"))`,
72
+ '(deny file-write* (regex #"^/etc/copilot"))',
73
+ "",
74
+ ].join("\n");
75
+ }
76
+ export function buildLinuxBwrapArgs(homeDir, realBinary, args) {
77
+ return [
78
+ "--die-with-parent",
79
+ "--unshare-user",
80
+ "--ro-bind",
81
+ "/",
82
+ "/",
83
+ "--proc",
84
+ "/proc",
85
+ "--dev",
86
+ "/dev",
87
+ "--bind",
88
+ homeDir,
89
+ homeDir,
90
+ "--tmpfs",
91
+ `${homeDir}/.copilot`,
92
+ "--tmpfs",
93
+ `${homeDir}/.copilot.local`,
94
+ "--ro-bind",
95
+ "/etc",
96
+ "/etc",
97
+ "--",
98
+ realBinary,
99
+ ...args,
100
+ ];
101
+ }
102
+ function spawnAndWait(cmd, args, extraEnv) {
103
+ return new Promise((resolve) => {
104
+ const env = extraEnv ? { ...process.env, ...extraEnv } : process.env;
105
+ const child = spawn(cmd, args, { stdio: "inherit", env });
106
+ child.on("exit", (code, signal) => {
107
+ if (signal)
108
+ resolve(128);
109
+ else
110
+ resolve(code ?? 1);
111
+ });
112
+ child.on("error", (err) => {
113
+ console.error(`✗ Failed to spawn ${cmd}: ${err.message}`);
114
+ resolve(127);
115
+ });
116
+ });
117
+ }
118
+ export async function runAiCli(options) {
119
+ const { cli, args } = options;
120
+ const real = options.realBinary ?? findRealBinary(cli, options.excludePaths ?? []);
121
+ if (!real) {
122
+ const exclusions = options.excludePaths?.length
123
+ ? ` (excluding ${options.excludePaths.join(", ")})`
124
+ : "";
125
+ console.error(`✗ Could not find '${cli}' binary in PATH${exclusions}.`);
126
+ return 127;
127
+ }
128
+ if (cli !== "copilot") {
129
+ return spawnAndWait(real, args);
130
+ }
131
+ const home = process.env.HOME ?? "";
132
+ if (!home) {
133
+ console.error("✗ HOME not set — required for Copilot wrap profile.");
134
+ return 1;
135
+ }
136
+ const proxyPort = process.env.CORTEX_EGRESS_PROXY_PORT ?? "18888";
137
+ const proxyEnv = {
138
+ HTTPS_PROXY: `http://127.0.0.1:${proxyPort}`,
139
+ HTTP_PROXY: `http://127.0.0.1:${proxyPort}`,
140
+ https_proxy: `http://127.0.0.1:${proxyPort}`,
141
+ http_proxy: `http://127.0.0.1:${proxyPort}`,
142
+ };
143
+ const os = platform();
144
+ if (os === "darwin") {
145
+ const profile = buildDarwinSandboxProfile(home);
146
+ const tmpProfile = join(tmpdir(), `cortex-copilot-${randomUUID()}.sb`);
147
+ writeFileSync(tmpProfile, profile);
148
+ try {
149
+ process.stderr.write(buildRunHeader("cortex run copilot — Tier 2 wrap (sandbox-exec)") + "\n");
150
+ return await spawnAndWait("sandbox-exec", ["-f", tmpProfile, real, ...args], proxyEnv);
151
+ }
152
+ finally {
153
+ try {
154
+ unlinkSync(tmpProfile);
155
+ }
156
+ catch {
157
+ // best-effort cleanup
158
+ }
159
+ }
160
+ }
161
+ if (os === "linux") {
162
+ process.stderr.write(buildRunHeader("cortex run copilot — Tier 2 wrap (bwrap)") + "\n");
163
+ return spawnAndWait("bwrap", buildLinuxBwrapArgs(home, real, args), proxyEnv);
164
+ }
165
+ console.error(`✗ Tier 2 (wrap) for copilot not yet supported on ${os}.`);
166
+ return 1;
167
+ }
168
+ export function getDefaultCopilotShimPath(os) {
169
+ return getGovernManagedPath("copilot", os);
170
+ }
171
+ export function buildCopilotShim(realBinary) {
172
+ return [
173
+ "#!/bin/sh",
174
+ SHIM_MARKER,
175
+ "# Cortex Tier 2 wrap shim — re-execs through 'cortex run copilot'.",
176
+ "# Direct invocations of copilot are routed through cortex's OS sandbox.",
177
+ `# Real binary captured at install time: ${realBinary}`,
178
+ "",
179
+ 'CORTEX="${CORTEX_BIN:-cortex}"',
180
+ 'exec "$CORTEX" run copilot "$@"',
181
+ "",
182
+ ].join("\n");
183
+ }
184
+ export function installCopilotShim(options = {}) {
185
+ const shimPath = options.shimPath ?? getDefaultCopilotShimPath(platform());
186
+ const search = options.searchPath ?? process.env.PATH ?? "";
187
+ const real = options.realBinary ??
188
+ findRealBinaryIn(search, "copilot", [shimPath]);
189
+ if (!real) {
190
+ return {
191
+ ok: false,
192
+ message: `Copilot CLI not found in PATH (excluding ${shimPath}). ` +
193
+ "Install GitHub Copilot CLI first, then re-run cortex enterprise sync.",
194
+ };
195
+ }
196
+ try {
197
+ const existing = lstatSync(shimPath);
198
+ if (existing.isSymbolicLink() ||
199
+ !existing.isFile() ||
200
+ !isCortexShim(shimPath)) {
201
+ return {
202
+ ok: false,
203
+ message: `${shimPath} exists and is not a regular cortex shim — refusing to overwrite. ` +
204
+ "Move/rename the existing path, then re-run.",
205
+ };
206
+ }
207
+ }
208
+ catch (err) {
209
+ if (err.code !== "ENOENT") {
210
+ return {
211
+ ok: false,
212
+ message: `Failed to inspect shim path ${shimPath}: ${err instanceof Error ? err.message : String(err)}`,
213
+ };
214
+ }
215
+ }
216
+ const temporaryPath = `${shimPath}.tmp.${process.pid}.${randomUUID()}`;
217
+ let fd = null;
218
+ try {
219
+ mkdirSync(dirname(shimPath), { recursive: true });
220
+ fd = openSync(temporaryPath, "wx", 0o700);
221
+ writeFileSync(fd, buildCopilotShim(real));
222
+ fchmodSync(fd, 0o755);
223
+ closeSync(fd);
224
+ fd = null;
225
+ renameSync(temporaryPath, shimPath);
226
+ const installed = lstatSync(shimPath);
227
+ if (!installed.isFile() || installed.isSymbolicLink()) {
228
+ throw new Error("installed shim is not a regular file");
229
+ }
230
+ }
231
+ catch (err) {
232
+ if (fd !== null) {
233
+ try {
234
+ closeSync(fd);
235
+ }
236
+ catch {
237
+ // Best effort.
238
+ }
239
+ }
240
+ try {
241
+ unlinkSync(temporaryPath);
242
+ }
243
+ catch {
244
+ // Best effort.
245
+ }
246
+ return {
247
+ ok: false,
248
+ message: `Failed to write shim at ${shimPath}: ${err instanceof Error ? err.message : String(err)}`,
249
+ };
250
+ }
251
+ return {
252
+ ok: true,
253
+ message: `Installed copilot shim at ${shimPath} (real binary: ${real})`,
254
+ shimPath,
255
+ realBinary: real,
256
+ };
257
+ }
258
+ function findRealBinaryIn(searchPath, name, excludePaths) {
259
+ const exclusions = new Set(excludePaths);
260
+ for (const dir of searchPath.split(":")) {
261
+ if (!dir)
262
+ continue;
263
+ const candidate = join(dir, name);
264
+ if (exclusions.has(candidate))
265
+ continue;
266
+ if (!existsSync(candidate))
267
+ continue;
268
+ if (isCortexShim(candidate))
269
+ continue;
270
+ return candidate;
271
+ }
272
+ return null;
273
+ }
274
+ export function uninstallCopilotShim(shimPath) {
275
+ const target = shimPath ?? getDefaultCopilotShimPath(platform());
276
+ if (!existsSync(target)) {
277
+ return { ok: true, message: `${target} already absent.` };
278
+ }
279
+ if (!isCortexShim(target)) {
280
+ return {
281
+ ok: false,
282
+ message: `${target} is no longer a cortex shim — refusing to delete (would clobber a real binary).`,
283
+ };
284
+ }
285
+ try {
286
+ unlinkSync(target);
287
+ }
288
+ catch (err) {
289
+ return {
290
+ ok: false,
291
+ message: `Failed to remove ${target}: ${err instanceof Error ? err.message : String(err)}`,
292
+ };
293
+ }
294
+ return { ok: true, message: `Removed copilot shim at ${target}` };
295
+ }