farming-code 2.2.28 → 2.2.30

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 (83) hide show
  1. package/README.md +27 -90
  2. package/README.zh_cn.md +17 -90
  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 +38 -17
  7. package/backend/acp-session-state.js +78 -88
  8. package/backend/acp-transcript.js +174 -9
  9. package/backend/agent-manager.js +27 -2
  10. package/backend/agent-session-history.js +45 -12
  11. package/backend/codex-session-history.js +1 -1
  12. package/backend/codex-transcript.js +0 -202
  13. package/backend/config-manager.js +44 -16
  14. package/backend/data/LICENSE.agent-browser +201 -0
  15. package/backend/data/runtime-dependency-manifest.json +199 -0
  16. package/backend/data/runtime-dependency-sources.json +5 -0
  17. package/backend/executable-discovery.js +7 -0
  18. package/backend/farming-agent-bootstrap.zh_cn.md +4 -2
  19. package/backend/farming-app-cli.js +161 -65
  20. package/backend/farming-cli.js +7 -59
  21. package/backend/index-html.js +31 -0
  22. package/backend/main-agent-skills.js +7 -28
  23. package/backend/npm-update-helper.js +43 -6
  24. package/backend/provider-adapters.js +10 -0
  25. package/backend/runtime-dependency-manager.js +759 -0
  26. package/backend/runtime-executable-invocation.js +20 -0
  27. package/backend/server-process-identity.js +47 -0
  28. package/backend/server.js +145 -204
  29. package/backend/slash-command-discovery.js +138 -1
  30. package/backend/storage-layout.js +30 -0
  31. package/backend/update-service.js +60 -914
  32. package/config/farming.deploy.env.example +1 -1
  33. package/config/farming.install.env.example +5 -1
  34. package/dist/acp/LICENSE.claude-agent-acp +191 -0
  35. package/dist/acp/LICENSE.claude-agent-sdk +1 -0
  36. package/dist/acp/claude-agent-acp-0.59.0.mjs +41606 -0
  37. package/dist/acp/{codex-acp-1.1.4.js → codex-acp-1.1.4.mjs} +387 -7
  38. package/dist/assets/{App-CM7Cy-l4.css → App-BKNUILuL.css} +1 -1
  39. package/dist/assets/App-Z1N7T0eM.js +695 -0
  40. package/dist/assets/{FileEditorMarkdownPreview-DYOMm-b1.js → FileEditorMarkdownPreview-Cs9cLv8a.js} +1 -1
  41. package/dist/assets/FileEditorPane-CNtuGjQ3.js +2 -0
  42. package/dist/assets/IconGlyphs-BClgag6E.js +1 -0
  43. package/dist/assets/ProjectFilesSection-K3GHuQ4U.js +12 -0
  44. package/dist/assets/ReviewPage-BRuxFaH5.js +3 -0
  45. package/dist/assets/code-dark-kLsQLvpf.css +1 -0
  46. package/dist/assets/{index-BzQO3pUr.js → index-D-zetSol.js} +2 -2
  47. package/dist/assets/main-DUTz-Rd2.css +1 -0
  48. package/dist/assets/{workspace-editor-monaco-tXA-udGx.js → workspace-editor-monaco-CJNwbpCy.js} +1 -1
  49. package/dist/assets/workspace-editor-monaco-CO9w8YV0.js +1 -0
  50. package/dist/assets/workspace-view-state-BjKPYp0E.js +7 -0
  51. package/dist/index.html +33 -3
  52. package/extensions/browser/backend/agent-browser-runtime.js +1041 -0
  53. package/extensions/browser/backend/browser-mcp-server.js +352 -0
  54. package/extensions/browser/backend/browser-resource-manager.js +921 -114
  55. package/extensions/browser/backend/browser-resource-store.js +47 -6
  56. package/extensions/browser/backend/browser-router.js +15 -2
  57. package/extensions/browser/backend/executable-discovery.js +164 -17
  58. package/extensions/browser/backend/managed-chromium-installer.js +593 -0
  59. package/extensions/browser/bin/farming-browser +384 -30
  60. package/extensions/browser/frontend/BrowserSidebarPortals.tsx +17 -19
  61. package/extensions/browser/frontend/BrowserViewer.tsx +217 -46
  62. package/extensions/browser/frontend/browser.css +68 -17
  63. package/extensions/browser/frontend/types.ts +13 -0
  64. package/extensions/browser/frontend/useBrowserResources.ts +18 -3
  65. package/index.html +32 -2
  66. package/package.json +10 -6
  67. package/shared/backend-connection-status.d.ts +12 -0
  68. package/shared/backend-connection-status.js +35 -4
  69. package/backend/agent-activity.js +0 -35
  70. package/backend/agent-memory-report.js +0 -583
  71. package/backend/bundle-update-helper.js +0 -113
  72. package/backend/codex-rollout-follower.js +0 -326
  73. package/dist/assets/App-CPG4WcjO.js +0 -658
  74. package/dist/assets/FileEditorPane-DXXq82aM.js +0 -2
  75. package/dist/assets/IconGlyphs-DrdPjYXr.js +0 -1
  76. package/dist/assets/ProjectFilesSection-CP66XVPl.js +0 -12
  77. package/dist/assets/ReviewPage-w21MEPD6.js +0 -3
  78. package/dist/assets/code-dark-BDjwj7Um.css +0 -1
  79. package/dist/assets/main-BlVUXfxa.css +0 -1
  80. package/dist/assets/workspace-editor-monaco-CFEfFNcA.js +0 -1
  81. package/dist/assets/workspace-view-state-BS_V8JVs.js +0 -7
  82. package/extensions/browser/backend/cdp-browser-runtime.js +0 -648
  83. package/extensions/browser/backend/cdp-client.js +0 -167
@@ -8,6 +8,12 @@ const { URLSearchParams } = require('url');
8
8
  const { execFileSync, spawn } = require('child_process');
9
9
  const { run: runControlCli } = require('./farming-cli');
10
10
  const { PACKAGED_CODEX_ACP_ARG, runPackagedCodexAcp } = require('./acp/packaged-codex-acp');
11
+ const { PACKAGED_CLAUDE_ACP_ARG, runPackagedClaudeAcp } = require('./acp/packaged-claude-acp');
12
+ const {
13
+ SERVER_PROCESS_IDENTITY_FORMAT,
14
+ matchingProcessIdentity,
15
+ readServerProcessIdentity,
16
+ } = require('./server-process-identity');
11
17
  const storageLayout = require('./storage-layout');
12
18
 
13
19
  const SERVER_MODE_ARG = '--farming-server';
@@ -20,9 +26,8 @@ const DEFAULT_SERVER_START_TIMEOUT_MS = 30_000;
20
26
  const DEFAULT_SERVER_START_STABILITY_MS = 1_500;
21
27
  const DEFAULT_SERVER_STOP_TIMEOUT_MS = 30_000;
22
28
  const SERVER_STOP_POLL_MS = 100;
23
- const SERVER_PROCESS_IDENTITY_FORMAT = 'ps-lstart-c-utc-v1';
24
29
  const SERVER_COMMANDS = new Set(['start', 'serve', 'daemon', 'stop', 'status', 'logs', 'url', 'help']);
25
- const CONTROL_COMMANDS = new Set(['skills', 'capabilities', 'memory', 'report', 'list', 'spawn', 'output', 'send', 'kill']);
30
+ const CONTROL_COMMANDS = new Set(['skills', 'capabilities', 'list', 'spawn', 'output', 'send', 'kill']);
26
31
  const SERVER_BACKED_CONTROL_COMMANDS = new Set(['capabilities', 'list', 'spawn', 'output', 'send', 'kill']);
27
32
 
28
33
  function defaultConfigDir(env = process.env) {
@@ -437,7 +442,9 @@ function childInvocation(env = process.env) {
437
442
  // making daemon ownership impossible to prove on Linux.
438
443
  return {
439
444
  command: '/usr/bin/env',
440
- args: buildCleanEnvExecArgs(env, process.execPath, ['--']),
445
+ // FARMING_RUN_SERVER selects the embedded server entry. Do not append a
446
+ // sentinel argument: pkg may interpret it as an external script path.
447
+ args: buildCleanEnvExecArgs(env, process.execPath),
441
448
  };
442
449
  }
443
450
  const nodePath = env.FARMING_NODE_BIN || process.execPath;
@@ -482,45 +489,20 @@ function canonicalConfigDir(configDir) {
482
489
  }
483
490
  }
484
491
 
485
- async function readServerProcessIdentity(pid) {
486
- const processId = Number(pid);
487
- if (!Number.isSafeInteger(processId) || processId <= 0 || process.platform === 'win32') return null;
488
- let stdout;
489
- try {
490
- stdout = execFileSync(
491
- '/bin/ps',
492
- ['-p', String(processId), '-o', 'pid=', '-o', 'pgid=', '-o', 'lstart='],
493
- {
494
- encoding: 'utf8',
495
- timeout: 1_000,
496
- maxBuffer: 16_384,
497
- env: { ...process.env, LANG: 'C', LC_ALL: 'C', TZ: 'UTC' },
498
- },
499
- );
500
- } catch (error) {
501
- if (error?.status === 1 || error?.code === 'ESRCH') return null;
502
- throw error;
503
- }
504
- const match = String(stdout || '').trim().match(/^(\d+)\s+(\d+)\s+(.+)$/);
505
- if (!match || Number(match[1]) !== processId) return null;
506
- return {
507
- pid: processId,
508
- processGroupId: Number(match[2]),
509
- startedAt: match[3].trim(),
510
- format: SERVER_PROCESS_IDENTITY_FORMAT,
511
- };
512
- }
513
-
514
- function writeServerState(configDir, env, pid, processIdentity) {
492
+ function writeServerState(configDir, env, pid, processIdentity, phase = 'running') {
515
493
  const state = {
516
494
  pid,
517
495
  port: Number(env.PORT || DEFAULT_PORT),
518
496
  basePath: env.FARMING_BASE_PATH || DEFAULT_BASE_PATH,
519
497
  configDir: canonicalConfigDir(configDir),
520
498
  processIdentity,
499
+ phase,
521
500
  updatedAt: new Date().toISOString(),
522
501
  };
523
- fs.writeFileSync(serverStateFile(configDir), `${JSON.stringify(state, null, 2)}\n`);
502
+ const target = serverStateFile(configDir);
503
+ const temporary = `${target}.${process.pid}.tmp`;
504
+ fs.writeFileSync(temporary, `${JSON.stringify(state, null, 2)}\n`, { mode: 0o600 });
505
+ fs.renameSync(temporary, target);
524
506
  }
525
507
 
526
508
  function processLooksLikeFarmingServer(pid) {
@@ -664,15 +646,6 @@ function processOwnsListeningPort(pid, port) {
664
646
  }
665
647
  }
666
648
 
667
- function matchingProcessIdentity(expected, current) {
668
- return Boolean(
669
- current
670
- && current.pid === Number(expected?.pid)
671
- && current.processGroupId === Number(expected?.processGroupId)
672
- && current.startedAt === String(expected?.startedAt || '')
673
- );
674
- }
675
-
676
649
  async function assertServerProcessIdentity(configDir, pid, state) {
677
650
  const expected = state?.processIdentity;
678
651
  if (
@@ -694,7 +667,7 @@ async function assertServerProcessIdentity(configDir, pid, state) {
694
667
  `Refusing to stop Farming PID ${pid}: the live process identity does not match the server control metadata`,
695
668
  );
696
669
  }
697
- if (!processOwnsListeningPort(pid, Number(state.port))) {
670
+ if (state.phase !== 'starting' && !processOwnsListeningPort(pid, Number(state.port))) {
698
671
  throw new Error(`Refusing to stop Farming PID ${pid}: the process does not own listening port ${state.port}`);
699
672
  }
700
673
  return current;
@@ -752,7 +725,8 @@ function isRunning(pid) {
752
725
  try {
753
726
  process.kill(pid, 0);
754
727
  return true;
755
- } catch {
728
+ } catch (error) {
729
+ if (error?.code === 'EPERM' || error?.code === 'EACCES') return true;
756
730
  return false;
757
731
  }
758
732
  }
@@ -840,12 +814,31 @@ function waitForProcessStability(pid, durationMs = DEFAULT_SERVER_START_STABILIT
840
814
  });
841
815
  }
842
816
 
843
- function cleanupFailedDaemonStart(configDir, childPid) {
817
+ async function cleanupFailedDaemonStart(configDir, childPid, expectedIdentity = null) {
844
818
  if (isRunning(childPid)) {
819
+ const expected = expectedIdentity || readServerState(configDir).processIdentity;
820
+ const current = readServerProcessIdentity(childPid);
821
+ if (!matchingProcessIdentity(expected, current)) {
822
+ throw new Error(`Failed Farming startup left live PID ${childPid} with an unproved identity; refusing to signal it`);
823
+ }
845
824
  try {
846
- process.kill(childPid, 'SIGTERM');
847
- } catch {
848
- // The child may exit between the liveness check and the signal.
825
+ process.kill(childPid, 'SIGKILL');
826
+ } catch (error) {
827
+ if (error?.code === 'EPERM' || error?.code === 'EACCES') {
828
+ throw new Error(
829
+ `Failed Farming startup left Server PID ${childPid} running because cleanup lacks permission. `
830
+ + 'Stop it as the operating-system user that owns the process, then retry.',
831
+ { cause: error },
832
+ );
833
+ }
834
+ if (error?.code !== 'ESRCH') throw error;
835
+ }
836
+ const startedAt = Date.now();
837
+ while (matchingProcessIdentity(expected, readServerProcessIdentity(childPid))) {
838
+ if (Date.now() - startedAt >= 5_000) {
839
+ throw new Error(`Failed Farming startup left Server PID ${childPid} running after SIGKILL`);
840
+ }
841
+ await new Promise(resolve => setTimeout(resolve, SERVER_STOP_POLL_MS));
849
842
  }
850
843
  }
851
844
  if (readPid(configDir) !== childPid) return;
@@ -910,10 +903,31 @@ function waitForServer(env, timeoutMs = serverStartTimeoutMs(env), childPid = 0)
910
903
  });
911
904
  }
912
905
 
913
- function runServerInCurrentProcess() {
906
+ async function runServerInCurrentProcess() {
914
907
  process.env = buildServerEnv();
908
+ ensureConfigDir(process.env.FARMING_CONFIG_DIR);
909
+ fs.writeFileSync(pidFile(process.env.FARMING_CONFIG_DIR), String(process.pid), { mode: 0o600 });
910
+ const processIdentity = await readServerProcessIdentity(process.pid);
911
+ if (!processIdentity) throw new Error('server process identity could not be committed before startup');
912
+ writeServerState(process.env.FARMING_CONFIG_DIR, process.env, process.pid, processIdentity, 'starting');
915
913
  const { startServer } = require('./server');
916
- startServer();
914
+ const server = startServer();
915
+ server.once('listening', () => {
916
+ writeServerState(
917
+ process.env.FARMING_CONFIG_DIR,
918
+ process.env,
919
+ process.pid,
920
+ processIdentity,
921
+ 'running',
922
+ );
923
+ const { pruneRuntimeDependencies } = require('./runtime-dependency-manager');
924
+ void pruneRuntimeDependencies({
925
+ configDir: process.env.FARMING_CONFIG_DIR,
926
+ env: process.env,
927
+ }).catch(error => {
928
+ console.warn(`Startup dependency cache cleanup failed: ${error?.message || error}`);
929
+ });
930
+ });
917
931
  }
918
932
 
919
933
  function runNativePtyHostInCurrentProcess() {
@@ -921,8 +935,24 @@ function runNativePtyHostInCurrentProcess() {
921
935
  startNativePtyHostProcess();
922
936
  }
923
937
 
938
+ async function prepareStartupDependencies(env, options = {}) {
939
+ if (
940
+ options.force !== true
941
+ && (env.FARMING_SKIP_RUNTIME_PREPARE === '1' || env.NODE_ENV === 'test')
942
+ ) {
943
+ return { skipped: true };
944
+ }
945
+ const { prepareRuntimeDependencies } = require('./runtime-dependency-manager');
946
+ return prepareRuntimeDependencies({
947
+ configDir: env.FARMING_CONFIG_DIR,
948
+ env,
949
+ });
950
+ }
951
+
924
952
  async function startForeground(parsed) {
925
- const env = await adaptServerPort(buildServerEnv(parsed.env), parsed);
953
+ const env = buildServerEnv(parsed.env);
954
+ await prepareStartupDependencies(env);
955
+ await adaptServerPort(env, parsed);
926
956
  env[SERVER_MODE_ENV] = '1';
927
957
  ensureConfigDir(env.FARMING_CONFIG_DIR);
928
958
  const invocation = childInvocation(env);
@@ -952,13 +982,33 @@ async function startDaemon(parsed) {
952
982
  const existingPid = readPid(configDir);
953
983
  if (isRunning(existingPid)) {
954
984
  const state = readServerState(configDir);
955
- if (state.port) env.PORT = String(state.port);
956
- if (state.basePath) env.FARMING_BASE_PATH = state.basePath;
957
- console.log(`Farming is already running (PID ${existingPid})`);
958
- console.log(entryUrl(env));
959
- return 0;
985
+ try {
986
+ if (state.processIdentity?.format === SERVER_PROCESS_IDENTITY_FORMAT) {
987
+ await assertServerProcessIdentity(configDir, existingPid, state);
988
+ } else {
989
+ await migrateLegacyServerIdentity(configDir, existingPid, state);
990
+ }
991
+ } catch (error) {
992
+ console.error(
993
+ `Farming PID ${existingPid} is live, but this config directory does not prove that it owns the Server: `
994
+ + `${error.message || error}`,
995
+ );
996
+ return 1;
997
+ }
998
+ if (state.phase === 'starting') {
999
+ console.warn(`Previous Farming startup (PID ${existingPid}) did not complete; replacing it.`);
1000
+ const stopCode = await stopDaemon({ env });
1001
+ if (stopCode !== 0) return stopCode;
1002
+ } else {
1003
+ if (state.port) env.PORT = String(state.port);
1004
+ if (state.basePath) env.FARMING_BASE_PATH = state.basePath;
1005
+ console.log(`Farming is already running (PID ${existingPid})`);
1006
+ console.log(entryUrl(env));
1007
+ return 0;
1008
+ }
960
1009
  }
961
1010
 
1011
+ await prepareStartupDependencies(env);
962
1012
  const out = fs.openSync(logFile(configDir), 'a');
963
1013
  const invocation = childInvocation(env);
964
1014
  if (env.FARMING_DEBUG_CLI === '1') {
@@ -979,9 +1029,11 @@ async function startDaemon(parsed) {
979
1029
  child.unref();
980
1030
  fs.writeFileSync(pidFile(configDir), String(child.pid));
981
1031
 
1032
+ let processIdentity = null;
982
1033
  try {
983
- const processIdentity = await readServerProcessIdentity(child.pid);
1034
+ processIdentity = await readServerProcessIdentity(child.pid);
984
1035
  if (!processIdentity) throw new Error('server process identity could not be verified after launch');
1036
+ writeServerState(configDir, env, child.pid, processIdentity, 'starting');
985
1037
  await waitForServer(env, serverStartTimeoutMs(env), child.pid);
986
1038
  await waitForProcessStability(child.pid, serverStartStabilityMs(env));
987
1039
  await waitForServer(env, Math.min(serverStartTimeoutMs(env), 5_000), child.pid);
@@ -993,9 +1045,14 @@ async function startDaemon(parsed) {
993
1045
  ) {
994
1046
  throw new Error('server process identity changed during startup');
995
1047
  }
996
- writeServerState(configDir, env, child.pid, stableIdentity);
1048
+ writeServerState(configDir, env, child.pid, stableIdentity, 'running');
997
1049
  } catch (error) {
998
- cleanupFailedDaemonStart(configDir, child.pid);
1050
+ try {
1051
+ await cleanupFailedDaemonStart(configDir, child.pid, processIdentity);
1052
+ } catch (cleanupError) {
1053
+ console.error(`${error.message}; cleanup failed: ${cleanupError.message || cleanupError}`);
1054
+ return 1;
1055
+ }
999
1056
  console.error(error.message);
1000
1057
  const logs = tailFile(logFile(configDir), 80);
1001
1058
  if (logs) console.error(logs);
@@ -1031,8 +1088,15 @@ async function stopDaemon(parsed) {
1031
1088
  await migrateLegacyServerIdentity(configDir, pid, state);
1032
1089
  }
1033
1090
  try {
1034
- process.kill(pid, 'SIGTERM');
1091
+ process.kill(pid, 'SIGKILL');
1035
1092
  } catch (error) {
1093
+ if (error?.code === 'EPERM' || error?.code === 'EACCES') {
1094
+ throw new Error(
1095
+ `Farming cannot stop server ${pid} because this command lacks permission. `
1096
+ + 'Run it as the operating-system user that owns the process (or as an administrator), then start Farming again.',
1097
+ { cause: error },
1098
+ );
1099
+ }
1036
1100
  if (error?.code !== 'ESRCH') throw error;
1037
1101
  }
1038
1102
  await waitForDaemonStop(pid, port, { timeoutMs: serverStopTimeoutMs(env) });
@@ -1042,7 +1106,7 @@ async function stopDaemon(parsed) {
1042
1106
  return 0;
1043
1107
  }
1044
1108
 
1045
- function statusDaemon(parsed) {
1109
+ async function statusDaemon(parsed) {
1046
1110
  const env = buildServerEnv(parsed.env);
1047
1111
  const configDir = env.FARMING_CONFIG_DIR;
1048
1112
  const pid = readPid(configDir);
@@ -1050,6 +1114,24 @@ function statusDaemon(parsed) {
1050
1114
  console.log('Farming is not running.');
1051
1115
  return 0;
1052
1116
  }
1117
+ const state = readServerState(configDir);
1118
+ try {
1119
+ if (state.processIdentity?.format === SERVER_PROCESS_IDENTITY_FORMAT) {
1120
+ await assertServerProcessIdentity(configDir, pid, state);
1121
+ } else {
1122
+ await migrateLegacyServerIdentity(configDir, pid, state);
1123
+ }
1124
+ } catch (error) {
1125
+ console.error(
1126
+ `Farming PID ${pid} is live, but this config directory does not prove that it owns the Server: `
1127
+ + `${error.message || error}`,
1128
+ );
1129
+ return 1;
1130
+ }
1131
+ if (state.phase === 'starting') {
1132
+ console.log(`Farming is starting (PID ${pid}). Run 'farming start' to replace an interrupted startup.`);
1133
+ return 1;
1134
+ }
1053
1135
  console.log(`Farming is running (PID ${pid})`);
1054
1136
  const logs = tailFile(logFile(configDir), 30);
1055
1137
  if (logs) console.log(logs);
@@ -1082,12 +1164,13 @@ function usage() {
1082
1164
  farming stop
1083
1165
  farming logs
1084
1166
  farming url
1167
+ farming runtime prepare [--config-dir ~/.farming]
1085
1168
  farming review <git-dir> <old-revision> <new-revision|now> [--branch <branch>] [--no-open]
1086
1169
 
1087
1170
  Agent control commands are also available:
1088
1171
  farming skills
1089
1172
  farming capabilities [--json]
1090
- farming browser <capability|create|list|start|stop|snapshot|navigate|click|fill|type|press|scroll|screenshot> ...
1173
+ farming browser ...
1091
1174
  farming list [--json] [--parent <agentId>]
1092
1175
  farming spawn --workspace <repo> -- <command...>
1093
1176
  farming output <agentId> [--tail <chars>]
@@ -1106,7 +1189,12 @@ async function run(argv = process.argv.slice(2)) {
1106
1189
  }
1107
1190
 
1108
1191
  if (argv[0] === PACKAGED_CODEX_ACP_ARG) {
1109
- runPackagedCodexAcp();
1192
+ await runPackagedCodexAcp();
1193
+ return 0;
1194
+ }
1195
+
1196
+ if (argv[0] === PACKAGED_CLAUDE_ACP_ARG) {
1197
+ await runPackagedClaudeAcp();
1110
1198
  return 0;
1111
1199
  }
1112
1200
 
@@ -1123,7 +1211,7 @@ async function run(argv = process.argv.slice(2)) {
1123
1211
  }
1124
1212
 
1125
1213
  if (process.env[SERVER_MODE_ENV] === '1' || argv[0] === SERVER_MODE_ARG) {
1126
- runServerInCurrentProcess();
1214
+ await runServerInCurrentProcess();
1127
1215
  return 0;
1128
1216
  }
1129
1217
 
@@ -1132,6 +1220,14 @@ async function run(argv = process.argv.slice(2)) {
1132
1220
  return runBrowserCli(argv.slice(1));
1133
1221
  }
1134
1222
 
1223
+ if (argv[0] === 'runtime' && argv[1] === 'prepare') {
1224
+ const parsed = parseServerArgs(['start', ...argv.slice(2)]);
1225
+ const env = buildServerEnv(parsed.env);
1226
+ const result = await prepareStartupDependencies(env, { force: true });
1227
+ console.log(JSON.stringify(result, null, 2));
1228
+ return 0;
1229
+ }
1230
+
1135
1231
  if (SERVER_BACKED_CONTROL_COMMANDS.has(argv[0])) {
1136
1232
  return runServerBackedControlCli(argv);
1137
1233
  }
@@ -1,7 +1,6 @@
1
1
  const fs = require('fs');
2
2
  const http = require('http');
3
3
  const https = require('https');
4
- const { buildMemoryReport, formatMemoryReport } = require('./agent-memory-report');
5
4
  const { renderMainAgentSkills } = require('./main-agent-skills');
6
5
  const storageLayout = require('./storage-layout');
7
6
 
@@ -9,7 +8,6 @@ function usage() {
9
8
  return `Usage:
10
9
  farming skills
11
10
  farming capabilities [--json]
12
- farming memory report [--period today|yesterday|week] [--since <time>] [--until <time>] [--home <path>] [--json]
13
11
  farming list [--json] [--parent <agentId>]
14
12
  farming spawn [--workspace <path>] [--task <text>] [--parent <agentId>] [--json] -- <command...>
15
13
  farming output <agentId> [--tail <chars>]
@@ -18,8 +16,6 @@ function usage() {
18
16
 
19
17
  Examples:
20
18
  farming spawn --workspace /repo --task "Inspect this module for bugs" -- claude
21
- farming memory report --period today
22
- farming memory report --period week --json
23
19
  farming skills
24
20
  farming capabilities
25
21
  farming list --parent "$FARMING_AGENT_ID"
@@ -85,43 +81,6 @@ function parseArgs(argv) {
85
81
  return { command, options: { json: rest.includes('--json') } };
86
82
  }
87
83
 
88
- if (command === 'memory' || command === 'report') {
89
- const subcommand = command === 'memory' ? rest.shift() : 'report';
90
- if (subcommand !== 'report') {
91
- throw new Error('memory requires the report subcommand');
92
- }
93
-
94
- const options = {
95
- period: 'today',
96
- since: '',
97
- until: '',
98
- homeDir: '',
99
- json: false,
100
- };
101
-
102
- for (let i = 0; i < rest.length; i++) {
103
- if (rest[i] === '--period') {
104
- options.period = splitOptionValue(rest, i, '--period');
105
- i++;
106
- } else if (rest[i] === '--since') {
107
- options.since = splitOptionValue(rest, i, '--since');
108
- i++;
109
- } else if (rest[i] === '--until') {
110
- options.until = splitOptionValue(rest, i, '--until');
111
- i++;
112
- } else if (rest[i] === '--home') {
113
- options.homeDir = splitOptionValue(rest, i, '--home');
114
- i++;
115
- } else if (rest[i] === '--json') {
116
- options.json = true;
117
- } else {
118
- throw new Error(`Unknown option: ${rest[i]}`);
119
- }
120
- }
121
-
122
- return { command: 'memory-report', options };
123
- }
124
-
125
84
  if (command === 'list') {
126
85
  const options = { json: false, parent: '' };
127
86
  for (let i = 0; i < rest.length; i++) {
@@ -314,13 +273,17 @@ function farmingCapabilities(browser) {
314
273
  id: 'browser',
315
274
  state,
316
275
  summary: state === 'available'
317
- ? 'An installed system Chromium Browser can be created or attached in Farming on demand.'
318
- : (browser?.message || 'System Browser integration is unavailable in Farming.'),
276
+ ? (browser?.browser?.kind === 'external-cdp'
277
+ ? 'Default browser path for web tasks. An externally managed CDP Browser can be created or attached as a shared, user-visible Farming Resource.'
278
+ : browser?.browser?.kind === 'managed-chromium'
279
+ ? 'Default browser path for web tasks. Farming-managed Chromium can be created or attached as a shared, user-visible Farming Resource.'
280
+ : 'Default browser path for web tasks. An installed system Chromium Browser can be created or attached as a shared, user-visible Farming Resource.')
281
+ : (browser?.message || 'Browser integration is unavailable in Farming.'),
319
282
  commands: state === 'available'
320
283
  ? {
321
284
  list: 'farming browser list',
322
285
  create: 'farming browser create',
323
- help: 'farming browser --help',
286
+ workflow: 'farming browser help workflow',
324
287
  }
325
288
  : {},
326
289
  }],
@@ -359,21 +322,6 @@ async function run(argv = process.argv.slice(2), io = process) {
359
322
  return 0;
360
323
  }
361
324
 
362
- if (parsed.command === 'memory-report') {
363
- const report = buildMemoryReport({
364
- period: parsed.options.period,
365
- since: parsed.options.since,
366
- until: parsed.options.until,
367
- homeDir: parsed.options.homeDir || undefined,
368
- });
369
- if (parsed.options.json) {
370
- io.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
371
- } else {
372
- io.stdout.write(`${formatMemoryReport(report)}\n`);
373
- }
374
- return 0;
375
- }
376
-
377
325
  if (parsed.command === 'list') {
378
326
  const query = parsed.options.parent ? `?parent=${encodeURIComponent(parsed.options.parent)}` : '';
379
327
  const state = await request(`/api/control/agents${query}`);
@@ -41,7 +41,38 @@ function appendIndexHtmlAssetToken(html, token) {
41
41
  });
42
42
  }
43
43
 
44
+ function applyIndexHtmlAppearance(html, appearance) {
45
+ const normalizedAppearance = ['light', 'dark'].includes(appearance) ? appearance : 'system';
46
+ let source = String(html || '');
47
+ if (/\bdata-appearance-preference="[^"]*"/i.test(source)) {
48
+ source = source.replace(
49
+ /\bdata-appearance-preference="[^"]*"/i,
50
+ `data-appearance-preference="${normalizedAppearance}"`
51
+ );
52
+ } else {
53
+ source = source.replace(
54
+ /<html\b/i,
55
+ `<html data-appearance-preference="${normalizedAppearance}"`
56
+ );
57
+ }
58
+
59
+ const colorScheme = normalizedAppearance === 'system'
60
+ ? 'light dark'
61
+ : normalizedAppearance;
62
+ const themeColor = normalizedAppearance === 'dark' ? '#181818' : '#ffffff';
63
+ return source
64
+ .replace(
65
+ /(<meta\s+name="color-scheme"\s+content=")[^"]*(")/i,
66
+ `$1${colorScheme}$2`
67
+ )
68
+ .replace(
69
+ /(<meta\s+name="theme-color"\s+content=")[^"]*(")/i,
70
+ `$1${themeColor}$2`
71
+ );
72
+ }
73
+
44
74
  module.exports = {
75
+ applyIndexHtmlAppearance,
45
76
  normalizeBasePath,
46
77
  routePath,
47
78
  rewriteIndexHtmlForBasePath,
@@ -4,7 +4,7 @@ const path = require('path');
4
4
  const MANAGED_BLOCK_START = '<!-- FARMING_MAIN_AGENT_SKILLS:START -->';
5
5
  const MANAGED_BLOCK_END = '<!-- FARMING_MAIN_AGENT_SKILLS:END -->';
6
6
 
7
- const REMOVED_MAIN_AGENT_SKILL_IDS = [];
7
+ const REMOVED_MAIN_AGENT_SKILL_IDS = ['memory-report'];
8
8
 
9
9
  const MAIN_AGENT_OPERATING_GUIDE = [
10
10
  {
@@ -19,7 +19,6 @@ const MAIN_AGENT_OPERATING_GUIDE = [
19
19
  lines: [
20
20
  'On startup or resume, orient yourself with `farming list --json` and inspect `FARMING_PROJECT_WORKSPACE` / `FARMING_MAIN_WORKSPACE` before proposing work.',
21
21
  'Do not spawn child agents just because you can. Wait for a concrete user goal, or ask one short clarification if the goal is too vague to route safely.',
22
- 'Use `farming memory report` only when recent local context would change what you do next.',
23
22
  ],
24
23
  },
25
24
  {
@@ -61,28 +60,12 @@ const MAIN_AGENT_SKILLS = [
61
60
  id: 'browser-resource',
62
61
  name: '系统浏览器操作',
63
62
  trigger: '用户要求打开、查看或操作 Farming Project 下的 Browser Resource,或者需要绕过静态 HTML Viewer 限制验证真实网页时',
64
- summary: '通过 Farming Browser ID 操作用户当前看到的同一个 Headless 系统浏览器;先 Snapshot 获取当前 Ref,再进行点击或输入,页面变化后重新 Snapshot。',
63
+ summary: '通过 Farming Browser ID 操作用户当前看到的同一个系统浏览器;先走 Browser 的渐进式帮助和标准 Workflow,只在需要时展开具体问题域与原子命令。',
65
64
  commands: [
66
- 'farming browser list --workspace <repo>',
67
- 'farming browser start <browser-id>',
68
- 'farming browser snapshot <browser-id>',
69
- 'farming browser navigate <browser-id> <url>',
70
- 'farming browser click <browser-id> <ref-or-css=selector>',
71
- 'farming browser fill <browser-id> <ref-or-css=selector> <text>',
72
- 'farming browser screenshot <browser-id> <output.png>',
73
- 'farming browser stop <browser-id>',
74
- ],
75
- },
76
- {
77
- id: 'memory-report',
78
- name: '记忆读取总结',
79
- trigger: '用户要求总结今日、昨日、本周做过什么,或需要先了解这台机器上各 agent 的近期工作记忆时',
80
- summary: '只读扫描本机 Claude/Qwen/Codex 本地历史线索,按时间段生成 Farming 日报或周报。',
81
- commands: [
82
- 'farming memory report --period today',
83
- 'farming memory report --period yesterday',
84
- 'farming memory report --period week',
85
- 'farming memory report --since <YYYY-MM-DD> --until <YYYY-MM-DD>',
65
+ 'farming capabilities',
66
+ 'farming browser list',
67
+ 'farming browser help workflow',
68
+ 'farming browser help <lifecycle|navigation|interaction|inspection|debugging|state|files>',
86
69
  ],
87
70
  },
88
71
  {
@@ -153,8 +136,7 @@ function renderMainAgentSkills() {
153
136
  });
154
137
 
155
138
  lines.push('Rules:');
156
- lines.push('- Use `farming browser` only for Browser Resources already in the user-selected Project scope; take a new snapshot after navigation or DOM-changing actions.');
157
- lines.push('- Use `farming memory report` when you need recent context from local agent memories.');
139
+ lines.push('- Use `farming browser` only for Browser Resources already in the user-selected Project scope; follow `farming browser help workflow`, and reveal only the help topic or command needed for the current step.');
158
140
  lines.push('- Use “牧场除虫计划” when the user asks for systematic bug hunting across a directory or module tree.');
159
141
  lines.push('- Before spawning child agents for pest control, map modules and module protocols first; do not send overlapping or vague tasks.');
160
142
  lines.push('- Keep child agents scoped to their assigned module and require evidence, reproduction notes, or tests for each bug claim.');
@@ -178,9 +160,6 @@ function renderCanonicalAgentsFile() {
178
160
  '',
179
161
  'Useful commands:',
180
162
  '- `farming skills`',
181
- '- `farming memory report --period today`',
182
- '- `farming memory report --period yesterday`',
183
- '- `farming memory report --period week`',
184
163
  '- `farming spawn --workspace <repo> --task "<module bug hunt task>" -- <coding-agent-command>`',
185
164
  '- `farming list --parent "$FARMING_AGENT_ID"`',
186
165
  '- `farming output <agent-id> --tail 2000`',