happy-coder 0.7.1-beta.3 → 0.7.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/README.md +4 -1
- package/bin/happy.mjs +35 -2
- package/dist/index.cjs +502 -434
- package/dist/index.mjs +511 -443
- package/dist/lib.cjs +1 -1
- package/dist/lib.d.cts +2 -2
- package/dist/lib.d.mts +2 -2
- package/dist/lib.mjs +1 -1
- package/dist/{types-CzvFvJwf.cjs → types-B4GgojGc.cjs} +1 -1
- package/dist/{types-BZC9-exR.mjs → types-CnqIfv9n.mjs} +1 -1
- package/package.json +5 -4
- package/bin/happy.cmd +0 -3
package/dist/index.mjs
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import { l as logger, b as backoff, d as delay, R as RawJSONLinesSchema, c as configuration, e as encodeBase64,
|
|
2
|
+
import { l as logger, b as backoff, d as delay, R as RawJSONLinesSchema, c as configuration, e as encodeBase64, f as encodeBase64Url, g as decodeBase64, A as ApiClient } from './types-CnqIfv9n.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';
|
|
6
6
|
import { createInterface } from 'node:readline';
|
|
7
7
|
import { fileURLToPath as fileURLToPath$1 } from 'node:url';
|
|
8
8
|
import { existsSync, readFileSync, mkdirSync, watch, constants, readdirSync, statSync, rmSync } from 'node:fs';
|
|
9
|
-
import os, { homedir } from 'node:os';
|
|
9
|
+
import os$1, { homedir } from 'node:os';
|
|
10
10
|
import { dirname, resolve as resolve$1, join as join$1 } from 'path';
|
|
11
11
|
import { fileURLToPath } from 'url';
|
|
12
12
|
import { readFile, unlink, mkdir, writeFile as writeFile$1, open, stat as stat$1, rename } from 'node:fs/promises';
|
|
@@ -26,11 +26,11 @@ import { z as z$1 } from 'zod';
|
|
|
26
26
|
import { spawn as spawn$1, exec, execSync as execSync$1 } from 'child_process';
|
|
27
27
|
import { promisify } from 'util';
|
|
28
28
|
import { createHash } from 'crypto';
|
|
29
|
-
import qrcode from 'qrcode-terminal';
|
|
30
|
-
import open$1 from 'open';
|
|
31
29
|
import fastify from 'fastify';
|
|
32
30
|
import { validatorCompiler, serializerCompiler } from 'fastify-type-provider-zod';
|
|
33
|
-
import os
|
|
31
|
+
import os from 'os';
|
|
32
|
+
import qrcode from 'qrcode-terminal';
|
|
33
|
+
import open$1 from 'open';
|
|
34
34
|
import { existsSync as existsSync$1, writeFileSync, chmodSync, unlinkSync } from 'fs';
|
|
35
35
|
|
|
36
36
|
class Session {
|
|
@@ -2343,7 +2343,7 @@ async function loop(opts) {
|
|
|
2343
2343
|
}
|
|
2344
2344
|
|
|
2345
2345
|
var name = "happy-coder";
|
|
2346
|
-
var version = "0.7.1
|
|
2346
|
+
var version = "0.7.1";
|
|
2347
2347
|
var description = "Claude Code session sharing CLI";
|
|
2348
2348
|
var author = "Kirill Dubovitskiy";
|
|
2349
2349
|
var license = "MIT";
|
|
@@ -2428,7 +2428,7 @@ var dependencies = {
|
|
|
2428
2428
|
};
|
|
2429
2429
|
var devDependencies = {
|
|
2430
2430
|
"@eslint/compat": "^1",
|
|
2431
|
-
"@types/node": ">=
|
|
2431
|
+
"@types/node": ">=20",
|
|
2432
2432
|
"cross-env": "^10.0.0",
|
|
2433
2433
|
eslint: "^9",
|
|
2434
2434
|
"eslint-config-prettier": "^10",
|
|
@@ -2439,9 +2439,10 @@ var devDependencies = {
|
|
|
2439
2439
|
typescript: "^5",
|
|
2440
2440
|
vitest: "^3.2.4"
|
|
2441
2441
|
};
|
|
2442
|
-
var
|
|
2442
|
+
var resolutions = {
|
|
2443
2443
|
"whatwg-url": "14.2.0"
|
|
2444
2444
|
};
|
|
2445
|
+
var packageManager = "yarn@1.22.22";
|
|
2445
2446
|
var packageJson = {
|
|
2446
2447
|
name: name,
|
|
2447
2448
|
version: version,
|
|
@@ -2461,7 +2462,8 @@ var packageJson = {
|
|
|
2461
2462
|
scripts: scripts,
|
|
2462
2463
|
dependencies: dependencies,
|
|
2463
2464
|
devDependencies: devDependencies,
|
|
2464
|
-
|
|
2465
|
+
resolutions: resolutions,
|
|
2466
|
+
packageManager: packageManager
|
|
2465
2467
|
};
|
|
2466
2468
|
|
|
2467
2469
|
function run(args, options) {
|
|
@@ -3525,6 +3527,16 @@ async function runDoctorCommand() {
|
|
|
3525
3527
|
console.log(`Platform: ${chalk.green(process.platform)} ${process.arch}`);
|
|
3526
3528
|
console.log(`Node.js Version: ${chalk.green(process.version)}`);
|
|
3527
3529
|
console.log("");
|
|
3530
|
+
console.log(chalk.bold("\u{1F527} Daemon Spawn Diagnostics"));
|
|
3531
|
+
const projectRoot = projectPath();
|
|
3532
|
+
const wrapperPath = join(projectRoot, "bin", "happy.mjs");
|
|
3533
|
+
const cliEntrypoint = join(projectRoot, "dist", "index.mjs");
|
|
3534
|
+
console.log(`Project Root: ${chalk.blue(projectRoot)}`);
|
|
3535
|
+
console.log(`Wrapper Script: ${chalk.blue(wrapperPath)}`);
|
|
3536
|
+
console.log(`CLI Entrypoint: ${chalk.blue(cliEntrypoint)}`);
|
|
3537
|
+
console.log(`Wrapper Exists: ${existsSync(wrapperPath) ? chalk.green("\u2713 Yes") : chalk.red("\u274C No")}`);
|
|
3538
|
+
console.log(`CLI Exists: ${existsSync(cliEntrypoint) ? chalk.green("\u2713 Yes") : chalk.red("\u274C No")}`);
|
|
3539
|
+
console.log("");
|
|
3528
3540
|
console.log(chalk.bold("\u2699\uFE0F Configuration"));
|
|
3529
3541
|
console.log(`Happy Home: ${chalk.blue(configuration.happyHomeDir)}`);
|
|
3530
3542
|
console.log(`Server URL: ${chalk.blue(configuration.serverUrl)}`);
|
|
@@ -3696,313 +3708,175 @@ var controlClient = /*#__PURE__*/Object.freeze({
|
|
|
3696
3708
|
stopDaemonSession: stopDaemonSession
|
|
3697
3709
|
});
|
|
3698
3710
|
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3711
|
+
function startDaemonControlServer({
|
|
3712
|
+
getChildren,
|
|
3713
|
+
stopSession,
|
|
3714
|
+
spawnSession,
|
|
3715
|
+
requestShutdown,
|
|
3716
|
+
onHappySessionWebhook
|
|
3717
|
+
}) {
|
|
3718
|
+
return new Promise((resolve) => {
|
|
3719
|
+
const app = fastify({
|
|
3720
|
+
logger: false
|
|
3721
|
+
// We use our own logger
|
|
3722
|
+
});
|
|
3723
|
+
app.setValidatorCompiler(validatorCompiler);
|
|
3724
|
+
app.setSerializerCompiler(serializerCompiler);
|
|
3725
|
+
const typed = app.withTypeProvider();
|
|
3726
|
+
typed.post("/session-started", {
|
|
3727
|
+
schema: {
|
|
3728
|
+
body: z$1.object({
|
|
3729
|
+
sessionId: z$1.string(),
|
|
3730
|
+
metadata: z$1.any()
|
|
3731
|
+
// Metadata type from API
|
|
3732
|
+
})
|
|
3733
|
+
}
|
|
3734
|
+
}, async (request, reply) => {
|
|
3735
|
+
const { sessionId, metadata } = request.body;
|
|
3736
|
+
logger.debug(`[CONTROL SERVER] Session started: ${sessionId}`);
|
|
3737
|
+
onHappySessionWebhook(sessionId, metadata);
|
|
3738
|
+
return { status: "ok" };
|
|
3739
|
+
});
|
|
3740
|
+
typed.post("/list", async (request, reply) => {
|
|
3741
|
+
const children = getChildren();
|
|
3742
|
+
logger.debug(`[CONTROL SERVER] Listing ${children.length} sessions`);
|
|
3743
|
+
return { children };
|
|
3744
|
+
});
|
|
3745
|
+
typed.post("/stop-session", {
|
|
3746
|
+
schema: {
|
|
3747
|
+
body: z$1.object({
|
|
3748
|
+
sessionId: z$1.string()
|
|
3749
|
+
})
|
|
3750
|
+
}
|
|
3751
|
+
}, async (request, reply) => {
|
|
3752
|
+
const { sessionId } = request.body;
|
|
3753
|
+
logger.debug(`[CONTROL SERVER] Stop session request: ${sessionId}`);
|
|
3754
|
+
const success = stopSession(sessionId);
|
|
3755
|
+
return { success };
|
|
3756
|
+
});
|
|
3757
|
+
typed.post("/spawn-session", {
|
|
3758
|
+
schema: {
|
|
3759
|
+
body: z$1.object({
|
|
3760
|
+
directory: z$1.string(),
|
|
3761
|
+
sessionId: z$1.string().optional()
|
|
3762
|
+
})
|
|
3763
|
+
}
|
|
3764
|
+
}, async (request, reply) => {
|
|
3765
|
+
const { directory, sessionId } = request.body;
|
|
3766
|
+
logger.debug(`[CONTROL SERVER] Spawn session request: dir=${directory}, sessionId=${sessionId || "new"}`);
|
|
3767
|
+
const session = await spawnSession(directory, sessionId);
|
|
3768
|
+
if (session) {
|
|
3769
|
+
return {
|
|
3770
|
+
success: true,
|
|
3771
|
+
pid: session.pid,
|
|
3772
|
+
sessionId: session.happySessionId || "pending"
|
|
3773
|
+
};
|
|
3774
|
+
} else {
|
|
3775
|
+
reply.code(500);
|
|
3776
|
+
return { error: "Failed to spawn session" };
|
|
3777
|
+
}
|
|
3778
|
+
});
|
|
3779
|
+
typed.post("/stop", async (request, reply) => {
|
|
3780
|
+
logger.debug("[CONTROL SERVER] Stop daemon request received");
|
|
3781
|
+
setTimeout(() => {
|
|
3782
|
+
logger.debug("[CONTROL SERVER] Triggering daemon shutdown");
|
|
3783
|
+
requestShutdown();
|
|
3784
|
+
}, 50);
|
|
3785
|
+
return { status: "stopping" };
|
|
3786
|
+
});
|
|
3787
|
+
app.listen({ port: 0, host: "127.0.0.1" }, (err, address) => {
|
|
3788
|
+
if (err) {
|
|
3789
|
+
logger.debug("[CONTROL SERVER] Failed to start:", err);
|
|
3790
|
+
throw err;
|
|
3791
|
+
}
|
|
3792
|
+
const port = parseInt(address.split(":").pop());
|
|
3793
|
+
logger.debug(`[CONTROL SERVER] Started on port ${port}`);
|
|
3794
|
+
resolve({
|
|
3795
|
+
port,
|
|
3796
|
+
stop: async () => {
|
|
3797
|
+
logger.debug("[CONTROL SERVER] Stopping server");
|
|
3798
|
+
await app.close();
|
|
3799
|
+
logger.debug("[CONTROL SERVER] Server stopped");
|
|
3800
|
+
}
|
|
3801
|
+
});
|
|
3802
|
+
});
|
|
3803
|
+
});
|
|
3804
|
+
}
|
|
3805
|
+
|
|
3806
|
+
function displayQRCode(url) {
|
|
3807
|
+
console.log("=".repeat(80));
|
|
3808
|
+
console.log("\u{1F4F1} To authenticate, scan this QR code with your mobile device:");
|
|
3809
|
+
console.log("=".repeat(80));
|
|
3810
|
+
qrcode.generate(url, { small: true }, (qr) => {
|
|
3811
|
+
for (let l of qr.split("\n")) {
|
|
3812
|
+
console.log(" ".repeat(10) + l);
|
|
3813
|
+
}
|
|
3814
|
+
});
|
|
3815
|
+
console.log("=".repeat(80));
|
|
3816
|
+
}
|
|
3817
|
+
|
|
3818
|
+
function generateWebAuthUrl(publicKey) {
|
|
3819
|
+
const publicKeyBase64 = encodeBase64(publicKey, "base64url");
|
|
3820
|
+
return `https://app.happy.engineering/terminal/connect#key=${publicKeyBase64}`;
|
|
3821
|
+
}
|
|
3822
|
+
|
|
3823
|
+
async function openBrowser(url) {
|
|
3727
3824
|
try {
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
logger.debug(`[START] Reported session ${response.id} to daemon`);
|
|
3825
|
+
if (!process.stdout.isTTY || process.env.CI || process.env.HEADLESS) {
|
|
3826
|
+
logger.debug("[browser] Headless environment detected, skipping browser open");
|
|
3827
|
+
return false;
|
|
3732
3828
|
}
|
|
3829
|
+
logger.debug(`[browser] Attempting to open URL: ${url}`);
|
|
3830
|
+
await open$1(url);
|
|
3831
|
+
logger.debug("[browser] Browser opened successfully");
|
|
3832
|
+
return true;
|
|
3733
3833
|
} catch (error) {
|
|
3734
|
-
logger.debug("[
|
|
3834
|
+
logger.debug("[browser] Failed to open browser:", error);
|
|
3835
|
+
return false;
|
|
3735
3836
|
}
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3837
|
+
}
|
|
3838
|
+
|
|
3839
|
+
const AuthSelector = ({ onSelect, onCancel }) => {
|
|
3840
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
3841
|
+
const options = [
|
|
3842
|
+
{
|
|
3843
|
+
method: "mobile",
|
|
3844
|
+
label: "Mobile App"
|
|
3845
|
+
},
|
|
3846
|
+
{
|
|
3847
|
+
method: "web",
|
|
3848
|
+
label: "Web Browser"
|
|
3849
|
+
}
|
|
3850
|
+
];
|
|
3851
|
+
useInput((input, key) => {
|
|
3852
|
+
if (key.upArrow) {
|
|
3853
|
+
setSelectedIndex((prev) => Math.max(0, prev - 1));
|
|
3854
|
+
} else if (key.downArrow) {
|
|
3855
|
+
setSelectedIndex((prev) => Math.min(options.length - 1, prev + 1));
|
|
3856
|
+
} else if (key.return) {
|
|
3857
|
+
onSelect(options[selectedIndex].method);
|
|
3858
|
+
} else if (key.escape || key.ctrl && input === "c") {
|
|
3859
|
+
onCancel();
|
|
3860
|
+
} else if (input === "1") {
|
|
3861
|
+
setSelectedIndex(0);
|
|
3862
|
+
onSelect("mobile");
|
|
3863
|
+
} else if (input === "2") {
|
|
3864
|
+
setSelectedIndex(1);
|
|
3865
|
+
onSelect("web");
|
|
3747
3866
|
}
|
|
3748
3867
|
});
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
const
|
|
3758
|
-
if (
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
const messageQueue = new MessageQueue2((mode) => hashObject(mode));
|
|
3762
|
-
registerHandlers(session);
|
|
3763
|
-
let currentPermissionMode = options.permissionMode;
|
|
3764
|
-
let currentModel = options.model;
|
|
3765
|
-
let currentFallbackModel = void 0;
|
|
3766
|
-
let currentCustomSystemPrompt = void 0;
|
|
3767
|
-
let currentAppendSystemPrompt = void 0;
|
|
3768
|
-
let currentAllowedTools = void 0;
|
|
3769
|
-
let currentDisallowedTools = void 0;
|
|
3770
|
-
session.onUserMessage((message) => {
|
|
3771
|
-
let messagePermissionMode = currentPermissionMode;
|
|
3772
|
-
if (message.meta?.permissionMode) {
|
|
3773
|
-
const validModes = ["default", "acceptEdits", "bypassPermissions", "plan"];
|
|
3774
|
-
if (validModes.includes(message.meta.permissionMode)) {
|
|
3775
|
-
messagePermissionMode = message.meta.permissionMode;
|
|
3776
|
-
currentPermissionMode = messagePermissionMode;
|
|
3777
|
-
logger.debug(`[loop] Permission mode updated from user message to: ${currentPermissionMode}`);
|
|
3778
|
-
} else {
|
|
3779
|
-
logger.debug(`[loop] Invalid permission mode received: ${message.meta.permissionMode}`);
|
|
3780
|
-
}
|
|
3781
|
-
} else {
|
|
3782
|
-
logger.debug(`[loop] User message received with no permission mode override, using current: ${currentPermissionMode}`);
|
|
3783
|
-
}
|
|
3784
|
-
let messageModel = currentModel;
|
|
3785
|
-
if (message.meta?.hasOwnProperty("model")) {
|
|
3786
|
-
messageModel = message.meta.model || void 0;
|
|
3787
|
-
currentModel = messageModel;
|
|
3788
|
-
logger.debug(`[loop] Model updated from user message: ${messageModel || "reset to default"}`);
|
|
3789
|
-
} else {
|
|
3790
|
-
logger.debug(`[loop] User message received with no model override, using current: ${currentModel || "default"}`);
|
|
3791
|
-
}
|
|
3792
|
-
let messageCustomSystemPrompt = currentCustomSystemPrompt;
|
|
3793
|
-
if (message.meta?.hasOwnProperty("customSystemPrompt")) {
|
|
3794
|
-
messageCustomSystemPrompt = message.meta.customSystemPrompt || void 0;
|
|
3795
|
-
currentCustomSystemPrompt = messageCustomSystemPrompt;
|
|
3796
|
-
logger.debug(`[loop] Custom system prompt updated from user message: ${messageCustomSystemPrompt ? "set" : "reset to none"}`);
|
|
3797
|
-
} else {
|
|
3798
|
-
logger.debug(`[loop] User message received with no custom system prompt override, using current: ${currentCustomSystemPrompt ? "set" : "none"}`);
|
|
3799
|
-
}
|
|
3800
|
-
let messageFallbackModel = currentFallbackModel;
|
|
3801
|
-
if (message.meta?.hasOwnProperty("fallbackModel")) {
|
|
3802
|
-
messageFallbackModel = message.meta.fallbackModel || void 0;
|
|
3803
|
-
currentFallbackModel = messageFallbackModel;
|
|
3804
|
-
logger.debug(`[loop] Fallback model updated from user message: ${messageFallbackModel || "reset to none"}`);
|
|
3805
|
-
} else {
|
|
3806
|
-
logger.debug(`[loop] User message received with no fallback model override, using current: ${currentFallbackModel || "none"}`);
|
|
3807
|
-
}
|
|
3808
|
-
let messageAppendSystemPrompt = currentAppendSystemPrompt;
|
|
3809
|
-
if (message.meta?.hasOwnProperty("appendSystemPrompt")) {
|
|
3810
|
-
messageAppendSystemPrompt = message.meta.appendSystemPrompt || void 0;
|
|
3811
|
-
currentAppendSystemPrompt = messageAppendSystemPrompt;
|
|
3812
|
-
logger.debug(`[loop] Append system prompt updated from user message: ${messageAppendSystemPrompt ? "set" : "reset to none"}`);
|
|
3813
|
-
} else {
|
|
3814
|
-
logger.debug(`[loop] User message received with no append system prompt override, using current: ${currentAppendSystemPrompt ? "set" : "none"}`);
|
|
3815
|
-
}
|
|
3816
|
-
let messageAllowedTools = currentAllowedTools;
|
|
3817
|
-
if (message.meta?.hasOwnProperty("allowedTools")) {
|
|
3818
|
-
messageAllowedTools = message.meta.allowedTools || void 0;
|
|
3819
|
-
currentAllowedTools = messageAllowedTools;
|
|
3820
|
-
logger.debug(`[loop] Allowed tools updated from user message: ${messageAllowedTools ? messageAllowedTools.join(", ") : "reset to none"}`);
|
|
3821
|
-
} else {
|
|
3822
|
-
logger.debug(`[loop] User message received with no allowed tools override, using current: ${currentAllowedTools ? currentAllowedTools.join(", ") : "none"}`);
|
|
3823
|
-
}
|
|
3824
|
-
let messageDisallowedTools = currentDisallowedTools;
|
|
3825
|
-
if (message.meta?.hasOwnProperty("disallowedTools")) {
|
|
3826
|
-
messageDisallowedTools = message.meta.disallowedTools || void 0;
|
|
3827
|
-
currentDisallowedTools = messageDisallowedTools;
|
|
3828
|
-
logger.debug(`[loop] Disallowed tools updated from user message: ${messageDisallowedTools ? messageDisallowedTools.join(", ") : "reset to none"}`);
|
|
3829
|
-
} else {
|
|
3830
|
-
logger.debug(`[loop] User message received with no disallowed tools override, using current: ${currentDisallowedTools ? currentDisallowedTools.join(", ") : "none"}`);
|
|
3831
|
-
}
|
|
3832
|
-
const specialCommand = parseSpecialCommand(message.content.text);
|
|
3833
|
-
if (specialCommand.type === "compact") {
|
|
3834
|
-
logger.debug("[start] Detected /compact command");
|
|
3835
|
-
const enhancedMode2 = {
|
|
3836
|
-
permissionMode: messagePermissionMode || "default",
|
|
3837
|
-
model: messageModel,
|
|
3838
|
-
fallbackModel: messageFallbackModel,
|
|
3839
|
-
customSystemPrompt: messageCustomSystemPrompt,
|
|
3840
|
-
appendSystemPrompt: messageAppendSystemPrompt,
|
|
3841
|
-
allowedTools: messageAllowedTools,
|
|
3842
|
-
disallowedTools: messageDisallowedTools
|
|
3843
|
-
};
|
|
3844
|
-
messageQueue.pushIsolateAndClear(specialCommand.originalMessage || message.content.text, enhancedMode2);
|
|
3845
|
-
logger.debugLargeJson("[start] /compact command pushed to queue:", message);
|
|
3846
|
-
return;
|
|
3847
|
-
}
|
|
3848
|
-
if (specialCommand.type === "clear") {
|
|
3849
|
-
logger.debug("[start] Detected /clear command");
|
|
3850
|
-
const enhancedMode2 = {
|
|
3851
|
-
permissionMode: messagePermissionMode || "default",
|
|
3852
|
-
model: messageModel,
|
|
3853
|
-
fallbackModel: messageFallbackModel,
|
|
3854
|
-
customSystemPrompt: messageCustomSystemPrompt,
|
|
3855
|
-
appendSystemPrompt: messageAppendSystemPrompt,
|
|
3856
|
-
allowedTools: messageAllowedTools,
|
|
3857
|
-
disallowedTools: messageDisallowedTools
|
|
3858
|
-
};
|
|
3859
|
-
messageQueue.pushIsolateAndClear(specialCommand.originalMessage || message.content.text, enhancedMode2);
|
|
3860
|
-
logger.debugLargeJson("[start] /compact command pushed to queue:", message);
|
|
3861
|
-
return;
|
|
3862
|
-
}
|
|
3863
|
-
const enhancedMode = {
|
|
3864
|
-
permissionMode: messagePermissionMode || "default",
|
|
3865
|
-
model: messageModel,
|
|
3866
|
-
fallbackModel: messageFallbackModel,
|
|
3867
|
-
customSystemPrompt: messageCustomSystemPrompt,
|
|
3868
|
-
appendSystemPrompt: messageAppendSystemPrompt,
|
|
3869
|
-
allowedTools: messageAllowedTools,
|
|
3870
|
-
disallowedTools: messageDisallowedTools
|
|
3871
|
-
};
|
|
3872
|
-
messageQueue.push(message.content.text, enhancedMode);
|
|
3873
|
-
logger.debugLargeJson("User message pushed to queue:", message);
|
|
3874
|
-
});
|
|
3875
|
-
const cleanup = async () => {
|
|
3876
|
-
logger.debug("[START] Received termination signal, cleaning up...");
|
|
3877
|
-
try {
|
|
3878
|
-
if (session) {
|
|
3879
|
-
session.sendSessionDeath();
|
|
3880
|
-
await session.flush();
|
|
3881
|
-
await session.close();
|
|
3882
|
-
}
|
|
3883
|
-
stopCaffeinate();
|
|
3884
|
-
logger.debug("[START] Cleanup complete, exiting");
|
|
3885
|
-
process.exit(0);
|
|
3886
|
-
} catch (error) {
|
|
3887
|
-
logger.debug("[START] Error during cleanup:", error);
|
|
3888
|
-
process.exit(1);
|
|
3889
|
-
}
|
|
3890
|
-
};
|
|
3891
|
-
process.on("SIGTERM", cleanup);
|
|
3892
|
-
process.on("SIGINT", cleanup);
|
|
3893
|
-
process.on("uncaughtException", (error) => {
|
|
3894
|
-
logger.debug("[START] Uncaught exception:", error);
|
|
3895
|
-
cleanup();
|
|
3896
|
-
});
|
|
3897
|
-
process.on("unhandledRejection", (reason) => {
|
|
3898
|
-
logger.debug("[START] Unhandled rejection:", reason);
|
|
3899
|
-
cleanup();
|
|
3900
|
-
});
|
|
3901
|
-
await loop({
|
|
3902
|
-
path: workingDirectory,
|
|
3903
|
-
model: options.model,
|
|
3904
|
-
permissionMode: options.permissionMode,
|
|
3905
|
-
startingMode: options.startingMode,
|
|
3906
|
-
messageQueue,
|
|
3907
|
-
api,
|
|
3908
|
-
onModeChange: (newMode) => {
|
|
3909
|
-
session.sendSessionEvent({ type: "switch", mode: newMode });
|
|
3910
|
-
session.updateAgentState((currentState) => ({
|
|
3911
|
-
...currentState,
|
|
3912
|
-
controlledByUser: newMode === "local"
|
|
3913
|
-
}));
|
|
3914
|
-
},
|
|
3915
|
-
onSessionReady: (sessionInstance) => {
|
|
3916
|
-
},
|
|
3917
|
-
mcpServers: {},
|
|
3918
|
-
session,
|
|
3919
|
-
claudeEnvVars: options.claudeEnvVars,
|
|
3920
|
-
claudeArgs: options.claudeArgs
|
|
3921
|
-
});
|
|
3922
|
-
session.sendSessionDeath();
|
|
3923
|
-
logger.debug("Waiting for socket to flush...");
|
|
3924
|
-
await session.flush();
|
|
3925
|
-
logger.debug("Closing session...");
|
|
3926
|
-
await session.close();
|
|
3927
|
-
stopCaffeinate();
|
|
3928
|
-
logger.debug("Stopped sleep prevention");
|
|
3929
|
-
process.exit(0);
|
|
3930
|
-
}
|
|
3931
|
-
|
|
3932
|
-
function displayQRCode(url) {
|
|
3933
|
-
console.log("=".repeat(80));
|
|
3934
|
-
console.log("\u{1F4F1} To authenticate, scan this QR code with your mobile device:");
|
|
3935
|
-
console.log("=".repeat(80));
|
|
3936
|
-
qrcode.generate(url, { small: true }, (qr) => {
|
|
3937
|
-
for (let l of qr.split("\n")) {
|
|
3938
|
-
console.log(" ".repeat(10) + l);
|
|
3939
|
-
}
|
|
3940
|
-
});
|
|
3941
|
-
console.log("=".repeat(80));
|
|
3942
|
-
}
|
|
3943
|
-
|
|
3944
|
-
function generateWebAuthUrl(publicKey) {
|
|
3945
|
-
const publicKeyBase64 = encodeBase64(publicKey, "base64url");
|
|
3946
|
-
return `https://app.happy.engineering/terminal/connect#key=${publicKeyBase64}`;
|
|
3947
|
-
}
|
|
3948
|
-
|
|
3949
|
-
async function openBrowser(url) {
|
|
3950
|
-
try {
|
|
3951
|
-
if (!process.stdout.isTTY || process.env.CI || process.env.HEADLESS) {
|
|
3952
|
-
logger.debug("[browser] Headless environment detected, skipping browser open");
|
|
3953
|
-
return false;
|
|
3954
|
-
}
|
|
3955
|
-
logger.debug(`[browser] Attempting to open URL: ${url}`);
|
|
3956
|
-
await open$1(url);
|
|
3957
|
-
logger.debug("[browser] Browser opened successfully");
|
|
3958
|
-
return true;
|
|
3959
|
-
} catch (error) {
|
|
3960
|
-
logger.debug("[browser] Failed to open browser:", error);
|
|
3961
|
-
return false;
|
|
3962
|
-
}
|
|
3963
|
-
}
|
|
3964
|
-
|
|
3965
|
-
const AuthSelector = ({ onSelect, onCancel }) => {
|
|
3966
|
-
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
3967
|
-
const options = [
|
|
3968
|
-
{
|
|
3969
|
-
method: "mobile",
|
|
3970
|
-
label: "Mobile App"
|
|
3971
|
-
},
|
|
3972
|
-
{
|
|
3973
|
-
method: "web",
|
|
3974
|
-
label: "Web Browser"
|
|
3975
|
-
}
|
|
3976
|
-
];
|
|
3977
|
-
useInput((input, key) => {
|
|
3978
|
-
if (key.upArrow) {
|
|
3979
|
-
setSelectedIndex((prev) => Math.max(0, prev - 1));
|
|
3980
|
-
} else if (key.downArrow) {
|
|
3981
|
-
setSelectedIndex((prev) => Math.min(options.length - 1, prev + 1));
|
|
3982
|
-
} else if (key.return) {
|
|
3983
|
-
onSelect(options[selectedIndex].method);
|
|
3984
|
-
} else if (key.escape || key.ctrl && input === "c") {
|
|
3985
|
-
onCancel();
|
|
3986
|
-
} else if (input === "1") {
|
|
3987
|
-
setSelectedIndex(0);
|
|
3988
|
-
onSelect("mobile");
|
|
3989
|
-
} else if (input === "2") {
|
|
3990
|
-
setSelectedIndex(1);
|
|
3991
|
-
onSelect("web");
|
|
3992
|
-
}
|
|
3993
|
-
});
|
|
3994
|
-
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) => {
|
|
3995
|
-
const isSelected = selectedIndex === index;
|
|
3996
|
-
return /* @__PURE__ */ React.createElement(Box, { key: option.method, marginY: 0 }, /* @__PURE__ */ React.createElement(Text, { color: isSelected ? "cyan" : "gray" }, isSelected ? "\u203A " : " ", index + 1, ". ", option.label));
|
|
3997
|
-
})), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "Use arrows or 1-2 to select, Enter to confirm")));
|
|
3998
|
-
};
|
|
3999
|
-
|
|
4000
|
-
async function doAuth() {
|
|
4001
|
-
console.clear();
|
|
4002
|
-
const authMethod = await selectAuthenticationMethod();
|
|
4003
|
-
if (!authMethod) {
|
|
4004
|
-
console.log("\nAuthentication cancelled.\n");
|
|
4005
|
-
return null;
|
|
3868
|
+
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) => {
|
|
3869
|
+
const isSelected = selectedIndex === index;
|
|
3870
|
+
return /* @__PURE__ */ React.createElement(Box, { key: option.method, marginY: 0 }, /* @__PURE__ */ React.createElement(Text, { color: isSelected ? "cyan" : "gray" }, isSelected ? "\u203A " : " ", index + 1, ". ", option.label));
|
|
3871
|
+
})), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "Use arrows or 1-2 to select, Enter to confirm")));
|
|
3872
|
+
};
|
|
3873
|
+
|
|
3874
|
+
async function doAuth() {
|
|
3875
|
+
console.clear();
|
|
3876
|
+
const authMethod = await selectAuthenticationMethod();
|
|
3877
|
+
if (!authMethod) {
|
|
3878
|
+
console.log("\nAuthentication cancelled.\n");
|
|
3879
|
+
return null;
|
|
4006
3880
|
}
|
|
4007
3881
|
const secret = new Uint8Array(randomBytes(32));
|
|
4008
3882
|
const keypair = tweetnacl.box.keyPair.fromSecretKey(secret);
|
|
@@ -4146,6 +4020,8 @@ async function authAndSetupMachineIfNeeded() {
|
|
|
4146
4020
|
}
|
|
4147
4021
|
const settings = await updateSettings(async (s) => {
|
|
4148
4022
|
if (!s.machineId) {
|
|
4023
|
+
const newMachineId = randomUUID();
|
|
4024
|
+
console.log(`[AUTH] No machine ID found, generating new one: ${newMachineId}; We will not create machine on startup since we don't have api client intialized`);
|
|
4149
4025
|
return {
|
|
4150
4026
|
...s,
|
|
4151
4027
|
machineId: randomUUID()
|
|
@@ -4157,101 +4033,33 @@ async function authAndSetupMachineIfNeeded() {
|
|
|
4157
4033
|
return { credentials, machineId: settings.machineId };
|
|
4158
4034
|
}
|
|
4159
4035
|
|
|
4160
|
-
function
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
}
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
return { status: "ok" };
|
|
4188
|
-
});
|
|
4189
|
-
typed.post("/list", async (request, reply) => {
|
|
4190
|
-
const children = getChildren();
|
|
4191
|
-
logger.debug(`[CONTROL SERVER] Listing ${children.length} sessions`);
|
|
4192
|
-
return { children };
|
|
4193
|
-
});
|
|
4194
|
-
typed.post("/stop-session", {
|
|
4195
|
-
schema: {
|
|
4196
|
-
body: z$1.object({
|
|
4197
|
-
sessionId: z$1.string()
|
|
4198
|
-
})
|
|
4199
|
-
}
|
|
4200
|
-
}, async (request, reply) => {
|
|
4201
|
-
const { sessionId } = request.body;
|
|
4202
|
-
logger.debug(`[CONTROL SERVER] Stop session request: ${sessionId}`);
|
|
4203
|
-
const success = stopSession(sessionId);
|
|
4204
|
-
return { success };
|
|
4205
|
-
});
|
|
4206
|
-
typed.post("/spawn-session", {
|
|
4207
|
-
schema: {
|
|
4208
|
-
body: z$1.object({
|
|
4209
|
-
directory: z$1.string(),
|
|
4210
|
-
sessionId: z$1.string().optional()
|
|
4211
|
-
})
|
|
4212
|
-
}
|
|
4213
|
-
}, async (request, reply) => {
|
|
4214
|
-
const { directory, sessionId } = request.body;
|
|
4215
|
-
logger.debug(`[CONTROL SERVER] Spawn session request: dir=${directory}, sessionId=${sessionId || "new"}`);
|
|
4216
|
-
const session = await spawnSession(directory, sessionId);
|
|
4217
|
-
if (session) {
|
|
4218
|
-
return {
|
|
4219
|
-
success: true,
|
|
4220
|
-
pid: session.pid,
|
|
4221
|
-
sessionId: session.happySessionId || "pending"
|
|
4222
|
-
};
|
|
4223
|
-
} else {
|
|
4224
|
-
reply.code(500);
|
|
4225
|
-
return { error: "Failed to spawn session" };
|
|
4226
|
-
}
|
|
4227
|
-
});
|
|
4228
|
-
typed.post("/stop", async (request, reply) => {
|
|
4229
|
-
logger.debug("[CONTROL SERVER] Stop daemon request received");
|
|
4230
|
-
setTimeout(() => {
|
|
4231
|
-
logger.debug("[CONTROL SERVER] Triggering daemon shutdown");
|
|
4232
|
-
requestShutdown();
|
|
4233
|
-
}, 50);
|
|
4234
|
-
return { status: "stopping" };
|
|
4235
|
-
});
|
|
4236
|
-
app.listen({ port: 0, host: "127.0.0.1" }, (err, address) => {
|
|
4237
|
-
if (err) {
|
|
4238
|
-
logger.debug("[CONTROL SERVER] Failed to start:", err);
|
|
4239
|
-
throw err;
|
|
4240
|
-
}
|
|
4241
|
-
const port = parseInt(address.split(":").pop());
|
|
4242
|
-
logger.debug(`[CONTROL SERVER] Started on port ${port}`);
|
|
4243
|
-
resolve({
|
|
4244
|
-
port,
|
|
4245
|
-
stop: async () => {
|
|
4246
|
-
logger.debug("[CONTROL SERVER] Stopping server");
|
|
4247
|
-
await app.close();
|
|
4248
|
-
logger.debug("[CONTROL SERVER] Server stopped");
|
|
4249
|
-
}
|
|
4250
|
-
});
|
|
4251
|
-
});
|
|
4252
|
-
});
|
|
4253
|
-
}
|
|
4254
|
-
|
|
4036
|
+
function spawnHappyCLI(args, options = {}) {
|
|
4037
|
+
const projectRoot = projectPath();
|
|
4038
|
+
const entrypoint = join(projectRoot, "dist", "index.mjs");
|
|
4039
|
+
let directory;
|
|
4040
|
+
if ("cwd" in options) {
|
|
4041
|
+
directory = options.cwd;
|
|
4042
|
+
} else {
|
|
4043
|
+
directory = process.cwd();
|
|
4044
|
+
}
|
|
4045
|
+
const fullCommand = `happy ${args.join(" ")}`;
|
|
4046
|
+
logger.debug(`[DAEMON RUN] Spawning: ${fullCommand} in ${directory}`);
|
|
4047
|
+
const nodeArgs = [
|
|
4048
|
+
"--no-warnings",
|
|
4049
|
+
"--no-deprecation",
|
|
4050
|
+
entrypoint,
|
|
4051
|
+
...args
|
|
4052
|
+
];
|
|
4053
|
+
return spawn$1("node", nodeArgs, options);
|
|
4054
|
+
}
|
|
4055
|
+
|
|
4056
|
+
const initialMachineMetadata = {
|
|
4057
|
+
host: os.hostname(),
|
|
4058
|
+
platform: os.platform(),
|
|
4059
|
+
happyCliVersion: packageJson.version,
|
|
4060
|
+
homeDir: os.homedir(),
|
|
4061
|
+
happyHomeDir: configuration.happyHomeDir
|
|
4062
|
+
};
|
|
4255
4063
|
async function startDaemon() {
|
|
4256
4064
|
logger.debug("[DAEMON RUN] Starting daemon process...");
|
|
4257
4065
|
logger.debugLargeJson("[DAEMON RUN] Environment", getEnvironmentInfo());
|
|
@@ -4260,6 +4068,7 @@ async function startDaemon() {
|
|
|
4260
4068
|
try {
|
|
4261
4069
|
process.kill(runningDaemon.pid, 0);
|
|
4262
4070
|
logger.debug("[DAEMON RUN] Daemon already running");
|
|
4071
|
+
console.log(`Daemon already running (PID: ${runningDaemon.pid})`);
|
|
4263
4072
|
process.exit(0);
|
|
4264
4073
|
} catch {
|
|
4265
4074
|
logger.debug("[DAEMON RUN] Stale state found, cleaning up");
|
|
@@ -4314,9 +4123,7 @@ async function startDaemon() {
|
|
|
4314
4123
|
"--started-by",
|
|
4315
4124
|
"daemon"
|
|
4316
4125
|
];
|
|
4317
|
-
const
|
|
4318
|
-
logger.debug(`[DAEMON RUN] Spawning: ${fullCommand} in ${directory}`);
|
|
4319
|
-
const happyProcess = spawn$1(happyBinPath, args, {
|
|
4126
|
+
const happyProcess = spawnHappyCLI(args, {
|
|
4320
4127
|
cwd: directory,
|
|
4321
4128
|
detached: true,
|
|
4322
4129
|
// Sessions stay alive when daemon stops
|
|
@@ -4421,13 +4228,6 @@ async function startDaemon() {
|
|
|
4421
4228
|
};
|
|
4422
4229
|
await writeDaemonState(fileState);
|
|
4423
4230
|
logger.debug("[DAEMON RUN] Daemon state written");
|
|
4424
|
-
const initialMetadata = {
|
|
4425
|
-
host: os$1.hostname(),
|
|
4426
|
-
platform: os$1.platform(),
|
|
4427
|
-
happyCliVersion: packageJson.version,
|
|
4428
|
-
homeDir: os$1.homedir(),
|
|
4429
|
-
happyHomeDir: configuration.happyHomeDir
|
|
4430
|
-
};
|
|
4431
4231
|
const initialDaemonState = {
|
|
4432
4232
|
status: "offline",
|
|
4433
4233
|
pid: process.pid,
|
|
@@ -4435,9 +4235,9 @@ async function startDaemon() {
|
|
|
4435
4235
|
startedAt: Date.now()
|
|
4436
4236
|
};
|
|
4437
4237
|
const api = new ApiClient(credentials.token, credentials.secret);
|
|
4438
|
-
const machine = await api.
|
|
4238
|
+
const machine = await api.createMachineOrGetExistingAsIs({
|
|
4439
4239
|
machineId,
|
|
4440
|
-
metadata:
|
|
4240
|
+
metadata: initialMachineMetadata,
|
|
4441
4241
|
daemonState: initialDaemonState
|
|
4442
4242
|
});
|
|
4443
4243
|
logger.debug(`[DAEMON RUN] Machine registered: ${machine.id}`);
|
|
@@ -4502,6 +4302,247 @@ async function startDaemon() {
|
|
|
4502
4302
|
}
|
|
4503
4303
|
}
|
|
4504
4304
|
|
|
4305
|
+
async function start(credentials, options = {}) {
|
|
4306
|
+
const workingDirectory = process.cwd();
|
|
4307
|
+
const sessionTag = randomUUID();
|
|
4308
|
+
logger.debugLargeJson("[START] Happy process started", getEnvironmentInfo());
|
|
4309
|
+
logger.debug(`[START] Options: startedBy=${options.startedBy}, startingMode=${options.startingMode}`);
|
|
4310
|
+
if (options.startedBy === "daemon" && options.startingMode === "local") {
|
|
4311
|
+
logger.debug("Daemon spawn requested with local mode - forcing remote mode");
|
|
4312
|
+
options.startingMode = "remote";
|
|
4313
|
+
}
|
|
4314
|
+
const api = new ApiClient(credentials.token, credentials.secret);
|
|
4315
|
+
let state = {};
|
|
4316
|
+
const settings = await readSettings();
|
|
4317
|
+
let machineId = settings?.machineId;
|
|
4318
|
+
if (!machineId) {
|
|
4319
|
+
console.error(`[START] No machine ID found in settings, which is unexepcted since authAndSetupMachineIfNeeded should have created it, using 'unknown' id instead`);
|
|
4320
|
+
machineId = "unknown";
|
|
4321
|
+
}
|
|
4322
|
+
logger.debug(`Using machineId: ${machineId}`);
|
|
4323
|
+
let metadata = {
|
|
4324
|
+
path: workingDirectory,
|
|
4325
|
+
host: os$1.hostname(),
|
|
4326
|
+
version: packageJson.version,
|
|
4327
|
+
os: os$1.platform(),
|
|
4328
|
+
machineId,
|
|
4329
|
+
homeDir: os$1.homedir(),
|
|
4330
|
+
happyHomeDir: configuration.happyHomeDir,
|
|
4331
|
+
startedFromDaemon: options.startedBy === "daemon",
|
|
4332
|
+
hostPid: process.pid,
|
|
4333
|
+
startedBy: options.startedBy || "terminal"
|
|
4334
|
+
};
|
|
4335
|
+
const response = await api.getOrCreateSession({ tag: sessionTag, metadata, state });
|
|
4336
|
+
logger.debug(`Session created: ${response.id}`);
|
|
4337
|
+
await api.createMachineOrGetExistingAsIs({
|
|
4338
|
+
machineId,
|
|
4339
|
+
metadata: initialMachineMetadata
|
|
4340
|
+
});
|
|
4341
|
+
try {
|
|
4342
|
+
const daemonState = await getDaemonState();
|
|
4343
|
+
if (daemonState?.httpPort) {
|
|
4344
|
+
await notifyDaemonSessionStarted(response.id, metadata);
|
|
4345
|
+
logger.debug(`[START] Reported session ${response.id} to daemon`);
|
|
4346
|
+
}
|
|
4347
|
+
} catch (error) {
|
|
4348
|
+
logger.debug("[START] Failed to report to daemon (may not be running):", error);
|
|
4349
|
+
}
|
|
4350
|
+
extractSDKMetadataAsync(async (sdkMetadata) => {
|
|
4351
|
+
logger.debug("[start] SDK metadata extracted, updating session:", sdkMetadata);
|
|
4352
|
+
try {
|
|
4353
|
+
api.sessionSyncClient(response).updateMetadata((currentMetadata) => ({
|
|
4354
|
+
...currentMetadata,
|
|
4355
|
+
tools: sdkMetadata.tools,
|
|
4356
|
+
slashCommands: sdkMetadata.slashCommands
|
|
4357
|
+
}));
|
|
4358
|
+
logger.debug("[start] Session metadata updated with SDK capabilities");
|
|
4359
|
+
} catch (error) {
|
|
4360
|
+
logger.debug("[start] Failed to update session metadata:", error);
|
|
4361
|
+
}
|
|
4362
|
+
});
|
|
4363
|
+
const session = api.sessionSyncClient(response);
|
|
4364
|
+
const logPath = await logger.logFilePathPromise;
|
|
4365
|
+
logger.infoDeveloper(`Session: ${response.id}`);
|
|
4366
|
+
logger.infoDeveloper(`Logs: ${logPath}`);
|
|
4367
|
+
session.updateAgentState((currentState) => ({
|
|
4368
|
+
...currentState,
|
|
4369
|
+
controlledByUser: options.startingMode !== "remote"
|
|
4370
|
+
}));
|
|
4371
|
+
const caffeinateStarted = startCaffeinate();
|
|
4372
|
+
if (caffeinateStarted) {
|
|
4373
|
+
logger.infoDeveloper("Sleep prevention enabled (macOS)");
|
|
4374
|
+
}
|
|
4375
|
+
const messageQueue = new MessageQueue2((mode) => hashObject(mode));
|
|
4376
|
+
registerHandlers(session);
|
|
4377
|
+
let currentPermissionMode = options.permissionMode;
|
|
4378
|
+
let currentModel = options.model;
|
|
4379
|
+
let currentFallbackModel = void 0;
|
|
4380
|
+
let currentCustomSystemPrompt = void 0;
|
|
4381
|
+
let currentAppendSystemPrompt = void 0;
|
|
4382
|
+
let currentAllowedTools = void 0;
|
|
4383
|
+
let currentDisallowedTools = void 0;
|
|
4384
|
+
session.onUserMessage((message) => {
|
|
4385
|
+
let messagePermissionMode = currentPermissionMode;
|
|
4386
|
+
if (message.meta?.permissionMode) {
|
|
4387
|
+
const validModes = ["default", "acceptEdits", "bypassPermissions", "plan"];
|
|
4388
|
+
if (validModes.includes(message.meta.permissionMode)) {
|
|
4389
|
+
messagePermissionMode = message.meta.permissionMode;
|
|
4390
|
+
currentPermissionMode = messagePermissionMode;
|
|
4391
|
+
logger.debug(`[loop] Permission mode updated from user message to: ${currentPermissionMode}`);
|
|
4392
|
+
} else {
|
|
4393
|
+
logger.debug(`[loop] Invalid permission mode received: ${message.meta.permissionMode}`);
|
|
4394
|
+
}
|
|
4395
|
+
} else {
|
|
4396
|
+
logger.debug(`[loop] User message received with no permission mode override, using current: ${currentPermissionMode}`);
|
|
4397
|
+
}
|
|
4398
|
+
let messageModel = currentModel;
|
|
4399
|
+
if (message.meta?.hasOwnProperty("model")) {
|
|
4400
|
+
messageModel = message.meta.model || void 0;
|
|
4401
|
+
currentModel = messageModel;
|
|
4402
|
+
logger.debug(`[loop] Model updated from user message: ${messageModel || "reset to default"}`);
|
|
4403
|
+
} else {
|
|
4404
|
+
logger.debug(`[loop] User message received with no model override, using current: ${currentModel || "default"}`);
|
|
4405
|
+
}
|
|
4406
|
+
let messageCustomSystemPrompt = currentCustomSystemPrompt;
|
|
4407
|
+
if (message.meta?.hasOwnProperty("customSystemPrompt")) {
|
|
4408
|
+
messageCustomSystemPrompt = message.meta.customSystemPrompt || void 0;
|
|
4409
|
+
currentCustomSystemPrompt = messageCustomSystemPrompt;
|
|
4410
|
+
logger.debug(`[loop] Custom system prompt updated from user message: ${messageCustomSystemPrompt ? "set" : "reset to none"}`);
|
|
4411
|
+
} else {
|
|
4412
|
+
logger.debug(`[loop] User message received with no custom system prompt override, using current: ${currentCustomSystemPrompt ? "set" : "none"}`);
|
|
4413
|
+
}
|
|
4414
|
+
let messageFallbackModel = currentFallbackModel;
|
|
4415
|
+
if (message.meta?.hasOwnProperty("fallbackModel")) {
|
|
4416
|
+
messageFallbackModel = message.meta.fallbackModel || void 0;
|
|
4417
|
+
currentFallbackModel = messageFallbackModel;
|
|
4418
|
+
logger.debug(`[loop] Fallback model updated from user message: ${messageFallbackModel || "reset to none"}`);
|
|
4419
|
+
} else {
|
|
4420
|
+
logger.debug(`[loop] User message received with no fallback model override, using current: ${currentFallbackModel || "none"}`);
|
|
4421
|
+
}
|
|
4422
|
+
let messageAppendSystemPrompt = currentAppendSystemPrompt;
|
|
4423
|
+
if (message.meta?.hasOwnProperty("appendSystemPrompt")) {
|
|
4424
|
+
messageAppendSystemPrompt = message.meta.appendSystemPrompt || void 0;
|
|
4425
|
+
currentAppendSystemPrompt = messageAppendSystemPrompt;
|
|
4426
|
+
logger.debug(`[loop] Append system prompt updated from user message: ${messageAppendSystemPrompt ? "set" : "reset to none"}`);
|
|
4427
|
+
} else {
|
|
4428
|
+
logger.debug(`[loop] User message received with no append system prompt override, using current: ${currentAppendSystemPrompt ? "set" : "none"}`);
|
|
4429
|
+
}
|
|
4430
|
+
let messageAllowedTools = currentAllowedTools;
|
|
4431
|
+
if (message.meta?.hasOwnProperty("allowedTools")) {
|
|
4432
|
+
messageAllowedTools = message.meta.allowedTools || void 0;
|
|
4433
|
+
currentAllowedTools = messageAllowedTools;
|
|
4434
|
+
logger.debug(`[loop] Allowed tools updated from user message: ${messageAllowedTools ? messageAllowedTools.join(", ") : "reset to none"}`);
|
|
4435
|
+
} else {
|
|
4436
|
+
logger.debug(`[loop] User message received with no allowed tools override, using current: ${currentAllowedTools ? currentAllowedTools.join(", ") : "none"}`);
|
|
4437
|
+
}
|
|
4438
|
+
let messageDisallowedTools = currentDisallowedTools;
|
|
4439
|
+
if (message.meta?.hasOwnProperty("disallowedTools")) {
|
|
4440
|
+
messageDisallowedTools = message.meta.disallowedTools || void 0;
|
|
4441
|
+
currentDisallowedTools = messageDisallowedTools;
|
|
4442
|
+
logger.debug(`[loop] Disallowed tools updated from user message: ${messageDisallowedTools ? messageDisallowedTools.join(", ") : "reset to none"}`);
|
|
4443
|
+
} else {
|
|
4444
|
+
logger.debug(`[loop] User message received with no disallowed tools override, using current: ${currentDisallowedTools ? currentDisallowedTools.join(", ") : "none"}`);
|
|
4445
|
+
}
|
|
4446
|
+
const specialCommand = parseSpecialCommand(message.content.text);
|
|
4447
|
+
if (specialCommand.type === "compact") {
|
|
4448
|
+
logger.debug("[start] Detected /compact command");
|
|
4449
|
+
const enhancedMode2 = {
|
|
4450
|
+
permissionMode: messagePermissionMode || "default",
|
|
4451
|
+
model: messageModel,
|
|
4452
|
+
fallbackModel: messageFallbackModel,
|
|
4453
|
+
customSystemPrompt: messageCustomSystemPrompt,
|
|
4454
|
+
appendSystemPrompt: messageAppendSystemPrompt,
|
|
4455
|
+
allowedTools: messageAllowedTools,
|
|
4456
|
+
disallowedTools: messageDisallowedTools
|
|
4457
|
+
};
|
|
4458
|
+
messageQueue.pushIsolateAndClear(specialCommand.originalMessage || message.content.text, enhancedMode2);
|
|
4459
|
+
logger.debugLargeJson("[start] /compact command pushed to queue:", message);
|
|
4460
|
+
return;
|
|
4461
|
+
}
|
|
4462
|
+
if (specialCommand.type === "clear") {
|
|
4463
|
+
logger.debug("[start] Detected /clear command");
|
|
4464
|
+
const enhancedMode2 = {
|
|
4465
|
+
permissionMode: messagePermissionMode || "default",
|
|
4466
|
+
model: messageModel,
|
|
4467
|
+
fallbackModel: messageFallbackModel,
|
|
4468
|
+
customSystemPrompt: messageCustomSystemPrompt,
|
|
4469
|
+
appendSystemPrompt: messageAppendSystemPrompt,
|
|
4470
|
+
allowedTools: messageAllowedTools,
|
|
4471
|
+
disallowedTools: messageDisallowedTools
|
|
4472
|
+
};
|
|
4473
|
+
messageQueue.pushIsolateAndClear(specialCommand.originalMessage || message.content.text, enhancedMode2);
|
|
4474
|
+
logger.debugLargeJson("[start] /compact command pushed to queue:", message);
|
|
4475
|
+
return;
|
|
4476
|
+
}
|
|
4477
|
+
const enhancedMode = {
|
|
4478
|
+
permissionMode: messagePermissionMode || "default",
|
|
4479
|
+
model: messageModel,
|
|
4480
|
+
fallbackModel: messageFallbackModel,
|
|
4481
|
+
customSystemPrompt: messageCustomSystemPrompt,
|
|
4482
|
+
appendSystemPrompt: messageAppendSystemPrompt,
|
|
4483
|
+
allowedTools: messageAllowedTools,
|
|
4484
|
+
disallowedTools: messageDisallowedTools
|
|
4485
|
+
};
|
|
4486
|
+
messageQueue.push(message.content.text, enhancedMode);
|
|
4487
|
+
logger.debugLargeJson("User message pushed to queue:", message);
|
|
4488
|
+
});
|
|
4489
|
+
const cleanup = async () => {
|
|
4490
|
+
logger.debug("[START] Received termination signal, cleaning up...");
|
|
4491
|
+
try {
|
|
4492
|
+
if (session) {
|
|
4493
|
+
session.sendSessionDeath();
|
|
4494
|
+
await session.flush();
|
|
4495
|
+
await session.close();
|
|
4496
|
+
}
|
|
4497
|
+
stopCaffeinate();
|
|
4498
|
+
logger.debug("[START] Cleanup complete, exiting");
|
|
4499
|
+
process.exit(0);
|
|
4500
|
+
} catch (error) {
|
|
4501
|
+
logger.debug("[START] Error during cleanup:", error);
|
|
4502
|
+
process.exit(1);
|
|
4503
|
+
}
|
|
4504
|
+
};
|
|
4505
|
+
process.on("SIGTERM", cleanup);
|
|
4506
|
+
process.on("SIGINT", cleanup);
|
|
4507
|
+
process.on("uncaughtException", (error) => {
|
|
4508
|
+
logger.debug("[START] Uncaught exception:", error);
|
|
4509
|
+
cleanup();
|
|
4510
|
+
});
|
|
4511
|
+
process.on("unhandledRejection", (reason) => {
|
|
4512
|
+
logger.debug("[START] Unhandled rejection:", reason);
|
|
4513
|
+
cleanup();
|
|
4514
|
+
});
|
|
4515
|
+
await loop({
|
|
4516
|
+
path: workingDirectory,
|
|
4517
|
+
model: options.model,
|
|
4518
|
+
permissionMode: options.permissionMode,
|
|
4519
|
+
startingMode: options.startingMode,
|
|
4520
|
+
messageQueue,
|
|
4521
|
+
api,
|
|
4522
|
+
onModeChange: (newMode) => {
|
|
4523
|
+
session.sendSessionEvent({ type: "switch", mode: newMode });
|
|
4524
|
+
session.updateAgentState((currentState) => ({
|
|
4525
|
+
...currentState,
|
|
4526
|
+
controlledByUser: newMode === "local"
|
|
4527
|
+
}));
|
|
4528
|
+
},
|
|
4529
|
+
onSessionReady: (sessionInstance) => {
|
|
4530
|
+
},
|
|
4531
|
+
mcpServers: {},
|
|
4532
|
+
session,
|
|
4533
|
+
claudeEnvVars: options.claudeEnvVars,
|
|
4534
|
+
claudeArgs: options.claudeArgs
|
|
4535
|
+
});
|
|
4536
|
+
session.sendSessionDeath();
|
|
4537
|
+
logger.debug("Waiting for socket to flush...");
|
|
4538
|
+
await session.flush();
|
|
4539
|
+
logger.debug("Closing session...");
|
|
4540
|
+
await session.close();
|
|
4541
|
+
stopCaffeinate();
|
|
4542
|
+
logger.debug("Stopped sleep prevention");
|
|
4543
|
+
process.exit(0);
|
|
4544
|
+
}
|
|
4545
|
+
|
|
4505
4546
|
function trimIdent(text) {
|
|
4506
4547
|
const lines = text.split("\n");
|
|
4507
4548
|
while (lines.length > 0 && lines[0].trim() === "") {
|
|
@@ -4559,10 +4600,10 @@ async function install$1() {
|
|
|
4559
4600
|
<true/>
|
|
4560
4601
|
|
|
4561
4602
|
<key>StandardErrorPath</key>
|
|
4562
|
-
<string>${os
|
|
4603
|
+
<string>${os.homedir()}/.happy/daemon.err</string>
|
|
4563
4604
|
|
|
4564
4605
|
<key>StandardOutPath</key>
|
|
4565
|
-
<string>${os
|
|
4606
|
+
<string>${os.homedir()}/.happy/daemon.log</string>
|
|
4566
4607
|
|
|
4567
4608
|
<key>WorkingDirectory</key>
|
|
4568
4609
|
<string>/tmp</string>
|
|
@@ -4733,7 +4774,7 @@ async function handleAuthLogin(args) {
|
|
|
4733
4774
|
if (existingCreds && settings?.machineId) {
|
|
4734
4775
|
console.log(chalk.green("\u2713 Already authenticated"));
|
|
4735
4776
|
console.log(chalk.gray(` Machine ID: ${settings.machineId}`));
|
|
4736
|
-
console.log(chalk.gray(` Host: ${os.hostname()}`));
|
|
4777
|
+
console.log(chalk.gray(` Host: ${os$1.hostname()}`));
|
|
4737
4778
|
console.log(chalk.gray(` Use 'happy auth login --force' to re-authenticate`));
|
|
4738
4779
|
return;
|
|
4739
4780
|
} else if (existingCreds && !settings?.machineId) {
|
|
@@ -4802,7 +4843,7 @@ async function handleAuthShowBackup() {
|
|
|
4802
4843
|
console.log("");
|
|
4803
4844
|
console.log(chalk.cyan("Machine Information:"));
|
|
4804
4845
|
console.log(` Machine ID: ${settings?.machineId || "not set"}`);
|
|
4805
|
-
console.log(` Host: ${os.hostname()}`);
|
|
4846
|
+
console.log(` Host: ${os$1.hostname()}`);
|
|
4806
4847
|
console.log("");
|
|
4807
4848
|
console.log(chalk.bold("How to use this backup key:"));
|
|
4808
4849
|
console.log(chalk.gray("\u2022 In Happy mobile app: Go to restore/link device and enter this key"));
|
|
@@ -4827,7 +4868,7 @@ async function handleAuthStatus() {
|
|
|
4827
4868
|
if (settings?.machineId) {
|
|
4828
4869
|
console.log(chalk.green("\u2713 Machine registered"));
|
|
4829
4870
|
console.log(chalk.gray(` Machine ID: ${settings.machineId}`));
|
|
4830
|
-
console.log(chalk.gray(` Host: ${os.hostname()}`));
|
|
4871
|
+
console.log(chalk.gray(` Host: ${os$1.hostname()}`));
|
|
4831
4872
|
} else {
|
|
4832
4873
|
console.log(chalk.yellow("\u26A0\uFE0F Machine not registered"));
|
|
4833
4874
|
console.log(chalk.gray(' Run "happy auth login --force" to fix this'));
|
|
@@ -4847,6 +4888,34 @@ async function handleAuthStatus() {
|
|
|
4847
4888
|
}
|
|
4848
4889
|
}
|
|
4849
4890
|
|
|
4891
|
+
const DaemonPrompt = ({ onSelect }) => {
|
|
4892
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
4893
|
+
const options = [
|
|
4894
|
+
{ value: true, label: "Yes (recommended)", key: "Y" },
|
|
4895
|
+
{ value: false, label: "No", key: "N" }
|
|
4896
|
+
];
|
|
4897
|
+
useInput((input, key) => {
|
|
4898
|
+
const upperInput = input.toUpperCase();
|
|
4899
|
+
if (key.upArrow || key.leftArrow) {
|
|
4900
|
+
setSelectedIndex(0);
|
|
4901
|
+
} else if (key.downArrow || key.rightArrow) {
|
|
4902
|
+
setSelectedIndex(1);
|
|
4903
|
+
} else if (key.return) {
|
|
4904
|
+
onSelect(options[selectedIndex].value);
|
|
4905
|
+
} else if (upperInput === "Y") {
|
|
4906
|
+
onSelect(true);
|
|
4907
|
+
} else if (upperInput === "N") {
|
|
4908
|
+
onSelect(false);
|
|
4909
|
+
} else if (key.escape || key.ctrl && input === "c") {
|
|
4910
|
+
onSelect(false);
|
|
4911
|
+
}
|
|
4912
|
+
});
|
|
4913
|
+
return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column" }, /* @__PURE__ */ React.createElement(Box, { marginBottom: 1 }, /* @__PURE__ */ React.createElement(Text, { bold: true, color: "cyan" }, "\u{1F680} Happy Daemon Setup")), /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", marginBottom: 1 }, /* @__PURE__ */ React.createElement(Text, null, "\u{1F4F1} Happy can run a background service that allows you to:"), /* @__PURE__ */ React.createElement(Text, { color: "cyan" }, " \u2022 Spawn new conversations from your phone"), /* @__PURE__ */ React.createElement(Text, { color: "cyan" }, " \u2022 Continue closed conversations remotely"), /* @__PURE__ */ React.createElement(Text, { color: "cyan" }, " \u2022 Work with Claude while your computer has internet")), /* @__PURE__ */ React.createElement(Box, { marginBottom: 1 }, /* @__PURE__ */ React.createElement(Text, null, "Would you like Happy to start this service automatically?")), /* @__PURE__ */ React.createElement(Box, { flexDirection: "column" }, options.map((option, index) => {
|
|
4914
|
+
const isSelected = selectedIndex === index;
|
|
4915
|
+
return /* @__PURE__ */ React.createElement(Box, { key: option.key }, /* @__PURE__ */ React.createElement(Text, { color: isSelected ? "green" : "gray" }, isSelected ? "\u203A " : " ", "[", option.key, "] ", option.label));
|
|
4916
|
+
})), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "Press Y/N or use arrows + Enter to select")));
|
|
4917
|
+
};
|
|
4918
|
+
|
|
4850
4919
|
(async () => {
|
|
4851
4920
|
const args = process.argv.slice(2);
|
|
4852
4921
|
logger.debug("Starting happy CLI with args: ", process.argv);
|
|
@@ -4923,8 +4992,7 @@ async function handleAuthStatus() {
|
|
|
4923
4992
|
}
|
|
4924
4993
|
return;
|
|
4925
4994
|
} else if (daemonSubcommand === "start") {
|
|
4926
|
-
const
|
|
4927
|
-
const child = spawn$1(happyBinPath, ["daemon", "start-sync"], {
|
|
4995
|
+
const child = spawnHappyCLI(["daemon", "start-sync"], {
|
|
4928
4996
|
detached: true,
|
|
4929
4997
|
stdio: "ignore",
|
|
4930
4998
|
env: process.env
|
|
@@ -5112,38 +5180,38 @@ ${chalk.bold.cyan("Claude Code Options (from `claude --help`):")}
|
|
|
5112
5180
|
const result = await authAndSetupMachineIfNeeded();
|
|
5113
5181
|
credentials = result.credentials;
|
|
5114
5182
|
}
|
|
5183
|
+
const isExperimentalEnabled = ["true", "1", "yes"].includes(process.env.HAPPY_EXPERIMENTAL?.toLowerCase() || "");
|
|
5115
5184
|
let settings = await readSettings();
|
|
5116
|
-
if (settings && settings.daemonAutoStartWhenRunningHappy === void 0) {
|
|
5117
|
-
|
|
5118
|
-
|
|
5119
|
-
|
|
5120
|
-
|
|
5121
|
-
|
|
5122
|
-
|
|
5123
|
-
|
|
5124
|
-
|
|
5125
|
-
|
|
5126
|
-
|
|
5127
|
-
|
|
5185
|
+
if (isExperimentalEnabled && settings && settings.daemonAutoStartWhenRunningHappy === void 0) {
|
|
5186
|
+
const shouldAutoStart = await new Promise((resolve) => {
|
|
5187
|
+
let hasResolved = false;
|
|
5188
|
+
const onSelect = (autoStart) => {
|
|
5189
|
+
if (!hasResolved) {
|
|
5190
|
+
hasResolved = true;
|
|
5191
|
+
app.unmount();
|
|
5192
|
+
resolve(autoStart);
|
|
5193
|
+
}
|
|
5194
|
+
};
|
|
5195
|
+
const app = render(React.createElement(DaemonPrompt, { onSelect }), {
|
|
5196
|
+
exitOnCtrlC: false,
|
|
5197
|
+
patchConsole: false
|
|
5198
|
+
});
|
|
5128
5199
|
});
|
|
5129
|
-
rl.close();
|
|
5130
|
-
const shouldAutoStart = answer.toLowerCase() !== "n";
|
|
5131
5200
|
settings = await updateSettings((settings2) => ({
|
|
5132
5201
|
...settings2,
|
|
5133
5202
|
daemonAutoStartWhenRunningHappy: shouldAutoStart
|
|
5134
5203
|
}));
|
|
5135
5204
|
if (shouldAutoStart) {
|
|
5136
|
-
console.log(chalk.green("\u2713 Happy will start the background service automatically"));
|
|
5205
|
+
console.log(chalk.green("\n\u2713 Happy will start the background service automatically"));
|
|
5137
5206
|
console.log(chalk.gray(" The service will run whenever you use the happy command"));
|
|
5138
5207
|
} else {
|
|
5139
|
-
console.log(chalk.yellow(" You can enable this later by running: happy daemon install"));
|
|
5208
|
+
console.log(chalk.yellow("\n You can enable this later by running: happy daemon install"));
|
|
5140
5209
|
}
|
|
5141
5210
|
}
|
|
5142
|
-
if (settings && settings.daemonAutoStartWhenRunningHappy) {
|
|
5211
|
+
if (isExperimentalEnabled && settings && settings.daemonAutoStartWhenRunningHappy) {
|
|
5143
5212
|
logger.debug("Starting Happy background service...");
|
|
5144
5213
|
if (!await isDaemonRunning()) {
|
|
5145
|
-
const
|
|
5146
|
-
const daemonProcess = spawn$1(happyBinPath, ["daemon", "start-sync"], {
|
|
5214
|
+
const daemonProcess = spawnHappyCLI(["daemon", "start-sync"], {
|
|
5147
5215
|
detached: true,
|
|
5148
5216
|
stdio: "ignore",
|
|
5149
5217
|
env: process.env
|