Spaces:
Paused
Paused
| /** | |
| * JSON-RPC 2.0 message types and protocol helpers for the host ↔ worker IPC | |
| * channel. | |
| * | |
| * The Paperclip plugin runtime uses JSON-RPC 2.0 over stdio to communicate | |
| * between the host process and each plugin worker process. This module defines: | |
| * | |
| * - Core JSON-RPC 2.0 envelope types (request, response, notification, error) | |
| * - Standard and plugin-specific error codes | |
| * - Typed method maps for host→worker and worker→host calls | |
| * - Helper functions for creating well-formed messages | |
| * | |
| * @see PLUGIN_SPEC.md §12.1 — Process Model | |
| * @see PLUGIN_SPEC.md §13 — Host-Worker Protocol | |
| * @see https://www.jsonrpc.org/specification | |
| */ | |
| import type { | |
| PaperclipPluginManifestV1, | |
| PluginLauncherBounds, | |
| PluginLauncherRenderContextSnapshot, | |
| PluginLauncherRenderEnvironment, | |
| PluginStateScopeKind, | |
| Company, | |
| Project, | |
| Issue, | |
| IssueComment, | |
| IssueDocument, | |
| IssueDocumentSummary, | |
| Agent, | |
| Goal, | |
| } from "@paperclipai/shared"; | |
| export type { PluginLauncherRenderContextSnapshot } from "@paperclipai/shared"; | |
| import type { | |
| PluginEvent, | |
| PluginJobContext, | |
| PluginWorkspace, | |
| ToolRunContext, | |
| ToolResult, | |
| } from "./types.js"; | |
| import type { | |
| PluginHealthDiagnostics, | |
| PluginConfigValidationResult, | |
| PluginWebhookInput, | |
| } from "./define-plugin.js"; | |
| // --------------------------------------------------------------------------- | |
| // JSON-RPC 2.0 — Core Protocol Types | |
| // --------------------------------------------------------------------------- | |
| /** The JSON-RPC protocol version. Always `"2.0"`. */ | |
| export const JSONRPC_VERSION = "2.0" as const; | |
| /** | |
| * A unique request identifier. JSON-RPC 2.0 allows strings or numbers; | |
| * we use strings (UUIDs or monotonic counters) for all Paperclip messages. | |
| */ | |
| export type JsonRpcId = string | number; | |
| /** | |
| * A JSON-RPC 2.0 request message. | |
| * | |
| * The host sends requests to the worker (or vice versa) and expects a | |
| * matching response with the same `id`. | |
| */ | |
| export interface JsonRpcRequest< | |
| TMethod extends string = string, | |
| TParams = unknown, | |
| > { | |
| readonly jsonrpc: typeof JSONRPC_VERSION; | |
| /** Unique request identifier. Must be echoed in the response. */ | |
| readonly id: JsonRpcId; | |
| /** The RPC method name to invoke. */ | |
| readonly method: TMethod; | |
| /** Structured parameters for the method call. */ | |
| readonly params: TParams; | |
| } | |
| /** | |
| * A JSON-RPC 2.0 success response. | |
| */ | |
| export interface JsonRpcSuccessResponse<TResult = unknown> { | |
| readonly jsonrpc: typeof JSONRPC_VERSION; | |
| /** Echoed request identifier. */ | |
| readonly id: JsonRpcId; | |
| /** The method return value. */ | |
| readonly result: TResult; | |
| readonly error?: never; | |
| } | |
| /** | |
| * A JSON-RPC 2.0 error object embedded in an error response. | |
| */ | |
| export interface JsonRpcError<TData = unknown> { | |
| /** Machine-readable error code. */ | |
| readonly code: number; | |
| /** Human-readable error message. */ | |
| readonly message: string; | |
| /** Optional structured error data. */ | |
| readonly data?: TData; | |
| } | |
| /** | |
| * A JSON-RPC 2.0 error response. | |
| */ | |
| export interface JsonRpcErrorResponse<TData = unknown> { | |
| readonly jsonrpc: typeof JSONRPC_VERSION; | |
| /** Echoed request identifier. */ | |
| readonly id: JsonRpcId | null; | |
| readonly result?: never; | |
| /** The error object. */ | |
| readonly error: JsonRpcError<TData>; | |
| } | |
| /** | |
| * A JSON-RPC 2.0 response — either success or error. | |
| */ | |
| export type JsonRpcResponse<TResult = unknown, TData = unknown> = | |
| | JsonRpcSuccessResponse<TResult> | |
| | JsonRpcErrorResponse<TData>; | |
| /** | |
| * A JSON-RPC 2.0 notification (a request with no `id`). | |
| * | |
| * Notifications are fire-and-forget — no response is expected. | |
| */ | |
| export interface JsonRpcNotification< | |
| TMethod extends string = string, | |
| TParams = unknown, | |
| > { | |
| readonly jsonrpc: typeof JSONRPC_VERSION; | |
| readonly id?: never; | |
| /** The notification method name. */ | |
| readonly method: TMethod; | |
| /** Structured parameters for the notification. */ | |
| readonly params: TParams; | |
| } | |
| /** | |
| * Any well-formed JSON-RPC 2.0 message (request, response, or notification). | |
| */ | |
| export type JsonRpcMessage = | |
| | JsonRpcRequest | |
| | JsonRpcResponse | |
| | JsonRpcNotification; | |
| // --------------------------------------------------------------------------- | |
| // Error Codes | |
| // --------------------------------------------------------------------------- | |
| /** | |
| * Standard JSON-RPC 2.0 error codes. | |
| * | |
| * @see https://www.jsonrpc.org/specification#error_object | |
| */ | |
| export const JSONRPC_ERROR_CODES = { | |
| /** Invalid JSON was received by the server. */ | |
| PARSE_ERROR: -32700, | |
| /** The JSON sent is not a valid Request object. */ | |
| INVALID_REQUEST: -32600, | |
| /** The method does not exist or is not available. */ | |
| METHOD_NOT_FOUND: -32601, | |
| /** Invalid method parameter(s). */ | |
| INVALID_PARAMS: -32602, | |
| /** Internal JSON-RPC error. */ | |
| INTERNAL_ERROR: -32603, | |
| } as const; | |
| export type JsonRpcErrorCode = | |
| (typeof JSONRPC_ERROR_CODES)[keyof typeof JSONRPC_ERROR_CODES]; | |
| /** | |
| * Paperclip plugin-specific error codes. | |
| * | |
| * These live in the JSON-RPC "server error" reserved range (-32000 to -32099) | |
| * as specified by JSON-RPC 2.0 for implementation-defined server errors. | |
| * | |
| * @see PLUGIN_SPEC.md §19.7 — Error Propagation Through The Bridge | |
| */ | |
| export const PLUGIN_RPC_ERROR_CODES = { | |
| /** The worker process is not running or not reachable. */ | |
| WORKER_UNAVAILABLE: -32000, | |
| /** The plugin does not have the required capability for this operation. */ | |
| CAPABILITY_DENIED: -32001, | |
| /** The worker reported an unhandled error during method execution. */ | |
| WORKER_ERROR: -32002, | |
| /** The method call timed out waiting for the worker response. */ | |
| TIMEOUT: -32003, | |
| /** The worker does not implement the requested optional method. */ | |
| METHOD_NOT_IMPLEMENTED: -32004, | |
| /** A catch-all for errors that do not fit other categories. */ | |
| UNKNOWN: -32099, | |
| } as const; | |
| export type PluginRpcErrorCode = | |
| (typeof PLUGIN_RPC_ERROR_CODES)[keyof typeof PLUGIN_RPC_ERROR_CODES]; | |
| // --------------------------------------------------------------------------- | |
| // Host → Worker Method Signatures (§13 Host-Worker Protocol) | |
| // --------------------------------------------------------------------------- | |
| /** | |
| * Input for the `initialize` RPC method. | |
| * | |
| * @see PLUGIN_SPEC.md §13.1 — `initialize` | |
| */ | |
| export interface InitializeParams { | |
| /** Full plugin manifest snapshot. */ | |
| manifest: PaperclipPluginManifestV1; | |
| /** Resolved operator configuration (validated against `instanceConfigSchema`). */ | |
| config: Record<string, unknown>; | |
| /** Instance-level metadata. */ | |
| instanceInfo: { | |
| /** UUID of this Paperclip instance. */ | |
| instanceId: string; | |
| /** Semver version of the running Paperclip host. */ | |
| hostVersion: string; | |
| }; | |
| /** Host API version. */ | |
| apiVersion: number; | |
| } | |
| /** | |
| * Result returned by the `initialize` RPC method. | |
| */ | |
| export interface InitializeResult { | |
| /** Whether initialization succeeded. */ | |
| ok: boolean; | |
| /** Optional methods the worker has implemented (e.g. "validateConfig", "onEvent"). */ | |
| supportedMethods?: string[]; | |
| } | |
| /** | |
| * Input for the `configChanged` RPC method. | |
| * | |
| * @see PLUGIN_SPEC.md §13.4 — `configChanged` | |
| */ | |
| export interface ConfigChangedParams { | |
| /** The newly resolved configuration. */ | |
| config: Record<string, unknown>; | |
| } | |
| /** | |
| * Input for the `validateConfig` RPC method. | |
| * | |
| * @see PLUGIN_SPEC.md §13.3 — `validateConfig` | |
| */ | |
| export interface ValidateConfigParams { | |
| /** The configuration to validate. */ | |
| config: Record<string, unknown>; | |
| } | |
| /** | |
| * Input for the `onEvent` RPC method. | |
| * | |
| * @see PLUGIN_SPEC.md §13.5 — `onEvent` | |
| */ | |
| export interface OnEventParams { | |
| /** The domain event to deliver. */ | |
| event: PluginEvent; | |
| } | |
| /** | |
| * Input for the `runJob` RPC method. | |
| * | |
| * @see PLUGIN_SPEC.md §13.6 — `runJob` | |
| */ | |
| export interface RunJobParams { | |
| /** Job execution context. */ | |
| job: PluginJobContext; | |
| } | |
| /** | |
| * Input for the `getData` RPC method. | |
| * | |
| * @see PLUGIN_SPEC.md §13.8 — `getData` | |
| */ | |
| export interface GetDataParams { | |
| /** Plugin-defined data key (e.g. `"sync-health"`). */ | |
| key: string; | |
| /** Context and query parameters from the UI. */ | |
| params: Record<string, unknown>; | |
| /** Optional launcher/container metadata from the host render environment. */ | |
| renderEnvironment?: PluginLauncherRenderContextSnapshot | null; | |
| } | |
| /** | |
| * Input for the `performAction` RPC method. | |
| * | |
| * @see PLUGIN_SPEC.md §13.9 — `performAction` | |
| */ | |
| export interface PerformActionParams { | |
| /** Plugin-defined action key (e.g. `"resync"`). */ | |
| key: string; | |
| /** Action parameters from the UI. */ | |
| params: Record<string, unknown>; | |
| /** Optional launcher/container metadata from the host render environment. */ | |
| renderEnvironment?: PluginLauncherRenderContextSnapshot | null; | |
| } | |
| /** | |
| * Input for the `executeTool` RPC method. | |
| * | |
| * @see PLUGIN_SPEC.md §13.10 — `executeTool` | |
| */ | |
| export interface ExecuteToolParams { | |
| /** Tool name (without plugin namespace prefix). */ | |
| toolName: string; | |
| /** Parsed parameters matching the tool's declared schema. */ | |
| parameters: unknown; | |
| /** Agent run context. */ | |
| runContext: ToolRunContext; | |
| } | |
| // --------------------------------------------------------------------------- | |
| // UI launcher / modal host interaction payloads | |
| // --------------------------------------------------------------------------- | |
| /** | |
| * Bounds request issued by a plugin UI running inside a host-managed launcher | |
| * container such as a modal, drawer, or popover. | |
| */ | |
| export interface PluginModalBoundsRequest { | |
| /** High-level size preset requested from the host. */ | |
| bounds: PluginLauncherBounds; | |
| /** Optional explicit width override in CSS pixels. */ | |
| width?: number; | |
| /** Optional explicit height override in CSS pixels. */ | |
| height?: number; | |
| /** Optional lower bounds for host resizing decisions. */ | |
| minWidth?: number; | |
| minHeight?: number; | |
| /** Optional upper bounds for host resizing decisions. */ | |
| maxWidth?: number; | |
| maxHeight?: number; | |
| } | |
| /** | |
| * Reason metadata supplied by host-managed close lifecycle callbacks. | |
| */ | |
| export interface PluginRenderCloseEvent { | |
| reason: | |
| | "escapeKey" | |
| | "backdrop" | |
| | "hostNavigation" | |
| | "programmatic" | |
| | "submit" | |
| | "unknown"; | |
| nativeEvent?: unknown; | |
| } | |
| /** | |
| * Map of host→worker RPC method names to their `[params, result]` types. | |
| * | |
| * This type is the single source of truth for all methods the host can call | |
| * on a worker. Used by both the host dispatcher and the worker handler to | |
| * ensure type safety across the IPC boundary. | |
| */ | |
| export interface HostToWorkerMethods { | |
| /** @see PLUGIN_SPEC.md §13.1 */ | |
| initialize: [params: InitializeParams, result: InitializeResult]; | |
| /** @see PLUGIN_SPEC.md §13.2 */ | |
| health: [params: Record<string, never>, result: PluginHealthDiagnostics]; | |
| /** @see PLUGIN_SPEC.md §12.5 */ | |
| shutdown: [params: Record<string, never>, result: void]; | |
| /** @see PLUGIN_SPEC.md §13.3 */ | |
| validateConfig: [params: ValidateConfigParams, result: PluginConfigValidationResult]; | |
| /** @see PLUGIN_SPEC.md §13.4 */ | |
| configChanged: [params: ConfigChangedParams, result: void]; | |
| /** @see PLUGIN_SPEC.md §13.5 */ | |
| onEvent: [params: OnEventParams, result: void]; | |
| /** @see PLUGIN_SPEC.md §13.6 */ | |
| runJob: [params: RunJobParams, result: void]; | |
| /** @see PLUGIN_SPEC.md §13.7 */ | |
| handleWebhook: [params: PluginWebhookInput, result: void]; | |
| /** @see PLUGIN_SPEC.md §13.8 */ | |
| getData: [params: GetDataParams, result: unknown]; | |
| /** @see PLUGIN_SPEC.md §13.9 */ | |
| performAction: [params: PerformActionParams, result: unknown]; | |
| /** @see PLUGIN_SPEC.md §13.10 */ | |
| executeTool: [params: ExecuteToolParams, result: ToolResult]; | |
| } | |
| /** Union of all host→worker method names. */ | |
| export type HostToWorkerMethodName = keyof HostToWorkerMethods; | |
| /** Required methods the worker MUST implement. */ | |
| export const HOST_TO_WORKER_REQUIRED_METHODS: readonly HostToWorkerMethodName[] = [ | |
| "initialize", | |
| "health", | |
| "shutdown", | |
| ] as const; | |
| /** Optional methods the worker MAY implement. */ | |
| export const HOST_TO_WORKER_OPTIONAL_METHODS: readonly HostToWorkerMethodName[] = [ | |
| "validateConfig", | |
| "configChanged", | |
| "onEvent", | |
| "runJob", | |
| "handleWebhook", | |
| "getData", | |
| "performAction", | |
| "executeTool", | |
| ] as const; | |
| // --------------------------------------------------------------------------- | |
| // Worker → Host Method Signatures (SDK client calls) | |
| // --------------------------------------------------------------------------- | |
| /** | |
| * Map of worker→host RPC method names to their `[params, result]` types. | |
| * | |
| * These represent the SDK client calls that the worker makes back to the | |
| * host to access platform services (state, entities, config, etc.). | |
| */ | |
| export interface WorkerToHostMethods { | |
| // Config | |
| "config.get": [params: Record<string, never>, result: Record<string, unknown>]; | |
| // State | |
| "state.get": [ | |
| params: { scopeKind: string; scopeId?: string; namespace?: string; stateKey: string }, | |
| result: unknown, | |
| ]; | |
| "state.set": [ | |
| params: { scopeKind: string; scopeId?: string; namespace?: string; stateKey: string; value: unknown }, | |
| result: void, | |
| ]; | |
| "state.delete": [ | |
| params: { scopeKind: string; scopeId?: string; namespace?: string; stateKey: string }, | |
| result: void, | |
| ]; | |
| // Entities | |
| "entities.upsert": [ | |
| params: { | |
| entityType: string; | |
| scopeKind: PluginStateScopeKind; | |
| scopeId?: string; | |
| externalId?: string; | |
| title?: string; | |
| status?: string; | |
| data: Record<string, unknown>; | |
| }, | |
| result: { | |
| id: string; | |
| entityType: string; | |
| scopeKind: PluginStateScopeKind; | |
| scopeId: string | null; | |
| externalId: string | null; | |
| title: string | null; | |
| status: string | null; | |
| data: Record<string, unknown>; | |
| createdAt: string; | |
| updatedAt: string; | |
| }, | |
| ]; | |
| "entities.list": [ | |
| params: { | |
| entityType?: string; | |
| scopeKind?: PluginStateScopeKind; | |
| scopeId?: string; | |
| externalId?: string; | |
| limit?: number; | |
| offset?: number; | |
| }, | |
| result: Array<{ | |
| id: string; | |
| entityType: string; | |
| scopeKind: PluginStateScopeKind; | |
| scopeId: string | null; | |
| externalId: string | null; | |
| title: string | null; | |
| status: string | null; | |
| data: Record<string, unknown>; | |
| createdAt: string; | |
| updatedAt: string; | |
| }>, | |
| ]; | |
| // Events | |
| "events.emit": [ | |
| params: { name: string; companyId: string; payload: unknown }, | |
| result: void, | |
| ]; | |
| "events.subscribe": [ | |
| params: { eventPattern: string; filter?: Record<string, unknown> | null }, | |
| result: void, | |
| ]; | |
| // HTTP | |
| "http.fetch": [ | |
| params: { url: string; init?: Record<string, unknown> }, | |
| result: { status: number; statusText: string; headers: Record<string, string>; body: string }, | |
| ]; | |
| // Secrets | |
| "secrets.resolve": [ | |
| params: { secretRef: string }, | |
| result: string, | |
| ]; | |
| // Activity | |
| "activity.log": [ | |
| params: { | |
| companyId: string; | |
| message: string; | |
| entityType?: string; | |
| entityId?: string; | |
| metadata?: Record<string, unknown>; | |
| }, | |
| result: void, | |
| ]; | |
| // Metrics | |
| "metrics.write": [ | |
| params: { name: string; value: number; tags?: Record<string, string> }, | |
| result: void, | |
| ]; | |
| // Telemetry | |
| "telemetry.track": [ | |
| params: { eventName: string; dimensions?: Record<string, string | number | boolean> }, | |
| result: void, | |
| ]; | |
| // Logger | |
| "log": [ | |
| params: { level: "info" | "warn" | "error" | "debug"; message: string; meta?: Record<string, unknown> }, | |
| result: void, | |
| ]; | |
| // Companies (read) | |
| "companies.list": [ | |
| params: { limit?: number; offset?: number }, | |
| result: Company[], | |
| ]; | |
| "companies.get": [ | |
| params: { companyId: string }, | |
| result: Company | null, | |
| ]; | |
| // Projects (read) | |
| "projects.list": [ | |
| params: { companyId: string; limit?: number; offset?: number }, | |
| result: Project[], | |
| ]; | |
| "projects.get": [ | |
| params: { projectId: string; companyId: string }, | |
| result: Project | null, | |
| ]; | |
| "projects.listWorkspaces": [ | |
| params: { projectId: string; companyId: string }, | |
| result: PluginWorkspace[], | |
| ]; | |
| "projects.getPrimaryWorkspace": [ | |
| params: { projectId: string; companyId: string }, | |
| result: PluginWorkspace | null, | |
| ]; | |
| "projects.getWorkspaceForIssue": [ | |
| params: { issueId: string; companyId: string }, | |
| result: PluginWorkspace | null, | |
| ]; | |
| // Issues | |
| "issues.list": [ | |
| params: { | |
| companyId: string; | |
| projectId?: string; | |
| assigneeAgentId?: string; | |
| status?: string; | |
| limit?: number; | |
| offset?: number; | |
| }, | |
| result: Issue[], | |
| ]; | |
| "issues.get": [ | |
| params: { issueId: string; companyId: string }, | |
| result: Issue | null, | |
| ]; | |
| "issues.create": [ | |
| params: { | |
| companyId: string; | |
| projectId?: string; | |
| goalId?: string; | |
| parentId?: string; | |
| inheritExecutionWorkspaceFromIssueId?: string; | |
| title: string; | |
| description?: string; | |
| priority?: string; | |
| assigneeAgentId?: string; | |
| }, | |
| result: Issue, | |
| ]; | |
| "issues.update": [ | |
| params: { | |
| issueId: string; | |
| patch: Record<string, unknown>; | |
| companyId: string; | |
| }, | |
| result: Issue, | |
| ]; | |
| "issues.listComments": [ | |
| params: { issueId: string; companyId: string }, | |
| result: IssueComment[], | |
| ]; | |
| "issues.createComment": [ | |
| params: { issueId: string; body: string; companyId: string; authorAgentId?: string }, | |
| result: IssueComment, | |
| ]; | |
| // Issue Documents | |
| "issues.documents.list": [ | |
| params: { issueId: string; companyId: string }, | |
| result: IssueDocumentSummary[], | |
| ]; | |
| "issues.documents.get": [ | |
| params: { issueId: string; key: string; companyId: string }, | |
| result: IssueDocument | null, | |
| ]; | |
| "issues.documents.upsert": [ | |
| params: { | |
| issueId: string; | |
| key: string; | |
| body: string; | |
| companyId: string; | |
| title?: string; | |
| format?: string; | |
| changeSummary?: string; | |
| }, | |
| result: IssueDocument, | |
| ]; | |
| "issues.documents.delete": [ | |
| params: { issueId: string; key: string; companyId: string }, | |
| result: void, | |
| ]; | |
| // Agents (read) | |
| "agents.list": [ | |
| params: { companyId: string; status?: string; limit?: number; offset?: number }, | |
| result: Agent[], | |
| ]; | |
| "agents.get": [ | |
| params: { agentId: string; companyId: string }, | |
| result: Agent | null, | |
| ]; | |
| // Agents (write) | |
| "agents.pause": [ | |
| params: { agentId: string; companyId: string }, | |
| result: Agent, | |
| ]; | |
| "agents.resume": [ | |
| params: { agentId: string; companyId: string }, | |
| result: Agent, | |
| ]; | |
| "agents.invoke": [ | |
| params: { agentId: string; companyId: string; prompt: string; reason?: string }, | |
| result: { runId: string }, | |
| ]; | |
| // Agent Sessions | |
| "agents.sessions.create": [ | |
| params: { agentId: string; companyId: string; taskKey?: string; reason?: string }, | |
| result: { sessionId: string; agentId: string; companyId: string; status: "active" | "closed"; createdAt: string }, | |
| ]; | |
| "agents.sessions.list": [ | |
| params: { agentId: string; companyId: string }, | |
| result: Array<{ sessionId: string; agentId: string; companyId: string; status: "active" | "closed"; createdAt: string }>, | |
| ]; | |
| "agents.sessions.sendMessage": [ | |
| params: { sessionId: string; companyId: string; prompt: string; reason?: string }, | |
| result: { runId: string }, | |
| ]; | |
| "agents.sessions.close": [ | |
| params: { sessionId: string; companyId: string }, | |
| result: void, | |
| ]; | |
| // Goals | |
| "goals.list": [ | |
| params: { companyId: string; level?: string; status?: string; limit?: number; offset?: number }, | |
| result: Goal[], | |
| ]; | |
| "goals.get": [ | |
| params: { goalId: string; companyId: string }, | |
| result: Goal | null, | |
| ]; | |
| "goals.create": [ | |
| params: { | |
| companyId: string; | |
| title: string; | |
| description?: string; | |
| level?: string; | |
| status?: string; | |
| parentId?: string; | |
| ownerAgentId?: string; | |
| }, | |
| result: Goal, | |
| ]; | |
| "goals.update": [ | |
| params: { | |
| goalId: string; | |
| patch: Record<string, unknown>; | |
| companyId: string; | |
| }, | |
| result: Goal, | |
| ]; | |
| } | |
| /** Union of all worker→host method names. */ | |
| export type WorkerToHostMethodName = keyof WorkerToHostMethods; | |
| // --------------------------------------------------------------------------- | |
| // Worker→Host Notification Types (fire-and-forget, no response) | |
| // --------------------------------------------------------------------------- | |
| /** | |
| * Typed parameter shapes for worker→host JSON-RPC notifications. | |
| * | |
| * Notifications are fire-and-forget — the worker does not wait for a response. | |
| * These are used for streaming events and logging, not for request-response RPCs. | |
| */ | |
| export interface WorkerToHostNotifications { | |
| /** | |
| * Forward a stream event to connected SSE clients. | |
| * | |
| * Emitted by the worker for each event on a stream channel. The host | |
| * publishes to the PluginStreamBus, which fans out to all SSE clients | |
| * subscribed to the (pluginId, channel, companyId) tuple. | |
| * | |
| * The `event` payload is JSON-serializable and sent as SSE `data:`. | |
| * The default SSE event type is `"message"`. | |
| */ | |
| "streams.emit": { | |
| channel: string; | |
| companyId: string; | |
| event: unknown; | |
| }; | |
| /** | |
| * Signal that a stream channel has been opened. | |
| * | |
| * Emitted when the worker calls `ctx.streams.open(channel, companyId)`. | |
| * UI clients may use this to display a "connected" indicator or begin | |
| * buffering input. The host tracks open channels so it can emit synthetic | |
| * close events if the worker crashes. | |
| */ | |
| "streams.open": { | |
| channel: string; | |
| companyId: string; | |
| }; | |
| /** | |
| * Signal that a stream channel has been closed. | |
| * | |
| * Emitted when the worker calls `ctx.streams.close(channel)`, or | |
| * synthetically by the host when a worker process exits with channels | |
| * still open. UI clients should treat this as terminal and disconnect | |
| * the SSE connection. | |
| */ | |
| "streams.close": { | |
| channel: string; | |
| companyId: string; | |
| }; | |
| } | |
| /** Union of all worker→host notification method names. */ | |
| export type WorkerToHostNotificationName = keyof WorkerToHostNotifications; | |
| // --------------------------------------------------------------------------- | |
| // Typed Request / Response Helpers | |
| // --------------------------------------------------------------------------- | |
| /** | |
| * A typed JSON-RPC request for a specific host→worker method. | |
| */ | |
| export type HostToWorkerRequest<M extends HostToWorkerMethodName> = | |
| JsonRpcRequest<M, HostToWorkerMethods[M][0]>; | |
| /** | |
| * A typed JSON-RPC success response for a specific host→worker method. | |
| */ | |
| export type HostToWorkerResponse<M extends HostToWorkerMethodName> = | |
| JsonRpcSuccessResponse<HostToWorkerMethods[M][1]>; | |
| /** | |
| * A typed JSON-RPC request for a specific worker→host method. | |
| */ | |
| export type WorkerToHostRequest<M extends WorkerToHostMethodName> = | |
| JsonRpcRequest<M, WorkerToHostMethods[M][0]>; | |
| /** | |
| * A typed JSON-RPC success response for a specific worker→host method. | |
| */ | |
| export type WorkerToHostResponse<M extends WorkerToHostMethodName> = | |
| JsonRpcSuccessResponse<WorkerToHostMethods[M][1]>; | |
| // --------------------------------------------------------------------------- | |
| // Message Factory Functions | |
| // --------------------------------------------------------------------------- | |
| /** Counter for generating unique request IDs when no explicit ID is provided. */ | |
| let _nextId = 1; | |
| /** Wrap around before reaching Number.MAX_SAFE_INTEGER to prevent precision loss. */ | |
| const MAX_SAFE_RPC_ID = Number.MAX_SAFE_INTEGER - 1; | |
| /** | |
| * Create a JSON-RPC 2.0 request message. | |
| * | |
| * @param method - The RPC method name | |
| * @param params - Structured parameters | |
| * @param id - Optional explicit request ID (auto-generated if omitted) | |
| */ | |
| export function createRequest<TMethod extends string>( | |
| method: TMethod, | |
| params: unknown, | |
| id?: JsonRpcId, | |
| ): JsonRpcRequest<TMethod> { | |
| if (_nextId >= MAX_SAFE_RPC_ID) { | |
| _nextId = 1; | |
| } | |
| return { | |
| jsonrpc: JSONRPC_VERSION, | |
| id: id ?? _nextId++, | |
| method, | |
| params, | |
| }; | |
| } | |
| /** | |
| * Create a JSON-RPC 2.0 success response. | |
| * | |
| * @param id - The request ID being responded to | |
| * @param result - The result value | |
| */ | |
| export function createSuccessResponse<TResult>( | |
| id: JsonRpcId, | |
| result: TResult, | |
| ): JsonRpcSuccessResponse<TResult> { | |
| return { | |
| jsonrpc: JSONRPC_VERSION, | |
| id, | |
| result, | |
| }; | |
| } | |
| /** | |
| * Create a JSON-RPC 2.0 error response. | |
| * | |
| * @param id - The request ID being responded to (null if the request ID could not be determined) | |
| * @param code - Machine-readable error code | |
| * @param message - Human-readable error message | |
| * @param data - Optional structured error data | |
| */ | |
| export function createErrorResponse<TData = unknown>( | |
| id: JsonRpcId | null, | |
| code: number, | |
| message: string, | |
| data?: TData, | |
| ): JsonRpcErrorResponse<TData> { | |
| const response: JsonRpcErrorResponse<TData> = { | |
| jsonrpc: JSONRPC_VERSION, | |
| id, | |
| error: data !== undefined | |
| ? { code, message, data } | |
| : { code, message } as JsonRpcError<TData>, | |
| }; | |
| return response; | |
| } | |
| /** | |
| * Create a JSON-RPC 2.0 notification (fire-and-forget, no response expected). | |
| * | |
| * @param method - The notification method name | |
| * @param params - Structured parameters | |
| */ | |
| export function createNotification<TMethod extends string>( | |
| method: TMethod, | |
| params: unknown, | |
| ): JsonRpcNotification<TMethod> { | |
| return { | |
| jsonrpc: JSONRPC_VERSION, | |
| method, | |
| params, | |
| }; | |
| } | |
| // --------------------------------------------------------------------------- | |
| // Type Guards | |
| // --------------------------------------------------------------------------- | |
| /** | |
| * Check whether a value is a well-formed JSON-RPC 2.0 request. | |
| * | |
| * A request has `jsonrpc: "2.0"`, a string `method`, and an `id`. | |
| */ | |
| export function isJsonRpcRequest(value: unknown): value is JsonRpcRequest { | |
| if (typeof value !== "object" || value === null) return false; | |
| const obj = value as Record<string, unknown>; | |
| return ( | |
| obj.jsonrpc === JSONRPC_VERSION && | |
| typeof obj.method === "string" && | |
| "id" in obj && | |
| obj.id !== undefined && | |
| obj.id !== null | |
| ); | |
| } | |
| /** | |
| * Check whether a value is a well-formed JSON-RPC 2.0 notification. | |
| * | |
| * A notification has `jsonrpc: "2.0"`, a string `method`, but no `id`. | |
| */ | |
| export function isJsonRpcNotification( | |
| value: unknown, | |
| ): value is JsonRpcNotification { | |
| if (typeof value !== "object" || value === null) return false; | |
| const obj = value as Record<string, unknown>; | |
| return ( | |
| obj.jsonrpc === JSONRPC_VERSION && | |
| typeof obj.method === "string" && | |
| !("id" in obj) | |
| ); | |
| } | |
| /** | |
| * Check whether a value is a well-formed JSON-RPC 2.0 response (success or error). | |
| */ | |
| export function isJsonRpcResponse(value: unknown): value is JsonRpcResponse { | |
| if (typeof value !== "object" || value === null) return false; | |
| const obj = value as Record<string, unknown>; | |
| return ( | |
| obj.jsonrpc === JSONRPC_VERSION && | |
| "id" in obj && | |
| ("result" in obj || "error" in obj) | |
| ); | |
| } | |
| /** | |
| * Check whether a JSON-RPC response is a success response. | |
| */ | |
| export function isJsonRpcSuccessResponse( | |
| response: JsonRpcResponse, | |
| ): response is JsonRpcSuccessResponse { | |
| return "result" in response && !("error" in response && response.error !== undefined); | |
| } | |
| /** | |
| * Check whether a JSON-RPC response is an error response. | |
| */ | |
| export function isJsonRpcErrorResponse( | |
| response: JsonRpcResponse, | |
| ): response is JsonRpcErrorResponse { | |
| return "error" in response && response.error !== undefined; | |
| } | |
| // --------------------------------------------------------------------------- | |
| // Serialization Helpers | |
| // --------------------------------------------------------------------------- | |
| /** | |
| * Line delimiter for JSON-RPC messages over stdio. | |
| * | |
| * Each message is a single line of JSON terminated by a newline character. | |
| * This follows the newline-delimited JSON (NDJSON) convention. | |
| */ | |
| export const MESSAGE_DELIMITER = "\n" as const; | |
| /** | |
| * Serialize a JSON-RPC message to a newline-delimited string for transmission | |
| * over stdio. | |
| * | |
| * @param message - Any JSON-RPC message (request, response, or notification) | |
| * @returns The JSON string terminated with a newline | |
| */ | |
| export function serializeMessage(message: JsonRpcMessage): string { | |
| return JSON.stringify(message) + MESSAGE_DELIMITER; | |
| } | |
| /** | |
| * Parse a JSON string into a JSON-RPC message. | |
| * | |
| * Returns the parsed message or throws a `JsonRpcParseError` if the input | |
| * is not valid JSON or does not conform to the JSON-RPC 2.0 structure. | |
| * | |
| * @param line - A single line of JSON text (with or without trailing newline) | |
| * @returns The parsed JSON-RPC message | |
| * @throws {JsonRpcParseError} If parsing fails | |
| */ | |
| export function parseMessage(line: string): JsonRpcMessage { | |
| const trimmed = line.trim(); | |
| if (trimmed.length === 0) { | |
| throw new JsonRpcParseError("Empty message"); | |
| } | |
| let parsed: unknown; | |
| try { | |
| parsed = JSON.parse(trimmed); | |
| } catch { | |
| throw new JsonRpcParseError(`Invalid JSON: ${trimmed.slice(0, 200)}`); | |
| } | |
| if (typeof parsed !== "object" || parsed === null) { | |
| throw new JsonRpcParseError("Message must be a JSON object"); | |
| } | |
| const obj = parsed as Record<string, unknown>; | |
| if (obj.jsonrpc !== JSONRPC_VERSION) { | |
| throw new JsonRpcParseError( | |
| `Invalid or missing jsonrpc version (expected "${JSONRPC_VERSION}", got ${JSON.stringify(obj.jsonrpc)})`, | |
| ); | |
| } | |
| // It's a valid JSON-RPC 2.0 envelope — return as-is and let the caller | |
| // use the type guards for more specific classification. | |
| return parsed as JsonRpcMessage; | |
| } | |
| // --------------------------------------------------------------------------- | |
| // Error Classes | |
| // --------------------------------------------------------------------------- | |
| /** | |
| * Error thrown when a JSON-RPC message cannot be parsed. | |
| */ | |
| export class JsonRpcParseError extends Error { | |
| override readonly name = "JsonRpcParseError"; | |
| constructor(message: string) { | |
| super(message); | |
| } | |
| } | |
| /** | |
| * Error thrown when a JSON-RPC call fails with a structured error response. | |
| * | |
| * Captures the full `JsonRpcError` so callers can inspect the code and data. | |
| */ | |
| export class JsonRpcCallError extends Error { | |
| override readonly name = "JsonRpcCallError"; | |
| /** The JSON-RPC error code. */ | |
| readonly code: number; | |
| /** Optional structured error data from the response. */ | |
| readonly data: unknown; | |
| constructor(error: JsonRpcError) { | |
| super(error.message); | |
| this.code = error.code; | |
| this.data = error.data; | |
| } | |
| } | |
| // --------------------------------------------------------------------------- | |
| // Reset helper (testing only) | |
| // --------------------------------------------------------------------------- | |
| /** | |
| * Reset the internal request ID counter. **For testing only.** | |
| * | |
| * @internal | |
| */ | |
| export function _resetIdCounter(): void { | |
| _nextId = 1; | |
| } | |