@gricha/perry 0.2.2 → 0.2.4
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 +11 -0
- package/dist/agent/file-watcher.js +134 -0
- package/dist/agent/router.js +48 -2
- package/dist/agent/run.js +44 -4
- package/dist/agent/web/assets/index-BmFYrCoX.css +1 -0
- package/dist/agent/web/assets/index-IavvQP8G.js +104 -0
- package/dist/agent/web/index.html +2 -2
- package/dist/agents/__tests__/claude-code.test.js +125 -0
- package/dist/agents/__tests__/codex.test.js +64 -0
- package/dist/agents/__tests__/opencode.test.js +130 -0
- package/dist/agents/__tests__/sync.test.js +272 -0
- package/dist/agents/index.js +177 -0
- package/dist/agents/sync/claude-code.js +84 -0
- package/dist/agents/sync/codex.js +29 -0
- package/dist/agents/sync/copier.js +89 -0
- package/dist/agents/sync/opencode.js +51 -0
- package/dist/agents/sync/types.js +1 -0
- package/dist/agents/types.js +1 -0
- package/dist/chat/base-chat-websocket.js +2 -2
- package/dist/chat/base-claude-session.js +169 -0
- package/dist/chat/base-opencode-session.js +181 -0
- package/dist/chat/handler.js +14 -157
- package/dist/chat/host-handler.js +13 -142
- package/dist/chat/host-opencode-handler.js +28 -187
- package/dist/chat/opencode-handler.js +38 -197
- package/dist/chat/opencode-websocket.js +1 -1
- package/dist/chat/types.js +1 -0
- package/dist/chat/websocket.js +2 -2
- package/dist/client/api.js +25 -0
- package/dist/config/loader.js +20 -2
- package/dist/docker/eager-pull.js +19 -3
- package/dist/docker/index.js +28 -1
- package/dist/index.js +83 -12
- package/dist/perry-worker +0 -0
- package/dist/shared/constants.js +1 -0
- package/dist/shared/types.js +0 -1
- package/dist/terminal/websocket.js +1 -1
- package/dist/workspace/manager.js +178 -115
- package/package.json +4 -3
- package/dist/agent/web/assets/index-DIOWcVH-.css +0 -1
- package/dist/agent/web/assets/index-DN_QW9sL.js +0 -104
|
@@ -1,21 +1,37 @@
|
|
|
1
1
|
import { homedir } from 'os';
|
|
2
2
|
import { promises as fs } from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
import { BaseOpencodeSession } from './base-opencode-session';
|
|
5
|
+
export class HostOpencodeSession extends BaseOpencodeSession {
|
|
6
6
|
workDir;
|
|
7
|
-
sessionId;
|
|
8
|
-
model;
|
|
9
|
-
sessionModel;
|
|
10
|
-
onMessage;
|
|
11
|
-
buffer = '';
|
|
12
|
-
historyLoaded = false;
|
|
13
7
|
constructor(options, onMessage) {
|
|
8
|
+
super(options.sessionId, options.model, onMessage);
|
|
14
9
|
this.workDir = options.workDir || homedir();
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
10
|
+
}
|
|
11
|
+
getLogPrefix() {
|
|
12
|
+
return 'host-opencode';
|
|
13
|
+
}
|
|
14
|
+
getNoOutputErrorMessage() {
|
|
15
|
+
return 'No response from OpenCode. Check if OpenCode is installed and configured.';
|
|
16
|
+
}
|
|
17
|
+
getSpawnConfig(userMessage) {
|
|
18
|
+
const args = ['stdbuf', '-oL', 'opencode', 'run', '--format', 'json'];
|
|
19
|
+
if (this.sessionId) {
|
|
20
|
+
args.push('--session', this.sessionId);
|
|
21
|
+
}
|
|
22
|
+
if (this.model) {
|
|
23
|
+
args.push('--model', this.model);
|
|
24
|
+
}
|
|
25
|
+
args.push(userMessage);
|
|
26
|
+
return {
|
|
27
|
+
command: args,
|
|
28
|
+
options: {
|
|
29
|
+
cwd: this.workDir,
|
|
30
|
+
env: {
|
|
31
|
+
...process.env,
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
};
|
|
19
35
|
}
|
|
20
36
|
async loadHistory() {
|
|
21
37
|
if (this.historyLoaded || !this.sessionId) {
|
|
@@ -122,181 +138,6 @@ export class HostOpencodeSession {
|
|
|
122
138
|
console.error('[host-opencode] Failed to load history:', err);
|
|
123
139
|
}
|
|
124
140
|
}
|
|
125
|
-
async sendMessage(userMessage) {
|
|
126
|
-
if (this.sessionId && !this.historyLoaded) {
|
|
127
|
-
await this.loadHistory();
|
|
128
|
-
}
|
|
129
|
-
const args = ['-oL', 'opencode', 'run', '--format', 'json'];
|
|
130
|
-
if (this.sessionId) {
|
|
131
|
-
args.push('--session', this.sessionId);
|
|
132
|
-
}
|
|
133
|
-
if (this.model) {
|
|
134
|
-
args.push('--model', this.model);
|
|
135
|
-
}
|
|
136
|
-
args.push(userMessage);
|
|
137
|
-
console.log('[host-opencode] Running: stdbuf', args.join(' '));
|
|
138
|
-
this.onMessage({
|
|
139
|
-
type: 'system',
|
|
140
|
-
content: 'Processing your message...',
|
|
141
|
-
timestamp: new Date().toISOString(),
|
|
142
|
-
});
|
|
143
|
-
try {
|
|
144
|
-
const proc = Bun.spawn(['stdbuf', ...args], {
|
|
145
|
-
cwd: this.workDir,
|
|
146
|
-
stdin: 'ignore',
|
|
147
|
-
stdout: 'pipe',
|
|
148
|
-
stderr: 'pipe',
|
|
149
|
-
env: {
|
|
150
|
-
...process.env,
|
|
151
|
-
},
|
|
152
|
-
});
|
|
153
|
-
this.process = proc;
|
|
154
|
-
if (!proc.stdout || !proc.stderr) {
|
|
155
|
-
throw new Error('Failed to get process streams');
|
|
156
|
-
}
|
|
157
|
-
console.log('[host-opencode] Process spawned, waiting for output...');
|
|
158
|
-
const stderrPromise = new Response(proc.stderr).text();
|
|
159
|
-
const decoder = new TextDecoder();
|
|
160
|
-
let receivedAnyOutput = false;
|
|
161
|
-
for await (const chunk of proc.stdout) {
|
|
162
|
-
const text = decoder.decode(chunk);
|
|
163
|
-
console.log('[host-opencode] Received chunk:', text.length, 'bytes');
|
|
164
|
-
receivedAnyOutput = true;
|
|
165
|
-
this.buffer += text;
|
|
166
|
-
this.processBuffer();
|
|
167
|
-
}
|
|
168
|
-
const exitCode = await proc.exited;
|
|
169
|
-
console.log('[host-opencode] Process exited with code:', exitCode, 'receivedOutput:', receivedAnyOutput);
|
|
170
|
-
const stderrText = await stderrPromise;
|
|
171
|
-
if (stderrText) {
|
|
172
|
-
console.error('[host-opencode] stderr:', stderrText);
|
|
173
|
-
}
|
|
174
|
-
if (exitCode !== 0) {
|
|
175
|
-
this.onMessage({
|
|
176
|
-
type: 'error',
|
|
177
|
-
content: stderrText || `OpenCode exited with code ${exitCode}`,
|
|
178
|
-
timestamp: new Date().toISOString(),
|
|
179
|
-
});
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
if (!receivedAnyOutput) {
|
|
183
|
-
this.onMessage({
|
|
184
|
-
type: 'error',
|
|
185
|
-
content: 'No response from OpenCode. Check if OpenCode is installed and configured.',
|
|
186
|
-
timestamp: new Date().toISOString(),
|
|
187
|
-
});
|
|
188
|
-
return;
|
|
189
|
-
}
|
|
190
|
-
this.onMessage({
|
|
191
|
-
type: 'done',
|
|
192
|
-
content: 'Response complete',
|
|
193
|
-
timestamp: new Date().toISOString(),
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
catch (err) {
|
|
197
|
-
console.error('[host-opencode] Error:', err);
|
|
198
|
-
this.onMessage({
|
|
199
|
-
type: 'error',
|
|
200
|
-
content: err.message,
|
|
201
|
-
timestamp: new Date().toISOString(),
|
|
202
|
-
});
|
|
203
|
-
}
|
|
204
|
-
finally {
|
|
205
|
-
this.process = null;
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
processBuffer() {
|
|
209
|
-
const lines = this.buffer.split('\n');
|
|
210
|
-
this.buffer = lines.pop() || '';
|
|
211
|
-
for (const line of lines) {
|
|
212
|
-
if (!line.trim())
|
|
213
|
-
continue;
|
|
214
|
-
try {
|
|
215
|
-
const event = JSON.parse(line);
|
|
216
|
-
this.handleStreamEvent(event);
|
|
217
|
-
}
|
|
218
|
-
catch {
|
|
219
|
-
console.error('[host-opencode] Failed to parse:', line);
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
handleStreamEvent(event) {
|
|
224
|
-
const timestamp = new Date().toISOString();
|
|
225
|
-
if (event.type === 'step_start' && event.sessionID) {
|
|
226
|
-
if (!this.sessionId) {
|
|
227
|
-
this.sessionId = event.sessionID;
|
|
228
|
-
this.sessionModel = this.model;
|
|
229
|
-
this.historyLoaded = true;
|
|
230
|
-
this.onMessage({
|
|
231
|
-
type: 'system',
|
|
232
|
-
content: `Session started ${this.sessionId}`,
|
|
233
|
-
timestamp,
|
|
234
|
-
});
|
|
235
|
-
}
|
|
236
|
-
return;
|
|
237
|
-
}
|
|
238
|
-
if (event.type === 'text' && event.part?.text) {
|
|
239
|
-
this.onMessage({
|
|
240
|
-
type: 'assistant',
|
|
241
|
-
content: event.part.text,
|
|
242
|
-
timestamp,
|
|
243
|
-
});
|
|
244
|
-
return;
|
|
245
|
-
}
|
|
246
|
-
if (event.type === 'tool_use' && event.part) {
|
|
247
|
-
const toolName = event.part.tool || 'unknown';
|
|
248
|
-
const toolId = event.part.callID || event.part.id;
|
|
249
|
-
const input = event.part.state?.input;
|
|
250
|
-
const output = event.part.state?.output;
|
|
251
|
-
const title = event.part.state?.title || input?.description || toolName;
|
|
252
|
-
console.log('[host-opencode] Tool use:', toolName, title);
|
|
253
|
-
this.onMessage({
|
|
254
|
-
type: 'tool_use',
|
|
255
|
-
content: JSON.stringify(input, null, 2),
|
|
256
|
-
toolName: title || toolName,
|
|
257
|
-
toolId,
|
|
258
|
-
timestamp,
|
|
259
|
-
});
|
|
260
|
-
if (output) {
|
|
261
|
-
this.onMessage({
|
|
262
|
-
type: 'tool_result',
|
|
263
|
-
content: output,
|
|
264
|
-
toolName,
|
|
265
|
-
toolId,
|
|
266
|
-
timestamp,
|
|
267
|
-
});
|
|
268
|
-
}
|
|
269
|
-
return;
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
async interrupt() {
|
|
273
|
-
if (this.process) {
|
|
274
|
-
this.process.kill();
|
|
275
|
-
this.process = null;
|
|
276
|
-
this.onMessage({
|
|
277
|
-
type: 'system',
|
|
278
|
-
content: 'Chat interrupted',
|
|
279
|
-
timestamp: new Date().toISOString(),
|
|
280
|
-
});
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
setModel(model) {
|
|
284
|
-
if (this.model !== model) {
|
|
285
|
-
this.model = model;
|
|
286
|
-
if (this.sessionModel !== model) {
|
|
287
|
-
this.sessionId = undefined;
|
|
288
|
-
this.historyLoaded = false;
|
|
289
|
-
this.onMessage({
|
|
290
|
-
type: 'system',
|
|
291
|
-
content: `Switching to model: ${model}`,
|
|
292
|
-
timestamp: new Date().toISOString(),
|
|
293
|
-
});
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
getSessionId() {
|
|
298
|
-
return this.sessionId;
|
|
299
|
-
}
|
|
300
141
|
}
|
|
301
142
|
export function createHostOpencodeSession(options, onMessage) {
|
|
302
143
|
return new HostOpencodeSession(options, onMessage);
|
|
@@ -1,21 +1,47 @@
|
|
|
1
|
+
import { BaseOpencodeSession } from './base-opencode-session';
|
|
1
2
|
import { opencodeProvider } from '../sessions/agents/opencode';
|
|
2
|
-
export class OpencodeSession {
|
|
3
|
-
process = null;
|
|
3
|
+
export class OpencodeSession extends BaseOpencodeSession {
|
|
4
4
|
containerName;
|
|
5
5
|
workDir;
|
|
6
|
-
sessionId;
|
|
7
|
-
model;
|
|
8
|
-
sessionModel;
|
|
9
|
-
onMessage;
|
|
10
|
-
buffer = '';
|
|
11
|
-
historyLoaded = false;
|
|
12
6
|
constructor(options, onMessage) {
|
|
7
|
+
super(options.sessionId, options.model, onMessage);
|
|
13
8
|
this.containerName = options.containerName;
|
|
14
9
|
this.workDir = options.workDir || '/home/workspace';
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
10
|
+
}
|
|
11
|
+
getLogPrefix() {
|
|
12
|
+
return 'opencode';
|
|
13
|
+
}
|
|
14
|
+
getNoOutputErrorMessage() {
|
|
15
|
+
return 'No response from OpenCode. Check if OpenCode is configured in the workspace.';
|
|
16
|
+
}
|
|
17
|
+
getSpawnConfig(userMessage) {
|
|
18
|
+
const args = [
|
|
19
|
+
'docker',
|
|
20
|
+
'exec',
|
|
21
|
+
'-i',
|
|
22
|
+
'-u',
|
|
23
|
+
'workspace',
|
|
24
|
+
'-w',
|
|
25
|
+
this.workDir,
|
|
26
|
+
this.containerName,
|
|
27
|
+
'stdbuf',
|
|
28
|
+
'-oL',
|
|
29
|
+
'opencode',
|
|
30
|
+
'run',
|
|
31
|
+
'--format',
|
|
32
|
+
'json',
|
|
33
|
+
];
|
|
34
|
+
if (this.sessionId) {
|
|
35
|
+
args.push('--session', this.sessionId);
|
|
36
|
+
}
|
|
37
|
+
if (this.model) {
|
|
38
|
+
args.push('--model', this.model);
|
|
39
|
+
}
|
|
40
|
+
args.push(userMessage);
|
|
41
|
+
return {
|
|
42
|
+
command: args,
|
|
43
|
+
options: {},
|
|
44
|
+
};
|
|
19
45
|
}
|
|
20
46
|
async loadHistory() {
|
|
21
47
|
if (this.historyLoaded || !this.sessionId) {
|
|
@@ -68,191 +94,6 @@ export class OpencodeSession {
|
|
|
68
94
|
console.error('[opencode] Failed to load history:', err);
|
|
69
95
|
}
|
|
70
96
|
}
|
|
71
|
-
async sendMessage(userMessage) {
|
|
72
|
-
if (this.sessionId && !this.historyLoaded) {
|
|
73
|
-
await this.loadHistory();
|
|
74
|
-
}
|
|
75
|
-
const args = [
|
|
76
|
-
'exec',
|
|
77
|
-
'-i',
|
|
78
|
-
'-u',
|
|
79
|
-
'workspace',
|
|
80
|
-
'-w',
|
|
81
|
-
this.workDir,
|
|
82
|
-
this.containerName,
|
|
83
|
-
'stdbuf',
|
|
84
|
-
'-oL',
|
|
85
|
-
'opencode',
|
|
86
|
-
'run',
|
|
87
|
-
'--format',
|
|
88
|
-
'json',
|
|
89
|
-
];
|
|
90
|
-
if (this.sessionId) {
|
|
91
|
-
args.push('--session', this.sessionId);
|
|
92
|
-
}
|
|
93
|
-
if (this.model) {
|
|
94
|
-
args.push('--model', this.model);
|
|
95
|
-
}
|
|
96
|
-
args.push(userMessage);
|
|
97
|
-
console.log('[opencode] Running:', 'docker', args.join(' '));
|
|
98
|
-
this.onMessage({
|
|
99
|
-
type: 'system',
|
|
100
|
-
content: 'Processing your message...',
|
|
101
|
-
timestamp: new Date().toISOString(),
|
|
102
|
-
});
|
|
103
|
-
try {
|
|
104
|
-
const proc = Bun.spawn(['docker', ...args], {
|
|
105
|
-
stdin: 'ignore',
|
|
106
|
-
stdout: 'pipe',
|
|
107
|
-
stderr: 'pipe',
|
|
108
|
-
});
|
|
109
|
-
this.process = proc;
|
|
110
|
-
if (!proc.stdout || !proc.stderr) {
|
|
111
|
-
throw new Error('Failed to get process streams');
|
|
112
|
-
}
|
|
113
|
-
console.log('[opencode] Process spawned, waiting for output...');
|
|
114
|
-
const stderrPromise = new Response(proc.stderr).text();
|
|
115
|
-
const decoder = new TextDecoder();
|
|
116
|
-
let receivedAnyOutput = false;
|
|
117
|
-
for await (const chunk of proc.stdout) {
|
|
118
|
-
const text = decoder.decode(chunk);
|
|
119
|
-
console.log('[opencode] Received chunk:', text.length, 'bytes');
|
|
120
|
-
receivedAnyOutput = true;
|
|
121
|
-
this.buffer += text;
|
|
122
|
-
this.processBuffer();
|
|
123
|
-
}
|
|
124
|
-
const exitCode = await proc.exited;
|
|
125
|
-
console.log('[opencode] Process exited with code:', exitCode, 'receivedOutput:', receivedAnyOutput);
|
|
126
|
-
const stderrText = await stderrPromise;
|
|
127
|
-
if (stderrText) {
|
|
128
|
-
console.error('[opencode] stderr:', stderrText);
|
|
129
|
-
}
|
|
130
|
-
if (exitCode !== 0) {
|
|
131
|
-
this.onMessage({
|
|
132
|
-
type: 'error',
|
|
133
|
-
content: stderrText || `OpenCode exited with code ${exitCode}`,
|
|
134
|
-
timestamp: new Date().toISOString(),
|
|
135
|
-
});
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
if (!receivedAnyOutput) {
|
|
139
|
-
this.onMessage({
|
|
140
|
-
type: 'error',
|
|
141
|
-
content: 'No response from OpenCode. Check if OpenCode is configured in the workspace.',
|
|
142
|
-
timestamp: new Date().toISOString(),
|
|
143
|
-
});
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
this.onMessage({
|
|
147
|
-
type: 'done',
|
|
148
|
-
content: 'Response complete',
|
|
149
|
-
timestamp: new Date().toISOString(),
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
catch (err) {
|
|
153
|
-
console.error('[opencode] Error:', err);
|
|
154
|
-
this.onMessage({
|
|
155
|
-
type: 'error',
|
|
156
|
-
content: err.message,
|
|
157
|
-
timestamp: new Date().toISOString(),
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
finally {
|
|
161
|
-
this.process = null;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
processBuffer() {
|
|
165
|
-
const lines = this.buffer.split('\n');
|
|
166
|
-
this.buffer = lines.pop() || '';
|
|
167
|
-
for (const line of lines) {
|
|
168
|
-
if (!line.trim())
|
|
169
|
-
continue;
|
|
170
|
-
try {
|
|
171
|
-
const event = JSON.parse(line);
|
|
172
|
-
this.handleStreamEvent(event);
|
|
173
|
-
}
|
|
174
|
-
catch {
|
|
175
|
-
console.error('[opencode] Failed to parse:', line);
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
handleStreamEvent(event) {
|
|
180
|
-
const timestamp = new Date().toISOString();
|
|
181
|
-
if (event.type === 'step_start' && event.sessionID) {
|
|
182
|
-
if (!this.sessionId) {
|
|
183
|
-
this.sessionId = event.sessionID;
|
|
184
|
-
this.sessionModel = this.model;
|
|
185
|
-
this.historyLoaded = true;
|
|
186
|
-
this.onMessage({
|
|
187
|
-
type: 'system',
|
|
188
|
-
content: `Session started ${this.sessionId}`,
|
|
189
|
-
timestamp,
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
if (event.type === 'text' && event.part?.text) {
|
|
195
|
-
this.onMessage({
|
|
196
|
-
type: 'assistant',
|
|
197
|
-
content: event.part.text,
|
|
198
|
-
timestamp,
|
|
199
|
-
});
|
|
200
|
-
return;
|
|
201
|
-
}
|
|
202
|
-
if (event.type === 'tool_use' && event.part) {
|
|
203
|
-
const toolName = event.part.tool || 'unknown';
|
|
204
|
-
const toolId = event.part.callID || event.part.id;
|
|
205
|
-
const input = event.part.state?.input;
|
|
206
|
-
const output = event.part.state?.output;
|
|
207
|
-
const title = event.part.state?.title || input?.description || toolName;
|
|
208
|
-
console.log('[opencode] Tool use:', toolName, title);
|
|
209
|
-
this.onMessage({
|
|
210
|
-
type: 'tool_use',
|
|
211
|
-
content: JSON.stringify(input, null, 2),
|
|
212
|
-
toolName: title || toolName,
|
|
213
|
-
toolId,
|
|
214
|
-
timestamp,
|
|
215
|
-
});
|
|
216
|
-
if (output) {
|
|
217
|
-
this.onMessage({
|
|
218
|
-
type: 'tool_result',
|
|
219
|
-
content: output,
|
|
220
|
-
toolName,
|
|
221
|
-
toolId,
|
|
222
|
-
timestamp,
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
|
-
return;
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
async interrupt() {
|
|
229
|
-
if (this.process) {
|
|
230
|
-
this.process.kill();
|
|
231
|
-
this.process = null;
|
|
232
|
-
this.onMessage({
|
|
233
|
-
type: 'system',
|
|
234
|
-
content: 'Chat interrupted',
|
|
235
|
-
timestamp: new Date().toISOString(),
|
|
236
|
-
});
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
setModel(model) {
|
|
240
|
-
if (this.model !== model) {
|
|
241
|
-
this.model = model;
|
|
242
|
-
if (this.sessionModel !== model) {
|
|
243
|
-
this.sessionId = undefined;
|
|
244
|
-
this.historyLoaded = false;
|
|
245
|
-
this.onMessage({
|
|
246
|
-
type: 'system',
|
|
247
|
-
content: `Switching to model: ${model}`,
|
|
248
|
-
timestamp: new Date().toISOString(),
|
|
249
|
-
});
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
getSessionId() {
|
|
254
|
-
return this.sessionId;
|
|
255
|
-
}
|
|
256
97
|
}
|
|
257
98
|
export function createOpencodeSession(options, onMessage) {
|
|
258
99
|
return new OpencodeSession(options, onMessage);
|
|
@@ -15,7 +15,7 @@ export class OpencodeWebSocketServer extends BaseChatWebSocketServer {
|
|
|
15
15
|
workspaceName,
|
|
16
16
|
};
|
|
17
17
|
}
|
|
18
|
-
createHostSession(sessionId, onMessage, messageModel) {
|
|
18
|
+
createHostSession(sessionId, onMessage, messageModel, _projectPath) {
|
|
19
19
|
const model = messageModel || this.getConfig?.()?.agents?.opencode?.model;
|
|
20
20
|
return createHostOpencodeSession({ sessionId, model }, onMessage);
|
|
21
21
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/chat/websocket.js
CHANGED
|
@@ -15,10 +15,10 @@ export class ChatWebSocketServer extends BaseChatWebSocketServer {
|
|
|
15
15
|
workspaceName,
|
|
16
16
|
};
|
|
17
17
|
}
|
|
18
|
-
createHostSession(sessionId, onMessage, messageModel) {
|
|
18
|
+
createHostSession(sessionId, onMessage, messageModel, projectPath) {
|
|
19
19
|
const config = this.getConfig();
|
|
20
20
|
const model = messageModel || config.agents?.claude_code?.model;
|
|
21
|
-
return createHostChatSession({ sessionId, model }, onMessage);
|
|
21
|
+
return createHostChatSession({ sessionId, model, workDir: projectPath }, onMessage);
|
|
22
22
|
}
|
|
23
23
|
createContainerSession(containerName, sessionId, onMessage, messageModel) {
|
|
24
24
|
const config = this.getConfig();
|
package/dist/client/api.js
CHANGED
|
@@ -98,6 +98,31 @@ export class ApiClient {
|
|
|
98
98
|
throw this.wrapError(err);
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
|
+
async getPortForwards(name) {
|
|
102
|
+
try {
|
|
103
|
+
const result = await this.client.workspaces.getPortForwards({ name });
|
|
104
|
+
return result.forwards;
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
throw this.wrapError(err);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
async setPortForwards(name, forwards) {
|
|
111
|
+
try {
|
|
112
|
+
return await this.client.workspaces.setPortForwards({ name, forwards });
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
throw this.wrapError(err);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
async cloneWorkspace(sourceName, cloneName) {
|
|
119
|
+
try {
|
|
120
|
+
return await this.client.workspaces.clone({ sourceName, cloneName });
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
throw this.wrapError(err);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
101
126
|
getTerminalUrl(name) {
|
|
102
127
|
const wsUrl = this.baseUrl.replace(/^http/, 'ws');
|
|
103
128
|
return `${wsUrl}/rpc/terminal/${encodeURIComponent(name)}`;
|
package/dist/config/loader.js
CHANGED
|
@@ -16,7 +16,10 @@ export function createDefaultAgentConfig() {
|
|
|
16
16
|
env: {},
|
|
17
17
|
files: {},
|
|
18
18
|
},
|
|
19
|
-
scripts: {
|
|
19
|
+
scripts: {
|
|
20
|
+
post_start: ['~/.perry/userscripts'],
|
|
21
|
+
fail_on_error: false,
|
|
22
|
+
},
|
|
20
23
|
agents: {},
|
|
21
24
|
allowHostAccess: true,
|
|
22
25
|
ssh: {
|
|
@@ -29,6 +32,18 @@ export function createDefaultAgentConfig() {
|
|
|
29
32
|
},
|
|
30
33
|
};
|
|
31
34
|
}
|
|
35
|
+
function migratePostStart(value) {
|
|
36
|
+
if (!value) {
|
|
37
|
+
return ['~/.perry/userscripts'];
|
|
38
|
+
}
|
|
39
|
+
if (typeof value === 'string') {
|
|
40
|
+
return [value, '~/.perry/userscripts'];
|
|
41
|
+
}
|
|
42
|
+
if (Array.isArray(value)) {
|
|
43
|
+
return value.length > 0 ? value : ['~/.perry/userscripts'];
|
|
44
|
+
}
|
|
45
|
+
return ['~/.perry/userscripts'];
|
|
46
|
+
}
|
|
32
47
|
export async function loadAgentConfig(configDir) {
|
|
33
48
|
const dir = getConfigDir(configDir);
|
|
34
49
|
const configPath = path.join(dir, CONFIG_FILE);
|
|
@@ -41,7 +56,10 @@ export async function loadAgentConfig(configDir) {
|
|
|
41
56
|
env: config.credentials?.env || {},
|
|
42
57
|
files: config.credentials?.files || {},
|
|
43
58
|
},
|
|
44
|
-
scripts:
|
|
59
|
+
scripts: {
|
|
60
|
+
post_start: migratePostStart(config.scripts?.post_start),
|
|
61
|
+
fail_on_error: config.scripts?.fail_on_error ?? false,
|
|
62
|
+
},
|
|
45
63
|
agents: config.agents || {},
|
|
46
64
|
allowHostAccess: config.allowHostAccess ?? true,
|
|
47
65
|
ssh: {
|
|
@@ -5,6 +5,7 @@ const RETRY_INTERVAL_MS = 20000;
|
|
|
5
5
|
const MAX_RETRIES = 10;
|
|
6
6
|
let pullInProgress = false;
|
|
7
7
|
let pullComplete = false;
|
|
8
|
+
let abortController = null;
|
|
8
9
|
async function isDockerAvailable() {
|
|
9
10
|
try {
|
|
10
11
|
await getDockerVersion();
|
|
@@ -35,7 +36,13 @@ export async function startEagerImagePull() {
|
|
|
35
36
|
return;
|
|
36
37
|
}
|
|
37
38
|
pullInProgress = true;
|
|
39
|
+
abortController = new AbortController();
|
|
40
|
+
const signal = abortController.signal;
|
|
38
41
|
const attemptPull = async (attempt) => {
|
|
42
|
+
if (signal.aborted) {
|
|
43
|
+
pullInProgress = false;
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
39
46
|
if (attempt > MAX_RETRIES) {
|
|
40
47
|
console.log('[agent] Max retries reached for image pull - giving up background pull');
|
|
41
48
|
pullInProgress = false;
|
|
@@ -46,7 +53,8 @@ export async function startEagerImagePull() {
|
|
|
46
53
|
if (attempt === 1) {
|
|
47
54
|
console.log('[agent] Docker not available - will retry in background');
|
|
48
55
|
}
|
|
49
|
-
setTimeout(() => attemptPull(attempt + 1), RETRY_INTERVAL_MS);
|
|
56
|
+
const timer = setTimeout(() => attemptPull(attempt + 1), RETRY_INTERVAL_MS);
|
|
57
|
+
timer.unref();
|
|
50
58
|
return;
|
|
51
59
|
}
|
|
52
60
|
const success = await pullWorkspaceImage();
|
|
@@ -54,12 +62,20 @@ export async function startEagerImagePull() {
|
|
|
54
62
|
pullComplete = true;
|
|
55
63
|
pullInProgress = false;
|
|
56
64
|
}
|
|
57
|
-
else {
|
|
58
|
-
setTimeout(() => attemptPull(attempt + 1), RETRY_INTERVAL_MS);
|
|
65
|
+
else if (!signal.aborted) {
|
|
66
|
+
const timer = setTimeout(() => attemptPull(attempt + 1), RETRY_INTERVAL_MS);
|
|
67
|
+
timer.unref();
|
|
59
68
|
}
|
|
60
69
|
};
|
|
61
70
|
attemptPull(1);
|
|
62
71
|
}
|
|
72
|
+
export function stopEagerImagePull() {
|
|
73
|
+
if (abortController) {
|
|
74
|
+
abortController.abort();
|
|
75
|
+
abortController = null;
|
|
76
|
+
}
|
|
77
|
+
pullInProgress = false;
|
|
78
|
+
}
|
|
63
79
|
export function isImagePullComplete() {
|
|
64
80
|
return pullComplete;
|
|
65
81
|
}
|
package/dist/docker/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn } from 'child_process';
|
|
2
|
+
import { CONTAINER_PREFIX } from '../shared/constants';
|
|
2
3
|
export * from './types';
|
|
3
|
-
const CONTAINER_PREFIX = 'workspace-';
|
|
4
4
|
export function getContainerName(name) {
|
|
5
5
|
return `${CONTAINER_PREFIX}${name}`;
|
|
6
6
|
}
|
|
@@ -410,3 +410,30 @@ export async function getLogs(containerName, options = {}) {
|
|
|
410
410
|
const { stdout, stderr } = await docker(args);
|
|
411
411
|
return stdout + stderr;
|
|
412
412
|
}
|
|
413
|
+
export async function cloneVolume(sourceVolume, destVolume) {
|
|
414
|
+
if (!(await volumeExists(sourceVolume))) {
|
|
415
|
+
throw new Error(`Source volume '${sourceVolume}' does not exist`);
|
|
416
|
+
}
|
|
417
|
+
if (await volumeExists(destVolume)) {
|
|
418
|
+
throw new Error(`Volume '${destVolume}' already exists`);
|
|
419
|
+
}
|
|
420
|
+
await createVolume(destVolume);
|
|
421
|
+
try {
|
|
422
|
+
await docker([
|
|
423
|
+
'run',
|
|
424
|
+
'--rm',
|
|
425
|
+
'-v',
|
|
426
|
+
`${sourceVolume}:/source:ro`,
|
|
427
|
+
'-v',
|
|
428
|
+
`${destVolume}:/dest`,
|
|
429
|
+
'alpine',
|
|
430
|
+
'sh',
|
|
431
|
+
'-c',
|
|
432
|
+
'cp -a /source/. /dest/',
|
|
433
|
+
]);
|
|
434
|
+
}
|
|
435
|
+
catch (err) {
|
|
436
|
+
await removeVolume(destVolume, true).catch(() => { });
|
|
437
|
+
throw new Error(`Failed to clone volume: ${err.message}`);
|
|
438
|
+
}
|
|
439
|
+
}
|