partnercore-proxy 0.1.5 → 0.4.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.
@@ -43,6 +43,9 @@ exports.ToolRouter = void 0;
43
43
  const fs = __importStar(require("fs"));
44
44
  const path = __importStar(require("path"));
45
45
  const types_js_1 = require("../config/types.js");
46
+ const project_memory_js_1 = require("../memory/project-memory.js");
47
+ const bc_container_js_1 = require("../container/bc-container.js");
48
+ const git_operations_js_1 = require("../git/git-operations.js");
46
49
  const logger_js_1 = require("../utils/logger.js");
47
50
  const loader_js_1 = require("../config/loader.js");
48
51
  const security_js_1 = require("../utils/security.js");
@@ -53,6 +56,9 @@ class ToolRouter {
53
56
  routing;
54
57
  alServer = null;
55
58
  cloudClient = null;
59
+ projectMemory = null;
60
+ containerManager = null;
61
+ gitOperations = null;
56
62
  workspaceRoot;
57
63
  logger = (0, logger_js_1.getLogger)();
58
64
  localToolDefinitions = [];
@@ -165,6 +171,44 @@ class ToolRouter {
165
171
  return this.handleHover(call.arguments);
166
172
  case 'al_completion':
167
173
  return this.handleCompletion(call.arguments);
174
+ // New LSP tools (Code Actions, Signature Help, Formatting)
175
+ case 'al_code_actions':
176
+ return this.handleCodeActions(call.arguments);
177
+ case 'al_signature_help':
178
+ return this.handleSignatureHelp(call.arguments);
179
+ case 'al_format':
180
+ return this.handleFormat(call.arguments);
181
+ // Additional LSP tools (complete coverage)
182
+ case 'al_document_highlight':
183
+ return this.handleDocumentHighlight(call.arguments);
184
+ case 'al_folding_ranges':
185
+ return this.handleFoldingRanges(call.arguments);
186
+ case 'al_selection_range':
187
+ return this.handleSelectionRange(call.arguments);
188
+ case 'al_type_definition':
189
+ return this.handleTypeDefinition(call.arguments);
190
+ case 'al_implementation':
191
+ return this.handleImplementation(call.arguments);
192
+ case 'al_format_on_type':
193
+ return this.handleFormatOnType(call.arguments);
194
+ case 'al_code_lens':
195
+ return this.handleCodeLens(call.arguments);
196
+ case 'al_document_links':
197
+ return this.handleDocumentLinks(call.arguments);
198
+ case 'al_execute_command':
199
+ return this.handleExecuteCommand(call.arguments);
200
+ case 'al_semantic_tokens':
201
+ return this.handleSemanticTokens(call.arguments);
202
+ case 'al_close_document':
203
+ return this.handleCloseDocument(call.arguments);
204
+ case 'al_save_document':
205
+ return this.handleSaveDocument(call.arguments);
206
+ case 'al_restart_server':
207
+ return this.handleRestartServer();
208
+ case 'al_find_referencing_symbols':
209
+ return this.handleFindReferencingSymbols(call.arguments);
210
+ case 'al_insert_before_symbol':
211
+ return this.handleInsertBeforeSymbol(call.arguments);
168
212
  case 'read_file':
169
213
  return this.handleReadFile(call.arguments);
170
214
  case 'write_file':
@@ -173,8 +217,87 @@ class ToolRouter {
173
217
  return this.handleListFiles(call.arguments);
174
218
  case 'search_files':
175
219
  return this.handleSearchFiles(call.arguments);
220
+ case 'find_file':
221
+ return this.handleFindFile(call.arguments);
222
+ case 'replace_content':
223
+ return this.handleReplaceContent(call.arguments);
176
224
  case 'al_get_started':
177
225
  return this.handleGetStarted();
226
+ // Symbol-based editing tools
227
+ case 'al_rename_symbol':
228
+ return this.handleRenameSymbol(call.arguments);
229
+ case 'al_insert_after_symbol':
230
+ return this.handleInsertAfterSymbol(call.arguments);
231
+ case 'al_replace_symbol_body':
232
+ return this.handleReplaceSymbolBody(call.arguments);
233
+ // Advanced file operations
234
+ case 'delete_lines':
235
+ return this.handleDeleteLines(call.arguments);
236
+ case 'replace_lines':
237
+ return this.handleReplaceLines(call.arguments);
238
+ case 'insert_at_line':
239
+ return this.handleInsertAtLine(call.arguments);
240
+ // Project memory tools
241
+ case 'write_memory':
242
+ return this.handleWriteMemory(call.arguments);
243
+ case 'read_memory':
244
+ return this.handleReadMemory(call.arguments);
245
+ case 'list_memories':
246
+ return this.handleListMemories();
247
+ case 'delete_memory':
248
+ return this.handleDeleteMemory(call.arguments);
249
+ case 'edit_memory':
250
+ return this.handleEditMemory(call.arguments);
251
+ // BC Container tools
252
+ case 'bc_list_containers':
253
+ return this.handleListContainers();
254
+ case 'bc_compile':
255
+ return this.handleCompile(call.arguments);
256
+ case 'bc_publish':
257
+ return this.handlePublish(call.arguments);
258
+ case 'bc_run_tests':
259
+ return this.handleRunTests(call.arguments);
260
+ case 'bc_container_logs':
261
+ return this.handleContainerLogs(call.arguments);
262
+ case 'bc_start_container':
263
+ return this.handleStartContainer(call.arguments);
264
+ case 'bc_stop_container':
265
+ return this.handleStopContainer(call.arguments);
266
+ case 'bc_restart_container':
267
+ return this.handleRestartContainer(call.arguments);
268
+ case 'bc_download_symbols':
269
+ return this.handleDownloadSymbols(call.arguments);
270
+ case 'bc_create_container':
271
+ return this.handleCreateContainer(call.arguments);
272
+ case 'bc_remove_container':
273
+ return this.handleRemoveContainer(call.arguments);
274
+ case 'bc_get_extensions':
275
+ return this.handleGetExtensions(call.arguments);
276
+ case 'bc_uninstall_app':
277
+ return this.handleUninstallApp(call.arguments);
278
+ case 'bc_compile_warnings':
279
+ return this.handleCompileWarnings(call.arguments);
280
+ // Git tools
281
+ case 'git_status':
282
+ return this.handleGitStatus();
283
+ case 'git_diff':
284
+ return this.handleGitDiff(call.arguments);
285
+ case 'git_stage':
286
+ return this.handleGitStage(call.arguments);
287
+ case 'git_commit':
288
+ return this.handleGitCommit(call.arguments);
289
+ case 'git_log':
290
+ return this.handleGitLog(call.arguments);
291
+ case 'git_branches':
292
+ return this.handleGitBranches(call.arguments);
293
+ case 'git_checkout':
294
+ return this.handleGitCheckout(call.arguments);
295
+ case 'git_pull':
296
+ return this.handleGitPull(call.arguments);
297
+ case 'git_push':
298
+ return this.handleGitPush(call.arguments);
299
+ case 'git_stash':
300
+ return this.handleGitStash(call.arguments);
178
301
  default:
179
302
  return {
180
303
  success: false,
@@ -296,251 +419,2209 @@ class ToolRouter {
296
419
  const completions = await this.alServer.getCompletions(uri, line, character);
297
420
  return { success: true, content: completions };
298
421
  }
299
- // ==================== File System Tool Handlers ====================
300
- // All file operations are sandboxed to the workspace root for security
301
- handleReadFile(args) {
302
- // Validate required arguments
303
- (0, security_js_1.validateToolArgs)(args, ['path'], { path: 'string' });
304
- const filePath = (0, security_js_1.sanitizeString)(args['path']);
305
- // Detect workspace dynamically if needed
306
- const workspaceRoot = this.getWorkspaceRoot(filePath);
307
- // Security: Sanitize path to prevent directory traversal
308
- const resolved = (0, security_js_1.sanitizePath)(filePath, workspaceRoot);
309
- if (!fs.existsSync(resolved)) {
310
- return { success: false, content: `File not found: ${filePath}`, isError: true };
422
+ // ==================== New LSP Tool Handlers ====================
423
+ async handleCodeActions(args) {
424
+ if (!this.alServer) {
425
+ return { success: false, content: 'AL Language Server not initialized', isError: true };
311
426
  }
312
- const stat = fs.statSync(resolved);
313
- if (!stat.isFile()) {
314
- return { success: false, content: `Not a file: ${filePath}`, isError: true };
427
+ (0, security_js_1.validateToolArgs)(args, ['uri'], { uri: 'string' });
428
+ const uri = args['uri'];
429
+ // If line/character provided, create a point range; otherwise use full diagnostics
430
+ let range;
431
+ if (args['line'] !== undefined && args['character'] !== undefined) {
432
+ const line = args['line'];
433
+ const character = args['character'];
434
+ range = { start: { line, character }, end: { line, character } };
315
435
  }
316
- // Security: Limit file size to prevent memory exhaustion
317
- const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
318
- if (stat.size > MAX_FILE_SIZE) {
319
- return { success: false, content: `File too large (max ${MAX_FILE_SIZE} bytes)`, isError: true };
436
+ else if (args['startLine'] !== undefined) {
437
+ // Range provided
438
+ range = {
439
+ start: { line: args['startLine'], character: args['startCharacter'] || 0 },
440
+ end: { line: args['endLine'] || args['startLine'], character: args['endCharacter'] || 0 },
441
+ };
320
442
  }
321
- const content = fs.readFileSync(resolved, 'utf-8');
322
- return { success: true, content };
323
- }
324
- handleWriteFile(args) {
325
- // Validate required arguments
326
- (0, security_js_1.validateToolArgs)(args, ['path', 'content'], { path: 'string', content: 'string' });
327
- const filePath = (0, security_js_1.sanitizeString)(args['path']);
328
- const content = args['content'];
329
- // Detect workspace dynamically if needed
330
- const workspaceRoot = this.getWorkspaceRoot(filePath);
331
- // Security: Sanitize path to prevent directory traversal
332
- const resolved = (0, security_js_1.sanitizePath)(filePath, workspaceRoot);
333
- const dir = path.dirname(resolved);
334
- // Security: Limit content size
335
- const MAX_CONTENT_SIZE = 10 * 1024 * 1024; // 10MB
336
- if (content.length > MAX_CONTENT_SIZE) {
337
- return { success: false, content: `Content too large (max ${MAX_CONTENT_SIZE} bytes)`, isError: true };
443
+ else {
444
+ // Default to start of file
445
+ range = { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } };
338
446
  }
339
- fs.mkdirSync(dir, { recursive: true });
340
- fs.writeFileSync(resolved, content, 'utf-8');
341
- // Return relative path in response (don't expose full paths)
342
- const relativePath = path.relative(workspaceRoot, resolved);
343
- return { success: true, content: `File written: ${relativePath}` };
447
+ // Get diagnostics if we should include them
448
+ let diagnostics;
449
+ if (args['includeDiagnostics'] !== false) {
450
+ const diags = await this.alServer.getDiagnostics(uri);
451
+ diagnostics = diags.filter(d => {
452
+ // Filter to diagnostics that overlap with the range
453
+ return d.range.start.line <= range.end.line && d.range.end.line >= range.start.line;
454
+ });
455
+ }
456
+ const only = args['only'];
457
+ const actions = await this.alServer.getCodeActions(uri, range, { diagnostics, only });
458
+ return {
459
+ success: true,
460
+ content: {
461
+ count: actions.length,
462
+ actions: actions.map(a => ({
463
+ title: a.title,
464
+ kind: a.kind,
465
+ isPreferred: a.isPreferred,
466
+ hasEdit: !!a.edit,
467
+ hasCommand: !!a.command,
468
+ })),
469
+ details: actions,
470
+ }
471
+ };
344
472
  }
345
- handleListFiles(args) {
346
- // Validate required arguments
347
- (0, security_js_1.validateToolArgs)(args, ['path'], { path: 'string' });
348
- const dirPath = (0, security_js_1.sanitizeString)(args['path']);
349
- const pattern = args['pattern'] ? (0, security_js_1.sanitizeString)(args['pattern']) : undefined;
350
- // Detect workspace dynamically if needed
351
- const workspaceRoot = this.getWorkspaceRoot(dirPath);
352
- // Security: Sanitize path to prevent directory traversal
353
- const resolved = (0, security_js_1.sanitizePath)(dirPath, workspaceRoot);
354
- if (!fs.existsSync(resolved)) {
355
- return { success: false, content: `Directory not found: ${dirPath}`, isError: true };
473
+ async handleSignatureHelp(args) {
474
+ if (!this.alServer) {
475
+ return { success: false, content: 'AL Language Server not initialized', isError: true };
356
476
  }
357
- const stat = fs.statSync(resolved);
358
- if (!stat.isDirectory()) {
359
- return { success: false, content: `Not a directory: ${dirPath}`, isError: true };
477
+ (0, security_js_1.validateToolArgs)(args, ['uri', 'line', 'character'], {
478
+ uri: 'string',
479
+ line: 'number',
480
+ character: 'number'
481
+ });
482
+ const uri = args['uri'];
483
+ const line = args['line'];
484
+ const character = args['character'];
485
+ const help = await this.alServer.getSignatureHelp(uri, line, character);
486
+ if (!help) {
487
+ return { success: true, content: { message: 'No signature help available at this position' } };
360
488
  }
361
- const files = this.listFilesRecursive(resolved, pattern);
362
- // Return relative paths for security
363
- const relativePaths = files.map(f => path.relative(workspaceRoot, f));
364
- return { success: true, content: relativePaths };
489
+ return {
490
+ success: true,
491
+ content: {
492
+ activeSignature: help.activeSignature,
493
+ activeParameter: help.activeParameter,
494
+ signatures: help.signatures.map(sig => ({
495
+ label: sig.label,
496
+ documentation: sig.documentation,
497
+ parameters: sig.parameters?.map(p => ({
498
+ label: p.label,
499
+ documentation: p.documentation,
500
+ })),
501
+ })),
502
+ }
503
+ };
365
504
  }
366
- listFilesRecursive(dir, pattern, depth = 0) {
367
- // Security: Limit recursion depth
368
- const MAX_DEPTH = 20;
369
- if (depth > MAX_DEPTH)
370
- return [];
371
- // Use imports from top of file (fs and path are already imported)
372
- const results = [];
373
- let entries;
374
- try {
375
- entries = fs.readdirSync(dir, { withFileTypes: true });
505
+ async handleFormat(args) {
506
+ if (!this.alServer) {
507
+ return { success: false, content: 'AL Language Server not initialized', isError: true };
376
508
  }
377
- catch {
378
- // Silently skip directories we can't read
379
- return results;
509
+ (0, security_js_1.validateToolArgs)(args, ['uri'], { uri: 'string' });
510
+ const uri = args['uri'];
511
+ const tabSize = args['tabSize'];
512
+ const insertSpaces = args['insertSpaces'];
513
+ let edits;
514
+ // Check if range formatting is requested
515
+ if (args['startLine'] !== undefined) {
516
+ const range = {
517
+ start: {
518
+ line: args['startLine'],
519
+ character: args['startCharacter'] || 0
520
+ },
521
+ end: {
522
+ line: args['endLine'] || args['startLine'],
523
+ character: args['endCharacter'] || Number.MAX_SAFE_INTEGER
524
+ },
525
+ };
526
+ edits = await this.alServer.formatRange(uri, range, { tabSize, insertSpaces });
380
527
  }
381
- // Security: Limit total files to prevent DoS
382
- const MAX_FILES = 10000;
383
- for (const entry of entries) {
384
- if (results.length >= MAX_FILES)
385
- break;
386
- const fullPath = path.join(dir, entry.name);
387
- // Security: Skip hidden files/directories and common ignored paths
388
- if (entry.name.startsWith('.'))
389
- continue;
390
- if (entry.isDirectory()) {
391
- // Skip common ignored directories
392
- const ignoredDirs = ['node_modules', '.git', '.svn', 'dist', 'bin', 'obj', '.alpackages', '.snapshots'];
393
- if (!ignoredDirs.includes(entry.name)) {
394
- results.push(...this.listFilesRecursive(fullPath, pattern, depth + 1));
528
+ else {
529
+ // Format entire document
530
+ edits = await this.alServer.formatDocument(uri, { tabSize, insertSpaces });
531
+ }
532
+ if (edits.length === 0) {
533
+ return { success: true, content: { message: 'Document is already formatted', edits: [] } };
534
+ }
535
+ // If apply is true, apply the edits to the file
536
+ if (args['apply'] === true) {
537
+ const filePath = this.uriToPath(uri);
538
+ const workspaceRoot = this.getWorkspaceRoot(filePath);
539
+ const safePath = (0, security_js_1.sanitizePath)(filePath, workspaceRoot);
540
+ let content = fs.readFileSync(safePath, 'utf-8');
541
+ const lines = content.split('\n');
542
+ // Apply edits in reverse order to maintain positions
543
+ const sortedEdits = [...edits].sort((a, b) => {
544
+ if (a.range.start.line !== b.range.start.line) {
545
+ return b.range.start.line - a.range.start.line;
395
546
  }
547
+ return b.range.start.character - a.range.start.character;
548
+ });
549
+ for (const edit of sortedEdits) {
550
+ const startLine = edit.range.start.line;
551
+ const endLine = edit.range.end.line;
552
+ const startChar = edit.range.start.character;
553
+ const endChar = edit.range.end.character;
554
+ // Get the text before and after the edit range
555
+ const beforeText = lines.slice(0, startLine).join('\n') +
556
+ (startLine > 0 ? '\n' : '') +
557
+ lines[startLine].substring(0, startChar);
558
+ const afterText = lines[endLine].substring(endChar) +
559
+ (endLine < lines.length - 1 ? '\n' : '') +
560
+ lines.slice(endLine + 1).join('\n');
561
+ content = beforeText + edit.newText + afterText;
562
+ // Re-split for next iteration
563
+ const newLines = content.split('\n');
564
+ lines.length = 0;
565
+ lines.push(...newLines);
396
566
  }
397
- else {
398
- if (!pattern || this.matchesPattern(entry.name, pattern)) {
399
- results.push(fullPath);
567
+ fs.writeFileSync(safePath, content, 'utf-8');
568
+ return {
569
+ success: true,
570
+ content: {
571
+ message: `Applied ${edits.length} formatting edits`,
572
+ editsApplied: edits.length,
400
573
  }
574
+ };
575
+ }
576
+ return {
577
+ success: true,
578
+ content: {
579
+ message: `Found ${edits.length} formatting edits (use apply:true to apply)`,
580
+ edits: edits.map(e => ({
581
+ range: e.range,
582
+ newText: e.newText.length > 100 ? e.newText.substring(0, 100) + '...' : e.newText,
583
+ })),
401
584
  }
585
+ };
586
+ }
587
+ // ==================== Additional LSP Tool Handlers ====================
588
+ async handleDocumentHighlight(args) {
589
+ if (!this.alServer) {
590
+ return { success: false, content: 'AL Language Server not initialized', isError: true };
402
591
  }
403
- return results;
592
+ (0, security_js_1.validateToolArgs)(args, ['uri', 'line', 'character'], {
593
+ uri: 'string', line: 'number', character: 'number'
594
+ });
595
+ const uri = args['uri'];
596
+ const line = args['line'];
597
+ const character = args['character'];
598
+ const highlights = await this.alServer.getDocumentHighlights(uri, line, character);
599
+ return {
600
+ success: true,
601
+ content: {
602
+ count: highlights.length,
603
+ highlights: highlights.map(h => ({
604
+ range: h.range,
605
+ kind: h.kind || 'text',
606
+ })),
607
+ }
608
+ };
404
609
  }
405
- matchesPattern(filename, pattern) {
406
- // Security: Escape regex special characters except * and ?
407
- const escaped = pattern
408
- .replace(/[.+^${}()|[\]\\]/g, '\\$&')
409
- .replace(/\*/g, '.*')
410
- .replace(/\?/g, '.');
411
- const regex = new RegExp(`^${escaped}$`, 'i');
412
- return regex.test(filename);
610
+ async handleFoldingRanges(args) {
611
+ if (!this.alServer) {
612
+ return { success: false, content: 'AL Language Server not initialized', isError: true };
613
+ }
614
+ (0, security_js_1.validateToolArgs)(args, ['uri'], { uri: 'string' });
615
+ const uri = args['uri'];
616
+ const ranges = await this.alServer.getFoldingRanges(uri);
617
+ return {
618
+ success: true,
619
+ content: {
620
+ count: ranges.length,
621
+ ranges: ranges.map(r => ({
622
+ startLine: r.startLine,
623
+ endLine: r.endLine,
624
+ kind: r.kind,
625
+ })),
626
+ }
627
+ };
413
628
  }
414
- handleSearchFiles(args) {
415
- // Validate required arguments
416
- (0, security_js_1.validateToolArgs)(args, ['path', 'query'], { path: 'string', query: 'string' });
417
- const dirPath = (0, security_js_1.sanitizeString)(args['path']);
418
- const query = (0, security_js_1.sanitizeString)(args['query'], 1000); // Limit query length
419
- const filePattern = args['filePattern'] ? (0, security_js_1.sanitizeString)(args['filePattern']) : '*.al';
420
- // Detect workspace dynamically if needed
421
- const workspaceRoot = this.getWorkspaceRoot(dirPath);
422
- // Security: Sanitize path
423
- const resolved = (0, security_js_1.sanitizePath)(dirPath, workspaceRoot);
424
- const files = this.listFilesRecursive(resolved, filePattern);
425
- const results = [];
426
- const queryLower = query.toLowerCase();
427
- // Security: Limit results
428
- const MAX_RESULTS = 500;
429
- for (const file of files) {
430
- if (results.length >= MAX_RESULTS)
431
- break;
432
- try {
433
- const content = fs.readFileSync(file, 'utf-8');
434
- const lines = content.split('\n');
435
- lines.forEach((line, index) => {
436
- if (results.length >= MAX_RESULTS)
437
- return;
438
- if (line.toLowerCase().includes(queryLower)) {
439
- results.push({
440
- file: path.relative(workspaceRoot, file), // Return relative paths
441
- line: index + 1,
442
- content: line.trim().slice(0, 500), // Limit line length in results
443
- });
444
- }
445
- });
629
+ async handleSelectionRange(args) {
630
+ if (!this.alServer) {
631
+ return { success: false, content: 'AL Language Server not initialized', isError: true };
632
+ }
633
+ (0, security_js_1.validateToolArgs)(args, ['uri', 'positions'], { uri: 'string' });
634
+ const uri = args['uri'];
635
+ const positions = args['positions'];
636
+ const ranges = await this.alServer.getSelectionRanges(uri, positions);
637
+ // Flatten nested parents for cleaner output
638
+ const flattenRange = (sr, depth = 0) => {
639
+ const result = [{ depth, range: sr.range }];
640
+ if (sr.parent && depth < 10) {
641
+ result.push(...flattenRange(sr.parent, depth + 1));
446
642
  }
447
- catch {
448
- // Skip files we can't read
643
+ return result;
644
+ };
645
+ return {
646
+ success: true,
647
+ content: {
648
+ count: ranges.length,
649
+ ranges: ranges.map((r, i) => ({
650
+ position: positions[i],
651
+ selections: flattenRange(r),
652
+ })),
449
653
  }
654
+ };
655
+ }
656
+ async handleTypeDefinition(args) {
657
+ if (!this.alServer) {
658
+ return { success: false, content: 'AL Language Server not initialized', isError: true };
450
659
  }
451
- return { success: true, content: results };
660
+ (0, security_js_1.validateToolArgs)(args, ['uri', 'line', 'character'], {
661
+ uri: 'string', line: 'number', character: 'number'
662
+ });
663
+ const uri = args['uri'];
664
+ const line = args['line'];
665
+ const character = args['character'];
666
+ const locations = await this.alServer.getTypeDefinition(uri, line, character);
667
+ return {
668
+ success: true,
669
+ content: {
670
+ count: locations.length,
671
+ locations: locations.map(loc => ({
672
+ uri: loc.uri,
673
+ range: loc.range,
674
+ })),
675
+ }
676
+ };
452
677
  }
453
- // ==================== Getting Started Handler ====================
454
- async handleGetStarted() {
455
- const workspace = this.workspaceRoot || (0, loader_js_1.findALWorkspace)();
456
- const hasWorkspace = !!workspace;
457
- // Check AL Language Server status
458
- const alServerReady = this.alServer !== null;
459
- // Get cloud status and prompts
460
- let cloudConnected = false;
461
- let prompts = [];
462
- if (this.cloudClient) {
463
- try {
464
- cloudConnected = await this.cloudClient.checkConnection();
465
- prompts = await this.cloudClient.getPrompts();
678
+ async handleImplementation(args) {
679
+ if (!this.alServer) {
680
+ return { success: false, content: 'AL Language Server not initialized', isError: true };
681
+ }
682
+ (0, security_js_1.validateToolArgs)(args, ['uri', 'line', 'character'], {
683
+ uri: 'string', line: 'number', character: 'number'
684
+ });
685
+ const uri = args['uri'];
686
+ const line = args['line'];
687
+ const character = args['character'];
688
+ const locations = await this.alServer.getImplementation(uri, line, character);
689
+ return {
690
+ success: true,
691
+ content: {
692
+ count: locations.length,
693
+ locations: locations.map(loc => ({
694
+ uri: loc.uri,
695
+ range: loc.range,
696
+ })),
466
697
  }
467
- catch {
468
- cloudConnected = false;
698
+ };
699
+ }
700
+ async handleFormatOnType(args) {
701
+ if (!this.alServer) {
702
+ return { success: false, content: 'AL Language Server not initialized', isError: true };
703
+ }
704
+ (0, security_js_1.validateToolArgs)(args, ['uri', 'line', 'character', 'ch'], {
705
+ uri: 'string', line: 'number', character: 'number', ch: 'string'
706
+ });
707
+ const uri = args['uri'];
708
+ const line = args['line'];
709
+ const character = args['character'];
710
+ const ch = args['ch'];
711
+ const tabSize = args['tabSize'];
712
+ const insertSpaces = args['insertSpaces'];
713
+ const edits = await this.alServer.formatOnType(uri, line, character, ch, { tabSize, insertSpaces });
714
+ return {
715
+ success: true,
716
+ content: {
717
+ count: edits.length,
718
+ edits,
469
719
  }
720
+ };
721
+ }
722
+ async handleCodeLens(args) {
723
+ if (!this.alServer) {
724
+ return { success: false, content: 'AL Language Server not initialized', isError: true };
470
725
  }
471
- // Build the getting started response
472
- const response = {
473
- welcome: '🚀 Welcome to PartnerCore AL Development!',
474
- status: {
475
- workspace: hasWorkspace ? workspace : '⚠️ No AL workspace detected (no app.json found)',
476
- alLanguageServer: alServerReady ? '✅ Ready' : '⚠️ Not initialized',
477
- cloudConnection: cloudConnected ? '✅ Connected' : '⚠️ Offline (local tools only)',
478
- },
479
- availableWorkflows: prompts.slice(0, 15).map(p => ({
480
- name: p.name,
481
- description: p.description,
482
- usage: `Call al-assistant with action='get-prompt' and params={ prompt_name: '${p.name}' }`,
483
- })),
484
- quickStart: {
485
- step1: {
486
- description: 'Search knowledge base for best practices',
487
- tool: 'al-assistant',
488
- example: { action: 'learn', params: { action: 'search', query: 'table design patterns' } },
489
- },
490
- step2: {
491
- description: 'Get a template for your object type',
492
- tool: 'al-assistant',
493
- example: { action: 'get-prompt', params: { prompt_name: 'al-create-table' } },
494
- },
495
- step3: {
496
- description: 'Write the AL file',
497
- tool: 'write_file',
498
- example: { path: 'src/MyTable.Table.al', content: '// Generated AL code...' },
499
- },
500
- step4: {
501
- description: 'Check for compilation errors',
502
- tool: 'al_get_diagnostics',
503
- example: { uri: 'file:///path/to/MyTable.Table.al' },
504
- },
505
- step5: {
506
- description: 'Review code for AppSource compliance',
507
- tool: 'al-assistant',
508
- example: { action: 'review', params: { code: '// Your AL code...' } },
509
- },
510
- },
511
- localTools: [
512
- { name: 'read_file', description: 'Read file contents' },
513
- { name: 'write_file', description: 'Write/create files' },
514
- { name: 'list_files', description: 'List directory contents' },
515
- { name: 'search_files', description: 'Search text in files' },
516
- { name: 'al_get_diagnostics', description: 'Get compiler errors/warnings' },
517
- { name: 'al_find_symbol', description: 'Search symbols in workspace' },
518
- { name: 'al_get_symbols', description: 'Get symbols in a file' },
519
- { name: 'al_completion', description: 'Get code completions' },
520
- { name: 'al_hover', description: 'Get type info at position' },
521
- ],
522
- cloudTools: cloudConnected ? [
523
- { name: 'al-assistant learn', description: 'Search knowledge base (674 articles)' },
524
- { name: 'al-assistant generate', description: 'Get code generation guidance' },
525
- { name: 'al-assistant get-prompt', description: 'Get structured prompts (29 available)' },
526
- { name: 'al-assistant review', description: 'Code review (252+ patterns)' },
527
- { name: 'al-assistant validate', description: 'AppSource compliance check' },
528
- ] : [],
529
- tips: [
530
- '💡 Always run al_get_diagnostics after writing AL files',
531
- '💡 Use al-assistant learn before creating new objects',
532
- '💡 Use al-assistant review for AppSource compliance',
533
- '💡 Include DataClassification on all table fields',
534
- ],
726
+ (0, security_js_1.validateToolArgs)(args, ['uri'], { uri: 'string' });
727
+ const uri = args['uri'];
728
+ const resolve = args['resolve'];
729
+ let lenses = await this.alServer.getCodeLenses(uri);
730
+ // Optionally resolve all lenses
731
+ if (resolve) {
732
+ lenses = await Promise.all(lenses.map(lens => this.alServer.resolveCodeLens(lens)));
733
+ }
734
+ return {
735
+ success: true,
736
+ content: {
737
+ count: lenses.length,
738
+ lenses: lenses.map(l => ({
739
+ range: l.range,
740
+ command: l.command?.title,
741
+ })),
742
+ }
535
743
  };
536
- return { success: true, content: response };
537
744
  }
538
- // ==================== Tool Definitions ====================
539
- initLocalToolDefinitions() {
540
- this.localToolDefinitions = [
745
+ async handleDocumentLinks(args) {
746
+ if (!this.alServer) {
747
+ return { success: false, content: 'AL Language Server not initialized', isError: true };
748
+ }
749
+ (0, security_js_1.validateToolArgs)(args, ['uri'], { uri: 'string' });
750
+ const uri = args['uri'];
751
+ const links = await this.alServer.getDocumentLinks(uri);
752
+ return {
753
+ success: true,
754
+ content: {
755
+ count: links.length,
756
+ links: links.map(l => ({
757
+ range: l.range,
758
+ target: l.target,
759
+ tooltip: l.tooltip,
760
+ })),
761
+ }
762
+ };
763
+ }
764
+ async handleExecuteCommand(args) {
765
+ if (!this.alServer) {
766
+ return { success: false, content: 'AL Language Server not initialized', isError: true };
767
+ }
768
+ (0, security_js_1.validateToolArgs)(args, ['command'], { command: 'string' });
769
+ const command = args['command'];
770
+ const commandArgs = args['arguments'];
771
+ try {
772
+ const result = await this.alServer.executeCommand(command, commandArgs);
773
+ return { success: true, content: { result } };
774
+ }
775
+ catch (error) {
776
+ const errorMessage = error instanceof Error ? error.message : String(error);
777
+ return {
778
+ success: false,
779
+ content: `Command execution failed: ${errorMessage}`,
780
+ isError: true
781
+ };
782
+ }
783
+ }
784
+ async handleSemanticTokens(args) {
785
+ if (!this.alServer) {
786
+ return { success: false, content: 'AL Language Server not initialized', isError: true };
787
+ }
788
+ (0, security_js_1.validateToolArgs)(args, ['uri'], { uri: 'string' });
789
+ const uri = args['uri'];
790
+ // Check if range is provided
791
+ if (args['startLine'] !== undefined) {
792
+ const range = {
793
+ start: {
794
+ line: args['startLine'],
795
+ character: args['startCharacter'] || 0
796
+ },
797
+ end: {
798
+ line: args['endLine'] || args['startLine'],
799
+ character: args['endCharacter'] || Number.MAX_SAFE_INTEGER
800
+ },
801
+ };
802
+ const tokens = await this.alServer.getSemanticTokensRange(uri, range);
803
+ if (!tokens) {
804
+ return { success: true, content: { message: 'No semantic tokens available' } };
805
+ }
806
+ return {
807
+ success: true,
808
+ content: {
809
+ resultId: tokens.resultId,
810
+ tokenCount: tokens.data.length / 5, // Each token is 5 integers
811
+ data: tokens.data.slice(0, 100), // Limit output
812
+ }
813
+ };
814
+ }
815
+ const tokens = await this.alServer.getSemanticTokens(uri);
816
+ if (!tokens) {
817
+ return { success: true, content: { message: 'No semantic tokens available' } };
818
+ }
819
+ return {
820
+ success: true,
821
+ content: {
822
+ resultId: tokens.resultId,
823
+ tokenCount: tokens.data.length / 5,
824
+ data: tokens.data.slice(0, 100), // Limit output for readability
825
+ }
826
+ };
827
+ }
828
+ async handleCloseDocument(args) {
829
+ if (!this.alServer) {
830
+ return { success: false, content: 'AL Language Server not initialized', isError: true };
831
+ }
832
+ (0, security_js_1.validateToolArgs)(args, ['uri'], { uri: 'string' });
833
+ const uri = args['uri'];
834
+ await this.alServer.closeDocument(uri);
835
+ return { success: true, content: { message: `Document closed: ${uri}` } };
836
+ }
837
+ async handleSaveDocument(args) {
838
+ if (!this.alServer) {
839
+ return { success: false, content: 'AL Language Server not initialized', isError: true };
840
+ }
841
+ (0, security_js_1.validateToolArgs)(args, ['uri'], { uri: 'string' });
842
+ const uri = args['uri'];
843
+ const text = args['text'];
844
+ await this.alServer.saveDocument(uri, text);
845
+ return { success: true, content: { message: `Document save notification sent: ${uri}` } };
846
+ }
847
+ // ==================== Extended Tool Handlers ====================
848
+ async handleRestartServer() {
849
+ if (!this.alServer) {
850
+ return { success: false, content: 'AL Language Server not initialized', isError: true };
851
+ }
852
+ try {
853
+ await this.alServer.restart();
854
+ return { success: true, content: { message: 'AL Language Server restarted successfully' } };
855
+ }
856
+ catch (error) {
857
+ const errorMessage = error instanceof Error ? error.message : String(error);
858
+ return {
859
+ success: false,
860
+ content: `Failed to restart AL Language Server: ${errorMessage}`,
861
+ isError: true
862
+ };
863
+ }
864
+ }
865
+ async handleFindReferencingSymbols(args) {
866
+ if (!this.alServer) {
867
+ return { success: false, content: 'AL Language Server not initialized', isError: true };
868
+ }
869
+ (0, security_js_1.validateToolArgs)(args, ['uri', 'line', 'character'], {
870
+ uri: 'string', line: 'number', character: 'number'
871
+ });
872
+ const uri = args['uri'];
873
+ const line = args['line'];
874
+ const character = args['character'];
875
+ const includeDeclaration = args['includeDeclaration'];
876
+ const contextLinesBefore = args['contextLinesBefore'];
877
+ const contextLinesAfter = args['contextLinesAfter'];
878
+ const results = await this.alServer.findReferencingSymbols(uri, line, character, {
879
+ includeDeclaration,
880
+ contextLinesBefore,
881
+ contextLinesAfter,
882
+ });
883
+ return {
884
+ success: true,
885
+ content: {
886
+ count: results.length,
887
+ references: results.map(r => ({
888
+ uri: r.location.uri,
889
+ range: r.location.range,
890
+ containingSymbol: r.containingSymbol ? {
891
+ name: r.containingSymbol.name,
892
+ kind: r.containingSymbol.kind,
893
+ } : undefined,
894
+ contextSnippet: r.contextSnippet,
895
+ })),
896
+ },
897
+ };
898
+ }
899
+ async handleInsertBeforeSymbol(args) {
900
+ if (!this.alServer) {
901
+ return { success: false, content: 'AL Language Server not initialized', isError: true };
902
+ }
903
+ (0, security_js_1.validateToolArgs)(args, ['uri', 'symbolName', 'content'], {
904
+ uri: 'string', symbolName: 'string', content: 'string'
905
+ });
906
+ const uri = args['uri'];
907
+ const symbolName = args['symbolName'];
908
+ const content = args['content'];
909
+ // Find the symbol
910
+ const symbol = await this.alServer.findSymbolByName(uri, symbolName);
911
+ if (!symbol) {
912
+ return {
913
+ success: false,
914
+ content: `Symbol '${symbolName}' not found in ${uri}`,
915
+ isError: true
916
+ };
917
+ }
918
+ // Read the file and insert before the symbol
919
+ const filePath = this.uriToPath(uri);
920
+ const workspaceRoot = this.getWorkspaceRoot(filePath);
921
+ const safePath = (0, security_js_1.sanitizePath)(filePath, workspaceRoot);
922
+ const fileContent = fs.readFileSync(safePath, 'utf-8');
923
+ const lines = fileContent.split('\n');
924
+ const insertLine = symbol.range.start.line;
925
+ const newContent = content.endsWith('\n') ? content : content + '\n';
926
+ lines.splice(insertLine, 0, ...newContent.split('\n').slice(0, -1));
927
+ fs.writeFileSync(safePath, lines.join('\n'), 'utf-8');
928
+ return {
929
+ success: true,
930
+ content: {
931
+ message: `Inserted content before symbol '${symbolName}' at line ${insertLine + 1}`,
932
+ insertedAt: insertLine,
933
+ }
934
+ };
935
+ }
936
+ handleFindFile(args) {
937
+ (0, security_js_1.validateToolArgs)(args, ['pattern'], { pattern: 'string' });
938
+ const pattern = args['pattern'];
939
+ const directory = args['directory'] || '.';
940
+ const workspaceRoot = this.getWorkspaceRoot();
941
+ const searchDir = path.resolve(workspaceRoot, directory);
942
+ if (!searchDir.startsWith(workspaceRoot)) {
943
+ return { success: false, content: 'Directory is outside workspace', isError: true };
944
+ }
945
+ const matches = [];
946
+ const searchRecursive = (dir) => {
947
+ try {
948
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
949
+ for (const entry of entries) {
950
+ const fullPath = path.join(dir, entry.name);
951
+ if (entry.isDirectory()) {
952
+ // Skip common ignored directories
953
+ if (!['node_modules', '.git', '.alpackages', '.output', '.partnercore'].includes(entry.name)) {
954
+ searchRecursive(fullPath);
955
+ }
956
+ }
957
+ else if (entry.isFile()) {
958
+ // Check if filename matches pattern (supports * and ? wildcards)
959
+ const regex = new RegExp('^' + pattern.replace(/\*/g, '.*').replace(/\?/g, '.') + '$', 'i');
960
+ if (regex.test(entry.name)) {
961
+ matches.push(path.relative(workspaceRoot, fullPath));
962
+ }
963
+ }
964
+ }
965
+ }
966
+ catch {
967
+ // Skip inaccessible directories
968
+ }
969
+ };
970
+ searchRecursive(searchDir);
971
+ return {
972
+ success: true,
973
+ content: {
974
+ count: matches.length,
975
+ files: matches,
976
+ },
977
+ };
978
+ }
979
+ handleReplaceContent(args) {
980
+ (0, security_js_1.validateToolArgs)(args, ['path', 'needle', 'replacement'], {
981
+ path: 'string', needle: 'string', replacement: 'string'
982
+ });
983
+ const filePath = args['path'];
984
+ const needle = args['needle'];
985
+ const replacement = args['replacement'];
986
+ const mode = args['mode'] || 'literal';
987
+ const allowMultiple = args['allowMultiple'];
988
+ const workspaceRoot = this.getWorkspaceRoot(filePath);
989
+ const safePath = (0, security_js_1.sanitizePath)(filePath, workspaceRoot);
990
+ if (!fs.existsSync(safePath)) {
991
+ return { success: false, content: `File not found: ${filePath}`, isError: true };
992
+ }
993
+ let content = fs.readFileSync(safePath, 'utf-8');
994
+ let replacements = 0;
995
+ if (mode === 'regex') {
996
+ try {
997
+ const regex = new RegExp(needle, 'gm');
998
+ const matches = content.match(regex);
999
+ replacements = matches ? matches.length : 0;
1000
+ if (replacements === 0) {
1001
+ return {
1002
+ success: false,
1003
+ content: `Pattern '${needle}' not found in file`,
1004
+ isError: true
1005
+ };
1006
+ }
1007
+ if (replacements > 1 && !allowMultiple) {
1008
+ return {
1009
+ success: false,
1010
+ content: `Pattern matches ${replacements} times. Set allowMultiple:true to replace all.`,
1011
+ isError: true
1012
+ };
1013
+ }
1014
+ content = content.replace(regex, replacement);
1015
+ }
1016
+ catch (error) {
1017
+ const errorMessage = error instanceof Error ? error.message : String(error);
1018
+ return {
1019
+ success: false,
1020
+ content: `Invalid regex: ${errorMessage}`,
1021
+ isError: true
1022
+ };
1023
+ }
1024
+ }
1025
+ else {
1026
+ // Literal mode
1027
+ const occurrences = content.split(needle).length - 1;
1028
+ if (occurrences === 0) {
1029
+ return {
1030
+ success: false,
1031
+ content: `Text '${needle}' not found in file`,
1032
+ isError: true
1033
+ };
1034
+ }
1035
+ if (occurrences > 1 && !allowMultiple) {
1036
+ return {
1037
+ success: false,
1038
+ content: `Text matches ${occurrences} times. Set allowMultiple:true to replace all.`,
1039
+ isError: true
1040
+ };
1041
+ }
1042
+ if (allowMultiple) {
1043
+ content = content.split(needle).join(replacement);
1044
+ replacements = occurrences;
1045
+ }
1046
+ else {
1047
+ content = content.replace(needle, replacement);
1048
+ replacements = 1;
1049
+ }
1050
+ }
1051
+ fs.writeFileSync(safePath, content, 'utf-8');
1052
+ return {
1053
+ success: true,
1054
+ content: {
1055
+ message: `Replaced ${replacements} occurrence(s)`,
1056
+ replacements,
1057
+ },
1058
+ };
1059
+ }
1060
+ handleEditMemory(args) {
1061
+ (0, security_js_1.validateToolArgs)(args, ['name', 'needle', 'replacement'], {
1062
+ name: 'string', needle: 'string', replacement: 'string'
1063
+ });
1064
+ const name = args['name'];
1065
+ const needle = args['needle'];
1066
+ const replacement = args['replacement'];
1067
+ const mode = args['mode'] || 'literal';
1068
+ const allowMultiple = args['allowMultiple'];
1069
+ const memory = this.getProjectMemory();
1070
+ const result = memory.editMemory(name, needle, replacement, { mode, allowMultiple });
1071
+ return {
1072
+ success: result.success,
1073
+ content: result,
1074
+ isError: !result.success,
1075
+ };
1076
+ }
1077
+ async handleGetExtensions(args) {
1078
+ (0, security_js_1.validateToolArgs)(args, ['containerName'], { containerName: 'string' });
1079
+ const containerName = (0, security_js_1.sanitizeString)(args['containerName']);
1080
+ const manager = this.getContainerManager();
1081
+ const result = await manager.getExtensions(containerName);
1082
+ return {
1083
+ success: result.success,
1084
+ content: result,
1085
+ isError: !result.success,
1086
+ };
1087
+ }
1088
+ async handleUninstallApp(args) {
1089
+ (0, security_js_1.validateToolArgs)(args, ['containerName', 'name'], {
1090
+ containerName: 'string', name: 'string'
1091
+ });
1092
+ const containerName = (0, security_js_1.sanitizeString)(args['containerName']);
1093
+ const manager = this.getContainerManager();
1094
+ const result = await manager.uninstallApp(containerName, {
1095
+ name: args['name'],
1096
+ publisher: args['publisher'],
1097
+ version: args['version'],
1098
+ force: args['force'],
1099
+ credential: args['username'] && args['password'] ? {
1100
+ username: args['username'],
1101
+ password: args['password'],
1102
+ } : undefined,
1103
+ });
1104
+ return {
1105
+ success: result.success,
1106
+ content: result,
1107
+ isError: !result.success,
1108
+ };
1109
+ }
1110
+ async handleCompileWarnings(args) {
1111
+ (0, security_js_1.validateToolArgs)(args, ['containerName'], { containerName: 'string' });
1112
+ const containerName = (0, security_js_1.sanitizeString)(args['containerName']);
1113
+ const manager = this.getContainerManager();
1114
+ const result = await manager.compileWarningsOnly(containerName, {
1115
+ appProjectFolder: args['appProjectFolder'],
1116
+ });
1117
+ return {
1118
+ success: result.success,
1119
+ content: result,
1120
+ isError: !result.success,
1121
+ };
1122
+ }
1123
+ uriToPath(uri) {
1124
+ if (uri.startsWith('file:///')) {
1125
+ // Windows: file:///C:/path -> C:/path
1126
+ const path = uri.slice(8);
1127
+ // Handle URL encoding
1128
+ return decodeURIComponent(path);
1129
+ }
1130
+ return uri;
1131
+ }
1132
+ // ==================== File System Tool Handlers ====================
1133
+ // All file operations are sandboxed to the workspace root for security
1134
+ handleReadFile(args) {
1135
+ // Validate required arguments
1136
+ (0, security_js_1.validateToolArgs)(args, ['path'], { path: 'string' });
1137
+ const filePath = (0, security_js_1.sanitizeString)(args['path']);
1138
+ // Detect workspace dynamically if needed
1139
+ const workspaceRoot = this.getWorkspaceRoot(filePath);
1140
+ // Security: Sanitize path to prevent directory traversal
1141
+ const resolved = (0, security_js_1.sanitizePath)(filePath, workspaceRoot);
1142
+ if (!fs.existsSync(resolved)) {
1143
+ return { success: false, content: `File not found: ${filePath}`, isError: true };
1144
+ }
1145
+ const stat = fs.statSync(resolved);
1146
+ if (!stat.isFile()) {
1147
+ return { success: false, content: `Not a file: ${filePath}`, isError: true };
1148
+ }
1149
+ // Security: Limit file size to prevent memory exhaustion
1150
+ const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
1151
+ if (stat.size > MAX_FILE_SIZE) {
1152
+ return { success: false, content: `File too large (max ${MAX_FILE_SIZE} bytes)`, isError: true };
1153
+ }
1154
+ const content = fs.readFileSync(resolved, 'utf-8');
1155
+ return { success: true, content };
1156
+ }
1157
+ handleWriteFile(args) {
1158
+ // Validate required arguments
1159
+ (0, security_js_1.validateToolArgs)(args, ['path', 'content'], { path: 'string', content: 'string' });
1160
+ const filePath = (0, security_js_1.sanitizeString)(args['path']);
1161
+ const content = args['content'];
1162
+ // Detect workspace dynamically if needed
1163
+ const workspaceRoot = this.getWorkspaceRoot(filePath);
1164
+ // Security: Sanitize path to prevent directory traversal
1165
+ const resolved = (0, security_js_1.sanitizePath)(filePath, workspaceRoot);
1166
+ const dir = path.dirname(resolved);
1167
+ // Security: Limit content size
1168
+ const MAX_CONTENT_SIZE = 10 * 1024 * 1024; // 10MB
1169
+ if (content.length > MAX_CONTENT_SIZE) {
1170
+ return { success: false, content: `Content too large (max ${MAX_CONTENT_SIZE} bytes)`, isError: true };
1171
+ }
1172
+ fs.mkdirSync(dir, { recursive: true });
1173
+ fs.writeFileSync(resolved, content, 'utf-8');
1174
+ // Return relative path in response (don't expose full paths)
1175
+ const relativePath = path.relative(workspaceRoot, resolved);
1176
+ return { success: true, content: `File written: ${relativePath}` };
1177
+ }
1178
+ handleListFiles(args) {
1179
+ // Validate required arguments
1180
+ (0, security_js_1.validateToolArgs)(args, ['path'], { path: 'string' });
1181
+ const dirPath = (0, security_js_1.sanitizeString)(args['path']);
1182
+ const pattern = args['pattern'] ? (0, security_js_1.sanitizeString)(args['pattern']) : undefined;
1183
+ // Detect workspace dynamically if needed
1184
+ const workspaceRoot = this.getWorkspaceRoot(dirPath);
1185
+ // Security: Sanitize path to prevent directory traversal
1186
+ const resolved = (0, security_js_1.sanitizePath)(dirPath, workspaceRoot);
1187
+ if (!fs.existsSync(resolved)) {
1188
+ return { success: false, content: `Directory not found: ${dirPath}`, isError: true };
1189
+ }
1190
+ const stat = fs.statSync(resolved);
1191
+ if (!stat.isDirectory()) {
1192
+ return { success: false, content: `Not a directory: ${dirPath}`, isError: true };
1193
+ }
1194
+ const files = this.listFilesRecursive(resolved, pattern);
1195
+ // Return relative paths for security
1196
+ const relativePaths = files.map(f => path.relative(workspaceRoot, f));
1197
+ return { success: true, content: relativePaths };
1198
+ }
1199
+ listFilesRecursive(dir, pattern, depth = 0) {
1200
+ // Security: Limit recursion depth
1201
+ const MAX_DEPTH = 20;
1202
+ if (depth > MAX_DEPTH)
1203
+ return [];
1204
+ // Use imports from top of file (fs and path are already imported)
1205
+ const results = [];
1206
+ let entries;
1207
+ try {
1208
+ entries = fs.readdirSync(dir, { withFileTypes: true });
1209
+ }
1210
+ catch {
1211
+ // Silently skip directories we can't read
1212
+ return results;
1213
+ }
1214
+ // Security: Limit total files to prevent DoS
1215
+ const MAX_FILES = 10000;
1216
+ for (const entry of entries) {
1217
+ if (results.length >= MAX_FILES)
1218
+ break;
1219
+ const fullPath = path.join(dir, entry.name);
1220
+ // Security: Skip hidden files/directories and common ignored paths
1221
+ if (entry.name.startsWith('.'))
1222
+ continue;
1223
+ if (entry.isDirectory()) {
1224
+ // Skip common ignored directories
1225
+ const ignoredDirs = ['node_modules', '.git', '.svn', 'dist', 'bin', 'obj', '.alpackages', '.snapshots'];
1226
+ if (!ignoredDirs.includes(entry.name)) {
1227
+ results.push(...this.listFilesRecursive(fullPath, pattern, depth + 1));
1228
+ }
1229
+ }
1230
+ else {
1231
+ if (!pattern || this.matchesPattern(entry.name, pattern)) {
1232
+ results.push(fullPath);
1233
+ }
1234
+ }
1235
+ }
1236
+ return results;
1237
+ }
1238
+ matchesPattern(filename, pattern) {
1239
+ // Security: Escape regex special characters except * and ?
1240
+ const escaped = pattern
1241
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
1242
+ .replace(/\*/g, '.*')
1243
+ .replace(/\?/g, '.');
1244
+ const regex = new RegExp(`^${escaped}$`, 'i');
1245
+ return regex.test(filename);
1246
+ }
1247
+ handleSearchFiles(args) {
1248
+ // Validate required arguments
1249
+ (0, security_js_1.validateToolArgs)(args, ['path', 'query'], { path: 'string', query: 'string' });
1250
+ const dirPath = (0, security_js_1.sanitizeString)(args['path']);
1251
+ const query = (0, security_js_1.sanitizeString)(args['query'], 1000); // Limit query length
1252
+ const filePattern = args['filePattern'] ? (0, security_js_1.sanitizeString)(args['filePattern']) : '*.al';
1253
+ // Detect workspace dynamically if needed
1254
+ const workspaceRoot = this.getWorkspaceRoot(dirPath);
1255
+ // Security: Sanitize path
1256
+ const resolved = (0, security_js_1.sanitizePath)(dirPath, workspaceRoot);
1257
+ const files = this.listFilesRecursive(resolved, filePattern);
1258
+ const results = [];
1259
+ const queryLower = query.toLowerCase();
1260
+ // Security: Limit results
1261
+ const MAX_RESULTS = 500;
1262
+ for (const file of files) {
1263
+ if (results.length >= MAX_RESULTS)
1264
+ break;
1265
+ try {
1266
+ const content = fs.readFileSync(file, 'utf-8');
1267
+ const lines = content.split('\n');
1268
+ lines.forEach((line, index) => {
1269
+ if (results.length >= MAX_RESULTS)
1270
+ return;
1271
+ if (line.toLowerCase().includes(queryLower)) {
1272
+ results.push({
1273
+ file: path.relative(workspaceRoot, file), // Return relative paths
1274
+ line: index + 1,
1275
+ content: line.trim().slice(0, 500), // Limit line length in results
1276
+ });
1277
+ }
1278
+ });
1279
+ }
1280
+ catch {
1281
+ // Skip files we can't read
1282
+ }
1283
+ }
1284
+ return { success: true, content: results };
1285
+ }
1286
+ // ==================== Symbol-Based Editing Handlers ====================
1287
+ async handleRenameSymbol(args) {
1288
+ if (!this.alServer) {
1289
+ return { success: false, content: 'AL Language Server not initialized', isError: true };
1290
+ }
1291
+ (0, security_js_1.validateToolArgs)(args, ['uri', 'line', 'character', 'newName'], {
1292
+ uri: 'string', line: 'number', character: 'number', newName: 'string'
1293
+ });
1294
+ const uri = args['uri'];
1295
+ const line = args['line'];
1296
+ const character = args['character'];
1297
+ const newName = (0, security_js_1.sanitizeString)(args['newName']);
1298
+ const workspaceEdit = await this.alServer.renameSymbol(uri, line, character, newName);
1299
+ if (!workspaceEdit) {
1300
+ return { success: false, content: 'Rename not available at this position', isError: true };
1301
+ }
1302
+ // Apply the edits
1303
+ const appliedFiles = [];
1304
+ if (workspaceEdit.changes) {
1305
+ for (const [fileUri, edits] of Object.entries(workspaceEdit.changes)) {
1306
+ const filePath = this.uriToPath(fileUri);
1307
+ let content = fs.readFileSync(filePath, 'utf-8');
1308
+ // Apply edits in reverse order to preserve positions
1309
+ const sortedEdits = [...edits].sort((a, b) => {
1310
+ if (a.range.start.line !== b.range.start.line) {
1311
+ return b.range.start.line - a.range.start.line;
1312
+ }
1313
+ return b.range.start.character - a.range.start.character;
1314
+ });
1315
+ const lines = content.split('\n');
1316
+ for (const edit of sortedEdits) {
1317
+ const startLine = edit.range.start.line;
1318
+ const endLine = edit.range.end.line;
1319
+ const startChar = edit.range.start.character;
1320
+ const endChar = edit.range.end.character;
1321
+ if (startLine === endLine) {
1322
+ // Single line edit
1323
+ const line = lines[startLine];
1324
+ lines[startLine] = line.slice(0, startChar) + edit.newText + line.slice(endChar);
1325
+ }
1326
+ else {
1327
+ // Multi-line edit
1328
+ const firstLine = lines[startLine].slice(0, startChar);
1329
+ const lastLine = lines[endLine].slice(endChar);
1330
+ const newLines = edit.newText.split('\n');
1331
+ newLines[0] = firstLine + newLines[0];
1332
+ newLines[newLines.length - 1] += lastLine;
1333
+ lines.splice(startLine, endLine - startLine + 1, ...newLines);
1334
+ }
1335
+ }
1336
+ content = lines.join('\n');
1337
+ fs.writeFileSync(filePath, content, 'utf-8');
1338
+ appliedFiles.push(filePath);
1339
+ }
1340
+ }
1341
+ return {
1342
+ success: true,
1343
+ content: {
1344
+ message: `Renamed symbol to '${newName}'`,
1345
+ filesModified: appliedFiles.length,
1346
+ files: appliedFiles,
1347
+ }
1348
+ };
1349
+ }
1350
+ async handleInsertAfterSymbol(args) {
1351
+ if (!this.alServer) {
1352
+ return { success: false, content: 'AL Language Server not initialized', isError: true };
1353
+ }
1354
+ (0, security_js_1.validateToolArgs)(args, ['uri', 'symbolName', 'content'], {
1355
+ uri: 'string', symbolName: 'string', content: 'string'
1356
+ });
1357
+ const uri = args['uri'];
1358
+ const symbolName = (0, security_js_1.sanitizeString)(args['symbolName']);
1359
+ const insertContent = args['content'];
1360
+ const filePath = this.uriToPath(uri);
1361
+ // Find the symbol
1362
+ const symbol = await this.alServer.findSymbolByName(uri, symbolName);
1363
+ if (!symbol) {
1364
+ return { success: false, content: `Symbol '${symbolName}' not found`, isError: true };
1365
+ }
1366
+ // Read the file and insert after the symbol's end
1367
+ let content = fs.readFileSync(filePath, 'utf-8');
1368
+ const lines = content.split('\n');
1369
+ const insertLine = symbol.range.end.line;
1370
+ // Insert the new content after the symbol
1371
+ lines.splice(insertLine + 1, 0, '', insertContent);
1372
+ content = lines.join('\n');
1373
+ fs.writeFileSync(filePath, content, 'utf-8');
1374
+ const workspaceRoot = this.getWorkspaceRoot(filePath);
1375
+ const relativePath = path.relative(workspaceRoot, filePath);
1376
+ return {
1377
+ success: true,
1378
+ content: {
1379
+ message: `Inserted content after symbol '${symbolName}'`,
1380
+ file: relativePath,
1381
+ insertedAtLine: insertLine + 2, // 1-based
1382
+ }
1383
+ };
1384
+ }
1385
+ async handleReplaceSymbolBody(args) {
1386
+ if (!this.alServer) {
1387
+ return { success: false, content: 'AL Language Server not initialized', isError: true };
1388
+ }
1389
+ (0, security_js_1.validateToolArgs)(args, ['uri', 'symbolName', 'newBody'], {
1390
+ uri: 'string', symbolName: 'string', newBody: 'string'
1391
+ });
1392
+ const uri = args['uri'];
1393
+ const symbolName = (0, security_js_1.sanitizeString)(args['symbolName']);
1394
+ const newBody = args['newBody'];
1395
+ const filePath = this.uriToPath(uri);
1396
+ // Find the symbol
1397
+ const symbol = await this.alServer.findSymbolByName(uri, symbolName);
1398
+ if (!symbol) {
1399
+ return { success: false, content: `Symbol '${symbolName}' not found`, isError: true };
1400
+ }
1401
+ // Read the file and replace the symbol's range
1402
+ let content = fs.readFileSync(filePath, 'utf-8');
1403
+ const lines = content.split('\n');
1404
+ const startLine = symbol.range.start.line;
1405
+ const endLine = symbol.range.end.line;
1406
+ const startChar = symbol.range.start.character;
1407
+ const endChar = symbol.range.end.character;
1408
+ // Replace the symbol body
1409
+ const before = lines.slice(0, startLine).join('\n');
1410
+ const firstLinePart = lines[startLine].slice(0, startChar);
1411
+ const lastLinePart = lines[endLine].slice(endChar);
1412
+ const after = lines.slice(endLine + 1).join('\n');
1413
+ content = before + (before ? '\n' : '') + firstLinePart + newBody + lastLinePart + (after ? '\n' + after : '');
1414
+ fs.writeFileSync(filePath, content, 'utf-8');
1415
+ const workspaceRoot = this.getWorkspaceRoot(filePath);
1416
+ const relativePath = path.relative(workspaceRoot, filePath);
1417
+ return {
1418
+ success: true,
1419
+ content: {
1420
+ message: `Replaced body of symbol '${symbolName}'`,
1421
+ file: relativePath,
1422
+ linesReplaced: endLine - startLine + 1,
1423
+ }
1424
+ };
1425
+ }
1426
+ // ==================== Advanced File Operations ====================
1427
+ handleDeleteLines(args) {
1428
+ (0, security_js_1.validateToolArgs)(args, ['path', 'startLine', 'endLine'], {
1429
+ path: 'string', startLine: 'number', endLine: 'number'
1430
+ });
1431
+ const filePath = (0, security_js_1.sanitizeString)(args['path']);
1432
+ const startLine = args['startLine']; // 1-based
1433
+ const endLine = args['endLine']; // 1-based
1434
+ const workspaceRoot = this.getWorkspaceRoot(filePath);
1435
+ const resolved = (0, security_js_1.sanitizePath)(filePath, workspaceRoot);
1436
+ if (!fs.existsSync(resolved)) {
1437
+ return { success: false, content: `File not found: ${filePath}`, isError: true };
1438
+ }
1439
+ let content = fs.readFileSync(resolved, 'utf-8');
1440
+ const lines = content.split('\n');
1441
+ if (startLine < 1 || endLine > lines.length || startLine > endLine) {
1442
+ return { success: false, content: `Invalid line range: ${startLine}-${endLine}`, isError: true };
1443
+ }
1444
+ // Delete lines (convert to 0-based)
1445
+ lines.splice(startLine - 1, endLine - startLine + 1);
1446
+ content = lines.join('\n');
1447
+ fs.writeFileSync(resolved, content, 'utf-8');
1448
+ const relativePath = path.relative(workspaceRoot, resolved);
1449
+ return {
1450
+ success: true,
1451
+ content: {
1452
+ message: `Deleted lines ${startLine}-${endLine}`,
1453
+ file: relativePath,
1454
+ linesDeleted: endLine - startLine + 1,
1455
+ }
1456
+ };
1457
+ }
1458
+ handleReplaceLines(args) {
1459
+ (0, security_js_1.validateToolArgs)(args, ['path', 'startLine', 'endLine', 'newContent'], {
1460
+ path: 'string', startLine: 'number', endLine: 'number', newContent: 'string'
1461
+ });
1462
+ const filePath = (0, security_js_1.sanitizeString)(args['path']);
1463
+ const startLine = args['startLine']; // 1-based
1464
+ const endLine = args['endLine']; // 1-based
1465
+ const newContent = args['newContent'];
1466
+ const workspaceRoot = this.getWorkspaceRoot(filePath);
1467
+ const resolved = (0, security_js_1.sanitizePath)(filePath, workspaceRoot);
1468
+ if (!fs.existsSync(resolved)) {
1469
+ return { success: false, content: `File not found: ${filePath}`, isError: true };
1470
+ }
1471
+ let content = fs.readFileSync(resolved, 'utf-8');
1472
+ const lines = content.split('\n');
1473
+ if (startLine < 1 || endLine > lines.length || startLine > endLine) {
1474
+ return { success: false, content: `Invalid line range: ${startLine}-${endLine}`, isError: true };
1475
+ }
1476
+ // Replace lines (convert to 0-based)
1477
+ const newLines = newContent.split('\n');
1478
+ lines.splice(startLine - 1, endLine - startLine + 1, ...newLines);
1479
+ content = lines.join('\n');
1480
+ fs.writeFileSync(resolved, content, 'utf-8');
1481
+ const relativePath = path.relative(workspaceRoot, resolved);
1482
+ return {
1483
+ success: true,
1484
+ content: {
1485
+ message: `Replaced lines ${startLine}-${endLine}`,
1486
+ file: relativePath,
1487
+ linesReplaced: endLine - startLine + 1,
1488
+ newLinesCount: newLines.length,
1489
+ }
1490
+ };
1491
+ }
1492
+ handleInsertAtLine(args) {
1493
+ (0, security_js_1.validateToolArgs)(args, ['path', 'line', 'content'], {
1494
+ path: 'string', line: 'number', content: 'string'
1495
+ });
1496
+ const filePath = (0, security_js_1.sanitizeString)(args['path']);
1497
+ const lineNumber = args['line']; // 1-based
1498
+ const insertContent = args['content'];
1499
+ const workspaceRoot = this.getWorkspaceRoot(filePath);
1500
+ const resolved = (0, security_js_1.sanitizePath)(filePath, workspaceRoot);
1501
+ if (!fs.existsSync(resolved)) {
1502
+ return { success: false, content: `File not found: ${filePath}`, isError: true };
1503
+ }
1504
+ let content = fs.readFileSync(resolved, 'utf-8');
1505
+ const lines = content.split('\n');
1506
+ if (lineNumber < 1 || lineNumber > lines.length + 1) {
1507
+ return { success: false, content: `Invalid line number: ${lineNumber}`, isError: true };
1508
+ }
1509
+ // Insert at line (convert to 0-based)
1510
+ const newLines = insertContent.split('\n');
1511
+ lines.splice(lineNumber - 1, 0, ...newLines);
1512
+ content = lines.join('\n');
1513
+ fs.writeFileSync(resolved, content, 'utf-8');
1514
+ const relativePath = path.relative(workspaceRoot, resolved);
1515
+ return {
1516
+ success: true,
1517
+ content: {
1518
+ message: `Inserted ${newLines.length} line(s) at line ${lineNumber}`,
1519
+ file: relativePath,
1520
+ linesInserted: newLines.length,
1521
+ }
1522
+ };
1523
+ }
1524
+ // ==================== Project Memory Handlers ====================
1525
+ getProjectMemory() {
1526
+ if (!this.projectMemory) {
1527
+ const workspaceRoot = this.getWorkspaceRoot();
1528
+ this.projectMemory = new project_memory_js_1.ProjectMemory(workspaceRoot);
1529
+ }
1530
+ return this.projectMemory;
1531
+ }
1532
+ handleWriteMemory(args) {
1533
+ (0, security_js_1.validateToolArgs)(args, ['name', 'content'], { name: 'string', content: 'string' });
1534
+ const name = (0, security_js_1.sanitizeString)(args['name']);
1535
+ const content = args['content'];
1536
+ const tags = args['tags'];
1537
+ const memory = this.getProjectMemory();
1538
+ const result = memory.writeMemory(name, content, tags);
1539
+ return {
1540
+ success: true,
1541
+ content: {
1542
+ message: `Memory '${name}' saved`,
1543
+ memory: {
1544
+ name: result.name,
1545
+ createdAt: result.createdAt,
1546
+ updatedAt: result.updatedAt,
1547
+ tags: result.tags,
1548
+ }
1549
+ }
1550
+ };
1551
+ }
1552
+ handleReadMemory(args) {
1553
+ (0, security_js_1.validateToolArgs)(args, ['name'], { name: 'string' });
1554
+ const name = (0, security_js_1.sanitizeString)(args['name']);
1555
+ const memory = this.getProjectMemory();
1556
+ const result = memory.readMemory(name);
1557
+ if (!result) {
1558
+ return { success: false, content: `Memory '${name}' not found`, isError: true };
1559
+ }
1560
+ return {
1561
+ success: true,
1562
+ content: result
1563
+ };
1564
+ }
1565
+ handleListMemories() {
1566
+ const memory = this.getProjectMemory();
1567
+ const memories = memory.listMemories();
1568
+ return {
1569
+ success: true,
1570
+ content: {
1571
+ count: memories.length,
1572
+ memories: memories.map(m => ({
1573
+ name: m.name,
1574
+ updatedAt: m.updatedAt,
1575
+ tags: m.tags,
1576
+ preview: m.content.slice(0, 100) + (m.content.length > 100 ? '...' : ''),
1577
+ })),
1578
+ }
1579
+ };
1580
+ }
1581
+ handleDeleteMemory(args) {
1582
+ (0, security_js_1.validateToolArgs)(args, ['name'], { name: 'string' });
1583
+ const name = (0, security_js_1.sanitizeString)(args['name']);
1584
+ const memory = this.getProjectMemory();
1585
+ const deleted = memory.deleteMemory(name);
1586
+ if (!deleted) {
1587
+ return { success: false, content: `Memory '${name}' not found`, isError: true };
1588
+ }
1589
+ return {
1590
+ success: true,
1591
+ content: { message: `Memory '${name}' deleted` }
1592
+ };
1593
+ }
1594
+ // ==================== BC Container Handlers ====================
1595
+ getContainerManager() {
1596
+ if (!this.containerManager) {
1597
+ const workspaceRoot = this.getWorkspaceRoot();
1598
+ this.containerManager = new bc_container_js_1.BCContainerManager(workspaceRoot);
1599
+ }
1600
+ return this.containerManager;
1601
+ }
1602
+ async handleListContainers() {
1603
+ const manager = this.getContainerManager();
1604
+ const containers = await manager.listContainers();
1605
+ return {
1606
+ success: true,
1607
+ content: {
1608
+ count: containers.length,
1609
+ containers: containers.map(c => ({
1610
+ name: c.name,
1611
+ image: c.image,
1612
+ status: c.status,
1613
+ running: c.running,
1614
+ ports: c.ports,
1615
+ })),
1616
+ }
1617
+ };
1618
+ }
1619
+ async handleCompile(args) {
1620
+ (0, security_js_1.validateToolArgs)(args, ['containerName'], { containerName: 'string' });
1621
+ const containerName = (0, security_js_1.sanitizeString)(args['containerName']);
1622
+ const manager = this.getContainerManager();
1623
+ const result = await manager.compile(containerName, {
1624
+ appProjectFolder: args['appProjectFolder'],
1625
+ outputFolder: args['outputFolder'],
1626
+ });
1627
+ return {
1628
+ success: result.success,
1629
+ content: {
1630
+ success: result.success,
1631
+ appFile: result.appFile,
1632
+ errors: result.errors,
1633
+ warnings: result.warnings,
1634
+ duration: `${(result.duration / 1000).toFixed(2)}s`,
1635
+ },
1636
+ isError: !result.success,
1637
+ };
1638
+ }
1639
+ async handlePublish(args) {
1640
+ (0, security_js_1.validateToolArgs)(args, ['containerName'], { containerName: 'string' });
1641
+ const containerName = (0, security_js_1.sanitizeString)(args['containerName']);
1642
+ const manager = this.getContainerManager();
1643
+ const result = await manager.publish(containerName, {
1644
+ appFile: args['appFile'],
1645
+ syncMode: args['syncMode'],
1646
+ skipVerification: args['skipVerification'],
1647
+ install: args['install'],
1648
+ });
1649
+ return {
1650
+ success: result.success,
1651
+ content: result,
1652
+ isError: !result.success,
1653
+ };
1654
+ }
1655
+ async handleRunTests(args) {
1656
+ (0, security_js_1.validateToolArgs)(args, ['containerName'], { containerName: 'string' });
1657
+ const containerName = (0, security_js_1.sanitizeString)(args['containerName']);
1658
+ const manager = this.getContainerManager();
1659
+ const result = await manager.runTests(containerName, {
1660
+ testCodeunit: args['testCodeunit'],
1661
+ testFunction: args['testFunction'],
1662
+ extensionId: args['extensionId'],
1663
+ detailed: args['detailed'],
1664
+ });
1665
+ return {
1666
+ success: result.success,
1667
+ content: {
1668
+ success: result.success,
1669
+ testsRun: result.testsRun,
1670
+ testsPassed: result.testsPassed,
1671
+ testsFailed: result.testsFailed,
1672
+ testsSkipped: result.testsSkipped,
1673
+ duration: `${(result.duration / 1000).toFixed(2)}s`,
1674
+ results: result.results.slice(0, 50), // Limit results to prevent huge payloads
1675
+ },
1676
+ isError: !result.success,
1677
+ };
1678
+ }
1679
+ async handleContainerLogs(args) {
1680
+ (0, security_js_1.validateToolArgs)(args, ['containerName'], { containerName: 'string' });
1681
+ const containerName = (0, security_js_1.sanitizeString)(args['containerName']);
1682
+ const manager = this.getContainerManager();
1683
+ const logs = await manager.getLogs(containerName, {
1684
+ tail: args['tail'],
1685
+ since: args['since'],
1686
+ });
1687
+ return {
1688
+ success: true,
1689
+ content: logs,
1690
+ };
1691
+ }
1692
+ async handleStartContainer(args) {
1693
+ (0, security_js_1.validateToolArgs)(args, ['containerName'], { containerName: 'string' });
1694
+ const containerName = (0, security_js_1.sanitizeString)(args['containerName']);
1695
+ const manager = this.getContainerManager();
1696
+ const result = await manager.startContainer(containerName);
1697
+ return {
1698
+ success: result.success,
1699
+ content: result,
1700
+ isError: !result.success,
1701
+ };
1702
+ }
1703
+ async handleStopContainer(args) {
1704
+ (0, security_js_1.validateToolArgs)(args, ['containerName'], { containerName: 'string' });
1705
+ const containerName = (0, security_js_1.sanitizeString)(args['containerName']);
1706
+ const manager = this.getContainerManager();
1707
+ const result = await manager.stopContainer(containerName);
1708
+ return {
1709
+ success: result.success,
1710
+ content: result,
1711
+ isError: !result.success,
1712
+ };
1713
+ }
1714
+ async handleRestartContainer(args) {
1715
+ (0, security_js_1.validateToolArgs)(args, ['containerName'], { containerName: 'string' });
1716
+ const containerName = (0, security_js_1.sanitizeString)(args['containerName']);
1717
+ const manager = this.getContainerManager();
1718
+ const result = await manager.restartContainer(containerName);
1719
+ return {
1720
+ success: result.success,
1721
+ content: result,
1722
+ isError: !result.success,
1723
+ };
1724
+ }
1725
+ async handleDownloadSymbols(args) {
1726
+ (0, security_js_1.validateToolArgs)(args, ['containerName'], { containerName: 'string' });
1727
+ const containerName = (0, security_js_1.sanitizeString)(args['containerName']);
1728
+ const manager = this.getContainerManager();
1729
+ const result = await manager.downloadSymbols(containerName, args['targetFolder']);
1730
+ return {
1731
+ success: result.success,
1732
+ content: result,
1733
+ isError: !result.success,
1734
+ };
1735
+ }
1736
+ async handleCreateContainer(args) {
1737
+ (0, security_js_1.validateToolArgs)(args, ['containerName'], { containerName: 'string' });
1738
+ const containerName = (0, security_js_1.sanitizeString)(args['containerName']);
1739
+ const manager = this.getContainerManager();
1740
+ // Build options from arguments
1741
+ const options = {
1742
+ artifactUrl: args['artifactUrl'],
1743
+ version: args['version'],
1744
+ country: args['country'],
1745
+ type: args['type'],
1746
+ auth: args['auth'],
1747
+ credential: args['username'] && args['password'] ? {
1748
+ username: args['username'],
1749
+ password: args['password'],
1750
+ } : undefined,
1751
+ licenseFile: args['licenseFile'],
1752
+ accept_eula: args['accept_eula'] !== false,
1753
+ accept_outdated: args['accept_outdated'],
1754
+ includeTestToolkit: args['includeTestToolkit'],
1755
+ includeTestLibrariesOnly: args['includeTestLibrariesOnly'],
1756
+ includeTestFrameworkOnly: args['includeTestFrameworkOnly'],
1757
+ enableTaskScheduler: args['enableTaskScheduler'],
1758
+ assignPremiumPlan: args['assignPremiumPlan'],
1759
+ multitenant: args['multitenant'],
1760
+ memoryLimit: args['memoryLimit'],
1761
+ isolation: args['isolation'],
1762
+ updateHosts: args['updateHosts'],
1763
+ };
1764
+ const result = await manager.createContainer(containerName, options);
1765
+ return {
1766
+ success: result.success,
1767
+ content: result,
1768
+ isError: !result.success,
1769
+ };
1770
+ }
1771
+ async handleRemoveContainer(args) {
1772
+ (0, security_js_1.validateToolArgs)(args, ['containerName'], { containerName: 'string' });
1773
+ const containerName = (0, security_js_1.sanitizeString)(args['containerName']);
1774
+ const force = args['force'];
1775
+ const manager = this.getContainerManager();
1776
+ const result = await manager.removeContainer(containerName, force);
1777
+ return {
1778
+ success: result.success,
1779
+ content: result,
1780
+ isError: !result.success,
1781
+ };
1782
+ }
1783
+ // ==================== Git Operations Handlers ====================
1784
+ getGitOperations() {
1785
+ if (!this.gitOperations) {
1786
+ const workspaceRoot = this.getWorkspaceRoot();
1787
+ this.gitOperations = new git_operations_js_1.GitOperations(workspaceRoot);
1788
+ }
1789
+ return this.gitOperations;
1790
+ }
1791
+ async handleGitStatus() {
1792
+ const git = this.getGitOperations();
1793
+ if (!await git.isGitRepository()) {
1794
+ return { success: false, content: 'Not a git repository', isError: true };
1795
+ }
1796
+ const status = await git.getStatus();
1797
+ return { success: true, content: status };
1798
+ }
1799
+ async handleGitDiff(args) {
1800
+ const git = this.getGitOperations();
1801
+ const diff = await git.getDiff({
1802
+ staged: args['staged'],
1803
+ file: args['file'],
1804
+ unified: args['unified'],
1805
+ });
1806
+ return {
1807
+ success: true,
1808
+ content: diff || '(No changes)',
1809
+ };
1810
+ }
1811
+ async handleGitStage(args) {
1812
+ (0, security_js_1.validateToolArgs)(args, ['paths'], {});
1813
+ const git = this.getGitOperations();
1814
+ const paths = args['paths'];
1815
+ const result = await git.stage(paths);
1816
+ return {
1817
+ success: result.success,
1818
+ content: result,
1819
+ isError: !result.success,
1820
+ };
1821
+ }
1822
+ async handleGitCommit(args) {
1823
+ (0, security_js_1.validateToolArgs)(args, ['message'], { message: 'string' });
1824
+ const git = this.getGitOperations();
1825
+ const message = args['message'];
1826
+ const result = await git.commit(message, {
1827
+ amend: args['amend'],
1828
+ allowEmpty: args['allowEmpty'],
1829
+ });
1830
+ return {
1831
+ success: result.success,
1832
+ content: result,
1833
+ isError: !result.success,
1834
+ };
1835
+ }
1836
+ async handleGitLog(args) {
1837
+ const git = this.getGitOperations();
1838
+ const commits = await git.getLog({
1839
+ limit: args['limit'] || 20,
1840
+ since: args['since'],
1841
+ author: args['author'],
1842
+ grep: args['grep'],
1843
+ file: args['file'],
1844
+ });
1845
+ return {
1846
+ success: true,
1847
+ content: {
1848
+ count: commits.length,
1849
+ commits,
1850
+ }
1851
+ };
1852
+ }
1853
+ async handleGitBranches(args) {
1854
+ const git = this.getGitOperations();
1855
+ const remote = args['remote'];
1856
+ const branches = await git.listBranches(remote);
1857
+ const currentBranch = branches.find(b => b.current);
1858
+ return {
1859
+ success: true,
1860
+ content: {
1861
+ current: currentBranch?.name,
1862
+ count: branches.length,
1863
+ branches,
1864
+ }
1865
+ };
1866
+ }
1867
+ async handleGitCheckout(args) {
1868
+ (0, security_js_1.validateToolArgs)(args, ['target'], { target: 'string' });
1869
+ const git = this.getGitOperations();
1870
+ const target = (0, security_js_1.sanitizeString)(args['target']);
1871
+ const result = await git.checkout(target, {
1872
+ create: args['create'],
1873
+ });
1874
+ return {
1875
+ success: result.success,
1876
+ content: result,
1877
+ isError: !result.success,
1878
+ };
1879
+ }
1880
+ async handleGitPull(args) {
1881
+ const git = this.getGitOperations();
1882
+ const result = await git.pull({
1883
+ remote: args['remote'],
1884
+ branch: args['branch'],
1885
+ rebase: args['rebase'],
1886
+ });
1887
+ return {
1888
+ success: result.success,
1889
+ content: result,
1890
+ isError: !result.success,
1891
+ };
1892
+ }
1893
+ async handleGitPush(args) {
1894
+ const git = this.getGitOperations();
1895
+ const result = await git.push({
1896
+ remote: args['remote'],
1897
+ branch: args['branch'],
1898
+ setUpstream: args['setUpstream'],
1899
+ force: args['force'],
1900
+ });
1901
+ return {
1902
+ success: result.success,
1903
+ content: result,
1904
+ isError: !result.success,
1905
+ };
1906
+ }
1907
+ async handleGitStash(args) {
1908
+ const git = this.getGitOperations();
1909
+ const action = args['action'] || 'list';
1910
+ switch (action) {
1911
+ case 'save':
1912
+ case 'push': {
1913
+ const result = await git.stash({
1914
+ message: args['message'],
1915
+ includeUntracked: args['includeUntracked'],
1916
+ });
1917
+ return { success: result.success, content: result, isError: !result.success };
1918
+ }
1919
+ case 'pop': {
1920
+ const result = await git.stashPop(args['index']);
1921
+ return { success: result.success, content: result, isError: !result.success };
1922
+ }
1923
+ case 'list':
1924
+ default: {
1925
+ const stashes = await git.stashList();
1926
+ return { success: true, content: { count: stashes.length, stashes } };
1927
+ }
1928
+ }
1929
+ }
1930
+ // ==================== Getting Started Handler ====================
1931
+ async handleGetStarted() {
1932
+ const workspace = this.workspaceRoot || (0, loader_js_1.findALWorkspace)();
1933
+ const hasWorkspace = !!workspace;
1934
+ // Check AL Language Server status
1935
+ const alServerReady = this.alServer !== null;
1936
+ // Get cloud status
1937
+ let cloudConnected = false;
1938
+ if (this.cloudClient) {
1939
+ try {
1940
+ cloudConnected = await this.cloudClient.checkConnection();
1941
+ }
1942
+ catch {
1943
+ cloudConnected = false;
1944
+ }
1945
+ }
1946
+ // Build the getting started response
1947
+ const response = {
1948
+ welcome: '🚀 PartnerCore AL Development - Ready to help!',
1949
+ status: {
1950
+ workspace: hasWorkspace ? `✅ ${workspace}` : '⚠️ No AL workspace detected (looking for app.json)',
1951
+ alLanguageServer: alServerReady ? '✅ Ready' : '⚠️ Will initialize on first AL file operation',
1952
+ cloudConnection: cloudConnected ? '✅ Connected (AI review, KB, templates available)' : '⚠️ Offline (local tools only)',
1953
+ },
1954
+ workflows: {
1955
+ newObject: [
1956
+ '1. partnercore_kb_search → Find best practices',
1957
+ '2. partnercore_template → Get code template',
1958
+ '3. write_file → Write the AL code',
1959
+ '4. al_get_diagnostics → Check compilation (ALWAYS!)',
1960
+ '5. partnercore_review → Code review',
1961
+ '6. git_commit → Save your work',
1962
+ ],
1963
+ codeReview: [
1964
+ '1. read_file → Read the code',
1965
+ '2. al_get_diagnostics → Check errors',
1966
+ '3. partnercore_review → Get AI review',
1967
+ '4. al_code_actions → Get suggested fixes',
1968
+ '5. write_file → Apply improvements',
1969
+ ],
1970
+ bcContainer: [
1971
+ '1. bc_list_containers → Check containers',
1972
+ '2. bc_compile → Compile app',
1973
+ '3. bc_publish → Deploy to container',
1974
+ '4. bc_run_tests → Run tests',
1975
+ ],
1976
+ git: [
1977
+ '1. git_status → See changes',
1978
+ '2. git_diff → Review changes',
1979
+ '3. git_stage → Stage files',
1980
+ '4. git_commit → Commit',
1981
+ '5. git_push → Push to remote',
1982
+ ],
1983
+ refactoring: [
1984
+ '1. al_get_symbols → Understand structure',
1985
+ '2. al_find_references → Find usages',
1986
+ '3. al_rename_symbol → Rename',
1987
+ '4. al_format → Clean up',
1988
+ '5. al_get_diagnostics → Verify',
1989
+ ],
1990
+ },
1991
+ tools: {
1992
+ fileOperations: [
1993
+ 'read_file - Read file contents',
1994
+ 'write_file - Write/create files',
1995
+ 'list_files - List directory contents',
1996
+ 'search_files - Search text in files',
1997
+ 'find_file - Find files by pattern',
1998
+ 'replace_content - Search/replace with regex',
1999
+ 'delete_lines - Delete line range',
2000
+ 'replace_lines - Replace line range',
2001
+ 'insert_at_line - Insert at specific line',
2002
+ ],
2003
+ alLanguageServer: [
2004
+ 'al_get_diagnostics - ⭐ ALWAYS USE after writing AL code',
2005
+ 'al_get_symbols - Get all symbols in a file',
2006
+ 'al_find_symbol - Search symbols by name',
2007
+ 'al_find_references - Find all references to a symbol',
2008
+ 'al_go_to_definition - Navigate to symbol definition',
2009
+ 'al_hover - Get type info and documentation',
2010
+ 'al_completion - Get code completion suggestions',
2011
+ 'al_code_actions - Get quick fixes and refactorings',
2012
+ 'al_signature_help - Get function parameter hints',
2013
+ 'al_format - Format document',
2014
+ 'al_rename_symbol - Rename across workspace',
2015
+ ],
2016
+ bcContainers: [
2017
+ 'bc_list_containers - List BC Docker containers',
2018
+ 'bc_create_container - Create new container',
2019
+ 'bc_remove_container - Remove container',
2020
+ 'bc_compile - Compile AL project',
2021
+ 'bc_publish - Publish app to container',
2022
+ 'bc_run_tests - Run automated tests',
2023
+ 'bc_download_symbols - Download symbol files',
2024
+ ],
2025
+ git: [
2026
+ 'git_status - Get current status',
2027
+ 'git_diff - Show changes',
2028
+ 'git_stage - Stage files',
2029
+ 'git_commit - Commit changes',
2030
+ 'git_push - Push to remote',
2031
+ 'git_branches - List branches',
2032
+ 'git_checkout - Switch/create branches',
2033
+ ],
2034
+ projectMemory: [
2035
+ 'write_memory - Save project knowledge for future sessions',
2036
+ 'read_memory - Retrieve saved memory',
2037
+ 'list_memories - List all memories',
2038
+ 'delete_memory - Delete a memory',
2039
+ ],
2040
+ cloud: cloudConnected ? [
2041
+ 'partnercore_kb_search - Search knowledge base',
2042
+ 'partnercore_template - Get code templates',
2043
+ 'partnercore_review - AI code review',
2044
+ 'partnercore_validate - AppSource compliance check',
2045
+ ] : ['(Not connected - set PARTNERCORE_API_KEY for cloud features)'],
2046
+ },
2047
+ criticalReminders: [
2048
+ '⚠️ ALWAYS call al_get_diagnostics after writing any AL file',
2049
+ '⚠️ Use MCP tools (read_file, list_files) instead of shell commands',
2050
+ '⚠️ Workspace is auto-detected from app.json - no hardcoded paths needed',
2051
+ ],
2052
+ nextSteps: hasWorkspace
2053
+ ? 'Use list_files to explore the project, or describe what you want to build.'
2054
+ : 'Navigate to an AL project directory (containing app.json) to enable full functionality.',
2055
+ };
2056
+ return { success: true, content: response };
2057
+ }
2058
+ // ==================== Tool Definitions ====================
2059
+ initLocalToolDefinitions() {
2060
+ this.localToolDefinitions = [
2061
+ {
2062
+ name: 'al_get_started',
2063
+ description: '🚀 START HERE - The recommended first tool to call in any AL development session. Returns: workspace status, AL Language Server status, cloud connection status, available workflows/prompts, tool categories, and a quick-start guide. Use this to understand what capabilities are available before beginning work.',
2064
+ inputSchema: {
2065
+ type: 'object',
2066
+ properties: {},
2067
+ required: [],
2068
+ },
2069
+ },
2070
+ {
2071
+ name: 'al_get_symbols',
2072
+ description: 'Get all symbols (procedures, fields, variables, etc.) in an AL file',
2073
+ inputSchema: {
2074
+ type: 'object',
2075
+ properties: {
2076
+ uri: {
2077
+ type: 'string',
2078
+ description: 'File URI (e.g., file:///C:/project/src/MyTable.Table.al)',
2079
+ },
2080
+ },
2081
+ required: ['uri'],
2082
+ },
2083
+ },
2084
+ {
2085
+ name: 'al_find_symbol',
2086
+ description: 'Search for symbols by name across the workspace',
2087
+ inputSchema: {
2088
+ type: 'object',
2089
+ properties: {
2090
+ query: {
2091
+ type: 'string',
2092
+ description: 'Symbol name to search for',
2093
+ },
2094
+ },
2095
+ required: ['query'],
2096
+ },
2097
+ },
2098
+ {
2099
+ name: 'al_find_references',
2100
+ description: 'Find all references to a symbol at a specific position',
2101
+ inputSchema: {
2102
+ type: 'object',
2103
+ properties: {
2104
+ uri: { type: 'string', description: 'File URI' },
2105
+ line: { type: 'number', description: 'Line number (0-based)' },
2106
+ character: { type: 'number', description: 'Character position (0-based)' },
2107
+ },
2108
+ required: ['uri', 'line', 'character'],
2109
+ },
2110
+ },
2111
+ {
2112
+ name: 'al_get_diagnostics',
2113
+ description: 'Get compiler diagnostics (errors, warnings) for an AL file',
2114
+ inputSchema: {
2115
+ type: 'object',
2116
+ properties: {
2117
+ uri: { type: 'string', description: 'File URI' },
2118
+ },
2119
+ required: ['uri'],
2120
+ },
2121
+ },
2122
+ {
2123
+ name: 'al_go_to_definition',
2124
+ description: 'Go to the definition of a symbol at a specific position',
2125
+ inputSchema: {
2126
+ type: 'object',
2127
+ properties: {
2128
+ uri: { type: 'string', description: 'File URI' },
2129
+ line: { type: 'number', description: 'Line number (0-based)' },
2130
+ character: { type: 'number', description: 'Character position (0-based)' },
2131
+ },
2132
+ required: ['uri', 'line', 'character'],
2133
+ },
2134
+ },
2135
+ {
2136
+ name: 'al_hover',
2137
+ description: 'Get hover information (type info, documentation) for a position',
2138
+ inputSchema: {
2139
+ type: 'object',
2140
+ properties: {
2141
+ uri: { type: 'string', description: 'File URI' },
2142
+ line: { type: 'number', description: 'Line number (0-based)' },
2143
+ character: { type: 'number', description: 'Character position (0-based)' },
2144
+ },
2145
+ required: ['uri', 'line', 'character'],
2146
+ },
2147
+ },
2148
+ {
2149
+ name: 'al_completion',
2150
+ description: 'Get code completion suggestions at a position',
2151
+ inputSchema: {
2152
+ type: 'object',
2153
+ properties: {
2154
+ uri: { type: 'string', description: 'File URI' },
2155
+ line: { type: 'number', description: 'Line number (0-based)' },
2156
+ character: { type: 'number', description: 'Character position (0-based)' },
2157
+ },
2158
+ required: ['uri', 'line', 'character'],
2159
+ },
2160
+ },
2161
+ // ==================== New LSP Tools ====================
2162
+ {
2163
+ name: 'al_code_actions',
2164
+ description: 'Get code actions (quick fixes, refactorings) for a position or range. Returns available fixes for diagnostics and refactoring options.',
2165
+ inputSchema: {
2166
+ type: 'object',
2167
+ properties: {
2168
+ uri: { type: 'string', description: 'File URI' },
2169
+ line: { type: 'number', description: 'Line number (0-based) for point query' },
2170
+ character: { type: 'number', description: 'Character position (0-based) for point query' },
2171
+ startLine: { type: 'number', description: 'Start line for range query' },
2172
+ startCharacter: { type: 'number', description: 'Start character for range query' },
2173
+ endLine: { type: 'number', description: 'End line for range query' },
2174
+ endCharacter: { type: 'number', description: 'End character for range query' },
2175
+ only: {
2176
+ type: 'array',
2177
+ items: { type: 'string' },
2178
+ description: 'Filter actions by kind (e.g., "quickfix", "refactor")'
2179
+ },
2180
+ includeDiagnostics: { type: 'boolean', description: 'Include diagnostics in context (default: true)' },
2181
+ },
2182
+ required: ['uri'],
2183
+ },
2184
+ },
2185
+ {
2186
+ name: 'al_signature_help',
2187
+ description: 'Get signature help (function parameter hints) at a position. Useful when cursor is inside function call parentheses.',
2188
+ inputSchema: {
2189
+ type: 'object',
2190
+ properties: {
2191
+ uri: { type: 'string', description: 'File URI' },
2192
+ line: { type: 'number', description: 'Line number (0-based)' },
2193
+ character: { type: 'number', description: 'Character position (0-based), typically after "(" or ","' },
2194
+ },
2195
+ required: ['uri', 'line', 'character'],
2196
+ },
2197
+ },
2198
+ {
2199
+ name: 'al_format',
2200
+ description: 'Format an AL document or a range within it. Can preview or apply formatting changes.',
2201
+ inputSchema: {
2202
+ type: 'object',
2203
+ properties: {
2204
+ uri: { type: 'string', description: 'File URI' },
2205
+ startLine: { type: 'number', description: 'Start line for range formatting (omit for full document)' },
2206
+ startCharacter: { type: 'number', description: 'Start character for range formatting' },
2207
+ endLine: { type: 'number', description: 'End line for range formatting' },
2208
+ endCharacter: { type: 'number', description: 'End character for range formatting' },
2209
+ tabSize: { type: 'number', description: 'Tab size (default: 4)' },
2210
+ insertSpaces: { type: 'boolean', description: 'Use spaces instead of tabs (default: true)' },
2211
+ apply: { type: 'boolean', description: 'Apply changes to file (default: false, preview only)' },
2212
+ },
2213
+ required: ['uri'],
2214
+ },
2215
+ },
2216
+ // ==================== Additional LSP Tools (Complete Coverage) ====================
2217
+ {
2218
+ name: 'al_document_highlight',
2219
+ description: 'Highlight all occurrences of the symbol under cursor. Useful for seeing where a variable/function is used.',
2220
+ inputSchema: {
2221
+ type: 'object',
2222
+ properties: {
2223
+ uri: { type: 'string', description: 'File URI' },
2224
+ line: { type: 'number', description: 'Line number (0-based)' },
2225
+ character: { type: 'number', description: 'Character position (0-based)' },
2226
+ },
2227
+ required: ['uri', 'line', 'character'],
2228
+ },
2229
+ },
2230
+ {
2231
+ name: 'al_folding_ranges',
2232
+ description: 'Get code folding ranges (regions, procedures, comments). Useful for understanding document structure.',
2233
+ inputSchema: {
2234
+ type: 'object',
2235
+ properties: {
2236
+ uri: { type: 'string', description: 'File URI' },
2237
+ },
2238
+ required: ['uri'],
2239
+ },
2240
+ },
2241
+ {
2242
+ name: 'al_selection_range',
2243
+ description: 'Get smart selection ranges for expanding/shrinking selection. Returns nested ranges from inner to outer.',
2244
+ inputSchema: {
2245
+ type: 'object',
2246
+ properties: {
2247
+ uri: { type: 'string', description: 'File URI' },
2248
+ positions: {
2249
+ type: 'array',
2250
+ items: {
2251
+ type: 'object',
2252
+ properties: {
2253
+ line: { type: 'number' },
2254
+ character: { type: 'number' },
2255
+ },
2256
+ },
2257
+ description: 'Positions to get selection ranges for'
2258
+ },
2259
+ },
2260
+ required: ['uri', 'positions'],
2261
+ },
2262
+ },
2263
+ {
2264
+ name: 'al_type_definition',
2265
+ description: 'Go to the type definition of a variable. E.g., for "var x: Customer", goes to Customer table.',
2266
+ inputSchema: {
2267
+ type: 'object',
2268
+ properties: {
2269
+ uri: { type: 'string', description: 'File URI' },
2270
+ line: { type: 'number', description: 'Line number (0-based)' },
2271
+ character: { type: 'number', description: 'Character position (0-based)' },
2272
+ },
2273
+ required: ['uri', 'line', 'character'],
2274
+ },
2275
+ },
2276
+ {
2277
+ name: 'al_implementation',
2278
+ description: 'Find implementations of an interface or abstract method.',
2279
+ inputSchema: {
2280
+ type: 'object',
2281
+ properties: {
2282
+ uri: { type: 'string', description: 'File URI' },
2283
+ line: { type: 'number', description: 'Line number (0-based)' },
2284
+ character: { type: 'number', description: 'Character position (0-based)' },
2285
+ },
2286
+ required: ['uri', 'line', 'character'],
2287
+ },
2288
+ },
2289
+ {
2290
+ name: 'al_format_on_type',
2291
+ description: 'Format code after typing a specific character (e.g., semicolon, closing brace).',
2292
+ inputSchema: {
2293
+ type: 'object',
2294
+ properties: {
2295
+ uri: { type: 'string', description: 'File URI' },
2296
+ line: { type: 'number', description: 'Line number (0-based)' },
2297
+ character: { type: 'number', description: 'Character position (0-based)' },
2298
+ ch: { type: 'string', description: 'Character that was typed (e.g., ";", "}")' },
2299
+ tabSize: { type: 'number', description: 'Tab size (default: 4)' },
2300
+ insertSpaces: { type: 'boolean', description: 'Use spaces instead of tabs (default: true)' },
2301
+ },
2302
+ required: ['uri', 'line', 'character', 'ch'],
2303
+ },
2304
+ },
2305
+ {
2306
+ name: 'al_code_lens',
2307
+ description: 'Get code lenses (inline hints like reference counts, run test buttons).',
2308
+ inputSchema: {
2309
+ type: 'object',
2310
+ properties: {
2311
+ uri: { type: 'string', description: 'File URI' },
2312
+ resolve: { type: 'boolean', description: 'Resolve lens commands (default: false)' },
2313
+ },
2314
+ required: ['uri'],
2315
+ },
2316
+ },
2317
+ {
2318
+ name: 'al_document_links',
2319
+ description: 'Get clickable document links (URLs in comments, file references).',
2320
+ inputSchema: {
2321
+ type: 'object',
2322
+ properties: {
2323
+ uri: { type: 'string', description: 'File URI' },
2324
+ },
2325
+ required: ['uri'],
2326
+ },
2327
+ },
2328
+ {
2329
+ name: 'al_execute_command',
2330
+ description: 'Execute an LSP command (from code actions or code lenses).',
2331
+ inputSchema: {
2332
+ type: 'object',
2333
+ properties: {
2334
+ command: { type: 'string', description: 'Command identifier' },
2335
+ arguments: {
2336
+ type: 'array',
2337
+ items: {},
2338
+ description: 'Command arguments'
2339
+ },
2340
+ },
2341
+ required: ['command'],
2342
+ },
2343
+ },
2344
+ {
2345
+ name: 'al_semantic_tokens',
2346
+ description: 'Get semantic tokens for syntax highlighting. Returns token types and modifiers.',
2347
+ inputSchema: {
2348
+ type: 'object',
2349
+ properties: {
2350
+ uri: { type: 'string', description: 'File URI' },
2351
+ startLine: { type: 'number', description: 'Start line for range (omit for full document)' },
2352
+ startCharacter: { type: 'number', description: 'Start character for range' },
2353
+ endLine: { type: 'number', description: 'End line for range' },
2354
+ endCharacter: { type: 'number', description: 'End character for range' },
2355
+ },
2356
+ required: ['uri'],
2357
+ },
2358
+ },
2359
+ {
2360
+ name: 'al_close_document',
2361
+ description: 'Close a document in the language server (cleanup resources).',
2362
+ inputSchema: {
2363
+ type: 'object',
2364
+ properties: {
2365
+ uri: { type: 'string', description: 'File URI to close' },
2366
+ },
2367
+ required: ['uri'],
2368
+ },
2369
+ },
2370
+ {
2371
+ name: 'al_save_document',
2372
+ description: 'Send save notification to language server (may trigger recompilation).',
2373
+ inputSchema: {
2374
+ type: 'object',
2375
+ properties: {
2376
+ uri: { type: 'string', description: 'File URI' },
2377
+ text: { type: 'string', description: 'Optional: current file content' },
2378
+ },
2379
+ required: ['uri'],
2380
+ },
2381
+ },
2382
+ // ==================== Extended AL Tools ====================
2383
+ {
2384
+ name: 'al_restart_server',
2385
+ description: 'Restart the AL Language Server. Use when the server hangs or after external changes.',
2386
+ inputSchema: {
2387
+ type: 'object',
2388
+ properties: {},
2389
+ required: [],
2390
+ },
2391
+ },
2392
+ {
2393
+ name: 'al_find_referencing_symbols',
2394
+ description: 'Find all symbols that reference the symbol at a position. Returns context around each reference.',
2395
+ inputSchema: {
2396
+ type: 'object',
2397
+ properties: {
2398
+ uri: { type: 'string', description: 'File URI' },
2399
+ line: { type: 'number', description: 'Line number (0-based)' },
2400
+ character: { type: 'number', description: 'Character position (0-based)' },
2401
+ includeDeclaration: { type: 'boolean', description: 'Include the declaration itself (default: false)' },
2402
+ contextLinesBefore: { type: 'number', description: 'Lines of context before reference (default: 1)' },
2403
+ contextLinesAfter: { type: 'number', description: 'Lines of context after reference (default: 1)' },
2404
+ },
2405
+ required: ['uri', 'line', 'character'],
2406
+ },
2407
+ },
2408
+ {
2409
+ name: 'al_insert_before_symbol',
2410
+ description: 'Insert content before a named symbol (e.g., add import, decorator, or method before a class).',
2411
+ inputSchema: {
2412
+ type: 'object',
2413
+ properties: {
2414
+ uri: { type: 'string', description: 'File URI' },
2415
+ symbolName: { type: 'string', description: 'Name of the symbol to insert before' },
2416
+ content: { type: 'string', description: 'Content to insert' },
2417
+ },
2418
+ required: ['uri', 'symbolName', 'content'],
2419
+ },
2420
+ },
2421
+ {
2422
+ name: 'find_file',
2423
+ description: 'Find files matching a pattern (supports * and ? wildcards).',
2424
+ inputSchema: {
2425
+ type: 'object',
2426
+ properties: {
2427
+ pattern: { type: 'string', description: 'File pattern to match (e.g., "*.Table.al", "Customer*")' },
2428
+ directory: { type: 'string', description: 'Directory to search in (default: workspace root)' },
2429
+ },
2430
+ required: ['pattern'],
2431
+ },
2432
+ },
2433
+ {
2434
+ name: 'replace_content',
2435
+ description: 'Replace content in a file using literal string or regex. Powerful for multi-line edits.',
2436
+ inputSchema: {
2437
+ type: 'object',
2438
+ properties: {
2439
+ path: { type: 'string', description: 'File path' },
2440
+ needle: { type: 'string', description: 'String or regex pattern to find' },
2441
+ replacement: { type: 'string', description: 'Replacement text' },
2442
+ mode: { type: 'string', enum: ['literal', 'regex'], description: 'Match mode (default: literal)' },
2443
+ allowMultiple: { type: 'boolean', description: 'Allow replacing multiple matches (default: false)' },
2444
+ },
2445
+ required: ['path', 'needle', 'replacement'],
2446
+ },
2447
+ },
2448
+ {
2449
+ name: 'edit_memory',
2450
+ description: 'Edit a memory using search/replace (supports regex).',
2451
+ inputSchema: {
2452
+ type: 'object',
2453
+ properties: {
2454
+ name: { type: 'string', description: 'Memory name' },
2455
+ needle: { type: 'string', description: 'String or regex pattern to find' },
2456
+ replacement: { type: 'string', description: 'Replacement text' },
2457
+ mode: { type: 'string', enum: ['literal', 'regex'], description: 'Match mode (default: literal)' },
2458
+ allowMultiple: { type: 'boolean', description: 'Allow replacing multiple matches (default: false)' },
2459
+ },
2460
+ required: ['name', 'needle', 'replacement'],
2461
+ },
2462
+ },
2463
+ {
2464
+ name: 'read_file',
2465
+ description: 'Read the contents of a file',
2466
+ inputSchema: {
2467
+ type: 'object',
2468
+ properties: {
2469
+ path: { type: 'string', description: 'File path' },
2470
+ },
2471
+ required: ['path'],
2472
+ },
2473
+ },
2474
+ {
2475
+ name: 'write_file',
2476
+ description: 'Write content to a file',
2477
+ inputSchema: {
2478
+ type: 'object',
2479
+ properties: {
2480
+ path: { type: 'string', description: 'File path' },
2481
+ content: { type: 'string', description: 'File content' },
2482
+ },
2483
+ required: ['path', 'content'],
2484
+ },
2485
+ },
541
2486
  {
542
- name: 'al_get_started',
543
- description: 'Start AL development - shows available workflows, prompts, and connection status. Call this first to understand what tools and prompts are available.',
2487
+ name: 'list_files',
2488
+ description: 'List files in a directory',
2489
+ inputSchema: {
2490
+ type: 'object',
2491
+ properties: {
2492
+ path: { type: 'string', description: 'Directory path' },
2493
+ pattern: { type: 'string', description: 'File pattern (e.g., *.al)' },
2494
+ },
2495
+ required: ['path'],
2496
+ },
2497
+ },
2498
+ {
2499
+ name: 'search_files',
2500
+ description: 'Search for text in files',
2501
+ inputSchema: {
2502
+ type: 'object',
2503
+ properties: {
2504
+ path: { type: 'string', description: 'Directory path' },
2505
+ query: { type: 'string', description: 'Search query' },
2506
+ filePattern: { type: 'string', description: 'File pattern (default: *.al)' },
2507
+ },
2508
+ required: ['path', 'query'],
2509
+ },
2510
+ },
2511
+ // ==================== Symbol-Based Editing Tools ====================
2512
+ {
2513
+ name: 'al_rename_symbol',
2514
+ description: 'Rename a symbol (variable, procedure, field, etc.) across the entire workspace. Uses LSP refactoring capabilities for accurate renaming.',
2515
+ inputSchema: {
2516
+ type: 'object',
2517
+ properties: {
2518
+ uri: { type: 'string', description: 'File URI where the symbol is defined' },
2519
+ line: { type: 'number', description: 'Line number of the symbol (0-based)' },
2520
+ character: { type: 'number', description: 'Character position of the symbol (0-based)' },
2521
+ newName: { type: 'string', description: 'The new name for the symbol' },
2522
+ },
2523
+ required: ['uri', 'line', 'character', 'newName'],
2524
+ },
2525
+ },
2526
+ {
2527
+ name: 'al_insert_after_symbol',
2528
+ description: 'Insert content after a named symbol (procedure, field, etc.). Useful for adding new procedures or fields after existing ones.',
2529
+ inputSchema: {
2530
+ type: 'object',
2531
+ properties: {
2532
+ uri: { type: 'string', description: 'File URI' },
2533
+ symbolName: { type: 'string', description: 'Name of the symbol to insert after' },
2534
+ content: { type: 'string', description: 'Content to insert' },
2535
+ },
2536
+ required: ['uri', 'symbolName', 'content'],
2537
+ },
2538
+ },
2539
+ {
2540
+ name: 'al_replace_symbol_body',
2541
+ description: 'Replace the entire body of a symbol (procedure body, trigger body, etc.). Useful for rewriting implementations.',
2542
+ inputSchema: {
2543
+ type: 'object',
2544
+ properties: {
2545
+ uri: { type: 'string', description: 'File URI' },
2546
+ symbolName: { type: 'string', description: 'Name of the symbol to replace' },
2547
+ newBody: { type: 'string', description: 'New body content for the symbol' },
2548
+ },
2549
+ required: ['uri', 'symbolName', 'newBody'],
2550
+ },
2551
+ },
2552
+ // ==================== Advanced File Operations ====================
2553
+ {
2554
+ name: 'delete_lines',
2555
+ description: 'Delete a range of lines from a file. Line numbers are 1-based.',
2556
+ inputSchema: {
2557
+ type: 'object',
2558
+ properties: {
2559
+ path: { type: 'string', description: 'File path' },
2560
+ startLine: { type: 'number', description: 'First line to delete (1-based)' },
2561
+ endLine: { type: 'number', description: 'Last line to delete (1-based, inclusive)' },
2562
+ },
2563
+ required: ['path', 'startLine', 'endLine'],
2564
+ },
2565
+ },
2566
+ {
2567
+ name: 'replace_lines',
2568
+ description: 'Replace a range of lines in a file with new content. Line numbers are 1-based.',
2569
+ inputSchema: {
2570
+ type: 'object',
2571
+ properties: {
2572
+ path: { type: 'string', description: 'File path' },
2573
+ startLine: { type: 'number', description: 'First line to replace (1-based)' },
2574
+ endLine: { type: 'number', description: 'Last line to replace (1-based, inclusive)' },
2575
+ newContent: { type: 'string', description: 'New content to insert (can be multi-line)' },
2576
+ },
2577
+ required: ['path', 'startLine', 'endLine', 'newContent'],
2578
+ },
2579
+ },
2580
+ {
2581
+ name: 'insert_at_line',
2582
+ description: 'Insert content at a specific line number. Existing content is pushed down. Line number is 1-based.',
2583
+ inputSchema: {
2584
+ type: 'object',
2585
+ properties: {
2586
+ path: { type: 'string', description: 'File path' },
2587
+ line: { type: 'number', description: 'Line number to insert at (1-based)' },
2588
+ content: { type: 'string', description: 'Content to insert (can be multi-line)' },
2589
+ },
2590
+ required: ['path', 'line', 'content'],
2591
+ },
2592
+ },
2593
+ // ==================== Project Memory Tools ====================
2594
+ {
2595
+ name: 'write_memory',
2596
+ description: 'Save project-specific knowledge/memory for future reference. Memories persist across sessions and can be used to maintain context about the project.',
2597
+ inputSchema: {
2598
+ type: 'object',
2599
+ properties: {
2600
+ name: { type: 'string', description: 'Memory name (unique identifier)' },
2601
+ content: { type: 'string', description: 'Memory content (markdown supported)' },
2602
+ tags: {
2603
+ type: 'array',
2604
+ items: { type: 'string' },
2605
+ description: 'Optional tags for categorization'
2606
+ },
2607
+ },
2608
+ required: ['name', 'content'],
2609
+ },
2610
+ },
2611
+ {
2612
+ name: 'read_memory',
2613
+ description: 'Read a specific memory by name. Use list_memories first to see available memories.',
2614
+ inputSchema: {
2615
+ type: 'object',
2616
+ properties: {
2617
+ name: { type: 'string', description: 'Memory name to read' },
2618
+ },
2619
+ required: ['name'],
2620
+ },
2621
+ },
2622
+ {
2623
+ name: 'list_memories',
2624
+ description: 'List all saved project memories. Returns names, timestamps, tags, and content previews.',
544
2625
  inputSchema: {
545
2626
  type: 'object',
546
2627
  properties: {},
@@ -548,142 +2629,347 @@ class ToolRouter {
548
2629
  },
549
2630
  },
550
2631
  {
551
- name: 'al_get_symbols',
552
- description: 'Get all symbols (procedures, fields, variables, etc.) in an AL file',
2632
+ name: 'delete_memory',
2633
+ description: 'Delete a specific memory by name.',
553
2634
  inputSchema: {
554
2635
  type: 'object',
555
2636
  properties: {
556
- uri: {
557
- type: 'string',
558
- description: 'File URI (e.g., file:///C:/project/src/MyTable.Table.al)',
559
- },
2637
+ name: { type: 'string', description: 'Memory name to delete' },
560
2638
  },
561
- required: ['uri'],
2639
+ required: ['name'],
562
2640
  },
563
2641
  },
2642
+ // ==================== BC Container Tools ====================
564
2643
  {
565
- name: 'al_find_symbol',
566
- description: 'Search for symbols by name across the workspace',
2644
+ name: 'bc_list_containers',
2645
+ description: 'List all Business Central Docker containers. Shows container name, image, status, and ports.',
2646
+ inputSchema: {
2647
+ type: 'object',
2648
+ properties: {},
2649
+ required: [],
2650
+ },
2651
+ },
2652
+ {
2653
+ name: 'bc_compile',
2654
+ description: 'Compile the AL project in a BC container. Runs all CodeCops (AppSource, UI, PerTenant). Returns app file path, errors, and warnings.',
567
2655
  inputSchema: {
568
2656
  type: 'object',
569
2657
  properties: {
570
- query: {
2658
+ containerName: { type: 'string', description: 'Name of the BC container' },
2659
+ appProjectFolder: { type: 'string', description: 'Path to AL project folder (default: workspace root)' },
2660
+ outputFolder: { type: 'string', description: 'Path for output .app file (default: .output)' },
2661
+ },
2662
+ required: ['containerName'],
2663
+ },
2664
+ },
2665
+ {
2666
+ name: 'bc_publish',
2667
+ description: 'Publish an AL app to a BC container. Automatically finds the latest compiled .app file or uses specified path.',
2668
+ inputSchema: {
2669
+ type: 'object',
2670
+ properties: {
2671
+ containerName: { type: 'string', description: 'Name of the BC container' },
2672
+ appFile: { type: 'string', description: 'Path to .app file (default: latest in .output)' },
2673
+ syncMode: {
571
2674
  type: 'string',
572
- description: 'Symbol name to search for',
2675
+ enum: ['Add', 'Clean', 'Development', 'ForceSync'],
2676
+ description: 'Sync mode for schema changes (default: Development)'
573
2677
  },
2678
+ skipVerification: { type: 'boolean', description: 'Skip code signing verification' },
2679
+ install: { type: 'boolean', description: 'Install the app after publishing' },
574
2680
  },
575
- required: ['query'],
2681
+ required: ['containerName'],
576
2682
  },
577
2683
  },
578
2684
  {
579
- name: 'al_find_references',
580
- description: 'Find all references to a symbol at a specific position',
2685
+ name: 'bc_run_tests',
2686
+ description: 'Run automated tests in a BC container. Can run all tests, specific codeunit, or specific test function.',
581
2687
  inputSchema: {
582
2688
  type: 'object',
583
2689
  properties: {
584
- uri: { type: 'string', description: 'File URI' },
585
- line: { type: 'number', description: 'Line number (0-based)' },
586
- character: { type: 'number', description: 'Character position (0-based)' },
2690
+ containerName: { type: 'string', description: 'Name of the BC container' },
2691
+ testCodeunit: { type: 'number', description: 'Specific test codeunit ID to run' },
2692
+ testFunction: { type: 'string', description: 'Specific test function name to run' },
2693
+ extensionId: { type: 'string', description: 'Extension ID to filter tests' },
2694
+ detailed: { type: 'boolean', description: 'Return detailed test results' },
587
2695
  },
588
- required: ['uri', 'line', 'character'],
2696
+ required: ['containerName'],
589
2697
  },
590
2698
  },
591
2699
  {
592
- name: 'al_get_diagnostics',
593
- description: 'Get compiler diagnostics (errors, warnings) for an AL file',
2700
+ name: 'bc_container_logs',
2701
+ description: 'Get logs from a BC container. Useful for debugging startup or runtime issues.',
594
2702
  inputSchema: {
595
2703
  type: 'object',
596
2704
  properties: {
597
- uri: { type: 'string', description: 'File URI' },
2705
+ containerName: { type: 'string', description: 'Name of the BC container' },
2706
+ tail: { type: 'number', description: 'Number of lines from end (default: all)' },
2707
+ since: { type: 'string', description: 'Show logs since timestamp (e.g., "2h", "30m")' },
598
2708
  },
599
- required: ['uri'],
2709
+ required: ['containerName'],
600
2710
  },
601
2711
  },
602
2712
  {
603
- name: 'al_go_to_definition',
604
- description: 'Go to the definition of a symbol at a specific position',
2713
+ name: 'bc_start_container',
2714
+ description: 'Start a stopped BC container.',
605
2715
  inputSchema: {
606
2716
  type: 'object',
607
2717
  properties: {
608
- uri: { type: 'string', description: 'File URI' },
609
- line: { type: 'number', description: 'Line number (0-based)' },
610
- character: { type: 'number', description: 'Character position (0-based)' },
2718
+ containerName: { type: 'string', description: 'Name of the BC container' },
611
2719
  },
612
- required: ['uri', 'line', 'character'],
2720
+ required: ['containerName'],
613
2721
  },
614
2722
  },
615
2723
  {
616
- name: 'al_hover',
617
- description: 'Get hover information (type info, documentation) for a position',
2724
+ name: 'bc_stop_container',
2725
+ description: 'Stop a running BC container.',
618
2726
  inputSchema: {
619
2727
  type: 'object',
620
2728
  properties: {
621
- uri: { type: 'string', description: 'File URI' },
622
- line: { type: 'number', description: 'Line number (0-based)' },
623
- character: { type: 'number', description: 'Character position (0-based)' },
2729
+ containerName: { type: 'string', description: 'Name of the BC container' },
624
2730
  },
625
- required: ['uri', 'line', 'character'],
2731
+ required: ['containerName'],
626
2732
  },
627
2733
  },
628
2734
  {
629
- name: 'al_completion',
630
- description: 'Get code completion suggestions at a position',
2735
+ name: 'bc_restart_container',
2736
+ description: 'Restart a BC container.',
631
2737
  inputSchema: {
632
2738
  type: 'object',
633
2739
  properties: {
634
- uri: { type: 'string', description: 'File URI' },
635
- line: { type: 'number', description: 'Line number (0-based)' },
636
- character: { type: 'number', description: 'Character position (0-based)' },
2740
+ containerName: { type: 'string', description: 'Name of the BC container' },
637
2741
  },
638
- required: ['uri', 'line', 'character'],
2742
+ required: ['containerName'],
639
2743
  },
640
2744
  },
641
2745
  {
642
- name: 'read_file',
643
- description: 'Read the contents of a file',
2746
+ name: 'bc_download_symbols',
2747
+ description: 'Download symbol files (.app) from a BC container. Required for code completion and compilation.',
644
2748
  inputSchema: {
645
2749
  type: 'object',
646
2750
  properties: {
647
- path: { type: 'string', description: 'File path' },
2751
+ containerName: { type: 'string', description: 'Name of the BC container' },
2752
+ targetFolder: { type: 'string', description: 'Target folder for symbols (default: .alpackages)' },
648
2753
  },
649
- required: ['path'],
2754
+ required: ['containerName'],
650
2755
  },
651
2756
  },
652
2757
  {
653
- name: 'write_file',
654
- description: 'Write content to a file',
2758
+ name: 'bc_create_container',
2759
+ description: 'Create a new Business Central Docker container using BcContainerHelper. Takes ~10-30 minutes.',
655
2760
  inputSchema: {
656
2761
  type: 'object',
657
2762
  properties: {
658
- path: { type: 'string', description: 'File path' },
659
- content: { type: 'string', description: 'File content' },
2763
+ containerName: { type: 'string', description: 'Name for the new container' },
2764
+ version: { type: 'string', description: 'BC version (e.g., "23.0", "24.1"). Default: latest' },
2765
+ country: { type: 'string', description: 'Country/region (e.g., "us", "w1", "de"). Default: us' },
2766
+ type: { type: 'string', enum: ['Sandbox', 'OnPrem'], description: 'Container type. Default: Sandbox' },
2767
+ auth: { type: 'string', enum: ['UserPassword', 'NavUserPassword', 'Windows', 'AAD'], description: 'Authentication type' },
2768
+ username: { type: 'string', description: 'Admin username (with auth)' },
2769
+ password: { type: 'string', description: 'Admin password (with auth)' },
2770
+ artifactUrl: { type: 'string', description: 'Direct artifact URL (overrides version/country/type)' },
2771
+ licenseFile: { type: 'string', description: 'Path to license file' },
2772
+ accept_eula: { type: 'boolean', description: 'Accept EULA (default: true)' },
2773
+ accept_outdated: { type: 'boolean', description: 'Accept outdated images' },
2774
+ includeTestToolkit: { type: 'boolean', description: 'Include test toolkit' },
2775
+ includeTestLibrariesOnly: { type: 'boolean', description: 'Include only test libraries' },
2776
+ includeTestFrameworkOnly: { type: 'boolean', description: 'Include only test framework' },
2777
+ enableTaskScheduler: { type: 'boolean', description: 'Enable task scheduler' },
2778
+ assignPremiumPlan: { type: 'boolean', description: 'Assign premium plan to admin' },
2779
+ multitenant: { type: 'boolean', description: 'Create multitenant container' },
2780
+ memoryLimit: { type: 'string', description: 'Memory limit (e.g., "8G")' },
2781
+ isolation: { type: 'string', enum: ['hyperv', 'process'], description: 'Container isolation mode' },
2782
+ updateHosts: { type: 'boolean', description: 'Update hosts file with container name' },
660
2783
  },
661
- required: ['path', 'content'],
2784
+ required: ['containerName'],
662
2785
  },
663
2786
  },
664
2787
  {
665
- name: 'list_files',
666
- description: 'List files in a directory',
2788
+ name: 'bc_remove_container',
2789
+ description: 'Remove a Business Central Docker container and clean up resources.',
667
2790
  inputSchema: {
668
2791
  type: 'object',
669
2792
  properties: {
670
- path: { type: 'string', description: 'Directory path' },
671
- pattern: { type: 'string', description: 'File pattern (e.g., *.al)' },
2793
+ containerName: { type: 'string', description: 'Name of the container to remove' },
2794
+ force: { type: 'boolean', description: 'Force remove even if running (default: false)' },
672
2795
  },
673
- required: ['path'],
2796
+ required: ['containerName'],
674
2797
  },
675
2798
  },
676
2799
  {
677
- name: 'search_files',
678
- description: 'Search for text in files',
2800
+ name: 'bc_get_extensions',
2801
+ description: 'List all installed extensions/apps in a BC container.',
679
2802
  inputSchema: {
680
2803
  type: 'object',
681
2804
  properties: {
682
- path: { type: 'string', description: 'Directory path' },
683
- query: { type: 'string', description: 'Search query' },
684
- filePattern: { type: 'string', description: 'File pattern (default: *.al)' },
2805
+ containerName: { type: 'string', description: 'Name of the BC container' },
685
2806
  },
686
- required: ['path', 'query'],
2807
+ required: ['containerName'],
2808
+ },
2809
+ },
2810
+ {
2811
+ name: 'bc_uninstall_app',
2812
+ description: 'Uninstall an app from a BC container.',
2813
+ inputSchema: {
2814
+ type: 'object',
2815
+ properties: {
2816
+ containerName: { type: 'string', description: 'Name of the BC container' },
2817
+ name: { type: 'string', description: 'Name of the app to uninstall' },
2818
+ publisher: { type: 'string', description: 'Publisher of the app (optional)' },
2819
+ version: { type: 'string', description: 'Version of the app (optional)' },
2820
+ force: { type: 'boolean', description: 'Force uninstall (default: false)' },
2821
+ username: { type: 'string', description: 'Admin username (optional)' },
2822
+ password: { type: 'string', description: 'Admin password (optional)' },
2823
+ },
2824
+ required: ['containerName', 'name'],
2825
+ },
2826
+ },
2827
+ {
2828
+ name: 'bc_compile_warnings',
2829
+ description: 'Compile AL project and return only warnings (quick check without full build output).',
2830
+ inputSchema: {
2831
+ type: 'object',
2832
+ properties: {
2833
+ containerName: { type: 'string', description: 'Name of the BC container' },
2834
+ appProjectFolder: { type: 'string', description: 'Path to app project folder (default: workspace root)' },
2835
+ },
2836
+ required: ['containerName'],
2837
+ },
2838
+ },
2839
+ // ==================== Git Tools ====================
2840
+ {
2841
+ name: 'git_status',
2842
+ description: 'Get current Git status including branch, staged/modified/untracked files, and ahead/behind counts.',
2843
+ inputSchema: {
2844
+ type: 'object',
2845
+ properties: {},
2846
+ required: [],
2847
+ },
2848
+ },
2849
+ {
2850
+ name: 'git_diff',
2851
+ description: 'Show Git diff of changes. Can show all changes, staged changes, or changes to a specific file.',
2852
+ inputSchema: {
2853
+ type: 'object',
2854
+ properties: {
2855
+ staged: { type: 'boolean', description: 'Show only staged changes' },
2856
+ file: { type: 'string', description: 'Show diff for a specific file' },
2857
+ unified: { type: 'number', description: 'Number of context lines' },
2858
+ },
2859
+ required: [],
2860
+ },
2861
+ },
2862
+ {
2863
+ name: 'git_stage',
2864
+ description: 'Stage files for commit. Use "all" to stage all changes.',
2865
+ inputSchema: {
2866
+ type: 'object',
2867
+ properties: {
2868
+ paths: {
2869
+ oneOf: [
2870
+ { type: 'array', items: { type: 'string' } },
2871
+ { type: 'string', enum: ['all'] }
2872
+ ],
2873
+ description: 'Files to stage, or "all" for all changes'
2874
+ },
2875
+ },
2876
+ required: ['paths'],
2877
+ },
2878
+ },
2879
+ {
2880
+ name: 'git_commit',
2881
+ description: 'Commit staged changes with a message.',
2882
+ inputSchema: {
2883
+ type: 'object',
2884
+ properties: {
2885
+ message: { type: 'string', description: 'Commit message' },
2886
+ amend: { type: 'boolean', description: 'Amend the previous commit' },
2887
+ allowEmpty: { type: 'boolean', description: 'Allow empty commit' },
2888
+ },
2889
+ required: ['message'],
2890
+ },
2891
+ },
2892
+ {
2893
+ name: 'git_log',
2894
+ description: 'Show commit history with filtering options.',
2895
+ inputSchema: {
2896
+ type: 'object',
2897
+ properties: {
2898
+ limit: { type: 'number', description: 'Number of commits (default: 20)' },
2899
+ since: { type: 'string', description: 'Show commits since date (e.g., "2 weeks ago")' },
2900
+ author: { type: 'string', description: 'Filter by author name/email' },
2901
+ grep: { type: 'string', description: 'Search commit messages' },
2902
+ file: { type: 'string', description: 'Show commits for a specific file' },
2903
+ },
2904
+ required: [],
2905
+ },
2906
+ },
2907
+ {
2908
+ name: 'git_branches',
2909
+ description: 'List Git branches. Shows current branch, tracking info, and upstream.',
2910
+ inputSchema: {
2911
+ type: 'object',
2912
+ properties: {
2913
+ remote: { type: 'boolean', description: 'List remote branches instead of local' },
2914
+ },
2915
+ required: [],
2916
+ },
2917
+ },
2918
+ {
2919
+ name: 'git_checkout',
2920
+ description: 'Switch to a branch or create a new branch.',
2921
+ inputSchema: {
2922
+ type: 'object',
2923
+ properties: {
2924
+ target: { type: 'string', description: 'Branch name to switch to' },
2925
+ create: { type: 'boolean', description: 'Create new branch (-b flag)' },
2926
+ },
2927
+ required: ['target'],
2928
+ },
2929
+ },
2930
+ {
2931
+ name: 'git_pull',
2932
+ description: 'Pull changes from remote repository.',
2933
+ inputSchema: {
2934
+ type: 'object',
2935
+ properties: {
2936
+ remote: { type: 'string', description: 'Remote name (default: origin)' },
2937
+ branch: { type: 'string', description: 'Branch to pull' },
2938
+ rebase: { type: 'boolean', description: 'Rebase instead of merge' },
2939
+ },
2940
+ required: [],
2941
+ },
2942
+ },
2943
+ {
2944
+ name: 'git_push',
2945
+ description: 'Push commits to remote repository.',
2946
+ inputSchema: {
2947
+ type: 'object',
2948
+ properties: {
2949
+ remote: { type: 'string', description: 'Remote name (default: origin)' },
2950
+ branch: { type: 'string', description: 'Branch to push' },
2951
+ setUpstream: { type: 'boolean', description: 'Set upstream (-u flag)' },
2952
+ force: { type: 'boolean', description: 'Force push with lease' },
2953
+ },
2954
+ required: [],
2955
+ },
2956
+ },
2957
+ {
2958
+ name: 'git_stash',
2959
+ description: 'Manage Git stashes (save, pop, list).',
2960
+ inputSchema: {
2961
+ type: 'object',
2962
+ properties: {
2963
+ action: {
2964
+ type: 'string',
2965
+ enum: ['save', 'push', 'pop', 'list'],
2966
+ description: 'Action to perform (default: list)'
2967
+ },
2968
+ message: { type: 'string', description: 'Stash message (for save/push)' },
2969
+ includeUntracked: { type: 'boolean', description: 'Include untracked files (for save/push)' },
2970
+ index: { type: 'number', description: 'Stash index (for pop)' },
2971
+ },
2972
+ required: [],
687
2973
  },
688
2974
  },
689
2975
  ];