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
package/dist/index.cjs ADDED
@@ -0,0 +1,603 @@
1
+ 'use strict';
2
+
3
+ var axios = require('axios');
4
+ var fs = require('node:fs');
5
+ var promises = require('node:fs/promises');
6
+ var node_crypto = require('node:crypto');
7
+ var tweetnacl = require('tweetnacl');
8
+ var node_os = require('node:os');
9
+ var node_path = require('node:path');
10
+ var chalk = require('chalk');
11
+ var node_events = require('node:events');
12
+ var socket_ioClient = require('socket.io-client');
13
+ var zod = require('zod');
14
+ var qrcode = require('qrcode-terminal');
15
+ var node_child_process = require('node:child_process');
16
+
17
+ function _interopNamespaceDefault(e) {
18
+ var n = Object.create(null);
19
+ if (e) {
20
+ Object.keys(e).forEach(function (k) {
21
+ if (k !== 'default') {
22
+ var d = Object.getOwnPropertyDescriptor(e, k);
23
+ Object.defineProperty(n, k, d.get ? d : {
24
+ enumerable: true,
25
+ get: function () { return e[k]; }
26
+ });
27
+ }
28
+ });
29
+ }
30
+ n.default = e;
31
+ return Object.freeze(n);
32
+ }
33
+
34
+ var fs__namespace = /*#__PURE__*/_interopNamespaceDefault(fs);
35
+
36
+ function encodeBase64(buffer) {
37
+ return Buffer.from(buffer).toString("base64");
38
+ }
39
+ function encodeBase64Url(buffer) {
40
+ return Buffer.from(buffer).toString("base64").replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
41
+ }
42
+ function decodeBase64(base64) {
43
+ return new Uint8Array(Buffer.from(base64, "base64"));
44
+ }
45
+ function getRandomBytes(size) {
46
+ return new Uint8Array(node_crypto.randomBytes(size));
47
+ }
48
+ function encrypt(data, secret) {
49
+ const nonce = getRandomBytes(tweetnacl.secretbox.nonceLength);
50
+ const encrypted = tweetnacl.secretbox(new TextEncoder().encode(JSON.stringify(data)), nonce, secret);
51
+ const result = new Uint8Array(nonce.length + encrypted.length);
52
+ result.set(nonce);
53
+ result.set(encrypted, nonce.length);
54
+ return result;
55
+ }
56
+ function decrypt(data, secret) {
57
+ const nonce = data.slice(0, tweetnacl.secretbox.nonceLength);
58
+ const encrypted = data.slice(tweetnacl.secretbox.nonceLength);
59
+ const decrypted = tweetnacl.secretbox.open(encrypted, nonce, secret);
60
+ if (!decrypted) {
61
+ return null;
62
+ }
63
+ return JSON.parse(new TextDecoder().decode(decrypted));
64
+ }
65
+ function authChallenge(secret) {
66
+ const keypair = tweetnacl.sign.keyPair.fromSeed(secret);
67
+ const challenge = getRandomBytes(32);
68
+ const signature = tweetnacl.sign.detached(challenge, keypair.secretKey);
69
+ return {
70
+ challenge,
71
+ publicKey: keypair.publicKey,
72
+ signature
73
+ };
74
+ }
75
+
76
+ async function getOrCreateSecretKey() {
77
+ const keyPath = node_path.join(node_os.homedir(), ".handy", "access.key");
78
+ if (fs.existsSync(keyPath)) {
79
+ const keyBase642 = fs.readFileSync(keyPath, "utf8").trim();
80
+ return new Uint8Array(Buffer.from(keyBase642, "base64"));
81
+ }
82
+ const secret = getRandomBytes(32);
83
+ const keyBase64 = encodeBase64(secret);
84
+ fs.mkdirSync(node_path.join(node_os.homedir(), ".handy"), { recursive: true });
85
+ fs.writeFileSync(keyPath, keyBase64);
86
+ await promises.chmod(keyPath, 384);
87
+ return secret;
88
+ }
89
+ async function authGetToken(secret) {
90
+ const { challenge, publicKey, signature } = authChallenge(secret);
91
+ const response = await axios.post(`https://handy-api.korshakov.org/v1/auth`, {
92
+ challenge: encodeBase64(challenge),
93
+ publicKey: encodeBase64(publicKey),
94
+ signature: encodeBase64(signature)
95
+ });
96
+ if (!response.data.success || !response.data.token) {
97
+ throw new Error("Authentication failed");
98
+ }
99
+ return response.data.token;
100
+ }
101
+ function generateAppUrl(secret) {
102
+ const secretBase64Url = encodeBase64Url(secret);
103
+ return `handy://${secretBase64Url}`;
104
+ }
105
+
106
+ class Logger {
107
+ debug(message, ...args) {
108
+ if (process.env.DEBUG) {
109
+ this.log("DEBUG" /* DEBUG */, message, ...args);
110
+ }
111
+ }
112
+ error(message, ...args) {
113
+ this.log("ERROR" /* ERROR */, message, ...args);
114
+ }
115
+ info(message, ...args) {
116
+ this.log("INFO" /* INFO */, message, ...args);
117
+ }
118
+ warn(message, ...args) {
119
+ this.log("WARN" /* WARN */, message, ...args);
120
+ }
121
+ getTimestamp() {
122
+ return (/* @__PURE__ */ new Date()).toISOString();
123
+ }
124
+ log(level, message, ...args) {
125
+ const timestamp = this.getTimestamp();
126
+ const prefix = `[${timestamp}] [${level}]`;
127
+ switch (level) {
128
+ case "DEBUG" /* DEBUG */: {
129
+ console.log(chalk.gray(prefix), message, ...args);
130
+ break;
131
+ }
132
+ case "ERROR" /* ERROR */: {
133
+ console.error(chalk.red(prefix), message, ...args);
134
+ break;
135
+ }
136
+ case "INFO" /* INFO */: {
137
+ console.log(chalk.blue(prefix), message, ...args);
138
+ break;
139
+ }
140
+ case "WARN" /* WARN */: {
141
+ console.log(chalk.yellow(prefix), message, ...args);
142
+ break;
143
+ }
144
+ }
145
+ }
146
+ }
147
+ const logger = new Logger();
148
+
149
+ const SessionMessageContentSchema = zod.z.object({
150
+ c: zod.z.string(),
151
+ // Base64 encoded encrypted content
152
+ t: zod.z.literal("encrypted")
153
+ });
154
+ const UpdateBodySchema = zod.z.object({
155
+ message: zod.z.object({
156
+ id: zod.z.string(),
157
+ seq: zod.z.number(),
158
+ content: SessionMessageContentSchema
159
+ }),
160
+ sid: zod.z.string(),
161
+ // Session ID
162
+ t: zod.z.literal("new-message")
163
+ });
164
+ zod.z.object({
165
+ id: zod.z.string(),
166
+ seq: zod.z.number(),
167
+ body: UpdateBodySchema,
168
+ createdAt: zod.z.number()
169
+ });
170
+ zod.z.object({
171
+ createdAt: zod.z.number(),
172
+ id: zod.z.string(),
173
+ seq: zod.z.number(),
174
+ updatedAt: zod.z.number()
175
+ });
176
+ zod.z.object({
177
+ content: SessionMessageContentSchema,
178
+ createdAt: zod.z.number(),
179
+ id: zod.z.string(),
180
+ seq: zod.z.number(),
181
+ updatedAt: zod.z.number()
182
+ });
183
+ zod.z.object({
184
+ session: zod.z.object({
185
+ id: zod.z.string(),
186
+ tag: zod.z.string(),
187
+ seq: zod.z.number(),
188
+ createdAt: zod.z.number(),
189
+ updatedAt: zod.z.number()
190
+ })
191
+ });
192
+ const UserMessageSchema = zod.z.object({
193
+ role: zod.z.literal("user"),
194
+ content: zod.z.object({
195
+ type: zod.z.literal("text"),
196
+ text: zod.z.string()
197
+ })
198
+ });
199
+ const AgentMessageSchema = zod.z.object({
200
+ role: zod.z.literal("agent"),
201
+ content: zod.z.any()
202
+ });
203
+ zod.z.union([UserMessageSchema, AgentMessageSchema]);
204
+
205
+ class ApiSessionClient extends node_events.EventEmitter {
206
+ token;
207
+ secret;
208
+ sessionId;
209
+ socket;
210
+ receivedMessages = /* @__PURE__ */ new Set();
211
+ pendingMessages = [];
212
+ pendingMessageCallback = null;
213
+ constructor(token, secret, sessionId) {
214
+ super();
215
+ this.token = token;
216
+ this.secret = secret;
217
+ this.sessionId = sessionId;
218
+ this.socket = socket_ioClient.io("https://handy-api.korshakov.org", {
219
+ auth: {
220
+ token: this.token
221
+ },
222
+ path: "/v1/updates",
223
+ reconnection: true,
224
+ reconnectionAttempts: Infinity,
225
+ reconnectionDelay: 1e3,
226
+ reconnectionDelayMax: 5e3,
227
+ transports: ["websocket"],
228
+ withCredentials: true,
229
+ autoConnect: false
230
+ });
231
+ this.socket.on("connect", () => {
232
+ logger.info("Socket connected successfully");
233
+ });
234
+ this.socket.on("disconnect", (reason) => {
235
+ logger.warn("Socket disconnected:", reason);
236
+ });
237
+ this.socket.on("connect_error", (error) => {
238
+ logger.error("Socket connection error:", error.message);
239
+ });
240
+ this.socket.on("update", (data) => {
241
+ if (data.body.t === "new-message" && data.body.message.content.t === "encrypted") {
242
+ const body = decrypt(decodeBase64(data.body.message.content.c), this.secret);
243
+ const result = UserMessageSchema.safeParse(body);
244
+ if (result.success) {
245
+ if (!this.receivedMessages.has(data.body.message.id)) {
246
+ this.receivedMessages.add(data.body.message.id);
247
+ if (this.pendingMessageCallback) {
248
+ this.pendingMessageCallback(result.data);
249
+ } else {
250
+ this.pendingMessages.push(result.data);
251
+ }
252
+ }
253
+ }
254
+ }
255
+ });
256
+ this.socket.connect();
257
+ }
258
+ onUserMessage(callback) {
259
+ this.pendingMessageCallback = callback;
260
+ while (this.pendingMessages.length > 0) {
261
+ callback(this.pendingMessages.shift());
262
+ }
263
+ }
264
+ /**
265
+ * Send message to session
266
+ * @param body - Message body
267
+ */
268
+ sendMessage(body) {
269
+ let content = {
270
+ role: "agent",
271
+ content: body
272
+ };
273
+ const encrypted = encodeBase64(encrypt(content, this.secret));
274
+ this.socket.emit("message", {
275
+ sid: this.sessionId,
276
+ message: encrypted
277
+ });
278
+ }
279
+ async close() {
280
+ this.socket.close();
281
+ }
282
+ }
283
+
284
+ class ApiClient {
285
+ token;
286
+ secret;
287
+ constructor(token, secret) {
288
+ this.token = token;
289
+ this.secret = secret;
290
+ }
291
+ /**
292
+ * Create a new session or load existing one with the given tag
293
+ */
294
+ async getOrCreateSession(tag) {
295
+ try {
296
+ const response = await axios.post(
297
+ `https://handy-api.korshakov.org/v1/sessions`,
298
+ { tag },
299
+ {
300
+ headers: {
301
+ "Authorization": `Bearer ${this.token}`,
302
+ "Content-Type": "application/json"
303
+ }
304
+ }
305
+ );
306
+ logger.info(`Session created/loaded: ${response.data.session.id} (tag: ${tag})`);
307
+ return response.data;
308
+ } catch (error) {
309
+ logger.error("Failed to get or create session:", error);
310
+ throw new Error(`Failed to get or create session: ${error instanceof Error ? error.message : "Unknown error"}`);
311
+ }
312
+ }
313
+ /**
314
+ * Start realtime session client
315
+ * @param id - Session ID
316
+ * @returns Session client
317
+ */
318
+ session(id) {
319
+ return new ApiSessionClient(this.token, this.secret, id);
320
+ }
321
+ }
322
+
323
+ function displayQRCode(url) {
324
+ try {
325
+ logger.info("=".repeat(50));
326
+ logger.info("\u{1F4F1} Scan this QR code with your mobile device:");
327
+ logger.info("=".repeat(50));
328
+ qrcode.generate(url, { small: true }, (qr) => {
329
+ for (let l of qr.split("\n")) {
330
+ logger.info(" " + l);
331
+ }
332
+ });
333
+ logger.info("=".repeat(50));
334
+ } catch (error) {
335
+ logger.error("Failed to generate QR code:", error);
336
+ logger.info(`\u{1F4CB} Use this URL to connect: ${url}`);
337
+ }
338
+ }
339
+
340
+ function claudePath() {
341
+ if (fs__namespace.existsSync(process.env.HOME + "/.claude/local/claude")) {
342
+ return process.env.HOME + "/.claude/local/claude";
343
+ } else {
344
+ return "claude";
345
+ }
346
+ }
347
+
348
+ async function* claude(options) {
349
+ const args = buildArgs(options);
350
+ const path = claudePath();
351
+ logger.info("Spawning Claude CLI with args:", args);
352
+ const process = node_child_process.spawn(path, args, {
353
+ cwd: options.workingDirectory,
354
+ stdio: ["pipe", "pipe", "pipe"],
355
+ shell: false
356
+ });
357
+ process.stdin?.end();
358
+ let outputBuffer = "";
359
+ let stderrBuffer = "";
360
+ let processExited = false;
361
+ const outputQueue = [];
362
+ let outputResolve = null;
363
+ process.stdout?.on("data", (data) => {
364
+ outputBuffer += data.toString();
365
+ const lines = outputBuffer.split("\n");
366
+ outputBuffer = lines.pop() || "";
367
+ for (const line of lines) {
368
+ if (line.trim()) {
369
+ try {
370
+ const json = JSON.parse(line);
371
+ outputQueue.push({ type: "json", data: json });
372
+ } catch {
373
+ outputQueue.push({ type: "text", data: line });
374
+ }
375
+ if (outputResolve) {
376
+ outputResolve();
377
+ outputResolve = null;
378
+ }
379
+ }
380
+ }
381
+ });
382
+ process.stderr?.on("data", (data) => {
383
+ stderrBuffer += data.toString();
384
+ const lines = stderrBuffer.split("\n");
385
+ stderrBuffer = lines.pop() || "";
386
+ for (const line of lines) {
387
+ if (line.trim()) {
388
+ outputQueue.push({ type: "error", error: line });
389
+ if (outputResolve) {
390
+ outputResolve();
391
+ outputResolve = null;
392
+ }
393
+ }
394
+ }
395
+ });
396
+ process.on("exit", (code, signal) => {
397
+ processExited = true;
398
+ outputQueue.push({ type: "exit", code, signal });
399
+ if (outputResolve) {
400
+ outputResolve();
401
+ outputResolve = null;
402
+ }
403
+ });
404
+ process.on("error", (error) => {
405
+ outputQueue.push({ type: "error", error: error.message });
406
+ processExited = true;
407
+ if (outputResolve) {
408
+ outputResolve();
409
+ outputResolve = null;
410
+ }
411
+ });
412
+ while (!processExited || outputQueue.length > 0) {
413
+ if (outputQueue.length === 0) {
414
+ await new Promise((resolve) => {
415
+ outputResolve = resolve;
416
+ if (outputQueue.length > 0 || processExited) {
417
+ resolve();
418
+ outputResolve = null;
419
+ }
420
+ });
421
+ }
422
+ while (outputQueue.length > 0) {
423
+ const output = outputQueue.shift();
424
+ yield output;
425
+ }
426
+ }
427
+ if (outputBuffer.trim()) {
428
+ try {
429
+ const json = JSON.parse(outputBuffer);
430
+ yield { type: "json", data: json };
431
+ } catch {
432
+ yield { type: "text", data: outputBuffer };
433
+ }
434
+ }
435
+ if (stderrBuffer.trim()) {
436
+ yield { type: "error", error: stderrBuffer };
437
+ }
438
+ }
439
+ function buildArgs(options) {
440
+ const args = [
441
+ "--print",
442
+ options.command,
443
+ "--output-format",
444
+ "stream-json",
445
+ "--verbose"
446
+ ];
447
+ if (options.model) {
448
+ args.push("--model", options.model);
449
+ }
450
+ if (options.permissionMode) {
451
+ const modeMap = {
452
+ "auto": "acceptEdits",
453
+ "default": "default",
454
+ "plan": "bypassPermissions"
455
+ };
456
+ args.push("--permission-mode", modeMap[options.permissionMode]);
457
+ }
458
+ if (options.skipPermissions) {
459
+ args.push("--dangerously-skip-permissions");
460
+ }
461
+ if (options.sessionId) {
462
+ args.push("--resume", options.sessionId);
463
+ }
464
+ return args;
465
+ }
466
+
467
+ function startClaudeLoop(opts, session) {
468
+ let exiting = false;
469
+ const messageQueue = [];
470
+ let messageResolve = null;
471
+ let promise = (async () => {
472
+ session.onUserMessage((message) => {
473
+ messageQueue.push(message);
474
+ if (messageResolve) {
475
+ messageResolve();
476
+ messageResolve = null;
477
+ }
478
+ });
479
+ while (!exiting) {
480
+ if (messageQueue.length > 0) {
481
+ const message = messageQueue.shift();
482
+ if (message) {
483
+ for await (const output of claude({
484
+ command: message.content.text,
485
+ workingDirectory: opts.path,
486
+ model: opts.model,
487
+ permissionMode: opts.permissionMode
488
+ })) {
489
+ if (output.type === "exit") {
490
+ if (output.code !== 0 || output.code === void 0) {
491
+ session.sendMessage({
492
+ content: {
493
+ type: "error",
494
+ error: output.error,
495
+ code: output.code
496
+ },
497
+ role: "assistant"
498
+ });
499
+ }
500
+ break;
501
+ }
502
+ if (output.type === "json") {
503
+ session.sendMessage({
504
+ data: output.data,
505
+ type: "output"
506
+ });
507
+ }
508
+ }
509
+ }
510
+ }
511
+ await new Promise((resolve) => {
512
+ messageResolve = resolve;
513
+ });
514
+ }
515
+ })();
516
+ return async () => {
517
+ exiting = true;
518
+ if (messageResolve) {
519
+ messageResolve();
520
+ }
521
+ await promise;
522
+ };
523
+ }
524
+
525
+ async function start(options = {}) {
526
+ const workingDirectory = process.cwd();
527
+ const projectName = node_path.basename(workingDirectory);
528
+ const sessionTag = node_crypto.randomUUID();
529
+ logger.info(`Starting happy session for project: ${projectName}`);
530
+ const secret = await getOrCreateSecretKey();
531
+ logger.info("Secret key loaded");
532
+ const token = await authGetToken(secret);
533
+ logger.info("Authenticated with handy server");
534
+ const api = new ApiClient(token, secret);
535
+ const response = await api.getOrCreateSession(sessionTag);
536
+ logger.info(`Session created: ${response.session.id}`);
537
+ const handyUrl = generateAppUrl(secret);
538
+ displayQRCode(handyUrl);
539
+ const session = api.session(response.session.id);
540
+ const loopDestroy = startClaudeLoop({ path: workingDirectory }, session);
541
+ const shutdown = async () => {
542
+ logger.info("Shutting down...");
543
+ await loopDestroy();
544
+ await session.close();
545
+ process.exit(0);
546
+ };
547
+ process.on("SIGINT", shutdown);
548
+ process.on("SIGTERM", shutdown);
549
+ logger.info("Happy CLI is running. Press Ctrl+C to stop.");
550
+ await new Promise(() => {
551
+ });
552
+ }
553
+
554
+ const args = process.argv.slice(2);
555
+ const options = {};
556
+ let showHelp = false;
557
+ let showVersion = false;
558
+ for (let i = 0; i < args.length; i++) {
559
+ const arg = args[i];
560
+ if (arg === "-h" || arg === "--help") {
561
+ showHelp = true;
562
+ } else if (arg === "-v" || arg === "--version") {
563
+ showVersion = true;
564
+ } else if (arg === "-m" || arg === "--model") {
565
+ options.model = args[++i];
566
+ } else if (arg === "-p" || arg === "--permission-mode") {
567
+ options.permissionMode = args[++i];
568
+ } else {
569
+ console.error(chalk.red(`Unknown argument: ${arg}`));
570
+ process.exit(1);
571
+ }
572
+ }
573
+ if (showHelp) {
574
+ console.log(`
575
+ ${chalk.bold("happy")} - Claude Code session sharing
576
+
577
+ ${chalk.bold("Usage:")}
578
+ happy [options]
579
+
580
+ ${chalk.bold("Options:")}
581
+ -h, --help Show this help message
582
+ -v, --version Show version
583
+ -m, --model <model> Claude model to use (default: sonnet)
584
+ -p, --permission-mode Permission mode: auto, default, or plan
585
+
586
+ ${chalk.bold("Examples:")}
587
+ happy Start a session with default settings
588
+ happy -m opus Use Claude Opus model
589
+ happy -p plan Use plan permission mode
590
+ `);
591
+ process.exit(0);
592
+ }
593
+ if (showVersion) {
594
+ console.log("0.1.0");
595
+ process.exit(0);
596
+ }
597
+ start(options).catch((error) => {
598
+ console.error(chalk.red("Error:"), error.message);
599
+ if (process.env.DEBUG) {
600
+ console.error(error);
601
+ }
602
+ process.exit(1);
603
+ });
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
@@ -0,0 +1 @@
1
+ export { run } from '@oclif/core';
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { run } from '@oclif/core';