@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.
@@ -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
- }
@@ -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
- }