@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,1150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context Manifest Handlers
|
|
3
|
+
* Handler functions for context manifest MCP tools.
|
|
4
|
+
* Supports both local .context/manifest.json and remote Go API fallback.
|
|
5
|
+
*
|
|
6
|
+
* `getContext` is the unified handler replacing searchProjectContext,
|
|
7
|
+
* getRelatedFilesHandler, getProjectOverviewHandler, getCriticalFilesHandler,
|
|
8
|
+
* queryProjectGraph, analyzeImpact, getGraphStats, and analyzeProjectOrganization.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { exec } from 'child_process';
|
|
12
|
+
import { promisify } from 'util';
|
|
13
|
+
import { loadManifest, reloadManifest, saveManifest, getManifestSource } from '../lib/manifest-loader.js';
|
|
14
|
+
import { callZephlyAPI } from '../lib/http-client.js';
|
|
15
|
+
import {
|
|
16
|
+
getContextForTags,
|
|
17
|
+
getContextForTask,
|
|
18
|
+
getRelatedFiles,
|
|
19
|
+
getProjectOverview,
|
|
20
|
+
formatContextForPrompt,
|
|
21
|
+
suggestSimilarPaths,
|
|
22
|
+
getCriticalFiles,
|
|
23
|
+
updateManifestEntries,
|
|
24
|
+
createMeta,
|
|
25
|
+
} from '../lib/manifest-query.js';
|
|
26
|
+
|
|
27
|
+
const execAsync = promisify(exec);
|
|
28
|
+
|
|
29
|
+
// Max facts to include in context responses (pinned first, then newest)
|
|
30
|
+
const MAX_CONTEXT_FACTS = 50;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Fetch project facts for context inclusion.
|
|
34
|
+
* Returns up to MAX_CONTEXT_FACTS entries (pinned first), or empty array on error.
|
|
35
|
+
*/
|
|
36
|
+
async function fetchProjectFacts(projectId) {
|
|
37
|
+
try {
|
|
38
|
+
const result = await callZephlyAPI('mcpListFacts', { projectId });
|
|
39
|
+
const facts = result.facts || [];
|
|
40
|
+
return facts.slice(0, MAX_CONTEXT_FACTS);
|
|
41
|
+
} catch {
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ============================================================================
|
|
47
|
+
// get_context — unified context query handler
|
|
48
|
+
// ============================================================================
|
|
49
|
+
|
|
50
|
+
export async function getContext(args) {
|
|
51
|
+
const { entityType, query } = args;
|
|
52
|
+
|
|
53
|
+
// Mode 1: keyword search
|
|
54
|
+
if (query) {
|
|
55
|
+
return handleSearch(args);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Mode 2: file entity — dependency/related files
|
|
59
|
+
if (entityType === 'file') {
|
|
60
|
+
return handleFileContext(args);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Mode 3: project-level context (overview, critical_files, graph_stats, organization)
|
|
64
|
+
// Mode 4: any entity with graph/impact includes
|
|
65
|
+
return handleEntityContext(args);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ============================================================================
|
|
69
|
+
// Search mode — replaces search_project_context
|
|
70
|
+
// ============================================================================
|
|
71
|
+
|
|
72
|
+
async function handleSearch(args) {
|
|
73
|
+
const startTime = process.hrtime.bigint();
|
|
74
|
+
const {
|
|
75
|
+
projectId,
|
|
76
|
+
query,
|
|
77
|
+
tags,
|
|
78
|
+
limit: maxResults = 20,
|
|
79
|
+
format = 'compact',
|
|
80
|
+
} = args;
|
|
81
|
+
|
|
82
|
+
// Determine search mode from params
|
|
83
|
+
const mode = tags && tags.length > 0 ? 'tags' : 'task';
|
|
84
|
+
|
|
85
|
+
// Fetch facts in parallel with search
|
|
86
|
+
const factsPromise = fetchProjectFacts(projectId);
|
|
87
|
+
|
|
88
|
+
const source = await getManifestSource(projectId);
|
|
89
|
+
|
|
90
|
+
// Remote path: delegate to Go API
|
|
91
|
+
if (source.source === 'remote') {
|
|
92
|
+
const apiMode = mode === 'task' ? 'keyword' : mode;
|
|
93
|
+
const result = await callZephlyAPI('mcpSearchManifest', {
|
|
94
|
+
projectId: source.projectId,
|
|
95
|
+
query,
|
|
96
|
+
mode: apiMode,
|
|
97
|
+
tags: tags || [],
|
|
98
|
+
max_results: maxResults,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
let results = result.results || [];
|
|
102
|
+
let totalMatches = result.count || 0;
|
|
103
|
+
|
|
104
|
+
// Semantic fallback: if keyword search returns few results, try vector similarity
|
|
105
|
+
if (results.length < 5 && !tags) {
|
|
106
|
+
try {
|
|
107
|
+
const semanticResult = await callZephlyAPI('mcpSemanticSearchFiles', {
|
|
108
|
+
projectId: source.projectId,
|
|
109
|
+
query,
|
|
110
|
+
limit: maxResults,
|
|
111
|
+
});
|
|
112
|
+
if (semanticResult.files && semanticResult.files.length > 0) {
|
|
113
|
+
// Merge results, deduplicating by path
|
|
114
|
+
const existingPaths = new Set(results.map(r => r.path));
|
|
115
|
+
const newResults = (semanticResult.files || [])
|
|
116
|
+
.filter(f => !existingPaths.has(f.path))
|
|
117
|
+
.map(f => ({
|
|
118
|
+
path: f.path,
|
|
119
|
+
summary: f.summary || '',
|
|
120
|
+
domain: f.domain || null,
|
|
121
|
+
tags: f.tags || [],
|
|
122
|
+
score: f.score || 0,
|
|
123
|
+
_semantic: true,
|
|
124
|
+
}));
|
|
125
|
+
results = [...results, ...newResults].slice(0, maxResults);
|
|
126
|
+
totalMatches = results.length;
|
|
127
|
+
}
|
|
128
|
+
} catch {
|
|
129
|
+
// Semantic search is best-effort — don't fail the whole request
|
|
130
|
+
// It may not be available if embeddings haven't been generated yet
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const facts = await factsPromise;
|
|
135
|
+
return {
|
|
136
|
+
context: results.map(r => `${r.path}: ${r.summary}`).join('\n'),
|
|
137
|
+
totalMatches,
|
|
138
|
+
returnedCount: results.length,
|
|
139
|
+
topResults: results.slice(0, 5),
|
|
140
|
+
facts,
|
|
141
|
+
_source: 'remote',
|
|
142
|
+
_meta: { projectId: source.projectId },
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Local path: existing logic (semantic search not available locally — requires TEI server)
|
|
147
|
+
const manifest = source.manifest;
|
|
148
|
+
const limit = Math.min(Math.max(maxResults, 1), 100);
|
|
149
|
+
let scored;
|
|
150
|
+
|
|
151
|
+
if (mode === 'tags') {
|
|
152
|
+
const searchTags = tags || query.split(/[\s,]+/).filter(Boolean);
|
|
153
|
+
scored = getContextForTags(manifest, searchTags);
|
|
154
|
+
} else {
|
|
155
|
+
scored = getContextForTask(manifest, query, limit);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Apply limit for tag mode (task mode already limits internally)
|
|
159
|
+
if (mode === 'tags') {
|
|
160
|
+
scored = scored.slice(0, limit);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const entries = scored.map((s) => s.entry);
|
|
164
|
+
const formatted = formatContextForPrompt(entries, { mode: format });
|
|
165
|
+
const facts = await factsPromise;
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
context: formatted,
|
|
169
|
+
totalMatches: mode === 'tags'
|
|
170
|
+
? getContextForTags(manifest, tags || query.split(/[\s,]+/).filter(Boolean)).length
|
|
171
|
+
: scored.length,
|
|
172
|
+
returnedCount: entries.length,
|
|
173
|
+
topResults: entries.slice(0, 5).map((e) => ({
|
|
174
|
+
path: e.path,
|
|
175
|
+
score: scored.find((s) => s.entry === e)?.score || 0,
|
|
176
|
+
summary: e.summary,
|
|
177
|
+
domain: e.domain || null,
|
|
178
|
+
tags: e.tags,
|
|
179
|
+
})),
|
|
180
|
+
facts,
|
|
181
|
+
_source: 'local',
|
|
182
|
+
_meta: createMeta(manifest, entries.length, startTime),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ============================================================================
|
|
187
|
+
// File context mode — replaces get_related_files
|
|
188
|
+
// ============================================================================
|
|
189
|
+
|
|
190
|
+
async function handleFileContext(args) {
|
|
191
|
+
const startTime = process.hrtime.bigint();
|
|
192
|
+
const {
|
|
193
|
+
projectId,
|
|
194
|
+
entityId: filePath,
|
|
195
|
+
depth = 2,
|
|
196
|
+
direction = 'both',
|
|
197
|
+
format = 'compact',
|
|
198
|
+
include,
|
|
199
|
+
} = args;
|
|
200
|
+
|
|
201
|
+
// Fetch facts in parallel with file context
|
|
202
|
+
const factsPromise = fetchProjectFacts(projectId);
|
|
203
|
+
|
|
204
|
+
const source = await getManifestSource(projectId);
|
|
205
|
+
|
|
206
|
+
// Build base result from file dependencies
|
|
207
|
+
let result = {};
|
|
208
|
+
|
|
209
|
+
// Remote path
|
|
210
|
+
if (source.source === 'remote') {
|
|
211
|
+
const apiResult = await callZephlyAPI('mcpGetRelatedFiles', {
|
|
212
|
+
projectId: source.projectId,
|
|
213
|
+
filePath,
|
|
214
|
+
depth,
|
|
215
|
+
direction,
|
|
216
|
+
});
|
|
217
|
+
result = {
|
|
218
|
+
context: (apiResult.files || []).map(f => `${f.path} (${f.relationship}, depth ${f.depth})`).join('\n'),
|
|
219
|
+
sourceFile: filePath,
|
|
220
|
+
related: apiResult.files || [],
|
|
221
|
+
stats: {
|
|
222
|
+
dependencies: (apiResult.files || []).filter(f => f.relationship === 'dependency').length,
|
|
223
|
+
dependents: (apiResult.files || []).filter(f => f.relationship === 'dependent').length,
|
|
224
|
+
transitive: 0,
|
|
225
|
+
},
|
|
226
|
+
_source: 'remote',
|
|
227
|
+
_meta: { projectId: source.projectId, count: apiResult.count || 0 },
|
|
228
|
+
};
|
|
229
|
+
} else {
|
|
230
|
+
// Local path: existing logic
|
|
231
|
+
const manifest = source.manifest;
|
|
232
|
+
const related = getRelatedFiles(manifest, filePath, depth, direction);
|
|
233
|
+
|
|
234
|
+
// If no results, suggest similar paths
|
|
235
|
+
if (related.length === 0) {
|
|
236
|
+
const suggestions = suggestSimilarPaths(manifest, filePath);
|
|
237
|
+
if (suggestions.length > 0) {
|
|
238
|
+
return {
|
|
239
|
+
context: `File "${filePath}" not found in the manifest.`,
|
|
240
|
+
related: [],
|
|
241
|
+
suggestions,
|
|
242
|
+
hint: 'The file path was not found. Did you mean one of the suggested paths?',
|
|
243
|
+
_source: 'local',
|
|
244
|
+
_meta: createMeta(manifest, 0, startTime),
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
return {
|
|
248
|
+
context: `File "${filePath}" not found in the manifest and no similar paths were found.`,
|
|
249
|
+
related: [],
|
|
250
|
+
_source: 'local',
|
|
251
|
+
_meta: createMeta(manifest, 0, startTime),
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const formatted = formatContextForPrompt(related, { mode: format });
|
|
256
|
+
|
|
257
|
+
// Classify relationships for the source file
|
|
258
|
+
const sourceEntry = manifest.entries.find((e) => e.path === filePath);
|
|
259
|
+
const directDeps = new Set(sourceEntry?.dependencies || []);
|
|
260
|
+
const reverseDepsMap = new Map();
|
|
261
|
+
for (const entry of manifest.entries) {
|
|
262
|
+
for (const dep of entry.dependencies) {
|
|
263
|
+
if (!reverseDepsMap.has(dep)) reverseDepsMap.set(dep, []);
|
|
264
|
+
reverseDepsMap.get(dep).push(entry.path);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
const directRevDeps = new Set(reverseDepsMap.get(filePath) || []);
|
|
268
|
+
|
|
269
|
+
const classified = related.map((entry) => ({
|
|
270
|
+
path: entry.path,
|
|
271
|
+
relationship: directDeps.has(entry.path) ? 'dependency' :
|
|
272
|
+
directRevDeps.has(entry.path) ? 'dependent' : 'transitive',
|
|
273
|
+
summary: entry.summary,
|
|
274
|
+
domain: entry.domain || null,
|
|
275
|
+
}));
|
|
276
|
+
|
|
277
|
+
result = {
|
|
278
|
+
context: formatted,
|
|
279
|
+
sourceFile: filePath,
|
|
280
|
+
related: classified,
|
|
281
|
+
stats: {
|
|
282
|
+
dependencies: classified.filter((r) => r.relationship === 'dependency').length,
|
|
283
|
+
dependents: classified.filter((r) => r.relationship === 'dependent').length,
|
|
284
|
+
transitive: classified.filter((r) => r.relationship === 'transitive').length,
|
|
285
|
+
},
|
|
286
|
+
_source: 'local',
|
|
287
|
+
_meta: createMeta(manifest, related.length, startTime),
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Add graph/impact overlays if requested
|
|
292
|
+
if (include) {
|
|
293
|
+
if (include.includes('impact')) {
|
|
294
|
+
result.impact = await callZephlyAPI('mcpAnalyzeImpact', {
|
|
295
|
+
projectId,
|
|
296
|
+
filePath,
|
|
297
|
+
depth: args.depth,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
if (include.includes('graph')) {
|
|
301
|
+
result.graph = await callZephlyAPI('mcpQueryProjectGraph', {
|
|
302
|
+
projectId,
|
|
303
|
+
queryType: args.queryType || 'context',
|
|
304
|
+
filePath,
|
|
305
|
+
maxDepth: args.depth,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
result.facts = await factsPromise;
|
|
311
|
+
return result;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// ============================================================================
|
|
315
|
+
// Entity context mode — replaces get_project_overview, get_critical_files,
|
|
316
|
+
// get_graph_stats, analyze_project_organization, query_project_graph,
|
|
317
|
+
// analyze_impact
|
|
318
|
+
// ============================================================================
|
|
319
|
+
|
|
320
|
+
async function handleEntityContext(args) {
|
|
321
|
+
const {
|
|
322
|
+
projectId,
|
|
323
|
+
entityType = 'project',
|
|
324
|
+
entityId,
|
|
325
|
+
include,
|
|
326
|
+
} = args;
|
|
327
|
+
|
|
328
|
+
// Determine default includes based on entityType
|
|
329
|
+
const sections = include || getDefaultIncludes(entityType);
|
|
330
|
+
|
|
331
|
+
const results = {};
|
|
332
|
+
const promises = [];
|
|
333
|
+
|
|
334
|
+
// Always include project facts (lightweight, always useful)
|
|
335
|
+
promises.push(
|
|
336
|
+
fetchProjectFacts(projectId).then(r => { results.facts = r; })
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
// Project-level sections
|
|
340
|
+
if (sections.includes('overview')) {
|
|
341
|
+
promises.push(
|
|
342
|
+
fetchOverview(args).then(r => { results.overview = r; })
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (sections.includes('critical_files')) {
|
|
347
|
+
promises.push(
|
|
348
|
+
fetchCriticalFiles(args).then(r => { results.critical_files = r; })
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (sections.includes('graph_stats')) {
|
|
353
|
+
promises.push(
|
|
354
|
+
callZephlyAPI('mcpGetGraphStats', { projectId })
|
|
355
|
+
.then(r => { results.graph_stats = r; })
|
|
356
|
+
.catch(e => { results.graph_stats = { error: e.message }; })
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (sections.includes('organization')) {
|
|
361
|
+
promises.push(
|
|
362
|
+
callZephlyAPI('mcpAnalyzeProjectOrganization', { projectId })
|
|
363
|
+
.then(r => { results.organization = r; })
|
|
364
|
+
.catch(e => { results.organization = { error: e.message }; })
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Graph query (any entity type)
|
|
369
|
+
if (sections.includes('graph')) {
|
|
370
|
+
const graphParams = {
|
|
371
|
+
projectId,
|
|
372
|
+
queryType: args.queryType || 'context',
|
|
373
|
+
maxDepth: args.depth,
|
|
374
|
+
};
|
|
375
|
+
// Map entityType+entityId to the correct graph param
|
|
376
|
+
if (entityType === 'task') graphParams.taskId = entityId;
|
|
377
|
+
else if (entityType === 'component') graphParams.componentId = entityId;
|
|
378
|
+
else if (entityType === 'file') graphParams.filePath = entityId;
|
|
379
|
+
else if (entityType === 'tag') graphParams.tagId = entityId;
|
|
380
|
+
|
|
381
|
+
promises.push(
|
|
382
|
+
callZephlyAPI('mcpQueryProjectGraph', graphParams)
|
|
383
|
+
.then(r => { results.graph = r; })
|
|
384
|
+
.catch(e => { results.graph = { error: e.message }; })
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Impact analysis (any entity type)
|
|
389
|
+
if (sections.includes('impact')) {
|
|
390
|
+
const impactParams = { projectId, depth: args.depth };
|
|
391
|
+
if (entityType === 'file') impactParams.filePath = entityId;
|
|
392
|
+
else if (entityType === 'component') impactParams.componentId = entityId;
|
|
393
|
+
|
|
394
|
+
promises.push(
|
|
395
|
+
callZephlyAPI('mcpAnalyzeImpact', impactParams)
|
|
396
|
+
.then(r => { results.impact = r; })
|
|
397
|
+
.catch(e => { results.impact = { error: e.message }; })
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
await Promise.all(promises);
|
|
402
|
+
|
|
403
|
+
return {
|
|
404
|
+
entityType,
|
|
405
|
+
entityId: entityId || projectId,
|
|
406
|
+
...results,
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// ============================================================================
|
|
411
|
+
// Helper: fetch overview (local/remote aware)
|
|
412
|
+
// ============================================================================
|
|
413
|
+
|
|
414
|
+
async function fetchOverview(args) {
|
|
415
|
+
const startTime = process.hrtime.bigint();
|
|
416
|
+
const { projectId } = args;
|
|
417
|
+
|
|
418
|
+
const source = await getManifestSource(projectId);
|
|
419
|
+
|
|
420
|
+
if (source.source === 'remote') {
|
|
421
|
+
const params = { projectId: source.projectId };
|
|
422
|
+
const result = await callZephlyAPI('mcpGetManifestOverview', params);
|
|
423
|
+
return { ...result, _source: 'remote', _meta: { projectId: source.projectId } };
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const manifest = source.manifest;
|
|
427
|
+
const overview = getProjectOverview(manifest, {});
|
|
428
|
+
|
|
429
|
+
return {
|
|
430
|
+
...overview,
|
|
431
|
+
manifest: {
|
|
432
|
+
version: manifest.metadata.version,
|
|
433
|
+
generatedAt: manifest.metadata.generatedAt,
|
|
434
|
+
projectName: manifest.metadata.projectName,
|
|
435
|
+
generator: manifest.metadata.generator,
|
|
436
|
+
},
|
|
437
|
+
_source: 'local',
|
|
438
|
+
_meta: createMeta(manifest, overview.totalFiles, startTime),
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// ============================================================================
|
|
443
|
+
// Helper: fetch critical files (local/remote aware)
|
|
444
|
+
// ============================================================================
|
|
445
|
+
|
|
446
|
+
async function fetchCriticalFiles(args) {
|
|
447
|
+
const startTime = process.hrtime.bigint();
|
|
448
|
+
const { projectId, limit = 50 } = args;
|
|
449
|
+
|
|
450
|
+
const source = await getManifestSource(projectId);
|
|
451
|
+
|
|
452
|
+
if (source.source === 'remote') {
|
|
453
|
+
const result = await callZephlyAPI('mcpGetCriticalFiles', {
|
|
454
|
+
projectId: source.projectId,
|
|
455
|
+
limit,
|
|
456
|
+
});
|
|
457
|
+
return {
|
|
458
|
+
criticalFiles: result.files || [],
|
|
459
|
+
totalCritical: result.count || 0,
|
|
460
|
+
_source: 'remote',
|
|
461
|
+
_meta: { projectId: source.projectId },
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const manifest = source.manifest;
|
|
466
|
+
const critical = getCriticalFiles(manifest, {
|
|
467
|
+
threshold: 5,
|
|
468
|
+
limit,
|
|
469
|
+
unenrichedOnly: true,
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
return {
|
|
473
|
+
criticalFiles: critical,
|
|
474
|
+
totalCritical: critical.length,
|
|
475
|
+
_source: 'local',
|
|
476
|
+
_meta: createMeta(manifest, critical.length, startTime),
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// ============================================================================
|
|
481
|
+
// resolve_concepts — maps user-facing terms to code locations
|
|
482
|
+
// ============================================================================
|
|
483
|
+
|
|
484
|
+
export async function resolveConcepts(args) {
|
|
485
|
+
const { projectId, terms } = args;
|
|
486
|
+
|
|
487
|
+
if (!terms || terms.length === 0) {
|
|
488
|
+
throw new Error('At least one term is required');
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const source = await getManifestSource(projectId);
|
|
492
|
+
const results = {};
|
|
493
|
+
|
|
494
|
+
for (const term of terms) {
|
|
495
|
+
const termLower = term.toLowerCase();
|
|
496
|
+
const termResult = {
|
|
497
|
+
term,
|
|
498
|
+
aliasMatches: [],
|
|
499
|
+
contextMatches: [],
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
// Check concept aliases in manifest
|
|
503
|
+
if (source.source === 'local' && source.manifest) {
|
|
504
|
+
const aliases = source.manifest.conceptAliases || [];
|
|
505
|
+
for (const alias of aliases) {
|
|
506
|
+
const allNames = [
|
|
507
|
+
alias.alias.toLowerCase(),
|
|
508
|
+
...(alias.alternateNames || []).map(n => n.toLowerCase()),
|
|
509
|
+
];
|
|
510
|
+
if (allNames.some(name => name.includes(termLower) || termLower.includes(name))) {
|
|
511
|
+
termResult.aliasMatches.push({
|
|
512
|
+
alias: alias.alias,
|
|
513
|
+
targets: alias.targets,
|
|
514
|
+
description: alias.description,
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Also search via get_context for broader matching
|
|
521
|
+
try {
|
|
522
|
+
const contextResult = await handleSearch({
|
|
523
|
+
projectId,
|
|
524
|
+
query: term,
|
|
525
|
+
limit: 5,
|
|
526
|
+
});
|
|
527
|
+
termResult.contextMatches = (contextResult.topResults || []).map(r => ({
|
|
528
|
+
path: r.path,
|
|
529
|
+
summary: r.summary,
|
|
530
|
+
domain: r.domain,
|
|
531
|
+
score: r.score,
|
|
532
|
+
}));
|
|
533
|
+
} catch {
|
|
534
|
+
// Best effort
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
results[term] = termResult;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// Build a flat list of all resolved file paths for convenience
|
|
541
|
+
const allPaths = new Set();
|
|
542
|
+
for (const termResult of Object.values(results)) {
|
|
543
|
+
for (const match of termResult.aliasMatches) {
|
|
544
|
+
match.targets.forEach(t => allPaths.add(t));
|
|
545
|
+
}
|
|
546
|
+
for (const match of termResult.contextMatches) {
|
|
547
|
+
allPaths.add(match.path);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
return {
|
|
552
|
+
resolved: results,
|
|
553
|
+
allPaths: [...allPaths],
|
|
554
|
+
totalPaths: allPaths.size,
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// ============================================================================
|
|
559
|
+
// Helper: default includes per entity type
|
|
560
|
+
// ============================================================================
|
|
561
|
+
|
|
562
|
+
function getDefaultIncludes(entityType) {
|
|
563
|
+
switch (entityType) {
|
|
564
|
+
case 'project':
|
|
565
|
+
return ['overview', 'critical_files', 'graph_stats', 'organization'];
|
|
566
|
+
case 'task':
|
|
567
|
+
case 'epic':
|
|
568
|
+
return ['graph'];
|
|
569
|
+
case 'component':
|
|
570
|
+
return ['graph', 'impact'];
|
|
571
|
+
case 'file':
|
|
572
|
+
return ['dependencies', 'impact'];
|
|
573
|
+
case 'tag':
|
|
574
|
+
return ['graph'];
|
|
575
|
+
default:
|
|
576
|
+
return ['overview'];
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// ============================================================================
|
|
581
|
+
// rebuild_manifest (unchanged)
|
|
582
|
+
// ============================================================================
|
|
583
|
+
|
|
584
|
+
export async function rebuildManifest(args) {
|
|
585
|
+
const startTime = process.hrtime.bigint();
|
|
586
|
+
|
|
587
|
+
const { mode: rawMode = 'incremental', target_paths: targetPaths } = args;
|
|
588
|
+
|
|
589
|
+
// Check if local filesystem is available
|
|
590
|
+
const localManifest = await loadManifest();
|
|
591
|
+
if (!localManifest) {
|
|
592
|
+
// No local filesystem — cannot rebuild remotely
|
|
593
|
+
return {
|
|
594
|
+
success: false,
|
|
595
|
+
error: 'Manifest rebuild requires local filesystem access. ' +
|
|
596
|
+
'This tool is not available in remote/cloud environments. ' +
|
|
597
|
+
'Use GitHub Actions to sync your manifest to the API, or run ' +
|
|
598
|
+
'"npm run manifest:regen" locally.',
|
|
599
|
+
_source: 'local-only',
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// Reject "enriched" mode — LLM enrichment is only available via web UI.
|
|
604
|
+
// Fall back to "full" for backwards compatibility.
|
|
605
|
+
const mode = rawMode === 'enriched' ? 'full' : rawMode;
|
|
606
|
+
|
|
607
|
+
// Get pre-rebuild stats
|
|
608
|
+
const priorCount = localManifest?.metadata?.entryCount || 0;
|
|
609
|
+
|
|
610
|
+
let command;
|
|
611
|
+
if (mode === 'incremental') {
|
|
612
|
+
command = 'npx tsx scripts/context-manifest/update.ts';
|
|
613
|
+
} else {
|
|
614
|
+
command = 'npx tsx scripts/context-manifest/generate.ts';
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
if (targetPaths && targetPaths.length > 0) {
|
|
618
|
+
command += ` -- --paths ${targetPaths.join(',')}`;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
try {
|
|
622
|
+
const { stdout, stderr } = await execAsync(command, {
|
|
623
|
+
cwd: process.cwd(),
|
|
624
|
+
timeout: 300000, // 5 minutes
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
// Reload the manifest after rebuild
|
|
628
|
+
const newManifest = await reloadManifest();
|
|
629
|
+
const newCount = newManifest?.metadata?.entryCount || 0;
|
|
630
|
+
|
|
631
|
+
return {
|
|
632
|
+
success: true,
|
|
633
|
+
mode,
|
|
634
|
+
...(rawMode === 'enriched' && {
|
|
635
|
+
warning: 'LLM enrichment is no longer available via MCP. Fell back to "full" mode. '
|
|
636
|
+
+ 'Use get_critical_files + update_manifest_entries for local AI enrichment instead.',
|
|
637
|
+
}),
|
|
638
|
+
previousEntryCount: priorCount,
|
|
639
|
+
newEntryCount: newCount,
|
|
640
|
+
entriesAdded: Math.max(0, newCount - priorCount),
|
|
641
|
+
entriesRemoved: Math.max(0, priorCount - newCount),
|
|
642
|
+
generatedAt: newManifest?.metadata?.generatedAt,
|
|
643
|
+
output: stdout.trim(),
|
|
644
|
+
...(stderr.trim() && { warnings: stderr.trim() }),
|
|
645
|
+
_source: 'local',
|
|
646
|
+
_meta: createMeta(newManifest, newCount, startTime),
|
|
647
|
+
};
|
|
648
|
+
} catch (err) {
|
|
649
|
+
return {
|
|
650
|
+
success: false,
|
|
651
|
+
mode,
|
|
652
|
+
error: err.message,
|
|
653
|
+
...(err.stderr && { stderr: err.stderr.trim() }),
|
|
654
|
+
_source: 'local',
|
|
655
|
+
_meta: createMeta(localManifest, 0, startTime),
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// ============================================================================
|
|
661
|
+
// update_manifest_entries (unchanged)
|
|
662
|
+
// ============================================================================
|
|
663
|
+
|
|
664
|
+
export async function updateManifestEntriesHandler(args) {
|
|
665
|
+
const startTime = process.hrtime.bigint();
|
|
666
|
+
|
|
667
|
+
const { updates, project_id: projectId } = args;
|
|
668
|
+
|
|
669
|
+
if (!updates || updates.length === 0) {
|
|
670
|
+
throw new Error('No updates provided. Pass an array of {path, summary} objects.');
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
const source = await getManifestSource(projectId);
|
|
674
|
+
|
|
675
|
+
// Remote path
|
|
676
|
+
if (source.source === 'remote') {
|
|
677
|
+
const result = await callZephlyAPI('mcpUpdateManifestEntries', {
|
|
678
|
+
projectId: source.projectId,
|
|
679
|
+
updates,
|
|
680
|
+
});
|
|
681
|
+
return {
|
|
682
|
+
success: true,
|
|
683
|
+
updated: updates.map(u => u.path),
|
|
684
|
+
updatedCount: result.updated || updates.length,
|
|
685
|
+
notFound: [],
|
|
686
|
+
notFoundCount: 0,
|
|
687
|
+
_source: 'remote',
|
|
688
|
+
_meta: { projectId: source.projectId },
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// Local path: existing logic
|
|
693
|
+
const manifest = source.manifest;
|
|
694
|
+
const result = updateManifestEntries(manifest, updates);
|
|
695
|
+
|
|
696
|
+
// Save to disk
|
|
697
|
+
const saved = await saveManifest();
|
|
698
|
+
|
|
699
|
+
return {
|
|
700
|
+
success: saved,
|
|
701
|
+
updated: result.updated,
|
|
702
|
+
notFound: result.notFound,
|
|
703
|
+
updatedCount: result.updated.length,
|
|
704
|
+
notFoundCount: result.notFound.length,
|
|
705
|
+
timestamp: result.timestamp,
|
|
706
|
+
...((!saved) && {
|
|
707
|
+
warning: 'Changes applied to in-memory manifest but failed to save to disk.',
|
|
708
|
+
}),
|
|
709
|
+
_source: 'local',
|
|
710
|
+
_meta: createMeta(manifest, result.updated.length, startTime),
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// ============================================================================
|
|
715
|
+
// get_manifest_schema — returns the manifest JSON schema with docs and examples
|
|
716
|
+
// ============================================================================
|
|
717
|
+
|
|
718
|
+
export async function getManifestSchema(args) {
|
|
719
|
+
const { includeExamples = true, language } = args;
|
|
720
|
+
|
|
721
|
+
const schema = {
|
|
722
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
723
|
+
title: 'ContextManifest',
|
|
724
|
+
description:
|
|
725
|
+
'A structured index of all files in a project, their roles, dependencies, ' +
|
|
726
|
+
'and AI-useful summaries. Used by Zephly to power context queries, impact ' +
|
|
727
|
+
'analysis, and knowledge graph features.',
|
|
728
|
+
|
|
729
|
+
definitions: {
|
|
730
|
+
ManifestMetadata: {
|
|
731
|
+
type: 'object',
|
|
732
|
+
description: 'Metadata about the manifest itself',
|
|
733
|
+
required: ['version', 'generatedAt', 'projectName', 'entryCount', 'generator'],
|
|
734
|
+
properties: {
|
|
735
|
+
version: {
|
|
736
|
+
type: 'string',
|
|
737
|
+
description: 'Schema version (semver). Use "1.0.0"',
|
|
738
|
+
example: '1.0.0',
|
|
739
|
+
},
|
|
740
|
+
generatedAt: {
|
|
741
|
+
type: 'string',
|
|
742
|
+
description: 'ISO 8601 timestamp of when the manifest was generated',
|
|
743
|
+
example: '2026-03-22T12:00:00.000Z',
|
|
744
|
+
},
|
|
745
|
+
projectName: {
|
|
746
|
+
type: 'string',
|
|
747
|
+
description: 'Human-readable project name',
|
|
748
|
+
example: 'my-project',
|
|
749
|
+
},
|
|
750
|
+
entryCount: {
|
|
751
|
+
type: 'integer',
|
|
752
|
+
description: 'Total number of entries in the manifest. Must match entries.length',
|
|
753
|
+
},
|
|
754
|
+
generator: {
|
|
755
|
+
type: 'string',
|
|
756
|
+
description: 'Identifier for the tool that generated this manifest',
|
|
757
|
+
example: 'ai-agent-manifest-generator',
|
|
758
|
+
},
|
|
759
|
+
},
|
|
760
|
+
},
|
|
761
|
+
ManifestEntry: {
|
|
762
|
+
type: 'object',
|
|
763
|
+
description: 'A single file entry in the manifest',
|
|
764
|
+
required: ['path', 'type', 'tags', 'summary', 'dependencies', 'lastAnalyzed'],
|
|
765
|
+
properties: {
|
|
766
|
+
path: {
|
|
767
|
+
type: 'string',
|
|
768
|
+
description:
|
|
769
|
+
'Relative file path from project root. Use forward slashes. ' +
|
|
770
|
+
'Example: "src/services/auth.ts"',
|
|
771
|
+
},
|
|
772
|
+
type: {
|
|
773
|
+
type: 'string',
|
|
774
|
+
enum: ['source', 'config', 'documentation', 'architecture', 'test', 'infrastructure', 'script', 'style', 'asset', 'ci-cd'],
|
|
775
|
+
description:
|
|
776
|
+
'Broad category of the file. Most code files are "source". ' +
|
|
777
|
+
'Test files are "test". Config files (package.json, tsconfig, etc.) are "config".',
|
|
778
|
+
},
|
|
779
|
+
tags: {
|
|
780
|
+
type: 'array',
|
|
781
|
+
items: { type: 'string' },
|
|
782
|
+
description:
|
|
783
|
+
'Freeform tags for categorization. Include at minimum: the type, ' +
|
|
784
|
+
'the workspace/directory (e.g., "api", "frontend"), and the business domain ' +
|
|
785
|
+
'(e.g., "auth", "billing", "tasks"). Example: ["source", "handler", "auth", "api"]',
|
|
786
|
+
},
|
|
787
|
+
summary: {
|
|
788
|
+
type: 'string',
|
|
789
|
+
description:
|
|
790
|
+
'Concise, AI-useful description of what the file contains and why it matters. ' +
|
|
791
|
+
'1-3 sentences. Should capture intent, not just contents. ' +
|
|
792
|
+
'Example: "HTTP handler for user authentication. Implements login, signup, ' +
|
|
793
|
+
'password reset, and email verification endpoints."',
|
|
794
|
+
},
|
|
795
|
+
dependencies: {
|
|
796
|
+
type: 'array',
|
|
797
|
+
items: { type: 'string' },
|
|
798
|
+
description:
|
|
799
|
+
'Relative paths of OTHER files in this manifest that this file imports/depends on. ' +
|
|
800
|
+
'Only include project-internal dependencies, not external libraries. ' +
|
|
801
|
+
'Each path must match another entry\'s path field exactly.',
|
|
802
|
+
},
|
|
803
|
+
lastAnalyzed: {
|
|
804
|
+
type: 'string',
|
|
805
|
+
description: 'ISO 8601 timestamp of when this entry was analyzed',
|
|
806
|
+
},
|
|
807
|
+
language: {
|
|
808
|
+
type: 'string',
|
|
809
|
+
description:
|
|
810
|
+
'Programming language. Common values: "typescript", "javascript", "go", "python", ' +
|
|
811
|
+
'"rust", "java", "kotlin", "dart", "csharp", "swift", "php", "ruby", "cpp", "c", ' +
|
|
812
|
+
'"scala", "elixir", "haskell", "lua", "zig", "ocaml"',
|
|
813
|
+
},
|
|
814
|
+
layer: {
|
|
815
|
+
type: 'string',
|
|
816
|
+
enum: ['handler', 'service', 'component', 'hook', 'util', 'type', 'config', 'middleware', 'model', 'route', 'context', 'page', 'migration', 'command'],
|
|
817
|
+
description:
|
|
818
|
+
'Architectural layer. "handler" = HTTP handlers/controllers, "service" = business logic, ' +
|
|
819
|
+
'"component" = UI components, "model" = data models, "middleware" = request/response middleware, ' +
|
|
820
|
+
'"type" = type definitions/interfaces, "util" = utilities/helpers, "route" = routing config, ' +
|
|
821
|
+
'"page" = page components, "hook" = state management, "migration" = database migrations',
|
|
822
|
+
},
|
|
823
|
+
domain: {
|
|
824
|
+
type: 'string',
|
|
825
|
+
description:
|
|
826
|
+
'Business domain area. Common values: "auth", "billing", "tasks", "projects", "users", ' +
|
|
827
|
+
'"notifications", "analytics", "search", "settings", "onboarding". ' +
|
|
828
|
+
'Use whatever domain names make sense for your project.',
|
|
829
|
+
},
|
|
830
|
+
exports: {
|
|
831
|
+
type: 'array',
|
|
832
|
+
items: { type: 'string' },
|
|
833
|
+
description:
|
|
834
|
+
'Key exports from this file — function names, class names, type names. ' +
|
|
835
|
+
'Only include public/exported symbols, not internal ones. Max 50.',
|
|
836
|
+
},
|
|
837
|
+
enrichment_method: {
|
|
838
|
+
type: 'string',
|
|
839
|
+
enum: ['static', 'llm'],
|
|
840
|
+
description:
|
|
841
|
+
'How the summary was generated. Use "static" for rule-based/regex analysis, ' +
|
|
842
|
+
'"llm" for AI-generated summaries. Default: "static"',
|
|
843
|
+
default: 'static',
|
|
844
|
+
},
|
|
845
|
+
criticality_score: {
|
|
846
|
+
type: 'number',
|
|
847
|
+
description:
|
|
848
|
+
'Computed importance score from 0 to 1. Higher = more critical. ' +
|
|
849
|
+
'Based on: inbound dependency count, cross-domain impact, key directory location.',
|
|
850
|
+
},
|
|
851
|
+
criticality_signals: {
|
|
852
|
+
type: 'array',
|
|
853
|
+
items: { type: 'string' },
|
|
854
|
+
description:
|
|
855
|
+
'Signals that contributed to the criticality score. ' +
|
|
856
|
+
'Example: ["high_dependency_count:15", "key_directory:services/", "multi_domain:3"]',
|
|
857
|
+
},
|
|
858
|
+
},
|
|
859
|
+
},
|
|
860
|
+
},
|
|
861
|
+
|
|
862
|
+
type: 'object',
|
|
863
|
+
required: ['metadata', 'entries'],
|
|
864
|
+
properties: {
|
|
865
|
+
metadata: { $ref: '#/definitions/ManifestMetadata' },
|
|
866
|
+
entries: {
|
|
867
|
+
type: 'array',
|
|
868
|
+
items: { $ref: '#/definitions/ManifestEntry' },
|
|
869
|
+
},
|
|
870
|
+
},
|
|
871
|
+
};
|
|
872
|
+
|
|
873
|
+
const result = { schema };
|
|
874
|
+
|
|
875
|
+
// Add examples
|
|
876
|
+
if (includeExamples) {
|
|
877
|
+
const lang = language || 'generic';
|
|
878
|
+
result.examples = generateExamples(lang);
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
// Add agent prompt template
|
|
882
|
+
result.agentPrompt = generateAgentPrompt(language);
|
|
883
|
+
|
|
884
|
+
// Add supported languages info
|
|
885
|
+
result.supportedLanguages = {
|
|
886
|
+
native: [
|
|
887
|
+
'typescript', 'javascript', 'go', 'python', 'rust', 'java', 'kotlin',
|
|
888
|
+
'dart', 'csharp', 'swift', 'php', 'ruby', 'cpp', 'c',
|
|
889
|
+
],
|
|
890
|
+
note:
|
|
891
|
+
'These languages are parsed automatically by the built-in manifest generator. ' +
|
|
892
|
+
'For any other language, use this schema to generate a valid manifest yourself.',
|
|
893
|
+
};
|
|
894
|
+
|
|
895
|
+
return result;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
function generateExamples(language) {
|
|
899
|
+
// Base examples that work for any language
|
|
900
|
+
const examples = {
|
|
901
|
+
handler: {
|
|
902
|
+
path: 'src/handlers/auth.ext',
|
|
903
|
+
type: 'source',
|
|
904
|
+
tags: ['source', 'handler', 'auth'],
|
|
905
|
+
summary: 'HTTP handler for authentication. Implements login, signup, and password reset endpoints.',
|
|
906
|
+
dependencies: ['src/services/auth_service.ext', 'src/models/user.ext', 'src/middleware/jwt.ext'],
|
|
907
|
+
lastAnalyzed: new Date().toISOString(),
|
|
908
|
+
language: language === 'generic' ? 'your-language' : language,
|
|
909
|
+
layer: 'handler',
|
|
910
|
+
domain: 'auth',
|
|
911
|
+
exports: ['handleLogin', 'handleSignup', 'handlePasswordReset'],
|
|
912
|
+
enrichment_method: 'static',
|
|
913
|
+
},
|
|
914
|
+
service: {
|
|
915
|
+
path: 'src/services/auth_service.ext',
|
|
916
|
+
type: 'source',
|
|
917
|
+
tags: ['source', 'service', 'auth'],
|
|
918
|
+
summary: 'Core authentication business logic. Manages user sessions, password hashing, and token generation.',
|
|
919
|
+
dependencies: ['src/models/user.ext', 'src/db/repository.ext'],
|
|
920
|
+
lastAnalyzed: new Date().toISOString(),
|
|
921
|
+
language: language === 'generic' ? 'your-language' : language,
|
|
922
|
+
layer: 'service',
|
|
923
|
+
domain: 'auth',
|
|
924
|
+
exports: ['AuthService', 'createSession', 'validateToken'],
|
|
925
|
+
enrichment_method: 'static',
|
|
926
|
+
},
|
|
927
|
+
model: {
|
|
928
|
+
path: 'src/models/user.ext',
|
|
929
|
+
type: 'source',
|
|
930
|
+
tags: ['source', 'model', 'auth'],
|
|
931
|
+
summary: 'User data model. Defines user entity with profile fields, authentication methods, and role permissions.',
|
|
932
|
+
dependencies: [],
|
|
933
|
+
lastAnalyzed: new Date().toISOString(),
|
|
934
|
+
language: language === 'generic' ? 'your-language' : language,
|
|
935
|
+
layer: 'model',
|
|
936
|
+
domain: 'auth',
|
|
937
|
+
exports: ['User', 'UserRole'],
|
|
938
|
+
enrichment_method: 'static',
|
|
939
|
+
},
|
|
940
|
+
test: {
|
|
941
|
+
path: 'tests/auth_test.ext',
|
|
942
|
+
type: 'test',
|
|
943
|
+
tags: ['test', 'unit-test', 'auth'],
|
|
944
|
+
summary: 'Unit tests for authentication service. Covers login flow, token validation, and error cases.',
|
|
945
|
+
dependencies: ['src/services/auth_service.ext', 'src/models/user.ext'],
|
|
946
|
+
lastAnalyzed: new Date().toISOString(),
|
|
947
|
+
language: language === 'generic' ? 'your-language' : language,
|
|
948
|
+
domain: 'auth',
|
|
949
|
+
enrichment_method: 'static',
|
|
950
|
+
},
|
|
951
|
+
config: {
|
|
952
|
+
path: 'config/app.ext',
|
|
953
|
+
type: 'config',
|
|
954
|
+
tags: ['config'],
|
|
955
|
+
summary: 'Application configuration. Database connection, server port, feature flags.',
|
|
956
|
+
dependencies: [],
|
|
957
|
+
lastAnalyzed: new Date().toISOString(),
|
|
958
|
+
enrichment_method: 'static',
|
|
959
|
+
},
|
|
960
|
+
};
|
|
961
|
+
|
|
962
|
+
// Language-specific file extensions
|
|
963
|
+
const extMap = {
|
|
964
|
+
scala: '.scala', elixir: '.ex', haskell: '.hs', lua: '.lua',
|
|
965
|
+
zig: '.zig', ocaml: '.ml', r: '.R', perl: '.pl',
|
|
966
|
+
generic: '.ext',
|
|
967
|
+
};
|
|
968
|
+
const ext = extMap[language] || `.${language}` || '.ext';
|
|
969
|
+
|
|
970
|
+
// Replace .ext with actual extension
|
|
971
|
+
for (const [_key, example] of Object.entries(examples)) {
|
|
972
|
+
example.path = example.path.replace(/\.ext/g, ext);
|
|
973
|
+
if (example.dependencies) {
|
|
974
|
+
example.dependencies = example.dependencies.map(d => d.replace(/\.ext/g, ext));
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
return examples;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
function generateAgentPrompt(language) {
|
|
982
|
+
const lang = language ? language.charAt(0).toUpperCase() + language.slice(1) : '[LANGUAGE]';
|
|
983
|
+
|
|
984
|
+
return {
|
|
985
|
+
template:
|
|
986
|
+
`You are generating a context manifest for a ${lang} project.\n\n` +
|
|
987
|
+
'Walk the project directory and for each source file:\n' +
|
|
988
|
+
'1. Determine its type (source, test, config, documentation, etc.)\n' +
|
|
989
|
+
'2. Detect its programming language\n' +
|
|
990
|
+
'3. Identify its architectural layer (handler, service, model, etc.)\n' +
|
|
991
|
+
'4. Identify its business domain (auth, billing, tasks, etc.)\n' +
|
|
992
|
+
'5. Parse its import/use/require statements to find dependencies on OTHER project files\n' +
|
|
993
|
+
'6. Extract key exports (public functions, classes, types)\n' +
|
|
994
|
+
'7. Write a concise 1-3 sentence summary of what the file does and why it exists\n\n' +
|
|
995
|
+
'IMPORTANT rules for dependencies:\n' +
|
|
996
|
+
'- Only include paths to OTHER files that exist in this project\n' +
|
|
997
|
+
'- Skip external library/framework imports\n' +
|
|
998
|
+
'- Each dependency path must exactly match another entry\'s path field\n' +
|
|
999
|
+
'- Use forward slashes and relative paths from project root\n\n' +
|
|
1000
|
+
'Output the result as a JSON object matching the ContextManifest schema.\n' +
|
|
1001
|
+
'Set metadata.generator to "ai-agent" and metadata.version to "1.0.0".\n' +
|
|
1002
|
+
'Set enrichment_method to "llm" since you are generating the summaries.\n\n' +
|
|
1003
|
+
'After generating, call validate_manifest to check for errors before uploading.',
|
|
1004
|
+
usage:
|
|
1005
|
+
'Pass this prompt to your AI agent along with the schema from get_manifest_schema. ' +
|
|
1006
|
+
'The agent should walk the project, build the manifest, validate it, then upload ' +
|
|
1007
|
+
'via update_manifest_entries or rebuild_manifest.',
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
// ============================================================================
|
|
1012
|
+
// validate_manifest — validates a manifest against the schema
|
|
1013
|
+
// ============================================================================
|
|
1014
|
+
|
|
1015
|
+
export async function validateManifest(args) {
|
|
1016
|
+
const { manifest } = args;
|
|
1017
|
+
|
|
1018
|
+
const errors = [];
|
|
1019
|
+
const warnings = [];
|
|
1020
|
+
|
|
1021
|
+
// Check top-level structure
|
|
1022
|
+
if (!manifest) {
|
|
1023
|
+
errors.push({ field: 'manifest', message: 'Manifest object is required' });
|
|
1024
|
+
return { valid: false, errors, warnings };
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
if (!manifest.metadata) {
|
|
1028
|
+
errors.push({ field: 'metadata', message: 'metadata object is required' });
|
|
1029
|
+
} else {
|
|
1030
|
+
// Validate metadata fields
|
|
1031
|
+
const meta = manifest.metadata;
|
|
1032
|
+
if (!meta.version) errors.push({ field: 'metadata.version', message: 'version is required' });
|
|
1033
|
+
if (!meta.generatedAt) errors.push({ field: 'metadata.generatedAt', message: 'generatedAt is required' });
|
|
1034
|
+
if (!meta.projectName) errors.push({ field: 'metadata.projectName', message: 'projectName is required' });
|
|
1035
|
+
if (meta.entryCount === undefined) errors.push({ field: 'metadata.entryCount', message: 'entryCount is required' });
|
|
1036
|
+
if (!meta.generator) errors.push({ field: 'metadata.generator', message: 'generator is required' });
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
if (!manifest.entries || !Array.isArray(manifest.entries)) {
|
|
1040
|
+
errors.push({ field: 'entries', message: 'entries array is required' });
|
|
1041
|
+
return { valid: errors.length === 0, errors, warnings };
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
// Check entry count matches
|
|
1045
|
+
if (manifest.metadata && manifest.metadata.entryCount !== manifest.entries.length) {
|
|
1046
|
+
warnings.push({
|
|
1047
|
+
field: 'metadata.entryCount',
|
|
1048
|
+
message: `entryCount (${manifest.metadata.entryCount}) does not match entries.length (${manifest.entries.length})`,
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
// Valid enum values
|
|
1053
|
+
const validTypes = new Set(['source', 'config', 'documentation', 'architecture', 'test', 'infrastructure', 'script', 'style', 'asset', 'ci-cd']);
|
|
1054
|
+
const validLayers = new Set(['handler', 'service', 'component', 'hook', 'util', 'type', 'config', 'middleware', 'model', 'route', 'context', 'page', 'migration', 'command']);
|
|
1055
|
+
const validEnrichmentMethods = new Set(['static', 'llm']);
|
|
1056
|
+
|
|
1057
|
+
// Build path set for dependency validation
|
|
1058
|
+
const allPaths = new Set(manifest.entries.map(e => e.path));
|
|
1059
|
+
|
|
1060
|
+
// Validate each entry
|
|
1061
|
+
for (let i = 0; i < manifest.entries.length; i++) {
|
|
1062
|
+
const entry = manifest.entries[i];
|
|
1063
|
+
const prefix = `entries[${i}]`;
|
|
1064
|
+
|
|
1065
|
+
// Required fields
|
|
1066
|
+
if (!entry.path) errors.push({ field: `${prefix}.path`, message: 'path is required' });
|
|
1067
|
+
if (!entry.type) errors.push({ field: `${prefix}.type`, message: 'type is required' });
|
|
1068
|
+
if (!entry.tags || !Array.isArray(entry.tags)) errors.push({ field: `${prefix}.tags`, message: 'tags array is required' });
|
|
1069
|
+
if (!entry.summary) errors.push({ field: `${prefix}.summary`, message: 'summary is required' });
|
|
1070
|
+
if (!entry.dependencies || !Array.isArray(entry.dependencies)) {
|
|
1071
|
+
errors.push({ field: `${prefix}.dependencies`, message: 'dependencies array is required' });
|
|
1072
|
+
}
|
|
1073
|
+
if (!entry.lastAnalyzed) errors.push({ field: `${prefix}.lastAnalyzed`, message: 'lastAnalyzed is required' });
|
|
1074
|
+
|
|
1075
|
+
// Enum validation
|
|
1076
|
+
if (entry.type && !validTypes.has(entry.type)) {
|
|
1077
|
+
warnings.push({ field: `${prefix}.type`, message: `Unknown type "${entry.type}". Valid: ${[...validTypes].join(', ')}` });
|
|
1078
|
+
}
|
|
1079
|
+
if (entry.layer && !validLayers.has(entry.layer)) {
|
|
1080
|
+
warnings.push({ field: `${prefix}.layer`, message: `Unknown layer "${entry.layer}". Valid: ${[...validLayers].join(', ')}` });
|
|
1081
|
+
}
|
|
1082
|
+
if (entry.enrichment_method && !validEnrichmentMethods.has(entry.enrichment_method)) {
|
|
1083
|
+
warnings.push({ field: `${prefix}.enrichment_method`, message: `Unknown enrichment_method "${entry.enrichment_method}". Valid: static, llm` });
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
// Dependency path validation (check first 5 broken deps per entry to avoid noise)
|
|
1087
|
+
if (entry.dependencies && Array.isArray(entry.dependencies)) {
|
|
1088
|
+
let brokenCount = 0;
|
|
1089
|
+
for (const dep of entry.dependencies) {
|
|
1090
|
+
if (!allPaths.has(dep)) {
|
|
1091
|
+
if (brokenCount < 5) {
|
|
1092
|
+
warnings.push({ field: `${prefix}.dependencies`, message: `Dependency "${dep}" not found in manifest entries` });
|
|
1093
|
+
}
|
|
1094
|
+
brokenCount++;
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
if (brokenCount > 5) {
|
|
1098
|
+
warnings.push({ field: `${prefix}.dependencies`, message: `...and ${brokenCount - 5} more broken dependencies` });
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
// Path validation
|
|
1103
|
+
if (entry.path && entry.path.includes('\\')) {
|
|
1104
|
+
warnings.push({ field: `${prefix}.path`, message: 'Path contains backslashes. Use forward slashes.' });
|
|
1105
|
+
}
|
|
1106
|
+
if (entry.path && entry.path.startsWith('/')) {
|
|
1107
|
+
warnings.push({ field: `${prefix}.path`, message: 'Path is absolute. Use relative paths from project root.' });
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
// Summary quality
|
|
1111
|
+
if (entry.summary && entry.summary.length < 10) {
|
|
1112
|
+
warnings.push({ field: `${prefix}.summary`, message: 'Summary is very short. Aim for 1-3 sentences describing intent.' });
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
// Criticality score range
|
|
1116
|
+
if (entry.criticality_score !== undefined && (entry.criticality_score < 0 || entry.criticality_score > 1)) {
|
|
1117
|
+
warnings.push({ field: `${prefix}.criticality_score`, message: 'criticality_score should be between 0 and 1' });
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
// Stop after 50 errors to avoid massive output
|
|
1121
|
+
if (errors.length > 50) {
|
|
1122
|
+
errors.push({ field: 'manifest', message: 'Validation stopped after 50 errors. Fix these first.' });
|
|
1123
|
+
break;
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
// Check for duplicate paths
|
|
1128
|
+
const pathCounts = {};
|
|
1129
|
+
for (const entry of manifest.entries) {
|
|
1130
|
+
if (entry.path) {
|
|
1131
|
+
pathCounts[entry.path] = (pathCounts[entry.path] || 0) + 1;
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
for (const [path, count] of Object.entries(pathCounts)) {
|
|
1135
|
+
if (count > 1) {
|
|
1136
|
+
errors.push({ field: 'entries', message: `Duplicate path: "${path}" appears ${count} times` });
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
return {
|
|
1141
|
+
valid: errors.length === 0,
|
|
1142
|
+
errorCount: errors.length,
|
|
1143
|
+
warningCount: warnings.length,
|
|
1144
|
+
errors,
|
|
1145
|
+
warnings,
|
|
1146
|
+
summary: errors.length === 0
|
|
1147
|
+
? `Manifest is valid. ${manifest.entries.length} entries, ${warnings.length} warnings.`
|
|
1148
|
+
: `Manifest has ${errors.length} errors and ${warnings.length} warnings.`,
|
|
1149
|
+
};
|
|
1150
|
+
}
|