kickload-watcher-mcp 0.1.0 → 0.1.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/README.md +164 -233
- package/compliance-poller.js +216 -215
- package/config.js +113 -103
- package/email-sender.js +137 -137
- package/github-webhook.js +322 -321
- package/index.js +266 -270
- package/kickload-client.js +260 -254
- package/package.json +51 -51
- package/pipeline.js +448 -448
- package/setup.js +193 -201
package/kickload-client.js
CHANGED
|
@@ -1,254 +1,260 @@
|
|
|
1
|
-
// kickload-client.js
|
|
2
|
-
// Single source of truth for ALL Kickload API calls.
|
|
3
|
-
// Every endpoint verified against app.py source code.
|
|
4
|
-
//
|
|
5
|
-
// ⚠️ NEW BASE URL: https://kickload.neeyatai.com/api
|
|
6
|
-
// All routes are relative to this base, e.g.:
|
|
7
|
-
// POST https://kickload.neeyatai.com/api/generate-test-plan
|
|
8
|
-
// POST https://kickload.neeyatai.com/api/run-test/{filename}
|
|
9
|
-
// GET https://kickload.neeyatai.com/api/task-status/{id}
|
|
10
|
-
// POST https://kickload.neeyatai.com/api/analyzeJTL
|
|
11
|
-
// GET https://kickload.neeyatai.com/api/download/{filename}
|
|
12
|
-
|
|
13
|
-
import FormData from "form-data";
|
|
14
|
-
import fetch from "node-fetch";
|
|
15
|
-
import fs from "fs";
|
|
16
|
-
import path from "path";
|
|
17
|
-
|
|
18
|
-
const POLL_INTERVAL_MS = 5000; // 5s between polls — matches backend
|
|
19
|
-
const POLL_TIMEOUT_MS
|
|
20
|
-
|
|
21
|
-
export class KickloadClient {
|
|
22
|
-
/**
|
|
23
|
-
* @param {string} baseUrl — e.g. "https://kickload.neeyatai.com/api"
|
|
24
|
-
* @param {string} apiToken — X-API-Token value
|
|
25
|
-
*/
|
|
26
|
-
constructor(baseUrl, apiToken) {
|
|
27
|
-
// Normalize: strip trailing slash
|
|
28
|
-
this.baseUrl = baseUrl.replace(/\/$/, "");
|
|
29
|
-
this.token = apiToken;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// ─── Private authenticated fetch ─────────────────────────────
|
|
33
|
-
async _fetch(method, endpoint, { body, headers = {}, isJson = false } = {}) {
|
|
34
|
-
const url = `${this.baseUrl}${endpoint}`;
|
|
35
|
-
|
|
36
|
-
const reqHeaders = {
|
|
37
|
-
"X-API-Token": this.token,
|
|
38
|
-
...headers,
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
if (isJson) {
|
|
42
|
-
reqHeaders["Content-Type"] = "application/json";
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const response = await fetch(url, {
|
|
46
|
-
method,
|
|
47
|
-
headers: reqHeaders,
|
|
48
|
-
body,
|
|
49
|
-
signal: AbortSignal.timeout(POLL_TIMEOUT_MS),
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
const text = await response.text();
|
|
53
|
-
|
|
54
|
-
let parsed;
|
|
55
|
-
try {
|
|
56
|
-
parsed = JSON.parse(text);
|
|
57
|
-
} catch {
|
|
58
|
-
parsed = { _raw: text };
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (!response.ok) {
|
|
62
|
-
const err = new Error(
|
|
63
|
-
`Kickload ${method} ${endpoint} → ${response.status} ${response.statusText}\n` +
|
|
64
|
-
`URL: ${url}\n` +
|
|
65
|
-
`Response: ${text.substring(0, 500)}`
|
|
66
|
-
);
|
|
67
|
-
err.status = response.status;
|
|
68
|
-
err.body = text;
|
|
69
|
-
err.parsed = parsed;
|
|
70
|
-
throw err;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return parsed;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// ══════════════════════════════════════════════════════════════
|
|
77
|
-
// STEP 1 — Generate Test Plan
|
|
78
|
-
// POST /generate-test-plan
|
|
79
|
-
// Source: app.py line 1260
|
|
80
|
-
//
|
|
81
|
-
// Send prompt OR JMX file OR both.
|
|
82
|
-
// Returns: { status: "success", jmx_filename: "test_plan_xxx.jmx" }
|
|
83
|
-
// ══════════════════════════════════════════════════════════════
|
|
84
|
-
async generateTestPlan({ prompt, jmxFilePath } = {}) {
|
|
85
|
-
if (!prompt && !jmxFilePath) {
|
|
86
|
-
throw new Error("generateTestPlan: provide at least a prompt or jmxFilePath");
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const form = new FormData();
|
|
90
|
-
|
|
91
|
-
if (prompt) {
|
|
92
|
-
form.append("prompt", prompt);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if (jmxFilePath) {
|
|
96
|
-
const resolved = path.resolve(jmxFilePath);
|
|
97
|
-
if (!fs.existsSync(resolved)) {
|
|
98
|
-
throw new Error(`JMX file not found: ${resolved}`);
|
|
99
|
-
}
|
|
100
|
-
form.append("file", fs.createReadStream(resolved), {
|
|
101
|
-
filename: path.basename(resolved),
|
|
102
|
-
contentType: "application/xml",
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
console.log("\n📋 STEP 1: Generating test plan...");
|
|
107
|
-
if (prompt) console.log(` Prompt : ${prompt.substring(0, 100)}`);
|
|
108
|
-
if (jmxFilePath) console.log(` JMX file: ${jmxFilePath}`);
|
|
109
|
-
|
|
110
|
-
const result = await this._fetch("POST", "/generate-test-plan", {
|
|
111
|
-
body: form,
|
|
112
|
-
headers: form.getHeaders(),
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
if (result.status !== "success") {
|
|
116
|
-
throw new Error(
|
|
117
|
-
`Test plan generation failed: ${result.message || JSON.stringify(result)}`
|
|
118
|
-
);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
console.log(`✅ STEP 1 done — JMX: ${result.jmx_filename}`);
|
|
122
|
-
return result;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// ══════════════════════════════════════════════════════════════
|
|
126
|
-
// STEP 2 — Run Test
|
|
127
|
-
// POST /run-test/{jmx_filename}
|
|
128
|
-
// Source: app.py line 1022
|
|
129
|
-
//
|
|
130
|
-
// Returns: { status: "started", task_id: "uuid-xxxx" }
|
|
131
|
-
// ══════════════════════════════════════════════════════════════
|
|
132
|
-
async runTest(jmxFilename, { numThreads, loopCount, rampTime } = {}) {
|
|
133
|
-
if (!jmxFilename) {
|
|
134
|
-
throw new Error("runTest: jmxFilename is required");
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const body = {};
|
|
138
|
-
if (numThreads !== undefined) body.num_threads = numThreads;
|
|
139
|
-
if (loopCount !== undefined) body.loop_count = loopCount;
|
|
140
|
-
if (rampTime !== undefined) body.ramp_time = rampTime;
|
|
141
|
-
|
|
142
|
-
console.log(`\n▶️ STEP 2: Running test — ${jmxFilename}`);
|
|
143
|
-
console.log(` Overrides: ${JSON.stringify(body) || "none (using JMX defaults)"}`);
|
|
144
|
-
|
|
145
|
-
const result = await this._fetch(
|
|
146
|
-
"POST",
|
|
147
|
-
`/run-test/${encodeURIComponent(jmxFilename)}`,
|
|
148
|
-
{ body: JSON.stringify(body), isJson: true }
|
|
149
|
-
);
|
|
150
|
-
|
|
151
|
-
if (result.status !== "started") {
|
|
152
|
-
throw new Error(`Run test failed: ${result.message || JSON.stringify(result)}`);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
console.log(`✅ STEP 2 done — Task ID: ${result.task_id}`);
|
|
156
|
-
return result;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// ══════════════════════════════════════════════════════════════
|
|
160
|
-
// STEP 3 — Poll Task Status
|
|
161
|
-
// GET /task-status/{task_id}
|
|
162
|
-
// Source: app.py line 899
|
|
163
|
-
//
|
|
164
|
-
// Polls every 5s until status = "success" | "error"
|
|
165
|
-
// Returns: { status, result_file, pdf_file, summary_output }
|
|
166
|
-
// ══════════════════════════════════════════════════════════════
|
|
167
|
-
async pollUntilDone(taskId) {
|
|
168
|
-
if (!taskId) throw new Error("pollUntilDone: taskId is required");
|
|
169
|
-
|
|
170
|
-
console.log(`\n⏳ STEP 3: Polling task ${taskId}`);
|
|
171
|
-
const started = Date.now();
|
|
172
|
-
let polls = 0;
|
|
173
|
-
|
|
174
|
-
while (Date.now() - started < POLL_TIMEOUT_MS) {
|
|
175
|
-
await sleep(POLL_INTERVAL_MS);
|
|
176
|
-
polls++;
|
|
177
|
-
|
|
178
|
-
const result = await this._fetch("GET", `/task-status/${taskId}`);
|
|
179
|
-
const elapsed = Math.round((Date.now() - started) / 1000);
|
|
180
|
-
|
|
181
|
-
if (result.status === "running") {
|
|
182
|
-
process.stdout.write(`\r ⏳ Running... ${elapsed}s (poll #${polls})`);
|
|
183
|
-
continue;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
if (result.status === "success") {
|
|
187
|
-
console.log(`\n✅ STEP 3 done in ${elapsed}s`);
|
|
188
|
-
console.log(` JTL: ${result.result_file}`);
|
|
189
|
-
console.log(` PDF: ${result.pdf_file}`);
|
|
190
|
-
return result;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
if (result.status === "error") {
|
|
194
|
-
throw new Error(`Task ${taskId} failed: ${result.message}`);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
throw new Error(`Task ${taskId} unknown status: ${result.status}`);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
throw new Error(
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
// ══════════════════════════════════════════════════════════════
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
// ══════════════════════════════════════════════════════════════
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
}
|
|
1
|
+
// kickload-client.js
|
|
2
|
+
// Single source of truth for ALL Kickload API calls.
|
|
3
|
+
// Every endpoint verified against app.py source code.
|
|
4
|
+
//
|
|
5
|
+
// ⚠️ NEW BASE URL: https://kickload.neeyatai.com/api
|
|
6
|
+
// All routes are relative to this base, e.g.:
|
|
7
|
+
// POST https://kickload.neeyatai.com/api/generate-test-plan
|
|
8
|
+
// POST https://kickload.neeyatai.com/api/run-test/{filename}
|
|
9
|
+
// GET https://kickload.neeyatai.com/api/task-status/{id}
|
|
10
|
+
// POST https://kickload.neeyatai.com/api/analyzeJTL
|
|
11
|
+
// GET https://kickload.neeyatai.com/api/download/{filename}
|
|
12
|
+
|
|
13
|
+
import FormData from "form-data";
|
|
14
|
+
import fetch from "node-fetch";
|
|
15
|
+
import fs from "fs";
|
|
16
|
+
import path from "path";
|
|
17
|
+
|
|
18
|
+
const POLL_INTERVAL_MS = 5000; // 5s between polls — matches backend
|
|
19
|
+
const POLL_TIMEOUT_MS = 1800000; // 30 min
|
|
20
|
+
|
|
21
|
+
export class KickloadClient {
|
|
22
|
+
/**
|
|
23
|
+
* @param {string} baseUrl — e.g. "https://kickload.neeyatai.com/api"
|
|
24
|
+
* @param {string} apiToken — X-API-Token value
|
|
25
|
+
*/
|
|
26
|
+
constructor(baseUrl, apiToken) {
|
|
27
|
+
// Normalize: strip trailing slash
|
|
28
|
+
this.baseUrl = baseUrl.replace(/\/$/, "");
|
|
29
|
+
this.token = apiToken;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ─── Private authenticated fetch ─────────────────────────────
|
|
33
|
+
async _fetch(method, endpoint, { body, headers = {}, isJson = false } = {}) {
|
|
34
|
+
const url = `${this.baseUrl}${endpoint}`;
|
|
35
|
+
|
|
36
|
+
const reqHeaders = {
|
|
37
|
+
"X-API-Token": this.token,
|
|
38
|
+
...headers,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
if (isJson) {
|
|
42
|
+
reqHeaders["Content-Type"] = "application/json";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const response = await fetch(url, {
|
|
46
|
+
method,
|
|
47
|
+
headers: reqHeaders,
|
|
48
|
+
body,
|
|
49
|
+
signal: AbortSignal.timeout(POLL_TIMEOUT_MS),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const text = await response.text();
|
|
53
|
+
|
|
54
|
+
let parsed;
|
|
55
|
+
try {
|
|
56
|
+
parsed = JSON.parse(text);
|
|
57
|
+
} catch {
|
|
58
|
+
parsed = { _raw: text };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!response.ok) {
|
|
62
|
+
const err = new Error(
|
|
63
|
+
`Kickload ${method} ${endpoint} → ${response.status} ${response.statusText}\n` +
|
|
64
|
+
`URL: ${url}\n` +
|
|
65
|
+
`Response: ${text.substring(0, 500)}`
|
|
66
|
+
);
|
|
67
|
+
err.status = response.status;
|
|
68
|
+
err.body = text;
|
|
69
|
+
err.parsed = parsed;
|
|
70
|
+
throw err;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return parsed;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ══════════════════════════════════════════════════════════════
|
|
77
|
+
// STEP 1 — Generate Test Plan
|
|
78
|
+
// POST /generate-test-plan
|
|
79
|
+
// Source: app.py line 1260
|
|
80
|
+
//
|
|
81
|
+
// Send prompt OR JMX file OR both.
|
|
82
|
+
// Returns: { status: "success", jmx_filename: "test_plan_xxx.jmx" }
|
|
83
|
+
// ══════════════════════════════════════════════════════════════
|
|
84
|
+
async generateTestPlan({ prompt, jmxFilePath } = {}) {
|
|
85
|
+
if (!prompt && !jmxFilePath) {
|
|
86
|
+
throw new Error("generateTestPlan: provide at least a prompt or jmxFilePath");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const form = new FormData();
|
|
90
|
+
|
|
91
|
+
if (prompt) {
|
|
92
|
+
form.append("prompt", prompt);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (jmxFilePath) {
|
|
96
|
+
const resolved = path.resolve(jmxFilePath);
|
|
97
|
+
if (!fs.existsSync(resolved)) {
|
|
98
|
+
throw new Error(`JMX file not found: ${resolved}`);
|
|
99
|
+
}
|
|
100
|
+
form.append("file", fs.createReadStream(resolved), {
|
|
101
|
+
filename: path.basename(resolved),
|
|
102
|
+
contentType: "application/xml",
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
console.log("\n📋 STEP 1: Generating test plan...");
|
|
107
|
+
if (prompt) console.log(` Prompt : ${prompt.substring(0, 100)}`);
|
|
108
|
+
if (jmxFilePath) console.log(` JMX file: ${jmxFilePath}`);
|
|
109
|
+
|
|
110
|
+
const result = await this._fetch("POST", "/generate-test-plan", {
|
|
111
|
+
body: form,
|
|
112
|
+
headers: form.getHeaders(),
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
if (result.status !== "success") {
|
|
116
|
+
throw new Error(
|
|
117
|
+
`Test plan generation failed: ${result.message || JSON.stringify(result)}`
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
console.log(`✅ STEP 1 done — JMX: ${result.jmx_filename}`);
|
|
122
|
+
return result;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ══════════════════════════════════════════════════════════════
|
|
126
|
+
// STEP 2 — Run Test
|
|
127
|
+
// POST /run-test/{jmx_filename}
|
|
128
|
+
// Source: app.py line 1022
|
|
129
|
+
//
|
|
130
|
+
// Returns: { status: "started", task_id: "uuid-xxxx" }
|
|
131
|
+
// ══════════════════════════════════════════════════════════════
|
|
132
|
+
async runTest(jmxFilename, { numThreads, loopCount, rampTime } = {}) {
|
|
133
|
+
if (!jmxFilename) {
|
|
134
|
+
throw new Error("runTest: jmxFilename is required");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const body = {};
|
|
138
|
+
if (numThreads !== undefined) body.num_threads = numThreads;
|
|
139
|
+
if (loopCount !== undefined) body.loop_count = loopCount;
|
|
140
|
+
if (rampTime !== undefined) body.ramp_time = rampTime;
|
|
141
|
+
|
|
142
|
+
console.log(`\n▶️ STEP 2: Running test — ${jmxFilename}`);
|
|
143
|
+
console.log(` Overrides: ${JSON.stringify(body) || "none (using JMX defaults)"}`);
|
|
144
|
+
|
|
145
|
+
const result = await this._fetch(
|
|
146
|
+
"POST",
|
|
147
|
+
`/run-test/${encodeURIComponent(jmxFilename)}`,
|
|
148
|
+
{ body: JSON.stringify(body), isJson: true }
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
if (result.status !== "started") {
|
|
152
|
+
throw new Error(`Run test failed: ${result.message || JSON.stringify(result)}`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
console.log(`✅ STEP 2 done — Task ID: ${result.task_id}`);
|
|
156
|
+
return result;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ══════════════════════════════════════════════════════════════
|
|
160
|
+
// STEP 3 — Poll Task Status
|
|
161
|
+
// GET /task-status/{task_id}
|
|
162
|
+
// Source: app.py line 899
|
|
163
|
+
//
|
|
164
|
+
// Polls every 5s until status = "success" | "error"
|
|
165
|
+
// Returns: { status, result_file, pdf_file, summary_output }
|
|
166
|
+
// ══════════════════════════════════════════════════════════════
|
|
167
|
+
async pollUntilDone(taskId) {
|
|
168
|
+
if (!taskId) throw new Error("pollUntilDone: taskId is required");
|
|
169
|
+
|
|
170
|
+
console.log(`\n⏳ STEP 3: Polling task ${taskId}`);
|
|
171
|
+
const started = Date.now();
|
|
172
|
+
let polls = 0;
|
|
173
|
+
|
|
174
|
+
while (Date.now() - started < POLL_TIMEOUT_MS) {
|
|
175
|
+
await sleep(POLL_INTERVAL_MS);
|
|
176
|
+
polls++;
|
|
177
|
+
|
|
178
|
+
const result = await this._fetch("GET", `/task-status/${taskId}`);
|
|
179
|
+
const elapsed = Math.round((Date.now() - started) / 1000);
|
|
180
|
+
|
|
181
|
+
if (result.status === "running") {
|
|
182
|
+
process.stdout.write(`\r ⏳ Running... ${elapsed}s (poll #${polls})`);
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (result.status === "success") {
|
|
187
|
+
console.log(`\n✅ STEP 3 done in ${elapsed}s`);
|
|
188
|
+
console.log(` JTL: ${result.result_file}`);
|
|
189
|
+
console.log(` PDF: ${result.pdf_file}`);
|
|
190
|
+
return result;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (result.status === "error") {
|
|
194
|
+
throw new Error(`Task ${taskId} failed: ${result.message}`);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
throw new Error(`Task ${taskId} unknown status: ${result.status}`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
throw new Error(
|
|
201
|
+
`Task ${taskId} timed out after ${POLL_TIMEOUT_MS / 60000} minutes.\n` +
|
|
202
|
+
`Possible reasons:\n` +
|
|
203
|
+
`- Large load test\n` +
|
|
204
|
+
`- Slow backend\n` +
|
|
205
|
+
`- KickLoad server delay`
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ══════════════════════════════════════════════════════════════
|
|
210
|
+
// STEP 4 — Analyze JTL
|
|
211
|
+
// POST /analyzeJTL
|
|
212
|
+
// Source: app.py line 1127
|
|
213
|
+
//
|
|
214
|
+
// Returns: { filename: "analysis_xxx.pdf" }
|
|
215
|
+
// ══════════════════════════════════════════════════════════════
|
|
216
|
+
async analyzeJtl(jtlFilename) {
|
|
217
|
+
if (!jtlFilename) throw new Error("analyzeJtl: jtlFilename is required");
|
|
218
|
+
|
|
219
|
+
console.log(`\n🔬 STEP 4: Analyzing JTL — ${jtlFilename}`);
|
|
220
|
+
|
|
221
|
+
const result = await this._fetch("POST", "/analyzeJTL", {
|
|
222
|
+
body: JSON.stringify({ filename: jtlFilename }),
|
|
223
|
+
isJson: true,
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
if (!result.filename) {
|
|
227
|
+
throw new Error(`JTL analysis returned no filename: ${JSON.stringify(result)}`);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
console.log(`✅ STEP 4 done — Analysis PDF: ${result.filename}`);
|
|
231
|
+
return result;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ══════════════════════════════════════════════════════════════
|
|
235
|
+
// STEP 5 — Get Download URL
|
|
236
|
+
// GET /download/{filename}?mode=inline
|
|
237
|
+
// Source: app.py line 1472
|
|
238
|
+
//
|
|
239
|
+
// Returns: { status: "success", download_url: "https://s3.amazonaws.com/..." }
|
|
240
|
+
// ══════════════════════════════════════════════════════════════
|
|
241
|
+
async getDownloadUrl(filename) {
|
|
242
|
+
if (!filename) return null;
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
const result = await this._fetch(
|
|
246
|
+
"GET",
|
|
247
|
+
`/download/${encodeURIComponent(filename)}?mode=inline`
|
|
248
|
+
);
|
|
249
|
+
return result.download_url || null;
|
|
250
|
+
} catch (err) {
|
|
251
|
+
console.warn(`⚠️ Download URL failed for ${filename}: ${err.message}`);
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ─── Utility ──────────────────────────────────────────────────
|
|
258
|
+
function sleep(ms) {
|
|
259
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
260
|
+
}
|
package/package.json
CHANGED
|
@@ -1,51 +1,51 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "kickload-watcher-mcp",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Automated API performance testing for Claude Code teams via KickLoad",
|
|
5
|
-
"main": "index.js",
|
|
6
|
-
"type": "module",
|
|
7
|
-
"bin": {
|
|
8
|
-
"kickload-watcher-mcp": "./index.js"
|
|
9
|
-
},
|
|
10
|
-
"scripts": {
|
|
11
|
-
"start": "node index.js",
|
|
12
|
-
"dev": "node --watch index.js",
|
|
13
|
-
"setup": "node setup.js"
|
|
14
|
-
},
|
|
15
|
-
"dependencies": {
|
|
16
|
-
"@anthropic-ai/sdk": "^0.39.0",
|
|
17
|
-
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
18
|
-
"chokidar": "^3.6.0",
|
|
19
|
-
"nodemailer": "^6.9.0",
|
|
20
|
-
"node-fetch": "^3.3.0",
|
|
21
|
-
"form-data": "^4.0.0",
|
|
22
|
-
"dotenv": "^16.4.0",
|
|
23
|
-
"chalk": "^5.3.0",
|
|
24
|
-
"unzipper": "^0.10.14",
|
|
25
|
-
"tar": "^6.2.0"
|
|
26
|
-
},
|
|
27
|
-
"engines": {
|
|
28
|
-
"node": ">=18.0.0"
|
|
29
|
-
},
|
|
30
|
-
"keywords": [
|
|
31
|
-
"kickload",
|
|
32
|
-
"mcp",
|
|
33
|
-
"claude",
|
|
34
|
-
"testing",
|
|
35
|
-
"watcher",
|
|
36
|
-
"oneqa"
|
|
37
|
-
],
|
|
38
|
-
"author": "NeeyatAI",
|
|
39
|
-
"license": "MIT",
|
|
40
|
-
"repository": {
|
|
41
|
-
"type": "git",
|
|
42
|
-
"url": "https://github.com/KickLoad/kickload-watcher-mcp.git"
|
|
43
|
-
},
|
|
44
|
-
"homepage": "https://github.com/KickLoad/kickload-watcher-mcp",
|
|
45
|
-
"files": [
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
"preferGlobal": true
|
|
51
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "kickload-watcher-mcp",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "Automated API performance testing for Claude Code teams via KickLoad",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"kickload-watcher-mcp": "./index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node index.js",
|
|
12
|
+
"dev": "node --watch index.js",
|
|
13
|
+
"setup": "node setup.js"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@anthropic-ai/sdk": "^0.39.0",
|
|
17
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
18
|
+
"chokidar": "^3.6.0",
|
|
19
|
+
"nodemailer": "^6.9.0",
|
|
20
|
+
"node-fetch": "^3.3.0",
|
|
21
|
+
"form-data": "^4.0.0",
|
|
22
|
+
"dotenv": "^16.4.0",
|
|
23
|
+
"chalk": "^5.3.0",
|
|
24
|
+
"unzipper": "^0.10.14",
|
|
25
|
+
"tar": "^6.2.0"
|
|
26
|
+
},
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=18.0.0"
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"kickload",
|
|
32
|
+
"mcp",
|
|
33
|
+
"claude",
|
|
34
|
+
"testing",
|
|
35
|
+
"watcher",
|
|
36
|
+
"oneqa"
|
|
37
|
+
],
|
|
38
|
+
"author": "NeeyatAI",
|
|
39
|
+
"license": "MIT",
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "https://github.com/KickLoad/kickload-watcher-mcp.git"
|
|
43
|
+
},
|
|
44
|
+
"homepage": "https://github.com/KickLoad/kickload-watcher-mcp",
|
|
45
|
+
"files": [
|
|
46
|
+
"**/*.js",
|
|
47
|
+
"users.json",
|
|
48
|
+
"README.md"
|
|
49
|
+
],
|
|
50
|
+
"preferGlobal": true
|
|
51
|
+
}
|