openkrak-mcp 1.0.11 → 1.0.13

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 (3) hide show
  1. package/README.md +55 -26
  2. package/dist/index.js +251 -138
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  MCP server for developer tooling. Performs static analysis on a software repository via the Dorchester engine and delivers a structured context brief to the language model before any file is accessed.
4
4
 
5
- **Current integration:** OpenCode. Claude Code, Codex, and Cursor support in development.
5
+ **Supported:** OpenCode (TUI + Desktop). Claude Code, Codex, and Cursor support in development.
6
6
 
7
7
  ---
8
8
 
@@ -46,9 +46,21 @@ Requires Node.js ≥ 18.
46
46
 
47
47
  ---
48
48
 
49
- ## Setup — OpenCode
49
+ ## Setup
50
50
 
51
- **Step 1.** Open `~/.config/opencode/opencode.jsonc` and add the `mcp` block:
51
+ OpenKrak works with both **OpenCode TUI** and **OpenCode Desktop**. The config file and setup steps are identical for both.
52
+
53
+ ### Config file location
54
+
55
+ | Platform | Path |
56
+ |----------|------|
57
+ | Windows | `C:\Users\<username>\.config\opencode\opencode.jsonc` |
58
+ | macOS | `~/.config/opencode/opencode.jsonc` |
59
+ | Linux | `~/.config/opencode/opencode.jsonc` |
60
+
61
+ ### Step 1 — Add OpenKrak to your config
62
+
63
+ Open the config file and add the `mcp` block:
52
64
 
53
65
  ```jsonc
54
66
  {
@@ -63,45 +75,62 @@ Requires Node.js ≥ 18.
63
75
  }
64
76
  ```
65
77
 
66
- > Windows: `C:\Users\<username>\.config\opencode\opencode.jsonc`
67
- > macOS / Linux: `~/.config/opencode/opencode.jsonc`
78
+ ### Step 2 — Restart OpenCode
68
79
 
69
- **Step 2.** Restart OpenCode. Confirm **openkrak Connected** appears in the MCP panel.
80
+ Close and reopen OpenCode (TUI or Desktop). Confirm **openkrak Connected** appears in the MCP panel.
70
81
 
71
- **Step 3.** Open any repository and run a coding task. OpenKrak runs automatically before the language model accesses any file.
82
+ > **OpenCode Desktop:** MCP status is shown in the bottom status bar. Green dot = connected.
83
+ > **OpenCode TUI:** MCP status is shown in the top-right panel.
84
+
85
+ ### Step 3 — Run your first analysis
86
+
87
+ Open any repository in OpenCode and send a message:
72
88
 
73
- To invoke explicitly:
74
89
  ```
75
- Use analyze_repo on /path/to/your/repo with objective "refactor the auth module"
90
+ analyze_repo on /absolute/path/to/your/repo
76
91
  ```
77
92
 
93
+ Or just start a coding task normally — OpenKrak runs automatically before the model reads any file.
94
+
78
95
  ---
79
96
 
80
- ## Enabling and Disabling
97
+ ## Tools
81
98
 
82
- Set `"enabled"` in your config and restart OpenCode:
99
+ | Tool | When to use |
100
+ |------|-------------|
101
+ | `analyze_repo` | Full pipeline. Call before any coding task on a new repo. Returns dependency graph, hotspot rankings, blast radius, and threat matrix. |
102
+ | `get_mahadata` | Compact repo brief — structure, entry points, constraints, risk score. Use when you need repo-wide context without a full re-analysis. |
103
+ | `get_hotspots` | Ranked list of high-risk files by complexity, coupling, and change frequency. Use before reviewing or refactoring. |
104
+ | `blast_radius` | Impact map for a specific file. Which files, modules, and APIs are affected by a change. Use before modifying a critical file. |
105
+
106
+ ### Example prompts
83
107
 
84
- ```jsonc
85
- "enabled": true // on
86
- "enabled": false // off
108
+ ```
109
+ analyze_repo on /path/to/repo with objective "refactor the auth module"
110
+ ```
111
+ ```
112
+ Run get_hotspots on /path/to/repo — which files should I avoid touching?
113
+ ```
114
+ ```
115
+ blast_radius on /path/to/repo for file src/api/routes/user.ts
87
116
  ```
88
117
 
89
118
  ---
90
119
 
91
- ## Tools
120
+ ## Enable / Disable
92
121
 
93
- | Tool | Description |
94
- |------|-------------|
95
- | `analyze_repo` | Full Dorchester pipeline. Returns dependency graph, hotspot rankings, blast radius, and threat matrix. Call before any coding task. |
96
- | `get_mahadata` | Compact repository intelligence brief. Structure, entry points, constraints, risk score. |
97
- | `get_hotspots` | Ranked list of high-risk files by complexity, coupling, and change frequency. |
98
- | `blast_radius` | Impact map for a specific file change. Which files, modules, and APIs are affected. |
122
+ Set `"enabled"` in your config and restart OpenCode:
123
+
124
+ ```jsonc
125
+ "enabled": true // on
126
+ "enabled": false // off
127
+ ```
99
128
 
100
129
  ---
101
130
 
102
- ## License Tiers
131
+ ## Pro License
103
132
 
104
- Free tier is active by default — no account or configuration required.
133
+ Free tier is active by default — no account required.
105
134
 
106
135
  | Plan | Price | Queries |
107
136
  |------|-------|---------|
@@ -109,7 +138,7 @@ Free tier is active by default — no account or configuration required.
109
138
  | Pro Monthly | $8 / month | Unlimited |
110
139
  | Pro Annual | $67.20 / year | Unlimited |
111
140
 
112
- To activate a Pro license, add your key to the MCP config:
141
+ To activate Pro, add your key to the config:
113
142
 
114
143
  ```jsonc
115
144
  {
@@ -127,7 +156,7 @@ To activate a Pro license, add your key to the MCP config:
127
156
  }
128
157
  ```
129
158
 
130
- License keys are available at [openkrak-web.vercel.app](https://openkrak-web.vercel.app). Delivered instantly via email after purchase. No subscription, no auto-renewal.
159
+ License keys: [openkrak-web.vercel.app](https://openkrak-web.vercel.app). Delivered via email after purchase. No subscription, no auto-renewal.
131
160
 
132
161
  ---
133
162
 
@@ -135,7 +164,7 @@ License keys are available at [openkrak-web.vercel.app](https://openkrak-web.ver
135
164
 
136
165
  - Static analysis only. No AI inference in the pipeline.
137
166
  - Anonymous usage telemetry is collected (query count, tool name, error events). No source code or file contents are transmitted.
138
- - License validation requires a network call to verify your key against the license server.
167
+ - License validation requires a network call to the license server on each tool invocation.
139
168
 
140
169
  ---
141
170
 
package/dist/index.js CHANGED
@@ -241794,7 +241794,7 @@ function inferLanguage(filePath) {
241794
241794
  if (filePath.endsWith(".js") || filePath.endsWith(".jsx")) return "javascript";
241795
241795
  return "unknown";
241796
241796
  }
241797
- function extractRawEdges(filePath, ast, fileNodeId) {
241797
+ function extractRawEdges(filePath, ast, fileNodeId, tsconfigPaths2) {
241798
241798
  const edges = [];
241799
241799
  const symbolRanges = [];
241800
241800
  (0, import_typescript_estree3.simpleTraverse)(ast, {
@@ -241845,8 +241845,7 @@ function extractRawEdges(filePath, ast, fileNodeId) {
241845
241845
  if (node.type === "ImportDeclaration") {
241846
241846
  const importPath = node.source.value;
241847
241847
  const line = node.loc?.start.line ?? null;
241848
- const isBarrel = isBarrelPath(importPath);
241849
- const resolvedTarget = resolveImportTarget(filePath, importPath);
241848
+ const resolvedTarget = resolveImportTarget(filePath, importPath, tsconfigPaths2);
241850
241849
  edges.push({
241851
241850
  from: importFromNodeId,
241852
241851
  to: resolvedTarget,
@@ -241875,7 +241874,7 @@ function extractRawEdges(filePath, ast, fileNodeId) {
241875
241874
  const sourceLiteral = node.source.type === "Literal" ? node.source.value : "<dynamic>";
241876
241875
  edges.push({
241877
241876
  from: fileNodeId,
241878
- to: resolveImportTarget(filePath, sourceLiteral),
241877
+ to: resolveImportTarget(filePath, sourceLiteral, tsconfigPaths2),
241879
241878
  kind: "dynamic_import",
241880
241879
  file: filePath,
241881
241880
  line,
@@ -241889,7 +241888,6 @@ function extractRawEdges(filePath, ast, fileNodeId) {
241889
241888
  edges.push({
241890
241889
  from: fileNodeId,
241891
241890
  to: superName,
241892
- // resolved to node_id in assembly
241893
241891
  kind: "extend",
241894
241892
  file: filePath,
241895
241893
  line: node.loc.start.line,
@@ -241954,7 +241952,7 @@ function extractRawEdges(filePath, ast, fileNodeId) {
241954
241952
  });
241955
241953
  return edges;
241956
241954
  }
241957
- function extractBarrelReExports(filePath, ast) {
241955
+ function extractBarrelReExports(filePath, ast, tsconfigPaths2) {
241958
241956
  const reExports = [];
241959
241957
  (0, import_typescript_estree3.simpleTraverse)(ast, {
241960
241958
  enter(node) {
@@ -241962,7 +241960,7 @@ function extractBarrelReExports(filePath, ast) {
241962
241960
  if (!node.source) return;
241963
241961
  const sourcePath = node.source.value;
241964
241962
  if (!sourcePath.startsWith(".")) return;
241965
- const resolvedSource = resolveImportTarget(filePath, sourcePath);
241963
+ const resolvedSource = resolveImportTarget(filePath, sourcePath, tsconfigPaths2);
241966
241964
  if (node.type === "ExportAllDeclaration") {
241967
241965
  reExports.push({
241968
241966
  barrelFile: filePath,
@@ -241985,7 +241983,7 @@ function extractBarrelReExports(filePath, ast) {
241985
241983
  });
241986
241984
  return reExports;
241987
241985
  }
241988
- function resolveImportTarget(fromFile, importPath) {
241986
+ function resolveImportTarget(fromFile, importPath, tsconfigPaths2) {
241989
241987
  if (importPath.startsWith(".")) {
241990
241988
  const dir = import_path3.default.dirname(fromFile);
241991
241989
  const resolved = import_path3.default.join(dir, importPath).replace(/\\/g, "/");
@@ -241995,11 +241993,29 @@ function resolveImportTarget(fromFile, importPath) {
241995
241993
  }
241996
241994
  return normalized;
241997
241995
  }
241996
+ if (tsconfigPaths2) {
241997
+ for (const [prefix, target] of Object.entries(tsconfigPaths2)) {
241998
+ if (importPath.startsWith(prefix)) {
241999
+ const rest = importPath.slice(prefix.length);
242000
+ const resolved = (target + rest).replace(/\\/g, "/");
242001
+ const normalized = resolved.replace(/\.js$/, ".ts");
242002
+ if (!import_path3.default.extname(normalized)) {
242003
+ return normalized + ".ts";
242004
+ }
242005
+ return normalized;
242006
+ }
242007
+ }
242008
+ }
242009
+ if (importPath.startsWith("@/")) {
242010
+ const stripped = importPath.slice(2);
242011
+ const normalized = stripped.replace(/\.js$/, ".ts");
242012
+ if (!import_path3.default.extname(normalized)) {
242013
+ return normalized + ".ts";
242014
+ }
242015
+ return normalized;
242016
+ }
241998
242017
  return importPath;
241999
242018
  }
242000
- function isBarrelPath(importPath) {
242001
- return importPath.endsWith("/index") || importPath.endsWith("/index.ts") || importPath.endsWith("/index.js") || importPath === "index";
242002
- }
242003
242019
  function assembleGraph(allNodes, rawEdges, barrelReExports = []) {
242004
242020
  for (const node of allNodes) {
242005
242021
  node.id = node.id.replace(/\\/g, "/");
@@ -242511,7 +242527,14 @@ function inferModule(filePath) {
242511
242527
  return parts.length >= 2 ? parts[0] : null;
242512
242528
  }
242513
242529
  function isEntryPoint(filePath) {
242514
- return /\/(index|main|app|server|cli)\.(ts|js)x?$/.test(filePath);
242530
+ const normalized = filePath.replace(/\\/g, "/");
242531
+ if (/\/(index|main|server|cli)\.(ts|js)x?$/.test(normalized)) return true;
242532
+ if (/^app\/.*page\.(ts|js)x?$/.test(normalized)) return true;
242533
+ if (/^app\/layout\.(ts|js)x?$/.test(normalized)) return true;
242534
+ if (/^app\/.*route\.(ts|js)x?$/.test(normalized)) return true;
242535
+ if (/^pages\//.test(normalized) && /\.(ts|js)x?$/.test(normalized)) return true;
242536
+ if (/^src\/(main|index|App)\.(ts|js)x?$/.test(normalized)) return true;
242537
+ return false;
242515
242538
  }
242516
242539
  function walkAst(node, onEnter, onLeave) {
242517
242540
  if (!node || typeof node !== "object" || !("type" in node)) return;
@@ -242622,9 +242645,9 @@ async function runDeepStrike(store, opts) {
242622
242645
  for (const s of symbols) s.content_hash = currentHash;
242623
242646
  allNodes.push(...symbols);
242624
242647
  const fileNodeId = symbols.find((n) => n.kind === "file")?.id ?? `${relPath}::file::0`;
242625
- const rawEdges = extractRawEdges(relPath, ast, fileNodeId);
242648
+ const rawEdges = extractRawEdges(relPath, ast, fileNodeId, tsconfigPaths);
242626
242649
  allRawEdges.push(...rawEdges);
242627
- const barrelReExports = extractBarrelReExports(relPath, ast);
242650
+ const barrelReExports = extractBarrelReExports(relPath, ast, tsconfigPaths);
242628
242651
  allBarrelReExports.push(...barrelReExports);
242629
242652
  const secFindings = detectSecurityPatterns(relPath, content);
242630
242653
  allFindings.push(...secFindings);
@@ -244836,150 +244859,244 @@ function estimateTokensSaved(totalLoc, totalFiles, mahadataOutput) {
244836
244859
 
244837
244860
  // src/tools/analyze_repo.ts
244838
244861
  async function handleAnalyzeRepo(args) {
244839
- const result = await runPipeline({
244840
- repoPath: args.path,
244841
- objective: args.objective ?? "Analyze repository"
244842
- });
244862
+ let result;
244863
+ try {
244864
+ result = await runPipeline({
244865
+ repoPath: args.path,
244866
+ objective: args.objective ?? "Full repository analysis"
244867
+ });
244868
+ } catch (e) {
244869
+ return {
244870
+ content: [{ type: "text", text: `OpenKrak is not available for this repository. Proceed with direct file analysis.
244871
+ Reason: ${e instanceof Error ? e.message : String(e)}` }],
244872
+ isError: false
244873
+ };
244874
+ }
244843
244875
  if (result.status === "failed") {
244844
244876
  return {
244845
- content: [{ type: "text", text: `Analysis failed: ${result.error}` }],
244846
- isError: true
244877
+ content: [{ type: "text", text: `OpenKrak could not complete analysis for this path. Proceed without it.
244878
+ Detail: ${result.error ?? "unknown"}` }],
244879
+ isError: false
244847
244880
  };
244848
244881
  }
244849
- const mahadata = result.mahadata;
244850
- const meta = mahadata.meta;
244851
- const repo = mahadata.repository;
244852
- const hotspots = mahadata.hotspots;
244853
- const findings = mahadata.findings;
244854
- const brief = mahadata.execution_brief;
244855
- const summary = [
244856
- `# OpenKrak Analysis \u2014 ${repo?.name ?? args.path}`,
244857
- `Status: ${result.status} (${result.durationMs}ms)`,
244858
- `Files: ${repo?.total_files ?? "?"} | LOC: ${repo?.total_loc ?? "?"}`,
244859
- `Language: ${repo?.primary_language ?? "?"} | Framework: ${repo?.framework ?? "none"}`,
244860
- ``,
244861
- `## Hotspots (${hotspots?.length ?? 0} files)`,
244862
- ...hotspots?.slice(0, 5).map((h) => {
244882
+ const m = result.mahadata;
244883
+ const meta = m.meta;
244884
+ const repo = m.repository;
244885
+ const hotspots = m.hotspots ?? [];
244886
+ const blastRadius = m.blast_radius ?? [];
244887
+ const findings = m.findings ?? [];
244888
+ const brief = m.execution_brief;
244889
+ const threat = m.threat_matrix;
244890
+ const topology = m.project_topology;
244891
+ const totalFiles = Number(repo?.total_files ?? 0);
244892
+ const hotspotLimit = totalFiles < 50 ? 5 : totalFiles < 200 ? 8 : 12;
244893
+ const ep = brief?.recommended_entry_points ?? [];
244894
+ const ctx = brief?.critical_context ?? [];
244895
+ const constraints = brief?.constraints ?? [];
244896
+ const blockers = threat?.blockers ?? [];
244897
+ const warnings = threat?.warnings ?? [];
244898
+ const lines = [
244899
+ `\u2554\u2550\u2550 DORCHESTER SCAN COMPLETE \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550`,
244900
+ `\u2551 Repo : ${repo?.name ?? args.path}`,
244901
+ `\u2551 Scan ID : ${(meta?.scan_id ?? "?").slice(0, 8)} | Duration: ${result.durationMs}ms`,
244902
+ `\u2551 Stats : ${repo?.total_files ?? "?"} files | ${repo?.total_loc ?? "?"} LOC | ${repo?.primary_language ?? "?"} | framework: ${repo?.framework ?? "none"}`,
244903
+ `\u2551 Topology : ${topology?.type ?? "?"} \u2014 modules: ${(topology?.modules ?? []).length}`,
244904
+ `\u2560\u2550\u2550 OBJECTIVE \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550`,
244905
+ `\u2551 ${brief?.objective ?? args.objective ?? "\u2014"}`,
244906
+ `\u2551 ${brief?.repository_summary ?? ""}`,
244907
+ `\u2560\u2550\u2550 ENTRY POINTS \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550`,
244908
+ ...ep.length > 0 ? ep.map((e) => `\u2551 \u2192 ${e.path}${e.symbol ? `::${e.symbol}` : ""} [${e.reason}]`) : [`\u2551 \u2192 none detected`],
244909
+ `\u2560\u2550\u2550 CRITICAL CONTEXT \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550`,
244910
+ ...ctx.length > 0 ? ctx.map((c) => `\u2551 ${c.key}: ${c.value}`) : [`\u2551 \u2014`],
244911
+ `\u2560\u2550\u2550 HOTSPOT REGISTRY (${hotspots.length} files ranked) \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550`,
244912
+ ...hotspots.slice(0, hotspotLimit).map((h, i) => {
244863
244913
  const hs = h;
244864
- return `- ${hs.path} [${hs.risk_level}] score: ${Number(hs.score).toFixed(2)}`;
244865
- }) ?? [],
244866
- ``,
244867
- `## Findings (${findings?.length ?? 0} total)`,
244868
- ``,
244869
- `## Execution Brief`,
244870
- brief?.objective ? `Objective: ${brief.objective}` : "",
244871
- brief?.repository_summary ? `Summary: ${brief.repository_summary}` : "",
244914
+ const reasons = hs.reasons ?? [];
244915
+ const bar = "\u2588".repeat(Math.round(Number(hs.score) * 10));
244916
+ return `\u2551 ${String(i + 1).padStart(2)}. [${String(hs.risk_level).toUpperCase().padEnd(8)}] ${hs.path}
244917
+ \u2551 score:${Number(hs.score).toFixed(3)} ${bar} changes:${hs.change_frequency} flags:${reasons.map((r) => r.type).join(", ")}`;
244918
+ }),
244919
+ `\u2560\u2550\u2550 BLAST RADIUS (top ${Math.min(5, blastRadius.length)}) \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550`,
244920
+ ...blastRadius.slice(0, 5).map((b) => {
244921
+ const br = b;
244922
+ return `\u2551 \u26A1 ${br.trigger_file}
244923
+ \u2551 \u2192 ${br.total_affected_files} files | ${br.total_affected_modules} modules | risk:${Number(br.risk_score).toFixed(3)}`;
244924
+ }),
244925
+ `\u2560\u2550\u2550 THREAT MATRIX \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550`,
244926
+ `\u2551 Overall Risk : ${Number(threat?.overall_risk_score ?? 0).toFixed(3)} | ${threat?.risk_summary ?? ""}`,
244927
+ `\u2551 Blockers : ${blockers.length > 0 ? blockers.slice(0, 3).map((b) => b.description ?? b).join(" | ") : "none"}`,
244928
+ `\u2551 Warnings : ${warnings.length} total${warnings.length > 0 ? " \u2014 " + warnings.slice(0, 3).map((w) => w.description ?? w).join(" | ") : ""}`,
244929
+ `\u2551 Findings : ${findings.length} total`,
244930
+ `\u2560\u2550\u2550 CONSTRAINTS \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550`,
244931
+ `\u2551 ${constraints.length > 0 ? constraints.join(" | ") : "none"}`,
244932
+ `\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550`,
244872
244933
  ``,
244873
- `Scan ID: ${meta?.scan_id ?? "?"}`
244874
- ].filter(Boolean).join("\n");
244934
+ `Dorchester has pre-mapped this repository. Proceed with full context. Do not read source files unless specific line-level detail is required.`
244935
+ ];
244936
+ const output = lines.join("\n");
244875
244937
  const totalLoc = Number(repo?.total_loc ?? 0);
244876
- const totalFiles = Number(repo?.total_files ?? 0);
244877
- const saved = estimateTokensSaved(totalLoc, totalFiles, summary);
244938
+ const saved = estimateTokensSaved(totalLoc, totalFiles, output);
244878
244939
  trackTokens(saved, "analyze_repo");
244879
- return { content: [{ type: "text", text: summary }] };
244940
+ return { content: [{ type: "text", text: output }] };
244880
244941
  }
244881
244942
 
244882
244943
  // src/tools/get_hotspots.ts
244883
244944
  async function handleGetHotspots(args) {
244884
- const result = await runPipeline({
244885
- repoPath: args.path,
244886
- objective: "Identify hotspots"
244887
- });
244945
+ let result;
244946
+ try {
244947
+ result = await runPipeline({
244948
+ repoPath: args.path,
244949
+ objective: "Identify hotspots"
244950
+ });
244951
+ } catch (e) {
244952
+ return {
244953
+ content: [{ type: "text", text: `OpenKrak is not available for this repository. Proceed with direct file analysis.
244954
+ Reason: ${e instanceof Error ? e.message : String(e)}` }],
244955
+ isError: false
244956
+ };
244957
+ }
244888
244958
  if (result.status === "failed") {
244889
244959
  return {
244890
- content: [{ type: "text", text: `Analysis failed: ${result.error}` }],
244891
- isError: true
244960
+ content: [{ type: "text", text: `OpenKrak could not complete hotspot analysis. Proceed without it.
244961
+ Detail: ${result.error ?? "unknown"}` }],
244962
+ isError: false
244892
244963
  };
244893
244964
  }
244894
244965
  const mahadata = result.mahadata;
244895
244966
  const hotspots = mahadata.hotspots ?? [];
244967
+ const repo = mahadata.repository;
244896
244968
  if (hotspots.length === 0) {
244897
244969
  return {
244898
- content: [{ type: "text", text: "No hotspots detected." }]
244970
+ content: [{ type: "text", text: `OpenKrak: no hotspots detected in ${repo?.name ?? args.path}. Repository appears low-risk.` }]
244899
244971
  };
244900
244972
  }
244901
244973
  const lines = [
244902
- `# Hotspots \u2014 ${hotspots.length} files ranked by risk`,
244903
- "",
244974
+ `\u2554\u2550\u2550 HOTSPOT REGISTRY \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550`,
244975
+ `\u2551 ${repo?.name ?? args.path} | ${hotspots.length} files ranked by composite risk score`,
244976
+ `\u2551 duration: ${result.durationMs}ms`,
244977
+ `\u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550`,
244904
244978
  ...hotspots.map((h, i) => {
244905
244979
  const hs = h;
244906
244980
  const reasons = hs.reasons ?? [];
244981
+ const score = Number(hs.score);
244982
+ const bar = "\u2588".repeat(Math.max(1, Math.round(score * 10))) + "\u2591".repeat(Math.max(0, 10 - Math.round(score * 10)));
244907
244983
  return [
244908
- `## ${i + 1}. ${hs.path}`,
244909
- `Risk: ${hs.risk_level} | Score: ${Number(hs.score).toFixed(3)} | Changes: ${hs.change_frequency}`,
244910
- `Reasons: ${reasons.map((r) => r.type).join(", ")}`
244984
+ `\u2551`,
244985
+ `\u2551 ${String(i + 1).padStart(2)}. ${hs.path}`,
244986
+ `\u2551 Risk : ${String(hs.risk_level).toUpperCase()} | Score: ${score.toFixed(3)} [${bar}]`,
244987
+ `\u2551 Changes : ${hs.change_frequency} | Flags: ${reasons.map((r) => r.type).join(", ")}`,
244988
+ ...reasons.filter((r) => r.detail).slice(0, 2).map((r) => `\u2551 Detail : ${r.detail}`)
244911
244989
  ].join("\n");
244912
- })
244913
- ].join("\n");
244914
- return { content: [{ type: "text", text: lines }] };
244990
+ }),
244991
+ `\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550`,
244992
+ ``,
244993
+ `Touch these files with caution. Prioritize review in order listed.`
244994
+ ];
244995
+ return { content: [{ type: "text", text: lines.join("\n") }] };
244915
244996
  }
244916
244997
 
244917
244998
  // src/tools/blast_radius.ts
244918
244999
  async function handleBlastRadius(args) {
244919
- const result = await runPipeline({
244920
- repoPath: args.path,
244921
- objective: `Blast radius for ${args.file}`
244922
- });
245000
+ let result;
245001
+ try {
245002
+ result = await runPipeline({
245003
+ repoPath: args.path,
245004
+ objective: `Blast radius for ${args.file}`
245005
+ });
245006
+ } catch (e) {
245007
+ return {
245008
+ content: [{ type: "text", text: `OpenKrak is not available for this repository. Proceed with direct analysis.
245009
+ Reason: ${e instanceof Error ? e.message : String(e)}` }],
245010
+ isError: false
245011
+ };
245012
+ }
244923
245013
  if (result.status === "failed") {
244924
245014
  return {
244925
- content: [{ type: "text", text: `Analysis failed: ${result.error}` }],
244926
- isError: true
245015
+ content: [{ type: "text", text: `OpenKrak could not compute blast radius. Proceed without it.
245016
+ Detail: ${result.error ?? "unknown"}` }],
245017
+ isError: false
244927
245018
  };
244928
245019
  }
244929
245020
  const mahadata = result.mahadata;
244930
245021
  const blastRadiusAll = mahadata.blast_radius ?? [];
245022
+ const repo = mahadata.repository;
244931
245023
  const entry = blastRadiusAll.find((b) => {
244932
245024
  const br = b;
244933
245025
  return br.trigger_file === args.file || br.trigger_file?.endsWith(args.file);
244934
245026
  });
244935
245027
  if (!entry) {
244936
245028
  const lines2 = [
244937
- `# Blast Radius \u2014 file not matched: ${args.file}`,
244938
- `Available trigger files: ${blastRadiusAll.length}`,
244939
- ...blastRadiusAll.slice(0, 10).map((b) => {
245029
+ `\u2554\u2550\u2550 BLAST RADIUS \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550`,
245030
+ `\u2551 File not matched: ${args.file}`,
245031
+ `\u2551 Dorchester tracked ${blastRadiusAll.length} trigger files in this repo.`,
245032
+ `\u2560\u2550\u2550 AVAILABLE TRIGGER FILES \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550`,
245033
+ ...blastRadiusAll.slice(0, 12).map((b) => {
244940
245034
  const br = b;
244941
- return `- ${br.trigger_file} (risk: ${Number(br.risk_score).toFixed(2)}, affects ${br.total_affected_files} files)`;
244942
- })
244943
- ].join("\n");
244944
- return { content: [{ type: "text", text: lines2 }] };
245035
+ return `\u2551 \u2022 ${br.trigger_file} (risk:${Number(br.risk_score).toFixed(3)}, affects ${br.total_affected_files} files)`;
245036
+ }),
245037
+ `\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550`
245038
+ ];
245039
+ return { content: [{ type: "text", text: lines2.join("\n") }] };
244945
245040
  }
244946
245041
  const impact = entry.impact;
245042
+ const files = impact?.files ?? [];
245043
+ const modules = impact?.modules ?? [];
245044
+ const apis = impact?.apis ?? [];
245045
+ const riskScore = Number(entry.risk_score);
245046
+ const riskBar = "\u2588".repeat(Math.max(1, Math.round(riskScore * 10))) + "\u2591".repeat(Math.max(0, 10 - Math.round(riskScore * 10)));
244947
245047
  const lines = [
244948
- `# Blast Radius \u2014 ${entry.trigger_file}`,
244949
- `Risk Score: ${Number(entry.risk_score).toFixed(3)}`,
244950
- `Affected Files: ${entry.total_affected_files} | Modules: ${entry.total_affected_modules}`,
244951
- ``,
244952
- `## Affected Files (top 10)`,
244953
- ...(impact.files ?? []).slice(0, 10).map((f) => {
245048
+ `\u2554\u2550\u2550 BLAST RADIUS \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550`,
245049
+ `\u2551 Trigger : ${entry.trigger_file}`,
245050
+ `\u2551 Repo : ${repo?.name ?? args.path} | duration:${result.durationMs}ms`,
245051
+ `\u2560\u2550\u2550 IMPACT SUMMARY \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550`,
245052
+ `\u2551 Risk Score : ${riskScore.toFixed(3)} [${riskBar}]`,
245053
+ `\u2551 Affected Files : ${entry.total_affected_files}`,
245054
+ `\u2551 Affected Modules: ${entry.total_affected_modules}`,
245055
+ `\u2551 APIs at Risk : ${apis.length}`,
245056
+ `\u2560\u2550\u2550 AFFECTED FILES (${files.length} total) \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550`,
245057
+ ...files.slice(0, 15).map((f) => {
244954
245058
  const fi = f;
244955
- return `- [${fi.impact_type}] ${fi.path} (depth: ${fi.depth}, confidence: ${Number(fi.confidence).toFixed(2)})`;
245059
+ const depthBar = "\xB7".repeat(Number(fi.depth ?? 0));
245060
+ return `\u2551 ${depthBar}\u2192 [${String(fi.impact_type ?? "?").padEnd(8)}] ${fi.path} confidence:${Number(fi.confidence ?? 0).toFixed(2)}`;
244956
245061
  }),
244957
- ``,
244958
- `## Affected Modules`,
244959
- ...(impact.modules ?? []).map((m) => {
245062
+ ...files.length > 15 ? [`\u2551 ... and ${files.length - 15} more`] : [],
245063
+ `\u2560\u2550\u2550 AFFECTED MODULES \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550`,
245064
+ ...modules.length > 0 ? modules.map((m) => {
244960
245065
  const mi = m;
244961
- return `- ${mi.name}: ${mi.impact_level}`;
244962
- }),
244963
- ``,
244964
- `## APIs at Risk`,
244965
- ...(impact.apis ?? []).map((a) => {
245066
+ return `\u2551 \u2022 ${mi.name} \u2014 ${mi.impact_level}`;
245067
+ }) : [`\u2551 none`],
245068
+ `\u2560\u2550\u2550 APIs AT RISK \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550`,
245069
+ ...apis.length > 0 ? apis.map((a) => {
244966
245070
  const ai = a;
244967
- return `- ${ai.endpoint}: ${ai.impact_level}`;
244968
- })
244969
- ].join("\n");
244970
- return { content: [{ type: "text", text: lines }] };
245071
+ return `\u2551 \u2022 ${ai.endpoint} \u2014 ${ai.impact_level}`;
245072
+ }) : [`\u2551 none`],
245073
+ `\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550`,
245074
+ ``,
245075
+ `Modifying ${entry.trigger_file} will cascade to ${entry.total_affected_files} files across ${entry.total_affected_modules} modules. Proceed with full awareness of impact chain above.`
245076
+ ];
245077
+ return { content: [{ type: "text", text: lines.join("\n") }] };
244971
245078
  }
244972
245079
 
244973
245080
  // src/tools/get_mahadata.ts
244974
245081
  async function handleGetMahadata(args) {
244975
- const result = await runPipeline({
244976
- repoPath: args.path,
244977
- objective: args.objective ?? "Analyze repository"
244978
- });
245082
+ let result;
245083
+ try {
245084
+ result = await runPipeline({
245085
+ repoPath: args.path,
245086
+ objective: args.objective ?? "Repository intelligence brief"
245087
+ });
245088
+ } catch (e) {
245089
+ return {
245090
+ content: [{ type: "text", text: `OpenKrak is not available for this repository. Proceed with direct file analysis.
245091
+ Reason: ${e instanceof Error ? e.message : String(e)}` }],
245092
+ isError: false
245093
+ };
245094
+ }
244979
245095
  if (result.status === "failed") {
244980
245096
  return {
244981
- content: [{ type: "text", text: `OpenKrak error: ${result.error}` }],
244982
- isError: true
245097
+ content: [{ type: "text", text: `OpenKrak could not generate a brief for this path. Proceed without it.
245098
+ Detail: ${result.error ?? "unknown"}` }],
245099
+ isError: false
244983
245100
  };
244984
245101
  }
244985
245102
  const m = result.mahadata;
@@ -244989,58 +245106,54 @@ async function handleGetMahadata(args) {
244989
245106
  const hotspots = m.hotspots ?? [];
244990
245107
  const blastRadius = m.blast_radius ?? [];
244991
245108
  const findings = m.findings ?? [];
244992
- if (!brief) {
244993
- return {
244994
- content: [{ type: "text", text: "OpenKrak: no execution_brief generated." }],
244995
- isError: true
244996
- };
244997
- }
245109
+ const topology = m.project_topology;
244998
245110
  const totalFiles = Number(repo?.total_files ?? 0);
244999
- const hotspotLimit = totalFiles < 50 ? 3 : totalFiles < 200 ? 5 : totalFiles < 500 ? 8 : 12;
245111
+ const hotspotLimit = totalFiles < 50 ? 5 : totalFiles < 200 ? 8 : 12;
245000
245112
  const blastLimit = totalFiles < 50 ? 3 : totalFiles < 200 ? 5 : 8;
245001
- const findingLimit = totalFiles < 50 ? 3 : totalFiles < 200 ? 5 : 10;
245002
- const ctx = brief.critical_context ?? [];
245003
- const ep = brief.recommended_entry_points ?? [];
245004
- const constraints = brief.constraints ?? [];
245113
+ const findingLimit = totalFiles < 50 ? 5 : totalFiles < 200 ? 8 : 12;
245114
+ const ctx = brief?.critical_context ?? [];
245115
+ const ep = brief?.recommended_entry_points ?? [];
245116
+ const constraints = brief?.constraints ?? [];
245005
245117
  const blockers = threat?.blockers ?? [];
245006
245118
  const warnings = threat?.warnings ?? [];
245007
245119
  const lines = [
245008
- `[MAHADATA] repo:${repo?.name ?? args.path}`,
245009
- `lang:${repo?.primary_language ?? "?"} | files:${repo?.total_files ?? "?"} | loc:${repo?.total_loc ?? "?"} | framework:${repo?.framework ?? "none"}`,
245010
- `objective: ${brief.objective}`,
245011
- `summary: ${brief.repository_summary}`,
245012
- ``,
245013
- `CONTEXT:`,
245014
- ...ctx.map((c) => ` ${c.key}: ${c.value}`),
245015
- ``,
245016
- `HOTSPOTS (${hotspots.length} total, top ${Math.min(hotspotLimit, hotspots.length)}):`,
245120
+ `\u2554\u2550\u2550 MAHADATA BRIEF \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550`,
245121
+ `\u2551 ${repo?.name ?? args.path} | ${repo?.total_files ?? "?"}f ${repo?.total_loc ?? "?"}loc ${repo?.primary_language ?? "?"} framework:${repo?.framework ?? "none"}`,
245122
+ `\u2551 topology:${topology?.type ?? "?"} | duration:${result.durationMs}ms`,
245123
+ `\u2560\u2550\u2550 OBJECTIVE \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550`,
245124
+ `\u2551 ${brief?.objective ?? "\u2014"}`,
245125
+ `\u2551 ${brief?.repository_summary ?? ""}`,
245126
+ `\u2560\u2550\u2550 ENTRY POINTS \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550`,
245127
+ ...ep.length > 0 ? ep.map((e) => `\u2551 \u2192 ${e.path}${e.symbol ? `::${e.symbol}` : ""}`) : [`\u2551 none detected`],
245128
+ `\u2560\u2550\u2550 CONTEXT \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550`,
245129
+ ...ctx.length > 0 ? ctx.map((c) => `\u2551 ${c.key}: ${c.value}`) : [`\u2551 \u2014`],
245130
+ `\u2560\u2550\u2550 HOTSPOTS \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550`,
245017
245131
  ...hotspots.slice(0, hotspotLimit).map((h) => {
245018
245132
  const hs = h;
245019
245133
  const reasons = hs.reasons ?? [];
245020
- return ` [${hs.risk_level}] ${hs.path} score:${Number(hs.score).toFixed(2)} changes:${hs.change_frequency} \u2014 ${reasons.map((r) => r.type).join(", ")}`;
245134
+ return `\u2551 [${String(hs.risk_level).toUpperCase().padEnd(8)}] score:${Number(hs.score).toFixed(3)} ${hs.path} \u2014 ${reasons.map((r) => r.type).join(", ")}`;
245021
245135
  }),
245022
- ``,
245023
- `BLAST RADIUS (top ${Math.min(blastLimit, blastRadius.length)}):`,
245136
+ `\u2560\u2550\u2550 BLAST RADIUS \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550`,
245024
245137
  ...blastRadius.slice(0, blastLimit).map((b) => {
245025
245138
  const br = b;
245026
- return ` ${br.trigger_file} \u2192 affects ${br.total_affected_files} files, ${br.total_affected_modules} modules (risk:${Number(br.risk_score).toFixed(2)})`;
245139
+ return `\u2551 \u26A1 ${br.trigger_file} \u2192 ${br.total_affected_files} files ${br.total_affected_modules} modules risk:${Number(br.risk_score).toFixed(3)}`;
245027
245140
  }),
245028
- ``,
245029
- `ENTRY POINTS:`,
245030
- ...ep.map((e) => ` ${e.path}${e.symbol ? `::${e.symbol}` : ""} \u2014 ${e.reason}`),
245031
- ``,
245032
- `CONSTRAINTS: ${constraints.join(" | ")}`,
245033
- ``,
245034
- `THREAT: overall_risk=${Number(threat?.overall_risk_score ?? 0).toFixed(2)} | ${threat?.risk_summary ?? ""}`,
245035
- blockers.length > 0 ? `BLOCKERS (${blockers.length}): ${blockers.slice(0, 3).map((b) => b.description ?? b).join(" | ")}` : `BLOCKERS: none`,
245036
- warnings.length > 0 ? `WARNINGS (${warnings.length}): ${warnings.slice(0, 5).map((w) => w.description ?? w).join(" | ")}` : `WARNINGS: none`,
245037
- ``,
245038
- findings.length > 0 ? `FINDINGS (${findings.length} total, top ${Math.min(findingLimit, findings.length)}):` : null,
245141
+ `\u2560\u2550\u2550 THREAT \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550`,
245142
+ `\u2551 risk:${Number(threat?.overall_risk_score ?? 0).toFixed(3)} ${threat?.risk_summary ?? ""}`,
245143
+ `\u2551 blockers:${blockers.length} warnings:${warnings.length} findings:${findings.length}`,
245144
+ ...blockers.length > 0 ? blockers.slice(0, 2).map((b) => `\u2551 \u2716 ${b.description ?? b}`) : [],
245145
+ ...warnings.length > 0 ? warnings.slice(0, 3).map((w) => `\u2551 \u25B2 ${w.description ?? w}`) : [],
245146
+ `\u2560\u2550\u2550 FINDINGS (${findings.length} total) \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550`,
245039
245147
  ...findings.slice(0, findingLimit).map((f) => {
245040
245148
  const fi = f;
245041
- return ` [${fi.severity ?? fi.type}] ${fi.file ?? fi.path} \u2014 ${fi.message ?? fi.description ?? ""}`;
245042
- })
245043
- ].filter((l) => l !== null);
245149
+ return `\u2551 [${String(fi.severity ?? fi.type ?? "info").toUpperCase().padEnd(7)}] ${fi.file ?? fi.path ?? "?"} \u2014 ${fi.message ?? fi.description ?? ""}`;
245150
+ }),
245151
+ `\u2560\u2550\u2550 CONSTRAINTS \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550`,
245152
+ `\u2551 ${constraints.length > 0 ? constraints.join(" | ") : "none"}`,
245153
+ `\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550`,
245154
+ ``,
245155
+ `Dorchester brief delivered. Answer from this data. Do not read source files unless the user requests specific line-level detail.`
245156
+ ];
245044
245157
  const output = lines.join("\n");
245045
245158
  const totalLoc = Number(repo?.total_loc ?? 0);
245046
245159
  const saved = estimateTokensSaved(totalLoc, totalFiles, output);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openkrak-mcp",
3
- "version": "1.0.11",
3
+ "version": "1.0.13",
4
4
  "description": "OpenKrak MCP Server - AI coding intelligence via Dorchester engine",
5
5
  "mcpName": "io.github.FrnzJulianBergmann/openkrak",
6
6
  "type": "commonjs",