radio-faf 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 +21 -0
- package/README.md +129 -0
- package/dist/index.js +223 -0
- package/package.json +58 -0
- package/project.faf +116 -0
- package/src/index.ts +19 -0
- package/src/radio.ts +356 -0
- package/src/types.ts +136 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Wolfe James
|
|
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,129 @@
|
|
|
1
|
+
# mcpaas
|
|
2
|
+
|
|
3
|
+
**Your AI forgets you every session. MCPaaS remembers.**
|
|
4
|
+
|
|
5
|
+
You re-explain your stack to Claude. Switch to Gemini — re-explain everything. Open Cursor — again. Every session. Every tool. Every time.
|
|
6
|
+
|
|
7
|
+
That's the drift tax. [$49M/day](https://fafdev.tools/value) burned industry-wide.
|
|
8
|
+
|
|
9
|
+
This SDK connects you to [MCPaaS](https://mcpaas.live) — persistent AI context that follows you across tools, sessions, and teams.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install mcpaas
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Quick Start
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
import { Radio } from 'mcpaas';
|
|
21
|
+
|
|
22
|
+
const radio = new Radio('wss://mcpaas.live/beacon/radio');
|
|
23
|
+
await radio.connect();
|
|
24
|
+
|
|
25
|
+
// Tune to your project's frequency
|
|
26
|
+
await radio.tune('91.0');
|
|
27
|
+
|
|
28
|
+
// Your context arrives — from any AI, any session
|
|
29
|
+
radio.onBroadcast((frequency, context) => {
|
|
30
|
+
console.log(`Context update on ${frequency} FM:`, context);
|
|
31
|
+
});
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
That's it. Your context persists. Zero drift.
|
|
35
|
+
|
|
36
|
+
## How It Works
|
|
37
|
+
|
|
38
|
+
MCPaaS uses the **Radio Protocol** — broadcast once, every AI receives.
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
Traditional (the tax):
|
|
42
|
+
You → Claude (send 50KB context)
|
|
43
|
+
You → Grok (send 50KB again)
|
|
44
|
+
You → Gemini (send 50KB again)
|
|
45
|
+
= 3x cost, 3x latency, context drift
|
|
46
|
+
|
|
47
|
+
Radio Protocol (the fix):
|
|
48
|
+
You → Broadcast to 91.0 FM (send once)
|
|
49
|
+
Claude ← tuned to 91.0
|
|
50
|
+
Grok ← tuned to 91.0
|
|
51
|
+
Gemini ← tuned to 91.0
|
|
52
|
+
= 1x cost, instant, zero drift
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Multi-AI Example
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
import { Radio } from 'mcpaas';
|
|
59
|
+
|
|
60
|
+
const claude = new Radio('wss://mcpaas.live/beacon/radio');
|
|
61
|
+
const grok = new Radio('wss://mcpaas.live/beacon/radio');
|
|
62
|
+
const gemini = new Radio('wss://mcpaas.live/beacon/radio');
|
|
63
|
+
|
|
64
|
+
await Promise.all([claude.connect(), grok.connect(), gemini.connect()]);
|
|
65
|
+
await Promise.all([claude.tune('91.0'), grok.tune('91.0'), gemini.tune('91.0')]);
|
|
66
|
+
|
|
67
|
+
// All three AIs now share the same context, in real time
|
|
68
|
+
claude.onBroadcast((f, ctx) => console.log('Claude:', ctx));
|
|
69
|
+
grok.onBroadcast((f, ctx) => console.log('Grok:', ctx));
|
|
70
|
+
gemini.onBroadcast((f, ctx) => console.log('Gemini:', ctx));
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## API
|
|
74
|
+
|
|
75
|
+
### Constructor
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
const radio = new Radio(url: string, options?: {
|
|
79
|
+
reconnect?: boolean; // Auto-reconnect (default: true)
|
|
80
|
+
maxReconnectAttempts?: number; // Max attempts (default: 5)
|
|
81
|
+
reconnectDelay?: number; // Delay in ms (default: 1000)
|
|
82
|
+
heartbeatInterval?: number; // Ping interval (default: 30000)
|
|
83
|
+
debug?: boolean; // Debug logging (default: false)
|
|
84
|
+
});
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Methods
|
|
88
|
+
|
|
89
|
+
| Method | Description |
|
|
90
|
+
|--------|-------------|
|
|
91
|
+
| `connect()` | Connect to MCPaaS |
|
|
92
|
+
| `tune(frequency)` | Subscribe to a frequency |
|
|
93
|
+
| `untune(frequency)` | Unsubscribe from a frequency |
|
|
94
|
+
| `disconnect()` | Disconnect |
|
|
95
|
+
| `getState()` | `'DISCONNECTED'` \| `'CONNECTING'` \| `'CONNECTED'` \| `'RECONNECTING'` \| `'CLOSED'` |
|
|
96
|
+
| `getTunedFrequencies()` | Set of tuned frequencies |
|
|
97
|
+
|
|
98
|
+
### Events
|
|
99
|
+
|
|
100
|
+
| Event | Callback |
|
|
101
|
+
|-------|----------|
|
|
102
|
+
| `onBroadcast(fn)` | `(frequency, context) => void` |
|
|
103
|
+
| `onConnect(fn)` | `() => void` |
|
|
104
|
+
| `onDisconnect(fn)` | `(code?, reason?) => void` |
|
|
105
|
+
| `onError(fn)` | `(error) => void` |
|
|
106
|
+
| `onTuned(fn)` | `(frequency) => void` |
|
|
107
|
+
| `onUntuned(fn)` | `(frequency) => void` |
|
|
108
|
+
|
|
109
|
+
## Why Bun?
|
|
110
|
+
|
|
111
|
+
Anthropic acquired Bun in December 2025. Bun now powers Claude Code. This library is built for Bun first — native TypeScript, 7x faster WebSockets, zero config.
|
|
112
|
+
|
|
113
|
+
## Namepoints
|
|
114
|
+
|
|
115
|
+
Every frequency maps to a **namepoint** on MCPaaS — your permanent AI identity.
|
|
116
|
+
|
|
117
|
+
Free namepoints work. But `yourname.mcpaas.live` hits different than `user-38291.mcpaas.live`.
|
|
118
|
+
|
|
119
|
+
**Claim yours before someone else does:** [mcpaas.live/claim](https://mcpaas.live/claim)
|
|
120
|
+
|
|
121
|
+
If `mcpaas` has been useful, consider starring the repo — it helps others find it.
|
|
122
|
+
|
|
123
|
+
## License
|
|
124
|
+
|
|
125
|
+
MIT
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
Built by [Wolfe James](https://github.com/Wolfe-Jam) | Powered by [MCPaaS](https://mcpaas.live) | Format: [FAF](https://faf.one)
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/radio.ts
|
|
3
|
+
class Radio {
|
|
4
|
+
url;
|
|
5
|
+
ws = null;
|
|
6
|
+
state = "DISCONNECTED";
|
|
7
|
+
options;
|
|
8
|
+
handlers = {};
|
|
9
|
+
reconnectAttempts = 0;
|
|
10
|
+
heartbeatTimer = null;
|
|
11
|
+
tunedFrequencies = new Set;
|
|
12
|
+
constructor(url, options = {}) {
|
|
13
|
+
this.url = url;
|
|
14
|
+
this.options = {
|
|
15
|
+
reconnect: options.reconnect ?? true,
|
|
16
|
+
maxReconnectAttempts: options.maxReconnectAttempts ?? 5,
|
|
17
|
+
reconnectDelay: options.reconnectDelay ?? 1000,
|
|
18
|
+
heartbeatInterval: options.heartbeatInterval ?? 30000,
|
|
19
|
+
debug: options.debug ?? false
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
async connect() {
|
|
23
|
+
if (this.state === "CONNECTED" || this.state === "CONNECTING") {
|
|
24
|
+
this.log("Already connected or connecting");
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
return new Promise((resolve, reject) => {
|
|
28
|
+
this.state = "CONNECTING";
|
|
29
|
+
this.log(`Connecting to ${this.url}...`);
|
|
30
|
+
try {
|
|
31
|
+
this.ws = new WebSocket(this.url);
|
|
32
|
+
this.ws.onopen = () => {
|
|
33
|
+
this.state = "CONNECTED";
|
|
34
|
+
this.reconnectAttempts = 0;
|
|
35
|
+
this.log("Connected to Radio Protocol");
|
|
36
|
+
this.startHeartbeat();
|
|
37
|
+
this.handlers.onConnect?.();
|
|
38
|
+
resolve();
|
|
39
|
+
};
|
|
40
|
+
this.ws.onmessage = (event) => {
|
|
41
|
+
this.handleMessage(event.data);
|
|
42
|
+
};
|
|
43
|
+
this.ws.onerror = (event) => {
|
|
44
|
+
const error = new Error("WebSocket error");
|
|
45
|
+
this.log("WebSocket error:", event);
|
|
46
|
+
this.handlers.onError?.(error);
|
|
47
|
+
reject(error);
|
|
48
|
+
};
|
|
49
|
+
this.ws.onclose = (event) => {
|
|
50
|
+
this.handleClose(event.code, event.reason);
|
|
51
|
+
};
|
|
52
|
+
} catch (error) {
|
|
53
|
+
this.state = "DISCONNECTED";
|
|
54
|
+
reject(error);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
async tune(frequency) {
|
|
59
|
+
this.validateFrequency(frequency);
|
|
60
|
+
this.ensureConnected();
|
|
61
|
+
const message = {
|
|
62
|
+
action: "tune",
|
|
63
|
+
frequencies: [frequency]
|
|
64
|
+
};
|
|
65
|
+
this.send(message);
|
|
66
|
+
this.tunedFrequencies.add(frequency);
|
|
67
|
+
this.log(`Tuning to ${frequency} FM...`);
|
|
68
|
+
}
|
|
69
|
+
async untune(frequency) {
|
|
70
|
+
this.validateFrequency(frequency);
|
|
71
|
+
this.ensureConnected();
|
|
72
|
+
const message = {
|
|
73
|
+
action: "untune",
|
|
74
|
+
frequencies: [frequency]
|
|
75
|
+
};
|
|
76
|
+
this.send(message);
|
|
77
|
+
this.tunedFrequencies.delete(frequency);
|
|
78
|
+
this.log(`Untuning from ${frequency} FM...`);
|
|
79
|
+
}
|
|
80
|
+
disconnect() {
|
|
81
|
+
this.stopHeartbeat();
|
|
82
|
+
if (this.ws) {
|
|
83
|
+
this.state = "CLOSED";
|
|
84
|
+
this.ws.close();
|
|
85
|
+
this.ws = null;
|
|
86
|
+
}
|
|
87
|
+
this.tunedFrequencies.clear();
|
|
88
|
+
this.log("Disconnected");
|
|
89
|
+
}
|
|
90
|
+
onBroadcast(handler) {
|
|
91
|
+
this.handlers.onBroadcast = handler;
|
|
92
|
+
}
|
|
93
|
+
onConnect(handler) {
|
|
94
|
+
this.handlers.onConnect = handler;
|
|
95
|
+
}
|
|
96
|
+
onDisconnect(handler) {
|
|
97
|
+
this.handlers.onDisconnect = handler;
|
|
98
|
+
}
|
|
99
|
+
onError(handler) {
|
|
100
|
+
this.handlers.onError = handler;
|
|
101
|
+
}
|
|
102
|
+
onTuned(handler) {
|
|
103
|
+
this.handlers.onTuned = handler;
|
|
104
|
+
}
|
|
105
|
+
onUntuned(handler) {
|
|
106
|
+
this.handlers.onUntuned = handler;
|
|
107
|
+
}
|
|
108
|
+
getState() {
|
|
109
|
+
return this.state;
|
|
110
|
+
}
|
|
111
|
+
getTunedFrequencies() {
|
|
112
|
+
return new Set(this.tunedFrequencies);
|
|
113
|
+
}
|
|
114
|
+
handleMessage(data) {
|
|
115
|
+
try {
|
|
116
|
+
const text = typeof data === "string" ? data : new TextDecoder().decode(data);
|
|
117
|
+
const message = JSON.parse(text);
|
|
118
|
+
this.log("Received:", message.type);
|
|
119
|
+
switch (message.type) {
|
|
120
|
+
case "connected":
|
|
121
|
+
this.log(`\u2705 ${message.message} (Client ID: ${message.clientId})`);
|
|
122
|
+
this.log("Available frequencies:", message.availableFrequencies);
|
|
123
|
+
break;
|
|
124
|
+
case "tuned":
|
|
125
|
+
message.frequencies.forEach((freq) => {
|
|
126
|
+
this.handlers.onTuned?.(freq);
|
|
127
|
+
this.log(`\u2705 Tuned to ${freq} FM`);
|
|
128
|
+
});
|
|
129
|
+
if (message.invalid.length > 0) {
|
|
130
|
+
this.log(`\u26A0\uFE0F Invalid frequencies: ${message.invalid.join(", ")}`);
|
|
131
|
+
}
|
|
132
|
+
break;
|
|
133
|
+
case "untuned":
|
|
134
|
+
message.frequencies.forEach((freq) => {
|
|
135
|
+
this.handlers.onUntuned?.(freq);
|
|
136
|
+
this.log(`\u2705 Untuned from ${freq} FM`);
|
|
137
|
+
});
|
|
138
|
+
break;
|
|
139
|
+
case "broadcast":
|
|
140
|
+
this.handlers.onBroadcast?.(message.frequency, message.event);
|
|
141
|
+
this.log(`\uD83D\uDCFB Broadcast from ${message.frequency} FM`);
|
|
142
|
+
break;
|
|
143
|
+
case "pong":
|
|
144
|
+
this.log("\uD83D\uDC93 Heartbeat");
|
|
145
|
+
break;
|
|
146
|
+
case "error":
|
|
147
|
+
const error = new Error(message.message);
|
|
148
|
+
this.handlers.onError?.(error);
|
|
149
|
+
this.log(`\u274C Error: ${message.message}`);
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
} catch (error) {
|
|
153
|
+
this.log("Failed to parse message:", error);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
handleClose(code, reason) {
|
|
157
|
+
this.stopHeartbeat();
|
|
158
|
+
this.state = "DISCONNECTED";
|
|
159
|
+
this.log(`Connection closed (${code}): ${reason}`);
|
|
160
|
+
this.handlers.onDisconnect?.(code, reason);
|
|
161
|
+
if (this.options.reconnect && this.reconnectAttempts < this.options.maxReconnectAttempts) {
|
|
162
|
+
this.reconnect();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
async reconnect() {
|
|
166
|
+
this.reconnectAttempts++;
|
|
167
|
+
this.state = "RECONNECTING";
|
|
168
|
+
const delay = this.options.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
|
|
169
|
+
this.log(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts}/${this.options.maxReconnectAttempts})...`);
|
|
170
|
+
await Bun.sleep(delay);
|
|
171
|
+
try {
|
|
172
|
+
await this.connect();
|
|
173
|
+
for (const freq of this.tunedFrequencies) {
|
|
174
|
+
await this.tune(freq);
|
|
175
|
+
}
|
|
176
|
+
} catch (error) {
|
|
177
|
+
this.log("Reconnection failed:", error);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
send(message) {
|
|
181
|
+
if (!this.ws || this.state !== "CONNECTED") {
|
|
182
|
+
throw new Error("Not connected to Radio Protocol server");
|
|
183
|
+
}
|
|
184
|
+
this.ws.send(JSON.stringify(message));
|
|
185
|
+
}
|
|
186
|
+
startHeartbeat() {
|
|
187
|
+
this.stopHeartbeat();
|
|
188
|
+
this.heartbeatTimer = setInterval(() => {
|
|
189
|
+
if (this.state === "CONNECTED") {
|
|
190
|
+
this.send({ action: "ping" });
|
|
191
|
+
}
|
|
192
|
+
}, this.options.heartbeatInterval);
|
|
193
|
+
}
|
|
194
|
+
stopHeartbeat() {
|
|
195
|
+
if (this.heartbeatTimer) {
|
|
196
|
+
clearInterval(this.heartbeatTimer);
|
|
197
|
+
this.heartbeatTimer = null;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
validateFrequency(frequency) {
|
|
201
|
+
const pattern = /^[0-9]{1,3}\.[0-9]$/;
|
|
202
|
+
if (!pattern.test(frequency)) {
|
|
203
|
+
throw new Error(`Invalid frequency format: ${frequency}. Expected XX.X (e.g., "91.5")`);
|
|
204
|
+
}
|
|
205
|
+
const num = parseFloat(frequency);
|
|
206
|
+
if (num < 40 || num > 108) {
|
|
207
|
+
throw new Error(`Frequency out of range: ${frequency}. Must be between 40.0 and 108.0`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
ensureConnected() {
|
|
211
|
+
if (this.state !== "CONNECTED") {
|
|
212
|
+
throw new Error("Not connected. Call connect() first.");
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
log(...args) {
|
|
216
|
+
if (this.options.debug) {
|
|
217
|
+
console.log("[Radio]", ...args);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
export {
|
|
222
|
+
Radio
|
|
223
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "radio-faf",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Radio Protocol client for RadioFAF — AI Context Broadcasting via WebSocket. Built for Bun.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./src/index.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"bun": "./src/index.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"src",
|
|
16
|
+
"dist",
|
|
17
|
+
"README.md",
|
|
18
|
+
"LICENSE",
|
|
19
|
+
"project.faf"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"dev": "bun --hot src/index.ts",
|
|
23
|
+
"test": "bun test",
|
|
24
|
+
"build": "bun build src/index.ts --outdir dist --target bun",
|
|
25
|
+
"prepublishOnly": "bun run build",
|
|
26
|
+
"example": "bun run examples/basic.ts"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"ai-context",
|
|
30
|
+
"persistent-context",
|
|
31
|
+
"mcpaas",
|
|
32
|
+
"websocket",
|
|
33
|
+
"bun",
|
|
34
|
+
"claude",
|
|
35
|
+
"grok",
|
|
36
|
+
"gemini",
|
|
37
|
+
"faf"
|
|
38
|
+
],
|
|
39
|
+
"author": "Wolfe James",
|
|
40
|
+
"license": "MIT",
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"bun-types": "^1.3.0"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"bun-types": "^1.3.0"
|
|
46
|
+
},
|
|
47
|
+
"engines": {
|
|
48
|
+
"bun": ">=1.3.0"
|
|
49
|
+
},
|
|
50
|
+
"repository": {
|
|
51
|
+
"type": "git",
|
|
52
|
+
"url": "git+https://github.com/Wolfe-Jam/faf-radio-bun.git"
|
|
53
|
+
},
|
|
54
|
+
"bugs": {
|
|
55
|
+
"url": "https://github.com/Wolfe-Jam/faf-radio-bun/issues"
|
|
56
|
+
},
|
|
57
|
+
"homepage": "https://radiofaf.com"
|
|
58
|
+
}
|
package/project.faf
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
faf_version: 2.5.0
|
|
2
|
+
generated: 2026-03-04T01:30:00.000Z
|
|
3
|
+
ai_scoring_system: 2025-09-20
|
|
4
|
+
ai_score: 85%
|
|
5
|
+
ai_confidence: HIGH
|
|
6
|
+
ai_value: 30_seconds_replaces_20_minutes_of_questions
|
|
7
|
+
ai_tldr:
|
|
8
|
+
project: 'mcpaas - "Persistent AI context SDK — your AI remembers across tools, sessions, and teams"'
|
|
9
|
+
stack: TypeScript, Bun, WebSocket
|
|
10
|
+
quality_bar: production_ready
|
|
11
|
+
current_focus: npm publish as mcpaas@0.1.0
|
|
12
|
+
your_role: Build features with perfect context
|
|
13
|
+
instant_context:
|
|
14
|
+
what_building: MCPaaS SDK — persistent AI context via Radio Protocol
|
|
15
|
+
tech_stack: TypeScript, Bun, WebSocket
|
|
16
|
+
main_language: TypeScript
|
|
17
|
+
key_files:
|
|
18
|
+
- src/radio.ts
|
|
19
|
+
- src/types.ts
|
|
20
|
+
- src/index.ts
|
|
21
|
+
- package.json
|
|
22
|
+
context_quality:
|
|
23
|
+
slots_filled: 18/21 (86%)
|
|
24
|
+
ai_confidence: HIGH
|
|
25
|
+
handoff_ready: true
|
|
26
|
+
missing_context:
|
|
27
|
+
- CI/CD pipeline
|
|
28
|
+
- Database
|
|
29
|
+
- Authentication
|
|
30
|
+
project:
|
|
31
|
+
name: mcpaas
|
|
32
|
+
goal: Persistent AI context SDK that connects to mcpaas.live via Radio Protocol
|
|
33
|
+
main_language: TypeScript
|
|
34
|
+
type: sdk
|
|
35
|
+
ai_instructions:
|
|
36
|
+
priority_order:
|
|
37
|
+
- 1. Read THIS .faf file first
|
|
38
|
+
- 2. Check CLAUDE.md for session context
|
|
39
|
+
- 3. Review package.json for dependencies
|
|
40
|
+
working_style:
|
|
41
|
+
code_first: true
|
|
42
|
+
explanations: minimal
|
|
43
|
+
quality_bar: zero_errors
|
|
44
|
+
testing: required
|
|
45
|
+
warnings:
|
|
46
|
+
- All TypeScript must pass strict mode
|
|
47
|
+
- Test coverage required for new features
|
|
48
|
+
- Package name is mcpaas (not faf-radio-bun)
|
|
49
|
+
stack:
|
|
50
|
+
package_manager: bun
|
|
51
|
+
targetUser: developers building AI applications
|
|
52
|
+
coreProblem: AI forgets context between sessions, tools, and teams
|
|
53
|
+
missionPurpose: WebSocket SDK for Radio Protocol — broadcast once, every AI receives
|
|
54
|
+
mainLanguage: TypeScript
|
|
55
|
+
runtime: Bun
|
|
56
|
+
frontend: None
|
|
57
|
+
backend: None
|
|
58
|
+
database: None
|
|
59
|
+
build: Bun
|
|
60
|
+
hosting: npm registry
|
|
61
|
+
cicd: None
|
|
62
|
+
testing: Bun Test + WJTTC (19/19 passing)
|
|
63
|
+
ui_library: None
|
|
64
|
+
preferences:
|
|
65
|
+
quality_bar: zero_errors
|
|
66
|
+
commit_style: conventional_emoji
|
|
67
|
+
response_style: concise_code_first
|
|
68
|
+
explanation_level: minimal
|
|
69
|
+
communication: direct
|
|
70
|
+
testing: required
|
|
71
|
+
documentation: as_needed
|
|
72
|
+
state:
|
|
73
|
+
phase: publish
|
|
74
|
+
version: 0.1.0
|
|
75
|
+
focus: npm_publish
|
|
76
|
+
status: green_flag
|
|
77
|
+
next_milestone: mcpaas@0.1.0 live on npm
|
|
78
|
+
blockers: null
|
|
79
|
+
scores:
|
|
80
|
+
faf_score: 85
|
|
81
|
+
slot_based_percentage: 86
|
|
82
|
+
total_slots: 21
|
|
83
|
+
tags:
|
|
84
|
+
auto_generated:
|
|
85
|
+
- mcpaas
|
|
86
|
+
- typescript
|
|
87
|
+
- bun
|
|
88
|
+
- websocket
|
|
89
|
+
smart_defaults:
|
|
90
|
+
- .faf
|
|
91
|
+
- ai-ready
|
|
92
|
+
- "2026"
|
|
93
|
+
- sdk
|
|
94
|
+
- open-source
|
|
95
|
+
user_defined: null
|
|
96
|
+
human_context:
|
|
97
|
+
who: Developers using Claude, Grok, Gemini, Cursor — anyone tired of re-explaining context
|
|
98
|
+
what: MCPaaS SDK — persistent AI context via Radio Protocol (WebSocket broadcast)
|
|
99
|
+
why: Your AI forgets you every session. MCPaaS remembers. Broadcast once, every AI receives.
|
|
100
|
+
where: npm (mcpaas) + GitHub (Wolfe-Jam/faf-radio-bun) + mcpaas.live (hosted service)
|
|
101
|
+
when: v0.1.0 ready to publish — 19/19 tests passing, 6.6KB tarball
|
|
102
|
+
how: npm install mcpaas → connect to mcpaas.live → tune to frequency → context persists
|
|
103
|
+
additional_context: Part of MCPaaS ecosystem. SDK connects to mcpaas.live (Cloudflare Workers, 300+ edge locations). IANA-registered format (application/vnd.faf+yaml).
|
|
104
|
+
context_score: 100
|
|
105
|
+
total_prd_score: 100
|
|
106
|
+
success_rate: 100%
|
|
107
|
+
faf_dna:
|
|
108
|
+
birth_dna: 43
|
|
109
|
+
birth_certificate: FAF-2026-FAFRADIO-7PMX
|
|
110
|
+
birth_date: 2026-02-13T03:38:41.451Z
|
|
111
|
+
current_score: 85
|
|
112
|
+
stack_signature: typescript-bun-websocket
|
|
113
|
+
detected_frameworks:
|
|
114
|
+
- Bun
|
|
115
|
+
- TypeScript
|
|
116
|
+
- WebSocket
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @faf/radio-bun
|
|
3
|
+
*
|
|
4
|
+
* Radio Protocol client for Bun - AI Context Broadcasting
|
|
5
|
+
* Built for Bun, Claude's Runtime
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { Radio } from '@faf/radio-bun';
|
|
10
|
+
*
|
|
11
|
+
* const radio = new Radio('wss://mcpaas.live/beacon/radio');
|
|
12
|
+
* await radio.connect();
|
|
13
|
+
* await radio.tune('91.5');
|
|
14
|
+
* radio.onBroadcast((freq, msg) => console.log(freq, msg));
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export { Radio } from './radio';
|
|
19
|
+
export type * from './types';
|
package/src/radio.ts
ADDED
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Radio Protocol Client for Bun
|
|
3
|
+
* Built for Bun - Claude's Runtime
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```typescript
|
|
7
|
+
* const radio = new Radio('wss://mcpaas.live/beacon/radio');
|
|
8
|
+
* await radio.connect();
|
|
9
|
+
* await radio.tune('91.5');
|
|
10
|
+
* radio.onBroadcast((freq, msg) => console.log(freq, msg));
|
|
11
|
+
* await radio.broadcast('91.5', { hello: 'world' });
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type {
|
|
16
|
+
Frequency,
|
|
17
|
+
RadioOptions,
|
|
18
|
+
RadioEventHandlers,
|
|
19
|
+
ConnectionState,
|
|
20
|
+
ServerMessage,
|
|
21
|
+
ClientMessage,
|
|
22
|
+
BroadcastReceivedMessage,
|
|
23
|
+
} from './types';
|
|
24
|
+
|
|
25
|
+
export class Radio {
|
|
26
|
+
private url: string;
|
|
27
|
+
private ws: WebSocket | null = null;
|
|
28
|
+
private state: ConnectionState = 'DISCONNECTED';
|
|
29
|
+
private options: Required<RadioOptions>;
|
|
30
|
+
private handlers: RadioEventHandlers = {};
|
|
31
|
+
private reconnectAttempts = 0;
|
|
32
|
+
private heartbeatTimer: Timer | null = null;
|
|
33
|
+
private tunedFrequencies = new Set<Frequency>();
|
|
34
|
+
|
|
35
|
+
constructor(url: string, options: RadioOptions = {}) {
|
|
36
|
+
this.url = url;
|
|
37
|
+
this.options = {
|
|
38
|
+
reconnect: options.reconnect ?? true,
|
|
39
|
+
maxReconnectAttempts: options.maxReconnectAttempts ?? 5,
|
|
40
|
+
reconnectDelay: options.reconnectDelay ?? 1000,
|
|
41
|
+
heartbeatInterval: options.heartbeatInterval ?? 30000,
|
|
42
|
+
debug: options.debug ?? false,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Connect to Radio Protocol server
|
|
48
|
+
*/
|
|
49
|
+
async connect(): Promise<void> {
|
|
50
|
+
if (this.state === 'CONNECTED' || this.state === 'CONNECTING') {
|
|
51
|
+
this.log('Already connected or connecting');
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return new Promise((resolve, reject) => {
|
|
56
|
+
this.state = 'CONNECTING';
|
|
57
|
+
this.log(`Connecting to ${this.url}...`);
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
this.ws = new WebSocket(this.url);
|
|
61
|
+
|
|
62
|
+
this.ws.onopen = () => {
|
|
63
|
+
this.state = 'CONNECTED';
|
|
64
|
+
this.reconnectAttempts = 0;
|
|
65
|
+
this.log('Connected to Radio Protocol');
|
|
66
|
+
this.startHeartbeat();
|
|
67
|
+
this.handlers.onConnect?.();
|
|
68
|
+
resolve();
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
this.ws.onmessage = (event) => {
|
|
72
|
+
this.handleMessage(event.data);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
this.ws.onerror = (event) => {
|
|
76
|
+
const error = new Error('WebSocket error');
|
|
77
|
+
this.log('WebSocket error:', event);
|
|
78
|
+
this.handlers.onError?.(error);
|
|
79
|
+
reject(error);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
this.ws.onclose = (event) => {
|
|
83
|
+
this.handleClose(event.code, event.reason);
|
|
84
|
+
};
|
|
85
|
+
} catch (error) {
|
|
86
|
+
this.state = 'DISCONNECTED';
|
|
87
|
+
reject(error);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Tune to a frequency (subscribe)
|
|
94
|
+
*/
|
|
95
|
+
async tune(frequency: Frequency): Promise<void> {
|
|
96
|
+
this.validateFrequency(frequency);
|
|
97
|
+
this.ensureConnected();
|
|
98
|
+
|
|
99
|
+
const message: ClientMessage = {
|
|
100
|
+
action: 'tune',
|
|
101
|
+
frequencies: [frequency],
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
this.send(message);
|
|
105
|
+
this.tunedFrequencies.add(frequency);
|
|
106
|
+
this.log(`Tuning to ${frequency} FM...`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Untune from a frequency (unsubscribe)
|
|
111
|
+
*/
|
|
112
|
+
async untune(frequency: Frequency): Promise<void> {
|
|
113
|
+
this.validateFrequency(frequency);
|
|
114
|
+
this.ensureConnected();
|
|
115
|
+
|
|
116
|
+
const message: ClientMessage = {
|
|
117
|
+
action: 'untune',
|
|
118
|
+
frequencies: [frequency],
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
this.send(message);
|
|
122
|
+
this.tunedFrequencies.delete(frequency);
|
|
123
|
+
this.log(`Untuning from ${frequency} FM...`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Disconnect from server
|
|
128
|
+
*/
|
|
129
|
+
disconnect(): void {
|
|
130
|
+
this.stopHeartbeat();
|
|
131
|
+
if (this.ws) {
|
|
132
|
+
this.state = 'CLOSED';
|
|
133
|
+
this.ws.close();
|
|
134
|
+
this.ws = null;
|
|
135
|
+
}
|
|
136
|
+
this.tunedFrequencies.clear();
|
|
137
|
+
this.log('Disconnected');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Event: On broadcast received
|
|
142
|
+
*/
|
|
143
|
+
onBroadcast(handler: (frequency: Frequency, message: any) => void): void {
|
|
144
|
+
this.handlers.onBroadcast = handler;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Event: On connection established
|
|
149
|
+
*/
|
|
150
|
+
onConnect(handler: () => void): void {
|
|
151
|
+
this.handlers.onConnect = handler;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Event: On disconnection
|
|
156
|
+
*/
|
|
157
|
+
onDisconnect(handler: (code?: number, reason?: string) => void): void {
|
|
158
|
+
this.handlers.onDisconnect = handler;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Event: On error
|
|
163
|
+
*/
|
|
164
|
+
onError(handler: (error: Error) => void): void {
|
|
165
|
+
this.handlers.onError = handler;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Event: On tuned confirmation
|
|
170
|
+
*/
|
|
171
|
+
onTuned(handler: (frequency: Frequency) => void): void {
|
|
172
|
+
this.handlers.onTuned = handler;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Event: On untuned confirmation
|
|
177
|
+
*/
|
|
178
|
+
onUntuned(handler: (frequency: Frequency) => void): void {
|
|
179
|
+
this.handlers.onUntuned = handler;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Get current connection state
|
|
184
|
+
*/
|
|
185
|
+
getState(): ConnectionState {
|
|
186
|
+
return this.state;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Get list of tuned frequencies
|
|
191
|
+
*/
|
|
192
|
+
getTunedFrequencies(): Set<Frequency> {
|
|
193
|
+
return new Set(this.tunedFrequencies);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Handle incoming server message
|
|
198
|
+
*/
|
|
199
|
+
private handleMessage(data: string | Blob | ArrayBuffer): void {
|
|
200
|
+
try {
|
|
201
|
+
const text = typeof data === 'string' ? data : new TextDecoder().decode(data as ArrayBuffer);
|
|
202
|
+
const message: ServerMessage = JSON.parse(text);
|
|
203
|
+
|
|
204
|
+
this.log('Received:', message.type);
|
|
205
|
+
|
|
206
|
+
switch (message.type) {
|
|
207
|
+
case 'connected':
|
|
208
|
+
this.log(`✅ ${message.message} (Client ID: ${message.clientId})`);
|
|
209
|
+
this.log('Available frequencies:', message.availableFrequencies);
|
|
210
|
+
break;
|
|
211
|
+
|
|
212
|
+
case 'tuned':
|
|
213
|
+
// Call handler for each tuned frequency
|
|
214
|
+
message.frequencies.forEach(freq => {
|
|
215
|
+
this.handlers.onTuned?.(freq);
|
|
216
|
+
this.log(`✅ Tuned to ${freq} FM`);
|
|
217
|
+
});
|
|
218
|
+
if (message.invalid.length > 0) {
|
|
219
|
+
this.log(`⚠️ Invalid frequencies: ${message.invalid.join(', ')}`);
|
|
220
|
+
}
|
|
221
|
+
break;
|
|
222
|
+
|
|
223
|
+
case 'untuned':
|
|
224
|
+
// Call handler for each untuned frequency
|
|
225
|
+
message.frequencies.forEach(freq => {
|
|
226
|
+
this.handlers.onUntuned?.(freq);
|
|
227
|
+
this.log(`✅ Untuned from ${freq} FM`);
|
|
228
|
+
});
|
|
229
|
+
break;
|
|
230
|
+
|
|
231
|
+
case 'broadcast':
|
|
232
|
+
this.handlers.onBroadcast?.(message.frequency, message.event);
|
|
233
|
+
this.log(`📻 Broadcast from ${message.frequency} FM`);
|
|
234
|
+
break;
|
|
235
|
+
|
|
236
|
+
case 'pong':
|
|
237
|
+
this.log('💓 Heartbeat');
|
|
238
|
+
break;
|
|
239
|
+
|
|
240
|
+
case 'error':
|
|
241
|
+
const error = new Error(message.message);
|
|
242
|
+
this.handlers.onError?.(error);
|
|
243
|
+
this.log(`❌ Error: ${message.message}`);
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
} catch (error) {
|
|
247
|
+
this.log('Failed to parse message:', error);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Handle connection close
|
|
253
|
+
*/
|
|
254
|
+
private handleClose(code?: number, reason?: string): void {
|
|
255
|
+
this.stopHeartbeat();
|
|
256
|
+
this.state = 'DISCONNECTED';
|
|
257
|
+
this.log(`Connection closed (${code}): ${reason}`);
|
|
258
|
+
this.handlers.onDisconnect?.(code, reason);
|
|
259
|
+
|
|
260
|
+
// Auto-reconnect if enabled
|
|
261
|
+
if (this.options.reconnect && this.reconnectAttempts < this.options.maxReconnectAttempts) {
|
|
262
|
+
this.reconnect();
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Reconnect with exponential backoff
|
|
268
|
+
*/
|
|
269
|
+
private async reconnect(): Promise<void> {
|
|
270
|
+
this.reconnectAttempts++;
|
|
271
|
+
this.state = 'RECONNECTING';
|
|
272
|
+
|
|
273
|
+
const delay = this.options.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
|
|
274
|
+
this.log(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts}/${this.options.maxReconnectAttempts})...`);
|
|
275
|
+
|
|
276
|
+
await Bun.sleep(delay);
|
|
277
|
+
|
|
278
|
+
try {
|
|
279
|
+
await this.connect();
|
|
280
|
+
|
|
281
|
+
// Re-tune to all previously tuned frequencies
|
|
282
|
+
for (const freq of this.tunedFrequencies) {
|
|
283
|
+
await this.tune(freq);
|
|
284
|
+
}
|
|
285
|
+
} catch (error) {
|
|
286
|
+
this.log('Reconnection failed:', error);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Send message to server
|
|
292
|
+
*/
|
|
293
|
+
private send(message: ClientMessage): void {
|
|
294
|
+
if (!this.ws || this.state !== 'CONNECTED') {
|
|
295
|
+
throw new Error('Not connected to Radio Protocol server');
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
this.ws.send(JSON.stringify(message));
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Start heartbeat timer
|
|
303
|
+
*/
|
|
304
|
+
private startHeartbeat(): void {
|
|
305
|
+
this.stopHeartbeat();
|
|
306
|
+
|
|
307
|
+
this.heartbeatTimer = setInterval(() => {
|
|
308
|
+
if (this.state === 'CONNECTED') {
|
|
309
|
+
this.send({ action: 'ping' });
|
|
310
|
+
}
|
|
311
|
+
}, this.options.heartbeatInterval);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Stop heartbeat timer
|
|
316
|
+
*/
|
|
317
|
+
private stopHeartbeat(): void {
|
|
318
|
+
if (this.heartbeatTimer) {
|
|
319
|
+
clearInterval(this.heartbeatTimer);
|
|
320
|
+
this.heartbeatTimer = null;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Validate frequency format
|
|
326
|
+
*/
|
|
327
|
+
private validateFrequency(frequency: Frequency): void {
|
|
328
|
+
const pattern = /^[0-9]{1,3}\.[0-9]$/;
|
|
329
|
+
if (!pattern.test(frequency)) {
|
|
330
|
+
throw new Error(`Invalid frequency format: ${frequency}. Expected XX.X (e.g., "91.5")`);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const num = parseFloat(frequency);
|
|
334
|
+
if (num < 40.0 || num > 108.0) {
|
|
335
|
+
throw new Error(`Frequency out of range: ${frequency}. Must be between 40.0 and 108.0`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Ensure client is connected
|
|
341
|
+
*/
|
|
342
|
+
private ensureConnected(): void {
|
|
343
|
+
if (this.state !== 'CONNECTED') {
|
|
344
|
+
throw new Error('Not connected. Call connect() first.');
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Debug logging
|
|
350
|
+
*/
|
|
351
|
+
private log(...args: any[]): void {
|
|
352
|
+
if (this.options.debug) {
|
|
353
|
+
console.log('[Radio]', ...args);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Radio Protocol Types for Bun
|
|
3
|
+
* Built for Bun - Claude's Runtime
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Radio Protocol message types (from protocol spec)
|
|
8
|
+
*/
|
|
9
|
+
export type MessageType =
|
|
10
|
+
| 'TUNE' // Subscribe to frequency
|
|
11
|
+
| 'UNTUNE' // Unsubscribe from frequency
|
|
12
|
+
| 'BROADCAST' // Send message to frequency
|
|
13
|
+
| 'PING' // Heartbeat
|
|
14
|
+
| 'TUNED' // Confirmation of tune
|
|
15
|
+
| 'UNTUNED' // Confirmation of untune
|
|
16
|
+
| 'PONG' // Heartbeat response
|
|
17
|
+
| 'ERROR'; // Error response
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Frequency format: XX.X (e.g., "91.5")
|
|
21
|
+
* Range: 40.0 - 108.0 FM
|
|
22
|
+
*/
|
|
23
|
+
export type Frequency = string;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Client → Server messages
|
|
27
|
+
* Note: Server expects "action" field (not "type") and "frequencies" array
|
|
28
|
+
*/
|
|
29
|
+
export interface TuneMessage {
|
|
30
|
+
action: 'tune';
|
|
31
|
+
frequencies: Frequency[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface UntuneMessage {
|
|
35
|
+
action: 'untune';
|
|
36
|
+
frequencies: Frequency[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface PingMessage {
|
|
40
|
+
action: 'ping';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type ClientMessage =
|
|
44
|
+
| TuneMessage
|
|
45
|
+
| UntuneMessage
|
|
46
|
+
| PingMessage;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Server → Client messages
|
|
50
|
+
* Note: Server sends lowercase "type" values
|
|
51
|
+
*/
|
|
52
|
+
export interface ConnectedMessage {
|
|
53
|
+
type: 'connected';
|
|
54
|
+
clientId: string;
|
|
55
|
+
message: string;
|
|
56
|
+
availableFrequencies: Record<string, string>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface TunedMessage {
|
|
60
|
+
type: 'tuned';
|
|
61
|
+
frequencies: Frequency[];
|
|
62
|
+
invalid: Frequency[];
|
|
63
|
+
souls: Array<{ frequency: Frequency; soul: string }>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface UntunedMessage {
|
|
67
|
+
type: 'untuned';
|
|
68
|
+
frequencies: Frequency[];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface BroadcastReceivedMessage {
|
|
72
|
+
type: 'broadcast';
|
|
73
|
+
frequency: Frequency;
|
|
74
|
+
event: any;
|
|
75
|
+
timestamp?: number;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface PongMessage {
|
|
79
|
+
type: 'pong';
|
|
80
|
+
timestamp: number;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface ErrorMessage {
|
|
84
|
+
type: 'error';
|
|
85
|
+
message: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export type ServerMessage =
|
|
89
|
+
| ConnectedMessage
|
|
90
|
+
| TunedMessage
|
|
91
|
+
| UntunedMessage
|
|
92
|
+
| BroadcastReceivedMessage
|
|
93
|
+
| PongMessage
|
|
94
|
+
| ErrorMessage;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Radio client options
|
|
98
|
+
*/
|
|
99
|
+
export interface RadioOptions {
|
|
100
|
+
/** Enable automatic reconnection (default: true) */
|
|
101
|
+
reconnect?: boolean;
|
|
102
|
+
|
|
103
|
+
/** Max reconnection attempts (default: 5) */
|
|
104
|
+
maxReconnectAttempts?: number;
|
|
105
|
+
|
|
106
|
+
/** Reconnection delay in ms (default: 1000) */
|
|
107
|
+
reconnectDelay?: number;
|
|
108
|
+
|
|
109
|
+
/** Heartbeat interval in ms (default: 30000) */
|
|
110
|
+
heartbeatInterval?: number;
|
|
111
|
+
|
|
112
|
+
/** Enable debug logging (default: false) */
|
|
113
|
+
debug?: boolean;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Radio connection state
|
|
118
|
+
*/
|
|
119
|
+
export type ConnectionState =
|
|
120
|
+
| 'DISCONNECTED'
|
|
121
|
+
| 'CONNECTING'
|
|
122
|
+
| 'CONNECTED'
|
|
123
|
+
| 'RECONNECTING'
|
|
124
|
+
| 'CLOSED';
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Event handlers
|
|
128
|
+
*/
|
|
129
|
+
export interface RadioEventHandlers {
|
|
130
|
+
onConnect?: () => void;
|
|
131
|
+
onDisconnect?: (code?: number, reason?: string) => void;
|
|
132
|
+
onError?: (error: Error) => void;
|
|
133
|
+
onBroadcast?: (frequency: Frequency, message: any) => void;
|
|
134
|
+
onTuned?: (frequency: Frequency) => void;
|
|
135
|
+
onUntuned?: (frequency: Frequency) => void;
|
|
136
|
+
}
|