@bod.ee/db 0.7.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.
Files changed (65) hide show
  1. package/.claude/settings.local.json +23 -0
  2. package/.claude/skills/config-file.md +54 -0
  3. package/.claude/skills/deploying-bod-db.md +29 -0
  4. package/.claude/skills/developing-bod-db.md +127 -0
  5. package/.claude/skills/using-bod-db.md +403 -0
  6. package/CLAUDE.md +110 -0
  7. package/README.md +252 -0
  8. package/admin/rules.ts +12 -0
  9. package/admin/server.ts +523 -0
  10. package/admin/ui.html +2281 -0
  11. package/cli.ts +177 -0
  12. package/client.ts +2 -0
  13. package/config.ts +20 -0
  14. package/deploy/.env.example +1 -0
  15. package/deploy/base.yaml +18 -0
  16. package/deploy/boddb-logs.yaml +10 -0
  17. package/deploy/boddb.yaml +10 -0
  18. package/deploy/demo.html +196 -0
  19. package/deploy/deploy.ts +32 -0
  20. package/deploy/prod-logs.config.ts +15 -0
  21. package/deploy/prod.config.ts +15 -0
  22. package/index.ts +20 -0
  23. package/mcp.ts +78 -0
  24. package/package.json +29 -0
  25. package/react.ts +1 -0
  26. package/src/client/BodClient.ts +515 -0
  27. package/src/react/hooks.ts +121 -0
  28. package/src/server/BodDB.ts +319 -0
  29. package/src/server/ExpressionRules.ts +250 -0
  30. package/src/server/FTSEngine.ts +76 -0
  31. package/src/server/FileAdapter.ts +116 -0
  32. package/src/server/MCPAdapter.ts +409 -0
  33. package/src/server/MQEngine.ts +286 -0
  34. package/src/server/QueryEngine.ts +45 -0
  35. package/src/server/RulesEngine.ts +108 -0
  36. package/src/server/StorageEngine.ts +464 -0
  37. package/src/server/StreamEngine.ts +320 -0
  38. package/src/server/SubscriptionEngine.ts +120 -0
  39. package/src/server/Transport.ts +479 -0
  40. package/src/server/VectorEngine.ts +115 -0
  41. package/src/shared/errors.ts +15 -0
  42. package/src/shared/pathUtils.ts +94 -0
  43. package/src/shared/protocol.ts +59 -0
  44. package/src/shared/transforms.ts +99 -0
  45. package/tests/batch.test.ts +60 -0
  46. package/tests/bench.ts +205 -0
  47. package/tests/e2e.test.ts +284 -0
  48. package/tests/expression-rules.test.ts +114 -0
  49. package/tests/file-adapter.test.ts +57 -0
  50. package/tests/fts.test.ts +58 -0
  51. package/tests/mq-flow.test.ts +204 -0
  52. package/tests/mq.test.ts +326 -0
  53. package/tests/push.test.ts +55 -0
  54. package/tests/query.test.ts +60 -0
  55. package/tests/rules.test.ts +78 -0
  56. package/tests/sse.test.ts +78 -0
  57. package/tests/storage.test.ts +199 -0
  58. package/tests/stream.test.ts +385 -0
  59. package/tests/stress.test.ts +202 -0
  60. package/tests/subscriptions.test.ts +86 -0
  61. package/tests/transforms.test.ts +92 -0
  62. package/tests/transport.test.ts +209 -0
  63. package/tests/ttl.test.ts +70 -0
  64. package/tests/vector.test.ts +69 -0
  65. package/tsconfig.json +27 -0
package/cli.ts ADDED
@@ -0,0 +1,177 @@
1
+ #!/usr/bin/env bun
2
+ import { BodDB } from './index.ts';
3
+ import { existsSync, mkdirSync } from 'fs';
4
+ import { dirname, resolve } from 'path';
5
+ import { cpus, totalmem } from 'os';
6
+
7
+ const args = process.argv.slice(2);
8
+ const flags: Record<string, string> = {};
9
+ let configPath: string | undefined;
10
+
11
+ // Parse args
12
+ for (let i = 0; i < args.length; i++) {
13
+ const arg = args[i];
14
+ if (arg === '--help' || arg === '-h') { printHelp(); process.exit(0); }
15
+ if (arg === '--version' || arg === '-v') { printVersion(); process.exit(0); }
16
+ if (arg === '--init') { await initConfig(args[i + 1]); process.exit(0); }
17
+ if (arg.startsWith('--')) {
18
+ const [key, val] = arg.slice(2).split('=');
19
+ const booleanFlags = ['memory'];
20
+ if (val != null) {
21
+ flags[key] = val;
22
+ } else if (booleanFlags.includes(key)) {
23
+ flags[key] = 'true';
24
+ } else {
25
+ flags[key] = args[++i] ?? '';
26
+ }
27
+ } else if (!configPath) {
28
+ configPath = arg;
29
+ }
30
+ }
31
+
32
+ // Load config file
33
+ let options: Record<string, unknown> = {};
34
+ if (configPath) {
35
+ const abs = resolve(configPath);
36
+ if (configPath.endsWith('.ts') || configPath.endsWith('.js')) {
37
+ const mod = await import(abs);
38
+ options = { ...(mod.default ?? mod.config ?? mod) };
39
+ } else if (configPath.endsWith('.json')) {
40
+ const text = require('fs').readFileSync(abs, 'utf-8');
41
+ const parsed = JSON.parse(text);
42
+ options = { ...(parsed.config ?? parsed) };
43
+ }
44
+ }
45
+
46
+ // CLI flags override config
47
+ if (flags.port) options.port = parseInt(flags.port, 10);
48
+ if (flags.path) options.path = flags.path;
49
+ if (flags.memory) options.path = ':memory:';
50
+
51
+ // Ensure db directory exists
52
+ const dbPath = (options.path as string) ?? ':memory:';
53
+ if (dbPath !== ':memory:') {
54
+ const dir = dirname(resolve(dbPath));
55
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
56
+ }
57
+
58
+ const db = await BodDB.create(options as any);
59
+ const server = db.serve();
60
+ const port = server?.port ?? options.port ?? 4400;
61
+
62
+ const line = (s: string) => ` │ ${s.padEnd(36)}│`;
63
+ console.log([
64
+ '',
65
+ ' ┌──────────────────────────────────────┐',
66
+ line(` BodDB v${getVersion()}`),
67
+ ' ├──────────────────────────────────────┤',
68
+ line(` Port: ${port}`),
69
+ line(` DB: ${dbPath === ':memory:' ? ':memory:' : resolve(dbPath)}`),
70
+ line(` Config: ${configPath ?? 'defaults'}`),
71
+ line(''),
72
+ line(` REST: http://localhost:${port}`),
73
+ line(` WS: ws://localhost:${port}`),
74
+ ' └──────────────────────────────────────┘',
75
+ '',
76
+ ].join('\n'));
77
+
78
+ // Admin stats (for admin UI stats bar)
79
+ if (options.transport?.staticRoutes) {
80
+ const { statSync } = require('fs');
81
+ let lastCpu = process.cpuUsage();
82
+ let lastTime = performance.now();
83
+ let lastOsCpus = cpus();
84
+ const systemCpuPercent = (): number => {
85
+ const cur = cpus();
86
+ let idleDelta = 0, totalDelta = 0;
87
+ for (let i = 0; i < cur.length; i++) {
88
+ const prev = lastOsCpus[i]?.times ?? cur[i].times;
89
+ const c = cur[i].times;
90
+ idleDelta += c.idle - prev.idle;
91
+ totalDelta += (c.user + c.nice + c.sys + c.irq + c.idle) - (prev.user + prev.nice + prev.sys + prev.irq + prev.idle);
92
+ }
93
+ lastOsCpus = cur;
94
+ return totalDelta > 0 ? +((1 - idleDelta / totalDelta) * 100).toFixed(1) : 0;
95
+ };
96
+ setInterval(() => {
97
+ if (!db.subs.subscriberCount('_admin')) return;
98
+ const now = performance.now();
99
+ const cpu = process.cpuUsage();
100
+ const elapsedUs = (now - lastTime) * 1000;
101
+ const cpuPercent = +((cpu.user - lastCpu.user + cpu.system - lastCpu.system) / elapsedUs * 100).toFixed(1);
102
+ lastCpu = cpu; lastTime = now;
103
+ const mem = process.memoryUsage();
104
+ const nodeCount = (db.storage.db.query('SELECT COUNT(*) as n FROM nodes WHERE mq_status IS NULL').get() as any).n;
105
+ let dbSizeMb = 0;
106
+ try { dbSizeMb = +(statSync(resolve(dbPath)).size / 1024 / 1024).toFixed(2); } catch {}
107
+ db.set('_admin/stats', {
108
+ process: { cpuPercent, heapUsedMb: +(mem.heapUsed / 1024 / 1024).toFixed(2), rssMb: +(mem.rss / 1024 / 1024).toFixed(2), uptimeSec: Math.floor(process.uptime()) },
109
+ db: { nodeCount, sizeMb: dbSizeMb },
110
+ system: { cpuCores: cpus().length, totalMemMb: +(totalmem() / 1024 / 1024).toFixed(0), cpuPercent: systemCpuPercent() },
111
+ subs: db.subs.subscriberCount(),
112
+ clients: db.transport?.clientCount ?? 0,
113
+ ts: Date.now(),
114
+ });
115
+ }, 1000);
116
+ }
117
+
118
+ // Graceful shutdown
119
+ process.on('SIGINT', () => { db.close(); process.exit(0); });
120
+ process.on('SIGTERM', () => { db.close(); process.exit(0); });
121
+
122
+ function getVersion(): string {
123
+ try { return require('./package.json').version; } catch { return '0.0.0'; }
124
+ }
125
+
126
+ function printVersion() {
127
+ console.log(`bod-db v${getVersion()}`);
128
+ }
129
+
130
+ function printHelp() {
131
+ console.log(`
132
+ bod-db — embedded reactive database
133
+
134
+ Usage:
135
+ bod-db [config] Start server with config file (.ts/.js/.json)
136
+ bod-db --init [path] Generate a starter config file
137
+
138
+ Options:
139
+ --port <n> Override port (default: 4400)
140
+ --path <file> Override SQLite path
141
+ --memory Use in-memory database
142
+ -v, --version Print version
143
+ -h, --help Show this help
144
+
145
+ Examples:
146
+ bod-db Start with defaults (memory, port 4400)
147
+ bod-db config.ts Start with config file
148
+ bod-db --port 3000 Start on port 3000
149
+ bod-db config.ts --port 5000 Config + port override
150
+ bod-db --init Generate ./bod.config.ts
151
+ `);
152
+ }
153
+
154
+ async function initConfig(dest?: string) {
155
+ const target = resolve(dest ?? 'bod.config.ts');
156
+ if (existsSync(target)) {
157
+ console.log(` ✗ ${target} already exists`);
158
+ process.exit(1);
159
+ }
160
+ const template = `import type { BodDBOptions } from 'bod-db';
161
+
162
+ export default {
163
+ path: './.tmp/bod.db',
164
+ port: 4400,
165
+ sweepInterval: 60000,
166
+ rules: {
167
+ '': { read: true, write: true },
168
+ },
169
+ indexes: {},
170
+ fts: {},
171
+ vectors: { dimensions: 384 },
172
+ mq: { visibilityTimeout: 30, maxDeliveries: 5 },
173
+ } satisfies Partial<BodDBOptions>;
174
+ `;
175
+ require('fs').writeFileSync(target, template);
176
+ console.log(` ✓ Created ${target}`);
177
+ }
package/client.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { BodClient, BodClientOptions, ValueSnapshot, ClientQueryBuilder, StreamReader, MQReader, MQMessageSnapshot } from './src/client/BodClient.ts';
2
+ export type { ChildEvent, StreamEventSnapshot } from './src/client/BodClient.ts';
package/config.ts ADDED
@@ -0,0 +1,20 @@
1
+ import type { BodDBOptions } from './src/server/BodDB.ts';
2
+
3
+ export default {
4
+ path: './.tmp/bod.db',
5
+ port: 4400,
6
+ sweepInterval: 60000,
7
+ rules: {
8
+ '': { read: true, write: true },
9
+ },
10
+ indexes: {
11
+ 'users': ['role', 'createdAt'],
12
+ 'posts': ['authorId', 'createdAt', 'likes'],
13
+ },
14
+ fts: {},
15
+ vectors: { dimensions: 384 },
16
+ mq: { visibilityTimeout: 30, maxDeliveries: 5 },
17
+ compact: {
18
+ 'events/logs': { maxAge: 86400 },
19
+ },
20
+ } satisfies Partial<BodDBOptions>;
@@ -0,0 +1 @@
1
+ SSH_KEY_PATH=~/.ssh/id_ed25519
@@ -0,0 +1,18 @@
1
+ droplet:
2
+ host: 157.151.221.191
3
+ user: ubuntu
4
+ ssh:
5
+ privateKey: ~/.ssh/oracle_cloud
6
+ app:
7
+ user: ubuntu
8
+ runtime:
9
+ host: 0.0.0.0
10
+ nodeEnv: production
11
+ service:
12
+ restart: always
13
+ restartSec: 2
14
+ killSignal: SIGINT
15
+ https:
16
+ email: admin@livshitz.com
17
+ deploy:
18
+ excludes: [.git, node_modules, .tmp, tests, .claude, .cursor, bun.lock, data]
@@ -0,0 +1,10 @@
1
+ app:
2
+ name: boddb-logs
3
+ dir: /opt/boddb-logs
4
+ runtime:
5
+ port: 4401
6
+ service:
7
+ name: boddb-logs
8
+ execStart: /home/ubuntu/.bun/bin/bun run cli.ts deploy/prod-logs.config.ts
9
+ https:
10
+ domain: db-logs.bod.ee
@@ -0,0 +1,10 @@
1
+ app:
2
+ name: boddb
3
+ dir: /opt/boddb
4
+ runtime:
5
+ port: 4400
6
+ service:
7
+ name: boddb
8
+ execStart: /home/ubuntu/.bun/bin/bun run cli.ts deploy/prod.config.ts
9
+ https:
10
+ domain: db-main.bod.ee
@@ -0,0 +1,196 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>BodDB Remote Demo</title>
7
+ <style>
8
+ * { margin: 0; padding: 0; box-sizing: border-box; }
9
+ body { font-family: system-ui, sans-serif; background: #0a0a0a; color: #e0e0e0; padding: 2rem; max-width: 800px; margin: 0 auto; }
10
+ h1 { font-size: 1.5rem; margin-bottom: 0.5rem; }
11
+ h2 { font-size: 1.1rem; margin: 1.5rem 0 0.5rem; color: #888; }
12
+ .status { padding: 0.5rem 1rem; border-radius: 6px; margin-bottom: 1rem; font-size: 0.9rem; }
13
+ .status.connected { background: #0a2e0a; border: 1px solid #1a5c1a; }
14
+ .status.disconnected { background: #2e0a0a; border: 1px solid #5c1a1a; }
15
+ .status.connecting { background: #2e2e0a; border: 1px solid #5c5c1a; }
16
+ .log { background: #111; border: 1px solid #333; border-radius: 6px; padding: 1rem; max-height: 200px; overflow-y: auto; font-family: monospace; font-size: 0.85rem; line-height: 1.6; }
17
+ .log .time { color: #666; }
18
+ .log .op { color: #6cf; }
19
+ .log .data { color: #8f8; }
20
+ .log .err { color: #f66; }
21
+ .controls { display: flex; gap: 0.5rem; flex-wrap: wrap; margin: 0.5rem 0; }
22
+ button { background: #222; border: 1px solid #444; color: #e0e0e0; padding: 0.4rem 0.8rem; border-radius: 4px; cursor: pointer; font-size: 0.85rem; }
23
+ button:hover { background: #333; }
24
+ input { background: #111; border: 1px solid #333; color: #e0e0e0; padding: 0.4rem 0.6rem; border-radius: 4px; font-size: 0.85rem; font-family: monospace; }
25
+ .row { display: flex; gap: 0.5rem; align-items: center; margin: 0.3rem 0; }
26
+ .live-value { background: #0a1a2e; border: 1px solid #1a3a5c; border-radius: 6px; padding: 1rem; font-family: monospace; font-size: 0.9rem; min-height: 3rem; white-space: pre-wrap; }
27
+ small { color: #666; }
28
+ #url { width: 300px; }
29
+ </style>
30
+ </head>
31
+ <body>
32
+
33
+ <h1>BodDB Remote Demo</h1>
34
+ <div class="row">
35
+ <input id="url" value="wss://db-main.bod.ee" placeholder="wss://...">
36
+ <button onclick="doConnect()">Connect</button>
37
+ <button onclick="doDisconnect()">Disconnect</button>
38
+ </div>
39
+ <div id="status" class="status disconnected">Disconnected</div>
40
+
41
+ <h2>CRUD</h2>
42
+ <div class="controls">
43
+ <button onclick="doSet()">Set demo/hello</button>
44
+ <button onclick="doGet()">Get demo/hello</button>
45
+ <button onclick="doDelete()">Delete demo/hello</button>
46
+ <button onclick="doPush()">Push to demo/logs</button>
47
+ <button onclick="doGetLogs()">Get demo/logs</button>
48
+ </div>
49
+
50
+ <h2>Live Subscription <small>(demo/counter)</small></h2>
51
+ <div class="controls">
52
+ <button onclick="doSubscribe()">Subscribe</button>
53
+ <button onclick="doUnsubscribe()">Unsubscribe</button>
54
+ <button onclick="doIncrement()">Increment</button>
55
+ </div>
56
+ <div id="live" class="live-value">—</div>
57
+
58
+ <h2>Event Log</h2>
59
+ <div id="log" class="log"></div>
60
+
61
+ <script>
62
+ let ws = null;
63
+ let msgId = 0;
64
+ let pending = {};
65
+ let subActive = false;
66
+
67
+ function log(op, data, isErr) {
68
+ const el = document.getElementById('log');
69
+ const time = new Date().toLocaleTimeString();
70
+ const cls = isErr ? 'err' : 'data';
71
+ el.innerHTML += `<div><span class="time">${time}</span> <span class="op">[${op}]</span> <span class="${cls}">${typeof data === 'string' ? data : JSON.stringify(data)}</span></div>`;
72
+ el.scrollTop = el.scrollHeight;
73
+ }
74
+
75
+ function setStatus(s) {
76
+ const el = document.getElementById('status');
77
+ el.textContent = s.charAt(0).toUpperCase() + s.slice(1);
78
+ el.className = 'status ' + s;
79
+ }
80
+
81
+ function send(op, params) {
82
+ return new Promise((resolve, reject) => {
83
+ const id = String(++msgId);
84
+ pending[id] = { resolve, reject };
85
+ ws.send(JSON.stringify({ id, op, ...params }));
86
+ setTimeout(() => { if (pending[id]) { delete pending[id]; reject(new Error('timeout')); } }, 10000);
87
+ });
88
+ }
89
+
90
+ function doConnect() {
91
+ if (ws && ws.readyState === WebSocket.OPEN) return;
92
+ const url = document.getElementById('url').value;
93
+ setStatus('connecting');
94
+ ws = new WebSocket(url);
95
+ ws.onopen = () => { setStatus('connected'); log('connect', 'Connected to ' + url); };
96
+ ws.onclose = () => { setStatus('disconnected'); log('close', 'Disconnected'); };
97
+ ws.onerror = (e) => { log('error', 'WebSocket error', true); };
98
+ ws.onmessage = (e) => {
99
+ const msg = JSON.parse(e.data);
100
+ // subscription event
101
+ if (msg.type === 'value') {
102
+ document.getElementById('live').textContent = JSON.stringify(msg.data, null, 2);
103
+ log('sub:value', { path: msg.path, data: msg.data });
104
+ return;
105
+ }
106
+ // request response
107
+ if (msg.id && pending[msg.id]) {
108
+ const p = pending[msg.id];
109
+ delete pending[msg.id];
110
+ if (msg.ok) p.resolve(msg.data);
111
+ else p.reject(new Error(msg.error));
112
+ }
113
+ };
114
+ }
115
+
116
+ function doDisconnect() { if (ws) { ws.close(); ws = null; subActive = false; } }
117
+
118
+ async function timed(op, fn) {
119
+ const t0 = performance.now();
120
+ try {
121
+ const result = await fn();
122
+ const ms = (performance.now() - t0).toFixed(1);
123
+ return { result, ms };
124
+ } catch (e) {
125
+ const ms = (performance.now() - t0).toFixed(1);
126
+ log(op, `${e.message} (${ms}ms)`, true);
127
+ throw e;
128
+ }
129
+ }
130
+
131
+ async function doSet() {
132
+ try {
133
+ const { ms } = await timed('set', () => send('set', { path: 'demo/hello', value: { message: 'Hello from browser!', ts: Date.now() } }));
134
+ log('set', `demo/hello written (${ms}ms)`);
135
+ } catch (e) {}
136
+ }
137
+
138
+ async function doGet() {
139
+ try {
140
+ const { result, ms } = await timed('get', () => send('get', { path: 'demo/hello' }));
141
+ log('get', { data: result, ms: ms + 'ms' });
142
+ } catch (e) {}
143
+ }
144
+
145
+ async function doDelete() {
146
+ try {
147
+ const { ms } = await timed('delete', () => send('delete', { path: 'demo/hello' }));
148
+ log('delete', `demo/hello deleted (${ms}ms)`);
149
+ } catch (e) {}
150
+ }
151
+
152
+ async function doPush() {
153
+ try {
154
+ const { result, ms } = await timed('push', () => send('push', { path: 'demo/logs', value: { msg: 'event-' + Date.now(), ts: Date.now() } }));
155
+ log('push', `pushed key: ${result} (${ms}ms)`);
156
+ } catch (e) {}
157
+ }
158
+
159
+ async function doGetLogs() {
160
+ try {
161
+ const { result, ms } = await timed('get', () => send('get', { path: 'demo/logs' }));
162
+ log('get', { data: result, ms: ms + 'ms' });
163
+ } catch (e) {}
164
+ }
165
+
166
+ async function doSubscribe() {
167
+ if (subActive) return;
168
+ try {
169
+ const { ms } = await timed('sub', () => send('sub', { path: 'demo/counter', event: 'value' }));
170
+ subActive = true;
171
+ log('sub', `subscribed to demo/counter (${ms}ms)`);
172
+ } catch (e) {}
173
+ }
174
+
175
+ async function doUnsubscribe() {
176
+ if (!subActive) return;
177
+ try {
178
+ const { ms } = await timed('unsub', () => send('unsub', { path: 'demo/counter', event: 'value' }));
179
+ subActive = false;
180
+ document.getElementById('live').textContent = '—';
181
+ log('unsub', `unsubscribed from demo/counter (${ms}ms)`);
182
+ } catch (e) {}
183
+ }
184
+
185
+ async function doIncrement() {
186
+ try {
187
+ const t0 = performance.now();
188
+ const cur = await send('get', { path: 'demo/counter' });
189
+ await send('set', { path: 'demo/counter', value: (typeof cur === 'number' ? cur : 0) + 1 });
190
+ const ms = (performance.now() - t0).toFixed(1);
191
+ log('set', `demo/counter incremented (${ms}ms total)`);
192
+ } catch (e) { log('set', e.message, true); }
193
+ }
194
+ </script>
195
+ </body>
196
+ </html>
@@ -0,0 +1,32 @@
1
+ // Usage: bun run deploy/deploy.ts <instance> [bootstrap|deploy|logs|ssh|provision]
2
+ // Deep-merges deploy/base.yaml + deploy/<instance>.yaml, writes .tmp/vmdrop.yaml, runs vmdrop
3
+
4
+ import { readFileSync, writeFileSync, mkdirSync } from 'fs';
5
+ import { parse, stringify } from 'yaml';
6
+
7
+ const [instance, cmd = 'deploy', ...rest] = process.argv.slice(2);
8
+ if (!instance) { console.error('Usage: bun deploy/deploy.ts <instance> [cmd]'); process.exit(1); }
9
+
10
+ const base = parse(readFileSync('deploy/base.yaml', 'utf8'));
11
+ const override = parse(readFileSync(`deploy/${instance}.yaml`, 'utf8'));
12
+ const merged = deepMerge(base, override);
13
+
14
+ mkdirSync('.tmp', { recursive: true });
15
+ writeFileSync('.tmp/vmdrop.yaml', stringify(merged));
16
+
17
+ const proc = Bun.spawn(['bunx', 'vmdrop', cmd, '--config', '.tmp/vmdrop.yaml', ...rest], {
18
+ stdio: ['inherit', 'inherit', 'inherit'],
19
+ });
20
+ await proc.exited;
21
+ process.exit(proc.exitCode ?? 0);
22
+
23
+ function deepMerge(a: any, b: any): any {
24
+ if (!b) return a;
25
+ const result = { ...a };
26
+ for (const key of Object.keys(b)) {
27
+ result[key] = (typeof a[key] === 'object' && typeof b[key] === 'object' && !Array.isArray(b[key]))
28
+ ? deepMerge(a[key], b[key])
29
+ : b[key];
30
+ }
31
+ return result;
32
+ }
@@ -0,0 +1,15 @@
1
+ export default {
2
+ path: './data/bod.db',
3
+ port: 4401,
4
+ sweepInterval: 60000,
5
+ rules: { '': { read: true, write: true } },
6
+ indexes: {},
7
+ fts: {},
8
+ mq: { visibilityTimeout: 30, maxDeliveries: 5 },
9
+ transport: {
10
+ staticRoutes: {
11
+ '/admin': './admin/ui.html',
12
+ '/': './admin/ui.html',
13
+ },
14
+ },
15
+ }
@@ -0,0 +1,15 @@
1
+ export default {
2
+ path: './data/bod.db',
3
+ port: 4400,
4
+ sweepInterval: 60000,
5
+ rules: { '': { read: true, write: true } },
6
+ indexes: {},
7
+ fts: {},
8
+ mq: { visibilityTimeout: 30, maxDeliveries: 5 },
9
+ transport: {
10
+ staticRoutes: {
11
+ '/admin': './admin/ui.html',
12
+ '/': './admin/ui.html',
13
+ },
14
+ },
15
+ }
package/index.ts ADDED
@@ -0,0 +1,20 @@
1
+ export { BodDB, BodDBOptions } from './src/server/BodDB.ts';
2
+ export type { TransactionProxy } from './src/server/BodDB.ts';
3
+ export { StorageEngine, StorageEngineOptions, generatePushId } from './src/server/StorageEngine.ts';
4
+ export { SubscriptionEngine } from './src/server/SubscriptionEngine.ts';
5
+ export { QueryEngine } from './src/server/QueryEngine.ts';
6
+ export { RulesEngine, RulesEngineOptions } from './src/server/RulesEngine.ts';
7
+ export type { RuleContext, PathRule } from './src/server/RulesEngine.ts';
8
+ export { Transport, TransportOptions } from './src/server/Transport.ts';
9
+ export { FTSEngine, FTSEngineOptions } from './src/server/FTSEngine.ts';
10
+ export { VectorEngine, VectorEngineOptions } from './src/server/VectorEngine.ts';
11
+ export { StreamEngine, StreamEngineOptions } from './src/server/StreamEngine.ts';
12
+ export { MQEngine, MQEngineOptions } from './src/server/MQEngine.ts';
13
+ export type { MQMessage } from './src/server/MQEngine.ts';
14
+ export type { StreamEvent, CompactOptions, CompactResult, StreamSnapshot } from './src/server/StreamEngine.ts';
15
+ export { FileAdapter, FileAdapterOptions } from './src/server/FileAdapter.ts';
16
+ export { compileRule } from './src/server/ExpressionRules.ts';
17
+ export { increment, serverTimestamp, arrayUnion, arrayRemove, ref } from './src/shared/transforms.ts';
18
+ export * from './src/shared/protocol.ts';
19
+ export * from './src/shared/errors.ts';
20
+ export { normalizePath, validatePath, ancestors, parentPath, pathKey, prefixEnd, flatten, reconstruct } from './src/shared/pathUtils.ts';
package/mcp.ts ADDED
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env bun
2
+ import { BodClient } from './src/client/BodClient.ts';
3
+ import { MCPAdapter } from './src/server/MCPAdapter.ts';
4
+
5
+ const args = process.argv.slice(2);
6
+ const flags: Record<string, string> = {};
7
+
8
+ for (let i = 0; i < args.length; i++) {
9
+ const arg = args[i];
10
+ if (arg === '--help' || arg === '-h') { printHelp(); process.exit(0); }
11
+ if (arg.startsWith('--')) {
12
+ const [key, val] = arg.slice(2).split('=');
13
+ const booleanFlags = ['stdio', 'http'];
14
+ if (val != null) flags[key] = val;
15
+ else if (booleanFlags.includes(key)) flags[key] = 'true';
16
+ else flags[key] = args[++i] ?? '';
17
+ }
18
+ }
19
+
20
+ const url = flags.url ?? `ws://localhost:${flags.port ?? '4400'}`;
21
+ const mode = flags.http ? 'http' : 'stdio';
22
+
23
+ const client = new BodClient({ url });
24
+ try {
25
+ await client.connect();
26
+ } catch (e: any) {
27
+ console.error(`Failed to connect to ${url} — is BodDB running? (${e.message ?? e})`);
28
+ process.exit(1);
29
+ }
30
+ console.error(`bod-db MCP connected to ${url}`);
31
+
32
+ const mcp = new MCPAdapter(client);
33
+
34
+ if (mode === 'stdio') {
35
+ mcp.serveStdio();
36
+ } else {
37
+ const httpPort = parseInt(flags['http-port'] ?? flags.port ?? '4401', 10);
38
+ const handler = mcp.httpHandler();
39
+ Bun.serve({ port: httpPort, fetch: handler });
40
+ console.error(`bod-db MCP HTTP on http://localhost:${httpPort}`);
41
+ }
42
+
43
+ process.on('SIGINT', () => process.exit(0));
44
+ process.on('SIGTERM', () => process.exit(0));
45
+
46
+ function printHelp() {
47
+ console.log(`
48
+ bod-db-mcp — MCP client for a running BodDB server
49
+
50
+ Usage:
51
+ bod-db-mcp [options]
52
+
53
+ Transport (pick one):
54
+ --stdio Line-delimited JSON-RPC on stdin/stdout (default)
55
+ --http HTTP POST JSON-RPC server
56
+
57
+ Options:
58
+ --url <ws://...> BodDB server URL (default: ws://localhost:4400)
59
+ --port <n> Shorthand for --url ws://localhost:<n>
60
+ --http-port <n> HTTP listen port when using --http (default: 4401)
61
+ -h, --help Show this help
62
+
63
+ Examples:
64
+ bod-db-mcp --stdio
65
+ bod-db-mcp --stdio --port 4400
66
+ bod-db-mcp --http --http-port 4401
67
+
68
+ Claude Code .mcp.json:
69
+ {
70
+ "mcpServers": {
71
+ "bod-db": {
72
+ "command": "bun",
73
+ "args": ["run", "${import.meta.dir}/mcp.ts", "--stdio"]
74
+ }
75
+ }
76
+ }
77
+ `);
78
+ }
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@bod.ee/db",
3
+ "version": "0.7.0",
4
+ "module": "index.ts",
5
+ "type": "module",
6
+ "bin": {
7
+ "bod-db": "cli.ts",
8
+ "bod-db-mcp": "mcp.ts"
9
+ },
10
+ "devDependencies": {
11
+ "@types/bun": "latest",
12
+ "yaml": "^2.8.2"
13
+ },
14
+ "scripts": {
15
+ "admin": "bun --watch run admin/server.ts",
16
+ "serve": "bun run cli.ts",
17
+ "start": "bun run cli.ts config.ts",
18
+ "publish-lib": "bun publish --access public",
19
+ "mcp": "bun run mcp.ts --stdio",
20
+ "deploy": "bun run deploy/deploy.ts boddb deploy",
21
+ "deploy-logs-db": "bun run deploy/deploy.ts boddb-logs ",
22
+ "deploy:bootstrap": "bun run deploy/deploy.ts boddb bootstrap",
23
+ "deploy:logs": "bun run deploy/deploy.ts boddb logs",
24
+ "deploy:ssh": "bun run deploy/deploy.ts boddb ssh"
25
+ },
26
+ "peerDependencies": {
27
+ "typescript": "^5"
28
+ }
29
+ }
package/react.ts ADDED
@@ -0,0 +1 @@
1
+ export { useValue, useChildren, useQuery, useMutation } from './src/react/hooks.ts';