@latentforce/shift 1.0.15 → 1.0.17
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 +4 -2
- package/build/cli/commands/init.js +7 -1
- package/build/daemon/tools-executor.js +146 -0
- package/build/mcp-server.js +330 -76
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -302,9 +302,11 @@ When left as-is (single entry with `language: null` and `path: ""`), the server
|
|
|
302
302
|
|
|
303
303
|
| Tool | Description |
|
|
304
304
|
|------|-------------|
|
|
305
|
-
| `
|
|
306
|
-
| `dependencies` | Get all dependencies for a file with relationship summaries |
|
|
305
|
+
| `project_overview` | Get the highest-level context for the project: architecture, tech stack, entry points, and top-level modules |
|
|
307
306
|
| `file_summary` | Get a summary of a file with optional parent directory context |
|
|
307
|
+
| `module_summary` | Get full documentation for the module that owns a given file, including DRG cluster metadata and parent/child module relationships |
|
|
308
|
+
| `dependencies` | Get all dependencies for a file with relationship summaries |
|
|
309
|
+
| `blast_radius` | Analyze what files would be affected if a file is modified or deleted |
|
|
308
310
|
|
|
309
311
|
Each tool accepts an optional `project_id` parameter. If not provided, it falls back to the `SHIFT_PROJECT_ID` environment variable.
|
|
310
312
|
|
|
@@ -249,7 +249,13 @@ export async function initCommand(options = {}) {
|
|
|
249
249
|
try {
|
|
250
250
|
const response = await sendInitScan(apiKey, project.projectId, payload);
|
|
251
251
|
console.log('[Init] ✓ Backend initialization completed');
|
|
252
|
-
|
|
252
|
+
const count = response.source_files_count ?? response.files_read;
|
|
253
|
+
if (count != null) {
|
|
254
|
+
console.log(`[Init] Source files queued: ${count}`);
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
console.log(`[Init] (backend did not return file count)`);
|
|
258
|
+
}
|
|
253
259
|
// Update local config with agent info (matching extension)
|
|
254
260
|
if (response.agents_created?.theme_planner) {
|
|
255
261
|
const agentInfo = {
|
|
@@ -22,6 +22,9 @@ export class ToolsExecutor {
|
|
|
22
22
|
'get_repository_root': this.getRepositoryRoot.bind(this),
|
|
23
23
|
'get_project_tree': this.getProjectTree.bind(this),
|
|
24
24
|
'execute_command': this.executeCommand.bind(this),
|
|
25
|
+
'run_bash': this.runBash.bind(this),
|
|
26
|
+
'glob_search': this.globSearch.bind(this),
|
|
27
|
+
'grep_search': this.grepSearch.bind(this),
|
|
25
28
|
};
|
|
26
29
|
}
|
|
27
30
|
/**
|
|
@@ -285,6 +288,149 @@ export class ToolsExecutor {
|
|
|
285
288
|
message: `No .git folder found after searching ${maxDepth} levels up`
|
|
286
289
|
};
|
|
287
290
|
}
|
|
291
|
+
/**
|
|
292
|
+
* Returns combined output matching tools.py's run_bash format.
|
|
293
|
+
*/
|
|
294
|
+
async runBash(params) {
|
|
295
|
+
const { command } = params;
|
|
296
|
+
if (!command) {
|
|
297
|
+
return { status: 'error', error: 'command is required' };
|
|
298
|
+
}
|
|
299
|
+
console.log(`[ToolsExecutor] run_bash: ${command}`);
|
|
300
|
+
try {
|
|
301
|
+
const { stdout, stderr } = await execAsync(command, {
|
|
302
|
+
cwd: this.workspaceRoot,
|
|
303
|
+
timeout: 30000,
|
|
304
|
+
maxBuffer: 1024 * 1024,
|
|
305
|
+
});
|
|
306
|
+
let output = stdout;
|
|
307
|
+
if (stderr)
|
|
308
|
+
output += `\n[STDERR]\n${stderr}`;
|
|
309
|
+
return {
|
|
310
|
+
status: 'success',
|
|
311
|
+
output: output.trim() || '[No output]',
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
catch (error) {
|
|
315
|
+
let output = error.stdout || '';
|
|
316
|
+
if (error.stderr)
|
|
317
|
+
output += `\n[STDERR]\n${error.stderr}`;
|
|
318
|
+
output += `\n[EXIT CODE: ${error.code || 1}]`;
|
|
319
|
+
return {
|
|
320
|
+
status: 'error',
|
|
321
|
+
output: output.trim(),
|
|
322
|
+
error: error.message,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Find files matching a glob pattern
|
|
328
|
+
*/
|
|
329
|
+
async globSearch(params) {
|
|
330
|
+
const { pattern } = params;
|
|
331
|
+
if (!pattern) {
|
|
332
|
+
return { status: 'error', error: 'pattern is required' };
|
|
333
|
+
}
|
|
334
|
+
console.log(`[ToolsExecutor] Glob search: ${pattern}`);
|
|
335
|
+
let basePath = '.';
|
|
336
|
+
let namePattern = pattern;
|
|
337
|
+
const lastSlash = pattern.lastIndexOf('/');
|
|
338
|
+
if (lastSlash !== -1) {
|
|
339
|
+
basePath = pattern.substring(0, lastSlash).replace(/\/?\*\*$/, '') || '.';
|
|
340
|
+
namePattern = pattern.substring(lastSlash + 1);
|
|
341
|
+
}
|
|
342
|
+
const searchDir = path.join(this.workspaceRoot, basePath);
|
|
343
|
+
const excludes = [
|
|
344
|
+
'-not', '-path', '*/node_modules/*',
|
|
345
|
+
'-not', '-path', '*/.git/*',
|
|
346
|
+
'-not', '-path', '*/__pycache__/*',
|
|
347
|
+
'-not', '-path', '*/dist/*',
|
|
348
|
+
'-not', '-path', '*/build/*',
|
|
349
|
+
'-not', '-path', '*/.venv/*',
|
|
350
|
+
];
|
|
351
|
+
// Quote searchDir to handle workspace paths that contain spaces
|
|
352
|
+
const cmd = `find "${searchDir}" -name "${namePattern}" ${excludes.join(' ')}`;
|
|
353
|
+
try {
|
|
354
|
+
const { stdout } = await execAsync(cmd, { timeout: 30000 });
|
|
355
|
+
const matches = stdout
|
|
356
|
+
.trim()
|
|
357
|
+
.split('\n')
|
|
358
|
+
.filter(Boolean)
|
|
359
|
+
.map(f => path.relative(this.workspaceRoot, f))
|
|
360
|
+
.slice(0, 200);
|
|
361
|
+
return {
|
|
362
|
+
status: 'success',
|
|
363
|
+
pattern,
|
|
364
|
+
matches,
|
|
365
|
+
count: matches.length,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
catch (error) {
|
|
369
|
+
return { status: 'error', error: error.message };
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Search file contents with a regex pattern
|
|
374
|
+
*/
|
|
375
|
+
async grepSearch(params) {
|
|
376
|
+
const { pattern, file_glob } = params;
|
|
377
|
+
if (!pattern) {
|
|
378
|
+
return { status: 'error', error: 'pattern is required' };
|
|
379
|
+
}
|
|
380
|
+
console.log(`[ToolsExecutor] Grep search: ${pattern}${file_glob ? ` (${file_glob})` : ''}`);
|
|
381
|
+
const baseArgs = [
|
|
382
|
+
'grep', '-rn', '-E',
|
|
383
|
+
'--exclude-dir=node_modules',
|
|
384
|
+
'--exclude-dir=.git',
|
|
385
|
+
'--exclude-dir=__pycache__',
|
|
386
|
+
'--exclude-dir=dist',
|
|
387
|
+
'--exclude-dir=build',
|
|
388
|
+
'--exclude-dir=.venv',
|
|
389
|
+
'--binary-files=without-match',
|
|
390
|
+
];
|
|
391
|
+
let searchPath = '.';
|
|
392
|
+
if (file_glob) {
|
|
393
|
+
if (file_glob.includes('/')) {
|
|
394
|
+
const match = file_glob.match(/^([^*]+?)\/(?:\*\*\/)?(.+)$/);
|
|
395
|
+
if (match) {
|
|
396
|
+
searchPath = match[1];
|
|
397
|
+
const namePat = match[2];
|
|
398
|
+
if (namePat && namePat !== '*')
|
|
399
|
+
baseArgs.push(`--include=${namePat}`);
|
|
400
|
+
}
|
|
401
|
+
else {
|
|
402
|
+
baseArgs.push(`--include=${file_glob}`);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
else {
|
|
406
|
+
baseArgs.push(`--include=${file_glob}`);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
const args = [...baseArgs, `'${pattern.replace(/'/g, `'\\''`)}'`, searchPath];
|
|
410
|
+
try {
|
|
411
|
+
const { stdout } = await execAsync(args.join(' '), {
|
|
412
|
+
cwd: this.workspaceRoot,
|
|
413
|
+
timeout: 30000,
|
|
414
|
+
maxBuffer: 1024 * 1024,
|
|
415
|
+
});
|
|
416
|
+
const lines = stdout.trim().split('\n').filter(Boolean);
|
|
417
|
+
const truncated = lines.length > 500;
|
|
418
|
+
const output = truncated ? lines.slice(0, 500).join('\n') + `\n\n... [truncated — ${lines.length} total matches, showing first 500]` : stdout.trim();
|
|
419
|
+
return {
|
|
420
|
+
status: 'success',
|
|
421
|
+
pattern,
|
|
422
|
+
file_glob: file_glob || null,
|
|
423
|
+
output,
|
|
424
|
+
match_count: lines.length,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
catch (error) {
|
|
428
|
+
if (error.code === 1 && !error.stderr) {
|
|
429
|
+
return { status: 'success', pattern, file_glob: file_glob || null, output: '', match_count: 0 };
|
|
430
|
+
}
|
|
431
|
+
return { status: 'error', error: error.message };
|
|
432
|
+
}
|
|
433
|
+
}
|
|
288
434
|
/**
|
|
289
435
|
* Get project tree — delegates to shared tree-scanner utility
|
|
290
436
|
*/
|
package/build/mcp-server.js
CHANGED
|
@@ -31,53 +31,118 @@ function normalizePath(filePath) {
|
|
|
31
31
|
p = p.replace(/^\//, '');
|
|
32
32
|
return p;
|
|
33
33
|
}
|
|
34
|
-
|
|
34
|
+
function getAuthHeaders() {
|
|
35
|
+
const apiKey = getApiKeyFromEnv();
|
|
36
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
37
|
+
if (apiKey)
|
|
38
|
+
headers['Authorization'] = `Bearer ${apiKey}`;
|
|
39
|
+
return headers;
|
|
40
|
+
}
|
|
41
|
+
function handleApiError(response, text) {
|
|
42
|
+
if (response.status === 404) {
|
|
43
|
+
if (text.includes('Knowledge graph not found') || text.includes('knowledge graph')) {
|
|
44
|
+
throw new Error("No knowledge graph exists for this project. Run 'shift-cli update-drg' to build it.");
|
|
45
|
+
}
|
|
46
|
+
if (text.includes('File not found') || text.includes('not found in')) {
|
|
47
|
+
const pathMatch = text.match(/['"]([^'"]+)['"]/);
|
|
48
|
+
const filePath = pathMatch ? pathMatch[1] : 'the requested file';
|
|
49
|
+
throw new Error(`File '${filePath}' not found in knowledge graph. Possible causes:\n` +
|
|
50
|
+
` 1. The path may be incorrect (check casing and slashes)\n` +
|
|
51
|
+
` 2. The file was added after the last 'shift-cli update-drg'\n` +
|
|
52
|
+
` 3. The file type is not supported (only JS/TS files are indexed)`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
throw new Error(`API call failed: ${response.status} ${response.statusText}${text ? ` - ${text}` : ""}`);
|
|
56
|
+
}
|
|
57
|
+
/** POST to backend API */
|
|
35
58
|
async function callBackendAPI(endpoint, data) {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const response = await fetch(`${BASE_URL}${endpoint}`, {
|
|
45
|
-
method: 'POST',
|
|
46
|
-
headers,
|
|
47
|
-
body: JSON.stringify(data),
|
|
48
|
-
});
|
|
49
|
-
if (!response.ok) {
|
|
50
|
-
const text = await response.text();
|
|
51
|
-
// Provide actionable error messages
|
|
52
|
-
if (response.status === 404) {
|
|
53
|
-
if (text.includes('Knowledge graph not found') || text.includes('knowledge graph')) {
|
|
54
|
-
throw new Error("No knowledge graph exists for this project. Run 'shift-cli update-drg' to build it.");
|
|
55
|
-
}
|
|
56
|
-
if (text.includes('File not found') || text.includes('not found in')) {
|
|
57
|
-
// Extract path from error if possible
|
|
58
|
-
const pathMatch = text.match(/['"]([^'"]+)['"]/);
|
|
59
|
-
const filePath = pathMatch ? pathMatch[1] : 'the requested file';
|
|
60
|
-
throw new Error(`File '${filePath}' not found in knowledge graph. Possible causes:\n` +
|
|
61
|
-
` 1. The path may be incorrect (check casing and slashes)\n` +
|
|
62
|
-
` 2. The file was added after the last 'shift-cli update-drg'\n` +
|
|
63
|
-
` 3. The file type is not supported (only JS/TS files are indexed)`);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
throw new Error(`API call failed: ${response.status} ${response.statusText}${text ? ` - ${text}` : ""}`);
|
|
67
|
-
}
|
|
68
|
-
return await response.json();
|
|
69
|
-
}
|
|
70
|
-
catch (error) {
|
|
71
|
-
throw error;
|
|
59
|
+
const response = await fetch(`${BASE_URL}${endpoint}`, {
|
|
60
|
+
method: 'POST',
|
|
61
|
+
headers: getAuthHeaders(),
|
|
62
|
+
body: JSON.stringify(data),
|
|
63
|
+
});
|
|
64
|
+
if (!response.ok) {
|
|
65
|
+
const text = await response.text();
|
|
66
|
+
handleApiError(response, text);
|
|
72
67
|
}
|
|
68
|
+
return await response.json();
|
|
73
69
|
}
|
|
70
|
+
/** GET from backend API with query params */
|
|
71
|
+
async function callBackendAPIGet(endpoint, params) {
|
|
72
|
+
const qs = new URLSearchParams(params).toString();
|
|
73
|
+
const url = `${BASE_URL}${endpoint}${qs ? `?${qs}` : ''}`;
|
|
74
|
+
const response = await fetch(url, {
|
|
75
|
+
method: 'GET',
|
|
76
|
+
headers: getAuthHeaders(),
|
|
77
|
+
});
|
|
78
|
+
if (!response.ok) {
|
|
79
|
+
const text = await response.text();
|
|
80
|
+
handleApiError(response, text);
|
|
81
|
+
}
|
|
82
|
+
return await response.json();
|
|
83
|
+
}
|
|
84
|
+
// ============= FORMATTERS =============
|
|
74
85
|
/** Format file_summary response as readable markdown */
|
|
75
86
|
function formatFileSummary(data) {
|
|
76
87
|
const lines = [];
|
|
77
88
|
lines.push(`## File: ${data.path}`);
|
|
89
|
+
if (data.module_name) {
|
|
90
|
+
lines.push(`**Module:** ${data.module_name}`);
|
|
91
|
+
}
|
|
78
92
|
lines.push('');
|
|
79
93
|
lines.push('### Summary');
|
|
80
94
|
lines.push(data.summary || 'No summary available.');
|
|
95
|
+
if (data.exports && Object.keys(data.exports).length > 0) {
|
|
96
|
+
lines.push('');
|
|
97
|
+
lines.push('### Exports');
|
|
98
|
+
for (const [category, items] of Object.entries(data.exports)) {
|
|
99
|
+
if (items?.length > 0) {
|
|
100
|
+
lines.push(`**${category}:** ${items.join(', ')}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (data.key_components?.length > 0) {
|
|
105
|
+
lines.push('');
|
|
106
|
+
lines.push('### Key Components');
|
|
107
|
+
for (const comp of data.key_components) {
|
|
108
|
+
if (typeof comp === 'object' && comp !== null) {
|
|
109
|
+
const name = comp.name || comp.component || Object.keys(comp)[0] || '';
|
|
110
|
+
const desc = comp.description || comp.purpose || Object.values(comp)[0] || '';
|
|
111
|
+
lines.push(`- **${name}**: ${desc}`);
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
lines.push(`- ${comp}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (data.internal_state) {
|
|
119
|
+
lines.push('');
|
|
120
|
+
lines.push(`**Internal State:** ${data.internal_state}`);
|
|
121
|
+
}
|
|
122
|
+
if (data.error_handling) {
|
|
123
|
+
lines.push(`**Error Handling:** ${data.error_handling}`);
|
|
124
|
+
}
|
|
125
|
+
if (data.constraints?.length > 0) {
|
|
126
|
+
lines.push('');
|
|
127
|
+
lines.push('### Constraints');
|
|
128
|
+
for (const c of data.constraints) {
|
|
129
|
+
lines.push(`- ${c}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (data.dependencies?.length > 0) {
|
|
133
|
+
lines.push('');
|
|
134
|
+
lines.push(`### Dependencies (${data.dependency_count ?? data.dependencies.length} imports)`);
|
|
135
|
+
for (const dep of data.dependencies) {
|
|
136
|
+
lines.push(`- ${dep}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (data.dependents?.length > 0) {
|
|
140
|
+
lines.push('');
|
|
141
|
+
lines.push(`### Dependents (${data.dependent_count ?? data.dependents.length} files import this)`);
|
|
142
|
+
for (const dep of data.dependents) {
|
|
143
|
+
lines.push(`- ${dep}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
81
146
|
if (data.parent_summaries?.length > 0) {
|
|
82
147
|
lines.push('');
|
|
83
148
|
lines.push('### Directory Context');
|
|
@@ -91,25 +156,75 @@ function formatFileSummary(data) {
|
|
|
91
156
|
function formatDependencies(data) {
|
|
92
157
|
const lines = [];
|
|
93
158
|
const deps = data.dependencies || [];
|
|
94
|
-
|
|
159
|
+
// edge_details is now a dict of {imports, usage_pattern, data_flow, dependency_summary}
|
|
160
|
+
const edgeDetails = data.edge_details || {};
|
|
95
161
|
lines.push(`## Dependencies: ${data.path}`);
|
|
162
|
+
if (data.module_name) {
|
|
163
|
+
lines.push(`**Module:** ${data.module_name}`);
|
|
164
|
+
}
|
|
96
165
|
lines.push('');
|
|
166
|
+
// --- Forward dependencies ---
|
|
97
167
|
if (deps.length === 0) {
|
|
98
168
|
lines.push('This file has no dependencies.');
|
|
99
169
|
}
|
|
100
170
|
else {
|
|
101
|
-
lines.push(
|
|
171
|
+
lines.push(`### Imports (${deps.length} file(s) this file depends on)`);
|
|
102
172
|
lines.push('');
|
|
103
173
|
for (const dep of deps) {
|
|
104
|
-
const
|
|
105
|
-
if (
|
|
106
|
-
lines.push(`- **${dep}
|
|
174
|
+
const edge = edgeDetails[dep];
|
|
175
|
+
if (edge) {
|
|
176
|
+
lines.push(`- **${dep}**`);
|
|
177
|
+
if (edge.usage_pattern)
|
|
178
|
+
lines.push(` - Usage: ${edge.usage_pattern}`);
|
|
179
|
+
if (edge.data_flow)
|
|
180
|
+
lines.push(` - Data flow: ${edge.data_flow}`);
|
|
181
|
+
if (edge.imports?.length > 0)
|
|
182
|
+
lines.push(` - Imports: ${edge.imports.join(', ')}`);
|
|
183
|
+
if (edge.dependency_summary)
|
|
184
|
+
lines.push(` - Summary: ${edge.dependency_summary}`);
|
|
107
185
|
}
|
|
108
186
|
else {
|
|
109
187
|
lines.push(`- **${dep}**`);
|
|
110
188
|
}
|
|
111
189
|
}
|
|
112
190
|
}
|
|
191
|
+
// --- Reverse dependencies ---
|
|
192
|
+
const dependents = data.dependents || [];
|
|
193
|
+
const dependentEdges = data.dependent_edge_details || {};
|
|
194
|
+
if (dependents.length > 0) {
|
|
195
|
+
lines.push('');
|
|
196
|
+
lines.push(`### Dependents (${dependents.length} file(s) that import this file)`);
|
|
197
|
+
lines.push('');
|
|
198
|
+
for (const dep of dependents) {
|
|
199
|
+
const edge = dependentEdges[dep];
|
|
200
|
+
if (edge) {
|
|
201
|
+
lines.push(`- **${dep}**`);
|
|
202
|
+
if (edge.usage_pattern)
|
|
203
|
+
lines.push(` - Usage: ${edge.usage_pattern}`);
|
|
204
|
+
if (edge.data_flow)
|
|
205
|
+
lines.push(` - Data flow: ${edge.data_flow}`);
|
|
206
|
+
if (edge.imports?.length > 0)
|
|
207
|
+
lines.push(` - Imports: ${edge.imports.join(', ')}`);
|
|
208
|
+
if (edge.dependency_summary)
|
|
209
|
+
lines.push(` - Summary: ${edge.dependency_summary}`);
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
lines.push(`- **${dep}**`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
// --- Implicit dependencies ---
|
|
217
|
+
const implicitDeps = data.implicit_dependencies || [];
|
|
218
|
+
if (implicitDeps.length > 0) {
|
|
219
|
+
lines.push('');
|
|
220
|
+
lines.push(`### Implicit / External Dependencies (${implicitDeps.length})`);
|
|
221
|
+
lines.push('');
|
|
222
|
+
const implicitEdges = data.implicit_edge_summaries || {};
|
|
223
|
+
for (const dep of implicitDeps) {
|
|
224
|
+
const summary = implicitEdges[dep];
|
|
225
|
+
lines.push(summary ? `- **${dep}**: ${summary}` : `- **${dep}**`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
113
228
|
return lines.join('\n');
|
|
114
229
|
}
|
|
115
230
|
/** Format blast_radius response as readable markdown */
|
|
@@ -117,6 +232,9 @@ function formatBlastRadius(data) {
|
|
|
117
232
|
const lines = [];
|
|
118
233
|
const affected = data.affected_files || [];
|
|
119
234
|
lines.push(`## Blast Radius: ${data.path}`);
|
|
235
|
+
if (data.module_name) {
|
|
236
|
+
lines.push(`**Module:** ${data.module_name}`);
|
|
237
|
+
}
|
|
120
238
|
lines.push('');
|
|
121
239
|
if (affected.length === 0) {
|
|
122
240
|
lines.push('No other files are affected by changes to this file.');
|
|
@@ -134,26 +252,155 @@ function formatBlastRadius(data) {
|
|
|
134
252
|
}
|
|
135
253
|
const levels = Object.keys(byLevel).map(Number).sort((a, b) => a - b);
|
|
136
254
|
for (const level of levels) {
|
|
137
|
-
|
|
138
|
-
lines.push(`### ${label}`);
|
|
255
|
+
lines.push(`### ${level === 1 ? 'Level 1 (direct)' : `Level ${level}`}`);
|
|
139
256
|
for (const file of byLevel[level]) {
|
|
140
257
|
lines.push(`- **${file.path}**: ${file.summary || 'No summary'}`);
|
|
141
258
|
}
|
|
142
259
|
lines.push('');
|
|
143
260
|
}
|
|
144
261
|
}
|
|
262
|
+
// --- Affected clusters ---
|
|
263
|
+
const clusters = data.cluster_blast_radius || [];
|
|
264
|
+
if (clusters.length > 0) {
|
|
265
|
+
lines.push('### Affected Modules/Folders');
|
|
266
|
+
for (const cluster of clusters) {
|
|
267
|
+
lines.push(`- **${cluster.path}**: ${cluster.summary}`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
145
270
|
return lines.join('\n');
|
|
146
271
|
}
|
|
272
|
+
/** Format module_summary response as readable markdown */
|
|
273
|
+
function formatModuleSummary(data) {
|
|
274
|
+
const lines = [];
|
|
275
|
+
lines.push(`## Module: ${data.module_name}`);
|
|
276
|
+
if (data.parent_module) {
|
|
277
|
+
lines.push(`**Parent module:** ${data.parent_module}`);
|
|
278
|
+
}
|
|
279
|
+
if (data.child_modules?.length > 0) {
|
|
280
|
+
lines.push(`**Sub-modules:** ${data.child_modules.join(', ')}`);
|
|
281
|
+
}
|
|
282
|
+
if (data.drg_cluster_summary) {
|
|
283
|
+
lines.push('');
|
|
284
|
+
lines.push('### Architecture Summary');
|
|
285
|
+
lines.push(data.drg_cluster_summary);
|
|
286
|
+
}
|
|
287
|
+
if (data.drg_cluster_metadata && Object.keys(data.drg_cluster_metadata).length > 0) {
|
|
288
|
+
const meta = data.drg_cluster_metadata;
|
|
289
|
+
if (meta.architecture_layers?.length > 0) {
|
|
290
|
+
lines.push('');
|
|
291
|
+
lines.push('**Architecture Layers:**');
|
|
292
|
+
for (const layer of meta.architecture_layers) {
|
|
293
|
+
lines.push(`- ${layer}`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (meta.technology_stack?.length > 0) {
|
|
297
|
+
lines.push('');
|
|
298
|
+
lines.push('**Technology Stack:**');
|
|
299
|
+
for (const tech of meta.technology_stack) {
|
|
300
|
+
lines.push(`- ${tech}`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
if (data.components?.length > 0) {
|
|
305
|
+
lines.push('');
|
|
306
|
+
lines.push(`### Files in this module (${data.components.length})`);
|
|
307
|
+
for (const comp of data.components) {
|
|
308
|
+
lines.push(`- ${comp}`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (data.content) {
|
|
312
|
+
lines.push('');
|
|
313
|
+
lines.push('### Full Documentation');
|
|
314
|
+
lines.push(data.content);
|
|
315
|
+
}
|
|
316
|
+
return lines.join('\n');
|
|
317
|
+
}
|
|
318
|
+
/** Format project_overview response as readable markdown */
|
|
319
|
+
function formatProjectOverview(data) {
|
|
320
|
+
const lines = [];
|
|
321
|
+
lines.push('## Project Overview');
|
|
322
|
+
lines.push('');
|
|
323
|
+
if (data.architecture_summary) {
|
|
324
|
+
lines.push('### Architecture Summary');
|
|
325
|
+
lines.push(data.architecture_summary);
|
|
326
|
+
lines.push('');
|
|
327
|
+
}
|
|
328
|
+
if (data.architecture_layers?.length > 0) {
|
|
329
|
+
lines.push('### Architecture Layers');
|
|
330
|
+
for (const layer of data.architecture_layers) {
|
|
331
|
+
if (typeof layer === 'string') {
|
|
332
|
+
lines.push(`- ${layer}`);
|
|
333
|
+
}
|
|
334
|
+
else if (layer && typeof layer === 'object') {
|
|
335
|
+
const name = layer.name || layer.layer || layer.title || Object.values(layer)[0];
|
|
336
|
+
const desc = layer.description || layer.desc || (Object.values(layer).length > 1 ? Object.values(layer)[1] : '');
|
|
337
|
+
lines.push(desc ? `- **${name}**: ${desc}` : `- ${name}`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
lines.push('');
|
|
341
|
+
}
|
|
342
|
+
if (data.technology_stack?.length > 0) {
|
|
343
|
+
lines.push('### Technology Stack');
|
|
344
|
+
for (const tech of data.technology_stack) {
|
|
345
|
+
if (typeof tech === 'string') {
|
|
346
|
+
lines.push(`- ${tech}`);
|
|
347
|
+
}
|
|
348
|
+
else if (tech && typeof tech === 'object') {
|
|
349
|
+
const name = tech.name || tech.technology || tech.title || Object.values(tech)[0];
|
|
350
|
+
const desc = tech.description || tech.desc || (Object.values(tech).length > 1 ? Object.values(tech)[1] : '');
|
|
351
|
+
lines.push(desc ? `- **${name}**: ${desc}` : `- ${name}`);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
lines.push('');
|
|
355
|
+
}
|
|
356
|
+
if (data.entry_points?.length > 0) {
|
|
357
|
+
lines.push('### Entry Points');
|
|
358
|
+
for (const ep of data.entry_points) {
|
|
359
|
+
if (typeof ep === 'string') {
|
|
360
|
+
lines.push(`- ${ep}`);
|
|
361
|
+
}
|
|
362
|
+
else if (ep && typeof ep === 'object') {
|
|
363
|
+
const name = ep.name || ep.file || ep.path || Object.values(ep)[0];
|
|
364
|
+
const desc = ep.description || ep.desc || (Object.values(ep).length > 1 ? Object.values(ep)[1] : '');
|
|
365
|
+
lines.push(desc ? `- **${name}**: ${desc}` : `- ${name}`);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
lines.push('');
|
|
369
|
+
}
|
|
370
|
+
if (data.constraints?.length > 0) {
|
|
371
|
+
lines.push('### Constraints');
|
|
372
|
+
for (const c of data.constraints) {
|
|
373
|
+
lines.push(`- ${c}`);
|
|
374
|
+
}
|
|
375
|
+
lines.push('');
|
|
376
|
+
}
|
|
377
|
+
if (data.top_level_modules?.length > 0) {
|
|
378
|
+
lines.push('### Top-level Modules');
|
|
379
|
+
lines.push('');
|
|
380
|
+
for (const mod of data.top_level_modules) {
|
|
381
|
+
lines.push(`#### ${mod.path}`);
|
|
382
|
+
lines.push(mod.summary || '');
|
|
383
|
+
if (mod.files?.length > 0) {
|
|
384
|
+
lines.push(`Files: ${mod.files.join(', ')}`);
|
|
385
|
+
}
|
|
386
|
+
lines.push('');
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
if (data.codewiki_overview) {
|
|
390
|
+
lines.push('### Project Documentation');
|
|
391
|
+
lines.push(data.codewiki_overview);
|
|
392
|
+
}
|
|
393
|
+
return lines.join('\n');
|
|
394
|
+
}
|
|
395
|
+
// ============= MCP SERVER =============
|
|
147
396
|
export async function startMcpServer() {
|
|
148
|
-
// Create server instance
|
|
149
397
|
const server = new McpServer({
|
|
150
398
|
name: "shift",
|
|
151
399
|
version,
|
|
152
400
|
});
|
|
153
|
-
//
|
|
154
|
-
// Blast radius
|
|
401
|
+
// ---- blast_radius ----
|
|
155
402
|
server.registerTool("blast_radius", {
|
|
156
|
-
description: "Use this BEFORE editing or refactoring any source file. Returns every file in the project that imports or depends on the given file, grouped by dependency depth (level 1 = direct importers, level 2 = their importers, etc.). Use this to understand the full impact of a change before making it. Only works on indexed source files: .js .jsx .ts .tsx .py .java .cpp .cs .go .c .h .css .scss .html — do not call for .json, .yaml, .md or other non-source files.",
|
|
403
|
+
description: "Use this BEFORE editing or refactoring any source file. Returns every file in the project that imports or depends on the given file, grouped by dependency depth (level 1 = direct importers, level 2 = their importers, etc.). Also returns the affected modules/folders. Use this to understand the full impact of a change before making it. Only works on indexed source files: .js .jsx .ts .tsx .py .java .cpp .cs .go .c .h .css .scss .html — do not call for .json, .yaml, .md or other non-source files.",
|
|
157
404
|
inputSchema: z.object({
|
|
158
405
|
file_path: z.string().describe("Path to the file (relative to project root)"),
|
|
159
406
|
project_id: z.string().optional().describe("Shift Lite project UUID; overrides SHIFT_PROJECT_ID if provided"),
|
|
@@ -166,18 +413,11 @@ export async function startMcpServer() {
|
|
|
166
413
|
project_id: projectId,
|
|
167
414
|
...(args.level != null && { level: args.level }),
|
|
168
415
|
});
|
|
169
|
-
return {
|
|
170
|
-
content: [
|
|
171
|
-
{
|
|
172
|
-
type: "text",
|
|
173
|
-
text: formatBlastRadius(data)
|
|
174
|
-
},
|
|
175
|
-
],
|
|
176
|
-
};
|
|
416
|
+
return { content: [{ type: "text", text: formatBlastRadius(data) }] };
|
|
177
417
|
});
|
|
178
|
-
//
|
|
418
|
+
// ---- dependencies ----
|
|
179
419
|
server.registerTool("dependencies", {
|
|
180
|
-
description: "Returns
|
|
420
|
+
description: "Returns bidirectional dependencies of a source file: what it imports (with usage pattern, data flow, and summary per dependency) AND what imports it (reverse dependencies). Use this to trace data flow, understand module relationships, follow a bug through the call chain, or map out what a file relies on before modifying it. Only works on indexed source files: .js .jsx .ts .tsx .py .java .cpp .cs .go .c .h .css .scss .html — do not call for .json, .yaml, .md or other non-source files.",
|
|
181
421
|
inputSchema: z.object({
|
|
182
422
|
file_path: z.string().describe("Path to the file"),
|
|
183
423
|
project_id: z.string().optional().describe("Shift Lite project UUID; overrides SHIFT_PROJECT_ID if provided"),
|
|
@@ -188,18 +428,11 @@ export async function startMcpServer() {
|
|
|
188
428
|
path: normalizePath(args.file_path),
|
|
189
429
|
project_id: projectId,
|
|
190
430
|
});
|
|
191
|
-
return {
|
|
192
|
-
content: [
|
|
193
|
-
{
|
|
194
|
-
type: "text",
|
|
195
|
-
text: formatDependencies(data)
|
|
196
|
-
},
|
|
197
|
-
],
|
|
198
|
-
};
|
|
431
|
+
return { content: [{ type: "text", text: formatDependencies(data) }] };
|
|
199
432
|
});
|
|
200
|
-
//
|
|
433
|
+
// ---- file_summary ----
|
|
201
434
|
server.registerTool("file_summary", {
|
|
202
|
-
description: "Returns an AI-generated summary of a source file: what it does, its key exports and functions,
|
|
435
|
+
description: "Returns an AI-generated summary of a source file: what it does, its key exports and functions, internal state, error handling, constraints, what it imports, who imports it, and which module it belongs to. Always call this before reading a raw file — it gives you the essential context without having to parse the full source. Use the 'level' parameter to include summaries of parent directories for broader context. Only works on indexed source files: .js .jsx .ts .tsx .py .java .cpp .cs .go .c .h .css .scss .html — for .json, .yaml, .md, or other non-source files, read them directly instead.",
|
|
203
436
|
inputSchema: z.object({
|
|
204
437
|
file_path: z.string().describe("Path to the file to summarize"),
|
|
205
438
|
project_id: z.string().optional().describe("Shift Lite project UUID; overrides SHIFT_PROJECT_ID if provided"),
|
|
@@ -212,14 +445,35 @@ export async function startMcpServer() {
|
|
|
212
445
|
project_id: projectId,
|
|
213
446
|
level: args.level ?? 0,
|
|
214
447
|
});
|
|
215
|
-
return {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
448
|
+
return { content: [{ type: "text", text: formatFileSummary(data) }] };
|
|
449
|
+
});
|
|
450
|
+
// ---- module_summary ----
|
|
451
|
+
server.registerTool("module_summary", {
|
|
452
|
+
description: "Given any file path, returns the full documentation for the module that owns it, the list of files in that module, the corresponding DRG cluster summary and metadata, and parent/child module relationships. Use this after file_summary to get broader module-level context before editing — especially useful when a change may affect an entire module rather than just one file.",
|
|
453
|
+
inputSchema: z.object({
|
|
454
|
+
file_path: z.string().describe("Path to any file in the module (relative to project root)"),
|
|
455
|
+
project_id: z.string().optional().describe("Shift Lite project UUID; overrides SHIFT_PROJECT_ID if provided"),
|
|
456
|
+
})
|
|
457
|
+
}, async (args) => {
|
|
458
|
+
const projectId = resolveProjectId(args);
|
|
459
|
+
const data = await callBackendAPI('/api/v1/mcp/module-summary', {
|
|
460
|
+
path: normalizePath(args.file_path),
|
|
461
|
+
project_id: projectId,
|
|
462
|
+
});
|
|
463
|
+
return { content: [{ type: "text", text: formatModuleSummary(data) }] };
|
|
464
|
+
});
|
|
465
|
+
// ---- project_overview ----
|
|
466
|
+
server.registerTool("project_overview", {
|
|
467
|
+
description: "Returns the highest-level context for the project: architecture summary, layers, tech stack, entry points, constraints, top-level modules with their files, and the project documentation overview. Call this first at the start of a session — before exploring any specific file — to understand the overall structure and where things live.",
|
|
468
|
+
inputSchema: z.object({
|
|
469
|
+
project_id: z.string().optional().describe("Shift Lite project UUID; overrides SHIFT_PROJECT_ID if provided"),
|
|
470
|
+
})
|
|
471
|
+
}, async (args) => {
|
|
472
|
+
const projectId = resolveProjectId(args);
|
|
473
|
+
const data = await callBackendAPIGet('/api/v1/mcp/project-overview', {
|
|
474
|
+
project_id: projectId,
|
|
475
|
+
});
|
|
476
|
+
return { content: [{ type: "text", text: formatProjectOverview(data) }] };
|
|
223
477
|
});
|
|
224
478
|
const transport = new StdioServerTransport();
|
|
225
479
|
await server.connect(transport);
|