kirograph 0.27.1 → 0.28.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 (222) hide show
  1. package/README.md +35 -2
  2. package/dist/bin/commands/annotations.js +56 -0
  3. package/dist/bin/commands/annotations.js.map +7 -0
  4. package/dist/bin/commands/ast-rewrite.js +55 -0
  5. package/dist/bin/commands/ast-rewrite.js.map +7 -0
  6. package/dist/bin/commands/bench.js +103 -0
  7. package/dist/bin/commands/bench.js.map +7 -0
  8. package/dist/bin/commands/branch.js +133 -0
  9. package/dist/bin/commands/branch.js.map +7 -0
  10. package/dist/bin/commands/callees.js +76 -0
  11. package/dist/bin/commands/callees.js.map +7 -0
  12. package/dist/bin/commands/callers.js +76 -0
  13. package/dist/bin/commands/callers.js.map +7 -0
  14. package/dist/bin/commands/changelog.js +55 -0
  15. package/dist/bin/commands/changelog.js.map +7 -0
  16. package/dist/bin/commands/circular-deps.js +68 -0
  17. package/dist/bin/commands/circular-deps.js.map +7 -0
  18. package/dist/bin/commands/commit-context.js +55 -0
  19. package/dist/bin/commands/commit-context.js.map +7 -0
  20. package/dist/bin/commands/complexity.js +60 -0
  21. package/dist/bin/commands/complexity.js.map +7 -0
  22. package/dist/bin/commands/cost.js +146 -0
  23. package/dist/bin/commands/cost.js.map +7 -0
  24. package/dist/bin/commands/dependency-depth.js +55 -0
  25. package/dist/bin/commands/dependency-depth.js.map +7 -0
  26. package/dist/bin/commands/diff-context.js +55 -0
  27. package/dist/bin/commands/diff-context.js.map +7 -0
  28. package/dist/bin/commands/distribution.js +56 -0
  29. package/dist/bin/commands/distribution.js.map +7 -0
  30. package/dist/bin/commands/doc-coverage.js +56 -0
  31. package/dist/bin/commands/doc-coverage.js.map +7 -0
  32. package/dist/bin/commands/doctor.js +202 -0
  33. package/dist/bin/commands/doctor.js.map +7 -0
  34. package/dist/bin/commands/dsm.js +56 -0
  35. package/dist/bin/commands/dsm.js.map +7 -0
  36. package/dist/bin/commands/gini.js +55 -0
  37. package/dist/bin/commands/gini.js.map +7 -0
  38. package/dist/bin/commands/god-class.js +56 -0
  39. package/dist/bin/commands/god-class.js.map +7 -0
  40. package/dist/bin/commands/health.js +55 -0
  41. package/dist/bin/commands/health.js.map +7 -0
  42. package/dist/bin/commands/help.js +224 -45
  43. package/dist/bin/commands/help.js.map +2 -2
  44. package/dist/bin/commands/impact.js +76 -0
  45. package/dist/bin/commands/impact.js.map +7 -0
  46. package/dist/bin/commands/inheritance-depth.js +56 -0
  47. package/dist/bin/commands/inheritance-depth.js.map +7 -0
  48. package/dist/bin/commands/insert-at.js +55 -0
  49. package/dist/bin/commands/insert-at.js.map +7 -0
  50. package/dist/bin/commands/largest.js +56 -0
  51. package/dist/bin/commands/largest.js.map +7 -0
  52. package/dist/bin/commands/manifest.js +158 -0
  53. package/dist/bin/commands/manifest.js.map +7 -0
  54. package/dist/bin/commands/module-api.js +57 -0
  55. package/dist/bin/commands/module-api.js.map +7 -0
  56. package/dist/bin/commands/monitor.js +91 -0
  57. package/dist/bin/commands/monitor.js.map +7 -0
  58. package/dist/bin/commands/multi-replace.js +66 -0
  59. package/dist/bin/commands/multi-replace.js.map +7 -0
  60. package/dist/bin/commands/pr-context.js +55 -0
  61. package/dist/bin/commands/pr-context.js.map +7 -0
  62. package/dist/bin/commands/query.js +19 -1
  63. package/dist/bin/commands/query.js.map +3 -3
  64. package/dist/bin/commands/rank.js +56 -0
  65. package/dist/bin/commands/rank.js.map +7 -0
  66. package/dist/bin/commands/read.js +1 -1
  67. package/dist/bin/commands/read.js.map +2 -2
  68. package/dist/bin/commands/recursion.js +56 -0
  69. package/dist/bin/commands/recursion.js.map +7 -0
  70. package/dist/bin/commands/rename-preview.js +56 -0
  71. package/dist/bin/commands/rename-preview.js.map +7 -0
  72. package/dist/bin/commands/session.js +60 -0
  73. package/dist/bin/commands/session.js.map +7 -0
  74. package/dist/bin/commands/simplify-scan.js +56 -0
  75. package/dist/bin/commands/simplify-scan.js.map +7 -0
  76. package/dist/bin/commands/str-replace.js +55 -0
  77. package/dist/bin/commands/str-replace.js.map +7 -0
  78. package/dist/bin/commands/test-coverage.js +59 -0
  79. package/dist/bin/commands/test-coverage.js.map +7 -0
  80. package/dist/bin/commands/test-map.js +56 -0
  81. package/dist/bin/commands/test-map.js.map +7 -0
  82. package/dist/bin/commands/test-risk.js +59 -0
  83. package/dist/bin/commands/test-risk.js.map +7 -0
  84. package/dist/bin/commands/type-hierarchy.js +76 -0
  85. package/dist/bin/commands/type-hierarchy.js.map +7 -0
  86. package/dist/bin/commands/unused-imports.js +55 -0
  87. package/dist/bin/commands/unused-imports.js.map +7 -0
  88. package/dist/bin/commands/upgrade.js +78 -0
  89. package/dist/bin/commands/upgrade.js.map +7 -0
  90. package/dist/bin/installer/common.js +19 -9
  91. package/dist/bin/installer/common.js.map +2 -2
  92. package/dist/bin/installer/config-prompt.js +147 -12
  93. package/dist/bin/installer/config-prompt.js.map +3 -3
  94. package/dist/bin/installer/index.js +62 -6
  95. package/dist/bin/installer/index.js.map +2 -2
  96. package/dist/bin/installer/instructions.js +14 -25
  97. package/dist/bin/installer/instructions.js.map +2 -2
  98. package/dist/bin/installer/mcp.js.map +2 -2
  99. package/dist/bin/installer/steering.js +569 -100
  100. package/dist/bin/installer/steering.js.map +2 -2
  101. package/dist/bin/installer/targets/aider.js +3 -3
  102. package/dist/bin/installer/targets/aider.js.map +2 -2
  103. package/dist/bin/installer/targets/amp.js +3 -3
  104. package/dist/bin/installer/targets/amp.js.map +2 -2
  105. package/dist/bin/installer/targets/antigravity.js +4 -4
  106. package/dist/bin/installer/targets/antigravity.js.map +2 -2
  107. package/dist/bin/installer/targets/augment.js +3 -3
  108. package/dist/bin/installer/targets/augment.js.map +2 -2
  109. package/dist/bin/installer/targets/claude.js +3 -3
  110. package/dist/bin/installer/targets/claude.js.map +2 -2
  111. package/dist/bin/installer/targets/cline.js +4 -4
  112. package/dist/bin/installer/targets/cline.js.map +2 -2
  113. package/dist/bin/installer/targets/codex.js +4 -4
  114. package/dist/bin/installer/targets/codex.js.map +2 -2
  115. package/dist/bin/installer/targets/continue.js +4 -4
  116. package/dist/bin/installer/targets/continue.js.map +2 -2
  117. package/dist/bin/installer/targets/copilot-cli.js +4 -4
  118. package/dist/bin/installer/targets/copilot-cli.js.map +2 -2
  119. package/dist/bin/installer/targets/copilot.js +4 -4
  120. package/dist/bin/installer/targets/copilot.js.map +2 -2
  121. package/dist/bin/installer/targets/cursor.js +5 -5
  122. package/dist/bin/installer/targets/cursor.js.map +2 -2
  123. package/dist/bin/installer/targets/devin.js +4 -4
  124. package/dist/bin/installer/targets/devin.js.map +2 -2
  125. package/dist/bin/installer/targets/gemini-cli.js +4 -4
  126. package/dist/bin/installer/targets/gemini-cli.js.map +2 -2
  127. package/dist/bin/installer/targets/generic.js +2 -2
  128. package/dist/bin/installer/targets/generic.js.map +2 -2
  129. package/dist/bin/installer/targets/goose.js +3 -4
  130. package/dist/bin/installer/targets/goose.js.map +3 -3
  131. package/dist/bin/installer/targets/index.js.map +1 -1
  132. package/dist/bin/installer/targets/junie.js +4 -4
  133. package/dist/bin/installer/targets/junie.js.map +2 -2
  134. package/dist/bin/installer/targets/kilo.js +4 -4
  135. package/dist/bin/installer/targets/kilo.js.map +2 -2
  136. package/dist/bin/installer/targets/kiro.js +27 -3
  137. package/dist/bin/installer/targets/kiro.js.map +2 -2
  138. package/dist/bin/installer/targets/opencode.js +3 -3
  139. package/dist/bin/installer/targets/opencode.js.map +2 -2
  140. package/dist/bin/installer/targets/openhands.js +3 -3
  141. package/dist/bin/installer/targets/openhands.js.map +2 -2
  142. package/dist/bin/installer/targets/qoder.js +2 -2
  143. package/dist/bin/installer/targets/qoder.js.map +2 -2
  144. package/dist/bin/installer/targets/qwen.js +2 -2
  145. package/dist/bin/installer/targets/qwen.js.map +2 -2
  146. package/dist/bin/installer/targets/replit.js +3 -3
  147. package/dist/bin/installer/targets/replit.js.map +2 -2
  148. package/dist/bin/installer/targets/roo.js +4 -4
  149. package/dist/bin/installer/targets/roo.js.map +2 -2
  150. package/dist/bin/installer/targets/tabnine.js +3 -3
  151. package/dist/bin/installer/targets/tabnine.js.map +2 -2
  152. package/dist/bin/installer/targets/trae.js +3 -3
  153. package/dist/bin/installer/targets/trae.js.map +2 -2
  154. package/dist/bin/installer/targets/warp.js +4 -4
  155. package/dist/bin/installer/targets/warp.js.map +2 -2
  156. package/dist/bin/installer/targets/windsurf.js +4 -4
  157. package/dist/bin/installer/targets/windsurf.js.map +2 -2
  158. package/dist/bin/kirograph.js +83 -1
  159. package/dist/bin/kirograph.js.map +3 -3
  160. package/dist/config.js +30 -8
  161. package/dist/config.js.map +2 -2
  162. package/dist/core/branch-manager.js +111 -0
  163. package/dist/core/branch-manager.js.map +7 -0
  164. package/dist/core/pipeline.js +3 -3
  165. package/dist/core/pipeline.js.map +2 -2
  166. package/dist/data/pixelrag-bridge.js +93 -0
  167. package/dist/data/pixelrag-bridge.js.map +7 -0
  168. package/dist/data/pixelrag-manager.js +416 -0
  169. package/dist/data/pixelrag-manager.js.map +7 -0
  170. package/dist/db/database.js +34 -7
  171. package/dist/db/database.js.map +2 -2
  172. package/dist/db/schema.sql +7 -1
  173. package/dist/extraction/complexity.js +116 -0
  174. package/dist/extraction/complexity.js.map +7 -0
  175. package/dist/extraction/extractor.js +52 -1
  176. package/dist/extraction/extractor.js.map +2 -2
  177. package/dist/graph/git-context.js +229 -0
  178. package/dist/graph/git-context.js.map +7 -0
  179. package/dist/index.js +4 -2
  180. package/dist/index.js.map +2 -2
  181. package/dist/mcp/cache.js +1 -1
  182. package/dist/mcp/cache.js.map +2 -2
  183. package/dist/mcp/handler.js +446 -0
  184. package/dist/mcp/handler.js.map +7 -0
  185. package/dist/mcp/handlers/architecture.js +297 -0
  186. package/dist/mcp/handlers/architecture.js.map +7 -0
  187. package/dist/mcp/handlers/branch.js +175 -0
  188. package/dist/mcp/handlers/branch.js.map +7 -0
  189. package/dist/mcp/handlers/code-health.js +563 -0
  190. package/dist/mcp/handlers/code-health.js.map +7 -0
  191. package/dist/mcp/handlers/complexity.js +267 -0
  192. package/dist/mcp/handlers/complexity.js.map +7 -0
  193. package/dist/mcp/handlers/core.js +611 -0
  194. package/dist/mcp/handlers/core.js.map +7 -0
  195. package/dist/mcp/handlers/data.js +337 -0
  196. package/dist/mcp/handlers/data.js.map +7 -0
  197. package/dist/mcp/handlers/docs.js +159 -0
  198. package/dist/mcp/handlers/docs.js.map +7 -0
  199. package/dist/mcp/handlers/edit-primitives.js +200 -0
  200. package/dist/mcp/handlers/edit-primitives.js.map +7 -0
  201. package/dist/mcp/handlers/git-context.js +286 -0
  202. package/dist/mcp/handlers/git-context.js.map +7 -0
  203. package/dist/mcp/handlers/memory.js +363 -0
  204. package/dist/mcp/handlers/memory.js.map +7 -0
  205. package/dist/mcp/handlers/patterns.js +310 -0
  206. package/dist/mcp/handlers/patterns.js.map +7 -0
  207. package/dist/mcp/handlers/security.js +699 -0
  208. package/dist/mcp/handlers/security.js.map +7 -0
  209. package/dist/mcp/handlers/utils.js +97 -0
  210. package/dist/mcp/handlers/utils.js.map +7 -0
  211. package/dist/mcp/handlers/watchmen.js +104 -0
  212. package/dist/mcp/handlers/watchmen.js.map +7 -0
  213. package/dist/mcp/handlers/wiki.js +242 -0
  214. package/dist/mcp/handlers/wiki.js.map +7 -0
  215. package/dist/mcp/server.js +72 -3
  216. package/dist/mcp/server.js.map +2 -2
  217. package/dist/mcp/tool-names.js +136 -10
  218. package/dist/mcp/tool-names.js.map +2 -2
  219. package/dist/mcp/tools.js +846 -3019
  220. package/dist/mcp/tools.js.map +3 -3
  221. package/dist/types.js.map +2 -2
  222. package/package.json +36 -10
package/dist/mcp/tools.js CHANGED
@@ -1,9 +1,7 @@
1
1
  "use strict";
2
- var __create = Object.create;
3
2
  var __defProp = Object.defineProperty;
4
3
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
- var __getProtoOf = Object.getPrototypeOf;
7
5
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
6
  var __export = (target, all) => {
9
7
  for (var name in all)
@@ -17,76 +15,17 @@ var __copyProps = (to, from, except, desc) => {
17
15
  }
18
16
  return to;
19
17
  };
20
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
- // If the importer is in node compatibility mode or this is not an ESM
22
- // file that has been converted to a CommonJS file using a Babel-
23
- // compatible transform (i.e. "__esModule" has not been set), then set
24
- // "default" to the CommonJS "module.exports" for node compatibility.
25
- isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
- mod
27
- ));
28
18
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
19
  var tools_exports = {};
30
20
  __export(tools_exports, {
31
21
  KIROGRAPH_TOOL_NAMES: () => import_tool_names.KIROGRAPH_TOOL_NAMES,
32
22
  LIVE_SEARCH_TOOL_DEFINITION: () => LIVE_SEARCH_TOOL_DEFINITION,
33
- ToolHandler: () => ToolHandler,
23
+ ToolHandler: () => import_handler.ToolHandler,
34
24
  tools: () => tools
35
25
  });
36
26
  module.exports = __toCommonJS(tools_exports);
37
- var fs = __toESM(require("fs"));
38
- var path = __toESM(require("path"));
39
- var crypto = __toESM(require("crypto"));
40
- var import_child_process = require("child_process");
41
- var import__ = __toESM(require("../index"));
42
- var import_errors = require("../errors");
43
- var import_compression = require("../compression/index");
44
- var import_tracker = require("../compression/tracker");
45
27
  var import_tool_names = require("./tool-names");
46
- const MAX_OUTPUT = 15e3;
47
- function truncate(s) {
48
- return s.length > MAX_OUTPUT ? s.slice(0, MAX_OUTPUT) + "\n\u2026[truncated]" : s;
49
- }
50
- function estimateFileTokens(projectRoot, filePaths) {
51
- let total = 0;
52
- for (const fp of filePaths) {
53
- try {
54
- const fullPath = path.isAbsolute(fp) ? fp : path.join(projectRoot, fp);
55
- const stat = fs.statSync(fullPath);
56
- total += Math.round(stat.size / 4);
57
- } catch {
58
- }
59
- }
60
- return total;
61
- }
62
- function clampLimit(value, defaultValue) {
63
- const n = typeof value === "number" ? value : defaultValue;
64
- return Math.max(1, Math.min(100, Math.round(n)));
65
- }
66
- function mapKind(kind) {
67
- if (kind === "type_alias") return "type";
68
- return kind;
69
- }
70
- function writeSessionMarker(projectRoot) {
71
- try {
72
- const hash = crypto.createHash("sha256").update(projectRoot).digest("hex").slice(0, 16);
73
- fs.writeFileSync(`/tmp/kirograph-consulted-${hash}`, String(Date.now()));
74
- } catch {
75
- }
76
- }
77
- function formatAge(timestamp) {
78
- const diff = Date.now() - timestamp;
79
- const minutes = Math.floor(diff / 6e4);
80
- if (minutes < 60) return `${minutes}m ago`;
81
- const hours = Math.floor(minutes / 60);
82
- if (hours < 24) return `${hours}h ago`;
83
- const days = Math.floor(hours / 24);
84
- if (days < 7) return `${days}d ago`;
85
- const weeks = Math.floor(days / 7);
86
- if (weeks < 5) return `${weeks}w ago`;
87
- const months = Math.floor(days / 30);
88
- return `${months}mo ago`;
89
- }
28
+ var import_handler = require("./handler");
90
29
  const tools = [
91
30
  {
92
31
  name: "kirograph_search",
@@ -95,27 +34,39 @@ const tools = [
95
34
  type: "object",
96
35
  properties: {
97
36
  query: { type: "string", description: 'Symbol name or partial name (e.g., "auth", "signIn", "UserService")' },
37
+ mode: {
38
+ type: "string",
39
+ description: 'Search mode: "name" (default, FTS prefix match) or "similar" (fuzzy substring match, broader results)',
40
+ enum: ["name", "similar"],
41
+ default: "name"
42
+ },
98
43
  kind: {
99
44
  type: "string",
100
45
  description: "Filter by node kind",
101
46
  enum: ["function", "method", "class", "interface", "type_alias", "variable", "route", "component"]
102
47
  },
103
48
  limit: { type: "number", description: "Max results 1-100 (default: 10)", default: 10 },
104
- projectPath: { type: "string", description: "Project root path (optional, defaults to current project)" }
49
+ projectPath: { type: "string" }
105
50
  },
106
51
  required: ["query"]
107
52
  }
108
53
  },
109
54
  {
110
55
  name: "kirograph_context",
111
- description: "PRIMARY TOOL: Build comprehensive context for a task or feature request. Returns entry points, related symbols, and key code \u2014 often enough to understand the codebase without additional tool calls.",
56
+ description: "Build comprehensive context for a task or feature request.",
112
57
  inputSchema: {
113
58
  type: "object",
114
59
  properties: {
115
- task: { type: "string", description: "Description of the task, bug, or feature to build context for" },
116
- maxNodes: { type: "number", description: "Max symbols to include (default: 20)", default: 20 },
117
- includeCode: { type: "boolean", description: "Include code snippets (default: true)", default: true },
118
- projectPath: { type: "string", description: "Project root path (optional, defaults to current project)" }
60
+ task: { type: "string", description: "Task description or feature name" },
61
+ maxNodes: { type: "number", description: "Max symbols to include", default: 20 },
62
+ includeCode: { type: "boolean", description: "Include code snippets. Deprecated \u2014 prefer detail.", default: true },
63
+ detail: {
64
+ type: "string",
65
+ description: "Code verbosity: full, signatures, or summary",
66
+ enum: ["full", "signatures", "summary"],
67
+ default: "full"
68
+ },
69
+ projectPath: { type: "string" }
119
70
  },
120
71
  required: ["task"]
121
72
  }
@@ -128,7 +79,7 @@ const tools = [
128
79
  properties: {
129
80
  symbol: { type: "string", description: "Symbol name to find callers for" },
130
81
  limit: { type: "number", description: "Max results 1-100 (default: 20)", default: 20 },
131
- projectPath: { type: "string", description: "Project root path (optional)" }
82
+ projectPath: { type: "string" }
132
83
  },
133
84
  required: ["symbol"]
134
85
  }
@@ -141,33 +92,40 @@ const tools = [
141
92
  properties: {
142
93
  symbol: { type: "string", description: "Symbol name to find callees for" },
143
94
  limit: { type: "number", description: "Max results 1-100 (default: 20)", default: 20 },
144
- projectPath: { type: "string", description: "Project root path (optional)" }
95
+ projectPath: { type: "string" }
145
96
  },
146
97
  required: ["symbol"]
147
98
  }
148
99
  },
149
100
  {
150
101
  name: "kirograph_impact",
151
- description: "Analyze what code would be affected by changing a symbol. Use before making changes.",
102
+ description: "Analyze what code would be affected by changing a symbol.",
152
103
  inputSchema: {
153
104
  type: "object",
154
105
  properties: {
155
106
  symbol: { type: "string", description: "Symbol name to analyze impact for" },
156
107
  depth: { type: "number", description: "Traversal depth (default: 2)", default: 2 },
157
- projectPath: { type: "string", description: "Project root path (optional)" }
108
+ projectPath: { type: "string" }
158
109
  },
159
110
  required: ["symbol"]
160
111
  }
161
112
  },
162
113
  {
163
114
  name: "kirograph_node",
164
- description: "Get details about a specific symbol, optionally including its source code.",
115
+ description: "Get details about a specific symbol.",
165
116
  inputSchema: {
166
117
  type: "object",
167
118
  properties: {
168
- symbol: { type: "string", description: "Symbol name to look up" },
169
- includeCode: { type: "boolean", description: "Include source code (default: false)", default: false },
170
- projectPath: { type: "string", description: "Project root path (optional)" }
119
+ symbol: { type: "string", description: "Symbol name or fully-qualified name to look up" },
120
+ qualified: { type: "boolean", description: "When true, treat symbol as a fully-qualified name (exact match, bypasses FTS \u2014 use when you have the exact qualifiedName from a previous result)", default: false },
121
+ includeCode: { type: "boolean", description: "Include full source code. Deprecated \u2014 prefer detail.", default: false },
122
+ detail: {
123
+ type: "string",
124
+ description: "Output level: summary, signatures, or full",
125
+ enum: ["summary", "signatures", "full"],
126
+ default: "summary"
127
+ },
128
+ projectPath: { type: "string" }
171
129
  },
172
130
  required: ["symbol"]
173
131
  }
@@ -178,27 +136,27 @@ const tools = [
178
136
  inputSchema: {
179
137
  type: "object",
180
138
  properties: {
181
- projectPath: { type: "string", description: "Project root path (optional)" }
139
+ projectPath: { type: "string" }
182
140
  }
183
141
  }
184
142
  },
185
143
  {
186
144
  name: "kirograph_files",
187
- description: "List the indexed file structure of the project. Supports filtering by path prefix, glob pattern, or depth.",
145
+ description: "List indexed file structure with optional filtering.",
188
146
  inputSchema: {
189
147
  type: "object",
190
148
  properties: {
191
149
  filterPath: { type: "string", description: 'Filter by directory path prefix (e.g., "src/")' },
192
150
  pattern: { type: "string", description: 'Filter by glob pattern (e.g., "**/*.ts")' },
193
- maxDepth: { type: "number", description: "Limit tree depth" },
151
+ maxDepth: { type: "number", description: "Max directory depth to traverse" },
194
152
  format: {
195
153
  type: "string",
196
154
  description: 'Output format: "tree" (default, visual tree), "flat" (one path per line), "grouped" (grouped by directory), "compact" (rtk-style summary with counts)',
197
155
  enum: ["tree", "flat", "grouped", "compact"],
198
156
  default: "tree"
199
157
  },
200
- includeMetadata: { type: "boolean", description: "Include language and symbol count (default: true)", default: true },
201
- projectPath: { type: "string", description: "Project root path (optional)" }
158
+ includeMetadata: { type: "boolean", description: "Include language and symbol count", default: true },
159
+ projectPath: { type: "string" }
202
160
  }
203
161
  }
204
162
  },
@@ -209,7 +167,7 @@ const tools = [
209
167
  type: "object",
210
168
  properties: {
211
169
  limit: { type: "number", description: "Max results 1-100 (default: 50)", default: 50 },
212
- projectPath: { type: "string", description: "Project root path (optional)" }
170
+ projectPath: { type: "string" }
213
171
  }
214
172
  }
215
173
  },
@@ -219,7 +177,7 @@ const tools = [
219
177
  inputSchema: {
220
178
  type: "object",
221
179
  properties: {
222
- projectPath: { type: "string", description: "Project root path (optional)" }
180
+ projectPath: { type: "string" }
223
181
  }
224
182
  }
225
183
  },
@@ -231,58 +189,71 @@ const tools = [
231
189
  properties: {
232
190
  from: { type: "string", description: "Source symbol name" },
233
191
  to: { type: "string", description: "Target symbol name" },
234
- projectPath: { type: "string", description: "Project root path (optional)" }
192
+ projectPath: { type: "string" }
235
193
  },
236
194
  required: ["from", "to"]
237
195
  }
238
196
  },
239
197
  {
240
198
  name: "kirograph_architecture",
241
- description: "Get the high-level software architecture: packages, layers, and their dependencies. Requires enableArchitecture=true in config. Call this first on a new task to orient yourself without reading files.",
199
+ description: "Show high-level architecture: packages, layers, dependencies.",
242
200
  inputSchema: {
243
201
  type: "object",
244
202
  properties: {
245
203
  level: {
246
204
  type: "string",
247
- description: 'View level: "packages" (package graph), "layers" (architectural layers), or "both" (default)',
205
+ description: "packages, layers, or both",
248
206
  enum: ["packages", "layers", "both"],
249
207
  default: "both"
250
208
  },
251
- includeFiles: { type: "boolean", description: "Include per-file package/layer assignments (default: false)", default: false },
252
- projectPath: { type: "string", description: "Project root path (optional)" }
209
+ includeFiles: { type: "boolean", description: "Include per-file package/layer assignments", default: false },
210
+ projectPath: { type: "string" }
253
211
  }
254
212
  }
255
213
  },
256
214
  {
257
215
  name: "kirograph_coupling",
258
- description: "Show coupling metrics for packages: afferent (Ca), efferent (Ce), and instability (Ce/(Ca+Ce)). High instability = depends on many others; low instability = depended on by many. Requires enableArchitecture=true.",
216
+ description: "Package coupling metrics: Ca, Ce, and instability (Ce/(Ca+Ce)).",
259
217
  inputSchema: {
260
218
  type: "object",
261
219
  properties: {
262
220
  sortBy: {
263
221
  type: "string",
264
- description: 'Sort order: "instability" (default), "afferent", or "efferent"',
222
+ description: "Sort by instability, ca, ce, or name",
265
223
  enum: ["instability", "afferent", "efferent"],
266
224
  default: "instability"
267
225
  },
268
226
  limit: { type: "number", description: "Max results (default: 20)", default: 20 },
269
- projectPath: { type: "string", description: "Project root path (optional)" }
227
+ projectPath: { type: "string" }
270
228
  }
271
229
  }
272
230
  },
273
231
  {
274
232
  name: "kirograph_package",
275
- description: "Drill into one package: files it contains, symbols it exports, packages it depends on, and packages that depend on it. Requires enableArchitecture=true.",
233
+ description: "Drill into a package: files, exports, and dependencies.",
276
234
  inputSchema: {
277
235
  type: "object",
278
236
  properties: {
279
237
  package: { type: "string", description: "Package name or path (partial match accepted)" },
280
238
  includeFiles: { type: "boolean", description: "List files in the package (default: true)", default: true },
281
- projectPath: { type: "string", description: "Project root path (optional)" }
239
+ projectPath: { type: "string" }
282
240
  },
283
241
  required: ["package"]
284
242
  }
285
243
  },
244
+ {
245
+ name: "kirograph_manifest",
246
+ description: "Workspace manifest summary: list all packages across ecosystems with versions, external dep counts, and license. Pass package= to drill into one package. Pass ecosystem= to filter (npm, cargo, go, python, maven, gradle, csproj). Pass showDrift=true to surface packages that appear in multiple manifest files with differing versions.",
247
+ inputSchema: {
248
+ type: "object",
249
+ properties: {
250
+ package: { type: "string", description: "Package name or path for a focused view (partial match accepted)" },
251
+ ecosystem: { type: "string", description: 'Filter by ecosystem / language (e.g. "npm", "cargo", "go", "python")' },
252
+ showDrift: { type: "boolean", description: "Highlight packages declared in multiple manifests with different versions (default: false)", default: false },
253
+ projectPath: { type: "string" }
254
+ }
255
+ }
256
+ },
286
257
  {
287
258
  name: "kirograph_hotspots",
288
259
  description: "Find the most-connected symbols in the codebase by total edge degree (incoming + outgoing). Useful for identifying load-bearing code, core abstractions, or blast-radius hot zones.",
@@ -290,7 +261,7 @@ const tools = [
290
261
  type: "object",
291
262
  properties: {
292
263
  limit: { type: "number", description: "Max results 1-100 (default: 20)", default: 20 },
293
- projectPath: { type: "string", description: "Project root path (optional)" }
264
+ projectPath: { type: "string" }
294
265
  }
295
266
  }
296
267
  },
@@ -301,78 +272,272 @@ const tools = [
301
272
  type: "object",
302
273
  properties: {
303
274
  limit: { type: "number", description: "Max results 1-100 (default: 20)", default: 20 },
304
- projectPath: { type: "string", description: "Project root path (optional)" }
275
+ projectPath: { type: "string" }
305
276
  }
306
277
  }
307
278
  },
308
279
  {
309
280
  name: "kirograph_diff",
310
- description: "Compare the current graph against a saved snapshot. Shows added/removed symbols and relationships since the snapshot was taken. Use `kirograph snapshot` CLI command to save a snapshot first.",
281
+ description: "Compare current graph against a saved snapshot.",
282
+ inputSchema: {
283
+ type: "object",
284
+ properties: {
285
+ snapshot: { type: "string", description: "Snapshot label; omit for latest" },
286
+ projectPath: { type: "string" }
287
+ }
288
+ }
289
+ },
290
+ {
291
+ name: "kirograph_snapshot_save",
292
+ description: "Save a snapshot of the current graph state. Snapshots can be compared with kirograph_diff to track structural changes over time.",
293
+ inputSchema: {
294
+ type: "object",
295
+ properties: {
296
+ label: { type: "string", description: "Label for the snapshot (default: timestamp-based label)" },
297
+ projectPath: { type: "string" }
298
+ }
299
+ }
300
+ },
301
+ {
302
+ name: "kirograph_snapshot_list",
303
+ description: "List all saved graph snapshots with their labels, timestamps, and symbol/edge counts.",
311
304
  inputSchema: {
312
305
  type: "object",
313
306
  properties: {
314
- snapshot: { type: "string", description: "Snapshot label to compare against. Omit to use the latest saved snapshot." },
315
- projectPath: { type: "string", description: "Project root path (optional)" }
307
+ projectPath: { type: "string" }
316
308
  }
317
309
  }
318
310
  },
319
311
  {
320
312
  name: "kirograph_type_hierarchy",
321
- description: "Traverse the type hierarchy of a class or interface (base types and derived types).",
313
+ description: "Traverse base and derived types of a class or interface.",
322
314
  inputSchema: {
323
315
  type: "object",
324
316
  properties: {
325
317
  symbol: { type: "string", description: "Class or interface name" },
326
318
  direction: {
327
319
  type: "string",
328
- description: 'Direction: "up" for base types, "down" for derived types, "both" for all (default)',
320
+ description: "up=base, down=derived, both=all",
329
321
  enum: ["up", "down", "both"],
330
322
  default: "both"
331
323
  },
332
- projectPath: { type: "string", description: "Project root path (optional)" }
324
+ projectPath: { type: "string" }
333
325
  },
334
326
  required: ["symbol"]
335
327
  }
336
328
  },
329
+ {
330
+ name: "kirograph_module_api",
331
+ description: "List all exported symbols in a file or directory \u2014 the public API surface.",
332
+ inputSchema: {
333
+ type: "object",
334
+ properties: {
335
+ path: { type: "string", description: 'File path (e.g. "src/auth/service.ts") or directory prefix (e.g. "src/auth/"). Omit for the whole project.' },
336
+ limit: { type: "number", description: "Max results (default: 100)", default: 100 },
337
+ projectPath: { type: "string" }
338
+ }
339
+ }
340
+ },
341
+ {
342
+ name: "kirograph_rename_preview",
343
+ description: "Show every reference site for a symbol \u2014 blast-radius preview before a rename.",
344
+ inputSchema: {
345
+ type: "object",
346
+ properties: {
347
+ symbol: { type: "string", description: "Symbol name to find all references for" },
348
+ projectPath: { type: "string" }
349
+ },
350
+ required: ["symbol"]
351
+ }
352
+ },
353
+ {
354
+ name: "kirograph_doc_coverage",
355
+ description: "Find exported symbols that are missing docstrings.",
356
+ inputSchema: {
357
+ type: "object",
358
+ properties: {
359
+ limit: { type: "number", description: "Max results (default: 50)", default: 50 },
360
+ projectPath: { type: "string" }
361
+ }
362
+ }
363
+ },
364
+ {
365
+ name: "kirograph_god_class",
366
+ description: "List classes ranked by member count to identify god classes.",
367
+ inputSchema: {
368
+ type: "object",
369
+ properties: {
370
+ threshold: { type: "number", description: "Member count threshold for flagging (default: 10)", default: 10 },
371
+ limit: { type: "number", description: "Max results (default: 20)", default: 20 },
372
+ projectPath: { type: "string" }
373
+ }
374
+ }
375
+ },
376
+ {
377
+ name: "kirograph_inheritance_depth",
378
+ description: "Show the deepest inheritance chains (BFS on extends/implements edges).",
379
+ inputSchema: {
380
+ type: "object",
381
+ properties: {
382
+ limit: { type: "number", description: "Max results (default: 20)", default: 20 },
383
+ projectPath: { type: "string" }
384
+ }
385
+ }
386
+ },
387
+ {
388
+ name: "kirograph_recursion",
389
+ description: "Find recursive and mutually-recursive functions (SCC on call edges).",
390
+ inputSchema: {
391
+ type: "object",
392
+ properties: {
393
+ limit: { type: "number", description: "Max results (default: 30)", default: 30 },
394
+ projectPath: { type: "string" }
395
+ }
396
+ }
397
+ },
398
+ {
399
+ name: "kirograph_largest",
400
+ description: "List symbols ranked by lines of code (end_line \u2212 start_line).",
401
+ inputSchema: {
402
+ type: "object",
403
+ properties: {
404
+ limit: { type: "number", description: "Max results (default: 30)", default: 30 },
405
+ projectPath: { type: "string" }
406
+ }
407
+ }
408
+ },
409
+ {
410
+ name: "kirograph_rank",
411
+ description: "Rank symbols by fan-in (callers) or fan-out (callees) edge count.",
412
+ inputSchema: {
413
+ type: "object",
414
+ properties: {
415
+ by: { type: "string", description: 'Ranking dimension: "fan-in" (default) or "fan-out"', enum: ["fan-in", "fan-out"], default: "fan-in" },
416
+ limit: { type: "number", description: "Max results (default: 30)", default: 30 },
417
+ projectPath: { type: "string" }
418
+ }
419
+ }
420
+ },
421
+ {
422
+ name: "kirograph_distribution",
423
+ description: "Symbol-kind breakdown (function, class, method\u2026) for a file, directory, or the whole project.",
424
+ inputSchema: {
425
+ type: "object",
426
+ properties: {
427
+ path: { type: "string", description: "Directory or file path prefix to scope. Omit for whole project." },
428
+ projectPath: { type: "string" }
429
+ }
430
+ }
431
+ },
432
+ {
433
+ name: "kirograph_annotations",
434
+ description: "Decorator/annotation histogram for the whole project, or list of symbols using a specific decorator.",
435
+ inputSchema: {
436
+ type: "object",
437
+ properties: {
438
+ decorator: { type: "string", description: 'Decorator name to filter (e.g. "Injectable"). Omit for full histogram.' },
439
+ limit: { type: "number", description: "Max results (default: 50)", default: 50 },
440
+ projectPath: { type: "string" }
441
+ }
442
+ }
443
+ },
444
+ {
445
+ name: "kirograph_session_start",
446
+ description: "Save the current graph metrics as a session baseline.",
447
+ inputSchema: {
448
+ type: "object",
449
+ properties: {
450
+ projectPath: { type: "string" }
451
+ }
452
+ }
453
+ },
454
+ {
455
+ name: "kirograph_session_end",
456
+ description: "Diff current graph metrics against the session baseline and report per-dimension deltas.",
457
+ inputSchema: {
458
+ type: "object",
459
+ properties: {
460
+ projectPath: { type: "string" }
461
+ }
462
+ }
463
+ },
464
+ {
465
+ name: "kirograph_unused_imports",
466
+ description: "Find import nodes with zero resolved downstream edges (i.e. nothing in the codebase references the imported symbol).",
467
+ inputSchema: {
468
+ type: "object",
469
+ properties: {
470
+ limit: { type: "number", description: "Max results (default: 50)" },
471
+ projectPath: { type: "string" }
472
+ }
473
+ }
474
+ },
475
+ {
476
+ name: "kirograph_gini",
477
+ description: "Compute the Gini inequality coefficient for a size metric (loc, fan-in, or fan-out) across all function/method nodes. Returns the coefficient plus top/bottom 5.",
478
+ inputSchema: {
479
+ type: "object",
480
+ properties: {
481
+ metric: {
482
+ type: "string",
483
+ description: 'Metric to measure: "loc" (lines of code), "fan-in" (incoming calls), "fan-out" (outgoing calls)',
484
+ enum: ["loc", "fan-in", "fan-out"],
485
+ default: "loc"
486
+ },
487
+ projectPath: { type: "string" }
488
+ }
489
+ }
490
+ },
491
+ {
492
+ name: "kirograph_dependency_depth",
493
+ description: "Run a Kahn topological sort on the file import graph and report each file's longest path from a root (depth). Useful for spotting deeply layered dependency chains.",
494
+ inputSchema: {
495
+ type: "object",
496
+ properties: {
497
+ limit: { type: "number", description: "Max files to show sorted by depth DESC (default: 20)" },
498
+ projectPath: { type: "string" }
499
+ }
500
+ }
501
+ },
337
502
  {
338
503
  name: "kirograph_exec",
339
- description: "Run a shell command and return token-optimized output. Automatically filters noise from git, test runners, linters, build tools, docker, and package managers. Use instead of raw shell for 60-90% token savings on verbose commands.",
504
+ description: "Run a shell command with token-optimized, noise-filtered output.",
340
505
  inputSchema: {
341
506
  type: "object",
342
507
  properties: {
343
- command: { type: "string", description: 'Shell command to execute (e.g., "git status", "npm test", "cargo build")' },
344
- cwd: { type: "string", description: "Working directory (default: project root)" },
508
+ command: { type: "string", description: "Shell command to execute" },
509
+ cwd: { type: "string", description: "Working directory" },
345
510
  level: {
346
511
  type: "string",
347
512
  description: 'Compression level: "normal" (balanced), "aggressive" (more compact), "ultra" (maximum compression)',
348
513
  enum: ["normal", "aggressive", "ultra"],
349
514
  default: "normal"
350
515
  },
351
- timeout: { type: "number", description: "Timeout in seconds (default: 60)", default: 60 },
352
- projectPath: { type: "string", description: "Project root path (optional)" }
516
+ timeout: { type: "number", description: "Timeout in seconds", default: 60 },
517
+ projectPath: { type: "string" }
353
518
  },
354
519
  required: ["command"]
355
520
  }
356
521
  },
357
522
  {
358
523
  name: "kirograph_gain",
359
- description: "Show token savings statistics \u2014 both from graph tools (vs manual file reads/grep) and from kirograph_exec shell compression.",
524
+ description: "Show token savings from graph tools and shell compression.",
360
525
  inputSchema: {
361
526
  type: "object",
362
527
  properties: {
363
528
  period: {
364
529
  type: "string",
365
- description: 'Time period: "session" (current), "today", "week", or "all"',
530
+ description: "Time window: session, hour, day, week, all",
366
531
  enum: ["session", "today", "week", "all"],
367
532
  default: "session"
368
533
  },
369
- projectPath: { type: "string", description: "Project root path (optional)" }
534
+ projectPath: { type: "string" }
370
535
  }
371
536
  }
372
537
  },
373
538
  {
374
539
  name: "kirograph_read",
375
- 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.',
540
+ description: "Read a file with caching; unchanged files return cache marker.",
376
541
  inputSchema: {
377
542
  type: "object",
378
543
  properties: {
@@ -385,66 +550,251 @@ const tools = [
385
550
  },
386
551
  start: { type: "number", description: "Start line (for lines mode)" },
387
552
  end: { type: "number", description: "End line (for lines mode)" },
388
- noCache: { type: "boolean", description: "Force fresh read, bypass cache (default: false)", default: false },
389
- projectPath: { type: "string", description: "Project root path (optional)" }
553
+ noCache: { type: "boolean", description: "Force fresh read, bypass cache", default: false },
554
+ projectPath: { type: "string" }
555
+ },
556
+ required: ["path"]
557
+ }
558
+ },
559
+ {
560
+ name: "kirograph_retrieve",
561
+ description: 'Retrieve cached file content by path (CCR \u2014 Cached Content Retrieval). Returns the full content stored in the session cache, or reads and caches the file if not yet seen. Use after kirograph_read returns a "[cached: file unchanged]" marker to get the actual content without a redundant filesystem read.',
562
+ inputSchema: {
563
+ type: "object",
564
+ properties: {
565
+ path: { type: "string", description: "File path to retrieve (absolute or relative to project root)" },
566
+ projectPath: { type: "string" }
390
567
  },
391
568
  required: ["path"]
392
569
  }
393
570
  },
571
+ {
572
+ name: "kirograph_compress",
573
+ description: "Compress text via rtk shell filters or caveman grammar engine.",
574
+ inputSchema: {
575
+ type: "object",
576
+ properties: {
577
+ text: { type: "string", description: "Text to compress" },
578
+ command: { type: "string", description: "Shell command text came from (selects rtk engine)" },
579
+ level: {
580
+ type: "string",
581
+ description: 'Compression intensity: "lite" / "normal" (light), "full" / "aggressive" (default), "ultra" (maximum)',
582
+ enum: ["lite", "normal", "full", "aggressive", "ultra"],
583
+ default: "full"
584
+ }
585
+ },
586
+ required: ["text"]
587
+ }
588
+ },
394
589
  {
395
590
  name: "kirograph_budget",
396
- description: "Show current session context budget usage. Returns tokens consumed, remaining budget, and utilization percentage.",
591
+ description: "Show session context budget: tokens used, remaining, utilization.",
397
592
  inputSchema: {
398
593
  type: "object",
399
594
  properties: {
400
- reset: { type: "boolean", description: "Reset session budget counters (default: false)", default: false },
401
- projectPath: { type: "string", description: "Project root path (optional)" }
595
+ reset: { type: "boolean", description: "Reset session budget counters", default: false },
596
+ projectPath: { type: "string" }
402
597
  }
403
598
  }
404
599
  },
405
600
  {
406
601
  name: "kirograph_flows",
407
- description: "Trace execution flows from entry points (routes, handlers, main functions) through the call graph. Returns ordered call chains sorted by criticality.",
602
+ description: "Trace execution flows from entry points through the call graph.",
408
603
  inputSchema: {
409
604
  type: "object",
410
605
  properties: {
411
- entryPoint: { type: "string", description: "Symbol name to trace from, or omit to auto-detect entry points" },
606
+ entryPoint: { type: "string", description: "Symbol to trace; omit to auto-detect" },
412
607
  maxFlows: { type: "number", description: "Max number of flows to return (default 10)" },
413
608
  maxDepth: { type: "number", description: "Max call chain depth (default 10)" },
414
- projectPath: { type: "string", description: "Project root path (optional)" }
609
+ projectPath: { type: "string" }
415
610
  }
416
611
  }
417
612
  },
418
613
  {
419
614
  name: "kirograph_communities",
420
- description: "Detect code communities (clusters of related symbols) using graph-based community detection. Shows which code belongs together and how communities are coupled.",
615
+ description: "Detect clusters of related symbols via graph community detection.",
421
616
  inputSchema: {
422
617
  type: "object",
423
618
  properties: {
424
- resolution: { type: "number", description: "Resolution parameter (default 1.0, higher = more communities)" },
619
+ resolution: { type: "number", description: "Granularity 0.1\u20132.0; higher=more smaller clusters" },
425
620
  limit: { type: "number", description: "Max communities to return (default 15)" },
426
- projectPath: { type: "string", description: "Project root path (optional)" }
621
+ projectPath: { type: "string" }
622
+ }
623
+ }
624
+ },
625
+ // ── Complexity tools (require enableComplexity=true) ──────────────────────────
626
+ {
627
+ name: "kirograph_complexity",
628
+ description: "Rank symbols by cyclomatic complexity, cognitive complexity, or maintainability index. Requires enableComplexity: true and a re-index.",
629
+ inputSchema: {
630
+ type: "object",
631
+ properties: {
632
+ metric: { type: "string", description: 'Ranking metric: "cyclomatic" (default), "cognitive", or "maintainability"', enum: ["cyclomatic", "cognitive", "maintainability"], default: "cyclomatic" },
633
+ threshold: { type: "number", description: "Flag symbols above (CC/cog) or below (MI) this threshold" },
634
+ limit: { type: "number", description: "Max results (default: 30)", default: 30 },
635
+ projectPath: { type: "string" }
636
+ }
637
+ }
638
+ },
639
+ {
640
+ name: "kirograph_simplify_scan",
641
+ description: "Find functions/methods that exceed complexity, size, or maintainability thresholds \u2014 simplification candidates.",
642
+ inputSchema: {
643
+ type: "object",
644
+ properties: {
645
+ thresholdCC: { type: "number", description: "Cyclomatic complexity threshold (default: 10)", default: 10 },
646
+ thresholdMI: { type: "number", description: "Maintainability index lower bound \u2014 flag below this (default: 50)", default: 50 },
647
+ thresholdLOC: { type: "number", description: "Lines of code threshold (default: 50)", default: 50 },
648
+ limit: { type: "number", description: "Max results (default: 30)", default: 30 },
649
+ projectPath: { type: "string" }
650
+ }
651
+ }
652
+ },
653
+ // ── Git context tools (require enableGitContext=true) ─────────────────────────
654
+ {
655
+ name: "kirograph_diff_context",
656
+ description: "Identify symbols changed in the working tree (or staged), with their callers and callees for immediate blast-radius context.",
657
+ inputSchema: {
658
+ type: "object",
659
+ properties: {
660
+ staged: { type: "boolean", description: "If true, show staged changes (git diff --cached) instead of unstaged", default: false },
661
+ projectPath: { type: "string" }
662
+ }
663
+ }
664
+ },
665
+ {
666
+ name: "kirograph_commit_context",
667
+ description: "Structured summary of staged changes \u2014 affected symbols, diff stat, staged files. Use to craft precise commit messages.",
668
+ inputSchema: {
669
+ type: "object",
670
+ properties: {
671
+ projectPath: { type: "string" }
672
+ }
673
+ }
674
+ },
675
+ {
676
+ name: "kirograph_pr_context",
677
+ description: "Semantic diff between two git refs \u2014 changed symbols with callers/callees. Use to write PR descriptions.",
678
+ inputSchema: {
679
+ type: "object",
680
+ properties: {
681
+ base: { type: "string", description: "Base ref (branch name, tag, or commit hash)" },
682
+ head: { type: "string", description: "Head ref (default: HEAD)", default: "HEAD" },
683
+ projectPath: { type: "string" }
684
+ },
685
+ required: ["base"]
686
+ }
687
+ },
688
+ {
689
+ name: "kirograph_changelog",
690
+ description: "Human-readable semantic changelog between two git refs: commit log + affected symbols.",
691
+ inputSchema: {
692
+ type: "object",
693
+ properties: {
694
+ ref1: { type: "string", description: "Start ref (older)" },
695
+ ref2: { type: "string", description: "End ref (default: HEAD)", default: "HEAD" },
696
+ projectPath: { type: "string" }
697
+ },
698
+ required: ["ref1"]
699
+ }
700
+ },
701
+ {
702
+ name: "kirograph_test_map",
703
+ description: "Find test files covering a symbol, or list exported symbols with no test coverage.",
704
+ inputSchema: {
705
+ type: "object",
706
+ properties: {
707
+ symbol: { type: "string", description: "Symbol name to find tests for. Omit to list all uncovered exported symbols." },
708
+ projectPath: { type: "string" }
427
709
  }
428
710
  }
429
711
  },
430
712
  {
431
713
  name: "kirograph_refactor",
432
- description: 'Refactoring assistant. Use mode "rename" to preview all locations that reference a symbol (rename preview). Use mode "suggest" for community-driven refactoring suggestions.',
714
+ description: "Rename preview (all references) or function summarization.",
433
715
  inputSchema: {
434
716
  type: "object",
435
717
  properties: {
436
- mode: { type: "string", description: 'Mode: "rename" (preview references) or "suggest" (refactoring suggestions)', enum: ["rename", "suggest"] },
718
+ mode: { type: "string", description: "rename=preview references, summarize=summary", enum: ["rename", "suggest"] },
437
719
  symbol: { type: "string", description: "Symbol name (required for rename mode)" },
438
720
  limit: { type: "number", description: "Max results (default 10)" },
439
- projectPath: { type: "string", description: "Project root path (optional)" }
721
+ projectPath: { type: "string" }
440
722
  },
441
723
  required: ["mode"]
442
724
  }
443
725
  },
726
+ // ── Edit primitives (require enableEditPrimitives=true) ───────────────────────
727
+ {
728
+ name: "kirograph_str_replace",
729
+ description: "Replace a unique string anchor in a file; fails if the anchor appears 0 or >1 times.",
730
+ inputSchema: {
731
+ type: "object",
732
+ properties: {
733
+ file: { type: "string", description: "File path relative to project root" },
734
+ old_str: { type: "string", description: "Exact string to find (must be unique in the file)" },
735
+ new_str: { type: "string", description: "Replacement string" },
736
+ projectPath: { type: "string" }
737
+ },
738
+ required: ["file", "old_str", "new_str"]
739
+ }
740
+ },
741
+ {
742
+ name: "kirograph_multi_str_replace",
743
+ description: "Apply N replacements as an all-or-nothing transaction; aborts if any anchor is ambiguous or missing.",
744
+ inputSchema: {
745
+ type: "object",
746
+ properties: {
747
+ file: { type: "string", description: "File path relative to project root" },
748
+ pairs: {
749
+ type: "array",
750
+ description: "Array of {old_str, new_str} replacement pairs applied in order",
751
+ items: {
752
+ type: "object",
753
+ properties: {
754
+ old_str: { type: "string", description: "Exact string to find (must be unique)" },
755
+ new_str: { type: "string", description: "Replacement string" }
756
+ }
757
+ }
758
+ },
759
+ projectPath: { type: "string" }
760
+ },
761
+ required: ["file", "pairs"]
762
+ }
763
+ },
764
+ {
765
+ name: "kirograph_insert_at",
766
+ description: "Insert content before or after a unique anchor string or line number.",
767
+ inputSchema: {
768
+ type: "object",
769
+ properties: {
770
+ file: { type: "string", description: "File path relative to project root" },
771
+ anchor: { type: "string", description: "Unique string anchor to insert relative to (mutually exclusive with line)" },
772
+ line: { type: "number", description: "1-based line number to insert relative to (mutually exclusive with anchor)" },
773
+ content: { type: "string", description: "Content to insert (include newline if needed)" },
774
+ position: { type: "string", description: '"before" or "after" the anchor/line (default: "after")', enum: ["before", "after"], default: "after" },
775
+ projectPath: { type: "string" }
776
+ },
777
+ required: ["file", "content"]
778
+ }
779
+ },
780
+ {
781
+ name: "kirograph_ast_grep_rewrite",
782
+ description: "Structural AST rewrite via ast-grep. Skipped if ast-grep is not installed.",
783
+ inputSchema: {
784
+ type: "object",
785
+ properties: {
786
+ file: { type: "string", description: "File path relative to project root" },
787
+ pattern: { type: "string", description: "ast-grep pattern to match" },
788
+ rewrite: { type: "string", description: "ast-grep rewrite template" },
789
+ projectPath: { type: "string" }
790
+ },
791
+ required: ["file", "pattern", "rewrite"]
792
+ }
793
+ },
444
794
  // ── Memory tools (require enableMemory=true) ────────────────────────────────
445
795
  {
446
796
  name: "kirograph_mem_search",
447
- description: "Search project memory for past decisions, errors, patterns, and context. Returns observations ranked by relevance.",
797
+ description: "Search project memory for decisions, errors, patterns, context.",
448
798
  inputSchema: {
449
799
  type: "object",
450
800
  properties: {
@@ -457,14 +807,14 @@ const tools = [
457
807
  limit: { type: "number", description: "Max results (default: 10)", default: 10 },
458
808
  sessionId: { type: "string", description: "Filter to specific session" },
459
809
  asOf: { type: "number", description: "Query facts valid at this timestamp (epoch ms). Filters out expired/superseded observations." },
460
- projectPath: { type: "string", description: "Project root path (optional)" }
810
+ projectPath: { type: "string" }
461
811
  },
462
812
  required: ["query"]
463
813
  }
464
814
  },
465
815
  {
466
816
  name: "kirograph_mem_store",
467
- description: "Store an observation in project memory. Content is automatically compressed (if caveman mode is on) and linked to relevant code symbols.",
817
+ description: "Store an observation in project memory.",
468
818
  inputSchema: {
469
819
  type: "object",
470
820
  properties: {
@@ -475,9 +825,9 @@ const tools = [
475
825
  enum: ["decision", "error", "pattern", "architecture", "summary", "note"],
476
826
  default: "note"
477
827
  },
478
- projectPath: { type: "string", description: "Project root path (optional)" },
828
+ projectPath: { type: "string" },
479
829
  topicKey: { type: "string", description: "Stable semantic key for this observation (e.g. 'architecture/auth-model'). Enables addressing by concept." },
480
- reviewAfter: { type: "number", description: "Epoch ms: schedule this observation for re-evaluation after this timestamp." }
830
+ reviewAfter: { type: "number", description: "ISO date to flag for review" }
481
831
  },
482
832
  required: ["content"]
483
833
  }
@@ -488,42 +838,42 @@ const tools = [
488
838
  inputSchema: {
489
839
  type: "object",
490
840
  properties: {
491
- limit: { type: "number", description: "Number of sessions to show (default: 5)", default: 5 },
841
+ limit: { type: "number", description: "Number of sessions to show", default: 5 },
492
842
  sessionId: { type: "string", description: "Show observations for a specific session" },
493
- projectPath: { type: "string", description: "Project root path (optional)" }
843
+ projectPath: { type: "string" }
494
844
  }
495
845
  }
496
846
  },
497
847
  {
498
848
  name: "kirograph_mem_status",
499
- description: "Memory subsystem health: session count, observations, embedding coverage, storage size.",
849
+ description: "Memory subsystem health: sessions, observations, embeddings, size.",
500
850
  inputSchema: {
501
851
  type: "object",
502
852
  properties: {
503
- projectPath: { type: "string", description: "Project root path (optional)" }
853
+ projectPath: { type: "string" }
504
854
  }
505
855
  }
506
856
  },
507
857
  // ── Engram-parity mem tools ──────────────────────────────────────────────────
508
858
  {
509
859
  name: "kirograph_mem_review",
510
- description: "List observations past their review_after date \u2014 stale facts the agent should re-evaluate, update, or supersede.",
860
+ description: "List observations past their review_after date.",
511
861
  inputSchema: {
512
862
  type: "object",
513
863
  properties: {
514
864
  limit: { type: "number", description: "Max results (default: 20)", default: 20 },
515
- projectPath: { type: "string", description: "Project root path (optional)" }
865
+ projectPath: { type: "string" }
516
866
  }
517
867
  }
518
868
  },
519
869
  {
520
870
  name: "kirograph_mem_mark_reviewed",
521
- description: "Mark an observation as reviewed \u2014 clears its review_after date.",
871
+ description: "Clear review_after date on an observation.",
522
872
  inputSchema: {
523
873
  type: "object",
524
874
  properties: {
525
875
  id: { type: "string", description: "Observation ID" },
526
- projectPath: { type: "string", description: "Project root path (optional)" }
876
+ projectPath: { type: "string" }
527
877
  },
528
878
  required: ["id"]
529
879
  }
@@ -538,9 +888,9 @@ const tools = [
538
888
  observationB: { type: "string", description: "Second observation ID or topic_key" },
539
889
  relation: { type: "string", enum: ["supersedes", "conflicts_with", "compatible", "scoped", "related", "not_conflict"], description: "Relation type" },
540
890
  confidence: { type: "number", description: "Confidence 0.0\u20131.0 (default: 1.0)" },
541
- reason: { type: "string", description: "Explanation for the relation" },
891
+ reason: { type: "string", description: "Explanation of the relation" },
542
892
  evidence: { type: "string", description: "Supporting evidence text" },
543
- projectPath: { type: "string", description: "Project root path (optional)" }
893
+ projectPath: { type: "string" }
544
894
  },
545
895
  required: ["observationA", "observationB", "relation"]
546
896
  }
@@ -554,218 +904,303 @@ const tools = [
554
904
  relationId: { type: "string", description: "Relation ID (from kirograph_mem_compare or kirograph_mem_search)" },
555
905
  relation: { type: "string", enum: ["supersedes", "conflicts_with", "compatible", "scoped", "related", "not_conflict"], description: "Final relation type" },
556
906
  confidence: { type: "number", description: "Final confidence 0.0\u20131.0" },
557
- reason: { type: "string", description: "Reasoning for judgment" },
907
+ reason: { type: "string", description: "Reasoning" },
558
908
  evidence: { type: "string", description: "Supporting evidence" },
559
- projectPath: { type: "string", description: "Project root path (optional)" }
909
+ projectPath: { type: "string" }
560
910
  },
561
911
  required: ["relationId", "relation", "confidence"]
562
912
  }
563
913
  },
564
914
  {
565
915
  name: "kirograph_mem_capture",
566
- description: "Extract and store structured learnings from a freeform text block. Looks for ## Key Learnings, ## Observations, ## Decisions, ## Key Changes sections and saves each bullet as a separate observation.",
916
+ description: "Extract and store structured learnings from freeform text.",
567
917
  inputSchema: {
568
918
  type: "object",
569
919
  properties: {
570
920
  content: { type: "string", description: "Text block with structured sections" },
571
- projectPath: { type: "string", description: "Project root path (optional)" }
921
+ projectPath: { type: "string" }
572
922
  },
573
923
  required: ["content"]
574
924
  }
575
925
  },
576
926
  {
577
927
  name: "kirograph_mem_save_prompt",
578
- description: "Save the current user prompt to session memory for context reconstruction.",
928
+ description: "Save the current user prompt to session memory.",
579
929
  inputSchema: {
580
930
  type: "object",
581
931
  properties: {
582
- content: { type: "string", description: "Prompt content to save" },
583
- projectPath: { type: "string", description: "Project root path (optional)" }
932
+ content: { type: "string", description: "The user prompt text to save" },
933
+ projectPath: { type: "string" }
584
934
  },
585
935
  required: ["content"]
586
936
  }
587
937
  },
588
938
  {
589
939
  name: "kirograph_mem_suggest_topic_key",
590
- description: 'Suggest a stable topic_key for an observation based on its kind and content. Returns a deterministic slug like "architecture/auth-model".',
940
+ description: "Suggest a stable topic_key slug for an observation.",
591
941
  inputSchema: {
592
942
  type: "object",
593
943
  properties: {
594
944
  kind: { type: "string", enum: ["decision", "error", "pattern", "architecture", "summary", "note"], description: "Observation kind" },
595
- title: { type: "string", description: "Short title or first sentence of the observation" },
596
- projectPath: { type: "string", description: "Project root path (optional)" }
945
+ title: { type: "string", description: "Short description of the observation" },
946
+ projectPath: { type: "string" }
597
947
  },
598
948
  required: ["kind", "title"]
599
949
  }
600
950
  },
601
951
  {
602
952
  name: "kirograph_mem_conflicts_scan",
603
- description: "Scan recent observations for potential conflicts using FTS similarity. Returns candidate pairs that may need a relation established via kirograph_mem_compare.",
953
+ description: "Scan recent observations for potential conflicts using FTS.",
604
954
  inputSchema: {
605
955
  type: "object",
606
956
  properties: {
607
957
  limit: { type: "number", description: "Max observations to scan (default: 50)", default: 50 },
608
- projectPath: { type: "string", description: "Project root path (optional)" }
958
+ projectPath: { type: "string" }
609
959
  }
610
960
  }
611
961
  },
612
- // ── Wiki tools (require enableWiki=true) ────────────────────────────────────
613
962
  {
614
- name: "kirograph_wiki_ingest",
615
- description: "Returns an ingest prompt (SCHEMA + MANIFEST + source content) for the LLM to produce a WIKI_DIFF. Call kirograph_wiki_apply_diff with the result.",
963
+ name: "kirograph_mem_prune",
964
+ description: "Remove memory observations older than a given duration.",
616
965
  inputSchema: {
617
966
  type: "object",
618
967
  properties: {
619
- source: { type: "string", description: "Source content to ingest (markdown, notes, ADR text, etc.)" },
620
- sourceName: { type: "string", description: 'Name or path of the source (e.g. "ADR-001.md")', default: "source" },
621
- projectPath: { type: "string", description: "Project root path (optional)" }
622
- },
623
- required: ["source"]
968
+ olderThan: { type: "string", description: 'Duration threshold e.g. "90d" or "6m"', default: "90d" },
969
+ projectPath: { type: "string" }
970
+ }
624
971
  }
625
972
  },
626
973
  {
627
- name: "kirograph_wiki_apply_diff",
628
- description: "Apply a WIKI_DIFF string (produced by the LLM after kirograph_wiki_ingest) to the wiki filesystem and SQLite index.",
974
+ name: "kirograph_mem_lint",
975
+ description: "Health check: stale links, model mismatch, orphaned sessions.",
629
976
  inputSchema: {
630
977
  type: "object",
631
978
  properties: {
632
- diff: { type: "string", description: "WIKI_DIFF string with WIKI_DIFF_START/END blocks" },
633
- projectPath: { type: "string", description: "Project root path (optional)" }
634
- },
635
- required: ["diff"]
979
+ fix: { type: "boolean", description: "Auto-remove stale links", default: false },
980
+ projectPath: { type: "string" }
981
+ }
636
982
  }
637
983
  },
638
984
  {
639
- name: "kirograph_wiki_search",
640
- description: "Full-text search over wiki pages. Returns page slugs, titles, and previews ranked by relevance.",
985
+ name: "kirograph_mem_conflicts_list",
986
+ description: "List pending conflict relations needing resolution.",
641
987
  inputSchema: {
642
988
  type: "object",
643
989
  properties: {
644
- query: { type: "string", description: "Search query" },
645
- limit: { type: "number", description: "Max results (default: 5)", default: 5 },
646
- projectPath: { type: "string", description: "Project root path (optional)" }
647
- },
990
+ limit: { type: "number", description: "Max results (default: 20)", default: 20 },
991
+ projectPath: { type: "string" }
992
+ }
993
+ }
994
+ },
995
+ {
996
+ name: "kirograph_mem_conflicts_ignore",
997
+ description: "Dismiss a pending conflict relation as not relevant.",
998
+ inputSchema: {
999
+ type: "object",
1000
+ properties: {
1001
+ relationId: { type: "string", description: "Relation ID to ignore (from kirograph_mem_conflicts_list)" },
1002
+ projectPath: { type: "string" }
1003
+ },
1004
+ required: ["relationId"]
1005
+ }
1006
+ },
1007
+ // ── Wiki tools (require enableWiki=true) ────────────────────────────────────
1008
+ {
1009
+ name: "kirograph_wiki_ingest",
1010
+ description: "Build ingest prompt for LLM to produce a WIKI_DIFF.",
1011
+ inputSchema: {
1012
+ type: "object",
1013
+ properties: {
1014
+ source: { type: "string", description: "Source content to ingest (markdown, notes, ADR text, etc.)" },
1015
+ sourceName: { type: "string", description: "Name or path of the source", default: "source" },
1016
+ projectPath: { type: "string" }
1017
+ },
1018
+ required: ["source"]
1019
+ }
1020
+ },
1021
+ {
1022
+ name: "kirograph_wiki_apply_diff",
1023
+ description: "Apply a WIKI_DIFF to the wiki filesystem and SQLite index.",
1024
+ inputSchema: {
1025
+ type: "object",
1026
+ properties: {
1027
+ diff: { type: "string", description: "WIKI_DIFF string with WIKI_DIFF_START/END blocks" },
1028
+ projectPath: { type: "string" }
1029
+ },
1030
+ required: ["diff"]
1031
+ }
1032
+ },
1033
+ {
1034
+ name: "kirograph_wiki_search",
1035
+ description: "FTS over wiki pages; returns slugs, titles, previews.",
1036
+ inputSchema: {
1037
+ type: "object",
1038
+ properties: {
1039
+ query: { type: "string", description: "Search query" },
1040
+ limit: { type: "number", description: "Max results (default: 5)", default: 5 },
1041
+ projectPath: { type: "string" }
1042
+ },
648
1043
  required: ["query"]
649
1044
  }
650
1045
  },
651
1046
  {
652
1047
  name: "kirograph_wiki_page",
653
- description: "Retrieve the full markdown content of a wiki page by slug.",
1048
+ description: "Retrieve full markdown of a wiki page by slug.",
654
1049
  inputSchema: {
655
1050
  type: "object",
656
1051
  properties: {
657
- slug: { type: "string", description: 'Page slug (e.g. "AuthService", "arch/auth-model")' },
658
- projectPath: { type: "string", description: "Project root path (optional)" }
1052
+ slug: { type: "string", description: 'Page slug (e.g. "arch/auth-model")' },
1053
+ projectPath: { type: "string" }
659
1054
  },
660
1055
  required: ["slug"]
661
1056
  }
662
1057
  },
663
1058
  {
664
1059
  name: "kirograph_wiki_lint",
665
- description: "Health check the wiki for broken links, orphan pages, stale sources, and potential contradictions between pages.",
1060
+ description: "Check wiki for broken links, orphans, stale sources, contradictions.",
666
1061
  inputSchema: {
667
1062
  type: "object",
668
1063
  properties: {
669
- projectPath: { type: "string", description: "Project root path (optional)" }
1064
+ projectPath: { type: "string" }
670
1065
  }
671
1066
  }
672
1067
  },
673
1068
  {
674
1069
  name: "kirograph_wiki_list",
675
- description: "List all wiki pages with slug, title, source count, and last updated date.",
1070
+ description: "List all wiki pages with slug, title, source count, date.",
1071
+ inputSchema: {
1072
+ type: "object",
1073
+ properties: {
1074
+ projectPath: { type: "string" }
1075
+ }
1076
+ }
1077
+ },
1078
+ {
1079
+ name: "kirograph_wiki_synthesize",
1080
+ description: 'Run local-model wiki synthesis: process the pending source queue (requires wikiSynthesisMode: "local").',
1081
+ inputSchema: {
1082
+ type: "object",
1083
+ properties: {
1084
+ projectPath: { type: "string" }
1085
+ }
1086
+ }
1087
+ },
1088
+ {
1089
+ name: "kirograph_wiki_init",
1090
+ description: "Initialize the wiki: create SCHEMA.md and MANIFEST.md in .kirograph/wiki/.",
676
1091
  inputSchema: {
677
1092
  type: "object",
678
1093
  properties: {
679
- projectPath: { type: "string", description: "Project root path (optional)" }
1094
+ projectPath: { type: "string" }
1095
+ }
1096
+ }
1097
+ },
1098
+ {
1099
+ name: "kirograph_wiki_reindex",
1100
+ description: "Rebuild the SQLite index from .kirograph/wiki/*.md files on disk.",
1101
+ inputSchema: {
1102
+ type: "object",
1103
+ properties: {
1104
+ projectPath: { type: "string" }
1105
+ }
1106
+ }
1107
+ },
1108
+ {
1109
+ name: "kirograph_wiki_status",
1110
+ description: "Show wiki subsystem stats: page count, total sources, oldest/newest page dates.",
1111
+ inputSchema: {
1112
+ type: "object",
1113
+ properties: {
1114
+ projectPath: { type: "string" }
680
1115
  }
681
1116
  }
682
1117
  },
683
1118
  // ── Docs tools (require enableDocs=true) ────────────────────────────────────
684
1119
  {
685
1120
  name: "kirograph_docs_toc",
686
- description: "Get table of contents for a documentation file or the whole project. Returns section IDs, titles, levels, and summaries.",
1121
+ description: "Get TOC for a doc file or whole project (IDs, titles, levels, summaries).",
687
1122
  inputSchema: {
688
1123
  type: "object",
689
1124
  properties: {
690
1125
  file: { type: "string", description: "Filter to a specific doc file (relative path). Omit for project-wide TOC." },
691
- tree: { type: "boolean", description: "Return nested tree structure (default: false, flat list)", default: false },
692
- projectPath: { type: "string", description: "Project root path (optional)" }
1126
+ tree: { type: "boolean", description: "Return nested tree structure.", default: false },
1127
+ projectPath: { type: "string" }
693
1128
  }
694
1129
  }
695
1130
  },
696
1131
  {
697
1132
  name: "kirograph_docs_search",
698
- description: "Search documentation sections by query. Returns matching sections ranked by relevance. Independent from kirograph_search (code-only).",
1133
+ description: "Search documentation sections by query, ranked by relevance.",
699
1134
  inputSchema: {
700
1135
  type: "object",
701
1136
  properties: {
702
1137
  query: { type: "string", description: "Search query (natural language or keywords)" },
703
- file: { type: "string", description: "Narrow search to a specific doc file (relative path)" },
1138
+ file: { type: "string", description: "Limit search to a specific doc file." },
704
1139
  limit: { type: "number", description: "Max results (default: 10)", default: 10 },
705
- projectPath: { type: "string", description: "Project root path (optional)" }
1140
+ projectPath: { type: "string" }
706
1141
  },
707
1142
  required: ["query"]
708
1143
  }
709
1144
  },
710
1145
  {
711
1146
  name: "kirograph_docs_section",
712
- description: "Retrieve full content of a documentation section by its stable ID. Use context=true to also get ancestor headings and child summaries.",
1147
+ description: "Retrieve full content of a doc section by its stable ID.",
713
1148
  inputSchema: {
714
1149
  type: "object",
715
1150
  properties: {
716
- id: { type: "string", description: "Section ID (from kirograph_docs_toc or kirograph_docs_search results)" },
717
- context: { type: "boolean", description: "Include ancestor heading chain and child summaries (default: false)", default: false },
718
- projectPath: { type: "string", description: "Project root path (optional)" }
1151
+ id: { type: "string", description: "Section ID." },
1152
+ context: { type: "boolean", description: "Include ancestor headings and child summaries.", default: false },
1153
+ projectPath: { type: "string" }
719
1154
  },
720
1155
  required: ["id"]
721
1156
  }
722
1157
  },
723
1158
  {
724
1159
  name: "kirograph_docs_outline",
725
- description: "Get the heading hierarchy for a single documentation file. Lighter than full TOC when you know which file is relevant.",
1160
+ description: "Get heading hierarchy for a single documentation file.",
726
1161
  inputSchema: {
727
1162
  type: "object",
728
1163
  properties: {
729
- file: { type: "string", description: "Relative path to the doc file" },
730
- projectPath: { type: "string", description: "Project root path (optional)" }
1164
+ file: { type: "string", description: "Documentation file path (relative to project root)." },
1165
+ projectPath: { type: "string" }
731
1166
  },
732
1167
  required: ["file"]
733
1168
  }
734
1169
  },
735
1170
  {
736
1171
  name: "kirograph_docs_refs",
737
- description: "Find code symbols referenced by a doc section, or doc sections that reference a code symbol. Bidirectional lookup.",
1172
+ description: "Bidirectional lookup: doc section\u2194code symbol references.",
738
1173
  inputSchema: {
739
1174
  type: "object",
740
1175
  properties: {
741
- sectionId: { type: "string", description: "Doc section ID (find code symbols it references)" },
742
- nodeId: { type: "string", description: "Code symbol qualified name (find doc sections that reference it)" },
743
- projectPath: { type: "string", description: "Project root path (optional)" }
1176
+ sectionId: { type: "string", description: "Doc section ID to find referenced symbols." },
1177
+ nodeId: { type: "string", description: "Code symbol ID to find referencing doc sections." },
1178
+ projectPath: { type: "string" }
744
1179
  }
745
1180
  }
746
1181
  },
747
1182
  // ── Data tools (require enableData=true) ────────────────────────────────────
748
1183
  {
749
1184
  name: "kirograph_data_list",
750
- description: "List all indexed datasets with row counts, column counts, and file sizes.",
751
- inputSchema: { type: "object", properties: { projectPath: { type: "string", description: "Project root path (optional)" } } }
1185
+ description: "List all indexed datasets with row/column counts and file sizes.",
1186
+ inputSchema: { type: "object", properties: { projectPath: { type: "string" } } }
752
1187
  },
753
1188
  {
754
1189
  name: "kirograph_data_describe",
755
- description: "Full schema profile of a dataset: column names, types, cardinality, null%, sample values. Use to orient on a dataset without reading any rows.",
1190
+ description: "Full schema profile: types, cardinality, null%, sample values.",
756
1191
  inputSchema: {
757
1192
  type: "object",
758
1193
  properties: {
759
1194
  dataset: { type: "string", description: "Dataset ID (from kirograph_data_list)" },
760
- column: { type: "string", description: "Optional: deep-dive on a single column" },
761
- projectPath: { type: "string", description: "Project root path (optional)" }
1195
+ column: { type: "string", description: "Deep-dive on a single column." },
1196
+ projectPath: { type: "string" }
762
1197
  },
763
1198
  required: ["dataset"]
764
1199
  }
765
1200
  },
766
1201
  {
767
1202
  name: "kirograph_data_query",
768
- description: "Filtered row retrieval with structured operators. Returns only matching rows (max 500). Use instead of reading raw data files.",
1203
+ description: "Filtered row retrieval with structured operators (max 500 rows).",
769
1204
  inputSchema: {
770
1205
  type: "object",
771
1206
  properties: {
@@ -774,14 +1209,14 @@ const tools = [
774
1209
  columns: { type: "array", description: "Column projection (only return these columns)" },
775
1210
  limit: { type: "number", description: "Max rows (default: 100, hard cap: 500)", default: 100 },
776
1211
  offset: { type: "number", description: "Pagination offset", default: 0 },
777
- projectPath: { type: "string", description: "Project root path (optional)" }
1212
+ projectPath: { type: "string" }
778
1213
  },
779
1214
  required: ["dataset"]
780
1215
  }
781
1216
  },
782
1217
  {
783
1218
  name: "kirograph_data_aggregate",
784
- 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.",
1219
+ description: "Server-side GROUP BY aggregation: count, sum, avg, min, max.",
785
1220
  inputSchema: {
786
1221
  type: "object",
787
1222
  properties: {
@@ -789,27 +1224,27 @@ const tools = [
789
1224
  groupBy: { type: "array", description: "Columns to group by" },
790
1225
  metrics: { type: "array", description: "Array of {column, op} metrics. Ops: count, sum, avg, min, max, count_distinct" },
791
1226
  filters: { type: "array", description: "Optional pre-filters (same format as kirograph_data_query)" },
792
- projectPath: { type: "string", description: "Project root path (optional)" }
1227
+ projectPath: { type: "string" }
793
1228
  },
794
1229
  required: ["dataset", "groupBy", "metrics"]
795
1230
  }
796
1231
  },
797
1232
  {
798
1233
  name: "kirograph_data_search",
799
- description: "Search column names and sample values by keyword. Tells you which column holds the answer without loading data.",
1234
+ description: "Search column names and sample values by keyword.",
800
1235
  inputSchema: {
801
1236
  type: "object",
802
1237
  properties: {
803
1238
  dataset: { type: "string", description: "Dataset ID" },
804
1239
  query: { type: "string", description: "Search keyword" },
805
- projectPath: { type: "string", description: "Project root path (optional)" }
1240
+ projectPath: { type: "string" }
806
1241
  },
807
1242
  required: ["dataset", "query"]
808
1243
  }
809
1244
  },
810
1245
  {
811
1246
  name: "kirograph_data_join",
812
- description: "SQL JOIN across two indexed datasets. Combines data without loading either file into context.",
1247
+ description: "SQL JOIN across two indexed datasets.",
813
1248
  inputSchema: {
814
1249
  type: "object",
815
1250
  properties: {
@@ -820,50 +1255,126 @@ const tools = [
820
1255
  type: { type: "string", description: "Join type: inner (default), left, right", enum: ["inner", "left", "right"], default: "inner" },
821
1256
  columns: { type: "array", description: "Column projection (prefix with dataset ID)" },
822
1257
  limit: { type: "number", description: "Max rows (default: 100, hard cap: 500)", default: 100 },
823
- projectPath: { type: "string", description: "Project root path (optional)" }
1258
+ projectPath: { type: "string" }
824
1259
  },
825
1260
  required: ["left", "right", "leftColumn", "rightColumn"]
826
1261
  }
827
1262
  },
828
1263
  {
829
1264
  name: "kirograph_data_correlations",
830
- description: "Pairwise Pearson correlations between numeric columns. Discovers hidden relationships without loading data.",
1265
+ description: "Pairwise Pearson correlations between numeric columns.",
831
1266
  inputSchema: {
832
1267
  type: "object",
833
1268
  properties: {
834
1269
  dataset: { type: "string", description: "Dataset ID" },
835
1270
  threshold: { type: "number", description: "Min absolute correlation to include (default: 0.3)", default: 0.3 },
836
- projectPath: { type: "string", description: "Project root path (optional)" }
1271
+ projectPath: { type: "string" }
837
1272
  },
838
1273
  required: ["dataset"]
839
1274
  }
840
1275
  },
841
1276
  {
842
1277
  name: "kirograph_data_quality",
843
- description: "Data quality triage: rank columns by risk (null rate, cardinality anomalies, type issues). Identifies problematic columns without loading data.",
1278
+ description: "Rank columns by data quality risk: nulls, cardinality, type issues.",
844
1279
  inputSchema: {
845
1280
  type: "object",
846
1281
  properties: {
847
1282
  dataset: { type: "string", description: "Dataset ID" },
848
- projectPath: { type: "string", description: "Project root path (optional)" }
1283
+ projectPath: { type: "string" }
1284
+ },
1285
+ required: ["dataset"]
1286
+ }
1287
+ },
1288
+ {
1289
+ name: "kirograph_data_drift",
1290
+ description: "Schema drift between last two index runs: columns, types, row counts.",
1291
+ inputSchema: {
1292
+ type: "object",
1293
+ properties: {
1294
+ dataset: { type: "string", description: "Dataset ID (from kirograph_data_list)" },
1295
+ projectPath: { type: "string" }
1296
+ },
1297
+ required: ["dataset"]
1298
+ }
1299
+ },
1300
+ {
1301
+ name: "kirograph_data_history",
1302
+ description: "History of schema snapshots: timestamps, row/column counts, hashes.",
1303
+ inputSchema: {
1304
+ type: "object",
1305
+ properties: {
1306
+ dataset: { type: "string", description: "Dataset ID (from kirograph_data_list)" },
1307
+ limit: { type: "number", description: "Max snapshots to return (default: 10)", default: 10 },
1308
+ projectPath: { type: "string" }
849
1309
  },
850
1310
  required: ["dataset"]
851
1311
  }
852
1312
  },
1313
+ // ── Watchmen tools (require enableMemory=true + enableWatchmen=true) ──────────
1314
+ {
1315
+ name: "kirograph_watchmen_status",
1316
+ description: "Show watchmen status: pending observation count, synthesis threshold, and which brief files would be written on next synthesis.",
1317
+ inputSchema: {
1318
+ type: "object",
1319
+ properties: {
1320
+ projectPath: { type: "string" }
1321
+ }
1322
+ }
1323
+ },
1324
+ {
1325
+ name: "kirograph_watchmen_synthesize",
1326
+ description: 'Run watchmen synthesis immediately using the local model (requires watchmenSynthesisMode: "local"). Processes pending observations into a workspace brief.',
1327
+ inputSchema: {
1328
+ type: "object",
1329
+ properties: {
1330
+ force: { type: "boolean", description: "Run even if below threshold (default: false)", default: false },
1331
+ projectPath: { type: "string" }
1332
+ }
1333
+ }
1334
+ },
1335
+ {
1336
+ name: "kirograph_watchmen_reset",
1337
+ description: "Reset the watchmen synthesis counter by storing a summary observation, without running full synthesis.",
1338
+ inputSchema: {
1339
+ type: "object",
1340
+ properties: {
1341
+ projectPath: { type: "string" }
1342
+ }
1343
+ }
1344
+ },
1345
+ // ── Affected tests ────────────────────────────────────────────────────────────
1346
+ {
1347
+ name: "kirograph_affected",
1348
+ description: "Find test files affected by a set of changed source files, by traversing the dependency graph.",
1349
+ inputSchema: {
1350
+ type: "object",
1351
+ properties: {
1352
+ files: {
1353
+ type: "array",
1354
+ items: { type: "string" },
1355
+ description: "List of changed source file paths (relative to project root)"
1356
+ },
1357
+ depth: { type: "number", description: "Max dependency traversal depth (default: 5)", default: 5 },
1358
+ testPattern: { type: "string", description: "Custom glob to identify test files (optional)" },
1359
+ projectPath: { type: "string" }
1360
+ },
1361
+ required: ["files"]
1362
+ }
1363
+ },
853
1364
  // ── Security tools (require enableSecurity=true) ────────────────────────────
854
1365
  {
855
1366
  name: "kirograph_security",
856
- description: "Security overview: vulnerability counts, affected/not_affected verdicts, stale data warnings. Requires enableSecurity=true and enableArchitecture=true.",
1367
+ description: "Security overview: vuln counts, verdicts, stale data warnings.",
857
1368
  inputSchema: {
858
1369
  type: "object",
859
1370
  properties: {
860
- projectPath: { type: "string", description: "Project root path (optional)" }
1371
+ projectPath: { type: "string" }
861
1372
  }
862
1373
  }
863
1374
  },
864
1375
  {
865
1376
  name: "kirograph_vulns",
866
- description: "List vulnerabilities with reachability verdicts, severity, and affected components. Supports on-demand refresh.",
1377
+ description: "List vulns with reachability verdicts, severity, affected components.",
867
1378
  inputSchema: {
868
1379
  type: "object",
869
1380
  properties: {
@@ -871,13 +1382,13 @@ const tools = [
871
1382
  verdict: { type: "string", description: "Filter by reachability verdict", enum: ["affected", "not_affected", "under_investigation"] },
872
1383
  limit: { type: "number", description: "Max results (default: 20)", default: 20 },
873
1384
  refresh: { type: "boolean", description: "Trigger fresh vulnerability enrichment before listing (default: false)", default: false },
874
- projectPath: { type: "string", description: "Project root path (optional)" }
1385
+ projectPath: { type: "string" }
875
1386
  }
876
1387
  }
877
1388
  },
878
1389
  {
879
1390
  name: "kirograph_vuln_add",
880
- description: "Manually register a CVE against a dependency without querying external databases.",
1391
+ description: "Manually register a CVE against a dependency.",
881
1392
  inputSchema: {
882
1393
  type: "object",
883
1394
  properties: {
@@ -886,178 +1397,263 @@ const tools = [
886
1397
  severity: { type: "number", description: "CVSS v3.1 base score (optional)" },
887
1398
  summary: { type: "string", description: "Vulnerability summary (optional)" },
888
1399
  fixedVersion: { type: "string", description: "Version that fixes the vulnerability (optional)" },
889
- projectPath: { type: "string", description: "Project root path (optional)" }
1400
+ projectPath: { type: "string" }
890
1401
  },
891
1402
  required: ["cveId", "package"]
892
1403
  }
893
1404
  },
894
1405
  {
895
1406
  name: "kirograph_vuln_suppress",
896
- 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.",
1407
+ description: "Suppress a CVE from vulnerability reports.",
897
1408
  inputSchema: {
898
1409
  type: "object",
899
1410
  properties: {
900
1411
  cveId: { type: "string", description: 'CVE identifier to suppress (e.g., "CVE-2024-1234")' },
901
1412
  reason: { type: "string", description: "Reason for suppression (optional)" },
902
1413
  expires: { type: "string", description: 'Expiry date in ISO format after which the suppression is removed (e.g. "2026-12-31", optional)' },
903
- projectPath: { type: "string", description: "Project root path (optional)" }
1414
+ projectPath: { type: "string" }
904
1415
  },
905
1416
  required: ["cveId"]
906
1417
  }
907
1418
  },
908
1419
  {
909
1420
  name: "kirograph_sbom",
910
- description: "Generate and return CycloneDX 1.5 SBOM JSON for the project.",
1421
+ description: "Generate CycloneDX 1.5 SBOM JSON for the project.",
911
1422
  inputSchema: {
912
1423
  type: "object",
913
1424
  properties: {
914
- projectPath: { type: "string", description: "Project root path (optional)" }
1425
+ projectPath: { type: "string" }
915
1426
  }
916
1427
  }
917
1428
  },
918
1429
  {
919
1430
  name: "kirograph_vex",
920
- description: "Generate and return CycloneDX 1.5 VEX JSON with reachability verdicts.",
1431
+ description: "Generate CycloneDX 1.5 VEX JSON with reachability verdicts.",
921
1432
  inputSchema: {
922
1433
  type: "object",
923
1434
  properties: {
924
- projectPath: { type: "string", description: "Project root path (optional)" }
1435
+ projectPath: { type: "string" }
925
1436
  }
926
1437
  }
927
1438
  },
928
1439
  {
929
1440
  name: "kirograph_reachability",
930
- description: "Check reachability for a specific dependency or vulnerability. Returns verdict, paths, and impact summary.",
1441
+ description: "Check reachability for a dependency or CVE.",
931
1442
  inputSchema: {
932
1443
  type: "object",
933
1444
  properties: {
934
1445
  target: { type: "string", description: "Dependency name or CVE ID to check reachability for" },
935
- projectPath: { type: "string", description: "Project root path (optional)" }
1446
+ projectPath: { type: "string" }
936
1447
  },
937
1448
  required: ["target"]
938
1449
  }
939
1450
  },
940
1451
  {
941
1452
  name: "kirograph_staleness",
942
- description: "Check dependency freshness \u2014 identifies packages significantly behind their latest published version.",
1453
+ description: "Check dependency freshness against latest published versions.",
943
1454
  inputSchema: {
944
1455
  type: "object",
945
1456
  properties: {
946
1457
  threshold: { type: "number", description: "Only return packages with staleness_score >= threshold (default: 0.3)", default: 0.3 },
947
1458
  refresh: { type: "boolean", description: "Fetch latest version info from registries before listing (default: false)", default: false },
948
- projectPath: { type: "string", description: "Project root path (optional)" }
1459
+ projectPath: { type: "string" }
949
1460
  }
950
1461
  }
951
1462
  },
952
1463
  {
953
1464
  name: "kirograph_licenses",
954
- 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).",
1465
+ description: "Show licenses and check against configured deny/warn policy.",
955
1466
  inputSchema: {
956
1467
  type: "object",
957
1468
  properties: {
958
1469
  policy: { type: "boolean", description: "Return only policy violations (default: false)", default: false },
959
- projectPath: { type: "string", description: "Project root path (optional)" }
1470
+ projectPath: { type: "string" }
960
1471
  }
961
1472
  }
962
1473
  },
963
1474
  {
964
1475
  name: "kirograph_attack_surface",
965
- description: "Map the attack surface: all HTTP routes and their paths to vulnerable dependencies, with hop count, authentication status, and risk score.",
1476
+ description: "Map HTTP routes to vulnerable deps with hop count and auth status.",
966
1477
  inputSchema: {
967
1478
  type: "object",
968
1479
  properties: {
969
1480
  limit: { type: "number", description: "Max routes to return (default: 20)", default: 20 },
970
1481
  publicOnly: { type: "boolean", description: "Only return public/unauthenticated routes", default: false },
971
- projectPath: { type: "string", description: "Project root path (optional)" }
1482
+ projectPath: { type: "string" }
972
1483
  }
973
1484
  }
974
1485
  },
975
1486
  {
976
1487
  name: "kirograph_secrets",
977
- description: "Scan for hardcoded secrets and credentials, enriched with call-graph blast radius showing which entry points reach the secret.",
1488
+ description: "Scan for hardcoded secrets with call-graph blast radius.",
978
1489
  inputSchema: {
979
1490
  type: "object",
980
1491
  properties: {
981
1492
  includeTests: { type: "boolean", description: "Include test files in scan", default: false },
982
1493
  severity: { type: "string", description: "Filter by severity: critical, high, medium, low", enum: ["critical", "high", "medium", "low"] },
983
- projectPath: { type: "string", description: "Project root path (optional)" }
1494
+ projectPath: { type: "string" }
984
1495
  }
985
1496
  }
986
1497
  },
987
1498
  {
988
1499
  name: "kirograph_security_flows",
989
- 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.",
1500
+ description: "SAST-lite: detect SQL injection, eval, deserialize, path, crypto flows.",
990
1501
  inputSchema: {
991
1502
  type: "object",
992
1503
  properties: {
993
1504
  type: { type: "string", description: "Filter by type: sql, eval, deserialize, path, crypto, all", default: "all" },
994
- projectPath: { type: "string", description: "Project root path (optional)" }
1505
+ projectPath: { type: "string" }
995
1506
  }
996
1507
  }
997
1508
  },
998
1509
  {
999
1510
  name: "kirograph_supply_chain",
1000
- description: "Supply chain health: OpenSSF Scorecard scores, maintainer count, abandoned package detection for project dependencies.",
1511
+ description: "OpenSSF Scorecard scores, maintainer count, abandoned package detection.",
1001
1512
  inputSchema: {
1002
1513
  type: "object",
1003
1514
  properties: {
1004
1515
  threshold: { type: "string", description: "Minimum risk level: critical, high, medium", enum: ["critical", "high", "medium"] },
1005
1516
  refresh: { type: "boolean", description: "Re-fetch from APIs", default: false },
1006
- projectPath: { type: "string", description: "Project root path (optional)" }
1517
+ projectPath: { type: "string" }
1007
1518
  }
1008
1519
  }
1009
1520
  },
1010
1521
  {
1011
1522
  name: "kirograph_dep_confusion",
1012
- description: "Detect dependency confusion vulnerabilities: internal package names that exist in public registries (typosquatting/supply chain attack vectors).",
1523
+ description: "Detect dependency confusion: internal names in public registries.",
1013
1524
  inputSchema: {
1014
1525
  type: "object",
1015
1526
  properties: {
1016
- projectPath: { type: "string", description: "Project root path (optional)" }
1527
+ projectPath: { type: "string" }
1017
1528
  }
1018
1529
  }
1019
1530
  },
1020
1531
  {
1021
1532
  name: "kirograph_remediation",
1022
- description: "Remediation SLA tracking: which vulnerabilities are overdue for fixing based on severity thresholds (critical=7d, high=30d, medium=90d).",
1533
+ description: "Remediation SLA tracking: overdue vulns by severity thresholds.",
1023
1534
  inputSchema: {
1024
1535
  type: "object",
1025
1536
  properties: {
1026
1537
  overdueOnly: { type: "boolean", description: "Show only overdue items", default: false },
1027
- projectPath: { type: "string", description: "Project root path (optional)" }
1538
+ projectPath: { type: "string" }
1539
+ }
1540
+ }
1541
+ },
1542
+ // ── Phase 3 complexity tools ──────────────────────────────────────────────────
1543
+ {
1544
+ name: "kirograph_health",
1545
+ description: "Composite graph health score (0\u201310000) across complexity, dead code, coupling, and circular dependencies. Higher is better.",
1546
+ inputSchema: {
1547
+ type: "object",
1548
+ properties: {
1549
+ projectPath: { type: "string" }
1550
+ }
1551
+ }
1552
+ },
1553
+ {
1554
+ name: "kirograph_dsm",
1555
+ description: "Design Structure Matrix \u2014 dependency counts between top-level modules. Reveals architectural coupling and layering violations.",
1556
+ inputSchema: {
1557
+ type: "object",
1558
+ properties: {
1559
+ limit: { type: "number", description: "Max module groups to include (default 15)", default: 15 },
1560
+ projectPath: { type: "string" }
1561
+ }
1562
+ }
1563
+ },
1564
+ {
1565
+ name: "kirograph_test_risk",
1566
+ description: "Risk-ranked list of functions: complexity \xD7 fan-in. Highest-risk functions are most likely to break when changed.",
1567
+ inputSchema: {
1568
+ type: "object",
1569
+ properties: {
1570
+ limit: { type: "number", description: "Max results (default 20)", default: 20 },
1571
+ threshold: { type: "number", description: "Min risk score to include (default 0)", default: 0 },
1572
+ projectPath: { type: "string" }
1573
+ }
1574
+ }
1575
+ },
1576
+ // ── Phase 3 git context tools ─────────────────────────────────────────────────
1577
+ {
1578
+ name: "kirograph_test_coverage",
1579
+ description: "Parse lcov/Istanbul/Cobertura coverage files and show per-file coverage percentage. Sorted worst-first by default.",
1580
+ inputSchema: {
1581
+ type: "object",
1582
+ properties: {
1583
+ sortBy: { type: "string", description: "Sort order: asc (worst first) or desc (best first)", enum: ["asc", "desc"], default: "asc" },
1584
+ limit: { type: "number", description: "Max results (default 30)", default: 30 },
1585
+ projectPath: { type: "string" }
1586
+ }
1587
+ }
1588
+ },
1589
+ // ── Phase 4 branch tools ──────────────────────────────────────────────────────
1590
+ {
1591
+ name: "kirograph_branch_list",
1592
+ description: "List tracked branches with their DB sizes and last sync times.",
1593
+ inputSchema: {
1594
+ type: "object",
1595
+ properties: {
1596
+ projectPath: { type: "string" }
1597
+ }
1598
+ }
1599
+ },
1600
+ {
1601
+ name: "kirograph_branch_diff",
1602
+ description: "Symbols added, removed, or changed between two tracked branches.",
1603
+ inputSchema: {
1604
+ type: "object",
1605
+ required: ["branchA"],
1606
+ properties: {
1607
+ branchA: { type: "string", description: "First branch name (required)" },
1608
+ branchB: { type: "string", description: "Second branch name (default: main)", default: "main" },
1609
+ projectPath: { type: "string" }
1610
+ }
1611
+ }
1612
+ },
1613
+ {
1614
+ name: "kirograph_branch_search",
1615
+ description: "Search for symbols in a specific tracked branch graph.",
1616
+ inputSchema: {
1617
+ type: "object",
1618
+ required: ["query", "branch"],
1619
+ properties: {
1620
+ query: { type: "string", description: "Search query (symbol name, partial match)" },
1621
+ branch: { type: "string", description: "Branch name to search in" },
1622
+ limit: { type: "number", description: "Max results (default 20)", default: 20 },
1623
+ projectPath: { type: "string" }
1028
1624
  }
1029
1625
  }
1030
1626
  },
1031
1627
  // ── Pattern tools (require enablePatterns=true) ─────────────────────────────
1032
1628
  {
1033
1629
  name: "kirograph_pattern_coverage",
1034
- description: "OWASP Top 10 coverage report: shows which vulnerability categories are covered by bundled pattern rules and how many matches exist in each.",
1630
+ description: "OWASP Top 10 coverage report: categories covered and match counts.",
1035
1631
  inputSchema: {
1036
1632
  type: "object",
1037
1633
  properties: {
1038
- projectPath: { type: "string", description: "Project root path (optional)" }
1634
+ projectPath: { type: "string" }
1039
1635
  }
1040
1636
  }
1041
1637
  },
1042
1638
  {
1043
1639
  name: "kirograph_pattern_save_baseline",
1044
- description: "Save current pattern match counts as a baseline for future diffing.",
1640
+ description: "Save current pattern match counts as a baseline.",
1045
1641
  inputSchema: {
1046
1642
  type: "object",
1047
1643
  properties: {
1048
1644
  label: { type: "string", description: 'Baseline label (default: "default")', default: "default" },
1049
- projectPath: { type: "string", description: "Project root path (optional)" }
1645
+ projectPath: { type: "string" }
1050
1646
  }
1051
1647
  }
1052
1648
  },
1053
1649
  {
1054
1650
  name: "kirograph_pattern_diff",
1055
- description: "Diff current pattern matches against a saved baseline. Shows new findings, resolved findings, and unchanged counts.",
1651
+ description: "Diff pattern matches against a saved baseline.",
1056
1652
  inputSchema: {
1057
1653
  type: "object",
1058
1654
  properties: {
1059
1655
  label: { type: "string", description: 'Baseline label (default: "default")', default: "default" },
1060
- projectPath: { type: "string", description: "Project root path (optional)" }
1656
+ projectPath: { type: "string" }
1061
1657
  }
1062
1658
  }
1063
1659
  }
@@ -1072,2779 +1668,10 @@ const LIVE_SEARCH_TOOL_DEFINITION = {
1072
1668
  pattern: { type: "string", description: 'ast-grep inline pattern (e.g. "eval($X)")' },
1073
1669
  language: { type: "string", description: "Language to search: javascript, typescript, python, go, rust, java, etc." },
1074
1670
  limit: { type: "number", description: "Max results (default 20, max 100)", default: 20 },
1075
- projectPath: { type: "string", description: "Project root path (optional)" }
1671
+ projectPath: { type: "string" }
1076
1672
  }
1077
1673
  }
1078
1674
  };
1079
- class ToolHandler {
1080
- constructor(cg) {
1081
- this.connections = /* @__PURE__ */ new Map();
1082
- /** Anti-loop: track recent data_query calls per dataset for pagination detection. */
1083
- this.queryTracker = /* @__PURE__ */ new Map();
1084
- this.defaultCg = cg;
1085
- }
1086
- setDefaultKiroGraph(cg) {
1087
- this.defaultCg = cg;
1088
- }
1089
- /** Close all cached cross-project connections. */
1090
- closeAll() {
1091
- for (const cg of this.connections.values()) {
1092
- try {
1093
- cg.close();
1094
- } catch {
1095
- }
1096
- }
1097
- this.connections.clear();
1098
- }
1099
- /** Anti-loop: detect pagination patterns and warn the agent. */
1100
- checkPaginationLoop(dataset, offset, response) {
1101
- const now = Date.now();
1102
- const key = dataset;
1103
- const currentOffset = offset ?? 0;
1104
- for (const [k, v] of this.queryTracker) {
1105
- if (now - v.lastCall > 6e4) this.queryTracker.delete(k);
1106
- }
1107
- const entry = this.queryTracker.get(key) ?? { offsets: [], lastCall: 0 };
1108
- entry.offsets.push(currentOffset);
1109
- entry.lastCall = now;
1110
- if (entry.offsets.length > 10) entry.offsets = entry.offsets.slice(-10);
1111
- this.queryTracker.set(key, entry);
1112
- if (entry.offsets.length > 5) {
1113
- const recent = entry.offsets.slice(-6);
1114
- let isIncrementing = true;
1115
- for (let i = 1; i < recent.length; i++) {
1116
- if (recent[i] <= recent[i - 1]) {
1117
- isIncrementing = false;
1118
- break;
1119
- }
1120
- }
1121
- if (isIncrementing) {
1122
- return response + "\n\n\u26A0 Pagination detected. Consider using kirograph_data_aggregate for summary statistics instead of paginating through all rows.";
1123
- }
1124
- }
1125
- return response;
1126
- }
1127
- async getConnection(projectPath) {
1128
- if (!projectPath) return this.defaultCg;
1129
- const resolved = path.resolve(projectPath);
1130
- if (!path.isAbsolute(resolved)) return null;
1131
- if (this.connections.has(resolved)) return this.connections.get(resolved);
1132
- try {
1133
- const cg = await import__.default.open(resolved);
1134
- this.connections.set(resolved, cg);
1135
- return cg;
1136
- } catch {
1137
- return null;
1138
- }
1139
- }
1140
- async handle(toolName, args) {
1141
- try {
1142
- const text = await this.dispatch(toolName, args);
1143
- const truncated = truncate(text);
1144
- if (toolName !== "kirograph_exec" && toolName !== "kirograph_gain") {
1145
- try {
1146
- const projectRoot = args.projectPath || this.defaultCg?.getProjectRoot() || process.cwd();
1147
- const { estimateNaiveCost } = await Promise.resolve().then(() => require("../compression/naive-cost.js"));
1148
- const { estimateTokens: estimateTokens2 } = await Promise.resolve().then(() => require("../compression/index.js"));
1149
- const outputTokens = estimateTokens2(truncated);
1150
- const naiveCost = estimateNaiveCost(toolName, outputTokens, args);
1151
- if (naiveCost !== null && naiveCost > outputTokens) {
1152
- const tracker = new import_tracker.TokenTracker(projectRoot);
1153
- if (toolName.startsWith("kirograph_mem_")) {
1154
- tracker.recordMemorySaving(toolName, outputTokens, naiveCost);
1155
- } else if (toolName.startsWith("kirograph_docs_")) {
1156
- tracker.recordDocsSaving(toolName, outputTokens, naiveCost);
1157
- } else if (toolName.startsWith("kirograph_data_")) {
1158
- tracker.recordDataSaving(toolName, outputTokens, naiveCost);
1159
- } else {
1160
- tracker.recordGraphSaving(toolName, outputTokens, naiveCost);
1161
- }
1162
- }
1163
- } catch {
1164
- }
1165
- }
1166
- return { content: [{ type: "text", text: truncated }] };
1167
- } catch (err) {
1168
- (0, import_errors.logError)("MCP tool error", { tool: toolName, error: err });
1169
- const message = err instanceof Error ? err.message : String(err);
1170
- return { content: [{ type: "text", text: `Error: ${message}` }], isError: true };
1171
- }
1172
- }
1173
- async dispatch(toolName, args) {
1174
- if (toolName === "kirograph_exec") {
1175
- const cmd = args.command;
1176
- if (!cmd) return "Error: command is required.";
1177
- const projectRoot = args.projectPath || args.cwd || process.cwd();
1178
- const execCwd = args.cwd || projectRoot;
1179
- let defaultLevel = "normal";
1180
- try {
1181
- const { loadConfig } = await Promise.resolve().then(() => require("../config.js"));
1182
- const config = await loadConfig(projectRoot);
1183
- if (config.shellCompressionLevel && config.shellCompressionLevel !== "off") {
1184
- defaultLevel = config.shellCompressionLevel;
1185
- }
1186
- } catch {
1187
- }
1188
- const level = args.level ?? defaultLevel;
1189
- const timeout = (args.timeout ?? 60) * 1e3;
1190
- let rawOutput;
1191
- let exitCode = 0;
1192
- try {
1193
- rawOutput = (0, import_child_process.execSync)(cmd, {
1194
- cwd: execCwd,
1195
- timeout,
1196
- encoding: "utf8",
1197
- stdio: ["pipe", "pipe", "pipe"],
1198
- maxBuffer: 10 * 1024 * 1024
1199
- });
1200
- } catch (err) {
1201
- rawOutput = (err.stdout || "") + (err.stderr || "");
1202
- exitCode = err.status ?? 1;
1203
- }
1204
- const result = (0, import_compression.compress)(cmd, rawOutput, { level, preserveErrors: exitCode !== 0 });
1205
- const tracker = new import_tracker.TokenTracker(projectRoot);
1206
- tracker.record(cmd, result.originalTokens, result.compressedTokens, result.strategy);
1207
- const header = exitCode !== 0 ? `[exit ${exitCode}] ` : "";
1208
- const footer = result.savings > 5 ? `
1209
-
1210
- [${result.savings}% tokens saved | ${result.originalTokens}\u2192${result.compressedTokens} | ${result.strategy}]` : "";
1211
- return `${header}${result.output}${footer}`;
1212
- }
1213
- if (toolName === "kirograph_gain") {
1214
- const projectRoot = args.projectPath || process.cwd();
1215
- const period = args.period ?? "session";
1216
- const tracker = new import_tracker.TokenTracker(projectRoot);
1217
- const stats = tracker.getStats(period);
1218
- if (stats.totalCommands === 0) {
1219
- return "No savings recorded yet. Use kirograph tools and kirograph_exec \u2014 savings are tracked automatically.";
1220
- }
1221
- const lines = [
1222
- `Token Savings (${period}):`,
1223
- ` Total calls: ${stats.totalCommands}`,
1224
- ` Tokens without KiroGraph: ~${stats.totalOriginal.toLocaleString()}`,
1225
- ` Tokens with KiroGraph: ~${stats.totalCompressed.toLocaleString()}`,
1226
- ` Saved: ${stats.totalSaved.toLocaleString()} tokens (${stats.savingsPercent}%)`
1227
- ];
1228
- if (stats.bySource.exec.count > 0 || stats.bySource.graph.count > 0 || stats.bySource.memory.count > 0 || stats.bySource.docs.count > 0) {
1229
- lines.push("", "By source:");
1230
- if (stats.bySource.graph.count > 0) {
1231
- lines.push(` Graph tools: ${stats.bySource.graph.count} calls, ~${stats.bySource.graph.saved.toLocaleString()} tokens saved (vs file reads/grep)`);
1232
- }
1233
- if (stats.bySource.docs.count > 0) {
1234
- lines.push(` Docs tools: ${stats.bySource.docs.count} calls, ~${stats.bySource.docs.saved.toLocaleString()} tokens saved (vs reading full doc files)`);
1235
- }
1236
- if (stats.bySource.data.count > 0) {
1237
- lines.push(` Data tools: ${stats.bySource.data.count} calls, ~${stats.bySource.data.saved.toLocaleString()} tokens saved (vs loading raw data files)`);
1238
- }
1239
- if (stats.bySource.exec.count > 0) {
1240
- lines.push(` Compression: ${stats.bySource.exec.count} calls, ~${stats.bySource.exec.saved.toLocaleString()} tokens saved (vs raw output)`);
1241
- }
1242
- if (stats.bySource.memory.count > 0) {
1243
- lines.push(` Memory: ${stats.bySource.memory.count} calls, ~${stats.bySource.memory.saved.toLocaleString()} tokens saved (vs re-discovering context)`);
1244
- }
1245
- }
1246
- if (Object.keys(stats.byFamily).length > 0) {
1247
- lines.push("", "Top families:");
1248
- for (const [family, data] of Object.entries(stats.byFamily).slice(0, 7)) {
1249
- lines.push(` ${family}: ${data.count} calls, ${data.savings}% avg savings`);
1250
- }
1251
- }
1252
- if (stats.recentCommands.length > 0) {
1253
- lines.push("", "Recent:");
1254
- for (const cmd of stats.recentCommands.slice(0, 5)) {
1255
- const tag = cmd.source === "graph" ? "\u{1F4CA}" : "\u26A1";
1256
- lines.push(` ${tag} ${cmd.command.slice(0, 40)} \u2192 ${cmd.savings}% saved`);
1257
- }
1258
- }
1259
- return lines.join("\n");
1260
- }
1261
- if (toolName === "kirograph_read") {
1262
- const filePath = args.path;
1263
- if (!filePath) return "Error: path is required.";
1264
- const projectRoot = args.projectPath || process.cwd();
1265
- const resolvedPath = path.isAbsolute(filePath) ? filePath : path.join(projectRoot, filePath);
1266
- const mode = args.mode ?? "full";
1267
- const noCache = args.noCache ?? false;
1268
- if (!fs.existsSync(resolvedPath)) {
1269
- return `Error: File not found: ${resolvedPath}`;
1270
- }
1271
- const { getFileReadCache } = await Promise.resolve().then(() => require("./cache.js"));
1272
- const { executeReadMode } = await Promise.resolve().then(() => require("./read-modes.js"));
1273
- const cache = getFileReadCache();
1274
- if (mode !== "full") {
1275
- let cg2 = null;
1276
- try {
1277
- cg2 = await this.getConnection(args.projectPath);
1278
- } catch {
1279
- }
1280
- const result2 = executeReadMode({
1281
- mode,
1282
- filePath: resolvedPath,
1283
- start: args.start,
1284
- end: args.end,
1285
- cg: cg2
1286
- });
1287
- cache.read(resolvedPath, true);
1288
- return result2.content;
1289
- }
1290
- const result = cache.read(resolvedPath, noCache);
1291
- if (result.cached) {
1292
- return result.content;
1293
- }
1294
- if (result.changed) {
1295
- return `[file changed since last read]
1296
-
1297
- ${result.content}`;
1298
- }
1299
- return result.content;
1300
- }
1301
- if (toolName === "kirograph_budget") {
1302
- const projectRoot = args.projectPath || process.cwd();
1303
- const reset = args.reset ?? false;
1304
- const { BudgetTracker } = await Promise.resolve().then(() => require("../compression/tracker.js"));
1305
- const budget = BudgetTracker.getInstance(projectRoot);
1306
- if (reset) {
1307
- budget.reset();
1308
- return "Context budget counters reset.";
1309
- }
1310
- const status = budget.getStatus();
1311
- const lines = [
1312
- "Context Budget:",
1313
- ` Tokens consumed: ${status.consumed.toLocaleString()}`,
1314
- ` Budget limit: ${status.limit > 0 ? status.limit.toLocaleString() : "unlimited"}`,
1315
- ` Remaining: ${status.limit > 0 ? status.remaining.toLocaleString() : "\u221E"}`,
1316
- ` Utilization: ${status.utilization}%`
1317
- ];
1318
- if (status.warning) {
1319
- lines.push(`
1320
- \u26A0 ${status.warning}`);
1321
- }
1322
- return lines.join("\n");
1323
- }
1324
- const cg = await this.getConnection(args.projectPath);
1325
- if (!cg) return "KiroGraph not initialized. Run `kirograph init` in your project first.";
1326
- writeSessionMarker(cg.getProjectRoot());
1327
- switch (toolName) {
1328
- case "kirograph_search": {
1329
- const limit = clampLimit(args.limit, 10);
1330
- const results = cg.searchNodes(
1331
- args.query,
1332
- args.kind,
1333
- limit
1334
- );
1335
- if (results.length === 0) return `No symbols found matching "${args.query}".`;
1336
- return results.map(
1337
- (r) => `${mapKind(r.node.kind)} ${r.node.name}
1338
- File: ${r.node.filePath}:${r.node.startLine}
1339
- Qualified: ${r.node.qualifiedName}`
1340
- ).join("\n\n");
1341
- }
1342
- case "kirograph_context": {
1343
- const ctx = await cg.buildContext(args.task, {
1344
- maxNodes: args.maxNodes ?? 20,
1345
- includeCode: args.includeCode ?? true
1346
- });
1347
- const lines = [ctx.summary, ""];
1348
- if (ctx.entryPoints.length === 0) {
1349
- lines.push("No matching symbols found. If this is a new feature, consider using kirograph_files to explore the codebase structure.");
1350
- } else {
1351
- lines.push("## Entry Points");
1352
- for (const n of ctx.entryPoints) {
1353
- lines.push(`- ${mapKind(n.kind)} \`${n.name}\` \u2014 ${n.filePath}:${n.startLine}`);
1354
- if (ctx.codeSnippets.has(n.id)) {
1355
- lines.push("```", ctx.codeSnippets.get(n.id), "```");
1356
- }
1357
- }
1358
- if (ctx.relatedNodes.length > 0) {
1359
- lines.push("", "## Related Symbols");
1360
- for (const n of ctx.relatedNodes.slice(0, 10)) {
1361
- lines.push(`- ${mapKind(n.kind)} \`${n.name}\` \u2014 ${n.filePath}:${n.startLine}`);
1362
- }
1363
- }
1364
- }
1365
- try {
1366
- const { loadConfig } = await Promise.resolve().then(() => require("../config.js"));
1367
- const projectRoot = cg.getProjectRoot();
1368
- const config = await loadConfig(projectRoot);
1369
- if (config.enableMemory) {
1370
- const { MemoryManager } = await Promise.resolve().then(() => require("../memory/index.js"));
1371
- const db = cg.getDatabase();
1372
- db.applyMemorySchema();
1373
- const mem = new MemoryManager(config, db.getRawDb());
1374
- mem.initialize();
1375
- const qualifiedNames = [
1376
- ...ctx.entryPoints.map((n) => n.qualifiedName),
1377
- ...ctx.relatedNodes.slice(0, 5).map((n) => n.qualifiedName)
1378
- ].filter(Boolean);
1379
- const contextLimit = config.memoryContextLimit ?? 3;
1380
- const contextThreshold = config.memoryContextThreshold ?? 0.3;
1381
- let memResults = mem.getLinkedObservations(qualifiedNames, contextLimit, contextThreshold);
1382
- if (memResults.length === 0) {
1383
- memResults = (await mem.search(args.task, { limit: contextLimit })).filter((r) => r.score >= contextThreshold);
1384
- }
1385
- if (memResults.length > 0) {
1386
- lines.push("", "## Related Memory");
1387
- for (const r of memResults.slice(0, contextLimit)) {
1388
- const age = formatAge(r.observation.createdAt);
1389
- lines.push(`- [${r.observation.kind}] ${r.observation.content} (${age})`);
1390
- }
1391
- }
1392
- }
1393
- } catch {
1394
- }
1395
- try {
1396
- const projectRoot2 = cg.getProjectRoot();
1397
- const config2 = await (await Promise.resolve().then(() => require("../config.js"))).loadConfig(projectRoot2);
1398
- if (config2.enableDocs && config2.docsContextLimit > 0) {
1399
- const db2 = cg.getDatabase();
1400
- db2.applyDocsSchema();
1401
- const { DocsQueries } = await Promise.resolve().then(() => require("../docs/queries.js"));
1402
- const docsQueries = new DocsQueries(db2.getRawDb(), projectRoot2);
1403
- const qNames = ctx.entryPoints.map((n) => n.qualifiedName).filter(Boolean);
1404
- if (qNames.length > 0) {
1405
- const docRefs = docsQueries.getRefs({ qualifiedName: qNames[0] });
1406
- const additionalRefs = qNames.slice(1, 5).flatMap((qn) => docsQueries.getRefs({ qualifiedName: qn }));
1407
- const allDocRefs = [...docRefs, ...additionalRefs];
1408
- const seenSections = /* @__PURE__ */ new Set();
1409
- const uniqueRefs = allDocRefs.filter((r) => {
1410
- if (seenSections.has(r.sectionId)) return false;
1411
- seenSections.add(r.sectionId);
1412
- return r.confidence >= config2.docsContextThreshold;
1413
- }).slice(0, config2.docsContextLimit);
1414
- if (uniqueRefs.length > 0) {
1415
- lines.push("", "## Related Documentation");
1416
- for (const ref of uniqueRefs) {
1417
- const section = docsQueries.getSection(ref.sectionId);
1418
- if (section) {
1419
- const summary = section.section.summary ?? section.section.title;
1420
- lines.push(`- [${ref.refType}] ${summary} \u2014 ${section.section.filePath} (ID: ${ref.sectionId})`);
1421
- }
1422
- }
1423
- }
1424
- }
1425
- }
1426
- } catch {
1427
- }
1428
- try {
1429
- const projectRoot3 = cg.getProjectRoot();
1430
- const config3 = await (await Promise.resolve().then(() => require("../config.js"))).loadConfig(projectRoot3);
1431
- if (config3.enableData && config3.dataContextLimit > 0) {
1432
- const db3 = cg.getDatabase();
1433
- db3.applyDataSchema();
1434
- const entryFiles = ctx.entryPoints.map((n) => n.filePath).filter(Boolean);
1435
- if (entryFiles.length > 0) {
1436
- const placeholders = entryFiles.map(() => "?").join(", ");
1437
- const dataRefs = db3.getRawDb().all(
1438
- `SELECT DISTINCT d.id, d.file_path, d.row_count, d.column_count
1439
- FROM data_code_refs r JOIN data_datasets d ON r.dataset_id = d.id
1440
- WHERE r.qualified_name IN (${placeholders})`,
1441
- entryFiles
1442
- );
1443
- if (dataRefs.length > 0) {
1444
- const { DataQueries } = await Promise.resolve().then(() => require("../data/queries.js"));
1445
- const dq = new DataQueries(db3.getRawDb());
1446
- const limit = config3.dataContextLimit;
1447
- lines.push("", "## Related Data");
1448
- for (const ref of dataRefs.slice(0, limit)) {
1449
- const info = dq.describeDataset(ref.id);
1450
- if (info) {
1451
- const colSummary = info.columns.map((c) => `${c.name}:${c.inferredType}`).join(", ");
1452
- lines.push(`- **${ref.id}** (${ref.file_path}) \u2014 ${ref.row_count} rows, ${ref.column_count} cols`);
1453
- lines.push(` Schema: ${colSummary}`);
1454
- }
1455
- }
1456
- }
1457
- }
1458
- }
1459
- } catch {
1460
- }
1461
- try {
1462
- const projectRootSec = cg.getProjectRoot();
1463
- const configSec = await (await Promise.resolve().then(() => require("../config.js"))).loadConfig(projectRootSec);
1464
- if (configSec.enableSecurity) {
1465
- const dbSec = cg.getDatabase();
1466
- dbSec.applySecuritySchema();
1467
- const rawDbSec = dbSec.getRawDb();
1468
- const contextNodeIds = [
1469
- ...ctx.entryPoints.map((n) => n.id),
1470
- ...ctx.relatedNodes.map((n) => n.id)
1471
- ].filter(Boolean);
1472
- if (contextNodeIds.length > 0) {
1473
- const { getSecurityWarningsForNodes, formatSecurityWarnings } = await Promise.resolve().then(() => require("../security/context-warnings.js"));
1474
- const warnings = getSecurityWarningsForNodes(rawDbSec, contextNodeIds);
1475
- if (warnings.length > 0) {
1476
- const nodeNames = /* @__PURE__ */ new Map();
1477
- for (const n of ctx.entryPoints) {
1478
- nodeNames.set(n.id, n.name);
1479
- }
1480
- for (const n of ctx.relatedNodes) {
1481
- nodeNames.set(n.id, n.name);
1482
- }
1483
- const secSection = formatSecurityWarnings(warnings, nodeNames);
1484
- if (secSection) {
1485
- lines.push(secSection);
1486
- }
1487
- }
1488
- }
1489
- }
1490
- } catch {
1491
- }
1492
- try {
1493
- const projectRootPat = cg.getProjectRoot();
1494
- const configPat = await (await Promise.resolve().then(() => require("../config.js"))).loadConfig(projectRootPat);
1495
- if (configPat.enablePatterns) {
1496
- const dbPat = cg.getDatabase();
1497
- dbPat.applyPatternsSchema();
1498
- const rawDbPat = dbPat.getRawDb();
1499
- const tableExists = rawDbPat.get("SELECT name FROM sqlite_master WHERE type='table' AND name='pattern_matches'");
1500
- if (tableExists) {
1501
- const contextFiles = [
1502
- ...ctx.entryPoints.map((n) => n.filePath),
1503
- ...ctx.relatedNodes.map((n) => n.filePath)
1504
- ].filter(Boolean);
1505
- const uniqueContextFiles = [...new Set(contextFiles)];
1506
- if (uniqueContextFiles.length > 0) {
1507
- const placeholders = uniqueContextFiles.map(() => "?").join(",");
1508
- const patternRows = rawDbPat.all(
1509
- `SELECT file_path, pattern_id, severity, owasp_category, line FROM pattern_matches WHERE file_path IN (${placeholders}) ORDER BY severity DESC LIMIT 5`,
1510
- uniqueContextFiles
1511
- );
1512
- if (patternRows.length > 0) {
1513
- const patternWarnings = patternRows.map(
1514
- (p) => `- **${p.pattern_id}** [${p.severity.toUpperCase()}/${p.owasp_category}]: ${p.file_path}:${p.line}`
1515
- ).join("\n");
1516
- lines.push("", "## \u26A0 Pattern Findings", "", patternWarnings);
1517
- if (patternRows.length === 5) lines.push("", "More findings \u2014 run `kirograph pattern --list` to see all rules.");
1518
- }
1519
- }
1520
- }
1521
- }
1522
- } catch {
1523
- }
1524
- const graphTokens = lines.join("\n").length / 4;
1525
- const uniqueFiles = /* @__PURE__ */ new Set([
1526
- ...ctx.entryPoints.map((n) => n.filePath),
1527
- ...ctx.relatedNodes.map((n) => n.filePath)
1528
- ]);
1529
- if (uniqueFiles.size > 0) {
1530
- const naiveTokens = estimateFileTokens(cg.getProjectRoot(), [...uniqueFiles]);
1531
- if (naiveTokens > 0) {
1532
- const savingsPct = Math.round((1 - graphTokens / naiveTokens) * 100);
1533
- if (savingsPct > 0) {
1534
- lines.push("", `---`, `Context savings: ~${Math.round(graphTokens)} tokens (graph) vs ~${naiveTokens} tokens (full files) \u2014 ${savingsPct}% reduction`);
1535
- }
1536
- }
1537
- }
1538
- return lines.join("\n");
1539
- }
1540
- case "kirograph_callers": {
1541
- const limit = clampLimit(args.limit, 20);
1542
- const results = cg.searchNodes(args.symbol, void 0, 5);
1543
- if (results.length === 0) return `Symbol "${args.symbol}" not found in index.`;
1544
- const node = results[0].node;
1545
- const callers = await cg.getCallers(node.id, limit);
1546
- if (callers.length === 0) return `No callers found for \`${node.name}\`.`;
1547
- return `Callers of \`${node.name}\`:
1548
- ` + callers.map(
1549
- (n) => `- ${mapKind(n.kind)} \`${n.name}\` \u2014 ${n.filePath}:${n.startLine}`
1550
- ).join("\n");
1551
- }
1552
- case "kirograph_callees": {
1553
- const limit = clampLimit(args.limit, 20);
1554
- const results = cg.searchNodes(args.symbol, void 0, 5);
1555
- if (results.length === 0) return `Symbol "${args.symbol}" not found in index.`;
1556
- const node = results[0].node;
1557
- const callees = await cg.getCallees(node.id, limit);
1558
- if (callees.length === 0) return `\`${node.name}\` doesn't call any indexed symbols.`;
1559
- return `\`${node.name}\` calls:
1560
- ` + callees.map(
1561
- (n) => `- ${mapKind(n.kind)} \`${n.name}\` \u2014 ${n.filePath}:${n.startLine}`
1562
- ).join("\n");
1563
- }
1564
- case "kirograph_impact": {
1565
- const results = cg.searchNodes(args.symbol, void 0, 5);
1566
- if (results.length === 0) return `Symbol "${args.symbol}" not found in index.`;
1567
- const node = results[0].node;
1568
- const affected = await cg.getImpactRadius(node.id, args.depth ?? 2);
1569
- if (affected.length === 0) return `No dependents found for \`${node.name}\`.`;
1570
- let output = `Changing \`${node.name}\` may affect ${affected.length} symbol(s):
1571
- ` + affected.map((n) => `- ${mapKind(n.kind)} \`${n.name}\` \u2014 ${n.filePath}:${n.startLine}`).join("\n");
1572
- try {
1573
- const { loadConfig } = await Promise.resolve().then(() => require("../config.js"));
1574
- const projectRoot = cg.getProjectRoot();
1575
- const config = await loadConfig(projectRoot);
1576
- if (config.enableMemory) {
1577
- const { MemoryManager } = await Promise.resolve().then(() => require("../memory/index.js"));
1578
- const db = cg.getDatabase();
1579
- db.applyMemorySchema();
1580
- const mem = new MemoryManager(config, db.getRawDb());
1581
- mem.initialize();
1582
- const contextLimit = config.memoryContextLimit ?? 3;
1583
- const contextThreshold = config.memoryContextThreshold ?? 0.3;
1584
- const memResults = mem.getLinkedObservations(
1585
- [node.qualifiedName],
1586
- contextLimit,
1587
- contextThreshold
1588
- );
1589
- if (memResults.length > 0) {
1590
- output += "\n\nRelated Memory:";
1591
- for (const r of memResults) {
1592
- const age = formatAge(r.observation.createdAt);
1593
- output += `
1594
- - [${r.observation.kind}] ${r.observation.content} (${age})`;
1595
- }
1596
- }
1597
- }
1598
- } catch {
1599
- }
1600
- try {
1601
- const rawDbImp = cg.getDatabase().getRawDb();
1602
- const tableExistsImp = rawDbImp.get("SELECT name FROM sqlite_master WHERE type='table' AND name='pattern_matches'");
1603
- if (tableExistsImp) {
1604
- const symNode = rawDbImp.get("SELECT id FROM nodes WHERE name = ? LIMIT 1", [args.symbol]);
1605
- if (symNode) {
1606
- const patternMatches = rawDbImp.all(
1607
- "SELECT pattern_id, severity, line FROM pattern_matches WHERE symbol_node_id = ?",
1608
- [symNode.id]
1609
- );
1610
- if (patternMatches.length > 0) {
1611
- output += "\n\n\u26A0 This symbol has pattern matches:";
1612
- for (const pm of patternMatches) {
1613
- output += `
1614
- [${pm.severity.toUpperCase()}] ${pm.pattern_id} at line ${pm.line}`;
1615
- }
1616
- }
1617
- }
1618
- }
1619
- } catch {
1620
- }
1621
- return output;
1622
- }
1623
- case "kirograph_node": {
1624
- const results = cg.searchNodes(args.symbol, void 0, 5);
1625
- if (results.length === 0) return `Symbol "${args.symbol}" not found in index.`;
1626
- const node = results[0].node;
1627
- const lines = [
1628
- `${mapKind(node.kind)} \`${node.name}\``,
1629
- `File: ${node.filePath}:${node.startLine}-${node.endLine}`,
1630
- `Qualified: ${node.qualifiedName}`,
1631
- node.signature ? `Signature: ${node.signature}` : "",
1632
- node.docstring ? `Docs: ${node.docstring}` : ""
1633
- ].filter(Boolean);
1634
- if (args.includeCode) {
1635
- const src = cg.getNodeSource(node);
1636
- if (src) lines.push("", "```", src, "```");
1637
- }
1638
- return lines.join("\n");
1639
- }
1640
- case "kirograph_status": {
1641
- const stats = await cg.getStats();
1642
- const langLine = Object.entries(stats.filesByLanguage).sort((a, b) => b[1] - a[1]).map(([k, v]) => `${k}=${v}`).join(", ");
1643
- const dbMb = (stats.dbSizeBytes / 1024 / 1024).toFixed(2);
1644
- const semanticLines = stats.embeddingsEnabled ? [
1645
- ` Semantic search: enabled`,
1646
- ` Semantic model: ${stats.embeddingModel}`,
1647
- ` Semantic engine: ${stats.semanticEngine === "turboquant" ? `turboquant (${stats.vecIndexCount} entries \xB7 ANN)` : stats.semanticEngine === "turbovec" ? `turbovec (${stats.vecIndexCount} entries \xB7 ${stats.turbovecBits ?? 4} bits \xB7 Rust/SIMD)` : stats.semanticEngine === "sqlite-vec" ? `sqlite-vec (${stats.vecIndexCount} entries in ANN index)` : stats.semanticEngine === "orama" ? `orama hybrid (${stats.vecIndexCount} docs in index)` : stats.semanticEngine === "pglite" ? `pglite+pgvector (${stats.vecIndexCount} rows in DB)` : stats.semanticEngine === "lancedb" ? `lancedb (${stats.vecIndexCount} entries in ANN index)` : stats.semanticEngine === "qdrant" ? `qdrant (${stats.vecIndexCount} points in collection)` : stats.semanticEngine === "typesense" ? `typesense (${stats.vecIndexCount} documents in collection)` : "in-process cosine"}`,
1648
- ` Embeddings: ${stats.embeddingCount} / ${stats.embeddableNodeCount || stats.nodes} embeddable symbols`,
1649
- ...stats.engineFallback ? [` \u26A0 Engine fallback: ${stats.engineFallback}`] : []
1650
- ] : [` Semantic search: disabled`];
1651
- const frameworkLine = stats.frameworks.length > 0 ? ` Frameworks: ${stats.frameworks.join(", ")}` : ` Frameworks: none detected`;
1652
- const archLine = stats.architectureEnabled ? stats.architectureStats ? ` Architecture: enabled \u2014 ${stats.architectureStats.packages} packages, ${stats.architectureStats.layers} layers, ${stats.architectureStats.packageDeps} deps` : ` Architecture: enabled (not yet analyzed \u2014 run kirograph index)` : ` Architecture: disabled`;
1653
- let docsLine = " Documentation: disabled";
1654
- try {
1655
- const { loadConfig: loadCfg } = await Promise.resolve().then(() => require("../config.js"));
1656
- const cfg = await loadCfg(cg.getProjectRoot());
1657
- if (cfg.enableDocs) {
1658
- const db = cg.getDatabase();
1659
- db.applyDocsSchema();
1660
- const rawDb = db.getRawDb();
1661
- const docFiles = rawDb.get("SELECT COUNT(DISTINCT file_path) as cnt FROM doc_sections")?.cnt ?? 0;
1662
- const docSections = rawDb.get("SELECT COUNT(*) as cnt FROM doc_sections")?.cnt ?? 0;
1663
- const docRefs = rawDb.get("SELECT COUNT(*) as cnt FROM doc_code_refs")?.cnt ?? 0;
1664
- docsLine = ` Documentation: enabled \u2014 ${docFiles} files, ${docSections} sections, ${docRefs} code refs`;
1665
- }
1666
- } catch {
1667
- }
1668
- let dataLine = " Data: disabled";
1669
- try {
1670
- const { loadConfig: loadCfg2 } = await Promise.resolve().then(() => require("../config.js"));
1671
- const cfg2 = await loadCfg2(cg.getProjectRoot());
1672
- if (cfg2.enableData) {
1673
- const rawDb2 = cg.getDatabase().getRawDb();
1674
- cg.getDatabase().applyDataSchema();
1675
- const datasetCount = rawDb2.get("SELECT COUNT(*) as cnt FROM data_datasets")?.cnt ?? 0;
1676
- if (datasetCount > 0) {
1677
- const totalRows = rawDb2.get("SELECT SUM(row_count) as total FROM data_datasets")?.total ?? 0;
1678
- const totalCols = rawDb2.get("SELECT SUM(column_count) as total FROM data_datasets")?.total ?? 0;
1679
- const totalSize = rawDb2.get("SELECT SUM(file_size) as total FROM data_datasets")?.total ?? 0;
1680
- const sizeMb = (totalSize / 1024 / 1024).toFixed(2);
1681
- dataLine = ` Data: enabled \u2014 ${datasetCount} datasets, ${totalRows.toLocaleString()} rows, ${totalCols} columns (${sizeMb} MB source)`;
1682
- } else {
1683
- dataLine = ` Data: enabled (no datasets indexed yet \u2014 run kirograph index)`;
1684
- }
1685
- }
1686
- } catch {
1687
- }
1688
- let securityLine = " Security: disabled";
1689
- try {
1690
- const { loadConfig: loadCfgSec } = await Promise.resolve().then(() => require("../config.js"));
1691
- const cfgSec = await loadCfgSec(cg.getProjectRoot());
1692
- if (cfgSec.enableSecurity) {
1693
- const db = cg.getDatabase();
1694
- db.applySecuritySchema();
1695
- const rawDb = db.getRawDb();
1696
- const depCount = rawDb.get("SELECT COUNT(*) as cnt FROM sec_dependencies")?.cnt ?? 0;
1697
- const vulnCount = rawDb.get("SELECT COUNT(*) as cnt FROM sec_vulnerabilities")?.cnt ?? 0;
1698
- const affectedCount = rawDb.get("SELECT COUNT(*) as cnt FROM sec_reachability WHERE verdict = 'affected'")?.cnt ?? 0;
1699
- const staleCount = rawDb.get("SELECT COUNT(*) as cnt FROM sec_dependencies WHERE vuln_data_stale = 1")?.cnt ?? 0;
1700
- const staleNote = staleCount > 0 ? ` \u26A0 ${staleCount} stale` : "";
1701
- securityLine = ` Security: enabled \u2014 ${depCount} deps, ${vulnCount} vulns (${affectedCount} affected)${staleNote}`;
1702
- }
1703
- } catch {
1704
- }
1705
- let patternsLine = " Patterns: disabled";
1706
- try {
1707
- const { loadConfig: loadCfgPat } = await Promise.resolve().then(() => require("../config.js"));
1708
- const cfgP = await loadCfgPat(cg.getProjectRoot());
1709
- if (cfgP.enablePatterns) {
1710
- const rawDbP = cg.getDatabase().getRawDb();
1711
- const tableExistsP = rawDbP.get("SELECT name FROM sqlite_master WHERE type='table' AND name='pattern_matches'");
1712
- if (tableExistsP) {
1713
- const matchCount = rawDbP.get("SELECT COUNT(*) as cnt FROM pattern_matches")?.cnt ?? 0;
1714
- const fileCount = rawDbP.get("SELECT COUNT(DISTINCT file_path) as cnt FROM pattern_matches")?.cnt ?? 0;
1715
- const ruleCount = rawDbP.get("SELECT COUNT(DISTINCT pattern_id) as cnt FROM pattern_matches")?.cnt ?? 0;
1716
- patternsLine = ` Patterns: enabled \u2014 ${matchCount} matches, ${ruleCount} rules triggered, ${fileCount} files`;
1717
- } else {
1718
- patternsLine = ` Patterns: enabled (no data yet \u2014 run kirograph index)`;
1719
- }
1720
- }
1721
- } catch {
1722
- }
1723
- const threshold = stats.syncWarningThreshold ?? 10;
1724
- const pendingFiles = stats.pendingFiles ?? 0;
1725
- const syncRunning = stats.syncRunning ?? false;
1726
- const syncLines = [];
1727
- if (syncRunning) {
1728
- syncLines.push(` \u26A0 Sync is currently running in the background.`);
1729
- }
1730
- if (threshold > 0 && pendingFiles >= threshold) {
1731
- syncLines.push(
1732
- ` \u26A0 Index may be incomplete \u2014 ${pendingFiles} file${pendingFiles !== 1 ? "s" : ""} pending sync.` + (syncRunning ? " Sync is running in background." : " Run `kirograph sync` to update.") + ` Would you like to wait before proceeding?`
1733
- );
1734
- }
1735
- const tracker = new import_tracker.TokenTracker(cg.getProjectRoot());
1736
- const gainStats = tracker.getStats("session");
1737
- const gainLines = [];
1738
- if (gainStats.totalCommands > 0) {
1739
- gainLines.push(` Compression: ${gainStats.totalCommands} commands, ${gainStats.savingsPercent}% avg savings (${gainStats.totalSaved.toLocaleString()} tokens saved this session)`);
1740
- }
1741
- return [
1742
- `KiroGraph Status`,
1743
- ` Project: ${cg.getProjectRoot()}`,
1744
- ` Files indexed: ${stats.files}`,
1745
- ` Symbols: ${stats.nodes}`,
1746
- ` Relationships: ${stats.edges}`,
1747
- ` By kind: ${Object.entries(stats.nodesByKind).map(([k, v]) => `${k}=${v}`).join(", ")}`,
1748
- langLine ? ` By language: ${langLine}` : "",
1749
- frameworkLine,
1750
- archLine,
1751
- docsLine,
1752
- dataLine,
1753
- securityLine,
1754
- patternsLine,
1755
- ` DB size: ${dbMb} MB`,
1756
- ...semanticLines,
1757
- ...syncLines,
1758
- ...gainLines
1759
- ].filter(Boolean).join("\n");
1760
- }
1761
- case "kirograph_files": {
1762
- let renderTree2 = function(nodes, prefix) {
1763
- for (let i = 0; i < nodes.length; i++) {
1764
- const node = nodes[i];
1765
- const isLast = i === nodes.length - 1;
1766
- const connector = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
1767
- const childPrefix = prefix + (isLast ? " " : "\u2502 ");
1768
- const meta = includeMetadata && node.type === "file" && node.language ? ` [${node.language}${node.symbolCount ? ` \xB7 ${node.symbolCount} symbols` : ""}]` : "";
1769
- lines.push(`${prefix}${connector}${node.name}${meta}`);
1770
- if (node.children?.length) renderTree2(node.children, childPrefix);
1771
- }
1772
- };
1773
- var renderTree = renderTree2;
1774
- const format = args.format ?? "tree";
1775
- const includeMetadata = args.includeMetadata !== false;
1776
- const tree = cg.getFiles({
1777
- filterPath: args.filterPath,
1778
- pattern: args.pattern,
1779
- maxDepth: args.maxDepth
1780
- });
1781
- if (format === "flat") {
1782
- let flattenTree2 = function(nodes) {
1783
- for (const node of nodes) {
1784
- if (node.type === "file") {
1785
- const meta = includeMetadata && node.language ? ` [${node.language}${node.symbolCount ? ` \xB7 ${node.symbolCount}` : ""}]` : "";
1786
- flat.push(`${node.path}${meta}`);
1787
- }
1788
- if (node.children?.length) flattenTree2(node.children);
1789
- }
1790
- };
1791
- var flattenTree = flattenTree2;
1792
- const flat = [];
1793
- flattenTree2(tree);
1794
- return flat.length > 0 ? flat.join("\n") : "No indexed files found.";
1795
- }
1796
- if (format === "grouped") {
1797
- let groupTree2 = function(nodes) {
1798
- for (const node of nodes) {
1799
- if (node.type === "file") {
1800
- const dir = node.path.includes("/") ? node.path.slice(0, node.path.lastIndexOf("/")) : ".";
1801
- if (!groups.has(dir)) groups.set(dir, []);
1802
- groups.get(dir).push(node);
1803
- }
1804
- if (node.children?.length) groupTree2(node.children);
1805
- }
1806
- };
1807
- var groupTree = groupTree2;
1808
- const groups = /* @__PURE__ */ new Map();
1809
- groupTree2(tree);
1810
- const lines2 = [];
1811
- for (const [dir, files] of [...groups.entries()].sort()) {
1812
- lines2.push(`${dir}/`);
1813
- for (const f of files) {
1814
- const meta = includeMetadata && f.language ? ` [${f.language}${f.symbolCount ? ` \xB7 ${f.symbolCount}` : ""}]` : "";
1815
- lines2.push(` ${f.name}${meta}`);
1816
- }
1817
- }
1818
- return lines2.length > 0 ? lines2.join("\n") : "No indexed files found.";
1819
- }
1820
- if (format === "compact") {
1821
- let compactTree2 = function(nodes) {
1822
- for (const node of nodes) {
1823
- if (node.type === "file") {
1824
- const dir = node.path.includes("/") ? node.path.slice(0, node.path.lastIndexOf("/")) : ".";
1825
- const stat = dirStats.get(dir) || { files: 0, symbols: 0, langs: /* @__PURE__ */ new Map() };
1826
- stat.files++;
1827
- stat.symbols += node.symbolCount || 0;
1828
- if (node.language) stat.langs.set(node.language, (stat.langs.get(node.language) || 0) + 1);
1829
- dirStats.set(dir, stat);
1830
- }
1831
- if (node.children?.length) compactTree2(node.children);
1832
- }
1833
- };
1834
- var compactTree = compactTree2;
1835
- const dirStats = /* @__PURE__ */ new Map();
1836
- compactTree2(tree);
1837
- const totalFiles = [...dirStats.values()].reduce((s, d) => s + d.files, 0);
1838
- const totalSymbols = [...dirStats.values()].reduce((s, d) => s + d.symbols, 0);
1839
- const lines2 = [`${totalFiles} files, ${totalSymbols} symbols in ${dirStats.size} directories:
1840
- `];
1841
- for (const [dir, stat] of [...dirStats.entries()].sort()) {
1842
- const langSummary = [...stat.langs.entries()].map(([l, c]) => `${l}:${c}`).join(" ");
1843
- lines2.push(`${dir}/ (${stat.files} files, ${stat.symbols} symbols) ${langSummary}`);
1844
- }
1845
- return lines2.join("\n");
1846
- }
1847
- const lines = [];
1848
- renderTree2(tree, "");
1849
- return lines.length > 0 ? lines.join("\n") : "No indexed files found.";
1850
- }
1851
- case "kirograph_dead_code": {
1852
- const limit = clampLimit(args.limit, 50);
1853
- const dead = cg.findDeadCode(limit);
1854
- if (dead.length === 0) return "No dead code detected.";
1855
- return `Potential dead code (${dead.length} unexported symbols with no incoming references):
1856
- ` + dead.map((n) => `- ${mapKind(n.kind)} \`${n.name}\` \u2014 ${n.filePath}:${n.startLine}`).join("\n");
1857
- }
1858
- case "kirograph_circular_deps": {
1859
- const cycles = cg.findCircularDependencies();
1860
- if (cycles.length === 0) return "No circular dependencies found.";
1861
- return `Found ${cycles.length} circular dependency cycle(s):
1862
- ` + cycles.map((cycle, i) => `Cycle ${i + 1}: ${cycle.join(" \u2192 ")}`).join("\n");
1863
- }
1864
- case "kirograph_path": {
1865
- const fromResults = cg.searchNodes(args.from, void 0, 3);
1866
- const toResults = cg.searchNodes(args.to, void 0, 3);
1867
- if (fromResults.length === 0) return `Symbol "${args.from}" not found in index.`;
1868
- if (toResults.length === 0) return `Symbol "${args.to}" not found in index.`;
1869
- const fromNode = fromResults[0].node;
1870
- const toNode = toResults[0].node;
1871
- const pathNodes = await cg.findPath(fromNode.id, toNode.id);
1872
- if (pathNodes.length === 0) return `No path found between \`${fromNode.name}\` and \`${toNode.name}\`.`;
1873
- return `Path from \`${fromNode.name}\` to \`${toNode.name}\` (${pathNodes.length} nodes):
1874
- ` + pathNodes.map((n, i) => `${i + 1}. ${mapKind(n.kind)} \`${n.name}\` \u2014 ${n.filePath}:${n.startLine}`).join("\n");
1875
- }
1876
- case "kirograph_type_hierarchy": {
1877
- const results = cg.searchNodes(args.symbol, void 0, 5);
1878
- if (results.length === 0) return `Symbol "${args.symbol}" not found in index.`;
1879
- const node = results[0].node;
1880
- const direction = args.direction ?? "both";
1881
- const hierarchy = cg.getTypeHierarchy(node.id, direction);
1882
- if (hierarchy.length === 0) return `No type hierarchy found for \`${node.name}\`.`;
1883
- return `Type hierarchy for \`${node.name}\` (direction: ${direction}):
1884
- ` + hierarchy.map((n) => `- ${mapKind(n.kind)} \`${n.name}\` \u2014 ${n.filePath}:${n.startLine}`).join("\n");
1885
- }
1886
- case "kirograph_architecture": {
1887
- if (!cg.isArchitectureEnabled()) {
1888
- return "Architecture analysis is disabled. Set enableArchitecture=true in .kirograph/config.json and re-index.";
1889
- }
1890
- const level = args.level ?? "both";
1891
- const includeFiles = args.includeFiles === true;
1892
- const arch = cg.getArchitecture();
1893
- const lines = ["# Architecture"];
1894
- if ((level === "packages" || level === "both") && arch.packages.length > 0) {
1895
- lines.push("\n## Packages");
1896
- for (const pkg of arch.packages) {
1897
- const meta = [pkg.language, pkg.version].filter(Boolean).join(", ");
1898
- lines.push(`- **${pkg.name}** (${pkg.path}) [${pkg.source}${meta ? " \xB7 " + meta : ""}]`);
1899
- }
1900
- if (arch.packageDeps.length > 0) {
1901
- lines.push("\n## Package Dependencies");
1902
- for (const dep of arch.packageDeps) {
1903
- const src = arch.packages.find((p) => p.id === dep.sourcePkg)?.name ?? dep.sourcePkg;
1904
- const tgt = arch.packages.find((p) => p.id === dep.targetPkg)?.name ?? dep.targetPkg;
1905
- lines.push(`- ${src} \u2192 ${tgt} (${dep.depCount} import${dep.depCount !== 1 ? "s" : ""})`);
1906
- }
1907
- }
1908
- }
1909
- if ((level === "layers" || level === "both") && arch.layers.length > 0) {
1910
- lines.push("\n## Layers");
1911
- for (const layer of arch.layers) {
1912
- const fileCount = Object.values(arch.fileLayers).filter((fl) => fl.some((l) => l.layerId === layer.id)).length;
1913
- lines.push(`- **${layer.name}** [${layer.source}] \u2014 ${fileCount} file${fileCount !== 1 ? "s" : ""}`);
1914
- }
1915
- if (arch.layerDeps.length > 0) {
1916
- lines.push("\n## Layer Dependencies");
1917
- for (const dep of arch.layerDeps) {
1918
- const src = dep.sourceLayer.replace("layer:", "");
1919
- const tgt = dep.targetLayer.replace("layer:", "");
1920
- lines.push(`- ${src} \u2192 ${tgt} (${dep.depCount})`);
1921
- }
1922
- }
1923
- }
1924
- if (includeFiles && (level === "packages" || level === "both")) {
1925
- lines.push("\n## File \u2192 Package");
1926
- for (const [file, pkgIds] of Object.entries(arch.filePackages).slice(0, 50)) {
1927
- const names = pkgIds.map((id) => arch.packages.find((p) => p.id === id)?.name ?? id).join(", ");
1928
- lines.push(`- ${file}: ${names}`);
1929
- }
1930
- if (Object.keys(arch.filePackages).length > 50) lines.push(" \u2026(truncated)");
1931
- }
1932
- if (arch.packages.length === 0 && arch.layers.length === 0) {
1933
- return "No architecture data found. Run `kirograph index` with enableArchitecture=true.";
1934
- }
1935
- return lines.join("\n");
1936
- }
1937
- case "kirograph_coupling": {
1938
- if (!cg.isArchitectureEnabled()) {
1939
- return "Architecture analysis is disabled. Set enableArchitecture=true in .kirograph/config.json and re-index.";
1940
- }
1941
- const sortBy = args.sortBy ?? "instability";
1942
- const limit = clampLimit(args.limit, 20);
1943
- const arch = cg.getArchitecture();
1944
- if (arch.coupling.length === 0) {
1945
- return "No coupling data. Run `kirograph index` with enableArchitecture=true.";
1946
- }
1947
- const sorted = [...arch.coupling].sort((a, b) => {
1948
- if (sortBy === "afferent") return b.afferent - a.afferent;
1949
- if (sortBy === "efferent") return b.efferent - a.efferent;
1950
- return b.instability - a.instability;
1951
- }).slice(0, limit);
1952
- const lines = [
1953
- `Coupling Metrics (sorted by ${sortBy}):`,
1954
- "",
1955
- "Package Ca Ce I",
1956
- "\u2500".repeat(52)
1957
- ];
1958
- for (const c of sorted) {
1959
- const pkg = arch.packages.find((p) => p.id === c.packageId);
1960
- const name = (pkg?.name ?? c.packageId).slice(0, 32).padEnd(32);
1961
- const ca = String(c.afferent).padStart(4);
1962
- const ce = String(c.efferent).padStart(4);
1963
- const inst = c.instability.toFixed(2).padStart(5);
1964
- lines.push(`${name} ${ca} ${ce} ${inst}`);
1965
- }
1966
- lines.push("", "Ca=afferent (depended on by), Ce=efferent (depends on), I=instability (Ce/(Ca+Ce))");
1967
- return lines.join("\n");
1968
- }
1969
- case "kirograph_package": {
1970
- if (!cg.isArchitectureEnabled()) {
1971
- return "Architecture analysis is disabled. Set enableArchitecture=true in .kirograph/config.json and re-index.";
1972
- }
1973
- const query = args.package.toLowerCase();
1974
- const includeFiles = args.includeFiles !== false;
1975
- const arch = cg.getArchitecture();
1976
- const pkg = arch.packages.find(
1977
- (p) => p.name.toLowerCase().includes(query) || p.path.toLowerCase().includes(query) || p.id.toLowerCase().includes(query)
1978
- );
1979
- if (!pkg) return `Package "${args.package}" not found. Use kirograph_architecture to list all packages.`;
1980
- const lines = [
1981
- `## Package: ${pkg.name}`,
1982
- `Path: ${pkg.path}`,
1983
- `Source: ${pkg.source}${pkg.manifestPath ? ` (${pkg.manifestPath})` : ""}`,
1984
- ...pkg.version ? [`Version: ${pkg.version}`] : [],
1985
- ...pkg.language ? [`Language: ${pkg.language}`] : []
1986
- ];
1987
- const deps = arch.packageDeps.filter((d) => d.sourcePkg === pkg.id);
1988
- const dependents = arch.packageDeps.filter((d) => d.targetPkg === pkg.id);
1989
- const coupling = arch.coupling.find((c) => c.packageId === pkg.id);
1990
- if (coupling) {
1991
- lines.push("", `Coupling: Ca=${coupling.afferent} Ce=${coupling.efferent} I=${coupling.instability.toFixed(2)}`);
1992
- }
1993
- if (deps.length > 0) {
1994
- lines.push("", `Depends on (${deps.length}):`);
1995
- for (const dep of deps) {
1996
- const name = arch.packages.find((p) => p.id === dep.targetPkg)?.name ?? dep.targetPkg;
1997
- lines.push(` \u2192 ${name} (${dep.depCount} import${dep.depCount !== 1 ? "s" : ""})`);
1998
- }
1999
- }
2000
- if (dependents.length > 0) {
2001
- lines.push("", `Depended on by (${dependents.length}):`);
2002
- for (const dep of dependents) {
2003
- const name = arch.packages.find((p) => p.id === dep.sourcePkg)?.name ?? dep.sourcePkg;
2004
- lines.push(` \u2190 ${name} (${dep.depCount} import${dep.depCount !== 1 ? "s" : ""})`);
2005
- }
2006
- }
2007
- if (pkg.externalDeps && pkg.externalDeps.length > 0) {
2008
- lines.push("", `External deps (${pkg.externalDeps.length}): ${pkg.externalDeps.slice(0, 10).join(", ")}${pkg.externalDeps.length > 10 ? "\u2026" : ""}`);
2009
- }
2010
- if (includeFiles) {
2011
- const files = Object.entries(arch.filePackages).filter(([, ids]) => ids.includes(pkg.id)).map(([f]) => f).sort();
2012
- if (files.length > 0) {
2013
- lines.push("", `Files (${files.length}):`);
2014
- for (const f of files.slice(0, 30)) lines.push(` ${f}`);
2015
- if (files.length > 30) lines.push(` \u2026and ${files.length - 30} more`);
2016
- }
2017
- }
2018
- return lines.join("\n");
2019
- }
2020
- case "kirograph_hotspots": {
2021
- const limit = clampLimit(args.limit, 20);
2022
- const hotspots = cg.findHotspots(limit);
2023
- if (hotspots.length === 0) return "No symbols found in index.";
2024
- const lines = [`Top ${hotspots.length} most-connected symbols (by edge degree):
2025
- `];
2026
- for (const n of hotspots) {
2027
- lines.push(`${mapKind(n.kind)} \`${n.name}\` \u2014 degree ${n.degree} (in: ${n.inDegree}, out: ${n.outDegree})`);
2028
- lines.push(` File: ${n.filePath}:${n.startLine}`);
2029
- }
2030
- return lines.join("\n");
2031
- }
2032
- case "kirograph_surprising": {
2033
- const limit = clampLimit(args.limit, 20);
2034
- const connections = cg.findSurprisingConnections(limit);
2035
- if (connections.length === 0) return "No surprising cross-file connections found.";
2036
- const lines = [`Top ${connections.length} surprising cross-file connections:
2037
- `];
2038
- for (const c of connections) {
2039
- lines.push(`${mapKind(c.source.kind)} \`${c.source.name}\` ${c.kind}\u2192 ${mapKind(c.target.kind)} \`${c.target.name}\` (score: ${c.score.toFixed(2)})`);
2040
- lines.push(` ${c.source.filePath} \u2192 ${c.target.filePath}`);
2041
- }
2042
- return lines.join("\n");
2043
- }
2044
- case "kirograph_diff": {
2045
- const sm = cg.createSnapshotManager();
2046
- const snapshot = args.snapshot ? sm.load(args.snapshot) : sm.loadLatest();
2047
- if (!snapshot) {
2048
- return args.snapshot ? `Snapshot "${args.snapshot}" not found. Use \`kirograph snapshot list\` to see available snapshots.` : "No snapshots found. Run `kirograph snapshot` to save one first.";
2049
- }
2050
- const diff = sm.diff(snapshot, sm.currentSnapshot());
2051
- const fromDate = new Date(diff.from.timestamp).toISOString().slice(0, 19).replace("T", " ");
2052
- const lines = [
2053
- `Graph diff: "${diff.from.label}" (${fromDate}) \u2192 current`,
2054
- ``,
2055
- `Symbols: +${diff.addedNodes.length} added, -${diff.removedNodes.length} removed`,
2056
- `Edges: +${diff.addedEdges.length} added, -${diff.removedEdges.length} removed`
2057
- ];
2058
- if (diff.addedNodes.length > 0) {
2059
- lines.push(`
2060
- ## Added symbols (${diff.addedNodes.length})`);
2061
- for (const n of diff.addedNodes.slice(0, 30)) {
2062
- lines.push(`+ ${mapKind(n.kind)} \`${n.name}\` \u2014 ${n.filePath}`);
2063
- }
2064
- if (diff.addedNodes.length > 30) lines.push(` \u2026and ${diff.addedNodes.length - 30} more`);
2065
- }
2066
- if (diff.removedNodes.length > 0) {
2067
- lines.push(`
2068
- ## Removed symbols (${diff.removedNodes.length})`);
2069
- for (const n of diff.removedNodes.slice(0, 30)) {
2070
- lines.push(`- ${mapKind(n.kind)} \`${n.name}\` \u2014 ${n.filePath}`);
2071
- }
2072
- if (diff.removedNodes.length > 30) lines.push(` \u2026and ${diff.removedNodes.length - 30} more`);
2073
- }
2074
- return lines.join("\n");
2075
- }
2076
- case "kirograph_flows": {
2077
- const { getExecutionFlows, traceFlow, detectEntryPoints } = await Promise.resolve().then(() => require("../graph/flows.js"));
2078
- const db = cg.getDatabase();
2079
- if (args.entryPoint) {
2080
- const results = cg.searchNodes(args.entryPoint, void 0, 5);
2081
- if (results.length === 0) return `Symbol "${args.entryPoint}" not found in index.`;
2082
- const hops = traceFlow(db, results[0].node.id, args.maxDepth ?? 10);
2083
- if (hops.length < 2) return `No outgoing call chain found from "${args.entryPoint}".`;
2084
- const lines2 = [`## Execution flow from \`${args.entryPoint}\``, ""];
2085
- for (let i = 0; i < hops.length; i++) {
2086
- const hop = hops[i];
2087
- const indent = " ".repeat(i);
2088
- const arrow = i === 0 ? "\u2192" : "\u21B3";
2089
- const conf = hop.confidence && hop.confidence !== "extracted" ? ` [${hop.confidence}]` : "";
2090
- lines2.push(`${indent}${arrow} ${hop.kind} \`${hop.symbol}\` \u2014 ${hop.filePath}:${hop.line}${conf}`);
2091
- }
2092
- return lines2.join("\n");
2093
- }
2094
- const flows = getExecutionFlows(db, {
2095
- maxFlows: args.maxFlows ?? 10,
2096
- maxDepth: args.maxDepth ?? 10
2097
- });
2098
- if (flows.length === 0) return "No execution flows detected. The graph may be too small or have no call edges.";
2099
- const lines = [`## Execution Flows (${flows.length} detected)`, ""];
2100
- for (const flow of flows) {
2101
- lines.push(`### \`${flow.entryPoint}\` (${flow.entryPointKind}) \u2014 criticality: ${flow.criticality.toFixed(2)}`);
2102
- lines.push(`File: ${flow.entryPointFile}`, "");
2103
- for (let i = 0; i < flow.hops.length; i++) {
2104
- const hop = flow.hops[i];
2105
- const indent = " ".repeat(Math.min(i, 5));
2106
- const arrow = i === 0 ? "\u2192" : "\u21B3";
2107
- const conf = hop.confidence && hop.confidence !== "extracted" ? ` [${hop.confidence}]` : "";
2108
- lines.push(`${indent}${arrow} \`${hop.symbol}\` (${hop.kind}) \u2014 ${hop.filePath}:${hop.line}${conf}`);
2109
- }
2110
- lines.push("");
2111
- }
2112
- return lines.join("\n");
2113
- }
2114
- case "kirograph_communities": {
2115
- const { detectCommunities } = await Promise.resolve().then(() => require("../graph/communities.js"));
2116
- const db = cg.getDatabase();
2117
- const result = detectCommunities(db, {
2118
- resolution: args.resolution ?? 1
2119
- });
2120
- if (result.communities.length === 0) return "No communities detected. The graph may be too small or have no edges.";
2121
- const limit = Math.min(args.limit ?? 15, result.communities.length);
2122
- const lines = [
2123
- `## Communities (${result.communities.length} detected, modularity: ${result.modularity.toFixed(3)})`,
2124
- `Graph: ${result.totalNodes} nodes, ${result.totalEdges} edges`,
2125
- ""
2126
- ];
2127
- for (const c of result.communities.slice(0, limit)) {
2128
- lines.push(`### ${c.label} (${c.memberCount} symbols)`);
2129
- lines.push(`- Directory: \`${c.dominantDirectory}\``);
2130
- lines.push(`- Language: ${c.dominantLanguage}`);
2131
- lines.push(`- Inter-community edges: ${c.interCommunityEdges}`);
2132
- lines.push(`- Top members:`);
2133
- for (const m of c.members.slice(0, 8)) {
2134
- lines.push(` - ${m.kind} \`${m.name}\` \u2014 ${m.filePath}`);
2135
- }
2136
- if (c.memberCount > 8) lines.push(` - \u2026and ${c.memberCount - 8} more`);
2137
- lines.push("");
2138
- }
2139
- return lines.join("\n");
2140
- }
2141
- case "kirograph_refactor": {
2142
- const { renamePreview, suggestRefactorings } = await Promise.resolve().then(() => require("../graph/refactor.js"));
2143
- const db = cg.getDatabase();
2144
- const mode = args.mode;
2145
- if (mode === "rename") {
2146
- if (!args.symbol) return 'Error: "symbol" parameter is required for rename mode.';
2147
- const preview = renamePreview(db, args.symbol);
2148
- if (!preview) return `Symbol "${args.symbol}" not found in index.`;
2149
- const lines = [
2150
- `## Rename Preview: \`${preview.symbol}\``,
2151
- `Kind: ${preview.kind}`,
2152
- `Defined at: ${preview.filePath}:${preview.line}`,
2153
- `Total references: ${preview.totalReferences}`,
2154
- ""
2155
- ];
2156
- if (preview.references.length === 0) {
2157
- lines.push("No references found \u2014 this symbol can be safely renamed without affecting other code.");
2158
- } else {
2159
- const byFile = /* @__PURE__ */ new Map();
2160
- for (const ref of preview.references) {
2161
- if (!byFile.has(ref.filePath)) byFile.set(ref.filePath, []);
2162
- byFile.get(ref.filePath).push(ref);
2163
- }
2164
- for (const [file, refs] of byFile) {
2165
- lines.push(`### ${file} (${refs.length} references)`);
2166
- for (const ref of refs.slice(0, 10)) {
2167
- lines.push(`- Line ${ref.line}: \`${ref.context}\` (${ref.edgeKind})`);
2168
- }
2169
- if (refs.length > 10) lines.push(` \u2026and ${refs.length - 10} more`);
2170
- lines.push("");
2171
- }
2172
- }
2173
- return lines.join("\n");
2174
- }
2175
- if (mode === "suggest") {
2176
- const suggestions = suggestRefactorings(db, args.limit ?? 10);
2177
- if (suggestions.length === 0) return "No refactoring suggestions \u2014 the codebase structure looks clean.";
2178
- const lines = [`## Refactoring Suggestions (${suggestions.length})`, ""];
2179
- for (const s of suggestions) {
2180
- const icon = s.priority === "high" ? "\u{1F534}" : s.priority === "medium" ? "\u{1F7E1}" : "\u{1F7E2}";
2181
- lines.push(`${icon} **${s.type}** [${s.priority}]: ${s.description}`);
2182
- lines.push(` Rationale: ${s.rationale}`);
2183
- if (s.symbols.length > 0) {
2184
- lines.push(` Symbols: ${s.symbols.slice(0, 3).join(", ")}`);
2185
- }
2186
- lines.push("");
2187
- }
2188
- return lines.join("\n");
2189
- }
2190
- return 'Unknown mode. Use "rename" or "suggest".';
2191
- }
2192
- // ── Memory tools ────────────────────────────────────────────────────────
2193
- case "kirograph_mem_search": {
2194
- const { loadConfig } = await Promise.resolve().then(() => require("../config.js"));
2195
- const projectRoot = cg.getProjectRoot();
2196
- const config = await loadConfig(projectRoot);
2197
- if (!config.enableMemory) return "Memory is not enabled. Set enableMemory: true in .kirograph/config.json";
2198
- const { MemoryManager } = await Promise.resolve().then(() => require("../memory/index.js"));
2199
- const db = cg.getDatabase();
2200
- db.applyMemorySchema();
2201
- const mem = new MemoryManager(config, db.getRawDb());
2202
- mem.initialize();
2203
- const results = await mem.search(args.query, {
2204
- limit: args.limit ?? 10,
2205
- kind: args.kind,
2206
- sessionId: args.sessionId,
2207
- asOf: args.asOf
2208
- });
2209
- if (results.length === 0) return `No memory observations found for "${args.query}".`;
2210
- return results.map((r, i) => {
2211
- const age = formatAge(r.observation.createdAt);
2212
- const lines = [`${i + 1}. [${r.observation.kind}] ${r.observation.content} (${age})`];
2213
- if (r.relations && r.relations.length > 0) {
2214
- for (const rel of r.relations) {
2215
- const other = rel.observationA === r.observation.id ? rel.observationB : rel.observationA;
2216
- const icon = rel.relation === "conflicts_with" ? "\u26A1" : rel.relation === "supersedes" ? "\u21A9" : "~";
2217
- lines.push(` ${icon} ${rel.relation} ${other} (confidence: ${rel.confidence})${rel.judgmentStatus === "judged" ? " (judged)" : ""}`);
2218
- }
2219
- }
2220
- return lines.join("\n");
2221
- }).join("\n");
2222
- }
2223
- case "kirograph_mem_store": {
2224
- const { loadConfig } = await Promise.resolve().then(() => require("../config.js"));
2225
- const projectRoot = cg.getProjectRoot();
2226
- const config = await loadConfig(projectRoot);
2227
- if (!config.enableMemory) return "Memory is not enabled. Set enableMemory: true in .kirograph/config.json";
2228
- const { MemoryManager } = await Promise.resolve().then(() => require("../memory/index.js"));
2229
- const db = cg.getDatabase();
2230
- db.applyMemorySchema();
2231
- const mem = new MemoryManager(config, db.getRawDb());
2232
- mem.initialize();
2233
- const id = await mem.store({
2234
- content: args.content,
2235
- kind: args.kind ?? "note",
2236
- source: "agent",
2237
- topicKey: args.topicKey,
2238
- reviewAfter: args.reviewAfter
2239
- });
2240
- if (!id) return "Observation already exists (duplicate content).";
2241
- return `Stored observation ${id} [${args.kind ?? "note"}]`;
2242
- }
2243
- case "kirograph_mem_timeline": {
2244
- const { loadConfig } = await Promise.resolve().then(() => require("../config.js"));
2245
- const projectRoot = cg.getProjectRoot();
2246
- const config = await loadConfig(projectRoot);
2247
- if (!config.enableMemory) return "Memory is not enabled. Set enableMemory: true in .kirograph/config.json";
2248
- const { MemoryManager } = await Promise.resolve().then(() => require("../memory/index.js"));
2249
- const db = cg.getDatabase();
2250
- db.applyMemorySchema();
2251
- const mem = new MemoryManager(config, db.getRawDb());
2252
- mem.initialize();
2253
- const { sessions, observations } = mem.timeline({
2254
- limit: args.limit ?? 5,
2255
- sessionId: args.sessionId
2256
- });
2257
- if (sessions.length === 0) return "No memory sessions found.";
2258
- const lines = [];
2259
- for (const session of sessions) {
2260
- const start = new Date(session.startedAt).toISOString().slice(0, 16).replace("T", " ");
2261
- const status = session.endedAt ? "ended" : "active";
2262
- const obs = observations.get(session.id) ?? [];
2263
- lines.push(`## ${start} [${session.ide ?? "unknown"}] (${status}, ${obs.length} observations)`);
2264
- for (const o of obs.slice(0, 5)) {
2265
- lines.push(` - [${o.kind}] ${o.content.slice(0, 120)}`);
2266
- }
2267
- if (obs.length > 5) lines.push(` \u2026and ${obs.length - 5} more`);
2268
- }
2269
- return lines.join("\n");
2270
- }
2271
- case "kirograph_mem_status": {
2272
- const { loadConfig } = await Promise.resolve().then(() => require("../config.js"));
2273
- const projectRoot = cg.getProjectRoot();
2274
- const config = await loadConfig(projectRoot);
2275
- if (!config.enableMemory) return "Memory is not enabled. Set enableMemory: true in .kirograph/config.json";
2276
- const { MemoryManager } = await Promise.resolve().then(() => require("../memory/index.js"));
2277
- const db = cg.getDatabase();
2278
- db.applyMemorySchema();
2279
- const mem = new MemoryManager(config, db.getRawDb());
2280
- mem.initialize();
2281
- const stats = mem.getStats();
2282
- const lines = [
2283
- "KiroGraph Memory Status",
2284
- ` Sessions: ${stats.sessions} (${stats.activeSessions} active)`,
2285
- ` Observations: ${stats.observations}`,
2286
- ` Symbol links: ${stats.links}`,
2287
- ` Embeddings: ${stats.vectors} / ${stats.embeddableCount}`,
2288
- ` Model mismatch: ${stats.modelMismatch ? "\u26A0 yes \u2014 run kirograph mem reembed" : "no"}`,
2289
- ` Relations: ${stats.relations} (${stats.pendingConflicts} pending)`,
2290
- ` Caveman compression: ${config.cavemanMode !== "off" ? config.cavemanMode : "off (storing raw)"}`
2291
- ];
2292
- return lines.join("\n");
2293
- }
2294
- case "kirograph_mem_review": {
2295
- const { loadConfig } = await Promise.resolve().then(() => require("../config.js"));
2296
- const projectRoot = args.projectPath ?? cg.getProjectRoot();
2297
- const config = await loadConfig(projectRoot);
2298
- if (!config.enableMemory) return "Memory is not enabled.";
2299
- const { MemoryManager } = await Promise.resolve().then(() => require("../memory/index.js"));
2300
- const db = cg.getDatabase();
2301
- db.applyMemorySchema();
2302
- const mem = new MemoryManager(config, db.getRawDb());
2303
- mem.initialize();
2304
- const obs = mem.getObservationsForReview(args.limit ?? 20);
2305
- if (obs.length === 0) return "No overdue observations \u2014 all review_after dates are in the future.";
2306
- const now = Date.now();
2307
- return obs.map((o, i) => {
2308
- const daysOverdue = Math.floor((now - (o.reviewAfter ?? now)) / 864e5);
2309
- return `${i + 1}. [${o.kind}]${o.topicKey ? ` (${o.topicKey})` : ""} ${o.content.slice(0, 120)} \u2014 overdue by ${daysOverdue}d`;
2310
- }).join("\n");
2311
- }
2312
- case "kirograph_mem_mark_reviewed": {
2313
- const { loadConfig } = await Promise.resolve().then(() => require("../config.js"));
2314
- const projectRoot = args.projectPath ?? cg.getProjectRoot();
2315
- const config = await loadConfig(projectRoot);
2316
- if (!config.enableMemory) return "Memory is not enabled.";
2317
- const { MemoryManager } = await Promise.resolve().then(() => require("../memory/index.js"));
2318
- const db = cg.getDatabase();
2319
- db.applyMemorySchema();
2320
- const mem = new MemoryManager(config, db.getRawDb());
2321
- mem.initialize();
2322
- mem.markReviewed(args.id);
2323
- return `Observation ${args.id} marked as reviewed.`;
2324
- }
2325
- case "kirograph_mem_compare": {
2326
- const { loadConfig } = await Promise.resolve().then(() => require("../config.js"));
2327
- const projectRoot = args.projectPath ?? cg.getProjectRoot();
2328
- const config = await loadConfig(projectRoot);
2329
- if (!config.enableMemory) return "Memory is not enabled.";
2330
- const { MemoryManager } = await Promise.resolve().then(() => require("../memory/index.js"));
2331
- const db = cg.getDatabase();
2332
- db.applyMemorySchema();
2333
- const mem = new MemoryManager(config, db.getRawDb());
2334
- mem.initialize();
2335
- const relationId = mem.compareObservations({
2336
- observationA: args.observationA,
2337
- observationB: args.observationB,
2338
- relation: args.relation,
2339
- confidence: args.confidence ?? 1,
2340
- reason: args.reason,
2341
- evidence: args.evidence
2342
- });
2343
- return `Relation ${relationId} created (${args.relation}, confidence: ${args.confidence ?? 1}). Use kirograph_mem_judge to finalize.`;
2344
- }
2345
- case "kirograph_mem_judge": {
2346
- const { loadConfig } = await Promise.resolve().then(() => require("../config.js"));
2347
- const projectRoot = args.projectPath ?? cg.getProjectRoot();
2348
- const config = await loadConfig(projectRoot);
2349
- if (!config.enableMemory) return "Memory is not enabled.";
2350
- const { MemoryManager } = await Promise.resolve().then(() => require("../memory/index.js"));
2351
- const db = cg.getDatabase();
2352
- db.applyMemorySchema();
2353
- const mem = new MemoryManager(config, db.getRawDb());
2354
- mem.initialize();
2355
- mem.judgeRelation(
2356
- args.relationId,
2357
- args.relation,
2358
- args.confidence,
2359
- args.reason,
2360
- args.evidence
2361
- );
2362
- return `Relation ${args.relationId} judged as [${args.relation}] (confidence: ${args.confidence}).`;
2363
- }
2364
- case "kirograph_mem_capture": {
2365
- const { loadConfig } = await Promise.resolve().then(() => require("../config.js"));
2366
- const projectRoot = args.projectPath ?? cg.getProjectRoot();
2367
- const config = await loadConfig(projectRoot);
2368
- if (!config.enableMemory) return "Memory is not enabled.";
2369
- const { MemoryManager } = await Promise.resolve().then(() => require("../memory/index.js"));
2370
- const db = cg.getDatabase();
2371
- db.applyMemorySchema();
2372
- const mem = new MemoryManager(config, db.getRawDb());
2373
- mem.initialize();
2374
- const extracted = mem.capturePassive(args.content);
2375
- if (extracted.length === 0) return "No structured sections found. Use ## Key Learnings, ## Observations, ## Decisions, or ## Key Changes headings.";
2376
- return `Captured ${extracted.length} observation(s):
2377
- ` + extracted.map((e, i) => `${i + 1}. [${e.kind}] ${e.content.slice(0, 100)}${e.id ? "" : " (duplicate)"}`).join("\n");
2378
- }
2379
- case "kirograph_mem_save_prompt": {
2380
- const { loadConfig } = await Promise.resolve().then(() => require("../config.js"));
2381
- const projectRoot = args.projectPath ?? cg.getProjectRoot();
2382
- const config = await loadConfig(projectRoot);
2383
- if (!config.enableMemory) return "Memory is not enabled.";
2384
- const { MemoryManager } = await Promise.resolve().then(() => require("../memory/index.js"));
2385
- const db = cg.getDatabase();
2386
- db.applyMemorySchema();
2387
- const mem = new MemoryManager(config, db.getRawDb());
2388
- mem.initialize();
2389
- const id = await mem.savePrompt(args.content);
2390
- return `Prompt saved as ${id}.`;
2391
- }
2392
- case "kirograph_mem_suggest_topic_key": {
2393
- const { loadConfig } = await Promise.resolve().then(() => require("../config.js"));
2394
- const projectRoot = args.projectPath ?? cg.getProjectRoot();
2395
- const config = await loadConfig(projectRoot);
2396
- if (!config.enableMemory) return "Memory is not enabled.";
2397
- const { MemoryManager } = await Promise.resolve().then(() => require("../memory/index.js"));
2398
- const db = cg.getDatabase();
2399
- db.applyMemorySchema();
2400
- const mem = new MemoryManager(config, db.getRawDb());
2401
- mem.initialize();
2402
- const key = mem.suggestTopicKey(args.kind, args.title);
2403
- return `Suggested topic_key: ${key}`;
2404
- }
2405
- case "kirograph_mem_conflicts_scan": {
2406
- const { loadConfig } = await Promise.resolve().then(() => require("../config.js"));
2407
- const projectRoot = args.projectPath ?? cg.getProjectRoot();
2408
- const config = await loadConfig(projectRoot);
2409
- if (!config.enableMemory) return "Memory is not enabled.";
2410
- const { MemoryManager } = await Promise.resolve().then(() => require("../memory/index.js"));
2411
- const db = cg.getDatabase();
2412
- db.applyMemorySchema();
2413
- const mem = new MemoryManager(config, db.getRawDb());
2414
- mem.initialize();
2415
- const candidates = mem.scanConflicts(args.limit ?? 50);
2416
- if (candidates.length === 0) return "No potential conflicts found among recent observations.";
2417
- return `Found ${candidates.length} potential conflict(s):
2418
- ` + candidates.map(
2419
- (c, i) => `${i + 1}. [${c.observationA.kind}] "${c.observationA.content.slice(0, 80)}" \u2194 [${c.observationB.kind}] "${c.observationB.content.slice(0, 80)}" (similarity: ${c.similarity.toFixed(2)})`
2420
- ).join("\n") + "\n\nUse kirograph_mem_compare to establish relations.";
2421
- }
2422
- // ── Docs tools ────────────────────────────────────────────────────────────
2423
- case "kirograph_docs_toc": {
2424
- const { loadConfig } = await Promise.resolve().then(() => require("../config.js"));
2425
- const projectRoot = cg.getProjectRoot();
2426
- const config = await loadConfig(projectRoot);
2427
- if (!config.enableDocs) return "Documentation indexing is not enabled. Set enableDocs: true in .kirograph/config.json and run kirograph index.";
2428
- const { DocsQueries } = await Promise.resolve().then(() => require("../docs/queries.js"));
2429
- const db = cg.getDatabase();
2430
- db.applyDocsSchema();
2431
- const docs = new DocsQueries(db.getRawDb(), projectRoot);
2432
- const toc = docs.getToc({ file: args.file, tree: args.tree });
2433
- if (toc.length === 0) return args.file ? `No sections found in "${args.file}".` : "No documentation indexed. Run kirograph index.";
2434
- const lines = [];
2435
- const renderEntry = (entry, indent) => {
2436
- const prefix = "#".repeat(entry.level || 1);
2437
- const summary = entry.summary ? ` \u2014 ${entry.summary}` : "";
2438
- lines.push(`${indent}${prefix} ${entry.title}${summary}`);
2439
- lines.push(`${indent} ID: ${entry.id}`);
2440
- if (entry.children?.length) {
2441
- for (const child of entry.children) renderEntry(child, indent + " ");
2442
- }
2443
- };
2444
- if (args.tree) {
2445
- for (const entry of toc) renderEntry(entry, "");
2446
- } else {
2447
- for (const entry of toc) {
2448
- const prefix = "#".repeat(entry.level || 1);
2449
- const summary = entry.summary ? ` \u2014 ${entry.summary}` : "";
2450
- lines.push(`${prefix} ${entry.title} [${entry.filePath}]${summary}`);
2451
- lines.push(` ID: ${entry.id}`);
2452
- }
2453
- }
2454
- return lines.join("\n");
2455
- }
2456
- case "kirograph_docs_search": {
2457
- const { loadConfig } = await Promise.resolve().then(() => require("../config.js"));
2458
- const projectRoot = cg.getProjectRoot();
2459
- const config = await loadConfig(projectRoot);
2460
- if (!config.enableDocs) return "Documentation indexing is not enabled. Set enableDocs: true in .kirograph/config.json and run kirograph index.";
2461
- const { DocsQueries } = await Promise.resolve().then(() => require("../docs/queries.js"));
2462
- const db = cg.getDatabase();
2463
- db.applyDocsSchema();
2464
- const docs = new DocsQueries(db.getRawDb(), projectRoot, config);
2465
- const results = await docs.searchSections(args.query, {
2466
- file: args.file,
2467
- limit: args.limit ?? 10
2468
- });
2469
- if (results.length === 0) return `No documentation sections found matching "${args.query}".`;
2470
- return results.map((r, i) => {
2471
- const summary = r.section.summary ? `
2472
- ${r.section.summary}` : "";
2473
- return `${i + 1}. ${r.section.title} [${r.section.filePath}]${summary}
2474
- ID: ${r.section.id}`;
2475
- }).join("\n\n");
2476
- }
2477
- case "kirograph_docs_section": {
2478
- const { loadConfig } = await Promise.resolve().then(() => require("../config.js"));
2479
- const projectRoot = cg.getProjectRoot();
2480
- const config = await loadConfig(projectRoot);
2481
- if (!config.enableDocs) return "Documentation indexing is not enabled. Set enableDocs: true in .kirograph/config.json and run kirograph index.";
2482
- const { DocsQueries } = await Promise.resolve().then(() => require("../docs/queries.js"));
2483
- const db = cg.getDatabase();
2484
- db.applyDocsSchema();
2485
- const docs = new DocsQueries(db.getRawDb(), projectRoot);
2486
- const result = docs.getSection(args.id, { context: args.context });
2487
- if (!result) return `Section "${args.id}" not found.`;
2488
- const lines = [];
2489
- if (result.ancestors?.length) {
2490
- lines.push("Breadcrumb: " + result.ancestors.map((a) => a.title).join(" > ") + " > " + result.section.title);
2491
- lines.push("");
2492
- }
2493
- lines.push(result.content);
2494
- if (result.children?.length) {
2495
- lines.push("", "## Child sections:");
2496
- for (const child of result.children) {
2497
- const summary = child.summary ? ` \u2014 ${child.summary}` : "";
2498
- lines.push(` - ${child.title}${summary} (ID: ${child.id})`);
2499
- }
2500
- }
2501
- return lines.join("\n");
2502
- }
2503
- case "kirograph_docs_outline": {
2504
- const { loadConfig } = await Promise.resolve().then(() => require("../config.js"));
2505
- const projectRoot = cg.getProjectRoot();
2506
- const config = await loadConfig(projectRoot);
2507
- if (!config.enableDocs) return "Documentation indexing is not enabled. Set enableDocs: true in .kirograph/config.json and run kirograph index.";
2508
- const { DocsQueries } = await Promise.resolve().then(() => require("../docs/queries.js"));
2509
- const db = cg.getDatabase();
2510
- db.applyDocsSchema();
2511
- const docs = new DocsQueries(db.getRawDb(), projectRoot);
2512
- const outline = docs.getOutline(args.file);
2513
- if (outline.length === 0) return `No sections found in "${args.file}". Is the file indexed?`;
2514
- const lines = [`Outline: ${args.file}`, ""];
2515
- const renderOutline = (entries, indent) => {
2516
- for (const entry of entries) {
2517
- const summary = entry.summary ? ` \u2014 ${entry.summary}` : "";
2518
- lines.push(`${indent}${"#".repeat(entry.level || 1)} ${entry.title}${summary}`);
2519
- if (entry.children?.length) renderOutline(entry.children, indent + " ");
2520
- }
2521
- };
2522
- renderOutline(outline, "");
2523
- return lines.join("\n");
2524
- }
2525
- case "kirograph_docs_refs": {
2526
- const { loadConfig } = await Promise.resolve().then(() => require("../config.js"));
2527
- const projectRoot = cg.getProjectRoot();
2528
- const config = await loadConfig(projectRoot);
2529
- if (!config.enableDocs) return "Documentation indexing is not enabled. Set enableDocs: true in .kirograph/config.json and run kirograph index.";
2530
- const { DocsQueries } = await Promise.resolve().then(() => require("../docs/queries.js"));
2531
- const db = cg.getDatabase();
2532
- db.applyDocsSchema();
2533
- const docs = new DocsQueries(db.getRawDb(), projectRoot);
2534
- const refs = docs.getRefs({
2535
- sectionId: args.sectionId,
2536
- qualifiedName: args.nodeId
2537
- });
2538
- if (refs.length === 0) {
2539
- if (args.sectionId) return `No code references found in section "${args.sectionId}".`;
2540
- if (args.nodeId) return `No documentation sections reference "${args.nodeId}".`;
2541
- return "Provide either sectionId or nodeId to look up cross-references.";
2542
- }
2543
- return refs.map((r) => {
2544
- const direction = args.sectionId ? `\u2192 ${r.qualifiedName}` : `\u2190 ${r.sectionTitle ?? r.sectionId}`;
2545
- return `[${r.refType}] ${direction} (confidence: ${r.confidence.toFixed(2)})`;
2546
- }).join("\n");
2547
- }
2548
- // ── Data tools ────────────────────────────────────────────────────────────
2549
- case "kirograph_data_list": {
2550
- const { loadConfig } = await Promise.resolve().then(() => require("../config.js"));
2551
- const projectRoot = cg.getProjectRoot();
2552
- const config = await loadConfig(projectRoot);
2553
- if (!config.enableData) return "Data indexing is not enabled. Set enableData: true in .kirograph/config.json and run kirograph index.";
2554
- const { DataQueries } = await Promise.resolve().then(() => require("../data/queries.js"));
2555
- const db = cg.getDatabase();
2556
- db.applyDataSchema();
2557
- const dq = new DataQueries(db.getRawDb());
2558
- const datasets = dq.listDatasets();
2559
- if (datasets.length === 0) return "No datasets indexed. Run kirograph index or kirograph data reindex.";
2560
- return datasets.map((ds) => {
2561
- const sizeMb = (ds.fileSize / 1024 / 1024).toFixed(2);
2562
- return `${ds.id} (${ds.format})
2563
- File: ${ds.filePath}
2564
- Rows: ${ds.rowCount.toLocaleString()} | Columns: ${ds.columnCount} | Size: ${sizeMb} MB`;
2565
- }).join("\n\n");
2566
- }
2567
- case "kirograph_data_describe": {
2568
- const { loadConfig } = await Promise.resolve().then(() => require("../config.js"));
2569
- const projectRoot = cg.getProjectRoot();
2570
- const config = await loadConfig(projectRoot);
2571
- if (!config.enableData) return "Data indexing is not enabled. Set enableData: true in .kirograph/config.json and run kirograph index.";
2572
- const { DataQueries } = await Promise.resolve().then(() => require("../data/queries.js"));
2573
- const db = cg.getDatabase();
2574
- db.applyDataSchema();
2575
- const dq = new DataQueries(db.getRawDb());
2576
- if (args.column) {
2577
- const col = dq.describeColumn(args.dataset, args.column);
2578
- if (!col) return `Column "${args.column}" not found in dataset "${args.dataset}".`;
2579
- return [
2580
- `Column: ${col.name}`,
2581
- `Type: ${col.inferredType}`,
2582
- `Nullable: ${col.nullable} (${(col.nullPct * 100).toFixed(1)}% null)`,
2583
- `Cardinality: ${col.cardinality}`,
2584
- col.minValue ? `Min: ${col.minValue}` : "",
2585
- col.maxValue ? `Max: ${col.maxValue}` : "",
2586
- col.meanValue !== null ? `Mean: ${col.meanValue.toFixed(2)}` : "",
2587
- `Samples: ${col.sampleValues.join(", ")}`
2588
- ].filter(Boolean).join("\n");
2589
- }
2590
- const result = dq.describeDataset(args.dataset);
2591
- if (!result) return `Dataset "${args.dataset}" not found. Use kirograph_data_list to see available datasets.`;
2592
- const lines = [
2593
- `Dataset: ${result.dataset.id} (${result.dataset.format})`,
2594
- `File: ${result.dataset.filePath}`,
2595
- `Rows: ${result.dataset.rowCount.toLocaleString()} | Columns: ${result.dataset.columnCount}`,
2596
- "",
2597
- "Columns:"
2598
- ];
2599
- for (const col of result.columns) {
2600
- const nullInfo = col.nullable ? ` (${(col.nullPct * 100).toFixed(0)}% null)` : "";
2601
- const samples = col.sampleValues.length > 0 ? ` \u2014 samples: ${col.sampleValues.slice(0, 3).join(", ")}` : "";
2602
- const summary = col.summary ? ` [${col.summary}]` : "";
2603
- lines.push(` ${col.name}: ${col.inferredType}${nullInfo} [${col.cardinality} distinct]${samples}${summary}`);
2604
- }
2605
- const rules = dq.validationRules(args.dataset);
2606
- if (rules && rules.length > 0) {
2607
- lines.push("", "Validation rules:");
2608
- for (const r of rules.slice(0, 10)) {
2609
- lines.push(` ${r.column}: ${r.rules.join("; ")}`);
2610
- }
2611
- }
2612
- const hints = dq.sampleHints(args.dataset);
2613
- if (hints && hints.length > 0) {
2614
- lines.push("", "Sample data hints:");
2615
- for (const h of hints.slice(0, 10)) {
2616
- lines.push(` ${h.column}: ${h.hint}`);
2617
- }
2618
- }
2619
- return lines.join("\n");
2620
- }
2621
- case "kirograph_data_query": {
2622
- const { loadConfig } = await Promise.resolve().then(() => require("../config.js"));
2623
- const projectRoot = cg.getProjectRoot();
2624
- const config = await loadConfig(projectRoot);
2625
- if (!config.enableData) return "Data indexing is not enabled. Set enableData: true in .kirograph/config.json and run kirograph index.";
2626
- const { DataQueries } = await Promise.resolve().then(() => require("../data/queries.js"));
2627
- const db = cg.getDatabase();
2628
- db.applyDataSchema();
2629
- const dq = new DataQueries(db.getRawDb());
2630
- const result = dq.queryRows(args.dataset, {
2631
- filters: args.filters,
2632
- columns: args.columns,
2633
- limit: args.limit ?? 100,
2634
- offset: args.offset ?? 0
2635
- });
2636
- if (!result) return `Dataset "${args.dataset}" not found.`;
2637
- if (result.rows.length === 0) return `No rows match the given filters (${result.totalMatching} total in dataset).`;
2638
- const header = `${result.rows.length} rows returned (${result.totalMatching} total matching):
2639
- `;
2640
- const rowStrs = result.rows.slice(0, 50).map((row, i) => {
2641
- const vals = Object.entries(row).map(([k, v]) => `${k}=${v ?? "null"}`).join(", ");
2642
- return ` ${i + 1}. ${vals}`;
2643
- });
2644
- if (result.rows.length > 50) rowStrs.push(` \u2026and ${result.rows.length - 50} more rows`);
2645
- let response = header + rowStrs.join("\n");
2646
- const maxChars = config.dataMaxResponseTokens * 4;
2647
- if (response.length > maxChars) {
2648
- response = response.slice(0, maxChars) + "\n\n[truncated: response exceeded token budget]";
2649
- }
2650
- response = this.checkPaginationLoop(args.dataset, args.offset, response);
2651
- return response;
2652
- }
2653
- case "kirograph_data_aggregate": {
2654
- const { loadConfig } = await Promise.resolve().then(() => require("../config.js"));
2655
- const projectRoot = cg.getProjectRoot();
2656
- const config = await loadConfig(projectRoot);
2657
- if (!config.enableData) return "Data indexing is not enabled. Set enableData: true in .kirograph/config.json and run kirograph index.";
2658
- const { DataQueries } = await Promise.resolve().then(() => require("../data/queries.js"));
2659
- const db = cg.getDatabase();
2660
- db.applyDataSchema();
2661
- const dq = new DataQueries(db.getRawDb());
2662
- const result = dq.aggregate(args.dataset, {
2663
- groupBy: args.groupBy ?? [],
2664
- metrics: args.metrics ?? [],
2665
- filters: args.filters
2666
- });
2667
- if (!result) return `Dataset "${args.dataset}" not found.`;
2668
- if (result.rows.length === 0) return "No results (empty dataset or all rows filtered out).";
2669
- const keys = Object.keys(result.rows[0]);
2670
- const header = keys.join(" | ");
2671
- const separator = keys.map(() => "---").join(" | ");
2672
- const rows = result.rows.slice(0, 100).map((row) => keys.map((k) => row[k] ?? "null").join(" | "));
2673
- let response = `${header}
2674
- ${separator}
2675
- ${rows.join("\n")}${result.rows.length > 100 ? `
2676
- \u2026and ${result.rows.length - 100} more groups` : ""}`;
2677
- const maxChars = config.dataMaxResponseTokens * 4;
2678
- if (response.length > maxChars) {
2679
- response = response.slice(0, maxChars) + "\n\n[truncated: response exceeded token budget]";
2680
- }
2681
- return response;
2682
- }
2683
- case "kirograph_data_search": {
2684
- const { loadConfig } = await Promise.resolve().then(() => require("../config.js"));
2685
- const projectRoot = cg.getProjectRoot();
2686
- const config = await loadConfig(projectRoot);
2687
- if (!config.enableData) return "Data indexing is not enabled. Set enableData: true in .kirograph/config.json and run kirograph index.";
2688
- const { DataQueries } = await Promise.resolve().then(() => require("../data/queries.js"));
2689
- const db = cg.getDatabase();
2690
- db.applyDataSchema();
2691
- const dq = new DataQueries(db.getRawDb());
2692
- const cols = dq.searchColumns(args.dataset, args.query);
2693
- if (cols.length === 0) return `No columns matching "${args.query}" in dataset "${args.dataset}".`;
2694
- return cols.map((c) => {
2695
- const samples = c.sampleValues.length > 0 ? ` \u2014 samples: ${c.sampleValues.slice(0, 3).join(", ")}` : "";
2696
- return `${c.name}: ${c.inferredType} [${c.cardinality} distinct]${samples}`;
2697
- }).join("\n");
2698
- }
2699
- case "kirograph_data_join": {
2700
- const { loadConfig } = await Promise.resolve().then(() => require("../config.js"));
2701
- const projectRoot = cg.getProjectRoot();
2702
- const config = await loadConfig(projectRoot);
2703
- if (!config.enableData) return "Data indexing is not enabled. Set enableData: true in .kirograph/config.json and run kirograph index.";
2704
- const { DataQueries } = await Promise.resolve().then(() => require("../data/queries.js"));
2705
- const db = cg.getDatabase();
2706
- db.applyDataSchema();
2707
- const dq = new DataQueries(db.getRawDb());
2708
- try {
2709
- const result = dq.join({
2710
- left: args.left,
2711
- right: args.right,
2712
- leftColumn: args.leftColumn,
2713
- rightColumn: args.rightColumn,
2714
- type: args.type ?? "inner",
2715
- columns: args.columns,
2716
- limit: args.limit
2717
- });
2718
- if (!result) return `Dataset not found. Verify both dataset IDs with kirograph_data_list.`;
2719
- const joinTypeStr = String(args.type ?? "inner").toUpperCase();
2720
- const header = `Join: ${args.left}.${args.leftColumn} ${joinTypeStr} JOIN ${args.right}.${args.rightColumn}
2721
- Matching rows: ${result.totalMatching} (showing ${result.rows.length})`;
2722
- if (result.rows.length === 0) return `${header}
2723
-
2724
- No matching rows.`;
2725
- const lines = result.rows.map((r) => JSON.stringify(r));
2726
- let response = `${header}
2727
-
2728
- ${lines.join("\n")}`;
2729
- const maxChars = config.dataMaxResponseTokens * 4;
2730
- if (response.length > maxChars) {
2731
- response = response.slice(0, maxChars) + "\n\n[truncated: response exceeded token budget]";
2732
- }
2733
- return response;
2734
- } catch (err) {
2735
- return `Error: ${err instanceof Error ? err.message : String(err)}`;
2736
- }
2737
- }
2738
- case "kirograph_data_correlations": {
2739
- const { loadConfig } = await Promise.resolve().then(() => require("../config.js"));
2740
- const projectRoot = cg.getProjectRoot();
2741
- const config = await loadConfig(projectRoot);
2742
- if (!config.enableData) return "Data indexing is not enabled. Set enableData: true in .kirograph/config.json and run kirograph index.";
2743
- const { DataQueries } = await Promise.resolve().then(() => require("../data/queries.js"));
2744
- const db = cg.getDatabase();
2745
- db.applyDataSchema();
2746
- const dq = new DataQueries(db.getRawDb());
2747
- const pairs = dq.correlations(args.dataset, args.threshold);
2748
- if (pairs === null) return `Dataset "${args.dataset}" not found.`;
2749
- 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.`;
2750
- const lines = pairs.map(
2751
- (p) => `${p.column1} \u2194 ${p.column2}: ${p.correlation > 0 ? "+" : ""}${p.correlation.toFixed(4)} (${p.strength})`
2752
- );
2753
- return `Correlations for "${args.dataset}" (threshold: ${args.threshold ?? 0.3}):
2754
-
2755
- ${lines.join("\n")}`;
2756
- }
2757
- case "kirograph_data_quality": {
2758
- const { loadConfig } = await Promise.resolve().then(() => require("../config.js"));
2759
- const projectRoot = cg.getProjectRoot();
2760
- const config = await loadConfig(projectRoot);
2761
- if (!config.enableData) return "Data indexing is not enabled. Set enableData: true in .kirograph/config.json and run kirograph index.";
2762
- const { DataQueries } = await Promise.resolve().then(() => require("../data/queries.js"));
2763
- const db = cg.getDatabase();
2764
- db.applyDataSchema();
2765
- const dq = new DataQueries(db.getRawDb());
2766
- const quality = dq.quality(args.dataset);
2767
- if (quality === null) return `Dataset "${args.dataset}" not found.`;
2768
- if (quality.length === 0) return `No quality issues detected in "${args.dataset}". All columns look healthy.`;
2769
- const lines = quality.map(
2770
- (q) => `${q.column} (risk: ${(q.riskScore * 100).toFixed(0)}%): ${q.issues.join("; ")}`
2771
- );
2772
- return `Quality report for "${args.dataset}" (${quality.length} columns with issues):
2773
-
2774
- ${lines.join("\n")}`;
2775
- }
2776
- // ── Security tools (require enableSecurity=true) ──────────────────────────
2777
- case "kirograph_security": {
2778
- const { loadConfig } = await Promise.resolve().then(() => require("../config.js"));
2779
- const projectRoot = cg.getProjectRoot();
2780
- const config = await loadConfig(projectRoot);
2781
- if (!config.enableSecurity) return "Security analysis is not enabled. Set enableSecurity: true in .kirograph/config.json and run kirograph index.";
2782
- if (!config.enableArchitecture) return "Security requires architecture analysis. Set enableArchitecture: true in .kirograph/config.json and run kirograph index.";
2783
- const db = cg.getDatabase();
2784
- db.applySecuritySchema();
2785
- const rawDb = db.getRawDb();
2786
- const depCount = rawDb.get(
2787
- `SELECT COUNT(*) as count FROM sec_dependencies`
2788
- ) ?? { count: 0 };
2789
- const vulnCount = rawDb.get(
2790
- `SELECT COUNT(*) as count FROM sec_vulnerabilities`
2791
- ) ?? { count: 0 };
2792
- const verdictRows = rawDb.all(
2793
- `SELECT verdict, COUNT(*) as count FROM sec_reachability GROUP BY verdict`
2794
- );
2795
- const verdicts = {};
2796
- for (const row of verdictRows) {
2797
- verdicts[row.verdict] = row.count;
2798
- }
2799
- const staleCount = rawDb.get(
2800
- `SELECT COUNT(*) as count FROM sec_dependencies WHERE vuln_data_stale = 1`
2801
- ) ?? { count: 0 };
2802
- const { SuppressionManager: SecSuppressionManager } = await Promise.resolve().then(() => require("../security/suppressions.js"));
2803
- const secSuppressions = new SecSuppressionManager(projectRoot);
2804
- const allCveIds = rawDb.all(`SELECT cve_id FROM sec_vulnerabilities`);
2805
- const suppressedCveCount = allCveIds.filter((r) => secSuppressions.isSuppressed(r.cve_id)).length;
2806
- const visibleVulnCount = vulnCount.count - suppressedCveCount;
2807
- const lines = [
2808
- "# Security Overview",
2809
- "",
2810
- `Dependencies: ${depCount.count}`,
2811
- `Vulnerabilities: ${visibleVulnCount}${suppressedCveCount > 0 ? ` (${suppressedCveCount} suppressed)` : ""}`
2812
- ];
2813
- if (visibleVulnCount > 0) {
2814
- const affected = verdicts["affected"] ?? 0;
2815
- const notAffected = verdicts["not_affected"] ?? 0;
2816
- const underInvestigation = verdicts["under_investigation"] ?? 0;
2817
- const pending = visibleVulnCount - affected - notAffected - underInvestigation;
2818
- lines.push("", "## Reachability Verdicts", "");
2819
- if (affected > 0) lines.push(`\u25CF Affected: ${affected}`);
2820
- if (notAffected > 0) lines.push(`\u25CF Not affected: ${notAffected}`);
2821
- if (underInvestigation > 0) lines.push(`\u25CF Under investigation: ${underInvestigation}`);
2822
- if (pending > 0) lines.push(`\u25CF Pending analysis: ${pending}`);
2823
- }
2824
- if (staleCount.count > 0) {
2825
- lines.push("", `\u26A0 ${staleCount.count} dependenc${staleCount.count === 1 ? "y has" : "ies have"} stale vulnerability data. Use kirograph_vulns with refresh=true to update.`);
2826
- }
2827
- const lastVulnCheck = rawDb.get(
2828
- `SELECT MIN(last_vuln_check) as oldest FROM sec_dependencies WHERE last_vuln_check IS NOT NULL`
2829
- )?.oldest;
2830
- if (lastVulnCheck != null) {
2831
- const ageMs = Date.now() - lastVulnCheck;
2832
- const ageDays = ageMs / (1e3 * 60 * 60 * 24);
2833
- const maxAge = config.securityEnrichMaxAgeDays ?? 7;
2834
- if (ageDays > maxAge) {
2835
- lines.push("", `\u26A0 Vulnerability data is ${Math.floor(ageDays)} days old (max: ${maxAge}). Run kirograph vulns --refresh to update.`);
2836
- }
2837
- }
2838
- try {
2839
- const rawDbSec = db.getRawDb();
2840
- const tableExistsSec = rawDbSec.get("SELECT name FROM sqlite_master WHERE type='table' AND name='pattern_matches'");
2841
- if (tableExistsSec) {
2842
- const patternCount = rawDbSec.get("SELECT COUNT(*) as cnt FROM pattern_matches")?.cnt ?? 0;
2843
- if (patternCount > 0) {
2844
- const critCount = rawDbSec.get("SELECT COUNT(*) as cnt FROM pattern_matches WHERE severity = 'critical'")?.cnt ?? 0;
2845
- lines.push(`SAST findings: ${patternCount} pattern matches (${critCount} critical)`);
2846
- }
2847
- }
2848
- } catch {
2849
- }
2850
- return lines.join("\n");
2851
- }
2852
- case "kirograph_vulns": {
2853
- const { loadConfig } = await Promise.resolve().then(() => require("../config.js"));
2854
- const projectRoot = cg.getProjectRoot();
2855
- const config = await loadConfig(projectRoot);
2856
- if (!config.enableSecurity) return "Security analysis is not enabled. Set enableSecurity: true in .kirograph/config.json and run kirograph index.";
2857
- const db = cg.getDatabase();
2858
- db.applySecuritySchema();
2859
- const rawDb = db.getRawDb();
2860
- if (args.refresh === true) {
2861
- const { OsvAdapter } = await Promise.resolve().then(() => require("../security/vuln/osv-adapter.js"));
2862
- const { VulnerabilityDatabaseClient } = await Promise.resolve().then(() => require("../security/vuln/client.js"));
2863
- const adapters = config.securityDatabases.map((dbName) => {
2864
- if (dbName.toUpperCase() === "OSV") return new OsvAdapter();
2865
- return null;
2866
- }).filter(Boolean);
2867
- const client = new VulnerabilityDatabaseClient(adapters, db);
2868
- await client.enrichAll();
2869
- }
2870
- let query = `
2871
- SELECT
2872
- v.node_id, v.cve_id, v.severity_score, v.fixed_version, v.summary,
2873
- v.epss_score, v.epss_percentile, v.risk_score,
2874
- d.package_name, d.ecosystem, d.resolved_version, d.declared_constraint,
2875
- r.verdict
2876
- FROM sec_vulnerabilities v
2877
- LEFT JOIN edges e ON e.target = v.node_id AND e.kind = 'has_vulnerability'
2878
- LEFT JOIN sec_dependencies d ON d.node_id = e.source
2879
- LEFT JOIN sec_reachability r ON r.vulnerability_node_id = v.node_id
2880
- WHERE 1=1
2881
- `;
2882
- const params = [];
2883
- if (args.severity) {
2884
- const severityRanges = {
2885
- critical: [9, 10],
2886
- high: [7, 8.9],
2887
- medium: [4, 6.9],
2888
- low: [0.1, 3.9]
2889
- };
2890
- const range = severityRanges[args.severity.toLowerCase()];
2891
- if (!range) return `Invalid severity: ${args.severity}. Use: critical, high, medium, low`;
2892
- query += ` AND v.severity_score >= ? AND v.severity_score <= ?`;
2893
- params.push(range[0], range[1]);
2894
- }
2895
- if (args.verdict) {
2896
- const validVerdicts = ["affected", "not_affected", "under_investigation"];
2897
- if (!validVerdicts.includes(args.verdict)) {
2898
- return `Invalid verdict: ${args.verdict}. Use: affected, not_affected, under_investigation`;
2899
- }
2900
- query += ` AND r.verdict = ?`;
2901
- params.push(args.verdict);
2902
- }
2903
- const limit = clampLimit(args.limit, 20);
2904
- query += ` ORDER BY v.risk_score DESC NULLS LAST LIMIT ?`;
2905
- params.push(limit);
2906
- const rows = rawDb.all(query, params);
2907
- const verdictRankMcp = (v) => v === "affected" ? 3 : v === "under_investigation" ? 2 : v === "not_affected" ? 1 : 0;
2908
- const dedupMapMcp = /* @__PURE__ */ new Map();
2909
- for (const row of rows) {
2910
- const key = `${row.cve_id}::${row.package_name ?? ""}::${row.ecosystem ?? ""}`;
2911
- const ex = dedupMapMcp.get(key);
2912
- if (!ex || verdictRankMcp(row.verdict) > verdictRankMcp(ex.verdict)) dedupMapMcp.set(key, row);
2913
- }
2914
- const dedupedRowsMcp = [...dedupMapMcp.values()];
2915
- const { SuppressionManager } = await Promise.resolve().then(() => require("../security/suppressions.js"));
2916
- const suppressionMgr = new SuppressionManager(projectRoot);
2917
- const suppressedCount = dedupedRowsMcp.filter((row) => suppressionMgr.isSuppressed(row.cve_id)).length;
2918
- const filteredRows = dedupedRowsMcp.filter((row) => !suppressionMgr.isSuppressed(row.cve_id));
2919
- if (filteredRows.length === 0) {
2920
- const noMatch = "No vulnerabilities found" + (args.severity || args.verdict ? " matching filters." : ".");
2921
- return suppressedCount > 0 ? `${noMatch} (${suppressedCount} suppressed)` : noMatch;
2922
- }
2923
- const { formatFixSuggestion } = await Promise.resolve().then(() => require("../security/export/fix-suggestions.js"));
2924
- const lines = [`Vulnerabilities (${filteredRows.length}${suppressedCount > 0 ? `, ${suppressedCount} suppressed` : ""}):
2925
- `];
2926
- for (const row of filteredRows) {
2927
- const score = row.severity_score;
2928
- let severityLabel;
2929
- if (score == null) severityLabel = "UNKNOWN";
2930
- else if (score >= 9) severityLabel = "CRITICAL";
2931
- else if (score >= 7) severityLabel = "HIGH";
2932
- else if (score >= 4) severityLabel = "MEDIUM";
2933
- else severityLabel = "LOW";
2934
- let verdictLabel;
2935
- if (!row.verdict) verdictLabel = "pending";
2936
- else if (row.verdict === "affected") verdictLabel = "affected";
2937
- else if (row.verdict === "not_affected") verdictLabel = "not affected";
2938
- else verdictLabel = "investigating";
2939
- const pkg = row.package_name ? `${row.package_name}@${row.resolved_version || row.declared_constraint || "?"}` : "unknown package";
2940
- const epssNote = row.epss_score != null ? ` [EPSS: ${row.epss_score.toFixed(2)}]` : "";
2941
- const riskNote = row.risk_score != null ? ` [Risk: ${row.risk_score.toFixed(1)}]` : "";
2942
- lines.push(`${severityLabel} ${row.cve_id} ${pkg} [${verdictLabel}]${epssNote}${riskNote}`);
2943
- if (row.summary && row.summary !== "Manually registered") {
2944
- const truncSummary = row.summary.length > 120 ? row.summary.slice(0, 120) + "\u2026" : row.summary;
2945
- lines.push(` ${truncSummary}`);
2946
- }
2947
- if (row.fixed_version && row.ecosystem && row.package_name) {
2948
- const fix = formatFixSuggestion(row.ecosystem, row.package_name, row.fixed_version);
2949
- if (fix) lines.push(` ${fix}`);
2950
- }
2951
- }
2952
- return truncate(lines.join("\n"));
2953
- }
2954
- case "kirograph_vuln_add": {
2955
- const { loadConfig } = await Promise.resolve().then(() => require("../config.js"));
2956
- const projectRoot = cg.getProjectRoot();
2957
- const config = await loadConfig(projectRoot);
2958
- if (!config.enableSecurity) return "Security analysis is not enabled. Set enableSecurity: true in .kirograph/config.json and run kirograph index.";
2959
- const cveId = args.cveId;
2960
- const pkgName = args.package;
2961
- if (!cveId) return "Error: cveId is required.";
2962
- if (!pkgName) return "Error: package is required.";
2963
- const db = cg.getDatabase();
2964
- db.applySecuritySchema();
2965
- const rawDb = db.getRawDb();
2966
- const depRow = rawDb.get(
2967
- `SELECT node_id, ecosystem FROM sec_dependencies WHERE package_name = ?`,
2968
- [pkgName]
2969
- );
2970
- if (!depRow) {
2971
- return `No dependency found matching "${pkgName}". Run kirograph index first to discover dependencies.`;
2972
- }
2973
- const vulnNodeId = `vuln:${cveId}`;
2974
- const now = Date.now();
2975
- const severity = args.severity;
2976
- const summary = args.summary ?? "Manually registered";
2977
- const fixedVersion = args.fixedVersion ?? null;
2978
- rawDb.run(
2979
- `INSERT OR REPLACE INTO nodes
2980
- (id, kind, name, qualified_name, file_path, language,
2981
- start_line, end_line, start_column, end_column,
2982
- is_exported, is_async, is_static, is_abstract, updated_at)
2983
- VALUES (?, 'vulnerability', ?, ?, '', 'unknown', 0, 0, 0, 0, 0, 0, 0, 0, ?)`,
2984
- [vulnNodeId, cveId, cveId, now]
2985
- );
2986
- rawDb.run(
2987
- `INSERT OR REPLACE INTO sec_vulnerabilities
2988
- (node_id, cve_id, severity_score, affected_ranges, fixed_version, summary, source_database)
2989
- VALUES (?, ?, ?, '[]', ?, ?, 'manual')`,
2990
- [vulnNodeId, cveId, severity ?? null, fixedVersion, summary]
2991
- );
2992
- rawDb.run(
2993
- `INSERT OR IGNORE INTO edges (source, target, kind, confidence, confidence_score)
2994
- VALUES (?, ?, 'has_vulnerability', 'extracted', 1.0)`,
2995
- [depRow.node_id, vulnNodeId]
2996
- );
2997
- return `Registered ${cveId} against ${pkgName}.`;
2998
- }
2999
- case "kirograph_vuln_suppress": {
3000
- const projectRoot = cg.getProjectRoot();
3001
- const cveId = args.cveId;
3002
- if (!cveId) return "Error: cveId is required.";
3003
- const { SuppressionManager } = await Promise.resolve().then(() => require("../security/suppressions.js"));
3004
- const manager = new SuppressionManager(projectRoot);
3005
- manager.add(cveId, args.reason, args.expires);
3006
- const reasonNote = args.reason ? ` Reason: ${args.reason}.` : "";
3007
- const expiresNote = args.expires ? ` Expires: ${args.expires}.` : "";
3008
- return `${cveId} suppressed.${reasonNote}${expiresNote}`;
3009
- }
3010
- case "kirograph_sbom": {
3011
- const { loadConfig } = await Promise.resolve().then(() => require("../config.js"));
3012
- const projectRoot = cg.getProjectRoot();
3013
- const config = await loadConfig(projectRoot);
3014
- if (!config.enableSecurity) return "Security analysis is not enabled. Set enableSecurity: true in .kirograph/config.json and run kirograph index.";
3015
- const db = cg.getDatabase();
3016
- db.applySecuritySchema();
3017
- const { SBOMExporter } = await Promise.resolve().then(() => require("../security/export/sbom.js"));
3018
- const exporter = new SBOMExporter(db, projectRoot);
3019
- const json = exporter.exportJSON();
3020
- return truncate(json);
3021
- }
3022
- case "kirograph_vex": {
3023
- const { loadConfig } = await Promise.resolve().then(() => require("../config.js"));
3024
- const projectRoot = cg.getProjectRoot();
3025
- const config = await loadConfig(projectRoot);
3026
- if (!config.enableSecurity) return "Security analysis is not enabled. Set enableSecurity: true in .kirograph/config.json and run kirograph index.";
3027
- const db = cg.getDatabase();
3028
- db.applySecuritySchema();
3029
- const { VEXExporter } = await Promise.resolve().then(() => require("../security/export/vex.js"));
3030
- const exporter = new VEXExporter(db, projectRoot);
3031
- const json = exporter.exportJSON();
3032
- return truncate(json);
3033
- }
3034
- case "kirograph_reachability": {
3035
- const { loadConfig } = await Promise.resolve().then(() => require("../config.js"));
3036
- const projectRoot = cg.getProjectRoot();
3037
- const config = await loadConfig(projectRoot);
3038
- if (!config.enableSecurity) return "Security analysis is not enabled. Set enableSecurity: true in .kirograph/config.json and run kirograph index.";
3039
- const target = args.target;
3040
- if (!target) return "Error: target is required (dependency name or CVE ID).";
3041
- const db = cg.getDatabase();
3042
- db.applySecuritySchema();
3043
- const rawDb = db.getRawDb();
3044
- let vulnerabilityNodeId = null;
3045
- let targetLabel = target;
3046
- const vulnRow = rawDb.get(
3047
- `SELECT node_id FROM sec_vulnerabilities WHERE cve_id = ?`,
3048
- [target]
3049
- );
3050
- if (vulnRow) {
3051
- vulnerabilityNodeId = vulnRow.node_id;
3052
- } else {
3053
- const depRow = rawDb.get(
3054
- `SELECT node_id, package_name FROM sec_dependencies WHERE package_name = ?`,
3055
- [target]
3056
- );
3057
- if (depRow) {
3058
- targetLabel = depRow.package_name;
3059
- const vulnEdge = rawDb.get(
3060
- `SELECT target FROM edges WHERE source = ? AND kind = 'has_vulnerability' LIMIT 1`,
3061
- [depRow.node_id]
3062
- );
3063
- if (vulnEdge) {
3064
- vulnerabilityNodeId = vulnEdge.target;
3065
- } else {
3066
- return `No vulnerabilities found for dependency "${target}". The dependency exists but has no known vulnerabilities.`;
3067
- }
3068
- } else {
3069
- return `Target "${target}" not found. Provide a valid CVE ID or dependency package name.`;
3070
- }
3071
- }
3072
- const { ReachabilityAnalyzer } = await Promise.resolve().then(() => require("../security/reachability.js"));
3073
- const analyzer = new ReachabilityAnalyzer(db, config);
3074
- const result = await analyzer.analyze(vulnerabilityNodeId);
3075
- const lines = [
3076
- `# Reachability: ${targetLabel}`,
3077
- "",
3078
- `Verdict: ${result.verdict}`,
3079
- `Reaching entry points: ${result.reachingEntryPointCount}`
3080
- ];
3081
- if (result.paths.length > 0) {
3082
- lines.push("", "## Paths");
3083
- for (const p of result.paths.slice(0, 5)) {
3084
- lines.push(`- From ${p.entryPoint}: ${p.path.join(" \u2192 ")}`);
3085
- }
3086
- if (result.paths.length > 5) {
3087
- lines.push(` \u2026and ${result.paths.length - 5} more paths`);
3088
- }
3089
- }
3090
- if (result.unresolvedSymbols.length > 0) {
3091
- lines.push("", "## Unresolved Symbols");
3092
- for (const sym of result.unresolvedSymbols.slice(0, 10)) {
3093
- lines.push(`- ${sym}`);
3094
- }
3095
- if (result.unresolvedSymbols.length > 10) {
3096
- lines.push(` \u2026and ${result.unresolvedSymbols.length - 10} more`);
3097
- }
3098
- }
3099
- if (result.verdict === "affected") {
3100
- const impact = await analyzer.getImpactSummary(vulnerabilityNodeId);
3101
- if (impact) {
3102
- lines.push("", "## Impact Summary");
3103
- if (impact.affectedLayers.length > 0) {
3104
- lines.push(`Affected layers: ${impact.affectedLayers.join(", ")}`);
3105
- }
3106
- lines.push(`Affected entry points: ${impact.affectedEntryPoints.length}`);
3107
- lines.push(`Distinct paths: ${impact.distinctPathCount}`);
3108
- }
3109
- }
3110
- return truncate(lines.join("\n"));
3111
- }
3112
- case "kirograph_staleness": {
3113
- const { loadConfig } = await Promise.resolve().then(() => require("../config.js"));
3114
- const projectRoot = cg.getProjectRoot();
3115
- const config = await loadConfig(projectRoot);
3116
- if (!config.enableSecurity) return "Security analysis is not enabled. Set enableSecurity: true in .kirograph/config.json and run kirograph index.";
3117
- const db = cg.getDatabase();
3118
- db.applySecuritySchema();
3119
- const rawDb = db.getRawDb();
3120
- const threshold = typeof args.threshold === "number" ? args.threshold : 0.3;
3121
- if (args.refresh === true) {
3122
- const { StalenessChecker } = await Promise.resolve().then(() => require("../security/staleness.js"));
3123
- const checker = new StalenessChecker(db);
3124
- await checker.checkAll();
3125
- }
3126
- const rows = rawDb.all(
3127
- `SELECT package_name, ecosystem, resolved_version, declared_constraint,
3128
- latest_version, latest_published, staleness_score
3129
- FROM sec_dependencies
3130
- WHERE staleness_score >= ?
3131
- ORDER BY staleness_score DESC`,
3132
- [threshold]
3133
- );
3134
- if (rows.length === 0) {
3135
- return `No dependencies found with staleness_score >= ${threshold}.` + (args.refresh ? "" : " Use refresh=true to fetch latest version data from registries.");
3136
- }
3137
- const lines = [`Stale Dependencies (threshold: ${threshold}):
3138
- `];
3139
- for (const row of rows) {
3140
- const resolved = row.resolved_version ?? row.declared_constraint ?? "?";
3141
- const latest = row.latest_version ?? "?";
3142
- const score = row.staleness_score ?? 0;
3143
- const months = row.latest_published ? Math.round((Date.now() - row.latest_published) / (1e3 * 60 * 60 * 24 * 30)) : null;
3144
- const bar = "\u2588".repeat(Math.round(score * 10)) + "\u2591".repeat(10 - Math.round(score * 10));
3145
- const monthsStr = months !== null ? `, ${months}mo since latest` : "";
3146
- lines.push(`${row.package_name} (${row.ecosystem}): ${resolved} \u2192 ${latest}${monthsStr}`);
3147
- lines.push(` ${bar} ${score.toFixed(2)}`);
3148
- }
3149
- const totalCount = rawDb.get(`SELECT COUNT(*) as count FROM sec_dependencies`) ?? { count: 0 };
3150
- lines.push("", `${rows.length} of ${totalCount.count} dependencies are stale (score >= ${threshold})`);
3151
- return truncate(lines.join("\n"));
3152
- }
3153
- case "kirograph_licenses": {
3154
- const { loadConfig } = await Promise.resolve().then(() => require("../config.js"));
3155
- const projectRoot = cg.getProjectRoot();
3156
- const config = await loadConfig(projectRoot);
3157
- if (!config.enableSecurity) return "Security analysis is not enabled. Set enableSecurity: true in .kirograph/config.json and run kirograph index.";
3158
- const db = cg.getDatabase();
3159
- db.applySecuritySchema();
3160
- const rawDb = db.getRawDb();
3161
- const deps = rawDb.all(
3162
- `SELECT package_name, ecosystem, license FROM sec_dependencies ORDER BY ecosystem, package_name`
3163
- );
3164
- const { checkLicensePolicy } = await Promise.resolve().then(() => require("../security/license.js"));
3165
- const policy = config.securityLicensePolicy;
3166
- const violations = checkLicensePolicy(deps, policy);
3167
- if (args.policy === true) {
3168
- if (violations.length === 0) {
3169
- return "No license policy violations found.";
3170
- }
3171
- const lines2 = ["# License Policy Violations\n"];
3172
- for (const v of violations) {
3173
- lines2.push(`${v.severity.toUpperCase()} ${v.packageName} [${v.ecosystem}] ${v.license}`);
3174
- }
3175
- const denyCount2 = violations.filter((v) => v.severity === "deny").length;
3176
- const warnCount2 = violations.filter((v) => v.severity === "warn").length;
3177
- lines2.push("");
3178
- if (denyCount2 > 0) lines2.push(`${denyCount2} denied license${denyCount2 !== 1 ? "s" : ""}`);
3179
- if (warnCount2 > 0) lines2.push(`${warnCount2} license warning${warnCount2 !== 1 ? "s" : ""}`);
3180
- return truncate(lines2.join("\n"));
3181
- }
3182
- if (deps.length === 0) {
3183
- return "No dependencies found. Run kirograph index first.";
3184
- }
3185
- const violationMap = /* @__PURE__ */ new Map();
3186
- for (const v of violations) {
3187
- violationMap.set(`${v.ecosystem}:${v.packageName}`, v.severity);
3188
- }
3189
- const lines = [`# License Report (${deps.length} dependencies)
3190
- `];
3191
- if (violations.length > 0) {
3192
- lines.push("## Policy Violations\n");
3193
- for (const v of violations) {
3194
- lines.push(`${v.severity.toUpperCase()} ${v.packageName} [${v.ecosystem}] ${v.license}`);
3195
- }
3196
- lines.push("");
3197
- }
3198
- lines.push("## All Dependencies\n");
3199
- lines.push("package | ecosystem | license | status");
3200
- lines.push("------- | --------- | ------- | ------");
3201
- for (const dep of deps) {
3202
- const key = `${dep.ecosystem}:${dep.package_name}`;
3203
- const violation = violationMap.get(key);
3204
- const license = dep.license ?? "(unknown)";
3205
- const status = violation ?? (dep.license ? "ok" : "unknown");
3206
- lines.push(`${dep.package_name} | ${dep.ecosystem} | ${license} | ${status}`);
3207
- }
3208
- const denyCount = violations.filter((v) => v.severity === "deny").length;
3209
- const warnCount = violations.filter((v) => v.severity === "warn").length;
3210
- const unknownCount = deps.filter((d) => !d.license).length;
3211
- lines.push("");
3212
- if (denyCount > 0) lines.push(`${denyCount} denied license${denyCount !== 1 ? "s" : ""}`);
3213
- if (warnCount > 0) lines.push(`${warnCount} license warning${warnCount !== 1 ? "s" : ""}`);
3214
- if (unknownCount > 0) lines.push(`${unknownCount} unknown license${unknownCount !== 1 ? "s" : ""}`);
3215
- if (denyCount === 0 && warnCount === 0) lines.push("No policy violations");
3216
- return truncate(lines.join("\n"));
3217
- }
3218
- case "kirograph_attack_surface": {
3219
- const { loadConfig } = await Promise.resolve().then(() => require("../config.js"));
3220
- const projectRoot = cg.getProjectRoot();
3221
- const config = await loadConfig(projectRoot);
3222
- if (!config.enableSecurity) return "Security analysis is not enabled. Set enableSecurity: true in .kirograph/config.json and run kirograph index.";
3223
- const db = cg.getDatabase();
3224
- db.applySecuritySchema();
3225
- const { AttackSurfaceAnalyzer } = await Promise.resolve().then(() => require("../security/attack-surface.js"));
3226
- const analyzer = new AttackSurfaceAnalyzer(db);
3227
- const result = await analyzer.analyze();
3228
- if (result.totalRoutes === 0) {
3229
- return "No route nodes found in the graph. Ensure the project has been indexed with architecture analysis enabled.";
3230
- }
3231
- const limit = clampLimit(args.limit, 20);
3232
- const routes = args.publicOnly === true ? result.criticalPaths.filter((r) => r.exposureLevel === "public") : result.criticalPaths;
3233
- const lines = [
3234
- "# Attack Surface",
3235
- "",
3236
- `Total routes: ${result.totalRoutes} Public: ${result.publicRoutes} Authenticated: ${result.authenticatedRoutes} Routes with vulns: ${result.routesWithVulns}`,
3237
- ""
3238
- ];
3239
- const displayed = routes.slice(0, limit);
3240
- if (displayed.length === 0) {
3241
- lines.push(args.publicOnly === true ? "No public routes with vulnerable dependencies found." : "No routes with vulnerable dependencies found.");
3242
- } else {
3243
- for (const entry of displayed) {
3244
- const authTag = entry.isAuthenticated ? "[auth]" : "[public]";
3245
- const riskTag = entry.riskScore > 0 ? ` risk=${entry.riskScore.toFixed(1)}` : "";
3246
- lines.push(`${authTag} ${entry.route} (${entry.exposureLevel})${riskTag} ${entry.filePath}`);
3247
- for (const dep of entry.vulnerableDeps.slice(0, 3)) {
3248
- lines.push(` \u2514 ${dep.cveId} via ${dep.packageName} (${dep.hopCount} hop${dep.hopCount !== 1 ? "s" : ""}${dep.verdict ? `, ${dep.verdict}` : ""})`);
3249
- }
3250
- if (entry.vulnerableDeps.length > 3) {
3251
- lines.push(` \u2514 \u2026and ${entry.vulnerableDeps.length - 3} more vulns`);
3252
- }
3253
- }
3254
- if (routes.length > limit) {
3255
- lines.push("", `\u2026and ${routes.length - limit} more routes (increase limit to see all)`);
3256
- }
3257
- }
3258
- return truncate(lines.join("\n"));
3259
- }
3260
- case "kirograph_secrets": {
3261
- const { loadConfig } = await Promise.resolve().then(() => require("../config.js"));
3262
- const projectRoot = cg.getProjectRoot();
3263
- const config = await loadConfig(projectRoot);
3264
- if (!config.enableSecurity) return "Security analysis is not enabled. Set enableSecurity: true in .kirograph/config.json and run kirograph index.";
3265
- const db = cg.getDatabase();
3266
- db.applySecuritySchema();
3267
- const { SecretsScanner } = await Promise.resolve().then(() => require("../security/secrets.js"));
3268
- const scanner = new SecretsScanner(db, projectRoot);
3269
- const result = await scanner.scan({ includeTests: args.includeTests === true });
3270
- if (result.totalFindings === 0) {
3271
- return `No secrets found. Scanned ${result.filesScanned} file${result.filesScanned !== 1 ? "s" : ""}.`;
3272
- }
3273
- let findings = result.findings;
3274
- if (args.severity) {
3275
- findings = findings.filter((f) => f.severity === args.severity);
3276
- }
3277
- if (findings.length === 0) {
3278
- return `No secrets found matching severity "${args.severity}". Total findings (all severities): ${result.totalFindings}.`;
3279
- }
3280
- const lines = [
3281
- `# Secrets Scan (${result.filesScanned} files scanned)`,
3282
- "",
3283
- `Found: ${result.totalFindings} Critical: ${result.criticalCount} High: ${result.highCount}`,
3284
- ""
3285
- ];
3286
- for (const f of findings) {
3287
- lines.push(`${f.severity.toUpperCase()} ${f.type}`);
3288
- lines.push(` ${f.filePath}:${f.line}:${f.column} snippet: ${f.snippet}`);
3289
- if (f.nodeName) {
3290
- lines.push(` in function: ${f.nodeName}`);
3291
- }
3292
- if (f.entryPointCount > 0) {
3293
- lines.push(` reachable from ${f.entryPointCount} entry point${f.entryPointCount !== 1 ? "s" : ""}`);
3294
- }
3295
- }
3296
- return truncate(lines.join("\n"));
3297
- }
3298
- case "kirograph_security_flows": {
3299
- const { loadConfig } = await Promise.resolve().then(() => require("../config.js"));
3300
- const projectRoot = cg.getProjectRoot();
3301
- const config = await loadConfig(projectRoot);
3302
- if (!config.enableSecurity) return "Security analysis is not enabled. Set enableSecurity: true in .kirograph/config.json and run kirograph index.";
3303
- const db = cg.getDatabase();
3304
- db.applySecuritySchema();
3305
- const { DataFlowAnalyzer } = await Promise.resolve().then(() => require("../security/data-flows.js"));
3306
- const analyzer = new DataFlowAnalyzer(db);
3307
- let findings = await analyzer.analyze();
3308
- const typeFilter = args.type ?? "all";
3309
- const TYPE_MAP = {
3310
- sql: "sql-injection",
3311
- eval: "dangerous-eval",
3312
- deserialize: "unsafe-deserialize",
3313
- path: "path-traversal",
3314
- crypto: "hardcoded-crypto"
3315
- };
3316
- if (typeFilter !== "all") {
3317
- const mapped = TYPE_MAP[typeFilter];
3318
- if (!mapped) return `Invalid type filter "${typeFilter}". Use: sql, eval, deserialize, path, crypto, all`;
3319
- findings = findings.filter((f) => f.type === mapped);
3320
- }
3321
- if (findings.length === 0) {
3322
- return typeFilter === "all" ? "No dangerous data flows detected." : `No "${typeFilter}" findings detected.`;
3323
- }
3324
- const lines = [
3325
- `# Security Flows (${findings.length} finding${findings.length !== 1 ? "s" : ""})`,
3326
- ""
3327
- ];
3328
- for (const f of findings) {
3329
- lines.push(`${f.severity.toUpperCase()} [${f.owaspCategory}] ${f.type}`);
3330
- lines.push(` ${f.filePath}:${f.line} symbol: ${f.symbol}`);
3331
- lines.push(` ${f.description}`);
3332
- lines.push(` Fix: ${f.recommendation}`);
3333
- lines.push("");
3334
- }
3335
- return truncate(lines.join("\n"));
3336
- }
3337
- case "kirograph_supply_chain": {
3338
- const { loadConfig } = await Promise.resolve().then(() => require("../config.js"));
3339
- const projectRoot = cg.getProjectRoot();
3340
- const config = await loadConfig(projectRoot);
3341
- if (!config.enableSecurity) return "Security analysis is not enabled. Set enableSecurity: true in .kirograph/config.json and run kirograph index.";
3342
- const db = cg.getDatabase();
3343
- db.applySecuritySchema();
3344
- const { SupplyChainChecker } = await Promise.resolve().then(() => require("../security/supply-chain.js"));
3345
- const checker = new SupplyChainChecker(db);
3346
- const { results, errors } = await checker.checkAll();
3347
- const RISK_ORDER = { critical: 0, high: 1, medium: 2, low: 3, unknown: 4 };
3348
- const thresholdArg = args.threshold;
3349
- const thresholdOrder = thresholdArg ? RISK_ORDER[thresholdArg] ?? 4 : 2;
3350
- const filtered = results.filter((r) => (RISK_ORDER[r.riskLevel] ?? 4) <= thresholdOrder).sort((a, b) => (RISK_ORDER[a.riskLevel] ?? 4) - (RISK_ORDER[b.riskLevel] ?? 4));
3351
- if (filtered.length === 0) {
3352
- return `No supply chain risks found at threshold "${thresholdArg ?? "medium"}" or above. Checked ${results.length} dependencies.`;
3353
- }
3354
- const lines = [
3355
- `# Supply Chain Health (${filtered.length} risks, threshold: ${thresholdArg ?? "medium"})`,
3356
- ""
3357
- ];
3358
- for (const r of filtered) {
3359
- const scoreStr = r.scorecardScore !== null ? ` scorecard=${r.scorecardScore.toFixed(1)}/10` : "";
3360
- const maintainerStr = r.maintainerCount !== null ? ` maintainers=${r.maintainerCount}` : "";
3361
- lines.push(`${r.riskLevel.toUpperCase()} ${r.packageName} (${r.ecosystem})${scoreStr}${maintainerStr}`);
3362
- for (const reason of r.riskReasons) {
3363
- lines.push(` \u2022 ${reason}`);
3364
- }
3365
- }
3366
- if (errors.length > 0) {
3367
- lines.push("", `\u26A0 ${errors.length} package${errors.length !== 1 ? "s" : ""} could not be checked (network errors)`);
3368
- }
3369
- return truncate(lines.join("\n"));
3370
- }
3371
- case "kirograph_dep_confusion": {
3372
- const { loadConfig } = await Promise.resolve().then(() => require("../config.js"));
3373
- const projectRoot = cg.getProjectRoot();
3374
- const config = await loadConfig(projectRoot);
3375
- if (!config.enableSecurity) return "Security analysis is not enabled. Set enableSecurity: true in .kirograph/config.json and run kirograph index.";
3376
- const db = cg.getDatabase();
3377
- db.applySecuritySchema();
3378
- const { DepConfusionChecker } = await Promise.resolve().then(() => require("../security/dep-confusion.js"));
3379
- const checker = new DepConfusionChecker(db);
3380
- const findings = await checker.check();
3381
- if (findings.length === 0) {
3382
- return "No dependency confusion vulnerabilities detected.";
3383
- }
3384
- const RISK_ORDER = { critical: 0, high: 1, medium: 2 };
3385
- const sorted = [...findings].sort((a, b) => (RISK_ORDER[a.riskLevel] ?? 3) - (RISK_ORDER[b.riskLevel] ?? 3));
3386
- const lines = [
3387
- `# Dependency Confusion (${findings.length} finding${findings.length !== 1 ? "s" : ""})`,
3388
- ""
3389
- ];
3390
- for (const f of sorted) {
3391
- lines.push(`${f.riskLevel.toUpperCase()} ${f.packageName} (${f.ecosystem})`);
3392
- lines.push(` ${f.explanation}`);
3393
- if (f.publicExists && f.publicVersion) {
3394
- lines.push(` Public version: ${f.publicVersion}${f.publicPublishedAt ? ` published: ${f.publicPublishedAt}` : ""}`);
3395
- }
3396
- }
3397
- return truncate(lines.join("\n"));
3398
- }
3399
- case "kirograph_remediation": {
3400
- const { loadConfig } = await Promise.resolve().then(() => require("../config.js"));
3401
- const projectRoot = cg.getProjectRoot();
3402
- const config = await loadConfig(projectRoot);
3403
- if (!config.enableSecurity) return "Security analysis is not enabled. Set enableSecurity: true in .kirograph/config.json and run kirograph index.";
3404
- const db = cg.getDatabase();
3405
- db.applySecuritySchema();
3406
- const { RemediationTracker } = await Promise.resolve().then(() => require("../security/remediation.js"));
3407
- const tracker = new RemediationTracker(db);
3408
- let statuses = tracker.getStatus();
3409
- if (args.overdueOnly === true) {
3410
- statuses = statuses.filter((s) => s.isOverdue);
3411
- }
3412
- if (statuses.length === 0) {
3413
- return args.overdueOnly === true ? "No overdue vulnerabilities found." : "No open vulnerabilities with SLA tracking data found.";
3414
- }
3415
- const SLA_ORDER = { overdue: 0, warning: 1, no_fix: 2, ok: 3 };
3416
- statuses.sort((a, b) => {
3417
- const slaA = SLA_ORDER[a.slaStatus] ?? 3;
3418
- const slaB = SLA_ORDER[b.slaStatus] ?? 3;
3419
- if (slaA !== slaB) return slaA - slaB;
3420
- return (b.severity ?? 0) - (a.severity ?? 0);
3421
- });
3422
- const overdueCount = statuses.filter((s) => s.isOverdue).length;
3423
- const warningCount = statuses.filter((s) => s.slaStatus === "warning").length;
3424
- const lines = [
3425
- `# Remediation SLA (${statuses.length} open${overdueCount > 0 ? `, ${overdueCount} overdue` : ""}${warningCount > 0 ? `, ${warningCount} warning` : ""})`,
3426
- ""
3427
- ];
3428
- for (const s of statuses) {
3429
- const severityLabel = s.severity == null ? "UNKNOWN" : s.severity >= 9 ? "CRITICAL" : s.severity >= 7 ? "HIGH" : s.severity >= 4 ? "MEDIUM" : "LOW";
3430
- const slaTag = s.slaStatus === "overdue" ? "[OVERDUE]" : s.slaStatus === "warning" ? "[WARNING]" : s.slaStatus === "no_fix" ? "[NO_FIX]" : "[OK]";
3431
- lines.push(`${slaTag} ${severityLabel} ${s.cveId} ${s.packageName}`);
3432
- if (s.daysOpen !== null) lines.push(` Open for ${s.daysOpen} day${s.daysOpen !== 1 ? "s" : ""}`);
3433
- if (s.daysWithFixAvailable !== null) lines.push(` Fix available for ${s.daysWithFixAvailable} day${s.daysWithFixAvailable !== 1 ? "s" : ""}`);
3434
- if (s.slaDeadline !== null) {
3435
- const deadlineDate = new Date(s.slaDeadline).toISOString().slice(0, 10);
3436
- lines.push(` SLA deadline: ${deadlineDate}`);
3437
- }
3438
- }
3439
- return truncate(lines.join("\n"));
3440
- }
3441
- case "kirograph_live_search": {
3442
- const { loadConfig } = await Promise.resolve().then(() => require("../config.js"));
3443
- const projectRoot = cg.getProjectRoot();
3444
- const config = await loadConfig(projectRoot);
3445
- if (!config.enablePatterns) {
3446
- return "kirograph_live_search requires enablePatterns: true in .kirograph/config.json";
3447
- }
3448
- const { PatternRunner } = await Promise.resolve().then(() => require("../patterns/runner.js"));
3449
- const runner = new PatternRunner();
3450
- if (!runner.isAvailable()) {
3451
- return "kirograph_live_search requires @ast-grep/napi. Run: npm install @ast-grep/napi";
3452
- }
3453
- const pattern = args.pattern;
3454
- const language = args.language;
3455
- const rawLimit = typeof args.limit === "number" ? args.limit : 20;
3456
- const limit = Math.max(1, Math.min(100, Math.round(rawLimit)));
3457
- const db = cg.getDatabase();
3458
- const rawDb = db.getRawDb();
3459
- const files = rawDb.all(
3460
- "SELECT path FROM files WHERE language = ? LIMIT 5000",
3461
- [language]
3462
- );
3463
- const results = [];
3464
- let truncated = false;
3465
- for (const file of files) {
3466
- if (results.length >= limit) {
3467
- truncated = true;
3468
- break;
3469
- }
3470
- let content;
3471
- try {
3472
- const fullPath = path.isAbsolute(file.path) ? file.path : path.join(projectRoot, file.path);
3473
- content = fs.readFileSync(fullPath, "utf8");
3474
- } catch {
3475
- continue;
3476
- }
3477
- const matches = await runner.runInline(pattern, language, content);
3478
- for (const m of matches) {
3479
- if (results.length >= limit) {
3480
- truncated = true;
3481
- break;
3482
- }
3483
- results.push({ filePath: file.path, line: m.line, matchText: m.matchText });
3484
- }
3485
- }
3486
- if (results.length === 0) {
3487
- return `0 matches for '${pattern}' in ${language} files`;
3488
- }
3489
- const lines = [
3490
- `${results.length} match${results.length !== 1 ? "es" : ""} for '${pattern}' in ${language} files`,
3491
- ""
3492
- ];
3493
- for (const r of results) {
3494
- lines.push(` ${r.filePath}:${r.line}`);
3495
- lines.push(` ${r.matchText}`);
3496
- lines.push("");
3497
- }
3498
- if (truncated) {
3499
- lines.push(`[truncated \u2014 showing first ${limit} results; refine your pattern or use --lang to narrow scope]`);
3500
- }
3501
- return truncate(lines.join("\n"));
3502
- }
3503
- // ── Pattern tools ──────────────────────────────────────────────────────────
3504
- case "kirograph_pattern_coverage": {
3505
- const { loadConfig } = await Promise.resolve().then(() => require("../config.js"));
3506
- const projectRoot = cg.getProjectRoot();
3507
- const config = await loadConfig(projectRoot);
3508
- if (!config.enablePatterns) {
3509
- return "Pattern analysis is not enabled. Set enablePatterns: true in .kirograph/config.json and run kirograph index.";
3510
- }
3511
- const { PatternLibraryLoader } = await Promise.resolve().then(() => require("../patterns/loader.js"));
3512
- const loader = new PatternLibraryLoader();
3513
- const builtinPath = path.join(__dirname, "../patterns/library");
3514
- const customPath = config.patternLibraryPath;
3515
- let rules = [];
3516
- try {
3517
- rules = loader.load(builtinPath, customPath);
3518
- } catch {
3519
- }
3520
- const rulesByCategory = /* @__PURE__ */ new Map();
3521
- for (const rule of rules) {
3522
- const prefix = rule.owaspCategory.match(/^(A\d{2})/)?.[1] ?? rule.owaspCategory;
3523
- if (!rulesByCategory.has(prefix)) rulesByCategory.set(prefix, []);
3524
- rulesByCategory.get(prefix).push(rule);
3525
- }
3526
- const db = cg.getDatabase();
3527
- db.applyPatternsSchema();
3528
- const rawDb = db.getRawDb();
3529
- const matchCountsByCategory = /* @__PURE__ */ new Map();
3530
- try {
3531
- const tableExists = rawDb.get("SELECT name FROM sqlite_master WHERE type='table' AND name='pattern_matches'");
3532
- if (tableExists) {
3533
- const rows = rawDb.all(
3534
- "SELECT owasp_category, COUNT(*) as cnt FROM pattern_matches GROUP BY owasp_category"
3535
- );
3536
- for (const row of rows) {
3537
- const prefix = row.owasp_category.match(/^(A\d{2})/)?.[1] ?? row.owasp_category;
3538
- matchCountsByCategory.set(prefix, (matchCountsByCategory.get(prefix) ?? 0) + row.cnt);
3539
- }
3540
- }
3541
- } catch {
3542
- }
3543
- const owaspTop10 = [
3544
- "A01",
3545
- "A02",
3546
- "A03",
3547
- "A04",
3548
- "A05",
3549
- "A06",
3550
- "A07",
3551
- "A08",
3552
- "A09",
3553
- "A10"
3554
- ];
3555
- const owaspNames = {
3556
- A01: "Broken Access Control",
3557
- A02: "Cryptographic Failures",
3558
- A03: "Injection",
3559
- A04: "Insecure Design",
3560
- A05: "Security Misconfiguration",
3561
- A06: "Vulnerable and Outdated Components",
3562
- A07: "Identification and Authentication Failures",
3563
- A08: "Software and Data Integrity Failures",
3564
- A09: "Security Logging and Monitoring Failures",
3565
- A10: "Server-Side Request Forgery"
3566
- };
3567
- const covered = [];
3568
- const uncovered = [];
3569
- const lines = [
3570
- "# OWASP Top 10 Pattern Coverage",
3571
- "",
3572
- `Total rules loaded: ${rules.length}`,
3573
- "",
3574
- "Category | Rules | Matches",
3575
- "-------- | ----- | -------"
3576
- ];
3577
- for (const cat of owaspTop10) {
3578
- const catRules = rulesByCategory.get(cat) ?? [];
3579
- const matchCount = matchCountsByCategory.get(cat) ?? 0;
3580
- const label = `${cat}: ${owaspNames[cat] ?? cat}`;
3581
- if (catRules.length > 0) {
3582
- covered.push(label);
3583
- lines.push(`${label} | ${catRules.length} | ${matchCount}`);
3584
- } else {
3585
- uncovered.push(label);
3586
- }
3587
- }
3588
- for (const [cat, catRules] of rulesByCategory.entries()) {
3589
- if (!owaspTop10.includes(cat)) {
3590
- const matchCount = matchCountsByCategory.get(cat) ?? 0;
3591
- lines.push(`${cat} | ${catRules.length} | ${matchCount}`);
3592
- }
3593
- }
3594
- if (uncovered.length > 0) {
3595
- lines.push("", `Uncovered categories (${uncovered.length}):`, ...uncovered.map((c) => ` - ${c}`));
3596
- }
3597
- lines.push("", `Coverage: ${covered.length} / ${owaspTop10.length} OWASP Top 10 categories`);
3598
- return lines.join("\n");
3599
- }
3600
- case "kirograph_pattern_save_baseline": {
3601
- const { loadConfig } = await Promise.resolve().then(() => require("../config.js"));
3602
- const projectRoot = cg.getProjectRoot();
3603
- const config = await loadConfig(projectRoot);
3604
- if (!config.enablePatterns) {
3605
- return "Pattern analysis is not enabled. Set enablePatterns: true in .kirograph/config.json and run kirograph index.";
3606
- }
3607
- const label = args.label ?? "default";
3608
- const db = cg.getDatabase();
3609
- db.applyPatternsSchema();
3610
- const rawDb = db.getRawDb();
3611
- const tableExists = rawDb.get("SELECT name FROM sqlite_master WHERE type='table' AND name='pattern_matches'");
3612
- if (!tableExists) {
3613
- return "No pattern_matches table found. Run kirograph index first to populate pattern data.";
3614
- }
3615
- const rows = rawDb.all(
3616
- "SELECT pattern_id, COUNT(*) as cnt FROM pattern_matches GROUP BY pattern_id"
3617
- );
3618
- const baseline = {};
3619
- for (const row of rows) {
3620
- baseline[row.pattern_id] = row.cnt;
3621
- }
3622
- const kirographDir = path.join(projectRoot, ".kirograph");
3623
- if (!fs.existsSync(kirographDir)) fs.mkdirSync(kirographDir, { recursive: true });
3624
- const baselinePath = path.join(kirographDir, `pattern-baseline-${label}.json`);
3625
- fs.writeFileSync(baselinePath, JSON.stringify({ label, savedAt: Date.now(), counts: baseline }, null, 2));
3626
- const totalMatches = Object.values(baseline).reduce((s, c) => s + c, 0);
3627
- return `Baseline "${label}" saved: ${Object.keys(baseline).length} patterns, ${totalMatches} total matches \u2192 ${baselinePath}`;
3628
- }
3629
- case "kirograph_pattern_diff": {
3630
- const { loadConfig } = await Promise.resolve().then(() => require("../config.js"));
3631
- const projectRoot = cg.getProjectRoot();
3632
- const config = await loadConfig(projectRoot);
3633
- if (!config.enablePatterns) {
3634
- return "Pattern analysis is not enabled. Set enablePatterns: true in .kirograph/config.json and run kirograph index.";
3635
- }
3636
- const label = args.label ?? "default";
3637
- const baselinePath = path.join(projectRoot, ".kirograph", `pattern-baseline-${label}.json`);
3638
- if (!fs.existsSync(baselinePath)) {
3639
- return `Baseline "${label}" not found. Run kirograph_pattern_save_baseline first.`;
3640
- }
3641
- let baseline;
3642
- try {
3643
- baseline = JSON.parse(fs.readFileSync(baselinePath, "utf8"));
3644
- } catch {
3645
- return `Failed to read baseline file: ${baselinePath}`;
3646
- }
3647
- const db = cg.getDatabase();
3648
- db.applyPatternsSchema();
3649
- const rawDb = db.getRawDb();
3650
- const tableExists = rawDb.get("SELECT name FROM sqlite_master WHERE type='table' AND name='pattern_matches'");
3651
- const currentCounts = {};
3652
- if (tableExists) {
3653
- const rows = rawDb.all(
3654
- "SELECT pattern_id, COUNT(*) as cnt FROM pattern_matches GROUP BY pattern_id"
3655
- );
3656
- for (const row of rows) {
3657
- currentCounts[row.pattern_id] = row.cnt;
3658
- }
3659
- }
3660
- const allPatternIds = /* @__PURE__ */ new Set([...Object.keys(baseline.counts), ...Object.keys(currentCounts)]);
3661
- const newFindings = [];
3662
- const resolvedFindings = [];
3663
- const unchanged = [];
3664
- const changed = [];
3665
- for (const id of allPatternIds) {
3666
- const before = baseline.counts[id] ?? 0;
3667
- const after = currentCounts[id] ?? 0;
3668
- if (before === 0 && after > 0) {
3669
- newFindings.push({ id, count: after });
3670
- } else if (before > 0 && after === 0) {
3671
- resolvedFindings.push({ id, count: before });
3672
- } else if (before === after) {
3673
- unchanged.push({ id, count: after });
3674
- } else {
3675
- changed.push({ id, before, after, delta: after - before });
3676
- }
3677
- }
3678
- const savedDate = new Date(baseline.savedAt).toISOString().slice(0, 16).replace("T", " ");
3679
- const lines = [
3680
- `# Pattern Diff: "${label}" (saved ${savedDate}) \u2192 current`,
3681
- "",
3682
- `NEW: ${newFindings.length} RESOLVED: ${resolvedFindings.length} CHANGED: ${changed.length} UNCHANGED: ${unchanged.length}`
3683
- ];
3684
- if (newFindings.length > 0) {
3685
- lines.push("", "## New Findings (not in baseline)");
3686
- for (const f of newFindings.sort((a, b) => b.count - a.count)) {
3687
- lines.push(` + ${f.id}: ${f.count} matches`);
3688
- }
3689
- }
3690
- if (resolvedFindings.length > 0) {
3691
- lines.push("", "## Resolved (in baseline, not in current)");
3692
- for (const f of resolvedFindings.sort((a, b) => b.count - a.count)) {
3693
- lines.push(` - ${f.id}: was ${f.count} matches`);
3694
- }
3695
- }
3696
- if (changed.length > 0) {
3697
- lines.push("", "## Changed");
3698
- for (const f of changed.sort((a, b) => Math.abs(b.delta) - Math.abs(a.delta))) {
3699
- const sign = f.delta > 0 ? "+" : "";
3700
- lines.push(` ~ ${f.id}: ${f.before} \u2192 ${f.after} (${sign}${f.delta})`);
3701
- }
3702
- }
3703
- return lines.join("\n");
3704
- }
3705
- // ── Wiki tools ────────────────────────────────────────────────────────────
3706
- case "kirograph_wiki_ingest": {
3707
- const { loadConfig } = await Promise.resolve().then(() => require("../config.js"));
3708
- const projectRoot = cg.getProjectRoot();
3709
- const config = await loadConfig(projectRoot);
3710
- if (!config.enableWiki) return "Wiki is not enabled. Set enableWiki: true in .kirograph/config.json";
3711
- const { KiroGraphWiki } = await Promise.resolve().then(() => require("../wiki/index.js"));
3712
- const db = cg.getDatabase();
3713
- db.applyWikiSchema();
3714
- const wiki = new KiroGraphWiki(db.getRawDb(), projectRoot + "/.kirograph", {
3715
- autoResolveConflicts: config.wikiAutoResolveConflicts ?? false
3716
- });
3717
- wiki.initialize();
3718
- const source = args.source ?? "";
3719
- const sourceName = args.sourceName ?? "source";
3720
- if (!source) return "source is required";
3721
- if (config.wikiSynthesisMode === "local") {
3722
- wiki.queueSource(source, sourceName);
3723
- const count = wiki.getQueueCount();
3724
- return `Source "${sourceName}" queued for local wiki synthesis (${count} in queue). The agentStop hook will run "kirograph wiki synthesize" automatically.`;
3725
- }
3726
- const prompt = wiki.getIngestPrompt(source, sourceName);
3727
- return `WIKI_INGEST_PROMPT
3728
- ${prompt}
3729
-
3730
- Produce WIKI_DIFF blocks following SCHEMA.md, then call kirograph_wiki_apply_diff with the diff string.`;
3731
- }
3732
- case "kirograph_wiki_apply_diff": {
3733
- const { loadConfig } = await Promise.resolve().then(() => require("../config.js"));
3734
- const projectRoot = cg.getProjectRoot();
3735
- const config = await loadConfig(projectRoot);
3736
- if (!config.enableWiki) return "Wiki is not enabled. Set enableWiki: true in .kirograph/config.json";
3737
- const { KiroGraphWiki } = await Promise.resolve().then(() => require("../wiki/index.js"));
3738
- const db = cg.getDatabase();
3739
- db.applyWikiSchema();
3740
- const wiki = new KiroGraphWiki(db.getRawDb(), projectRoot + "/.kirograph", {
3741
- autoResolveConflicts: config.wikiAutoResolveConflicts ?? false
3742
- });
3743
- wiki.initialize();
3744
- const diff = args.diff ?? "";
3745
- if (!diff) return "diff is required";
3746
- const result = wiki.applyDiff(diff);
3747
- const lines = ["\u2713 Wiki diff applied"];
3748
- if (result.created.length) lines.push(` Created: ${result.created.join(", ")}`);
3749
- if (result.updated.length) lines.push(` Updated: ${result.updated.join(", ")}`);
3750
- if (result.conflictsResolved.length) lines.push(` Conflicts auto-resolved: ${result.conflictsResolved.join(", ")}`);
3751
- if (result.conflictsPending.length) {
3752
- lines.push(` \u26A1 Conflicts pending (${result.conflictsPending.length}):`);
3753
- for (const c of result.conflictsPending) {
3754
- lines.push(` ${c.page} \xA7${c.section}: "${c.existing}" vs "${c.incoming}" (source: ${c.source})`);
3755
- }
3756
- }
3757
- return lines.join("\n");
3758
- }
3759
- case "kirograph_wiki_search": {
3760
- const { loadConfig } = await Promise.resolve().then(() => require("../config.js"));
3761
- const projectRoot = cg.getProjectRoot();
3762
- const config = await loadConfig(projectRoot);
3763
- if (!config.enableWiki) return "Wiki is not enabled. Set enableWiki: true in .kirograph/config.json";
3764
- const { KiroGraphWiki } = await Promise.resolve().then(() => require("../wiki/index.js"));
3765
- const db = cg.getDatabase();
3766
- db.applyWikiSchema();
3767
- const wiki = new KiroGraphWiki(db.getRawDb(), projectRoot + "/.kirograph");
3768
- wiki.initialize();
3769
- const query = args.query ?? "";
3770
- if (!query) return "query is required";
3771
- const limit = Math.min(Number(args.limit ?? 5), 20);
3772
- const results = wiki.search(query, limit);
3773
- if (results.length === 0) return `No wiki pages found for "${query}".`;
3774
- const lines = [`Wiki Search: ${query}`, ""];
3775
- for (const { page, score } of results) {
3776
- lines.push(`[${page.slug}] ${page.title} (score: ${score.toFixed(3)})`);
3777
- const preview = page.content.split("\n").find((l) => l.trim() && !l.startsWith("#"));
3778
- if (preview) lines.push(` ${preview.trim().slice(0, 120)}`);
3779
- lines.push("");
3780
- }
3781
- return lines.join("\n").trim();
3782
- }
3783
- case "kirograph_wiki_page": {
3784
- const { loadConfig } = await Promise.resolve().then(() => require("../config.js"));
3785
- const projectRoot = cg.getProjectRoot();
3786
- const config = await loadConfig(projectRoot);
3787
- if (!config.enableWiki) return "Wiki is not enabled. Set enableWiki: true in .kirograph/config.json";
3788
- const { KiroGraphWiki } = await Promise.resolve().then(() => require("../wiki/index.js"));
3789
- const db = cg.getDatabase();
3790
- db.applyWikiSchema();
3791
- const wiki = new KiroGraphWiki(db.getRawDb(), projectRoot + "/.kirograph");
3792
- wiki.initialize();
3793
- const slug = args.slug ?? "";
3794
- if (!slug) return "slug is required";
3795
- const page = wiki.getPage(slug);
3796
- if (!page) return `Wiki page "${slug}" not found. Use kirograph_wiki_list to see available pages.`;
3797
- return page.content;
3798
- }
3799
- case "kirograph_wiki_lint": {
3800
- const { loadConfig } = await Promise.resolve().then(() => require("../config.js"));
3801
- const projectRoot = cg.getProjectRoot();
3802
- const config = await loadConfig(projectRoot);
3803
- if (!config.enableWiki) return "Wiki is not enabled. Set enableWiki: true in .kirograph/config.json";
3804
- const { KiroGraphWiki } = await Promise.resolve().then(() => require("../wiki/index.js"));
3805
- const db = cg.getDatabase();
3806
- db.applyWikiSchema();
3807
- const wiki = new KiroGraphWiki(db.getRawDb(), projectRoot + "/.kirograph");
3808
- wiki.initialize();
3809
- const issues = wiki.lint();
3810
- if (issues.length === 0) return "\u2713 Wiki lint passed \u2014 no issues found.";
3811
- const lines = [`Wiki Lint \u2014 ${issues.length} issue(s)`, ""];
3812
- for (const issue of issues) {
3813
- const icon = issue.kind === "contradiction" ? "\u26A1" : issue.kind === "orphan" ? "\u25CB" : issue.kind === "broken_link" ? "\u{1F517}" : "\u26A0";
3814
- lines.push(`${icon} [${issue.kind}] ${issue.slug}`);
3815
- lines.push(` ${issue.detail}`);
3816
- lines.push("");
3817
- }
3818
- return lines.join("\n").trim();
3819
- }
3820
- case "kirograph_wiki_list": {
3821
- const { loadConfig } = await Promise.resolve().then(() => require("../config.js"));
3822
- const projectRoot = cg.getProjectRoot();
3823
- const config = await loadConfig(projectRoot);
3824
- if (!config.enableWiki) return "Wiki is not enabled. Set enableWiki: true in .kirograph/config.json";
3825
- const { KiroGraphWiki } = await Promise.resolve().then(() => require("../wiki/index.js"));
3826
- const db = cg.getDatabase();
3827
- db.applyWikiSchema();
3828
- const wiki = new KiroGraphWiki(db.getRawDb(), projectRoot + "/.kirograph");
3829
- wiki.initialize();
3830
- const pages = wiki.listPages();
3831
- if (pages.length === 0) return "Wiki is empty. Use kirograph_wiki_ingest to add pages.";
3832
- const stats = wiki.getStats();
3833
- const lines = [
3834
- `Wiki \u2014 ${stats.pageCount} page(s), ${stats.totalSources} total sources`,
3835
- ""
3836
- ];
3837
- for (const page of pages) {
3838
- const date = new Date(page.updatedAt).toISOString().slice(0, 10);
3839
- lines.push(` ${page.slug.padEnd(32)} ${page.title} (${page.sourceCount} src, ${date})`);
3840
- }
3841
- return lines.join("\n");
3842
- }
3843
- default:
3844
- return `Unknown tool: ${toolName}`;
3845
- }
3846
- }
3847
- }
3848
1675
  // Annotate the CommonJS export names for ESM import in node:
3849
1676
  0 && (module.exports = {
3850
1677
  KIROGRAPH_TOOL_NAMES,