@gricha/perry 0.0.1 → 0.1.1
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 +25 -33
- package/dist/agent/router.js +39 -404
- package/dist/agent/web/assets/index-BGbqUzMS.js +104 -0
- package/dist/agent/web/assets/index-CHEQQv1U.css +1 -0
- package/dist/agent/web/favicon.ico +0 -0
- package/dist/agent/web/index.html +3 -3
- package/dist/agent/web/logo-192.png +0 -0
- package/dist/agent/web/logo-512.png +0 -0
- package/dist/agent/web/logo.png +0 -0
- package/dist/agent/web/logo.webp +0 -0
- package/dist/chat/base-chat-websocket.js +83 -0
- package/dist/chat/host-opencode-handler.js +115 -3
- package/dist/chat/opencode-handler.js +60 -0
- package/dist/chat/opencode-server.js +252 -0
- package/dist/chat/opencode-websocket.js +15 -87
- package/dist/chat/websocket.js +19 -86
- package/dist/client/ws-shell.js +15 -5
- package/dist/docker/index.js +41 -1
- package/dist/index.js +18 -3
- package/dist/sessions/agents/claude.js +86 -0
- package/dist/sessions/agents/codex.js +110 -0
- package/dist/sessions/agents/index.js +44 -0
- package/dist/sessions/agents/opencode.js +168 -0
- package/dist/sessions/agents/types.js +1 -0
- package/dist/sessions/agents/utils.js +31 -0
- package/dist/shared/base-websocket.js +13 -1
- package/dist/shared/constants.js +2 -1
- package/dist/terminal/base-handler.js +68 -0
- package/dist/terminal/handler.js +18 -75
- package/dist/terminal/host-handler.js +7 -61
- package/dist/terminal/websocket.js +2 -4
- package/dist/workspace/manager.js +33 -22
- package/dist/workspace/state.js +33 -2
- package/package.json +1 -1
- package/dist/agent/web/assets/index-9t2sFIJM.js +0 -101
- package/dist/agent/web/assets/index-CCFpTruF.css +0 -1
- package/dist/agent/web/vite.svg +0 -1
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { execInContainer } from '../docker';
|
|
2
|
+
const serverPorts = new Map();
|
|
3
|
+
const serverStarting = new Map();
|
|
4
|
+
async function findAvailablePort(containerName) {
|
|
5
|
+
const script = `import socket; s=socket.socket(); s.bind(('', 0)); print(s.getsockname()[1]); s.close()`;
|
|
6
|
+
const result = await execInContainer(containerName, ['python3', '-c', script], {
|
|
7
|
+
user: 'workspace',
|
|
8
|
+
});
|
|
9
|
+
return parseInt(result.stdout.trim(), 10);
|
|
10
|
+
}
|
|
11
|
+
async function isServerRunning(containerName, port) {
|
|
12
|
+
try {
|
|
13
|
+
const result = await execInContainer(containerName, ['curl', '-s', '-o', '/dev/null', '-w', '%{http_code}', `http://localhost:${port}/session`], { user: 'workspace' });
|
|
14
|
+
return result.stdout.trim() === '200';
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
async function startServer(containerName) {
|
|
21
|
+
const existing = serverPorts.get(containerName);
|
|
22
|
+
if (existing && (await isServerRunning(containerName, existing))) {
|
|
23
|
+
return existing;
|
|
24
|
+
}
|
|
25
|
+
const starting = serverStarting.get(containerName);
|
|
26
|
+
if (starting) {
|
|
27
|
+
return starting;
|
|
28
|
+
}
|
|
29
|
+
const startPromise = (async () => {
|
|
30
|
+
const port = await findAvailablePort(containerName);
|
|
31
|
+
console.log(`[opencode-server] Starting server on port ${port} in ${containerName}`);
|
|
32
|
+
await execInContainer(containerName, [
|
|
33
|
+
'sh',
|
|
34
|
+
'-c',
|
|
35
|
+
`nohup opencode serve --port ${port} --hostname 127.0.0.1 > /tmp/opencode-server.log 2>&1 &`,
|
|
36
|
+
], { user: 'workspace' });
|
|
37
|
+
for (let i = 0; i < 30; i++) {
|
|
38
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
39
|
+
if (await isServerRunning(containerName, port)) {
|
|
40
|
+
console.log(`[opencode-server] Server started on port ${port}`);
|
|
41
|
+
serverPorts.set(containerName, port);
|
|
42
|
+
serverStarting.delete(containerName);
|
|
43
|
+
return port;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
serverStarting.delete(containerName);
|
|
47
|
+
throw new Error('Failed to start OpenCode server');
|
|
48
|
+
})();
|
|
49
|
+
serverStarting.set(containerName, startPromise);
|
|
50
|
+
return startPromise;
|
|
51
|
+
}
|
|
52
|
+
export class OpenCodeServerSession {
|
|
53
|
+
containerName;
|
|
54
|
+
workDir;
|
|
55
|
+
sessionId;
|
|
56
|
+
onMessage;
|
|
57
|
+
sseProcess = null;
|
|
58
|
+
responseComplete = false;
|
|
59
|
+
seenToolUse = new Set();
|
|
60
|
+
seenToolResult = new Set();
|
|
61
|
+
constructor(options, onMessage) {
|
|
62
|
+
this.containerName = options.containerName;
|
|
63
|
+
this.workDir = options.workDir || '/home/workspace';
|
|
64
|
+
this.sessionId = options.sessionId;
|
|
65
|
+
this.onMessage = onMessage;
|
|
66
|
+
}
|
|
67
|
+
async sendMessage(userMessage) {
|
|
68
|
+
const port = await startServer(this.containerName);
|
|
69
|
+
const baseUrl = `http://localhost:${port}`;
|
|
70
|
+
this.onMessage({
|
|
71
|
+
type: 'system',
|
|
72
|
+
content: 'Processing your message...',
|
|
73
|
+
timestamp: new Date().toISOString(),
|
|
74
|
+
});
|
|
75
|
+
try {
|
|
76
|
+
if (!this.sessionId) {
|
|
77
|
+
const createResult = await execInContainer(this.containerName, [
|
|
78
|
+
'curl',
|
|
79
|
+
'-s',
|
|
80
|
+
'-X',
|
|
81
|
+
'POST',
|
|
82
|
+
`${baseUrl}/session`,
|
|
83
|
+
'-H',
|
|
84
|
+
'Content-Type: application/json',
|
|
85
|
+
'-d',
|
|
86
|
+
'{}',
|
|
87
|
+
], { user: 'workspace' });
|
|
88
|
+
const session = JSON.parse(createResult.stdout);
|
|
89
|
+
this.sessionId = session.id;
|
|
90
|
+
this.onMessage({
|
|
91
|
+
type: 'system',
|
|
92
|
+
content: `Session started ${this.sessionId}`,
|
|
93
|
+
timestamp: new Date().toISOString(),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
this.responseComplete = false;
|
|
97
|
+
this.seenToolUse.clear();
|
|
98
|
+
this.seenToolResult.clear();
|
|
99
|
+
const ssePromise = this.startSSEStream(port);
|
|
100
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
101
|
+
const messagePayload = JSON.stringify({
|
|
102
|
+
parts: [{ type: 'text', text: userMessage }],
|
|
103
|
+
});
|
|
104
|
+
execInContainer(this.containerName, [
|
|
105
|
+
'curl',
|
|
106
|
+
'-s',
|
|
107
|
+
'-X',
|
|
108
|
+
'POST',
|
|
109
|
+
`${baseUrl}/session/${this.sessionId}/message`,
|
|
110
|
+
'-H',
|
|
111
|
+
'Content-Type: application/json',
|
|
112
|
+
'-d',
|
|
113
|
+
messagePayload,
|
|
114
|
+
], { user: 'workspace' }).catch((err) => {
|
|
115
|
+
console.error('[opencode-server] Send error:', err);
|
|
116
|
+
});
|
|
117
|
+
await ssePromise;
|
|
118
|
+
this.onMessage({
|
|
119
|
+
type: 'done',
|
|
120
|
+
content: 'Response complete',
|
|
121
|
+
timestamp: new Date().toISOString(),
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
console.error('[opencode-server] Error:', err);
|
|
126
|
+
this.onMessage({
|
|
127
|
+
type: 'error',
|
|
128
|
+
content: err.message,
|
|
129
|
+
timestamp: new Date().toISOString(),
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async startSSEStream(port) {
|
|
134
|
+
return new Promise((resolve) => {
|
|
135
|
+
const proc = Bun.spawn([
|
|
136
|
+
'docker',
|
|
137
|
+
'exec',
|
|
138
|
+
'-i',
|
|
139
|
+
this.containerName,
|
|
140
|
+
'curl',
|
|
141
|
+
'-s',
|
|
142
|
+
'-N',
|
|
143
|
+
'--max-time',
|
|
144
|
+
'120',
|
|
145
|
+
`http://localhost:${port}/event`,
|
|
146
|
+
], {
|
|
147
|
+
stdin: 'ignore',
|
|
148
|
+
stdout: 'pipe',
|
|
149
|
+
stderr: 'pipe',
|
|
150
|
+
});
|
|
151
|
+
this.sseProcess = proc;
|
|
152
|
+
const decoder = new TextDecoder();
|
|
153
|
+
let buffer = '';
|
|
154
|
+
const processChunk = (chunk) => {
|
|
155
|
+
buffer += decoder.decode(chunk);
|
|
156
|
+
const lines = buffer.split('\n');
|
|
157
|
+
buffer = lines.pop() || '';
|
|
158
|
+
for (const line of lines) {
|
|
159
|
+
if (!line.startsWith('data: '))
|
|
160
|
+
continue;
|
|
161
|
+
const data = line.slice(6).trim();
|
|
162
|
+
if (!data)
|
|
163
|
+
continue;
|
|
164
|
+
try {
|
|
165
|
+
const event = JSON.parse(data);
|
|
166
|
+
this.handleEvent(event);
|
|
167
|
+
if (event.type === 'session.idle') {
|
|
168
|
+
this.responseComplete = true;
|
|
169
|
+
proc.kill();
|
|
170
|
+
resolve();
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
(async () => {
|
|
180
|
+
if (!proc.stdout) {
|
|
181
|
+
resolve();
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
for await (const chunk of proc.stdout) {
|
|
185
|
+
processChunk(chunk);
|
|
186
|
+
if (this.responseComplete)
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
resolve();
|
|
190
|
+
})();
|
|
191
|
+
setTimeout(() => {
|
|
192
|
+
if (!this.responseComplete) {
|
|
193
|
+
proc.kill();
|
|
194
|
+
resolve();
|
|
195
|
+
}
|
|
196
|
+
}, 120000);
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
handleEvent(event) {
|
|
200
|
+
const timestamp = new Date().toISOString();
|
|
201
|
+
if (event.type === 'message.part.updated' && event.properties.part) {
|
|
202
|
+
const part = event.properties.part;
|
|
203
|
+
if (part.type === 'text' && event.properties.delta) {
|
|
204
|
+
this.onMessage({
|
|
205
|
+
type: 'assistant',
|
|
206
|
+
content: event.properties.delta,
|
|
207
|
+
timestamp,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
else if (part.type === 'tool' && part.tool) {
|
|
211
|
+
const state = part.state;
|
|
212
|
+
const partId = part.id;
|
|
213
|
+
if (!this.seenToolUse.has(partId)) {
|
|
214
|
+
this.seenToolUse.add(partId);
|
|
215
|
+
this.onMessage({
|
|
216
|
+
type: 'tool_use',
|
|
217
|
+
content: JSON.stringify(state?.input, null, 2),
|
|
218
|
+
toolName: state?.title || part.tool,
|
|
219
|
+
toolId: partId,
|
|
220
|
+
timestamp,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
if (state?.status === 'completed' && state?.output && !this.seenToolResult.has(partId)) {
|
|
224
|
+
this.seenToolResult.add(partId);
|
|
225
|
+
this.onMessage({
|
|
226
|
+
type: 'tool_result',
|
|
227
|
+
content: state.output,
|
|
228
|
+
toolId: partId,
|
|
229
|
+
timestamp,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
async interrupt() {
|
|
236
|
+
if (this.sseProcess) {
|
|
237
|
+
this.sseProcess.kill();
|
|
238
|
+
this.sseProcess = null;
|
|
239
|
+
this.onMessage({
|
|
240
|
+
type: 'system',
|
|
241
|
+
content: 'Chat interrupted',
|
|
242
|
+
timestamp: new Date().toISOString(),
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
getSessionId() {
|
|
247
|
+
return this.sessionId;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
export function createOpenCodeServerSession(options, onMessage) {
|
|
251
|
+
return new OpenCodeServerSession(options, onMessage);
|
|
252
|
+
}
|
|
@@ -1,95 +1,23 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { BaseWebSocketServer } from '../shared/base-websocket';
|
|
3
|
-
import { createOpencodeSession } from './opencode-handler';
|
|
1
|
+
import { BaseChatWebSocketServer, } from './base-chat-websocket';
|
|
4
2
|
import { createHostOpencodeSession } from './host-opencode-handler';
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
super(options);
|
|
11
|
-
this.isHostAccessAllowed = options.isHostAccessAllowed || (() => false);
|
|
12
|
-
}
|
|
13
|
-
handleConnection(ws, workspaceName) {
|
|
14
|
-
const isHostMode = workspaceName === HOST_WORKSPACE_NAME;
|
|
15
|
-
if (isHostMode && !this.isHostAccessAllowed()) {
|
|
16
|
-
ws.close(4003, 'Host access is disabled');
|
|
17
|
-
return;
|
|
18
|
-
}
|
|
19
|
-
const connection = {
|
|
3
|
+
import { createOpenCodeServerSession } from './opencode-server';
|
|
4
|
+
export class OpencodeWebSocketServer extends BaseChatWebSocketServer {
|
|
5
|
+
agentType = 'opencode';
|
|
6
|
+
createConnection(ws, workspaceName) {
|
|
7
|
+
return {
|
|
20
8
|
ws,
|
|
21
9
|
session: null,
|
|
22
10
|
workspaceName,
|
|
23
11
|
};
|
|
24
|
-
this.connections.set(ws, connection);
|
|
25
|
-
ws.send(JSON.stringify({
|
|
26
|
-
type: 'connected',
|
|
27
|
-
workspaceName,
|
|
28
|
-
agentType: 'opencode',
|
|
29
|
-
timestamp: new Date().toISOString(),
|
|
30
|
-
}));
|
|
31
|
-
ws.on('message', async (data) => {
|
|
32
|
-
const str = typeof data === 'string' ? data : data.toString();
|
|
33
|
-
try {
|
|
34
|
-
const message = JSON.parse(str);
|
|
35
|
-
if (message.type === 'interrupt') {
|
|
36
|
-
if (connection.session) {
|
|
37
|
-
await connection.session.interrupt();
|
|
38
|
-
connection.session = null;
|
|
39
|
-
}
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
if (message.type === 'message' && message.content) {
|
|
43
|
-
const onMessage = (chatMessage) => {
|
|
44
|
-
if (ws.readyState === WebSocket.OPEN) {
|
|
45
|
-
ws.send(JSON.stringify(chatMessage));
|
|
46
|
-
}
|
|
47
|
-
};
|
|
48
|
-
if (!connection.session) {
|
|
49
|
-
if (isHostMode) {
|
|
50
|
-
connection.session = createHostOpencodeSession({
|
|
51
|
-
sessionId: message.sessionId,
|
|
52
|
-
}, onMessage);
|
|
53
|
-
}
|
|
54
|
-
else {
|
|
55
|
-
const containerName = getContainerName(workspaceName);
|
|
56
|
-
connection.session = createOpencodeSession({
|
|
57
|
-
containerName,
|
|
58
|
-
workDir: '/home/workspace',
|
|
59
|
-
sessionId: message.sessionId,
|
|
60
|
-
}, onMessage);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
await connection.session.sendMessage(message.content);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
catch (err) {
|
|
67
|
-
ws.send(JSON.stringify({
|
|
68
|
-
type: 'error',
|
|
69
|
-
content: err.message,
|
|
70
|
-
timestamp: new Date().toISOString(),
|
|
71
|
-
}));
|
|
72
|
-
}
|
|
73
|
-
});
|
|
74
|
-
ws.on('close', () => {
|
|
75
|
-
const conn = this.connections.get(ws);
|
|
76
|
-
if (conn?.session) {
|
|
77
|
-
conn.session.interrupt().catch(() => { });
|
|
78
|
-
}
|
|
79
|
-
this.connections.delete(ws);
|
|
80
|
-
});
|
|
81
|
-
ws.on('error', (err) => {
|
|
82
|
-
console.error('OpenCode WebSocket error:', err);
|
|
83
|
-
const conn = this.connections.get(ws);
|
|
84
|
-
if (conn?.session) {
|
|
85
|
-
conn.session.interrupt().catch(() => { });
|
|
86
|
-
}
|
|
87
|
-
this.connections.delete(ws);
|
|
88
|
-
});
|
|
89
12
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
13
|
+
createHostSession(sessionId, onMessage) {
|
|
14
|
+
return createHostOpencodeSession({ sessionId }, onMessage);
|
|
15
|
+
}
|
|
16
|
+
createContainerSession(containerName, sessionId, onMessage) {
|
|
17
|
+
return createOpenCodeServerSession({
|
|
18
|
+
containerName,
|
|
19
|
+
workDir: '/home/workspace',
|
|
20
|
+
sessionId,
|
|
21
|
+
}, onMessage);
|
|
94
22
|
}
|
|
95
23
|
}
|
package/dist/chat/websocket.js
CHANGED
|
@@ -1,100 +1,33 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { BaseWebSocketServer } from '../shared/base-websocket';
|
|
1
|
+
import { BaseChatWebSocketServer, } from './base-chat-websocket';
|
|
3
2
|
import { createChatSession } from './handler';
|
|
4
3
|
import { createHostChatSession } from './host-handler';
|
|
5
|
-
|
|
6
|
-
import { HOST_WORKSPACE_NAME } from '../shared/types';
|
|
7
|
-
export class ChatWebSocketServer extends BaseWebSocketServer {
|
|
4
|
+
export class ChatWebSocketServer extends BaseChatWebSocketServer {
|
|
8
5
|
getConfig;
|
|
9
|
-
|
|
6
|
+
agentType = '';
|
|
10
7
|
constructor(options) {
|
|
11
8
|
super(options);
|
|
12
9
|
this.getConfig = options.getConfig;
|
|
13
|
-
this.isHostAccessAllowed = options.isHostAccessAllowed || (() => false);
|
|
14
10
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
if (isHostMode && !this.isHostAccessAllowed()) {
|
|
18
|
-
ws.close(4003, 'Host access is disabled');
|
|
19
|
-
return;
|
|
20
|
-
}
|
|
21
|
-
const connection = {
|
|
11
|
+
createConnection(ws, workspaceName) {
|
|
12
|
+
return {
|
|
22
13
|
ws,
|
|
23
14
|
session: null,
|
|
24
15
|
workspaceName,
|
|
25
16
|
};
|
|
26
|
-
this.connections.set(ws, connection);
|
|
27
|
-
ws.send(JSON.stringify({
|
|
28
|
-
type: 'connected',
|
|
29
|
-
workspaceName,
|
|
30
|
-
timestamp: new Date().toISOString(),
|
|
31
|
-
}));
|
|
32
|
-
ws.on('message', async (data) => {
|
|
33
|
-
const str = typeof data === 'string' ? data : data.toString();
|
|
34
|
-
try {
|
|
35
|
-
const message = JSON.parse(str);
|
|
36
|
-
if (message.type === 'interrupt') {
|
|
37
|
-
if (connection.session) {
|
|
38
|
-
await connection.session.interrupt();
|
|
39
|
-
connection.session = null;
|
|
40
|
-
}
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
if (message.type === 'message' && message.content) {
|
|
44
|
-
const onMessage = (chatMessage) => {
|
|
45
|
-
if (ws.readyState === WebSocket.OPEN) {
|
|
46
|
-
ws.send(JSON.stringify(chatMessage));
|
|
47
|
-
}
|
|
48
|
-
};
|
|
49
|
-
if (!connection.session) {
|
|
50
|
-
const config = this.getConfig();
|
|
51
|
-
const model = config.agents?.claude_code?.model;
|
|
52
|
-
if (isHostMode) {
|
|
53
|
-
connection.session = createHostChatSession({
|
|
54
|
-
sessionId: message.sessionId,
|
|
55
|
-
model,
|
|
56
|
-
}, onMessage);
|
|
57
|
-
}
|
|
58
|
-
else {
|
|
59
|
-
const containerName = getContainerName(workspaceName);
|
|
60
|
-
connection.session = createChatSession({
|
|
61
|
-
containerName,
|
|
62
|
-
workDir: '/home/workspace',
|
|
63
|
-
sessionId: message.sessionId,
|
|
64
|
-
model,
|
|
65
|
-
}, onMessage);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
await connection.session.sendMessage(message.content);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
catch (err) {
|
|
72
|
-
ws.send(JSON.stringify({
|
|
73
|
-
type: 'error',
|
|
74
|
-
content: err.message,
|
|
75
|
-
timestamp: new Date().toISOString(),
|
|
76
|
-
}));
|
|
77
|
-
}
|
|
78
|
-
});
|
|
79
|
-
ws.on('close', () => {
|
|
80
|
-
const conn = this.connections.get(ws);
|
|
81
|
-
if (conn?.session) {
|
|
82
|
-
conn.session.interrupt().catch(() => { });
|
|
83
|
-
}
|
|
84
|
-
this.connections.delete(ws);
|
|
85
|
-
});
|
|
86
|
-
ws.on('error', (err) => {
|
|
87
|
-
console.error('Chat WebSocket error:', err);
|
|
88
|
-
const conn = this.connections.get(ws);
|
|
89
|
-
if (conn?.session) {
|
|
90
|
-
conn.session.interrupt().catch(() => { });
|
|
91
|
-
}
|
|
92
|
-
this.connections.delete(ws);
|
|
93
|
-
});
|
|
94
17
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
18
|
+
createHostSession(sessionId, onMessage) {
|
|
19
|
+
const config = this.getConfig();
|
|
20
|
+
const model = config.agents?.claude_code?.model;
|
|
21
|
+
return createHostChatSession({ sessionId, model }, onMessage);
|
|
22
|
+
}
|
|
23
|
+
createContainerSession(containerName, sessionId, onMessage) {
|
|
24
|
+
const config = this.getConfig();
|
|
25
|
+
const model = config.agents?.claude_code?.model;
|
|
26
|
+
return createChatSession({
|
|
27
|
+
containerName,
|
|
28
|
+
workDir: '/home/workspace',
|
|
29
|
+
sessionId,
|
|
30
|
+
model,
|
|
31
|
+
}, onMessage);
|
|
99
32
|
}
|
|
100
33
|
}
|
package/dist/client/ws-shell.js
CHANGED
|
@@ -54,9 +54,21 @@ export async function openWSShell(options) {
|
|
|
54
54
|
let connected = false;
|
|
55
55
|
const stdin = process.stdin;
|
|
56
56
|
const stdout = process.stdout;
|
|
57
|
+
const safeSend = (data) => {
|
|
58
|
+
if (ws.readyState !== WebSocket.OPEN) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
ws.send(data);
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
57
69
|
const sendResize = () => {
|
|
58
|
-
if (
|
|
59
|
-
|
|
70
|
+
if (stdout.columns && stdout.rows) {
|
|
71
|
+
safeSend(JSON.stringify({ type: 'resize', cols: stdout.columns, rows: stdout.rows }));
|
|
60
72
|
}
|
|
61
73
|
};
|
|
62
74
|
ws.on('open', () => {
|
|
@@ -93,9 +105,7 @@ export async function openWSShell(options) {
|
|
|
93
105
|
}
|
|
94
106
|
});
|
|
95
107
|
stdin.on('data', (data) => {
|
|
96
|
-
|
|
97
|
-
ws.send(data);
|
|
98
|
-
}
|
|
108
|
+
safeSend(data);
|
|
99
109
|
});
|
|
100
110
|
stdout.on('resize', sendResize);
|
|
101
111
|
const cleanup = () => {
|
package/dist/docker/index.js
CHANGED
|
@@ -188,6 +188,24 @@ export async function createContainer(options) {
|
|
|
188
188
|
export async function startContainer(name) {
|
|
189
189
|
await docker(['start', name]);
|
|
190
190
|
}
|
|
191
|
+
export async function waitForContainerReady(name, options = {}) {
|
|
192
|
+
const timeout = options.timeout ?? 30000;
|
|
193
|
+
const interval = options.interval ?? 100;
|
|
194
|
+
const startTime = Date.now();
|
|
195
|
+
while (Date.now() - startTime < timeout) {
|
|
196
|
+
try {
|
|
197
|
+
const result = await execInContainer(name, ['true']);
|
|
198
|
+
if (result.exitCode === 0) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
// Container not ready yet
|
|
204
|
+
}
|
|
205
|
+
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
206
|
+
}
|
|
207
|
+
throw new Error(`Container '${name}' did not become ready within ${timeout}ms`);
|
|
208
|
+
}
|
|
191
209
|
export async function stopContainer(name, timeout = 10) {
|
|
192
210
|
await docker(['stop', '-t', String(timeout), name]);
|
|
193
211
|
}
|
|
@@ -335,7 +353,29 @@ export async function imageExists(tag) {
|
|
|
335
353
|
}
|
|
336
354
|
}
|
|
337
355
|
export async function pullImage(tag) {
|
|
338
|
-
|
|
356
|
+
return new Promise((resolve, reject) => {
|
|
357
|
+
const child = spawn('docker', ['pull', tag], {
|
|
358
|
+
stdio: ['ignore', 'inherit', 'inherit'],
|
|
359
|
+
});
|
|
360
|
+
child.on('error', reject);
|
|
361
|
+
child.on('close', (code) => {
|
|
362
|
+
if (code === 0) {
|
|
363
|
+
resolve();
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
reject(new Error(`Failed to pull image ${tag}`));
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
export async function tryPullImage(tag) {
|
|
372
|
+
try {
|
|
373
|
+
await pullImage(tag);
|
|
374
|
+
return true;
|
|
375
|
+
}
|
|
376
|
+
catch {
|
|
377
|
+
return false;
|
|
378
|
+
}
|
|
339
379
|
}
|
|
340
380
|
export async function buildImage(tag, context, options = {}) {
|
|
341
381
|
const args = ['build', '-t', tag];
|
package/dist/index.js
CHANGED
|
@@ -11,7 +11,7 @@ import { startProxy, parsePortForward, formatPortForwards } from './client/proxy
|
|
|
11
11
|
import { startDockerProxy, parsePortForward as parseDockerPortForward, formatPortForwards as formatDockerPortForwards, } from './client/docker-proxy';
|
|
12
12
|
import { loadAgentConfig, getConfigDir, ensureConfigDir } from './config/loader';
|
|
13
13
|
import { buildImage } from './docker';
|
|
14
|
-
import { DEFAULT_AGENT_PORT,
|
|
14
|
+
import { DEFAULT_AGENT_PORT, WORKSPACE_IMAGE_LOCAL } from './shared/constants';
|
|
15
15
|
const program = new Command();
|
|
16
16
|
program
|
|
17
17
|
.name('perry')
|
|
@@ -430,9 +430,9 @@ program
|
|
|
430
430
|
.option('--no-cache', 'Build without cache')
|
|
431
431
|
.action(async (options) => {
|
|
432
432
|
const buildContext = './perry';
|
|
433
|
-
console.log(`Building workspace image ${
|
|
433
|
+
console.log(`Building workspace image ${WORKSPACE_IMAGE_LOCAL}...`);
|
|
434
434
|
try {
|
|
435
|
-
await buildImage(
|
|
435
|
+
await buildImage(WORKSPACE_IMAGE_LOCAL, buildContext, {
|
|
436
436
|
noCache: options.noCache === false ? false : !options.cache,
|
|
437
437
|
});
|
|
438
438
|
console.log('Build complete.');
|
|
@@ -452,6 +452,21 @@ function handleError(err) {
|
|
|
452
452
|
else if (err instanceof Error) {
|
|
453
453
|
console.error(`Error: ${err.message}`);
|
|
454
454
|
}
|
|
455
|
+
else if (err && typeof err === 'object') {
|
|
456
|
+
const errObj = err;
|
|
457
|
+
if ('message' in errObj && typeof errObj.message === 'string') {
|
|
458
|
+
console.error(`Error: ${errObj.message}`);
|
|
459
|
+
}
|
|
460
|
+
else if ('code' in errObj) {
|
|
461
|
+
console.error(`Error: ${String(errObj.code)}`);
|
|
462
|
+
}
|
|
463
|
+
else {
|
|
464
|
+
console.error(`Error: ${JSON.stringify(err)}`);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
else if (err !== undefined && err !== null) {
|
|
468
|
+
console.error(`Error: ${String(err)}`);
|
|
469
|
+
}
|
|
455
470
|
else {
|
|
456
471
|
console.error('An unknown error occurred');
|
|
457
472
|
}
|