kirograph 0.16.1 → 0.19.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.
- package/LICENSE +1 -1
- package/README.md +103 -1605
- package/dist/architecture/layers/dart.js +112 -0
- package/dist/architecture/layers/dart.js.map +7 -0
- package/dist/architecture/layers/index.js +3 -1
- package/dist/architecture/layers/index.js.map +2 -2
- package/dist/bin/banner.js +74 -6
- package/dist/bin/banner.js.map +2 -2
- package/dist/bin/commands/affected.js +40 -5
- package/dist/bin/commands/affected.js.map +2 -2
- package/dist/bin/commands/attack-surface.js +157 -0
- package/dist/bin/commands/attack-surface.js.map +7 -0
- package/dist/bin/commands/benchmark.js +206 -0
- package/dist/bin/commands/benchmark.js.map +7 -0
- package/dist/bin/commands/budget.js +70 -0
- package/dist/bin/commands/budget.js.map +7 -0
- package/dist/bin/commands/communities.js +77 -0
- package/dist/bin/commands/communities.js.map +7 -0
- package/dist/bin/commands/data.js +665 -0
- package/dist/bin/commands/data.js.map +7 -0
- package/dist/bin/commands/dep-confusion.js +119 -0
- package/dist/bin/commands/dep-confusion.js.map +7 -0
- package/dist/bin/commands/export.js +304 -10
- package/dist/bin/commands/export.js.map +2 -2
- package/dist/bin/commands/flows.js +97 -0
- package/dist/bin/commands/flows.js.map +7 -0
- package/dist/bin/commands/help.js +386 -100
- package/dist/bin/commands/help.js.map +2 -2
- package/dist/bin/commands/install.js +90 -8
- package/dist/bin/commands/install.js.map +2 -2
- package/dist/bin/commands/licenses.js +201 -0
- package/dist/bin/commands/licenses.js.map +7 -0
- package/dist/bin/commands/reachability.js +160 -0
- package/dist/bin/commands/reachability.js.map +7 -0
- package/dist/bin/commands/read.js +100 -0
- package/dist/bin/commands/read.js.map +7 -0
- package/dist/bin/commands/refactor.js +116 -0
- package/dist/bin/commands/refactor.js.map +7 -0
- package/dist/bin/commands/remediation.js +133 -0
- package/dist/bin/commands/remediation.js.map +7 -0
- package/dist/bin/commands/sbom.js +76 -0
- package/dist/bin/commands/sbom.js.map +7 -0
- package/dist/bin/commands/security-ci-report.js +267 -0
- package/dist/bin/commands/security-ci-report.js.map +7 -0
- package/dist/bin/commands/security-export.js +1349 -0
- package/dist/bin/commands/security-export.js.map +7 -0
- package/dist/bin/commands/security-flows.js +126 -0
- package/dist/bin/commands/security-flows.js.map +7 -0
- package/dist/bin/commands/security-secrets.js +170 -0
- package/dist/bin/commands/security-secrets.js.map +7 -0
- package/dist/bin/commands/security.js +193 -0
- package/dist/bin/commands/security.js.map +7 -0
- package/dist/bin/commands/staleness.js +144 -0
- package/dist/bin/commands/staleness.js.map +7 -0
- package/dist/bin/commands/status.js +102 -2
- package/dist/bin/commands/status.js.map +2 -2
- package/dist/bin/commands/supply-chain.js +138 -0
- package/dist/bin/commands/supply-chain.js.map +7 -0
- package/dist/bin/commands/uninit.js +109 -60
- package/dist/bin/commands/uninit.js.map +2 -2
- package/dist/bin/commands/vex.js +76 -0
- package/dist/bin/commands/vex.js.map +7 -0
- package/dist/bin/commands/vuln-suppress.js +95 -0
- package/dist/bin/commands/vuln-suppress.js.map +7 -0
- package/dist/bin/commands/vulns.js +382 -0
- package/dist/bin/commands/vulns.js.map +7 -0
- package/dist/bin/installer/auto-detect.js +125 -0
- package/dist/bin/installer/auto-detect.js.map +7 -0
- package/dist/bin/installer/cli-agent.js +28 -5
- package/dist/bin/installer/cli-agent.js.map +2 -2
- package/dist/bin/installer/common.js +47 -0
- package/dist/bin/installer/common.js.map +2 -2
- package/dist/bin/installer/config-prompt.js +39 -1
- package/dist/bin/installer/config-prompt.js.map +2 -2
- package/dist/bin/installer/detect.js +309 -0
- package/dist/bin/installer/detect.js.map +7 -0
- package/dist/bin/installer/index.js +36 -1
- package/dist/bin/installer/index.js.map +2 -2
- package/dist/bin/installer/instructions.js +195 -3
- package/dist/bin/installer/instructions.js.map +2 -2
- package/dist/bin/installer/steering.js +440 -0
- package/dist/bin/installer/steering.js.map +2 -2
- package/dist/bin/installer/targets/aider.js +3 -3
- package/dist/bin/installer/targets/aider.js.map +2 -2
- package/dist/bin/installer/targets/amp.js +3 -3
- package/dist/bin/installer/targets/amp.js.map +2 -2
- package/dist/bin/installer/targets/antigravity.js +35 -6
- package/dist/bin/installer/targets/antigravity.js.map +2 -2
- package/dist/bin/installer/targets/augment.js +3 -3
- package/dist/bin/installer/targets/augment.js.map +2 -2
- package/dist/bin/installer/targets/claude.js +49 -5
- package/dist/bin/installer/targets/claude.js.map +2 -2
- package/dist/bin/installer/targets/cline.js +32 -6
- package/dist/bin/installer/targets/cline.js.map +2 -2
- package/dist/bin/installer/targets/codex.js +64 -12
- package/dist/bin/installer/targets/codex.js.map +2 -2
- package/dist/bin/installer/targets/continue.js +2 -2
- package/dist/bin/installer/targets/continue.js.map +2 -2
- package/dist/bin/installer/targets/copilot-cli.js +101 -0
- package/dist/bin/installer/targets/copilot-cli.js.map +7 -0
- package/dist/bin/installer/targets/copilot.js +31 -5
- package/dist/bin/installer/targets/copilot.js.map +2 -2
- package/dist/bin/installer/targets/cursor.js +4 -4
- package/dist/bin/installer/targets/cursor.js.map +2 -2
- package/dist/bin/installer/targets/devin.js +2 -2
- package/dist/bin/installer/targets/devin.js.map +2 -2
- package/dist/bin/installer/targets/gemini-cli.js +2 -2
- package/dist/bin/installer/targets/gemini-cli.js.map +2 -2
- package/dist/bin/installer/targets/generic.js +2 -14
- package/dist/bin/installer/targets/generic.js.map +2 -2
- package/dist/bin/installer/targets/goose.js +3 -3
- package/dist/bin/installer/targets/goose.js.map +2 -2
- package/dist/bin/installer/targets/index.js +250 -0
- package/dist/bin/installer/targets/index.js.map +2 -2
- package/dist/bin/installer/targets/junie.js +2 -2
- package/dist/bin/installer/targets/junie.js.map +2 -2
- package/dist/bin/installer/targets/kilo.js +2 -2
- package/dist/bin/installer/targets/kilo.js.map +2 -2
- package/dist/bin/installer/targets/kiro.js +3 -3
- package/dist/bin/installer/targets/kiro.js.map +2 -2
- package/dist/bin/installer/targets/opencode.js +2 -2
- package/dist/bin/installer/targets/opencode.js.map +2 -2
- package/dist/bin/installer/targets/openhands.js +3 -3
- package/dist/bin/installer/targets/openhands.js.map +2 -2
- package/dist/bin/installer/targets/qoder.js +91 -0
- package/dist/bin/installer/targets/qoder.js.map +7 -0
- package/dist/bin/installer/targets/qwen.js +96 -0
- package/dist/bin/installer/targets/qwen.js.map +7 -0
- package/dist/bin/installer/targets/replit.js +3 -3
- package/dist/bin/installer/targets/replit.js.map +2 -2
- package/dist/bin/installer/targets/roo.js +2 -2
- package/dist/bin/installer/targets/roo.js.map +2 -2
- package/dist/bin/installer/targets/tabnine.js +3 -3
- package/dist/bin/installer/targets/tabnine.js.map +2 -2
- package/dist/bin/installer/targets/trae.js +5 -5
- package/dist/bin/installer/targets/trae.js.map +2 -2
- package/dist/bin/installer/targets/warp.js +2 -2
- package/dist/bin/installer/targets/warp.js.map +2 -2
- package/dist/bin/installer/targets/windsurf.js +35 -6
- package/dist/bin/installer/targets/windsurf.js.map +2 -2
- package/dist/bin/kirograph.js +74 -6
- package/dist/bin/kirograph.js.map +3 -3
- package/dist/bin/progress.js +30 -0
- package/dist/bin/progress.js.map +2 -2
- package/dist/compression/naive-cost.js +25 -0
- package/dist/compression/naive-cost.js.map +2 -2
- package/dist/compression/tracker.js +91 -2
- package/dist/compression/tracker.js.map +2 -2
- package/dist/compression/types.js.map +1 -1
- package/dist/config.js +126 -3
- package/dist/config.js.map +2 -2
- package/dist/core/pipeline.js +92 -4
- package/dist/core/pipeline.js.map +2 -2
- package/dist/data/filters.js +105 -0
- package/dist/data/filters.js.map +7 -0
- package/dist/data/indexer.js +225 -0
- package/dist/data/indexer.js.map +7 -0
- package/dist/data/linker.js +150 -0
- package/dist/data/linker.js.map +7 -0
- package/dist/data/lint.js +109 -0
- package/dist/data/lint.js.map +7 -0
- package/dist/data/parsers/csv.js +132 -0
- package/dist/data/parsers/csv.js.map +7 -0
- package/dist/data/parsers/excel.js +88 -0
- package/dist/data/parsers/excel.js.map +7 -0
- package/dist/data/parsers/index.js +79 -0
- package/dist/data/parsers/index.js.map +7 -0
- package/dist/data/parsers/json-array.js +89 -0
- package/dist/data/parsers/json-array.js.map +7 -0
- package/dist/data/parsers/jsonl.js +84 -0
- package/dist/data/parsers/jsonl.js.map +7 -0
- package/dist/data/parsers/parquet.js +95 -0
- package/dist/data/parsers/parquet.js.map +7 -0
- package/dist/data/profiler.js +182 -0
- package/dist/data/profiler.js.map +7 -0
- package/dist/data/queries.js +512 -0
- package/dist/data/queries.js.map +7 -0
- package/dist/data/types.js +17 -0
- package/dist/data/types.js.map +7 -0
- package/dist/db/data-schema.sql +61 -0
- package/dist/db/database.js +67 -5
- package/dist/db/database.js.map +2 -2
- package/dist/db/memory-schema.sql +5 -1
- package/dist/db/schema.sql +3 -1
- package/dist/db/security-schema.sql +72 -0
- package/dist/extraction/extractor.js +81 -0
- package/dist/extraction/extractor.js.map +2 -2
- package/dist/extraction/grammars.js +32 -8
- package/dist/extraction/grammars.js.map +2 -2
- package/dist/extraction/languages.js +43 -1
- package/dist/extraction/languages.js.map +2 -2
- package/dist/extraction/notebook.js +300 -0
- package/dist/extraction/notebook.js.map +7 -0
- package/dist/extraction/wasm/tree-sitter-astro.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-gdscript.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-julia.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-nix.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-perl.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-powershell.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-r.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-sql.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-verilog.wasm +0 -0
- package/dist/frameworks/flutter.js +130 -0
- package/dist/frameworks/flutter.js.map +7 -0
- package/dist/frameworks/index.js +7 -1
- package/dist/frameworks/index.js.map +3 -3
- package/dist/graph/communities.js +317 -0
- package/dist/graph/communities.js.map +7 -0
- package/dist/graph/flows.js +138 -0
- package/dist/graph/flows.js.map +7 -0
- package/dist/graph/refactor.js +155 -0
- package/dist/graph/refactor.js.map +7 -0
- package/dist/index.js +1 -1
- package/dist/index.js.map +2 -2
- package/dist/mcp/cache.js +172 -0
- package/dist/mcp/cache.js.map +7 -0
- package/dist/mcp/read-modes.js +295 -0
- package/dist/mcp/read-modes.js.map +7 -0
- package/dist/mcp/server.js +2 -1
- package/dist/mcp/server.js.map +2 -2
- package/dist/mcp/tool-names.js +29 -1
- package/dist/mcp/tool-names.js.map +2 -2
- package/dist/mcp/tools.js +1588 -6
- package/dist/mcp/tools.js.map +3 -3
- package/dist/memory/database.js +32 -2
- package/dist/memory/database.js.map +2 -2
- package/dist/memory/types.js.map +1 -1
- package/dist/resolution/bridges/android-rn.js +298 -0
- package/dist/resolution/bridges/android-rn.js.map +7 -0
- package/dist/resolution/bridges/expo-modules.js +155 -0
- package/dist/resolution/bridges/expo-modules.js.map +7 -0
- package/dist/resolution/bridges/flutter-channel.js +385 -0
- package/dist/resolution/bridges/flutter-channel.js.map +7 -0
- package/dist/resolution/bridges/index.js +80 -0
- package/dist/resolution/bridges/index.js.map +7 -0
- package/dist/resolution/bridges/native-events.js +188 -0
- package/dist/resolution/bridges/native-events.js.map +7 -0
- package/dist/resolution/bridges/native-views.js +244 -0
- package/dist/resolution/bridges/native-views.js.map +7 -0
- package/dist/resolution/bridges/react-native.js +161 -0
- package/dist/resolution/bridges/react-native.js.map +7 -0
- package/dist/resolution/bridges/swift-objc.js +227 -0
- package/dist/resolution/bridges/swift-objc.js.map +7 -0
- package/dist/resolution/bridges/turbomodules.js +216 -0
- package/dist/resolution/bridges/turbomodules.js.map +7 -0
- package/dist/resolution/index.js +61 -2
- package/dist/resolution/index.js.map +2 -2
- package/dist/security/attack-surface.js +164 -0
- package/dist/security/attack-surface.js.map +7 -0
- package/dist/security/context-warnings.js +123 -0
- package/dist/security/context-warnings.js.map +7 -0
- package/dist/security/context-warnings.test.js +300 -0
- package/dist/security/context-warnings.test.js.map +7 -0
- package/dist/security/data-flows.js +228 -0
- package/dist/security/data-flows.js.map +7 -0
- package/dist/security/dep-confusion.js +255 -0
- package/dist/security/dep-confusion.js.map +7 -0
- package/dist/security/errors.js +51 -0
- package/dist/security/errors.js.map +7 -0
- package/dist/security/export/fix-suggestions.js +58 -0
- package/dist/security/export/fix-suggestions.js.map +7 -0
- package/dist/security/export/fix-suggestions.test.js +74 -0
- package/dist/security/export/fix-suggestions.test.js.map +7 -0
- package/dist/security/export/sbom.js +230 -0
- package/dist/security/export/sbom.js.map +7 -0
- package/dist/security/export/sbom.test.js +288 -0
- package/dist/security/export/sbom.test.js.map +7 -0
- package/dist/security/export/serialization.js +142 -0
- package/dist/security/export/serialization.js.map +7 -0
- package/dist/security/export/serialization.test.js +310 -0
- package/dist/security/export/serialization.test.js.map +7 -0
- package/dist/security/export/vex.js +220 -0
- package/dist/security/export/vex.js.map +7 -0
- package/dist/security/export/vex.test.js +278 -0
- package/dist/security/export/vex.test.js.map +7 -0
- package/dist/security/index.js +67 -0
- package/dist/security/index.js.map +7 -0
- package/dist/security/integrator-transitives.test.js +359 -0
- package/dist/security/integrator-transitives.test.js.map +7 -0
- package/dist/security/integrator.js +491 -0
- package/dist/security/integrator.js.map +7 -0
- package/dist/security/integrator.test.js +237 -0
- package/dist/security/integrator.test.js.map +7 -0
- package/dist/security/license.js +70 -0
- package/dist/security/license.js.map +7 -0
- package/dist/security/manifest/adapter.js +243 -0
- package/dist/security/manifest/adapter.js.map +7 -0
- package/dist/security/manifest/adapter.test.js +272 -0
- package/dist/security/manifest/adapter.test.js.map +7 -0
- package/dist/security/manifest/parser.js +376 -0
- package/dist/security/manifest/parser.js.map +7 -0
- package/dist/security/manifest/parser.test.js +110 -0
- package/dist/security/manifest/parser.test.js.map +7 -0
- package/dist/security/manifest/plugins/cargo.js +233 -0
- package/dist/security/manifest/plugins/cargo.js.map +7 -0
- package/dist/security/manifest/plugins/cargo.test.js +304 -0
- package/dist/security/manifest/plugins/cargo.test.js.map +7 -0
- package/dist/security/manifest/plugins/composer.js +152 -0
- package/dist/security/manifest/plugins/composer.js.map +7 -0
- package/dist/security/manifest/plugins/go.js +146 -0
- package/dist/security/manifest/plugins/go.js.map +7 -0
- package/dist/security/manifest/plugins/go.test.js +341 -0
- package/dist/security/manifest/plugins/go.test.js.map +7 -0
- package/dist/security/manifest/plugins/gradle.js +159 -0
- package/dist/security/manifest/plugins/gradle.js.map +7 -0
- package/dist/security/manifest/plugins/hex.js +159 -0
- package/dist/security/manifest/plugins/hex.js.map +7 -0
- package/dist/security/manifest/plugins/maven.js +128 -0
- package/dist/security/manifest/plugins/maven.js.map +7 -0
- package/dist/security/manifest/plugins/maven.test.js +389 -0
- package/dist/security/manifest/plugins/maven.test.js.map +7 -0
- package/dist/security/manifest/plugins/npm.js +284 -0
- package/dist/security/manifest/plugins/npm.js.map +7 -0
- package/dist/security/manifest/plugins/npm.test.js +298 -0
- package/dist/security/manifest/plugins/npm.test.js.map +7 -0
- package/dist/security/manifest/plugins/nuget.js +186 -0
- package/dist/security/manifest/plugins/nuget.js.map +7 -0
- package/dist/security/manifest/plugins/pip.js +123 -0
- package/dist/security/manifest/plugins/pip.js.map +7 -0
- package/dist/security/manifest/plugins/pip.test.js +208 -0
- package/dist/security/manifest/plugins/pip.test.js.map +7 -0
- package/dist/security/manifest/plugins/pubspec.js +212 -0
- package/dist/security/manifest/plugins/pubspec.js.map +7 -0
- package/dist/security/manifest/plugins/pyproject.js +261 -0
- package/dist/security/manifest/plugins/pyproject.js.map +7 -0
- package/dist/security/manifest/plugins/rubygems.js +185 -0
- package/dist/security/manifest/plugins/rubygems.js.map +7 -0
- package/dist/security/manifest/plugins/swift.js +200 -0
- package/dist/security/manifest/plugins/swift.js.map +7 -0
- package/dist/security/owasp.js +104 -0
- package/dist/security/owasp.js.map +7 -0
- package/dist/security/pipeline.js +125 -0
- package/dist/security/pipeline.js.map +7 -0
- package/dist/security/reachability.js +276 -0
- package/dist/security/reachability.js.map +7 -0
- package/dist/security/reachability.test.js +394 -0
- package/dist/security/reachability.test.js.map +7 -0
- package/dist/security/remediation.js +126 -0
- package/dist/security/remediation.js.map +7 -0
- package/dist/security/secrets.js +176 -0
- package/dist/security/secrets.js.map +7 -0
- package/dist/security/staleness.js +231 -0
- package/dist/security/staleness.js.map +7 -0
- package/dist/security/supply-chain.js +301 -0
- package/dist/security/supply-chain.js.map +7 -0
- package/dist/security/suppressions.js +106 -0
- package/dist/security/suppressions.js.map +7 -0
- package/dist/security/types.js +17 -0
- package/dist/security/types.js.map +7 -0
- package/dist/security/vuln/client.js +355 -0
- package/dist/security/vuln/client.js.map +7 -0
- package/dist/security/vuln/client.test.js +288 -0
- package/dist/security/vuln/client.test.js.map +7 -0
- package/dist/security/vuln/epss-client.js +95 -0
- package/dist/security/vuln/epss-client.js.map +7 -0
- package/dist/security/vuln/index.js +29 -0
- package/dist/security/vuln/index.js.map +7 -0
- package/dist/security/vuln/osv-adapter.js +321 -0
- package/dist/security/vuln/osv-adapter.js.map +7 -0
- package/dist/security/vuln/osv-adapter.test.js +391 -0
- package/dist/security/vuln/osv-adapter.test.js.map +7 -0
- package/dist/security/vuln/types.js +17 -0
- package/dist/security/vuln/types.js.map +7 -0
- package/dist/types.js.map +2 -2
- package/package.json +5 -2
package/dist/mcp/tools.js
CHANGED
|
@@ -46,6 +46,18 @@ const MAX_OUTPUT = 15e3;
|
|
|
46
46
|
function truncate(s) {
|
|
47
47
|
return s.length > MAX_OUTPUT ? s.slice(0, MAX_OUTPUT) + "\n\u2026[truncated]" : s;
|
|
48
48
|
}
|
|
49
|
+
function estimateFileTokens(projectRoot, filePaths) {
|
|
50
|
+
let total = 0;
|
|
51
|
+
for (const fp of filePaths) {
|
|
52
|
+
try {
|
|
53
|
+
const fullPath = path.isAbsolute(fp) ? fp : path.join(projectRoot, fp);
|
|
54
|
+
const stat = fs.statSync(fullPath);
|
|
55
|
+
total += Math.round(stat.size / 4);
|
|
56
|
+
} catch {
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return total;
|
|
60
|
+
}
|
|
49
61
|
function clampLimit(value, defaultValue) {
|
|
50
62
|
const n = typeof value === "number" ? value : defaultValue;
|
|
51
63
|
return Math.max(1, Math.min(100, Math.round(n)));
|
|
@@ -357,6 +369,77 @@ const tools = [
|
|
|
357
369
|
}
|
|
358
370
|
}
|
|
359
371
|
},
|
|
372
|
+
{
|
|
373
|
+
name: "kirograph_read",
|
|
374
|
+
description: 'Read a file with caching and multiple modes. First read returns full content; subsequent reads of unchanged files return a compact "cached" marker (~13 tokens). Supports modes: full, map, signatures, diff, lines, imports, exports.',
|
|
375
|
+
inputSchema: {
|
|
376
|
+
type: "object",
|
|
377
|
+
properties: {
|
|
378
|
+
path: { type: "string", description: "File path to read (absolute or relative to project root)" },
|
|
379
|
+
mode: {
|
|
380
|
+
type: "string",
|
|
381
|
+
description: 'Read mode: "full" (default), "map" (structure overview), "signatures" (function signatures), "diff" (changes since last read), "lines" (line range), "imports", "exports"',
|
|
382
|
+
enum: ["full", "map", "signatures", "diff", "lines", "imports", "exports"],
|
|
383
|
+
default: "full"
|
|
384
|
+
},
|
|
385
|
+
start: { type: "number", description: "Start line (for lines mode)" },
|
|
386
|
+
end: { type: "number", description: "End line (for lines mode)" },
|
|
387
|
+
noCache: { type: "boolean", description: "Force fresh read, bypass cache (default: false)", default: false },
|
|
388
|
+
projectPath: { type: "string", description: "Project root path (optional)" }
|
|
389
|
+
},
|
|
390
|
+
required: ["path"]
|
|
391
|
+
}
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
name: "kirograph_budget",
|
|
395
|
+
description: "Show current session context budget usage. Returns tokens consumed, remaining budget, and utilization percentage.",
|
|
396
|
+
inputSchema: {
|
|
397
|
+
type: "object",
|
|
398
|
+
properties: {
|
|
399
|
+
reset: { type: "boolean", description: "Reset session budget counters (default: false)", default: false },
|
|
400
|
+
projectPath: { type: "string", description: "Project root path (optional)" }
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
},
|
|
404
|
+
{
|
|
405
|
+
name: "kirograph_flows",
|
|
406
|
+
description: "Trace execution flows from entry points (routes, handlers, main functions) through the call graph. Returns ordered call chains sorted by criticality.",
|
|
407
|
+
inputSchema: {
|
|
408
|
+
type: "object",
|
|
409
|
+
properties: {
|
|
410
|
+
entryPoint: { type: "string", description: "Symbol name to trace from, or omit to auto-detect entry points" },
|
|
411
|
+
maxFlows: { type: "number", description: "Max number of flows to return (default 10)" },
|
|
412
|
+
maxDepth: { type: "number", description: "Max call chain depth (default 10)" },
|
|
413
|
+
projectPath: { type: "string", description: "Project root path (optional)" }
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
},
|
|
417
|
+
{
|
|
418
|
+
name: "kirograph_communities",
|
|
419
|
+
description: "Detect code communities (clusters of related symbols) using graph-based community detection. Shows which code belongs together and how communities are coupled.",
|
|
420
|
+
inputSchema: {
|
|
421
|
+
type: "object",
|
|
422
|
+
properties: {
|
|
423
|
+
resolution: { type: "number", description: "Resolution parameter (default 1.0, higher = more communities)" },
|
|
424
|
+
limit: { type: "number", description: "Max communities to return (default 15)" },
|
|
425
|
+
projectPath: { type: "string", description: "Project root path (optional)" }
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
},
|
|
429
|
+
{
|
|
430
|
+
name: "kirograph_refactor",
|
|
431
|
+
description: 'Refactoring assistant. Use mode "rename" to preview all locations that reference a symbol (rename preview). Use mode "suggest" for community-driven refactoring suggestions.',
|
|
432
|
+
inputSchema: {
|
|
433
|
+
type: "object",
|
|
434
|
+
properties: {
|
|
435
|
+
mode: { type: "string", description: 'Mode: "rename" (preview references) or "suggest" (refactoring suggestions)', enum: ["rename", "suggest"] },
|
|
436
|
+
symbol: { type: "string", description: "Symbol name (required for rename mode)" },
|
|
437
|
+
limit: { type: "number", description: "Max results (default 10)" },
|
|
438
|
+
projectPath: { type: "string", description: "Project root path (optional)" }
|
|
439
|
+
},
|
|
440
|
+
required: ["mode"]
|
|
441
|
+
}
|
|
442
|
+
},
|
|
360
443
|
// ── Memory tools (require enableMemory=true) ────────────────────────────────
|
|
361
444
|
{
|
|
362
445
|
name: "kirograph_mem_search",
|
|
@@ -372,6 +455,7 @@ const tools = [
|
|
|
372
455
|
},
|
|
373
456
|
limit: { type: "number", description: "Max results (default: 10)", default: 10 },
|
|
374
457
|
sessionId: { type: "string", description: "Filter to specific session" },
|
|
458
|
+
asOf: { type: "number", description: "Query facts valid at this timestamp (epoch ms). Filters out expired/superseded observations." },
|
|
375
459
|
projectPath: { type: "string", description: "Project root path (optional)" }
|
|
376
460
|
},
|
|
377
461
|
required: ["query"]
|
|
@@ -480,11 +564,297 @@ const tools = [
|
|
|
480
564
|
projectPath: { type: "string", description: "Project root path (optional)" }
|
|
481
565
|
}
|
|
482
566
|
}
|
|
567
|
+
},
|
|
568
|
+
// ── Data tools (require enableData=true) ────────────────────────────────────
|
|
569
|
+
{
|
|
570
|
+
name: "kirograph_data_list",
|
|
571
|
+
description: "List all indexed datasets with row counts, column counts, and file sizes.",
|
|
572
|
+
inputSchema: { type: "object", properties: { projectPath: { type: "string", description: "Project root path (optional)" } } }
|
|
573
|
+
},
|
|
574
|
+
{
|
|
575
|
+
name: "kirograph_data_describe",
|
|
576
|
+
description: "Full schema profile of a dataset: column names, types, cardinality, null%, sample values. Use to orient on a dataset without reading any rows.",
|
|
577
|
+
inputSchema: {
|
|
578
|
+
type: "object",
|
|
579
|
+
properties: {
|
|
580
|
+
dataset: { type: "string", description: "Dataset ID (from kirograph_data_list)" },
|
|
581
|
+
column: { type: "string", description: "Optional: deep-dive on a single column" },
|
|
582
|
+
projectPath: { type: "string", description: "Project root path (optional)" }
|
|
583
|
+
},
|
|
584
|
+
required: ["dataset"]
|
|
585
|
+
}
|
|
586
|
+
},
|
|
587
|
+
{
|
|
588
|
+
name: "kirograph_data_query",
|
|
589
|
+
description: "Filtered row retrieval with structured operators. Returns only matching rows (max 500). Use instead of reading raw data files.",
|
|
590
|
+
inputSchema: {
|
|
591
|
+
type: "object",
|
|
592
|
+
properties: {
|
|
593
|
+
dataset: { type: "string", description: "Dataset ID" },
|
|
594
|
+
filters: { type: "array", description: "Array of {column, op, value} filters. Ops: eq, neq, gt, gte, lt, lte, contains, in, is_null, between" },
|
|
595
|
+
columns: { type: "array", description: "Column projection (only return these columns)" },
|
|
596
|
+
limit: { type: "number", description: "Max rows (default: 100, hard cap: 500)", default: 100 },
|
|
597
|
+
offset: { type: "number", description: "Pagination offset", default: 0 },
|
|
598
|
+
projectPath: { type: "string", description: "Project root path (optional)" }
|
|
599
|
+
},
|
|
600
|
+
required: ["dataset"]
|
|
601
|
+
}
|
|
602
|
+
},
|
|
603
|
+
{
|
|
604
|
+
name: "kirograph_data_aggregate",
|
|
605
|
+
description: "Server-side GROUP BY aggregation. Computation runs in SQLite \u2014 only the result set enters context. Use for count, sum, avg, min, max questions.",
|
|
606
|
+
inputSchema: {
|
|
607
|
+
type: "object",
|
|
608
|
+
properties: {
|
|
609
|
+
dataset: { type: "string", description: "Dataset ID" },
|
|
610
|
+
groupBy: { type: "array", description: "Columns to group by" },
|
|
611
|
+
metrics: { type: "array", description: "Array of {column, op} metrics. Ops: count, sum, avg, min, max, count_distinct" },
|
|
612
|
+
filters: { type: "array", description: "Optional pre-filters (same format as kirograph_data_query)" },
|
|
613
|
+
projectPath: { type: "string", description: "Project root path (optional)" }
|
|
614
|
+
},
|
|
615
|
+
required: ["dataset", "groupBy", "metrics"]
|
|
616
|
+
}
|
|
617
|
+
},
|
|
618
|
+
{
|
|
619
|
+
name: "kirograph_data_search",
|
|
620
|
+
description: "Search column names and sample values by keyword. Tells you which column holds the answer without loading data.",
|
|
621
|
+
inputSchema: {
|
|
622
|
+
type: "object",
|
|
623
|
+
properties: {
|
|
624
|
+
dataset: { type: "string", description: "Dataset ID" },
|
|
625
|
+
query: { type: "string", description: "Search keyword" },
|
|
626
|
+
projectPath: { type: "string", description: "Project root path (optional)" }
|
|
627
|
+
},
|
|
628
|
+
required: ["dataset", "query"]
|
|
629
|
+
}
|
|
630
|
+
},
|
|
631
|
+
{
|
|
632
|
+
name: "kirograph_data_join",
|
|
633
|
+
description: "SQL JOIN across two indexed datasets. Combines data without loading either file into context.",
|
|
634
|
+
inputSchema: {
|
|
635
|
+
type: "object",
|
|
636
|
+
properties: {
|
|
637
|
+
left: { type: "string", description: "Left dataset ID" },
|
|
638
|
+
right: { type: "string", description: "Right dataset ID" },
|
|
639
|
+
leftColumn: { type: "string", description: "Join column from left dataset" },
|
|
640
|
+
rightColumn: { type: "string", description: "Join column from right dataset" },
|
|
641
|
+
type: { type: "string", description: "Join type: inner (default), left, right", enum: ["inner", "left", "right"], default: "inner" },
|
|
642
|
+
columns: { type: "array", description: "Column projection (prefix with dataset ID)" },
|
|
643
|
+
limit: { type: "number", description: "Max rows (default: 100, hard cap: 500)", default: 100 },
|
|
644
|
+
projectPath: { type: "string", description: "Project root path (optional)" }
|
|
645
|
+
},
|
|
646
|
+
required: ["left", "right", "leftColumn", "rightColumn"]
|
|
647
|
+
}
|
|
648
|
+
},
|
|
649
|
+
{
|
|
650
|
+
name: "kirograph_data_correlations",
|
|
651
|
+
description: "Pairwise Pearson correlations between numeric columns. Discovers hidden relationships without loading data.",
|
|
652
|
+
inputSchema: {
|
|
653
|
+
type: "object",
|
|
654
|
+
properties: {
|
|
655
|
+
dataset: { type: "string", description: "Dataset ID" },
|
|
656
|
+
threshold: { type: "number", description: "Min absolute correlation to include (default: 0.3)", default: 0.3 },
|
|
657
|
+
projectPath: { type: "string", description: "Project root path (optional)" }
|
|
658
|
+
},
|
|
659
|
+
required: ["dataset"]
|
|
660
|
+
}
|
|
661
|
+
},
|
|
662
|
+
{
|
|
663
|
+
name: "kirograph_data_quality",
|
|
664
|
+
description: "Data quality triage: rank columns by risk (null rate, cardinality anomalies, type issues). Identifies problematic columns without loading data.",
|
|
665
|
+
inputSchema: {
|
|
666
|
+
type: "object",
|
|
667
|
+
properties: {
|
|
668
|
+
dataset: { type: "string", description: "Dataset ID" },
|
|
669
|
+
projectPath: { type: "string", description: "Project root path (optional)" }
|
|
670
|
+
},
|
|
671
|
+
required: ["dataset"]
|
|
672
|
+
}
|
|
673
|
+
},
|
|
674
|
+
// ── Security tools (require enableSecurity=true) ────────────────────────────
|
|
675
|
+
{
|
|
676
|
+
name: "kirograph_security",
|
|
677
|
+
description: "Security overview: vulnerability counts, affected/not_affected verdicts, stale data warnings. Requires enableSecurity=true and enableArchitecture=true.",
|
|
678
|
+
inputSchema: {
|
|
679
|
+
type: "object",
|
|
680
|
+
properties: {
|
|
681
|
+
projectPath: { type: "string", description: "Project root path (optional)" }
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
},
|
|
685
|
+
{
|
|
686
|
+
name: "kirograph_vulns",
|
|
687
|
+
description: "List vulnerabilities with reachability verdicts, severity, and affected components. Supports on-demand refresh.",
|
|
688
|
+
inputSchema: {
|
|
689
|
+
type: "object",
|
|
690
|
+
properties: {
|
|
691
|
+
severity: { type: "string", description: "Filter by severity level", enum: ["critical", "high", "medium", "low"] },
|
|
692
|
+
verdict: { type: "string", description: "Filter by reachability verdict", enum: ["affected", "not_affected", "under_investigation"] },
|
|
693
|
+
limit: { type: "number", description: "Max results (default: 20)", default: 20 },
|
|
694
|
+
refresh: { type: "boolean", description: "Trigger fresh vulnerability enrichment before listing (default: false)", default: false },
|
|
695
|
+
projectPath: { type: "string", description: "Project root path (optional)" }
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
},
|
|
699
|
+
{
|
|
700
|
+
name: "kirograph_vuln_add",
|
|
701
|
+
description: "Manually register a CVE against a dependency without querying external databases.",
|
|
702
|
+
inputSchema: {
|
|
703
|
+
type: "object",
|
|
704
|
+
properties: {
|
|
705
|
+
cveId: { type: "string", description: 'CVE identifier (e.g., "CVE-2023-12345")' },
|
|
706
|
+
package: { type: "string", description: "Package name to associate the CVE with" },
|
|
707
|
+
severity: { type: "number", description: "CVSS v3.1 base score (optional)" },
|
|
708
|
+
summary: { type: "string", description: "Vulnerability summary (optional)" },
|
|
709
|
+
fixedVersion: { type: "string", description: "Version that fixes the vulnerability (optional)" },
|
|
710
|
+
projectPath: { type: "string", description: "Project root path (optional)" }
|
|
711
|
+
},
|
|
712
|
+
required: ["cveId", "package"]
|
|
713
|
+
}
|
|
714
|
+
},
|
|
715
|
+
{
|
|
716
|
+
name: "kirograph_vuln_suppress",
|
|
717
|
+
description: "Suppress a CVE so it no longer appears in vulnerability reports (mark as false positive or accepted risk). Suppressions are stored in .kirograph/security-suppressions.json.",
|
|
718
|
+
inputSchema: {
|
|
719
|
+
type: "object",
|
|
720
|
+
properties: {
|
|
721
|
+
cveId: { type: "string", description: 'CVE identifier to suppress (e.g., "CVE-2024-1234")' },
|
|
722
|
+
reason: { type: "string", description: "Reason for suppression (optional)" },
|
|
723
|
+
expires: { type: "string", description: 'Expiry date in ISO format after which the suppression is removed (e.g. "2026-12-31", optional)' },
|
|
724
|
+
projectPath: { type: "string", description: "Project root path (optional)" }
|
|
725
|
+
},
|
|
726
|
+
required: ["cveId"]
|
|
727
|
+
}
|
|
728
|
+
},
|
|
729
|
+
{
|
|
730
|
+
name: "kirograph_sbom",
|
|
731
|
+
description: "Generate and return CycloneDX 1.5 SBOM JSON for the project.",
|
|
732
|
+
inputSchema: {
|
|
733
|
+
type: "object",
|
|
734
|
+
properties: {
|
|
735
|
+
projectPath: { type: "string", description: "Project root path (optional)" }
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
},
|
|
739
|
+
{
|
|
740
|
+
name: "kirograph_vex",
|
|
741
|
+
description: "Generate and return CycloneDX 1.5 VEX JSON with reachability verdicts.",
|
|
742
|
+
inputSchema: {
|
|
743
|
+
type: "object",
|
|
744
|
+
properties: {
|
|
745
|
+
projectPath: { type: "string", description: "Project root path (optional)" }
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
},
|
|
749
|
+
{
|
|
750
|
+
name: "kirograph_reachability",
|
|
751
|
+
description: "Check reachability for a specific dependency or vulnerability. Returns verdict, paths, and impact summary.",
|
|
752
|
+
inputSchema: {
|
|
753
|
+
type: "object",
|
|
754
|
+
properties: {
|
|
755
|
+
target: { type: "string", description: "Dependency name or CVE ID to check reachability for" },
|
|
756
|
+
projectPath: { type: "string", description: "Project root path (optional)" }
|
|
757
|
+
},
|
|
758
|
+
required: ["target"]
|
|
759
|
+
}
|
|
760
|
+
},
|
|
761
|
+
{
|
|
762
|
+
name: "kirograph_staleness",
|
|
763
|
+
description: "Check dependency freshness \u2014 identifies packages significantly behind their latest published version.",
|
|
764
|
+
inputSchema: {
|
|
765
|
+
type: "object",
|
|
766
|
+
properties: {
|
|
767
|
+
threshold: { type: "number", description: "Only return packages with staleness_score >= threshold (default: 0.3)", default: 0.3 },
|
|
768
|
+
refresh: { type: "boolean", description: "Fetch latest version info from registries before listing (default: false)", default: false },
|
|
769
|
+
projectPath: { type: "string", description: "Project root path (optional)" }
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
},
|
|
773
|
+
{
|
|
774
|
+
name: "kirograph_licenses",
|
|
775
|
+
description: "Show dependency licenses and check against the configured license policy (deny/warn lists). Supports wildcard patterns (e.g. GPL-* matches GPL-2.0, GPL-3.0-only).",
|
|
776
|
+
inputSchema: {
|
|
777
|
+
type: "object",
|
|
778
|
+
properties: {
|
|
779
|
+
policy: { type: "boolean", description: "Return only policy violations (default: false)", default: false },
|
|
780
|
+
projectPath: { type: "string", description: "Project root path (optional)" }
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
},
|
|
784
|
+
{
|
|
785
|
+
name: "kirograph_attack_surface",
|
|
786
|
+
description: "Map the attack surface: all HTTP routes and their paths to vulnerable dependencies, with hop count, authentication status, and risk score.",
|
|
787
|
+
inputSchema: {
|
|
788
|
+
type: "object",
|
|
789
|
+
properties: {
|
|
790
|
+
limit: { type: "number", description: "Max routes to return (default: 20)", default: 20 },
|
|
791
|
+
publicOnly: { type: "boolean", description: "Only return public/unauthenticated routes", default: false },
|
|
792
|
+
projectPath: { type: "string", description: "Project root path (optional)" }
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
},
|
|
796
|
+
{
|
|
797
|
+
name: "kirograph_secrets",
|
|
798
|
+
description: "Scan for hardcoded secrets and credentials, enriched with call-graph blast radius showing which entry points reach the secret.",
|
|
799
|
+
inputSchema: {
|
|
800
|
+
type: "object",
|
|
801
|
+
properties: {
|
|
802
|
+
includeTests: { type: "boolean", description: "Include test files in scan", default: false },
|
|
803
|
+
severity: { type: "string", description: "Filter by severity: critical, high, medium, low", enum: ["critical", "high", "medium", "low"] },
|
|
804
|
+
projectPath: { type: "string", description: "Project root path (optional)" }
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
},
|
|
808
|
+
{
|
|
809
|
+
name: "kirograph_security_flows",
|
|
810
|
+
description: "SAST-lite: detect dangerous data flows (SQL injection, dangerous eval, unsafe deserialization, path traversal, weak crypto). Each finding tagged with OWASP Top 10 category.",
|
|
811
|
+
inputSchema: {
|
|
812
|
+
type: "object",
|
|
813
|
+
properties: {
|
|
814
|
+
type: { type: "string", description: "Filter by type: sql, eval, deserialize, path, crypto, all", default: "all" },
|
|
815
|
+
projectPath: { type: "string", description: "Project root path (optional)" }
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
},
|
|
819
|
+
{
|
|
820
|
+
name: "kirograph_supply_chain",
|
|
821
|
+
description: "Supply chain health: OpenSSF Scorecard scores, maintainer count, abandoned package detection for project dependencies.",
|
|
822
|
+
inputSchema: {
|
|
823
|
+
type: "object",
|
|
824
|
+
properties: {
|
|
825
|
+
threshold: { type: "string", description: "Minimum risk level: critical, high, medium", enum: ["critical", "high", "medium"] },
|
|
826
|
+
refresh: { type: "boolean", description: "Re-fetch from APIs", default: false },
|
|
827
|
+
projectPath: { type: "string", description: "Project root path (optional)" }
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
},
|
|
831
|
+
{
|
|
832
|
+
name: "kirograph_dep_confusion",
|
|
833
|
+
description: "Detect dependency confusion vulnerabilities: internal package names that exist in public registries (typosquatting/supply chain attack vectors).",
|
|
834
|
+
inputSchema: {
|
|
835
|
+
type: "object",
|
|
836
|
+
properties: {
|
|
837
|
+
projectPath: { type: "string", description: "Project root path (optional)" }
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
},
|
|
841
|
+
{
|
|
842
|
+
name: "kirograph_remediation",
|
|
843
|
+
description: "Remediation SLA tracking: which vulnerabilities are overdue for fixing based on severity thresholds (critical=7d, high=30d, medium=90d).",
|
|
844
|
+
inputSchema: {
|
|
845
|
+
type: "object",
|
|
846
|
+
properties: {
|
|
847
|
+
overdueOnly: { type: "boolean", description: "Show only overdue items", default: false },
|
|
848
|
+
projectPath: { type: "string", description: "Project root path (optional)" }
|
|
849
|
+
}
|
|
850
|
+
}
|
|
483
851
|
}
|
|
484
852
|
];
|
|
485
853
|
class ToolHandler {
|
|
486
854
|
constructor(cg) {
|
|
487
855
|
this.connections = /* @__PURE__ */ new Map();
|
|
856
|
+
/** Anti-loop: track recent data_query calls per dataset for pagination detection. */
|
|
857
|
+
this.queryTracker = /* @__PURE__ */ new Map();
|
|
488
858
|
this.defaultCg = cg;
|
|
489
859
|
}
|
|
490
860
|
setDefaultKiroGraph(cg) {
|
|
@@ -500,9 +870,38 @@ class ToolHandler {
|
|
|
500
870
|
}
|
|
501
871
|
this.connections.clear();
|
|
502
872
|
}
|
|
873
|
+
/** Anti-loop: detect pagination patterns and warn the agent. */
|
|
874
|
+
checkPaginationLoop(dataset, offset, response) {
|
|
875
|
+
const now = Date.now();
|
|
876
|
+
const key = dataset;
|
|
877
|
+
const currentOffset = offset ?? 0;
|
|
878
|
+
for (const [k, v] of this.queryTracker) {
|
|
879
|
+
if (now - v.lastCall > 6e4) this.queryTracker.delete(k);
|
|
880
|
+
}
|
|
881
|
+
const entry = this.queryTracker.get(key) ?? { offsets: [], lastCall: 0 };
|
|
882
|
+
entry.offsets.push(currentOffset);
|
|
883
|
+
entry.lastCall = now;
|
|
884
|
+
if (entry.offsets.length > 10) entry.offsets = entry.offsets.slice(-10);
|
|
885
|
+
this.queryTracker.set(key, entry);
|
|
886
|
+
if (entry.offsets.length > 5) {
|
|
887
|
+
const recent = entry.offsets.slice(-6);
|
|
888
|
+
let isIncrementing = true;
|
|
889
|
+
for (let i = 1; i < recent.length; i++) {
|
|
890
|
+
if (recent[i] <= recent[i - 1]) {
|
|
891
|
+
isIncrementing = false;
|
|
892
|
+
break;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
if (isIncrementing) {
|
|
896
|
+
return response + "\n\n\u26A0 Pagination detected. Consider using kirograph_data_aggregate for summary statistics instead of paginating through all rows.";
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
return response;
|
|
900
|
+
}
|
|
503
901
|
async getConnection(projectPath) {
|
|
504
902
|
if (!projectPath) return this.defaultCg;
|
|
505
903
|
const resolved = path.resolve(projectPath);
|
|
904
|
+
if (!path.isAbsolute(resolved)) return null;
|
|
506
905
|
if (this.connections.has(resolved)) return this.connections.get(resolved);
|
|
507
906
|
try {
|
|
508
907
|
const cg = await import__.default.open(resolved);
|
|
@@ -529,6 +928,8 @@ class ToolHandler {
|
|
|
529
928
|
tracker.recordMemorySaving(toolName, outputTokens, naiveCost);
|
|
530
929
|
} else if (toolName.startsWith("kirograph_docs_")) {
|
|
531
930
|
tracker.recordDocsSaving(toolName, outputTokens, naiveCost);
|
|
931
|
+
} else if (toolName.startsWith("kirograph_data_")) {
|
|
932
|
+
tracker.recordDataSaving(toolName, outputTokens, naiveCost);
|
|
532
933
|
} else {
|
|
533
934
|
tracker.recordGraphSaving(toolName, outputTokens, naiveCost);
|
|
534
935
|
}
|
|
@@ -606,6 +1007,9 @@ class ToolHandler {
|
|
|
606
1007
|
if (stats.bySource.docs.count > 0) {
|
|
607
1008
|
lines.push(` Docs tools: ${stats.bySource.docs.count} calls, ~${stats.bySource.docs.saved.toLocaleString()} tokens saved (vs reading full doc files)`);
|
|
608
1009
|
}
|
|
1010
|
+
if (stats.bySource.data.count > 0) {
|
|
1011
|
+
lines.push(` Data tools: ${stats.bySource.data.count} calls, ~${stats.bySource.data.saved.toLocaleString()} tokens saved (vs loading raw data files)`);
|
|
1012
|
+
}
|
|
609
1013
|
if (stats.bySource.exec.count > 0) {
|
|
610
1014
|
lines.push(` Compression: ${stats.bySource.exec.count} calls, ~${stats.bySource.exec.saved.toLocaleString()} tokens saved (vs raw output)`);
|
|
611
1015
|
}
|
|
@@ -628,6 +1032,69 @@ class ToolHandler {
|
|
|
628
1032
|
}
|
|
629
1033
|
return lines.join("\n");
|
|
630
1034
|
}
|
|
1035
|
+
if (toolName === "kirograph_read") {
|
|
1036
|
+
const filePath = args.path;
|
|
1037
|
+
if (!filePath) return "Error: path is required.";
|
|
1038
|
+
const projectRoot = args.projectPath || process.cwd();
|
|
1039
|
+
const resolvedPath = path.isAbsolute(filePath) ? filePath : path.join(projectRoot, filePath);
|
|
1040
|
+
const mode = args.mode ?? "full";
|
|
1041
|
+
const noCache = args.noCache ?? false;
|
|
1042
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
1043
|
+
return `Error: File not found: ${resolvedPath}`;
|
|
1044
|
+
}
|
|
1045
|
+
const { getFileReadCache } = await Promise.resolve().then(() => require("./cache.js"));
|
|
1046
|
+
const { executeReadMode } = await Promise.resolve().then(() => require("./read-modes.js"));
|
|
1047
|
+
const cache = getFileReadCache();
|
|
1048
|
+
if (mode !== "full") {
|
|
1049
|
+
let cg2 = null;
|
|
1050
|
+
try {
|
|
1051
|
+
cg2 = await this.getConnection(args.projectPath);
|
|
1052
|
+
} catch {
|
|
1053
|
+
}
|
|
1054
|
+
const result2 = executeReadMode({
|
|
1055
|
+
mode,
|
|
1056
|
+
filePath: resolvedPath,
|
|
1057
|
+
start: args.start,
|
|
1058
|
+
end: args.end,
|
|
1059
|
+
cg: cg2
|
|
1060
|
+
});
|
|
1061
|
+
cache.read(resolvedPath, true);
|
|
1062
|
+
return result2.content;
|
|
1063
|
+
}
|
|
1064
|
+
const result = cache.read(resolvedPath, noCache);
|
|
1065
|
+
if (result.cached) {
|
|
1066
|
+
return result.content;
|
|
1067
|
+
}
|
|
1068
|
+
if (result.changed) {
|
|
1069
|
+
return `[file changed since last read]
|
|
1070
|
+
|
|
1071
|
+
${result.content}`;
|
|
1072
|
+
}
|
|
1073
|
+
return result.content;
|
|
1074
|
+
}
|
|
1075
|
+
if (toolName === "kirograph_budget") {
|
|
1076
|
+
const projectRoot = args.projectPath || process.cwd();
|
|
1077
|
+
const reset = args.reset ?? false;
|
|
1078
|
+
const { BudgetTracker } = await Promise.resolve().then(() => require("../compression/tracker.js"));
|
|
1079
|
+
const budget = BudgetTracker.getInstance(projectRoot);
|
|
1080
|
+
if (reset) {
|
|
1081
|
+
budget.reset();
|
|
1082
|
+
return "Context budget counters reset.";
|
|
1083
|
+
}
|
|
1084
|
+
const status = budget.getStatus();
|
|
1085
|
+
const lines = [
|
|
1086
|
+
"Context Budget:",
|
|
1087
|
+
` Tokens consumed: ${status.consumed.toLocaleString()}`,
|
|
1088
|
+
` Budget limit: ${status.limit > 0 ? status.limit.toLocaleString() : "unlimited"}`,
|
|
1089
|
+
` Remaining: ${status.limit > 0 ? status.remaining.toLocaleString() : "\u221E"}`,
|
|
1090
|
+
` Utilization: ${status.utilization}%`
|
|
1091
|
+
];
|
|
1092
|
+
if (status.warning) {
|
|
1093
|
+
lines.push(`
|
|
1094
|
+
\u26A0 ${status.warning}`);
|
|
1095
|
+
}
|
|
1096
|
+
return lines.join("\n");
|
|
1097
|
+
}
|
|
631
1098
|
const cg = await this.getConnection(args.projectPath);
|
|
632
1099
|
if (!cg) return "KiroGraph not initialized. Run `kirograph init` in your project first.";
|
|
633
1100
|
writeSessionMarker(cg.getProjectRoot());
|
|
@@ -732,11 +1199,89 @@ class ToolHandler {
|
|
|
732
1199
|
}
|
|
733
1200
|
} catch {
|
|
734
1201
|
}
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
1202
|
+
try {
|
|
1203
|
+
const projectRoot3 = cg.getProjectRoot();
|
|
1204
|
+
const config3 = await (await Promise.resolve().then(() => require("../config.js"))).loadConfig(projectRoot3);
|
|
1205
|
+
if (config3.enableData && config3.dataContextLimit > 0) {
|
|
1206
|
+
const db3 = cg.getDatabase();
|
|
1207
|
+
db3.applyDataSchema();
|
|
1208
|
+
const entryFiles = ctx.entryPoints.map((n) => n.filePath).filter(Boolean);
|
|
1209
|
+
if (entryFiles.length > 0) {
|
|
1210
|
+
const placeholders = entryFiles.map(() => "?").join(", ");
|
|
1211
|
+
const dataRefs = db3.getRawDb().all(
|
|
1212
|
+
`SELECT DISTINCT d.id, d.file_path, d.row_count, d.column_count
|
|
1213
|
+
FROM data_code_refs r JOIN data_datasets d ON r.dataset_id = d.id
|
|
1214
|
+
WHERE r.qualified_name IN (${placeholders})`,
|
|
1215
|
+
entryFiles
|
|
1216
|
+
);
|
|
1217
|
+
if (dataRefs.length > 0) {
|
|
1218
|
+
const { DataQueries } = await Promise.resolve().then(() => require("../data/queries.js"));
|
|
1219
|
+
const dq = new DataQueries(db3.getRawDb());
|
|
1220
|
+
const limit = config3.dataContextLimit;
|
|
1221
|
+
lines.push("", "## Related Data");
|
|
1222
|
+
for (const ref of dataRefs.slice(0, limit)) {
|
|
1223
|
+
const info = dq.describeDataset(ref.id);
|
|
1224
|
+
if (info) {
|
|
1225
|
+
const colSummary = info.columns.map((c) => `${c.name}:${c.inferredType}`).join(", ");
|
|
1226
|
+
lines.push(`- **${ref.id}** (${ref.file_path}) \u2014 ${ref.row_count} rows, ${ref.column_count} cols`);
|
|
1227
|
+
lines.push(` Schema: ${colSummary}`);
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
} catch {
|
|
1234
|
+
}
|
|
1235
|
+
try {
|
|
1236
|
+
const projectRootSec = cg.getProjectRoot();
|
|
1237
|
+
const configSec = await (await Promise.resolve().then(() => require("../config.js"))).loadConfig(projectRootSec);
|
|
1238
|
+
if (configSec.enableSecurity) {
|
|
1239
|
+
const dbSec = cg.getDatabase();
|
|
1240
|
+
dbSec.applySecuritySchema();
|
|
1241
|
+
const rawDbSec = dbSec.getRawDb();
|
|
1242
|
+
const contextNodeIds = [
|
|
1243
|
+
...ctx.entryPoints.map((n) => n.id),
|
|
1244
|
+
...ctx.relatedNodes.map((n) => n.id)
|
|
1245
|
+
].filter(Boolean);
|
|
1246
|
+
if (contextNodeIds.length > 0) {
|
|
1247
|
+
const { getSecurityWarningsForNodes, formatSecurityWarnings } = await Promise.resolve().then(() => require("../security/context-warnings.js"));
|
|
1248
|
+
const warnings = getSecurityWarningsForNodes(rawDbSec, contextNodeIds);
|
|
1249
|
+
if (warnings.length > 0) {
|
|
1250
|
+
const nodeNames = /* @__PURE__ */ new Map();
|
|
1251
|
+
for (const n of ctx.entryPoints) {
|
|
1252
|
+
nodeNames.set(n.id, n.name);
|
|
1253
|
+
}
|
|
1254
|
+
for (const n of ctx.relatedNodes) {
|
|
1255
|
+
nodeNames.set(n.id, n.name);
|
|
1256
|
+
}
|
|
1257
|
+
const secSection = formatSecurityWarnings(warnings, nodeNames);
|
|
1258
|
+
if (secSection) {
|
|
1259
|
+
lines.push(secSection);
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
} catch {
|
|
1265
|
+
}
|
|
1266
|
+
const graphTokens = lines.join("\n").length / 4;
|
|
1267
|
+
const uniqueFiles = /* @__PURE__ */ new Set([
|
|
1268
|
+
...ctx.entryPoints.map((n) => n.filePath),
|
|
1269
|
+
...ctx.relatedNodes.map((n) => n.filePath)
|
|
1270
|
+
]);
|
|
1271
|
+
if (uniqueFiles.size > 0) {
|
|
1272
|
+
const naiveTokens = estimateFileTokens(cg.getProjectRoot(), [...uniqueFiles]);
|
|
1273
|
+
if (naiveTokens > 0) {
|
|
1274
|
+
const savingsPct = Math.round((1 - graphTokens / naiveTokens) * 100);
|
|
1275
|
+
if (savingsPct > 0) {
|
|
1276
|
+
lines.push("", `---`, `Context savings: ~${Math.round(graphTokens)} tokens (graph) vs ~${naiveTokens} tokens (full files) \u2014 ${savingsPct}% reduction`);
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
return lines.join("\n");
|
|
1281
|
+
}
|
|
1282
|
+
case "kirograph_callers": {
|
|
1283
|
+
const limit = clampLimit(args.limit, 20);
|
|
1284
|
+
const results = cg.searchNodes(args.symbol, void 0, 5);
|
|
740
1285
|
if (results.length === 0) return `Symbol "${args.symbol}" not found in index.`;
|
|
741
1286
|
const node = results[0].node;
|
|
742
1287
|
const callers = await cg.getCallers(node.id, limit);
|
|
@@ -841,6 +1386,43 @@ class ToolHandler {
|
|
|
841
1386
|
}
|
|
842
1387
|
} catch {
|
|
843
1388
|
}
|
|
1389
|
+
let dataLine = " Data: disabled";
|
|
1390
|
+
try {
|
|
1391
|
+
const { loadConfig: loadCfg2 } = await Promise.resolve().then(() => require("../config.js"));
|
|
1392
|
+
const cfg2 = await loadCfg2(cg.getProjectRoot());
|
|
1393
|
+
if (cfg2.enableData) {
|
|
1394
|
+
const rawDb2 = cg.getDatabase().getRawDb();
|
|
1395
|
+
cg.getDatabase().applyDataSchema();
|
|
1396
|
+
const datasetCount = rawDb2.get("SELECT COUNT(*) as cnt FROM data_datasets")?.cnt ?? 0;
|
|
1397
|
+
if (datasetCount > 0) {
|
|
1398
|
+
const totalRows = rawDb2.get("SELECT SUM(row_count) as total FROM data_datasets")?.total ?? 0;
|
|
1399
|
+
const totalCols = rawDb2.get("SELECT SUM(column_count) as total FROM data_datasets")?.total ?? 0;
|
|
1400
|
+
const totalSize = rawDb2.get("SELECT SUM(file_size) as total FROM data_datasets")?.total ?? 0;
|
|
1401
|
+
const sizeMb = (totalSize / 1024 / 1024).toFixed(2);
|
|
1402
|
+
dataLine = ` Data: enabled \u2014 ${datasetCount} datasets, ${totalRows.toLocaleString()} rows, ${totalCols} columns (${sizeMb} MB source)`;
|
|
1403
|
+
} else {
|
|
1404
|
+
dataLine = ` Data: enabled (no datasets indexed yet \u2014 run kirograph index)`;
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
} catch {
|
|
1408
|
+
}
|
|
1409
|
+
let securityLine = " Security: disabled";
|
|
1410
|
+
try {
|
|
1411
|
+
const { loadConfig: loadCfgSec } = await Promise.resolve().then(() => require("../config.js"));
|
|
1412
|
+
const cfgSec = await loadCfgSec(cg.getProjectRoot());
|
|
1413
|
+
if (cfgSec.enableSecurity) {
|
|
1414
|
+
const db = cg.getDatabase();
|
|
1415
|
+
db.applySecuritySchema();
|
|
1416
|
+
const rawDb = db.getRawDb();
|
|
1417
|
+
const depCount = rawDb.get("SELECT COUNT(*) as cnt FROM sec_dependencies")?.cnt ?? 0;
|
|
1418
|
+
const vulnCount = rawDb.get("SELECT COUNT(*) as cnt FROM sec_vulnerabilities")?.cnt ?? 0;
|
|
1419
|
+
const affectedCount = rawDb.get("SELECT COUNT(*) as cnt FROM sec_reachability WHERE verdict = 'affected'")?.cnt ?? 0;
|
|
1420
|
+
const staleCount = rawDb.get("SELECT COUNT(*) as cnt FROM sec_dependencies WHERE vuln_data_stale = 1")?.cnt ?? 0;
|
|
1421
|
+
const staleNote = staleCount > 0 ? ` \u26A0 ${staleCount} stale` : "";
|
|
1422
|
+
securityLine = ` Security: enabled \u2014 ${depCount} deps, ${vulnCount} vulns (${affectedCount} affected)${staleNote}`;
|
|
1423
|
+
}
|
|
1424
|
+
} catch {
|
|
1425
|
+
}
|
|
844
1426
|
const threshold = stats.syncWarningThreshold ?? 10;
|
|
845
1427
|
const pendingFiles = stats.pendingFiles ?? 0;
|
|
846
1428
|
const syncRunning = stats.syncRunning ?? false;
|
|
@@ -870,6 +1452,8 @@ class ToolHandler {
|
|
|
870
1452
|
frameworkLine,
|
|
871
1453
|
archLine,
|
|
872
1454
|
docsLine,
|
|
1455
|
+
dataLine,
|
|
1456
|
+
securityLine,
|
|
873
1457
|
` DB size: ${dbMb} MB`,
|
|
874
1458
|
...semanticLines,
|
|
875
1459
|
...syncLines,
|
|
@@ -1191,6 +1775,122 @@ class ToolHandler {
|
|
|
1191
1775
|
}
|
|
1192
1776
|
return lines.join("\n");
|
|
1193
1777
|
}
|
|
1778
|
+
case "kirograph_flows": {
|
|
1779
|
+
const { getExecutionFlows, traceFlow, detectEntryPoints } = await Promise.resolve().then(() => require("../graph/flows.js"));
|
|
1780
|
+
const db = cg.getDatabase();
|
|
1781
|
+
if (args.entryPoint) {
|
|
1782
|
+
const results = cg.searchNodes(args.entryPoint, void 0, 5);
|
|
1783
|
+
if (results.length === 0) return `Symbol "${args.entryPoint}" not found in index.`;
|
|
1784
|
+
const hops = traceFlow(db, results[0].node.id, args.maxDepth ?? 10);
|
|
1785
|
+
if (hops.length < 2) return `No outgoing call chain found from "${args.entryPoint}".`;
|
|
1786
|
+
const lines2 = [`## Execution flow from \`${args.entryPoint}\``, ""];
|
|
1787
|
+
for (let i = 0; i < hops.length; i++) {
|
|
1788
|
+
const hop = hops[i];
|
|
1789
|
+
const indent = " ".repeat(i);
|
|
1790
|
+
const arrow = i === 0 ? "\u2192" : "\u21B3";
|
|
1791
|
+
const conf = hop.confidence && hop.confidence !== "extracted" ? ` [${hop.confidence}]` : "";
|
|
1792
|
+
lines2.push(`${indent}${arrow} ${hop.kind} \`${hop.symbol}\` \u2014 ${hop.filePath}:${hop.line}${conf}`);
|
|
1793
|
+
}
|
|
1794
|
+
return lines2.join("\n");
|
|
1795
|
+
}
|
|
1796
|
+
const flows = getExecutionFlows(db, {
|
|
1797
|
+
maxFlows: args.maxFlows ?? 10,
|
|
1798
|
+
maxDepth: args.maxDepth ?? 10
|
|
1799
|
+
});
|
|
1800
|
+
if (flows.length === 0) return "No execution flows detected. The graph may be too small or have no call edges.";
|
|
1801
|
+
const lines = [`## Execution Flows (${flows.length} detected)`, ""];
|
|
1802
|
+
for (const flow of flows) {
|
|
1803
|
+
lines.push(`### \`${flow.entryPoint}\` (${flow.entryPointKind}) \u2014 criticality: ${flow.criticality.toFixed(2)}`);
|
|
1804
|
+
lines.push(`File: ${flow.entryPointFile}`, "");
|
|
1805
|
+
for (let i = 0; i < flow.hops.length; i++) {
|
|
1806
|
+
const hop = flow.hops[i];
|
|
1807
|
+
const indent = " ".repeat(Math.min(i, 5));
|
|
1808
|
+
const arrow = i === 0 ? "\u2192" : "\u21B3";
|
|
1809
|
+
const conf = hop.confidence && hop.confidence !== "extracted" ? ` [${hop.confidence}]` : "";
|
|
1810
|
+
lines.push(`${indent}${arrow} \`${hop.symbol}\` (${hop.kind}) \u2014 ${hop.filePath}:${hop.line}${conf}`);
|
|
1811
|
+
}
|
|
1812
|
+
lines.push("");
|
|
1813
|
+
}
|
|
1814
|
+
return lines.join("\n");
|
|
1815
|
+
}
|
|
1816
|
+
case "kirograph_communities": {
|
|
1817
|
+
const { detectCommunities } = await Promise.resolve().then(() => require("../graph/communities.js"));
|
|
1818
|
+
const db = cg.getDatabase();
|
|
1819
|
+
const result = detectCommunities(db, {
|
|
1820
|
+
resolution: args.resolution ?? 1
|
|
1821
|
+
});
|
|
1822
|
+
if (result.communities.length === 0) return "No communities detected. The graph may be too small or have no edges.";
|
|
1823
|
+
const limit = Math.min(args.limit ?? 15, result.communities.length);
|
|
1824
|
+
const lines = [
|
|
1825
|
+
`## Communities (${result.communities.length} detected, modularity: ${result.modularity.toFixed(3)})`,
|
|
1826
|
+
`Graph: ${result.totalNodes} nodes, ${result.totalEdges} edges`,
|
|
1827
|
+
""
|
|
1828
|
+
];
|
|
1829
|
+
for (const c of result.communities.slice(0, limit)) {
|
|
1830
|
+
lines.push(`### ${c.label} (${c.memberCount} symbols)`);
|
|
1831
|
+
lines.push(`- Directory: \`${c.dominantDirectory}\``);
|
|
1832
|
+
lines.push(`- Language: ${c.dominantLanguage}`);
|
|
1833
|
+
lines.push(`- Inter-community edges: ${c.interCommunityEdges}`);
|
|
1834
|
+
lines.push(`- Top members:`);
|
|
1835
|
+
for (const m of c.members.slice(0, 8)) {
|
|
1836
|
+
lines.push(` - ${m.kind} \`${m.name}\` \u2014 ${m.filePath}`);
|
|
1837
|
+
}
|
|
1838
|
+
if (c.memberCount > 8) lines.push(` - \u2026and ${c.memberCount - 8} more`);
|
|
1839
|
+
lines.push("");
|
|
1840
|
+
}
|
|
1841
|
+
return lines.join("\n");
|
|
1842
|
+
}
|
|
1843
|
+
case "kirograph_refactor": {
|
|
1844
|
+
const { renamePreview, suggestRefactorings } = await Promise.resolve().then(() => require("../graph/refactor.js"));
|
|
1845
|
+
const db = cg.getDatabase();
|
|
1846
|
+
const mode = args.mode;
|
|
1847
|
+
if (mode === "rename") {
|
|
1848
|
+
if (!args.symbol) return 'Error: "symbol" parameter is required for rename mode.';
|
|
1849
|
+
const preview = renamePreview(db, args.symbol);
|
|
1850
|
+
if (!preview) return `Symbol "${args.symbol}" not found in index.`;
|
|
1851
|
+
const lines = [
|
|
1852
|
+
`## Rename Preview: \`${preview.symbol}\``,
|
|
1853
|
+
`Kind: ${preview.kind}`,
|
|
1854
|
+
`Defined at: ${preview.filePath}:${preview.line}`,
|
|
1855
|
+
`Total references: ${preview.totalReferences}`,
|
|
1856
|
+
""
|
|
1857
|
+
];
|
|
1858
|
+
if (preview.references.length === 0) {
|
|
1859
|
+
lines.push("No references found \u2014 this symbol can be safely renamed without affecting other code.");
|
|
1860
|
+
} else {
|
|
1861
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
1862
|
+
for (const ref of preview.references) {
|
|
1863
|
+
if (!byFile.has(ref.filePath)) byFile.set(ref.filePath, []);
|
|
1864
|
+
byFile.get(ref.filePath).push(ref);
|
|
1865
|
+
}
|
|
1866
|
+
for (const [file, refs] of byFile) {
|
|
1867
|
+
lines.push(`### ${file} (${refs.length} references)`);
|
|
1868
|
+
for (const ref of refs.slice(0, 10)) {
|
|
1869
|
+
lines.push(`- Line ${ref.line}: \`${ref.context}\` (${ref.edgeKind})`);
|
|
1870
|
+
}
|
|
1871
|
+
if (refs.length > 10) lines.push(` \u2026and ${refs.length - 10} more`);
|
|
1872
|
+
lines.push("");
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
return lines.join("\n");
|
|
1876
|
+
}
|
|
1877
|
+
if (mode === "suggest") {
|
|
1878
|
+
const suggestions = suggestRefactorings(db, args.limit ?? 10);
|
|
1879
|
+
if (suggestions.length === 0) return "No refactoring suggestions \u2014 the codebase structure looks clean.";
|
|
1880
|
+
const lines = [`## Refactoring Suggestions (${suggestions.length})`, ""];
|
|
1881
|
+
for (const s of suggestions) {
|
|
1882
|
+
const icon = s.priority === "high" ? "\u{1F534}" : s.priority === "medium" ? "\u{1F7E1}" : "\u{1F7E2}";
|
|
1883
|
+
lines.push(`${icon} **${s.type}** [${s.priority}]: ${s.description}`);
|
|
1884
|
+
lines.push(` Rationale: ${s.rationale}`);
|
|
1885
|
+
if (s.symbols.length > 0) {
|
|
1886
|
+
lines.push(` Symbols: ${s.symbols.slice(0, 3).join(", ")}`);
|
|
1887
|
+
}
|
|
1888
|
+
lines.push("");
|
|
1889
|
+
}
|
|
1890
|
+
return lines.join("\n");
|
|
1891
|
+
}
|
|
1892
|
+
return 'Unknown mode. Use "rename" or "suggest".';
|
|
1893
|
+
}
|
|
1194
1894
|
// ── Memory tools ────────────────────────────────────────────────────────
|
|
1195
1895
|
case "kirograph_mem_search": {
|
|
1196
1896
|
const { loadConfig } = await Promise.resolve().then(() => require("../config.js"));
|
|
@@ -1205,7 +1905,8 @@ class ToolHandler {
|
|
|
1205
1905
|
const results = await mem.search(args.query, {
|
|
1206
1906
|
limit: args.limit ?? 10,
|
|
1207
1907
|
kind: args.kind,
|
|
1208
|
-
sessionId: args.sessionId
|
|
1908
|
+
sessionId: args.sessionId,
|
|
1909
|
+
asOf: args.asOf
|
|
1209
1910
|
});
|
|
1210
1911
|
if (results.length === 0) return `No memory observations found for "${args.query}".`;
|
|
1211
1912
|
return results.map((r, i) => {
|
|
@@ -1407,6 +2108,887 @@ class ToolHandler {
|
|
|
1407
2108
|
return `[${r.refType}] ${direction} (confidence: ${r.confidence.toFixed(2)})`;
|
|
1408
2109
|
}).join("\n");
|
|
1409
2110
|
}
|
|
2111
|
+
// ── Data tools ────────────────────────────────────────────────────────────
|
|
2112
|
+
case "kirograph_data_list": {
|
|
2113
|
+
const { loadConfig } = await Promise.resolve().then(() => require("../config.js"));
|
|
2114
|
+
const projectRoot = cg.getProjectRoot();
|
|
2115
|
+
const config = await loadConfig(projectRoot);
|
|
2116
|
+
if (!config.enableData) return "Data indexing is not enabled. Set enableData: true in .kirograph/config.json and run kirograph index.";
|
|
2117
|
+
const { DataQueries } = await Promise.resolve().then(() => require("../data/queries.js"));
|
|
2118
|
+
const db = cg.getDatabase();
|
|
2119
|
+
db.applyDataSchema();
|
|
2120
|
+
const dq = new DataQueries(db.getRawDb());
|
|
2121
|
+
const datasets = dq.listDatasets();
|
|
2122
|
+
if (datasets.length === 0) return "No datasets indexed. Run kirograph index or kirograph data reindex.";
|
|
2123
|
+
return datasets.map((ds) => {
|
|
2124
|
+
const sizeMb = (ds.fileSize / 1024 / 1024).toFixed(2);
|
|
2125
|
+
return `${ds.id} (${ds.format})
|
|
2126
|
+
File: ${ds.filePath}
|
|
2127
|
+
Rows: ${ds.rowCount.toLocaleString()} | Columns: ${ds.columnCount} | Size: ${sizeMb} MB`;
|
|
2128
|
+
}).join("\n\n");
|
|
2129
|
+
}
|
|
2130
|
+
case "kirograph_data_describe": {
|
|
2131
|
+
const { loadConfig } = await Promise.resolve().then(() => require("../config.js"));
|
|
2132
|
+
const projectRoot = cg.getProjectRoot();
|
|
2133
|
+
const config = await loadConfig(projectRoot);
|
|
2134
|
+
if (!config.enableData) return "Data indexing is not enabled. Set enableData: true in .kirograph/config.json and run kirograph index.";
|
|
2135
|
+
const { DataQueries } = await Promise.resolve().then(() => require("../data/queries.js"));
|
|
2136
|
+
const db = cg.getDatabase();
|
|
2137
|
+
db.applyDataSchema();
|
|
2138
|
+
const dq = new DataQueries(db.getRawDb());
|
|
2139
|
+
if (args.column) {
|
|
2140
|
+
const col = dq.describeColumn(args.dataset, args.column);
|
|
2141
|
+
if (!col) return `Column "${args.column}" not found in dataset "${args.dataset}".`;
|
|
2142
|
+
return [
|
|
2143
|
+
`Column: ${col.name}`,
|
|
2144
|
+
`Type: ${col.inferredType}`,
|
|
2145
|
+
`Nullable: ${col.nullable} (${(col.nullPct * 100).toFixed(1)}% null)`,
|
|
2146
|
+
`Cardinality: ${col.cardinality}`,
|
|
2147
|
+
col.minValue ? `Min: ${col.minValue}` : "",
|
|
2148
|
+
col.maxValue ? `Max: ${col.maxValue}` : "",
|
|
2149
|
+
col.meanValue !== null ? `Mean: ${col.meanValue.toFixed(2)}` : "",
|
|
2150
|
+
`Samples: ${col.sampleValues.join(", ")}`
|
|
2151
|
+
].filter(Boolean).join("\n");
|
|
2152
|
+
}
|
|
2153
|
+
const result = dq.describeDataset(args.dataset);
|
|
2154
|
+
if (!result) return `Dataset "${args.dataset}" not found. Use kirograph_data_list to see available datasets.`;
|
|
2155
|
+
const lines = [
|
|
2156
|
+
`Dataset: ${result.dataset.id} (${result.dataset.format})`,
|
|
2157
|
+
`File: ${result.dataset.filePath}`,
|
|
2158
|
+
`Rows: ${result.dataset.rowCount.toLocaleString()} | Columns: ${result.dataset.columnCount}`,
|
|
2159
|
+
"",
|
|
2160
|
+
"Columns:"
|
|
2161
|
+
];
|
|
2162
|
+
for (const col of result.columns) {
|
|
2163
|
+
const nullInfo = col.nullable ? ` (${(col.nullPct * 100).toFixed(0)}% null)` : "";
|
|
2164
|
+
const samples = col.sampleValues.length > 0 ? ` \u2014 samples: ${col.sampleValues.slice(0, 3).join(", ")}` : "";
|
|
2165
|
+
const summary = col.summary ? ` [${col.summary}]` : "";
|
|
2166
|
+
lines.push(` ${col.name}: ${col.inferredType}${nullInfo} [${col.cardinality} distinct]${samples}${summary}`);
|
|
2167
|
+
}
|
|
2168
|
+
const rules = dq.validationRules(args.dataset);
|
|
2169
|
+
if (rules && rules.length > 0) {
|
|
2170
|
+
lines.push("", "Validation rules:");
|
|
2171
|
+
for (const r of rules.slice(0, 10)) {
|
|
2172
|
+
lines.push(` ${r.column}: ${r.rules.join("; ")}`);
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
const hints = dq.sampleHints(args.dataset);
|
|
2176
|
+
if (hints && hints.length > 0) {
|
|
2177
|
+
lines.push("", "Sample data hints:");
|
|
2178
|
+
for (const h of hints.slice(0, 10)) {
|
|
2179
|
+
lines.push(` ${h.column}: ${h.hint}`);
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
return lines.join("\n");
|
|
2183
|
+
}
|
|
2184
|
+
case "kirograph_data_query": {
|
|
2185
|
+
const { loadConfig } = await Promise.resolve().then(() => require("../config.js"));
|
|
2186
|
+
const projectRoot = cg.getProjectRoot();
|
|
2187
|
+
const config = await loadConfig(projectRoot);
|
|
2188
|
+
if (!config.enableData) return "Data indexing is not enabled. Set enableData: true in .kirograph/config.json and run kirograph index.";
|
|
2189
|
+
const { DataQueries } = await Promise.resolve().then(() => require("../data/queries.js"));
|
|
2190
|
+
const db = cg.getDatabase();
|
|
2191
|
+
db.applyDataSchema();
|
|
2192
|
+
const dq = new DataQueries(db.getRawDb());
|
|
2193
|
+
const result = dq.queryRows(args.dataset, {
|
|
2194
|
+
filters: args.filters,
|
|
2195
|
+
columns: args.columns,
|
|
2196
|
+
limit: args.limit ?? 100,
|
|
2197
|
+
offset: args.offset ?? 0
|
|
2198
|
+
});
|
|
2199
|
+
if (!result) return `Dataset "${args.dataset}" not found.`;
|
|
2200
|
+
if (result.rows.length === 0) return `No rows match the given filters (${result.totalMatching} total in dataset).`;
|
|
2201
|
+
const header = `${result.rows.length} rows returned (${result.totalMatching} total matching):
|
|
2202
|
+
`;
|
|
2203
|
+
const rowStrs = result.rows.slice(0, 50).map((row, i) => {
|
|
2204
|
+
const vals = Object.entries(row).map(([k, v]) => `${k}=${v ?? "null"}`).join(", ");
|
|
2205
|
+
return ` ${i + 1}. ${vals}`;
|
|
2206
|
+
});
|
|
2207
|
+
if (result.rows.length > 50) rowStrs.push(` \u2026and ${result.rows.length - 50} more rows`);
|
|
2208
|
+
let response = header + rowStrs.join("\n");
|
|
2209
|
+
const maxChars = config.dataMaxResponseTokens * 4;
|
|
2210
|
+
if (response.length > maxChars) {
|
|
2211
|
+
response = response.slice(0, maxChars) + "\n\n[truncated: response exceeded token budget]";
|
|
2212
|
+
}
|
|
2213
|
+
response = this.checkPaginationLoop(args.dataset, args.offset, response);
|
|
2214
|
+
return response;
|
|
2215
|
+
}
|
|
2216
|
+
case "kirograph_data_aggregate": {
|
|
2217
|
+
const { loadConfig } = await Promise.resolve().then(() => require("../config.js"));
|
|
2218
|
+
const projectRoot = cg.getProjectRoot();
|
|
2219
|
+
const config = await loadConfig(projectRoot);
|
|
2220
|
+
if (!config.enableData) return "Data indexing is not enabled. Set enableData: true in .kirograph/config.json and run kirograph index.";
|
|
2221
|
+
const { DataQueries } = await Promise.resolve().then(() => require("../data/queries.js"));
|
|
2222
|
+
const db = cg.getDatabase();
|
|
2223
|
+
db.applyDataSchema();
|
|
2224
|
+
const dq = new DataQueries(db.getRawDb());
|
|
2225
|
+
const result = dq.aggregate(args.dataset, {
|
|
2226
|
+
groupBy: args.groupBy ?? [],
|
|
2227
|
+
metrics: args.metrics ?? [],
|
|
2228
|
+
filters: args.filters
|
|
2229
|
+
});
|
|
2230
|
+
if (!result) return `Dataset "${args.dataset}" not found.`;
|
|
2231
|
+
if (result.rows.length === 0) return "No results (empty dataset or all rows filtered out).";
|
|
2232
|
+
const keys = Object.keys(result.rows[0]);
|
|
2233
|
+
const header = keys.join(" | ");
|
|
2234
|
+
const separator = keys.map(() => "---").join(" | ");
|
|
2235
|
+
const rows = result.rows.slice(0, 100).map((row) => keys.map((k) => row[k] ?? "null").join(" | "));
|
|
2236
|
+
let response = `${header}
|
|
2237
|
+
${separator}
|
|
2238
|
+
${rows.join("\n")}${result.rows.length > 100 ? `
|
|
2239
|
+
\u2026and ${result.rows.length - 100} more groups` : ""}`;
|
|
2240
|
+
const maxChars = config.dataMaxResponseTokens * 4;
|
|
2241
|
+
if (response.length > maxChars) {
|
|
2242
|
+
response = response.slice(0, maxChars) + "\n\n[truncated: response exceeded token budget]";
|
|
2243
|
+
}
|
|
2244
|
+
return response;
|
|
2245
|
+
}
|
|
2246
|
+
case "kirograph_data_search": {
|
|
2247
|
+
const { loadConfig } = await Promise.resolve().then(() => require("../config.js"));
|
|
2248
|
+
const projectRoot = cg.getProjectRoot();
|
|
2249
|
+
const config = await loadConfig(projectRoot);
|
|
2250
|
+
if (!config.enableData) return "Data indexing is not enabled. Set enableData: true in .kirograph/config.json and run kirograph index.";
|
|
2251
|
+
const { DataQueries } = await Promise.resolve().then(() => require("../data/queries.js"));
|
|
2252
|
+
const db = cg.getDatabase();
|
|
2253
|
+
db.applyDataSchema();
|
|
2254
|
+
const dq = new DataQueries(db.getRawDb());
|
|
2255
|
+
const cols = dq.searchColumns(args.dataset, args.query);
|
|
2256
|
+
if (cols.length === 0) return `No columns matching "${args.query}" in dataset "${args.dataset}".`;
|
|
2257
|
+
return cols.map((c) => {
|
|
2258
|
+
const samples = c.sampleValues.length > 0 ? ` \u2014 samples: ${c.sampleValues.slice(0, 3).join(", ")}` : "";
|
|
2259
|
+
return `${c.name}: ${c.inferredType} [${c.cardinality} distinct]${samples}`;
|
|
2260
|
+
}).join("\n");
|
|
2261
|
+
}
|
|
2262
|
+
case "kirograph_data_join": {
|
|
2263
|
+
const { loadConfig } = await Promise.resolve().then(() => require("../config.js"));
|
|
2264
|
+
const projectRoot = cg.getProjectRoot();
|
|
2265
|
+
const config = await loadConfig(projectRoot);
|
|
2266
|
+
if (!config.enableData) return "Data indexing is not enabled. Set enableData: true in .kirograph/config.json and run kirograph index.";
|
|
2267
|
+
const { DataQueries } = await Promise.resolve().then(() => require("../data/queries.js"));
|
|
2268
|
+
const db = cg.getDatabase();
|
|
2269
|
+
db.applyDataSchema();
|
|
2270
|
+
const dq = new DataQueries(db.getRawDb());
|
|
2271
|
+
try {
|
|
2272
|
+
const result = dq.join({
|
|
2273
|
+
left: args.left,
|
|
2274
|
+
right: args.right,
|
|
2275
|
+
leftColumn: args.leftColumn,
|
|
2276
|
+
rightColumn: args.rightColumn,
|
|
2277
|
+
type: args.type ?? "inner",
|
|
2278
|
+
columns: args.columns,
|
|
2279
|
+
limit: args.limit
|
|
2280
|
+
});
|
|
2281
|
+
if (!result) return `Dataset not found. Verify both dataset IDs with kirograph_data_list.`;
|
|
2282
|
+
const joinTypeStr = String(args.type ?? "inner").toUpperCase();
|
|
2283
|
+
const header = `Join: ${args.left}.${args.leftColumn} ${joinTypeStr} JOIN ${args.right}.${args.rightColumn}
|
|
2284
|
+
Matching rows: ${result.totalMatching} (showing ${result.rows.length})`;
|
|
2285
|
+
if (result.rows.length === 0) return `${header}
|
|
2286
|
+
|
|
2287
|
+
No matching rows.`;
|
|
2288
|
+
const lines = result.rows.map((r) => JSON.stringify(r));
|
|
2289
|
+
let response = `${header}
|
|
2290
|
+
|
|
2291
|
+
${lines.join("\n")}`;
|
|
2292
|
+
const maxChars = config.dataMaxResponseTokens * 4;
|
|
2293
|
+
if (response.length > maxChars) {
|
|
2294
|
+
response = response.slice(0, maxChars) + "\n\n[truncated: response exceeded token budget]";
|
|
2295
|
+
}
|
|
2296
|
+
return response;
|
|
2297
|
+
} catch (err) {
|
|
2298
|
+
return `Error: ${err instanceof Error ? err.message : String(err)}`;
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
case "kirograph_data_correlations": {
|
|
2302
|
+
const { loadConfig } = await Promise.resolve().then(() => require("../config.js"));
|
|
2303
|
+
const projectRoot = cg.getProjectRoot();
|
|
2304
|
+
const config = await loadConfig(projectRoot);
|
|
2305
|
+
if (!config.enableData) return "Data indexing is not enabled. Set enableData: true in .kirograph/config.json and run kirograph index.";
|
|
2306
|
+
const { DataQueries } = await Promise.resolve().then(() => require("../data/queries.js"));
|
|
2307
|
+
const db = cg.getDatabase();
|
|
2308
|
+
db.applyDataSchema();
|
|
2309
|
+
const dq = new DataQueries(db.getRawDb());
|
|
2310
|
+
const pairs = dq.correlations(args.dataset, args.threshold);
|
|
2311
|
+
if (pairs === null) return `Dataset "${args.dataset}" not found.`;
|
|
2312
|
+
if (pairs.length === 0) return `No correlations above threshold ${args.threshold ?? 0.3} found. The dataset may have fewer than 2 numeric columns or no significant correlations.`;
|
|
2313
|
+
const lines = pairs.map(
|
|
2314
|
+
(p) => `${p.column1} \u2194 ${p.column2}: ${p.correlation > 0 ? "+" : ""}${p.correlation.toFixed(4)} (${p.strength})`
|
|
2315
|
+
);
|
|
2316
|
+
return `Correlations for "${args.dataset}" (threshold: ${args.threshold ?? 0.3}):
|
|
2317
|
+
|
|
2318
|
+
${lines.join("\n")}`;
|
|
2319
|
+
}
|
|
2320
|
+
case "kirograph_data_quality": {
|
|
2321
|
+
const { loadConfig } = await Promise.resolve().then(() => require("../config.js"));
|
|
2322
|
+
const projectRoot = cg.getProjectRoot();
|
|
2323
|
+
const config = await loadConfig(projectRoot);
|
|
2324
|
+
if (!config.enableData) return "Data indexing is not enabled. Set enableData: true in .kirograph/config.json and run kirograph index.";
|
|
2325
|
+
const { DataQueries } = await Promise.resolve().then(() => require("../data/queries.js"));
|
|
2326
|
+
const db = cg.getDatabase();
|
|
2327
|
+
db.applyDataSchema();
|
|
2328
|
+
const dq = new DataQueries(db.getRawDb());
|
|
2329
|
+
const quality = dq.quality(args.dataset);
|
|
2330
|
+
if (quality === null) return `Dataset "${args.dataset}" not found.`;
|
|
2331
|
+
if (quality.length === 0) return `No quality issues detected in "${args.dataset}". All columns look healthy.`;
|
|
2332
|
+
const lines = quality.map(
|
|
2333
|
+
(q) => `${q.column} (risk: ${(q.riskScore * 100).toFixed(0)}%): ${q.issues.join("; ")}`
|
|
2334
|
+
);
|
|
2335
|
+
return `Quality report for "${args.dataset}" (${quality.length} columns with issues):
|
|
2336
|
+
|
|
2337
|
+
${lines.join("\n")}`;
|
|
2338
|
+
}
|
|
2339
|
+
// ── Security tools (require enableSecurity=true) ──────────────────────────
|
|
2340
|
+
case "kirograph_security": {
|
|
2341
|
+
const { loadConfig } = await Promise.resolve().then(() => require("../config.js"));
|
|
2342
|
+
const projectRoot = cg.getProjectRoot();
|
|
2343
|
+
const config = await loadConfig(projectRoot);
|
|
2344
|
+
if (!config.enableSecurity) return "Security analysis is not enabled. Set enableSecurity: true in .kirograph/config.json and run kirograph index.";
|
|
2345
|
+
if (!config.enableArchitecture) return "Security requires architecture analysis. Set enableArchitecture: true in .kirograph/config.json and run kirograph index.";
|
|
2346
|
+
const db = cg.getDatabase();
|
|
2347
|
+
db.applySecuritySchema();
|
|
2348
|
+
const rawDb = db.getRawDb();
|
|
2349
|
+
const depCount = rawDb.get(
|
|
2350
|
+
`SELECT COUNT(*) as count FROM sec_dependencies`
|
|
2351
|
+
) ?? { count: 0 };
|
|
2352
|
+
const vulnCount = rawDb.get(
|
|
2353
|
+
`SELECT COUNT(*) as count FROM sec_vulnerabilities`
|
|
2354
|
+
) ?? { count: 0 };
|
|
2355
|
+
const verdictRows = rawDb.all(
|
|
2356
|
+
`SELECT verdict, COUNT(*) as count FROM sec_reachability GROUP BY verdict`
|
|
2357
|
+
);
|
|
2358
|
+
const verdicts = {};
|
|
2359
|
+
for (const row of verdictRows) {
|
|
2360
|
+
verdicts[row.verdict] = row.count;
|
|
2361
|
+
}
|
|
2362
|
+
const staleCount = rawDb.get(
|
|
2363
|
+
`SELECT COUNT(*) as count FROM sec_dependencies WHERE vuln_data_stale = 1`
|
|
2364
|
+
) ?? { count: 0 };
|
|
2365
|
+
const { SuppressionManager: SecSuppressionManager } = await Promise.resolve().then(() => require("../security/suppressions.js"));
|
|
2366
|
+
const secSuppressions = new SecSuppressionManager(projectRoot);
|
|
2367
|
+
const allCveIds = rawDb.all(`SELECT cve_id FROM sec_vulnerabilities`);
|
|
2368
|
+
const suppressedCveCount = allCveIds.filter((r) => secSuppressions.isSuppressed(r.cve_id)).length;
|
|
2369
|
+
const visibleVulnCount = vulnCount.count - suppressedCveCount;
|
|
2370
|
+
const lines = [
|
|
2371
|
+
"# Security Overview",
|
|
2372
|
+
"",
|
|
2373
|
+
`Dependencies: ${depCount.count}`,
|
|
2374
|
+
`Vulnerabilities: ${visibleVulnCount}${suppressedCveCount > 0 ? ` (${suppressedCveCount} suppressed)` : ""}`
|
|
2375
|
+
];
|
|
2376
|
+
if (visibleVulnCount > 0) {
|
|
2377
|
+
const affected = verdicts["affected"] ?? 0;
|
|
2378
|
+
const notAffected = verdicts["not_affected"] ?? 0;
|
|
2379
|
+
const underInvestigation = verdicts["under_investigation"] ?? 0;
|
|
2380
|
+
const pending = visibleVulnCount - affected - notAffected - underInvestigation;
|
|
2381
|
+
lines.push("", "## Reachability Verdicts", "");
|
|
2382
|
+
if (affected > 0) lines.push(`\u25CF Affected: ${affected}`);
|
|
2383
|
+
if (notAffected > 0) lines.push(`\u25CF Not affected: ${notAffected}`);
|
|
2384
|
+
if (underInvestigation > 0) lines.push(`\u25CF Under investigation: ${underInvestigation}`);
|
|
2385
|
+
if (pending > 0) lines.push(`\u25CF Pending analysis: ${pending}`);
|
|
2386
|
+
}
|
|
2387
|
+
if (staleCount.count > 0) {
|
|
2388
|
+
lines.push("", `\u26A0 ${staleCount.count} dependenc${staleCount.count === 1 ? "y has" : "ies have"} stale vulnerability data. Use kirograph_vulns with refresh=true to update.`);
|
|
2389
|
+
}
|
|
2390
|
+
const lastVulnCheck = rawDb.get(
|
|
2391
|
+
`SELECT MIN(last_vuln_check) as oldest FROM sec_dependencies WHERE last_vuln_check IS NOT NULL`
|
|
2392
|
+
)?.oldest;
|
|
2393
|
+
if (lastVulnCheck != null) {
|
|
2394
|
+
const ageMs = Date.now() - lastVulnCheck;
|
|
2395
|
+
const ageDays = ageMs / (1e3 * 60 * 60 * 24);
|
|
2396
|
+
const maxAge = config.securityEnrichMaxAgeDays ?? 7;
|
|
2397
|
+
if (ageDays > maxAge) {
|
|
2398
|
+
lines.push("", `\u26A0 Vulnerability data is ${Math.floor(ageDays)} days old (max: ${maxAge}). Run kirograph vulns --refresh to update.`);
|
|
2399
|
+
}
|
|
2400
|
+
}
|
|
2401
|
+
return lines.join("\n");
|
|
2402
|
+
}
|
|
2403
|
+
case "kirograph_vulns": {
|
|
2404
|
+
const { loadConfig } = await Promise.resolve().then(() => require("../config.js"));
|
|
2405
|
+
const projectRoot = cg.getProjectRoot();
|
|
2406
|
+
const config = await loadConfig(projectRoot);
|
|
2407
|
+
if (!config.enableSecurity) return "Security analysis is not enabled. Set enableSecurity: true in .kirograph/config.json and run kirograph index.";
|
|
2408
|
+
const db = cg.getDatabase();
|
|
2409
|
+
db.applySecuritySchema();
|
|
2410
|
+
const rawDb = db.getRawDb();
|
|
2411
|
+
if (args.refresh === true) {
|
|
2412
|
+
const { OsvAdapter } = await Promise.resolve().then(() => require("../security/vuln/osv-adapter.js"));
|
|
2413
|
+
const { VulnerabilityDatabaseClient } = await Promise.resolve().then(() => require("../security/vuln/client.js"));
|
|
2414
|
+
const adapters = config.securityDatabases.map((dbName) => {
|
|
2415
|
+
if (dbName.toUpperCase() === "OSV") return new OsvAdapter();
|
|
2416
|
+
return null;
|
|
2417
|
+
}).filter(Boolean);
|
|
2418
|
+
const client = new VulnerabilityDatabaseClient(adapters, db);
|
|
2419
|
+
await client.enrichAll();
|
|
2420
|
+
}
|
|
2421
|
+
let query = `
|
|
2422
|
+
SELECT
|
|
2423
|
+
v.node_id, v.cve_id, v.severity_score, v.fixed_version, v.summary,
|
|
2424
|
+
v.epss_score, v.epss_percentile, v.risk_score,
|
|
2425
|
+
d.package_name, d.ecosystem, d.resolved_version, d.declared_constraint,
|
|
2426
|
+
r.verdict
|
|
2427
|
+
FROM sec_vulnerabilities v
|
|
2428
|
+
LEFT JOIN edges e ON e.target = v.node_id AND e.kind = 'has_vulnerability'
|
|
2429
|
+
LEFT JOIN sec_dependencies d ON d.node_id = e.source
|
|
2430
|
+
LEFT JOIN sec_reachability r ON r.vulnerability_node_id = v.node_id
|
|
2431
|
+
WHERE 1=1
|
|
2432
|
+
`;
|
|
2433
|
+
const params = [];
|
|
2434
|
+
if (args.severity) {
|
|
2435
|
+
const severityRanges = {
|
|
2436
|
+
critical: [9, 10],
|
|
2437
|
+
high: [7, 8.9],
|
|
2438
|
+
medium: [4, 6.9],
|
|
2439
|
+
low: [0.1, 3.9]
|
|
2440
|
+
};
|
|
2441
|
+
const range = severityRanges[args.severity.toLowerCase()];
|
|
2442
|
+
if (!range) return `Invalid severity: ${args.severity}. Use: critical, high, medium, low`;
|
|
2443
|
+
query += ` AND v.severity_score >= ? AND v.severity_score <= ?`;
|
|
2444
|
+
params.push(range[0], range[1]);
|
|
2445
|
+
}
|
|
2446
|
+
if (args.verdict) {
|
|
2447
|
+
const validVerdicts = ["affected", "not_affected", "under_investigation"];
|
|
2448
|
+
if (!validVerdicts.includes(args.verdict)) {
|
|
2449
|
+
return `Invalid verdict: ${args.verdict}. Use: affected, not_affected, under_investigation`;
|
|
2450
|
+
}
|
|
2451
|
+
query += ` AND r.verdict = ?`;
|
|
2452
|
+
params.push(args.verdict);
|
|
2453
|
+
}
|
|
2454
|
+
const limit = clampLimit(args.limit, 20);
|
|
2455
|
+
query += ` ORDER BY v.risk_score DESC NULLS LAST LIMIT ?`;
|
|
2456
|
+
params.push(limit);
|
|
2457
|
+
const rows = rawDb.all(query, params);
|
|
2458
|
+
const verdictRankMcp = (v) => v === "affected" ? 3 : v === "under_investigation" ? 2 : v === "not_affected" ? 1 : 0;
|
|
2459
|
+
const dedupMapMcp = /* @__PURE__ */ new Map();
|
|
2460
|
+
for (const row of rows) {
|
|
2461
|
+
const key = `${row.cve_id}::${row.package_name ?? ""}::${row.ecosystem ?? ""}`;
|
|
2462
|
+
const ex = dedupMapMcp.get(key);
|
|
2463
|
+
if (!ex || verdictRankMcp(row.verdict) > verdictRankMcp(ex.verdict)) dedupMapMcp.set(key, row);
|
|
2464
|
+
}
|
|
2465
|
+
const dedupedRowsMcp = [...dedupMapMcp.values()];
|
|
2466
|
+
const { SuppressionManager } = await Promise.resolve().then(() => require("../security/suppressions.js"));
|
|
2467
|
+
const suppressionMgr = new SuppressionManager(projectRoot);
|
|
2468
|
+
const suppressedCount = dedupedRowsMcp.filter((row) => suppressionMgr.isSuppressed(row.cve_id)).length;
|
|
2469
|
+
const filteredRows = dedupedRowsMcp.filter((row) => !suppressionMgr.isSuppressed(row.cve_id));
|
|
2470
|
+
if (filteredRows.length === 0) {
|
|
2471
|
+
const noMatch = "No vulnerabilities found" + (args.severity || args.verdict ? " matching filters." : ".");
|
|
2472
|
+
return suppressedCount > 0 ? `${noMatch} (${suppressedCount} suppressed)` : noMatch;
|
|
2473
|
+
}
|
|
2474
|
+
const { formatFixSuggestion } = await Promise.resolve().then(() => require("../security/export/fix-suggestions.js"));
|
|
2475
|
+
const lines = [`Vulnerabilities (${filteredRows.length}${suppressedCount > 0 ? `, ${suppressedCount} suppressed` : ""}):
|
|
2476
|
+
`];
|
|
2477
|
+
for (const row of filteredRows) {
|
|
2478
|
+
const score = row.severity_score;
|
|
2479
|
+
let severityLabel;
|
|
2480
|
+
if (score == null) severityLabel = "UNKNOWN";
|
|
2481
|
+
else if (score >= 9) severityLabel = "CRITICAL";
|
|
2482
|
+
else if (score >= 7) severityLabel = "HIGH";
|
|
2483
|
+
else if (score >= 4) severityLabel = "MEDIUM";
|
|
2484
|
+
else severityLabel = "LOW";
|
|
2485
|
+
let verdictLabel;
|
|
2486
|
+
if (!row.verdict) verdictLabel = "pending";
|
|
2487
|
+
else if (row.verdict === "affected") verdictLabel = "affected";
|
|
2488
|
+
else if (row.verdict === "not_affected") verdictLabel = "not affected";
|
|
2489
|
+
else verdictLabel = "investigating";
|
|
2490
|
+
const pkg = row.package_name ? `${row.package_name}@${row.resolved_version || row.declared_constraint || "?"}` : "unknown package";
|
|
2491
|
+
const epssNote = row.epss_score != null ? ` [EPSS: ${row.epss_score.toFixed(2)}]` : "";
|
|
2492
|
+
const riskNote = row.risk_score != null ? ` [Risk: ${row.risk_score.toFixed(1)}]` : "";
|
|
2493
|
+
lines.push(`${severityLabel} ${row.cve_id} ${pkg} [${verdictLabel}]${epssNote}${riskNote}`);
|
|
2494
|
+
if (row.summary && row.summary !== "Manually registered") {
|
|
2495
|
+
const truncSummary = row.summary.length > 120 ? row.summary.slice(0, 120) + "\u2026" : row.summary;
|
|
2496
|
+
lines.push(` ${truncSummary}`);
|
|
2497
|
+
}
|
|
2498
|
+
if (row.fixed_version && row.ecosystem && row.package_name) {
|
|
2499
|
+
const fix = formatFixSuggestion(row.ecosystem, row.package_name, row.fixed_version);
|
|
2500
|
+
if (fix) lines.push(` ${fix}`);
|
|
2501
|
+
}
|
|
2502
|
+
}
|
|
2503
|
+
return truncate(lines.join("\n"));
|
|
2504
|
+
}
|
|
2505
|
+
case "kirograph_vuln_add": {
|
|
2506
|
+
const { loadConfig } = await Promise.resolve().then(() => require("../config.js"));
|
|
2507
|
+
const projectRoot = cg.getProjectRoot();
|
|
2508
|
+
const config = await loadConfig(projectRoot);
|
|
2509
|
+
if (!config.enableSecurity) return "Security analysis is not enabled. Set enableSecurity: true in .kirograph/config.json and run kirograph index.";
|
|
2510
|
+
const cveId = args.cveId;
|
|
2511
|
+
const pkgName = args.package;
|
|
2512
|
+
if (!cveId) return "Error: cveId is required.";
|
|
2513
|
+
if (!pkgName) return "Error: package is required.";
|
|
2514
|
+
const db = cg.getDatabase();
|
|
2515
|
+
db.applySecuritySchema();
|
|
2516
|
+
const rawDb = db.getRawDb();
|
|
2517
|
+
const depRow = rawDb.get(
|
|
2518
|
+
`SELECT node_id, ecosystem FROM sec_dependencies WHERE package_name = ?`,
|
|
2519
|
+
[pkgName]
|
|
2520
|
+
);
|
|
2521
|
+
if (!depRow) {
|
|
2522
|
+
return `No dependency found matching "${pkgName}". Run kirograph index first to discover dependencies.`;
|
|
2523
|
+
}
|
|
2524
|
+
const vulnNodeId = `vuln:${cveId}`;
|
|
2525
|
+
const now = Date.now();
|
|
2526
|
+
const severity = args.severity;
|
|
2527
|
+
const summary = args.summary ?? "Manually registered";
|
|
2528
|
+
const fixedVersion = args.fixedVersion ?? null;
|
|
2529
|
+
rawDb.run(
|
|
2530
|
+
`INSERT OR REPLACE INTO nodes
|
|
2531
|
+
(id, kind, name, qualified_name, file_path, language,
|
|
2532
|
+
start_line, end_line, start_column, end_column,
|
|
2533
|
+
is_exported, is_async, is_static, is_abstract, updated_at)
|
|
2534
|
+
VALUES (?, 'vulnerability', ?, ?, '', 'unknown', 0, 0, 0, 0, 0, 0, 0, 0, ?)`,
|
|
2535
|
+
[vulnNodeId, cveId, cveId, now]
|
|
2536
|
+
);
|
|
2537
|
+
rawDb.run(
|
|
2538
|
+
`INSERT OR REPLACE INTO sec_vulnerabilities
|
|
2539
|
+
(node_id, cve_id, severity_score, affected_ranges, fixed_version, summary, source_database)
|
|
2540
|
+
VALUES (?, ?, ?, '[]', ?, ?, 'manual')`,
|
|
2541
|
+
[vulnNodeId, cveId, severity ?? null, fixedVersion, summary]
|
|
2542
|
+
);
|
|
2543
|
+
rawDb.run(
|
|
2544
|
+
`INSERT OR IGNORE INTO edges (source, target, kind, confidence, confidence_score)
|
|
2545
|
+
VALUES (?, ?, 'has_vulnerability', 'extracted', 1.0)`,
|
|
2546
|
+
[depRow.node_id, vulnNodeId]
|
|
2547
|
+
);
|
|
2548
|
+
return `Registered ${cveId} against ${pkgName}.`;
|
|
2549
|
+
}
|
|
2550
|
+
case "kirograph_vuln_suppress": {
|
|
2551
|
+
const projectRoot = cg.getProjectRoot();
|
|
2552
|
+
const cveId = args.cveId;
|
|
2553
|
+
if (!cveId) return "Error: cveId is required.";
|
|
2554
|
+
const { SuppressionManager } = await Promise.resolve().then(() => require("../security/suppressions.js"));
|
|
2555
|
+
const manager = new SuppressionManager(projectRoot);
|
|
2556
|
+
manager.add(cveId, args.reason, args.expires);
|
|
2557
|
+
const reasonNote = args.reason ? ` Reason: ${args.reason}.` : "";
|
|
2558
|
+
const expiresNote = args.expires ? ` Expires: ${args.expires}.` : "";
|
|
2559
|
+
return `${cveId} suppressed.${reasonNote}${expiresNote}`;
|
|
2560
|
+
}
|
|
2561
|
+
case "kirograph_sbom": {
|
|
2562
|
+
const { loadConfig } = await Promise.resolve().then(() => require("../config.js"));
|
|
2563
|
+
const projectRoot = cg.getProjectRoot();
|
|
2564
|
+
const config = await loadConfig(projectRoot);
|
|
2565
|
+
if (!config.enableSecurity) return "Security analysis is not enabled. Set enableSecurity: true in .kirograph/config.json and run kirograph index.";
|
|
2566
|
+
const db = cg.getDatabase();
|
|
2567
|
+
db.applySecuritySchema();
|
|
2568
|
+
const { SBOMExporter } = await Promise.resolve().then(() => require("../security/export/sbom.js"));
|
|
2569
|
+
const exporter = new SBOMExporter(db, projectRoot);
|
|
2570
|
+
const json = exporter.exportJSON();
|
|
2571
|
+
return truncate(json);
|
|
2572
|
+
}
|
|
2573
|
+
case "kirograph_vex": {
|
|
2574
|
+
const { loadConfig } = await Promise.resolve().then(() => require("../config.js"));
|
|
2575
|
+
const projectRoot = cg.getProjectRoot();
|
|
2576
|
+
const config = await loadConfig(projectRoot);
|
|
2577
|
+
if (!config.enableSecurity) return "Security analysis is not enabled. Set enableSecurity: true in .kirograph/config.json and run kirograph index.";
|
|
2578
|
+
const db = cg.getDatabase();
|
|
2579
|
+
db.applySecuritySchema();
|
|
2580
|
+
const { VEXExporter } = await Promise.resolve().then(() => require("../security/export/vex.js"));
|
|
2581
|
+
const exporter = new VEXExporter(db, projectRoot);
|
|
2582
|
+
const json = exporter.exportJSON();
|
|
2583
|
+
return truncate(json);
|
|
2584
|
+
}
|
|
2585
|
+
case "kirograph_reachability": {
|
|
2586
|
+
const { loadConfig } = await Promise.resolve().then(() => require("../config.js"));
|
|
2587
|
+
const projectRoot = cg.getProjectRoot();
|
|
2588
|
+
const config = await loadConfig(projectRoot);
|
|
2589
|
+
if (!config.enableSecurity) return "Security analysis is not enabled. Set enableSecurity: true in .kirograph/config.json and run kirograph index.";
|
|
2590
|
+
const target = args.target;
|
|
2591
|
+
if (!target) return "Error: target is required (dependency name or CVE ID).";
|
|
2592
|
+
const db = cg.getDatabase();
|
|
2593
|
+
db.applySecuritySchema();
|
|
2594
|
+
const rawDb = db.getRawDb();
|
|
2595
|
+
let vulnerabilityNodeId = null;
|
|
2596
|
+
let targetLabel = target;
|
|
2597
|
+
const vulnRow = rawDb.get(
|
|
2598
|
+
`SELECT node_id FROM sec_vulnerabilities WHERE cve_id = ?`,
|
|
2599
|
+
[target]
|
|
2600
|
+
);
|
|
2601
|
+
if (vulnRow) {
|
|
2602
|
+
vulnerabilityNodeId = vulnRow.node_id;
|
|
2603
|
+
} else {
|
|
2604
|
+
const depRow = rawDb.get(
|
|
2605
|
+
`SELECT node_id, package_name FROM sec_dependencies WHERE package_name = ?`,
|
|
2606
|
+
[target]
|
|
2607
|
+
);
|
|
2608
|
+
if (depRow) {
|
|
2609
|
+
targetLabel = depRow.package_name;
|
|
2610
|
+
const vulnEdge = rawDb.get(
|
|
2611
|
+
`SELECT target FROM edges WHERE source = ? AND kind = 'has_vulnerability' LIMIT 1`,
|
|
2612
|
+
[depRow.node_id]
|
|
2613
|
+
);
|
|
2614
|
+
if (vulnEdge) {
|
|
2615
|
+
vulnerabilityNodeId = vulnEdge.target;
|
|
2616
|
+
} else {
|
|
2617
|
+
return `No vulnerabilities found for dependency "${target}". The dependency exists but has no known vulnerabilities.`;
|
|
2618
|
+
}
|
|
2619
|
+
} else {
|
|
2620
|
+
return `Target "${target}" not found. Provide a valid CVE ID or dependency package name.`;
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2623
|
+
const { ReachabilityAnalyzer } = await Promise.resolve().then(() => require("../security/reachability.js"));
|
|
2624
|
+
const analyzer = new ReachabilityAnalyzer(db, config);
|
|
2625
|
+
const result = await analyzer.analyze(vulnerabilityNodeId);
|
|
2626
|
+
const lines = [
|
|
2627
|
+
`# Reachability: ${targetLabel}`,
|
|
2628
|
+
"",
|
|
2629
|
+
`Verdict: ${result.verdict}`,
|
|
2630
|
+
`Reaching entry points: ${result.reachingEntryPointCount}`
|
|
2631
|
+
];
|
|
2632
|
+
if (result.paths.length > 0) {
|
|
2633
|
+
lines.push("", "## Paths");
|
|
2634
|
+
for (const p of result.paths.slice(0, 5)) {
|
|
2635
|
+
lines.push(`- From ${p.entryPoint}: ${p.path.join(" \u2192 ")}`);
|
|
2636
|
+
}
|
|
2637
|
+
if (result.paths.length > 5) {
|
|
2638
|
+
lines.push(` \u2026and ${result.paths.length - 5} more paths`);
|
|
2639
|
+
}
|
|
2640
|
+
}
|
|
2641
|
+
if (result.unresolvedSymbols.length > 0) {
|
|
2642
|
+
lines.push("", "## Unresolved Symbols");
|
|
2643
|
+
for (const sym of result.unresolvedSymbols.slice(0, 10)) {
|
|
2644
|
+
lines.push(`- ${sym}`);
|
|
2645
|
+
}
|
|
2646
|
+
if (result.unresolvedSymbols.length > 10) {
|
|
2647
|
+
lines.push(` \u2026and ${result.unresolvedSymbols.length - 10} more`);
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2650
|
+
if (result.verdict === "affected") {
|
|
2651
|
+
const impact = await analyzer.getImpactSummary(vulnerabilityNodeId);
|
|
2652
|
+
if (impact) {
|
|
2653
|
+
lines.push("", "## Impact Summary");
|
|
2654
|
+
if (impact.affectedLayers.length > 0) {
|
|
2655
|
+
lines.push(`Affected layers: ${impact.affectedLayers.join(", ")}`);
|
|
2656
|
+
}
|
|
2657
|
+
lines.push(`Affected entry points: ${impact.affectedEntryPoints.length}`);
|
|
2658
|
+
lines.push(`Distinct paths: ${impact.distinctPathCount}`);
|
|
2659
|
+
}
|
|
2660
|
+
}
|
|
2661
|
+
return truncate(lines.join("\n"));
|
|
2662
|
+
}
|
|
2663
|
+
case "kirograph_staleness": {
|
|
2664
|
+
const { loadConfig } = await Promise.resolve().then(() => require("../config.js"));
|
|
2665
|
+
const projectRoot = cg.getProjectRoot();
|
|
2666
|
+
const config = await loadConfig(projectRoot);
|
|
2667
|
+
if (!config.enableSecurity) return "Security analysis is not enabled. Set enableSecurity: true in .kirograph/config.json and run kirograph index.";
|
|
2668
|
+
const db = cg.getDatabase();
|
|
2669
|
+
db.applySecuritySchema();
|
|
2670
|
+
const rawDb = db.getRawDb();
|
|
2671
|
+
const threshold = typeof args.threshold === "number" ? args.threshold : 0.3;
|
|
2672
|
+
if (args.refresh === true) {
|
|
2673
|
+
const { StalenessChecker } = await Promise.resolve().then(() => require("../security/staleness.js"));
|
|
2674
|
+
const checker = new StalenessChecker(db);
|
|
2675
|
+
await checker.checkAll();
|
|
2676
|
+
}
|
|
2677
|
+
const rows = rawDb.all(
|
|
2678
|
+
`SELECT package_name, ecosystem, resolved_version, declared_constraint,
|
|
2679
|
+
latest_version, latest_published, staleness_score
|
|
2680
|
+
FROM sec_dependencies
|
|
2681
|
+
WHERE staleness_score >= ?
|
|
2682
|
+
ORDER BY staleness_score DESC`,
|
|
2683
|
+
[threshold]
|
|
2684
|
+
);
|
|
2685
|
+
if (rows.length === 0) {
|
|
2686
|
+
return `No dependencies found with staleness_score >= ${threshold}.` + (args.refresh ? "" : " Use refresh=true to fetch latest version data from registries.");
|
|
2687
|
+
}
|
|
2688
|
+
const lines = [`Stale Dependencies (threshold: ${threshold}):
|
|
2689
|
+
`];
|
|
2690
|
+
for (const row of rows) {
|
|
2691
|
+
const resolved = row.resolved_version ?? row.declared_constraint ?? "?";
|
|
2692
|
+
const latest = row.latest_version ?? "?";
|
|
2693
|
+
const score = row.staleness_score ?? 0;
|
|
2694
|
+
const months = row.latest_published ? Math.round((Date.now() - row.latest_published) / (1e3 * 60 * 60 * 24 * 30)) : null;
|
|
2695
|
+
const bar = "\u2588".repeat(Math.round(score * 10)) + "\u2591".repeat(10 - Math.round(score * 10));
|
|
2696
|
+
const monthsStr = months !== null ? `, ${months}mo since latest` : "";
|
|
2697
|
+
lines.push(`${row.package_name} (${row.ecosystem}): ${resolved} \u2192 ${latest}${monthsStr}`);
|
|
2698
|
+
lines.push(` ${bar} ${score.toFixed(2)}`);
|
|
2699
|
+
}
|
|
2700
|
+
const totalCount = rawDb.get(`SELECT COUNT(*) as count FROM sec_dependencies`) ?? { count: 0 };
|
|
2701
|
+
lines.push("", `${rows.length} of ${totalCount.count} dependencies are stale (score >= ${threshold})`);
|
|
2702
|
+
return truncate(lines.join("\n"));
|
|
2703
|
+
}
|
|
2704
|
+
case "kirograph_licenses": {
|
|
2705
|
+
const { loadConfig } = await Promise.resolve().then(() => require("../config.js"));
|
|
2706
|
+
const projectRoot = cg.getProjectRoot();
|
|
2707
|
+
const config = await loadConfig(projectRoot);
|
|
2708
|
+
if (!config.enableSecurity) return "Security analysis is not enabled. Set enableSecurity: true in .kirograph/config.json and run kirograph index.";
|
|
2709
|
+
const db = cg.getDatabase();
|
|
2710
|
+
db.applySecuritySchema();
|
|
2711
|
+
const rawDb = db.getRawDb();
|
|
2712
|
+
const deps = rawDb.all(
|
|
2713
|
+
`SELECT package_name, ecosystem, license FROM sec_dependencies ORDER BY ecosystem, package_name`
|
|
2714
|
+
);
|
|
2715
|
+
const { checkLicensePolicy } = await Promise.resolve().then(() => require("../security/license.js"));
|
|
2716
|
+
const policy = config.securityLicensePolicy;
|
|
2717
|
+
const violations = checkLicensePolicy(deps, policy);
|
|
2718
|
+
if (args.policy === true) {
|
|
2719
|
+
if (violations.length === 0) {
|
|
2720
|
+
return "No license policy violations found.";
|
|
2721
|
+
}
|
|
2722
|
+
const lines2 = ["# License Policy Violations\n"];
|
|
2723
|
+
for (const v of violations) {
|
|
2724
|
+
lines2.push(`${v.severity.toUpperCase()} ${v.packageName} [${v.ecosystem}] ${v.license}`);
|
|
2725
|
+
}
|
|
2726
|
+
const denyCount2 = violations.filter((v) => v.severity === "deny").length;
|
|
2727
|
+
const warnCount2 = violations.filter((v) => v.severity === "warn").length;
|
|
2728
|
+
lines2.push("");
|
|
2729
|
+
if (denyCount2 > 0) lines2.push(`${denyCount2} denied license${denyCount2 !== 1 ? "s" : ""}`);
|
|
2730
|
+
if (warnCount2 > 0) lines2.push(`${warnCount2} license warning${warnCount2 !== 1 ? "s" : ""}`);
|
|
2731
|
+
return truncate(lines2.join("\n"));
|
|
2732
|
+
}
|
|
2733
|
+
if (deps.length === 0) {
|
|
2734
|
+
return "No dependencies found. Run kirograph index first.";
|
|
2735
|
+
}
|
|
2736
|
+
const violationMap = /* @__PURE__ */ new Map();
|
|
2737
|
+
for (const v of violations) {
|
|
2738
|
+
violationMap.set(`${v.ecosystem}:${v.packageName}`, v.severity);
|
|
2739
|
+
}
|
|
2740
|
+
const lines = [`# License Report (${deps.length} dependencies)
|
|
2741
|
+
`];
|
|
2742
|
+
if (violations.length > 0) {
|
|
2743
|
+
lines.push("## Policy Violations\n");
|
|
2744
|
+
for (const v of violations) {
|
|
2745
|
+
lines.push(`${v.severity.toUpperCase()} ${v.packageName} [${v.ecosystem}] ${v.license}`);
|
|
2746
|
+
}
|
|
2747
|
+
lines.push("");
|
|
2748
|
+
}
|
|
2749
|
+
lines.push("## All Dependencies\n");
|
|
2750
|
+
lines.push("package | ecosystem | license | status");
|
|
2751
|
+
lines.push("------- | --------- | ------- | ------");
|
|
2752
|
+
for (const dep of deps) {
|
|
2753
|
+
const key = `${dep.ecosystem}:${dep.package_name}`;
|
|
2754
|
+
const violation = violationMap.get(key);
|
|
2755
|
+
const license = dep.license ?? "(unknown)";
|
|
2756
|
+
const status = violation ?? (dep.license ? "ok" : "unknown");
|
|
2757
|
+
lines.push(`${dep.package_name} | ${dep.ecosystem} | ${license} | ${status}`);
|
|
2758
|
+
}
|
|
2759
|
+
const denyCount = violations.filter((v) => v.severity === "deny").length;
|
|
2760
|
+
const warnCount = violations.filter((v) => v.severity === "warn").length;
|
|
2761
|
+
const unknownCount = deps.filter((d) => !d.license).length;
|
|
2762
|
+
lines.push("");
|
|
2763
|
+
if (denyCount > 0) lines.push(`${denyCount} denied license${denyCount !== 1 ? "s" : ""}`);
|
|
2764
|
+
if (warnCount > 0) lines.push(`${warnCount} license warning${warnCount !== 1 ? "s" : ""}`);
|
|
2765
|
+
if (unknownCount > 0) lines.push(`${unknownCount} unknown license${unknownCount !== 1 ? "s" : ""}`);
|
|
2766
|
+
if (denyCount === 0 && warnCount === 0) lines.push("No policy violations");
|
|
2767
|
+
return truncate(lines.join("\n"));
|
|
2768
|
+
}
|
|
2769
|
+
case "kirograph_attack_surface": {
|
|
2770
|
+
const { loadConfig } = await Promise.resolve().then(() => require("../config.js"));
|
|
2771
|
+
const projectRoot = cg.getProjectRoot();
|
|
2772
|
+
const config = await loadConfig(projectRoot);
|
|
2773
|
+
if (!config.enableSecurity) return "Security analysis is not enabled. Set enableSecurity: true in .kirograph/config.json and run kirograph index.";
|
|
2774
|
+
const db = cg.getDatabase();
|
|
2775
|
+
db.applySecuritySchema();
|
|
2776
|
+
const { AttackSurfaceAnalyzer } = await Promise.resolve().then(() => require("../security/attack-surface.js"));
|
|
2777
|
+
const analyzer = new AttackSurfaceAnalyzer(db);
|
|
2778
|
+
const result = await analyzer.analyze();
|
|
2779
|
+
if (result.totalRoutes === 0) {
|
|
2780
|
+
return "No route nodes found in the graph. Ensure the project has been indexed with architecture analysis enabled.";
|
|
2781
|
+
}
|
|
2782
|
+
const limit = clampLimit(args.limit, 20);
|
|
2783
|
+
const routes = args.publicOnly === true ? result.criticalPaths.filter((r) => r.exposureLevel === "public") : result.criticalPaths;
|
|
2784
|
+
const lines = [
|
|
2785
|
+
"# Attack Surface",
|
|
2786
|
+
"",
|
|
2787
|
+
`Total routes: ${result.totalRoutes} Public: ${result.publicRoutes} Authenticated: ${result.authenticatedRoutes} Routes with vulns: ${result.routesWithVulns}`,
|
|
2788
|
+
""
|
|
2789
|
+
];
|
|
2790
|
+
const displayed = routes.slice(0, limit);
|
|
2791
|
+
if (displayed.length === 0) {
|
|
2792
|
+
lines.push(args.publicOnly === true ? "No public routes with vulnerable dependencies found." : "No routes with vulnerable dependencies found.");
|
|
2793
|
+
} else {
|
|
2794
|
+
for (const entry of displayed) {
|
|
2795
|
+
const authTag = entry.isAuthenticated ? "[auth]" : "[public]";
|
|
2796
|
+
const riskTag = entry.riskScore > 0 ? ` risk=${entry.riskScore.toFixed(1)}` : "";
|
|
2797
|
+
lines.push(`${authTag} ${entry.route} (${entry.exposureLevel})${riskTag} ${entry.filePath}`);
|
|
2798
|
+
for (const dep of entry.vulnerableDeps.slice(0, 3)) {
|
|
2799
|
+
lines.push(` \u2514 ${dep.cveId} via ${dep.packageName} (${dep.hopCount} hop${dep.hopCount !== 1 ? "s" : ""}${dep.verdict ? `, ${dep.verdict}` : ""})`);
|
|
2800
|
+
}
|
|
2801
|
+
if (entry.vulnerableDeps.length > 3) {
|
|
2802
|
+
lines.push(` \u2514 \u2026and ${entry.vulnerableDeps.length - 3} more vulns`);
|
|
2803
|
+
}
|
|
2804
|
+
}
|
|
2805
|
+
if (routes.length > limit) {
|
|
2806
|
+
lines.push("", `\u2026and ${routes.length - limit} more routes (increase limit to see all)`);
|
|
2807
|
+
}
|
|
2808
|
+
}
|
|
2809
|
+
return truncate(lines.join("\n"));
|
|
2810
|
+
}
|
|
2811
|
+
case "kirograph_secrets": {
|
|
2812
|
+
const { loadConfig } = await Promise.resolve().then(() => require("../config.js"));
|
|
2813
|
+
const projectRoot = cg.getProjectRoot();
|
|
2814
|
+
const config = await loadConfig(projectRoot);
|
|
2815
|
+
if (!config.enableSecurity) return "Security analysis is not enabled. Set enableSecurity: true in .kirograph/config.json and run kirograph index.";
|
|
2816
|
+
const db = cg.getDatabase();
|
|
2817
|
+
db.applySecuritySchema();
|
|
2818
|
+
const { SecretsScanner } = await Promise.resolve().then(() => require("../security/secrets.js"));
|
|
2819
|
+
const scanner = new SecretsScanner(db, projectRoot);
|
|
2820
|
+
const result = await scanner.scan({ includeTests: args.includeTests === true });
|
|
2821
|
+
if (result.totalFindings === 0) {
|
|
2822
|
+
return `No secrets found. Scanned ${result.filesScanned} file${result.filesScanned !== 1 ? "s" : ""}.`;
|
|
2823
|
+
}
|
|
2824
|
+
let findings = result.findings;
|
|
2825
|
+
if (args.severity) {
|
|
2826
|
+
findings = findings.filter((f) => f.severity === args.severity);
|
|
2827
|
+
}
|
|
2828
|
+
if (findings.length === 0) {
|
|
2829
|
+
return `No secrets found matching severity "${args.severity}". Total findings (all severities): ${result.totalFindings}.`;
|
|
2830
|
+
}
|
|
2831
|
+
const lines = [
|
|
2832
|
+
`# Secrets Scan (${result.filesScanned} files scanned)`,
|
|
2833
|
+
"",
|
|
2834
|
+
`Found: ${result.totalFindings} Critical: ${result.criticalCount} High: ${result.highCount}`,
|
|
2835
|
+
""
|
|
2836
|
+
];
|
|
2837
|
+
for (const f of findings) {
|
|
2838
|
+
lines.push(`${f.severity.toUpperCase()} ${f.type}`);
|
|
2839
|
+
lines.push(` ${f.filePath}:${f.line}:${f.column} snippet: ${f.snippet}`);
|
|
2840
|
+
if (f.nodeName) {
|
|
2841
|
+
lines.push(` in function: ${f.nodeName}`);
|
|
2842
|
+
}
|
|
2843
|
+
if (f.entryPointCount > 0) {
|
|
2844
|
+
lines.push(` reachable from ${f.entryPointCount} entry point${f.entryPointCount !== 1 ? "s" : ""}`);
|
|
2845
|
+
}
|
|
2846
|
+
}
|
|
2847
|
+
return truncate(lines.join("\n"));
|
|
2848
|
+
}
|
|
2849
|
+
case "kirograph_security_flows": {
|
|
2850
|
+
const { loadConfig } = await Promise.resolve().then(() => require("../config.js"));
|
|
2851
|
+
const projectRoot = cg.getProjectRoot();
|
|
2852
|
+
const config = await loadConfig(projectRoot);
|
|
2853
|
+
if (!config.enableSecurity) return "Security analysis is not enabled. Set enableSecurity: true in .kirograph/config.json and run kirograph index.";
|
|
2854
|
+
const db = cg.getDatabase();
|
|
2855
|
+
db.applySecuritySchema();
|
|
2856
|
+
const { DataFlowAnalyzer } = await Promise.resolve().then(() => require("../security/data-flows.js"));
|
|
2857
|
+
const analyzer = new DataFlowAnalyzer(db);
|
|
2858
|
+
let findings = await analyzer.analyze();
|
|
2859
|
+
const typeFilter = args.type ?? "all";
|
|
2860
|
+
const TYPE_MAP = {
|
|
2861
|
+
sql: "sql-injection",
|
|
2862
|
+
eval: "dangerous-eval",
|
|
2863
|
+
deserialize: "unsafe-deserialize",
|
|
2864
|
+
path: "path-traversal",
|
|
2865
|
+
crypto: "hardcoded-crypto"
|
|
2866
|
+
};
|
|
2867
|
+
if (typeFilter !== "all") {
|
|
2868
|
+
const mapped = TYPE_MAP[typeFilter];
|
|
2869
|
+
if (!mapped) return `Invalid type filter "${typeFilter}". Use: sql, eval, deserialize, path, crypto, all`;
|
|
2870
|
+
findings = findings.filter((f) => f.type === mapped);
|
|
2871
|
+
}
|
|
2872
|
+
if (findings.length === 0) {
|
|
2873
|
+
return typeFilter === "all" ? "No dangerous data flows detected." : `No "${typeFilter}" findings detected.`;
|
|
2874
|
+
}
|
|
2875
|
+
const lines = [
|
|
2876
|
+
`# Security Flows (${findings.length} finding${findings.length !== 1 ? "s" : ""})`,
|
|
2877
|
+
""
|
|
2878
|
+
];
|
|
2879
|
+
for (const f of findings) {
|
|
2880
|
+
lines.push(`${f.severity.toUpperCase()} [${f.owaspCategory}] ${f.type}`);
|
|
2881
|
+
lines.push(` ${f.filePath}:${f.line} symbol: ${f.symbol}`);
|
|
2882
|
+
lines.push(` ${f.description}`);
|
|
2883
|
+
lines.push(` Fix: ${f.recommendation}`);
|
|
2884
|
+
lines.push("");
|
|
2885
|
+
}
|
|
2886
|
+
return truncate(lines.join("\n"));
|
|
2887
|
+
}
|
|
2888
|
+
case "kirograph_supply_chain": {
|
|
2889
|
+
const { loadConfig } = await Promise.resolve().then(() => require("../config.js"));
|
|
2890
|
+
const projectRoot = cg.getProjectRoot();
|
|
2891
|
+
const config = await loadConfig(projectRoot);
|
|
2892
|
+
if (!config.enableSecurity) return "Security analysis is not enabled. Set enableSecurity: true in .kirograph/config.json and run kirograph index.";
|
|
2893
|
+
const db = cg.getDatabase();
|
|
2894
|
+
db.applySecuritySchema();
|
|
2895
|
+
const { SupplyChainChecker } = await Promise.resolve().then(() => require("../security/supply-chain.js"));
|
|
2896
|
+
const checker = new SupplyChainChecker(db);
|
|
2897
|
+
const { results, errors } = await checker.checkAll();
|
|
2898
|
+
const RISK_ORDER = { critical: 0, high: 1, medium: 2, low: 3, unknown: 4 };
|
|
2899
|
+
const thresholdArg = args.threshold;
|
|
2900
|
+
const thresholdOrder = thresholdArg ? RISK_ORDER[thresholdArg] ?? 4 : 2;
|
|
2901
|
+
const filtered = results.filter((r) => (RISK_ORDER[r.riskLevel] ?? 4) <= thresholdOrder).sort((a, b) => (RISK_ORDER[a.riskLevel] ?? 4) - (RISK_ORDER[b.riskLevel] ?? 4));
|
|
2902
|
+
if (filtered.length === 0) {
|
|
2903
|
+
return `No supply chain risks found at threshold "${thresholdArg ?? "medium"}" or above. Checked ${results.length} dependencies.`;
|
|
2904
|
+
}
|
|
2905
|
+
const lines = [
|
|
2906
|
+
`# Supply Chain Health (${filtered.length} risks, threshold: ${thresholdArg ?? "medium"})`,
|
|
2907
|
+
""
|
|
2908
|
+
];
|
|
2909
|
+
for (const r of filtered) {
|
|
2910
|
+
const scoreStr = r.scorecardScore !== null ? ` scorecard=${r.scorecardScore.toFixed(1)}/10` : "";
|
|
2911
|
+
const maintainerStr = r.maintainerCount !== null ? ` maintainers=${r.maintainerCount}` : "";
|
|
2912
|
+
lines.push(`${r.riskLevel.toUpperCase()} ${r.packageName} (${r.ecosystem})${scoreStr}${maintainerStr}`);
|
|
2913
|
+
for (const reason of r.riskReasons) {
|
|
2914
|
+
lines.push(` \u2022 ${reason}`);
|
|
2915
|
+
}
|
|
2916
|
+
}
|
|
2917
|
+
if (errors.length > 0) {
|
|
2918
|
+
lines.push("", `\u26A0 ${errors.length} package${errors.length !== 1 ? "s" : ""} could not be checked (network errors)`);
|
|
2919
|
+
}
|
|
2920
|
+
return truncate(lines.join("\n"));
|
|
2921
|
+
}
|
|
2922
|
+
case "kirograph_dep_confusion": {
|
|
2923
|
+
const { loadConfig } = await Promise.resolve().then(() => require("../config.js"));
|
|
2924
|
+
const projectRoot = cg.getProjectRoot();
|
|
2925
|
+
const config = await loadConfig(projectRoot);
|
|
2926
|
+
if (!config.enableSecurity) return "Security analysis is not enabled. Set enableSecurity: true in .kirograph/config.json and run kirograph index.";
|
|
2927
|
+
const db = cg.getDatabase();
|
|
2928
|
+
db.applySecuritySchema();
|
|
2929
|
+
const { DepConfusionChecker } = await Promise.resolve().then(() => require("../security/dep-confusion.js"));
|
|
2930
|
+
const checker = new DepConfusionChecker(db);
|
|
2931
|
+
const findings = await checker.check();
|
|
2932
|
+
if (findings.length === 0) {
|
|
2933
|
+
return "No dependency confusion vulnerabilities detected.";
|
|
2934
|
+
}
|
|
2935
|
+
const RISK_ORDER = { critical: 0, high: 1, medium: 2 };
|
|
2936
|
+
const sorted = [...findings].sort((a, b) => (RISK_ORDER[a.riskLevel] ?? 3) - (RISK_ORDER[b.riskLevel] ?? 3));
|
|
2937
|
+
const lines = [
|
|
2938
|
+
`# Dependency Confusion (${findings.length} finding${findings.length !== 1 ? "s" : ""})`,
|
|
2939
|
+
""
|
|
2940
|
+
];
|
|
2941
|
+
for (const f of sorted) {
|
|
2942
|
+
lines.push(`${f.riskLevel.toUpperCase()} ${f.packageName} (${f.ecosystem})`);
|
|
2943
|
+
lines.push(` ${f.explanation}`);
|
|
2944
|
+
if (f.publicExists && f.publicVersion) {
|
|
2945
|
+
lines.push(` Public version: ${f.publicVersion}${f.publicPublishedAt ? ` published: ${f.publicPublishedAt}` : ""}`);
|
|
2946
|
+
}
|
|
2947
|
+
}
|
|
2948
|
+
return truncate(lines.join("\n"));
|
|
2949
|
+
}
|
|
2950
|
+
case "kirograph_remediation": {
|
|
2951
|
+
const { loadConfig } = await Promise.resolve().then(() => require("../config.js"));
|
|
2952
|
+
const projectRoot = cg.getProjectRoot();
|
|
2953
|
+
const config = await loadConfig(projectRoot);
|
|
2954
|
+
if (!config.enableSecurity) return "Security analysis is not enabled. Set enableSecurity: true in .kirograph/config.json and run kirograph index.";
|
|
2955
|
+
const db = cg.getDatabase();
|
|
2956
|
+
db.applySecuritySchema();
|
|
2957
|
+
const { RemediationTracker } = await Promise.resolve().then(() => require("../security/remediation.js"));
|
|
2958
|
+
const tracker = new RemediationTracker(db);
|
|
2959
|
+
let statuses = tracker.getStatus();
|
|
2960
|
+
if (args.overdueOnly === true) {
|
|
2961
|
+
statuses = statuses.filter((s) => s.isOverdue);
|
|
2962
|
+
}
|
|
2963
|
+
if (statuses.length === 0) {
|
|
2964
|
+
return args.overdueOnly === true ? "No overdue vulnerabilities found." : "No open vulnerabilities with SLA tracking data found.";
|
|
2965
|
+
}
|
|
2966
|
+
const SLA_ORDER = { overdue: 0, warning: 1, no_fix: 2, ok: 3 };
|
|
2967
|
+
statuses.sort((a, b) => {
|
|
2968
|
+
const slaA = SLA_ORDER[a.slaStatus] ?? 3;
|
|
2969
|
+
const slaB = SLA_ORDER[b.slaStatus] ?? 3;
|
|
2970
|
+
if (slaA !== slaB) return slaA - slaB;
|
|
2971
|
+
return (b.severity ?? 0) - (a.severity ?? 0);
|
|
2972
|
+
});
|
|
2973
|
+
const overdueCount = statuses.filter((s) => s.isOverdue).length;
|
|
2974
|
+
const warningCount = statuses.filter((s) => s.slaStatus === "warning").length;
|
|
2975
|
+
const lines = [
|
|
2976
|
+
`# Remediation SLA (${statuses.length} open${overdueCount > 0 ? `, ${overdueCount} overdue` : ""}${warningCount > 0 ? `, ${warningCount} warning` : ""})`,
|
|
2977
|
+
""
|
|
2978
|
+
];
|
|
2979
|
+
for (const s of statuses) {
|
|
2980
|
+
const severityLabel = s.severity == null ? "UNKNOWN" : s.severity >= 9 ? "CRITICAL" : s.severity >= 7 ? "HIGH" : s.severity >= 4 ? "MEDIUM" : "LOW";
|
|
2981
|
+
const slaTag = s.slaStatus === "overdue" ? "[OVERDUE]" : s.slaStatus === "warning" ? "[WARNING]" : s.slaStatus === "no_fix" ? "[NO_FIX]" : "[OK]";
|
|
2982
|
+
lines.push(`${slaTag} ${severityLabel} ${s.cveId} ${s.packageName}`);
|
|
2983
|
+
if (s.daysOpen !== null) lines.push(` Open for ${s.daysOpen} day${s.daysOpen !== 1 ? "s" : ""}`);
|
|
2984
|
+
if (s.daysWithFixAvailable !== null) lines.push(` Fix available for ${s.daysWithFixAvailable} day${s.daysWithFixAvailable !== 1 ? "s" : ""}`);
|
|
2985
|
+
if (s.slaDeadline !== null) {
|
|
2986
|
+
const deadlineDate = new Date(s.slaDeadline).toISOString().slice(0, 10);
|
|
2987
|
+
lines.push(` SLA deadline: ${deadlineDate}`);
|
|
2988
|
+
}
|
|
2989
|
+
}
|
|
2990
|
+
return truncate(lines.join("\n"));
|
|
2991
|
+
}
|
|
1410
2992
|
default:
|
|
1411
2993
|
return `Unknown tool: ${toolName}`;
|
|
1412
2994
|
}
|