@gricha/perry 0.0.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.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +153 -0
  3. package/dist/agent/index.js +6 -0
  4. package/dist/agent/router.js +1017 -0
  5. package/dist/agent/run.js +182 -0
  6. package/dist/agent/static.js +58 -0
  7. package/dist/agent/systemd.js +229 -0
  8. package/dist/agent/web/assets/index-9t2sFIJM.js +101 -0
  9. package/dist/agent/web/assets/index-CCFpTruF.css +1 -0
  10. package/dist/agent/web/index.html +14 -0
  11. package/dist/agent/web/vite.svg +1 -0
  12. package/dist/chat/handler.js +174 -0
  13. package/dist/chat/host-handler.js +170 -0
  14. package/dist/chat/host-opencode-handler.js +169 -0
  15. package/dist/chat/index.js +2 -0
  16. package/dist/chat/opencode-handler.js +177 -0
  17. package/dist/chat/opencode-websocket.js +95 -0
  18. package/dist/chat/websocket.js +100 -0
  19. package/dist/client/api.js +138 -0
  20. package/dist/client/config.js +34 -0
  21. package/dist/client/docker-proxy.js +103 -0
  22. package/dist/client/index.js +4 -0
  23. package/dist/client/proxy.js +96 -0
  24. package/dist/client/shell.js +71 -0
  25. package/dist/client/ws-shell.js +120 -0
  26. package/dist/config/loader.js +59 -0
  27. package/dist/docker/index.js +372 -0
  28. package/dist/docker/types.js +1 -0
  29. package/dist/index.js +475 -0
  30. package/dist/sessions/index.js +2 -0
  31. package/dist/sessions/metadata.js +55 -0
  32. package/dist/sessions/parser.js +553 -0
  33. package/dist/sessions/types.js +1 -0
  34. package/dist/shared/base-websocket.js +51 -0
  35. package/dist/shared/client-types.js +1 -0
  36. package/dist/shared/constants.js +11 -0
  37. package/dist/shared/types.js +5 -0
  38. package/dist/terminal/handler.js +86 -0
  39. package/dist/terminal/host-handler.js +76 -0
  40. package/dist/terminal/index.js +3 -0
  41. package/dist/terminal/types.js +8 -0
  42. package/dist/terminal/websocket.js +115 -0
  43. package/dist/workspace/index.js +3 -0
  44. package/dist/workspace/manager.js +475 -0
  45. package/dist/workspace/state.js +66 -0
  46. package/dist/workspace/types.js +1 -0
  47. package/package.json +68 -0
@@ -0,0 +1,553 @@
1
+ import { readdir, readFile, stat } from 'fs/promises';
2
+ import { join, basename } from 'path';
3
+ function decodeProjectPath(encoded) {
4
+ return encoded.replace(/-/g, '/');
5
+ }
6
+ function extractContent(content) {
7
+ if (!content)
8
+ return null;
9
+ if (typeof content === 'string')
10
+ return content;
11
+ if (Array.isArray(content)) {
12
+ const textParts = content.filter((c) => c.type === 'text' && c.text).map((c) => c.text);
13
+ return textParts.join('\n') || null;
14
+ }
15
+ return null;
16
+ }
17
+ function extractInterleavedContent(content) {
18
+ const messages = [];
19
+ for (const part of content) {
20
+ if (part.type === 'text' && 'text' in part && part.text) {
21
+ messages.push({
22
+ type: 'assistant',
23
+ content: part.text,
24
+ });
25
+ }
26
+ else if (part.type === 'tool_use' && 'name' in part && 'id' in part) {
27
+ const toolPart = part;
28
+ messages.push({
29
+ type: 'tool_use',
30
+ toolName: toolPart.name,
31
+ toolId: toolPart.id,
32
+ toolInput: JSON.stringify(toolPart.input, null, 2),
33
+ });
34
+ }
35
+ else if (part.type === 'tool_result' && 'tool_use_id' in part) {
36
+ const resultPart = part;
37
+ let resultContent;
38
+ if (typeof resultPart.content === 'string') {
39
+ resultContent = resultPart.content;
40
+ }
41
+ else if (Array.isArray(resultPart.content)) {
42
+ resultContent = resultPart.content
43
+ .filter((c) => c.type === 'text' && c.text)
44
+ .map((c) => c.text)
45
+ .join('\n');
46
+ }
47
+ messages.push({
48
+ type: 'tool_result',
49
+ toolId: resultPart.tool_use_id,
50
+ content: resultContent,
51
+ });
52
+ }
53
+ }
54
+ return messages;
55
+ }
56
+ function parseJsonlLine(line) {
57
+ try {
58
+ const obj = JSON.parse(line);
59
+ if (obj.isMeta) {
60
+ return [];
61
+ }
62
+ const messages = [];
63
+ const timestamp = obj.timestamp || (obj.ts ? new Date(obj.ts * 1000).toISOString() : undefined);
64
+ if (obj.type === 'user' || obj.role === 'user') {
65
+ const rawContent = obj.content || obj.message?.content;
66
+ if (Array.isArray(rawContent)) {
67
+ const hasToolResults = rawContent.some((p) => p.type === 'tool_result');
68
+ if (hasToolResults) {
69
+ const interleaved = extractInterleavedContent(rawContent);
70
+ for (const msg of interleaved) {
71
+ msg.timestamp = timestamp;
72
+ messages.push(msg);
73
+ }
74
+ }
75
+ else {
76
+ const content = extractContent(rawContent);
77
+ messages.push({
78
+ type: 'user',
79
+ content: content || undefined,
80
+ timestamp,
81
+ });
82
+ }
83
+ }
84
+ else {
85
+ const content = extractContent(rawContent);
86
+ messages.push({
87
+ type: 'user',
88
+ content: content || undefined,
89
+ timestamp,
90
+ });
91
+ }
92
+ }
93
+ else if (obj.type === 'assistant' || obj.role === 'assistant') {
94
+ const rawContent = obj.content || obj.message?.content;
95
+ if (Array.isArray(rawContent)) {
96
+ const interleaved = extractInterleavedContent(rawContent);
97
+ for (const msg of interleaved) {
98
+ msg.timestamp = timestamp;
99
+ messages.push(msg);
100
+ }
101
+ }
102
+ else {
103
+ const textContent = extractContent(rawContent);
104
+ if (textContent) {
105
+ messages.push({
106
+ type: 'assistant',
107
+ content: textContent,
108
+ timestamp,
109
+ });
110
+ }
111
+ }
112
+ }
113
+ else if (obj.type === 'result') {
114
+ if (obj.subtype === 'success' ||
115
+ obj.subtype === 'error_max_turns' ||
116
+ obj.subtype === 'error_during_execution') {
117
+ const summary = obj.subtype === 'success'
118
+ ? `Session completed (${obj.num_turns || 0} turns, $${(obj.cost_usd || 0).toFixed(4)})`
119
+ : `Session ended: ${obj.subtype}`;
120
+ messages.push({
121
+ type: 'system',
122
+ content: summary,
123
+ timestamp,
124
+ });
125
+ }
126
+ }
127
+ else if (obj.type === 'system' && obj.subtype !== 'init') {
128
+ messages.push({
129
+ type: 'system',
130
+ content: extractContent(obj.content) || undefined,
131
+ timestamp: obj.timestamp,
132
+ });
133
+ }
134
+ return messages;
135
+ }
136
+ catch {
137
+ return [];
138
+ }
139
+ }
140
+ export function parseClaudeSessionContent(content) {
141
+ const lines = content.split('\n').filter((line) => line.trim());
142
+ const messages = [];
143
+ for (const line of lines) {
144
+ const lineMessages = parseJsonlLine(line);
145
+ messages.push(...lineMessages);
146
+ }
147
+ return messages;
148
+ }
149
+ export async function parseSessionFile(filePath) {
150
+ const content = await readFile(filePath, 'utf-8');
151
+ return parseClaudeSessionContent(content);
152
+ }
153
+ export async function getSessionMetadata(filePath, agentType) {
154
+ try {
155
+ const fileName = basename(filePath, '.jsonl');
156
+ const dirName = basename(join(filePath, '..'));
157
+ const projectPath = decodeProjectPath(dirName);
158
+ const fileStat = await stat(filePath);
159
+ const messages = await parseSessionFile(filePath);
160
+ const userMessages = messages.filter((m) => m.type === 'user');
161
+ const firstPrompt = userMessages.length > 0 ? userMessages[0].content || null : null;
162
+ let sessionName = null;
163
+ const content = await readFile(filePath, 'utf-8');
164
+ const lines = content.split('\n').filter((l) => l.trim());
165
+ for (const line of lines) {
166
+ try {
167
+ const obj = JSON.parse(line);
168
+ if (obj.type === 'system' && obj.subtype === 'session_name') {
169
+ sessionName = obj.name || null;
170
+ break;
171
+ }
172
+ }
173
+ catch {
174
+ continue;
175
+ }
176
+ }
177
+ return {
178
+ id: fileName,
179
+ name: sessionName,
180
+ agentType,
181
+ projectPath,
182
+ messageCount: messages.length,
183
+ lastActivity: fileStat.mtime.toISOString(),
184
+ firstPrompt: firstPrompt ? firstPrompt.slice(0, 200) : null,
185
+ filePath,
186
+ };
187
+ }
188
+ catch (err) {
189
+ console.error(`[sessions] Failed to get metadata for ${filePath}:`, err);
190
+ return null;
191
+ }
192
+ }
193
+ export async function listClaudeCodeSessions(homeDir) {
194
+ const claudeDir = join(homeDir, '.claude', 'projects');
195
+ const sessions = [];
196
+ try {
197
+ const projectDirs = await readdir(claudeDir);
198
+ for (const projectDir of projectDirs) {
199
+ const projectPath = join(claudeDir, projectDir);
200
+ const projectStat = await stat(projectPath);
201
+ if (!projectStat.isDirectory())
202
+ continue;
203
+ const files = await readdir(projectPath);
204
+ const jsonlFiles = files.filter((f) => f.endsWith('.jsonl'));
205
+ for (const file of jsonlFiles) {
206
+ const filePath = join(projectPath, file);
207
+ const metadata = await getSessionMetadata(filePath, 'claude-code');
208
+ if (metadata) {
209
+ sessions.push(metadata);
210
+ }
211
+ }
212
+ }
213
+ sessions.sort((a, b) => new Date(b.lastActivity).getTime() - new Date(a.lastActivity).getTime());
214
+ return sessions;
215
+ }
216
+ catch (err) {
217
+ console.error(`[sessions] Failed to list Claude Code sessions:`, err);
218
+ return [];
219
+ }
220
+ }
221
+ export async function getSessionDetail(sessionId, homeDir, agentType) {
222
+ if (!agentType || agentType === 'claude-code') {
223
+ const result = await getClaudeCodeSessionDetail(sessionId, homeDir);
224
+ if (result)
225
+ return result;
226
+ }
227
+ if (!agentType || agentType === 'opencode') {
228
+ const result = await getOpenCodeSessionDetail(sessionId, homeDir);
229
+ if (result)
230
+ return result;
231
+ }
232
+ if (!agentType || agentType === 'codex') {
233
+ const result = await getCodexSessionDetail(sessionId, homeDir);
234
+ if (result)
235
+ return result;
236
+ }
237
+ return null;
238
+ }
239
+ async function getClaudeCodeSessionDetail(sessionId, homeDir) {
240
+ const claudeDir = join(homeDir, '.claude', 'projects');
241
+ try {
242
+ const projectDirs = await readdir(claudeDir);
243
+ for (const projectDir of projectDirs) {
244
+ const projectPath = join(claudeDir, projectDir);
245
+ const projectStat = await stat(projectPath);
246
+ if (!projectStat.isDirectory())
247
+ continue;
248
+ const filePath = join(projectPath, `${sessionId}.jsonl`);
249
+ try {
250
+ await stat(filePath);
251
+ const metadata = await getSessionMetadata(filePath, 'claude-code');
252
+ if (!metadata)
253
+ return null;
254
+ const messages = await parseSessionFile(filePath);
255
+ return {
256
+ ...metadata,
257
+ messages,
258
+ };
259
+ }
260
+ catch {
261
+ continue;
262
+ }
263
+ }
264
+ return null;
265
+ }
266
+ catch (err) {
267
+ console.error(`[sessions] Failed to get Claude Code session ${sessionId}:`, err);
268
+ return null;
269
+ }
270
+ }
271
+ async function getMessageParts(partDir, messageId) {
272
+ const parts = [];
273
+ const msgPartDir = join(partDir, messageId);
274
+ try {
275
+ const files = await readdir(msgPartDir);
276
+ const partFiles = files.filter((f) => f.startsWith('prt_') && f.endsWith('.json'));
277
+ partFiles.sort();
278
+ for (const file of partFiles) {
279
+ try {
280
+ const content = await readFile(join(msgPartDir, file), 'utf-8');
281
+ const part = JSON.parse(content);
282
+ parts.push(part);
283
+ }
284
+ catch {
285
+ continue;
286
+ }
287
+ }
288
+ }
289
+ catch {
290
+ return [];
291
+ }
292
+ return parts;
293
+ }
294
+ async function parseOpenCodeMessages(messageDir, partDir) {
295
+ const messages = [];
296
+ try {
297
+ const files = await readdir(messageDir);
298
+ const msgFiles = files.filter((f) => f.startsWith('msg_') && f.endsWith('.json'));
299
+ msgFiles.sort();
300
+ for (const file of msgFiles) {
301
+ try {
302
+ const content = await readFile(join(messageDir, file), 'utf-8');
303
+ const msg = JSON.parse(content);
304
+ if (!msg.id || !msg.role)
305
+ continue;
306
+ if (msg.role !== 'user' && msg.role !== 'assistant')
307
+ continue;
308
+ const parts = await getMessageParts(partDir, msg.id);
309
+ const timestamp = msg.time?.created ? new Date(msg.time.created).toISOString() : undefined;
310
+ for (const part of parts) {
311
+ if (part.type === 'text' && part.text) {
312
+ messages.push({
313
+ type: msg.role,
314
+ content: part.text,
315
+ timestamp,
316
+ });
317
+ }
318
+ else if (part.type === 'tool' && part.tool) {
319
+ messages.push({
320
+ type: 'tool_use',
321
+ content: undefined,
322
+ toolName: part.state?.title || part.tool,
323
+ toolId: part.callID || part.id,
324
+ toolInput: JSON.stringify(part.state?.input, null, 2),
325
+ timestamp,
326
+ });
327
+ if (part.state?.output) {
328
+ messages.push({
329
+ type: 'tool_result',
330
+ content: part.state.output,
331
+ toolId: part.callID || part.id,
332
+ timestamp,
333
+ });
334
+ }
335
+ }
336
+ }
337
+ }
338
+ catch {
339
+ continue;
340
+ }
341
+ }
342
+ }
343
+ catch (err) {
344
+ console.error(`[sessions] Failed to parse OpenCode messages in ${messageDir}:`, err);
345
+ return [];
346
+ }
347
+ return messages;
348
+ }
349
+ export async function listOpenCodeSessions(homeDir) {
350
+ const openCodeDir = join(homeDir, '.local', 'share', 'opencode', 'storage');
351
+ const sessions = [];
352
+ try {
353
+ const sessionDir = join(openCodeDir, 'session');
354
+ const projectDirs = await readdir(sessionDir);
355
+ for (const projectDir of projectDirs) {
356
+ const projectPath = join(sessionDir, projectDir);
357
+ const projectStat = await stat(projectPath);
358
+ if (!projectStat.isDirectory())
359
+ continue;
360
+ const files = await readdir(projectPath);
361
+ const sessionFiles = files.filter((f) => f.startsWith('ses_') && f.endsWith('.json'));
362
+ for (const file of sessionFiles) {
363
+ const filePath = join(projectPath, file);
364
+ try {
365
+ const content = await readFile(filePath, 'utf-8');
366
+ const session = JSON.parse(content);
367
+ const fileStat = await stat(filePath);
368
+ const messageDir = join(openCodeDir, 'message', session.id);
369
+ const partDir = join(openCodeDir, 'part');
370
+ const messages = await parseOpenCodeMessages(messageDir, partDir);
371
+ const userMessages = messages.filter((m) => m.type === 'user');
372
+ const firstPrompt = userMessages.length > 0 ? userMessages[0].content || null : null;
373
+ sessions.push({
374
+ id: session.id,
375
+ name: session.title || null,
376
+ agentType: 'opencode',
377
+ projectPath: session.directory || projectDir,
378
+ messageCount: messages.length,
379
+ lastActivity: session.time?.updated
380
+ ? new Date(session.time.updated).toISOString()
381
+ : fileStat.mtime.toISOString(),
382
+ firstPrompt: firstPrompt ? firstPrompt.slice(0, 200) : null,
383
+ filePath,
384
+ });
385
+ }
386
+ catch {
387
+ continue;
388
+ }
389
+ }
390
+ }
391
+ sessions.sort((a, b) => new Date(b.lastActivity).getTime() - new Date(a.lastActivity).getTime());
392
+ return sessions;
393
+ }
394
+ catch (err) {
395
+ console.error(`[sessions] Failed to list OpenCode sessions:`, err);
396
+ return [];
397
+ }
398
+ }
399
+ async function getOpenCodeSessionDetail(sessionId, homeDir) {
400
+ const openCodeDir = join(homeDir, '.local', 'share', 'opencode', 'storage');
401
+ try {
402
+ const sessionDir = join(openCodeDir, 'session');
403
+ const projectDirs = await readdir(sessionDir);
404
+ for (const projectDir of projectDirs) {
405
+ const projectPath = join(sessionDir, projectDir);
406
+ const filePath = join(projectPath, `${sessionId}.json`);
407
+ try {
408
+ const content = await readFile(filePath, 'utf-8');
409
+ const session = JSON.parse(content);
410
+ const fileStat = await stat(filePath);
411
+ const messageDir = join(openCodeDir, 'message', sessionId);
412
+ const partDir = join(openCodeDir, 'part');
413
+ const messages = await parseOpenCodeMessages(messageDir, partDir);
414
+ const userMessages = messages.filter((m) => m.type === 'user');
415
+ const firstPrompt = userMessages.length > 0 ? userMessages[0].content || null : null;
416
+ return {
417
+ id: session.id,
418
+ name: session.title || null,
419
+ agentType: 'opencode',
420
+ projectPath: session.directory || projectDir,
421
+ messageCount: messages.length,
422
+ lastActivity: session.time?.updated
423
+ ? new Date(session.time.updated).toISOString()
424
+ : fileStat.mtime.toISOString(),
425
+ firstPrompt: firstPrompt ? firstPrompt.slice(0, 200) : null,
426
+ filePath,
427
+ messages,
428
+ };
429
+ }
430
+ catch {
431
+ continue;
432
+ }
433
+ }
434
+ return null;
435
+ }
436
+ catch (err) {
437
+ console.error(`[sessions] Failed to get OpenCode session ${sessionId}:`, err);
438
+ return null;
439
+ }
440
+ }
441
+ function parseCodexLine(line) {
442
+ try {
443
+ const obj = JSON.parse(line);
444
+ const role = obj.payload?.role || obj.payload?.message?.role;
445
+ const content = obj.payload?.content || obj.payload?.message?.content;
446
+ if (role === 'user' || role === 'assistant') {
447
+ const textContent = extractContent(content);
448
+ return {
449
+ type: role,
450
+ content: textContent || undefined,
451
+ timestamp: obj.timestamp ? new Date(obj.timestamp).toISOString() : undefined,
452
+ };
453
+ }
454
+ return null;
455
+ }
456
+ catch {
457
+ return null;
458
+ }
459
+ }
460
+ async function parseCodexSessionFile(filePath) {
461
+ try {
462
+ const content = await readFile(filePath, 'utf-8');
463
+ const lines = content.split('\n').filter((line) => line.trim());
464
+ const messages = [];
465
+ let meta = null;
466
+ for (let i = 0; i < lines.length; i++) {
467
+ const line = lines[i];
468
+ if (i === 0) {
469
+ try {
470
+ meta = JSON.parse(line);
471
+ }
472
+ catch {
473
+ meta = null;
474
+ }
475
+ continue;
476
+ }
477
+ const msg = parseCodexLine(line);
478
+ if (msg) {
479
+ messages.push(msg);
480
+ }
481
+ }
482
+ return { meta, messages };
483
+ }
484
+ catch (err) {
485
+ console.error(`[sessions] Failed to parse Codex session file ${filePath}:`, err);
486
+ return { meta: null, messages: [] };
487
+ }
488
+ }
489
+ export async function listCodexSessions(homeDir) {
490
+ const codexDir = join(homeDir, '.codex', 'sessions');
491
+ const sessions = [];
492
+ async function scanDirectory(dir) {
493
+ try {
494
+ const entries = await readdir(dir);
495
+ for (const entry of entries) {
496
+ const entryPath = join(dir, entry);
497
+ const entryStat = await stat(entryPath);
498
+ if (entryStat.isDirectory()) {
499
+ await scanDirectory(entryPath);
500
+ }
501
+ else if (entry.startsWith('rollout-') && entry.endsWith('.jsonl')) {
502
+ try {
503
+ const { meta, messages } = await parseCodexSessionFile(entryPath);
504
+ const userMessages = messages.filter((m) => m.type === 'user');
505
+ const firstPrompt = userMessages.length > 0 ? userMessages[0].content || null : null;
506
+ const sessionId = meta?.session_id || basename(entry, '.jsonl');
507
+ sessions.push({
508
+ id: sessionId,
509
+ name: null,
510
+ agentType: 'codex',
511
+ projectPath: dir.replace(codexDir, '').replace(/^\//, '') || 'unknown',
512
+ messageCount: messages.length,
513
+ lastActivity: entryStat.mtime.toISOString(),
514
+ firstPrompt: firstPrompt ? firstPrompt.slice(0, 200) : null,
515
+ filePath: entryPath,
516
+ });
517
+ }
518
+ catch {
519
+ continue;
520
+ }
521
+ }
522
+ }
523
+ }
524
+ catch (err) {
525
+ console.error(`[sessions] Failed to scan Codex directory ${dir}:`, err);
526
+ return;
527
+ }
528
+ }
529
+ await scanDirectory(codexDir);
530
+ sessions.sort((a, b) => new Date(b.lastActivity).getTime() - new Date(a.lastActivity).getTime());
531
+ return sessions;
532
+ }
533
+ async function getCodexSessionDetail(sessionId, homeDir) {
534
+ const sessions = await listCodexSessions(homeDir);
535
+ const session = sessions.find((s) => s.id === sessionId);
536
+ if (!session)
537
+ return null;
538
+ const { messages } = await parseCodexSessionFile(session.filePath);
539
+ return {
540
+ ...session,
541
+ messages,
542
+ };
543
+ }
544
+ export async function listAllSessions(homeDir) {
545
+ const [claudeSessions, openCodeSessions, codexSessions] = await Promise.all([
546
+ listClaudeCodeSessions(homeDir),
547
+ listOpenCodeSessions(homeDir),
548
+ listCodexSessions(homeDir),
549
+ ]);
550
+ const allSessions = [...claudeSessions, ...openCodeSessions, ...codexSessions];
551
+ allSessions.sort((a, b) => new Date(b.lastActivity).getTime() - new Date(a.lastActivity).getTime());
552
+ return allSessions;
553
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,51 @@
1
+ import { WebSocketServer } from 'ws';
2
+ export class BaseWebSocketServer {
3
+ wss;
4
+ connections = new Map();
5
+ isWorkspaceRunning;
6
+ constructor(options) {
7
+ this.wss = new WebSocketServer({ noServer: true });
8
+ this.isWorkspaceRunning = options.isWorkspaceRunning;
9
+ this.wss.on('connection', this.onConnection.bind(this));
10
+ }
11
+ async handleUpgrade(request, socket, head, workspaceName) {
12
+ const running = await this.isWorkspaceRunning(workspaceName);
13
+ if (!running) {
14
+ socket.write('HTTP/1.1 404 Not Found\r\n\r\n');
15
+ socket.end();
16
+ return;
17
+ }
18
+ this.wss.handleUpgrade(request, socket, head, (ws) => {
19
+ ws.workspaceName = workspaceName;
20
+ this.wss.emit('connection', ws, request);
21
+ });
22
+ }
23
+ onConnection(ws) {
24
+ const workspaceName = ws.workspaceName;
25
+ if (!workspaceName) {
26
+ ws.close(1008, 'Missing workspace name');
27
+ return;
28
+ }
29
+ this.handleConnection(ws, workspaceName);
30
+ }
31
+ getConnectionCount() {
32
+ return this.connections.size;
33
+ }
34
+ closeConnectionsForWorkspace(workspaceName) {
35
+ for (const [ws, conn] of this.connections.entries()) {
36
+ if (conn.workspaceName === workspaceName) {
37
+ this.cleanupConnection(conn);
38
+ ws.close(1001, 'Workspace stopped');
39
+ this.connections.delete(ws);
40
+ }
41
+ }
42
+ }
43
+ close() {
44
+ for (const [ws, conn] of this.connections.entries()) {
45
+ this.cleanupConnection(conn);
46
+ ws.close(1001, 'Server shutting down');
47
+ }
48
+ this.connections.clear();
49
+ this.wss.close();
50
+ }
51
+ }
@@ -0,0 +1 @@
1
+ export const HOST_WORKSPACE_NAME = '@host';
@@ -0,0 +1,11 @@
1
+ export const DEFAULT_AGENT_PORT = 7391;
2
+ export const SSH_PORT_RANGE_START = 2200;
3
+ export const SSH_PORT_RANGE_END = 2400;
4
+ export const WORKSPACE_IMAGE = 'workspace:latest';
5
+ export const VOLUME_PREFIX = 'workspace-';
6
+ export const CONTAINER_PREFIX = 'workspace-';
7
+ export const AGENT_SESSION_PATHS = {
8
+ claudeCode: '.claude/projects',
9
+ opencode: '.local/share/opencode/storage',
10
+ codex: '.codex/sessions',
11
+ };
@@ -0,0 +1,5 @@
1
+ export const HOST_WORKSPACE_NAME = '@host';
2
+ export const DEFAULT_CONFIG_DIR = process.env.PERRY_CONFIG_DIR || `${process.env.HOME}/.config/perry`;
3
+ export const STATE_FILE = 'state.json';
4
+ export const CONFIG_FILE = 'config.json';
5
+ export const CLIENT_CONFIG_FILE = 'client.json';