@gricha/perry 0.3.12 → 0.3.14
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/dist/agent/router.js +142 -40
- package/dist/agent/web/assets/index-ChUt0xgV.js +104 -0
- package/dist/agent/web/index.html +1 -1
- package/dist/agents/__tests__/opencode.test.js +37 -1
- package/dist/agents/sync/opencode.js +8 -1
- package/dist/perry-worker +0 -0
- package/dist/session-manager/adapters/opencode.js +102 -13
- package/dist/session-manager/bun-handler.js +1 -1
- package/dist/session-manager/manager.js +35 -12
- package/dist/sessions/registry.js +19 -0
- package/dist/shared/constants.js +1 -0
- package/package.json +1 -1
- package/dist/agent/web/assets/index-hNfXv8YX.js +0 -104
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
7
|
<title>Perry</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-ChUt0xgV.js"></script>
|
|
9
9
|
<link rel="stylesheet" crossorigin href="/assets/index-B-qVBi35.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
2
|
import { opencodeSync } from '../sync/opencode';
|
|
3
|
+
import { DEFAULT_OPENCODE_MODEL } from '../../shared/constants';
|
|
3
4
|
function createMockContext(overrides = {}) {
|
|
4
5
|
return {
|
|
5
6
|
containerName: 'test-container',
|
|
@@ -62,7 +63,42 @@ describe('opencodeSync', () => {
|
|
|
62
63
|
expect(configs[0].permissions).toBe('600');
|
|
63
64
|
const parsed = JSON.parse(configs[0].content);
|
|
64
65
|
expect(parsed.provider.opencode.options.apiKey).toBe('test-token-123');
|
|
65
|
-
expect(parsed.model).toBe(
|
|
66
|
+
expect(parsed.model).toBe(DEFAULT_OPENCODE_MODEL);
|
|
67
|
+
});
|
|
68
|
+
it('uses host model when configured model missing', async () => {
|
|
69
|
+
const hostConfig = { model: 'opencode/claude-opus-4-5' };
|
|
70
|
+
const context = createMockContext({
|
|
71
|
+
agentConfig: {
|
|
72
|
+
port: 7777,
|
|
73
|
+
credentials: { env: {}, files: {} },
|
|
74
|
+
scripts: {},
|
|
75
|
+
agents: { opencode: { zen_token: 'test-token' } },
|
|
76
|
+
},
|
|
77
|
+
readHostFile: async (path) => path === '~/.config/opencode/opencode.json' ? JSON.stringify(hostConfig) : null,
|
|
78
|
+
});
|
|
79
|
+
const configs = await opencodeSync.getGeneratedConfigs(context);
|
|
80
|
+
const parsed = JSON.parse(configs[0].content);
|
|
81
|
+
expect(parsed.model).toBe('opencode/claude-opus-4-5');
|
|
82
|
+
});
|
|
83
|
+
it('prefers configured model over host model', async () => {
|
|
84
|
+
const hostConfig = { model: 'opencode/claude-sonnet-4' };
|
|
85
|
+
const context = createMockContext({
|
|
86
|
+
agentConfig: {
|
|
87
|
+
port: 7777,
|
|
88
|
+
credentials: { env: {}, files: {} },
|
|
89
|
+
scripts: {},
|
|
90
|
+
agents: {
|
|
91
|
+
opencode: {
|
|
92
|
+
zen_token: 'test-token',
|
|
93
|
+
model: 'opencode/claude-opus-4-5',
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
readHostFile: async (path) => path === '~/.config/opencode/opencode.json' ? JSON.stringify(hostConfig) : null,
|
|
98
|
+
});
|
|
99
|
+
const configs = await opencodeSync.getGeneratedConfigs(context);
|
|
100
|
+
const parsed = JSON.parse(configs[0].content);
|
|
101
|
+
expect(parsed.model).toBe('opencode/claude-opus-4-5');
|
|
66
102
|
});
|
|
67
103
|
it('does not include mcp when host has none', async () => {
|
|
68
104
|
const context = createMockContext({
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { DEFAULT_OPENCODE_MODEL } from '../../shared/constants';
|
|
1
2
|
export const opencodeSync = {
|
|
2
3
|
getRequiredDirs() {
|
|
3
4
|
return ['/home/workspace/.config/opencode'];
|
|
@@ -15,17 +16,23 @@ export const opencodeSync = {
|
|
|
15
16
|
}
|
|
16
17
|
const hostConfigContent = await context.readHostFile('~/.config/opencode/opencode.json');
|
|
17
18
|
let mcpConfig = {};
|
|
19
|
+
let hostModel;
|
|
18
20
|
if (hostConfigContent) {
|
|
19
21
|
try {
|
|
20
22
|
const parsed = JSON.parse(hostConfigContent);
|
|
21
23
|
if (parsed.mcp && typeof parsed.mcp === 'object') {
|
|
22
24
|
mcpConfig = parsed.mcp;
|
|
23
25
|
}
|
|
26
|
+
if (typeof parsed.model === 'string' && parsed.model.trim().length > 0) {
|
|
27
|
+
hostModel = parsed.model.trim();
|
|
28
|
+
}
|
|
24
29
|
}
|
|
25
30
|
catch {
|
|
26
31
|
// Invalid JSON, ignore
|
|
27
32
|
}
|
|
28
33
|
}
|
|
34
|
+
const configuredModel = context.agentConfig.agents?.opencode?.model?.trim();
|
|
35
|
+
const model = configuredModel || hostModel || DEFAULT_OPENCODE_MODEL;
|
|
29
36
|
const config = {
|
|
30
37
|
provider: {
|
|
31
38
|
opencode: {
|
|
@@ -34,7 +41,7 @@ export const opencodeSync = {
|
|
|
34
41
|
},
|
|
35
42
|
},
|
|
36
43
|
},
|
|
37
|
-
model
|
|
44
|
+
model,
|
|
38
45
|
};
|
|
39
46
|
if (Object.keys(mcpConfig).length > 0) {
|
|
40
47
|
config.mcp = mcpConfig;
|
package/dist/perry-worker
CHANGED
|
Binary file
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { execInContainer } from '../../docker';
|
|
2
2
|
const MESSAGE_TIMEOUT_MS = 30000;
|
|
3
|
-
const SSE_TIMEOUT_MS =
|
|
3
|
+
const SSE_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes for long-running operations
|
|
4
4
|
const serverPorts = new Map();
|
|
5
5
|
const serverStarting = new Map();
|
|
6
6
|
let hostServerPort = null;
|
|
@@ -13,9 +13,39 @@ async function findAvailablePort(containerName) {
|
|
|
13
13
|
});
|
|
14
14
|
return parseInt(result.stdout.trim(), 10);
|
|
15
15
|
}
|
|
16
|
+
async function findExistingServer(containerName) {
|
|
17
|
+
try {
|
|
18
|
+
const result = await execInContainer(containerName, ['sh', '-c', 'pgrep -a -f "opencode serve" | grep -oP "\\\\--port \\\\K[0-9]+"'], { user: 'workspace' });
|
|
19
|
+
const ports = result.stdout
|
|
20
|
+
.trim()
|
|
21
|
+
.split('\n')
|
|
22
|
+
.filter(Boolean)
|
|
23
|
+
.map((p) => parseInt(p, 10))
|
|
24
|
+
.filter((p) => !isNaN(p));
|
|
25
|
+
for (const port of ports) {
|
|
26
|
+
if (await isServerRunning(containerName, port)) {
|
|
27
|
+
return port;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// No existing server
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
16
36
|
async function isServerRunning(containerName, port) {
|
|
17
37
|
try {
|
|
18
|
-
const result = await execInContainer(containerName, [
|
|
38
|
+
const result = await execInContainer(containerName, [
|
|
39
|
+
'curl',
|
|
40
|
+
'-s',
|
|
41
|
+
'-o',
|
|
42
|
+
'/dev/null',
|
|
43
|
+
'-w',
|
|
44
|
+
'%{http_code}',
|
|
45
|
+
'--max-time',
|
|
46
|
+
'3',
|
|
47
|
+
`http://localhost:${port}/session`,
|
|
48
|
+
], { user: 'workspace' });
|
|
19
49
|
return result.stdout.trim() === '200';
|
|
20
50
|
}
|
|
21
51
|
catch {
|
|
@@ -34,8 +64,14 @@ async function getServerLogs(containerName) {
|
|
|
34
64
|
}
|
|
35
65
|
}
|
|
36
66
|
async function startServer(containerName) {
|
|
37
|
-
const
|
|
38
|
-
if (
|
|
67
|
+
const cached = serverPorts.get(containerName);
|
|
68
|
+
if (cached && (await isServerRunning(containerName, cached))) {
|
|
69
|
+
return cached;
|
|
70
|
+
}
|
|
71
|
+
const existing = await findExistingServer(containerName);
|
|
72
|
+
if (existing) {
|
|
73
|
+
console.log(`[opencode] Found existing server on port ${existing} in ${containerName}`);
|
|
74
|
+
serverPorts.set(containerName, existing);
|
|
39
75
|
return existing;
|
|
40
76
|
}
|
|
41
77
|
const starting = serverStarting.get(containerName);
|
|
@@ -173,9 +209,17 @@ export class OpenCodeAdapter {
|
|
|
173
209
|
}
|
|
174
210
|
const baseUrl = `http://localhost:${this.port}`;
|
|
175
211
|
try {
|
|
212
|
+
if (this.agentSessionId) {
|
|
213
|
+
const exists = await this.sessionExists(baseUrl, this.agentSessionId);
|
|
214
|
+
if (!exists) {
|
|
215
|
+
console.log(`[opencode] Session ${this.agentSessionId} not found on server, creating new`);
|
|
216
|
+
this.agentSessionId = undefined;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
176
219
|
if (!this.agentSessionId) {
|
|
177
220
|
this.agentSessionId = await this.createSession(baseUrl);
|
|
178
221
|
this.emit({ type: 'system', content: `Session: ${this.agentSessionId.slice(0, 8)}...` });
|
|
222
|
+
this.statusCallback?.(this.status);
|
|
179
223
|
}
|
|
180
224
|
this.setStatus('running');
|
|
181
225
|
this.emit({ type: 'system', content: 'Processing...' });
|
|
@@ -189,10 +233,35 @@ export class OpenCodeAdapter {
|
|
|
189
233
|
this.currentMessageId = undefined;
|
|
190
234
|
this.setStatus('error');
|
|
191
235
|
this.emitError(err);
|
|
192
|
-
this.emit({ type: 'error', content: err.message });
|
|
193
236
|
throw err;
|
|
194
237
|
}
|
|
195
238
|
}
|
|
239
|
+
async sessionExists(baseUrl, sessionId) {
|
|
240
|
+
try {
|
|
241
|
+
if (this.isHost) {
|
|
242
|
+
const response = await fetch(`${baseUrl}/session/${sessionId}`, {
|
|
243
|
+
method: 'GET',
|
|
244
|
+
signal: AbortSignal.timeout(5000),
|
|
245
|
+
});
|
|
246
|
+
return response.ok;
|
|
247
|
+
}
|
|
248
|
+
const result = await execInContainer(this.containerName, [
|
|
249
|
+
'curl',
|
|
250
|
+
'-s',
|
|
251
|
+
'-o',
|
|
252
|
+
'/dev/null',
|
|
253
|
+
'-w',
|
|
254
|
+
'%{http_code}',
|
|
255
|
+
'--max-time',
|
|
256
|
+
'5',
|
|
257
|
+
`${baseUrl}/session/${sessionId}`,
|
|
258
|
+
], { user: 'workspace' });
|
|
259
|
+
return result.stdout.trim() === '200';
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
196
265
|
async createSession(baseUrl) {
|
|
197
266
|
const payload = this.model ? { model: this.model } : {};
|
|
198
267
|
if (this.isHost) {
|
|
@@ -229,17 +298,20 @@ export class OpenCodeAdapter {
|
|
|
229
298
|
return session.id;
|
|
230
299
|
}
|
|
231
300
|
async sendAndStream(baseUrl, message) {
|
|
232
|
-
|
|
301
|
+
let sseError = null;
|
|
302
|
+
const sseReady = this.startSSEStream().catch((err) => {
|
|
303
|
+
sseError = err;
|
|
304
|
+
});
|
|
233
305
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
234
306
|
const payload = { parts: [{ type: 'text', text: message }] };
|
|
235
307
|
if (this.isHost) {
|
|
236
|
-
const response = await fetch(`${baseUrl}/session/${this.agentSessionId}/
|
|
308
|
+
const response = await fetch(`${baseUrl}/session/${this.agentSessionId}/prompt_async`, {
|
|
237
309
|
method: 'POST',
|
|
238
310
|
headers: { 'Content-Type': 'application/json' },
|
|
239
311
|
body: JSON.stringify(payload),
|
|
240
312
|
signal: AbortSignal.timeout(MESSAGE_TIMEOUT_MS),
|
|
241
313
|
});
|
|
242
|
-
if (!response.ok) {
|
|
314
|
+
if (!response.ok && response.status !== 204) {
|
|
243
315
|
throw new Error(`Failed to send message: ${response.statusText}`);
|
|
244
316
|
}
|
|
245
317
|
}
|
|
@@ -247,22 +319,29 @@ export class OpenCodeAdapter {
|
|
|
247
319
|
const result = await execInContainer(this.containerName, [
|
|
248
320
|
'curl',
|
|
249
321
|
'-s',
|
|
250
|
-
'-
|
|
322
|
+
'-w',
|
|
323
|
+
'%{http_code}',
|
|
324
|
+
'-o',
|
|
325
|
+
'/dev/null',
|
|
251
326
|
'--max-time',
|
|
252
327
|
String(MESSAGE_TIMEOUT_MS / 1000),
|
|
253
328
|
'-X',
|
|
254
329
|
'POST',
|
|
255
|
-
`${baseUrl}/session/${this.agentSessionId}/
|
|
330
|
+
`${baseUrl}/session/${this.agentSessionId}/prompt_async`,
|
|
256
331
|
'-H',
|
|
257
332
|
'Content-Type: application/json',
|
|
258
333
|
'-d',
|
|
259
334
|
JSON.stringify(payload),
|
|
260
335
|
], { user: 'workspace' });
|
|
261
|
-
|
|
262
|
-
|
|
336
|
+
const httpCode = result.stdout.trim();
|
|
337
|
+
if (result.exitCode !== 0 || (httpCode !== '204' && httpCode !== '200')) {
|
|
338
|
+
throw new Error(`Failed to send message: ${result.stderr || `HTTP ${httpCode}`}`);
|
|
263
339
|
}
|
|
264
340
|
}
|
|
265
341
|
await sseReady;
|
|
342
|
+
if (sseError) {
|
|
343
|
+
throw sseError;
|
|
344
|
+
}
|
|
266
345
|
}
|
|
267
346
|
startSSEStream() {
|
|
268
347
|
return new Promise((resolve, reject) => {
|
|
@@ -284,6 +363,7 @@ export class OpenCodeAdapter {
|
|
|
284
363
|
this.sseProcess = proc;
|
|
285
364
|
const decoder = new TextDecoder();
|
|
286
365
|
let buffer = '';
|
|
366
|
+
let eventCount = 0;
|
|
287
367
|
const finish = () => {
|
|
288
368
|
if (!resolved) {
|
|
289
369
|
resolved = true;
|
|
@@ -316,7 +396,13 @@ export class OpenCodeAdapter {
|
|
|
316
396
|
continue;
|
|
317
397
|
try {
|
|
318
398
|
const event = JSON.parse(data);
|
|
399
|
+
eventCount++;
|
|
319
400
|
if (event.type === 'session.idle') {
|
|
401
|
+
const idleSessionId = event.properties?.sessionID;
|
|
402
|
+
if (!idleSessionId || idleSessionId !== this.agentSessionId) {
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
console.log(`[opencode] SSE received session.idle after ${eventCount} events`);
|
|
320
406
|
receivedIdle = true;
|
|
321
407
|
clearTimeout(timeout);
|
|
322
408
|
proc.kill();
|
|
@@ -325,6 +411,9 @@ export class OpenCodeAdapter {
|
|
|
325
411
|
}
|
|
326
412
|
if (event.type === 'message.part.updated' && event.properties.part) {
|
|
327
413
|
const part = event.properties.part;
|
|
414
|
+
if (!part.sessionID || part.sessionID !== this.agentSessionId) {
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
328
417
|
if (part.messageID) {
|
|
329
418
|
this.currentMessageId = part.messageID;
|
|
330
419
|
}
|
|
@@ -375,7 +464,7 @@ export class OpenCodeAdapter {
|
|
|
375
464
|
}
|
|
376
465
|
else if (!resolved) {
|
|
377
466
|
resolved = true;
|
|
378
|
-
reject(new Error(
|
|
467
|
+
reject(new Error(`SSE stream ended unexpectedly without session.idle (received ${eventCount} events)`));
|
|
379
468
|
}
|
|
380
469
|
})().catch((err) => {
|
|
381
470
|
clearTimeout(timeout);
|
|
@@ -90,7 +90,7 @@ export class LiveChatHandler {
|
|
|
90
90
|
const agentType = message.agentType || this.agentType;
|
|
91
91
|
if (message.sessionId) {
|
|
92
92
|
// Look up by internal sessionId or agentSessionId (Claude session ID)
|
|
93
|
-
const found = sessionManager.findSession(message.sessionId);
|
|
93
|
+
const found = await sessionManager.findSession(message.sessionId);
|
|
94
94
|
if (found) {
|
|
95
95
|
connection.sessionId = found.sessionId;
|
|
96
96
|
const sendFn = (msg) => {
|
|
@@ -65,15 +65,6 @@ export class SessionManager {
|
|
|
65
65
|
});
|
|
66
66
|
const isHost = options.workspaceName === HOST_WORKSPACE_NAME;
|
|
67
67
|
const containerName = isHost ? undefined : getContainerName(options.workspaceName);
|
|
68
|
-
await adapter.start({
|
|
69
|
-
workspaceName: options.workspaceName,
|
|
70
|
-
containerName,
|
|
71
|
-
agentSessionId: options.agentSessionId,
|
|
72
|
-
model: options.model,
|
|
73
|
-
projectPath: options.projectPath,
|
|
74
|
-
isHost,
|
|
75
|
-
});
|
|
76
|
-
this.sessions.set(sessionId, session);
|
|
77
68
|
if (this.stateDir) {
|
|
78
69
|
await registry.createSession(this.stateDir, {
|
|
79
70
|
perrySessionId: sessionId,
|
|
@@ -83,6 +74,15 @@ export class SessionManager {
|
|
|
83
74
|
projectPath: options.projectPath ?? null,
|
|
84
75
|
});
|
|
85
76
|
}
|
|
77
|
+
await adapter.start({
|
|
78
|
+
workspaceName: options.workspaceName,
|
|
79
|
+
containerName,
|
|
80
|
+
agentSessionId: options.agentSessionId,
|
|
81
|
+
model: options.model,
|
|
82
|
+
projectPath: options.projectPath,
|
|
83
|
+
isHost,
|
|
84
|
+
});
|
|
85
|
+
this.sessions.set(sessionId, session);
|
|
86
86
|
return sessionId;
|
|
87
87
|
}
|
|
88
88
|
handleAdapterMessage(sessionId, message) {
|
|
@@ -241,18 +241,41 @@ export class SessionManager {
|
|
|
241
241
|
const session = this.sessions.get(sessionId);
|
|
242
242
|
return session?.info ?? null;
|
|
243
243
|
}
|
|
244
|
-
findSession(id) {
|
|
245
|
-
// First try direct lookup by internal sessionId
|
|
244
|
+
async findSession(id) {
|
|
245
|
+
// First try direct lookup by internal sessionId (in-memory cache)
|
|
246
246
|
const direct = this.sessions.get(id);
|
|
247
247
|
if (direct) {
|
|
248
248
|
return { sessionId: id, info: direct.info };
|
|
249
249
|
}
|
|
250
|
-
// Then search by agentSessionId
|
|
250
|
+
// Then search by agentSessionId in memory
|
|
251
251
|
for (const [sessionId, session] of this.sessions) {
|
|
252
252
|
if (session.info.agentSessionId === id) {
|
|
253
253
|
return { sessionId, info: session.info };
|
|
254
254
|
}
|
|
255
255
|
}
|
|
256
|
+
// Not in memory - check disk registry
|
|
257
|
+
if (this.stateDir) {
|
|
258
|
+
// Try lookup by perrySessionId first
|
|
259
|
+
let record = await registry.getSession(this.stateDir, id);
|
|
260
|
+
// Then try by agentSessionId
|
|
261
|
+
if (!record) {
|
|
262
|
+
record = await registry.findByAgentSessionId(this.stateDir, id);
|
|
263
|
+
}
|
|
264
|
+
// Found on disk - restore the session
|
|
265
|
+
if (record) {
|
|
266
|
+
const restoredId = await this.startSession({
|
|
267
|
+
sessionId: record.perrySessionId,
|
|
268
|
+
workspaceName: record.workspaceName,
|
|
269
|
+
agentType: record.agentType,
|
|
270
|
+
agentSessionId: record.agentSessionId ?? undefined,
|
|
271
|
+
projectPath: record.projectPath ?? undefined,
|
|
272
|
+
});
|
|
273
|
+
const restored = this.sessions.get(restoredId);
|
|
274
|
+
if (restored) {
|
|
275
|
+
return { sessionId: restoredId, info: restored.info };
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
256
279
|
return null;
|
|
257
280
|
}
|
|
258
281
|
getSessionStatus(sessionId) {
|
|
@@ -98,6 +98,25 @@ export async function getSessionsForWorkspace(stateDir, workspaceName) {
|
|
|
98
98
|
.filter((record) => record.workspaceName === workspaceName)
|
|
99
99
|
.sort((a, b) => new Date(b.lastActivity).getTime() - new Date(a.lastActivity).getTime());
|
|
100
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* Get a specific session by perrySessionId.
|
|
103
|
+
*/
|
|
104
|
+
export async function getSession(stateDir, perrySessionId) {
|
|
105
|
+
const registry = await loadRegistry(stateDir);
|
|
106
|
+
return registry.sessions[perrySessionId] ?? null;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Find a session by agentSessionId.
|
|
110
|
+
*/
|
|
111
|
+
export async function findByAgentSessionId(stateDir, agentSessionId) {
|
|
112
|
+
const registry = await loadRegistry(stateDir);
|
|
113
|
+
for (const record of Object.values(registry.sessions)) {
|
|
114
|
+
if (record.agentSessionId === agentSessionId) {
|
|
115
|
+
return record;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
101
120
|
/**
|
|
102
121
|
* Import an external session (discovered from agent storage).
|
|
103
122
|
* Creates a Perry session record for a session that wasn't started through Perry.
|
package/dist/shared/constants.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export const DEFAULT_AGENT_PORT = 7391;
|
|
2
2
|
export const DEFAULT_CLAUDE_MODEL = 'sonnet';
|
|
3
|
+
export const DEFAULT_OPENCODE_MODEL = 'opencode/claude-sonnet-4';
|
|
3
4
|
export const SSH_PORT_RANGE_START = 2200;
|
|
4
5
|
export const SSH_PORT_RANGE_END = 2400;
|
|
5
6
|
export const WORKSPACE_IMAGE_LOCAL = 'perry:latest';
|