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 +804 -179
- package/dist/cli/index.cjs +678 -114
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +671 -107
- package/dist/cli/index.js.map +1 -1
- package/dist/index.cjs +262 -27
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +91 -6
- package/dist/index.d.ts +91 -6
- package/dist/index.js +258 -27
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -4,144 +4,835 @@
|
|
|
4
4
|
[](https://nodejs.org/)
|
|
5
5
|
[](./LICENSE)
|
|
6
6
|
|
|
7
|
-
|
|
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
|
-
|
|
44
|
+
## Quick Start
|
|
10
45
|
|
|
11
|
-
|
|
46
|
+
```bash
|
|
47
|
+
pnpm install && pnpm build
|
|
12
48
|
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
74
|
+
---
|
|
27
75
|
|
|
28
76
|
## Install
|
|
29
77
|
|
|
30
|
-
|
|
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
|
-
|
|
88
|
+
**Global npm install:**
|
|
39
89
|
|
|
40
90
|
```bash
|
|
41
91
|
npm i -g aisnitch
|
|
42
92
|
aisnitch --help
|
|
43
93
|
```
|
|
44
94
|
|
|
45
|
-
|
|
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
|
-
|
|
97
|
+
**Homebrew:**
|
|
48
98
|
|
|
49
|
-
|
|
99
|
+
```bash
|
|
100
|
+
# Formula ships at Formula/aisnitch.rb — copy into your tap
|
|
101
|
+
brew install aisnitch
|
|
102
|
+
```
|
|
50
103
|
|
|
51
|
-
|
|
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 |
|
|
62
|
-
| --- | --- | --- |
|
|
63
|
-
| Claude Code |
|
|
64
|
-
| OpenCode |
|
|
65
|
-
| Gemini CLI |
|
|
66
|
-
| Codex |
|
|
67
|
-
| Goose |
|
|
68
|
-
| Copilot CLI |
|
|
69
|
-
| Aider |
|
|
70
|
-
| OpenClaw |
|
|
71
|
-
|
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
785
|
+
# Background daemon
|
|
83
786
|
aisnitch start --daemon
|
|
84
|
-
aisnitch status
|
|
85
|
-
aisnitch attach
|
|
86
|
-
aisnitch
|
|
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
|
|
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
|
-
#
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
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
|
|
880
|
+
AISnitch state lives under `~/.aisnitch/` by default (override with `AISNITCH_HOME` env var).
|
|
240
881
|
|
|
241
|
-
|
|
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
|
-
|
|
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
|
-
|
|
894
|
+
| Port | Purpose |
|
|
895
|
+
| --- | --- |
|
|
896
|
+
| `4820` | WebSocket stream (consumers connect here) |
|
|
897
|
+
| `4821` | HTTP hook receiver + `/health` endpoint |
|
|
250
898
|
|
|
251
|
-
|
|
252
|
-
- HTTP hooks: `4821`
|
|
899
|
+
---
|
|
253
900
|
|
|
254
|
-
##
|
|
901
|
+
## Development
|
|
255
902
|
|
|
256
903
|
```bash
|
|
257
|
-
pnpm
|
|
258
|
-
pnpm
|
|
259
|
-
pnpm
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
915
|
+
Contributing: [`CONTRIBUTING.md`](./CONTRIBUTING.md) | [`CODE_OF_CONDUCT.md`](./CODE_OF_CONDUCT.md) | [`AGENTS.md`](./AGENTS.md)
|
|
289
916
|
|
|
290
|
-
|
|
291
|
-
- [`CODE_OF_CONDUCT.md`](./CODE_OF_CONDUCT.md)
|
|
292
|
-
- [`AGENTS.md`](./AGENTS.md)
|
|
917
|
+
---
|
|
293
918
|
|
|
294
919
|
## License
|
|
295
920
|
|