kirograph 0.17.0 → 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.
Files changed (329) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +103 -1822
  3. package/dist/architecture/layers/dart.js +112 -0
  4. package/dist/architecture/layers/dart.js.map +7 -0
  5. package/dist/architecture/layers/index.js +3 -1
  6. package/dist/architecture/layers/index.js.map +2 -2
  7. package/dist/bin/banner.js +74 -6
  8. package/dist/bin/banner.js.map +2 -2
  9. package/dist/bin/commands/attack-surface.js +157 -0
  10. package/dist/bin/commands/attack-surface.js.map +7 -0
  11. package/dist/bin/commands/benchmark.js +206 -0
  12. package/dist/bin/commands/benchmark.js.map +7 -0
  13. package/dist/bin/commands/budget.js +70 -0
  14. package/dist/bin/commands/budget.js.map +7 -0
  15. package/dist/bin/commands/communities.js +77 -0
  16. package/dist/bin/commands/communities.js.map +7 -0
  17. package/dist/bin/commands/dep-confusion.js +119 -0
  18. package/dist/bin/commands/dep-confusion.js.map +7 -0
  19. package/dist/bin/commands/export.js +304 -10
  20. package/dist/bin/commands/export.js.map +2 -2
  21. package/dist/bin/commands/flows.js +97 -0
  22. package/dist/bin/commands/flows.js.map +7 -0
  23. package/dist/bin/commands/help.js +382 -101
  24. package/dist/bin/commands/help.js.map +2 -2
  25. package/dist/bin/commands/install.js +90 -8
  26. package/dist/bin/commands/install.js.map +2 -2
  27. package/dist/bin/commands/licenses.js +201 -0
  28. package/dist/bin/commands/licenses.js.map +7 -0
  29. package/dist/bin/commands/reachability.js +160 -0
  30. package/dist/bin/commands/reachability.js.map +7 -0
  31. package/dist/bin/commands/read.js +100 -0
  32. package/dist/bin/commands/read.js.map +7 -0
  33. package/dist/bin/commands/refactor.js +116 -0
  34. package/dist/bin/commands/refactor.js.map +7 -0
  35. package/dist/bin/commands/remediation.js +133 -0
  36. package/dist/bin/commands/remediation.js.map +7 -0
  37. package/dist/bin/commands/sbom.js +76 -0
  38. package/dist/bin/commands/sbom.js.map +7 -0
  39. package/dist/bin/commands/security-ci-report.js +267 -0
  40. package/dist/bin/commands/security-ci-report.js.map +7 -0
  41. package/dist/bin/commands/security-export.js +1349 -0
  42. package/dist/bin/commands/security-export.js.map +7 -0
  43. package/dist/bin/commands/security-flows.js +126 -0
  44. package/dist/bin/commands/security-flows.js.map +7 -0
  45. package/dist/bin/commands/security-secrets.js +170 -0
  46. package/dist/bin/commands/security-secrets.js.map +7 -0
  47. package/dist/bin/commands/security.js +193 -0
  48. package/dist/bin/commands/security.js.map +7 -0
  49. package/dist/bin/commands/staleness.js +144 -0
  50. package/dist/bin/commands/staleness.js.map +7 -0
  51. package/dist/bin/commands/status.js +102 -2
  52. package/dist/bin/commands/status.js.map +2 -2
  53. package/dist/bin/commands/supply-chain.js +138 -0
  54. package/dist/bin/commands/supply-chain.js.map +7 -0
  55. package/dist/bin/commands/uninit.js +109 -60
  56. package/dist/bin/commands/uninit.js.map +2 -2
  57. package/dist/bin/commands/vex.js +76 -0
  58. package/dist/bin/commands/vex.js.map +7 -0
  59. package/dist/bin/commands/vuln-suppress.js +95 -0
  60. package/dist/bin/commands/vuln-suppress.js.map +7 -0
  61. package/dist/bin/commands/vulns.js +382 -0
  62. package/dist/bin/commands/vulns.js.map +7 -0
  63. package/dist/bin/installer/auto-detect.js +125 -0
  64. package/dist/bin/installer/auto-detect.js.map +7 -0
  65. package/dist/bin/installer/cli-agent.js +28 -5
  66. package/dist/bin/installer/cli-agent.js.map +2 -2
  67. package/dist/bin/installer/common.js +47 -0
  68. package/dist/bin/installer/common.js.map +2 -2
  69. package/dist/bin/installer/config-prompt.js +12 -1
  70. package/dist/bin/installer/config-prompt.js.map +2 -2
  71. package/dist/bin/installer/detect.js +309 -0
  72. package/dist/bin/installer/detect.js.map +7 -0
  73. package/dist/bin/installer/index.js +9 -1
  74. package/dist/bin/installer/index.js.map +2 -2
  75. package/dist/bin/installer/instructions.js +195 -3
  76. package/dist/bin/installer/instructions.js.map +2 -2
  77. package/dist/bin/installer/steering.js +407 -0
  78. package/dist/bin/installer/steering.js.map +2 -2
  79. package/dist/bin/installer/targets/aider.js +3 -3
  80. package/dist/bin/installer/targets/aider.js.map +2 -2
  81. package/dist/bin/installer/targets/amp.js +3 -3
  82. package/dist/bin/installer/targets/amp.js.map +2 -2
  83. package/dist/bin/installer/targets/antigravity.js +35 -6
  84. package/dist/bin/installer/targets/antigravity.js.map +2 -2
  85. package/dist/bin/installer/targets/augment.js +3 -3
  86. package/dist/bin/installer/targets/augment.js.map +2 -2
  87. package/dist/bin/installer/targets/claude.js +49 -5
  88. package/dist/bin/installer/targets/claude.js.map +2 -2
  89. package/dist/bin/installer/targets/cline.js +32 -6
  90. package/dist/bin/installer/targets/cline.js.map +2 -2
  91. package/dist/bin/installer/targets/codex.js +64 -12
  92. package/dist/bin/installer/targets/codex.js.map +2 -2
  93. package/dist/bin/installer/targets/continue.js +2 -2
  94. package/dist/bin/installer/targets/continue.js.map +2 -2
  95. package/dist/bin/installer/targets/copilot-cli.js +101 -0
  96. package/dist/bin/installer/targets/copilot-cli.js.map +7 -0
  97. package/dist/bin/installer/targets/copilot.js +31 -5
  98. package/dist/bin/installer/targets/copilot.js.map +2 -2
  99. package/dist/bin/installer/targets/cursor.js +4 -4
  100. package/dist/bin/installer/targets/cursor.js.map +2 -2
  101. package/dist/bin/installer/targets/devin.js +2 -2
  102. package/dist/bin/installer/targets/devin.js.map +2 -2
  103. package/dist/bin/installer/targets/gemini-cli.js +2 -2
  104. package/dist/bin/installer/targets/gemini-cli.js.map +2 -2
  105. package/dist/bin/installer/targets/generic.js +2 -14
  106. package/dist/bin/installer/targets/generic.js.map +2 -2
  107. package/dist/bin/installer/targets/goose.js +3 -3
  108. package/dist/bin/installer/targets/goose.js.map +2 -2
  109. package/dist/bin/installer/targets/index.js +250 -0
  110. package/dist/bin/installer/targets/index.js.map +2 -2
  111. package/dist/bin/installer/targets/junie.js +2 -2
  112. package/dist/bin/installer/targets/junie.js.map +2 -2
  113. package/dist/bin/installer/targets/kilo.js +2 -2
  114. package/dist/bin/installer/targets/kilo.js.map +2 -2
  115. package/dist/bin/installer/targets/kiro.js +3 -3
  116. package/dist/bin/installer/targets/kiro.js.map +2 -2
  117. package/dist/bin/installer/targets/opencode.js +2 -2
  118. package/dist/bin/installer/targets/opencode.js.map +2 -2
  119. package/dist/bin/installer/targets/openhands.js +3 -3
  120. package/dist/bin/installer/targets/openhands.js.map +2 -2
  121. package/dist/bin/installer/targets/qoder.js +91 -0
  122. package/dist/bin/installer/targets/qoder.js.map +7 -0
  123. package/dist/bin/installer/targets/qwen.js +96 -0
  124. package/dist/bin/installer/targets/qwen.js.map +7 -0
  125. package/dist/bin/installer/targets/replit.js +3 -3
  126. package/dist/bin/installer/targets/replit.js.map +2 -2
  127. package/dist/bin/installer/targets/roo.js +2 -2
  128. package/dist/bin/installer/targets/roo.js.map +2 -2
  129. package/dist/bin/installer/targets/tabnine.js +3 -3
  130. package/dist/bin/installer/targets/tabnine.js.map +2 -2
  131. package/dist/bin/installer/targets/trae.js +5 -5
  132. package/dist/bin/installer/targets/trae.js.map +2 -2
  133. package/dist/bin/installer/targets/warp.js +2 -2
  134. package/dist/bin/installer/targets/warp.js.map +2 -2
  135. package/dist/bin/installer/targets/windsurf.js +35 -6
  136. package/dist/bin/installer/targets/windsurf.js.map +2 -2
  137. package/dist/bin/kirograph.js +72 -6
  138. package/dist/bin/kirograph.js.map +3 -3
  139. package/dist/compression/tracker.js +80 -0
  140. package/dist/compression/tracker.js.map +2 -2
  141. package/dist/config.js +90 -3
  142. package/dist/config.js.map +2 -2
  143. package/dist/core/pipeline.js +36 -2
  144. package/dist/core/pipeline.js.map +2 -2
  145. package/dist/db/database.js +56 -5
  146. package/dist/db/database.js.map +2 -2
  147. package/dist/db/memory-schema.sql +5 -1
  148. package/dist/db/schema.sql +3 -1
  149. package/dist/db/security-schema.sql +72 -0
  150. package/dist/extraction/extractor.js +81 -0
  151. package/dist/extraction/extractor.js.map +2 -2
  152. package/dist/extraction/grammars.js +32 -8
  153. package/dist/extraction/grammars.js.map +2 -2
  154. package/dist/extraction/languages.js +43 -1
  155. package/dist/extraction/languages.js.map +2 -2
  156. package/dist/extraction/notebook.js +300 -0
  157. package/dist/extraction/notebook.js.map +7 -0
  158. package/dist/extraction/wasm/tree-sitter-astro.wasm +0 -0
  159. package/dist/extraction/wasm/tree-sitter-gdscript.wasm +0 -0
  160. package/dist/extraction/wasm/tree-sitter-julia.wasm +0 -0
  161. package/dist/extraction/wasm/tree-sitter-nix.wasm +0 -0
  162. package/dist/extraction/wasm/tree-sitter-perl.wasm +0 -0
  163. package/dist/extraction/wasm/tree-sitter-powershell.wasm +0 -0
  164. package/dist/extraction/wasm/tree-sitter-r.wasm +0 -0
  165. package/dist/extraction/wasm/tree-sitter-sql.wasm +0 -0
  166. package/dist/extraction/wasm/tree-sitter-verilog.wasm +0 -0
  167. package/dist/frameworks/flutter.js +130 -0
  168. package/dist/frameworks/flutter.js.map +7 -0
  169. package/dist/frameworks/index.js +7 -1
  170. package/dist/frameworks/index.js.map +3 -3
  171. package/dist/graph/communities.js +317 -0
  172. package/dist/graph/communities.js.map +7 -0
  173. package/dist/graph/flows.js +138 -0
  174. package/dist/graph/flows.js.map +7 -0
  175. package/dist/graph/refactor.js +155 -0
  176. package/dist/graph/refactor.js.map +7 -0
  177. package/dist/index.js +1 -1
  178. package/dist/index.js.map +2 -2
  179. package/dist/mcp/cache.js +172 -0
  180. package/dist/mcp/cache.js.map +7 -0
  181. package/dist/mcp/read-modes.js +295 -0
  182. package/dist/mcp/read-modes.js.map +7 -0
  183. package/dist/mcp/server.js +2 -1
  184. package/dist/mcp/server.js.map +2 -2
  185. package/dist/mcp/tool-names.js +21 -1
  186. package/dist/mcp/tool-names.js.map +2 -2
  187. package/dist/mcp/tools.js +1160 -1
  188. package/dist/mcp/tools.js.map +3 -3
  189. package/dist/memory/database.js +32 -2
  190. package/dist/memory/database.js.map +2 -2
  191. package/dist/memory/types.js.map +1 -1
  192. package/dist/resolution/bridges/android-rn.js +298 -0
  193. package/dist/resolution/bridges/android-rn.js.map +7 -0
  194. package/dist/resolution/bridges/expo-modules.js +155 -0
  195. package/dist/resolution/bridges/expo-modules.js.map +7 -0
  196. package/dist/resolution/bridges/flutter-channel.js +385 -0
  197. package/dist/resolution/bridges/flutter-channel.js.map +7 -0
  198. package/dist/resolution/bridges/index.js +80 -0
  199. package/dist/resolution/bridges/index.js.map +7 -0
  200. package/dist/resolution/bridges/native-events.js +188 -0
  201. package/dist/resolution/bridges/native-events.js.map +7 -0
  202. package/dist/resolution/bridges/native-views.js +244 -0
  203. package/dist/resolution/bridges/native-views.js.map +7 -0
  204. package/dist/resolution/bridges/react-native.js +161 -0
  205. package/dist/resolution/bridges/react-native.js.map +7 -0
  206. package/dist/resolution/bridges/swift-objc.js +227 -0
  207. package/dist/resolution/bridges/swift-objc.js.map +7 -0
  208. package/dist/resolution/bridges/turbomodules.js +216 -0
  209. package/dist/resolution/bridges/turbomodules.js.map +7 -0
  210. package/dist/resolution/index.js +61 -2
  211. package/dist/resolution/index.js.map +2 -2
  212. package/dist/security/attack-surface.js +164 -0
  213. package/dist/security/attack-surface.js.map +7 -0
  214. package/dist/security/context-warnings.js +123 -0
  215. package/dist/security/context-warnings.js.map +7 -0
  216. package/dist/security/context-warnings.test.js +300 -0
  217. package/dist/security/context-warnings.test.js.map +7 -0
  218. package/dist/security/data-flows.js +228 -0
  219. package/dist/security/data-flows.js.map +7 -0
  220. package/dist/security/dep-confusion.js +255 -0
  221. package/dist/security/dep-confusion.js.map +7 -0
  222. package/dist/security/errors.js +51 -0
  223. package/dist/security/errors.js.map +7 -0
  224. package/dist/security/export/fix-suggestions.js +58 -0
  225. package/dist/security/export/fix-suggestions.js.map +7 -0
  226. package/dist/security/export/fix-suggestions.test.js +74 -0
  227. package/dist/security/export/fix-suggestions.test.js.map +7 -0
  228. package/dist/security/export/sbom.js +230 -0
  229. package/dist/security/export/sbom.js.map +7 -0
  230. package/dist/security/export/sbom.test.js +288 -0
  231. package/dist/security/export/sbom.test.js.map +7 -0
  232. package/dist/security/export/serialization.js +142 -0
  233. package/dist/security/export/serialization.js.map +7 -0
  234. package/dist/security/export/serialization.test.js +310 -0
  235. package/dist/security/export/serialization.test.js.map +7 -0
  236. package/dist/security/export/vex.js +220 -0
  237. package/dist/security/export/vex.js.map +7 -0
  238. package/dist/security/export/vex.test.js +278 -0
  239. package/dist/security/export/vex.test.js.map +7 -0
  240. package/dist/security/index.js +67 -0
  241. package/dist/security/index.js.map +7 -0
  242. package/dist/security/integrator-transitives.test.js +359 -0
  243. package/dist/security/integrator-transitives.test.js.map +7 -0
  244. package/dist/security/integrator.js +491 -0
  245. package/dist/security/integrator.js.map +7 -0
  246. package/dist/security/integrator.test.js +237 -0
  247. package/dist/security/integrator.test.js.map +7 -0
  248. package/dist/security/license.js +70 -0
  249. package/dist/security/license.js.map +7 -0
  250. package/dist/security/manifest/adapter.js +243 -0
  251. package/dist/security/manifest/adapter.js.map +7 -0
  252. package/dist/security/manifest/adapter.test.js +272 -0
  253. package/dist/security/manifest/adapter.test.js.map +7 -0
  254. package/dist/security/manifest/parser.js +376 -0
  255. package/dist/security/manifest/parser.js.map +7 -0
  256. package/dist/security/manifest/parser.test.js +110 -0
  257. package/dist/security/manifest/parser.test.js.map +7 -0
  258. package/dist/security/manifest/plugins/cargo.js +233 -0
  259. package/dist/security/manifest/plugins/cargo.js.map +7 -0
  260. package/dist/security/manifest/plugins/cargo.test.js +304 -0
  261. package/dist/security/manifest/plugins/cargo.test.js.map +7 -0
  262. package/dist/security/manifest/plugins/composer.js +152 -0
  263. package/dist/security/manifest/plugins/composer.js.map +7 -0
  264. package/dist/security/manifest/plugins/go.js +146 -0
  265. package/dist/security/manifest/plugins/go.js.map +7 -0
  266. package/dist/security/manifest/plugins/go.test.js +341 -0
  267. package/dist/security/manifest/plugins/go.test.js.map +7 -0
  268. package/dist/security/manifest/plugins/gradle.js +159 -0
  269. package/dist/security/manifest/plugins/gradle.js.map +7 -0
  270. package/dist/security/manifest/plugins/hex.js +159 -0
  271. package/dist/security/manifest/plugins/hex.js.map +7 -0
  272. package/dist/security/manifest/plugins/maven.js +128 -0
  273. package/dist/security/manifest/plugins/maven.js.map +7 -0
  274. package/dist/security/manifest/plugins/maven.test.js +389 -0
  275. package/dist/security/manifest/plugins/maven.test.js.map +7 -0
  276. package/dist/security/manifest/plugins/npm.js +284 -0
  277. package/dist/security/manifest/plugins/npm.js.map +7 -0
  278. package/dist/security/manifest/plugins/npm.test.js +298 -0
  279. package/dist/security/manifest/plugins/npm.test.js.map +7 -0
  280. package/dist/security/manifest/plugins/nuget.js +186 -0
  281. package/dist/security/manifest/plugins/nuget.js.map +7 -0
  282. package/dist/security/manifest/plugins/pip.js +123 -0
  283. package/dist/security/manifest/plugins/pip.js.map +7 -0
  284. package/dist/security/manifest/plugins/pip.test.js +208 -0
  285. package/dist/security/manifest/plugins/pip.test.js.map +7 -0
  286. package/dist/security/manifest/plugins/pubspec.js +212 -0
  287. package/dist/security/manifest/plugins/pubspec.js.map +7 -0
  288. package/dist/security/manifest/plugins/pyproject.js +261 -0
  289. package/dist/security/manifest/plugins/pyproject.js.map +7 -0
  290. package/dist/security/manifest/plugins/rubygems.js +185 -0
  291. package/dist/security/manifest/plugins/rubygems.js.map +7 -0
  292. package/dist/security/manifest/plugins/swift.js +200 -0
  293. package/dist/security/manifest/plugins/swift.js.map +7 -0
  294. package/dist/security/owasp.js +104 -0
  295. package/dist/security/owasp.js.map +7 -0
  296. package/dist/security/pipeline.js +125 -0
  297. package/dist/security/pipeline.js.map +7 -0
  298. package/dist/security/reachability.js +276 -0
  299. package/dist/security/reachability.js.map +7 -0
  300. package/dist/security/reachability.test.js +394 -0
  301. package/dist/security/reachability.test.js.map +7 -0
  302. package/dist/security/remediation.js +126 -0
  303. package/dist/security/remediation.js.map +7 -0
  304. package/dist/security/secrets.js +176 -0
  305. package/dist/security/secrets.js.map +7 -0
  306. package/dist/security/staleness.js +231 -0
  307. package/dist/security/staleness.js.map +7 -0
  308. package/dist/security/supply-chain.js +301 -0
  309. package/dist/security/supply-chain.js.map +7 -0
  310. package/dist/security/suppressions.js +106 -0
  311. package/dist/security/suppressions.js.map +7 -0
  312. package/dist/security/types.js +17 -0
  313. package/dist/security/types.js.map +7 -0
  314. package/dist/security/vuln/client.js +355 -0
  315. package/dist/security/vuln/client.js.map +7 -0
  316. package/dist/security/vuln/client.test.js +288 -0
  317. package/dist/security/vuln/client.test.js.map +7 -0
  318. package/dist/security/vuln/epss-client.js +95 -0
  319. package/dist/security/vuln/epss-client.js.map +7 -0
  320. package/dist/security/vuln/index.js +29 -0
  321. package/dist/security/vuln/index.js.map +7 -0
  322. package/dist/security/vuln/osv-adapter.js +321 -0
  323. package/dist/security/vuln/osv-adapter.js.map +7 -0
  324. package/dist/security/vuln/osv-adapter.test.js +391 -0
  325. package/dist/security/vuln/osv-adapter.test.js.map +7 -0
  326. package/dist/security/vuln/types.js +17 -0
  327. package/dist/security/vuln/types.js.map +7 -0
  328. package/dist/types.js.map +2 -2
  329. package/package.json +2 -1
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"]
@@ -586,6 +670,184 @@ const tools = [
586
670
  },
587
671
  required: ["dataset"]
588
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
+ }
589
851
  }
590
852
  ];
591
853
  class ToolHandler {
@@ -639,6 +901,7 @@ class ToolHandler {
639
901
  async getConnection(projectPath) {
640
902
  if (!projectPath) return this.defaultCg;
641
903
  const resolved = path.resolve(projectPath);
904
+ if (!path.isAbsolute(resolved)) return null;
642
905
  if (this.connections.has(resolved)) return this.connections.get(resolved);
643
906
  try {
644
907
  const cg = await import__.default.open(resolved);
@@ -769,6 +1032,69 @@ class ToolHandler {
769
1032
  }
770
1033
  return lines.join("\n");
771
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
+ }
772
1098
  const cg = await this.getConnection(args.projectPath);
773
1099
  if (!cg) return "KiroGraph not initialized. Run `kirograph init` in your project first.";
774
1100
  writeSessionMarker(cg.getProjectRoot());
@@ -906,6 +1232,51 @@ class ToolHandler {
906
1232
  }
907
1233
  } catch {
908
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
+ }
909
1280
  return lines.join("\n");
910
1281
  }
911
1282
  case "kirograph_callers": {
@@ -1035,6 +1406,23 @@ class ToolHandler {
1035
1406
  }
1036
1407
  } catch {
1037
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
+ }
1038
1426
  const threshold = stats.syncWarningThreshold ?? 10;
1039
1427
  const pendingFiles = stats.pendingFiles ?? 0;
1040
1428
  const syncRunning = stats.syncRunning ?? false;
@@ -1065,6 +1453,7 @@ class ToolHandler {
1065
1453
  archLine,
1066
1454
  docsLine,
1067
1455
  dataLine,
1456
+ securityLine,
1068
1457
  ` DB size: ${dbMb} MB`,
1069
1458
  ...semanticLines,
1070
1459
  ...syncLines,
@@ -1386,6 +1775,122 @@ class ToolHandler {
1386
1775
  }
1387
1776
  return lines.join("\n");
1388
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
+ }
1389
1894
  // ── Memory tools ────────────────────────────────────────────────────────
1390
1895
  case "kirograph_mem_search": {
1391
1896
  const { loadConfig } = await Promise.resolve().then(() => require("../config.js"));
@@ -1400,7 +1905,8 @@ class ToolHandler {
1400
1905
  const results = await mem.search(args.query, {
1401
1906
  limit: args.limit ?? 10,
1402
1907
  kind: args.kind,
1403
- sessionId: args.sessionId
1908
+ sessionId: args.sessionId,
1909
+ asOf: args.asOf
1404
1910
  });
1405
1911
  if (results.length === 0) return `No memory observations found for "${args.query}".`;
1406
1912
  return results.map((r, i) => {
@@ -1830,6 +2336,659 @@ ${lines.join("\n")}`;
1830
2336
 
1831
2337
  ${lines.join("\n")}`;
1832
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
+ }
1833
2992
  default:
1834
2993
  return `Unknown tool: ${toolName}`;
1835
2994
  }