wicked-bus 1.0.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/README.md ADDED
@@ -0,0 +1,153 @@
1
+ ```
2
+ ██╗ ██╗██╗ ██████╗██╗ ██╗███████╗██████╗ ██████╗ ██╗ ██╗███████╗
3
+ ██║ ██║██║██╔════╝██║ ██╔╝██╔════╝██╔══██╗ ██╔══██╗██║ ██║██╔════╝
4
+ ██║ █╗ ██║██║██║ █████╔╝ █████╗ ██║ ██║ ██████╔╝██║ ██║███████╗
5
+ ██║███╗██║██║██║ ██╔═██╗ ██╔══╝ ██║ ██║ ██╔══██╗██║ ██║╚════██║
6
+ ╚███╔███╔╝██║╚██████╗██║ ██╗███████╗██████╔╝ ██████╔╝╚██████╔╝███████║
7
+ ╚══╝╚══╝ ╚═╝ ╚═════╝╚═╝ ╚═╝╚══════╝╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝
8
+ ```
9
+
10
+ A lightweight, local-first event bus for AI agents and developer tools.
11
+
12
+ SQLite-backed, single-host, poll-based delivery with at-least-once semantics. No servers, no network transport, no infrastructure. Events stay on your machine.
13
+
14
+ Built for agent ecosystems where multiple tools need to communicate without coupling to each other — AI coding assistants, test runners, knowledge systems, deployment tools, or anything that benefits from local event-driven architecture.
15
+
16
+ ## Quick Start
17
+
18
+ ### Install
19
+
20
+ ```bash
21
+ npm install wicked-bus
22
+ ```
23
+
24
+ `better-sqlite3` is a required peer dependency (compiles a native addon).
25
+
26
+ ### Initialize
27
+
28
+ ```bash
29
+ wicked-bus init
30
+ ```
31
+
32
+ Creates `~/.something-wicked/wicked-bus/` with a WAL-mode SQLite database.
33
+
34
+ ### Emit an event
35
+
36
+ ```bash
37
+ wicked-bus emit \
38
+ --type wicked.task.completed \
39
+ --domain my-plugin \
40
+ --payload '{"taskId": "abc", "status": "done"}'
41
+ ```
42
+
43
+ ### Subscribe to events
44
+
45
+ ```bash
46
+ wicked-bus subscribe --filter 'wicked.task.*'
47
+ ```
48
+
49
+ Streams events as NDJSON. Use `--filter` with wildcards and `@domain` scoping.
50
+
51
+ ## Programmatic API
52
+
53
+ ```javascript
54
+ import { emit, poll, ack, register } from 'wicked-bus';
55
+ import { loadConfig } from 'wicked-bus/lib/config.js';
56
+ import { openDb } from 'wicked-bus/lib/db.js';
57
+
58
+ const config = loadConfig();
59
+ const db = openDb(config);
60
+
61
+ // Emit
62
+ const result = emit(db, config, {
63
+ event_type: 'wicked.deploy.completed',
64
+ domain: 'my-deploy',
65
+ subdomain: 'deploy.production',
66
+ payload: { version: '2.0.0' },
67
+ });
68
+
69
+ // Subscribe
70
+ const sub = register(db, {
71
+ plugin: 'my-consumer',
72
+ role: 'subscriber',
73
+ event_type_filter: 'wicked.deploy.*',
74
+ cursor_init: 'latest',
75
+ });
76
+
77
+ // Poll
78
+ const events = poll(db, config, {
79
+ cursor_id: sub.cursor_id,
80
+ filter: 'wicked.deploy.*',
81
+ });
82
+
83
+ // Acknowledge
84
+ if (events.events.length > 0) {
85
+ const lastId = events.events.at(-1).event_id;
86
+ ack(db, { cursor_id: sub.cursor_id, event_id: lastId });
87
+ }
88
+
89
+ db.close();
90
+ ```
91
+
92
+ ## CLI Commands
93
+
94
+ | Command | Description |
95
+ |---------|-------------|
96
+ | `init` | Create data directory and database |
97
+ | `emit` | Publish an event |
98
+ | `subscribe` | Stream events matching a filter |
99
+ | `status` | Show bus health and stats |
100
+ | `register` | Register as provider or subscriber |
101
+ | `deregister` | Soft-delete a registration |
102
+ | `list` | List registrations |
103
+ | `ack` | Acknowledge events (advance cursor) |
104
+ | `replay` | Reset a cursor to a specific position |
105
+ | `cleanup` | Run TTL sweep (delete expired events) |
106
+
107
+ All commands output structured JSON. Errors go to stderr with error codes (WB-001 through WB-006).
108
+
109
+ ## AI CLI Skills
110
+
111
+ wicked-bus ships skills for AI coding assistants (Claude, Gemini, Copilot, Codex, Cursor).
112
+
113
+ ### Install skills
114
+
115
+ ```bash
116
+ npx wicked-bus-install
117
+ ```
118
+
119
+ Auto-detects installed CLIs and copies skills. Available skills:
120
+
121
+ | Skill | Purpose |
122
+ |-------|---------|
123
+ | `wicked-bus/init` | Initialize or connect to the bus |
124
+ | `wicked-bus/emit` | Publish events |
125
+ | `wicked-bus/subscribe` | Consume events |
126
+ | `wicked-bus/naming` | Event naming conventions |
127
+ | `wicked-bus/query` | Query and debug |
128
+
129
+ ## Why wicked-bus?
130
+
131
+ Agent ecosystems have a communication problem. Tools that should work together — test runners, code reviewers, knowledge systems, deployment pipelines — end up tightly coupled or completely siloed. wicked-bus solves this with a dead-simple local event bridge.
132
+
133
+ - **Local-first**: everything lives in a single SQLite file. No servers to run, no ports to manage, no infrastructure.
134
+ - **At-least-once delivery**: cursors persist across restarts. Unacked events are re-delivered. No lost events.
135
+ - **Fire-and-forget**: producers are non-blocking. The bus never slows the caller. If it's not installed, callers degrade gracefully.
136
+ - **Agent-native**: designed for AI coding assistants and the tools around them. Ships with skills for Claude, Gemini, Copilot, Codex, and Cursor.
137
+ - **Two-timer TTL**: events auto-expire. No manual cleanup, no unbounded growth.
138
+
139
+ ## Documentation
140
+
141
+ - [ARCHITECTURE.md](./ARCHITECTURE.md) -- system design and module structure
142
+ - [USERS_GUIDE.md](./USERS_GUIDE.md) -- event naming, payload conventions, integration patterns
143
+ - [reqs/SPEC.md](./reqs/SPEC.md) -- full specification
144
+
145
+ ## Requirements
146
+
147
+ - Node.js >= 18.0.0
148
+ - `better-sqlite3` >= 9.0.0 (peer dependency)
149
+ - macOS, Linux, or Windows
150
+
151
+ ## License
152
+
153
+ MIT
@@ -0,0 +1,129 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * wicked-bus CLI entry point.
5
+ */
6
+
7
+ import { WBError, EXIT_CODES } from '../lib/errors.js';
8
+
9
+ // Argument parser
10
+ function parseArgs(argv) {
11
+ const args = {};
12
+ for (let i = 0; i < argv.length; i++) {
13
+ if (argv[i].startsWith('--')) {
14
+ const key = argv[i].slice(2);
15
+ if (i + 1 >= argv.length || argv[i + 1].startsWith('--')) {
16
+ args[key] = true;
17
+ } else {
18
+ args[key] = argv[++i];
19
+ }
20
+ }
21
+ }
22
+ return args;
23
+ }
24
+
25
+ function printUsage() {
26
+ const usage = {
27
+ usage: 'wicked-bus <command> [options]',
28
+ commands: [
29
+ 'init', 'emit', 'subscribe', 'status', 'replay',
30
+ 'cleanup', 'register', 'deregister', 'list', 'ack',
31
+ ],
32
+ global_flags: ['--db-path <path>', '--json', '--log-level <level>'],
33
+ };
34
+ process.stdout.write(JSON.stringify(usage, null, 2) + '\n');
35
+ }
36
+
37
+ function handleError(err) {
38
+ if (err instanceof WBError) {
39
+ process.stderr.write(JSON.stringify(err.toJSON()) + '\n');
40
+ process.exit(EXIT_CODES[err.error] || 1);
41
+ }
42
+ process.stderr.write(JSON.stringify({
43
+ error: 'UNKNOWN',
44
+ code: 'INTERNAL_ERROR',
45
+ message: err.message,
46
+ }) + '\n');
47
+ process.exit(1);
48
+ }
49
+
50
+ async function main() {
51
+ const argv = process.argv.slice(2);
52
+ const command = argv[0];
53
+ const flagArgv = argv.slice(1);
54
+ const args = parseArgs(flagArgv);
55
+
56
+ // Extract global flags
57
+ const globals = {
58
+ db_path: args['db-path'] || null,
59
+ json: args.json !== false,
60
+ log_level: args['log-level'] || null,
61
+ };
62
+
63
+ // Remove global flags from args
64
+ delete args['db-path'];
65
+ delete args.json;
66
+ delete args['log-level'];
67
+
68
+ try {
69
+ switch (command) {
70
+ case 'init': {
71
+ const { cmdInit } = await import('./cmd-init.js');
72
+ await cmdInit(args, globals);
73
+ break;
74
+ }
75
+ case 'emit': {
76
+ const { cmdEmit } = await import('./cmd-emit.js');
77
+ await cmdEmit(args, globals);
78
+ break;
79
+ }
80
+ case 'subscribe': {
81
+ const { cmdSubscribe } = await import('./cmd-subscribe.js');
82
+ await cmdSubscribe(args, globals);
83
+ break;
84
+ }
85
+ case 'status': {
86
+ const { cmdStatus } = await import('./cmd-status.js');
87
+ await cmdStatus(args, globals);
88
+ break;
89
+ }
90
+ case 'replay': {
91
+ const { cmdReplay } = await import('./cmd-replay.js');
92
+ await cmdReplay(args, globals);
93
+ break;
94
+ }
95
+ case 'cleanup': {
96
+ const { cmdCleanup } = await import('./cmd-cleanup.js');
97
+ await cmdCleanup(args, globals);
98
+ break;
99
+ }
100
+ case 'register': {
101
+ const { cmdRegister } = await import('./cmd-register.js');
102
+ await cmdRegister(args, globals);
103
+ break;
104
+ }
105
+ case 'deregister': {
106
+ const { cmdDeregister } = await import('./cmd-deregister.js');
107
+ await cmdDeregister(args, globals);
108
+ break;
109
+ }
110
+ case 'list': {
111
+ const { cmdList } = await import('./cmd-list.js');
112
+ await cmdList(args, globals);
113
+ break;
114
+ }
115
+ case 'ack': {
116
+ const { cmdAck } = await import('./cmd-ack.js');
117
+ await cmdAck(args, globals);
118
+ break;
119
+ }
120
+ default:
121
+ printUsage();
122
+ process.exit(command ? 1 : 0);
123
+ }
124
+ } catch (err) {
125
+ handleError(err);
126
+ }
127
+ }
128
+
129
+ main();
@@ -0,0 +1,31 @@
1
+ /**
2
+ * wicked-bus ack command.
3
+ */
4
+
5
+ import { loadConfig } from '../lib/config.js';
6
+ import { openDb } from '../lib/db.js';
7
+ import { ack } from '../lib/poll.js';
8
+
9
+ export async function cmdAck(args, globals) {
10
+ const configOverrides = {};
11
+ if (globals.db_path) configOverrides.db_path = globals.db_path;
12
+ if (globals.log_level) configOverrides.log_level = globals.log_level;
13
+
14
+ const config = loadConfig(configOverrides);
15
+ const db = openDb(config);
16
+
17
+ const cursorId = args['cursor-id'];
18
+ const lastEventId = Number(args['last-event-id']);
19
+
20
+ if (!cursorId) {
21
+ throw new Error('--cursor-id is required');
22
+ }
23
+ if (isNaN(lastEventId)) {
24
+ throw new Error('--last-event-id must be a number');
25
+ }
26
+
27
+ const result = ack(db, cursorId, lastEventId);
28
+ db.close();
29
+
30
+ process.stdout.write(JSON.stringify(result) + '\n');
31
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * wicked-bus cleanup command -- sweep expired events.
3
+ */
4
+
5
+ import { loadConfig } from '../lib/config.js';
6
+ import { openDb } from '../lib/db.js';
7
+ import { runSweep } from '../lib/sweep.js';
8
+
9
+ export async function cmdCleanup(args, globals) {
10
+ const configOverrides = {};
11
+ if (globals.db_path) configOverrides.db_path = globals.db_path;
12
+ if (globals.log_level) configOverrides.log_level = globals.log_level;
13
+
14
+ const config = loadConfig(configOverrides);
15
+
16
+ // --archive flag overrides config
17
+ if (args.archive === true) {
18
+ config.archive_mode = true;
19
+ }
20
+
21
+ const db = openDb(config);
22
+ const dryRun = args['dry-run'] === true;
23
+
24
+ if (dryRun) {
25
+ const now = Date.now();
26
+ const count = db.prepare(
27
+ 'SELECT COUNT(*) as count FROM events WHERE dedup_expires_at < ?'
28
+ ).get(now);
29
+
30
+ const result = {
31
+ events_deleted: count.count,
32
+ dry_run: true,
33
+ };
34
+ db.close();
35
+ process.stdout.write(JSON.stringify(result) + '\n');
36
+ return;
37
+ }
38
+
39
+ const result = runSweep(db, config);
40
+ result.dry_run = false;
41
+
42
+ db.close();
43
+ process.stdout.write(JSON.stringify(result) + '\n');
44
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * wicked-bus deregister command.
3
+ */
4
+
5
+ import { loadConfig } from '../lib/config.js';
6
+ import { openDb } from '../lib/db.js';
7
+ import { deregister } from '../lib/register.js';
8
+
9
+ export async function cmdDeregister(args, globals) {
10
+ const configOverrides = {};
11
+ if (globals.db_path) configOverrides.db_path = globals.db_path;
12
+ if (globals.log_level) configOverrides.log_level = globals.log_level;
13
+
14
+ const config = loadConfig(configOverrides);
15
+ const db = openDb(config);
16
+
17
+ const subscriptionId = args['subscription-id'];
18
+ if (!subscriptionId) {
19
+ throw new Error('--subscription-id is required');
20
+ }
21
+
22
+ const result = deregister(db, subscriptionId);
23
+ db.close();
24
+
25
+ process.stdout.write(JSON.stringify(result) + '\n');
26
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * wicked-bus emit command.
3
+ */
4
+
5
+ import { readFileSync } from 'node:fs';
6
+ import { loadConfig } from '../lib/config.js';
7
+ import { openDb } from '../lib/db.js';
8
+ import { emit } from '../lib/emit.js';
9
+
10
+ export async function cmdEmit(args, globals) {
11
+ const configOverrides = {};
12
+ if (globals.db_path) configOverrides.db_path = globals.db_path;
13
+ if (globals.log_level) configOverrides.log_level = globals.log_level;
14
+
15
+ const config = loadConfig(configOverrides);
16
+ const db = openDb(config);
17
+
18
+ // Parse payload -- support @file syntax
19
+ let payload = args.payload;
20
+ if (typeof payload === 'string' && payload.startsWith('@')) {
21
+ const filePath = payload.slice(1);
22
+ payload = readFileSync(filePath, 'utf8');
23
+ }
24
+
25
+ // Parse payload as JSON if it's a string
26
+ if (typeof payload === 'string') {
27
+ try {
28
+ payload = JSON.parse(payload);
29
+ } catch (_) {
30
+ // Will be caught by validation
31
+ }
32
+ }
33
+
34
+ const event = {
35
+ event_type: args.type,
36
+ domain: args.domain,
37
+ subdomain: args.subdomain || '',
38
+ payload,
39
+ schema_version: args['schema-version'] || undefined,
40
+ idempotency_key: args['idempotency-key'] || undefined,
41
+ metadata: args.metadata ? JSON.parse(args.metadata) : undefined,
42
+ };
43
+
44
+ if (args['ttl-hours'] != null) {
45
+ event.ttl_hours = Number(args['ttl-hours']);
46
+ }
47
+
48
+ const result = emit(db, config, event);
49
+ db.close();
50
+
51
+ process.stdout.write(JSON.stringify(result) + '\n');
52
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * wicked-bus init command.
3
+ */
4
+
5
+ import { ensureDataDir, resolveDbPath } from '../lib/paths.js';
6
+ import { loadConfig, writeDefaultConfig } from '../lib/config.js';
7
+ import { openDb } from '../lib/db.js';
8
+
9
+ export async function cmdInit(args, globals) {
10
+ const dataDir = ensureDataDir();
11
+ const configOverrides = {};
12
+ if (globals.db_path) configOverrides.db_path = globals.db_path;
13
+ if (globals.log_level) configOverrides.log_level = globals.log_level;
14
+
15
+ // Write default config (won't overwrite unless --force)
16
+ writeDefaultConfig(dataDir, args.force === true);
17
+
18
+ const config = loadConfig(configOverrides);
19
+ const db = openDb(config);
20
+ const dbPath = resolveDbPath(config);
21
+ db.close();
22
+
23
+ const result = {
24
+ initialized: true,
25
+ data_dir: dataDir,
26
+ db_path: dbPath,
27
+ };
28
+
29
+ process.stdout.write(JSON.stringify(result) + '\n');
30
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * wicked-bus list command.
3
+ */
4
+
5
+ import { loadConfig } from '../lib/config.js';
6
+ import { openDb } from '../lib/db.js';
7
+
8
+ export async function cmdList(args, globals) {
9
+ const configOverrides = {};
10
+ if (globals.db_path) configOverrides.db_path = globals.db_path;
11
+ if (globals.log_level) configOverrides.log_level = globals.log_level;
12
+
13
+ const config = loadConfig(configOverrides);
14
+ const db = openDb(config);
15
+
16
+ let sql = 'SELECT * FROM subscriptions';
17
+ const conditions = [];
18
+ const params = {};
19
+
20
+ if (args.role) {
21
+ conditions.push('role = :role');
22
+ params.role = args.role;
23
+ }
24
+
25
+ if (!args['include-deregistered']) {
26
+ conditions.push('deregistered_at IS NULL');
27
+ }
28
+
29
+ if (conditions.length > 0) {
30
+ sql += ' WHERE ' + conditions.join(' AND ');
31
+ }
32
+
33
+ sql += ' ORDER BY registered_at DESC';
34
+
35
+ const rows = db.prepare(sql).all(params);
36
+ db.close();
37
+
38
+ process.stdout.write(JSON.stringify(rows) + '\n');
39
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * wicked-bus register command.
3
+ */
4
+
5
+ import { loadConfig } from '../lib/config.js';
6
+ import { openDb } from '../lib/db.js';
7
+ import { register } from '../lib/register.js';
8
+
9
+ export async function cmdRegister(args, globals) {
10
+ const configOverrides = {};
11
+ if (globals.db_path) configOverrides.db_path = globals.db_path;
12
+ if (globals.log_level) configOverrides.log_level = globals.log_level;
13
+
14
+ const config = loadConfig(configOverrides);
15
+ const db = openDb(config);
16
+
17
+ const opts = {
18
+ plugin: args.plugin,
19
+ role: args.role,
20
+ filter: args.events || args.filter || '',
21
+ schema_version: args['schema-version'] || undefined,
22
+ cursor_init: args['cursor-init'] || 'latest',
23
+ };
24
+
25
+ const result = register(db, opts);
26
+ db.close();
27
+
28
+ process.stdout.write(JSON.stringify(result) + '\n');
29
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * wicked-bus replay command -- reset cursor to a specific event ID.
3
+ */
4
+
5
+ import { loadConfig } from '../lib/config.js';
6
+ import { openDb } from '../lib/db.js';
7
+ import { WBError } from '../lib/errors.js';
8
+
9
+ export async function cmdReplay(args, globals) {
10
+ const configOverrides = {};
11
+ if (globals.db_path) configOverrides.db_path = globals.db_path;
12
+ if (globals.log_level) configOverrides.log_level = globals.log_level;
13
+
14
+ const config = loadConfig(configOverrides);
15
+ const db = openDb(config);
16
+
17
+ const cursorId = args['cursor-id'];
18
+ const fromEventId = Number(args['from-event-id']);
19
+
20
+ if (!cursorId) {
21
+ throw new Error('--cursor-id is required');
22
+ }
23
+ if (isNaN(fromEventId)) {
24
+ throw new Error('--from-event-id must be a number');
25
+ }
26
+
27
+ // Verify cursor exists
28
+ const cursor = db.prepare(
29
+ 'SELECT * FROM cursors WHERE cursor_id = ? AND deregistered_at IS NULL'
30
+ ).get(cursorId);
31
+
32
+ if (!cursor) {
33
+ throw new WBError('WB-006', 'CURSOR_NOT_FOUND', {
34
+ message: `Cursor not found: ${cursorId}`,
35
+ cursor_id: cursorId,
36
+ reason: 'cursor not found or deregistered',
37
+ });
38
+ }
39
+
40
+ // Check that from_event_id is not below the oldest available event
41
+ const oldest = db.prepare('SELECT MIN(event_id) as min_id FROM events').get();
42
+ if (oldest && oldest.min_id != null && fromEventId < oldest.min_id) {
43
+ throw new WBError('WB-003', 'CURSOR_BEHIND_TTL_WINDOW', {
44
+ message: `from_event_id ${fromEventId} is below the oldest available event (${oldest.min_id})`,
45
+ cursor_last_event_id: cursor.last_event_id,
46
+ oldest_available_event_id: oldest.min_id,
47
+ });
48
+ }
49
+
50
+ // Reset cursor to from_event_id - 1
51
+ const resetTo = fromEventId - 1;
52
+ db.prepare(
53
+ 'UPDATE cursors SET last_event_id = ? WHERE cursor_id = ?'
54
+ ).run(resetTo, cursorId);
55
+
56
+ const result = {
57
+ replayed: true,
58
+ cursor_id: cursorId,
59
+ reset_to: resetTo,
60
+ from_event_id: fromEventId,
61
+ };
62
+
63
+ db.close();
64
+ process.stdout.write(JSON.stringify(result) + '\n');
65
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * wicked-bus status command.
3
+ */
4
+
5
+ import { loadConfig } from '../lib/config.js';
6
+ import { openDb } from '../lib/db.js';
7
+ import { resolveDbPath } from '../lib/paths.js';
8
+
9
+ export async function cmdStatus(args, globals) {
10
+ const configOverrides = {};
11
+ if (globals.db_path) configOverrides.db_path = globals.db_path;
12
+ if (globals.log_level) configOverrides.log_level = globals.log_level;
13
+
14
+ const config = loadConfig(configOverrides);
15
+ const db = openDb(config);
16
+ const dbPath = resolveDbPath(config);
17
+
18
+ const totalEvents = db.prepare('SELECT COUNT(*) as count FROM events').get().count;
19
+ const oldest = db.prepare('SELECT MIN(event_id) as min_id FROM events').get();
20
+ const newest = db.prepare('SELECT MAX(event_id) as max_id FROM events').get();
21
+
22
+ // Events by type
23
+ const byType = db.prepare(
24
+ 'SELECT event_type, COUNT(*) as count FROM events GROUP BY event_type'
25
+ ).all();
26
+ const eventsByType = {};
27
+ for (const row of byType) {
28
+ eventsByType[row.event_type] = row.count;
29
+ }
30
+
31
+ // Subscribers with lag
32
+ const subscribers = db.prepare(`
33
+ SELECT s.subscription_id, s.plugin, s.event_type_filter,
34
+ c.cursor_id, c.last_event_id, c.acked_at
35
+ FROM subscriptions s
36
+ JOIN cursors c ON c.subscription_id = s.subscription_id
37
+ WHERE s.role = 'subscriber' AND s.deregistered_at IS NULL AND c.deregistered_at IS NULL
38
+ `).all();
39
+
40
+ const newestId = newest.max_id || 0;
41
+ const subscriberList = subscribers.map(s => ({
42
+ subscription_id: s.subscription_id,
43
+ plugin: s.plugin,
44
+ filter: s.event_type_filter,
45
+ cursor_id: s.cursor_id,
46
+ last_event_id: s.last_event_id,
47
+ lag: newestId - s.last_event_id,
48
+ acked_at: s.acked_at,
49
+ }));
50
+
51
+ // Providers
52
+ const providers = db.prepare(`
53
+ SELECT subscription_id, plugin, event_type_filter, schema_version
54
+ FROM subscriptions
55
+ WHERE role = 'provider' AND deregistered_at IS NULL
56
+ `).all();
57
+
58
+ const result = {
59
+ db_path: dbPath,
60
+ total_events: totalEvents,
61
+ oldest_event_id: oldest.min_id || null,
62
+ newest_event_id: newest.max_id || null,
63
+ events_by_type: eventsByType,
64
+ subscribers: subscriberList,
65
+ providers,
66
+ };
67
+
68
+ db.close();
69
+ process.stdout.write(JSON.stringify(result) + '\n');
70
+ }