aisnitch 0.2.3 → 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 +312 -669
- package/dist/cli/index.cjs +210 -71
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +210 -71
- package/dist/cli/index.js.map +1 -1
- package/dist/index.cjs +198 -73
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +13 -1
- package/dist/index.d.ts +13 -1
- package/dist/index.js +198 -73
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,128 +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?
|
|
41
|
+
|
|
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.
|
|
43
|
+
|
|
44
|
+
**AISnitch solves this in one line:**
|
|
45
45
|
|
|
46
46
|
```bash
|
|
47
|
-
|
|
47
|
+
aisnitch start
|
|
48
|
+
```
|
|
48
49
|
|
|
49
|
-
|
|
50
|
-
node dist/cli/index.js start
|
|
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
51
|
|
|
52
|
-
|
|
53
|
-
node dist/cli/index.js start --mock all
|
|
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.
|
|
54
53
|
|
|
55
|
-
|
|
56
|
-
node dist/cli/index.js logger
|
|
57
|
-
```
|
|
54
|
+
---
|
|
58
55
|
|
|
59
|
-
|
|
56
|
+
## Quick Start
|
|
60
57
|
|
|
61
|
-
|
|
58
|
+
```bash
|
|
59
|
+
# Install and run
|
|
60
|
+
npm i -g aisnitch
|
|
61
|
+
aisnitch start
|
|
62
|
+
|
|
63
|
+
# Try it without any AI tool — simulated events
|
|
64
|
+
aisnitch start --mock all
|
|
65
|
+
```
|
|
62
66
|
|
|
63
|
-
|
|
67
|
+
That's it. The TUI dashboard opens, and you see live activity from every configured AI tool.
|
|
64
68
|
|
|
65
|
-
To
|
|
69
|
+
To set up tools:
|
|
66
70
|
|
|
67
71
|
```bash
|
|
68
|
-
#
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const ws = new WebSocket('ws://127.0.0.1:4820');
|
|
72
|
-
ws.on('message', m => {
|
|
73
|
-
const e = JSON.parse(m.toString());
|
|
74
|
-
if (e.type !== 'welcome') console.log(e.type, e['aisnitch.tool'], e.data?.project);
|
|
75
|
-
});
|
|
76
|
-
"
|
|
72
|
+
aisnitch setup claude-code # hooks into Claude Code
|
|
73
|
+
aisnitch setup opencode # hooks into OpenCode
|
|
74
|
+
aisnitch adapters # check what's enabled
|
|
77
75
|
```
|
|
78
76
|
|
|
79
77
|
---
|
|
80
78
|
|
|
81
79
|
## Install
|
|
82
80
|
|
|
83
|
-
**
|
|
81
|
+
**npm (recommended):**
|
|
84
82
|
|
|
85
83
|
```bash
|
|
86
|
-
|
|
87
|
-
cd AISnitch
|
|
88
|
-
pnpm install
|
|
89
|
-
pnpm build
|
|
90
|
-
node dist/cli/index.js --help
|
|
84
|
+
npm i -g aisnitch
|
|
91
85
|
```
|
|
92
86
|
|
|
93
|
-
**
|
|
87
|
+
**Homebrew:**
|
|
94
88
|
|
|
95
89
|
```bash
|
|
96
|
-
|
|
97
|
-
aisnitch --help
|
|
90
|
+
brew install aisnitch
|
|
98
91
|
```
|
|
99
92
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
**Homebrew:**
|
|
93
|
+
**From source:**
|
|
103
94
|
|
|
104
95
|
```bash
|
|
105
|
-
|
|
106
|
-
|
|
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
|
|
107
100
|
```
|
|
108
101
|
|
|
109
102
|
---
|
|
110
103
|
|
|
111
|
-
##
|
|
104
|
+
## Ecosystem
|
|
112
105
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
|
116
|
-
|
|
117
|
-
|
|
|
118
|
-
| **
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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.
|
|
114
|
+
|
|
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
|
+
```
|
|
124
125
|
|
|
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)**.
|
|
126
127
|
|
|
127
128
|
---
|
|
128
129
|
|
|
@@ -155,611 +156,277 @@ Adapters are disabled by default. Run `aisnitch setup <tool>` to arm them, then
|
|
|
155
156
|
(your consumers) (built-in)
|
|
156
157
|
```
|
|
157
158
|
|
|
158
|
-
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).
|
|
159
160
|
|
|
160
|
-
**Nothing is stored on disk.** Events exist in memory during transit, then they're gone.
|
|
161
|
+
**Nothing is stored on disk.** Events exist in memory during transit, then they're gone. Privacy-first by design.
|
|
161
162
|
|
|
162
163
|
---
|
|
163
164
|
|
|
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
|
+
---
|
|
218
|
+
|
|
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.
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## Event Model
|
|
165
238
|
|
|
166
239
|
Every event is a [CloudEvents v1.0](https://cloudevents.io/) envelope with AISnitch extensions:
|
|
167
240
|
|
|
168
241
|
```jsonc
|
|
169
242
|
{
|
|
170
|
-
// CloudEvents core
|
|
171
243
|
"specversion": "1.0",
|
|
172
244
|
"id": "019713a4-beef-7000-8000-deadbeef0042", // UUIDv7
|
|
173
245
|
"source": "aisnitch://claude-code/myproject",
|
|
174
|
-
"type": "agent.coding", // one of 12 types
|
|
246
|
+
"type": "agent.coding", // one of 12 types below
|
|
175
247
|
"time": "2026-03-28T14:30:00.000Z",
|
|
176
248
|
|
|
177
|
-
|
|
178
|
-
"aisnitch.
|
|
179
|
-
"aisnitch.
|
|
180
|
-
"aisnitch.seqnum": 42, // sequence number in this session
|
|
249
|
+
"aisnitch.tool": "claude-code",
|
|
250
|
+
"aisnitch.sessionid": "claude-code:myproject:p12345",
|
|
251
|
+
"aisnitch.seqnum": 42,
|
|
181
252
|
|
|
182
|
-
// Normalized payload
|
|
183
253
|
"data": {
|
|
184
|
-
"state": "agent.coding",
|
|
185
|
-
"project": "myproject",
|
|
254
|
+
"state": "agent.coding",
|
|
255
|
+
"project": "myproject",
|
|
186
256
|
"projectPath": "/home/user/myproject",
|
|
187
|
-
"activeFile": "src/index.ts",
|
|
188
|
-
"toolName": "Edit",
|
|
189
|
-
"toolInput": {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
"
|
|
193
|
-
"
|
|
194
|
-
"
|
|
195
|
-
"
|
|
196
|
-
"
|
|
197
|
-
"
|
|
198
|
-
"
|
|
199
|
-
|
|
200
|
-
// Error fields (only on agent.error)
|
|
201
|
-
"errorMessage": "Rate limit exceeded",
|
|
202
|
-
"errorType": "rate_limit", // rate_limit | context_overflow | tool_failure | api_error
|
|
203
|
-
|
|
204
|
-
// Raw source payload (adapter-specific, for advanced consumers)
|
|
205
|
-
"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 */ }
|
|
206
270
|
}
|
|
207
271
|
}
|
|
208
272
|
```
|
|
209
273
|
|
|
210
274
|
### The 12 Event Types
|
|
211
275
|
|
|
212
|
-
| Type |
|
|
213
|
-
|
|
214
|
-
| `session.start` | A tool session began |
|
|
215
|
-
| `session.end` | Session closed |
|
|
216
|
-
| `task.start` | User submitted a prompt
|
|
217
|
-
| `task.complete` | Task finished |
|
|
218
|
-
| `agent.thinking` | Model is reasoning |
|
|
219
|
-
| `agent.streaming` | Model is generating output |
|
|
220
|
-
| `agent.coding` | Model
|
|
221
|
-
| `agent.tool_call` | Model
|
|
222
|
-
| `agent.asking_user` | Waiting for human input |
|
|
223
|
-
| `agent.idle` | No activity (
|
|
224
|
-
| `agent.error` | Something went wrong
|
|
225
|
-
| `agent.compact` | Context compaction
|
|
226
|
-
|
|
227
|
-
### Recognized Tool Names
|
|
228
|
-
|
|
229
|
-
`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 |
|
|
230
290
|
|
|
231
291
|
---
|
|
232
292
|
|
|
233
|
-
##
|
|
293
|
+
## Build on Top of AISnitch
|
|
234
294
|
|
|
235
|
-
|
|
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:
|
|
236
296
|
|
|
237
|
-
###
|
|
297
|
+
### Live Dashboard
|
|
238
298
|
|
|
239
|
-
```
|
|
299
|
+
```typescript
|
|
300
|
+
import { createAISnitchClient, describeEvent } from '@aisnitch/client';
|
|
240
301
|
import WebSocket from 'ws';
|
|
241
302
|
|
|
242
|
-
|
|
243
|
-
const ws = new WebSocket('ws://127.0.0.1:4820');
|
|
244
|
-
|
|
245
|
-
ws.on('open', () => {
|
|
246
|
-
console.log('Connected to AISnitch');
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
ws.on('message', (buffer) => {
|
|
250
|
-
const event = JSON.parse(buffer.toString('utf8'));
|
|
303
|
+
const client = createAISnitchClient({ WebSocketClass: WebSocket as any });
|
|
251
304
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
console.log('Active tools:', event.activeTools);
|
|
256
|
-
return;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
// 📖 Every other message is a normalized AISnitch event
|
|
260
|
-
console.log({
|
|
261
|
-
type: event.type, // "agent.coding"
|
|
262
|
-
tool: event['aisnitch.tool'], // "claude-code"
|
|
263
|
-
session: event['aisnitch.sessionid'],// "claude-code:myproject:p12345"
|
|
264
|
-
seq: event['aisnitch.seqnum'], // 42
|
|
265
|
-
project: event.data?.project, // "myproject"
|
|
266
|
-
file: event.data?.activeFile, // "src/index.ts"
|
|
267
|
-
model: event.data?.model, // "claude-sonnet-4-5-20250514"
|
|
268
|
-
});
|
|
305
|
+
client.on('connected', (w) => {
|
|
306
|
+
console.log(`Connected to AISnitch v${w.version}`);
|
|
307
|
+
console.log(`Active tools: ${w.activeTools.join(', ')}`);
|
|
269
308
|
});
|
|
270
309
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
310
|
+
client.on('event', (e) => {
|
|
311
|
+
const line = describeEvent(e);
|
|
312
|
+
console.log(`[${e['aisnitch.tool']}] ${line}`);
|
|
274
313
|
});
|
|
275
314
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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);
|
|
279
323
|
```
|
|
280
324
|
|
|
281
|
-
###
|
|
282
|
-
|
|
283
|
-
The WebSocket is plain `ws://` on localhost — browsers can connect directly with the native `WebSocket` API. No library needed.
|
|
325
|
+
### Sound Notifications (PeonPing-style)
|
|
284
326
|
|
|
285
|
-
|
|
327
|
+
```typescript
|
|
328
|
+
import { createAISnitchClient, filters } from '@aisnitch/client';
|
|
286
329
|
|
|
287
|
-
|
|
288
|
-
// 📖 Connect to AISnitch from any browser page on the same machine
|
|
289
|
-
const ws = new WebSocket('ws://127.0.0.1:4820');
|
|
330
|
+
const client = createAISnitchClient({ WebSocketClass: WebSocket as any });
|
|
290
331
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
`${event['aisnitch.tool']} — ${event.type}`;
|
|
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',
|
|
298
338
|
};
|
|
299
|
-
```
|
|
300
339
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
interface AISnitchEvent {
|
|
307
|
-
type: string;
|
|
308
|
-
time: string;
|
|
309
|
-
'aisnitch.tool': string;
|
|
310
|
-
'aisnitch.sessionid': string;
|
|
311
|
-
'aisnitch.seqnum': number;
|
|
312
|
-
data: {
|
|
313
|
-
state: string;
|
|
314
|
-
project?: string;
|
|
315
|
-
activeFile?: string;
|
|
316
|
-
toolName?: string;
|
|
317
|
-
toolInput?: { filePath?: string; command?: string };
|
|
318
|
-
model?: string;
|
|
319
|
-
tokensUsed?: number;
|
|
320
|
-
errorMessage?: string;
|
|
321
|
-
errorType?: string;
|
|
322
|
-
terminal?: string;
|
|
323
|
-
cwd?: string;
|
|
324
|
-
pid?: number;
|
|
325
|
-
instanceIndex?: number;
|
|
326
|
-
instanceTotal?: number;
|
|
327
|
-
};
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
// 📖 Drop-in React hook — auto-reconnects every 3s if AISnitch restarts
|
|
331
|
-
export function useAISnitch(url = 'ws://127.0.0.1:4820') {
|
|
332
|
-
const [events, setEvents] = useState<AISnitchEvent[]>([]);
|
|
333
|
-
const [connected, setConnected] = useState(false);
|
|
334
|
-
const [latestEvent, setLatestEvent] = useState<AISnitchEvent | null>(null);
|
|
335
|
-
const wsRef = useRef<WebSocket | null>(null);
|
|
336
|
-
|
|
337
|
-
const connect = useCallback(() => {
|
|
338
|
-
const ws = new WebSocket(url);
|
|
339
|
-
wsRef.current = ws;
|
|
340
|
-
|
|
341
|
-
ws.onopen = () => setConnected(true);
|
|
342
|
-
ws.onclose = () => {
|
|
343
|
-
setConnected(false);
|
|
344
|
-
setTimeout(connect, 3000); // 📖 Reconnect after 3s
|
|
345
|
-
};
|
|
346
|
-
|
|
347
|
-
ws.onmessage = (msg) => {
|
|
348
|
-
const event = JSON.parse(msg.data) as AISnitchEvent;
|
|
349
|
-
if (event.type === 'welcome') return;
|
|
350
|
-
|
|
351
|
-
setLatestEvent(event);
|
|
352
|
-
setEvents((prev) => [...prev.slice(-499), event]); // 📖 Keep last 500
|
|
353
|
-
};
|
|
354
|
-
|
|
355
|
-
ws.onerror = () => ws.close();
|
|
356
|
-
}, [url]);
|
|
357
|
-
|
|
358
|
-
useEffect(() => {
|
|
359
|
-
connect();
|
|
360
|
-
return () => wsRef.current?.close();
|
|
361
|
-
}, [connect]);
|
|
362
|
-
|
|
363
|
-
const clear = useCallback(() => {
|
|
364
|
-
setEvents([]);
|
|
365
|
-
setLatestEvent(null);
|
|
366
|
-
}, []);
|
|
367
|
-
|
|
368
|
-
return { events, latestEvent, connected, clear };
|
|
369
|
-
}
|
|
340
|
+
client.on('event', (e) => {
|
|
341
|
+
const sound = SOUNDS[e.type];
|
|
342
|
+
if (sound) playSound(`./sounds/${sound}`);
|
|
343
|
+
});
|
|
370
344
|
```
|
|
371
345
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
```tsx
|
|
375
|
-
function AIActivityPanel() {
|
|
376
|
-
const { events, latestEvent, connected } = useAISnitch();
|
|
377
|
-
|
|
378
|
-
return (
|
|
379
|
-
<div>
|
|
380
|
-
<span>{connected ? '🟢 Live' : '🔴 Disconnected'}</span>
|
|
381
|
-
|
|
382
|
-
{latestEvent && (
|
|
383
|
-
<p>
|
|
384
|
-
{latestEvent['aisnitch.tool']} — {latestEvent.type}
|
|
385
|
-
{latestEvent.data.project && ` on ${latestEvent.data.project}`}
|
|
386
|
-
</p>
|
|
387
|
-
)}
|
|
388
|
-
|
|
389
|
-
<ul>
|
|
390
|
-
{events.map((e, i) => (
|
|
391
|
-
<li key={i}>
|
|
392
|
-
[{e['aisnitch.tool']}] {e.type}
|
|
393
|
-
{e.data.activeFile && ` → ${e.data.activeFile}`}
|
|
394
|
-
</li>
|
|
395
|
-
))}
|
|
396
|
-
</ul>
|
|
397
|
-
</div>
|
|
398
|
-
);
|
|
399
|
-
}
|
|
400
|
-
```
|
|
346
|
+
### Animated Mascot / Companion
|
|
401
347
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
```ts
|
|
405
|
-
import { ref, onMounted, onUnmounted } from 'vue';
|
|
406
|
-
|
|
407
|
-
export function useAISnitch(url = 'ws://127.0.0.1:4820') {
|
|
408
|
-
const events = ref<any[]>([]);
|
|
409
|
-
const connected = ref(false);
|
|
410
|
-
const latestEvent = ref<any>(null);
|
|
411
|
-
let ws: WebSocket | null = null;
|
|
412
|
-
let reconnectTimer: ReturnType<typeof setTimeout>;
|
|
413
|
-
|
|
414
|
-
function connect() {
|
|
415
|
-
ws = new WebSocket(url);
|
|
416
|
-
ws.onopen = () => (connected.value = true);
|
|
417
|
-
ws.onclose = () => {
|
|
418
|
-
connected.value = false;
|
|
419
|
-
reconnectTimer = setTimeout(connect, 3000);
|
|
420
|
-
};
|
|
421
|
-
ws.onmessage = (msg) => {
|
|
422
|
-
const event = JSON.parse(msg.data);
|
|
423
|
-
if (event.type === 'welcome') return;
|
|
424
|
-
latestEvent.value = event;
|
|
425
|
-
events.value = [...events.value.slice(-499), event];
|
|
426
|
-
};
|
|
427
|
-
ws.onerror = () => ws?.close();
|
|
428
|
-
}
|
|
348
|
+
```typescript
|
|
349
|
+
import { createAISnitchClient, eventToMascotState } from '@aisnitch/client';
|
|
429
350
|
|
|
430
|
-
|
|
431
|
-
onUnmounted(() => {
|
|
432
|
-
clearTimeout(reconnectTimer);
|
|
433
|
-
ws?.close();
|
|
434
|
-
});
|
|
351
|
+
const client = createAISnitchClient();
|
|
435
352
|
|
|
436
|
-
|
|
437
|
-
|
|
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
|
+
});
|
|
438
362
|
```
|
|
439
363
|
|
|
440
|
-
###
|
|
441
|
-
|
|
442
|
-
This is the core use case: transform raw events into sentences like *"Claude Code is editing src/index.ts"*.
|
|
443
|
-
|
|
444
|
-
```ts
|
|
445
|
-
// 📖 Maps event type + data into a short human-readable description
|
|
446
|
-
function describeEvent(event: AISnitchEvent): string {
|
|
447
|
-
const tool = event['aisnitch.tool'];
|
|
448
|
-
const d = event.data;
|
|
449
|
-
const file = d.activeFile ? ` → ${d.activeFile}` : '';
|
|
450
|
-
const project = d.project ? ` [${d.project}]` : '';
|
|
451
|
-
|
|
452
|
-
switch (event.type) {
|
|
453
|
-
case 'session.start':
|
|
454
|
-
return `${tool} started a new session${project}`;
|
|
455
|
-
|
|
456
|
-
case 'session.end':
|
|
457
|
-
return `${tool} session ended${project}`;
|
|
458
|
-
|
|
459
|
-
case 'task.start':
|
|
460
|
-
return `${tool} received a new prompt${project}`;
|
|
461
|
-
|
|
462
|
-
case 'task.complete':
|
|
463
|
-
return `${tool} finished the task${project}` +
|
|
464
|
-
(d.duration ? ` (${Math.round(d.duration / 1000)}s)` : '');
|
|
465
|
-
|
|
466
|
-
case 'agent.thinking':
|
|
467
|
-
return `${tool} is thinking...${project}`;
|
|
364
|
+
### Slack / Discord Bot
|
|
468
365
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
case 'agent.coding':
|
|
473
|
-
return `${tool} is editing code${file}${project}`;
|
|
474
|
-
|
|
475
|
-
case 'agent.tool_call':
|
|
476
|
-
return `${tool} is using ${d.toolName ?? 'a tool'}` +
|
|
477
|
-
(d.toolInput?.command ? `: ${d.toolInput.command}` : file) +
|
|
478
|
-
project;
|
|
479
|
-
|
|
480
|
-
case 'agent.asking_user':
|
|
481
|
-
return `${tool} needs your input${project}`;
|
|
482
|
-
|
|
483
|
-
case 'agent.idle':
|
|
484
|
-
return `${tool} is idle${project}`;
|
|
485
|
-
|
|
486
|
-
case 'agent.error':
|
|
487
|
-
return `${tool} error: ${d.errorMessage ?? d.errorType ?? 'unknown'}${project}`;
|
|
366
|
+
```typescript
|
|
367
|
+
import { createAISnitchClient, filters, formatStatusLine } from '@aisnitch/client';
|
|
368
|
+
import WebSocket from 'ws';
|
|
488
369
|
|
|
489
|
-
|
|
490
|
-
return `${tool} is compacting context${project}`;
|
|
370
|
+
const client = createAISnitchClient({ WebSocketClass: WebSocket as any });
|
|
491
371
|
|
|
492
|
-
|
|
493
|
-
|
|
372
|
+
// Only notify on events that need attention
|
|
373
|
+
client.on('event', (e) => {
|
|
374
|
+
if (filters.needsAttention(e)) {
|
|
375
|
+
postToSlack(`⚠️ ${formatStatusLine(e)}`);
|
|
494
376
|
}
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
// Example output:
|
|
498
|
-
// "claude-code started a new session [myproject]"
|
|
499
|
-
// "claude-code is editing code → src/index.ts [myproject]"
|
|
500
|
-
// "codex is using Bash: npm test [api-server]"
|
|
501
|
-
// "gemini-cli needs your input"
|
|
502
|
-
// "claude-code error: Rate limit exceeded [myproject]"
|
|
503
|
-
```
|
|
504
|
-
|
|
505
|
-
**Full status line with session number:**
|
|
506
|
-
|
|
507
|
-
```ts
|
|
508
|
-
// 📖 Tracks session indices and builds numbered status lines
|
|
509
|
-
const sessionIndex = new Map<string, number>();
|
|
510
|
-
let sessionCounter = 0;
|
|
511
377
|
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
sessionIndex.set(sessionId, ++sessionCounter);
|
|
378
|
+
if (e.type === 'task.complete') {
|
|
379
|
+
postToSlack(`✅ ${formatStatusLine(e)}`);
|
|
515
380
|
}
|
|
516
|
-
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
function formatStatusLine(event: AISnitchEvent): string {
|
|
520
|
-
const num = getSessionNumber(event['aisnitch.sessionid']);
|
|
521
|
-
const desc = describeEvent(event);
|
|
522
|
-
const cwd = event.data.cwd ?? '';
|
|
523
|
-
return `#${num} ${cwd} — ${desc}`;
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
// Output:
|
|
527
|
-
// "#1 /home/user/myproject — claude-code is thinking..."
|
|
528
|
-
// "#2 /home/user/api — codex is editing code → src/db.ts"
|
|
529
|
-
// "#1 /home/user/myproject — claude-code finished the task (12s)"
|
|
381
|
+
});
|
|
530
382
|
```
|
|
531
383
|
|
|
532
|
-
###
|
|
533
|
-
|
|
534
|
-
Events carry `aisnitch.sessionid` for grouping. A session represents one tool instance working on one project.
|
|
535
|
-
|
|
536
|
-
```ts
|
|
537
|
-
interface SessionState {
|
|
538
|
-
tool: string;
|
|
539
|
-
sessionId: string;
|
|
540
|
-
project?: string;
|
|
541
|
-
cwd?: string;
|
|
542
|
-
lastEvent: AISnitchEvent;
|
|
543
|
-
lastActivity: string; // human-readable
|
|
544
|
-
eventCount: number;
|
|
545
|
-
startedAt: string;
|
|
546
|
-
}
|
|
384
|
+
### Menu Bar Widget (Electron / Tauri)
|
|
547
385
|
|
|
548
|
-
|
|
386
|
+
```typescript
|
|
387
|
+
import { createAISnitchClient, formatStatusLine } from '@aisnitch/client';
|
|
549
388
|
|
|
550
|
-
|
|
551
|
-
|
|
389
|
+
const client = createAISnitchClient();
|
|
390
|
+
let sessionCounter = 0;
|
|
391
|
+
const sessionMap = new Map<string, number>();
|
|
552
392
|
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
393
|
+
client.on('event', (e) => {
|
|
394
|
+
if (!sessionMap.has(e['aisnitch.sessionid'])) {
|
|
395
|
+
sessionMap.set(e['aisnitch.sessionid'], ++sessionCounter);
|
|
556
396
|
}
|
|
397
|
+
const num = sessionMap.get(e['aisnitch.sessionid'])!;
|
|
557
398
|
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
project: event.data.project ?? existing?.project,
|
|
563
|
-
cwd: event.data.cwd ?? existing?.cwd,
|
|
564
|
-
lastEvent: event,
|
|
565
|
-
lastActivity: describeEvent(event),
|
|
566
|
-
eventCount: (existing?.eventCount ?? 0) + 1,
|
|
567
|
-
startedAt: existing?.startedAt ?? event.time,
|
|
568
|
-
});
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
// 📖 Call updateSession() on every event, then read sessions Map for current state
|
|
572
|
-
// sessions.values() gives you all active sessions across all tools
|
|
573
|
-
```
|
|
574
|
-
|
|
575
|
-
### Filter by Tool or Event Type
|
|
576
|
-
|
|
577
|
-
Filtering is client-side. The server broadcasts everything — you pick what you need.
|
|
578
|
-
|
|
579
|
-
```ts
|
|
580
|
-
// 📖 Filter examples — apply to your ws.onmessage handler
|
|
581
|
-
|
|
582
|
-
// Only Claude Code events
|
|
583
|
-
const isClaudeCode = (e: AISnitchEvent) => e['aisnitch.tool'] === 'claude-code';
|
|
584
|
-
|
|
585
|
-
// Only coding activity (edits, tool calls)
|
|
586
|
-
const isCodingActivity = (e: AISnitchEvent) =>
|
|
587
|
-
e.type === 'agent.coding' || e.type === 'agent.tool_call';
|
|
588
|
-
|
|
589
|
-
// Only errors
|
|
590
|
-
const isError = (e: AISnitchEvent) => e.type === 'agent.error';
|
|
591
|
-
|
|
592
|
-
// Only rate limits specifically
|
|
593
|
-
const isRateLimit = (e: AISnitchEvent) =>
|
|
594
|
-
e.type === 'agent.error' && e.data.errorType === 'rate_limit';
|
|
595
|
-
|
|
596
|
-
// Only events for a specific project
|
|
597
|
-
const isMyProject = (e: AISnitchEvent) => e.data.project === 'myproject';
|
|
598
|
-
|
|
599
|
-
// Events that need user attention
|
|
600
|
-
const needsAttention = (e: AISnitchEvent) =>
|
|
601
|
-
e.type === 'agent.asking_user' || e.type === 'agent.error';
|
|
602
|
-
|
|
603
|
-
// Combine filters
|
|
604
|
-
ws.onmessage = (msg) => {
|
|
605
|
-
const event = JSON.parse(msg.data);
|
|
606
|
-
if (event.type === 'welcome') return;
|
|
607
|
-
|
|
608
|
-
if (isClaudeCode(event) && isCodingActivity(event)) {
|
|
609
|
-
// Only Claude Code writing code
|
|
610
|
-
updateUI(event);
|
|
611
|
-
}
|
|
612
|
-
};
|
|
399
|
+
// Update your menu bar / tray icon
|
|
400
|
+
tray.setTitle(formatStatusLine(e, num));
|
|
401
|
+
tray.setToolTip(`${client.sessions?.count ?? 0} active sessions`);
|
|
402
|
+
});
|
|
613
403
|
```
|
|
614
404
|
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
```ts
|
|
618
|
-
// 📖 Map event types to sounds — perfect for a companion/pet app
|
|
619
|
-
const SOUND_MAP: Record<string, string> = {
|
|
620
|
-
'session.start': '/sounds/boot.mp3',
|
|
621
|
-
'session.end': '/sounds/shutdown.mp3',
|
|
622
|
-
'task.start': '/sounds/ping.mp3',
|
|
623
|
-
'task.complete': '/sounds/success.mp3',
|
|
624
|
-
'agent.thinking': '/sounds/thinking-loop.mp3', // loop this one
|
|
625
|
-
'agent.coding': '/sounds/keyboard.mp3',
|
|
626
|
-
'agent.tool_call': '/sounds/tool.mp3',
|
|
627
|
-
'agent.asking_user':'/sounds/alert.mp3',
|
|
628
|
-
'agent.error': '/sounds/error.mp3',
|
|
629
|
-
'agent.idle': '/sounds/idle.mp3',
|
|
630
|
-
};
|
|
631
|
-
|
|
632
|
-
function playEventSound(event: AISnitchEvent): void {
|
|
633
|
-
const soundFile = SOUND_MAP[event.type];
|
|
634
|
-
if (!soundFile) return;
|
|
635
|
-
|
|
636
|
-
// Browser: use Web Audio API
|
|
637
|
-
const audio = new Audio(soundFile);
|
|
638
|
-
audio.play();
|
|
639
|
-
}
|
|
405
|
+
For complete API docs, React/Vue hooks, filters, TypeScript integration, and more examples, see the **[Client SDK README](./packages/client/README.md)**.
|
|
640
406
|
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
if (event.type === 'agent.asking_user') {
|
|
644
|
-
new Notification(`${event['aisnitch.tool']} needs input`, {
|
|
645
|
-
body: event.data.project
|
|
646
|
-
? `Project: ${event.data.project}`
|
|
647
|
-
: 'Waiting for your response...',
|
|
648
|
-
});
|
|
649
|
-
}
|
|
407
|
+
<details>
|
|
408
|
+
<summary>Raw WebSocket (without SDK)</summary>
|
|
650
409
|
|
|
651
|
-
|
|
652
|
-
new Notification(`${event['aisnitch.tool']} error`, {
|
|
653
|
-
body: event.data.errorMessage ?? 'Something went wrong',
|
|
654
|
-
});
|
|
655
|
-
}
|
|
410
|
+
If you don't want the SDK, you can connect directly:
|
|
656
411
|
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
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
|
+
"
|
|
665
422
|
```
|
|
666
423
|
|
|
667
|
-
|
|
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.
|
|
668
425
|
|
|
669
|
-
|
|
670
|
-
// 📖 Map event states to mascot moods — for animated desktop pets,
|
|
671
|
-
// menu bar companions, or overlay widgets
|
|
672
|
-
|
|
673
|
-
interface MascotState {
|
|
674
|
-
mood: 'idle' | 'thinking' | 'working' | 'waiting' | 'celebrating' | 'panicking';
|
|
675
|
-
animation: string;
|
|
676
|
-
color: string;
|
|
677
|
-
label: string;
|
|
678
|
-
detail?: string;
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
function eventToMascotState(event: AISnitchEvent): MascotState {
|
|
682
|
-
const d = event.data;
|
|
683
|
-
|
|
684
|
-
switch (event.type) {
|
|
685
|
-
case 'agent.thinking':
|
|
686
|
-
return {
|
|
687
|
-
mood: 'thinking',
|
|
688
|
-
animation: 'orbit',
|
|
689
|
-
color: '#f59e0b',
|
|
690
|
-
label: 'Thinking...',
|
|
691
|
-
detail: d.model,
|
|
692
|
-
};
|
|
693
|
-
|
|
694
|
-
case 'agent.coding':
|
|
695
|
-
return {
|
|
696
|
-
mood: 'working',
|
|
697
|
-
animation: 'typing',
|
|
698
|
-
color: '#3b82f6',
|
|
699
|
-
label: 'Coding',
|
|
700
|
-
detail: d.activeFile,
|
|
701
|
-
};
|
|
702
|
-
|
|
703
|
-
case 'agent.tool_call':
|
|
704
|
-
return {
|
|
705
|
-
mood: 'working',
|
|
706
|
-
animation: 'inspect',
|
|
707
|
-
color: '#14b8a6',
|
|
708
|
-
label: `Using ${d.toolName ?? 'tool'}`,
|
|
709
|
-
detail: d.toolInput?.command ?? d.toolInput?.filePath,
|
|
710
|
-
};
|
|
711
|
-
|
|
712
|
-
case 'agent.streaming':
|
|
713
|
-
return {
|
|
714
|
-
mood: 'working',
|
|
715
|
-
animation: 'pulse',
|
|
716
|
-
color: '#8b5cf6',
|
|
717
|
-
label: 'Generating...',
|
|
718
|
-
};
|
|
719
|
-
|
|
720
|
-
case 'agent.asking_user':
|
|
721
|
-
return {
|
|
722
|
-
mood: 'waiting',
|
|
723
|
-
animation: 'wave',
|
|
724
|
-
color: '#ec4899',
|
|
725
|
-
label: 'Needs you!',
|
|
726
|
-
};
|
|
727
|
-
|
|
728
|
-
case 'agent.error':
|
|
729
|
-
return {
|
|
730
|
-
mood: 'panicking',
|
|
731
|
-
animation: 'shake',
|
|
732
|
-
color: '#ef4444',
|
|
733
|
-
label: d.errorType === 'rate_limit' ? 'Rate limited!' : 'Error!',
|
|
734
|
-
detail: d.errorMessage,
|
|
735
|
-
};
|
|
736
|
-
|
|
737
|
-
case 'task.complete':
|
|
738
|
-
return {
|
|
739
|
-
mood: 'celebrating',
|
|
740
|
-
animation: 'celebrate',
|
|
741
|
-
color: '#22c55e',
|
|
742
|
-
label: 'Done!',
|
|
743
|
-
};
|
|
744
|
-
|
|
745
|
-
default:
|
|
746
|
-
return {
|
|
747
|
-
mood: 'idle',
|
|
748
|
-
animation: 'breathe',
|
|
749
|
-
color: '#94a3b8',
|
|
750
|
-
label: 'Idle',
|
|
751
|
-
};
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
// 📖 Plug this into your rendering loop:
|
|
756
|
-
// ws.onmessage → eventToMascotState(event) → update sprite/CSS/canvas
|
|
757
|
-
```
|
|
426
|
+
</details>
|
|
758
427
|
|
|
759
428
|
### Health Check
|
|
760
429
|
|
|
761
|
-
AISnitch exposes a health endpoint on the HTTP port:
|
|
762
|
-
|
|
763
430
|
```bash
|
|
764
431
|
curl http://127.0.0.1:4821/health
|
|
765
432
|
```
|
|
@@ -774,8 +441,6 @@ curl http://127.0.0.1:4821/health
|
|
|
774
441
|
}
|
|
775
442
|
```
|
|
776
443
|
|
|
777
|
-
Useful for monitoring if the bridge is alive before connecting your consumer.
|
|
778
|
-
|
|
779
444
|
---
|
|
780
445
|
|
|
781
446
|
## CLI Reference
|
|
@@ -790,9 +455,12 @@ aisnitch start --view full-data # expanded JSON inspector
|
|
|
790
455
|
# Background daemon
|
|
791
456
|
aisnitch start --daemon
|
|
792
457
|
aisnitch status # check if daemon is running
|
|
793
|
-
aisnitch attach # open
|
|
458
|
+
aisnitch attach # open TUI attached to running daemon
|
|
794
459
|
aisnitch stop # kill daemon
|
|
795
460
|
|
|
461
|
+
# Raw event logger (no TUI, full payload)
|
|
462
|
+
aisnitch logger
|
|
463
|
+
|
|
796
464
|
# Tool setup (run once per tool)
|
|
797
465
|
aisnitch setup claude-code
|
|
798
466
|
aisnitch setup opencode
|
|
@@ -807,11 +475,11 @@ aisnitch setup claude-code --revert # undo setup
|
|
|
807
475
|
# Check enabled adapters
|
|
808
476
|
aisnitch adapters
|
|
809
477
|
|
|
810
|
-
# Demo mode
|
|
478
|
+
# Demo mode (simulated events)
|
|
811
479
|
aisnitch mock claude-code --speed 2 --duration 20
|
|
812
|
-
aisnitch start --mock all
|
|
480
|
+
aisnitch start --mock all
|
|
813
481
|
|
|
814
|
-
# PTY wrapper
|
|
482
|
+
# PTY wrapper (any unsupported CLI)
|
|
815
483
|
aisnitch wrap aider --model sonnet
|
|
816
484
|
aisnitch wrap goose session
|
|
817
485
|
```
|
|
@@ -821,9 +489,9 @@ aisnitch wrap goose session
|
|
|
821
489
|
## TUI Keybinds
|
|
822
490
|
|
|
823
491
|
| Key | Action |
|
|
824
|
-
|
|
492
|
+
|---|---|
|
|
825
493
|
| `q` / `Ctrl+C` | Quit |
|
|
826
|
-
| `d` | Start / stop the daemon
|
|
494
|
+
| `d` | Start / stop the daemon |
|
|
827
495
|
| `r` | Refresh daemon status |
|
|
828
496
|
| `v` | Toggle full-data JSON inspector |
|
|
829
497
|
| `f` | Tool filter picker |
|
|
@@ -834,70 +502,25 @@ aisnitch wrap goose session
|
|
|
834
502
|
| `c` | Clear event buffer |
|
|
835
503
|
| `?` | Help overlay |
|
|
836
504
|
| `Tab` | Switch panel focus |
|
|
837
|
-
| `↑↓` / `jk` | Navigate rows
|
|
505
|
+
| `↑↓` / `jk` | Navigate rows |
|
|
838
506
|
| `[` `]` | Page inspector up / down |
|
|
839
507
|
|
|
840
508
|
---
|
|
841
509
|
|
|
842
|
-
## Architecture
|
|
843
|
-
|
|
844
|
-
```mermaid
|
|
845
|
-
flowchart LR
|
|
846
|
-
subgraph Tools["External AI tools"]
|
|
847
|
-
CC["Claude Code"]
|
|
848
|
-
OC["OpenCode"]
|
|
849
|
-
GM["Gemini CLI"]
|
|
850
|
-
CX["Codex"]
|
|
851
|
-
GS["Goose"]
|
|
852
|
-
AD["Aider"]
|
|
853
|
-
OCL["OpenClaw"]
|
|
854
|
-
PTY["Generic PTY"]
|
|
855
|
-
end
|
|
856
|
-
|
|
857
|
-
subgraph AIS["AISnitch runtime"]
|
|
858
|
-
HTTP["HTTP hook receiver :4821"]
|
|
859
|
-
UDS["UDS ingest"]
|
|
860
|
-
REG["Adapter registry"]
|
|
861
|
-
BUS["Typed EventBus"]
|
|
862
|
-
WS["WebSocket server :4820"]
|
|
863
|
-
TUI["Ink TUI"]
|
|
864
|
-
end
|
|
865
|
-
|
|
866
|
-
CC --> HTTP
|
|
867
|
-
OC --> HTTP
|
|
868
|
-
GM --> HTTP
|
|
869
|
-
OCL --> HTTP
|
|
870
|
-
CX --> REG
|
|
871
|
-
GS --> REG
|
|
872
|
-
AD --> REG
|
|
873
|
-
PTY --> UDS
|
|
874
|
-
HTTP --> BUS
|
|
875
|
-
UDS --> BUS
|
|
876
|
-
REG --> BUS
|
|
877
|
-
BUS --> WS
|
|
878
|
-
BUS --> TUI
|
|
879
|
-
```
|
|
880
|
-
|
|
881
|
-
---
|
|
882
|
-
|
|
883
510
|
## Config Reference
|
|
884
511
|
|
|
885
|
-
AISnitch state lives under `~/.aisnitch/`
|
|
512
|
+
AISnitch state lives under `~/.aisnitch/` (override with `AISNITCH_HOME`).
|
|
886
513
|
|
|
887
514
|
| Path | Purpose |
|
|
888
|
-
|
|
889
|
-
|
|
|
890
|
-
|
|
|
891
|
-
|
|
|
892
|
-
|
|
|
893
|
-
|
|
|
894
|
-
| `~/.aisnitch/auto-update.json` | Silent self-update state |
|
|
895
|
-
| `~/.aisnitch/auto-update.log` | Last silent self-update worker log |
|
|
896
|
-
|
|
897
|
-
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) |
|
|
898
521
|
|
|
899
522
|
| Port | Purpose |
|
|
900
|
-
|
|
523
|
+
|---|---|
|
|
901
524
|
| `4820` | WebSocket stream (consumers connect here) |
|
|
902
525
|
| `4821` | HTTP hook receiver + `/health` endpoint |
|
|
903
526
|
|
|
@@ -907,20 +530,40 @@ The dashboard surfaces the active WebSocket URL directly in the header so it is
|
|
|
907
530
|
|
|
908
531
|
```bash
|
|
909
532
|
pnpm install
|
|
910
|
-
pnpm build
|
|
911
|
-
pnpm lint
|
|
912
|
-
pnpm typecheck
|
|
913
|
-
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)
|
|
914
537
|
pnpm test:coverage
|
|
915
|
-
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
|
|
916
559
|
```
|
|
917
560
|
|
|
918
|
-
|
|
561
|
+
Docs: [`docs/index.md`](./docs/index.md) | Tasks: [`tasks/tasks.md`](./tasks/tasks.md)
|
|
919
562
|
|
|
920
|
-
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)
|
|
921
564
|
|
|
922
565
|
---
|
|
923
566
|
|
|
924
567
|
## License
|
|
925
568
|
|
|
926
|
-
Apache-2.0, © Vanessa Depraute / vava-nessa.
|
|
569
|
+
Apache-2.0, © [Vanessa Depraute / vava-nessa](https://github.com/vava-nessa).
|