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
@@ -0,0 +1,20 @@
1
+ function runtimeExecutableInvocation(
2
+ executablePath,
3
+ args = [],
4
+ env = process.env,
5
+ platform = process.platform,
6
+ ) {
7
+ if (
8
+ platform === 'linux'
9
+ && env.FARMING_NODE_LD
10
+ && env.FARMING_NODE_LIBRARY_PATH
11
+ ) {
12
+ return {
13
+ command: env.FARMING_NODE_LD,
14
+ args: ['--library-path', env.FARMING_NODE_LIBRARY_PATH, executablePath, ...args],
15
+ };
16
+ }
17
+ return { command: executablePath, args };
18
+ }
19
+
20
+ module.exports = { runtimeExecutableInvocation };
@@ -0,0 +1,47 @@
1
+ const { execFileSync } = require('child_process');
2
+
3
+ const SERVER_PROCESS_IDENTITY_FORMAT = 'ps-lstart-c-utc-v1';
4
+
5
+ function readServerProcessIdentity(pid) {
6
+ const processId = Number(pid);
7
+ if (!Number.isSafeInteger(processId) || processId <= 0 || process.platform === 'win32') return null;
8
+ let stdout;
9
+ try {
10
+ stdout = execFileSync(
11
+ '/bin/ps',
12
+ ['-p', String(processId), '-o', 'pid=', '-o', 'pgid=', '-o', 'lstart='],
13
+ {
14
+ encoding: 'utf8',
15
+ timeout: 1_000,
16
+ maxBuffer: 16_384,
17
+ env: { ...process.env, LANG: 'C', LC_ALL: 'C', TZ: 'UTC' },
18
+ },
19
+ );
20
+ } catch (error) {
21
+ if (error?.status === 1 || error?.code === 'ESRCH') return null;
22
+ throw error;
23
+ }
24
+ const match = String(stdout || '').trim().match(/^(\d+)\s+(\d+)\s+(.+)$/);
25
+ if (!match || Number(match[1]) !== processId) return null;
26
+ return {
27
+ pid: processId,
28
+ processGroupId: Number(match[2]),
29
+ startedAt: match[3].trim(),
30
+ format: SERVER_PROCESS_IDENTITY_FORMAT,
31
+ };
32
+ }
33
+
34
+ function matchingProcessIdentity(expected, current) {
35
+ return Boolean(
36
+ current
37
+ && current.pid === Number(expected?.pid)
38
+ && current.processGroupId === Number(expected?.processGroupId)
39
+ && current.startedAt === String(expected?.startedAt || '')
40
+ );
41
+ }
42
+
43
+ module.exports = {
44
+ SERVER_PROCESS_IDENTITY_FORMAT,
45
+ matchingProcessIdentity,
46
+ readServerProcessIdentity,
47
+ };
package/backend/server.js CHANGED
@@ -51,9 +51,8 @@ const { CodexContextWindowReader } = require('./codex-context-window');
51
51
  const { DEFAULT_MAX_TURNS: DEFAULT_CODEX_TRANSCRIPT_MAX_TURNS, readCodexTranscript } = require('./codex-transcript');
52
52
  const { AsyncCache } = require('./async-cache');
53
53
  const { getMainAgentSkillsCatalog } = require('./main-agent-skills');
54
- const { discoverSlashCommands } = require('./slash-command-discovery');
54
+ const { discoverAgentExtensions, discoverSlashCommands } = require('./slash-command-discovery');
55
55
  const { FarmingUpdateService } = require('./update-service');
56
- const { isRestartBlockingAgent } = require('./agent-activity');
57
56
  const { inputPartsFromMessage } = require('./input-parts');
58
57
  const { cleanupTerminalRuntime } = require('./terminal-runtime-cleanup');
59
58
  const { QrShareTicketStore, SHARE_TICKET_TTL_MS } = require('./qr-share-tickets');
@@ -65,11 +64,13 @@ const { ReviewSessionStore } = require('./review-session-store');
65
64
  const { ReviewSessionService } = require('./review-session-service');
66
65
  const { createReviewSessionRouter } = require('./review-session-router');
67
66
  const {
67
+ applyIndexHtmlAppearance,
68
68
  normalizeBasePath,
69
69
  routePath,
70
70
  rewriteIndexHtmlForBasePath,
71
71
  appendIndexHtmlAssetToken,
72
72
  } = require('./index-html');
73
+ const { decodeAcpTranscriptMedia } = require('./acp-transcript');
73
74
  const {
74
75
  coalesceSessionStream,
75
76
  deliverSessionStreamToClients,
@@ -136,8 +137,12 @@ const workspaceRootRegistry = new WorkspaceRootRegistry(agentManager);
136
137
  const browserResourceManager = new BrowserResourceManager({
137
138
  configDir: configManager.farmingDir,
138
139
  isEnabled: () => configManager.getSettings().browserExtensionEnabled === true,
140
+ getBrowserSettings: () => configManager.getSettings(),
141
+ });
142
+ const browserRuntimeRecoveryPromise = browserResourceManager.init().catch(error => {
143
+ console.warn('Failed to recover Browser runtimes:', error?.message || error);
144
+ return null;
139
145
  });
140
- browserResourceManager.init();
141
146
  const updateService = new FarmingUpdateService({
142
147
  rootDir: path.join(__dirname, '..'),
143
148
  configDir: configManager.farmingDir,
@@ -255,6 +260,22 @@ function getAvailableAgentsForRequest() {
255
260
  supported: true,
256
261
  interactive: true,
257
262
  },
263
+ {
264
+ name: 'opencode',
265
+ command: 'opencode',
266
+ description: 'OpenCode - AI coding assistant',
267
+ category: 'coding',
268
+ supported: true,
269
+ interactive: true,
270
+ },
271
+ {
272
+ name: 'qoder',
273
+ command: 'qodercli',
274
+ description: 'Qoder - AI coding assistant',
275
+ category: 'coding',
276
+ supported: true,
277
+ interactive: true,
278
+ },
258
279
  {
259
280
  name: 'bash',
260
281
  command: 'bash',
@@ -574,7 +595,10 @@ app.get([
574
595
  }
575
596
  res.setHeader('Content-Type', 'text/html; charset=utf-8');
576
597
  res.setHeader('Cache-Control', 'no-cache');
577
- const rewrittenHtml = rewriteIndexHtmlForBasePath(html, BASE_PATH);
598
+ const rewrittenHtml = applyIndexHtmlAppearance(
599
+ rewriteIndexHtmlForBasePath(html, BASE_PATH),
600
+ configManager.getSettings().appearance
601
+ );
578
602
  const requestUrl = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
579
603
  const requestToken = requestUrl.searchParams.has('token') ? tokenAuth.extractToken(req) : '';
580
604
  const htmlWithAssetToken = authEnabled && requestToken && tokenAuth.verify(requestToken)
@@ -613,6 +637,40 @@ app.get(routePath(BASE_PATH, '/api/skills'), (_req, res) => {
613
637
  res.json({ skills: getMainAgentSkillsCatalog() });
614
638
  });
615
639
 
640
+ app.get(routePath(BASE_PATH, '/api/agent-extensions'), (_req, res) => {
641
+ const agents = getAvailableAgentsForRequest()
642
+ .filter(agent => agent.category === 'coding')
643
+ .map(agent => {
644
+ const provider = String(agent.name || agent.command || '').trim().toLowerCase();
645
+ const configuredHomes = configManager.getAgentHomes(provider);
646
+ const homes = configuredHomes.length > 0
647
+ ? configuredHomes
648
+ : [{ id: 'default', path: '' }];
649
+ return {
650
+ id: provider,
651
+ name: agent.name,
652
+ description: agent.description || '',
653
+ discoverySupported: provider === 'codex' || provider === 'claude',
654
+ homes: homes.map(home => ({
655
+ id: home.id,
656
+ extensions: discoverAgentExtensions({
657
+ provider,
658
+ providerHomePath: home.path,
659
+ }).map(extension => ({
660
+ id: extension.command,
661
+ command: extension.command,
662
+ name: extension.label,
663
+ description: extension.description,
664
+ kind: extension.source === 'custom' ? 'command' : extension.source || 'command',
665
+ scope: extension.scope || '',
666
+ })),
667
+ })),
668
+ };
669
+ });
670
+ res.setHeader('Cache-Control', 'no-store');
671
+ res.json({ agents });
672
+ });
673
+
616
674
  app.get(routePath(BASE_PATH, '/api/slash-commands'), (req, res) => {
617
675
  const provider = typeof req.query.provider === 'string' ? req.query.provider : '';
618
676
  const workspace = typeof req.query.workspace === 'string' ? req.query.workspace : '';
@@ -815,26 +873,10 @@ app.post(routePath(BASE_PATH, '/api/codex/context-windows'), express.json(), asy
815
873
  }
816
874
  });
817
875
 
818
- function blockingUpdateAgents() {
819
- return agentManager.getState().agents
820
- .filter(isRestartBlockingAgent)
821
- .map(agent => ({
822
- id: agent.id,
823
- command: agent.command,
824
- task: agent.task || agent.customTitle || agent.sessionTitle || '',
825
- cwd: agent.cwd,
826
- }));
827
- }
828
-
829
876
  app.get(routePath(BASE_PATH, '/api/update'), async (req, res) => {
830
877
  try {
831
878
  const update = await updateService.check({ force: req.query.force === '1' });
832
- res.json({
833
- update: {
834
- ...update,
835
- blockingAgents: blockingUpdateAgents(),
836
- },
837
- });
879
+ res.json({ update });
838
880
  } catch (error) {
839
881
  res.status(502).json({ error: error.message || 'Failed to check for updates' });
840
882
  }
@@ -845,31 +887,16 @@ app.post(routePath(BASE_PATH, '/api/update/install'), express.json(), async (req
845
887
  const state = await updateService.startInstall({
846
888
  assetName: req.body && typeof req.body.assetName === 'string' ? req.body.assetName : '',
847
889
  });
848
- res.status(202).json({
849
- update: {
850
- state,
851
- blockingAgents: blockingUpdateAgents(),
852
- },
853
- });
890
+ res.status(202).json({ update: { state } });
854
891
  } catch (error) {
855
892
  res.status(500).json({ error: error.message || 'Failed to start update' });
856
893
  }
857
894
  });
858
895
 
859
896
  app.post(routePath(BASE_PATH, '/api/update/restart'), express.json(), async (req, res) => {
860
- const blockers = blockingUpdateAgents();
861
- const force = req.body && req.body.force === true;
862
- if (blockers.length > 0 && !force) {
863
- res.status(409).json({
864
- error: 'Cannot restart for update while non-recoverable project agents are running',
865
- blockingAgents: blockers,
866
- });
867
- return;
868
- }
869
-
870
897
  try {
871
898
  const state = await updateService.applyPreparedUpdate();
872
- res.status(202).json({ update: { state, blockingAgents: blockers } });
899
+ res.status(202).json({ update: { state } });
873
900
  } catch (error) {
874
901
  res.status(500).json({ error: error.message || 'Failed to restart for update' });
875
902
  }
@@ -1152,8 +1179,17 @@ app.get(routePath(BASE_PATH, '/api/agents/:agentId/acp-transcript'), async (req,
1152
1179
  ? Math.min(MAX_CODEX_TRANSCRIPT_TURNS, Math.max(20, requestedMaxTurns))
1153
1180
  : DEFAULT_CODEX_TRANSCRIPT_MAX_TURNS;
1154
1181
  const requestedRevision = Number.parseInt(String(req.query.sinceRevision || ''), 10);
1182
+ const externalMedia = req.query.media === 'external-v1';
1155
1183
  res.json({ transcript: agentManager.getAcpTranscript(req.params.agentId, {
1156
1184
  maxTurns,
1185
+ ...(externalMedia
1186
+ ? {
1187
+ mediaPathPrefix: routePath(
1188
+ BASE_PATH,
1189
+ `/api/agents/${encodeURIComponent(req.params.agentId)}/acp-media`
1190
+ ),
1191
+ }
1192
+ : {}),
1157
1193
  ...(Number.isFinite(requestedRevision) && requestedRevision >= 0
1158
1194
  ? { sinceRevision: requestedRevision }
1159
1195
  : {}),
@@ -1164,6 +1200,32 @@ app.get(routePath(BASE_PATH, '/api/agents/:agentId/acp-transcript'), async (req,
1164
1200
  }
1165
1201
  });
1166
1202
 
1203
+ app.get(routePath(BASE_PATH, '/api/agents/:agentId/acp-media/:entryId/:mediaId'), (req, res) => {
1204
+ try {
1205
+ const media = agentManager.getAcpTranscriptMedia(
1206
+ req.params.agentId,
1207
+ req.params.entryId,
1208
+ req.params.mediaId
1209
+ );
1210
+ const decoded = decodeAcpTranscriptMedia(media);
1211
+ if (!decoded) {
1212
+ res.status(415).json({ error: 'unsupported ACP transcript media' });
1213
+ return;
1214
+ }
1215
+ res.setHeader('Cache-Control', 'private, max-age=31536000, immutable');
1216
+ res.setHeader('X-Content-Type-Options', 'nosniff');
1217
+ res.type(decoded.mimeType).send(decoded.content);
1218
+ } catch (error) {
1219
+ const message = error && error.message ? error.message : 'Failed to read ACP transcript media';
1220
+ const status = message === 'Agent not found'
1221
+ || message === 'ACP transcript entry not found'
1222
+ || message === 'ACP transcript media not found'
1223
+ ? 404
1224
+ : 409;
1225
+ res.status(status).json({ error: message });
1226
+ }
1227
+ });
1228
+
1167
1229
  app.get(routePath(BASE_PATH, '/api/agents/:agentId/acp-tool-details/:toolCallId'), async (req, res) => {
1168
1230
  try {
1169
1231
  res.json(agentManager.getAcpToolDetail(req.params.agentId, req.params.toolCallId));
@@ -2209,19 +2271,44 @@ app.post(routePath(BASE_PATH, '/api/settings'), express.json(), async (req, res)
2209
2271
  delete settingsPatch.mainPageSessionKeys;
2210
2272
  delete settingsPatch.projectWorkspaces;
2211
2273
  delete settingsPatch.pinnedProjectWorkspaces;
2274
+ const browserConfigurationKeys = [
2275
+ 'browserSource',
2276
+ 'browserExecutablePath',
2277
+ 'browserExternalCdpUrl',
2278
+ ];
2212
2279
  const changesBrowserExtension = Object.prototype.hasOwnProperty.call(settingsPatch, 'browserExtensionEnabled');
2280
+ const changesBrowserConfiguration = browserConfigurationKeys.some(key =>
2281
+ Object.prototype.hasOwnProperty.call(settingsPatch, key)
2282
+ );
2283
+ const currentSettings = configManager.getSettings();
2213
2284
  const browserExtensionEnabled = settingsPatch.browserExtensionEnabled === true;
2214
- if (changesBrowserExtension && browserExtensionEnabled && !browserResourceManager.capability().browser) {
2215
- res.status(400).json({
2216
- error: 'Install a Chromium-based browser to use the system Browser in Farming',
2217
- code: 'BROWSER_EXECUTABLE_NOT_FOUND',
2285
+ const desiredBrowserEnabled = changesBrowserExtension
2286
+ ? browserExtensionEnabled
2287
+ : currentSettings.browserExtensionEnabled === true;
2288
+ if ((changesBrowserExtension && browserExtensionEnabled) || changesBrowserConfiguration) {
2289
+ const desiredSelection = browserResourceManager.browserSelection({
2290
+ ...currentSettings,
2291
+ ...settingsPatch,
2218
2292
  });
2219
- return;
2293
+ const probe = await browserResourceManager.probeCapability(desiredSelection);
2294
+ if (
2295
+ (changesBrowserConfiguration || desiredBrowserEnabled)
2296
+ && (!probe.runtimeCapability || probe.runtimeCapability.error)
2297
+ ) {
2298
+ res.status(400).json({
2299
+ error: probe.runtimeCapability?.error
2300
+ || 'Install a Chromium-based browser or configure a loopback external CDP endpoint',
2301
+ code: 'BROWSER_EXECUTABLE_NOT_FOUND',
2302
+ });
2303
+ return;
2304
+ }
2220
2305
  }
2221
2306
  if (
2222
- changesBrowserExtension
2223
- && !browserExtensionEnabled
2224
- && configManager.getSettings().browserExtensionEnabled === true
2307
+ currentSettings.browserExtensionEnabled === true
2308
+ && (
2309
+ (changesBrowserExtension && !browserExtensionEnabled)
2310
+ || changesBrowserConfiguration
2311
+ )
2225
2312
  ) {
2226
2313
  try {
2227
2314
  await browserResourceManager.stopAll();
@@ -2234,6 +2321,9 @@ app.post(routePath(BASE_PATH, '/api/settings'), express.json(), async (req, res)
2234
2321
  }
2235
2322
  }
2236
2323
  configManager.updateSettings(settingsPatch);
2324
+ if (changesBrowserExtension || changesBrowserConfiguration) {
2325
+ await browserResourceManager.refreshCapability();
2326
+ }
2237
2327
  agentSessionsCache.invalidate();
2238
2328
  res.json({
2239
2329
  success: true,
@@ -2857,7 +2947,6 @@ const pendingAgentUpdates = new Map();
2857
2947
  const pendingSessionStreams = new Map();
2858
2948
  let sessionStreamBroadcastTimer = null;
2859
2949
  let lastSessionStreamBroadcastAt = 0;
2860
- let shutdownStarted = false;
2861
2950
 
2862
2951
  function broadcastState() {
2863
2952
  lastStateBroadcastAt = Date.now();
@@ -3072,106 +3161,6 @@ function scheduleSessionStreamBroadcast(stream) {
3072
3161
  }
3073
3162
  }
3074
3163
 
3075
- function clearBroadcastTimers() {
3076
- if (stateBroadcastTimer) {
3077
- clearTimeout(stateBroadcastTimer);
3078
- stateBroadcastTimer = null;
3079
- }
3080
- for (const entry of pendingPreviewBroadcasts.values()) {
3081
- if (entry && entry.timer) {
3082
- clearTimeout(entry.timer);
3083
- }
3084
- }
3085
- pendingPreviewBroadcasts.clear();
3086
- for (const entry of pendingAgentActivityBroadcasts.values()) {
3087
- if (entry && entry.timer) clearTimeout(entry.timer);
3088
- }
3089
- pendingAgentActivityBroadcasts.clear();
3090
- for (const entry of pendingAgentUpdates.values()) {
3091
- if (entry && entry.timer) clearTimeout(entry.timer);
3092
- }
3093
- pendingAgentUpdates.clear();
3094
- if (sessionStreamBroadcastTimer) {
3095
- clearTimeout(sessionStreamBroadcastTimer);
3096
- sessionStreamBroadcastTimer = null;
3097
- }
3098
- pendingSessionStreams.clear();
3099
- }
3100
-
3101
- function closeHttpServer() {
3102
- return new Promise(resolve => {
3103
- if (!server.listening) {
3104
- resolve();
3105
- return;
3106
- }
3107
- server.close(error => {
3108
- if (error && error.code !== 'ERR_SERVER_NOT_RUNNING') {
3109
- console.warn('Failed to close HTTP server:', error.message || error);
3110
- }
3111
- resolve();
3112
- });
3113
- });
3114
- }
3115
-
3116
- function closeWebSocketServer() {
3117
- for (const client of wss.clients) {
3118
- clearWorkspaceFileWatch(client);
3119
- try {
3120
- client.close(1001, 'Farming server shutting down');
3121
- } catch {
3122
- // ignore shutdown races
3123
- }
3124
- }
3125
- return new Promise(resolve => {
3126
- wss.close(() => resolve());
3127
- setTimeout(resolve, 250).unref?.();
3128
- });
3129
- }
3130
-
3131
- async function shutdownServer(options = {}) {
3132
- if (shutdownStarted) return;
3133
- shutdownStarted = true;
3134
- const preserveTerminalHost = options.preserveTerminalHost !== undefined
3135
- ? options.preserveTerminalHost === true
3136
- : process.env.FARMING_NATIVE_PTY_HOST_PERSIST !== '0';
3137
-
3138
- try {
3139
- await browserResourceManager.dispose();
3140
- await agentManager.dispose({ preserveTerminalHost });
3141
- } catch (error) {
3142
- const teardownFrozen = agentManager.disposing === true;
3143
- shutdownStarted = false;
3144
- console.error('Farming shutdown blocked because runtime cleanup could not be verified:', error);
3145
- if (teardownFrozen) {
3146
- clearBroadcastTimers();
3147
- tokenAuth.cleanup();
3148
- await Promise.allSettled([
3149
- closeWebSocketServer(),
3150
- workspaceFileService.dispose(),
3151
- closeHttpServer(),
3152
- ]);
3153
- }
3154
- if (options.exit === true) process.exitCode = 1;
3155
- return {
3156
- error: error?.message || 'Runtime cleanup could not be verified',
3157
- ...(teardownFrozen ? { frozen: true } : {}),
3158
- };
3159
- }
3160
-
3161
- clearBroadcastTimers();
3162
- tokenAuth.cleanup();
3163
- await Promise.allSettled([
3164
- closeWebSocketServer(),
3165
- workspaceFileService.dispose(),
3166
- closeHttpServer(),
3167
- ]);
3168
-
3169
- if (options.exit === true) {
3170
- process.exit(options.exitCode || 0);
3171
- }
3172
- return { stopped: true };
3173
- }
3174
-
3175
3164
  agentManager.onSessionStream((stream) => {
3176
3165
  scheduleSessionStreamBroadcast(stream);
3177
3166
  });
@@ -3221,8 +3210,10 @@ function startServer() {
3221
3210
  if (serverStarted) return server;
3222
3211
  serverStarted = true;
3223
3212
 
3224
- void runTerminalRuntimeStartupCleanup().finally(() => {
3225
- if (shutdownStarted) return;
3213
+ void Promise.all([
3214
+ runTerminalRuntimeStartupCleanup(),
3215
+ browserRuntimeRecoveryPromise,
3216
+ ]).finally(() => {
3226
3217
  server.listen(PORT, () => {
3227
3218
  const token = tokenAuth.getToken();
3228
3219
  const localIPs = getLocalIPs();
@@ -3252,13 +3243,6 @@ function startServer() {
3252
3243
  });
3253
3244
  });
3254
3245
 
3255
- process.on('SIGINT', () => {
3256
- void shutdownServer({ exit: true });
3257
- });
3258
- process.on('SIGTERM', () => {
3259
- void shutdownServer({ exit: true });
3260
- });
3261
-
3262
3246
  return server;
3263
3247
  }
3264
3248
 
@@ -3279,6 +3263,5 @@ module.exports = {
3279
3263
  rewriteIndexHtmlForBasePath,
3280
3264
  appendIndexHtmlAssetToken,
3281
3265
  startServer,
3282
- shutdownServer,
3283
3266
  runTerminalRuntimeStartupCleanup,
3284
3267
  };
@@ -159,6 +159,7 @@ function discoverClaudeSkillCommands(commands, skillsDir, sourceLabel) {
159
159
  label: commandLabel(entry.name),
160
160
  description: `Claude skill from ${sourceLabel}`,
161
161
  source: 'skill',
162
+ scope: sourceLabel === 'home' ? 'Personal' : commandLabel(sourceLabel),
162
163
  });
163
164
  });
164
165
  }
@@ -180,10 +181,100 @@ function discoverClaudeCustomCommands(commands, commandsDir, sourceLabel) {
180
181
  label: commandLabel(name),
181
182
  description: `Claude custom command from ${sourceLabel}`,
182
183
  source: 'custom',
184
+ scope: sourceLabel === 'home' ? 'Personal' : commandLabel(sourceLabel),
183
185
  });
184
186
  });
185
187
  }
186
188
 
189
+ function discoverClaudePluginComponents(commands, pluginPath, pluginName) {
190
+ discoverSkillFiles(path.join(pluginPath, 'skills')).forEach(skillFile => {
191
+ const skillName = path.basename(path.dirname(skillFile));
192
+ const metadata = parseSkillFrontMatter(skillFile);
193
+ addCommand(commands, {
194
+ command: `/${pluginName}:${skillName}`,
195
+ label: commandLabel(metadata.name || skillName),
196
+ description: metadata.description || `Claude skill from ${pluginName}`,
197
+ source: 'skill',
198
+ scope: 'Plugin',
199
+ });
200
+ });
201
+
202
+ let commandEntries = [];
203
+ try {
204
+ commandEntries = fs.readdirSync(path.join(pluginPath, 'commands'), { withFileTypes: true });
205
+ } catch {
206
+ // Plugins do not have to provide commands.
207
+ }
208
+ commandEntries.forEach(entry => {
209
+ if (!entry.isFile() || !entry.name.endsWith('.md')) return;
210
+ const commandName = entry.name.slice(0, -3);
211
+ if (!SAFE_COMMAND_NAME.test(commandName)) return;
212
+ addCommand(commands, {
213
+ command: `/${pluginName}:${commandName}`,
214
+ label: commandLabel(commandName),
215
+ description: `Claude command from ${pluginName}`,
216
+ source: 'custom',
217
+ scope: 'Plugin',
218
+ });
219
+ });
220
+ }
221
+
222
+ function readBoundedJson(filePath, maxBytes = 1024 * 1024) {
223
+ try {
224
+ const stats = fs.statSync(filePath);
225
+ if (!stats.isFile() || stats.size > maxBytes) return null;
226
+ const value = JSON.parse(fs.readFileSync(filePath, 'utf8'));
227
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
228
+ } catch {
229
+ return null;
230
+ }
231
+ }
232
+
233
+ function addClaudePlugin(commands, pluginName, pluginPath, scope = 'Plugin') {
234
+ if (!SAFE_COMMAND_NAME.test(pluginName)) return;
235
+ const manifest = readBoundedJson(path.join(pluginPath, '.claude-plugin', 'plugin.json'));
236
+ addCommand(commands, {
237
+ command: `plugin:${pluginName}`,
238
+ label: String(manifest?.name || commandLabel(pluginName)).slice(0, 100),
239
+ description: String(manifest?.description || 'Claude Code plugin').slice(0, 500),
240
+ source: 'plugin',
241
+ scope,
242
+ });
243
+ discoverClaudePluginComponents(commands, pluginPath, pluginName);
244
+ }
245
+
246
+ function discoverClaudeInstalledPlugins(commands, homePath) {
247
+ const pluginsRoot = path.join(homePath, 'plugins');
248
+ const installed = readBoundedJson(path.join(pluginsRoot, 'installed_plugins.json'));
249
+ Object.entries(installed?.plugins || {}).slice(0, MAX_DISCOVERED_SKILLS).forEach(([key, rawEntries]) => {
250
+ const pluginName = key.split('@')[0];
251
+ const entries = Array.isArray(rawEntries) ? rawEntries : [];
252
+ const installedEntry = entries.find(entry => (
253
+ entry && typeof entry === 'object' && typeof entry.installPath === 'string'
254
+ ));
255
+ if (!installedEntry) return;
256
+ addClaudePlugin(
257
+ commands,
258
+ pluginName,
259
+ installedEntry.installPath,
260
+ String(installedEntry.scope || 'Plugin'),
261
+ );
262
+ });
263
+
264
+ let entries = [];
265
+ try {
266
+ entries = fs.readdirSync(pluginsRoot, { withFileTypes: true });
267
+ } catch {
268
+ return;
269
+ }
270
+ entries.forEach(entry => {
271
+ if (!entry.isDirectory() || !SAFE_COMMAND_NAME.test(entry.name)) return;
272
+ const pluginPath = path.join(pluginsRoot, entry.name);
273
+ if (!fs.existsSync(path.join(pluginPath, '.claude-plugin', 'plugin.json'))) return;
274
+ addClaudePlugin(commands, entry.name, pluginPath);
275
+ });
276
+ }
277
+
187
278
  function discoverClaudeSlashCommands({ homeDir = os.homedir(), workspace } = {}) {
188
279
  const commands = [];
189
280
  const normalizedWorkspace = normalizeWorkspace(workspace, homeDir);
@@ -249,15 +340,26 @@ function discoverPluginSkillFiles(root, depth = 0, skillFiles = []) {
249
340
 
250
341
  function pluginNameForSkillFile(skillFile, pluginsCacheDir) {
251
342
  const parts = path.relative(pluginsCacheDir, skillFile).split(path.sep);
252
- if (parts[0] === 'openai-curated' && parts[1]) return parts[1];
343
+ if ((parts[0] === 'openai-curated' || parts[0] === 'openai-curated-remote') && parts[1]) return parts[1];
253
344
  if ((parts[0] === 'openai-primary-runtime' || parts[0] === 'openai-bundled') && parts[1]) return parts[1];
254
345
  return parts[0] || '';
255
346
  }
256
347
 
257
348
  function discoverCodexPluginSkills(commands, homeDir) {
258
349
  const pluginsCacheDir = path.join(homeDir, '.codex', 'plugins', 'cache');
350
+ discoverCodexPluginSkillsAt(commands, pluginsCacheDir);
351
+ }
352
+
353
+ function discoverCodexPluginSkillsAt(commands, pluginsCacheDir) {
259
354
  discoverPluginSkillFiles(pluginsCacheDir).forEach(skillFile => {
260
355
  const pluginName = pluginNameForSkillFile(skillFile, pluginsCacheDir);
356
+ addCommand(commands, {
357
+ command: `plugin:${pluginName}`,
358
+ label: commandLabel(pluginName),
359
+ description: 'Codex plugin',
360
+ source: 'plugin',
361
+ scope: 'Plugin',
362
+ });
261
363
  addSkillMention(commands, skillFile, {
262
364
  mentionPrefix: pluginName,
263
365
  fallbackName: path.basename(path.dirname(skillFile)),
@@ -278,6 +380,40 @@ function discoverCodexSkillMentions({ homeDir = os.homedir(), workspace } = {})
278
380
  return commands.slice(0, MAX_DISCOVERED_SKILLS);
279
381
  }
280
382
 
383
+ function discoverAgentExtensions({
384
+ provider,
385
+ providerHomePath,
386
+ workspace,
387
+ homeDir = os.homedir(),
388
+ } = {}) {
389
+ const normalizedProvider = normalizeProvider(provider);
390
+ const homePath = normalizeWorkspace(providerHomePath);
391
+ const normalizedHomeDir = normalizeWorkspace(homeDir) || os.homedir();
392
+ if (!normalizedProvider || !homePath) return [];
393
+
394
+ const commands = [];
395
+ if (normalizedProvider === 'claude') {
396
+ const normalizedWorkspace = normalizeWorkspace(workspace);
397
+ if (normalizedWorkspace) {
398
+ discoverClaudeSkillCommands(commands, path.join(normalizedWorkspace, '.claude', 'skills'), 'workspace');
399
+ discoverClaudeCustomCommands(commands, path.join(normalizedWorkspace, '.claude', 'commands'), 'workspace');
400
+ }
401
+ discoverClaudeSkillCommands(commands, path.join(homePath, 'skills'), 'home');
402
+ discoverClaudeCustomCommands(commands, path.join(homePath, 'commands'), 'home');
403
+ discoverClaudeInstalledPlugins(commands, homePath);
404
+ return commands.slice(0, MAX_DISCOVERED_SKILLS);
405
+ }
406
+
407
+ workspaceSkillRoots(workspace, normalizedHomeDir)
408
+ .forEach(root => discoverDirectCodexSkills(commands, root, 'Repo'));
409
+ discoverDirectCodexSkills(commands, path.join(normalizedHomeDir, '.agents', 'skills'), 'Personal');
410
+ discoverDirectCodexSkills(commands, path.join(homePath, 'skills'), 'Personal');
411
+ discoverDirectCodexSkills(commands, path.join(homePath, 'skills', '.system'), 'System');
412
+ discoverDirectCodexSkills(commands, path.join('/etc', 'codex', 'skills'), 'Admin');
413
+ discoverCodexPluginSkillsAt(commands, path.join(homePath, 'plugins', 'cache'));
414
+ return commands.slice(0, MAX_DISCOVERED_SKILLS);
415
+ }
416
+
281
417
  function discoverSlashCommands({ provider, homeDir = os.homedir(), workspace } = {}) {
282
418
  const normalizedProvider = normalizeProvider(provider);
283
419
  if (normalizedProvider === 'codex') {
@@ -290,6 +426,7 @@ function discoverSlashCommands({ provider, homeDir = os.homedir(), workspace } =
290
426
  }
291
427
 
292
428
  module.exports = {
429
+ discoverAgentExtensions,
293
430
  discoverSlashCommands,
294
431
  discoverClaudeSlashCommands,
295
432
  discoverCodexSkillMentions,