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