claude-crap 0.3.3 → 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 +36 -0
- package/README.md +116 -472
- package/dist/dashboard/server.d.ts.map +1 -1
- package/dist/dashboard/server.js +116 -3
- 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 +6 -2
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.mcp.json +2 -3
- package/plugin/bundle/launcher.mjs +112 -0
- package/plugin/bundle/mcp-server.mjs +120 -49
- package/plugin/bundle/mcp-server.mjs.map +3 -3
- package/plugin/eslint.config.mjs +27 -0
- package/plugin/hooks/lib/hook-io.mjs +1 -1
- package/plugin/hooks/pre-tool-use.mjs +1 -1
- package/plugin/launcher.mjs +112 -0
- package/plugin/package-lock.json +2714 -0
- package/plugin/package.json +5 -1
- package/scripts/bundle-plugin.mjs +30 -0
- package/scripts/doctor.mjs +2 -2
- package/src/dashboard/server.ts +144 -3
- package/src/sarif/sarif-store.ts +1 -0
package/plugin/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-crap-plugin",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.5",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "Runtime dependencies for the claude-crap plugin bundle",
|
|
6
6
|
"type": "module",
|
|
@@ -14,5 +14,9 @@
|
|
|
14
14
|
},
|
|
15
15
|
"engines": {
|
|
16
16
|
"node": ">=20.0.0"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@eslint/js": "^10.0.1",
|
|
20
|
+
"eslint": "^10.2.0"
|
|
17
21
|
}
|
|
18
22
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// scripts/bundle-plugin.mjs
|
|
2
2
|
import { build } from "esbuild";
|
|
3
3
|
import { cp, mkdir, rm } from "node:fs/promises";
|
|
4
|
+
import { execFileSync } from "node:child_process";
|
|
4
5
|
import { resolve } from "node:path";
|
|
5
6
|
import { fileURLToPath } from "node:url";
|
|
6
7
|
|
|
@@ -66,6 +67,35 @@ async function main() {
|
|
|
66
67
|
resolve(BUNDLE_DIR, "dashboard/public"),
|
|
67
68
|
{ recursive: true },
|
|
68
69
|
);
|
|
70
|
+
|
|
71
|
+
// 4. Copy the launcher wrapper into the bundle directory.
|
|
72
|
+
// launcher.mjs is the .mcp.json entry point and is hand-written
|
|
73
|
+
// (not bundled by esbuild). It ensures node_modules/ exist before
|
|
74
|
+
// dynamically importing the real MCP server.
|
|
75
|
+
await cp(
|
|
76
|
+
resolve(ROOT, "plugin/launcher.mjs"),
|
|
77
|
+
resolve(BUNDLE_DIR, "launcher.mjs"),
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
// 5. Generate plugin/package-lock.json for deterministic installs.
|
|
81
|
+
// This lockfile ships with the plugin so that the launcher's
|
|
82
|
+
// `npm install --omit=dev` resolves the exact same versions on
|
|
83
|
+
// every developer's machine. `--package-lock-only` does NOT
|
|
84
|
+
// create node_modules/, keeping the repo light.
|
|
85
|
+
try {
|
|
86
|
+
execFileSync("npm", [
|
|
87
|
+
"install",
|
|
88
|
+
"--package-lock-only",
|
|
89
|
+
"--omit=dev",
|
|
90
|
+
], {
|
|
91
|
+
cwd: resolve(ROOT, "plugin"),
|
|
92
|
+
stdio: ["ignore", "inherit", "inherit"],
|
|
93
|
+
});
|
|
94
|
+
} catch (err) {
|
|
95
|
+
process.stderr.write(
|
|
96
|
+
`warning: could not generate plugin/package-lock.json: ${err.message}\n`,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
69
99
|
}
|
|
70
100
|
|
|
71
101
|
main().catch((err) => {
|
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,8 +24,8 @@
|
|
|
24
24
|
* @module dashboard/server
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
|
-
import { promises as fs } from "node:fs";
|
|
28
|
-
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";
|
|
29
29
|
import { fileURLToPath } from "node:url";
|
|
30
30
|
|
|
31
31
|
import Fastify, { type FastifyInstance } from "fastify";
|
|
@@ -99,7 +99,7 @@ export async function startDashboard(options: StartDashboardOptions): Promise<Da
|
|
|
99
99
|
// ------------------------------------------------------------------
|
|
100
100
|
// /api/health — liveness probe
|
|
101
101
|
// ------------------------------------------------------------------
|
|
102
|
-
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" }));
|
|
103
103
|
|
|
104
104
|
// ------------------------------------------------------------------
|
|
105
105
|
// /api/score — live project score
|
|
@@ -125,13 +125,24 @@ export async function startDashboard(options: StartDashboardOptions): Promise<Da
|
|
|
125
125
|
return reply.sendFile("index.html");
|
|
126
126
|
});
|
|
127
127
|
|
|
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
|
+
|
|
128
134
|
await fastify.listen({ port: config.dashboardPort, host: "127.0.0.1" });
|
|
135
|
+
|
|
129
136
|
const url = `http://127.0.0.1:${config.dashboardPort}`;
|
|
130
137
|
logger.info({ url, publicRoot }, "claude-crap dashboard listening");
|
|
131
138
|
|
|
139
|
+
// Write PID file so the next session can find and kill us.
|
|
140
|
+
writePidFile(pidFilePath, config.dashboardPort);
|
|
141
|
+
|
|
132
142
|
return {
|
|
133
143
|
url,
|
|
134
144
|
async close() {
|
|
145
|
+
removePidFile(pidFilePath);
|
|
135
146
|
await fastify.close();
|
|
136
147
|
},
|
|
137
148
|
};
|
|
@@ -188,6 +199,136 @@ function urlOf(fastify: FastifyInstance, config: CrapConfig): string {
|
|
|
188
199
|
return `http://127.0.0.1:${config.dashboardPort}`;
|
|
189
200
|
}
|
|
190
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
|
+
|
|
215
|
+
/**
|
|
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.
|
|
269
|
+
*
|
|
270
|
+
* @param pidFilePath Absolute path to `dashboard.pid`.
|
|
271
|
+
* @param port The configured dashboard port.
|
|
272
|
+
* @param logger Pino logger for diagnostics.
|
|
273
|
+
*/
|
|
274
|
+
async function killStaleDashboard(
|
|
275
|
+
pidFilePath: string,
|
|
276
|
+
port: number,
|
|
277
|
+
logger: Logger,
|
|
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;
|
|
288
|
+
}
|
|
289
|
+
|
|
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",
|
|
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));
|
|
330
|
+
}
|
|
331
|
+
|
|
191
332
|
/**
|
|
192
333
|
* Wrap {@link computeProjectScore} so the dashboard endpoint can call
|
|
193
334
|
* it with the live store and provide consistent location metadata.
|