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. |