@gricha/perry 0.3.0 → 0.3.2
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 +1 -0
- package/dist/client/docker-proxy.js +2 -16
- package/dist/client/port-forward.js +23 -0
- package/dist/client/proxy.js +2 -16
- package/dist/config/loader.js +2 -6
- package/dist/index.js +14 -15
- package/dist/perry-worker +0 -0
- package/dist/sessions/agents/utils.js +6 -2
- package/dist/sessions/parser.js +1 -11
- package/dist/shared/format-utils.js +15 -0
- package/dist/shared/path-utils.js +8 -0
- package/dist/ssh/sync.js +1 -8
- package/dist/update-checker.js +1 -4
- package/package.json +4 -7
- package/dist/chat/base-chat-websocket.js +0 -86
- package/dist/chat/base-claude-session.js +0 -215
- package/dist/chat/base-opencode-session.js +0 -181
- package/dist/chat/handler.js +0 -47
- package/dist/chat/host-handler.js +0 -41
- package/dist/chat/host-opencode-handler.js +0 -144
- package/dist/chat/index.js +0 -2
- package/dist/chat/opencode-handler.js +0 -100
- package/dist/chat/opencode-server.js +0 -502
- package/dist/chat/opencode-websocket.js +0 -31
- package/dist/chat/session-monitor.js +0 -186
- package/dist/chat/session-utils.js +0 -155
- package/dist/chat/websocket.js +0 -33
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared session monitoring and error handling for both Claude Code and OpenCode sessions.
|
|
3
|
-
*
|
|
4
|
-
* This module provides common functionality:
|
|
5
|
-
* - Activity/heartbeat monitoring
|
|
6
|
-
* - Timeout handling with user-friendly messages
|
|
7
|
-
* - Session state management
|
|
8
|
-
* - Error message formatting
|
|
9
|
-
*/
|
|
10
|
-
/**
|
|
11
|
-
* Monitors a session for timeouts and activity.
|
|
12
|
-
* Handles cleanup of timers automatically.
|
|
13
|
-
*/
|
|
14
|
-
export class SessionMonitor {
|
|
15
|
-
config;
|
|
16
|
-
callbacks;
|
|
17
|
-
operationTimer = null;
|
|
18
|
-
initialResponseTimer = null;
|
|
19
|
-
activityTimer = null;
|
|
20
|
-
lastActivity = 0;
|
|
21
|
-
started = false;
|
|
22
|
-
completed = false;
|
|
23
|
-
constructor(config, callbacks) {
|
|
24
|
-
this.config = config;
|
|
25
|
-
this.callbacks = callbacks;
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* Start monitoring the session
|
|
29
|
-
*/
|
|
30
|
-
start() {
|
|
31
|
-
if (this.started)
|
|
32
|
-
return;
|
|
33
|
-
this.started = true;
|
|
34
|
-
// Set up initial response timeout
|
|
35
|
-
this.initialResponseTimer = setTimeout(() => {
|
|
36
|
-
if (!this.completed && this.lastActivity === 0) {
|
|
37
|
-
this.handleTimeout('No response received from server');
|
|
38
|
-
}
|
|
39
|
-
}, this.config.initialResponseTimeout);
|
|
40
|
-
// Set up overall operation timeout
|
|
41
|
-
this.operationTimer = setTimeout(() => {
|
|
42
|
-
if (!this.completed) {
|
|
43
|
-
this.handleTimeout(`Request timed out after ${this.config.operationTimeout / 1000}s. The operation took too long to complete.`);
|
|
44
|
-
}
|
|
45
|
-
}, this.config.operationTimeout);
|
|
46
|
-
// Set up activity monitoring if configured
|
|
47
|
-
if (this.config.activityTimeout && this.config.activityTimeout > 0) {
|
|
48
|
-
this.activityTimer = setInterval(() => {
|
|
49
|
-
const timeSinceActivity = Date.now() - this.lastActivity;
|
|
50
|
-
if (timeSinceActivity > this.config.activityTimeout && !this.completed) {
|
|
51
|
-
this.handleActivityTimeout();
|
|
52
|
-
}
|
|
53
|
-
}, this.config.activityTimeout / 2);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
/**
|
|
57
|
-
* Mark that we received activity (data, heartbeat, etc.)
|
|
58
|
-
*/
|
|
59
|
-
markActivity() {
|
|
60
|
-
this.lastActivity = Date.now();
|
|
61
|
-
// Cancel initial response timeout once we receive first data
|
|
62
|
-
if (this.initialResponseTimer) {
|
|
63
|
-
clearTimeout(this.initialResponseTimer);
|
|
64
|
-
this.initialResponseTimer = null;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
/**
|
|
68
|
-
* Mark the operation as complete and clean up
|
|
69
|
-
*/
|
|
70
|
-
complete() {
|
|
71
|
-
this.completed = true;
|
|
72
|
-
this.cleanup();
|
|
73
|
-
}
|
|
74
|
-
/**
|
|
75
|
-
* Clean up all timers
|
|
76
|
-
*/
|
|
77
|
-
cleanup() {
|
|
78
|
-
if (this.operationTimer) {
|
|
79
|
-
clearTimeout(this.operationTimer);
|
|
80
|
-
this.operationTimer = null;
|
|
81
|
-
}
|
|
82
|
-
if (this.initialResponseTimer) {
|
|
83
|
-
clearTimeout(this.initialResponseTimer);
|
|
84
|
-
this.initialResponseTimer = null;
|
|
85
|
-
}
|
|
86
|
-
if (this.activityTimer) {
|
|
87
|
-
clearInterval(this.activityTimer);
|
|
88
|
-
this.activityTimer = null;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
/**
|
|
92
|
-
* Check if the monitor has been completed
|
|
93
|
-
*/
|
|
94
|
-
isCompleted() {
|
|
95
|
-
return this.completed;
|
|
96
|
-
}
|
|
97
|
-
handleTimeout(message) {
|
|
98
|
-
this.completed = true;
|
|
99
|
-
this.callbacks.onError({
|
|
100
|
-
type: 'error',
|
|
101
|
-
content: `${message} Please try again or check if ${this.config.operationName} is responding.`,
|
|
102
|
-
timestamp: new Date().toISOString(),
|
|
103
|
-
});
|
|
104
|
-
this.callbacks.onTimeout();
|
|
105
|
-
this.cleanup();
|
|
106
|
-
}
|
|
107
|
-
handleActivityTimeout() {
|
|
108
|
-
this.completed = true;
|
|
109
|
-
this.callbacks.onError({
|
|
110
|
-
type: 'error',
|
|
111
|
-
content: `Connection to ${this.config.operationName} lost. Please try again.`,
|
|
112
|
-
timestamp: new Date().toISOString(),
|
|
113
|
-
});
|
|
114
|
-
if (this.callbacks.onActivityTimeout) {
|
|
115
|
-
this.callbacks.onActivityTimeout();
|
|
116
|
-
}
|
|
117
|
-
this.cleanup();
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
/**
|
|
121
|
-
* Validates and formats error messages to be user-friendly
|
|
122
|
-
*/
|
|
123
|
-
export function formatErrorMessage(error, context) {
|
|
124
|
-
if (error instanceof Error) {
|
|
125
|
-
// Remove stack traces and internal error patterns
|
|
126
|
-
let message = error.message || '';
|
|
127
|
-
// Common patterns to clean up
|
|
128
|
-
const cleanPatterns = [
|
|
129
|
-
/\s+at\s+.*/g, // Stack traces
|
|
130
|
-
/Error:\s+/g, // Redundant "Error: " prefix
|
|
131
|
-
/TypeError:\s+/g,
|
|
132
|
-
/ReferenceError:\s+/g,
|
|
133
|
-
];
|
|
134
|
-
for (const pattern of cleanPatterns) {
|
|
135
|
-
message = message.replace(pattern, '');
|
|
136
|
-
}
|
|
137
|
-
message = message.trim();
|
|
138
|
-
// If message is too technical or empty, provide a generic message
|
|
139
|
-
if (!message || message.length < 5 || message.includes('undefined')) {
|
|
140
|
-
return `${context} failed. Please try again.`;
|
|
141
|
-
}
|
|
142
|
-
// Ensure first letter is capitalized
|
|
143
|
-
message = message.charAt(0).toUpperCase() + message.slice(1);
|
|
144
|
-
// Ensure it ends with punctuation
|
|
145
|
-
if (!/[.!?]$/.test(message)) {
|
|
146
|
-
message += '.';
|
|
147
|
-
}
|
|
148
|
-
return message;
|
|
149
|
-
}
|
|
150
|
-
return `${context} failed. Please try again.`;
|
|
151
|
-
}
|
|
152
|
-
/**
|
|
153
|
-
* Wraps an async operation with timeout and error handling
|
|
154
|
-
*/
|
|
155
|
-
export async function withTimeout(operation, timeoutMs, errorMessage) {
|
|
156
|
-
const timeout = new Promise((_, reject) => {
|
|
157
|
-
setTimeout(() => reject(new Error(errorMessage)), timeoutMs);
|
|
158
|
-
});
|
|
159
|
-
return Promise.race([operation, timeout]);
|
|
160
|
-
}
|
|
161
|
-
/**
|
|
162
|
-
* Configuration presets for different session types
|
|
163
|
-
*/
|
|
164
|
-
export const MONITOR_PRESETS = {
|
|
165
|
-
// For Claude Code CLI sessions (simpler, no heartbeat)
|
|
166
|
-
claudeCode: {
|
|
167
|
-
operationTimeout: 300000, // 5 minutes for long operations
|
|
168
|
-
initialResponseTimeout: 10000, // 10 seconds to start responding
|
|
169
|
-
activityTimeout: 0, // Disabled - subprocess output is continuous
|
|
170
|
-
operationName: 'Claude Code',
|
|
171
|
-
},
|
|
172
|
-
// For OpenCode HTTP/SSE sessions (needs heartbeat monitoring)
|
|
173
|
-
openCode: {
|
|
174
|
-
operationTimeout: 120000, // 2 minutes
|
|
175
|
-
initialResponseTimeout: 5000, // 5 seconds to connect
|
|
176
|
-
activityTimeout: 45000, // 45 seconds (OpenCode heartbeats every 30s)
|
|
177
|
-
operationName: 'OpenCode',
|
|
178
|
-
},
|
|
179
|
-
// For quick operations (health checks, session verification)
|
|
180
|
-
quick: {
|
|
181
|
-
operationTimeout: 10000, // 10 seconds
|
|
182
|
-
initialResponseTimeout: 5000, // 5 seconds
|
|
183
|
-
activityTimeout: 0,
|
|
184
|
-
operationName: 'Operation',
|
|
185
|
-
},
|
|
186
|
-
};
|
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared utilities for session management across Claude Code and OpenCode.
|
|
3
|
-
*
|
|
4
|
-
* Provides common functionality for:
|
|
5
|
-
* - Session verification
|
|
6
|
-
* - Session state checking
|
|
7
|
-
* - Session pickup handling
|
|
8
|
-
*/
|
|
9
|
-
/**
|
|
10
|
-
* Abstract base class for session verification.
|
|
11
|
-
* Subclasses implement the actual verification mechanism.
|
|
12
|
-
*/
|
|
13
|
-
export class SessionVerifier {
|
|
14
|
-
sessionId;
|
|
15
|
-
constructor(sessionId) {
|
|
16
|
-
this.sessionId = sessionId;
|
|
17
|
-
}
|
|
18
|
-
/**
|
|
19
|
-
* Verify session and notify callbacks about state
|
|
20
|
-
*/
|
|
21
|
-
async verifyAndNotify(callbacks) {
|
|
22
|
-
if (!this.sessionId) {
|
|
23
|
-
return true; // No session to verify
|
|
24
|
-
}
|
|
25
|
-
const exists = await this.verify();
|
|
26
|
-
if (!exists) {
|
|
27
|
-
callbacks.onMessage({
|
|
28
|
-
type: 'system',
|
|
29
|
-
content: 'Previous session expired, starting new session...',
|
|
30
|
-
timestamp: new Date().toISOString(),
|
|
31
|
-
});
|
|
32
|
-
if (callbacks.onSessionExpired) {
|
|
33
|
-
callbacks.onSessionExpired();
|
|
34
|
-
}
|
|
35
|
-
return false;
|
|
36
|
-
}
|
|
37
|
-
// Check session status
|
|
38
|
-
const status = await this.getStatus();
|
|
39
|
-
if (status === 'busy') {
|
|
40
|
-
callbacks.onMessage({
|
|
41
|
-
type: 'system',
|
|
42
|
-
content: 'Session is currently busy, waiting for it to become available...',
|
|
43
|
-
timestamp: new Date().toISOString(),
|
|
44
|
-
});
|
|
45
|
-
if (callbacks.onSessionBusy) {
|
|
46
|
-
callbacks.onSessionBusy();
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
return true;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
/**
|
|
53
|
-
* Verifier for OpenCode sessions (uses HTTP API)
|
|
54
|
-
*/
|
|
55
|
-
export class OpenCodeSessionVerifier extends SessionVerifier {
|
|
56
|
-
containerName;
|
|
57
|
-
port;
|
|
58
|
-
execInContainer;
|
|
59
|
-
constructor(sessionId, containerName, port, execInContainer) {
|
|
60
|
-
super(sessionId);
|
|
61
|
-
this.containerName = containerName;
|
|
62
|
-
this.port = port;
|
|
63
|
-
this.execInContainer = execInContainer;
|
|
64
|
-
}
|
|
65
|
-
async verify() {
|
|
66
|
-
if (!this.sessionId)
|
|
67
|
-
return true;
|
|
68
|
-
try {
|
|
69
|
-
const result = await this.execInContainer(this.containerName, [
|
|
70
|
-
'curl',
|
|
71
|
-
'-s',
|
|
72
|
-
'-o',
|
|
73
|
-
'/dev/null',
|
|
74
|
-
'-w',
|
|
75
|
-
'%{http_code}',
|
|
76
|
-
'--max-time',
|
|
77
|
-
'5',
|
|
78
|
-
`http://localhost:${this.port}/session/${this.sessionId}`,
|
|
79
|
-
], { user: 'workspace' });
|
|
80
|
-
return result.stdout.trim() === '200';
|
|
81
|
-
}
|
|
82
|
-
catch {
|
|
83
|
-
return false;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
async getStatus() {
|
|
87
|
-
if (!this.sessionId)
|
|
88
|
-
return 'unknown';
|
|
89
|
-
try {
|
|
90
|
-
const result = await this.execInContainer(this.containerName, ['curl', '-s', '--max-time', '5', `http://localhost:${this.port}/session/status`], { user: 'workspace' });
|
|
91
|
-
const statuses = JSON.parse(result.stdout);
|
|
92
|
-
const status = statuses[this.sessionId];
|
|
93
|
-
if (status?.type === 'idle')
|
|
94
|
-
return 'idle';
|
|
95
|
-
if (status?.type === 'busy')
|
|
96
|
-
return 'busy';
|
|
97
|
-
return 'unknown';
|
|
98
|
-
}
|
|
99
|
-
catch {
|
|
100
|
-
return 'unknown';
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
/**
|
|
105
|
-
* Verifier for Claude Code sessions (filesystem-based)
|
|
106
|
-
*/
|
|
107
|
-
export class ClaudeCodeSessionVerifier extends SessionVerifier {
|
|
108
|
-
checkSessionFile;
|
|
109
|
-
constructor(sessionId, checkSessionFile) {
|
|
110
|
-
super(sessionId);
|
|
111
|
-
this.checkSessionFile = checkSessionFile;
|
|
112
|
-
}
|
|
113
|
-
async verify() {
|
|
114
|
-
if (!this.sessionId)
|
|
115
|
-
return true;
|
|
116
|
-
try {
|
|
117
|
-
return await this.checkSessionFile(this.sessionId);
|
|
118
|
-
}
|
|
119
|
-
catch {
|
|
120
|
-
return false;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
async getStatus() {
|
|
124
|
-
// Claude Code sessions don't have a "busy" state in the same way
|
|
125
|
-
// They're file-based and managed by the CLI
|
|
126
|
-
return 'unknown';
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
/**
|
|
130
|
-
* Helper to handle session pickup flow consistently
|
|
131
|
-
*/
|
|
132
|
-
export async function handleSessionPickup(verifier, callbacks) {
|
|
133
|
-
const exists = await verifier.verifyAndNotify(callbacks);
|
|
134
|
-
if (!exists) {
|
|
135
|
-
// Session doesn't exist, need to create new one
|
|
136
|
-
await callbacks.onNoSession();
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
/**
|
|
140
|
-
* Create appropriate session verifier based on session type
|
|
141
|
-
*/
|
|
142
|
-
export function createSessionVerifier(type, sessionId, config) {
|
|
143
|
-
if (type === 'opencode') {
|
|
144
|
-
if (!config.containerName || !config.port || !config.execInContainer) {
|
|
145
|
-
throw new Error('OpenCode verifier requires containerName, port, and execInContainer');
|
|
146
|
-
}
|
|
147
|
-
return new OpenCodeSessionVerifier(sessionId, config.containerName, config.port, config.execInContainer);
|
|
148
|
-
}
|
|
149
|
-
else {
|
|
150
|
-
if (!config.checkSessionFile) {
|
|
151
|
-
throw new Error('Claude Code verifier requires checkSessionFile');
|
|
152
|
-
}
|
|
153
|
-
return new ClaudeCodeSessionVerifier(sessionId, config.checkSessionFile);
|
|
154
|
-
}
|
|
155
|
-
}
|
package/dist/chat/websocket.js
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { BaseChatWebSocketServer, } from './base-chat-websocket';
|
|
2
|
-
import { createChatSession } from './handler';
|
|
3
|
-
import { createHostChatSession } from './host-handler';
|
|
4
|
-
export class ChatWebSocketServer extends BaseChatWebSocketServer {
|
|
5
|
-
getConfig;
|
|
6
|
-
agentType = '';
|
|
7
|
-
constructor(options) {
|
|
8
|
-
super(options);
|
|
9
|
-
this.getConfig = options.getConfig;
|
|
10
|
-
}
|
|
11
|
-
createConnection(ws, workspaceName) {
|
|
12
|
-
return {
|
|
13
|
-
ws,
|
|
14
|
-
session: null,
|
|
15
|
-
workspaceName,
|
|
16
|
-
};
|
|
17
|
-
}
|
|
18
|
-
createHostSession(sessionId, onMessage, messageModel, projectPath) {
|
|
19
|
-
const config = this.getConfig();
|
|
20
|
-
const model = messageModel || config.agents?.claude_code?.model;
|
|
21
|
-
return createHostChatSession({ sessionId, model, workDir: projectPath }, onMessage);
|
|
22
|
-
}
|
|
23
|
-
createContainerSession(containerName, sessionId, onMessage, messageModel) {
|
|
24
|
-
const config = this.getConfig();
|
|
25
|
-
const model = messageModel || config.agents?.claude_code?.model;
|
|
26
|
-
return createChatSession({
|
|
27
|
-
containerName,
|
|
28
|
-
workDir: '/home/workspace',
|
|
29
|
-
sessionId,
|
|
30
|
-
model,
|
|
31
|
-
}, onMessage);
|
|
32
|
-
}
|
|
33
|
-
}
|