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/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "wicked-bus",
3
+ "version": "1.0.0",
4
+ "description": "Lightweight, local-first SQLite event bus for AI agents and developer tools",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": {
8
+ "import": "./lib/index.js",
9
+ "require": "./lib/index.cjs"
10
+ },
11
+ "./cli": "./commands/cli.js"
12
+ },
13
+ "main": "./lib/index.cjs",
14
+ "module": "./lib/index.js",
15
+ "bin": {
16
+ "wicked-bus": "./commands/cli.js",
17
+ "wicked-bus-install": "./install.mjs"
18
+ },
19
+ "engines": {
20
+ "node": ">=18.0.0"
21
+ },
22
+ "scripts": {
23
+ "postinstall": "node scripts/postinstall.js",
24
+ "test": "vitest run",
25
+ "test:watch": "vitest",
26
+ "test:coverage": "vitest run --coverage"
27
+ },
28
+ "dependencies": {
29
+ "uuid": "^9.0.0"
30
+ },
31
+ "peerDependencies": {
32
+ "better-sqlite3": ">=9.0.0"
33
+ },
34
+ "devDependencies": {
35
+ "@vitest/coverage-v8": "^4.1.4",
36
+ "better-sqlite3": "^11.0.0",
37
+ "vitest": "^4.1.4"
38
+ },
39
+ "files": [
40
+ "lib/",
41
+ "commands/",
42
+ "scripts/",
43
+ "skills/",
44
+ "install.mjs"
45
+ ],
46
+ "keywords": [
47
+ "event-bus",
48
+ "sqlite",
49
+ "local-first",
50
+ "ai-agents",
51
+ "developer-tools",
52
+ "cursor-poll",
53
+ "at-least-once"
54
+ ],
55
+ "license": "MIT"
56
+ }
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Postinstall script -- auto-create data directory on npm install.
5
+ * Must not fail -- all errors are swallowed.
6
+ */
7
+
8
+ import { ensureDataDir } from '../lib/paths.js';
9
+
10
+ try {
11
+ ensureDataDir();
12
+ } catch (_) {
13
+ // Swallow: postinstall must not fail npm install
14
+ }
@@ -0,0 +1,147 @@
1
+ ---
2
+ description: Emit events to the wicked-bus. Use when publishing events from a plugin, logging activity to the bus, or integrating a new system with the event bridge. Covers both programmatic (Node.js) and CLI usage.
3
+ ---
4
+
5
+ # wicked-bus:emit
6
+
7
+ Guide for publishing events to the wicked-bus.
8
+
9
+ ## When to use
10
+
11
+ - User wants to emit an event from their code
12
+ - User asks "how do I publish to the bus"
13
+ - Integrating a plugin with wicked-bus for the first time
14
+ - User wants to fire-and-forget an event
15
+
16
+ ## Prerequisites
17
+
18
+ Check that wicked-bus is initialized. If not, trigger `wicked-bus-init`.
19
+
20
+ ```bash
21
+ npx wicked-bus status 2>/dev/null
22
+ ```
23
+
24
+ ## Programmatic Usage (Node.js)
25
+
26
+ ### Basic emit
27
+
28
+ ```javascript
29
+ import { emit } from 'wicked-bus';
30
+ import { loadConfig } from 'wicked-bus/lib/config.js';
31
+ import { openDb } from 'wicked-bus/lib/db.js';
32
+
33
+ const config = loadConfig();
34
+ const db = openDb(config);
35
+
36
+ const result = emit(db, config, {
37
+ event_type: 'wicked.task.completed',
38
+ domain: 'my-plugin',
39
+ subdomain: 'workflow.task',
40
+ payload: { taskId: 'abc-123', status: 'done' },
41
+ });
42
+
43
+ console.log(result);
44
+ // { event_id: 42, idempotency_key: '550e8400-...' }
45
+
46
+ db.close();
47
+ ```
48
+
49
+ ### Fire-and-forget pattern (recommended for integrations)
50
+
51
+ Plugins should never block on the bus. Use this pattern:
52
+
53
+ ```javascript
54
+ let _busEmit = null;
55
+ let _busChecked = false;
56
+
57
+ async function emitToBus(event) {
58
+ if (!_busChecked) {
59
+ _busChecked = true;
60
+ try {
61
+ const mod = await import('wicked-bus');
62
+ const { loadConfig } = await import('wicked-bus/lib/config.js');
63
+ const { openDb } = await import('wicked-bus/lib/db.js');
64
+ const config = loadConfig();
65
+ const db = openDb(config);
66
+ _busEmit = (evt) => mod.emit(db, config, evt);
67
+ } catch (_) {
68
+ _busEmit = null; // Bus not installed — degrade gracefully
69
+ }
70
+ }
71
+ if (!_busEmit) return null;
72
+ try {
73
+ return _busEmit(event);
74
+ } catch (_) {
75
+ return null; // Never throw from fire-and-forget
76
+ }
77
+ }
78
+ ```
79
+
80
+ ### With custom TTL
81
+
82
+ ```javascript
83
+ emit(db, config, {
84
+ event_type: 'wicked.cache.invalidated',
85
+ domain: 'my-plugin',
86
+ payload: { keys: ['user:123'] },
87
+ ttl_hours: 4, // Override default 72h TTL
88
+ });
89
+ ```
90
+
91
+ ### With explicit idempotency key
92
+
93
+ ```javascript
94
+ emit(db, config, {
95
+ event_type: 'wicked.job.completed',
96
+ domain: 'my-plugin',
97
+ subdomain: 'jobs.batch',
98
+ payload: { jobId: 'job-42' },
99
+ idempotency_key: 'job-42-completed', // Prevents duplicate events
100
+ });
101
+ ```
102
+
103
+ ## CLI Usage
104
+
105
+ ### Basic emit
106
+
107
+ ```bash
108
+ npx wicked-bus emit \
109
+ --type wicked.task.completed \
110
+ --domain my-plugin \
111
+ --subdomain workflow.task \
112
+ --payload '{"taskId": "abc-123", "status": "done"}'
113
+ ```
114
+
115
+ ### Payload from file
116
+
117
+ ```bash
118
+ npx wicked-bus emit \
119
+ --type wicked.report.generated \
120
+ --domain my-plugin \
121
+ --payload @./report-data.json
122
+ ```
123
+
124
+ ### With metadata
125
+
126
+ ```bash
127
+ npx wicked-bus emit \
128
+ --type wicked.deploy.completed \
129
+ --domain my-deploy \
130
+ --subdomain deploy.production \
131
+ --payload '{"version": "2.0.0"}' \
132
+ --metadata '{"host": "prod-01"}'
133
+ ```
134
+
135
+ ## Error Handling
136
+
137
+ | Error | Code | Meaning |
138
+ |-------|------|---------|
139
+ | WB-001 | INVALID_EVENT_SCHEMA | Event failed validation (bad type, missing fields, payload too large) |
140
+ | WB-002 | DUPLICATE_EVENT | Idempotency key already exists |
141
+ | WB-004 | DISK_FULL | SQLite database disk is full |
142
+ | WB-005 | SCHEMA_VERSION_UNSUPPORTED | schema_version > 1.x |
143
+
144
+ ## Event Naming
145
+
146
+ For help choosing event_type, domain, and subdomain values, use the
147
+ `wicked-bus-naming` skill.
@@ -0,0 +1,94 @@
1
+ ---
2
+ description: Initialize wicked-bus or connect to an existing instance. Use when setting up the bus for the first time, checking if it's running, or configuring a project to use it. Auto-triggered when any wicked-bus skill detects no config.
3
+ ---
4
+
5
+ # wicked-bus:init
6
+
7
+ Set up wicked-bus for the current project. Detects an existing running instance
8
+ before creating a new one.
9
+
10
+ ## When to use
11
+
12
+ - First time using wicked-bus in a project
13
+ - Another wicked-bus skill detected no config and redirected here
14
+ - User asks to "set up the bus", "init wicked-bus", or "connect to the bus"
15
+
16
+ ## Process
17
+
18
+ ### Step 1: Check for existing instance
19
+
20
+ Before creating anything, check if wicked-bus is already initialized:
21
+
22
+ ```bash
23
+ # Check if the data directory exists
24
+ ls ~/.something-wicked/wicked-bus/bus.db 2>/dev/null
25
+ ```
26
+
27
+ If the data dir and DB exist, wicked-bus is already running. Skip to Step 4.
28
+
29
+ Also check if another agent or process already initialized it this session:
30
+
31
+ ```bash
32
+ # Check if the CLI is available
33
+ npx wicked-bus status 2>/dev/null
34
+ ```
35
+
36
+ If status returns valid JSON, the bus is live. Report to the user and skip init.
37
+
38
+ ### Step 2: Check if wicked-bus is installed
39
+
40
+ ```bash
41
+ # Check if the package is available
42
+ node -e "require.resolve('wicked-bus')" 2>/dev/null || \
43
+ node -e "import('wicked-bus').then(() => console.log('found'))" 2>/dev/null
44
+ ```
45
+
46
+ If not installed:
47
+ ```
48
+ wicked-bus is not installed. Install it:
49
+ npm install wicked-bus
50
+ ```
51
+
52
+ ### Step 3: Initialize
53
+
54
+ Run the init command:
55
+
56
+ ```bash
57
+ npx wicked-bus init
58
+ ```
59
+
60
+ This creates:
61
+ - `~/.something-wicked/wicked-bus/` data directory
62
+ - `bus.db` SQLite database with WAL mode
63
+ - `config.json` with defaults
64
+
65
+ Verify success by checking the JSON output for `"initialized": true`.
66
+
67
+ ### Step 4: Register the current project (optional)
68
+
69
+ If the user wants this project to emit events, register as a provider:
70
+
71
+ Ask: "What domain name should this project use?" (default: directory name)
72
+
73
+ ```bash
74
+ npx wicked-bus register \
75
+ --role provider \
76
+ --plugin {domain} \
77
+ --filter 'wicked.*'
78
+ ```
79
+
80
+ ### Step 5: Confirm
81
+
82
+ Report:
83
+ ```
84
+ wicked-bus is ready.
85
+ Data dir: ~/.something-wicked/wicked-bus/
86
+ DB: bus.db (WAL mode)
87
+ Status: {event count} events, {subscriber count} subscribers
88
+ ```
89
+
90
+ If the bus was already running, say so:
91
+ ```
92
+ wicked-bus is already initialized and running.
93
+ {status output}
94
+ ```
@@ -0,0 +1,151 @@
1
+ ---
2
+ description: Guide for naming wicked-bus events — helps choose event_type, domain, and subdomain when emitting events. Use when creating new events, integrating a plugin with the bus, or reviewing event naming for consistency.
3
+ ---
4
+
5
+ # wicked-bus Event Naming
6
+
7
+ Interactive guide for naming events in the wicked-bus ecosystem. Helps users
8
+ choose correct event_type, domain, and subdomain values.
9
+
10
+ ## When to use
11
+
12
+ - User is adding wicked-bus integration to a plugin
13
+ - User asks "how do I name this event" or "what event_type should I use"
14
+ - User is emitting events and needs to pick domain/subdomain
15
+ - Reviewing event names for consistency with the catalog
16
+ - User asks about the event naming convention
17
+
18
+ ## The Three Fields
19
+
20
+ Every event has three identity fields:
21
+
22
+ | Field | Purpose | Rule |
23
+ |-------|---------|------|
24
+ | `event_type` | **What happened** — semantic, shared across producers | `wicked.<noun>.<past-tense-verb>` |
25
+ | `domain` | **Who did it** — the publishing plugin's package name | Your npm package name (e.g., `wicked-testing`) |
26
+ | `subdomain` | **Where in the system** — functional area within the plugin | Dot-separated hierarchy (e.g., `crew.phase`, `test.run`) |
27
+
28
+ ## event_type Rules
29
+
30
+ Pattern: `wicked.<noun>.<past-tense-verb>`
31
+
32
+ 1. Always starts with `wicked.`
33
+ 2. Second segment = **noun** (the thing that changed): `run`, `phase`, `memory`, `project`, `gate`
34
+ 3. Third segment = **past-tense verb** (what happened): `completed`, `started`, `stored`, `failed`, `created`
35
+ 4. Lowercase, `[a-z0-9_]` only, dot-separated
36
+ 5. Max 128 characters
37
+ 6. **Semantic, not source-specific** — two plugins emitting the same kind of event share the type
38
+
39
+ ### Common mistakes to catch
40
+
41
+ | Wrong | Problem | Correct |
42
+ |-------|---------|---------|
43
+ | `wicked-testing.run.completed` | Domain leaked into type | `wicked.run.completed` + domain=`wicked-testing` |
44
+ | `wicked.test_run_completed` | Underscores instead of dots | `wicked.run.completed` |
45
+ | `wicked.run.complete` | Not past tense | `wicked.run.completed` |
46
+ | `wicked.crew.phase.started` | Subdomain leaked into type (4 segments) | `wicked.phase.started` + subdomain=`crew.phase` |
47
+ | `run.completed` | Missing `wicked.` prefix | `wicked.run.completed` |
48
+
49
+ ## domain Rules
50
+
51
+ 1. A unique identifier for the publishing system — package name, service name, or tool name
52
+ 2. Max 64 characters
53
+ 3. One domain per system — don't subdivide at this level
54
+ 4. This is what subscribers use in `@domain` filters
55
+
56
+ ## subdomain Rules
57
+
58
+ 1. Dot-separated hierarchy: `<area>.<entity>` (e.g., `deploy.staging`)
59
+ 2. First segment = top-level area within your plugin
60
+ 3. Second segment = specific entity or concern
61
+ 4. Defaults to `''` if not provided
62
+ 5. Max 64 characters
63
+ 6. Can be arbitrarily deep if needed
64
+
65
+ ## Process
66
+
67
+ When a user needs to name an event:
68
+
69
+ ### Step 1: Identify what happened
70
+
71
+ Ask: "What changed and what happened to it?"
72
+
73
+ Map to: `wicked.<noun>.<past-tense-verb>`
74
+
75
+ - Thing created → `wicked.<thing>.created`
76
+ - Thing completed → `wicked.<thing>.completed`
77
+ - Thing failed → `wicked.<thing>.failed`
78
+ - Thing updated → `wicked.<thing>.updated`
79
+ - Thing deleted/removed → `wicked.<thing>.deleted`
80
+ - Thing started → `wicked.<thing>.started`
81
+
82
+ ### Step 2: Identify the publisher
83
+
84
+ Ask: "What plugin is emitting this?"
85
+
86
+ Map to `domain` = their package name.
87
+
88
+ ### Step 3: Identify the functional area
89
+
90
+ Ask: "What part of the system does this come from?"
91
+
92
+ Map to `subdomain` using the pattern `<area>.<entity>`. Examples:
93
+ - A deployment subsystem → `deploy.staging`
94
+ - An auth module → `auth.session`
95
+ - A build pipeline → `build.artifact`
96
+
97
+ ### Step 4: Validate
98
+
99
+ Check that your event follows the rules:
100
+
101
+ 1. event_type starts with `wicked.` and has exactly 3 dot-separated segments
102
+ 2. Third segment is past tense (`created`, not `create`)
103
+ 3. Domain doesn't appear in the event_type
104
+ 4. Subdomain doesn't appear in the event_type
105
+ 5. If another plugin emits the same semantic event, you should share the event_type
106
+
107
+ **Example validation:**
108
+
109
+ | Proposed | Valid? | Issue |
110
+ |----------|--------|-------|
111
+ | `wicked.deployment.started` + domain=`my-deploy` | Yes | |
112
+ | `my-deploy.deployment.started` | No | Domain in type |
113
+ | `wicked.deploy.staging.started` | No | 4 segments — subdomain leaked in |
114
+ | `wicked.deployment.start` | No | Not past tense |
115
+
116
+ ### Step 5: Generate the emit call
117
+
118
+ ```javascript
119
+ import { emit } from 'wicked-bus';
120
+
121
+ emit(db, config, {
122
+ event_type: '{event_type}',
123
+ domain: '{domain}',
124
+ subdomain: '{subdomain}',
125
+ payload: { /* event-specific data */ },
126
+ });
127
+ ```
128
+
129
+ ### Step 6: Show the subscriber filter
130
+
131
+ ```bash
132
+ # All events of this type from any source
133
+ wicked-bus subscribe --filter '{event_type}'
134
+
135
+ # Only from this domain
136
+ wicked-bus subscribe --filter '{event_type}@{domain}'
137
+
138
+ # All events from this domain
139
+ wicked-bus subscribe --filter '*@{domain}'
140
+ ```
141
+
142
+ ## Design Decisions
143
+
144
+ **Why event_type is semantic (not source-specific):**
145
+ Same `wicked.project.created` from `wicked-garden` and `wicked-testing`.
146
+ A subscriber wanting "all project creations" uses one filter. Baking domain
147
+ into event_type forces subscribers to enumerate every producer.
148
+
149
+ **Why subdomain is a column (not in event_type):**
150
+ `wicked.phase.started` is semantic. Whether it's `crew.phase` or `deploy.phase`
151
+ is identity, not semantics. Columns enable index-based filtering.
@@ -0,0 +1,177 @@
1
+ ---
2
+ description: Query and debug the wicked-bus. Use when checking bus health, inspecting events, debugging delivery issues, tracing event flow, or investigating why a subscriber isn't receiving events. Covers status, replay, and direct SQLite queries.
3
+ ---
4
+
5
+ # wicked-bus:query
6
+
7
+ Tools for inspecting, debugging, and querying the wicked-bus.
8
+
9
+ ## When to use
10
+
11
+ - User asks "what's in the bus" or "show me recent events"
12
+ - Debugging why a subscriber isn't receiving events
13
+ - Checking bus health or event counts
14
+ - Investigating delivery lag or missed events
15
+ - User asks about cursor positions or subscriber state
16
+
17
+ ## Quick Health Check
18
+
19
+ ```bash
20
+ npx wicked-bus status
21
+ ```
22
+
23
+ Returns JSON with:
24
+ - Total event count
25
+ - Active subscriber count
26
+ - Provider list
27
+ - Cursor lag per subscriber
28
+
29
+ ## Inspecting Events
30
+
31
+ ### Recent events via SQLite
32
+
33
+ ```bash
34
+ # Last 10 events
35
+ sqlite3 ~/.something-wicked/wicked-bus/bus.db \
36
+ "SELECT event_id, event_type, domain, subdomain, datetime(emitted_at/1000, 'unixepoch') as time FROM events ORDER BY event_id DESC LIMIT 10;"
37
+ ```
38
+
39
+ ### Events by type
40
+
41
+ ```bash
42
+ sqlite3 ~/.something-wicked/wicked-bus/bus.db \
43
+ "SELECT event_id, domain, subdomain, datetime(emitted_at/1000, 'unixepoch') as time FROM events WHERE event_type = 'wicked.phase.completed' ORDER BY event_id DESC LIMIT 10;"
44
+ ```
45
+
46
+ ### Events by domain
47
+
48
+ ```bash
49
+ sqlite3 ~/.something-wicked/wicked-bus/bus.db \
50
+ "SELECT event_id, event_type, subdomain, datetime(emitted_at/1000, 'unixepoch') as time FROM events WHERE domain = 'wicked-garden' ORDER BY event_id DESC LIMIT 10;"
51
+ ```
52
+
53
+ ### Event count by type
54
+
55
+ ```bash
56
+ sqlite3 ~/.something-wicked/wicked-bus/bus.db \
57
+ "SELECT event_type, COUNT(*) as count FROM events GROUP BY event_type ORDER BY count DESC;"
58
+ ```
59
+
60
+ ### Full event payload
61
+
62
+ ```bash
63
+ sqlite3 ~/.something-wicked/wicked-bus/bus.db \
64
+ "SELECT event_id, event_type, domain, payload FROM events WHERE event_id = {id};"
65
+ ```
66
+
67
+ ## Debugging Subscribers
68
+
69
+ ### Check cursor positions
70
+
71
+ ```bash
72
+ sqlite3 ~/.something-wicked/wicked-bus/bus.db \
73
+ "SELECT c.cursor_id, s.plugin, s.event_type_filter, c.last_event_id, datetime(c.acked_at/1000, 'unixepoch') as last_ack FROM cursors c JOIN subscriptions s ON c.subscription_id = s.subscription_id WHERE c.deregistered_at IS NULL;"
74
+ ```
75
+
76
+ ### Find subscriber lag
77
+
78
+ ```bash
79
+ # Compare cursor position to latest event
80
+ sqlite3 ~/.something-wicked/wicked-bus/bus.db \
81
+ "SELECT s.plugin, c.last_event_id, (SELECT MAX(event_id) FROM events) - c.last_event_id as lag FROM cursors c JOIN subscriptions s ON c.subscription_id = s.subscription_id WHERE c.deregistered_at IS NULL;"
82
+ ```
83
+
84
+ ### Check if subscriber is registered
85
+
86
+ ```bash
87
+ npx wicked-bus list --role subscriber --json
88
+ ```
89
+
90
+ ### Check active vs deregistered
91
+
92
+ ```bash
93
+ npx wicked-bus list --include-deregistered --json
94
+ ```
95
+
96
+ ## Common Issues
97
+
98
+ ### "Subscriber isn't receiving events"
99
+
100
+ 1. **Check registration**: `npx wicked-bus list --role subscriber`
101
+ 2. **Check filter**: does the filter match the event_type?
102
+ - `wicked.run.*` matches `wicked.run.completed` but NOT `wicked.run.step.completed`
103
+ - `@domain` suffix must match the `domain` column exactly
104
+ 3. **Check cursor position**: is the cursor ahead of the events?
105
+ 4. **Check expiry**: events past `expires_at` (default 72h) are invisible
106
+ 5. **Check deregistration**: was the subscription soft-deleted?
107
+
108
+ ### "WB-003: Cursor behind oldest event"
109
+
110
+ The subscriber's cursor is behind the oldest event in the table. Events
111
+ between the cursor and the oldest event were swept (deleted after
112
+ `dedup_expires_at`). These events are permanently lost for this subscriber.
113
+
114
+ **Fix**: Reset the cursor to the current position:
115
+ ```bash
116
+ npx wicked-bus replay --cursor-id {cursor_id} --event-id {latest_event_id}
117
+ ```
118
+
119
+ ### "Events seem to disappear"
120
+
121
+ Events are deleted by the sweep process after `dedup_expires_at` (default 24h).
122
+ Check your sweep configuration:
123
+
124
+ ```bash
125
+ sqlite3 ~/.something-wicked/wicked-bus/bus.db \
126
+ "SELECT * FROM schema_migrations;"
127
+ ```
128
+
129
+ Check config:
130
+ ```bash
131
+ cat ~/.something-wicked/wicked-bus/config.json
132
+ ```
133
+
134
+ ### "Duplicate events"
135
+
136
+ Events have a UNIQUE `idempotency_key`. If you're seeing duplicates, the
137
+ emitter is generating different keys for logically identical events. Fix
138
+ by using a deterministic key:
139
+
140
+ ```javascript
141
+ emit(db, config, {
142
+ event_type: 'wicked.job.completed',
143
+ domain: 'my-plugin',
144
+ payload: { jobId: 'job-42' },
145
+ idempotency_key: `job-42-completed`, // Deterministic
146
+ });
147
+ ```
148
+
149
+ ## Cleanup and Maintenance
150
+
151
+ ### Manual sweep
152
+
153
+ ```bash
154
+ # Dry run — see what would be deleted
155
+ npx wicked-bus cleanup --dry-run
156
+
157
+ # Delete expired events
158
+ npx wicked-bus cleanup
159
+
160
+ # Delete and archive to events_archive table
161
+ npx wicked-bus cleanup --archive
162
+ ```
163
+
164
+ ### Check database size
165
+
166
+ ```bash
167
+ ls -lh ~/.something-wicked/wicked-bus/bus.db
168
+ ```
169
+
170
+ ### Check WAL size
171
+
172
+ ```bash
173
+ ls -lh ~/.something-wicked/wicked-bus/bus.db-wal
174
+ ```
175
+
176
+ Large WAL files indicate checkpointing isn't happening. This is normal
177
+ for busy periods — SQLite auto-checkpoints at 1000 pages.