happy-mcp-server 0.1.0

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/LICENSE +21 -0
  2. package/README.md +159 -0
  3. package/dist/api/client.d.ts +39 -0
  4. package/dist/api/client.js +49 -0
  5. package/dist/auth/credentials.d.ts +22 -0
  6. package/dist/auth/credentials.js +80 -0
  7. package/dist/auth/crypto.d.ts +118 -0
  8. package/dist/auth/crypto.js +249 -0
  9. package/dist/auth/pairing.d.ts +16 -0
  10. package/dist/auth/pairing.js +90 -0
  11. package/dist/auth/refresh.d.ts +11 -0
  12. package/dist/auth/refresh.js +50 -0
  13. package/dist/config.d.ts +11 -0
  14. package/dist/config.js +13 -0
  15. package/dist/errors.d.ts +15 -0
  16. package/dist/errors.js +30 -0
  17. package/dist/index.d.ts +2 -0
  18. package/dist/index.js +306 -0
  19. package/dist/logger.d.ts +8 -0
  20. package/dist/logger.js +22 -0
  21. package/dist/relay/client.d.ts +34 -0
  22. package/dist/relay/client.js +242 -0
  23. package/dist/server.d.ts +16 -0
  24. package/dist/server.js +89 -0
  25. package/dist/session/keys.d.ts +25 -0
  26. package/dist/session/keys.js +41 -0
  27. package/dist/session/manager.d.ts +27 -0
  28. package/dist/session/manager.js +187 -0
  29. package/dist/session/types.d.ts +101 -0
  30. package/dist/session/types.js +1 -0
  31. package/dist/tools/answer_question.d.ts +5 -0
  32. package/dist/tools/answer_question.js +52 -0
  33. package/dist/tools/approve_permission.d.ts +4 -0
  34. package/dist/tools/approve_permission.js +54 -0
  35. package/dist/tools/deny_permission.d.ts +4 -0
  36. package/dist/tools/deny_permission.js +31 -0
  37. package/dist/tools/get_session.d.ts +4 -0
  38. package/dist/tools/get_session.js +106 -0
  39. package/dist/tools/list_computers.d.ts +4 -0
  40. package/dist/tools/list_computers.js +36 -0
  41. package/dist/tools/list_sessions.d.ts +4 -0
  42. package/dist/tools/list_sessions.js +46 -0
  43. package/dist/tools/send_message.d.ts +4 -0
  44. package/dist/tools/send_message.js +54 -0
  45. package/dist/tools/start_session.d.ts +5 -0
  46. package/dist/tools/start_session.js +49 -0
  47. package/dist/tools/watch_session.d.ts +4 -0
  48. package/dist/tools/watch_session.js +91 -0
  49. package/dist/types/wire.d.ts +148 -0
  50. package/dist/types/wire.js +9 -0
  51. package/package.json +66 -0
@@ -0,0 +1,249 @@
1
+ import { createHmac, createHash, createCipheriv, createDecipheriv, randomBytes } from 'crypto';
2
+ import nacl from 'tweetnacl';
3
+ // ============================================================
4
+ // Base64 Utilities
5
+ // ============================================================
6
+ export function encodeBase64(buf) {
7
+ return Buffer.from(buf).toString('base64');
8
+ }
9
+ export function decodeBase64(base64) {
10
+ return new Uint8Array(Buffer.from(base64, 'base64'));
11
+ }
12
+ export function encodeBase64Url(buf) {
13
+ return Buffer.from(buf).toString('base64url');
14
+ }
15
+ export function decodeBase64Url(base64url) {
16
+ return new Uint8Array(Buffer.from(base64url, 'base64url'));
17
+ }
18
+ // ============================================================
19
+ // HMAC-SHA512
20
+ // ============================================================
21
+ export function hmacSha512(key, data) {
22
+ const hmac = createHmac('sha512', key);
23
+ hmac.update(data);
24
+ return new Uint8Array(hmac.digest());
25
+ }
26
+ /**
27
+ * Derive root of key tree.
28
+ * CRITICAL: key = encode(usage + ' Master Seed'), data = seed
29
+ * Verified against upstream: packages/happy-agent/src/encryption.ts
30
+ */
31
+ export function deriveSecretKeyTreeRoot(seed, usage) {
32
+ const I = hmacSha512(new TextEncoder().encode(usage + ' Master Seed'), seed);
33
+ return { key: I.slice(0, 32), chainCode: I.slice(32) };
34
+ }
35
+ /**
36
+ * Derive child key from chain code.
37
+ * data = [0x00, ...encode(index)]
38
+ */
39
+ export function deriveSecretKeyTreeChild(chainCode, index) {
40
+ const indexBytes = new TextEncoder().encode(index);
41
+ const data = new Uint8Array(1 + indexBytes.length);
42
+ data[0] = 0x00;
43
+ data.set(indexBytes, 1);
44
+ const I = hmacSha512(chainCode, data);
45
+ return { key: I.slice(0, 32), chainCode: I.slice(32) };
46
+ }
47
+ /**
48
+ * Derive key at path. Root + iterate children.
49
+ * Test vectors:
50
+ * deriveKey(encode("test seed"), "test usage", [])
51
+ * => E6E55652456F9FE47D6FF46CA3614E85B499F77E7B340FBBB1553307CEDC1E74
52
+ * deriveKey(encode("test seed"), "test usage", ["child1", "child2"])
53
+ * => 1011C097D2105D27362B987A631496BBF68B836124D1D072E9D1613C6028CF75
54
+ */
55
+ export function deriveKey(seed, usage, path) {
56
+ let state = deriveSecretKeyTreeRoot(seed, usage);
57
+ for (const index of path) {
58
+ state = deriveSecretKeyTreeChild(state.chainCode, index);
59
+ }
60
+ return state.key;
61
+ }
62
+ // ============================================================
63
+ // Content Keypair (Curve25519 for NaCl Box)
64
+ // ============================================================
65
+ /**
66
+ * CRITICAL: Has a SHA-512 step to match libsodium's crypto_box_seed_keypair behavior.
67
+ * 1. Derive seed via HMAC tree
68
+ * 2. SHA-512(seed)[0:32] = box secret key
69
+ * 3. nacl.box.keyPair.fromSecretKey(boxSecretKey)
70
+ *
71
+ * Source: packages/happy-agent/src/encryption.ts:deriveContentKeyPair
72
+ */
73
+ export function deriveContentKeyPair(secret) {
74
+ const seed = deriveKey(secret, 'Happy EnCoder', ['content']);
75
+ const hashedSeed = new Uint8Array(createHash('sha512').update(seed).digest());
76
+ const boxSecretKey = hashedSeed.slice(0, 32);
77
+ return nacl.box.keyPair.fromSecretKey(boxSecretKey);
78
+ }
79
+ // ============================================================
80
+ // Auth Challenge (Ed25519 signing)
81
+ // ============================================================
82
+ /**
83
+ * Generate auth challenge for token refresh.
84
+ * CRITICAL: Uses nacl.sign.keyPair.fromSeed(secret) with the RAW account secret.
85
+ * NOT the derived content keypair (which is Curve25519, not Ed25519).
86
+ *
87
+ * Source: packages/happy-agent/src/encryption.ts:authChallenge
88
+ */
89
+ export function authChallenge(secret) {
90
+ const signingKeyPair = nacl.sign.keyPair.fromSeed(secret);
91
+ const challenge = randomBytes(32);
92
+ const signature = nacl.sign.detached(challenge, signingKeyPair.secretKey);
93
+ return { challenge, publicKey: signingKeyPair.publicKey, signature };
94
+ }
95
+ // ============================================================
96
+ // NaCl Box (Key Exchange / Decryption)
97
+ // ============================================================
98
+ /**
99
+ * Decrypt a NaCl box bundle: [ephemeralPubKey(32)][nonce(24)][ciphertext]
100
+ * Returns decrypted Uint8Array or null on failure.
101
+ *
102
+ * Source: packages/happy-agent/src/encryption.ts
103
+ */
104
+ export function decryptBoxBundle(bundle, recipientSecretKey) {
105
+ if (bundle.length < 56)
106
+ return null; // 32 + 24 minimum
107
+ const ephemeralPubKey = bundle.slice(0, 32);
108
+ const nonce = bundle.slice(32, 56);
109
+ const ciphertext = bundle.slice(56);
110
+ return nacl.box.open(ciphertext, nonce, ephemeralPubKey, recipientSecretKey);
111
+ }
112
+ /**
113
+ * Encrypt data for a public key using NaCl box.
114
+ * Returns: [ephemeralPubKey(32)][nonce(24)][ciphertext]
115
+ */
116
+ export function encryptForPublicKey(data, recipientPublicKey) {
117
+ const ephemeral = nacl.box.keyPair();
118
+ const nonce = randomBytes(24);
119
+ const encrypted = nacl.box(data, nonce, recipientPublicKey, ephemeral.secretKey);
120
+ if (!encrypted)
121
+ throw new Error('NaCl box encryption failed');
122
+ const result = new Uint8Array(32 + 24 + encrypted.length);
123
+ result.set(ephemeral.publicKey, 0);
124
+ result.set(nonce, 32);
125
+ result.set(encrypted, 56);
126
+ return result;
127
+ }
128
+ // ============================================================
129
+ // AES-256-GCM (Modern encryption)
130
+ // ============================================================
131
+ /**
132
+ * Encrypt with AES-256-GCM.
133
+ * Bundle format: [version(1)=0x00][nonce(12)][ciphertext(N)][authTag(16)]
134
+ */
135
+ export function encryptAesGcm(key, data) {
136
+ const nonce = randomBytes(12);
137
+ const plaintext = Buffer.from(JSON.stringify(data), 'utf-8');
138
+ const cipher = createCipheriv('aes-256-gcm', key, nonce);
139
+ const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]);
140
+ const authTag = cipher.getAuthTag();
141
+ const result = new Uint8Array(1 + 12 + encrypted.length + 16);
142
+ result[0] = 0x00; // version byte
143
+ result.set(nonce, 1);
144
+ result.set(encrypted, 13);
145
+ result.set(authTag, 13 + encrypted.length);
146
+ return result;
147
+ }
148
+ /**
149
+ * Decrypt AES-256-GCM bundle.
150
+ * Returns parsed JSON or null on failure.
151
+ */
152
+ export function decryptAesGcm(key, bundle) {
153
+ if (bundle.length < 29)
154
+ return null; // 1 + 12 + 16 minimum
155
+ if (bundle[0] !== 0x00)
156
+ return null; // version check
157
+ const nonce = bundle.slice(1, 13);
158
+ const authTag = bundle.slice(bundle.length - 16);
159
+ const ciphertext = bundle.slice(13, bundle.length - 16);
160
+ try {
161
+ const decipher = createDecipheriv('aes-256-gcm', key, nonce);
162
+ decipher.setAuthTag(authTag);
163
+ const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
164
+ return JSON.parse(decrypted.toString('utf-8'));
165
+ }
166
+ catch {
167
+ return null;
168
+ }
169
+ }
170
+ // ============================================================
171
+ // NaCl SecretBox (Legacy encryption)
172
+ // ============================================================
173
+ /**
174
+ * Encrypt with NaCl SecretBox.
175
+ * Bundle format: [nonce(24)][ciphertext]
176
+ */
177
+ export function encryptSecretBox(key, data) {
178
+ const nonce = randomBytes(24);
179
+ const plaintext = new TextEncoder().encode(JSON.stringify(data));
180
+ const encrypted = nacl.secretbox(plaintext, nonce, key);
181
+ const result = new Uint8Array(24 + encrypted.length);
182
+ result.set(nonce, 0);
183
+ result.set(encrypted, 24);
184
+ return result;
185
+ }
186
+ /**
187
+ * Decrypt NaCl SecretBox bundle.
188
+ * Returns parsed JSON or null on failure.
189
+ */
190
+ export function decryptSecretBox(key, bundle) {
191
+ if (bundle.length < 24)
192
+ return null;
193
+ const nonce = bundle.slice(0, 24);
194
+ const ciphertext = bundle.slice(24);
195
+ const decrypted = nacl.secretbox.open(ciphertext, nonce, key);
196
+ if (!decrypted)
197
+ return null;
198
+ try {
199
+ return JSON.parse(new TextDecoder().decode(decrypted));
200
+ }
201
+ catch {
202
+ return null;
203
+ }
204
+ }
205
+ /**
206
+ * Encrypt data using the appropriate variant.
207
+ */
208
+ export function encrypt(key, variant, data) {
209
+ return variant === 'dataKey' ? encryptAesGcm(key, data) : encryptSecretBox(key, data);
210
+ }
211
+ /**
212
+ * Decrypt data using the appropriate variant.
213
+ */
214
+ export function decrypt(key, variant, bundle) {
215
+ return variant === 'dataKey' ? decryptAesGcm(key, bundle) : decryptSecretBox(key, bundle);
216
+ }
217
+ /**
218
+ * Encrypt data and return base64 string.
219
+ */
220
+ export function encryptToBase64(encryption, data) {
221
+ return encodeBase64(encrypt(encryption.key, encryption.variant, data));
222
+ }
223
+ /**
224
+ * Decrypt base64-encoded data.
225
+ */
226
+ export function decryptFromBase64(encryption, base64) {
227
+ return decrypt(encryption.key, encryption.variant, decodeBase64(base64));
228
+ }
229
+ /**
230
+ * Resolve session encryption key.
231
+ * CRITICAL: Strip version byte (first byte) before NaCl box decryption.
232
+ * Pass contentKeyPair.secretKey, NOT the full keypair.
233
+ *
234
+ * Source: packages/happy-agent/src/api.ts:resolveSessionEncryption
235
+ */
236
+ export function resolveSessionEncryption(dataEncryptionKey, credentials) {
237
+ if (dataEncryptionKey) {
238
+ const encrypted = decodeBase64(dataEncryptionKey);
239
+ // CRITICAL: Strip version byte (first byte, always 0x00)
240
+ const bundle = encrypted.slice(1);
241
+ const sessionKey = decryptBoxBundle(bundle, credentials.contentKeyPair.secretKey);
242
+ if (!sessionKey) {
243
+ throw new Error('Failed to decrypt session encryption key');
244
+ }
245
+ return { key: sessionKey, variant: 'dataKey' };
246
+ }
247
+ // Legacy: use account secret directly
248
+ return { key: credentials.secret, variant: 'legacy' };
249
+ }
@@ -0,0 +1,16 @@
1
+ import type { Credentials } from './crypto.js';
2
+ import type { Config } from '../config.js';
3
+ /**
4
+ * Load existing credentials or initiate pairing flow.
5
+ */
6
+ export declare function loadOrPairCredentials(config: Config): Promise<Credentials>;
7
+ /**
8
+ * Perform the QR pairing flow.
9
+ * 1. Generate ephemeral Curve25519 keypair
10
+ * 2. POST public key to relay
11
+ * 3. Display QR code on stderr
12
+ * 4. Poll until authorized
13
+ * 5. Decrypt account secret
14
+ * 6. Persist credentials
15
+ */
16
+ export declare function performPairing(config: Config): Promise<Credentials>;
@@ -0,0 +1,90 @@
1
+ import axios from 'axios';
2
+ import nacl from 'tweetnacl';
3
+ import qrcode from 'qrcode-terminal';
4
+ import { encodeBase64, encodeBase64Url, decodeBase64, decryptBoxBundle } from './crypto.js';
5
+ import { writeCredentials, readCredentials, validateFilePermissions } from './credentials.js';
6
+ import { logger } from '../logger.js';
7
+ import { AuthError } from '../errors.js';
8
+ const POLL_INTERVAL = 1000;
9
+ const AUTH_TIMEOUT = 120_000;
10
+ function sleep(ms) {
11
+ return new Promise(resolve => setTimeout(resolve, ms));
12
+ }
13
+ /**
14
+ * Load existing credentials or initiate pairing flow.
15
+ */
16
+ export async function loadOrPairCredentials(config) {
17
+ const creds = readCredentials(config.credentialsPath);
18
+ if (creds) {
19
+ validateFilePermissions(config.credentialsPath);
20
+ logger.info('Loaded existing credentials');
21
+ return creds;
22
+ }
23
+ console.error('[happy-mcp] No credentials found. Starting pairing flow...');
24
+ return performPairing(config);
25
+ }
26
+ /**
27
+ * Perform the QR pairing flow.
28
+ * 1. Generate ephemeral Curve25519 keypair
29
+ * 2. POST public key to relay
30
+ * 3. Display QR code on stderr
31
+ * 4. Poll until authorized
32
+ * 5. Decrypt account secret
33
+ * 6. Persist credentials
34
+ */
35
+ export async function performPairing(config) {
36
+ // 1. Generate ephemeral Curve25519 keypair
37
+ const seed = nacl.randomBytes(32);
38
+ const keypair = nacl.box.keyPair.fromSecretKey(seed);
39
+ const publicKeyBase64 = encodeBase64(keypair.publicKey);
40
+ const publicKeyBase64Url = encodeBase64Url(keypair.publicKey);
41
+ // 2. POST to /v1/auth/account/request
42
+ try {
43
+ await axios.post(`${config.serverUrl}/v1/auth/account/request`, {
44
+ publicKey: publicKeyBase64,
45
+ });
46
+ }
47
+ catch (err) {
48
+ throw new AuthError(`Failed to initiate pairing: ${err.message}`);
49
+ }
50
+ // 3. Display QR code on stderr
51
+ const qrData = `happy:///account?${publicKeyBase64Url}`;
52
+ console.error('\nScan this QR code with the Happy app to pair:\n');
53
+ qrcode.generate(qrData, { small: true }, (code) => {
54
+ console.error(code);
55
+ });
56
+ console.error(`\nOr visit: https://app.happy.engineering/account/connect#key=${publicKeyBase64Url}\n`);
57
+ console.error('Waiting for authorization...');
58
+ // 4. Poll until authorized (with timeout)
59
+ const startTime = Date.now();
60
+ while (Date.now() - startTime < AUTH_TIMEOUT) {
61
+ await sleep(POLL_INTERVAL);
62
+ try {
63
+ const res = await axios.post(`${config.serverUrl}/v1/auth/account/request`, {
64
+ publicKey: publicKeyBase64,
65
+ });
66
+ if (res.data.state === 'authorized') {
67
+ // 5. Decrypt the account secret
68
+ const encryptedResponse = decodeBase64(res.data.response);
69
+ const accountSecret = decryptBoxBundle(encryptedResponse, keypair.secretKey);
70
+ if (!accountSecret) {
71
+ throw new AuthError('Failed to decrypt pairing response');
72
+ }
73
+ // 6. Persist credentials
74
+ writeCredentials(config.credentialsPath, res.data.token, accountSecret, config.serverUrl);
75
+ console.error('[happy-mcp] Paired successfully!');
76
+ const creds = readCredentials(config.credentialsPath);
77
+ if (!creds) {
78
+ throw new AuthError('Failed to read back written credentials');
79
+ }
80
+ return creds;
81
+ }
82
+ }
83
+ catch (err) {
84
+ if (err instanceof AuthError)
85
+ throw err;
86
+ logger.debug('Polling...', err.message);
87
+ }
88
+ }
89
+ throw new AuthError('Pairing timed out after 2 minutes. Please try again.');
90
+ }
@@ -0,0 +1,11 @@
1
+ import type { Credentials } from './crypto.js';
2
+ import type { Config } from '../config.js';
3
+ /**
4
+ * Refresh the JWT token using Ed25519 challenge-response.
5
+ * Uses a single-flight mutex so concurrent callers share one refresh.
6
+ *
7
+ * Endpoint: POST /v1/auth
8
+ * Body: { challenge: base64, publicKey: base64, signature: base64 }
9
+ * Response: { success: boolean, token: string }
10
+ */
11
+ export declare function refreshToken(credentials: Credentials, config: Config): Promise<string>;
@@ -0,0 +1,50 @@
1
+ import axios from 'axios';
2
+ import { authChallenge, encodeBase64 } from './crypto.js';
3
+ import { writeCredentials } from './credentials.js';
4
+ import { logger } from '../logger.js';
5
+ import { AuthError } from '../errors.js';
6
+ let refreshPromise = null;
7
+ /**
8
+ * Refresh the JWT token using Ed25519 challenge-response.
9
+ * Uses a single-flight mutex so concurrent callers share one refresh.
10
+ *
11
+ * Endpoint: POST /v1/auth
12
+ * Body: { challenge: base64, publicKey: base64, signature: base64 }
13
+ * Response: { success: boolean, token: string }
14
+ */
15
+ export async function refreshToken(credentials, config) {
16
+ // Single-flight: if a refresh is already in progress, return that promise
17
+ if (refreshPromise) {
18
+ return refreshPromise;
19
+ }
20
+ refreshPromise = doRefresh(credentials, config).finally(() => {
21
+ refreshPromise = null;
22
+ });
23
+ return refreshPromise;
24
+ }
25
+ async function doRefresh(credentials, config) {
26
+ logger.info('Refreshing JWT token...');
27
+ const { challenge, publicKey, signature } = authChallenge(credentials.secret);
28
+ try {
29
+ const res = await axios.post(`${config.serverUrl}/v1/auth`, {
30
+ challenge: encodeBase64(challenge),
31
+ publicKey: encodeBase64(publicKey),
32
+ signature: encodeBase64(signature),
33
+ });
34
+ if (!res.data.success || !res.data.token) {
35
+ throw new AuthError('Token refresh failed: server returned unsuccessful response');
36
+ }
37
+ const newToken = res.data.token;
38
+ // Update credentials on disk (preserve serverUrl from config)
39
+ writeCredentials(config.credentialsPath, newToken, credentials.secret, config.serverUrl);
40
+ // Update in-memory token
41
+ credentials.token = newToken;
42
+ logger.info('Token refreshed successfully');
43
+ return newToken;
44
+ }
45
+ catch (err) {
46
+ if (err instanceof AuthError)
47
+ throw err;
48
+ throw new AuthError(`Token refresh failed: ${err.message}`);
49
+ }
50
+ }
@@ -0,0 +1,11 @@
1
+ export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
2
+ export interface Config {
3
+ serverUrl: string;
4
+ computers: string[];
5
+ projectPaths: string[];
6
+ credentialsPath: string;
7
+ logLevel: LogLevel;
8
+ sessionCacheTtl: number;
9
+ enableStart: boolean;
10
+ }
11
+ export declare function loadConfig(): Config;
package/dist/config.js ADDED
@@ -0,0 +1,13 @@
1
+ import { hostname } from 'os';
2
+ import { homedir } from 'os';
3
+ import { join } from 'path';
4
+ export function loadConfig() {
5
+ const serverUrl = (process.env.HAPPY_SERVER_URL ?? 'https://api.cluster-fluster.com').replace(/\/+$/, '');
6
+ const computers = process.env.HAPPY_MCP_COMPUTERS?.split(',').map(s => s.trim()).filter(Boolean) ?? [hostname()];
7
+ const projectPaths = process.env.HAPPY_MCP_PROJECT_PATHS?.split(',').map(s => s.trim()).filter(Boolean) ?? [process.cwd()];
8
+ const credentialsPath = process.env.HAPPY_MCP_CREDENTIALS_PATH ?? join(homedir(), '.happy-mcp', 'credentials.json');
9
+ const logLevel = (process.env.HAPPY_MCP_LOG_LEVEL ?? 'warn');
10
+ const sessionCacheTtl = parseInt(process.env.HAPPY_MCP_SESSION_CACHE_TTL ?? '300', 10);
11
+ const enableStart = process.env.HAPPY_MCP_ENABLE_START !== 'false';
12
+ return { serverUrl, computers, projectPaths, credentialsPath, logLevel, sessionCacheTtl, enableStart };
13
+ }
@@ -0,0 +1,15 @@
1
+ export declare class HappyMcpError extends Error {
2
+ constructor(message: string);
3
+ }
4
+ export declare class CryptoError extends HappyMcpError {
5
+ constructor(message: string);
6
+ }
7
+ export declare class AuthError extends HappyMcpError {
8
+ constructor(message: string);
9
+ }
10
+ export declare class RelayError extends HappyMcpError {
11
+ constructor(message: string);
12
+ }
13
+ export declare class ToolError extends HappyMcpError {
14
+ constructor(message: string);
15
+ }
package/dist/errors.js ADDED
@@ -0,0 +1,30 @@
1
+ export class HappyMcpError extends Error {
2
+ constructor(message) {
3
+ super(message);
4
+ this.name = 'HappyMcpError';
5
+ }
6
+ }
7
+ export class CryptoError extends HappyMcpError {
8
+ constructor(message) {
9
+ super(message);
10
+ this.name = 'CryptoError';
11
+ }
12
+ }
13
+ export class AuthError extends HappyMcpError {
14
+ constructor(message) {
15
+ super(message);
16
+ this.name = 'AuthError';
17
+ }
18
+ }
19
+ export class RelayError extends HappyMcpError {
20
+ constructor(message) {
21
+ super(message);
22
+ this.name = 'RelayError';
23
+ }
24
+ }
25
+ export class ToolError extends HappyMcpError {
26
+ constructor(message) {
27
+ super(message);
28
+ this.name = 'ToolError';
29
+ }
30
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};