{"_id":"@boryslav-golubiev/channel-base","name":"@boryslav-golubiev/channel-base","dist-tags":{"latest":"0.14.1"},"versions":{"0.14.1":{"name":"@boryslav-golubiev/channel-base","version":"0.14.1","description":"Base channel infrastructure for Qwen Code","type":"module","main":"dist/index.js","types":"dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"}},"scripts":{"build":"tsc --build"},"dependencies":{"@agentclientprotocol/sdk":"^0.14.1"},"devDependencies":{"typescript":"^5.0.0"},"gitHead":"6785a8d9089c82fe90268e97c3e31d612023656f","_id":"@boryslav-golubiev/channel-base@0.14.1","_nodeVersion":"25.8.2","_npmVersion":"11.11.1","dist":{"integrity":"sha512-YuEbInXjDbJgmfesbH5N74g54yEwZ+t7eN7YBLDJ/btXpXikk6tOCfWPgq3ztUNQGr3T84BmTsblxc/NyLbymw==","shasum":"1472ed2798d8c797ff85717f77ed2649e609520a","tarball":"https://registry.npmjs.org/@boryslav-golubiev/channel-base/-/channel-base-0.14.1.tgz","fileCount":29,"unpackedSize":104174,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIByVLQVwD/+oAarPI+pBvIjxDwBzhU9Uxf4IUjNm0F/LAiEAhWR13Nx5Ae+OOK68BQ1abWk48NExgSWPkU7EpVYmZh4="}]},"_npmUser":{"name":"boryslav-golubiev","email":"borislav.golu@gmail.com"},"directories":{},"maintainers":[{"name":"boryslav-golubiev","email":"borislav.golu@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/channel-base_0.14.1_1775515316094_0.8309153494497847"},"_hasShrinkwrap":false}},"time":{"created":"2026-04-06T22:41:55.922Z","0.14.1":"2026-04-06T22:41:56.247Z","modified":"2026-04-06T22:41:56.537Z"},"maintainers":[{"name":"boryslav-golubiev","email":"borislav.golu@gmail.com"}],"description":"Base channel infrastructure for Qwen Code","readme":"# @qwen-code/channel-base\n\nBase infrastructure for building Qwen Code channel adapters. Provides the abstract base class, access control, session routing, and the ACP bridge that communicates with the agent.\n\nIf you're building a channel plugin, this is your only dependency.\n\n## Install\n\n```bash\nnpm install @qwen-code/channel-base\n```\n\n## Quick start\n\nSubclass `ChannelBase` and implement three methods:\n\n```typescript\nimport { ChannelBase } from '@qwen-code/channel-base';\nimport type {\n  ChannelConfig,\n  Envelope,\n  AcpBridge,\n} from '@qwen-code/channel-base';\n\nclass MyChannel extends ChannelBase {\n  async connect(): Promise<void> {\n    // Connect to platform API, register message handlers.\n    // When a message arrives, build an Envelope and call:\n    //   this.handleInbound(envelope)\n  }\n\n  async sendMessage(chatId: string, text: string): Promise<void> {\n    // Deliver the agent's response to the platform.\n  }\n\n  disconnect(): void {\n    // Clean up connections on shutdown.\n  }\n}\n```\n\nExport a `ChannelPlugin` object so the extension loader can discover it:\n\n```typescript\nimport type { ChannelPlugin } from '@qwen-code/channel-base';\n\nexport const plugin: ChannelPlugin = {\n  channelType: 'my-platform',\n  displayName: 'My Platform',\n  requiredConfigFields: ['apiKey'],\n  createChannel: (name, config, bridge, options) =>\n    new MyChannel(name, config, bridge, options),\n};\n```\n\nFor a complete working example, see [`@qwen-code/channel-plugin-example`](../plugin-example/).\n\n## Architecture\n\n```\nInbound:  Platform message\n            → Envelope (with attachments)\n            → GroupGate (group policy + mention gating)\n            → SenderGate (allowlist / pairing / open)\n            → Slash commands (/clear, /help, /status)\n            → SessionRouter (resolve or create ACP session)\n            → Resolve attachments (images → bridge, files → prompt text)\n            → AcpBridge.prompt() → agent\n\nOutbound: Agent response\n            → BlockStreamer (if enabled: split into blocks at paragraph boundaries)\n            → sendMessage() → platform\n```\n\nEverything between `handleInbound()` and `sendMessage()` is handled by the base class — your adapter only deals with platform I/O.\n\n## Exports\n\n### Classes\n\n| Class           | Purpose                                                          |\n| --------------- | ---------------------------------------------------------------- |\n| `ChannelBase`   | Abstract base class — extend this to build a channel adapter     |\n| `AcpBridge`     | Spawns and communicates with the `qwen-code --acp` agent process |\n| `BlockStreamer` | Progressive multi-message delivery for block streaming           |\n| `SessionRouter` | Maps senders to ACP sessions with configurable scoping           |\n| `SenderGate`    | DM access control (allowlist / pairing / open)                   |\n| `GroupGate`     | Group chat policy and @mention gating                            |\n| `PairingStore`  | Pairing code generation, approval, and allowlist persistence     |\n\n### Types\n\n| Type            | Description                                    |\n| --------------- | ---------------------------------------------- |\n| `Attachment`    | Structured file/image/audio/video attachment   |\n| `ChannelConfig` | Channel configuration from `settings.json`     |\n| `ChannelPlugin` | Plugin factory interface (what you export)     |\n| `Envelope`      | Normalized inbound message format              |\n| `SenderPolicy`  | `'allowlist' \\| 'pairing' \\| 'open'`           |\n| `GroupPolicy`   | `'disabled' \\| 'allowlist' \\| 'open'`          |\n| `SessionScope`  | `'user' \\| 'thread' \\| 'single'`               |\n| `GroupConfig`   | Per-group settings (e.g. `requireMention`)     |\n| `SessionTarget` | Maps a session back to its channel/sender/chat |\n\n## API reference\n\n### ChannelBase\n\n```typescript\nconstructor(name: string, config: ChannelConfig, bridge: AcpBridge, options?: ChannelBaseOptions)\n```\n\n**Abstract methods** (you must implement):\n\n| Method          | Signature                                                                    |\n| --------------- | ---------------------------------------------------------------------------- |\n| `connect()`     | `() => Promise<void>` — Connect to the platform and start receiving messages |\n| `sendMessage()` | `(chatId: string, text: string) => Promise<void>` — Deliver agent response   |\n| `disconnect()`  | `() => void` — Clean up on shutdown                                          |\n\n**Provided methods:**\n\n| Method                                            | Description                                                                                                                       |\n| ------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |\n| `handleInbound(envelope)`                         | Route an inbound message through the full pipeline (gate checks, commands, session, prompt). Call this from your message handler. |\n| `setBridge(bridge)`                               | Replace the ACP bridge after crash recovery                                                                                       |\n| `registerCommand(name, handler)`                  | Register a custom slash command (e.g. `/mycommand`)                                                                               |\n| `onToolCall(chatId, event)`                       | Hook called on agent tool invocations — override to show indicators                                                               |\n| `onResponseChunk(chatId, chunk, sessionId)`       | Hook called per streaming text chunk — override for progressive display (default: no-op)                                          |\n| `onResponseComplete(chatId, fullText, sessionId)` | Hook called when full response is ready — override to customize delivery (default: `sendMessage()`)                               |\n\n**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.\n\n**Built-in slash commands:** `/clear` (`/reset`, `/new`), `/help`, `/status`\n\n### AcpBridge\n\nManages the `qwen-code --acp` child process and ACP sessions.\n\n```typescript\nconstructor(options: { cliEntryPath: string; cwd: string; model?: string })\n```\n\n| Method                              | Description                                                                                                       |\n| ----------------------------------- | ----------------------------------------------------------------------------------------------------------------- |\n| `start()`                           | Spawn the agent process                                                                                           |\n| `stop()`                            | Kill the agent process                                                                                            |\n| `newSession(cwd)`                   | Create a new ACP session, returns `sessionId`                                                                     |\n| `loadSession(sessionId, cwd)`       | Restore an existing session                                                                                       |\n| `prompt(sessionId, text, options?)` | Send a message to the agent, returns the full response text. Supports optional `imageBase64` and `imageMimeType`. |\n| `isConnected`                       | Whether the agent process is alive                                                                                |\n\n**Events** (EventEmitter):\n\n| Event          | Payload                  | Description              |\n| -------------- | ------------------------ | ------------------------ |\n| `textChunk`    | `(sessionId, chunk)`     | Streaming response chunk |\n| `toolCall`     | `(event: ToolCallEvent)` | Agent invoked a tool     |\n| `disconnected` | `(code, signal)`         | Agent process exited     |\n\n### SessionRouter\n\nMaps senders to ACP sessions based on the configured scope.\n\n```typescript\nconstructor(bridge: AcpBridge, defaultCwd: string, scope?: SessionScope, persistPath?: string)\n```\n\n**Routing keys by scope:**\n\n| Scope            | Key format                | Effect                                    |\n| ---------------- | ------------------------- | ----------------------------------------- |\n| `user` (default) | `channel:senderId:chatId` | Each user gets their own session per chat |\n| `thread`         | `channel:threadId`        | One session per thread                    |\n| `single`         | `channel:__single__`      | One shared session for the entire channel |\n\n| Method                                                    | Description                                                 |\n| --------------------------------------------------------- | ----------------------------------------------------------- |\n| `resolve(channelName, senderId, chatId, threadId?, cwd?)` | Get or create a session for the given sender                |\n| `removeSession(channelName, senderId, chatId?)`           | Remove session(s) — used by `/clear`                        |\n| `restoreSessions()`                                       | Reload sessions from disk after bridge restart              |\n| `clearAll()`                                              | Clear all sessions and delete persist file (clean shutdown) |\n\n### SenderGate\n\n```typescript\nconstructor(policy: SenderPolicy, allowedUsers?: string[], pairingStore?: PairingStore)\n```\n\n| Method                         | Description                                                  |\n| ------------------------------ | ------------------------------------------------------------ |\n| `check(senderId, senderName?)` | Returns `{ allowed: boolean, pairingCode?: string \\| null }` |\n\n**Policy behavior:**\n\n| Policy      | Behavior                                                                                                  |\n| ----------- | --------------------------------------------------------------------------------------------------------- |\n| `open`      | Everyone allowed                                                                                          |\n| `allowlist` | Only `allowedUsers` allowed                                                                               |\n| `pairing`   | Check allowlist, then approved pairings, then generate a pairing code (8-char, 1hr expiry, max 3 pending) |\n\n### GroupGate\n\n```typescript\nconstructor(policy?: GroupPolicy, groups?: Record<string, GroupConfig>)\n```\n\n| Method            | Description                                                                                    |\n| ----------------- | ---------------------------------------------------------------------------------------------- |\n| `check(envelope)` | Returns `{ allowed: boolean, reason?: 'disabled' \\| 'not_allowlisted' \\| 'mention_required' }` |\n\n**Policy behavior:**\n\n| Policy      | Behavior                                 |\n| ----------- | ---------------------------------------- |\n| `disabled`  | All group messages rejected              |\n| `allowlist` | Only groups listed in config are allowed |\n| `open`      | All groups allowed                       |\n\nWhen `requireMention` is `true` (default), group messages are only processed if the bot is @mentioned or the message is a reply to the bot.\n\n### PairingStore\n\n```typescript\nconstructor(channelName: string)\n```\n\nPersists pairing state to `~/.qwen/channels/{channelName}-pairing.json` and `{channelName}-allowlist.json`.\n\n| Method                                | Description                                                                                               |\n| ------------------------------------- | --------------------------------------------------------------------------------------------------------- |\n| `createRequest(senderId, senderName)` | Generate an 8-char pairing code (or return existing). Returns `null` if 3 pending requests already exist. |\n| `approve(code)`                       | Approve a pairing request, adds sender to allowlist. Returns the request or `null`.                       |\n| `isApproved(senderId)`                | Check if sender is in the approved allowlist                                                              |\n| `listPending()`                       | Get active (non-expired) pending requests                                                                 |\n\n## Envelope\n\nThe normalized message format your adapter must construct:\n\n```typescript\ninterface Envelope {\n  channelName: string; // your channel instance name\n  senderId: string; // stable, unique sender ID\n  senderName: string; // display name\n  chatId: string; // distinguishes DMs from groups\n  text: string; // message text (@mentions stripped)\n  messageId?: string; // platform message ID\n  threadId?: string; // for thread-scoped sessions\n  isGroup: boolean; // true for group chats\n  isMentioned: boolean; // true if bot was @mentioned\n  isReplyToBot: boolean; // true if replying to bot's message\n  referencedText?: string; // quoted message text\n  imageBase64?: string; // base64-encoded image (legacy — prefer attachments)\n  imageMimeType?: string; // e.g. 'image/jpeg' (legacy — prefer attachments)\n  attachments?: Attachment[]; // structured file/image/audio/video attachments\n}\n\ninterface Attachment {\n  type: 'image' | 'file' | 'audio' | 'video';\n  data?: string; // base64-encoded data (images, small files)\n  filePath?: string; // absolute path to local file (large files)\n  mimeType: string; // e.g. 'application/pdf', 'image/jpeg'\n  fileName?: string; // original file name from the platform\n}\n```\n\n`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.\n\n## Block Streaming\n\nWhen `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.\n\n**Config fields** (on `ChannelConfig`):\n\n| Field                    | Type                     | Default         | Description                                                                 |\n| ------------------------ | ------------------------ | --------------- | --------------------------------------------------------------------------- |\n| `blockStreaming`         | `'on' \\| 'off'`          | `'off'`         | Enable/disable block streaming                                              |\n| `blockStreamingChunk`    | `{ minChars, maxChars }` | `{ 400, 1000 }` | `minChars`: don't emit until this size. `maxChars`: force-emit at this size |\n| `blockStreamingCoalesce` | `{ idleMs }`             | `{ 1500 }`      | Emit buffered text after this many ms of silence from the agent             |\n\n**How it works:**\n\n1. Text accumulates as the agent streams its response\n2. When the buffer reaches `minChars` and hits a paragraph break (`\\n\\n`), that block is sent as a separate message\n3. If the buffer reaches `maxChars` without a paragraph break, it force-splits at the best break point (newline > space)\n4. If the agent goes quiet for `idleMs`, the buffer is flushed (as long as it's past `minChars`)\n5. When the agent finishes, any remaining text is sent immediately regardless of `minChars`\n\nBlock streaming and `onResponseChunk` work independently — plugins can override `onResponseChunk` for their own purposes while block streaming handles delivery.\n\n## Further reading\n\n- [Channel Plugin Developer Guide](../../docs/developers/channel-plugins.md)\n- [`@qwen-code/channel-plugin-example`](../plugin-example/) — working reference implementation\n","readmeFilename":"README.md","_rev":"1-d7c92f7286b8e8bf4362a2690b0ecd6e"}