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