happy-coder 0.5.0 → 0.6.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.
package/dist/index.cjs CHANGED
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  var chalk = require('chalk');
4
- var types$1 = require('./types-BBpJNhIN.cjs');
4
+ var types$1 = require('./types-iMUxaPkI.cjs');
5
5
  var node_crypto = require('node:crypto');
6
6
  var node_child_process = require('node:child_process');
7
7
  var node_path = require('node:path');
@@ -28,6 +28,7 @@ var child_process = require('child_process');
28
28
  var util = require('util');
29
29
  var crypto = require('crypto');
30
30
  var qrcode = require('qrcode-terminal');
31
+ var open = require('open');
31
32
  var fs = require('fs');
32
33
  var os$1 = require('os');
33
34
 
@@ -2261,7 +2262,7 @@ async function loop(opts) {
2261
2262
  }
2262
2263
 
2263
2264
  var name = "happy-coder";
2264
- var version = "0.5.0";
2265
+ var version = "0.6.1";
2265
2266
  var description = "Claude Code session sharing CLI";
2266
2267
  var author = "Kirill Dubovitskiy";
2267
2268
  var license = "MIT";
@@ -2329,7 +2330,7 @@ var dependencies = {
2329
2330
  "http-proxy": "^1.18.1",
2330
2331
  "http-proxy-middleware": "^3.0.5",
2331
2332
  ink: "^6.1.0",
2332
- "ink-box": "^2.0.0",
2333
+ open: "^10.2.0",
2333
2334
  "qrcode-terminal": "^0.12.0",
2334
2335
  react: "^19.1.1",
2335
2336
  "socket.io-client": "^4.8.1",
@@ -3086,6 +3087,10 @@ async function start(credentials, options = {}) {
3086
3087
  const logPath = await types$1.logger.logFilePathPromise;
3087
3088
  types$1.logger.infoDeveloper(`Session: ${response.id}`);
3088
3089
  types$1.logger.infoDeveloper(`Logs: ${logPath}`);
3090
+ session.updateAgentState((currentState) => ({
3091
+ ...currentState,
3092
+ controlledByUser: options.startingMode === "local"
3093
+ }));
3089
3094
  const caffeinateStarted = startCaffeinate();
3090
3095
  if (caffeinateStarted) {
3091
3096
  types$1.logger.infoDeveloper("Sleep prevention enabled (macOS)");
@@ -3184,7 +3189,7 @@ async function start(credentials, options = {}) {
3184
3189
  session.sendSessionEvent({ type: "switch", mode: newMode });
3185
3190
  session.updateAgentState((currentState) => ({
3186
3191
  ...currentState,
3187
- controlledByUser: false
3192
+ controlledByUser: newMode === "local"
3188
3193
  }));
3189
3194
  },
3190
3195
  mcpServers: {},
@@ -3214,8 +3219,69 @@ function displayQRCode(url) {
3214
3219
  console.log("=".repeat(80));
3215
3220
  }
3216
3221
 
3222
+ function generateWebAuthUrl(publicKey) {
3223
+ const publicKeyBase64 = types$1.encodeBase64(publicKey, "base64url");
3224
+ return `https://app.happy.engineering/terminal/connect#key=${publicKeyBase64}`;
3225
+ }
3226
+
3227
+ async function openBrowser(url) {
3228
+ try {
3229
+ if (!process.stdout.isTTY || process.env.CI || process.env.HEADLESS) {
3230
+ types$1.logger.debug("[browser] Headless environment detected, skipping browser open");
3231
+ return false;
3232
+ }
3233
+ types$1.logger.debug(`[browser] Attempting to open URL: ${url}`);
3234
+ await open(url);
3235
+ types$1.logger.debug("[browser] Browser opened successfully");
3236
+ return true;
3237
+ } catch (error) {
3238
+ types$1.logger.debug("[browser] Failed to open browser:", error);
3239
+ return false;
3240
+ }
3241
+ }
3242
+
3243
+ const AuthSelector = ({ onSelect, onCancel }) => {
3244
+ const [selectedIndex, setSelectedIndex] = React.useState(0);
3245
+ const options = [
3246
+ {
3247
+ method: "mobile",
3248
+ label: "Mobile App"
3249
+ },
3250
+ {
3251
+ method: "web",
3252
+ label: "Web Browser"
3253
+ }
3254
+ ];
3255
+ ink.useInput((input, key) => {
3256
+ if (key.upArrow) {
3257
+ setSelectedIndex((prev) => Math.max(0, prev - 1));
3258
+ } else if (key.downArrow) {
3259
+ setSelectedIndex((prev) => Math.min(options.length - 1, prev + 1));
3260
+ } else if (key.return) {
3261
+ onSelect(options[selectedIndex].method);
3262
+ } else if (key.escape || key.ctrl && input === "c") {
3263
+ onCancel();
3264
+ } else if (input === "1") {
3265
+ setSelectedIndex(0);
3266
+ onSelect("mobile");
3267
+ } else if (input === "2") {
3268
+ setSelectedIndex(1);
3269
+ onSelect("web");
3270
+ }
3271
+ });
3272
+ return /* @__PURE__ */ React.createElement(ink.Box, { flexDirection: "column", paddingY: 1 }, /* @__PURE__ */ React.createElement(ink.Box, { marginBottom: 1 }, /* @__PURE__ */ React.createElement(ink.Text, null, "How would you like to authenticate?")), /* @__PURE__ */ React.createElement(ink.Box, { flexDirection: "column" }, options.map((option, index) => {
3273
+ const isSelected = selectedIndex === index;
3274
+ return /* @__PURE__ */ React.createElement(ink.Box, { key: option.method, marginY: 0 }, /* @__PURE__ */ React.createElement(ink.Text, { color: isSelected ? "cyan" : "gray" }, isSelected ? "\u203A " : " ", index + 1, ". ", option.label));
3275
+ })), /* @__PURE__ */ React.createElement(ink.Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(ink.Text, { dimColor: true }, "Use arrows or 1-2 to select, Enter to confirm")));
3276
+ };
3277
+
3217
3278
  async function doAuth() {
3218
- console.log("Starting authentication...");
3279
+ console.clear();
3280
+ const authMethod = await selectAuthenticationMethod();
3281
+ if (!authMethod) {
3282
+ console.log("\nAuthentication cancelled.\n");
3283
+ return null;
3284
+ }
3219
3285
  const secret = new Uint8Array(node_crypto.randomBytes(32));
3220
3286
  const keypair = tweetnacl.box.keyPair.fromSecretKey(secret);
3221
3287
  try {
@@ -3226,38 +3292,106 @@ async function doAuth() {
3226
3292
  console.log("Failed to create authentication request, please try again later.");
3227
3293
  return null;
3228
3294
  }
3229
- console.log("Please, authenticate using mobile app");
3295
+ if (authMethod === "mobile") {
3296
+ return await doMobileAuth(keypair);
3297
+ } else {
3298
+ return await doWebAuth(keypair);
3299
+ }
3300
+ }
3301
+ function selectAuthenticationMethod() {
3302
+ return new Promise((resolve) => {
3303
+ let hasResolved = false;
3304
+ const onSelect = (method) => {
3305
+ if (!hasResolved) {
3306
+ hasResolved = true;
3307
+ app.unmount();
3308
+ resolve(method);
3309
+ }
3310
+ };
3311
+ const onCancel = () => {
3312
+ if (!hasResolved) {
3313
+ hasResolved = true;
3314
+ app.unmount();
3315
+ resolve(null);
3316
+ }
3317
+ };
3318
+ const app = ink.render(React.createElement(AuthSelector, { onSelect, onCancel }), {
3319
+ exitOnCtrlC: false,
3320
+ patchConsole: false
3321
+ });
3322
+ });
3323
+ }
3324
+ async function doMobileAuth(keypair) {
3325
+ console.clear();
3326
+ console.log("\nMobile Authentication\n");
3327
+ console.log("Scan this QR code with your Happy mobile app:\n");
3230
3328
  const authUrl = "happy://terminal?" + types$1.encodeBase64Url(keypair.publicKey);
3231
3329
  displayQRCode(authUrl);
3232
- console.log("\n\u{1F4CB} For manual entry, copy this URL:");
3330
+ console.log("\nOr manually enter this URL:");
3233
3331
  console.log(authUrl);
3234
- let credentials = null;
3235
- while (true) {
3236
- try {
3237
- const response = await axios.post(`${types$1.configuration.serverUrl}/v1/auth/request`, {
3238
- publicKey: types$1.encodeBase64(keypair.publicKey)
3239
- });
3240
- if (response.data.state === "authorized") {
3241
- let token = response.data.token;
3242
- let r = types$1.decodeBase64(response.data.response);
3243
- let decrypted = decryptWithEphemeralKey(r, keypair.secretKey);
3244
- if (decrypted) {
3245
- credentials = {
3246
- secret: decrypted,
3247
- token
3248
- };
3249
- await writeCredentials(credentials);
3250
- return credentials;
3251
- } else {
3252
- console.log("Failed to decrypt response, please try again later.");
3253
- return null;
3332
+ console.log("");
3333
+ return await waitForAuthentication(keypair);
3334
+ }
3335
+ async function doWebAuth(keypair) {
3336
+ console.clear();
3337
+ console.log("\nWeb Authentication\n");
3338
+ const webUrl = generateWebAuthUrl(keypair.publicKey);
3339
+ console.log("Opening your browser...");
3340
+ const browserOpened = await openBrowser(webUrl);
3341
+ if (browserOpened) {
3342
+ console.log("\u2713 Browser opened\n");
3343
+ console.log("Complete authentication in your browser window.");
3344
+ } else {
3345
+ console.log("Could not open browser automatically.\n");
3346
+ console.log("Please open this URL manually:");
3347
+ console.log(webUrl);
3348
+ }
3349
+ console.log("");
3350
+ return await waitForAuthentication(keypair);
3351
+ }
3352
+ async function waitForAuthentication(keypair) {
3353
+ process.stdout.write("Waiting for authentication");
3354
+ let dots = 0;
3355
+ let cancelled = false;
3356
+ const handleInterrupt = () => {
3357
+ cancelled = true;
3358
+ console.log("\n\nAuthentication cancelled.");
3359
+ process.exit(0);
3360
+ };
3361
+ process.on("SIGINT", handleInterrupt);
3362
+ try {
3363
+ while (!cancelled) {
3364
+ try {
3365
+ const response = await axios.post(`${types$1.configuration.serverUrl}/v1/auth/request`, {
3366
+ publicKey: types$1.encodeBase64(keypair.publicKey)
3367
+ });
3368
+ if (response.data.state === "authorized") {
3369
+ let token = response.data.token;
3370
+ let r = types$1.decodeBase64(response.data.response);
3371
+ let decrypted = decryptWithEphemeralKey(r, keypair.secretKey);
3372
+ if (decrypted) {
3373
+ const credentials = {
3374
+ secret: decrypted,
3375
+ token
3376
+ };
3377
+ await writeCredentials(credentials);
3378
+ console.log("\n\n\u2713 Authentication successful\n");
3379
+ return credentials;
3380
+ } else {
3381
+ console.log("\n\nFailed to decrypt response. Please try again.");
3382
+ return null;
3383
+ }
3254
3384
  }
3385
+ } catch (error) {
3386
+ console.log("\n\nFailed to check authentication status. Please try again.");
3387
+ return null;
3255
3388
  }
3256
- } catch (error) {
3257
- console.log("Failed to create authentication request, please try again later.");
3258
- return null;
3389
+ process.stdout.write("\rWaiting for authentication" + ".".repeat(dots % 3 + 1) + " ");
3390
+ dots++;
3391
+ await types$1.delay(1e3);
3259
3392
  }
3260
- await types$1.delay(1e3);
3393
+ } finally {
3394
+ process.off("SIGINT", handleInterrupt);
3261
3395
  }
3262
3396
  return null;
3263
3397
  }
@@ -3488,12 +3622,11 @@ class ApiDaemonSession extends node_events.EventEmitter {
3488
3622
  this.stopKeepAlive();
3489
3623
  this.keepAliveInterval = setInterval(() => {
3490
3624
  const payload = {
3491
- type: "machine-scoped",
3492
3625
  machineId: this.machineIdentity.machineId,
3493
3626
  time: Date.now()
3494
3627
  };
3495
- types$1.logger.debugLargeJson(`[DAEMON SESSION] Emitting session-alive`, payload);
3496
- this.socket.emit("session-alive", payload);
3628
+ types$1.logger.debugLargeJson(`[DAEMON SESSION] Emitting machine-alive`, payload);
3629
+ this.socket.emit("machine-alive", payload);
3497
3630
  }, 2e4);
3498
3631
  }
3499
3632
  stopKeepAlive() {
@@ -3527,7 +3660,7 @@ class ApiDaemonSession extends node_events.EventEmitter {
3527
3660
  };
3528
3661
  const encrypted = types$1.encrypt(JSON.stringify(metadata), this.secret);
3529
3662
  const encryptedMetadata = types$1.encodeBase64(encrypted);
3530
- this.socket.emit("update-machine-metadata", { metadata: encryptedMetadata });
3663
+ this.socket.emit("update-machine", { metadata: encryptedMetadata });
3531
3664
  }
3532
3665
  shutdown() {
3533
3666
  types$1.logger.debug(`[DAEMON SESSION] Shutting down daemon, killing ${this.spawnedProcesses.size} spawned processes`);
@@ -3961,6 +4094,17 @@ async function uninstall() {
3961
4094
  process.exit(1);
3962
4095
  }
3963
4096
  return;
4097
+ } else if (subcommand === "notify") {
4098
+ try {
4099
+ await handleNotifyCommand(args.slice(1));
4100
+ } catch (error) {
4101
+ console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
4102
+ if (process.env.DEBUG) {
4103
+ console.error(error);
4104
+ }
4105
+ process.exit(1);
4106
+ }
4107
+ return;
3964
4108
  } else if (subcommand === "daemon") {
3965
4109
  const daemonSubcommand = args[1];
3966
4110
  if (daemonSubcommand === "start") {
@@ -4041,6 +4185,7 @@ ${chalk.bold("happy")} - Claude Code On the Go
4041
4185
 
4042
4186
  ${chalk.bold("Usage:")}
4043
4187
  happy [options]
4188
+ happy notify Send notification
4044
4189
  happy logout Logs out of your account and removes data directory
4045
4190
  happy daemon Manage the background daemon (macOS only)
4046
4191
 
@@ -4073,6 +4218,7 @@ ${chalk.bold("Examples:")}
4073
4218
  happy -m opus Use Claude Opus model
4074
4219
  happy -p plan Use plan permission mode
4075
4220
  happy --auth Force re-authentication before starting session
4221
+ happy notify -p "Hello!" Send notification
4076
4222
  happy --claude-env KEY=VALUE
4077
4223
  Set environment variable for Claude Code
4078
4224
  happy --claude-arg --option
@@ -4187,3 +4333,71 @@ async function cleanKey() {
4187
4333
  console.log(chalk.blue("Operation cancelled"));
4188
4334
  }
4189
4335
  }
4336
+ async function handleNotifyCommand(args) {
4337
+ let message = "";
4338
+ let title = "";
4339
+ let showHelp = false;
4340
+ for (let i = 0; i < args.length; i++) {
4341
+ const arg = args[i];
4342
+ if (arg === "-p" && i + 1 < args.length) {
4343
+ message = args[++i];
4344
+ } else if (arg === "-t" && i + 1 < args.length) {
4345
+ title = args[++i];
4346
+ } else if (arg === "-h" || arg === "--help") {
4347
+ showHelp = true;
4348
+ } else {
4349
+ console.error(chalk.red(`Unknown argument for notify command: ${arg}`));
4350
+ process.exit(1);
4351
+ }
4352
+ }
4353
+ if (showHelp) {
4354
+ console.log(`
4355
+ ${chalk.bold("happy notify")} - Send notification
4356
+
4357
+ ${chalk.bold("Usage:")}
4358
+ happy notify -p <message> [-t <title>] Send notification with custom message and optional title
4359
+ happy notify -h, --help Show this help
4360
+
4361
+ ${chalk.bold("Options:")}
4362
+ -p <message> Notification message (required)
4363
+ -t <title> Notification title (optional, defaults to "Happy")
4364
+
4365
+ ${chalk.bold("Examples:")}
4366
+ happy notify -p "Deployment complete!"
4367
+ happy notify -p "System update complete" -t "Server Status"
4368
+ happy notify -t "Alert" -p "Database connection restored"
4369
+ `);
4370
+ return;
4371
+ }
4372
+ if (!message) {
4373
+ console.error(chalk.red('Error: Message is required. Use -p "your message" to specify the notification text.'));
4374
+ console.log(chalk.gray('Run "happy notify --help" for usage information.'));
4375
+ process.exit(1);
4376
+ }
4377
+ let credentials = await readCredentials();
4378
+ if (!credentials) {
4379
+ console.error(chalk.red('Error: Not authenticated. Please run "happy --auth" first.'));
4380
+ process.exit(1);
4381
+ }
4382
+ console.log(chalk.blue("\u{1F4F1} Sending push notification..."));
4383
+ try {
4384
+ const api = new types$1.ApiClient(credentials.token, credentials.secret);
4385
+ const notificationTitle = title || "Happy";
4386
+ api.push().sendToAllDevices(
4387
+ notificationTitle,
4388
+ message,
4389
+ {
4390
+ source: "cli",
4391
+ timestamp: Date.now()
4392
+ }
4393
+ );
4394
+ console.log(chalk.green("\u2713 Push notification sent successfully!"));
4395
+ console.log(chalk.gray(` Title: ${notificationTitle}`));
4396
+ console.log(chalk.gray(` Message: ${message}`));
4397
+ console.log(chalk.gray(" Check your mobile device for the notification."));
4398
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
4399
+ } catch (error) {
4400
+ console.error(chalk.red("\u2717 Failed to send push notification"));
4401
+ throw error;
4402
+ }
4403
+ }
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import chalk from 'chalk';
2
- import { l as logger, d as backoff, e as delay, R as RawJSONLinesSchema, c as configuration, f as encodeBase64, A as ApiClient, g as encodeBase64Url, h as decodeBase64, j as encrypt, b as initializeConfiguration, i as initLoggerWithGlobalConfiguration } from './types-DDjn6Ovv.mjs';
2
+ import { l as logger, d as backoff, e as delay, R as RawJSONLinesSchema, c as configuration, f as encodeBase64, A as ApiClient, g as encodeBase64Url, h as decodeBase64, j as encrypt, b as initializeConfiguration, i as initLoggerWithGlobalConfiguration } from './types-DKVMGtcN.mjs';
3
3
  import { randomUUID, randomBytes } from 'node:crypto';
4
4
  import { spawn, execSync } from 'node:child_process';
5
5
  import { resolve, join, dirname as dirname$1 } from 'node:path';
@@ -27,6 +27,7 @@ import { spawn as spawn$1, exec, execSync as execSync$1 } from 'child_process';
27
27
  import { promisify } from 'util';
28
28
  import crypto, { createHash } from 'crypto';
29
29
  import qrcode from 'qrcode-terminal';
30
+ import open from 'open';
30
31
  import { existsSync as existsSync$1, readFileSync as readFileSync$1, writeFileSync, unlinkSync, mkdirSync as mkdirSync$1, chmodSync } from 'fs';
31
32
  import { hostname, homedir as homedir$1 } from 'os';
32
33
 
@@ -2240,7 +2241,7 @@ async function loop(opts) {
2240
2241
  }
2241
2242
 
2242
2243
  var name = "happy-coder";
2243
- var version = "0.5.0";
2244
+ var version = "0.6.1";
2244
2245
  var description = "Claude Code session sharing CLI";
2245
2246
  var author = "Kirill Dubovitskiy";
2246
2247
  var license = "MIT";
@@ -2308,7 +2309,7 @@ var dependencies = {
2308
2309
  "http-proxy": "^1.18.1",
2309
2310
  "http-proxy-middleware": "^3.0.5",
2310
2311
  ink: "^6.1.0",
2311
- "ink-box": "^2.0.0",
2312
+ open: "^10.2.0",
2312
2313
  "qrcode-terminal": "^0.12.0",
2313
2314
  react: "^19.1.1",
2314
2315
  "socket.io-client": "^4.8.1",
@@ -3065,6 +3066,10 @@ async function start(credentials, options = {}) {
3065
3066
  const logPath = await logger.logFilePathPromise;
3066
3067
  logger.infoDeveloper(`Session: ${response.id}`);
3067
3068
  logger.infoDeveloper(`Logs: ${logPath}`);
3069
+ session.updateAgentState((currentState) => ({
3070
+ ...currentState,
3071
+ controlledByUser: options.startingMode === "local"
3072
+ }));
3068
3073
  const caffeinateStarted = startCaffeinate();
3069
3074
  if (caffeinateStarted) {
3070
3075
  logger.infoDeveloper("Sleep prevention enabled (macOS)");
@@ -3163,7 +3168,7 @@ async function start(credentials, options = {}) {
3163
3168
  session.sendSessionEvent({ type: "switch", mode: newMode });
3164
3169
  session.updateAgentState((currentState) => ({
3165
3170
  ...currentState,
3166
- controlledByUser: false
3171
+ controlledByUser: newMode === "local"
3167
3172
  }));
3168
3173
  },
3169
3174
  mcpServers: {},
@@ -3193,8 +3198,69 @@ function displayQRCode(url) {
3193
3198
  console.log("=".repeat(80));
3194
3199
  }
3195
3200
 
3201
+ function generateWebAuthUrl(publicKey) {
3202
+ const publicKeyBase64 = encodeBase64(publicKey, "base64url");
3203
+ return `https://app.happy.engineering/terminal/connect#key=${publicKeyBase64}`;
3204
+ }
3205
+
3206
+ async function openBrowser(url) {
3207
+ try {
3208
+ if (!process.stdout.isTTY || process.env.CI || process.env.HEADLESS) {
3209
+ logger.debug("[browser] Headless environment detected, skipping browser open");
3210
+ return false;
3211
+ }
3212
+ logger.debug(`[browser] Attempting to open URL: ${url}`);
3213
+ await open(url);
3214
+ logger.debug("[browser] Browser opened successfully");
3215
+ return true;
3216
+ } catch (error) {
3217
+ logger.debug("[browser] Failed to open browser:", error);
3218
+ return false;
3219
+ }
3220
+ }
3221
+
3222
+ const AuthSelector = ({ onSelect, onCancel }) => {
3223
+ const [selectedIndex, setSelectedIndex] = useState(0);
3224
+ const options = [
3225
+ {
3226
+ method: "mobile",
3227
+ label: "Mobile App"
3228
+ },
3229
+ {
3230
+ method: "web",
3231
+ label: "Web Browser"
3232
+ }
3233
+ ];
3234
+ useInput((input, key) => {
3235
+ if (key.upArrow) {
3236
+ setSelectedIndex((prev) => Math.max(0, prev - 1));
3237
+ } else if (key.downArrow) {
3238
+ setSelectedIndex((prev) => Math.min(options.length - 1, prev + 1));
3239
+ } else if (key.return) {
3240
+ onSelect(options[selectedIndex].method);
3241
+ } else if (key.escape || key.ctrl && input === "c") {
3242
+ onCancel();
3243
+ } else if (input === "1") {
3244
+ setSelectedIndex(0);
3245
+ onSelect("mobile");
3246
+ } else if (input === "2") {
3247
+ setSelectedIndex(1);
3248
+ onSelect("web");
3249
+ }
3250
+ });
3251
+ return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", paddingY: 1 }, /* @__PURE__ */ React.createElement(Box, { marginBottom: 1 }, /* @__PURE__ */ React.createElement(Text, null, "How would you like to authenticate?")), /* @__PURE__ */ React.createElement(Box, { flexDirection: "column" }, options.map((option, index) => {
3252
+ const isSelected = selectedIndex === index;
3253
+ return /* @__PURE__ */ React.createElement(Box, { key: option.method, marginY: 0 }, /* @__PURE__ */ React.createElement(Text, { color: isSelected ? "cyan" : "gray" }, isSelected ? "\u203A " : " ", index + 1, ". ", option.label));
3254
+ })), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "Use arrows or 1-2 to select, Enter to confirm")));
3255
+ };
3256
+
3196
3257
  async function doAuth() {
3197
- console.log("Starting authentication...");
3258
+ console.clear();
3259
+ const authMethod = await selectAuthenticationMethod();
3260
+ if (!authMethod) {
3261
+ console.log("\nAuthentication cancelled.\n");
3262
+ return null;
3263
+ }
3198
3264
  const secret = new Uint8Array(randomBytes(32));
3199
3265
  const keypair = tweetnacl.box.keyPair.fromSecretKey(secret);
3200
3266
  try {
@@ -3205,38 +3271,106 @@ async function doAuth() {
3205
3271
  console.log("Failed to create authentication request, please try again later.");
3206
3272
  return null;
3207
3273
  }
3208
- console.log("Please, authenticate using mobile app");
3274
+ if (authMethod === "mobile") {
3275
+ return await doMobileAuth(keypair);
3276
+ } else {
3277
+ return await doWebAuth(keypair);
3278
+ }
3279
+ }
3280
+ function selectAuthenticationMethod() {
3281
+ return new Promise((resolve) => {
3282
+ let hasResolved = false;
3283
+ const onSelect = (method) => {
3284
+ if (!hasResolved) {
3285
+ hasResolved = true;
3286
+ app.unmount();
3287
+ resolve(method);
3288
+ }
3289
+ };
3290
+ const onCancel = () => {
3291
+ if (!hasResolved) {
3292
+ hasResolved = true;
3293
+ app.unmount();
3294
+ resolve(null);
3295
+ }
3296
+ };
3297
+ const app = render(React.createElement(AuthSelector, { onSelect, onCancel }), {
3298
+ exitOnCtrlC: false,
3299
+ patchConsole: false
3300
+ });
3301
+ });
3302
+ }
3303
+ async function doMobileAuth(keypair) {
3304
+ console.clear();
3305
+ console.log("\nMobile Authentication\n");
3306
+ console.log("Scan this QR code with your Happy mobile app:\n");
3209
3307
  const authUrl = "happy://terminal?" + encodeBase64Url(keypair.publicKey);
3210
3308
  displayQRCode(authUrl);
3211
- console.log("\n\u{1F4CB} For manual entry, copy this URL:");
3309
+ console.log("\nOr manually enter this URL:");
3212
3310
  console.log(authUrl);
3213
- let credentials = null;
3214
- while (true) {
3215
- try {
3216
- const response = await axios.post(`${configuration.serverUrl}/v1/auth/request`, {
3217
- publicKey: encodeBase64(keypair.publicKey)
3218
- });
3219
- if (response.data.state === "authorized") {
3220
- let token = response.data.token;
3221
- let r = decodeBase64(response.data.response);
3222
- let decrypted = decryptWithEphemeralKey(r, keypair.secretKey);
3223
- if (decrypted) {
3224
- credentials = {
3225
- secret: decrypted,
3226
- token
3227
- };
3228
- await writeCredentials(credentials);
3229
- return credentials;
3230
- } else {
3231
- console.log("Failed to decrypt response, please try again later.");
3232
- return null;
3311
+ console.log("");
3312
+ return await waitForAuthentication(keypair);
3313
+ }
3314
+ async function doWebAuth(keypair) {
3315
+ console.clear();
3316
+ console.log("\nWeb Authentication\n");
3317
+ const webUrl = generateWebAuthUrl(keypair.publicKey);
3318
+ console.log("Opening your browser...");
3319
+ const browserOpened = await openBrowser(webUrl);
3320
+ if (browserOpened) {
3321
+ console.log("\u2713 Browser opened\n");
3322
+ console.log("Complete authentication in your browser window.");
3323
+ } else {
3324
+ console.log("Could not open browser automatically.\n");
3325
+ console.log("Please open this URL manually:");
3326
+ console.log(webUrl);
3327
+ }
3328
+ console.log("");
3329
+ return await waitForAuthentication(keypair);
3330
+ }
3331
+ async function waitForAuthentication(keypair) {
3332
+ process.stdout.write("Waiting for authentication");
3333
+ let dots = 0;
3334
+ let cancelled = false;
3335
+ const handleInterrupt = () => {
3336
+ cancelled = true;
3337
+ console.log("\n\nAuthentication cancelled.");
3338
+ process.exit(0);
3339
+ };
3340
+ process.on("SIGINT", handleInterrupt);
3341
+ try {
3342
+ while (!cancelled) {
3343
+ try {
3344
+ const response = await axios.post(`${configuration.serverUrl}/v1/auth/request`, {
3345
+ publicKey: encodeBase64(keypair.publicKey)
3346
+ });
3347
+ if (response.data.state === "authorized") {
3348
+ let token = response.data.token;
3349
+ let r = decodeBase64(response.data.response);
3350
+ let decrypted = decryptWithEphemeralKey(r, keypair.secretKey);
3351
+ if (decrypted) {
3352
+ const credentials = {
3353
+ secret: decrypted,
3354
+ token
3355
+ };
3356
+ await writeCredentials(credentials);
3357
+ console.log("\n\n\u2713 Authentication successful\n");
3358
+ return credentials;
3359
+ } else {
3360
+ console.log("\n\nFailed to decrypt response. Please try again.");
3361
+ return null;
3362
+ }
3233
3363
  }
3364
+ } catch (error) {
3365
+ console.log("\n\nFailed to check authentication status. Please try again.");
3366
+ return null;
3234
3367
  }
3235
- } catch (error) {
3236
- console.log("Failed to create authentication request, please try again later.");
3237
- return null;
3368
+ process.stdout.write("\rWaiting for authentication" + ".".repeat(dots % 3 + 1) + " ");
3369
+ dots++;
3370
+ await delay(1e3);
3238
3371
  }
3239
- await delay(1e3);
3372
+ } finally {
3373
+ process.off("SIGINT", handleInterrupt);
3240
3374
  }
3241
3375
  return null;
3242
3376
  }
@@ -3467,12 +3601,11 @@ class ApiDaemonSession extends EventEmitter {
3467
3601
  this.stopKeepAlive();
3468
3602
  this.keepAliveInterval = setInterval(() => {
3469
3603
  const payload = {
3470
- type: "machine-scoped",
3471
3604
  machineId: this.machineIdentity.machineId,
3472
3605
  time: Date.now()
3473
3606
  };
3474
- logger.debugLargeJson(`[DAEMON SESSION] Emitting session-alive`, payload);
3475
- this.socket.emit("session-alive", payload);
3607
+ logger.debugLargeJson(`[DAEMON SESSION] Emitting machine-alive`, payload);
3608
+ this.socket.emit("machine-alive", payload);
3476
3609
  }, 2e4);
3477
3610
  }
3478
3611
  stopKeepAlive() {
@@ -3506,7 +3639,7 @@ class ApiDaemonSession extends EventEmitter {
3506
3639
  };
3507
3640
  const encrypted = encrypt(JSON.stringify(metadata), this.secret);
3508
3641
  const encryptedMetadata = encodeBase64(encrypted);
3509
- this.socket.emit("update-machine-metadata", { metadata: encryptedMetadata });
3642
+ this.socket.emit("update-machine", { metadata: encryptedMetadata });
3510
3643
  }
3511
3644
  shutdown() {
3512
3645
  logger.debug(`[DAEMON SESSION] Shutting down daemon, killing ${this.spawnedProcesses.size} spawned processes`);
@@ -3940,6 +4073,17 @@ async function uninstall() {
3940
4073
  process.exit(1);
3941
4074
  }
3942
4075
  return;
4076
+ } else if (subcommand === "notify") {
4077
+ try {
4078
+ await handleNotifyCommand(args.slice(1));
4079
+ } catch (error) {
4080
+ console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
4081
+ if (process.env.DEBUG) {
4082
+ console.error(error);
4083
+ }
4084
+ process.exit(1);
4085
+ }
4086
+ return;
3943
4087
  } else if (subcommand === "daemon") {
3944
4088
  const daemonSubcommand = args[1];
3945
4089
  if (daemonSubcommand === "start") {
@@ -4020,6 +4164,7 @@ ${chalk.bold("happy")} - Claude Code On the Go
4020
4164
 
4021
4165
  ${chalk.bold("Usage:")}
4022
4166
  happy [options]
4167
+ happy notify Send notification
4023
4168
  happy logout Logs out of your account and removes data directory
4024
4169
  happy daemon Manage the background daemon (macOS only)
4025
4170
 
@@ -4052,6 +4197,7 @@ ${chalk.bold("Examples:")}
4052
4197
  happy -m opus Use Claude Opus model
4053
4198
  happy -p plan Use plan permission mode
4054
4199
  happy --auth Force re-authentication before starting session
4200
+ happy notify -p "Hello!" Send notification
4055
4201
  happy --claude-env KEY=VALUE
4056
4202
  Set environment variable for Claude Code
4057
4203
  happy --claude-arg --option
@@ -4166,3 +4312,71 @@ async function cleanKey() {
4166
4312
  console.log(chalk.blue("Operation cancelled"));
4167
4313
  }
4168
4314
  }
4315
+ async function handleNotifyCommand(args) {
4316
+ let message = "";
4317
+ let title = "";
4318
+ let showHelp = false;
4319
+ for (let i = 0; i < args.length; i++) {
4320
+ const arg = args[i];
4321
+ if (arg === "-p" && i + 1 < args.length) {
4322
+ message = args[++i];
4323
+ } else if (arg === "-t" && i + 1 < args.length) {
4324
+ title = args[++i];
4325
+ } else if (arg === "-h" || arg === "--help") {
4326
+ showHelp = true;
4327
+ } else {
4328
+ console.error(chalk.red(`Unknown argument for notify command: ${arg}`));
4329
+ process.exit(1);
4330
+ }
4331
+ }
4332
+ if (showHelp) {
4333
+ console.log(`
4334
+ ${chalk.bold("happy notify")} - Send notification
4335
+
4336
+ ${chalk.bold("Usage:")}
4337
+ happy notify -p <message> [-t <title>] Send notification with custom message and optional title
4338
+ happy notify -h, --help Show this help
4339
+
4340
+ ${chalk.bold("Options:")}
4341
+ -p <message> Notification message (required)
4342
+ -t <title> Notification title (optional, defaults to "Happy")
4343
+
4344
+ ${chalk.bold("Examples:")}
4345
+ happy notify -p "Deployment complete!"
4346
+ happy notify -p "System update complete" -t "Server Status"
4347
+ happy notify -t "Alert" -p "Database connection restored"
4348
+ `);
4349
+ return;
4350
+ }
4351
+ if (!message) {
4352
+ console.error(chalk.red('Error: Message is required. Use -p "your message" to specify the notification text.'));
4353
+ console.log(chalk.gray('Run "happy notify --help" for usage information.'));
4354
+ process.exit(1);
4355
+ }
4356
+ let credentials = await readCredentials();
4357
+ if (!credentials) {
4358
+ console.error(chalk.red('Error: Not authenticated. Please run "happy --auth" first.'));
4359
+ process.exit(1);
4360
+ }
4361
+ console.log(chalk.blue("\u{1F4F1} Sending push notification..."));
4362
+ try {
4363
+ const api = new ApiClient(credentials.token, credentials.secret);
4364
+ const notificationTitle = title || "Happy";
4365
+ api.push().sendToAllDevices(
4366
+ notificationTitle,
4367
+ message,
4368
+ {
4369
+ source: "cli",
4370
+ timestamp: Date.now()
4371
+ }
4372
+ );
4373
+ console.log(chalk.green("\u2713 Push notification sent successfully!"));
4374
+ console.log(chalk.gray(` Title: ${notificationTitle}`));
4375
+ console.log(chalk.gray(` Message: ${message}`));
4376
+ console.log(chalk.gray(" Check your mobile device for the notification."));
4377
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
4378
+ } catch (error) {
4379
+ console.error(chalk.red("\u2717 Failed to send push notification"));
4380
+ throw error;
4381
+ }
4382
+ }
package/dist/lib.cjs CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var types = require('./types-BBpJNhIN.cjs');
3
+ var types = require('./types-iMUxaPkI.cjs');
4
4
  require('axios');
5
5
  require('chalk');
6
6
  require('fs');
package/dist/lib.mjs CHANGED
@@ -1,4 +1,4 @@
1
- export { A as ApiClient, a as ApiSessionClient, R as RawJSONLinesSchema, c as configuration, i as initLoggerWithGlobalConfiguration, b as initializeConfiguration, l as logger } from './types-DDjn6Ovv.mjs';
1
+ export { A as ApiClient, a as ApiSessionClient, R as RawJSONLinesSchema, c as configuration, i as initLoggerWithGlobalConfiguration, b as initializeConfiguration, l as logger } from './types-DKVMGtcN.mjs';
2
2
  import 'axios';
3
3
  import 'chalk';
4
4
  import 'fs';
@@ -544,7 +544,6 @@ class ApiSessionClient extends EventEmitter {
544
544
  */
545
545
  keepAlive(thinking, mode) {
546
546
  this.socket.volatile.emit("session-alive", {
547
- type: "session-scoped",
548
547
  sid: this.sessionId,
549
548
  time: Date.now(),
550
549
  thinking,
@@ -693,6 +692,9 @@ class PushNotificationClient {
693
692
  }
694
693
  );
695
694
  logger.debug(`Fetched ${response.data.tokens.length} push tokens`);
695
+ response.data.tokens.forEach((token, index) => {
696
+ logger.debug(`[PUSH] Token ${index + 1}: id=${token.id}, token=${token.token}, created=${new Date(token.createdAt).toISOString()}, updated=${new Date(token.updatedAt).toISOString()}`);
697
+ });
696
698
  return response.data.tokens;
697
699
  } catch (error) {
698
700
  logger.debug("[PUSH] [ERROR] Failed to fetch push tokens:", error);
@@ -725,7 +727,8 @@ class PushNotificationClient {
725
727
  const ticketChunk = await this.expo.sendPushNotificationsAsync(chunk);
726
728
  const errors = ticketChunk.filter((ticket) => ticket.status === "error");
727
729
  if (errors.length > 0) {
728
- logger.debug("[PUSH] Some notifications failed:", errors);
730
+ const errorDetails = errors.map((e) => ({ message: e.message, details: e.details }));
731
+ logger.debug("[PUSH] Some notifications failed:", errorDetails);
729
732
  }
730
733
  if (errors.length === ticketChunk.length) {
731
734
  throw new Error("All push notifications in chunk failed");
@@ -757,22 +760,33 @@ class PushNotificationClient {
757
760
  * @param data - Additional data to send with the notification
758
761
  */
759
762
  sendToAllDevices(title, body, data) {
763
+ logger.debug(`[PUSH] sendToAllDevices called with title: "${title}", body: "${body}"`);
760
764
  (async () => {
761
765
  try {
766
+ logger.debug("[PUSH] Fetching push tokens...");
762
767
  const tokens = await this.fetchPushTokens();
768
+ logger.debug(`[PUSH] Fetched ${tokens.length} push tokens`);
769
+ tokens.forEach((token, index) => {
770
+ logger.debug(`[PUSH] Using token ${index + 1}: id=${token.id}, token=${token.token}`);
771
+ });
763
772
  if (tokens.length === 0) {
764
773
  logger.debug("No push tokens found for user");
765
774
  return;
766
775
  }
767
- const messages = tokens.map((token) => ({
768
- to: token.token,
769
- title,
770
- body,
771
- data,
772
- sound: "default",
773
- priority: "high"
774
- }));
776
+ const messages = tokens.map((token, index) => {
777
+ logger.debug(`[PUSH] Creating message ${index + 1} for token: ${token.token}`);
778
+ return {
779
+ to: token.token,
780
+ title,
781
+ body,
782
+ data,
783
+ sound: "default",
784
+ priority: "high"
785
+ };
786
+ });
787
+ logger.debug(`[PUSH] Sending ${messages.length} push notifications...`);
775
788
  await this.sendPushNotifications(messages);
789
+ logger.debug("[PUSH] Push notifications sent successfully");
776
790
  } catch (error) {
777
791
  logger.debug("[PUSH] Error sending to all devices:", error);
778
792
  }
@@ -546,7 +546,6 @@ class ApiSessionClient extends node_events.EventEmitter {
546
546
  */
547
547
  keepAlive(thinking, mode) {
548
548
  this.socket.volatile.emit("session-alive", {
549
- type: "session-scoped",
550
549
  sid: this.sessionId,
551
550
  time: Date.now(),
552
551
  thinking,
@@ -695,6 +694,9 @@ class PushNotificationClient {
695
694
  }
696
695
  );
697
696
  exports.logger.debug(`Fetched ${response.data.tokens.length} push tokens`);
697
+ response.data.tokens.forEach((token, index) => {
698
+ exports.logger.debug(`[PUSH] Token ${index + 1}: id=${token.id}, token=${token.token}, created=${new Date(token.createdAt).toISOString()}, updated=${new Date(token.updatedAt).toISOString()}`);
699
+ });
698
700
  return response.data.tokens;
699
701
  } catch (error) {
700
702
  exports.logger.debug("[PUSH] [ERROR] Failed to fetch push tokens:", error);
@@ -727,7 +729,8 @@ class PushNotificationClient {
727
729
  const ticketChunk = await this.expo.sendPushNotificationsAsync(chunk);
728
730
  const errors = ticketChunk.filter((ticket) => ticket.status === "error");
729
731
  if (errors.length > 0) {
730
- exports.logger.debug("[PUSH] Some notifications failed:", errors);
732
+ const errorDetails = errors.map((e) => ({ message: e.message, details: e.details }));
733
+ exports.logger.debug("[PUSH] Some notifications failed:", errorDetails);
731
734
  }
732
735
  if (errors.length === ticketChunk.length) {
733
736
  throw new Error("All push notifications in chunk failed");
@@ -759,22 +762,33 @@ class PushNotificationClient {
759
762
  * @param data - Additional data to send with the notification
760
763
  */
761
764
  sendToAllDevices(title, body, data) {
765
+ exports.logger.debug(`[PUSH] sendToAllDevices called with title: "${title}", body: "${body}"`);
762
766
  (async () => {
763
767
  try {
768
+ exports.logger.debug("[PUSH] Fetching push tokens...");
764
769
  const tokens = await this.fetchPushTokens();
770
+ exports.logger.debug(`[PUSH] Fetched ${tokens.length} push tokens`);
771
+ tokens.forEach((token, index) => {
772
+ exports.logger.debug(`[PUSH] Using token ${index + 1}: id=${token.id}, token=${token.token}`);
773
+ });
765
774
  if (tokens.length === 0) {
766
775
  exports.logger.debug("No push tokens found for user");
767
776
  return;
768
777
  }
769
- const messages = tokens.map((token) => ({
770
- to: token.token,
771
- title,
772
- body,
773
- data,
774
- sound: "default",
775
- priority: "high"
776
- }));
778
+ const messages = tokens.map((token, index) => {
779
+ exports.logger.debug(`[PUSH] Creating message ${index + 1} for token: ${token.token}`);
780
+ return {
781
+ to: token.token,
782
+ title,
783
+ body,
784
+ data,
785
+ sound: "default",
786
+ priority: "high"
787
+ };
788
+ });
789
+ exports.logger.debug(`[PUSH] Sending ${messages.length} push notifications...`);
777
790
  await this.sendPushNotifications(messages);
791
+ exports.logger.debug("[PUSH] Push notifications sent successfully");
778
792
  } catch (error) {
779
793
  exports.logger.debug("[PUSH] Error sending to all devices:", error);
780
794
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "happy-coder",
3
- "version": "0.5.0",
3
+ "version": "0.6.1",
4
4
  "description": "Claude Code session sharing CLI",
5
5
  "author": "Kirill Dubovitskiy",
6
6
  "license": "MIT",
@@ -68,7 +68,7 @@
68
68
  "http-proxy": "^1.18.1",
69
69
  "http-proxy-middleware": "^3.0.5",
70
70
  "ink": "^6.1.0",
71
- "ink-box": "^2.0.0",
71
+ "open": "^10.2.0",
72
72
  "qrcode-terminal": "^0.12.0",
73
73
  "react": "^19.1.1",
74
74
  "socket.io-client": "^4.8.1",