@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.
Files changed (2) hide show
  1. package/README.md +290 -0
  2. 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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maincode-ai/channel-base",
3
- "version": "0.19.0",
3
+ "version": "0.19.2",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",