farming-code 2.2.19 → 2.2.22

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 (53) hide show
  1. package/README.md +5 -5
  2. package/README.zh_cn.md +5 -5
  3. package/THIRD_PARTY_NOTICES.md +5 -8
  4. package/backend/acp/client-services.js +37 -5
  5. package/backend/acp/patch-decisions.js +19 -3
  6. package/backend/acp-runtime.js +1201 -198
  7. package/backend/acp-transcript.js +66 -0
  8. package/backend/agent-lifecycle-journal.js +133 -0
  9. package/backend/agent-manager.js +2715 -354
  10. package/backend/agent-order.js +8 -4
  11. package/backend/agent-runtime-binding.js +1 -0
  12. package/backend/bundle-update-helper.js +5 -1
  13. package/backend/config-manager.js +16 -0
  14. package/backend/control-api.js +146 -20
  15. package/backend/farming-app-cli.js +335 -10
  16. package/backend/farming-session-store.js +283 -36
  17. package/backend/json-cli-runtime.js +120 -6
  18. package/backend/npm-update-helper.js +97 -18
  19. package/backend/provider-adapters.js +18 -6
  20. package/backend/review-session-service.js +31 -17
  21. package/backend/server.js +337 -103
  22. package/backend/storage-layout.js +10 -0
  23. package/backend/terminal-screen-worker-pool.js +15 -6
  24. package/backend/theme-manager.js +23 -5
  25. package/backend/update-service.js +239 -27
  26. package/backend/usage-monitor.js +11 -0
  27. package/backend/workspace-file-router.js +9 -3
  28. package/backend/workspace-file-service.js +292 -62
  29. package/dist/acp/codex-acp-1.1.4.js +32 -1
  30. package/dist/assets/App-CmZdNavW.js +210 -0
  31. package/dist/assets/{FileEditorMarkdownPreview-R0jwwLME.js → FileEditorMarkdownPreview-KbnE8VC8.js} +1 -1
  32. package/dist/assets/FileEditorPane-5_HUejjd.js +2 -0
  33. package/dist/assets/IconGlyphs-C2WFcoJS.js +1 -0
  34. package/dist/assets/ProjectFilesSection-C1RAJWOE.js +12 -0
  35. package/dist/assets/{ReviewPage-DBKNOWsJ.js → ReviewPage-L1vh1Ggs.js} +3 -3
  36. package/dist/assets/code-dark-BKtvf0Xc.css +1 -0
  37. package/dist/assets/file-icons-CHWt5nSy.js +1 -0
  38. package/dist/assets/{index-DQdtPfCk.js → index-RvUiMpGP.js} +2 -2
  39. package/dist/assets/main-XMXgyJJt.css +1 -0
  40. package/dist/assets/workspace-view-state-BqB4HM7r.js +7 -0
  41. package/dist/index.html +1 -1
  42. package/frontend/skins/crt/app.js +365 -107
  43. package/frontend/skins/crt/index.html +5 -10
  44. package/frontend/skins/crt/styles/billing.css +6 -1
  45. package/package.json +9 -3
  46. package/dist/assets/App-CbXYBccJ.js +0 -210
  47. package/dist/assets/FileEditorPane-CJs6kfZO.js +0 -2
  48. package/dist/assets/IconGlyphs-CZzDr5o2.js +0 -1
  49. package/dist/assets/ProjectFilesSection-DWCV07lA.js +0 -12
  50. package/dist/assets/code-dark-qIBiLBWV.css +0 -1
  51. package/dist/assets/file-icons-DpbrD5ZP.js +0 -1
  52. package/dist/assets/main-yzZEfgft.css +0 -1
  53. package/dist/assets/workspace-view-state-BpPINu84.js +0 -7
package/README.md CHANGED
@@ -1,11 +1,11 @@
1
1
  <p align="center">
2
- <img src="./public/farming-2/app-icon-v2-512.png" alt="Farming" width="112">
2
+ <img src="./public/farming-2/app-icon-v2-512.png" alt="Farming Code" width="112">
3
3
  </p>
4
4
 
5
- <h1 align="center">Farming</h1>
5
+ <h1 align="center">Farming Code</h1>
6
6
 
7
7
  <p align="center">
8
- Farming is an open-source remote web workbench for running and supervising AI coding agents on a development machine.
8
+ Farming Code is an open-source, self-hosted browser workspace for running and supervising Codex, Claude Code, OpenCode, and other AI coding agents.
9
9
  </p>
10
10
 
11
11
  <p align="center"><a href="./README.zh_cn.md">简体中文</a></p>
@@ -21,7 +21,7 @@
21
21
 
22
22
  ![Farming Code workbench](./docs/products/code/assets/01-code-workspace.png)
23
23
 
24
- Farming runs on the same development machine as your repositories and coding CLIs. Agent processes, terminals, and project files stay on that machine; a desktop or phone browser connects to those real sessions.
24
+ Farming Code runs on the same development machine as your repositories and coding CLIs. Agent processes, terminals, and project files stay on that machine; a desktop or phone browser connects to those real sessions.
25
25
 
26
26
  ## Quick Start
27
27
 
@@ -119,7 +119,7 @@ farming logs
119
119
  farming stop
120
120
  ```
121
121
 
122
- The startup token is stored in `~/.farming/.session-token` and reused across restarts and upgrades. **Settings → Updates** can upgrade npm installations in place. npm updates first use the machine's configured registry and retry the registry shown in Settings only when that registry lacks the selected version. Before installing, Farming verifies that npm will update 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.
122
+ 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.
123
123
 
124
124
  To run from source with the same port and base path:
125
125
 
package/README.zh_cn.md CHANGED
@@ -1,11 +1,11 @@
1
1
  <p align="center">
2
- <img src="./public/farming-2/app-icon-v2-512.png" alt="Farming" width="112">
2
+ <img src="./public/farming-2/app-icon-v2-512.png" alt="Farming Code" width="112">
3
3
  </p>
4
4
 
5
- <h1 align="center">Farming</h1>
5
+ <h1 align="center">Farming Code</h1>
6
6
 
7
7
  <p align="center">
8
- Farming 是一个开源的远程 Web 工作台,用于在开发机上启动和管理 AI Coding Agent。
8
+ Farming Code 是一个开源、自托管的浏览器工作区,用于启动和管理 Codex、Claude Code、OpenCode 及其他 AI Coding Agent。
9
9
  </p>
10
10
 
11
11
  <p align="center"><a href="./README.md">English</a></p>
@@ -21,7 +21,7 @@
21
21
 
22
22
  ![Farming Code 工作台](./docs/products/code/assets/01-code-workspace.png)
23
23
 
24
- Farming 与代码仓库和 Coding CLI 运行在同一台开发机上。Agent 进程、终端和项目文件都留在这台机器上;电脑或手机浏览器连接并操作这些真实 Session。
24
+ Farming Code 与代码仓库和 Coding CLI 运行在同一台开发机上。Agent 进程、终端和项目文件都留在这台机器上;电脑或手机浏览器连接并操作这些真实 Session。
25
25
 
26
26
  ## 快速开始
27
27
 
@@ -119,7 +119,7 @@ farming logs
119
119
  farming stop
120
120
  ```
121
121
 
122
- 启动 Token 存放在 `~/.farming/.session-token`,重启和升级会继续复用。**Settings → Updates** 可以原地升级 npm 安装。npm 升级会先使用机器配置的 registry;只有该 registry 缺少所选版本时,才会回退到 Settings 展示版本所用的 registry。安装前 Farming 会校验 npm 将更新的包根目录与启动当前服务的包根目录一致。GitHub Releases 也提供独立 CLI 和目录 Bundle;当前产物见 [GitHub Releases](https://github.com/zhuwenzhuang/farming/releases)。
122
+ 启动 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)。
123
123
 
124
124
  从源码运行,并使用相同的端口与 Base Path:
125
125
 
@@ -121,11 +121,8 @@ Microsoft, or the maintainers of the third-party projects listed above.
121
121
 
122
122
  ## Design References
123
123
 
124
- Farming's interface is influenced by modern coding workbenches and agent tools,
125
- including Codex, Visual Studio Code, and browser IDEs. This notice covers
126
- software and assets used by Farming; design inspiration alone does not imply
127
- that those projects are bundled with Farming.
128
-
129
- The CRT skin also studies the visual behavior of cool-retro-term. Farming's
130
- browser shaders are an independent implementation and do not copy or bundle
131
- cool-retro-term source code.
124
+ Farming Code's interface design references Visual Studio Code and Codex.
125
+ Farming's browser-based approach to terminal and coding-agent sessions also
126
+ draws on the idea explored by
127
+ [VibeTunnel](https://github.com/amantus-ai/vibetunnel). These design references
128
+ are separate from the software and assets identified above.
@@ -1,3 +1,4 @@
1
+ const crypto = require('crypto');
1
2
  const fs = require('fs');
2
3
  const path = require('path');
3
4
  const { spawn } = require('child_process');
@@ -20,6 +21,10 @@ function requireMatchingSession(binding, params) {
20
21
  }
21
22
  }
22
23
 
24
+ function requireOpenBinding(binding) {
25
+ if (binding?.exited === true) throw new Error('ACP Agent connection is closed');
26
+ }
27
+
23
28
  async function resolveWorkspacePath(binding, requestedPath, options = {}) {
24
29
  const value = String(requestedPath || '');
25
30
  if (!path.isAbsolute(value)) throw new Error('ACP file and terminal paths must be absolute');
@@ -76,6 +81,7 @@ class AcpClientFileSystem {
76
81
  throw new Error('ACP text file is too large to write');
77
82
  }
78
83
  const target = await resolveWorkspacePath(binding, params.path, { allowMissing: true });
84
+ requireOpenBinding(binding);
79
85
  let mode = 0o666;
80
86
  try {
81
87
  const existing = await fsp.realpath(target);
@@ -87,9 +93,14 @@ class AcpClientFileSystem {
87
93
  } catch (error) {
88
94
  if (error?.code !== 'ENOENT') throw error;
89
95
  }
90
- const temporary = path.join(path.dirname(target), `.${path.basename(target)}.farming-acp-${process.pid}-${Date.now()}.tmp`);
96
+ const temporary = path.join(
97
+ path.dirname(target),
98
+ `.${path.basename(target)}.farming-acp-${process.pid}-${crypto.randomUUID()}.tmp`,
99
+ );
91
100
  try {
101
+ requireOpenBinding(binding);
92
102
  await fsp.writeFile(temporary, content, { mode });
103
+ requireOpenBinding(binding);
93
104
  await fsp.rename(temporary, target);
94
105
  } catch (error) {
95
106
  await fsp.rm(temporary, { force: true }).catch(() => {});
@@ -120,8 +131,8 @@ class AcpClientTerminalManager {
120
131
  : typeof options.spawn === 'function'
121
132
  ? null
122
133
  : nodePty.spawn;
123
- this.sequence = 0;
124
134
  this.terminals = new Map();
135
+ this.pendingCreates = new Map();
125
136
  }
126
137
 
127
138
  activeCount(agentId) {
@@ -132,23 +143,44 @@ class AcpClientTerminalManager {
132
143
  return count;
133
144
  }
134
145
 
146
+ reserveCreate(agentId) {
147
+ const pending = this.pendingCreates.get(agentId) || 0;
148
+ if (this.activeCount(agentId) + pending >= MAX_ACTIVE_TERMINALS_PER_AGENT) {
149
+ throw new Error('ACP terminal limit reached for this Agent');
150
+ }
151
+ this.pendingCreates.set(agentId, pending + 1);
152
+ }
153
+
154
+ releaseCreate(agentId) {
155
+ const pending = this.pendingCreates.get(agentId) || 0;
156
+ if (pending <= 1) this.pendingCreates.delete(agentId);
157
+ else this.pendingCreates.set(agentId, pending - 1);
158
+ }
159
+
135
160
  async create(binding, params) {
136
161
  requireMatchingSession(binding, params);
137
- if (this.activeCount(binding.agentId) >= MAX_ACTIVE_TERMINALS_PER_AGENT) {
138
- throw new Error('ACP terminal limit reached for this Agent');
162
+ this.reserveCreate(binding.agentId);
163
+ try {
164
+ return await this.createReserved(binding, params);
165
+ } finally {
166
+ this.releaseCreate(binding.agentId);
139
167
  }
168
+ }
169
+
170
+ async createReserved(binding, params) {
140
171
  const command = String(params.command || '').trim();
141
172
  if (!command) throw new Error('ACP terminal command is required');
142
173
  const cwd = params.cwd
143
174
  ? await resolveWorkspacePath(binding, params.cwd)
144
175
  : await fsp.realpath(binding.cwd);
176
+ requireOpenBinding(binding);
145
177
  const env = { ...binding.env };
146
178
  for (const item of Array.isArray(params.env) ? params.env : []) {
147
179
  const name = String(item?.name || '');
148
180
  if (!name || name.includes('=') || name.includes('\0')) throw new Error('Invalid ACP terminal environment variable');
149
181
  env[name] = String(item?.value ?? '');
150
182
  }
151
- const terminalId = `acp-terminal-${++this.sequence}`;
183
+ const terminalId = `acp-terminal-${crypto.randomUUID()}`;
152
184
  const args = Array.isArray(params.args) ? params.args.map(String) : [];
153
185
  const child = this.ptySpawn
154
186
  ? this.ptySpawn(command, args, {
@@ -1,3 +1,4 @@
1
+ const crypto = require('crypto');
1
2
  const fs = require('fs');
2
3
  const path = require('path');
3
4
  const { resolveWorkspacePath } = require('./client-services');
@@ -91,7 +92,10 @@ async function missingTarget(root, requestedPath) {
91
92
  }
92
93
 
93
94
  async function atomicWrite(target, content, mode = 0o666) {
94
- const temporary = path.join(path.dirname(target), `.${path.basename(target)}.farming-acp-revert-${process.pid}-${Date.now()}.tmp`);
95
+ const temporary = path.join(
96
+ path.dirname(target),
97
+ `.${path.basename(target)}.farming-acp-revert-${process.pid}-${crypto.randomUUID()}.tmp`,
98
+ );
95
99
  try {
96
100
  await fsp.writeFile(temporary, content, { mode });
97
101
  await fsp.rename(temporary, target);
@@ -111,14 +115,26 @@ async function rejectPatch({ entry, root, requestedPath }) {
111
115
  const current = await existingTarget(root, requestedPath);
112
116
 
113
117
  if (deleted) {
114
- if (current.exists) throw new AcpPatchDecisionError('File changed after this ACP patch; it was not reverted', 409);
118
+ if (current.exists) {
119
+ const currentText = await fsp.readFile(current.target, 'utf8');
120
+ if (currentText === oldText) {
121
+ return { action: 'reverted', path: relativeWorkspacePath(root, current.logical) };
122
+ }
123
+ throw new AcpPatchDecisionError('File changed after this ACP patch; it was not reverted', 409);
124
+ }
115
125
  const target = await missingTarget(root, requestedPath);
116
126
  await atomicWrite(target.target, oldText);
117
127
  return { action: 'reverted', path: relativeWorkspacePath(root, target.logical) };
118
128
  }
119
129
 
120
- if (!current.exists) throw new AcpPatchDecisionError('File changed after this ACP patch; it was not reverted', 409);
130
+ if (!current.exists) {
131
+ if (added) return { action: 'reverted', path: relativeWorkspacePath(root, current.logical) };
132
+ throw new AcpPatchDecisionError('File changed after this ACP patch; it was not reverted', 409);
133
+ }
121
134
  const currentText = await fsp.readFile(current.target, 'utf8');
135
+ if (!added && currentText === oldText) {
136
+ return { action: 'reverted', path: relativeWorkspacePath(root, current.logical) };
137
+ }
122
138
  if (currentText !== newText) throw new AcpPatchDecisionError('File changed after this ACP patch; it was not reverted', 409);
123
139
 
124
140
  if (added) {