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