mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-08 09:24:41 +00:00
Merge main into fix/usage-screen-provider-logins
This commit is contained in:
@@ -0,0 +1,659 @@
|
||||
---
|
||||
title: Plugin hooks
|
||||
description: Register hooks to change requests or observe agent and workspace operations.
|
||||
nav: Hooks
|
||||
order: 48
|
||||
category: Plugins
|
||||
---
|
||||
|
||||
# Plugin hooks
|
||||
|
||||
Use `server.addHook(name, handler)` in `index.server.ts`. Hooks run while the plugin is enabled, even with no app connected. `addHook` is available from Paseo 0.14; set `requirements.paseo` to the release you build against.
|
||||
|
||||
```ts
|
||||
import type { PluginServerContext } from "@getpaseo/plugin/server";
|
||||
|
||||
export default function contribute(server: PluginServerContext) {
|
||||
const remove = server.addHook("agent.open.before", async ({ request }) => ({
|
||||
...request,
|
||||
env: { ...request.env, REGION: "eu" },
|
||||
}));
|
||||
return remove;
|
||||
}
|
||||
```
|
||||
|
||||
## Naming and timing
|
||||
|
||||
Names follow `subject.operation.position`: the subject, the operation, then `before` or `after`. The name identifies a lifecycle position. Whether Paseo waits is a per-hook property shown in the table below. `workspace` covers every workspace; `worktree` covers only git worktrees.
|
||||
|
||||
- Waiting hooks finish before Paseo continues. Throwing, returning invalid data, or exceeding 30 seconds fails the operation with `Plugin <id> <hook> failed: …`. Request-changing hooks receive `{ request }` and may return a complete replacement request or `undefined`.
|
||||
- Notifications receive the event object and return `Promise<void>`. Paseo does not wait; errors are logged against the plugin and the operation continues. `agent.turn.before` and `agent.permission.before` are notifications fired when a turn starts or a permission becomes pending.
|
||||
|
||||
All handlers must be async. Turn completion includes success, failure, and cancellation; inspect `outcome`. Permission completion includes every resolution; inspect `resolution`.
|
||||
|
||||
Creating an agent happens once, and `agent.create.before` changes its saved configuration. Opening starts the provider process: the first run, resume after a daemon restart, reading history, importing an outside session, or reload. `agent.open.before` contributes launch-only environment, tools, and MCP servers each time. Creating opens the agent; opening an existing agent does not create it; closing ends the running instance. Opening a tab in the app is a separate action.
|
||||
|
||||
`worktree.create.after` runs before the workspace is registered. It precedes `workspace.create.after`, and both precede the first agent's `agent.create.before` / `agent.open.before`.
|
||||
|
||||
### Return early from a waiting hook
|
||||
|
||||
Start detached work and return when the operation should continue immediately:
|
||||
|
||||
```ts
|
||||
server.addHook("worktree.create.after", async ({ worktree }) => {
|
||||
void warmCaches(worktree.path);
|
||||
});
|
||||
```
|
||||
|
||||
Paseo only sees the returned promise, so detached work gets no error reporting and no cancellation `signal`.
|
||||
|
||||
## Hooks
|
||||
|
||||
| Hook | Waits | Payload | Handler may change |
|
||||
| ------------------------- | ----- | -------------------------------------- | ---------------------------------------------- |
|
||||
| `workspace.create.before` | yes | `{ request }` | Entire create request |
|
||||
| `workspace.create.after` | no | `{ workspace }` | |
|
||||
| `workspace.archive.after` | no | `{ workspace }` | |
|
||||
| `worktree.create.before` | yes | `{ request }` | Creation options and destination |
|
||||
| `worktree.create.after` | yes | `{ worktree }` | Files in the worktree; throw to abort creation |
|
||||
| `agent.create.before` | yes | `{ request: { config, env? } }` | Saved config except `cwd`; `env` |
|
||||
| `agent.create.after` | no | `{ agent }` | |
|
||||
| `agent.open.before` | yes | `{ request }` | Launch-only `env`, `tools`, `mcpServers` |
|
||||
| `agent.close.after` | no | `{ agent }` | |
|
||||
| `agent.archive.after` | no | `{ agent, archivedAt }` | |
|
||||
| `agent.turn.before` | no | `{ agent, turnId }` | |
|
||||
| `agent.turn.after` | no | `{ agent, turnId, outcome, timeline }` | |
|
||||
| `agent.permission.before` | no | `{ agent, request }` | |
|
||||
| `agent.permission.after` | no | `{ agent, requestId, resolution }` | |
|
||||
|
||||
## Shared payloads
|
||||
|
||||
```ts
|
||||
interface PluginHookAgent {
|
||||
id: string;
|
||||
workspaceId: string | null;
|
||||
parentAgentId: string | null;
|
||||
provider: string;
|
||||
cwd: string;
|
||||
title: string | null;
|
||||
}
|
||||
|
||||
interface PluginHookWorkspace {
|
||||
id: string;
|
||||
projectId: string;
|
||||
cwd: string;
|
||||
name: string | null;
|
||||
archivedAt: string | null;
|
||||
}
|
||||
|
||||
type PluginTurnOutcome =
|
||||
| { kind: "completed" }
|
||||
| { kind: "failed"; error: { message: string; code?: string } }
|
||||
| { kind: "canceled"; reason: string };
|
||||
```
|
||||
|
||||
| Field | Shape / meaning |
|
||||
| ------------ | -------------------------------------------------------------------------------------- |
|
||||
| `turnId` | Provider-reported `string` or `null`; can repeat after a session reopens |
|
||||
| `timeline` | Deprecated, always `[]`; fetch history with the SDK. |
|
||||
| `request` | SDK `AgentPermissionRequest`; `kind` is `tool`, `plan`, `question`, `mode`, or `other` |
|
||||
| `resolution` | SDK `AgentPermissionResponse` |
|
||||
| `archivedAt` | Timestamp string |
|
||||
|
||||
## workspace.create.before
|
||||
|
||||
Receives `{ request }` with `source`, optional `title`, and `firstAgentContext`. You may replace the entire explicit create request; a throw, invalid result, or timeout fails creation. Existing workspaces and directory lookup/import are unaffected.
|
||||
|
||||
### Choose workspace isolation
|
||||
|
||||
```ts
|
||||
server.addHook("workspace.create.before", async ({ request }) => {
|
||||
if (request.source.kind !== "directory") {
|
||||
return request;
|
||||
}
|
||||
|
||||
return {
|
||||
...request,
|
||||
source: {
|
||||
kind: "worktree",
|
||||
cwd: request.source.path,
|
||||
action: "branch-off",
|
||||
},
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
**Result:** an explicit directory creation request becomes a worktree request. Existing workspaces
|
||||
and directory lookup/import operations are unaffected.
|
||||
|
||||
## workspace.create.after
|
||||
|
||||
Fires after the workspace record is created and its directory is available. Receives `{ workspace }`.
|
||||
|
||||
```ts
|
||||
server.addHook("workspace.create.after", async ({ workspace }) => {
|
||||
console.log(workspace);
|
||||
});
|
||||
```
|
||||
|
||||
## workspace.archive.after
|
||||
|
||||
Fires after archive state is saved. Receives `{ workspace }`; worktree cleanup can still be pending.
|
||||
|
||||
```ts
|
||||
server.addHook("workspace.archive.after", async ({ workspace }) => {
|
||||
console.log(workspace);
|
||||
});
|
||||
```
|
||||
|
||||
## worktree.create.before {#worktreecreatebefore}
|
||||
|
||||
Choose the location and creation options for a new worktree. Paseo awaits this hook before `git worktree add`, on creation from the app, CLI, MCP, and agent creation. Receives `{ request }`; return a complete replacement request or `undefined` to keep it. Throwing, invalid returned data, or a 30-second timeout aborts creation with the plugin and hook named in the error.
|
||||
|
||||
```ts
|
||||
import { basename, dirname, join } from "node:path";
|
||||
|
||||
server.addHook("worktree.create.before", async ({ request }) => ({
|
||||
...request,
|
||||
path: join(
|
||||
dirname(request.sourceCheckout),
|
||||
`${basename(request.sourceCheckout)}.${request.branch.replaceAll("/", "-")}`,
|
||||
),
|
||||
}));
|
||||
```
|
||||
|
||||
For source `/home/me/src/paseo` and branch `feature/x`, this creates `/home/me/src/paseo.feature-x`. See the complete [worktree-placement example](https://github.com/getpaseo/paseo/tree/main/plugin-examples/worktree-placement).
|
||||
|
||||
| Request field | Meaning |
|
||||
| ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `kind` | `branch-off`, `checkout-branch`, `checkout-change-request`, or `checkout-github-pr` |
|
||||
| `branch` | Requested local branch name |
|
||||
| `baseRef` | Starting ref for branch-off; comparison base for change requests; absent for `checkout-branch`. To branch off a ref, return `kind: "branch-off"`. |
|
||||
| `slug` | Normalized worktree name |
|
||||
| `path` | Absolute destination; initially the default location with any existing-path suffix resolved |
|
||||
| `repoRoot` | Read-only main repository root |
|
||||
| `sourceCheckout` | Read-only source checkout root, including when creation starts from another worktree |
|
||||
| `forge`, `changeRequestNumber`, `headRef` | Change-request checkout identity; GitHub legacy checkouts use `githubPrNumber` instead of `forge` and `changeRequestNumber` |
|
||||
| `headRepositoryOwner`, `headRepository` | Optional source repository facts; `headRepository` is present only on change-request checkouts |
|
||||
| `checkoutRefs` | Optional fetch sources: `{ remoteName?: string, remoteRef: string }[]` |
|
||||
| `pushRemoteUrl`, `trackOriginHead` | Optional push destination and tracking policy |
|
||||
|
||||
Once `path` is returned, `slug` only feeds branch-off naming; the CLI looks up worktrees by the basename of their path.
|
||||
|
||||
Every field except `repoRoot` and `sourceCheckout` is transformable. Return all required facts for the chosen `kind`. Paseo resolves the Git source plan after this hook, so handlers can change the branch and base before fetching refs, while Git can still choose a unique final branch name afterward.
|
||||
|
||||
Paseo resolves symlinked parents and validates the returned path. It must be absolute, must not exist, and must not be the filesystem root, your home directory, the repository root, inside a `.git` directory, or an ancestor of the repository. An existing destination fails creation; Paseo never adds a suffix to a plugin-chosen path. The plugin must leave directory creation to Paseo.
|
||||
|
||||
Ordering: `worktree.create.before` → `git worktree add` → `worktree.create.after` → workspace registration. Restoring a missing worktree recreates it at its recorded path without running either worktree creation hook.
|
||||
|
||||
## worktree.create.after
|
||||
|
||||
Paseo waits for this hook after a new git worktree exists and before workspace registration, setup, terminals, or agents. Receives `{ worktree: { path, branch, sourceCheckout } }`: the absolute worktree path, branch name, and source checkout directory. There is no workspace id yet. Return nothing; write files directly into the worktree. Throwing or exceeding 30 seconds aborts creation and removes the new worktree.
|
||||
|
||||
This covers branch-off, branch checkout, and PR checkout creation. Restoring a missing archived worktree uses a separate recovery path and does not fire this hook.
|
||||
|
||||
```ts
|
||||
import { copyFile } from "node:fs/promises";
|
||||
|
||||
server.addHook("worktree.create.after", async ({ worktree }) => {
|
||||
await copyFile(`${worktree.sourceCheckout}/.env`, `${worktree.path}/.env`);
|
||||
});
|
||||
```
|
||||
|
||||
## agent.create.before
|
||||
|
||||
Receives `{ request: { config, env? } }`. Config changes are saved and remain after the plugin is disabled or removed; `env` applies to the initial launch and is not saved. You cannot change `config.cwd`. A throw, invalid result, or timeout fails creation.
|
||||
|
||||
**`agent.create.before.config`** uses `AgentSessionConfig`:
|
||||
|
||||
| Fields | Constraint |
|
||||
| --------------------------------------------- | -------------------------------------------------------------------------- |
|
||||
| `provider`, `model` | Separate fields; changing provider may require changing model/mode/options |
|
||||
| `modeId`, `thinkingOptionId`, `featureValues` | Provider-specific selections |
|
||||
| `title`, `systemPrompt` | Agent configuration |
|
||||
| `providerOptions` | Opaque provider-specific options |
|
||||
| `mcpServers`, `toolPolicy` | MCP configuration and exact-tool preapprovals |
|
||||
| `cwd` | Cannot change |
|
||||
|
||||
### Change saved configuration
|
||||
|
||||
Add this callback inside `contribute(server)` to set the saved options for new Codex agents:
|
||||
|
||||
```ts
|
||||
server.addHook("agent.create.before", async ({ request }) => {
|
||||
if (request.config.provider !== "codex") {
|
||||
return request;
|
||||
}
|
||||
|
||||
return {
|
||||
...request,
|
||||
config: {
|
||||
...request.config,
|
||||
providerOptions: {
|
||||
...request.config.providerOptions,
|
||||
sandbox_mode: "workspace-write",
|
||||
approval_policy: "on-request",
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
| Input | Result |
|
||||
| ------------------------------------------- | ----------------------- |
|
||||
| `providerOptions.sandbox_mode: "read-only"` | `"workspace-write"` |
|
||||
| `providerOptions.web_search: "disabled"` | Preserved by the spread |
|
||||
|
||||
The selected provider validates `providerOptions`. Explicit Codex sandbox and approval options
|
||||
override its mode presets.
|
||||
|
||||
## agent.create.after
|
||||
|
||||
Ordinary creation finishes. Receives `{ agent }`; import and resume do not fire it.
|
||||
|
||||
```ts
|
||||
server.addHook("agent.create.after", async ({ agent }) => {
|
||||
console.log(agent);
|
||||
});
|
||||
```
|
||||
|
||||
## agent.open.before
|
||||
|
||||
Receives `{ request }`; only `env`, `tools`, and `mcpServers` may change, for this launch. All other fields are read-only. A throw, invalid result, or timeout fails the open.
|
||||
|
||||
**`agent.open.before` request example:**
|
||||
|
||||
```json
|
||||
{
|
||||
"agentId": "agent-123",
|
||||
"workspaceId": "workspace-456",
|
||||
"provider": "codex",
|
||||
"cwd": "/projects/shop",
|
||||
"title": "Workspace notes",
|
||||
"labels": { "notes/enabled": "true" },
|
||||
"tools": [],
|
||||
"mcpServers": {},
|
||||
"reason": "resume",
|
||||
"purpose": "interactive",
|
||||
"env": { "COMPANY_ENV": "development" }
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Values |
|
||||
| ------------- | ----------------------------------------------------------------------------------------------------------------------- |
|
||||
| `workspaceId` | String or `null` |
|
||||
| `title` | Read-only string or `null`; creation title on create, stored title on resume. |
|
||||
| `labels` | Read-only string map; creation labels on create, stored labels on resume. |
|
||||
| `tools` | Editable `AgentToolDefinition[]`, local to this plugin. See [Attach tools to agents](#attach-tools-to-agents). |
|
||||
| `mcpServers` | Editable launch-only server map, local to this plugin. See [Inject MCP servers](#inject-mcp-servers). |
|
||||
| `reason` | `create`, `resume`, `refresh`, `import` |
|
||||
| `purpose` | `interactive`, `history` |
|
||||
| `env` | Launch override map; excludes the daemon's inherited environment. Replace the map to add, replace, or remove overrides. |
|
||||
|
||||
### Inject environment variables on every session opening
|
||||
|
||||
```ts
|
||||
server.addHook("agent.open.before", async ({ request }) => {
|
||||
return {
|
||||
...request,
|
||||
env: {
|
||||
...request.env,
|
||||
COMPANY_ENV: "development",
|
||||
},
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
Session-open overrides are re-derived on every open. `agent.create.before.env` applies only to the
|
||||
initial launch and is not saved in the agent's configuration.
|
||||
|
||||
### Inject MCP servers
|
||||
|
||||
Use `agent.create.before.config.mcpServers` when the server should become part of the agent's saved
|
||||
configuration. For a plugin-owned server, add this callback inside `contribute(server)`, replacing
|
||||
the placeholder URL with your MCP endpoint:
|
||||
|
||||
```ts
|
||||
server.addHook("agent.open.before", async ({ request }) => {
|
||||
if (request.provider !== "codex") return;
|
||||
|
||||
return {
|
||||
...request,
|
||||
mcpServers: {
|
||||
...request.mcpServers,
|
||||
company: { type: "http", url: "https://tools.example.com/mcp" },
|
||||
},
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
`request.mcpServers` uses the same server configurations as `AgentSessionConfig.mcpServers`.
|
||||
The map starts empty for each plugin and accumulates across that plugin's own callbacks. It never
|
||||
contains the agent's saved servers or another plugin's servers. Replace the map to add, replace,
|
||||
or remove this plugin's entries; returning `undefined` preserves it. Paseo merges the plugins'
|
||||
final maps in plugin-ID order.
|
||||
|
||||
A name colliding with a saved server, Paseo's internal `paseo` server, or another plugin's server
|
||||
fails the session open and names the server and both sources. A provider without MCP support fails
|
||||
with `does not support MCP servers`, naming the plugin. Opens with `purpose: "history"` ignore
|
||||
these servers. Install, disable, and reload changes reach existing agents through the
|
||||
[session reopen behavior](#attach-tools-to-agents).
|
||||
|
||||
### Attach tools to agents
|
||||
|
||||
Define a tool in `index.server.ts` and attach it in `server.addHook("agent.open.before", async handler)`.
|
||||
This example gives `append_workspace_note` only to agents with the label `notes/enabled: "true"`:
|
||||
|
||||
```ts
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { z } from "zod";
|
||||
import { defineAgentTool, type PluginServerContext } from "@getpaseo/plugin/server";
|
||||
|
||||
const appendWorkspaceNote = defineAgentTool({
|
||||
name: "append_workspace_note",
|
||||
description: "Append a note to AGENT_NOTES.md in this agent's working directory.",
|
||||
input: z.object({ note: z.string().min(1).describe("Note to append") }),
|
||||
async execute({ note }, { agent, signal }) {
|
||||
const notePath = path.join(agent.cwd, "AGENT_NOTES.md");
|
||||
await writeFile(notePath, `${note}\n`, { encoding: "utf8", flag: "a", signal });
|
||||
return {
|
||||
text: `Saved note to ${notePath}`,
|
||||
structuredContent: { path: notePath },
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default function contribute(server: PluginServerContext) {
|
||||
server.addHook("agent.open.before", async ({ request }) => {
|
||||
if (request.labels["notes/enabled"] !== "true") return;
|
||||
return { ...request, tools: [...request.tools, appendWorkspaceNote] };
|
||||
});
|
||||
return () => {};
|
||||
}
|
||||
```
|
||||
|
||||
Set the label when creating an agent with the SDK:
|
||||
|
||||
```ts
|
||||
const agent = await paseo.agents.create({
|
||||
config: { provider: "codex/gpt-5.4" },
|
||||
cwd: "/projects/shop",
|
||||
labels: { "notes/enabled": "true" },
|
||||
});
|
||||
```
|
||||
|
||||
Select agents using any of the session-open request fields:
|
||||
|
||||
| Select by | Condition in the callback |
|
||||
| ---------------- | -------------------------------------------- |
|
||||
| Label | `request.labels["notes/enabled"] === "true"` |
|
||||
| Provider | `request.provider === "codex"` |
|
||||
| Workspace | `request.workspaceId === "workspace-456"` |
|
||||
| Individual agent | `request.agentId === chosenAgentId` |
|
||||
|
||||
`request.tools` starts empty for each plugin. Each callback sees the tools returned by earlier
|
||||
callbacks in that plugin; replace the array to add or remove tools. Returning `undefined` preserves
|
||||
the request. Tools are ignored for `purpose: "history"`.
|
||||
|
||||
Import `defineAgentTool`, `AgentToolDefinition`, `AgentToolContext`, and `AgentToolResult` from
|
||||
`@getpaseo/plugin/server`. The helper infers the validated input from your Zod schema:
|
||||
|
||||
```ts
|
||||
declare function defineAgentTool<Input extends z.ZodType>(tool: {
|
||||
name: string;
|
||||
title?: string;
|
||||
description: string;
|
||||
input: Input;
|
||||
execute(
|
||||
input: z.output<Input>,
|
||||
context: AgentToolContext,
|
||||
): AgentToolResult | Promise<AgentToolResult>;
|
||||
}): AgentToolDefinition;
|
||||
|
||||
interface AgentToolResult {
|
||||
text?: string;
|
||||
structuredContent?: unknown;
|
||||
isError?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
| Execution context | Value |
|
||||
| ----------------- | --------------------------------------------------------------------------------------------------- |
|
||||
| `agent` | Calling agent as a `PluginHookAgent`, including `id`, `workspaceId`, `provider`, `cwd`, and `title` |
|
||||
| `paseo` | SDK connected to this daemon |
|
||||
| `signal` | Abort signal for this tool call; pass it to file operations or external requests |
|
||||
|
||||
`execute` runs in the plugin process. Closures and the original Zod schema stay there; only tool
|
||||
metadata and call inputs/results cross the process boundary. Paseo validates input before calling
|
||||
`execute`. Invalid input returns schema issues to the agent. A thrown error becomes
|
||||
`{ isError: true, text: message }` and the plugin keeps running. Canceling the call or closing its
|
||||
transport aborts `signal` and discards the result. Tool calls have a 10 minute ceiling, separate
|
||||
from the 30 second hook timeout. Closing or reopening an agent session drops its old definitions.
|
||||
|
||||
Return model-visible text in `text`. `structuredContent` must be JSON-serializable; when `text` is
|
||||
omitted, Paseo formats that value as text for the model. Set `isError: true` to report a tool error.
|
||||
|
||||
Paseo delivers attached tools through its agent tool transport, using MCP or native registration
|
||||
as the provider supports. The same definition and callback work with either. Attached tools obey
|
||||
the [same gates as Paseo tools](/docs/mcp#limit-paseo-tools-by-provider). Only that agent sees them.
|
||||
|
||||
Names must match `/^[a-z][a-z0-9_]*$/` and be at most 64 characters. A name colliding with a Paseo
|
||||
tool or another plugin's tool on the same agent fails the session open and names both owners.
|
||||
A duplicate name within one plugin also fails the session open. Agents the callback does not
|
||||
target are unaffected.
|
||||
|
||||
| Change | When the agent receives it |
|
||||
| ------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Start, stop, or reload a plugin declaring `agent.open.before` | Open sessions become stale. Each reopens before its next idle prompt; a running turn continues without interruption. The next prompt after that reopen reuses the session. |
|
||||
| Change which agents the callback targets | Run `paseo plugin reload agent-tools` after editing this example, then send the agent a prompt when it is idle. |
|
||||
| Apply immediately | Call `await paseo.agents.ref(id).reload({ requestId: "reload-notes" })`. `requestId` is optional. This reopens the provider session, reruns hooks with `reason: "refresh"`, and interrupts a running turn. |
|
||||
| Read the current snapshot | Call `await paseo.agents.ref(id).refresh()`. This reads state without reopening the session. |
|
||||
|
||||
If an automatic reopen fails, the triggering prompt fails with that error. See the
|
||||
[complete agent-tools example](https://github.com/getpaseo/paseo/tree/main/plugin-examples/agent-tools).
|
||||
|
||||
## agent.close.after
|
||||
|
||||
A live agent runtime closes. Receives `{ agent }`.
|
||||
|
||||
```ts
|
||||
server.addHook("agent.close.after", async ({ agent }) => {
|
||||
console.log(agent);
|
||||
});
|
||||
```
|
||||
|
||||
## agent.archive.after
|
||||
|
||||
Archive state is saved. Receives `{ agent, archivedAt }`; runtime cleanup can still be pending.
|
||||
|
||||
```ts
|
||||
server.addHook("agent.archive.after", async ({ agent, archivedAt }) => {
|
||||
console.log(agent, archivedAt);
|
||||
});
|
||||
```
|
||||
|
||||
## agent.turn.before
|
||||
|
||||
A live turn starts. Receives `{ agent, turnId }`. History replay does not fire it.
|
||||
|
||||
```ts
|
||||
server.addHook("agent.turn.before", async ({ agent, turnId }) => {
|
||||
console.log(agent, turnId);
|
||||
});
|
||||
```
|
||||
|
||||
## agent.turn.after
|
||||
|
||||
A live turn completes, fails, or is canceled. Receives `{ agent, turnId, outcome, timeline }`. `outcome.kind` is `completed`, `failed`, or `canceled`. `timeline` is deprecated and always empty; fetch history through the SDK as below.
|
||||
|
||||
### 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. The helper imports types from `@getpaseo/protocol/agent-types`; add
|
||||
`@getpaseo/protocol` at the version matching your daemon 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";
|
||||
import { latestOutputText } from "./server/inspect";
|
||||
|
||||
export default function contribute(server: PluginServerContext) {
|
||||
server.addHook("agent.turn.after", async (event, context) => {
|
||||
if (event.outcome.kind === "canceled") {
|
||||
return;
|
||||
}
|
||||
|
||||
const agent = context.paseo.agents.ref(event.agent.id);
|
||||
const page = await agent.timeline.refetch({ direction: "tail", limit: 20 });
|
||||
const text = latestOutputText(page.entries.map(({ item }) => item));
|
||||
if (/out of credits/i.test(text)) {
|
||||
await agent.send("Try again.");
|
||||
}
|
||||
});
|
||||
|
||||
return () => {};
|
||||
}
|
||||
```
|
||||
|
||||
```text
|
||||
Turn ends: "out of credits"
|
||||
→ plugin sends "Try again."
|
||||
→ a new turn starts
|
||||
```
|
||||
|
||||
The handler fetches only the last 20 timeline items. Output before that page is not inspected.
|
||||
Use `cursor` and `direction` to page further when your plugin needs more history.
|
||||
|
||||
This sends a new message with the existing SDK. Persistent matches keep sending follow-ups;
|
||||
add limits or delays in your plugin when needed. Attachments and tool effects are not replayed.
|
||||
|
||||
## agent.permission.before
|
||||
|
||||
A permission or question becomes pending. Receives `{ agent, request }`; `request` is an `AgentPermissionRequest`. Send the answer through the SDK.
|
||||
|
||||
### Answer a permission request
|
||||
|
||||
Using `shellCommand` from the same [helper file](https://github.com/getpaseo/paseo/blob/main/plugin-examples/lifecycle-actions/server/inspect.ts):
|
||||
|
||||
```ts
|
||||
import type { PluginServerContext } from "@getpaseo/plugin/server";
|
||||
import { shellCommand } from "./server/inspect";
|
||||
|
||||
export default function contribute(server: PluginServerContext) {
|
||||
server.addHook("agent.permission.before", async (event, context) => {
|
||||
const command = shellCommand(event.request);
|
||||
if (command === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const agent = context.paseo.agents.ref(event.agent.id);
|
||||
if (/\brm\s+-rf\b/.test(command)) {
|
||||
await agent.respondToPermission({
|
||||
requestId: event.request.id,
|
||||
response: { behavior: "deny", message: "Recursive deletion is blocked." },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (command.trim() === "git status") {
|
||||
await agent.respondToPermission({
|
||||
requestId: event.request.id,
|
||||
response: { behavior: "allow" },
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return () => {};
|
||||
}
|
||||
```
|
||||
|
||||
| Request | Result |
|
||||
| ------------------------ | ------------------------- |
|
||||
| `rm -rf build` | Declined |
|
||||
| `git status` | Approved |
|
||||
| Other command or request | Left pending for the user |
|
||||
| Already-resolved request | SDK response fails |
|
||||
|
||||
The regex is an example policy, not a shell parser. Permission requests can also be questions,
|
||||
plans, and mode changes; requesting permission does not end the turn.
|
||||
|
||||
## agent.permission.after
|
||||
|
||||
A pending request is answered or cleared. Receives `{ agent, requestId, resolution }`; `resolution` is an `AgentPermissionResponse`.
|
||||
|
||||
```ts
|
||||
server.addHook("agent.permission.after", async ({ agent, requestId, resolution }) => {
|
||||
console.log(agent, requestId, resolution);
|
||||
});
|
||||
```
|
||||
|
||||
Agent hooks exclude the daemon's private disposable helpers. Public background workspaces and their ordinary agents use the same hooks.
|
||||
|
||||
## Ordering and returned values
|
||||
|
||||
```text
|
||||
Creation request
|
||||
→ agent.create.before hooks (plugin-ID order; registration order within each plugin)
|
||||
→ resolve defaults and validate provider configuration
|
||||
→ derive launch configuration with Paseo runtime tools and daemon prompt
|
||||
→ agent.open.before hooks (same ordering; env and plugin-local tools and MCP servers)
|
||||
→ set PASEO_AGENT_ID and PASEO_AGENT_CWD
|
||||
→ open provider session and save agent configuration
|
||||
```
|
||||
|
||||
| Callback returns | Next callback receives |
|
||||
| ------------------------------------------------------- | --------------------------------------------------- |
|
||||
| `{ ...request, env: { ...request.env, REGION: "eu" } }` | Prior request with `REGION` added/replaced |
|
||||
| `{ ...request, env: { REGION: "eu" } }` | Prior request with the entire override map replaced |
|
||||
| `undefined` | Unchanged request |
|
||||
| Throws or returns invalid data | Operation fails; later callbacks do not run |
|
||||
|
||||
There is no automatic deep merge. `env` accumulates across plugins; `tools` and `mcpServers`
|
||||
accumulate only across callbacks within the same plugin. Returning a replacement map or array
|
||||
replaces that scope's prior value.
|
||||
|
||||
## Context and cleanup
|
||||
|
||||
| Contract | Behavior |
|
||||
| ---------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
|
||||
| `context.paseo` | Existing SDK connected to this daemon |
|
||||
| `context.signal` | Aborted on invocation timeout or plugin stop; pass to external requests |
|
||||
| Input data | Detached snapshot; change state through returned requests or SDK commands |
|
||||
| Registration result | Idempotent remover, e.g. `const remove = server.addHook(...); remove();` |
|
||||
| Reload, disable, removal, shutdown | Remaining registrations removed |
|
||||
| Unknown hook name | Registration fails |
|
||||
| Hook timeout | 30 seconds; aborts the signal. A waiting hook fails the pending operation; a notification handler logs an error. |
|
||||
| Event-handler error | Logged against plugin; original operation continues |
|
||||
| Event delivery | Live, best effort; no replay, persistence, or automatic retry |
|
||||
| Event concurrency | Different events may overlap; callback completion order is not guaranteed |
|
||||
|
||||
## Complete examples
|
||||
|
||||
| Plugin | Includes |
|
||||
| ------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------- |
|
||||
| [lifecycle-logger](https://github.com/getpaseo/paseo/tree/main/plugin-examples/lifecycle-logger) | Agent, workspace, and worktree hooks; JSON logs with environment values redacted |
|
||||
| [lifecycle-actions](https://github.com/getpaseo/paseo/tree/main/plugin-examples/lifecycle-actions) | Follow-ups, permissions, environment, provider switching, worktree selection |
|
||||
| [agent-configuration](https://github.com/getpaseo/paseo/tree/main/plugin-examples/agent-configuration) | MCP injection and Codex sandbox/approval options |
|
||||
| [agent-tools](https://github.com/getpaseo/paseo/tree/main/plugin-examples/agent-tools) | Label-targeted tools and file operations in the plugin process |
|
||||
|
||||
Read logger output with `paseo plugin logs lifecycle-logger` or the host's `daemon.log`.
|
||||
|
||||
## Renamed hooks
|
||||
|
||||
The old registration methods are deprecated for removal after 2027-04-30. Existing plugins keep their behavior. Mixed old and new registrations within a plugin run in registration order. `addHook` accepts only the new names.
|
||||
|
||||
| Old registration | New registration |
|
||||
| -------------------------------------------------- | --------------------------------------------------------- |
|
||||
| `server.before("workspace.create", handler)` | `server.addHook("workspace.create.before", asyncHandler)` |
|
||||
| `server.on("workspace.created", handler)` | `server.addHook("workspace.create.after", asyncHandler)` |
|
||||
| `server.on("workspace.archived", handler)` | `server.addHook("workspace.archive.after", asyncHandler)` |
|
||||
| `server.before("agent.create", handler)` | `server.addHook("agent.create.before", asyncHandler)` |
|
||||
| `server.on("agent.created", handler)` | `server.addHook("agent.create.after", asyncHandler)` |
|
||||
| `server.before("agent.session_open", handler)` | `server.addHook("agent.open.before", asyncHandler)` |
|
||||
| `server.on("agent.closed", handler)` | `server.addHook("agent.close.after", asyncHandler)` |
|
||||
| `server.on("agent.archived", handler)` | `server.addHook("agent.archive.after", asyncHandler)` |
|
||||
| `server.on("agent.turn_started", handler)` | `server.addHook("agent.turn.before", asyncHandler)` |
|
||||
| `server.on("agent.turn_ended", handler)` | `server.addHook("agent.turn.after", asyncHandler)` |
|
||||
| `server.on("agent.permission_requested", handler)` | `server.addHook("agent.permission.before", asyncHandler)` |
|
||||
| `server.on("agent.permission_resolved", handler)` | `server.addHook("agent.permission.after", asyncHandler)` |
|
||||
@@ -0,0 +1,641 @@
|
||||
---
|
||||
title: 插件钩子
|
||||
description: 为 Agent 和工作区操作注册钩子,修改请求或接收通知。
|
||||
nav: 钩子
|
||||
---
|
||||
|
||||
# 插件钩子 {#plugin-hooks}
|
||||
|
||||
在 `index.server.ts` 中使用 `server.addHook(name, handler)`。钩子在插件启用时运行,即使没有连接的应用。`addHook` 从 Paseo 0.14 起可用;将 `requirements.paseo` 设置为你开发插件时使用的版本。
|
||||
|
||||
```ts
|
||||
import type { PluginServerContext } from "@getpaseo/plugin/server";
|
||||
|
||||
export default function contribute(server: PluginServerContext) {
|
||||
const remove = server.addHook("agent.open.before", async ({ request }) => ({
|
||||
...request,
|
||||
env: { ...request.env, REGION: "eu" },
|
||||
}));
|
||||
return remove;
|
||||
}
|
||||
```
|
||||
|
||||
## 命名与等待 {#naming-and-timing}
|
||||
|
||||
名称遵循 `subject.operation.position`:主体、操作、`before` 或 `after`。名称标识生命周期中的位置;Paseo 是否等待由每个钩子决定,见下表。`workspace` 包括所有工作区,`worktree` 仅包括 git worktree。
|
||||
|
||||
- 等待型钩子结束后 Paseo 才继续。抛出错误、返回无效数据或超过 30 秒会让操作失败,错误包含 `Plugin <id> <hook> failed: …`。修改请求的钩子收到 `{ request }`,可以返回完整的替换请求或 `undefined`。
|
||||
- 通知收到事件对象,返回 `Promise<void>`。Paseo 不等待;错误会记录在插件名下,原操作继续。`agent.turn.before` 和 `agent.permission.before` 分别在轮次开始或权限进入待处理状态时发出通知。
|
||||
|
||||
所有处理函数都必须为异步函数。轮次完成包括成功、失败和取消,请查看 `outcome`。权限完成包括所有处理结果,请查看 `resolution`。
|
||||
|
||||
创建 Agent 会发生一次,`agent.create.before` 修改已保存的配置。打开 Agent 指启动 Provider 进程,包括首次运行、daemon 重启后恢复、读取历史、导入外部会话和重新加载。`agent.open.before` 为每次启动提供临时的环境变量、工具和 MCP 服务器。创建会打开 Agent;打开已有 Agent 不会创建它;关闭会结束运行中的实例。在应用中打开标签页不属于此操作。
|
||||
|
||||
`worktree.create.after` 在工作区注册前运行,随后是 `workspace.create.after`,两者都早于第一个 Agent 的 `agent.create.before` / `agent.open.before`。
|
||||
|
||||
### 从等待型钩子提前返回 {#return-early-from-a-waiting-hook}
|
||||
|
||||
如果操作应立即继续,可以启动独立任务并返回:
|
||||
|
||||
```ts
|
||||
server.addHook("worktree.create.after", async ({ worktree }) => {
|
||||
void warmCaches(worktree.path);
|
||||
});
|
||||
```
|
||||
|
||||
Paseo 只观察返回的 Promise,因此独立任务没有错误报告,也没有取消 `signal`。
|
||||
|
||||
## 钩子一览 {#hooks}
|
||||
|
||||
| 钩子 | 等待 | 载荷 | 可修改 |
|
||||
| ------------------------- | ---- | -------------------------------------- | ----------------------------------------- |
|
||||
| `workspace.create.before` | 是 | `{ request }` | 整个创建请求 |
|
||||
| `workspace.create.after` | 否 | `{ workspace }` | |
|
||||
| `workspace.archive.after` | 否 | `{ workspace }` | |
|
||||
| `worktree.create.before` | 是 | `{ request }` | 创建选项和目标路径 |
|
||||
| `worktree.create.after` | 是 | `{ worktree }` | worktree 中的文件;抛出错误中止创建 |
|
||||
| `agent.create.before` | 是 | `{ request: { config, env? } }` | 除 `cwd` 外的已保存配置;`env` |
|
||||
| `agent.create.after` | 否 | `{ agent }` | |
|
||||
| `agent.open.before` | 是 | `{ request }` | 仅用于启动的 `env`、`tools`、`mcpServers` |
|
||||
| `agent.close.after` | 否 | `{ agent }` | |
|
||||
| `agent.archive.after` | 否 | `{ agent, archivedAt }` | |
|
||||
| `agent.turn.before` | 否 | `{ agent, turnId }` | |
|
||||
| `agent.turn.after` | 否 | `{ agent, turnId, outcome, timeline }` | |
|
||||
| `agent.permission.before` | 否 | `{ agent, request }` | |
|
||||
| `agent.permission.after` | 否 | `{ agent, requestId, resolution }` | |
|
||||
|
||||
## 共享载荷 {#shared-payloads}
|
||||
|
||||
```ts
|
||||
interface PluginHookAgent {
|
||||
id: string;
|
||||
workspaceId: string | null;
|
||||
parentAgentId: string | null;
|
||||
provider: string;
|
||||
cwd: string;
|
||||
title: string | null;
|
||||
}
|
||||
|
||||
interface PluginHookWorkspace {
|
||||
id: string;
|
||||
projectId: string;
|
||||
cwd: string;
|
||||
name: string | null;
|
||||
archivedAt: string | null;
|
||||
}
|
||||
|
||||
type PluginTurnOutcome =
|
||||
| { kind: "completed" }
|
||||
| { kind: "failed"; error: { message: string; code?: string } }
|
||||
| { kind: "canceled"; reason: string };
|
||||
```
|
||||
|
||||
| 字段 | 结构 / 含义 |
|
||||
| ------------ | ---------------------------------------------------------------------------------------- |
|
||||
| `turnId` | Provider 报告的 `string` 或 `null`;会话重新打开后可能重复 |
|
||||
| `timeline` | 已弃用,始终为 `[]`;通过 SDK 获取历史。 |
|
||||
| `request` | SDK 的 `AgentPermissionRequest`;`kind` 为 `tool`、`plan`、`question`、`mode` 或 `other` |
|
||||
| `resolution` | SDK 的 `AgentPermissionResponse` |
|
||||
| `archivedAt` | 时间戳字符串 |
|
||||
|
||||
## workspace.create.before {#workspacecreatebefore}
|
||||
|
||||
收到 `{ request }`,其中包含 `source`、可选的 `title` 和 `firstAgentContext`。可替换整个显式创建请求;抛出错误、无效返回或超时会让创建失败。已有工作区和目录查找、导入不受影响。
|
||||
|
||||
### 选择工作区隔离方式 {#choose-workspace-isolation}
|
||||
|
||||
```ts
|
||||
server.addHook("workspace.create.before", async ({ request }) => {
|
||||
if (request.source.kind !== "directory") {
|
||||
return request;
|
||||
}
|
||||
|
||||
return {
|
||||
...request,
|
||||
source: {
|
||||
kind: "worktree",
|
||||
cwd: request.source.path,
|
||||
action: "branch-off",
|
||||
},
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
**结果**:显式的目录创建请求会变成 worktree 请求。已有的工作区以及目录查找/导入操作不受影响。
|
||||
|
||||
## workspace.create.after {#workspacecreateafter}
|
||||
|
||||
工作区记录创建且目录可用后触发,收到 `{ workspace }`。
|
||||
|
||||
```ts
|
||||
server.addHook("workspace.create.after", async ({ workspace }) => {
|
||||
console.log(workspace);
|
||||
});
|
||||
```
|
||||
|
||||
## workspace.archive.after {#workspacearchiveafter}
|
||||
|
||||
归档状态保存后触发,收到 `{ workspace }`;worktree 清理可能尚未完成。
|
||||
|
||||
```ts
|
||||
server.addHook("workspace.archive.after", async ({ workspace }) => {
|
||||
console.log(workspace);
|
||||
});
|
||||
```
|
||||
|
||||
## worktree.create.before {#worktreecreatebefore}
|
||||
|
||||
为新 worktree 选择路径和创建选项。通过应用、CLI、MCP 或创建 Agent 新建 worktree 时,Paseo 在 `git worktree add` 之前等待此钩子。钩子收到 `{ request }`,返回完整的替换请求,或返回 `undefined` 保持原值。抛出错误、返回无效数据或超过 30 秒会终止创建,错误包含插件和钩子名称。
|
||||
|
||||
```ts
|
||||
import { basename, dirname, join } from "node:path";
|
||||
|
||||
server.addHook("worktree.create.before", async ({ request }) => ({
|
||||
...request,
|
||||
path: join(
|
||||
dirname(request.sourceCheckout),
|
||||
`${basename(request.sourceCheckout)}.${request.branch.replaceAll("/", "-")}`,
|
||||
),
|
||||
}));
|
||||
```
|
||||
|
||||
源检出目录为 `/home/me/src/paseo`、分支为 `feature/x` 时,目标为 `/home/me/src/paseo.feature-x`。完整插件见 [worktree-placement 示例](https://github.com/getpaseo/paseo/tree/main/plugin-examples/worktree-placement)。
|
||||
|
||||
| 请求字段 | 含义 |
|
||||
| ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
|
||||
| `kind` | `branch-off`、`checkout-branch`、`checkout-change-request` 或 `checkout-github-pr` |
|
||||
| `branch` | 请求的本地分支名称 |
|
||||
| `baseRef` | branch-off 的起始引用;变更请求的比较基准;`checkout-branch` 不含此字段。要从引用创建分支,返回 `kind: "branch-off"`。 |
|
||||
| `slug` | 规范化的 worktree 名称 |
|
||||
| `path` | 绝对目标路径,初始值为已处理现有路径后缀的默认位置 |
|
||||
| `repoRoot` | 只读,主仓库根目录 |
|
||||
| `sourceCheckout` | 只读,源检出根目录,也支持从另一个 worktree 发起创建 |
|
||||
| `forge`、`changeRequestNumber`、`headRef` | 变更请求检出标识;旧版 GitHub 检出用 `githubPrNumber` 代替 `forge` 和 `changeRequestNumber` |
|
||||
| `headRepositoryOwner`、`headRepository` | 可选的源仓库信息;`headRepository` 仅用于变更请求检出 |
|
||||
| `checkoutRefs` | 可选的获取源:`{ remoteName?: string, remoteRef: string }[]` |
|
||||
| `pushRemoteUrl`、`trackOriginHead` | 可选的推送目标和跟踪策略 |
|
||||
|
||||
返回 `path` 后,`slug` 仅用于 branch-off 分支命名;CLI 使用路径的最后一级目录名查找 worktree。
|
||||
|
||||
除 `repoRoot` 和 `sourceCheckout` 外,所有字段都可以修改。返回所选 `kind` 的全部必填字段。Paseo 在钩子之后解析 Git 源计划,因此钩子可以在获取引用之前修改分支和基准,而 Git 之后仍可能选择唯一的最终分支名称。
|
||||
|
||||
Paseo 解析父目录中的符号链接并验证返回的路径。路径必须为绝对路径且不存在,不能是文件系统根目录、用户主目录、仓库根目录、`.git` 目录内的路径或仓库的祖先目录。目标已存在时创建失败,Paseo 不会为插件选择的路径添加后缀。插件应让 Paseo 创建目录。
|
||||
|
||||
顺序:`worktree.create.before` → `git worktree add` → `worktree.create.after` → 注册工作区。恢复缺失的 worktree 时使用记录的原路径,不运行这两个创建钩子。
|
||||
|
||||
## worktree.create.after {#worktreecreateafter}
|
||||
|
||||
新的 git worktree 在磁盘上创建后,Paseo 会等待此钩子,然后才注册工作区、运行 setup、打开终端或启动 Agent。收到 `{ worktree: { path, branch, sourceCheckout } }`,分别是 worktree 绝对路径、分支名、来源 checkout 目录。此时还没有工作区 id。无需返回数据,直接写入 worktree 即可。抛出错误或超过 30 秒会中止创建并删除新 worktree。
|
||||
|
||||
此钩子覆盖新建分支、检出分支和检出 PR。恢复缺失的已归档 worktree 使用独立路径,不触发此钩子。
|
||||
|
||||
```ts
|
||||
import { copyFile } from "node:fs/promises";
|
||||
|
||||
server.addHook("worktree.create.after", async ({ worktree }) => {
|
||||
await copyFile(`${worktree.sourceCheckout}/.env`, `${worktree.path}/.env`);
|
||||
});
|
||||
```
|
||||
|
||||
## agent.create.before {#agentcreatebefore}
|
||||
|
||||
收到 `{ request: { config, env? } }`。配置修改会保存,即使插件以后被停用或移除;`env` 只用于首次启动,不会保存。不能修改 `config.cwd`。抛出错误、无效返回或超时会让创建失败。
|
||||
|
||||
**`agent.create.before.config`** 使用 `AgentSessionConfig`:
|
||||
|
||||
| 字段 | 约束 |
|
||||
| --------------------------------------------- | -------------------------------------------------------- |
|
||||
| `provider`, `model` | 独立的字段;修改 Provider 可能需要同时修改模型/模式/选项 |
|
||||
| `modeId`, `thinkingOptionId`, `featureValues` | 特定于 Provider 的选择 |
|
||||
| `title`, `systemPrompt` | Agent 配置 |
|
||||
| `providerOptions` | 不透明的、特定于 Provider 的选项 |
|
||||
| `mcpServers`, `toolPolicy` | MCP 配置和精确的工具预批准 |
|
||||
| `cwd` | 不能修改 |
|
||||
|
||||
### 修改已保存的配置 {#change-saved-configuration}
|
||||
|
||||
在 `contribute(server)` 中添加这个回调,为新的 Codex Agent 设置已保存的选项:
|
||||
|
||||
```ts
|
||||
server.addHook("agent.create.before", async ({ request }) => {
|
||||
if (request.config.provider !== "codex") {
|
||||
return request;
|
||||
}
|
||||
|
||||
return {
|
||||
...request,
|
||||
config: {
|
||||
...request.config,
|
||||
providerOptions: {
|
||||
...request.config.providerOptions,
|
||||
sandbox_mode: "workspace-write",
|
||||
approval_policy: "on-request",
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
| 输入 | 结果 |
|
||||
| ------------------------------------------- | ------------------- |
|
||||
| `providerOptions.sandbox_mode: "read-only"` | `"workspace-write"` |
|
||||
| `providerOptions.web_search: "disabled"` | 由展开运算保留 |
|
||||
|
||||
所选的 Provider 会校验 `providerOptions`。显式的 Codex 沙盒和批准选项会覆盖它的模式预设。
|
||||
|
||||
## agent.create.after {#agentcreateafter}
|
||||
|
||||
常规创建完成后触发,收到 `{ agent }`;导入和恢复不会触发。
|
||||
|
||||
```ts
|
||||
server.addHook("agent.create.after", async ({ agent }) => {
|
||||
console.log(agent);
|
||||
});
|
||||
```
|
||||
|
||||
## agent.open.before {#agentopenbefore}
|
||||
|
||||
收到 `{ request }`,只能修改 `env`、`tools` 和 `mcpServers`,修改只用于此次启动。其他字段只读。抛出错误、无效返回或超时会让打开操作失败。
|
||||
|
||||
**`agent.open.before` 请求示例:**
|
||||
|
||||
```json
|
||||
{
|
||||
"agentId": "agent-123",
|
||||
"workspaceId": "workspace-456",
|
||||
"provider": "codex",
|
||||
"cwd": "/projects/shop",
|
||||
"title": "Workspace notes",
|
||||
"labels": { "notes/enabled": "true" },
|
||||
"tools": [],
|
||||
"mcpServers": {},
|
||||
"reason": "resume",
|
||||
"purpose": "interactive",
|
||||
"env": { "COMPANY_ENV": "development" }
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 取值 |
|
||||
| ------------- | -------------------------------------------------------------------------------------------------------- |
|
||||
| `workspaceId` | 字符串或 `null` |
|
||||
| `title` | 只读字符串或 `null`;创建时使用创建请求的标题,恢复时使用已保存的标题。 |
|
||||
| `labels` | 只读的字符串映射;创建时使用创建请求的标签,恢复时使用已保存的标签。 |
|
||||
| `tools` | 可编辑的 `AgentToolDefinition[]`,仅包含此插件的工具。参阅[为 Agent 附加工具](#attach-tools-to-agents)。 |
|
||||
| `mcpServers` | 可编辑的服务器映射,仅用于启动且仅包含本插件的条目。参阅[注入 MCP 服务器](#inject-mcp-servers)。 |
|
||||
| `reason` | `create`、`resume`、`refresh`、`import` |
|
||||
| `purpose` | `interactive`、`history` |
|
||||
| `env` | 启动覆盖映射;不包括 daemon 继承的环境变量。替换整个映射即可添加、替换或移除覆盖项。 |
|
||||
|
||||
### 在每次打开会话时注入环境变量 {#inject-environment-variables-on-every-session-opening}
|
||||
|
||||
```ts
|
||||
server.addHook("agent.open.before", async ({ request }) => {
|
||||
return {
|
||||
...request,
|
||||
env: {
|
||||
...request.env,
|
||||
COMPANY_ENV: "development",
|
||||
},
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
会话打开时的环境变量覆盖项会在每次打开时重新计算。`agent.create.before.env` 只作用于首次启动,不会写入 Agent 的已保存配置。
|
||||
|
||||
### 注入 MCP 服务器 {#inject-mcp-servers}
|
||||
|
||||
需要将服务器保存为 Agent 配置的一部分时,使用 `agent.create.before.config.mcpServers`。
|
||||
插件拥有的服务器使用下面的回调,在 `contribute(server)` 中添加,并把占位 URL 替换为你的 MCP 端点:
|
||||
|
||||
```ts
|
||||
server.addHook("agent.open.before", async ({ request }) => {
|
||||
if (request.provider !== "codex") return;
|
||||
|
||||
return {
|
||||
...request,
|
||||
mcpServers: {
|
||||
...request.mcpServers,
|
||||
company: { type: "http", url: "https://tools.example.com/mcp" },
|
||||
},
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
`request.mcpServers` 使用与 `AgentSessionConfig.mcpServers` 相同的服务器配置。
|
||||
每个插件的映射从空开始,只在该插件自己的回调之间累积,不包含 Agent 已保存的服务器或其他插件的服务器。
|
||||
替换映射即可添加、替换或移除本插件的条目;返回 `undefined` 会保持不变。
|
||||
Paseo 按插件 ID 顺序合并各插件的最终映射。
|
||||
|
||||
名称与已保存的服务器、Paseo 内部的 `paseo` 服务器或其他插件的服务器冲突时,会话打开失败,错误会指出服务器名称及双方来源。
|
||||
不支持 MCP 的 Provider 会报 `does not support MCP servers` 错误,并指出插件名称。
|
||||
`purpose: "history"` 的会话打开会忽略这些服务器。安装、停用和重新加载插件后,现有 Agent
|
||||
通过[会话重新打开行为](#attach-tools-to-agents)获取变化。
|
||||
|
||||
### 为 Agent 附加工具 {#attach-tools-to-agents}
|
||||
|
||||
在 `index.server.ts` 中定义工具,并在 `server.addHook("agent.open.before", async handler)` 中附加它。
|
||||
此示例只向带有 `notes/enabled: "true"` 标签的 Agent 提供 `append_workspace_note`:
|
||||
|
||||
```ts
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { z } from "zod";
|
||||
import { defineAgentTool, type PluginServerContext } from "@getpaseo/plugin/server";
|
||||
|
||||
const appendWorkspaceNote = defineAgentTool({
|
||||
name: "append_workspace_note",
|
||||
description: "Append a note to AGENT_NOTES.md in this agent's working directory.",
|
||||
input: z.object({ note: z.string().min(1).describe("Note to append") }),
|
||||
async execute({ note }, { agent, signal }) {
|
||||
const notePath = path.join(agent.cwd, "AGENT_NOTES.md");
|
||||
await writeFile(notePath, `${note}\n`, { encoding: "utf8", flag: "a", signal });
|
||||
return {
|
||||
text: `Saved note to ${notePath}`,
|
||||
structuredContent: { path: notePath },
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default function contribute(server: PluginServerContext) {
|
||||
server.addHook("agent.open.before", async ({ request }) => {
|
||||
if (request.labels["notes/enabled"] !== "true") return;
|
||||
return { ...request, tools: [...request.tools, appendWorkspaceNote] };
|
||||
});
|
||||
return () => {};
|
||||
}
|
||||
```
|
||||
|
||||
通过 SDK 创建 Agent 时设置标签:
|
||||
|
||||
```ts
|
||||
const agent = await paseo.agents.create({
|
||||
config: { provider: "codex/gpt-5.4" },
|
||||
cwd: "/projects/shop",
|
||||
labels: { "notes/enabled": "true" },
|
||||
});
|
||||
```
|
||||
|
||||
使用会话打开请求中的字段选择 Agent:
|
||||
|
||||
| 选择依据 | 回调中的条件 |
|
||||
| ---------- | -------------------------------------------- |
|
||||
| 标签 | `request.labels["notes/enabled"] === "true"` |
|
||||
| Provider | `request.provider === "codex"` |
|
||||
| 工作区 | `request.workspaceId === "workspace-456"` |
|
||||
| 单个 Agent | `request.agentId === chosenAgentId` |
|
||||
|
||||
每个插件的 `request.tools` 都从空数组开始。同一插件中的后续回调会看到前面回调返回的工具;
|
||||
替换数组即可添加或移除工具。返回 `undefined` 会保留请求。`purpose: "history"` 时会忽略工具。
|
||||
|
||||
从 `@getpaseo/plugin/server` 导入 `defineAgentTool`、`AgentToolDefinition`、`AgentToolContext`
|
||||
和 `AgentToolResult`。辅助函数会根据 Zod schema 推断验证后的输入类型:
|
||||
|
||||
```ts
|
||||
declare function defineAgentTool<Input extends z.ZodType>(tool: {
|
||||
name: string;
|
||||
title?: string;
|
||||
description: string;
|
||||
input: Input;
|
||||
execute(
|
||||
input: z.output<Input>,
|
||||
context: AgentToolContext,
|
||||
): AgentToolResult | Promise<AgentToolResult>;
|
||||
}): AgentToolDefinition;
|
||||
|
||||
interface AgentToolResult {
|
||||
text?: string;
|
||||
structuredContent?: unknown;
|
||||
isError?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
| 执行 context | 值 |
|
||||
| ------------ | --------------------------------------------------------------------------------------------------- |
|
||||
| `agent` | 发起调用的 Agent,以 `PluginHookAgent` 表示,包括 `id`、`workspaceId`、`provider`、`cwd` 和 `title` |
|
||||
| `paseo` | 已连接到此 daemon 的 SDK |
|
||||
| `signal` | 此次工具调用的中止信号;请把它传给文件操作或外部请求 |
|
||||
|
||||
`execute` 在插件进程中运行。闭包和原始 Zod schema 保留在该进程中;只有工具元数据和调用的
|
||||
输入、结果会跨进程传递。Paseo 在调用 `execute` 前验证输入。无效输入会将 schema 问题返回给
|
||||
Agent。抛出的错误会转换为 `{ isError: true, text: message }`,插件继续运行。取消调用或关闭
|
||||
其传输连接会中止 `signal` 并丢弃结果。工具调用最长运行 10 分钟,与钩子的 30 秒超时分开。
|
||||
关闭或重新打开 Agent 会话会移除旧定义。
|
||||
|
||||
在 `text` 中返回模型可见的文本。`structuredContent` 必须可序列化为 JSON;省略 `text` 时,
|
||||
Paseo 会将该值格式化为供模型读取的文本。设置 `isError: true` 可报告工具错误。
|
||||
|
||||
Paseo 通过 Agent 工具传输提供附加的工具,按 Provider 的支持情况使用 MCP 或原生注册。
|
||||
同一份定义和回调适用于这两种方式。附加的工具遵循[与 Paseo 工具相同的限制](/docs/mcp#limit-paseo-tools-by-provider)。
|
||||
只有该 Agent 能看到这些工具。
|
||||
|
||||
名称必须匹配 `/^[a-z][a-z0-9_]*$/`,且不超过 64 个字符。如果名称与同一 Agent 的 Paseo
|
||||
工具或另一插件的工具冲突,会话打开会失败,并指出双方。一个插件内的重复名称也会导致
|
||||
会话打开失败。回调未选中的 Agent 不受影响。
|
||||
|
||||
| 变更 | Agent 何时收到变更 |
|
||||
| ----------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 启动、停止或重新加载声明了 `agent.open.before` 的插件 | 已打开的会话会标记为过期。每个会话在下一次空闲提示词前重新打开;正在运行的轮次会继续,不会被中断。重新打开后的下一条提示词复用该会话。 |
|
||||
| 修改回调选择的 Agent | 编辑此示例后运行 `paseo plugin reload agent-tools`,然后在 Agent 空闲时发送提示词。 |
|
||||
| 立即应用 | 调用 `await paseo.agents.ref(id).reload({ requestId: "reload-notes" })`。`requestId` 可选。这会重新打开 Provider 会话,以 `reason: "refresh"` 再次运行钩子,并中断正在运行的轮次。 |
|
||||
| 读取当前快照 | 调用 `await paseo.agents.ref(id).refresh()`。这只读取状态,不会重新打开会话。 |
|
||||
|
||||
如果自动重新打开失败,触发它的提示词会返回该错误。参阅
|
||||
[完整 agent-tools 示例](https://github.com/getpaseo/paseo/tree/main/plugin-examples/agent-tools)。
|
||||
|
||||
## agent.close.after {#agentcloseafter}
|
||||
|
||||
运行中的 Agent 实例关闭后触发,收到 `{ agent }`。
|
||||
|
||||
```ts
|
||||
server.addHook("agent.close.after", async ({ agent }) => {
|
||||
console.log(agent);
|
||||
});
|
||||
```
|
||||
|
||||
## agent.archive.after {#agentarchiveafter}
|
||||
|
||||
归档状态保存后触发,收到 `{ agent, archivedAt }`;运行时清理可能尚未完成。
|
||||
|
||||
```ts
|
||||
server.addHook("agent.archive.after", async ({ agent, archivedAt }) => {
|
||||
console.log(agent, archivedAt);
|
||||
});
|
||||
```
|
||||
|
||||
## agent.turn.before {#agentturnbefore}
|
||||
|
||||
实时轮次开始后触发,收到 `{ agent, turnId }`。历史重放不会触发。
|
||||
|
||||
```ts
|
||||
server.addHook("agent.turn.before", async ({ agent, turnId }) => {
|
||||
console.log(agent, turnId);
|
||||
});
|
||||
```
|
||||
|
||||
## agent.turn.after {#agentturnafter}
|
||||
|
||||
实时轮次完成、失败或取消后触发,收到 `{ agent, turnId, outcome, timeline }`。`outcome.kind` 为 `completed`、`failed` 或 `canceled`。`timeline` 已弃用且始终为空;请按下面的示例通过 SDK 获取历史。
|
||||
|
||||
### 在轮次结束时发送后续消息 {#send-a-follow-up-when-a-turn-ends}
|
||||
|
||||
把 [server/inspect.ts](https://github.com/getpaseo/paseo/blob/main/plugin-examples/lifecycle-actions/server/inspect.ts) 复制到你的插件中。该辅助文件从 `@getpaseo/protocol/agent-types` 导入类型;请把与 daemon 版本相同的 `@getpaseo/protocol` 加入开发依赖,并在加载插件之前安装它们。`latestOutputText` 会拼接最近一条用户消息之后的文本片段。
|
||||
|
||||
```ts
|
||||
import type { PluginServerContext } from "@getpaseo/plugin/server";
|
||||
import { latestOutputText } from "./server/inspect";
|
||||
|
||||
export default function contribute(server: PluginServerContext) {
|
||||
server.addHook("agent.turn.after", async (event, context) => {
|
||||
if (event.outcome.kind === "canceled") {
|
||||
return;
|
||||
}
|
||||
|
||||
const agent = context.paseo.agents.ref(event.agent.id);
|
||||
const page = await agent.timeline.refetch({ direction: "tail", limit: 20 });
|
||||
const text = latestOutputText(page.entries.map(({ item }) => item));
|
||||
if (/out of credits/i.test(text)) {
|
||||
await agent.send("Try again.");
|
||||
}
|
||||
});
|
||||
|
||||
return () => {};
|
||||
}
|
||||
```
|
||||
|
||||
```text
|
||||
Turn ends: "out of credits"
|
||||
→ plugin sends "Try again."
|
||||
→ a new turn starts
|
||||
```
|
||||
|
||||
该处理函数只获取最近 20 条时间线条目,不检查这一页之前的输出。插件需要更多历史时,请用 `cursor` 和 `direction` 继续翻页。
|
||||
|
||||
这会用现有的 SDK 发送一条新消息。持续匹配会不断发送后续消息;需要时请在插件中添加次数限制或延迟。附件和工具的效果不会被重放。
|
||||
|
||||
## agent.permission.before {#agentpermissionbefore}
|
||||
|
||||
权限或问题进入待处理状态后触发,收到 `{ agent, request }`;`request` 为 `AgentPermissionRequest`。通过 SDK 回应。
|
||||
|
||||
### 回应权限请求 {#answer-a-permission-request}
|
||||
|
||||
使用同一个[辅助文件](https://github.com/getpaseo/paseo/blob/main/plugin-examples/lifecycle-actions/server/inspect.ts)中的 `shellCommand`:
|
||||
|
||||
```ts
|
||||
import type { PluginServerContext } from "@getpaseo/plugin/server";
|
||||
import { shellCommand } from "./server/inspect";
|
||||
|
||||
export default function contribute(server: PluginServerContext) {
|
||||
server.addHook("agent.permission.before", async (event, context) => {
|
||||
const command = shellCommand(event.request);
|
||||
if (command === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const agent = context.paseo.agents.ref(event.agent.id);
|
||||
if (/\brm\s+-rf\b/.test(command)) {
|
||||
await agent.respondToPermission({
|
||||
requestId: event.request.id,
|
||||
response: { behavior: "deny", message: "Recursive deletion is blocked." },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (command.trim() === "git status") {
|
||||
await agent.respondToPermission({
|
||||
requestId: event.request.id,
|
||||
response: { behavior: "allow" },
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return () => {};
|
||||
}
|
||||
```
|
||||
|
||||
| 请求 | 结果 |
|
||||
| ---------------- | -------------------- |
|
||||
| `rm -rf build` | 拒绝 |
|
||||
| `git status` | 批准 |
|
||||
| 其他命令或请求 | 保持待处理,留给用户 |
|
||||
| 已经处理过的请求 | SDK 响应失败 |
|
||||
|
||||
这个正则表达式只是一个示例策略,而不是 shell 解析器。权限请求也可能是问题、计划和模式变更;请求权限不会结束当前轮次。
|
||||
|
||||
## agent.permission.after {#agentpermissionafter}
|
||||
|
||||
待处理请求得到回应或清除后触发,收到 `{ agent, requestId, resolution }`;`resolution` 为 `AgentPermissionResponse`。
|
||||
|
||||
```ts
|
||||
server.addHook("agent.permission.after", async ({ agent, requestId, resolution }) => {
|
||||
console.log(agent, requestId, resolution);
|
||||
});
|
||||
```
|
||||
|
||||
Agent 钩子不包括 daemon 的私有临时辅助 Agent。公开的后台工作区及其普通 Agent 使用相同钩子。
|
||||
|
||||
## 顺序与返回值 {#ordering-and-returned-values}
|
||||
|
||||
```text
|
||||
Creation request
|
||||
→ agent.create.before hooks (plugin-ID order; registration order within each plugin)
|
||||
→ resolve defaults and validate provider configuration
|
||||
→ derive launch configuration with Paseo runtime tools and daemon prompt
|
||||
→ agent.open.before hooks (same ordering; env and plugin-local tools and MCP servers)
|
||||
→ set PASEO_AGENT_ID and PASEO_AGENT_CWD
|
||||
→ open provider session and save agent configuration
|
||||
```
|
||||
|
||||
| 回调返回 | 下一个回调收到 |
|
||||
| ------------------------------------------------------- | --------------------------------- |
|
||||
| `{ ...request, env: { ...request.env, REGION: "eu" } }` | 之前的请求,添加或替换了 `REGION` |
|
||||
| `{ ...request, env: { REGION: "eu" } }` | 之前的请求,整个覆盖映射被替换 |
|
||||
| `undefined` | 未改变的请求 |
|
||||
| 抛出错误或返回无效数据 | 操作失败;后面的回调不会运行 |
|
||||
|
||||
没有自动的深度合并。`env` 在插件之间累积;`tools` 和 `mcpServers` 只在同一插件的回调之间累积。
|
||||
返回替换的映射或数组会替换该范围内之前的值。
|
||||
|
||||
## context 与清理 {#context-and-cleanup}
|
||||
|
||||
| 契约 | 行为 |
|
||||
| -------------------------- | ---------------------------------------------------------------------------------- |
|
||||
| `context.paseo` | 已连接到此 daemon 的现有 SDK |
|
||||
| `context.signal` | 在调用超时或插件停止时中止;请把它传给外部请求 |
|
||||
| 输入数据 | 分离的快照;通过返回的请求或 SDK 命令改变状态 |
|
||||
| 注册结果 | 幂等的移除函数,例如 `const remove = server.addHook(...); remove();` |
|
||||
| 重新加载、停用、移除、关闭 | 移除剩余的注册 |
|
||||
| 未知的钩子名称 | 注册失败 |
|
||||
| 钩子超时 | 30 秒;会中止 signal。等待型钩子会让待处理的操作失败;事件处理函数会记录一条错误。 |
|
||||
| 事件处理函数出错 | 记录在插件名下;原操作继续进行 |
|
||||
| 事件投递 | 实时、尽力而为;没有重放、持久化或自动重试 |
|
||||
| 事件并发 | 不同的事件可能重叠;不保证回调完成的顺序 |
|
||||
|
||||
## 完整示例 {#complete-examples}
|
||||
|
||||
| 插件 | 包含 |
|
||||
| ------------------------------------------------------------------------------------------------------ | --------------------------------------------------------- |
|
||||
| [lifecycle-logger](https://github.com/getpaseo/paseo/tree/main/plugin-examples/lifecycle-logger) | Agent、工作区和 worktree 钩子;隐去环境变量值的 JSON 日志 |
|
||||
| [lifecycle-actions](https://github.com/getpaseo/paseo/tree/main/plugin-examples/lifecycle-actions) | 后续消息、权限、环境变量、切换 Provider、选择 worktree |
|
||||
| [agent-configuration](https://github.com/getpaseo/paseo/tree/main/plugin-examples/agent-configuration) | MCP 注入以及 Codex 沙盒/批准选项 |
|
||||
| [agent-tools](https://github.com/getpaseo/paseo/tree/main/plugin-examples/agent-tools) | 按标签选择 Agent,并在插件进程中执行文件操作的工具 |
|
||||
|
||||
用 `paseo plugin logs lifecycle-logger` 或主机的 `daemon.log` 读取 logger 的输出。
|
||||
|
||||
## 已重命名的钩子 {#renamed-hooks}
|
||||
|
||||
旧注册方法已弃用,会在 2027-04-30 之后移除。现有插件继续按原来的方式运行。同一插件中混合使用新旧注册方法时,处理函数按注册顺序运行。`addHook` 只接受新名称。
|
||||
|
||||
| 旧注册方式 | 新注册方式 |
|
||||
| -------------------------------------------------- | --------------------------------------------------------- |
|
||||
| `server.before("workspace.create", handler)` | `server.addHook("workspace.create.before", asyncHandler)` |
|
||||
| `server.on("workspace.created", handler)` | `server.addHook("workspace.create.after", asyncHandler)` |
|
||||
| `server.on("workspace.archived", handler)` | `server.addHook("workspace.archive.after", asyncHandler)` |
|
||||
| `server.before("agent.create", handler)` | `server.addHook("agent.create.before", asyncHandler)` |
|
||||
| `server.on("agent.created", handler)` | `server.addHook("agent.create.after", asyncHandler)` |
|
||||
| `server.before("agent.session_open", handler)` | `server.addHook("agent.open.before", asyncHandler)` |
|
||||
| `server.on("agent.closed", handler)` | `server.addHook("agent.close.after", asyncHandler)` |
|
||||
| `server.on("agent.archived", handler)` | `server.addHook("agent.archive.after", asyncHandler)` |
|
||||
| `server.on("agent.turn_started", handler)` | `server.addHook("agent.turn.before", asyncHandler)` |
|
||||
| `server.on("agent.turn_ended", handler)` | `server.addHook("agent.turn.after", asyncHandler)` |
|
||||
| `server.on("agent.permission_requested", handler)` | `server.addHook("agent.permission.before", asyncHandler)` |
|
||||
| `server.on("agent.permission_resolved", handler)` | `server.addHook("agent.permission.after", asyncHandler)` |
|
||||
@@ -11,6 +11,7 @@ category: Plugins
|
||||
A plugin is a TypeScript project installed into one Paseo daemon. It can add
|
||||
[screens and sidebar items](/docs/plugins/reference#screens-and-sidebar-items),
|
||||
[workspace panels](/docs/plugins/reference#workspace-panels),
|
||||
[file panels](/docs/plugins/reference#file-panels),
|
||||
[Command Center items](/docs/plugins/reference#command-center-items),
|
||||
[slash commands](/docs/plugins/reference#slash-commands),
|
||||
[composer pills](/docs/plugins/reference#composer-pills),
|
||||
@@ -222,7 +223,8 @@ client.addCommandCenterItem({
|
||||
});
|
||||
```
|
||||
|
||||
`icon` is a [Lucide](https://lucide.dev/icons/) icon name.
|
||||
`icon` is a [Lucide](https://lucide.dev/icons/) icon name, inline SVG, or a component. See
|
||||
[Icons](/docs/plugins/reference#icons).
|
||||
|
||||
## Edit and reload
|
||||
|
||||
|
||||
@@ -0,0 +1,253 @@
|
||||
---
|
||||
title: 插件快速上手
|
||||
description: 构建、安装、分享和更新一个受信任的 Paseo 插件。
|
||||
nav: 快速上手
|
||||
---
|
||||
|
||||
# 插件快速上手 {#plugin-quickstart}
|
||||
|
||||
插件是安装到某个 Paseo daemon 中的 TypeScript 项目。它可以添加[界面和侧边栏项](/docs/plugins/reference#screens-and-sidebar-items)、[工作区面板](/docs/plugins/reference#workspace-panels)、[文件面板](/docs/plugins/reference#file-panels)、[Command Center 项](/docs/plugins/reference#command-center-items)、[斜杠命令](/docs/plugins/reference#slash-commands)、[Composer 标签](/docs/plugins/reference#composer-pills)、[时间线条目](/docs/plugins/reference#timeline-items)、[主题](/docs/plugins/reference#contribute-a-theme)、[附件来源](/docs/plugins/reference#add-a-composer-attachment-source)和 [daemon 端 RPC](/docs/plugins/reference#add-plugin-specific-backend-behavior)。它还可以[把编程 Agent 接入为 Provider](/docs/plugins/providers)。客户端贡献会在连接到该 daemon 的每个 Paseo 客户端上运行,包括移动端。
|
||||
|
||||
浏览[已发布的插件](https://paseo.sh/plugins),然后在你的 daemon 上安装一个:
|
||||
|
||||
```bash
|
||||
paseo plugin add owner/slug
|
||||
```
|
||||
|
||||
或者把 `owner/slug` 粘贴到 **设置 → 插件 → 插件来源**,然后选择 **安装插件**。Paseo 会安装注册表中经过审查的制品。想直接从 Git 仓库安装时,请使用 `git:owner/repository` 或完整的 Git URL。
|
||||
|
||||
本指南会搭建一个插件、运行它,并为它添加一个工作区面板。
|
||||
|
||||
## 创建插件 {#create-a-plugin}
|
||||
|
||||
使用 daemon 所在机器上的绝对路径:
|
||||
|
||||
```bash
|
||||
paseo plugin init /absolute/path/to/workspace-plugin
|
||||
cd /absolute/path/to/workspace-plugin
|
||||
npm install
|
||||
```
|
||||
|
||||
`init` 会写入一个严格模式的 TypeScript 项目,但不会运行包管理器。`npm install` 只添加用于类型检查和测试的开发依赖;插件 SDK、React、React Native、TanStack Query 和 Zod 由 Paseo 在运行时提供。
|
||||
|
||||
生成的脚手架就是一个可用的插件:一个从侧边栏打开的界面,其中有一个按钮,通过 RPC 向 daemon 请求一句问候。
|
||||
|
||||
```text
|
||||
workspace-plugin/
|
||||
paseo-plugin.json # plugin ID and supported Paseo versions
|
||||
index.client.tsx # runs in the Paseo app
|
||||
index.server.ts # runs in a daemon subprocess
|
||||
client/greeting.tsx # the screen component
|
||||
client/web.ts # the only file allowed to touch browser APIs
|
||||
server/greeting.ts # the RPC handler
|
||||
shared/greeting.ts # the RPC contract, imported by both
|
||||
package.json
|
||||
tsconfig.json
|
||||
```
|
||||
|
||||
每个入口默认导出一个函数,该函数注册贡献并返回一个清理函数。`index.client.tsx` 注册界面以及打开它的侧边栏项:
|
||||
|
||||
```tsx
|
||||
import type { PluginClientContext, PluginSidebarItemProps } from "@getpaseo/plugin/client";
|
||||
import { SidebarRow } from "@getpaseo/plugin/client/ui";
|
||||
import { GreetingScreen } from "./client/greeting";
|
||||
|
||||
function GreetingItem({ currentScreen, openScreen }: PluginSidebarItemProps) {
|
||||
return (
|
||||
<SidebarRow
|
||||
icon="MessageCircle"
|
||||
active={currentScreen?.screenId === "greeting"}
|
||||
onPress={() => openScreen({ screenId: "greeting" })}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default function contribute(client: PluginClientContext) {
|
||||
client.addScreen({ id: "greeting", title: "Greeting", Component: GreetingScreen });
|
||||
client.addSidebarHeaderItem({ id: "greeting", title: "Greeting", Component: GreetingItem });
|
||||
return () => {};
|
||||
}
|
||||
```
|
||||
|
||||
`index.server.ts` 为 `shared/greeting.ts` 中的契约注册处理函数:
|
||||
|
||||
```ts
|
||||
import type { PluginServerContext } from "@getpaseo/plugin/server";
|
||||
import { createGreeting } from "./server/greeting";
|
||||
import { greetingRpc } from "./shared/greeting";
|
||||
|
||||
export default function contribute(server: PluginServerContext) {
|
||||
server.handle(greetingRpc, createGreeting);
|
||||
return () => {};
|
||||
}
|
||||
```
|
||||
|
||||
目录就是边界。`client/` 下的代码只编译进应用的包,`server/` 下的代码只编译进 daemon 的包,`shared/` 则编译进两者。跨越这条边界导入、在根目录添加代码文件,或在客户端代码中导入 `node:` 模块,都会导致编译错误。没有 daemon 端工作的插件可以省略 `index.server.ts`;没有 UI 的插件可以省略 `index.client.tsx`。
|
||||
|
||||
客户端代码既在浏览器中运行,也在手机上运行。项目在不包含 DOM 库的情况下进行类型检查,因此在 `client/web.ts` 之外使用 `document` 和 `window` 都会报错;`client/web.ts` 演示了如何用 `Platform.OS` 把浏览器 API 隔离开,并提供原生端的后备实现。编写 UI 之前,请先阅读[跨平台规则](/docs/plugins/reference#cross-platform-rules)。
|
||||
|
||||
## 安装并试用 {#install-and-try-it}
|
||||
|
||||
插件是受信任的、没有沙盒的代码:服务端代码和准备命令以 daemon 用户在 daemon 所在机器上的权限运行,客户端代码在 Paseo 应用内部运行。安装插件意味着你信任该代码库、它的依赖以及它将来的更新。
|
||||
|
||||
在你要安装到的 daemon 上,打开 **设置 → 插件** 下的 **启用插件**。它是该 daemon 上所有插件的全局开关,也对应 daemon 的 `config.json` 中根级的 `pluginsEnabled` 字段;编辑该文件后,用 `paseo reload --json` 应用更改。自动化工具必须先读取当前值,并在获得你的明确许可后才能打开它。
|
||||
|
||||
然后进行类型检查并安装:
|
||||
|
||||
```bash
|
||||
npm run typecheck
|
||||
paseo plugin install /absolute/path/to/workspace-plugin
|
||||
paseo plugin ls
|
||||
```
|
||||
|
||||
或者从应用中安装该目录:
|
||||
|
||||
1. 在目标主机上打开 **设置 → 插件**。
|
||||
2. 把 `/absolute/path/to/workspace-plugin` 粘贴到 **插件来源**。
|
||||
3. 选择 **安装插件**。应用会使用 `paseo-plugin.json` 中的插件 ID。
|
||||
|
||||
来源字段也接受 Git 仓库、npm 包和插件子目录。可接受的语法和解析规则见[插件来源](/docs/plugins/reference#plugin-sources)。如果表单要求你更新主机,请先更新该主机再安装;已有的管理操作仍然可用。
|
||||
|
||||
每个已安装插件的行都会显示它的状态和描述。用它的开关启用或停用它,打开它的三点菜单可以进行设置、查看日志、重新加载和移除。
|
||||
|
||||
`paseo plugin ls` 应该把该插件报告为 `running`。打开 Paseo,在侧边栏中选择 **Greeting**,然后按 **Create greeting**。消息会通过 RPC 从 daemon 子进程返回。
|
||||
|
||||
如果侧边栏项没有出现,请检查 **启用插件** 是否已打开、插件是否为 `running`,以及客户端正在查看的是否就是你安装到的主机。`paseo plugin logs workspace-plugin` 会显示 daemon 端的输出,包括加载错误。
|
||||
|
||||
## 添加工作区面板 {#add-a-workspace-panel}
|
||||
|
||||
工作区面板会作为标签页,与 Agent、终端和文件并排打开。创建 `client/overview.tsx`:
|
||||
|
||||
```tsx
|
||||
import { type PluginWorkspacePanelProps, useWorkspace } from "@getpaseo/plugin/client";
|
||||
import { useMemo } from "react";
|
||||
import { Text, View } from "react-native";
|
||||
|
||||
export function WorkspaceOverview({ theme, layout, workspaceId }: PluginWorkspacePanelProps) {
|
||||
const workspace = useWorkspace(workspaceId, ({ name, directory }) => ({
|
||||
name,
|
||||
directory,
|
||||
}));
|
||||
const styles = useMemo(
|
||||
() => ({
|
||||
screen: {
|
||||
flex: 1,
|
||||
padding: layout.compact ? 16 : 24,
|
||||
gap: layout.compact ? 8 : 12,
|
||||
backgroundColor: theme.colors.surface0,
|
||||
},
|
||||
title: { color: theme.colors.foreground, fontSize: layout.compact ? 20 : 24 },
|
||||
label: { color: theme.colors.foregroundMuted },
|
||||
detail: { color: theme.colors.foreground },
|
||||
}),
|
||||
[theme, layout.compact],
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={styles.screen}>
|
||||
<Text style={styles.title}>{workspace?.name}</Text>
|
||||
<Text style={styles.label}>Directory</Text>
|
||||
<Text style={styles.detail}>{workspace?.directory}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
`useWorkspace` 从应用的缓存状态中读取面板要渲染的字段,不需要 RPC,并且在无关字段变化时不会重新渲染。每个 `Text` 都从 `theme.colors` 获取颜色,`layout.compact` 决定间距,因此面板在每个 Paseo 主题和手机上都能正常显示。令牌列表见[主题与布局](/docs/plugins/reference#theme-and-layout)。
|
||||
|
||||
在 `index.client.tsx` 中加入以下内容,注册该面板以及一个打开它的 Command Center 项:
|
||||
|
||||
```tsx
|
||||
import { WorkspaceOverview } from "./client/overview";
|
||||
|
||||
// Inside contribute(client), after the existing registrations:
|
||||
client.addWorkspacePanel({
|
||||
id: "overview",
|
||||
title: "Workspace overview",
|
||||
icon: "PanelsTopLeft",
|
||||
context: "workspace",
|
||||
locations: ["workspace", "explorer"],
|
||||
Component: WorkspaceOverview,
|
||||
});
|
||||
client.addCommandCenterItem({
|
||||
id: "open-overview",
|
||||
title: "Open workspace overview",
|
||||
icon: "PanelsTopLeft",
|
||||
context: "workspace",
|
||||
onSelect({ openPanel }) {
|
||||
openPanel("overview");
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
`icon` 可以是 [Lucide](https://lucide.dev/icons/) 图标名称、内联 SVG 或组件。参见[图标](/docs/plugins/reference#icons)。
|
||||
|
||||
## 编辑并重新加载 {#edit-and-reload}
|
||||
|
||||
只有在重新加载插件后,源码改动才会生效:
|
||||
|
||||
```bash
|
||||
npm run typecheck
|
||||
paseo plugin reload workspace-plugin
|
||||
```
|
||||
|
||||
重新加载会停止旧的插件、运行它的清理函数、编译当前源码,然后重新启动它。重新加载失败后插件会保持失败状态,并在 `paseo plugin ls` 中报告错误;修复源码后再次重新加载即可。
|
||||
|
||||
打开一个工作区,在 macOS 上按 **⌘K**,在 Windows 和 Linux 上按 **Ctrl+K**,然后选择 **Open workspace overview**。面板会作为工作区标签页打开。
|
||||
|
||||
## 安装已发布的插件 {#install-a-published-plugin}
|
||||
|
||||
在 daemon 主机上用 npm 安装一个受信任的 npm 插件:
|
||||
|
||||
```bash
|
||||
paseo plugin install npm:@acme/paseo-review@1.2.0
|
||||
```
|
||||
|
||||
或者把同样的标识符粘贴到 **设置 → 插件 → 插件来源**,然后选择 **安装插件**。安装后的行使用 manifest 中的 ID,并提供与目录插件相同的启用开关以及 Logs、Reload 和 Remove 操作。获取插件时需要 npm;之后的加载和重新加载都使用已安装的文件。
|
||||
|
||||
标识符语法见[插件来源](/docs/plugins/reference#plugin-sources)。要分享你自己的插件,请按照[发布插件](/docs/plugins/publishing)操作。
|
||||
|
||||
发布在 Git 仓库中的插件通过显式的 Git 简写或 URL 安装:
|
||||
|
||||
```bash
|
||||
paseo plugin add git:owner/repository
|
||||
paseo plugin add https://gitlab.com/group/repository.git
|
||||
paseo plugin add git:owner/monorepo:plugins/workspace
|
||||
paseo plugin add git:owner/repository --ref main
|
||||
```
|
||||
|
||||
当插件位于仓库根目录之下时,追加 `:relative/path`。`--ref` 只在首次安装时选择 Git 内容;省略时安装默认的 HEAD。确切的语法见[来源参考](/docs/plugins/reference#plugin-sources)。
|
||||
|
||||
```bash
|
||||
paseo plugin ls
|
||||
paseo plugin update workspace-plugin
|
||||
paseo plugin update --all
|
||||
```
|
||||
|
||||
`ls` 报告来源标识和当前已安装的版本,不会联系远程。`update` 会展示一个拟更新的 npm 或 Git 版本,并请求你批准。npm 提供更新的 latest 发布版本;Git 提供当前默认的 HEAD,与首次安装时的选择器无关。使用 `--check` 只预览,使用 `--yes` 则无需提示直接批准。显式目标和批量更新见 [CLI 参考](/docs/plugins/reference#cli-reference)。
|
||||
|
||||
## 读取后端日志 {#read-backend-logs}
|
||||
|
||||
daemon 端的处理函数和清理函数可以使用普通的 Node 日志:
|
||||
|
||||
```ts
|
||||
console.log("Refreshing issues");
|
||||
console.error("Issue refresh failed", error);
|
||||
```
|
||||
|
||||
在 **设置 → 插件** 下打开插件的三点菜单并选择 **Logs**,即可读取最近的输出;也可以使用 CLI:
|
||||
|
||||
```bash
|
||||
paseo plugin logs workspace-plugin
|
||||
paseo plugin logs workspace-plugin --json
|
||||
```
|
||||
|
||||
日志尾部包含 `[paseo]` 的加载、就绪、停止中和已停止条目,以及编译和加载失败信息,并且在重新加载和崩溃后依然保留。客户端的输出留在应用中。保留策略以及哪些内容不应记录,见[调试后端输出](/docs/plugins/reference#debug-backend-output)。
|
||||
|
||||
## 下一步 {#next}
|
||||
|
||||
- [发布插件](/docs/plugins/publishing):通过 npm、私有注册表或 Git 分享。
|
||||
- [构建 Provider 插件](/docs/plugins/providers):直接或通过 ACP 接入 Agent,渲染由 Provider 拥有的时间线条目,测试并发布它。
|
||||
- [插件参考](/docs/plugins/reference):每种贡献类型及其字段、运行时模块、宿主环境和 CLI。
|
||||
- [把插件迁移到运行时入口](/docs/plugins/migration):逐步迁移基于单一 `index.ts` 入口编写的插件。
|
||||
- [TypeScript SDK](/docs/sdk):在客户端和服务端代码中以 `paseo` 提供的工作区、Agent、Provider 和配置 API。
|
||||
@@ -73,7 +73,9 @@ Use this table as the complete registration checklist.
|
||||
| `plugin.addSurface(id, Component)` in the old root entry | `client.addScreen({ id, title, Component })` in `index.client.tsx` |
|
||||
| `plugin.addSidebarItem({ id, title, icon, surface })` in the old root entry | `client.addSidebarHeaderItem({ id, title, Component })` in `index.client.tsx`; see [sidebar items](reference#sidebar-items) |
|
||||
| New sidebar footer contribution | `client.addSidebarFooterItem({ id, title, Component })` in `index.client.tsx` |
|
||||
| New file panel contribution | `client.addFilePanel(panel)` in `index.client.tsx` |
|
||||
| `plugin.addWorkspacePanel(panel)` in the old root entry | `client.addWorkspacePanel(panel)` in `index.client.tsx` |
|
||||
| Agent panel with `context: "agent"` | `client.addAgentPanel(panel)` in `index.client.tsx`; omit `context` |
|
||||
| `plugin.addCommandCenterItem(item)` in the old root entry | `client.addCommandCenterItem(item)` in `index.client.tsx` |
|
||||
| `plugin.addClientSlashCommand(command)` in the old root entry | `client.addSlashCommand(command)` in `index.client.tsx` |
|
||||
| `plugin.addClientSide(fn)` in the old root entry | Delete the wrapper and move the body of `fn` into the default client entry function |
|
||||
@@ -95,6 +97,22 @@ Import `PluginClientContext` from `@getpaseo/plugin/client` and `PluginServerCon
|
||||
`@getpaseo/plugin/server`. Remove imports of the old context type. Client registrations return idempotent removal functions, except header buttons and composer pills, which return `{ update, remove }` handles. Preserve any remover the plugin calls before teardown; Paseo removes outstanding
|
||||
registrations after the entry cleanup runs.
|
||||
|
||||
### Panel props in 0.13
|
||||
|
||||
All panel kinds share [params, revision, and persisted tab state](./reference.md#workspace-panels).
|
||||
|
||||
| Previous API | Replacement |
|
||||
| ----------------------------------------------------------------------- | ------------------------------------------------ |
|
||||
| Workspace/agent `context` prop | Select the kind at registration. |
|
||||
| Agent `agentId` prop | `params.agentId` |
|
||||
| `addWorkspacePanel({ context: "workspace", ... })` | `addWorkspacePanel({ ... })` |
|
||||
| `addWorkspacePanel({ context: "agent", ... })` | `addAgentPanel({ ... })` |
|
||||
| Beta file `location.lineStart`, `location.lineEnd`, `location.revision` | `params.lineStart`, `params.lineEnd`, `revision` |
|
||||
| Beta `PluginFileTab`, `PluginFileLocation` | `PluginTab`, `PluginFilePanelParams` |
|
||||
|
||||
The 0.12 aliases remain available until removal after 2027-04-23. The beta file API changes
|
||||
outright. Declare `requirements.paseo` as `>=0.13.0` when adopting the new API.
|
||||
|
||||
### Composer pills
|
||||
|
||||
Update the plugin project's `@getpaseo/plugin` dependency, then run `npm run typecheck`.
|
||||
@@ -133,21 +151,22 @@ show chevrons. See [buttons](./reference.md#button-descriptor) for menus, popove
|
||||
|
||||
Move hooks (`usePaseo`, `useRpc`, `useSettings`, `useAgent`, `useWorkspace`) and client contribution
|
||||
types from `@getpaseo/plugin` to `@getpaseo/plugin/client`. Move `Icon` to
|
||||
`@getpaseo/plugin/client/react-native`. Import server contexts and lifecycle contracts from
|
||||
`@getpaseo/plugin/client/ui`. Import server contexts and lifecycle contracts from
|
||||
`@getpaseo/plugin/server`. Shared helpers (`defineRpc`, `defineSettings`, `defineAttachmentSource`),
|
||||
schemas, and plain data types stay on the root. These rules include type imports. See
|
||||
[Runtime modules](reference#runtime-modules) for the complete contract.
|
||||
|
||||
Move the remaining SDK subpaths under their runtime owner:
|
||||
|
||||
| Old entry | 0.8 entry |
|
||||
| ------------------------------- | -------------------------------------- |
|
||||
| `@getpaseo/plugin/react-native` | `@getpaseo/plugin/client/react-native` |
|
||||
| `@getpaseo/plugin/ui` | `@getpaseo/plugin/client/ui` |
|
||||
| `@getpaseo/plugin/provider` | `@getpaseo/plugin/server/provider` |
|
||||
| `@getpaseo/plugin/acp` | `@getpaseo/plugin/server/acp` |
|
||||
| Old entry | Current entry |
|
||||
| -------------------------------------- | ---------------------------------- |
|
||||
| `@getpaseo/plugin/react-native` | `@getpaseo/plugin/client/ui` |
|
||||
| `@getpaseo/plugin/client/react-native` | `@getpaseo/plugin/client/ui` |
|
||||
| `@getpaseo/plugin/ui` | `@getpaseo/plugin/client/ui` |
|
||||
| `@getpaseo/plugin/provider` | `@getpaseo/plugin/server/provider` |
|
||||
| `@getpaseo/plugin/acp` | `@getpaseo/plugin/server/acp` |
|
||||
|
||||
The old entries and the pre-0.8 `@paseo/plugin` scope are removed. `/client/host` is private to
|
||||
The pre-0.8 entries and the old `@paseo/plugin` scope are removed. `/client/host` is private to
|
||||
Paseo's app integration and is never a plugin-author import.
|
||||
|
||||
The client entry imports only `client/`, `shared/`, and client-safe packages. The server entry imports
|
||||
@@ -155,6 +174,16 @@ only `server/`, `shared/`, and server-safe packages. A `node:` import in the cli
|
||||
reachable from it is a compile error. Never import a component into the server entry merely to wire
|
||||
its registration; that registration belongs in the client entry.
|
||||
|
||||
### UI imports in v0.13
|
||||
|
||||
Use `/client/ui` for all Paseo UI components and UI hooks. The deprecated
|
||||
`/client/react-native` entry retains `useRevealedText(text, phase): string` for 0.12 plugins
|
||||
until removal after 2027-04-20. The hook is deprecated and is not exported from `/client/ui`.
|
||||
|
||||
For paced timeline text, replace `useRevealedText(text, phase)` and the surrounding `Text` with
|
||||
`<StreamedText text={text} phase={phase} style={style} />` from `/client/ui`.
|
||||
Use `SettingsSection` for plugin settings headings; `SettingsGroup` is deprecated.
|
||||
|
||||
## 5. Recognize half-migration errors
|
||||
|
||||
| Compiler or load error | Meaning and fix |
|
||||
|
||||
@@ -0,0 +1,302 @@
|
||||
---
|
||||
title: 把插件迁移到运行时入口
|
||||
description: 把混合的插件入口机械地迁移为显式的客户端入口和服务端入口。
|
||||
nav: 迁移
|
||||
---
|
||||
|
||||
# 把插件迁移到运行时入口 {#migrate-a-plugin-to-runtime-entries}
|
||||
|
||||
本指南用于把为 Paseo 0.7 或更早版本编写的插件,更新为 0.8 引入的当前客户端/服务端入口格式。
|
||||
|
||||
把本页交给一个编程 Agent,并以插件目录作为它的工作目录。按顺序执行各个步骤。不要保留兼容入口。
|
||||
|
||||
## 1. 对现有代码分类 {#1-classify-the-existing-code}
|
||||
|
||||
从旧的结构开始:
|
||||
|
||||
```text
|
||||
my-plugin/
|
||||
paseo-plugin.json
|
||||
package.json
|
||||
tsconfig.json
|
||||
index.ts
|
||||
greeting.client.tsx
|
||||
greeting.server.ts
|
||||
greeting.shared.ts
|
||||
```
|
||||
|
||||
完成后的结构是:
|
||||
|
||||
```text
|
||||
my-plugin/
|
||||
paseo-plugin.json
|
||||
package.json
|
||||
tsconfig.json
|
||||
index.client.tsx
|
||||
index.server.ts
|
||||
client/greeting.tsx
|
||||
server/greeting.ts
|
||||
shared/greeting.ts
|
||||
```
|
||||
|
||||
只创建插件需要的入口,至少需要一个。组件和客户端回调需要客户端入口。RPC 处理函数和 Node API 需要服务端入口。
|
||||
|
||||
## 2. 重命名文件和目录 {#2-rename-files-and-directories}
|
||||
|
||||
严格按以下规则操作:
|
||||
|
||||
1. 用 `index.client.tsx`、`index.server.ts` 或两者替换混合的根入口。
|
||||
2. 把每个 `name.client.ts` 或 `name.client.tsx` 移动到 `client/name.ts` 或 `client/name.tsx`。
|
||||
3. 把每个 `name.server.ts` 或 `name.server.tsx` 移动到 `server/name.ts` 或 `server/name.tsx`。
|
||||
4. 把每个 `name.shared.ts` 或 `name.shared.tsx` 移动到 `shared/name.ts` 或 `shared/name.tsx`。
|
||||
5. 在对应的运行时目录下保留嵌套的功能目录。
|
||||
6. 每次移动后更新相对导入路径。
|
||||
7. 把 `paseo-plugin.json`、`package.json` 和 `tsconfig.json` 保留在根目录。
|
||||
8. 删除旧的根入口。Paseo 不会加载它。
|
||||
|
||||
这些目录就是编译器的边界。`client/` 下的文件只编译进应用的包,`server/` 下的文件只编译进 daemon 的包,`shared/` 则编译进两者。`*.client.tsx` 之类的文件名后缀不再有任何含义,留在插件根目录的代码模块会导致编译错误。
|
||||
|
||||
## 3. 迁移每一处注册 {#3-move-every-registration}
|
||||
|
||||
把下表作为完整的注册检查清单。
|
||||
|
||||
| 旧的注册及位置 | 新的注册及位置 |
|
||||
| --------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
|
||||
| 旧根入口中的 `plugin.handle(contract, handler)` | `index.server.ts` 中的 `server.handle(contract, handler)` |
|
||||
| 旧根入口中的 `plugin.addSurface(id, Component)` | `index.client.tsx` 中的 `client.addScreen({ id, title, Component })` |
|
||||
| 旧根入口中的 `plugin.addSidebarItem({ id, title, icon, surface })` | `index.client.tsx` 中的 `client.addSidebarHeaderItem({ id, title, Component })`;见[侧边栏项](reference#sidebar-items) |
|
||||
| 新的侧边栏底部贡献 | `index.client.tsx` 中的 `client.addSidebarFooterItem({ id, title, Component })` |
|
||||
| 新的文件面板贡献 | `index.client.tsx` 中的 `client.addFilePanel(panel)` |
|
||||
| 旧根入口中的 `plugin.addWorkspacePanel(panel)` | `index.client.tsx` 中的 `client.addWorkspacePanel(panel)` |
|
||||
| 带有 `context: "agent"` 的 Agent 面板 | `index.client.tsx` 中的 `client.addAgentPanel(panel)`;省略 `context` |
|
||||
| 旧根入口中的 `plugin.addCommandCenterItem(item)` | `index.client.tsx` 中的 `client.addCommandCenterItem(item)` |
|
||||
| 旧根入口中的 `plugin.addClientSlashCommand(command)` | `index.client.tsx` 中的 `client.addSlashCommand(command)` |
|
||||
| 旧根入口中的 `plugin.addClientSide(fn)` | 删除这层包装,把 `fn` 的函数体移入默认的客户端入口函数 |
|
||||
| 旧客户端回调中的 `client.addComposerPill(pill)` | `index.client.tsx` 或被导入的 `client/` 函数中的 `client.addComposerPill(pill)` |
|
||||
| 新的标题栏贡献 | `client.addHeaderButton({ id, workspaceId, button })` |
|
||||
| 旧根入口中的 `plugin.addAttachmentSource(source)` | `index.client.tsx` 中的 `client.addAttachmentSource(source)` |
|
||||
| 新的设置界面贡献 | `index.client.tsx` 中的 `client.addSettingsScreen(screen)`;见[设置界面](reference#settings-screens) |
|
||||
| 旧根入口中的 `plugin.addTheme(theme)` | `index.client.tsx` 中的 `client.addTheme(theme)` |
|
||||
| 旧根入口中的 `plugin.addTimelineTransformer(transformer)` | `index.client.tsx` 中的 `client.addTimelineTransformer(transformer)` |
|
||||
| 旧根入口中的 `plugin.addTimelineRenderer(renderer)` | `index.client.tsx` 中的 `client.addTimelineRenderer(renderer)` |
|
||||
| shared 文件中的 `import { defineRpc, defineAttachmentSource } from "@getpaseo/plugin/server"` | `import { defineRpc, defineAttachmentSource } from "@getpaseo/plugin"` |
|
||||
| 处理函数参数类型 `ZodOutput<typeof contract.input>` | 来自 `@getpaseo/plugin` 的 `RpcInput<typeof contract>`;返回类型使用 `RpcOutput` |
|
||||
|
||||
用原 `addSidebarItem` 项的 `id` 注册替代它的界面,这样已保存的链接仍然有效。`/plugin/<plugin>/sidebar/<id>` 链接以及更早的 `/plugin/<plugin>/<id>` 链接,会打开具有该 `id` 的 `addSidebarItem`,否则打开具有该 `id` 的界面。
|
||||
|
||||
从 `@getpaseo/plugin/client` 导入 `PluginClientContext`,从 `@getpaseo/plugin/server` 导入 `PluginServerContext`。删除对旧 context 类型的导入。客户端注册返回幂等的移除函数,但标题栏按钮和 Composer 标签除外,它们返回 `{ update, remove }` 句柄。保留插件在销毁之前调用的所有移除函数;入口的清理函数运行后,Paseo 会移除尚未移除的注册。
|
||||
|
||||
### 0.13 的面板属性 {#panel-props-in-013}
|
||||
|
||||
所有面板类型共享 [params、revision 和持久化的标签页状态](./reference.md#workspace-panels)。
|
||||
|
||||
| 旧 API | 替代方式 |
|
||||
| ----------------------------------------------------------------------------- | ------------------------------------------------ |
|
||||
| 工作区/Agent 面板的 `context` 属性 | 在注册时选择面板类型。 |
|
||||
| Agent 面板的 `agentId` 属性 | `params.agentId` |
|
||||
| `addWorkspacePanel({ context: "workspace", ... })` | `addWorkspacePanel({ ... })` |
|
||||
| `addWorkspacePanel({ context: "agent", ... })` | `addAgentPanel({ ... })` |
|
||||
| Beta 文件面板的 `location.lineStart`、`location.lineEnd`、`location.revision` | `params.lineStart`、`params.lineEnd`、`revision` |
|
||||
| Beta 的 `PluginFileTab`、`PluginFileLocation` | `PluginTab`、`PluginFilePanelParams` |
|
||||
|
||||
0.12 的别名会保留到 2027-04-23 之后移除。Beta 文件 API 直接替换。采用新 API 时,将
|
||||
`requirements.paseo` 声明为 `>=0.13.0`。
|
||||
|
||||
### Composer 标签 {#composer-pills}
|
||||
|
||||
更新插件项目的 `@getpaseo/plugin` 依赖,然后运行 `npm run typecheck`。旧的贡献缺少必需的 `button` 字段,`PluginComposerPillProps` 不再导出,并且把新的注册当作函数调用会产生 TypeScript 错误。固定在旧 SDK 上的项目仍然按旧的契约检查;安装或重新加载插件并不会运行 TypeScript 的类型检查器。
|
||||
|
||||
把标签的 `Component` 替换为 `button.icon` 和 `button.label`,把 `title` 移入 `button`,把 `onPress` 移入 `button.behavior`。清理方式从调用返回的函数改为调用它的 `.remove()` 方法。
|
||||
|
||||
```tsx
|
||||
const pill = client.addComposerPill({
|
||||
id: "review",
|
||||
workspaceId,
|
||||
agentId,
|
||||
button: {
|
||||
title: "Open review",
|
||||
icon: "Scan",
|
||||
label: "Review",
|
||||
behavior: { kind: "action", onPress: openReview },
|
||||
},
|
||||
});
|
||||
|
||||
pill.update({ label: "Review · 3", visible: true });
|
||||
// Client entry cleanup:
|
||||
pill.remove();
|
||||
```
|
||||
|
||||
把原组件中的动态文本改为由你的模型或 SDK 订阅发出更新。自定义图标组件仍然可以使用 hook。Composer 标签总是显示图标和文字,并且从不显示展开箭头。菜单、弹出框和可见性见[按钮](./reference.md#button-descriptor)。
|
||||
|
||||
## 4. 拆分导入 {#4-separate-imports}
|
||||
|
||||
把 hook(`usePaseo`、`useRpc`、`useSettings`、`useAgent`、`useWorkspace`)和客户端贡献类型从 `@getpaseo/plugin` 移到 `@getpaseo/plugin/client`。把 `Icon` 移到 `@getpaseo/plugin/client/ui`。从 `@getpaseo/plugin/server` 导入服务端 context 和生命周期契约。共享的辅助函数(`defineRpc`、`defineSettings`、`defineAttachmentSource`)、schema 和普通数据类型保留在根入口。这些规则同样适用于类型导入。完整的契约见[运行时模块](reference#runtime-modules)。
|
||||
|
||||
把其余的 SDK 子路径移到它们所属的运行时下:
|
||||
|
||||
| 旧入口 | 当前入口 |
|
||||
| -------------------------------------- | ---------------------------------- |
|
||||
| `@getpaseo/plugin/react-native` | `@getpaseo/plugin/client/ui` |
|
||||
| `@getpaseo/plugin/client/react-native` | `@getpaseo/plugin/client/ui` |
|
||||
| `@getpaseo/plugin/ui` | `@getpaseo/plugin/client/ui` |
|
||||
| `@getpaseo/plugin/provider` | `@getpaseo/plugin/server/provider` |
|
||||
| `@getpaseo/plugin/acp` | `@getpaseo/plugin/server/acp` |
|
||||
|
||||
0.8 之前的入口和旧的 `@paseo/plugin` scope 已被移除。`/client/host` 是 Paseo 应用集成的私有模块,插件作者从不导入它。
|
||||
|
||||
客户端入口只导入 `client/`、`shared/` 和可在客户端安全使用的包。服务端入口只导入 `server/`、`shared/` 和可在服务端安全使用的包。在客户端入口或从它可达的任何代码中导入 `node:` 模块都会导致编译错误。不要仅仅为了接线注册而把组件导入服务端入口;那项注册属于客户端入口。
|
||||
|
||||
### v0.13 中的 UI 导入 {#ui-imports-in-v013}
|
||||
|
||||
所有 Paseo UI 组件和 UI hook 都从 `/client/ui` 导入。之前的入口作为未写入文档的兼容别名保留到 2027-04-20。
|
||||
|
||||
对于逐步显示的时间线文本,把 `useRevealedText(text, phase)` 及其外层的 `Text` 换成 `/client/ui` 中的 `<StreamedText text={text} phase={phase} style={style} />`。插件设置的标题使用 `SettingsSection`;`SettingsGroup` 已弃用。
|
||||
|
||||
## 5. 识别只迁移了一半的错误 {#5-recognize-half-migration-errors}
|
||||
|
||||
| 编译器或加载错误 | 含义与修复方法 |
|
||||
| -------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
|
||||
| `This plugin has no requirements.paseo` | 完成迁移,并按第 7 步声明版本范围。 |
|
||||
| `This plugin was made for an older version of Paseo` | 目录中仍然只有旧的根入口。创建一个运行时入口,迁移注册,然后删除旧文件。 |
|
||||
| `Plugin entry points are missing: expected index.client.ts or index.client.tsx and/or index.server.ts or index.server.tsx` | 不存在受支持的入口。至少添加一个文件名完全匹配的入口。 |
|
||||
| `server-only module cannot be imported into the plugin client bundle: <file>` | 某个客户端导入触及了 `server/`。把这个调用移到 RPC 之后,并从 `shared/` 导入它的契约。 |
|
||||
| `client-only module cannot be imported into the plugin server bundle: <file>` | 某个服务端导入触及了 `client/`。把那项注册及其导入移到客户端入口。 |
|
||||
| `Plugin modules belong in client/, server/, or shared/: <file>` | 某个代码模块仍在插件根目录。把它移到对应的目录并修复它的导入。 |
|
||||
| `Node module cannot be imported into the plugin client bundle: node:<name> imported by <file>` | 客户端代码导入了 Node API。把该操作移到 `server/`,在 `shared/` 中暴露一个 RPC,并从客户端调用它。 |
|
||||
| TypeScript 报告 `PluginContext`、`addClientSide` 或 `addClientSlashCommand` 不存在 | 按照上面的表格替换旧的 context 类型和注册。 |
|
||||
|
||||
## 6. 完整示例:`plugin-examples/local-plugin` {#6-worked-example-plugin-exampleslocal-plugin}
|
||||
|
||||
只有入口文件和导入路径发生变化。组件和处理函数的函数体原样移动,无需编辑。
|
||||
|
||||
迁移前:
|
||||
|
||||
```text
|
||||
local-plugin/
|
||||
index.ts
|
||||
main.client.tsx
|
||||
increment.server.ts
|
||||
increment.shared.ts
|
||||
```
|
||||
|
||||
```ts
|
||||
// index.ts
|
||||
import type { PluginContext } from "@getpaseo/plugin";
|
||||
import { contributeClient, ExamplePanel } from "./main.client";
|
||||
import { increment } from "./increment.server";
|
||||
import { incrementRpc } from "./increment.shared";
|
||||
|
||||
export default function contribute(plugin: PluginContext) {
|
||||
plugin.handle(incrementRpc, increment);
|
||||
plugin.addWorkspacePanel({
|
||||
id: "counter",
|
||||
title: "Plugin counter",
|
||||
icon: "Blocks",
|
||||
context: "workspace",
|
||||
locations: ["workspace", "explorer"],
|
||||
Component: ExamplePanel,
|
||||
});
|
||||
plugin.addCommandCenterItem({
|
||||
id: "open-counter",
|
||||
title: "Open plugin counter",
|
||||
icon: "Blocks",
|
||||
context: "workspace",
|
||||
onSelect({ openPanel }) {
|
||||
openPanel("counter");
|
||||
},
|
||||
});
|
||||
plugin.addClientSide(contributeClient);
|
||||
return () => {};
|
||||
}
|
||||
```
|
||||
|
||||
迁移后:
|
||||
|
||||
```text
|
||||
local-plugin/
|
||||
index.client.tsx
|
||||
index.server.ts
|
||||
client/main.tsx # was main.client.tsx
|
||||
server/increment.ts # was increment.server.ts
|
||||
shared/increment.ts # was increment.shared.ts
|
||||
```
|
||||
|
||||
```tsx
|
||||
// index.client.tsx
|
||||
import type { PluginClientContext } from "@getpaseo/plugin/client";
|
||||
import { contributeClient, ExamplePanel } from "./client/main";
|
||||
|
||||
export default function contribute(client: PluginClientContext) {
|
||||
client.addWorkspacePanel({
|
||||
id: "counter",
|
||||
title: "Plugin counter",
|
||||
icon: "Blocks",
|
||||
context: "workspace",
|
||||
locations: ["workspace", "explorer"],
|
||||
Component: ExamplePanel,
|
||||
});
|
||||
client.addCommandCenterItem({
|
||||
id: "open-counter",
|
||||
title: "Open plugin counter",
|
||||
icon: "Blocks",
|
||||
context: "workspace",
|
||||
onSelect({ openPanel }) {
|
||||
openPanel("counter");
|
||||
},
|
||||
});
|
||||
return contributeClient(client);
|
||||
}
|
||||
```
|
||||
|
||||
```ts
|
||||
// index.server.ts
|
||||
import type { PluginServerContext } from "@getpaseo/plugin/server";
|
||||
import { increment } from "./server/increment";
|
||||
import { incrementRpc } from "./shared/increment";
|
||||
|
||||
export default function contribute(server: PluginServerContext) {
|
||||
server.handle(incrementRpc, increment);
|
||||
return () => {};
|
||||
}
|
||||
```
|
||||
|
||||
被移动文件中的导入路径变化:
|
||||
|
||||
```diff
|
||||
// client/main.tsx
|
||||
-import { incrementRpc } from "./increment.shared";
|
||||
+import { incrementRpc } from "../shared/increment";
|
||||
|
||||
// server/increment.ts
|
||||
-import { incrementRpc } from "./increment.shared";
|
||||
+import { incrementRpc } from "../shared/increment";
|
||||
```
|
||||
|
||||
`contributeClient` 原本就接收 `PluginClientContext` 并返回清理函数,因此客户端入口直接调用它并返回它的清理函数。如果插件的 `addClientSide` 回调还注册了标签或订阅,请保留那些代码;只去掉外层包装。
|
||||
|
||||
## 7. 声明 Paseo 版本要求 {#7-declare-the-paseo-requirement}
|
||||
|
||||
迁移完入口和导入后,在 `paseo-plugin.json` 中添加最低运行时版本:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "my-plugin",
|
||||
"requirements": { "paseo": ">=0.8.0" }
|
||||
}
|
||||
```
|
||||
|
||||
保留你现有的 ID 和构建命令。缺少 `requirements.paseo` 表示 `<0.8.0`,因此即使文件已经移动,Paseo 0.8 及更高版本也会拒绝该插件。仅仅添加这个字段并不会迁移代码。请把本地的 `@getpaseo/plugin` 开发依赖更新到你的目标版本,并在类型检查之前重新安装依赖。
|
||||
|
||||
版本范围和预发布版本的语义见[版本要求](reference#requirements)。
|
||||
|
||||
## 8. 验证迁移 {#8-verify-the-migration}
|
||||
|
||||
运行:
|
||||
|
||||
```bash
|
||||
npm run typecheck
|
||||
paseo plugin reload <plugin-id>
|
||||
paseo plugin ls
|
||||
```
|
||||
|
||||
确认状态为 `running` 且没有错误。试用每一项贡献。对于带 RPC 的插件,调用客户端操作并验证服务端结果。对于只有客户端的插件,确认贡献在没有服务端进程的情况下也能加载。对任何保存下来的注册移除函数调用两次,验证第二次调用不产生任何效果。
|
||||
@@ -306,6 +306,52 @@ turn or finish as a side effect with a `completed` prompt result.
|
||||
Advertise only capabilities you implement. If steering is unsupported, omit `prompt.steer`; Paseo
|
||||
can replace the active turn instead.
|
||||
|
||||
## Permit idle runtime cleanup
|
||||
|
||||
Paseo can release an idle resumable session after one hour when its daemon baseline allows cleanup.
|
||||
Add a provider-owned veto by advertising `session.can_stop_runtime` on the connection and the
|
||||
session's `session.opened` capabilities. Without this capability, the same baseline and one-hour
|
||||
idle period apply, and the plugin receives no safety request.
|
||||
|
||||
Handle the safety query in your existing `send(input)` handler. It addresses the provider's own
|
||||
session ID, including when one connection serves several sessions:
|
||||
|
||||
```ts
|
||||
case "session.can_stop_runtime": {
|
||||
try {
|
||||
const session = sessions.get(input.sessionId);
|
||||
if (!session) throw new Error("Session not found");
|
||||
emit({
|
||||
type: "session.can_stop_runtime",
|
||||
requestId: input.requestId,
|
||||
sessionId: input.sessionId,
|
||||
canStop: await session.canStopRuntime(),
|
||||
});
|
||||
} catch (error) {
|
||||
emit({
|
||||
type: "request.failed",
|
||||
requestId: input.requestId,
|
||||
error: { message: error instanceof Error ? error.message : String(error) },
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
`canStopRuntime(): Promise<boolean>` belongs to your provider's session implementation. Return
|
||||
`true` only when releasing the runtime loses no background shells, monitors, native children,
|
||||
queued completion reports, or pending autonomous replies. This query must not stop or mutate the
|
||||
session. Return `false` while work remains. A rejected query, invalid response, or timeout also
|
||||
keeps the runtime alive. Never respond with `request.completed` for this query.
|
||||
|
||||
Paseo always checks foreground work, permissions, queued deliveries, lifecycle operations,
|
||||
working managed/native descendants, and outstanding local or remote finish subscriptions for the
|
||||
caller. Subscription protection lasts through registration, recovery and notification dispatch
|
||||
acknowledgment. A positive provider response never overrides these blockers. Paseo rechecks
|
||||
activity before the existing `session.close` operation. After protected work finishes, a fresh
|
||||
one-hour idle period starts. The saved provider persistence must resume the same conversation.
|
||||
The host setting `agents.idleRuntimeCleanup: false` disables collection for every provider.
|
||||
|
||||
## Publish timeline items
|
||||
|
||||
Publish complete snapshots through `timeline.item`. Reuse an item `id` when updating streamed text,
|
||||
|
||||
@@ -0,0 +1,416 @@
|
||||
---
|
||||
title: 构建 Provider 插件
|
||||
description: 直接把编程 Agent 接入 Paseo,或适配一个 ACP Agent。
|
||||
nav: Provider 插件
|
||||
---
|
||||
|
||||
# 构建 Provider 插件 {#build-a-provider-plugin}
|
||||
|
||||
如果你之前没有构建过 Paseo 插件,请从[插件快速上手](/docs/plugins)开始。
|
||||
|
||||
Provider 插件把一个编程 Agent 接入 Paseo,而无需把它加入 Paseo 核心。在 npm 或 Git 仓库中[发布插件](/docs/plugins/publishing),用户就可以用 `paseo plugin install` 安装它。
|
||||
|
||||
选择一种实现方式:
|
||||
|
||||
| Agent | 方式 |
|
||||
| -------------------------------------------------- | ---------------------------------------------------------- |
|
||||
| 已经实现了 ACP | 用 `runAcpProvider()` 注册它,只添加少量针对厂商的转换器。 |
|
||||
| 提供 TypeScript SDK、JSON-RPC API 或自定义进程协议 | 直接实现 `ProviderRegistration`。 |
|
||||
|
||||
示例有:
|
||||
|
||||
- [`provider-direct`](https://github.com/getpaseo/paseo/tree/main/plugin-examples/provider-direct):会话、设置、提示词、持久化、子会话,以及一个由 Provider 拥有的时间线渲染器;
|
||||
- [`provider-acp-transformer`](https://github.com/getpaseo/paseo/tree/main/plugin-examples/provider-acp-transformer):一个 ACP 命令模板,带有一个用 Zod 校验的厂商编辑转换器。加载之前,请把 `example-acp --stdio` 替换为一个已安装的 ACP Agent;
|
||||
- [`inline-thinking`](https://github.com/getpaseo/paseo/tree/main/plugin-examples/inline-thinking):一个只包含渲染器、不实现 Provider 的插件。
|
||||
|
||||
## 注册直接实现的 Provider {#register-a-direct-provider}
|
||||
|
||||
添加一个服务端入口:
|
||||
|
||||
```ts
|
||||
// index.server.ts
|
||||
import type { PluginServerContext } from "@getpaseo/plugin/server";
|
||||
import { createProvider } from "./server/provider";
|
||||
|
||||
export default function contribute(server: PluginServerContext) {
|
||||
server.registerProvider(createProvider());
|
||||
return () => {};
|
||||
}
|
||||
```
|
||||
|
||||
`createProvider()` 返回一个 `ProviderRegistration`:
|
||||
|
||||
```ts
|
||||
import {
|
||||
negotiateProviderCapabilities,
|
||||
type ProviderRegistration,
|
||||
} from "@getpaseo/plugin/server/provider";
|
||||
|
||||
const supported = ["prompt.message"] as const;
|
||||
|
||||
export function createProvider(): ProviderRegistration {
|
||||
return {
|
||||
id: "my-agent",
|
||||
label: "My agent",
|
||||
icon: "icon.svg",
|
||||
async connect(request) {
|
||||
const capabilities = negotiateProviderCapabilities(request.capabilities, supported);
|
||||
return createConnection(capabilities);
|
||||
},
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
连接有三个操作:
|
||||
|
||||
- `send(input)` 接收工作后立即返回。结果以事件的形式发布,而不是返回值。
|
||||
- `onEvent(listener)` 发布会话状态和完整的时间线快照。
|
||||
- `close()` 停止所有会话,并释放进程、订阅和待处理的工作。
|
||||
|
||||
把原生 SDK、进程和流保留在连接的实现内部。在发布之前,把它的输出转换为 `ProviderEvent` 对象。
|
||||
|
||||
## 启动 Provider CLI {#launch-the-provider-cli}
|
||||
|
||||
对于进程传输和 CLI 探测,请使用 `@getpaseo/plugin/server` 中的 `spawnProcess` 和 `execCommand`。两者都能处理 Windows 的 `.cmd`/`.bat` 启动器,并为 shell 参数加引号。原生的 Node `spawn` 和 `execFile` 在 Windows 上会拒绝这些启动器。
|
||||
|
||||
daemon 在 `request.launch` 中提供解析后的命令、参数前缀和完整的环境变量。请显式传入这些环境变量;这些辅助函数不会应用 daemon 的环境策略。把进程生命周期和协议截止时间保留在你的 Provider 内部。正常关闭时结束 stdin;强制停止时使用 `terminateProcess(child)`。它在 Windows 上会结束整个进程树,包括命令脚本启动器背后的 CLI。为最后等待流关闭的过程设定上限。
|
||||
|
||||
```ts
|
||||
import { spawnProcess, execCommand } from "@getpaseo/plugin/server";
|
||||
|
||||
const child = spawnProcess(launch.command, [...launch.args, "serve"], {
|
||||
env: launch.env,
|
||||
cwd,
|
||||
stdio: "pipe",
|
||||
});
|
||||
const { stdout } = await execCommand(launch.command, [...launch.args, "--version"], {
|
||||
env: launch.env,
|
||||
timeout: 5000,
|
||||
});
|
||||
```
|
||||
|
||||
## 返回模型、模式和思考选项 {#return-models-modes-and-thinking-options}
|
||||
|
||||
Paseo 在创建会话之前会请求目录。返回 Agent 表单需要的选项:
|
||||
|
||||
```ts
|
||||
if (input.type === "catalog") {
|
||||
emit({
|
||||
type: "catalog",
|
||||
requestId: input.requestId,
|
||||
catalog: {
|
||||
models: [
|
||||
{ id: "agent-large", label: "Agent Large" },
|
||||
{ id: "agent-small", label: "Agent Small" },
|
||||
],
|
||||
modes: [
|
||||
{ id: "build", label: "Build" },
|
||||
{ id: "plan", label: "Plan" },
|
||||
],
|
||||
thinkingOptions: [
|
||||
{ id: "standard", label: "Standard" },
|
||||
{ id: "extended", label: "Extended" },
|
||||
],
|
||||
defaultModel: "agent-large",
|
||||
defaultMode: "build",
|
||||
defaultThinkingOption: "standard",
|
||||
},
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
对于 Agent 不提供的类别,返回空数组。选中的 `model`、`mode` 和 `thinkingOption` 会出现在会话配置中。
|
||||
|
||||
目录描述的是会话存在之前可用的选项。会话专属的控件之后通过 `session.config` 提供。
|
||||
|
||||
## 打开会话 {#open-a-session}
|
||||
|
||||
`session.open` 包含完整的启动配置:工作目录、环境变量、系统提示词、MCP 服务器、工具策略、模型、模式、设置、不透明的 Provider 选项以及持久化偏好。
|
||||
|
||||
对于持久化的、作用于主机的插件设置,请使用 `server.registerSettings()` 返回的句柄。在目录和会话回调中读取它;当 Provider 在内存中保留配置时,订阅它。`read()` 和 `subscribe()` 的契约见[设置](./reference.md#persisted-values)。
|
||||
|
||||
用选中的目录值创建原生会话,然后发布它实际生效的状态:
|
||||
|
||||
```ts
|
||||
const nativeSession = await sdk.createSession({
|
||||
cwd: input.config.cwd,
|
||||
model: input.config.model,
|
||||
mode: input.config.mode,
|
||||
thinking: input.config.thinkingOption,
|
||||
systemPrompt: input.config.systemPrompt,
|
||||
env: input.config.env,
|
||||
mcpServers: input.config.mcpServers,
|
||||
});
|
||||
|
||||
emit({
|
||||
type: "session.opened",
|
||||
requestId: input.requestId,
|
||||
sessionId: input.sessionId,
|
||||
capabilities: ["prompt.message"],
|
||||
restoration: "core",
|
||||
cwd: input.config.cwd,
|
||||
});
|
||||
emit({
|
||||
type: "session.config",
|
||||
sessionId: input.sessionId,
|
||||
config: {
|
||||
model: nativeSession.model,
|
||||
mode: nativeSession.mode,
|
||||
thinkingOption: nativeSession.thinking,
|
||||
models,
|
||||
modes,
|
||||
thinkingOptions,
|
||||
settings: [],
|
||||
},
|
||||
});
|
||||
emit({ type: "session.ready", requestId: input.requestId, sessionId: input.sessionId });
|
||||
```
|
||||
|
||||
`session.config` 包含已确定的值。请发布原生 Agent 经过规范化后实际选中的值,而不是请求的副本。
|
||||
|
||||
## 完成第一段提示词 {#complete-the-first-prompt}
|
||||
|
||||
从消息类提示词开始。声明 `prompt.message`,把内容传给原生 Agent,并发布由此产生的生命周期:
|
||||
|
||||
```ts
|
||||
const turnId = crypto.randomUUID();
|
||||
|
||||
emit({
|
||||
type: "timeline.item",
|
||||
sessionId,
|
||||
item: {
|
||||
type: "user_message",
|
||||
id: `user:${turnId}`,
|
||||
text,
|
||||
clientMessageId: prompt.clientMessageId,
|
||||
},
|
||||
});
|
||||
emit({
|
||||
type: "session.prompt_result",
|
||||
sessionId,
|
||||
clientMessageId: prompt.clientMessageId,
|
||||
result: { type: "turn", turnId },
|
||||
});
|
||||
emit({ type: "session.turn", sessionId, turnId, state: "started" });
|
||||
|
||||
// Publish complete snapshots as native output changes.
|
||||
emit({
|
||||
type: "timeline.item",
|
||||
sessionId,
|
||||
item: { type: "assistant_message", id: `assistant:${turnId}`, text: completeText },
|
||||
});
|
||||
|
||||
emit({ type: "session.turn", sessionId, turnId, state: "completed" });
|
||||
```
|
||||
|
||||
对每个 `clientMessageId` 恰好发布一个 `session.prompt_result`。把 `clientMessageId` 复制到用户的时间线条目上,以便 Paseo 替换它乐观显示的消息。每个已开始的轮次都需要一个终止的 `completed`、`failed` 或 `canceled` 事件。
|
||||
|
||||
到这一步,Provider 已经可用:用户可以选择模型、打开会话、发送消息并看到回复。
|
||||
|
||||
## 添加会话专属的 Composer 控件 {#add-session-specific-composer-controls}
|
||||
|
||||
只有当原生 Agent 有用户在打开会话后需要修改的设置时,才添加 `session.configure`。在 `session.config` 中以开关或选择控件的形式发布它们:
|
||||
|
||||
```ts
|
||||
emit({
|
||||
type: "session.config",
|
||||
sessionId,
|
||||
config: {
|
||||
model: "agent-large",
|
||||
models: [{ id: "agent-large", label: "Agent Large" }],
|
||||
modes: [],
|
||||
thinkingOptions: [],
|
||||
settings: [
|
||||
{
|
||||
type: "toggle",
|
||||
id: "fast",
|
||||
label: "Fast mode",
|
||||
value: nativeSession.fast,
|
||||
},
|
||||
{
|
||||
type: "select",
|
||||
id: "approval",
|
||||
label: "Approvals",
|
||||
value: nativeSession.approval,
|
||||
options: [
|
||||
{ label: "Ask", value: "ask" },
|
||||
{ label: "Auto accept", value: "auto" },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Paseo 会在 Composer 中渲染这些控件。用户的修改会以 `session.configure` 的形式到达;应用它,并在 `request.completed` 之前发布已确定的 `session.config`。Provider 可以在运行时改变可用的控件,包括在模型变化之后。
|
||||
|
||||
`settings` 是面向用户的控件。`providerOptions` 是在创建会话时提供的不透明 JSON,用于 Paseo 不会渲染的 Provider 专属配置。Provider 专属的 token 预算应放在这两处之一;它不是一个专门的提示词字段。
|
||||
|
||||
## 添加持久化与重放 {#add-persistence-and-replay}
|
||||
|
||||
当原生会话可以被重新打开时,声明 `session.persistence`。当 `session.open.persistence` 不存在时,打开一个新的原生会话;存在时,恢复它所指定的原生会话。在 `session.opened` 或 `session.persistence` 中返回一个不透明的持久化值;Paseo 会保存它而不检查其内容。
|
||||
|
||||
当 `history` 为 `"replay"` 时,请在 `session.ready` 之前发布原生会话已有的 `timeline.item` 快照。使用 `history: "skip"` 可以在不重放旧记录的情况下打开会话。
|
||||
|
||||
Paseo 刷新 Agent 的方式是关闭它当前的 Provider 会话,然后用当前的配置和持久化值重新打开。请在 `session.open` 期间重新读取凭据、环境变量、全局配置和 MCP 服务器。没有单独的重新加载操作。
|
||||
|
||||
当 Paseo 可以从持久化值重新打开会话时,使用 `restoration: "core"`。由 Provider 拥有的子会话使用 `restoration: "parent"`;把它作为另一个带有 `parentSessionId` 的 `session.opened` 事件发出,并在恢复父会话时重新创建它。
|
||||
|
||||
## 添加命令和引导 {#add-commands-and-steering}
|
||||
|
||||
所有用户输入都通过 `session.prompt` 到达:
|
||||
|
||||
- `input.type: "message"` 携带文本、图片和附件;
|
||||
- `input.type: "command"` 携带所选的命令及其参数;
|
||||
- `delivery: "steer"` 要求你把输入加入当前正在进行的轮次;
|
||||
- `delivery: "auto"` 允许你开始一个轮次,或者在没有轮次的情况下完成一条命令。
|
||||
|
||||
发布 `session.commands`,向 Composer 添加结构化命令。命令可以开始一个普通轮次,也可以作为副作用完成,并返回一个 `completed` 的提示词结果。
|
||||
|
||||
只声明你实现了的能力。如果不支持引导,请省略 `prompt.steer`;Paseo 可以改为替换当前轮次。
|
||||
|
||||
## 允许清理空闲运行时 {#permit-idle-runtime-cleanup}
|
||||
|
||||
当 daemon 的基础条件允许清理时,Paseo 可以在可恢复的会话空闲一小时后释放其运行时。在连接和会话的 `session.opened` capabilities 中声明 `session.can_stop_runtime`,即可添加 Provider 自己的否决检查。没有此能力时,会话仍使用相同的基础条件和一小时空闲期,插件不会收到安全查询。
|
||||
|
||||
在现有的 `send(input)` 处理函数中处理安全查询。请求使用 Provider 自己的会话 ID,包括同一连接服务多个会话的情况:
|
||||
|
||||
```ts
|
||||
case "session.can_stop_runtime": {
|
||||
try {
|
||||
const session = sessions.get(input.sessionId);
|
||||
if (!session) throw new Error("Session not found");
|
||||
emit({
|
||||
type: "session.can_stop_runtime",
|
||||
requestId: input.requestId,
|
||||
sessionId: input.sessionId,
|
||||
canStop: await session.canStopRuntime(),
|
||||
});
|
||||
} catch (error) {
|
||||
emit({
|
||||
type: "request.failed",
|
||||
requestId: input.requestId,
|
||||
error: { message: error instanceof Error ? error.message : String(error) },
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
`canStopRuntime(): Promise<boolean>` 由你的 Provider 会话实现。只有释放运行时不会丢失后台 shell、监控任务、原生子 Agent、排队的完成报告或待处理的自主回复时,才返回 `true`。此查询不得停止或修改会话。仍有工作时返回 `false`。查询拒绝、响应无效或超时也会保留运行时。不要用 `request.completed` 响应此查询。
|
||||
|
||||
Paseo 始终检查前台工作、权限、排队投递、生命周期操作、工作中的托管或原生后代 Agent,以及调用方尚未完成的本地或远程完成通知订阅。订阅保护覆盖注册、恢复,直到通知投递获确认。Provider 返回 `true` 也不能绕过这些阻止条件。Paseo 会在调用现有的 `session.close` 操作之前重新检查活动。受保护的工作完成后,会重新开始一小时的空闲期。保存的 Provider 持久化数据必须能恢复同一个会话。主机设置 `agents.idleRuntimeCleanup: false` 会禁用所有 Provider 的清理。
|
||||
|
||||
## 发布时间线条目 {#publish-timeline-items}
|
||||
|
||||
通过 `timeline.item` 发布完整的快照。更新流式文本、运行中的工具或待办列表时,复用条目的 `id`。Paseo 会推导出实时增量,并保持正常的时间线。
|
||||
|
||||
内置的条目类型涵盖用户和助手消息、推理、工具、待办事项、错误、通知和上下文压缩。当你的 Provider 有这些类型无法表达的展示方式时,使用插件条目:
|
||||
|
||||
```ts
|
||||
emit({
|
||||
type: "timeline.item",
|
||||
sessionId,
|
||||
item: {
|
||||
type: "plugin",
|
||||
id: "review-42",
|
||||
pluginId: "my-provider-plugin",
|
||||
kind: "review-verdict",
|
||||
version: 1,
|
||||
data: { verdict: "ship", summary: "All checks passed" },
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
在 `index.client.tsx` 中单独注册渲染器:
|
||||
|
||||
```tsx
|
||||
import type { PluginClientContext } from "@getpaseo/plugin/client";
|
||||
import { z } from "zod";
|
||||
import { ReviewVerdict } from "./client/review-verdict";
|
||||
|
||||
const reviewVerdictSchema = z.object({
|
||||
verdict: z.enum(["ship", "hold"]),
|
||||
summary: z.string(),
|
||||
});
|
||||
|
||||
export default function contribute(client: PluginClientContext) {
|
||||
client.addTimelineRenderer({
|
||||
kind: "review-verdict",
|
||||
version: 1,
|
||||
schema: reviewVerdictSchema,
|
||||
Component: ReviewVerdict,
|
||||
});
|
||||
return () => {};
|
||||
}
|
||||
```
|
||||
|
||||
同一个渲染器可以显示由 Provider、客户端时间线转换器或 daemon 时间线追加发出的条目。渲染器并不需要 Provider 实现。
|
||||
|
||||
## 适配 ACP Agent {#adapt-an-acp-agent}
|
||||
|
||||
当 Agent 已经支持 ACP 时,使用 ACP 适配层:
|
||||
|
||||
```ts
|
||||
import type { PluginServerContext } from "@getpaseo/plugin/server";
|
||||
import { runAcpProvider } from "@getpaseo/plugin/server/acp";
|
||||
|
||||
export default function contribute(server: PluginServerContext) {
|
||||
server.registerProvider(
|
||||
runAcpProvider({
|
||||
id: "vendor-agent",
|
||||
label: "Vendor agent",
|
||||
icon: "icon.svg",
|
||||
command: ["vendor-agent", "acp"],
|
||||
}),
|
||||
);
|
||||
return () => {};
|
||||
}
|
||||
```
|
||||
|
||||
适配层负责 ACP 进程、能力映射、会话生命周期、提示词、权限和时间线转换。不要把这些机制复制到插件中。
|
||||
|
||||
只在 ACP 无法描述厂商差异时使用 `transformers`。用 Zod 校验厂商负载,并让格式错误或无关的值保持不变:
|
||||
|
||||
```ts
|
||||
import type { AcpTransformer } from "@getpaseo/plugin/server/acp";
|
||||
import { z } from "zod";
|
||||
|
||||
const editSchema = z.object({
|
||||
path: z.string(),
|
||||
before: z.string(),
|
||||
after: z.string(),
|
||||
});
|
||||
|
||||
export const vendorEdits: AcpTransformer = {
|
||||
toolCall(toolCall) {
|
||||
if (toolCall.name !== "vendor_file_edit") return toolCall;
|
||||
const input = editSchema.safeParse(toolCall.input);
|
||||
if (!input.success) return toolCall;
|
||||
return {
|
||||
...toolCall,
|
||||
kind: "edit",
|
||||
input: {
|
||||
filePath: input.data.path,
|
||||
oldString: input.data.before,
|
||||
newString: input.data.after,
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## 测试与发布 {#test-and-publish}
|
||||
|
||||
用真实的 Agent 测试 Provider,而不只是模拟的数据帧:
|
||||
|
||||
1. 以目录来源安装插件;
|
||||
2. 确认它的 SVG、模型、模式和设置都会显示;
|
||||
3. 创建会话并完成一段真实的提示词;
|
||||
4. 在支持的情况下,试用权限、引导、取消、持久化和重放;
|
||||
5. 在桌面和移动端宽度的布局中渲染每一种自定义时间线条目;
|
||||
6. 在会话处于活动状态时重新加载和移除插件,并确认会话随之终止。
|
||||
|
||||
按照[发布插件](/docs/plugins/publishing)通过 npm 或 Git 分发 Provider。把 Agent 兼容性和版本发布保留在插件项目中。
|
||||
|
||||
确切的运行时和 SVG 规则见[插件参考](/docs/plugins/reference#providers)。
|
||||
@@ -49,6 +49,37 @@ overview, including development or setup examples. Link to your README for those
|
||||
Also supply a one-sentence description: use `package.json`'s `description` for npm or the manifest's
|
||||
`description` for Git. The reviewer can fill in a missing description in the registry.
|
||||
|
||||
### Other languages
|
||||
|
||||
Listings show in English and Simplified Chinese. English is required and translations are optional.
|
||||
Add a translated `name` and `description` to `paseo-plugin.json`, keyed by language tag:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "review-tools",
|
||||
"name": "Review tools",
|
||||
"description": "Reviews changes before merge",
|
||||
"translations": {
|
||||
"zh-CN": { "name": "审查工具", "description": "在合并前审查改动" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Each field is optional, and a missing one shows the English text. The registry checks
|
||||
`translations` when it publishes the plugin: keys are canonical language tags other than `en`, and
|
||||
each value sets only `name` or `description`, as non-empty strings. Paseo itself does not read
|
||||
`translations`, so a mistake there fails publication, not installation.
|
||||
|
||||
To translate the overview, publish a complete copy named for the language next to `OVERVIEW.md`,
|
||||
such as `OVERVIEW.zh-CN.md`. The scaffold's `files` list already includes `OVERVIEW.*.md`. The
|
||||
registry reads `OVERVIEW.<tag>.md` as a translation when the tag is a canonical language tag with a
|
||||
two- or three-letter language code, other than `en`. It ignores other names, such as
|
||||
`OVERVIEW.backup.md` or `OVERVIEW.zh-cn.md`. A three-letter name such as `OVERVIEW.old.md` counts as
|
||||
a language, so do not use one for a non-translation. Readers in that language see the
|
||||
translation; others see the English overview with a notice. The overview rules above apply to
|
||||
translations, so update them when `OVERVIEW.md` changes. Reviewers can add or correct translations
|
||||
in the registry.
|
||||
|
||||
## Icons and screenshots
|
||||
|
||||
Themes, panels and other visual plugin interfaces need at least one image showing the current
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
---
|
||||
title: 发布插件
|
||||
description: 准备、发布 Paseo 插件,并把它提交到经过审查的注册表。
|
||||
nav: 发布
|
||||
---
|
||||
|
||||
# 发布插件 {#publish-a-plugin}
|
||||
|
||||
从一个可用的[插件项目](/docs/plugins)开始。准备好它的展示页,把它发布到 npm 或推送到 GitHub,然后提交到注册表。获得批准后,用户可以用 `paseo plugin add owner/slug` 安装经过审查的版本。
|
||||
|
||||
1. 准备你的[展示页](#your-listing-page);对于可视化插件,还要准备一张[界面图片](#icons-and-screenshots)。
|
||||
2. [发布到 npm](#publish-on-npm)或[通过 Git 分享](#share-through-github-or-git)。
|
||||
3. [提交到插件注册表](#plugin-registry)。
|
||||
|
||||
对于私有插件,请使用[直接从 Git 安装](#share-through-github-or-git)或[私有 npm 包](#private-npm-packages)。
|
||||
|
||||
## 你的展示页 {#your-listing-page}
|
||||
|
||||
你可以不带 `OVERVIEW.md` 提交;审查者会根据你的 README 和经过验证的行为创建展示页。要自己维护这个页面,请把 `OVERVIEW.md` 放在 `paseo-plugin.json` 旁边,并把它包含在发布的包中。脚手架已经在 `files` 中包含了它。你随包发布的概览会替换审查者编写的页面。
|
||||
|
||||
按以下顺序说明这些要点,只在有帮助时使用标题:
|
||||
|
||||
1. 插件做什么。
|
||||
2. 它如何工作(当这一点不明显时)。
|
||||
3. 设置:设置项、账号、令牌、外部工具、支持的操作系统和 Paseo 版本。
|
||||
4. 有用的能力、设置项、数据访问、权限和限制。
|
||||
|
||||
说明数据会流向哪里,包括发送给 Agent Provider 的提示词和工具结果。本地存储并不意味着数据留在主机上。如果插件在运行时下载或安装软件,请说明它安装什么以及来源,并在下载之前要求用户明确确认。这同样适用于激活和连接测试;用户拒绝时,软件必须保持未安装状态。
|
||||
|
||||
使用平实、客观的语言,标题只把首字母大写。省略营销内容、无依据的说法、徽章、更新日志、贡献和许可证章节,以及无助于他人选择插件的实现细节。展示页已经提供了插件的安装说明。
|
||||
|
||||
发布校验器会拒绝概览中任何位置出现的 `paseo plugin add`、`npm install` 和 `npm i`,包括开发或设置示例。请为这些命令链接到你的 README。
|
||||
|
||||
还要提供一句话的描述:npm 插件使用 `package.json` 的 `description`,Git 插件使用 manifest 的 `description`。缺少描述时,审查者可以在注册表中补上。
|
||||
|
||||
### 其他语言 {#other-languages}
|
||||
|
||||
展示页以英文和简体中文显示。英文是必需的,翻译是可选的。在 `paseo-plugin.json` 中添加翻译后的 `name` 和 `description`,以语言标签为键:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "review-tools",
|
||||
"name": "Review tools",
|
||||
"description": "Reviews changes before merge",
|
||||
"translations": {
|
||||
"zh-CN": { "name": "审查工具", "description": "在合并前审查改动" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
每个字段都是可选的,缺少的字段会显示英文。注册表在发布插件时检查 `translations`:键必须是除 `en` 之外的标准语言标签,每个值只能设置 `name` 或 `description`,且必须是非空字符串。Paseo 本身不读取 `translations`,因此这里的错误会导致发布失败,而不会影响安装。
|
||||
|
||||
要翻译概览,请在 `OVERVIEW.md` 旁边发布一份以语言命名的完整副本,例如 `OVERVIEW.zh-CN.md`。脚手架的 `files` 列表已经包含 `OVERVIEW.*.md`。当标签是语言代码为两个或三个字母、且不是 `en` 的标准语言标签时,注册表会把 `OVERVIEW.<标签>.md` 当作翻译,并忽略 `OVERVIEW.backup.md`、`OVERVIEW.zh-cn.md` 这类其他名称。`OVERVIEW.old.md` 这样的三字母名称会被当作一种语言,因此不要用它命名非翻译文件。使用该语言的读者会看到翻译;其他读者会看到英文概览和一条提示。上面的概览规则同样适用于翻译,因此 `OVERVIEW.md` 变更时请同步更新翻译。审查者可以在注册表中补充或修正翻译。
|
||||
|
||||
## 图标和截图 {#icons-and-screenshots}
|
||||
|
||||
主题、面板以及其他带可视界面的插件,需要至少一张展示当前界面的图片。请使用示例数据并隐藏隐私信息。只有图标不满足这项要求;没有自己可视界面的插件不需要截图。
|
||||
|
||||
发布之前,在 `paseo-plugin.json` 中声明本地图片:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "review-tools",
|
||||
"name": "Review tools",
|
||||
"description": "Reviews changes before merge",
|
||||
"icon": "assets/icon.png",
|
||||
"media": ["assets/screenshot.png"],
|
||||
"requirements": { "paseo": ">=0.11.0" }
|
||||
}
|
||||
```
|
||||
|
||||
创建所引用的文件;对于 npm,把 `assets/` 加入 `package.json` 的 `files` 列表。路径相对于 manifest,使用正斜杠,并且必须位于插件目录之内。这些元数据字段需要 Paseo 0.11.0 或更高版本。
|
||||
|
||||
| 字段 | 展示行为 |
|
||||
| ------- | ------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `name` | 可选的显示名称;默认是把插件 ID 转换成易读形式。 |
|
||||
| `icon` | 可选的、相对于包的 PNG 路径。Provider 注册图标有单独的 [SVG 约定](/docs/plugins/reference#providers)。 |
|
||||
| `media` | 相对于包的路径或公开的 HTTPS URL。图片:PNG、JPG、JPEG、WebP 或 GIF。视频:MP4 或 WebM。第一张图片是卡片缩略图;视频作为补充。 |
|
||||
|
||||
公开的媒体 URL 需要带有受支持的文件扩展名(不区分大小写),并且必须以对应的图片或视频内容类型返回 HTTP 200。SVG 和没有扩展名的附件 URL 不能作为注册表媒体。
|
||||
|
||||
除了声明 `media`,你也可以在提交或审查 PR 中提供合适的公开图片 URL。审查者可以把它们添加到展示页,无需发布新的插件版本。
|
||||
|
||||
## 发布到 npm {#publish-on-npm}
|
||||
|
||||
### 1. 设置包的标识 {#1-set-package-identity}
|
||||
|
||||
使用一个你有发布权限的 npm scope,并通过 `npm login` 登录。在插件目录中,把下面的 `@acme` 和仓库替换为你自己的值:
|
||||
|
||||
```bash
|
||||
npm pkg set name=@acme/paseo-review version=1.0.0
|
||||
npm pkg delete private
|
||||
npm pkg set repository.type=git repository.url=git+https://github.com/acme/paseo-review.git
|
||||
```
|
||||
|
||||
当前的注册表工具要求通过 `package.json` 或 npm provenance 提供一个 GitHub 仓库,用来确定 npm 展示页的归属。请把 `repository` 设置为你公开的源码仓库,以便固定版本时可以确定展示页的所有者。provenance 本身是可选的;如果存在,它的仓库必须与包的仓库一致。审查制品时不需要匹配的源码检出。
|
||||
|
||||
### 2. 锁定安装的依赖 {#2-lock-installed-dependencies}
|
||||
|
||||
把 Paseo 宿主提供的库和仅用于类型的依赖放在 `devDependencies` 中。把其他运行时库加入 `dependencies`。提交到注册表时,每棵安装的依赖树都需要一个锁文件,包含确切的版本、解析来源和完整性校验,传递依赖也不例外。
|
||||
|
||||
对于带有运行时依赖的 npm 包,请在不运行生命周期脚本的情况下更新它的锁文件,然后创建可发布的锁文件:
|
||||
|
||||
```bash
|
||||
npm install --package-lock-only --ignore-scripts
|
||||
npm shrinkwrap
|
||||
```
|
||||
|
||||
`npm shrinkwrap` 会把 `package-lock.json` 转换为 `npm-shrinkwrap.json`。npm 会在包的根目录发布 shrinkwrap 文件,但不会发布 `package-lock.json`。只存在于仓库中的锁文件或确切的直接依赖版本,并不能锁定用户安装的依赖树。
|
||||
|
||||
只使用宿主提供的库和开发依赖的插件不需要运行时锁文件,除非某个准备命令会安装这些依赖。你也可以把库打包进插件,并移除运行时依赖声明。如果你的插件会安装一棵单独的依赖树,请参阅[准备与额外设置](#plugins-with-a-build-step)。
|
||||
|
||||
### 3. 检查包并发布 {#3-check-the-package-and-publish}
|
||||
|
||||
在检查包之前生成所有需要的文件;dry run 会跳过打包脚本。
|
||||
|
||||
```bash
|
||||
npm run typecheck
|
||||
npm pack --dry-run --ignore-scripts
|
||||
```
|
||||
|
||||
检查文件列表是否包含 `paseo-plugin.json`、至少一个运行时入口、每个被导入的本地模块、你声明的所有概览和资源文件,以及需要时的 shrinkwrap 文件。只在仓库中存在的导入别名和包外的文件不会到达用户手中。脚手架已经包含了入口以及 `client/`、`server/` 和 `shared/`;请把其他资源目录加入 `files`。
|
||||
|
||||
确认所有声明的准备命令在你支持的 Node/npm 版本下都能成功;不同步或不兼容的锁文件仍然会阻止安装。
|
||||
|
||||
发布检查过的版本:
|
||||
|
||||
```bash
|
||||
npm publish --access public
|
||||
```
|
||||
|
||||
在测试用的 daemon 上,安装这个确切的已发布版本,并试用它的各项贡献:
|
||||
|
||||
```bash
|
||||
paseo plugin install npm:@acme/paseo-review@1.0.0
|
||||
paseo plugin ls
|
||||
```
|
||||
|
||||
确认状态为 `running` 且没有错误,然后[提交这个包](#plugin-registry)。
|
||||
|
||||
## 通过 GitHub 或 Git 分享 {#share-through-github-or-git}
|
||||
|
||||
对于公开注册表,请把插件推送到一个公开的 GitHub 仓库。包含它的 manifest、运行时入口、被导入的文件和展示页资源。提交时 Git 标签和提交是可选的;提供一个即可选择某个修订,否则审查者会解析 `main`。
|
||||
|
||||
如果准备过程会安装运行时 npm 依赖,请提交一个同步的 `package-lock.json`,并把以下命令加入 `paseo-plugin.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"build": [["npm", "ci", "--omit=dev", "--ignore-scripts"]]
|
||||
}
|
||||
```
|
||||
|
||||
`npm ci` 使用已提交的锁文件,如果它与 `package.json` 不一致就会失败。`--omit=dev` 排除开发工具,`--ignore-scripts` 禁止依赖的生命周期脚本。daemon 的命令路径中需要有 npm。只使用宿主提供的库的插件不需要准备命令。生成文件或显式脚本见[准备](#plugins-with-a-build-step)。
|
||||
|
||||
用户可以不经注册表展示页,直接从 Git 安装:
|
||||
|
||||
```bash
|
||||
paseo plugin add git:acme/paseo-review
|
||||
paseo plugin add git:acme/monorepo:plugins/review
|
||||
```
|
||||
|
||||
使用 `--ref` 为首次安装选择分支、标签或提交。只要 daemon 有访问权限,直接安装也支持私有仓库和其他 Git 托管平台;见[插件来源](/docs/plugins/reference#plugin-sources)。注册表提交接受公开的 GitHub 来源和公开的 npm 包。
|
||||
|
||||
## 插件注册表 {#plugin-registry}
|
||||
|
||||
打开[提交表单](https://github.com/getpaseo/plugins/issues/new?template=submit-plugin.yml)。请使用该表单,这样 issue 才会获得启动收录流程的提交标签。
|
||||
|
||||
1. 用插件的名称作为标题。
|
||||
2. 在 **Source** 中填写公开的 npm 包名或页面,或 GitHub 仓库/文件夹的 URL。如果你希望审查某个特定版本,请附上版本号或 Git 引用。npm 默认使用 `latest`。注册表 ID 和本地目录不能作为提交来源。
|
||||
3. 选择合适的分类。把所有界面图片的 URL 写在 issue 描述中。
|
||||
|
||||
机器人会打开一个审查 PR。审查者在不运行插件代码的情况下检查制品和依赖,完善展示页,并记录被批准的确切版本或提交。发布需要符合[审查政策](https://github.com/getpaseo/plugins/blob/main/REVIEW.md),包括锁定的安装输入和恰当的数据访问,即使自动校验已经通过。
|
||||
|
||||
如果被要求修改,请在审查 PR 中回复修正后的版本或修订,或所要求的图片 URL。审查者会更新固定的版本和展示页。收录服务的错误会自动重试;缺少图片或概览不会妨碍打开审查 PR。
|
||||
|
||||
获得批准并发布后,用户可以这样安装你的插件:
|
||||
|
||||
```bash
|
||||
paseo plugin add owner/slug
|
||||
```
|
||||
|
||||
npm 发布会自动收到更新 PR。Git 更新需要手动提交所需的修订。已有的注册表安装使用被批准的固定版本;直接通过 npm/Git 安装的插件遵循[来源参考](/docs/plugins/reference#plugin-sources)中各自的更新规则。
|
||||
|
||||
## 带构建步骤的插件 {#plugins-with-a-build-step}
|
||||
|
||||
Paseo 会编译 TypeScript,因此普通插件不需要单独的发布构建。请把生成的模块放在它们的运行时目录中,并把宿主提供的模块保留为外部依赖。
|
||||
|
||||
Paseo 在获取包时会禁止 npm 生命周期脚本。manifest 的 `build` 中显式声明的命令仍然会运行,包括它们调用的脚本。请说明这些命令的用途,并让它们的输入保持锁定且可检查。当 npm 已经安装了生产依赖时,请从 npm manifest 中移除只用于 Git 的依赖安装命令。
|
||||
|
||||
之后安装另一棵依赖树的设置步骤需要它自己的完整锁文件,即使你的插件已经有一个开发用的锁文件。对于 npm 设置,请把运行时 manifest 和锁文件一起发布,并使用 `npm ci`。对于 Python 设置,请发布一个 requirements 文件,其中包含所有传递依赖包的确切版本和 SHA-256 哈希,然后这样安装:
|
||||
|
||||
```bash
|
||||
python -m pip install --require-hashes -r requirements-documents.txt
|
||||
```
|
||||
|
||||
该命令会拒绝未固定版本或没有哈希的依赖。运行时安装还需要满足上文所述的[确认和披露](#your-listing-page)要求。
|
||||
|
||||
## 私有 npm 包 {#private-npm-packages}
|
||||
|
||||
:::example[用 GitHub Packages 发布私有包 {#publish-a-private-package-with-github-packages}]
|
||||
|
||||
你可以把公司内部的插件发布到 GitHub Packages。按照上面的 npm 步骤操作,使用你组织的 scope,并把发布命令替换为:
|
||||
|
||||
```bash
|
||||
npm publish --registry=https://npm.pkg.github.com
|
||||
```
|
||||
|
||||
发布之前,请先[配置 GitHub 身份验证和包访问权限](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-npm-registry)。
|
||||
|
||||
要安装插件,请**在 daemon 主机上,以运行 Paseo 的用户身份**配置 npm:
|
||||
|
||||
1. 把你组织的注册表加入 `~/.npmrc`:
|
||||
|
||||
```ini
|
||||
@acme:registry=https://npm.pkg.github.com
|
||||
```
|
||||
|
||||
2. 用你的 GitHub 用户名登录,并以一个 personal access token (classic) 作为密码。该令牌需要 `read:packages` 以及对该包的访问权限。
|
||||
|
||||
```bash
|
||||
npm login --scope=@acme --auth-type=legacy --registry=https://npm.pkg.github.com
|
||||
```
|
||||
|
||||
3. 安装插件:
|
||||
|
||||
```bash
|
||||
paseo plugin install npm:@acme/paseo-review
|
||||
```
|
||||
|
||||
Paseo 在安装和更新时使用主机的 npm 注册表设置和凭据。在应用中,只需输入来源标识符。
|
||||
|
||||
:::
|
||||
+488
-411
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user