From 229f3cd5a8bfb5caacda457cc918bd910cad8da2 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Tue, 8 Sep 2026 08:30:52 +0200 Subject: [PATCH] Accept prerelease Paseo versions in plugin requirements and prepare plugin docs for the 0.8 beta (#4452) Accept plugin requirements against the runtime version or its stable core while preserving the legacy version boundary. Validate all example manifests, correct the ACP example metadata, and prepare plugin author documentation for the beta. --- README.md | 9 ++++ .../protocol/src/plugin-requirements.test.ts | 22 +++++----- packages/protocol/src/plugin-requirements.ts | 10 +++-- .../src/server/plugins/manifest.test.ts | 15 ++++++- .../src/plugin-docs-navigation.test.ts | 7 ++-- plugin-examples/lifecycle-actions/README.md | 4 ++ plugin-examples/linear/README.md | 19 ++++----- .../paseo-plugin.json | 3 -- public-docs/plugins/index.md | 4 +- public-docs/plugins/v0.8/index.md | 6 +-- public-docs/plugins/v0.8/migration.md | 6 +-- public-docs/plugins/v0.8/providers.md | 6 +-- public-docs/plugins/v0.8/reference.md | 42 +++++++++---------- 13 files changed, 88 insertions(+), 65 deletions(-) diff --git a/README.md b/README.md index 3478e15e36..1c834f1291 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,15 @@ Run agents in parallel on your own machines. Ship from your phone or your desk. - **Cross-device:** iOS, Android, desktop, web, and CLI. Start work at your desk, check in from your phone, script it from the terminal. - **Privacy-first:** Paseo doesn't have any telemetry, tracking, or forced log-ins. +## Plugins + +Add themes, workspace panels, commands, settings screens, and coding-agent providers with trusted +TypeScript plugins. Install from a local directory or Git repository with `paseo plugin add `. + +See the [plugin docs](https://paseo.sh/docs/plugins) for your Paseo version, or start with the +[0.8 beta quickstart](https://paseo.sh/docs/plugins/v0.8). Plugins run with access to your daemon +machine and inside connected clients; install only code you trust. + ## Getting Started Paseo runs a local server called the daemon that manages your coding agents. Clients like the desktop app, mobile app, web app, and CLI connect to it. diff --git a/packages/protocol/src/plugin-requirements.test.ts b/packages/protocol/src/plugin-requirements.test.ts index ec00f60694..9d47e68e1f 100644 --- a/packages/protocol/src/plugin-requirements.test.ts +++ b/packages/protocol/src/plugin-requirements.test.ts @@ -1,24 +1,26 @@ import { describe, expect, it } from "vitest"; import { assertPluginCompatibility, validatePluginRequirements } from "./plugin-requirements.js"; -describe("plugin requirements", () => { +describe.each(["daemon", "app"] as const)("plugin requirements on %s", (runtime) => { it("rejects legacy manifests on the first breaking release with migration instructions", () => { - expect(() => - assertPluginCompatibility({ id: "legacy", version: "0.8.0", runtime: "daemon" }), - ).toThrow(/legacy.*<0\.8\.0.*0\.8\.0.*https:\/\/paseo.sh\/docs\/plugins\/v0.8\/migration/); + expect(() => assertPluginCompatibility({ id: "legacy", version: "0.8.0", runtime })).toThrow( + /legacy.*<0\.8\.0.*0\.8\.0.*https:\/\/paseo.sh\/docs\/plugins\/v0.8\/migration/, + ); }); it.each([ [undefined, "0.7.2"], [">=0.8.0", "0.8.0"], + [">=0.8.0", "0.8.0-beta.1"], [">=0.8.0", "1.0.0"], ["^0.8.0", "0.8.4"], [">=0.8.0-beta.1", "0.8.0-beta.2"], + [">=0.8.0-beta.1", "0.8.0-beta.1"], [">=0.8.0-beta.1", "0.8.0"], ["^0.8.0 || ^0.9.0", "0.9.2+build.42"], ])("accepts %s on %s", (paseo, version) => { expect(() => - assertPluginCompatibility({ id: "test", requirements: { paseo }, version, runtime: "app" }), + assertPluginCompatibility({ id: "test", requirements: { paseo }, version, runtime }), ).not.toThrow(); }); @@ -27,11 +29,11 @@ describe("plugin requirements", () => { [undefined, "0.9.0"], [">=0.8.0", "0.7.2"], ["^0.8.0", "0.9.0"], - [">=0.8.0", "0.8.0-beta.1"], + ["<0.8.0", "0.8.0-beta.1"], ])("rejects %s on %s", (paseo, version) => { expect(() => - assertPluginCompatibility({ id: "test", requirements: { paseo }, version, runtime: "app" }), - ).toThrow(`Your app is ${version}`); + assertPluginCompatibility({ id: "test", requirements: { paseo }, version, runtime }), + ).toThrow(`Your ${runtime} is ${version}`); }); it.each(["", " ", "latest", ">=potato", "0.8.0 nonsense"])( @@ -47,8 +49,8 @@ describe("plugin requirements", () => { id: "test", requirements: { paseo: "*" }, version, - runtime: "app", + runtime, }), - ).toThrow("Paseo app version is unknown"); + ).toThrow(`Paseo ${runtime} version is unknown`); }); }); diff --git a/packages/protocol/src/plugin-requirements.ts b/packages/protocol/src/plugin-requirements.ts index 2e0a360099..1659a3fe52 100644 --- a/packages/protocol/src/plugin-requirements.ts +++ b/packages/protocol/src/plugin-requirements.ts @@ -1,5 +1,5 @@ import type { PluginRequirements } from "./messages.js"; -import valid from "semver/functions/valid.js"; +import parse from "semver/functions/parse.js"; import validRange from "semver/ranges/valid.js"; import satisfies from "semver/functions/satisfies.js"; @@ -21,14 +21,16 @@ interface PluginCompatibilityInput { export function assertPluginCompatibility(input: PluginCompatibilityInput): void { validatePluginRequirements(input.requirements); - // COMPAT(plugin-requirements): added in v0.8.0; remove after 2027-03-07 once pre-0.8 plugins and catalogs are unsupported. + // COMPAT(plugin-requirements): added in v0.8.0-beta.1; remove after 2027-03-07 once pre-0.8 plugins and catalogs are unsupported. The legacy range excludes 0.8 prereleases and their stable core. const range = input.requirements?.paseo ?? "<0.8.0"; - if (!input.version || !valid(input.version)) { + const version = input.version ? parse(input.version) : null; + if (!version) { throw new Error( `Cannot check plugin "${input.id}" requirements: Paseo ${input.runtime} version is unknown. Update the ${input.runtime}.`, ); } - if (satisfies(input.version, range)) return; + const stableCore = `${version.major}.${version.minor}.${version.patch}`; + if (satisfies(version, range) || satisfies(stableCore, range)) return; const action = input.requirements?.paseo === undefined ? "This plugin has no requirements.paseo and targets Paseo before 0.8. Ask its author to migrate it: https://paseo.sh/docs/plugins/v0.8/migration" diff --git a/packages/server/src/server/plugins/manifest.test.ts b/packages/server/src/server/plugins/manifest.test.ts index fa73c48b25..12637d8540 100644 --- a/packages/server/src/server/plugins/manifest.test.ts +++ b/packages/server/src/server/plugins/manifest.test.ts @@ -1,16 +1,29 @@ -import { mkdtemp, rm, writeFile } from "node:fs/promises"; +import { mkdtemp, readdir, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; +import { fileURLToPath } from "node:url"; import { afterEach, describe, expect, it } from "vitest"; import { readPluginManifest } from "./manifest.js"; const directories: string[] = []; +const examplesDirectory = fileURLToPath( + new URL("../../../../../plugin-examples/", import.meta.url), +); +const examples = (await readdir(examplesDirectory, { withFileTypes: true })) + .filter((entry) => entry.isDirectory()) + .map((entry) => entry.name); afterEach(async () => { await Promise.all(directories.splice(0).map((directory) => rm(directory, { recursive: true }))); }); describe("plugin manifest", () => { + it.each(examples)("validates the %s example manifest", async (name) => { + await expect(readPluginManifest(path.join(examplesDirectory, name))).resolves.toMatchObject({ + id: expect.any(String), + }); + }); + it("reads and validates requirements before any plugin code runs", async () => { const directory = await mkdtemp(path.join(tmpdir(), "paseo-plugin-manifest-")); directories.push(directory); diff --git a/packages/website/src/plugin-docs-navigation.test.ts b/packages/website/src/plugin-docs-navigation.test.ts index 1d44644b46..cd243f6acd 100644 --- a/packages/website/src/plugin-docs-navigation.test.ts +++ b/packages/website/src/plugin-docs-navigation.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest"; import { buildDocsNavTree, getDocs, getLegacyDocsRedirect } from "./docs"; describe("versioned plugin documentation", () => { - it("keeps current and preview releases together in the Plugins navigation", () => { + it("keeps stable and beta releases together in the Plugins navigation", () => { const plugins = buildDocsNavTree(getDocs()).find( (node) => node.type === "category" && node.label === "Plugins", ); @@ -19,11 +19,12 @@ describe("versioned plugin documentation", () => { }, { type: "group", - label: "Paseo v0.8 — Preview", + label: "Paseo v0.8 — Beta", href: "/docs/plugins/v0.8", children: [ - { type: "page", label: "Reference", href: "/docs/plugins/v0.8/reference" }, + { type: "page", label: "Provider plugins", href: "/docs/plugins/v0.8/providers" }, { type: "page", label: "Migration", href: "/docs/plugins/v0.8/migration" }, + { type: "page", label: "Reference", href: "/docs/plugins/v0.8/reference" }, ], }, ], diff --git a/plugin-examples/lifecycle-actions/README.md b/plugin-examples/lifecycle-actions/README.md index 0ff23d4cc2..71d48f0806 100644 --- a/plugin-examples/lifecycle-actions/README.md +++ b/plugin-examples/lifecycle-actions/README.md @@ -17,6 +17,10 @@ paseo plugin install /absolute/path/to/plugin-examples/lifecycle-logger paseo plugin logs lifecycle-logger ``` +Outside the Paseo repository, `server/inspect.ts` needs `@getpaseo/protocol` installed for its +type imports. Add it to your plugin development dependencies at the same version as +`@getpaseo/plugin` before installing this example. + Copy the callbacks you need into your own plugin. The follow-up example sends a new message; it does not replay attachments or undo previous tool effects. If the provider keeps returning the matching phrase, it keeps sending follow-ups. Add limits or delay in your plugin when your workflow needs them. diff --git a/plugin-examples/linear/README.md b/plugin-examples/linear/README.md index 2a227bf71c..c21c52f1d0 100644 --- a/plugin-examples/linear/README.md +++ b/plugin-examples/linear/README.md @@ -9,21 +9,16 @@ Set a personal API key in the daemon environment: export LINEAR_API_KEY="lin_api_..." ``` -Register the extension in `$PASEO_HOME/config.json`: +Start your daemon with that environment variable set. Then turn on **Enable plugins** in +**Settings → Plugins** and install the example: -```json -{ - "pluginsEnabled": true, - "plugins": { - "linear": { - "source": "directory", - "path": "/absolute/path/to/paseo/plugin-examples/linear" - } - } -} +```bash +paseo plugin add /absolute/path/to/paseo/plugin-examples/linear +paseo plugin ls linear ``` -Restart the development daemon after changing its environment or plugin configuration. +Use `paseo plugin reload linear` after source edits. Changing the API key requires restarting the +daemon with the new environment; installing or reloading plugin source does not. The split entry point demonstrates the complete attachment-source pattern: diff --git a/plugin-examples/provider-acp-transformer/paseo-plugin.json b/plugin-examples/provider-acp-transformer/paseo-plugin.json index 9c3f23a174..9aee2c4cc4 100644 --- a/plugin-examples/provider-acp-transformer/paseo-plugin.json +++ b/plugin-examples/provider-acp-transformer/paseo-plugin.json @@ -1,8 +1,5 @@ { "id": "provider-acp-transformer-example", - "name": "ACP provider transformer example", - "version": "1.0.0", - "description": "Wraps an ACP command and normalizes one vendor file-edit shape.", "requirements": { "paseo": ">=0.8.0" } diff --git a/public-docs/plugins/index.md b/public-docs/plugins/index.md index 552a0476e0..16fe2a8bbc 100644 --- a/public-docs/plugins/index.md +++ b/public-docs/plugins/index.md @@ -15,7 +15,7 @@ change between releases. Build and maintain plugins for the latest stable Paseo release. -## [Paseo v0.8 — Preview](/docs/plugins/v0.8) +## [Paseo v0.8 — Beta](/docs/plugins/v0.8) -Prepare plugins for the upcoming client and server entry layout. Existing plugin authors should +Build plugins for Paseo 0.8 beta, with separate client and server entries. Existing plugin authors should follow the [v0.8 migration guide](/docs/plugins/v0.8/migration). diff --git a/public-docs/plugins/v0.8/index.md b/public-docs/plugins/v0.8/index.md index 0329b31b2b..96e6fbb35a 100644 --- a/public-docs/plugins/v0.8/index.md +++ b/public-docs/plugins/v0.8/index.md @@ -1,15 +1,15 @@ --- title: Plugin quickstart description: Build, install, share, and update a trusted Paseo plugin. -nav: Paseo v0.8 — Preview +nav: Paseo v0.8 — Beta order: 46 category: Plugins --- # Plugin quickstart -> **For the upcoming Paseo v0.8 release.** Use the [current v0.7 docs](/docs/plugins/v0.7) -> unless you are preparing a plugin for v0.8. +> **For Paseo v0.8 beta.** Use the [v0.7 docs](/docs/plugins/v0.7) +> if you run the stable release. > **Experimental:** The plugin API is still evolving, so expect breaking changes and updates to > your plugins as Paseo evolves. See the [plugin roadmap](https://github.com/getpaseo/paseo/labels/plugins) diff --git a/public-docs/plugins/v0.8/migration.md b/public-docs/plugins/v0.8/migration.md index ce5c94d075..e77a830c8f 100644 --- a/public-docs/plugins/v0.8/migration.md +++ b/public-docs/plugins/v0.8/migration.md @@ -8,7 +8,7 @@ category: Plugins # Migrate a plugin to runtime entries -> **For the upcoming Paseo v0.8 release.** This migration is not required for Paseo v0.7. +> **For Paseo v0.8 beta.** This migration is not required for Paseo v0.7. Give this page to a coding agent with the plugin directory as its working directory. Execute the steps in order. Do not keep a compatibility entry. @@ -255,8 +255,8 @@ rejects the plugin even if its files have been moved. Adding the field alone doe code. Update the local `@getpaseo/plugin` development dependency to the version you target and reinstall dependencies before typechecking. -For a 0.8 beta, use its explicit version in both the SDK dependency and range, for example -`>=0.8.0-beta.1`. See [requirements](reference#requirements) for range and prerelease semantics. +For a 0.8 beta, use its explicit version in the SDK dependency and `>=0.8.0` in the manifest. +See [requirements](reference#requirements) for range and prerelease semantics. ## 8. Verify the migration diff --git a/public-docs/plugins/v0.8/providers.md b/public-docs/plugins/v0.8/providers.md index f9af6596e9..71eaa08dce 100644 --- a/public-docs/plugins/v0.8/providers.md +++ b/public-docs/plugins/v0.8/providers.md @@ -8,7 +8,7 @@ category: Plugins # Build a provider plugin -> **For the upcoming Paseo v0.8 release.** Start with the +> **For Paseo v0.8 beta.** Start with the > [plugin quickstart](/docs/plugins/v0.8) if you have not built a Paseo plugin before. A provider plugin connects a coding agent to Paseo without adding it to Paseo core. Publish the @@ -22,10 +22,10 @@ Choose one implementation path: | It already implements ACP | Register it with `runAcpProvider()` and add only narrow vendor transformers. | | It has a TypeScript SDK, JSON-RPC API, or custom process protocol | Implement `ProviderRegistration` directly. | -The complete examples are: +The examples are: - [`provider-direct`](https://github.com/getpaseo/paseo/tree/main/plugin-examples/provider-direct): sessions, settings, prompts, persistence, child sessions, and a provider-owned timeline renderer; -- [`provider-acp-transformer`](https://github.com/getpaseo/paseo/tree/main/plugin-examples/provider-acp-transformer): an ACP command with a Zod-validated vendor edit transformer; +- [`provider-acp-transformer`](https://github.com/getpaseo/paseo/tree/main/plugin-examples/provider-acp-transformer): an ACP command template with a Zod-validated vendor edit transformer. Replace `example-acp --stdio` with an installed ACP agent before loading it; - [`inline-thinking`](https://github.com/getpaseo/paseo/tree/main/plugin-examples/inline-thinking): a renderer-only plugin that does not implement a provider. ## Register a direct provider diff --git a/public-docs/plugins/v0.8/reference.md b/public-docs/plugins/v0.8/reference.md index 6147693d1f..2cad2a163a 100644 --- a/public-docs/plugins/v0.8/reference.md +++ b/public-docs/plugins/v0.8/reference.md @@ -8,7 +8,7 @@ category: Plugins # Plugin reference -> **For the upcoming Paseo v0.8 release.** Return to the [v0.8 quickstart](/docs/plugins/v0.8). +> **For Paseo v0.8 beta.** Return to the [v0.8 quickstart](/docs/plugins/v0.8). Migrating an existing plugin? Follow the standalone [runtime-entry migration guide](/docs/plugins/v0.8/migration). @@ -55,15 +55,13 @@ range. An omitted `requirements.paseo` means `<0.8.0`: the plugin predates the f plugin release. Paseo 0.8 and later reject it with a link to the [migration guide](migration). Empty strings, invalid ranges, and unknown manifest requirement keys are rejected. -| Range | Compatible releases | -| ---------------- | ------------------------------------------------------------------- | -| `>=0.8.0` | 0.8.0 and later stable releases, including future breaking releases | -| `^0.8.0` | 0.8.x stable releases only | -| `>=0.8.3 <0.9.0` | 0.8.3 through the last 0.8 patch | -| `>=0.8.0-beta.1` | Betas from 0.8.0-beta.1, then 0.8.0 and later stable releases | +| Range | Compatible releases | +| ---------------- | ---------------------------------------------------------------------------- | +| `>=0.8.0` | 0.8.0 and later releases, including prereleases and future breaking releases | +| `^0.8.0` | 0.8.x releases, including prereleases | +| `>=0.8.3 <0.9.0` | 0.8.3 through the last 0.8 patch, including prereleases | -Standard npm prerelease rules apply: `>=0.8.0` excludes `0.8.0-beta.1`. A prerelease must be -explicitly included for its major/minor/patch tuple; no version tags are stripped. +Prerelease Paseo versions also satisfy a range their stable core (`major.minor.patch`) satisfies, so `0.8.0-beta.1` satisfies `>=0.8.0` but not `<0.8.0`. `paseo plugin init` writes `>=` followed by the current CLI version and pins the matching SDK for typechecking. Raise the minimum when adopting a newer API. Add an upper bound when a later @@ -81,10 +79,10 @@ and cannot show this new diagnostic. ### Runtime entries -| Entry | Runtime | Receives | Required | -| ------------------ | --------------------- | --------------------- | ----------------------------------------------------------------- | -| `index.client.tsx` | Paseo app, per client | `PluginClientContext` | When the plugin has any UI, callback, theme, or attachment source | -| `index.server.ts` | Daemon subprocess | `PluginServerContext` | When the plugin handles RPCs | +| Entry | Runtime | Receives | Required | +| ------------------ | --------------------- | --------------------- | ------------------------------------------------------------------------------- | +| `index.client.tsx` | Paseo app, per client | `PluginClientContext` | When the plugin has any UI, callback, theme, or attachment source | +| `index.server.ts` | Daemon subprocess | `PluginServerContext` | When the plugin contributes handlers, hooks, settings persistence, or providers | At least one entry is required; both accept `.ts` or `.tsx`. A directory that still has only the old `index.ts` fails to load and points at the [migration guide](/docs/plugins/v0.8/migration). @@ -346,7 +344,9 @@ and directory lookup/import operations are unaffected. ### Send a follow-up when a turn ends Copy [server/inspect.ts](https://github.com/getpaseo/paseo/blob/main/plugin-examples/lifecycle-actions/server/inspect.ts) -into your plugin. `latestOutputText` joins text chunks after the latest user message. +into your plugin. The helper imports types from `@getpaseo/protocol/agent-types`; add +`@getpaseo/protocol` at the same version as your plugin SDK to your development dependencies +and install them before loading the plugin. `latestOutputText` joins text chunks after the latest user message. ```ts import type { PluginServerContext } from "@getpaseo/plugin/server"; @@ -1215,7 +1215,7 @@ import { z } from "zod"; const refreshReview = defineRpc({ name: "review.refresh", - input: z.object({ agentId: z.string() }), + input: z.object({ agentId: z.string(), scope: z.string().optional() }), output: z.object({ refreshed: z.boolean() }), }); @@ -1376,10 +1376,10 @@ registered by the same plugin. Use `usePaseo()` for ordinary Paseo operations from a surface. It borrows the selected host's existing connection; do not create another client. ```tsx -import { usePaseo } from "@getpaseo/plugin/client"; +import { type PluginSurfaceProps, usePaseo } from "@getpaseo/plugin/client"; import { Pressable, Text } from "react-native"; -function PullRequestAction() { +function PullRequestAction({ theme }: PluginSurfaceProps) { const paseo = usePaseo(); async function createReviewWorkspace() { @@ -1400,7 +1400,7 @@ function PullRequestAction() { return ( void createReviewWorkspace()}> - Create review workspace + Create review workspace ); } @@ -1659,7 +1659,7 @@ compilation, activation, or replacement. A failing command reports its output, d candidate, and leaves the installed/running version intact. The daemon log records each command and output; with the global `--host` option, execution is on that daemon host. -Run `npm run typecheck` before install or reload. Never edit the daemon config directly. +Run `npm run typecheck` before install or reload. Manage plugin source entries with the CLI or Settings. The daemon-wide **Enable plugins** switch lives under **Settings → Plugins**. A configured plugin remains `disabled` until that switch and the plugin's own enabled state are both on. @@ -1673,8 +1673,8 @@ Use `paseo plugin ls` to read the current status and error. | --------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | | `This plugin was made for an older version of Paseo` | The directory has only an `index.ts` entry. Follow the [migration guide](/docs/plugins/v0.8/migration). | | `Plugin entry points are missing` | Neither `index.client.tsx` nor `index.server.ts` exists with that exact name. | -| `server-only module cannot be imported into the plugin client bundle` | Client code imports `server/` or a `*.server.*` file. Move the work behind an RPC and import its contract from `shared/`. | -| `client-only module cannot be imported into the plugin server bundle` | Server code imports `client/` or a `*.client.*` file. Register that contribution from `index.client.tsx` instead. | +| `server-only module cannot be imported into the plugin client bundle` | Client code imports `server/`. Move the work behind an RPC and import its contract from `shared/`. | +| `client-only module cannot be imported into the plugin server bundle` | Server code imports `client/`. Register that contribution from `index.client.tsx` instead. | | `Node module cannot be imported into the plugin client bundle` | Client code imports `node:*`. Move the operation to `server/` and call it through an RPC. | | Sidebar item is missing | The plugin is `running`, the item references an existing surface, the icon name is valid, and the client is on the installation's host. | | Client module is unavailable | Import only the host-provided client modules listed above. |