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/README.md +233 -0
- package/compliance-poller.js +215 -0
- package/config.js +103 -0
- package/email-cooldown.js +23 -0
- package/email-sender.js +137 -0
- package/env-check.js +117 -0
- package/file-watcher.js +230 -0
- package/github-webhook.js +321 -0
- package/identity-map.js +75 -0
- package/index.js +270 -0
- package/kickload-client.js +254 -0
- package/logger.js +118 -0
- package/ngrok-manager.js +313 -0
- package/package.json +51 -0
- package/pipeline.js +448 -0
- package/server-detector.js +109 -0
- package/setup.js +201 -0
- package/test-generator.js +66 -0
- package/users.json +5 -0
package/README.md
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
# KickLoad Watcher MCP
|
|
2
|
+
|
|
3
|
+
Automated API performance testing for Claude Enterprise teams.
|
|
4
|
+
|
|
5
|
+
When a developer writes an API using Claude Code, this watcher **automatically**:
|
|
6
|
+
1. Detects the new endpoint
|
|
7
|
+
2. Generates smart load test parameters using Claude
|
|
8
|
+
3. Runs the full test pipeline via KickLoad
|
|
9
|
+
4. Emails the developer their results
|
|
10
|
+
|
|
11
|
+
**No GitHub required. Works directly with Claude Code CLI.**
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## How It Works
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
Developer writes API with Claude Code
|
|
19
|
+
↓
|
|
20
|
+
Watcher detects new session (Compliance API) + saved file (file watcher)
|
|
21
|
+
↓
|
|
22
|
+
Claude generates optimal test parameters for the endpoint
|
|
23
|
+
↓
|
|
24
|
+
KickLoad runs: generate JMX → run test → analyze JTL → return PDF
|
|
25
|
+
↓
|
|
26
|
+
Developer receives email with PASS/FAIL + latency + error rate + PDF link
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Prerequisites
|
|
32
|
+
|
|
33
|
+
| Requirement | Where to Get It |
|
|
34
|
+
|---|---|
|
|
35
|
+
| Claude Enterprise plan | Required for Compliance API |
|
|
36
|
+
| KickLoad API token | kickload.neeyatai.com → API's → Generate API Key |
|
|
37
|
+
| Anthropic API key | platform.claude.com → API Keys |
|
|
38
|
+
| Node.js 18+ | nodejs.org |
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Setup (3 Steps)
|
|
43
|
+
|
|
44
|
+
### Step 1 — Install
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
git clone <this-repo>
|
|
48
|
+
cd kickload-watcher-mcp
|
|
49
|
+
npm install
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Step 2 — Configure
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
cp .env.example .env
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Fill in your `.env`:
|
|
59
|
+
|
|
60
|
+
```env
|
|
61
|
+
ANTHROPIC_API_KEY=sk-ant-api03-...
|
|
62
|
+
ANTHROPIC_COMPLIANCE_API_KEY= # From Anthropic Console → Data and Privacy
|
|
63
|
+
KICKLOAD_API_TOKEN=kl-... # From kickload.neeyatai.com → API's
|
|
64
|
+
EMAIL_PROVIDER=smtp
|
|
65
|
+
SMTP_HOST=smtp.gmail.com
|
|
66
|
+
SMTP_USER=your@gmail.com
|
|
67
|
+
SMTP_PASS=your-app-password
|
|
68
|
+
EMAIL_FROM_ADDRESS=your@gmail.com
|
|
69
|
+
WATCH_PATHS=/home/user/projects
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Step 3 — Map Developers
|
|
73
|
+
|
|
74
|
+
Edit `users.json` to map Claude user IDs to emails:
|
|
75
|
+
|
|
76
|
+
```json
|
|
77
|
+
{
|
|
78
|
+
"usr_abc123": "john@company.com",
|
|
79
|
+
"usr_xyz789": "jane@company.com"
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Find Claude user IDs in: **Anthropic Console → Members**
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Add to Claude Code
|
|
88
|
+
|
|
89
|
+
**Option A — Claude Desktop** (`claude_desktop_config.json`):
|
|
90
|
+
|
|
91
|
+
```json
|
|
92
|
+
{
|
|
93
|
+
"mcpServers": {
|
|
94
|
+
"kickload-watcher": {
|
|
95
|
+
"command": "npx",
|
|
96
|
+
"args": ["-y", "@kickload/watcher-mcp"],
|
|
97
|
+
"env": {
|
|
98
|
+
"KICKLOAD_API_TOKEN": "your-token",
|
|
99
|
+
"ANTHROPIC_API_KEY": "your-key",
|
|
100
|
+
"EMAIL_PROVIDER": "smtp",
|
|
101
|
+
"SMTP_HOST": "smtp.gmail.com",
|
|
102
|
+
"SMTP_USER": "your@gmail.com",
|
|
103
|
+
"SMTP_PASS": "your-password",
|
|
104
|
+
"EMAIL_FROM_ADDRESS": "your@gmail.com",
|
|
105
|
+
"WATCH_PATHS": "/home/user/projects"
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
**Option B — Claude Code CLI**:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
claude mcp add kickload-watcher --type stdio --command "node /path/to/index.js"
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
**Option C — Run directly**:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
node index.js
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Enable Compliance API (Enterprise Required)
|
|
127
|
+
|
|
128
|
+
1. Go to **platform.claude.com**
|
|
129
|
+
2. Organization Settings → Data and Privacy
|
|
130
|
+
3. Enable Compliance API
|
|
131
|
+
4. Create a key and add it to `.env` as `ANTHROPIC_COMPLIANCE_API_KEY`
|
|
132
|
+
|
|
133
|
+
> Only Primary Owners can enable the Compliance API.
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## MCP Tools Available to Claude
|
|
138
|
+
|
|
139
|
+
Once connected, Claude can call these tools directly:
|
|
140
|
+
|
|
141
|
+
| Tool | What It Does |
|
|
142
|
+
|---|---|
|
|
143
|
+
| `run_kickload_test` | Manually trigger a test for any endpoint |
|
|
144
|
+
| `get_test_status` | Check watcher status and configuration |
|
|
145
|
+
| `lookup_developer` | Find email for a Claude user ID |
|
|
146
|
+
| `send_test_email` | Verify email configuration |
|
|
147
|
+
|
|
148
|
+
**Example — Claude can say:**
|
|
149
|
+
> "Run a load test on /api/checkout and email results to dev@company.com"
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## Email Providers
|
|
154
|
+
|
|
155
|
+
| Provider | Config |
|
|
156
|
+
|---|---|
|
|
157
|
+
| SMTP (Gmail) | `EMAIL_PROVIDER=smtp` + SMTP_* vars |
|
|
158
|
+
| SendGrid | `EMAIL_PROVIDER=sendgrid` + `SENDGRID_API_KEY` |
|
|
159
|
+
| AWS SES | `EMAIL_PROVIDER=ses` + AWS_* vars |
|
|
160
|
+
|
|
161
|
+
**Gmail tip**: Use an App Password, not your account password.
|
|
162
|
+
Generate at: Google Account → Security → 2-Step Verification → App passwords
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## Project Structure
|
|
167
|
+
|
|
168
|
+
```
|
|
169
|
+
kickload-watcher-mcp/
|
|
170
|
+
├── index.js ← MCP server + tool handlers
|
|
171
|
+
├── orchestrator.js ← Main pipeline coordinator
|
|
172
|
+
├── compliance-poller.js ← Anthropic Compliance API watcher
|
|
173
|
+
├── file-watcher.js ← Filesystem watcher for saved code
|
|
174
|
+
├── test-generator.js ← Claude API test parameter generator
|
|
175
|
+
├── kickload-runner.js ← KickLoad API integration (/cra-line)
|
|
176
|
+
├── email-sender.js ← Email delivery (SMTP/SendGrid/SES)
|
|
177
|
+
├── identity-map.js ← Claude user ID → email mapping
|
|
178
|
+
├── config.js ← All configuration
|
|
179
|
+
├── users.json ← Developer identity map (you fill this)
|
|
180
|
+
├── .env.example ← Environment variable template
|
|
181
|
+
└── package.json
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## What the Email Looks Like
|
|
187
|
+
|
|
188
|
+
```
|
|
189
|
+
Subject: [KickLoad Watcher] ✅ PASS — /api/checkout tested
|
|
190
|
+
|
|
191
|
+
Hi John,
|
|
192
|
+
|
|
193
|
+
Your API endpoint was automatically tested by KickLoad Watcher.
|
|
194
|
+
|
|
195
|
+
STATUS: ✅ PASS
|
|
196
|
+
ENDPOINT: /api/checkout
|
|
197
|
+
TESTED AT: 2 Apr 2026, 14:32 IST
|
|
198
|
+
|
|
199
|
+
RESULTS:
|
|
200
|
+
Average Latency: 123ms
|
|
201
|
+
Error Rate: 0.5%
|
|
202
|
+
Throughput: 4,500 req/s
|
|
203
|
+
Test Duration: 47s
|
|
204
|
+
|
|
205
|
+
📄 Download Full Report → [link expires in 24hrs]
|
|
206
|
+
|
|
207
|
+
— KickLoad Watcher | Automated by NeeyatAI
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## Troubleshooting
|
|
213
|
+
|
|
214
|
+
**No email received?**
|
|
215
|
+
- Run `send_test_email` MCP tool to verify email config
|
|
216
|
+
- Check `users.json` has the correct Claude user ID mapped
|
|
217
|
+
- Gmail: ensure App Password is used, not account password
|
|
218
|
+
|
|
219
|
+
**Compliance API not working?**
|
|
220
|
+
- Verify Enterprise plan is active
|
|
221
|
+
- Only Primary Owners can enable Compliance API
|
|
222
|
+
- File watcher still works without it
|
|
223
|
+
|
|
224
|
+
**Test not triggering?**
|
|
225
|
+
- Check `WATCH_PATHS` includes your project directories
|
|
226
|
+
- Ensure files have supported extensions (.js, .ts, .py, etc.)
|
|
227
|
+
- Check console logs for "API code detected" messages
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## License
|
|
232
|
+
|
|
233
|
+
MIT © NeeyatAI
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
// compliance-poller.js — Polls Anthropic Compliance API for new Claude Code sessions
|
|
2
|
+
// Enterprise plan required. Enable at:
|
|
3
|
+
// platform.claude.com → Organization Settings → Data and Privacy → Compliance API
|
|
4
|
+
|
|
5
|
+
import { config } from "./config.js";
|
|
6
|
+
|
|
7
|
+
let lastEventTimestamp = null;
|
|
8
|
+
let pollingInterval = null;
|
|
9
|
+
const sessionCallbacks = [];
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Register a callback to be called when a new API-related session is detected.
|
|
13
|
+
* Callback receives: { userId, sessionId, generatedCode, prompt, timestamp }
|
|
14
|
+
*/
|
|
15
|
+
export function onNewSession(callback) {
|
|
16
|
+
sessionCallbacks.push(callback);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Start polling the Compliance API on the configured interval.
|
|
21
|
+
*/
|
|
22
|
+
export function startCompliancePoller() {
|
|
23
|
+
if (!config.anthropic.complianceApiKey) {
|
|
24
|
+
console.warn("⚠️ ANTHROPIC_COMPLIANCE_API_KEY not set.");
|
|
25
|
+
console.warn(" Compliance API polling disabled.");
|
|
26
|
+
console.warn(" File watcher will be the only trigger.");
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
console.log(
|
|
31
|
+
`🔍 Compliance API poller started (every ${config.anthropic.pollIntervalMs / 1000}s)`
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
// Set initial timestamp to now so we only pick up new events
|
|
35
|
+
lastEventTimestamp = new Date().toISOString();
|
|
36
|
+
|
|
37
|
+
pollingInterval = setInterval(async () => {
|
|
38
|
+
try {
|
|
39
|
+
await pollComplianceEvents();
|
|
40
|
+
} catch (err) {
|
|
41
|
+
console.error("❌ Compliance API poll failed:", err.message);
|
|
42
|
+
}
|
|
43
|
+
}, config.anthropic.pollIntervalMs);
|
|
44
|
+
|
|
45
|
+
// Run immediately on start
|
|
46
|
+
pollComplianceEvents().catch((err) =>
|
|
47
|
+
console.error("❌ Initial compliance poll failed:", err.message)
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Stop the compliance poller.
|
|
53
|
+
*/
|
|
54
|
+
export function stopCompliancePoller() {
|
|
55
|
+
if (pollingInterval) {
|
|
56
|
+
clearInterval(pollingInterval);
|
|
57
|
+
pollingInterval = null;
|
|
58
|
+
console.log("⏹ Compliance API poller stopped.");
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Fetch recent events from the Compliance API and filter for API-related code.
|
|
64
|
+
*/
|
|
65
|
+
async function pollComplianceEvents() {
|
|
66
|
+
const url = new URL(`${config.anthropic.complianceBaseUrl}/organizations/audit-log`);
|
|
67
|
+
|
|
68
|
+
if (lastEventTimestamp) {
|
|
69
|
+
url.searchParams.set("after", lastEventTimestamp);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
url.searchParams.set("limit", "50");
|
|
73
|
+
|
|
74
|
+
const response = await fetch(url.toString(), {
|
|
75
|
+
method: "GET",
|
|
76
|
+
headers: {
|
|
77
|
+
"anthropic-organization-key": config.anthropic.complianceApiKey,
|
|
78
|
+
"Content-Type": "application/json",
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
if (!response.ok) {
|
|
83
|
+
const body = await response.text();
|
|
84
|
+
throw new Error(`Compliance API ${response.status}: ${body}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const data = await response.json();
|
|
88
|
+
const events = data.events || data.data || [];
|
|
89
|
+
|
|
90
|
+
if (events.length === 0) return;
|
|
91
|
+
|
|
92
|
+
// Update timestamp to latest event so we don't re-process
|
|
93
|
+
const latest = events[events.length - 1];
|
|
94
|
+
if (latest.created_at) {
|
|
95
|
+
lastEventTimestamp = latest.created_at;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Filter events that contain API-related code
|
|
99
|
+
for (const event of events) {
|
|
100
|
+
const session = extractApiSession(event);
|
|
101
|
+
if (session) {
|
|
102
|
+
console.log(`📡 New API session detected from user: ${session.userId}`);
|
|
103
|
+
for (const cb of sessionCallbacks) {
|
|
104
|
+
try {
|
|
105
|
+
await cb(session);
|
|
106
|
+
} catch (err) {
|
|
107
|
+
console.error("❌ Session callback error:", err.message);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Extract relevant session data from a compliance event.
|
|
116
|
+
* Returns null if the event doesn't contain API-related code.
|
|
117
|
+
*/
|
|
118
|
+
function extractApiSession(event) {
|
|
119
|
+
try {
|
|
120
|
+
// Compliance API event structure
|
|
121
|
+
const userId =
|
|
122
|
+
event.actor?.id ||
|
|
123
|
+
event.user_id ||
|
|
124
|
+
event.actor?.user_id ||
|
|
125
|
+
null;
|
|
126
|
+
|
|
127
|
+
const sessionId = event.id || event.session_id || null;
|
|
128
|
+
const timestamp = event.created_at || new Date().toISOString();
|
|
129
|
+
|
|
130
|
+
// Get message content — structure varies by event type
|
|
131
|
+
const messages =
|
|
132
|
+
event.payload?.messages ||
|
|
133
|
+
event.messages ||
|
|
134
|
+
event.content?.messages ||
|
|
135
|
+
[];
|
|
136
|
+
|
|
137
|
+
// Look through messages for generated code with API endpoints
|
|
138
|
+
let generatedCode = null;
|
|
139
|
+
let userPrompt = null;
|
|
140
|
+
|
|
141
|
+
for (const msg of messages) {
|
|
142
|
+
// Capture the user's original prompt
|
|
143
|
+
if (msg.role === "user" && typeof msg.content === "string") {
|
|
144
|
+
userPrompt = msg.content;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Look for assistant response containing API code
|
|
148
|
+
if (msg.role === "assistant") {
|
|
149
|
+
const content =
|
|
150
|
+
typeof msg.content === "string"
|
|
151
|
+
? msg.content
|
|
152
|
+
: Array.isArray(msg.content)
|
|
153
|
+
? msg.content.map((b) => b.text || "").join("\n")
|
|
154
|
+
: "";
|
|
155
|
+
|
|
156
|
+
// Detect if this looks like API endpoint code
|
|
157
|
+
if (containsApiCode(content)) {
|
|
158
|
+
generatedCode = content;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Only return if we found actual API code
|
|
164
|
+
if (!generatedCode || !userId) return null;
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
userId,
|
|
168
|
+
sessionId,
|
|
169
|
+
generatedCode,
|
|
170
|
+
prompt: userPrompt || "No prompt captured",
|
|
171
|
+
timestamp,
|
|
172
|
+
source: "compliance_api",
|
|
173
|
+
};
|
|
174
|
+
} catch (err) {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Detect if code content contains API endpoint definitions.
|
|
181
|
+
* Covers Express, Flask, FastAPI, Django, Spring, Go, Rails patterns.
|
|
182
|
+
*/
|
|
183
|
+
function containsApiCode(content) {
|
|
184
|
+
const apiPatterns = [
|
|
185
|
+
// JavaScript/Node.js (Express, Fastify, Hapi)
|
|
186
|
+
/app\.(get|post|put|patch|delete|use)\s*\(\s*['"`]/i,
|
|
187
|
+
/router\.(get|post|put|patch|delete)\s*\(\s*['"`]/i,
|
|
188
|
+
/fastify\.(get|post|put|patch|delete)\s*\(\s*['"`]/i,
|
|
189
|
+
|
|
190
|
+
// Python (Flask, FastAPI, Django)
|
|
191
|
+
/@app\.route\s*\(\s*['"`]/i,
|
|
192
|
+
/@router\.(get|post|put|patch|delete)\s*\(\s*['"`]/i,
|
|
193
|
+
/path\s*\(\s*['"`][^'"]+['"`]\s*,/i,
|
|
194
|
+
/@app\.(get|post|put|patch|delete)\s*\(\s*['"`]/i,
|
|
195
|
+
|
|
196
|
+
// Go (net/http, gin, echo)
|
|
197
|
+
/http\.HandleFunc\s*\(\s*['"`]/i,
|
|
198
|
+
/r\.(GET|POST|PUT|PATCH|DELETE)\s*\(\s*['"`]/i,
|
|
199
|
+
/e\.(GET|POST|PUT|PATCH|DELETE)\s*\(\s*['"`]/i,
|
|
200
|
+
|
|
201
|
+
// Java/Spring
|
|
202
|
+
/@(Get|Post|Put|Delete|Patch)Mapping\s*\(\s*['"`]/i,
|
|
203
|
+
/@RequestMapping\s*\(/i,
|
|
204
|
+
|
|
205
|
+
// Ruby on Rails
|
|
206
|
+
/resources\s+:/i,
|
|
207
|
+
/get\s+['"`][^'"]+['"`]\s*,\s*to:/i,
|
|
208
|
+
|
|
209
|
+
// General endpoint patterns
|
|
210
|
+
/\/api\//i,
|
|
211
|
+
/endpoint/i,
|
|
212
|
+
];
|
|
213
|
+
|
|
214
|
+
return apiPatterns.some((pattern) => pattern.test(content));
|
|
215
|
+
}
|
package/config.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import dotenv from "dotenv";
|
|
2
|
+
dotenv.config();
|
|
3
|
+
|
|
4
|
+
export const config = {
|
|
5
|
+
anthropic: {
|
|
6
|
+
apiKey: process.env.ANTHROPIC_API_KEY || "",
|
|
7
|
+
complianceApiKey: process.env.ANTHROPIC_COMPLIANCE_API_KEY || "",
|
|
8
|
+
complianceBaseUrl:"https://api.anthropic.com/v1",
|
|
9
|
+
pollIntervalMs: parseInt(process.env.POLL_INTERVAL_MS || "60000"),
|
|
10
|
+
model: "claude-sonnet-4-20250514",
|
|
11
|
+
},
|
|
12
|
+
|
|
13
|
+
kickload: {
|
|
14
|
+
apiToken: process.env.KICKLOAD_API_TOKEN || "",
|
|
15
|
+
baseUrl: process.env.KICKLOAD_BASE_URL || "https://kickload.neeyatai.com/api",
|
|
16
|
+
defaultThreads: parseInt(process.env.DEFAULT_THREADS || "5"),
|
|
17
|
+
defaultLoopCount: parseInt(process.env.DEFAULT_LOOPS || "2"),
|
|
18
|
+
defaultRampTime: parseInt(process.env.DEFAULT_RAMP || "2"),
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
targetApiBaseUrl: process.env.TARGET_API_BASE_URL || null,
|
|
22
|
+
|
|
23
|
+
// ngrok.enabled is now advisory only — pipeline auto-enables for localhost
|
|
24
|
+
ngrok: {
|
|
25
|
+
enabled: process.env.NGROK_ENABLED !== "false", // default true
|
|
26
|
+
authToken: process.env.NGROK_AUTHTOKEN || "",
|
|
27
|
+
binDir: ".bin",
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
email: {
|
|
31
|
+
provider: process.env.EMAIL_PROVIDER || "smtp",
|
|
32
|
+
smtp: {
|
|
33
|
+
host: process.env.SMTP_HOST || "smtp.gmail.com",
|
|
34
|
+
port: parseInt(process.env.SMTP_PORT || "465"),
|
|
35
|
+
user: process.env.SMTP_USER || "",
|
|
36
|
+
pass: process.env.SMTP_PASS || "",
|
|
37
|
+
},
|
|
38
|
+
sendgrid: { apiKey: process.env.SENDGRID_API_KEY || "" },
|
|
39
|
+
ses: {
|
|
40
|
+
accessKey: process.env.AWS_ACCESS_KEY_ID || "",
|
|
41
|
+
secretKey: process.env.AWS_SECRET_ACCESS_KEY || "",
|
|
42
|
+
region: process.env.AWS_REGION || "us-east-1",
|
|
43
|
+
},
|
|
44
|
+
fromName: process.env.EMAIL_FROM_NAME || "KickLoad Watcher",
|
|
45
|
+
fromAddress: process.env.EMAIL_FROM_ADDRESS || "",
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
watcher: {
|
|
49
|
+
watchPaths: (process.env.WATCH_PATHS || ".").split(",").map(p => p.trim()),
|
|
50
|
+
extensions: [".js", ".ts", ".py", ".go", ".java", ".rb", ".php", ".cs", ".rs"],
|
|
51
|
+
ignore: ["**/node_modules/**", "**/.git/**", "**/dist/**", "**/build/**"],
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
identity: {
|
|
55
|
+
mapFile: process.env.IDENTITY_MAP || "./users.json",
|
|
56
|
+
defaultUserId: process.env.DEFAULT_USER_ID || null,
|
|
57
|
+
defaultDevEmail: process.env.DEFAULT_DEVELOPER_EMAIL || null,
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
triggerMode: process.env.TRIGGER_MODE || "claudecode",
|
|
61
|
+
|
|
62
|
+
github: {
|
|
63
|
+
token: process.env.GITHUB_TOKEN || "",
|
|
64
|
+
webhookPort: parseInt(process.env.GITHUB_WEBHOOK_PORT || "3456"),
|
|
65
|
+
webhookSecret: process.env.GITHUB_WEBHOOK_SECRET || "",
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
logLevel: process.env.LOG_LEVEL || "info",
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export function validateConfig() {
|
|
72
|
+
const errors = [];
|
|
73
|
+
const warnings = [];
|
|
74
|
+
|
|
75
|
+
if (!config.anthropic.apiKey) errors.push("ANTHROPIC_API_KEY is required");
|
|
76
|
+
if (!config.kickload.apiToken) errors.push("KICKLOAD_API_TOKEN is required");
|
|
77
|
+
if (!config.email.fromAddress) errors.push("EMAIL_FROM_ADDRESS is required");
|
|
78
|
+
|
|
79
|
+
if (config.email.provider === "smtp") {
|
|
80
|
+
if (!config.email.smtp.user) errors.push("SMTP_USER is required for smtp provider");
|
|
81
|
+
if (!config.email.smtp.pass) errors.push("SMTP_PASS is required for smtp provider");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (config.email.provider === "sendgrid" && !config.email.sendgrid.apiKey) {
|
|
85
|
+
errors.push("SENDGRID_API_KEY is required when EMAIL_PROVIDER=sendgrid");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ngrok token is a warning, not a hard error — auto-mode handles it gracefully
|
|
89
|
+
if (!config.ngrok.authToken) {
|
|
90
|
+
warnings.push("NGROK_AUTHTOKEN not set — ngrok tunnel will not authenticate (localhost backends cannot be tested)");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (warnings.length) {
|
|
94
|
+
warnings.forEach(w => console.warn(` ⚠️ ${w}`));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (errors.length) {
|
|
98
|
+
console.error("\n❌ Missing required configuration:");
|
|
99
|
+
errors.forEach(e => console.error(` • ${e}`));
|
|
100
|
+
console.error("\n Fix: delete .env and re-run to go through setup again, or edit .env manually.\n");
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const COOLDOWN_MS = 60_000;
|
|
2
|
+
const lastSent = new Map();
|
|
3
|
+
|
|
4
|
+
export function isEmailAllowed(toEmail, identifier) {
|
|
5
|
+
const key = `${toEmail}:${identifier}`;
|
|
6
|
+
const last = lastSent.get(key);
|
|
7
|
+
return !last || Date.now() - last >= COOLDOWN_MS;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function getCooldownRemaining(toEmail, identifier) {
|
|
11
|
+
const last = lastSent.get(`${toEmail}:${identifier}`);
|
|
12
|
+
if (!last) return 0;
|
|
13
|
+
const rem = COOLDOWN_MS - (Date.now() - last);
|
|
14
|
+
return rem > 0 ? Math.ceil(rem / 1000) : 0;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function recordEmailSent(toEmail, identifier) {
|
|
18
|
+
const key = `${toEmail}:${identifier}`;
|
|
19
|
+
lastSent.set(key, Date.now());
|
|
20
|
+
for (const [k, ts] of lastSent.entries()) {
|
|
21
|
+
if (Date.now() - ts > COOLDOWN_MS * 2) lastSent.delete(k);
|
|
22
|
+
}
|
|
23
|
+
}
|