farming-code 2.2.7 → 2.2.11

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 (102) hide show
  1. package/README.md +110 -199
  2. package/README.zh_cn.md +106 -237
  3. package/THIRD_PARTY_NOTICES.md +11 -2
  4. package/backend/acp-checkpoint-store.js +198 -0
  5. package/backend/acp-runtime.js +306 -83
  6. package/backend/acp-session-state.js +202 -6
  7. package/backend/acp-transcript.js +112 -0
  8. package/backend/agent-activity.js +6 -157
  9. package/backend/agent-manager.js +1496 -737
  10. package/backend/agent-provider-session.js +17 -242
  11. package/backend/agent-runtime-binding.js +219 -0
  12. package/backend/agent-session-history.js +66 -1
  13. package/backend/auth.js +79 -6
  14. package/backend/codex-models.js +81 -84
  15. package/backend/codex-session-archive.js +45 -0
  16. package/backend/codex-terminal-profile.js +500 -0
  17. package/backend/codex-transcript-sanitizer.js +12 -0
  18. package/backend/codex-transcript.js +230 -8
  19. package/backend/config-manager.js +30 -0
  20. package/backend/control-api.js +192 -17
  21. package/backend/farming-net-pass.js +285 -0
  22. package/backend/farming-net-registry.js +112 -0
  23. package/backend/farming-net-server.js +298 -0
  24. package/backend/farming-session-store.js +5 -13
  25. package/backend/git-worktree-info.js +181 -0
  26. package/backend/local-session-engine.js +411 -186
  27. package/backend/main-page-session.js +5 -2
  28. package/backend/native-pty-controller-generation.js +106 -0
  29. package/backend/native-pty-host-client.js +275 -7
  30. package/backend/native-pty-host-identity.js +86 -0
  31. package/backend/native-pty-host.js +813 -114
  32. package/backend/native-session-engine.js +100 -28
  33. package/backend/packaged-node-pty.js +22 -2
  34. package/backend/provider-adapters.js +253 -0
  35. package/backend/provider-session-service.js +241 -0
  36. package/backend/runtime-observation.js +81 -0
  37. package/backend/server.js +294 -86
  38. package/backend/session-engine-bridge.js +21 -2
  39. package/backend/session-engine-router.js +1 -1
  40. package/backend/session-engine.js +1 -1
  41. package/backend/session-stream-protocol.js +185 -0
  42. package/backend/storage-layout.js +55 -0
  43. package/backend/terminal-attach-checkpoint.js +74 -0
  44. package/backend/terminal-exit-quiescence.js +39 -0
  45. package/backend/terminal-reducer-flow-control.js +97 -0
  46. package/backend/terminal-screen-state.js +11 -2
  47. package/backend/terminal-screen-worker-pool.js +59 -6
  48. package/backend/terminal-screen-worker-thread.js +97 -57
  49. package/backend/terminal-screen-worker.js +133 -51
  50. package/backend/terminal-state-serialization.js +127 -0
  51. package/backend/terminal-status.js +23 -4
  52. package/backend/usage-monitor.js +81 -4
  53. package/backend/workspace-directory.js +152 -0
  54. package/backend/workspace-file-router.js +182 -76
  55. package/backend/workspace-file-service.js +319 -4
  56. package/backend/workspace-root-registry.js +164 -0
  57. package/dist/assets/App-CHYpgECZ.js +208 -0
  58. package/dist/assets/{FileEditorMarkdownPreview-elKWc8Im.js → FileEditorMarkdownPreview--Cri80nQ.js} +92 -92
  59. package/dist/assets/FileEditorPane-DGJlEKdf.js +2 -0
  60. package/dist/assets/IconGlyphs-Cc14sepw.js +1 -0
  61. package/dist/assets/ProjectFilesSection-Cz1wG4NT.js +12 -0
  62. package/dist/assets/{ReviewPage-BaXu1ZdX.js → ReviewPage-LonRc6Fz.js} +1 -1
  63. package/dist/assets/code-dark-B1pAtO_P.css +1 -0
  64. package/dist/assets/file-icons-Bw2qd5iT.js +1 -0
  65. package/dist/assets/{index-B8SHldPE.js → index-miO2r9IZ.js} +3 -3
  66. package/dist/assets/main-DNe0jPw-.css +1 -0
  67. package/dist/assets/workspace-editor-model-BQol4qbA.js +1 -0
  68. package/dist/assets/workspace-editor-monaco-5gFbeDPF.js +1 -0
  69. package/dist/assets/workspace-editor-monaco-xPv8JzHH.js +4 -0
  70. package/dist/assets/workspace-view-state-DvYG_9PH.js +7 -0
  71. package/dist/assets/workspace-working-copy-D8-s_Sgh.js +1 -0
  72. package/dist/index.html +1 -1
  73. package/frontend/farming-net/app.css +625 -0
  74. package/frontend/farming-net/app.js +268 -0
  75. package/frontend/farming-net/index.html +86 -0
  76. package/frontend/reading-anchor.js +198 -0
  77. package/frontend/session-bridge.js +12 -3
  78. package/frontend/session-modal-bridge.js +5 -12
  79. package/frontend/skins/crt/app.js +1976 -793
  80. package/frontend/skins/crt/index.html +313 -23
  81. package/frontend/skins/crt/styles/billing.css +294 -223
  82. package/frontend/skins/crt/styles/monochrome-green.css +7 -2
  83. package/frontend/terminal-replay.js +372 -0
  84. package/package.json +10 -3
  85. package/shared/browser-protocol.d.ts +5 -0
  86. package/shared/browser-protocol.js +130 -0
  87. package/dist/assets/App-iA2BU9P6.js +0 -124
  88. package/dist/assets/FileEditorPane-RWiFD2cq.js +0 -5
  89. package/dist/assets/IconGlyphs-DfL0EBnj.js +0 -1
  90. package/dist/assets/ProjectFilesSection-Q4PDsWmM.js +0 -12
  91. package/dist/assets/code-dark-CDkOQAtK.css +0 -1
  92. package/dist/assets/file-icons-EFUGSSwf.js +0 -1
  93. package/dist/assets/main-DpYuBonn.css +0 -1
  94. package/dist/assets/qoder-C9LmmOSf.svg +0 -1
  95. package/dist/assets/qoder-Cf9gl0Y5.svg +0 -1
  96. package/dist/assets/qoder-gHCinseV.svg +0 -1
  97. package/dist/assets/workspace-view-state-CTyDzk2D.js +0 -1
  98. package/dist/assets/zsh-CLpveKlF.svg +0 -1
  99. package/dist/assets/zsh-FxSpMPbz.svg +0 -1
  100. /package/dist/assets/{api-D1lyBYIQ.js → api-D8nyOEbz.js} +0 -0
  101. /package/dist/assets/{core-ZlAPicox.js → core-D0LFJkDt.js} +0 -0
  102. /package/dist/assets/{useWorkspaceMenuKeyboard-CneKAZUJ.js → useWorkspaceMenuKeyboard-Brws6Ar9.js} +0 -0
package/backend/auth.js CHANGED
@@ -1,5 +1,6 @@
1
1
  const crypto = require('crypto');
2
2
  const fs = require('fs');
3
+ const { FarmingNetPassVerifier, PASS_QUERY_PARAM } = require('./farming-net-pass');
3
4
  const { createPoeticToken, generatePoeticToken, getPoeticTokenEntropyBits } = require('./haiku-token');
4
5
  const storageLayout = require('./storage-layout');
5
6
 
@@ -24,6 +25,21 @@ function decodeCookieToken(token) {
24
25
  }
25
26
  }
26
27
 
28
+ function normalizeCookieName(value) {
29
+ const cookieName = String(value || '').trim();
30
+ return /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/.test(cookieName) ? cookieName : 'farming_token';
31
+ }
32
+
33
+ function normalizeCookiePath(value) {
34
+ const cookiePath = String(value || '/').trim();
35
+ if (!cookiePath.startsWith('/') || /[;\r\n]/.test(cookiePath)) return '/';
36
+ return cookiePath;
37
+ }
38
+
39
+ function escapeRegExp(value) {
40
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
41
+ }
42
+
27
43
  function readExistingTokenFile(tokenFile) {
28
44
  try {
29
45
  const token = fs.readFileSync(tokenFile, 'utf8').trim();
@@ -35,12 +51,17 @@ function readExistingTokenFile(tokenFile) {
35
51
 
36
52
  class TokenAuth {
37
53
  constructor(options = {}) {
38
- this.disabled = options.disabled === true || isTruthyEnv(process.env.FARMING_DISABLE_AUTH);
54
+ const authEnv = options.env || process.env;
55
+ this.disabled = options.disabled === true || isTruthyEnv(authEnv.FARMING_DISABLE_AUTH);
39
56
  this.basePath = normalizeBasePath(options.basePath || '/');
40
57
  this.authStatusPath = this.basePath ? `${this.basePath}/api/auth/status` : '/api/auth/status';
58
+ this.cookieName = normalizeCookieName(options.cookieName);
59
+ this.cookiePath = normalizeCookiePath(options.cookiePath);
60
+ this.redirectQueryToken = options.redirectQueryToken === true;
41
61
  this.tokenFile = '';
42
62
  this.token = '';
43
63
  this.tokenInfo = null;
64
+ this.farmingNetPassVerifier = null;
44
65
 
45
66
  if (this.disabled) {
46
67
  return;
@@ -51,7 +72,15 @@ class TokenAuth {
51
72
  fs.mkdirSync(farmingDir, { recursive: true });
52
73
  }
53
74
  this.tokenFile = storageLayout.sessionTokenFile(farmingDir);
54
- const configuredToken = String(options.token || process.env.FARMING_TOKEN || '').trim();
75
+ if (options.farmingNetPassVerifier !== false) {
76
+ this.farmingNetPassVerifier = options.farmingNetPassVerifier || new FarmingNetPassVerifier({
77
+ trustFile: options.farmingNetTrustFile || storageLayout.farmingNetTrustFile(farmingDir),
78
+ });
79
+ }
80
+ const configuredTokenSource = Object.prototype.hasOwnProperty.call(options, 'token')
81
+ ? options.token
82
+ : authEnv.FARMING_TOKEN;
83
+ const configuredToken = String(configuredTokenSource || '').trim();
55
84
  const existingToken = configuredToken ? '' : readExistingTokenFile(this.tokenFile);
56
85
  if (configuredToken) {
57
86
  this.token = configuredToken;
@@ -70,7 +99,7 @@ class TokenAuth {
70
99
  entropyBits: 0,
71
100
  };
72
101
  } else {
73
- this.tokenInfo = createPoeticToken({ locale: options.tokenLocale, env: options.env, timeZone: options.timeZone });
102
+ this.tokenInfo = createPoeticToken({ locale: options.tokenLocale, env: authEnv, timeZone: options.timeZone });
74
103
  this.token = this.tokenInfo.token;
75
104
  }
76
105
  this.saveTokenFile();
@@ -97,6 +126,10 @@ class TokenAuth {
97
126
  return this.tokenInfo;
98
127
  }
99
128
 
129
+ getCookieName() {
130
+ return this.cookieName;
131
+ }
132
+
100
133
  verify(token) {
101
134
  if (this.disabled) return true;
102
135
  if (!token || !this.token) return false;
@@ -115,13 +148,32 @@ class TokenAuth {
115
148
  const queryToken = url.searchParams.get('token');
116
149
  if (queryToken) return queryToken;
117
150
 
151
+ return this.extractCookieToken(req);
152
+ }
153
+
154
+ extractCookieToken(req) {
118
155
  const cookies = req.headers.cookie || '';
119
- const match = cookies.match(/(?:^|;\s*)farming_token=([^;]+)/);
156
+ const match = cookies.match(new RegExp(`(?:^|;\\s*)${escapeRegExp(this.cookieName)}=([^;]+)`));
120
157
  if (match) return decodeCookieToken(match[1]);
121
158
 
122
159
  return null;
123
160
  }
124
161
 
162
+ setAuthenticatedCookie(res) {
163
+ res.setHeader('Set-Cookie',
164
+ `${this.cookieName}=${encodeCookieToken(this.token)}; Path=${this.cookiePath}; HttpOnly; SameSite=Lax`);
165
+ }
166
+
167
+ redirectWithoutQueryParameter(res, url, parameter) {
168
+ url.searchParams.delete(parameter);
169
+ const search = url.searchParams.toString();
170
+ res.writeHead(302, {
171
+ 'Cache-Control': 'no-store',
172
+ Location: `${url.pathname}${search ? `?${search}` : ''}`,
173
+ });
174
+ res.end();
175
+ }
176
+
125
177
  middleware() {
126
178
  return (req, res, next) => {
127
179
  if (this.disabled) return next();
@@ -133,13 +185,34 @@ class TokenAuth {
133
185
  return next();
134
186
  }
135
187
 
188
+ const method = String(req.method || 'GET').toUpperCase();
189
+ if (['GET', 'HEAD'].includes(method) && url.searchParams.has(PASS_QUERY_PARAM)) {
190
+ const pass = url.searchParams.get(PASS_QUERY_PARAM);
191
+ const passResult = this.farmingNetPassVerifier
192
+ ? this.farmingNetPassVerifier.verify(pass)
193
+ : { valid: false };
194
+ if (passResult.valid) {
195
+ this.setAuthenticatedCookie(res);
196
+ this.redirectWithoutQueryParameter(res, url, PASS_QUERY_PARAM);
197
+ return;
198
+ }
199
+ const cookieToken = this.extractCookieToken(req);
200
+ if (cookieToken && this.verify(cookieToken)) {
201
+ this.redirectWithoutQueryParameter(res, url, PASS_QUERY_PARAM);
202
+ return;
203
+ }
204
+ }
205
+
136
206
  const token = this.extractToken(req);
137
207
 
138
208
  // URL has token query param -> validate, set cookie, redirect
139
209
  if (url.searchParams.has('token')) {
140
210
  if (token && this.verify(token)) {
141
- res.setHeader('Set-Cookie',
142
- `farming_token=${encodeCookieToken(this.token)}; Path=/; HttpOnly; SameSite=Lax`);
211
+ this.setAuthenticatedCookie(res);
212
+ if (this.redirectQueryToken && ['GET', 'HEAD'].includes(method)) {
213
+ this.redirectWithoutQueryParameter(res, url, 'token');
214
+ return;
215
+ }
143
216
  return next();
144
217
  }
145
218
  }
@@ -1,69 +1,24 @@
1
1
  const { execFile } = require('child_process');
2
2
 
3
- const FALLBACK_MODELS = [
4
- {
5
- slug: 'gpt-5.5',
6
- display_name: 'GPT-5.5',
7
- description: 'Frontier model for complex coding, research, and real-world work.',
8
- default_reasoning_level: 'medium',
9
- supported_reasoning_levels: [
10
- { effort: 'low', description: 'Fast responses with lighter reasoning' },
11
- { effort: 'medium', description: 'Balances speed and reasoning depth for everyday tasks' },
12
- { effort: 'high', description: 'Greater reasoning depth for complex problems' },
13
- { effort: 'xhigh', description: 'Extra high reasoning depth for complex problems' },
14
- ],
15
- service_tiers: [
16
- { id: 'priority', name: 'Fast', description: '1.5x speed, increased usage' },
17
- ],
18
- visibility: 'list',
19
- },
20
- {
21
- slug: 'gpt-5.4',
22
- display_name: 'GPT-5.4',
23
- description: 'Strong model for everyday coding.',
24
- default_reasoning_level: 'medium',
25
- supported_reasoning_levels: [
26
- { effort: 'low', description: 'Fast responses with lighter reasoning' },
27
- { effort: 'medium', description: 'Balances speed and reasoning depth for everyday tasks' },
28
- { effort: 'high', description: 'Greater reasoning depth for complex problems' },
29
- { effort: 'xhigh', description: 'Extra high reasoning depth for complex problems' },
30
- ],
31
- service_tiers: [
32
- { id: 'priority', name: 'Fast', description: '1.5x speed, increased usage' },
33
- ],
34
- visibility: 'list',
35
- },
36
- {
37
- slug: 'gpt-5.4-mini',
38
- display_name: 'GPT-5.4 Mini',
39
- description: 'Small, fast, and cost-efficient model for simpler coding tasks.',
40
- default_reasoning_level: 'medium',
41
- supported_reasoning_levels: [
42
- { effort: 'low', description: 'Fast responses with lighter reasoning' },
43
- { effort: 'medium', description: 'Balances speed and reasoning depth for everyday tasks' },
44
- { effort: 'high', description: 'Greater reasoning depth for complex problems' },
45
- ],
46
- visibility: 'list',
47
- },
48
- {
49
- slug: 'gpt-5.3-codex-spark',
50
- display_name: 'GPT-5.3 Codex Spark',
51
- description: 'Ultra-fast coding model.',
52
- default_reasoning_level: 'medium',
53
- supported_reasoning_levels: [
54
- { effort: 'low', description: 'Fast responses with lighter reasoning' },
55
- { effort: 'medium', description: 'Balances speed and reasoning depth for everyday tasks' },
56
- ],
57
- visibility: 'list',
58
- },
59
- ];
3
+ const DEFAULT_CODEX_MODELS_TIMEOUT_MS = 15_000;
4
+
5
+ class CodexModelCatalogError extends Error {
6
+ constructor(code, message, cause = null) {
7
+ super(message);
8
+ this.name = 'CodexModelCatalogError';
9
+ this.code = code;
10
+ if (cause) this.cause = cause;
11
+ }
12
+ }
60
13
 
61
14
  const EFFORT_LABELS = {
15
+ minimal: 'Minimal',
62
16
  low: 'Low',
63
17
  medium: 'Medium',
64
18
  high: 'High',
65
19
  xhigh: 'Extra High',
66
20
  max: 'Max',
21
+ ultra: 'Ultra',
67
22
  };
68
23
 
69
24
  const DEFAULT_SERVICE_TIER = {
@@ -182,46 +137,88 @@ function buildModelOptions(models, source = 'codex') {
182
137
 
183
138
  function listCodexModelOptions(options = {}) {
184
139
  const codexBin = options.codexBin || process.env.FARMING_CODEX_BIN || 'codex';
185
- const timeout = options.timeout || 3000;
186
-
187
- return new Promise((resolve) => {
188
- const fallback = () => resolve({
189
- models: buildModelOptions(FALLBACK_MODELS, 'fallback'),
190
- catalog: buildModelCatalog(FALLBACK_MODELS, 'fallback'),
191
- source: 'fallback',
192
- });
140
+ const timeout = Number.isFinite(options.timeout)
141
+ ? Math.max(1, options.timeout)
142
+ : DEFAULT_CODEX_MODELS_TIMEOUT_MS;
143
+ const runExecFile = options.execFile || execFile;
193
144
 
145
+ return new Promise((resolve, reject) => {
194
146
  try {
195
- execFile(codexBin, ['debug', 'models'], {
147
+ runExecFile(codexBin, ['debug', 'models'], {
196
148
  timeout,
197
149
  maxBuffer: 20 * 1024 * 1024,
198
- }, (error, stdout) => {
199
- if (!error && stdout) {
200
- try {
201
- const models = catalogModelsFromJson(stdout);
202
- const modelOptions = buildModelOptions(models, 'codex');
203
- if (modelOptions.length > 0) {
204
- resolve({
205
- models: modelOptions,
206
- catalog: buildModelCatalog(models, 'codex'),
207
- source: 'codex',
208
- });
209
- return;
210
- }
211
- } catch {
212
- // fall through to fallback catalog
150
+ }, (error, stdout, stderr) => {
151
+ if (error) {
152
+ const timedOut = error.code === 'ETIMEDOUT' || error.killed === true;
153
+ if (timedOut) {
154
+ reject(new CodexModelCatalogError(
155
+ 'CODEX_MODELS_TIMEOUT',
156
+ `Codex model catalog timed out after ${timeout}ms`,
157
+ error
158
+ ));
159
+ return;
213
160
  }
161
+
162
+ const detail = String(stderr || error.message || '').trim().split(/\r?\n/, 1)[0];
163
+ reject(new CodexModelCatalogError(
164
+ 'CODEX_MODELS_COMMAND_FAILED',
165
+ detail
166
+ ? `Codex model catalog command failed: ${detail}`
167
+ : 'Codex model catalog command failed',
168
+ error
169
+ ));
170
+ return;
171
+ }
172
+
173
+ if (!String(stdout || '').trim()) {
174
+ reject(new CodexModelCatalogError(
175
+ 'CODEX_MODELS_EMPTY_OUTPUT',
176
+ 'Codex model catalog command returned no output'
177
+ ));
178
+ return;
179
+ }
180
+
181
+ let models;
182
+ try {
183
+ models = catalogModelsFromJson(stdout);
184
+ } catch (error) {
185
+ reject(new CodexModelCatalogError(
186
+ 'CODEX_MODELS_INVALID_JSON',
187
+ 'Codex model catalog returned invalid JSON',
188
+ error
189
+ ));
190
+ return;
191
+ }
192
+
193
+ const modelOptions = buildModelOptions(models, 'codex');
194
+ const catalog = buildModelCatalog(models, 'codex');
195
+ if (modelOptions.length === 0 || catalog.length === 0) {
196
+ reject(new CodexModelCatalogError(
197
+ 'CODEX_MODELS_EMPTY_CATALOG',
198
+ 'Codex model catalog did not contain any visible models'
199
+ ));
200
+ return;
214
201
  }
215
202
 
216
- fallback();
203
+ resolve({
204
+ models: modelOptions,
205
+ catalog,
206
+ source: 'codex',
207
+ });
217
208
  });
218
- } catch {
219
- fallback();
209
+ } catch (error) {
210
+ reject(new CodexModelCatalogError(
211
+ 'CODEX_MODELS_COMMAND_FAILED',
212
+ `Failed to start Codex model catalog command: ${error.message || error}`,
213
+ error
214
+ ));
220
215
  }
221
216
  });
222
217
  }
223
218
 
224
219
  module.exports = {
220
+ CodexModelCatalogError,
221
+ DEFAULT_CODEX_MODELS_TIMEOUT_MS,
225
222
  buildModelCatalog,
226
223
  buildModelOptions,
227
224
  catalogModelsFromJson,
@@ -0,0 +1,45 @@
1
+ const { execFile } = require('child_process');
2
+ const os = require('os');
3
+ const { promisify } = require('util');
4
+ const { resolveCompatibleCodexExecutable } = require('./executable-discovery');
5
+
6
+ const execFileAsync = promisify(execFile);
7
+
8
+ async function unarchiveCodexSession(sessionId, session = {}, options = {}) {
9
+ const resolveExecutable = options.resolveCompatibleCodexExecutable || resolveCompatibleCodexExecutable;
10
+ const runExecFile = options.execFileAsync || execFileAsync;
11
+ const processEnv = options.processEnv || process.env;
12
+ const codexResolution = resolveExecutable(session.cliVersion || '', processEnv.PATH || '');
13
+ if (!codexResolution.compatible) {
14
+ return {
15
+ error: codexResolution.error || 'Codex CLI is not compatible with this session',
16
+ status: 400,
17
+ };
18
+ }
19
+
20
+ try {
21
+ await runExecFile(codexResolution.path || 'codex', ['unarchive', sessionId], {
22
+ cwd: session.cwd || session.workspace || os.homedir(),
23
+ env: session.providerHomePath
24
+ ? { ...processEnv, CODEX_HOME: session.providerHomePath }
25
+ : processEnv,
26
+ timeout: 30_000,
27
+ maxBuffer: 1024 * 1024,
28
+ });
29
+ return { unarchived: true };
30
+ } catch (error) {
31
+ const message = [
32
+ error && error.stdout ? String(error.stdout).trim() : '',
33
+ error && error.stderr ? String(error.stderr).trim() : '',
34
+ error && error.message ? String(error.message).trim() : '',
35
+ ].filter(Boolean).join('\n') || 'failed to unarchive Codex session';
36
+ return {
37
+ error: message,
38
+ status: 409,
39
+ };
40
+ }
41
+ }
42
+
43
+ module.exports = {
44
+ unarchiveCodexSession,
45
+ };