Clarify plugin authoring and external dev workflow

This commit is contained in:
Dotta
2026-03-14 10:40:21 -05:00
parent cb5d7e76fb
commit 30888759f2
36 changed files with 693 additions and 410 deletions

View File

@@ -62,7 +62,6 @@ export function createPluginBundlerPresets(input: PluginBundlerPresetInput = {})
const uiExternal = [
"@paperclipai/plugin-sdk/ui",
"@paperclipai/plugin-sdk/ui/hooks",
"@paperclipai/plugin-sdk/ui/components",
"react",
"react-dom",
"react/jsx-runtime",
@@ -84,7 +83,7 @@ export function createPluginBundlerPresets(input: PluginBundlerPresetInput = {})
target: "node20",
sourcemap,
minify,
external: ["@paperclipai/plugin-sdk", "@paperclipai/plugin-sdk/ui", "react", "react-dom"],
external: ["react", "react-dom"],
};
const esbuildManifest: EsbuildLikeOptions = {
@@ -119,7 +118,7 @@ export function createPluginBundlerPresets(input: PluginBundlerPresetInput = {})
sourcemap,
entryFileNames: "worker.js",
},
external: ["@paperclipai/plugin-sdk", "react", "react-dom"],
external: ["react", "react-dom"],
};
const rollupManifest: RollupLikeConfig = {

View File

@@ -118,12 +118,6 @@ export interface HostServices {
resolve(params: WorkerToHostMethods["secrets.resolve"][0]): Promise<string>;
};
/** Provides `assets.upload`, `assets.getUrl`. */
assets: {
upload(params: WorkerToHostMethods["assets.upload"][0]): Promise<WorkerToHostMethods["assets.upload"][1]>;
getUrl(params: WorkerToHostMethods["assets.getUrl"][0]): Promise<string>;
};
/** Provides `activity.log`. */
activity: {
log(params: {
@@ -274,10 +268,6 @@ const METHOD_CAPABILITY_MAP: Record<WorkerToHostMethodName, PluginCapability | n
// Secrets
"secrets.resolve": "secrets.read-ref",
// Assets
"assets.upload": "assets.write",
"assets.getUrl": "assets.read",
// Activity
"activity.log": "activity.log.write",
@@ -428,14 +418,6 @@ export function createHostClientHandlers(
return services.secrets.resolve(params);
}),
// Assets
"assets.upload": gated("assets.upload", async (params) => {
return services.assets.upload(params);
}),
"assets.getUrl": gated("assets.getUrl", async (params) => {
return services.assets.getUrl(params);
}),
// Activity
"activity.log": gated("activity.log", async (params) => {
return services.activity.log(params);

View File

@@ -164,7 +164,6 @@ export type {
PluginLaunchersClient,
PluginHttpClient,
PluginSecretsClient,
PluginAssetsClient,
PluginActivityClient,
PluginActivityLogEntry,
PluginStateClient,

View File

@@ -495,16 +495,6 @@ export interface WorkerToHostMethods {
result: string,
];
// Assets
"assets.upload": [
params: { filename: string; contentType: string; data: string },
result: { assetId: string; url: string },
];
"assets.getUrl": [
params: { assetId: string },
result: string,
];
// Activity
"activity.log": [
params: {

View File

@@ -136,8 +136,6 @@ export function createTestHarness(options: TestHarnessOptions): TestHarness {
const state = new Map<string, unknown>();
const entities = new Map<string, PluginEntityRecord>();
const entityExternalIndex = new Map<string, string>();
const assets = new Map<string, { contentType: string; data: Uint8Array }>();
const companies = new Map<string, Company>();
const projects = new Map<string, Project>();
const issues = new Map<string, Issue>();
@@ -207,19 +205,6 @@ export function createTestHarness(options: TestHarnessOptions): TestHarness {
return `resolved:${secretRef}`;
},
},
assets: {
async upload(filename, contentType, data) {
requireCapability(manifest, capabilitySet, "assets.write");
const assetId = `asset_${randomUUID()}`;
assets.set(assetId, { contentType, data: data instanceof Uint8Array ? data : new Uint8Array(data) });
return { assetId, url: `memory://assets/${filename}` };
},
async getUrl(assetId) {
requireCapability(manifest, capabilitySet, "assets.read");
if (!assets.has(assetId)) throw new Error(`Asset not found: ${assetId}`);
return `memory://assets/${assetId}`;
},
},
activity: {
async log(entry) {
requireCapability(manifest, capabilitySet, "activity.log.write");

View File

@@ -452,34 +452,6 @@ export interface PluginSecretsClient {
resolve(secretRef: string): Promise<string>;
}
/**
* `ctx.assets` — read and write assets (files, images, etc.).
*
* `assets.read` capability required for `getUrl()`.
* `assets.write` capability required for `upload()`.
*
* @see PLUGIN_SPEC.md §15.1 — Capabilities: Data Write
*/
export interface PluginAssetsClient {
/**
* Upload an asset (e.g. a screenshot or generated file).
*
* @param filename - Name for the asset file
* @param contentType - MIME type
* @param data - Raw asset data as a Buffer or Uint8Array
* @returns The asset ID and public URL
*/
upload(filename: string, contentType: string, data: Buffer | Uint8Array): Promise<{ assetId: string; url: string }>;
/**
* Get the public URL for an existing asset by ID.
*
* @param assetId - Asset identifier
* @returns The public URL
*/
getUrl(assetId: string): Promise<string>;
}
/**
* Input for writing a plugin activity log entry.
*
@@ -1069,9 +1041,6 @@ export interface PluginContext {
/** Resolve secret references. Requires `secrets.read-ref`. */
secrets: PluginSecretsClient;
/** Read and write assets. Requires `assets.read` / `assets.write`. */
assets: PluginAssetsClient;
/** Write activity log entries. Requires `activity.log.write`. */
activity: PluginActivityClient;

View File

@@ -29,9 +29,9 @@ import { getSdkUiRuntimeValue } from "./runtime.js";
* companyId: context.companyId,
* });
*
* if (loading) return <Spinner />;
* if (loading) return <div>Loading…</div>;
* if (error) return <div>Error: {error.message}</div>;
* return <MetricCard label="Synced Issues" value={data!.syncedCount} />;
* return <div>Synced Issues: {data!.syncedCount}</div>;
* }
* ```
*

View File

@@ -12,14 +12,7 @@
* @example
* ```tsx
* // Plugin UI bundle entry (dist/ui/index.tsx)
* import {
* usePluginData,
* usePluginAction,
* useHostContext,
* MetricCard,
* StatusBadge,
* Spinner,
* } from "@paperclipai/plugin-sdk/ui";
* import { usePluginData, usePluginAction } from "@paperclipai/plugin-sdk/ui";
* import type { PluginWidgetProps } from "@paperclipai/plugin-sdk/ui";
*
* export function DashboardWidget({ context }: PluginWidgetProps) {
@@ -28,12 +21,13 @@
* });
* const resync = usePluginAction("resync");
*
* if (loading) return <Spinner />;
* if (loading) return <div>Loading…</div>;
* if (error) return <div>Error: {error.message}</div>;
*
* return (
* <div>
* <MetricCard label="Synced Issues" value={data!.syncedCount} />
* <div style={{ display: "grid", gap: 8 }}>
* <strong>Synced Issues</strong>
* <div>{data!.syncedCount}</div>
* <button onClick={() => resync({ companyId: context.companyId })}>
* Resync Now
* </button>
@@ -91,40 +85,3 @@ export type {
PluginCommentContextMenuItemProps,
PluginSettingsPageProps,
} from "./types.js";
// Shared UI components
export {
MetricCard,
StatusBadge,
DataTable,
TimeseriesChart,
MarkdownBlock,
KeyValueList,
ActionBar,
LogView,
JsonTree,
Spinner,
ErrorBoundary,
} from "./components.js";
// Shared component prop types (for plugin authors who need to extend them)
export type {
MetricCardProps,
MetricTrend,
StatusBadgeProps,
StatusBadgeVariant,
DataTableProps,
DataTableColumn,
TimeseriesChartProps,
TimeseriesDataPoint,
MarkdownBlockProps,
KeyValueListProps,
KeyValuePair,
ActionBarProps,
ActionBarItem,
LogViewProps,
LogViewEntry,
JsonTreeProps,
SpinnerProps,
ErrorBoundaryProps,
} from "./components.js";

View File

@@ -456,26 +456,6 @@ export function startWorkerRpcHost(options: WorkerRpcHostOptions): WorkerRpcHost
},
},
assets: {
async upload(
filename: string,
contentType: string,
data: Buffer | Uint8Array,
): Promise<{ assetId: string; url: string }> {
// Base64-encode binary data for JSON serialization
const base64 = Buffer.from(data).toString("base64");
return callHost("assets.upload", {
filename,
contentType,
data: base64,
});
},
async getUrl(assetId: string): Promise<string> {
return callHost("assets.getUrl", { assetId });
},
},
activity: {
async log(entry): Promise<void> {
await callHost("activity.log", {