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

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":"AAAA;;;;;GAKG;AAiDH;;;;;GAKG;AACH,MAAM,UAAU,0BAA0B,CAAC,QAAkC,EAAE;IAC7E,MAAM,UAAU,GAAG;QACjB,4BAA4B;QAC5B,kCAAkC;QAClC,OAAO;QACP,WAAW;QACX,mBAAmB;KACpB,CAAC;IAEF,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,IAAI,MAAM,CAAC;IACtC,MAAM,WAAW,GAAG,KAAK,CAAC,WAAW,IAAI,eAAe,CAAC;IACzD,MAAM,aAAa,GAAG,KAAK,CAAC,aAAa,IAAI,iBAAiB,CAAC;IAC/D,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC;IAC9B,MAAM,SAAS,GAAG,KAAK,CAAC,SAAS,IAAI,IAAI,CAAC;IAC1C,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,IAAI,KAAK,CAAC;IAErC,MAAM,aAAa,GAAuB;QACxC,WAAW,EAAE,CAAC,WAAW,CAAC;QAC1B,MAAM;QACN,MAAM,EAAE,IAAI;QACZ,MAAM,EAAE,KAAK;QACb,QAAQ,EAAE,MAAM;QAChB,MAAM,EAAE,QAAQ;QAChB,SAAS;QACT,MAAM;QACN,QAAQ,EAAE,CAAC,OAAO,EAAE,WAAW,CAAC;KACjC,CAAC;IAEF,MAAM,eAAe,GAAuB;QAC1C,WAAW,EAAE,CAAC,aAAa,CAAC;QAC5B,MAAM;QACN,MAAM,EAAE,KAAK;QACb,MAAM,EAAE,KAAK;QACb,QAAQ,EAAE,MAAM;QAChB,MAAM,EAAE,QAAQ;QAChB,SAAS;KACV,CAAC;IAEF,MAAM,SAAS,GAAG,OAAO;QACvB,CAAC,CAAC;YACA,WAAW,EAAE,CAAC,OAAO,CAAC;YACtB,MAAM,EAAE,GAAG,MAAM,KAAK;YACtB,MAAM,EAAE,IAAI;YACZ,MAAM,EAAE,KAAc;YACtB,QAAQ,EAAE,SAAkB;YAC5B,MAAM,EAAE,QAAQ;YAChB,SAAS;YACT,MAAM;YACN,QAAQ,EAAE,UAAU;SACrB;QACD,CAAC,CAAC,SAAS,CAAC;IAEd,MAAM,YAAY,GAAqB;QACrC,KAAK,EAAE,WAAW;QAClB,MAAM,EAAE;YACN,GAAG,EAAE,MAAM;YACX,MAAM,EAAE,IAAI;YACZ,SAAS;YACT,cAAc,EAAE,WAAW;SAC5B;QACD,QAAQ,EAAE,CAAC,OAAO,EAAE,WAAW,CAAC;KACjC,CAAC;IAEF,MAAM,cAAc,GAAqB;QACvC,KAAK,EAAE,aAAa;QACpB,MAAM,EAAE;YACN,GAAG,EAAE,MAAM;YACX,MAAM,EAAE,IAAI;YACZ,SAAS;YACT,cAAc,EAAE,aAAa;SAC9B;QACD,QAAQ,EAAE,CAAC,yBAAyB,CAAC;KACtC,CAAC;IAEF,MAAM,QAAQ,GAAG,OAAO;QACtB,CAAC,CAAC;YACA,KAAK,EAAE,OAAO;YACd,MAAM,EAAE;gBACN,GAAG,EAAE,GAAG,MAAM,KAAK;gBACnB,MAAM,EAAE,IAAa;gBACrB,SAAS;gBACT,cAAc,EAAE,UAAU;aAC3B;YACD,QAAQ,EAAE,UAAU;SACrB;QACD,CAAC,CAAC,SAAS,CAAC;IAEd,OAAO;QACL,OAAO,EAAE;YACP,MAAM,EAAE,aAAa;YACrB,QAAQ,EAAE,eAAe;YACzB,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACxC;QACD,MAAM,EAAE;YACN,MAAM,EAAE,YAAY;YACpB,QAAQ,EAAE,cAAc;YACxB,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACtC;KACF,CAAC;AACJ,CAAC"}

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":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAO/B;;;GAGG;AACH,MAAM,WAAW,WAAW;IAC1B,8BAA8B;IAC9B,SAAS,EAAE,IAAI,GAAG,MAAM,GAAG,MAAM,CAAC;IAClC,uDAAuD;IACvD,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,8BAA8B;AAC9B,MAAM,WAAW,eAAe;IAC9B,kEAAkE;IAClE,KAAK,EAAE,MAAM,CAAC;IACd,mCAAmC;IACnC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IACvB,gCAAgC;IAChC,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,+DAA+D;IAC/D,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,iDAAiD;IACjD,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,yCAAyC;AACzC,MAAM,MAAM,kBAAkB,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,GAAG,MAAM,GAAG,SAAS,CAAC;AAEjF,+BAA+B;AAC/B,MAAM,WAAW,gBAAgB;IAC/B,4BAA4B;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,yCAAyC;IACzC,MAAM,EAAE,kBAAkB,CAAC;CAC5B;AAED,kDAAkD;AAClD,MAAM,WAAW,eAAe,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAC1D,sDAAsD;IACtD,GAAG,EAAE,MAAM,CAAC,GAAG,MAAM,CAAC;IACtB,2BAA2B;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,qCAAqC;IACrC,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,KAAK,KAAK,CAAC,SAAS,CAAC;IACrD,uCAAuC;IACvC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,2CAA2C;IAC3C,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,6BAA6B;AAC7B,MAAM,WAAW,cAAc,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IACzD,0BAA0B;IAC1B,OAAO,EAAE,eAAe,CAAC,CAAC,CAAC,EAAE,CAAC;IAC9B,0DAA0D;IAC1D,IAAI,EAAE,CAAC,EAAE,CAAC;IACV,8CAA8C;IAC9C,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,0CAA0C;IAC1C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,wEAAwE;IACxE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,8CAA8C;IAC9C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,sCAAsC;IACtC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,kCAAkC;IAClC,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC,wDAAwD;IACxD,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,GAAG,MAAM,KAAK,IAAI,CAAC;CAC3D;AAED,iDAAiD;AACjD,MAAM,WAAW,mBAAmB;IAClC,0BAA0B;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,qBAAqB;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,oCAAoC;IACpC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,mCAAmC;AACnC,MAAM,WAAW,oBAAoB;IACnC,mBAAmB;IACnB,IAAI,EAAE,mBAAmB,EAAE,CAAC;IAC5B,mBAAmB;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,oBAAoB;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,wCAAwC;IACxC,IAAI,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;IACtB,wDAAwD;IACxD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,8CAA8C;IAC9C,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,iCAAiC;AACjC,MAAM,WAAW,kBAAkB;IACjC,kCAAkC;IAClC,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,kDAAkD;AAClD,MAAM,WAAW,YAAY;IAC3B,yBAAyB;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,kEAAkE;IAClE,KAAK,EAAE,KAAK,CAAC,SAAS,CAAC;CACxB;AAED,gCAAgC;AAChC,MAAM,WAAW,iBAAiB;IAChC,mCAAmC;IACnC,KAAK,EAAE,YAAY,EAAE,CAAC;CACvB;AAED,8CAA8C;AAC9C,MAAM,WAAW,aAAa;IAC5B,oBAAoB;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,gDAAgD;IAChD,SAAS,EAAE,MAAM,CAAC;IAClB,yDAAyD;IACzD,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,+CAA+C;IAC/C,OAAO,CAAC,EAAE,SAAS,GAAG,SAAS,GAAG,aAAa,CAAC;IAChD,8DAA8D;IAC9D,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,sEAAsE;IACtE,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,6BAA6B;AAC7B,MAAM,WAAW,cAAc;IAC7B,0BAA0B;IAC1B,OAAO,EAAE,aAAa,EAAE,CAAC;IACzB,oEAAoE;IACpE,SAAS,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IACzD,mCAAmC;IACnC,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CACvD;AAED,uCAAuC;AACvC,MAAM,WAAW,YAAY;IAC3B,0BAA0B;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,iBAAiB;IACjB,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,OAAO,CAAC;IAC3C,mBAAmB;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,oCAAoC;IACpC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC;AAED,2BAA2B;AAC3B,MAAM,WAAW,YAAY;IAC3B,8BAA8B;IAC9B,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,qFAAqF;IACrF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kDAAkD;IAClD,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,4CAA4C;IAC5C,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,4BAA4B;AAC5B,MAAM,WAAW,aAAa;IAC5B,qDAAqD;IACrD,IAAI,EAAE,OAAO,CAAC;IACd,gDAAgD;IAChD,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,2BAA2B;AAC3B,MAAM,WAAW,YAAY;IAC3B,+CAA+C;IAC/C,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;IAC1B,+DAA+D;IAC/D,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,iCAAiC;AACjC,MAAM,WAAW,kBAAkB;IACjC,mDAAmD;IACnD,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,kEAAkE;IAClE,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC3B,gEAAgE;IAChE,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,CAAC,SAAS,KAAK,IAAI,CAAC;CACzD;AAqBD,eAAO,MAAM,UAAU,sCAAsD,CAAC;AAE9E;;;;GAIG;AACH,eAAO,MAAM,WAAW,uCAAwD,CAAC;AAEjF;;;;GAIG;AACH,eAAO,MAAM,SAAS,8DAAoD,CAAC;AAE3E;;;;GAIG;AACH,eAAO,MAAM,eAAe,2CAAgE,CAAC;AAE7F;;;;GAIG;AACH,eAAO,MAAM,aAAa,yCAA4D,CAAC;AAEvF;;;;GAIG;AACH,eAAO,MAAM,YAAY,wCAA0D,CAAC;AAEpF;;;;GAIG;AACH,eAAO,MAAM,SAAS,qCAAoD,CAAC;AAE3E;;;;GAIG;AACH,eAAO,MAAM,OAAO,mCAAgD,CAAC;AAErE;;;;GAIG;AACH,eAAO,MAAM,QAAQ,oCAAkD,CAAC;AAExE;;;;GAIG;AACH,eAAO,MAAM,OAAO,mCAAgD,CAAC;AAErE;;;;;GAKG;AACH,eAAO,MAAM,aAAa,yCAA4D,CAAC"}

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