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 +140 -129
- package/package.json +16 -7
- package/src/cli/daemon-client.ts +41 -6
- package/src/cli/setup.ts +24 -1
- package/src/cli/start.ts +7 -16
- package/src/cli/status.ts +22 -4
- package/src/cli/stop.ts +19 -36
- package/src/mcp/channel.ts +26 -8
- package/src/mcp/tools.ts +58 -162
- package/src/server/connection-manager.ts +11 -15
- package/src/server/connections/file.ts +7 -8
- package/src/server/connections/mqtt.ts +9 -10
- package/src/server/connections/webhook.ts +9 -10
- package/src/server/connections/websocket.ts +9 -10
- package/src/server/index.ts +14 -19
- package/src/server/stream-store.ts +128 -0
- package/src/server/watchers/engine.ts +7 -2
- package/src/server/watchers/manager.ts +80 -114
- package/src/shared/canonical/cli.ts +144 -0
- package/src/shared/canonical/index.ts +10 -0
- package/src/shared/canonical/meta.ts +222 -0
- package/src/shared/canonical/tools.ts +178 -0
- package/src/shared/catalog-generators.ts +60 -43
- package/src/shared/command-catalog.ts +3 -147
- package/src/server/redis.ts +0 -62
|
@@ -1,27 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Generates help text, --llm-help JSON, and MCP instructions from
|
|
2
|
+
* Generates help text, --llm-help JSON, and MCP instructions from canonical data.
|
|
3
|
+
* All generators consume the single source of truth in ./canonical/.
|
|
3
4
|
*/
|
|
4
5
|
|
|
5
|
-
import { CLI_COMMANDS, type CatalogCommand } from './
|
|
6
|
-
import {
|
|
6
|
+
import { CLI_COMMANDS, type CatalogCommand, TOOLS, META } from './canonical/index.js'
|
|
7
|
+
import { readFileSync } from 'fs'
|
|
8
|
+
import { resolve } from 'path'
|
|
9
|
+
|
|
10
|
+
const PKG = JSON.parse(readFileSync(resolve(import.meta.dir, '../../package.json'), 'utf-8'))
|
|
11
|
+
const VERSION: string = PKG.version
|
|
7
12
|
|
|
8
13
|
/**
|
|
9
14
|
* Generate human-readable --help text.
|
|
10
15
|
*/
|
|
11
16
|
export function generateHelpText(): string {
|
|
12
17
|
const lines: string[] = [
|
|
13
|
-
|
|
18
|
+
`${META.name} — ${META.description}`,
|
|
14
19
|
'',
|
|
15
20
|
'Usage:',
|
|
16
21
|
]
|
|
17
22
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
{
|
|
21
|
-
{
|
|
22
|
-
{
|
|
23
|
-
{
|
|
24
|
-
{ label: '', commands: ['watch', 'unwatch', 'watchers'] },
|
|
23
|
+
const groups: { commands: string[] }[] = [
|
|
24
|
+
{ commands: ['setup'] },
|
|
25
|
+
{ commands: ['start', 'stop', 'status'] },
|
|
26
|
+
{ commands: ['tap', 'untap', 'taps'] },
|
|
27
|
+
{ commands: ['sip'] },
|
|
28
|
+
{ commands: ['watch', 'unwatch', 'watchers'] },
|
|
25
29
|
]
|
|
26
30
|
|
|
27
31
|
for (const group of groups) {
|
|
@@ -50,23 +54,12 @@ export function generateHelpText(): string {
|
|
|
50
54
|
*/
|
|
51
55
|
export function generateLlmHelp(): object {
|
|
52
56
|
return {
|
|
53
|
-
name:
|
|
54
|
-
version:
|
|
55
|
-
description:
|
|
57
|
+
name: META.name,
|
|
58
|
+
version: VERSION,
|
|
59
|
+
description: META.description,
|
|
56
60
|
setup: {
|
|
57
|
-
steps:
|
|
58
|
-
|
|
59
|
-
'Step 2: npx livetap setup (creates .mcp.json in project root — do NOT create it manually)',
|
|
60
|
-
'Step 3: Tell user to restart Claude Code: claude --dangerously-load-development-channels server:livetap --continue',
|
|
61
|
-
'Step 4: After restart, use create_connection to connect, read_stream to sample, create_watcher to alert',
|
|
62
|
-
],
|
|
63
|
-
do_not: [
|
|
64
|
-
'Do NOT add livetap to ~/.claude/mcp.json — it goes in .mcp.json in the project root',
|
|
65
|
-
'Do NOT configure livetap as type:http — it is a stdio MCP server (command + args)',
|
|
66
|
-
'Do NOT start the daemon manually — the MCP proxy auto-starts it on Claude restart',
|
|
67
|
-
'Do NOT guess field names — always read_stream first to see actual payload structure',
|
|
68
|
-
'Do NOT use npm init — use the existing project directory',
|
|
69
|
-
],
|
|
61
|
+
steps: META.setupSteps,
|
|
62
|
+
do_not: META.doNotRules,
|
|
70
63
|
},
|
|
71
64
|
commands: CLI_COMMANDS,
|
|
72
65
|
mcp_tools: TOOLS,
|
|
@@ -75,21 +68,50 @@ export function generateLlmHelp(): object {
|
|
|
75
68
|
|
|
76
69
|
/**
|
|
77
70
|
* Generate MCP instructions string for the LLM.
|
|
78
|
-
*
|
|
71
|
+
* Assembled entirely from canonical data — no hand-written prose.
|
|
79
72
|
*/
|
|
80
73
|
export function generateInstructions(): string {
|
|
81
74
|
const toolNames = TOOLS.map((t) => t.name)
|
|
82
75
|
const connectionTools = toolNames.filter((n) => n.includes('connection'))
|
|
83
76
|
const watcherTools = toolNames.filter((n) => n.includes('watcher') || n.includes('watch'))
|
|
84
77
|
|
|
78
|
+
// Build source type examples for CONNECT section
|
|
79
|
+
const sourceExamples = META.sourceTypes
|
|
80
|
+
.filter((s) => s.status !== 'built-deferred')
|
|
81
|
+
.map((s) => {
|
|
82
|
+
switch (s.type) {
|
|
83
|
+
case 'mqtt':
|
|
84
|
+
return ` - MQTT: create_connection({ type: "mqtt", broker: "hostname", port: 1883, tls: false, topics: ["topic/#"], username: "", password: "" })`
|
|
85
|
+
case 'websocket':
|
|
86
|
+
return ` - WebSocket: create_connection({ type: "websocket", url: "wss://..." })`
|
|
87
|
+
case 'file':
|
|
88
|
+
return ` - File: create_connection({ type: "file", path: "/var/log/app.log" }) → tails the file for new lines`
|
|
89
|
+
default:
|
|
90
|
+
return ` - ${s.type}: create_connection(${s.params})`
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
.join('\n')
|
|
94
|
+
|
|
95
|
+
// Build operator list from canonical
|
|
96
|
+
const operatorList = META.operators.join(', ')
|
|
97
|
+
|
|
98
|
+
// Build data shape section from canonical
|
|
99
|
+
const dataShapeLines = META.dataShapes.map((ds) => {
|
|
100
|
+
return `- ${ds.source} (${ds.format}): ${ds.description}\n ${ds.usage}`
|
|
101
|
+
}).join('\n')
|
|
102
|
+
|
|
103
|
+
// Build alert guidance from canonical
|
|
104
|
+
const alertGuidanceLines = META.alertGuidance.map((g) => `- ${g}`).join('\n')
|
|
105
|
+
|
|
106
|
+
// Build tips section from canonical
|
|
107
|
+
const tipLines = META.tips.map((t) => `- ${t}`).join('\n')
|
|
108
|
+
|
|
85
109
|
return `
|
|
86
110
|
You have access to LiveTap, a live data streaming tool. Use it to connect to data sources, sample streams, and set up expression-based watchers that alert you when conditions match.
|
|
87
111
|
|
|
88
112
|
WORKFLOW:
|
|
89
113
|
1. CONNECT: Use create_connection to tap into a data source.
|
|
90
|
-
|
|
91
|
-
- WebSocket: create_connection({ type: "websocket", url: "wss://..." })
|
|
92
|
-
- File: create_connection({ type: "file", path: "/var/log/app.log" }) → tails the file for new lines
|
|
114
|
+
${sourceExamples}
|
|
93
115
|
Note: for MQTT, set tls: false and port: 1883 for unencrypted brokers.
|
|
94
116
|
|
|
95
117
|
2. SAMPLE: Use read_stream to inspect what data is flowing.
|
|
@@ -99,7 +121,7 @@ WORKFLOW:
|
|
|
99
121
|
|
|
100
122
|
3. WATCH: Use create_watcher to set up expression-based alerts.
|
|
101
123
|
- create_watcher({ connectionId: "conn_xxx", conditions: [{ field: "sensors.temperature.value", op: ">", value: 50 }], match: "all", cooldown: 60 })
|
|
102
|
-
- Supported operators:
|
|
124
|
+
- Supported operators: ${operatorList} (regex)
|
|
103
125
|
- match: "all" = AND (all conditions must be true), "any" = OR (at least one)
|
|
104
126
|
- cooldown: seconds between repeated alerts. Use 0 for rare events, 30-60 for sensors, 300+ for high-frequency.
|
|
105
127
|
- Alerts arrive as <channel> events. When you see one, act on it as the user requested.
|
|
@@ -107,12 +129,16 @@ WORKFLOW:
|
|
|
107
129
|
4. MANAGE:
|
|
108
130
|
- ${connectionTools.join(', ')} — manage connections
|
|
109
131
|
- ${watcherTools.join(', ')} — manage watchers
|
|
132
|
+
- status — check daemon health, uptime, active connections and watchers
|
|
110
133
|
- Watcher IDs (w_xxx) are globally unique. No connectionId needed for get/update/delete.
|
|
111
134
|
|
|
112
135
|
CHANNEL EVENTS:
|
|
113
136
|
- <channel source="LiveTap" type="alert"> = a watcher condition matched. Read the payload and act on it.
|
|
114
137
|
The payload contains: watcherId, expression, matched_values, and the full stream entry.
|
|
115
138
|
|
|
139
|
+
WHEN AN ALERT FIRES:
|
|
140
|
+
${alertGuidanceLines}
|
|
141
|
+
|
|
116
142
|
When the user asks to "monitor", "watch", or "alert on" something:
|
|
117
143
|
1. First check list_connections — reuse an existing connection if possible
|
|
118
144
|
2. If no connection exists, create one
|
|
@@ -123,19 +149,10 @@ When the user asks to "monitor", "watch", or "alert on" something:
|
|
|
123
149
|
AVAILABLE TOOLS: ${toolNames.join(', ')}
|
|
124
150
|
|
|
125
151
|
DATA SHAPE BY SOURCE:
|
|
126
|
-
|
|
127
|
-
Use dot-paths into the parsed JSON: "sensors.temperature.value", "metadata.device_name"
|
|
128
|
-
- File (plain text lines): entries have { payload: "the raw line", format: "text" }.
|
|
129
|
-
Use field "payload" with contains/matches: { field: "payload", op: "contains", value: "ERROR" }
|
|
130
|
-
or { field: "payload", op: "matches", value: "5[0-9]{2}" }
|
|
131
|
-
- File (JSON lines): entries have { payload: "{...json...}", format: "json" }. Parsed as JSON.
|
|
132
|
-
Use dot-paths like MQTT: "level", "msg", "status"
|
|
152
|
+
${dataShapeLines}
|
|
133
153
|
- IMPORTANT: always use read_stream first to see the actual field names. Do NOT guess — the field is "payload", not "line" or "message".
|
|
134
154
|
|
|
135
155
|
TIPS:
|
|
136
|
-
|
|
137
|
-
- Common MQTT brokers: broker.emqx.io (public demo), test.mosquitto.org (public test).
|
|
138
|
-
- For regex watchers, use the "matches" operator: { field: "payload", op: "matches", value: "ERROR|FATAL" }
|
|
139
|
-
- If a field path doesn't exist in the payload, the condition evaluates to false (no crash, no error).
|
|
156
|
+
${tipLines}
|
|
140
157
|
`.trim()
|
|
141
158
|
}
|
|
@@ -1,149 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* Consumed by:
|
|
5
|
-
* 1. src/mcp/tools.ts → MCP tool registration (imports TOOLS directly)
|
|
6
|
-
* 2. src/mcp/channel.ts → LLM instructions (imports generateInstructions)
|
|
7
|
-
* 3. bin/livetap.ts --help → human-readable help (imports generateHelpText)
|
|
8
|
-
* 4. bin/livetap.ts --llm-help → machine-readable JSON (imports generateLlmHelp)
|
|
2
|
+
* Re-export from canonical source.
|
|
3
|
+
* @deprecated Import from './canonical/index.js' instead.
|
|
9
4
|
*/
|
|
10
|
-
|
|
11
|
-
export interface CatalogCommand {
|
|
12
|
-
name: string
|
|
13
|
-
usage: string
|
|
14
|
-
description: string
|
|
15
|
-
args?: { position: number; name: string; required: boolean; description?: string }[]
|
|
16
|
-
flags?: { name: string; type: string; default?: unknown; description: string }[]
|
|
17
|
-
examples?: string[]
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* All CLI commands. This is the single source — help text and --llm-help
|
|
22
|
-
* are generated from this array.
|
|
23
|
-
*/
|
|
24
|
-
export const CLI_COMMANDS: CatalogCommand[] = [
|
|
25
|
-
// --- Setup ---
|
|
26
|
-
{
|
|
27
|
-
name: 'setup',
|
|
28
|
-
usage: 'livetap setup',
|
|
29
|
-
description: 'Configure .mcp.json for Claude Code and print restart instructions. Run this after npm install.',
|
|
30
|
-
},
|
|
31
|
-
// --- Daemon ---
|
|
32
|
-
{
|
|
33
|
-
name: 'start',
|
|
34
|
-
usage: 'livetap start',
|
|
35
|
-
description: 'Start the livetap daemon (embedded Redis + HTTP API)',
|
|
36
|
-
flags: [
|
|
37
|
-
{ name: '--port', type: 'number', default: 8788, description: 'Daemon port (env: LIVETAP_PORT)' },
|
|
38
|
-
{ name: '--foreground', type: 'boolean', description: 'Run in foreground (don\'t detach)' },
|
|
39
|
-
],
|
|
40
|
-
},
|
|
41
|
-
{
|
|
42
|
-
name: 'stop',
|
|
43
|
-
usage: 'livetap stop',
|
|
44
|
-
description: 'Stop the daemon',
|
|
45
|
-
},
|
|
46
|
-
{
|
|
47
|
-
name: 'status',
|
|
48
|
-
usage: 'livetap status',
|
|
49
|
-
description: 'Show daemon, taps, and watchers',
|
|
50
|
-
flags: [
|
|
51
|
-
{ name: '--json', type: 'boolean', description: 'Output as JSON' },
|
|
52
|
-
],
|
|
53
|
-
},
|
|
54
|
-
// --- Connections ---
|
|
55
|
-
{
|
|
56
|
-
name: 'tap',
|
|
57
|
-
usage: 'livetap tap <uri|file.json>',
|
|
58
|
-
description: 'Tap into a data source (MQTT, WebSocket, file, or webhook)',
|
|
59
|
-
args: [
|
|
60
|
-
{ position: 0, name: 'source', required: true, description: 'URI (mqtt://..., wss://..., file:///path), "webhook", or a .json config file' },
|
|
61
|
-
],
|
|
62
|
-
flags: [
|
|
63
|
-
{ name: '--name', type: 'string', description: 'Display name for the connection' },
|
|
64
|
-
],
|
|
65
|
-
examples: [
|
|
66
|
-
'livetap tap mqtt://broker.emqx.io:1883/sensors/#',
|
|
67
|
-
'livetap tap wss://stream.example.com/prices',
|
|
68
|
-
'livetap tap file:///var/log/nginx/error.log',
|
|
69
|
-
'livetap tap webhook',
|
|
70
|
-
'livetap tap connection.json',
|
|
71
|
-
],
|
|
72
|
-
},
|
|
73
|
-
{
|
|
74
|
-
name: 'untap',
|
|
75
|
-
usage: 'livetap untap <connectionId>',
|
|
76
|
-
description: 'Remove a tap',
|
|
77
|
-
args: [
|
|
78
|
-
{ position: 0, name: 'connectionId', required: true },
|
|
79
|
-
],
|
|
80
|
-
},
|
|
81
|
-
{
|
|
82
|
-
name: 'taps',
|
|
83
|
-
usage: 'livetap taps',
|
|
84
|
-
description: 'List active taps',
|
|
85
|
-
flags: [
|
|
86
|
-
{ name: '--json', type: 'boolean', description: 'Output as JSON' },
|
|
87
|
-
],
|
|
88
|
-
},
|
|
89
|
-
// --- Sampling ---
|
|
90
|
-
{
|
|
91
|
-
name: 'sip',
|
|
92
|
-
usage: 'livetap sip <connectionId>',
|
|
93
|
-
description: 'Sip from a stream (sample recent entries as pretty JSON)',
|
|
94
|
-
args: [
|
|
95
|
-
{ position: 0, name: 'connectionId', required: true },
|
|
96
|
-
],
|
|
97
|
-
flags: [
|
|
98
|
-
{ name: '--max', type: 'number', default: 10, description: 'Max entries to return' },
|
|
99
|
-
{ name: '--back', type: 'number', default: 60, description: 'Backfill seconds' },
|
|
100
|
-
{ name: '--raw', type: 'boolean', description: 'Output raw JSON' },
|
|
101
|
-
],
|
|
102
|
-
},
|
|
103
|
-
// --- Watchers ---
|
|
104
|
-
{
|
|
105
|
-
name: 'watch',
|
|
106
|
-
usage: 'livetap watch <connectionId> "expression"',
|
|
107
|
-
description: 'Create an expression-based watcher',
|
|
108
|
-
args: [
|
|
109
|
-
{ position: 0, name: 'connectionId', required: true },
|
|
110
|
-
{ position: 1, name: 'expression', required: true, description: '"field > value", supports AND/OR' },
|
|
111
|
-
],
|
|
112
|
-
flags: [
|
|
113
|
-
{ name: '--cooldown', type: 'number', default: 60, description: 'Seconds between repeated alerts (0 = every match)' },
|
|
114
|
-
{ name: '--action', type: 'string', description: '"channel_alert" (default), "webhook:URL", or "shell:command"' },
|
|
115
|
-
],
|
|
116
|
-
examples: [
|
|
117
|
-
'livetap watch conn_abc "temperature > 50"',
|
|
118
|
-
'livetap watch conn_abc "temp > 50 AND humidity > 90"',
|
|
119
|
-
'livetap watch conn_abc "temp > 50 OR smoke > 0.05"',
|
|
120
|
-
'livetap watch conn_abc "price > 70000" --cooldown 300',
|
|
121
|
-
],
|
|
122
|
-
},
|
|
123
|
-
{
|
|
124
|
-
name: 'unwatch',
|
|
125
|
-
usage: 'livetap unwatch <watcherId>',
|
|
126
|
-
description: 'Remove a watcher',
|
|
127
|
-
args: [
|
|
128
|
-
{ position: 0, name: 'watcherId', required: true },
|
|
129
|
-
],
|
|
130
|
-
},
|
|
131
|
-
{
|
|
132
|
-
name: 'watchers',
|
|
133
|
-
usage: 'livetap watchers [connectionId|watcherId]',
|
|
134
|
-
description: 'List watchers, show watcher details, or view watcher logs',
|
|
135
|
-
args: [
|
|
136
|
-
{ position: 0, name: 'connectionId or watcherId', required: false, description: 'Filter by connection (conn_xxx) or show details for a watcher (w_xxx)' },
|
|
137
|
-
],
|
|
138
|
-
flags: [
|
|
139
|
-
{ name: '--json', type: 'boolean', description: 'Output as JSON' },
|
|
140
|
-
{ name: '--logs', type: 'string', description: 'Show evaluation logs for a watcher ID' },
|
|
141
|
-
],
|
|
142
|
-
examples: [
|
|
143
|
-
'livetap watchers',
|
|
144
|
-
'livetap watchers conn_abc',
|
|
145
|
-
'livetap watchers w_abc',
|
|
146
|
-
'livetap watchers --logs w_abc',
|
|
147
|
-
],
|
|
148
|
-
},
|
|
149
|
-
]
|
|
5
|
+
export { CLI_COMMANDS, type CatalogCommand } from './canonical/cli.js'
|
package/src/server/redis.ts
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Embedded Redis lifecycle manager.
|
|
3
|
-
* Uses the `redis-server` npm package which bundles a Redis binary.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import RedisServer from 'redis-server'
|
|
7
|
-
import Redis from 'ioredis'
|
|
8
|
-
|
|
9
|
-
export interface RedisManager {
|
|
10
|
-
port: number
|
|
11
|
-
url: string
|
|
12
|
-
client: Redis
|
|
13
|
-
stop(): Promise<void>
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Start an embedded redis-server on the given port (or random free port).
|
|
18
|
-
* Returns a manager with a connected ioredis client.
|
|
19
|
-
*/
|
|
20
|
-
export async function startRedis(preferredPort?: number): Promise<RedisManager> {
|
|
21
|
-
const port = preferredPort ?? await findFreePort()
|
|
22
|
-
|
|
23
|
-
const server = new RedisServer({ port })
|
|
24
|
-
|
|
25
|
-
await new Promise<void>((resolve, reject) => {
|
|
26
|
-
server.open((err: Error | null) => {
|
|
27
|
-
if (err) reject(err)
|
|
28
|
-
else resolve()
|
|
29
|
-
})
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
const url = `redis://127.0.0.1:${port}`
|
|
33
|
-
const client = new Redis(url, { maxRetriesPerRequest: 3, lazyConnect: false })
|
|
34
|
-
|
|
35
|
-
await client.ping()
|
|
36
|
-
|
|
37
|
-
return {
|
|
38
|
-
port,
|
|
39
|
-
url,
|
|
40
|
-
client,
|
|
41
|
-
async stop() {
|
|
42
|
-
client.disconnect()
|
|
43
|
-
await new Promise<void>((resolve, reject) => {
|
|
44
|
-
server.close((err: Error | null) => {
|
|
45
|
-
if (err) reject(err)
|
|
46
|
-
else resolve()
|
|
47
|
-
})
|
|
48
|
-
})
|
|
49
|
-
},
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
async function findFreePort(): Promise<number> {
|
|
54
|
-
const server = Bun.listen({
|
|
55
|
-
hostname: '127.0.0.1',
|
|
56
|
-
port: 0,
|
|
57
|
-
socket: { data() {} },
|
|
58
|
-
})
|
|
59
|
-
const port = server.port
|
|
60
|
-
server.stop()
|
|
61
|
-
return port
|
|
62
|
-
}
|