@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 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
- | `blast_radius` | Analyze what files would be affected if a file is modified or deleted |
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
- console.log(`[Init] Source files queued: ${response.source_files_count}`);
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
  */
@@ -31,53 +31,118 @@ function normalizePath(filePath) {
31
31
  p = p.replace(/^\//, '');
32
32
  return p;
33
33
  }
34
- // helper
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
- try {
37
- const apiKey = getApiKeyFromEnv();
38
- const headers = {
39
- 'Content-Type': 'application/json',
40
- };
41
- if (apiKey) {
42
- headers['Authorization'] = `Bearer ${apiKey}`;
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
- const edges = data.edge_summaries || {};
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(`This file depends on **${deps.length}** file(s):`);
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 summary = edges[dep];
105
- if (summary) {
106
- lines.push(`- **${dep}**: ${summary}`);
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
- const label = level === 1 ? 'Level 1 (direct)' : `Level ${level}`;
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
- // Tools:
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
- // Dependencies
418
+ // ---- dependencies ----
179
419
  server.registerTool("dependencies", {
180
- description: "Returns every file that the given source file imports or depends on, along with a short summary of why each dependency is used. 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.",
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
- // File summary (maps to what-is-this-file)
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, its role in the project, and how it fits into the surrounding architecture. 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.",
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
- content: [
217
- {
218
- type: "text",
219
- text: formatFileSummary(data)
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@latentforce/shift",
3
- "version": "1.0.15",
3
+ "version": "1.0.17",
4
4
  "description": "Shift CLI - AI-powered code intelligence with MCP support",
5
5
  "type": "module",
6
6
  "main": "./build/index.js",