livetap 0.1.5 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,29 +4,36 @@
4
4
 
5
5
  Connect MQTT brokers, WebSocket feeds, or tail log files. Your agent samples, watches, and acts on real-time data through natural language.
6
6
 
7
- <!-- TODO: Add demo GIF here after recording -->
8
- <!-- ![livetap demo](demo.gif) -->
7
+ ![Claude Code connects to a live MQTT stream of solar inverter data, samples payloads with OBIS codes, and creates a watcher for high power consumption (16.7.0 > 3000W). When the alert fires, it autonomously formats the data and analyzes the reading.](docs/images/demo-setup.png)
8
+ *Real solar inverter data — from natural language request to live alert in under 30 seconds*
9
+
10
+ ![A second alert fires and Claude Code autonomously compares previous and current values, noting consumption increased by 174W and is still climbing. It formats a comparison table and recommends investigation.](docs/images/demo-alert.png)
11
+ *Escalating alerts — the agent compares values across alerts and analyzes trends autonomously*
9
12
 
10
13
  ## Quick start
11
14
 
12
- **Requirements:** [Bun](https://bun.sh), [Redis](https://redis.io/) (`brew install redis`), Claude Code v2.1.80+
15
+ **Requirements:** [Bun](https://bun.sh) or Node.js 18+, Claude Code v2.1.80+
13
16
 
14
17
  ```bash
15
- bun add livetap
18
+ npm install livetap
19
+ npx livetap setup
16
20
  ```
17
21
 
18
- Start the daemon and Claude Code:
22
+ Restart Claude Code with Channels enabled:
19
23
 
20
24
  ```bash
21
- livetap start
22
- claude --dangerously-load-development-channels server:livetap
25
+ claude --dangerously-load-development-channels server:livetap --continue
23
26
  ```
24
27
 
28
+ > **What's that flag?** Claude Code [Channels](https://code.claude.com/docs/en/channels) let MCP servers push real-time events (like watcher alerts) into your conversation. The `--dangerously-load-development-channels` flag enables this for locally-installed servers. It only loads servers listed in your project's `.mcp.json` — nothing is sent externally.
29
+
25
30
  Then ask your agent:
26
31
 
27
32
  > "Connect to the IoT demo at mqtt://broker.emqx.io on topic justinx/demo/# and watch for temperature above 23 degrees"
28
33
 
29
- ## Setup with Claude Code (for AI agents)
34
+ The agent connects to the free public MQTT broker, samples the data to learn the payload structure, creates a watcher, and alerts you when temperature exceeds 23C.
35
+
36
+ ## Setup for AI agents
30
37
 
31
38
  If you are Claude Code or another AI agent, run `npx livetap --llm-help` for structured setup instructions.
32
39
 
@@ -34,57 +41,33 @@ Quick version:
34
41
 
35
42
  ```bash
36
43
  npm install livetap # Step 1: install
37
- npx livetap setup # Step 2: creates .mcp.json
44
+ npx livetap setup # Step 2: creates .mcp.json and starts daemon
38
45
  # Step 3: tell user to restart Claude Code with:
39
46
  # claude --dangerously-load-development-channels server:livetap --continue
40
47
  ```
41
48
 
49
+ After restart, your 13 MCP tools are automatically available — no discovery step needed.
50
+
42
51
  **IMPORTANT — do NOT:**
43
52
  - Add livetap to `~/.claude/mcp.json` — it goes in `.mcp.json` in the project root
44
53
  - Configure livetap as `type: "http"` — it is a stdio MCP server (command + args)
45
- - Start the daemon manually — the MCP proxy auto-starts it on Claude restart
54
+ - Worry about the daemon — setup starts it, and the MCP proxy auto-starts it if needed
46
55
  - Guess field names — always `read_stream` first to see actual payload structure
56
+ - Use `npm init` — use the existing project directory
47
57
 
48
- ### After restart — use MCP tools
49
-
50
- Once restarted with the channel flag, you have 12 MCP tools available. Follow this workflow:
58
+ ### After restart — workflow
51
59
 
52
60
  1. **CONNECT** — `create_connection({ type: "mqtt", broker: "broker.emqx.io", port: 1883, tls: false, topics: ["justinx/demo/#"] })`
53
61
  2. **SAMPLE** — `read_stream({ connectionId: "conn_xxx", backfillSeconds: 60, maxEntries: 10 })` — ALWAYS sample first to see field paths
54
62
  3. **WATCH** — `create_watcher({ connectionId: "conn_xxx", conditions: [{ field: "sensors.temperature.value", op: ">", value: 50 }], match: "all", cooldown: 60 })`
55
-
56
- ### Supported source types
57
-
58
- | Type | create_connection params | CLI |
59
- |------|------------------------|-----|
60
- | MQTT | `{ type: "mqtt", broker: "host", port: 1883, tls: false, topics: ["topic/#"] }` | `livetap tap mqtt://host:1883/topic/#` |
61
- | WebSocket | `{ type: "websocket", url: "wss://..." }` | `livetap tap wss://...` |
62
- | File | `{ type: "file", path: "/var/log/app.log" }` | `livetap tap file:///var/log/app.log` |
63
-
64
- ### Data shape by source
65
-
66
- - **MQTT/WebSocket (JSON):** payload is parsed. Use dot-paths: `sensors.temperature.value`
67
- - **File (plain text):** field is `payload`. Use: `{ field: "payload", op: "contains", value: "ERROR" }` or `{ field: "payload", op: "matches", value: "5[0-9]{2}" }`
68
- - **File (JSON lines):** parsed. Use dot-paths: `level`, `msg`
69
- - **IMPORTANT:** always `read_stream` first. The field is `payload`, NOT `line` or `message`.
70
-
71
- ### Watcher operators
72
-
73
- `>`, `<`, `>=`, `<=`, `==`, `!=`, `contains`, `matches` (regex)
74
-
75
- ### If the daemon is not running
76
-
77
- The MCP proxy auto-starts the daemon. If it fails, run:
78
- ```bash
79
- livetap start
80
- ```
63
+ 4. **ACT** — when `<channel>` alerts arrive, do what the user asked
81
64
 
82
65
  ## What it does
83
66
 
84
- livetap runs a background daemon that connects to live data sources, buffers messages in embedded Redis, and pushes alerts into your Claude Code session via the [Channels API](https://code.claude.com/docs/en/channels). Your agent sees the data in real-time and can create expression-based watchers that fire when conditions match.
67
+ LiveTap runs a background daemon that connects to live data sources, buffers messages in an in-memory StreamStore, and pushes alerts into your Claude Code session via the [Channels API](https://code.claude.com/docs/en/channels). Your agent sees the data in real-time and can create expression-based watchers that fire when conditions match.
85
68
 
86
69
  ```
87
- Source (MQTT/WS/File) ──> Subscriber ──> Redis Stream ──> Watcher Engine
70
+ Source (MQTT/WS/File) ──> Subscriber ──> StreamStore ──> Watcher Engine
88
71
  | |
89
72
  v v (on match)
90
73
  read_stream Channel Alert
@@ -92,26 +75,37 @@ Source (MQTT/WS/File) ──> Subscriber ──> Redis Stream ──> Watcher En
92
75
  ──> agent acts
93
76
  ```
94
77
 
78
+ ## Supported sources and data shapes
79
+
80
+ | Type | create_connection params | CLI | Payload format |
81
+ |------|------------------------|-----|----------------|
82
+ | **MQTT** | `{ type: "mqtt", broker, port, tls, topics, username?, password? }` | `livetap tap mqtt://host:port/topic/#` | JSON parsed — use dot-paths: `sensors.temperature.value` |
83
+ | **WebSocket** | `{ type: "websocket", url, headers?, handshake? }` | `livetap tap wss://...` | JSON parsed — use dot-paths: `p`, `data.value` |
84
+ | **File** | `{ type: "file", path }` | `livetap tap file:///path/to/log` | Plain text: `{ payload: "the raw line" }`. JSON lines: parsed into dot-paths |
85
+
86
+ **IMPORTANT:** always `read_stream` first to see actual field names. The field is `payload`, NOT `line` or `message`.
87
+
95
88
  ## Examples
96
89
 
97
90
  ### IoT sensor monitoring
98
91
 
99
92
  ```
100
- You: "Connect to mqtt://broker.emqx.io:1883/justinx/demo/# and watch for temperature above 25°C"
93
+ You: "Connect to mqtt://broker.emqx.io:1883/justinx/demo/# and watch for temperature above 25C"
101
94
 
102
- Agent: Creates connection, samples the data to learn the payload structure,
95
+ Agent: Connects to the free public broker, samples the data to learn the payload structure,
103
96
  sets up watcher on sensors.environmental.temperature.value > 25.
104
- When it fires, summarizes: "sensor-zone-c hit 25.4°C at 10:05:08Z"
97
+ When it fires: "sensor-zone-c hit 25.4C at 10:05:08Z"
105
98
  ```
106
99
 
107
- ### Crypto price alerts
100
+ ### WebSocket trade stream
108
101
 
109
102
  ```
110
- You: "Tap the Binance BTC/USDT trade stream and alert me if price drops below 60000"
103
+ You: "Tap the Binance BTC/USDT trade stream and log each trade"
111
104
 
112
105
  Agent: Connects to wss://stream.binance.com:9443/ws/btcusdt@trade,
113
- samples to see the price field is "p" (string),
114
- sets up watcher with regex: p matches "^[1-5]" (prices starting with 1-5 = below 60k)
106
+ samples to discover trade fields (p=price, q=quantity, T=timestamp),
107
+ sets up a watcher to log each trade. Can filter by quantity or
108
+ use regex on the symbol field.
115
109
  ```
116
110
 
117
111
  ### Log file monitoring
@@ -120,8 +114,8 @@ Agent: Connects to wss://stream.binance.com:9443/ws/btcusdt@trade,
120
114
  You: "Watch my nginx error log for 5xx errors and summarize each one"
121
115
 
122
116
  Agent: Taps file:///var/log/nginx/error.log,
123
- creates watcher: payload matches "5[0-9]{2}",
124
- when an error appears, analyzes it:
117
+ creates regex watcher: payload matches "5[0-9]{2}",
118
+ summarizes each match:
125
119
  "503 Service Unavailable on /api/data — upstream auth-service not responding"
126
120
  ```
127
121
 
@@ -131,145 +125,162 @@ Agent: Taps file:///var/log/nginx/error.log,
131
125
  You: "Monitor /var/log/wifi.log and alert me when WiFi drops"
132
126
 
133
127
  Agent: Taps the file, samples to see log format, creates regex watcher
134
- for power state changes. When you toggle WiFi:
128
+ for power state changes. Reports outage duration:
135
129
  "Wi-Fi powered OFF at 17:51:45, back ON at 17:51:47 (2s outage)"
136
130
  ```
137
131
 
132
+ ### CLI walkthrough (no agent)
133
+
134
+ You can also use LiveTap directly from the terminal:
135
+
136
+ ```bash
137
+ # 1. Tap a source
138
+ livetap tap mqtt://broker.emqx.io:1883/justinx/demo/#
139
+
140
+ # 2. Sample the data to see what's flowing
141
+ livetap sip conn_xxxx
142
+
143
+ # 3. Set up a watcher
144
+ livetap watch conn_xxxx "sensors.environmental.temperature.value > 25"
145
+
146
+ # 4. Check status
147
+ livetap status
148
+ livetap watchers --logs w_xxxx
149
+ ```
150
+
151
+ ## Expression watchers
152
+
153
+ Watchers use structured conditions:
154
+
155
+ ```json
156
+ {
157
+ "conditions": [
158
+ { "field": "sensors.temperature.value", "op": ">", "value": 50 },
159
+ { "field": "sensors.humidity.value", "op": ">", "value": 90 }
160
+ ],
161
+ "match": "all",
162
+ "cooldown": 60
163
+ }
164
+ ```
165
+
166
+ **Operators:** `>`, `<`, `>=`, `<=`, `==`, `!=`, `contains`, `matches` (regex)
167
+
168
+ **Match modes:** `"all"` = AND (all conditions must be true), `"any"` = OR (at least one)
169
+
170
+ **Cooldown:** Seconds between repeated alerts. `0` for every match, `60` default. Use 0 for rare events, 30-60 for sensors, 300+ for high-frequency streams.
171
+
172
+ When a watcher fires, the alert arrives as a `<channel>` tag in your Claude Code session. The agent reads it and acts — writing to a file, calling an API, or whatever you asked.
173
+
138
174
  ## CLI
139
175
 
140
176
  ```bash
177
+ # Setup
178
+ livetap setup # Configure .mcp.json, start daemon, print restart instructions
179
+
141
180
  # Daemon
142
- livetap start # Start (embedded Redis + API)
143
- livetap stop # Stop
144
- livetap status # Dashboard
181
+ livetap start # Start daemon (auto-started by setup)
182
+ livetap start --port 9000 # Custom port (default 8788, env: LIVETAP_PORT)
183
+ livetap start --foreground # Run in foreground (don't detach)
184
+ livetap stop # Stop daemon
185
+ livetap status # Show daemon, taps, and watchers
186
+ livetap status --json # JSON output
145
187
 
146
188
  # Tap into data sources
147
189
  livetap tap mqtt://broker.emqx.io:1883/sensors/# # MQTT broker
148
190
  livetap tap wss://stream.binance.com:9443/ws/btcusdt@trade # WebSocket
149
191
  livetap tap file:///var/log/nginx/error.log # Log file
192
+ livetap tap connection.json # Config from file
193
+ livetap tap <uri> --name "my-source" # With display name
150
194
  livetap taps # List active taps
151
- livetap untap <connectionId> # Remove
195
+ livetap untap <connectionId> # Remove a tap
152
196
 
153
197
  # Sample data
154
198
  livetap sip <connectionId> # Pretty JSON output
155
199
  livetap sip <connectionId> --raw # Raw JSON
200
+ livetap sip <connectionId> --max 20 --back 120 # 20 entries, last 120 seconds
156
201
 
157
202
  # Watchers
158
203
  livetap watch <connId> "temperature > 50" # Numeric
159
204
  livetap watch <connId> "payload matches 'ERROR|FATAL'" # Regex
160
205
  livetap watch <connId> "temp > 50 AND humidity > 90" # AND
206
+ livetap watch <connId> "temp > 50 OR smoke > 0.05" # OR
161
207
  livetap watch <connId> "price > 70000" --cooldown 300 # Custom cooldown
208
+ livetap watch <connId> "status == 'error'" --action webhook:https://... # Webhook action
162
209
  livetap watchers # List all
163
- livetap watchers --logs <watcherId> # View logs
210
+ livetap watchers <connectionId> # Filter by connection
211
+ livetap watchers <watcherId> # Show details
212
+ livetap watchers --logs <watcherId> # View evaluation logs
164
213
  livetap unwatch <watcherId> # Remove
165
214
  ```
166
215
 
167
- ## Supported sources
168
-
169
- | Protocol | Example | Use case |
170
- |----------|---------|----------|
171
- | **MQTT** | `livetap tap mqtt://broker.emqx.io:1883/sensors/#` | IoT sensors, home automation |
172
- | **WebSocket** | `livetap tap wss://stream.binance.com:9443/ws/btcusdt@trade` | Finance, real-time APIs |
173
- | **File tailing** | `livetap tap file:///var/log/nginx/error.log` | Log monitoring, DevOps |
174
- | Webhooks | Planned v0.1 | CI/CD, external services |
175
- | Kafka | Planned v0.2 | Event sourcing, analytics |
216
+ `livetap --help` for the full reference. `livetap --llm-help` for machine-readable JSON.
176
217
 
177
218
  ## MCP tools
178
219
 
179
- livetap exposes 12 MCP tools that your agent uses automatically:
220
+ LiveTap exposes 13 MCP tools that your agent uses automatically:
180
221
 
181
222
  | Tool | What it does |
182
223
  |------|-------------|
183
- | `create_connection` | Connect to MQTT, WebSocket, or file |
184
- | `list_connections` | List active connections |
185
- | `get_connection` | Connection details and stats |
186
- | `destroy_connection` | Remove a connection |
224
+ | `create_connection` | Connect to MQTT, WebSocket, file, or webhook |
225
+ | `list_connections` | List active connections with status and message rate |
226
+ | `get_connection` | Get detailed connection status |
227
+ | `destroy_connection` | Stop and remove a connection |
187
228
  | `read_stream` | Sample recent entries from a stream |
188
229
  | `create_watcher` | Set up expression-based alerts |
189
- | `list_watchers` | List watchers |
190
- | `get_watcher` | Watcher details |
191
- | `get_watcher_logs` | View MATCH/SUPPRESSED logs |
192
- | `update_watcher` | Change conditions or cooldown |
193
- | `delete_watcher` | Remove a watcher |
230
+ | `list_watchers` | List watchers, optionally filter by connection |
231
+ | `get_watcher` | Watcher details: conditions, status, match count |
232
+ | `get_watcher_logs` | View MATCH, SUPPRESSED, FIELD_NOT_FOUND logs |
233
+ | `update_watcher` | Change conditions, match mode, action, or cooldown |
234
+ | `delete_watcher` | Stop and remove a watcher |
194
235
  | `restart_watcher` | Restart a stopped watcher |
236
+ | `status` | Daemon health, uptime, connections, and watchers summary |
195
237
 
196
- ## Expression watchers
197
-
198
- Watchers use structured conditions — not arbitrary code:
199
-
200
- ```json
201
- {
202
- "conditions": [
203
- { "field": "sensors.temperature.value", "op": ">", "value": 50 },
204
- { "field": "sensors.humidity.value", "op": ">", "value": 90 }
205
- ],
206
- "match": "all",
207
- "cooldown": 60
208
- }
209
- ```
210
-
211
- **Operators:** `>`, `<`, `>=`, `<=`, `==`, `!=`, `contains`, `matches` (regex)
238
+ ## Troubleshooting
212
239
 
213
- **Cooldown:** Seconds between repeated alerts. `0` for every match, `60` default.
240
+ **Daemon won't start / "Unable to connect"**
241
+ Run `livetap start --foreground` to see error output. Check if port 8788 is in use: `lsof -i :8788`. Use `--port` or `LIVETAP_PORT` env var to change.
214
242
 
215
- When a watcher fires, the alert arrives as a `<channel>` tag in your Claude Code session. The agent reads it and acts — writing to a file, calling an API, or whatever you asked.
243
+ **MQTT connection refused**
244
+ Verify the broker is reachable: `nc -zv broker.emqx.io 1883`. Check that `tls: false` and `port: 1883` are set for unencrypted brokers. Brokers on port 8883 typically require `tls: true`.
216
245
 
217
- ## How the agent uses livetap
246
+ **Watcher not firing**
247
+ Run `read_stream` (or `livetap sip`) to verify data is flowing. Check field paths match the actual payload structure. View watcher logs: `livetap watchers --logs <watcherId>` — look for FIELD_NOT_FOUND or SUPPRESSED events.
218
248
 
219
- The MCP instructions teach the agent this workflow:
249
+ **MCP tools not showing after restart**
250
+ Verify `.mcp.json` exists in the project root (not `~/.claude/mcp.json`). Restart Claude Code with the `--dangerously-load-development-channels server:livetap` flag.
220
251
 
221
- 1. **CONNECT** — `create_connection` to tap a source
222
- 2. **SAMPLE** — `read_stream` to see the data shape (always before creating watchers)
223
- 3. **WATCH** — `create_watcher` with the correct field paths
224
- 4. **ACT** — when `<channel>` alerts arrive, do what the user asked
252
+ ## Limitations
225
253
 
226
- The agent knows field paths differ by source:
227
- - **MQTT/WebSocket:** JSON payload parseduse dot-paths like `sensors.temperature.value`
228
- - **File (text):** raw line in `payload` field use `payload contains "ERROR"` or `payload matches "5[0-9]{2}"`
229
- - **File (JSON lines):** parsed use dot-paths like `level`, `msg`
254
+ - **In-memory stream buffer** data does not persist across daemon restarts.
255
+ - **No daemon API auth**the HTTP API listens on localhost only (port 8788).
256
+ - **Throughput** tested with streams up to ~50 msg/s. High-throughput streams (1000+ msg/s) may need higher cooldowns on watchers.
257
+ - **Credentials** MQTT credentials are passed as tool parameters, not stored on disk.
258
+ - **Single instance** — one daemon per port. Multiple projects can share a daemon or use different ports.
230
259
 
231
260
  ## Configuration
232
261
 
233
262
  **Daemon port:** Default `:8788`. Override with `--port` or `LIVETAP_PORT` env var.
234
263
 
235
- **State directory:** `~/.livetap/` stores daemon PID, logs, and watcher evaluation logs.
264
+ **State directory:** `~/.livetap/` stores `daemon.pid`, daemon logs, and watcher evaluation logs.
236
265
 
237
- **MCP config:** `.mcp.json` in your project root:
238
- ```json
239
- {
240
- "mcpServers": {
241
- "livetap": {
242
- "command": "bun",
243
- "args": ["path/to/src/mcp/channel.ts"]
244
- }
245
- }
246
- }
247
- ```
266
+ **MCP config:** `npx livetap setup` generates `.mcp.json` in your project root with the correct absolute path.
248
267
 
249
- **Machine-readable help:** `livetap --llm-help` outputs structured JSON for AI agents.
268
+ **Machine-readable help:** `npx livetap --llm-help` outputs structured JSON with setup steps, CLI commands, and MCP tool schemas.
250
269
 
251
270
  ## Development
252
271
 
253
272
  ```bash
254
273
  git clone https://github.com/livetap/livetap.git
255
- cd livetap && git checkout v0
274
+ cd livetap
256
275
  bun install
257
- bun test # 103 tests
258
- bun test tests/phase1/ # Specific phase
259
- SKIP_LIVE_MQTT=1 bun test # Skip tests needing broker.emqx.io
276
+ bun test # Run all tests
277
+ bun test tests/phase0/ # Canonical drift detection
278
+ SKIP_LIVE_MQTT=1 bun test # Skip tests needing external brokers
260
279
  ```
261
280
 
262
- See [docs/PLAN.md](docs/PLAN.md) for the full build plan with phased architecture.
263
-
264
281
  ## Contributing
265
282
 
266
- 1. Fork and clone
267
- 2. `bun install && bun test`
268
- 3. Make changes, add tests
269
- 4. `bun test` must pass
270
- 5. PR to `v0` branch
271
-
272
- See [docs/PLAN.md](docs/PLAN.md) for architecture and module layout.
283
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for setup, architecture, testing, and PR guidelines.
273
284
 
274
285
  ## License
275
286
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "livetap",
3
- "version": "0.1.5",
3
+ "version": "0.2.1",
4
4
  "description": "Push live data streams into your AI coding agent. LLM/AI agent? Run: npx livetap --llm-help",
5
5
  "type": "module",
6
6
  "bin": {
@@ -10,9 +10,20 @@
10
10
  "test": "bun test"
11
11
  },
12
12
  "keywords": [
13
- "mqtt", "kafka", "websocket", "webhook", "streaming",
14
- "real-time", "monitoring", "alerts", "mcp", "claude-code",
15
- "ai-agent", "iot", "observability", "data-pipeline"
13
+ "mqtt",
14
+ "kafka",
15
+ "websocket",
16
+ "webhook",
17
+ "streaming",
18
+ "real-time",
19
+ "monitoring",
20
+ "alerts",
21
+ "mcp",
22
+ "claude-code",
23
+ "ai-agent",
24
+ "iot",
25
+ "observability",
26
+ "data-pipeline"
16
27
  ],
17
28
  "license": "MIT",
18
29
  "repository": {
@@ -28,9 +39,7 @@
28
39
  ],
29
40
  "dependencies": {
30
41
  "@modelcontextprotocol/sdk": "^1.28.0",
31
- "ioredis": "^5.10.1",
32
- "mqtt": "^5.15.1",
33
- "redis-server": "^1.2.2"
42
+ "mqtt": "^5.15.1"
34
43
  },
35
44
  "devDependencies": {
36
45
  "@types/bun": "latest"
@@ -4,15 +4,22 @@
4
4
 
5
5
  import { resolve } from 'path'
6
6
  import { homedir } from 'os'
7
- import { existsSync, readFileSync } from 'fs'
7
+ import { existsSync, readFileSync, unlinkSync } from 'fs'
8
8
 
9
- const STATE_PATH = resolve(homedir(), '.livetap', 'state.json')
9
+ const STATE_DIR = resolve(homedir(), '.livetap')
10
+ const PID_PATH = resolve(STATE_DIR, 'daemon.pid')
11
+
12
+ export { PID_PATH, STATE_DIR }
10
13
 
11
14
  export function getDaemonUrl(): string {
12
15
  const port = process.env.LIVETAP_PORT || '8788'
13
16
  return `http://127.0.0.1:${port}`
14
17
  }
15
18
 
19
+ export function getDaemonPort(): number {
20
+ return parseInt(process.env.LIVETAP_PORT || '8788')
21
+ }
22
+
16
23
  export async function isDaemonRunning(): Promise<boolean> {
17
24
  try {
18
25
  const res = await fetch(`${getDaemonUrl()}/status`)
@@ -22,9 +29,37 @@ export async function isDaemonRunning(): Promise<boolean> {
22
29
  }
23
30
  }
24
31
 
25
- export function requireDaemon() {
26
- // Called at start of commands that need the daemon
27
- // Actual check is async, so this just prints the message format
32
+ /**
33
+ * Read PID from daemon.pid file. Returns undefined if missing or stale.
34
+ */
35
+ export function readPid(): number | undefined {
36
+ try {
37
+ if (!existsSync(PID_PATH)) return undefined
38
+ const pid = parseInt(readFileSync(PID_PATH, 'utf-8').trim())
39
+ if (isNaN(pid)) return undefined
40
+ return pid
41
+ } catch {
42
+ return undefined
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Check if a PID is alive.
48
+ */
49
+ export function isPidAlive(pid: number): boolean {
50
+ try {
51
+ process.kill(pid, 0)
52
+ return true
53
+ } catch {
54
+ return false
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Clean up stale PID file.
60
+ */
61
+ export function cleanPidFile(): void {
62
+ try { unlinkSync(PID_PATH) } catch { /* ok */ }
28
63
  }
29
64
 
30
65
  export async function daemonFetch(path: string, opts?: RequestInit): Promise<Response> {
@@ -32,7 +67,7 @@ export async function daemonFetch(path: string, opts?: RequestInit): Promise<Res
32
67
  try {
33
68
  return await fetch(url, opts)
34
69
  } catch (err) {
35
- console.error('Error: livetap daemon is not running. Use "livetap start" first.')
70
+ console.error('Error: livetap daemon is not running. Use "livetap start" or "livetap setup" to start it.')
36
71
  process.exit(1)
37
72
  }
38
73
  }
package/src/cli/setup.ts CHANGED
@@ -1,10 +1,11 @@
1
1
  /**
2
- * livetap setup — creates .mcp.json and prints restart instructions.
2
+ * livetap setup — creates .mcp.json, starts the daemon, and prints restart instructions.
3
3
  * Works with both npm and bun. Node-compatible (no bun APIs).
4
4
  */
5
5
 
6
6
  import { existsSync, readFileSync, writeFileSync } from 'fs'
7
7
  import { resolve } from 'path'
8
+ import { isDaemonRunning } from './daemon-client.js'
8
9
 
9
10
  export async function run(_args: string[]) {
10
11
  const root = process.cwd()
@@ -60,6 +61,28 @@ export async function run(_args: string[]) {
60
61
  console.log('✓ .mcp.json created with livetap MCP server entry')
61
62
  }
62
63
 
64
+ // Start the daemon if not running
65
+ if (await isDaemonRunning()) {
66
+ console.log('✓ Daemon already running')
67
+ } else {
68
+ process.stdout.write(' Starting daemon...')
69
+ // Import and call start.ts run() to start the daemon
70
+ const { run: startDaemon } = await import('./start.js')
71
+ // Capture console.log output from start.ts
72
+ const origLog = console.log
73
+ let startMsg = ''
74
+ console.log = (msg: string) => { startMsg = msg }
75
+ await startDaemon([])
76
+ console.log = origLog
77
+
78
+ if (startMsg.includes('started') || startMsg.includes('already running')) {
79
+ process.stdout.write('\r✓ Daemon started \n')
80
+ } else {
81
+ process.stdout.write('\r✗ Daemon failed to start \n')
82
+ console.error(' Run "livetap start" manually to debug.')
83
+ }
84
+ }
85
+
63
86
  console.log('')
64
87
  console.log('→ Next step: restart Claude Code with:')
65
88
  console.log(' claude --dangerously-load-development-channels server:livetap --continue')
package/src/cli/start.ts CHANGED
@@ -4,12 +4,10 @@
4
4
 
5
5
  import { resolve } from 'path'
6
6
  import { homedir } from 'os'
7
- import { mkdirSync, writeFileSync, existsSync } from 'fs'
8
- import { isDaemonRunning, getDaemonUrl } from './daemon-client.js'
7
+ import { mkdirSync, writeFileSync } from 'fs'
8
+ import { isDaemonRunning, getDaemonUrl, getDaemonPort, PID_PATH, STATE_DIR } from './daemon-client.js'
9
9
 
10
- const STATE_DIR = resolve(homedir(), '.livetap')
11
10
  const LOG_DIR = resolve(STATE_DIR, 'logs')
12
- const STATE_PATH = resolve(STATE_DIR, 'state.json')
13
11
 
14
12
  export async function run(args: string[]) {
15
13
  const foreground = args.includes('--foreground') || args.includes('-f')
@@ -29,21 +27,20 @@ export async function run(args: string[]) {
29
27
 
30
28
  if (foreground) {
31
29
  console.log('Starting livetap in foreground...')
32
- // Import and run directly
33
30
  await import('../server/index.js')
34
31
  return
35
32
  }
36
33
 
37
34
  // Background: spawn detached
38
- const port = process.env.LIVETAP_PORT || '8788'
39
- const logFile = Bun.file(resolve(LOG_DIR, 'daemon.log'))
40
- const logFd = logFile.writer()
35
+ const port = String(getDaemonPort())
36
+ const daemonEntry = resolve(import.meta.dir, '../server/index.ts')
41
37
 
42
- const proc = Bun.spawn(['bun', resolve(import.meta.dir, '../server/index.ts')], {
38
+ const proc = Bun.spawn(['bun', daemonEntry], {
43
39
  env: { ...process.env, LIVETAP_PORT: port },
44
40
  stdout: 'ignore',
45
41
  stderr: 'ignore',
46
42
  })
43
+ proc.unref()
47
44
 
48
45
  // Wait for it to be ready
49
46
  const deadline = Date.now() + 15_000
@@ -52,13 +49,7 @@ export async function run(args: string[]) {
52
49
  try {
53
50
  const res = await fetch(`http://127.0.0.1:${port}/status`)
54
51
  if (res.ok) {
55
- const data = await res.json()
56
- writeFileSync(STATE_PATH, JSON.stringify({
57
- pid: proc.pid,
58
- port: parseInt(port),
59
- redisPort: data.redisPort,
60
- startedAt: new Date().toISOString(),
61
- }, null, 2))
52
+ writeFileSync(PID_PATH, String(proc.pid))
62
53
  ready = true
63
54
  break
64
55
  }