claude-crap 0.3.4 → 0.3.5
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/CHANGELOG.md +16 -0
- package/README.md +116 -472
- package/dist/dashboard/server.d.ts.map +1 -1
- package/dist/dashboard/server.js +112 -48
- package/dist/dashboard/server.js.map +1 -1
- package/dist/sarif/sarif-store.d.ts.map +1 -1
- package/dist/sarif/sarif-store.js +1 -1
- package/dist/sarif/sarif-store.js.map +1 -1
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.mcp.json +1 -2
- package/plugin/bundle/mcp-server.mjs +119 -85
- package/plugin/bundle/mcp-server.mjs.map +3 -3
- package/plugin/eslint.config.mjs +12 -0
- package/plugin/hooks/lib/hook-io.mjs +1 -1
- package/plugin/hooks/pre-tool-use.mjs +1 -1
- package/plugin/package-lock.json +2 -2
- package/plugin/package.json +1 -1
- package/scripts/doctor.mjs +2 -2
- package/src/dashboard/server.ts +137 -61
- package/src/sarif/sarif-store.ts +1 -0
package/plugin/eslint.config.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import js from "@eslint/js";
|
|
2
|
+
import globals from "globals";
|
|
2
3
|
|
|
3
4
|
export default [
|
|
4
5
|
js.configs.recommended,
|
|
@@ -12,4 +13,15 @@ export default [
|
|
|
12
13
|
"**/*.min.js",
|
|
13
14
|
],
|
|
14
15
|
},
|
|
16
|
+
{
|
|
17
|
+
languageOptions: {
|
|
18
|
+
globals: globals.node,
|
|
19
|
+
},
|
|
20
|
+
rules: {
|
|
21
|
+
"preserve-caught-error": "warn",
|
|
22
|
+
"no-empty": "warn",
|
|
23
|
+
"no-useless-assignment": "warn",
|
|
24
|
+
"no-unused-vars": ["warn", { argsIgnorePattern: "^_" }],
|
|
25
|
+
},
|
|
26
|
+
},
|
|
15
27
|
];
|
|
@@ -50,7 +50,7 @@ export async function readStdinJson() {
|
|
|
50
50
|
try {
|
|
51
51
|
return JSON.parse(raw);
|
|
52
52
|
} catch (err) {
|
|
53
|
-
throw new Error(`stdin is not valid JSON: ${/** @type {Error} */ (err).message}
|
|
53
|
+
throw new Error(`stdin is not valid JSON: ${/** @type {Error} */ (err).message}`, { cause: err });
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
|
|
@@ -111,7 +111,7 @@ function parseStdinJson(raw) {
|
|
|
111
111
|
try {
|
|
112
112
|
return JSON.parse(raw);
|
|
113
113
|
} catch (err) {
|
|
114
|
-
throw new Error(`stdin is not valid JSON: ${/** @type {Error} */ (err).message}
|
|
114
|
+
throw new Error(`stdin is not valid JSON: ${/** @type {Error} */ (err).message}`, { cause: err });
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
117
|
|
package/plugin/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-crap-plugin",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.5",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "claude-crap-plugin",
|
|
9
|
-
"version": "0.3.
|
|
9
|
+
"version": "0.3.5",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@fastify/static": "^8.0.3",
|
|
12
12
|
"@modelcontextprotocol/sdk": "^1.0.4",
|
package/plugin/package.json
CHANGED
package/scripts/doctor.mjs
CHANGED
|
@@ -152,13 +152,13 @@ async function checkDist(pluginRoot) {
|
|
|
152
152
|
const stat = await fs.stat(npmEntry);
|
|
153
153
|
npmAge = Math.round((Date.now() - stat.mtimeMs) / (1000 * 60 * 60));
|
|
154
154
|
npmOk = true;
|
|
155
|
-
} catch {}
|
|
155
|
+
} catch { /* probe — absence is expected */ }
|
|
156
156
|
|
|
157
157
|
try {
|
|
158
158
|
const stat = await fs.stat(gitEntry);
|
|
159
159
|
gitAge = Math.round((Date.now() - stat.mtimeMs) / (1000 * 60 * 60));
|
|
160
160
|
gitOk = true;
|
|
161
|
-
} catch {}
|
|
161
|
+
} catch { /* probe — absence is expected */ }
|
|
162
162
|
|
|
163
163
|
const details = [];
|
|
164
164
|
if (npmOk) details.push(`dist/index.js (~${npmAge}h)`);
|
package/src/dashboard/server.ts
CHANGED
|
@@ -24,9 +24,8 @@
|
|
|
24
24
|
* @module dashboard/server
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
|
-
import { promises as fs } from "node:fs";
|
|
28
|
-
import {
|
|
29
|
-
import { dirname, resolve } from "node:path";
|
|
27
|
+
import { promises as fs, existsSync, readFileSync, writeFileSync, unlinkSync } from "node:fs";
|
|
28
|
+
import { dirname, join, resolve } from "node:path";
|
|
30
29
|
import { fileURLToPath } from "node:url";
|
|
31
30
|
|
|
32
31
|
import Fastify, { type FastifyInstance } from "fastify";
|
|
@@ -100,7 +99,7 @@ export async function startDashboard(options: StartDashboardOptions): Promise<Da
|
|
|
100
99
|
// ------------------------------------------------------------------
|
|
101
100
|
// /api/health — liveness probe
|
|
102
101
|
// ------------------------------------------------------------------
|
|
103
|
-
fastify.get("/api/health", async () => ({ status: "ok", server: "claude-crap", version: "0.3.
|
|
102
|
+
fastify.get("/api/health", async () => ({ status: "ok", server: "claude-crap", version: "0.3.5" }));
|
|
104
103
|
|
|
105
104
|
// ------------------------------------------------------------------
|
|
106
105
|
// /api/score — live project score
|
|
@@ -126,27 +125,24 @@ export async function startDashboard(options: StartDashboardOptions): Promise<Da
|
|
|
126
125
|
return reply.sendFile("index.html");
|
|
127
126
|
});
|
|
128
127
|
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const url = `http://127.0.0.1:${boundPort}`;
|
|
139
|
-
if (boundPort !== config.dashboardPort) {
|
|
140
|
-
logger.warn(
|
|
141
|
-
{ url, configuredPort: config.dashboardPort, actualPort: boundPort },
|
|
142
|
-
"claude-crap dashboard bound to fallback port (configured port was in use)",
|
|
143
|
-
);
|
|
144
|
-
}
|
|
128
|
+
// Kill any stale dashboard from a previous session so we always
|
|
129
|
+
// bind to the configured port. This mirrors claude-mem's PID file
|
|
130
|
+
// pattern: write a PID file when alive, check + kill on next boot.
|
|
131
|
+
const pidFilePath = resolvePidFilePath(config);
|
|
132
|
+
await killStaleDashboard(pidFilePath, config.dashboardPort, logger);
|
|
133
|
+
|
|
134
|
+
await fastify.listen({ port: config.dashboardPort, host: "127.0.0.1" });
|
|
135
|
+
|
|
136
|
+
const url = `http://127.0.0.1:${config.dashboardPort}`;
|
|
145
137
|
logger.info({ url, publicRoot }, "claude-crap dashboard listening");
|
|
146
138
|
|
|
139
|
+
// Write PID file so the next session can find and kill us.
|
|
140
|
+
writePidFile(pidFilePath, config.dashboardPort);
|
|
141
|
+
|
|
147
142
|
return {
|
|
148
143
|
url,
|
|
149
144
|
async close() {
|
|
145
|
+
removePidFile(pidFilePath);
|
|
150
146
|
await fastify.close();
|
|
151
147
|
},
|
|
152
148
|
};
|
|
@@ -203,54 +199,134 @@ function urlOf(fastify: FastifyInstance, config: CrapConfig): string {
|
|
|
203
199
|
return `http://127.0.0.1:${config.dashboardPort}`;
|
|
204
200
|
}
|
|
205
201
|
|
|
202
|
+
// ------------------------------------------------------------------
|
|
203
|
+
// PID file management — mirrors claude-mem's worker.pid pattern
|
|
204
|
+
// ------------------------------------------------------------------
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Shape of the PID file written by the dashboard process.
|
|
208
|
+
*/
|
|
209
|
+
interface DashboardPidFile {
|
|
210
|
+
pid: number;
|
|
211
|
+
port: number;
|
|
212
|
+
startedAt: string;
|
|
213
|
+
}
|
|
214
|
+
|
|
206
215
|
/**
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
|
|
216
|
+
* Resolve the path to the PID file. Stored under
|
|
217
|
+
* `.claude-crap/dashboard.pid` in the workspace so it survives
|
|
218
|
+
* across sessions but is gitignored with the rest of `.claude-crap/`.
|
|
219
|
+
*/
|
|
220
|
+
function resolvePidFilePath(config: CrapConfig): string {
|
|
221
|
+
return join(config.pluginRoot, ".claude-crap", "dashboard.pid");
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Write the PID file atomically after the dashboard has started.
|
|
226
|
+
*/
|
|
227
|
+
function writePidFile(path: string, port: number): void {
|
|
228
|
+
const data: DashboardPidFile = {
|
|
229
|
+
pid: process.pid,
|
|
230
|
+
port,
|
|
231
|
+
startedAt: new Date().toISOString(),
|
|
232
|
+
};
|
|
233
|
+
try {
|
|
234
|
+
writeFileSync(path, JSON.stringify(data, null, 2) + "\n");
|
|
235
|
+
} catch {
|
|
236
|
+
/* best effort — dashboard still works without a PID file */
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Remove the PID file during graceful shutdown.
|
|
242
|
+
*/
|
|
243
|
+
function removePidFile(path: string): void {
|
|
244
|
+
try {
|
|
245
|
+
unlinkSync(path);
|
|
246
|
+
} catch {
|
|
247
|
+
/* already gone or never written */
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Check whether a process is alive using the signal-0 probe.
|
|
253
|
+
* Returns `true` when the process exists and is reachable.
|
|
254
|
+
*/
|
|
255
|
+
function isPidAlive(pid: number): boolean {
|
|
256
|
+
try {
|
|
257
|
+
process.kill(pid, 0);
|
|
258
|
+
return true;
|
|
259
|
+
} catch {
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Read the PID file, kill any stale dashboard process, and free the
|
|
266
|
+
* port so the current session can bind to it. This is the key
|
|
267
|
+
* difference from the port-fallback approach: instead of drifting to
|
|
268
|
+
* 5118, 5119, etc., we reclaim the configured port every time.
|
|
211
269
|
*
|
|
212
|
-
* @param
|
|
213
|
-
* @param
|
|
214
|
-
* @param logger
|
|
215
|
-
* @returns The first free port found.
|
|
216
|
-
* @throws When all candidate ports are occupied.
|
|
270
|
+
* @param pidFilePath Absolute path to `dashboard.pid`.
|
|
271
|
+
* @param port The configured dashboard port.
|
|
272
|
+
* @param logger Pino logger for diagnostics.
|
|
217
273
|
*/
|
|
218
|
-
async function
|
|
219
|
-
|
|
220
|
-
|
|
274
|
+
async function killStaleDashboard(
|
|
275
|
+
pidFilePath: string,
|
|
276
|
+
port: number,
|
|
221
277
|
logger: Logger,
|
|
222
|
-
): Promise<
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
resolvePromise(false);
|
|
233
|
-
}
|
|
234
|
-
});
|
|
235
|
-
probe.listen({ port: candidatePort, host: "127.0.0.1" }, () => {
|
|
236
|
-
probe.close(() => resolvePromise(true));
|
|
237
|
-
});
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
if (isFree) return candidatePort;
|
|
241
|
-
|
|
242
|
-
if (attempt < maxRetries) {
|
|
243
|
-
logger.info(
|
|
244
|
-
{ port: candidatePort, nextPort: candidatePort + 1 },
|
|
245
|
-
"dashboard port in use, trying next",
|
|
246
|
-
);
|
|
247
|
-
}
|
|
278
|
+
): Promise<void> {
|
|
279
|
+
if (!existsSync(pidFilePath)) return;
|
|
280
|
+
|
|
281
|
+
let stale: DashboardPidFile;
|
|
282
|
+
try {
|
|
283
|
+
stale = JSON.parse(readFileSync(pidFilePath, "utf8"));
|
|
284
|
+
} catch {
|
|
285
|
+
// Corrupted PID file — remove it and move on.
|
|
286
|
+
removePidFile(pidFilePath);
|
|
287
|
+
return;
|
|
248
288
|
}
|
|
249
289
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
290
|
+
if (!isPidAlive(stale.pid)) {
|
|
291
|
+
logger.info({ stalePid: stale.pid }, "stale dashboard PID file found (process dead), removing");
|
|
292
|
+
removePidFile(pidFilePath);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Process is alive — kill it so we can reclaim the port.
|
|
297
|
+
logger.info(
|
|
298
|
+
{ stalePid: stale.pid, port: stale.port, startedAt: stale.startedAt },
|
|
299
|
+
"killing stale dashboard process from previous session",
|
|
253
300
|
);
|
|
301
|
+
|
|
302
|
+
try {
|
|
303
|
+
process.kill(stale.pid, "SIGTERM");
|
|
304
|
+
} catch {
|
|
305
|
+
// Permission denied or already gone — remove PID file either way.
|
|
306
|
+
removePidFile(pidFilePath);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Wait up to 3 seconds for the process to exit.
|
|
311
|
+
for (let i = 0; i < 30; i++) {
|
|
312
|
+
if (!isPidAlive(stale.pid)) break;
|
|
313
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// If still alive after 3s, escalate to SIGKILL.
|
|
317
|
+
if (isPidAlive(stale.pid)) {
|
|
318
|
+
try {
|
|
319
|
+
process.kill(stale.pid, "SIGKILL");
|
|
320
|
+
} catch {
|
|
321
|
+
/* best effort */
|
|
322
|
+
}
|
|
323
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
removePidFile(pidFilePath);
|
|
327
|
+
|
|
328
|
+
// Give the OS a moment to release the TCP port after the process dies.
|
|
329
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
254
330
|
}
|
|
255
331
|
|
|
256
332
|
/**
|