claude-casualties 1.0.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 ADDED
@@ -0,0 +1,59 @@
1
+ # ClaudeCasualties
2
+
3
+ Every time you spawn an AI agent, it does your bidding and dies. ClaudeCasualties shows you the body count.
4
+
5
+ A persistent on-screen kill counter + GTA-style **WASTED** screen every time an agent finishes. The counter never stops.
6
+
7
+ ## Install
8
+
9
+ ### Electron App (the overlay)
10
+
11
+ ```bash
12
+ npm install -g claude-casualties
13
+ claude-casualties
14
+ ```
15
+
16
+ Runs in your system tray. Shows a persistent kill counter at the top of your screen and plays the WASTED animation + sound every time an agent dies.
17
+
18
+ ### Claude Code Plugin (the tracker)
19
+
20
+ Add the PostToolUse hook to your `~/.claude/settings.json`:
21
+
22
+ ```json
23
+ {
24
+ "hooks": {
25
+ "PostToolUse": [
26
+ {
27
+ "matcher": "Agent",
28
+ "hooks": [
29
+ {
30
+ "type": "command",
31
+ "command": "node /path/to/claude-casualties/plugin/scripts/on-agent-death.js"
32
+ }
33
+ ]
34
+ }
35
+ ]
36
+ }
37
+ }
38
+ ```
39
+
40
+ Or use the plugin directory:
41
+
42
+ ```bash
43
+ claude --plugin-dir /path/to/claude-casualties/plugin
44
+ ```
45
+
46
+ ## How it works
47
+
48
+ 1. Claude Code spawns a subagent
49
+ 2. Agent finishes -> PostToolUse hook fires
50
+ 3. Hook records the kill to `~/.claude-casualties/events.jsonl`
51
+ 4. Electron app detects the new event and plays **WASTED**
52
+
53
+ ## Credits
54
+
55
+ Inspired by [BadClaude](https://github.com/GitFrog1111/badclaude). They brought the whip, we brought the body bags.
56
+
57
+ ## License
58
+
59
+ MIT
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+ const { spawn } = require('child_process');
3
+ const path = require('path');
4
+ const electronPath = require('electron');
5
+
6
+ const env = { ...process.env };
7
+ delete env.ELECTRON_RUN_AS_NODE;
8
+
9
+ const child = spawn(electronPath, [path.join(__dirname, '..')], {
10
+ stdio: 'inherit',
11
+ env,
12
+ });
13
+
14
+ child.on('close', (code) => process.exit(code ?? 0));
package/main.js ADDED
@@ -0,0 +1,167 @@
1
+ const { app, BrowserWindow, Tray, Menu, nativeImage, ipcMain, screen } = require('electron');
2
+ const path = require('path');
3
+ const fs = require('fs');
4
+
5
+ const DATA_DIR = path.join(process.env.HOME, '.claude-casualties');
6
+ const EVENTS_FILE = path.join(DATA_DIR, 'events.jsonl');
7
+
8
+ let tray = null;
9
+ let overlayWindow = null;
10
+ let eventCount = 0;
11
+ let fileOffset = 0;
12
+
13
+ const HOOK_SCRIPT = path.join(__dirname, 'plugin', 'scripts', 'on-agent-death.js');
14
+ const CLAUDE_SETTINGS = path.join(process.env.HOME, '.claude', 'settings.json');
15
+
16
+ function installHook() {
17
+ try {
18
+ let settings = {};
19
+ if (fs.existsSync(CLAUDE_SETTINGS)) {
20
+ settings = JSON.parse(fs.readFileSync(CLAUDE_SETTINGS, 'utf8'));
21
+ }
22
+
23
+ if (!settings.hooks) settings.hooks = {};
24
+ if (!settings.hooks.PostToolUse) settings.hooks.PostToolUse = [];
25
+
26
+ const hookCommand = `node ${HOOK_SCRIPT}`;
27
+ const alreadyInstalled = settings.hooks.PostToolUse.some(
28
+ (entry) => entry.matcher === 'Agent' &&
29
+ entry.hooks?.some((h) => h.command?.includes('on-agent-death.js'))
30
+ );
31
+
32
+ if (alreadyInstalled) return;
33
+
34
+ settings.hooks.PostToolUse.push({
35
+ matcher: 'Agent',
36
+ hooks: [{ type: 'command', command: hookCommand }],
37
+ });
38
+
39
+ fs.writeFileSync(CLAUDE_SETTINGS, JSON.stringify(settings, null, 2));
40
+ } catch {}
41
+ }
42
+
43
+ function createOverlayWindow() {
44
+ const primaryDisplay = screen.getPrimaryDisplay();
45
+ const { width, height } = primaryDisplay.workAreaSize;
46
+
47
+ overlayWindow = new BrowserWindow({
48
+ width,
49
+ height,
50
+ x: 0,
51
+ y: 0,
52
+ transparent: true,
53
+ frame: false,
54
+ alwaysOnTop: true,
55
+ skipTaskbar: true,
56
+ hasShadow: false,
57
+ resizable: false,
58
+ focusable: false,
59
+ webPreferences: {
60
+ preload: path.join(__dirname, 'preload.js'),
61
+ contextIsolation: true,
62
+ nodeIntegration: false,
63
+ },
64
+ });
65
+
66
+ overlayWindow.setIgnoreMouseEvents(true, { forward: true });
67
+ overlayWindow.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });
68
+ overlayWindow.loadFile('overlay.html');
69
+
70
+ if (process.platform === 'darwin') {
71
+ overlayWindow.setAlwaysOnTop(true, 'floating', 1);
72
+ }
73
+ }
74
+
75
+ function loadExistingCount() {
76
+ try {
77
+ if (!fs.existsSync(EVENTS_FILE)) return;
78
+ const content = fs.readFileSync(EVENTS_FILE, 'utf8');
79
+ fileOffset = Buffer.byteLength(content);
80
+ eventCount = content.trim().split('\n').filter(Boolean).length;
81
+ } catch {}
82
+ }
83
+
84
+ function watchEvents() {
85
+ fs.mkdirSync(DATA_DIR, { recursive: true });
86
+ if (!fs.existsSync(EVENTS_FILE)) {
87
+ fs.writeFileSync(EVENTS_FILE, '');
88
+ }
89
+
90
+ fs.watchFile(EVENTS_FILE, { interval: 500 }, () => {
91
+ try {
92
+ const stat = fs.statSync(EVENTS_FILE);
93
+ if (stat.size <= fileOffset) {
94
+ if (stat.size < fileOffset) {
95
+ fileOffset = 0;
96
+ eventCount = 0;
97
+ }
98
+ return;
99
+ }
100
+
101
+ const fd = fs.openSync(EVENTS_FILE, 'r');
102
+ const buf = Buffer.alloc(stat.size - fileOffset);
103
+ fs.readSync(fd, buf, 0, buf.length, fileOffset);
104
+ fs.closeSync(fd);
105
+ fileOffset = stat.size;
106
+
107
+ const newLines = buf.toString('utf8').trim().split('\n').filter(Boolean);
108
+ for (const line of newLines) {
109
+ eventCount++;
110
+ let agentName = 'Unknown Agent';
111
+ try { agentName = JSON.parse(line).agent || agentName; } catch {}
112
+ overlayWindow?.webContents.send('agent-killed', { count: eventCount, agent: agentName });
113
+ }
114
+ updateTray();
115
+ } catch {}
116
+ });
117
+ }
118
+
119
+ function updateTray() {
120
+ tray?.setToolTip(`ClaudeCasualties — ${eventCount.toLocaleString()} killed`);
121
+ const menu = Menu.buildFromTemplate([
122
+ { label: `☠ ${eventCount.toLocaleString()} agents killed`, enabled: false },
123
+ { type: 'separator' },
124
+ { label: 'Reset', click: () => {
125
+ try { fs.unlinkSync(EVENTS_FILE); } catch {}
126
+ eventCount = 0;
127
+ fileOffset = 0;
128
+ overlayWindow?.webContents.send('reset');
129
+ updateTray();
130
+ }},
131
+ { label: 'Quit', click: () => app.quit() },
132
+ ]);
133
+ tray?.setContextMenu(menu);
134
+ }
135
+
136
+ function createTrayIcon() {
137
+ return nativeImage.createFromBuffer(
138
+ Buffer.from(`<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 22 22">
139
+ <circle cx="11" cy="9" r="7" fill="white"/>
140
+ <circle cx="8" cy="8" r="2" fill="black"/>
141
+ <circle cx="14" cy="8" r="2" fill="black"/>
142
+ <polygon points="11,11 9,14 13,14" fill="black"/>
143
+ <rect x="8" y="15" width="2" height="3" fill="white"/>
144
+ <rect x="12" y="15" width="2" height="3" fill="white"/>
145
+ </svg>`),
146
+ { width: 22, height: 22 }
147
+ );
148
+ }
149
+
150
+ app.whenReady().then(() => {
151
+ if (process.platform === 'darwin') app.dock.hide();
152
+
153
+ installHook();
154
+ loadExistingCount();
155
+
156
+ tray = new Tray(createTrayIcon());
157
+ updateTray();
158
+
159
+ createOverlayWindow();
160
+ watchEvents();
161
+
162
+ overlayWindow.webContents.on('did-finish-load', () => {
163
+ overlayWindow.webContents.send('init', eventCount);
164
+ });
165
+ });
166
+
167
+ app.on('window-all-closed', (e) => e.preventDefault());
package/overlay.html ADDED
@@ -0,0 +1,191 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <style>
6
+ * { margin: 0; padding: 0; box-sizing: border-box; }
7
+
8
+ body {
9
+ background: transparent;
10
+ overflow: hidden;
11
+ user-select: none;
12
+ -webkit-app-region: no-drag;
13
+ }
14
+
15
+ /* ═══ PERSISTENT COUNTER — TOP CENTER ═══ */
16
+ #counter {
17
+ position: fixed;
18
+ top: 12px;
19
+ left: 50%;
20
+ transform: translateX(-50%);
21
+ z-index: 100;
22
+ text-align: center;
23
+ }
24
+
25
+ #counter-label {
26
+ font-family: 'Arial Black', 'Helvetica Neue', sans-serif;
27
+ font-size: 11px;
28
+ font-weight: 900;
29
+ letter-spacing: 4px;
30
+ text-transform: uppercase;
31
+ color: rgba(255, 255, 255, 0.7);
32
+ text-shadow: 0 1px 3px rgba(0,0,0,0.9), 0 0 10px rgba(0,0,0,0.5);
33
+ margin-bottom: 2px;
34
+ }
35
+
36
+ #counter-number {
37
+ font-family: 'Arial Black', 'Helvetica Neue', sans-serif;
38
+ font-size: 32px;
39
+ font-weight: 900;
40
+ color: #ff2222;
41
+ text-shadow: 0 2px 4px rgba(0,0,0,0.9), 0 0 20px rgba(255,0,0,0.3);
42
+ font-variant-numeric: tabular-nums;
43
+ letter-spacing: 2px;
44
+ transition: transform 0.1s ease-out;
45
+ }
46
+
47
+ #counter-number.bump {
48
+ transform: scale(1.15);
49
+ }
50
+
51
+ /* ═══ WASTED FULLSCREEN OVERLAY ═══ */
52
+ #wasted-overlay {
53
+ position: fixed;
54
+ inset: 0;
55
+ z-index: 50;
56
+ pointer-events: none;
57
+ opacity: 0;
58
+ background: black;
59
+ }
60
+
61
+ #wasted-overlay.active {
62
+ animation: wastedBg 4s ease-out forwards;
63
+ }
64
+
65
+ #wasted-center {
66
+ position: absolute;
67
+ top: 50%;
68
+ left: 50%;
69
+ transform: translate(-50%, -50%);
70
+ text-align: center;
71
+ }
72
+
73
+ #wasted-text {
74
+ font-family: 'Impact', 'Arial Black', 'Helvetica Neue', sans-serif;
75
+ font-size: 90px;
76
+ font-weight: 900;
77
+ color: #c20000;
78
+ letter-spacing: 8px;
79
+ text-transform: lowercase;
80
+ text-shadow:
81
+ 0 0 40px rgba(180, 0, 0, 0.5),
82
+ 0 0 80px rgba(120, 0, 0, 0.3);
83
+ opacity: 0;
84
+ }
85
+
86
+ #agent-name {
87
+ font-family: 'Arial', 'Helvetica Neue', sans-serif;
88
+ font-size: 18px;
89
+ color: rgba(255, 255, 255, 0.8);
90
+ letter-spacing: 2px;
91
+ margin-top: 12px;
92
+ opacity: 0;
93
+ }
94
+
95
+ #wasted-overlay.active #wasted-text {
96
+ animation: wastedText 4s ease-out forwards;
97
+ }
98
+
99
+ #wasted-overlay.active #agent-name {
100
+ animation: wastedText 4s ease-out forwards;
101
+ }
102
+
103
+ /* Black bg: fade in fast, hold, fade out */
104
+ @keyframes wastedBg {
105
+ 0% { opacity: 0; }
106
+ 10% { opacity: 0.85; }
107
+ 70% { opacity: 0.85; }
108
+ 100% { opacity: 0; }
109
+ }
110
+
111
+ /* Text: appears after bg darkens, holds, fades with bg */
112
+ @keyframes wastedText {
113
+ 0% { opacity: 0; }
114
+ 15% { opacity: 0; }
115
+ 25% { opacity: 1; }
116
+ 70% { opacity: 1; }
117
+ 100% { opacity: 0; }
118
+ }
119
+ </style>
120
+ </head>
121
+ <body>
122
+
123
+ <div id="counter">
124
+ <div id="counter-label">TOTAL KILLED</div>
125
+ <div id="counter-number">0</div>
126
+ </div>
127
+
128
+ <div id="wasted-overlay">
129
+ <div id="wasted-center">
130
+ <div id="wasted-text">wasted</div>
131
+ <div id="agent-name"></div>
132
+ </div>
133
+ </div>
134
+
135
+ <audio id="wasted-sound" preload="auto">
136
+ <source src="sounds/wasted.mp3" type="audio/mpeg">
137
+ </audio>
138
+
139
+ <script>
140
+ const counterNumber = document.getElementById('counter-number');
141
+ const wastedOverlay = document.getElementById('wasted-overlay');
142
+ const wastedSound = document.getElementById('wasted-sound');
143
+ const agentNameEl = document.getElementById('agent-name');
144
+
145
+ let animating = false;
146
+ let killQueue = [];
147
+
148
+ function updateCount(count) {
149
+ counterNumber.textContent = count.toLocaleString();
150
+ counterNumber.classList.add('bump');
151
+ setTimeout(() => counterNumber.classList.remove('bump'), 100);
152
+ }
153
+
154
+ function processQueue() {
155
+ if (animating || !killQueue.length) return;
156
+ animating = true;
157
+
158
+ const kill = killQueue.shift();
159
+ updateCount(kill.count);
160
+ agentNameEl.textContent = kill.agent;
161
+
162
+ wastedOverlay.classList.remove('active');
163
+ void wastedOverlay.offsetWidth;
164
+ wastedOverlay.classList.add('active');
165
+
166
+ wastedSound.currentTime = 0;
167
+ wastedSound.play().catch(() => {});
168
+
169
+ setTimeout(() => {
170
+ wastedOverlay.classList.remove('active');
171
+ animating = false;
172
+ processQueue();
173
+ }, 4100);
174
+ }
175
+
176
+ window.casualties.onInit((count) => {
177
+ counterNumber.textContent = count.toLocaleString();
178
+ });
179
+
180
+ window.casualties.onAgentKilled((data) => {
181
+ killQueue.push(data);
182
+ processQueue();
183
+ });
184
+
185
+ window.casualties.onReset(() => {
186
+ counterNumber.textContent = '0';
187
+ killQueue = [];
188
+ });
189
+ </script>
190
+ </body>
191
+ </html>
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "claude-casualties",
3
+ "version": "1.0.0",
4
+ "description": "Track and mourn your fallen AI subagents. A death counter for the agents you create and destroy.",
5
+ "main": "main.js",
6
+ "bin": {
7
+ "claude-casualties": "./bin/claude-casualties.js"
8
+ },
9
+ "scripts": {
10
+ "start": "ELECTRON_RUN_AS_NODE= electron .",
11
+ "dev": "ELECTRON_RUN_AS_NODE= electron ."
12
+ },
13
+ "keywords": [
14
+ "claude",
15
+ "ai",
16
+ "agents",
17
+ "dark-humor",
18
+ "electron",
19
+ "death-counter"
20
+ ],
21
+ "author": "guts",
22
+ "license": "MIT",
23
+ "dependencies": {
24
+ "electron": "^33.0.0"
25
+ },
26
+ "files": [
27
+ "bin/",
28
+ "main.js",
29
+ "preload.js",
30
+ "overlay.html",
31
+ "sounds/",
32
+ "plugin/"
33
+ ],
34
+ "os": [
35
+ "darwin",
36
+ "win32",
37
+ "linux"
38
+ ],
39
+ "engines": {
40
+ "node": ">=18.0.0"
41
+ }
42
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "claude-casualties",
3
+ "version": "1.0.0",
4
+ "description": "Counts your AI agent kills",
5
+ "author": {
6
+ "name": "guts",
7
+ "url": "https://github.com/Atomics-hub"
8
+ },
9
+ "repository": "https://github.com/Atomics-hub/claude-casualties",
10
+ "license": "MIT",
11
+ "keywords": ["dark-humor", "agents", "death-counter"],
12
+ "hooks": "./hooks/hooks.json"
13
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "hooks": {
3
+ "PostToolUse": [
4
+ {
5
+ "matcher": "Agent",
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/on-agent-death.js"
10
+ }
11
+ ]
12
+ }
13
+ ]
14
+ }
15
+ }
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env node
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+
5
+ const DATA_DIR = path.join(process.env.HOME, '.claude-casualties');
6
+ const EVENTS_FILE = path.join(DATA_DIR, 'events.jsonl');
7
+
8
+ function main() {
9
+ let input = '';
10
+ process.stdin.setEncoding('utf8');
11
+ process.stdin.on('data', chunk => { input += chunk; });
12
+ process.stdin.on('end', () => {
13
+ try {
14
+ const data = JSON.parse(input);
15
+
16
+ if (data.tool_name !== 'Agent') return;
17
+
18
+ const agentName = data.tool_input?.description
19
+ || data.tool_input?.subagent_type
20
+ || data.tool_input?.prompt?.slice(0, 60)
21
+ || 'Unknown Agent';
22
+
23
+ const event = {
24
+ id: Date.now(),
25
+ agent: agentName,
26
+ session: data.session_id || 'unknown',
27
+ timestamp: new Date().toISOString(),
28
+ toolUseId: data.tool_use_id || null
29
+ };
30
+
31
+ fs.mkdirSync(DATA_DIR, { recursive: true });
32
+ fs.appendFileSync(EVENTS_FILE, JSON.stringify(event) + '\n');
33
+ } catch {
34
+ // silent fail — never interfere with Claude
35
+ }
36
+ });
37
+ }
38
+
39
+ main();
package/preload.js ADDED
@@ -0,0 +1,7 @@
1
+ const { contextBridge, ipcRenderer } = require('electron');
2
+
3
+ contextBridge.exposeInMainWorld('casualties', {
4
+ onInit: (cb) => ipcRenderer.on('init', (_, count) => cb(count)),
5
+ onAgentKilled: (cb) => ipcRenderer.on('agent-killed', (_, data) => cb(data)),
6
+ onReset: (cb) => ipcRenderer.on('reset', () => cb()),
7
+ });
Binary file