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 +249 -35
- package/dist/index.mjs +249 -35
- package/dist/lib.cjs +1 -1
- package/dist/lib.mjs +1 -1
- package/dist/{types-DDjn6Ovv.mjs → types-DKVMGtcN.mjs} +24 -10
- package/dist/{types-BBpJNhIN.cjs → types-iMUxaPkI.cjs} +24 -10
- package/package.json +2 -2
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-
|
|
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.
|
|
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
|
-
|
|
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:
|
|
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.
|
|
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
|
-
|
|
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("\
|
|
3330
|
+
console.log("\nOr manually enter this URL:");
|
|
3233
3331
|
console.log(authUrl);
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
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
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3389
|
+
process.stdout.write("\rWaiting for authentication" + ".".repeat(dots % 3 + 1) + " ");
|
|
3390
|
+
dots++;
|
|
3391
|
+
await types$1.delay(1e3);
|
|
3259
3392
|
}
|
|
3260
|
-
|
|
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
|
|
3496
|
-
this.socket.emit("
|
|
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
|
|
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-
|
|
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.
|
|
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
|
-
|
|
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:
|
|
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.
|
|
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
|
-
|
|
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("\
|
|
3309
|
+
console.log("\nOr manually enter this URL:");
|
|
3212
3310
|
console.log(authUrl);
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
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
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3368
|
+
process.stdout.write("\rWaiting for authentication" + ".".repeat(dots % 3 + 1) + " ");
|
|
3369
|
+
dots++;
|
|
3370
|
+
await delay(1e3);
|
|
3238
3371
|
}
|
|
3239
|
-
|
|
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
|
|
3475
|
-
this.socket.emit("
|
|
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
|
|
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
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-
|
|
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
|
-
|
|
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
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
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
|
-
|
|
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
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
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.
|
|
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
|
-
"
|
|
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",
|