@maincode-ai/channel-base 0.19.0 → 0.19.2
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 +290 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,5 +1,295 @@
|
|
|
1
1
|
# @maincode-ai/channel-base
|
|
2
2
|
|
|
3
|
+
Base infrastructure for building Matilda Code channel adapters. Provides the abstract base class, access control, session routing, and the ACP bridge that communicates with the agent.
|
|
4
|
+
|
|
5
|
+
If you're building a channel plugin, this is your only dependency.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
3
9
|
```bash
|
|
4
10
|
npm install @maincode-ai/channel-base
|
|
5
11
|
```
|
|
12
|
+
|
|
13
|
+
## Quick start
|
|
14
|
+
|
|
15
|
+
Subclass `ChannelBase` and implement three methods:
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import { ChannelBase } from '@maincode-ai/channel-base';
|
|
19
|
+
import type {
|
|
20
|
+
ChannelConfig,
|
|
21
|
+
Envelope,
|
|
22
|
+
AcpBridge,
|
|
23
|
+
} from '@maincode-ai/channel-base';
|
|
24
|
+
|
|
25
|
+
class MyChannel extends ChannelBase {
|
|
26
|
+
async connect(): Promise<void> {
|
|
27
|
+
// Connect to platform API, register message handlers.
|
|
28
|
+
// When a message arrives, build an Envelope and call:
|
|
29
|
+
// this.handleInbound(envelope)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async sendMessage(chatId: string, text: string): Promise<void> {
|
|
33
|
+
// Deliver the agent's response to the platform.
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
disconnect(): void {
|
|
37
|
+
// Clean up connections on shutdown.
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Export a `ChannelPlugin` object so the extension loader can discover it:
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
import type { ChannelPlugin } from '@maincode-ai/channel-base';
|
|
46
|
+
|
|
47
|
+
export const plugin: ChannelPlugin = {
|
|
48
|
+
channelType: 'my-platform',
|
|
49
|
+
displayName: 'My Platform',
|
|
50
|
+
requiredConfigFields: ['apiKey'],
|
|
51
|
+
createChannel: (name, config, bridge, options) =>
|
|
52
|
+
new MyChannel(name, config, bridge, options),
|
|
53
|
+
};
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
For a complete working example, see [`@matilda-code/channel-plugin-example`](../plugin-example/).
|
|
57
|
+
|
|
58
|
+
## Architecture
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
Inbound: Platform message
|
|
62
|
+
→ Envelope (with attachments)
|
|
63
|
+
→ GroupGate (group policy + mention gating)
|
|
64
|
+
→ SenderGate (allowlist / pairing / open)
|
|
65
|
+
→ Slash commands (/clear, /help, /status)
|
|
66
|
+
→ SessionRouter (resolve or create ACP session)
|
|
67
|
+
→ Resolve attachments (images → bridge, files → prompt text)
|
|
68
|
+
→ AcpBridge.prompt() → agent
|
|
69
|
+
|
|
70
|
+
Outbound: Agent response
|
|
71
|
+
→ BlockStreamer (if enabled: split into blocks at paragraph boundaries)
|
|
72
|
+
→ sendMessage() → platform
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Everything between `handleInbound()` and `sendMessage()` is handled by the base class — your adapter only deals with platform I/O.
|
|
76
|
+
|
|
77
|
+
## Exports
|
|
78
|
+
|
|
79
|
+
### Classes
|
|
80
|
+
|
|
81
|
+
| Class | Purpose |
|
|
82
|
+
| --------------- | ------------------------------------------------------------------- |
|
|
83
|
+
| `ChannelBase` | Abstract base class — extend this to build a channel adapter |
|
|
84
|
+
| `AcpBridge` | Spawns and communicates with the `matilda-code --acp` agent process |
|
|
85
|
+
| `BlockStreamer` | Progressive multi-message delivery for block streaming |
|
|
86
|
+
| `SessionRouter` | Maps senders to ACP sessions with configurable scoping |
|
|
87
|
+
| `SenderGate` | DM access control (allowlist / pairing / open) |
|
|
88
|
+
| `GroupGate` | Group chat policy and @mention gating |
|
|
89
|
+
| `PairingStore` | Pairing code generation, approval, and allowlist persistence |
|
|
90
|
+
|
|
91
|
+
### Types
|
|
92
|
+
|
|
93
|
+
| Type | Description |
|
|
94
|
+
| --------------- | ---------------------------------------------- |
|
|
95
|
+
| `Attachment` | Structured file/image/audio/video attachment |
|
|
96
|
+
| `ChannelConfig` | Channel configuration from `settings.json` |
|
|
97
|
+
| `ChannelPlugin` | Plugin factory interface (what you export) |
|
|
98
|
+
| `Envelope` | Normalized inbound message format |
|
|
99
|
+
| `SenderPolicy` | `'allowlist' \| 'pairing' \| 'open'` |
|
|
100
|
+
| `GroupPolicy` | `'disabled' \| 'allowlist' \| 'open'` |
|
|
101
|
+
| `SessionScope` | `'user' \| 'thread' \| 'single'` |
|
|
102
|
+
| `GroupConfig` | Per-group settings (e.g. `requireMention`) |
|
|
103
|
+
| `SessionTarget` | Maps a session back to its channel/sender/chat |
|
|
104
|
+
|
|
105
|
+
## API reference
|
|
106
|
+
|
|
107
|
+
### ChannelBase
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
constructor(name: string, config: ChannelConfig, bridge: AcpBridge, options?: ChannelBaseOptions)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
**Abstract methods** (you must implement):
|
|
114
|
+
|
|
115
|
+
| Method | Signature |
|
|
116
|
+
| --------------- | ---------------------------------------------------------------------------- |
|
|
117
|
+
| `connect()` | `() => Promise<void>` — Connect to the platform and start receiving messages |
|
|
118
|
+
| `sendMessage()` | `(chatId: string, text: string) => Promise<void>` — Deliver agent response |
|
|
119
|
+
| `disconnect()` | `() => void` — Clean up on shutdown |
|
|
120
|
+
|
|
121
|
+
**Provided methods:**
|
|
122
|
+
|
|
123
|
+
| Method | Description |
|
|
124
|
+
| ------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
|
|
125
|
+
| `handleInbound(envelope)` | Route an inbound message through the full pipeline (gate checks, commands, session, prompt). Call this from your message handler. |
|
|
126
|
+
| `setBridge(bridge)` | Replace the ACP bridge after crash recovery |
|
|
127
|
+
| `registerCommand(name, handler)` | Register a custom slash command (e.g. `/mycommand`) |
|
|
128
|
+
| `onToolCall(chatId, event)` | Hook called on agent tool invocations — override to show indicators |
|
|
129
|
+
| `onResponseChunk(chatId, chunk, sessionId)` | Hook called per streaming text chunk — override for progressive display (default: no-op) |
|
|
130
|
+
| `onResponseComplete(chatId, fullText, sessionId)` | Hook called when full response is ready — override to customize delivery (default: `sendMessage()`) |
|
|
131
|
+
|
|
132
|
+
**Block streaming:** When `blockStreaming: "on"` is set in the channel config, the base class automatically splits the agent's streaming response into multiple messages at paragraph boundaries. See [Block Streaming](#block-streaming) below.
|
|
133
|
+
|
|
134
|
+
**Built-in slash commands:** `/clear` (`/reset`, `/new`), `/help`, `/status`
|
|
135
|
+
|
|
136
|
+
### AcpBridge
|
|
137
|
+
|
|
138
|
+
Manages the `matilda-code --acp` child process and ACP sessions.
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
constructor(options: { cliEntryPath: string; cwd: string; model?: string })
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
| Method | Description |
|
|
145
|
+
| ----------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
|
|
146
|
+
| `start()` | Spawn the agent process |
|
|
147
|
+
| `stop()` | Kill the agent process |
|
|
148
|
+
| `newSession(cwd)` | Create a new ACP session, returns `sessionId` |
|
|
149
|
+
| `loadSession(sessionId, cwd)` | Restore an existing session |
|
|
150
|
+
| `prompt(sessionId, text, options?)` | Send a message to the agent, returns the full response text. Supports optional `imageBase64` and `imageMimeType`. |
|
|
151
|
+
| `isConnected` | Whether the agent process is alive |
|
|
152
|
+
|
|
153
|
+
**Events** (EventEmitter):
|
|
154
|
+
|
|
155
|
+
| Event | Payload | Description |
|
|
156
|
+
| -------------- | ------------------------ | ------------------------ |
|
|
157
|
+
| `textChunk` | `(sessionId, chunk)` | Streaming response chunk |
|
|
158
|
+
| `toolCall` | `(event: ToolCallEvent)` | Agent invoked a tool |
|
|
159
|
+
| `disconnected` | `(code, signal)` | Agent process exited |
|
|
160
|
+
|
|
161
|
+
### SessionRouter
|
|
162
|
+
|
|
163
|
+
Maps senders to ACP sessions based on the configured scope.
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
constructor(bridge: AcpBridge, defaultCwd: string, scope?: SessionScope, persistPath?: string)
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
**Routing keys by scope:**
|
|
170
|
+
|
|
171
|
+
| Scope | Key format | Effect |
|
|
172
|
+
| ---------------- | ------------------------- | ----------------------------------------- |
|
|
173
|
+
| `user` (default) | `channel:senderId:chatId` | Each user gets their own session per chat |
|
|
174
|
+
| `thread` | `channel:threadId` | One session per thread |
|
|
175
|
+
| `single` | `channel:__single__` | One shared session for the entire channel |
|
|
176
|
+
|
|
177
|
+
| Method | Description |
|
|
178
|
+
| --------------------------------------------------------- | ----------------------------------------------------------- |
|
|
179
|
+
| `resolve(channelName, senderId, chatId, threadId?, cwd?)` | Get or create a session for the given sender |
|
|
180
|
+
| `removeSession(channelName, senderId, chatId?)` | Remove session(s) — used by `/clear` |
|
|
181
|
+
| `restoreSessions()` | Reload sessions from disk after bridge restart |
|
|
182
|
+
| `clearAll()` | Clear all sessions and delete persist file (clean shutdown) |
|
|
183
|
+
|
|
184
|
+
### SenderGate
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
constructor(policy: SenderPolicy, allowedUsers?: string[], pairingStore?: PairingStore)
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
| Method | Description |
|
|
191
|
+
| ------------------------------ | ------------------------------------------------------------ |
|
|
192
|
+
| `check(senderId, senderName?)` | Returns `{ allowed: boolean, pairingCode?: string \| null }` |
|
|
193
|
+
|
|
194
|
+
**Policy behavior:**
|
|
195
|
+
|
|
196
|
+
| Policy | Behavior |
|
|
197
|
+
| ----------- | --------------------------------------------------------------------------------------------------------- |
|
|
198
|
+
| `open` | Everyone allowed |
|
|
199
|
+
| `allowlist` | Only `allowedUsers` allowed |
|
|
200
|
+
| `pairing` | Check allowlist, then approved pairings, then generate a pairing code (8-char, 1hr expiry, max 3 pending) |
|
|
201
|
+
|
|
202
|
+
### GroupGate
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
constructor(policy?: GroupPolicy, groups?: Record<string, GroupConfig>)
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
| Method | Description |
|
|
209
|
+
| ----------------- | ---------------------------------------------------------------------------------------------- |
|
|
210
|
+
| `check(envelope)` | Returns `{ allowed: boolean, reason?: 'disabled' \| 'not_allowlisted' \| 'mention_required' }` |
|
|
211
|
+
|
|
212
|
+
**Policy behavior:**
|
|
213
|
+
|
|
214
|
+
| Policy | Behavior |
|
|
215
|
+
| ----------- | ---------------------------------------- |
|
|
216
|
+
| `disabled` | All group messages rejected |
|
|
217
|
+
| `allowlist` | Only groups listed in config are allowed |
|
|
218
|
+
| `open` | All groups allowed |
|
|
219
|
+
|
|
220
|
+
When `requireMention` is `true` (default), group messages are only processed if the bot is @mentioned or the message is a reply to the bot.
|
|
221
|
+
|
|
222
|
+
### PairingStore
|
|
223
|
+
|
|
224
|
+
```typescript
|
|
225
|
+
constructor(channelName: string)
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Persists pairing state to `~/.matilda/channels/{channelName}-pairing.json` and `{channelName}-allowlist.json`.
|
|
229
|
+
|
|
230
|
+
| Method | Description |
|
|
231
|
+
| ------------------------------------- | --------------------------------------------------------------------------------------------------------- |
|
|
232
|
+
| `createRequest(senderId, senderName)` | Generate an 8-char pairing code (or return existing). Returns `null` if 3 pending requests already exist. |
|
|
233
|
+
| `approve(code)` | Approve a pairing request, adds sender to allowlist. Returns the request or `null`. |
|
|
234
|
+
| `isApproved(senderId)` | Check if sender is in the approved allowlist |
|
|
235
|
+
| `listPending()` | Get active (non-expired) pending requests |
|
|
236
|
+
|
|
237
|
+
## Envelope
|
|
238
|
+
|
|
239
|
+
The normalized message format your adapter must construct:
|
|
240
|
+
|
|
241
|
+
```typescript
|
|
242
|
+
interface Envelope {
|
|
243
|
+
channelName: string; // your channel instance name
|
|
244
|
+
senderId: string; // stable, unique sender ID
|
|
245
|
+
senderName: string; // display name
|
|
246
|
+
chatId: string; // distinguishes DMs from groups
|
|
247
|
+
text: string; // message text (@mentions stripped)
|
|
248
|
+
messageId?: string; // platform message ID
|
|
249
|
+
threadId?: string; // for thread-scoped sessions
|
|
250
|
+
isGroup: boolean; // true for group chats
|
|
251
|
+
isMentioned: boolean; // true if bot was @mentioned
|
|
252
|
+
isReplyToBot: boolean; // true if replying to bot's message
|
|
253
|
+
referencedText?: string; // quoted message text
|
|
254
|
+
imageBase64?: string; // base64-encoded image (legacy — prefer attachments)
|
|
255
|
+
imageMimeType?: string; // e.g. 'image/jpeg' (legacy — prefer attachments)
|
|
256
|
+
attachments?: Attachment[]; // structured file/image/audio/video attachments
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
interface Attachment {
|
|
260
|
+
type: 'image' | 'file' | 'audio' | 'video';
|
|
261
|
+
data?: string; // base64-encoded data (images, small files)
|
|
262
|
+
filePath?: string; // absolute path to local file (large files)
|
|
263
|
+
mimeType: string; // e.g. 'application/pdf', 'image/jpeg'
|
|
264
|
+
fileName?: string; // original file name from the platform
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
`handleInbound()` automatically resolves attachments: images with `data` are sent to the model as vision input, files with `filePath` get their path appended to the prompt text so the agent can read them with its tools.
|
|
269
|
+
|
|
270
|
+
## Block Streaming
|
|
271
|
+
|
|
272
|
+
When `blockStreaming: "on"` is set in a channel's config, the agent's response is delivered as multiple separate messages instead of one large wall of text. The `BlockStreamer` accumulates streaming chunks and emits completed blocks based on paragraph boundaries and size heuristics.
|
|
273
|
+
|
|
274
|
+
**Config fields** (on `ChannelConfig`):
|
|
275
|
+
|
|
276
|
+
| Field | Type | Default | Description |
|
|
277
|
+
| ------------------------ | ------------------------ | --------------- | --------------------------------------------------------------------------- |
|
|
278
|
+
| `blockStreaming` | `'on' \| 'off'` | `'off'` | Enable/disable block streaming |
|
|
279
|
+
| `blockStreamingChunk` | `{ minChars, maxChars }` | `{ 400, 1000 }` | `minChars`: don't emit until this size. `maxChars`: force-emit at this size |
|
|
280
|
+
| `blockStreamingCoalesce` | `{ idleMs }` | `{ 1500 }` | Emit buffered text after this many ms of silence from the agent |
|
|
281
|
+
|
|
282
|
+
**How it works:**
|
|
283
|
+
|
|
284
|
+
1. Text accumulates as the agent streams its response
|
|
285
|
+
2. When the buffer reaches `minChars` and hits a paragraph break (`\n\n`), that block is sent as a separate message
|
|
286
|
+
3. If the buffer reaches `maxChars` without a paragraph break, it force-splits at the best break point (newline > space)
|
|
287
|
+
4. If the agent goes quiet for `idleMs`, the buffer is flushed (as long as it's past `minChars`)
|
|
288
|
+
5. When the agent finishes, any remaining text is sent immediately regardless of `minChars`
|
|
289
|
+
|
|
290
|
+
Block streaming and `onResponseChunk` work independently — plugins can override `onResponseChunk` for their own purposes while block streaming handles delivery.
|
|
291
|
+
|
|
292
|
+
## Further reading
|
|
293
|
+
|
|
294
|
+
- [Channel Plugin Developer Guide](../../docs/developers/channel-plugins.md)
|
|
295
|
+
- [`@matilda-code/channel-plugin-example`](../plugin-example/) — working reference implementation
|