@cloudcli-ai/cloudcli 1.36.0 → 1.36.2
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.de.md +17 -18
- package/README.ja.md +16 -17
- package/README.ko.md +17 -18
- package/README.md +19 -20
- package/README.ru.md +17 -18
- package/README.tr.md +17 -18
- package/README.zh-CN.md +17 -18
- package/README.zh-TW.md +17 -18
- package/dist/api-docs.html +2 -3
- package/dist/assets/{index-S6cIy7hj.js → index-CDGzV0LN.js} +264 -259
- package/dist/assets/index-DLmhQ15L.css +32 -0
- package/dist/assets/{vendor-xterm-CJZjLICi.js → vendor-xterm-CS4rQ5Mr.js} +12 -12
- package/dist/index.html +3 -3
- package/dist-server/server/claude-sdk.js +37 -90
- package/dist-server/server/claude-sdk.js.map +1 -1
- package/dist-server/server/cli.js +1 -4
- package/dist-server/server/cli.js.map +1 -1
- package/dist-server/server/cursor-cli.js +12 -11
- package/dist-server/server/cursor-cli.js.map +1 -1
- package/dist-server/server/index.js +10 -141
- package/dist-server/server/index.js.map +1 -1
- package/dist-server/server/modules/assets/assets.routes.js +89 -0
- package/dist-server/server/modules/assets/assets.routes.js.map +1 -0
- package/dist-server/server/modules/assets/index.js +4 -0
- package/dist-server/server/modules/assets/index.js.map +1 -0
- package/dist-server/server/modules/assets/services/image-assets.service.js +57 -0
- package/dist-server/server/modules/assets/services/image-assets.service.js.map +1 -0
- package/dist-server/server/modules/assets/tests/image-assets.service.test.js +36 -0
- package/dist-server/server/modules/assets/tests/image-assets.service.test.js.map +1 -0
- package/dist-server/server/modules/browser-use/browser-use.service.js +6 -3
- package/dist-server/server/modules/browser-use/browser-use.service.js.map +1 -1
- package/dist-server/server/modules/database/tests/sessions-provider-mapping.test.js +2 -2
- package/dist-server/server/modules/database/tests/sessions-provider-mapping.test.js.map +1 -1
- package/dist-server/server/modules/notifications/services/notification-orchestrator.service.js +0 -1
- package/dist-server/server/modules/notifications/services/notification-orchestrator.service.js.map +1 -1
- package/dist-server/server/modules/projects/services/project-clone.service.js +2 -1
- package/dist-server/server/modules/projects/services/project-clone.service.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js +28 -0
- package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js +54 -1
- package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js +36 -1
- package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/cursor/cursor-models.provider.js +3 -17
- package/dist-server/server/modules/providers/list/cursor/cursor-models.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.js +7 -1
- package/dist-server/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/cursor/cursor-sessions.provider.js +49 -13
- package/dist-server/server/modules/providers/list/cursor/cursor-sessions.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/opencode/opencode-auth.provider.js +0 -1
- package/dist-server/server/modules/providers/list/opencode/opencode-auth.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js +8 -14
- package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js +24 -5
- package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/opencode/opencode-sessions.provider.js +10 -21
- package/dist-server/server/modules/providers/list/opencode/opencode-sessions.provider.js.map +1 -1
- package/dist-server/server/modules/providers/provider.registry.js +0 -2
- package/dist-server/server/modules/providers/provider.registry.js.map +1 -1
- package/dist-server/server/modules/providers/provider.routes.js +0 -1
- package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
- package/dist-server/server/modules/providers/services/provider-capabilities.service.js +7 -14
- package/dist-server/server/modules/providers/services/provider-capabilities.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/provider-models.service.js +1 -1
- package/dist-server/server/modules/providers/services/provider-models.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/session-conversations-search.service.js +3 -77
- package/dist-server/server/modules/providers/services/session-conversations-search.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/session-synchronizer.service.js +0 -1
- package/dist-server/server/modules/providers/services/session-synchronizer.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/sessions-watcher.service.js +0 -13
- package/dist-server/server/modules/providers/services/sessions-watcher.service.js.map +1 -1
- package/dist-server/server/modules/providers/tests/codex-sessions.test.js +96 -0
- package/dist-server/server/modules/providers/tests/codex-sessions.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/mcp.test.js +3 -29
- package/dist-server/server/modules/providers/tests/mcp.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/opencode-models.test.js +7 -0
- package/dist-server/server/modules/providers/tests/opencode-models.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/opencode-sessions.test.js +117 -0
- package/dist-server/server/modules/providers/tests/opencode-sessions.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/provider-image-history.test.js +147 -0
- package/dist-server/server/modules/providers/tests/provider-image-history.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/provider-models.service.test.js +8 -8
- package/dist-server/server/modules/providers/tests/skills.test.js +3 -26
- package/dist-server/server/modules/providers/tests/skills.test.js.map +1 -1
- package/dist-server/server/modules/websocket/services/chat-run-registry.service.js +14 -0
- package/dist-server/server/modules/websocket/services/chat-run-registry.service.js.map +1 -1
- package/dist-server/server/modules/websocket/services/chat-websocket.service.js +36 -2
- package/dist-server/server/modules/websocket/services/chat-websocket.service.js.map +1 -1
- package/dist-server/server/modules/websocket/services/shell-websocket.service.js +3 -12
- package/dist-server/server/modules/websocket/services/shell-websocket.service.js.map +1 -1
- package/dist-server/server/modules/websocket/tests/chat-image-filter.test.js +33 -0
- package/dist-server/server/modules/websocket/tests/chat-image-filter.test.js.map +1 -0
- package/dist-server/server/modules/websocket/tests/chat-run-registry.test.js +37 -4
- package/dist-server/server/modules/websocket/tests/chat-run-registry.test.js.map +1 -1
- package/dist-server/server/openai-codex.js +8 -2
- package/dist-server/server/openai-codex.js.map +1 -1
- package/dist-server/server/opencode-cli.js +43 -6
- package/dist-server/server/opencode-cli.js.map +1 -1
- package/dist-server/server/opencode-cli.test.js +103 -3
- package/dist-server/server/opencode-cli.test.js.map +1 -1
- package/dist-server/server/routes/agent.js +9 -19
- package/dist-server/server/routes/agent.js.map +1 -1
- package/dist-server/server/routes/commands.js +1 -2
- package/dist-server/server/routes/commands.js.map +1 -1
- package/dist-server/server/routes/git.js +179 -53
- package/dist-server/server/routes/git.js.map +1 -1
- package/dist-server/server/routes/git.test.js +80 -0
- package/dist-server/server/routes/git.test.js.map +1 -0
- package/dist-server/server/routes/taskmaster.js +3 -1
- package/dist-server/server/routes/taskmaster.js.map +1 -1
- package/dist-server/server/routes/user.js +2 -1
- package/dist-server/server/routes/user.js.map +1 -1
- package/dist-server/server/shared/image-attachments.js +270 -0
- package/dist-server/server/shared/image-attachments.js.map +1 -0
- package/dist-server/server/shared/tests/image-attachments.test.js +220 -0
- package/dist-server/server/shared/tests/image-attachments.test.js.map +1 -0
- package/dist-server/server/shared/utils.js +43 -0
- package/dist-server/server/shared/utils.js.map +1 -1
- package/dist-server/server/utils/gitConfig.js +2 -1
- package/dist-server/server/utils/gitConfig.js.map +1 -1
- package/dist-server/server/utils/plugin-process-manager.js +2 -1
- package/dist-server/server/utils/plugin-process-manager.js.map +1 -1
- package/electron/launcher/launcher.js +1 -1
- package/package.json +4 -4
- package/public/api-docs.html +2 -3
- package/server/claude-sdk.js +37 -105
- package/server/cli.js +1 -4
- package/server/cursor-cli.js +13 -12
- package/server/index.js +11 -159
- package/server/modules/assets/assets.routes.ts +103 -0
- package/server/modules/assets/index.ts +3 -0
- package/server/modules/assets/services/image-assets.service.ts +82 -0
- package/server/modules/assets/tests/image-assets.service.test.ts +46 -0
- package/server/modules/browser-use/browser-use.service.ts +7 -3
- package/server/modules/database/tests/sessions-provider-mapping.test.ts +2 -2
- package/server/modules/notifications/services/notification-orchestrator.service.js +0 -1
- package/server/modules/projects/services/project-clone.service.ts +3 -1
- package/server/modules/providers/README.md +1 -7
- package/server/modules/providers/list/claude/claude-sessions.provider.ts +30 -0
- package/server/modules/providers/list/codex/codex-session-synchronizer.provider.ts +60 -1
- package/server/modules/providers/list/codex/codex-sessions.provider.ts +41 -1
- package/server/modules/providers/list/cursor/cursor-models.provider.ts +3 -17
- package/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.ts +7 -1
- package/server/modules/providers/list/cursor/cursor-sessions.provider.ts +56 -15
- package/server/modules/providers/list/opencode/opencode-auth.provider.ts +0 -1
- package/server/modules/providers/list/opencode/opencode-models.provider.ts +15 -19
- package/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.ts +25 -4
- package/server/modules/providers/list/opencode/opencode-sessions.provider.ts +10 -21
- package/server/modules/providers/provider.registry.ts +0 -2
- package/server/modules/providers/provider.routes.ts +0 -1
- package/server/modules/providers/services/provider-capabilities.service.ts +7 -14
- package/server/modules/providers/services/provider-models.service.ts +1 -1
- package/server/modules/providers/services/session-conversations-search.service.ts +3 -93
- package/server/modules/providers/services/session-synchronizer.service.ts +0 -1
- package/server/modules/providers/services/sessions-watcher.service.ts +0 -14
- package/server/modules/providers/tests/codex-sessions.test.ts +111 -0
- package/server/modules/providers/tests/mcp.test.ts +3 -34
- package/server/modules/providers/tests/opencode-models.test.ts +7 -0
- package/server/modules/providers/tests/opencode-sessions.test.ts +139 -0
- package/server/modules/providers/tests/provider-image-history.test.ts +180 -0
- package/server/modules/providers/tests/provider-models.service.test.ts +8 -8
- package/server/modules/providers/tests/skills.test.ts +3 -42
- package/server/modules/websocket/services/chat-run-registry.service.ts +16 -0
- package/server/modules/websocket/services/chat-websocket.service.ts +41 -2
- package/server/modules/websocket/services/shell-websocket.service.ts +1 -11
- package/server/modules/websocket/tests/chat-image-filter.test.ts +44 -0
- package/server/modules/websocket/tests/chat-run-registry.test.ts +42 -4
- package/server/openai-codex.js +9 -1
- package/server/opencode-cli.js +44 -6
- package/server/opencode-cli.test.js +112 -3
- package/server/routes/agent.js +9 -19
- package/server/routes/commands.js +1 -2
- package/server/routes/git.js +201 -60
- package/server/routes/git.test.js +106 -0
- package/server/routes/taskmaster.js +3 -1
- package/server/routes/user.js +2 -1
- package/server/shared/image-attachments.ts +341 -0
- package/server/shared/tests/image-attachments.test.ts +298 -0
- package/server/shared/types.ts +1 -1
- package/server/shared/utils.ts +45 -0
- package/server/utils/gitConfig.js +2 -1
- package/server/utils/plugin-process-manager.js +3 -1
- package/dist/assets/index-uLUE6Qt1.css +0 -32
- package/dist/icons/gemini-ai-icon.svg +0 -1
- package/dist-server/server/gemini-cli.js +0 -551
- package/dist-server/server/gemini-cli.js.map +0 -1
- package/dist-server/server/gemini-response-handler.js +0 -106
- package/dist-server/server/gemini-response-handler.js.map +0 -1
- package/dist-server/server/modules/providers/list/gemini/gemini-auth.provider.js +0 -254
- package/dist-server/server/modules/providers/list/gemini/gemini-auth.provider.js.map +0 -1
- package/dist-server/server/modules/providers/list/gemini/gemini-mcp.provider.js +0 -82
- package/dist-server/server/modules/providers/list/gemini/gemini-mcp.provider.js.map +0 -1
- package/dist-server/server/modules/providers/list/gemini/gemini-models.provider.js +0 -24
- package/dist-server/server/modules/providers/list/gemini/gemini-models.provider.js.map +0 -1
- package/dist-server/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.js +0 -312
- package/dist-server/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.js.map +0 -1
- package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js +0 -474
- package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js.map +0 -1
- package/dist-server/server/modules/providers/list/gemini/gemini-skills.provider.js +0 -40
- package/dist-server/server/modules/providers/list/gemini/gemini-skills.provider.js.map +0 -1
- package/dist-server/server/modules/providers/list/gemini/gemini.provider.js +0 -19
- package/dist-server/server/modules/providers/list/gemini/gemini.provider.js.map +0 -1
- package/dist-server/server/routes/gemini.js +0 -21
- package/dist-server/server/routes/gemini.js.map +0 -1
- package/dist-server/server/sessionManager.js +0 -194
- package/dist-server/server/sessionManager.js.map +0 -1
- package/server/gemini-cli.js +0 -638
- package/server/gemini-response-handler.js +0 -117
- package/server/modules/providers/list/gemini/gemini-auth.provider.ts +0 -307
- package/server/modules/providers/list/gemini/gemini-mcp.provider.ts +0 -110
- package/server/modules/providers/list/gemini/gemini-models.provider.ts +0 -39
- package/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.ts +0 -405
- package/server/modules/providers/list/gemini/gemini-sessions.provider.ts +0 -540
- package/server/modules/providers/list/gemini/gemini-skills.provider.ts +0 -44
- package/server/modules/providers/list/gemini/gemini.provider.ts +0 -27
- package/server/routes/gemini.js +0 -25
- package/server/sessionManager.js +0 -226
|
@@ -129,6 +129,44 @@ test('complete marks the run finished and duplicate completes are dropped', asyn
|
|
|
129
129
|
});
|
|
130
130
|
});
|
|
131
131
|
|
|
132
|
+
test('a finished run\'s safety net cannot complete the session\'s next run', async () => {
|
|
133
|
+
await withIsolatedDatabase(() => {
|
|
134
|
+
sessionsDb.createAppSession('app-run-9', 'codex', '/workspace/demo');
|
|
135
|
+
const connection = new FakeConnection();
|
|
136
|
+
|
|
137
|
+
const firstRun = chatRunRegistry.startRun({
|
|
138
|
+
appSessionId: 'app-run-9',
|
|
139
|
+
provider: 'codex',
|
|
140
|
+
providerSessionId: null,
|
|
141
|
+
connection,
|
|
142
|
+
userId: null,
|
|
143
|
+
});
|
|
144
|
+
assert.ok(firstRun);
|
|
145
|
+
firstRun.writer.send({ kind: 'complete', provider: 'codex', sessionId: 'native-9', exitCode: 0 });
|
|
146
|
+
|
|
147
|
+
// A queued message starts the next run before the first run's runtime
|
|
148
|
+
// promise settles (the chat handler's `finally` hasn't executed yet).
|
|
149
|
+
const secondRun = chatRunRegistry.startRun({
|
|
150
|
+
appSessionId: 'app-run-9',
|
|
151
|
+
provider: 'codex',
|
|
152
|
+
providerSessionId: null,
|
|
153
|
+
connection,
|
|
154
|
+
userId: null,
|
|
155
|
+
});
|
|
156
|
+
assert.ok(secondRun);
|
|
157
|
+
|
|
158
|
+
// First run's safety net fires late: it must not touch the new run.
|
|
159
|
+
chatRunRegistry.completeRunIfCurrent(firstRun, { exitCode: 1 });
|
|
160
|
+
assert.equal(chatRunRegistry.isProcessing('app-run-9'), true);
|
|
161
|
+
assert.equal(connection.frames.filter((frame) => frame.kind === 'complete').length, 1);
|
|
162
|
+
|
|
163
|
+
// The second run's own safety net still works while it is current.
|
|
164
|
+
chatRunRegistry.completeRunIfCurrent(secondRun, { exitCode: 1 });
|
|
165
|
+
assert.equal(chatRunRegistry.isProcessing('app-run-9'), false);
|
|
166
|
+
assert.equal(connection.frames.filter((frame) => frame.kind === 'complete').length, 2);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
132
170
|
test('listRunningRuns returns only currently running app sessions', async () => {
|
|
133
171
|
await withIsolatedDatabase(() => {
|
|
134
172
|
sessionsDb.createAppSession('app-run-7', 'claude', '/workspace/demo');
|
|
@@ -186,22 +224,22 @@ test('replayEvents returns only events after the requested seq', async () => {
|
|
|
186
224
|
|
|
187
225
|
test('attachConnection reroutes the live stream to a new socket', async () => {
|
|
188
226
|
await withIsolatedDatabase(() => {
|
|
189
|
-
sessionsDb.createAppSession('app-run-5', '
|
|
227
|
+
sessionsDb.createAppSession('app-run-5', 'opencode', '/workspace/demo');
|
|
190
228
|
const firstConnection = new FakeConnection();
|
|
191
229
|
const run = chatRunRegistry.startRun({
|
|
192
230
|
appSessionId: 'app-run-5',
|
|
193
|
-
provider: '
|
|
231
|
+
provider: 'opencode',
|
|
194
232
|
providerSessionId: null,
|
|
195
233
|
connection: firstConnection,
|
|
196
234
|
userId: null,
|
|
197
235
|
});
|
|
198
236
|
assert.ok(run);
|
|
199
237
|
|
|
200
|
-
run.writer.send({ kind: 'stream_delta', provider: '
|
|
238
|
+
run.writer.send({ kind: 'stream_delta', provider: 'opencode', sessionId: 'o', content: 'before' });
|
|
201
239
|
|
|
202
240
|
const secondConnection = new FakeConnection();
|
|
203
241
|
assert.equal(chatRunRegistry.attachConnection('app-run-5', secondConnection), true);
|
|
204
|
-
run.writer.send({ kind: 'stream_delta', provider: '
|
|
242
|
+
run.writer.send({ kind: 'stream_delta', provider: 'opencode', sessionId: 'o', content: 'after' });
|
|
205
243
|
|
|
206
244
|
assert.deepEqual(firstConnection.frames.map((frame) => frame.content), ['before']);
|
|
207
245
|
assert.deepEqual(secondConnection.frames.map((frame) => frame.content), ['after']);
|
package/server/openai-codex.js
CHANGED
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import { Codex } from '@openai/codex-sdk';
|
|
17
|
+
|
|
18
|
+
import { buildCodexInputItems, normalizeImageDescriptors } from './shared/image-attachments.js';
|
|
17
19
|
import { notifyRunFailed, notifyRunStopped } from './services/notification-orchestrator.js';
|
|
18
20
|
import { sessionsService } from './modules/providers/services/sessions.service.js';
|
|
19
21
|
import { providerAuthService } from './modules/providers/services/provider-auth.service.js';
|
|
@@ -228,6 +230,7 @@ export async function queryCodex(command, options = {}, ws) {
|
|
|
228
230
|
projectPath,
|
|
229
231
|
model,
|
|
230
232
|
effort,
|
|
233
|
+
images,
|
|
231
234
|
permissionMode = 'default'
|
|
232
235
|
} = options;
|
|
233
236
|
|
|
@@ -288,7 +291,12 @@ export async function queryCodex(command, options = {}, ws) {
|
|
|
288
291
|
registerSession(capturedSessionId);
|
|
289
292
|
}
|
|
290
293
|
|
|
291
|
-
|
|
294
|
+
// Execute with streaming. Turns with image attachments send structured
|
|
295
|
+
// input items so Codex reads the images from their local asset paths.
|
|
296
|
+
const turnInput = normalizeImageDescriptors(images).length > 0
|
|
297
|
+
? buildCodexInputItems(command, images, workingDirectory)
|
|
298
|
+
: command;
|
|
299
|
+
const streamedTurn = await thread.runStreamed(turnInput, {
|
|
292
300
|
signal: abortController.signal
|
|
293
301
|
});
|
|
294
302
|
|
package/server/opencode-cli.js
CHANGED
|
@@ -1,19 +1,51 @@
|
|
|
1
|
-
import { spawn } from 'child_process';
|
|
2
1
|
import fsSync from 'node:fs';
|
|
3
2
|
|
|
4
3
|
import crossSpawn from 'cross-spawn';
|
|
5
4
|
import Database from 'better-sqlite3';
|
|
6
5
|
|
|
6
|
+
import { appendImagesInputTag } from './shared/image-attachments.js';
|
|
7
7
|
import { sessionsService } from './modules/providers/services/sessions.service.js';
|
|
8
8
|
import { providerAuthService } from './modules/providers/services/provider-auth.service.js';
|
|
9
9
|
import { providerModelsService } from './modules/providers/services/provider-models.service.js';
|
|
10
10
|
import { notifyRunFailed, notifyRunStopped } from './services/notification-orchestrator.js';
|
|
11
|
-
import { createCompleteMessage, createNormalizedMessage, getOpenCodeDatabasePath } from './shared/utils.js';
|
|
11
|
+
import { createCompleteMessage, createNormalizedMessage, flattenPromptForWindowsShell, getOpenCodeDatabasePath } from './shared/utils.js';
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
// cross-spawn resolves .cmd shims/PATHEXT on Windows and delegates to
|
|
14
|
+
// child_process.spawn everywhere else.
|
|
15
|
+
const spawnFunction = crossSpawn;
|
|
14
16
|
|
|
15
17
|
const activeOpenCodeProcesses = new Map();
|
|
16
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Maps the UI permission mode onto OpenCode's non-interactive controls.
|
|
21
|
+
*
|
|
22
|
+
* OpenCode has no single "permission mode" flag; each mode uses a different
|
|
23
|
+
* lever of the `opencode run` CLI (verified against v1.17.13):
|
|
24
|
+
* - plan → the built-in read-only `plan` agent (`--agent plan`).
|
|
25
|
+
* - bypassPermissions → `--auto`, which auto-approves every permission that
|
|
26
|
+
* is not explicitly denied in the user's config.
|
|
27
|
+
* - acceptEdits → the OPENCODE_PERMISSION env var, whose JSON body the
|
|
28
|
+
* CLI merges into its permission config. Forcing
|
|
29
|
+
* `edit: allow` guarantees file edits go through while
|
|
30
|
+
* every other rule stays under the user's own config.
|
|
31
|
+
* - default → nothing; the user's opencode.json governs. In
|
|
32
|
+
* non-interactive `run` mode any `ask` rule is denied.
|
|
33
|
+
*
|
|
34
|
+
* Exported for tests only.
|
|
35
|
+
*/
|
|
36
|
+
export function resolveOpenCodePermissionOptions(permissionMode) {
|
|
37
|
+
switch (permissionMode) {
|
|
38
|
+
case 'plan':
|
|
39
|
+
return { args: ['--agent', 'plan'], env: {} };
|
|
40
|
+
case 'bypassPermissions':
|
|
41
|
+
return { args: ['--auto'], env: {} };
|
|
42
|
+
case 'acceptEdits':
|
|
43
|
+
return { args: [], env: { OPENCODE_PERMISSION: JSON.stringify({ edit: 'allow' }) } };
|
|
44
|
+
default:
|
|
45
|
+
return { args: [], env: {} };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
17
49
|
function resolveOpenCodeEffort(model, effort, modelsDefinition) {
|
|
18
50
|
const selectedModel = modelsDefinition?.OPTIONS?.find((option) => option.value === model);
|
|
19
51
|
const allowedEfforts = selectedModel?.effort?.values?.map((value) => value.value) || [];
|
|
@@ -92,7 +124,7 @@ function readOpenCodeTokenUsage(sessionId) {
|
|
|
92
124
|
|
|
93
125
|
async function spawnOpenCode(command, options = {}, ws) {
|
|
94
126
|
return new Promise((resolve, reject) => {
|
|
95
|
-
const { sessionId, projectPath, cwd, model, effort, sessionSummary } = options;
|
|
127
|
+
const { sessionId, projectPath, cwd, model, effort, sessionSummary, images, permissionMode } = options;
|
|
96
128
|
const workingDir = cwd || projectPath || process.cwd();
|
|
97
129
|
const processKey = sessionId || Date.now().toString();
|
|
98
130
|
let capturedSessionId = sessionId || null;
|
|
@@ -223,14 +255,20 @@ async function spawnOpenCode(command, options = {}, ws) {
|
|
|
223
255
|
if (resolvedEffort) {
|
|
224
256
|
args.push('--variant', resolvedEffort);
|
|
225
257
|
}
|
|
258
|
+
const permissionOptions = resolveOpenCodePermissionOptions(permissionMode);
|
|
259
|
+
args.push(...permissionOptions.args);
|
|
226
260
|
if (command && command.trim()) {
|
|
227
|
-
|
|
261
|
+
// Image attachments ride along as an <images_input> path list appended
|
|
262
|
+
// to the prompt; the session history reader strips the tag back out.
|
|
263
|
+
// opencode is a .cmd shim on Windows, so the whole argument must be
|
|
264
|
+
// newline-free or cmd.exe silently truncates it at the first newline.
|
|
265
|
+
args.push(flattenPromptForWindowsShell(appendImagesInputTag(command.trim(), images)));
|
|
228
266
|
}
|
|
229
267
|
|
|
230
268
|
opencodeProcess = spawnFunction('opencode', args, {
|
|
231
269
|
cwd: workingDir,
|
|
232
270
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
233
|
-
env: { ...process.env },
|
|
271
|
+
env: { ...process.env, ...permissionOptions.env },
|
|
234
272
|
});
|
|
235
273
|
|
|
236
274
|
activeOpenCodeProcesses.set(processKey, opencodeProcess);
|
|
@@ -4,7 +4,7 @@ import os from 'node:os';
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import test from 'node:test';
|
|
6
6
|
|
|
7
|
-
import { spawnOpenCode } from './opencode-cli.js';
|
|
7
|
+
import { resolveOpenCodePermissionOptions, spawnOpenCode } from './opencode-cli.js';
|
|
8
8
|
|
|
9
9
|
const findEnvKey = (name) =>
|
|
10
10
|
Object.keys(process.env).find((key) => key.toLowerCase() === name.toLowerCase()) || name;
|
|
@@ -14,7 +14,10 @@ async function createFakeOpenCodeExecutable(binDir) {
|
|
|
14
14
|
await writeFile(scriptPath, `
|
|
15
15
|
const capturePath = process.env.OPENCODE_ARGS_CAPTURE;
|
|
16
16
|
if (capturePath) {
|
|
17
|
-
require('node:fs').writeFileSync(capturePath, JSON.stringify(
|
|
17
|
+
require('node:fs').writeFileSync(capturePath, JSON.stringify({
|
|
18
|
+
args: process.argv.slice(2),
|
|
19
|
+
permissionEnv: process.env.OPENCODE_PERMISSION ?? null,
|
|
20
|
+
}));
|
|
18
21
|
}
|
|
19
22
|
|
|
20
23
|
const events = [
|
|
@@ -86,10 +89,116 @@ test('spawnOpenCode emits session_created before normalized live messages for ne
|
|
|
86
89
|
assert.equal(complete?.sessionId, 'open-live-1');
|
|
87
90
|
assert.equal(messages.some((message) => message.kind === 'error'), false);
|
|
88
91
|
|
|
89
|
-
const
|
|
92
|
+
const capture = JSON.parse(await readFile(argsCapturePath, 'utf8'));
|
|
93
|
+
const launchedArgs = capture.args;
|
|
90
94
|
assert.ok(Array.isArray(launchedArgs));
|
|
91
95
|
assert.deepEqual(launchedArgs.slice(0, 4), ['run', '--format', 'json', '--dir']);
|
|
92
96
|
assert.equal(launchedArgs[4], tempRoot);
|
|
97
|
+
// No permission mode requested → no permission flags and no env override.
|
|
98
|
+
assert.equal(launchedArgs.includes('--auto'), false);
|
|
99
|
+
assert.equal(launchedArgs.includes('--agent'), false);
|
|
100
|
+
assert.equal(capture.permissionEnv, null);
|
|
101
|
+
} finally {
|
|
102
|
+
if (previousPath === undefined) {
|
|
103
|
+
delete process.env[pathKey];
|
|
104
|
+
} else {
|
|
105
|
+
process.env[pathKey] = previousPath;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (previousPathExt === undefined) {
|
|
109
|
+
delete process.env[pathExtKey];
|
|
110
|
+
} else {
|
|
111
|
+
process.env[pathExtKey] = previousPathExt;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (previousArgsCapture === undefined) {
|
|
115
|
+
delete process.env.OPENCODE_ARGS_CAPTURE;
|
|
116
|
+
} else {
|
|
117
|
+
process.env.OPENCODE_ARGS_CAPTURE = previousArgsCapture;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
await rm(tempRoot, { recursive: true, force: true });
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('resolveOpenCodePermissionOptions maps UI permission modes onto OpenCode controls', () => {
|
|
125
|
+
assert.deepEqual(resolveOpenCodePermissionOptions('plan'), {
|
|
126
|
+
args: ['--agent', 'plan'],
|
|
127
|
+
env: {},
|
|
128
|
+
});
|
|
129
|
+
assert.deepEqual(resolveOpenCodePermissionOptions('bypassPermissions'), {
|
|
130
|
+
args: ['--auto'],
|
|
131
|
+
env: {},
|
|
132
|
+
});
|
|
133
|
+
assert.deepEqual(resolveOpenCodePermissionOptions('acceptEdits'), {
|
|
134
|
+
args: [],
|
|
135
|
+
env: { OPENCODE_PERMISSION: '{"edit":"allow"}' },
|
|
136
|
+
});
|
|
137
|
+
// default and anything unknown leave the user's own opencode config in charge.
|
|
138
|
+
assert.deepEqual(resolveOpenCodePermissionOptions('default'), { args: [], env: {} });
|
|
139
|
+
assert.deepEqual(resolveOpenCodePermissionOptions(undefined), { args: [], env: {} });
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('spawnOpenCode passes permission mode flags and env to the CLI', async () => {
|
|
143
|
+
const tempRoot = await mkdtemp(path.join(os.tmpdir(), 'opencode-cli-perms-'));
|
|
144
|
+
const pathKey = findEnvKey('PATH');
|
|
145
|
+
const pathExtKey = findEnvKey('PATHEXT');
|
|
146
|
+
const previousPath = process.env[pathKey];
|
|
147
|
+
const previousPathExt = process.env[pathExtKey];
|
|
148
|
+
const previousArgsCapture = process.env.OPENCODE_ARGS_CAPTURE;
|
|
149
|
+
const writer = {
|
|
150
|
+
userId: null,
|
|
151
|
+
sessionId: null,
|
|
152
|
+
send() {},
|
|
153
|
+
setSessionId(sessionId) {
|
|
154
|
+
this.sessionId = sessionId;
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
await createFakeOpenCodeExecutable(tempRoot);
|
|
160
|
+
process.env[pathKey] = `${tempRoot}${path.delimiter}${previousPath || ''}`;
|
|
161
|
+
if (process.platform === 'win32') {
|
|
162
|
+
process.env[pathExtKey] = previousPathExt?.toUpperCase().includes('.CMD')
|
|
163
|
+
? previousPathExt
|
|
164
|
+
: `.COM;.EXE;.BAT;.CMD${previousPathExt ? `;${previousPathExt}` : ''}`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const scenarios = [
|
|
168
|
+
{
|
|
169
|
+
permissionMode: 'plan',
|
|
170
|
+
expectArgs: ['--agent', 'plan'],
|
|
171
|
+
expectPermissionEnv: null,
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
permissionMode: 'bypassPermissions',
|
|
175
|
+
expectArgs: ['--auto'],
|
|
176
|
+
expectPermissionEnv: null,
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
permissionMode: 'acceptEdits',
|
|
180
|
+
expectArgs: [],
|
|
181
|
+
expectPermissionEnv: '{"edit":"allow"}',
|
|
182
|
+
},
|
|
183
|
+
];
|
|
184
|
+
|
|
185
|
+
for (const scenario of scenarios) {
|
|
186
|
+
const argsCapturePath = path.join(tempRoot, `opencode-args-${scenario.permissionMode}.json`);
|
|
187
|
+
process.env.OPENCODE_ARGS_CAPTURE = argsCapturePath;
|
|
188
|
+
|
|
189
|
+
await spawnOpenCode('Hi', { cwd: tempRoot, permissionMode: scenario.permissionMode }, writer);
|
|
190
|
+
|
|
191
|
+
const capture = JSON.parse(await readFile(argsCapturePath, 'utf8'));
|
|
192
|
+
for (const expectedArg of scenario.expectArgs) {
|
|
193
|
+
assert.ok(
|
|
194
|
+
capture.args.includes(expectedArg),
|
|
195
|
+
`${scenario.permissionMode}: expected "${expectedArg}" in ${JSON.stringify(capture.args)}`,
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
// The prompt stays the last positional argument, after any permission flags.
|
|
199
|
+
assert.equal(capture.args[capture.args.length - 1], 'Hi');
|
|
200
|
+
assert.equal(capture.permissionEnv, scenario.expectPermissionEnv);
|
|
201
|
+
}
|
|
93
202
|
} finally {
|
|
94
203
|
if (previousPath === undefined) {
|
|
95
204
|
delete process.env[pathKey];
|
package/server/routes/agent.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import express from 'express';
|
|
2
|
-
|
|
2
|
+
// cross-spawn: drop-in spawn with Windows .cmd/PATHEXT resolution.
|
|
3
|
+
import spawn from 'cross-spawn';
|
|
3
4
|
import path from 'path';
|
|
4
5
|
import os from 'os';
|
|
5
6
|
import { promises as fs } from 'fs';
|
|
@@ -8,7 +9,6 @@ import { userDb, apiKeysDb, githubTokensDb, projectsDb } from '../modules/databa
|
|
|
8
9
|
import { queryClaudeSDK } from '../claude-sdk.js';
|
|
9
10
|
import { spawnCursor } from '../cursor-cli.js';
|
|
10
11
|
import { queryCodex } from '../openai-codex.js';
|
|
11
|
-
import { spawnGemini } from '../gemini-cli.js';
|
|
12
12
|
import { spawnOpenCode } from '../opencode-cli.js';
|
|
13
13
|
import { Octokit } from '@octokit/rest';
|
|
14
14
|
import { providerModelsService } from '../modules/providers/services/provider-models.service.js';
|
|
@@ -636,7 +636,7 @@ class ResponseCollector {
|
|
|
636
636
|
* - Source for auto-generated branch names (if createBranch=true and no branchName)
|
|
637
637
|
* - Fallback for PR title if no commits are made
|
|
638
638
|
*
|
|
639
|
-
* @param {string} provider - (Optional) AI provider to use. Options: 'claude' | 'cursor' | 'codex' | '
|
|
639
|
+
* @param {string} provider - (Optional) AI provider to use. Options: 'claude' | 'cursor' | 'codex' | 'opencode'
|
|
640
640
|
* Default: 'claude'
|
|
641
641
|
*
|
|
642
642
|
* @param {boolean} stream - (Optional) Enable Server-Sent Events (SSE) streaming for real-time updates.
|
|
@@ -648,7 +648,7 @@ class ResponseCollector {
|
|
|
648
648
|
*
|
|
649
649
|
* Claude models: 'default', 'sonnet', 'opus', 'haiku', 'sonnet[1m]', 'opus[1m]', 'fable'
|
|
650
650
|
* Cursor models: 'gpt-5' (default), 'gpt-5.2', 'gpt-5.2-high', 'sonnet-4.5', 'opus-4.5',
|
|
651
|
-
* '
|
|
651
|
+
* 'composer-1', 'auto', 'gpt-5.1', 'gpt-5.1-high',
|
|
652
652
|
* 'gpt-5.1-codex', 'gpt-5.1-codex-high', 'gpt-5.1-codex-max',
|
|
653
653
|
* 'gpt-5.1-codex-max-high', 'opus-4.1', 'grok', and thinking variants
|
|
654
654
|
* Codex models: 'gpt-5.4' (default), 'gpt-5.5', 'gpt-5.4-mini'
|
|
@@ -759,7 +759,7 @@ class ResponseCollector {
|
|
|
759
759
|
* Input Validations (400 Bad Request):
|
|
760
760
|
* - Either githubUrl OR projectPath must be provided (not neither)
|
|
761
761
|
* - message must be non-empty string
|
|
762
|
-
* - provider must be 'claude', 'cursor', 'codex',
|
|
762
|
+
* - provider must be 'claude', 'cursor', 'codex', or 'opencode'
|
|
763
763
|
* - createBranch/createPR requires githubUrl OR projectPath (not neither)
|
|
764
764
|
* - branchName must pass Git naming rules (if provided)
|
|
765
765
|
*
|
|
@@ -870,8 +870,8 @@ router.post('/', validateExternalApiKey, async (req, res) => {
|
|
|
870
870
|
return res.status(400).json({ error: 'message is required' });
|
|
871
871
|
}
|
|
872
872
|
|
|
873
|
-
if (!['claude', 'cursor', 'codex', '
|
|
874
|
-
return res.status(400).json({ error: 'provider must be "claude", "cursor", "codex",
|
|
873
|
+
if (!['claude', 'cursor', 'codex', 'opencode'].includes(provider)) {
|
|
874
|
+
return res.status(400).json({ error: 'provider must be "claude", "cursor", "codex", or "opencode"' });
|
|
875
875
|
}
|
|
876
876
|
|
|
877
877
|
// Validate GitHub branch/PR creation requirements
|
|
@@ -950,7 +950,6 @@ router.post('/', validateExternalApiKey, async (req, res) => {
|
|
|
950
950
|
}
|
|
951
951
|
|
|
952
952
|
const codexModels = (await providerModelsService.getProviderModels('codex')).models;
|
|
953
|
-
const geminiModels = (await providerModelsService.getProviderModels('gemini')).models;
|
|
954
953
|
const opencodeModels = (await providerModelsService.getProviderModels('opencode')).models;
|
|
955
954
|
|
|
956
955
|
// Start the appropriate session
|
|
@@ -987,16 +986,6 @@ router.post('/', validateExternalApiKey, async (req, res) => {
|
|
|
987
986
|
effort,
|
|
988
987
|
permissionMode: 'bypassPermissions'
|
|
989
988
|
}, writer);
|
|
990
|
-
} else if (provider === 'gemini') {
|
|
991
|
-
console.log('✨ Starting Gemini CLI session');
|
|
992
|
-
|
|
993
|
-
await spawnGemini(message.trim(), {
|
|
994
|
-
projectPath: finalProjectPath,
|
|
995
|
-
cwd: finalProjectPath,
|
|
996
|
-
sessionId: sessionId || null,
|
|
997
|
-
model: model || geminiModels.DEFAULT,
|
|
998
|
-
skipPermissions: true // CLI mode bypasses permissions
|
|
999
|
-
}, writer);
|
|
1000
989
|
} else if (provider === 'opencode') {
|
|
1001
990
|
console.log('Starting OpenCode CLI session');
|
|
1002
991
|
|
|
@@ -1005,7 +994,8 @@ router.post('/', validateExternalApiKey, async (req, res) => {
|
|
|
1005
994
|
cwd: finalProjectPath,
|
|
1006
995
|
sessionId: sessionId || null,
|
|
1007
996
|
model: model || opencodeModels.DEFAULT,
|
|
1008
|
-
effort
|
|
997
|
+
effort,
|
|
998
|
+
permissionMode: 'bypassPermissions' // Agent runs are non-interactive, like the other providers above
|
|
1009
999
|
}, writer);
|
|
1010
1000
|
}
|
|
1011
1001
|
|
|
@@ -15,13 +15,12 @@ const APP_ROOT = findAppRoot(__dirname);
|
|
|
15
15
|
|
|
16
16
|
const router = express.Router();
|
|
17
17
|
|
|
18
|
-
const MODEL_PROVIDERS = ["claude", "cursor", "codex", "
|
|
18
|
+
const MODEL_PROVIDERS = ["claude", "cursor", "codex", "opencode"];
|
|
19
19
|
|
|
20
20
|
const MODEL_PROVIDER_LABELS = {
|
|
21
21
|
claude: "Claude",
|
|
22
22
|
cursor: "Cursor",
|
|
23
23
|
codex: "Codex",
|
|
24
|
-
gemini: "Gemini",
|
|
25
24
|
opencode: "OpenCode",
|
|
26
25
|
};
|
|
27
26
|
|