happy-coder 0.1.7 → 0.1.10

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 (51) hide show
  1. package/dist/index.cjs +950 -938
  2. package/dist/index.mjs +880 -868
  3. package/dist/lib.cjs +32 -0
  4. package/dist/lib.d.cts +527 -0
  5. package/dist/lib.d.mts +527 -0
  6. package/dist/lib.mjs +14 -0
  7. package/dist/types-B2JzqUiU.cjs +831 -0
  8. package/dist/types-DnQGY77F.mjs +818 -0
  9. package/package.json +25 -10
  10. package/dist/auth/auth.d.ts +0 -38
  11. package/dist/auth/auth.js +0 -76
  12. package/dist/auth/auth.test.d.ts +0 -7
  13. package/dist/auth/auth.test.js +0 -96
  14. package/dist/auth/crypto.d.ts +0 -25
  15. package/dist/auth/crypto.js +0 -36
  16. package/dist/claude/claude.d.ts +0 -54
  17. package/dist/claude/claude.js +0 -170
  18. package/dist/claude/claude.test.d.ts +0 -7
  19. package/dist/claude/claude.test.js +0 -130
  20. package/dist/claude/types.d.ts +0 -37
  21. package/dist/claude/types.js +0 -7
  22. package/dist/commands/start.d.ts +0 -38
  23. package/dist/commands/start.js +0 -161
  24. package/dist/commands/start.test.d.ts +0 -7
  25. package/dist/commands/start.test.js +0 -307
  26. package/dist/handlers/message-handler.d.ts +0 -65
  27. package/dist/handlers/message-handler.js +0 -187
  28. package/dist/index.d.ts +0 -1
  29. package/dist/index.js +0 -1
  30. package/dist/session/service.d.ts +0 -27
  31. package/dist/session/service.js +0 -93
  32. package/dist/session/service.test.d.ts +0 -7
  33. package/dist/session/service.test.js +0 -71
  34. package/dist/session/types.d.ts +0 -44
  35. package/dist/session/types.js +0 -4
  36. package/dist/socket/client.d.ts +0 -50
  37. package/dist/socket/client.js +0 -136
  38. package/dist/socket/client.test.d.ts +0 -7
  39. package/dist/socket/client.test.js +0 -74
  40. package/dist/socket/types.d.ts +0 -80
  41. package/dist/socket/types.js +0 -12
  42. package/dist/utils/config.d.ts +0 -22
  43. package/dist/utils/config.js +0 -23
  44. package/dist/utils/logger.d.ts +0 -26
  45. package/dist/utils/logger.js +0 -60
  46. package/dist/utils/paths.d.ts +0 -18
  47. package/dist/utils/paths.js +0 -24
  48. package/dist/utils/qrcode.d.ts +0 -19
  49. package/dist/utils/qrcode.js +0 -37
  50. package/dist/utils/qrcode.test.d.ts +0 -7
  51. package/dist/utils/qrcode.test.js +0 -14
@@ -1,307 +0,0 @@
1
- /**
2
- * End-to-end integration test for handy-cli bin/dev.js
3
- *
4
- * This test demonstrates how to properly spawn and control the CLI process
5
- * for integration testing using promise-based approach for deterministic results.
6
- */
7
- import { authGetToken, getOrCreateSecretKey } from '#auth/auth';
8
- import { SessionService } from '#session/service';
9
- import { SocketClient } from '#socket/client';
10
- import { getConfig } from '#utils/config';
11
- import { expect } from 'chai';
12
- import { execSync, spawn } from 'node:child_process';
13
- import { join } from 'node:path';
14
- describe('CLI bin/dev.js Integration <-> server <-> mocked client', function () {
15
- this.timeout(30_000); // 30 second timeout for integration tests
16
- let cliProcess = null;
17
- const projectRoot = process.cwd();
18
- const binPath = join(projectRoot, 'bin', 'dev.js');
19
- // Clean up Claude sessions before each test
20
- beforeEach(() => {
21
- // Remove any existing Claude sessions for the playground project
22
- const claudeProjectPath = process.env.HOME + '/.claude/projects/-Users-kirilldubovitskiy-projects-handy-cli-claude-cli-playground-project';
23
- try {
24
- execSync(`rm -rf "${claudeProjectPath}"`, { stdio: 'ignore' });
25
- }
26
- catch {
27
- // Ignore errors if directory doesn't exist
28
- }
29
- });
30
- afterEach(() => {
31
- // Clean up any running processes
32
- if (cliProcess && !cliProcess.killed) {
33
- cliProcess.kill('SIGINT');
34
- cliProcess = null;
35
- }
36
- });
37
- it('Sanity check: should show version with --version flag', (done) => {
38
- const outputs = [];
39
- let processExited = false;
40
- cliProcess = spawn('node', [binPath, '--version'], {
41
- cwd: projectRoot,
42
- env: { ...process.env },
43
- stdio: ['pipe', 'pipe', 'pipe']
44
- });
45
- cliProcess.stdout?.on('data', (data) => {
46
- outputs.push(data.toString());
47
- });
48
- cliProcess.on('exit', (code, _signal) => {
49
- processExited = true;
50
- // Should exit successfully
51
- expect(code).to.equal(0);
52
- // Should show version
53
- const output = outputs.join('');
54
- expect(output).to.match(/\d+\.\d+\.\d+/); // Matches semantic version
55
- done();
56
- });
57
- cliProcess.on('error', (error) => {
58
- done(error);
59
- });
60
- // Set a timeout in case process doesn't exit
61
- setTimeout(() => {
62
- if (!processExited) {
63
- cliProcess?.kill('SIGTERM');
64
- done(new Error('CLI process did not exit within timeout'));
65
- }
66
- }, 5000);
67
- });
68
- /**
69
- * Run our bin/dev.js start command and respond to a single client messages
70
- * that lists files in the directory of the playground project
71
- * with a single file hello-world.js
72
- *
73
- * This test is a bit more complex, because we need to:
74
- * 1. Spawn the CLI process in a playground directory with a single hello-world.js
75
- * 2. Wait for the CLI process to start
76
- * 3. We by default send a message to claude code to show current directory
77
- * to make sure we are in the expected project + sanity check our system.
78
- * 4. Extract the session ID from the CLI process output
79
- * 5. Create a test client and connect to the server
80
- * 6. Assert client recieves current directory within 5 seconds
81
- * 7. The client sends a message asking to list files in the toy directory
82
- * 8. Wait for the response to contain the hello-world.js file for 10 seconds
83
- * 9. Disconnect the test client
84
- * 8. Clean up the CLI process
85
- */
86
- it('should run start command and respond to client messages', async () => {
87
- console.log('=== Starting End-to-End Test ===');
88
- const playgroundPath = join(projectRoot, 'claude-cli-playground-project');
89
- // Create a promise-based test runner
90
- const testResult = await new Promise((resolve, reject) => {
91
- let sessionId = null;
92
- let testCompleted = false;
93
- const cleanup = () => {
94
- if (cliProcess && !cliProcess.killed) {
95
- cliProcess.kill('SIGINT');
96
- cliProcess = null;
97
- }
98
- };
99
- const completeTest = (error) => {
100
- if (testCompleted)
101
- return;
102
- testCompleted = true;
103
- cleanup();
104
- if (error) {
105
- reject(error);
106
- }
107
- else {
108
- resolve();
109
- }
110
- };
111
- // Overall test timeout
112
- const testTimeout = setTimeout(() => {
113
- completeTest(new Error('Test timeout: Did not complete within 30 seconds'));
114
- }, 30_000);
115
- console.log('Step 1: Spawning CLI process...');
116
- cliProcess = spawn('node', [binPath, 'start'], {
117
- cwd: playgroundPath,
118
- env: { ...process.env },
119
- stdio: ['pipe', 'pipe', 'pipe']
120
- });
121
- cliProcess.stdout?.on('data', (data) => {
122
- const output = data.toString();
123
- console.log('CLI stdout:', output);
124
- // Parse session ID from output
125
- const sessionMatch = output.match(/Session created: ([a-zA-Z0-9-]+)/);
126
- if (sessionMatch) {
127
- sessionId = sessionMatch[1];
128
- console.log('Step 2: Extracted session ID:', sessionId);
129
- }
130
- // Look for ready signal
131
- if (output.includes('Handy CLI is running') && sessionId) {
132
- console.log('Step 3: CLI is ready with session ID!');
133
- clearTimeout(testTimeout);
134
- createTestClient(sessionId)
135
- .then(() => completeTest())
136
- .catch(completeTest);
137
- }
138
- });
139
- cliProcess.stderr?.on('data', (data) => {
140
- const error = data.toString();
141
- // Only log real errors, not warnings
142
- if (!error.includes('ExperimentalWarning') && !error.includes('DeprecationWarning')) {
143
- console.log('CLI stderr:', error);
144
- }
145
- });
146
- cliProcess.on('exit', (code, signal) => {
147
- console.log(`CLI process exited with code ${code} and signal ${signal}`);
148
- if (!testCompleted) {
149
- completeTest(new Error(`CLI exited unexpectedly with code ${code}`));
150
- }
151
- });
152
- cliProcess.on('error', (error) => {
153
- console.log('CLI process error:', error);
154
- completeTest(error);
155
- });
156
- });
157
- async function createTestClient(sessionId) {
158
- return new Promise((resolve, reject) => {
159
- const runTestClient = async () => {
160
- try {
161
- console.log('Step 4: Creating test client...');
162
- // Get auth token
163
- const config = getConfig();
164
- // This key would normally be read from the QR code from the terminal
165
- const secret = await getOrCreateSecretKey();
166
- const authToken = await authGetToken(config.serverUrl, secret);
167
- console.log('Auth token obtained');
168
- // Use the parsed session ID
169
- const session = { id: sessionId };
170
- const sessionService = new SessionService(config.serverUrl, authToken);
171
- console.log('Using session:', session.id);
172
- console.log('Step 5: Connecting test client...');
173
- const socketClient = new SocketClient({
174
- authToken,
175
- serverUrl: config.serverUrl,
176
- socketPath: config.socketPath,
177
- });
178
- let cwdResponseReceived = false;
179
- let fileListResponseReceived = false;
180
- let messageSent = false;
181
- // Listen for updates
182
- // eslint-disable-next-line complexity
183
- socketClient.on('update', (update) => {
184
- console.log('Received update type:', update.content?.t, 'for session:', update.content?.sid);
185
- // Check if this is for our session
186
- if (update.content.t === 'new-message' && update.content.sid === session.id) {
187
- try {
188
- // Handle nested decryption structure
189
- let decryptedContent;
190
- if (typeof update.content.c === 'object' && update.content.c.c && update.content.c.t === 'encrypted') {
191
- // Nested encrypted structure
192
- decryptedContent = sessionService.decryptContent(update.content.c.c);
193
- }
194
- else if (typeof update.content.c === 'string') {
195
- decryptedContent = sessionService.decryptContent(update.content.c);
196
- }
197
- else {
198
- // Already decrypted object
199
- decryptedContent = update.content.c;
200
- }
201
- console.log('Decrypted content type:', decryptedContent?.type, 'data type:', decryptedContent?.data?.type);
202
- // Check if it's a Claude response
203
- if (typeof decryptedContent === 'object' &&
204
- decryptedContent !== null &&
205
- 'type' in decryptedContent &&
206
- decryptedContent.type === 'claude-response') {
207
- const responseData = JSON.stringify(decryptedContent).toLowerCase();
208
- console.log('Checking response - cwdResponseReceived:', cwdResponseReceived, 'includes playground:', responseData.includes('claude-cli-playground-project'));
209
- // Step 6: Check for current working directory (initial command)
210
- if (!cwdResponseReceived && responseData.includes('claude-cli-playground-project')) {
211
- console.log('Step 6: Received current working directory response within 5 seconds!');
212
- cwdResponseReceived = true;
213
- // Now send the client message to list files
214
- if (!messageSent) {
215
- messageSent = true;
216
- console.log('Step 7: Sending client message to list files...');
217
- sessionService.sendMessage(session.id, {
218
- content: 'ls',
219
- type: 'text-input'
220
- }).then(() => {
221
- console.log('Client message sent successfully');
222
- }).catch((error) => {
223
- console.error('Failed to send client message:', error);
224
- reject(error);
225
- });
226
- }
227
- }
228
- // Step 8: Check for hello-world.js in file listing
229
- if (cwdResponseReceived && messageSent) {
230
- // Log tool results to debug
231
- if (decryptedContent.data?.type === 'user' && decryptedContent.data?.message?.content) {
232
- // Check tool results in user messages
233
- const toolResults = decryptedContent.data.message.content.filter((c) => c.type === 'tool_result');
234
- // eslint-disable-next-line max-depth
235
- if (toolResults.length > 0) {
236
- console.log('Tool results found:', JSON.stringify(toolResults));
237
- // Check if any tool result contains hello-world.js
238
- const hasHelloWorld = toolResults.some((result) => result.content && result.content.toLowerCase().includes('hello-world.js'));
239
- // eslint-disable-next-line max-depth
240
- if (hasHelloWorld) {
241
- console.log('Step 8: Found hello-world.js in tool results!');
242
- fileListResponseReceived = true;
243
- socketClient.disconnect();
244
- console.log('=== Test Completed Successfully ===');
245
- // Give the CLI time to shut down gracefully
246
- setTimeout(() => {
247
- if (cliProcess && !cliProcess.killed) {
248
- cliProcess.kill('SIGINT'); // Use SIGINT for graceful shutdown
249
- }
250
- }, 100);
251
- resolve();
252
- }
253
- }
254
- }
255
- // Also check in the full response
256
- if (responseData.includes('hello-world.js')) {
257
- console.log('Step 8: Found hello-world.js in response!');
258
- fileListResponseReceived = true;
259
- socketClient.disconnect();
260
- console.log('=== Test Completed Successfully ===');
261
- // Give the CLI time to shut down gracefully
262
- setTimeout(() => {
263
- if (cliProcess && !cliProcess.killed) {
264
- cliProcess.kill('SIGINT'); // Use SIGINT for graceful shutdown
265
- }
266
- }, 100);
267
- resolve();
268
- }
269
- }
270
- }
271
- }
272
- catch (decryptError) {
273
- console.log('Failed to decrypt message:', decryptError);
274
- reject(decryptError);
275
- }
276
- }
277
- });
278
- socketClient.connect();
279
- await socketClient.waitForAuth();
280
- console.log('Test client connected and authenticated');
281
- // Don't send any message here - we'll wait for CWD response first
282
- console.log('Waiting for initial current working directory response...');
283
- // Set timeouts for two-stage check
284
- setTimeout(() => {
285
- if (!cwdResponseReceived) {
286
- socketClient.disconnect();
287
- reject(new Error('Did not receive current working directory response within 5 seconds'));
288
- }
289
- }, 5000); // 5 second timeout for CWD response
290
- setTimeout(() => {
291
- if (cwdResponseReceived && !fileListResponseReceived) {
292
- socketClient.disconnect();
293
- reject(new Error('Did not receive hello-world.js in file listing response within 10 seconds'));
294
- }
295
- }, 15_000); // 15 second timeout for complete test
296
- }
297
- catch (error) {
298
- console.log('Error in createTestClient:', error);
299
- reject(error);
300
- }
301
- };
302
- runTestClient();
303
- });
304
- }
305
- return testResult;
306
- });
307
- });
@@ -1,65 +0,0 @@
1
- /**
2
- * Message handler for handy-cli
3
- *
4
- * This module handles incoming messages from the socket server
5
- * and coordinates between the socket client and Claude CLI.
6
- *
7
- * Key responsibilities:
8
- * - Process incoming text-input messages
9
- * - Forward messages to Claude CLI
10
- * - Send Claude responses back through socket
11
- * - Handle message encryption/decryption (future)
12
- *
13
- * Design decisions:
14
- * - Uses event-driven architecture for loose coupling
15
- * - Handles only text-input messages initially
16
- * - Prepared for future encryption support
17
- */
18
- import { ClaudeOptions } from '#claude/claude';
19
- import { SessionService } from '#session/service';
20
- import { SocketClient } from '#socket/client';
21
- import { EventEmitter } from 'node:events';
22
- export interface MessageHandlerOptions {
23
- claudeOptions?: Partial<ClaudeOptions>;
24
- sessionId: string;
25
- sessionService: SessionService;
26
- socketClient: SocketClient;
27
- workingDirectory: string;
28
- }
29
- export declare class MessageHandler extends EventEmitter {
30
- private claude;
31
- private claudeOptions;
32
- private sessionId;
33
- private sessionService;
34
- private socketClient;
35
- private workingDirectory;
36
- constructor(options: MessageHandlerOptions);
37
- /**
38
- * Handle initial command directly (for startup)
39
- */
40
- handleInitialCommand(command: string): void;
41
- /**
42
- * Start handling messages
43
- */
44
- start(): void;
45
- /**
46
- * Stop handling messages
47
- */
48
- stop(): void;
49
- /**
50
- * Handle Claude responses
51
- */
52
- private handleClaudeResponse;
53
- /**
54
- * Handle text input messages
55
- */
56
- private handleTextInput;
57
- /**
58
- * Handle update messages from the server
59
- */
60
- private handleUpdate;
61
- /**
62
- * Set up event handlers
63
- */
64
- private setupHandlers;
65
- }
@@ -1,187 +0,0 @@
1
- /**
2
- * Message handler for handy-cli
3
- *
4
- * This module handles incoming messages from the socket server
5
- * and coordinates between the socket client and Claude CLI.
6
- *
7
- * Key responsibilities:
8
- * - Process incoming text-input messages
9
- * - Forward messages to Claude CLI
10
- * - Send Claude responses back through socket
11
- * - Handle message encryption/decryption (future)
12
- *
13
- * Design decisions:
14
- * - Uses event-driven architecture for loose coupling
15
- * - Handles only text-input messages initially
16
- * - Prepared for future encryption support
17
- */
18
- import { Claude } from '#claude/claude';
19
- import { logger } from '#utils/logger';
20
- import { EventEmitter } from 'node:events';
21
- // eslint-disable-next-line unicorn/prefer-event-target
22
- export class MessageHandler extends EventEmitter {
23
- claude;
24
- claudeOptions;
25
- sessionId;
26
- sessionService;
27
- socketClient;
28
- workingDirectory;
29
- constructor(options) {
30
- super();
31
- this.socketClient = options.socketClient;
32
- this.claude = new Claude();
33
- this.sessionService = options.sessionService;
34
- this.sessionId = options.sessionId;
35
- this.workingDirectory = options.workingDirectory;
36
- this.claudeOptions = options.claudeOptions || {};
37
- this.setupHandlers();
38
- }
39
- /**
40
- * Handle initial command directly (for startup)
41
- */
42
- handleInitialCommand(command) {
43
- logger.info('Handling initial command:', command);
44
- // Run Claude command
45
- this.claude.runClaudeCodeTurn(command, undefined, // No session ID for initial command
46
- {
47
- workingDirectory: this.workingDirectory,
48
- ...this.claudeOptions
49
- });
50
- }
51
- /**
52
- * Start handling messages
53
- */
54
- start() {
55
- logger.info('Message handler started');
56
- // Any initialization logic can go here
57
- }
58
- /**
59
- * Stop handling messages
60
- */
61
- stop() {
62
- logger.info('Message handler stopped');
63
- this.claude.kill();
64
- }
65
- /**
66
- * Handle Claude responses
67
- */
68
- async handleClaudeResponse(response) {
69
- logger.info('Claude response:', JSON.stringify(response, null, 2));
70
- try {
71
- // Send the response to the server session
72
- await this.sessionService.sendMessage(this.sessionId, {
73
- data: response,
74
- type: 'claude-response'
75
- });
76
- // Emit for local handling
77
- this.emit('claudeResponse', response);
78
- }
79
- catch (error) {
80
- logger.error('Failed to send Claude response to server:', error);
81
- this.emit('error', error);
82
- }
83
- }
84
- /**
85
- * Handle text input messages
86
- */
87
- handleTextInput(message) {
88
- logger.info('Received text input:', message.content);
89
- // Run Claude command (kills any existing process automatically)
90
- this.claude.runClaudeCodeTurn(message.content, undefined, // No session resuming for now
91
- {
92
- workingDirectory: this.workingDirectory,
93
- ...this.claudeOptions
94
- });
95
- }
96
- /**
97
- * Handle update messages from the server
98
- */
99
- handleUpdate(update) {
100
- logger.debug('Received update:', JSON.stringify(update, null, 2));
101
- // Check if this is a new message for our session
102
- if (update.content.t === 'new-message') {
103
- const { c, sid } = update.content;
104
- // Verify this message is for our session
105
- if (sid !== this.sessionId) {
106
- logger.debug('Message for different session, ignoring');
107
- return;
108
- }
109
- try {
110
- // Log the raw content structure for debugging
111
- logger.debug('Raw content (c):', JSON.stringify(c));
112
- logger.debug('Content type:', typeof c);
113
- // Check if content is already a string or needs extraction
114
- let encryptedContent;
115
- if (typeof c === 'string') {
116
- // Direct string content
117
- encryptedContent = c;
118
- }
119
- else if (typeof c === 'object' && c !== null && 'c' in c && typeof c.c === 'string') {
120
- // Nested structure: { c: 'base64string', t: 'encrypted' }
121
- logger.debug('Extracting from nested structure');
122
- encryptedContent = c.c;
123
- }
124
- else {
125
- logger.error('Invalid content structure:', c);
126
- return;
127
- }
128
- // Decrypt the content
129
- const decryptedContent = this.sessionService.decryptContent(encryptedContent);
130
- logger.debug('Decrypted content:', decryptedContent);
131
- // Type guard for the decrypted content
132
- if (typeof decryptedContent === 'object' &&
133
- decryptedContent !== null &&
134
- 'type' in decryptedContent) {
135
- const message = decryptedContent;
136
- // Handle the message based on type
137
- if (message.type === 'text-input') {
138
- this.handleTextInput(decryptedContent);
139
- }
140
- else if (message.type === 'claude-response') {
141
- // This is our own claude-response being echoed back from the server
142
- // We can safely ignore it or log it for debugging
143
- logger.debug('Received claude-response echo from server:', message);
144
- }
145
- else {
146
- logger.warn('Unknown message type:', message.type);
147
- }
148
- }
149
- else {
150
- logger.error('Invalid message format');
151
- }
152
- }
153
- catch (error) {
154
- logger.error('Failed to process message:', error);
155
- }
156
- }
157
- }
158
- /**
159
- * Set up event handlers
160
- */
161
- setupHandlers() {
162
- // Handle updates from socket server
163
- this.socketClient.on('update', (update) => {
164
- this.handleUpdate(update);
165
- });
166
- // Handle Claude responses
167
- this.claude.on('response', (response) => {
168
- this.handleClaudeResponse(response);
169
- });
170
- // Handle Claude output (non-JSON)
171
- this.claude.on('output', (output) => {
172
- logger.debug('Claude output:', output);
173
- // For now, we'll log non-JSON output
174
- // In the future, we might want to send this to the client
175
- });
176
- // Handle Claude errors
177
- this.claude.on('error', (error) => {
178
- logger.error('Claude error:', error);
179
- this.emit('error', error);
180
- });
181
- // Handle Claude exit
182
- this.claude.on('exit', (exitInfo) => {
183
- logger.info('Claude exited:', exitInfo);
184
- this.emit('claudeExit', exitInfo);
185
- });
186
- }
187
- }
package/dist/index.d.ts DELETED
@@ -1 +0,0 @@
1
- export { run } from '@oclif/core';
package/dist/index.js DELETED
@@ -1 +0,0 @@
1
- export { run } from '@oclif/core';
@@ -1,27 +0,0 @@
1
- /**
2
- * Session service for managing handy-server sessions
3
- */
4
- import type { CreateSessionResponse, GetMessagesResponse, SendMessageResponse } from '#session/types';
5
- export declare class SessionService {
6
- private readonly serverUrl;
7
- private readonly authToken;
8
- constructor(serverUrl: string, authToken: string);
9
- /**
10
- * Create a new session or load existing one with the given tag
11
- */
12
- createSession(tag: string): Promise<CreateSessionResponse>;
13
- /**
14
- * Decrypt a message content
15
- * Note: In real implementation, this would use proper decryption
16
- */
17
- decryptContent(encryptedContent: string): unknown;
18
- /**
19
- * Get messages from a session
20
- */
21
- getMessages(sessionId: string): Promise<GetMessagesResponse>;
22
- /**
23
- * Send a message to a session
24
- * Note: In real implementation, we'd encrypt the content before sending
25
- */
26
- sendMessage(sessionId: string, content: unknown): Promise<SendMessageResponse>;
27
- }