kickload-watcher-mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/setup.js ADDED
@@ -0,0 +1,201 @@
1
+ import readline from "readline";
2
+ import fs from "fs";
3
+ import path from "path";
4
+
5
+ const ENV_PATH = path.resolve(".env");
6
+ const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
7
+
8
+ /**
9
+ * STARTUP FIX: After writing .env, we reload env and CONTINUE — never exit.
10
+ * The process stays alive and proceeds to start the watcher.
11
+ */
12
+ export async function runFirstRunSetup() {
13
+ if (fs.existsSync(ENV_PATH)) {
14
+ console.log("⚠ .env already exists. Delete it to re-run setup.");
15
+ ensureGitignore();
16
+ return;
17
+ }
18
+
19
+ printBanner();
20
+ console.log("No .env found — running one-time setup before starting.\n");
21
+ console.log("You will need:");
22
+ console.log(" • ANTHROPIC_API_KEY — console.anthropic.com");
23
+ console.log(" • KICKLOAD_API_TOKEN — kickload.neeyatai.com");
24
+ console.log(" • Gmail address + App Password (for result emails)");
25
+ console.log(" • NGROK_AUTHTOKEN — dashboard.ngrok.com (required for localhost backends)\n");
26
+ console.log("⚠️ Values are stored in a local .env file. Never commit it.\n");
27
+
28
+ const anthropicKey = await askRequired("ANTHROPIC_API_KEY : ", true);
29
+ const kickloadToken = await askRequired("KICKLOAD_API_TOKEN : ", true);
30
+ const smtpEmail = await askEmail( "Your Gmail address : ");
31
+ const smtpPass = await askRequired(
32
+ "Gmail App Password : (Use App Password, NOT your normal password)\n Create here: https://myaccount.google.com/apppasswords\n> ",
33
+ true
34
+ );
35
+ const devEmail = await askEmail( "Developer email : ");
36
+ const ngrokToken = await askRequired(
37
+ "NGROK_AUTHTOKEN : (Required for localhost testing)\n Get it from: https://dashboard.ngrok.com/get-started/your-authtoken\n> ",
38
+ true
39
+ );
40
+
41
+ if (!ngrokToken) {
42
+ console.log("\n⚠️ ngrok skipped.");
43
+ console.log(" Localhost backends will attempt unauthenticated tunnels (may be rate-limited).");
44
+ console.log(" For reliable testing, add to .env later:");
45
+ console.log(" NGROK_AUTHTOKEN=<your_token> (dashboard.ngrok.com)");
46
+ console.log("");
47
+ }
48
+
49
+ const lines = [
50
+ "# KickLoad Watcher — generated by setup",
51
+ "# ⚠️ Keep this file SECRET — never commit to git",
52
+ "",
53
+ `ANTHROPIC_API_KEY=${anthropicKey}`,
54
+ `KICKLOAD_API_TOKEN=${kickloadToken}`,
55
+ `KICKLOAD_BASE_URL=https://kickload.neeyatai.com/api`,
56
+ "",
57
+ `# ngrok: auto-enabled for localhost backends regardless of this flag`,
58
+ `NGROK_AUTHTOKEN=${ngrokToken || ""}`,
59
+ "",
60
+ `EMAIL_PROVIDER=smtp`,
61
+ `EMAIL_FROM_NAME=KickLoad Watcher`,
62
+ `EMAIL_FROM_ADDRESS=${smtpEmail}`,
63
+ `SMTP_HOST=smtp.gmail.com`,
64
+ `SMTP_PORT=465`,
65
+ `SMTP_USER=${smtpEmail}`,
66
+ `SMTP_PASS=${smtpPass}`,
67
+ "",
68
+ `DEFAULT_DEVELOPER_EMAIL=${devEmail}`,
69
+ "",
70
+ `WATCH_PATHS=.`,
71
+ `TRIGGER_MODE=claudecode`,
72
+ `LOG_LEVEL=info`,
73
+ "",
74
+ "# Optional: override auto-detected backend URL",
75
+ "# TARGET_API_BASE_URL=https://your-api.example.com",
76
+ "",
77
+ ];
78
+
79
+ fs.writeFileSync(ENV_PATH, lines.join("\n"), "utf-8");
80
+ ensureGitignore();
81
+
82
+ // Re-load env immediately so the rest of the process sees the values.
83
+ const { default: dotenv } = await import("dotenv");
84
+ dotenv.config({ override: true });
85
+
86
+ // STARTUP FIX: Do NOT exit — continue execution
87
+ console.log("\n✅ Setup complete — starting KickLoad Watcher now...\n");
88
+ }
89
+
90
+ export function ensureGitignore() {
91
+ const gitignorePath = path.resolve(".gitignore");
92
+ const required = [".env", ".bin/"];
93
+
94
+ let content = "";
95
+ try {
96
+ if (fs.existsSync(gitignorePath)) content = fs.readFileSync(gitignorePath, "utf-8");
97
+ } catch { /* non-fatal */ }
98
+
99
+ const existing = content.split("\n").map(l => l.trim());
100
+ const missing = required.filter(r => !existing.includes(r));
101
+ if (missing.length === 0) return;
102
+
103
+ const prefix = content && !content.endsWith("\n") ? "\n" : "";
104
+ fs.appendFileSync(gitignorePath, `${prefix}# KickLoad Watcher\n${missing.join("\n")}\n`, "utf-8");
105
+ console.warn("⚠️ Added .env and .bin/ to .gitignore — never commit your API keys.");
106
+ }
107
+
108
+ // ── Input helpers ─────────────────────────────────────────────────────────────
109
+
110
+ async function askRequired(prompt, masked) {
111
+ while (true) {
112
+ const val = masked ? await inputMasked(prompt) : await inputPlain(prompt);
113
+ if (val.length > 0) return val;
114
+ console.log(" This field is required — please try again.");
115
+ }
116
+ }
117
+
118
+ async function askEmail(prompt) {
119
+ while (true) {
120
+ const val = await inputPlain(prompt);
121
+ if (EMAIL_RE.test(val)) return val;
122
+ console.log(" Invalid email address — please try again.");
123
+ }
124
+ }
125
+
126
+ async function askOptional(prompt) {
127
+ return inputPlain(prompt);
128
+ }
129
+
130
+ function inputPlain(prompt) {
131
+ return new Promise((resolve) => {
132
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
133
+ rl.question(prompt, (a) => { rl.close(); resolve(a.trim()); });
134
+ });
135
+ }
136
+
137
+ function inputMasked(prompt) {
138
+ if (!process.stdin.isTTY) return inputPlain(prompt);
139
+
140
+ return new Promise((resolve) => {
141
+ process.stdout.write(prompt);
142
+
143
+ let value = "";
144
+ let done = false;
145
+
146
+ const cleanup = () => {
147
+ if (done) return;
148
+ done = true;
149
+ try { process.stdin.setRawMode(false); } catch { /* ignore */ }
150
+ process.stdin.removeListener("data", handler);
151
+ process.stdin.pause();
152
+ };
153
+
154
+ const handler = (raw) => {
155
+ const char = raw.toString();
156
+
157
+ if (char === "\u0003") {
158
+ cleanup();
159
+ process.stdout.write("\n");
160
+ process.exit(0);
161
+ }
162
+
163
+ if (char === "\r" || char === "\n") {
164
+ cleanup();
165
+ process.stdout.write("\n");
166
+ resolve(value.trim());
167
+ return;
168
+ }
169
+
170
+ if (char === "\u007F" || char === "\b") {
171
+ if (value.length > 0) {
172
+ value = value.slice(0, -1);
173
+ process.stdout.clearLine(0);
174
+ process.stdout.cursorTo(0);
175
+ process.stdout.write(prompt + "*".repeat(value.length));
176
+ }
177
+ return;
178
+ }
179
+
180
+ value += char;
181
+ process.stdout.write("*");
182
+ };
183
+
184
+ process.stdin.setRawMode(true);
185
+ process.stdin.resume();
186
+ process.stdin.setEncoding("utf8");
187
+ process.stdin.on("data", handler);
188
+ });
189
+ }
190
+
191
+ function printBanner() {
192
+ console.log(`
193
+ ╔══════════════════════════════════════════════╗
194
+ ║ KickLoad Watcher — First-Run Setup ║
195
+ ║ Automated API Performance Testing (OneQA) ║
196
+ ╚══════════════════════════════════════════════╝`);
197
+ }
198
+ runFirstRunSetup().catch(err => {
199
+ console.error("❌ Setup failed:", err.message);
200
+ process.exit(1);
201
+ });
@@ -0,0 +1,66 @@
1
+ import Anthropic from "@anthropic-ai/sdk";
2
+ import { config } from "./config.js";
3
+ import { createLogger } from "./logger.js";
4
+
5
+ const logger = createLogger("test-generator");
6
+ const client = new Anthropic({ apiKey: config.anthropic.apiKey });
7
+
8
+ export async function generateTestPrompt({ fileContent, detectedEndpoints, originalPrompt, fileName, backendBaseUrl }) {
9
+ if (!detectedEndpoints || detectedEndpoints.length === 0) {
10
+ throw new Error("generateTestPrompt: no endpoints provided");
11
+ }
12
+
13
+ const resolvedBase = (backendBaseUrl || process.env.TARGET_API_BASE_URL || "").replace(/\/$/, "");
14
+
15
+ if (!resolvedBase) {
16
+ throw new Error("No backend URL — set TARGET_API_BASE_URL or ensure backend is running");
17
+ }
18
+
19
+ if (resolvedBase.includes("localhost") || resolvedBase.includes("127.0.0.1")) {
20
+ throw new Error(
21
+ `Localhost URL cannot be sent to KickLoad: ${resolvedBase}\n` +
22
+ `Set NGROK_ENABLED=true in .env or set TARGET_API_BASE_URL to a public URL.`
23
+ );
24
+ }
25
+
26
+ logger.info(`Generating prompt for ${detectedEndpoints.length} endpoint(s) at ${resolvedBase}`);
27
+
28
+ const fullUrls = detectedEndpoints.map(ep => `${resolvedBase}${ep}`);
29
+
30
+ let numThreads = 5, loopCount = 2, rampTime = 2;
31
+
32
+ try {
33
+ const response = await client.messages.create({
34
+ model: config.anthropic.model,
35
+ max_tokens: 200,
36
+ system:
37
+ `You are a JMeter load test engineer. Return ONLY valid JSON — no markdown, no explanation.
38
+ {"num_threads":<5-50>,"loop_count":<2-10>,"ramp_time":<2-10>,"reasoning":"<one sentence>"}
39
+ Be conservative for new APIs.`,
40
+ messages: [{
41
+ role: "user",
42
+ content: `File: ${fileName}\nPrompt: ${originalPrompt || "N/A"}\nEndpoints: ${detectedEndpoints.join(", ")}\n\nCode:\n${fileContent.substring(0, 2000)}`,
43
+ }],
44
+ });
45
+
46
+ const parsed = JSON.parse((response.content[0]?.text || "").replace(/```json|```/gi, "").trim());
47
+ numThreads = clamp(parsed.num_threads, 5, 50);
48
+ loopCount = clamp(parsed.loop_count, 2, 10);
49
+ rampTime = clamp(parsed.ramp_time, 2, 10);
50
+ logger.info(`Parameters: threads=${numThreads} loops=${loopCount} ramp=${rampTime}s`);
51
+ } catch (err) {
52
+ logger.warn(`Claude unavailable — using defaults: ${err.message}`);
53
+ }
54
+
55
+ const testPrompt =
56
+ `Generate a JMeter load test. Send HTTP GET requests to the following endpoints: ${fullUrls.join(" ")}. ` +
57
+ `Use ${numThreads} threads, ${loopCount} loops, ${rampTime} seconds ramp-up. ` +
58
+ `Validate status 200. Include summary report.`;
59
+
60
+ return { testPrompt, numThreads, loopCount, rampTime, endpoints: detectedEndpoints, fullUrls, primaryEndpoint: detectedEndpoints[0] };
61
+ }
62
+
63
+ function clamp(value, min, max) {
64
+ const n = Number(value);
65
+ return isNaN(n) ? min : Math.min(Math.max(n, min), max);
66
+ }
package/users.json ADDED
@@ -0,0 +1,5 @@
1
+ {
2
+ "usr_abc123def456": "john.doe@yourcompany.com",
3
+ "usr_xyz789ghi012": "jane.smith@yourcompany.com",
4
+ "usr_mno345pqr678": "dev.team@yourcompany.com"
5
+ }