aisnitch 0.2.2 → 0.2.4
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 +314 -666
- package/dist/cli/index.cjs +431 -63
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +431 -63
- package/dist/cli/index.js.map +1 -1
- package/dist/index.cjs +199 -74
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +14 -2
- package/dist/index.d.ts +14 -2
- package/dist/index.js +199 -74
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,123 +1,129 @@
|
|
|
1
1
|
# AISnitch
|
|
2
2
|
|
|
3
|
+
**See what your AI agents are doing. All of them. In real time.**
|
|
4
|
+
|
|
3
5
|
[](https://github.com/vava-nessa/AISnitch/actions/workflows/ci.yml)
|
|
6
|
+
[](https://www.npmjs.com/package/aisnitch)
|
|
7
|
+
[](https://www.npmjs.com/package/@aisnitch/client)
|
|
4
8
|
[](https://nodejs.org/)
|
|
5
9
|
[](./LICENSE)
|
|
6
10
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
You connect your own frontend, dashboard, companion app, notification system, or sound engine to that WebSocket, and you get live sentences like:
|
|
11
|
+
AISnitch is a local daemon that captures activity from **every AI coding tool** running on your machine — Claude Code, OpenCode, Gemini CLI, Codex, Goose, Aider, Copilot CLI, OpenClaw, and any CLI via PTY fallback — normalizes everything into a single event stream, and broadcasts it over WebSocket.
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
- **One stream, all tools** — no more switching between terminals to see what each agent is doing
|
|
14
|
+
- **Zero storage** — pure memory transit, nothing persists to disk, ever
|
|
15
|
+
- **Build anything on top** — dashboards, sound engines, animated companions, Slack bots, menu bar widgets
|
|
14
16
|
|
|
15
|
-
|
|
17
|
+
<!-- TODO: Add TUI demo GIF here -->
|
|
16
18
|
|
|
17
19
|
---
|
|
18
20
|
|
|
19
21
|
## Table of Contents
|
|
20
22
|
|
|
23
|
+
- [Why AISnitch?](#why-aisnitch)
|
|
21
24
|
- [Quick Start](#quick-start)
|
|
22
25
|
- [Install](#install)
|
|
23
|
-
- [
|
|
26
|
+
- [Ecosystem](#ecosystem)
|
|
24
27
|
- [How It Works](#how-it-works)
|
|
25
|
-
- [
|
|
26
|
-
- [
|
|
27
|
-
|
|
28
|
-
|
|
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)
|
|
28
|
+
- [Architecture](#architecture)
|
|
29
|
+
- [Supported Tools](#supported-tools)
|
|
30
|
+
- [Event Model](#event-model)
|
|
31
|
+
- [Build on Top of AISnitch](#build-on-top-of-aisnitch)
|
|
35
32
|
- [CLI Reference](#cli-reference)
|
|
36
33
|
- [TUI Keybinds](#tui-keybinds)
|
|
37
|
-
- [Architecture](#architecture)
|
|
38
34
|
- [Config Reference](#config-reference)
|
|
39
35
|
- [Development](#development)
|
|
40
36
|
- [License](#license)
|
|
41
37
|
|
|
42
38
|
---
|
|
43
39
|
|
|
44
|
-
##
|
|
40
|
+
## Why AISnitch?
|
|
45
41
|
|
|
46
|
-
|
|
47
|
-
pnpm install && pnpm build
|
|
42
|
+
You run Claude Code on your main project, Codex on the API, Aider reviewing a legacy repo. Three agents, three terminals, no shared visibility. You tab-switch constantly. You miss a permission prompt. You don't know which one is idle and which one is burning tokens.
|
|
48
43
|
|
|
49
|
-
|
|
50
|
-
node dist/cli/index.js start
|
|
44
|
+
**AISnitch solves this in one line:**
|
|
51
45
|
|
|
52
|
-
|
|
53
|
-
|
|
46
|
+
```bash
|
|
47
|
+
aisnitch start
|
|
54
48
|
```
|
|
55
49
|
|
|
56
|
-
|
|
50
|
+
Now every tool's activity flows into one dashboard. You see who's thinking, who's coding, who needs input, and who's hit a rate limit — all at once, in real time.
|
|
51
|
+
|
|
52
|
+
Want to build your own UI instead? The entire stream is available on `ws://127.0.0.1:4820` — connect with the [Client SDK](#ecosystem) and build dashboards, sound engines, animated companions, or anything else.
|
|
57
53
|
|
|
58
|
-
|
|
54
|
+
---
|
|
59
55
|
|
|
60
|
-
|
|
56
|
+
## Quick Start
|
|
61
57
|
|
|
62
58
|
```bash
|
|
63
|
-
#
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
"
|
|
72
|
-
```
|
|
59
|
+
# Install and run
|
|
60
|
+
npm i -g aisnitch
|
|
61
|
+
aisnitch start
|
|
73
62
|
|
|
74
|
-
|
|
63
|
+
# Try it without any AI tool — simulated events
|
|
64
|
+
aisnitch start --mock all
|
|
65
|
+
```
|
|
75
66
|
|
|
76
|
-
|
|
67
|
+
That's it. The TUI dashboard opens, and you see live activity from every configured AI tool.
|
|
77
68
|
|
|
78
|
-
|
|
69
|
+
To set up tools:
|
|
79
70
|
|
|
80
71
|
```bash
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
pnpm build
|
|
85
|
-
node dist/cli/index.js --help
|
|
72
|
+
aisnitch setup claude-code # hooks into Claude Code
|
|
73
|
+
aisnitch setup opencode # hooks into OpenCode
|
|
74
|
+
aisnitch adapters # check what's enabled
|
|
86
75
|
```
|
|
87
76
|
|
|
88
|
-
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Install
|
|
80
|
+
|
|
81
|
+
**npm (recommended):**
|
|
89
82
|
|
|
90
83
|
```bash
|
|
91
84
|
npm i -g aisnitch
|
|
92
|
-
aisnitch --help
|
|
93
85
|
```
|
|
94
86
|
|
|
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.
|
|
96
|
-
|
|
97
87
|
**Homebrew:**
|
|
98
88
|
|
|
99
89
|
```bash
|
|
100
|
-
# Formula ships at Formula/aisnitch.rb — copy into your tap
|
|
101
90
|
brew install aisnitch
|
|
102
91
|
```
|
|
103
92
|
|
|
93
|
+
**From source:**
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
git clone https://github.com/vava-nessa/AISnitch.git
|
|
97
|
+
cd AISnitch
|
|
98
|
+
pnpm install && pnpm build
|
|
99
|
+
node dist/cli/index.js start
|
|
100
|
+
```
|
|
101
|
+
|
|
104
102
|
---
|
|
105
103
|
|
|
106
|
-
##
|
|
104
|
+
## Ecosystem
|
|
107
105
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
|
111
|
-
|
|
112
|
-
|
|
|
113
|
-
| **
|
|
114
|
-
|
|
115
|
-
|
|
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>` |
|
|
106
|
+
AISnitch ships as two packages with distinct audiences:
|
|
107
|
+
|
|
108
|
+
| Package | For | Install |
|
|
109
|
+
|---|---|---|
|
|
110
|
+
| [`aisnitch`](https://www.npmjs.com/package/aisnitch) | **Users** — the daemon, CLI, TUI dashboard, adapters | `npm i -g aisnitch` |
|
|
111
|
+
| [`@aisnitch/client`](https://www.npmjs.com/package/@aisnitch/client) | **Developers** — TypeScript SDK to consume the event stream | `pnpm add @aisnitch/client zod` |
|
|
112
|
+
|
|
113
|
+
**You're a user?** Install `aisnitch`, run `aisnitch start`, you're done.
|
|
119
114
|
|
|
120
|
-
|
|
115
|
+
**You're building something on top?** Install `@aisnitch/client` and connect in 3 lines:
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
import { createAISnitchClient, describeEvent } from '@aisnitch/client';
|
|
119
|
+
import WebSocket from 'ws';
|
|
120
|
+
|
|
121
|
+
const client = createAISnitchClient({ WebSocketClass: WebSocket as any });
|
|
122
|
+
client.on('event', (e) => console.log(describeEvent(e)));
|
|
123
|
+
// → "claude-code is editing code → src/index.ts [myproject]"
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Auto-reconnect, Zod-validated parsing, session tracking, filters, mascot state mapping — all included. See the full **[Client SDK documentation](./packages/client/README.md)**.
|
|
121
127
|
|
|
122
128
|
---
|
|
123
129
|
|
|
@@ -150,611 +156,277 @@ Adapters are disabled by default. Run `aisnitch setup <tool>` to arm them, then
|
|
|
150
156
|
(your consumers) (built-in)
|
|
151
157
|
```
|
|
152
158
|
|
|
153
|
-
Each adapter captures tool activity using the best available strategy
|
|
159
|
+
Each adapter captures tool activity using the best available strategy — hooks for tools that support them (Claude Code, OpenCode, Gemini CLI), file watching for log-based tools (Codex, Aider), process detection as universal fallback. Events are validated against Zod schemas, normalized into CloudEvents, enriched with context (terminal, working directory, PID, multi-instance tracking), then pushed through an in-memory EventBus. The WebSocket server broadcasts to all connected clients with per-client ring buffers (1,000 events, oldest-first drop).
|
|
160
|
+
|
|
161
|
+
**Nothing is stored on disk.** Events exist in memory during transit, then they're gone. Privacy-first by design.
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Architecture
|
|
166
|
+
|
|
167
|
+
```mermaid
|
|
168
|
+
flowchart LR
|
|
169
|
+
subgraph Tools["External AI tools"]
|
|
170
|
+
CC["Claude Code"]
|
|
171
|
+
OC["OpenCode"]
|
|
172
|
+
GM["Gemini CLI"]
|
|
173
|
+
CX["Codex"]
|
|
174
|
+
GS["Goose"]
|
|
175
|
+
AD["Aider"]
|
|
176
|
+
OCL["OpenClaw"]
|
|
177
|
+
PTY["Generic PTY"]
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
subgraph AIS["AISnitch runtime"]
|
|
181
|
+
HTTP["HTTP hook receiver :4821"]
|
|
182
|
+
UDS["UDS ingest"]
|
|
183
|
+
REG["Adapter registry"]
|
|
184
|
+
BUS["Typed EventBus"]
|
|
185
|
+
WS["WebSocket server :4820"]
|
|
186
|
+
TUI["Ink TUI"]
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
subgraph SDK["Consumer ecosystem"]
|
|
190
|
+
CLIENT["@aisnitch/client SDK"]
|
|
191
|
+
DASH["Dashboards"]
|
|
192
|
+
SOUND["Sound engines"]
|
|
193
|
+
MASCOT["Companions"]
|
|
194
|
+
BOT["Bots"]
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
CC --> HTTP
|
|
198
|
+
OC --> HTTP
|
|
199
|
+
GM --> HTTP
|
|
200
|
+
OCL --> HTTP
|
|
201
|
+
CX --> REG
|
|
202
|
+
GS --> REG
|
|
203
|
+
AD --> REG
|
|
204
|
+
PTY --> UDS
|
|
205
|
+
HTTP --> BUS
|
|
206
|
+
UDS --> BUS
|
|
207
|
+
REG --> BUS
|
|
208
|
+
BUS --> WS
|
|
209
|
+
BUS --> TUI
|
|
210
|
+
WS --> CLIENT
|
|
211
|
+
CLIENT --> DASH
|
|
212
|
+
CLIENT --> SOUND
|
|
213
|
+
CLIENT --> MASCOT
|
|
214
|
+
CLIENT --> BOT
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
---
|
|
154
218
|
|
|
155
|
-
|
|
219
|
+
## Supported Tools
|
|
220
|
+
|
|
221
|
+
| Tool | Strategy | Setup |
|
|
222
|
+
|---|---|---|
|
|
223
|
+
| **Claude Code** | HTTP hooks + JSONL transcript watching + process detection | `aisnitch setup claude-code` |
|
|
224
|
+
| **OpenCode** | Local plugin + process detection | `aisnitch setup opencode` |
|
|
225
|
+
| **Gemini CLI** | Command hooks + `logs.json` watching + process detection | `aisnitch setup gemini-cli` |
|
|
226
|
+
| **Codex** | `codex-tui.log` parsing + process detection | `aisnitch setup codex` |
|
|
227
|
+
| **Goose** | `goosed` API polling + SSE streams + SQLite fallback | `aisnitch setup goose` |
|
|
228
|
+
| **Copilot CLI** | Repo hooks + session-state JSONL watching | `aisnitch setup copilot-cli` |
|
|
229
|
+
| **Aider** | `.aider.chat.history.md` watching + notifications command | `aisnitch setup aider` |
|
|
230
|
+
| **OpenClaw** | Managed hooks + command/memory/session watchers | `aisnitch setup openclaw` |
|
|
231
|
+
| **Any other CLI** | PTY wrapper with output heuristics | `aisnitch wrap <command>` |
|
|
232
|
+
|
|
233
|
+
Run `aisnitch setup <tool>` to configure each tool, then `aisnitch adapters` to verify what's active.
|
|
156
234
|
|
|
157
235
|
---
|
|
158
236
|
|
|
159
|
-
## Event Model
|
|
237
|
+
## Event Model
|
|
160
238
|
|
|
161
239
|
Every event is a [CloudEvents v1.0](https://cloudevents.io/) envelope with AISnitch extensions:
|
|
162
240
|
|
|
163
241
|
```jsonc
|
|
164
242
|
{
|
|
165
|
-
// CloudEvents core
|
|
166
243
|
"specversion": "1.0",
|
|
167
244
|
"id": "019713a4-beef-7000-8000-deadbeef0042", // UUIDv7
|
|
168
245
|
"source": "aisnitch://claude-code/myproject",
|
|
169
|
-
"type": "agent.coding", // one of 12 types
|
|
246
|
+
"type": "agent.coding", // one of 12 types below
|
|
170
247
|
"time": "2026-03-28T14:30:00.000Z",
|
|
171
248
|
|
|
172
|
-
|
|
173
|
-
"aisnitch.
|
|
174
|
-
"aisnitch.
|
|
175
|
-
"aisnitch.seqnum": 42, // sequence number in this session
|
|
249
|
+
"aisnitch.tool": "claude-code",
|
|
250
|
+
"aisnitch.sessionid": "claude-code:myproject:p12345",
|
|
251
|
+
"aisnitch.seqnum": 42,
|
|
176
252
|
|
|
177
|
-
// Normalized payload
|
|
178
253
|
"data": {
|
|
179
|
-
"state": "agent.coding",
|
|
180
|
-
"project": "myproject",
|
|
254
|
+
"state": "agent.coding",
|
|
255
|
+
"project": "myproject",
|
|
181
256
|
"projectPath": "/home/user/myproject",
|
|
182
|
-
"activeFile": "src/index.ts",
|
|
183
|
-
"toolName": "Edit",
|
|
184
|
-
"toolInput": {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
"
|
|
188
|
-
"
|
|
189
|
-
"
|
|
190
|
-
"
|
|
191
|
-
"
|
|
192
|
-
"
|
|
193
|
-
"
|
|
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 */ }
|
|
257
|
+
"activeFile": "src/index.ts",
|
|
258
|
+
"toolName": "Edit",
|
|
259
|
+
"toolInput": { "filePath": "src/index.ts" },
|
|
260
|
+
"model": "claude-sonnet-4-5-20250514",
|
|
261
|
+
"tokensUsed": 1500,
|
|
262
|
+
"terminal": "iTerm2",
|
|
263
|
+
"cwd": "/home/user/myproject",
|
|
264
|
+
"pid": 12345,
|
|
265
|
+
"instanceIndex": 1,
|
|
266
|
+
"instanceTotal": 3,
|
|
267
|
+
"errorMessage": "Rate limit exceeded", // only on agent.error
|
|
268
|
+
"errorType": "rate_limit", // only on agent.error
|
|
269
|
+
"raw": { /* original adapter payload */ }
|
|
201
270
|
}
|
|
202
271
|
}
|
|
203
272
|
```
|
|
204
273
|
|
|
205
274
|
### The 12 Event Types
|
|
206
275
|
|
|
207
|
-
| Type |
|
|
208
|
-
|
|
209
|
-
| `session.start` | A tool session began |
|
|
210
|
-
| `session.end` | Session closed |
|
|
211
|
-
| `task.start` | User submitted a prompt
|
|
212
|
-
| `task.complete` | Task finished |
|
|
213
|
-
| `agent.thinking` | Model is reasoning |
|
|
214
|
-
| `agent.streaming` | Model is generating output |
|
|
215
|
-
| `agent.coding` | Model
|
|
216
|
-
| `agent.tool_call` | Model
|
|
217
|
-
| `agent.asking_user` | Waiting for human input |
|
|
218
|
-
| `agent.idle` | No activity (
|
|
219
|
-
| `agent.error` | Something went wrong
|
|
220
|
-
| `agent.compact` | Context compaction
|
|
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`
|
|
276
|
+
| Type | What it means |
|
|
277
|
+
|---|---|
|
|
278
|
+
| `session.start` | A tool session began |
|
|
279
|
+
| `session.end` | Session closed |
|
|
280
|
+
| `task.start` | User submitted a prompt |
|
|
281
|
+
| `task.complete` | Task finished |
|
|
282
|
+
| `agent.thinking` | Model is reasoning |
|
|
283
|
+
| `agent.streaming` | Model is generating output |
|
|
284
|
+
| `agent.coding` | Model is editing files |
|
|
285
|
+
| `agent.tool_call` | Model is using a tool (Bash, Grep, etc.) |
|
|
286
|
+
| `agent.asking_user` | Waiting for human input |
|
|
287
|
+
| `agent.idle` | No activity (120s timeout, configurable) |
|
|
288
|
+
| `agent.error` | Something went wrong (rate limit, API error, tool failure) |
|
|
289
|
+
| `agent.compact` | Context compaction / memory cleanup |
|
|
225
290
|
|
|
226
291
|
---
|
|
227
292
|
|
|
228
|
-
##
|
|
293
|
+
## Build on Top of AISnitch
|
|
229
294
|
|
|
230
|
-
|
|
295
|
+
The whole point of AISnitch is to be a platform. Here are 5 things you can build with the [`@aisnitch/client`](./packages/client/README.md) SDK:
|
|
231
296
|
|
|
232
|
-
###
|
|
297
|
+
### Live Dashboard
|
|
233
298
|
|
|
234
|
-
```
|
|
299
|
+
```typescript
|
|
300
|
+
import { createAISnitchClient, describeEvent } from '@aisnitch/client';
|
|
235
301
|
import WebSocket from 'ws';
|
|
236
302
|
|
|
237
|
-
|
|
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
|
-
}
|
|
303
|
+
const client = createAISnitchClient({ WebSocketClass: WebSocket as any });
|
|
253
304
|
|
|
254
|
-
|
|
255
|
-
console.log({
|
|
256
|
-
|
|
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
|
-
});
|
|
305
|
+
client.on('connected', (w) => {
|
|
306
|
+
console.log(`Connected to AISnitch v${w.version}`);
|
|
307
|
+
console.log(`Active tools: ${w.activeTools.join(', ')}`);
|
|
264
308
|
});
|
|
265
309
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
310
|
+
client.on('event', (e) => {
|
|
311
|
+
const line = describeEvent(e);
|
|
312
|
+
console.log(`[${e['aisnitch.tool']}] ${line}`);
|
|
269
313
|
});
|
|
270
314
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
315
|
+
// Track all active sessions
|
|
316
|
+
setInterval(() => {
|
|
317
|
+
const sessions = client.sessions?.getAll() ?? [];
|
|
318
|
+
console.log(`\n--- ${sessions.length} active session(s) ---`);
|
|
319
|
+
for (const s of sessions) {
|
|
320
|
+
console.log(` ${s.tool} → ${s.lastActivity} (${s.eventCount} events)`);
|
|
321
|
+
}
|
|
322
|
+
}, 5000);
|
|
274
323
|
```
|
|
275
324
|
|
|
276
|
-
###
|
|
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:**
|
|
325
|
+
### Sound Notifications (PeonPing-style)
|
|
281
326
|
|
|
282
|
-
```
|
|
283
|
-
|
|
284
|
-
const ws = new WebSocket('ws://127.0.0.1:4820');
|
|
327
|
+
```typescript
|
|
328
|
+
import { createAISnitchClient, filters } from '@aisnitch/client';
|
|
285
329
|
|
|
286
|
-
|
|
287
|
-
const event = JSON.parse(msg.data);
|
|
288
|
-
if (event.type === 'welcome') return;
|
|
330
|
+
const client = createAISnitchClient({ WebSocketClass: WebSocket as any });
|
|
289
331
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
332
|
+
const SOUNDS: Record<string, string> = {
|
|
333
|
+
'session.start': 'boot.mp3',
|
|
334
|
+
'task.complete': 'success.mp3',
|
|
335
|
+
'agent.asking_user': 'alert.mp3',
|
|
336
|
+
'agent.error': 'error.mp3',
|
|
337
|
+
'agent.coding': 'keyboard.mp3',
|
|
293
338
|
};
|
|
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
339
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
-
}
|
|
340
|
+
client.on('event', (e) => {
|
|
341
|
+
const sound = SOUNDS[e.type];
|
|
342
|
+
if (sound) playSound(`./sounds/${sound}`);
|
|
343
|
+
});
|
|
365
344
|
```
|
|
366
345
|
|
|
367
|
-
|
|
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
|
-
```
|
|
346
|
+
### Animated Mascot / Companion
|
|
396
347
|
|
|
397
|
-
|
|
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
|
-
}
|
|
348
|
+
```typescript
|
|
349
|
+
import { createAISnitchClient, eventToMascotState } from '@aisnitch/client';
|
|
424
350
|
|
|
425
|
-
|
|
426
|
-
onUnmounted(() => {
|
|
427
|
-
clearTimeout(reconnectTimer);
|
|
428
|
-
ws?.close();
|
|
429
|
-
});
|
|
351
|
+
const client = createAISnitchClient();
|
|
430
352
|
|
|
431
|
-
|
|
432
|
-
|
|
353
|
+
client.on('event', (e) => {
|
|
354
|
+
const state = eventToMascotState(e);
|
|
355
|
+
// state.mood → 'thinking' | 'working' | 'celebrating' | 'panicking' | ...
|
|
356
|
+
// state.animation → 'ponder' | 'type' | 'dance' | 'shake' | ...
|
|
357
|
+
// state.color → '#a855f7' (hex)
|
|
358
|
+
// state.label → 'Thinking...'
|
|
359
|
+
// state.detail → 'src/index.ts' (optional)
|
|
360
|
+
updateMySprite(state);
|
|
361
|
+
});
|
|
433
362
|
```
|
|
434
363
|
|
|
435
|
-
###
|
|
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)` : '');
|
|
364
|
+
### Slack / Discord Bot
|
|
460
365
|
|
|
461
|
-
|
|
462
|
-
|
|
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}`;
|
|
366
|
+
```typescript
|
|
367
|
+
import { createAISnitchClient, filters, formatStatusLine } from '@aisnitch/client';
|
|
368
|
+
import WebSocket from 'ws';
|
|
483
369
|
|
|
484
|
-
|
|
485
|
-
return `${tool} is compacting context${project}`;
|
|
370
|
+
const client = createAISnitchClient({ WebSocketClass: WebSocket as any });
|
|
486
371
|
|
|
487
|
-
|
|
488
|
-
|
|
372
|
+
// Only notify on events that need attention
|
|
373
|
+
client.on('event', (e) => {
|
|
374
|
+
if (filters.needsAttention(e)) {
|
|
375
|
+
postToSlack(`⚠️ ${formatStatusLine(e)}`);
|
|
489
376
|
}
|
|
490
|
-
}
|
|
491
377
|
|
|
492
|
-
|
|
493
|
-
|
|
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);
|
|
378
|
+
if (e.type === 'task.complete') {
|
|
379
|
+
postToSlack(`✅ ${formatStatusLine(e)}`);
|
|
510
380
|
}
|
|
511
|
-
|
|
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)"
|
|
381
|
+
});
|
|
525
382
|
```
|
|
526
383
|
|
|
527
|
-
###
|
|
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
|
-
}
|
|
384
|
+
### Menu Bar Widget (Electron / Tauri)
|
|
542
385
|
|
|
543
|
-
|
|
386
|
+
```typescript
|
|
387
|
+
import { createAISnitchClient, formatStatusLine } from '@aisnitch/client';
|
|
544
388
|
|
|
545
|
-
|
|
546
|
-
|
|
389
|
+
const client = createAISnitchClient();
|
|
390
|
+
let sessionCounter = 0;
|
|
391
|
+
const sessionMap = new Map<string, number>();
|
|
547
392
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
393
|
+
client.on('event', (e) => {
|
|
394
|
+
if (!sessionMap.has(e['aisnitch.sessionid'])) {
|
|
395
|
+
sessionMap.set(e['aisnitch.sessionid'], ++sessionCounter);
|
|
551
396
|
}
|
|
397
|
+
const num = sessionMap.get(e['aisnitch.sessionid'])!;
|
|
552
398
|
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
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
|
-
};
|
|
399
|
+
// Update your menu bar / tray icon
|
|
400
|
+
tray.setTitle(formatStatusLine(e, num));
|
|
401
|
+
tray.setToolTip(`${client.sessions?.count ?? 0} active sessions`);
|
|
402
|
+
});
|
|
608
403
|
```
|
|
609
404
|
|
|
610
|
-
|
|
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
|
-
}
|
|
405
|
+
For complete API docs, React/Vue hooks, filters, TypeScript integration, and more examples, see the **[Client SDK README](./packages/client/README.md)**.
|
|
635
406
|
|
|
636
|
-
|
|
637
|
-
|
|
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
|
-
}
|
|
407
|
+
<details>
|
|
408
|
+
<summary>Raw WebSocket (without SDK)</summary>
|
|
645
409
|
|
|
646
|
-
|
|
647
|
-
new Notification(`${event['aisnitch.tool']} error`, {
|
|
648
|
-
body: event.data.errorMessage ?? 'Something went wrong',
|
|
649
|
-
});
|
|
650
|
-
}
|
|
410
|
+
If you don't want the SDK, you can connect directly:
|
|
651
411
|
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
412
|
+
```bash
|
|
413
|
+
# One-liner to see raw events
|
|
414
|
+
node -e "
|
|
415
|
+
const WebSocket = require('ws');
|
|
416
|
+
const ws = new WebSocket('ws://127.0.0.1:4820');
|
|
417
|
+
ws.on('message', m => {
|
|
418
|
+
const e = JSON.parse(m.toString());
|
|
419
|
+
if (e.type !== 'welcome') console.log(e.type, e['aisnitch.tool'], e.data?.project);
|
|
420
|
+
});
|
|
421
|
+
"
|
|
660
422
|
```
|
|
661
423
|
|
|
662
|
-
|
|
424
|
+
The first message is always a `welcome` payload with version, active tools, and uptime. Every subsequent message is a CloudEvents event as described above.
|
|
663
425
|
|
|
664
|
-
|
|
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
|
-
```
|
|
426
|
+
</details>
|
|
753
427
|
|
|
754
428
|
### Health Check
|
|
755
429
|
|
|
756
|
-
AISnitch exposes a health endpoint on the HTTP port:
|
|
757
|
-
|
|
758
430
|
```bash
|
|
759
431
|
curl http://127.0.0.1:4821/health
|
|
760
432
|
```
|
|
@@ -769,8 +441,6 @@ curl http://127.0.0.1:4821/health
|
|
|
769
441
|
}
|
|
770
442
|
```
|
|
771
443
|
|
|
772
|
-
Useful for monitoring if the bridge is alive before connecting your consumer.
|
|
773
|
-
|
|
774
444
|
---
|
|
775
445
|
|
|
776
446
|
## CLI Reference
|
|
@@ -785,9 +455,12 @@ aisnitch start --view full-data # expanded JSON inspector
|
|
|
785
455
|
# Background daemon
|
|
786
456
|
aisnitch start --daemon
|
|
787
457
|
aisnitch status # check if daemon is running
|
|
788
|
-
aisnitch attach # open
|
|
458
|
+
aisnitch attach # open TUI attached to running daemon
|
|
789
459
|
aisnitch stop # kill daemon
|
|
790
460
|
|
|
461
|
+
# Raw event logger (no TUI, full payload)
|
|
462
|
+
aisnitch logger
|
|
463
|
+
|
|
791
464
|
# Tool setup (run once per tool)
|
|
792
465
|
aisnitch setup claude-code
|
|
793
466
|
aisnitch setup opencode
|
|
@@ -802,11 +475,11 @@ aisnitch setup claude-code --revert # undo setup
|
|
|
802
475
|
# Check enabled adapters
|
|
803
476
|
aisnitch adapters
|
|
804
477
|
|
|
805
|
-
# Demo mode
|
|
478
|
+
# Demo mode (simulated events)
|
|
806
479
|
aisnitch mock claude-code --speed 2 --duration 20
|
|
807
|
-
aisnitch start --mock all
|
|
480
|
+
aisnitch start --mock all
|
|
808
481
|
|
|
809
|
-
# PTY wrapper
|
|
482
|
+
# PTY wrapper (any unsupported CLI)
|
|
810
483
|
aisnitch wrap aider --model sonnet
|
|
811
484
|
aisnitch wrap goose session
|
|
812
485
|
```
|
|
@@ -816,9 +489,9 @@ aisnitch wrap goose session
|
|
|
816
489
|
## TUI Keybinds
|
|
817
490
|
|
|
818
491
|
| Key | Action |
|
|
819
|
-
|
|
492
|
+
|---|---|
|
|
820
493
|
| `q` / `Ctrl+C` | Quit |
|
|
821
|
-
| `d` | Start / stop the daemon
|
|
494
|
+
| `d` | Start / stop the daemon |
|
|
822
495
|
| `r` | Refresh daemon status |
|
|
823
496
|
| `v` | Toggle full-data JSON inspector |
|
|
824
497
|
| `f` | Tool filter picker |
|
|
@@ -829,70 +502,25 @@ aisnitch wrap goose session
|
|
|
829
502
|
| `c` | Clear event buffer |
|
|
830
503
|
| `?` | Help overlay |
|
|
831
504
|
| `Tab` | Switch panel focus |
|
|
832
|
-
| `↑↓` / `jk` | Navigate rows
|
|
505
|
+
| `↑↓` / `jk` | Navigate rows |
|
|
833
506
|
| `[` `]` | Page inspector up / down |
|
|
834
507
|
|
|
835
508
|
---
|
|
836
509
|
|
|
837
|
-
## Architecture
|
|
838
|
-
|
|
839
|
-
```mermaid
|
|
840
|
-
flowchart LR
|
|
841
|
-
subgraph Tools["External AI tools"]
|
|
842
|
-
CC["Claude Code"]
|
|
843
|
-
OC["OpenCode"]
|
|
844
|
-
GM["Gemini CLI"]
|
|
845
|
-
CX["Codex"]
|
|
846
|
-
GS["Goose"]
|
|
847
|
-
AD["Aider"]
|
|
848
|
-
OCL["OpenClaw"]
|
|
849
|
-
PTY["Generic PTY"]
|
|
850
|
-
end
|
|
851
|
-
|
|
852
|
-
subgraph AIS["AISnitch runtime"]
|
|
853
|
-
HTTP["HTTP hook receiver :4821"]
|
|
854
|
-
UDS["UDS ingest"]
|
|
855
|
-
REG["Adapter registry"]
|
|
856
|
-
BUS["Typed EventBus"]
|
|
857
|
-
WS["WebSocket server :4820"]
|
|
858
|
-
TUI["Ink TUI"]
|
|
859
|
-
end
|
|
860
|
-
|
|
861
|
-
CC --> HTTP
|
|
862
|
-
OC --> HTTP
|
|
863
|
-
GM --> HTTP
|
|
864
|
-
OCL --> HTTP
|
|
865
|
-
CX --> REG
|
|
866
|
-
GS --> REG
|
|
867
|
-
AD --> REG
|
|
868
|
-
PTY --> UDS
|
|
869
|
-
HTTP --> BUS
|
|
870
|
-
UDS --> BUS
|
|
871
|
-
REG --> BUS
|
|
872
|
-
BUS --> WS
|
|
873
|
-
BUS --> TUI
|
|
874
|
-
```
|
|
875
|
-
|
|
876
|
-
---
|
|
877
|
-
|
|
878
510
|
## Config Reference
|
|
879
511
|
|
|
880
|
-
AISnitch state lives under `~/.aisnitch/`
|
|
512
|
+
AISnitch state lives under `~/.aisnitch/` (override with `AISNITCH_HOME`).
|
|
881
513
|
|
|
882
514
|
| Path | Purpose |
|
|
883
|
-
|
|
884
|
-
|
|
|
885
|
-
|
|
|
886
|
-
|
|
|
887
|
-
|
|
|
888
|
-
|
|
|
889
|
-
| `~/.aisnitch/auto-update.json` | Silent self-update state |
|
|
890
|
-
| `~/.aisnitch/auto-update.log` | Last silent self-update worker log |
|
|
891
|
-
|
|
892
|
-
The dashboard surfaces the active WebSocket URL directly in the header so it is easy to copy into another consumer.
|
|
515
|
+
|---|---|
|
|
516
|
+
| `config.json` | User configuration |
|
|
517
|
+
| `aisnitch.pid` | Daemon PID file |
|
|
518
|
+
| `daemon-state.json` | Daemon connection info |
|
|
519
|
+
| `daemon.log` | Daemon output log (5 MB max) |
|
|
520
|
+
| `aisnitch.sock` | Unix domain socket (IPC) |
|
|
893
521
|
|
|
894
522
|
| Port | Purpose |
|
|
895
|
-
|
|
523
|
+
|---|---|
|
|
896
524
|
| `4820` | WebSocket stream (consumers connect here) |
|
|
897
525
|
| `4821` | HTTP hook receiver + `/health` endpoint |
|
|
898
526
|
|
|
@@ -902,20 +530,40 @@ The dashboard surfaces the active WebSocket URL directly in the header so it is
|
|
|
902
530
|
|
|
903
531
|
```bash
|
|
904
532
|
pnpm install
|
|
905
|
-
pnpm build
|
|
906
|
-
pnpm lint
|
|
907
|
-
pnpm typecheck
|
|
908
|
-
pnpm test
|
|
533
|
+
pnpm build # ESM + CJS + .d.ts (main + client SDK)
|
|
534
|
+
pnpm lint # ESLint
|
|
535
|
+
pnpm typecheck # tsc --noEmit
|
|
536
|
+
pnpm test # Vitest (156 tests)
|
|
909
537
|
pnpm test:coverage
|
|
910
|
-
pnpm test:e2e
|
|
538
|
+
pnpm test:e2e # requires opencode installed
|
|
539
|
+
|
|
540
|
+
# Client SDK only
|
|
541
|
+
pnpm --filter @aisnitch/client build
|
|
542
|
+
pnpm --filter @aisnitch/client test # 48 tests
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
Project structure:
|
|
546
|
+
|
|
547
|
+
```
|
|
548
|
+
aisnitch/ # main package — daemon, CLI, TUI, adapters
|
|
549
|
+
├── src/
|
|
550
|
+
│ ├── adapters/ # 13 adapter implementations
|
|
551
|
+
│ ├── cli/ # commander commands
|
|
552
|
+
│ ├── core/ # events, pipeline, config
|
|
553
|
+
│ └── tui/ # Ink dashboard
|
|
554
|
+
├── packages/
|
|
555
|
+
│ └── client/ # @aisnitch/client SDK
|
|
556
|
+
│ └── src/ # types, client, sessions, filters, helpers
|
|
557
|
+
├── docs/ # technical documentation
|
|
558
|
+
└── tasks/ # kanban task board
|
|
911
559
|
```
|
|
912
560
|
|
|
913
|
-
|
|
561
|
+
Docs: [`docs/index.md`](./docs/index.md) | Tasks: [`tasks/tasks.md`](./tasks/tasks.md)
|
|
914
562
|
|
|
915
|
-
Contributing: [`CONTRIBUTING.md`](./CONTRIBUTING.md) | [`CODE_OF_CONDUCT.md`](./CODE_OF_CONDUCT.md)
|
|
563
|
+
Contributing: [`CONTRIBUTING.md`](./CONTRIBUTING.md) | [`CODE_OF_CONDUCT.md`](./CODE_OF_CONDUCT.md)
|
|
916
564
|
|
|
917
565
|
---
|
|
918
566
|
|
|
919
567
|
## License
|
|
920
568
|
|
|
921
|
-
Apache-2.0, © Vanessa Depraute / vava-nessa.
|
|
569
|
+
Apache-2.0, © [Vanessa Depraute / vava-nessa](https://github.com/vava-nessa).
|