livetap 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 livetap contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,223 @@
1
+ # livetap
2
+
3
+ > Push live data streams into your AI coding agent.
4
+
5
+ Connect MQTT brokers, WebSocket feeds, or tail log files. Your agent samples, watches, and acts on real-time data through natural language.
6
+
7
+ <!-- TODO: Add demo GIF here after recording -->
8
+ <!-- ![livetap demo](demo.gif) -->
9
+
10
+ ## Quick start
11
+
12
+ **Requirements:** [Bun](https://bun.sh), [Redis](https://redis.io/) (`brew install redis`), Claude Code v2.1.80+
13
+
14
+ ```bash
15
+ bun add livetap
16
+ ```
17
+
18
+ Start the daemon and Claude Code:
19
+
20
+ ```bash
21
+ livetap start
22
+ claude --dangerously-load-development-channels server:livetap
23
+ ```
24
+
25
+ Then ask your agent:
26
+
27
+ > "Connect to the IoT demo at mqtt://broker.emqx.io on topic justinx/demo/# and watch for temperature above 23 degrees"
28
+
29
+ ## What it does
30
+
31
+ 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.
32
+
33
+ ```
34
+ Source (MQTT/WS/File) ──> Subscriber ──> Redis Stream ──> Watcher Engine
35
+ | |
36
+ v v (on match)
37
+ read_stream Channel Alert
38
+ (agent samples) ──> Claude Code
39
+ ──> agent acts
40
+ ```
41
+
42
+ ## Examples
43
+
44
+ ### IoT sensor monitoring
45
+
46
+ ```
47
+ You: "Connect to mqtt://broker.emqx.io:1883/justinx/demo/# and watch for temperature above 25°C"
48
+
49
+ Agent: Creates connection, samples the data to learn the payload structure,
50
+ sets up watcher on sensors.environmental.temperature.value > 25.
51
+ When it fires, summarizes: "sensor-zone-c hit 25.4°C at 10:05:08Z"
52
+ ```
53
+
54
+ ### Crypto price alerts
55
+
56
+ ```
57
+ You: "Tap the Binance BTC/USDT trade stream and alert me if price drops below 60000"
58
+
59
+ Agent: Connects to wss://stream.binance.com:9443/ws/btcusdt@trade,
60
+ samples to see the price field is "p" (string),
61
+ sets up watcher with regex: p matches "^[1-5]" (prices starting with 1-5 = below 60k)
62
+ ```
63
+
64
+ ### Log file monitoring
65
+
66
+ ```
67
+ You: "Watch my nginx error log for 5xx errors and summarize each one"
68
+
69
+ Agent: Taps file:///var/log/nginx/error.log,
70
+ creates watcher: payload matches "5[0-9]{2}",
71
+ when an error appears, analyzes it:
72
+ "503 Service Unavailable on /api/data — upstream auth-service not responding"
73
+ ```
74
+
75
+ ### WiFi disconnect detection
76
+
77
+ ```
78
+ You: "Monitor /var/log/wifi.log and alert me when WiFi drops"
79
+
80
+ Agent: Taps the file, samples to see log format, creates regex watcher
81
+ for power state changes. When you toggle WiFi:
82
+ "Wi-Fi powered OFF at 17:51:45, back ON at 17:51:47 (2s outage)"
83
+ ```
84
+
85
+ ## CLI
86
+
87
+ ```bash
88
+ # Daemon
89
+ livetap start # Start (embedded Redis + API)
90
+ livetap stop # Stop
91
+ livetap status # Dashboard
92
+
93
+ # Tap into data sources
94
+ livetap tap mqtt://broker.emqx.io:1883/sensors/# # MQTT broker
95
+ livetap tap wss://stream.binance.com:9443/ws/btcusdt@trade # WebSocket
96
+ livetap tap file:///var/log/nginx/error.log # Log file
97
+ livetap taps # List active taps
98
+ livetap untap <connectionId> # Remove
99
+
100
+ # Sample data
101
+ livetap sip <connectionId> # Pretty JSON output
102
+ livetap sip <connectionId> --raw # Raw JSON
103
+
104
+ # Watchers
105
+ livetap watch <connId> "temperature > 50" # Numeric
106
+ livetap watch <connId> "payload matches 'ERROR|FATAL'" # Regex
107
+ livetap watch <connId> "temp > 50 AND humidity > 90" # AND
108
+ livetap watch <connId> "price > 70000" --cooldown 300 # Custom cooldown
109
+ livetap watchers # List all
110
+ livetap watchers --logs <watcherId> # View logs
111
+ livetap unwatch <watcherId> # Remove
112
+ ```
113
+
114
+ ## Supported sources
115
+
116
+ | Protocol | Example | Use case |
117
+ |----------|---------|----------|
118
+ | **MQTT** | `livetap tap mqtt://broker.emqx.io:1883/sensors/#` | IoT sensors, home automation |
119
+ | **WebSocket** | `livetap tap wss://stream.binance.com:9443/ws/btcusdt@trade` | Finance, real-time APIs |
120
+ | **File tailing** | `livetap tap file:///var/log/nginx/error.log` | Log monitoring, DevOps |
121
+ | Webhooks | Planned v0.1 | CI/CD, external services |
122
+ | Kafka | Planned v0.2 | Event sourcing, analytics |
123
+
124
+ ## MCP tools
125
+
126
+ livetap exposes 12 MCP tools that your agent uses automatically:
127
+
128
+ | Tool | What it does |
129
+ |------|-------------|
130
+ | `create_connection` | Connect to MQTT, WebSocket, or file |
131
+ | `list_connections` | List active connections |
132
+ | `get_connection` | Connection details and stats |
133
+ | `destroy_connection` | Remove a connection |
134
+ | `read_stream` | Sample recent entries from a stream |
135
+ | `create_watcher` | Set up expression-based alerts |
136
+ | `list_watchers` | List watchers |
137
+ | `get_watcher` | Watcher details |
138
+ | `get_watcher_logs` | View MATCH/SUPPRESSED logs |
139
+ | `update_watcher` | Change conditions or cooldown |
140
+ | `delete_watcher` | Remove a watcher |
141
+ | `restart_watcher` | Restart a stopped watcher |
142
+
143
+ ## Expression watchers
144
+
145
+ Watchers use structured conditions — not arbitrary code:
146
+
147
+ ```json
148
+ {
149
+ "conditions": [
150
+ { "field": "sensors.temperature.value", "op": ">", "value": 50 },
151
+ { "field": "sensors.humidity.value", "op": ">", "value": 90 }
152
+ ],
153
+ "match": "all",
154
+ "cooldown": 60
155
+ }
156
+ ```
157
+
158
+ **Operators:** `>`, `<`, `>=`, `<=`, `==`, `!=`, `contains`, `matches` (regex)
159
+
160
+ **Cooldown:** Seconds between repeated alerts. `0` for every match, `60` default.
161
+
162
+ 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.
163
+
164
+ ## How the agent uses livetap
165
+
166
+ The MCP instructions teach the agent this workflow:
167
+
168
+ 1. **CONNECT** — `create_connection` to tap a source
169
+ 2. **SAMPLE** — `read_stream` to see the data shape (always before creating watchers)
170
+ 3. **WATCH** — `create_watcher` with the correct field paths
171
+ 4. **ACT** — when `<channel>` alerts arrive, do what the user asked
172
+
173
+ The agent knows field paths differ by source:
174
+ - **MQTT/WebSocket:** JSON payload parsed — use dot-paths like `sensors.temperature.value`
175
+ - **File (text):** raw line in `payload` field — use `payload contains "ERROR"` or `payload matches "5[0-9]{2}"`
176
+ - **File (JSON lines):** parsed — use dot-paths like `level`, `msg`
177
+
178
+ ## Configuration
179
+
180
+ **Daemon port:** Default `:8788`. Override with `--port` or `LIVETAP_PORT` env var.
181
+
182
+ **State directory:** `~/.livetap/` stores daemon PID, logs, and watcher evaluation logs.
183
+
184
+ **MCP config:** `.mcp.json` in your project root:
185
+ ```json
186
+ {
187
+ "mcpServers": {
188
+ "livetap": {
189
+ "command": "bun",
190
+ "args": ["path/to/src/mcp/channel.ts"]
191
+ }
192
+ }
193
+ }
194
+ ```
195
+
196
+ **Machine-readable help:** `livetap --llm-help` outputs structured JSON for AI agents.
197
+
198
+ ## Development
199
+
200
+ ```bash
201
+ git clone https://github.com/livetap/livetap.git
202
+ cd livetap && git checkout v0
203
+ bun install
204
+ bun test # 103 tests
205
+ bun test tests/phase1/ # Specific phase
206
+ SKIP_LIVE_MQTT=1 bun test # Skip tests needing broker.emqx.io
207
+ ```
208
+
209
+ See [docs/PLAN.md](docs/PLAN.md) for the full build plan with phased architecture.
210
+
211
+ ## Contributing
212
+
213
+ 1. Fork and clone
214
+ 2. `bun install && bun test`
215
+ 3. Make changes, add tests
216
+ 4. `bun test` must pass
217
+ 5. PR to `v0` branch
218
+
219
+ See [docs/PLAN.md](docs/PLAN.md) for architecture and module layout.
220
+
221
+ ## License
222
+
223
+ MIT
package/bin/livetap.ts ADDED
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * livetap CLI entry point.
4
+ */
5
+
6
+ const [cmd, ...args] = Bun.argv.slice(2)
7
+
8
+ const commands: Record<string, () => Promise<void>> = {
9
+ start: () => import('../src/cli/start.js').then((m) => m.run(args)),
10
+ stop: () => import('../src/cli/stop.js').then((m) => m.run(args)),
11
+ status: () => import('../src/cli/status.js').then((m) => m.run(args)),
12
+ tap: () => import('../src/cli/tap.js').then((m) => m.run(args)),
13
+ untap: () => import('../src/cli/untap.js').then((m) => m.run(args)),
14
+ taps: () => import('../src/cli/taps.js').then((m) => m.run(args)),
15
+ sip: () => import('../src/cli/sip.js').then((m) => m.run(args)),
16
+ watch: () => import('../src/cli/watch.js').then((m) => m.run(args)),
17
+ unwatch: () => import('../src/cli/unwatch.js').then((m) => m.run(args)),
18
+ watchers: () => import('../src/cli/watchers.js').then((m) => m.run(args)),
19
+ mcp: () => import('../src/mcp/channel.js'),
20
+ help: () => import('../src/cli/help.js').then((m) => m.run(args)),
21
+ }
22
+
23
+ if (!cmd || cmd === '--help' || cmd === '-h') {
24
+ await commands.help()
25
+ } else if (cmd === '--llm-help') {
26
+ const { generateLlmHelp } = await import('../src/shared/catalog-generators.js')
27
+ console.log(JSON.stringify(generateLlmHelp(), null, 2))
28
+ } else if (commands[cmd]) {
29
+ await commands[cmd]()
30
+ } else {
31
+ console.error(`Unknown command: ${cmd}. Run 'livetap help' for usage.`)
32
+ process.exit(1)
33
+ }
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "livetap",
3
+ "version": "0.1.0",
4
+ "description": "Push live data streams into your AI coding agent. Connect MQTT, WebSocket, or webhooks.",
5
+ "type": "module",
6
+ "bin": {
7
+ "livetap": "./bin/livetap.ts"
8
+ },
9
+ "scripts": {
10
+ "postinstall": "bun ./scripts/postinstall.ts",
11
+ "test": "bun test"
12
+ },
13
+ "keywords": [
14
+ "mqtt", "kafka", "websocket", "webhook", "streaming",
15
+ "real-time", "monitoring", "alerts", "mcp", "claude-code",
16
+ "ai-agent", "iot", "observability", "data-pipeline"
17
+ ],
18
+ "license": "MIT",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "https://github.com/livetap/livetap"
22
+ },
23
+ "homepage": "https://github.com/livetap/livetap",
24
+ "files": [
25
+ "bin/",
26
+ "src/",
27
+ "scripts/postinstall.ts",
28
+ "README.md",
29
+ "LICENSE"
30
+ ],
31
+ "dependencies": {
32
+ "@modelcontextprotocol/sdk": "^1.28.0",
33
+ "ioredis": "^5.10.1",
34
+ "mqtt": "^5.15.1",
35
+ "redis-server": "^1.2.2"
36
+ },
37
+ "devDependencies": {
38
+ "@types/bun": "latest"
39
+ },
40
+ "peerDependencies": {
41
+ "typescript": "^5"
42
+ }
43
+ }
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Postinstall script — auto-configures .mcp.json for Claude Code.
4
+ * Runs after `bun add livetap` or `npm install livetap`.
5
+ */
6
+
7
+ import { existsSync, readFileSync, writeFileSync } from 'fs'
8
+ import { resolve } from 'path'
9
+
10
+ const MCP_ENTRY = {
11
+ livetap: {
12
+ command: 'bun',
13
+ args: [resolve(import.meta.dir, '..', 'src', 'mcp', 'channel.ts')],
14
+ },
15
+ }
16
+
17
+ function findProjectRoot(): string {
18
+ // If we're inside node_modules, walk up past it to find the project root
19
+ const scriptDir = resolve(import.meta.dir)
20
+ const nmIdx = scriptDir.lastIndexOf('node_modules')
21
+ if (nmIdx !== -1) {
22
+ const root = scriptDir.slice(0, nmIdx).replace(/\/$/, '')
23
+ if (existsSync(resolve(root, 'package.json'))) return root
24
+ }
25
+ // Fallback to cwd (e.g. when running postinstall directly for testing)
26
+ return process.cwd()
27
+ }
28
+
29
+ function run() {
30
+ // Skip in CI or when explicitly disabled
31
+ if (process.env.CI || process.env.LIVETAP_SKIP_POSTINSTALL) return
32
+
33
+ const root = findProjectRoot()
34
+ const mcpPath = resolve(root, '.mcp.json')
35
+
36
+ let config: any = {}
37
+ if (existsSync(mcpPath)) {
38
+ try {
39
+ config = JSON.parse(readFileSync(mcpPath, 'utf-8'))
40
+ } catch {
41
+ console.warn('\n ⚠ .mcp.json exists but is malformed. Skipping auto-config.')
42
+ console.warn(' Add the livetap entry manually (see below).\n')
43
+ printManualInstructions()
44
+ return
45
+ }
46
+ }
47
+
48
+ // Don't overwrite if livetap entry already exists
49
+ if (config.mcpServers?.livetap) {
50
+ console.log('\n ✓ livetap already configured in .mcp.json\n')
51
+ printRestartInstructions()
52
+ return
53
+ }
54
+
55
+ // Add livetap entry
56
+ if (!config.mcpServers) config.mcpServers = {}
57
+ config.mcpServers.livetap = MCP_ENTRY.livetap
58
+ writeFileSync(mcpPath, JSON.stringify(config, null, 2) + '\n')
59
+
60
+ console.log('\n ✓ livetap added to .mcp.json\n')
61
+ printRestartInstructions()
62
+ }
63
+
64
+ function printRestartInstructions() {
65
+ console.log(' To enable live data streaming in Claude Code, restart with:\n')
66
+ console.log(' claude --dangerously-load-development-channels server:livetap\n')
67
+ console.log(' Then ask Claude: "Connect to mqtt://broker.emqx.io:1883/sensors/#"\n')
68
+ }
69
+
70
+ function printManualInstructions() {
71
+ console.log(' Add to .mcp.json:\n')
72
+ console.log(' ' + JSON.stringify({ mcpServers: MCP_ENTRY }, null, 2).split('\n').join('\n ') + '\n')
73
+ }
74
+
75
+ run()
@@ -0,0 +1,43 @@
1
+ /**
2
+ * HTTP client for talking to the livetap daemon.
3
+ */
4
+
5
+ import { resolve } from 'path'
6
+ import { homedir } from 'os'
7
+ import { existsSync, readFileSync } from 'fs'
8
+
9
+ const STATE_PATH = resolve(homedir(), '.livetap', 'state.json')
10
+
11
+ export function getDaemonUrl(): string {
12
+ const port = process.env.LIVETAP_PORT || '8788'
13
+ return `http://127.0.0.1:${port}`
14
+ }
15
+
16
+ export async function isDaemonRunning(): Promise<boolean> {
17
+ try {
18
+ const res = await fetch(`${getDaemonUrl()}/status`)
19
+ return res.ok
20
+ } catch {
21
+ return false
22
+ }
23
+ }
24
+
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
28
+ }
29
+
30
+ export async function daemonFetch(path: string, opts?: RequestInit): Promise<Response> {
31
+ const url = `${getDaemonUrl()}${path}`
32
+ try {
33
+ return await fetch(url, opts)
34
+ } catch (err) {
35
+ console.error('Error: livetap daemon is not running. Use "livetap start" first.')
36
+ process.exit(1)
37
+ }
38
+ }
39
+
40
+ export async function daemonJson(path: string, opts?: RequestInit): Promise<any> {
41
+ const res = await daemonFetch(path, opts)
42
+ return res.json()
43
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * livetap help — generated from command catalog.
3
+ */
4
+
5
+ import { generateHelpText } from '../shared/catalog-generators.js'
6
+
7
+ export async function run(_args: string[]) {
8
+ console.log(generateHelpText())
9
+ }
package/src/cli/sip.ts ADDED
@@ -0,0 +1,63 @@
1
+ /**
2
+ * livetap sip <connectionId> — Sample recent stream entries.
3
+ */
4
+
5
+ import { daemonFetch } from './daemon-client.js'
6
+
7
+ export async function run(args: string[]) {
8
+ const id = args[0]
9
+ if (!id) {
10
+ console.error('Usage: livetap sip <connectionId>')
11
+ process.exit(1)
12
+ }
13
+
14
+ const rawMode = args.includes('--raw')
15
+ const maxIdx = args.indexOf('--max')
16
+ const maxEntries = maxIdx !== -1 ? parseInt(args[maxIdx + 1]) : 10
17
+ const backIdx = args.indexOf('--back')
18
+ const backfillSeconds = backIdx !== -1 ? parseInt(args[backIdx + 1]) : 60
19
+
20
+ const params = new URLSearchParams({
21
+ backfillSeconds: String(backfillSeconds),
22
+ maxEntries: String(maxEntries),
23
+ })
24
+
25
+ const res = await daemonFetch(`/connections/${id}/stream?${params}`)
26
+ const data = await res.json()
27
+
28
+ if (!res.ok) {
29
+ console.error(`Error: ${data.error}`)
30
+ process.exit(1)
31
+ }
32
+
33
+ if (rawMode) {
34
+ console.log(JSON.stringify(data, null, 2))
35
+ return
36
+ }
37
+
38
+ const entries = data.entries || []
39
+ if (entries.length === 0) {
40
+ console.log('No entries yet. Data may still be flowing in — try again in a few seconds.')
41
+ return
42
+ }
43
+
44
+ console.log(`${entries.length} entries:\n`)
45
+ for (const entry of entries) {
46
+ const time = new Date(entry.ts).toISOString().slice(11, 19)
47
+ const topic = entry.fields.topic || ''
48
+ const payload = entry.fields.payload || ''
49
+
50
+ console.log(`[${time}]${topic ? ` topic=${topic}` : ''}`)
51
+
52
+ // Pretty-print JSON payload with indentation
53
+ try {
54
+ const parsed = JSON.parse(payload)
55
+ const pretty = JSON.stringify(parsed, null, 2)
56
+ const indented = pretty.split('\n').map((l) => ` ${l}`).join('\n')
57
+ console.log(indented)
58
+ } catch {
59
+ console.log(` ${payload}`)
60
+ }
61
+ console.log()
62
+ }
63
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * livetap start — Start the daemon in background or foreground.
3
+ */
4
+
5
+ import { resolve } from 'path'
6
+ import { homedir } from 'os'
7
+ import { mkdirSync, writeFileSync, existsSync } from 'fs'
8
+ import { isDaemonRunning, getDaemonUrl } from './daemon-client.js'
9
+
10
+ const STATE_DIR = resolve(homedir(), '.livetap')
11
+ const LOG_DIR = resolve(STATE_DIR, 'logs')
12
+ const STATE_PATH = resolve(STATE_DIR, 'state.json')
13
+
14
+ export async function run(args: string[]) {
15
+ const foreground = args.includes('--foreground') || args.includes('-f')
16
+ const portFlag = args.indexOf('--port')
17
+ if (portFlag !== -1 && args[portFlag + 1]) {
18
+ process.env.LIVETAP_PORT = args[portFlag + 1]
19
+ }
20
+
21
+ if (await isDaemonRunning()) {
22
+ const res = await fetch(`${getDaemonUrl()}/status`)
23
+ const status = await res.json()
24
+ console.log(`livetap is already running on :${status.port} (${status.connections.length} connections)`)
25
+ return
26
+ }
27
+
28
+ mkdirSync(LOG_DIR, { recursive: true })
29
+
30
+ if (foreground) {
31
+ console.log('Starting livetap in foreground...')
32
+ // Import and run directly
33
+ await import('../server/index.js')
34
+ return
35
+ }
36
+
37
+ // 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()
41
+
42
+ const proc = Bun.spawn(['bun', resolve(import.meta.dir, '../server/index.ts')], {
43
+ env: { ...process.env, LIVETAP_PORT: port },
44
+ stdout: 'ignore',
45
+ stderr: 'ignore',
46
+ })
47
+
48
+ // Wait for it to be ready
49
+ const deadline = Date.now() + 15_000
50
+ let ready = false
51
+ while (Date.now() < deadline) {
52
+ try {
53
+ const res = await fetch(`http://127.0.0.1:${port}/status`)
54
+ 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))
62
+ ready = true
63
+ break
64
+ }
65
+ } catch { /* not ready */ }
66
+ await new Promise((r) => setTimeout(r, 500))
67
+ }
68
+
69
+ if (ready) {
70
+ console.log(`livetap daemon started on :${port} (pid ${proc.pid})`)
71
+ } else {
72
+ console.error('Error: daemon failed to start within 15s. Check ~/.livetap/logs/daemon.log')
73
+ process.exit(1)
74
+ }
75
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * livetap status — Show daemon, connections, and watchers.
3
+ */
4
+
5
+ import { isDaemonRunning, daemonJson } from './daemon-client.js'
6
+
7
+ export async function run(args: string[]) {
8
+ if (!(await isDaemonRunning())) {
9
+ console.log('livetap is not running. Use "livetap start" to begin.')
10
+ return
11
+ }
12
+
13
+ const jsonMode = args.includes('--json')
14
+ const data = await daemonJson('/status')
15
+
16
+ if (jsonMode) {
17
+ console.log(JSON.stringify(data, null, 2))
18
+ return
19
+ }
20
+
21
+ const uptime = formatUptime(data.uptime)
22
+ console.log(`livetap daemon running on :${data.port} (uptime ${uptime})`)
23
+ console.log(`Redis: localhost:${data.redisPort}\n`)
24
+
25
+ const conns = data.connections || []
26
+ if (conns.length === 0) {
27
+ console.log('No active connections. Use "livetap tap <uri>" to connect.\n')
28
+ } else {
29
+ console.log(`Connections (${conns.length}):`)
30
+ for (const c of conns) {
31
+ const rate = `${c.msgPerSec} msg/s`
32
+ const buf = `${c.bufferedCount} buffered`
33
+ console.log(` ${c.connectionId} ${c.type.padEnd(9)} ${c.summary.slice(0, 40).padEnd(42)} ${rate.padStart(10)} ${buf}`)
34
+ }
35
+ console.log()
36
+ }
37
+ }
38
+
39
+ function formatUptime(seconds: number): string {
40
+ if (seconds < 60) return `${Math.round(seconds)}s`
41
+ if (seconds < 3600) return `${Math.round(seconds / 60)}m`
42
+ const h = Math.floor(seconds / 3600)
43
+ const m = Math.round((seconds % 3600) / 60)
44
+ return `${h}h ${m}m`
45
+ }