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 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
  [![CI](https://github.com/vava-nessa/AISnitch/actions/workflows/ci.yml/badge.svg)](https://github.com/vava-nessa/AISnitch/actions/workflows/ci.yml)
6
+ [![npm](https://img.shields.io/npm/v/aisnitch?logo=npm&label=aisnitch)](https://www.npmjs.com/package/aisnitch)
7
+ [![npm](https://img.shields.io/npm/v/@aisnitch/client?logo=npm&label=@aisnitch/client)](https://www.npmjs.com/package/@aisnitch/client)
4
8
  [![Node >=20](https://img.shields.io/badge/node-%3E%3D20-339933?logo=node.js&logoColor=white)](https://nodejs.org/)
5
9
  [![License: Apache-2.0](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](./LICENSE)
6
10
 
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:
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
- > *"Session #3Claude Code is thinking..."*
12
- > *"#2 ~/projects/myappCodex: Refactoring tables tool call: Edit file"*
13
- > *"#5 Claude Code edited 4 JS files"*
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
- No data is stored. No cloud. No logs on disk. Pure live memory-only transit.
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
- - [Supported Tools](#supported-tools)
26
+ - [Ecosystem](#ecosystem)
24
27
  - [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)
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
- ## Quick Start
40
+ ## Why AISnitch?
45
41
 
46
- ```bash
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
- # Open the PM2-style dashboard
50
- node dist/cli/index.js start
44
+ **AISnitch solves this in one line:**
51
45
 
52
- # Launch with simulated events (no real AI tool needed)
53
- node dist/cli/index.js start --mock all
46
+ ```bash
47
+ aisnitch start
54
48
  ```
55
49
 
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`.
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
- `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.
54
+ ---
59
55
 
60
- To consume the stream from another terminal:
56
+ ## Quick Start
61
57
 
62
58
  ```bash
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
- "
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
- ## Install
67
+ That's it. The TUI dashboard opens, and you see live activity from every configured AI tool.
77
68
 
78
- **From source (recommended for development):**
69
+ To set up tools:
79
70
 
80
71
  ```bash
81
- git clone https://github.com/vava-nessa/AISnitch.git
82
- cd AISnitch
83
- pnpm install
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
- **Global npm install:**
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
- ## Supported Tools
104
+ ## Ecosystem
107
105
 
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>` |
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
- Adapters are disabled by default. Run `aisnitch setup <tool>` to arm them, then `aisnitch adapters` to verify.
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 (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.
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
- **Nothing is stored on disk.** Events exist in memory during transit, then they're gone.
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 Reference
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
- // 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
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", // mirrors type
180
- "project": "myproject", // project name
254
+ "state": "agent.coding",
255
+ "project": "myproject",
181
256
  "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 */ }
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 | 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`
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
- ## Consumer Integration Guide
293
+ ## Build on Top of AISnitch
229
294
 
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.
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
- ### Connect from Node.js / TypeScript
297
+ ### Live Dashboard
233
298
 
234
- ```ts
299
+ ```typescript
300
+ import { createAISnitchClient, describeEvent } from '@aisnitch/client';
235
301
  import WebSocket from 'ws';
236
302
 
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
- }
303
+ const client = createAISnitchClient({ WebSocketClass: WebSocket as any });
253
304
 
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
- });
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
- ws.on('close', () => {
267
- console.log('Disconnected AISnitch stopped or restarted');
268
- // 📖 Implement reconnect logic here if needed
310
+ client.on('event', (e) => {
311
+ const line = describeEvent(e);
312
+ console.log(`[${e['aisnitch.tool']}] ${line}`);
269
313
  });
270
314
 
271
- ws.on('error', (err) => {
272
- console.error('WebSocket error:', err.message);
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
- ### 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:**
325
+ ### Sound Notifications (PeonPing-style)
281
326
 
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');
327
+ ```typescript
328
+ import { createAISnitchClient, filters } from '@aisnitch/client';
285
329
 
286
- ws.onmessage = (msg) => {
287
- const event = JSON.parse(msg.data);
288
- if (event.type === 'welcome') return;
330
+ const client = createAISnitchClient({ WebSocketClass: WebSocket as any });
289
331
 
290
- // Do anything: update DOM, play sound, trigger animation...
291
- document.getElementById('status').textContent =
292
- `${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',
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
- // 📖 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
- }
340
+ client.on('event', (e) => {
341
+ const sound = SOUNDS[e.type];
342
+ if (sound) playSound(`./sounds/${sound}`);
343
+ });
365
344
  ```
366
345
 
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
- ```
346
+ ### Animated Mascot / Companion
396
347
 
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
- }
348
+ ```typescript
349
+ import { createAISnitchClient, eventToMascotState } from '@aisnitch/client';
424
350
 
425
- onMounted(connect);
426
- onUnmounted(() => {
427
- clearTimeout(reconnectTimer);
428
- ws?.close();
429
- });
351
+ const client = createAISnitchClient();
430
352
 
431
- return { events, latestEvent, connected };
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
- ### 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)` : '');
364
+ ### Slack / Discord Bot
460
365
 
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}`;
366
+ ```typescript
367
+ import { createAISnitchClient, filters, formatStatusLine } from '@aisnitch/client';
368
+ import WebSocket from 'ws';
483
369
 
484
- case 'agent.compact':
485
- return `${tool} is compacting context${project}`;
370
+ const client = createAISnitchClient({ WebSocketClass: WebSocket as any });
486
371
 
487
- default:
488
- return `${tool}: ${event.type}`;
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
- // 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);
378
+ if (e.type === 'task.complete') {
379
+ postToSlack(`✅ ${formatStatusLine(e)}`);
510
380
  }
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)"
381
+ });
525
382
  ```
526
383
 
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
- }
384
+ ### Menu Bar Widget (Electron / Tauri)
542
385
 
543
- const sessions = new Map<string, SessionState>();
386
+ ```typescript
387
+ import { createAISnitchClient, formatStatusLine } from '@aisnitch/client';
544
388
 
545
- function updateSession(event: AISnitchEvent): void {
546
- const sid = event['aisnitch.sessionid'];
389
+ const client = createAISnitchClient();
390
+ let sessionCounter = 0;
391
+ const sessionMap = new Map<string, number>();
547
392
 
548
- if (event.type === 'session.end') {
549
- sessions.delete(sid);
550
- return;
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
- 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
- };
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
- ### 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
- }
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
- // 📖 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
- }
407
+ <details>
408
+ <summary>Raw WebSocket (without SDK)</summary>
645
409
 
646
- if (event.type === 'agent.error') {
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
- 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
- }
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
- ### Build an Animated Mascot / Companion
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
- ```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
- ```
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 the same dashboard and attach if active
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 --mock-duration 20
480
+ aisnitch start --mock all
808
481
 
809
- # PTY wrapper fallback (any unsupported CLI)
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 from the dashboard |
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 / inspector |
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/` by default (override with `AISNITCH_HOME` env var).
512
+ AISnitch state lives under `~/.aisnitch/` (override with `AISNITCH_HOME`).
881
513
 
882
514
  | 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 |
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 # ESM + CJS + .d.ts
906
- pnpm lint # ESLint
907
- pnpm typecheck # tsc --noEmit
908
- pnpm test # Vitest (142 tests)
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 # requires opencode installed
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
- Detailed docs: [`docs/index.md`](./docs/index.md) | [`tasks/tasks.md`](./tasks/tasks.md)
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) | [`AGENTS.md`](./AGENTS.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).