Initial commit: Paperclip plugin for Legal AI integration

16 agent tools, event handler for auto-linking, sync job every 15m.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-04 08:03:43 +00:00
commit 587a2a76ca
1183 changed files with 629235 additions and 0 deletions

21
node_modules/@paperclipai/plugin-sdk/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Paperclip AI
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

888
node_modules/@paperclipai/plugin-sdk/README.md generated vendored Normal file
View File

@@ -0,0 +1,888 @@
# `@paperclipai/plugin-sdk`
Official TypeScript SDK for Paperclip plugin authors.
- **Worker SDK:** `@paperclipai/plugin-sdk``definePlugin`, context, lifecycle
- **UI SDK:** `@paperclipai/plugin-sdk/ui` — React hooks and slot props
- **Testing:** `@paperclipai/plugin-sdk/testing` — in-memory host harness
- **Bundlers:** `@paperclipai/plugin-sdk/bundlers` — esbuild/rollup presets
- **Dev server:** `@paperclipai/plugin-sdk/dev-server` — static UI server + SSE reload
Reference: `doc/plugins/PLUGIN_SPEC.md`
## Package surface
| Import | Purpose |
|--------|--------|
| `@paperclipai/plugin-sdk` | Worker entry: `definePlugin`, `runWorker`, context types, protocol helpers |
| `@paperclipai/plugin-sdk/ui` | UI entry: `usePluginData`, `usePluginAction`, `usePluginStream`, `useHostContext`, slot prop types |
| `@paperclipai/plugin-sdk/ui/hooks` | Hooks only |
| `@paperclipai/plugin-sdk/ui/types` | UI types and slot prop interfaces |
| `@paperclipai/plugin-sdk/testing` | `createTestHarness` for unit/integration tests |
| `@paperclipai/plugin-sdk/bundlers` | `createPluginBundlerPresets` for worker/manifest/ui builds |
| `@paperclipai/plugin-sdk/dev-server` | `startPluginDevServer`, `getUiBuildSnapshot` |
| `@paperclipai/plugin-sdk/protocol` | JSON-RPC protocol types and helpers (advanced) |
| `@paperclipai/plugin-sdk/types` | Worker context and API types (advanced) |
## Manifest entrypoints
In your plugin manifest you declare:
- **`entrypoints.worker`** (required) — Path to the worker bundle (e.g. `dist/worker.js`). The host loads this and calls `setup(ctx)`.
- **`entrypoints.ui`** (required if you use UI) — Path to the UI bundle directory. The host loads components from here for slots and launchers.
## Install
```bash
pnpm add @paperclipai/plugin-sdk
```
## Current deployment caveats
The SDK is stable enough for local development and first-party examples, but the runtime deployment model is still early.
- Plugin workers and plugin UI should both be treated as trusted code today.
- Plugin UI bundles run as same-origin JavaScript inside the main Paperclip app. They can call ordinary Paperclip HTTP APIs with the board session, so manifest capabilities are not a frontend sandbox.
- Local-path installs and the repo example plugins are development workflows. They assume the plugin source checkout exists on disk.
- For deployed plugins, publish an npm package and install that package into the Paperclip instance at runtime.
- The current host runtime expects a writable filesystem, `npm` available at runtime, and network access to the package registry used for plugin installation.
- Dynamic plugin install is currently best suited to single-node persistent deployments. Multi-instance cloud deployments still need a shared artifact/distribution model before runtime installs are reliable across nodes.
- The host does not currently ship a real shared React component kit for plugins. Build your plugin UI with ordinary React components and CSS.
- `ctx.assets` is not part of the supported runtime in this build. Do not depend on asset upload/read APIs yet.
If you are authoring a plugin for others to deploy, treat npm-packaged installation as the supported path and treat repo-local example installs as a development convenience.
## Worker quick start
```ts
import { definePlugin, runWorker } from "@paperclipai/plugin-sdk";
const plugin = definePlugin({
async setup(ctx) {
ctx.events.on("issue.created", async (event) => {
ctx.logger.info("Issue created", { issueId: event.entityId });
});
ctx.data.register("health", async () => ({ status: "ok" }));
ctx.actions.register("ping", async () => ({ pong: true }));
ctx.tools.register("calculator", {
displayName: "Calculator",
description: "Basic math",
parametersSchema: {
type: "object",
properties: { a: { type: "number" }, b: { type: "number" } },
required: ["a", "b"]
}
}, async (params) => {
const { a, b } = params as { a: number; b: number };
return { content: `Result: ${a + b}`, data: { result: a + b } };
});
},
});
export default plugin;
runWorker(plugin, import.meta.url);
```
**Note:** `runWorker(plugin, import.meta.url)` must be called so that when the host runs your worker (e.g. `node dist/worker.js`), the RPC host starts and the process stays alive. When the file is imported (e.g. for tests), the main-module check prevents the host from starting.
### Worker lifecycle and context
**Lifecycle (definePlugin):**
| Hook | Purpose |
|------|--------|
| `setup(ctx)` | **Required.** Called once at startup. Register event handlers, jobs, data/actions/tools, etc. |
| `onHealth?()` | Optional. Return `{ status, message?, details? }` for health dashboard. |
| `onConfigChanged?(newConfig)` | Optional. Apply new config without restart; if omitted, host restarts worker. |
| `onShutdown?()` | Optional. Clean up before process exit (limited time window). |
| `onValidateConfig?(config)` | Optional. Return `{ ok, warnings?, errors? }` for settings UI / Test Connection. |
| `onWebhook?(input)` | Optional. Handle `POST /api/plugins/:pluginId/webhooks/:endpointKey`; required if webhooks declared. |
**Context (`ctx`) in setup:** `config`, `events`, `jobs`, `launchers`, `http`, `secrets`, `activity`, `state`, `entities`, `projects`, `companies`, `issues`, `agents`, `goals`, `data`, `actions`, `streams`, `tools`, `metrics`, `logger`, `manifest`. Worker-side host APIs are capability-gated; declare capabilities in the manifest.
**Agents:** `ctx.agents.invoke(agentId, companyId, opts)` for one-shot invocation. `ctx.agents.sessions` for two-way chat: `create`, `list`, `sendMessage` (with streaming `onEvent` callback), `close`. See the [Plugin Authoring Guide](../../doc/plugins/PLUGIN_AUTHORING_GUIDE.md#agent-sessions-two-way-chat) for details.
**Jobs:** Declare in `manifest.jobs` with `jobKey`, `displayName`, `schedule` (cron). Register handler with `ctx.jobs.register(jobKey, fn)`. **Webhooks:** Declare in `manifest.webhooks` with `endpointKey`; handle in `onWebhook(input)`. **State:** `ctx.state.get/set/delete(scopeKey)`; scope kinds: `instance`, `company`, `project`, `project_workspace`, `agent`, `issue`, `goal`, `run`.
## Events
Subscribe in `setup` with `ctx.events.on(name, handler)` or `ctx.events.on(name, filter, handler)`. Emit plugin-scoped events with `ctx.events.emit(name, companyId, payload)` (requires `events.emit`).
**Core domain events (subscribe with `events.subscribe`):**
| Event | Typical entity |
|-------|-----------------|
| `company.created`, `company.updated` | company |
| `project.created`, `project.updated` | project |
| `project.workspace_created`, `project.workspace_updated`, `project.workspace_deleted` | project_workspace |
| `issue.created`, `issue.updated`, `issue.comment.created` | issue |
| `agent.created`, `agent.updated`, `agent.status_changed` | agent |
| `agent.run.started`, `agent.run.finished`, `agent.run.failed`, `agent.run.cancelled` | run |
| `goal.created`, `goal.updated` | goal |
| `approval.created`, `approval.decided` | approval |
| `cost_event.created` | cost |
| `activity.logged` | activity |
**Plugin-to-plugin:** Subscribe to `plugin.<pluginId>.<eventName>` (e.g. `plugin.acme.linear.sync-done`). Emit with `ctx.events.emit("sync-done", companyId, payload)`; the host namespaces it automatically.
**Filter (optional):** Pass a second argument to `on()`: `{ projectId?, companyId?, agentId? }` so the host only delivers matching events.
**Company context:** Events still carry `companyId` for company-scoped data, but plugin installation and activation are instance-wide in the current runtime.
## Scheduled (recurring) jobs
Plugins can declare **scheduled jobs** that the host runs on a cron schedule. Use this for recurring tasks like syncs, digest reports, or cleanup.
1. **Capability:** Add `jobs.schedule` to `manifest.capabilities`.
2. **Declare jobs** in `manifest.jobs`: each entry has `jobKey`, `displayName`, optional `description`, and `schedule` (a 5-field cron expression).
3. **Register a handler** in `setup()` with `ctx.jobs.register(jobKey, async (job) => { ... })`.
**Cron format** (5 fields: minute, hour, day-of-month, month, day-of-week):
| Field | Values | Example |
|-------------|----------|---------|
| minute | 059 | `0`, `*/15` |
| hour | 023 | `2`, `*` |
| day of month | 131 | `1`, `*` |
| month | 112 | `*` |
| day of week | 06 (Sun=0) | `*`, `1-5` |
Examples: `"0 * * * *"` = every hour at minute 0; `"*/5 * * * *"` = every 5 minutes; `"0 2 * * *"` = daily at 2:00.
**Job handler context** (`PluginJobContext`):
| Field | Type | Description |
|-------------|----------|-------------|
| `jobKey` | string | Matches the manifest declaration. |
| `runId` | string | UUID for this run. |
| `trigger` | `"schedule" \| "manual" \| "retry"` | What caused this run. |
| `scheduledAt` | string | ISO 8601 time when the run was scheduled. |
Runs can be triggered by the **schedule**, **manually** from the UI/API, or as a **retry** (when an operator re-runs a job after a failure). Re-throw from the handler to mark the run as failed; the host records the failure. The host does not automatically retry—operators can trigger another run manually from the UI or API.
Example:
**Manifest** — include `jobs.schedule` and declare the job:
```ts
// In your manifest (e.g. manifest.ts):
const manifest = {
// ...
capabilities: ["jobs.schedule", "plugin.state.write"],
jobs: [
{
jobKey: "heartbeat",
displayName: "Heartbeat",
description: "Runs every 5 minutes",
schedule: "*/5 * * * *",
},
],
// ...
};
```
**Worker** — register the handler in `setup()`:
```ts
ctx.jobs.register("heartbeat", async (job) => {
ctx.logger.info("Heartbeat run", { runId: job.runId, trigger: job.trigger });
await ctx.state.set({ scopeKind: "instance", stateKey: "last-heartbeat" }, new Date().toISOString());
});
```
## UI slots and launchers
Slots are mount points for plugin React components. Launchers are host-rendered entry points (buttons, menu items) that open plugin UI. Declare slots in `manifest.ui.slots` with `type`, `id`, `displayName`, `exportName`; for context-sensitive slots add `entityTypes`. Declare launchers in `manifest.ui.launchers` (or legacy `manifest.launchers`).
### Slot types / launcher placement zones
The same set of values is used as **slot types** (where a component mounts) and **launcher placement zones** (where a launcher can appear). Hierarchy:
| Slot type / placement zone | Scope | Entity types (when context-sensitive) |
|----------------------------|-------|---------------------------------------|
| `page` | Global | — |
| `sidebar` | Global | — |
| `sidebarPanel` | Global | — |
| `settingsPage` | Global | — |
| `dashboardWidget` | Global | — |
| `globalToolbarButton` | Global | — |
| `detailTab` | Entity | `project`, `issue`, `agent`, `goal`, `run` |
| `taskDetailView` | Entity | (task/issue context) |
| `commentAnnotation` | Entity | `comment` |
| `commentContextMenuItem` | Entity | `comment` |
| `projectSidebarItem` | Entity | `project` |
| `toolbarButton` | Entity | varies by host surface |
| `contextMenuItem` | Entity | varies by host surface |
**Scope** describes whether the slot requires an entity to render. **Global** slots render without a specific entity but still receive the active `companyId` through `PluginHostContext` — use it to scope data fetches to the current company. **Entity** slots additionally require `entityId` and `entityType` (e.g. a detail tab on a specific issue).
**Entity types** (for `entityTypes` on slots): `project` \| `issue` \| `agent` \| `goal` \| `run` \| `comment`. Full list: import `PLUGIN_UI_SLOT_TYPES` and `PLUGIN_UI_SLOT_ENTITY_TYPES` from `@paperclipai/plugin-sdk`.
### Slot component descriptions
#### `page`
A full-page extension mounted at `/plugins/:pluginId` (global) or `/:company/plugins/:pluginId` (company-context route). Use this for rich, standalone plugin experiences such as dashboards, configuration wizards, or multi-step workflows. Receives `PluginPageProps` with `context.companyId` set to the active company. Requires the `ui.page.register` capability.
#### `sidebar`
Adds a navigation-style entry to the main company sidebar navigation area, rendered alongside the core nav items (Dashboard, Issues, Goals, etc.). Use this for lightweight, always-visible links or status indicators that feel native to the sidebar. Receives `PluginSidebarProps` with `context.companyId` set to the active company. Requires the `ui.sidebar.register` capability.
#### `sidebarPanel`
Renders richer inline content in a dedicated panel area below the company sidebar navigation sections. Use this for mini-widgets, summary cards, quick-action panels, or at-a-glance status views that need more vertical space than a nav link. Receives `context.companyId` set to the active company via `useHostContext()`. Requires the `ui.sidebar.register` capability.
#### `settingsPage`
Replaces the auto-generated JSON Schema settings form with a custom React component. Use this when the default form is insufficient — for example, when your plugin needs multi-step configuration, OAuth flows, "Test Connection" buttons, or rich input controls. Receives `PluginSettingsPageProps` with `context.companyId` set to the active company. The component is responsible for reading and writing config through the bridge (via `usePluginData` and `usePluginAction`).
#### `dashboardWidget`
A card or section rendered on the main dashboard. Use this for at-a-glance metrics, status indicators, or summary views that surface plugin data alongside core Paperclip information. Receives `PluginWidgetProps` with `context.companyId` set to the active company. Requires the `ui.dashboardWidget.register` capability.
#### `detailTab`
An additional tab on a project, issue, agent, goal, or run detail page. Rendered when the user navigates to that entity's detail view. Receives `PluginDetailTabProps` with `context.companyId` set to the active company and `context.entityId` / `context.entityType` guaranteed to be non-null, so you can immediately scope data fetches to the relevant entity. Specify which entity types the tab applies to via the `entityTypes` array in the manifest slot declaration. Requires the `ui.detailTab.register` capability.
#### `taskDetailView`
A specialized slot rendered in the context of a task or issue detail view. Similar to `detailTab` but designed for inline content within the task detail layout rather than a separate tab. Receives `context.companyId`, `context.entityId`, and `context.entityType` like `detailTab`. Requires the `ui.detailTab.register` capability.
#### `projectSidebarItem`
A link or small component rendered **once per project** under that project's row in the sidebar Projects list. Use this to add project-scoped navigation entries (e.g. "Files", "Linear Sync") that deep-link into a plugin detail tab: `/:company/projects/:projectRef?tab=plugin:<key>:<slotId>`. Receives `PluginProjectSidebarItemProps` with `context.companyId` set to the active company, `context.entityId` set to the project id, and `context.entityType` set to `"project"`. Use the optional `order` field in the manifest slot to control sort position. Requires the `ui.sidebar.register` capability.
#### `globalToolbarButton`
A button rendered in the global top bar (breadcrumb bar) that appears on every page. Use this for company-wide actions that are not scoped to a specific entity — for example, a universal search trigger, a global sync status indicator, or a floating action that applies across the whole workspace. Receives only `context.companyId` and `context.companyPrefix`; no entity context is available. Requires the `ui.action.register` capability.
#### `toolbarButton`
A button rendered in the toolbar of an entity page (e.g. project detail, issue detail). Use this for short-lived, contextual actions scoped to the current entity — like triggering a project sync, opening a picker, or running a quick command on that entity. The component can open a plugin-owned modal internally for confirmations or compact forms. Receives `context.companyId`, `context.entityId`, and `context.entityType`; declare `entityTypes` in the manifest to control which entity pages the button appears on. Requires the `ui.action.register` capability.
#### `contextMenuItem`
An entry added to a right-click or overflow context menu on a host surface. Use this for secondary actions that apply to the entity under the cursor (e.g. "Copy to Linear", "Re-run analysis"). Receives `context.companyId` set to the active company; entity context varies by host surface. Requires the `ui.action.register` capability.
#### `commentAnnotation`
A per-comment annotation region rendered below each individual comment in the issue detail timeline. Use this to augment comments with parsed file links, sentiment badges, inline actions, or any per-comment metadata. Receives `PluginCommentAnnotationProps` with `context.entityId` set to the comment UUID, `context.entityType` set to `"comment"`, `context.parentEntityId` set to the parent issue UUID, `context.projectId` set to the issue's project (if any), and `context.companyPrefix` set to the active company slug. Requires the `ui.commentAnnotation.register` capability.
#### `commentContextMenuItem`
A per-comment context menu item rendered in the "more" dropdown menu (⋮) on each comment in the issue detail timeline. Use this to add per-comment actions such as "Create sub-issue from comment", "Translate", "Flag for review", or custom plugin actions. Receives `PluginCommentContextMenuItemProps` with `context.entityId` set to the comment UUID, `context.entityType` set to `"comment"`, `context.parentEntityId` set to the parent issue UUID, `context.projectId` set to the issue's project (if any), and `context.companyPrefix` set to the active company slug. Plugins can open drawers, modals, or popovers scoped to that comment. The ⋮ menu button only appears on comments where at least one plugin renders visible content. Requires the `ui.action.register` capability.
### Launcher actions and render options
| Launcher action | Description |
|-----------------|-------------|
| `navigate` | Navigate to a route (plugin or host). |
| `openModal` | Open a modal. |
| `openDrawer` | Open a drawer. |
| `openPopover` | Open a popover. |
| `performAction` | Run an action (e.g. call plugin). |
| `deepLink` | Deep link to plugin or external URL. |
| Render option | Values | Description |
|---------------|--------|-------------|
| `environment` | `hostInline`, `hostOverlay`, `hostRoute`, `external`, `iframe` | Container the launcher expects after activation. |
| `bounds` | `inline`, `compact`, `default`, `wide`, `full` | Size hint for overlays/drawers. |
### Capabilities
Declare in `manifest.capabilities`. Grouped by scope:
| Scope | Capability |
|-------|------------|
| **Company** | `companies.read` |
| | `projects.read` |
| | `project.workspaces.read` |
| | `issues.read` |
| | `issue.comments.read` |
| | `agents.read` |
| | `goals.read` |
| | `goals.create` |
| | `goals.update` |
| | `activity.read` |
| | `costs.read` |
| | `issues.create` |
| | `issues.update` |
| | `issue.comments.create` |
| | `activity.log.write` |
| | `metrics.write` |
| **Instance** | `instance.settings.register` |
| | `plugin.state.read` |
| | `plugin.state.write` |
| **Runtime** | `events.subscribe` |
| | `events.emit` |
| | `jobs.schedule` |
| | `webhooks.receive` |
| | `http.outbound` |
| | `secrets.read-ref` |
| **Agent** | `agent.tools.register` |
| | `agents.invoke` |
| | `agent.sessions.create` |
| | `agent.sessions.list` |
| | `agent.sessions.send` |
| | `agent.sessions.close` |
| **UI** | `ui.sidebar.register` |
| | `ui.page.register` |
| | `ui.detailTab.register` |
| | `ui.dashboardWidget.register` |
| | `ui.commentAnnotation.register` |
| | `ui.action.register` |
Full list in code: import `PLUGIN_CAPABILITIES` from `@paperclipai/plugin-sdk`.
## UI quick start
```tsx
import { usePluginData, usePluginAction } from "@paperclipai/plugin-sdk/ui";
export function DashboardWidget() {
const { data } = usePluginData<{ status: string }>("health");
const ping = usePluginAction("ping");
return (
<div style={{ display: "grid", gap: 8 }}>
<strong>Health</strong>
<div>{data?.status ?? "unknown"}</div>
<button onClick={() => void ping()}>Ping</button>
</div>
);
}
```
### Hooks reference
#### `usePluginData<T>(key, params?)`
Fetches data from the worker's registered `getData` handler. Re-fetches when `params` changes. Returns `{ data, loading, error, refresh }`.
```tsx
import { usePluginData } from "@paperclipai/plugin-sdk/ui";
interface SyncStatus {
lastSyncAt: string;
syncedCount: number;
healthy: boolean;
}
export function SyncStatusWidget({ context }: PluginWidgetProps) {
const { data, loading, error, refresh } = usePluginData<SyncStatus>("sync-status", {
companyId: context.companyId,
});
if (loading) return <div>Loading</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<p>Status: {data!.healthy ? "Healthy" : "Unhealthy"}</p>
<p>Synced {data!.syncedCount} items</p>
<p>Last sync: {data!.lastSyncAt}</p>
<button onClick={refresh}>Refresh</button>
</div>
);
}
```
#### `usePluginAction(key)`
Returns an async function that calls the worker's `performAction` handler. Throws `PluginBridgeError` on failure.
```tsx
import { useState } from "react";
import { usePluginAction, type PluginBridgeError } from "@paperclipai/plugin-sdk/ui";
export function ResyncButton({ context }: PluginWidgetProps) {
const resync = usePluginAction("resync");
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string | null>(null);
async function handleClick() {
setBusy(true);
setError(null);
try {
await resync({ companyId: context.companyId });
} catch (err) {
setError((err as PluginBridgeError).message);
} finally {
setBusy(false);
}
}
return (
<div>
<button onClick={handleClick} disabled={busy}>
{busy ? "Syncing..." : "Resync Now"}
</button>
{error && <p style={{ color: "red" }}>{error}</p>}
</div>
);
}
```
#### `useHostContext()`
Reads the active company, project, entity, and user context. Use this to scope data fetches and actions.
```tsx
import { useHostContext, usePluginData } from "@paperclipai/plugin-sdk/ui";
import type { PluginDetailTabProps } from "@paperclipai/plugin-sdk/ui";
export function IssueLinearLink({ context }: PluginDetailTabProps) {
const { companyId, entityId, entityType } = context;
const { data } = usePluginData<{ url: string }>("linear-link", {
companyId,
issueId: entityId,
});
if (!data?.url) return <p>No linked Linear issue.</p>;
return <a href={data.url} target="_blank" rel="noopener">View in Linear</a>;
}
```
#### `usePluginStream<T>(channel, options?)`
Subscribes to a real-time event stream pushed from the plugin worker via SSE. The worker pushes events using `ctx.streams.emit(channel, event)` and the hook receives them as they arrive. Returns `{ events, lastEvent, connecting, connected, error, close }`.
```tsx
import { usePluginStream } from "@paperclipai/plugin-sdk/ui";
interface ChatToken {
text: string;
}
export function ChatMessages({ context }: PluginWidgetProps) {
const { events, connected, close } = usePluginStream<ChatToken>("chat-stream", {
companyId: context.companyId ?? undefined,
});
return (
<div>
{events.map((e, i) => <span key={i}>{e.text}</span>)}
{connected && <span className="pulse" />}
<button onClick={close}>Stop</button>
</div>
);
}
```
The SSE connection targets `GET /api/plugins/:pluginId/bridge/stream/:channel?companyId=...`. The host bridge manages the EventSource lifecycle; `close()` terminates the connection.
### UI authoring note
The current host does **not** provide a real shared component library to plugins yet. Use normal React components, your own CSS, or your own small design primitives inside the plugin package.
### Slot component props
Each slot type receives a typed props object with `context: PluginHostContext`. Import from `@paperclipai/plugin-sdk/ui`.
| Slot type | Props interface | `context` extras |
|-----------|----------------|------------------|
| `page` | `PluginPageProps` | — |
| `sidebar` | `PluginSidebarProps` | — |
| `settingsPage` | `PluginSettingsPageProps` | — |
| `dashboardWidget` | `PluginWidgetProps` | — |
| `globalToolbarButton` | `PluginGlobalToolbarButtonProps` | — |
| `detailTab` | `PluginDetailTabProps` | `entityId: string`, `entityType: string` |
| `toolbarButton` | `PluginToolbarButtonProps` | `entityId: string`, `entityType: string` |
| `commentAnnotation` | `PluginCommentAnnotationProps` | `entityId: string`, `entityType: "comment"`, `parentEntityId: string`, `projectId`, `companyPrefix` |
| `commentContextMenuItem` | `PluginCommentContextMenuItemProps` | `entityId: string`, `entityType: "comment"`, `parentEntityId: string`, `projectId`, `companyPrefix` |
| `projectSidebarItem` | `PluginProjectSidebarItemProps` | `entityId: string`, `entityType: "project"` |
Example detail tab with entity context:
```tsx
import type { PluginDetailTabProps } from "@paperclipai/plugin-sdk/ui";
import { usePluginData } from "@paperclipai/plugin-sdk/ui";
export function AgentMetricsTab({ context }: PluginDetailTabProps) {
const { data, loading } = usePluginData<Record<string, string>>("agent-metrics", {
agentId: context.entityId,
companyId: context.companyId,
});
if (loading) return <div>Loading</div>;
if (!data) return <p>No metrics available.</p>;
return (
<dl>
{Object.entries(data).map(([label, value]) => (
<div key={label}>
<dt>{label}</dt>
<dd>{value}</dd>
</div>
))}
</dl>
);
}
```
## Launcher surfaces and modals
V1 does not provide a dedicated `modal` slot. Plugins can either:
- declare concrete UI mount points in `ui.slots`
- declare host-rendered entry points in `ui.launchers`
Supported launcher placement zones currently mirror the major host surfaces such as `projectSidebarItem`, `globalToolbarButton`, `toolbarButton`, `detailTab`, `settingsPage`, and `contextMenuItem`. Plugins may still open their own local modal from those entry points when needed.
Declarative launcher example:
```json
{
"ui": {
"launchers": [
{
"id": "sync-project",
"displayName": "Sync",
"placementZone": "toolbarButton",
"entityTypes": ["project"],
"action": {
"type": "openDrawer",
"target": "sync-project"
},
"render": {
"environment": "hostOverlay",
"bounds": "wide"
}
}
]
}
}
```
The host returns launcher metadata from `GET /api/plugins/ui-contributions` alongside slot declarations.
When a launcher opens a host-owned overlay or page, `useHostContext()`,
`usePluginData()`, and `usePluginAction()` receive the current
`renderEnvironment` through the bridge. Use that to tailor compact modal UI vs.
full-page layouts without adding custom route parsing in the plugin.
## Project sidebar item
Plugins can add a link under each project in the sidebar via the `projectSidebarItem` slot. This is the recommended slot-based launcher pattern for project-scoped workflows because it can deep-link into a richer plugin tab. The component is rendered once per project with that projects id in `context.entityId`. Declare the slot and capability in your manifest:
```json
{
"ui": {
"slots": [
{
"type": "projectSidebarItem",
"id": "files",
"displayName": "Files",
"exportName": "FilesLink",
"entityTypes": ["project"]
}
]
},
"capabilities": ["ui.sidebar.register", "ui.detailTab.register"]
}
```
Minimal React component that links to the projects plugin tab (see project detail tabs in the spec):
```tsx
import type { PluginProjectSidebarItemProps } from "@paperclipai/plugin-sdk/ui";
export function FilesLink({ context }: PluginProjectSidebarItemProps) {
const projectId = context.entityId;
const prefix = context.companyPrefix ? `/${context.companyPrefix}` : "";
const projectRef = projectId; // or resolve from host; entityId is project id
return (
<a href={`${prefix}/projects/${projectRef}?tab=plugin:your-plugin:files`}>
Files
</a>
);
}
```
Use optional `order` in the slot to sort among other project sidebar items. See §19.5.1 in the plugin spec and project detail plugin tabs (§19.3) for the full flow.
## Toolbar launcher with a local modal
Two toolbar slot types are available depending on where the button should appear:
- **`globalToolbarButton`** — renders in the top bar on every page, scoped to the company. No entity context. Use for workspace-wide actions.
- **`toolbarButton`** — renders on entity detail pages (project, issue, etc.). Receives `entityId` and `entityType`. Declare `entityTypes` to control which pages the button appears on.
For short-lived actions, mount the appropriate slot type and open a plugin-owned modal inside the component. Use `useHostContext()` to scope the action to the current company or entity.
Project-scoped example (appears only on project detail pages):
```json
{
"ui": {
"slots": [
{
"type": "toolbarButton",
"id": "sync-toolbar-button",
"displayName": "Sync",
"exportName": "SyncToolbarButton",
"entityTypes": ["project"]
}
]
},
"capabilities": ["ui.action.register"]
}
```
```tsx
import { useState } from "react";
import {
useHostContext,
usePluginAction,
} from "@paperclipai/plugin-sdk/ui";
export function SyncToolbarButton() {
const context = useHostContext();
const syncProject = usePluginAction("sync-project");
const [open, setOpen] = useState(false);
const [submitting, setSubmitting] = useState(false);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
async function confirm() {
if (!context.projectId) return;
setSubmitting(true);
setErrorMessage(null);
try {
await syncProject({ projectId: context.projectId });
setOpen(false);
} catch (err) {
setErrorMessage(err instanceof Error ? err.message : "Sync failed");
} finally {
setSubmitting(false);
}
}
return (
<>
<button type="button" onClick={() => setOpen(true)}>
Sync
</button>
{open ? (
<div
role="dialog"
aria-modal="true"
className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4"
onClick={() => !submitting && setOpen(false)}
>
<div
className="w-full max-w-md rounded-lg bg-background p-4 shadow-xl"
onClick={(event) => event.stopPropagation()}
>
<h2 className="text-base font-semibold">Sync this project?</h2>
<p className="mt-2 text-sm text-muted-foreground">
Queue a sync for <code>{context.projectId}</code>.
</p>
{errorMessage ? (
<p className="mt-2 text-sm text-destructive">{errorMessage}</p>
) : null}
<div className="mt-4 flex justify-end gap-2">
<button type="button" onClick={() => setOpen(false)}>
Cancel
</button>
<button type="button" onClick={() => void confirm()} disabled={submitting}>
{submitting ? "Running…" : "Run sync"}
</button>
</div>
</div>
</div>
) : null}
</>
);
}
```
Prefer deep-linkable tabs and pages for primary workflows. Reserve plugin-owned modals for confirmations, pickers, and compact editors.
## Real-time streaming (`ctx.streams`)
Plugins can push real-time events from the worker to the UI using server-sent events (SSE). This is useful for streaming LLM tokens, live sync progress, or any push-based data.
### Worker side
In `setup()`, use `ctx.streams` to open a channel, emit events, and close when done:
```ts
const plugin = definePlugin({
async setup(ctx) {
ctx.actions.register("chat", async (params) => {
const companyId = params.companyId as string;
ctx.streams.open("chat-stream", companyId);
for await (const token of streamFromLLM(params.prompt as string)) {
ctx.streams.emit("chat-stream", { text: token });
}
ctx.streams.close("chat-stream");
return { ok: true };
});
},
});
```
**API:**
| Method | Description |
|--------|-------------|
| `ctx.streams.open(channel, companyId)` | Open a named stream channel and associate it with a company. Sends a `streams.open` notification to the host. |
| `ctx.streams.emit(channel, event)` | Push an event to the channel. The `companyId` is automatically resolved from the prior `open()` call. |
| `ctx.streams.close(channel)` | Close the channel and clear the company mapping. Sends a `streams.close` notification. |
Stream notifications are fire-and-forget JSON-RPC messages (no `id` field). They are sent via `notifyHost()` synchronously during handler execution.
### UI side
Use the `usePluginStream` hook (see [Hooks reference](#usepluginstreamtchannel-options) above) to subscribe to events from the UI.
### Host-side architecture
The host maintains an in-memory `PluginStreamBus` that fans out worker notifications to connected SSE clients:
1. Worker emits `streams.emit` notification via stdout
2. Host (`plugin-worker-manager`) receives the notification and publishes to `PluginStreamBus`
3. SSE endpoint (`GET /api/plugins/:pluginId/bridge/stream/:channel?companyId=...`) subscribes to the bus and writes events to the response
The bus is keyed by `pluginId:channel:companyId`, so multiple UI clients can subscribe to the same stream independently.
### Streaming agent responses to the UI
`ctx.streams` and `ctx.agents.sessions` are complementary. The worker sits between them, relaying agent events to the browser in real time:
```
UI ──usePluginAction──▶ Worker ──sessions.sendMessage──▶ Agent
UI ◀──usePluginStream── Worker ◀──onEvent callback────── Agent
```
The agent doesn't know about streams — the worker decides what to relay. Encode the agent ID in the channel name to scope streams per agent.
**Worker:**
```ts
ctx.actions.register("ask-agent", async (params) => {
const { agentId, companyId, prompt } = params as {
agentId: string; companyId: string; prompt: string;
};
const channel = `agent:${agentId}`;
ctx.streams.open(channel, companyId);
const session = await ctx.agents.sessions.create(agentId, companyId);
await ctx.agents.sessions.sendMessage(session.sessionId, companyId, {
prompt,
onEvent: (event) => {
ctx.streams.emit(channel, {
type: event.eventType, // "chunk" | "done" | "error"
text: event.message ?? "",
});
},
});
ctx.streams.close(channel);
return { sessionId: session.sessionId };
});
```
**UI:**
```tsx
import { useState } from "react";
import { usePluginAction, usePluginStream } from "@paperclipai/plugin-sdk/ui";
interface AgentEvent {
type: "chunk" | "done" | "error";
text: string;
}
export function AgentChat({ agentId, companyId }: { agentId: string; companyId: string }) {
const askAgent = usePluginAction("ask-agent");
const { events, connected, close } = usePluginStream<AgentEvent>(`agent:${agentId}`, { companyId });
const [prompt, setPrompt] = useState("");
async function send() {
setPrompt("");
await askAgent({ agentId, companyId, prompt });
}
return (
<div>
<div>{events.filter(e => e.type === "chunk").map((e, i) => <span key={i}>{e.text}</span>)}</div>
<input value={prompt} onChange={(e) => setPrompt(e.target.value)} />
<button onClick={send}>Send</button>
{connected && <button onClick={close}>Stop</button>}
</div>
);
}
```
## Agent sessions (two-way chat)
Plugins can hold multi-turn conversational sessions with agents:
```ts
// Create a session
const session = await ctx.agents.sessions.create(agentId, companyId);
// Send a message and stream the response
await ctx.agents.sessions.sendMessage(session.sessionId, companyId, {
prompt: "Help me triage this issue",
onEvent: (event) => {
if (event.eventType === "chunk") console.log(event.message);
if (event.eventType === "done") console.log("Stream complete");
},
});
// List active sessions
const sessions = await ctx.agents.sessions.list(agentId, companyId);
// Close when done
await ctx.agents.sessions.close(session.sessionId, companyId);
```
Requires capabilities: `agent.sessions.create`, `agent.sessions.list`, `agent.sessions.send`, `agent.sessions.close`.
Exported types: `AgentSession`, `AgentSessionEvent`, `AgentSessionSendResult`, `PluginAgentSessionsClient`.
## Testing utilities
```ts
import { createTestHarness } from "@paperclipai/plugin-sdk/testing";
import plugin from "../src/worker.js";
import manifest from "../src/manifest.js";
const harness = createTestHarness({ manifest });
await plugin.definition.setup(harness.ctx);
await harness.emit("issue.created", { issueId: "iss_1" }, { entityId: "iss_1", entityType: "issue" });
```
## Bundler presets
```ts
import { createPluginBundlerPresets } from "@paperclipai/plugin-sdk/bundlers";
const presets = createPluginBundlerPresets({ uiEntry: "src/ui/index.tsx" });
// presets.esbuild.worker / presets.esbuild.manifest / presets.esbuild.ui
// presets.rollup.worker / presets.rollup.manifest / presets.rollup.ui
```
## Local dev server (hot-reload events)
```bash
paperclip-plugin-dev-server --root . --ui-dir dist/ui --port 4177
```
Or programmatically:
```ts
import { startPluginDevServer } from "@paperclipai/plugin-sdk/dev-server";
const server = await startPluginDevServer({ rootDir: process.cwd() });
```
Dev server endpoints:
- `GET /__paperclip__/health` returns `{ ok, rootDir, uiDir }`
- `GET /__paperclip__/events` streams `reload` SSE events on UI build changes

View File

@@ -0,0 +1,57 @@
/**
* Bundling presets for Paperclip plugins.
*
* These helpers return plain config objects so plugin authors can use them
* with esbuild or rollup without re-implementing host contract defaults.
*/
export interface PluginBundlerPresetInput {
pluginRoot?: string;
manifestEntry?: string;
workerEntry?: string;
uiEntry?: string;
outdir?: string;
sourcemap?: boolean;
minify?: boolean;
}
export interface EsbuildLikeOptions {
entryPoints: string[];
outdir: string;
bundle: boolean;
format: "esm";
platform: "node" | "browser";
target: string;
sourcemap?: boolean;
minify?: boolean;
external?: string[];
}
export interface RollupLikeConfig {
input: string;
output: {
dir: string;
format: "es";
sourcemap?: boolean;
entryFileNames?: string;
};
external?: string[];
plugins?: unknown[];
}
export interface PluginBundlerPresets {
esbuild: {
worker: EsbuildLikeOptions;
ui?: EsbuildLikeOptions;
manifest: EsbuildLikeOptions;
};
rollup: {
worker: RollupLikeConfig;
ui?: RollupLikeConfig;
manifest: RollupLikeConfig;
};
}
/**
* Build esbuild/rollup baseline configs for plugin worker, manifest, and UI bundles.
*
* The presets intentionally externalize host/runtime deps (`react`, SDK packages)
* to match the Paperclip plugin loader contract.
*/
export declare function createPluginBundlerPresets(input?: PluginBundlerPresetInput): PluginBundlerPresets;
//# sourceMappingURL=bundlers.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"bundlers.d.ts","sourceRoot":"","sources":["../src/bundlers.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,WAAW,wBAAwB;IACvC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,kBAAkB;IACjC,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,OAAO,CAAC;IAChB,MAAM,EAAE,KAAK,CAAC;IACd,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;CACrB;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE;QACN,GAAG,EAAE,MAAM,CAAC;QACZ,MAAM,EAAE,IAAI,CAAC;QACb,SAAS,CAAC,EAAE,OAAO,CAAC;QACpB,cAAc,CAAC,EAAE,MAAM,CAAC;KACzB,CAAC;IACF,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,OAAO,CAAC,EAAE,OAAO,EAAE,CAAC;CACrB;AAED,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE;QACP,MAAM,EAAE,kBAAkB,CAAC;QAC3B,EAAE,CAAC,EAAE,kBAAkB,CAAC;QACxB,QAAQ,EAAE,kBAAkB,CAAC;KAC9B,CAAC;IACF,MAAM,EAAE;QACN,MAAM,EAAE,gBAAgB,CAAC;QACzB,EAAE,CAAC,EAAE,gBAAgB,CAAC;QACtB,QAAQ,EAAE,gBAAgB,CAAC;KAC5B,CAAC;CACH;AAED;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,KAAK,GAAE,wBAA6B,GAAG,oBAAoB,CAmGrG"}

105
node_modules/@paperclipai/plugin-sdk/dist/bundlers.js generated vendored Normal file
View File

@@ -0,0 +1,105 @@
/**
* Bundling presets for Paperclip plugins.
*
* These helpers return plain config objects so plugin authors can use them
* with esbuild or rollup without re-implementing host contract defaults.
*/
/**
* Build esbuild/rollup baseline configs for plugin worker, manifest, and UI bundles.
*
* The presets intentionally externalize host/runtime deps (`react`, SDK packages)
* to match the Paperclip plugin loader contract.
*/
export function createPluginBundlerPresets(input = {}) {
const uiExternal = [
"@paperclipai/plugin-sdk/ui",
"@paperclipai/plugin-sdk/ui/hooks",
"react",
"react-dom",
"react/jsx-runtime",
];
const outdir = input.outdir ?? "dist";
const workerEntry = input.workerEntry ?? "src/worker.ts";
const manifestEntry = input.manifestEntry ?? "src/manifest.ts";
const uiEntry = input.uiEntry;
const sourcemap = input.sourcemap ?? true;
const minify = input.minify ?? false;
const esbuildWorker = {
entryPoints: [workerEntry],
outdir,
bundle: true,
format: "esm",
platform: "node",
target: "node20",
sourcemap,
minify,
external: ["react", "react-dom"],
};
const esbuildManifest = {
entryPoints: [manifestEntry],
outdir,
bundle: false,
format: "esm",
platform: "node",
target: "node20",
sourcemap,
};
const esbuildUi = uiEntry
? {
entryPoints: [uiEntry],
outdir: `${outdir}/ui`,
bundle: true,
format: "esm",
platform: "browser",
target: "es2022",
sourcemap,
minify,
external: uiExternal,
}
: undefined;
const rollupWorker = {
input: workerEntry,
output: {
dir: outdir,
format: "es",
sourcemap,
entryFileNames: "worker.js",
},
external: ["react", "react-dom"],
};
const rollupManifest = {
input: manifestEntry,
output: {
dir: outdir,
format: "es",
sourcemap,
entryFileNames: "manifest.js",
},
external: ["@paperclipai/plugin-sdk"],
};
const rollupUi = uiEntry
? {
input: uiEntry,
output: {
dir: `${outdir}/ui`,
format: "es",
sourcemap,
entryFileNames: "index.js",
},
external: uiExternal,
}
: undefined;
return {
esbuild: {
worker: esbuildWorker,
manifest: esbuildManifest,
...(esbuildUi ? { ui: esbuildUi } : {}),
},
rollup: {
worker: rollupWorker,
manifest: rollupManifest,
...(rollupUi ? { ui: rollupUi } : {}),
},
};
}
//# sourceMappingURL=bundlers.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"bundlers.js","sourceRoot":"","sources":["../src/bundlers.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,218 @@
/**
* `definePlugin` — the top-level helper for authoring a Paperclip plugin.
*
* Plugin authors call `definePlugin()` and export the result as the default
* export from their worker entrypoint. The host imports the worker module,
* calls `setup()` with a `PluginContext`, and from that point the plugin
* responds to events, jobs, webhooks, and UI requests through the context.
*
* @see PLUGIN_SPEC.md §14.1 — Example SDK Shape
*
* @example
* ```ts
* // dist/worker.ts
* import { definePlugin } from "@paperclipai/plugin-sdk";
*
* export default definePlugin({
* async setup(ctx) {
* ctx.logger.info("Linear sync plugin starting");
*
* // Subscribe to events
* ctx.events.on("issue.created", async (event) => {
* const config = await ctx.config.get();
* await ctx.http.fetch(`https://api.linear.app/...`, {
* method: "POST",
* headers: { Authorization: `Bearer ${await ctx.secrets.resolve(config.apiKeyRef as string)}` },
* body: JSON.stringify({ title: event.payload.title }),
* });
* });
*
* // Register a job handler
* ctx.jobs.register("full-sync", async (job) => {
* ctx.logger.info("Running full-sync job", { runId: job.runId });
* // ... sync logic
* });
*
* // Register data for the UI
* ctx.data.register("sync-health", async ({ companyId }) => {
* const state = await ctx.state.get({
* scopeKind: "company",
* scopeId: String(companyId),
* stateKey: "last-sync",
* });
* return { lastSync: state };
* });
* },
* });
* ```
*/
import type { PluginContext } from "./types.js";
/**
* Optional plugin-reported diagnostics returned from the `health()` RPC method.
*
* @see PLUGIN_SPEC.md §13.2 — `health`
*/
export interface PluginHealthDiagnostics {
/** Machine-readable status: `"ok"` | `"degraded"` | `"error"`. */
status: "ok" | "degraded" | "error";
/** Human-readable description of the current health state. */
message?: string;
/** Plugin-reported key-value diagnostics (e.g. connection status, queue depth). */
details?: Record<string, unknown>;
}
/**
* Result returned from the `validateConfig()` RPC method.
*
* @see PLUGIN_SPEC.md §13.3 — `validateConfig`
*/
export interface PluginConfigValidationResult {
/** Whether the config is valid. */
ok: boolean;
/** Non-fatal warnings about the config. */
warnings?: string[];
/** Validation errors (populated when `ok` is `false`). */
errors?: string[];
}
/**
* Input received by the plugin worker's `handleWebhook` handler.
*
* @see PLUGIN_SPEC.md §13.7 — `handleWebhook`
*/
export interface PluginWebhookInput {
/** Endpoint key matching the manifest declaration. */
endpointKey: string;
/** Inbound request headers. */
headers: Record<string, string | string[]>;
/** Raw request body as a UTF-8 string. */
rawBody: string;
/** Parsed JSON body (if applicable and parseable). */
parsedBody?: unknown;
/** Unique request identifier for idempotency checks. */
requestId: string;
}
/**
* The plugin definition shape passed to `definePlugin()`.
*
* The only required field is `setup`, which receives the `PluginContext` and
* is where the plugin registers its handlers (events, jobs, data, actions,
* tools, etc.).
*
* All other lifecycle hooks are optional. If a hook is not implemented the
* host applies default behaviour (e.g. restarting the worker on config change
* instead of calling `onConfigChanged`).
*
* @see PLUGIN_SPEC.md §13 — Host-Worker Protocol
*/
export interface PluginDefinition {
/**
* Called once when the plugin worker starts up, after `initialize` completes.
*
* This is where the plugin registers all its handlers: event subscriptions,
* job handlers, data/action handlers, and tool registrations. Registration
* must be synchronous after `setup` resolves — do not register handlers
* inside async callbacks that may resolve after `setup` returns.
*
* @param ctx - The full plugin context provided by the host
*/
setup(ctx: PluginContext): Promise<void>;
/**
* Called when the host wants to know if the plugin is healthy.
*
* The host polls this on a regular interval and surfaces the result in the
* plugin health dashboard. If not implemented, the host infers health from
* worker process liveness.
*
* @see PLUGIN_SPEC.md §13.2 — `health`
*/
onHealth?(): Promise<PluginHealthDiagnostics>;
/**
* Called when the operator updates the plugin's instance configuration at
* runtime, without restarting the worker.
*
* If not implemented, the host restarts the worker to apply the new config.
*
* @param newConfig - The newly resolved configuration
* @see PLUGIN_SPEC.md §13.4 — `configChanged`
*/
onConfigChanged?(newConfig: Record<string, unknown>): Promise<void>;
/**
* Called when the host is about to shut down the plugin worker.
*
* The worker has at most 10 seconds (configurable via plugin config) to
* finish in-flight work and resolve this promise. After the deadline the
* host sends SIGTERM, then SIGKILL.
*
* @see PLUGIN_SPEC.md §12.5 — Graceful Shutdown Policy
*/
onShutdown?(): Promise<void>;
/**
* Called to validate the current plugin configuration.
*
* The host calls this:
* - after the plugin starts (to surface config errors immediately)
* - after the operator saves a new config (to validate before persisting)
* - via the "Test Connection" button in the settings UI
*
* @param config - The configuration to validate
* @see PLUGIN_SPEC.md §13.3 — `validateConfig`
*/
onValidateConfig?(config: Record<string, unknown>): Promise<PluginConfigValidationResult>;
/**
* Called to handle an inbound webhook delivery.
*
* The host routes `POST /api/plugins/:pluginId/webhooks/:endpointKey` to
* this handler. The plugin is responsible for signature verification using
* a resolved secret ref.
*
* If not implemented but webhooks are declared in the manifest, the host
* returns HTTP 501 for webhook deliveries.
*
* @param input - Webhook delivery metadata and payload
* @see PLUGIN_SPEC.md §13.7 — `handleWebhook`
*/
onWebhook?(input: PluginWebhookInput): Promise<void>;
}
/**
* The sealed plugin object returned by `definePlugin()`.
*
* Plugin authors export this as the default export from their worker
* entrypoint. The host imports it and calls the lifecycle methods.
*
* @see PLUGIN_SPEC.md §14 — SDK Surface
*/
export interface PaperclipPlugin {
/** The original plugin definition passed to `definePlugin()`. */
readonly definition: PluginDefinition;
}
/**
* Define a Paperclip plugin.
*
* Call this function in your worker entrypoint and export the result as the
* default export. The host will import the module and call lifecycle methods
* on the returned object.
*
* @param definition - Plugin lifecycle handlers
* @returns A sealed `PaperclipPlugin` object for the host to consume
*
* @example
* ```ts
* import { definePlugin } from "@paperclipai/plugin-sdk";
*
* export default definePlugin({
* async setup(ctx) {
* ctx.logger.info("Plugin started");
* ctx.events.on("issue.created", async (event) => {
* // handle event
* });
* },
*
* async onHealth() {
* return { status: "ok" };
* },
* });
* ```
*
* @see PLUGIN_SPEC.md §14.1 — Example SDK Shape
*/
export declare function definePlugin(definition: PluginDefinition): PaperclipPlugin;
//# sourceMappingURL=define-plugin.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"define-plugin.d.ts","sourceRoot":"","sources":["../src/define-plugin.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAMhD;;;;GAIG;AACH,MAAM,WAAW,uBAAuB;IACtC,kEAAkE;IAClE,MAAM,EAAE,IAAI,GAAG,UAAU,GAAG,OAAO,CAAC;IACpC,8DAA8D;IAC9D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,mFAAmF;IACnF,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC;AAMD;;;;GAIG;AACH,MAAM,WAAW,4BAA4B;IAC3C,mCAAmC;IACnC,EAAE,EAAE,OAAO,CAAC;IACZ,2CAA2C;IAC3C,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,0DAA0D;IAC1D,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;CACnB;AAMD;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC,sDAAsD;IACtD,WAAW,EAAE,MAAM,CAAC;IACpB,+BAA+B;IAC/B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAAC;IAC3C,0CAA0C;IAC1C,OAAO,EAAE,MAAM,CAAC;IAChB,sDAAsD;IACtD,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,wDAAwD;IACxD,SAAS,EAAE,MAAM,CAAC;CACnB;AAMD;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;;;;;;;;OASG;IACH,KAAK,CAAC,GAAG,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEzC;;;;;;;;OAQG;IACH,QAAQ,CAAC,IAAI,OAAO,CAAC,uBAAuB,CAAC,CAAC;IAE9C;;;;;;;;OAQG;IACH,eAAe,CAAC,CAAC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEpE;;;;;;;;OAQG;IACH,UAAU,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAE7B;;;;;;;;;;OAUG;IACH,gBAAgB,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,4BAA4B,CAAC,CAAC;IAE1F;;;;;;;;;;;;OAYG;IACH,SAAS,CAAC,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACtD;AAMD;;;;;;;GAOG;AACH,MAAM,WAAW,eAAe;IAC9B,iEAAiE;IACjE,QAAQ,CAAC,UAAU,EAAE,gBAAgB,CAAC;CACvC;AAMD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,YAAY,CAAC,UAAU,EAAE,gBAAgB,GAAG,eAAe,CAE1E"}

View File

@@ -0,0 +1,85 @@
/**
* `definePlugin` — the top-level helper for authoring a Paperclip plugin.
*
* Plugin authors call `definePlugin()` and export the result as the default
* export from their worker entrypoint. The host imports the worker module,
* calls `setup()` with a `PluginContext`, and from that point the plugin
* responds to events, jobs, webhooks, and UI requests through the context.
*
* @see PLUGIN_SPEC.md §14.1 — Example SDK Shape
*
* @example
* ```ts
* // dist/worker.ts
* import { definePlugin } from "@paperclipai/plugin-sdk";
*
* export default definePlugin({
* async setup(ctx) {
* ctx.logger.info("Linear sync plugin starting");
*
* // Subscribe to events
* ctx.events.on("issue.created", async (event) => {
* const config = await ctx.config.get();
* await ctx.http.fetch(`https://api.linear.app/...`, {
* method: "POST",
* headers: { Authorization: `Bearer ${await ctx.secrets.resolve(config.apiKeyRef as string)}` },
* body: JSON.stringify({ title: event.payload.title }),
* });
* });
*
* // Register a job handler
* ctx.jobs.register("full-sync", async (job) => {
* ctx.logger.info("Running full-sync job", { runId: job.runId });
* // ... sync logic
* });
*
* // Register data for the UI
* ctx.data.register("sync-health", async ({ companyId }) => {
* const state = await ctx.state.get({
* scopeKind: "company",
* scopeId: String(companyId),
* stateKey: "last-sync",
* });
* return { lastSync: state };
* });
* },
* });
* ```
*/
// ---------------------------------------------------------------------------
// definePlugin — top-level factory
// ---------------------------------------------------------------------------
/**
* Define a Paperclip plugin.
*
* Call this function in your worker entrypoint and export the result as the
* default export. The host will import the module and call lifecycle methods
* on the returned object.
*
* @param definition - Plugin lifecycle handlers
* @returns A sealed `PaperclipPlugin` object for the host to consume
*
* @example
* ```ts
* import { definePlugin } from "@paperclipai/plugin-sdk";
*
* export default definePlugin({
* async setup(ctx) {
* ctx.logger.info("Plugin started");
* ctx.events.on("issue.created", async (event) => {
* // handle event
* });
* },
*
* async onHealth() {
* return { status: "ok" };
* },
* });
* ```
*
* @see PLUGIN_SPEC.md §14.1 — Example SDK Shape
*/
export function definePlugin(definition) {
return Object.freeze({ definition });
}
//# sourceMappingURL=define-plugin.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"define-plugin.js","sourceRoot":"","sources":["../src/define-plugin.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AA2KH,8EAA8E;AAC9E,mCAAmC;AACnC,8EAA8E;AAE9E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,MAAM,UAAU,YAAY,CAAC,UAA4B;IACvD,OAAO,MAAM,CAAC,MAAM,CAAC,EAAE,UAAU,EAAE,CAAC,CAAC;AACvC,CAAC"}

View File

@@ -0,0 +1,3 @@
#!/usr/bin/env node
export {};
//# sourceMappingURL=dev-cli.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"dev-cli.d.ts","sourceRoot":"","sources":["../src/dev-cli.ts"],"names":[],"mappings":""}

49
node_modules/@paperclipai/plugin-sdk/dist/dev-cli.js generated vendored Executable file
View File

@@ -0,0 +1,49 @@
#!/usr/bin/env node
import path from "node:path";
import { startPluginDevServer } from "./dev-server.js";
function parseArg(flag) {
const index = process.argv.indexOf(flag);
if (index < 0)
return undefined;
return process.argv[index + 1];
}
/**
* CLI entrypoint for the local plugin UI preview server.
*
* This is intentionally minimal and delegates all serving behavior to
* `startPluginDevServer` so tests and programmatic usage share one path.
*/
async function main() {
const rootDir = parseArg("--root") ?? process.cwd();
const uiDir = parseArg("--ui-dir") ?? "dist/ui";
const host = parseArg("--host") ?? "127.0.0.1";
const rawPort = parseArg("--port") ?? "4177";
const port = Number.parseInt(rawPort, 10);
if (!Number.isFinite(port) || port <= 0 || port > 65535) {
throw new Error(`Invalid --port value: ${rawPort}`);
}
const server = await startPluginDevServer({
rootDir: path.resolve(rootDir),
uiDir,
host,
port,
});
// eslint-disable-next-line no-console
console.log(`Paperclip plugin dev server listening at ${server.url}`);
const shutdown = async () => {
await server.close();
process.exit(0);
};
process.on("SIGINT", () => {
void shutdown();
});
process.on("SIGTERM", () => {
void shutdown();
});
}
void main().catch((error) => {
// eslint-disable-next-line no-console
console.error(error instanceof Error ? error.message : String(error));
process.exit(1);
});
//# sourceMappingURL=dev-cli.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"dev-cli.js","sourceRoot":"","sources":["../src/dev-cli.ts"],"names":[],"mappings":";AACA,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAEvD,SAAS,QAAQ,CAAC,IAAY;IAC5B,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IACzC,IAAI,KAAK,GAAG,CAAC;QAAE,OAAO,SAAS,CAAC;IAChC,OAAO,OAAO,CAAC,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC;AACjC,CAAC;AAED;;;;;GAKG;AACH,KAAK,UAAU,IAAI;IACjB,MAAM,OAAO,GAAG,QAAQ,CAAC,QAAQ,CAAC,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC;IACpD,MAAM,KAAK,GAAG,QAAQ,CAAC,UAAU,CAAC,IAAI,SAAS,CAAC;IAChD,MAAM,IAAI,GAAG,QAAQ,CAAC,QAAQ,CAAC,IAAI,WAAW,CAAC;IAC/C,MAAM,OAAO,GAAG,QAAQ,CAAC,QAAQ,CAAC,IAAI,MAAM,CAAC;IAC7C,MAAM,IAAI,GAAG,MAAM,CAAC,QAAQ,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;IAC1C,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,IAAI,IAAI,GAAG,KAAK,EAAE,CAAC;QACxD,MAAM,IAAI,KAAK,CAAC,yBAAyB,OAAO,EAAE,CAAC,CAAC;IACtD,CAAC;IAED,MAAM,MAAM,GAAG,MAAM,oBAAoB,CAAC;QACxC,OAAO,EAAE,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC;QAC9B,KAAK;QACL,IAAI;QACJ,IAAI;KACL,CAAC,CAAC;IAEH,sCAAsC;IACtC,OAAO,CAAC,GAAG,CAAC,4CAA4C,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC;IAEtE,MAAM,QAAQ,GAAG,KAAK,IAAI,EAAE;QAC1B,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;QACrB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC,CAAC;IAEF,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,EAAE;QACxB,KAAK,QAAQ,EAAE,CAAC;IAClB,CAAC,CAAC,CAAC;IACH,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,GAAG,EAAE;QACzB,KAAK,QAAQ,EAAE,CAAC;IAClB,CAAC,CAAC,CAAC;AACL,CAAC;AAED,KAAK,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE;IAC1B,sCAAsC;IACtC,OAAO,CAAC,KAAK,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;IACtE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}

View File

@@ -0,0 +1,34 @@
export interface PluginDevServerOptions {
/** Plugin project root. Defaults to `process.cwd()`. */
rootDir?: string;
/** Relative path from root to built UI assets. Defaults to `dist/ui`. */
uiDir?: string;
/** Bind port for local preview server. Defaults to `4177`. */
port?: number;
/** Bind host. Defaults to `127.0.0.1`. */
host?: string;
}
export interface PluginDevServer {
url: string;
close(): Promise<void>;
}
/**
* Start a local static server for plugin UI assets with SSE reload events.
*
* Endpoint summary:
* - `GET /__paperclip__/health` for diagnostics
* - `GET /__paperclip__/events` for hot-reload stream
* - Any other path serves files from the configured UI build directory
*/
export declare function startPluginDevServer(options?: PluginDevServerOptions): Promise<PluginDevServer>;
/**
* Return a stable file+mtime snapshot for a built plugin UI directory.
*
* Used by the polling watcher fallback and useful for tests that need to assert
* whether a UI build has changed between runs.
*/
export declare function getUiBuildSnapshot(rootDir: string, uiDir?: string): Promise<Array<{
file: string;
mtimeMs: number;
}>>;
//# sourceMappingURL=dev-server.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"dev-server.d.ts","sourceRoot":"","sources":["../src/dev-server.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,sBAAsB;IACrC,wDAAwD;IACxD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,yEAAyE;IACzE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,8DAA8D;IAC9D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,0CAA0C;IAC1C,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAkGD;;;;;;;GAOG;AACH,wBAAsB,oBAAoB,CAAC,OAAO,GAAE,sBAA2B,GAAG,OAAO,CAAC,eAAe,CAAC,CAiFzG;AAED;;;;;GAKG;AACH,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,SAAY,GAAG,OAAO,CAAC,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC,CAY9H"}

194
node_modules/@paperclipai/plugin-sdk/dist/dev-server.js generated vendored Normal file
View File

@@ -0,0 +1,194 @@
import { createReadStream, existsSync, statSync, watch } from "node:fs";
import { mkdir, readdir, stat } from "node:fs/promises";
import { createServer } from "node:http";
import path from "node:path";
function contentType(filePath) {
if (filePath.endsWith(".js"))
return "text/javascript; charset=utf-8";
if (filePath.endsWith(".css"))
return "text/css; charset=utf-8";
if (filePath.endsWith(".json"))
return "application/json; charset=utf-8";
if (filePath.endsWith(".html"))
return "text/html; charset=utf-8";
if (filePath.endsWith(".svg"))
return "image/svg+xml";
return "application/octet-stream";
}
function normalizeFilePath(baseDir, reqPath) {
const pathname = reqPath.split("?")[0] || "/";
const resolved = pathname === "/" ? "/index.js" : pathname;
const absolute = path.resolve(baseDir, `.${resolved}`);
const normalizedBase = `${path.resolve(baseDir)}${path.sep}`;
if (!absolute.startsWith(normalizedBase) && absolute !== path.resolve(baseDir)) {
throw new Error("path traversal blocked");
}
return absolute;
}
function send404(res) {
res.statusCode = 404;
res.setHeader("Content-Type", "application/json; charset=utf-8");
res.end(JSON.stringify({ error: "Not found" }));
}
function sendJson(res, value) {
res.statusCode = 200;
res.setHeader("Content-Type", "application/json; charset=utf-8");
res.end(JSON.stringify(value));
}
async function ensureUiDir(uiDir) {
if (existsSync(uiDir))
return;
await mkdir(uiDir, { recursive: true });
}
async function listFilesRecursive(dir) {
const out = [];
const entries = await readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const abs = path.join(dir, entry.name);
if (entry.isDirectory()) {
out.push(...await listFilesRecursive(abs));
}
else if (entry.isFile()) {
out.push(abs);
}
}
return out;
}
function snapshotSignature(rows) {
return rows.map((row) => `${row.file}:${Math.trunc(row.mtimeMs)}`).join("|");
}
async function startUiWatcher(uiDir, onReload) {
try {
// macOS/Windows support recursive native watching.
const watcher = watch(uiDir, { recursive: true }, (_eventType, filename) => {
if (!filename)
return;
onReload(path.join(uiDir, filename));
});
return watcher;
}
catch {
// Linux may reject recursive watch. Fall back to polling snapshots.
let previous = snapshotSignature((await getUiBuildSnapshot(path.dirname(uiDir), path.basename(uiDir))).map((row) => ({
file: row.file,
mtimeMs: row.mtimeMs,
})));
const timer = setInterval(async () => {
try {
const nextRows = await getUiBuildSnapshot(path.dirname(uiDir), path.basename(uiDir));
const next = snapshotSignature(nextRows);
if (next === previous)
return;
previous = next;
onReload("__snapshot__");
}
catch {
// Ignore transient read errors while bundlers are writing files.
}
}, 500);
return {
close() {
clearInterval(timer);
},
};
}
}
/**
* Start a local static server for plugin UI assets with SSE reload events.
*
* Endpoint summary:
* - `GET /__paperclip__/health` for diagnostics
* - `GET /__paperclip__/events` for hot-reload stream
* - Any other path serves files from the configured UI build directory
*/
export async function startPluginDevServer(options = {}) {
const rootDir = path.resolve(options.rootDir ?? process.cwd());
const uiDir = path.resolve(rootDir, options.uiDir ?? "dist/ui");
const host = options.host ?? "127.0.0.1";
const port = options.port ?? 4177;
await ensureUiDir(uiDir);
const sseClients = new Set();
const handleRequest = async (req, res) => {
const url = req.url ?? "/";
if (url === "/__paperclip__/health") {
sendJson(res, { ok: true, rootDir, uiDir });
return;
}
if (url === "/__paperclip__/events") {
res.writeHead(200, {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache, no-transform",
Connection: "keep-alive",
});
res.write(`event: connected\ndata: {"ok":true}\n\n`);
sseClients.add(res);
req.on("close", () => {
sseClients.delete(res);
});
return;
}
try {
const filePath = normalizeFilePath(uiDir, url);
if (!existsSync(filePath) || !statSync(filePath).isFile()) {
send404(res);
return;
}
res.statusCode = 200;
res.setHeader("Content-Type", contentType(filePath));
createReadStream(filePath).pipe(res);
}
catch {
send404(res);
}
};
const server = createServer((req, res) => {
void handleRequest(req, res);
});
const notifyReload = (filePath) => {
const rel = path.relative(uiDir, filePath);
const payload = JSON.stringify({ type: "reload", file: rel, at: new Date().toISOString() });
for (const client of sseClients) {
client.write(`event: reload\ndata: ${payload}\n\n`);
}
};
const watcher = await startUiWatcher(uiDir, notifyReload);
await new Promise((resolve, reject) => {
server.once("error", reject);
server.listen(port, host, () => resolve());
});
const address = server.address();
const actualPort = address && typeof address === "object" ? address.port : port;
return {
url: `http://${host}:${actualPort}`,
async close() {
watcher.close();
for (const client of sseClients) {
client.end();
}
await new Promise((resolve, reject) => {
server.close((err) => (err ? reject(err) : resolve()));
});
},
};
}
/**
* Return a stable file+mtime snapshot for a built plugin UI directory.
*
* Used by the polling watcher fallback and useful for tests that need to assert
* whether a UI build has changed between runs.
*/
export async function getUiBuildSnapshot(rootDir, uiDir = "dist/ui") {
const baseDir = path.resolve(rootDir, uiDir);
if (!existsSync(baseDir))
return [];
const files = await listFilesRecursive(baseDir);
const rows = await Promise.all(files.map(async (filePath) => {
const fileStat = await stat(filePath);
return {
file: path.relative(baseDir, filePath),
mtimeMs: fileStat.mtimeMs,
};
}));
return rows.sort((a, b) => a.file.localeCompare(b.file));
}
//# sourceMappingURL=dev-server.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,229 @@
/**
* Host-side client factory — creates capability-gated handler maps for
* servicing worker→host JSON-RPC calls.
*
* When a plugin worker calls `ctx.state.get(...)` inside its process, the
* SDK serializes the call as a JSON-RPC request over stdio. On the host side,
* the `PluginWorkerManager` receives the request and dispatches it to the
* handler registered for that method. This module provides a factory that
* creates those handlers for all `WorkerToHostMethods`, with automatic
* capability enforcement.
*
* ## Design
*
* 1. **Capability gating**: Each handler checks the plugin's declared
* capabilities before executing. If the plugin lacks a required capability,
* the handler throws a `CapabilityDeniedError` (which the worker manager
* translates into a JSON-RPC error response with code
* `CAPABILITY_DENIED`).
*
* 2. **Service adapters**: The caller provides a `HostServices` object with
* concrete implementations of each platform service. The factory wires
* each handler to the appropriate service method.
*
* 3. **Type safety**: The returned handler map is typed as
* `WorkerToHostHandlers` (from `plugin-worker-manager.ts`) so it plugs
* directly into `WorkerStartOptions.hostHandlers`.
*
* @example
* ```ts
* const handlers = createHostClientHandlers({
* pluginId: "acme.linear",
* capabilities: manifest.capabilities,
* services: {
* config: { get: () => registry.getConfig(pluginId) },
* state: { get: ..., set: ..., delete: ... },
* entities: { upsert: ..., list: ... },
* // ... all services
* },
* });
*
* await workerManager.startWorker("acme.linear", {
* // ...
* hostHandlers: handlers,
* });
* ```
*
* @see PLUGIN_SPEC.md §13 — Host-Worker Protocol
* @see PLUGIN_SPEC.md §15 — Capability Model
*/
import type { PluginCapability } from "@paperclipai/shared";
import type { WorkerToHostMethods, WorkerToHostMethodName } from "./protocol.js";
/**
* Thrown when a plugin calls a host method it does not have the capability for.
*
* The `code` field is set to `PLUGIN_RPC_ERROR_CODES.CAPABILITY_DENIED` so
* the worker manager can propagate it as the correct JSON-RPC error code.
*/
export declare class CapabilityDeniedError extends Error {
readonly name = "CapabilityDeniedError";
readonly code: -32001;
constructor(pluginId: string, method: string, capability: PluginCapability);
}
/**
* Service adapters that the host must provide. Each property maps to a group
* of `WorkerToHostMethods`. The factory wires JSON-RPC params to these
* function signatures.
*
* All methods return promises to support async I/O (database, HTTP, etc.).
*/
export interface HostServices {
/** Provides `config.get`. */
config: {
get(): Promise<Record<string, unknown>>;
};
/** Provides `state.get`, `state.set`, `state.delete`. */
state: {
get(params: WorkerToHostMethods["state.get"][0]): Promise<WorkerToHostMethods["state.get"][1]>;
set(params: WorkerToHostMethods["state.set"][0]): Promise<void>;
delete(params: WorkerToHostMethods["state.delete"][0]): Promise<void>;
};
/** Provides `entities.upsert`, `entities.list`. */
entities: {
upsert(params: WorkerToHostMethods["entities.upsert"][0]): Promise<WorkerToHostMethods["entities.upsert"][1]>;
list(params: WorkerToHostMethods["entities.list"][0]): Promise<WorkerToHostMethods["entities.list"][1]>;
};
/** Provides `events.emit` and `events.subscribe`. */
events: {
emit(params: WorkerToHostMethods["events.emit"][0]): Promise<void>;
subscribe(params: WorkerToHostMethods["events.subscribe"][0]): Promise<void>;
};
/** Provides `http.fetch`. */
http: {
fetch(params: WorkerToHostMethods["http.fetch"][0]): Promise<WorkerToHostMethods["http.fetch"][1]>;
};
/** Provides `secrets.resolve`. */
secrets: {
resolve(params: WorkerToHostMethods["secrets.resolve"][0]): Promise<string>;
};
/** Provides `activity.log`. */
activity: {
log(params: {
companyId: string;
message: string;
entityType?: string;
entityId?: string;
metadata?: Record<string, unknown>;
}): Promise<void>;
};
/** Provides `metrics.write`. */
metrics: {
write(params: WorkerToHostMethods["metrics.write"][0]): Promise<void>;
};
/** Provides `log`. */
logger: {
log(params: WorkerToHostMethods["log"][0]): Promise<void>;
};
/** Provides `companies.list`, `companies.get`. */
companies: {
list(params: WorkerToHostMethods["companies.list"][0]): Promise<WorkerToHostMethods["companies.list"][1]>;
get(params: WorkerToHostMethods["companies.get"][0]): Promise<WorkerToHostMethods["companies.get"][1]>;
};
/** Provides `projects.list`, `projects.get`, `projects.listWorkspaces`, `projects.getPrimaryWorkspace`, `projects.getWorkspaceForIssue`. */
projects: {
list(params: WorkerToHostMethods["projects.list"][0]): Promise<WorkerToHostMethods["projects.list"][1]>;
get(params: WorkerToHostMethods["projects.get"][0]): Promise<WorkerToHostMethods["projects.get"][1]>;
listWorkspaces(params: WorkerToHostMethods["projects.listWorkspaces"][0]): Promise<WorkerToHostMethods["projects.listWorkspaces"][1]>;
getPrimaryWorkspace(params: WorkerToHostMethods["projects.getPrimaryWorkspace"][0]): Promise<WorkerToHostMethods["projects.getPrimaryWorkspace"][1]>;
getWorkspaceForIssue(params: WorkerToHostMethods["projects.getWorkspaceForIssue"][0]): Promise<WorkerToHostMethods["projects.getWorkspaceForIssue"][1]>;
};
/** Provides `issues.list`, `issues.get`, `issues.create`, `issues.update`, `issues.listComments`, `issues.createComment`. */
issues: {
list(params: WorkerToHostMethods["issues.list"][0]): Promise<WorkerToHostMethods["issues.list"][1]>;
get(params: WorkerToHostMethods["issues.get"][0]): Promise<WorkerToHostMethods["issues.get"][1]>;
create(params: WorkerToHostMethods["issues.create"][0]): Promise<WorkerToHostMethods["issues.create"][1]>;
update(params: WorkerToHostMethods["issues.update"][0]): Promise<WorkerToHostMethods["issues.update"][1]>;
listComments(params: WorkerToHostMethods["issues.listComments"][0]): Promise<WorkerToHostMethods["issues.listComments"][1]>;
createComment(params: WorkerToHostMethods["issues.createComment"][0]): Promise<WorkerToHostMethods["issues.createComment"][1]>;
};
/** Provides `issues.documents.list`, `issues.documents.get`, `issues.documents.upsert`, `issues.documents.delete`. */
issueDocuments: {
list(params: WorkerToHostMethods["issues.documents.list"][0]): Promise<WorkerToHostMethods["issues.documents.list"][1]>;
get(params: WorkerToHostMethods["issues.documents.get"][0]): Promise<WorkerToHostMethods["issues.documents.get"][1]>;
upsert(params: WorkerToHostMethods["issues.documents.upsert"][0]): Promise<WorkerToHostMethods["issues.documents.upsert"][1]>;
delete(params: WorkerToHostMethods["issues.documents.delete"][0]): Promise<WorkerToHostMethods["issues.documents.delete"][1]>;
};
/** Provides `agents.list`, `agents.get`, `agents.pause`, `agents.resume`, `agents.invoke`. */
agents: {
list(params: WorkerToHostMethods["agents.list"][0]): Promise<WorkerToHostMethods["agents.list"][1]>;
get(params: WorkerToHostMethods["agents.get"][0]): Promise<WorkerToHostMethods["agents.get"][1]>;
pause(params: WorkerToHostMethods["agents.pause"][0]): Promise<WorkerToHostMethods["agents.pause"][1]>;
resume(params: WorkerToHostMethods["agents.resume"][0]): Promise<WorkerToHostMethods["agents.resume"][1]>;
invoke(params: WorkerToHostMethods["agents.invoke"][0]): Promise<WorkerToHostMethods["agents.invoke"][1]>;
};
/** Provides `agents.sessions.create`, `agents.sessions.list`, `agents.sessions.sendMessage`, `agents.sessions.close`. */
agentSessions: {
create(params: WorkerToHostMethods["agents.sessions.create"][0]): Promise<WorkerToHostMethods["agents.sessions.create"][1]>;
list(params: WorkerToHostMethods["agents.sessions.list"][0]): Promise<WorkerToHostMethods["agents.sessions.list"][1]>;
sendMessage(params: WorkerToHostMethods["agents.sessions.sendMessage"][0]): Promise<WorkerToHostMethods["agents.sessions.sendMessage"][1]>;
close(params: WorkerToHostMethods["agents.sessions.close"][0]): Promise<void>;
};
/** Provides `goals.list`, `goals.get`, `goals.create`, `goals.update`. */
goals: {
list(params: WorkerToHostMethods["goals.list"][0]): Promise<WorkerToHostMethods["goals.list"][1]>;
get(params: WorkerToHostMethods["goals.get"][0]): Promise<WorkerToHostMethods["goals.get"][1]>;
create(params: WorkerToHostMethods["goals.create"][0]): Promise<WorkerToHostMethods["goals.create"][1]>;
update(params: WorkerToHostMethods["goals.update"][0]): Promise<WorkerToHostMethods["goals.update"][1]>;
};
}
/**
* Options for `createHostClientHandlers`.
*/
export interface HostClientFactoryOptions {
/** The plugin ID. Used for error messages and logging. */
pluginId: string;
/**
* The capabilities declared by the plugin in its manifest. The factory
* enforces these at runtime before delegating to the service adapter.
*/
capabilities: readonly PluginCapability[];
/**
* Concrete implementations of host platform services. Each handler in the
* returned map delegates to the corresponding service method.
*/
services: HostServices;
}
/**
* A handler function for a specific worker→host method.
*/
type HostHandler<M extends WorkerToHostMethodName> = (params: WorkerToHostMethods[M][0]) => Promise<WorkerToHostMethods[M][1]>;
/**
* A complete map of all worker→host method handlers.
*
* This type matches `WorkerToHostHandlers` from `plugin-worker-manager.ts`
* but makes every handler required (the factory always provides all handlers).
*/
export type HostClientHandlers = {
[M in WorkerToHostMethodName]: HostHandler<M>;
};
/**
* Create a complete handler map for all worker→host JSON-RPC methods.
*
* Each handler:
* 1. Checks the plugin's declared capabilities against the required capability
* for the method (if any).
* 2. Delegates to the corresponding service adapter method.
* 3. Returns the service result, which is serialized as the JSON-RPC response
* by the worker manager.
*
* If a capability check fails, the handler throws a `CapabilityDeniedError`
* with code `CAPABILITY_DENIED`. The worker manager catches this and sends a
* JSON-RPC error response to the worker, which surfaces as a `JsonRpcCallError`
* in the plugin's SDK client.
*
* @param options - Plugin ID, capabilities, and service adapters
* @returns A handler map suitable for `WorkerStartOptions.hostHandlers`
*/
export declare function createHostClientHandlers(options: HostClientFactoryOptions): HostClientHandlers;
/**
* Get the capability required for a given worker→host method, or `null` if
* no capability is required.
*
* Useful for inspecting capability requirements without calling the factory.
*
* @param method - The worker→host method name
* @returns The required capability, or `null`
*/
export declare function getRequiredCapability(method: WorkerToHostMethodName): PluginCapability | null;
export {};
//# sourceMappingURL=host-client-factory.d.ts.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,353 @@
/**
* Host-side client factory — creates capability-gated handler maps for
* servicing worker→host JSON-RPC calls.
*
* When a plugin worker calls `ctx.state.get(...)` inside its process, the
* SDK serializes the call as a JSON-RPC request over stdio. On the host side,
* the `PluginWorkerManager` receives the request and dispatches it to the
* handler registered for that method. This module provides a factory that
* creates those handlers for all `WorkerToHostMethods`, with automatic
* capability enforcement.
*
* ## Design
*
* 1. **Capability gating**: Each handler checks the plugin's declared
* capabilities before executing. If the plugin lacks a required capability,
* the handler throws a `CapabilityDeniedError` (which the worker manager
* translates into a JSON-RPC error response with code
* `CAPABILITY_DENIED`).
*
* 2. **Service adapters**: The caller provides a `HostServices` object with
* concrete implementations of each platform service. The factory wires
* each handler to the appropriate service method.
*
* 3. **Type safety**: The returned handler map is typed as
* `WorkerToHostHandlers` (from `plugin-worker-manager.ts`) so it plugs
* directly into `WorkerStartOptions.hostHandlers`.
*
* @example
* ```ts
* const handlers = createHostClientHandlers({
* pluginId: "acme.linear",
* capabilities: manifest.capabilities,
* services: {
* config: { get: () => registry.getConfig(pluginId) },
* state: { get: ..., set: ..., delete: ... },
* entities: { upsert: ..., list: ... },
* // ... all services
* },
* });
*
* await workerManager.startWorker("acme.linear", {
* // ...
* hostHandlers: handlers,
* });
* ```
*
* @see PLUGIN_SPEC.md §13 — Host-Worker Protocol
* @see PLUGIN_SPEC.md §15 — Capability Model
*/
import { PLUGIN_RPC_ERROR_CODES } from "./protocol.js";
// ---------------------------------------------------------------------------
// Error types
// ---------------------------------------------------------------------------
/**
* Thrown when a plugin calls a host method it does not have the capability for.
*
* The `code` field is set to `PLUGIN_RPC_ERROR_CODES.CAPABILITY_DENIED` so
* the worker manager can propagate it as the correct JSON-RPC error code.
*/
export class CapabilityDeniedError extends Error {
name = "CapabilityDeniedError";
code = PLUGIN_RPC_ERROR_CODES.CAPABILITY_DENIED;
constructor(pluginId, method, capability) {
super(`Plugin "${pluginId}" is missing required capability "${capability}" for method "${method}"`);
}
}
// ---------------------------------------------------------------------------
// Capability → method mapping
// ---------------------------------------------------------------------------
/**
* Maps each worker→host RPC method to the capability required to invoke it.
* Methods without a capability requirement (e.g. `config.get`, `log`) are
* mapped to `null`.
*
* @see PLUGIN_SPEC.md §15 — Capability Model
*/
const METHOD_CAPABILITY_MAP = {
// Config — always allowed
"config.get": null,
// State
"state.get": "plugin.state.read",
"state.set": "plugin.state.write",
"state.delete": "plugin.state.write",
// Entities — no specific capability required (plugin-scoped by design)
"entities.upsert": null,
"entities.list": null,
// Events
"events.emit": "events.emit",
"events.subscribe": "events.subscribe",
// HTTP
"http.fetch": "http.outbound",
// Secrets
"secrets.resolve": "secrets.read-ref",
// Activity
"activity.log": "activity.log.write",
// Metrics
"metrics.write": "metrics.write",
// Logger — always allowed
"log": null,
// Companies
"companies.list": "companies.read",
"companies.get": "companies.read",
// Projects
"projects.list": "projects.read",
"projects.get": "projects.read",
"projects.listWorkspaces": "project.workspaces.read",
"projects.getPrimaryWorkspace": "project.workspaces.read",
"projects.getWorkspaceForIssue": "project.workspaces.read",
// Issues
"issues.list": "issues.read",
"issues.get": "issues.read",
"issues.create": "issues.create",
"issues.update": "issues.update",
"issues.listComments": "issue.comments.read",
"issues.createComment": "issue.comments.create",
// Issue Documents
"issues.documents.list": "issue.documents.read",
"issues.documents.get": "issue.documents.read",
"issues.documents.upsert": "issue.documents.write",
"issues.documents.delete": "issue.documents.write",
// Agents
"agents.list": "agents.read",
"agents.get": "agents.read",
"agents.pause": "agents.pause",
"agents.resume": "agents.resume",
"agents.invoke": "agents.invoke",
// Agent Sessions
"agents.sessions.create": "agent.sessions.create",
"agents.sessions.list": "agent.sessions.list",
"agents.sessions.sendMessage": "agent.sessions.send",
"agents.sessions.close": "agent.sessions.close",
// Goals
"goals.list": "goals.read",
"goals.get": "goals.read",
"goals.create": "goals.create",
"goals.update": "goals.update",
};
// ---------------------------------------------------------------------------
// Factory
// ---------------------------------------------------------------------------
/**
* Create a complete handler map for all worker→host JSON-RPC methods.
*
* Each handler:
* 1. Checks the plugin's declared capabilities against the required capability
* for the method (if any).
* 2. Delegates to the corresponding service adapter method.
* 3. Returns the service result, which is serialized as the JSON-RPC response
* by the worker manager.
*
* If a capability check fails, the handler throws a `CapabilityDeniedError`
* with code `CAPABILITY_DENIED`. The worker manager catches this and sends a
* JSON-RPC error response to the worker, which surfaces as a `JsonRpcCallError`
* in the plugin's SDK client.
*
* @param options - Plugin ID, capabilities, and service adapters
* @returns A handler map suitable for `WorkerStartOptions.hostHandlers`
*/
export function createHostClientHandlers(options) {
const { pluginId, services } = options;
const capabilitySet = new Set(options.capabilities);
/**
* Assert that the plugin has the required capability for a method.
* Throws `CapabilityDeniedError` if the capability is missing.
*/
function requireCapability(method) {
const required = METHOD_CAPABILITY_MAP[method];
if (required === null)
return; // No capability required
if (capabilitySet.has(required))
return;
throw new CapabilityDeniedError(pluginId, method, required);
}
/**
* Create a capability-gated proxy handler for a method.
*
* @param method - The RPC method name (used for capability lookup)
* @param handler - The actual handler implementation
* @returns A wrapper that checks capabilities before delegating
*/
function gated(method, handler) {
return async (params) => {
requireCapability(method);
return handler(params);
};
}
// -------------------------------------------------------------------------
// Build the complete handler map
// -------------------------------------------------------------------------
return {
// Config
"config.get": gated("config.get", async () => {
return services.config.get();
}),
// State
"state.get": gated("state.get", async (params) => {
return services.state.get(params);
}),
"state.set": gated("state.set", async (params) => {
return services.state.set(params);
}),
"state.delete": gated("state.delete", async (params) => {
return services.state.delete(params);
}),
// Entities
"entities.upsert": gated("entities.upsert", async (params) => {
return services.entities.upsert(params);
}),
"entities.list": gated("entities.list", async (params) => {
return services.entities.list(params);
}),
// Events
"events.emit": gated("events.emit", async (params) => {
return services.events.emit(params);
}),
"events.subscribe": gated("events.subscribe", async (params) => {
return services.events.subscribe(params);
}),
// HTTP
"http.fetch": gated("http.fetch", async (params) => {
return services.http.fetch(params);
}),
// Secrets
"secrets.resolve": gated("secrets.resolve", async (params) => {
return services.secrets.resolve(params);
}),
// Activity
"activity.log": gated("activity.log", async (params) => {
return services.activity.log(params);
}),
// Metrics
"metrics.write": gated("metrics.write", async (params) => {
return services.metrics.write(params);
}),
// Logger
"log": gated("log", async (params) => {
return services.logger.log(params);
}),
// Companies
"companies.list": gated("companies.list", async (params) => {
return services.companies.list(params);
}),
"companies.get": gated("companies.get", async (params) => {
return services.companies.get(params);
}),
// Projects
"projects.list": gated("projects.list", async (params) => {
return services.projects.list(params);
}),
"projects.get": gated("projects.get", async (params) => {
return services.projects.get(params);
}),
"projects.listWorkspaces": gated("projects.listWorkspaces", async (params) => {
return services.projects.listWorkspaces(params);
}),
"projects.getPrimaryWorkspace": gated("projects.getPrimaryWorkspace", async (params) => {
return services.projects.getPrimaryWorkspace(params);
}),
"projects.getWorkspaceForIssue": gated("projects.getWorkspaceForIssue", async (params) => {
return services.projects.getWorkspaceForIssue(params);
}),
// Issues
"issues.list": gated("issues.list", async (params) => {
return services.issues.list(params);
}),
"issues.get": gated("issues.get", async (params) => {
return services.issues.get(params);
}),
"issues.create": gated("issues.create", async (params) => {
return services.issues.create(params);
}),
"issues.update": gated("issues.update", async (params) => {
return services.issues.update(params);
}),
"issues.listComments": gated("issues.listComments", async (params) => {
return services.issues.listComments(params);
}),
"issues.createComment": gated("issues.createComment", async (params) => {
return services.issues.createComment(params);
}),
// Issue Documents
"issues.documents.list": gated("issues.documents.list", async (params) => {
return services.issueDocuments.list(params);
}),
"issues.documents.get": gated("issues.documents.get", async (params) => {
return services.issueDocuments.get(params);
}),
"issues.documents.upsert": gated("issues.documents.upsert", async (params) => {
return services.issueDocuments.upsert(params);
}),
"issues.documents.delete": gated("issues.documents.delete", async (params) => {
return services.issueDocuments.delete(params);
}),
// Agents
"agents.list": gated("agents.list", async (params) => {
return services.agents.list(params);
}),
"agents.get": gated("agents.get", async (params) => {
return services.agents.get(params);
}),
"agents.pause": gated("agents.pause", async (params) => {
return services.agents.pause(params);
}),
"agents.resume": gated("agents.resume", async (params) => {
return services.agents.resume(params);
}),
"agents.invoke": gated("agents.invoke", async (params) => {
return services.agents.invoke(params);
}),
// Agent Sessions
"agents.sessions.create": gated("agents.sessions.create", async (params) => {
return services.agentSessions.create(params);
}),
"agents.sessions.list": gated("agents.sessions.list", async (params) => {
return services.agentSessions.list(params);
}),
"agents.sessions.sendMessage": gated("agents.sessions.sendMessage", async (params) => {
return services.agentSessions.sendMessage(params);
}),
"agents.sessions.close": gated("agents.sessions.close", async (params) => {
return services.agentSessions.close(params);
}),
// Goals
"goals.list": gated("goals.list", async (params) => {
return services.goals.list(params);
}),
"goals.get": gated("goals.get", async (params) => {
return services.goals.get(params);
}),
"goals.create": gated("goals.create", async (params) => {
return services.goals.create(params);
}),
"goals.update": gated("goals.update", async (params) => {
return services.goals.update(params);
}),
};
}
// ---------------------------------------------------------------------------
// Utility: getRequiredCapability
// ---------------------------------------------------------------------------
/**
* Get the capability required for a given worker→host method, or `null` if
* no capability is required.
*
* Useful for inspecting capability requirements without calling the factory.
*
* @param method - The worker→host method name
* @returns The required capability, or `null`
*/
export function getRequiredCapability(method) {
return METHOD_CAPABILITY_MAP[method];
}
//# sourceMappingURL=host-client-factory.js.map

File diff suppressed because one or more lines are too long

84
node_modules/@paperclipai/plugin-sdk/dist/index.d.ts generated vendored Normal file
View File

@@ -0,0 +1,84 @@
/**
* `@paperclipai/plugin-sdk` — Paperclip plugin worker-side SDK.
*
* This is the main entrypoint for plugin worker code. For plugin UI bundles,
* import from `@paperclipai/plugin-sdk/ui` instead.
*
* @example
* ```ts
* // Plugin worker entrypoint (dist/worker.ts)
* import { definePlugin, runWorker, z } from "@paperclipai/plugin-sdk";
*
* const plugin = definePlugin({
* async setup(ctx) {
* ctx.logger.info("Plugin starting up");
*
* ctx.events.on("issue.created", async (event) => {
* ctx.logger.info("Issue created", { issueId: event.entityId });
* });
*
* ctx.jobs.register("full-sync", async (job) => {
* ctx.logger.info("Starting full sync", { runId: job.runId });
* // ... sync implementation
* });
*
* ctx.data.register("sync-health", async ({ companyId }) => {
* const state = await ctx.state.get({
* scopeKind: "company",
* scopeId: String(companyId),
* stateKey: "last-sync-at",
* });
* return { lastSync: state };
* });
* },
*
* async onHealth() {
* return { status: "ok" };
* },
* });
*
* export default plugin;
* runWorker(plugin, import.meta.url);
* ```
*
* @see PLUGIN_SPEC.md §14 — SDK Surface
* @see PLUGIN_SPEC.md §29.2 — SDK Versioning
*/
export { definePlugin } from "./define-plugin.js";
export { createTestHarness } from "./testing.js";
export { createPluginBundlerPresets } from "./bundlers.js";
export { startPluginDevServer, getUiBuildSnapshot } from "./dev-server.js";
export { startWorkerRpcHost, runWorker } from "./worker-rpc-host.js";
export { createHostClientHandlers, getRequiredCapability, CapabilityDeniedError, } from "./host-client-factory.js";
export { JSONRPC_VERSION, JSONRPC_ERROR_CODES, PLUGIN_RPC_ERROR_CODES, HOST_TO_WORKER_REQUIRED_METHODS, HOST_TO_WORKER_OPTIONAL_METHODS, MESSAGE_DELIMITER, createRequest, createSuccessResponse, createErrorResponse, createNotification, isJsonRpcRequest, isJsonRpcNotification, isJsonRpcResponse, isJsonRpcSuccessResponse, isJsonRpcErrorResponse, serializeMessage, parseMessage, JsonRpcParseError, JsonRpcCallError, _resetIdCounter, } from "./protocol.js";
export type { PluginDefinition, PaperclipPlugin, PluginHealthDiagnostics, PluginConfigValidationResult, PluginWebhookInput, } from "./define-plugin.js";
export type { TestHarness, TestHarnessOptions, TestHarnessLogEntry, } from "./testing.js";
export type { PluginBundlerPresetInput, PluginBundlerPresets, EsbuildLikeOptions, RollupLikeConfig, } from "./bundlers.js";
export type { PluginDevServer, PluginDevServerOptions } from "./dev-server.js";
export type { WorkerRpcHostOptions, WorkerRpcHost, RunWorkerOptions, } from "./worker-rpc-host.js";
export type { HostServices, HostClientFactoryOptions, HostClientHandlers, } from "./host-client-factory.js";
export type { JsonRpcId, JsonRpcRequest, JsonRpcSuccessResponse, JsonRpcError, JsonRpcErrorResponse, JsonRpcResponse, JsonRpcNotification, JsonRpcMessage, JsonRpcErrorCode, PluginRpcErrorCode, InitializeParams, InitializeResult, ConfigChangedParams, ValidateConfigParams, OnEventParams, RunJobParams, GetDataParams, PerformActionParams, ExecuteToolParams, PluginModalBoundsRequest, PluginRenderCloseEvent, PluginLauncherRenderContextSnapshot, HostToWorkerMethods, HostToWorkerMethodName, WorkerToHostMethods, WorkerToHostMethodName, HostToWorkerRequest, HostToWorkerResponse, WorkerToHostRequest, WorkerToHostResponse, WorkerToHostNotifications, WorkerToHostNotificationName, } from "./protocol.js";
export type { PluginContext, PluginConfigClient, PluginEventsClient, PluginJobsClient, PluginLaunchersClient, PluginHttpClient, PluginSecretsClient, PluginActivityClient, PluginActivityLogEntry, PluginStateClient, PluginEntitiesClient, PluginProjectsClient, PluginCompaniesClient, PluginIssuesClient, PluginAgentsClient, PluginAgentSessionsClient, AgentSession, AgentSessionEvent, AgentSessionSendResult, PluginGoalsClient, PluginDataClient, PluginActionsClient, PluginStreamsClient, PluginToolsClient, PluginMetricsClient, PluginLogger, } from "./types.js";
export type { ScopeKey, EventFilter, PluginEvent, PluginJobContext, PluginLauncherRegistration, ToolRunContext, ToolResult, PluginEntityUpsert, PluginEntityRecord, PluginEntityQuery, PluginWorkspace, Company, Project, Issue, IssueComment, Agent, Goal, } from "./types.js";
export type { PaperclipPluginManifestV1, PluginJobDeclaration, PluginWebhookDeclaration, PluginToolDeclaration, PluginUiSlotDeclaration, PluginUiDeclaration, PluginLauncherActionDeclaration, PluginLauncherRenderDeclaration, PluginLauncherDeclaration, PluginMinimumHostVersion, PluginRecord, PluginConfig, JsonSchema, PluginStatus, PluginCategory, PluginCapability, PluginUiSlotType, PluginUiSlotEntityType, PluginLauncherPlacementZone, PluginLauncherAction, PluginLauncherBounds, PluginLauncherRenderEnvironment, PluginStateScopeKind, PluginJobStatus, PluginJobRunStatus, PluginJobRunTrigger, PluginWebhookDeliveryStatus, PluginEventType, PluginBridgeErrorCode, } from "./types.js";
/**
* Zod is re-exported for plugin authors to use when defining their
* `instanceConfigSchema` and tool `parametersSchema`.
*
* Plugin authors do not need to add a separate `zod` dependency.
*
* @see PLUGIN_SPEC.md §14.1 — Example SDK Shape
*
* @example
* ```ts
* import { z } from "@paperclipai/plugin-sdk";
*
* const configSchema = z.object({
* apiKey: z.string().describe("Your API key"),
* workspace: z.string().optional(),
* });
* ```
*/
export { z } from "zod";
export { PLUGIN_API_VERSION, PLUGIN_STATUSES, PLUGIN_CATEGORIES, PLUGIN_CAPABILITIES, PLUGIN_UI_SLOT_TYPES, PLUGIN_UI_SLOT_ENTITY_TYPES, PLUGIN_STATE_SCOPE_KINDS, PLUGIN_JOB_STATUSES, PLUGIN_JOB_RUN_STATUSES, PLUGIN_JOB_RUN_TRIGGERS, PLUGIN_WEBHOOK_DELIVERY_STATUSES, PLUGIN_EVENT_TYPES, PLUGIN_BRIDGE_ERROR_CODES, } from "@paperclipai/shared";
//# sourceMappingURL=index.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}

84
node_modules/@paperclipai/plugin-sdk/dist/index.js generated vendored Normal file
View File

@@ -0,0 +1,84 @@
/**
* `@paperclipai/plugin-sdk` — Paperclip plugin worker-side SDK.
*
* This is the main entrypoint for plugin worker code. For plugin UI bundles,
* import from `@paperclipai/plugin-sdk/ui` instead.
*
* @example
* ```ts
* // Plugin worker entrypoint (dist/worker.ts)
* import { definePlugin, runWorker, z } from "@paperclipai/plugin-sdk";
*
* const plugin = definePlugin({
* async setup(ctx) {
* ctx.logger.info("Plugin starting up");
*
* ctx.events.on("issue.created", async (event) => {
* ctx.logger.info("Issue created", { issueId: event.entityId });
* });
*
* ctx.jobs.register("full-sync", async (job) => {
* ctx.logger.info("Starting full sync", { runId: job.runId });
* // ... sync implementation
* });
*
* ctx.data.register("sync-health", async ({ companyId }) => {
* const state = await ctx.state.get({
* scopeKind: "company",
* scopeId: String(companyId),
* stateKey: "last-sync-at",
* });
* return { lastSync: state };
* });
* },
*
* async onHealth() {
* return { status: "ok" };
* },
* });
*
* export default plugin;
* runWorker(plugin, import.meta.url);
* ```
*
* @see PLUGIN_SPEC.md §14 — SDK Surface
* @see PLUGIN_SPEC.md §29.2 — SDK Versioning
*/
// ---------------------------------------------------------------------------
// Main factory
// ---------------------------------------------------------------------------
export { definePlugin } from "./define-plugin.js";
export { createTestHarness } from "./testing.js";
export { createPluginBundlerPresets } from "./bundlers.js";
export { startPluginDevServer, getUiBuildSnapshot } from "./dev-server.js";
export { startWorkerRpcHost, runWorker } from "./worker-rpc-host.js";
export { createHostClientHandlers, getRequiredCapability, CapabilityDeniedError, } from "./host-client-factory.js";
// JSON-RPC protocol helpers and constants
export { JSONRPC_VERSION, JSONRPC_ERROR_CODES, PLUGIN_RPC_ERROR_CODES, HOST_TO_WORKER_REQUIRED_METHODS, HOST_TO_WORKER_OPTIONAL_METHODS, MESSAGE_DELIMITER, createRequest, createSuccessResponse, createErrorResponse, createNotification, isJsonRpcRequest, isJsonRpcNotification, isJsonRpcResponse, isJsonRpcSuccessResponse, isJsonRpcErrorResponse, serializeMessage, parseMessage, JsonRpcParseError, JsonRpcCallError, _resetIdCounter, } from "./protocol.js";
// ---------------------------------------------------------------------------
// Zod re-export
// ---------------------------------------------------------------------------
/**
* Zod is re-exported for plugin authors to use when defining their
* `instanceConfigSchema` and tool `parametersSchema`.
*
* Plugin authors do not need to add a separate `zod` dependency.
*
* @see PLUGIN_SPEC.md §14.1 — Example SDK Shape
*
* @example
* ```ts
* import { z } from "@paperclipai/plugin-sdk";
*
* const configSchema = z.object({
* apiKey: z.string().describe("Your API key"),
* workspace: z.string().optional(),
* });
* ```
*/
export { z } from "zod";
// ---------------------------------------------------------------------------
// Constants re-exports (for plugin code that needs to check values at runtime)
// ---------------------------------------------------------------------------
export { PLUGIN_API_VERSION, PLUGIN_STATUSES, PLUGIN_CATEGORIES, PLUGIN_CAPABILITIES, PLUGIN_UI_SLOT_TYPES, PLUGIN_UI_SLOT_ENTITY_TYPES, PLUGIN_STATE_SCOPE_KINDS, PLUGIN_JOB_STATUSES, PLUGIN_JOB_RUN_STATUSES, PLUGIN_JOB_RUN_TRIGGERS, PLUGIN_WEBHOOK_DELIVERY_STATUSES, PLUGIN_EVENT_TYPES, PLUGIN_BRIDGE_ERROR_CODES, } from "@paperclipai/shared";
//# sourceMappingURL=index.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AAEH,8EAA8E;AAC9E,eAAe;AACf,8EAA8E;AAE9E,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,EAAE,0BAA0B,EAAE,MAAM,eAAe,CAAC;AAC3D,OAAO,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAC3E,OAAO,EAAE,kBAAkB,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACrE,OAAO,EACL,wBAAwB,EACxB,qBAAqB,EACrB,qBAAqB,GACtB,MAAM,0BAA0B,CAAC;AAElC,0CAA0C;AAC1C,OAAO,EACL,eAAe,EACf,mBAAmB,EACnB,sBAAsB,EACtB,+BAA+B,EAC/B,+BAA+B,EAC/B,iBAAiB,EACjB,aAAa,EACb,qBAAqB,EACrB,mBAAmB,EACnB,kBAAkB,EAClB,gBAAgB,EAChB,qBAAqB,EACrB,iBAAiB,EACjB,wBAAwB,EACxB,sBAAsB,EACtB,gBAAgB,EAChB,YAAY,EACZ,iBAAiB,EACjB,gBAAgB,EAChB,eAAe,GAChB,MAAM,eAAe,CAAC;AA+JvB,8EAA8E;AAC9E,gBAAgB;AAChB,8EAA8E;AAE9E;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,8EAA8E;AAC9E,+EAA+E;AAC/E,8EAA8E;AAE9E,OAAO,EACL,kBAAkB,EAClB,eAAe,EACf,iBAAiB,EACjB,mBAAmB,EACnB,oBAAoB,EACpB,2BAA2B,EAC3B,wBAAwB,EACxB,mBAAmB,EACnB,uBAAuB,EACvB,uBAAuB,EACvB,gCAAgC,EAChC,kBAAkB,EAClB,yBAAyB,GAC1B,MAAM,qBAAqB,CAAC"}

881
node_modules/@paperclipai/plugin-sdk/dist/protocol.d.ts generated vendored Normal file
View File

@@ -0,0 +1,881 @@
/**
* 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, 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";
/** The JSON-RPC protocol version. Always `"2.0"`. */
export declare const JSONRPC_VERSION: "2.0";
/**
* 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;
/**
* Standard JSON-RPC 2.0 error codes.
*
* @see https://www.jsonrpc.org/specification#error_object
*/
export declare const JSONRPC_ERROR_CODES: {
/** Invalid JSON was received by the server. */
readonly PARSE_ERROR: -32700;
/** The JSON sent is not a valid Request object. */
readonly INVALID_REQUEST: -32600;
/** The method does not exist or is not available. */
readonly METHOD_NOT_FOUND: -32601;
/** Invalid method parameter(s). */
readonly INVALID_PARAMS: -32602;
/** Internal JSON-RPC error. */
readonly INTERNAL_ERROR: -32603;
};
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 declare const PLUGIN_RPC_ERROR_CODES: {
/** The worker process is not running or not reachable. */
readonly WORKER_UNAVAILABLE: -32000;
/** The plugin does not have the required capability for this operation. */
readonly CAPABILITY_DENIED: -32001;
/** The worker reported an unhandled error during method execution. */
readonly WORKER_ERROR: -32002;
/** The method call timed out waiting for the worker response. */
readonly TIMEOUT: -32003;
/** The worker does not implement the requested optional method. */
readonly METHOD_NOT_IMPLEMENTED: -32004;
/** A catch-all for errors that do not fit other categories. */
readonly UNKNOWN: -32099;
};
export type PluginRpcErrorCode = (typeof PLUGIN_RPC_ERROR_CODES)[keyof typeof PLUGIN_RPC_ERROR_CODES];
/**
* 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;
}
/**
* 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 declare const HOST_TO_WORKER_REQUIRED_METHODS: readonly HostToWorkerMethodName[];
/** Optional methods the worker MAY implement. */
export declare const HOST_TO_WORKER_OPTIONAL_METHODS: readonly HostToWorkerMethodName[];
/**
* 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.get": [params: Record<string, never>, result: Record<string, unknown>];
"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.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.emit": [
params: {
name: string;
companyId: string;
payload: unknown;
},
result: void
];
"events.subscribe": [
params: {
eventPattern: string;
filter?: Record<string, unknown> | null;
},
result: void
];
"http.fetch": [
params: {
url: string;
init?: Record<string, unknown>;
},
result: {
status: number;
statusText: string;
headers: Record<string, string>;
body: string;
}
];
"secrets.resolve": [
params: {
secretRef: string;
},
result: string
];
"activity.log": [
params: {
companyId: string;
message: string;
entityType?: string;
entityId?: string;
metadata?: Record<string, unknown>;
},
result: void
];
"metrics.write": [
params: {
name: string;
value: number;
tags?: Record<string, string>;
},
result: void
];
"log": [
params: {
level: "info" | "warn" | "error" | "debug";
message: string;
meta?: Record<string, unknown>;
},
result: void
];
"companies.list": [
params: {
limit?: number;
offset?: number;
},
result: Company[]
];
"companies.get": [
params: {
companyId: string;
},
result: Company | null
];
"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.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;
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;
},
result: IssueComment
];
"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.list": [
params: {
companyId: string;
status?: string;
limit?: number;
offset?: number;
},
result: Agent[]
];
"agents.get": [
params: {
agentId: string;
companyId: string;
},
result: Agent | null
];
"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;
}
];
"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.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;
/**
* 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;
/**
* 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]>;
/**
* 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 declare function createRequest<TMethod extends string>(method: TMethod, params: unknown, id?: JsonRpcId): JsonRpcRequest<TMethod>;
/**
* Create a JSON-RPC 2.0 success response.
*
* @param id - The request ID being responded to
* @param result - The result value
*/
export declare function createSuccessResponse<TResult>(id: JsonRpcId, result: TResult): JsonRpcSuccessResponse<TResult>;
/**
* 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 declare function createErrorResponse<TData = unknown>(id: JsonRpcId | null, code: number, message: string, data?: TData): JsonRpcErrorResponse<TData>;
/**
* Create a JSON-RPC 2.0 notification (fire-and-forget, no response expected).
*
* @param method - The notification method name
* @param params - Structured parameters
*/
export declare function createNotification<TMethod extends string>(method: TMethod, params: unknown): JsonRpcNotification<TMethod>;
/**
* 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 declare function isJsonRpcRequest(value: unknown): value is JsonRpcRequest;
/**
* 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 declare function isJsonRpcNotification(value: unknown): value is JsonRpcNotification;
/**
* Check whether a value is a well-formed JSON-RPC 2.0 response (success or error).
*/
export declare function isJsonRpcResponse(value: unknown): value is JsonRpcResponse;
/**
* Check whether a JSON-RPC response is a success response.
*/
export declare function isJsonRpcSuccessResponse(response: JsonRpcResponse): response is JsonRpcSuccessResponse;
/**
* Check whether a JSON-RPC response is an error response.
*/
export declare function isJsonRpcErrorResponse(response: JsonRpcResponse): response is JsonRpcErrorResponse;
/**
* 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 declare const MESSAGE_DELIMITER: "\n";
/**
* 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 declare function serializeMessage(message: JsonRpcMessage): string;
/**
* 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 declare function parseMessage(line: string): JsonRpcMessage;
/**
* Error thrown when a JSON-RPC message cannot be parsed.
*/
export declare class JsonRpcParseError extends Error {
readonly name = "JsonRpcParseError";
constructor(message: string);
}
/**
* 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 declare class JsonRpcCallError extends Error {
readonly name = "JsonRpcCallError";
/** The JSON-RPC error code. */
readonly code: number;
/** Optional structured error data from the response. */
readonly data: unknown;
constructor(error: JsonRpcError);
}
/**
* Reset the internal request ID counter. **For testing only.**
*
* @internal
*/
export declare function _resetIdCounter(): void;
//# sourceMappingURL=protocol.d.ts.map

File diff suppressed because one or more lines are too long

297
node_modules/@paperclipai/plugin-sdk/dist/protocol.js generated vendored Normal file
View File

@@ -0,0 +1,297 @@
/**
* 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
*/
// ---------------------------------------------------------------------------
// JSON-RPC 2.0 — Core Protocol Types
// ---------------------------------------------------------------------------
/** The JSON-RPC protocol version. Always `"2.0"`. */
export const JSONRPC_VERSION = "2.0";
// ---------------------------------------------------------------------------
// 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,
};
/**
* 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,
};
/** Required methods the worker MUST implement. */
export const HOST_TO_WORKER_REQUIRED_METHODS = [
"initialize",
"health",
"shutdown",
];
/** Optional methods the worker MAY implement. */
export const HOST_TO_WORKER_OPTIONAL_METHODS = [
"validateConfig",
"configChanged",
"onEvent",
"runJob",
"handleWebhook",
"getData",
"performAction",
"executeTool",
];
// ---------------------------------------------------------------------------
// 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(method, params, id) {
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(id, result) {
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(id, code, message, data) {
const response = {
jsonrpc: JSONRPC_VERSION,
id,
error: data !== undefined
? { code, message, data }
: { code, message },
};
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(method, params) {
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) {
if (typeof value !== "object" || value === null)
return false;
const obj = value;
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) {
if (typeof value !== "object" || value === null)
return false;
const obj = value;
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) {
if (typeof value !== "object" || value === null)
return false;
const obj = value;
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) {
return "result" in response && !("error" in response && response.error !== undefined);
}
/**
* Check whether a JSON-RPC response is an error response.
*/
export function isJsonRpcErrorResponse(response) {
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";
/**
* 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) {
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) {
const trimmed = line.trim();
if (trimmed.length === 0) {
throw new JsonRpcParseError("Empty message");
}
let parsed;
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;
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;
}
// ---------------------------------------------------------------------------
// Error Classes
// ---------------------------------------------------------------------------
/**
* Error thrown when a JSON-RPC message cannot be parsed.
*/
export class JsonRpcParseError extends Error {
name = "JsonRpcParseError";
constructor(message) {
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 {
name = "JsonRpcCallError";
/** The JSON-RPC error code. */
code;
/** Optional structured error data from the response. */
data;
constructor(error) {
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() {
_nextId = 1;
}
//# sourceMappingURL=protocol.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"protocol.js","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"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"}

63
node_modules/@paperclipai/plugin-sdk/dist/testing.d.ts generated vendored Normal file
View File

@@ -0,0 +1,63 @@
import type { PaperclipPluginManifestV1, PluginCapability, PluginEventType, Company, Project, Issue, IssueComment, Agent, Goal } from "@paperclipai/shared";
import type { PluginContext, PluginJobContext, PluginEvent, ScopeKey, ToolResult, ToolRunContext, AgentSessionEvent } from "./types.js";
export interface TestHarnessOptions {
/** Plugin manifest used to seed capability checks and metadata. */
manifest: PaperclipPluginManifestV1;
/** Optional capability override. Defaults to `manifest.capabilities`. */
capabilities?: PluginCapability[];
/** Initial config returned by `ctx.config.get()`. */
config?: Record<string, unknown>;
}
export interface TestHarnessLogEntry {
level: "info" | "warn" | "error" | "debug";
message: string;
meta?: Record<string, unknown>;
}
export interface TestHarness {
/** Fully-typed in-memory plugin context passed to `plugin.setup(ctx)`. */
ctx: PluginContext;
/** Seed host entities for `ctx.companies/projects/issues/agents/goals` reads. */
seed(input: {
companies?: Company[];
projects?: Project[];
issues?: Issue[];
issueComments?: IssueComment[];
agents?: Agent[];
goals?: Goal[];
}): void;
setConfig(config: Record<string, unknown>): void;
/** Dispatch a host or plugin event to registered handlers. */
emit(eventType: PluginEventType | `plugin.${string}`, payload: unknown, base?: Partial<PluginEvent>): Promise<void>;
/** Execute a previously-registered scheduled job handler. */
runJob(jobKey: string, partial?: Partial<PluginJobContext>): Promise<void>;
/** Invoke a `ctx.data.register(...)` handler by key. */
getData<T = unknown>(key: string, params?: Record<string, unknown>): Promise<T>;
/** Invoke a `ctx.actions.register(...)` handler by key. */
performAction<T = unknown>(key: string, params?: Record<string, unknown>): Promise<T>;
/** Execute a registered tool handler via `ctx.tools.execute(...)`. */
executeTool<T = ToolResult>(name: string, params: unknown, runCtx?: Partial<ToolRunContext>): Promise<T>;
/** Read raw in-memory state for assertions. */
getState(input: ScopeKey): unknown;
/** Simulate a streaming event arriving for an active session. */
simulateSessionEvent(sessionId: string, event: Omit<AgentSessionEvent, "sessionId">): void;
logs: TestHarnessLogEntry[];
activity: Array<{
message: string;
entityType?: string;
entityId?: string;
metadata?: Record<string, unknown>;
}>;
metrics: Array<{
name: string;
value: number;
tags?: Record<string, string>;
}>;
}
/**
* Create an in-memory host harness for plugin worker tests.
*
* The harness enforces declared capabilities and simulates host APIs, so tests
* can validate plugin behavior without spinning up the Paperclip server runtime.
*/
export declare function createTestHarness(options: TestHarnessOptions): TestHarness;
//# sourceMappingURL=testing.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"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"}

700
node_modules/@paperclipai/plugin-sdk/dist/testing.js generated vendored Normal file
View File

@@ -0,0 +1,700 @@
import { randomUUID } from "node:crypto";
function normalizeScope(input) {
return {
scopeKind: input.scopeKind,
scopeId: input.scopeId,
namespace: input.namespace ?? "default",
stateKey: input.stateKey,
};
}
function stateMapKey(input) {
const normalized = normalizeScope(input);
return `${normalized.scopeKind}|${normalized.scopeId ?? ""}|${normalized.namespace}|${normalized.stateKey}`;
}
function allowsEvent(filter, event) {
if (!filter)
return true;
if (filter.companyId && filter.companyId !== String(event.payload?.companyId ?? ""))
return false;
if (filter.projectId && filter.projectId !== String(event.payload?.projectId ?? ""))
return false;
if (filter.agentId && filter.agentId !== String(event.payload?.agentId ?? ""))
return false;
return true;
}
function requireCapability(manifest, allowed, capability) {
if (allowed.has(capability))
return;
throw new Error(`Plugin '${manifest.id}' is missing required capability '${capability}' in test harness`);
}
function requireCompanyId(companyId) {
if (!companyId)
throw new Error("companyId is required for this operation");
return companyId;
}
function isInCompany(record, companyId) {
return Boolean(record && record.companyId === companyId);
}
/**
* Create an in-memory host harness for plugin worker tests.
*
* The harness enforces declared capabilities and simulates host APIs, so tests
* can validate plugin behavior without spinning up the Paperclip server runtime.
*/
export function createTestHarness(options) {
const manifest = options.manifest;
const capabilitySet = new Set(options.capabilities ?? manifest.capabilities);
let currentConfig = { ...(options.config ?? {}) };
const logs = [];
const activity = [];
const metrics = [];
const state = new Map();
const entities = new Map();
const entityExternalIndex = new Map();
const companies = new Map();
const projects = new Map();
const issues = new Map();
const issueComments = new Map();
const agents = new Map();
const goals = new Map();
const projectWorkspaces = new Map();
const sessions = new Map();
const sessionEventCallbacks = new Map();
const events = [];
const jobs = new Map();
const launchers = new Map();
const dataHandlers = new Map();
const actionHandlers = new Map();
const toolHandlers = new Map();
const ctx = {
manifest,
config: {
async get() {
return { ...currentConfig };
},
},
events: {
on(name, filterOrFn, maybeFn) {
requireCapability(manifest, capabilitySet, "events.subscribe");
let registration;
if (typeof filterOrFn === "function") {
registration = { name, fn: filterOrFn };
}
else {
if (!maybeFn)
throw new Error("event handler is required");
registration = { name, filter: filterOrFn, fn: maybeFn };
}
events.push(registration);
return () => {
const idx = events.indexOf(registration);
if (idx !== -1)
events.splice(idx, 1);
};
},
async emit(name, companyId, payload) {
requireCapability(manifest, capabilitySet, "events.emit");
await harness.emit(`plugin.${manifest.id}.${name}`, payload, { companyId });
},
},
jobs: {
register(key, fn) {
requireCapability(manifest, capabilitySet, "jobs.schedule");
jobs.set(key, fn);
},
},
launchers: {
register(launcher) {
launchers.set(launcher.id, launcher);
},
},
http: {
async fetch(url, init) {
requireCapability(manifest, capabilitySet, "http.outbound");
return fetch(url, init);
},
},
secrets: {
async resolve(secretRef) {
requireCapability(manifest, capabilitySet, "secrets.read-ref");
return `resolved:${secretRef}`;
},
},
activity: {
async log(entry) {
requireCapability(manifest, capabilitySet, "activity.log.write");
activity.push(entry);
},
},
state: {
async get(input) {
requireCapability(manifest, capabilitySet, "plugin.state.read");
return state.has(stateMapKey(input)) ? state.get(stateMapKey(input)) : null;
},
async set(input, value) {
requireCapability(manifest, capabilitySet, "plugin.state.write");
state.set(stateMapKey(input), value);
},
async delete(input) {
requireCapability(manifest, capabilitySet, "plugin.state.write");
state.delete(stateMapKey(input));
},
},
entities: {
async upsert(input) {
const externalKey = input.externalId
? `${input.entityType}|${input.scopeKind}|${input.scopeId ?? ""}|${input.externalId}`
: null;
const existingId = externalKey ? entityExternalIndex.get(externalKey) : undefined;
const existing = existingId ? entities.get(existingId) : undefined;
const now = new Date().toISOString();
const previousExternalKey = existing?.externalId
? `${existing.entityType}|${existing.scopeKind}|${existing.scopeId ?? ""}|${existing.externalId}`
: null;
const record = existing
? {
...existing,
entityType: input.entityType,
scopeKind: input.scopeKind,
scopeId: input.scopeId ?? null,
externalId: input.externalId ?? null,
title: input.title ?? null,
status: input.status ?? null,
data: input.data,
updatedAt: now,
}
: {
id: randomUUID(),
entityType: input.entityType,
scopeKind: input.scopeKind,
scopeId: input.scopeId ?? null,
externalId: input.externalId ?? null,
title: input.title ?? null,
status: input.status ?? null,
data: input.data,
createdAt: now,
updatedAt: now,
};
entities.set(record.id, record);
if (previousExternalKey && previousExternalKey !== externalKey) {
entityExternalIndex.delete(previousExternalKey);
}
if (externalKey)
entityExternalIndex.set(externalKey, record.id);
return record;
},
async list(query) {
let out = [...entities.values()];
if (query.entityType)
out = out.filter((r) => r.entityType === query.entityType);
if (query.scopeKind)
out = out.filter((r) => r.scopeKind === query.scopeKind);
if (query.scopeId)
out = out.filter((r) => r.scopeId === query.scopeId);
if (query.externalId)
out = out.filter((r) => r.externalId === query.externalId);
if (query.offset)
out = out.slice(query.offset);
if (query.limit)
out = out.slice(0, query.limit);
return out;
},
},
projects: {
async list(input) {
requireCapability(manifest, capabilitySet, "projects.read");
const companyId = requireCompanyId(input?.companyId);
let out = [...projects.values()];
out = out.filter((project) => project.companyId === companyId);
if (input?.offset)
out = out.slice(input.offset);
if (input?.limit)
out = out.slice(0, input.limit);
return out;
},
async get(projectId, companyId) {
requireCapability(manifest, capabilitySet, "projects.read");
const project = projects.get(projectId);
return isInCompany(project, companyId) ? project : null;
},
async listWorkspaces(projectId, companyId) {
requireCapability(manifest, capabilitySet, "project.workspaces.read");
if (!isInCompany(projects.get(projectId), companyId))
return [];
return projectWorkspaces.get(projectId) ?? [];
},
async getPrimaryWorkspace(projectId, companyId) {
requireCapability(manifest, capabilitySet, "project.workspaces.read");
if (!isInCompany(projects.get(projectId), companyId))
return null;
const workspaces = projectWorkspaces.get(projectId) ?? [];
return workspaces.find((workspace) => workspace.isPrimary) ?? null;
},
async getWorkspaceForIssue(issueId, companyId) {
requireCapability(manifest, capabilitySet, "project.workspaces.read");
const issue = issues.get(issueId);
if (!isInCompany(issue, companyId))
return null;
const projectId = issue?.projectId;
if (!projectId)
return null;
if (!isInCompany(projects.get(projectId), companyId))
return null;
const workspaces = projectWorkspaces.get(projectId) ?? [];
return workspaces.find((workspace) => workspace.isPrimary) ?? null;
},
},
companies: {
async list(input) {
requireCapability(manifest, capabilitySet, "companies.read");
let out = [...companies.values()];
if (input?.offset)
out = out.slice(input.offset);
if (input?.limit)
out = out.slice(0, input.limit);
return out;
},
async get(companyId) {
requireCapability(manifest, capabilitySet, "companies.read");
return companies.get(companyId) ?? null;
},
},
issues: {
async list(input) {
requireCapability(manifest, capabilitySet, "issues.read");
const companyId = requireCompanyId(input?.companyId);
let out = [...issues.values()];
out = out.filter((issue) => issue.companyId === companyId);
if (input?.projectId)
out = out.filter((issue) => issue.projectId === input.projectId);
if (input?.assigneeAgentId)
out = out.filter((issue) => issue.assigneeAgentId === input.assigneeAgentId);
if (input?.status)
out = out.filter((issue) => issue.status === input.status);
if (input?.offset)
out = out.slice(input.offset);
if (input?.limit)
out = out.slice(0, input.limit);
return out;
},
async get(issueId, companyId) {
requireCapability(manifest, capabilitySet, "issues.read");
const issue = issues.get(issueId);
return isInCompany(issue, companyId) ? issue : null;
},
async create(input) {
requireCapability(manifest, capabilitySet, "issues.create");
const now = new Date();
const record = {
id: randomUUID(),
companyId: input.companyId,
projectId: input.projectId ?? null,
projectWorkspaceId: null,
goalId: input.goalId ?? null,
parentId: input.parentId ?? null,
title: input.title,
description: input.description ?? null,
status: "todo",
priority: input.priority ?? "medium",
assigneeAgentId: input.assigneeAgentId ?? null,
assigneeUserId: null,
checkoutRunId: null,
executionRunId: null,
executionAgentNameKey: null,
executionLockedAt: null,
createdByAgentId: null,
createdByUserId: null,
issueNumber: null,
identifier: null,
requestDepth: 0,
billingCode: null,
assigneeAdapterOverrides: null,
executionWorkspaceId: null,
executionWorkspacePreference: null,
executionWorkspaceSettings: null,
startedAt: null,
completedAt: null,
cancelledAt: null,
hiddenAt: null,
createdAt: now,
updatedAt: now,
};
issues.set(record.id, record);
return record;
},
async update(issueId, patch, companyId) {
requireCapability(manifest, capabilitySet, "issues.update");
const record = issues.get(issueId);
if (!isInCompany(record, companyId))
throw new Error(`Issue not found: ${issueId}`);
const updated = {
...record,
...patch,
updatedAt: new Date(),
};
issues.set(issueId, updated);
return updated;
},
async listComments(issueId, companyId) {
requireCapability(manifest, capabilitySet, "issue.comments.read");
if (!isInCompany(issues.get(issueId), companyId))
return [];
return issueComments.get(issueId) ?? [];
},
async createComment(issueId, body, companyId) {
requireCapability(manifest, capabilitySet, "issue.comments.create");
const parentIssue = issues.get(issueId);
if (!isInCompany(parentIssue, companyId)) {
throw new Error(`Issue not found: ${issueId}`);
}
const now = new Date();
const comment = {
id: randomUUID(),
companyId: parentIssue.companyId,
issueId,
authorAgentId: null,
authorUserId: null,
body,
createdAt: now,
updatedAt: now,
};
const current = issueComments.get(issueId) ?? [];
current.push(comment);
issueComments.set(issueId, current);
return comment;
},
documents: {
async list(issueId, companyId) {
requireCapability(manifest, capabilitySet, "issue.documents.read");
if (!isInCompany(issues.get(issueId), companyId))
return [];
return [];
},
async get(issueId, _key, companyId) {
requireCapability(manifest, capabilitySet, "issue.documents.read");
if (!isInCompany(issues.get(issueId), companyId))
return null;
return null;
},
async upsert(input) {
requireCapability(manifest, capabilitySet, "issue.documents.write");
const parentIssue = issues.get(input.issueId);
if (!isInCompany(parentIssue, input.companyId)) {
throw new Error(`Issue not found: ${input.issueId}`);
}
throw new Error("documents.upsert is not implemented in test context");
},
async delete(issueId, _key, companyId) {
requireCapability(manifest, capabilitySet, "issue.documents.write");
const parentIssue = issues.get(issueId);
if (!isInCompany(parentIssue, companyId)) {
throw new Error(`Issue not found: ${issueId}`);
}
},
},
},
agents: {
async list(input) {
requireCapability(manifest, capabilitySet, "agents.read");
const companyId = requireCompanyId(input?.companyId);
let out = [...agents.values()];
out = out.filter((agent) => agent.companyId === companyId);
if (input?.status)
out = out.filter((agent) => agent.status === input.status);
if (input?.offset)
out = out.slice(input.offset);
if (input?.limit)
out = out.slice(0, input.limit);
return out;
},
async get(agentId, companyId) {
requireCapability(manifest, capabilitySet, "agents.read");
const agent = agents.get(agentId);
return isInCompany(agent, companyId) ? agent : null;
},
async pause(agentId, companyId) {
requireCapability(manifest, capabilitySet, "agents.pause");
const cid = requireCompanyId(companyId);
const agent = agents.get(agentId);
if (!isInCompany(agent, cid))
throw new Error(`Agent not found: ${agentId}`);
if (agent.status === "terminated")
throw new Error("Cannot pause terminated agent");
const updated = { ...agent, status: "paused", updatedAt: new Date() };
agents.set(agentId, updated);
return updated;
},
async resume(agentId, companyId) {
requireCapability(manifest, capabilitySet, "agents.resume");
const cid = requireCompanyId(companyId);
const agent = agents.get(agentId);
if (!isInCompany(agent, cid))
throw new Error(`Agent not found: ${agentId}`);
if (agent.status === "terminated")
throw new Error("Cannot resume terminated agent");
if (agent.status === "pending_approval")
throw new Error("Pending approval agents cannot be resumed");
const updated = { ...agent, status: "idle", updatedAt: new Date() };
agents.set(agentId, updated);
return updated;
},
async invoke(agentId, companyId, opts) {
requireCapability(manifest, capabilitySet, "agents.invoke");
const cid = requireCompanyId(companyId);
const agent = agents.get(agentId);
if (!isInCompany(agent, cid))
throw new Error(`Agent not found: ${agentId}`);
if (agent.status === "paused" ||
agent.status === "terminated" ||
agent.status === "pending_approval") {
throw new Error(`Agent is not invokable in its current state: ${agent.status}`);
}
return { runId: randomUUID() };
},
sessions: {
async create(agentId, companyId, opts) {
requireCapability(manifest, capabilitySet, "agent.sessions.create");
const cid = requireCompanyId(companyId);
const agent = agents.get(agentId);
if (!isInCompany(agent, cid))
throw new Error(`Agent not found: ${agentId}`);
const session = {
sessionId: randomUUID(),
agentId,
companyId: cid,
status: "active",
createdAt: new Date().toISOString(),
};
sessions.set(session.sessionId, session);
return session;
},
async list(agentId, companyId) {
requireCapability(manifest, capabilitySet, "agent.sessions.list");
const cid = requireCompanyId(companyId);
return [...sessions.values()].filter((s) => s.agentId === agentId && s.companyId === cid && s.status === "active");
},
async sendMessage(sessionId, companyId, opts) {
requireCapability(manifest, capabilitySet, "agent.sessions.send");
const session = sessions.get(sessionId);
if (!session || session.status !== "active")
throw new Error(`Session not found or closed: ${sessionId}`);
if (session.companyId !== companyId)
throw new Error(`Session not found: ${sessionId}`);
if (opts.onEvent) {
sessionEventCallbacks.set(sessionId, opts.onEvent);
}
return { runId: randomUUID() };
},
async close(sessionId, companyId) {
requireCapability(manifest, capabilitySet, "agent.sessions.close");
const session = sessions.get(sessionId);
if (!session)
throw new Error(`Session not found: ${sessionId}`);
if (session.companyId !== companyId)
throw new Error(`Session not found: ${sessionId}`);
session.status = "closed";
sessionEventCallbacks.delete(sessionId);
},
},
},
goals: {
async list(input) {
requireCapability(manifest, capabilitySet, "goals.read");
const companyId = requireCompanyId(input?.companyId);
let out = [...goals.values()];
out = out.filter((goal) => goal.companyId === companyId);
if (input?.level)
out = out.filter((goal) => goal.level === input.level);
if (input?.status)
out = out.filter((goal) => goal.status === input.status);
if (input?.offset)
out = out.slice(input.offset);
if (input?.limit)
out = out.slice(0, input.limit);
return out;
},
async get(goalId, companyId) {
requireCapability(manifest, capabilitySet, "goals.read");
const goal = goals.get(goalId);
return isInCompany(goal, companyId) ? goal : null;
},
async create(input) {
requireCapability(manifest, capabilitySet, "goals.create");
const now = new Date();
const record = {
id: randomUUID(),
companyId: input.companyId,
title: input.title,
description: input.description ?? null,
level: input.level ?? "task",
status: input.status ?? "planned",
parentId: input.parentId ?? null,
ownerAgentId: input.ownerAgentId ?? null,
createdAt: now,
updatedAt: now,
};
goals.set(record.id, record);
return record;
},
async update(goalId, patch, companyId) {
requireCapability(manifest, capabilitySet, "goals.update");
const record = goals.get(goalId);
if (!isInCompany(record, companyId))
throw new Error(`Goal not found: ${goalId}`);
const updated = {
...record,
...patch,
updatedAt: new Date(),
};
goals.set(goalId, updated);
return updated;
},
},
data: {
register(key, handler) {
dataHandlers.set(key, handler);
},
},
actions: {
register(key, handler) {
actionHandlers.set(key, handler);
},
},
streams: (() => {
const channelCompanyMap = new Map();
return {
open(channel, companyId) {
channelCompanyMap.set(channel, companyId);
},
emit(_channel, _event) {
// No-op in test harness — events are not forwarded
},
close(channel) {
channelCompanyMap.delete(channel);
},
};
})(),
tools: {
register(name, _decl, fn) {
requireCapability(manifest, capabilitySet, "agent.tools.register");
toolHandlers.set(name, fn);
},
},
metrics: {
async write(name, value, tags) {
requireCapability(manifest, capabilitySet, "metrics.write");
metrics.push({ name, value, tags });
},
},
logger: {
info(message, meta) {
logs.push({ level: "info", message, meta });
},
warn(message, meta) {
logs.push({ level: "warn", message, meta });
},
error(message, meta) {
logs.push({ level: "error", message, meta });
},
debug(message, meta) {
logs.push({ level: "debug", message, meta });
},
},
};
const harness = {
ctx,
seed(input) {
for (const row of input.companies ?? [])
companies.set(row.id, row);
for (const row of input.projects ?? [])
projects.set(row.id, row);
for (const row of input.issues ?? [])
issues.set(row.id, row);
for (const row of input.issueComments ?? []) {
const list = issueComments.get(row.issueId) ?? [];
list.push(row);
issueComments.set(row.issueId, list);
}
for (const row of input.agents ?? [])
agents.set(row.id, row);
for (const row of input.goals ?? [])
goals.set(row.id, row);
},
setConfig(config) {
currentConfig = { ...config };
},
async emit(eventType, payload, base) {
const event = {
eventId: base?.eventId ?? randomUUID(),
eventType,
companyId: base?.companyId ?? "test-company",
occurredAt: base?.occurredAt ?? new Date().toISOString(),
actorId: base?.actorId,
actorType: base?.actorType,
entityId: base?.entityId,
entityType: base?.entityType,
payload,
};
for (const handler of events) {
const exactMatch = handler.name === event.eventType;
const wildcardPluginAll = handler.name === "plugin.*" && String(event.eventType).startsWith("plugin.");
const wildcardPluginOne = String(handler.name).endsWith(".*")
&& String(event.eventType).startsWith(String(handler.name).slice(0, -1));
if (!exactMatch && !wildcardPluginAll && !wildcardPluginOne)
continue;
if (!allowsEvent(handler.filter, event))
continue;
await handler.fn(event);
}
},
async runJob(jobKey, partial = {}) {
const handler = jobs.get(jobKey);
if (!handler)
throw new Error(`No job handler registered for '${jobKey}'`);
await handler({
jobKey,
runId: partial.runId ?? randomUUID(),
trigger: partial.trigger ?? "manual",
scheduledAt: partial.scheduledAt ?? new Date().toISOString(),
});
},
async getData(key, params = {}) {
const handler = dataHandlers.get(key);
if (!handler)
throw new Error(`No data handler registered for '${key}'`);
return await handler(params);
},
async performAction(key, params = {}) {
const handler = actionHandlers.get(key);
if (!handler)
throw new Error(`No action handler registered for '${key}'`);
return await handler(params);
},
async executeTool(name, params, runCtx = {}) {
const handler = toolHandlers.get(name);
if (!handler)
throw new Error(`No tool handler registered for '${name}'`);
const ctxToPass = {
agentId: runCtx.agentId ?? "agent-test",
runId: runCtx.runId ?? randomUUID(),
companyId: runCtx.companyId ?? "company-test",
projectId: runCtx.projectId ?? "project-test",
};
return await handler(params, ctxToPass);
},
getState(input) {
return state.get(stateMapKey(input));
},
simulateSessionEvent(sessionId, event) {
const cb = sessionEventCallbacks.get(sessionId);
if (!cb)
throw new Error(`No active session event callback for session: ${sessionId}`);
cb({ ...event, sessionId });
},
logs,
activity,
metrics,
};
return harness;
}
//# sourceMappingURL=testing.js.map

File diff suppressed because one or more lines are too long

982
node_modules/@paperclipai/plugin-sdk/dist/types.d.ts generated vendored Normal file
View File

@@ -0,0 +1,982 @@
/**
* Core types for the Paperclip plugin worker-side SDK.
*
* These types define the stable public API surface that plugin workers import
* from `@paperclipai/plugin-sdk`. The host provides a concrete implementation
* of `PluginContext` to the plugin at initialisation time.
*
* @see PLUGIN_SPEC.md §14 — SDK Surface
* @see PLUGIN_SPEC.md §29.2 — SDK Versioning
*/
import type { PaperclipPluginManifestV1, PluginStateScopeKind, PluginEventType, PluginToolDeclaration, PluginLauncherDeclaration, Company, Project, Issue, IssueComment, IssueDocument, IssueDocumentSummary, Agent, Goal } from "@paperclipai/shared";
export type { PaperclipPluginManifestV1, PluginJobDeclaration, PluginWebhookDeclaration, PluginToolDeclaration, PluginUiSlotDeclaration, PluginUiDeclaration, PluginLauncherActionDeclaration, PluginLauncherRenderDeclaration, PluginLauncherDeclaration, PluginMinimumHostVersion, PluginRecord, PluginConfig, JsonSchema, PluginStatus, PluginCategory, PluginCapability, PluginUiSlotType, PluginUiSlotEntityType, PluginLauncherPlacementZone, PluginLauncherAction, PluginLauncherBounds, PluginLauncherRenderEnvironment, PluginStateScopeKind, PluginJobStatus, PluginJobRunStatus, PluginJobRunTrigger, PluginWebhookDeliveryStatus, PluginEventType, PluginBridgeErrorCode, Company, Project, Issue, IssueComment, IssueDocument, IssueDocumentSummary, Agent, Goal, } from "@paperclipai/shared";
/**
* A scope key identifies the exact location where plugin state is stored.
* Scope is partitioned by `scopeKind` and optional `scopeId`.
*
* Examples:
* - `{ scopeKind: "instance" }` — single global value for the whole instance
* - `{ scopeKind: "project", scopeId: "proj-uuid" }` — per-project state
* - `{ scopeKind: "issue", scopeId: "iss-uuid" }` — per-issue state
*
* @see PLUGIN_SPEC.md §21.3 `plugin_state`
*/
export interface ScopeKey {
/** What kind of Paperclip object this state is scoped to. */
scopeKind: PluginStateScopeKind;
/** UUID or text identifier for the scoped object. Omit for `instance` scope. */
scopeId?: string;
/** Optional sub-namespace within the scope to avoid key collisions. Defaults to `"default"`. */
namespace?: string;
/** The state key within the namespace. */
stateKey: string;
}
/**
* Optional filter applied when subscribing to an event. The host evaluates
* the filter server-side so filtered-out events never cross the process boundary.
*
* All filter fields are optional. If omitted the plugin receives every event
* of the subscribed type.
*
* @see PLUGIN_SPEC.md §16.1 — Event Filtering
*/
export interface EventFilter {
/** Only receive events for this project. */
projectId?: string;
/** Only receive events for this company. */
companyId?: string;
/** Only receive events for this agent. */
agentId?: string;
/** Additional arbitrary filter fields. */
[key: string]: unknown;
}
/**
* Envelope wrapping every domain event delivered to a plugin worker.
*
* @see PLUGIN_SPEC.md §16 — Event System
*/
export interface PluginEvent<TPayload = unknown> {
/** Unique event identifier (UUID). */
eventId: string;
/** The event type (e.g. `"issue.created"`). */
eventType: PluginEventType | `plugin.${string}`;
/** ISO 8601 timestamp when the event occurred. */
occurredAt: string;
/** ID of the actor that caused the event, if applicable. */
actorId?: string;
/** Type of actor: `"user"`, `"agent"`, `"system"`, or `"plugin"`. */
actorType?: "user" | "agent" | "system" | "plugin";
/** Primary entity involved in the event. */
entityId?: string;
/** Type of the primary entity. */
entityType?: string;
/** UUID of the company this event belongs to. */
companyId: string;
/** Typed event payload. */
payload: TPayload;
}
/**
* Context passed to a plugin job handler when the host triggers a scheduled run.
*
* @see PLUGIN_SPEC.md §13.6 — `runJob`
*/
export interface PluginJobContext {
/** Stable job key matching the declaration in the manifest. */
jobKey: string;
/** UUID for this specific job run instance. */
runId: string;
/** What triggered this run. */
trigger: "schedule" | "manual" | "retry";
/** ISO 8601 timestamp when the run was scheduled to start. */
scheduledAt: string;
}
/**
* Run context passed to a plugin tool handler when an agent invokes the tool.
*
* @see PLUGIN_SPEC.md §13.10 — `executeTool`
*/
export interface ToolRunContext {
/** UUID of the agent invoking the tool. */
agentId: string;
/** UUID of the current agent run. */
runId: string;
/** UUID of the company the run belongs to. */
companyId: string;
/** UUID of the project the run belongs to. */
projectId: string;
}
/**
* Result returned from a plugin tool handler.
*
* @see PLUGIN_SPEC.md §13.10 — `executeTool`
*/
export interface ToolResult {
/** String content returned to the agent. Required for success responses. */
content?: string;
/** Structured data returned alongside or instead of string content. */
data?: unknown;
/** If present, indicates the tool call failed. */
error?: string;
}
/**
* Input for creating or updating a plugin-owned entity.
*
* @see PLUGIN_SPEC.md §21.3 `plugin_entities`
*/
export interface PluginEntityUpsert {
/** Plugin-defined entity type (e.g. `"linear-issue"`, `"github-pr"`). */
entityType: string;
/** Scope where this entity lives. */
scopeKind: PluginStateScopeKind;
/** Optional scope ID. */
scopeId?: string;
/** External identifier in the remote system (e.g. Linear issue ID). */
externalId?: string;
/** Human-readable title for display in the Paperclip UI. */
title?: string;
/** Optional status string. */
status?: string;
/** Full entity data blob. Must be JSON-serializable. */
data: Record<string, unknown>;
}
/**
* A plugin-owned entity record as returned by `ctx.entities.list()`.
*
* @see PLUGIN_SPEC.md §21.3 `plugin_entities`
*/
export interface PluginEntityRecord {
/** UUID primary key. */
id: string;
/** Plugin-defined entity type. */
entityType: string;
/** Scope kind. */
scopeKind: PluginStateScopeKind;
/** Scope ID, if any. */
scopeId: string | null;
/** External identifier, if any. */
externalId: string | null;
/** Human-readable title. */
title: string | null;
/** Status string. */
status: string | null;
/** Full entity data. */
data: Record<string, unknown>;
/** ISO 8601 creation timestamp. */
createdAt: string;
/** ISO 8601 last-updated timestamp. */
updatedAt: string;
}
/**
* Query parameters for `ctx.entities.list()`.
*/
export interface PluginEntityQuery {
/** Filter by entity type. */
entityType?: string;
/** Filter by scope kind. */
scopeKind?: PluginStateScopeKind;
/** Filter by scope ID. */
scopeId?: string;
/** Filter by external ID. */
externalId?: string;
/** Maximum number of results to return. */
limit?: number;
/** Number of results to skip (for pagination). */
offset?: number;
}
/**
* Workspace metadata provided by the host. Plugins use this to resolve local
* filesystem paths for file browsing, git, terminal, and process operations.
*
* @see PLUGIN_SPEC.md §7 — Project Workspaces
* @see PLUGIN_SPEC.md §20 — Local Tooling
*/
export interface PluginWorkspace {
/** UUID primary key. */
id: string;
/** UUID of the parent project. */
projectId: string;
/** Display name for this workspace. */
name: string;
/** Absolute filesystem path to the workspace directory. */
path: string;
/** Whether this is the project's primary workspace. */
isPrimary: boolean;
/** ISO 8601 creation timestamp. */
createdAt: string;
/** ISO 8601 last-updated timestamp. */
updatedAt: string;
}
/**
* `ctx.config` — read resolved operator configuration for this plugin.
*
* Plugin workers receive the resolved config at initialisation. Use `get()`
* to access the current configuration at any time. The host calls
* `configChanged` on the worker when the operator updates config at runtime.
*
* @see PLUGIN_SPEC.md §13.3 — `validateConfig`
* @see PLUGIN_SPEC.md §13.4 — `configChanged`
*/
export interface PluginConfigClient {
/**
* Returns the resolved operator configuration for this plugin instance.
* Values are validated against the plugin's `instanceConfigSchema` by the
* host before being passed to the worker.
*/
get(): Promise<Record<string, unknown>>;
}
/**
* `ctx.events` — subscribe to and emit Paperclip domain events.
*
* Requires `events.subscribe` capability for `on()`.
* Requires `events.emit` capability for `emit()`.
*
* @see PLUGIN_SPEC.md §16 — Event System
*/
export interface PluginEventsClient {
/**
* Subscribe to a core Paperclip domain event or a plugin-namespaced event.
*
* @param name - Event type, e.g. `"issue.created"` or `"plugin.@acme/linear.sync-done"`
* @param fn - Async event handler
*/
on(name: PluginEventType | `plugin.${string}`, fn: (event: PluginEvent) => Promise<void>): () => void;
/**
* Subscribe to an event with an optional server-side filter.
*
* @param name - Event type
* @param filter - Server-side filter evaluated before dispatching to the worker
* @param fn - Async event handler
* @returns An unsubscribe function that removes the handler
*/
on(name: PluginEventType | `plugin.${string}`, filter: EventFilter, fn: (event: PluginEvent) => Promise<void>): () => void;
/**
* Emit a plugin-namespaced event. Other plugins with `events.subscribe` can
* subscribe to it using `"plugin.<pluginId>.<eventName>"`.
*
* Requires the `events.emit` capability.
*
* Plugin-emitted events are automatically namespaced: if the plugin ID is
* `"acme.linear"` and the event name is `"sync-done"`, the full event type
* becomes `"plugin.acme.linear.sync-done"`.
*
* @see PLUGIN_SPEC.md §16.2 — Plugin-to-Plugin Events
*
* @param name - Bare event name (e.g. `"sync-done"`)
* @param companyId - UUID of the company this event belongs to
* @param payload - JSON-serializable event payload
*/
emit(name: string, companyId: string, payload: unknown): Promise<void>;
}
/**
* `ctx.jobs` — register handlers for scheduled jobs declared in the manifest.
*
* Requires `jobs.schedule` capability.
*
* @see PLUGIN_SPEC.md §17 — Scheduled Jobs
*/
export interface PluginJobsClient {
/**
* Register a handler for a scheduled job.
*
* The `key` must match a `jobKey` declared in the plugin manifest.
* The host calls this handler according to the job's declared `schedule`.
*
* @param key - Job key matching the manifest declaration
* @param fn - Async job handler
*/
register(key: string, fn: (job: PluginJobContext) => Promise<void>): void;
}
/**
* A runtime launcher registration uses the same declaration shape as a
* manifest launcher entry.
*/
export type PluginLauncherRegistration = PluginLauncherDeclaration;
/**
* `ctx.launchers` — register launcher declarations at runtime.
*/
export interface PluginLaunchersClient {
/**
* Register launcher metadata for host discovery.
*
* If a launcher with the same id is registered more than once, the latest
* declaration replaces the previous one.
*/
register(launcher: PluginLauncherRegistration): void;
}
/**
* `ctx.http` — make outbound HTTP requests.
*
* Requires `http.outbound` capability.
*
* @see PLUGIN_SPEC.md §15.1 — Capabilities: Runtime/Integration
*/
export interface PluginHttpClient {
/**
* Perform an outbound HTTP request.
*
* The host enforces `http.outbound` capability before allowing the call.
* Plugins may also use standard Node `fetch` or other libraries directly —
* this client exists for host-managed tracing and audit logging.
*
* @param url - Target URL
* @param init - Standard `RequestInit` options
* @returns The response
*/
fetch(url: string, init?: RequestInit): Promise<Response>;
}
/**
* `ctx.secrets` — resolve secret references.
*
* Requires `secrets.read-ref` capability.
*
* Plugins store secret *references* in their config (e.g. a secret name).
* This client resolves the reference through the Paperclip secret provider
* system and returns the resolved value at execution time.
*
* @see PLUGIN_SPEC.md §22 — Secrets
*/
export interface PluginSecretsClient {
/**
* Resolve a secret reference to its current value.
*
* The reference is a string identifier pointing to a secret configured
* in the Paperclip secret provider (e.g. `"MY_API_KEY"`).
*
* Secret values are resolved at call time and must never be cached or
* written to logs, config, or other persistent storage.
*
* @param secretRef - The secret reference string from plugin config
* @returns The resolved secret value
*/
resolve(secretRef: string): Promise<string>;
}
/**
* Input for writing a plugin activity log entry.
*
* @see PLUGIN_SPEC.md §21.4 — Activity Log Changes
*/
export interface PluginActivityLogEntry {
/** UUID of the company this activity belongs to. Required for auditing. */
companyId: string;
/** Human-readable description of the activity. */
message: string;
/** Optional entity type this activity relates to. */
entityType?: string;
/** Optional entity ID this activity relates to. */
entityId?: string;
/** Optional additional metadata. */
metadata?: Record<string, unknown>;
}
/**
* `ctx.activity` — write plugin-originated activity log entries.
*
* Requires `activity.log.write` capability.
*
* @see PLUGIN_SPEC.md §21.4 — Activity Log Changes
*/
export interface PluginActivityClient {
/**
* Write an activity log entry attributed to this plugin.
*
* The host writes the entry with `actor_type = plugin` and
* `actor_id = <pluginId>`.
*
* @param entry - The activity log entry to write
*/
log(entry: PluginActivityLogEntry): Promise<void>;
}
/**
* `ctx.state` — read and write plugin-scoped key-value state.
*
* Each plugin gets an isolated namespace: state written by plugin A can never
* be read or overwritten by plugin B. Within a plugin, state is partitioned by
* a five-part composite key: `(pluginId, scopeKind, scopeId, namespace, stateKey)`.
*
* **Scope kinds**
*
* | `scopeKind` | `scopeId` | Typical use |
* |-------------|-----------|-------------|
* | `"instance"` | omit | Global flags, last full-sync timestamps |
* | `"company"` | company UUID | Per-company sync cursors |
* | `"project"` | project UUID | Per-project settings, branch tracking |
* | `"project_workspace"` | workspace UUID | Per-workspace state |
* | `"agent"` | agent UUID | Per-agent memory |
* | `"issue"` | issue UUID | Idempotency keys, linked external IDs |
* | `"goal"` | goal UUID | Per-goal progress |
* | `"run"` | run UUID | Per-run checkpoints |
*
* **Namespaces**
*
* The optional `namespace` field (default: `"default"`) lets you group related
* keys within a scope without risking collisions between different logical
* subsystems inside the same plugin.
*
* **Security**
*
* Never store resolved secret values. Store only secret references and resolve
* them at call time via `ctx.secrets.resolve()`.
*
* @example
* ```ts
* // Instance-global flag
* await ctx.state.set({ scopeKind: "instance", stateKey: "schema-version" }, 2);
*
* // Idempotency key per issue
* const synced = await ctx.state.get({ scopeKind: "issue", scopeId: issueId, stateKey: "synced-to-linear" });
* if (!synced) {
* await syncToLinear(issueId);
* await ctx.state.set({ scopeKind: "issue", scopeId: issueId, stateKey: "synced-to-linear" }, true);
* }
*
* // Per-project, namespaced for two integrations
* await ctx.state.set({ scopeKind: "project", scopeId: projectId, namespace: "linear", stateKey: "cursor" }, cursor);
* await ctx.state.set({ scopeKind: "project", scopeId: projectId, namespace: "github", stateKey: "last-event" }, eventId);
* ```
*
* `plugin.state.read` capability required for `get()`.
* `plugin.state.write` capability required for `set()` and `delete()`.
*
* @see PLUGIN_SPEC.md §21.3 `plugin_state`
*/
export interface PluginStateClient {
/**
* Read a state value.
*
* Returns the stored JSON value as-is, or `null` if no entry has been set
* for this scope+key combination. Falsy values (`false`, `0`, `""`) are
* returned correctly and are not confused with "not set".
*
* @param input - Scope key identifying the entry to read
* @returns The stored JSON value, or `null` if no value has been set
*/
get(input: ScopeKey): Promise<unknown>;
/**
* Write a state value. Creates the row if it does not exist; replaces it
* atomically (upsert) if it does. Safe to call concurrently.
*
* Any JSON-serializable value is accepted: objects, arrays, strings,
* numbers, booleans, and `null`.
*
* @param input - Scope key identifying the entry to write
* @param value - JSON-serializable value to store
*/
set(input: ScopeKey, value: unknown): Promise<void>;
/**
* Delete a state value. No-ops silently if the entry does not exist
* (idempotent by design — safe to call without prior `get()`).
*
* @param input - Scope key identifying the entry to delete
*/
delete(input: ScopeKey): Promise<void>;
}
/**
* `ctx.entities` — create and query plugin-owned entity records.
*
* @see PLUGIN_SPEC.md §21.3 `plugin_entities`
*/
export interface PluginEntitiesClient {
/**
* Create or update a plugin entity record (upsert by `externalId` within
* the given scope, or by `id` if provided).
*
* @param input - Entity data to upsert
*/
upsert(input: PluginEntityUpsert): Promise<PluginEntityRecord>;
/**
* Query plugin entity records.
*
* @param query - Filter criteria
* @returns Matching entity records
*/
list(query: PluginEntityQuery): Promise<PluginEntityRecord[]>;
}
/**
* `ctx.projects` — read project and workspace metadata.
*
* Requires `projects.read` capability.
* Requires `project.workspaces.read` capability for workspace operations.
*
* @see PLUGIN_SPEC.md §7 — Project Workspaces
*/
export interface PluginProjectsClient {
/**
* List projects visible to the plugin.
*
* Requires the `projects.read` capability.
*/
list(input: {
companyId: string;
limit?: number;
offset?: number;
}): Promise<Project[]>;
/**
* Get a single project by ID.
*
* Requires the `projects.read` capability.
*/
get(projectId: string, companyId: string): Promise<Project | null>;
/**
* List all workspaces attached to a project.
*
* @param projectId - UUID of the project
* @param companyId - UUID of the company that owns the project
* @returns All workspaces for the project, ordered with primary first
*/
listWorkspaces(projectId: string, companyId: string): Promise<PluginWorkspace[]>;
/**
* Get the primary workspace for a project.
*
* @param projectId - UUID of the project
* @param companyId - UUID of the company that owns the project
* @returns The primary workspace, or `null` if no workspace is configured
*/
getPrimaryWorkspace(projectId: string, companyId: string): Promise<PluginWorkspace | null>;
/**
* Resolve the primary workspace for an issue by looking up the issue's
* project and returning its primary workspace.
*
* This is a convenience method that combines `issues.get()` and
* `getPrimaryWorkspace()` in a single RPC call.
*
* @param issueId - UUID of the issue
* @param companyId - UUID of the company that owns the issue
* @returns The primary workspace for the issue's project, or `null` if
* the issue has no project or the project has no workspace
*
* @see PLUGIN_SPEC.md §20 — Local Tooling
*/
getWorkspaceForIssue(issueId: string, companyId: string): Promise<PluginWorkspace | null>;
}
/**
* `ctx.data` — register `getData` handlers that back `usePluginData()` in the
* plugin's frontend components.
*
* The plugin's UI calls `usePluginData(key, params)` which routes through the
* host bridge to the worker's registered handler.
*
* @see PLUGIN_SPEC.md §13.8 — `getData`
*/
export interface PluginDataClient {
/**
* Register a handler for a plugin-defined data key.
*
* @param key - Stable string identifier for this data type (e.g. `"sync-health"`)
* @param handler - Async function that receives request params and returns JSON-serializable data
*/
register(key: string, handler: (params: Record<string, unknown>) => Promise<unknown>): void;
}
/**
* `ctx.actions` — register `performAction` handlers that back
* `usePluginAction()` in the plugin's frontend components.
*
* @see PLUGIN_SPEC.md §13.9 — `performAction`
*/
export interface PluginActionsClient {
/**
* Register a handler for a plugin-defined action key.
*
* @param key - Stable string identifier for this action (e.g. `"resync"`)
* @param handler - Async function that receives action params and returns a result
*/
register(key: string, handler: (params: Record<string, unknown>) => Promise<unknown>): void;
}
/**
* `ctx.tools` — register handlers for agent tools declared in the manifest.
*
* Requires `agent.tools.register` capability.
*
* Tool names are automatically namespaced by plugin ID at runtime.
*
* @see PLUGIN_SPEC.md §11 — Agent Tools
*/
export interface PluginToolsClient {
/**
* Register a handler for a plugin-contributed agent tool.
*
* @param name - Tool name matching the manifest declaration (without namespace prefix)
* @param declaration - Tool metadata (displayName, description, parametersSchema)
* @param fn - Async handler that executes the tool
*/
register(name: string, declaration: Pick<PluginToolDeclaration, "displayName" | "description" | "parametersSchema">, fn: (params: unknown, runCtx: ToolRunContext) => Promise<ToolResult>): void;
}
/**
* `ctx.logger` — structured logging from the plugin worker.
*
* Log output is captured by the host, stored, and surfaced in the plugin
* health dashboard.
*
* @see PLUGIN_SPEC.md §26.1 — Logging
*/
export interface PluginLogger {
/** Log an informational message. */
info(message: string, meta?: Record<string, unknown>): void;
/** Log a warning. */
warn(message: string, meta?: Record<string, unknown>): void;
/** Log an error. */
error(message: string, meta?: Record<string, unknown>): void;
/** Log a debug message (may be suppressed in production). */
debug(message: string, meta?: Record<string, unknown>): void;
}
/**
* `ctx.metrics` — write plugin-contributed metrics.
*
* Requires `metrics.write` capability.
*
* @see PLUGIN_SPEC.md §15.1 — Capabilities: Data Write
*/
export interface PluginMetricsClient {
/**
* Write a numeric metric data point.
*
* @param name - Metric name (plugin-namespaced by the host)
* @param value - Numeric value
* @param tags - Optional key-value tags for filtering
*/
write(name: string, value: number, tags?: Record<string, string>): Promise<void>;
}
/**
* `ctx.companies` — read company metadata.
*
* Requires `companies.read` capability.
*/
export interface PluginCompaniesClient {
/**
* List companies visible to this plugin.
*/
list(input?: {
limit?: number;
offset?: number;
}): Promise<Company[]>;
/**
* Get one company by ID.
*/
get(companyId: string): Promise<Company | null>;
}
/**
* `ctx.issues.documents` — read and write issue documents.
*
* Requires:
* - `issue.documents.read` for `list` and `get`
* - `issue.documents.write` for `upsert` and `delete`
*
* @see PLUGIN_SPEC.md §14 — SDK Surface
*/
export interface PluginIssueDocumentsClient {
/**
* List all documents attached to an issue.
*
* Returns summary metadata (id, key, title, format, timestamps) without
* the full document body. Use `get()` to fetch a specific document's body.
*
* Requires the `issue.documents.read` capability.
*/
list(issueId: string, companyId: string): Promise<IssueDocumentSummary[]>;
/**
* Get a single document by key, including its full body content.
*
* Returns `null` if no document exists with the given key.
*
* Requires the `issue.documents.read` capability.
*
* @param issueId - UUID of the issue
* @param key - Document key (e.g. `"plan"`, `"design-spec"`)
* @param companyId - UUID of the company
*/
get(issueId: string, key: string, companyId: string): Promise<IssueDocument | null>;
/**
* Create or update a document on an issue.
*
* If a document with the given key already exists, it is updated and a new
* revision is created. If it does not exist, it is created.
*
* Requires the `issue.documents.write` capability.
*
* @param input - Document data including issueId, key, body, and optional title/format/changeSummary
*/
upsert(input: {
issueId: string;
key: string;
body: string;
companyId: string;
title?: string;
format?: string;
changeSummary?: string;
}): Promise<IssueDocument>;
/**
* Delete a document and all its revisions.
*
* No-ops silently if the document does not exist (idempotent).
*
* Requires the `issue.documents.write` capability.
*
* @param issueId - UUID of the issue
* @param key - Document key to delete
* @param companyId - UUID of the company
*/
delete(issueId: string, key: string, companyId: string): Promise<void>;
}
/**
* `ctx.issues` — read and mutate issues plus comments.
*
* Requires:
* - `issues.read` for read operations
* - `issues.create` for create
* - `issues.update` for update
* - `issue.comments.read` for `listComments`
* - `issue.comments.create` for `createComment`
* - `issue.documents.read` for `documents.list` and `documents.get`
* - `issue.documents.write` for `documents.upsert` and `documents.delete`
*/
export interface PluginIssuesClient {
list(input: {
companyId: string;
projectId?: string;
assigneeAgentId?: string;
status?: Issue["status"];
limit?: number;
offset?: number;
}): Promise<Issue[]>;
get(issueId: string, companyId: string): Promise<Issue | null>;
create(input: {
companyId: string;
projectId?: string;
goalId?: string;
parentId?: string;
title: string;
description?: string;
priority?: Issue["priority"];
assigneeAgentId?: string;
}): Promise<Issue>;
update(issueId: string, patch: Partial<Pick<Issue, "title" | "description" | "status" | "priority" | "assigneeAgentId">>, companyId: string): Promise<Issue>;
listComments(issueId: string, companyId: string): Promise<IssueComment[]>;
createComment(issueId: string, body: string, companyId: string): Promise<IssueComment>;
/** Read and write issue documents. Requires `issue.documents.read` / `issue.documents.write`. */
documents: PluginIssueDocumentsClient;
}
/**
* `ctx.agents` — read and manage agents.
*
* Requires `agents.read` for reads; `agents.pause` / `agents.resume` /
* `agents.invoke` for write operations.
*/
export interface PluginAgentsClient {
list(input: {
companyId: string;
status?: Agent["status"];
limit?: number;
offset?: number;
}): Promise<Agent[]>;
get(agentId: string, companyId: string): Promise<Agent | null>;
/** Pause an agent. Throws if agent is terminated or not found. Requires `agents.pause`. */
pause(agentId: string, companyId: string): Promise<Agent>;
/** Resume a paused agent (sets status to idle). Throws if terminated, pending_approval, or not found. Requires `agents.resume`. */
resume(agentId: string, companyId: string): Promise<Agent>;
/** Invoke (wake up) an agent with a prompt payload. Throws if paused, terminated, pending_approval, or not found. Requires `agents.invoke`. */
invoke(agentId: string, companyId: string, opts: {
prompt: string;
reason?: string;
}): Promise<{
runId: string;
}>;
/** Create, message, and close agent chat sessions. Requires `agent.sessions.*` capabilities. */
sessions: PluginAgentSessionsClient;
}
/**
* Represents an active conversational session with an agent.
* Maps to an `AgentTaskSession` row on the host.
*/
export interface AgentSession {
sessionId: string;
agentId: string;
companyId: string;
status: "active" | "closed";
createdAt: string;
}
/**
* A streaming event received during a session's `sendMessage` call.
* Delivered via JSON-RPC notifications from host to worker.
*/
export interface AgentSessionEvent {
sessionId: string;
runId: string;
seq: number;
/** The kind of event: "chunk" for output data, "status" for run state changes, "done" for end-of-stream, "error" for failures. */
eventType: "chunk" | "status" | "done" | "error";
stream: "stdout" | "stderr" | "system" | null;
message: string | null;
payload: Record<string, unknown> | null;
}
/**
* Result of sending a message to a session.
*/
export interface AgentSessionSendResult {
runId: string;
}
/**
* `ctx.agents.sessions` — create, message, and close agent chat sessions.
*
* Requires `agent.sessions.create` for create, `agent.sessions.list` for list,
* `agent.sessions.send` for sendMessage, `agent.sessions.close` for close.
*/
export interface PluginAgentSessionsClient {
/** Create a new conversational session with an agent. Requires `agent.sessions.create`. */
create(agentId: string, companyId: string, opts?: {
taskKey?: string;
reason?: string;
}): Promise<AgentSession>;
/** List active sessions for an agent owned by this plugin. Requires `agent.sessions.list`. */
list(agentId: string, companyId: string): Promise<AgentSession[]>;
/**
* Send a message to a session and receive streaming events via the `onEvent` callback.
* Returns immediately with `{ runId }`. Events are delivered asynchronously.
* Requires `agent.sessions.send`.
*/
sendMessage(sessionId: string, companyId: string, opts: {
prompt: string;
reason?: string;
onEvent?: (event: AgentSessionEvent) => void;
}): Promise<AgentSessionSendResult>;
/** Close a session, releasing resources. Requires `agent.sessions.close`. */
close(sessionId: string, companyId: string): Promise<void>;
}
/**
* `ctx.goals` — read and mutate goals.
*
* Requires:
* - `goals.read` for read operations
* - `goals.create` for create
* - `goals.update` for update
*/
export interface PluginGoalsClient {
list(input: {
companyId: string;
level?: Goal["level"];
status?: Goal["status"];
limit?: number;
offset?: number;
}): Promise<Goal[]>;
get(goalId: string, companyId: string): Promise<Goal | null>;
create(input: {
companyId: string;
title: string;
description?: string;
level?: Goal["level"];
status?: Goal["status"];
parentId?: string;
ownerAgentId?: string;
}): Promise<Goal>;
update(goalId: string, patch: Partial<Pick<Goal, "title" | "description" | "level" | "status" | "parentId" | "ownerAgentId">>, companyId: string): Promise<Goal>;
}
/**
* `ctx.streams` — push real-time events from the worker to the plugin UI.
*
* The worker opens a named channel, emits events on it, and closes it when
* done. On the UI side, `usePluginStream(channel)` receives these events in
* real time via SSE.
*
* Streams are scoped to `(pluginId, channel, companyId)`. Multiple UI clients
* can subscribe to the same channel concurrently.
*
* @example
* ```ts
* // Worker: stream chat tokens to the UI
* ctx.streams.open("chat", companyId);
* for await (const token of tokenStream) {
* ctx.streams.emit("chat", { type: "token", text: token });
* }
* ctx.streams.close("chat");
* ```
*
* @see usePluginStream in `@paperclipai/plugin-sdk/ui`
*/
export interface PluginStreamsClient {
/**
* Open a named stream channel. Optional — `emit()` implicitly opens if needed.
* Sends a `stream:open` event to connected UI clients.
*/
open(channel: string, companyId: string): void;
/**
* Push an event to all UI clients subscribed to this channel.
*
* @param channel - Stream channel name (e.g. `"chat"`, `"logs"`)
* @param event - JSON-serializable event payload
*/
emit(channel: string, event: unknown): void;
/**
* Close a stream channel. Sends a `stream:close` event to connected UI
* clients so they know no more events will arrive.
*/
close(channel: string): void;
}
/**
* The full plugin context object passed to the plugin worker at initialisation.
*
* This is the central interface plugin authors use to interact with the host.
* Every client is capability-gated: calling a client method without the
* required capability declared in the manifest results in a runtime error.
*
* @example
* ```ts
* import { definePlugin } from "@paperclipai/plugin-sdk";
*
* export default definePlugin({
* async setup(ctx) {
* ctx.events.on("issue.created", async (event) => {
* ctx.logger.info("Issue created", { issueId: event.entityId });
* });
*
* ctx.data.register("sync-health", async ({ companyId }) => {
* const state = await ctx.state.get({ scopeKind: "company", scopeId: String(companyId), stateKey: "last-sync" });
* return { lastSync: state };
* });
* },
* });
* ```
*
* @see PLUGIN_SPEC.md §14 — SDK Surface
*/
export interface PluginContext {
/** The plugin's manifest as validated at install time. */
manifest: PaperclipPluginManifestV1;
/** Read resolved operator configuration. */
config: PluginConfigClient;
/** Subscribe to and emit domain events. Requires `events.subscribe` / `events.emit`. */
events: PluginEventsClient;
/** Register handlers for scheduled jobs. Requires `jobs.schedule`. */
jobs: PluginJobsClient;
/** Register launcher metadata that the host can surface in plugin UI entry points. */
launchers: PluginLaunchersClient;
/** Make outbound HTTP requests. Requires `http.outbound`. */
http: PluginHttpClient;
/** Resolve secret references. Requires `secrets.read-ref`. */
secrets: PluginSecretsClient;
/** Write activity log entries. Requires `activity.log.write`. */
activity: PluginActivityClient;
/** Read and write scoped plugin state. Requires `plugin.state.read` / `plugin.state.write`. */
state: PluginStateClient;
/** Create and query plugin-owned entity records. */
entities: PluginEntitiesClient;
/** Read project and workspace metadata. Requires `projects.read` / `project.workspaces.read`. */
projects: PluginProjectsClient;
/** Read company metadata. Requires `companies.read`. */
companies: PluginCompaniesClient;
/** Read and write issues, comments, and documents. Requires issue capabilities. */
issues: PluginIssuesClient;
/** Read and manage agents. Requires `agents.read` for reads; `agents.pause` / `agents.resume` / `agents.invoke` for write ops. */
agents: PluginAgentsClient;
/** Read and mutate goals. Requires `goals.read` for reads; `goals.create` / `goals.update` for write ops. */
goals: PluginGoalsClient;
/** Register getData handlers for the plugin's UI components. */
data: PluginDataClient;
/** Register performAction handlers for the plugin's UI components. */
actions: PluginActionsClient;
/** Push real-time events from the worker to the plugin UI via SSE. */
streams: PluginStreamsClient;
/** Register agent tool handlers. Requires `agent.tools.register`. */
tools: PluginToolsClient;
/** Write plugin metrics. Requires `metrics.write`. */
metrics: PluginMetricsClient;
/** Structured logger. Output is captured and surfaced in the plugin health dashboard. */
logger: PluginLogger;
}
//# sourceMappingURL=types.d.ts.map

File diff suppressed because one or more lines are too long

12
node_modules/@paperclipai/plugin-sdk/dist/types.js generated vendored Normal file
View File

@@ -0,0 +1,12 @@
/**
* Core types for the Paperclip plugin worker-side SDK.
*
* These types define the stable public API surface that plugin workers import
* from `@paperclipai/plugin-sdk`. The host provides a concrete implementation
* of `PluginContext` to the plugin at initialisation time.
*
* @see PLUGIN_SPEC.md §14 — SDK Surface
* @see PLUGIN_SPEC.md §29.2 — SDK Versioning
*/
export {};
//# sourceMappingURL=types.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG"}

View File

@@ -0,0 +1,257 @@
/**
* Shared UI component declarations for plugin frontends.
*
* These components are exported from `@paperclipai/plugin-sdk/ui` and are
* provided by the host at runtime. They match the host's design tokens and
* visual language, reducing the boilerplate needed to build consistent plugin UIs.
*
* **Plugins are not required to use these components.** They exist to reduce
* boilerplate and keep visual consistency. A plugin may render entirely custom
* UI using any React component library.
*
* Component implementations are provided by the host — plugin bundles contain
* only the type declarations; the runtime implementations are injected via the
* host module registry.
*
* @see PLUGIN_SPEC.md §19.6 — Shared Components In `@paperclipai/plugin-sdk/ui`
*/
import type React from "react";
/**
* A trend value that can accompany a metric.
* Positive values indicate upward trends; negative values indicate downward trends.
*/
export interface MetricTrend {
/** Direction of the trend. */
direction: "up" | "down" | "flat";
/** Percentage change value (e.g. `12.5` for 12.5%). */
percentage?: number;
}
/** Props for `MetricCard`. */
export interface MetricCardProps {
/** Short label describing the metric (e.g. `"Synced Issues"`). */
label: string;
/** The metric value to display. */
value: number | string;
/** Optional trend indicator. */
trend?: MetricTrend;
/** Optional sparkline data (array of numbers, latest last). */
sparkline?: number[];
/** Optional unit suffix (e.g. `"%"`, `"ms"`). */
unit?: string;
}
/** Status variants for `StatusBadge`. */
export type StatusBadgeVariant = "ok" | "warning" | "error" | "info" | "pending";
/** Props for `StatusBadge`. */
export interface StatusBadgeProps {
/** Human-readable label. */
label: string;
/** Visual variant determining colour. */
status: StatusBadgeVariant;
}
/** A single column definition for `DataTable`. */
export interface DataTableColumn<T = Record<string, unknown>> {
/** Column key, matching a field on the row object. */
key: keyof T & string;
/** Column header label. */
header: string;
/** Optional custom cell renderer. */
render?: (value: unknown, row: T) => React.ReactNode;
/** Whether this column is sortable. */
sortable?: boolean;
/** CSS width (e.g. `"120px"`, `"20%"`). */
width?: string;
}
/** Props for `DataTable`. */
export interface DataTableProps<T = Record<string, unknown>> {
/** Column definitions. */
columns: DataTableColumn<T>[];
/** Row data. Each row should have a stable `id` field. */
rows: T[];
/** Whether the table is currently loading. */
loading?: boolean;
/** Message shown when `rows` is empty. */
emptyMessage?: string;
/** Total row count for pagination (if different from `rows.length`). */
totalCount?: number;
/** Current page (0-based, for pagination). */
page?: number;
/** Rows per page (for pagination). */
pageSize?: number;
/** Callback when page changes. */
onPageChange?: (page: number) => void;
/** Callback when a column header is clicked to sort. */
onSort?: (key: string, direction: "asc" | "desc") => void;
}
/** A single data point for `TimeseriesChart`. */
export interface TimeseriesDataPoint {
/** ISO 8601 timestamp. */
timestamp: string;
/** Numeric value. */
value: number;
/** Optional label for the point. */
label?: string;
}
/** Props for `TimeseriesChart`. */
export interface TimeseriesChartProps {
/** Series data. */
data: TimeseriesDataPoint[];
/** Chart title. */
title?: string;
/** Y-axis label. */
yLabel?: string;
/** Chart type. Defaults to `"line"`. */
type?: "line" | "bar";
/** Height of the chart in pixels. Defaults to `200`. */
height?: number;
/** Whether the chart is currently loading. */
loading?: boolean;
}
/** Props for `MarkdownBlock`. */
export interface MarkdownBlockProps {
/** Markdown content to render. */
content: string;
}
/** A single key-value pair for `KeyValueList`. */
export interface KeyValuePair {
/** Label for the key. */
label: string;
/** Value to display. May be a string, number, or a React node. */
value: React.ReactNode;
}
/** Props for `KeyValueList`. */
export interface KeyValueListProps {
/** Pairs to render in the list. */
pairs: KeyValuePair[];
}
/** A single action button for `ActionBar`. */
export interface ActionBarItem {
/** Button label. */
label: string;
/** Action key to call via the plugin bridge. */
actionKey: string;
/** Optional parameters to pass to the action handler. */
params?: Record<string, unknown>;
/** Button variant. Defaults to `"default"`. */
variant?: "default" | "primary" | "destructive";
/** Whether to show a confirmation dialog before executing. */
confirm?: boolean;
/** Text for the confirmation dialog (used when `confirm` is true). */
confirmMessage?: string;
}
/** Props for `ActionBar`. */
export interface ActionBarProps {
/** Action definitions. */
actions: ActionBarItem[];
/** Called after an action succeeds. Use to trigger data refresh. */
onSuccess?: (actionKey: string, result: unknown) => void;
/** Called when an action fails. */
onError?: (actionKey: string, error: unknown) => void;
}
/** A single log line for `LogView`. */
export interface LogViewEntry {
/** ISO 8601 timestamp. */
timestamp: string;
/** Log level. */
level: "info" | "warn" | "error" | "debug";
/** Log message. */
message: string;
/** Optional structured metadata. */
meta?: Record<string, unknown>;
}
/** Props for `LogView`. */
export interface LogViewProps {
/** Log entries to display. */
entries: LogViewEntry[];
/** Maximum height of the scrollable container (CSS value). Defaults to `"400px"`. */
maxHeight?: string;
/** Whether to auto-scroll to the latest entry. */
autoScroll?: boolean;
/** Whether the log is currently loading. */
loading?: boolean;
}
/** Props for `JsonTree`. */
export interface JsonTreeProps {
/** The data to render as a collapsible JSON tree. */
data: unknown;
/** Initial depth to expand. Defaults to `2`. */
defaultExpandDepth?: number;
}
/** Props for `Spinner`. */
export interface SpinnerProps {
/** Size of the spinner. Defaults to `"md"`. */
size?: "sm" | "md" | "lg";
/** Accessible label for the spinner (used as `aria-label`). */
label?: string;
}
/** Props for `ErrorBoundary`. */
export interface ErrorBoundaryProps {
/** Content to render inside the error boundary. */
children: React.ReactNode;
/** Optional custom fallback to render when an error is caught. */
fallback?: React.ReactNode;
/** Called when an error is caught, for logging or reporting. */
onError?: (error: Error, info: React.ErrorInfo) => void;
}
export declare const MetricCard: React.ComponentType<MetricCardProps>;
/**
* Displays an inline status badge (ok / warning / error / info / pending).
*
* @see PLUGIN_SPEC.md §19.6 — Shared Components
*/
export declare const StatusBadge: React.ComponentType<StatusBadgeProps>;
/**
* Sortable, paginated data table.
*
* @see PLUGIN_SPEC.md §19.6 — Shared Components
*/
export declare const DataTable: React.ComponentType<DataTableProps<Record<string, unknown>>>;
/**
* Line or bar chart for time-series data.
*
* @see PLUGIN_SPEC.md §19.6 — Shared Components
*/
export declare const TimeseriesChart: React.ComponentType<TimeseriesChartProps>;
/**
* Renders Markdown text as HTML.
*
* @see PLUGIN_SPEC.md §19.6 — Shared Components
*/
export declare const MarkdownBlock: React.ComponentType<MarkdownBlockProps>;
/**
* Renders a definition-list of label/value pairs.
*
* @see PLUGIN_SPEC.md §19.6 — Shared Components
*/
export declare const KeyValueList: React.ComponentType<KeyValueListProps>;
/**
* Row of action buttons wired to the plugin bridge's `performAction` handlers.
*
* @see PLUGIN_SPEC.md §19.6 — Shared Components
*/
export declare const ActionBar: React.ComponentType<ActionBarProps>;
/**
* Scrollable, timestamped log output viewer.
*
* @see PLUGIN_SPEC.md §19.6 — Shared Components
*/
export declare const LogView: React.ComponentType<LogViewProps>;
/**
* Collapsible JSON tree for debugging or raw data inspection.
*
* @see PLUGIN_SPEC.md §19.6 — Shared Components
*/
export declare const JsonTree: React.ComponentType<JsonTreeProps>;
/**
* Loading indicator.
*
* @see PLUGIN_SPEC.md §19.6 — Shared Components
*/
export declare const Spinner: React.ComponentType<SpinnerProps>;
/**
* React error boundary that prevents plugin rendering errors from crashing
* the host page.
*
* @see PLUGIN_SPEC.md §19.7 — Error Propagation Through The Bridge
*/
export declare const ErrorBoundary: React.ComponentType<ErrorBoundaryProps>;
//# sourceMappingURL=components.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"components.d.ts","sourceRoot":"","sources":["../../src/ui/components.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,97 @@
/**
* Shared UI component declarations for plugin frontends.
*
* These components are exported from `@paperclipai/plugin-sdk/ui` and are
* provided by the host at runtime. They match the host's design tokens and
* visual language, reducing the boilerplate needed to build consistent plugin UIs.
*
* **Plugins are not required to use these components.** They exist to reduce
* boilerplate and keep visual consistency. A plugin may render entirely custom
* UI using any React component library.
*
* Component implementations are provided by the host — plugin bundles contain
* only the type declarations; the runtime implementations are injected via the
* host module registry.
*
* @see PLUGIN_SPEC.md §19.6 — Shared Components In `@paperclipai/plugin-sdk/ui`
*/
import { renderSdkUiComponent } from "./runtime.js";
// ---------------------------------------------------------------------------
// Component declarations (provided by host at runtime)
// ---------------------------------------------------------------------------
// These are declared as ambient values so plugin TypeScript code can import
// and use them with full type-checking. The host's module registry provides
// the concrete React component implementations at bundle load time.
/**
* Displays a single metric with an optional trend indicator and sparkline.
*
* @see PLUGIN_SPEC.md §19.6 — Shared Components
*/
function createSdkUiComponent(name) {
return function PaperclipSdkUiComponent(props) {
return renderSdkUiComponent(name, props);
};
}
export const MetricCard = createSdkUiComponent("MetricCard");
/**
* Displays an inline status badge (ok / warning / error / info / pending).
*
* @see PLUGIN_SPEC.md §19.6 — Shared Components
*/
export const StatusBadge = createSdkUiComponent("StatusBadge");
/**
* Sortable, paginated data table.
*
* @see PLUGIN_SPEC.md §19.6 — Shared Components
*/
export const DataTable = createSdkUiComponent("DataTable");
/**
* Line or bar chart for time-series data.
*
* @see PLUGIN_SPEC.md §19.6 — Shared Components
*/
export const TimeseriesChart = createSdkUiComponent("TimeseriesChart");
/**
* Renders Markdown text as HTML.
*
* @see PLUGIN_SPEC.md §19.6 — Shared Components
*/
export const MarkdownBlock = createSdkUiComponent("MarkdownBlock");
/**
* Renders a definition-list of label/value pairs.
*
* @see PLUGIN_SPEC.md §19.6 — Shared Components
*/
export const KeyValueList = createSdkUiComponent("KeyValueList");
/**
* Row of action buttons wired to the plugin bridge's `performAction` handlers.
*
* @see PLUGIN_SPEC.md §19.6 — Shared Components
*/
export const ActionBar = createSdkUiComponent("ActionBar");
/**
* Scrollable, timestamped log output viewer.
*
* @see PLUGIN_SPEC.md §19.6 — Shared Components
*/
export const LogView = createSdkUiComponent("LogView");
/**
* Collapsible JSON tree for debugging or raw data inspection.
*
* @see PLUGIN_SPEC.md §19.6 — Shared Components
*/
export const JsonTree = createSdkUiComponent("JsonTree");
/**
* Loading indicator.
*
* @see PLUGIN_SPEC.md §19.6 — Shared Components
*/
export const Spinner = createSdkUiComponent("Spinner");
/**
* React error boundary that prevents plugin rendering errors from crashing
* the host page.
*
* @see PLUGIN_SPEC.md §19.7 — Error Propagation Through The Bridge
*/
export const ErrorBoundary = createSdkUiComponent("ErrorBoundary");
//# sourceMappingURL=components.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"components.js","sourceRoot":"","sources":["../../src/ui/components.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAGH,OAAO,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAwMpD,8EAA8E;AAC9E,uDAAuD;AACvD,8EAA8E;AAE9E,4EAA4E;AAC5E,4EAA4E;AAC5E,oEAAoE;AAEpE;;;;GAIG;AACH,SAAS,oBAAoB,CAAS,IAAY;IAChD,OAAO,SAAS,uBAAuB,CAAC,KAAa;QACnD,OAAO,oBAAoB,CAAC,IAAI,EAAE,KAAK,CAAoB,CAAC;IAC9D,CAAC,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,MAAM,UAAU,GAAG,oBAAoB,CAAkB,YAAY,CAAC,CAAC;AAE9E;;;;GAIG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,oBAAoB,CAAmB,aAAa,CAAC,CAAC;AAEjF;;;;GAIG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG,oBAAoB,CAAiB,WAAW,CAAC,CAAC;AAE3E;;;;GAIG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,oBAAoB,CAAuB,iBAAiB,CAAC,CAAC;AAE7F;;;;GAIG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,oBAAoB,CAAqB,eAAe,CAAC,CAAC;AAEvF;;;;GAIG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,oBAAoB,CAAoB,cAAc,CAAC,CAAC;AAEpF;;;;GAIG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG,oBAAoB,CAAiB,WAAW,CAAC,CAAC;AAE3E;;;;GAIG;AACH,MAAM,CAAC,MAAM,OAAO,GAAG,oBAAoB,CAAe,SAAS,CAAC,CAAC;AAErE;;;;GAIG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,oBAAoB,CAAgB,UAAU,CAAC,CAAC;AAExE;;;;GAIG;AACH,MAAM,CAAC,MAAM,OAAO,GAAG,oBAAoB,CAAe,SAAS,CAAC,CAAC;AAErE;;;;;GAKG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,oBAAoB,CAAqB,eAAe,CAAC,CAAC"}

120
node_modules/@paperclipai/plugin-sdk/dist/ui/hooks.d.ts generated vendored Normal file
View File

@@ -0,0 +1,120 @@
import type { PluginDataResult, PluginActionFn, PluginHostContext, PluginStreamResult, PluginToastFn } from "./types.js";
/**
* Fetch data from the plugin worker's registered `getData` handler.
*
* Calls `ctx.data.register(key, handler)` in the worker and returns the
* result as reactive state. Re-fetches when `params` changes.
*
* @template T The expected shape of the returned data
* @param key - The data key matching the handler registered with `ctx.data.register()`
* @param params - Optional parameters forwarded to the handler
* @returns `PluginDataResult<T>` with `data`, `loading`, `error`, and `refresh`
*
* @example
* ```tsx
* function SyncWidget({ context }: PluginWidgetProps) {
* const { data, loading, error } = usePluginData<SyncHealth>("sync-health", {
* companyId: context.companyId,
* });
*
* if (loading) return <div>Loading…</div>;
* if (error) return <div>Error: {error.message}</div>;
* return <div>Synced Issues: {data!.syncedCount}</div>;
* }
* ```
*
* @see PLUGIN_SPEC.md §13.8 — `getData`
* @see PLUGIN_SPEC.md §19.7 — Error Propagation Through The Bridge
*/
export declare function usePluginData<T = unknown>(key: string, params?: Record<string, unknown>): PluginDataResult<T>;
/**
* Get a callable function that invokes the plugin worker's registered
* `performAction` handler.
*
* The returned function is async and throws a `PluginBridgeError` on failure.
*
* @param key - The action key matching the handler registered with `ctx.actions.register()`
* @returns An async function that sends the action to the worker and resolves with the result
*
* @example
* ```tsx
* function ResyncButton({ context }: PluginWidgetProps) {
* const resync = usePluginAction("resync");
* const [error, setError] = useState<string | null>(null);
*
* async function handleClick() {
* try {
* await resync({ companyId: context.companyId });
* } catch (err) {
* setError((err as PluginBridgeError).message);
* }
* }
*
* return <button onClick={handleClick}>Resync Now</button>;
* }
* ```
*
* @see PLUGIN_SPEC.md §13.9 — `performAction`
* @see PLUGIN_SPEC.md §19.7 — Error Propagation Through The Bridge
*/
export declare function usePluginAction(key: string): PluginActionFn;
/**
* Read the current host context (active company, project, entity, user).
*
* Use this to know which context the plugin component is being rendered in
* so you can scope data requests and actions accordingly.
*
* @returns The current `PluginHostContext`
*
* @example
* ```tsx
* function IssueTab() {
* const { companyId, entityId } = useHostContext();
* const { data } = usePluginData("linear-link", { issueId: entityId });
* return <div>{data?.linearIssueUrl}</div>;
* }
* ```
*
* @see PLUGIN_SPEC.md §19 — UI Extension Model
*/
export declare function useHostContext(): PluginHostContext;
/**
* Subscribe to a real-time event stream pushed from the plugin worker.
*
* Opens an SSE connection to `GET /api/plugins/:pluginId/bridge/stream/:channel`
* and accumulates events as they arrive. The worker pushes events using
* `ctx.streams.emit(channel, event)`.
*
* @template T The expected shape of each streamed event
* @param channel - The stream channel name (must match what the worker uses in `ctx.streams.emit`)
* @param options - Optional configuration for the stream
* @returns `PluginStreamResult<T>` with `events`, `lastEvent`, connection status, and `close()`
*
* @example
* ```tsx
* function ChatMessages() {
* const { events, connected, close } = usePluginStream<ChatToken>("chat-stream");
*
* return (
* <div>
* {events.map((e, i) => <span key={i}>{e.text}</span>)}
* {connected && <span className="pulse" />}
* <button onClick={close}>Stop</button>
* </div>
* );
* }
* ```
*
* @see PLUGIN_SPEC.md §19.8 — Real-Time Streaming
*/
export declare function usePluginStream<T = unknown>(channel: string, options?: {
companyId?: string;
}): PluginStreamResult<T>;
/**
* Trigger a host toast notification from plugin UI.
*
* This lets plugin pages and widgets surface user-facing feedback through the
* same toast system as the host app without reaching into host internals.
*/
export declare function usePluginToast(): PluginToastFn;
//# sourceMappingURL=hooks.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../../src/ui/hooks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,gBAAgB,EAChB,cAAc,EACd,iBAAiB,EACjB,kBAAkB,EAClB,aAAa,EACd,MAAM,YAAY,CAAC;AAOpB;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,aAAa,CAAC,CAAC,GAAG,OAAO,EACvC,GAAG,EAAE,MAAM,EACX,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC/B,gBAAgB,CAAC,CAAC,CAAC,CAKrB;AAMD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,cAAc,CAG3D;AAMD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,cAAc,IAAI,iBAAiB,CAGlD;AAMD;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,eAAe,CAAC,CAAC,GAAG,OAAO,EACzC,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GAC/B,kBAAkB,CAAC,CAAC,CAAC,CAKvB;AAMD;;;;;GAKG;AACH,wBAAgB,cAAc,IAAI,aAAa,CAG9C"}

148
node_modules/@paperclipai/plugin-sdk/dist/ui/hooks.js generated vendored Normal file
View File

@@ -0,0 +1,148 @@
import { getSdkUiRuntimeValue } from "./runtime.js";
// ---------------------------------------------------------------------------
// usePluginData
// ---------------------------------------------------------------------------
/**
* Fetch data from the plugin worker's registered `getData` handler.
*
* Calls `ctx.data.register(key, handler)` in the worker and returns the
* result as reactive state. Re-fetches when `params` changes.
*
* @template T The expected shape of the returned data
* @param key - The data key matching the handler registered with `ctx.data.register()`
* @param params - Optional parameters forwarded to the handler
* @returns `PluginDataResult<T>` with `data`, `loading`, `error`, and `refresh`
*
* @example
* ```tsx
* function SyncWidget({ context }: PluginWidgetProps) {
* const { data, loading, error } = usePluginData<SyncHealth>("sync-health", {
* companyId: context.companyId,
* });
*
* if (loading) return <div>Loading…</div>;
* if (error) return <div>Error: {error.message}</div>;
* return <div>Synced Issues: {data!.syncedCount}</div>;
* }
* ```
*
* @see PLUGIN_SPEC.md §13.8 — `getData`
* @see PLUGIN_SPEC.md §19.7 — Error Propagation Through The Bridge
*/
export function usePluginData(key, params) {
const impl = getSdkUiRuntimeValue("usePluginData");
return impl(key, params);
}
// ---------------------------------------------------------------------------
// usePluginAction
// ---------------------------------------------------------------------------
/**
* Get a callable function that invokes the plugin worker's registered
* `performAction` handler.
*
* The returned function is async and throws a `PluginBridgeError` on failure.
*
* @param key - The action key matching the handler registered with `ctx.actions.register()`
* @returns An async function that sends the action to the worker and resolves with the result
*
* @example
* ```tsx
* function ResyncButton({ context }: PluginWidgetProps) {
* const resync = usePluginAction("resync");
* const [error, setError] = useState<string | null>(null);
*
* async function handleClick() {
* try {
* await resync({ companyId: context.companyId });
* } catch (err) {
* setError((err as PluginBridgeError).message);
* }
* }
*
* return <button onClick={handleClick}>Resync Now</button>;
* }
* ```
*
* @see PLUGIN_SPEC.md §13.9 — `performAction`
* @see PLUGIN_SPEC.md §19.7 — Error Propagation Through The Bridge
*/
export function usePluginAction(key) {
const impl = getSdkUiRuntimeValue("usePluginAction");
return impl(key);
}
// ---------------------------------------------------------------------------
// useHostContext
// ---------------------------------------------------------------------------
/**
* Read the current host context (active company, project, entity, user).
*
* Use this to know which context the plugin component is being rendered in
* so you can scope data requests and actions accordingly.
*
* @returns The current `PluginHostContext`
*
* @example
* ```tsx
* function IssueTab() {
* const { companyId, entityId } = useHostContext();
* const { data } = usePluginData("linear-link", { issueId: entityId });
* return <div>{data?.linearIssueUrl}</div>;
* }
* ```
*
* @see PLUGIN_SPEC.md §19 — UI Extension Model
*/
export function useHostContext() {
const impl = getSdkUiRuntimeValue("useHostContext");
return impl();
}
// ---------------------------------------------------------------------------
// usePluginStream
// ---------------------------------------------------------------------------
/**
* Subscribe to a real-time event stream pushed from the plugin worker.
*
* Opens an SSE connection to `GET /api/plugins/:pluginId/bridge/stream/:channel`
* and accumulates events as they arrive. The worker pushes events using
* `ctx.streams.emit(channel, event)`.
*
* @template T The expected shape of each streamed event
* @param channel - The stream channel name (must match what the worker uses in `ctx.streams.emit`)
* @param options - Optional configuration for the stream
* @returns `PluginStreamResult<T>` with `events`, `lastEvent`, connection status, and `close()`
*
* @example
* ```tsx
* function ChatMessages() {
* const { events, connected, close } = usePluginStream<ChatToken>("chat-stream");
*
* return (
* <div>
* {events.map((e, i) => <span key={i}>{e.text}</span>)}
* {connected && <span className="pulse" />}
* <button onClick={close}>Stop</button>
* </div>
* );
* }
* ```
*
* @see PLUGIN_SPEC.md §19.8 — Real-Time Streaming
*/
export function usePluginStream(channel, options) {
const impl = getSdkUiRuntimeValue("usePluginStream");
return impl(channel, options);
}
// ---------------------------------------------------------------------------
// usePluginToast
// ---------------------------------------------------------------------------
/**
* Trigger a host toast notification from plugin UI.
*
* This lets plugin pages and widgets surface user-facing feedback through the
* same toast system as the host app without reaching into host internals.
*/
export function usePluginToast() {
const impl = getSdkUiRuntimeValue("usePluginToast");
return impl();
}
//# sourceMappingURL=hooks.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"hooks.js","sourceRoot":"","sources":["../../src/ui/hooks.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAEpD,8EAA8E;AAC9E,gBAAgB;AAChB,8EAA8E;AAE9E;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,UAAU,aAAa,CAC3B,GAAW,EACX,MAAgC;IAEhC,MAAM,IAAI,GAAG,oBAAoB,CAE/B,eAAe,CAAC,CAAC;IACnB,OAAO,IAAI,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;AAC3B,CAAC;AAED,8EAA8E;AAC9E,kBAAkB;AAClB,8EAA8E;AAE9E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,MAAM,UAAU,eAAe,CAAC,GAAW;IACzC,MAAM,IAAI,GAAG,oBAAoB,CAAsC,iBAAiB,CAAC,CAAC;IAC1F,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC;AACnB,CAAC;AAED,8EAA8E;AAC9E,iBAAiB;AACjB,8EAA8E;AAE9E;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,cAAc;IAC5B,MAAM,IAAI,GAAG,oBAAoB,CAA0B,gBAAgB,CAAC,CAAC;IAC7E,OAAO,IAAI,EAAE,CAAC;AAChB,CAAC;AAED,8EAA8E;AAC9E,kBAAkB;AAClB,8EAA8E;AAE9E;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,MAAM,UAAU,eAAe,CAC7B,OAAe,EACf,OAAgC;IAEhC,MAAM,IAAI,GAAG,oBAAoB,CAE/B,iBAAiB,CAAC,CAAC;IACrB,OAAO,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;AAChC,CAAC;AAED,8EAA8E;AAC9E,iBAAiB;AACjB,8EAA8E;AAE9E;;;;;GAKG;AACH,MAAM,UAAU,cAAc;IAC5B,MAAM,IAAI,GAAG,oBAAoB,CAAsB,gBAAgB,CAAC,CAAC;IACzE,OAAO,IAAI,EAAE,CAAC;AAChB,CAAC"}

View File

@@ -0,0 +1,50 @@
/**
* `@paperclipai/plugin-sdk/ui` — Paperclip plugin UI SDK.
*
* Import this subpath from plugin UI bundles (React components that run in
* the host frontend). Do **not** import this from plugin worker code.
*
* The worker-side SDK is available from `@paperclipai/plugin-sdk` (root).
*
* @see PLUGIN_SPEC.md §19.0.1 — Plugin UI SDK
* @see PLUGIN_SPEC.md §29.2 — SDK Versioning
*
* @example
* ```tsx
* // Plugin UI bundle entry (dist/ui/index.tsx)
* import { usePluginData, usePluginAction } from "@paperclipai/plugin-sdk/ui";
* import type { PluginWidgetProps } from "@paperclipai/plugin-sdk/ui";
*
* export function DashboardWidget({ context }: PluginWidgetProps) {
* const { data, loading, error } = usePluginData("sync-health", {
* companyId: context.companyId,
* });
* const resync = usePluginAction("resync");
*
* if (loading) return <div>Loading…</div>;
* if (error) return <div>Error: {error.message}</div>;
*
* return (
* <div style={{ display: "grid", gap: 8 }}>
* <strong>Synced Issues</strong>
* <div>{data!.syncedCount}</div>
* <button onClick={() => resync({ companyId: context.companyId })}>
* Resync Now
* </button>
* </div>
* );
* }
* ```
*/
/**
* Bridge hooks for plugin UI components to communicate with the plugin worker.
*
* - `usePluginData(key, params)` — fetch data from the worker's `getData` handler
* - `usePluginAction(key)` — get a callable that invokes the worker's `performAction` handler
* - `useHostContext()` — read the current active company, project, entity, and user IDs
* - `usePluginStream(channel)` — subscribe to real-time SSE events from the worker
*/
export { usePluginData, usePluginAction, useHostContext, usePluginStream, usePluginToast, } from "./hooks.js";
export type { PluginBridgeError, PluginBridgeErrorCode, PluginHostContext, PluginModalBoundsRequest, PluginRenderCloseEvent, PluginRenderCloseHandler, PluginRenderCloseLifecycle, PluginRenderEnvironmentContext, PluginLauncherBounds, PluginLauncherRenderEnvironment, PluginDataResult, PluginActionFn, PluginStreamResult, PluginToastTone, PluginToastAction, PluginToastInput, PluginToastFn, } from "./types.js";
export type { PluginPageProps, PluginWidgetProps, PluginDetailTabProps, PluginSidebarProps, PluginProjectSidebarItemProps, PluginCommentAnnotationProps, PluginCommentContextMenuItemProps, PluginSettingsPageProps, } from "./types.js";
//# sourceMappingURL=index.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ui/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAEH;;;;;;;GAOG;AACH,OAAO,EACL,aAAa,EACb,eAAe,EACf,cAAc,EACd,eAAe,EACf,cAAc,GACf,MAAM,YAAY,CAAC;AAGpB,YAAY,EACV,iBAAiB,EACjB,qBAAqB,EACrB,iBAAiB,EACjB,wBAAwB,EACxB,sBAAsB,EACtB,wBAAwB,EACxB,0BAA0B,EAC1B,8BAA8B,EAC9B,oBAAoB,EACpB,+BAA+B,EAC/B,gBAAgB,EAChB,cAAc,EACd,kBAAkB,EAClB,eAAe,EACf,iBAAiB,EACjB,gBAAgB,EAChB,aAAa,GACd,MAAM,YAAY,CAAC;AAGpB,YAAY,EACV,eAAe,EACf,iBAAiB,EACjB,oBAAoB,EACpB,kBAAkB,EAClB,6BAA6B,EAC7B,4BAA4B,EAC5B,iCAAiC,EACjC,uBAAuB,GACxB,MAAM,YAAY,CAAC"}

48
node_modules/@paperclipai/plugin-sdk/dist/ui/index.js generated vendored Normal file
View File

@@ -0,0 +1,48 @@
/**
* `@paperclipai/plugin-sdk/ui` — Paperclip plugin UI SDK.
*
* Import this subpath from plugin UI bundles (React components that run in
* the host frontend). Do **not** import this from plugin worker code.
*
* The worker-side SDK is available from `@paperclipai/plugin-sdk` (root).
*
* @see PLUGIN_SPEC.md §19.0.1 — Plugin UI SDK
* @see PLUGIN_SPEC.md §29.2 — SDK Versioning
*
* @example
* ```tsx
* // Plugin UI bundle entry (dist/ui/index.tsx)
* import { usePluginData, usePluginAction } from "@paperclipai/plugin-sdk/ui";
* import type { PluginWidgetProps } from "@paperclipai/plugin-sdk/ui";
*
* export function DashboardWidget({ context }: PluginWidgetProps) {
* const { data, loading, error } = usePluginData("sync-health", {
* companyId: context.companyId,
* });
* const resync = usePluginAction("resync");
*
* if (loading) return <div>Loading…</div>;
* if (error) return <div>Error: {error.message}</div>;
*
* return (
* <div style={{ display: "grid", gap: 8 }}>
* <strong>Synced Issues</strong>
* <div>{data!.syncedCount}</div>
* <button onClick={() => resync({ companyId: context.companyId })}>
* Resync Now
* </button>
* </div>
* );
* }
* ```
*/
/**
* Bridge hooks for plugin UI components to communicate with the plugin worker.
*
* - `usePluginData(key, params)` — fetch data from the worker's `getData` handler
* - `usePluginAction(key)` — get a callable that invokes the worker's `performAction` handler
* - `useHostContext()` — read the current active company, project, entity, and user IDs
* - `usePluginStream(channel)` — subscribe to real-time SSE events from the worker
*/
export { usePluginData, usePluginAction, useHostContext, usePluginStream, usePluginToast, } from "./hooks.js";
//# sourceMappingURL=index.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/ui/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAEH;;;;;;;GAOG;AACH,OAAO,EACL,aAAa,EACb,eAAe,EACf,cAAc,EACd,eAAe,EACf,cAAc,GACf,MAAM,YAAY,CAAC"}

View File

@@ -0,0 +1,3 @@
export declare function getSdkUiRuntimeValue<T>(name: string): T;
export declare function renderSdkUiComponent<TProps>(name: string, props: TProps): unknown;
//# sourceMappingURL=runtime.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../src/ui/runtime.ts"],"names":[],"mappings":"AAsBA,wBAAgB,oBAAoB,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,CAAC,CAMvD;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EACzC,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,GACZ,OAAO,CAiBT"}

View File

@@ -0,0 +1,30 @@
function getBridgeRegistry() {
return globalThis.__paperclipPluginBridge__;
}
function missingBridgeValueError(name) {
return new Error(`Paperclip plugin UI runtime is not initialized for "${name}". ` +
'Ensure the host loaded the plugin bridge before rendering this UI module.');
}
export function getSdkUiRuntimeValue(name) {
const value = getBridgeRegistry()?.sdkUi?.[name];
if (value === undefined) {
throw missingBridgeValueError(name);
}
return value;
}
export function renderSdkUiComponent(name, props) {
const registry = getBridgeRegistry();
const component = registry?.sdkUi?.[name];
if (component === undefined) {
throw missingBridgeValueError(name);
}
const createElement = registry?.react?.createElement;
if (typeof createElement === "function") {
return createElement(component, props);
}
if (typeof component === "function") {
return component(props);
}
throw new Error(`Paperclip plugin UI component "${name}" is not callable`);
}
//# sourceMappingURL=runtime.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"runtime.js","sourceRoot":"","sources":["../../src/ui/runtime.ts"],"names":[],"mappings":"AAWA,SAAS,iBAAiB;IACxB,OAAQ,UAA2B,CAAC,yBAAyB,CAAC;AAChE,CAAC;AAED,SAAS,uBAAuB,CAAC,IAAY;IAC3C,OAAO,IAAI,KAAK,CACd,uDAAuD,IAAI,KAAK;QAC9D,2EAA2E,CAC9E,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAI,IAAY;IAClD,MAAM,KAAK,GAAG,iBAAiB,EAAE,EAAE,KAAK,EAAE,CAAC,IAAI,CAAC,CAAC;IACjD,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACxB,MAAM,uBAAuB,CAAC,IAAI,CAAC,CAAC;IACtC,CAAC;IACD,OAAO,KAAU,CAAC;AACpB,CAAC;AAED,MAAM,UAAU,oBAAoB,CAClC,IAAY,EACZ,KAAa;IAEb,MAAM,QAAQ,GAAG,iBAAiB,EAAE,CAAC;IACrC,MAAM,SAAS,GAAG,QAAQ,EAAE,KAAK,EAAE,CAAC,IAAI,CAAC,CAAC;IAC1C,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;QAC5B,MAAM,uBAAuB,CAAC,IAAI,CAAC,CAAC;IACtC,CAAC;IAED,MAAM,aAAa,GAAG,QAAQ,EAAE,KAAK,EAAE,aAAa,CAAC;IACrD,IAAI,OAAO,aAAa,KAAK,UAAU,EAAE,CAAC;QACxC,OAAO,aAAa,CAAC,SAAS,EAAE,KAAgC,CAAC,CAAC;IACpE,CAAC;IAED,IAAI,OAAO,SAAS,KAAK,UAAU,EAAE,CAAC;QACpC,OAAO,SAAS,CAAC,KAAK,CAAC,CAAC;IAC1B,CAAC;IAED,MAAM,IAAI,KAAK,CAAC,kCAAkC,IAAI,mBAAmB,CAAC,CAAC;AAC7E,CAAC"}

308
node_modules/@paperclipai/plugin-sdk/dist/ui/types.d.ts generated vendored Normal file
View File

@@ -0,0 +1,308 @@
/**
* Paperclip plugin UI SDK — types for plugin frontend components.
*
* Plugin UI bundles import from `@paperclipai/plugin-sdk/ui`. This subpath
* provides the bridge hooks, component prop interfaces, and error types that
* plugin React components use to communicate with the host.
*
* Plugin UI bundles are loaded as ES modules into designated extension slots.
* All communication with the plugin worker goes through the host bridge — plugin
* components must NOT access host internals or call host APIs directly.
*
* @see PLUGIN_SPEC.md §19 — UI Extension Model
* @see PLUGIN_SPEC.md §19.0.1 — Plugin UI SDK
* @see PLUGIN_SPEC.md §29.2 — SDK Versioning
*/
import type { PluginBridgeErrorCode } from "@paperclipai/shared";
import type { PluginLauncherRenderContextSnapshot, PluginModalBoundsRequest, PluginRenderCloseEvent } from "../protocol.js";
export type { PluginBridgeErrorCode, PluginLauncherBounds, PluginLauncherRenderEnvironment, } from "@paperclipai/shared";
export type { PluginLauncherRenderContextSnapshot, PluginModalBoundsRequest, PluginRenderCloseEvent, } from "../protocol.js";
/**
* Structured error returned by the bridge when a UI → worker call fails.
*
* Plugin components receive this in `usePluginData()` as the `error` field
* and may encounter it as a thrown value from `usePluginAction()`.
*
* Error codes:
* - `WORKER_UNAVAILABLE` — plugin worker is not running
* - `CAPABILITY_DENIED` — plugin lacks the required capability
* - `WORKER_ERROR` — worker returned an error from its handler
* - `TIMEOUT` — worker did not respond within the configured timeout
* - `UNKNOWN` — unexpected bridge-level failure
*
* @see PLUGIN_SPEC.md §19.7 — Error Propagation Through The Bridge
*/
export interface PluginBridgeError {
/** Machine-readable error code. */
code: PluginBridgeErrorCode;
/** Human-readable error message. */
message: string;
/**
* Original error details from the worker, if available.
* Only present when `code === "WORKER_ERROR"`.
*/
details?: unknown;
}
/**
* Read-only host context passed to every plugin component via `useHostContext()`.
*
* Plugin components use this to know which company, project, or entity is
* currently active so they can scope their data requests accordingly.
*
* @see PLUGIN_SPEC.md §19 — UI Extension Model
*/
export interface PluginHostContext {
/** UUID of the currently active company, if any. */
companyId: string | null;
/** URL prefix for the current company (e.g. `"my-company"`). */
companyPrefix: string | null;
/** UUID of the currently active project, if any. */
projectId: string | null;
/** UUID of the current entity (for detail tab contexts), if any. */
entityId: string | null;
/** Type of the current entity (e.g. `"issue"`, `"agent"`). */
entityType: string | null;
/**
* UUID of the parent entity when rendering nested slots.
* For `commentAnnotation` slots this is the issue ID containing the comment.
*/
parentEntityId?: string | null;
/** UUID of the current authenticated user. */
userId: string | null;
/** Runtime metadata for the host container currently rendering this plugin UI. */
renderEnvironment?: PluginRenderEnvironmentContext | null;
}
/**
* Async-capable callback invoked during a host-managed close lifecycle.
*/
export type PluginRenderCloseHandler = (event: PluginRenderCloseEvent) => void | Promise<void>;
/**
* Close lifecycle hooks available when the plugin UI is rendered inside a
* host-managed launcher environment.
*/
export interface PluginRenderCloseLifecycle {
/** Register a callback before the host closes the current environment. */
onBeforeClose?(handler: PluginRenderCloseHandler): () => void;
/** Register a callback after the host closes the current environment. */
onClose?(handler: PluginRenderCloseHandler): () => void;
}
/**
* Runtime information about the host container currently rendering a plugin UI.
*/
export interface PluginRenderEnvironmentContext extends PluginLauncherRenderContextSnapshot {
/** Optional host callback for requesting new bounds while a modal is open. */
requestModalBounds?(request: PluginModalBoundsRequest): Promise<void>;
/** Optional close lifecycle callbacks for host-managed overlays. */
closeLifecycle?: PluginRenderCloseLifecycle | null;
}
/**
* Props passed to a plugin page component.
*
* A page is a full-page extension at `/plugins/:pluginId` or `/:company/plugins/:pluginId`.
*
* @see PLUGIN_SPEC.md §19.1 — Global Operator Routes
* @see PLUGIN_SPEC.md §19.2 — Company-Context Routes
*/
export interface PluginPageProps {
/** The current host context. */
context: PluginHostContext;
}
/**
* Props passed to a plugin dashboard widget component.
*
* A dashboard widget is rendered as a card or section on the main dashboard.
*
* @see PLUGIN_SPEC.md §19.4 — Dashboard Widgets
*/
export interface PluginWidgetProps {
/** The current host context. */
context: PluginHostContext;
}
/**
* Props passed to a plugin detail tab component.
*
* A detail tab is rendered as an additional tab on a project, issue, agent,
* goal, or run detail page.
*
* @see PLUGIN_SPEC.md §19.3 — Detail Tabs
*/
export interface PluginDetailTabProps {
/** The current host context, always including `entityId` and `entityType`. */
context: PluginHostContext & {
entityId: string;
entityType: string;
};
}
/**
* Props passed to a plugin sidebar component.
*
* A sidebar entry adds a link or section to the application sidebar.
*
* @see PLUGIN_SPEC.md §19.5 — Sidebar Entries
*/
export interface PluginSidebarProps {
/** The current host context. */
context: PluginHostContext;
}
/**
* Props passed to a plugin project sidebar item component.
*
* A project sidebar item is rendered **once per project** under that project's
* row in the sidebar Projects list. The host passes the current project's id
* in `context.entityId` and `context.entityType` is `"project"`.
*
* Use this slot to add a link (e.g. "Files", "Linear Sync") that navigates to
* the project detail with a plugin tab selected: `/projects/:projectRef?tab=plugin:key:slotId`.
*
* @see PLUGIN_SPEC.md §19.5.1 — Project sidebar items
*/
export interface PluginProjectSidebarItemProps {
/** Host context plus entityId (project id) and entityType "project". */
context: PluginHostContext & {
entityId: string;
entityType: "project";
};
}
/**
* Props passed to a plugin comment annotation component.
*
* A comment annotation is rendered below each individual comment in the
* issue detail timeline. The host passes the comment ID as `entityId`
* and `"comment"` as `entityType`, plus the parent issue ID as
* `parentEntityId` so the plugin can scope data fetches to both.
*
* Use this slot to augment comments with parsed file links, sentiment
* badges, inline actions, or any per-comment metadata.
*
* @see PLUGIN_SPEC.md §19.6 — Comment Annotations
*/
export interface PluginCommentAnnotationProps {
/** Host context with comment and parent issue identifiers. */
context: PluginHostContext & {
/** UUID of the comment being annotated. */
entityId: string;
/** Always `"comment"` for comment annotation slots. */
entityType: "comment";
/** UUID of the parent issue containing this comment. */
parentEntityId: string;
};
}
/**
* Props passed to a plugin comment context menu item component.
*
* A comment context menu item is rendered in a "more" dropdown menu on
* each comment in the issue detail timeline. The host passes the comment
* ID as `entityId` and `"comment"` as `entityType`, plus the parent
* issue ID as `parentEntityId`.
*
* Use this slot to add per-comment actions such as "Create sub-issue from
* comment", "Translate", "Flag for review", or any custom plugin action.
*
* @see PLUGIN_SPEC.md §19.7 — Comment Context Menu Items
*/
export interface PluginCommentContextMenuItemProps {
/** Host context with comment and parent issue identifiers. */
context: PluginHostContext & {
/** UUID of the comment this menu item acts on. */
entityId: string;
/** Always `"comment"` for comment context menu item slots. */
entityType: "comment";
/** UUID of the parent issue containing this comment. */
parentEntityId: string;
};
}
/**
* Props passed to a plugin settings page component.
*
* Overrides the auto-generated JSON Schema form when the plugin declares
* a `settingsPage` UI slot. The component is responsible for reading and
* writing config through the bridge.
*
* @see PLUGIN_SPEC.md §19.8 — Plugin Settings UI
*/
export interface PluginSettingsPageProps {
/** The current host context. */
context: PluginHostContext;
}
/**
* Return value of `usePluginData(key, params)`.
*
* Mirrors a standard async data-fetching hook pattern:
* exactly one of `data` or `error` is non-null at any time (unless `loading`).
*
* @template T The type of the data returned by the worker handler
*
* @see PLUGIN_SPEC.md §19.7 — Error Propagation Through The Bridge
*/
export interface PluginDataResult<T = unknown> {
/** The data returned by the worker's `getData` handler. `null` while loading or on error. */
data: T | null;
/** `true` while the initial request or a refresh is in flight. */
loading: boolean;
/** Bridge error if the request failed. `null` on success or while loading. */
error: PluginBridgeError | null;
/**
* Manually trigger a data refresh.
* Useful for poll-based updates or post-action refreshes.
*/
refresh(): void;
}
export type PluginToastTone = "info" | "success" | "warn" | "error";
export interface PluginToastAction {
label: string;
href: string;
}
export interface PluginToastInput {
id?: string;
dedupeKey?: string;
title: string;
body?: string;
tone?: PluginToastTone;
ttlMs?: number;
action?: PluginToastAction;
}
export type PluginToastFn = (input: PluginToastInput) => string | null;
/**
* Return value of `usePluginStream<T>(channel)`.
*
* Provides a growing array of events pushed from the plugin worker via SSE,
* plus connection status metadata.
*
* @template T The type of each event emitted by the worker
*
* @see PLUGIN_SPEC.md §19.8 — Real-Time Streaming
*/
export interface PluginStreamResult<T = unknown> {
/** All events received so far, in arrival order. */
events: T[];
/** The most recently received event, or `null` if none yet. */
lastEvent: T | null;
/** `true` while the SSE connection is being established. */
connecting: boolean;
/** `true` once the SSE connection is open and receiving events. */
connected: boolean;
/** Error if the SSE connection failed or was interrupted. `null` otherwise. */
error: Error | null;
/** Close the SSE connection and stop receiving events. */
close(): void;
}
/**
* Return value of `usePluginAction(key)`.
*
* Returns an async function that, when called, sends an action request
* to the worker's `performAction` handler and returns the result.
*
* On failure, the async function throws a `PluginBridgeError`.
*
* @see PLUGIN_SPEC.md §19.7 — Error Propagation Through The Bridge
*
* @example
* ```tsx
* const resync = usePluginAction("resync");
* <button onClick={() => resync({ companyId }).catch(err => console.error(err))}>
* Resync Now
* </button>
* ```
*/
export type PluginActionFn = (params?: Record<string, unknown>) => Promise<unknown>;
//# sourceMappingURL=types.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/ui/types.ts"],"names":[],"mappings":"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"}

17
node_modules/@paperclipai/plugin-sdk/dist/ui/types.js generated vendored Normal file
View File

@@ -0,0 +1,17 @@
/**
* Paperclip plugin UI SDK — types for plugin frontend components.
*
* Plugin UI bundles import from `@paperclipai/plugin-sdk/ui`. This subpath
* provides the bridge hooks, component prop interfaces, and error types that
* plugin React components use to communicate with the host.
*
* Plugin UI bundles are loaded as ES modules into designated extension slots.
* All communication with the plugin worker goes through the host bridge — plugin
* components must NOT access host internals or call host APIs directly.
*
* @see PLUGIN_SPEC.md §19 — UI Extension Model
* @see PLUGIN_SPEC.md §19.0.1 — Plugin UI SDK
* @see PLUGIN_SPEC.md §29.2 — SDK Versioning
*/
export {};
//# sourceMappingURL=types.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/ui/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG"}

View File

@@ -0,0 +1,127 @@
/**
* Worker-side RPC host — runs inside the child process spawned by the host.
*
* This module is the worker-side counterpart to the server's
* `PluginWorkerManager`. It:
*
* 1. Reads newline-delimited JSON-RPC 2.0 requests from **stdin**
* 2. Dispatches them to the appropriate plugin handler (events, jobs, tools, …)
* 3. Writes JSON-RPC 2.0 responses back on **stdout**
* 4. Provides a concrete `PluginContext` whose SDK client methods (e.g.
* `ctx.state.get()`, `ctx.events.emit()`) send JSON-RPC requests to the
* host on stdout and await responses on stdin.
*
* ## Message flow
*
* ```
* Host (parent) Worker (this module)
* | |
* |--- request(initialize) -------------> | → calls plugin.setup(ctx)
* |<-- response(ok:true) ---------------- |
* | |
* |--- notification(onEvent) -----------> | → dispatches to registered handler
* | |
* |<-- request(state.get) --------------- | ← SDK client call from plugin code
* |--- response(result) ----------------> |
* | |
* |--- request(shutdown) ---------------> | → calls plugin.onShutdown()
* |<-- response(void) ------------------ |
* | (process exits)
* ```
*
* @see PLUGIN_SPEC.md §12 — Process Model
* @see PLUGIN_SPEC.md §13 — Host-Worker Protocol
* @see PLUGIN_SPEC.md §14 — SDK Surface
*/
import type { PaperclipPlugin } from "./define-plugin.js";
/**
* Options for starting the worker-side RPC host.
*/
export interface WorkerRpcHostOptions {
/**
* The plugin definition returned by `definePlugin()`.
*
* The worker entrypoint should import its plugin and pass it here.
*/
plugin: PaperclipPlugin;
/**
* Input stream to read JSON-RPC messages from.
* Defaults to `process.stdin`.
*/
stdin?: NodeJS.ReadableStream;
/**
* Output stream to write JSON-RPC messages to.
* Defaults to `process.stdout`.
*/
stdout?: NodeJS.WritableStream;
/**
* Default timeout (ms) for worker→host RPC calls.
* Defaults to 30 000 ms.
*/
rpcTimeoutMs?: number;
}
/**
* A running worker RPC host instance.
*
* Returned by `startWorkerRpcHost()`. Callers (usually just the worker
* bootstrap) hold a reference so they can inspect status or force-stop.
*/
export interface WorkerRpcHost {
/** Whether the host is currently running and listening for messages. */
readonly running: boolean;
/**
* Stop the RPC host immediately. Closes readline, rejects pending
* outbound calls, and does NOT call the plugin's shutdown hook (that
* should have already been called via the `shutdown` RPC method).
*/
stop(): void;
}
/**
* Options for runWorker when testing (optional stdio to avoid using process streams).
* When both stdin and stdout are provided, the "is main module" check is skipped
* and the host is started with these streams. Used by tests.
*/
export interface RunWorkerOptions {
stdin?: NodeJS.ReadableStream;
stdout?: NodeJS.WritableStream;
}
/**
* Start the worker when this module is the process entrypoint.
*
* Call this at the bottom of your worker file so that when the host runs
* `node dist/worker.js`, the RPC host starts and the process stays alive.
* When the module is imported (e.g. for re-exports or tests), nothing runs.
*
* When `options.stdin` and `options.stdout` are provided (e.g. in tests),
* the main-module check is skipped and the host is started with those streams.
*
* @example
* ```ts
* const plugin = definePlugin({ ... });
* export default plugin;
* runWorker(plugin, import.meta.url);
* ```
*/
export declare function runWorker(plugin: PaperclipPlugin, moduleUrl: string, options?: RunWorkerOptions): WorkerRpcHost | void;
/**
* Start the worker-side RPC host.
*
* This function is typically called from a thin bootstrap script that is the
* actual entrypoint of the child process:
*
* ```ts
* // worker-bootstrap.ts
* import plugin from "./worker.js";
* import { startWorkerRpcHost } from "@paperclipai/plugin-sdk";
*
* startWorkerRpcHost({ plugin });
* ```
*
* The host begins listening on stdin immediately. It does NOT call
* `plugin.definition.setup()` yet — that happens when the host sends the
* `initialize` RPC.
*
* @returns A handle for inspecting or stopping the RPC host
*/
export declare function startWorkerRpcHost(options: WorkerRpcHostOptions): WorkerRpcHost;
//# sourceMappingURL=worker-rpc-host.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"worker-rpc-host.d.ts","sourceRoot":"","sources":["../src/worker-rpc-host.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAQH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAwD1D;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC;;;;OAIG;IACH,MAAM,EAAE,eAAe,CAAC;IAExB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC,cAAc,CAAC;IAE9B;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC,cAAc,CAAC;IAE/B;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;;;GAKG;AACH,MAAM,WAAW,aAAa;IAC5B,wEAAwE;IACxE,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAE1B;;;;OAIG;IACH,IAAI,IAAI,IAAI,CAAC;CACd;AAuBD;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAC,cAAc,CAAC;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAC,cAAc,CAAC;CAChC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,SAAS,CACvB,MAAM,EAAE,eAAe,EACvB,SAAS,EAAE,MAAM,EACjB,OAAO,CAAC,EAAE,gBAAgB,GACzB,aAAa,GAAG,IAAI,CAkBtB;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,oBAAoB,GAAG,aAAa,CA0+B/E"}

View File

@@ -0,0 +1,941 @@
/**
* Worker-side RPC host — runs inside the child process spawned by the host.
*
* This module is the worker-side counterpart to the server's
* `PluginWorkerManager`. It:
*
* 1. Reads newline-delimited JSON-RPC 2.0 requests from **stdin**
* 2. Dispatches them to the appropriate plugin handler (events, jobs, tools, …)
* 3. Writes JSON-RPC 2.0 responses back on **stdout**
* 4. Provides a concrete `PluginContext` whose SDK client methods (e.g.
* `ctx.state.get()`, `ctx.events.emit()`) send JSON-RPC requests to the
* host on stdout and await responses on stdin.
*
* ## Message flow
*
* ```
* Host (parent) Worker (this module)
* | |
* |--- request(initialize) -------------> | → calls plugin.setup(ctx)
* |<-- response(ok:true) ---------------- |
* | |
* |--- notification(onEvent) -----------> | → dispatches to registered handler
* | |
* |<-- request(state.get) --------------- | ← SDK client call from plugin code
* |--- response(result) ----------------> |
* | |
* |--- request(shutdown) ---------------> | → calls plugin.onShutdown()
* |<-- response(void) ------------------ |
* | (process exits)
* ```
*
* @see PLUGIN_SPEC.md §12 — Process Model
* @see PLUGIN_SPEC.md §13 — Host-Worker Protocol
* @see PLUGIN_SPEC.md §14 — SDK Surface
*/
import path from "node:path";
import { createInterface } from "node:readline";
import { fileURLToPath } from "node:url";
import { JSONRPC_ERROR_CODES, PLUGIN_RPC_ERROR_CODES, createRequest, createSuccessResponse, createErrorResponse, createNotification, parseMessage, serializeMessage, isJsonRpcRequest, isJsonRpcResponse, isJsonRpcNotification, isJsonRpcSuccessResponse, isJsonRpcErrorResponse, JsonRpcParseError, JsonRpcCallError, } from "./protocol.js";
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
/** Default timeout for worker→host RPC calls. */
const DEFAULT_RPC_TIMEOUT_MS = 30_000;
/**
* Start the worker when this module is the process entrypoint.
*
* Call this at the bottom of your worker file so that when the host runs
* `node dist/worker.js`, the RPC host starts and the process stays alive.
* When the module is imported (e.g. for re-exports or tests), nothing runs.
*
* When `options.stdin` and `options.stdout` are provided (e.g. in tests),
* the main-module check is skipped and the host is started with those streams.
*
* @example
* ```ts
* const plugin = definePlugin({ ... });
* export default plugin;
* runWorker(plugin, import.meta.url);
* ```
*/
export function runWorker(plugin, moduleUrl, options) {
if (options?.stdin != null &&
options?.stdout != null) {
return startWorkerRpcHost({
plugin,
stdin: options.stdin,
stdout: options.stdout,
});
}
const entry = process.argv[1];
if (typeof entry !== "string")
return;
const thisFile = path.resolve(fileURLToPath(moduleUrl));
const entryPath = path.resolve(entry);
if (thisFile === entryPath) {
startWorkerRpcHost({ plugin });
}
}
/**
* Start the worker-side RPC host.
*
* This function is typically called from a thin bootstrap script that is the
* actual entrypoint of the child process:
*
* ```ts
* // worker-bootstrap.ts
* import plugin from "./worker.js";
* import { startWorkerRpcHost } from "@paperclipai/plugin-sdk";
*
* startWorkerRpcHost({ plugin });
* ```
*
* The host begins listening on stdin immediately. It does NOT call
* `plugin.definition.setup()` yet — that happens when the host sends the
* `initialize` RPC.
*
* @returns A handle for inspecting or stopping the RPC host
*/
export function startWorkerRpcHost(options) {
const { plugin } = options;
const stdinStream = options.stdin ?? process.stdin;
const stdoutStream = options.stdout ?? process.stdout;
const rpcTimeoutMs = options.rpcTimeoutMs ?? DEFAULT_RPC_TIMEOUT_MS;
// -----------------------------------------------------------------------
// State
// -----------------------------------------------------------------------
let running = true;
let initialized = false;
let manifest = null;
let currentConfig = {};
// Plugin handler registrations (populated during setup())
const eventHandlers = [];
const jobHandlers = new Map();
const launcherRegistrations = new Map();
const dataHandlers = new Map();
const actionHandlers = new Map();
const toolHandlers = new Map();
// Agent session event callbacks (populated by sendMessage, cleared by close)
const sessionEventCallbacks = new Map();
// Pending outbound (worker→host) requests
const pendingRequests = new Map();
let nextOutboundId = 1;
const MAX_OUTBOUND_ID = Number.MAX_SAFE_INTEGER - 1;
// -----------------------------------------------------------------------
// Outbound messaging (worker → host)
// -----------------------------------------------------------------------
function sendMessage(message) {
if (!running)
return;
const serialized = serializeMessage(message);
stdoutStream.write(serialized);
}
/**
* Send a typed JSON-RPC request to the host and await the response.
*/
function callHost(method, params, timeoutMs) {
return new Promise((resolve, reject) => {
if (!running) {
reject(new Error(`Cannot call "${method}" — worker RPC host is not running`));
return;
}
if (nextOutboundId >= MAX_OUTBOUND_ID) {
nextOutboundId = 1;
}
const id = nextOutboundId++;
const timeout = timeoutMs ?? rpcTimeoutMs;
let settled = false;
const settle = (fn, value) => {
if (settled)
return;
settled = true;
clearTimeout(timer);
pendingRequests.delete(id);
fn(value);
};
const timer = setTimeout(() => {
settle(reject, new JsonRpcCallError({
code: PLUGIN_RPC_ERROR_CODES.TIMEOUT,
message: `Worker→host call "${method}" timed out after ${timeout}ms`,
}));
}, timeout);
pendingRequests.set(id, {
resolve: (response) => {
if (isJsonRpcSuccessResponse(response)) {
settle(resolve, response.result);
}
else if (isJsonRpcErrorResponse(response)) {
settle(reject, new JsonRpcCallError(response.error));
}
else {
settle(reject, new Error(`Unexpected response format for "${method}"`));
}
},
timer,
});
try {
const request = createRequest(method, params, id);
sendMessage(request);
}
catch (err) {
settle(reject, err instanceof Error ? err : new Error(String(err)));
}
});
}
/**
* Send a JSON-RPC notification to the host (fire-and-forget).
*/
function notifyHost(method, params) {
try {
sendMessage(createNotification(method, params));
}
catch {
// Swallow — the host may have closed stdin
}
}
// -----------------------------------------------------------------------
// Build the PluginContext (SDK surface for plugin code)
// -----------------------------------------------------------------------
function buildContext() {
return {
get manifest() {
if (!manifest)
throw new Error("Plugin context accessed before initialization");
return manifest;
},
config: {
async get() {
return callHost("config.get", {});
},
},
events: {
on(name, filterOrFn, maybeFn) {
let registration;
if (typeof filterOrFn === "function") {
registration = { name, fn: filterOrFn };
}
else {
if (!maybeFn)
throw new Error("Event handler function is required");
registration = { name, filter: filterOrFn, fn: maybeFn };
}
eventHandlers.push(registration);
// Register subscription on the host so events are forwarded to this worker
void callHost("events.subscribe", { eventPattern: name, filter: registration.filter ?? null }).catch((err) => {
notifyHost("log", {
level: "warn",
message: `Failed to subscribe to event "${name}" on host: ${err instanceof Error ? err.message : String(err)}`,
});
});
return () => {
const idx = eventHandlers.indexOf(registration);
if (idx !== -1)
eventHandlers.splice(idx, 1);
};
},
async emit(name, companyId, payload) {
await callHost("events.emit", { name, companyId, payload });
},
},
jobs: {
register(key, fn) {
jobHandlers.set(key, fn);
},
},
launchers: {
register(launcher) {
launcherRegistrations.set(launcher.id, launcher);
},
},
http: {
async fetch(url, init) {
const serializedInit = {};
if (init) {
if (init.method)
serializedInit.method = init.method;
if (init.headers) {
// Normalize headers to a plain object
if (init.headers instanceof Headers) {
const obj = {};
init.headers.forEach((v, k) => { obj[k] = v; });
serializedInit.headers = obj;
}
else if (Array.isArray(init.headers)) {
const obj = {};
for (const [k, v] of init.headers)
obj[k] = v;
serializedInit.headers = obj;
}
else {
serializedInit.headers = init.headers;
}
}
if (init.body !== undefined && init.body !== null) {
serializedInit.body = typeof init.body === "string"
? init.body
: String(init.body);
}
}
const result = await callHost("http.fetch", {
url,
init: Object.keys(serializedInit).length > 0 ? serializedInit : undefined,
});
// Reconstruct a Response-like object from the serialized result
return new Response(result.body, {
status: result.status,
statusText: result.statusText,
headers: result.headers,
});
},
},
secrets: {
async resolve(secretRef) {
return callHost("secrets.resolve", { secretRef });
},
},
activity: {
async log(entry) {
await callHost("activity.log", {
companyId: entry.companyId,
message: entry.message,
entityType: entry.entityType,
entityId: entry.entityId,
metadata: entry.metadata,
});
},
},
state: {
async get(input) {
return callHost("state.get", {
scopeKind: input.scopeKind,
scopeId: input.scopeId,
namespace: input.namespace,
stateKey: input.stateKey,
});
},
async set(input, value) {
await callHost("state.set", {
scopeKind: input.scopeKind,
scopeId: input.scopeId,
namespace: input.namespace,
stateKey: input.stateKey,
value,
});
},
async delete(input) {
await callHost("state.delete", {
scopeKind: input.scopeKind,
scopeId: input.scopeId,
namespace: input.namespace,
stateKey: input.stateKey,
});
},
},
entities: {
async upsert(input) {
return callHost("entities.upsert", {
entityType: input.entityType,
scopeKind: input.scopeKind,
scopeId: input.scopeId,
externalId: input.externalId,
title: input.title,
status: input.status,
data: input.data,
});
},
async list(query) {
return callHost("entities.list", {
entityType: query.entityType,
scopeKind: query.scopeKind,
scopeId: query.scopeId,
externalId: query.externalId,
limit: query.limit,
offset: query.offset,
});
},
},
projects: {
async list(input) {
return callHost("projects.list", {
companyId: input.companyId,
limit: input.limit,
offset: input.offset,
});
},
async get(projectId, companyId) {
return callHost("projects.get", { projectId, companyId });
},
async listWorkspaces(projectId, companyId) {
return callHost("projects.listWorkspaces", { projectId, companyId });
},
async getPrimaryWorkspace(projectId, companyId) {
return callHost("projects.getPrimaryWorkspace", { projectId, companyId });
},
async getWorkspaceForIssue(issueId, companyId) {
return callHost("projects.getWorkspaceForIssue", { issueId, companyId });
},
},
companies: {
async list(input) {
return callHost("companies.list", {
limit: input?.limit,
offset: input?.offset,
});
},
async get(companyId) {
return callHost("companies.get", { companyId });
},
},
issues: {
async list(input) {
return callHost("issues.list", {
companyId: input.companyId,
projectId: input.projectId,
assigneeAgentId: input.assigneeAgentId,
status: input.status,
limit: input.limit,
offset: input.offset,
});
},
async get(issueId, companyId) {
return callHost("issues.get", { issueId, companyId });
},
async create(input) {
return callHost("issues.create", {
companyId: input.companyId,
projectId: input.projectId,
goalId: input.goalId,
parentId: input.parentId,
title: input.title,
description: input.description,
priority: input.priority,
assigneeAgentId: input.assigneeAgentId,
});
},
async update(issueId, patch, companyId) {
return callHost("issues.update", {
issueId,
patch: patch,
companyId,
});
},
async listComments(issueId, companyId) {
return callHost("issues.listComments", { issueId, companyId });
},
async createComment(issueId, body, companyId) {
return callHost("issues.createComment", { issueId, body, companyId });
},
documents: {
async list(issueId, companyId) {
return callHost("issues.documents.list", { issueId, companyId });
},
async get(issueId, key, companyId) {
return callHost("issues.documents.get", { issueId, key, companyId });
},
async upsert(input) {
return callHost("issues.documents.upsert", {
issueId: input.issueId,
key: input.key,
body: input.body,
companyId: input.companyId,
title: input.title,
format: input.format,
changeSummary: input.changeSummary,
});
},
async delete(issueId, key, companyId) {
return callHost("issues.documents.delete", { issueId, key, companyId });
},
},
},
agents: {
async list(input) {
return callHost("agents.list", {
companyId: input.companyId,
status: input.status,
limit: input.limit,
offset: input.offset,
});
},
async get(agentId, companyId) {
return callHost("agents.get", { agentId, companyId });
},
async pause(agentId, companyId) {
return callHost("agents.pause", { agentId, companyId });
},
async resume(agentId, companyId) {
return callHost("agents.resume", { agentId, companyId });
},
async invoke(agentId, companyId, opts) {
return callHost("agents.invoke", { agentId, companyId, prompt: opts.prompt, reason: opts.reason });
},
sessions: {
async create(agentId, companyId, opts) {
return callHost("agents.sessions.create", {
agentId,
companyId,
taskKey: opts?.taskKey,
reason: opts?.reason,
});
},
async list(agentId, companyId) {
return callHost("agents.sessions.list", { agentId, companyId });
},
async sendMessage(sessionId, companyId, opts) {
if (opts.onEvent) {
sessionEventCallbacks.set(sessionId, opts.onEvent);
}
try {
return await callHost("agents.sessions.sendMessage", {
sessionId,
companyId,
prompt: opts.prompt,
reason: opts.reason,
});
}
catch (err) {
sessionEventCallbacks.delete(sessionId);
throw err;
}
},
async close(sessionId, companyId) {
sessionEventCallbacks.delete(sessionId);
await callHost("agents.sessions.close", { sessionId, companyId });
},
},
},
goals: {
async list(input) {
return callHost("goals.list", {
companyId: input.companyId,
level: input.level,
status: input.status,
limit: input.limit,
offset: input.offset,
});
},
async get(goalId, companyId) {
return callHost("goals.get", { goalId, companyId });
},
async create(input) {
return callHost("goals.create", {
companyId: input.companyId,
title: input.title,
description: input.description,
level: input.level,
status: input.status,
parentId: input.parentId,
ownerAgentId: input.ownerAgentId,
});
},
async update(goalId, patch, companyId) {
return callHost("goals.update", {
goalId,
patch: patch,
companyId,
});
},
},
data: {
register(key, handler) {
dataHandlers.set(key, handler);
},
},
actions: {
register(key, handler) {
actionHandlers.set(key, handler);
},
},
streams: (() => {
// Track channel → companyId so emit/close don't require companyId
const channelCompanyMap = new Map();
return {
open(channel, companyId) {
channelCompanyMap.set(channel, companyId);
notifyHost("streams.open", { channel, companyId });
},
emit(channel, event) {
const companyId = channelCompanyMap.get(channel) ?? "";
notifyHost("streams.emit", { channel, companyId, event });
},
close(channel) {
const companyId = channelCompanyMap.get(channel) ?? "";
channelCompanyMap.delete(channel);
notifyHost("streams.close", { channel, companyId });
},
};
})(),
tools: {
register(name, declaration, fn) {
toolHandlers.set(name, { declaration, fn });
},
},
metrics: {
async write(name, value, tags) {
await callHost("metrics.write", { name, value, tags });
},
},
logger: {
info(message, meta) {
notifyHost("log", { level: "info", message, meta });
},
warn(message, meta) {
notifyHost("log", { level: "warn", message, meta });
},
error(message, meta) {
notifyHost("log", { level: "error", message, meta });
},
debug(message, meta) {
notifyHost("log", { level: "debug", message, meta });
},
},
};
}
const ctx = buildContext();
// -----------------------------------------------------------------------
// Inbound message handling (host → worker)
// -----------------------------------------------------------------------
/**
* Handle an incoming JSON-RPC request from the host.
*
* Dispatches to the correct handler based on the method name.
*/
async function handleHostRequest(request) {
const { id, method, params } = request;
try {
const result = await dispatchMethod(method, params);
sendMessage(createSuccessResponse(id, result ?? null));
}
catch (err) {
const errorMessage = err instanceof Error ? err.message : String(err);
// Propagate specific error codes from handler errors (e.g.
// METHOD_NOT_FOUND, METHOD_NOT_IMPLEMENTED) — fall back to
// WORKER_ERROR for untyped exceptions.
const errorCode = typeof err?.code === "number"
? err.code
: PLUGIN_RPC_ERROR_CODES.WORKER_ERROR;
sendMessage(createErrorResponse(id, errorCode, errorMessage));
}
}
/**
* Dispatch a host→worker method call to the appropriate handler.
*/
async function dispatchMethod(method, params) {
switch (method) {
case "initialize":
return handleInitialize(params);
case "health":
return handleHealth();
case "shutdown":
return handleShutdown();
case "validateConfig":
return handleValidateConfig(params);
case "configChanged":
return handleConfigChanged(params);
case "onEvent":
return handleOnEvent(params);
case "runJob":
return handleRunJob(params);
case "handleWebhook":
return handleWebhook(params);
case "getData":
return handleGetData(params);
case "performAction":
return handlePerformAction(params);
case "executeTool":
return handleExecuteTool(params);
default:
throw Object.assign(new Error(`Unknown method: ${method}`), { code: JSONRPC_ERROR_CODES.METHOD_NOT_FOUND });
}
}
// -----------------------------------------------------------------------
// Host→Worker method handlers
// -----------------------------------------------------------------------
async function handleInitialize(params) {
if (initialized) {
throw new Error("Worker already initialized");
}
manifest = params.manifest;
currentConfig = params.config;
// Call the plugin's setup function
await plugin.definition.setup(ctx);
initialized = true;
// Report which optional methods this plugin implements
const supportedMethods = [];
if (plugin.definition.onValidateConfig)
supportedMethods.push("validateConfig");
if (plugin.definition.onConfigChanged)
supportedMethods.push("configChanged");
if (plugin.definition.onHealth)
supportedMethods.push("health");
if (plugin.definition.onShutdown)
supportedMethods.push("shutdown");
return { ok: true, supportedMethods };
}
async function handleHealth() {
if (plugin.definition.onHealth) {
return plugin.definition.onHealth();
}
// Default: report OK if the worker is alive
return { status: "ok" };
}
async function handleShutdown() {
if (plugin.definition.onShutdown) {
await plugin.definition.onShutdown();
}
// Schedule cleanup after we send the response.
// Use setImmediate to let the response flush before exiting.
// Only call process.exit() when running with real process streams.
// When custom streams are provided (tests), just clean up.
setImmediate(() => {
cleanup();
if (!options.stdin && !options.stdout) {
process.exit(0);
}
});
}
async function handleValidateConfig(params) {
if (!plugin.definition.onValidateConfig) {
throw Object.assign(new Error("validateConfig is not implemented by this plugin"), { code: PLUGIN_RPC_ERROR_CODES.METHOD_NOT_IMPLEMENTED });
}
return plugin.definition.onValidateConfig(params.config);
}
async function handleConfigChanged(params) {
currentConfig = params.config;
if (plugin.definition.onConfigChanged) {
await plugin.definition.onConfigChanged(params.config);
}
}
async function handleOnEvent(params) {
const event = params.event;
for (const registration of eventHandlers) {
// Check event type match
const exactMatch = registration.name === event.eventType;
const wildcardPluginAll = registration.name === "plugin.*" &&
event.eventType.startsWith("plugin.");
const wildcardPluginOne = registration.name.endsWith(".*") &&
event.eventType.startsWith(registration.name.slice(0, -1));
if (!exactMatch && !wildcardPluginAll && !wildcardPluginOne)
continue;
// Check filter
if (registration.filter && !allowsEvent(registration.filter, event))
continue;
try {
await registration.fn(event);
}
catch (err) {
// Log error but continue processing other handlers so one failing
// handler doesn't prevent the rest from running.
notifyHost("log", {
level: "error",
message: `Event handler for "${registration.name}" failed: ${err instanceof Error ? err.message : String(err)}`,
meta: { eventType: event.eventType, stack: err instanceof Error ? err.stack : undefined },
});
}
}
}
async function handleRunJob(params) {
const handler = jobHandlers.get(params.job.jobKey);
if (!handler) {
throw new Error(`No handler registered for job "${params.job.jobKey}"`);
}
await handler(params.job);
}
async function handleWebhook(params) {
if (!plugin.definition.onWebhook) {
throw Object.assign(new Error("handleWebhook is not implemented by this plugin"), { code: PLUGIN_RPC_ERROR_CODES.METHOD_NOT_IMPLEMENTED });
}
await plugin.definition.onWebhook(params);
}
async function handleGetData(params) {
const handler = dataHandlers.get(params.key);
if (!handler) {
throw new Error(`No data handler registered for key "${params.key}"`);
}
return handler(params.renderEnvironment === undefined
? params.params
: { ...params.params, renderEnvironment: params.renderEnvironment });
}
async function handlePerformAction(params) {
const handler = actionHandlers.get(params.key);
if (!handler) {
throw new Error(`No action handler registered for key "${params.key}"`);
}
return handler(params.renderEnvironment === undefined
? params.params
: { ...params.params, renderEnvironment: params.renderEnvironment });
}
async function handleExecuteTool(params) {
const entry = toolHandlers.get(params.toolName);
if (!entry) {
throw new Error(`No tool handler registered for "${params.toolName}"`);
}
return entry.fn(params.parameters, params.runContext);
}
// -----------------------------------------------------------------------
// Event filter helper
// -----------------------------------------------------------------------
function allowsEvent(filter, event) {
const payload = event.payload;
if (filter.companyId !== undefined) {
const companyId = event.companyId ?? String(payload?.companyId ?? "");
if (companyId !== filter.companyId)
return false;
}
if (filter.projectId !== undefined) {
const projectId = event.entityType === "project"
? event.entityId
: String(payload?.projectId ?? "");
if (projectId !== filter.projectId)
return false;
}
if (filter.agentId !== undefined) {
const agentId = event.entityType === "agent"
? event.entityId
: String(payload?.agentId ?? "");
if (agentId !== filter.agentId)
return false;
}
return true;
}
// -----------------------------------------------------------------------
// Inbound response handling (host → worker, response to our outbound call)
// -----------------------------------------------------------------------
function handleHostResponse(response) {
const id = response.id;
if (id === null || id === undefined)
return;
const pending = pendingRequests.get(id);
if (!pending)
return;
clearTimeout(pending.timer);
pendingRequests.delete(id);
pending.resolve(response);
}
// -----------------------------------------------------------------------
// Incoming line handler
// -----------------------------------------------------------------------
function handleLine(line) {
if (!line.trim())
return;
let message;
try {
message = parseMessage(line);
}
catch (err) {
if (err instanceof JsonRpcParseError) {
// Send parse error response
sendMessage(createErrorResponse(null, JSONRPC_ERROR_CODES.PARSE_ERROR, `Parse error: ${err.message}`));
}
return;
}
if (isJsonRpcResponse(message)) {
// This is a response to one of our outbound worker→host calls
handleHostResponse(message);
}
else if (isJsonRpcRequest(message)) {
// This is a host→worker RPC call — dispatch it
handleHostRequest(message).catch((err) => {
// Unhandled error in the async handler — send error response
const errorMessage = err instanceof Error ? err.message : String(err);
const errorCode = err?.code ?? PLUGIN_RPC_ERROR_CODES.WORKER_ERROR;
try {
sendMessage(createErrorResponse(message.id, typeof errorCode === "number" ? errorCode : PLUGIN_RPC_ERROR_CODES.WORKER_ERROR, errorMessage));
}
catch {
// Cannot send response, stdout may be closed
}
});
}
else if (isJsonRpcNotification(message)) {
// Dispatch host→worker push notifications
const notif = message;
if (notif.method === "agents.sessions.event" && notif.params) {
const event = notif.params;
const cb = sessionEventCallbacks.get(event.sessionId);
if (cb)
cb(event);
}
else if (notif.method === "onEvent" && notif.params) {
// Plugin event bus notifications — dispatch to registered event handlers
handleOnEvent(notif.params).catch((err) => {
notifyHost("log", {
level: "error",
message: `Failed to handle event notification: ${err instanceof Error ? err.message : String(err)}`,
});
});
}
}
}
// -----------------------------------------------------------------------
// Cleanup
// -----------------------------------------------------------------------
function cleanup() {
running = false;
// Close readline
if (readline) {
readline.close();
readline = null;
}
// Reject all pending outbound calls
for (const [id, pending] of pendingRequests) {
clearTimeout(pending.timer);
pending.resolve(createErrorResponse(id, PLUGIN_RPC_ERROR_CODES.WORKER_UNAVAILABLE, "Worker RPC host is shutting down"));
}
pendingRequests.clear();
sessionEventCallbacks.clear();
}
// -----------------------------------------------------------------------
// Bootstrap: wire up stdin readline
// -----------------------------------------------------------------------
let readline = createInterface({
input: stdinStream,
crlfDelay: Infinity,
});
readline.on("line", handleLine);
// If stdin closes, we should exit gracefully
readline.on("close", () => {
if (running) {
cleanup();
if (!options.stdin && !options.stdout) {
process.exit(0);
}
}
});
// Handle uncaught errors in the worker process.
// Only install these when using the real process streams (not in tests
// where the caller provides custom streams).
if (!options.stdin && !options.stdout) {
process.on("uncaughtException", (err) => {
notifyHost("log", {
level: "error",
message: `Uncaught exception: ${err.message}`,
meta: { stack: err.stack },
});
// Give the notification a moment to flush, then exit
setTimeout(() => process.exit(1), 100);
});
process.on("unhandledRejection", (reason) => {
const message = reason instanceof Error ? reason.message : String(reason);
const stack = reason instanceof Error ? reason.stack : undefined;
notifyHost("log", {
level: "error",
message: `Unhandled rejection: ${message}`,
meta: { stack },
});
});
}
// -----------------------------------------------------------------------
// Return the handle
// -----------------------------------------------------------------------
return {
get running() {
return running;
},
stop() {
cleanup();
},
};
}
//# sourceMappingURL=worker-rpc-host.js.map

File diff suppressed because one or more lines are too long

88
node_modules/@paperclipai/plugin-sdk/package.json generated vendored Normal file
View File

@@ -0,0 +1,88 @@
{
"name": "@paperclipai/plugin-sdk",
"version": "2026.325.0",
"description": "Stable public API for Paperclip plugins — worker-side context and UI bridge hooks",
"license": "MIT",
"homepage": "https://github.com/paperclipai/paperclip",
"bugs": {
"url": "https://github.com/paperclipai/paperclip/issues"
},
"repository": {
"type": "git",
"url": "https://github.com/paperclipai/paperclip",
"directory": "packages/plugins/sdk"
},
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
},
"./protocol": {
"types": "./dist/protocol.d.ts",
"import": "./dist/protocol.js"
},
"./types": {
"types": "./dist/types.d.ts",
"import": "./dist/types.js"
},
"./ui": {
"types": "./dist/ui/index.d.ts",
"import": "./dist/ui/index.js"
},
"./ui/hooks": {
"types": "./dist/ui/hooks.d.ts",
"import": "./dist/ui/hooks.js"
},
"./ui/types": {
"types": "./dist/ui/types.d.ts",
"import": "./dist/ui/types.js"
},
"./testing": {
"types": "./dist/testing.d.ts",
"import": "./dist/testing.js"
},
"./bundlers": {
"types": "./dist/bundlers.d.ts",
"import": "./dist/bundlers.js"
},
"./dev-server": {
"types": "./dist/dev-server.d.ts",
"import": "./dist/dev-server.js"
}
},
"bin": {
"paperclip-plugin-dev-server": "./dist/dev-cli.js"
},
"publishConfig": {
"access": "public"
},
"files": [
"dist"
],
"dependencies": {
"@paperclipai/shared": "2026.325.0",
"zod": "^3.24.2"
},
"devDependencies": {
"@types/node": "^24.6.0",
"@types/react": "^19.0.8",
"typescript": "^5.7.3"
},
"peerDependencies": {
"react": ">=18"
},
"peerDependenciesMeta": {
"react": {
"optional": true
}
},
"scripts": {
"build": "pnpm --filter @paperclipai/shared build && tsc",
"clean": "rm -rf dist",
"typecheck": "pnpm --filter @paperclipai/shared build && tsc --noEmit",
"dev:server": "tsx src/dev-cli.ts"
},
"main": "./dist/index.js",
"types": "./dist/index.d.ts"
}