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.mjs
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import os$1, { homedir } from 'node:os';
|
|
3
3
|
import { randomUUID, randomBytes } from 'node:crypto';
|
|
4
|
-
import { l as logger, b as backoff, d as delay, R as RawJSONLinesSchema, e as AsyncLock, r as readDaemonState,
|
|
5
|
-
import { spawn, execSync } from 'node:child_process';
|
|
6
|
-
import { resolve, join
|
|
4
|
+
import { l as logger, b as backoff, d as delay, R as RawJSONLinesSchema, e as AsyncLock, r as readDaemonState, f as clearDaemonState, p as packageJson, c as configuration, g as readSettings, h as readCredentials, i as encodeBase64, u as updateSettings, j as encodeBase64Url, k as decodeBase64, w as writeCredentials, m as acquireDaemonLock, n as writeDaemonState, A as ApiClient, o as releaseDaemonLock, q as clearCredentials, s as clearMachineId, t as getLatestDaemonLog } from './types-CGbH1LGX.mjs';
|
|
5
|
+
import { spawn, execSync, execFileSync } from 'node:child_process';
|
|
6
|
+
import { resolve, join } from 'node:path';
|
|
7
7
|
import { createInterface } from 'node:readline';
|
|
8
|
-
import { fileURLToPath as fileURLToPath$1 } from 'node:url';
|
|
9
8
|
import { existsSync, readFileSync, mkdirSync, watch, readdirSync, statSync, rmSync } from 'node:fs';
|
|
10
9
|
import { dirname, resolve as resolve$1, join as join$1 } from 'path';
|
|
11
10
|
import { fileURLToPath } from 'url';
|
|
@@ -13,6 +12,7 @@ import { readFile } from 'node:fs/promises';
|
|
|
13
12
|
import fs, { watch as watch$1, access, readFile as readFile$1, stat, writeFile, readdir } from 'fs/promises';
|
|
14
13
|
import { useStdout, useInput, Box, Text, render } from 'ink';
|
|
15
14
|
import React, { useState, useRef, useEffect, useCallback } from 'react';
|
|
15
|
+
import { fileURLToPath as fileURLToPath$1 } from 'node:url';
|
|
16
16
|
import axios from 'axios';
|
|
17
17
|
import 'node:events';
|
|
18
18
|
import 'socket.io-client';
|
|
@@ -20,17 +20,20 @@ import tweetnacl from 'tweetnacl';
|
|
|
20
20
|
import 'expo-server-sdk';
|
|
21
21
|
import { spawn as spawn$1, exec, execSync as execSync$1 } from 'child_process';
|
|
22
22
|
import { promisify } from 'util';
|
|
23
|
-
import { createHash } from 'crypto';
|
|
23
|
+
import { createHash, randomBytes as randomBytes$1 } from 'crypto';
|
|
24
|
+
import { readFileSync as readFileSync$1, existsSync as existsSync$1, writeFileSync, chmodSync, unlinkSync } from 'fs';
|
|
25
|
+
import psList from 'ps-list';
|
|
26
|
+
import spawn$2 from 'cross-spawn';
|
|
24
27
|
import os from 'os';
|
|
25
28
|
import qrcode from 'qrcode-terminal';
|
|
26
29
|
import open from 'open';
|
|
27
30
|
import fastify from 'fastify';
|
|
28
31
|
import { z } from 'zod';
|
|
29
32
|
import { validatorCompiler, serializerCompiler } from 'fastify-type-provider-zod';
|
|
30
|
-
import { readFileSync as readFileSync$1, existsSync as existsSync$1, writeFileSync, chmodSync, unlinkSync } from 'fs';
|
|
31
33
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
32
34
|
import { createServer } from 'node:http';
|
|
33
35
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
36
|
+
import { createServer as createServer$1 } from 'http';
|
|
34
37
|
|
|
35
38
|
class Session {
|
|
36
39
|
path;
|
|
@@ -40,6 +43,7 @@ class Session {
|
|
|
40
43
|
queue;
|
|
41
44
|
claudeEnvVars;
|
|
42
45
|
claudeArgs;
|
|
46
|
+
// Made mutable to allow filtering
|
|
43
47
|
mcpServers;
|
|
44
48
|
allowedTools;
|
|
45
49
|
_onModeChange;
|
|
@@ -74,6 +78,11 @@ class Session {
|
|
|
74
78
|
};
|
|
75
79
|
onSessionFound = (sessionId) => {
|
|
76
80
|
this.sessionId = sessionId;
|
|
81
|
+
this.client.updateMetadata((metadata) => ({
|
|
82
|
+
...metadata,
|
|
83
|
+
claudeSessionId: sessionId
|
|
84
|
+
}));
|
|
85
|
+
logger.debug(`[Session] Claude Code session ID ${sessionId} added to metadata`);
|
|
77
86
|
};
|
|
78
87
|
/**
|
|
79
88
|
* Clear the current session ID (used by /clear command)
|
|
@@ -82,6 +91,33 @@ class Session {
|
|
|
82
91
|
this.sessionId = null;
|
|
83
92
|
logger.debug("[Session] Session ID cleared");
|
|
84
93
|
};
|
|
94
|
+
/**
|
|
95
|
+
* Consume one-time Claude flags from claudeArgs after Claude spawn
|
|
96
|
+
* Currently handles: --resume (with or without session ID)
|
|
97
|
+
*/
|
|
98
|
+
consumeOneTimeFlags = () => {
|
|
99
|
+
if (!this.claudeArgs) return;
|
|
100
|
+
const filteredArgs = [];
|
|
101
|
+
for (let i = 0; i < this.claudeArgs.length; i++) {
|
|
102
|
+
if (this.claudeArgs[i] === "--resume") {
|
|
103
|
+
if (i + 1 < this.claudeArgs.length) {
|
|
104
|
+
const nextArg = this.claudeArgs[i + 1];
|
|
105
|
+
if (!nextArg.startsWith("-") && nextArg.includes("-")) {
|
|
106
|
+
i++;
|
|
107
|
+
logger.debug(`[Session] Consumed --resume flag with session ID: ${nextArg}`);
|
|
108
|
+
} else {
|
|
109
|
+
logger.debug("[Session] Consumed --resume flag (no session ID)");
|
|
110
|
+
}
|
|
111
|
+
} else {
|
|
112
|
+
logger.debug("[Session] Consumed --resume flag (no session ID)");
|
|
113
|
+
}
|
|
114
|
+
} else {
|
|
115
|
+
filteredArgs.push(this.claudeArgs[i]);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
this.claudeArgs = filteredArgs.length > 0 ? filteredArgs : void 0;
|
|
119
|
+
logger.debug(`[Session] Consumed one-time flags, remaining args:`, this.claudeArgs);
|
|
120
|
+
};
|
|
85
121
|
}
|
|
86
122
|
|
|
87
123
|
function getProjectPath(workingDirectory) {
|
|
@@ -136,9 +172,19 @@ function trimIdent(text) {
|
|
|
136
172
|
|
|
137
173
|
const systemPrompt = trimIdent(`
|
|
138
174
|
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.
|
|
175
|
+
|
|
176
|
+
When making commit messages, instead of just giving co-credit to Claude, also give credit to Happy like so:
|
|
177
|
+
|
|
178
|
+
<main commit message>
|
|
179
|
+
|
|
180
|
+
Generated with [Claude Code](https://claude.ai/code)
|
|
181
|
+
via [Happy](https://happy.engineering)
|
|
182
|
+
|
|
183
|
+
Co-Authored-By: Claude <noreply@anthropic.com>
|
|
184
|
+
Co-Authored-By: Happy <yesreply@happy.engineering>
|
|
139
185
|
`);
|
|
140
186
|
|
|
141
|
-
|
|
187
|
+
const claudeCliPath = resolve(join(projectPath(), "scripts", "claude_local_launcher.cjs"));
|
|
142
188
|
async function claudeLocal(opts) {
|
|
143
189
|
const projectDir = getProjectPath(opts.path);
|
|
144
190
|
mkdirSync(projectDir, { recursive: true });
|
|
@@ -195,7 +241,6 @@ async function claudeLocal(opts) {
|
|
|
195
241
|
if (opts.claudeArgs) {
|
|
196
242
|
args.push(...opts.claudeArgs);
|
|
197
243
|
}
|
|
198
|
-
const claudeCliPath = resolve(join(projectPath(), "scripts", "claude_local_launcher.cjs"));
|
|
199
244
|
if (!claudeCliPath || !existsSync(claudeCliPath)) {
|
|
200
245
|
throw new Error("Claude local launcher not found. Please ensure HAPPY_PROJECT_ROOT is set correctly for development.");
|
|
201
246
|
}
|
|
@@ -595,6 +640,7 @@ async function claudeLocalLauncher(session) {
|
|
|
595
640
|
mcpServers: session.mcpServers,
|
|
596
641
|
allowedTools: session.allowedTools
|
|
597
642
|
});
|
|
643
|
+
session.consumeOneTimeFlags();
|
|
598
644
|
if (!exitReason) {
|
|
599
645
|
exitReason = "exit";
|
|
600
646
|
break;
|
|
@@ -1398,6 +1444,26 @@ async function claudeRemote(opts) {
|
|
|
1398
1444
|
if (opts.sessionId && !claudeCheckSession(opts.sessionId, opts.path)) {
|
|
1399
1445
|
startFrom = null;
|
|
1400
1446
|
}
|
|
1447
|
+
if (!startFrom && opts.claudeArgs) {
|
|
1448
|
+
for (let i = 0; i < opts.claudeArgs.length; i++) {
|
|
1449
|
+
if (opts.claudeArgs[i] === "--resume") {
|
|
1450
|
+
if (i + 1 < opts.claudeArgs.length) {
|
|
1451
|
+
const nextArg = opts.claudeArgs[i + 1];
|
|
1452
|
+
if (!nextArg.startsWith("-") && nextArg.includes("-")) {
|
|
1453
|
+
startFrom = nextArg;
|
|
1454
|
+
logger.debug(`[claudeRemote] Found --resume with session ID: ${startFrom}`);
|
|
1455
|
+
break;
|
|
1456
|
+
} else {
|
|
1457
|
+
logger.debug("[claudeRemote] Found --resume without session ID - not supported in remote mode");
|
|
1458
|
+
break;
|
|
1459
|
+
}
|
|
1460
|
+
} else {
|
|
1461
|
+
logger.debug("[claudeRemote] Found --resume without session ID - not supported in remote mode");
|
|
1462
|
+
break;
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1401
1467
|
if (opts.claudeEnvVars) {
|
|
1402
1468
|
Object.entries(opts.claudeEnvVars).forEach(([key, value]) => {
|
|
1403
1469
|
process.env[key] = value;
|
|
@@ -2586,7 +2652,7 @@ async function claudeRemoteLauncher(session) {
|
|
|
2586
2652
|
let modeHash = null;
|
|
2587
2653
|
let mode = null;
|
|
2588
2654
|
try {
|
|
2589
|
-
await claudeRemote({
|
|
2655
|
+
const remoteResult = await claudeRemote({
|
|
2590
2656
|
sessionId: session.sessionId,
|
|
2591
2657
|
path: session.path,
|
|
2592
2658
|
allowedTools: session.allowedTools ?? [],
|
|
@@ -2647,6 +2713,7 @@ async function claudeRemoteLauncher(session) {
|
|
|
2647
2713
|
},
|
|
2648
2714
|
signal: abortController.signal
|
|
2649
2715
|
});
|
|
2716
|
+
session.consumeOneTimeFlags();
|
|
2650
2717
|
if (!exitReason && abortController.signal.aborted) {
|
|
2651
2718
|
session.client.sendSessionEvent({ type: "message", message: "Aborted by user" });
|
|
2652
2719
|
}
|
|
@@ -2770,7 +2837,7 @@ function run(args, options) {
|
|
|
2770
2837
|
});
|
|
2771
2838
|
}
|
|
2772
2839
|
|
|
2773
|
-
const execAsync = promisify(exec);
|
|
2840
|
+
const execAsync$1 = promisify(exec);
|
|
2774
2841
|
function registerHandlers(session) {
|
|
2775
2842
|
session.setHandler("bash", async (data) => {
|
|
2776
2843
|
logger.debug("Shell command request:", data.command);
|
|
@@ -2780,7 +2847,7 @@ function registerHandlers(session) {
|
|
|
2780
2847
|
timeout: data.timeout || 3e4
|
|
2781
2848
|
// Default 30 seconds timeout
|
|
2782
2849
|
};
|
|
2783
|
-
const { stdout, stderr } = await execAsync(data.command, options);
|
|
2850
|
+
const { stdout, stderr } = await execAsync$1(data.command, options);
|
|
2784
2851
|
return {
|
|
2785
2852
|
success: true,
|
|
2786
2853
|
stdout: stdout ? stdout.toString() : "",
|
|
@@ -2979,25 +3046,15 @@ function registerHandlers(session) {
|
|
|
2979
3046
|
};
|
|
2980
3047
|
}
|
|
2981
3048
|
});
|
|
3049
|
+
}
|
|
3050
|
+
function registerKillSessionHandler(session, killThisHappy) {
|
|
2982
3051
|
session.setHandler("killSession", async () => {
|
|
2983
3052
|
logger.debug("Kill session request received");
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
setTimeout(() => {
|
|
2990
|
-
logger.debug("[KILL SESSION] Exiting process as requested");
|
|
2991
|
-
process.exit(0);
|
|
2992
|
-
}, 100);
|
|
2993
|
-
return response;
|
|
2994
|
-
} catch (error) {
|
|
2995
|
-
logger.debug("Failed to kill session:", error);
|
|
2996
|
-
return {
|
|
2997
|
-
success: false,
|
|
2998
|
-
message: error instanceof Error ? error.message : "Failed to kill session"
|
|
2999
|
-
};
|
|
3000
|
-
}
|
|
3053
|
+
void killThisHappy();
|
|
3054
|
+
return {
|
|
3055
|
+
success: true,
|
|
3056
|
+
message: "Killing happy-cli process"
|
|
3057
|
+
};
|
|
3001
3058
|
});
|
|
3002
3059
|
}
|
|
3003
3060
|
|
|
@@ -3534,7 +3591,7 @@ async function checkIfDaemonRunningAndCleanupStaleState() {
|
|
|
3534
3591
|
return false;
|
|
3535
3592
|
}
|
|
3536
3593
|
}
|
|
3537
|
-
async function
|
|
3594
|
+
async function isDaemonRunningCurrentlyInstalledHappyVersion() {
|
|
3538
3595
|
logger.debug("[DAEMON CONTROL] Checking if daemon is running same version");
|
|
3539
3596
|
const runningDaemon = await checkIfDaemonRunningAndCleanupStaleState();
|
|
3540
3597
|
if (!runningDaemon) {
|
|
@@ -3547,8 +3604,11 @@ async function isDaemonRunningSameVersion() {
|
|
|
3547
3604
|
return false;
|
|
3548
3605
|
}
|
|
3549
3606
|
try {
|
|
3550
|
-
|
|
3551
|
-
|
|
3607
|
+
const packageJsonPath = join$1(projectPath(), "package.json");
|
|
3608
|
+
const packageJson = JSON.parse(readFileSync$1(packageJsonPath, "utf-8"));
|
|
3609
|
+
const currentCliVersion = packageJson.version;
|
|
3610
|
+
logger.debug(`[DAEMON CONTROL] Current CLI version: ${currentCliVersion}, Daemon started with version: ${state.startedWithCliVersion}`);
|
|
3611
|
+
return currentCliVersion === state.startedWithCliVersion;
|
|
3552
3612
|
} catch (error) {
|
|
3553
3613
|
logger.debug("[DAEMON CONTROL] Error checking daemon version", error);
|
|
3554
3614
|
return false;
|
|
@@ -3601,137 +3661,73 @@ async function waitForProcessDeath(pid, timeout) {
|
|
|
3601
3661
|
throw new Error("Process did not die within timeout");
|
|
3602
3662
|
}
|
|
3603
3663
|
|
|
3604
|
-
function findAllHappyProcesses() {
|
|
3664
|
+
async function findAllHappyProcesses() {
|
|
3605
3665
|
try {
|
|
3666
|
+
const processes = await psList();
|
|
3606
3667
|
const allProcesses = [];
|
|
3607
|
-
|
|
3608
|
-
const
|
|
3609
|
-
const
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
type = "user-session";
|
|
3628
|
-
}
|
|
3629
|
-
allProcesses.push({ pid, command, type });
|
|
3630
|
-
}
|
|
3631
|
-
} catch {
|
|
3632
|
-
}
|
|
3633
|
-
try {
|
|
3634
|
-
const devOutput = execSync('ps aux | grep -E "tsx.*src/index\\.ts" | grep -v grep', { encoding: "utf8" });
|
|
3635
|
-
const devLines = devOutput.trim().split("\n").filter((line) => line.trim());
|
|
3636
|
-
for (const line of devLines) {
|
|
3637
|
-
const parts = line.trim().split(/\s+/);
|
|
3638
|
-
if (parts.length < 11) continue;
|
|
3639
|
-
const pid = parseInt(parts[1]);
|
|
3640
|
-
const command = parts.slice(10).join(" ");
|
|
3641
|
-
if (!command.includes("happy-cli/node_modules/tsx") && !command.includes("/bin/tsx src/index.ts")) {
|
|
3642
|
-
continue;
|
|
3643
|
-
}
|
|
3644
|
-
let type = "unknown";
|
|
3645
|
-
if (pid === process.pid) {
|
|
3646
|
-
type = "current";
|
|
3647
|
-
} else if (command.includes("--version")) {
|
|
3648
|
-
type = "dev-daemon-version-check";
|
|
3649
|
-
} else if (command.includes("daemon start-sync") || command.includes("daemon start")) {
|
|
3650
|
-
type = "dev-daemon";
|
|
3651
|
-
} else if (command.includes("--started-by daemon")) {
|
|
3652
|
-
type = "dev-daemon-spawned";
|
|
3653
|
-
} else if (command.includes("doctor")) {
|
|
3654
|
-
type = "dev-doctor";
|
|
3655
|
-
} else if (command.includes("--yolo")) {
|
|
3656
|
-
type = "dev-session";
|
|
3657
|
-
} else {
|
|
3658
|
-
type = "dev-related";
|
|
3659
|
-
}
|
|
3660
|
-
allProcesses.push({ pid, command, type });
|
|
3668
|
+
for (const proc of processes) {
|
|
3669
|
+
const cmd = proc.cmd || "";
|
|
3670
|
+
const name = proc.name || "";
|
|
3671
|
+
const isHappy = name.includes("happy") || name === "node" && (cmd.includes("happy-cli") || cmd.includes("dist/index.mjs")) || cmd.includes("happy.mjs") || cmd.includes("happy-coder") || cmd.includes("tsx") && cmd.includes("src/index.ts") && cmd.includes("happy-cli");
|
|
3672
|
+
if (!isHappy) continue;
|
|
3673
|
+
let type = "unknown";
|
|
3674
|
+
if (proc.pid === process.pid) {
|
|
3675
|
+
type = "current";
|
|
3676
|
+
} else if (cmd.includes("--version")) {
|
|
3677
|
+
type = cmd.includes("tsx") ? "dev-daemon-version-check" : "daemon-version-check";
|
|
3678
|
+
} else if (cmd.includes("daemon start-sync") || cmd.includes("daemon start")) {
|
|
3679
|
+
type = cmd.includes("tsx") ? "dev-daemon" : "daemon";
|
|
3680
|
+
} else if (cmd.includes("--started-by daemon")) {
|
|
3681
|
+
type = cmd.includes("tsx") ? "dev-daemon-spawned" : "daemon-spawned-session";
|
|
3682
|
+
} else if (cmd.includes("doctor")) {
|
|
3683
|
+
type = cmd.includes("tsx") ? "dev-doctor" : "doctor";
|
|
3684
|
+
} else if (cmd.includes("--yolo")) {
|
|
3685
|
+
type = "dev-session";
|
|
3686
|
+
} else {
|
|
3687
|
+
type = cmd.includes("tsx") ? "dev-related" : "user-session";
|
|
3661
3688
|
}
|
|
3662
|
-
|
|
3689
|
+
allProcesses.push({ pid: proc.pid, command: cmd || name, type });
|
|
3663
3690
|
}
|
|
3664
3691
|
return allProcesses;
|
|
3665
3692
|
} catch (error) {
|
|
3666
3693
|
return [];
|
|
3667
3694
|
}
|
|
3668
3695
|
}
|
|
3669
|
-
function findRunawayHappyProcesses() {
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
const lines = output.trim().split("\n").filter((line) => line.trim());
|
|
3675
|
-
for (const line of lines) {
|
|
3676
|
-
const parts = line.trim().split(/\s+/);
|
|
3677
|
-
if (parts.length < 11) continue;
|
|
3678
|
-
const pid = parseInt(parts[1]);
|
|
3679
|
-
const command = parts.slice(10).join(" ");
|
|
3680
|
-
if (pid === process.pid) continue;
|
|
3681
|
-
if (command.includes("--started-by daemon") || command.includes("daemon start-sync") || command.includes("daemon start") || command.includes("--version")) {
|
|
3682
|
-
processes.push({ pid, command });
|
|
3683
|
-
}
|
|
3684
|
-
}
|
|
3685
|
-
} catch {
|
|
3686
|
-
}
|
|
3687
|
-
try {
|
|
3688
|
-
const devOutput = execSync('ps aux | grep -E "tsx.*src/index\\.ts" | grep -v grep', { encoding: "utf8" });
|
|
3689
|
-
const devLines = devOutput.trim().split("\n").filter((line) => line.trim());
|
|
3690
|
-
for (const line of devLines) {
|
|
3691
|
-
const parts = line.trim().split(/\s+/);
|
|
3692
|
-
if (parts.length < 11) continue;
|
|
3693
|
-
const pid = parseInt(parts[1]);
|
|
3694
|
-
const command = parts.slice(10).join(" ");
|
|
3695
|
-
if (pid === process.pid) continue;
|
|
3696
|
-
if (!command.includes("happy-cli/node_modules/tsx") && !command.includes("/bin/tsx src/index.ts")) {
|
|
3697
|
-
continue;
|
|
3698
|
-
}
|
|
3699
|
-
if (command.includes("--started-by daemon") || command.includes("daemon start-sync") || command.includes("daemon start") || command.includes("--version")) {
|
|
3700
|
-
processes.push({ pid, command });
|
|
3701
|
-
}
|
|
3702
|
-
}
|
|
3703
|
-
} catch {
|
|
3704
|
-
}
|
|
3705
|
-
return processes;
|
|
3706
|
-
} catch (error) {
|
|
3707
|
-
return [];
|
|
3708
|
-
}
|
|
3696
|
+
async function findRunawayHappyProcesses() {
|
|
3697
|
+
const allProcesses = await findAllHappyProcesses();
|
|
3698
|
+
return allProcesses.filter(
|
|
3699
|
+
(p) => p.pid !== process.pid && (p.type === "daemon" || p.type === "dev-daemon" || p.type === "daemon-spawned-session" || p.type === "dev-daemon-spawned" || p.type === "daemon-version-check" || p.type === "dev-daemon-version-check")
|
|
3700
|
+
).map((p) => ({ pid: p.pid, command: p.command }));
|
|
3709
3701
|
}
|
|
3710
3702
|
async function killRunawayHappyProcesses() {
|
|
3711
|
-
const runawayProcesses = findRunawayHappyProcesses();
|
|
3703
|
+
const runawayProcesses = await findRunawayHappyProcesses();
|
|
3712
3704
|
const errors = [];
|
|
3713
|
-
|
|
3705
|
+
let killed = 0;
|
|
3706
|
+
for (const { pid, command } of runawayProcesses) {
|
|
3714
3707
|
try {
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
process.kill(pid, "
|
|
3722
|
-
|
|
3708
|
+
console.log(`Killing runaway process PID ${pid}: ${command}`);
|
|
3709
|
+
if (process.platform === "win32") {
|
|
3710
|
+
const result = spawn$2.sync("taskkill", ["/F", "/PID", pid.toString()], { stdio: "pipe" });
|
|
3711
|
+
if (result.error) throw result.error;
|
|
3712
|
+
if (result.status !== 0) throw new Error(`taskkill exited with code ${result.status}`);
|
|
3713
|
+
} else {
|
|
3714
|
+
process.kill(pid, "SIGTERM");
|
|
3715
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
3716
|
+
const processes = await psList();
|
|
3717
|
+
const stillAlive = processes.find((p) => p.pid === pid);
|
|
3718
|
+
if (stillAlive) {
|
|
3719
|
+
console.log(`Process PID ${pid} ignored SIGTERM, using SIGKILL`);
|
|
3720
|
+
process.kill(pid, "SIGKILL");
|
|
3721
|
+
}
|
|
3723
3722
|
}
|
|
3724
3723
|
console.log(`Successfully killed runaway process PID ${pid}`);
|
|
3725
|
-
|
|
3724
|
+
killed++;
|
|
3726
3725
|
} catch (error) {
|
|
3727
3726
|
const errorMessage = error.message;
|
|
3728
3727
|
errors.push({ pid, error: errorMessage });
|
|
3729
3728
|
console.log(`Failed to kill process PID ${pid}: ${errorMessage}`);
|
|
3730
|
-
return { success: false, pid, command };
|
|
3731
3729
|
}
|
|
3732
|
-
}
|
|
3733
|
-
const results = await Promise.all(killPromises);
|
|
3734
|
-
const killed = results.filter((r) => r.success).length;
|
|
3730
|
+
}
|
|
3735
3731
|
return { killed, errors };
|
|
3736
3732
|
}
|
|
3737
3733
|
|
|
@@ -3847,7 +3843,7 @@ async function runDoctorCommand(filter) {
|
|
|
3847
3843
|
console.log(chalk.blue(`Location: ${configuration.daemonStateFile}`));
|
|
3848
3844
|
console.log(chalk.gray(JSON.stringify(state, null, 2)));
|
|
3849
3845
|
}
|
|
3850
|
-
const allProcesses = findAllHappyProcesses();
|
|
3846
|
+
const allProcesses = await findAllHappyProcesses();
|
|
3851
3847
|
if (allProcesses.length > 0) {
|
|
3852
3848
|
console.log(chalk.bold("\n\u{1F50D} All Happy CLI Processes"));
|
|
3853
3849
|
const grouped = allProcesses.reduce((groups, process2) => {
|
|
@@ -4203,31 +4199,54 @@ function startDaemonControlServer({
|
|
|
4203
4199
|
sessionId: z.string(),
|
|
4204
4200
|
metadata: z.any()
|
|
4205
4201
|
// Metadata type from API
|
|
4206
|
-
})
|
|
4202
|
+
}),
|
|
4203
|
+
response: {
|
|
4204
|
+
200: z.object({
|
|
4205
|
+
status: z.literal("ok")
|
|
4206
|
+
})
|
|
4207
|
+
}
|
|
4207
4208
|
}
|
|
4208
|
-
}, async (request
|
|
4209
|
+
}, async (request) => {
|
|
4209
4210
|
const { sessionId, metadata } = request.body;
|
|
4210
4211
|
logger.debug(`[CONTROL SERVER] Session started: ${sessionId}`);
|
|
4211
4212
|
onHappySessionWebhook(sessionId, metadata);
|
|
4212
4213
|
return { status: "ok" };
|
|
4213
4214
|
});
|
|
4214
|
-
typed.post("/list",
|
|
4215
|
+
typed.post("/list", {
|
|
4216
|
+
schema: {
|
|
4217
|
+
response: {
|
|
4218
|
+
200: z.object({
|
|
4219
|
+
children: z.array(z.object({
|
|
4220
|
+
startedBy: z.string(),
|
|
4221
|
+
happySessionId: z.string(),
|
|
4222
|
+
pid: z.number()
|
|
4223
|
+
}))
|
|
4224
|
+
})
|
|
4225
|
+
}
|
|
4226
|
+
}
|
|
4227
|
+
}, async () => {
|
|
4215
4228
|
const children = getChildren();
|
|
4216
4229
|
logger.debug(`[CONTROL SERVER] Listing ${children.length} sessions`);
|
|
4217
4230
|
return {
|
|
4218
|
-
children: children.map((child) => {
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
|
|
4231
|
+
children: children.filter((child) => child.happySessionId !== void 0).map((child) => ({
|
|
4232
|
+
startedBy: child.startedBy,
|
|
4233
|
+
happySessionId: child.happySessionId,
|
|
4234
|
+
pid: child.pid
|
|
4235
|
+
}))
|
|
4222
4236
|
};
|
|
4223
4237
|
});
|
|
4224
4238
|
typed.post("/stop-session", {
|
|
4225
4239
|
schema: {
|
|
4226
4240
|
body: z.object({
|
|
4227
4241
|
sessionId: z.string()
|
|
4228
|
-
})
|
|
4242
|
+
}),
|
|
4243
|
+
response: {
|
|
4244
|
+
200: z.object({
|
|
4245
|
+
success: z.boolean()
|
|
4246
|
+
})
|
|
4247
|
+
}
|
|
4229
4248
|
}
|
|
4230
|
-
}, async (request
|
|
4249
|
+
}, async (request) => {
|
|
4231
4250
|
const { sessionId } = request.body;
|
|
4232
4251
|
logger.debug(`[CONTROL SERVER] Stop session request: ${sessionId}`);
|
|
4233
4252
|
const success = stopSession(sessionId);
|
|
@@ -4238,28 +4257,68 @@ function startDaemonControlServer({
|
|
|
4238
4257
|
body: z.object({
|
|
4239
4258
|
directory: z.string(),
|
|
4240
4259
|
sessionId: z.string().optional()
|
|
4241
|
-
})
|
|
4260
|
+
}),
|
|
4261
|
+
response: {
|
|
4262
|
+
200: z.object({
|
|
4263
|
+
success: z.boolean(),
|
|
4264
|
+
sessionId: z.string().optional(),
|
|
4265
|
+
approvedNewDirectoryCreation: z.boolean().optional()
|
|
4266
|
+
}),
|
|
4267
|
+
409: z.object({
|
|
4268
|
+
success: z.boolean(),
|
|
4269
|
+
requiresUserApproval: z.boolean().optional(),
|
|
4270
|
+
actionRequired: z.string().optional(),
|
|
4271
|
+
directory: z.string().optional()
|
|
4272
|
+
}),
|
|
4273
|
+
500: z.object({
|
|
4274
|
+
success: z.boolean(),
|
|
4275
|
+
error: z.string().optional()
|
|
4276
|
+
})
|
|
4277
|
+
}
|
|
4242
4278
|
}
|
|
4243
4279
|
}, async (request, reply) => {
|
|
4244
4280
|
const { directory, sessionId } = request.body;
|
|
4245
4281
|
logger.debug(`[CONTROL SERVER] Spawn session request: dir=${directory}, sessionId=${sessionId || "new"}`);
|
|
4246
|
-
const
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
4259
|
-
|
|
4282
|
+
const result = await spawnSession({ directory, sessionId });
|
|
4283
|
+
switch (result.type) {
|
|
4284
|
+
case "success":
|
|
4285
|
+
if (!result.sessionId) {
|
|
4286
|
+
reply.code(500);
|
|
4287
|
+
return {
|
|
4288
|
+
success: false,
|
|
4289
|
+
error: "Failed to spawn session: no session ID returned"
|
|
4290
|
+
};
|
|
4291
|
+
}
|
|
4292
|
+
return {
|
|
4293
|
+
success: true,
|
|
4294
|
+
sessionId: result.sessionId,
|
|
4295
|
+
approvedNewDirectoryCreation: true
|
|
4296
|
+
};
|
|
4297
|
+
case "requestToApproveDirectoryCreation":
|
|
4298
|
+
reply.code(409);
|
|
4299
|
+
return {
|
|
4300
|
+
success: false,
|
|
4301
|
+
requiresUserApproval: true,
|
|
4302
|
+
actionRequired: "CREATE_DIRECTORY",
|
|
4303
|
+
directory: result.directory
|
|
4304
|
+
};
|
|
4305
|
+
case "error":
|
|
4306
|
+
reply.code(500);
|
|
4307
|
+
return {
|
|
4308
|
+
success: false,
|
|
4309
|
+
error: result.errorMessage
|
|
4310
|
+
};
|
|
4260
4311
|
}
|
|
4261
4312
|
});
|
|
4262
|
-
typed.post("/stop",
|
|
4313
|
+
typed.post("/stop", {
|
|
4314
|
+
schema: {
|
|
4315
|
+
response: {
|
|
4316
|
+
200: z.object({
|
|
4317
|
+
status: z.string()
|
|
4318
|
+
})
|
|
4319
|
+
}
|
|
4320
|
+
}
|
|
4321
|
+
}, async () => {
|
|
4263
4322
|
logger.debug("[CONTROL SERVER] Stop daemon request received");
|
|
4264
4323
|
setTimeout(() => {
|
|
4265
4324
|
logger.debug("[CONTROL SERVER] Triggering daemon shutdown");
|
|
@@ -4267,21 +4326,6 @@ function startDaemonControlServer({
|
|
|
4267
4326
|
}, 50);
|
|
4268
4327
|
return { status: "stopping" };
|
|
4269
4328
|
});
|
|
4270
|
-
typed.post("/dev-simulate-error", {
|
|
4271
|
-
schema: {
|
|
4272
|
-
body: z.object({
|
|
4273
|
-
error: z.string()
|
|
4274
|
-
})
|
|
4275
|
-
}
|
|
4276
|
-
}, async (request, reply) => {
|
|
4277
|
-
const { error } = request.body;
|
|
4278
|
-
logger.debug(`[CONTROL SERVER] Dev: Simulating error: ${error}`);
|
|
4279
|
-
setTimeout(() => {
|
|
4280
|
-
logger.debug(`[CONTROL SERVER] Dev: Throwing simulated error now`);
|
|
4281
|
-
throw new Error(error);
|
|
4282
|
-
}, 100);
|
|
4283
|
-
return { status: "error will be thrown" };
|
|
4284
|
-
});
|
|
4285
4329
|
app.listen({ port: 0, host: "127.0.0.1" }, (err, address) => {
|
|
4286
4330
|
if (err) {
|
|
4287
4331
|
logger.debug("[CONTROL SERVER] Failed to start:", err);
|
|
@@ -4349,7 +4393,7 @@ async function startDaemon() {
|
|
|
4349
4393
|
});
|
|
4350
4394
|
logger.debug("[DAEMON RUN] Starting daemon process...");
|
|
4351
4395
|
logger.debugLargeJson("[DAEMON RUN] Environment", getEnvironmentInfo());
|
|
4352
|
-
const runningDaemonVersionMatches = await
|
|
4396
|
+
const runningDaemonVersionMatches = await isDaemonRunningCurrentlyInstalledHappyVersion();
|
|
4353
4397
|
if (!runningDaemonVersionMatches) {
|
|
4354
4398
|
logger.debug("[DAEMON RUN] Daemon version mismatch detected, restarting daemon with current CLI version");
|
|
4355
4399
|
await stopDaemon();
|
|
@@ -4404,16 +4448,22 @@ async function startDaemon() {
|
|
|
4404
4448
|
logger.debug(`[DAEMON RUN] Registered externally-started session ${sessionId}`);
|
|
4405
4449
|
}
|
|
4406
4450
|
};
|
|
4407
|
-
const spawnSession = async (
|
|
4451
|
+
const spawnSession = async (options) => {
|
|
4452
|
+
logger.debugLargeJson("[DAEMON RUN] Spawning session", options);
|
|
4453
|
+
const { directory, sessionId, machineId: machineId2, approvedNewDirectoryCreation = true } = options;
|
|
4408
4454
|
let directoryCreated = false;
|
|
4409
|
-
if (directory.startsWith("~")) {
|
|
4410
|
-
directory = resolve$1(os.homedir(), directory.replace("~", ""));
|
|
4411
|
-
}
|
|
4412
4455
|
try {
|
|
4413
4456
|
await fs.access(directory);
|
|
4414
4457
|
logger.debug(`[DAEMON RUN] Directory exists: ${directory}`);
|
|
4415
4458
|
} catch (error) {
|
|
4416
4459
|
logger.debug(`[DAEMON RUN] Directory doesn't exist, creating: ${directory}`);
|
|
4460
|
+
if (!approvedNewDirectoryCreation) {
|
|
4461
|
+
logger.debug(`[DAEMON RUN] Directory creation not approved for: ${directory}`);
|
|
4462
|
+
return {
|
|
4463
|
+
type: "requestToApproveDirectoryCreation",
|
|
4464
|
+
directory
|
|
4465
|
+
};
|
|
4466
|
+
}
|
|
4417
4467
|
try {
|
|
4418
4468
|
await fs.mkdir(directory, { recursive: true });
|
|
4419
4469
|
logger.debug(`[DAEMON RUN] Successfully created directory: ${directory}`);
|
|
@@ -4432,7 +4482,10 @@ async function startDaemon() {
|
|
|
4432
4482
|
errorMessage += `System error: ${mkdirError.message || mkdirError}. Please verify the path is valid and you have the necessary permissions.`;
|
|
4433
4483
|
}
|
|
4434
4484
|
logger.debug(`[DAEMON RUN] Directory creation failed: ${errorMessage}`);
|
|
4435
|
-
return
|
|
4485
|
+
return {
|
|
4486
|
+
type: "error",
|
|
4487
|
+
errorMessage
|
|
4488
|
+
};
|
|
4436
4489
|
}
|
|
4437
4490
|
}
|
|
4438
4491
|
try {
|
|
@@ -4460,7 +4513,10 @@ async function startDaemon() {
|
|
|
4460
4513
|
}
|
|
4461
4514
|
if (!happyProcess.pid) {
|
|
4462
4515
|
logger.debug("[DAEMON RUN] Failed to spawn process - no PID returned");
|
|
4463
|
-
return
|
|
4516
|
+
return {
|
|
4517
|
+
type: "error",
|
|
4518
|
+
errorMessage: "Failed to spawn Happy process - no PID returned"
|
|
4519
|
+
};
|
|
4464
4520
|
}
|
|
4465
4521
|
logger.debug(`[DAEMON RUN] Spawned process with PID ${happyProcess.pid}`);
|
|
4466
4522
|
const trackedSession = {
|
|
@@ -4488,17 +4544,27 @@ async function startDaemon() {
|
|
|
4488
4544
|
const timeout = setTimeout(() => {
|
|
4489
4545
|
pidToAwaiter.delete(happyProcess.pid);
|
|
4490
4546
|
logger.debug(`[DAEMON RUN] Session webhook timeout for PID ${happyProcess.pid}`);
|
|
4491
|
-
resolve2(
|
|
4547
|
+
resolve2({
|
|
4548
|
+
type: "error",
|
|
4549
|
+
errorMessage: `Session webhook timeout for PID ${happyProcess.pid}`
|
|
4550
|
+
});
|
|
4492
4551
|
}, 1e4);
|
|
4493
4552
|
pidToAwaiter.set(happyProcess.pid, (completedSession) => {
|
|
4494
4553
|
clearTimeout(timeout);
|
|
4495
4554
|
logger.debug(`[DAEMON RUN] Session ${completedSession.happySessionId} fully spawned with webhook`);
|
|
4496
|
-
resolve2(
|
|
4555
|
+
resolve2({
|
|
4556
|
+
type: "success",
|
|
4557
|
+
sessionId: completedSession.happySessionId
|
|
4558
|
+
});
|
|
4497
4559
|
});
|
|
4498
4560
|
});
|
|
4499
4561
|
} catch (error) {
|
|
4562
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4500
4563
|
logger.debug("[DAEMON RUN] Failed to spawn session:", error);
|
|
4501
|
-
return
|
|
4564
|
+
return {
|
|
4565
|
+
type: "error",
|
|
4566
|
+
errorMessage: `Failed to spawn session: ${errorMessage}`
|
|
4567
|
+
};
|
|
4502
4568
|
}
|
|
4503
4569
|
};
|
|
4504
4570
|
const stopSession = (sessionId) => {
|
|
@@ -4769,7 +4835,10 @@ async function start(credentials, options = {}) {
|
|
|
4769
4835
|
happyHomeDir: configuration.happyHomeDir,
|
|
4770
4836
|
startedFromDaemon: options.startedBy === "daemon",
|
|
4771
4837
|
hostPid: process.pid,
|
|
4772
|
-
startedBy: options.startedBy || "terminal"
|
|
4838
|
+
startedBy: options.startedBy || "terminal",
|
|
4839
|
+
// Initialize lifecycle state
|
|
4840
|
+
lifecycleState: "running",
|
|
4841
|
+
lifecycleStateSince: Date.now()
|
|
4773
4842
|
};
|
|
4774
4843
|
const response = await api.getOrCreateSession({ tag: sessionTag, metadata, state });
|
|
4775
4844
|
logger.debug(`Session created: ${response.id}`);
|
|
@@ -4937,6 +5006,13 @@ async function start(credentials, options = {}) {
|
|
|
4937
5006
|
logger.debug("[START] Received termination signal, cleaning up...");
|
|
4938
5007
|
try {
|
|
4939
5008
|
if (session) {
|
|
5009
|
+
session.updateMetadata((currentMetadata) => ({
|
|
5010
|
+
...currentMetadata,
|
|
5011
|
+
lifecycleState: "archived",
|
|
5012
|
+
lifecycleStateSince: Date.now(),
|
|
5013
|
+
archivedBy: "cli",
|
|
5014
|
+
archiveReason: "User terminated"
|
|
5015
|
+
}));
|
|
4940
5016
|
session.sendSessionDeath();
|
|
4941
5017
|
await session.flush();
|
|
4942
5018
|
await session.close();
|
|
@@ -4960,6 +5036,7 @@ async function start(credentials, options = {}) {
|
|
|
4960
5036
|
logger.debug("[START] Unhandled rejection:", reason);
|
|
4961
5037
|
cleanup();
|
|
4962
5038
|
});
|
|
5039
|
+
registerKillSessionHandler(session, cleanup);
|
|
4963
5040
|
await loop({
|
|
4964
5041
|
path: workingDirectory,
|
|
4965
5042
|
model: options.model,
|
|
@@ -4975,7 +5052,7 @@ async function start(credentials, options = {}) {
|
|
|
4975
5052
|
controlledByUser: newMode === "local"
|
|
4976
5053
|
}));
|
|
4977
5054
|
},
|
|
4978
|
-
onSessionReady: (
|
|
5055
|
+
onSessionReady: (_sessionInstance) => {
|
|
4979
5056
|
},
|
|
4980
5057
|
mcpServers: {
|
|
4981
5058
|
"happy": {
|
|
@@ -5164,19 +5241,12 @@ ${chalk.bold("Usage:")}
|
|
|
5164
5241
|
happy auth login [--force] Authenticate with Happy
|
|
5165
5242
|
happy auth logout Remove authentication and machine data
|
|
5166
5243
|
happy auth status Show authentication status
|
|
5167
|
-
happy auth
|
|
5244
|
+
happy auth backup Display backup key for mobile/web clients
|
|
5168
5245
|
happy auth help Show this help message
|
|
5169
5246
|
|
|
5170
5247
|
${chalk.bold("Options:")}
|
|
5171
5248
|
--force Clear credentials, machine ID, and stop daemon before re-auth
|
|
5172
5249
|
|
|
5173
|
-
${chalk.bold("Examples:")}
|
|
5174
|
-
happy auth login Authenticate if not already logged in
|
|
5175
|
-
happy auth login --force Force re-authentication (complete reset)
|
|
5176
|
-
happy auth status Check authentication and machine status
|
|
5177
|
-
happy auth show-backup Get backup key to link other devices
|
|
5178
|
-
happy auth logout Remove all authentication data
|
|
5179
|
-
|
|
5180
5250
|
${chalk.bold("Notes:")}
|
|
5181
5251
|
\u2022 Use 'auth login --force' when you need to re-register your machine
|
|
5182
5252
|
\u2022 'auth show-backup' displays the key format expected by mobile/web clients
|
|
@@ -5324,33 +5394,561 @@ async function handleAuthStatus() {
|
|
|
5324
5394
|
}
|
|
5325
5395
|
}
|
|
5326
5396
|
|
|
5327
|
-
const
|
|
5328
|
-
|
|
5329
|
-
|
|
5330
|
-
|
|
5331
|
-
|
|
5332
|
-
|
|
5333
|
-
|
|
5334
|
-
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
|
|
5338
|
-
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
|
|
5397
|
+
const CLIENT_ID$2 = "app_EMoamEEZ73f0CkXaXp7hrann";
|
|
5398
|
+
const AUTH_BASE_URL = "https://auth.openai.com";
|
|
5399
|
+
const DEFAULT_PORT$2 = 1455;
|
|
5400
|
+
function generatePKCE$2() {
|
|
5401
|
+
const verifier = randomBytes$1(32).toString("base64url").replace(/[^a-zA-Z0-9\-._~]/g, "");
|
|
5402
|
+
const challenge = createHash("sha256").update(verifier).digest("base64url").replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
|
|
5403
|
+
return { verifier, challenge };
|
|
5404
|
+
}
|
|
5405
|
+
function generateState$2() {
|
|
5406
|
+
return randomBytes$1(16).toString("hex");
|
|
5407
|
+
}
|
|
5408
|
+
function parseJWT(token) {
|
|
5409
|
+
const parts = token.split(".");
|
|
5410
|
+
if (parts.length !== 3) {
|
|
5411
|
+
throw new Error("Invalid JWT format");
|
|
5412
|
+
}
|
|
5413
|
+
const payload = Buffer.from(parts[1], "base64url").toString();
|
|
5414
|
+
return JSON.parse(payload);
|
|
5415
|
+
}
|
|
5416
|
+
async function findAvailablePort$2() {
|
|
5417
|
+
return new Promise((resolve) => {
|
|
5418
|
+
const server = createServer$1();
|
|
5419
|
+
server.listen(0, "127.0.0.1", () => {
|
|
5420
|
+
const port = server.address().port;
|
|
5421
|
+
server.close(() => resolve(port));
|
|
5422
|
+
});
|
|
5423
|
+
});
|
|
5424
|
+
}
|
|
5425
|
+
async function isPortAvailable$2(port) {
|
|
5426
|
+
return new Promise((resolve) => {
|
|
5427
|
+
const testServer = createServer$1();
|
|
5428
|
+
testServer.once("error", () => {
|
|
5429
|
+
testServer.close();
|
|
5430
|
+
resolve(false);
|
|
5431
|
+
});
|
|
5432
|
+
testServer.listen(port, "127.0.0.1", () => {
|
|
5433
|
+
testServer.close(() => resolve(true));
|
|
5434
|
+
});
|
|
5435
|
+
});
|
|
5436
|
+
}
|
|
5437
|
+
async function exchangeCodeForTokens$2(code, verifier, port) {
|
|
5438
|
+
const response = await fetch(`${AUTH_BASE_URL}/oauth/token`, {
|
|
5439
|
+
method: "POST",
|
|
5440
|
+
headers: {
|
|
5441
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
5442
|
+
},
|
|
5443
|
+
body: new URLSearchParams({
|
|
5444
|
+
grant_type: "authorization_code",
|
|
5445
|
+
client_id: CLIENT_ID$2,
|
|
5446
|
+
code,
|
|
5447
|
+
code_verifier: verifier,
|
|
5448
|
+
redirect_uri: `http://localhost:${port}/auth/callback`
|
|
5449
|
+
})
|
|
5450
|
+
});
|
|
5451
|
+
if (!response.ok) {
|
|
5452
|
+
const error = await response.text();
|
|
5453
|
+
throw new Error(`Token exchange failed: ${error}`);
|
|
5454
|
+
}
|
|
5455
|
+
const data = await response.json();
|
|
5456
|
+
const idTokenPayload = parseJWT(data.id_token);
|
|
5457
|
+
let accountId = idTokenPayload.chatgpt_account_id;
|
|
5458
|
+
if (!accountId) {
|
|
5459
|
+
const authClaim = idTokenPayload["https://api.openai.com/auth"];
|
|
5460
|
+
if (authClaim && typeof authClaim === "object") {
|
|
5461
|
+
accountId = authClaim.chatgpt_account_id || authClaim.account_id;
|
|
5347
5462
|
}
|
|
5463
|
+
}
|
|
5464
|
+
return {
|
|
5465
|
+
id_token: data.id_token,
|
|
5466
|
+
access_token: data.access_token || data.id_token,
|
|
5467
|
+
refresh_token: data.refresh_token,
|
|
5468
|
+
account_id: accountId
|
|
5469
|
+
};
|
|
5470
|
+
}
|
|
5471
|
+
async function startCallbackServer$2(state, verifier, port) {
|
|
5472
|
+
return new Promise((resolve, reject) => {
|
|
5473
|
+
const server = createServer$1(async (req, res) => {
|
|
5474
|
+
const url = new URL(req.url, `http://localhost:${port}`);
|
|
5475
|
+
if (url.pathname === "/auth/callback") {
|
|
5476
|
+
const code = url.searchParams.get("code");
|
|
5477
|
+
const receivedState = url.searchParams.get("state");
|
|
5478
|
+
if (receivedState !== state) {
|
|
5479
|
+
res.writeHead(400);
|
|
5480
|
+
res.end("Invalid state parameter");
|
|
5481
|
+
server.close();
|
|
5482
|
+
reject(new Error("Invalid state parameter"));
|
|
5483
|
+
return;
|
|
5484
|
+
}
|
|
5485
|
+
if (!code) {
|
|
5486
|
+
res.writeHead(400);
|
|
5487
|
+
res.end("No authorization code received");
|
|
5488
|
+
server.close();
|
|
5489
|
+
reject(new Error("No authorization code received"));
|
|
5490
|
+
return;
|
|
5491
|
+
}
|
|
5492
|
+
try {
|
|
5493
|
+
const tokens = await exchangeCodeForTokens$2(code, verifier, port);
|
|
5494
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
5495
|
+
res.end(`
|
|
5496
|
+
<html>
|
|
5497
|
+
<body style="font-family: sans-serif; padding: 20px;">
|
|
5498
|
+
<h2>\u2705 Authentication Successful!</h2>
|
|
5499
|
+
<p>You can close this window and return to your terminal.</p>
|
|
5500
|
+
<script>setTimeout(() => window.close(), 3000);<\/script>
|
|
5501
|
+
</body>
|
|
5502
|
+
</html>
|
|
5503
|
+
`);
|
|
5504
|
+
server.close();
|
|
5505
|
+
resolve(tokens);
|
|
5506
|
+
} catch (error) {
|
|
5507
|
+
res.writeHead(500);
|
|
5508
|
+
res.end("Token exchange failed");
|
|
5509
|
+
server.close();
|
|
5510
|
+
reject(error);
|
|
5511
|
+
}
|
|
5512
|
+
}
|
|
5513
|
+
});
|
|
5514
|
+
server.listen(port, "127.0.0.1", () => {
|
|
5515
|
+
});
|
|
5516
|
+
setTimeout(() => {
|
|
5517
|
+
server.close();
|
|
5518
|
+
reject(new Error("Authentication timeout"));
|
|
5519
|
+
}, 5 * 60 * 1e3);
|
|
5348
5520
|
});
|
|
5349
|
-
|
|
5350
|
-
|
|
5351
|
-
|
|
5352
|
-
|
|
5353
|
-
|
|
5521
|
+
}
|
|
5522
|
+
async function authenticateCodex() {
|
|
5523
|
+
const { verifier, challenge } = generatePKCE$2();
|
|
5524
|
+
const state = generateState$2();
|
|
5525
|
+
let port = DEFAULT_PORT$2;
|
|
5526
|
+
const portAvailable = await isPortAvailable$2(port);
|
|
5527
|
+
if (!portAvailable) {
|
|
5528
|
+
port = await findAvailablePort$2();
|
|
5529
|
+
}
|
|
5530
|
+
const serverPromise = startCallbackServer$2(state, verifier, port);
|
|
5531
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
5532
|
+
const redirect_uri = `http://localhost:${port}/auth/callback`;
|
|
5533
|
+
const params = [
|
|
5534
|
+
["response_type", "code"],
|
|
5535
|
+
["client_id", CLIENT_ID$2],
|
|
5536
|
+
["redirect_uri", redirect_uri],
|
|
5537
|
+
["scope", "openid profile email offline_access"],
|
|
5538
|
+
["code_challenge", challenge],
|
|
5539
|
+
["code_challenge_method", "S256"],
|
|
5540
|
+
["id_token_add_organizations", "true"],
|
|
5541
|
+
["codex_cli_simplified_flow", "true"],
|
|
5542
|
+
["state", state]
|
|
5543
|
+
];
|
|
5544
|
+
const queryString = params.map(([key, value]) => `${key}=${encodeURIComponent(value)}`).join("&");
|
|
5545
|
+
const authUrl = `${AUTH_BASE_URL}/oauth/authorize?${queryString}`;
|
|
5546
|
+
console.log("\u{1F4CB} Opening browser for authentication...");
|
|
5547
|
+
console.log(`If browser doesn't open, visit:
|
|
5548
|
+
${authUrl}
|
|
5549
|
+
`);
|
|
5550
|
+
await openBrowser(authUrl);
|
|
5551
|
+
const tokens = await serverPromise;
|
|
5552
|
+
console.log("\u{1F389} Authentication successful!");
|
|
5553
|
+
return tokens;
|
|
5554
|
+
}
|
|
5555
|
+
|
|
5556
|
+
const CLIENT_ID$1 = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
|
|
5557
|
+
const CLAUDE_AI_AUTHORIZE_URL = "https://claude.ai/oauth/authorize";
|
|
5558
|
+
const TOKEN_URL$1 = "https://console.anthropic.com/v1/oauth/token";
|
|
5559
|
+
const DEFAULT_PORT$1 = 54545;
|
|
5560
|
+
const SCOPE = "user:inference";
|
|
5561
|
+
function generatePKCE$1() {
|
|
5562
|
+
const verifier = randomBytes$1(32).toString("base64url").replace(/[^a-zA-Z0-9\-._~]/g, "");
|
|
5563
|
+
const challenge = createHash("sha256").update(verifier).digest("base64url").replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
|
|
5564
|
+
return { verifier, challenge };
|
|
5565
|
+
}
|
|
5566
|
+
function generateState$1() {
|
|
5567
|
+
return randomBytes$1(32).toString("base64url");
|
|
5568
|
+
}
|
|
5569
|
+
async function findAvailablePort$1() {
|
|
5570
|
+
return new Promise((resolve) => {
|
|
5571
|
+
const server = createServer$1();
|
|
5572
|
+
server.listen(0, "127.0.0.1", () => {
|
|
5573
|
+
const port = server.address().port;
|
|
5574
|
+
server.close(() => resolve(port));
|
|
5575
|
+
});
|
|
5576
|
+
});
|
|
5577
|
+
}
|
|
5578
|
+
async function isPortAvailable$1(port) {
|
|
5579
|
+
return new Promise((resolve) => {
|
|
5580
|
+
const testServer = createServer$1();
|
|
5581
|
+
testServer.once("error", () => {
|
|
5582
|
+
testServer.close();
|
|
5583
|
+
resolve(false);
|
|
5584
|
+
});
|
|
5585
|
+
testServer.listen(port, "127.0.0.1", () => {
|
|
5586
|
+
testServer.close(() => resolve(true));
|
|
5587
|
+
});
|
|
5588
|
+
});
|
|
5589
|
+
}
|
|
5590
|
+
async function exchangeCodeForTokens$1(code, verifier, port, state) {
|
|
5591
|
+
const tokenResponse = await fetch(TOKEN_URL$1, {
|
|
5592
|
+
method: "POST",
|
|
5593
|
+
headers: {
|
|
5594
|
+
"Content-Type": "application/json"
|
|
5595
|
+
},
|
|
5596
|
+
body: JSON.stringify({
|
|
5597
|
+
grant_type: "authorization_code",
|
|
5598
|
+
code,
|
|
5599
|
+
redirect_uri: `http://localhost:${port}/callback`,
|
|
5600
|
+
client_id: CLIENT_ID$1,
|
|
5601
|
+
code_verifier: verifier,
|
|
5602
|
+
state
|
|
5603
|
+
})
|
|
5604
|
+
});
|
|
5605
|
+
if (!tokenResponse.ok) {
|
|
5606
|
+
throw new Error(`Token exchange failed: ${tokenResponse.statusText}`);
|
|
5607
|
+
}
|
|
5608
|
+
const tokenData = await tokenResponse.json();
|
|
5609
|
+
return {
|
|
5610
|
+
raw: tokenData,
|
|
5611
|
+
token: tokenData.access_token,
|
|
5612
|
+
expires: Date.now() + tokenData.expires_in * 1e3
|
|
5613
|
+
};
|
|
5614
|
+
}
|
|
5615
|
+
async function startCallbackServer$1(state, verifier, port) {
|
|
5616
|
+
return new Promise((resolve, reject) => {
|
|
5617
|
+
const server = createServer$1(async (req, res) => {
|
|
5618
|
+
const url = new URL(req.url, `http://localhost:${port}`);
|
|
5619
|
+
if (url.pathname === "/callback") {
|
|
5620
|
+
const code = url.searchParams.get("code");
|
|
5621
|
+
const receivedState = url.searchParams.get("state");
|
|
5622
|
+
if (receivedState !== state) {
|
|
5623
|
+
res.writeHead(400);
|
|
5624
|
+
res.end("Invalid state parameter");
|
|
5625
|
+
server.close();
|
|
5626
|
+
reject(new Error("Invalid state parameter"));
|
|
5627
|
+
return;
|
|
5628
|
+
}
|
|
5629
|
+
if (!code) {
|
|
5630
|
+
res.writeHead(400);
|
|
5631
|
+
res.end("No authorization code received");
|
|
5632
|
+
server.close();
|
|
5633
|
+
reject(new Error("No authorization code received"));
|
|
5634
|
+
return;
|
|
5635
|
+
}
|
|
5636
|
+
try {
|
|
5637
|
+
const tokens = await exchangeCodeForTokens$1(code, verifier, port, state);
|
|
5638
|
+
res.writeHead(302, {
|
|
5639
|
+
"Location": "https://console.anthropic.com/oauth/code/success?app=claude-code"
|
|
5640
|
+
});
|
|
5641
|
+
res.end();
|
|
5642
|
+
server.close();
|
|
5643
|
+
resolve(tokens);
|
|
5644
|
+
} catch (error) {
|
|
5645
|
+
res.writeHead(500);
|
|
5646
|
+
res.end("Token exchange failed");
|
|
5647
|
+
server.close();
|
|
5648
|
+
reject(error);
|
|
5649
|
+
}
|
|
5650
|
+
}
|
|
5651
|
+
});
|
|
5652
|
+
server.listen(port, "127.0.0.1", () => {
|
|
5653
|
+
});
|
|
5654
|
+
setTimeout(() => {
|
|
5655
|
+
server.close();
|
|
5656
|
+
reject(new Error("Authentication timeout"));
|
|
5657
|
+
}, 5 * 60 * 1e3);
|
|
5658
|
+
});
|
|
5659
|
+
}
|
|
5660
|
+
async function authenticateClaude() {
|
|
5661
|
+
console.log("\u{1F680} Starting Anthropic Claude authentication...");
|
|
5662
|
+
const { verifier, challenge } = generatePKCE$1();
|
|
5663
|
+
const state = generateState$1();
|
|
5664
|
+
let port = DEFAULT_PORT$1;
|
|
5665
|
+
const portAvailable = await isPortAvailable$1(port);
|
|
5666
|
+
if (!portAvailable) {
|
|
5667
|
+
console.log(`Port ${port} is in use, finding an available port...`);
|
|
5668
|
+
port = await findAvailablePort$1();
|
|
5669
|
+
}
|
|
5670
|
+
console.log(`\u{1F4E1} Using callback port: ${port}`);
|
|
5671
|
+
const serverPromise = startCallbackServer$1(state, verifier, port);
|
|
5672
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
5673
|
+
const redirect_uri = `http://localhost:${port}/callback`;
|
|
5674
|
+
const params = new URLSearchParams({
|
|
5675
|
+
code: "true",
|
|
5676
|
+
// This tells Claude.ai to show the code AND redirect
|
|
5677
|
+
client_id: CLIENT_ID$1,
|
|
5678
|
+
response_type: "code",
|
|
5679
|
+
redirect_uri,
|
|
5680
|
+
scope: SCOPE,
|
|
5681
|
+
code_challenge: challenge,
|
|
5682
|
+
code_challenge_method: "S256",
|
|
5683
|
+
state
|
|
5684
|
+
});
|
|
5685
|
+
const authUrl = `${CLAUDE_AI_AUTHORIZE_URL}?${params}`;
|
|
5686
|
+
console.log("\u{1F4CB} Opening browser for authentication...");
|
|
5687
|
+
console.log("If browser doesn't open, visit this URL:");
|
|
5688
|
+
console.log();
|
|
5689
|
+
console.log(`${authUrl}`);
|
|
5690
|
+
console.log();
|
|
5691
|
+
await openBrowser(authUrl);
|
|
5692
|
+
try {
|
|
5693
|
+
const tokens = await serverPromise;
|
|
5694
|
+
console.log("\u{1F389} Authentication successful!");
|
|
5695
|
+
console.log("\u2705 OAuth tokens received");
|
|
5696
|
+
return tokens;
|
|
5697
|
+
} catch (error) {
|
|
5698
|
+
console.error("\n\u274C Failed to authenticate with Anthropic");
|
|
5699
|
+
throw error;
|
|
5700
|
+
}
|
|
5701
|
+
}
|
|
5702
|
+
|
|
5703
|
+
const execAsync = promisify(exec);
|
|
5704
|
+
const CLIENT_ID = "681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com";
|
|
5705
|
+
const CLIENT_SECRET = "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";
|
|
5706
|
+
const AUTHORIZE_URL = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
5707
|
+
const TOKEN_URL = "https://oauth2.googleapis.com/token";
|
|
5708
|
+
const DEFAULT_PORT = 54545;
|
|
5709
|
+
const SCOPES = [
|
|
5710
|
+
"https://www.googleapis.com/auth/cloud-platform",
|
|
5711
|
+
"https://www.googleapis.com/auth/userinfo.email",
|
|
5712
|
+
"https://www.googleapis.com/auth/userinfo.profile"
|
|
5713
|
+
].join(" ");
|
|
5714
|
+
function generatePKCE() {
|
|
5715
|
+
const verifier = randomBytes$1(32).toString("base64url").replace(/[^a-zA-Z0-9\-._~]/g, "");
|
|
5716
|
+
const challenge = createHash("sha256").update(verifier).digest("base64url").replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
|
|
5717
|
+
return { verifier, challenge };
|
|
5718
|
+
}
|
|
5719
|
+
function generateState() {
|
|
5720
|
+
return randomBytes$1(32).toString("hex");
|
|
5721
|
+
}
|
|
5722
|
+
async function findAvailablePort() {
|
|
5723
|
+
return new Promise((resolve) => {
|
|
5724
|
+
const server = createServer$1();
|
|
5725
|
+
server.listen(0, "127.0.0.1", () => {
|
|
5726
|
+
const port = server.address().port;
|
|
5727
|
+
server.close(() => resolve(port));
|
|
5728
|
+
});
|
|
5729
|
+
});
|
|
5730
|
+
}
|
|
5731
|
+
async function isPortAvailable(port) {
|
|
5732
|
+
return new Promise((resolve) => {
|
|
5733
|
+
const testServer = createServer$1();
|
|
5734
|
+
testServer.once("error", () => {
|
|
5735
|
+
testServer.close();
|
|
5736
|
+
resolve(false);
|
|
5737
|
+
});
|
|
5738
|
+
testServer.listen(port, "127.0.0.1", () => {
|
|
5739
|
+
testServer.close(() => resolve(true));
|
|
5740
|
+
});
|
|
5741
|
+
});
|
|
5742
|
+
}
|
|
5743
|
+
async function exchangeCodeForTokens(code, verifier, port) {
|
|
5744
|
+
const response = await fetch(TOKEN_URL, {
|
|
5745
|
+
method: "POST",
|
|
5746
|
+
headers: {
|
|
5747
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
5748
|
+
},
|
|
5749
|
+
body: new URLSearchParams({
|
|
5750
|
+
grant_type: "authorization_code",
|
|
5751
|
+
client_id: CLIENT_ID,
|
|
5752
|
+
client_secret: CLIENT_SECRET,
|
|
5753
|
+
code,
|
|
5754
|
+
code_verifier: verifier,
|
|
5755
|
+
redirect_uri: `http://localhost:${port}/oauth2callback`
|
|
5756
|
+
})
|
|
5757
|
+
});
|
|
5758
|
+
if (!response.ok) {
|
|
5759
|
+
const error = await response.text();
|
|
5760
|
+
throw new Error(`Token exchange failed: ${error}`);
|
|
5761
|
+
}
|
|
5762
|
+
const data = await response.json();
|
|
5763
|
+
return data;
|
|
5764
|
+
}
|
|
5765
|
+
async function startCallbackServer(state, verifier, port) {
|
|
5766
|
+
return new Promise((resolve, reject) => {
|
|
5767
|
+
const server = createServer$1(async (req, res) => {
|
|
5768
|
+
const url = new URL(req.url, `http://localhost:${port}`);
|
|
5769
|
+
if (url.pathname === "/oauth2callback") {
|
|
5770
|
+
const code = url.searchParams.get("code");
|
|
5771
|
+
const receivedState = url.searchParams.get("state");
|
|
5772
|
+
const error = url.searchParams.get("error");
|
|
5773
|
+
if (error) {
|
|
5774
|
+
res.writeHead(302, {
|
|
5775
|
+
"Location": "https://developers.google.com/gemini-code-assist/auth_failure_gemini"
|
|
5776
|
+
});
|
|
5777
|
+
res.end();
|
|
5778
|
+
server.close();
|
|
5779
|
+
reject(new Error(`Authentication error: ${error}`));
|
|
5780
|
+
return;
|
|
5781
|
+
}
|
|
5782
|
+
if (receivedState !== state) {
|
|
5783
|
+
res.writeHead(400);
|
|
5784
|
+
res.end("State mismatch. Possible CSRF attack");
|
|
5785
|
+
server.close();
|
|
5786
|
+
reject(new Error("Invalid state parameter"));
|
|
5787
|
+
return;
|
|
5788
|
+
}
|
|
5789
|
+
if (!code) {
|
|
5790
|
+
res.writeHead(400);
|
|
5791
|
+
res.end("No authorization code received");
|
|
5792
|
+
server.close();
|
|
5793
|
+
reject(new Error("No authorization code received"));
|
|
5794
|
+
return;
|
|
5795
|
+
}
|
|
5796
|
+
try {
|
|
5797
|
+
const tokens = await exchangeCodeForTokens(code, verifier, port);
|
|
5798
|
+
res.writeHead(302, {
|
|
5799
|
+
"Location": "https://developers.google.com/gemini-code-assist/auth_success_gemini"
|
|
5800
|
+
});
|
|
5801
|
+
res.end();
|
|
5802
|
+
server.close();
|
|
5803
|
+
resolve(tokens);
|
|
5804
|
+
} catch (error2) {
|
|
5805
|
+
res.writeHead(500);
|
|
5806
|
+
res.end("Token exchange failed");
|
|
5807
|
+
server.close();
|
|
5808
|
+
reject(error2);
|
|
5809
|
+
}
|
|
5810
|
+
}
|
|
5811
|
+
});
|
|
5812
|
+
server.listen(port, "127.0.0.1", () => {
|
|
5813
|
+
});
|
|
5814
|
+
setTimeout(() => {
|
|
5815
|
+
server.close();
|
|
5816
|
+
reject(new Error("Authentication timeout"));
|
|
5817
|
+
}, 5 * 60 * 1e3);
|
|
5818
|
+
});
|
|
5819
|
+
}
|
|
5820
|
+
async function authenticateGemini() {
|
|
5821
|
+
console.log("\u{1F680} Starting Google Gemini authentication...");
|
|
5822
|
+
const { verifier, challenge } = generatePKCE();
|
|
5823
|
+
const state = generateState();
|
|
5824
|
+
let port = DEFAULT_PORT;
|
|
5825
|
+
const portAvailable = await isPortAvailable(port);
|
|
5826
|
+
if (!portAvailable) {
|
|
5827
|
+
console.log(`Port ${port} is in use, finding an available port...`);
|
|
5828
|
+
port = await findAvailablePort();
|
|
5829
|
+
}
|
|
5830
|
+
console.log(`\u{1F4E1} Using callback port: ${port}`);
|
|
5831
|
+
const serverPromise = startCallbackServer(state, verifier, port);
|
|
5832
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
5833
|
+
const redirect_uri = `http://localhost:${port}/oauth2callback`;
|
|
5834
|
+
const params = new URLSearchParams({
|
|
5835
|
+
client_id: CLIENT_ID,
|
|
5836
|
+
response_type: "code",
|
|
5837
|
+
redirect_uri,
|
|
5838
|
+
scope: SCOPES,
|
|
5839
|
+
access_type: "offline",
|
|
5840
|
+
// To get refresh token
|
|
5841
|
+
code_challenge: challenge,
|
|
5842
|
+
code_challenge_method: "S256",
|
|
5843
|
+
state,
|
|
5844
|
+
prompt: "consent"
|
|
5845
|
+
// Force consent to get refresh token
|
|
5846
|
+
});
|
|
5847
|
+
const authUrl = `${AUTHORIZE_URL}?${params}`;
|
|
5848
|
+
console.log("\n\u{1F4CB} Opening browser for authentication...");
|
|
5849
|
+
console.log("If browser doesn't open, visit this URL:");
|
|
5850
|
+
console.log(`
|
|
5851
|
+
${authUrl}
|
|
5852
|
+
`);
|
|
5853
|
+
const platform = process.platform;
|
|
5854
|
+
const openCommand = platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open";
|
|
5855
|
+
try {
|
|
5856
|
+
await execAsync(`${openCommand} "${authUrl}"`);
|
|
5857
|
+
} catch {
|
|
5858
|
+
console.log("\u26A0\uFE0F Could not open browser automatically");
|
|
5859
|
+
}
|
|
5860
|
+
try {
|
|
5861
|
+
const tokens = await serverPromise;
|
|
5862
|
+
console.log("\n\u{1F389} Authentication successful!");
|
|
5863
|
+
console.log("\u2705 OAuth tokens received");
|
|
5864
|
+
return tokens;
|
|
5865
|
+
} catch (error) {
|
|
5866
|
+
console.error("\n\u274C Failed to authenticate with Google");
|
|
5867
|
+
throw error;
|
|
5868
|
+
}
|
|
5869
|
+
}
|
|
5870
|
+
|
|
5871
|
+
async function handleConnectCommand(args) {
|
|
5872
|
+
const subcommand = args[0];
|
|
5873
|
+
if (!subcommand || subcommand === "help" || subcommand === "--help" || subcommand === "-h") {
|
|
5874
|
+
showConnectHelp();
|
|
5875
|
+
return;
|
|
5876
|
+
}
|
|
5877
|
+
switch (subcommand.toLowerCase()) {
|
|
5878
|
+
case "codex":
|
|
5879
|
+
await handleConnectVendor("codex", "OpenAI");
|
|
5880
|
+
break;
|
|
5881
|
+
case "claude":
|
|
5882
|
+
await handleConnectVendor("claude", "Anthropic");
|
|
5883
|
+
break;
|
|
5884
|
+
case "gemini":
|
|
5885
|
+
await handleConnectVendor("gemini", "Gemini");
|
|
5886
|
+
break;
|
|
5887
|
+
default:
|
|
5888
|
+
console.error(chalk.red(`Unknown connect target: ${subcommand}`));
|
|
5889
|
+
showConnectHelp();
|
|
5890
|
+
process.exit(1);
|
|
5891
|
+
}
|
|
5892
|
+
}
|
|
5893
|
+
function showConnectHelp() {
|
|
5894
|
+
console.log(`
|
|
5895
|
+
${chalk.bold("happy connect")} - Connect AI vendor API keys to Happy cloud
|
|
5896
|
+
|
|
5897
|
+
${chalk.bold("Usage:")}
|
|
5898
|
+
happy connect codex Store your Codex API key in Happy cloud
|
|
5899
|
+
happy connect anthropic Store your Anthropic API key in Happy cloud
|
|
5900
|
+
happy connect gemini Store your Gemini API key in Happy cloud
|
|
5901
|
+
happy connect help Show this help message
|
|
5902
|
+
|
|
5903
|
+
${chalk.bold("Description:")}
|
|
5904
|
+
The connect command allows you to securely store your AI vendor API keys
|
|
5905
|
+
in Happy cloud. This enables you to use these services through Happy
|
|
5906
|
+
without exposing your API keys locally.
|
|
5907
|
+
|
|
5908
|
+
${chalk.bold("Examples:")}
|
|
5909
|
+
happy connect codex
|
|
5910
|
+
happy connect anthropic
|
|
5911
|
+
happy connect gemini
|
|
5912
|
+
|
|
5913
|
+
${chalk.bold("Notes:")}
|
|
5914
|
+
\u2022 You must be authenticated with Happy first (run 'happy auth login')
|
|
5915
|
+
\u2022 API keys are encrypted and stored securely in Happy cloud
|
|
5916
|
+
\u2022 You can manage your stored keys at app.happy.engineering
|
|
5917
|
+
`);
|
|
5918
|
+
}
|
|
5919
|
+
async function handleConnectVendor(vendor, displayName) {
|
|
5920
|
+
console.log(chalk.bold(`
|
|
5921
|
+
\u{1F50C} Connecting ${displayName} to Happy cloud
|
|
5922
|
+
`));
|
|
5923
|
+
const credentials = await readCredentials();
|
|
5924
|
+
if (!credentials) {
|
|
5925
|
+
console.log(chalk.yellow("\u26A0\uFE0F Not authenticated with Happy"));
|
|
5926
|
+
console.log(chalk.gray(' Please run "happy auth login" first'));
|
|
5927
|
+
process.exit(1);
|
|
5928
|
+
}
|
|
5929
|
+
const api = new ApiClient(credentials.token, credentials.secret);
|
|
5930
|
+
if (vendor === "codex") {
|
|
5931
|
+
console.log("\u{1F680} Registering Codex token with server");
|
|
5932
|
+
const codexAuthTokens = await authenticateCodex();
|
|
5933
|
+
await api.registerVendorToken("openai", { oauth: codexAuthTokens });
|
|
5934
|
+
console.log("\u2705 Codex token registered with server");
|
|
5935
|
+
process.exit(0);
|
|
5936
|
+
} else if (vendor === "claude") {
|
|
5937
|
+
console.log("\u{1F680} Registering Anthropic token with server");
|
|
5938
|
+
const anthropicAuthTokens = await authenticateClaude();
|
|
5939
|
+
await api.registerVendorToken("anthropic", { oauth: anthropicAuthTokens });
|
|
5940
|
+
console.log("\u2705 Anthropic token registered with server");
|
|
5941
|
+
process.exit(0);
|
|
5942
|
+
} else if (vendor === "gemini") {
|
|
5943
|
+
console.log("\u{1F680} Registering Gemini token with server");
|
|
5944
|
+
const geminiAuthTokens = await authenticateGemini();
|
|
5945
|
+
await api.registerVendorToken("gemini", { oauth: geminiAuthTokens });
|
|
5946
|
+
console.log("\u2705 Gemini token registered with server");
|
|
5947
|
+
process.exit(0);
|
|
5948
|
+
} else {
|
|
5949
|
+
throw new Error(`Unsupported vendor: ${vendor}`);
|
|
5950
|
+
}
|
|
5951
|
+
}
|
|
5354
5952
|
|
|
5355
5953
|
(async () => {
|
|
5356
5954
|
const args = process.argv.slice(2);
|
|
@@ -5380,6 +5978,17 @@ const DaemonPrompt = ({ onSelect }) => {
|
|
|
5380
5978
|
process.exit(1);
|
|
5381
5979
|
}
|
|
5382
5980
|
return;
|
|
5981
|
+
} else if (subcommand === "connect") {
|
|
5982
|
+
try {
|
|
5983
|
+
await handleConnectCommand(args.slice(1));
|
|
5984
|
+
} catch (error) {
|
|
5985
|
+
console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
|
|
5986
|
+
if (process.env.DEBUG) {
|
|
5987
|
+
console.error(error);
|
|
5988
|
+
}
|
|
5989
|
+
process.exit(1);
|
|
5990
|
+
}
|
|
5991
|
+
return;
|
|
5383
5992
|
} else if (subcommand === "logout") {
|
|
5384
5993
|
console.log(chalk.yellow('Note: "happy logout" is deprecated. Use "happy auth logout" instead.\n'));
|
|
5385
5994
|
try {
|
|
@@ -5516,7 +6125,7 @@ ${chalk.bold("To clean up runaway processes:")} Use ${chalk.cyan("happy doctor c
|
|
|
5516
6125
|
} else if (arg === "-v" || arg === "--version") {
|
|
5517
6126
|
showVersion = true;
|
|
5518
6127
|
unknownArgs.push(arg);
|
|
5519
|
-
} else if (arg === "--
|
|
6128
|
+
} else if (arg === "--happy-starting-mode") {
|
|
5520
6129
|
options.startingMode = z.enum(["local", "remote"]).parse(args[++i]);
|
|
5521
6130
|
} else if (arg === "--yolo") {
|
|
5522
6131
|
unknownArgs.push("--dangerously-skip-permissions");
|
|
@@ -5545,22 +6154,21 @@ ${chalk.bold("Usage:")}
|
|
|
5545
6154
|
|
|
5546
6155
|
${chalk.bold("Examples:")}
|
|
5547
6156
|
happy Start session
|
|
5548
|
-
happy --yolo
|
|
5549
|
-
|
|
6157
|
+
happy --yolo Start with bypassing permissions
|
|
6158
|
+
happy sugar for --dangerously-skip-permissions
|
|
5550
6159
|
happy auth login --force Authenticate
|
|
5551
6160
|
happy doctor Run diagnostics
|
|
5552
6161
|
|
|
5553
|
-
${chalk.bold("Happy is a wrapper around Claude Code that enables remote control via mobile app.")}
|
|
5554
|
-
|
|
5555
6162
|
${chalk.bold("Happy supports ALL Claude options!")}
|
|
5556
|
-
Use any claude flag
|
|
6163
|
+
Use any claude flag with happy as you would with claude. Our favorite:
|
|
6164
|
+
|
|
6165
|
+
happy --resume
|
|
5557
6166
|
|
|
5558
6167
|
${chalk.gray("\u2500".repeat(60))}
|
|
5559
6168
|
${chalk.bold.cyan("Claude Code Options (from `claude --help`):")}
|
|
5560
6169
|
`);
|
|
5561
|
-
const { execSync } = await import('child_process');
|
|
5562
6170
|
try {
|
|
5563
|
-
const claudeHelp =
|
|
6171
|
+
const claudeHelp = execFileSync(process.execPath, [claudeCliPath, "--help"], { encoding: "utf8" });
|
|
5564
6172
|
console.log(claudeHelp);
|
|
5565
6173
|
} catch (e) {
|
|
5566
6174
|
console.log(chalk.yellow("Could not retrieve claude help. Make sure claude is installed."));
|
|
@@ -5568,53 +6176,21 @@ ${chalk.bold.cyan("Claude Code Options (from `claude --help`):")}
|
|
|
5568
6176
|
process.exit(0);
|
|
5569
6177
|
}
|
|
5570
6178
|
if (showVersion) {
|
|
5571
|
-
console.log(packageJson.version);
|
|
5572
|
-
process.exit(0);
|
|
6179
|
+
console.log(`happy version: ${packageJson.version}`);
|
|
5573
6180
|
}
|
|
5574
6181
|
const {
|
|
5575
6182
|
credentials
|
|
5576
6183
|
} = await authAndSetupMachineIfNeeded();
|
|
5577
|
-
|
|
5578
|
-
if (
|
|
5579
|
-
|
|
5580
|
-
|
|
5581
|
-
|
|
5582
|
-
|
|
5583
|
-
|
|
5584
|
-
app.unmount();
|
|
5585
|
-
resolve(autoStart);
|
|
5586
|
-
}
|
|
5587
|
-
};
|
|
5588
|
-
const app = render(React.createElement(DaemonPrompt, { onSelect }), {
|
|
5589
|
-
exitOnCtrlC: false,
|
|
5590
|
-
patchConsole: false
|
|
5591
|
-
});
|
|
6184
|
+
logger.debug("Ensuring Happy background service is running & matches our version...");
|
|
6185
|
+
if (!await isDaemonRunningCurrentlyInstalledHappyVersion()) {
|
|
6186
|
+
logger.debug("Starting Happy background service...");
|
|
6187
|
+
const daemonProcess = spawnHappyCLI(["daemon", "start-sync"], {
|
|
6188
|
+
detached: true,
|
|
6189
|
+
stdio: "ignore",
|
|
6190
|
+
env: process.env
|
|
5592
6191
|
});
|
|
5593
|
-
|
|
5594
|
-
|
|
5595
|
-
daemonAutoStartWhenRunningHappy: shouldAutoStart
|
|
5596
|
-
}));
|
|
5597
|
-
if (shouldAutoStart) {
|
|
5598
|
-
console.log(chalk.green("\n\u2713 Happy will start the background service automatically"));
|
|
5599
|
-
console.log(chalk.gray(" The service will run whenever you use the happy command"));
|
|
5600
|
-
} else {
|
|
5601
|
-
console.log(chalk.yellow("\n You can enable this later by running: happy daemon install"));
|
|
5602
|
-
}
|
|
5603
|
-
}
|
|
5604
|
-
if (settings && settings.daemonAutoStartWhenRunningHappy) {
|
|
5605
|
-
logger.debug("Ensuring Happy background service is running & matches our version...");
|
|
5606
|
-
if (!await isDaemonRunningSameVersion()) {
|
|
5607
|
-
logger.debug("Starting Happy background service...");
|
|
5608
|
-
const daemonProcess = spawnHappyCLI(["daemon", "start-sync"], {
|
|
5609
|
-
detached: true,
|
|
5610
|
-
stdio: "ignore",
|
|
5611
|
-
env: process.env
|
|
5612
|
-
});
|
|
5613
|
-
daemonProcess.unref();
|
|
5614
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
5615
|
-
} else {
|
|
5616
|
-
logger.debug("Happy background service is running & matches our version");
|
|
5617
|
-
}
|
|
6192
|
+
daemonProcess.unref();
|
|
6193
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
5618
6194
|
}
|
|
5619
6195
|
try {
|
|
5620
6196
|
await start(credentials, options);
|