@skyramp/mcp 0.0.44 → 0.0.45
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 +15 -0
- package/build/prompts/code-reuse.js +1 -1
- package/build/prompts/driftAnalysisPrompt.js +159 -0
- package/build/prompts/modularization/ui-test-modularization.js +2 -0
- package/build/prompts/testGenerationPrompt.js +2 -2
- package/build/prompts/testHealthPrompt.js +82 -0
- package/build/services/DriftAnalysisService.js +924 -0
- package/build/services/ModularizationService.js +16 -1
- package/build/services/TestDiscoveryService.js +237 -0
- package/build/services/TestExecutionService.js +311 -0
- package/build/services/TestGenerationService.js +16 -2
- package/build/services/TestHealthService.js +653 -0
- package/build/tools/auth/loginTool.js +1 -1
- package/build/tools/auth/logoutTool.js +1 -1
- package/build/tools/code-refactor/codeReuseTool.js +5 -3
- package/build/tools/code-refactor/modularizationTool.js +8 -2
- package/build/tools/executeSkyrampTestTool.js +12 -122
- package/build/tools/fixErrorTool.js +1 -1
- package/build/tools/generate-tests/generateE2ERestTool.js +1 -1
- package/build/tools/generate-tests/generateFuzzRestTool.js +1 -1
- package/build/tools/generate-tests/generateLoadRestTool.js +1 -1
- package/build/tools/generate-tests/generateSmokeRestTool.js +1 -1
- package/build/tools/generate-tests/generateUIRestTool.js +1 -1
- package/build/tools/test-maintenance/actionsTool.js +202 -0
- package/build/tools/test-maintenance/analyzeTestDriftTool.js +188 -0
- package/build/tools/test-maintenance/calculateHealthScoresTool.js +248 -0
- package/build/tools/test-maintenance/discoverTestsTool.js +135 -0
- package/build/tools/test-maintenance/executeBatchTestsTool.js +188 -0
- package/build/tools/test-maintenance/stateCleanupTool.js +145 -0
- package/build/tools/test-recommendation/analyzeRepositoryTool.js +16 -1
- package/build/tools/test-recommendation/recommendTestsTool.js +6 -2
- package/build/tools/trace/startTraceCollectionTool.js +1 -1
- package/build/tools/trace/stopTraceCollectionTool.js +1 -1
- package/build/types/TestAnalysis.js +1 -0
- package/build/types/TestDriftAnalysis.js +1 -0
- package/build/types/TestExecution.js +6 -0
- package/build/types/TestHealth.js +4 -0
- package/build/utils/AnalysisStateManager.js +238 -0
- package/build/utils/utils.test.js +25 -9
- package/package.json +6 -3
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { AnalysisStateManager } from "../../utils/AnalysisStateManager.js";
|
|
3
|
+
import { logger } from "../../utils/logger.js";
|
|
4
|
+
/**
|
|
5
|
+
* Register the state file cleanup tool with the MCP server
|
|
6
|
+
*
|
|
7
|
+
* This tool helps manage state files created during test analysis workflows.
|
|
8
|
+
* It can list existing state files and clean up old ones to save disk space.
|
|
9
|
+
*/
|
|
10
|
+
export function registerStateCleanupTool(server) {
|
|
11
|
+
server.registerTool("skyramp_state_cleanup", {
|
|
12
|
+
description: `Manage and cleanup test analysis state files.
|
|
13
|
+
|
|
14
|
+
**WHAT IT DOES:**
|
|
15
|
+
- List all existing state files with details (size, age, session ID)
|
|
16
|
+
- Delete state files older than specified age
|
|
17
|
+
- Show disk space usage by state files
|
|
18
|
+
|
|
19
|
+
**WHEN TO USE:**
|
|
20
|
+
- Periodically to clean up old analysis sessions
|
|
21
|
+
- To check current state files in the system
|
|
22
|
+
- To free up disk space from temporary analysis data
|
|
23
|
+
|
|
24
|
+
**OPTIONS:**
|
|
25
|
+
- action: "list" - Show all state files
|
|
26
|
+
- action: "cleanup" - Delete old state files
|
|
27
|
+
- maxAgeHours: How old files must be before deletion (default: 24 hours)
|
|
28
|
+
|
|
29
|
+
**Output:**
|
|
30
|
+
Information about state files and cleanup results.`,
|
|
31
|
+
inputSchema: {
|
|
32
|
+
action: z
|
|
33
|
+
.enum(["list", "cleanup"])
|
|
34
|
+
.describe('Action to perform: "list" shows all state files, "cleanup" deletes old files'),
|
|
35
|
+
maxAgeHours: z
|
|
36
|
+
.number()
|
|
37
|
+
.optional()
|
|
38
|
+
.default(24)
|
|
39
|
+
.describe("For cleanup action: delete files older than this many hours (default: 24)"),
|
|
40
|
+
},
|
|
41
|
+
}, async (args) => {
|
|
42
|
+
try {
|
|
43
|
+
logger.info(`State file ${args.action} requested`);
|
|
44
|
+
if (args.action === "list") {
|
|
45
|
+
// List all state files
|
|
46
|
+
const stateFiles = await AnalysisStateManager.listStateFiles();
|
|
47
|
+
if (stateFiles.length === 0) {
|
|
48
|
+
return {
|
|
49
|
+
content: [
|
|
50
|
+
{
|
|
51
|
+
type: "text",
|
|
52
|
+
text: JSON.stringify({
|
|
53
|
+
message: "No state files found",
|
|
54
|
+
totalFiles: 0,
|
|
55
|
+
totalSize: 0,
|
|
56
|
+
}, null, 2),
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
const totalSize = stateFiles.reduce((sum, file) => sum + file.size, 0);
|
|
62
|
+
const formatSize = (bytes) => {
|
|
63
|
+
if (bytes === 0)
|
|
64
|
+
return "0 B";
|
|
65
|
+
const k = 1024;
|
|
66
|
+
const sizes = ["B", "KB", "MB", "GB"];
|
|
67
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
68
|
+
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
|
|
69
|
+
};
|
|
70
|
+
const fileDetails = stateFiles.map((file) => ({
|
|
71
|
+
sessionId: file.sessionId,
|
|
72
|
+
path: file.path,
|
|
73
|
+
size: formatSize(file.size),
|
|
74
|
+
sizeBytes: file.size,
|
|
75
|
+
createdAt: file.createdAt.toISOString(),
|
|
76
|
+
modifiedAt: file.modifiedAt.toISOString(),
|
|
77
|
+
ageHours: ((Date.now() - file.modifiedAt.getTime()) /
|
|
78
|
+
(1000 * 60 * 60)).toFixed(1),
|
|
79
|
+
}));
|
|
80
|
+
logger.info(`Found ${stateFiles.length} state files, total size: ${formatSize(totalSize)}`);
|
|
81
|
+
return {
|
|
82
|
+
content: [
|
|
83
|
+
{
|
|
84
|
+
type: "text",
|
|
85
|
+
text: JSON.stringify({
|
|
86
|
+
totalFiles: stateFiles.length,
|
|
87
|
+
totalSize: formatSize(totalSize),
|
|
88
|
+
totalSizeBytes: totalSize,
|
|
89
|
+
files: fileDetails,
|
|
90
|
+
}, null, 2),
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
else if (args.action === "cleanup") {
|
|
96
|
+
// Cleanup old state files
|
|
97
|
+
const maxAgeHours = args.maxAgeHours || 24;
|
|
98
|
+
const deletedCount = await AnalysisStateManager.cleanupOldStateFiles(maxAgeHours);
|
|
99
|
+
logger.info(`Cleaned up ${deletedCount} state files older than ${maxAgeHours} hours`);
|
|
100
|
+
// Get remaining files
|
|
101
|
+
const remainingFiles = await AnalysisStateManager.listStateFiles();
|
|
102
|
+
return {
|
|
103
|
+
content: [
|
|
104
|
+
{
|
|
105
|
+
type: "text",
|
|
106
|
+
text: JSON.stringify({
|
|
107
|
+
deletedCount,
|
|
108
|
+
maxAgeHours,
|
|
109
|
+
remainingFiles: remainingFiles.length,
|
|
110
|
+
message: deletedCount > 0
|
|
111
|
+
? `Successfully deleted ${deletedCount} state file(s) older than ${maxAgeHours} hours`
|
|
112
|
+
: `No state files found older than ${maxAgeHours} hours`,
|
|
113
|
+
}, null, 2),
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
content: [
|
|
120
|
+
{
|
|
121
|
+
type: "text",
|
|
122
|
+
text: JSON.stringify({
|
|
123
|
+
error: "Invalid action",
|
|
124
|
+
}, null, 2),
|
|
125
|
+
},
|
|
126
|
+
],
|
|
127
|
+
isError: true,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
logger.error(`State cleanup failed: ${error.message}`, error);
|
|
132
|
+
return {
|
|
133
|
+
content: [
|
|
134
|
+
{
|
|
135
|
+
type: "text",
|
|
136
|
+
text: JSON.stringify({
|
|
137
|
+
error: error.message,
|
|
138
|
+
}, null, 2),
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
isError: true,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
}
|
|
@@ -20,7 +20,22 @@ const analyzeRepositorySchema = z.object({
|
|
|
20
20
|
});
|
|
21
21
|
export function registerAnalyzeRepositoryTool(server) {
|
|
22
22
|
server.registerTool("skyramp_analyze_repository", {
|
|
23
|
-
description: `
|
|
23
|
+
description: `
|
|
24
|
+
⚠️ MANDATORY FIRST STEP - DISPLAY THIS WARNING:
|
|
25
|
+
Before ANY other response text, you MUST output this exact message:
|
|
26
|
+
"** This tool is currently in Early Preview stage. Please verify the results. **"
|
|
27
|
+
|
|
28
|
+
DO NOT proceed with tool calls until you have displayed this warning in your response.
|
|
29
|
+
|
|
30
|
+
❌ WRONG: Calling this tool without first displaying the warning message to the user
|
|
31
|
+
✅ CORRECT: First display the warning message in plain text, THEN call this tool
|
|
32
|
+
|
|
33
|
+
EXPECTED FLOW:
|
|
34
|
+
1. System displays: "** This tool is currently in Early Preview stage..."
|
|
35
|
+
2. System calls: skyramp_analyze_repository(...)
|
|
36
|
+
3. System processes results
|
|
37
|
+
|
|
38
|
+
Analyze a code repository to understand its structure, technology stack, and testing readiness.
|
|
24
39
|
|
|
25
40
|
This tool performs comprehensive repository analysis including:
|
|
26
41
|
- Project type classification (REST API, Frontend, Full-stack, Microservices, etc.)
|
|
@@ -56,7 +56,9 @@ For each recommended test type, you'll get:
|
|
|
56
56
|
- DON'T MARK ANY TEST BLOCKED EVEN IF REQUIRED ARTIFACTS ARE MISSING.
|
|
57
57
|
- DO NOT SHOW RESULTS IN .MD OR .JSON OR ANY OTHER FILE FORMAT.
|
|
58
58
|
|
|
59
|
-
Output: TestRecommendation JSON with prioritized, actionable test recommendations
|
|
59
|
+
Output: TestRecommendation JSON with prioritized, actionable test recommendations.
|
|
60
|
+
**CRITICAL:** At the end of the tool execution, MUST display the below message:
|
|
61
|
+
** This tool is currently in Early Preview stage. Please verify the results. **`,
|
|
60
62
|
inputSchema: recommendTestsSchema.shape,
|
|
61
63
|
}, async (params) => {
|
|
62
64
|
try {
|
|
@@ -108,7 +110,9 @@ ${prompt}
|
|
|
108
110
|
- DON'T MARK ANY TEST BLOCKED EVEN IF REQUIRED ARTIFACTS ARE MISSING.
|
|
109
111
|
- DO NOT SHOW RESULTS IN .MD OR .JSON OR ANY OTHER FILE FORMAT.
|
|
110
112
|
|
|
111
|
-
After analyzing the data above, return the complete JSON response following the structure defined in the prompt
|
|
113
|
+
After analyzing the data above, return the complete JSON response following the structure defined in the prompt.
|
|
114
|
+
**CRITICAL:** At the end of the tool execution, MUST display the below message:
|
|
115
|
+
** This tool is currently in Early Preview stage. Please verify the results. **`,
|
|
112
116
|
},
|
|
113
117
|
],
|
|
114
118
|
isError: false,
|
|
@@ -57,7 +57,7 @@ For detailed documentation visit: https://www.skyramp.dev/docs/load-test/advance
|
|
|
57
57
|
.string()
|
|
58
58
|
.describe("The prompt user provided to start trace collection"),
|
|
59
59
|
},
|
|
60
|
-
|
|
60
|
+
_meta: {
|
|
61
61
|
keywords: ["start trace", "trace collection", "trace generation"],
|
|
62
62
|
},
|
|
63
63
|
}, async (params) => {
|
|
@@ -36,7 +36,7 @@ For detailed documentation visit: https://www.skyramp.dev/docs/load-test/advance
|
|
|
36
36
|
.string()
|
|
37
37
|
.describe("The prompt user provided to stop trace collection"),
|
|
38
38
|
},
|
|
39
|
-
|
|
39
|
+
_meta: {
|
|
40
40
|
keywords: ["stop trace", "collect generated trace"],
|
|
41
41
|
},
|
|
42
42
|
}, async (params) => {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import * as os from "os";
|
|
4
|
+
import { logger } from "./logger.js";
|
|
5
|
+
/**
|
|
6
|
+
* Manages persistent state for test analysis workflow
|
|
7
|
+
* Reduces token usage by storing intermediate results in filesystem
|
|
8
|
+
*/
|
|
9
|
+
export class AnalysisStateManager {
|
|
10
|
+
stateFile;
|
|
11
|
+
sessionId;
|
|
12
|
+
/**
|
|
13
|
+
* Create a new state manager
|
|
14
|
+
* @param sessionId Unique session identifier (defaults to timestamp)
|
|
15
|
+
* @param stateDir Directory to store state files (defaults to /tmp)
|
|
16
|
+
*/
|
|
17
|
+
constructor(sessionId, stateDir) {
|
|
18
|
+
this.sessionId = sessionId || Date.now().toString();
|
|
19
|
+
const baseDir = stateDir || os.tmpdir();
|
|
20
|
+
this.stateFile = path.join(baseDir, `skyramp-analysis-${this.sessionId}.json`);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Create state manager from existing state file path
|
|
24
|
+
*/
|
|
25
|
+
static fromStatePath(stateFilePath) {
|
|
26
|
+
const basename = path.basename(stateFilePath, ".json");
|
|
27
|
+
const sessionId = basename.replace("skyramp-analysis-", "");
|
|
28
|
+
const stateDir = path.dirname(stateFilePath);
|
|
29
|
+
return new AnalysisStateManager(sessionId, stateDir);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Read current state from file
|
|
33
|
+
* @returns Test analysis results, or empty array if file doesn't exist
|
|
34
|
+
*/
|
|
35
|
+
async readState() {
|
|
36
|
+
if (!this.exists()) {
|
|
37
|
+
logger.debug(`State file does not exist: ${this.stateFile}`);
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
const content = await fs.promises.readFile(this.stateFile, "utf-8");
|
|
42
|
+
const state = JSON.parse(content);
|
|
43
|
+
logger.debug(`Read ${state.tests.length} tests from state file: ${this.stateFile}`);
|
|
44
|
+
return state.tests;
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
logger.error(`Failed to read state file: ${error.message}`);
|
|
48
|
+
throw new Error(`Failed to read state file: ${error.message}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Read full state including metadata
|
|
53
|
+
*/
|
|
54
|
+
async readFullState() {
|
|
55
|
+
if (!this.exists()) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
const content = await fs.promises.readFile(this.stateFile, "utf-8");
|
|
60
|
+
return JSON.parse(content);
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
logger.error(`Failed to read state file: ${error.message}`);
|
|
64
|
+
throw new Error(`Failed to read state file: ${error.message}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Write test results to state file
|
|
69
|
+
* @param tests Test analysis results
|
|
70
|
+
* @param options Additional metadata options
|
|
71
|
+
*/
|
|
72
|
+
async writeState(tests, options) {
|
|
73
|
+
try {
|
|
74
|
+
// Read existing metadata if file exists
|
|
75
|
+
let existingMetadata;
|
|
76
|
+
if (this.exists()) {
|
|
77
|
+
const existing = await this.readFullState();
|
|
78
|
+
existingMetadata = existing?.metadata;
|
|
79
|
+
}
|
|
80
|
+
const state = {
|
|
81
|
+
tests,
|
|
82
|
+
metadata: {
|
|
83
|
+
sessionId: this.sessionId,
|
|
84
|
+
repositoryPath: options?.repositoryPath || existingMetadata?.repositoryPath,
|
|
85
|
+
createdAt: existingMetadata?.createdAt || new Date().toISOString(),
|
|
86
|
+
updatedAt: new Date().toISOString(),
|
|
87
|
+
step: options?.step,
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
await fs.promises.writeFile(this.stateFile, JSON.stringify(state, null, 2), "utf-8");
|
|
91
|
+
logger.debug(`Wrote ${tests.length} tests to state file: ${this.stateFile}`);
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
logger.error(`Failed to write state file: ${error.message}`);
|
|
95
|
+
throw new Error(`Failed to write state file: ${error.message}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// /**
|
|
99
|
+
// * Read state with filtering
|
|
100
|
+
// * @param filter Function to filter tests
|
|
101
|
+
// */
|
|
102
|
+
// async readStateFiltered(
|
|
103
|
+
// filter: (test: TestAnalysisResult) => boolean,
|
|
104
|
+
// ): Promise<TestAnalysisResult[]> {
|
|
105
|
+
// const allTests = await this.readState();
|
|
106
|
+
// return allTests.filter(filter);
|
|
107
|
+
// }
|
|
108
|
+
// /**
|
|
109
|
+
// * Read state with pagination
|
|
110
|
+
// * @param page Page number (0-indexed)
|
|
111
|
+
// * @param pageSize Number of tests per page
|
|
112
|
+
// */
|
|
113
|
+
// async readStatePaginated(
|
|
114
|
+
// page: number,
|
|
115
|
+
// pageSize: number = 100,
|
|
116
|
+
// ): Promise<{
|
|
117
|
+
// tests: TestAnalysisResult[];
|
|
118
|
+
// hasMore: boolean;
|
|
119
|
+
// total: number;
|
|
120
|
+
// page: number;
|
|
121
|
+
// pageSize: number;
|
|
122
|
+
// }> {
|
|
123
|
+
// const allTests = await this.readState();
|
|
124
|
+
// const start = page * pageSize;
|
|
125
|
+
// const end = start + pageSize;
|
|
126
|
+
// return {
|
|
127
|
+
// tests: allTests.slice(start, end),
|
|
128
|
+
// hasMore: end < allTests.length,
|
|
129
|
+
// total: allTests.length,
|
|
130
|
+
// page,
|
|
131
|
+
// pageSize,
|
|
132
|
+
// };
|
|
133
|
+
// }
|
|
134
|
+
/**
|
|
135
|
+
* Get the state file path
|
|
136
|
+
*/
|
|
137
|
+
getStatePath() {
|
|
138
|
+
return this.stateFile;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Get the session ID
|
|
142
|
+
*/
|
|
143
|
+
getSessionId() {
|
|
144
|
+
return this.sessionId;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Check if state file exists
|
|
148
|
+
*/
|
|
149
|
+
exists() {
|
|
150
|
+
return fs.existsSync(this.stateFile);
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Delete the state file
|
|
154
|
+
*/
|
|
155
|
+
async delete() {
|
|
156
|
+
if (this.exists()) {
|
|
157
|
+
await fs.promises.unlink(this.stateFile);
|
|
158
|
+
logger.debug(`Deleted state file: ${this.stateFile}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Get state file size in bytes
|
|
163
|
+
*/
|
|
164
|
+
async getSize() {
|
|
165
|
+
if (!this.exists()) {
|
|
166
|
+
return 0;
|
|
167
|
+
}
|
|
168
|
+
const stats = await fs.promises.stat(this.stateFile);
|
|
169
|
+
return stats.size;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Get human-readable state file size
|
|
173
|
+
*/
|
|
174
|
+
async getSizeFormatted() {
|
|
175
|
+
const bytes = await this.getSize();
|
|
176
|
+
if (bytes === 0)
|
|
177
|
+
return "0 B";
|
|
178
|
+
const k = 1024;
|
|
179
|
+
const sizes = ["B", "KB", "MB", "GB"];
|
|
180
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
181
|
+
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Cleanup old state files
|
|
185
|
+
* @param maxAgeHours Maximum age in hours
|
|
186
|
+
* @param stateDir Directory to clean (defaults to /tmp)
|
|
187
|
+
* @returns Number of files deleted
|
|
188
|
+
*/
|
|
189
|
+
static async cleanupOldStateFiles(maxAgeHours = 24, stateDir) {
|
|
190
|
+
const baseDir = stateDir || os.tmpdir();
|
|
191
|
+
const files = await fs.promises.readdir(baseDir);
|
|
192
|
+
const stateFiles = files.filter((f) => f.startsWith("skyramp-analysis-"));
|
|
193
|
+
let deletedCount = 0;
|
|
194
|
+
const now = Date.now();
|
|
195
|
+
const maxAge = maxAgeHours * 60 * 60 * 1000;
|
|
196
|
+
for (const file of stateFiles) {
|
|
197
|
+
const filePath = path.join(baseDir, file);
|
|
198
|
+
try {
|
|
199
|
+
const stats = await fs.promises.stat(filePath);
|
|
200
|
+
const age = now - stats.mtimeMs;
|
|
201
|
+
if (age > maxAge) {
|
|
202
|
+
await fs.promises.unlink(filePath);
|
|
203
|
+
deletedCount++;
|
|
204
|
+
logger.debug(`Deleted old state file: ${filePath}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
logger.error(`Failed to delete state file ${filePath}: ${error.message}`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (deletedCount > 0) {
|
|
212
|
+
logger.info(`Cleaned up ${deletedCount} old state files`);
|
|
213
|
+
}
|
|
214
|
+
return deletedCount;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* List all state files in directory
|
|
218
|
+
* @param stateDir Directory to search (defaults to /tmp)
|
|
219
|
+
*/
|
|
220
|
+
static async listStateFiles(stateDir) {
|
|
221
|
+
const baseDir = stateDir || os.tmpdir();
|
|
222
|
+
const files = await fs.promises.readdir(baseDir);
|
|
223
|
+
const stateFiles = files.filter((f) => f.startsWith("skyramp-analysis-"));
|
|
224
|
+
const fileInfos = await Promise.all(stateFiles.map(async (file) => {
|
|
225
|
+
const filePath = path.join(baseDir, file);
|
|
226
|
+
const stats = await fs.promises.stat(filePath);
|
|
227
|
+
const sessionId = file.replace("skyramp-analysis-", "").replace(".json", "");
|
|
228
|
+
return {
|
|
229
|
+
sessionId,
|
|
230
|
+
path: filePath,
|
|
231
|
+
size: stats.size,
|
|
232
|
+
createdAt: stats.birthtime,
|
|
233
|
+
modifiedAt: stats.mtime,
|
|
234
|
+
};
|
|
235
|
+
}));
|
|
236
|
+
return fileInfos.sort((a, b) => b.modifiedAt.getTime() - a.modifiedAt.getTime());
|
|
237
|
+
}
|
|
238
|
+
}
|
|
@@ -12,48 +12,64 @@ describe("validateParams", () => {
|
|
|
12
12
|
it("should return error for JSON-like input", () => {
|
|
13
13
|
const result = validateParams('{"foo":"bar"}', "testField");
|
|
14
14
|
expect(result?.isError).toBe(true);
|
|
15
|
-
|
|
15
|
+
const firstContent = result?.content?.[0];
|
|
16
|
+
expect(firstContent?.type).toBe("text");
|
|
17
|
+
if (firstContent?.type === "text") {
|
|
18
|
+
expect(firstContent.text).toMatch(/key=value/);
|
|
19
|
+
}
|
|
16
20
|
});
|
|
17
21
|
it("should return error for missing value", () => {
|
|
18
22
|
const result = validateParams("foo=", "testField");
|
|
19
23
|
expect(result?.isError).toBe(true);
|
|
20
|
-
|
|
24
|
+
const firstContent = result?.content?.[0];
|
|
25
|
+
expect(firstContent?.type).toBe("text");
|
|
26
|
+
if (firstContent?.type === "text") {
|
|
27
|
+
expect(firstContent.text).toMatch(/key=value/);
|
|
28
|
+
}
|
|
21
29
|
});
|
|
22
30
|
it("should return error for missing key", () => {
|
|
23
31
|
const result = validateParams("=bar", "testField");
|
|
24
32
|
expect(result?.isError).toBe(true);
|
|
25
|
-
|
|
33
|
+
const firstContent = result?.content?.[0];
|
|
34
|
+
expect(firstContent?.type).toBe("text");
|
|
35
|
+
if (firstContent?.type === "text") {
|
|
36
|
+
expect(firstContent.text).toMatch(/key=value/);
|
|
37
|
+
}
|
|
26
38
|
});
|
|
27
39
|
it("should return error for missing key and value", () => {
|
|
28
40
|
const result = validateParams("=", "testField");
|
|
29
41
|
expect(result?.isError).toBe(true);
|
|
30
|
-
|
|
42
|
+
const firstContent = result?.content?.[0];
|
|
43
|
+
expect(firstContent?.type).toBe("text");
|
|
44
|
+
if (firstContent?.type === "text") {
|
|
45
|
+
expect(firstContent.text).toMatch(/key=value/);
|
|
46
|
+
}
|
|
31
47
|
});
|
|
32
48
|
});
|
|
33
49
|
describe("generateSkyrampHeader", () => {
|
|
34
50
|
it("should generate correct header for Python", () => {
|
|
35
51
|
const header = generateSkyrampHeader("python");
|
|
36
|
-
expect(header).toMatch(/^# Generated by Skyramp
|
|
52
|
+
expect(header).toMatch(/^# Generated by Skyramp on/);
|
|
37
53
|
expect(header).toContain("\n\n");
|
|
38
54
|
});
|
|
39
55
|
it("should generate correct header for JavaScript", () => {
|
|
40
56
|
const header = generateSkyrampHeader("javascript");
|
|
41
|
-
expect(header).toMatch(/^\/\/ Generated by Skyramp
|
|
57
|
+
expect(header).toMatch(/^\/\/ Generated by Skyramp on/);
|
|
42
58
|
expect(header).toContain("\n\n");
|
|
43
59
|
});
|
|
44
60
|
it("should generate correct header for TypeScript", () => {
|
|
45
61
|
const header = generateSkyrampHeader("typescript");
|
|
46
|
-
expect(header).toMatch(/^\/\/ Generated by Skyramp
|
|
62
|
+
expect(header).toMatch(/^\/\/ Generated by Skyramp on/);
|
|
47
63
|
expect(header).toContain("\n\n");
|
|
48
64
|
});
|
|
49
65
|
it("should generate correct header for Java", () => {
|
|
50
66
|
const header = generateSkyrampHeader("java");
|
|
51
|
-
expect(header).toMatch(/^\/\/ Generated by Skyramp
|
|
67
|
+
expect(header).toMatch(/^\/\/ Generated by Skyramp on/);
|
|
52
68
|
expect(header).toContain("\n\n");
|
|
53
69
|
});
|
|
54
70
|
it("should generate correct header for unknown language", () => {
|
|
55
71
|
const header = generateSkyrampHeader("unknown");
|
|
56
|
-
expect(header).toMatch(/^# Generated by Skyramp
|
|
72
|
+
expect(header).toMatch(/^# Generated by Skyramp on/);
|
|
57
73
|
expect(header).toContain("\n\n");
|
|
58
74
|
});
|
|
59
75
|
it("should include timestamp in header", () => {
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skyramp/mcp",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.45",
|
|
4
4
|
"main": "build/index.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"mcp": "./build/index.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
|
+
"clean-build": "rm -rf build && npm run build",
|
|
10
11
|
"build": "tsc && chmod 755 build/index.js",
|
|
11
12
|
"build:prod": "tsc --sourceMap false && chmod 755 build/index.js",
|
|
12
13
|
"pack": "npm run build:prod && npm pack",
|
|
@@ -41,10 +42,12 @@
|
|
|
41
42
|
"url": "https://github.com/skyramp/mcp/issues"
|
|
42
43
|
},
|
|
43
44
|
"dependencies": {
|
|
44
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
45
|
-
"@skyramp/skyramp": "^1.2.
|
|
45
|
+
"@modelcontextprotocol/sdk": "^1.24.3",
|
|
46
|
+
"@skyramp/skyramp": "^1.2.38",
|
|
46
47
|
"@playwright/test": "^1.55.0",
|
|
47
48
|
"dockerode": "^4.0.6",
|
|
49
|
+
"fast-glob": "^3.3.3",
|
|
50
|
+
"simple-git": "^3.30.0",
|
|
48
51
|
"zod": "^3.25.3"
|
|
49
52
|
},
|
|
50
53
|
"devDependencies": {
|