farming-code 2.2.16 → 2.2.17

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 (37) hide show
  1. package/README.md +2 -2
  2. package/README.zh_cn.md +2 -2
  3. package/THIRD_PARTY_NOTICES.md +16 -0
  4. package/backend/acp-runtime.js +221 -10
  5. package/backend/agent-manager.js +396 -47
  6. package/backend/agent-provider-session.js +15 -1
  7. package/backend/codex-rollout-follower.js +83 -0
  8. package/backend/codex-session-archive.js +12 -1
  9. package/backend/codex-session-history.js +94 -0
  10. package/backend/config-manager.js +3 -3
  11. package/backend/farming-app-cli.js +8 -0
  12. package/backend/native-pty-host-client.js +59 -2
  13. package/backend/provider-adapters.js +117 -36
  14. package/backend/provider-session-service.js +138 -34
  15. package/backend/server.js +5 -1
  16. package/backend/storage-layout.js +5 -0
  17. package/backend/usage-history-client.js +305 -0
  18. package/backend/usage-history-scanner.generated.js +2250 -0
  19. package/backend/usage-history-scanner.ts +2718 -0
  20. package/backend/usage-history-smoke.js +64 -0
  21. package/backend/usage-history-worker.js +34 -0
  22. package/backend/usage-monitor.js +188 -588
  23. package/backend/vendor/usage-parsers/CC_STATISTICS_LICENSE +21 -0
  24. package/backend/vendor/usage-parsers/CODEXBAR_LICENSE +21 -0
  25. package/backend/vendor/usage-parsers/VENDOR.json +45 -0
  26. package/dist/assets/App-6yWdwzj8.js +210 -0
  27. package/dist/assets/code-dark-5esj0bRu.css +1 -0
  28. package/dist/assets/{index-iKBvPjWS.js → index-C8ZFKye1.js} +2 -2
  29. package/dist/assets/main-DSO3i_Gs.css +1 -0
  30. package/dist/index.html +1 -1
  31. package/frontend/skins/crt/app.js +9 -3
  32. package/package.json +12 -8
  33. package/shared/backend-connection-status.d.ts +25 -0
  34. package/shared/backend-connection-status.js +40 -0
  35. package/dist/assets/App-y6p2yRar.js +0 -206
  36. package/dist/assets/code-dark-DKBGxoPX.css +0 -1
  37. package/dist/assets/main-CdkLOXYl.css +0 -1
package/README.md CHANGED
@@ -15,7 +15,7 @@
15
15
  <a href="https://github.com/zhuwenzhuang/farming/releases"><img alt="Release" src="https://img.shields.io/github/v/release/zhuwenzhuang/farming?label=release"></a>
16
16
  <a href="https://www.npmjs.com/package/farming-code"><img alt="npm" src="https://img.shields.io/npm/v/farming-code?label=npm"></a>
17
17
  <a href="./LICENSE"><img alt="License" src="https://img.shields.io/github/license/zhuwenzhuang/farming"></a>
18
- <img alt="Node.js 22+" src="https://img.shields.io/badge/node-%3E%3D22-339933?logo=nodedotjs&amp;logoColor=white">
18
+ <img alt="Node.js 22.13 LTS or 24+" src="https://img.shields.io/badge/node-22.13_LTS_%7C_24%2B-339933?logo=nodedotjs&amp;logoColor=white">
19
19
  <img alt="macOS and Linux" src="https://img.shields.io/badge/platform-macOS%20%7C%20Linux-555">
20
20
  </p>
21
21
 
@@ -25,7 +25,7 @@ Farming runs on the same development machine as your repositories and coding CLI
25
25
 
26
26
  ## Quick Start
27
27
 
28
- With Node.js 22 or newer and at least one supported coding CLI installed and signed in:
28
+ With Node.js 22.13 LTS (22.x) or Node.js 24+ and at least one supported coding CLI installed and signed in:
29
29
 
30
30
  ```bash
31
31
  npm install --global farming-code@latest && farming daemon
package/README.zh_cn.md CHANGED
@@ -15,7 +15,7 @@
15
15
  <a href="https://github.com/zhuwenzhuang/farming/releases"><img alt="Release" src="https://img.shields.io/github/v/release/zhuwenzhuang/farming?label=release"></a>
16
16
  <a href="https://www.npmjs.com/package/farming-code"><img alt="npm" src="https://img.shields.io/npm/v/farming-code?label=npm"></a>
17
17
  <a href="./LICENSE"><img alt="License" src="https://img.shields.io/github/license/zhuwenzhuang/farming"></a>
18
- <img alt="Node.js 22+" src="https://img.shields.io/badge/node-%3E%3D22-339933?logo=nodedotjs&amp;logoColor=white">
18
+ <img alt="Node.js 22.13 LTS or 24+" src="https://img.shields.io/badge/node-22.13_LTS_%7C_24%2B-339933?logo=nodedotjs&amp;logoColor=white">
19
19
  <img alt="macOS and Linux" src="https://img.shields.io/badge/platform-macOS%20%7C%20Linux-555">
20
20
  </p>
21
21
 
@@ -25,7 +25,7 @@ Farming 与代码仓库和 Coding CLI 运行在同一台开发机上。Agent 进
25
25
 
26
26
  ## 快速开始
27
27
 
28
- 准备好 Node.js 22 或更新版本,以及至少一个已安装、已登录的受支持 Coding CLI:
28
+ 准备好 Node.js 22.13 LTS(22.x)或 Node.js 24+,以及至少一个已安装、已登录的受支持 Coding CLI:
29
29
 
30
30
  ```bash
31
31
  npm install --global farming-code@latest && farming daemon
@@ -58,6 +58,22 @@ beside the vendored files at `frontend/vendor/ghostty-web/LICENSE`.
58
58
  If vendored assets are updated, keep this notice in sync with the package name,
59
59
  version, and license.
60
60
 
61
+ Farming's TypeScript token-history scanner adapts the cumulative Codex
62
+ accounting and copied-prefix classification semantics from
63
+ [`CodexBar`](https://github.com/steipete/CodexBar) 0.45.2 at commit
64
+ `91560ca98e776b96fdf910d4a0423c2f0c07a3b9` (MIT, Copyright (c) 2026 Peter
65
+ Steinberger). It adapts Claude assistant usage extraction, streaming message-id
66
+ deduplication, and cached-input normalization from
67
+ [`cc-statistics`](https://github.com/androidZzT/cc-statistics) 1.1.0 at commit
68
+ `c98be0af52bbc7f09a1f277747744ace48d9e014` (MIT, Copyright (c) 2026
69
+ androidZzT).
70
+
71
+ Farming does not bundle either application's runtime. The adapted TypeScript
72
+ scanner, incremental filesystem checkpoints, SQLite schema, scan budgeting,
73
+ and product result shape are maintained by Farming. Exact source files,
74
+ adaptation scope, revisions, and both MIT licenses are retained under
75
+ `backend/vendor/usage-parsers/`.
76
+
61
77
  The CRT skin bundles one font file under `frontend/skins/crt/fonts/`:
62
78
 
63
79
  | Font | License | Purpose |
@@ -2,9 +2,10 @@ const EventEmitter = require('events');
2
2
  const crypto = require('crypto');
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
- const { spawn } = require('child_process');
5
+ const { execFile, spawn } = require('child_process');
6
6
  const { Readable, Writable } = require('stream');
7
7
  const { createRequire } = require('module');
8
+ const { promisify } = require('util');
8
9
  const packageJson = require('../package.json');
9
10
  const { AcpCheckpointStore } = require('./acp-checkpoint-store');
10
11
  const { AcpSessionState } = require('./acp-session-state');
@@ -14,6 +15,7 @@ const { PACKAGED_CODEX_ACP_ARG } = require('./acp/packaged-codex-acp');
14
15
  const { permissionSecurityWarnings } = require('./acp/permission-security');
15
16
  const { rejectPatch } = require('./acp/patch-decisions');
16
17
  const { getProviderAdapter, listProviderAdapters } = require('./provider-adapters');
18
+ const { isSafeProviderSessionId } = require('./provider-session-id');
17
19
 
18
20
  const ADAPTER_VERSIONS = Object.freeze(Object.fromEntries(
19
21
  listProviderAdapters().filter(adapter => adapter.acp).map(adapter => [adapter.id, adapter.acp.version]),
@@ -26,6 +28,9 @@ const DEFAULT_CANCEL_TIMEOUT_MS = 15_000;
26
28
  const DEFAULT_HISTORY_REPLAY_MIN_WAIT_MS = 350;
27
29
  const DEFAULT_HISTORY_REPLAY_QUIET_MS = 150;
28
30
  const DEFAULT_HISTORY_REPLAY_MAX_WAIT_MS = 5_000;
31
+ const IDENTITY_ADAPTER_GRACEFUL_EXIT_MS = 3_000;
32
+ const IDENTITY_ADAPTER_TERMINATE_MS = 1_000;
33
+ const IDENTITY_ADAPTER_KILL_MS = 1_000;
29
34
  const CODEX_SET_SESSION_MODEL_METHOD = 'session/set_model';
30
35
  const CODEX_STEER_METHOD = '_codex/session/steer';
31
36
  const CODEX_ACP_PACKAGE = '@agentclientprotocol/codex-acp';
@@ -38,6 +43,7 @@ const CODEX_ACP_VENDOR_ENTRY = path.join(
38
43
  'acp',
39
44
  `codex-acp-${CODEX_ACP_VERSION}.js`,
40
45
  );
46
+ const execFileAsync = promisify(execFile);
41
47
 
42
48
  let sdkPromise;
43
49
  const runtimeRequire = createRequire(__filename);
@@ -290,6 +296,81 @@ function isCodexSteerUnavailableError(error) {
290
296
  || text.includes('cannot steer a compact turn');
291
297
  }
292
298
 
299
+ function childHasExited(child) {
300
+ return !child
301
+ || (child.exitCode !== null && child.exitCode !== undefined)
302
+ || (child.signalCode !== null && child.signalCode !== undefined);
303
+ }
304
+
305
+ function processGroupHasExited(binding) {
306
+ if (!binding?.ownsProcessGroup || !binding.child?.pid || process.platform === 'win32') {
307
+ return childHasExited(binding?.child);
308
+ }
309
+ try {
310
+ process.kill(-binding.child.pid, 0);
311
+ return false;
312
+ } catch (error) {
313
+ return error?.code === 'ESRCH';
314
+ }
315
+ }
316
+
317
+ async function waitForProcessTreeExit(binding, timeoutMs) {
318
+ const deadline = Date.now() + timeoutMs;
319
+ while (Date.now() <= deadline) {
320
+ if (processGroupHasExited(binding)) return true;
321
+ await new Promise(resolve => setTimeout(resolve, 25));
322
+ }
323
+ return processGroupHasExited(binding);
324
+ }
325
+
326
+ function signalProcessTree(binding, signal) {
327
+ if (!binding?.child || processGroupHasExited(binding)) return;
328
+ if (binding.ownsProcessGroup && binding.child.pid && process.platform !== 'win32') {
329
+ try {
330
+ process.kill(-binding.child.pid, signal);
331
+ return;
332
+ } catch (error) {
333
+ if (error?.code === 'ESRCH') return;
334
+ }
335
+ }
336
+ binding.child.kill(signal);
337
+ }
338
+
339
+ async function deleteProviderSessionIdentity(options = {}) {
340
+ const provider = String(options.provider || '').trim().toLowerCase();
341
+ const sessionId = String(options.sessionId || '').trim();
342
+ if (!isSafeProviderSessionId(sessionId)) {
343
+ throw new Error('Provider session rollback requires a safe exact session id');
344
+ }
345
+ let args;
346
+ if (provider === 'codex') args = ['delete', '--force', sessionId];
347
+ else if (provider === 'opencode') args = ['session', 'delete', sessionId];
348
+ else throw new Error(`${provider || 'Provider'} does not support identity rollback`);
349
+ await execFileAsync(options.executable || getProviderAdapter(provider)?.executable, args, {
350
+ cwd: options.cwd,
351
+ env: options.env,
352
+ timeout: 15_000,
353
+ maxBuffer: 1024 * 1024,
354
+ });
355
+ }
356
+
357
+ function attachProviderSessionIdentity(error, identity, rollbackError) {
358
+ const target = error instanceof Error ? error : new Error(String(error || 'Provider session identity failed'));
359
+ Object.defineProperty(target, 'providerSessionIdentity', {
360
+ value: identity,
361
+ enumerable: false,
362
+ configurable: true,
363
+ });
364
+ if (rollbackError) {
365
+ Object.defineProperty(target, 'providerSessionRollbackError', {
366
+ value: rollbackError,
367
+ enumerable: false,
368
+ configurable: true,
369
+ });
370
+ }
371
+ return target;
372
+ }
373
+
293
374
  class AcpRuntime extends EventEmitter {
294
375
  constructor(options = {}) {
295
376
  super();
@@ -304,6 +385,7 @@ class AcpRuntime extends EventEmitter {
304
385
  this.historyReplayMinWaitMs = options.historyReplayMinWaitMs ?? DEFAULT_HISTORY_REPLAY_MIN_WAIT_MS;
305
386
  this.historyReplayQuietMs = options.historyReplayQuietMs ?? DEFAULT_HISTORY_REPLAY_QUIET_MS;
306
387
  this.historyReplayMaxWaitMs = options.historyReplayMaxWaitMs ?? DEFAULT_HISTORY_REPLAY_MAX_WAIT_MS;
388
+ this.deleteProviderSessionIdentity = options.deleteProviderSessionIdentity || deleteProviderSessionIdentity;
307
389
  this.checkpointStore = options.checkpointStore
308
390
  || (options.configDir ? new AcpCheckpointStore(options.configDir, options.checkpointOptions) : null);
309
391
  this.bindings = new Map();
@@ -331,10 +413,12 @@ class AcpRuntime extends EventEmitter {
331
413
  launch,
332
414
  restartOptions: { ...options, agentId, provider },
333
415
  approvalMode: options.approvalMode || 'approve',
416
+ ownsProcessGroup: options.identityOnly === true && process.platform !== 'win32',
334
417
  child: null,
335
418
  connection: null,
336
419
  initializeResponse: null,
337
420
  sessionId: '',
421
+ untrustedSessionId: '',
338
422
  state: 'connecting',
339
423
  error: '',
340
424
  stopReason: '',
@@ -366,6 +450,7 @@ class AcpRuntime extends EventEmitter {
366
450
  cwd: binding.cwd,
367
451
  env: binding.env,
368
452
  stdio: ['pipe', 'pipe', 'pipe'],
453
+ detached: binding.ownsProcessGroup,
369
454
  });
370
455
  binding.child = child;
371
456
  child.stderr.on('data', chunk => {
@@ -512,8 +597,12 @@ class AcpRuntime extends EventEmitter {
512
597
  this.sessionSetupTimeoutMs,
513
598
  'ACP session/new'
514
599
  );
515
- binding.sessionId = String(sessionResponse.sessionId || '');
516
- if (!binding.sessionId) throw new Error('ACP session/new did not return a session id');
600
+ const returnedSessionId = String(sessionResponse.sessionId || '').trim();
601
+ if (!isSafeProviderSessionId(returnedSessionId)) {
602
+ binding.untrustedSessionId = returnedSessionId;
603
+ throw new Error('ACP session/new returned an invalid resumable session id');
604
+ }
605
+ binding.sessionId = returnedSessionId;
517
606
  binding.sessionState = new AcpSessionState({ provider, sessionId: binding.sessionId, cwd: binding.cwd, maxUpdates: this.maxUpdates });
518
607
  }
519
608
  binding.modes = sessionResponse?.modes || null;
@@ -546,11 +635,109 @@ class AcpRuntime extends EventEmitter {
546
635
  } catch (error) {
547
636
  const runtimeError = new Error(acpErrorMessage(error), { cause: error });
548
637
  this.handleExit(binding, runtimeError);
549
- this.unregisterAgent(agentId);
638
+ if (options.identityOnly === true) {
639
+ const returnedIdentity = binding.sessionId || binding.untrustedSessionId;
640
+ const identity = returnedIdentity
641
+ ? {
642
+ provider,
643
+ executable: options.executable || getProviderAdapter(provider)?.executable,
644
+ env: binding.env,
645
+ cwd: binding.cwd,
646
+ sessionId: returnedIdentity,
647
+ producerStopped: false,
648
+ }
649
+ : null;
650
+ let producerStopped = false;
651
+ try {
652
+ await this.unregisterAgentAndWait(agentId);
653
+ producerStopped = true;
654
+ if (identity) identity.producerStopped = true;
655
+ } catch (cleanupError) {
656
+ Object.defineProperty(runtimeError, 'adapterCleanupError', {
657
+ value: cleanupError,
658
+ enumerable: false,
659
+ configurable: true,
660
+ });
661
+ if (identity) attachProviderSessionIdentity(runtimeError, identity);
662
+ }
663
+ if (identity && producerStopped && isSafeProviderSessionId(identity.sessionId)) {
664
+ try {
665
+ await this.deleteProviderSessionIdentity(identity);
666
+ } catch (rollbackError) {
667
+ attachProviderSessionIdentity(runtimeError, identity, rollbackError);
668
+ }
669
+ } else if (identity && producerStopped) {
670
+ attachProviderSessionIdentity(runtimeError, identity);
671
+ }
672
+ } else {
673
+ this.unregisterAgent(agentId);
674
+ }
550
675
  throw runtimeError;
551
676
  }
552
677
  }
553
678
 
679
+ async createSessionIdentity(options = {}) {
680
+ const agentId = `provider-session-${crypto.randomUUID()}`;
681
+ let prepared = null;
682
+ let result = null;
683
+ let failure = null;
684
+ let identity = null;
685
+ try {
686
+ prepared = await this.prepareAgent({
687
+ ...options,
688
+ agentId,
689
+ identityOnly: true,
690
+ sessionId: '',
691
+ });
692
+ const binding = this.requireBinding(agentId);
693
+ identity = {
694
+ provider: String(options.provider || '').trim().toLowerCase(),
695
+ executable: options.executable || getProviderAdapter(options.provider)?.executable,
696
+ env: binding.env,
697
+ cwd: binding.cwd,
698
+ sessionId: prepared.sessionId,
699
+ producerStopped: false,
700
+ };
701
+ result = {
702
+ ...prepared,
703
+ sessionRequestOptions: this.getSessionRequestOptions(agentId),
704
+ };
705
+ } catch (error) {
706
+ failure = error instanceof Error ? error : new Error(String(error || 'Provider session identity failed'));
707
+ }
708
+
709
+ if (prepared) {
710
+ let producerStopped = false;
711
+ try {
712
+ await this.unregisterAgentAndWait(agentId);
713
+ producerStopped = true;
714
+ if (identity) identity.producerStopped = true;
715
+ } catch (cleanupError) {
716
+ if (!failure) {
717
+ failure = cleanupError instanceof Error
718
+ ? cleanupError
719
+ : new Error(String(cleanupError || 'ACP identity adapter cleanup failed'));
720
+ } else {
721
+ Object.defineProperty(failure, 'adapterCleanupError', {
722
+ value: cleanupError,
723
+ enumerable: false,
724
+ configurable: true,
725
+ });
726
+ }
727
+ if (identity) failure = attachProviderSessionIdentity(failure, identity);
728
+ }
729
+ if (failure && identity?.sessionId && producerStopped) {
730
+ try {
731
+ await this.deleteProviderSessionIdentity(identity);
732
+ } catch (rollbackError) {
733
+ failure = attachProviderSessionIdentity(failure, identity, rollbackError);
734
+ }
735
+ }
736
+ }
737
+ if (failure) throw failure;
738
+ return result;
739
+ }
740
+
554
741
  checkpointIdentity(binding, sessionId = binding?.sessionId) {
555
742
  if (!binding || !sessionId) return null;
556
743
  return {
@@ -1589,11 +1776,10 @@ class AcpRuntime extends EventEmitter {
1589
1776
  this.emitRuntime(binding);
1590
1777
  }
1591
1778
 
1592
- unregisterAgent(agentId) {
1593
- const binding = this.bindings.get(agentId);
1594
- if (!binding) return;
1779
+ detachAgentBinding(binding) {
1780
+ if (!binding || this.bindings.get(binding.agentId) !== binding) return false;
1595
1781
  if (binding.sessionState && !binding.promptActive) this.scheduleCheckpoint(binding, { exact: true });
1596
- this.bindings.delete(agentId);
1782
+ this.bindings.delete(binding.agentId);
1597
1783
  for (const resolve of binding.permissionResolvers.values()) resolve({ outcome: { outcome: 'cancelled' } });
1598
1784
  for (const resolve of binding.elicitationResolvers.values()) resolve({ action: 'cancel' });
1599
1785
  binding.permissionResolvers.clear();
@@ -1607,9 +1793,33 @@ class AcpRuntime extends EventEmitter {
1607
1793
  try {
1608
1794
  binding.connection?.close();
1609
1795
  } catch {
1610
- // The adapter process is terminated below even if transport cleanup raced its exit.
1796
+ // Process cleanup below remains authoritative if transport cleanup races.
1797
+ }
1798
+ if (binding.child?.stdin?.writable && !binding.child.stdin.destroyed) {
1799
+ try {
1800
+ binding.child.stdin.end();
1801
+ } catch {
1802
+ // Process cleanup below remains authoritative if stdin already closed.
1803
+ }
1611
1804
  }
1612
- if (binding.child && !binding.child.killed) binding.child.kill('SIGTERM');
1805
+ return true;
1806
+ }
1807
+
1808
+ unregisterAgent(agentId) {
1809
+ const binding = this.bindings.get(agentId);
1810
+ if (!binding || !this.detachAgentBinding(binding)) return;
1811
+ signalProcessTree(binding, 'SIGTERM');
1812
+ }
1813
+
1814
+ async unregisterAgentAndWait(agentId) {
1815
+ const binding = this.bindings.get(agentId);
1816
+ if (!binding || !this.detachAgentBinding(binding)) return;
1817
+ if (await waitForProcessTreeExit(binding, IDENTITY_ADAPTER_GRACEFUL_EXIT_MS)) return;
1818
+ signalProcessTree(binding, 'SIGTERM');
1819
+ if (await waitForProcessTreeExit(binding, IDENTITY_ADAPTER_TERMINATE_MS)) return;
1820
+ signalProcessTree(binding, 'SIGKILL');
1821
+ if (await waitForProcessTreeExit(binding, IDENTITY_ADAPTER_KILL_MS)) return;
1822
+ throw new Error(`ACP identity adapter process tree ${binding.child?.pid || ''} did not exit`);
1613
1823
  }
1614
1824
 
1615
1825
  async dispose() {
@@ -1634,4 +1844,5 @@ module.exports = {
1634
1844
  supportsCodexSteer,
1635
1845
  isCodexSteerUnavailableError,
1636
1846
  CODEX_STEER_METHOD,
1847
+ deleteProviderSessionIdentity,
1637
1848
  };