@ezmodo/mcp-server 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +305 -0
- package/config/development.js +20 -0
- package/config/endpoint-map.js +351 -0
- package/config/index.js +34 -0
- package/config/production.js +18 -0
- package/config/staging.js +18 -0
- package/handlers/access.js +141 -0
- package/handlers/activity.js +112 -0
- package/handlers/agents.js +95 -0
- package/handlers/ai-intelligence.js +55 -0
- package/handlers/attachments.js +30 -0
- package/handlers/catalogs.js +169 -0
- package/handlers/components.js +282 -0
- package/handlers/context-manifest.js +1150 -0
- package/handlers/decisions.js +114 -0
- package/handlers/designs.js +118 -0
- package/handlers/documents.js +227 -0
- package/handlers/entities.js +95 -0
- package/handlers/epics.js +190 -0
- package/handlers/facts.js +62 -0
- package/handlers/feature-flags.js +142 -0
- package/handlers/features.js +137 -0
- package/handlers/folders.js +127 -0
- package/handlers/git-context.js +917 -0
- package/handlers/github.js +72 -0
- package/handlers/graph.js +23 -0
- package/handlers/index.js +205 -0
- package/handlers/links.js +156 -0
- package/handlers/milestones.js +131 -0
- package/handlers/organizations.js +14 -0
- package/handlers/projects.js +122 -0
- package/handlers/recurring-tasks.js +33 -0
- package/handlers/tags.js +124 -0
- package/handlers/tasks.js +561 -0
- package/handlers/testing.js +116 -0
- package/handlers/todos.js +43 -0
- package/handlers/watchers.js +54 -0
- package/handlers/work-templates.js +32 -0
- package/index.js +175 -0
- package/lib/active-session.js +86 -0
- package/lib/auto-assign.js +93 -0
- package/lib/autolink.js +176 -0
- package/lib/changed-files.js +22 -0
- package/lib/env.js +45 -0
- package/lib/git-helpers.js +553 -0
- package/lib/git-utils.js +73 -0
- package/lib/http-client.js +164 -0
- package/lib/links-at-create.js +94 -0
- package/lib/local-cache.js +140 -0
- package/lib/logger.js +109 -0
- package/lib/manifest-loader.js +182 -0
- package/lib/manifest-query.js +686 -0
- package/lib/repo-config-dir.js +118 -0
- package/lib/version.js +10 -0
- package/lib/web-url.js +69 -0
- package/lib/worktree-tools.js +950 -0
- package/package.json +62 -0
- package/prompts/ai-workflow-automation.js +96 -0
- package/prompts/index.js +39 -0
- package/prompts/zephly-usage-guide-content.txt +631 -0
- package/prompts/zephly-usage-guide.js +119 -0
- package/tools/access-entity-types.js +28 -0
- package/tools/access.js +152 -0
- package/tools/activity.js +38 -0
- package/tools/agents.js +208 -0
- package/tools/ai-intelligence.js +111 -0
- package/tools/attachments.js +92 -0
- package/tools/catalogs.js +341 -0
- package/tools/components.js +249 -0
- package/tools/context-manifest.js +236 -0
- package/tools/decisions.js +168 -0
- package/tools/designs.js +222 -0
- package/tools/documents.js +287 -0
- package/tools/entities.js +223 -0
- package/tools/epics.js +267 -0
- package/tools/facts.js +70 -0
- package/tools/feature-flags.js +300 -0
- package/tools/features.js +246 -0
- package/tools/folders.js +122 -0
- package/tools/git-context.js +109 -0
- package/tools/github.js +172 -0
- package/tools/graph.js +70 -0
- package/tools/index.js +77 -0
- package/tools/link-params.js +93 -0
- package/tools/linkable-types.js +36 -0
- package/tools/links.js +199 -0
- package/tools/milestones.js +176 -0
- package/tools/organizations.js +23 -0
- package/tools/projects.js +172 -0
- package/tools/recurring-tasks.js +115 -0
- package/tools/tags.js +219 -0
- package/tools/task-item-schema.js +57 -0
- package/tools/task-type.js +33 -0
- package/tools/tasks.js +680 -0
- package/tools/testing.js +344 -0
- package/tools/todos.js +69 -0
- package/tools/watchers.js +81 -0
- package/tools/work-templates.js +96 -0
|
@@ -0,0 +1,686 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context Manifest Query Engine
|
|
3
|
+
* Port of scripts/context-manifest/query.ts for use in the MCP server.
|
|
4
|
+
* Provides search, related files, project overview, and formatting utilities.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// ============================================================================
|
|
8
|
+
// Stop Words
|
|
9
|
+
// ============================================================================
|
|
10
|
+
|
|
11
|
+
const STOP_WORDS = new Set([
|
|
12
|
+
'a', 'an', 'the', 'is', 'are', 'was', 'were', 'be', 'been', 'being',
|
|
13
|
+
'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could',
|
|
14
|
+
'should', 'may', 'might', 'shall', 'can', 'need', 'must',
|
|
15
|
+
'i', 'me', 'my', 'we', 'our', 'you', 'your', 'he', 'she', 'it',
|
|
16
|
+
'they', 'them', 'this', 'that', 'these', 'those', 'what', 'which',
|
|
17
|
+
'who', 'whom', 'where', 'when', 'why', 'how',
|
|
18
|
+
'and', 'but', 'or', 'nor', 'not', 'no', 'so', 'if', 'then',
|
|
19
|
+
'in', 'on', 'at', 'to', 'for', 'of', 'with', 'by', 'from', 'up',
|
|
20
|
+
'about', 'into', 'through', 'during', 'before', 'after', 'above',
|
|
21
|
+
'below', 'between', 'out', 'off', 'over', 'under', 'again',
|
|
22
|
+
'all', 'each', 'every', 'both', 'few', 'more', 'most', 'other',
|
|
23
|
+
'some', 'such', 'only', 'own', 'same', 'than', 'too', 'very',
|
|
24
|
+
'just', 'also', 'new', 'add', 'make', 'get', 'set', 'use',
|
|
25
|
+
'file', 'files', 'code', 'change', 'update', 'create',
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
// ============================================================================
|
|
29
|
+
// getContextForTags
|
|
30
|
+
// ============================================================================
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Returns all manifest entries matching ANY of the given tags.
|
|
34
|
+
* Results sorted by number of matching tags (most relevant first).
|
|
35
|
+
*
|
|
36
|
+
* @param {object} manifest - Parsed context manifest
|
|
37
|
+
* @param {string[]} tags - Tags to search for
|
|
38
|
+
* @returns {Array<{entry: object, score: number}>} Scored entries
|
|
39
|
+
*/
|
|
40
|
+
export function getContextForTags(manifest, tags) {
|
|
41
|
+
if (!tags || tags.length === 0) return [];
|
|
42
|
+
|
|
43
|
+
const lowerTags = tags.map((t) => t.toLowerCase());
|
|
44
|
+
const scored = [];
|
|
45
|
+
|
|
46
|
+
for (const entry of manifest.entries) {
|
|
47
|
+
let score = 0;
|
|
48
|
+
for (const entryTag of entry.tags) {
|
|
49
|
+
const lowerEntryTag = entryTag.toLowerCase();
|
|
50
|
+
for (const searchTag of lowerTags) {
|
|
51
|
+
if (lowerEntryTag === searchTag) {
|
|
52
|
+
score += 2;
|
|
53
|
+
} else if (lowerEntryTag.startsWith(searchTag) || searchTag.startsWith(lowerEntryTag)) {
|
|
54
|
+
score += 1;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (score > 0) {
|
|
60
|
+
scored.push({ entry, score });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return scored.sort((a, b) => b.score - a.score);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ============================================================================
|
|
68
|
+
// getContextForTask
|
|
69
|
+
// ============================================================================
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Extract keywords from a natural language description.
|
|
73
|
+
* Removes stop words, special chars, and deduplicates.
|
|
74
|
+
*/
|
|
75
|
+
function extractKeywords(text) {
|
|
76
|
+
return text
|
|
77
|
+
.toLowerCase()
|
|
78
|
+
.replace(/[^a-z0-9\s\-_]/g, ' ')
|
|
79
|
+
.split(/\s+/)
|
|
80
|
+
.filter((word) => word.length >= 2 && !STOP_WORDS.has(word))
|
|
81
|
+
.filter((word, i, arr) => arr.indexOf(word) === i);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Score a manifest entry against a set of keywords.
|
|
86
|
+
*/
|
|
87
|
+
function scoreEntry(entry, keywords) {
|
|
88
|
+
let score = 0;
|
|
89
|
+
|
|
90
|
+
for (const keyword of keywords) {
|
|
91
|
+
// Tags: highest weight (3 points)
|
|
92
|
+
for (const tag of entry.tags) {
|
|
93
|
+
if (tag.toLowerCase() === keyword) score += 3;
|
|
94
|
+
else if (tag.toLowerCase().includes(keyword)) score += 1;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Domain: high weight (3 points)
|
|
98
|
+
if (entry.domain?.toLowerCase() === keyword) score += 3;
|
|
99
|
+
|
|
100
|
+
// Summary: medium weight (2 points)
|
|
101
|
+
if (entry.summary.toLowerCase().includes(keyword)) score += 2;
|
|
102
|
+
|
|
103
|
+
// Path: low weight (1 point)
|
|
104
|
+
if (entry.path.toLowerCase().includes(keyword)) score += 1;
|
|
105
|
+
|
|
106
|
+
// Exports: medium weight (2 points)
|
|
107
|
+
if (entry.exports) {
|
|
108
|
+
for (const exp of entry.exports) {
|
|
109
|
+
if (exp.toLowerCase().includes(keyword)) {
|
|
110
|
+
score += 2;
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return score;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Given a natural language task description, returns the most relevant manifest entries.
|
|
122
|
+
* Uses keyword matching against tags, summaries, paths, and domains.
|
|
123
|
+
*
|
|
124
|
+
* @param {object} manifest - Parsed context manifest
|
|
125
|
+
* @param {string} taskDescription - Natural language description
|
|
126
|
+
* @param {number} limit - Maximum results to return (default 20)
|
|
127
|
+
* @returns {Array<{entry: object, score: number}>} Scored entries
|
|
128
|
+
*/
|
|
129
|
+
export function getContextForTask(manifest, taskDescription, limit = 20) {
|
|
130
|
+
if (!taskDescription || taskDescription.trim().length === 0) return [];
|
|
131
|
+
|
|
132
|
+
const keywords = extractKeywords(taskDescription);
|
|
133
|
+
if (keywords.length === 0) return [];
|
|
134
|
+
|
|
135
|
+
const scored = [];
|
|
136
|
+
|
|
137
|
+
for (const entry of manifest.entries) {
|
|
138
|
+
const score = scoreEntry(entry, keywords);
|
|
139
|
+
if (score > 0) {
|
|
140
|
+
scored.push({ entry, score });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return scored
|
|
145
|
+
.sort((a, b) => b.score - a.score)
|
|
146
|
+
.slice(0, limit);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ============================================================================
|
|
150
|
+
// getRelatedFiles
|
|
151
|
+
// ============================================================================
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Returns manifest entries related to a given file via the dependency graph.
|
|
155
|
+
* Includes both direct dependencies and reverse dependencies.
|
|
156
|
+
*
|
|
157
|
+
* @param {object} manifest - Parsed context manifest
|
|
158
|
+
* @param {string} filePath - Relative file path from project root
|
|
159
|
+
* @param {number} depth - How many levels deep to traverse (default 1)
|
|
160
|
+
* @param {string} direction - "both", "forward", or "reverse" (default "both")
|
|
161
|
+
* @returns {object[]} Related manifest entries with relationship info
|
|
162
|
+
*/
|
|
163
|
+
export function getRelatedFiles(manifest, filePath, depth = 1, direction = 'both') {
|
|
164
|
+
if (!filePath) return [];
|
|
165
|
+
|
|
166
|
+
// Build lookup maps
|
|
167
|
+
const entryMap = new Map();
|
|
168
|
+
const reverseDeps = new Map();
|
|
169
|
+
|
|
170
|
+
for (const entry of manifest.entries) {
|
|
171
|
+
entryMap.set(entry.path, entry);
|
|
172
|
+
for (const dep of entry.dependencies) {
|
|
173
|
+
if (!reverseDeps.has(dep)) reverseDeps.set(dep, []);
|
|
174
|
+
reverseDeps.get(dep).push(entry.path);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const sourceEntry = entryMap.get(filePath);
|
|
179
|
+
if (!sourceEntry) return [];
|
|
180
|
+
|
|
181
|
+
// BFS to collect related files at each depth level
|
|
182
|
+
const visited = new Set([filePath]);
|
|
183
|
+
let currentLevel = new Set([filePath]);
|
|
184
|
+
|
|
185
|
+
for (let d = 0; d < depth; d++) {
|
|
186
|
+
const nextLevel = new Set();
|
|
187
|
+
|
|
188
|
+
for (const current of currentLevel) {
|
|
189
|
+
const entry = entryMap.get(current);
|
|
190
|
+
if (!entry) continue;
|
|
191
|
+
|
|
192
|
+
// Forward dependencies
|
|
193
|
+
if (direction === 'both' || direction === 'forward') {
|
|
194
|
+
for (const dep of entry.dependencies) {
|
|
195
|
+
if (!visited.has(dep)) {
|
|
196
|
+
visited.add(dep);
|
|
197
|
+
nextLevel.add(dep);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Reverse dependencies
|
|
203
|
+
if (direction === 'both' || direction === 'reverse') {
|
|
204
|
+
const revDeps = reverseDeps.get(current) || [];
|
|
205
|
+
for (const revDep of revDeps) {
|
|
206
|
+
if (!visited.has(revDep)) {
|
|
207
|
+
visited.add(revDep);
|
|
208
|
+
nextLevel.add(revDep);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
currentLevel = nextLevel;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Remove the source file itself
|
|
218
|
+
visited.delete(filePath);
|
|
219
|
+
|
|
220
|
+
// Sort: direct deps first, then reverse deps, then deeper
|
|
221
|
+
const directDeps = new Set(sourceEntry.dependencies);
|
|
222
|
+
const directRevDeps = new Set(reverseDeps.get(filePath) || []);
|
|
223
|
+
|
|
224
|
+
return Array.from(visited)
|
|
225
|
+
.map((p) => entryMap.get(p))
|
|
226
|
+
.filter((e) => e !== undefined)
|
|
227
|
+
.sort((a, b) => {
|
|
228
|
+
const aIsDirect = directDeps.has(a.path) ? 1 : 0;
|
|
229
|
+
const bIsDirect = directDeps.has(b.path) ? 1 : 0;
|
|
230
|
+
if (aIsDirect !== bIsDirect) return bIsDirect - aIsDirect;
|
|
231
|
+
|
|
232
|
+
const aIsReverse = directRevDeps.has(a.path) ? 1 : 0;
|
|
233
|
+
const bIsReverse = directRevDeps.has(b.path) ? 1 : 0;
|
|
234
|
+
if (aIsReverse !== bIsReverse) return bIsReverse - aIsReverse;
|
|
235
|
+
|
|
236
|
+
return a.path.localeCompare(b.path);
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ============================================================================
|
|
241
|
+
// getProjectOverview
|
|
242
|
+
// ============================================================================
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Aggregate manifest metadata into a high-level project overview.
|
|
246
|
+
* Provides domain breakdown, file counts by type/layer, and key entry points.
|
|
247
|
+
*
|
|
248
|
+
* @param {object} manifest - Parsed context manifest
|
|
249
|
+
* @param {object} options - Optional filters
|
|
250
|
+
* @param {string} options.domain - Filter to a specific domain
|
|
251
|
+
* @param {string} options.layer - Filter to a specific layer
|
|
252
|
+
* @returns {object} Project overview
|
|
253
|
+
*/
|
|
254
|
+
export function getProjectOverview(manifest, options = {}) {
|
|
255
|
+
const { domain, layer } = options;
|
|
256
|
+
|
|
257
|
+
let entries = manifest.entries;
|
|
258
|
+
|
|
259
|
+
// Apply filters
|
|
260
|
+
if (domain) {
|
|
261
|
+
entries = entries.filter((e) => e.domain === domain);
|
|
262
|
+
}
|
|
263
|
+
if (layer) {
|
|
264
|
+
entries = entries.filter((e) => e.layer === layer);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Aggregate by type
|
|
268
|
+
const byType = {};
|
|
269
|
+
for (const entry of entries) {
|
|
270
|
+
byType[entry.type] = (byType[entry.type] || 0) + 1;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Aggregate by layer
|
|
274
|
+
const byLayer = {};
|
|
275
|
+
for (const entry of entries) {
|
|
276
|
+
if (entry.layer) {
|
|
277
|
+
byLayer[entry.layer] = (byLayer[entry.layer] || 0) + 1;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Aggregate by domain
|
|
282
|
+
const byDomain = {};
|
|
283
|
+
for (const entry of entries) {
|
|
284
|
+
if (entry.domain) {
|
|
285
|
+
byDomain[entry.domain] = (byDomain[entry.domain] || 0) + 1;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Aggregate by language
|
|
290
|
+
const byLanguage = {};
|
|
291
|
+
for (const entry of entries) {
|
|
292
|
+
if (entry.language) {
|
|
293
|
+
byLanguage[entry.language] = (byLanguage[entry.language] || 0) + 1;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Find key entry points per domain (handlers, services, pages)
|
|
298
|
+
const entryPoints = {};
|
|
299
|
+
const entryPointLayers = new Set(['handler', 'service', 'page', 'route']);
|
|
300
|
+
for (const entry of entries) {
|
|
301
|
+
if (entry.layer && entryPointLayers.has(entry.layer) && entry.domain) {
|
|
302
|
+
if (!entryPoints[entry.domain]) entryPoints[entry.domain] = [];
|
|
303
|
+
entryPoints[entry.domain].push({
|
|
304
|
+
path: entry.path,
|
|
305
|
+
layer: entry.layer,
|
|
306
|
+
summary: entry.summary,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Sort entry points — limit to top 5 per domain
|
|
312
|
+
for (const d of Object.keys(entryPoints)) {
|
|
313
|
+
entryPoints[d] = entryPoints[d].slice(0, 5);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Dependency statistics
|
|
317
|
+
let totalDeps = 0;
|
|
318
|
+
let maxDeps = 0;
|
|
319
|
+
let maxDepsFile = null;
|
|
320
|
+
const reverseDeps = {};
|
|
321
|
+
|
|
322
|
+
for (const entry of entries) {
|
|
323
|
+
totalDeps += entry.dependencies.length;
|
|
324
|
+
if (entry.dependencies.length > maxDeps) {
|
|
325
|
+
maxDeps = entry.dependencies.length;
|
|
326
|
+
maxDepsFile = entry.path;
|
|
327
|
+
}
|
|
328
|
+
for (const dep of entry.dependencies) {
|
|
329
|
+
reverseDeps[dep] = (reverseDeps[dep] || 0) + 1;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Find most-depended-on files
|
|
334
|
+
const mostDependedOn = Object.entries(reverseDeps)
|
|
335
|
+
.sort(([, a], [, b]) => b - a)
|
|
336
|
+
.slice(0, 10)
|
|
337
|
+
.map(([path, count]) => ({ path, dependedOnBy: count }));
|
|
338
|
+
|
|
339
|
+
return {
|
|
340
|
+
totalFiles: entries.length,
|
|
341
|
+
byType,
|
|
342
|
+
byLayer,
|
|
343
|
+
byDomain,
|
|
344
|
+
byLanguage,
|
|
345
|
+
entryPoints,
|
|
346
|
+
dependencyStats: {
|
|
347
|
+
totalRelationships: totalDeps,
|
|
348
|
+
mostDependencies: maxDepsFile ? { path: maxDepsFile, count: maxDeps } : null,
|
|
349
|
+
mostDependedOn,
|
|
350
|
+
},
|
|
351
|
+
...(domain && { filteredByDomain: domain }),
|
|
352
|
+
...(layer && { filteredByLayer: layer }),
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// ============================================================================
|
|
357
|
+
// formatContextForPrompt
|
|
358
|
+
// ============================================================================
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Format manifest entries into a prompt-ready context block.
|
|
362
|
+
*
|
|
363
|
+
* @param {object[]} entries - Manifest entries to format
|
|
364
|
+
* @param {object} options - Formatting options
|
|
365
|
+
* @param {"compact"|"detailed"|"structured"} options.mode - Output format (default: "compact")
|
|
366
|
+
* @param {number} options.maxLength - Max output length in chars (0 = unlimited)
|
|
367
|
+
* @param {"type"|"domain"|"none"} options.groupBy - Grouping strategy (default: "none")
|
|
368
|
+
* @returns {string} Formatted context
|
|
369
|
+
*/
|
|
370
|
+
export function formatContextForPrompt(entries, options = {}) {
|
|
371
|
+
const { mode = 'compact', maxLength = 0, groupBy = 'none' } = options;
|
|
372
|
+
|
|
373
|
+
if (entries.length === 0) return 'No relevant context found.';
|
|
374
|
+
|
|
375
|
+
let output;
|
|
376
|
+
|
|
377
|
+
if (mode === 'structured') {
|
|
378
|
+
output = JSON.stringify(
|
|
379
|
+
entries.map((e) => ({
|
|
380
|
+
path: e.path,
|
|
381
|
+
type: e.type,
|
|
382
|
+
tags: e.tags,
|
|
383
|
+
summary: e.summary,
|
|
384
|
+
dependencies: e.dependencies,
|
|
385
|
+
...(e.layer && { layer: e.layer }),
|
|
386
|
+
...(e.domain && { domain: e.domain }),
|
|
387
|
+
...(e.exports && { exports: e.exports }),
|
|
388
|
+
})),
|
|
389
|
+
null,
|
|
390
|
+
2
|
|
391
|
+
);
|
|
392
|
+
} else if (groupBy !== 'none') {
|
|
393
|
+
output = formatGrouped(entries, mode, groupBy);
|
|
394
|
+
} else {
|
|
395
|
+
output = formatFlat(entries, mode);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Apply max length
|
|
399
|
+
if (maxLength > 0 && output.length > maxLength) {
|
|
400
|
+
output = output.substring(0, maxLength - 50) + '\n\n... (truncated, ' + entries.length + ' total entries)';
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return output;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function formatFlat(entries, mode) {
|
|
407
|
+
const lines = [];
|
|
408
|
+
lines.push(`## Relevant Context (${entries.length} files)\n`);
|
|
409
|
+
|
|
410
|
+
for (const entry of entries) {
|
|
411
|
+
if (mode === 'compact') {
|
|
412
|
+
lines.push(`- **${entry.path}**: ${entry.summary}`);
|
|
413
|
+
} else {
|
|
414
|
+
lines.push(`### ${entry.path}`);
|
|
415
|
+
lines.push(`- **Type**: ${entry.type}`);
|
|
416
|
+
lines.push(`- **Summary**: ${entry.summary}`);
|
|
417
|
+
lines.push(`- **Tags**: ${entry.tags.join(', ')}`);
|
|
418
|
+
if (entry.layer) lines.push(`- **Layer**: ${entry.layer}`);
|
|
419
|
+
if (entry.domain) lines.push(`- **Domain**: ${entry.domain}`);
|
|
420
|
+
if (entry.exports && entry.exports.length > 0) {
|
|
421
|
+
lines.push(`- **Exports**: ${entry.exports.slice(0, 10).join(', ')}`);
|
|
422
|
+
}
|
|
423
|
+
if (entry.dependencies.length > 0) {
|
|
424
|
+
const depList = entry.dependencies.slice(0, 5).join(', ');
|
|
425
|
+
const extra = entry.dependencies.length > 5
|
|
426
|
+
? ` (+${entry.dependencies.length - 5} more)` : '';
|
|
427
|
+
lines.push(`- **Dependencies**: ${depList}${extra}`);
|
|
428
|
+
}
|
|
429
|
+
lines.push('');
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return lines.join('\n');
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function formatGrouped(entries, mode, groupBy) {
|
|
437
|
+
const groups = new Map();
|
|
438
|
+
|
|
439
|
+
for (const entry of entries) {
|
|
440
|
+
const key = groupBy === 'type' ? entry.type : (entry.domain || 'other');
|
|
441
|
+
if (!groups.has(key)) groups.set(key, []);
|
|
442
|
+
groups.get(key).push(entry);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const lines = [];
|
|
446
|
+
lines.push(`## Relevant Context (${entries.length} files)\n`);
|
|
447
|
+
|
|
448
|
+
for (const [group, groupEntries] of groups) {
|
|
449
|
+
lines.push(`### ${group} (${groupEntries.length} files)\n`);
|
|
450
|
+
|
|
451
|
+
for (const entry of groupEntries) {
|
|
452
|
+
if (mode === 'compact') {
|
|
453
|
+
lines.push(`- **${entry.path}**: ${entry.summary}`);
|
|
454
|
+
} else {
|
|
455
|
+
lines.push(`- **${entry.path}**`);
|
|
456
|
+
lines.push(` - Summary: ${entry.summary}`);
|
|
457
|
+
lines.push(` - Tags: ${entry.tags.join(', ')}`);
|
|
458
|
+
if (entry.exports && entry.exports.length > 0) {
|
|
459
|
+
lines.push(` - Exports: ${entry.exports.slice(0, 10).join(', ')}`);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
lines.push('');
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
return lines.join('\n');
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// ============================================================================
|
|
471
|
+
// getCriticalFiles
|
|
472
|
+
// ============================================================================
|
|
473
|
+
|
|
474
|
+
/** Default directories that indicate critical/core files */
|
|
475
|
+
const KEY_DIRECTORIES = [
|
|
476
|
+
'services/',
|
|
477
|
+
'handlers/',
|
|
478
|
+
'core/',
|
|
479
|
+
'middleware/',
|
|
480
|
+
'contexts/',
|
|
481
|
+
'internal/core/',
|
|
482
|
+
'internal/api/handlers/',
|
|
483
|
+
'internal/api/middleware/',
|
|
484
|
+
];
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Identify critical files that would benefit from LLM-enriched summaries.
|
|
488
|
+
* A file is critical if it has high inbound dependencies, spans multiple
|
|
489
|
+
* domains, or lives in a key directory.
|
|
490
|
+
*
|
|
491
|
+
* @param {object} manifest - Parsed context manifest
|
|
492
|
+
* @param {object} options
|
|
493
|
+
* @param {number} options.threshold - Min inbound deps to qualify (default: 5)
|
|
494
|
+
* @param {number} options.limit - Max files to return (default: 50)
|
|
495
|
+
* @param {boolean} options.unenrichedOnly - Only return files not yet
|
|
496
|
+
* enriched (default: true)
|
|
497
|
+
* @returns {object[]} Critical file entries with scores and signals
|
|
498
|
+
*/
|
|
499
|
+
export function getCriticalFiles(manifest, options = {}) {
|
|
500
|
+
const {
|
|
501
|
+
threshold = 5,
|
|
502
|
+
limit = 50,
|
|
503
|
+
unenrichedOnly = true,
|
|
504
|
+
} = options;
|
|
505
|
+
|
|
506
|
+
// Build inbound dependency counts
|
|
507
|
+
const inboundCounts = new Map();
|
|
508
|
+
for (const entry of manifest.entries) {
|
|
509
|
+
for (const dep of entry.dependencies) {
|
|
510
|
+
inboundCounts.set(dep, (inboundCounts.get(dep) || 0) + 1);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Build domain map — count domains each file touches via its deps
|
|
515
|
+
const entryMap = new Map();
|
|
516
|
+
for (const entry of manifest.entries) {
|
|
517
|
+
entryMap.set(entry.path, entry);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const results = [];
|
|
521
|
+
|
|
522
|
+
for (const entry of manifest.entries) {
|
|
523
|
+
// Skip already-enriched files if requested
|
|
524
|
+
if (unenrichedOnly && entry.enrichment_method === 'llm') continue;
|
|
525
|
+
|
|
526
|
+
// Skip non-source files (no point enriching configs, assets, etc.)
|
|
527
|
+
if (entry.type !== 'source') continue;
|
|
528
|
+
|
|
529
|
+
const signals = [];
|
|
530
|
+
let rawScore = 0;
|
|
531
|
+
|
|
532
|
+
// Signal 1: High inbound dependency count
|
|
533
|
+
const inbound = inboundCounts.get(entry.path) || 0;
|
|
534
|
+
if (inbound >= threshold) {
|
|
535
|
+
signals.push(`high_dependency_count:${inbound}`);
|
|
536
|
+
rawScore += Math.min(inbound / 20, 1);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Signal 2: Multi-domain (cross-cutting)
|
|
540
|
+
const domains = new Set();
|
|
541
|
+
if (entry.domain) domains.add(entry.domain);
|
|
542
|
+
for (const dep of entry.dependencies) {
|
|
543
|
+
const depEntry = entryMap.get(dep);
|
|
544
|
+
if (depEntry?.domain) domains.add(depEntry.domain);
|
|
545
|
+
}
|
|
546
|
+
if (domains.size >= 3) {
|
|
547
|
+
signals.push(`multi_domain:${domains.size}`);
|
|
548
|
+
rawScore += 0.3;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// Signal 3: Key directory
|
|
552
|
+
for (const dir of KEY_DIRECTORIES) {
|
|
553
|
+
if (entry.path.includes(dir)) {
|
|
554
|
+
signals.push(`key_directory:${dir.replace(/\/$/, '')}`);
|
|
555
|
+
rawScore += 0.2;
|
|
556
|
+
break;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
if (signals.length === 0) continue;
|
|
561
|
+
|
|
562
|
+
results.push({
|
|
563
|
+
path: entry.path,
|
|
564
|
+
score: Math.min(rawScore, 1),
|
|
565
|
+
signals,
|
|
566
|
+
currentSummary: entry.summary,
|
|
567
|
+
enrichmentMethod: entry.enrichment_method || 'static',
|
|
568
|
+
domain: entry.domain || null,
|
|
569
|
+
layer: entry.layer || null,
|
|
570
|
+
language: entry.language || null,
|
|
571
|
+
inboundDeps: inbound,
|
|
572
|
+
dependencies: entry.dependencies,
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
return results
|
|
577
|
+
.sort((a, b) => b.score - a.score)
|
|
578
|
+
.slice(0, limit);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Update manifest entries with new enriched summaries.
|
|
583
|
+
* Modifies the manifest in-place (caller must save to disk).
|
|
584
|
+
*
|
|
585
|
+
* @param {object} manifest - Parsed context manifest (will be mutated)
|
|
586
|
+
* @param {Array<{path: string, summary: string}>} updates - Entries to update
|
|
587
|
+
* @returns {object} Summary of what was updated
|
|
588
|
+
*/
|
|
589
|
+
export function updateManifestEntries(manifest, updates) {
|
|
590
|
+
const entryMap = new Map();
|
|
591
|
+
for (const entry of manifest.entries) {
|
|
592
|
+
entryMap.set(entry.path, entry);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
const updated = [];
|
|
596
|
+
const notFound = [];
|
|
597
|
+
const now = new Date().toISOString();
|
|
598
|
+
|
|
599
|
+
for (const update of updates) {
|
|
600
|
+
const entry = entryMap.get(update.path);
|
|
601
|
+
if (!entry) {
|
|
602
|
+
notFound.push(update.path);
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
entry.summary = update.summary;
|
|
607
|
+
entry.enrichment_method = 'llm';
|
|
608
|
+
entry.lastAnalyzed = now;
|
|
609
|
+
|
|
610
|
+
if (update.criticality_score !== undefined) {
|
|
611
|
+
entry.criticality_score = update.criticality_score;
|
|
612
|
+
}
|
|
613
|
+
if (update.criticality_signals) {
|
|
614
|
+
entry.criticality_signals = update.criticality_signals;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
updated.push(update.path);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
return { updated, notFound, timestamp: now };
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// ============================================================================
|
|
624
|
+
// Utility: Suggest similar paths
|
|
625
|
+
// ============================================================================
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* Suggest similar file paths when an exact match isn't found.
|
|
629
|
+
* Uses simple substring matching on path segments.
|
|
630
|
+
*
|
|
631
|
+
* @param {object} manifest - Parsed context manifest
|
|
632
|
+
* @param {string} filePath - Path that wasn't found
|
|
633
|
+
* @param {number} limit - Max suggestions (default 5)
|
|
634
|
+
* @returns {string[]} Similar paths
|
|
635
|
+
*/
|
|
636
|
+
export function suggestSimilarPaths(manifest, filePath, limit = 5) {
|
|
637
|
+
const segments = filePath.toLowerCase().split('/').filter(Boolean);
|
|
638
|
+
const basename = segments[segments.length - 1] || '';
|
|
639
|
+
|
|
640
|
+
const scored = [];
|
|
641
|
+
|
|
642
|
+
for (const entry of manifest.entries) {
|
|
643
|
+
let score = 0;
|
|
644
|
+
const entryLower = entry.path.toLowerCase();
|
|
645
|
+
|
|
646
|
+
// Exact basename match
|
|
647
|
+
if (entryLower.endsWith(basename)) score += 3;
|
|
648
|
+
|
|
649
|
+
// Segment overlap
|
|
650
|
+
for (const seg of segments) {
|
|
651
|
+
if (entryLower.includes(seg)) score += 1;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
if (score > 0) {
|
|
655
|
+
scored.push({ path: entry.path, score });
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
return scored
|
|
660
|
+
.sort((a, b) => b.score - a.score)
|
|
661
|
+
.slice(0, limit)
|
|
662
|
+
.map((s) => s.path);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// ============================================================================
|
|
666
|
+
// Utility: Create _meta field
|
|
667
|
+
// ============================================================================
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* Create a _meta field for tool responses.
|
|
671
|
+
*
|
|
672
|
+
* @param {object} manifest - Parsed manifest (for version info)
|
|
673
|
+
* @param {number} resultCount - Number of results returned
|
|
674
|
+
* @param {number} startTime - process.hrtime.bigint() start
|
|
675
|
+
* @returns {object} Meta object
|
|
676
|
+
*/
|
|
677
|
+
export function createMeta(manifest, resultCount, startTime) {
|
|
678
|
+
const elapsed = Number(process.hrtime.bigint() - startTime) / 1_000_000;
|
|
679
|
+
return {
|
|
680
|
+
resultCount,
|
|
681
|
+
durationMs: Math.round(elapsed * 100) / 100,
|
|
682
|
+
manifestVersion: manifest?.metadata?.version || null,
|
|
683
|
+
manifestGeneratedAt: manifest?.metadata?.generatedAt || null,
|
|
684
|
+
totalManifestEntries: manifest?.metadata?.entryCount || 0,
|
|
685
|
+
};
|
|
686
|
+
}
|