@gricha/perry 0.3.10 → 0.3.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/file-watcher.js +13 -9
- package/dist/agent/run.js +27 -15
- package/dist/agents/index.js +0 -19
- package/dist/index.js +1 -1
- package/dist/perry-worker +0 -0
- package/dist/session-manager/adapters/opencode.js +34 -5
- package/dist/session-manager/bun-handler.js +1 -1
- package/dist/session-manager/manager.js +26 -3
- package/dist/session-manager/websocket.js +5 -5
- package/dist/sessions/registry.js +19 -0
- package/dist/worker/server.js +13 -9
- package/package.json +3 -2
|
@@ -14,7 +14,9 @@ export class FileWatcher {
|
|
|
14
14
|
this.config = options.config;
|
|
15
15
|
this.syncCallback = options.syncCallback;
|
|
16
16
|
this.debounceMs = options.debounceMs ?? 500;
|
|
17
|
-
this.setupWatchers()
|
|
17
|
+
this.setupWatchers().catch((err) => {
|
|
18
|
+
console.error('[file-watcher] Failed to setup watchers:', err);
|
|
19
|
+
});
|
|
18
20
|
}
|
|
19
21
|
updateConfig(config) {
|
|
20
22
|
this.config = config;
|
|
@@ -97,18 +99,18 @@ export class FileWatcher {
|
|
|
97
99
|
clearTimeout(this.debounceTimer);
|
|
98
100
|
}
|
|
99
101
|
this.pendingSync = true;
|
|
100
|
-
this.debounceTimer = setTimeout(
|
|
102
|
+
this.debounceTimer = setTimeout(() => {
|
|
101
103
|
this.debounceTimer = null;
|
|
102
104
|
if (this.pendingSync) {
|
|
103
105
|
this.pendingSync = false;
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
106
|
+
console.log('[file-watcher] Triggering sync...');
|
|
107
|
+
this.syncCallback()
|
|
108
|
+
.then(() => {
|
|
107
109
|
console.log('[file-watcher] Sync completed');
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
+
})
|
|
111
|
+
.catch((err) => {
|
|
110
112
|
console.error('[file-watcher] Sync failed:', err);
|
|
111
|
-
}
|
|
113
|
+
});
|
|
112
114
|
}
|
|
113
115
|
}, this.debounceMs);
|
|
114
116
|
}
|
|
@@ -127,7 +129,9 @@ export class FileWatcher {
|
|
|
127
129
|
}
|
|
128
130
|
for (const filePath of newPaths) {
|
|
129
131
|
if (!currentPaths.has(filePath)) {
|
|
130
|
-
this.watchFile(filePath)
|
|
132
|
+
this.watchFile(filePath).catch((err) => {
|
|
133
|
+
console.error(`[file-watcher] Failed to watch ${filePath}:`, err);
|
|
134
|
+
});
|
|
131
135
|
}
|
|
132
136
|
}
|
|
133
137
|
}
|
package/dist/agent/run.js
CHANGED
|
@@ -182,10 +182,14 @@ function createAgentServer(configDir, config, port, tailscale) {
|
|
|
182
182
|
terminalHandler.handleMessage(ws, data);
|
|
183
183
|
}
|
|
184
184
|
else if (type === 'live-claude') {
|
|
185
|
-
liveClaudeHandler.handleMessage(ws, data)
|
|
185
|
+
liveClaudeHandler.handleMessage(ws, data).catch((err) => {
|
|
186
|
+
console.error('[ws] Error handling claude message:', err);
|
|
187
|
+
});
|
|
186
188
|
}
|
|
187
189
|
else if (type === 'live-opencode') {
|
|
188
|
-
liveOpencodeHandler.handleMessage(ws, data)
|
|
190
|
+
liveOpencodeHandler.handleMessage(ws, data).catch((err) => {
|
|
191
|
+
console.error('[ws] Error handling opencode message:', err);
|
|
192
|
+
});
|
|
189
193
|
}
|
|
190
194
|
},
|
|
191
195
|
close(ws, code, reason) {
|
|
@@ -310,9 +314,11 @@ export async function startAgent(options = {}) {
|
|
|
310
314
|
console.log(`[agent] WebSocket terminal: ws://localhost:${port}/rpc/terminal/:name`);
|
|
311
315
|
console.log(`[agent] WebSocket chat (Claude): ws://localhost:${port}/rpc/live/claude/:name`);
|
|
312
316
|
console.log(`[agent] WebSocket chat (OpenCode): ws://localhost:${port}/rpc/live/opencode/:name`);
|
|
313
|
-
startEagerImagePull()
|
|
317
|
+
startEagerImagePull().catch((err) => {
|
|
318
|
+
console.error('[agent] Error during image pull:', err);
|
|
319
|
+
});
|
|
314
320
|
let isShuttingDown = false;
|
|
315
|
-
const shutdown =
|
|
321
|
+
const shutdown = () => {
|
|
316
322
|
if (isShuttingDown) {
|
|
317
323
|
console.log('[agent] Force exit');
|
|
318
324
|
process.exit(0);
|
|
@@ -326,17 +332,23 @@ export async function startAgent(options = {}) {
|
|
|
326
332
|
forceExitTimeout.unref();
|
|
327
333
|
stopEagerImagePull();
|
|
328
334
|
fileWatcher.stop();
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
335
|
+
const cleanup = async () => {
|
|
336
|
+
if (tailscaleServeActive) {
|
|
337
|
+
console.log('[agent] Stopping Tailscale Serve...');
|
|
338
|
+
await stopTailscaleServe();
|
|
339
|
+
}
|
|
340
|
+
liveClaudeHandler.close();
|
|
341
|
+
liveOpencodeHandler.close();
|
|
342
|
+
terminalHandler.close();
|
|
343
|
+
await server.stop();
|
|
344
|
+
clearTimeout(forceExitTimeout);
|
|
345
|
+
console.log('[agent] Server closed');
|
|
346
|
+
process.exit(0);
|
|
347
|
+
};
|
|
348
|
+
cleanup().catch((err) => {
|
|
349
|
+
console.error('[agent] Shutdown error:', err);
|
|
350
|
+
process.exit(1);
|
|
351
|
+
});
|
|
340
352
|
};
|
|
341
353
|
process.on('SIGTERM', shutdown);
|
|
342
354
|
process.on('SIGINT', shutdown);
|
package/dist/agents/index.js
CHANGED
|
@@ -153,25 +153,6 @@ export async function syncAllAgents(containerName, agentConfig, copier) {
|
|
|
153
153
|
return results;
|
|
154
154
|
}
|
|
155
155
|
export function getCredentialFilePaths() {
|
|
156
|
-
const paths = [];
|
|
157
|
-
for (const agent of Object.values(agents)) {
|
|
158
|
-
const dummyContext = {
|
|
159
|
-
containerName: '',
|
|
160
|
-
agentConfig: { port: 0, credentials: { env: {}, files: {} }, scripts: {} },
|
|
161
|
-
hostFileExists: async () => false,
|
|
162
|
-
hostDirExists: async () => false,
|
|
163
|
-
readHostFile: async () => null,
|
|
164
|
-
readContainerFile: async () => null,
|
|
165
|
-
};
|
|
166
|
-
const filesPromise = agent.sync.getFilesToSync(dummyContext);
|
|
167
|
-
filesPromise.then((files) => {
|
|
168
|
-
for (const file of files) {
|
|
169
|
-
if (file.category === 'credential') {
|
|
170
|
-
paths.push(file.source);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
156
|
return ['~/.claude/.credentials.json', '~/.codex/auth.json'];
|
|
176
157
|
}
|
|
177
158
|
export { createDockerFileCopier, createMockFileCopier } from './sync/copier';
|
package/dist/index.js
CHANGED
package/dist/perry-worker
CHANGED
|
Binary file
|
|
@@ -13,6 +13,19 @@ async function findAvailablePort(containerName) {
|
|
|
13
13
|
});
|
|
14
14
|
return parseInt(result.stdout.trim(), 10);
|
|
15
15
|
}
|
|
16
|
+
async function findExistingServer(containerName) {
|
|
17
|
+
try {
|
|
18
|
+
const result = await execInContainer(containerName, ['sh', '-c', 'pgrep -a -f "opencode serve" | grep -oP "\\--port \\K[0-9]+" | head -1'], { user: 'workspace' });
|
|
19
|
+
const port = parseInt(result.stdout.trim(), 10);
|
|
20
|
+
if (port && (await isServerRunning(containerName, port))) {
|
|
21
|
+
return port;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
// No existing server
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
16
29
|
async function isServerRunning(containerName, port) {
|
|
17
30
|
try {
|
|
18
31
|
const result = await execInContainer(containerName, ['curl', '-s', '-o', '/dev/null', '-w', '%{http_code}', `http://localhost:${port}/session`], { user: 'workspace' });
|
|
@@ -34,8 +47,14 @@ async function getServerLogs(containerName) {
|
|
|
34
47
|
}
|
|
35
48
|
}
|
|
36
49
|
async function startServer(containerName) {
|
|
37
|
-
const
|
|
38
|
-
if (
|
|
50
|
+
const cached = serverPorts.get(containerName);
|
|
51
|
+
if (cached && (await isServerRunning(containerName, cached))) {
|
|
52
|
+
return cached;
|
|
53
|
+
}
|
|
54
|
+
const existing = await findExistingServer(containerName);
|
|
55
|
+
if (existing) {
|
|
56
|
+
console.log(`[opencode] Found existing server on port ${existing} in ${containerName}`);
|
|
57
|
+
serverPorts.set(containerName, existing);
|
|
39
58
|
return existing;
|
|
40
59
|
}
|
|
41
60
|
const starting = serverStarting.get(containerName);
|
|
@@ -148,7 +167,7 @@ export class OpenCodeAdapter {
|
|
|
148
167
|
fetch: () => new Response(''),
|
|
149
168
|
});
|
|
150
169
|
const port = server.port;
|
|
151
|
-
server.stop();
|
|
170
|
+
await server.stop();
|
|
152
171
|
return port;
|
|
153
172
|
}
|
|
154
173
|
async isServerRunningHost(port) {
|
|
@@ -176,6 +195,7 @@ export class OpenCodeAdapter {
|
|
|
176
195
|
if (!this.agentSessionId) {
|
|
177
196
|
this.agentSessionId = await this.createSession(baseUrl);
|
|
178
197
|
this.emit({ type: 'system', content: `Session: ${this.agentSessionId.slice(0, 8)}...` });
|
|
198
|
+
this.statusCallback?.(this.status);
|
|
179
199
|
}
|
|
180
200
|
this.setStatus('running');
|
|
181
201
|
this.emit({ type: 'system', content: 'Processing...' });
|
|
@@ -229,7 +249,10 @@ export class OpenCodeAdapter {
|
|
|
229
249
|
return session.id;
|
|
230
250
|
}
|
|
231
251
|
async sendAndStream(baseUrl, message) {
|
|
232
|
-
|
|
252
|
+
let sseError = null;
|
|
253
|
+
const sseReady = this.startSSEStream().catch((err) => {
|
|
254
|
+
sseError = err;
|
|
255
|
+
});
|
|
233
256
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
234
257
|
const payload = { parts: [{ type: 'text', text: message }] };
|
|
235
258
|
if (this.isHost) {
|
|
@@ -263,6 +286,9 @@ export class OpenCodeAdapter {
|
|
|
263
286
|
}
|
|
264
287
|
}
|
|
265
288
|
await sseReady;
|
|
289
|
+
if (sseError) {
|
|
290
|
+
throw sseError;
|
|
291
|
+
}
|
|
266
292
|
}
|
|
267
293
|
startSSEStream() {
|
|
268
294
|
return new Promise((resolve, reject) => {
|
|
@@ -284,6 +310,7 @@ export class OpenCodeAdapter {
|
|
|
284
310
|
this.sseProcess = proc;
|
|
285
311
|
const decoder = new TextDecoder();
|
|
286
312
|
let buffer = '';
|
|
313
|
+
let eventCount = 0;
|
|
287
314
|
const finish = () => {
|
|
288
315
|
if (!resolved) {
|
|
289
316
|
resolved = true;
|
|
@@ -316,7 +343,9 @@ export class OpenCodeAdapter {
|
|
|
316
343
|
continue;
|
|
317
344
|
try {
|
|
318
345
|
const event = JSON.parse(data);
|
|
346
|
+
eventCount++;
|
|
319
347
|
if (event.type === 'session.idle') {
|
|
348
|
+
console.log(`[opencode] SSE received session.idle after ${eventCount} events`);
|
|
320
349
|
receivedIdle = true;
|
|
321
350
|
clearTimeout(timeout);
|
|
322
351
|
proc.kill();
|
|
@@ -375,7 +404,7 @@ export class OpenCodeAdapter {
|
|
|
375
404
|
}
|
|
376
405
|
else if (!resolved) {
|
|
377
406
|
resolved = true;
|
|
378
|
-
reject(new Error(
|
|
407
|
+
reject(new Error(`SSE stream ended unexpectedly without session.idle (received ${eventCount} events)`));
|
|
379
408
|
}
|
|
380
409
|
})().catch((err) => {
|
|
381
410
|
clearTimeout(timeout);
|
|
@@ -90,7 +90,7 @@ export class LiveChatHandler {
|
|
|
90
90
|
const agentType = message.agentType || this.agentType;
|
|
91
91
|
if (message.sessionId) {
|
|
92
92
|
// Look up by internal sessionId or agentSessionId (Claude session ID)
|
|
93
|
-
const found = sessionManager.findSession(message.sessionId);
|
|
93
|
+
const found = await sessionManager.findSession(message.sessionId);
|
|
94
94
|
if (found) {
|
|
95
95
|
connection.sessionId = found.sessionId;
|
|
96
96
|
const sendFn = (msg) => {
|
|
@@ -241,18 +241,41 @@ export class SessionManager {
|
|
|
241
241
|
const session = this.sessions.get(sessionId);
|
|
242
242
|
return session?.info ?? null;
|
|
243
243
|
}
|
|
244
|
-
findSession(id) {
|
|
245
|
-
// First try direct lookup by internal sessionId
|
|
244
|
+
async findSession(id) {
|
|
245
|
+
// First try direct lookup by internal sessionId (in-memory cache)
|
|
246
246
|
const direct = this.sessions.get(id);
|
|
247
247
|
if (direct) {
|
|
248
248
|
return { sessionId: id, info: direct.info };
|
|
249
249
|
}
|
|
250
|
-
// Then search by agentSessionId
|
|
250
|
+
// Then search by agentSessionId in memory
|
|
251
251
|
for (const [sessionId, session] of this.sessions) {
|
|
252
252
|
if (session.info.agentSessionId === id) {
|
|
253
253
|
return { sessionId, info: session.info };
|
|
254
254
|
}
|
|
255
255
|
}
|
|
256
|
+
// Not in memory - check disk registry
|
|
257
|
+
if (this.stateDir) {
|
|
258
|
+
// Try lookup by perrySessionId first
|
|
259
|
+
let record = await registry.getSession(this.stateDir, id);
|
|
260
|
+
// Then try by agentSessionId
|
|
261
|
+
if (!record) {
|
|
262
|
+
record = await registry.findByAgentSessionId(this.stateDir, id);
|
|
263
|
+
}
|
|
264
|
+
// Found on disk - restore the session
|
|
265
|
+
if (record) {
|
|
266
|
+
const restoredId = await this.startSession({
|
|
267
|
+
sessionId: record.perrySessionId,
|
|
268
|
+
workspaceName: record.workspaceName,
|
|
269
|
+
agentType: record.agentType,
|
|
270
|
+
agentSessionId: record.agentSessionId ?? undefined,
|
|
271
|
+
projectPath: record.projectPath ?? undefined,
|
|
272
|
+
});
|
|
273
|
+
const restored = this.sessions.get(restoredId);
|
|
274
|
+
if (restored) {
|
|
275
|
+
return { sessionId: restoredId, info: restored.info };
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
256
279
|
return null;
|
|
257
280
|
}
|
|
258
281
|
getSessionStatus(sessionId) {
|
|
@@ -29,9 +29,9 @@ export class LiveChatWebSocketServer extends BaseWebSocketServer {
|
|
|
29
29
|
agentType: this.agentType,
|
|
30
30
|
timestamp: new Date().toISOString(),
|
|
31
31
|
}));
|
|
32
|
-
ws.on('message',
|
|
32
|
+
ws.on('message', (data) => {
|
|
33
33
|
const str = typeof data === 'string' ? data : data.toString();
|
|
34
|
-
|
|
34
|
+
const handleMessage = async () => {
|
|
35
35
|
const message = JSON.parse(str);
|
|
36
36
|
if (message.type === 'connect') {
|
|
37
37
|
await this.handleConnect(connection, ws, workspaceName, message);
|
|
@@ -50,14 +50,14 @@ export class LiveChatWebSocketServer extends BaseWebSocketServer {
|
|
|
50
50
|
if (message.type === 'message' && message.content) {
|
|
51
51
|
await this.handleMessage(connection, ws, workspaceName, message);
|
|
52
52
|
}
|
|
53
|
-
}
|
|
54
|
-
catch
|
|
53
|
+
};
|
|
54
|
+
handleMessage().catch((err) => {
|
|
55
55
|
safeSend(ws, JSON.stringify({
|
|
56
56
|
type: 'error',
|
|
57
57
|
content: err.message,
|
|
58
58
|
timestamp: new Date().toISOString(),
|
|
59
59
|
}));
|
|
60
|
-
}
|
|
60
|
+
});
|
|
61
61
|
});
|
|
62
62
|
ws.on('close', () => {
|
|
63
63
|
this.handleDisconnect(connection);
|
|
@@ -98,6 +98,25 @@ export async function getSessionsForWorkspace(stateDir, workspaceName) {
|
|
|
98
98
|
.filter((record) => record.workspaceName === workspaceName)
|
|
99
99
|
.sort((a, b) => new Date(b.lastActivity).getTime() - new Date(a.lastActivity).getTime());
|
|
100
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* Get a specific session by perrySessionId.
|
|
103
|
+
*/
|
|
104
|
+
export async function getSession(stateDir, perrySessionId) {
|
|
105
|
+
const registry = await loadRegistry(stateDir);
|
|
106
|
+
return registry.sessions[perrySessionId] ?? null;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Find a session by agentSessionId.
|
|
110
|
+
*/
|
|
111
|
+
export async function findByAgentSessionId(stateDir, agentSessionId) {
|
|
112
|
+
const registry = await loadRegistry(stateDir);
|
|
113
|
+
for (const record of Object.values(registry.sessions)) {
|
|
114
|
+
if (record.agentSessionId === agentSessionId) {
|
|
115
|
+
return record;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
101
120
|
/**
|
|
102
121
|
* Import an external session (discovered from agent storage).
|
|
103
122
|
* Creates a Perry session record for a session that wasn't started through Perry.
|
package/dist/worker/server.js
CHANGED
|
@@ -56,14 +56,18 @@ export async function startWorkerServer(options = {}) {
|
|
|
56
56
|
},
|
|
57
57
|
});
|
|
58
58
|
console.error(`Worker server listening on port ${server.port}`);
|
|
59
|
-
|
|
59
|
+
const shutdown = () => {
|
|
60
60
|
sessionIndex.stopWatchers();
|
|
61
|
-
server
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
61
|
+
server
|
|
62
|
+
.stop()
|
|
63
|
+
.then(() => {
|
|
64
|
+
process.exit(0);
|
|
65
|
+
})
|
|
66
|
+
.catch((err) => {
|
|
67
|
+
console.error('[worker] Shutdown error:', err);
|
|
68
|
+
process.exit(1);
|
|
69
|
+
});
|
|
70
|
+
};
|
|
71
|
+
process.on('SIGINT', shutdown);
|
|
72
|
+
process.on('SIGTERM', shutdown);
|
|
69
73
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gricha/perry",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.13",
|
|
4
4
|
"description": "Self-contained CLI for spinning up Docker-in-Docker development environments with SSH and proxy helpers.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"build:web": "cd web && bun run build",
|
|
18
18
|
"test": "vitest run",
|
|
19
19
|
"test:web": "playwright test",
|
|
20
|
-
"lint": "oxlint src/ mobile/src/",
|
|
20
|
+
"lint": "oxlint --type-aware --tsconfig=tsconfig.json src/ && oxlint mobile/src/",
|
|
21
21
|
"format:check": "oxfmt --check src/ test/",
|
|
22
22
|
"check": "bun run lint && bun run format:check && bun x tsc --noEmit",
|
|
23
23
|
"lint:web": "cd web && bun run lint",
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
"@types/ws": "^8.18.1",
|
|
46
46
|
"oxfmt": "^0.21.0",
|
|
47
47
|
"oxlint": "^1.36.0",
|
|
48
|
+
"oxlint-tsgolint": "^0.11.0",
|
|
48
49
|
"typescript": "^5.6.3",
|
|
49
50
|
"vitest": "^4.0.6"
|
|
50
51
|
},
|