@latentforce/shift 1.0.7 → 1.0.9
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 +69 -25
- package/build/cli/commands/init.js +76 -58
- package/build/cli/commands/start.js +32 -69
- package/build/cli/commands/status.js +17 -7
- package/build/cli/commands/update-drg.js +175 -0
- package/build/daemon/tools-executor.js +19 -86
- package/build/index.js +38 -4
- package/build/mcp-server.js +105 -8
- package/build/utils/api-client.js +70 -35
- package/build/utils/auth-resolver.js +184 -0
- package/build/utils/config.js +3 -3
- package/build/utils/prompts.js +45 -0
- package/package.json +1 -1
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { exec } from 'child_process';
|
|
4
|
+
import { promisify } from 'util';
|
|
5
|
+
import { getApiKey, readProjectConfig } from '../../utils/config.js';
|
|
6
|
+
import { getProjectTree, extractAllFilePaths } from '../../utils/tree-scanner.js';
|
|
7
|
+
import { sendUpdateDrg } from '../../utils/api-client.js';
|
|
8
|
+
const execAsync = promisify(exec);
|
|
9
|
+
async function detectGitChanges(projectRoot, extensions) {
|
|
10
|
+
const changes = { added: [], modified: [], deleted: [] };
|
|
11
|
+
try {
|
|
12
|
+
const { stdout } = await execAsync('git status --porcelain', { cwd: projectRoot });
|
|
13
|
+
for (const line of stdout.split('\n')) {
|
|
14
|
+
if (!line.trim())
|
|
15
|
+
continue;
|
|
16
|
+
const statusCode = line.substring(0, 2);
|
|
17
|
+
let filePath = line.substring(3).trim();
|
|
18
|
+
// Handle renamed files (R status shows "old -> new")
|
|
19
|
+
if (filePath.includes(' -> ')) {
|
|
20
|
+
const parts = filePath.split(' -> ');
|
|
21
|
+
// Mark old path as deleted, new path as added
|
|
22
|
+
const oldPath = parts[0].trim();
|
|
23
|
+
const newPath = parts[1].trim();
|
|
24
|
+
const oldExt = path.extname(oldPath).toLowerCase();
|
|
25
|
+
const newExt = path.extname(newPath).toLowerCase();
|
|
26
|
+
if (extensions.has(oldExt))
|
|
27
|
+
changes.deleted.push(oldPath);
|
|
28
|
+
if (extensions.has(newExt))
|
|
29
|
+
changes.added.push(newPath);
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
// Filter by extensions
|
|
33
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
34
|
+
if (!extensions.has(ext))
|
|
35
|
+
continue;
|
|
36
|
+
// Categorize based on git status codes
|
|
37
|
+
if (statusCode === '??' || statusCode.trimEnd() === 'A') {
|
|
38
|
+
changes.added.push(filePath);
|
|
39
|
+
}
|
|
40
|
+
else if (statusCode.includes('D')) {
|
|
41
|
+
changes.deleted.push(filePath);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
// M, MM, AM, etc. — treat as modified
|
|
45
|
+
changes.modified.push(filePath);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
// Not a git repo or git not available
|
|
51
|
+
}
|
|
52
|
+
return changes;
|
|
53
|
+
}
|
|
54
|
+
const JS_TS_EXTENSIONS = new Set(['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs']);
|
|
55
|
+
export async function updateDrgCommand(options = {}) {
|
|
56
|
+
const projectRoot = process.cwd();
|
|
57
|
+
const mode = options.mode || 'incremental';
|
|
58
|
+
console.log('╔═══════════════════════════════════════════════╗');
|
|
59
|
+
console.log('║ Updating Dependency Graph (DRG) ║');
|
|
60
|
+
console.log('╚═══════════════════════════════════════════════╝\n');
|
|
61
|
+
// Step 1: Check API key
|
|
62
|
+
console.log('[DRG] Step 1/4: Checking API key...');
|
|
63
|
+
const apiKey = getApiKey();
|
|
64
|
+
if (!apiKey) {
|
|
65
|
+
console.error('\n❌ No API key found. Run "shift-cli init" first to configure your API key.\n');
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
console.log('[DRG] ✓ API key found\n');
|
|
69
|
+
// Step 2: Read project config
|
|
70
|
+
console.log('[DRG] Step 2/4: Reading project configuration...');
|
|
71
|
+
const projectConfig = readProjectConfig(projectRoot);
|
|
72
|
+
if (!projectConfig) {
|
|
73
|
+
console.error('\n❌ No project configured for this directory.');
|
|
74
|
+
console.log('Run "shift-cli start" first to configure the project.\n');
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
console.log(`[DRG] ✓ Project: ${projectConfig.project_name} (${projectConfig.project_id})\n`);
|
|
78
|
+
// Step 3: Scan project for JS/TS files
|
|
79
|
+
console.log('[DRG] Step 3/4: Scanning project for JS/TS files...');
|
|
80
|
+
const treeData = getProjectTree(projectRoot, {
|
|
81
|
+
depth: 0,
|
|
82
|
+
exclude_patterns: [
|
|
83
|
+
'.git',
|
|
84
|
+
'node_modules',
|
|
85
|
+
'__pycache__',
|
|
86
|
+
'.vscode',
|
|
87
|
+
'dist',
|
|
88
|
+
'build',
|
|
89
|
+
'.shift',
|
|
90
|
+
'.next',
|
|
91
|
+
'coverage',
|
|
92
|
+
'venv',
|
|
93
|
+
'env',
|
|
94
|
+
],
|
|
95
|
+
});
|
|
96
|
+
const allFiles = extractAllFilePaths(treeData.tree);
|
|
97
|
+
const jstsFiles = allFiles.filter((filePath) => {
|
|
98
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
99
|
+
return JS_TS_EXTENSIONS.has(ext);
|
|
100
|
+
});
|
|
101
|
+
console.log(`[DRG] Total files scanned: ${allFiles.length}`);
|
|
102
|
+
console.log(`[DRG] JS/TS files found: ${jstsFiles.length}\n`);
|
|
103
|
+
if (jstsFiles.length === 0) {
|
|
104
|
+
console.log('⚠️ No JS/TS files found in project. Nothing to update.\n');
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
// Read file contents
|
|
108
|
+
const files = [];
|
|
109
|
+
let readErrors = 0;
|
|
110
|
+
for (const filePath of jstsFiles) {
|
|
111
|
+
const absolutePath = path.join(projectRoot, filePath);
|
|
112
|
+
try {
|
|
113
|
+
const content = fs.readFileSync(absolutePath, 'utf-8');
|
|
114
|
+
files.push({ file_path: filePath, content });
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
readErrors++;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (readErrors > 0) {
|
|
121
|
+
console.log(`[DRG] ⚠️ Could not read ${readErrors} file(s)\n`);
|
|
122
|
+
}
|
|
123
|
+
// Always use git to compute added/modified/deleted (single source of truth)
|
|
124
|
+
console.log('[DRG] Detecting git changes...');
|
|
125
|
+
const gitChanges = await detectGitChanges(projectRoot, JS_TS_EXTENSIONS);
|
|
126
|
+
console.log(`[DRG] Git changes — added: ${gitChanges.added.length}, modified: ${gitChanges.modified.length}, deleted: ${gitChanges.deleted.length}`);
|
|
127
|
+
// Which files to send: incremental = only changed; baseline = all (full re-analysis)
|
|
128
|
+
const changedPaths = new Set([...gitChanges.added, ...gitChanges.modified]);
|
|
129
|
+
const filesToSend = mode === 'baseline'
|
|
130
|
+
? files
|
|
131
|
+
: files.filter((f) => changedPaths.has(f.file_path));
|
|
132
|
+
if (mode === 'baseline') {
|
|
133
|
+
console.log(`[DRG] Baseline: sending all ${filesToSend.length} JS/TS files.\n`);
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
console.log(`[DRG] Incremental: sending ${filesToSend.length} changed file(s).\n`);
|
|
137
|
+
}
|
|
138
|
+
if (filesToSend.length === 0 && gitChanges.deleted.length === 0) {
|
|
139
|
+
console.log('\n✓ No JS/TS changes detected. DRG is up to date.\n');
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
// Step 4: Send to backend
|
|
143
|
+
console.log(`[DRG] Step 4/4: Sending ${filesToSend.length} files to backend (mode: ${mode})...`);
|
|
144
|
+
const payload = {
|
|
145
|
+
project_id: projectConfig.project_id,
|
|
146
|
+
language: 'js_ts',
|
|
147
|
+
mode: mode,
|
|
148
|
+
changes: gitChanges,
|
|
149
|
+
files: filesToSend,
|
|
150
|
+
};
|
|
151
|
+
try {
|
|
152
|
+
const response = await sendUpdateDrg(apiKey, payload);
|
|
153
|
+
console.log('\n╔═══════════════════════════════════════════════╗');
|
|
154
|
+
console.log('║ ✓ DRG Update Complete ║');
|
|
155
|
+
console.log('╚═══════════════════════════════════════════════╝\n');
|
|
156
|
+
console.log(` Success: ${response.success}`);
|
|
157
|
+
console.log(` Message: ${response.message}`);
|
|
158
|
+
if (response.stats) {
|
|
159
|
+
console.log(` Files sent: ${response.stats.total_files_provided}`);
|
|
160
|
+
console.log(` Unique: ${response.stats.unique_paths}`);
|
|
161
|
+
console.log(` Added: ${response.stats.added}`);
|
|
162
|
+
console.log(` Modified: ${response.stats.modified}`);
|
|
163
|
+
console.log(` Deleted: ${response.stats.deleted}`);
|
|
164
|
+
const affected = response.stats.affected_file_paths;
|
|
165
|
+
if (affected?.length) {
|
|
166
|
+
console.log(` Affected by deletions (edges updated): ${affected.length} file(s)`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
console.log('\nUse "shift-cli status" to check progress.\n');
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
console.error(`\n❌ Failed to update DRG: ${error.message}`);
|
|
173
|
+
process.exit(1);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
@@ -6,6 +6,7 @@ import * as fs from 'fs/promises';
|
|
|
6
6
|
import * as path from 'path';
|
|
7
7
|
import { exec } from 'child_process';
|
|
8
8
|
import { promisify } from 'util';
|
|
9
|
+
import { getProjectTree as getProjectTreeSync } from '../utils/tree-scanner.js';
|
|
9
10
|
const execAsync = promisify(exec);
|
|
10
11
|
export class ToolsExecutor {
|
|
11
12
|
wsClient;
|
|
@@ -285,99 +286,31 @@ export class ToolsExecutor {
|
|
|
285
286
|
};
|
|
286
287
|
}
|
|
287
288
|
/**
|
|
288
|
-
* Get project tree
|
|
289
|
+
* Get project tree — delegates to shared tree-scanner utility
|
|
289
290
|
*/
|
|
290
291
|
async getProjectTree(params) {
|
|
291
|
-
const { depth = 0, exclude_patterns =
|
|
292
|
-
'.git',
|
|
293
|
-
'node_modules',
|
|
294
|
-
'__pycache__',
|
|
295
|
-
'.vscode',
|
|
296
|
-
'dist',
|
|
297
|
-
'build',
|
|
298
|
-
'.shift',
|
|
299
|
-
'.next',
|
|
300
|
-
'.cache',
|
|
301
|
-
'coverage',
|
|
302
|
-
'.pytest_cache',
|
|
303
|
-
'venv',
|
|
304
|
-
'env',
|
|
305
|
-
'.env'
|
|
306
|
-
] } = params;
|
|
292
|
+
const { depth = 0, exclude_patterns } = params;
|
|
307
293
|
console.log(`[ToolsExecutor] Getting project tree from workspace root: ${this.workspaceRoot}`);
|
|
308
294
|
console.log(`[ToolsExecutor] Depth limit: ${depth === 0 ? 'unlimited' : depth}`);
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
const scanDirectory = async (dirPath, currentDepth = 0, relativePath = '') => {
|
|
314
|
-
if (currentDepth >= max_depth) {
|
|
315
|
-
return [];
|
|
316
|
-
}
|
|
317
|
-
const items = [];
|
|
318
|
-
try {
|
|
319
|
-
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
320
|
-
for (const entry of entries) {
|
|
321
|
-
if (exclude_patterns.some((pattern) => entry.name.includes(pattern))) {
|
|
322
|
-
continue;
|
|
323
|
-
}
|
|
324
|
-
const itemPath = path.join(dirPath, entry.name);
|
|
325
|
-
const itemRelativePath = relativePath ? path.join(relativePath, entry.name) : entry.name;
|
|
326
|
-
if (entry.isDirectory()) {
|
|
327
|
-
dir_count++;
|
|
328
|
-
const children = await scanDirectory(itemPath, currentDepth + 1, itemRelativePath);
|
|
329
|
-
items.push({
|
|
330
|
-
name: entry.name,
|
|
331
|
-
type: 'directory',
|
|
332
|
-
path: itemRelativePath,
|
|
333
|
-
depth: currentDepth,
|
|
334
|
-
children: children
|
|
335
|
-
});
|
|
336
|
-
}
|
|
337
|
-
else if (entry.isFile()) {
|
|
338
|
-
file_count++;
|
|
339
|
-
try {
|
|
340
|
-
const stats = await fs.stat(itemPath);
|
|
341
|
-
total_size += stats.size;
|
|
342
|
-
const ext = path.extname(entry.name).toLowerCase();
|
|
343
|
-
items.push({
|
|
344
|
-
name: entry.name,
|
|
345
|
-
type: 'file',
|
|
346
|
-
path: itemRelativePath,
|
|
347
|
-
depth: currentDepth,
|
|
348
|
-
size: stats.size,
|
|
349
|
-
extension: ext,
|
|
350
|
-
modified: stats.mtime
|
|
351
|
-
});
|
|
352
|
-
}
|
|
353
|
-
catch {
|
|
354
|
-
console.warn(`[ToolsExecutor] Cannot read: ${itemPath}`);
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
catch (err) {
|
|
360
|
-
console.error(`[ToolsExecutor] Error scanning ${dirPath}:`, err.message);
|
|
361
|
-
}
|
|
362
|
-
return items;
|
|
363
|
-
};
|
|
364
|
-
const tree = await scanDirectory(this.workspaceRoot, 0, '');
|
|
365
|
-
const total_size_mb = (total_size / (1024 * 1024)).toFixed(2);
|
|
295
|
+
const result = getProjectTreeSync(this.workspaceRoot, {
|
|
296
|
+
depth,
|
|
297
|
+
...(exclude_patterns && { exclude_patterns }),
|
|
298
|
+
});
|
|
366
299
|
console.log(`[ToolsExecutor] ✓ Scanned project tree:`);
|
|
367
|
-
console.log(`[ToolsExecutor] Files: ${file_count}`);
|
|
368
|
-
console.log(`[ToolsExecutor] Directories: ${dir_count}`);
|
|
369
|
-
console.log(`[ToolsExecutor] Total size: ${total_size_mb} MB`);
|
|
300
|
+
console.log(`[ToolsExecutor] Files: ${result.file_count}`);
|
|
301
|
+
console.log(`[ToolsExecutor] Directories: ${result.dir_count}`);
|
|
302
|
+
console.log(`[ToolsExecutor] Total size: ${result.total_size_mb} MB`);
|
|
370
303
|
return {
|
|
371
304
|
status: 'success',
|
|
372
|
-
tree: tree,
|
|
373
|
-
file_count: file_count,
|
|
374
|
-
dir_count: dir_count,
|
|
375
|
-
total_size_bytes:
|
|
376
|
-
total_size_mb: total_size_mb,
|
|
377
|
-
scanned_from:
|
|
378
|
-
depth_limit:
|
|
379
|
-
actual_max_depth:
|
|
380
|
-
excluded_patterns:
|
|
305
|
+
tree: result.tree,
|
|
306
|
+
file_count: result.file_count,
|
|
307
|
+
dir_count: result.dir_count,
|
|
308
|
+
total_size_bytes: result.total_size_bytes,
|
|
309
|
+
total_size_mb: result.total_size_mb,
|
|
310
|
+
scanned_from: result.scanned_from,
|
|
311
|
+
depth_limit: result.depth_limit,
|
|
312
|
+
actual_max_depth: result.actual_max_depth,
|
|
313
|
+
excluded_patterns: result.excluded_patterns,
|
|
381
314
|
};
|
|
382
315
|
}
|
|
383
316
|
}
|
package/build/index.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from 'commander';
|
|
3
|
+
import { createRequire } from 'module';
|
|
4
|
+
const require = createRequire(import.meta.url);
|
|
5
|
+
const { version } = require('../package.json');
|
|
3
6
|
const program = new Command();
|
|
4
7
|
program
|
|
5
8
|
.name('shift-cli')
|
|
6
9
|
.description('Shift CLI - AI-powered code intelligence')
|
|
7
|
-
.version(
|
|
10
|
+
.version(version);
|
|
8
11
|
// MCP server mode (default when run via MCP host)
|
|
9
12
|
program
|
|
10
13
|
.command('mcp', { isDefault: true, hidden: true })
|
|
@@ -24,17 +27,40 @@ program
|
|
|
24
27
|
program
|
|
25
28
|
.command('start')
|
|
26
29
|
.description('Start the Shift daemon for this project')
|
|
27
|
-
.
|
|
30
|
+
.option('--guest', 'Use guest authentication (auto-creates project)')
|
|
31
|
+
.option('--api-key <key>', 'Provide API key directly')
|
|
32
|
+
.option('--project-name <name>', 'Create new project or match existing by name')
|
|
33
|
+
.option('--project-id <id>', 'Use existing project UUID')
|
|
34
|
+
.option('--template <id>', 'Migration template ID for project creation')
|
|
35
|
+
.action(async (options) => {
|
|
28
36
|
const { startCommand } = await import('./cli/commands/start.js');
|
|
29
|
-
await startCommand(
|
|
37
|
+
await startCommand({
|
|
38
|
+
guest: options.guest,
|
|
39
|
+
apiKey: options.apiKey,
|
|
40
|
+
projectName: options.projectName,
|
|
41
|
+
projectId: options.projectId,
|
|
42
|
+
template: options.template,
|
|
43
|
+
});
|
|
30
44
|
});
|
|
31
45
|
program
|
|
32
46
|
.command('init')
|
|
33
47
|
.description('Initialize and scan the project for file indexing')
|
|
34
48
|
.option('-f, --force', 'Force re-indexing even if project is already indexed')
|
|
49
|
+
.option('--guest', 'Use guest authentication (auto-creates project)')
|
|
50
|
+
.option('--api-key <key>', 'Provide API key directly')
|
|
51
|
+
.option('--project-name <name>', 'Create new project or match existing by name')
|
|
52
|
+
.option('--project-id <id>', 'Use existing project UUID')
|
|
53
|
+
.option('--template <id>', 'Migration template ID for project creation')
|
|
35
54
|
.action(async (options) => {
|
|
36
55
|
const { initCommand } = await import('./cli/commands/init.js');
|
|
37
|
-
await initCommand({
|
|
56
|
+
await initCommand({
|
|
57
|
+
force: options.force ?? false,
|
|
58
|
+
guest: options.guest,
|
|
59
|
+
apiKey: options.apiKey,
|
|
60
|
+
projectName: options.projectName,
|
|
61
|
+
projectId: options.projectId,
|
|
62
|
+
template: options.template,
|
|
63
|
+
});
|
|
38
64
|
});
|
|
39
65
|
program
|
|
40
66
|
.command('stop')
|
|
@@ -50,6 +76,14 @@ program
|
|
|
50
76
|
const { statusCommand } = await import('./cli/commands/status.js');
|
|
51
77
|
await statusCommand();
|
|
52
78
|
});
|
|
79
|
+
program
|
|
80
|
+
.command('update-drg')
|
|
81
|
+
.description('Update the dependency relationship graph (DRG) for this project')
|
|
82
|
+
.option('-m, --mode <mode>', 'Update mode: baseline (all files) or incremental (git-changed only)', 'incremental')
|
|
83
|
+
.action(async (options) => {
|
|
84
|
+
const { updateDrgCommand } = await import('./cli/commands/update-drg.js');
|
|
85
|
+
await updateDrgCommand({ mode: options.mode });
|
|
86
|
+
});
|
|
53
87
|
program
|
|
54
88
|
.command('config [action] [key] [value]')
|
|
55
89
|
.description('Manage Shift configuration (URLs, API key)')
|
package/build/mcp-server.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
3
|
import { z } from 'zod';
|
|
4
|
+
import { createRequire } from 'module';
|
|
5
|
+
const require = createRequire(import.meta.url);
|
|
6
|
+
const { version } = require('../package.json');
|
|
4
7
|
const BASE_URL = process.env.SHIFT_BACKEND_URL || "https://dev-shift-lite.latentforce.ai";
|
|
5
8
|
function getProjectIdFromEnv() {
|
|
6
9
|
const projectId = process.env.SHIFT_PROJECT_ID;
|
|
@@ -17,6 +20,13 @@ function resolveProjectId(args) {
|
|
|
17
20
|
return fromArgs;
|
|
18
21
|
return getProjectIdFromEnv();
|
|
19
22
|
}
|
|
23
|
+
/** Normalize file paths: backslash → forward slash, strip leading ./ and / */
|
|
24
|
+
function normalizePath(filePath) {
|
|
25
|
+
let p = filePath.replace(/\\/g, '/');
|
|
26
|
+
p = p.replace(/^\.\//, '');
|
|
27
|
+
p = p.replace(/^\//, '');
|
|
28
|
+
return p;
|
|
29
|
+
}
|
|
20
30
|
// helper
|
|
21
31
|
async function callBackendAPI(endpoint, data) {
|
|
22
32
|
try {
|
|
@@ -29,20 +39,107 @@ async function callBackendAPI(endpoint, data) {
|
|
|
29
39
|
});
|
|
30
40
|
if (!response.ok) {
|
|
31
41
|
const text = await response.text();
|
|
42
|
+
// Provide actionable error messages
|
|
43
|
+
if (response.status === 404) {
|
|
44
|
+
if (text.includes('Knowledge graph not found') || text.includes('knowledge graph')) {
|
|
45
|
+
throw new Error("No knowledge graph exists for this project. Run 'shift-cli update-drg' to build it.");
|
|
46
|
+
}
|
|
47
|
+
if (text.includes('File not found') || text.includes('not found in')) {
|
|
48
|
+
// Extract path from error if possible
|
|
49
|
+
const pathMatch = text.match(/['"]([^'"]+)['"]/);
|
|
50
|
+
const filePath = pathMatch ? pathMatch[1] : 'the requested file';
|
|
51
|
+
throw new Error(`File '${filePath}' not found in knowledge graph. Possible causes:\n` +
|
|
52
|
+
` 1. The path may be incorrect (check casing and slashes)\n` +
|
|
53
|
+
` 2. The file was added after the last 'shift-cli update-drg'\n` +
|
|
54
|
+
` 3. The file type is not supported (only JS/TS files are indexed)`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
32
57
|
throw new Error(`API call failed: ${response.status} ${response.statusText}${text ? ` - ${text}` : ""}`);
|
|
33
58
|
}
|
|
34
59
|
return await response.json();
|
|
35
60
|
}
|
|
36
61
|
catch (error) {
|
|
37
|
-
console.error(`Error calling ${endpoint}:`, error);
|
|
38
62
|
throw error;
|
|
39
63
|
}
|
|
40
64
|
}
|
|
65
|
+
/** Format file_summary response as readable markdown */
|
|
66
|
+
function formatFileSummary(data) {
|
|
67
|
+
const lines = [];
|
|
68
|
+
lines.push(`## File: ${data.path}`);
|
|
69
|
+
lines.push('');
|
|
70
|
+
lines.push('### Summary');
|
|
71
|
+
lines.push(data.summary || 'No summary available.');
|
|
72
|
+
if (data.parent_summaries?.length > 0) {
|
|
73
|
+
lines.push('');
|
|
74
|
+
lines.push('### Directory Context');
|
|
75
|
+
for (const parent of data.parent_summaries) {
|
|
76
|
+
lines.push(`- **${parent.path}**: ${parent.summary}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return lines.join('\n');
|
|
80
|
+
}
|
|
81
|
+
/** Format dependency response as readable markdown */
|
|
82
|
+
function formatDependencies(data) {
|
|
83
|
+
const lines = [];
|
|
84
|
+
const deps = data.dependencies || [];
|
|
85
|
+
const edges = data.edge_summaries || {};
|
|
86
|
+
lines.push(`## Dependencies: ${data.path}`);
|
|
87
|
+
lines.push('');
|
|
88
|
+
if (deps.length === 0) {
|
|
89
|
+
lines.push('This file has no dependencies.');
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
lines.push(`This file depends on **${deps.length}** file(s):`);
|
|
93
|
+
lines.push('');
|
|
94
|
+
for (const dep of deps) {
|
|
95
|
+
const summary = edges[dep];
|
|
96
|
+
if (summary) {
|
|
97
|
+
lines.push(`- **${dep}**: ${summary}`);
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
lines.push(`- **${dep}**`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return lines.join('\n');
|
|
105
|
+
}
|
|
106
|
+
/** Format blast_radius response as readable markdown */
|
|
107
|
+
function formatBlastRadius(data) {
|
|
108
|
+
const lines = [];
|
|
109
|
+
const affected = data.affected_files || [];
|
|
110
|
+
lines.push(`## Blast Radius: ${data.path}`);
|
|
111
|
+
lines.push('');
|
|
112
|
+
if (affected.length === 0) {
|
|
113
|
+
lines.push('No other files are affected by changes to this file.');
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
lines.push(`**${affected.length}** file(s) would be affected:`);
|
|
117
|
+
lines.push('');
|
|
118
|
+
// Group by level
|
|
119
|
+
const byLevel = {};
|
|
120
|
+
for (const file of affected) {
|
|
121
|
+
const level = file.level || 1;
|
|
122
|
+
if (!byLevel[level])
|
|
123
|
+
byLevel[level] = [];
|
|
124
|
+
byLevel[level].push(file);
|
|
125
|
+
}
|
|
126
|
+
const levels = Object.keys(byLevel).map(Number).sort((a, b) => a - b);
|
|
127
|
+
for (const level of levels) {
|
|
128
|
+
const label = level === 1 ? 'Level 1 (direct)' : `Level ${level}`;
|
|
129
|
+
lines.push(`### ${label}`);
|
|
130
|
+
for (const file of byLevel[level]) {
|
|
131
|
+
lines.push(`- **${file.path}**: ${file.summary || 'No summary'}`);
|
|
132
|
+
}
|
|
133
|
+
lines.push('');
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return lines.join('\n');
|
|
137
|
+
}
|
|
41
138
|
export async function startMcpServer() {
|
|
42
139
|
// Create server instance
|
|
43
140
|
const server = new McpServer({
|
|
44
141
|
name: "shift",
|
|
45
|
-
version
|
|
142
|
+
version,
|
|
46
143
|
});
|
|
47
144
|
// Tools:
|
|
48
145
|
// Blast radius
|
|
@@ -56,7 +153,7 @@ export async function startMcpServer() {
|
|
|
56
153
|
}, async (args) => {
|
|
57
154
|
const projectId = resolveProjectId(args);
|
|
58
155
|
const data = await callBackendAPI('/api/v1/mcp/blast-radius', {
|
|
59
|
-
path: args.file_path,
|
|
156
|
+
path: normalizePath(args.file_path),
|
|
60
157
|
project_id: projectId,
|
|
61
158
|
...(args.level != null && { level: args.level }),
|
|
62
159
|
});
|
|
@@ -64,7 +161,7 @@ export async function startMcpServer() {
|
|
|
64
161
|
content: [
|
|
65
162
|
{
|
|
66
163
|
type: "text",
|
|
67
|
-
text:
|
|
164
|
+
text: formatBlastRadius(data)
|
|
68
165
|
},
|
|
69
166
|
],
|
|
70
167
|
};
|
|
@@ -79,14 +176,14 @@ export async function startMcpServer() {
|
|
|
79
176
|
}, async (args) => {
|
|
80
177
|
const projectId = resolveProjectId(args);
|
|
81
178
|
const data = await callBackendAPI('/api/v1/mcp/dependency', {
|
|
82
|
-
path: args.file_path,
|
|
179
|
+
path: normalizePath(args.file_path),
|
|
83
180
|
project_id: projectId,
|
|
84
181
|
});
|
|
85
182
|
return {
|
|
86
183
|
content: [
|
|
87
184
|
{
|
|
88
185
|
type: "text",
|
|
89
|
-
text:
|
|
186
|
+
text: formatDependencies(data)
|
|
90
187
|
},
|
|
91
188
|
],
|
|
92
189
|
};
|
|
@@ -102,7 +199,7 @@ export async function startMcpServer() {
|
|
|
102
199
|
}, async (args) => {
|
|
103
200
|
const projectId = resolveProjectId(args);
|
|
104
201
|
const data = await callBackendAPI('/api/v1/mcp/what-is-this-file', {
|
|
105
|
-
path: args.file_path,
|
|
202
|
+
path: normalizePath(args.file_path),
|
|
106
203
|
project_id: projectId,
|
|
107
204
|
level: args.level ?? 0,
|
|
108
205
|
});
|
|
@@ -110,7 +207,7 @@ export async function startMcpServer() {
|
|
|
110
207
|
content: [
|
|
111
208
|
{
|
|
112
209
|
type: "text",
|
|
113
|
-
text:
|
|
210
|
+
text: formatFileSummary(data)
|
|
114
211
|
},
|
|
115
212
|
],
|
|
116
213
|
};
|