happy-coder 0.9.1 → 0.10.0-0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +766 -257
- package/dist/index.mjs +769 -260
- package/dist/lib.cjs +1 -1
- package/dist/lib.d.cts +23 -15
- package/dist/lib.d.mts +23 -15
- package/dist/lib.mjs +1 -1
- package/dist/{types-BS8Pr3Im.mjs → types-CGbH1LGX.mjs} +59 -25
- package/dist/{types-DNUk09Np.cjs → types-fU2E-jQl.cjs} +59 -25
- package/package.json +5 -1
package/dist/index.mjs
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import os$1, { homedir } from 'node:os';
|
|
3
3
|
import { randomUUID, randomBytes } from 'node:crypto';
|
|
4
|
-
import { l as logger, b as backoff, d as delay, R as RawJSONLinesSchema, e as AsyncLock, r as readDaemonState,
|
|
5
|
-
import { spawn, execSync } from 'node:child_process';
|
|
6
|
-
import { resolve, join
|
|
4
|
+
import { l as logger, b as backoff, d as delay, R as RawJSONLinesSchema, e as AsyncLock, r as readDaemonState, f as clearDaemonState, p as packageJson, c as configuration, g as readSettings, h as readCredentials, i as encodeBase64, u as updateSettings, j as encodeBase64Url, k as decodeBase64, w as writeCredentials, m as acquireDaemonLock, n as writeDaemonState, A as ApiClient, o as releaseDaemonLock, q as clearCredentials, s as clearMachineId, t as getLatestDaemonLog } from './types-CGbH1LGX.mjs';
|
|
5
|
+
import { spawn, execSync, execFileSync } from 'node:child_process';
|
|
6
|
+
import { resolve, join } from 'node:path';
|
|
7
7
|
import { createInterface } from 'node:readline';
|
|
8
|
-
import { fileURLToPath as fileURLToPath$1 } from 'node:url';
|
|
9
8
|
import { existsSync, readFileSync, mkdirSync, watch, readdirSync, statSync, rmSync } from 'node:fs';
|
|
10
9
|
import { dirname, resolve as resolve$1, join as join$1 } from 'path';
|
|
11
10
|
import { fileURLToPath } from 'url';
|
|
@@ -13,6 +12,7 @@ import { readFile } from 'node:fs/promises';
|
|
|
13
12
|
import fs, { watch as watch$1, access, readFile as readFile$1, stat, writeFile, readdir } from 'fs/promises';
|
|
14
13
|
import { useStdout, useInput, Box, Text, render } from 'ink';
|
|
15
14
|
import React, { useState, useRef, useEffect, useCallback } from 'react';
|
|
15
|
+
import { fileURLToPath as fileURLToPath$1 } from 'node:url';
|
|
16
16
|
import axios from 'axios';
|
|
17
17
|
import 'node:events';
|
|
18
18
|
import 'socket.io-client';
|
|
@@ -20,17 +20,20 @@ import tweetnacl from 'tweetnacl';
|
|
|
20
20
|
import 'expo-server-sdk';
|
|
21
21
|
import { spawn as spawn$1, exec, execSync as execSync$1 } from 'child_process';
|
|
22
22
|
import { promisify } from 'util';
|
|
23
|
-
import { createHash } from 'crypto';
|
|
23
|
+
import { createHash, randomBytes as randomBytes$1 } from 'crypto';
|
|
24
|
+
import { readFileSync as readFileSync$1, existsSync as existsSync$1, writeFileSync, chmodSync, unlinkSync } from 'fs';
|
|
25
|
+
import psList from 'ps-list';
|
|
26
|
+
import spawn$2 from 'cross-spawn';
|
|
24
27
|
import os from 'os';
|
|
25
28
|
import qrcode from 'qrcode-terminal';
|
|
26
29
|
import open from 'open';
|
|
27
30
|
import fastify from 'fastify';
|
|
28
31
|
import { z } from 'zod';
|
|
29
32
|
import { validatorCompiler, serializerCompiler } from 'fastify-type-provider-zod';
|
|
30
|
-
import { readFileSync as readFileSync$1, existsSync as existsSync$1, writeFileSync, chmodSync, unlinkSync } from 'fs';
|
|
31
33
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
32
34
|
import { createServer } from 'node:http';
|
|
33
35
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
36
|
+
import { createServer as createServer$1 } from 'http';
|
|
34
37
|
|
|
35
38
|
class Session {
|
|
36
39
|
path;
|
|
@@ -181,7 +184,7 @@ const systemPrompt = trimIdent(`
|
|
|
181
184
|
Co-Authored-By: Happy <yesreply@happy.engineering>
|
|
182
185
|
`);
|
|
183
186
|
|
|
184
|
-
|
|
187
|
+
const claudeCliPath = resolve(join(projectPath(), "scripts", "claude_local_launcher.cjs"));
|
|
185
188
|
async function claudeLocal(opts) {
|
|
186
189
|
const projectDir = getProjectPath(opts.path);
|
|
187
190
|
mkdirSync(projectDir, { recursive: true });
|
|
@@ -238,7 +241,6 @@ async function claudeLocal(opts) {
|
|
|
238
241
|
if (opts.claudeArgs) {
|
|
239
242
|
args.push(...opts.claudeArgs);
|
|
240
243
|
}
|
|
241
|
-
const claudeCliPath = resolve(join(projectPath(), "scripts", "claude_local_launcher.cjs"));
|
|
242
244
|
if (!claudeCliPath || !existsSync(claudeCliPath)) {
|
|
243
245
|
throw new Error("Claude local launcher not found. Please ensure HAPPY_PROJECT_ROOT is set correctly for development.");
|
|
244
246
|
}
|
|
@@ -2835,7 +2837,7 @@ function run(args, options) {
|
|
|
2835
2837
|
});
|
|
2836
2838
|
}
|
|
2837
2839
|
|
|
2838
|
-
const execAsync = promisify(exec);
|
|
2840
|
+
const execAsync$1 = promisify(exec);
|
|
2839
2841
|
function registerHandlers(session) {
|
|
2840
2842
|
session.setHandler("bash", async (data) => {
|
|
2841
2843
|
logger.debug("Shell command request:", data.command);
|
|
@@ -2845,7 +2847,7 @@ function registerHandlers(session) {
|
|
|
2845
2847
|
timeout: data.timeout || 3e4
|
|
2846
2848
|
// Default 30 seconds timeout
|
|
2847
2849
|
};
|
|
2848
|
-
const { stdout, stderr } = await execAsync(data.command, options);
|
|
2850
|
+
const { stdout, stderr } = await execAsync$1(data.command, options);
|
|
2849
2851
|
return {
|
|
2850
2852
|
success: true,
|
|
2851
2853
|
stdout: stdout ? stdout.toString() : "",
|
|
@@ -3044,25 +3046,15 @@ function registerHandlers(session) {
|
|
|
3044
3046
|
};
|
|
3045
3047
|
}
|
|
3046
3048
|
});
|
|
3049
|
+
}
|
|
3050
|
+
function registerKillSessionHandler(session, killThisHappy) {
|
|
3047
3051
|
session.setHandler("killSession", async () => {
|
|
3048
3052
|
logger.debug("Kill session request received");
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
setTimeout(() => {
|
|
3055
|
-
logger.debug("[KILL SESSION] Exiting process as requested");
|
|
3056
|
-
process.exit(0);
|
|
3057
|
-
}, 100);
|
|
3058
|
-
return response;
|
|
3059
|
-
} catch (error) {
|
|
3060
|
-
logger.debug("Failed to kill session:", error);
|
|
3061
|
-
return {
|
|
3062
|
-
success: false,
|
|
3063
|
-
message: error instanceof Error ? error.message : "Failed to kill session"
|
|
3064
|
-
};
|
|
3065
|
-
}
|
|
3053
|
+
void killThisHappy();
|
|
3054
|
+
return {
|
|
3055
|
+
success: true,
|
|
3056
|
+
message: "Killing happy-cli process"
|
|
3057
|
+
};
|
|
3066
3058
|
});
|
|
3067
3059
|
}
|
|
3068
3060
|
|
|
@@ -3599,7 +3591,7 @@ async function checkIfDaemonRunningAndCleanupStaleState() {
|
|
|
3599
3591
|
return false;
|
|
3600
3592
|
}
|
|
3601
3593
|
}
|
|
3602
|
-
async function
|
|
3594
|
+
async function isDaemonRunningCurrentlyInstalledHappyVersion() {
|
|
3603
3595
|
logger.debug("[DAEMON CONTROL] Checking if daemon is running same version");
|
|
3604
3596
|
const runningDaemon = await checkIfDaemonRunningAndCleanupStaleState();
|
|
3605
3597
|
if (!runningDaemon) {
|
|
@@ -3612,8 +3604,11 @@ async function isDaemonRunningSameVersion() {
|
|
|
3612
3604
|
return false;
|
|
3613
3605
|
}
|
|
3614
3606
|
try {
|
|
3615
|
-
|
|
3616
|
-
|
|
3607
|
+
const packageJsonPath = join$1(projectPath(), "package.json");
|
|
3608
|
+
const packageJson = JSON.parse(readFileSync$1(packageJsonPath, "utf-8"));
|
|
3609
|
+
const currentCliVersion = packageJson.version;
|
|
3610
|
+
logger.debug(`[DAEMON CONTROL] Current CLI version: ${currentCliVersion}, Daemon started with version: ${state.startedWithCliVersion}`);
|
|
3611
|
+
return currentCliVersion === state.startedWithCliVersion;
|
|
3617
3612
|
} catch (error) {
|
|
3618
3613
|
logger.debug("[DAEMON CONTROL] Error checking daemon version", error);
|
|
3619
3614
|
return false;
|
|
@@ -3666,137 +3661,73 @@ async function waitForProcessDeath(pid, timeout) {
|
|
|
3666
3661
|
throw new Error("Process did not die within timeout");
|
|
3667
3662
|
}
|
|
3668
3663
|
|
|
3669
|
-
function findAllHappyProcesses() {
|
|
3664
|
+
async function findAllHappyProcesses() {
|
|
3670
3665
|
try {
|
|
3666
|
+
const processes = await psList();
|
|
3671
3667
|
const allProcesses = [];
|
|
3672
|
-
|
|
3673
|
-
const
|
|
3674
|
-
const
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
type = "user-session";
|
|
3693
|
-
}
|
|
3694
|
-
allProcesses.push({ pid, command, type });
|
|
3695
|
-
}
|
|
3696
|
-
} catch {
|
|
3697
|
-
}
|
|
3698
|
-
try {
|
|
3699
|
-
const devOutput = execSync('ps aux | grep -E "tsx.*src/index\\.ts" | grep -v grep', { encoding: "utf8" });
|
|
3700
|
-
const devLines = devOutput.trim().split("\n").filter((line) => line.trim());
|
|
3701
|
-
for (const line of devLines) {
|
|
3702
|
-
const parts = line.trim().split(/\s+/);
|
|
3703
|
-
if (parts.length < 11) continue;
|
|
3704
|
-
const pid = parseInt(parts[1]);
|
|
3705
|
-
const command = parts.slice(10).join(" ");
|
|
3706
|
-
if (!command.includes("happy-cli/node_modules/tsx") && !command.includes("/bin/tsx src/index.ts")) {
|
|
3707
|
-
continue;
|
|
3708
|
-
}
|
|
3709
|
-
let type = "unknown";
|
|
3710
|
-
if (pid === process.pid) {
|
|
3711
|
-
type = "current";
|
|
3712
|
-
} else if (command.includes("--version")) {
|
|
3713
|
-
type = "dev-daemon-version-check";
|
|
3714
|
-
} else if (command.includes("daemon start-sync") || command.includes("daemon start")) {
|
|
3715
|
-
type = "dev-daemon";
|
|
3716
|
-
} else if (command.includes("--started-by daemon")) {
|
|
3717
|
-
type = "dev-daemon-spawned";
|
|
3718
|
-
} else if (command.includes("doctor")) {
|
|
3719
|
-
type = "dev-doctor";
|
|
3720
|
-
} else if (command.includes("--yolo")) {
|
|
3721
|
-
type = "dev-session";
|
|
3722
|
-
} else {
|
|
3723
|
-
type = "dev-related";
|
|
3724
|
-
}
|
|
3725
|
-
allProcesses.push({ pid, command, type });
|
|
3668
|
+
for (const proc of processes) {
|
|
3669
|
+
const cmd = proc.cmd || "";
|
|
3670
|
+
const name = proc.name || "";
|
|
3671
|
+
const isHappy = name.includes("happy") || name === "node" && (cmd.includes("happy-cli") || cmd.includes("dist/index.mjs")) || cmd.includes("happy.mjs") || cmd.includes("happy-coder") || cmd.includes("tsx") && cmd.includes("src/index.ts") && cmd.includes("happy-cli");
|
|
3672
|
+
if (!isHappy) continue;
|
|
3673
|
+
let type = "unknown";
|
|
3674
|
+
if (proc.pid === process.pid) {
|
|
3675
|
+
type = "current";
|
|
3676
|
+
} else if (cmd.includes("--version")) {
|
|
3677
|
+
type = cmd.includes("tsx") ? "dev-daemon-version-check" : "daemon-version-check";
|
|
3678
|
+
} else if (cmd.includes("daemon start-sync") || cmd.includes("daemon start")) {
|
|
3679
|
+
type = cmd.includes("tsx") ? "dev-daemon" : "daemon";
|
|
3680
|
+
} else if (cmd.includes("--started-by daemon")) {
|
|
3681
|
+
type = cmd.includes("tsx") ? "dev-daemon-spawned" : "daemon-spawned-session";
|
|
3682
|
+
} else if (cmd.includes("doctor")) {
|
|
3683
|
+
type = cmd.includes("tsx") ? "dev-doctor" : "doctor";
|
|
3684
|
+
} else if (cmd.includes("--yolo")) {
|
|
3685
|
+
type = "dev-session";
|
|
3686
|
+
} else {
|
|
3687
|
+
type = cmd.includes("tsx") ? "dev-related" : "user-session";
|
|
3726
3688
|
}
|
|
3727
|
-
|
|
3689
|
+
allProcesses.push({ pid: proc.pid, command: cmd || name, type });
|
|
3728
3690
|
}
|
|
3729
3691
|
return allProcesses;
|
|
3730
3692
|
} catch (error) {
|
|
3731
3693
|
return [];
|
|
3732
3694
|
}
|
|
3733
3695
|
}
|
|
3734
|
-
function findRunawayHappyProcesses() {
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
const lines = output.trim().split("\n").filter((line) => line.trim());
|
|
3740
|
-
for (const line of lines) {
|
|
3741
|
-
const parts = line.trim().split(/\s+/);
|
|
3742
|
-
if (parts.length < 11) continue;
|
|
3743
|
-
const pid = parseInt(parts[1]);
|
|
3744
|
-
const command = parts.slice(10).join(" ");
|
|
3745
|
-
if (pid === process.pid) continue;
|
|
3746
|
-
if (command.includes("--started-by daemon") || command.includes("daemon start-sync") || command.includes("daemon start") || command.includes("--version")) {
|
|
3747
|
-
processes.push({ pid, command });
|
|
3748
|
-
}
|
|
3749
|
-
}
|
|
3750
|
-
} catch {
|
|
3751
|
-
}
|
|
3752
|
-
try {
|
|
3753
|
-
const devOutput = execSync('ps aux | grep -E "tsx.*src/index\\.ts" | grep -v grep', { encoding: "utf8" });
|
|
3754
|
-
const devLines = devOutput.trim().split("\n").filter((line) => line.trim());
|
|
3755
|
-
for (const line of devLines) {
|
|
3756
|
-
const parts = line.trim().split(/\s+/);
|
|
3757
|
-
if (parts.length < 11) continue;
|
|
3758
|
-
const pid = parseInt(parts[1]);
|
|
3759
|
-
const command = parts.slice(10).join(" ");
|
|
3760
|
-
if (pid === process.pid) continue;
|
|
3761
|
-
if (!command.includes("happy-cli/node_modules/tsx") && !command.includes("/bin/tsx src/index.ts")) {
|
|
3762
|
-
continue;
|
|
3763
|
-
}
|
|
3764
|
-
if (command.includes("--started-by daemon") || command.includes("daemon start-sync") || command.includes("daemon start") || command.includes("--version")) {
|
|
3765
|
-
processes.push({ pid, command });
|
|
3766
|
-
}
|
|
3767
|
-
}
|
|
3768
|
-
} catch {
|
|
3769
|
-
}
|
|
3770
|
-
return processes;
|
|
3771
|
-
} catch (error) {
|
|
3772
|
-
return [];
|
|
3773
|
-
}
|
|
3696
|
+
async function findRunawayHappyProcesses() {
|
|
3697
|
+
const allProcesses = await findAllHappyProcesses();
|
|
3698
|
+
return allProcesses.filter(
|
|
3699
|
+
(p) => p.pid !== process.pid && (p.type === "daemon" || p.type === "dev-daemon" || p.type === "daemon-spawned-session" || p.type === "dev-daemon-spawned" || p.type === "daemon-version-check" || p.type === "dev-daemon-version-check")
|
|
3700
|
+
).map((p) => ({ pid: p.pid, command: p.command }));
|
|
3774
3701
|
}
|
|
3775
3702
|
async function killRunawayHappyProcesses() {
|
|
3776
|
-
const runawayProcesses = findRunawayHappyProcesses();
|
|
3703
|
+
const runawayProcesses = await findRunawayHappyProcesses();
|
|
3777
3704
|
const errors = [];
|
|
3778
|
-
|
|
3705
|
+
let killed = 0;
|
|
3706
|
+
for (const { pid, command } of runawayProcesses) {
|
|
3779
3707
|
try {
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
process.kill(pid, "
|
|
3787
|
-
|
|
3708
|
+
console.log(`Killing runaway process PID ${pid}: ${command}`);
|
|
3709
|
+
if (process.platform === "win32") {
|
|
3710
|
+
const result = spawn$2.sync("taskkill", ["/F", "/PID", pid.toString()], { stdio: "pipe" });
|
|
3711
|
+
if (result.error) throw result.error;
|
|
3712
|
+
if (result.status !== 0) throw new Error(`taskkill exited with code ${result.status}`);
|
|
3713
|
+
} else {
|
|
3714
|
+
process.kill(pid, "SIGTERM");
|
|
3715
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
3716
|
+
const processes = await psList();
|
|
3717
|
+
const stillAlive = processes.find((p) => p.pid === pid);
|
|
3718
|
+
if (stillAlive) {
|
|
3719
|
+
console.log(`Process PID ${pid} ignored SIGTERM, using SIGKILL`);
|
|
3720
|
+
process.kill(pid, "SIGKILL");
|
|
3721
|
+
}
|
|
3788
3722
|
}
|
|
3789
3723
|
console.log(`Successfully killed runaway process PID ${pid}`);
|
|
3790
|
-
|
|
3724
|
+
killed++;
|
|
3791
3725
|
} catch (error) {
|
|
3792
3726
|
const errorMessage = error.message;
|
|
3793
3727
|
errors.push({ pid, error: errorMessage });
|
|
3794
3728
|
console.log(`Failed to kill process PID ${pid}: ${errorMessage}`);
|
|
3795
|
-
return { success: false, pid, command };
|
|
3796
3729
|
}
|
|
3797
|
-
}
|
|
3798
|
-
const results = await Promise.all(killPromises);
|
|
3799
|
-
const killed = results.filter((r) => r.success).length;
|
|
3730
|
+
}
|
|
3800
3731
|
return { killed, errors };
|
|
3801
3732
|
}
|
|
3802
3733
|
|
|
@@ -3912,7 +3843,7 @@ async function runDoctorCommand(filter) {
|
|
|
3912
3843
|
console.log(chalk.blue(`Location: ${configuration.daemonStateFile}`));
|
|
3913
3844
|
console.log(chalk.gray(JSON.stringify(state, null, 2)));
|
|
3914
3845
|
}
|
|
3915
|
-
const allProcesses = findAllHappyProcesses();
|
|
3846
|
+
const allProcesses = await findAllHappyProcesses();
|
|
3916
3847
|
if (allProcesses.length > 0) {
|
|
3917
3848
|
console.log(chalk.bold("\n\u{1F50D} All Happy CLI Processes"));
|
|
3918
3849
|
const grouped = allProcesses.reduce((groups, process2) => {
|
|
@@ -4268,31 +4199,54 @@ function startDaemonControlServer({
|
|
|
4268
4199
|
sessionId: z.string(),
|
|
4269
4200
|
metadata: z.any()
|
|
4270
4201
|
// Metadata type from API
|
|
4271
|
-
})
|
|
4202
|
+
}),
|
|
4203
|
+
response: {
|
|
4204
|
+
200: z.object({
|
|
4205
|
+
status: z.literal("ok")
|
|
4206
|
+
})
|
|
4207
|
+
}
|
|
4272
4208
|
}
|
|
4273
|
-
}, async (request
|
|
4209
|
+
}, async (request) => {
|
|
4274
4210
|
const { sessionId, metadata } = request.body;
|
|
4275
4211
|
logger.debug(`[CONTROL SERVER] Session started: ${sessionId}`);
|
|
4276
4212
|
onHappySessionWebhook(sessionId, metadata);
|
|
4277
4213
|
return { status: "ok" };
|
|
4278
4214
|
});
|
|
4279
|
-
typed.post("/list",
|
|
4215
|
+
typed.post("/list", {
|
|
4216
|
+
schema: {
|
|
4217
|
+
response: {
|
|
4218
|
+
200: z.object({
|
|
4219
|
+
children: z.array(z.object({
|
|
4220
|
+
startedBy: z.string(),
|
|
4221
|
+
happySessionId: z.string(),
|
|
4222
|
+
pid: z.number()
|
|
4223
|
+
}))
|
|
4224
|
+
})
|
|
4225
|
+
}
|
|
4226
|
+
}
|
|
4227
|
+
}, async () => {
|
|
4280
4228
|
const children = getChildren();
|
|
4281
4229
|
logger.debug(`[CONTROL SERVER] Listing ${children.length} sessions`);
|
|
4282
4230
|
return {
|
|
4283
|
-
children: children.map((child) => {
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4231
|
+
children: children.filter((child) => child.happySessionId !== void 0).map((child) => ({
|
|
4232
|
+
startedBy: child.startedBy,
|
|
4233
|
+
happySessionId: child.happySessionId,
|
|
4234
|
+
pid: child.pid
|
|
4235
|
+
}))
|
|
4287
4236
|
};
|
|
4288
4237
|
});
|
|
4289
4238
|
typed.post("/stop-session", {
|
|
4290
4239
|
schema: {
|
|
4291
4240
|
body: z.object({
|
|
4292
4241
|
sessionId: z.string()
|
|
4293
|
-
})
|
|
4242
|
+
}),
|
|
4243
|
+
response: {
|
|
4244
|
+
200: z.object({
|
|
4245
|
+
success: z.boolean()
|
|
4246
|
+
})
|
|
4247
|
+
}
|
|
4294
4248
|
}
|
|
4295
|
-
}, async (request
|
|
4249
|
+
}, async (request) => {
|
|
4296
4250
|
const { sessionId } = request.body;
|
|
4297
4251
|
logger.debug(`[CONTROL SERVER] Stop session request: ${sessionId}`);
|
|
4298
4252
|
const success = stopSession(sessionId);
|
|
@@ -4303,28 +4257,68 @@ function startDaemonControlServer({
|
|
|
4303
4257
|
body: z.object({
|
|
4304
4258
|
directory: z.string(),
|
|
4305
4259
|
sessionId: z.string().optional()
|
|
4306
|
-
})
|
|
4260
|
+
}),
|
|
4261
|
+
response: {
|
|
4262
|
+
200: z.object({
|
|
4263
|
+
success: z.boolean(),
|
|
4264
|
+
sessionId: z.string().optional(),
|
|
4265
|
+
approvedNewDirectoryCreation: z.boolean().optional()
|
|
4266
|
+
}),
|
|
4267
|
+
409: z.object({
|
|
4268
|
+
success: z.boolean(),
|
|
4269
|
+
requiresUserApproval: z.boolean().optional(),
|
|
4270
|
+
actionRequired: z.string().optional(),
|
|
4271
|
+
directory: z.string().optional()
|
|
4272
|
+
}),
|
|
4273
|
+
500: z.object({
|
|
4274
|
+
success: z.boolean(),
|
|
4275
|
+
error: z.string().optional()
|
|
4276
|
+
})
|
|
4277
|
+
}
|
|
4307
4278
|
}
|
|
4308
4279
|
}, async (request, reply) => {
|
|
4309
4280
|
const { directory, sessionId } = request.body;
|
|
4310
4281
|
logger.debug(`[CONTROL SERVER] Spawn session request: dir=${directory}, sessionId=${sessionId || "new"}`);
|
|
4311
|
-
const
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
|
|
4282
|
+
const result = await spawnSession({ directory, sessionId });
|
|
4283
|
+
switch (result.type) {
|
|
4284
|
+
case "success":
|
|
4285
|
+
if (!result.sessionId) {
|
|
4286
|
+
reply.code(500);
|
|
4287
|
+
return {
|
|
4288
|
+
success: false,
|
|
4289
|
+
error: "Failed to spawn session: no session ID returned"
|
|
4290
|
+
};
|
|
4291
|
+
}
|
|
4292
|
+
return {
|
|
4293
|
+
success: true,
|
|
4294
|
+
sessionId: result.sessionId,
|
|
4295
|
+
approvedNewDirectoryCreation: true
|
|
4296
|
+
};
|
|
4297
|
+
case "requestToApproveDirectoryCreation":
|
|
4298
|
+
reply.code(409);
|
|
4299
|
+
return {
|
|
4300
|
+
success: false,
|
|
4301
|
+
requiresUserApproval: true,
|
|
4302
|
+
actionRequired: "CREATE_DIRECTORY",
|
|
4303
|
+
directory: result.directory
|
|
4304
|
+
};
|
|
4305
|
+
case "error":
|
|
4306
|
+
reply.code(500);
|
|
4307
|
+
return {
|
|
4308
|
+
success: false,
|
|
4309
|
+
error: result.errorMessage
|
|
4310
|
+
};
|
|
4325
4311
|
}
|
|
4326
4312
|
});
|
|
4327
|
-
typed.post("/stop",
|
|
4313
|
+
typed.post("/stop", {
|
|
4314
|
+
schema: {
|
|
4315
|
+
response: {
|
|
4316
|
+
200: z.object({
|
|
4317
|
+
status: z.string()
|
|
4318
|
+
})
|
|
4319
|
+
}
|
|
4320
|
+
}
|
|
4321
|
+
}, async () => {
|
|
4328
4322
|
logger.debug("[CONTROL SERVER] Stop daemon request received");
|
|
4329
4323
|
setTimeout(() => {
|
|
4330
4324
|
logger.debug("[CONTROL SERVER] Triggering daemon shutdown");
|
|
@@ -4332,21 +4326,6 @@ function startDaemonControlServer({
|
|
|
4332
4326
|
}, 50);
|
|
4333
4327
|
return { status: "stopping" };
|
|
4334
4328
|
});
|
|
4335
|
-
typed.post("/dev-simulate-error", {
|
|
4336
|
-
schema: {
|
|
4337
|
-
body: z.object({
|
|
4338
|
-
error: z.string()
|
|
4339
|
-
})
|
|
4340
|
-
}
|
|
4341
|
-
}, async (request, reply) => {
|
|
4342
|
-
const { error } = request.body;
|
|
4343
|
-
logger.debug(`[CONTROL SERVER] Dev: Simulating error: ${error}`);
|
|
4344
|
-
setTimeout(() => {
|
|
4345
|
-
logger.debug(`[CONTROL SERVER] Dev: Throwing simulated error now`);
|
|
4346
|
-
throw new Error(error);
|
|
4347
|
-
}, 100);
|
|
4348
|
-
return { status: "error will be thrown" };
|
|
4349
|
-
});
|
|
4350
4329
|
app.listen({ port: 0, host: "127.0.0.1" }, (err, address) => {
|
|
4351
4330
|
if (err) {
|
|
4352
4331
|
logger.debug("[CONTROL SERVER] Failed to start:", err);
|
|
@@ -4414,7 +4393,7 @@ async function startDaemon() {
|
|
|
4414
4393
|
});
|
|
4415
4394
|
logger.debug("[DAEMON RUN] Starting daemon process...");
|
|
4416
4395
|
logger.debugLargeJson("[DAEMON RUN] Environment", getEnvironmentInfo());
|
|
4417
|
-
const runningDaemonVersionMatches = await
|
|
4396
|
+
const runningDaemonVersionMatches = await isDaemonRunningCurrentlyInstalledHappyVersion();
|
|
4418
4397
|
if (!runningDaemonVersionMatches) {
|
|
4419
4398
|
logger.debug("[DAEMON RUN] Daemon version mismatch detected, restarting daemon with current CLI version");
|
|
4420
4399
|
await stopDaemon();
|
|
@@ -4469,16 +4448,22 @@ async function startDaemon() {
|
|
|
4469
4448
|
logger.debug(`[DAEMON RUN] Registered externally-started session ${sessionId}`);
|
|
4470
4449
|
}
|
|
4471
4450
|
};
|
|
4472
|
-
const spawnSession = async (
|
|
4451
|
+
const spawnSession = async (options) => {
|
|
4452
|
+
logger.debugLargeJson("[DAEMON RUN] Spawning session", options);
|
|
4453
|
+
const { directory, sessionId, machineId: machineId2, approvedNewDirectoryCreation = true } = options;
|
|
4473
4454
|
let directoryCreated = false;
|
|
4474
|
-
if (directory.startsWith("~")) {
|
|
4475
|
-
directory = resolve$1(os.homedir(), directory.replace("~", ""));
|
|
4476
|
-
}
|
|
4477
4455
|
try {
|
|
4478
4456
|
await fs.access(directory);
|
|
4479
4457
|
logger.debug(`[DAEMON RUN] Directory exists: ${directory}`);
|
|
4480
4458
|
} catch (error) {
|
|
4481
4459
|
logger.debug(`[DAEMON RUN] Directory doesn't exist, creating: ${directory}`);
|
|
4460
|
+
if (!approvedNewDirectoryCreation) {
|
|
4461
|
+
logger.debug(`[DAEMON RUN] Directory creation not approved for: ${directory}`);
|
|
4462
|
+
return {
|
|
4463
|
+
type: "requestToApproveDirectoryCreation",
|
|
4464
|
+
directory
|
|
4465
|
+
};
|
|
4466
|
+
}
|
|
4482
4467
|
try {
|
|
4483
4468
|
await fs.mkdir(directory, { recursive: true });
|
|
4484
4469
|
logger.debug(`[DAEMON RUN] Successfully created directory: ${directory}`);
|
|
@@ -4497,7 +4482,10 @@ async function startDaemon() {
|
|
|
4497
4482
|
errorMessage += `System error: ${mkdirError.message || mkdirError}. Please verify the path is valid and you have the necessary permissions.`;
|
|
4498
4483
|
}
|
|
4499
4484
|
logger.debug(`[DAEMON RUN] Directory creation failed: ${errorMessage}`);
|
|
4500
|
-
return
|
|
4485
|
+
return {
|
|
4486
|
+
type: "error",
|
|
4487
|
+
errorMessage
|
|
4488
|
+
};
|
|
4501
4489
|
}
|
|
4502
4490
|
}
|
|
4503
4491
|
try {
|
|
@@ -4525,7 +4513,10 @@ async function startDaemon() {
|
|
|
4525
4513
|
}
|
|
4526
4514
|
if (!happyProcess.pid) {
|
|
4527
4515
|
logger.debug("[DAEMON RUN] Failed to spawn process - no PID returned");
|
|
4528
|
-
return
|
|
4516
|
+
return {
|
|
4517
|
+
type: "error",
|
|
4518
|
+
errorMessage: "Failed to spawn Happy process - no PID returned"
|
|
4519
|
+
};
|
|
4529
4520
|
}
|
|
4530
4521
|
logger.debug(`[DAEMON RUN] Spawned process with PID ${happyProcess.pid}`);
|
|
4531
4522
|
const trackedSession = {
|
|
@@ -4553,17 +4544,27 @@ async function startDaemon() {
|
|
|
4553
4544
|
const timeout = setTimeout(() => {
|
|
4554
4545
|
pidToAwaiter.delete(happyProcess.pid);
|
|
4555
4546
|
logger.debug(`[DAEMON RUN] Session webhook timeout for PID ${happyProcess.pid}`);
|
|
4556
|
-
resolve2(
|
|
4547
|
+
resolve2({
|
|
4548
|
+
type: "error",
|
|
4549
|
+
errorMessage: `Session webhook timeout for PID ${happyProcess.pid}`
|
|
4550
|
+
});
|
|
4557
4551
|
}, 1e4);
|
|
4558
4552
|
pidToAwaiter.set(happyProcess.pid, (completedSession) => {
|
|
4559
4553
|
clearTimeout(timeout);
|
|
4560
4554
|
logger.debug(`[DAEMON RUN] Session ${completedSession.happySessionId} fully spawned with webhook`);
|
|
4561
|
-
resolve2(
|
|
4555
|
+
resolve2({
|
|
4556
|
+
type: "success",
|
|
4557
|
+
sessionId: completedSession.happySessionId
|
|
4558
|
+
});
|
|
4562
4559
|
});
|
|
4563
4560
|
});
|
|
4564
4561
|
} catch (error) {
|
|
4562
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4565
4563
|
logger.debug("[DAEMON RUN] Failed to spawn session:", error);
|
|
4566
|
-
return
|
|
4564
|
+
return {
|
|
4565
|
+
type: "error",
|
|
4566
|
+
errorMessage: `Failed to spawn session: ${errorMessage}`
|
|
4567
|
+
};
|
|
4567
4568
|
}
|
|
4568
4569
|
};
|
|
4569
4570
|
const stopSession = (sessionId) => {
|
|
@@ -5035,6 +5036,7 @@ async function start(credentials, options = {}) {
|
|
|
5035
5036
|
logger.debug("[START] Unhandled rejection:", reason);
|
|
5036
5037
|
cleanup();
|
|
5037
5038
|
});
|
|
5039
|
+
registerKillSessionHandler(session, cleanup);
|
|
5038
5040
|
await loop({
|
|
5039
5041
|
path: workingDirectory,
|
|
5040
5042
|
model: options.model,
|
|
@@ -5050,7 +5052,7 @@ async function start(credentials, options = {}) {
|
|
|
5050
5052
|
controlledByUser: newMode === "local"
|
|
5051
5053
|
}));
|
|
5052
5054
|
},
|
|
5053
|
-
onSessionReady: (
|
|
5055
|
+
onSessionReady: (_sessionInstance) => {
|
|
5054
5056
|
},
|
|
5055
5057
|
mcpServers: {
|
|
5056
5058
|
"happy": {
|
|
@@ -5392,33 +5394,561 @@ async function handleAuthStatus() {
|
|
|
5392
5394
|
}
|
|
5393
5395
|
}
|
|
5394
5396
|
|
|
5395
|
-
const
|
|
5396
|
-
|
|
5397
|
-
|
|
5398
|
-
|
|
5399
|
-
|
|
5400
|
-
|
|
5401
|
-
|
|
5402
|
-
|
|
5403
|
-
|
|
5404
|
-
|
|
5405
|
-
|
|
5406
|
-
|
|
5407
|
-
|
|
5408
|
-
|
|
5409
|
-
|
|
5410
|
-
|
|
5411
|
-
|
|
5412
|
-
|
|
5413
|
-
|
|
5414
|
-
|
|
5397
|
+
const CLIENT_ID$2 = "app_EMoamEEZ73f0CkXaXp7hrann";
|
|
5398
|
+
const AUTH_BASE_URL = "https://auth.openai.com";
|
|
5399
|
+
const DEFAULT_PORT$2 = 1455;
|
|
5400
|
+
function generatePKCE$2() {
|
|
5401
|
+
const verifier = randomBytes$1(32).toString("base64url").replace(/[^a-zA-Z0-9\-._~]/g, "");
|
|
5402
|
+
const challenge = createHash("sha256").update(verifier).digest("base64url").replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
|
|
5403
|
+
return { verifier, challenge };
|
|
5404
|
+
}
|
|
5405
|
+
function generateState$2() {
|
|
5406
|
+
return randomBytes$1(16).toString("hex");
|
|
5407
|
+
}
|
|
5408
|
+
function parseJWT(token) {
|
|
5409
|
+
const parts = token.split(".");
|
|
5410
|
+
if (parts.length !== 3) {
|
|
5411
|
+
throw new Error("Invalid JWT format");
|
|
5412
|
+
}
|
|
5413
|
+
const payload = Buffer.from(parts[1], "base64url").toString();
|
|
5414
|
+
return JSON.parse(payload);
|
|
5415
|
+
}
|
|
5416
|
+
async function findAvailablePort$2() {
|
|
5417
|
+
return new Promise((resolve) => {
|
|
5418
|
+
const server = createServer$1();
|
|
5419
|
+
server.listen(0, "127.0.0.1", () => {
|
|
5420
|
+
const port = server.address().port;
|
|
5421
|
+
server.close(() => resolve(port));
|
|
5422
|
+
});
|
|
5423
|
+
});
|
|
5424
|
+
}
|
|
5425
|
+
async function isPortAvailable$2(port) {
|
|
5426
|
+
return new Promise((resolve) => {
|
|
5427
|
+
const testServer = createServer$1();
|
|
5428
|
+
testServer.once("error", () => {
|
|
5429
|
+
testServer.close();
|
|
5430
|
+
resolve(false);
|
|
5431
|
+
});
|
|
5432
|
+
testServer.listen(port, "127.0.0.1", () => {
|
|
5433
|
+
testServer.close(() => resolve(true));
|
|
5434
|
+
});
|
|
5435
|
+
});
|
|
5436
|
+
}
|
|
5437
|
+
async function exchangeCodeForTokens$2(code, verifier, port) {
|
|
5438
|
+
const response = await fetch(`${AUTH_BASE_URL}/oauth/token`, {
|
|
5439
|
+
method: "POST",
|
|
5440
|
+
headers: {
|
|
5441
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
5442
|
+
},
|
|
5443
|
+
body: new URLSearchParams({
|
|
5444
|
+
grant_type: "authorization_code",
|
|
5445
|
+
client_id: CLIENT_ID$2,
|
|
5446
|
+
code,
|
|
5447
|
+
code_verifier: verifier,
|
|
5448
|
+
redirect_uri: `http://localhost:${port}/auth/callback`
|
|
5449
|
+
})
|
|
5450
|
+
});
|
|
5451
|
+
if (!response.ok) {
|
|
5452
|
+
const error = await response.text();
|
|
5453
|
+
throw new Error(`Token exchange failed: ${error}`);
|
|
5454
|
+
}
|
|
5455
|
+
const data = await response.json();
|
|
5456
|
+
const idTokenPayload = parseJWT(data.id_token);
|
|
5457
|
+
let accountId = idTokenPayload.chatgpt_account_id;
|
|
5458
|
+
if (!accountId) {
|
|
5459
|
+
const authClaim = idTokenPayload["https://api.openai.com/auth"];
|
|
5460
|
+
if (authClaim && typeof authClaim === "object") {
|
|
5461
|
+
accountId = authClaim.chatgpt_account_id || authClaim.account_id;
|
|
5415
5462
|
}
|
|
5463
|
+
}
|
|
5464
|
+
return {
|
|
5465
|
+
id_token: data.id_token,
|
|
5466
|
+
access_token: data.access_token || data.id_token,
|
|
5467
|
+
refresh_token: data.refresh_token,
|
|
5468
|
+
account_id: accountId
|
|
5469
|
+
};
|
|
5470
|
+
}
|
|
5471
|
+
async function startCallbackServer$2(state, verifier, port) {
|
|
5472
|
+
return new Promise((resolve, reject) => {
|
|
5473
|
+
const server = createServer$1(async (req, res) => {
|
|
5474
|
+
const url = new URL(req.url, `http://localhost:${port}`);
|
|
5475
|
+
if (url.pathname === "/auth/callback") {
|
|
5476
|
+
const code = url.searchParams.get("code");
|
|
5477
|
+
const receivedState = url.searchParams.get("state");
|
|
5478
|
+
if (receivedState !== state) {
|
|
5479
|
+
res.writeHead(400);
|
|
5480
|
+
res.end("Invalid state parameter");
|
|
5481
|
+
server.close();
|
|
5482
|
+
reject(new Error("Invalid state parameter"));
|
|
5483
|
+
return;
|
|
5484
|
+
}
|
|
5485
|
+
if (!code) {
|
|
5486
|
+
res.writeHead(400);
|
|
5487
|
+
res.end("No authorization code received");
|
|
5488
|
+
server.close();
|
|
5489
|
+
reject(new Error("No authorization code received"));
|
|
5490
|
+
return;
|
|
5491
|
+
}
|
|
5492
|
+
try {
|
|
5493
|
+
const tokens = await exchangeCodeForTokens$2(code, verifier, port);
|
|
5494
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
5495
|
+
res.end(`
|
|
5496
|
+
<html>
|
|
5497
|
+
<body style="font-family: sans-serif; padding: 20px;">
|
|
5498
|
+
<h2>\u2705 Authentication Successful!</h2>
|
|
5499
|
+
<p>You can close this window and return to your terminal.</p>
|
|
5500
|
+
<script>setTimeout(() => window.close(), 3000);<\/script>
|
|
5501
|
+
</body>
|
|
5502
|
+
</html>
|
|
5503
|
+
`);
|
|
5504
|
+
server.close();
|
|
5505
|
+
resolve(tokens);
|
|
5506
|
+
} catch (error) {
|
|
5507
|
+
res.writeHead(500);
|
|
5508
|
+
res.end("Token exchange failed");
|
|
5509
|
+
server.close();
|
|
5510
|
+
reject(error);
|
|
5511
|
+
}
|
|
5512
|
+
}
|
|
5513
|
+
});
|
|
5514
|
+
server.listen(port, "127.0.0.1", () => {
|
|
5515
|
+
});
|
|
5516
|
+
setTimeout(() => {
|
|
5517
|
+
server.close();
|
|
5518
|
+
reject(new Error("Authentication timeout"));
|
|
5519
|
+
}, 5 * 60 * 1e3);
|
|
5416
5520
|
});
|
|
5417
|
-
|
|
5418
|
-
|
|
5419
|
-
|
|
5420
|
-
|
|
5421
|
-
|
|
5521
|
+
}
|
|
5522
|
+
async function authenticateCodex() {
|
|
5523
|
+
const { verifier, challenge } = generatePKCE$2();
|
|
5524
|
+
const state = generateState$2();
|
|
5525
|
+
let port = DEFAULT_PORT$2;
|
|
5526
|
+
const portAvailable = await isPortAvailable$2(port);
|
|
5527
|
+
if (!portAvailable) {
|
|
5528
|
+
port = await findAvailablePort$2();
|
|
5529
|
+
}
|
|
5530
|
+
const serverPromise = startCallbackServer$2(state, verifier, port);
|
|
5531
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
5532
|
+
const redirect_uri = `http://localhost:${port}/auth/callback`;
|
|
5533
|
+
const params = [
|
|
5534
|
+
["response_type", "code"],
|
|
5535
|
+
["client_id", CLIENT_ID$2],
|
|
5536
|
+
["redirect_uri", redirect_uri],
|
|
5537
|
+
["scope", "openid profile email offline_access"],
|
|
5538
|
+
["code_challenge", challenge],
|
|
5539
|
+
["code_challenge_method", "S256"],
|
|
5540
|
+
["id_token_add_organizations", "true"],
|
|
5541
|
+
["codex_cli_simplified_flow", "true"],
|
|
5542
|
+
["state", state]
|
|
5543
|
+
];
|
|
5544
|
+
const queryString = params.map(([key, value]) => `${key}=${encodeURIComponent(value)}`).join("&");
|
|
5545
|
+
const authUrl = `${AUTH_BASE_URL}/oauth/authorize?${queryString}`;
|
|
5546
|
+
console.log("\u{1F4CB} Opening browser for authentication...");
|
|
5547
|
+
console.log(`If browser doesn't open, visit:
|
|
5548
|
+
${authUrl}
|
|
5549
|
+
`);
|
|
5550
|
+
await openBrowser(authUrl);
|
|
5551
|
+
const tokens = await serverPromise;
|
|
5552
|
+
console.log("\u{1F389} Authentication successful!");
|
|
5553
|
+
return tokens;
|
|
5554
|
+
}
|
|
5555
|
+
|
|
5556
|
+
const CLIENT_ID$1 = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
|
|
5557
|
+
const CLAUDE_AI_AUTHORIZE_URL = "https://claude.ai/oauth/authorize";
|
|
5558
|
+
const TOKEN_URL$1 = "https://console.anthropic.com/v1/oauth/token";
|
|
5559
|
+
const DEFAULT_PORT$1 = 54545;
|
|
5560
|
+
const SCOPE = "user:inference";
|
|
5561
|
+
function generatePKCE$1() {
|
|
5562
|
+
const verifier = randomBytes$1(32).toString("base64url").replace(/[^a-zA-Z0-9\-._~]/g, "");
|
|
5563
|
+
const challenge = createHash("sha256").update(verifier).digest("base64url").replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
|
|
5564
|
+
return { verifier, challenge };
|
|
5565
|
+
}
|
|
5566
|
+
function generateState$1() {
|
|
5567
|
+
return randomBytes$1(32).toString("base64url");
|
|
5568
|
+
}
|
|
5569
|
+
async function findAvailablePort$1() {
|
|
5570
|
+
return new Promise((resolve) => {
|
|
5571
|
+
const server = createServer$1();
|
|
5572
|
+
server.listen(0, "127.0.0.1", () => {
|
|
5573
|
+
const port = server.address().port;
|
|
5574
|
+
server.close(() => resolve(port));
|
|
5575
|
+
});
|
|
5576
|
+
});
|
|
5577
|
+
}
|
|
5578
|
+
async function isPortAvailable$1(port) {
|
|
5579
|
+
return new Promise((resolve) => {
|
|
5580
|
+
const testServer = createServer$1();
|
|
5581
|
+
testServer.once("error", () => {
|
|
5582
|
+
testServer.close();
|
|
5583
|
+
resolve(false);
|
|
5584
|
+
});
|
|
5585
|
+
testServer.listen(port, "127.0.0.1", () => {
|
|
5586
|
+
testServer.close(() => resolve(true));
|
|
5587
|
+
});
|
|
5588
|
+
});
|
|
5589
|
+
}
|
|
5590
|
+
async function exchangeCodeForTokens$1(code, verifier, port, state) {
|
|
5591
|
+
const tokenResponse = await fetch(TOKEN_URL$1, {
|
|
5592
|
+
method: "POST",
|
|
5593
|
+
headers: {
|
|
5594
|
+
"Content-Type": "application/json"
|
|
5595
|
+
},
|
|
5596
|
+
body: JSON.stringify({
|
|
5597
|
+
grant_type: "authorization_code",
|
|
5598
|
+
code,
|
|
5599
|
+
redirect_uri: `http://localhost:${port}/callback`,
|
|
5600
|
+
client_id: CLIENT_ID$1,
|
|
5601
|
+
code_verifier: verifier,
|
|
5602
|
+
state
|
|
5603
|
+
})
|
|
5604
|
+
});
|
|
5605
|
+
if (!tokenResponse.ok) {
|
|
5606
|
+
throw new Error(`Token exchange failed: ${tokenResponse.statusText}`);
|
|
5607
|
+
}
|
|
5608
|
+
const tokenData = await tokenResponse.json();
|
|
5609
|
+
return {
|
|
5610
|
+
raw: tokenData,
|
|
5611
|
+
token: tokenData.access_token,
|
|
5612
|
+
expires: Date.now() + tokenData.expires_in * 1e3
|
|
5613
|
+
};
|
|
5614
|
+
}
|
|
5615
|
+
async function startCallbackServer$1(state, verifier, port) {
|
|
5616
|
+
return new Promise((resolve, reject) => {
|
|
5617
|
+
const server = createServer$1(async (req, res) => {
|
|
5618
|
+
const url = new URL(req.url, `http://localhost:${port}`);
|
|
5619
|
+
if (url.pathname === "/callback") {
|
|
5620
|
+
const code = url.searchParams.get("code");
|
|
5621
|
+
const receivedState = url.searchParams.get("state");
|
|
5622
|
+
if (receivedState !== state) {
|
|
5623
|
+
res.writeHead(400);
|
|
5624
|
+
res.end("Invalid state parameter");
|
|
5625
|
+
server.close();
|
|
5626
|
+
reject(new Error("Invalid state parameter"));
|
|
5627
|
+
return;
|
|
5628
|
+
}
|
|
5629
|
+
if (!code) {
|
|
5630
|
+
res.writeHead(400);
|
|
5631
|
+
res.end("No authorization code received");
|
|
5632
|
+
server.close();
|
|
5633
|
+
reject(new Error("No authorization code received"));
|
|
5634
|
+
return;
|
|
5635
|
+
}
|
|
5636
|
+
try {
|
|
5637
|
+
const tokens = await exchangeCodeForTokens$1(code, verifier, port, state);
|
|
5638
|
+
res.writeHead(302, {
|
|
5639
|
+
"Location": "https://console.anthropic.com/oauth/code/success?app=claude-code"
|
|
5640
|
+
});
|
|
5641
|
+
res.end();
|
|
5642
|
+
server.close();
|
|
5643
|
+
resolve(tokens);
|
|
5644
|
+
} catch (error) {
|
|
5645
|
+
res.writeHead(500);
|
|
5646
|
+
res.end("Token exchange failed");
|
|
5647
|
+
server.close();
|
|
5648
|
+
reject(error);
|
|
5649
|
+
}
|
|
5650
|
+
}
|
|
5651
|
+
});
|
|
5652
|
+
server.listen(port, "127.0.0.1", () => {
|
|
5653
|
+
});
|
|
5654
|
+
setTimeout(() => {
|
|
5655
|
+
server.close();
|
|
5656
|
+
reject(new Error("Authentication timeout"));
|
|
5657
|
+
}, 5 * 60 * 1e3);
|
|
5658
|
+
});
|
|
5659
|
+
}
|
|
5660
|
+
async function authenticateClaude() {
|
|
5661
|
+
console.log("\u{1F680} Starting Anthropic Claude authentication...");
|
|
5662
|
+
const { verifier, challenge } = generatePKCE$1();
|
|
5663
|
+
const state = generateState$1();
|
|
5664
|
+
let port = DEFAULT_PORT$1;
|
|
5665
|
+
const portAvailable = await isPortAvailable$1(port);
|
|
5666
|
+
if (!portAvailable) {
|
|
5667
|
+
console.log(`Port ${port} is in use, finding an available port...`);
|
|
5668
|
+
port = await findAvailablePort$1();
|
|
5669
|
+
}
|
|
5670
|
+
console.log(`\u{1F4E1} Using callback port: ${port}`);
|
|
5671
|
+
const serverPromise = startCallbackServer$1(state, verifier, port);
|
|
5672
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
5673
|
+
const redirect_uri = `http://localhost:${port}/callback`;
|
|
5674
|
+
const params = new URLSearchParams({
|
|
5675
|
+
code: "true",
|
|
5676
|
+
// This tells Claude.ai to show the code AND redirect
|
|
5677
|
+
client_id: CLIENT_ID$1,
|
|
5678
|
+
response_type: "code",
|
|
5679
|
+
redirect_uri,
|
|
5680
|
+
scope: SCOPE,
|
|
5681
|
+
code_challenge: challenge,
|
|
5682
|
+
code_challenge_method: "S256",
|
|
5683
|
+
state
|
|
5684
|
+
});
|
|
5685
|
+
const authUrl = `${CLAUDE_AI_AUTHORIZE_URL}?${params}`;
|
|
5686
|
+
console.log("\u{1F4CB} Opening browser for authentication...");
|
|
5687
|
+
console.log("If browser doesn't open, visit this URL:");
|
|
5688
|
+
console.log();
|
|
5689
|
+
console.log(`${authUrl}`);
|
|
5690
|
+
console.log();
|
|
5691
|
+
await openBrowser(authUrl);
|
|
5692
|
+
try {
|
|
5693
|
+
const tokens = await serverPromise;
|
|
5694
|
+
console.log("\u{1F389} Authentication successful!");
|
|
5695
|
+
console.log("\u2705 OAuth tokens received");
|
|
5696
|
+
return tokens;
|
|
5697
|
+
} catch (error) {
|
|
5698
|
+
console.error("\n\u274C Failed to authenticate with Anthropic");
|
|
5699
|
+
throw error;
|
|
5700
|
+
}
|
|
5701
|
+
}
|
|
5702
|
+
|
|
5703
|
+
const execAsync = promisify(exec);
|
|
5704
|
+
const CLIENT_ID = "681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com";
|
|
5705
|
+
const CLIENT_SECRET = "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";
|
|
5706
|
+
const AUTHORIZE_URL = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
5707
|
+
const TOKEN_URL = "https://oauth2.googleapis.com/token";
|
|
5708
|
+
const DEFAULT_PORT = 54545;
|
|
5709
|
+
const SCOPES = [
|
|
5710
|
+
"https://www.googleapis.com/auth/cloud-platform",
|
|
5711
|
+
"https://www.googleapis.com/auth/userinfo.email",
|
|
5712
|
+
"https://www.googleapis.com/auth/userinfo.profile"
|
|
5713
|
+
].join(" ");
|
|
5714
|
+
function generatePKCE() {
|
|
5715
|
+
const verifier = randomBytes$1(32).toString("base64url").replace(/[^a-zA-Z0-9\-._~]/g, "");
|
|
5716
|
+
const challenge = createHash("sha256").update(verifier).digest("base64url").replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
|
|
5717
|
+
return { verifier, challenge };
|
|
5718
|
+
}
|
|
5719
|
+
function generateState() {
|
|
5720
|
+
return randomBytes$1(32).toString("hex");
|
|
5721
|
+
}
|
|
5722
|
+
async function findAvailablePort() {
|
|
5723
|
+
return new Promise((resolve) => {
|
|
5724
|
+
const server = createServer$1();
|
|
5725
|
+
server.listen(0, "127.0.0.1", () => {
|
|
5726
|
+
const port = server.address().port;
|
|
5727
|
+
server.close(() => resolve(port));
|
|
5728
|
+
});
|
|
5729
|
+
});
|
|
5730
|
+
}
|
|
5731
|
+
async function isPortAvailable(port) {
|
|
5732
|
+
return new Promise((resolve) => {
|
|
5733
|
+
const testServer = createServer$1();
|
|
5734
|
+
testServer.once("error", () => {
|
|
5735
|
+
testServer.close();
|
|
5736
|
+
resolve(false);
|
|
5737
|
+
});
|
|
5738
|
+
testServer.listen(port, "127.0.0.1", () => {
|
|
5739
|
+
testServer.close(() => resolve(true));
|
|
5740
|
+
});
|
|
5741
|
+
});
|
|
5742
|
+
}
|
|
5743
|
+
async function exchangeCodeForTokens(code, verifier, port) {
|
|
5744
|
+
const response = await fetch(TOKEN_URL, {
|
|
5745
|
+
method: "POST",
|
|
5746
|
+
headers: {
|
|
5747
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
5748
|
+
},
|
|
5749
|
+
body: new URLSearchParams({
|
|
5750
|
+
grant_type: "authorization_code",
|
|
5751
|
+
client_id: CLIENT_ID,
|
|
5752
|
+
client_secret: CLIENT_SECRET,
|
|
5753
|
+
code,
|
|
5754
|
+
code_verifier: verifier,
|
|
5755
|
+
redirect_uri: `http://localhost:${port}/oauth2callback`
|
|
5756
|
+
})
|
|
5757
|
+
});
|
|
5758
|
+
if (!response.ok) {
|
|
5759
|
+
const error = await response.text();
|
|
5760
|
+
throw new Error(`Token exchange failed: ${error}`);
|
|
5761
|
+
}
|
|
5762
|
+
const data = await response.json();
|
|
5763
|
+
return data;
|
|
5764
|
+
}
|
|
5765
|
+
async function startCallbackServer(state, verifier, port) {
|
|
5766
|
+
return new Promise((resolve, reject) => {
|
|
5767
|
+
const server = createServer$1(async (req, res) => {
|
|
5768
|
+
const url = new URL(req.url, `http://localhost:${port}`);
|
|
5769
|
+
if (url.pathname === "/oauth2callback") {
|
|
5770
|
+
const code = url.searchParams.get("code");
|
|
5771
|
+
const receivedState = url.searchParams.get("state");
|
|
5772
|
+
const error = url.searchParams.get("error");
|
|
5773
|
+
if (error) {
|
|
5774
|
+
res.writeHead(302, {
|
|
5775
|
+
"Location": "https://developers.google.com/gemini-code-assist/auth_failure_gemini"
|
|
5776
|
+
});
|
|
5777
|
+
res.end();
|
|
5778
|
+
server.close();
|
|
5779
|
+
reject(new Error(`Authentication error: ${error}`));
|
|
5780
|
+
return;
|
|
5781
|
+
}
|
|
5782
|
+
if (receivedState !== state) {
|
|
5783
|
+
res.writeHead(400);
|
|
5784
|
+
res.end("State mismatch. Possible CSRF attack");
|
|
5785
|
+
server.close();
|
|
5786
|
+
reject(new Error("Invalid state parameter"));
|
|
5787
|
+
return;
|
|
5788
|
+
}
|
|
5789
|
+
if (!code) {
|
|
5790
|
+
res.writeHead(400);
|
|
5791
|
+
res.end("No authorization code received");
|
|
5792
|
+
server.close();
|
|
5793
|
+
reject(new Error("No authorization code received"));
|
|
5794
|
+
return;
|
|
5795
|
+
}
|
|
5796
|
+
try {
|
|
5797
|
+
const tokens = await exchangeCodeForTokens(code, verifier, port);
|
|
5798
|
+
res.writeHead(302, {
|
|
5799
|
+
"Location": "https://developers.google.com/gemini-code-assist/auth_success_gemini"
|
|
5800
|
+
});
|
|
5801
|
+
res.end();
|
|
5802
|
+
server.close();
|
|
5803
|
+
resolve(tokens);
|
|
5804
|
+
} catch (error2) {
|
|
5805
|
+
res.writeHead(500);
|
|
5806
|
+
res.end("Token exchange failed");
|
|
5807
|
+
server.close();
|
|
5808
|
+
reject(error2);
|
|
5809
|
+
}
|
|
5810
|
+
}
|
|
5811
|
+
});
|
|
5812
|
+
server.listen(port, "127.0.0.1", () => {
|
|
5813
|
+
});
|
|
5814
|
+
setTimeout(() => {
|
|
5815
|
+
server.close();
|
|
5816
|
+
reject(new Error("Authentication timeout"));
|
|
5817
|
+
}, 5 * 60 * 1e3);
|
|
5818
|
+
});
|
|
5819
|
+
}
|
|
5820
|
+
async function authenticateGemini() {
|
|
5821
|
+
console.log("\u{1F680} Starting Google Gemini authentication...");
|
|
5822
|
+
const { verifier, challenge } = generatePKCE();
|
|
5823
|
+
const state = generateState();
|
|
5824
|
+
let port = DEFAULT_PORT;
|
|
5825
|
+
const portAvailable = await isPortAvailable(port);
|
|
5826
|
+
if (!portAvailable) {
|
|
5827
|
+
console.log(`Port ${port} is in use, finding an available port...`);
|
|
5828
|
+
port = await findAvailablePort();
|
|
5829
|
+
}
|
|
5830
|
+
console.log(`\u{1F4E1} Using callback port: ${port}`);
|
|
5831
|
+
const serverPromise = startCallbackServer(state, verifier, port);
|
|
5832
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
5833
|
+
const redirect_uri = `http://localhost:${port}/oauth2callback`;
|
|
5834
|
+
const params = new URLSearchParams({
|
|
5835
|
+
client_id: CLIENT_ID,
|
|
5836
|
+
response_type: "code",
|
|
5837
|
+
redirect_uri,
|
|
5838
|
+
scope: SCOPES,
|
|
5839
|
+
access_type: "offline",
|
|
5840
|
+
// To get refresh token
|
|
5841
|
+
code_challenge: challenge,
|
|
5842
|
+
code_challenge_method: "S256",
|
|
5843
|
+
state,
|
|
5844
|
+
prompt: "consent"
|
|
5845
|
+
// Force consent to get refresh token
|
|
5846
|
+
});
|
|
5847
|
+
const authUrl = `${AUTHORIZE_URL}?${params}`;
|
|
5848
|
+
console.log("\n\u{1F4CB} Opening browser for authentication...");
|
|
5849
|
+
console.log("If browser doesn't open, visit this URL:");
|
|
5850
|
+
console.log(`
|
|
5851
|
+
${authUrl}
|
|
5852
|
+
`);
|
|
5853
|
+
const platform = process.platform;
|
|
5854
|
+
const openCommand = platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open";
|
|
5855
|
+
try {
|
|
5856
|
+
await execAsync(`${openCommand} "${authUrl}"`);
|
|
5857
|
+
} catch {
|
|
5858
|
+
console.log("\u26A0\uFE0F Could not open browser automatically");
|
|
5859
|
+
}
|
|
5860
|
+
try {
|
|
5861
|
+
const tokens = await serverPromise;
|
|
5862
|
+
console.log("\n\u{1F389} Authentication successful!");
|
|
5863
|
+
console.log("\u2705 OAuth tokens received");
|
|
5864
|
+
return tokens;
|
|
5865
|
+
} catch (error) {
|
|
5866
|
+
console.error("\n\u274C Failed to authenticate with Google");
|
|
5867
|
+
throw error;
|
|
5868
|
+
}
|
|
5869
|
+
}
|
|
5870
|
+
|
|
5871
|
+
async function handleConnectCommand(args) {
|
|
5872
|
+
const subcommand = args[0];
|
|
5873
|
+
if (!subcommand || subcommand === "help" || subcommand === "--help" || subcommand === "-h") {
|
|
5874
|
+
showConnectHelp();
|
|
5875
|
+
return;
|
|
5876
|
+
}
|
|
5877
|
+
switch (subcommand.toLowerCase()) {
|
|
5878
|
+
case "codex":
|
|
5879
|
+
await handleConnectVendor("codex", "OpenAI");
|
|
5880
|
+
break;
|
|
5881
|
+
case "claude":
|
|
5882
|
+
await handleConnectVendor("claude", "Anthropic");
|
|
5883
|
+
break;
|
|
5884
|
+
case "gemini":
|
|
5885
|
+
await handleConnectVendor("gemini", "Gemini");
|
|
5886
|
+
break;
|
|
5887
|
+
default:
|
|
5888
|
+
console.error(chalk.red(`Unknown connect target: ${subcommand}`));
|
|
5889
|
+
showConnectHelp();
|
|
5890
|
+
process.exit(1);
|
|
5891
|
+
}
|
|
5892
|
+
}
|
|
5893
|
+
function showConnectHelp() {
|
|
5894
|
+
console.log(`
|
|
5895
|
+
${chalk.bold("happy connect")} - Connect AI vendor API keys to Happy cloud
|
|
5896
|
+
|
|
5897
|
+
${chalk.bold("Usage:")}
|
|
5898
|
+
happy connect codex Store your Codex API key in Happy cloud
|
|
5899
|
+
happy connect anthropic Store your Anthropic API key in Happy cloud
|
|
5900
|
+
happy connect gemini Store your Gemini API key in Happy cloud
|
|
5901
|
+
happy connect help Show this help message
|
|
5902
|
+
|
|
5903
|
+
${chalk.bold("Description:")}
|
|
5904
|
+
The connect command allows you to securely store your AI vendor API keys
|
|
5905
|
+
in Happy cloud. This enables you to use these services through Happy
|
|
5906
|
+
without exposing your API keys locally.
|
|
5907
|
+
|
|
5908
|
+
${chalk.bold("Examples:")}
|
|
5909
|
+
happy connect codex
|
|
5910
|
+
happy connect anthropic
|
|
5911
|
+
happy connect gemini
|
|
5912
|
+
|
|
5913
|
+
${chalk.bold("Notes:")}
|
|
5914
|
+
\u2022 You must be authenticated with Happy first (run 'happy auth login')
|
|
5915
|
+
\u2022 API keys are encrypted and stored securely in Happy cloud
|
|
5916
|
+
\u2022 You can manage your stored keys at app.happy.engineering
|
|
5917
|
+
`);
|
|
5918
|
+
}
|
|
5919
|
+
async function handleConnectVendor(vendor, displayName) {
|
|
5920
|
+
console.log(chalk.bold(`
|
|
5921
|
+
\u{1F50C} Connecting ${displayName} to Happy cloud
|
|
5922
|
+
`));
|
|
5923
|
+
const credentials = await readCredentials();
|
|
5924
|
+
if (!credentials) {
|
|
5925
|
+
console.log(chalk.yellow("\u26A0\uFE0F Not authenticated with Happy"));
|
|
5926
|
+
console.log(chalk.gray(' Please run "happy auth login" first'));
|
|
5927
|
+
process.exit(1);
|
|
5928
|
+
}
|
|
5929
|
+
const api = new ApiClient(credentials.token, credentials.secret);
|
|
5930
|
+
if (vendor === "codex") {
|
|
5931
|
+
console.log("\u{1F680} Registering Codex token with server");
|
|
5932
|
+
const codexAuthTokens = await authenticateCodex();
|
|
5933
|
+
await api.registerVendorToken("openai", { oauth: codexAuthTokens });
|
|
5934
|
+
console.log("\u2705 Codex token registered with server");
|
|
5935
|
+
process.exit(0);
|
|
5936
|
+
} else if (vendor === "claude") {
|
|
5937
|
+
console.log("\u{1F680} Registering Anthropic token with server");
|
|
5938
|
+
const anthropicAuthTokens = await authenticateClaude();
|
|
5939
|
+
await api.registerVendorToken("anthropic", { oauth: anthropicAuthTokens });
|
|
5940
|
+
console.log("\u2705 Anthropic token registered with server");
|
|
5941
|
+
process.exit(0);
|
|
5942
|
+
} else if (vendor === "gemini") {
|
|
5943
|
+
console.log("\u{1F680} Registering Gemini token with server");
|
|
5944
|
+
const geminiAuthTokens = await authenticateGemini();
|
|
5945
|
+
await api.registerVendorToken("gemini", { oauth: geminiAuthTokens });
|
|
5946
|
+
console.log("\u2705 Gemini token registered with server");
|
|
5947
|
+
process.exit(0);
|
|
5948
|
+
} else {
|
|
5949
|
+
throw new Error(`Unsupported vendor: ${vendor}`);
|
|
5950
|
+
}
|
|
5951
|
+
}
|
|
5422
5952
|
|
|
5423
5953
|
(async () => {
|
|
5424
5954
|
const args = process.argv.slice(2);
|
|
@@ -5448,6 +5978,17 @@ const DaemonPrompt = ({ onSelect }) => {
|
|
|
5448
5978
|
process.exit(1);
|
|
5449
5979
|
}
|
|
5450
5980
|
return;
|
|
5981
|
+
} else if (subcommand === "connect") {
|
|
5982
|
+
try {
|
|
5983
|
+
await handleConnectCommand(args.slice(1));
|
|
5984
|
+
} catch (error) {
|
|
5985
|
+
console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
|
|
5986
|
+
if (process.env.DEBUG) {
|
|
5987
|
+
console.error(error);
|
|
5988
|
+
}
|
|
5989
|
+
process.exit(1);
|
|
5990
|
+
}
|
|
5991
|
+
return;
|
|
5451
5992
|
} else if (subcommand === "logout") {
|
|
5452
5993
|
console.log(chalk.yellow('Note: "happy logout" is deprecated. Use "happy auth logout" instead.\n'));
|
|
5453
5994
|
try {
|
|
@@ -5626,9 +6167,8 @@ ${chalk.bold("Happy supports ALL Claude options!")}
|
|
|
5626
6167
|
${chalk.gray("\u2500".repeat(60))}
|
|
5627
6168
|
${chalk.bold.cyan("Claude Code Options (from `claude --help`):")}
|
|
5628
6169
|
`);
|
|
5629
|
-
const { execSync } = await import('child_process');
|
|
5630
6170
|
try {
|
|
5631
|
-
const claudeHelp =
|
|
6171
|
+
const claudeHelp = execFileSync(process.execPath, [claudeCliPath, "--help"], { encoding: "utf8" });
|
|
5632
6172
|
console.log(claudeHelp);
|
|
5633
6173
|
} catch (e) {
|
|
5634
6174
|
console.log(chalk.yellow("Could not retrieve claude help. Make sure claude is installed."));
|
|
@@ -5641,47 +6181,16 @@ ${chalk.bold.cyan("Claude Code Options (from `claude --help`):")}
|
|
|
5641
6181
|
const {
|
|
5642
6182
|
credentials
|
|
5643
6183
|
} = await authAndSetupMachineIfNeeded();
|
|
5644
|
-
|
|
5645
|
-
if (
|
|
5646
|
-
|
|
5647
|
-
|
|
5648
|
-
|
|
5649
|
-
|
|
5650
|
-
|
|
5651
|
-
app.unmount();
|
|
5652
|
-
resolve(autoStart);
|
|
5653
|
-
}
|
|
5654
|
-
};
|
|
5655
|
-
const app = render(React.createElement(DaemonPrompt, { onSelect }), {
|
|
5656
|
-
exitOnCtrlC: false,
|
|
5657
|
-
patchConsole: false
|
|
5658
|
-
});
|
|
6184
|
+
logger.debug("Ensuring Happy background service is running & matches our version...");
|
|
6185
|
+
if (!await isDaemonRunningCurrentlyInstalledHappyVersion()) {
|
|
6186
|
+
logger.debug("Starting Happy background service...");
|
|
6187
|
+
const daemonProcess = spawnHappyCLI(["daemon", "start-sync"], {
|
|
6188
|
+
detached: true,
|
|
6189
|
+
stdio: "ignore",
|
|
6190
|
+
env: process.env
|
|
5659
6191
|
});
|
|
5660
|
-
|
|
5661
|
-
|
|
5662
|
-
daemonAutoStartWhenRunningHappy: shouldAutoStart
|
|
5663
|
-
}));
|
|
5664
|
-
if (shouldAutoStart) {
|
|
5665
|
-
console.log(chalk.green("\n\u2713 Happy will start the background service automatically"));
|
|
5666
|
-
console.log(chalk.gray(" The service will run whenever you use the happy command"));
|
|
5667
|
-
} else {
|
|
5668
|
-
console.log(chalk.yellow("\n You can enable this later by running: happy daemon install"));
|
|
5669
|
-
}
|
|
5670
|
-
}
|
|
5671
|
-
if (settings && settings.daemonAutoStartWhenRunningHappy) {
|
|
5672
|
-
logger.debug("Ensuring Happy background service is running & matches our version...");
|
|
5673
|
-
if (!await isDaemonRunningSameVersion()) {
|
|
5674
|
-
logger.debug("Starting Happy background service...");
|
|
5675
|
-
const daemonProcess = spawnHappyCLI(["daemon", "start-sync"], {
|
|
5676
|
-
detached: true,
|
|
5677
|
-
stdio: "ignore",
|
|
5678
|
-
env: process.env
|
|
5679
|
-
});
|
|
5680
|
-
daemonProcess.unref();
|
|
5681
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
5682
|
-
} else {
|
|
5683
|
-
logger.debug("Happy background service is running & matches our version");
|
|
5684
|
-
}
|
|
6192
|
+
daemonProcess.unref();
|
|
6193
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
5685
6194
|
}
|
|
5686
6195
|
try {
|
|
5687
6196
|
await start(credentials, options);
|