@danielblomma/cortex-mcp 2.1.4 → 2.2.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.
Files changed (121) hide show
  1. package/README.md +53 -24
  2. package/bin/cortex.mjs +48 -21
  3. package/mcp-registry-submission.json +63 -0
  4. package/package.json +18 -5
  5. package/scaffold/AGENTS.md +13 -9
  6. package/scaffold/CLAUDE.md +13 -9
  7. package/scaffold/docs/architecture.md +1 -1
  8. package/scaffold/mcp/src/cli/query.ts +455 -0
  9. package/scaffold/mcp/tests/query-cli.test.mjs +115 -0
  10. package/scaffold/scripts/bootstrap.sh +3 -2
  11. package/scaffold/scripts/doctor.sh +9 -8
  12. package/scaffold/scripts/embed.sh +3 -2
  13. package/scaffold/scripts/load-ryu.sh +3 -2
  14. package/scaffold/scripts/memory-compile.mjs +5 -2
  15. package/scaffold/scripts/memory-lint.mjs +5 -2
  16. package/scaffold/scripts/parsers/node_modules/.package-lock.json +0 -56
  17. package/scaffold/scripts/parsers/node_modules/acorn/CHANGELOG.md +0 -972
  18. package/scaffold/scripts/parsers/node_modules/acorn/LICENSE +0 -21
  19. package/scaffold/scripts/parsers/node_modules/acorn/README.md +0 -301
  20. package/scaffold/scripts/parsers/node_modules/acorn/bin/acorn +0 -4
  21. package/scaffold/scripts/parsers/node_modules/acorn/dist/acorn.d.mts +0 -883
  22. package/scaffold/scripts/parsers/node_modules/acorn/dist/acorn.d.ts +0 -883
  23. package/scaffold/scripts/parsers/node_modules/acorn/dist/acorn.js +0 -6295
  24. package/scaffold/scripts/parsers/node_modules/acorn/dist/acorn.mjs +0 -6266
  25. package/scaffold/scripts/parsers/node_modules/acorn/dist/bin.js +0 -90
  26. package/scaffold/scripts/parsers/node_modules/acorn/package.json +0 -50
  27. package/scaffold/scripts/parsers/node_modules/acorn-typescript/CHANGELOG.md +0 -421
  28. package/scaffold/scripts/parsers/node_modules/acorn-typescript/LICENSE +0 -21
  29. package/scaffold/scripts/parsers/node_modules/acorn-typescript/README.md +0 -81
  30. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/error.d.ts +0 -103
  31. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/error.js +0 -78
  32. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/error.js.map +0 -1
  33. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/extentions/decorators.d.ts +0 -167
  34. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/extentions/decorators.js +0 -75
  35. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/extentions/decorators.js.map +0 -1
  36. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/extentions/import-assertions.d.ts +0 -177
  37. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/extentions/import-assertions.js +0 -56
  38. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/extentions/import-assertions.js.map +0 -1
  39. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/extentions/jsx/index.d.ts +0 -198
  40. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/extentions/jsx/index.js +0 -327
  41. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/extentions/jsx/index.js.map +0 -1
  42. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/extentions/jsx/xhtml.d.ts +0 -256
  43. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/extentions/jsx/xhtml.js +0 -256
  44. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/extentions/jsx/xhtml.js.map +0 -1
  45. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/index.d.ts +0 -472
  46. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/index.js +0 -1
  47. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/index.js.map +0 -1
  48. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/index.mjs +0 -1
  49. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/middleware.d.ts +0 -159
  50. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/middleware.js +0 -2
  51. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/middleware.js.map +0 -1
  52. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/parseutil.d.ts +0 -10
  53. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/parseutil.js +0 -38
  54. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/parseutil.js.map +0 -1
  55. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/scopeflags.d.ts +0 -12
  56. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/scopeflags.js +0 -29
  57. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/scopeflags.js.map +0 -1
  58. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/tokenType.d.ts +0 -2
  59. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/tokenType.js +0 -118
  60. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/tokenType.js.map +0 -1
  61. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/types.d.ts +0 -60
  62. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/types.js +0 -2
  63. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/types.js.map +0 -1
  64. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/whitespace.d.ts +0 -2
  65. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/whitespace.js +0 -19
  66. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/whitespace.js.map +0 -1
  67. package/scaffold/scripts/parsers/node_modules/acorn-typescript/package.json +0 -53
  68. package/scaffold/scripts/parsers/node_modules/acorn-typescript/tsconfig.json +0 -19
  69. package/scaffold/scripts/parsers/node_modules/acorn-walk/CHANGELOG.md +0 -209
  70. package/scaffold/scripts/parsers/node_modules/acorn-walk/LICENSE +0 -21
  71. package/scaffold/scripts/parsers/node_modules/acorn-walk/README.md +0 -124
  72. package/scaffold/scripts/parsers/node_modules/acorn-walk/dist/walk.d.mts +0 -152
  73. package/scaffold/scripts/parsers/node_modules/acorn-walk/dist/walk.d.ts +0 -152
  74. package/scaffold/scripts/parsers/node_modules/acorn-walk/dist/walk.js +0 -485
  75. package/scaffold/scripts/parsers/node_modules/acorn-walk/dist/walk.mjs +0 -467
  76. package/scaffold/scripts/parsers/node_modules/acorn-walk/package.json +0 -50
  77. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/LICENSE +0 -24
  78. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/README.md +0 -23
  79. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-bash.wasm +0 -0
  80. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-c.wasm +0 -0
  81. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-c_sharp.wasm +0 -0
  82. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-cpp.wasm +0 -0
  83. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-css.wasm +0 -0
  84. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-dart.wasm +0 -0
  85. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-elisp.wasm +0 -0
  86. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-elixir.wasm +0 -0
  87. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-elm.wasm +0 -0
  88. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-embedded_template.wasm +0 -0
  89. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-go.wasm +0 -0
  90. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-html.wasm +0 -0
  91. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-java.wasm +0 -0
  92. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-javascript.wasm +0 -0
  93. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-json.wasm +0 -0
  94. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-kotlin.wasm +0 -0
  95. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-lua.wasm +0 -0
  96. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-objc.wasm +0 -0
  97. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-ocaml.wasm +0 -0
  98. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-php.wasm +0 -0
  99. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-python.wasm +0 -0
  100. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-ql.wasm +0 -0
  101. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-rescript.wasm +0 -0
  102. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-ruby.wasm +0 -0
  103. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-rust.wasm +0 -0
  104. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-scala.wasm +0 -0
  105. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-solidity.wasm +0 -0
  106. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-swift.wasm +0 -0
  107. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-systemrdl.wasm +0 -0
  108. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-tlaplus.wasm +0 -0
  109. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-toml.wasm +0 -0
  110. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-tsx.wasm +0 -0
  111. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-typescript.wasm +0 -0
  112. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-vue.wasm +0 -0
  113. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-yaml.wasm +0 -0
  114. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-zig.wasm +0 -0
  115. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/package.json +0 -64
  116. package/scaffold/scripts/parsers/node_modules/web-tree-sitter/LICENSE +0 -21
  117. package/scaffold/scripts/parsers/node_modules/web-tree-sitter/README.md +0 -198
  118. package/scaffold/scripts/parsers/node_modules/web-tree-sitter/package.json +0 -37
  119. package/scaffold/scripts/parsers/node_modules/web-tree-sitter/tree-sitter-web.d.ts +0 -242
  120. package/scaffold/scripts/parsers/node_modules/web-tree-sitter/tree-sitter.js +0 -1
  121. package/scaffold/scripts/parsers/node_modules/web-tree-sitter/tree-sitter.wasm +0 -0
@@ -0,0 +1,455 @@
1
+ import { runContextRules } from "../rules.js";
2
+ import { runContextImpact, runContextRelated, runContextSearch } from "../search.js";
3
+ import type {
4
+ ImpactParams,
5
+ RelatedParams,
6
+ RelationType,
7
+ RulesParams,
8
+ SearchParams,
9
+ ToolPayload,
10
+ } from "../types.js";
11
+
12
+ type Flags = Record<string, string | boolean>;
13
+
14
+ type ParsedArgs = {
15
+ flags: Flags;
16
+ rest: string[];
17
+ };
18
+
19
+ type JsonEnvelope = {
20
+ ok: boolean;
21
+ command: string;
22
+ input?: Record<string, unknown>;
23
+ context_source?: unknown;
24
+ warning?: unknown;
25
+ data?: ToolPayload;
26
+ error?: {
27
+ code: string;
28
+ message: string;
29
+ };
30
+ };
31
+
32
+ const QUERY_COMMANDS = new Set(["search", "related", "impact", "rules", "explain"]);
33
+
34
+ const ENTITY_ID_PREFIXES = [
35
+ "file:",
36
+ "chunk:",
37
+ "rule:",
38
+ "adr:",
39
+ "module:",
40
+ "project:",
41
+ ];
42
+
43
+ export async function runQueryCommand(args: string[]): Promise<void> {
44
+ const command = args[0] ?? "help";
45
+ const rest = args.slice(1);
46
+
47
+ if (command === "help" || command === "--help" || command === "-h") {
48
+ printHelp();
49
+ return;
50
+ }
51
+
52
+ if (!QUERY_COMMANDS.has(command)) {
53
+ throw new Error(`Unknown query command: ${command}`);
54
+ }
55
+
56
+ const json = wantsJson(rest);
57
+ try {
58
+ switch (command) {
59
+ case "search":
60
+ return await runSearch(rest);
61
+ case "related":
62
+ return await runRelated(rest);
63
+ case "impact":
64
+ return await runImpact(rest);
65
+ case "rules":
66
+ return await runRules(rest);
67
+ case "explain":
68
+ return await runExplain(rest);
69
+ default:
70
+ throw new Error(`Unknown query command: ${command}`);
71
+ }
72
+ } catch (error) {
73
+ if (!json) {
74
+ throw error;
75
+ }
76
+ emitJson({
77
+ ok: false,
78
+ command,
79
+ error: {
80
+ code: "INVALID_ARGS",
81
+ message: error instanceof Error ? error.message : String(error),
82
+ },
83
+ });
84
+ process.exitCode = 1;
85
+ }
86
+ }
87
+
88
+ function printHelp(): void {
89
+ const lines = [
90
+ "Usage:",
91
+ " cortex search <query> [--top-k <n>] [--preset <full|compact|minimal>] [--include-content] [--json]",
92
+ " cortex related <entity-id> [--depth <n>] [--edges] [--metadata] [--json]",
93
+ " cortex impact <query-or-entity-id> [--entity-id <id>] [--query <q>] [--depth <n>] [--top-k <n>] [--json]",
94
+ " cortex rules [--scope <scope>] [--include-inactive] [--json]",
95
+ " cortex explain <query-or-entity-id> [--top-k <n>] [--json]",
96
+ "",
97
+ "These commands read the local Cortex graph and emit MCP-equivalent data with --json.",
98
+ ];
99
+ process.stdout.write(lines.join("\n") + "\n");
100
+ }
101
+
102
+ function parseArgs(args: string[]): ParsedArgs {
103
+ const flags: Flags = {};
104
+ const rest: string[] = [];
105
+ let i = 0;
106
+
107
+ while (i < args.length) {
108
+ const arg = args[i];
109
+ if (arg === "--") {
110
+ rest.push(...args.slice(i + 1));
111
+ break;
112
+ }
113
+
114
+ if (arg.startsWith("--")) {
115
+ const name = arg.slice(2);
116
+ const next = args[i + 1];
117
+ if (next === undefined || next.startsWith("--")) {
118
+ flags[name] = true;
119
+ i += 1;
120
+ continue;
121
+ }
122
+ flags[name] = next;
123
+ i += 2;
124
+ continue;
125
+ }
126
+
127
+ rest.push(arg);
128
+ i += 1;
129
+ }
130
+
131
+ return { flags, rest };
132
+ }
133
+
134
+ function wantsJson(args: string[]): boolean {
135
+ return args.includes("--json");
136
+ }
137
+
138
+ function isFlagEnabled(flags: Flags, name: string): boolean {
139
+ return flags[name] === true;
140
+ }
141
+
142
+ function optionalString(flags: Flags, name: string): string | undefined {
143
+ const value = flags[name];
144
+ return typeof value === "string" && value.length > 0 ? value : undefined;
145
+ }
146
+
147
+ function parsePositiveIntFlag(
148
+ flags: Flags,
149
+ name: string,
150
+ defaultValue: number,
151
+ max: number,
152
+ ): number {
153
+ const raw = flags[name];
154
+ if (raw === undefined) {
155
+ return defaultValue;
156
+ }
157
+ if (typeof raw !== "string" || raw.trim().length === 0) {
158
+ throw new Error(`--${name} requires a numeric value`);
159
+ }
160
+ const parsed = Number.parseInt(raw, 10);
161
+ if (!Number.isInteger(parsed) || parsed <= 0 || parsed > max) {
162
+ throw new Error(`--${name} must be an integer from 1 to ${max}`);
163
+ }
164
+ return parsed;
165
+ }
166
+
167
+ function parsePreset(flags: Flags): "full" | "compact" | "minimal" | undefined {
168
+ const value = optionalString(flags, "preset") ?? optionalString(flags, "response-preset");
169
+ if (value === undefined) {
170
+ return undefined;
171
+ }
172
+ if (value === "full" || value === "compact" || value === "minimal") {
173
+ return value;
174
+ }
175
+ throw new Error("--preset must be one of full, compact, minimal");
176
+ }
177
+
178
+ function parseCsvFlag<T extends string>(
179
+ flags: Flags,
180
+ name: string,
181
+ allowed: readonly T[],
182
+ ): T[] | undefined {
183
+ const raw = optionalString(flags, name);
184
+ if (!raw) {
185
+ return undefined;
186
+ }
187
+ const allowedSet = new Set<string>(allowed);
188
+ const parsed = raw.split(",").map((value) => value.trim()).filter(Boolean);
189
+ for (const value of parsed) {
190
+ if (!allowedSet.has(value)) {
191
+ throw new Error(`--${name} contains unsupported value: ${value}`);
192
+ }
193
+ }
194
+ return parsed as T[];
195
+ }
196
+
197
+ function positionalText(rest: string[], label: string): string {
198
+ const value = rest.join(" ").trim();
199
+ if (!value) {
200
+ throw new Error(`${label} is required`);
201
+ }
202
+ return value;
203
+ }
204
+
205
+ function emitJson(value: JsonEnvelope): void {
206
+ process.stdout.write(JSON.stringify(value, null, 2) + "\n");
207
+ }
208
+
209
+ function emitEnvelope(command: string, input: Record<string, unknown>, data: ToolPayload): void {
210
+ emitJson({
211
+ ok: true,
212
+ command,
213
+ input,
214
+ context_source: data.context_source,
215
+ warning: data.warning,
216
+ data,
217
+ });
218
+ }
219
+
220
+ function printSummary(command: string, data: ToolPayload): void {
221
+ const source = data.context_source ? ` context_source=${String(data.context_source)}` : "";
222
+ const warning = data.warning ? ` warning=${String(data.warning)}` : "";
223
+
224
+ if (Array.isArray(data.results)) {
225
+ process.stdout.write(`${command}: ${data.results.length} results${source}${warning}\n`);
226
+ for (const result of data.results.slice(0, 10)) {
227
+ printEntityLine(result);
228
+ }
229
+ return;
230
+ }
231
+
232
+ if (Array.isArray(data.related)) {
233
+ process.stdout.write(`${command}: ${data.related.length} related${source}${warning}\n`);
234
+ for (const result of data.related.slice(0, 10)) {
235
+ printEntityLine(result);
236
+ }
237
+ return;
238
+ }
239
+
240
+ if (Array.isArray(data.rules)) {
241
+ process.stdout.write(`${command}: ${data.rules.length} rules${source}${warning}\n`);
242
+ for (const rule of data.rules.slice(0, 10)) {
243
+ if (rule && typeof rule === "object") {
244
+ const row = rule as Record<string, unknown>;
245
+ process.stdout.write(`- ${String(row.id ?? "")} priority=${String(row.priority ?? "")} scope=${String(row.scope ?? "")}\n`);
246
+ }
247
+ }
248
+ return;
249
+ }
250
+
251
+ process.stdout.write(`${command}: ok${source}${warning}\n`);
252
+ }
253
+
254
+ function printEntityLine(value: unknown): void {
255
+ if (!value || typeof value !== "object") {
256
+ return;
257
+ }
258
+ const row = value as Record<string, unknown>;
259
+ const title = row.title ?? row.label ?? row.id ?? "";
260
+ const path = row.path ? ` ${String(row.path)}` : "";
261
+ process.stdout.write(`- ${String(title)}${path}\n`);
262
+ }
263
+
264
+ async function runSearch(args: string[]): Promise<void> {
265
+ const { flags, rest } = parseArgs(args);
266
+ const query = optionalString(flags, "query") ?? positionalText(rest, "query");
267
+ const input: SearchParams = {
268
+ query,
269
+ top_k: parsePositiveIntFlag(flags, "top-k", 5, 20),
270
+ include_deprecated: isFlagEnabled(flags, "include-deprecated"),
271
+ response_preset: parsePreset(flags),
272
+ include_scores: isFlagEnabled(flags, "scores") || isFlagEnabled(flags, "include-scores") || undefined,
273
+ include_matched_rules:
274
+ isFlagEnabled(flags, "matched-rules") || isFlagEnabled(flags, "include-matched-rules") || undefined,
275
+ include_content: isFlagEnabled(flags, "include-content") || undefined,
276
+ };
277
+ const data = await runContextSearch(input);
278
+ if (isFlagEnabled(flags, "json")) {
279
+ emitEnvelope("search", input, data);
280
+ return;
281
+ }
282
+ printSummary("search", data);
283
+ }
284
+
285
+ async function runRelated(args: string[]): Promise<void> {
286
+ const { flags, rest } = parseArgs(args);
287
+ const entityId = optionalString(flags, "entity-id") ?? positionalText(rest, "entity-id");
288
+ const input: RelatedParams = {
289
+ entity_id: entityId,
290
+ depth: parsePositiveIntFlag(flags, "depth", 1, 3),
291
+ include_edges: isFlagEnabled(flags, "edges") || isFlagEnabled(flags, "include-edges") || undefined,
292
+ response_preset: parsePreset(flags),
293
+ include_entity_metadata:
294
+ isFlagEnabled(flags, "metadata") || isFlagEnabled(flags, "include-entity-metadata") || undefined,
295
+ };
296
+ const data = await runContextRelated(input);
297
+ if (isFlagEnabled(flags, "json")) {
298
+ emitEnvelope("related", input, data);
299
+ return;
300
+ }
301
+ printSummary("related", data);
302
+ }
303
+
304
+ async function runImpact(args: string[]): Promise<void> {
305
+ const { flags, rest } = parseArgs(args);
306
+ const explicitEntityId = optionalString(flags, "entity-id");
307
+ const explicitQuery = optionalString(flags, "query");
308
+ const positional = rest.length > 0 ? positionalText(rest, "query-or-entity-id") : undefined;
309
+ const seed = explicitEntityId ?? explicitQuery ?? positional;
310
+
311
+ if (!seed) {
312
+ throw new Error("Either --entity-id, --query, or a positional seed is required");
313
+ }
314
+
315
+ const relationTypes = parseCsvFlag(flags, "relation-types", [
316
+ "CALLS",
317
+ "CALLS_SQL",
318
+ "IMPORTS",
319
+ "USES_CONFIG_KEY",
320
+ "USES_RESOURCE_KEY",
321
+ "USES_SETTING_KEY",
322
+ "USES_CONFIG",
323
+ "TRANSFORMS_CONFIG",
324
+ "PART_OF",
325
+ ] as const);
326
+ const input: ImpactParams = {
327
+ depth: parsePositiveIntFlag(flags, "depth", 2, 4),
328
+ top_k: parsePositiveIntFlag(flags, "top-k", 8, 20),
329
+ include_edges: !isFlagEnabled(flags, "no-edges"),
330
+ response_preset: parsePreset(flags),
331
+ include_scores: isFlagEnabled(flags, "scores") || isFlagEnabled(flags, "include-scores") || undefined,
332
+ include_reasons: isFlagEnabled(flags, "reasons") || isFlagEnabled(flags, "include-reasons") || undefined,
333
+ verbose_paths: isFlagEnabled(flags, "verbose-paths") || undefined,
334
+ max_path_hops_shown: parsePositiveIntFlag(flags, "max-path-hops-shown", 3, 8),
335
+ profile: parseProfile(flags),
336
+ sort_by: parseSortBy(flags),
337
+ relation_types: relationTypes as RelationType[] | undefined,
338
+ path_must_include: parseCsvFlag(flags, "path-must-include", relationTypesAllowed()) as RelationType[] | undefined,
339
+ path_must_exclude: parseCsvFlag(flags, "path-must-exclude", relationTypesAllowed()) as RelationType[] | undefined,
340
+ result_domains: parseCsvFlag(flags, "result-domains", [
341
+ "code",
342
+ "config",
343
+ "resource",
344
+ "settings",
345
+ "sql",
346
+ "project",
347
+ ] as const),
348
+ result_entity_types: parseCsvFlag(flags, "result-entity-types", [
349
+ "File",
350
+ "Chunk",
351
+ "Module",
352
+ "Project",
353
+ "ADR",
354
+ "Rule",
355
+ ] as const),
356
+ };
357
+
358
+ if (explicitEntityId || (!explicitQuery && seedLooksLikeEntityId(seed))) {
359
+ input.entity_id = seed;
360
+ } else {
361
+ input.query = seed;
362
+ }
363
+
364
+ const data = await runContextImpact(input);
365
+ if (isFlagEnabled(flags, "json")) {
366
+ emitEnvelope("impact", input as Record<string, unknown>, data);
367
+ return;
368
+ }
369
+ printSummary("impact", data);
370
+ }
371
+
372
+ function relationTypesAllowed(): readonly RelationType[] {
373
+ return [
374
+ "CALLS",
375
+ "CALLS_SQL",
376
+ "IMPORTS",
377
+ "USES_CONFIG_KEY",
378
+ "USES_RESOURCE_KEY",
379
+ "USES_SETTING_KEY",
380
+ "USES_CONFIG",
381
+ "TRANSFORMS_CONFIG",
382
+ "PART_OF",
383
+ ];
384
+ }
385
+
386
+ function seedLooksLikeEntityId(value: string): boolean {
387
+ return ENTITY_ID_PREFIXES.some((prefix) => value.startsWith(prefix));
388
+ }
389
+
390
+ function parseProfile(flags: Flags): ImpactParams["profile"] {
391
+ const value = optionalString(flags, "profile");
392
+ if (!value) {
393
+ return "all";
394
+ }
395
+ if (value === "all" || value === "config_only" || value === "config_to_sql" || value === "code_only" || value === "sql_only") {
396
+ return value;
397
+ }
398
+ throw new Error("--profile must be one of all, config_only, config_to_sql, code_only, sql_only");
399
+ }
400
+
401
+ function parseSortBy(flags: Flags): ImpactParams["sort_by"] {
402
+ const value = optionalString(flags, "sort-by");
403
+ if (!value) {
404
+ return "impact_score";
405
+ }
406
+ if (value === "impact_score" || value === "shortest_path" || value === "semantic_score" || value === "graph_score" || value === "trust_score") {
407
+ return value;
408
+ }
409
+ throw new Error("--sort-by must be one of impact_score, shortest_path, semantic_score, graph_score, trust_score");
410
+ }
411
+
412
+ async function runRules(args: string[]): Promise<void> {
413
+ const { flags } = parseArgs(args);
414
+ const input: RulesParams = {
415
+ scope: optionalString(flags, "scope"),
416
+ include_inactive: isFlagEnabled(flags, "include-inactive"),
417
+ };
418
+ const data = await runContextRules(input);
419
+ if (isFlagEnabled(flags, "json")) {
420
+ emitEnvelope("rules", input, data);
421
+ return;
422
+ }
423
+ printSummary("rules", data);
424
+ }
425
+
426
+ async function runExplain(args: string[]): Promise<void> {
427
+ const { flags, rest } = parseArgs(args);
428
+ const target = optionalString(flags, "id") ?? optionalString(flags, "query") ?? positionalText(rest, "query-or-entity-id");
429
+ const input: SearchParams = {
430
+ query: target,
431
+ top_k: parsePositiveIntFlag(flags, "top-k", 3, 20),
432
+ include_deprecated: isFlagEnabled(flags, "include-deprecated"),
433
+ response_preset: parsePreset(flags) ?? "full",
434
+ include_scores: true,
435
+ include_matched_rules: true,
436
+ include_content: isFlagEnabled(flags, "include-content") || undefined,
437
+ };
438
+ const search = await runContextSearch(input);
439
+ const data: ToolPayload = {
440
+ query: target,
441
+ entity_id: seedLooksLikeEntityId(target) ? target : undefined,
442
+ context_source: search.context_source,
443
+ warning: search.warning,
444
+ semantic_engine: search.semantic_engine,
445
+ ranking: search.ranking,
446
+ results: search.results,
447
+ explanation: "Scores and matched_rules come from the same local search ranking used by context.search.",
448
+ };
449
+
450
+ if (isFlagEnabled(flags, "json")) {
451
+ emitEnvelope("explain", input, data);
452
+ return;
453
+ }
454
+ printSummary("explain", data);
455
+ }
@@ -0,0 +1,115 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { spawnSync } from "node:child_process";
4
+ import { fileURLToPath, pathToFileURL } from "node:url";
5
+
6
+ const QUERY_MODULE = fileURLToPath(new URL("../dist/cli/query.js", import.meta.url));
7
+
8
+ function runQuery(args) {
9
+ const script = [
10
+ "const mod = await import(process.argv[1]);",
11
+ "await mod.runQueryCommand(JSON.parse(process.argv[2]));",
12
+ ].join("\n");
13
+
14
+ return spawnSync(
15
+ process.execPath,
16
+ [
17
+ "--input-type=module",
18
+ "-e",
19
+ script,
20
+ pathToFileURL(QUERY_MODULE).href,
21
+ JSON.stringify(args),
22
+ ],
23
+ { encoding: "utf8" },
24
+ );
25
+ }
26
+
27
+ function runJson(args, expectedStatus = 0) {
28
+ const result = runQuery(args);
29
+ assert.equal(result.status, expectedStatus, result.stderr || result.stdout);
30
+ return JSON.parse(result.stdout);
31
+ }
32
+
33
+ test("rules --json emits a stable envelope", () => {
34
+ const parsed = runJson(["rules", "--json"]);
35
+
36
+ assert.equal(parsed.ok, true);
37
+ assert.equal(parsed.command, "rules");
38
+ assert.equal(parsed.input.include_inactive, false);
39
+ assert.ok(["ryu", "cache"].includes(String(parsed.context_source)));
40
+ assert.ok(Array.isArray(parsed.data.rules));
41
+ assert.equal(parsed.data.count, parsed.data.rules.length);
42
+ });
43
+
44
+ test("search --json maps flags to context.search input", () => {
45
+ const parsed = runJson([
46
+ "search",
47
+ "rule.source_of_truth",
48
+ "--top-k",
49
+ "3",
50
+ "--preset",
51
+ "full",
52
+ "--scores",
53
+ "--matched-rules",
54
+ "--json",
55
+ ]);
56
+
57
+ assert.equal(parsed.ok, true);
58
+ assert.equal(parsed.command, "search");
59
+ assert.equal(parsed.input.query, "rule.source_of_truth");
60
+ assert.equal(parsed.input.top_k, 3);
61
+ assert.equal(parsed.input.response_preset, "full");
62
+ assert.equal(parsed.input.include_scores, true);
63
+ assert.equal(parsed.input.include_matched_rules, true);
64
+ assert.ok(Array.isArray(parsed.data.results));
65
+ assert.ok(parsed.data.results.length <= 3);
66
+ });
67
+
68
+ test("related --json preserves missing-entity runtime behavior", () => {
69
+ const parsed = runJson(["related", "file:no-such-file", "--json"]);
70
+
71
+ assert.equal(parsed.ok, true);
72
+ assert.equal(parsed.command, "related");
73
+ assert.equal(parsed.input.entity_id, "file:no-such-file");
74
+ assert.deepEqual(parsed.data.related, []);
75
+ assert.match(parsed.data.warning, /Entity not found/);
76
+ });
77
+
78
+ test("impact --json supports positional entity ids", () => {
79
+ const parsed = runJson([
80
+ "impact",
81
+ "file:bin/cortex.mjs",
82
+ "--depth",
83
+ "1",
84
+ "--top-k",
85
+ "2",
86
+ "--json",
87
+ ]);
88
+
89
+ assert.equal(parsed.ok, true);
90
+ assert.equal(parsed.command, "impact");
91
+ assert.equal(parsed.input.entity_id, "file:bin/cortex.mjs");
92
+ assert.equal(parsed.input.depth, 1);
93
+ assert.equal(parsed.input.top_k, 2);
94
+ assert.ok(Array.isArray(parsed.data.results));
95
+ });
96
+
97
+ test("explain --json enables scores and matched rules", () => {
98
+ const parsed = runJson(["explain", "rule.source_of_truth", "--json"]);
99
+
100
+ assert.equal(parsed.ok, true);
101
+ assert.equal(parsed.command, "explain");
102
+ assert.equal(parsed.input.include_scores, true);
103
+ assert.equal(parsed.input.include_matched_rules, true);
104
+ assert.equal(parsed.data.explanation.includes("context.search"), true);
105
+ assert.ok(Array.isArray(parsed.data.results));
106
+ });
107
+
108
+ test("json validation errors emit an error envelope", () => {
109
+ const parsed = runJson(["impact", "--json"], 1);
110
+
111
+ assert.equal(parsed.ok, false);
112
+ assert.equal(parsed.command, "impact");
113
+ assert.equal(parsed.error.code, "INVALID_ARGS");
114
+ assert.match(parsed.error.message, /Either --entity-id/);
115
+ });
@@ -3,7 +3,8 @@ set -euo pipefail
3
3
 
4
4
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
5
  REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
6
- MCP_DIR="$REPO_ROOT/.context/mcp"
6
+ CONTEXT_RUNTIME_DIR="$REPO_ROOT/.context/mcp"
7
+ MCP_DIR="$CONTEXT_RUNTIME_DIR"
7
8
  TOTAL_STEPS=6
8
9
  STEP_INDEX=0
9
10
 
@@ -55,7 +56,7 @@ install_deps_if_changed() {
55
56
  fi
56
57
  }
57
58
 
58
- step "Installing MCP dependencies"
59
+ step "Installing context runtime dependencies"
59
60
  info "note: upstream RyuGraph dependencies may print deprecation warnings during install"
60
61
  install_deps_if_changed "$MCP_DIR" "$MCP_DIR/.npm-cache"
61
62
  install_deps_if_changed "$REPO_ROOT/.context/scripts/parsers" "$REPO_ROOT/.context/scripts/parsers/.npm-cache"
@@ -4,7 +4,8 @@ set -euo pipefail
4
4
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
5
  REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
6
6
  CONTEXT_DIR="$REPO_ROOT/.context"
7
- MCP_DIR="$CONTEXT_DIR/mcp"
7
+ CONTEXT_RUNTIME_DIR="$CONTEXT_DIR/mcp"
8
+ MCP_DIR="$CONTEXT_RUNTIME_DIR"
8
9
 
9
10
  PASS=0
10
11
  FAIL=0
@@ -160,24 +161,24 @@ if [[ -f "$INGEST_MANIFEST" ]] && command -v git &>/dev/null && git -C "$REPO_RO
160
161
  fi
161
162
  fi
162
163
 
163
- # ── MCP Server ──────────────────────────────────────────
164
+ # ── Context Runtime / MCP Server ─────────────────────────
164
165
 
165
166
  echo ""
166
- echo " MCP Server"
167
+ echo " Context Runtime"
167
168
 
168
169
  if [[ -f "$MCP_DIR/dist/server.js" ]]; then
169
- pass ".context/mcp/dist/server.js exists"
170
+ pass "runtime dist present (.context/mcp/dist/server.js)"
170
171
  else
171
- fail ".context/mcp/dist/server.js missing — run: cd .context/mcp && npm run build"
172
+ fail "runtime dist missing — run: cd .context/mcp && npm run build"
172
173
  fi
173
174
 
174
175
  if [[ -d "$MCP_DIR/node_modules" ]]; then
175
- pass ".context/mcp/node_modules present"
176
+ pass "runtime dependencies present (.context/mcp/node_modules)"
176
177
  else
177
- fail ".context/mcp/node_modules missing — run: cd .context/mcp && npm install"
178
+ fail "runtime dependencies missing — run: cd .context/mcp && npm install"
178
179
  fi
179
180
 
180
- # Quick MCP import check
181
+ # Quick runtime import check
181
182
  if [[ -f "$MCP_DIR/dist/server.js" ]] && [[ -d "$MCP_DIR/node_modules" ]]; then
182
183
  MCP_CHECK=$(cd "$REPO_ROOT" && timeout 10 node -e '
183
184
  const start = Date.now();
@@ -3,7 +3,8 @@ set -euo pipefail
3
3
 
4
4
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
5
  REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
6
- MCP_DIR="$REPO_ROOT/.context/mcp"
6
+ CONTEXT_RUNTIME_DIR="$REPO_ROOT/.context/mcp"
7
+ MCP_DIR="$CONTEXT_RUNTIME_DIR"
7
8
 
8
9
  if ! command -v npm >/dev/null 2>&1; then
9
10
  echo "[embed] npm is required but not found on PATH"
@@ -12,5 +13,5 @@ fi
12
13
 
13
14
  mkdir -p "$MCP_DIR/.npm-cache"
14
15
 
15
- echo "[embed] generating embeddings via .context/mcp/embed"
16
+ echo "[embed] generating embeddings via context runtime"
16
17
  CORTEX_PROJECT_ROOT="$REPO_ROOT" NPM_CONFIG_CACHE="$MCP_DIR/.npm-cache" npm --prefix "$MCP_DIR" run embed --silent -- "$@"
@@ -3,7 +3,8 @@ set -euo pipefail
3
3
 
4
4
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
5
  REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
6
- MCP_DIR="$REPO_ROOT/.context/mcp"
6
+ CONTEXT_RUNTIME_DIR="$REPO_ROOT/.context/mcp"
7
+ MCP_DIR="$CONTEXT_RUNTIME_DIR"
7
8
 
8
9
  if [[ ! -f "$MCP_DIR/package.json" ]]; then
9
10
  echo "[graph-load] missing $MCP_DIR/package.json"
@@ -11,7 +12,7 @@ if [[ ! -f "$MCP_DIR/package.json" ]]; then
11
12
  fi
12
13
 
13
14
  if [[ ! -d "$MCP_DIR/node_modules" ]]; then
14
- echo "[graph-load] node_modules missing in .context/mcp/"
15
+ echo "[graph-load] node_modules missing in context runtime (.context/mcp compatibility path)"
15
16
  echo "[graph-load] run: cd .context/mcp && NPM_CONFIG_CACHE=$MCP_DIR/.npm-cache npm install"
16
17
  exit 1
17
18
  fi
@@ -1,11 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
- import { fileURLToPath } from "node:url";
5
- import { parseFrontmatter, parseStringList } from "../mcp/dist/frontmatter.js";
4
+ import { fileURLToPath, pathToFileURL } from "node:url";
6
5
 
7
6
  const __filename = fileURLToPath(import.meta.url);
8
7
  const __dirname = path.dirname(__filename);
8
+ const CONTEXT_RUNTIME_DIST = path.resolve(__dirname, "../mcp/dist");
9
+ const { parseFrontmatter, parseStringList } = await import(
10
+ pathToFileURL(path.join(CONTEXT_RUNTIME_DIST, "frontmatter.js")).href
11
+ );
9
12
 
10
13
  function normalizeForWsl(rawPath) {
11
14
  const m = rawPath.match(/^([A-Za-z]):[/\\](.*)/);