claude-live 0.4.8 → 1.1.1
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/bin/claude-live.js +36 -20
- package/package.json +13 -26
- package/.claude-plugin/hooks/hooks.json +0 -126
- package/.claude-plugin/marketplace.json +0 -27
- package/.claude-plugin/plugin.json +0 -14
- package/README.md +0 -60
- package/bin/check-and-restart.js +0 -63
- package/bin/send-hook.sh +0 -34
- package/bin/start-server.sh +0 -33
- package/client/dist/assets/index-B1BUdq7a.js +0 -47
- package/client/dist/assets/index-DjcKbX6b.css +0 -1
- package/client/dist/chords/chord_01.wav +0 -0
- package/client/dist/chords/chord_02.wav +0 -0
- package/client/dist/chords/chord_03.wav +0 -0
- package/client/dist/chords/chord_04.wav +0 -0
- package/client/dist/chords/chord_05.wav +0 -0
- package/client/dist/chords/chord_06.wav +0 -0
- package/client/dist/chords/chord_07.wav +0 -0
- package/client/dist/chords/chord_08.wav +0 -0
- package/client/dist/chords/chord_09.wav +0 -0
- package/client/dist/chords/chord_10.wav +0 -0
- package/client/dist/chords/chord_11.wav +0 -0
- package/client/dist/chords/chord_12.wav +0 -0
- package/client/dist/chords/chord_13.wav +0 -0
- package/client/dist/chords/chord_14.wav +0 -0
- package/client/dist/chords/chord_15.wav +0 -0
- package/client/dist/chords/chord_16.wav +0 -0
- package/client/dist/index.html +0 -18
- package/client/public/chords/chord_01.wav +0 -0
- package/client/public/chords/chord_02.wav +0 -0
- package/client/public/chords/chord_03.wav +0 -0
- package/client/public/chords/chord_04.wav +0 -0
- package/client/public/chords/chord_05.wav +0 -0
- package/client/public/chords/chord_06.wav +0 -0
- package/client/public/chords/chord_07.wav +0 -0
- package/client/public/chords/chord_08.wav +0 -0
- package/client/public/chords/chord_09.wav +0 -0
- package/client/public/chords/chord_10.wav +0 -0
- package/client/public/chords/chord_11.wav +0 -0
- package/client/public/chords/chord_12.wav +0 -0
- package/client/public/chords/chord_13.wav +0 -0
- package/client/public/chords/chord_14.wav +0 -0
- package/client/public/chords/chord_15.wav +0 -0
- package/client/public/chords/chord_16.wav +0 -0
- package/commands/claude-live.md +0 -145
- package/hooks/hooks.json +0 -166
- package/server/index.js +0 -288
- package/test-agent-animations-long.js +0 -144
- package/test-agent-animations.js +0 -126
- package/test-agents.js +0 -61
- package/tests/server.test.js +0 -48
- package/tests/store.test.ts +0 -141
- package/vitest.config.ts +0 -10
package/server/index.js
DELETED
|
@@ -1,288 +0,0 @@
|
|
|
1
|
-
import express from 'express'
|
|
2
|
-
import { createHash, randomUUID } from 'crypto'
|
|
3
|
-
import { createServer as createHttpServer } from 'http'
|
|
4
|
-
import { fileURLToPath } from 'url'
|
|
5
|
-
import { join, dirname } from 'path'
|
|
6
|
-
|
|
7
|
-
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
8
|
-
const DIST_DIR = join(__dirname, '../client/dist')
|
|
9
|
-
const EVENTS_PER_SESSION = 50 // rolling buffer per session
|
|
10
|
-
const SESSION_TIMEOUT_MS = 11 * 60 * 1000 // 11 minutes (server-side cleanup)
|
|
11
|
-
const HEARTBEAT_MS = 15000
|
|
12
|
-
|
|
13
|
-
// ── Ported from client constants/nodeKeys (JS) ──────────────────────────────
|
|
14
|
-
|
|
15
|
-
const TOOL_COLOR_HEX = {
|
|
16
|
-
Read: '#4ade80', Edit: '#60a5fa', Write: '#60a5fa',
|
|
17
|
-
Bash: '#f59e0b', Grep: '#a78bfa', Glob: '#a78bfa',
|
|
18
|
-
WebFetch: '#f472b6', Stop: '#888888', Notification: '#34d399',
|
|
19
|
-
}
|
|
20
|
-
const DEFAULT_HEX = '#555555'
|
|
21
|
-
const FILE_TOOLS = new Set(['Read', 'Edit', 'Write', 'Glob', 'Grep'])
|
|
22
|
-
|
|
23
|
-
function nodeKeyFor(event) {
|
|
24
|
-
const t = event.tool_name
|
|
25
|
-
if (!t) {
|
|
26
|
-
if (event.hook_event_name === 'Stop') return 'session:stop'
|
|
27
|
-
if (event.hook_event_name === 'Notification') {
|
|
28
|
-
const msg = event.tool_input?.message || ''
|
|
29
|
-
return `notification:${msg.slice(0, 20)}`
|
|
30
|
-
}
|
|
31
|
-
return null
|
|
32
|
-
}
|
|
33
|
-
const input = event.tool_input || {}
|
|
34
|
-
if (FILE_TOOLS.has(t)) {
|
|
35
|
-
const fp = input.file_path || input.path || null
|
|
36
|
-
return fp ? `file:${fp}` : null
|
|
37
|
-
}
|
|
38
|
-
if (t === 'Bash') return `bash:${input.command || ''}`
|
|
39
|
-
if (t === 'WebFetch') {
|
|
40
|
-
try { return `web:${new URL(input.url || '').hostname}` } catch { return 'web:unknown' }
|
|
41
|
-
}
|
|
42
|
-
return `tool:${t}`
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function nodeTypeFor(event) {
|
|
46
|
-
const t = event.tool_name
|
|
47
|
-
if (FILE_TOOLS.has(t || '')) return 'file'
|
|
48
|
-
if (t === 'Bash') return 'bash'
|
|
49
|
-
if (t === 'WebFetch') return 'web'
|
|
50
|
-
if (event.hook_event_name === 'Stop') return 'stop'
|
|
51
|
-
if (event.hook_event_name === 'Notification') return 'notification'
|
|
52
|
-
return 'tool'
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function labelFor(event) {
|
|
56
|
-
const t = event.tool_name
|
|
57
|
-
const input = event.tool_input || {}
|
|
58
|
-
if (FILE_TOOLS.has(t || '')) {
|
|
59
|
-
const fp = input.file_path || input.path || ''
|
|
60
|
-
return fp.split('/').pop() || fp
|
|
61
|
-
}
|
|
62
|
-
if (t === 'Bash') return `$ ${(input.command || '').slice(0, 22)}`
|
|
63
|
-
if (t === 'WebFetch') { try { return `↗ ${new URL(input.url || '').hostname}` } catch { return '↗ web' } }
|
|
64
|
-
if (event.hook_event_name === 'Stop') return '✓ done'
|
|
65
|
-
if (event.hook_event_name === 'Notification') return (input.message || 'notification').slice(0, 24)
|
|
66
|
-
return t || event.hook_event_name || '?'
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// ── State snapshot computation ───────────────────────────────────────────────
|
|
70
|
-
|
|
71
|
-
// Compute a lightweight state snapshot from a session's event buffer.
|
|
72
|
-
// Returns null if the session has no events.
|
|
73
|
-
function computeSessionSnapshot(session_id, events) {
|
|
74
|
-
if (events.length === 0) return null
|
|
75
|
-
|
|
76
|
-
let label = null
|
|
77
|
-
let cwd = null
|
|
78
|
-
let stopping = false
|
|
79
|
-
let eventCount = events.length
|
|
80
|
-
|
|
81
|
-
// file nodes: persistent, grow with each touch
|
|
82
|
-
const fileNodes = new Map() // key → { key, nodeType, label, colorHex, baseRadius }
|
|
83
|
-
// ephemeral keys seen in recent N events (they decay fast so only show recent ones)
|
|
84
|
-
const RECENT_N = 15
|
|
85
|
-
const recentEphemeralKeys = new Set()
|
|
86
|
-
|
|
87
|
-
for (let i = 0; i < events.length; i++) {
|
|
88
|
-
const event = events[i]
|
|
89
|
-
|
|
90
|
-
if (event.cwd) cwd = event.cwd
|
|
91
|
-
if (cwd && (!label || label.length <= 8)) {
|
|
92
|
-
const parts = cwd.split('/').filter(Boolean)
|
|
93
|
-
label = parts[parts.length - 1] || null
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
if (event.hook_event_name === 'Stop') {
|
|
97
|
-
stopping = true
|
|
98
|
-
} else if (event.hook_event_name !== 'SessionEnd') {
|
|
99
|
-
stopping = false
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const key = nodeKeyFor(event)
|
|
103
|
-
if (!key) continue
|
|
104
|
-
const type = nodeTypeFor(event)
|
|
105
|
-
const isFile = type === 'file'
|
|
106
|
-
const colorHex = TOOL_COLOR_HEX[event.tool_name || event.hook_event_name] ?? DEFAULT_HEX
|
|
107
|
-
|
|
108
|
-
if (isFile) {
|
|
109
|
-
if (fileNodes.has(key)) {
|
|
110
|
-
fileNodes.get(key).baseRadius = Math.min(8, fileNodes.get(key).baseRadius + 0.3)
|
|
111
|
-
} else {
|
|
112
|
-
fileNodes.set(key, { key, nodeType: type, label: labelFor(event), colorHex, baseRadius: 2.5 })
|
|
113
|
-
}
|
|
114
|
-
} else if (i >= events.length - RECENT_N) {
|
|
115
|
-
recentEphemeralKeys.add(key)
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// Build node list: files first, then recent ephemerals
|
|
120
|
-
const nodes = [...fileNodes.values()]
|
|
121
|
-
|
|
122
|
-
// Add recent ephemerals (not already in files)
|
|
123
|
-
const fileKeySet = new Set(fileNodes.keys())
|
|
124
|
-
for (let i = Math.max(0, events.length - RECENT_N); i < events.length; i++) {
|
|
125
|
-
const event = events[i]
|
|
126
|
-
const key = nodeKeyFor(event)
|
|
127
|
-
if (!key || fileKeySet.has(key)) continue
|
|
128
|
-
const type = nodeTypeFor(event)
|
|
129
|
-
const colorHex = TOOL_COLOR_HEX[event.tool_name || event.hook_event_name] ?? DEFAULT_HEX
|
|
130
|
-
if (!nodes.find(n => n.key === key)) {
|
|
131
|
-
nodes.push({ key, nodeType: type, label: labelFor(event), colorHex, baseRadius: 4 })
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Ring assignment is handled by the client (server just sends node data)
|
|
136
|
-
return {
|
|
137
|
-
session_id,
|
|
138
|
-
label: label || session_id.slice(0, 8),
|
|
139
|
-
cwd,
|
|
140
|
-
stopping,
|
|
141
|
-
eventCount,
|
|
142
|
-
nodes, // [{ key, nodeType, label, colorHex, baseRadius }]
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// ── Server ───────────────────────────────────────────────────────────────────
|
|
147
|
-
|
|
148
|
-
function makeSessionId(ip, ts) {
|
|
149
|
-
return 'unknown-' + createHash('sha1').update(ip + ts).digest('hex').slice(0, 8)
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function normalizeEvent(raw, remoteIp) {
|
|
153
|
-
const session_id = raw.session_id?.trim() || makeSessionId(remoteIp, Date.now().toString())
|
|
154
|
-
return {
|
|
155
|
-
id: randomUUID(),
|
|
156
|
-
session_id,
|
|
157
|
-
timestamp: Date.now(),
|
|
158
|
-
hook_event_name: raw.hook_event_name ?? null,
|
|
159
|
-
tool_name: raw.tool_name ?? null,
|
|
160
|
-
tool_input: raw.tool_input ?? null,
|
|
161
|
-
tool_response: raw.tool_response ?? null,
|
|
162
|
-
agent_id: raw.agent_id ?? null,
|
|
163
|
-
agent_type: raw.agent_type ?? null,
|
|
164
|
-
cwd: raw.cwd ?? null,
|
|
165
|
-
error: raw.error ?? null,
|
|
166
|
-
tool_use_id: raw.tool_use_id ?? null,
|
|
167
|
-
prompt: raw.prompt ?? null,
|
|
168
|
-
model: raw.model ?? null,
|
|
169
|
-
source: raw.source ?? null,
|
|
170
|
-
reason: raw.reason ?? null,
|
|
171
|
-
permission_mode: raw.permission_mode ?? null,
|
|
172
|
-
is_interrupt: raw.is_interrupt ?? null,
|
|
173
|
-
trigger: raw.trigger ?? null,
|
|
174
|
-
compact_summary: raw.compact_summary ?? null,
|
|
175
|
-
last_assistant_message: raw.last_assistant_message ?? null,
|
|
176
|
-
notification_type: raw.notification_type ?? null,
|
|
177
|
-
title: raw.title ?? null,
|
|
178
|
-
agent_transcript_path: raw.agent_transcript_path ?? null,
|
|
179
|
-
file_path_loaded: raw.file_path ?? null,
|
|
180
|
-
memory_type: raw.memory_type ?? null,
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
export function createServer({ port = 43451 } = {}) {
|
|
185
|
-
const app = express()
|
|
186
|
-
const clients = new Set()
|
|
187
|
-
const sessionBuffers = new Map() // session_id -> { events: [], lastEventTime: number }
|
|
188
|
-
|
|
189
|
-
function getOrCreateSession(sessionId) {
|
|
190
|
-
if (!sessionBuffers.has(sessionId)) {
|
|
191
|
-
sessionBuffers.set(sessionId, { events: [], lastEventTime: Date.now() })
|
|
192
|
-
}
|
|
193
|
-
return sessionBuffers.get(sessionId)
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function cleanupStaleSessions() {
|
|
197
|
-
const now = Date.now()
|
|
198
|
-
for (const [sid, session] of sessionBuffers) {
|
|
199
|
-
if (now - session.lastEventTime > SESSION_TIMEOUT_MS) {
|
|
200
|
-
sessionBuffers.delete(sid)
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
app.use(express.json())
|
|
206
|
-
|
|
207
|
-
app.post('/hook', (req, res) => {
|
|
208
|
-
const raw = req.body
|
|
209
|
-
if (!raw || typeof raw !== 'object') return res.status(400).json({ error: 'invalid json' })
|
|
210
|
-
const event = normalizeEvent(raw, req.ip)
|
|
211
|
-
const session = getOrCreateSession(event.session_id)
|
|
212
|
-
session.events.push(event)
|
|
213
|
-
session.lastEventTime = Date.now()
|
|
214
|
-
if (session.events.length > EVENTS_PER_SESSION) session.events.shift()
|
|
215
|
-
|
|
216
|
-
const data = `data: ${JSON.stringify(event)}\n\n`
|
|
217
|
-
for (const client of clients) {
|
|
218
|
-
try { client.write(data) } catch { clients.delete(client) }
|
|
219
|
-
}
|
|
220
|
-
res.json({ ok: true })
|
|
221
|
-
})
|
|
222
|
-
|
|
223
|
-
app.get('/events', (req, res) => {
|
|
224
|
-
res.setHeader('Content-Type', 'text/event-stream')
|
|
225
|
-
res.setHeader('Cache-Control', 'no-cache')
|
|
226
|
-
res.setHeader('Connection', 'keep-alive')
|
|
227
|
-
res.flushHeaders()
|
|
228
|
-
|
|
229
|
-
// Send state snapshot instead of replaying raw events
|
|
230
|
-
const sessions = []
|
|
231
|
-
for (const [sid, session] of sessionBuffers) {
|
|
232
|
-
const snap = computeSessionSnapshot(sid, session.events)
|
|
233
|
-
if (snap) sessions.push(snap)
|
|
234
|
-
}
|
|
235
|
-
res.write(`data: ${JSON.stringify({ type: 'state_snapshot', sessions })}\n\n`)
|
|
236
|
-
|
|
237
|
-
clients.add(res)
|
|
238
|
-
const heartbeat = setInterval(() => {
|
|
239
|
-
try { res.write(': heartbeat\n\n') } catch { clients.delete(res); clearInterval(heartbeat) }
|
|
240
|
-
}, HEARTBEAT_MS)
|
|
241
|
-
req.on('close', () => { clients.delete(res); clearInterval(heartbeat) })
|
|
242
|
-
res.on('error', () => { clients.delete(res); clearInterval(heartbeat) })
|
|
243
|
-
})
|
|
244
|
-
|
|
245
|
-
// expose session buffers for tests
|
|
246
|
-
app.get('/buffer', (req, res) => {
|
|
247
|
-
const allEvents = []
|
|
248
|
-
for (const session of sessionBuffers.values()) {
|
|
249
|
-
allEvents.push(...session.events)
|
|
250
|
-
}
|
|
251
|
-
res.json(allEvents)
|
|
252
|
-
})
|
|
253
|
-
|
|
254
|
-
// cleanup stale sessions periodically
|
|
255
|
-
setInterval(cleanupStaleSessions, 60000)
|
|
256
|
-
|
|
257
|
-
// serve static build
|
|
258
|
-
app.use(express.static(DIST_DIR))
|
|
259
|
-
|
|
260
|
-
return new Promise((resolve, reject) => {
|
|
261
|
-
const httpServer = createHttpServer(app)
|
|
262
|
-
httpServer.listen(port, () => {
|
|
263
|
-
const actualPort = httpServer.address().port
|
|
264
|
-
resolve({ server: httpServer, port: actualPort, app })
|
|
265
|
-
})
|
|
266
|
-
httpServer.on('error', reject)
|
|
267
|
-
})
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// Run standalone
|
|
271
|
-
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
272
|
-
const desiredPort = parseInt(process.env.PORT || '43451', 10)
|
|
273
|
-
let p = desiredPort
|
|
274
|
-
const tryStart = async () => {
|
|
275
|
-
try {
|
|
276
|
-
const { port } = await createServer({ port: p })
|
|
277
|
-
console.log(`claude-live server listening on http://localhost:${port}`)
|
|
278
|
-
if (p !== desiredPort) {
|
|
279
|
-
console.log(`(port ${desiredPort} was in use, using ${port} instead)`)
|
|
280
|
-
console.log(`Update your hook URL to: http://localhost:${port}/hook`)
|
|
281
|
-
}
|
|
282
|
-
} catch (e) {
|
|
283
|
-
if (e.code === 'EADDRINUSE') { p++; tryStart() }
|
|
284
|
-
else { console.error(e); process.exit(1) }
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
tryStart()
|
|
288
|
-
}
|
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Extended test with more agents and longer animation sequence
|
|
3
|
-
|
|
4
|
-
import http from 'http';
|
|
5
|
-
import { URL } from 'url';
|
|
6
|
-
|
|
7
|
-
const SERVER = 'http://localhost:43453';
|
|
8
|
-
const SESSION_ID = 'test-session-' + Date.now();
|
|
9
|
-
|
|
10
|
-
function sendHook(eventName, data = {}) {
|
|
11
|
-
return new Promise((resolve, reject) => {
|
|
12
|
-
const payload = JSON.stringify({
|
|
13
|
-
hook_event_name: eventName,
|
|
14
|
-
session_id: SESSION_ID,
|
|
15
|
-
timestamp: Date.now(),
|
|
16
|
-
...data
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
const url = new URL('/hook', SERVER);
|
|
20
|
-
const options = {
|
|
21
|
-
hostname: url.hostname,
|
|
22
|
-
port: url.port || 80,
|
|
23
|
-
path: url.pathname,
|
|
24
|
-
method: 'POST',
|
|
25
|
-
headers: {
|
|
26
|
-
'Content-Type': 'application/json',
|
|
27
|
-
'Content-Length': Buffer.byteLength(payload)
|
|
28
|
-
}
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
const req = http.request(options, (res) => {
|
|
32
|
-
resolve(`${eventName}: ${res.statusCode}`);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
req.on('error', reject);
|
|
36
|
-
req.write(payload);
|
|
37
|
-
req.end();
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
async function wait(ms) {
|
|
42
|
-
return new Promise(r => setTimeout(r, ms));
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
async function agentAction(agentId, tool, filePath, delay = 800) {
|
|
46
|
-
await sendHook('PreToolUse', {
|
|
47
|
-
tool_name: tool,
|
|
48
|
-
tool_input: { file_path: filePath, pattern: filePath },
|
|
49
|
-
agent_id: agentId
|
|
50
|
-
});
|
|
51
|
-
await wait(delay);
|
|
52
|
-
await sendHook('PostToolUse', {
|
|
53
|
-
tool_name: tool,
|
|
54
|
-
tool_input: { file_path: filePath, pattern: filePath },
|
|
55
|
-
agent_id: agentId
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
async function testAgentAnimationsLong() {
|
|
60
|
-
console.log('🚀 Extended Agent Animation Test (4 agents, many actions)\n');
|
|
61
|
-
|
|
62
|
-
try {
|
|
63
|
-
// Spawn 4 agents
|
|
64
|
-
console.log('📍 Spawning 4 agents...');
|
|
65
|
-
for (let i = 1; i <= 4; i++) {
|
|
66
|
-
await sendHook('SubagentStart', {
|
|
67
|
-
agent_id: `agent-${i}`,
|
|
68
|
-
agent_type: 'claude-opus-4-6'
|
|
69
|
-
});
|
|
70
|
-
console.log(` Agent ${i} spawned`);
|
|
71
|
-
await wait(300);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
await wait(1000);
|
|
75
|
-
|
|
76
|
-
// Agent 1: Multiple reads
|
|
77
|
-
console.log('\n📖 Agent 1: Multiple read operations...');
|
|
78
|
-
await agentAction('agent-1', 'Read', 'client/src/store.ts', 1000);
|
|
79
|
-
await wait(500);
|
|
80
|
-
await agentAction('agent-1', 'Read', 'client/src/types.ts', 1000);
|
|
81
|
-
await wait(500);
|
|
82
|
-
await agentAction('agent-1', 'Read', 'README.md', 1000);
|
|
83
|
-
|
|
84
|
-
// Agent 2: Grep operations
|
|
85
|
-
console.log('\n🔎 Agent 2: Search operations...');
|
|
86
|
-
await agentAction('agent-2', 'Grep', 'agentPositionMap', 1000);
|
|
87
|
-
await wait(500);
|
|
88
|
-
await agentAction('agent-2', 'Grep', 'getAnimationOrigin', 1000);
|
|
89
|
-
|
|
90
|
-
// Agent 3: Glob operations
|
|
91
|
-
console.log('\n📂 Agent 3: File matching operations...');
|
|
92
|
-
await agentAction('agent-3', 'Glob', 'client/src/**/*.tsx', 1200);
|
|
93
|
-
await wait(600);
|
|
94
|
-
await agentAction('agent-3', 'Glob', '**/*.ts', 1200);
|
|
95
|
-
|
|
96
|
-
// Agent 4: Mixed operations
|
|
97
|
-
console.log('\n🔄 Agent 4: Mixed operations...');
|
|
98
|
-
await agentAction('agent-4', 'Read', 'package.json', 1000);
|
|
99
|
-
await wait(500);
|
|
100
|
-
await agentAction('agent-4', 'Grep', 'vite', 1000);
|
|
101
|
-
|
|
102
|
-
await wait(1000);
|
|
103
|
-
|
|
104
|
-
// More concurrent-looking operations
|
|
105
|
-
console.log('\n⚡ Agents 1 & 2: Concurrent operations...');
|
|
106
|
-
await Promise.all([
|
|
107
|
-
agentAction('agent-1', 'Read', 'client/src/canvas/renderer.ts', 1200),
|
|
108
|
-
agentAction('agent-2', 'Grep', 'projectile', 1200)
|
|
109
|
-
]);
|
|
110
|
-
|
|
111
|
-
await wait(800);
|
|
112
|
-
|
|
113
|
-
// Agent 3 continues
|
|
114
|
-
console.log('\n📂 Agent 3: More operations...');
|
|
115
|
-
await agentAction('agent-3', 'Glob', 'docs/**/*.md', 1200);
|
|
116
|
-
|
|
117
|
-
await wait(1000);
|
|
118
|
-
|
|
119
|
-
// Final operations before termination
|
|
120
|
-
console.log('\n🎬 Final actions before termination...');
|
|
121
|
-
await agentAction('agent-1', 'Read', 'client/src/canvas/graph.ts', 1000);
|
|
122
|
-
await wait(400);
|
|
123
|
-
await agentAction('agent-4', 'Grep', 'animation', 1000);
|
|
124
|
-
await wait(400);
|
|
125
|
-
await agentAction('agent-2', 'Read', '.gitignore', 1000);
|
|
126
|
-
|
|
127
|
-
await wait(1500);
|
|
128
|
-
|
|
129
|
-
// Terminate agents one by one
|
|
130
|
-
console.log('\n🛑 Terminating agents...');
|
|
131
|
-
for (let i = 1; i <= 4; i++) {
|
|
132
|
-
await sendHook('SubagentStop', { agent_id: `agent-${i}` });
|
|
133
|
-
console.log(` Agent ${i} terminated (star fading out)`);
|
|
134
|
-
await wait(600);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
console.log('\n✅ Extended test complete! All agents have terminated.\n');
|
|
138
|
-
} catch (err) {
|
|
139
|
-
console.error('❌ Error:', err.message);
|
|
140
|
-
process.exit(1);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
testAgentAnimationsLong();
|
package/test-agent-animations.js
DELETED
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Test script to trigger agent animation routing by sending hooks to claude-live server
|
|
3
|
-
|
|
4
|
-
import http from 'http';
|
|
5
|
-
import { URL } from 'url';
|
|
6
|
-
|
|
7
|
-
const SERVER = 'http://localhost:43453';
|
|
8
|
-
const SESSION_ID = 'test-session-' + Date.now();
|
|
9
|
-
|
|
10
|
-
function sendHook(eventName, data = {}) {
|
|
11
|
-
return new Promise((resolve, reject) => {
|
|
12
|
-
const payload = JSON.stringify({
|
|
13
|
-
hook_event_name: eventName,
|
|
14
|
-
session_id: SESSION_ID,
|
|
15
|
-
timestamp: Date.now(),
|
|
16
|
-
...data
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
const url = new URL('/hook', SERVER);
|
|
20
|
-
const options = {
|
|
21
|
-
hostname: url.hostname,
|
|
22
|
-
port: url.port || 80,
|
|
23
|
-
path: url.pathname,
|
|
24
|
-
method: 'POST',
|
|
25
|
-
headers: {
|
|
26
|
-
'Content-Type': 'application/json',
|
|
27
|
-
'Content-Length': Buffer.byteLength(payload)
|
|
28
|
-
}
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
const req = http.request(options, (res) => {
|
|
32
|
-
resolve(`${eventName}: ${res.statusCode}`);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
req.on('error', reject);
|
|
36
|
-
req.write(payload);
|
|
37
|
-
req.end();
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
async function testAgentAnimations() {
|
|
42
|
-
console.log('🚀 Testing Agent Animation Routing...\n');
|
|
43
|
-
|
|
44
|
-
try {
|
|
45
|
-
// Agent 1: Spawn and read
|
|
46
|
-
console.log('📍 Agent 1: Spawning...');
|
|
47
|
-
await sendHook('SubagentStart', {
|
|
48
|
-
agent_id: 'agent-1',
|
|
49
|
-
agent_type: 'claude-opus-4-6'
|
|
50
|
-
});
|
|
51
|
-
await new Promise(r => setTimeout(r, 500));
|
|
52
|
-
|
|
53
|
-
console.log('📖 Agent 1: Reading file...');
|
|
54
|
-
await sendHook('PreToolUse', {
|
|
55
|
-
tool_name: 'Read',
|
|
56
|
-
tool_input: { file_path: 'client/src/store.ts' },
|
|
57
|
-
agent_id: 'agent-1'
|
|
58
|
-
});
|
|
59
|
-
await new Promise(r => setTimeout(r, 300));
|
|
60
|
-
|
|
61
|
-
await sendHook('PostToolUse', {
|
|
62
|
-
tool_name: 'Read',
|
|
63
|
-
tool_input: { file_path: 'client/src/store.ts' },
|
|
64
|
-
agent_id: 'agent-1'
|
|
65
|
-
});
|
|
66
|
-
await new Promise(r => setTimeout(r, 500));
|
|
67
|
-
|
|
68
|
-
// Agent 2: Spawn and read
|
|
69
|
-
console.log('📍 Agent 2: Spawning...');
|
|
70
|
-
await sendHook('SubagentStart', {
|
|
71
|
-
agent_id: 'agent-2',
|
|
72
|
-
agent_type: 'claude-opus-4-6'
|
|
73
|
-
});
|
|
74
|
-
await new Promise(r => setTimeout(r, 500));
|
|
75
|
-
|
|
76
|
-
console.log('📖 Agent 2: Reading file...');
|
|
77
|
-
await sendHook('PreToolUse', {
|
|
78
|
-
tool_name: 'Read',
|
|
79
|
-
tool_input: { file_path: 'README.md' },
|
|
80
|
-
agent_id: 'agent-2'
|
|
81
|
-
});
|
|
82
|
-
await new Promise(r => setTimeout(r, 300));
|
|
83
|
-
|
|
84
|
-
await sendHook('PostToolUse', {
|
|
85
|
-
tool_name: 'Read',
|
|
86
|
-
tool_input: { file_path: 'README.md' },
|
|
87
|
-
agent_id: 'agent-2'
|
|
88
|
-
});
|
|
89
|
-
await new Promise(r => setTimeout(r, 500));
|
|
90
|
-
|
|
91
|
-
// Agent 1: Glob operation
|
|
92
|
-
console.log('🔍 Agent 1: Globbing...');
|
|
93
|
-
await sendHook('PreToolUse', {
|
|
94
|
-
tool_name: 'Glob',
|
|
95
|
-
tool_input: { pattern: 'client/src/**/*.tsx' },
|
|
96
|
-
agent_id: 'agent-1'
|
|
97
|
-
});
|
|
98
|
-
await new Promise(r => setTimeout(r, 300));
|
|
99
|
-
|
|
100
|
-
await sendHook('PostToolUse', {
|
|
101
|
-
tool_name: 'Glob',
|
|
102
|
-
tool_input: { pattern: 'client/src/**/*.tsx' },
|
|
103
|
-
agent_id: 'agent-1'
|
|
104
|
-
});
|
|
105
|
-
await new Promise(r => setTimeout(r, 500));
|
|
106
|
-
|
|
107
|
-
// Terminate agents
|
|
108
|
-
console.log('🛑 Agent 1: Terminating...');
|
|
109
|
-
await sendHook('SubagentStop', {
|
|
110
|
-
agent_id: 'agent-1'
|
|
111
|
-
});
|
|
112
|
-
await new Promise(r => setTimeout(r, 300));
|
|
113
|
-
|
|
114
|
-
console.log('🛑 Agent 2: Terminating...');
|
|
115
|
-
await sendHook('SubagentStop', {
|
|
116
|
-
agent_id: 'agent-2'
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
console.log('\n✅ Test complete! Check http://localhost:43451 for animations.\n');
|
|
120
|
-
} catch (err) {
|
|
121
|
-
console.error('❌ Error:', err.message);
|
|
122
|
-
process.exit(1);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
testAgentAnimations();
|
package/test-agents.js
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
|
-
import path from "path";
|
|
3
|
-
import { fileURLToPath } from "url";
|
|
4
|
-
|
|
5
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
-
|
|
7
|
-
// Get all files in current directory
|
|
8
|
-
function getAllFiles(dir = ".") {
|
|
9
|
-
try {
|
|
10
|
-
return fs
|
|
11
|
-
.readdirSync(dir)
|
|
12
|
-
.filter((f) => fs.statSync(path.join(dir, f)).isFile())
|
|
13
|
-
.map((f) => path.join(dir, f));
|
|
14
|
-
} catch {
|
|
15
|
-
return [];
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
// Pick random items from array
|
|
20
|
-
function pickRandom(arr, count) {
|
|
21
|
-
const shuffled = [...arr].sort(() => Math.random() - 0.5);
|
|
22
|
-
return shuffled.slice(0, Math.min(count, arr.length));
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// Agent function
|
|
26
|
-
async function agent(id, interval = 2000) {
|
|
27
|
-
console.log(`[Agent ${id}] Started`);
|
|
28
|
-
|
|
29
|
-
const tick = () => {
|
|
30
|
-
const allFiles = getAllFiles();
|
|
31
|
-
const randomCount = Math.floor(Math.random() * 3) + 1; // 1-3 files
|
|
32
|
-
const randomFiles = pickRandom(allFiles, randomCount);
|
|
33
|
-
|
|
34
|
-
console.log(
|
|
35
|
-
`[Agent ${id}] Processing ${randomFiles.length} files:`,
|
|
36
|
-
randomFiles.join(", ")
|
|
37
|
-
);
|
|
38
|
-
|
|
39
|
-
// Simulate work
|
|
40
|
-
randomFiles.forEach((file) => {
|
|
41
|
-
try {
|
|
42
|
-
const stats = fs.statSync(file);
|
|
43
|
-
console.log(` ├─ ${file}: ${stats.size} bytes`);
|
|
44
|
-
} catch {
|
|
45
|
-
console.log(` ├─ ${file}: (error reading)`);
|
|
46
|
-
}
|
|
47
|
-
});
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
// Run immediately and then every interval
|
|
51
|
-
tick();
|
|
52
|
-
setInterval(tick, interval);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Start 3 agents
|
|
56
|
-
console.log("Starting 3 test agents...\n");
|
|
57
|
-
agent(1);
|
|
58
|
-
agent(2);
|
|
59
|
-
agent(3);
|
|
60
|
-
|
|
61
|
-
console.log("\nPress Ctrl+C to stop\n");
|
package/tests/server.test.js
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
// tests/server.test.js
|
|
2
|
-
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
|
|
3
|
-
import { createServer } from '../server/index.js'
|
|
4
|
-
|
|
5
|
-
describe('SSE server', () => {
|
|
6
|
-
let server, port
|
|
7
|
-
|
|
8
|
-
beforeAll(async () => {
|
|
9
|
-
({ server, port } = await createServer({ port: 0 })) // port 0 = OS assigns
|
|
10
|
-
})
|
|
11
|
-
|
|
12
|
-
afterAll(() => server.close())
|
|
13
|
-
|
|
14
|
-
it('returns 200 on POST /hook with valid JSON', async () => {
|
|
15
|
-
const res = await fetch(`http://localhost:${port}/hook`, {
|
|
16
|
-
method: 'POST',
|
|
17
|
-
headers: { 'Content-Type': 'application/json' },
|
|
18
|
-
body: JSON.stringify({
|
|
19
|
-
hook_event_name: 'PreToolUse',
|
|
20
|
-
tool_name: 'Read',
|
|
21
|
-
tool_input: { file_path: '/src/foo.ts' },
|
|
22
|
-
session_id: 'test-session'
|
|
23
|
-
})
|
|
24
|
-
})
|
|
25
|
-
expect(res.status).toBe(200)
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
it('normalizes missing session_id to fallback', async () => {
|
|
29
|
-
const res = await fetch(`http://localhost:${port}/hook`, {
|
|
30
|
-
method: 'POST',
|
|
31
|
-
headers: { 'Content-Type': 'application/json' },
|
|
32
|
-
body: JSON.stringify({ hook_event_name: 'Stop' })
|
|
33
|
-
})
|
|
34
|
-
expect(res.status).toBe(200)
|
|
35
|
-
// buffer should have an event with a non-empty session_id
|
|
36
|
-
const events = await fetch(`http://localhost:${port}/buffer`).then(r => r.json())
|
|
37
|
-
expect(events[events.length - 1].session_id).toBeTruthy()
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
it('returns 400 on invalid JSON', async () => {
|
|
41
|
-
const res = await fetch(`http://localhost:${port}/hook`, {
|
|
42
|
-
method: 'POST',
|
|
43
|
-
headers: { 'Content-Type': 'application/json' },
|
|
44
|
-
body: 'not json'
|
|
45
|
-
})
|
|
46
|
-
expect(res.status).toBe(400)
|
|
47
|
-
})
|
|
48
|
-
})
|