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.
- package/README.md +55 -26
- package/dist/index.js +251 -138
- 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
|
-
**
|
|
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
|
|
49
|
+
## Setup
|
|
50
50
|
|
|
51
|
-
**
|
|
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
|
-
|
|
67
|
-
> macOS / Linux: `~/.config/opencode/opencode.jsonc`
|
|
78
|
+
### Step 2 — Restart OpenCode
|
|
68
79
|
|
|
69
|
-
|
|
80
|
+
Close and reopen OpenCode (TUI or Desktop). Confirm **openkrak Connected** appears in the MCP panel.
|
|
70
81
|
|
|
71
|
-
**
|
|
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
|
-
|
|
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
|
-
##
|
|
97
|
+
## Tools
|
|
81
98
|
|
|
82
|
-
|
|
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
|
-
```
|
|
85
|
-
"
|
|
86
|
-
|
|
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
|
-
##
|
|
120
|
+
## Enable / Disable
|
|
92
121
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
|
131
|
+
## Pro License
|
|
103
132
|
|
|
104
|
-
Free tier is active by default — no account
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
244840
|
-
|
|
244841
|
-
|
|
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: `
|
|
244846
|
-
|
|
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
|
|
244850
|
-
const meta =
|
|
244851
|
-
const repo =
|
|
244852
|
-
const hotspots =
|
|
244853
|
-
const
|
|
244854
|
-
const
|
|
244855
|
-
const
|
|
244856
|
-
|
|
244857
|
-
|
|
244858
|
-
|
|
244859
|
-
|
|
244860
|
-
|
|
244861
|
-
|
|
244862
|
-
|
|
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
|
-
|
|
244865
|
-
|
|
244866
|
-
|
|
244867
|
-
|
|
244868
|
-
|
|
244869
|
-
|
|
244870
|
-
|
|
244871
|
-
|
|
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
|
-
`
|
|
244874
|
-
]
|
|
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
|
|
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:
|
|
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
|
-
|
|
244885
|
-
|
|
244886
|
-
|
|
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: `
|
|
244891
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
244909
|
-
|
|
244910
|
-
|
|
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
|
-
|
|
244914
|
-
|
|
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
|
-
|
|
244920
|
-
|
|
244921
|
-
|
|
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: `
|
|
244926
|
-
|
|
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
|
-
|
|
244938
|
-
|
|
244939
|
-
|
|
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
|
|
244942
|
-
})
|
|
244943
|
-
|
|
244944
|
-
|
|
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
|
-
|
|
244949
|
-
|
|
244950
|
-
|
|
244951
|
-
|
|
244952
|
-
|
|
244953
|
-
|
|
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
|
-
|
|
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
|
-
|
|
244959
|
-
...
|
|
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
|
|
244962
|
-
}),
|
|
244963
|
-
|
|
244964
|
-
|
|
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
|
|
244968
|
-
})
|
|
244969
|
-
|
|
244970
|
-
|
|
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
|
-
|
|
244976
|
-
|
|
244977
|
-
|
|
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
|
|
244982
|
-
|
|
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
|
-
|
|
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 ?
|
|
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 ?
|
|
245002
|
-
const ctx = brief
|
|
245003
|
-
const ep = brief
|
|
245004
|
-
const constraints = brief
|
|
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
|
-
|
|
245009
|
-
|
|
245010
|
-
|
|
245011
|
-
|
|
245012
|
-
|
|
245013
|
-
|
|
245014
|
-
|
|
245015
|
-
|
|
245016
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
245030
|
-
|
|
245031
|
-
|
|
245032
|
-
|
|
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
|
|
245042
|
-
})
|
|
245043
|
-
|
|
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);
|