Merge main into fix/usage-screen-provider-logins

This commit is contained in:
qichangzheng
2026-10-30 19:12:15 +08:00
1905 changed files with 138089 additions and 35552 deletions
+659
View File
@@ -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)` |
+641
View File
@@ -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)` |
+3 -1
View File
@@ -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
+253
View File
@@ -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。
+37 -8
View File
@@ -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 |
+302
View File
@@ -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 的插件,调用客户端操作并验证服务端结果。对于只有客户端的插件,确认贡献在没有服务端进程的情况下也能加载。对任何保存下来的注册移除函数调用两次,验证第二次调用不产生任何效果。
+46
View File
@@ -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,
+416
View File
@@ -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)。
+31
View File
@@ -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
+232
View File
@@ -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 注册表设置和凭据。在应用中,只需输入来源标识符。
:::
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff