File size: 8,803 Bytes
b152fd5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
/**
 * `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";

// ---------------------------------------------------------------------------
// Health check result
// ---------------------------------------------------------------------------

/**
 * 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>;
}

// ---------------------------------------------------------------------------
// Config validation result
// ---------------------------------------------------------------------------

/**
 * 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[];
}

// ---------------------------------------------------------------------------
// Webhook handler input
// ---------------------------------------------------------------------------

/**
 * 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;
}

// ---------------------------------------------------------------------------
// Plugin definition
// ---------------------------------------------------------------------------

/**
 * 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>;
}

// ---------------------------------------------------------------------------
// PaperclipPlugin — the sealed object returned by definePlugin()
// ---------------------------------------------------------------------------

/**
 * 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;
}

// ---------------------------------------------------------------------------
// 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: PluginDefinition): PaperclipPlugin {
  return Object.freeze({ definition });
}