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.
- package/README.md +9 -31
- package/README.zh_cn.md +8 -30
- package/THIRD_PARTY_NOTICES.md +25 -6
- package/backend/acp/packaged-claude-acp.js +13 -0
- package/backend/acp/packaged-codex-acp.js +2 -2
- package/backend/acp-runtime.js +37 -2
- package/backend/acp-transcript.js +174 -9
- package/backend/agent-manager.js +27 -2
- package/backend/config-manager.js +44 -0
- package/backend/data/LICENSE.agent-browser +201 -0
- package/backend/data/runtime-dependency-manifest.json +199 -0
- package/backend/data/runtime-dependency-sources.json +5 -0
- package/backend/executable-discovery.js +7 -0
- package/backend/farming-agent-bootstrap.zh_cn.md +4 -2
- package/backend/farming-app-cli.js +160 -64
- package/backend/farming-cli.js +5 -3
- package/backend/index-html.js +13 -0
- package/backend/main-agent-skills.js +6 -10
- package/backend/npm-update-helper.js +43 -6
- package/backend/provider-adapters.js +10 -0
- package/backend/runtime-dependency-manager.js +759 -0
- package/backend/runtime-executable-invocation.js +20 -0
- package/backend/server-process-identity.js +47 -0
- package/backend/server.js +140 -157
- package/backend/slash-command-discovery.js +138 -1
- package/backend/storage-layout.js +15 -0
- package/backend/update-service.js +27 -1
- package/config/farming.install.env.example +4 -0
- package/dist/acp/LICENSE.claude-agent-acp +191 -0
- package/dist/acp/LICENSE.claude-agent-sdk +1 -0
- package/dist/acp/claude-agent-acp-0.59.0.mjs +41606 -0
- package/dist/assets/{App-CM7Cy-l4.css → App-BKNUILuL.css} +1 -1
- package/dist/assets/App-Ex2oVVXO.js +693 -0
- package/dist/assets/{FileEditorMarkdownPreview-DYOMm-b1.js → FileEditorMarkdownPreview-Cs9cLv8a.js} +1 -1
- package/dist/assets/FileEditorPane-CNtuGjQ3.js +2 -0
- package/dist/assets/IconGlyphs-BClgag6E.js +1 -0
- package/dist/assets/ProjectFilesSection-Cf2BEcAN.js +12 -0
- package/dist/assets/ReviewPage-BRuxFaH5.js +3 -0
- package/dist/assets/code-dark-BEZTQwYZ.css +1 -0
- package/dist/assets/{index-BzQO3pUr.js → index-CQNaZc3l.js} +2 -2
- package/dist/assets/main-DK9Sy7-c.css +1 -0
- package/dist/assets/{workspace-editor-monaco-tXA-udGx.js → workspace-editor-monaco-CJNwbpCy.js} +1 -1
- package/dist/assets/workspace-editor-monaco-CO9w8YV0.js +1 -0
- package/dist/assets/workspace-view-state-BjKPYp0E.js +7 -0
- package/dist/index.html +32 -3
- package/extensions/browser/backend/agent-browser-runtime.js +1041 -0
- package/extensions/browser/backend/browser-mcp-server.js +352 -0
- package/extensions/browser/backend/browser-resource-manager.js +899 -114
- package/extensions/browser/backend/browser-resource-store.js +47 -6
- package/extensions/browser/backend/browser-router.js +7 -2
- package/extensions/browser/backend/executable-discovery.js +146 -13
- package/extensions/browser/bin/farming-browser +384 -30
- package/extensions/browser/frontend/BrowserSidebarPortals.tsx +17 -19
- package/extensions/browser/frontend/BrowserViewer.tsx +217 -46
- package/extensions/browser/frontend/browser.css +68 -17
- package/extensions/browser/frontend/types.ts +6 -0
- package/extensions/browser/frontend/useBrowserResources.ts +10 -3
- package/index.html +31 -2
- package/package.json +10 -6
- package/shared/backend-connection-status.d.ts +12 -0
- package/shared/backend-connection-status.js +35 -4
- package/backend/agent-activity.js +0 -35
- package/dist/assets/App-CPG4WcjO.js +0 -658
- package/dist/assets/FileEditorPane-DXXq82aM.js +0 -2
- package/dist/assets/IconGlyphs-DrdPjYXr.js +0 -1
- package/dist/assets/ProjectFilesSection-CP66XVPl.js +0 -12
- package/dist/assets/ReviewPage-w21MEPD6.js +0 -3
- package/dist/assets/code-dark-BDjwj7Um.css +0 -1
- package/dist/assets/main-BlVUXfxa.css +0 -1
- package/dist/assets/workspace-editor-monaco-CFEfFNcA.js +0 -1
- package/dist/assets/workspace-view-state-BS_V8JVs.js +0 -7
- package/extensions/browser/backend/cdp-browser-runtime.js +0 -648
- package/extensions/browser/backend/cdp-client.js +0 -167
- /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 =
|
|
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
|
|
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
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2223
|
-
&&
|
|
2224
|
-
|
|
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
|
|
3225
|
-
|
|
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,
|