aisnitch 0.1.0 → 0.2.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 CHANGED
@@ -4,144 +4,835 @@
4
4
  [![Node >=20](https://img.shields.io/badge/node-%3E%3D20-339933?logo=node.js&logoColor=white)](https://nodejs.org/)
5
5
  [![License: Apache-2.0](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](./LICENSE)
6
6
 
7
- Universal bridge for AI coding tool activity: capture, normalize, and stream live events from multiple AI coding tools into one local WebSocket feed and one terminal TUI.
7
+ **AISnitch** is a local event bridge for AI coding tools. It captures real-time activity from Claude Code, OpenCode, Gemini CLI, Codex, Goose, Aider, Copilot CLI, OpenClaw (and any CLI via PTY fallback), normalizes everything into one CloudEvents stream, and exposes it over a WebSocket on `ws://127.0.0.1:4820`.
8
+
9
+ You connect your own frontend, dashboard, companion app, notification system, or sound engine to that WebSocket, and you get live sentences like:
10
+
11
+ > *"Session #3 — Claude Code is thinking..."*
12
+ > *"#2 ~/projects/myapp — Codex: Refactoring tables — tool call: Edit file"*
13
+ > *"#5 Claude Code edited 4 JS files"*
14
+
15
+ No data is stored. No cloud. No logs on disk. Pure live memory-only transit.
16
+
17
+ ---
18
+
19
+ ## Table of Contents
20
+
21
+ - [Quick Start](#quick-start)
22
+ - [Install](#install)
23
+ - [Supported Tools](#supported-tools)
24
+ - [How It Works](#how-it-works)
25
+ - [Event Model Reference](#event-model-reference)
26
+ - [Consumer Integration Guide](#consumer-integration-guide)
27
+ - [Connect from Node.js / TypeScript](#connect-from-nodejs--typescript)
28
+ - [Connect from a Browser (React, Vue, Vanilla JS)](#connect-from-a-browser-react-vue-vanilla-js)
29
+ - [Build Human-Readable Status Lines](#build-human-readable-status-lines)
30
+ - [Track Sessions](#track-sessions)
31
+ - [Filter by Tool or Event Type](#filter-by-tool-or-event-type)
32
+ - [Trigger Sounds or Notifications](#trigger-sounds-or-notifications)
33
+ - [Build an Animated Mascot / Companion](#build-an-animated-mascot--companion)
34
+ - [Health Check](#health-check)
35
+ - [CLI Reference](#cli-reference)
36
+ - [TUI Keybinds](#tui-keybinds)
37
+ - [Architecture](#architecture)
38
+ - [Config Reference](#config-reference)
39
+ - [Development](#development)
40
+ - [License](#license)
41
+
42
+ ---
8
43
 
9
- ![AISnitch demo](./docs/assets/aisnitch-demo.gif)
44
+ ## Quick Start
10
45
 
11
- AISnitch is deliberately live-only in the MVP:
46
+ ```bash
47
+ pnpm install && pnpm build
12
48
 
13
- - no persisted event store
14
- - no replay UI
15
- - no cloud dependency
16
- - one in-memory pipeline, one normalized event stream
49
+ # Open the PM2-style dashboard
50
+ node dist/cli/index.js start
17
51
 
18
- ## Quick Start
52
+ # Launch with simulated events (no real AI tool needed)
53
+ node dist/cli/index.js start --mock all
54
+ ```
55
+
56
+ `start` now always opens the TUI dashboard. If the daemon is offline you still land in the UI, see `Daemon not active`, and can start or stop it from inside the TUI with `d`.
57
+
58
+ `start --mock all` boots the dashboard, ensures the daemon is active, then streams realistic fake events from Claude Code, OpenCode, and Gemini CLI so you can see the product immediately.
59
+
60
+ To consume the stream from another terminal:
19
61
 
20
62
  ```bash
21
- pnpm install
22
- pnpm build
23
- node dist/cli/index.js start --mock all
63
+ # Quick test: print raw events
64
+ node -e "
65
+ const WebSocket = require('ws');
66
+ const ws = new WebSocket('ws://127.0.0.1:4820');
67
+ ws.on('message', m => {
68
+ const e = JSON.parse(m.toString());
69
+ if (e.type !== 'welcome') console.log(e.type, e['aisnitch.tool'], e.data?.project);
70
+ });
71
+ "
24
72
  ```
25
73
 
26
- That boots the foreground TUI with realistic fake events so you can inspect the product without installing any AI tool first.
74
+ ---
27
75
 
28
76
  ## Install
29
77
 
30
- ### Local repository mode
78
+ **From source (recommended for development):**
31
79
 
32
80
  ```bash
81
+ git clone https://github.com/vava-nessa/AISnitch.git
82
+ cd AISnitch
33
83
  pnpm install
34
84
  pnpm build
35
85
  node dist/cli/index.js --help
36
86
  ```
37
87
 
38
- ### Global npm install
88
+ **Global npm install:**
39
89
 
40
90
  ```bash
41
91
  npm i -g aisnitch
42
92
  aisnitch --help
43
93
  ```
44
94
 
45
- ### Homebrew
95
+ Global installs now run a silent self-update check every time the dashboard opens. AISnitch auto-detects `npm`, `pnpm`, `bun`, or `brew` from the current install layout and upgrades itself in the background when a newer package version is available.
46
96
 
47
- The repository ships a Homebrew formula at [`Formula/aisnitch.rb`](./Formula/aisnitch.rb). The release workflow updates it from the real npm tarball SHA so it can be copied into a tap repository cleanly.
97
+ **Homebrew:**
48
98
 
49
- ## What It Does
99
+ ```bash
100
+ # Formula ships at Formula/aisnitch.rb — copy into your tap
101
+ brew install aisnitch
102
+ ```
50
103
 
51
- - watches hooks, plugins, transcripts, logs, and process fallbacks
52
- - normalizes everything into one CloudEvents-flavored event schema
53
- - exposes the stream over `ws://127.0.0.1:4820`
54
- - receives hook traffic over `http://127.0.0.1:4821`
55
- - renders a shared Ink TUI for both foreground mode and daemon attach
56
- - preserves raw source payloads in `event.data.raw`
57
- - stays memory-only for privacy and operational simplicity
104
+ ---
58
105
 
59
106
  ## Supported Tools
60
107
 
61
- | Tool | Status | Primary strategy | Setup command |
62
- | --- | --- | --- | --- |
63
- | Claude Code | ✅ Priority | HTTP hooks + JSONL + process fallback | `aisnitch setup claude-code` |
64
- | OpenCode | ✅ Priority | Local plugin + process fallback | `aisnitch setup opencode` |
65
- | Gemini CLI | | Hooks + `logs.json` + process fallback | `aisnitch setup gemini-cli` |
66
- | Codex | ✅ | `codex-tui.log` + process fallback | `aisnitch setup codex` |
67
- | Goose | ✅ | `goosed` polling + SSE + SQLite fallback | `aisnitch setup goose` |
68
- | Copilot CLI | ✅ | Repo hooks + session-state watcher | `aisnitch setup copilot-cli` |
69
- | Aider | ✅ | `.aider.chat.history.md` + notifications command | `aisnitch setup aider` |
70
- | OpenClaw | ✅ | Managed internal hooks + command/memory/session watchers | `aisnitch setup openclaw` |
71
- | Unknown / unsupported CLI | fallback | PTY wrapper heuristics | `aisnitch wrap <command>` |
108
+ | Tool | Strategy | Setup |
109
+ | --- | --- | --- |
110
+ | **Claude Code** | HTTP hooks + JSONL transcript watching + process detection | `aisnitch setup claude-code` |
111
+ | **OpenCode** | Local plugin + process detection | `aisnitch setup opencode` |
112
+ | **Gemini CLI** | Command hooks + `logs.json` watching + process detection | `aisnitch setup gemini-cli` |
113
+ | **Codex** | `codex-tui.log` parsing + process detection | `aisnitch setup codex` |
114
+ | **Goose** | `goosed` API polling + SSE streams + SQLite fallback | `aisnitch setup goose` |
115
+ | **Copilot CLI** | Repo hooks + session-state JSONL watching | `aisnitch setup copilot-cli` |
116
+ | **Aider** | `.aider.chat.history.md` watching + notifications command | `aisnitch setup aider` |
117
+ | **OpenClaw** | Managed hooks + command/memory/session watchers | `aisnitch setup openclaw` |
118
+ | **Any other CLI** | PTY wrapper with output heuristics | `aisnitch wrap <command>` |
119
+
120
+ Adapters are disabled by default. Run `aisnitch setup <tool>` to arm them, then `aisnitch adapters` to verify.
121
+
122
+ ---
123
+
124
+ ## How It Works
125
+
126
+ ```
127
+ Claude Code ──┐
128
+ OpenCode ─────┤
129
+ Gemini CLI ───┤── hooks / file watchers / process detection
130
+ Codex ────────┤
131
+ Goose ────────┤
132
+ Aider ────────┤
133
+ Copilot CLI ──┤
134
+ OpenClaw ─────┘
135
+
136
+
137
+ ┌─────────────────┐
138
+ │ AISnitch Core │
139
+ │ │
140
+ │ Validate (Zod) │
141
+ │ Normalize │
142
+ │ Enrich context │
143
+ │ (terminal, cwd, │
144
+ │ pid, session) │
145
+ └────────┬─────────┘
146
+
147
+ ┌────────┴─────────┐
148
+ ▼ ▼
149
+ ws://127.0.0.1:4820 TUI
150
+ (your consumers) (built-in)
151
+ ```
152
+
153
+ Each adapter captures tool activity using the best available strategy (hooks > file watching > process detection). Events are validated, normalized into CloudEvents, enriched with context (terminal, working directory, PID, multi-instance tracking), then pushed to the EventBus. The WebSocket server broadcasts to all connected clients with per-client backpressure handling.
154
+
155
+ **Nothing is stored on disk.** Events exist in memory during transit, then they're gone.
156
+
157
+ ---
158
+
159
+ ## Event Model Reference
160
+
161
+ Every event is a [CloudEvents v1.0](https://cloudevents.io/) envelope with AISnitch extensions:
162
+
163
+ ```jsonc
164
+ {
165
+ // CloudEvents core
166
+ "specversion": "1.0",
167
+ "id": "019713a4-beef-7000-8000-deadbeef0042", // UUIDv7
168
+ "source": "aisnitch://claude-code/myproject",
169
+ "type": "agent.coding", // one of 12 types
170
+ "time": "2026-03-28T14:30:00.000Z",
171
+
172
+ // AISnitch extensions
173
+ "aisnitch.tool": "claude-code", // which AI tool
174
+ "aisnitch.sessionid": "claude-code:myproject:p12345", // session identity
175
+ "aisnitch.seqnum": 42, // sequence number in this session
176
+
177
+ // Normalized payload
178
+ "data": {
179
+ "state": "agent.coding", // mirrors type
180
+ "project": "myproject", // project name
181
+ "projectPath": "/home/user/myproject",
182
+ "activeFile": "src/index.ts", // file being edited (if relevant)
183
+ "toolName": "Edit", // tool being used (if relevant)
184
+ "toolInput": { // tool arguments (if relevant)
185
+ "filePath": "src/index.ts"
186
+ },
187
+ "model": "claude-sonnet-4-5-20250514", // model in use (if known)
188
+ "tokensUsed": 1500, // token count (if known)
189
+ "terminal": "iTerm2", // detected terminal
190
+ "cwd": "/home/user/myproject", // working directory
191
+ "pid": 12345, // process ID
192
+ "instanceIndex": 1, // instance number (multi-instance)
193
+ "instanceTotal": 3, // total instances running
194
+
195
+ // Error fields (only on agent.error)
196
+ "errorMessage": "Rate limit exceeded",
197
+ "errorType": "rate_limit", // rate_limit | context_overflow | tool_failure | api_error
198
+
199
+ // Raw source payload (adapter-specific, for advanced consumers)
200
+ "raw": { /* original hook/log payload as-is */ }
201
+ }
202
+ }
203
+ ```
204
+
205
+ ### The 12 Event Types
206
+
207
+ | Type | Meaning | When it fires |
208
+ | --- | --- | --- |
209
+ | `session.start` | A tool session began | Tool launched, first hook received |
210
+ | `session.end` | Session closed | Tool exited, process disappeared |
211
+ | `task.start` | User submitted a prompt/task | New user message, task created |
212
+ | `task.complete` | Task finished | Response complete, stop signal |
213
+ | `agent.thinking` | Model is reasoning | Thinking block, internal reflection |
214
+ | `agent.streaming` | Model is generating output | Text streaming, response in progress |
215
+ | `agent.coding` | Model edited files | Write, Edit, MultiEdit tool calls |
216
+ | `agent.tool_call` | Model used a non-edit tool | Search, Bash, Grep, web search, etc. |
217
+ | `agent.asking_user` | Waiting for human input | Permission request, confirmation prompt |
218
+ | `agent.idle` | No activity (timeout) | 120s of silence (configurable) |
219
+ | `agent.error` | Something went wrong | Rate limit, API error, tool failure |
220
+ | `agent.compact` | Context compaction | Memory cleanup, history pruning |
221
+
222
+ ### Recognized Tool Names
223
+
224
+ `claude-code`, `opencode`, `gemini-cli`, `codex`, `goose`, `copilot-cli`, `cursor`, `aider`, `amp`, `cline`, `continue`, `windsurf`, `qwen-code`, `openclaw`, `openhands`, `kilo`, `unknown`
225
+
226
+ ---
227
+
228
+ ## Consumer Integration Guide
229
+
230
+ This is the main purpose of AISnitch: you connect to the WebSocket and exploit the event stream however you want. Below are complete examples for every common use case.
231
+
232
+ ### Connect from Node.js / TypeScript
233
+
234
+ ```ts
235
+ import WebSocket from 'ws';
236
+
237
+ // 📖 Default port is 4820, configurable via ~/.aisnitch/config.json
238
+ const ws = new WebSocket('ws://127.0.0.1:4820');
239
+
240
+ ws.on('open', () => {
241
+ console.log('Connected to AISnitch');
242
+ });
243
+
244
+ ws.on('message', (buffer) => {
245
+ const event = JSON.parse(buffer.toString('utf8'));
246
+
247
+ // 📖 First message is always a welcome payload — skip it
248
+ if (event.type === 'welcome') {
249
+ console.log('AISnitch version:', event.version);
250
+ console.log('Active tools:', event.activeTools);
251
+ return;
252
+ }
253
+
254
+ // 📖 Every other message is a normalized AISnitch event
255
+ console.log({
256
+ type: event.type, // "agent.coding"
257
+ tool: event['aisnitch.tool'], // "claude-code"
258
+ session: event['aisnitch.sessionid'],// "claude-code:myproject:p12345"
259
+ seq: event['aisnitch.seqnum'], // 42
260
+ project: event.data?.project, // "myproject"
261
+ file: event.data?.activeFile, // "src/index.ts"
262
+ model: event.data?.model, // "claude-sonnet-4-5-20250514"
263
+ });
264
+ });
265
+
266
+ ws.on('close', () => {
267
+ console.log('Disconnected — AISnitch stopped or restarted');
268
+ // 📖 Implement reconnect logic here if needed
269
+ });
270
+
271
+ ws.on('error', (err) => {
272
+ console.error('WebSocket error:', err.message);
273
+ });
274
+ ```
275
+
276
+ ### Connect from a Browser (React, Vue, Vanilla JS)
277
+
278
+ The WebSocket is plain `ws://` on localhost — browsers can connect directly with the native `WebSocket` API. No library needed.
279
+
280
+ **Vanilla JS:**
281
+
282
+ ```js
283
+ // 📖 Connect to AISnitch from any browser page on the same machine
284
+ const ws = new WebSocket('ws://127.0.0.1:4820');
285
+
286
+ ws.onmessage = (msg) => {
287
+ const event = JSON.parse(msg.data);
288
+ if (event.type === 'welcome') return;
289
+
290
+ // Do anything: update DOM, play sound, trigger animation...
291
+ document.getElementById('status').textContent =
292
+ `${event['aisnitch.tool']} — ${event.type}`;
293
+ };
294
+ ```
295
+
296
+ **React hook:**
297
+
298
+ ```tsx
299
+ import { useEffect, useState, useCallback, useRef } from 'react';
300
+
301
+ interface AISnitchEvent {
302
+ type: string;
303
+ time: string;
304
+ 'aisnitch.tool': string;
305
+ 'aisnitch.sessionid': string;
306
+ 'aisnitch.seqnum': number;
307
+ data: {
308
+ state: string;
309
+ project?: string;
310
+ activeFile?: string;
311
+ toolName?: string;
312
+ toolInput?: { filePath?: string; command?: string };
313
+ model?: string;
314
+ tokensUsed?: number;
315
+ errorMessage?: string;
316
+ errorType?: string;
317
+ terminal?: string;
318
+ cwd?: string;
319
+ pid?: number;
320
+ instanceIndex?: number;
321
+ instanceTotal?: number;
322
+ };
323
+ }
324
+
325
+ // 📖 Drop-in React hook — auto-reconnects every 3s if AISnitch restarts
326
+ export function useAISnitch(url = 'ws://127.0.0.1:4820') {
327
+ const [events, setEvents] = useState<AISnitchEvent[]>([]);
328
+ const [connected, setConnected] = useState(false);
329
+ const [latestEvent, setLatestEvent] = useState<AISnitchEvent | null>(null);
330
+ const wsRef = useRef<WebSocket | null>(null);
331
+
332
+ const connect = useCallback(() => {
333
+ const ws = new WebSocket(url);
334
+ wsRef.current = ws;
335
+
336
+ ws.onopen = () => setConnected(true);
337
+ ws.onclose = () => {
338
+ setConnected(false);
339
+ setTimeout(connect, 3000); // 📖 Reconnect after 3s
340
+ };
341
+
342
+ ws.onmessage = (msg) => {
343
+ const event = JSON.parse(msg.data) as AISnitchEvent;
344
+ if (event.type === 'welcome') return;
345
+
346
+ setLatestEvent(event);
347
+ setEvents((prev) => [...prev.slice(-499), event]); // 📖 Keep last 500
348
+ };
349
+
350
+ ws.onerror = () => ws.close();
351
+ }, [url]);
352
+
353
+ useEffect(() => {
354
+ connect();
355
+ return () => wsRef.current?.close();
356
+ }, [connect]);
357
+
358
+ const clear = useCallback(() => {
359
+ setEvents([]);
360
+ setLatestEvent(null);
361
+ }, []);
362
+
363
+ return { events, latestEvent, connected, clear };
364
+ }
365
+ ```
366
+
367
+ **Usage in a component:**
368
+
369
+ ```tsx
370
+ function AIActivityPanel() {
371
+ const { events, latestEvent, connected } = useAISnitch();
372
+
373
+ return (
374
+ <div>
375
+ <span>{connected ? '🟢 Live' : '🔴 Disconnected'}</span>
376
+
377
+ {latestEvent && (
378
+ <p>
379
+ {latestEvent['aisnitch.tool']} — {latestEvent.type}
380
+ {latestEvent.data.project && ` on ${latestEvent.data.project}`}
381
+ </p>
382
+ )}
383
+
384
+ <ul>
385
+ {events.map((e, i) => (
386
+ <li key={i}>
387
+ [{e['aisnitch.tool']}] {e.type}
388
+ {e.data.activeFile && ` → ${e.data.activeFile}`}
389
+ </li>
390
+ ))}
391
+ </ul>
392
+ </div>
393
+ );
394
+ }
395
+ ```
396
+
397
+ **Vue 3 composable:**
398
+
399
+ ```ts
400
+ import { ref, onMounted, onUnmounted } from 'vue';
401
+
402
+ export function useAISnitch(url = 'ws://127.0.0.1:4820') {
403
+ const events = ref<any[]>([]);
404
+ const connected = ref(false);
405
+ const latestEvent = ref<any>(null);
406
+ let ws: WebSocket | null = null;
407
+ let reconnectTimer: ReturnType<typeof setTimeout>;
408
+
409
+ function connect() {
410
+ ws = new WebSocket(url);
411
+ ws.onopen = () => (connected.value = true);
412
+ ws.onclose = () => {
413
+ connected.value = false;
414
+ reconnectTimer = setTimeout(connect, 3000);
415
+ };
416
+ ws.onmessage = (msg) => {
417
+ const event = JSON.parse(msg.data);
418
+ if (event.type === 'welcome') return;
419
+ latestEvent.value = event;
420
+ events.value = [...events.value.slice(-499), event];
421
+ };
422
+ ws.onerror = () => ws?.close();
423
+ }
424
+
425
+ onMounted(connect);
426
+ onUnmounted(() => {
427
+ clearTimeout(reconnectTimer);
428
+ ws?.close();
429
+ });
430
+
431
+ return { events, latestEvent, connected };
432
+ }
433
+ ```
72
434
 
73
- ## Core Commands
435
+ ### Build Human-Readable Status Lines
436
+
437
+ This is the core use case: transform raw events into sentences like *"Claude Code is editing src/index.ts"*.
438
+
439
+ ```ts
440
+ // 📖 Maps event type + data into a short human-readable description
441
+ function describeEvent(event: AISnitchEvent): string {
442
+ const tool = event['aisnitch.tool'];
443
+ const d = event.data;
444
+ const file = d.activeFile ? ` → ${d.activeFile}` : '';
445
+ const project = d.project ? ` [${d.project}]` : '';
446
+
447
+ switch (event.type) {
448
+ case 'session.start':
449
+ return `${tool} started a new session${project}`;
450
+
451
+ case 'session.end':
452
+ return `${tool} session ended${project}`;
453
+
454
+ case 'task.start':
455
+ return `${tool} received a new prompt${project}`;
456
+
457
+ case 'task.complete':
458
+ return `${tool} finished the task${project}` +
459
+ (d.duration ? ` (${Math.round(d.duration / 1000)}s)` : '');
460
+
461
+ case 'agent.thinking':
462
+ return `${tool} is thinking...${project}`;
463
+
464
+ case 'agent.streaming':
465
+ return `${tool} is generating a response${project}`;
466
+
467
+ case 'agent.coding':
468
+ return `${tool} is editing code${file}${project}`;
469
+
470
+ case 'agent.tool_call':
471
+ return `${tool} is using ${d.toolName ?? 'a tool'}` +
472
+ (d.toolInput?.command ? `: ${d.toolInput.command}` : file) +
473
+ project;
474
+
475
+ case 'agent.asking_user':
476
+ return `${tool} needs your input${project}`;
477
+
478
+ case 'agent.idle':
479
+ return `${tool} is idle${project}`;
480
+
481
+ case 'agent.error':
482
+ return `${tool} error: ${d.errorMessage ?? d.errorType ?? 'unknown'}${project}`;
483
+
484
+ case 'agent.compact':
485
+ return `${tool} is compacting context${project}`;
486
+
487
+ default:
488
+ return `${tool}: ${event.type}`;
489
+ }
490
+ }
491
+
492
+ // Example output:
493
+ // "claude-code started a new session [myproject]"
494
+ // "claude-code is editing code → src/index.ts [myproject]"
495
+ // "codex is using Bash: npm test [api-server]"
496
+ // "gemini-cli needs your input"
497
+ // "claude-code error: Rate limit exceeded [myproject]"
498
+ ```
499
+
500
+ **Full status line with session number:**
501
+
502
+ ```ts
503
+ // 📖 Tracks session indices and builds numbered status lines
504
+ const sessionIndex = new Map<string, number>();
505
+ let sessionCounter = 0;
506
+
507
+ function getSessionNumber(sessionId: string): number {
508
+ if (!sessionIndex.has(sessionId)) {
509
+ sessionIndex.set(sessionId, ++sessionCounter);
510
+ }
511
+ return sessionIndex.get(sessionId)!;
512
+ }
513
+
514
+ function formatStatusLine(event: AISnitchEvent): string {
515
+ const num = getSessionNumber(event['aisnitch.sessionid']);
516
+ const desc = describeEvent(event);
517
+ const cwd = event.data.cwd ?? '';
518
+ return `#${num} ${cwd} — ${desc}`;
519
+ }
520
+
521
+ // Output:
522
+ // "#1 /home/user/myproject — claude-code is thinking..."
523
+ // "#2 /home/user/api — codex is editing code → src/db.ts"
524
+ // "#1 /home/user/myproject — claude-code finished the task (12s)"
525
+ ```
526
+
527
+ ### Track Sessions
528
+
529
+ Events carry `aisnitch.sessionid` for grouping. A session represents one tool instance working on one project.
530
+
531
+ ```ts
532
+ interface SessionState {
533
+ tool: string;
534
+ sessionId: string;
535
+ project?: string;
536
+ cwd?: string;
537
+ lastEvent: AISnitchEvent;
538
+ lastActivity: string; // human-readable
539
+ eventCount: number;
540
+ startedAt: string;
541
+ }
542
+
543
+ const sessions = new Map<string, SessionState>();
544
+
545
+ function updateSession(event: AISnitchEvent): void {
546
+ const sid = event['aisnitch.sessionid'];
547
+
548
+ if (event.type === 'session.end') {
549
+ sessions.delete(sid);
550
+ return;
551
+ }
552
+
553
+ const existing = sessions.get(sid);
554
+ sessions.set(sid, {
555
+ tool: event['aisnitch.tool'],
556
+ sessionId: sid,
557
+ project: event.data.project ?? existing?.project,
558
+ cwd: event.data.cwd ?? existing?.cwd,
559
+ lastEvent: event,
560
+ lastActivity: describeEvent(event),
561
+ eventCount: (existing?.eventCount ?? 0) + 1,
562
+ startedAt: existing?.startedAt ?? event.time,
563
+ });
564
+ }
565
+
566
+ // 📖 Call updateSession() on every event, then read sessions Map for current state
567
+ // sessions.values() gives you all active sessions across all tools
568
+ ```
569
+
570
+ ### Filter by Tool or Event Type
571
+
572
+ Filtering is client-side. The server broadcasts everything — you pick what you need.
573
+
574
+ ```ts
575
+ // 📖 Filter examples — apply to your ws.onmessage handler
576
+
577
+ // Only Claude Code events
578
+ const isClaudeCode = (e: AISnitchEvent) => e['aisnitch.tool'] === 'claude-code';
579
+
580
+ // Only coding activity (edits, tool calls)
581
+ const isCodingActivity = (e: AISnitchEvent) =>
582
+ e.type === 'agent.coding' || e.type === 'agent.tool_call';
583
+
584
+ // Only errors
585
+ const isError = (e: AISnitchEvent) => e.type === 'agent.error';
586
+
587
+ // Only rate limits specifically
588
+ const isRateLimit = (e: AISnitchEvent) =>
589
+ e.type === 'agent.error' && e.data.errorType === 'rate_limit';
590
+
591
+ // Only events for a specific project
592
+ const isMyProject = (e: AISnitchEvent) => e.data.project === 'myproject';
593
+
594
+ // Events that need user attention
595
+ const needsAttention = (e: AISnitchEvent) =>
596
+ e.type === 'agent.asking_user' || e.type === 'agent.error';
597
+
598
+ // Combine filters
599
+ ws.onmessage = (msg) => {
600
+ const event = JSON.parse(msg.data);
601
+ if (event.type === 'welcome') return;
602
+
603
+ if (isClaudeCode(event) && isCodingActivity(event)) {
604
+ // Only Claude Code writing code
605
+ updateUI(event);
606
+ }
607
+ };
608
+ ```
609
+
610
+ ### Trigger Sounds or Notifications
611
+
612
+ ```ts
613
+ // 📖 Map event types to sounds — perfect for a companion/pet app
614
+ const SOUND_MAP: Record<string, string> = {
615
+ 'session.start': '/sounds/boot.mp3',
616
+ 'session.end': '/sounds/shutdown.mp3',
617
+ 'task.start': '/sounds/ping.mp3',
618
+ 'task.complete': '/sounds/success.mp3',
619
+ 'agent.thinking': '/sounds/thinking-loop.mp3', // loop this one
620
+ 'agent.coding': '/sounds/keyboard.mp3',
621
+ 'agent.tool_call': '/sounds/tool.mp3',
622
+ 'agent.asking_user':'/sounds/alert.mp3',
623
+ 'agent.error': '/sounds/error.mp3',
624
+ 'agent.idle': '/sounds/idle.mp3',
625
+ };
626
+
627
+ function playEventSound(event: AISnitchEvent): void {
628
+ const soundFile = SOUND_MAP[event.type];
629
+ if (!soundFile) return;
630
+
631
+ // Browser: use Web Audio API
632
+ const audio = new Audio(soundFile);
633
+ audio.play();
634
+ }
635
+
636
+ // 📖 Desktop notification for important events
637
+ function notifyIfNeeded(event: AISnitchEvent): void {
638
+ if (event.type === 'agent.asking_user') {
639
+ new Notification(`${event['aisnitch.tool']} needs input`, {
640
+ body: event.data.project
641
+ ? `Project: ${event.data.project}`
642
+ : 'Waiting for your response...',
643
+ });
644
+ }
645
+
646
+ if (event.type === 'agent.error') {
647
+ new Notification(`${event['aisnitch.tool']} error`, {
648
+ body: event.data.errorMessage ?? 'Something went wrong',
649
+ });
650
+ }
651
+
652
+ if (event.type === 'task.complete') {
653
+ new Notification(`${event['aisnitch.tool']} done!`, {
654
+ body: event.data.project
655
+ ? `Task completed on ${event.data.project}`
656
+ : 'Task completed',
657
+ });
658
+ }
659
+ }
660
+ ```
661
+
662
+ ### Build an Animated Mascot / Companion
663
+
664
+ ```ts
665
+ // 📖 Map event states to mascot moods — for animated desktop pets,
666
+ // menu bar companions, or overlay widgets
667
+
668
+ interface MascotState {
669
+ mood: 'idle' | 'thinking' | 'working' | 'waiting' | 'celebrating' | 'panicking';
670
+ animation: string;
671
+ color: string;
672
+ label: string;
673
+ detail?: string;
674
+ }
675
+
676
+ function eventToMascotState(event: AISnitchEvent): MascotState {
677
+ const d = event.data;
678
+
679
+ switch (event.type) {
680
+ case 'agent.thinking':
681
+ return {
682
+ mood: 'thinking',
683
+ animation: 'orbit',
684
+ color: '#f59e0b',
685
+ label: 'Thinking...',
686
+ detail: d.model,
687
+ };
688
+
689
+ case 'agent.coding':
690
+ return {
691
+ mood: 'working',
692
+ animation: 'typing',
693
+ color: '#3b82f6',
694
+ label: 'Coding',
695
+ detail: d.activeFile,
696
+ };
697
+
698
+ case 'agent.tool_call':
699
+ return {
700
+ mood: 'working',
701
+ animation: 'inspect',
702
+ color: '#14b8a6',
703
+ label: `Using ${d.toolName ?? 'tool'}`,
704
+ detail: d.toolInput?.command ?? d.toolInput?.filePath,
705
+ };
706
+
707
+ case 'agent.streaming':
708
+ return {
709
+ mood: 'working',
710
+ animation: 'pulse',
711
+ color: '#8b5cf6',
712
+ label: 'Generating...',
713
+ };
714
+
715
+ case 'agent.asking_user':
716
+ return {
717
+ mood: 'waiting',
718
+ animation: 'wave',
719
+ color: '#ec4899',
720
+ label: 'Needs you!',
721
+ };
722
+
723
+ case 'agent.error':
724
+ return {
725
+ mood: 'panicking',
726
+ animation: 'shake',
727
+ color: '#ef4444',
728
+ label: d.errorType === 'rate_limit' ? 'Rate limited!' : 'Error!',
729
+ detail: d.errorMessage,
730
+ };
731
+
732
+ case 'task.complete':
733
+ return {
734
+ mood: 'celebrating',
735
+ animation: 'celebrate',
736
+ color: '#22c55e',
737
+ label: 'Done!',
738
+ };
739
+
740
+ default:
741
+ return {
742
+ mood: 'idle',
743
+ animation: 'breathe',
744
+ color: '#94a3b8',
745
+ label: 'Idle',
746
+ };
747
+ }
748
+ }
749
+
750
+ // 📖 Plug this into your rendering loop:
751
+ // ws.onmessage → eventToMascotState(event) → update sprite/CSS/canvas
752
+ ```
753
+
754
+ ### Health Check
755
+
756
+ AISnitch exposes a health endpoint on the HTTP port:
74
757
 
75
758
  ```bash
76
- # Start foreground TUI
759
+ curl http://127.0.0.1:4821/health
760
+ ```
761
+
762
+ ```json
763
+ {
764
+ "status": "ok",
765
+ "uptime": 3600,
766
+ "consumers": 2,
767
+ "events": 1542,
768
+ "droppedEvents": 0
769
+ }
770
+ ```
771
+
772
+ Useful for monitoring if the bridge is alive before connecting your consumer.
773
+
774
+ ---
775
+
776
+ ## CLI Reference
777
+
778
+ ```bash
779
+ # Dashboard mode (always opens the TUI)
77
780
  aisnitch start
78
- aisnitch start --tool claude-code
79
- aisnitch start --type agent.coding
80
- aisnitch start --view full-data
781
+ aisnitch start --tool claude-code # pre-filter by tool
782
+ aisnitch start --type agent.coding # pre-filter by event type
783
+ aisnitch start --view full-data # expanded JSON inspector
81
784
 
82
- # Background daemon + attach
785
+ # Background daemon
83
786
  aisnitch start --daemon
84
- aisnitch status
85
- aisnitch attach
86
- aisnitch attach --view full-data
87
- aisnitch stop
787
+ aisnitch status # check if daemon is running
788
+ aisnitch attach # open the same dashboard and attach if active
789
+ aisnitch stop # kill daemon
88
790
 
89
- # Tool setup
791
+ # Tool setup (run once per tool)
90
792
  aisnitch setup claude-code
91
793
  aisnitch setup opencode
92
794
  aisnitch setup gemini-cli
93
- aisnitch setup aider
94
795
  aisnitch setup codex
95
796
  aisnitch setup goose
96
797
  aisnitch setup copilot-cli
798
+ aisnitch setup aider
97
799
  aisnitch setup openclaw
98
- aisnitch setup claude-code --revert
800
+ aisnitch setup claude-code --revert # undo setup
801
+
802
+ # Check enabled adapters
803
+ aisnitch adapters
99
804
 
100
- # Demo / development
805
+ # Demo mode
101
806
  aisnitch mock claude-code --speed 2 --duration 20
102
807
  aisnitch start --mock all --mock-duration 20
103
808
 
104
- # Fallback for tools without a first-class adapter
809
+ # PTY wrapper fallback (any unsupported CLI)
105
810
  aisnitch wrap aider --model sonnet
106
811
  aisnitch wrap goose session
107
812
  ```
108
813
 
109
- ## Setup Notes
110
-
111
- Adapters are disabled by default until you arm them with `setup`. After setup:
112
-
113
- ```bash
114
- aisnitch adapters
115
- ```
116
-
117
- You should see the tool listed as `enabled`. That only means the AISnitch-side bridge is armed. You still need to actually run the tool while AISnitch is running to receive events.
118
-
119
- ## TUI
120
-
121
- The TUI is the main operator surface for both `start` and `attach`.
122
-
123
- It includes:
124
-
125
- - live event feed
126
- - active session panel
127
- - tool / event-type / query filtering
128
- - freeze / clear controls
129
- - a colorful full-data inspector showing normalized JSON plus raw payloads
130
-
131
- ### Keybinds
132
-
133
- - `q` / `Ctrl+C`: quit
134
- - `v`: toggle full-data inspector
135
- - `f`: tool filter picker
136
- - `t`: event-type filter picker
137
- - `/`: free-text search
138
- - `Esc`: clear filters
139
- - `Space`: freeze or resume tailing
140
- - `c`: clear local buffer
141
- - `?`: help overlay
142
- - `Tab`: switch panel focus
143
- - `↑` / `↓` or `j` / `k`: navigate rows / inspector
144
- - `[` / `]`: page inspector up or down
814
+ ---
815
+
816
+ ## TUI Keybinds
817
+
818
+ | Key | Action |
819
+ | --- | --- |
820
+ | `q` / `Ctrl+C` | Quit |
821
+ | `d` | Start / stop the daemon from the dashboard |
822
+ | `r` | Refresh daemon status |
823
+ | `v` | Toggle full-data JSON inspector |
824
+ | `f` | Tool filter picker |
825
+ | `t` | Event type filter picker |
826
+ | `/` | Free-text search |
827
+ | `Esc` | Clear all filters |
828
+ | `Space` | Freeze / resume live tailing |
829
+ | `c` | Clear event buffer |
830
+ | `?` | Help overlay |
831
+ | `Tab` | Switch panel focus |
832
+ | `↑↓` / `jk` | Navigate rows / inspector |
833
+ | `[` `]` | Page inspector up / down |
834
+
835
+ ---
145
836
 
146
837
  ## Architecture
147
838
 
@@ -182,114 +873,48 @@ flowchart LR
182
873
  BUS --> TUI
183
874
  ```
184
875
 
185
- ## Event Model
186
-
187
- AISnitch emits CloudEvents-style envelopes with AISnitch-specific extensions:
188
-
189
- - `specversion`
190
- - `id`
191
- - `source`
192
- - `type`
193
- - `time`
194
- - `aisnitch.tool`
195
- - `aisnitch.sessionid`
196
- - `aisnitch.seqnum`
197
- - `data`
198
-
199
- Important normalized event types:
200
-
201
- - `session.start`
202
- - `session.end`
203
- - `task.start`
204
- - `task.complete`
205
- - `agent.idle`
206
- - `agent.thinking`
207
- - `agent.streaming`
208
- - `agent.tool_call`
209
- - `agent.coding`
210
- - `agent.asking_user`
211
- - `agent.compact`
212
- - `agent.error`
213
-
214
- See [`docs/events-schema.md`](./docs/events-schema.md) for the full contract.
215
-
216
- ## Build a Consumer
217
-
218
- The WebSocket stream is intentionally simple:
219
-
220
- ```ts
221
- import WebSocket from 'ws';
222
-
223
- const ws = new WebSocket('ws://127.0.0.1:4820');
224
-
225
- ws.on('message', (buffer) => {
226
- const event = JSON.parse(buffer.toString('utf8'));
227
- if (event.type === 'welcome') return;
228
- console.log(event.type, event['aisnitch.tool'], event.data);
229
- });
230
- ```
231
-
232
- Working examples:
233
-
234
- - [`examples/basic-consumer.ts`](./examples/basic-consumer.ts)
235
- - [`examples/mascot-consumer.ts`](./examples/mascot-consumer.ts)
876
+ ---
236
877
 
237
878
  ## Config Reference
238
879
 
239
- AISnitch stores local state under `~/.aisnitch/` by default. For isolated tests or sandboxed runs, every CLI command also honors `AISNITCH_HOME`.
880
+ AISnitch state lives under `~/.aisnitch/` by default (override with `AISNITCH_HOME` env var).
240
881
 
241
- Important paths:
882
+ | Path | Purpose |
883
+ | --- | --- |
884
+ | `~/.aisnitch/config.json` | User configuration |
885
+ | `~/.aisnitch/aisnitch.pid` | Daemon PID file |
886
+ | `~/.aisnitch/daemon-state.json` | Daemon connection info |
887
+ | `~/.aisnitch/daemon.log` | Daemon output log (5MB max) |
888
+ | `~/.aisnitch/aisnitch.sock` | Unix domain socket (daemon IPC) |
889
+ | `~/.aisnitch/auto-update.json` | Silent self-update state |
890
+ | `~/.aisnitch/auto-update.log` | Last silent self-update worker log |
242
891
 
243
- - `~/.aisnitch/config.json`
244
- - `~/.aisnitch/aisnitch.pid`
245
- - `~/.aisnitch/daemon-state.json`
246
- - `~/.aisnitch/daemon.log`
247
- - `~/.aisnitch/aisnitch.sock`
892
+ The dashboard surfaces the active WebSocket URL directly in the header so it is easy to copy into another consumer.
248
893
 
249
- Default ports:
894
+ | Port | Purpose |
895
+ | --- | --- |
896
+ | `4820` | WebSocket stream (consumers connect here) |
897
+ | `4821` | HTTP hook receiver + `/health` endpoint |
250
898
 
251
- - WebSocket: `4820`
252
- - HTTP hooks: `4821`
899
+ ---
253
900
 
254
- ## Testing
901
+ ## Development
255
902
 
256
903
  ```bash
257
- pnpm lint
258
- pnpm typecheck
259
- pnpm test
904
+ pnpm install
905
+ pnpm build # ESM + CJS + .d.ts
906
+ pnpm lint # ESLint
907
+ pnpm typecheck # tsc --noEmit
908
+ pnpm test # Vitest (142 tests)
260
909
  pnpm test:coverage
261
- pnpm build
262
- ```
263
-
264
- Real E2E smoke:
265
-
266
- ```bash
267
- # Prereq: opencode installed and one provider authenticated
268
- pnpm test:e2e
910
+ pnpm test:e2e # requires opencode installed
269
911
  ```
270
912
 
271
- The E2E suite uses a dedicated Vitest config so it does not slow down `pnpm test`.
272
-
273
- ## Development Docs
274
-
275
- - [`docs/index.md`](./docs/index.md)
276
- - [`docs/core-pipeline.md`](./docs/core-pipeline.md)
277
- - [`docs/cli-daemon.md`](./docs/cli-daemon.md)
278
- - [`docs/tool-setup.md`](./docs/tool-setup.md)
279
- - [`docs/priority-adapters.md`](./docs/priority-adapters.md)
280
- - [`docs/secondary-adapters.md`](./docs/secondary-adapters.md)
281
- - [`docs/testing.md`](./docs/testing.md)
282
- - [`docs/distribution.md`](./docs/distribution.md)
283
- - [`docs/launch-plan.md`](./docs/launch-plan.md)
284
- - [`tasks/tasks.md`](./tasks/tasks.md)
285
-
286
- ## Contributing
913
+ Detailed docs: [`docs/index.md`](./docs/index.md) | [`tasks/tasks.md`](./tasks/tasks.md)
287
914
 
288
- Read:
915
+ Contributing: [`CONTRIBUTING.md`](./CONTRIBUTING.md) | [`CODE_OF_CONDUCT.md`](./CODE_OF_CONDUCT.md) | [`AGENTS.md`](./AGENTS.md)
289
916
 
290
- - [`CONTRIBUTING.md`](./CONTRIBUTING.md)
291
- - [`CODE_OF_CONDUCT.md`](./CODE_OF_CONDUCT.md)
292
- - [`AGENTS.md`](./AGENTS.md)
917
+ ---
293
918
 
294
919
  ## License
295
920