From 582f466d32dccbae0ea6c5c2aa4909e944c6e301 Mon Sep 17 00:00:00 2001 From: Dotta Date: Sun, 10 May 2026 07:20:18 -0500 Subject: [PATCH] Remove bookmarks example plugin Co-Authored-By: Paperclip --- .../plugin-bookmarks-example/.gitignore | 3 - .../plugin-bookmarks-example/README.md | 53 --- .../esbuild.config.mjs | 17 - .../migrations/001_bookmarks.sql | 16 - .../plugin-bookmarks-example/package.json | 48 -- .../rollup.config.mjs | 28 -- .../plugin-bookmarks-example/src/manifest.ts | 102 ----- .../plugin-bookmarks-example/src/ui/index.tsx | 359 --------------- .../plugin-bookmarks-example/src/worker.ts | 421 ------------------ .../tests/plugin.spec.ts | 370 --------------- .../plugin-bookmarks-example/tsconfig.json | 27 -- .../plugin-bookmarks-example/vitest.config.ts | 8 - pnpm-workspace.yaml | 3 - server/src/routes/plugins.ts | 8 - 14 files changed, 1463 deletions(-) delete mode 100644 packages/plugins/examples/plugin-bookmarks-example/.gitignore delete mode 100644 packages/plugins/examples/plugin-bookmarks-example/README.md delete mode 100644 packages/plugins/examples/plugin-bookmarks-example/esbuild.config.mjs delete mode 100644 packages/plugins/examples/plugin-bookmarks-example/migrations/001_bookmarks.sql delete mode 100644 packages/plugins/examples/plugin-bookmarks-example/package.json delete mode 100644 packages/plugins/examples/plugin-bookmarks-example/rollup.config.mjs delete mode 100644 packages/plugins/examples/plugin-bookmarks-example/src/manifest.ts delete mode 100644 packages/plugins/examples/plugin-bookmarks-example/src/ui/index.tsx delete mode 100644 packages/plugins/examples/plugin-bookmarks-example/src/worker.ts delete mode 100644 packages/plugins/examples/plugin-bookmarks-example/tests/plugin.spec.ts delete mode 100644 packages/plugins/examples/plugin-bookmarks-example/tsconfig.json delete mode 100644 packages/plugins/examples/plugin-bookmarks-example/vitest.config.ts diff --git a/packages/plugins/examples/plugin-bookmarks-example/.gitignore b/packages/plugins/examples/plugin-bookmarks-example/.gitignore deleted file mode 100644 index 0430cc35608..00000000000 --- a/packages/plugins/examples/plugin-bookmarks-example/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -dist -node_modules -.paperclip-sdk diff --git a/packages/plugins/examples/plugin-bookmarks-example/README.md b/packages/plugins/examples/plugin-bookmarks-example/README.md deleted file mode 100644 index 17c48182f45..00000000000 --- a/packages/plugins/examples/plugin-bookmarks-example/README.md +++ /dev/null @@ -1,53 +0,0 @@ -# @paperclipai/plugin-bookmarks-example - -A first-party example plugin that adds a small but real product surface to Paperclip: a -company-scoped bookmark library backed by a plugin database namespace and a local markdown -folder. - -The package is a follow-up to the core plugin host surface work and is intended as a -focused reference for plugin authors. The orchestration smoke example remains the -acceptance fixture for the orchestration APIs; this plugin is the user-facing -counterpart that exercises the broader UI + storage surface. - -## Surfaces exercised - -- **Plugin database namespace.** A migration creates a `bookmarks` table inside the - plugin's restricted Postgres schema. The worker reads and writes only that - namespace; the restricted SQL helpers prevent cross-namespace access. -- **Local folders.** A `bookmarks-root` folder declaration is configured by the - operator and atomically receives one markdown file per bookmark via - `ctx.localFolders.writeTextAtomic`. The DB row remains the source of truth so - list and delete operations stay correct even when the folder is unconfigured - or the disk write fails. -- **Scoped API routes.** Three plugin-owned routes (`list`, `create`, `delete`) - declare board-or-agent auth and resolve the company from query/body - parameters before the worker sees them. -- **UI extension slots.** A full plugin page, a sidebar entry, a dashboard - widget, and a settings page are all wired up to the same worker handlers via - `usePluginData` / `usePluginAction`. - -## Install - -```sh -pnpm --filter @paperclipai/plugin-bookmarks-example build -pnpm paperclipai plugin install ./packages/plugins/examples/plugin-bookmarks-example -``` - -After install, configure the plugin's `bookmarks-root` local folder under the -plugin's Folders settings to enable the markdown sidecar files. The plugin works -without it (rows live in the database namespace), but operators that want -shareable, file-backed snapshots should point the folder at a checked-in -directory. - -## Development - -```sh -pnpm --filter @paperclipai/plugin-bookmarks-example test -pnpm --filter @paperclipai/plugin-bookmarks-example typecheck -pnpm --filter @paperclipai/plugin-bookmarks-example build -``` - -The worker code uses module-scoped handler closures populated in `setup()`, -mirroring the orchestration smoke example, so the handlers can be invoked from -both `ctx.data` / `ctx.actions` (UI) and `onApiRequest` (HTTP) without -duplicating logic. diff --git a/packages/plugins/examples/plugin-bookmarks-example/esbuild.config.mjs b/packages/plugins/examples/plugin-bookmarks-example/esbuild.config.mjs deleted file mode 100644 index b5cfd36ed95..00000000000 --- a/packages/plugins/examples/plugin-bookmarks-example/esbuild.config.mjs +++ /dev/null @@ -1,17 +0,0 @@ -import esbuild from "esbuild"; -import { createPluginBundlerPresets } from "@paperclipai/plugin-sdk/bundlers"; - -const presets = createPluginBundlerPresets({ uiEntry: "src/ui/index.tsx" }); -const watch = process.argv.includes("--watch"); - -const workerCtx = await esbuild.context(presets.esbuild.worker); -const manifestCtx = await esbuild.context(presets.esbuild.manifest); -const uiCtx = await esbuild.context(presets.esbuild.ui); - -if (watch) { - await Promise.all([workerCtx.watch(), manifestCtx.watch(), uiCtx.watch()]); - console.log("esbuild watch mode enabled for worker, manifest, and ui"); -} else { - await Promise.all([workerCtx.rebuild(), manifestCtx.rebuild(), uiCtx.rebuild()]); - await Promise.all([workerCtx.dispose(), manifestCtx.dispose(), uiCtx.dispose()]); -} diff --git a/packages/plugins/examples/plugin-bookmarks-example/migrations/001_bookmarks.sql b/packages/plugins/examples/plugin-bookmarks-example/migrations/001_bookmarks.sql deleted file mode 100644 index c326e11dab6..00000000000 --- a/packages/plugins/examples/plugin-bookmarks-example/migrations/001_bookmarks.sql +++ /dev/null @@ -1,16 +0,0 @@ -CREATE TABLE plugin_bookmarks_b34a9f8617.bookmarks ( - id uuid PRIMARY KEY, - company_id uuid NOT NULL, - slug text NOT NULL, - url text NOT NULL, - title text NOT NULL, - notes text NOT NULL DEFAULT '', - tags text[] NOT NULL DEFAULT '{}', - file_path text NOT NULL, - created_at timestamptz NOT NULL DEFAULT now(), - updated_at timestamptz NOT NULL DEFAULT now(), - CONSTRAINT bookmarks_company_slug_uq UNIQUE (company_id, slug) -); - -CREATE INDEX bookmarks_company_idx - ON plugin_bookmarks_b34a9f8617.bookmarks (company_id, created_at DESC); diff --git a/packages/plugins/examples/plugin-bookmarks-example/package.json b/packages/plugins/examples/plugin-bookmarks-example/package.json deleted file mode 100644 index be68a4b2e03..00000000000 --- a/packages/plugins/examples/plugin-bookmarks-example/package.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "name": "@paperclipai/plugin-bookmarks-example", - "version": "0.1.0", - "type": "module", - "private": true, - "description": "First-party example plugin: company-scoped bookmark library backed by a plugin database namespace and a local markdown folder.", - "scripts": { - "prebuild": "pnpm --filter @paperclipai/plugin-sdk ensure-build-deps", - "build": "node ./esbuild.config.mjs", - "build:rollup": "rollup -c", - "dev": "node ./esbuild.config.mjs --watch", - "dev:ui": "paperclip-plugin-dev-server --root . --ui-dir dist/ui --port 4178", - "test": "vitest run --config ./vitest.config.ts", - "typecheck": "pnpm --filter @paperclipai/plugin-sdk ensure-build-deps && tsc --noEmit" - }, - "paperclipPlugin": { - "manifest": "./dist/manifest.js", - "worker": "./dist/worker.js", - "ui": "./dist/ui/" - }, - "keywords": [ - "paperclip", - "plugin", - "example", - "bookmarks", - "knowledge" - ], - "author": "Paperclip", - "license": "MIT", - "dependencies": { - "@paperclipai/plugin-sdk": "workspace:*" - }, - "devDependencies": { - "@paperclipai/shared": "workspace:*", - "@rollup/plugin-node-resolve": "^16.0.1", - "@rollup/plugin-typescript": "^12.1.2", - "@types/node": "^24.6.0", - "@types/react": "^19.0.8", - "esbuild": "^0.27.3", - "rollup": "^4.38.0", - "tslib": "^2.8.1", - "typescript": "^5.7.3", - "vitest": "^3.0.5" - }, - "peerDependencies": { - "react": ">=18" - } -} diff --git a/packages/plugins/examples/plugin-bookmarks-example/rollup.config.mjs b/packages/plugins/examples/plugin-bookmarks-example/rollup.config.mjs deleted file mode 100644 index ccee40a7443..00000000000 --- a/packages/plugins/examples/plugin-bookmarks-example/rollup.config.mjs +++ /dev/null @@ -1,28 +0,0 @@ -import { nodeResolve } from "@rollup/plugin-node-resolve"; -import typescript from "@rollup/plugin-typescript"; -import { createPluginBundlerPresets } from "@paperclipai/plugin-sdk/bundlers"; - -const presets = createPluginBundlerPresets({ uiEntry: "src/ui/index.tsx" }); - -function withPlugins(config) { - if (!config) return null; - return { - ...config, - plugins: [ - nodeResolve({ - extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs"], - }), - typescript({ - tsconfig: "./tsconfig.json", - declaration: false, - declarationMap: false, - }), - ], - }; -} - -export default [ - withPlugins(presets.rollup.manifest), - withPlugins(presets.rollup.worker), - withPlugins(presets.rollup.ui), -].filter(Boolean); diff --git a/packages/plugins/examples/plugin-bookmarks-example/src/manifest.ts b/packages/plugins/examples/plugin-bookmarks-example/src/manifest.ts deleted file mode 100644 index 402efb51369..00000000000 --- a/packages/plugins/examples/plugin-bookmarks-example/src/manifest.ts +++ /dev/null @@ -1,102 +0,0 @@ -import type { PaperclipPluginManifestV1 } from "@paperclipai/plugin-sdk"; - -export const PLUGIN_ID = "paperclipai.plugin-bookmarks-example"; -export const BOOKMARKS_FOLDER_KEY = "bookmarks-root"; - -const manifest: PaperclipPluginManifestV1 = { - id: PLUGIN_ID, - apiVersion: 1, - version: "0.1.0", - displayName: "Bookmarks (Example)", - description: - "First-party example plugin: company-scoped bookmark library backed by a plugin database namespace and a local markdown folder. Demonstrates scoped API routes, local folders, dashboard widget, and a plugin page.", - author: "Paperclip", - categories: ["workspace", "ui"], - capabilities: [ - "api.routes.register", - "database.namespace.migrate", - "database.namespace.read", - "database.namespace.write", - "local.folders", - "companies.read", - "ui.dashboardWidget.register", - "ui.page.register", - "ui.sidebar.register", - "instance.settings.register", - ], - entrypoints: { - worker: "./dist/worker.js", - ui: "./dist/ui", - }, - database: { - namespaceSlug: "bookmarks", - migrationsDir: "migrations", - }, - localFolders: [ - { - folderKey: BOOKMARKS_FOLDER_KEY, - displayName: "Bookmarks root", - description: - "Company-scoped local folder that stores one markdown file per bookmark. Each file uses YAML frontmatter for url, title, tags, and notes.", - access: "readWrite", - requiredDirectories: ["bookmarks"], - }, - ], - apiRoutes: [ - { - routeKey: "list", - method: "GET", - path: "/bookmarks", - auth: "board-or-agent", - capability: "api.routes.register", - companyResolution: { from: "query", key: "companyId" }, - }, - { - routeKey: "create", - method: "POST", - path: "/bookmarks", - auth: "board-or-agent", - capability: "api.routes.register", - companyResolution: { from: "body", key: "companyId" }, - }, - { - routeKey: "delete", - method: "DELETE", - path: "/bookmarks/:slug", - auth: "board-or-agent", - capability: "api.routes.register", - companyResolution: { from: "query", key: "companyId" }, - }, - ], - ui: { - slots: [ - { - type: "page", - id: "bookmarks-page", - displayName: "Bookmarks", - exportName: "BookmarksPage", - routePath: "bookmarks", - }, - { - type: "sidebar", - id: "bookmarks-sidebar-link", - displayName: "Bookmarks", - exportName: "BookmarksSidebarLink", - }, - { - type: "dashboardWidget", - id: "bookmarks-widget", - displayName: "Bookmarks", - exportName: "BookmarksDashboardWidget", - }, - { - type: "settingsPage", - id: "bookmarks-settings", - displayName: "Bookmarks", - exportName: "BookmarksSettingsPage", - }, - ], - }, -}; - -export default manifest; diff --git a/packages/plugins/examples/plugin-bookmarks-example/src/ui/index.tsx b/packages/plugins/examples/plugin-bookmarks-example/src/ui/index.tsx deleted file mode 100644 index b832006df25..00000000000 --- a/packages/plugins/examples/plugin-bookmarks-example/src/ui/index.tsx +++ /dev/null @@ -1,359 +0,0 @@ -import { - usePluginAction, - usePluginData, - type PluginPageProps, - type PluginSettingsPageProps, - type PluginSidebarProps, - type PluginWidgetProps, -} from "@paperclipai/plugin-sdk/ui"; -import { useMemo, useState, type CSSProperties, type FormEvent } from "react"; - -interface BookmarkRecord { - id: string; - companyId: string; - slug: string; - url: string; - title: string; - notes: string; - tags: string[]; - filePath: string; - createdAt: string; - updatedAt: string; -} - -interface BookmarkListResult { - databaseNamespace: string; - bookmarks: BookmarkRecord[]; -} - -const containerStyle: CSSProperties = { - display: "grid", - gap: 16, - font: "13px system-ui, sans-serif", - color: "#111827", - padding: 16, - maxWidth: 760, -}; - -const cardStyle: CSSProperties = { - border: "1px solid #e5e7eb", - borderRadius: 8, - padding: 12, - background: "#fff", -}; - -const inputStyle: CSSProperties = { - border: "1px solid #d1d5db", - borderRadius: 6, - padding: "6px 8px", - font: "inherit", - width: "100%", -}; - -const buttonStyle: CSSProperties = { - border: "1px solid #1f2937", - background: "#111827", - color: "#fff", - borderRadius: 6, - padding: "6px 10px", - font: "inherit", - cursor: "pointer", -}; - -const subtleButtonStyle: CSSProperties = { - border: "1px solid #d1d5db", - background: "#fff", - color: "#111827", - borderRadius: 6, - padding: "4px 8px", - font: "inherit", - cursor: "pointer", -}; - -const tagStyle: CSSProperties = { - display: "inline-block", - border: "1px solid #cbd5f5", - background: "#eef2ff", - color: "#3730a3", - borderRadius: 999, - padding: "1px 8px", - marginRight: 4, - fontSize: 11, -}; - -function parseTagInput(raw: string): string[] { - return raw - .split(/[\s,]+/) - .map((entry) => entry.trim()) - .filter(Boolean); -} - -function formatDate(value: string): string { - const date = new Date(value); - if (Number.isNaN(date.getTime())) return value; - return date.toLocaleString(); -} - -function BookmarksList({ - result, - onDelete, - loading, -}: { - result: BookmarkListResult | null; - onDelete: (slug: string) => Promise; - loading: boolean; -}) { - if (loading) return
Loading bookmarks…
; - if (!result) return null; - if (result.bookmarks.length === 0) { - return
No bookmarks yet.
; - } - return ( -
- {result.bookmarks.map((bookmark) => ( -
-
- - {bookmark.title} - - -
-
{bookmark.url}
- {bookmark.tags.length > 0 ? ( -
- {bookmark.tags.map((tag) => ( - - {tag} - - ))} -
- ) : null} - {bookmark.notes ? ( -
{bookmark.notes}
- ) : null} -
- Added {formatDate(bookmark.createdAt)} · {bookmark.filePath} -
-
- ))} -
- ); -} - -function BookmarksSurface({ companyId }: { companyId: string }) { - const [search, setSearch] = useState(""); - const [tag, setTag] = useState(""); - const [url, setUrl] = useState(""); - const [title, setTitle] = useState(""); - const [tagsRaw, setTagsRaw] = useState(""); - const [notes, setNotes] = useState(""); - const [errorMessage, setErrorMessage] = useState(null); - - const listParams = useMemo( - () => ({ companyId, search: search.trim() || null, tag: tag.trim() || null }), - [companyId, search, tag], - ); - - const { data, loading, error, refresh } = usePluginData("list", listParams); - const create = usePluginAction("create"); - const remove = usePluginAction("delete"); - - async function handleCreate(event: FormEvent) { - event.preventDefault(); - setErrorMessage(null); - try { - await create({ - companyId, - url, - title: title || null, - notes: notes || null, - tags: parseTagInput(tagsRaw), - }); - setUrl(""); - setTitle(""); - setNotes(""); - setTagsRaw(""); - refresh(); - } catch (cause) { - setErrorMessage(cause instanceof Error ? cause.message : "Failed to add bookmark"); - } - } - - async function handleDelete(slug: string) { - setErrorMessage(null); - try { - await remove({ companyId, slug }); - refresh(); - } catch (cause) { - setErrorMessage(cause instanceof Error ? cause.message : "Failed to delete bookmark"); - } - } - - return ( -
-
-

Bookmarks

-
- Company-scoped bookmark library backed by a plugin database namespace and a local markdown - folder. -
-
- -
- Add bookmark - setUrl(event.target.value)} - /> - setTitle(event.target.value)} - /> - setTagsRaw(event.target.value)} - /> -