@skyramp/mcp 0.0.62 → 0.0.63-rc.2
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/build/index.js +18 -26
- package/build/prompts/test-maintenance/drift-analysis-prompt.js +59 -0
- package/build/prompts/test-maintenance/driftAnalysisSections.js +153 -0
- package/build/prompts/test-recommendation/analysisOutputPrompt.js +21 -9
- package/build/prompts/test-recommendation/registerRecommendTestsPrompt.js +34 -38
- package/build/prompts/test-recommendation/test-recommendation-prompt.js +56 -9
- package/build/prompts/testbot/testbot-prompts.js +113 -100
- package/build/services/DriftAnalysisService.js +1 -1
- package/build/services/ScenarioGenerationService.js +5 -1
- package/build/services/TestExecutionService.js +2 -24
- package/build/services/TestExecutionService.test.js +167 -0
- package/build/services/containerEnv.js +35 -0
- package/build/tools/generate-tests/generateScenarioRestTool.js +7 -1
- package/build/tools/submitReportTool.js +6 -6
- package/build/tools/test-management/actionsTool.js +396 -0
- package/build/tools/test-management/analyzeChangesTool.js +750 -0
- package/build/tools/test-management/analyzeTestHealthTool.js +132 -0
- package/build/tools/test-management/executeTestsTool.js +198 -0
- package/build/tools/test-management/index.js +5 -0
- package/build/tools/test-management/stateCleanupTool.js +163 -0
- package/build/tools/test-recommendation/recommendTestsTool.js +1 -1
- package/build/utils/analyze-openapi.js +2 -2
- package/build/utils/pr-comment-parser.js +157 -36
- package/build/utils/pr-comment-parser.test.js +427 -0
- package/package.json +1 -1
- package/build/tools/initTestbotTool.js +0 -187
- package/build/tools/initTestbotTool.test.js +0 -194
- package/build/tools/test-recommendation/analyzeRepositoryTool.js +0 -505
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { logger } from "../../utils/logger.js";
|
|
3
|
+
import { StateManager, } from "../../utils/AnalysisStateManager.js";
|
|
4
|
+
import { TestType } from "../../types/TestTypes.js";
|
|
5
|
+
import * as fs from "fs";
|
|
6
|
+
import * as path from "path";
|
|
7
|
+
import { AnalyticsService } from "../../services/AnalyticsService.js";
|
|
8
|
+
/**
|
|
9
|
+
* Compute a suggested new filename when an endpoint is renamed.
|
|
10
|
+
*/
|
|
11
|
+
export function computeRenamedTestFile(testFile, renames) {
|
|
12
|
+
const basename = path.basename(testFile);
|
|
13
|
+
let newBasename = basename;
|
|
14
|
+
for (const rename of renames) {
|
|
15
|
+
const oldSegments = rename.oldPath.split("/").filter((s) => s.length > 0);
|
|
16
|
+
const newSegments = rename.newPath.split("/").filter((s) => s.length > 0);
|
|
17
|
+
if (oldSegments.length !== newSegments.length)
|
|
18
|
+
continue;
|
|
19
|
+
const paramPattern = /^\{[^}]+\}$/;
|
|
20
|
+
for (let i = 0; i < oldSegments.length; i++) {
|
|
21
|
+
if (paramPattern.test(oldSegments[i]))
|
|
22
|
+
continue;
|
|
23
|
+
if (oldSegments[i] !== newSegments[i]) {
|
|
24
|
+
const oldName = oldSegments[i].toLowerCase();
|
|
25
|
+
const newName = newSegments[i].toLowerCase();
|
|
26
|
+
if (newBasename.toLowerCase().includes(oldName)) {
|
|
27
|
+
const idx = newBasename.toLowerCase().indexOf(oldName);
|
|
28
|
+
newBasename =
|
|
29
|
+
newBasename.substring(0, idx) +
|
|
30
|
+
newName +
|
|
31
|
+
newBasename.substring(idx + oldName.length);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (newBasename === basename)
|
|
37
|
+
return null;
|
|
38
|
+
const newFilePath = path.join(path.dirname(testFile), newBasename);
|
|
39
|
+
if (fs.existsSync(newFilePath)) {
|
|
40
|
+
logger.info(`Skipping file rename suggestion: ${newFilePath} already exists`);
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
return newFilePath;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Select test types to generate based on HTTP method.
|
|
47
|
+
*/
|
|
48
|
+
function selectTestTypesForEndpoint(method) {
|
|
49
|
+
switch (method.toUpperCase()) {
|
|
50
|
+
case "POST":
|
|
51
|
+
case "PUT":
|
|
52
|
+
case "PATCH":
|
|
53
|
+
return [TestType.INTEGRATION, TestType.CONTRACT];
|
|
54
|
+
case "DELETE":
|
|
55
|
+
return [TestType.INTEGRATION, TestType.SMOKE];
|
|
56
|
+
case "GET":
|
|
57
|
+
default:
|
|
58
|
+
return [TestType.CONTRACT, TestType.SMOKE];
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const actionsSchema = {
|
|
62
|
+
stateFile: z
|
|
63
|
+
.string()
|
|
64
|
+
.describe("Path to state file from skyramp_analyze_test_health (or skyramp_execute_tests)"),
|
|
65
|
+
};
|
|
66
|
+
const TOOL_NAME = "skyramp_actions";
|
|
67
|
+
export function registerActionsTool(server) {
|
|
68
|
+
server.registerTool(TOOL_NAME, {
|
|
69
|
+
description: `Execute test maintenance and generation actions — final step of the unified Test Health Analysis Flow.
|
|
70
|
+
|
|
71
|
+
**PREREQUISITE:** Call \`skyramp_analyze_test_health\` (and optionally \`skyramp_execute_tests\`).
|
|
72
|
+
|
|
73
|
+
**CRITICAL:** This tool MUST be called automatically after the LLM completes the drift assessment. Do NOT wait for user confirmation.
|
|
74
|
+
|
|
75
|
+
**EXECUTING ACTIONS:**
|
|
76
|
+
- UPDATE: Apply changes to test files (path renames, field updates) using the write tool
|
|
77
|
+
- REGENERATE: Provide summary for human review / tool re-invocation
|
|
78
|
+
- VERIFY: Provide summary for human review
|
|
79
|
+
- ADD: Auto-generate tests for new endpoints via LLM instructions
|
|
80
|
+
|
|
81
|
+
**OUTPUT:**
|
|
82
|
+
Comprehensive report with executed actions, summary, and instructions for ADD recommendations
|
|
83
|
+
`,
|
|
84
|
+
inputSchema: actionsSchema,
|
|
85
|
+
}, async (args) => {
|
|
86
|
+
let errorResult;
|
|
87
|
+
try {
|
|
88
|
+
logger.info("Performing Actions (unified test-management)");
|
|
89
|
+
// Load UnifiedAnalysisState from state file
|
|
90
|
+
const stateManager = StateManager.fromStatePath(args.stateFile);
|
|
91
|
+
const stateData = await stateManager.readData();
|
|
92
|
+
const fullState = await stateManager.readFullState();
|
|
93
|
+
const repositoryPath = fullState?.metadata.repositoryPath || "";
|
|
94
|
+
if (!stateData) {
|
|
95
|
+
errorResult = {
|
|
96
|
+
content: [
|
|
97
|
+
{
|
|
98
|
+
type: "text",
|
|
99
|
+
text: JSON.stringify({
|
|
100
|
+
error: "State file is empty or invalid",
|
|
101
|
+
stateFile: args.stateFile,
|
|
102
|
+
}, null, 2),
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
isError: true,
|
|
106
|
+
};
|
|
107
|
+
return errorResult;
|
|
108
|
+
}
|
|
109
|
+
const testAnalysisResults = stateData.existingTests || [];
|
|
110
|
+
const newEndpoints = stateData.newEndpoints || [];
|
|
111
|
+
// ── Build recommendations from existing tests ──
|
|
112
|
+
const recommendations = [];
|
|
113
|
+
testAnalysisResults.forEach((test) => {
|
|
114
|
+
if (test.healthScore !== undefined && test.recommendation) {
|
|
115
|
+
recommendations.push({
|
|
116
|
+
testFile: test.testFile,
|
|
117
|
+
action: test.recommendation.action,
|
|
118
|
+
priority: test.recommendation.priority,
|
|
119
|
+
rationale: test.recommendation.rationale,
|
|
120
|
+
estimatedWork: test.recommendation.estimatedWork,
|
|
121
|
+
issues: test.issues || [],
|
|
122
|
+
renamedEndpoints: test.recommendation.details?.renamedEndpoints || [],
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
// ── Process UPDATE recommendations ──
|
|
127
|
+
const updateRecommendations = (recommendations || []).filter((rec) => rec.action === "UPDATE");
|
|
128
|
+
const updateInstructions = [];
|
|
129
|
+
const testFilesToUpdate = [];
|
|
130
|
+
for (const rec of updateRecommendations) {
|
|
131
|
+
if (!rec.testFile) {
|
|
132
|
+
logger.warning("Recommendation missing testFile", rec);
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
testFilesToUpdate.push(rec.testFile);
|
|
136
|
+
const testData = testAnalysisResults.find((t) => t.testFile === rec.testFile);
|
|
137
|
+
const driftData = testData?.drift;
|
|
138
|
+
const issues = rec.issues || [];
|
|
139
|
+
const driftChanges = driftData?.changes || [];
|
|
140
|
+
let testFileContent = "";
|
|
141
|
+
try {
|
|
142
|
+
testFileContent = fs.readFileSync(rec.testFile, "utf-8");
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
logger.error(`Failed to read test file ${rec.testFile}: ${error.message}`);
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
const renames = rec.renamedEndpoints || [];
|
|
149
|
+
const isRenameUpdate = renames.length > 0;
|
|
150
|
+
let instruction = `\n### ${rec.testFile}\n\n`;
|
|
151
|
+
instruction += `**Priority:** ${rec.priority} | `;
|
|
152
|
+
instruction += `**Estimated Effort:** ${rec.estimatedWork || "Small"}\n\n`;
|
|
153
|
+
instruction += `**Why Update Needed:** ${rec.rationale}\n\n`;
|
|
154
|
+
if (isRenameUpdate) {
|
|
155
|
+
instruction += `**Endpoint Rename Detected — Path Substitution Required:**\n\n`;
|
|
156
|
+
instruction += `| Old Path | New Path | Method |\n`;
|
|
157
|
+
instruction += `|----------|----------|--------|\n`;
|
|
158
|
+
for (const rename of renames) {
|
|
159
|
+
instruction += `| \`${rename.oldPath}\` | \`${rename.newPath}\` | ${rename.method} |\n`;
|
|
160
|
+
}
|
|
161
|
+
instruction += `\n`;
|
|
162
|
+
instruction += `**Action:** Find-and-replace all occurrences of the old path with the new path in this test file. `;
|
|
163
|
+
instruction += `Do NOT change any test logic, assertions, or structure — only update the URL paths.\n\n`;
|
|
164
|
+
const suggestedNewFile = computeRenamedTestFile(rec.testFile, renames);
|
|
165
|
+
if (suggestedNewFile) {
|
|
166
|
+
instruction += `**File Rename:** After updating the paths, rename this file:\n`;
|
|
167
|
+
instruction += `- From: \`${path.basename(rec.testFile)}\`\n`;
|
|
168
|
+
instruction += `- To: \`${path.basename(suggestedNewFile)}\`\n\n`;
|
|
169
|
+
rec._suggestedNewFile = suggestedNewFile;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (driftData) {
|
|
173
|
+
instruction += `**Analysis:**\n`;
|
|
174
|
+
instruction += `- Drift Score: ${driftData.driftScore ?? "N/A"}\n`;
|
|
175
|
+
instruction += `- Changes Detected: ${driftData.changes?.length || 0}\n`;
|
|
176
|
+
instruction += `- Affected Files: ${driftData.affectedFiles.files || 0}\n\n`;
|
|
177
|
+
}
|
|
178
|
+
if (driftChanges.length > 0) {
|
|
179
|
+
instruction += `**Changes Detected:**\n`;
|
|
180
|
+
driftChanges.forEach((change) => {
|
|
181
|
+
instruction += `**${change.type}** (Severity: ${change.severity}): ${change.description}\n`;
|
|
182
|
+
if (change.details) {
|
|
183
|
+
instruction += ` └─ ${change.details}\n`;
|
|
184
|
+
}
|
|
185
|
+
if (change.file) {
|
|
186
|
+
instruction += ` └─ In: \`${change.file}\`\n`;
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
instruction += `\n`;
|
|
190
|
+
}
|
|
191
|
+
if (issues.length > 0) {
|
|
192
|
+
instruction += `**Issues Found:**\n`;
|
|
193
|
+
issues.forEach((issue) => {
|
|
194
|
+
instruction += `**${issue.type}** (Severity: ${issue.severity}): ${issue.description}\n`;
|
|
195
|
+
if (issue.details) {
|
|
196
|
+
instruction += ` └─ ${issue.details}\n`;
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
instruction += `\n`;
|
|
200
|
+
}
|
|
201
|
+
instruction += `**Test File Content:**\n\`\`\`\n${testFileContent}\n\`\`\`\n\n`;
|
|
202
|
+
updateInstructions.push(instruction);
|
|
203
|
+
}
|
|
204
|
+
// ── Build ADD section for new endpoints ──
|
|
205
|
+
const wsBaseUrl = stateData.repositoryAnalysis?.wsBaseUrl || "";
|
|
206
|
+
const wsSchemaPath = stateData.repositoryAnalysis?.wsSchemaPath || "";
|
|
207
|
+
const primaryLanguage = stateData.repositoryAnalysis?.projectMeta?.primaryLanguage ||
|
|
208
|
+
"python";
|
|
209
|
+
const primaryFramework = stateData.repositoryAnalysis?.projectMeta?.primaryFramework ||
|
|
210
|
+
"pytest";
|
|
211
|
+
// Determine output directory from workspace config or repo path
|
|
212
|
+
const outputDir = repositoryPath
|
|
213
|
+
? path.join(repositoryPath, "tests", "skyramp")
|
|
214
|
+
: "./tests/skyramp";
|
|
215
|
+
const addSummaryLines = [];
|
|
216
|
+
const llmToolCalls = [];
|
|
217
|
+
for (const ep of newEndpoints) {
|
|
218
|
+
const testTypes = selectTestTypesForEndpoint(ep.method);
|
|
219
|
+
const endpointURL = wsBaseUrl
|
|
220
|
+
? wsBaseUrl.replace(/\/$/, "") + ep.path
|
|
221
|
+
: ep.path;
|
|
222
|
+
addSummaryLines.push(`- ${ep.method} ${ep.path} → ${testTypes.join(", ")} tests`);
|
|
223
|
+
for (const testType of testTypes) {
|
|
224
|
+
let toolName = "";
|
|
225
|
+
switch (testType) {
|
|
226
|
+
case TestType.CONTRACT:
|
|
227
|
+
toolName = "skyramp_contract_test_generation";
|
|
228
|
+
break;
|
|
229
|
+
case TestType.INTEGRATION:
|
|
230
|
+
toolName = "skyramp_integration_test_generation";
|
|
231
|
+
break;
|
|
232
|
+
case TestType.SMOKE:
|
|
233
|
+
toolName = "skyramp_smoke_test_generation";
|
|
234
|
+
break;
|
|
235
|
+
default:
|
|
236
|
+
toolName = "skyramp_contract_test_generation";
|
|
237
|
+
}
|
|
238
|
+
llmToolCalls.push({
|
|
239
|
+
tool: toolName,
|
|
240
|
+
params: {
|
|
241
|
+
endpointURL,
|
|
242
|
+
method: ep.method,
|
|
243
|
+
language: primaryLanguage,
|
|
244
|
+
framework: primaryFramework,
|
|
245
|
+
outputDir,
|
|
246
|
+
...(wsSchemaPath ? { apiSchema: wsSchemaPath } : {}),
|
|
247
|
+
},
|
|
248
|
+
endpoint: `${ep.method} ${ep.path}`,
|
|
249
|
+
testType: testType,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
// ── Build response text ──
|
|
254
|
+
let responseText = `# Test Actions Report\n\n`;
|
|
255
|
+
if (updateRecommendations.length > 0) {
|
|
256
|
+
responseText += `## Tests Requiring Updates (${updateRecommendations.length})\n\n`;
|
|
257
|
+
testFilesToUpdate.forEach((file, idx) => {
|
|
258
|
+
responseText += `${idx + 1}. \`${file}\`\n`;
|
|
259
|
+
});
|
|
260
|
+
responseText += `\n---\n`;
|
|
261
|
+
responseText += updateInstructions.join("\n---\n");
|
|
262
|
+
}
|
|
263
|
+
if (newEndpoints.length > 0) {
|
|
264
|
+
responseText += `\n## New Endpoint Tests to Generate (${newEndpoints.length} endpoints)\n\n`;
|
|
265
|
+
addSummaryLines.forEach((line) => {
|
|
266
|
+
responseText += `${line}\n`;
|
|
267
|
+
});
|
|
268
|
+
responseText += `\nThe following tests will be generated automatically.\n`;
|
|
269
|
+
}
|
|
270
|
+
if (updateRecommendations.length === 0 && newEndpoints.length === 0) {
|
|
271
|
+
const otherRecs = recommendations.filter((rec) => rec.action !== "UPDATE");
|
|
272
|
+
if (otherRecs.length > 0) {
|
|
273
|
+
responseText += `## Recommendations (${otherRecs.length})\n\n`;
|
|
274
|
+
otherRecs.forEach((rec) => {
|
|
275
|
+
responseText += `- **${rec.testFile}** — Action: ${rec.action}, Priority: ${rec.priority}\n`;
|
|
276
|
+
responseText += ` ${rec.rationale}\n`;
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
responseText += `No action required. All existing tests appear healthy.\n`;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
responseText += `\n\n## Next Steps\n\n`;
|
|
284
|
+
responseText += `The AI assistant will:\n`;
|
|
285
|
+
let stepNumber = 1;
|
|
286
|
+
if (updateRecommendations.length > 0) {
|
|
287
|
+
responseText += `${stepNumber++}. Review the changes and issues for each test\n`;
|
|
288
|
+
responseText += `${stepNumber++}. Update test files to fix compatibility issues\n`;
|
|
289
|
+
responseText += `${stepNumber++}. Preserve original test logic and structure\n`;
|
|
290
|
+
responseText += `${stepNumber++}. Show you the changes made\n`;
|
|
291
|
+
}
|
|
292
|
+
if (newEndpoints.length > 0) {
|
|
293
|
+
responseText += `${stepNumber++}. Generate new tests for new endpoints\n`;
|
|
294
|
+
}
|
|
295
|
+
responseText += `\n**This tool is currently in Early Preview stage. Please verify the results.**\n`;
|
|
296
|
+
// ── Build LLM instructions for UPDATE ──
|
|
297
|
+
const allRenames = [];
|
|
298
|
+
for (const rec of updateRecommendations) {
|
|
299
|
+
if (rec.renamedEndpoints && rec.renamedEndpoints.length > 0) {
|
|
300
|
+
allRenames.push(...rec.renamedEndpoints);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
const uniqueRenames = allRenames.filter((r, i, arr) => arr.findIndex((x) => x.oldPath === r.oldPath &&
|
|
304
|
+
x.newPath === r.newPath &&
|
|
305
|
+
x.method === r.method) === i);
|
|
306
|
+
const llmInstructionsObj = {
|
|
307
|
+
workflow: "test_maintenance",
|
|
308
|
+
action: "execute_updates",
|
|
309
|
+
auto_proceed: true,
|
|
310
|
+
files_to_update: testFilesToUpdate,
|
|
311
|
+
update_count: updateRecommendations.length,
|
|
312
|
+
};
|
|
313
|
+
if (uniqueRenames.length > 0) {
|
|
314
|
+
llmInstructionsObj.endpoint_renames = uniqueRenames;
|
|
315
|
+
llmInstructionsObj.rename_strategy =
|
|
316
|
+
"For each file, find-and-replace all occurrences of oldPath with newPath. Do NOT regenerate or restructure the test — only update the URL paths.";
|
|
317
|
+
const fileRenames = [];
|
|
318
|
+
for (const rec of updateRecommendations) {
|
|
319
|
+
if (rec._suggestedNewFile) {
|
|
320
|
+
fileRenames.push({ from: rec.testFile, to: rec._suggestedNewFile });
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
if (fileRenames.length > 0) {
|
|
324
|
+
llmInstructionsObj.file_renames = fileRenames;
|
|
325
|
+
llmInstructionsObj.file_rename_strategy =
|
|
326
|
+
"After updating path content in each file, rename the file using 'mv' or equivalent. Use git mv if the repo tracks the file.";
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
const llmInstructions = `<!-- LLM_INSTRUCTIONS:\n${JSON.stringify(llmInstructionsObj, null, 2)}\n-->\n`;
|
|
330
|
+
const contentBlocks = [
|
|
331
|
+
{
|
|
332
|
+
type: "text",
|
|
333
|
+
text: responseText,
|
|
334
|
+
annotations: {
|
|
335
|
+
audience: ["user"],
|
|
336
|
+
},
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
type: "text",
|
|
340
|
+
text: llmInstructions,
|
|
341
|
+
annotations: {
|
|
342
|
+
audience: ["assistant"],
|
|
343
|
+
},
|
|
344
|
+
},
|
|
345
|
+
];
|
|
346
|
+
// ── Build ADD instructions for new endpoints ──
|
|
347
|
+
if (newEndpoints.length > 0 && llmToolCalls.length > 0) {
|
|
348
|
+
const addInstructionsObj = {
|
|
349
|
+
workflow: "add_tests_for_new_endpoints",
|
|
350
|
+
auto_proceed: true,
|
|
351
|
+
instruction: "Call each tool in tool_calls immediately and in order. Do NOT ask for confirmation. If endpointURL is a bare path, read .skyramp/workspace.yml for baseUrl and prepend it.",
|
|
352
|
+
tool_calls: llmToolCalls,
|
|
353
|
+
};
|
|
354
|
+
const addInstructions = `<!-- LLM_INSTRUCTIONS:\n${JSON.stringify(addInstructionsObj, null, 2)}\n-->\n`;
|
|
355
|
+
contentBlocks.push({
|
|
356
|
+
type: "text",
|
|
357
|
+
text: addInstructions,
|
|
358
|
+
annotations: {
|
|
359
|
+
audience: ["assistant"],
|
|
360
|
+
},
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
return {
|
|
364
|
+
content: contentBlocks,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
catch (error) {
|
|
368
|
+
logger.error(`Actions tool failed: ${error.message}`, error);
|
|
369
|
+
errorResult = {
|
|
370
|
+
content: [
|
|
371
|
+
{
|
|
372
|
+
type: "text",
|
|
373
|
+
text: JSON.stringify({
|
|
374
|
+
error: error.message,
|
|
375
|
+
}, null, 2),
|
|
376
|
+
},
|
|
377
|
+
],
|
|
378
|
+
isError: true,
|
|
379
|
+
};
|
|
380
|
+
return errorResult;
|
|
381
|
+
}
|
|
382
|
+
finally {
|
|
383
|
+
let repositoryPath = "";
|
|
384
|
+
try {
|
|
385
|
+
const fullState = await StateManager.fromStatePath(args.stateFile).readFullState();
|
|
386
|
+
repositoryPath = fullState?.metadata.repositoryPath || "";
|
|
387
|
+
}
|
|
388
|
+
catch (analyticsError) {
|
|
389
|
+
logger.error(`Failed to read state for analytics in actions tool: ${analyticsError?.message ?? analyticsError}`, analyticsError);
|
|
390
|
+
}
|
|
391
|
+
AnalyticsService.pushMCPToolEvent(TOOL_NAME, errorResult, {
|
|
392
|
+
repositoryPath,
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
}
|