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