farming-code 2.2.8 → 2.2.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/README.md +36 -23
  2. package/README.zh_cn.md +36 -23
  3. package/THIRD_PARTY_NOTICES.md +11 -2
  4. package/backend/acp-checkpoint-store.js +198 -0
  5. package/backend/acp-runtime.js +306 -83
  6. package/backend/acp-session-state.js +202 -6
  7. package/backend/acp-transcript.js +112 -0
  8. package/backend/agent-activity.js +6 -157
  9. package/backend/agent-manager.js +1482 -730
  10. package/backend/agent-provider-session.js +17 -242
  11. package/backend/agent-runtime-binding.js +219 -0
  12. package/backend/agent-session-history.js +66 -1
  13. package/backend/auth.js +79 -6
  14. package/backend/codex-models.js +81 -84
  15. package/backend/codex-session-archive.js +45 -0
  16. package/backend/codex-terminal-profile.js +500 -0
  17. package/backend/codex-transcript-sanitizer.js +12 -0
  18. package/backend/codex-transcript.js +230 -8
  19. package/backend/config-manager.js +30 -0
  20. package/backend/control-api.js +192 -17
  21. package/backend/farming-net-pass.js +285 -0
  22. package/backend/farming-net-registry.js +112 -0
  23. package/backend/farming-net-server.js +298 -0
  24. package/backend/farming-session-store.js +5 -13
  25. package/backend/git-worktree-info.js +181 -0
  26. package/backend/local-session-engine.js +411 -186
  27. package/backend/main-page-session.js +5 -2
  28. package/backend/native-pty-controller-generation.js +106 -0
  29. package/backend/native-pty-host-client.js +275 -7
  30. package/backend/native-pty-host-identity.js +86 -0
  31. package/backend/native-pty-host.js +813 -114
  32. package/backend/native-session-engine.js +100 -28
  33. package/backend/packaged-node-pty.js +22 -2
  34. package/backend/provider-adapters.js +253 -0
  35. package/backend/provider-session-service.js +241 -0
  36. package/backend/runtime-observation.js +81 -0
  37. package/backend/server.js +294 -86
  38. package/backend/session-engine-bridge.js +21 -2
  39. package/backend/session-engine-router.js +1 -1
  40. package/backend/session-engine.js +1 -1
  41. package/backend/session-stream-protocol.js +185 -0
  42. package/backend/storage-layout.js +55 -0
  43. package/backend/terminal-attach-checkpoint.js +74 -0
  44. package/backend/terminal-exit-quiescence.js +39 -0
  45. package/backend/terminal-reducer-flow-control.js +97 -0
  46. package/backend/terminal-screen-state.js +11 -2
  47. package/backend/terminal-screen-worker-pool.js +59 -6
  48. package/backend/terminal-screen-worker-thread.js +97 -57
  49. package/backend/terminal-screen-worker.js +133 -51
  50. package/backend/terminal-state-serialization.js +127 -0
  51. package/backend/terminal-status.js +23 -4
  52. package/backend/usage-monitor.js +81 -4
  53. package/backend/workspace-directory.js +152 -0
  54. package/backend/workspace-file-router.js +182 -76
  55. package/backend/workspace-file-service.js +319 -4
  56. package/backend/workspace-root-registry.js +164 -0
  57. package/dist/assets/App-CHYpgECZ.js +208 -0
  58. package/dist/assets/{FileEditorMarkdownPreview-elKWc8Im.js → FileEditorMarkdownPreview--Cri80nQ.js} +92 -92
  59. package/dist/assets/FileEditorPane-DGJlEKdf.js +2 -0
  60. package/dist/assets/IconGlyphs-Cc14sepw.js +1 -0
  61. package/dist/assets/ProjectFilesSection-Cz1wG4NT.js +12 -0
  62. package/dist/assets/{ReviewPage-BaXu1ZdX.js → ReviewPage-LonRc6Fz.js} +1 -1
  63. package/dist/assets/code-dark-B1pAtO_P.css +1 -0
  64. package/dist/assets/file-icons-Bw2qd5iT.js +1 -0
  65. package/dist/assets/{index-BrbljRqn.js → index-miO2r9IZ.js} +3 -3
  66. package/dist/assets/main-DNe0jPw-.css +1 -0
  67. package/dist/assets/workspace-editor-model-BQol4qbA.js +1 -0
  68. package/dist/assets/workspace-editor-monaco-5gFbeDPF.js +1 -0
  69. package/dist/assets/workspace-editor-monaco-xPv8JzHH.js +4 -0
  70. package/dist/assets/workspace-view-state-DvYG_9PH.js +7 -0
  71. package/dist/assets/workspace-working-copy-D8-s_Sgh.js +1 -0
  72. package/dist/index.html +1 -1
  73. package/frontend/farming-net/app.css +625 -0
  74. package/frontend/farming-net/app.js +268 -0
  75. package/frontend/farming-net/index.html +86 -0
  76. package/frontend/reading-anchor.js +198 -0
  77. package/frontend/session-bridge.js +12 -3
  78. package/frontend/session-modal-bridge.js +5 -12
  79. package/frontend/skins/crt/app.js +1976 -793
  80. package/frontend/skins/crt/index.html +313 -23
  81. package/frontend/skins/crt/styles/billing.css +294 -223
  82. package/frontend/skins/crt/styles/monochrome-green.css +7 -2
  83. package/frontend/terminal-replay.js +372 -0
  84. package/package.json +10 -3
  85. package/shared/browser-protocol.d.ts +5 -0
  86. package/shared/browser-protocol.js +130 -0
  87. package/dist/assets/App-8dYAM6ql.js +0 -124
  88. package/dist/assets/FileEditorPane-RWiFD2cq.js +0 -5
  89. package/dist/assets/IconGlyphs-DfL0EBnj.js +0 -1
  90. package/dist/assets/ProjectFilesSection-Q4PDsWmM.js +0 -12
  91. package/dist/assets/code-dark-CDkOQAtK.css +0 -1
  92. package/dist/assets/file-icons-EFUGSSwf.js +0 -1
  93. package/dist/assets/main-D073SnW4.css +0 -1
  94. package/dist/assets/qoder-C9LmmOSf.svg +0 -1
  95. package/dist/assets/qoder-Cf9gl0Y5.svg +0 -1
  96. package/dist/assets/qoder-gHCinseV.svg +0 -1
  97. package/dist/assets/workspace-view-state-CTyDzk2D.js +0 -1
  98. package/dist/assets/zsh-CLpveKlF.svg +0 -1
  99. package/dist/assets/zsh-FxSpMPbz.svg +0 -1
  100. /package/dist/assets/{api-D1lyBYIQ.js → api-D8nyOEbz.js} +0 -0
  101. /package/dist/assets/{core-ZlAPicox.js → core-D0LFJkDt.js} +0 -0
  102. /package/dist/assets/{useWorkspaceMenuKeyboard-CneKAZUJ.js → useWorkspaceMenuKeyboard-Brws6Ar9.js} +0 -0
@@ -2,6 +2,7 @@ const SessionEngine = require('./session-engine');
2
2
  const NativePtyHostClient = require('./native-pty-host-client');
3
3
  const { normalizeShellSessionOptions } = require('./local-session-engine');
4
4
  const { cleanupShellBusyIntegration } = require('./shell-busy-integration');
5
+ const { compareNativePtyRuntimeEpochs } = require('./native-pty-controller-generation');
5
6
 
6
7
  function isRecoverableConnectError(error) {
7
8
  const code = error && error.code;
@@ -13,6 +14,18 @@ function nativeSessionId(entry, fallback = '') {
13
14
  return entry.sessionId || entry.agentId || entry.metadata?.agentId || fallback || '';
14
15
  }
15
16
 
17
+ function recoveredRuntimeEpoch(entry) {
18
+ if (!entry || typeof entry !== 'object') return '';
19
+ if (typeof entry.runtimeEpoch === 'string' && entry.runtimeEpoch) return entry.runtimeEpoch;
20
+ return typeof entry.state?.runtimeEpoch === 'string' ? entry.state.runtimeEpoch : '';
21
+ }
22
+
23
+ function shouldAdvanceRuntimeEpoch(currentEpoch, nextEpoch) {
24
+ if (!nextEpoch) return false;
25
+ if (!currentEpoch || currentEpoch === nextEpoch) return true;
26
+ return compareNativePtyRuntimeEpochs(nextEpoch, currentEpoch) === 1;
27
+ }
28
+
16
29
  class NativeSessionEngine extends SessionEngine {
17
30
  constructor(options = {}) {
18
31
  super();
@@ -23,7 +36,10 @@ class NativeSessionEngine extends SessionEngine {
23
36
  });
24
37
  this.preserveHostOnDispose = options.preserveHostOnDispose === true;
25
38
  this.activeSessionIds = new Set();
26
- this.reconcilingHostDisconnect = false;
39
+ this.activeSessionEpochs = new Map();
40
+ this.hostDisconnectGeneration = 0;
41
+ this.reconciledHostDisconnectGeneration = 0;
42
+ this.hostDisconnectReconcilePromise = null;
27
43
  this.bindClientEvents();
28
44
  }
29
45
 
@@ -31,6 +47,7 @@ class NativeSessionEngine extends SessionEngine {
31
47
  [
32
48
  'session-started',
33
49
  'session-output',
50
+ 'session-transition',
34
51
  'session-sync',
35
52
  'session-preview',
36
53
  'session-title',
@@ -45,6 +62,7 @@ class NativeSessionEngine extends SessionEngine {
45
62
  });
46
63
  });
47
64
  this.client.on('host-disconnect', () => {
65
+ this.hostDisconnectGeneration += 1;
48
66
  this.reconcileHostDisconnect().catch(error => {
49
67
  const message = error && error.message ? error.message : 'Native pty host disconnected';
50
68
  this.failActiveSessions(message);
@@ -63,43 +81,68 @@ class NativeSessionEngine extends SessionEngine {
63
81
  const sessionId = nativeSessionId(payload);
64
82
  if (!sessionId) return;
65
83
  if (eventName === 'session-started') {
66
- this.activeSessionIds.add(sessionId);
84
+ const runtimeEpoch = typeof payload.runtimeEpoch === 'string' ? payload.runtimeEpoch : '';
85
+ const currentEpoch = this.activeSessionEpochs.get(sessionId) || '';
86
+ if (shouldAdvanceRuntimeEpoch(currentEpoch, runtimeEpoch)) {
87
+ this.activeSessionIds.add(sessionId);
88
+ this.activeSessionEpochs.set(sessionId, runtimeEpoch);
89
+ } else if (!currentEpoch && !runtimeEpoch) {
90
+ this.activeSessionIds.add(sessionId);
91
+ }
67
92
  } else if (eventName === 'session-exited') {
93
+ const currentEpoch = this.activeSessionEpochs.get(sessionId) || '';
94
+ const exitedEpoch = typeof payload.runtimeEpoch === 'string' ? payload.runtimeEpoch : '';
95
+ if (currentEpoch ? exitedEpoch !== currentEpoch : Boolean(exitedEpoch)) return;
68
96
  this.activeSessionIds.delete(sessionId);
97
+ this.activeSessionEpochs.delete(sessionId);
69
98
  }
70
99
  }
71
100
 
72
101
  async reconcileHostDisconnect() {
73
- if (this.reconcilingHostDisconnect) return;
74
- const expectedSessionIds = [...this.activeSessionIds];
75
- if (expectedSessionIds.length === 0) return;
76
-
77
- this.reconcilingHostDisconnect = true;
78
- try {
79
- const recovered = await this.recoverSessions({ startHost: true });
80
- const recoveredIds = new Set((recovered || [])
81
- .map(entry => nativeSessionId(entry))
82
- .filter(Boolean));
83
-
84
- for (const sessionId of expectedSessionIds) {
85
- if (recoveredIds.has(sessionId)) continue;
86
- this.activeSessionIds.delete(sessionId);
87
- this.emit('session-error', {
102
+ if (this.hostDisconnectReconcilePromise) return this.hostDisconnectReconcilePromise;
103
+ this.hostDisconnectReconcilePromise = (async () => {
104
+ while (this.reconciledHostDisconnectGeneration < this.hostDisconnectGeneration) {
105
+ const generation = this.hostDisconnectGeneration;
106
+ const expectedSessions = [...this.activeSessionIds].map(sessionId => ({
88
107
  sessionId,
89
- error: 'Native pty host disconnected; terminal session is no longer recoverable',
90
- fatal: true,
91
- });
108
+ runtimeEpoch: this.activeSessionEpochs.get(sessionId) || '',
109
+ }));
110
+ if (expectedSessions.length > 0) {
111
+ const recovered = await this.recoverSessions({ startHost: true });
112
+ const recoveredIds = new Set((recovered || [])
113
+ .map(entry => nativeSessionId(entry))
114
+ .filter(Boolean));
115
+
116
+ for (const expected of expectedSessions) {
117
+ if (recoveredIds.has(expected.sessionId)) continue;
118
+ this.activeSessionIds.delete(expected.sessionId);
119
+ this.activeSessionEpochs.delete(expected.sessionId);
120
+ this.emit('session-error', {
121
+ sessionId: expected.sessionId,
122
+ runtimeEpoch: expected.runtimeEpoch,
123
+ error: 'Native pty host disconnected; terminal session is no longer recoverable',
124
+ fatal: true,
125
+ });
126
+ }
127
+ }
128
+ this.reconciledHostDisconnectGeneration = generation;
92
129
  }
130
+ })();
131
+ try {
132
+ await this.hostDisconnectReconcilePromise;
93
133
  } finally {
94
- this.reconcilingHostDisconnect = false;
134
+ this.hostDisconnectReconcilePromise = null;
95
135
  }
96
136
  }
97
137
 
98
138
  failActiveSessions(message) {
99
139
  for (const sessionId of [...this.activeSessionIds]) {
100
140
  this.activeSessionIds.delete(sessionId);
141
+ const runtimeEpoch = this.activeSessionEpochs.get(sessionId) || '';
142
+ this.activeSessionEpochs.delete(sessionId);
101
143
  this.emit('session-error', {
102
144
  sessionId,
145
+ runtimeEpoch,
103
146
  error: message,
104
147
  fatal: true,
105
148
  });
@@ -128,25 +171,37 @@ class NativeSessionEngine extends SessionEngine {
128
171
  return result;
129
172
  }
130
173
 
131
- async sendInput(sessionId, input) {
132
- return this.client.request('sendInput', { sessionId, input });
174
+ async sendInput(sessionId, input, options = {}) {
175
+ return this.client.request('sendInput', {
176
+ sessionId,
177
+ input,
178
+ expectedRuntimeEpoch: options.expectedRuntimeEpoch || '',
179
+ }, {
180
+ retryOnDisconnect: false,
181
+ });
133
182
  }
134
183
 
135
- async interruptSession(sessionId, input = '\x03') {
136
- return this.sendInput(sessionId, input);
184
+ async interruptSession(sessionId, input = '\x03', options = {}) {
185
+ return this.sendInput(sessionId, input, options);
137
186
  }
138
187
 
139
188
  async resizeSession(sessionId, cols, rows) {
140
189
  return this.client.request('resizeSession', { sessionId, cols, rows });
141
190
  }
142
191
 
143
- async clearBuffer(sessionId) {
144
- return this.client.request('clearBuffer', { sessionId });
192
+ async clearBuffer(sessionId, options = {}) {
193
+ return this.client.request('clearBuffer', {
194
+ sessionId,
195
+ expectedRuntimeEpoch: options.expectedRuntimeEpoch || '',
196
+ }, {
197
+ retryOnDisconnect: false,
198
+ });
145
199
  }
146
200
 
147
201
  async killSession(sessionId) {
148
202
  const result = await this.client.request('killSession', { sessionId });
149
203
  this.activeSessionIds.delete(sessionId);
204
+ this.activeSessionEpochs.delete(sessionId);
150
205
  return result;
151
206
  }
152
207
 
@@ -154,6 +209,10 @@ class NativeSessionEngine extends SessionEngine {
154
209
  return this.client.request('getSessionState', { sessionId });
155
210
  }
156
211
 
212
+ async getSessionAttachCheckpoint(sessionId) {
213
+ return this.client.request('getSessionAttachCheckpoint', { sessionId });
214
+ }
215
+
157
216
  async getSessionPreview(sessionId) {
158
217
  return this.client.request('getSessionPreview', { sessionId });
159
218
  }
@@ -172,7 +231,13 @@ class NativeSessionEngine extends SessionEngine {
172
231
  const recovered = await this.client.request('recoverSessions', {}, { startHost });
173
232
  for (const entry of recovered || []) {
174
233
  const sessionId = nativeSessionId(entry);
175
- if (sessionId) this.activeSessionIds.add(sessionId);
234
+ if (!sessionId) continue;
235
+ this.activeSessionIds.add(sessionId);
236
+ const runtimeEpoch = recoveredRuntimeEpoch(entry);
237
+ const currentEpoch = this.activeSessionEpochs.get(sessionId) || '';
238
+ if (shouldAdvanceRuntimeEpoch(currentEpoch, runtimeEpoch)) {
239
+ this.activeSessionEpochs.set(sessionId, runtimeEpoch);
240
+ }
176
241
  }
177
242
  return recovered;
178
243
  } catch (error) {
@@ -185,11 +250,18 @@ class NativeSessionEngine extends SessionEngine {
185
250
  return this.client.request('updateSessionMetadata', { sessionId, patch });
186
251
  }
187
252
 
253
+ consumeRuntimeRotation() {
254
+ return typeof this.client.consumeRuntimeRotation === 'function'
255
+ ? this.client.consumeRuntimeRotation()
256
+ : null;
257
+ }
258
+
188
259
  dispose(options = {}) {
189
260
  this.client.disconnect({
190
261
  preserveHost: options.preserveHost === true || this.preserveHostOnDispose,
191
262
  });
192
263
  this.activeSessionIds.clear();
264
+ this.activeSessionEpochs.clear();
193
265
  }
194
266
  }
195
267
 
@@ -15,8 +15,27 @@ function packagedNodePtyTargetDir() {
15
15
  function copyIfExists(source, target, mode) {
16
16
  if (!fs.existsSync(source)) return false;
17
17
  fs.mkdirSync(path.dirname(target), { recursive: true });
18
- fs.copyFileSync(source, target);
19
- if (mode !== undefined) fs.chmodSync(target, mode);
18
+ let unchanged = false;
19
+ try {
20
+ const sourceStat = fs.statSync(source);
21
+ const targetStat = fs.statSync(target);
22
+ unchanged = sourceStat.size === targetStat.size
23
+ && fs.readFileSync(source).equals(fs.readFileSync(target));
24
+ } catch {
25
+ unchanged = false;
26
+ }
27
+ if (!unchanged) {
28
+ const temporary = `${target}.${process.pid}.${Math.random().toString(16).slice(2)}.tmp`;
29
+ try {
30
+ fs.copyFileSync(source, temporary, fs.constants.COPYFILE_EXCL);
31
+ if (mode !== undefined) fs.chmodSync(temporary, mode);
32
+ fs.renameSync(temporary, target);
33
+ } finally {
34
+ fs.rmSync(temporary, { force: true });
35
+ }
36
+ } else if (mode !== undefined) {
37
+ fs.chmodSync(target, mode);
38
+ }
20
39
  return true;
21
40
  }
22
41
 
@@ -75,5 +94,6 @@ function loadNodePty() {
75
94
 
76
95
  module.exports = loadNodePty();
77
96
  module.exports.copyPackagedSpawnHelper = copyPackagedSpawnHelper;
97
+ module.exports.copyIfExists = copyIfExists;
78
98
  module.exports.preparePackagedNodePtyRuntime = preparePackagedNodePtyRuntime;
79
99
  module.exports.isPackagedRuntime = isPackagedRuntime;
@@ -0,0 +1,253 @@
1
+ const path = require('path');
2
+ const {
3
+ createProviderSessionId,
4
+ createTemporaryProviderSessionId,
5
+ isSafeProviderSessionId,
6
+ } = require('./provider-session-id');
7
+
8
+ const CODEX_VALUE_OPTIONS = new Set([
9
+ '-a', '-c', '-C', '-m', '-s', '--ask-for-approval', '--cd', '--config',
10
+ '--config-profile', '--model', '--profile', '--sandbox',
11
+ ]);
12
+
13
+ function optionTakesValue(option, valueOptions) {
14
+ return Boolean(option && !option.includes('=') && valueOptions.has(option));
15
+ }
16
+
17
+ function firstCodexSubcommand(args) {
18
+ for (let index = 0; index < args.length; index += 1) {
19
+ const arg = args[index];
20
+ if (!arg) continue;
21
+ if (arg.startsWith('-')) {
22
+ if (optionTakesValue(arg, CODEX_VALUE_OPTIONS)) index += 1;
23
+ continue;
24
+ }
25
+ return { value: arg, index };
26
+ }
27
+ return null;
28
+ }
29
+
30
+ function codexSessionIdAfterSubcommand(args, subcommandIndex) {
31
+ const positional = [];
32
+ for (let index = subcommandIndex + 1; index < args.length; index += 1) {
33
+ const arg = args[index];
34
+ if (!arg) continue;
35
+ if (arg.startsWith('-')) {
36
+ if (optionTakesValue(arg, CODEX_VALUE_OPTIONS)) index += 1;
37
+ continue;
38
+ }
39
+ positional.push(arg);
40
+ }
41
+ const sessionId = positional.at(-1) || '';
42
+ return isSafeProviderSessionId(sessionId) ? sessionId : '';
43
+ }
44
+
45
+ function argValue(args, names) {
46
+ for (let index = 0; index < args.length; index += 1) {
47
+ const arg = args[index];
48
+ for (const name of names) {
49
+ if (arg === name) return args[index + 1] || '';
50
+ if (arg.startsWith(`${name}=`)) return arg.slice(name.length + 1);
51
+ }
52
+ }
53
+ return '';
54
+ }
55
+
56
+ function hasArg(args, names) {
57
+ return args.some(arg => names.includes(arg) || names.some(name => arg.startsWith(`${name}=`)));
58
+ }
59
+
60
+ function codexSessionPlan(rawArgs) {
61
+ const subcommand = firstCodexSubcommand(rawArgs);
62
+ if (subcommand?.value === 'resume') {
63
+ const id = codexSessionIdAfterSubcommand(rawArgs, subcommand.index);
64
+ if (id) return { id, temporary: false, source: 'resume' };
65
+ }
66
+ if (subcommand?.value === 'fork') {
67
+ return {
68
+ id: createTemporaryProviderSessionId(),
69
+ temporary: true,
70
+ source: 'codex-fork-temporary',
71
+ forkedFromProviderSessionId: codexSessionIdAfterSubcommand(rawArgs, subcommand.index),
72
+ };
73
+ }
74
+ if (subcommand?.value) return null;
75
+ return { id: createTemporaryProviderSessionId(), temporary: true, source: 'codex-temporary' };
76
+ }
77
+
78
+ function explicitSessionPlan(provider, rawArgs, launchArgs) {
79
+ const explicitSessionId = argValue(rawArgs, ['--session-id']);
80
+ const resumeSessionId = argValue(rawArgs, ['--resume']);
81
+ const isFork = hasArg(rawArgs, ['--fork-session']);
82
+ const isContinue = hasArg(rawArgs, provider === 'claude' ? ['--continue', '-c'] : ['--continue']);
83
+ if (explicitSessionId && isSafeProviderSessionId(explicitSessionId)) {
84
+ return {
85
+ id: explicitSessionId,
86
+ temporary: false,
87
+ source: 'launch-session-id',
88
+ forkedFromProviderSessionId: isFork && isSafeProviderSessionId(resumeSessionId) ? resumeSessionId : '',
89
+ };
90
+ }
91
+ if (resumeSessionId && isSafeProviderSessionId(resumeSessionId) && !isFork) {
92
+ return { id: resumeSessionId, temporary: false, source: 'resume' };
93
+ }
94
+ if (isContinue) return null;
95
+ const id = createProviderSessionId();
96
+ return {
97
+ id,
98
+ temporary: false,
99
+ source: isFork ? `${provider}-fork-session-id` : `${provider}-session-id`,
100
+ forkedFromProviderSessionId: isFork && isSafeProviderSessionId(resumeSessionId) ? resumeSessionId : '',
101
+ args: ['--session-id', id, ...launchArgs],
102
+ };
103
+ }
104
+
105
+ function openCodeSessionPlan(rawArgs) {
106
+ const id = argValue(rawArgs, ['--session', '-s']);
107
+ if (!id || !isSafeProviderSessionId(id)) return null;
108
+ return hasArg(rawArgs, ['--fork'])
109
+ ? {
110
+ id: createTemporaryProviderSessionId(),
111
+ temporary: true,
112
+ source: 'opencode-fork-temporary',
113
+ forkedFromProviderSessionId: id,
114
+ }
115
+ : { id, temporary: false, source: 'resume' };
116
+ }
117
+
118
+ function codexAcpEnvironment(options = {}) {
119
+ const env = { ...(options.env || process.env) };
120
+ if (options.executable && !env.CODEX_PATH) env.CODEX_PATH = options.executable;
121
+ let config = {};
122
+ if (env.CODEX_CONFIG) {
123
+ try {
124
+ const parsed = JSON.parse(env.CODEX_CONFIG);
125
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) config = parsed;
126
+ } catch {
127
+ // A selected Farming profile below replaces an invalid adapter config.
128
+ }
129
+ }
130
+ if (options.model && options.model !== 'config') config.model = options.model;
131
+ if (options.reasoningEffort && options.reasoningEffort !== 'config') config.model_reasoning_effort = options.reasoningEffort;
132
+ if (options.serviceTier && !['config', 'default'].includes(options.serviceTier)) config.service_tier = options.serviceTier;
133
+ if (Object.keys(config).length > 0) env.CODEX_CONFIG = JSON.stringify(config);
134
+ const initialMode = { ask: 'read-only', approve: 'agent', full: 'agent-full-access' }[options.approvalMode];
135
+ if (initialMode) env.INITIAL_AGENT_MODE = initialMode;
136
+ return env;
137
+ }
138
+
139
+ const PROVIDER_ADAPTERS = Object.freeze([
140
+ {
141
+ id: 'codex',
142
+ displayName: 'codex',
143
+ executable: 'codex',
144
+ homeEnvKey: 'CODEX_HOME',
145
+ interruptInput: '\x1b',
146
+ freshAcpSessionSources: ['codex-temporary'],
147
+ commands: ['codex'],
148
+ supportedRuntimes: ['terminal', 'acp', 'json', 'app-server'],
149
+ planSession: codexSessionPlan,
150
+ acp: { packageName: '@agentclientprotocol/codex-acp', version: '1.1.4' },
151
+ prepareAcpEnvironment: codexAcpEnvironment,
152
+ capabilities: { runtimeSwitch: true, terminalProfile: true, goals: true },
153
+ },
154
+ {
155
+ id: 'claude',
156
+ displayName: 'claude code',
157
+ executable: 'claude',
158
+ homeEnvKey: 'CLAUDE_CONFIG_DIR',
159
+ interruptInput: '\x1b',
160
+ freshAcpSessionSources: ['claude-session-id'],
161
+ commands: ['claude'],
162
+ supportedRuntimes: ['terminal', 'acp'],
163
+ planSession: (rawArgs, launchArgs) => explicitSessionPlan('claude', rawArgs, launchArgs),
164
+ acp: { packageName: '@agentclientprotocol/claude-agent-acp', version: '0.59.0' },
165
+ capabilities: { runtimeSwitch: true, terminalProfile: false, goals: false },
166
+ },
167
+ {
168
+ id: 'opencode',
169
+ displayName: 'opencode',
170
+ executable: 'opencode',
171
+ homeEnvKey: 'OPENCODE_CONFIG_DIR',
172
+ interruptInput: '\x03',
173
+ freshAcpSessionSources: [],
174
+ commands: ['opencode'],
175
+ supportedRuntimes: ['terminal', 'acp', 'json'],
176
+ planSession: openCodeSessionPlan,
177
+ acp: {
178
+ version: 'native',
179
+ launch: options => ({
180
+ command: options.executable || 'opencode',
181
+ args: ['acp', '--cwd', path.resolve(options.cwd || process.cwd())],
182
+ }),
183
+ },
184
+ capabilities: { runtimeSwitch: true, terminalProfile: false, goals: false },
185
+ },
186
+ {
187
+ id: 'qoder',
188
+ displayName: 'qoder',
189
+ executable: 'qodercli',
190
+ homeEnvKey: 'QODER_CONFIG_DIR',
191
+ interruptInput: '\x1b',
192
+ freshAcpSessionSources: ['qoder-session-id'],
193
+ commands: ['qoder', 'qodercli'],
194
+ supportedRuntimes: ['terminal', 'acp'],
195
+ planSession: (rawArgs, launchArgs) => explicitSessionPlan('qoder', rawArgs, launchArgs),
196
+ acp: {
197
+ version: 'native',
198
+ launch: options => ({ command: options.executable || 'qodercli', args: ['--acp'] }),
199
+ },
200
+ capabilities: { runtimeSwitch: true, terminalProfile: false, goals: false },
201
+ },
202
+ ]);
203
+
204
+ const ADAPTER_BY_ID = new Map(PROVIDER_ADAPTERS.map(adapter => [adapter.id, Object.freeze(adapter)]));
205
+ const ADAPTER_BY_COMMAND = new Map(PROVIDER_ADAPTERS.flatMap(adapter => (
206
+ adapter.commands.map(command => [command, adapter])
207
+ )));
208
+
209
+ function getProviderAdapter(provider) {
210
+ return ADAPTER_BY_ID.get(String(provider || '').trim().toLowerCase()) || null;
211
+ }
212
+
213
+ function providerForProgram(program) {
214
+ return ADAPTER_BY_COMMAND.get(path.basename(String(program || '').trim()))?.id || '';
215
+ }
216
+
217
+ function listProviderAdapters() {
218
+ return [...PROVIDER_ADAPTERS];
219
+ }
220
+
221
+ function providerCapabilities(provider) {
222
+ const adapter = getProviderAdapter(provider);
223
+ return {
224
+ supportedRuntimes: adapter ? [...adapter.supportedRuntimes] : ['terminal'],
225
+ runtimeSwitch: adapter?.capabilities?.runtimeSwitch === true,
226
+ terminalProfile: adapter?.capabilities?.terminalProfile === true,
227
+ goals: adapter?.capabilities?.goals === true,
228
+ };
229
+ }
230
+
231
+ function providerSupportsRuntime(provider, runtime) {
232
+ return getProviderAdapter(provider)?.supportedRuntimes.includes(runtime) === true;
233
+ }
234
+
235
+ function applyProviderHomeEnvironment(env, provider, homePath) {
236
+ const key = getProviderAdapter(provider)?.homeEnvKey;
237
+ if (key && homePath) env[key] = homePath;
238
+ return env;
239
+ }
240
+
241
+ function isFreshAcpSessionSource(provider, source) {
242
+ return getProviderAdapter(provider)?.freshAcpSessionSources.includes(source) === true;
243
+ }
244
+
245
+ module.exports = {
246
+ getProviderAdapter,
247
+ applyProviderHomeEnvironment,
248
+ isFreshAcpSessionSource,
249
+ listProviderAdapters,
250
+ providerCapabilities,
251
+ providerForProgram,
252
+ providerSupportsRuntime,
253
+ };