farming-code 2.2.28 → 2.2.29

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.
Files changed (74) hide show
  1. package/README.md +9 -31
  2. package/README.zh_cn.md +8 -30
  3. package/THIRD_PARTY_NOTICES.md +25 -6
  4. package/backend/acp/packaged-claude-acp.js +13 -0
  5. package/backend/acp/packaged-codex-acp.js +2 -2
  6. package/backend/acp-runtime.js +37 -2
  7. package/backend/acp-transcript.js +174 -9
  8. package/backend/agent-manager.js +27 -2
  9. package/backend/config-manager.js +44 -0
  10. package/backend/data/LICENSE.agent-browser +201 -0
  11. package/backend/data/runtime-dependency-manifest.json +199 -0
  12. package/backend/data/runtime-dependency-sources.json +5 -0
  13. package/backend/executable-discovery.js +7 -0
  14. package/backend/farming-agent-bootstrap.zh_cn.md +4 -2
  15. package/backend/farming-app-cli.js +160 -64
  16. package/backend/farming-cli.js +5 -3
  17. package/backend/index-html.js +13 -0
  18. package/backend/main-agent-skills.js +6 -10
  19. package/backend/npm-update-helper.js +43 -6
  20. package/backend/provider-adapters.js +10 -0
  21. package/backend/runtime-dependency-manager.js +759 -0
  22. package/backend/runtime-executable-invocation.js +20 -0
  23. package/backend/server-process-identity.js +47 -0
  24. package/backend/server.js +140 -157
  25. package/backend/slash-command-discovery.js +138 -1
  26. package/backend/storage-layout.js +15 -0
  27. package/backend/update-service.js +27 -1
  28. package/config/farming.install.env.example +4 -0
  29. package/dist/acp/LICENSE.claude-agent-acp +191 -0
  30. package/dist/acp/LICENSE.claude-agent-sdk +1 -0
  31. package/dist/acp/claude-agent-acp-0.59.0.mjs +41606 -0
  32. package/dist/assets/{App-CM7Cy-l4.css → App-BKNUILuL.css} +1 -1
  33. package/dist/assets/App-Ex2oVVXO.js +693 -0
  34. package/dist/assets/{FileEditorMarkdownPreview-DYOMm-b1.js → FileEditorMarkdownPreview-Cs9cLv8a.js} +1 -1
  35. package/dist/assets/FileEditorPane-CNtuGjQ3.js +2 -0
  36. package/dist/assets/IconGlyphs-BClgag6E.js +1 -0
  37. package/dist/assets/ProjectFilesSection-Cf2BEcAN.js +12 -0
  38. package/dist/assets/ReviewPage-BRuxFaH5.js +3 -0
  39. package/dist/assets/code-dark-BEZTQwYZ.css +1 -0
  40. package/dist/assets/{index-BzQO3pUr.js → index-CQNaZc3l.js} +2 -2
  41. package/dist/assets/main-DK9Sy7-c.css +1 -0
  42. package/dist/assets/{workspace-editor-monaco-tXA-udGx.js → workspace-editor-monaco-CJNwbpCy.js} +1 -1
  43. package/dist/assets/workspace-editor-monaco-CO9w8YV0.js +1 -0
  44. package/dist/assets/workspace-view-state-BjKPYp0E.js +7 -0
  45. package/dist/index.html +32 -3
  46. package/extensions/browser/backend/agent-browser-runtime.js +1041 -0
  47. package/extensions/browser/backend/browser-mcp-server.js +352 -0
  48. package/extensions/browser/backend/browser-resource-manager.js +899 -114
  49. package/extensions/browser/backend/browser-resource-store.js +47 -6
  50. package/extensions/browser/backend/browser-router.js +7 -2
  51. package/extensions/browser/backend/executable-discovery.js +146 -13
  52. package/extensions/browser/bin/farming-browser +384 -30
  53. package/extensions/browser/frontend/BrowserSidebarPortals.tsx +17 -19
  54. package/extensions/browser/frontend/BrowserViewer.tsx +217 -46
  55. package/extensions/browser/frontend/browser.css +68 -17
  56. package/extensions/browser/frontend/types.ts +6 -0
  57. package/extensions/browser/frontend/useBrowserResources.ts +10 -3
  58. package/index.html +31 -2
  59. package/package.json +10 -6
  60. package/shared/backend-connection-status.d.ts +12 -0
  61. package/shared/backend-connection-status.js +35 -4
  62. package/backend/agent-activity.js +0 -35
  63. package/dist/assets/App-CPG4WcjO.js +0 -658
  64. package/dist/assets/FileEditorPane-DXXq82aM.js +0 -2
  65. package/dist/assets/IconGlyphs-DrdPjYXr.js +0 -1
  66. package/dist/assets/ProjectFilesSection-CP66XVPl.js +0 -12
  67. package/dist/assets/ReviewPage-w21MEPD6.js +0 -3
  68. package/dist/assets/code-dark-BDjwj7Um.css +0 -1
  69. package/dist/assets/main-BlVUXfxa.css +0 -1
  70. package/dist/assets/workspace-editor-monaco-CFEfFNcA.js +0 -1
  71. package/dist/assets/workspace-view-state-BS_V8JVs.js +0 -7
  72. package/extensions/browser/backend/cdp-browser-runtime.js +0 -648
  73. package/extensions/browser/backend/cdp-client.js +0 -167
  74. /package/dist/acp/{codex-acp-1.1.4.js → codex-acp-1.1.4.mjs} +0 -0
package/README.md CHANGED
@@ -55,17 +55,17 @@ Start or resume Codex, Claude Code, OpenCode, Qoder, and other detected coding C
55
55
 
56
56
  Browse, search, and lightly edit Project Files without leaving the current task. Inspect Git Changes, History, Diff, and Blame, then open a commit or working-copy change in Review.
57
57
 
58
- ![Farming Code project files and blame](./docs/products/code/assets/04-files-editor-blame.png)
59
-
60
58
  ### Browser Resources
61
59
 
62
- Each Project can own multiple named Browser Resources. Farming starts a compatible Chromium-based system browser with an isolated profile, streams that exact headless page into the workspace, and sends both human input and Agent actions to the same CDP session. Farming does not download Chromium or depend on Playwright. Agents can discover and operate these resources with `farming browser`; npm installations also expose the `farming-browser` alias.
60
+ Each Project can own multiple named Browser Resources. In **Plugins → Browser**, users choose a discovered Chromium-based system browser or an external loopback CDP endpoint. Farming prepares a pinned `agent-browser` runtime during installation or update, verifies the cache again before startup, and sends both human input and Agent actions to the same session. Farming does not download Chromium or depend on Playwright. Agents discover the live capability with `farming capabilities`, then use the progressively disclosed `farming browser` CLI for navigation, interaction, inspection, debugging, page state, frames/dialogs, and Project-scoped file transfer; npm installations also expose the `farming-browser` alias. See [Farming Browser for Agents](docs/products/code/browser-agent-cli.md).
61
+
62
+ Farming can also connect to an externally managed Chromium CDP endpoint, including one exposed by Docker; see [external CDP browser setup](./docs/products/code/external-cdp-browser.md).
63
63
 
64
64
  This first Browser Viewer targets web pages and simple Agent interaction. It is not a replacement for the browser's full window chrome, DevTools, downloads UI, extensions, or Computer Use.
65
65
 
66
66
  ## Supported Agents
67
67
 
68
- Farming discovers installed CLIs on the host. Codex, Claude Code, OpenCode, and Qoder support both structured Chat and a native Terminal; other detected coding agents use the Terminal path.
68
+ Farming prepares the pinned Codex and Claude executables needed by its structured runtimes during installation or update and verifies them again before startup, while continuing to discover other installed CLIs on the host. Codex, Claude Code, OpenCode, and Qoder support both structured Chat and a native Terminal; other detected coding agents use the Terminal path.
69
69
 
70
70
  | Agent | Structured Chat | Terminal | History / resume |
71
71
  | --- | --- | --- | --- |
@@ -73,10 +73,6 @@ Farming discovers installed CLIs on the host. Codex, Claude Code, OpenCode, and
73
73
  | Claude Code | Yes | Yes | Yes |
74
74
  | OpenCode | Yes | Yes | Yes |
75
75
  | Qoder | Yes | Yes | Yes |
76
- | Qwen Code | — | Yes | CLI-dependent |
77
- | Aider | — | Yes | CLI-dependent |
78
- | GitHub Copilot CLI | — | Yes | CLI-dependent |
79
- | Amazon Q | — | Yes | CLI-dependent |
80
76
  | bash / zsh | — | Yes | No |
81
77
 
82
78
  Farming hosts CLIs that already work on the same machine. It does not replace provider installation, login, or account configuration.
@@ -94,20 +90,16 @@ Development machine
94
90
  ├── coding Agent processes
95
91
  ├── real terminals
96
92
  ├── repositories and project files
97
- └── optional system-browser processes
93
+ └── optional agent-browser sessions
98
94
  ```
99
95
 
100
96
  The browser can disconnect and reconnect without stopping an Agent. A normal Farming server restart can also reconnect supported live terminal sessions. The desktop layout keeps projects, conversations, files, and review together; the mobile layout focuses one conversation, terminal, or file at a time.
101
97
 
102
- <p align="center">
103
- <img src="./docs/products/code/assets/05-mobile-agent-chat.png" alt="Farming Code reconnecting to a running Agent from a phone" width="390">
104
- </p>
105
-
106
98
  ## Farming CRT
107
99
 
108
100
  Farming CRT is an optional keyboard-first, retro control-room interface for scanning many Agents, opening their Chat or Terminal sessions, searching history, and viewing usage telemetry.
109
101
 
110
- ![Farming CRT multi-Agent control room](./docs/products/crt/assets/01-crt-dashboard.png)
102
+ ![Farming CRT multi-agent dashboard](./docs/products/crt/assets/01-crt-dashboard.png)
111
103
 
112
104
  Code and CRT use the same backend Agents and sessions. Switching interfaces does not create a second Agent. Farming Code remains the default interface and the supported phone interface. See the [Farming CRT guide](./docs/products/crt/README.md) for controls and workflows.
113
105
 
@@ -117,25 +109,11 @@ Farming Net is a separate, token-protected directory for Farming deployments. It
117
109
 
118
110
  ## Installation And Updates
119
111
 
120
- The installed `farming` CLI defaults to port `6694`, base path `/farming`, config directory `~/.farming`, and token authentication. Useful daemon commands are:
112
+ Install with the Quick Start command above. For npm installations, **Settings → Updates** prepares a selected release while the current Server keeps running, then applies it after confirmation. The existing startup token is retained.
121
113
 
122
- ```bash
123
- farming status
124
- farming url
125
- farming logs
126
- farming stop
127
- ```
128
-
129
- The startup token is stored in `~/.farming/.session-token` and reused across restarts and upgrades. **Settings → Updates** uses a two-step flow for npm installations: **Prepare** installs the selected version into a separate staging directory while the current server keeps running, then **Restart to update** switches the package directory and restarts Farming only after the user clicks it. If the new server does not start, Farming restores the previous package directory. npm preparation first uses the machine's configured registry and retries the registry shown in Settings only when that registry lacks the selected version. Before preparing or applying an update, Farming verifies that npm targets the same package root that launched the server. GitHub Releases also provide standalone CLI and directory bundles; see [GitHub Releases](https://github.com/zhuwenzhuang/farming/releases) for current artifacts.
130
-
131
- To run from source with the same port and base path:
132
-
133
- ```bash
134
- npm install
135
- PORT=6694 FARMING_BASE_PATH=/farming npm start
136
- ```
114
+ ![Farming npm update settings](./docs/products/code/assets/14-code-settings.png)
137
115
 
138
- For trusted local development only, `npm run start:no-auth` disables token authentication.
116
+ Standalone CLI and directory bundles remain available from [GitHub Releases](https://github.com/zhuwenzhuang/farming/releases).
139
117
 
140
118
  ## Security
141
119
 
package/README.zh_cn.md CHANGED
@@ -55,17 +55,17 @@ Farming Code 是默认的桌面与手机界面。它按项目组织工作,把
55
55
 
56
56
  浏览、搜索并轻量编辑 Project Files,不需要离开当前任务。检查 Git Changes、History、Diff 和 Blame,再把 Commit 或 Working Copy 修改打开到 Review。
57
57
 
58
- ![Farming Code 项目文件与 Blame](./docs/products/code/assets/04-files-editor-blame.png)
59
-
60
58
  ### Browser Resource
61
59
 
62
- 每个 Project 可以拥有多个可命名的 Browser Resource。Farming 使用兼容的 Chromium 系系统浏览器和独立 Profile 启动浏览器,把这个 Headless 页面流式展示在工作区中,并把人的输入和 Agent 操作发送给同一个 CDP Session。Farming 不下载 Chromium,也不依赖 Playwright。Agent 可以通过 `farming browser` 发现并操作这些 Resource;npm 安装还会提供 `farming-browser` 别名。
60
+ 每个 Project 可以拥有多个可命名的 Browser Resource。用户在**插件 → 浏览器**中选择已发现的 Chromium 系统浏览器,或填写外部回环 CDP Endpoint。Farming 在安装或更新阶段准备锁定版本的 `agent-browser` Runtime,并在启动前再次校验 Cache,然后把人的输入和 Agent 操作发送给同一个 Session。Farming 不下载 Chromium,也不依赖 Playwright。Agent 先用 `farming capabilities` 发现实时能力,再通过渐进式披露的 `farming browser` CLI 使用导航、交互、检查、调试、页面状态、Frame/Dialog 与 Project 级文件传输;npm 安装还会提供 `farming-browser` 别名。详见 [Farming Browser Agent CLI](docs/products/code/browser-agent-cli.zh_cn.md)。
61
+
62
+ Farming 也可以连接由管理员独立管理的 Chromium CDP Endpoint,包括 Docker 中暴露的 Chromium;配置方式见[外部 CDP 浏览器指南](./docs/products/code/external-cdp-browser.zh_cn.md)。
63
63
 
64
64
  第一版 Browser Viewer 面向网页展示与简单 Agent 操作,不替代浏览器完整窗口、DevTools、下载界面、浏览器扩展或 Computer Use。
65
65
 
66
66
  ## 支持的 Agent
67
67
 
68
- Farming 会发现开发机上已经安装的 CLI。Codex、Claude Code、OpenCode 和 Qoder 同时支持结构化 Chat 与原生 Terminal;其他检测到的 Coding Agent 使用 Terminal 路径。
68
+ Farming 在安装或更新阶段准备结构化 Runtime 所需的锁定版本 Codex 与 Claude Executable,并在启动前再次校验,同时继续发现开发机上安装的其他 CLI。Codex、Claude Code、OpenCode 和 Qoder 同时支持结构化 Chat 与原生 Terminal;其他检测到的 Coding Agent 使用 Terminal 路径。
69
69
 
70
70
  | Agent | 结构化 Chat | Terminal | History / Resume |
71
71
  | --- | --- | --- | --- |
@@ -73,10 +73,6 @@ Farming 会发现开发机上已经安装的 CLI。Codex、Claude Code、OpenCod
73
73
  | Claude Code | 是 | 是 | 是 |
74
74
  | OpenCode | 是 | 是 | 是 |
75
75
  | Qoder | 是 | 是 | 是 |
76
- | Qwen Code | — | 是 | 取决于 CLI |
77
- | Aider | — | 是 | 取决于 CLI |
78
- | GitHub Copilot CLI | — | 是 | 取决于 CLI |
79
- | Amazon Q | — | 是 | 取决于 CLI |
80
76
  | bash / zsh | — | 是 | 否 |
81
77
 
82
78
  Farming 承载的是已经能在同一台机器正常工作的 CLI,不替代 Provider 的安装、登录和账户配置。
@@ -99,15 +95,11 @@ Farming 承载的是已经能在同一台机器正常工作的 CLI,不替代 P
99
95
 
100
96
  浏览器断开或重新连接不会停止 Agent。Farming Server 正常重启后也可以重新连接受支持的实时 Terminal Session。桌面布局把项目、对话、文件和 Review 放在一起;手机布局一次聚焦一段对话、一个 Terminal 或一个文件。
101
97
 
102
- <p align="center">
103
- <img src="./docs/products/code/assets/05-mobile-agent-chat.png" alt="使用手机重新连接仍在运行的 Farming Code Agent" width="390">
104
- </p>
105
-
106
98
  ## Farming CRT
107
99
 
108
100
  Farming CRT 是可选的键盘优先复古控制室,用来扫视多个 Agent、打开它们的 Chat 或 Terminal、搜索 History,以及查看用量遥测。
109
101
 
110
- ![Farming CRT 多 Agent 控制室](./docs/products/crt/assets/01-crt-dashboard.png)
102
+ ![Farming CRT 多 Agent 仪表盘](./docs/products/crt/assets/01-crt-dashboard.png)
111
103
 
112
104
  Code 与 CRT 使用相同的后端 Agent 和 Session,切换界面不会创建第二个 Agent。Farming Code 仍是默认界面,也是受支持的手机界面。控制方式和完整流程见 [Farming CRT 指南](./docs/products/crt/README.zh_cn.md)。
113
105
 
@@ -117,25 +109,11 @@ Farming Net 是独立、带 Token 鉴权的 Farming 部署目录。它提供一
117
109
 
118
110
  ## 安装与更新
119
111
 
120
- 安装后的 `farming` CLI 默认端口是 `6694`,Base Path 是 `/farming`,配置目录是 `~/.farming`,Token 鉴权默认开启。常用 Daemon 命令:
112
+ 按上面的快速开始命令安装即可。对于 npm 安装,**Settings → Updates** 会在当前 Server 继续运行时准备所选版本,确认后再应用更新;原有启动 Token 保持不变。
121
113
 
122
- ```bash
123
- farming status
124
- farming url
125
- farming logs
126
- farming stop
127
- ```
128
-
129
- 启动 Token 存放在 `~/.farming/.session-token`,重启和升级会继续复用。**Settings → Updates** 为 npm 安装提供两阶段更新:点击**准备**后,所选版本会安装到独立 staging 目录,当前服务继续运行;准备完成后,只有用户点击**重启并应用**,Farming 才会切换包目录并重启。如果新服务启动失败,Farming 会恢复旧包目录。npm 准备阶段会先使用机器配置的 registry;只有该 registry 缺少所选版本时,才会回退到 Settings 展示版本所用的 registry。准备和应用更新前,Farming 都会校验 npm 目标包根目录与启动当前服务的包根目录一致。GitHub Releases 也提供独立 CLI 和目录 Bundle;当前产物见 [GitHub Releases](https://github.com/zhuwenzhuang/farming/releases)。
130
-
131
- 从源码运行,并使用相同的端口与 Base Path:
132
-
133
- ```bash
134
- npm install
135
- PORT=6694 FARMING_BASE_PATH=/farming npm start
136
- ```
114
+ ![Farming npm 更新设置](./docs/products/code/assets/14-code-settings.png)
137
115
 
138
- 只有在可信本地开发环境中,才可以用 `npm run start:no-auth` 关闭 Token 鉴权。
116
+ 独立 CLI 和目录 Bundle 仍可从 [GitHub Releases](https://github.com/zhuwenzhuang/farming/releases) 下载。
139
117
 
140
118
  ## 安全
141
119
 
@@ -15,8 +15,6 @@ preserved in bundled `node_modules` when bundled dependencies are enabled.
15
15
  | Package | Version | License | Purpose |
16
16
  | --- | --- | --- | --- |
17
17
  | `@agentclientprotocol/sdk` | 1.2.1 | Apache-2.0 | ACP JSON-RPC client and protocol types |
18
- | `@agentclientprotocol/codex-acp` | 1.1.4 | Apache-2.0 | Codex ACP adapter |
19
- | `@agentclientprotocol/claude-agent-acp` | 0.59.0 | Apache-2.0 | Claude Code ACP adapter |
20
18
  | `@modelcontextprotocol/sdk` | 1.29.0 | MIT | Browser Extension MCP server and stdio transport |
21
19
  | `@xterm/addon-clipboard` | 0.2.0 | MIT | Browser terminal clipboard integration |
22
20
  | `@xterm/addon-fit` | 0.11.0 | MIT | Browser terminal sizing |
@@ -47,12 +45,32 @@ preserved in bundled `node_modules` when bundled dependencies are enabled.
47
45
  | `remark-gfm` | 4.0.1 | MIT | GitHub Flavored Markdown support |
48
46
  | `remark-math` | 6.0.0 | MIT | Markdown math syntax support |
49
47
  | `ripgrep` | 0.3.1 | MIT | Node wrapper for file search support |
48
+ | `tar` | 7.5.19 | BlueOak-1.0.0 | Safe extraction of version-locked startup dependency archives |
50
49
  | `ws` | 8.21.0 | MIT | WebSocket server/client support |
51
50
  | `yaml` | 2.9.0 | ISC | YAML parsing |
52
51
  | `zod` | 3.25.76 | MIT | MCP tool input schema validation |
53
52
 
54
53
  ## Vendored Assets
55
54
 
55
+ Farming builds version- and SHA-256-locked ACP runtime files from these exact
56
+ development dependencies and ships their license texts beside the runtime
57
+ files under `dist/acp/`:
58
+
59
+ | Package | Version | License | Purpose |
60
+ | --- | --- | --- | --- |
61
+ | `@agentclientprotocol/codex-acp` | 1.1.4 | Apache-2.0 | Reviewed and patched Codex ACP adapter |
62
+ | `@openai/codex` | 0.144.6 | Apache-2.0 | Codex executable discovery and launch bridge; platform CLI binaries are excluded |
63
+ | `@agentclientprotocol/claude-agent-acp` | 0.59.0 | Apache-2.0 | Claude Code ACP adapter |
64
+ | `@anthropic-ai/claude-agent-sdk` | 0.3.207 | Anthropic commercial terms | Claude Agent SDK bridge; platform CLI binaries are excluded |
65
+
66
+ Before starting a fresh Server, Farming may prepare the exact Codex CLI
67
+ 0.144.6, Claude Agent SDK CLI package 0.3.207, and `agent-browser` 0.32.3
68
+ platform artifacts in its private runtime cache. They are not part of the npm
69
+ or application package and remain independent works under their own terms.
70
+ Farming accepts only the version-locked sources and integrity values declared
71
+ by its release manifest. The `agent-browser` license text is included at
72
+ `backend/data/LICENSE.agent-browser`.
73
+
56
74
  Farming vendors `ghostty-web` distribution files under
57
75
  `frontend/vendor/ghostty-web` for the optional Ghostty debug renderer. The
58
76
  source package is `ghostty-web` 0.4.0, licensed under MIT. Its license is kept
@@ -114,10 +132,11 @@ their code.
114
132
 
115
133
  Farming can launch or observe coding-agent CLIs installed by the user, including
116
134
  OpenCode and the terminal forms of OpenAI Codex CLI and Anthropic Claude Code.
117
- The optional structured ACP runtime includes the pinned Codex and Claude ACP
118
- adapter dependency trees, including the provider SDK/runtime packages required
119
- by those adapters. Those packages remain separate projects governed by their
120
- own terms and licenses; package-level license files remain in `node_modules`.
135
+ The structured ACP runtime includes reviewed Codex and Claude adapter code, but
136
+ Farming does not bundle a platform Codex or Claude Code CLI inside its package.
137
+ The adapters use the exact system or verified startup-cache executable prepared
138
+ by the Farming launcher. Those tools remain separate projects governed by their
139
+ own terms and licenses.
121
140
 
122
141
  Farming is not affiliated with, sponsored by, or endorsed by OpenAI, Anthropic,
123
142
  Microsoft, or the maintainers of the third-party projects listed above.
@@ -0,0 +1,13 @@
1
+ const PACKAGED_CLAUDE_ACP_ARG = '--farming-claude-acp';
2
+
3
+ async function runPackagedClaudeAcp() {
4
+ if (!process.pkg) {
5
+ throw new Error('The packaged Claude ACP entry is available only in a standalone Farming CLI');
6
+ }
7
+ throw new Error('The standalone Farming CLI omitted its embedded Claude ACP runtime');
8
+ }
9
+
10
+ module.exports = {
11
+ PACKAGED_CLAUDE_ACP_ARG,
12
+ runPackagedClaudeAcp,
13
+ };
@@ -1,10 +1,10 @@
1
1
  const PACKAGED_CODEX_ACP_ARG = '--farming-codex-acp';
2
2
 
3
- function runPackagedCodexAcp() {
3
+ async function runPackagedCodexAcp() {
4
4
  if (!process.pkg) {
5
5
  throw new Error('The packaged Codex ACP entry is available only in a standalone Farming CLI');
6
6
  }
7
- require('../../dist/acp/codex-acp-1.1.4.js');
7
+ throw new Error('The standalone Farming CLI omitted its embedded Codex ACP runtime');
8
8
  }
9
9
 
10
10
  module.exports = {
@@ -12,6 +12,7 @@ const { AcpSessionState } = require('./acp-session-state');
12
12
  const { readCodexHistoryImageData } = require('./codex-transcript');
13
13
  const { AcpClientFileSystem, AcpClientTerminalManager } = require('./acp/client-services');
14
14
  const { PACKAGED_CODEX_ACP_ARG } = require('./acp/packaged-codex-acp');
15
+ const { PACKAGED_CLAUDE_ACP_ARG } = require('./acp/packaged-claude-acp');
15
16
  const { permissionSecurityWarnings } = require('./acp/permission-security');
16
17
  const { patchBlock, rejectPatch } = require('./acp/patch-decisions');
17
18
  const { getProviderAdapter, listProviderAdapters } = require('./provider-adapters');
@@ -36,12 +37,22 @@ const CODEX_STEER_METHOD = '_codex/session/steer';
36
37
  const CODEX_ACP_PACKAGE = '@agentclientprotocol/codex-acp';
37
38
  const CODEX_ACP_VERSION = '1.1.4';
38
39
  const CODEX_ACP_SHA256 = 'e36876cc2250737c719644e6c69ab054a3f8b58071fad45c44407013082942ff';
40
+ const CLAUDE_ACP_PACKAGE = '@agentclientprotocol/claude-agent-acp';
41
+ const CLAUDE_ACP_VERSION = '0.59.0';
42
+ const CLAUDE_ACP_SHA256 = 'a6aa515dd02382617bf46d9eac47b8a1022c6835bcf7a8d61e2c63939be2e49c';
39
43
  const CODEX_ACP_VENDOR_ENTRY = path.join(
40
44
  __dirname,
41
45
  '..',
42
46
  'dist',
43
47
  'acp',
44
- `codex-acp-${CODEX_ACP_VERSION}.js`,
48
+ `codex-acp-${CODEX_ACP_VERSION}.mjs`,
49
+ );
50
+ const CLAUDE_ACP_VENDOR_ENTRY = path.join(
51
+ __dirname,
52
+ '..',
53
+ 'dist',
54
+ 'acp',
55
+ `claude-agent-acp-${CLAUDE_ACP_VERSION}.mjs`,
45
56
  );
46
57
  const execFileAsync = promisify(execFile);
47
58
 
@@ -70,6 +81,15 @@ function adapterEntry(packageName) {
70
81
  if (packageName === CODEX_ACP_PACKAGE && fs.existsSync(CODEX_ACP_VENDOR_ENTRY)) {
71
82
  return verifiedCodexAcpEntry(CODEX_ACP_VENDOR_ENTRY);
72
83
  }
84
+ if (packageName === CLAUDE_ACP_PACKAGE && fs.existsSync(CLAUDE_ACP_VENDOR_ENTRY)) {
85
+ const actualSha256 = fileSha256(CLAUDE_ACP_VENDOR_ENTRY);
86
+ if (actualSha256 !== CLAUDE_ACP_SHA256) {
87
+ throw new Error(
88
+ `Claude ACP runtime failed integrity verification: expected ${CLAUDE_ACP_SHA256}, found ${actualSha256}`,
89
+ );
90
+ }
91
+ return CLAUDE_ACP_VENDOR_ENTRY;
92
+ }
73
93
  let sdkDirectory;
74
94
  try {
75
95
  sdkDirectory = path.dirname(runtimeRequire.resolve('@agentclientprotocol/sdk'));
@@ -109,7 +129,11 @@ function resolveAcpLaunch(provider, options = {}) {
109
129
  const launch = nodeAdapterLaunch(
110
130
  adapterEntry(adapter.acp.packageName),
111
131
  options.runtimeEnv || process.env,
112
- adapter.acp.packageName === CODEX_ACP_PACKAGE ? PACKAGED_CODEX_ACP_ARG : '',
132
+ adapter.acp.packageName === CODEX_ACP_PACKAGE
133
+ ? PACKAGED_CODEX_ACP_ARG
134
+ : adapter.acp.packageName === CLAUDE_ACP_PACKAGE
135
+ ? PACKAGED_CLAUDE_ACP_ARG
136
+ : '',
113
137
  );
114
138
  return { ...launch, version: adapter.acp.version };
115
139
  }
@@ -2508,6 +2532,17 @@ class AcpRuntime extends EventEmitter {
2508
2532
  return visible;
2509
2533
  }
2510
2534
 
2535
+ getTranscriptEntry(agentId, entryId) {
2536
+ const binding = this.requireBinding(agentId);
2537
+ const entries = (binding.sessionState?.entries || []).filter(candidate => (
2538
+ String(candidate?.id || '') === String(entryId || '')
2539
+ ));
2540
+ if (entries.length !== 1) return null;
2541
+ const [entry] = entries;
2542
+ if (!entry || binding.sessionState.isInternalEntry(entry)) return null;
2543
+ return entry;
2544
+ }
2545
+
2511
2546
  getPatchDecision(agentId, toolCallId, requestedPath) {
2512
2547
  const binding = this.requireBinding(agentId);
2513
2548
  return binding.patchDecisions.get(`${String(toolCallId || '')}\n${String(requestedPath || '')}`) || '';
@@ -1,12 +1,31 @@
1
+ const crypto = require('crypto');
1
2
  const { createTwoFilesPatch, diffLines } = require('diff');
2
3
 
3
4
  const MAX_RENDERED_DIFF_CHARS = 64 * 1024;
4
5
  const MAX_INLINE_TOOL_DETAIL_CHARS = 4 * 1024;
6
+ const MAX_TRANSCRIPT_INLINE_TOOL_DETAIL_CHARS = 64 * 1024;
7
+ const MAX_TRANSCRIPT_MEDIA_BYTES = 25 * 1024 * 1024;
8
+ const MAX_TRANSCRIPT_MEDIA_BASE64_CHARS = Math.ceil(MAX_TRANSCRIPT_MEDIA_BYTES / 3) * 4;
5
9
  const MAX_EMBEDDED_RESOURCE_TEXT_CHARS = 4 * 1024;
6
10
  const MAX_COLLABORATION_AGENTS = 16;
7
11
  const MAX_COLLABORATION_ID_CHARS = 160;
8
12
  const MAX_COLLABORATION_PATH_CHARS = 512;
9
13
  const MAX_COLLABORATION_MESSAGE_CHARS = 160;
14
+ const TRANSCRIPT_MEDIA_MIME_TYPES = new Set([
15
+ 'audio/aac',
16
+ 'audio/flac',
17
+ 'audio/mp4',
18
+ 'audio/mpeg',
19
+ 'audio/ogg',
20
+ 'audio/wav',
21
+ 'audio/wave',
22
+ 'audio/webm',
23
+ 'audio/x-wav',
24
+ 'image/gif',
25
+ 'image/jpeg',
26
+ 'image/png',
27
+ 'image/webp',
28
+ ]);
10
29
 
11
30
  function diffBlocks(content) {
12
31
  return (Array.isArray(content) ? content : [])
@@ -184,7 +203,92 @@ function transcriptResource(resourceValue) {
184
203
  };
185
204
  }
186
205
 
187
- function transcriptMediaBlocks(entry) {
206
+ function transcriptMediaValue(block) {
207
+ if (!block || typeof block !== 'object') return null;
208
+ if (['image', 'audio'].includes(block.type)) return block;
209
+ if (
210
+ block.type === 'content'
211
+ && block.content
212
+ && typeof block.content === 'object'
213
+ && ['image', 'audio'].includes(block.content.type)
214
+ ) {
215
+ return block.content;
216
+ }
217
+ return null;
218
+ }
219
+
220
+ function validatedTranscriptMedia(block) {
221
+ const type = String(block?.type || '');
222
+ const data = typeof block?.data === 'string' ? block.data : '';
223
+ const mimeType = String(block?.mimeType || '').trim().toLowerCase();
224
+ if (
225
+ !['image', 'audio'].includes(type)
226
+ || !data
227
+ || data.length > MAX_TRANSCRIPT_MEDIA_BASE64_CHARS
228
+ || data.length % 4 !== 0
229
+ || !TRANSCRIPT_MEDIA_MIME_TYPES.has(mimeType)
230
+ || !/^[a-z0-9+/]*={0,2}$/i.test(data)
231
+ ) return null;
232
+ const paddingChars = data.endsWith('==') ? 2 : (data.endsWith('=') ? 1 : 0);
233
+ const decodedBytes = (data.length / 4) * 3 - paddingChars;
234
+ if (decodedBytes <= 0 || decodedBytes > MAX_TRANSCRIPT_MEDIA_BYTES) return null;
235
+ return { data, decodedBytes, mimeType, type };
236
+ }
237
+
238
+ function transcriptMediaId(block) {
239
+ const media = validatedTranscriptMedia(block);
240
+ if (!media) return '';
241
+ return crypto.createHash('sha256')
242
+ .update(media.type)
243
+ .update('\0')
244
+ .update(media.mimeType)
245
+ .update('\0')
246
+ .update(media.data)
247
+ .digest('hex');
248
+ }
249
+
250
+ function decodeAcpTranscriptMedia(block) {
251
+ const media = validatedTranscriptMedia(block);
252
+ if (!media) return null;
253
+ const content = Buffer.from(media.data, 'base64');
254
+ if (content.length !== media.decodedBytes) return null;
255
+ return {
256
+ content,
257
+ mimeType: media.mimeType,
258
+ };
259
+ }
260
+
261
+ function compactTranscriptMedia(block, entry, options = {}) {
262
+ const mediaPathPrefix = String(options.mediaPathPrefix || '').replace(/\/+$/, '');
263
+ if (!mediaPathPrefix) return JSON.parse(JSON.stringify(block));
264
+ const hasInlineData = typeof block?.data === 'string' && block.data.length > 0;
265
+ if (!hasInlineData) return JSON.parse(JSON.stringify(block));
266
+ const mediaId = transcriptMediaId(block);
267
+ const projected = { ...block };
268
+ delete projected.data;
269
+ if (!mediaId) return projected;
270
+ projected.url = `${mediaPathPrefix}/${encodeURIComponent(String(entry?.id || ''))}/${mediaId}`;
271
+ return projected;
272
+ }
273
+
274
+ function acpTranscriptMedia(entry, requestedMediaId) {
275
+ const mediaId = String(requestedMediaId || '').trim().toLowerCase();
276
+ if (!/^[a-f0-9]{64}$/.test(mediaId)) return null;
277
+ const direct = Array.isArray(entry?.content) ? entry.content : [];
278
+ const output = entry?.rawOutput && typeof entry.rawOutput === 'object' ? entry.rawOutput : {};
279
+ const result = output?.result && typeof output.result === 'object' ? output.result : {};
280
+ const raw = Array.isArray(result.content)
281
+ ? result.content
282
+ : (Array.isArray(output.content) ? output.content : []);
283
+ for (const block of [...direct, ...raw]) {
284
+ const media = transcriptMediaValue(block);
285
+ if (!media) continue;
286
+ if (transcriptMediaId(media) === mediaId) return media;
287
+ }
288
+ return null;
289
+ }
290
+
291
+ function transcriptMediaBlocks(entry, options = {}) {
188
292
  const direct = Array.isArray(entry?.content) ? entry.content : [];
189
293
  const output = entry?.rawOutput && typeof entry.rawOutput === 'object' ? entry.rawOutput : {};
190
294
  const result = output?.result && typeof output.result === 'object' ? output.result : {};
@@ -194,7 +298,10 @@ function transcriptMediaBlocks(entry) {
194
298
  const blocks = [...direct, ...raw].flatMap(block => {
195
299
  if (!block || typeof block !== 'object') return [];
196
300
  if (block.type === 'terminal') return [{ type: 'terminal', terminalId: String(block.terminalId || '') }];
197
- if (['image', 'audio', 'resource_link'].includes(block.type)) return [JSON.parse(JSON.stringify(block))];
301
+ if (['image', 'audio'].includes(block.type)) {
302
+ return [compactTranscriptMedia(block, entry, options)];
303
+ }
304
+ if (block.type === 'resource_link') return [JSON.parse(JSON.stringify(block))];
198
305
  if (block.type === 'resource') {
199
306
  return [{
200
307
  type: 'resource',
@@ -203,7 +310,11 @@ function transcriptMediaBlocks(entry) {
203
310
  }
204
311
  if (block.type !== 'content' || !block.content || typeof block.content !== 'object') return [];
205
312
  const content = block.content;
206
- if (['image', 'audio', 'resource_link'].includes(content.type)) {
313
+ if (['image', 'audio'].includes(content.type)) {
314
+ const projected = compactTranscriptMedia(content, entry, options);
315
+ return [{ type: 'content', content: projected }];
316
+ }
317
+ if (content.type === 'resource_link') {
207
318
  return [{ type: 'content', content: JSON.parse(JSON.stringify(content)) }];
208
319
  }
209
320
  if (content.type === 'resource') {
@@ -226,6 +337,20 @@ function transcriptMediaBlocks(entry) {
226
337
  });
227
338
  }
228
339
 
340
+ function transcriptEntryWithCompactMedia(entry, options = {}) {
341
+ const mediaPathPrefix = String(options.mediaPathPrefix || '').trim();
342
+ if (!mediaPathPrefix || !Array.isArray(entry?.content)) return entry;
343
+ const content = entry.content.map(block => {
344
+ const media = transcriptMediaValue(block);
345
+ if (!media) return block;
346
+ const projected = compactTranscriptMedia(media, entry, options);
347
+ return block?.type === 'content'
348
+ ? { ...block, content: projected }
349
+ : projected;
350
+ });
351
+ return { ...entry, content };
352
+ }
353
+
229
354
  function generatedMediaTool(entry) {
230
355
  const title = String(entry?.title || '').trim().toLowerCase();
231
356
  const id = String(entry?.id || '').trim().toLowerCase();
@@ -296,9 +421,13 @@ function transcriptCodexToolMeta(entry) {
296
421
  return Object.keys(compact).length > 0 ? compact : null;
297
422
  }
298
423
 
299
- function acpTranscriptToolEntry(entry) {
424
+ function acpTranscriptToolEntry(entry, options = {}) {
300
425
  if (!entry || entry.type !== 'tool') return entry;
301
426
  const detail = detailForTool(entry);
427
+ const requestedInlineDetailChars = Number(options.maxInlineDetailChars);
428
+ const inlineDetailChars = Number.isFinite(requestedInlineDetailChars)
429
+ ? Math.max(0, Math.min(MAX_INLINE_TOOL_DETAIL_CHARS, Math.floor(requestedInlineDetailChars)))
430
+ : MAX_INLINE_TOOL_DETAIL_CHARS;
302
431
  const changes = patchChanges(entry.content).map(change => ({
303
432
  path: change.path,
304
433
  kind: change.kind,
@@ -313,7 +442,11 @@ function acpTranscriptToolEntry(entry) {
313
442
  .join('\n');
314
443
  const meta = {};
315
444
  if (entry?._meta?.subagent_session_info) {
316
- meta.subagent_session_info = JSON.parse(JSON.stringify(entry._meta.subagent_session_info));
445
+ const sessionId = boundedCollaborationString(
446
+ entry._meta.subagent_session_info.session_id,
447
+ MAX_COLLABORATION_ID_CHARS
448
+ );
449
+ if (sessionId) meta.subagent_session_info = { session_id: sessionId };
317
450
  }
318
451
  if (entry?._meta?.farming_patch_decisions) {
319
452
  meta.farming_patch_decisions = JSON.parse(JSON.stringify(entry._meta.farming_patch_decisions));
@@ -326,12 +459,12 @@ function acpTranscriptToolEntry(entry) {
326
459
  title: String(entry.title || ''),
327
460
  kind: String(entry.kind || 'other'),
328
461
  status: String(entry.status || ''),
329
- content: transcriptMediaBlocks(entry),
462
+ content: transcriptMediaBlocks(entry, options),
330
463
  ...(Object.keys(meta).length > 0 ? { _meta: meta } : {}),
331
- transcriptDetail: detail.length <= MAX_INLINE_TOOL_DETAIL_CHARS
464
+ transcriptDetail: detail.length <= inlineDetailChars
332
465
  ? detail
333
- : `${detail.slice(0, MAX_INLINE_TOOL_DETAIL_CHARS)}\n\n[Open to load full detail]`,
334
- transcriptDetailTruncated: detail.length > MAX_INLINE_TOOL_DETAIL_CHARS,
466
+ : `${detail.slice(0, inlineDetailChars)}${inlineDetailChars > 0 ? '\n\n' : ''}[Open to load full detail]`,
467
+ transcriptDetailTruncated: detail.length > inlineDetailChars,
335
468
  transcriptPatchSummary: patchSummary,
336
469
  transcriptChanges: changes,
337
470
  generatedMedia: generatedMediaTool(entry),
@@ -339,9 +472,41 @@ function acpTranscriptToolEntry(entry) {
339
472
  };
340
473
  }
341
474
 
475
+ function acpTranscriptEntries(entries, options = {}) {
476
+ const requestedBudget = Number(options.maxInlineToolDetailChars);
477
+ let remainingInlineDetailChars = Number.isFinite(requestedBudget)
478
+ ? Math.max(0, Math.floor(requestedBudget))
479
+ : MAX_TRANSCRIPT_INLINE_TOOL_DETAIL_CHARS;
480
+ const source = Array.isArray(entries) ? entries : [];
481
+ const projected = new Array(source.length);
482
+
483
+ // The transcript page owns only a bounded summary. Exact tool and subagent
484
+ // detail remains backend-owned and is loaded on explicit expansion.
485
+ for (let index = source.length - 1; index >= 0; index -= 1) {
486
+ const entry = source[index];
487
+ if (entry?.type !== 'tool') {
488
+ projected[index] = transcriptEntryWithCompactMedia(entry, options);
489
+ continue;
490
+ }
491
+ const projectedEntry = acpTranscriptToolEntry(entry, {
492
+ maxInlineDetailChars: remainingInlineDetailChars,
493
+ mediaPathPrefix: options.mediaPathPrefix,
494
+ });
495
+ remainingInlineDetailChars = Math.max(
496
+ 0,
497
+ remainingInlineDetailChars - String(projectedEntry.transcriptDetail || '').length
498
+ );
499
+ projected[index] = projectedEntry;
500
+ }
501
+ return projected;
502
+ }
503
+
342
504
  module.exports = {
505
+ acpTranscriptEntries,
506
+ acpTranscriptMedia,
343
507
  acpToolChanges: entry => patchChanges(entry?.content, { includeDiff: true }),
344
508
  acpToolDetail: detailForTool,
345
509
  acpToolReviewChanges: entry => patchReviewChanges(entry?.content),
346
510
  acpTranscriptToolEntry,
511
+ decodeAcpTranscriptMedia,
347
512
  };
@@ -50,7 +50,13 @@ const {
50
50
  stopPersistedAcpProcessGroup,
51
51
  } = require('./acp-runtime');
52
52
  const { chatRuntimeForProvider, isChatMode } = require('./chat-runtime');
53
- const { acpToolChanges, acpToolDetail, acpToolReviewChanges, acpTranscriptToolEntry } = require('./acp-transcript');
53
+ const {
54
+ acpTranscriptEntries,
55
+ acpTranscriptMedia,
56
+ acpToolChanges,
57
+ acpToolDetail,
58
+ acpToolReviewChanges,
59
+ } = require('./acp-transcript');
54
60
  const {
55
61
  applyCodexTerminalProfile,
56
62
  codexTerminalProfileFromOutput,
@@ -5085,10 +5091,21 @@ class AgentManager extends EventEmitter {
5085
5091
  const transcript = this.acpRuntime.getTranscriptSession(agentId, options);
5086
5092
  return {
5087
5093
  ...transcript,
5088
- entries: transcript.entries.map(entry => entry?.type === 'tool' ? acpTranscriptToolEntry(entry) : entry),
5094
+ entries: acpTranscriptEntries(transcript.entries, {
5095
+ mediaPathPrefix: options.mediaPathPrefix,
5096
+ }),
5089
5097
  };
5090
5098
  }
5091
5099
 
5100
+ getAcpTranscriptMedia(agentId, entryId, mediaId) {
5101
+ this.requireLiveAcpAgent(agentId);
5102
+ const entry = this.acpRuntime.getTranscriptEntry(agentId, entryId);
5103
+ if (!entry) throw new Error('ACP transcript entry not found');
5104
+ const media = acpTranscriptMedia(entry, mediaId);
5105
+ if (!media) throw new Error('ACP transcript media not found');
5106
+ return media;
5107
+ }
5108
+
5092
5109
  getAcpToolDetail(agentId, toolCallId) {
5093
5110
  this.requireLiveAcpAgent(agentId);
5094
5111
  const entry = this.acpRuntime.getToolEntry(agentId, toolCallId);
@@ -5661,6 +5678,14 @@ class AgentManager extends EventEmitter {
5661
5678
  if (admission) {
5662
5679
  transitionLifecycleOperation(staged, admission.operation.id, 'succeeded');
5663
5680
  }
5681
+ if (!admission && !structuralUpdateChanged && !readUpdateChanged) {
5682
+ return {
5683
+ agentId,
5684
+ ...updates,
5685
+ changed: false,
5686
+ requiresState: false,
5687
+ };
5688
+ }
5664
5689
  try {
5665
5690
  this.ensurePersistentAgentSession(staged);
5666
5691
  } catch (error) {