happy-coder 0.1.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 (50) hide show
  1. package/README.md +38 -0
  2. package/bin/happy +2 -0
  3. package/bin/happy.cmd +2 -0
  4. package/dist/auth/auth.d.ts +38 -0
  5. package/dist/auth/auth.js +76 -0
  6. package/dist/auth/auth.test.d.ts +7 -0
  7. package/dist/auth/auth.test.js +96 -0
  8. package/dist/auth/crypto.d.ts +25 -0
  9. package/dist/auth/crypto.js +36 -0
  10. package/dist/claude/claude.d.ts +54 -0
  11. package/dist/claude/claude.js +170 -0
  12. package/dist/claude/claude.test.d.ts +7 -0
  13. package/dist/claude/claude.test.js +130 -0
  14. package/dist/claude/types.d.ts +37 -0
  15. package/dist/claude/types.js +7 -0
  16. package/dist/commands/start.d.ts +38 -0
  17. package/dist/commands/start.js +161 -0
  18. package/dist/commands/start.test.d.ts +7 -0
  19. package/dist/commands/start.test.js +307 -0
  20. package/dist/handlers/message-handler.d.ts +65 -0
  21. package/dist/handlers/message-handler.js +187 -0
  22. package/dist/index.cjs +603 -0
  23. package/dist/index.d.cts +1 -0
  24. package/dist/index.d.mts +1 -0
  25. package/dist/index.d.ts +1 -0
  26. package/dist/index.js +1 -0
  27. package/dist/index.mjs +583 -0
  28. package/dist/session/service.d.ts +27 -0
  29. package/dist/session/service.js +93 -0
  30. package/dist/session/service.test.d.ts +7 -0
  31. package/dist/session/service.test.js +71 -0
  32. package/dist/session/types.d.ts +44 -0
  33. package/dist/session/types.js +4 -0
  34. package/dist/socket/client.d.ts +50 -0
  35. package/dist/socket/client.js +136 -0
  36. package/dist/socket/client.test.d.ts +7 -0
  37. package/dist/socket/client.test.js +74 -0
  38. package/dist/socket/types.d.ts +80 -0
  39. package/dist/socket/types.js +12 -0
  40. package/dist/utils/config.d.ts +22 -0
  41. package/dist/utils/config.js +23 -0
  42. package/dist/utils/logger.d.ts +26 -0
  43. package/dist/utils/logger.js +60 -0
  44. package/dist/utils/paths.d.ts +18 -0
  45. package/dist/utils/paths.js +24 -0
  46. package/dist/utils/qrcode.d.ts +19 -0
  47. package/dist/utils/qrcode.js +37 -0
  48. package/dist/utils/qrcode.test.d.ts +7 -0
  49. package/dist/utils/qrcode.test.js +14 -0
  50. package/package.json +60 -0
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Integration tests for Claude CLI
3
+ *
4
+ * These tests verify that we can properly interact with Claude CLI,
5
+ * send commands, and receive responses.
6
+ */
7
+ import { Claude } from '#claude/claude';
8
+ import { logger } from '#utils/logger';
9
+ import { expect } from 'chai';
10
+ import { existsSync } from 'node:fs';
11
+ import { resolve } from 'node:path';
12
+ describe('Claude CLI Integration', function () {
13
+ this.timeout(60_000); // 60 second timeout for Claude operations
14
+ let claude;
15
+ const playgroundPath = resolve('./claude-cli-playground-project');
16
+ before(() => {
17
+ // Verify playground directory exists
18
+ if (!existsSync(playgroundPath)) {
19
+ throw new Error('Playground directory not found. Run from handy-cli root directory.');
20
+ }
21
+ });
22
+ beforeEach(() => {
23
+ // Create a new Claude instance for each test
24
+ claude = new Claude();
25
+ });
26
+ afterEach(() => {
27
+ // Clean up Claude process
28
+ claude.kill();
29
+ });
30
+ it('should execute ls command and list files in playground directory', (done) => {
31
+ const responses = [];
32
+ let hasListedFiles = false;
33
+ let sessionId;
34
+ let testCompleted = false;
35
+ let hasSeenToolUse = false;
36
+ let hasSeenToolResult = false;
37
+ // Set a timeout to prevent infinite hanging
38
+ const timeout = setTimeout(() => {
39
+ if (!testCompleted) {
40
+ testCompleted = true;
41
+ claude.kill();
42
+ done(new Error('Test timeout: Claude did not respond within expected time'));
43
+ }
44
+ }, 15_000); // 15 second timeout
45
+ claude.on('response', (response) => {
46
+ responses.push(response);
47
+ logger.info('Response type:', response.type, 'Session ID:', response.session_id);
48
+ // Capture session ID
49
+ if (response.session_id) {
50
+ sessionId = response.session_id;
51
+ logger.info('Captured session ID:', sessionId);
52
+ }
53
+ // Check for tool use and tool results based on actual Claude output
54
+ if (response.type === 'assistant' && response.data) {
55
+ const content = JSON.stringify(response.data).toLowerCase();
56
+ if (content.includes('tool_use') || content.includes('ls')) {
57
+ hasSeenToolUse = true;
58
+ logger.info('Detected tool use in assistant response');
59
+ }
60
+ }
61
+ if (response.type === 'user' && response.data) {
62
+ const content = JSON.stringify(response.data).toLowerCase();
63
+ if (content.includes('tool_result') || content.includes('hello-world.js')) {
64
+ hasSeenToolResult = true;
65
+ hasListedFiles = true;
66
+ logger.info('Detected tool result with file listing');
67
+ }
68
+ }
69
+ // Check various response types for file listing
70
+ if (response.type === 'assistant' || response.type === 'user' || response.type === 'claude-response') {
71
+ const content = JSON.stringify(response).toLowerCase();
72
+ // Check for expected files in playground
73
+ if (content.includes('hello-world.js')) {
74
+ hasListedFiles = true;
75
+ logger.info('Found hello-world.js in response');
76
+ }
77
+ }
78
+ // Complete test when we have seen both tool use and tool result
79
+ if (hasSeenToolUse && hasSeenToolResult && hasListedFiles && sessionId && !testCompleted) {
80
+ testCompleted = true;
81
+ clearTimeout(timeout);
82
+ expect(sessionId).to.be.a('string');
83
+ expect(sessionId).to.have.length.greaterThan(0);
84
+ expect(hasListedFiles).to.equal(true, 'Claude should have listed files');
85
+ expect(responses).to.have.length.greaterThan(0);
86
+ done();
87
+ }
88
+ });
89
+ claude.on('error', (error) => {
90
+ if (!testCompleted) {
91
+ testCompleted = true;
92
+ clearTimeout(timeout);
93
+ done(new Error(`Claude error: ${error}`));
94
+ }
95
+ });
96
+ claude.on('processError', (error) => {
97
+ if (!testCompleted) {
98
+ testCompleted = true;
99
+ clearTimeout(timeout);
100
+ // Claude CLI might not be installed
101
+ console.warn('Claude CLI not available:', error.message);
102
+ done();
103
+ }
104
+ });
105
+ claude.on('exit', (exitInfo) => {
106
+ if (!testCompleted) {
107
+ testCompleted = true;
108
+ clearTimeout(timeout);
109
+ logger.info(`Claude process exited with code ${exitInfo.code} and signal ${exitInfo.signal}`);
110
+ // Check if we got the expected responses even though process exited
111
+ if (sessionId && responses.length > 0 && (hasListedFiles || hasSeenToolResult)) {
112
+ // Process exited but we got expected responses - this is normal
113
+ expect(sessionId).to.be.a('string');
114
+ expect(responses).to.have.length.greaterThan(0);
115
+ done();
116
+ }
117
+ else {
118
+ // Process exited without proper responses - this is an error
119
+ done(new Error(`Claude process exited with code ${exitInfo.code} and signal ${exitInfo.signal} without providing expected responses`));
120
+ }
121
+ }
122
+ });
123
+ // Start Claude with ls command
124
+ claude.runClaudeCodeTurn('Use the LS tool to list files in the current directory and tell me the first 5 files or folders you see.', undefined, {
125
+ model: 'sonnet',
126
+ permissionMode: 'default',
127
+ workingDirectory: playgroundPath
128
+ });
129
+ });
130
+ });
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Types for Claude CLI interaction
3
+ *
4
+ * This module defines types for the JSON line output from Claude CLI
5
+ * and configuration options for spawning Claude processes.
6
+ */
7
+ import type { ChildProcess } from 'node:child_process';
8
+ /**
9
+ * Claude CLI spawn options
10
+ */
11
+ export interface ClaudeSpawnOptions {
12
+ allowedTools?: string[];
13
+ disallowedTools?: string[];
14
+ model?: string;
15
+ permissionMode?: 'auto' | 'default' | 'plan';
16
+ sessionId?: string;
17
+ skipPermissions?: boolean;
18
+ workingDirectory: string;
19
+ }
20
+ /**
21
+ * Claude CLI JSON output types
22
+ */
23
+ export interface ClaudeResponse {
24
+ data?: unknown;
25
+ error?: string;
26
+ message?: string;
27
+ session_id?: string;
28
+ type: string;
29
+ }
30
+ /**
31
+ * Claude process state
32
+ */
33
+ export interface ClaudeProcess {
34
+ isRunning: boolean;
35
+ process: ChildProcess;
36
+ sessionId?: string;
37
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Types for Claude CLI interaction
3
+ *
4
+ * This module defines types for the JSON line output from Claude CLI
5
+ * and configuration options for spawning Claude processes.
6
+ */
7
+ export {};
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Start command for handy-cli
3
+ *
4
+ * This is the main command that starts a Claude Code session and connects
5
+ * it to the handy server for remote access.
6
+ *
7
+ * Key responsibilities:
8
+ * - Initialize authentication
9
+ * - Establish socket connection
10
+ * - Start Claude session
11
+ * - Handle message routing
12
+ * - Graceful shutdown
13
+ *
14
+ * Design decisions:
15
+ * - Uses oclif command framework as requested
16
+ * - Handles all initialization in sequence
17
+ * - Provides clear error messages
18
+ * - Supports graceful shutdown on SIGINT/SIGTERM
19
+ */
20
+ import { Command } from '@oclif/core';
21
+ export default class Start extends Command {
22
+ static description: string;
23
+ static examples: string[];
24
+ static flags: {
25
+ model: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
26
+ 'permission-mode': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
27
+ 'skip-permissions': import("@oclif/core/interfaces").BooleanFlag<boolean>;
28
+ 'test-session': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
29
+ };
30
+ private messageHandler?;
31
+ private sessionId?;
32
+ private sessionService?;
33
+ private socketClient?;
34
+ run(): Promise<void>;
35
+ private cleanup;
36
+ private setupShutdownHandlers;
37
+ private shutdown;
38
+ }
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Start command for handy-cli
3
+ *
4
+ * This is the main command that starts a Claude Code session and connects
5
+ * it to the handy server for remote access.
6
+ *
7
+ * Key responsibilities:
8
+ * - Initialize authentication
9
+ * - Establish socket connection
10
+ * - Start Claude session
11
+ * - Handle message routing
12
+ * - Graceful shutdown
13
+ *
14
+ * Design decisions:
15
+ * - Uses oclif command framework as requested
16
+ * - Handles all initialization in sequence
17
+ * - Provides clear error messages
18
+ * - Supports graceful shutdown on SIGINT/SIGTERM
19
+ */
20
+ import { authGetToken, generateHandyUrl, getOrCreateSecretKey } from '#auth/auth';
21
+ import { MessageHandler } from '#handlers/message-handler';
22
+ import { SessionService } from '#session/service';
23
+ import { SocketClient } from '#socket/client';
24
+ import { getConfig } from '#utils/config';
25
+ import { logger } from '#utils/logger';
26
+ import { displayQRCode } from '#utils/qrcode';
27
+ import { Command, Flags } from '@oclif/core';
28
+ import { basename } from 'node:path';
29
+ export default class Start extends Command {
30
+ static description = 'Start a Claude Code session connected to the handy server';
31
+ static examples = [
32
+ '<%= config.bin %> <%= command.id %>',
33
+ '<%= config.bin %> <%= command.id %> --model sonnet',
34
+ '<%= config.bin %> <%= command.id %> --skip-permissions',
35
+ ];
36
+ static flags = {
37
+ model: Flags.string({
38
+ char: 'm',
39
+ default: 'sonnet',
40
+ description: 'Claude model to use',
41
+ options: ['sonnet', 'opus', 'haiku'],
42
+ }),
43
+ 'permission-mode': Flags.string({
44
+ default: 'auto',
45
+ description: 'Permission mode for Claude',
46
+ options: ['plan', 'auto', 'default'],
47
+ }),
48
+ 'skip-permissions': Flags.boolean({
49
+ default: true,
50
+ description: 'Skip permission prompts (dangerous)',
51
+ }),
52
+ 'test-session': Flags.string({
53
+ description: 'Use a specific session ID for testing (internal use)',
54
+ hidden: true,
55
+ }),
56
+ };
57
+ messageHandler;
58
+ sessionId;
59
+ sessionService;
60
+ socketClient;
61
+ async run() {
62
+ const { flags } = await this.parse(Start);
63
+ try {
64
+ // Load configuration
65
+ const config = getConfig();
66
+ logger.info('Starting handy-cli...');
67
+ // Step 1: Authentication
68
+ logger.info('Authenticating with server...');
69
+ const secret = await getOrCreateSecretKey();
70
+ const authToken = await authGetToken(config.serverUrl, secret);
71
+ logger.info('Authentication successful');
72
+ // Step 1.5: Display QR code for mobile connection
73
+ const handyUrl = generateHandyUrl(secret);
74
+ displayQRCode(handyUrl);
75
+ // Step 2: Connect to socket server
76
+ logger.info('Connecting to socket server...');
77
+ this.socketClient = new SocketClient({
78
+ authToken,
79
+ serverUrl: config.serverUrl,
80
+ socketPath: config.socketPath,
81
+ });
82
+ this.socketClient.connect();
83
+ // Wait for authentication
84
+ const user = await this.socketClient.waitForAuth();
85
+ logger.info(`Connected as user: ${user}`);
86
+ // Step 3: Create server session
87
+ const workingDirectory = process.cwd();
88
+ const sessionTag = basename(workingDirectory);
89
+ logger.info(`Creating server session with tag: ${sessionTag}`);
90
+ this.sessionService = new SessionService(config.serverUrl, authToken);
91
+ const { session } = await this.sessionService.createSession(sessionTag);
92
+ this.sessionId = session.id;
93
+ logger.info(`Session created: ${this.sessionId}`);
94
+ // Step 4: Initialize Claude
95
+ logger.info(`Initializing Claude in: ${workingDirectory}`);
96
+ // Step 4: Set up message handler with session ID
97
+ this.messageHandler = new MessageHandler({
98
+ claudeOptions: {
99
+ model: flags.model,
100
+ permissionMode: flags['permission-mode'],
101
+ skipPermissions: flags['skip-permissions']
102
+ },
103
+ sessionId: this.sessionId,
104
+ sessionService: this.sessionService,
105
+ socketClient: this.socketClient,
106
+ workingDirectory
107
+ });
108
+ this.messageHandler.start();
109
+ // Set up event handlers for logging
110
+ this.messageHandler.on('claudeResponse', (response) => {
111
+ logger.info('Claude response:', JSON.stringify(response, null, 2));
112
+ });
113
+ this.messageHandler.on('error', (error) => {
114
+ logger.error('Handler error:', error);
115
+ });
116
+ this.messageHandler.on('claudeExit', (exitInfo) => {
117
+ logger.info('Claude process exited:', exitInfo);
118
+ });
119
+ // Step 5: Start initial Claude session
120
+ logger.info('Starting Claude Code session...');
121
+ logger.info('Model:', flags.model);
122
+ logger.info('Permission mode:', flags['permission-mode']);
123
+ logger.info('Skip permissions:', flags['skip-permissions']);
124
+ // Start with a command to show current working directory to ensure we
125
+ // are in the correct project
126
+ const initialCommand = 'Show current working directory';
127
+ logger.info('Sending initial command to Claude:', initialCommand);
128
+ // Send the initial command through the message handler to ensure it's properly captured
129
+ this.messageHandler.handleInitialCommand(initialCommand);
130
+ // Set up graceful shutdown
131
+ this.setupShutdownHandlers();
132
+ logger.info('Handy CLI is running. Press Ctrl+C to stop.');
133
+ logger.info('Waiting for commands from connected clients...');
134
+ // Keep the process running
135
+ await new Promise(() => { });
136
+ }
137
+ catch (error) {
138
+ logger.error('Failed to start:', error);
139
+ this.cleanup();
140
+ this.exit(1);
141
+ }
142
+ }
143
+ cleanup() {
144
+ if (this.messageHandler) {
145
+ this.messageHandler.stop();
146
+ }
147
+ if (this.socketClient) {
148
+ this.socketClient.disconnect();
149
+ }
150
+ }
151
+ setupShutdownHandlers() {
152
+ process.on('SIGINT', () => this.shutdown());
153
+ process.on('SIGTERM', () => this.shutdown());
154
+ }
155
+ shutdown() {
156
+ logger.info('Shutting down...');
157
+ this.cleanup();
158
+ // Use OCLIF's exit method for proper shutdown
159
+ this.exit(0);
160
+ }
161
+ }
@@ -0,0 +1,7 @@
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
+ export {};