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 +59 -0
- package/bin/claude-casualties.js +14 -0
- package/main.js +167 -0
- package/overlay.html +191 -0
- package/package.json +42 -0
- package/plugin/.claude-plugin/plugin.json +13 -0
- package/plugin/hooks/hooks.json +15 -0
- package/plugin/scripts/on-agent-death.js +39 -0
- package/preload.js +7 -0
- package/sounds/wasted.mp3 +0 -0
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,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
|