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.
- package/dist/index.cjs +354 -917
- package/dist/index.mjs +280 -843
- package/dist/install-B2r_gX72.cjs +109 -0
- package/dist/install-HKe7dyS4.mjs +107 -0
- package/dist/lib.cjs +32 -0
- package/dist/lib.d.cts +727 -0
- package/dist/lib.d.mts +727 -0
- package/dist/lib.mjs +14 -0
- package/dist/run-FBXkmmN7.mjs +32 -0
- package/dist/run-q2To6b-c.cjs +34 -0
- package/dist/types-fXgEaaqP.mjs +861 -0
- package/dist/types-mykDX2xe.cjs +872 -0
- package/dist/uninstall-C42CoSCI.cjs +53 -0
- package/dist/uninstall-CLkTtlMv.mjs +51 -0
- package/package.json +25 -10
- package/dist/auth/auth.d.ts +0 -38
- package/dist/auth/auth.js +0 -76
- package/dist/auth/auth.test.d.ts +0 -7
- package/dist/auth/auth.test.js +0 -96
- package/dist/auth/crypto.d.ts +0 -25
- package/dist/auth/crypto.js +0 -36
- package/dist/claude/claude.d.ts +0 -54
- package/dist/claude/claude.js +0 -170
- package/dist/claude/claude.test.d.ts +0 -7
- package/dist/claude/claude.test.js +0 -130
- package/dist/claude/types.d.ts +0 -37
- package/dist/claude/types.js +0 -7
- package/dist/commands/start.d.ts +0 -38
- package/dist/commands/start.js +0 -161
- package/dist/commands/start.test.d.ts +0 -7
- package/dist/commands/start.test.js +0 -307
- package/dist/handlers/message-handler.d.ts +0 -65
- package/dist/handlers/message-handler.js +0 -187
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/session/service.d.ts +0 -27
- package/dist/session/service.js +0 -93
- package/dist/session/service.test.d.ts +0 -7
- package/dist/session/service.test.js +0 -71
- package/dist/session/types.d.ts +0 -44
- package/dist/session/types.js +0 -4
- package/dist/socket/client.d.ts +0 -50
- package/dist/socket/client.js +0 -136
- package/dist/socket/client.test.d.ts +0 -7
- package/dist/socket/client.test.js +0 -74
- package/dist/socket/types.d.ts +0 -80
- package/dist/socket/types.js +0 -12
- package/dist/utils/config.d.ts +0 -22
- package/dist/utils/config.js +0 -23
- package/dist/utils/logger.d.ts +0 -26
- package/dist/utils/logger.js +0 -60
- package/dist/utils/paths.d.ts +0 -18
- package/dist/utils/paths.js +0 -24
- package/dist/utils/qrcode.d.ts +0 -19
- package/dist/utils/qrcode.js +0 -37
- package/dist/utils/qrcode.test.d.ts +0 -7
- 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.
|
|
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
|
-
"
|
|
19
|
-
"
|
|
20
|
-
|
|
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
|
-
"
|
|
23
|
-
"
|
|
24
|
-
|
|
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
|
+
}
|
package/dist/auth/auth.d.ts
DELETED
|
@@ -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
|
-
}
|
package/dist/auth/auth.test.d.ts
DELETED
package/dist/auth/auth.test.js
DELETED
|
@@ -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
|
-
});
|
package/dist/auth/crypto.d.ts
DELETED
|
@@ -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;
|
package/dist/auth/crypto.js
DELETED
|
@@ -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
|
-
}
|
package/dist/claude/claude.d.ts
DELETED
|
@@ -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
|
-
}
|
package/dist/claude/claude.js
DELETED
|
@@ -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
|
-
}
|