@gricha/perry 0.2.2 → 0.2.3
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 +8 -0
- package/dist/agent/file-watcher.js +138 -0
- package/dist/agent/router.js +5 -1
- package/dist/agent/run.js +29 -3
- package/dist/agent/web/assets/{index-DN_QW9sL.js → index-BF-4SpMu.js} +21 -21
- package/dist/agent/web/index.html +1 -1
- package/dist/chat/base-chat-websocket.js +1 -1
- 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/types.js +1 -0
- package/dist/docker/index.js +1 -1
- 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/package.json +4 -3
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
import { homedir } from 'os';
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
import { BaseClaudeSession } from './base-claude-session';
|
|
3
|
+
export class HostChatSession extends BaseClaudeSession {
|
|
4
4
|
workDir;
|
|
5
|
-
sessionId;
|
|
6
|
-
model;
|
|
7
|
-
onMessage;
|
|
8
|
-
buffer = '';
|
|
9
5
|
constructor(options, onMessage) {
|
|
6
|
+
super(options.sessionId, options.model, onMessage);
|
|
10
7
|
this.workDir = options.workDir || homedir();
|
|
11
|
-
this.sessionId = options.sessionId;
|
|
12
|
-
this.model = options.model || 'sonnet';
|
|
13
|
-
this.onMessage = onMessage;
|
|
14
8
|
}
|
|
15
|
-
|
|
9
|
+
getLogPrefix() {
|
|
10
|
+
return 'host-chat';
|
|
11
|
+
}
|
|
12
|
+
getSpawnConfig(userMessage) {
|
|
16
13
|
const args = [
|
|
14
|
+
'claude',
|
|
17
15
|
'--print',
|
|
18
16
|
'--verbose',
|
|
19
17
|
'--output-format',
|
|
@@ -27,142 +25,15 @@ export class HostChatSession {
|
|
|
27
25
|
args.push('--resume', this.sessionId);
|
|
28
26
|
}
|
|
29
27
|
args.push(userMessage);
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
content: 'Processing your message...',
|
|
34
|
-
timestamp: new Date().toISOString(),
|
|
35
|
-
});
|
|
36
|
-
try {
|
|
37
|
-
const proc = Bun.spawn(['claude', ...args], {
|
|
28
|
+
return {
|
|
29
|
+
command: args,
|
|
30
|
+
options: {
|
|
38
31
|
cwd: this.workDir,
|
|
39
|
-
stdin: 'ignore',
|
|
40
|
-
stdout: 'pipe',
|
|
41
|
-
stderr: 'pipe',
|
|
42
32
|
env: {
|
|
43
33
|
...process.env,
|
|
44
34
|
},
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
if (!proc.stdout || !proc.stderr) {
|
|
48
|
-
throw new Error('Failed to get process streams');
|
|
49
|
-
}
|
|
50
|
-
console.log('[host-chat] Process spawned, waiting for output...');
|
|
51
|
-
const stderrPromise = new Response(proc.stderr).text();
|
|
52
|
-
const decoder = new TextDecoder();
|
|
53
|
-
let receivedAnyOutput = false;
|
|
54
|
-
for await (const chunk of proc.stdout) {
|
|
55
|
-
const text = decoder.decode(chunk);
|
|
56
|
-
console.log('[host-chat] Received chunk:', text.length, 'bytes');
|
|
57
|
-
receivedAnyOutput = true;
|
|
58
|
-
this.buffer += text;
|
|
59
|
-
this.processBuffer();
|
|
60
|
-
}
|
|
61
|
-
const exitCode = await proc.exited;
|
|
62
|
-
console.log('[host-chat] Process exited with code:', exitCode, 'receivedOutput:', receivedAnyOutput);
|
|
63
|
-
const stderrText = await stderrPromise;
|
|
64
|
-
if (stderrText) {
|
|
65
|
-
console.error('[host-chat] stderr:', stderrText);
|
|
66
|
-
}
|
|
67
|
-
if (exitCode !== 0) {
|
|
68
|
-
this.onMessage({
|
|
69
|
-
type: 'error',
|
|
70
|
-
content: stderrText || `Claude exited with code ${exitCode}`,
|
|
71
|
-
timestamp: new Date().toISOString(),
|
|
72
|
-
});
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
if (!receivedAnyOutput) {
|
|
76
|
-
this.onMessage({
|
|
77
|
-
type: 'error',
|
|
78
|
-
content: 'No response from Claude. Check if Claude is authenticated.',
|
|
79
|
-
timestamp: new Date().toISOString(),
|
|
80
|
-
});
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
this.onMessage({
|
|
84
|
-
type: 'done',
|
|
85
|
-
content: 'Response complete',
|
|
86
|
-
timestamp: new Date().toISOString(),
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
catch (err) {
|
|
90
|
-
console.error('[host-chat] Error:', err);
|
|
91
|
-
this.onMessage({
|
|
92
|
-
type: 'error',
|
|
93
|
-
content: err.message,
|
|
94
|
-
timestamp: new Date().toISOString(),
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
finally {
|
|
98
|
-
this.process = null;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
processBuffer() {
|
|
102
|
-
const lines = this.buffer.split('\n');
|
|
103
|
-
this.buffer = lines.pop() || '';
|
|
104
|
-
for (const line of lines) {
|
|
105
|
-
if (!line.trim())
|
|
106
|
-
continue;
|
|
107
|
-
try {
|
|
108
|
-
const msg = JSON.parse(line);
|
|
109
|
-
this.handleStreamMessage(msg);
|
|
110
|
-
}
|
|
111
|
-
catch {
|
|
112
|
-
console.error('[host-chat] Failed to parse:', line);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
handleStreamMessage(msg) {
|
|
117
|
-
const timestamp = new Date().toISOString();
|
|
118
|
-
if (msg.type === 'system' && msg.subtype === 'init') {
|
|
119
|
-
this.sessionId = msg.session_id;
|
|
120
|
-
this.onMessage({
|
|
121
|
-
type: 'system',
|
|
122
|
-
content: `Session started: ${msg.session_id?.slice(0, 8)}...`,
|
|
123
|
-
timestamp,
|
|
124
|
-
});
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
if (msg.type === 'assistant' && msg.message?.content) {
|
|
128
|
-
for (const block of msg.message.content) {
|
|
129
|
-
if (block.type === 'tool_use') {
|
|
130
|
-
this.onMessage({
|
|
131
|
-
type: 'tool_use',
|
|
132
|
-
content: JSON.stringify(block.input, null, 2),
|
|
133
|
-
toolName: block.name,
|
|
134
|
-
toolId: block.id,
|
|
135
|
-
timestamp,
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
if (msg.type === 'stream_event' && msg.event?.type === 'content_block_delta') {
|
|
142
|
-
const delta = msg.event?.delta;
|
|
143
|
-
if (delta?.type === 'text_delta' && delta?.text) {
|
|
144
|
-
this.onMessage({
|
|
145
|
-
type: 'assistant',
|
|
146
|
-
content: delta.text,
|
|
147
|
-
timestamp,
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
async interrupt() {
|
|
154
|
-
if (this.process) {
|
|
155
|
-
this.process.kill();
|
|
156
|
-
this.process = null;
|
|
157
|
-
this.onMessage({
|
|
158
|
-
type: 'system',
|
|
159
|
-
content: 'Chat interrupted',
|
|
160
|
-
timestamp: new Date().toISOString(),
|
|
161
|
-
});
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
getSessionId() {
|
|
165
|
-
return this.sessionId;
|
|
35
|
+
},
|
|
36
|
+
};
|
|
166
37
|
}
|
|
167
38
|
}
|
|
168
39
|
export function createHostChatSession(options, onMessage) {
|
|
@@ -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);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/docker/index.js
CHANGED
package/dist/perry-worker
CHANGED
|
Binary file
|
package/dist/shared/constants.js
CHANGED
package/dist/shared/types.js
CHANGED
|
@@ -3,7 +3,7 @@ import { BaseWebSocketServer, safeSend } from '../shared/base-websocket';
|
|
|
3
3
|
import { createTerminalSession } from './handler';
|
|
4
4
|
import { createHostTerminalSession } from './host-handler';
|
|
5
5
|
import { isControlMessage } from './types';
|
|
6
|
-
import { HOST_WORKSPACE_NAME } from '../shared/types';
|
|
6
|
+
import { HOST_WORKSPACE_NAME } from '../shared/client-types';
|
|
7
7
|
export class TerminalWebSocketServer extends BaseWebSocketServer {
|
|
8
8
|
getContainerName;
|
|
9
9
|
isHostAccessAllowed;
|