@massu/core 0.1.0 → 0.1.1

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 (114) hide show
  1. package/LICENSE +71 -0
  2. package/dist/hooks/cost-tracker.js +127 -11493
  3. package/dist/hooks/post-edit-context.js +125 -11491
  4. package/dist/hooks/post-tool-use.js +127 -11493
  5. package/dist/hooks/pre-compact.js +127 -11493
  6. package/dist/hooks/pre-delete-check.js +126 -11492
  7. package/dist/hooks/quality-event.js +127 -11493
  8. package/dist/hooks/session-end.js +127 -11493
  9. package/dist/hooks/session-start.js +127 -11493
  10. package/dist/hooks/user-prompt.js +127 -11493
  11. package/package.json +9 -8
  12. package/src/__tests__/adr-generator.test.ts +260 -0
  13. package/src/__tests__/analytics.test.ts +282 -0
  14. package/src/__tests__/audit-trail.test.ts +382 -0
  15. package/src/__tests__/backfill-sessions.test.ts +690 -0
  16. package/src/__tests__/cli.test.ts +290 -0
  17. package/src/__tests__/cloud-sync.test.ts +261 -0
  18. package/src/__tests__/config-sections.test.ts +359 -0
  19. package/src/__tests__/config.test.ts +732 -0
  20. package/src/__tests__/cost-tracker.test.ts +348 -0
  21. package/src/__tests__/db.test.ts +177 -0
  22. package/src/__tests__/dependency-scorer.test.ts +325 -0
  23. package/src/__tests__/docs-integration.test.ts +178 -0
  24. package/src/__tests__/docs-tools.test.ts +199 -0
  25. package/src/__tests__/domains.test.ts +236 -0
  26. package/src/__tests__/hooks.test.ts +221 -0
  27. package/src/__tests__/import-resolver.test.ts +95 -0
  28. package/src/__tests__/integration/path-traversal.test.ts +134 -0
  29. package/src/__tests__/integration/pricing-consistency.test.ts +88 -0
  30. package/src/__tests__/integration/tool-registration.test.ts +146 -0
  31. package/src/__tests__/memory-db.test.ts +404 -0
  32. package/src/__tests__/memory-enhancements.test.ts +316 -0
  33. package/src/__tests__/memory-tools.test.ts +199 -0
  34. package/src/__tests__/middleware-tree.test.ts +177 -0
  35. package/src/__tests__/observability-tools.test.ts +595 -0
  36. package/src/__tests__/observability.test.ts +437 -0
  37. package/src/__tests__/observation-extractor.test.ts +167 -0
  38. package/src/__tests__/page-deps.test.ts +60 -0
  39. package/src/__tests__/prompt-analyzer.test.ts +298 -0
  40. package/src/__tests__/regression-detector.test.ts +295 -0
  41. package/src/__tests__/rules.test.ts +87 -0
  42. package/src/__tests__/schema-mapper.test.ts +29 -0
  43. package/src/__tests__/security-scorer.test.ts +238 -0
  44. package/src/__tests__/security-utils.test.ts +175 -0
  45. package/src/__tests__/sentinel-db.test.ts +491 -0
  46. package/src/__tests__/sentinel-scanner.test.ts +750 -0
  47. package/src/__tests__/sentinel-tools.test.ts +324 -0
  48. package/src/__tests__/sentinel-types.test.ts +750 -0
  49. package/src/__tests__/server.test.ts +452 -0
  50. package/src/__tests__/session-archiver.test.ts +524 -0
  51. package/src/__tests__/session-state-generator.test.ts +900 -0
  52. package/src/__tests__/team-knowledge.test.ts +327 -0
  53. package/src/__tests__/tools.test.ts +340 -0
  54. package/src/__tests__/transcript-parser.test.ts +195 -0
  55. package/src/__tests__/trpc-index.test.ts +25 -0
  56. package/src/__tests__/validate-features-runner.test.ts +517 -0
  57. package/src/__tests__/validation-engine.test.ts +300 -0
  58. package/src/adr-generator.ts +285 -0
  59. package/src/analytics.ts +367 -0
  60. package/src/audit-trail.ts +443 -0
  61. package/src/backfill-sessions.ts +180 -0
  62. package/src/cli.ts +105 -0
  63. package/src/cloud-sync.ts +194 -0
  64. package/src/commands/doctor.ts +300 -0
  65. package/src/commands/init.ts +399 -0
  66. package/src/commands/install-hooks.ts +26 -0
  67. package/src/config.ts +357 -0
  68. package/src/core-tools.ts +685 -0
  69. package/src/cost-tracker.ts +350 -0
  70. package/src/db.ts +233 -0
  71. package/src/dependency-scorer.ts +330 -0
  72. package/src/docs-map.json +100 -0
  73. package/src/docs-tools.ts +514 -0
  74. package/src/domains.ts +181 -0
  75. package/src/hooks/cost-tracker.ts +66 -0
  76. package/src/hooks/intent-suggester.ts +131 -0
  77. package/src/hooks/post-edit-context.ts +91 -0
  78. package/src/hooks/post-tool-use.ts +175 -0
  79. package/src/hooks/pre-compact.ts +146 -0
  80. package/src/hooks/pre-delete-check.ts +153 -0
  81. package/src/hooks/quality-event.ts +127 -0
  82. package/src/hooks/security-gate.ts +121 -0
  83. package/src/hooks/session-end.ts +467 -0
  84. package/src/hooks/session-start.ts +210 -0
  85. package/src/hooks/user-prompt.ts +91 -0
  86. package/src/import-resolver.ts +224 -0
  87. package/src/memory-db.ts +48 -0
  88. package/src/memory-queries.ts +804 -0
  89. package/src/memory-schema.ts +546 -0
  90. package/src/memory-tools.ts +392 -0
  91. package/src/middleware-tree.ts +70 -0
  92. package/src/observability-tools.ts +332 -0
  93. package/src/observation-extractor.ts +411 -0
  94. package/src/page-deps.ts +283 -0
  95. package/src/prompt-analyzer.ts +325 -0
  96. package/src/regression-detector.ts +313 -0
  97. package/src/rules.ts +57 -0
  98. package/src/schema-mapper.ts +232 -0
  99. package/src/security-scorer.ts +398 -0
  100. package/src/security-utils.ts +133 -0
  101. package/src/sentinel-db.ts +623 -0
  102. package/src/sentinel-scanner.ts +405 -0
  103. package/src/sentinel-tools.ts +515 -0
  104. package/src/sentinel-types.ts +140 -0
  105. package/src/server.ts +190 -0
  106. package/src/session-archiver.ts +112 -0
  107. package/src/session-state-generator.ts +174 -0
  108. package/src/team-knowledge.ts +400 -0
  109. package/src/tool-helpers.ts +41 -0
  110. package/src/tools.ts +111 -0
  111. package/src/transcript-parser.ts +458 -0
  112. package/src/trpc-index.ts +214 -0
  113. package/src/validate-features-runner.ts +107 -0
  114. package/src/validation-engine.ts +351 -0
@@ -0,0 +1,400 @@
1
+ // Copyright (c) 2026 Massu. All rights reserved.
2
+ // Licensed under BSL 1.1 - see LICENSE file for details.
3
+
4
+ import type Database from 'better-sqlite3';
5
+ import type { ToolDefinition, ToolResult } from './tool-helpers.ts';
6
+ import { p, text } from './tool-helpers.ts';
7
+ import { getConfig } from './config.ts';
8
+
9
+ // ============================================================
10
+ // Team Knowledge Graph
11
+ // ============================================================
12
+
13
+ /**
14
+ * Calculate expertise score for a developer in a module.
15
+ * Based on session depth (how many sessions) and observation quality.
16
+ */
17
+ export function calculateExpertise(
18
+ sessionCount: number,
19
+ observationCount: number
20
+ ): number {
21
+ const config = getConfig();
22
+ const sessionWeight = config.team?.expertise_weights?.session ?? 20;
23
+ const observationWeight = config.team?.expertise_weights?.observation ?? 10;
24
+
25
+ const sessionScore = Math.log2(sessionCount + 1) * sessionWeight;
26
+ const obsScore = Math.log2(observationCount + 1) * observationWeight;
27
+ return Math.min(100, Math.round(sessionScore + obsScore));
28
+ }
29
+
30
+ /**
31
+ * Update developer expertise based on session observations.
32
+ */
33
+ export function updateExpertise(
34
+ db: Database.Database,
35
+ developerId: string,
36
+ sessionId: string
37
+ ): void {
38
+ const fileChanges = db.prepare(`
39
+ SELECT DISTINCT files_involved FROM observations
40
+ WHERE session_id = ? AND type IN ('file_change', 'feature', 'bugfix', 'refactor')
41
+ `).all(sessionId) as Array<{ files_involved: string }>;
42
+
43
+ const modules = new Set<string>();
44
+ for (const fc of fileChanges) {
45
+ try {
46
+ const files = JSON.parse(fc.files_involved) as string[];
47
+ for (const file of files) {
48
+ const module = extractModule(file);
49
+ if (module) modules.add(module);
50
+ }
51
+ } catch { /* skip */ }
52
+ }
53
+
54
+ for (const module of modules) {
55
+ const existing = db.prepare(
56
+ 'SELECT session_count, observation_count FROM developer_expertise WHERE developer_id = ? AND module = ?'
57
+ ).get(developerId, module) as { session_count: number; observation_count: number } | undefined;
58
+
59
+ const sessionCount = (existing?.session_count ?? 0) + 1;
60
+ const obsCount = (existing?.observation_count ?? 0) + fileChanges.length;
61
+ const score = calculateExpertise(sessionCount, obsCount);
62
+
63
+ db.prepare(`
64
+ INSERT INTO developer_expertise (developer_id, module, session_count, observation_count, expertise_score, last_active)
65
+ VALUES (?, ?, ?, ?, ?, datetime('now'))
66
+ ON CONFLICT(developer_id, module) DO UPDATE SET
67
+ session_count = ?,
68
+ observation_count = ?,
69
+ expertise_score = ?,
70
+ last_active = datetime('now')
71
+ `).run(
72
+ developerId, module, sessionCount, obsCount, score,
73
+ sessionCount, obsCount, score
74
+ );
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Detect potential conflicts between developers working on same files.
80
+ */
81
+ export function detectConflicts(
82
+ db: Database.Database,
83
+ daysBack: number = 7
84
+ ): Array<{
85
+ filePath: string;
86
+ developerA: string;
87
+ developerB: string;
88
+ conflictType: string;
89
+ }> {
90
+ const conflicts = db.prepare(`
91
+ SELECT so1.file_path,
92
+ so1.developer_id as developer_a,
93
+ so2.developer_id as developer_b,
94
+ 'concurrent_edit' as conflict_type
95
+ FROM shared_observations so1
96
+ JOIN shared_observations so2 ON so1.file_path = so2.file_path
97
+ WHERE so1.developer_id != so2.developer_id
98
+ AND so1.file_path IS NOT NULL
99
+ AND so1.created_at >= datetime('now', ?)
100
+ AND so2.created_at >= datetime('now', ?)
101
+ GROUP BY so1.file_path, so1.developer_id, so2.developer_id
102
+ `).all(`-${daysBack} days`, `-${daysBack} days`) as Array<{
103
+ file_path: string;
104
+ developer_a: string;
105
+ developer_b: string;
106
+ conflict_type: string;
107
+ }>;
108
+
109
+ return conflicts.map(c => ({
110
+ filePath: c.file_path,
111
+ developerA: c.developer_a,
112
+ developerB: c.developer_b,
113
+ conflictType: c.conflict_type,
114
+ }));
115
+ }
116
+
117
+ /**
118
+ * Share an observation for team visibility.
119
+ */
120
+ export function shareObservation(
121
+ db: Database.Database,
122
+ developerId: string,
123
+ project: string,
124
+ observationType: string,
125
+ summary: string,
126
+ opts?: {
127
+ originalId?: number;
128
+ filePath?: string;
129
+ module?: string;
130
+ severity?: number;
131
+ }
132
+ ): number {
133
+ const result = db.prepare(`
134
+ INSERT INTO shared_observations
135
+ (original_id, developer_id, project, observation_type, summary, file_path, module, severity, is_shared, shared_at)
136
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, TRUE, datetime('now'))
137
+ `).run(
138
+ opts?.originalId ?? null,
139
+ developerId, project, observationType, summary,
140
+ opts?.filePath ?? null,
141
+ opts?.module ?? null,
142
+ opts?.severity ?? 3
143
+ );
144
+ return Number(result.lastInsertRowid);
145
+ }
146
+
147
+ /**
148
+ * Extract business module from a file path.
149
+ * Uses configurable module extraction patterns if provided.
150
+ */
151
+ function extractModule(filePath: string): string | null {
152
+ // Route-based modules
153
+ const routerMatch = filePath.match(/routers\/([^/.]+)/);
154
+ if (routerMatch) return routerMatch[1];
155
+
156
+ // Page-based modules
157
+ const pageMatch = filePath.match(/app\/\(([^)]+)\)/);
158
+ if (pageMatch) return pageMatch[1];
159
+
160
+ // Component-based
161
+ const compMatch = filePath.match(/components\/([^/.]+)/);
162
+ if (compMatch) return compMatch[1];
163
+
164
+ return null;
165
+ }
166
+
167
+ // ============================================================
168
+ // MCP Tool Definitions & Handlers
169
+ // ============================================================
170
+
171
+ export function getTeamToolDefinitions(): ToolDefinition[] {
172
+ return [
173
+ {
174
+ name: p('team_search'),
175
+ description: 'Search team-shared observations. Find what other developers learned about a module or file.',
176
+ inputSchema: {
177
+ type: 'object',
178
+ properties: {
179
+ query: { type: 'string', description: 'Search text' },
180
+ module: { type: 'string', description: 'Filter by business module' },
181
+ },
182
+ required: ['query'],
183
+ },
184
+ },
185
+ {
186
+ name: p('team_expertise'),
187
+ description: 'Who knows what. Shows developers ranked by expertise for a module or file area.',
188
+ inputSchema: {
189
+ type: 'object',
190
+ properties: {
191
+ module: { type: 'string', description: 'Business module (e.g., orders, products, design)' },
192
+ file_path: { type: 'string', description: 'File path to find experts for' },
193
+ },
194
+ required: [],
195
+ },
196
+ },
197
+ {
198
+ name: p('team_conflicts'),
199
+ description: 'Detect concurrent work conflicts. Find areas where multiple developers are making changes.',
200
+ inputSchema: {
201
+ type: 'object',
202
+ properties: {
203
+ file_path: { type: 'string', description: 'Check specific file for conflicts' },
204
+ days: { type: 'number', description: 'Days to look back (default: 7)' },
205
+ },
206
+ required: [],
207
+ },
208
+ },
209
+ ];
210
+ }
211
+
212
+ const TEAM_BASE_NAMES = new Set(['team_search', 'team_expertise', 'team_conflicts']);
213
+
214
+ export function isTeamTool(name: string): boolean {
215
+ const pfx = getConfig().toolPrefix + '_';
216
+ const baseName = name.startsWith(pfx) ? name.slice(pfx.length) : name;
217
+ return TEAM_BASE_NAMES.has(baseName);
218
+ }
219
+
220
+ export function handleTeamToolCall(
221
+ name: string,
222
+ args: Record<string, unknown>,
223
+ memoryDb: Database.Database
224
+ ): ToolResult {
225
+ try {
226
+ const pfx = getConfig().toolPrefix + '_';
227
+ const baseName = name.startsWith(pfx) ? name.slice(pfx.length) : name;
228
+
229
+ switch (baseName) {
230
+ case 'team_search':
231
+ return handleTeamSearch(args, memoryDb);
232
+ case 'team_expertise':
233
+ return handleTeamExpertise(args, memoryDb);
234
+ case 'team_conflicts':
235
+ return handleTeamConflicts(args, memoryDb);
236
+ default:
237
+ return text(`Unknown team tool: ${name}`);
238
+ }
239
+ } catch (error) {
240
+ return text(`Error in ${name}: ${error instanceof Error ? error.message : String(error)}\n\nUsage: ${p('team_search')} { query: "pattern" }, ${p('team_expertise')} { module: "tasks" }`);
241
+ }
242
+ }
243
+
244
+ function handleTeamSearch(args: Record<string, unknown>, db: Database.Database): ToolResult {
245
+ const query = args.query as string;
246
+ if (!query) return text(`Usage: ${p('team_search')} { query: "search term", module: "optional-module" } - Search team-shared observations.`);
247
+
248
+ const module = args.module as string | undefined;
249
+
250
+ let sql = `
251
+ SELECT id, developer_id, observation_type, summary, file_path, module, severity, created_at
252
+ FROM shared_observations
253
+ WHERE is_shared = TRUE AND summary LIKE ?
254
+ `;
255
+ const params: (string | number)[] = [`%${query}%`];
256
+
257
+ if (module) {
258
+ sql += ' AND module = ?';
259
+ params.push(module);
260
+ }
261
+
262
+ sql += ' ORDER BY created_at DESC LIMIT 20';
263
+
264
+ const results = db.prepare(sql).all(...params) as Array<Record<string, unknown>>;
265
+
266
+ if (results.length === 0) {
267
+ return text(`No shared observations found for "${query}". Team knowledge is populated when developers share observations across sessions. Try: ${p('team_expertise')} {} to see module expertise, or broaden your search term.`);
268
+ }
269
+
270
+ const lines = [
271
+ `## Team Knowledge: "${query}" (${results.length} results)`,
272
+ '',
273
+ '| Developer | Type | Summary | Module | Date |',
274
+ '|-----------|------|---------|--------|------|',
275
+ ];
276
+
277
+ for (const r of results) {
278
+ lines.push(
279
+ `| ${r.developer_id} | ${r.observation_type} | ${(r.summary as string).slice(0, 60)} | ${r.module ?? '-'} | ${(r.created_at as string).split('T')[0]} |`
280
+ );
281
+ }
282
+
283
+ return text(lines.join('\n'));
284
+ }
285
+
286
+ function handleTeamExpertise(args: Record<string, unknown>, db: Database.Database): ToolResult {
287
+ const module = args.module as string | undefined;
288
+ const filePath = args.file_path as string | undefined;
289
+
290
+ let targetModule = module;
291
+ if (!targetModule && filePath) {
292
+ targetModule = extractModule(filePath) ?? undefined;
293
+ }
294
+
295
+ if (!targetModule) {
296
+ const modules = db.prepare(`
297
+ SELECT module, COUNT(DISTINCT developer_id) as developers, MAX(expertise_score) as top_score
298
+ FROM developer_expertise
299
+ GROUP BY module
300
+ ORDER BY developers DESC
301
+ `).all() as Array<Record<string, unknown>>;
302
+
303
+ if (modules.length === 0) {
304
+ return text(`No expertise data yet. Expertise is built automatically as developers work on modules across sessions. Try: ${p('team_search')} { query: "keyword" } to search shared observations instead.`);
305
+ }
306
+
307
+ const lines = [
308
+ '## Team Expertise Overview',
309
+ '',
310
+ '| Module | Developers | Top Score |',
311
+ '|--------|-----------|-----------|',
312
+ ];
313
+
314
+ for (const m of modules) {
315
+ lines.push(`| ${m.module} | ${m.developers} | ${m.top_score} |`);
316
+ }
317
+
318
+ lines.push('');
319
+ lines.push(`Use ${p('team_expertise')} { module: "module_name" } to see developers ranked by expertise.`);
320
+
321
+ return text(lines.join('\n'));
322
+ }
323
+
324
+ const experts = db.prepare(`
325
+ SELECT developer_id, expertise_score, session_count, observation_count, last_active
326
+ FROM developer_expertise
327
+ WHERE module = ?
328
+ ORDER BY expertise_score DESC
329
+ `).all(targetModule) as Array<Record<string, unknown>>;
330
+
331
+ if (experts.length === 0) {
332
+ return text(`No expertise data for module "${targetModule}". Expertise builds as developers work on files in this module across sessions. Try: ${p('team_expertise')} {} to see all modules with tracked expertise.`);
333
+ }
334
+
335
+ const lines = [
336
+ `## Expertise: ${targetModule}`,
337
+ '',
338
+ '| Developer | Score | Sessions | Observations | Last Active |',
339
+ '|-----------|-------|----------|--------------|-------------|',
340
+ ];
341
+
342
+ for (const e of experts) {
343
+ lines.push(
344
+ `| ${e.developer_id} | ${e.expertise_score} | ${e.session_count} | ${e.observation_count} | ${(e.last_active as string).split('T')[0]} |`
345
+ );
346
+ }
347
+
348
+ return text(lines.join('\n'));
349
+ }
350
+
351
+ function handleTeamConflicts(args: Record<string, unknown>, db: Database.Database): ToolResult {
352
+ const days = (args.days as number) ?? 7;
353
+ const filePath = args.file_path as string | undefined;
354
+
355
+ if (filePath) {
356
+ const conflicts = db.prepare(`
357
+ SELECT developer_a, developer_b, conflict_type, detected_at, resolved
358
+ FROM knowledge_conflicts
359
+ WHERE file_path = ?
360
+ ORDER BY detected_at DESC LIMIT 10
361
+ `).all(filePath) as Array<Record<string, unknown>>;
362
+
363
+ if (conflicts.length === 0) {
364
+ return text(`No conflicts detected for "${filePath}". Conflicts are detected when multiple developers modify the same file within the lookback window. Try: ${p('team_conflicts')} { days: 30 } to check for conflicts across all files.`);
365
+ }
366
+
367
+ const lines = [
368
+ `## Conflicts: ${filePath}`,
369
+ '',
370
+ ];
371
+
372
+ for (const c of conflicts) {
373
+ lines.push(`- ${c.developer_a} vs ${c.developer_b} (${c.conflict_type}) - ${c.resolved ? 'resolved' : 'ACTIVE'}`);
374
+ }
375
+
376
+ return text(lines.join('\n'));
377
+ }
378
+
379
+ // General conflict detection
380
+ const conflicts = detectConflicts(db, days);
381
+
382
+ if (conflicts.length === 0) {
383
+ return text(`No concurrent work conflicts detected in the last ${days} days. Conflicts are tracked when multiple developers modify the same files. Try a longer time range: ${p('team_conflicts')} { days: 90 }.`);
384
+ }
385
+
386
+ const lines = [
387
+ `## Work Conflicts (${days} days)`,
388
+ `Detected: ${conflicts.length}`,
389
+ '',
390
+ '| File | Developer A | Developer B | Type |',
391
+ '|------|-----------|-----------|------|',
392
+ ];
393
+
394
+ for (const c of conflicts) {
395
+ lines.push(`| ${c.filePath} | ${c.developerA} | ${c.developerB} | ${c.conflictType} |`);
396
+ }
397
+
398
+ return text(lines.join('\n'));
399
+ }
400
+
@@ -0,0 +1,41 @@
1
+ // Copyright (c) 2026 Massu. All rights reserved.
2
+ // Licensed under BSL 1.1 - see LICENSE file for details.
3
+
4
+ import { getConfig } from './config.ts';
5
+
6
+ export interface ToolDefinition {
7
+ name: string;
8
+ description: string;
9
+ inputSchema: Record<string, unknown>;
10
+ }
11
+
12
+ export interface ToolResult {
13
+ content: { type: 'text'; text: string }[];
14
+ }
15
+
16
+ /** Get the configured tool prefix (e.g., 'massu' or 'myapp') */
17
+ function prefix(): string {
18
+ return getConfig().toolPrefix;
19
+ }
20
+
21
+ /** Prefix a base tool name with the configured tool prefix. */
22
+ export function p(baseName: string): string {
23
+ return `${prefix()}_${baseName}`;
24
+ }
25
+
26
+ /**
27
+ * Strip the configured prefix from a tool name to get the base name.
28
+ * e.g., "massu_sync" -> "sync", "massu_memory_search" -> "memory_search"
29
+ */
30
+ export function stripPrefix(name: string): string {
31
+ const pfx = prefix() + '_';
32
+ if (name.startsWith(pfx)) {
33
+ return name.slice(pfx.length);
34
+ }
35
+ return name;
36
+ }
37
+
38
+ /** Create a text tool result. */
39
+ export function text(content: string): ToolResult {
40
+ return { content: [{ type: 'text', text: content }] };
41
+ }
package/src/tools.ts ADDED
@@ -0,0 +1,111 @@
1
+ // Copyright (c) 2026 Massu. All rights reserved.
2
+ // Licensed under BSL 1.1 - see LICENSE file for details.
3
+
4
+ import type Database from 'better-sqlite3';
5
+ import { getMemoryToolDefinitions, handleMemoryToolCall, isMemoryTool } from './memory-tools.ts';
6
+ import { getMemoryDb } from './memory-db.ts';
7
+ import { getDocsToolDefinitions, handleDocsToolCall, isDocsTool } from './docs-tools.ts';
8
+ import { getObservabilityToolDefinitions, handleObservabilityToolCall, isObservabilityTool } from './observability-tools.ts';
9
+ import { getSentinelToolDefinitions, handleSentinelToolCall, isSentinelTool } from './sentinel-tools.ts';
10
+ import { getAnalyticsToolDefinitions, isAnalyticsTool, handleAnalyticsToolCall } from './analytics.ts';
11
+ import { getCostToolDefinitions, isCostTool, handleCostToolCall } from './cost-tracker.ts';
12
+ import { getPromptToolDefinitions, isPromptTool, handlePromptToolCall } from './prompt-analyzer.ts';
13
+ import { getAuditToolDefinitions, isAuditTool, handleAuditToolCall } from './audit-trail.ts';
14
+ import { getValidationToolDefinitions, isValidationTool, handleValidationToolCall } from './validation-engine.ts';
15
+ import { getAdrToolDefinitions, isAdrTool, handleAdrToolCall } from './adr-generator.ts';
16
+ import { getSecurityToolDefinitions, isSecurityTool, handleSecurityToolCall } from './security-scorer.ts';
17
+ import { getDependencyToolDefinitions, isDependencyTool, handleDependencyToolCall } from './dependency-scorer.ts';
18
+ import { getTeamToolDefinitions, isTeamTool, handleTeamToolCall } from './team-knowledge.ts';
19
+ import { getRegressionToolDefinitions, isRegressionTool, handleRegressionToolCall } from './regression-detector.ts';
20
+ import { getCoreToolDefinitions, isCoreTool, handleCoreToolCall, ensureIndexes } from './core-tools.ts';
21
+ import { getConfig } from './config.ts';
22
+ import { text } from './tool-helpers.ts';
23
+ import type { ToolDefinition, ToolResult } from './tool-helpers.ts';
24
+
25
+ export type { ToolDefinition, ToolResult } from './tool-helpers.ts';
26
+
27
+ /**
28
+ * Run a function with a memoryDb instance, ensuring it is closed after use.
29
+ */
30
+ function withMemoryDb<T>(fn: (db: Database.Database) => T): T {
31
+ const memDb = getMemoryDb();
32
+ try { return fn(memDb); }
33
+ finally { memDb.close(); }
34
+ }
35
+
36
+ /**
37
+ * Get all tool definitions for the MCP server.
38
+ */
39
+ export function getToolDefinitions(): ToolDefinition[] {
40
+ const config = getConfig();
41
+
42
+ return [
43
+ // Memory tools
44
+ ...getMemoryToolDefinitions(),
45
+ // Observability tools
46
+ ...getObservabilityToolDefinitions(),
47
+ // Docs tools
48
+ ...getDocsToolDefinitions(),
49
+ // Sentinel tools (feature registry)
50
+ ...getSentinelToolDefinitions(),
51
+ // Analytics layer (quality trends, cost tracking, prompt analysis)
52
+ ...getAnalyticsToolDefinitions(),
53
+ ...getCostToolDefinitions(),
54
+ ...getPromptToolDefinitions(),
55
+ // Governance layer (audit trail, validation, ADR)
56
+ ...getAuditToolDefinitions(),
57
+ ...getValidationToolDefinitions(),
58
+ ...getAdrToolDefinitions(),
59
+ // Security layer (security scoring, dependency risk)
60
+ ...getSecurityToolDefinitions(),
61
+ ...getDependencyToolDefinitions(),
62
+ // Enterprise layer (team knowledge — cloud-only; regression detection — always)
63
+ ...(config.cloud?.enabled ? getTeamToolDefinitions() : []),
64
+ ...getRegressionToolDefinitions(),
65
+ // Core tools (sync, context, impact, domains, schema, trpc_map, coupling_check)
66
+ ...getCoreToolDefinitions(),
67
+ ];
68
+ }
69
+
70
+ /**
71
+ * Handle a tool call and return the result.
72
+ */
73
+ export function handleToolCall(
74
+ name: string,
75
+ args: Record<string, unknown>,
76
+ dataDb: Database.Database,
77
+ codegraphDb: Database.Database
78
+ ): ToolResult {
79
+ // Ensure indexes are built before any tool call
80
+ ensureIndexes(dataDb, codegraphDb);
81
+
82
+ try {
83
+ // Route to module tools via isTool() matchers + withMemoryDb helper
84
+ if (isMemoryTool(name)) return withMemoryDb(db => handleMemoryToolCall(name, args, db));
85
+ if (isObservabilityTool(name)) return withMemoryDb(db => handleObservabilityToolCall(name, args, db));
86
+ if (isDocsTool(name)) return handleDocsToolCall(name, args);
87
+ if (isSentinelTool(name)) return handleSentinelToolCall(name, args, dataDb);
88
+ if (isAnalyticsTool(name)) return withMemoryDb(db => handleAnalyticsToolCall(name, args, db));
89
+ if (isCostTool(name)) return withMemoryDb(db => handleCostToolCall(name, args, db));
90
+ if (isPromptTool(name)) return withMemoryDb(db => handlePromptToolCall(name, args, db));
91
+ if (isAuditTool(name)) return withMemoryDb(db => handleAuditToolCall(name, args, db));
92
+ if (isValidationTool(name)) return withMemoryDb(db => handleValidationToolCall(name, args, db));
93
+ if (isAdrTool(name)) return withMemoryDb(db => handleAdrToolCall(name, args, db));
94
+ if (isSecurityTool(name)) return withMemoryDb(db => handleSecurityToolCall(name, args, db));
95
+ if (isDependencyTool(name)) return withMemoryDb(db => handleDependencyToolCall(name, args, db));
96
+ if (isTeamTool(name)) {
97
+ if (!getConfig().cloud?.enabled) {
98
+ return text('This tool requires Cloud Team or Enterprise. Configure cloud sync to enable.');
99
+ }
100
+ return withMemoryDb(db => handleTeamToolCall(name, args, db));
101
+ }
102
+ if (isRegressionTool(name)) return withMemoryDb(db => handleRegressionToolCall(name, args, db));
103
+
104
+ // Core tools (sync, context, trpc_map, coupling_check, impact, domains, schema)
105
+ if (isCoreTool(name)) return handleCoreToolCall(name, args, dataDb, codegraphDb);
106
+
107
+ return text(`Unknown tool: ${name}`);
108
+ } catch (error) {
109
+ return text(`Error in ${name}: ${error instanceof Error ? error.message : String(error)}`);
110
+ }
111
+ }