partnercore-proxy 0.1.4 → 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,6 +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);
224
+ case 'al_get_started':
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);
176
301
  default:
177
302
  return {
178
303
  success: false,
@@ -294,300 +419,2557 @@ class ToolRouter {
294
419
  const completions = await this.alServer.getCompletions(uri, line, character);
295
420
  return { success: true, content: completions };
296
421
  }
297
- // ==================== File System Tool Handlers ====================
298
- // All file operations are sandboxed to the workspace root for security
299
- handleReadFile(args) {
300
- // Validate required arguments
301
- (0, security_js_1.validateToolArgs)(args, ['path'], { path: 'string' });
302
- const filePath = (0, security_js_1.sanitizeString)(args['path']);
303
- // Detect workspace dynamically if needed
304
- const workspaceRoot = this.getWorkspaceRoot(filePath);
305
- // Security: Sanitize path to prevent directory traversal
306
- const resolved = (0, security_js_1.sanitizePath)(filePath, workspaceRoot);
307
- if (!fs.existsSync(resolved)) {
308
- 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 };
309
426
  }
310
- const stat = fs.statSync(resolved);
311
- if (!stat.isFile()) {
312
- 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 } };
313
435
  }
314
- // Security: Limit file size to prevent memory exhaustion
315
- const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
316
- if (stat.size > MAX_FILE_SIZE) {
317
- 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
+ };
318
442
  }
319
- const content = fs.readFileSync(resolved, 'utf-8');
320
- return { success: true, content };
321
- }
322
- handleWriteFile(args) {
323
- // Validate required arguments
324
- (0, security_js_1.validateToolArgs)(args, ['path', 'content'], { path: 'string', content: 'string' });
325
- const filePath = (0, security_js_1.sanitizeString)(args['path']);
326
- const content = args['content'];
327
- // Detect workspace dynamically if needed
328
- const workspaceRoot = this.getWorkspaceRoot(filePath);
329
- // Security: Sanitize path to prevent directory traversal
330
- const resolved = (0, security_js_1.sanitizePath)(filePath, workspaceRoot);
331
- const dir = path.dirname(resolved);
332
- // Security: Limit content size
333
- const MAX_CONTENT_SIZE = 10 * 1024 * 1024; // 10MB
334
- if (content.length > MAX_CONTENT_SIZE) {
335
- 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 } };
336
446
  }
337
- fs.mkdirSync(dir, { recursive: true });
338
- fs.writeFileSync(resolved, content, 'utf-8');
339
- // Return relative path in response (don't expose full paths)
340
- const relativePath = path.relative(workspaceRoot, resolved);
341
- 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
+ };
342
472
  }
343
- handleListFiles(args) {
344
- // Validate required arguments
345
- (0, security_js_1.validateToolArgs)(args, ['path'], { path: 'string' });
346
- const dirPath = (0, security_js_1.sanitizeString)(args['path']);
347
- const pattern = args['pattern'] ? (0, security_js_1.sanitizeString)(args['pattern']) : undefined;
348
- // Detect workspace dynamically if needed
349
- const workspaceRoot = this.getWorkspaceRoot(dirPath);
350
- // Security: Sanitize path to prevent directory traversal
351
- const resolved = (0, security_js_1.sanitizePath)(dirPath, workspaceRoot);
352
- if (!fs.existsSync(resolved)) {
353
- 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 };
354
476
  }
355
- const stat = fs.statSync(resolved);
356
- if (!stat.isDirectory()) {
357
- 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' } };
358
488
  }
359
- const files = this.listFilesRecursive(resolved, pattern);
360
- // Return relative paths for security
361
- const relativePaths = files.map(f => path.relative(workspaceRoot, f));
362
- 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
+ };
363
504
  }
364
- listFilesRecursive(dir, pattern, depth = 0) {
365
- // Security: Limit recursion depth
366
- const MAX_DEPTH = 20;
367
- if (depth > MAX_DEPTH)
368
- return [];
369
- // Use imports from top of file (fs and path are already imported)
370
- const results = [];
371
- let entries;
372
- try {
373
- 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 };
374
508
  }
375
- catch {
376
- // Silently skip directories we can't read
377
- 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 });
378
527
  }
379
- // Security: Limit total files to prevent DoS
380
- const MAX_FILES = 10000;
381
- for (const entry of entries) {
382
- if (results.length >= MAX_FILES)
383
- break;
384
- const fullPath = path.join(dir, entry.name);
385
- // Security: Skip hidden files/directories and common ignored paths
386
- if (entry.name.startsWith('.'))
387
- continue;
388
- if (entry.isDirectory()) {
389
- // Skip common ignored directories
390
- const ignoredDirs = ['node_modules', '.git', '.svn', 'dist', 'bin', 'obj', '.alpackages', '.snapshots'];
391
- if (!ignoredDirs.includes(entry.name)) {
392
- 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;
393
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);
394
566
  }
395
- else {
396
- if (!pattern || this.matchesPattern(entry.name, pattern)) {
397
- 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,
398
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
+ })),
399
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 };
400
591
  }
401
- 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
+ };
402
609
  }
403
- matchesPattern(filename, pattern) {
404
- // Security: Escape regex special characters except * and ?
405
- const escaped = pattern
406
- .replace(/[.+^${}()|[\]\\]/g, '\\$&')
407
- .replace(/\*/g, '.*')
408
- .replace(/\?/g, '.');
409
- const regex = new RegExp(`^${escaped}$`, 'i');
410
- 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
+ };
411
628
  }
412
- handleSearchFiles(args) {
413
- // Validate required arguments
414
- (0, security_js_1.validateToolArgs)(args, ['path', 'query'], { path: 'string', query: 'string' });
415
- const dirPath = (0, security_js_1.sanitizeString)(args['path']);
416
- const query = (0, security_js_1.sanitizeString)(args['query'], 1000); // Limit query length
417
- const filePattern = args['filePattern'] ? (0, security_js_1.sanitizeString)(args['filePattern']) : '*.al';
418
- // Detect workspace dynamically if needed
419
- const workspaceRoot = this.getWorkspaceRoot(dirPath);
420
- // Security: Sanitize path
421
- const resolved = (0, security_js_1.sanitizePath)(dirPath, workspaceRoot);
422
- const files = this.listFilesRecursive(resolved, filePattern);
423
- const results = [];
424
- const queryLower = query.toLowerCase();
425
- // Security: Limit results
426
- const MAX_RESULTS = 500;
427
- for (const file of files) {
428
- if (results.length >= MAX_RESULTS)
429
- break;
430
- try {
431
- const content = fs.readFileSync(file, 'utf-8');
432
- const lines = content.split('\n');
433
- lines.forEach((line, index) => {
434
- if (results.length >= MAX_RESULTS)
435
- return;
436
- if (line.toLowerCase().includes(queryLower)) {
437
- results.push({
438
- file: path.relative(workspaceRoot, file), // Return relative paths
439
- line: index + 1,
440
- content: line.trim().slice(0, 500), // Limit line length in results
441
- });
442
- }
443
- });
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));
444
642
  }
445
- catch {
446
- // 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
+ })),
447
653
  }
654
+ };
655
+ }
656
+ async handleTypeDefinition(args) {
657
+ if (!this.alServer) {
658
+ return { success: false, content: 'AL Language Server not initialized', isError: true };
448
659
  }
449
- 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
+ };
450
677
  }
451
- // ==================== Tool Definitions ====================
452
- initLocalToolDefinitions() {
453
- this.localToolDefinitions = [
454
- {
455
- name: 'al_get_symbols',
456
- description: 'Get all symbols (procedures, fields, variables, etc.) in an AL file',
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
+ })),
697
+ }
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,
719
+ }
720
+ };
721
+ }
722
+ async handleCodeLens(args) {
723
+ if (!this.alServer) {
724
+ return { success: false, content: 'AL Language Server not initialized', isError: true };
725
+ }
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
+ }
743
+ };
744
+ }
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',
457
2477
  inputSchema: {
458
2478
  type: 'object',
459
2479
  properties: {
460
- uri: {
461
- type: 'string',
462
- description: 'File URI (e.g., file:///C:/project/src/MyTable.Table.al)',
463
- },
2480
+ path: { type: 'string', description: 'File path' },
2481
+ content: { type: 'string', description: 'File content' },
464
2482
  },
465
- required: ['uri'],
2483
+ required: ['path', 'content'],
466
2484
  },
467
2485
  },
468
2486
  {
469
- name: 'al_find_symbol',
470
- description: 'Search for symbols by name across the workspace',
2487
+ name: 'list_files',
2488
+ description: 'List files in a directory',
471
2489
  inputSchema: {
472
2490
  type: 'object',
473
2491
  properties: {
474
- query: {
475
- type: 'string',
476
- description: 'Symbol name to search for',
477
- },
2492
+ path: { type: 'string', description: 'Directory path' },
2493
+ pattern: { type: 'string', description: 'File pattern (e.g., *.al)' },
478
2494
  },
479
- required: ['query'],
2495
+ required: ['path'],
480
2496
  },
481
2497
  },
482
2498
  {
483
- name: 'al_find_references',
484
- description: 'Find all references to a symbol at a specific position',
2499
+ name: 'search_files',
2500
+ description: 'Search for text in files',
485
2501
  inputSchema: {
486
2502
  type: 'object',
487
2503
  properties: {
488
- uri: { type: 'string', description: 'File URI' },
489
- line: { type: 'number', description: 'Line number (0-based)' },
490
- character: { type: 'number', description: 'Character position (0-based)' },
2504
+ path: { type: 'string', description: 'Directory path' },
2505
+ query: { type: 'string', description: 'Search query' },
2506
+ filePattern: { type: 'string', description: 'File pattern (default: *.al)' },
491
2507
  },
492
- required: ['uri', 'line', 'character'],
2508
+ required: ['path', 'query'],
493
2509
  },
494
2510
  },
2511
+ // ==================== Symbol-Based Editing Tools ====================
495
2512
  {
496
- name: 'al_get_diagnostics',
497
- description: 'Get compiler diagnostics (errors, warnings) for an AL file',
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.',
498
2515
  inputSchema: {
499
2516
  type: 'object',
500
2517
  properties: {
501
- uri: { type: 'string', description: 'File URI' },
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' },
502
2522
  },
503
- required: ['uri'],
2523
+ required: ['uri', 'line', 'character', 'newName'],
504
2524
  },
505
2525
  },
506
2526
  {
507
- name: 'al_go_to_definition',
508
- description: 'Go to the definition of a symbol at a specific position',
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.',
509
2529
  inputSchema: {
510
2530
  type: 'object',
511
2531
  properties: {
512
2532
  uri: { type: 'string', description: 'File URI' },
513
- line: { type: 'number', description: 'Line number (0-based)' },
514
- character: { type: 'number', description: 'Character position (0-based)' },
2533
+ symbolName: { type: 'string', description: 'Name of the symbol to insert after' },
2534
+ content: { type: 'string', description: 'Content to insert' },
515
2535
  },
516
- required: ['uri', 'line', 'character'],
2536
+ required: ['uri', 'symbolName', 'content'],
517
2537
  },
518
2538
  },
519
2539
  {
520
- name: 'al_hover',
521
- description: 'Get hover information (type info, documentation) for a position',
2540
+ name: 'al_replace_symbol_body',
2541
+ description: 'Replace the entire body of a symbol (procedure body, trigger body, etc.). Useful for rewriting implementations.',
522
2542
  inputSchema: {
523
2543
  type: 'object',
524
2544
  properties: {
525
2545
  uri: { type: 'string', description: 'File URI' },
526
- line: { type: 'number', description: 'Line number (0-based)' },
527
- character: { type: 'number', description: 'Character position (0-based)' },
2546
+ symbolName: { type: 'string', description: 'Name of the symbol to replace' },
2547
+ newBody: { type: 'string', description: 'New body content for the symbol' },
528
2548
  },
529
- required: ['uri', 'line', 'character'],
2549
+ required: ['uri', 'symbolName', 'newBody'],
530
2550
  },
531
2551
  },
2552
+ // ==================== Advanced File Operations ====================
532
2553
  {
533
- name: 'al_completion',
534
- description: 'Get code completion suggestions at a position',
2554
+ name: 'delete_lines',
2555
+ description: 'Delete a range of lines from a file. Line numbers are 1-based.',
535
2556
  inputSchema: {
536
2557
  type: 'object',
537
2558
  properties: {
538
- uri: { type: 'string', description: 'File URI' },
539
- line: { type: 'number', description: 'Line number (0-based)' },
540
- character: { type: 'number', description: 'Character position (0-based)' },
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)' },
541
2562
  },
542
- required: ['uri', 'line', 'character'],
2563
+ required: ['path', 'startLine', 'endLine'],
543
2564
  },
544
2565
  },
545
2566
  {
546
- name: 'read_file',
547
- description: 'Read the contents of a file',
2567
+ name: 'replace_lines',
2568
+ description: 'Replace a range of lines in a file with new content. Line numbers are 1-based.',
548
2569
  inputSchema: {
549
2570
  type: 'object',
550
2571
  properties: {
551
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)' },
552
2576
  },
553
- required: ['path'],
2577
+ required: ['path', 'startLine', 'endLine', 'newContent'],
554
2578
  },
555
2579
  },
556
2580
  {
557
- name: 'write_file',
558
- description: 'Write content to a file',
2581
+ name: 'insert_at_line',
2582
+ description: 'Insert content at a specific line number. Existing content is pushed down. Line number is 1-based.',
559
2583
  inputSchema: {
560
2584
  type: 'object',
561
2585
  properties: {
562
2586
  path: { type: 'string', description: 'File path' },
563
- content: { type: 'string', description: 'File content' },
2587
+ line: { type: 'number', description: 'Line number to insert at (1-based)' },
2588
+ content: { type: 'string', description: 'Content to insert (can be multi-line)' },
564
2589
  },
565
- required: ['path', 'content'],
2590
+ required: ['path', 'line', 'content'],
566
2591
  },
567
2592
  },
2593
+ // ==================== Project Memory Tools ====================
568
2594
  {
569
- name: 'list_files',
570
- description: 'List files in a directory',
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.',
571
2597
  inputSchema: {
572
2598
  type: 'object',
573
2599
  properties: {
574
- path: { type: 'string', description: 'Directory path' },
575
- pattern: { type: 'string', description: 'File pattern (e.g., *.al)' },
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
+ },
576
2607
  },
577
- required: ['path'],
2608
+ required: ['name', 'content'],
578
2609
  },
579
2610
  },
580
2611
  {
581
- name: 'search_files',
582
- description: 'Search for text in files',
2612
+ name: 'read_memory',
2613
+ description: 'Read a specific memory by name. Use list_memories first to see available memories.',
583
2614
  inputSchema: {
584
2615
  type: 'object',
585
2616
  properties: {
586
- path: { type: 'string', description: 'Directory path' },
587
- query: { type: 'string', description: 'Search query' },
588
- filePattern: { type: 'string', description: 'File pattern (default: *.al)' },
2617
+ name: { type: 'string', description: 'Memory name to read' },
589
2618
  },
590
- required: ['path', 'query'],
2619
+ required: ['name'],
2620
+ },
2621
+ },
2622
+ {
2623
+ name: 'list_memories',
2624
+ description: 'List all saved project memories. Returns names, timestamps, tags, and content previews.',
2625
+ inputSchema: {
2626
+ type: 'object',
2627
+ properties: {},
2628
+ required: [],
2629
+ },
2630
+ },
2631
+ {
2632
+ name: 'delete_memory',
2633
+ description: 'Delete a specific memory by name.',
2634
+ inputSchema: {
2635
+ type: 'object',
2636
+ properties: {
2637
+ name: { type: 'string', description: 'Memory name to delete' },
2638
+ },
2639
+ required: ['name'],
2640
+ },
2641
+ },
2642
+ // ==================== BC Container Tools ====================
2643
+ {
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.',
2655
+ inputSchema: {
2656
+ type: 'object',
2657
+ properties: {
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: {
2674
+ type: 'string',
2675
+ enum: ['Add', 'Clean', 'Development', 'ForceSync'],
2676
+ description: 'Sync mode for schema changes (default: Development)'
2677
+ },
2678
+ skipVerification: { type: 'boolean', description: 'Skip code signing verification' },
2679
+ install: { type: 'boolean', description: 'Install the app after publishing' },
2680
+ },
2681
+ required: ['containerName'],
2682
+ },
2683
+ },
2684
+ {
2685
+ name: 'bc_run_tests',
2686
+ description: 'Run automated tests in a BC container. Can run all tests, specific codeunit, or specific test function.',
2687
+ inputSchema: {
2688
+ type: 'object',
2689
+ properties: {
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' },
2695
+ },
2696
+ required: ['containerName'],
2697
+ },
2698
+ },
2699
+ {
2700
+ name: 'bc_container_logs',
2701
+ description: 'Get logs from a BC container. Useful for debugging startup or runtime issues.',
2702
+ inputSchema: {
2703
+ type: 'object',
2704
+ properties: {
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")' },
2708
+ },
2709
+ required: ['containerName'],
2710
+ },
2711
+ },
2712
+ {
2713
+ name: 'bc_start_container',
2714
+ description: 'Start a stopped BC container.',
2715
+ inputSchema: {
2716
+ type: 'object',
2717
+ properties: {
2718
+ containerName: { type: 'string', description: 'Name of the BC container' },
2719
+ },
2720
+ required: ['containerName'],
2721
+ },
2722
+ },
2723
+ {
2724
+ name: 'bc_stop_container',
2725
+ description: 'Stop a running BC container.',
2726
+ inputSchema: {
2727
+ type: 'object',
2728
+ properties: {
2729
+ containerName: { type: 'string', description: 'Name of the BC container' },
2730
+ },
2731
+ required: ['containerName'],
2732
+ },
2733
+ },
2734
+ {
2735
+ name: 'bc_restart_container',
2736
+ description: 'Restart a BC container.',
2737
+ inputSchema: {
2738
+ type: 'object',
2739
+ properties: {
2740
+ containerName: { type: 'string', description: 'Name of the BC container' },
2741
+ },
2742
+ required: ['containerName'],
2743
+ },
2744
+ },
2745
+ {
2746
+ name: 'bc_download_symbols',
2747
+ description: 'Download symbol files (.app) from a BC container. Required for code completion and compilation.',
2748
+ inputSchema: {
2749
+ type: 'object',
2750
+ properties: {
2751
+ containerName: { type: 'string', description: 'Name of the BC container' },
2752
+ targetFolder: { type: 'string', description: 'Target folder for symbols (default: .alpackages)' },
2753
+ },
2754
+ required: ['containerName'],
2755
+ },
2756
+ },
2757
+ {
2758
+ name: 'bc_create_container',
2759
+ description: 'Create a new Business Central Docker container using BcContainerHelper. Takes ~10-30 minutes.',
2760
+ inputSchema: {
2761
+ type: 'object',
2762
+ properties: {
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' },
2783
+ },
2784
+ required: ['containerName'],
2785
+ },
2786
+ },
2787
+ {
2788
+ name: 'bc_remove_container',
2789
+ description: 'Remove a Business Central Docker container and clean up resources.',
2790
+ inputSchema: {
2791
+ type: 'object',
2792
+ properties: {
2793
+ containerName: { type: 'string', description: 'Name of the container to remove' },
2794
+ force: { type: 'boolean', description: 'Force remove even if running (default: false)' },
2795
+ },
2796
+ required: ['containerName'],
2797
+ },
2798
+ },
2799
+ {
2800
+ name: 'bc_get_extensions',
2801
+ description: 'List all installed extensions/apps in a BC container.',
2802
+ inputSchema: {
2803
+ type: 'object',
2804
+ properties: {
2805
+ containerName: { type: 'string', description: 'Name of the BC container' },
2806
+ },
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: [],
591
2973
  },
592
2974
  },
593
2975
  ];