happy-coder 0.9.0 → 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 +851 -275
- package/dist/index.mjs +854 -278
- package/dist/lib.cjs +1 -1
- package/dist/lib.d.cts +64 -15
- package/dist/lib.d.mts +64 -15
- package/dist/lib.mjs +1 -1
- package/dist/{types-Cezp_n6O.mjs → types-CGbH1LGX.mjs} +102 -32
- package/dist/{types-CyOnnZ8M.cjs → types-fU2E-jQl.cjs} +102 -32
- package/package.json +6 -2
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 {
|
|
@@ -43,6 +46,7 @@ class Session {
|
|
|
43
46
|
queue;
|
|
44
47
|
claudeEnvVars;
|
|
45
48
|
claudeArgs;
|
|
49
|
+
// Made mutable to allow filtering
|
|
46
50
|
mcpServers;
|
|
47
51
|
allowedTools;
|
|
48
52
|
_onModeChange;
|
|
@@ -77,6 +81,11 @@ class Session {
|
|
|
77
81
|
};
|
|
78
82
|
onSessionFound = (sessionId) => {
|
|
79
83
|
this.sessionId = sessionId;
|
|
84
|
+
this.client.updateMetadata((metadata) => ({
|
|
85
|
+
...metadata,
|
|
86
|
+
claudeSessionId: sessionId
|
|
87
|
+
}));
|
|
88
|
+
types.logger.debug(`[Session] Claude Code session ID ${sessionId} added to metadata`);
|
|
80
89
|
};
|
|
81
90
|
/**
|
|
82
91
|
* Clear the current session ID (used by /clear command)
|
|
@@ -85,6 +94,33 @@ class Session {
|
|
|
85
94
|
this.sessionId = null;
|
|
86
95
|
types.logger.debug("[Session] Session ID cleared");
|
|
87
96
|
};
|
|
97
|
+
/**
|
|
98
|
+
* Consume one-time Claude flags from claudeArgs after Claude spawn
|
|
99
|
+
* Currently handles: --resume (with or without session ID)
|
|
100
|
+
*/
|
|
101
|
+
consumeOneTimeFlags = () => {
|
|
102
|
+
if (!this.claudeArgs) return;
|
|
103
|
+
const filteredArgs = [];
|
|
104
|
+
for (let i = 0; i < this.claudeArgs.length; i++) {
|
|
105
|
+
if (this.claudeArgs[i] === "--resume") {
|
|
106
|
+
if (i + 1 < this.claudeArgs.length) {
|
|
107
|
+
const nextArg = this.claudeArgs[i + 1];
|
|
108
|
+
if (!nextArg.startsWith("-") && nextArg.includes("-")) {
|
|
109
|
+
i++;
|
|
110
|
+
types.logger.debug(`[Session] Consumed --resume flag with session ID: ${nextArg}`);
|
|
111
|
+
} else {
|
|
112
|
+
types.logger.debug("[Session] Consumed --resume flag (no session ID)");
|
|
113
|
+
}
|
|
114
|
+
} else {
|
|
115
|
+
types.logger.debug("[Session] Consumed --resume flag (no session ID)");
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
filteredArgs.push(this.claudeArgs[i]);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
this.claudeArgs = filteredArgs.length > 0 ? filteredArgs : void 0;
|
|
122
|
+
types.logger.debug(`[Session] Consumed one-time flags, remaining args:`, this.claudeArgs);
|
|
123
|
+
};
|
|
88
124
|
}
|
|
89
125
|
|
|
90
126
|
function getProjectPath(workingDirectory) {
|
|
@@ -139,9 +175,19 @@ function trimIdent(text) {
|
|
|
139
175
|
|
|
140
176
|
const systemPrompt = trimIdent(`
|
|
141
177
|
ALWAYS when you start a new chat - you must call a tool "mcp__happy__change_title" to set a chat title. When you think chat title is not relevant anymore - call the tool again to change it. When chat name is too generic and you have a change to make it more specific - call the tool again to change it. This title is needed to easily find the chat in the future. Help human.
|
|
178
|
+
|
|
179
|
+
When making commit messages, instead of just giving co-credit to Claude, also give credit to Happy like so:
|
|
180
|
+
|
|
181
|
+
<main commit message>
|
|
182
|
+
|
|
183
|
+
Generated with [Claude Code](https://claude.ai/code)
|
|
184
|
+
via [Happy](https://happy.engineering)
|
|
185
|
+
|
|
186
|
+
Co-Authored-By: Claude <noreply@anthropic.com>
|
|
187
|
+
Co-Authored-By: Happy <yesreply@happy.engineering>
|
|
142
188
|
`);
|
|
143
189
|
|
|
144
|
-
|
|
190
|
+
const claudeCliPath = node_path.resolve(node_path.join(projectPath(), "scripts", "claude_local_launcher.cjs"));
|
|
145
191
|
async function claudeLocal(opts) {
|
|
146
192
|
const projectDir = getProjectPath(opts.path);
|
|
147
193
|
node_fs.mkdirSync(projectDir, { recursive: true });
|
|
@@ -198,7 +244,6 @@ async function claudeLocal(opts) {
|
|
|
198
244
|
if (opts.claudeArgs) {
|
|
199
245
|
args.push(...opts.claudeArgs);
|
|
200
246
|
}
|
|
201
|
-
const claudeCliPath = node_path.resolve(node_path.join(projectPath(), "scripts", "claude_local_launcher.cjs"));
|
|
202
247
|
if (!claudeCliPath || !node_fs.existsSync(claudeCliPath)) {
|
|
203
248
|
throw new Error("Claude local launcher not found. Please ensure HAPPY_PROJECT_ROOT is set correctly for development.");
|
|
204
249
|
}
|
|
@@ -598,6 +643,7 @@ async function claudeLocalLauncher(session) {
|
|
|
598
643
|
mcpServers: session.mcpServers,
|
|
599
644
|
allowedTools: session.allowedTools
|
|
600
645
|
});
|
|
646
|
+
session.consumeOneTimeFlags();
|
|
601
647
|
if (!exitReason) {
|
|
602
648
|
exitReason = "exit";
|
|
603
649
|
break;
|
|
@@ -1401,6 +1447,26 @@ async function claudeRemote(opts) {
|
|
|
1401
1447
|
if (opts.sessionId && !claudeCheckSession(opts.sessionId, opts.path)) {
|
|
1402
1448
|
startFrom = null;
|
|
1403
1449
|
}
|
|
1450
|
+
if (!startFrom && opts.claudeArgs) {
|
|
1451
|
+
for (let i = 0; i < opts.claudeArgs.length; i++) {
|
|
1452
|
+
if (opts.claudeArgs[i] === "--resume") {
|
|
1453
|
+
if (i + 1 < opts.claudeArgs.length) {
|
|
1454
|
+
const nextArg = opts.claudeArgs[i + 1];
|
|
1455
|
+
if (!nextArg.startsWith("-") && nextArg.includes("-")) {
|
|
1456
|
+
startFrom = nextArg;
|
|
1457
|
+
types.logger.debug(`[claudeRemote] Found --resume with session ID: ${startFrom}`);
|
|
1458
|
+
break;
|
|
1459
|
+
} else {
|
|
1460
|
+
types.logger.debug("[claudeRemote] Found --resume without session ID - not supported in remote mode");
|
|
1461
|
+
break;
|
|
1462
|
+
}
|
|
1463
|
+
} else {
|
|
1464
|
+
types.logger.debug("[claudeRemote] Found --resume without session ID - not supported in remote mode");
|
|
1465
|
+
break;
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1404
1470
|
if (opts.claudeEnvVars) {
|
|
1405
1471
|
Object.entries(opts.claudeEnvVars).forEach(([key, value]) => {
|
|
1406
1472
|
process.env[key] = value;
|
|
@@ -2589,7 +2655,7 @@ async function claudeRemoteLauncher(session) {
|
|
|
2589
2655
|
let modeHash = null;
|
|
2590
2656
|
let mode = null;
|
|
2591
2657
|
try {
|
|
2592
|
-
await claudeRemote({
|
|
2658
|
+
const remoteResult = await claudeRemote({
|
|
2593
2659
|
sessionId: session.sessionId,
|
|
2594
2660
|
path: session.path,
|
|
2595
2661
|
allowedTools: session.allowedTools ?? [],
|
|
@@ -2650,6 +2716,7 @@ async function claudeRemoteLauncher(session) {
|
|
|
2650
2716
|
},
|
|
2651
2717
|
signal: abortController.signal
|
|
2652
2718
|
});
|
|
2719
|
+
session.consumeOneTimeFlags();
|
|
2653
2720
|
if (!exitReason && abortController.signal.aborted) {
|
|
2654
2721
|
session.client.sendSessionEvent({ type: "message", message: "Aborted by user" });
|
|
2655
2722
|
}
|
|
@@ -2773,7 +2840,7 @@ function run(args, options) {
|
|
|
2773
2840
|
});
|
|
2774
2841
|
}
|
|
2775
2842
|
|
|
2776
|
-
const execAsync = util.promisify(child_process.exec);
|
|
2843
|
+
const execAsync$1 = util.promisify(child_process.exec);
|
|
2777
2844
|
function registerHandlers(session) {
|
|
2778
2845
|
session.setHandler("bash", async (data) => {
|
|
2779
2846
|
types.logger.debug("Shell command request:", data.command);
|
|
@@ -2783,7 +2850,7 @@ function registerHandlers(session) {
|
|
|
2783
2850
|
timeout: data.timeout || 3e4
|
|
2784
2851
|
// Default 30 seconds timeout
|
|
2785
2852
|
};
|
|
2786
|
-
const { stdout, stderr } = await execAsync(data.command, options);
|
|
2853
|
+
const { stdout, stderr } = await execAsync$1(data.command, options);
|
|
2787
2854
|
return {
|
|
2788
2855
|
success: true,
|
|
2789
2856
|
stdout: stdout ? stdout.toString() : "",
|
|
@@ -2982,25 +3049,15 @@ function registerHandlers(session) {
|
|
|
2982
3049
|
};
|
|
2983
3050
|
}
|
|
2984
3051
|
});
|
|
3052
|
+
}
|
|
3053
|
+
function registerKillSessionHandler(session, killThisHappy) {
|
|
2985
3054
|
session.setHandler("killSession", async () => {
|
|
2986
3055
|
types.logger.debug("Kill session request received");
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
setTimeout(() => {
|
|
2993
|
-
types.logger.debug("[KILL SESSION] Exiting process as requested");
|
|
2994
|
-
process.exit(0);
|
|
2995
|
-
}, 100);
|
|
2996
|
-
return response;
|
|
2997
|
-
} catch (error) {
|
|
2998
|
-
types.logger.debug("Failed to kill session:", error);
|
|
2999
|
-
return {
|
|
3000
|
-
success: false,
|
|
3001
|
-
message: error instanceof Error ? error.message : "Failed to kill session"
|
|
3002
|
-
};
|
|
3003
|
-
}
|
|
3056
|
+
void killThisHappy();
|
|
3057
|
+
return {
|
|
3058
|
+
success: true,
|
|
3059
|
+
message: "Killing happy-cli process"
|
|
3060
|
+
};
|
|
3004
3061
|
});
|
|
3005
3062
|
}
|
|
3006
3063
|
|
|
@@ -3537,7 +3594,7 @@ async function checkIfDaemonRunningAndCleanupStaleState() {
|
|
|
3537
3594
|
return false;
|
|
3538
3595
|
}
|
|
3539
3596
|
}
|
|
3540
|
-
async function
|
|
3597
|
+
async function isDaemonRunningCurrentlyInstalledHappyVersion() {
|
|
3541
3598
|
types.logger.debug("[DAEMON CONTROL] Checking if daemon is running same version");
|
|
3542
3599
|
const runningDaemon = await checkIfDaemonRunningAndCleanupStaleState();
|
|
3543
3600
|
if (!runningDaemon) {
|
|
@@ -3550,8 +3607,11 @@ async function isDaemonRunningSameVersion() {
|
|
|
3550
3607
|
return false;
|
|
3551
3608
|
}
|
|
3552
3609
|
try {
|
|
3553
|
-
|
|
3554
|
-
|
|
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;
|
|
3555
3615
|
} catch (error) {
|
|
3556
3616
|
types.logger.debug("[DAEMON CONTROL] Error checking daemon version", error);
|
|
3557
3617
|
return false;
|
|
@@ -3604,137 +3664,73 @@ async function waitForProcessDeath(pid, timeout) {
|
|
|
3604
3664
|
throw new Error("Process did not die within timeout");
|
|
3605
3665
|
}
|
|
3606
3666
|
|
|
3607
|
-
function findAllHappyProcesses() {
|
|
3667
|
+
async function findAllHappyProcesses() {
|
|
3608
3668
|
try {
|
|
3669
|
+
const processes = await psList();
|
|
3609
3670
|
const allProcesses = [];
|
|
3610
|
-
|
|
3611
|
-
const
|
|
3612
|
-
const
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
type = "user-session";
|
|
3631
|
-
}
|
|
3632
|
-
allProcesses.push({ pid, command, type });
|
|
3633
|
-
}
|
|
3634
|
-
} catch {
|
|
3635
|
-
}
|
|
3636
|
-
try {
|
|
3637
|
-
const devOutput = node_child_process.execSync('ps aux | grep -E "tsx.*src/index\\.ts" | grep -v grep', { encoding: "utf8" });
|
|
3638
|
-
const devLines = devOutput.trim().split("\n").filter((line) => line.trim());
|
|
3639
|
-
for (const line of devLines) {
|
|
3640
|
-
const parts = line.trim().split(/\s+/);
|
|
3641
|
-
if (parts.length < 11) continue;
|
|
3642
|
-
const pid = parseInt(parts[1]);
|
|
3643
|
-
const command = parts.slice(10).join(" ");
|
|
3644
|
-
if (!command.includes("happy-cli/node_modules/tsx") && !command.includes("/bin/tsx src/index.ts")) {
|
|
3645
|
-
continue;
|
|
3646
|
-
}
|
|
3647
|
-
let type = "unknown";
|
|
3648
|
-
if (pid === process.pid) {
|
|
3649
|
-
type = "current";
|
|
3650
|
-
} else if (command.includes("--version")) {
|
|
3651
|
-
type = "dev-daemon-version-check";
|
|
3652
|
-
} else if (command.includes("daemon start-sync") || command.includes("daemon start")) {
|
|
3653
|
-
type = "dev-daemon";
|
|
3654
|
-
} else if (command.includes("--started-by daemon")) {
|
|
3655
|
-
type = "dev-daemon-spawned";
|
|
3656
|
-
} else if (command.includes("doctor")) {
|
|
3657
|
-
type = "dev-doctor";
|
|
3658
|
-
} else if (command.includes("--yolo")) {
|
|
3659
|
-
type = "dev-session";
|
|
3660
|
-
} else {
|
|
3661
|
-
type = "dev-related";
|
|
3662
|
-
}
|
|
3663
|
-
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";
|
|
3664
3691
|
}
|
|
3665
|
-
|
|
3692
|
+
allProcesses.push({ pid: proc.pid, command: cmd || name, type });
|
|
3666
3693
|
}
|
|
3667
3694
|
return allProcesses;
|
|
3668
3695
|
} catch (error) {
|
|
3669
3696
|
return [];
|
|
3670
3697
|
}
|
|
3671
3698
|
}
|
|
3672
|
-
function findRunawayHappyProcesses() {
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
const lines = output.trim().split("\n").filter((line) => line.trim());
|
|
3678
|
-
for (const line of lines) {
|
|
3679
|
-
const parts = line.trim().split(/\s+/);
|
|
3680
|
-
if (parts.length < 11) continue;
|
|
3681
|
-
const pid = parseInt(parts[1]);
|
|
3682
|
-
const command = parts.slice(10).join(" ");
|
|
3683
|
-
if (pid === process.pid) continue;
|
|
3684
|
-
if (command.includes("--started-by daemon") || command.includes("daemon start-sync") || command.includes("daemon start") || command.includes("--version")) {
|
|
3685
|
-
processes.push({ pid, command });
|
|
3686
|
-
}
|
|
3687
|
-
}
|
|
3688
|
-
} catch {
|
|
3689
|
-
}
|
|
3690
|
-
try {
|
|
3691
|
-
const devOutput = node_child_process.execSync('ps aux | grep -E "tsx.*src/index\\.ts" | grep -v grep', { encoding: "utf8" });
|
|
3692
|
-
const devLines = devOutput.trim().split("\n").filter((line) => line.trim());
|
|
3693
|
-
for (const line of devLines) {
|
|
3694
|
-
const parts = line.trim().split(/\s+/);
|
|
3695
|
-
if (parts.length < 11) continue;
|
|
3696
|
-
const pid = parseInt(parts[1]);
|
|
3697
|
-
const command = parts.slice(10).join(" ");
|
|
3698
|
-
if (pid === process.pid) continue;
|
|
3699
|
-
if (!command.includes("happy-cli/node_modules/tsx") && !command.includes("/bin/tsx src/index.ts")) {
|
|
3700
|
-
continue;
|
|
3701
|
-
}
|
|
3702
|
-
if (command.includes("--started-by daemon") || command.includes("daemon start-sync") || command.includes("daemon start") || command.includes("--version")) {
|
|
3703
|
-
processes.push({ pid, command });
|
|
3704
|
-
}
|
|
3705
|
-
}
|
|
3706
|
-
} catch {
|
|
3707
|
-
}
|
|
3708
|
-
return processes;
|
|
3709
|
-
} catch (error) {
|
|
3710
|
-
return [];
|
|
3711
|
-
}
|
|
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 }));
|
|
3712
3704
|
}
|
|
3713
3705
|
async function killRunawayHappyProcesses() {
|
|
3714
|
-
const runawayProcesses = findRunawayHappyProcesses();
|
|
3706
|
+
const runawayProcesses = await findRunawayHappyProcesses();
|
|
3715
3707
|
const errors = [];
|
|
3716
|
-
|
|
3708
|
+
let killed = 0;
|
|
3709
|
+
for (const { pid, command } of runawayProcesses) {
|
|
3717
3710
|
try {
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
process.kill(pid, "
|
|
3725
|
-
|
|
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
|
+
}
|
|
3726
3725
|
}
|
|
3727
3726
|
console.log(`Successfully killed runaway process PID ${pid}`);
|
|
3728
|
-
|
|
3727
|
+
killed++;
|
|
3729
3728
|
} catch (error) {
|
|
3730
3729
|
const errorMessage = error.message;
|
|
3731
3730
|
errors.push({ pid, error: errorMessage });
|
|
3732
3731
|
console.log(`Failed to kill process PID ${pid}: ${errorMessage}`);
|
|
3733
|
-
return { success: false, pid, command };
|
|
3734
3732
|
}
|
|
3735
|
-
}
|
|
3736
|
-
const results = await Promise.all(killPromises);
|
|
3737
|
-
const killed = results.filter((r) => r.success).length;
|
|
3733
|
+
}
|
|
3738
3734
|
return { killed, errors };
|
|
3739
3735
|
}
|
|
3740
3736
|
|
|
@@ -3850,7 +3846,7 @@ async function runDoctorCommand(filter) {
|
|
|
3850
3846
|
console.log(chalk.blue(`Location: ${types.configuration.daemonStateFile}`));
|
|
3851
3847
|
console.log(chalk.gray(JSON.stringify(state, null, 2)));
|
|
3852
3848
|
}
|
|
3853
|
-
const allProcesses = findAllHappyProcesses();
|
|
3849
|
+
const allProcesses = await findAllHappyProcesses();
|
|
3854
3850
|
if (allProcesses.length > 0) {
|
|
3855
3851
|
console.log(chalk.bold("\n\u{1F50D} All Happy CLI Processes"));
|
|
3856
3852
|
const grouped = allProcesses.reduce((groups, process2) => {
|
|
@@ -4206,31 +4202,54 @@ function startDaemonControlServer({
|
|
|
4206
4202
|
sessionId: z.z.string(),
|
|
4207
4203
|
metadata: z.z.any()
|
|
4208
4204
|
// Metadata type from API
|
|
4209
|
-
})
|
|
4205
|
+
}),
|
|
4206
|
+
response: {
|
|
4207
|
+
200: z.z.object({
|
|
4208
|
+
status: z.z.literal("ok")
|
|
4209
|
+
})
|
|
4210
|
+
}
|
|
4210
4211
|
}
|
|
4211
|
-
}, async (request
|
|
4212
|
+
}, async (request) => {
|
|
4212
4213
|
const { sessionId, metadata } = request.body;
|
|
4213
4214
|
types.logger.debug(`[CONTROL SERVER] Session started: ${sessionId}`);
|
|
4214
4215
|
onHappySessionWebhook(sessionId, metadata);
|
|
4215
4216
|
return { status: "ok" };
|
|
4216
4217
|
});
|
|
4217
|
-
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 () => {
|
|
4218
4231
|
const children = getChildren();
|
|
4219
4232
|
types.logger.debug(`[CONTROL SERVER] Listing ${children.length} sessions`);
|
|
4220
4233
|
return {
|
|
4221
|
-
children: children.map((child) => {
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4234
|
+
children: children.filter((child) => child.happySessionId !== void 0).map((child) => ({
|
|
4235
|
+
startedBy: child.startedBy,
|
|
4236
|
+
happySessionId: child.happySessionId,
|
|
4237
|
+
pid: child.pid
|
|
4238
|
+
}))
|
|
4225
4239
|
};
|
|
4226
4240
|
});
|
|
4227
4241
|
typed.post("/stop-session", {
|
|
4228
4242
|
schema: {
|
|
4229
4243
|
body: z.z.object({
|
|
4230
4244
|
sessionId: z.z.string()
|
|
4231
|
-
})
|
|
4245
|
+
}),
|
|
4246
|
+
response: {
|
|
4247
|
+
200: z.z.object({
|
|
4248
|
+
success: z.z.boolean()
|
|
4249
|
+
})
|
|
4250
|
+
}
|
|
4232
4251
|
}
|
|
4233
|
-
}, async (request
|
|
4252
|
+
}, async (request) => {
|
|
4234
4253
|
const { sessionId } = request.body;
|
|
4235
4254
|
types.logger.debug(`[CONTROL SERVER] Stop session request: ${sessionId}`);
|
|
4236
4255
|
const success = stopSession(sessionId);
|
|
@@ -4241,28 +4260,68 @@ function startDaemonControlServer({
|
|
|
4241
4260
|
body: z.z.object({
|
|
4242
4261
|
directory: z.z.string(),
|
|
4243
4262
|
sessionId: z.z.string().optional()
|
|
4244
|
-
})
|
|
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
|
+
}
|
|
4245
4281
|
}
|
|
4246
4282
|
}, async (request, reply) => {
|
|
4247
4283
|
const { directory, sessionId } = request.body;
|
|
4248
4284
|
types.logger.debug(`[CONTROL SERVER] Spawn session request: dir=${directory}, sessionId=${sessionId || "new"}`);
|
|
4249
|
-
const
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
4259
|
-
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
|
|
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
|
+
};
|
|
4263
4314
|
}
|
|
4264
4315
|
});
|
|
4265
|
-
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 () => {
|
|
4266
4325
|
types.logger.debug("[CONTROL SERVER] Stop daemon request received");
|
|
4267
4326
|
setTimeout(() => {
|
|
4268
4327
|
types.logger.debug("[CONTROL SERVER] Triggering daemon shutdown");
|
|
@@ -4270,21 +4329,6 @@ function startDaemonControlServer({
|
|
|
4270
4329
|
}, 50);
|
|
4271
4330
|
return { status: "stopping" };
|
|
4272
4331
|
});
|
|
4273
|
-
typed.post("/dev-simulate-error", {
|
|
4274
|
-
schema: {
|
|
4275
|
-
body: z.z.object({
|
|
4276
|
-
error: z.z.string()
|
|
4277
|
-
})
|
|
4278
|
-
}
|
|
4279
|
-
}, async (request, reply) => {
|
|
4280
|
-
const { error } = request.body;
|
|
4281
|
-
types.logger.debug(`[CONTROL SERVER] Dev: Simulating error: ${error}`);
|
|
4282
|
-
setTimeout(() => {
|
|
4283
|
-
types.logger.debug(`[CONTROL SERVER] Dev: Throwing simulated error now`);
|
|
4284
|
-
throw new Error(error);
|
|
4285
|
-
}, 100);
|
|
4286
|
-
return { status: "error will be thrown" };
|
|
4287
|
-
});
|
|
4288
4332
|
app.listen({ port: 0, host: "127.0.0.1" }, (err, address) => {
|
|
4289
4333
|
if (err) {
|
|
4290
4334
|
types.logger.debug("[CONTROL SERVER] Failed to start:", err);
|
|
@@ -4352,7 +4396,7 @@ async function startDaemon() {
|
|
|
4352
4396
|
});
|
|
4353
4397
|
types.logger.debug("[DAEMON RUN] Starting daemon process...");
|
|
4354
4398
|
types.logger.debugLargeJson("[DAEMON RUN] Environment", getEnvironmentInfo());
|
|
4355
|
-
const runningDaemonVersionMatches = await
|
|
4399
|
+
const runningDaemonVersionMatches = await isDaemonRunningCurrentlyInstalledHappyVersion();
|
|
4356
4400
|
if (!runningDaemonVersionMatches) {
|
|
4357
4401
|
types.logger.debug("[DAEMON RUN] Daemon version mismatch detected, restarting daemon with current CLI version");
|
|
4358
4402
|
await stopDaemon();
|
|
@@ -4407,16 +4451,22 @@ async function startDaemon() {
|
|
|
4407
4451
|
types.logger.debug(`[DAEMON RUN] Registered externally-started session ${sessionId}`);
|
|
4408
4452
|
}
|
|
4409
4453
|
};
|
|
4410
|
-
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;
|
|
4411
4457
|
let directoryCreated = false;
|
|
4412
|
-
if (directory.startsWith("~")) {
|
|
4413
|
-
directory = path.resolve(os$1.homedir(), directory.replace("~", ""));
|
|
4414
|
-
}
|
|
4415
4458
|
try {
|
|
4416
4459
|
await fs.access(directory);
|
|
4417
4460
|
types.logger.debug(`[DAEMON RUN] Directory exists: ${directory}`);
|
|
4418
4461
|
} catch (error) {
|
|
4419
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
|
+
}
|
|
4420
4470
|
try {
|
|
4421
4471
|
await fs.mkdir(directory, { recursive: true });
|
|
4422
4472
|
types.logger.debug(`[DAEMON RUN] Successfully created directory: ${directory}`);
|
|
@@ -4435,7 +4485,10 @@ async function startDaemon() {
|
|
|
4435
4485
|
errorMessage += `System error: ${mkdirError.message || mkdirError}. Please verify the path is valid and you have the necessary permissions.`;
|
|
4436
4486
|
}
|
|
4437
4487
|
types.logger.debug(`[DAEMON RUN] Directory creation failed: ${errorMessage}`);
|
|
4438
|
-
return
|
|
4488
|
+
return {
|
|
4489
|
+
type: "error",
|
|
4490
|
+
errorMessage
|
|
4491
|
+
};
|
|
4439
4492
|
}
|
|
4440
4493
|
}
|
|
4441
4494
|
try {
|
|
@@ -4463,7 +4516,10 @@ async function startDaemon() {
|
|
|
4463
4516
|
}
|
|
4464
4517
|
if (!happyProcess.pid) {
|
|
4465
4518
|
types.logger.debug("[DAEMON RUN] Failed to spawn process - no PID returned");
|
|
4466
|
-
return
|
|
4519
|
+
return {
|
|
4520
|
+
type: "error",
|
|
4521
|
+
errorMessage: "Failed to spawn Happy process - no PID returned"
|
|
4522
|
+
};
|
|
4467
4523
|
}
|
|
4468
4524
|
types.logger.debug(`[DAEMON RUN] Spawned process with PID ${happyProcess.pid}`);
|
|
4469
4525
|
const trackedSession = {
|
|
@@ -4491,17 +4547,27 @@ async function startDaemon() {
|
|
|
4491
4547
|
const timeout = setTimeout(() => {
|
|
4492
4548
|
pidToAwaiter.delete(happyProcess.pid);
|
|
4493
4549
|
types.logger.debug(`[DAEMON RUN] Session webhook timeout for PID ${happyProcess.pid}`);
|
|
4494
|
-
resolve2(
|
|
4550
|
+
resolve2({
|
|
4551
|
+
type: "error",
|
|
4552
|
+
errorMessage: `Session webhook timeout for PID ${happyProcess.pid}`
|
|
4553
|
+
});
|
|
4495
4554
|
}, 1e4);
|
|
4496
4555
|
pidToAwaiter.set(happyProcess.pid, (completedSession) => {
|
|
4497
4556
|
clearTimeout(timeout);
|
|
4498
4557
|
types.logger.debug(`[DAEMON RUN] Session ${completedSession.happySessionId} fully spawned with webhook`);
|
|
4499
|
-
resolve2(
|
|
4558
|
+
resolve2({
|
|
4559
|
+
type: "success",
|
|
4560
|
+
sessionId: completedSession.happySessionId
|
|
4561
|
+
});
|
|
4500
4562
|
});
|
|
4501
4563
|
});
|
|
4502
4564
|
} catch (error) {
|
|
4565
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4503
4566
|
types.logger.debug("[DAEMON RUN] Failed to spawn session:", error);
|
|
4504
|
-
return
|
|
4567
|
+
return {
|
|
4568
|
+
type: "error",
|
|
4569
|
+
errorMessage: `Failed to spawn session: ${errorMessage}`
|
|
4570
|
+
};
|
|
4505
4571
|
}
|
|
4506
4572
|
};
|
|
4507
4573
|
const stopSession = (sessionId) => {
|
|
@@ -4772,7 +4838,10 @@ async function start(credentials, options = {}) {
|
|
|
4772
4838
|
happyHomeDir: types.configuration.happyHomeDir,
|
|
4773
4839
|
startedFromDaemon: options.startedBy === "daemon",
|
|
4774
4840
|
hostPid: process.pid,
|
|
4775
|
-
startedBy: options.startedBy || "terminal"
|
|
4841
|
+
startedBy: options.startedBy || "terminal",
|
|
4842
|
+
// Initialize lifecycle state
|
|
4843
|
+
lifecycleState: "running",
|
|
4844
|
+
lifecycleStateSince: Date.now()
|
|
4776
4845
|
};
|
|
4777
4846
|
const response = await api.getOrCreateSession({ tag: sessionTag, metadata, state });
|
|
4778
4847
|
types.logger.debug(`Session created: ${response.id}`);
|
|
@@ -4940,6 +5009,13 @@ async function start(credentials, options = {}) {
|
|
|
4940
5009
|
types.logger.debug("[START] Received termination signal, cleaning up...");
|
|
4941
5010
|
try {
|
|
4942
5011
|
if (session) {
|
|
5012
|
+
session.updateMetadata((currentMetadata) => ({
|
|
5013
|
+
...currentMetadata,
|
|
5014
|
+
lifecycleState: "archived",
|
|
5015
|
+
lifecycleStateSince: Date.now(),
|
|
5016
|
+
archivedBy: "cli",
|
|
5017
|
+
archiveReason: "User terminated"
|
|
5018
|
+
}));
|
|
4943
5019
|
session.sendSessionDeath();
|
|
4944
5020
|
await session.flush();
|
|
4945
5021
|
await session.close();
|
|
@@ -4963,6 +5039,7 @@ async function start(credentials, options = {}) {
|
|
|
4963
5039
|
types.logger.debug("[START] Unhandled rejection:", reason);
|
|
4964
5040
|
cleanup();
|
|
4965
5041
|
});
|
|
5042
|
+
registerKillSessionHandler(session, cleanup);
|
|
4966
5043
|
await loop({
|
|
4967
5044
|
path: workingDirectory,
|
|
4968
5045
|
model: options.model,
|
|
@@ -4978,7 +5055,7 @@ async function start(credentials, options = {}) {
|
|
|
4978
5055
|
controlledByUser: newMode === "local"
|
|
4979
5056
|
}));
|
|
4980
5057
|
},
|
|
4981
|
-
onSessionReady: (
|
|
5058
|
+
onSessionReady: (_sessionInstance) => {
|
|
4982
5059
|
},
|
|
4983
5060
|
mcpServers: {
|
|
4984
5061
|
"happy": {
|
|
@@ -5167,19 +5244,12 @@ ${chalk.bold("Usage:")}
|
|
|
5167
5244
|
happy auth login [--force] Authenticate with Happy
|
|
5168
5245
|
happy auth logout Remove authentication and machine data
|
|
5169
5246
|
happy auth status Show authentication status
|
|
5170
|
-
happy auth
|
|
5247
|
+
happy auth backup Display backup key for mobile/web clients
|
|
5171
5248
|
happy auth help Show this help message
|
|
5172
5249
|
|
|
5173
5250
|
${chalk.bold("Options:")}
|
|
5174
5251
|
--force Clear credentials, machine ID, and stop daemon before re-auth
|
|
5175
5252
|
|
|
5176
|
-
${chalk.bold("Examples:")}
|
|
5177
|
-
happy auth login Authenticate if not already logged in
|
|
5178
|
-
happy auth login --force Force re-authentication (complete reset)
|
|
5179
|
-
happy auth status Check authentication and machine status
|
|
5180
|
-
happy auth show-backup Get backup key to link other devices
|
|
5181
|
-
happy auth logout Remove all authentication data
|
|
5182
|
-
|
|
5183
5253
|
${chalk.bold("Notes:")}
|
|
5184
5254
|
\u2022 Use 'auth login --force' when you need to re-register your machine
|
|
5185
5255
|
\u2022 'auth show-backup' displays the key format expected by mobile/web clients
|
|
@@ -5327,33 +5397,561 @@ async function handleAuthStatus() {
|
|
|
5327
5397
|
}
|
|
5328
5398
|
}
|
|
5329
5399
|
|
|
5330
|
-
const
|
|
5331
|
-
|
|
5332
|
-
|
|
5333
|
-
|
|
5334
|
-
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
|
|
5338
|
-
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
|
|
5347
|
-
|
|
5348
|
-
|
|
5349
|
-
|
|
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;
|
|
5350
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);
|
|
5351
5523
|
});
|
|
5352
|
-
|
|
5353
|
-
|
|
5354
|
-
|
|
5355
|
-
|
|
5356
|
-
|
|
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
|
+
}
|
|
5357
5955
|
|
|
5358
5956
|
(async () => {
|
|
5359
5957
|
const args = process.argv.slice(2);
|
|
@@ -5383,6 +5981,17 @@ const DaemonPrompt = ({ onSelect }) => {
|
|
|
5383
5981
|
process.exit(1);
|
|
5384
5982
|
}
|
|
5385
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;
|
|
5386
5995
|
} else if (subcommand === "logout") {
|
|
5387
5996
|
console.log(chalk.yellow('Note: "happy logout" is deprecated. Use "happy auth logout" instead.\n'));
|
|
5388
5997
|
try {
|
|
@@ -5519,7 +6128,7 @@ ${chalk.bold("To clean up runaway processes:")} Use ${chalk.cyan("happy doctor c
|
|
|
5519
6128
|
} else if (arg === "-v" || arg === "--version") {
|
|
5520
6129
|
showVersion = true;
|
|
5521
6130
|
unknownArgs.push(arg);
|
|
5522
|
-
} else if (arg === "--
|
|
6131
|
+
} else if (arg === "--happy-starting-mode") {
|
|
5523
6132
|
options.startingMode = z.z.enum(["local", "remote"]).parse(args[++i]);
|
|
5524
6133
|
} else if (arg === "--yolo") {
|
|
5525
6134
|
unknownArgs.push("--dangerously-skip-permissions");
|
|
@@ -5548,22 +6157,21 @@ ${chalk.bold("Usage:")}
|
|
|
5548
6157
|
|
|
5549
6158
|
${chalk.bold("Examples:")}
|
|
5550
6159
|
happy Start session
|
|
5551
|
-
happy --yolo
|
|
5552
|
-
|
|
6160
|
+
happy --yolo Start with bypassing permissions
|
|
6161
|
+
happy sugar for --dangerously-skip-permissions
|
|
5553
6162
|
happy auth login --force Authenticate
|
|
5554
6163
|
happy doctor Run diagnostics
|
|
5555
6164
|
|
|
5556
|
-
${chalk.bold("Happy is a wrapper around Claude Code that enables remote control via mobile app.")}
|
|
5557
|
-
|
|
5558
6165
|
${chalk.bold("Happy supports ALL Claude options!")}
|
|
5559
|
-
Use any claude flag
|
|
6166
|
+
Use any claude flag with happy as you would with claude. Our favorite:
|
|
6167
|
+
|
|
6168
|
+
happy --resume
|
|
5560
6169
|
|
|
5561
6170
|
${chalk.gray("\u2500".repeat(60))}
|
|
5562
6171
|
${chalk.bold.cyan("Claude Code Options (from `claude --help`):")}
|
|
5563
6172
|
`);
|
|
5564
|
-
const { execSync } = await import('child_process');
|
|
5565
6173
|
try {
|
|
5566
|
-
const claudeHelp =
|
|
6174
|
+
const claudeHelp = node_child_process.execFileSync(process.execPath, [claudeCliPath, "--help"], { encoding: "utf8" });
|
|
5567
6175
|
console.log(claudeHelp);
|
|
5568
6176
|
} catch (e) {
|
|
5569
6177
|
console.log(chalk.yellow("Could not retrieve claude help. Make sure claude is installed."));
|
|
@@ -5571,53 +6179,21 @@ ${chalk.bold.cyan("Claude Code Options (from `claude --help`):")}
|
|
|
5571
6179
|
process.exit(0);
|
|
5572
6180
|
}
|
|
5573
6181
|
if (showVersion) {
|
|
5574
|
-
console.log(types.packageJson.version);
|
|
5575
|
-
process.exit(0);
|
|
6182
|
+
console.log(`happy version: ${types.packageJson.version}`);
|
|
5576
6183
|
}
|
|
5577
6184
|
const {
|
|
5578
6185
|
credentials
|
|
5579
6186
|
} = await authAndSetupMachineIfNeeded();
|
|
5580
|
-
|
|
5581
|
-
if (
|
|
5582
|
-
|
|
5583
|
-
|
|
5584
|
-
|
|
5585
|
-
|
|
5586
|
-
|
|
5587
|
-
app.unmount();
|
|
5588
|
-
resolve(autoStart);
|
|
5589
|
-
}
|
|
5590
|
-
};
|
|
5591
|
-
const app = ink.render(React.createElement(DaemonPrompt, { onSelect }), {
|
|
5592
|
-
exitOnCtrlC: false,
|
|
5593
|
-
patchConsole: false
|
|
5594
|
-
});
|
|
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
|
|
5595
6194
|
});
|
|
5596
|
-
|
|
5597
|
-
|
|
5598
|
-
daemonAutoStartWhenRunningHappy: shouldAutoStart
|
|
5599
|
-
}));
|
|
5600
|
-
if (shouldAutoStart) {
|
|
5601
|
-
console.log(chalk.green("\n\u2713 Happy will start the background service automatically"));
|
|
5602
|
-
console.log(chalk.gray(" The service will run whenever you use the happy command"));
|
|
5603
|
-
} else {
|
|
5604
|
-
console.log(chalk.yellow("\n You can enable this later by running: happy daemon install"));
|
|
5605
|
-
}
|
|
5606
|
-
}
|
|
5607
|
-
if (settings && settings.daemonAutoStartWhenRunningHappy) {
|
|
5608
|
-
types.logger.debug("Ensuring Happy background service is running & matches our version...");
|
|
5609
|
-
if (!await isDaemonRunningSameVersion()) {
|
|
5610
|
-
types.logger.debug("Starting Happy background service...");
|
|
5611
|
-
const daemonProcess = spawnHappyCLI(["daemon", "start-sync"], {
|
|
5612
|
-
detached: true,
|
|
5613
|
-
stdio: "ignore",
|
|
5614
|
-
env: process.env
|
|
5615
|
-
});
|
|
5616
|
-
daemonProcess.unref();
|
|
5617
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
5618
|
-
} else {
|
|
5619
|
-
types.logger.debug("Happy background service is running & matches our version");
|
|
5620
|
-
}
|
|
6195
|
+
daemonProcess.unref();
|
|
6196
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
5621
6197
|
}
|
|
5622
6198
|
try {
|
|
5623
6199
|
await start(credentials, options);
|