jetbrains-pi-acp 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -195
- package/README.zh-CN.md +100 -0
- package/examples/acp.json +1 -1
- package/package.json +2 -1
- package/src/agent.js +32 -14
package/README.md
CHANGED
|
@@ -1,234 +1,98 @@
|
|
|
1
|
-
#
|
|
1
|
+
# JetBrains pi ACP
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[English](README.md) | [简体中文](README.zh-CN.md)
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
IDEA / Air 插件 ← ACP JSON-RPC / stdio → 本仓库适配器 ← RPC JSONL → pi
|
|
7
|
-
```
|
|
5
|
+
Connect the [pi coding agent](https://github.com/earendil-works/pi) to JetBrains IDEs through ACP, with session history, permission controls, file diffs, subagents, and background tasks.
|
|
8
6
|
|
|
9
|
-
|
|
7
|
+
Requires **Node.js 22.19.0+** and a JetBrains IDE with ACP support, such as IntelliJ IDEA with the Air plugin (`com.intellij.air`). The npm package includes pi `1.2.0`; no global pi installation or build step is required.
|
|
10
8
|
|
|
11
|
-
##
|
|
9
|
+
## Quick start
|
|
12
10
|
|
|
13
|
-
1.
|
|
14
|
-
2. 在本项目目录生成适合当前机器的 IDEA 配置:
|
|
11
|
+
1. Sign in to your model provider from a terminal:
|
|
15
12
|
|
|
16
13
|
```bash
|
|
17
|
-
|
|
18
|
-
# 使用外部 pi 时,显式传入路径:
|
|
19
|
-
npm run --silent config -- /绝对路径/pi
|
|
14
|
+
npx --yes jetbrains-pi-acp@0.1.2 --setup
|
|
20
15
|
```
|
|
21
16
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
```json
|
|
40
|
-
{
|
|
41
|
-
"agent_servers": {
|
|
42
|
-
"pi": {
|
|
43
|
-
"command": "npx",
|
|
44
|
-
"args": ["--yes", "jetbrains-pi-acp@0.1.0"],
|
|
45
|
-
"use_idea_mcp": true,
|
|
46
|
-
"use_custom_mcp": true
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
IDE 需要能找到 `npx`;也可先运行 `npm install --global jetbrains-pi-acp@0.1.0`,再使用命令的绝对路径。Registry 收录需要另行审核,源码仓库存在不代表已经上架。
|
|
53
|
-
|
|
54
|
-
发布步骤见 [发布指南](docs/publishing.md),可直接复制的收录材料位于 [registry/jetbrains-pi-acp](registry/jetbrains-pi-acp)。
|
|
55
|
-
|
|
56
|
-
## 指定模型
|
|
57
|
-
|
|
58
|
-
把 `--` 及后续 pi 参数加在 IDEA 配置的 `args` 末尾,例如:
|
|
59
|
-
|
|
60
|
-
```json
|
|
61
|
-
["/绝对路径/jetbrains-acp/src/index.js", "--pi-command", "/绝对路径/pi",
|
|
62
|
-
"--", "--model", "openai/你的模型ID", "--thinking", "medium"]
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
`--` 后传递 pi 的配置选项,不传递初始提示、交互模式或会话选择参数。适配器统一指定 `--mode rpc`、会话 ID 或恢复文件及自己的扩展。也可使用 `PI_ACP_PI_COMMAND` 环境变量指定外部 pi 可执行文件。未指定时通过当前 Node.js 启动随依赖安装的 pi,不依赖 PATH 中的全局 pi。IDEA 启动时的 PATH 可能与终端不同,推荐使用生成的绝对路径。
|
|
66
|
-
|
|
67
|
-
## 已实现的协议功能
|
|
68
|
-
|
|
69
|
-
| ACP 功能 | 行为 |
|
|
70
|
-
| --- | --- |
|
|
71
|
-
| `initialize` | 协商 v1,返回实际支持的能力 |
|
|
72
|
-
| `session/new` | 每个会话启动独立 pi 进程,使用 IDEA 传入的绝对 `cwd`,按 pi 配置持久保存 |
|
|
73
|
-
| `session/list` | 使用 pi 原生会话索引发现历史,可按项目 `cwd` 过滤;返回会话标题和更新时间,支持游标分页;Air 会话索引附带可关联的分叉来源 |
|
|
74
|
-
| `session/load` / `session/resume` | 加载会回放当前分支的消息、思考和工具记录;恢复只重新打开,随后均可继续聊天 |
|
|
75
|
-
| `session/close` / `session/delete` / `session/fork` | 关闭释放当前 pi 子进程;删除移除对应历史文件;分叉复制当前分支,或按指定助手消息保留其所在整轮,并保留源会话 |
|
|
76
|
-
| 模型列表与切换 | 返回 `configOptions` 与兼容旧版的 `models`;支持 `session/set_config_option` 和 `session/set_model` |
|
|
77
|
-
| 推理级别 | 通过 `thought_level` 配置选项连接 pi 的推理设置;模型切换后刷新可用级别与实际值 |
|
|
78
|
-
| 权限模式 | 默认完全访问,支持规划、只读和标准访问;同时兼容 `session/set_mode` 与 `session/set_config_option` |
|
|
79
|
-
| `session/prompt` | 转发文本、内嵌图片、文本资源和资源引用;v1 完成响应附带本轮模型及自动压缩 token 用量 |
|
|
80
|
-
| `_session/steering` | 按 Air 扩展约定接收执行中的补充提示,通过 pi steering 引导后续执行;空闲时启动新回合 |
|
|
81
|
-
| `session/update` | 转发回答、思考、工具状态、命令列表、上下文用量、会话累计费用和压缩状态;同步 pi 会话名变更及恢复后的标题 |
|
|
82
|
-
| 命令与上下文 | 发布 pi 扩展命令、prompt 和 skill;内置 `/status` 与 `/compact [要求]` |
|
|
83
|
-
| 终端输出 | `bash/powershell` 发送 terminal 内容、协商后的输出增量及退出码 |
|
|
84
|
-
| 终端输入 | Air v1 可在执行中的补充输入入口发送 `/stdin 文本` 或 `/eof`;其他客户端启用 `_meta.terminal_input: true` 后通过 `_terminal/input` 写入 UTF-8 stdin 或 EOF |
|
|
85
|
-
| 文件 diff | 内置 `edit/write` 成功后发送标准 ACP diff 和文件位置;编辑提供首个修改行号,Air 可协商 Git patch 及每轮文件报告,历史加载回放同一 diff;Git patch 支持特殊路径与空新文件 |
|
|
86
|
-
| 其他工具的文件报告 | 客户端协商并请求文件报告后,对比本轮执行前后工作目录的文件元数据,将命令、MCP、扩展造成的新增、修改、删除、重命名路径并入报告;包含二进制与 Git 忽略文件,其他工具暂不生成逐文件 diff |
|
|
87
|
-
| 扩展交互 | 表单桥接 `confirm/select/input/editor`;URL 交互通过扩展事件总线请求及完成通知,均需客户端协商 |
|
|
88
|
-
| 子 agent 会话 | `spawn_agent` 创建独立 pi 会话,协商后发布原生父子生命周期,未协商时在父工具中显示回答;支持对子会话取消和关闭 |
|
|
89
|
-
| 后台任务 | `background_shell` 启动后台命令,`task_status` 查询,`stop_task` 定向停止;Air 协商 `asyncTasks` 后显示后台任务状态 |
|
|
90
|
-
| `session/cancel` | 解除待授权请求、清空 pi 队列并中止执行 |
|
|
91
|
-
| `session/request_permission` | pi 扩展在工具执行前阻塞,等待 IDEA 的授权结果 |
|
|
92
|
-
| MCP | 转发 stdio 和 HTTP 配置;不支持 SSE |
|
|
17
|
+
In pi, run `/login`, complete sign-in, then exit pi and return to the IDE. Credentials are managed by pi.
|
|
18
|
+
|
|
19
|
+
2. Choose **Add Custom Agent** in the IDE chat interface. Merge this configuration into `~/.jetbrains/acp.json`, keeping existing agents:
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"agent_servers": {
|
|
24
|
+
"pi": {
|
|
25
|
+
"command": "npx",
|
|
26
|
+
"args": ["--yes", "jetbrains-pi-acp@0.1.2"],
|
|
27
|
+
"use_idea_mcp": true,
|
|
28
|
+
"use_custom_mcp": true
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
```
|
|
93
33
|
|
|
94
|
-
|
|
34
|
+
3. Select **pi** and send a message.
|
|
95
35
|
|
|
96
|
-
|
|
36
|
+
The IDE must be able to find `npx`; use its absolute path if needed. Menu locations depend on the IDE/plugin version. See the [JetBrains ACP documentation](https://www.jetbrains.com/help/ai-assistant/acp.html).
|
|
97
37
|
|
|
98
|
-
|
|
38
|
+
## Features
|
|
99
39
|
|
|
100
|
-
|
|
40
|
+
- ACP v1 and v2 draft support, with streaming responses, reasoning, tool status, terminal output, and usage reporting.
|
|
41
|
+
- Native pi session history: list, load, resume, rename, fork, and delete sessions.
|
|
42
|
+
- Model and reasoning-level selection, plus full-access, standard, read-only, and plan modes.
|
|
43
|
+
- File diffs for built-in `edit`/`write` tools, with optional Air Git patches and per-turn file reports.
|
|
44
|
+
- stdio/HTTP MCP forwarding, including IDE tools; subagents and background shell tasks with targeted cancellation.
|
|
45
|
+
- Steering, terminal stdin, forms, and URL interactions when supported by the client. Air v1 provides `/stdin text` and `/eof` during command execution.
|
|
101
46
|
|
|
102
|
-
|
|
47
|
+
**Full access is the default and automatically permits tool calls.** Standard mode requests approval for tools other than built-in read-only tools; read-only and plan modes reject those tools. Permission modes are not an OS sandbox, and installed pi extensions remain trusted code. Client capabilities determine which features are available.
|
|
103
48
|
|
|
104
|
-
|
|
49
|
+
## Configuration
|
|
105
50
|
|
|
106
|
-
|
|
51
|
+
Append pi options after `--` in the agent's `args`, for example:
|
|
107
52
|
|
|
108
53
|
```json
|
|
109
|
-
|
|
54
|
+
["--yes", "jetbrains-pi-acp@0.1.2", "--", "--model", "provider/model-id", "--thinking", "medium"]
|
|
110
55
|
```
|
|
111
56
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
专用终端输入框仍需客户端接入这个新增接口;Air v1 的聊天命令可作为输入入口,v2 不提供此聊天入口。当前提供 stdin 管道,不提供 PTY、终端尺寸、全屏交互、tty 密码提示或快捷键信号;`data: "\u0003"` 是输入字节,不等同于 Ctrl+C,应使用 `session/cancel`。使用 stdin 传递 shell 命令的旧 WSL 配置无法同时用于交互输入。Windows PowerShell 通路已有实现,尚未在 Windows 实机验证。
|
|
57
|
+
Use configuration options here, not initial prompts, interactive mode, or session-selection arguments. The adapter manages RPC mode and sessions.
|
|
115
58
|
|
|
116
|
-
|
|
59
|
+
To use an external pi executable, add `--pi-command /absolute/path/to/pi` before `--`, or set `PI_ACP_PI_COMMAND`. Verify compatibility when using other pi versions.
|
|
117
60
|
|
|
118
|
-
|
|
61
|
+
Sessions use pi's native storage, normally `~/.pi/agent/sessions/`, and respect pi's session-directory settings. History is filtered by the project working directory. Avoid accessing the same session simultaneously from multiple adapters or from pi and the IDE.
|
|
119
62
|
|
|
120
|
-
|
|
121
|
-
- `background_shell({command, timeout?})`:启动后台 shell 命令,`timeout` 为秒;返回 `taskId`。命令使用 pi 配置的 shell 和命令前缀。
|
|
122
|
-
- `task_status({taskId})`:查询本会话任务的状态及最近最多 256 Ki 个 UTF-16 代码单元的输出,截断时返回 `truncated: true`。
|
|
123
|
-
- `stop_task({taskId})`:只停止该任务;后台命令终止整个进程组,子 agent 停止当前执行及其后代任务。
|
|
63
|
+
Restart the agent connection after updating the adapter or changing startup options.
|
|
124
64
|
|
|
125
|
-
|
|
65
|
+
## Limitations
|
|
126
66
|
|
|
127
|
-
|
|
67
|
+
- Tools execute locally through pi; ACP client-side `fs/*` and `terminal/*` execution is not implemented. Save editor buffers before asking the agent to read or change files.
|
|
68
|
+
- Terminal input uses stdin pipes, not a PTY. Full-screen apps and TTY password prompts are unsupported; background tasks have no stdin input.
|
|
69
|
+
- Diffs have size limits and may be unavailable for binary files or older history. File reports scan at most 20,000 entries or two seconds per snapshot and cannot reliably attribute concurrent external changes.
|
|
70
|
+
- MCP SSE, audio, and binary embedded resources are unsupported. Image support depends on the model.
|
|
71
|
+
- ACP v2 follows a draft schema. Client UI behavior, real provider sign-in, and Windows hardware still need verification.
|
|
128
72
|
|
|
129
|
-
|
|
73
|
+
## Development
|
|
130
74
|
|
|
131
|
-
|
|
75
|
+
From a source checkout:
|
|
132
76
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
```ts
|
|
138
|
-
const task = await new Promise((resolve, reject) => {
|
|
139
|
-
pi.events.emit('acp:agent:spawn', {
|
|
140
|
-
toolCallId,
|
|
141
|
-
params: { prompt: '需要委派的任务', name: '审查 agent' },
|
|
142
|
-
resolve,
|
|
143
|
-
reject,
|
|
144
|
-
});
|
|
145
|
-
});
|
|
77
|
+
```bash
|
|
78
|
+
npm ci
|
|
79
|
+
node src/index.js --setup
|
|
80
|
+
npm run --silent config
|
|
146
81
|
```
|
|
147
82
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
## 历史会话
|
|
151
|
-
|
|
152
|
-
适配器声明 `loadSession` 以及 list、resume、close、delete、fork 会话能力。Air 可通过 `session/list` 发现已有 pi 会话,再通过 `session/load` 打开并回放,或通过 `session/resume` 仅重新打开。列表读取 pi 的原生存储,不复制会话到 IDEA;默认按 IDEA 当前项目的绝对 `cwd` 过滤,其他目录下的历史不会出现在该项目的列表中。分叉使用 pi 原生 clone,生成独立历史文件;源会话由新的 pi 子进程继续持有。空闲会话在 `session/load` 或 `session/resume` 收到新的 `mcpServers` 时自动重建 pi 连接,保留历史、模型、推理级别和权限;重连失败保留原连接,有正在运行的后台任务时拒绝切换。分叉可传入当前 IDE 的 `mcpServers` 配置;新会话使用该配置,源会话保持原配置,省略时继承源配置。指定历史消息时,在 `session/fork` 的 `_meta.jetbrains.air.fork` 传入 `{version: 1, messageId}`,仅复制当前分支截至该助手消息所在整轮的历史(含工具结果)。支持 `:segment:<n>` 后缀;可选 `messageFingerprint: "sha256:<64 位十六进制>"` 在 ID 未匹配时按回答文本匹配,`messageOccurrence` 选择重复回答中的第几次(从 1 开始,默认 1)。无效参数或未找到消息返回 `-32602`,不会退回复制整个分支。
|
|
153
|
-
|
|
154
|
-
指定消息分叉的请求必须带 `_meta.jetbrains.air.fork.messageId`。若日志显示 `Session fork requested ... message=null` 和 `Session fork started ... turn=null`,说明分叉点未传到后端,适配器会按整段会话分叉。仅凭该日志不能判断用户选择了哪个界面入口;从消息或回合菜单出现此行为时,需要继续核对客户端参数传递。
|
|
155
|
-
|
|
156
|
-
安装在 pi 中的 `session-title` 扩展生成会话名后,适配器会实时推送给 Air;手动改名也使用同一通道。需要保持该扩展启用,Air 的实时刷新取决于其版本是否处理 ACP `session_info_update`。
|
|
157
|
-
|
|
158
|
-
默认存储位置是 `~/.pi/agent/sessions/`;同时遵循 pi 的 `--session-dir`、`PI_CODING_AGENT_SESSION_DIR` 和 `sessionDir` 设置,以及 `PI_CODING_AGENT_DIR`。新聊天同样持久保存,可从终端 pi 恢复。之前由本适配器以 `--no-session` 创建的临时聊天没有历史文件,无法补回。
|
|
159
|
-
|
|
160
|
-
更新适配器后,需要重启 Air 的 pi Agent 连接(或重启 IDEA)以重新协商能力。命令列表在会话创建时发布;旧连接不会自动加载代码修改。已兼容 Air 命令输入的类型字段,重连后可在已创建的会话中输入 `/` 检查补全。能否自动显示历史还取决于 Air 版本是否调用 `session/list`。打开历史继续聊天会写入原来的 pi 会话文件,避免同时在终端和 Air 中操作同一会话。
|
|
161
|
-
|
|
162
|
-
## 验证
|
|
83
|
+
The config command prints JSON with absolute paths without editing IDE settings. For an external pi installation, run `npm run --silent config -- /absolute/path/to/pi`.
|
|
163
84
|
|
|
164
85
|
```bash
|
|
165
86
|
npm test
|
|
166
|
-
# 使用本机 pi 验证启动、扩展握手及本地模型替身 steering,不调用远端模型:
|
|
167
87
|
PI_ACP_SMOKE=1 npm test
|
|
168
88
|
```
|
|
169
89
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
## URL 交互扩展接口
|
|
173
|
-
|
|
174
|
-
客户端在 ACP v1 的 `clientCapabilities` 或 v2 的 `capabilities` 中声明 `elicitation: { url: {} }` 后,pi 扩展可通过事件总线请求浏览器交互:
|
|
175
|
-
|
|
176
|
-
```ts
|
|
177
|
-
const request = (event: string, params: object) => new Promise<any>((resolve, reject) => {
|
|
178
|
-
pi.events.emit(event, { params, resolve, reject });
|
|
179
|
-
});
|
|
180
|
-
const result = await request('acp:elicitation:url', {
|
|
181
|
-
url: 'https://example.com/connect', message: '请授权账号以继续',
|
|
182
|
-
});
|
|
183
|
-
if (result.action === 'accept') {
|
|
184
|
-
// 由扩展等待自己的网页回调并核实操作结果,再发送完成通知。
|
|
185
|
-
await waitForVerifiedCallback();
|
|
186
|
-
await request('acp:elicitation:complete', { elicitationId: result.elicitationId });
|
|
187
|
-
}
|
|
188
|
-
```
|
|
189
|
-
|
|
190
|
-
此代码应放在本适配器加载的 pi 扩展工具或命令处理器中,`waitForVerifiedCallback` 由实际业务实现。请求通过已有 fd 3 双向通道传递;`resolve/reject` 只在扩展进程内使用。
|
|
191
|
-
|
|
192
|
-
请求返回 `{ action, elicitationId }`,`action` 为 `accept/decline/cancel`;客户端不支持 URL、回合已取消或客户端回复错误时返回 `cancel`。非法参数通过 `reject` 返回错误。`accept` 仅表示同意打开网址;浏览器交互和凭据存储由扩展处理。完成调用返回 `{ completed: true }` 并发送 ACP `elicitation/complete`;未知、重复、未接受或不属于当前会话的 ID 返回 `{ completed: false }`。
|
|
193
|
-
|
|
194
|
-
只允许 HTTPS 及 `localhost/127.0.0.1/[::1]` 的 HTTP 网址,禁止 URL 内用户名和密码,网址及说明各最多 10000 字符。扩展不得在网址或说明中放入凭据、个人敏感信息或可直接访问受保护资源的授权链接,也不得通过此接口回传网页凭据。适配器生成连接内唯一 ID,最多保留 100 个待完成交互;已接受交互可在模型回合结束后完成,会话取消、关闭、进程失败或切换只读/规划模式时清理。客户端负责展示网址、征求用户同意并打开浏览器。
|
|
195
|
-
|
|
196
|
-
此接口提供扩展接入契约;尚未自动接入任意 MCP 服务端的 URL elicitation。协议依据:[ACP elicitation 说明](https://github.com/agentclientprotocol/agent-client-protocol/blob/main/docs/rfds/elicitation.mdx)。
|
|
197
|
-
|
|
198
|
-
## ACP v2 协议适配
|
|
199
|
-
|
|
200
|
-
客户端以 `protocolVersion: 2`、`info: { name, version }` 和可选的 `capabilities` 初始化,适配器自动选择 v2。对照 2026-10-15 的[上游草案 Schema](https://github.com/agentclientprotocol/agent-client-protocol/blob/bacbb62b5f3181719eecec714e19fdb34a764f75/schema/v2/schema.json),已覆盖会话基础协议及声明的图片、文本内嵌上下文、stdio/HTTP MCP 和删除能力;分叉使用上游可选的实验性 `session.fork`。子 agent 与后台任务仍按 Air 扩展协商,v2 不把 `subagents` 当作标准能力。
|
|
201
|
-
|
|
202
|
-
- 支持 `session/new/list/resume/close/prompt/cancel` 及 `session/set_config_option`,配置使用 `configId`,设置选择项需要 `type: "id"`。MCP 的可选 `args/env/headers` 可省略。
|
|
203
|
-
- 提示正式插入后立即返回 `messageId` 并回显用户消息,后续通过 `state_update` 报告 `running/requires_action/idle`。排队提示按会话串行执行;插入前 `$/cancel_request` 返回 `-32800`,插入后使用 `session/cancel`。执行失败以 `stopReason: "error"` 和结构化 `error` 报告;关闭活动会话先报告取消完成,再返回成功。
|
|
204
|
-
- `session/resume` 不指定 `replayFrom` 或传 `null` 时只恢复上下文;`{ type: "start" }` 在响应前回放当前分支。拒绝其他游标;保留用户消息 ID,回放前按消息类型清空旧内容,并重置终端输出,避免重复追加。
|
|
205
|
-
- 工具、Git diff、终端字节输出、结构化计划、配置、用量、授权、表单及 URL 交互均转换为 v2 格式。压缩完成及历史回放包含摘要;新压缩使用独立 ID。
|
|
206
|
-
- 客户端声明 `capabilities.auth.terminal: {}` 后,握手发布带 `methodId` 和环境变量的终端登录入口。客户端以原启动配置运行交互进程,完成后重新连接;不能对终端方式调用 `auth/login`。`auth/logout` 取消活动与排队提示并关闭当前连接,后续请求返回 `-32000`;不删除 pi 保存的凭据。
|
|
207
|
-
- 未声明音频、额外工作目录和实验性 `systemPrompt/NES` 能力;不支持的工作目录及系统提示参数会报错。v1 的 `session/load/set_mode/set_model` 和 Air `_session/steering` 在 v2 中不可用,分别使用恢复、配置和提示队列接口。
|
|
208
|
-
|
|
209
|
-
自动验证:`node --test test/protocol.test.js test/agent.test.js test/url-elicitation.test.js`;本机 pi 验证:`PI_ACP_SMOKE=1 node --test --test-name-pattern='ACP v2|本机 pi 文件 diff' test/agent.test.js`。协议消息已按上游 Schema 核对,IDEA/其他 v2 客户端的界面展示仍需实机验收。草案后续变更需要重新核对。
|
|
210
|
-
|
|
211
|
-
## 当前边界
|
|
212
|
-
|
|
213
|
-
- 历史回放展示当前分支的用户消息、回答、思考和工具记录;v2 压缩事件包含摘要,pi 扩展自定义消息尚未映射为独立的 ACP 展示项。上下文恢复由 pi 原生处理。IDEA 的模型菜单显示 pi 的可用模型,初始模型通过启动参数或 pi 设置选择。
|
|
214
|
-
- 文件操作和命令由本机 pi 执行;未接入 ACP `fs/*` 或 `terminal/*`。使用前保存 IDEA 编辑器内容。内置 `edit` 使用 pi 返回的补丁按片段展示 diff;`write` 在授权后捕获旧文本,区分新建与覆盖,并将快照保存在 pi 会话记录中。仅展示不超过 256 KiB 的编辑补丁或写入新旧文本合计;二进制、读取失败、重叠的同文件操作和失败工具只保留原有工具输出。写入快照不受 pi 文件锁保护,外部进程同时修改目标文件可能造成 diff 偏差。旧历史中没有快照的 `write` 无法补回 diff。
|
|
215
|
-
- 每轮文件报告仅在客户端协商并请求时扫描工作目录,读取大小、时间戳、权限等元数据,不读取正文。每次快照最多扫描 20000 项或 2 秒,跳过 `.git`,不跟随符号链接;达到限制或读取失败时标注 `truncated: true`,不把未扫描路径误判为新增或删除。重命名报告旧、新两个路径,权限或时间戳变化也可能进入清单。并发外部修改无法归属,轮内新增后删除、目录外和轮次结束后的后台修改可能遗漏,因此保留 `declaredComplete: false`。失败工具已留下的修改可进入正常结束轮次的报告;取消或整轮失败仍沿用 `unavailable`。
|
|
216
|
-
- 权限模式不是操作系统沙箱。完全访问自动允许全部工具;标准访问自动允许内置只读工具,其他工具逐次授权;只读与规划模式自动拒绝其他工具。规划模式同时给模型增加只制定计划的指令,但不等价于容器或系统级隔离。选择“本会话允许全部工具”等同于切换完全访问。用户安装的 pi 扩展自身仍属于受信任代码,其直接副作用不属于工具授权边界。
|
|
217
|
-
- 客户端声明 ACP form elicitation 时,pi 扩展的 `select`、`input`、`editor` 和普通 `confirm` 会转成结构化表单;客户端未声明时返回取消。v1 通知需客户端声明 notices,v2 可直接转发;状态、widget 和编辑器控制仍不映射到 ACP 展示。
|
|
218
|
-
- 图片支持最终取决于所选模型;不支持音频和二进制内嵌资源。资源引用只转发 URI,由 pi 根据可用工具读取。
|
|
219
|
-
- 已有归档/列表订阅、结构化计划和长期目标状态管理;goal 不提供自主续跑或预算管理。客户端 `fs/*` / `terminal/*` 执行和完整账户/provider 管理仍未接入;登录复用终端入口。指定消息分叉支持当前分支中的助手消息,保留所在完整回合;分叉来源在父会话仍处于查询范围时发布。详见 [功能对比](docs/功能对比.md)。
|
|
90
|
+
Smoke tests start real pi processes with a local model stub and do not call remote models.
|
|
220
91
|
|
|
221
|
-
|
|
92
|
+
- [Publishing guide (Chinese)](docs/publishing.md)
|
|
93
|
+
- [Registry entry](registry/jetbrains-pi-acp/agent.json) · [Registry submission](https://github.com/agentclientprotocol/registry/pull/691)
|
|
94
|
+
- [ACP protocol](https://agentclientprotocol.com/protocol/v1/overview) · [pi RPC](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/rpc.md)
|
|
222
95
|
|
|
223
|
-
|
|
224
|
-
- [ACP v2 草案 Schema](https://github.com/agentclientprotocol/agent-client-protocol/blob/bacbb62b5f3181719eecec714e19fdb34a764f75/schema/v2/schema.json)
|
|
225
|
-
- [ACP 初始化](https://agentclientprotocol.com/protocol/v1/initialization)
|
|
226
|
-
- [ACP 会话创建与 MCP](https://agentclientprotocol.com/protocol/v1/session-setup)
|
|
227
|
-
- [ACP 历史会话列表](https://agentclientprotocol.com/protocol/v1/session-list)
|
|
228
|
-
- [ACP 提示回合与取消](https://agentclientprotocol.com/protocol/v1/prompt-turn)
|
|
229
|
-
- [ACP 工具调用与授权](https://agentclientprotocol.com/protocol/v1/tool-calls)
|
|
230
|
-
- [Air steering 扩展约定](https://github.com/agentclientprotocol/codex-acp/blob/main/docs/air-extensions.md#jetbrains-shared-keys)
|
|
231
|
-
- [pi RPC 官方文档](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/rpc.md)
|
|
232
|
-
- [pi 扩展官方文档](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/extensions.md)
|
|
96
|
+
## License
|
|
233
97
|
|
|
234
|
-
|
|
98
|
+
[MIT](LICENSE)
|
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# JetBrains pi ACP
|
|
2
|
+
|
|
3
|
+
[English](README.md) | [简体中文](README.zh-CN.md)
|
|
4
|
+
|
|
5
|
+
通过 ACP 将 [pi coding agent](https://github.com/earendil-works/pi) 连接到 JetBrains IDE,支持历史会话、权限控制、文件 diff、子 agent 和后台任务。
|
|
6
|
+
|
|
7
|
+
需要 **Node.js 22.19.0+** 和支持 ACP 的 JetBrains IDE,例如安装了 Air 插件(`com.intellij.air`)的 IntelliJ IDEA。npm 包自带 pi `1.2.0`,无需全局安装 pi,也无需构建。
|
|
8
|
+
|
|
9
|
+
## 快速开始
|
|
10
|
+
|
|
11
|
+
1. 在终端中登录模型提供商:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx --yes jetbrains-pi-acp@0.1.2 --setup
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
在 pi 中运行 `/login`,完成登录后退出 pi 并返回 IDE。凭据由 pi 管理。
|
|
18
|
+
|
|
19
|
+
2. 在 IDE 聊天界面选择 **Add Custom Agent**,将以下配置合并到 `~/.jetbrains/acp.json`,保留已有 Agent:
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"agent_servers": {
|
|
24
|
+
"pi": {
|
|
25
|
+
"command": "npx",
|
|
26
|
+
"args": ["--yes", "jetbrains-pi-acp@0.1.2"],
|
|
27
|
+
"use_idea_mcp": true,
|
|
28
|
+
"use_custom_mcp": true
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
3. 选择 **pi** 并发送消息。
|
|
35
|
+
|
|
36
|
+
IDE 必须能找到 `npx`,必要时使用其绝对路径。菜单位置随 IDE 和插件版本变化,参见 [JetBrains ACP 文档](https://www.jetbrains.com/help/ai-assistant/acp.html)。
|
|
37
|
+
|
|
38
|
+
## 主要功能
|
|
39
|
+
|
|
40
|
+
- 支持 ACP v1 和 v2 草案,流式展示回答、思考、工具状态、终端输出和用量。
|
|
41
|
+
- 每轮汇总模型请求与自动压缩的输入、输出及缓存读写 token,取消仍保留已发生用量;通过 `usage_update.cost` 发布 pi 记录的会话累计美元费用,恢复历史后继续累计。可用 `/status` 查看累计 token、费用和上下文用量。
|
|
42
|
+
- 使用 pi 原生历史,支持会话列表、加载、恢复、改名、分叉和删除。
|
|
43
|
+
- 支持切换模型与推理级别,以及完全访问、标准访问、只读和规划模式。名称和模式说明在启动时按系统语言自动切换中英文:依次读取非空的 `LC_ALL`、`LC_MESSAGES`、`LANG`,未设置时使用系统默认语言;中文使用中文,其余语言使用英文。更改系统语言后需重启适配器。
|
|
44
|
+
- 展示内置 `edit/write` 工具的文件 diff,可协商 Air Git patch 和每轮文件报告。
|
|
45
|
+
- 转发 stdio/HTTP MCP 配置及 IDE 工具;支持子 agent、后台 shell 任务和定向停止。
|
|
46
|
+
- 按客户端能力提供 steering、终端 stdin、表单和 URL 交互。Air v1 可在命令执行期间发送 `/stdin 文本` 和 `/eof`。
|
|
47
|
+
|
|
48
|
+
**默认完全访问,自动允许工具调用。** 标准访问对内置只读工具以外的工具逐次授权;只读和规划模式拒绝这些工具。权限模式不是操作系统沙箱,安装的 pi 扩展仍属于受信任代码。具体功能取决于客户端协商能力。
|
|
49
|
+
|
|
50
|
+
## 配置
|
|
51
|
+
|
|
52
|
+
在 Agent 的 `args` 末尾添加 `--`,随后传递 pi 参数,例如:
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
["--yes", "jetbrains-pi-acp@0.1.2", "--", "--model", "provider/model-id", "--thinking", "medium"]
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
这里只传配置选项,不传初始提示、交互模式或会话选择参数。RPC 模式和会话由适配器管理。
|
|
59
|
+
|
|
60
|
+
使用外部 pi 时,在 `--` 前添加 `--pi-command /绝对路径/pi`,或设置 `PI_ACP_PI_COMMAND`。其他 pi 版本需要自行验证兼容性。
|
|
61
|
+
|
|
62
|
+
历史使用 pi 原生存储,通常位于 `~/.pi/agent/sessions/`,同时遵循 pi 的会话目录设置。列表按项目工作目录过滤。避免多个适配器,或终端 pi 与 IDE,同时操作同一会话。
|
|
63
|
+
|
|
64
|
+
更新适配器或调整启动参数后,需要重启 Agent 连接。
|
|
65
|
+
|
|
66
|
+
## 限制
|
|
67
|
+
|
|
68
|
+
- 工具由本机 pi 执行,尚未接入客户端 `fs/*` 和 `terminal/*` 执行接口。让 Agent 读取或修改文件前,请保存编辑器内容。
|
|
69
|
+
- 终端输入使用 stdin 管道,不提供 PTY,不支持全屏终端应用和 TTY 密码提示;后台任务不支持 stdin 输入。
|
|
70
|
+
- diff 有大小限制,二进制文件或旧历史可能无法展示。每轮文件报告的单次快照最多扫描 20,000 项或两秒,无法准确区分外部并发修改。
|
|
71
|
+
- 不支持 MCP SSE、音频和二进制内嵌资源,图片支持取决于模型。
|
|
72
|
+
- ACP v2 仍为草案,客户端界面展示、真实提供商账号登录及 Windows 实机仍需验证。
|
|
73
|
+
- 每轮 token 在 v1 提示响应的 `usage`、v2 完成状态的 `usage` 中返回;未上报用量时省略,不单列思考 token 或每轮费用。费用取自 pi 的模型计价数据;上下文用量不可用时不发送 `usage_update`,IDE 展示取决于客户端支持。
|
|
74
|
+
|
|
75
|
+
## 开发
|
|
76
|
+
|
|
77
|
+
在源码目录运行:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
npm ci
|
|
81
|
+
node src/index.js --setup
|
|
82
|
+
npm run --silent config
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
配置生成命令只打印包含绝对路径的 JSON,不修改 IDE 设置。使用外部 pi 时运行 `npm run --silent config -- /绝对路径/pi`。
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
npm test
|
|
89
|
+
PI_ACP_SMOKE=1 npm test
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
冒烟测试启动真实 pi 进程并使用本地模型替身,不调用远端模型。
|
|
93
|
+
|
|
94
|
+
- [发布指南](docs/publishing.md)
|
|
95
|
+
- [Registry 清单](registry/jetbrains-pi-acp/agent.json) · [Registry 收录申请](https://github.com/agentclientprotocol/registry/pull/691)
|
|
96
|
+
- [ACP 协议](https://agentclientprotocol.com/protocol/v1/overview) · [pi RPC](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/rpc.md)
|
|
97
|
+
|
|
98
|
+
## 许可证
|
|
99
|
+
|
|
100
|
+
[MIT](LICENSE)
|
package/examples/acp.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jetbrains-pi-acp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "连接 JetBrains IDE 与 pi coding agent 的 ACP 适配器",
|
|
6
6
|
"bin": {
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"url": "https://github.com/fun90/jetbrains-pi-acp/issues"
|
|
25
25
|
},
|
|
26
26
|
"files": [
|
|
27
|
+
"README.zh-CN.md",
|
|
27
28
|
"src/",
|
|
28
29
|
"extensions/",
|
|
29
30
|
"scripts/",
|
package/src/agent.js
CHANGED
|
@@ -15,12 +15,20 @@ import { changedFiles, fileSnapshot } from './file-changes.js';
|
|
|
15
15
|
const executeFile = promisify(execFile);
|
|
16
16
|
const extensionPath = fileURLToPath(new URL('../extensions/acp.ts', import.meta.url));
|
|
17
17
|
const invalid = message => Object.assign(new Error(message), { code: -32602 });
|
|
18
|
-
const modes =
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
18
|
+
const modes = {
|
|
19
|
+
zh: [
|
|
20
|
+
{ id: 'plan', name: '规划', description: '只分析和制定计划,自动拒绝修改文件及执行命令' },
|
|
21
|
+
{ id: 'read-only', name: '只读', description: '自动拒绝写文件、执行命令及其他非内置只读工具' },
|
|
22
|
+
{ id: 'standard', name: '标准访问', description: '写文件、执行命令及其他非内置只读工具逐次请求授权' },
|
|
23
|
+
{ id: 'full', name: '完全访问', description: '本会话自动允许全部工具调用,包括写文件和执行命令' },
|
|
24
|
+
],
|
|
25
|
+
en: [
|
|
26
|
+
{ id: 'plan', name: 'Plan', description: 'Analyze and plan only; automatically deny file changes and command execution' },
|
|
27
|
+
{ id: 'read-only', name: 'Read Only', description: 'Automatically deny file writes, command execution, and tools other than built-in read-only tools' },
|
|
28
|
+
{ id: 'standard', name: 'Standard Access', description: 'Request approval for each file write, command execution, or tool other than built-in read-only tools' },
|
|
29
|
+
{ id: 'full', name: 'Full Access', description: 'Automatically allow all tool calls in this session, including file writes and command execution' },
|
|
30
|
+
],
|
|
31
|
+
};
|
|
24
32
|
const toolKind = name => ({ read: 'read', grep: 'search', find: 'search', ls: 'read',
|
|
25
33
|
write: 'edit', edit: 'edit', update_plan: 'think', background_shell: 'execute', bash: 'execute', powershell: 'execute' })[name] || 'other';
|
|
26
34
|
|
|
@@ -93,6 +101,10 @@ export function convertPrompt(blocks) {
|
|
|
93
101
|
|
|
94
102
|
export class Agent {
|
|
95
103
|
constructor({ send, command = 'pi', args = [] }) {
|
|
104
|
+
const locale = process.env.LC_ALL || process.env.LC_MESSAGES || process.env.LANG ||
|
|
105
|
+
Intl.DateTimeFormat().resolvedOptions().locale;
|
|
106
|
+
this.chinese = /^zh(?:[-_.@]|$)/i.test(locale);
|
|
107
|
+
this.modes = modes[this.chinese ? 'zh' : 'en'];
|
|
96
108
|
this.send = send;
|
|
97
109
|
this.command = command;
|
|
98
110
|
this.args = args;
|
|
@@ -258,7 +270,7 @@ export class Agent {
|
|
|
258
270
|
}
|
|
259
271
|
if (method === 'session/set_mode' || (method === 'session/set_config_option' && params.configId === 'mode')) {
|
|
260
272
|
const modeId = method === 'session/set_mode' ? params.modeId : params.value;
|
|
261
|
-
if (!modes.some(mode => mode.id === modeId)) throw invalid('未知权限模式');
|
|
273
|
+
if (!this.modes.some(mode => mode.id === modeId)) throw invalid('未知权限模式');
|
|
262
274
|
await this.setMode(session, modeId);
|
|
263
275
|
return method === 'session/set_mode' ? {} : { configOptions: this.modelConfig(session) };
|
|
264
276
|
}
|
|
@@ -1209,7 +1221,7 @@ export class Agent {
|
|
|
1209
1221
|
|
|
1210
1222
|
sessionState(session) {
|
|
1211
1223
|
return { sessionId: session.id, configOptions: this.modelConfig(session),
|
|
1212
|
-
modes: { currentModeId: session.mode, availableModes: modes }, models: {
|
|
1224
|
+
modes: { currentModeId: session.mode, availableModes: this.modes }, models: {
|
|
1213
1225
|
currentModelId: session.currentModelId,
|
|
1214
1226
|
availableModels: session.availableModels.map(model => ({
|
|
1215
1227
|
modelId: `${model.provider}/${model.id}`, name: `${model.name || model.id} (${model.provider})`,
|
|
@@ -1263,7 +1275,7 @@ export class Agent {
|
|
|
1263
1275
|
}
|
|
1264
1276
|
|
|
1265
1277
|
modelConfig(session) {
|
|
1266
|
-
return [{ id: 'model', name: '模型', category: 'model', type: 'select',
|
|
1278
|
+
return [{ id: 'model', name: this.chinese ? '模型' : 'Model', category: 'model', type: 'select',
|
|
1267
1279
|
currentValue: session.currentModelId,
|
|
1268
1280
|
...(this.airRecommendedValue ? { _meta: { jetbrains: { air: {
|
|
1269
1281
|
version: 1, recommendedValue: session.recommendedModelId,
|
|
@@ -1271,17 +1283,19 @@ export class Agent {
|
|
|
1271
1283
|
options: session.availableModels.map(model => ({
|
|
1272
1284
|
value: `${model.provider}/${model.id}`, name: `${model.name || model.id} (${model.provider})`,
|
|
1273
1285
|
})),
|
|
1274
|
-
}, { id: 'thinking', name: '推理级别', category: 'thought_level', type: 'select',
|
|
1286
|
+
}, { id: 'thinking', name: this.chinese ? '推理级别' : 'Thinking Level', category: 'thought_level', type: 'select',
|
|
1275
1287
|
currentValue: session.thinkingLevel,
|
|
1276
1288
|
...(this.airRecommendedValue && session.thinkingLevels.includes(session.recommendedThinkingLevel) ? { _meta: { jetbrains: { air: {
|
|
1277
1289
|
version: 1, recommendedValue: session.recommendedThinkingLevel,
|
|
1278
1290
|
} } } } : {}),
|
|
1279
1291
|
options: session.thinkingLevels.map(level => ({ value: level,
|
|
1280
|
-
name: (
|
|
1292
|
+
name: (this.chinese
|
|
1293
|
+
? { off: '关闭', minimal: '最低', low: '低', medium: '中', high: '高', xhigh: '极高', max: '最高' }
|
|
1294
|
+
: { off: 'Off', minimal: 'Minimal', low: 'Low', medium: 'Medium', high: 'High', xhigh: 'Extra High', max: 'Max' })[level] || level,
|
|
1281
1295
|
})),
|
|
1282
|
-
}, { id: 'mode', name: '权限模式', category: 'mode', type: 'select',
|
|
1296
|
+
}, { id: 'mode', name: this.chinese ? '权限模式' : 'Permission Mode', category: 'mode', type: 'select',
|
|
1283
1297
|
currentValue: session.mode,
|
|
1284
|
-
options: modes.map(mode => ({ value: mode.id, name: mode.name, description: mode.description })),
|
|
1298
|
+
options: this.modes.map(mode => ({ value: mode.id, name: mode.name, description: mode.description })),
|
|
1285
1299
|
}];
|
|
1286
1300
|
}
|
|
1287
1301
|
|
|
@@ -1609,7 +1623,7 @@ export class Agent {
|
|
|
1609
1623
|
return;
|
|
1610
1624
|
}
|
|
1611
1625
|
const call = JSON.parse(event.message);
|
|
1612
|
-
if (
|
|
1626
|
+
if (session.turn?.cancelled) {
|
|
1613
1627
|
session.pi.send({ type: 'extension_ui_response', id: event.id, cancelled: true });
|
|
1614
1628
|
return;
|
|
1615
1629
|
}
|
|
@@ -1625,6 +1639,10 @@ export class Agent {
|
|
|
1625
1639
|
this.update(session, { sessionUpdate: 'tool_call_update', toolCallId: call.toolCallId, status: 'in_progress' });
|
|
1626
1640
|
return;
|
|
1627
1641
|
}
|
|
1642
|
+
if (!session.turn) {
|
|
1643
|
+
session.pi.send({ type: 'extension_ui_response', id: event.id, cancelled: true });
|
|
1644
|
+
return;
|
|
1645
|
+
}
|
|
1628
1646
|
const tool = session.tools.get(call.toolCallId) ?? this.tool(session, call);
|
|
1629
1647
|
const id = `acp-permission-${++this.sequence}`;
|
|
1630
1648
|
const decision = new Promise(resolve => this.permissions.set(id, { session, resolve, kind: 'permission' }));
|