aibroker 0.2.6 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +164 -4
- package/dist/adapters/iterm/core.d.ts +2 -0
- package/dist/adapters/iterm/core.d.ts.map +1 -1
- package/dist/adapters/iterm/core.js +13 -5
- package/dist/adapters/iterm/core.js.map +1 -1
- package/dist/adapters/kokoro/media.d.ts +2 -1
- package/dist/adapters/kokoro/media.d.ts.map +1 -1
- package/dist/adapters/kokoro/media.js +51 -3
- package/dist/adapters/kokoro/media.js.map +1 -1
- package/dist/adapters/pailot/gateway.d.ts +49 -0
- package/dist/adapters/pailot/gateway.d.ts.map +1 -0
- package/dist/adapters/pailot/gateway.js +502 -0
- package/dist/adapters/pailot/gateway.js.map +1 -0
- package/dist/backend/api.d.ts +5 -1
- package/dist/backend/api.d.ts.map +1 -1
- package/dist/backend/api.js +74 -3
- package/dist/backend/api.js.map +1 -1
- package/dist/core/hybrid.d.ts +5 -0
- package/dist/core/hybrid.d.ts.map +1 -1
- package/dist/core/hybrid.js +25 -0
- package/dist/core/hybrid.js.map +1 -1
- package/dist/core/state.d.ts +3 -0
- package/dist/core/state.d.ts.map +1 -1
- package/dist/core/state.js +4 -0
- package/dist/core/state.js.map +1 -1
- package/dist/daemon/adapter-registry.d.ts +58 -0
- package/dist/daemon/adapter-registry.d.ts.map +1 -0
- package/dist/daemon/adapter-registry.js +179 -0
- package/dist/daemon/adapter-registry.js.map +1 -0
- package/dist/daemon/cli.d.ts +13 -0
- package/dist/daemon/cli.d.ts.map +1 -0
- package/dist/daemon/cli.js +58 -0
- package/dist/daemon/cli.js.map +1 -0
- package/dist/daemon/core-handlers.d.ts +24 -0
- package/dist/daemon/core-handlers.d.ts.map +1 -0
- package/dist/daemon/core-handlers.js +146 -0
- package/dist/daemon/core-handlers.js.map +1 -0
- package/dist/daemon/create-adapter.d.ts +22 -0
- package/dist/daemon/create-adapter.d.ts.map +1 -0
- package/dist/daemon/create-adapter.js +152 -0
- package/dist/daemon/create-adapter.js.map +1 -0
- package/dist/daemon/index.d.ts +12 -0
- package/dist/daemon/index.d.ts.map +1 -0
- package/dist/daemon/index.js +83 -0
- package/dist/daemon/index.js.map +1 -0
- package/dist/daemon/pai-projects.d.ts +68 -0
- package/dist/daemon/pai-projects.d.ts.map +1 -0
- package/dist/daemon/pai-projects.js +174 -0
- package/dist/daemon/pai-projects.js.map +1 -0
- package/dist/index.d.ts +11 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -1
- package/dist/index.js.map +1 -1
- package/dist/ipc/client.d.ts +4 -1
- package/dist/ipc/client.d.ts.map +1 -1
- package/dist/ipc/client.js +10 -1
- package/dist/ipc/client.js.map +1 -1
- package/dist/types/adapter.d.ts +41 -0
- package/dist/types/adapter.d.ts.map +1 -0
- package/dist/types/adapter.js +2 -0
- package/dist/types/adapter.js.map +1 -0
- package/dist/types/backend.d.ts +29 -1
- package/dist/types/backend.d.ts.map +1 -1
- package/dist/types/broker.d.ts +45 -0
- package/dist/types/broker.d.ts.map +1 -0
- package/dist/types/broker.js +21 -0
- package/dist/types/broker.js.map +1 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -1
- package/package.json +9 -2
- package/templates/adapter/ONBOARDING_PROMPT.md +287 -0
- package/templates/adapter/README.md.tmpl +98 -0
- package/templates/adapter/package.json.tmpl +23 -0
- package/templates/adapter/src/watcher/cli.ts.tmpl +12 -0
- package/templates/adapter/src/watcher/commands.ts.tmpl +146 -0
- package/templates/adapter/src/watcher/connection.ts.tmpl +59 -0
- package/templates/adapter/src/watcher/index.ts.tmpl +177 -0
- package/templates/adapter/src/watcher/ipc-server.ts.tmpl +226 -0
- package/templates/adapter/src/watcher/send.ts.tmpl +62 -0
- package/templates/adapter/src/watcher/state.ts.tmpl +39 -0
- package/templates/adapter/tsconfig.json.tmpl +14 -0
package/package.json
CHANGED
|
@@ -1,18 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aibroker",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Platform-agnostic AI message broker — routes between user channels and AI backends",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
8
8
|
"files": [
|
|
9
9
|
"dist",
|
|
10
|
+
"templates",
|
|
10
11
|
"README.md",
|
|
11
12
|
"LICENSE"
|
|
12
13
|
],
|
|
14
|
+
"bin": {
|
|
15
|
+
"aibroker": "dist/daemon/cli.js"
|
|
16
|
+
},
|
|
13
17
|
"scripts": {
|
|
14
18
|
"build": "tsc",
|
|
15
19
|
"dev": "tsc --watch",
|
|
20
|
+
"start": "node dist/daemon/cli.js start",
|
|
16
21
|
"prepublishOnly": "npm run build"
|
|
17
22
|
},
|
|
18
23
|
"repository": {
|
|
@@ -37,10 +42,12 @@
|
|
|
37
42
|
},
|
|
38
43
|
"dependencies": {
|
|
39
44
|
"@anthropic-ai/claude-agent-sdk": "^0.1.0",
|
|
40
|
-
"kokoro-js": "^1.2.1"
|
|
45
|
+
"kokoro-js": "^1.2.1",
|
|
46
|
+
"ws": "^8.19.0"
|
|
41
47
|
},
|
|
42
48
|
"devDependencies": {
|
|
43
49
|
"@types/node": "^22.0.0",
|
|
50
|
+
"@types/ws": "^8.5.0",
|
|
44
51
|
"typescript": "^5.0.0"
|
|
45
52
|
}
|
|
46
53
|
}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
# AIBroker Adapter Onboarding — AI-Guided Creation
|
|
2
|
+
|
|
3
|
+
You are helping a developer create a new AIBroker messaging adapter. Your job is to ask up to 5 targeted questions, then generate a complete, working adapter from the scaffold templates.
|
|
4
|
+
|
|
5
|
+
Work through the following phases in order.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Phase 1: Interview (5 questions max)
|
|
10
|
+
|
|
11
|
+
Ask only what you need. If the answer is obvious from context (e.g. the user says "Discord"), skip the question. Combine questions where sensible. Never ask more than 5.
|
|
12
|
+
|
|
13
|
+
**The 5 questions:**
|
|
14
|
+
|
|
15
|
+
1. **Service name** — What service are you connecting? (e.g. Signal, Discord, Slack, IRC, Matrix)
|
|
16
|
+
2. **Package name** — What npm package should be used? (If you don't know, say "search for me" — you will search npm.)
|
|
17
|
+
3. **Auth method** — How does the SDK authenticate? (QR code scan, phone number + OTP, API key, OAuth, bot token, username/password)
|
|
18
|
+
4. **Message model** — Does the service have the concept of channels/rooms/groups, or is it always a 1:1 direct chat? Who is the default recipient?
|
|
19
|
+
5. **Voice support** — Does the service support sending voice/audio messages? If yes, what format does it accept? (Most accept OGG Opus or MP3.)
|
|
20
|
+
|
|
21
|
+
If the user already answered any of these in their initial message, skip those questions.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Phase 2: npm Research
|
|
26
|
+
|
|
27
|
+
Before generating code, search npm for the best library for this service:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# Search npm for available packages
|
|
31
|
+
npm search <service-name> --json | head -20
|
|
32
|
+
|
|
33
|
+
# Read the top candidate's README
|
|
34
|
+
npm show <package-name> readme
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Pick the most maintained package (high weekly downloads, recent publish date, active repo). If the user specified a package, verify it exists and read its docs anyway.
|
|
38
|
+
|
|
39
|
+
Read the package README for:
|
|
40
|
+
- Connection/auth pattern (how to establish a session)
|
|
41
|
+
- How to receive incoming messages (event name, callback signature)
|
|
42
|
+
- How to send a text message (method name, arguments)
|
|
43
|
+
- How to send a file/audio (if applicable)
|
|
44
|
+
- Session persistence (does it save credentials to disk?)
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Phase 3: Generate the Adapter
|
|
49
|
+
|
|
50
|
+
Generate all files listed below. Use the scaffold templates in `templates/adapter/` as the starting point and replace all `{{ADAPTER_NAME}}` and `{{DISPLAY_NAME}}` placeholders.
|
|
51
|
+
|
|
52
|
+
**Variable substitution:**
|
|
53
|
+
- `{{ADAPTER_NAME}}` — lowercase hyphenated package name (e.g. `signal-bridge`, `discord-adapter`)
|
|
54
|
+
- `{{DISPLAY_NAME}}` — human-readable service name (e.g. `Signal`, `Discord`)
|
|
55
|
+
|
|
56
|
+
### Files to generate
|
|
57
|
+
|
|
58
|
+
**`package.json`** — from `templates/adapter/package.json.tmpl`
|
|
59
|
+
- Fill in name, description
|
|
60
|
+
- Add the chosen npm library to `dependencies`
|
|
61
|
+
|
|
62
|
+
**`tsconfig.json`** — from `templates/adapter/tsconfig.json.tmpl`
|
|
63
|
+
- No changes needed
|
|
64
|
+
|
|
65
|
+
**`src/watcher/state.ts`** — from `templates/adapter/src/watcher/state.ts.tmpl`
|
|
66
|
+
- Replace placeholders only
|
|
67
|
+
- The socket path must be `/tmp/{{ADAPTER_NAME}}-watcher.sock`
|
|
68
|
+
|
|
69
|
+
**`src/watcher/connection.ts`** — from `templates/adapter/src/watcher/connection.ts.tmpl`
|
|
70
|
+
- THIS IS THE CORE IMPLEMENTATION FILE — replace the stub with real SDK code
|
|
71
|
+
- Implement `connectWatcher()` using the library you researched
|
|
72
|
+
- The function must:
|
|
73
|
+
1. Load credentials from `appDir` (use `getAppDir()` from aibroker if available, or `os.homedir()` + `.{{ADAPTER_NAME}}/auth/`)
|
|
74
|
+
2. Establish connection to the service
|
|
75
|
+
3. Subscribe to incoming messages, calling `onMessage(text, timestamp)` for each
|
|
76
|
+
4. Return `cleanup()` to disconnect gracefully
|
|
77
|
+
5. Return `triggerLogin()` to start a fresh auth flow (QR, OTP, etc.)
|
|
78
|
+
- Include all necessary imports from the chosen npm package
|
|
79
|
+
- Handle reconnection if the SDK supports it
|
|
80
|
+
- Log connection events with `log()` from aibroker
|
|
81
|
+
|
|
82
|
+
**`src/watcher/send.ts`** — from `templates/adapter/src/watcher/send.ts.tmpl`
|
|
83
|
+
- Replace stubs with real SDK delivery calls
|
|
84
|
+
- `sendText(text, recipient?)` — send plain text
|
|
85
|
+
- `sendVoice(audioPath, recipient?)` — send OGG Opus audio file (skip with a clear comment if service does not support voice)
|
|
86
|
+
- `sendFile(filePath, caption?, mimetype?, recipient?)` — send document attachment
|
|
87
|
+
|
|
88
|
+
**`src/watcher/commands.ts`** — from `templates/adapter/src/watcher/commands.ts.tmpl`
|
|
89
|
+
- Replace placeholders only
|
|
90
|
+
- Add any service-specific slash commands if the service warrants them (e.g. `/channel #general` to switch rooms)
|
|
91
|
+
|
|
92
|
+
**`src/watcher/ipc-server.ts`** — from `templates/adapter/src/watcher/ipc-server.ts.tmpl`
|
|
93
|
+
- Replace placeholders only
|
|
94
|
+
- The four required IPC handlers must be present: `deliver`, `health`, `connection_status`, `login`
|
|
95
|
+
|
|
96
|
+
**`src/watcher/index.ts`** — from `templates/adapter/src/watcher/index.ts.tmpl`
|
|
97
|
+
- Replace placeholders only
|
|
98
|
+
- The hub detection pattern must remain intact
|
|
99
|
+
|
|
100
|
+
**`src/watcher/cli.ts`** — from `templates/adapter/src/watcher/cli.ts.tmpl`
|
|
101
|
+
- Replace placeholders only
|
|
102
|
+
|
|
103
|
+
**`src/index.ts`** — MCP server entry point
|
|
104
|
+
- Model this on Whazaa's `src/index.ts` (the reference implementation)
|
|
105
|
+
- Expose MCP tools named `<service>_send`, `<service>_status`, `<service>_tts` (voice, if supported), `<service>_send_file`
|
|
106
|
+
- Wire each tool to the corresponding IPC handler via `WatcherClient`
|
|
107
|
+
- Socket path: `/tmp/{{ADAPTER_NAME}}-watcher.sock`
|
|
108
|
+
|
|
109
|
+
**`README.md`** — from `templates/adapter/README.md.tmpl`
|
|
110
|
+
- Fill in service name, auth instructions, and any service-specific setup steps
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## Phase 4: Wire into AIBroker Config
|
|
115
|
+
|
|
116
|
+
After generating the adapter files, register the adapter with the AIBroker hub:
|
|
117
|
+
|
|
118
|
+
1. **Check if `~/.aibroker/config.json` exists.** If yes, add an entry to the `adapters` array:
|
|
119
|
+
```json
|
|
120
|
+
{
|
|
121
|
+
"name": "{{ADAPTER_NAME}}",
|
|
122
|
+
"socketPath": "/tmp/{{ADAPTER_NAME}}-watcher.sock",
|
|
123
|
+
"autoStart": false
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
2. **Register as an MCP server** in `~/.claude.json` under `mcpServers`:
|
|
128
|
+
```json
|
|
129
|
+
"{{ADAPTER_NAME}}": {
|
|
130
|
+
"type": "stdio",
|
|
131
|
+
"command": "node",
|
|
132
|
+
"args": ["/path/to/{{ADAPTER_NAME}}/dist/index.js"],
|
|
133
|
+
"description": "{{DISPLAY_NAME}} MCP adapter"
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
3. **Add permission** in `~/.claude/settings.json` under `permissions.allow`:
|
|
138
|
+
```json
|
|
139
|
+
"mcp__{{ADAPTER_NAME}}"
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## Phase 5: Test the Connection
|
|
145
|
+
|
|
146
|
+
After generating all files:
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
# Build
|
|
150
|
+
cd /path/to/{{ADAPTER_NAME}}
|
|
151
|
+
npm install
|
|
152
|
+
npm run build
|
|
153
|
+
|
|
154
|
+
# Start the watcher (runs the upstream connection)
|
|
155
|
+
node dist/watcher/cli.js watch
|
|
156
|
+
|
|
157
|
+
# In a separate terminal, verify the IPC server is responding
|
|
158
|
+
node -e "
|
|
159
|
+
import { WatcherClient } from 'aibroker';
|
|
160
|
+
const c = new WatcherClient('/tmp/{{ADAPTER_NAME}}-watcher.sock');
|
|
161
|
+
c.call_raw('health', {}).then(r => console.log(JSON.stringify(r, null, 2)));
|
|
162
|
+
"
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Expected health response:
|
|
166
|
+
```json
|
|
167
|
+
{
|
|
168
|
+
"ok": true,
|
|
169
|
+
"result": {
|
|
170
|
+
"status": "ok",
|
|
171
|
+
"connectionStatus": "connected",
|
|
172
|
+
"stats": { "messagesReceived": 0, "messagesSent": 0, "errors": 0 },
|
|
173
|
+
"lastMessageAgo": null
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
If `status` is `"down"` or `connectionStatus` is not `"connected"`, check the watcher logs for auth/connection errors and run `triggerLogin()` via the `login` IPC handler.
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## Interface Contracts (Reference)
|
|
183
|
+
|
|
184
|
+
### MessengerAdapter interface
|
|
185
|
+
|
|
186
|
+
Every adapter fulfills this interface structurally via IPC handlers:
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
interface MessengerAdapter {
|
|
190
|
+
// Lifecycle
|
|
191
|
+
start(config: AdapterConfig): Promise<void>;
|
|
192
|
+
stop(): Promise<void>;
|
|
193
|
+
health(): Promise<AdapterHealth>; // IPC: "health"
|
|
194
|
+
|
|
195
|
+
// Auth
|
|
196
|
+
login(): Promise<string>; // IPC: "login"
|
|
197
|
+
connectionStatus(): Promise<AdapterConnectionStatus>; // IPC: "connection_status"
|
|
198
|
+
|
|
199
|
+
// Outbound
|
|
200
|
+
sendText(text, recipient?): Promise<void>; // IPC: "send"
|
|
201
|
+
sendVoice(audioPath, recipient?): Promise<void>; // IPC: "send_voice"
|
|
202
|
+
sendFile(filePath, caption?, mimetype?, recipient?): Promise<void>; // IPC: "send_file"
|
|
203
|
+
sendImage(imagePath, caption?, recipient?): Promise<void>; // IPC: "send_image"
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### BrokerMessage (hub envelope)
|
|
208
|
+
|
|
209
|
+
```typescript
|
|
210
|
+
interface BrokerMessage {
|
|
211
|
+
id: string; // UUID
|
|
212
|
+
timestamp: number; // epoch ms
|
|
213
|
+
source: string; // adapter name that sent it
|
|
214
|
+
target?: string; // target adapter name (omit for default routing)
|
|
215
|
+
type: "text" | "voice" | "file" | "command" | "status";
|
|
216
|
+
payload: {
|
|
217
|
+
text?: string;
|
|
218
|
+
filePath?: string;
|
|
219
|
+
audioPath?: string;
|
|
220
|
+
mimetype?: string;
|
|
221
|
+
caption?: string;
|
|
222
|
+
recipient?: string;
|
|
223
|
+
channel?: string;
|
|
224
|
+
metadata?: Record<string, unknown>;
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Required IPC handlers
|
|
230
|
+
|
|
231
|
+
| Handler | Called by | Must return |
|
|
232
|
+
|---------|-----------|-------------|
|
|
233
|
+
| `deliver` | Hub, when routing a BrokerMessage to this adapter | `{ ok: true, result: { delivered: true } }` or `{ ok: false, error: string }` |
|
|
234
|
+
| `health` | Hub health polling | `{ ok: true, result: AdapterHealth }` |
|
|
235
|
+
| `connection_status` | Hub, MCP tools | `{ ok: true, result: { status: AdapterConnectionStatus } }` |
|
|
236
|
+
| `login` | User, via MCP tool | `{ ok: true, result: { message: string } }` |
|
|
237
|
+
| `send` | MCP tool | `{ ok: true, result: { sent: true } }` |
|
|
238
|
+
| `send_voice` | MCP tool | `{ ok: true, result: { sent: true } }` |
|
|
239
|
+
| `send_file` | MCP tool | `{ ok: true, result: { sent: true } }` |
|
|
240
|
+
| `status` | MCP tool | Human-readable status object |
|
|
241
|
+
|
|
242
|
+
### Hub detection pattern
|
|
243
|
+
|
|
244
|
+
The watcher probes the AIBroker daemon socket at startup:
|
|
245
|
+
|
|
246
|
+
```typescript
|
|
247
|
+
async function detectHubMode(): Promise<boolean> {
|
|
248
|
+
const client = new WatcherClient(DAEMON_SOCKET_PATH);
|
|
249
|
+
try {
|
|
250
|
+
const result = await Promise.race([
|
|
251
|
+
client.call_raw("status", {}),
|
|
252
|
+
new Promise<null>((_, reject) => setTimeout(() => reject(new Error("timeout")), 2000)),
|
|
253
|
+
]);
|
|
254
|
+
return result !== null;
|
|
255
|
+
} catch {
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
If `detectHubMode()` returns `true`, the adapter:
|
|
262
|
+
- Forwards incoming messages to the hub via `route_message`
|
|
263
|
+
- Registers itself via `register_adapter` so the hub can push outbound messages back
|
|
264
|
+
|
|
265
|
+
If `false`, the adapter handles everything locally (embedded mode).
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
## Reference Implementation
|
|
270
|
+
|
|
271
|
+
**Whazaa** is the canonical reference adapter. When in doubt, model behaviour on Whazaa:
|
|
272
|
+
|
|
273
|
+
- Repo: `~/dev/ai/Whazaa/`
|
|
274
|
+
- Key file — connection: `src/watcher/baileys.ts` (WhatsApp-specific connection)
|
|
275
|
+
- Key file — send: `src/watcher/send.ts`
|
|
276
|
+
- Key file — IPC: `src/watcher/ipc-server.ts`
|
|
277
|
+
- Key file — MCP tools: `src/index.ts`
|
|
278
|
+
- Key pattern — Baileys saves auth state to `~/.whazaa/auth/` via `useMultiFileAuthState()`
|
|
279
|
+
|
|
280
|
+
The Whazaa pattern for `connectWatcher()` is:
|
|
281
|
+
1. Call `useMultiFileAuthState(authDir)` (or equivalent for your SDK)
|
|
282
|
+
2. Create the client instance with the auth state
|
|
283
|
+
3. Set up message event listener calling `onMessage(text, timestamp)`
|
|
284
|
+
4. Call `client.connect()` or equivalent
|
|
285
|
+
5. Return `cleanup` (calls `client.logout()` or `client.disconnect()`) and `triggerLogin` (generates new QR/pairing)
|
|
286
|
+
|
|
287
|
+
Follow this pattern exactly. The template scaffold maps cleanly onto it.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# {{ADAPTER_NAME}}
|
|
2
|
+
|
|
3
|
+
AIBroker adapter for {{DISPLAY_NAME}}.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This adapter bridges {{DISPLAY_NAME}} and the [AIBroker](https://github.com/mnott/AIBroker) hub, enabling bidirectional message routing between {{DISPLAY_NAME}} and AI backends (Claude Code, Anthropic API, etc.).
|
|
8
|
+
|
|
9
|
+
## Setup
|
|
10
|
+
|
|
11
|
+
### 1. Install dependencies
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### 2. Implement the connection
|
|
18
|
+
|
|
19
|
+
Open `src/watcher/connection.ts` and implement `connectWatcher()` with your {{DISPLAY_NAME}} SDK.
|
|
20
|
+
|
|
21
|
+
The function must:
|
|
22
|
+
- Connect to {{DISPLAY_NAME}} using your credentials
|
|
23
|
+
- Call `onMessage(text, timestamp)` for each incoming message
|
|
24
|
+
- Return `cleanup()` (disconnect) and `triggerLogin()` (fresh auth flow)
|
|
25
|
+
|
|
26
|
+
### 3. Implement outbound delivery
|
|
27
|
+
|
|
28
|
+
Open `src/watcher/send.ts` and implement `sendText()`, `sendVoice()`, and `sendFile()`.
|
|
29
|
+
|
|
30
|
+
### 4. Build
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm run build
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### 5. Run
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
# Standalone (embedded) mode — no hub required
|
|
40
|
+
npm run watch
|
|
41
|
+
|
|
42
|
+
# Or: with AIBroker hub running
|
|
43
|
+
aibroker start # in another terminal
|
|
44
|
+
npm run watch # adapter auto-detects and registers with hub
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Architecture
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
{{DISPLAY_NAME}} <── connectWatcher() ──> watcher/index.ts
|
|
51
|
+
│
|
|
52
|
+
createMessageHandler() startIpcServer()
|
|
53
|
+
│ │
|
|
54
|
+
commands.ts ipc-server.ts
|
|
55
|
+
│
|
|
56
|
+
AIBroker hub / embedded
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Configuration
|
|
60
|
+
|
|
61
|
+
Set these environment variables before running:
|
|
62
|
+
|
|
63
|
+
| Variable | Default | Description |
|
|
64
|
+
|----------|---------|-------------|
|
|
65
|
+
| `AIBROKER_MODEL` | `sonnet` | AI model for API sessions |
|
|
66
|
+
| `AIBROKER_CWD` | `~` | Working directory for AI sessions |
|
|
67
|
+
| `AIBROKER_MAX_TURNS` | `30` | Max turns per API session |
|
|
68
|
+
| `AIBROKER_MAX_BUDGET` | `1.0` | Max USD budget per API session |
|
|
69
|
+
| `AIBROKER_PERMISSION_MODE` | `acceptEdits` | Claude permission mode |
|
|
70
|
+
|
|
71
|
+
## Slash Commands
|
|
72
|
+
|
|
73
|
+
| Command | Description |
|
|
74
|
+
|---------|-------------|
|
|
75
|
+
| `/h`, `/help` | Show help |
|
|
76
|
+
| `/s`, `/sessions` | List all sessions |
|
|
77
|
+
| `/ss`, `/status` | Show active session status |
|
|
78
|
+
| `/n [name]` | Create a new API session |
|
|
79
|
+
| `/1`, `/2`, ... | Switch to session by index |
|
|
80
|
+
|
|
81
|
+
## IPC Methods
|
|
82
|
+
|
|
83
|
+
The adapter exposes these methods on its Unix socket (`/tmp/{{ADAPTER_NAME}}-watcher.sock`):
|
|
84
|
+
|
|
85
|
+
| Method | Description |
|
|
86
|
+
|--------|-------------|
|
|
87
|
+
| `deliver` | Hub pushes a BrokerMessage for outbound delivery |
|
|
88
|
+
| `health` | Returns adapter health (status, stats, lastMessageAgo) |
|
|
89
|
+
| `connection_status` | Returns upstream connection status string |
|
|
90
|
+
| `send` | Send a text message |
|
|
91
|
+
| `send_voice` | Send a voice note (OGG Opus path) |
|
|
92
|
+
| `send_file` | Send a file attachment |
|
|
93
|
+
| `login` | Trigger a fresh login / QR pairing flow |
|
|
94
|
+
| `status` | Human-readable adapter status summary |
|
|
95
|
+
|
|
96
|
+
## License
|
|
97
|
+
|
|
98
|
+
MIT
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{ADAPTER_NAME}}",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "AIBroker adapter for {{DISPLAY_NAME}}",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"{{ADAPTER_NAME}}": "dist/watcher/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"dev": "tsc --watch",
|
|
13
|
+
"watch": "node dist/watcher/cli.js watch",
|
|
14
|
+
"prepublishOnly": "npm run build"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"aibroker": "^0.5.0"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@types/node": "^22.0.0",
|
|
21
|
+
"typescript": "^5.0.0"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { watch } from "./index.js";
|
|
3
|
+
|
|
4
|
+
const command = process.argv[2];
|
|
5
|
+
if (command === "watch" || !command) {
|
|
6
|
+
watch().catch((err) => {
|
|
7
|
+
console.error("Fatal:", err);
|
|
8
|
+
process.exit(1);
|
|
9
|
+
});
|
|
10
|
+
} else {
|
|
11
|
+
console.log(`Usage: {{ADAPTER_NAME}} watch`);
|
|
12
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* commands.ts — Slash command handler for {{DISPLAY_NAME}}
|
|
3
|
+
*
|
|
4
|
+
* Parses incoming message text. If the text starts with "/" it is treated as
|
|
5
|
+
* a slash command; otherwise it is delivered as a plain message to the active
|
|
6
|
+
* AI session via the HybridSessionManager.
|
|
7
|
+
*
|
|
8
|
+
* Extend this file with adapter-specific slash commands as needed.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
hybridManager,
|
|
13
|
+
deliverViaApi,
|
|
14
|
+
log,
|
|
15
|
+
} from "aibroker";
|
|
16
|
+
import { sendText } from "./send.js";
|
|
17
|
+
|
|
18
|
+
// ── Command handler factory ───────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Build and return the message handler closure used by the watcher.
|
|
22
|
+
*
|
|
23
|
+
* The closure captures mutable state via getter/setter callbacks so the
|
|
24
|
+
* watcher's `watch()` function can keep its own local variables authoritative
|
|
25
|
+
* while sharing them with the handler.
|
|
26
|
+
*/
|
|
27
|
+
export function createMessageHandler(): (text: string, timestamp: number) => void {
|
|
28
|
+
return function handleMessage(text: string, _timestamp: number): void {
|
|
29
|
+
const trimmed = text.trim();
|
|
30
|
+
|
|
31
|
+
if (!trimmed.startsWith("/")) {
|
|
32
|
+
// Plain text — deliver to the active AI session
|
|
33
|
+
void deliverMessage(trimmed);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Slash command dispatch
|
|
38
|
+
void handleSlashCommand(trimmed);
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ── Slash command dispatcher ─────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
async function handleSlashCommand(cmd: string): Promise<void> {
|
|
45
|
+
const [base, ...args] = cmd.split(/\s+/);
|
|
46
|
+
|
|
47
|
+
switch (base) {
|
|
48
|
+
case "/h":
|
|
49
|
+
case "/help":
|
|
50
|
+
await sendText(buildHelp());
|
|
51
|
+
return;
|
|
52
|
+
|
|
53
|
+
case "/s":
|
|
54
|
+
case "/sessions": {
|
|
55
|
+
const mgr = hybridManager;
|
|
56
|
+
if (!mgr) { await sendText("No session manager active."); return; }
|
|
57
|
+
await sendText(mgr.formatSessionList());
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
case "/ss":
|
|
62
|
+
case "/status": {
|
|
63
|
+
const mgr = hybridManager;
|
|
64
|
+
if (!mgr) { await sendText("No session manager active."); return; }
|
|
65
|
+
const status = mgr.formatActiveStatus();
|
|
66
|
+
await sendText(status ?? "Active session is a visual terminal — no text status available.");
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
case "/n":
|
|
71
|
+
case "/new": {
|
|
72
|
+
const mgr = hybridManager;
|
|
73
|
+
if (!mgr) { await sendText("No session manager active."); return; }
|
|
74
|
+
const cwd = process.env.AIBROKER_CWD ?? process.env.HOME ?? "/";
|
|
75
|
+
const name = args[0] ?? `Session ${Date.now()}`;
|
|
76
|
+
const session = mgr.createApiSession(name, cwd);
|
|
77
|
+
await sendText(`Created new API session: "${session.name}" (${session.id})`);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
default:
|
|
82
|
+
// Numeric shortcut: /1, /2, /3 — switch to session by index
|
|
83
|
+
if (/^\/\d+$/.test(base)) {
|
|
84
|
+
const idx = parseInt(base.slice(1), 10);
|
|
85
|
+
const mgr = hybridManager;
|
|
86
|
+
if (!mgr) { await sendText("No session manager active."); return; }
|
|
87
|
+
const session = mgr.switchToIndex(idx);
|
|
88
|
+
if (session) {
|
|
89
|
+
await sendText(`Switched to: "${session.name}" (${session.kind})`);
|
|
90
|
+
} else {
|
|
91
|
+
await sendText(`No session at index ${idx}.`);
|
|
92
|
+
}
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
log(`[{{ADAPTER_NAME}}] Unknown command: ${cmd}`);
|
|
97
|
+
await sendText(`Unknown command: ${cmd}\nType /h for help.`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ── Message delivery ─────────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
async function deliverMessage(text: string): Promise<void> {
|
|
104
|
+
const mgr = hybridManager;
|
|
105
|
+
if (!mgr) {
|
|
106
|
+
log("[{{ADAPTER_NAME}}] No hybrid manager — dropping message");
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const active = mgr.activeSession;
|
|
111
|
+
if (!active) {
|
|
112
|
+
await sendText("No active session. Use /n to create one.");
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (active.kind === "api") {
|
|
117
|
+
// Deliver via APIBackend — response comes back asynchronously via deliverViaApi
|
|
118
|
+
const response = await deliverViaApi(text, async (responseText: string) => {
|
|
119
|
+
await sendText(responseText);
|
|
120
|
+
});
|
|
121
|
+
if (response) {
|
|
122
|
+
// Synchronous response (some backends return inline)
|
|
123
|
+
await sendText(response);
|
|
124
|
+
}
|
|
125
|
+
} else {
|
|
126
|
+
// Visual (iTerm2) session — TODO: type into terminal if iTerm2 adapter is available
|
|
127
|
+
log(`[{{ADAPTER_NAME}}] Visual session delivery not yet implemented`);
|
|
128
|
+
await sendText("Visual session delivery not yet implemented for this adapter.");
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ── Help text ────────────────────────────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
function buildHelp(): string {
|
|
135
|
+
return [
|
|
136
|
+
"*{{DISPLAY_NAME}} Adapter — Commands*",
|
|
137
|
+
"",
|
|
138
|
+
"/h, /help — This help message",
|
|
139
|
+
"/s, /sessions — List all sessions",
|
|
140
|
+
"/ss, /status — Show active session status",
|
|
141
|
+
"/n [name] — Create a new API session",
|
|
142
|
+
"/1, /2, ... — Switch to session by index",
|
|
143
|
+
"",
|
|
144
|
+
"Any other text is delivered to the active AI session.",
|
|
145
|
+
].join("\n");
|
|
146
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* connection.ts — Connect to {{DISPLAY_NAME}}
|
|
3
|
+
*
|
|
4
|
+
* TODO: Implement the upstream service connection here.
|
|
5
|
+
* This is the adapter-specific part — use the SDK/library for your service.
|
|
6
|
+
*
|
|
7
|
+
* Replace this stub with a real connection using the appropriate SDK, e.g.:
|
|
8
|
+
* - @whiskeysockets/baileys for WhatsApp
|
|
9
|
+
* - gramjs / telegram for Telegram
|
|
10
|
+
* - discord.js for Discord
|
|
11
|
+
* - signal-cli for Signal
|
|
12
|
+
*
|
|
13
|
+
* The onMessage callback receives every incoming message text so the watcher
|
|
14
|
+
* can route it through the command handler / hub forwarding.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export interface ConnectionResult {
|
|
18
|
+
/** Call to cleanly disconnect from the upstream service. */
|
|
19
|
+
cleanup: () => void;
|
|
20
|
+
/**
|
|
21
|
+
* Trigger a new login flow (QR code, pairing, etc.) and return a
|
|
22
|
+
* human-readable status string to send back to the user.
|
|
23
|
+
*/
|
|
24
|
+
triggerLogin: () => Promise<string>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Establish the upstream connection.
|
|
29
|
+
*
|
|
30
|
+
* @param onMessage - Called for each incoming message the adapter should process.
|
|
31
|
+
* Receives the message text and the message timestamp (epoch ms).
|
|
32
|
+
*/
|
|
33
|
+
export async function connectWatcher(
|
|
34
|
+
onMessage: (text: string, timestamp: number) => void,
|
|
35
|
+
): Promise<ConnectionResult> {
|
|
36
|
+
// TODO: Replace this stub with your actual connection logic.
|
|
37
|
+
//
|
|
38
|
+
// Typical pattern:
|
|
39
|
+
// 1. Load credentials from appDir (set by setAppDir() in index.ts)
|
|
40
|
+
// 2. Connect to the upstream service
|
|
41
|
+
// 3. Subscribe to incoming messages, calling onMessage() for each
|
|
42
|
+
// 4. Return cleanup() and triggerLogin() implementations
|
|
43
|
+
//
|
|
44
|
+
console.log("[{{ADAPTER_NAME}}] Connection stub — implement connectWatcher()");
|
|
45
|
+
|
|
46
|
+
// Suppress unused-parameter warning in stub
|
|
47
|
+
void onMessage;
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
cleanup: () => {
|
|
51
|
+
// TODO: close WebSocket, stop polling, revoke auth, etc.
|
|
52
|
+
console.log("[{{ADAPTER_NAME}}] cleanup() called");
|
|
53
|
+
},
|
|
54
|
+
triggerLogin: async () => {
|
|
55
|
+
// TODO: start QR code / pairing flow, return status string
|
|
56
|
+
return "Login not yet implemented for {{DISPLAY_NAME}}";
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|