jetbrains-pi-acp 0.1.2 → 0.1.4
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 +6 -4
- package/README.zh-CN.md +7 -5
- package/extensions/acp.ts +27 -22
- package/package.json +2 -2
- package/scripts/config.js +2 -1
- package/src/agent.js +291 -1065
- package/src/commands.js +107 -0
- package/src/convert.js +55 -0
- package/src/i18n.js +418 -0
- package/src/index.js +6 -5
- package/src/interactions.js +221 -0
- package/src/jsonl.js +3 -1
- package/src/pi.js +8 -7
- package/src/protocol.js +29 -27
- package/src/session-fork.js +127 -0
- package/src/session-index.js +160 -0
- package/src/shared.js +18 -0
- package/src/task-worker.js +2 -1
- package/src/tasks.js +30 -25
- package/src/terminal-input.js +11 -9
- package/src/transcript.js +188 -0
package/README.md
CHANGED
|
@@ -4,14 +4,14 @@
|
|
|
4
4
|
|
|
5
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.
|
|
6
6
|
|
|
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.
|
|
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.3.0`; no global pi installation or build step is required.
|
|
8
8
|
|
|
9
9
|
## Quick start
|
|
10
10
|
|
|
11
11
|
1. Sign in to your model provider from a terminal:
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
npx --yes jetbrains-pi-acp@0.1.
|
|
14
|
+
npx --yes jetbrains-pi-acp@0.1.4 --setup
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
In pi, run `/login`, complete sign-in, then exit pi and return to the IDE. Credentials are managed by pi.
|
|
@@ -23,7 +23,7 @@ Requires **Node.js 22.19.0+** and a JetBrains IDE with ACP support, such as Inte
|
|
|
23
23
|
"agent_servers": {
|
|
24
24
|
"pi": {
|
|
25
25
|
"command": "npx",
|
|
26
|
-
"args": ["--yes", "jetbrains-pi-acp@0.1.
|
|
26
|
+
"args": ["--yes", "jetbrains-pi-acp@0.1.4"],
|
|
27
27
|
"use_idea_mcp": true,
|
|
28
28
|
"use_custom_mcp": true
|
|
29
29
|
}
|
|
@@ -40,6 +40,7 @@ The IDE must be able to find `npx`; use its absolute path if needed. Menu locati
|
|
|
40
40
|
- ACP v1 and v2 draft support, with streaming responses, reasoning, tool status, terminal output, and usage reporting.
|
|
41
41
|
- Native pi session history: list, load, resume, rename, fork, and delete sessions.
|
|
42
42
|
- Model and reasoning-level selection, plus full-access, standard, read-only, and plan modes.
|
|
43
|
+
- UI text (config names, mode and command descriptions, permission options, errors) follows the system language: Chinese for `zh*` locales, English otherwise, based on the first non-empty `LC_ALL`, `LC_MESSAGES`, or `LANG`, falling back to the system locale. Tool descriptions and system prompts sent to the model are always English. Restart the adapter after changing the language.
|
|
43
44
|
- File diffs for built-in `edit`/`write` tools, with optional Air Git patches and per-turn file reports.
|
|
44
45
|
- stdio/HTTP MCP forwarding, including IDE tools; subagents and background shell tasks with targeted cancellation.
|
|
45
46
|
- Steering, terminal stdin, forms, and URL interactions when supported by the client. Air v1 provides `/stdin text` and `/eof` during command execution.
|
|
@@ -51,7 +52,7 @@ The IDE must be able to find `npx`; use its absolute path if needed. Menu locati
|
|
|
51
52
|
Append pi options after `--` in the agent's `args`, for example:
|
|
52
53
|
|
|
53
54
|
```json
|
|
54
|
-
["--yes", "jetbrains-pi-acp@0.1.
|
|
55
|
+
["--yes", "jetbrains-pi-acp@0.1.4", "--", "--model", "provider/model-id", "--thinking", "medium"]
|
|
55
56
|
```
|
|
56
57
|
|
|
57
58
|
Use configuration options here, not initial prompts, interactive mode, or session-selection arguments. The adapter manages RPC mode and sessions.
|
|
@@ -66,6 +67,7 @@ Restart the agent connection after updating the adapter or changing startup opti
|
|
|
66
67
|
|
|
67
68
|
- 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
69
|
- Terminal input uses stdin pipes, not a PTY. Full-screen apps and TTY password prompts are unsupported; background tasks have no stdin input.
|
|
70
|
+
- Only commands the model runs with `interactive: true` accept input. Other commands get an empty stdin (`/dev/null`) like native pi, so commands that read stdin by mistake exit instead of hanging.
|
|
69
71
|
- 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
72
|
- MCP SSE, audio, and binary embedded resources are unsupported. Image support depends on the model.
|
|
71
73
|
- ACP v2 follows a draft schema. Client UI behavior, real provider sign-in, and Windows hardware still need verification.
|
package/README.zh-CN.md
CHANGED
|
@@ -4,14 +4,14 @@
|
|
|
4
4
|
|
|
5
5
|
通过 ACP 将 [pi coding agent](https://github.com/earendil-works/pi) 连接到 JetBrains IDE,支持历史会话、权限控制、文件 diff、子 agent 和后台任务。
|
|
6
6
|
|
|
7
|
-
需要 **Node.js 22.19.0+** 和支持 ACP 的 JetBrains IDE,例如安装了 Air 插件(`com.intellij.air`)的 IntelliJ IDEA。npm 包自带 pi `1.
|
|
7
|
+
需要 **Node.js 22.19.0+** 和支持 ACP 的 JetBrains IDE,例如安装了 Air 插件(`com.intellij.air`)的 IntelliJ IDEA。npm 包自带 pi `1.3.0`,无需全局安装 pi,也无需构建。
|
|
8
8
|
|
|
9
9
|
## 快速开始
|
|
10
10
|
|
|
11
11
|
1. 在终端中登录模型提供商:
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
npx --yes jetbrains-pi-acp@0.1.
|
|
14
|
+
npx --yes jetbrains-pi-acp@0.1.4 --setup
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
在 pi 中运行 `/login`,完成登录后退出 pi 并返回 IDE。凭据由 pi 管理。
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"agent_servers": {
|
|
24
24
|
"pi": {
|
|
25
25
|
"command": "npx",
|
|
26
|
-
"args": ["--yes", "jetbrains-pi-acp@0.1.
|
|
26
|
+
"args": ["--yes", "jetbrains-pi-acp@0.1.4"],
|
|
27
27
|
"use_idea_mcp": true,
|
|
28
28
|
"use_custom_mcp": true
|
|
29
29
|
}
|
|
@@ -40,7 +40,8 @@ IDE 必须能找到 `npx`,必要时使用其绝对路径。菜单位置随 IDE
|
|
|
40
40
|
- 支持 ACP v1 和 v2 草案,流式展示回答、思考、工具状态、终端输出和用量。
|
|
41
41
|
- 每轮汇总模型请求与自动压缩的输入、输出及缓存读写 token,取消仍保留已发生用量;通过 `usage_update.cost` 发布 pi 记录的会话累计美元费用,恢复历史后继续累计。可用 `/status` 查看累计 token、费用和上下文用量。
|
|
42
42
|
- 使用 pi 原生历史,支持会话列表、加载、恢复、改名、分叉和删除。
|
|
43
|
-
-
|
|
43
|
+
- 支持切换模型与推理级别,以及完全访问、标准访问、只读和规划模式。
|
|
44
|
+
- 界面文案(配置名称、模式说明、命令说明、授权选项、报错等)按系统语言自动切换中英文,给模型看的工具描述和系统提示固定使用英文。语言判断依次读取非空的 `LC_ALL`、`LC_MESSAGES`、`LANG`,未设置时使用系统默认语言;中文使用中文,其余语言使用英文。更改系统语言后需重启适配器。
|
|
44
45
|
- 展示内置 `edit/write` 工具的文件 diff,可协商 Air Git patch 和每轮文件报告。
|
|
45
46
|
- 转发 stdio/HTTP MCP 配置及 IDE 工具;支持子 agent、后台 shell 任务和定向停止。
|
|
46
47
|
- 按客户端能力提供 steering、终端 stdin、表单和 URL 交互。Air v1 可在命令执行期间发送 `/stdin 文本` 和 `/eof`。
|
|
@@ -52,7 +53,7 @@ IDE 必须能找到 `npx`,必要时使用其绝对路径。菜单位置随 IDE
|
|
|
52
53
|
在 Agent 的 `args` 末尾添加 `--`,随后传递 pi 参数,例如:
|
|
53
54
|
|
|
54
55
|
```json
|
|
55
|
-
["--yes", "jetbrains-pi-acp@0.1.
|
|
56
|
+
["--yes", "jetbrains-pi-acp@0.1.4", "--", "--model", "provider/model-id", "--thinking", "medium"]
|
|
56
57
|
```
|
|
57
58
|
|
|
58
59
|
这里只传配置选项,不传初始提示、交互模式或会话选择参数。RPC 模式和会话由适配器管理。
|
|
@@ -67,6 +68,7 @@ IDE 必须能找到 `npx`,必要时使用其绝对路径。菜单位置随 IDE
|
|
|
67
68
|
|
|
68
69
|
- 工具由本机 pi 执行,尚未接入客户端 `fs/*` 和 `terminal/*` 执行接口。让 Agent 读取或修改文件前,请保存编辑器内容。
|
|
69
70
|
- 终端输入使用 stdin 管道,不提供 PTY,不支持全屏终端应用和 TTY 密码提示;后台任务不支持 stdin 输入。
|
|
71
|
+
- 只有模型以 `interactive: true` 调用的命令可以接收输入;其他命令与 pi 原生一致,stdin 为空(`/dev/null`),误读 stdin 的命令会直接结束,不会卡住。
|
|
70
72
|
- diff 有大小限制,二进制文件或旧历史可能无法展示。每轮文件报告的单次快照最多扫描 20,000 项或两秒,无法准确区分外部并发修改。
|
|
71
73
|
- 不支持 MCP SSE、音频和二进制内嵌资源,图片支持取决于模型。
|
|
72
74
|
- ACP v2 仍为草案,客户端界面展示、真实提供商账号登录及 Windows 实机仍需验证。
|
package/extensions/acp.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { createBashToolDefinition, createPowerShellToolDefinition, getShellConfi
|
|
|
7
7
|
import { readJsonl, writeJsonl } from '../src/jsonl.js';
|
|
8
8
|
import { TerminalProcesses } from '../src/terminal-input.js';
|
|
9
9
|
import { fileText, MAX_DIFF_BYTES, toolPath } from '../src/file-diff.js';
|
|
10
|
+
import { t } from '../src/i18n.js';
|
|
10
11
|
|
|
11
12
|
// 由适配器创建的临时配置,避免覆盖用户或项目的 MCP 设置。
|
|
12
13
|
export default function (pi: any) {
|
|
@@ -22,14 +23,14 @@ export default function (pi: any) {
|
|
|
22
23
|
pendingTasks.set(id, { resolve, reject });
|
|
23
24
|
if (!inputChannel || inputChannel.destroyed) {
|
|
24
25
|
pendingTasks.delete(id);
|
|
25
|
-
reject(new Error('
|
|
26
|
+
reject(new Error(t('taskChannelDisconnected')));
|
|
26
27
|
return;
|
|
27
28
|
}
|
|
28
29
|
writeJsonl(inputChannel, { type: 'acp_task_request', id, action, toolCallId, params });
|
|
29
30
|
});
|
|
30
31
|
const closeChannel = () => {
|
|
31
32
|
shared.terminals.close();
|
|
32
|
-
for (const pending of pendingTasks.values()) pending.reject(new Error('
|
|
33
|
+
for (const pending of pendingTasks.values()) pending.reject(new Error(t('taskChannelClosed')));
|
|
33
34
|
pendingTasks.clear();
|
|
34
35
|
};
|
|
35
36
|
for (const [event, action] of [
|
|
@@ -78,13 +79,17 @@ export default function (pi: any) {
|
|
|
78
79
|
const createTool = name === 'bash' ? createBashToolDefinition : createPowerShellToolDefinition;
|
|
79
80
|
const definition = createTool(process.cwd());
|
|
80
81
|
pi.registerTool({ ...definition,
|
|
82
|
+
parameters: Type.Object({ ...definition.parameters.properties,
|
|
83
|
+
interactive: Type.Optional(Type.Boolean({ description: 'Set true only when the command must read input typed by the user on stdin. ' +
|
|
84
|
+
'Default false: stdin is empty (/dev/null) and no input can be sent while it runs.' })) }),
|
|
81
85
|
execute: async (id: string, args: any, signal: AbortSignal, update: any, ctx: any) => {
|
|
86
|
+
const interactive = args.interactive === true;
|
|
82
87
|
const settings = SettingsManager.create(ctx.cwd);
|
|
83
88
|
const tool = createTool(ctx.cwd, {
|
|
84
89
|
commandPrefix: settings.getShellCommandPrefix(),
|
|
85
90
|
operations: { exec: (command: string, cwd: string, options: any) => terminals.exec(id,
|
|
86
91
|
name === 'bash' ? getShellConfig(settings.getShellPath()) : getPowerShellConfig(),
|
|
87
|
-
command, cwd, options, () => ctx.ui.notify(`PI_ACP_TERMINAL_READY:${id}`, 'info')) },
|
|
92
|
+
command, cwd, { ...options, interactive }, () => ctx.ui.notify(`PI_ACP_TERMINAL_READY:${id}`, 'info')) },
|
|
88
93
|
});
|
|
89
94
|
return tool.execute(id, args, signal, update, ctx);
|
|
90
95
|
},
|
|
@@ -92,12 +97,12 @@ export default function (pi: any) {
|
|
|
92
97
|
}
|
|
93
98
|
}
|
|
94
99
|
for (const [name, description, parameters] of [
|
|
95
|
-
['spawn_agent', '
|
|
100
|
+
['spawn_agent', 'Start a subagent with its own context in the background. Returns a taskId; query it with task_status and stop it with stop_task.',
|
|
96
101
|
Type.Object({ prompt: Type.String({ minLength: 1 }), name: Type.Optional(Type.String()) })],
|
|
97
|
-
['background_shell', '
|
|
102
|
+
['background_shell', 'Start a background shell command. Returns a taskId; read output with task_status and stop the whole process group with stop_task.',
|
|
98
103
|
Type.Object({ command: Type.String({ minLength: 1 }), timeout: Type.Optional(Type.Number({ minimum: 0.001 })) })],
|
|
99
|
-
['task_status', '
|
|
100
|
-
['stop_task', '
|
|
104
|
+
['task_status', 'Get the status and recent output of a background command or subagent created in this session.', Type.Object({ taskId: Type.String() })],
|
|
105
|
+
['stop_task', 'Stop only the background command or subagent with the given taskId, leaving other tasks running.', Type.Object({ taskId: Type.String() })],
|
|
101
106
|
] as const) {
|
|
102
107
|
pi.registerTool({ name, label: name, description, parameters,
|
|
103
108
|
execute: async (id: string, args: any, _signal: AbortSignal, _update: any, ctx: any) => {
|
|
@@ -124,40 +129,40 @@ export default function (pi: any) {
|
|
|
124
129
|
event.systemPromptOptions.sections = {
|
|
125
130
|
...sections,
|
|
126
131
|
...(config.customInstructions ? { acp_client_instructions: config.customInstructions } : {}),
|
|
127
|
-
...(planMode ? { acp_plan_mode: '
|
|
128
|
-
...(goal?.status === 'active' ? { acp_goal:
|
|
132
|
+
...(planMode ? { acp_plan_mode: 'Only analyze the problem and produce an actionable plan. Do not modify files, run commands, or use other tools with side effects.' } : {}),
|
|
133
|
+
...(goal?.status === 'active' ? { acp_goal: `Long-term goal for this session: ${goal.objective}` } : {}),
|
|
129
134
|
};
|
|
130
135
|
});
|
|
131
136
|
pi.registerCommand('__acp_mode', {
|
|
132
|
-
description: '
|
|
137
|
+
description: 'Toggle ACP plan mode',
|
|
133
138
|
handler: async (args: string) => { planMode = args.trim() === 'plan'; },
|
|
134
139
|
});
|
|
135
140
|
pi.registerCommand('__acp_fork', {
|
|
136
|
-
description: '
|
|
141
|
+
description: 'Fork the ACP session after a given history entry',
|
|
137
142
|
handler: async (args: string, ctx: any) => {
|
|
138
143
|
await ctx.fork(JSON.parse(args), { position: 'at' });
|
|
139
144
|
},
|
|
140
145
|
});
|
|
141
146
|
pi.registerCommand('__acp_goal', {
|
|
142
|
-
description: '
|
|
147
|
+
description: 'Sync the ACP goal',
|
|
143
148
|
handler: async (args: string) => {
|
|
144
149
|
const value = JSON.parse(args);
|
|
145
150
|
if (value !== null && (typeof value?.objective !== 'string' ||
|
|
146
|
-
!['active', 'paused'].includes(value.status))) throw new Error('
|
|
151
|
+
!['active', 'paused'].includes(value.status))) throw new Error(t('goalInvalid'));
|
|
147
152
|
pi.appendEntry('acp_goal', value);
|
|
148
153
|
goal = value;
|
|
149
154
|
},
|
|
150
155
|
});
|
|
151
156
|
pi.registerTool({
|
|
152
|
-
name: 'update_plan', label: '
|
|
153
|
-
description: '
|
|
157
|
+
name: 'update_plan', label: t('updatePlanLabel'),
|
|
158
|
+
description: 'Update the execution plan and progress of the current task with structured steps. Only records the plan; performs no file or command operations.',
|
|
154
159
|
parameters: Type.Object({ entries: Type.Array(Type.Object({
|
|
155
160
|
content: Type.String({ minLength: 1, maxLength: 10000 }),
|
|
156
161
|
priority: Type.Union([Type.Literal('high'), Type.Literal('medium'), Type.Literal('low')]),
|
|
157
162
|
status: Type.Union([Type.Literal('pending'), Type.Literal('in_progress'), Type.Literal('completed')]),
|
|
158
163
|
}), { maxItems: 100 }) }),
|
|
159
164
|
execute: async (_id: string, input: any) => ({
|
|
160
|
-
content: [{ type: 'text', text: '
|
|
165
|
+
content: [{ type: 'text', text: 'Plan updated' }], details: { acpPlan: input.entries },
|
|
161
166
|
}),
|
|
162
167
|
});
|
|
163
168
|
const messageIds = new Map<string, string>();
|
|
@@ -178,7 +183,7 @@ export default function (pi: any) {
|
|
|
178
183
|
pi.registerMcpServer(name, server);
|
|
179
184
|
}
|
|
180
185
|
pi.registerCommand('__acp_check', {
|
|
181
|
-
description: '
|
|
186
|
+
description: 'Check the ACP adapter connection',
|
|
182
187
|
handler: async (_args: string, ctx: any) => {
|
|
183
188
|
goal = ctx.sessionManager.getBranch().findLast((entry: any) =>
|
|
184
189
|
entry.type === 'custom' && entry.customType === 'acp_goal')?.data ?? null;
|
|
@@ -187,11 +192,11 @@ export default function (pi: any) {
|
|
|
187
192
|
},
|
|
188
193
|
});
|
|
189
194
|
pi.registerCommand('__acp_archive', {
|
|
190
|
-
description: '
|
|
195
|
+
description: 'Sync the ACP archive state',
|
|
191
196
|
handler: async (args: string) => { pi.appendEntry('acp_archive', args.trim() === 'true'); },
|
|
192
197
|
});
|
|
193
198
|
pi.registerCommand('__acp_list', {
|
|
194
|
-
description: '
|
|
199
|
+
description: 'List pi sessions that ACP can resume',
|
|
195
200
|
handler: async (args: string, ctx: any) => {
|
|
196
201
|
const directory = config.sessionDir || process.env.PI_CODING_AGENT_SESSION_DIR ||
|
|
197
202
|
SettingsManager.create(ctx.cwd).getSessionDir();
|
|
@@ -219,14 +224,14 @@ export default function (pi: any) {
|
|
|
219
224
|
});
|
|
220
225
|
pi.on('tool_call', async (event: any, ctx: any) => {
|
|
221
226
|
if (['read', 'grep', 'find', 'ls', 'update_plan', 'task_status'].includes(event.toolName)) return;
|
|
222
|
-
if (planMode) return { block: true, reason: '
|
|
227
|
+
if (planMode) return { block: true, reason: t('planModeBlocked') };
|
|
223
228
|
try {
|
|
224
229
|
const approved = await ctx.ui.confirm('PI_ACP_PERMISSION', JSON.stringify({
|
|
225
230
|
toolCallId: event.toolCallId, toolName: event.toolName, args: event.input,
|
|
226
231
|
}));
|
|
227
|
-
if (!approved) return { block: true, reason: '
|
|
232
|
+
if (!approved) return { block: true, reason: t('userDenied') };
|
|
228
233
|
} catch {
|
|
229
|
-
return { block: true, reason: '
|
|
234
|
+
return { block: true, reason: t('permissionChannelUnavailable') };
|
|
230
235
|
}
|
|
231
236
|
if (!['edit', 'write'].includes(event.toolName) || typeof event.input?.path !== 'string') return;
|
|
232
237
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jetbrains-pi-acp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "连接 JetBrains IDE 与 pi coding agent 的 ACP 适配器",
|
|
6
6
|
"bin": {
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"examples/"
|
|
32
32
|
],
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@earendil-works/pi-coding-agent": "1.
|
|
34
|
+
"@earendil-works/pi-coding-agent": "1.3.0",
|
|
35
35
|
"typebox": "1.3.27"
|
|
36
36
|
},
|
|
37
37
|
"license": "MIT"
|
package/scripts/config.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { accessSync, constants } from 'node:fs';
|
|
2
2
|
import { delimiter, isAbsolute, join, resolve } from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { t } from '../src/i18n.js';
|
|
4
5
|
|
|
5
6
|
const requested = process.argv[2] || process.env.PI_ACP_PI_COMMAND;
|
|
6
7
|
const candidates = !requested ? [] : isAbsolute(requested) ? [requested]
|
|
@@ -9,7 +10,7 @@ const command = candidates.find(path => {
|
|
|
9
10
|
try { accessSync(path, constants.X_OK); return true; } catch { return false; }
|
|
10
11
|
});
|
|
11
12
|
if (requested && !command) {
|
|
12
|
-
process.stderr.write('
|
|
13
|
+
process.stderr.write(`${t('piNotFound')}\n`);
|
|
13
14
|
process.exit(1);
|
|
14
15
|
}
|
|
15
16
|
console.log(JSON.stringify({ agent_servers: { pi: {
|