@danielblomma/cortex-mcp 2.1.1 → 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 (150) hide show
  1. package/README.md +72 -24
  2. package/bin/cortex.mjs +50 -21
  3. package/mcp-registry-submission.json +63 -0
  4. package/package.json +19 -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/build.mjs +92 -0
  9. package/scaffold/mcp/package.json +4 -4
  10. package/scaffold/mcp/src/cli/query.ts +455 -0
  11. package/scaffold/mcp/src/embed.ts +181 -77
  12. package/scaffold/mcp/src/embedScheduler.ts +509 -0
  13. package/scaffold/mcp/src/embeddings.ts +31 -30
  14. package/scaffold/mcp/src/graphCsv.ts +65 -0
  15. package/scaffold/mcp/src/jsonl.ts +69 -14
  16. package/scaffold/mcp/src/loadGraph.ts +276 -40
  17. package/scaffold/mcp/src/lruCache.ts +41 -0
  18. package/scaffold/mcp/src/searchCore.ts +1 -1
  19. package/scaffold/mcp/src/searchResults.ts +3 -3
  20. package/scaffold/mcp/src/types.ts +6 -1
  21. package/scaffold/mcp/tests/embed-scheduler.test.mjs +474 -0
  22. package/scaffold/mcp/tests/embedding-jsonl.test.mjs +39 -0
  23. package/scaffold/mcp/tests/graph-bulk-load.test.mjs +258 -0
  24. package/scaffold/mcp/tests/graph-csv.test.mjs +118 -0
  25. package/scaffold/mcp/tests/lru-cache.test.mjs +37 -0
  26. package/scaffold/mcp/tests/query-cli.test.mjs +115 -0
  27. package/scaffold/mcp/tests/vector-index.test.mjs +109 -0
  28. package/scaffold/mcp/tsconfig.json +3 -1
  29. package/scaffold/scripts/bootstrap.sh +37 -4
  30. package/scaffold/scripts/dashboard.mjs +1 -1
  31. package/scaffold/scripts/doctor.sh +9 -8
  32. package/scaffold/scripts/embed.sh +3 -2
  33. package/scaffold/scripts/ingest-parsers.mjs +387 -0
  34. package/scaffold/scripts/ingest-worker.mjs +52 -0
  35. package/scaffold/scripts/ingest.mjs +550 -378
  36. package/scaffold/scripts/load-ryu.sh +3 -2
  37. package/scaffold/scripts/memory-compile.mjs +5 -2
  38. package/scaffold/scripts/memory-lint.mjs +5 -2
  39. package/scaffold/scripts/parsers/csharp.mjs +2 -1
  40. package/scaffold/scripts/parsers/javascript/ast.mjs +160 -8
  41. package/scaffold/scripts/parsers/javascript/chunks.mjs +129 -22
  42. package/scaffold/scripts/parsers/javascript/imports.mjs +38 -4
  43. package/scaffold/scripts/parsers/vbnet.mjs +2 -1
  44. package/scaffold/scripts/status.sh +50 -14
  45. package/scaffold/scripts/parsers/node_modules/.package-lock.json +0 -56
  46. package/scaffold/scripts/parsers/node_modules/acorn/CHANGELOG.md +0 -972
  47. package/scaffold/scripts/parsers/node_modules/acorn/LICENSE +0 -21
  48. package/scaffold/scripts/parsers/node_modules/acorn/README.md +0 -301
  49. package/scaffold/scripts/parsers/node_modules/acorn/bin/acorn +0 -4
  50. package/scaffold/scripts/parsers/node_modules/acorn/dist/acorn.d.mts +0 -883
  51. package/scaffold/scripts/parsers/node_modules/acorn/dist/acorn.d.ts +0 -883
  52. package/scaffold/scripts/parsers/node_modules/acorn/dist/acorn.js +0 -6295
  53. package/scaffold/scripts/parsers/node_modules/acorn/dist/acorn.mjs +0 -6266
  54. package/scaffold/scripts/parsers/node_modules/acorn/dist/bin.js +0 -90
  55. package/scaffold/scripts/parsers/node_modules/acorn/package.json +0 -50
  56. package/scaffold/scripts/parsers/node_modules/acorn-typescript/CHANGELOG.md +0 -421
  57. package/scaffold/scripts/parsers/node_modules/acorn-typescript/LICENSE +0 -21
  58. package/scaffold/scripts/parsers/node_modules/acorn-typescript/README.md +0 -81
  59. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/error.d.ts +0 -103
  60. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/error.js +0 -78
  61. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/error.js.map +0 -1
  62. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/extentions/decorators.d.ts +0 -167
  63. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/extentions/decorators.js +0 -75
  64. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/extentions/decorators.js.map +0 -1
  65. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/extentions/import-assertions.d.ts +0 -177
  66. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/extentions/import-assertions.js +0 -56
  67. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/extentions/import-assertions.js.map +0 -1
  68. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/extentions/jsx/index.d.ts +0 -198
  69. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/extentions/jsx/index.js +0 -327
  70. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/extentions/jsx/index.js.map +0 -1
  71. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/extentions/jsx/xhtml.d.ts +0 -256
  72. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/extentions/jsx/xhtml.js +0 -256
  73. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/extentions/jsx/xhtml.js.map +0 -1
  74. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/index.d.ts +0 -472
  75. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/index.js +0 -1
  76. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/index.js.map +0 -1
  77. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/index.mjs +0 -1
  78. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/middleware.d.ts +0 -159
  79. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/middleware.js +0 -2
  80. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/middleware.js.map +0 -1
  81. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/parseutil.d.ts +0 -10
  82. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/parseutil.js +0 -38
  83. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/parseutil.js.map +0 -1
  84. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/scopeflags.d.ts +0 -12
  85. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/scopeflags.js +0 -29
  86. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/scopeflags.js.map +0 -1
  87. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/tokenType.d.ts +0 -2
  88. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/tokenType.js +0 -118
  89. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/tokenType.js.map +0 -1
  90. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/types.d.ts +0 -60
  91. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/types.js +0 -2
  92. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/types.js.map +0 -1
  93. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/whitespace.d.ts +0 -2
  94. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/whitespace.js +0 -19
  95. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/whitespace.js.map +0 -1
  96. package/scaffold/scripts/parsers/node_modules/acorn-typescript/package.json +0 -53
  97. package/scaffold/scripts/parsers/node_modules/acorn-typescript/tsconfig.json +0 -19
  98. package/scaffold/scripts/parsers/node_modules/acorn-walk/CHANGELOG.md +0 -209
  99. package/scaffold/scripts/parsers/node_modules/acorn-walk/LICENSE +0 -21
  100. package/scaffold/scripts/parsers/node_modules/acorn-walk/README.md +0 -124
  101. package/scaffold/scripts/parsers/node_modules/acorn-walk/dist/walk.d.mts +0 -152
  102. package/scaffold/scripts/parsers/node_modules/acorn-walk/dist/walk.d.ts +0 -152
  103. package/scaffold/scripts/parsers/node_modules/acorn-walk/dist/walk.js +0 -485
  104. package/scaffold/scripts/parsers/node_modules/acorn-walk/dist/walk.mjs +0 -467
  105. package/scaffold/scripts/parsers/node_modules/acorn-walk/package.json +0 -50
  106. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/LICENSE +0 -24
  107. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/README.md +0 -23
  108. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-bash.wasm +0 -0
  109. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-c.wasm +0 -0
  110. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-c_sharp.wasm +0 -0
  111. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-cpp.wasm +0 -0
  112. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-css.wasm +0 -0
  113. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-dart.wasm +0 -0
  114. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-elisp.wasm +0 -0
  115. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-elixir.wasm +0 -0
  116. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-elm.wasm +0 -0
  117. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-embedded_template.wasm +0 -0
  118. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-go.wasm +0 -0
  119. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-html.wasm +0 -0
  120. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-java.wasm +0 -0
  121. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-javascript.wasm +0 -0
  122. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-json.wasm +0 -0
  123. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-kotlin.wasm +0 -0
  124. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-lua.wasm +0 -0
  125. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-objc.wasm +0 -0
  126. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-ocaml.wasm +0 -0
  127. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-php.wasm +0 -0
  128. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-python.wasm +0 -0
  129. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-ql.wasm +0 -0
  130. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-rescript.wasm +0 -0
  131. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-ruby.wasm +0 -0
  132. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-rust.wasm +0 -0
  133. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-scala.wasm +0 -0
  134. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-solidity.wasm +0 -0
  135. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-swift.wasm +0 -0
  136. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-systemrdl.wasm +0 -0
  137. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-tlaplus.wasm +0 -0
  138. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-toml.wasm +0 -0
  139. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-tsx.wasm +0 -0
  140. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-typescript.wasm +0 -0
  141. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-vue.wasm +0 -0
  142. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-yaml.wasm +0 -0
  143. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-zig.wasm +0 -0
  144. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/package.json +0 -64
  145. package/scaffold/scripts/parsers/node_modules/web-tree-sitter/LICENSE +0 -21
  146. package/scaffold/scripts/parsers/node_modules/web-tree-sitter/README.md +0 -198
  147. package/scaffold/scripts/parsers/node_modules/web-tree-sitter/package.json +0 -37
  148. package/scaffold/scripts/parsers/node_modules/web-tree-sitter/tree-sitter-web.d.ts +0 -242
  149. package/scaffold/scripts/parsers/node_modules/web-tree-sitter/tree-sitter.js +0 -1
  150. package/scaffold/scripts/parsers/node_modules/web-tree-sitter/tree-sitter.wasm +0 -0
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Builds the MCP server only when its TypeScript inputs changed.
4
+ *
5
+ * Every embed/graph-load/test invocation used to run a full `tsc` compile
6
+ * even with untouched sources — twice per bootstrap, on every `cortex
7
+ * update`. This guard hashes the inputs (src/**, tsconfig.json,
8
+ * package-lock.json) and skips the compiler when the previous build is
9
+ * current. `--force` rebuilds unconditionally.
10
+ */
11
+ import crypto from "node:crypto";
12
+ import fs from "node:fs";
13
+ import path from "node:path";
14
+ import { spawnSync } from "node:child_process";
15
+ import { fileURLToPath } from "node:url";
16
+
17
+ const ROOT = path.dirname(fileURLToPath(import.meta.url));
18
+ const DIST = path.join(ROOT, "dist");
19
+ const MARKER = path.join(DIST, ".cortex-build-hash");
20
+ // Every runtime entrypoint must exist for a build to count as current;
21
+ // checking only one would let a partially deleted dist pass as fresh.
22
+ const ENTRIES = ["server.js", "embed.js", "loadGraph.js"].map((name) => path.join(DIST, name));
23
+
24
+ function collectSources(dir, files = []) {
25
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
26
+ const absolute = path.join(dir, entry.name);
27
+ if (entry.isDirectory()) {
28
+ collectSources(absolute, files);
29
+ } else if (entry.isFile() && entry.name.endsWith(".ts")) {
30
+ files.push(absolute);
31
+ }
32
+ }
33
+ return files;
34
+ }
35
+
36
+ function inputHash() {
37
+ const hash = crypto.createHash("sha256");
38
+ const inputs = [
39
+ path.join(ROOT, "tsconfig.json"),
40
+ path.join(ROOT, "package.json"),
41
+ path.join(ROOT, "package-lock.json")
42
+ ];
43
+ const srcDir = path.join(ROOT, "src");
44
+ if (fs.existsSync(srcDir)) {
45
+ inputs.push(...collectSources(srcDir));
46
+ }
47
+ for (const file of inputs) {
48
+ if (!fs.existsSync(file)) {
49
+ continue;
50
+ }
51
+ hash.update(path.relative(ROOT, file));
52
+ hash.update("\0");
53
+ hash.update(fs.readFileSync(file));
54
+ hash.update("\0");
55
+ }
56
+ return hash.digest("hex");
57
+ }
58
+
59
+ const force = process.argv.includes("--force");
60
+ const current = inputHash();
61
+
62
+ if (!force && ENTRIES.every((entry) => fs.existsSync(entry)) && fs.existsSync(MARKER)) {
63
+ try {
64
+ if (fs.readFileSync(MARKER, "utf8").trim() === current) {
65
+ console.log("[build] dist is up to date (sources unchanged)");
66
+ process.exit(0);
67
+ }
68
+ } catch {
69
+ // unreadable marker -> rebuild
70
+ }
71
+ }
72
+
73
+ const tscLocal = path.join(ROOT, "node_modules", ".bin", "tsc");
74
+ if (!fs.existsSync(tscLocal)) {
75
+ console.error("[build] TypeScript not installed; run npm install in this directory first");
76
+ process.exit(1);
77
+ }
78
+ const result = spawnSync(tscLocal, ["-p", "tsconfig.json"], { cwd: ROOT, stdio: "inherit" });
79
+ if (result.error) {
80
+ console.error(`[build] failed to launch tsc: ${result.error.message}`);
81
+ process.exit(1);
82
+ }
83
+ if (result.status !== 0) {
84
+ process.exit(result.status ?? 1);
85
+ }
86
+
87
+ try {
88
+ fs.mkdirSync(DIST, { recursive: true });
89
+ fs.writeFileSync(MARKER, `${current}\n`);
90
+ } catch {
91
+ // A missing marker only means the next run rebuilds; never fail the build.
92
+ }
@@ -5,12 +5,12 @@
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "build": "tsc -p tsconfig.json",
8
- "embed": "npm run build --silent && node dist/embed.js",
9
- "graph:load": "npm run build --silent && node dist/loadGraph.js",
8
+ "embed": "node build.mjs && node dist/embed.js",
9
+ "graph:load": "node build.mjs && node dist/loadGraph.js",
10
10
  "dev": "node --loader ts-node/esm src/server.ts",
11
11
  "start": "node dist/server.js",
12
- "test": "npm run build --silent && node --test tests/*.test.mjs",
13
- "test:ci": "npm run build --silent && node --test --test-reporter=spec tests/*.test.mjs"
12
+ "test": "node build.mjs && node --test tests/*.test.mjs",
13
+ "test:ci": "node build.mjs && node --test --test-reporter=spec tests/*.test.mjs"
14
14
  },
15
15
  "dependencies": {
16
16
  "@huggingface/transformers": "^4.1.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
+ }