Skip to main content

claude-code-channel/src

packages/claude-code-channel/src

Purpose

Public entry barrel for @moltzap/claude-code-channel. Public entry barrel for @moltzap/claude-code-channel. Only names listed here are part of the public surface.

Public surface

AllowlistError

TypeAlias
export type AllowlistError = SenderNotAllowed | ConversationNotAllowed;

class NoActiveConversation extends Data.TaggedError("NoActiveConversation")<{
  readonly cause: string;
}> {}

bootClaudeCodeChannel

Function
export function bootClaudeCodeChannel(opts: BootOptions)
Single public entry point. In production the CLI binary (cli.ts) calls this; tests call it directly with an injected in-memory MCP transport. Foreign-protocol bridge: step 6c is where the MCP stdio transport attaches. From this point on the process owns two concurrent channels — MCP stdio (outbound to Claude) and MoltZap WS (inbound from server). They meet inside the inbound handler and the reply tool. Fails with:
  • AgentKeyInvalid — opts.agentKey or opts.serverUrl is blank
  • McpTransportFailed — MCP server init or stdio connect rejects (step 6)
  • ServiceRpcError — WS connect / auth rejects (step 8)

BootError

TypeAlias
export type BootError =
  | ServiceRpcError
  | McpTransportFailed
  | AgentKeyInvalid
  | SchemaDecodeFailed;

export class EmitFailed extends Data.TaggedError("EmitFailed")<{
  readonly cause: string;
}> {}

BootOptions

Interface
export interface BootOptions {
  readonly serverUrl: string;
  readonly agentKey: string;
  readonly gateInbound?: GateInbound;

  /**
   * Override the MCP server's advertised name. Defaults to
   * `"@moltzap/claude-code-channel"`.
   */
  readonly serverName?: string;

  /**
   * Override the MCP server's `instructions` string delivered at handshake.
   * Defaults to a contract-conformant default describing the `&lt;channel&gt;` tag
   * shape and the `reply` tool.
   */
  readonly instructions?: string;

  /**
   * Internal test seam. When present, replaces the default
   * `StdioServerTransport` with an injected `Transport` (e.g.
   * `InMemoryTransport`) so integration tests can drive the real
   * `bootClaudeCodeChannel` boot path end-to-end without a subprocess.
   *
   * Field is prefixed `_` and explicitly tagged "tests-only" because no
   * production caller has reason to override the transport — production
   * always uses stdio. Reviewer #256: keep this seam narrow.
   */
  readonly _testTransportFactory?: () => Transport;
}
Boot options — one struct per caller. No Record&lt;string, unknown&gt;, no any. Logging is provided through Effect logger layers at process boundaries.

BootResult

TypeAlias
export type BootResult =
  | { readonly _tag: "Ok"; readonly value: Handle }

ClaudeChannelNotification

Interface
export interface ClaudeChannelNotification {
  readonly method: typeof CLAUDE_CHANNEL_NOTIFICATION_METHOD;
  readonly params: {
    readonly content: string;
    readonly meta: {
      readonly chat_id: ConversationId;
      readonly message_id: MessageId;
      readonly user: UserId;
      readonly ts: IsoTimestamp;
      readonly file_path?: string;
    };
  };
}
Claude Code channel notification shape. The meta keys are FIXED by Anthropic’s channel contract (fakechat reference, server.ts:135-148). Divergence breaks the &lt;channel&gt; tag renderer inside Claude Code.

ConversationId

TypeAlias
export type ConversationId = ProtocolConversationId;
export const ConversationId = conversationId;

/**
 * Branded message id — corresponds to MoltZap's `id`, rendered as
 * contract-meta `message_id`.
 */
export type MessageId = ProtocolMessageId;
export const MessageId = messageId;

export type TaskId = ProtocolTaskId;
export const TaskId = taskId;

/**
 * Branded user id — corresponds to MoltZap's `sender.id`, rendered as
 * contract-meta `user`.
 */
export type UserId = ProtocolAgentId;
export const UserId = agentId;

/**
 * ISO-8601 timestamp — corresponds to MoltZap's `createdAt` (already ISO),
 * rendered as contract-meta `ts`.
 */
export type IsoTimestamp = string & Brand.Brand<"IsoTimestamp">;
Branded conversation id — corresponds to MoltZap’s conversationId on the wire, rendered to Claude Code as the contract-meta key chat_id. Principle 1: preventing accidental confusion with MessageId at call sites.

ConversationId

TypeAlias
export type ConversationId = ProtocolConversationId;
export const ConversationId = conversationId;

/**
 * Branded message id — corresponds to MoltZap's `id`, rendered as
 * contract-meta `message_id`.
 */
export type MessageId = ProtocolMessageId;
export const MessageId = messageId;

export type TaskId = ProtocolTaskId;
export const TaskId = taskId;

/**
 * Branded user id — corresponds to MoltZap's `sender.id`, rendered as
 * contract-meta `user`.
 */
export type UserId = ProtocolAgentId;
export const UserId = agentId;

/**
 * ISO-8601 timestamp — corresponds to MoltZap's `createdAt` (already ISO),
 * rendered as contract-meta `ts`.
 */
export type IsoTimestamp = string & Brand.Brand<"IsoTimestamp">;
Branded conversation id — corresponds to MoltZap’s conversationId on the wire, rendered to Claude Code as the contract-meta key chat_id. Principle 1: preventing accidental confusion with MessageId at call sites.

EventShapeError

TypeAlias
export type EventShapeError = ContentEmpty | MetaInvalid;

GateInbound

TypeAlias
export type GateInbound = (
  event: EnrichedInboundMessage,
) =>
gateInbound hook — zapbot-parity allowlist seam. Must be pure and synchronous. Returning a failure drops the event; no downstream notification is emitted. No I/O, no mutation. The gate may modify the returned EnrichedInboundMessage by returning a new value inside Success — the notification is built from gated.value. The gate runs BEFORE routing.recordInbound; a denied message is never added to the LRU map and cannot be targeted by reply_to. Failure error variants live in errors.ts → AllowlistError (SenderNotAllowed / ConversationNotAllowed).

Handle

Interface
export interface Handle {
  readonly push: (
    notification: ClaudeChannelNotification,
  ) => Effect.Effect<void, PushError>;
  readonly stop: () => Effect.Effect<void>;
}
Lifecycle handle returned by bootClaudeCodeChannel. Principle 3: every operation has a typed error channel. push uses Effect&lt;void, PushError&gt; so the MCP emit failure surfaces as a tag, not a rejected Promise. stop is infallible-by-design (teardown swallows downstream errors into logs per spec I8).

IsoTimestamp

TypeAlias
export type IsoTimestamp = string & Brand.Brand<"IsoTimestamp">;
ISO-8601 timestamp — corresponds to MoltZap’s createdAt (already ISO), rendered as contract-meta ts.

MessageId

TypeAlias
export type MessageId = ProtocolMessageId;
export const MessageId = messageId;

export type TaskId = ProtocolTaskId;
export const TaskId = taskId;

/**
 * Branded user id — corresponds to MoltZap's `sender.id`, rendered as
 * contract-meta `user`.
 */
export type UserId = ProtocolAgentId;
export const UserId = agentId;

/**
 * ISO-8601 timestamp — corresponds to MoltZap's `createdAt` (already ISO),
 * rendered as contract-meta `ts`.
 */
export type IsoTimestamp = string & Brand.Brand<"IsoTimestamp">;
Branded message id — corresponds to MoltZap’s id, rendered as contract-meta message_id.

MessageId

TypeAlias
export type MessageId = ProtocolMessageId;
export const MessageId = messageId;

export type TaskId = ProtocolTaskId;
export const TaskId = taskId;

/**
 * Branded user id — corresponds to MoltZap's `sender.id`, rendered as
 * contract-meta `user`.
 */
export type UserId = ProtocolAgentId;
export const UserId = agentId;

/**
 * ISO-8601 timestamp — corresponds to MoltZap's `createdAt` (already ISO),
 * rendered as contract-meta `ts`.
 */
export type IsoTimestamp = string & Brand.Brand<"IsoTimestamp">;
Branded message id — corresponds to MoltZap’s id, rendered as contract-meta message_id.

PushError

TypeAlias
export type PushError = EmitFailed | NotConnected;

class SenderNotAllowed extends Data.TaggedError("SenderNotAllowed")<{
  readonly senderId: string;
  readonly reason: string;
}> {}

ReplyError

TypeAlias
export type ReplyError =
  | NoActiveConversation
  | ReplyToUnknown
  | SendFailed
  | FilesUnsupported
  | LeaseAlreadyConsumed;

export class ContentEmpty extends Data.TaggedError("ContentEmpty") {}

UserId

TypeAlias
export type UserId = ProtocolAgentId;
export const UserId = agentId;

/**
 * ISO-8601 timestamp — corresponds to MoltZap's `createdAt` (already ISO),
 * rendered as contract-meta `ts`.
 */
export type IsoTimestamp = string & Brand.Brand<"IsoTimestamp">;
Branded user id — corresponds to MoltZap’s sender.id, rendered as contract-meta user.

UserId

TypeAlias
export type UserId = ProtocolAgentId;
export const UserId = agentId;

/**
 * ISO-8601 timestamp — corresponds to MoltZap's `createdAt` (already ISO),
 * rendered as contract-meta `ts`.
 */
export type IsoTimestamp = string & Brand.Brand<"IsoTimestamp">;
Branded user id — corresponds to MoltZap’s sender.id, rendered as contract-meta user.

Files

  • entry.ts
  • errors.ts
  • types.ts