@latentforce/latentgraph 1.0.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.
@@ -0,0 +1,484 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { z } from 'zod';
4
+ import { createRequire } from 'module';
5
+ import { getApiKey } from './utils/config.js';
6
+ const require = createRequire(import.meta.url);
7
+ const { version } = require('../package.json');
8
+ const BASE_URL = process.env.SHIFT_BACKEND_URL || "https://dev-shift-lite.latentforce.ai";
9
+ function getApiKeyFromEnv() {
10
+ return process.env.SHIFT_API_KEY?.trim() || getApiKey();
11
+ }
12
+ function getProjectIdFromEnv() {
13
+ const projectId = process.env.SHIFT_PROJECT_ID;
14
+ if (!projectId || projectId.trim() === "") {
15
+ throw new Error("SHIFT_PROJECT_ID environment variable is not set. " +
16
+ "Set it to your Shift Lite project UUID, or pass project_id in each tool call.");
17
+ }
18
+ return projectId.trim();
19
+ }
20
+ /** Resolve project_id: use tool arg if provided, else fall back to env. */
21
+ function resolveProjectId(args) {
22
+ const fromArgs = args.project_id?.trim();
23
+ if (fromArgs)
24
+ return fromArgs;
25
+ return getProjectIdFromEnv();
26
+ }
27
+ /** Normalize file paths: backslash → forward slash, strip leading ./ and / */
28
+ function normalizePath(filePath) {
29
+ let p = filePath.replace(/\\/g, '/');
30
+ p = p.replace(/^\.\//, '');
31
+ p = p.replace(/^\//, '');
32
+ return p;
33
+ }
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 'lgraph 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 'lgraph 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 */
58
+ async function callBackendAPI(endpoint, data) {
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);
67
+ }
68
+ return await response.json();
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 =============
85
+ /** Format file_summary response as readable markdown */
86
+ function formatFileSummary(data) {
87
+ const lines = [];
88
+ lines.push(`## File: ${data.path}`);
89
+ if (data.module_name) {
90
+ lines.push(`**Module:** ${data.module_name}`);
91
+ }
92
+ lines.push('');
93
+ lines.push('### Summary');
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
+ }
146
+ if (data.parent_summaries?.length > 0) {
147
+ lines.push('');
148
+ lines.push('### Directory Context');
149
+ for (const parent of data.parent_summaries) {
150
+ lines.push(`- **${parent.path}**: ${parent.summary}`);
151
+ }
152
+ }
153
+ return lines.join('\n');
154
+ }
155
+ /** Format dependency response as readable markdown */
156
+ function formatDependencies(data) {
157
+ const lines = [];
158
+ const deps = data.dependencies || [];
159
+ // edge_details is now a dict of {imports, usage_pattern, data_flow, dependency_summary}
160
+ const edgeDetails = data.edge_details || {};
161
+ lines.push(`## Dependencies: ${data.path}`);
162
+ if (data.module_name) {
163
+ lines.push(`**Module:** ${data.module_name}`);
164
+ }
165
+ lines.push('');
166
+ // --- Forward dependencies ---
167
+ if (deps.length === 0) {
168
+ lines.push('This file has no dependencies.');
169
+ }
170
+ else {
171
+ lines.push(`### Imports (${deps.length} file(s) this file depends on)`);
172
+ lines.push('');
173
+ for (const dep of deps) {
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}`);
185
+ }
186
+ else {
187
+ lines.push(`- **${dep}**`);
188
+ }
189
+ }
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
+ }
228
+ return lines.join('\n');
229
+ }
230
+ /** Format blast_radius response as readable markdown */
231
+ function formatBlastRadius(data) {
232
+ const lines = [];
233
+ const affected = data.affected_files || [];
234
+ lines.push(`## Blast Radius: ${data.path}`);
235
+ if (data.module_name) {
236
+ lines.push(`**Module:** ${data.module_name}`);
237
+ }
238
+ lines.push('');
239
+ if (affected.length === 0) {
240
+ lines.push('No other files are affected by changes to this file.');
241
+ }
242
+ else {
243
+ lines.push(`**${affected.length}** file(s) would be affected:`);
244
+ lines.push('');
245
+ // Group by level
246
+ const byLevel = {};
247
+ for (const file of affected) {
248
+ const level = file.level || 1;
249
+ if (!byLevel[level])
250
+ byLevel[level] = [];
251
+ byLevel[level].push(file);
252
+ }
253
+ const levels = Object.keys(byLevel).map(Number).sort((a, b) => a - b);
254
+ for (const level of levels) {
255
+ lines.push(`### ${level === 1 ? 'Level 1 (direct)' : `Level ${level}`}`);
256
+ for (const file of byLevel[level]) {
257
+ lines.push(`- **${file.path}**: ${file.summary || 'No summary'}`);
258
+ }
259
+ lines.push('');
260
+ }
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
+ }
270
+ return lines.join('\n');
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 =============
396
+ export async function startMcpServer() {
397
+ const server = new McpServer({
398
+ name: "shift",
399
+ version,
400
+ });
401
+ // ---- blast_radius ----
402
+ server.registerTool("blast_radius", {
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.",
404
+ inputSchema: z.object({
405
+ file_path: z.string().describe("Path to the file (relative to project root)"),
406
+ project_id: z.string().optional().describe("Shift Lite project UUID; overrides SHIFT_PROJECT_ID if provided"),
407
+ level: z.number().optional().describe("Max depth of blast radius (optional)"),
408
+ })
409
+ }, async (args) => {
410
+ const projectId = resolveProjectId(args);
411
+ const data = await callBackendAPI('/api/v1/mcp/blast-radius', {
412
+ path: normalizePath(args.file_path),
413
+ project_id: projectId,
414
+ ...(args.level != null && { level: args.level }),
415
+ });
416
+ return { content: [{ type: "text", text: formatBlastRadius(data) }] };
417
+ });
418
+ // ---- dependencies ----
419
+ server.registerTool("dependencies", {
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.",
421
+ inputSchema: z.object({
422
+ file_path: z.string().describe("Path to the file"),
423
+ project_id: z.string().optional().describe("Shift Lite project UUID; overrides SHIFT_PROJECT_ID if provided"),
424
+ })
425
+ }, async (args) => {
426
+ const projectId = resolveProjectId(args);
427
+ const data = await callBackendAPI('/api/v1/mcp/dependency', {
428
+ path: normalizePath(args.file_path),
429
+ project_id: projectId,
430
+ });
431
+ return { content: [{ type: "text", text: formatDependencies(data) }] };
432
+ });
433
+ // ---- file_summary ----
434
+ server.registerTool("file_summary", {
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.",
436
+ inputSchema: z.object({
437
+ file_path: z.string().describe("Path to the file to summarize"),
438
+ project_id: z.string().optional().describe("Shift Lite project UUID; overrides SHIFT_PROJECT_ID if provided"),
439
+ level: z.number().optional().describe("Number of parent directory levels to include (default 0)"),
440
+ })
441
+ }, async (args) => {
442
+ const projectId = resolveProjectId(args);
443
+ const data = await callBackendAPI('/api/v1/mcp/what-is-this-file', {
444
+ path: normalizePath(args.file_path),
445
+ project_id: projectId,
446
+ level: args.level ?? 0,
447
+ });
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) }] };
477
+ });
478
+ const transport = new StdioServerTransport();
479
+ await server.connect(transport);
480
+ console.error("Shift MCP Server running on stdio");
481
+ if (!process.env.SHIFT_PROJECT_ID) {
482
+ console.error("Warning: SHIFT_PROJECT_ID is not set. Pass project_id in each tool call, or set the env var.");
483
+ }
484
+ }
@@ -0,0 +1,147 @@
1
+ import { API_BASE_URL, API_BASE_URL_ORCH } from './config.js';
2
+ import { getMachineFingerprint } from './machine-id.js';
3
+ /**
4
+ * Request a guest API key bound to this machine
5
+ * The backend will:
6
+ * - Create a new guest key if this machine_id hasn't been seen before
7
+ * - Return the existing guest key if this machine_id already has one
8
+ * - Optionally enforce usage limits per machine
9
+ */
10
+ export async function requestGuestKey() {
11
+ const fingerprint = getMachineFingerprint();
12
+ const response = await fetch(`${API_BASE_URL}/api/guest-key`, {
13
+ method: 'POST',
14
+ headers: {
15
+ 'Content-Type': 'application/json',
16
+ },
17
+ body: JSON.stringify({
18
+ machine_id: fingerprint.machine_id,
19
+ platform: fingerprint.platform,
20
+ }),
21
+ });
22
+ if (!response.ok) {
23
+ const text = await response.text();
24
+ throw new Error(text || `HTTP ${response.status}`);
25
+ }
26
+ return await response.json();
27
+ }
28
+ /**
29
+ * Fetch available projects for the user
30
+ * Matching extension's fetchProjects function in api-client.js
31
+ */
32
+ export async function fetchProjects(apiKey) {
33
+ const response = await fetch(`${API_BASE_URL}/api/v1/mcp/projects`, {
34
+ method: 'GET',
35
+ headers: {
36
+ 'Authorization': `Bearer ${apiKey}`,
37
+ },
38
+ });
39
+ if (!response.ok) {
40
+ const text = await response.text();
41
+ throw new Error(text || `HTTP ${response.status}`);
42
+ }
43
+ const data = await response.json();
44
+ return data.projects || [];
45
+ }
46
+ /**
47
+ * Send init scan to backend
48
+ * Matching extension's init-scan API call
49
+ */
50
+ export async function sendInitScan(apiKey, projectId, payload) {
51
+ try {
52
+ // Ensure proper URL construction with leading slash
53
+ const path = `/api/projects/${projectId}/init-scan`;
54
+ const url = new URL(path, API_BASE_URL_ORCH).toString();
55
+ const response = await fetch(url, {
56
+ method: 'POST',
57
+ headers: {
58
+ 'Authorization': `Bearer ${apiKey}`,
59
+ 'Content-Type': 'application/json',
60
+ },
61
+ body: JSON.stringify(payload),
62
+ });
63
+ if (!response.ok) {
64
+ const text = await response.text();
65
+ throw new Error(text || `HTTP ${response.status}`);
66
+ }
67
+ return await response.json();
68
+ }
69
+ catch (error) {
70
+ // Re-throw for caller to handle; keep stack/context
71
+ throw error;
72
+ }
73
+ }
74
+ /**
75
+ * Send update-drg request to backend
76
+ * Calls POST /api/v1/mcp/update-drg
77
+ */
78
+ export async function sendUpdateDrg(apiKey, payload) {
79
+ const response = await fetch(`${API_BASE_URL}/api/v1/mcp/update-drg`, {
80
+ method: 'POST',
81
+ headers: {
82
+ 'Authorization': `Bearer ${apiKey}`,
83
+ 'Content-Type': 'application/json',
84
+ },
85
+ body: JSON.stringify(payload),
86
+ });
87
+ if (!response.ok) {
88
+ const text = await response.text();
89
+ throw new Error(text || `HTTP ${response.status}`);
90
+ }
91
+ return await response.json();
92
+ }
93
+ export async function fetchMigrationTemplates(apiKey) {
94
+ const response = await fetch(`${API_BASE_URL}/api/v1/mcp/migration-templates`, {
95
+ method: 'GET',
96
+ headers: { 'Authorization': `Bearer ${apiKey}` },
97
+ });
98
+ if (!response.ok) {
99
+ const text = await response.text();
100
+ throw new Error(text || `HTTP ${response.status}`);
101
+ }
102
+ const data = await response.json();
103
+ return data.templates || [];
104
+ }
105
+ export async function createProject(apiKey, params) {
106
+ const response = await fetch(`${API_BASE_URL}/api/v1/mcp/projects/create`, {
107
+ method: 'POST',
108
+ headers: {
109
+ 'Authorization': `Bearer ${apiKey}`,
110
+ 'Content-Type': 'application/json',
111
+ },
112
+ body: JSON.stringify(params),
113
+ });
114
+ if (!response.ok) {
115
+ const text = await response.text();
116
+ throw new Error(text || `HTTP ${response.status}`);
117
+ }
118
+ const data = await response.json();
119
+ return data.project || data;
120
+ }
121
+ /**
122
+ * Fetch project indexing/work status
123
+ * Returns whether the knowledge graph has been built for this project
124
+ */
125
+ export async function fetchProjectStatus(apiKey, projectId) {
126
+ const controller = new AbortController();
127
+ const timeoutId = setTimeout(() => controller.abort(), 5000); // 5 second timeout
128
+ try {
129
+ const response = await fetch(`${API_BASE_URL}/api/v1/mcp/projects/${projectId}/status`, {
130
+ method: 'GET',
131
+ headers: {
132
+ 'Authorization': `Bearer ${apiKey}`,
133
+ },
134
+ signal: controller.signal,
135
+ });
136
+ clearTimeout(timeoutId);
137
+ if (!response.ok) {
138
+ const text = await response.text();
139
+ throw new Error(text || `HTTP ${response.status}`);
140
+ }
141
+ return await response.json();
142
+ }
143
+ catch (error) {
144
+ clearTimeout(timeoutId);
145
+ throw error;
146
+ }
147
+ }