happy-coder 0.1.7 → 0.1.9

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 (57) hide show
  1. package/dist/index.cjs +354 -917
  2. package/dist/index.mjs +280 -843
  3. package/dist/install-B2r_gX72.cjs +109 -0
  4. package/dist/install-HKe7dyS4.mjs +107 -0
  5. package/dist/lib.cjs +32 -0
  6. package/dist/lib.d.cts +727 -0
  7. package/dist/lib.d.mts +727 -0
  8. package/dist/lib.mjs +14 -0
  9. package/dist/run-FBXkmmN7.mjs +32 -0
  10. package/dist/run-q2To6b-c.cjs +34 -0
  11. package/dist/types-fXgEaaqP.mjs +861 -0
  12. package/dist/types-mykDX2xe.cjs +872 -0
  13. package/dist/uninstall-C42CoSCI.cjs +53 -0
  14. package/dist/uninstall-CLkTtlMv.mjs +51 -0
  15. package/package.json +25 -10
  16. package/dist/auth/auth.d.ts +0 -38
  17. package/dist/auth/auth.js +0 -76
  18. package/dist/auth/auth.test.d.ts +0 -7
  19. package/dist/auth/auth.test.js +0 -96
  20. package/dist/auth/crypto.d.ts +0 -25
  21. package/dist/auth/crypto.js +0 -36
  22. package/dist/claude/claude.d.ts +0 -54
  23. package/dist/claude/claude.js +0 -170
  24. package/dist/claude/claude.test.d.ts +0 -7
  25. package/dist/claude/claude.test.js +0 -130
  26. package/dist/claude/types.d.ts +0 -37
  27. package/dist/claude/types.js +0 -7
  28. package/dist/commands/start.d.ts +0 -38
  29. package/dist/commands/start.js +0 -161
  30. package/dist/commands/start.test.d.ts +0 -7
  31. package/dist/commands/start.test.js +0 -307
  32. package/dist/handlers/message-handler.d.ts +0 -65
  33. package/dist/handlers/message-handler.js +0 -187
  34. package/dist/index.d.ts +0 -1
  35. package/dist/index.js +0 -1
  36. package/dist/session/service.d.ts +0 -27
  37. package/dist/session/service.js +0 -93
  38. package/dist/session/service.test.d.ts +0 -7
  39. package/dist/session/service.test.js +0 -71
  40. package/dist/session/types.d.ts +0 -44
  41. package/dist/session/types.js +0 -4
  42. package/dist/socket/client.d.ts +0 -50
  43. package/dist/socket/client.js +0 -136
  44. package/dist/socket/client.test.d.ts +0 -7
  45. package/dist/socket/client.test.js +0 -74
  46. package/dist/socket/types.d.ts +0 -80
  47. package/dist/socket/types.js +0 -12
  48. package/dist/utils/config.d.ts +0 -22
  49. package/dist/utils/config.js +0 -23
  50. package/dist/utils/logger.d.ts +0 -26
  51. package/dist/utils/logger.js +0 -60
  52. package/dist/utils/paths.d.ts +0 -18
  53. package/dist/utils/paths.js +0 -24
  54. package/dist/utils/qrcode.d.ts +0 -19
  55. package/dist/utils/qrcode.js +0 -37
  56. package/dist/utils/qrcode.test.d.ts +0 -7
  57. package/dist/utils/qrcode.test.js +0 -14
@@ -0,0 +1,53 @@
1
+ 'use strict';
2
+
3
+ var types = require('./types-mykDX2xe.cjs');
4
+ var fs = require('fs');
5
+ var child_process = require('child_process');
6
+ require('axios');
7
+ require('chalk');
8
+ require('node:os');
9
+ require('node:path');
10
+ require('node:fs/promises');
11
+ require('node:fs');
12
+ require('node:events');
13
+ require('socket.io-client');
14
+ require('zod');
15
+ require('node:crypto');
16
+ require('tweetnacl');
17
+ require('expo-server-sdk');
18
+
19
+ const PLIST_LABEL = "com.happy-cli.daemon";
20
+ const PLIST_FILE = `/Library/LaunchDaemons/${PLIST_LABEL}.plist`;
21
+ async function uninstall$1() {
22
+ try {
23
+ if (!fs.existsSync(PLIST_FILE)) {
24
+ types.logger.info("Daemon plist not found. Nothing to uninstall.");
25
+ return;
26
+ }
27
+ try {
28
+ child_process.execSync(`launchctl unload ${PLIST_FILE}`, { stdio: "inherit" });
29
+ types.logger.info("Daemon stopped successfully");
30
+ } catch (error) {
31
+ types.logger.info("Failed to unload daemon (it might not be running)");
32
+ }
33
+ fs.unlinkSync(PLIST_FILE);
34
+ types.logger.info(`Removed daemon plist from ${PLIST_FILE}`);
35
+ types.logger.info("Daemon uninstalled successfully");
36
+ } catch (error) {
37
+ types.logger.debug("Failed to uninstall daemon:", error);
38
+ throw error;
39
+ }
40
+ }
41
+
42
+ async function uninstall() {
43
+ if (process.platform !== "darwin") {
44
+ throw new Error("Daemon uninstallation is currently only supported on macOS");
45
+ }
46
+ if (process.getuid && process.getuid() !== 0) {
47
+ throw new Error("Daemon uninstallation requires sudo privileges. Please run with sudo.");
48
+ }
49
+ types.logger.info("Uninstalling Happy CLI daemon for macOS...");
50
+ await uninstall$1();
51
+ }
52
+
53
+ exports.uninstall = uninstall;
@@ -0,0 +1,51 @@
1
+ import { l as logger } from './types-fXgEaaqP.mjs';
2
+ import { existsSync, unlinkSync } from 'fs';
3
+ import { execSync } from 'child_process';
4
+ import 'axios';
5
+ import 'chalk';
6
+ import 'node:os';
7
+ import 'node:path';
8
+ import 'node:fs/promises';
9
+ import 'node:fs';
10
+ import 'node:events';
11
+ import 'socket.io-client';
12
+ import 'zod';
13
+ import 'node:crypto';
14
+ import 'tweetnacl';
15
+ import 'expo-server-sdk';
16
+
17
+ const PLIST_LABEL = "com.happy-cli.daemon";
18
+ const PLIST_FILE = `/Library/LaunchDaemons/${PLIST_LABEL}.plist`;
19
+ async function uninstall$1() {
20
+ try {
21
+ if (!existsSync(PLIST_FILE)) {
22
+ logger.info("Daemon plist not found. Nothing to uninstall.");
23
+ return;
24
+ }
25
+ try {
26
+ execSync(`launchctl unload ${PLIST_FILE}`, { stdio: "inherit" });
27
+ logger.info("Daemon stopped successfully");
28
+ } catch (error) {
29
+ logger.info("Failed to unload daemon (it might not be running)");
30
+ }
31
+ unlinkSync(PLIST_FILE);
32
+ logger.info(`Removed daemon plist from ${PLIST_FILE}`);
33
+ logger.info("Daemon uninstalled successfully");
34
+ } catch (error) {
35
+ logger.debug("Failed to uninstall daemon:", error);
36
+ throw error;
37
+ }
38
+ }
39
+
40
+ async function uninstall() {
41
+ if (process.platform !== "darwin") {
42
+ throw new Error("Daemon uninstallation is currently only supported on macOS");
43
+ }
44
+ if (process.getuid && process.getuid() !== 0) {
45
+ throw new Error("Daemon uninstallation requires sudo privileges. Please run with sudo.");
46
+ }
47
+ logger.info("Uninstalling Happy CLI daemon for macOS...");
48
+ await uninstall$1();
49
+ }
50
+
51
+ export { uninstall };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "happy-coder",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "Claude Code session sharing CLI",
5
5
  "author": "Kirill Dubovitskiy",
6
6
  "license": "MIT",
@@ -15,19 +15,32 @@
15
15
  "module": "./dist/index.mjs",
16
16
  "types": "./dist/index.d.cts",
17
17
  "exports": {
18
- "require": {
19
- "types": "./dist/index.d.cts",
20
- "default": "./dist/index.cjs"
18
+ ".": {
19
+ "require": {
20
+ "types": "./dist/index.d.cts",
21
+ "default": "./dist/index.cjs"
22
+ },
23
+ "import": {
24
+ "types": "./dist/index.d.mts",
25
+ "default": "./dist/index.mjs"
26
+ }
21
27
  },
22
- "import": {
23
- "types": "./dist/index.d.mts",
24
- "default": "./dist/index.mjs"
28
+ "./lib": {
29
+ "require": {
30
+ "types": "./dist/lib.d.cts",
31
+ "default": "./dist/lib.cjs"
32
+ },
33
+ "import": {
34
+ "types": "./dist/lib.d.mts",
35
+ "default": "./dist/lib.mjs"
36
+ }
25
37
  }
26
38
  },
27
39
  "files": [
28
40
  "dist",
29
41
  "bin",
30
- "scripts"
42
+ "scripts",
43
+ "package.json"
31
44
  ],
32
45
  "scripts": {
33
46
  "test": "vitest run",
@@ -35,16 +48,18 @@
35
48
  "build": "pkgroll",
36
49
  "prepublishOnly": "tsc --noEmit && yarn build && yarn test",
37
50
  "dev": "npx tsx --env-file .env.sample src/index.ts",
38
- "dev:local": "HANDY_SERVER_URL=http://localhost:3005 npx tsx --env-file .env.sample src/index.ts"
51
+ "dev:local-server": "HANDY_SERVER_URL=http://localhost:3005 npx tsx --env-file .env.sample src/index.ts"
39
52
  },
40
53
  "dependencies": {
41
54
  "@anthropic-ai/claude-code": "^1.0.51",
42
55
  "@anthropic-ai/sdk": "^0.56.0",
43
56
  "@modelcontextprotocol/sdk": "^1.15.1",
57
+ "@stablelib/base64": "^2.0.1",
44
58
  "@types/qrcode-terminal": "^0.12.2",
45
59
  "axios": "^1.10.0",
46
60
  "chalk": "^5.4.1",
47
61
  "expo-server-sdk": "^3.15.0",
62
+ "http-proxy-middleware": "^3.0.5",
48
63
  "qrcode-terminal": "^0.12.0",
49
64
  "socket.io-client": "^4.8.1",
50
65
  "tweetnacl": "^1.0.3",
@@ -62,4 +77,4 @@
62
77
  "typescript": "^5",
63
78
  "vitest": "^3.2.4"
64
79
  }
65
- }
80
+ }
@@ -1,38 +0,0 @@
1
- /**
2
- * Authentication module for handy-cli
3
- *
4
- * This module handles authentication with the handy server using public key cryptography.
5
- * It manages secret key generation, storage, and the authentication flow.
6
- *
7
- * Key responsibilities:
8
- * - Generate and persist secret keys
9
- * - Implement challenge-response authentication
10
- * - Obtain and manage auth tokens
11
- *
12
- * Design decisions:
13
- * - Secret keys are stored in the user's home directory for persistence
14
- * - Uses tweetnacl for cryptographic operations
15
- * - Auth tokens are kept in memory only (not persisted)
16
- */
17
- /**
18
- * Generate or load a secret key for authentication
19
- * Creates a new key if one doesn't exist, otherwise loads the existing key
20
- */
21
- export declare function getOrCreateSecretKey(): Promise<Uint8Array>;
22
- /**
23
- * Generate authentication challenge response
24
- */
25
- export declare function authChallenge(secret: Uint8Array): {
26
- challenge: Uint8Array;
27
- publicKey: Uint8Array;
28
- signature: Uint8Array;
29
- };
30
- /**
31
- * Authenticate with the server and obtain an auth token
32
- */
33
- export declare function authGetToken(serverUrl: string, secret: Uint8Array): Promise<string>;
34
- /**
35
- * Generate handy:// URL with the secret key encoded in base64url format
36
- * This URL is used for QR code generation to allow mobile clients to connect
37
- */
38
- export declare function generateHandyUrl(secret: Uint8Array): string;
package/dist/auth/auth.js DELETED
@@ -1,76 +0,0 @@
1
- /**
2
- * Authentication module for handy-cli
3
- *
4
- * This module handles authentication with the handy server using public key cryptography.
5
- * It manages secret key generation, storage, and the authentication flow.
6
- *
7
- * Key responsibilities:
8
- * - Generate and persist secret keys
9
- * - Implement challenge-response authentication
10
- * - Obtain and manage auth tokens
11
- *
12
- * Design decisions:
13
- * - Secret keys are stored in the user's home directory for persistence
14
- * - Uses tweetnacl for cryptographic operations
15
- * - Auth tokens are kept in memory only (not persisted)
16
- */
17
- import { getSecretKeyPath } from '#utils/paths';
18
- import axios from 'axios';
19
- import { existsSync, readFileSync, writeFileSync } from 'node:fs';
20
- import { chmod } from 'node:fs/promises';
21
- import nacl from 'tweetnacl';
22
- import { encodeBase64, encodeBase64Url, getRandomBytes } from './crypto.js';
23
- /**
24
- * Generate or load a secret key for authentication
25
- * Creates a new key if one doesn't exist, otherwise loads the existing key
26
- */
27
- export async function getOrCreateSecretKey() {
28
- const keyPath = getSecretKeyPath();
29
- if (existsSync(keyPath)) {
30
- const keyBase64 = readFileSync(keyPath, 'utf8').trim();
31
- return new Uint8Array(Buffer.from(keyBase64, 'base64'));
32
- }
33
- // Generate a new 32-byte secret key (256 bits)
34
- const secret = getRandomBytes(32);
35
- const keyBase64 = encodeBase64(secret);
36
- // Write to file with restricted permissions
37
- writeFileSync(keyPath, keyBase64);
38
- await chmod(keyPath, 0o600); // Read/write for owner only
39
- return secret;
40
- }
41
- /**
42
- * Generate authentication challenge response
43
- */
44
- export function authChallenge(secret) {
45
- const keypair = nacl.sign.keyPair.fromSeed(secret);
46
- const challenge = getRandomBytes(32);
47
- const signature = nacl.sign.detached(challenge, keypair.secretKey);
48
- return {
49
- challenge,
50
- publicKey: keypair.publicKey,
51
- signature
52
- };
53
- }
54
- /**
55
- * Authenticate with the server and obtain an auth token
56
- */
57
- export async function authGetToken(serverUrl, secret) {
58
- const { challenge, publicKey, signature } = authChallenge(secret);
59
- const response = await axios.post(`${serverUrl}/v1/auth`, {
60
- challenge: encodeBase64(challenge),
61
- publicKey: encodeBase64(publicKey),
62
- signature: encodeBase64(signature)
63
- });
64
- if (!response.data.success || !response.data.token) {
65
- throw new Error('Authentication failed');
66
- }
67
- return response.data.token;
68
- }
69
- /**
70
- * Generate handy:// URL with the secret key encoded in base64url format
71
- * This URL is used for QR code generation to allow mobile clients to connect
72
- */
73
- export function generateHandyUrl(secret) {
74
- const secretBase64Url = encodeBase64Url(secret);
75
- return `handy://${secretBase64Url}`;
76
- }
@@ -1,7 +0,0 @@
1
- /**
2
- * Tests for the authentication module
3
- *
4
- * These tests use the real handy server API to verify authentication works correctly.
5
- * No mocking is used as per project requirements.
6
- */
7
- export {};
@@ -1,96 +0,0 @@
1
- /**
2
- * Tests for the authentication module
3
- *
4
- * These tests use the real handy server API to verify authentication works correctly.
5
- * No mocking is used as per project requirements.
6
- */
7
- import { getConfig } from '#utils/config';
8
- import { getSecretKeyPath } from '#utils/paths';
9
- import { expect } from 'chai';
10
- import { existsSync, unlinkSync } from 'node:fs';
11
- import { authChallenge, authGetToken, generateHandyUrl, getOrCreateSecretKey } from './auth.js';
12
- import { encodeBase64 } from './crypto.js';
13
- describe('Authentication', () => {
14
- const keyPath = getSecretKeyPath();
15
- // Clean up any existing key before tests
16
- beforeEach(() => {
17
- if (existsSync(keyPath)) {
18
- unlinkSync(keyPath);
19
- }
20
- });
21
- // Clean up after tests
22
- afterEach(() => {
23
- if (existsSync(keyPath)) {
24
- unlinkSync(keyPath);
25
- }
26
- });
27
- describe('getOrCreateSecretKey', () => {
28
- it('should create a new secret key if none exists', async () => {
29
- const secret = await getOrCreateSecretKey();
30
- expect(secret).to.be.an.instanceOf(Uint8Array);
31
- expect(secret.length).to.equal(32);
32
- expect(existsSync(keyPath)).to.equal(true);
33
- });
34
- it('should return the same key on subsequent calls', async () => {
35
- const secret1 = await getOrCreateSecretKey();
36
- const secret2 = await getOrCreateSecretKey();
37
- expect(encodeBase64(secret1)).to.equal(encodeBase64(secret2));
38
- });
39
- });
40
- describe('authChallenge', () => {
41
- it('should generate valid challenge response', async () => {
42
- const secret = await getOrCreateSecretKey();
43
- const { challenge, publicKey, signature } = authChallenge(secret);
44
- expect(challenge).to.be.instanceOf(Uint8Array);
45
- expect(challenge.length).to.equal(32);
46
- expect(signature).to.be.instanceOf(Uint8Array);
47
- expect(signature.length).to.equal(64);
48
- expect(publicKey).to.be.instanceOf(Uint8Array);
49
- expect(publicKey.length).to.equal(32);
50
- });
51
- });
52
- describe('authGetToken', () => {
53
- it('should authenticate with the server and get a token', async () => {
54
- const config = getConfig();
55
- const secret = await getOrCreateSecretKey();
56
- const token = await authGetToken(config.serverUrl, secret);
57
- expect(token).to.be.a('string');
58
- expect(token.length).to.be.greaterThan(0);
59
- });
60
- it('should get valid tokens for the same public key', async () => {
61
- const config = getConfig();
62
- const secret = await getOrCreateSecretKey();
63
- const token1 = await authGetToken(config.serverUrl, secret);
64
- const token2 = await authGetToken(config.serverUrl, secret);
65
- // Both should be valid JWT tokens (format: header.payload.signature)
66
- expect(token1.split('.')).to.have.lengthOf(3);
67
- expect(token2.split('.')).to.have.lengthOf(3);
68
- });
69
- });
70
- describe('generateHandyUrl', () => {
71
- it('should generate a valid handy:// URL with base64url encoded secret', async () => {
72
- const secret = await getOrCreateSecretKey();
73
- const url = generateHandyUrl(secret);
74
- expect(url).to.be.a('string');
75
- expect(url).to.match(/^handy:\/\/[A-Za-z0-9_-]+$/);
76
- });
77
- it('should generate URLs that do not contain base64 padding or unsafe characters', async () => {
78
- const secret = await getOrCreateSecretKey();
79
- const url = generateHandyUrl(secret);
80
- // Extract just the base64url part (after handy://)
81
- const base64urlPart = url.slice('handy://'.length);
82
- // Base64url should not contain +, /, or = characters
83
- expect(base64urlPart).to.not.include('+');
84
- expect(base64urlPart).to.not.include('/');
85
- expect(base64urlPart).to.not.include('=');
86
- // Should be base64url safe characters only
87
- expect(base64urlPart).to.match(/^[A-Za-z0-9_-]+$/);
88
- });
89
- it('should generate consistent URLs for the same secret', async () => {
90
- const secret = await getOrCreateSecretKey();
91
- const url1 = generateHandyUrl(secret);
92
- const url2 = generateHandyUrl(secret);
93
- expect(url1).to.equal(url2);
94
- });
95
- });
96
- });
@@ -1,25 +0,0 @@
1
- /**
2
- * Crypto utilities for handy-cli
3
- *
4
- * This module provides cryptographic functions for authentication with the handy server.
5
- * It handles base64 encoding/decoding and random byte generation for secret keys.
6
- *
7
- * Key responsibilities:
8
- * - Base64 encoding/decoding for communication with server
9
- * - Base64URL encoding for handy:// URL generation
10
- * - Secure random byte generation for secret keys
11
- * - Conversion between different buffer formats
12
- */
13
- /**
14
- * Encode a Uint8Array to base64 string
15
- */
16
- export declare function encodeBase64(buffer: Uint8Array): string;
17
- /**
18
- * Encode a Uint8Array to base64url string (URL-safe base64)
19
- * Base64URL uses '-' instead of '+', '_' instead of '/', and removes padding
20
- */
21
- export declare function encodeBase64Url(buffer: Uint8Array): string;
22
- /**
23
- * Generate secure random bytes
24
- */
25
- export declare function getRandomBytes(size: number): Uint8Array;
@@ -1,36 +0,0 @@
1
- /**
2
- * Crypto utilities for handy-cli
3
- *
4
- * This module provides cryptographic functions for authentication with the handy server.
5
- * It handles base64 encoding/decoding and random byte generation for secret keys.
6
- *
7
- * Key responsibilities:
8
- * - Base64 encoding/decoding for communication with server
9
- * - Base64URL encoding for handy:// URL generation
10
- * - Secure random byte generation for secret keys
11
- * - Conversion between different buffer formats
12
- */
13
- import { randomBytes } from 'node:crypto';
14
- /**
15
- * Encode a Uint8Array to base64 string
16
- */
17
- export function encodeBase64(buffer) {
18
- return Buffer.from(buffer).toString('base64');
19
- }
20
- /**
21
- * Encode a Uint8Array to base64url string (URL-safe base64)
22
- * Base64URL uses '-' instead of '+', '_' instead of '/', and removes padding
23
- */
24
- export function encodeBase64Url(buffer) {
25
- return Buffer.from(buffer)
26
- .toString('base64')
27
- .replaceAll('+', '-')
28
- .replaceAll('/', '_')
29
- .replaceAll('=', '');
30
- }
31
- /**
32
- * Generate secure random bytes
33
- */
34
- export function getRandomBytes(size) {
35
- return new Uint8Array(randomBytes(size));
36
- }
@@ -1,54 +0,0 @@
1
- /**
2
- * Simplified Claude CLI integration
3
- *
4
- * This module provides a simple interface to spawn Claude CLI for each command.
5
- * Each command runs in its own process and exits when complete.
6
- *
7
- * Key responsibilities:
8
- * - Spawn Claude CLI with appropriate arguments
9
- * - Track session ID across command invocations
10
- * - Parse and emit Claude responses
11
- * - Handle process lifecycle for each command
12
- *
13
- * Design decisions:
14
- * - One process per command (no stdin interaction)
15
- * - Session ID persisted in memory only
16
- * - Kill any existing process before starting new one
17
- * - Simple event-based API for responses
18
- */
19
- import { EventEmitter } from 'node:events';
20
- export interface ClaudeOptions {
21
- model?: string;
22
- permissionMode?: 'auto' | 'default' | 'plan';
23
- skipPermissions?: boolean;
24
- workingDirectory: string;
25
- }
26
- export declare class Claude extends EventEmitter {
27
- private currentProcess?;
28
- private currentSessionId?;
29
- /**
30
- * Get the current session ID
31
- */
32
- getSessionId(): string | undefined;
33
- /**
34
- * Kill the current process if running
35
- */
36
- kill(): void;
37
- /**
38
- * Run a single Claude command
39
- * Kills any existing process and spawns a new one
40
- */
41
- runClaudeCodeTurn(command: string, sessionId: string | undefined, options: ClaudeOptions): Promise<void>;
42
- /**
43
- * Build command line arguments for Claude
44
- */
45
- private buildArgs;
46
- /**
47
- * Kill the current process and wait for it to exit
48
- */
49
- private killAndWait;
50
- /**
51
- * Process a line of output from Claude
52
- */
53
- private processOutput;
54
- }
@@ -1,170 +0,0 @@
1
- /**
2
- * Simplified Claude CLI integration
3
- *
4
- * This module provides a simple interface to spawn Claude CLI for each command.
5
- * Each command runs in its own process and exits when complete.
6
- *
7
- * Key responsibilities:
8
- * - Spawn Claude CLI with appropriate arguments
9
- * - Track session ID across command invocations
10
- * - Parse and emit Claude responses
11
- * - Handle process lifecycle for each command
12
- *
13
- * Design decisions:
14
- * - One process per command (no stdin interaction)
15
- * - Session ID persisted in memory only
16
- * - Kill any existing process before starting new one
17
- * - Simple event-based API for responses
18
- */
19
- import { logger } from '#utils/logger';
20
- import { spawn } from 'node:child_process';
21
- import { EventEmitter } from 'node:events';
22
- // eslint-disable-next-line unicorn/prefer-event-target
23
- export class Claude extends EventEmitter {
24
- currentProcess;
25
- currentSessionId;
26
- /**
27
- * Get the current session ID
28
- */
29
- getSessionId() {
30
- return this.currentSessionId;
31
- }
32
- /**
33
- * Kill the current process if running
34
- */
35
- kill() {
36
- if (this.currentProcess && !this.currentProcess.killed) {
37
- logger.info('Killing Claude process');
38
- this.currentProcess.kill();
39
- this.currentProcess = undefined;
40
- }
41
- }
42
- /**
43
- * Run a single Claude command
44
- * Kills any existing process and spawns a new one
45
- */
46
- async runClaudeCodeTurn(command, sessionId, options) {
47
- // Kill any existing process - wait for it to exit
48
- if (this.currentProcess && !this.currentProcess.killed) {
49
- logger.info('Killing existing Claude process');
50
- await this.killAndWait();
51
- }
52
- // Build command arguments (no session resuming for now)
53
- const args = this.buildArgs(command, undefined, options);
54
- logger.info('Spawning Claude CLI with args:', args);
55
- // Spawn the process
56
- this.currentProcess = spawn('claude', args, {
57
- cwd: options.workingDirectory,
58
- stdio: ['pipe', 'pipe', 'pipe']
59
- });
60
- // Close stdin immediately (we don't send input)
61
- this.currentProcess.stdin?.end();
62
- // Handle stdout (JSON responses)
63
- let outputBuffer = '';
64
- this.currentProcess.stdout?.on('data', (data) => {
65
- outputBuffer += data.toString();
66
- // Process complete lines
67
- const lines = outputBuffer.split('\n');
68
- outputBuffer = lines.pop() || '';
69
- for (const line of lines) {
70
- if (line.trim()) {
71
- this.processOutput(line);
72
- }
73
- }
74
- });
75
- // Handle stderr
76
- this.currentProcess.stderr?.on('data', (data) => {
77
- const error = data.toString();
78
- logger.error('Claude stderr:', error);
79
- this.emit('error', error);
80
- });
81
- // Handle process exit
82
- this.currentProcess.on('exit', (code, signal) => {
83
- logger.info(`Claude process exited with code ${code} and signal ${signal}`);
84
- this.emit('exit', { code, signal });
85
- this.currentProcess = undefined;
86
- });
87
- // Handle process errors
88
- this.currentProcess.on('error', (error) => {
89
- logger.error('Claude process error:', error);
90
- this.emit('processError', error);
91
- this.currentProcess = undefined;
92
- });
93
- }
94
- /**
95
- * Build command line arguments for Claude
96
- */
97
- buildArgs(command, sessionId, options) {
98
- const args = [
99
- '--print', command,
100
- '--output-format', 'stream-json',
101
- '--verbose'
102
- ];
103
- // Add model
104
- if (options.model) {
105
- args.push('--model', options.model);
106
- }
107
- // Add permission mode
108
- if (options.permissionMode) {
109
- const modeMap = {
110
- 'auto': 'acceptEdits',
111
- 'default': 'default',
112
- 'plan': 'bypassPermissions'
113
- };
114
- args.push('--permission-mode', modeMap[options.permissionMode]);
115
- }
116
- // Add skip permissions flag
117
- if (options.skipPermissions) {
118
- args.push('--dangerously-skip-permissions');
119
- }
120
- // Add session resume if we have a session ID
121
- if (sessionId) {
122
- args.push('--resume', sessionId);
123
- }
124
- return args;
125
- }
126
- /**
127
- * Kill the current process and wait for it to exit
128
- */
129
- async killAndWait() {
130
- if (!this.currentProcess || this.currentProcess.killed) {
131
- return;
132
- }
133
- return new Promise((resolve) => {
134
- const process = this.currentProcess;
135
- // Set up exit handler
136
- const exitHandler = () => {
137
- this.currentProcess = undefined;
138
- resolve();
139
- };
140
- process.once('exit', exitHandler);
141
- // Kill the process
142
- process.kill();
143
- // Set a timeout in case the process doesn't exit
144
- setTimeout(() => {
145
- process.removeListener('exit', exitHandler);
146
- this.currentProcess = undefined;
147
- resolve();
148
- }, 1000); // 1 second timeout
149
- });
150
- }
151
- /**
152
- * Process a line of output from Claude
153
- */
154
- processOutput(line) {
155
- try {
156
- const response = JSON.parse(line);
157
- // Capture session ID from responses
158
- if (response.session_id) {
159
- this.currentSessionId = response.session_id;
160
- logger.info('Session ID updated:', this.currentSessionId);
161
- }
162
- // Emit the parsed response
163
- this.emit('response', response);
164
- }
165
- catch {
166
- // Not JSON, emit as regular output
167
- this.emit('output', line);
168
- }
169
- }
170
- }