happy-coder 0.9.1 → 0.10.0-1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +785 -521
- package/dist/index.mjs +786 -522
- package/dist/lib.cjs +7 -1
- package/dist/lib.d.cts +99 -34
- package/dist/lib.d.mts +99 -34
- package/dist/lib.mjs +7 -1
- package/dist/{types-BS8Pr3Im.mjs → types-BUXwivpV.mjs} +437 -142
- package/dist/{types-DNUk09Np.cjs → types-D9P2bndj.cjs} +438 -141
- package/package.json +5 -1
package/dist/index.cjs
CHANGED
|
@@ -3,36 +3,39 @@
|
|
|
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-D9P2bndj.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
|
-
var path = require('path');
|
|
13
|
-
var url = require('url');
|
|
14
11
|
var promises = require('node:fs/promises');
|
|
15
12
|
var fs = require('fs/promises');
|
|
16
13
|
var ink = require('ink');
|
|
17
14
|
var React = require('react');
|
|
15
|
+
var node_url = require('node:url');
|
|
18
16
|
var axios = require('axios');
|
|
19
17
|
require('node:events');
|
|
20
18
|
require('socket.io-client');
|
|
21
19
|
var tweetnacl = require('tweetnacl');
|
|
22
20
|
require('expo-server-sdk');
|
|
23
|
-
var child_process = require('child_process');
|
|
24
|
-
var util = require('util');
|
|
25
21
|
var crypto = require('crypto');
|
|
22
|
+
var child_process = require('child_process');
|
|
23
|
+
var fs$1 = require('fs');
|
|
24
|
+
var path = require('path');
|
|
25
|
+
var psList = require('ps-list');
|
|
26
|
+
var spawn = require('cross-spawn');
|
|
26
27
|
var os$1 = require('os');
|
|
27
28
|
var qrcode = require('qrcode-terminal');
|
|
28
29
|
var open = require('open');
|
|
29
30
|
var fastify = require('fastify');
|
|
30
31
|
var z = require('zod');
|
|
31
32
|
var fastifyTypeProviderZod = require('fastify-type-provider-zod');
|
|
32
|
-
var fs$1 = require('fs');
|
|
33
33
|
var mcp_js = require('@modelcontextprotocol/sdk/server/mcp.js');
|
|
34
34
|
var node_http = require('node:http');
|
|
35
35
|
var streamableHttp_js = require('@modelcontextprotocol/sdk/server/streamableHttp.js');
|
|
36
|
+
var http = require('http');
|
|
37
|
+
var util = require('util');
|
|
38
|
+
require('url');
|
|
36
39
|
|
|
37
40
|
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
38
41
|
class Session {
|
|
@@ -145,12 +148,6 @@ function claudeCheckSession(sessionId, path) {
|
|
|
145
148
|
return hasGoodMessage;
|
|
146
149
|
}
|
|
147
150
|
|
|
148
|
-
const __dirname$2 = path.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href))));
|
|
149
|
-
function projectPath() {
|
|
150
|
-
const path$1 = path.resolve(__dirname$2, "..");
|
|
151
|
-
return path$1;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
151
|
function trimIdent(text) {
|
|
155
152
|
const lines = text.split("\n");
|
|
156
153
|
while (lines.length > 0 && lines[0].trim() === "") {
|
|
@@ -184,7 +181,7 @@ const systemPrompt = trimIdent(`
|
|
|
184
181
|
Co-Authored-By: Happy <yesreply@happy.engineering>
|
|
185
182
|
`);
|
|
186
183
|
|
|
187
|
-
|
|
184
|
+
const claudeCliPath = node_path.resolve(node_path.join(types.projectPath(), "scripts", "claude_local_launcher.cjs"));
|
|
188
185
|
async function claudeLocal(opts) {
|
|
189
186
|
const projectDir = getProjectPath(opts.path);
|
|
190
187
|
node_fs.mkdirSync(projectDir, { recursive: true });
|
|
@@ -241,7 +238,6 @@ async function claudeLocal(opts) {
|
|
|
241
238
|
if (opts.claudeArgs) {
|
|
242
239
|
args.push(...opts.claudeArgs);
|
|
243
240
|
}
|
|
244
|
-
const claudeCliPath = node_path.resolve(node_path.join(projectPath(), "scripts", "claude_local_launcher.cjs"));
|
|
245
241
|
if (!claudeCliPath || !node_fs.existsSync(claudeCliPath)) {
|
|
246
242
|
throw new Error("Claude local launcher not found. Please ensure HAPPY_PROJECT_ROOT is set correctly for development.");
|
|
247
243
|
}
|
|
@@ -612,8 +608,8 @@ async function claudeLocalLauncher(session) {
|
|
|
612
608
|
}
|
|
613
609
|
await abort();
|
|
614
610
|
}
|
|
615
|
-
session.client.
|
|
616
|
-
session.client.
|
|
611
|
+
session.client.rpcHandlerManager.registerHandler("abort", doAbort);
|
|
612
|
+
session.client.rpcHandlerManager.registerHandler("switch", doSwitch);
|
|
617
613
|
session.queue.setOnMessage((message, mode) => {
|
|
618
614
|
doSwitch();
|
|
619
615
|
});
|
|
@@ -659,9 +655,9 @@ async function claudeLocalLauncher(session) {
|
|
|
659
655
|
}
|
|
660
656
|
} finally {
|
|
661
657
|
exutFuture.resolve(void 0);
|
|
662
|
-
session.client.
|
|
658
|
+
session.client.rpcHandlerManager.registerHandler("abort", async () => {
|
|
663
659
|
});
|
|
664
|
-
session.client.
|
|
660
|
+
session.client.rpcHandlerManager.registerHandler("switch", async () => {
|
|
665
661
|
});
|
|
666
662
|
session.queue.setOnMessage(null);
|
|
667
663
|
await scanner.cleanup();
|
|
@@ -1508,7 +1504,7 @@ async function claudeRemote(opts) {
|
|
|
1508
1504
|
executable: "node",
|
|
1509
1505
|
abort: opts.signal,
|
|
1510
1506
|
pathToClaudeCodeExecutable: (() => {
|
|
1511
|
-
return node_path.resolve(node_path.join(projectPath(), "scripts", "claude_remote_launcher.cjs"));
|
|
1507
|
+
return node_path.resolve(node_path.join(types.projectPath(), "scripts", "claude_remote_launcher.cjs"));
|
|
1512
1508
|
})()
|
|
1513
1509
|
};
|
|
1514
1510
|
let thinking = false;
|
|
@@ -1923,7 +1919,7 @@ class PermissionHandler {
|
|
|
1923
1919
|
* Sets up the client handler for permission responses
|
|
1924
1920
|
*/
|
|
1925
1921
|
setupClientHandler() {
|
|
1926
|
-
this.session.client.
|
|
1922
|
+
this.session.client.rpcHandlerManager.registerHandler("permission", async (message) => {
|
|
1927
1923
|
types.logger.debug(`Permission response: ${JSON.stringify(message)}`);
|
|
1928
1924
|
const id = message.id;
|
|
1929
1925
|
const pending = this.pendingRequests.get(id);
|
|
@@ -2493,8 +2489,8 @@ async function claudeRemoteLauncher(session) {
|
|
|
2493
2489
|
}
|
|
2494
2490
|
await abort();
|
|
2495
2491
|
}
|
|
2496
|
-
session.client.
|
|
2497
|
-
session.client.
|
|
2492
|
+
session.client.rpcHandlerManager.registerHandler("abort", doAbort);
|
|
2493
|
+
session.client.rpcHandlerManager.registerHandler("switch", doSwitch);
|
|
2498
2494
|
const permissionHandler = new PermissionHandler(session);
|
|
2499
2495
|
const messageQueue = new OutgoingMessageQueue(
|
|
2500
2496
|
(logMessage) => session.client.sendClaudeSessionMessage(logMessage)
|
|
@@ -2810,265 +2806,6 @@ async function loop(opts) {
|
|
|
2810
2806
|
}
|
|
2811
2807
|
}
|
|
2812
2808
|
|
|
2813
|
-
function run(args, options) {
|
|
2814
|
-
const RUNNER_PATH = path.resolve(path.join(projectPath(), "scripts", "ripgrep_launcher.cjs"));
|
|
2815
|
-
return new Promise((resolve2, reject) => {
|
|
2816
|
-
const child = child_process.spawn("node", [RUNNER_PATH, JSON.stringify(args)], {
|
|
2817
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
2818
|
-
cwd: options?.cwd
|
|
2819
|
-
});
|
|
2820
|
-
let stdout = "";
|
|
2821
|
-
let stderr = "";
|
|
2822
|
-
child.stdout.on("data", (data) => {
|
|
2823
|
-
stdout += data.toString();
|
|
2824
|
-
});
|
|
2825
|
-
child.stderr.on("data", (data) => {
|
|
2826
|
-
stderr += data.toString();
|
|
2827
|
-
});
|
|
2828
|
-
child.on("close", (code) => {
|
|
2829
|
-
resolve2({
|
|
2830
|
-
exitCode: code || 0,
|
|
2831
|
-
stdout,
|
|
2832
|
-
stderr
|
|
2833
|
-
});
|
|
2834
|
-
});
|
|
2835
|
-
child.on("error", (err) => {
|
|
2836
|
-
reject(err);
|
|
2837
|
-
});
|
|
2838
|
-
});
|
|
2839
|
-
}
|
|
2840
|
-
|
|
2841
|
-
const execAsync = util.promisify(child_process.exec);
|
|
2842
|
-
function registerHandlers(session) {
|
|
2843
|
-
session.setHandler("bash", async (data) => {
|
|
2844
|
-
types.logger.debug("Shell command request:", data.command);
|
|
2845
|
-
try {
|
|
2846
|
-
const options = {
|
|
2847
|
-
cwd: data.cwd,
|
|
2848
|
-
timeout: data.timeout || 3e4
|
|
2849
|
-
// Default 30 seconds timeout
|
|
2850
|
-
};
|
|
2851
|
-
const { stdout, stderr } = await execAsync(data.command, options);
|
|
2852
|
-
return {
|
|
2853
|
-
success: true,
|
|
2854
|
-
stdout: stdout ? stdout.toString() : "",
|
|
2855
|
-
stderr: stderr ? stderr.toString() : "",
|
|
2856
|
-
exitCode: 0
|
|
2857
|
-
};
|
|
2858
|
-
} catch (error) {
|
|
2859
|
-
const execError = error;
|
|
2860
|
-
if (execError.code === "ETIMEDOUT" || execError.killed) {
|
|
2861
|
-
return {
|
|
2862
|
-
success: false,
|
|
2863
|
-
stdout: execError.stdout || "",
|
|
2864
|
-
stderr: execError.stderr || "",
|
|
2865
|
-
exitCode: typeof execError.code === "number" ? execError.code : -1,
|
|
2866
|
-
error: "Command timed out"
|
|
2867
|
-
};
|
|
2868
|
-
}
|
|
2869
|
-
return {
|
|
2870
|
-
success: false,
|
|
2871
|
-
stdout: execError.stdout ? execError.stdout.toString() : "",
|
|
2872
|
-
stderr: execError.stderr ? execError.stderr.toString() : execError.message || "Command failed",
|
|
2873
|
-
exitCode: typeof execError.code === "number" ? execError.code : 1,
|
|
2874
|
-
error: execError.message || "Command failed"
|
|
2875
|
-
};
|
|
2876
|
-
}
|
|
2877
|
-
});
|
|
2878
|
-
session.setHandler("readFile", async (data) => {
|
|
2879
|
-
types.logger.debug("Read file request:", data.path);
|
|
2880
|
-
try {
|
|
2881
|
-
const buffer = await fs.readFile(data.path);
|
|
2882
|
-
const content = buffer.toString("base64");
|
|
2883
|
-
return { success: true, content };
|
|
2884
|
-
} catch (error) {
|
|
2885
|
-
types.logger.debug("Failed to read file:", error);
|
|
2886
|
-
return { success: false, error: error instanceof Error ? error.message : "Failed to read file" };
|
|
2887
|
-
}
|
|
2888
|
-
});
|
|
2889
|
-
session.setHandler("writeFile", async (data) => {
|
|
2890
|
-
types.logger.debug("Write file request:", data.path);
|
|
2891
|
-
try {
|
|
2892
|
-
if (data.expectedHash !== null && data.expectedHash !== void 0) {
|
|
2893
|
-
try {
|
|
2894
|
-
const existingBuffer = await fs.readFile(data.path);
|
|
2895
|
-
const existingHash = crypto.createHash("sha256").update(existingBuffer).digest("hex");
|
|
2896
|
-
if (existingHash !== data.expectedHash) {
|
|
2897
|
-
return {
|
|
2898
|
-
success: false,
|
|
2899
|
-
error: `File hash mismatch. Expected: ${data.expectedHash}, Actual: ${existingHash}`
|
|
2900
|
-
};
|
|
2901
|
-
}
|
|
2902
|
-
} catch (error) {
|
|
2903
|
-
const nodeError = error;
|
|
2904
|
-
if (nodeError.code !== "ENOENT") {
|
|
2905
|
-
throw error;
|
|
2906
|
-
}
|
|
2907
|
-
return {
|
|
2908
|
-
success: false,
|
|
2909
|
-
error: "File does not exist but hash was provided"
|
|
2910
|
-
};
|
|
2911
|
-
}
|
|
2912
|
-
} else {
|
|
2913
|
-
try {
|
|
2914
|
-
await fs.stat(data.path);
|
|
2915
|
-
return {
|
|
2916
|
-
success: false,
|
|
2917
|
-
error: "File already exists but was expected to be new"
|
|
2918
|
-
};
|
|
2919
|
-
} catch (error) {
|
|
2920
|
-
const nodeError = error;
|
|
2921
|
-
if (nodeError.code !== "ENOENT") {
|
|
2922
|
-
throw error;
|
|
2923
|
-
}
|
|
2924
|
-
}
|
|
2925
|
-
}
|
|
2926
|
-
const buffer = Buffer.from(data.content, "base64");
|
|
2927
|
-
await fs.writeFile(data.path, buffer);
|
|
2928
|
-
const hash = crypto.createHash("sha256").update(buffer).digest("hex");
|
|
2929
|
-
return { success: true, hash };
|
|
2930
|
-
} catch (error) {
|
|
2931
|
-
types.logger.debug("Failed to write file:", error);
|
|
2932
|
-
return { success: false, error: error instanceof Error ? error.message : "Failed to write file" };
|
|
2933
|
-
}
|
|
2934
|
-
});
|
|
2935
|
-
session.setHandler("listDirectory", async (data) => {
|
|
2936
|
-
types.logger.debug("List directory request:", data.path);
|
|
2937
|
-
try {
|
|
2938
|
-
const entries = await fs.readdir(data.path, { withFileTypes: true });
|
|
2939
|
-
const directoryEntries = await Promise.all(
|
|
2940
|
-
entries.map(async (entry) => {
|
|
2941
|
-
const fullPath = path.join(data.path, entry.name);
|
|
2942
|
-
let type = "other";
|
|
2943
|
-
let size;
|
|
2944
|
-
let modified;
|
|
2945
|
-
if (entry.isDirectory()) {
|
|
2946
|
-
type = "directory";
|
|
2947
|
-
} else if (entry.isFile()) {
|
|
2948
|
-
type = "file";
|
|
2949
|
-
}
|
|
2950
|
-
try {
|
|
2951
|
-
const stats = await fs.stat(fullPath);
|
|
2952
|
-
size = stats.size;
|
|
2953
|
-
modified = stats.mtime.getTime();
|
|
2954
|
-
} catch (error) {
|
|
2955
|
-
types.logger.debug(`Failed to stat ${fullPath}:`, error);
|
|
2956
|
-
}
|
|
2957
|
-
return {
|
|
2958
|
-
name: entry.name,
|
|
2959
|
-
type,
|
|
2960
|
-
size,
|
|
2961
|
-
modified
|
|
2962
|
-
};
|
|
2963
|
-
})
|
|
2964
|
-
);
|
|
2965
|
-
directoryEntries.sort((a, b) => {
|
|
2966
|
-
if (a.type === "directory" && b.type !== "directory") return -1;
|
|
2967
|
-
if (a.type !== "directory" && b.type === "directory") return 1;
|
|
2968
|
-
return a.name.localeCompare(b.name);
|
|
2969
|
-
});
|
|
2970
|
-
return { success: true, entries: directoryEntries };
|
|
2971
|
-
} catch (error) {
|
|
2972
|
-
types.logger.debug("Failed to list directory:", error);
|
|
2973
|
-
return { success: false, error: error instanceof Error ? error.message : "Failed to list directory" };
|
|
2974
|
-
}
|
|
2975
|
-
});
|
|
2976
|
-
session.setHandler("getDirectoryTree", async (data) => {
|
|
2977
|
-
types.logger.debug("Get directory tree request:", data.path, "maxDepth:", data.maxDepth);
|
|
2978
|
-
async function buildTree(path$1, name, currentDepth) {
|
|
2979
|
-
try {
|
|
2980
|
-
const stats = await fs.stat(path$1);
|
|
2981
|
-
const node = {
|
|
2982
|
-
name,
|
|
2983
|
-
path: path$1,
|
|
2984
|
-
type: stats.isDirectory() ? "directory" : "file",
|
|
2985
|
-
size: stats.size,
|
|
2986
|
-
modified: stats.mtime.getTime()
|
|
2987
|
-
};
|
|
2988
|
-
if (stats.isDirectory() && currentDepth < data.maxDepth) {
|
|
2989
|
-
const entries = await fs.readdir(path$1, { withFileTypes: true });
|
|
2990
|
-
const children = [];
|
|
2991
|
-
await Promise.all(
|
|
2992
|
-
entries.map(async (entry) => {
|
|
2993
|
-
if (entry.isSymbolicLink()) {
|
|
2994
|
-
types.logger.debug(`Skipping symlink: ${path.join(path$1, entry.name)}`);
|
|
2995
|
-
return;
|
|
2996
|
-
}
|
|
2997
|
-
const childPath = path.join(path$1, entry.name);
|
|
2998
|
-
const childNode = await buildTree(childPath, entry.name, currentDepth + 1);
|
|
2999
|
-
if (childNode) {
|
|
3000
|
-
children.push(childNode);
|
|
3001
|
-
}
|
|
3002
|
-
})
|
|
3003
|
-
);
|
|
3004
|
-
children.sort((a, b) => {
|
|
3005
|
-
if (a.type === "directory" && b.type !== "directory") return -1;
|
|
3006
|
-
if (a.type !== "directory" && b.type === "directory") return 1;
|
|
3007
|
-
return a.name.localeCompare(b.name);
|
|
3008
|
-
});
|
|
3009
|
-
node.children = children;
|
|
3010
|
-
}
|
|
3011
|
-
return node;
|
|
3012
|
-
} catch (error) {
|
|
3013
|
-
types.logger.debug(`Failed to process ${path$1}:`, error instanceof Error ? error.message : String(error));
|
|
3014
|
-
return null;
|
|
3015
|
-
}
|
|
3016
|
-
}
|
|
3017
|
-
try {
|
|
3018
|
-
if (data.maxDepth < 0) {
|
|
3019
|
-
return { success: false, error: "maxDepth must be non-negative" };
|
|
3020
|
-
}
|
|
3021
|
-
const baseName = data.path === "/" ? "/" : data.path.split("/").pop() || data.path;
|
|
3022
|
-
const tree = await buildTree(data.path, baseName, 0);
|
|
3023
|
-
if (!tree) {
|
|
3024
|
-
return { success: false, error: "Failed to access the specified path" };
|
|
3025
|
-
}
|
|
3026
|
-
return { success: true, tree };
|
|
3027
|
-
} catch (error) {
|
|
3028
|
-
types.logger.debug("Failed to get directory tree:", error);
|
|
3029
|
-
return { success: false, error: error instanceof Error ? error.message : "Failed to get directory tree" };
|
|
3030
|
-
}
|
|
3031
|
-
});
|
|
3032
|
-
session.setHandler("ripgrep", async (data) => {
|
|
3033
|
-
types.logger.debug("Ripgrep request with args:", data.args, "cwd:", data.cwd);
|
|
3034
|
-
try {
|
|
3035
|
-
const result = await run(data.args, { cwd: data.cwd });
|
|
3036
|
-
return {
|
|
3037
|
-
success: true,
|
|
3038
|
-
exitCode: result.exitCode,
|
|
3039
|
-
stdout: result.stdout.toString(),
|
|
3040
|
-
stderr: result.stderr.toString()
|
|
3041
|
-
};
|
|
3042
|
-
} catch (error) {
|
|
3043
|
-
types.logger.debug("Failed to run ripgrep:", error);
|
|
3044
|
-
return {
|
|
3045
|
-
success: false,
|
|
3046
|
-
error: error instanceof Error ? error.message : "Failed to run ripgrep"
|
|
3047
|
-
};
|
|
3048
|
-
}
|
|
3049
|
-
});
|
|
3050
|
-
session.setHandler("killSession", async () => {
|
|
3051
|
-
types.logger.debug("Kill session request received");
|
|
3052
|
-
try {
|
|
3053
|
-
const response = {
|
|
3054
|
-
success: true,
|
|
3055
|
-
message: "Session termination acknowledged, exiting in 100ms"
|
|
3056
|
-
};
|
|
3057
|
-
setTimeout(() => {
|
|
3058
|
-
types.logger.debug("[KILL SESSION] Exiting process as requested");
|
|
3059
|
-
process.exit(0);
|
|
3060
|
-
}, 100);
|
|
3061
|
-
return response;
|
|
3062
|
-
} catch (error) {
|
|
3063
|
-
types.logger.debug("Failed to kill session:", error);
|
|
3064
|
-
return {
|
|
3065
|
-
success: false,
|
|
3066
|
-
message: error instanceof Error ? error.message : "Failed to kill session"
|
|
3067
|
-
};
|
|
3068
|
-
}
|
|
3069
|
-
});
|
|
3070
|
-
}
|
|
3071
|
-
|
|
3072
2809
|
class MessageQueue2 {
|
|
3073
2810
|
queue = [];
|
|
3074
2811
|
// Made public for testing
|
|
@@ -3602,7 +3339,7 @@ async function checkIfDaemonRunningAndCleanupStaleState() {
|
|
|
3602
3339
|
return false;
|
|
3603
3340
|
}
|
|
3604
3341
|
}
|
|
3605
|
-
async function
|
|
3342
|
+
async function isDaemonRunningCurrentlyInstalledHappyVersion() {
|
|
3606
3343
|
types.logger.debug("[DAEMON CONTROL] Checking if daemon is running same version");
|
|
3607
3344
|
const runningDaemon = await checkIfDaemonRunningAndCleanupStaleState();
|
|
3608
3345
|
if (!runningDaemon) {
|
|
@@ -3615,8 +3352,11 @@ async function isDaemonRunningSameVersion() {
|
|
|
3615
3352
|
return false;
|
|
3616
3353
|
}
|
|
3617
3354
|
try {
|
|
3618
|
-
|
|
3619
|
-
|
|
3355
|
+
const packageJsonPath = path.join(types.projectPath(), "package.json");
|
|
3356
|
+
const packageJson = JSON.parse(fs$1.readFileSync(packageJsonPath, "utf-8"));
|
|
3357
|
+
const currentCliVersion = packageJson.version;
|
|
3358
|
+
types.logger.debug(`[DAEMON CONTROL] Current CLI version: ${currentCliVersion}, Daemon started with version: ${state.startedWithCliVersion}`);
|
|
3359
|
+
return currentCliVersion === state.startedWithCliVersion;
|
|
3620
3360
|
} catch (error) {
|
|
3621
3361
|
types.logger.debug("[DAEMON CONTROL] Error checking daemon version", error);
|
|
3622
3362
|
return false;
|
|
@@ -3669,137 +3409,73 @@ async function waitForProcessDeath(pid, timeout) {
|
|
|
3669
3409
|
throw new Error("Process did not die within timeout");
|
|
3670
3410
|
}
|
|
3671
3411
|
|
|
3672
|
-
function findAllHappyProcesses() {
|
|
3412
|
+
async function findAllHappyProcesses() {
|
|
3673
3413
|
try {
|
|
3414
|
+
const processes = await psList();
|
|
3674
3415
|
const allProcesses = [];
|
|
3675
|
-
|
|
3676
|
-
const
|
|
3677
|
-
const
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
type = "user-session";
|
|
3696
|
-
}
|
|
3697
|
-
allProcesses.push({ pid, command, type });
|
|
3698
|
-
}
|
|
3699
|
-
} catch {
|
|
3700
|
-
}
|
|
3701
|
-
try {
|
|
3702
|
-
const devOutput = node_child_process.execSync('ps aux | grep -E "tsx.*src/index\\.ts" | grep -v grep', { encoding: "utf8" });
|
|
3703
|
-
const devLines = devOutput.trim().split("\n").filter((line) => line.trim());
|
|
3704
|
-
for (const line of devLines) {
|
|
3705
|
-
const parts = line.trim().split(/\s+/);
|
|
3706
|
-
if (parts.length < 11) continue;
|
|
3707
|
-
const pid = parseInt(parts[1]);
|
|
3708
|
-
const command = parts.slice(10).join(" ");
|
|
3709
|
-
if (!command.includes("happy-cli/node_modules/tsx") && !command.includes("/bin/tsx src/index.ts")) {
|
|
3710
|
-
continue;
|
|
3711
|
-
}
|
|
3712
|
-
let type = "unknown";
|
|
3713
|
-
if (pid === process.pid) {
|
|
3714
|
-
type = "current";
|
|
3715
|
-
} else if (command.includes("--version")) {
|
|
3716
|
-
type = "dev-daemon-version-check";
|
|
3717
|
-
} else if (command.includes("daemon start-sync") || command.includes("daemon start")) {
|
|
3718
|
-
type = "dev-daemon";
|
|
3719
|
-
} else if (command.includes("--started-by daemon")) {
|
|
3720
|
-
type = "dev-daemon-spawned";
|
|
3721
|
-
} else if (command.includes("doctor")) {
|
|
3722
|
-
type = "dev-doctor";
|
|
3723
|
-
} else if (command.includes("--yolo")) {
|
|
3724
|
-
type = "dev-session";
|
|
3725
|
-
} else {
|
|
3726
|
-
type = "dev-related";
|
|
3727
|
-
}
|
|
3728
|
-
allProcesses.push({ pid, command, type });
|
|
3416
|
+
for (const proc of processes) {
|
|
3417
|
+
const cmd = proc.cmd || "";
|
|
3418
|
+
const name = proc.name || "";
|
|
3419
|
+
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");
|
|
3420
|
+
if (!isHappy) continue;
|
|
3421
|
+
let type = "unknown";
|
|
3422
|
+
if (proc.pid === process.pid) {
|
|
3423
|
+
type = "current";
|
|
3424
|
+
} else if (cmd.includes("--version")) {
|
|
3425
|
+
type = cmd.includes("tsx") ? "dev-daemon-version-check" : "daemon-version-check";
|
|
3426
|
+
} else if (cmd.includes("daemon start-sync") || cmd.includes("daemon start")) {
|
|
3427
|
+
type = cmd.includes("tsx") ? "dev-daemon" : "daemon";
|
|
3428
|
+
} else if (cmd.includes("--started-by daemon")) {
|
|
3429
|
+
type = cmd.includes("tsx") ? "dev-daemon-spawned" : "daemon-spawned-session";
|
|
3430
|
+
} else if (cmd.includes("doctor")) {
|
|
3431
|
+
type = cmd.includes("tsx") ? "dev-doctor" : "doctor";
|
|
3432
|
+
} else if (cmd.includes("--yolo")) {
|
|
3433
|
+
type = "dev-session";
|
|
3434
|
+
} else {
|
|
3435
|
+
type = cmd.includes("tsx") ? "dev-related" : "user-session";
|
|
3729
3436
|
}
|
|
3730
|
-
|
|
3437
|
+
allProcesses.push({ pid: proc.pid, command: cmd || name, type });
|
|
3731
3438
|
}
|
|
3732
3439
|
return allProcesses;
|
|
3733
3440
|
} catch (error) {
|
|
3734
3441
|
return [];
|
|
3735
3442
|
}
|
|
3736
3443
|
}
|
|
3737
|
-
function findRunawayHappyProcesses() {
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
const lines = output.trim().split("\n").filter((line) => line.trim());
|
|
3743
|
-
for (const line of lines) {
|
|
3744
|
-
const parts = line.trim().split(/\s+/);
|
|
3745
|
-
if (parts.length < 11) continue;
|
|
3746
|
-
const pid = parseInt(parts[1]);
|
|
3747
|
-
const command = parts.slice(10).join(" ");
|
|
3748
|
-
if (pid === process.pid) continue;
|
|
3749
|
-
if (command.includes("--started-by daemon") || command.includes("daemon start-sync") || command.includes("daemon start") || command.includes("--version")) {
|
|
3750
|
-
processes.push({ pid, command });
|
|
3751
|
-
}
|
|
3752
|
-
}
|
|
3753
|
-
} catch {
|
|
3754
|
-
}
|
|
3755
|
-
try {
|
|
3756
|
-
const devOutput = node_child_process.execSync('ps aux | grep -E "tsx.*src/index\\.ts" | grep -v grep', { encoding: "utf8" });
|
|
3757
|
-
const devLines = devOutput.trim().split("\n").filter((line) => line.trim());
|
|
3758
|
-
for (const line of devLines) {
|
|
3759
|
-
const parts = line.trim().split(/\s+/);
|
|
3760
|
-
if (parts.length < 11) continue;
|
|
3761
|
-
const pid = parseInt(parts[1]);
|
|
3762
|
-
const command = parts.slice(10).join(" ");
|
|
3763
|
-
if (pid === process.pid) continue;
|
|
3764
|
-
if (!command.includes("happy-cli/node_modules/tsx") && !command.includes("/bin/tsx src/index.ts")) {
|
|
3765
|
-
continue;
|
|
3766
|
-
}
|
|
3767
|
-
if (command.includes("--started-by daemon") || command.includes("daemon start-sync") || command.includes("daemon start") || command.includes("--version")) {
|
|
3768
|
-
processes.push({ pid, command });
|
|
3769
|
-
}
|
|
3770
|
-
}
|
|
3771
|
-
} catch {
|
|
3772
|
-
}
|
|
3773
|
-
return processes;
|
|
3774
|
-
} catch (error) {
|
|
3775
|
-
return [];
|
|
3776
|
-
}
|
|
3444
|
+
async function findRunawayHappyProcesses() {
|
|
3445
|
+
const allProcesses = await findAllHappyProcesses();
|
|
3446
|
+
return allProcesses.filter(
|
|
3447
|
+
(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")
|
|
3448
|
+
).map((p) => ({ pid: p.pid, command: p.command }));
|
|
3777
3449
|
}
|
|
3778
3450
|
async function killRunawayHappyProcesses() {
|
|
3779
|
-
const runawayProcesses = findRunawayHappyProcesses();
|
|
3451
|
+
const runawayProcesses = await findRunawayHappyProcesses();
|
|
3780
3452
|
const errors = [];
|
|
3781
|
-
|
|
3453
|
+
let killed = 0;
|
|
3454
|
+
for (const { pid, command } of runawayProcesses) {
|
|
3782
3455
|
try {
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
process.kill(pid, "
|
|
3790
|
-
|
|
3456
|
+
console.log(`Killing runaway process PID ${pid}: ${command}`);
|
|
3457
|
+
if (process.platform === "win32") {
|
|
3458
|
+
const result = spawn.sync("taskkill", ["/F", "/PID", pid.toString()], { stdio: "pipe" });
|
|
3459
|
+
if (result.error) throw result.error;
|
|
3460
|
+
if (result.status !== 0) throw new Error(`taskkill exited with code ${result.status}`);
|
|
3461
|
+
} else {
|
|
3462
|
+
process.kill(pid, "SIGTERM");
|
|
3463
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
3464
|
+
const processes = await psList();
|
|
3465
|
+
const stillAlive = processes.find((p) => p.pid === pid);
|
|
3466
|
+
if (stillAlive) {
|
|
3467
|
+
console.log(`Process PID ${pid} ignored SIGTERM, using SIGKILL`);
|
|
3468
|
+
process.kill(pid, "SIGKILL");
|
|
3469
|
+
}
|
|
3791
3470
|
}
|
|
3792
3471
|
console.log(`Successfully killed runaway process PID ${pid}`);
|
|
3793
|
-
|
|
3472
|
+
killed++;
|
|
3794
3473
|
} catch (error) {
|
|
3795
3474
|
const errorMessage = error.message;
|
|
3796
3475
|
errors.push({ pid, error: errorMessage });
|
|
3797
3476
|
console.log(`Failed to kill process PID ${pid}: ${errorMessage}`);
|
|
3798
|
-
return { success: false, pid, command };
|
|
3799
3477
|
}
|
|
3800
|
-
}
|
|
3801
|
-
const results = await Promise.all(killPromises);
|
|
3802
|
-
const killed = results.filter((r) => r.success).length;
|
|
3478
|
+
}
|
|
3803
3479
|
return { killed, errors };
|
|
3804
3480
|
}
|
|
3805
3481
|
|
|
@@ -3853,7 +3529,7 @@ async function runDoctorCommand(filter) {
|
|
|
3853
3529
|
console.log(`Node.js Version: ${chalk.green(process.version)}`);
|
|
3854
3530
|
console.log("");
|
|
3855
3531
|
console.log(chalk.bold("\u{1F527} Daemon Spawn Diagnostics"));
|
|
3856
|
-
const projectRoot = projectPath();
|
|
3532
|
+
const projectRoot = types.projectPath();
|
|
3857
3533
|
const wrapperPath = node_path.join(projectRoot, "bin", "happy.mjs");
|
|
3858
3534
|
const cliEntrypoint = node_path.join(projectRoot, "dist", "index.mjs");
|
|
3859
3535
|
console.log(`Project Root: ${chalk.blue(projectRoot)}`);
|
|
@@ -3915,7 +3591,7 @@ async function runDoctorCommand(filter) {
|
|
|
3915
3591
|
console.log(chalk.blue(`Location: ${types.configuration.daemonStateFile}`));
|
|
3916
3592
|
console.log(chalk.gray(JSON.stringify(state, null, 2)));
|
|
3917
3593
|
}
|
|
3918
|
-
const allProcesses = findAllHappyProcesses();
|
|
3594
|
+
const allProcesses = await findAllHappyProcesses();
|
|
3919
3595
|
if (allProcesses.length > 0) {
|
|
3920
3596
|
console.log(chalk.bold("\n\u{1F50D} All Happy CLI Processes"));
|
|
3921
3597
|
const grouped = allProcesses.reduce((groups, process2) => {
|
|
@@ -4226,7 +3902,7 @@ async function authAndSetupMachineIfNeeded() {
|
|
|
4226
3902
|
}
|
|
4227
3903
|
|
|
4228
3904
|
function spawnHappyCLI(args, options = {}) {
|
|
4229
|
-
const projectRoot = projectPath();
|
|
3905
|
+
const projectRoot = types.projectPath();
|
|
4230
3906
|
const entrypoint = node_path.join(projectRoot, "dist", "index.mjs");
|
|
4231
3907
|
let directory;
|
|
4232
3908
|
if ("cwd" in options) {
|
|
@@ -4271,31 +3947,54 @@ function startDaemonControlServer({
|
|
|
4271
3947
|
sessionId: z.z.string(),
|
|
4272
3948
|
metadata: z.z.any()
|
|
4273
3949
|
// Metadata type from API
|
|
4274
|
-
})
|
|
3950
|
+
}),
|
|
3951
|
+
response: {
|
|
3952
|
+
200: z.z.object({
|
|
3953
|
+
status: z.z.literal("ok")
|
|
3954
|
+
})
|
|
3955
|
+
}
|
|
4275
3956
|
}
|
|
4276
|
-
}, async (request
|
|
3957
|
+
}, async (request) => {
|
|
4277
3958
|
const { sessionId, metadata } = request.body;
|
|
4278
3959
|
types.logger.debug(`[CONTROL SERVER] Session started: ${sessionId}`);
|
|
4279
3960
|
onHappySessionWebhook(sessionId, metadata);
|
|
4280
3961
|
return { status: "ok" };
|
|
4281
3962
|
});
|
|
4282
|
-
typed.post("/list",
|
|
3963
|
+
typed.post("/list", {
|
|
3964
|
+
schema: {
|
|
3965
|
+
response: {
|
|
3966
|
+
200: z.z.object({
|
|
3967
|
+
children: z.z.array(z.z.object({
|
|
3968
|
+
startedBy: z.z.string(),
|
|
3969
|
+
happySessionId: z.z.string(),
|
|
3970
|
+
pid: z.z.number()
|
|
3971
|
+
}))
|
|
3972
|
+
})
|
|
3973
|
+
}
|
|
3974
|
+
}
|
|
3975
|
+
}, async () => {
|
|
4283
3976
|
const children = getChildren();
|
|
4284
3977
|
types.logger.debug(`[CONTROL SERVER] Listing ${children.length} sessions`);
|
|
4285
3978
|
return {
|
|
4286
|
-
children: children.map((child) => {
|
|
4287
|
-
|
|
4288
|
-
|
|
4289
|
-
|
|
3979
|
+
children: children.filter((child) => child.happySessionId !== void 0).map((child) => ({
|
|
3980
|
+
startedBy: child.startedBy,
|
|
3981
|
+
happySessionId: child.happySessionId,
|
|
3982
|
+
pid: child.pid
|
|
3983
|
+
}))
|
|
4290
3984
|
};
|
|
4291
3985
|
});
|
|
4292
3986
|
typed.post("/stop-session", {
|
|
4293
3987
|
schema: {
|
|
4294
3988
|
body: z.z.object({
|
|
4295
3989
|
sessionId: z.z.string()
|
|
4296
|
-
})
|
|
3990
|
+
}),
|
|
3991
|
+
response: {
|
|
3992
|
+
200: z.z.object({
|
|
3993
|
+
success: z.z.boolean()
|
|
3994
|
+
})
|
|
3995
|
+
}
|
|
4297
3996
|
}
|
|
4298
|
-
}, async (request
|
|
3997
|
+
}, async (request) => {
|
|
4299
3998
|
const { sessionId } = request.body;
|
|
4300
3999
|
types.logger.debug(`[CONTROL SERVER] Stop session request: ${sessionId}`);
|
|
4301
4000
|
const success = stopSession(sessionId);
|
|
@@ -4306,28 +4005,68 @@ function startDaemonControlServer({
|
|
|
4306
4005
|
body: z.z.object({
|
|
4307
4006
|
directory: z.z.string(),
|
|
4308
4007
|
sessionId: z.z.string().optional()
|
|
4309
|
-
})
|
|
4008
|
+
}),
|
|
4009
|
+
response: {
|
|
4010
|
+
200: z.z.object({
|
|
4011
|
+
success: z.z.boolean(),
|
|
4012
|
+
sessionId: z.z.string().optional(),
|
|
4013
|
+
approvedNewDirectoryCreation: z.z.boolean().optional()
|
|
4014
|
+
}),
|
|
4015
|
+
409: z.z.object({
|
|
4016
|
+
success: z.z.boolean(),
|
|
4017
|
+
requiresUserApproval: z.z.boolean().optional(),
|
|
4018
|
+
actionRequired: z.z.string().optional(),
|
|
4019
|
+
directory: z.z.string().optional()
|
|
4020
|
+
}),
|
|
4021
|
+
500: z.z.object({
|
|
4022
|
+
success: z.z.boolean(),
|
|
4023
|
+
error: z.z.string().optional()
|
|
4024
|
+
})
|
|
4025
|
+
}
|
|
4310
4026
|
}
|
|
4311
4027
|
}, async (request, reply) => {
|
|
4312
4028
|
const { directory, sessionId } = request.body;
|
|
4313
4029
|
types.logger.debug(`[CONTROL SERVER] Spawn session request: dir=${directory}, sessionId=${sessionId || "new"}`);
|
|
4314
|
-
const
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
|
|
4325
|
-
|
|
4326
|
-
|
|
4327
|
-
|
|
4030
|
+
const result = await spawnSession({ directory, sessionId });
|
|
4031
|
+
switch (result.type) {
|
|
4032
|
+
case "success":
|
|
4033
|
+
if (!result.sessionId) {
|
|
4034
|
+
reply.code(500);
|
|
4035
|
+
return {
|
|
4036
|
+
success: false,
|
|
4037
|
+
error: "Failed to spawn session: no session ID returned"
|
|
4038
|
+
};
|
|
4039
|
+
}
|
|
4040
|
+
return {
|
|
4041
|
+
success: true,
|
|
4042
|
+
sessionId: result.sessionId,
|
|
4043
|
+
approvedNewDirectoryCreation: true
|
|
4044
|
+
};
|
|
4045
|
+
case "requestToApproveDirectoryCreation":
|
|
4046
|
+
reply.code(409);
|
|
4047
|
+
return {
|
|
4048
|
+
success: false,
|
|
4049
|
+
requiresUserApproval: true,
|
|
4050
|
+
actionRequired: "CREATE_DIRECTORY",
|
|
4051
|
+
directory: result.directory
|
|
4052
|
+
};
|
|
4053
|
+
case "error":
|
|
4054
|
+
reply.code(500);
|
|
4055
|
+
return {
|
|
4056
|
+
success: false,
|
|
4057
|
+
error: result.errorMessage
|
|
4058
|
+
};
|
|
4328
4059
|
}
|
|
4329
4060
|
});
|
|
4330
|
-
typed.post("/stop",
|
|
4061
|
+
typed.post("/stop", {
|
|
4062
|
+
schema: {
|
|
4063
|
+
response: {
|
|
4064
|
+
200: z.z.object({
|
|
4065
|
+
status: z.z.string()
|
|
4066
|
+
})
|
|
4067
|
+
}
|
|
4068
|
+
}
|
|
4069
|
+
}, async () => {
|
|
4331
4070
|
types.logger.debug("[CONTROL SERVER] Stop daemon request received");
|
|
4332
4071
|
setTimeout(() => {
|
|
4333
4072
|
types.logger.debug("[CONTROL SERVER] Triggering daemon shutdown");
|
|
@@ -4335,21 +4074,6 @@ function startDaemonControlServer({
|
|
|
4335
4074
|
}, 50);
|
|
4336
4075
|
return { status: "stopping" };
|
|
4337
4076
|
});
|
|
4338
|
-
typed.post("/dev-simulate-error", {
|
|
4339
|
-
schema: {
|
|
4340
|
-
body: z.z.object({
|
|
4341
|
-
error: z.z.string()
|
|
4342
|
-
})
|
|
4343
|
-
}
|
|
4344
|
-
}, async (request, reply) => {
|
|
4345
|
-
const { error } = request.body;
|
|
4346
|
-
types.logger.debug(`[CONTROL SERVER] Dev: Simulating error: ${error}`);
|
|
4347
|
-
setTimeout(() => {
|
|
4348
|
-
types.logger.debug(`[CONTROL SERVER] Dev: Throwing simulated error now`);
|
|
4349
|
-
throw new Error(error);
|
|
4350
|
-
}, 100);
|
|
4351
|
-
return { status: "error will be thrown" };
|
|
4352
|
-
});
|
|
4353
4077
|
app.listen({ port: 0, host: "127.0.0.1" }, (err, address) => {
|
|
4354
4078
|
if (err) {
|
|
4355
4079
|
types.logger.debug("[CONTROL SERVER] Failed to start:", err);
|
|
@@ -4417,7 +4141,7 @@ async function startDaemon() {
|
|
|
4417
4141
|
});
|
|
4418
4142
|
types.logger.debug("[DAEMON RUN] Starting daemon process...");
|
|
4419
4143
|
types.logger.debugLargeJson("[DAEMON RUN] Environment", getEnvironmentInfo());
|
|
4420
|
-
const runningDaemonVersionMatches = await
|
|
4144
|
+
const runningDaemonVersionMatches = await isDaemonRunningCurrentlyInstalledHappyVersion();
|
|
4421
4145
|
if (!runningDaemonVersionMatches) {
|
|
4422
4146
|
types.logger.debug("[DAEMON RUN] Daemon version mismatch detected, restarting daemon with current CLI version");
|
|
4423
4147
|
await stopDaemon();
|
|
@@ -4472,16 +4196,22 @@ async function startDaemon() {
|
|
|
4472
4196
|
types.logger.debug(`[DAEMON RUN] Registered externally-started session ${sessionId}`);
|
|
4473
4197
|
}
|
|
4474
4198
|
};
|
|
4475
|
-
const spawnSession = async (
|
|
4199
|
+
const spawnSession = async (options) => {
|
|
4200
|
+
types.logger.debugLargeJson("[DAEMON RUN] Spawning session", options);
|
|
4201
|
+
const { directory, sessionId, machineId: machineId2, approvedNewDirectoryCreation = true } = options;
|
|
4476
4202
|
let directoryCreated = false;
|
|
4477
|
-
if (directory.startsWith("~")) {
|
|
4478
|
-
directory = path.resolve(os$1.homedir(), directory.replace("~", ""));
|
|
4479
|
-
}
|
|
4480
4203
|
try {
|
|
4481
4204
|
await fs.access(directory);
|
|
4482
4205
|
types.logger.debug(`[DAEMON RUN] Directory exists: ${directory}`);
|
|
4483
4206
|
} catch (error) {
|
|
4484
4207
|
types.logger.debug(`[DAEMON RUN] Directory doesn't exist, creating: ${directory}`);
|
|
4208
|
+
if (!approvedNewDirectoryCreation) {
|
|
4209
|
+
types.logger.debug(`[DAEMON RUN] Directory creation not approved for: ${directory}`);
|
|
4210
|
+
return {
|
|
4211
|
+
type: "requestToApproveDirectoryCreation",
|
|
4212
|
+
directory
|
|
4213
|
+
};
|
|
4214
|
+
}
|
|
4485
4215
|
try {
|
|
4486
4216
|
await fs.mkdir(directory, { recursive: true });
|
|
4487
4217
|
types.logger.debug(`[DAEMON RUN] Successfully created directory: ${directory}`);
|
|
@@ -4500,7 +4230,10 @@ async function startDaemon() {
|
|
|
4500
4230
|
errorMessage += `System error: ${mkdirError.message || mkdirError}. Please verify the path is valid and you have the necessary permissions.`;
|
|
4501
4231
|
}
|
|
4502
4232
|
types.logger.debug(`[DAEMON RUN] Directory creation failed: ${errorMessage}`);
|
|
4503
|
-
return
|
|
4233
|
+
return {
|
|
4234
|
+
type: "error",
|
|
4235
|
+
errorMessage
|
|
4236
|
+
};
|
|
4504
4237
|
}
|
|
4505
4238
|
}
|
|
4506
4239
|
try {
|
|
@@ -4528,7 +4261,10 @@ async function startDaemon() {
|
|
|
4528
4261
|
}
|
|
4529
4262
|
if (!happyProcess.pid) {
|
|
4530
4263
|
types.logger.debug("[DAEMON RUN] Failed to spawn process - no PID returned");
|
|
4531
|
-
return
|
|
4264
|
+
return {
|
|
4265
|
+
type: "error",
|
|
4266
|
+
errorMessage: "Failed to spawn Happy process - no PID returned"
|
|
4267
|
+
};
|
|
4532
4268
|
}
|
|
4533
4269
|
types.logger.debug(`[DAEMON RUN] Spawned process with PID ${happyProcess.pid}`);
|
|
4534
4270
|
const trackedSession = {
|
|
@@ -4556,17 +4292,27 @@ async function startDaemon() {
|
|
|
4556
4292
|
const timeout = setTimeout(() => {
|
|
4557
4293
|
pidToAwaiter.delete(happyProcess.pid);
|
|
4558
4294
|
types.logger.debug(`[DAEMON RUN] Session webhook timeout for PID ${happyProcess.pid}`);
|
|
4559
|
-
resolve2(
|
|
4295
|
+
resolve2({
|
|
4296
|
+
type: "error",
|
|
4297
|
+
errorMessage: `Session webhook timeout for PID ${happyProcess.pid}`
|
|
4298
|
+
});
|
|
4560
4299
|
}, 1e4);
|
|
4561
4300
|
pidToAwaiter.set(happyProcess.pid, (completedSession) => {
|
|
4562
4301
|
clearTimeout(timeout);
|
|
4563
4302
|
types.logger.debug(`[DAEMON RUN] Session ${completedSession.happySessionId} fully spawned with webhook`);
|
|
4564
|
-
resolve2(
|
|
4303
|
+
resolve2({
|
|
4304
|
+
type: "success",
|
|
4305
|
+
sessionId: completedSession.happySessionId
|
|
4306
|
+
});
|
|
4565
4307
|
});
|
|
4566
4308
|
});
|
|
4567
4309
|
} catch (error) {
|
|
4310
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4568
4311
|
types.logger.debug("[DAEMON RUN] Failed to spawn session:", error);
|
|
4569
|
-
return
|
|
4312
|
+
return {
|
|
4313
|
+
type: "error",
|
|
4314
|
+
errorMessage: `Failed to spawn session: ${errorMessage}`
|
|
4315
|
+
};
|
|
4570
4316
|
}
|
|
4571
4317
|
};
|
|
4572
4318
|
const stopSession = (sessionId) => {
|
|
@@ -4623,7 +4369,7 @@ async function startDaemon() {
|
|
|
4623
4369
|
startedAt: Date.now()
|
|
4624
4370
|
};
|
|
4625
4371
|
const api = new types.ApiClient(credentials.token, credentials.secret);
|
|
4626
|
-
const machine = await api.
|
|
4372
|
+
const machine = await api.getOrCreateMachine({
|
|
4627
4373
|
machineId,
|
|
4628
4374
|
metadata: initialMachineMetadata,
|
|
4629
4375
|
daemonState: initialDaemonState
|
|
@@ -4654,7 +4400,7 @@ async function startDaemon() {
|
|
|
4654
4400
|
pidToTrackedSession.delete(pid);
|
|
4655
4401
|
}
|
|
4656
4402
|
}
|
|
4657
|
-
const projectVersion = JSON.parse(fs$1.readFileSync(path.join(projectPath(), "package.json"), "utf-8")).version;
|
|
4403
|
+
const projectVersion = JSON.parse(fs$1.readFileSync(path.join(types.projectPath(), "package.json"), "utf-8")).version;
|
|
4658
4404
|
if (projectVersion !== types.configuration.currentCliVersion) {
|
|
4659
4405
|
types.logger.debug("[DAEMON RUN] Daemon is outdated, triggering self-restart with latest version, clearing heartbeat interval");
|
|
4660
4406
|
clearInterval(restartOnStaleVersionAndHeartbeat);
|
|
@@ -4805,6 +4551,17 @@ async function startHappyServer(client) {
|
|
|
4805
4551
|
};
|
|
4806
4552
|
}
|
|
4807
4553
|
|
|
4554
|
+
function registerKillSessionHandler(rpcHandlerManager, killThisHappy) {
|
|
4555
|
+
rpcHandlerManager.registerHandler("killSession", async () => {
|
|
4556
|
+
types.logger.debug("Kill session request received");
|
|
4557
|
+
void killThisHappy();
|
|
4558
|
+
return {
|
|
4559
|
+
success: true,
|
|
4560
|
+
message: "Killing happy-cli process"
|
|
4561
|
+
};
|
|
4562
|
+
});
|
|
4563
|
+
}
|
|
4564
|
+
|
|
4808
4565
|
async function start(credentials, options = {}) {
|
|
4809
4566
|
const workingDirectory = process.cwd();
|
|
4810
4567
|
const sessionTag = node_crypto.randomUUID();
|
|
@@ -4823,7 +4580,7 @@ async function start(credentials, options = {}) {
|
|
|
4823
4580
|
process.exit(1);
|
|
4824
4581
|
}
|
|
4825
4582
|
types.logger.debug(`Using machineId: ${machineId}`);
|
|
4826
|
-
await api.
|
|
4583
|
+
await api.getOrCreateMachine({
|
|
4827
4584
|
machineId,
|
|
4828
4585
|
metadata: initialMachineMetadata
|
|
4829
4586
|
});
|
|
@@ -4891,7 +4648,6 @@ async function start(credentials, options = {}) {
|
|
|
4891
4648
|
allowedTools: mode.allowedTools,
|
|
4892
4649
|
disallowedTools: mode.disallowedTools
|
|
4893
4650
|
}));
|
|
4894
|
-
registerHandlers(session);
|
|
4895
4651
|
let currentPermissionMode = options.permissionMode;
|
|
4896
4652
|
let currentModel = options.model;
|
|
4897
4653
|
let currentFallbackModel = void 0;
|
|
@@ -5038,6 +4794,7 @@ async function start(credentials, options = {}) {
|
|
|
5038
4794
|
types.logger.debug("[START] Unhandled rejection:", reason);
|
|
5039
4795
|
cleanup();
|
|
5040
4796
|
});
|
|
4797
|
+
registerKillSessionHandler(session.rpcHandlerManager, cleanup);
|
|
5041
4798
|
await loop({
|
|
5042
4799
|
path: workingDirectory,
|
|
5043
4800
|
model: options.model,
|
|
@@ -5053,7 +4810,7 @@ async function start(credentials, options = {}) {
|
|
|
5053
4810
|
controlledByUser: newMode === "local"
|
|
5054
4811
|
}));
|
|
5055
4812
|
},
|
|
5056
|
-
onSessionReady: (
|
|
4813
|
+
onSessionReady: (_sessionInstance) => {
|
|
5057
4814
|
},
|
|
5058
4815
|
mcpServers: {
|
|
5059
4816
|
"happy": {
|
|
@@ -5395,33 +5152,561 @@ async function handleAuthStatus() {
|
|
|
5395
5152
|
}
|
|
5396
5153
|
}
|
|
5397
5154
|
|
|
5398
|
-
const
|
|
5399
|
-
|
|
5400
|
-
|
|
5401
|
-
|
|
5402
|
-
|
|
5403
|
-
|
|
5404
|
-
|
|
5405
|
-
|
|
5406
|
-
|
|
5407
|
-
|
|
5408
|
-
|
|
5409
|
-
|
|
5410
|
-
|
|
5411
|
-
|
|
5412
|
-
|
|
5413
|
-
|
|
5414
|
-
|
|
5415
|
-
|
|
5416
|
-
|
|
5417
|
-
|
|
5155
|
+
const CLIENT_ID$2 = "app_EMoamEEZ73f0CkXaXp7hrann";
|
|
5156
|
+
const AUTH_BASE_URL = "https://auth.openai.com";
|
|
5157
|
+
const DEFAULT_PORT$2 = 1455;
|
|
5158
|
+
function generatePKCE$2() {
|
|
5159
|
+
const verifier = crypto.randomBytes(32).toString("base64url").replace(/[^a-zA-Z0-9\-._~]/g, "");
|
|
5160
|
+
const challenge = crypto.createHash("sha256").update(verifier).digest("base64url").replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
|
|
5161
|
+
return { verifier, challenge };
|
|
5162
|
+
}
|
|
5163
|
+
function generateState$2() {
|
|
5164
|
+
return crypto.randomBytes(16).toString("hex");
|
|
5165
|
+
}
|
|
5166
|
+
function parseJWT(token) {
|
|
5167
|
+
const parts = token.split(".");
|
|
5168
|
+
if (parts.length !== 3) {
|
|
5169
|
+
throw new Error("Invalid JWT format");
|
|
5170
|
+
}
|
|
5171
|
+
const payload = Buffer.from(parts[1], "base64url").toString();
|
|
5172
|
+
return JSON.parse(payload);
|
|
5173
|
+
}
|
|
5174
|
+
async function findAvailablePort$2() {
|
|
5175
|
+
return new Promise((resolve) => {
|
|
5176
|
+
const server = http.createServer();
|
|
5177
|
+
server.listen(0, "127.0.0.1", () => {
|
|
5178
|
+
const port = server.address().port;
|
|
5179
|
+
server.close(() => resolve(port));
|
|
5180
|
+
});
|
|
5181
|
+
});
|
|
5182
|
+
}
|
|
5183
|
+
async function isPortAvailable$2(port) {
|
|
5184
|
+
return new Promise((resolve) => {
|
|
5185
|
+
const testServer = http.createServer();
|
|
5186
|
+
testServer.once("error", () => {
|
|
5187
|
+
testServer.close();
|
|
5188
|
+
resolve(false);
|
|
5189
|
+
});
|
|
5190
|
+
testServer.listen(port, "127.0.0.1", () => {
|
|
5191
|
+
testServer.close(() => resolve(true));
|
|
5192
|
+
});
|
|
5193
|
+
});
|
|
5194
|
+
}
|
|
5195
|
+
async function exchangeCodeForTokens$2(code, verifier, port) {
|
|
5196
|
+
const response = await fetch(`${AUTH_BASE_URL}/oauth/token`, {
|
|
5197
|
+
method: "POST",
|
|
5198
|
+
headers: {
|
|
5199
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
5200
|
+
},
|
|
5201
|
+
body: new URLSearchParams({
|
|
5202
|
+
grant_type: "authorization_code",
|
|
5203
|
+
client_id: CLIENT_ID$2,
|
|
5204
|
+
code,
|
|
5205
|
+
code_verifier: verifier,
|
|
5206
|
+
redirect_uri: `http://localhost:${port}/auth/callback`
|
|
5207
|
+
})
|
|
5208
|
+
});
|
|
5209
|
+
if (!response.ok) {
|
|
5210
|
+
const error = await response.text();
|
|
5211
|
+
throw new Error(`Token exchange failed: ${error}`);
|
|
5212
|
+
}
|
|
5213
|
+
const data = await response.json();
|
|
5214
|
+
const idTokenPayload = parseJWT(data.id_token);
|
|
5215
|
+
let accountId = idTokenPayload.chatgpt_account_id;
|
|
5216
|
+
if (!accountId) {
|
|
5217
|
+
const authClaim = idTokenPayload["https://api.openai.com/auth"];
|
|
5218
|
+
if (authClaim && typeof authClaim === "object") {
|
|
5219
|
+
accountId = authClaim.chatgpt_account_id || authClaim.account_id;
|
|
5418
5220
|
}
|
|
5221
|
+
}
|
|
5222
|
+
return {
|
|
5223
|
+
id_token: data.id_token,
|
|
5224
|
+
access_token: data.access_token || data.id_token,
|
|
5225
|
+
refresh_token: data.refresh_token,
|
|
5226
|
+
account_id: accountId
|
|
5227
|
+
};
|
|
5228
|
+
}
|
|
5229
|
+
async function startCallbackServer$2(state, verifier, port) {
|
|
5230
|
+
return new Promise((resolve, reject) => {
|
|
5231
|
+
const server = http.createServer(async (req, res) => {
|
|
5232
|
+
const url = new URL(req.url, `http://localhost:${port}`);
|
|
5233
|
+
if (url.pathname === "/auth/callback") {
|
|
5234
|
+
const code = url.searchParams.get("code");
|
|
5235
|
+
const receivedState = url.searchParams.get("state");
|
|
5236
|
+
if (receivedState !== state) {
|
|
5237
|
+
res.writeHead(400);
|
|
5238
|
+
res.end("Invalid state parameter");
|
|
5239
|
+
server.close();
|
|
5240
|
+
reject(new Error("Invalid state parameter"));
|
|
5241
|
+
return;
|
|
5242
|
+
}
|
|
5243
|
+
if (!code) {
|
|
5244
|
+
res.writeHead(400);
|
|
5245
|
+
res.end("No authorization code received");
|
|
5246
|
+
server.close();
|
|
5247
|
+
reject(new Error("No authorization code received"));
|
|
5248
|
+
return;
|
|
5249
|
+
}
|
|
5250
|
+
try {
|
|
5251
|
+
const tokens = await exchangeCodeForTokens$2(code, verifier, port);
|
|
5252
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
5253
|
+
res.end(`
|
|
5254
|
+
<html>
|
|
5255
|
+
<body style="font-family: sans-serif; padding: 20px;">
|
|
5256
|
+
<h2>\u2705 Authentication Successful!</h2>
|
|
5257
|
+
<p>You can close this window and return to your terminal.</p>
|
|
5258
|
+
<script>setTimeout(() => window.close(), 3000);<\/script>
|
|
5259
|
+
</body>
|
|
5260
|
+
</html>
|
|
5261
|
+
`);
|
|
5262
|
+
server.close();
|
|
5263
|
+
resolve(tokens);
|
|
5264
|
+
} catch (error) {
|
|
5265
|
+
res.writeHead(500);
|
|
5266
|
+
res.end("Token exchange failed");
|
|
5267
|
+
server.close();
|
|
5268
|
+
reject(error);
|
|
5269
|
+
}
|
|
5270
|
+
}
|
|
5271
|
+
});
|
|
5272
|
+
server.listen(port, "127.0.0.1", () => {
|
|
5273
|
+
});
|
|
5274
|
+
setTimeout(() => {
|
|
5275
|
+
server.close();
|
|
5276
|
+
reject(new Error("Authentication timeout"));
|
|
5277
|
+
}, 5 * 60 * 1e3);
|
|
5419
5278
|
});
|
|
5420
|
-
|
|
5421
|
-
|
|
5422
|
-
|
|
5423
|
-
|
|
5424
|
-
|
|
5279
|
+
}
|
|
5280
|
+
async function authenticateCodex() {
|
|
5281
|
+
const { verifier, challenge } = generatePKCE$2();
|
|
5282
|
+
const state = generateState$2();
|
|
5283
|
+
let port = DEFAULT_PORT$2;
|
|
5284
|
+
const portAvailable = await isPortAvailable$2(port);
|
|
5285
|
+
if (!portAvailable) {
|
|
5286
|
+
port = await findAvailablePort$2();
|
|
5287
|
+
}
|
|
5288
|
+
const serverPromise = startCallbackServer$2(state, verifier, port);
|
|
5289
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
5290
|
+
const redirect_uri = `http://localhost:${port}/auth/callback`;
|
|
5291
|
+
const params = [
|
|
5292
|
+
["response_type", "code"],
|
|
5293
|
+
["client_id", CLIENT_ID$2],
|
|
5294
|
+
["redirect_uri", redirect_uri],
|
|
5295
|
+
["scope", "openid profile email offline_access"],
|
|
5296
|
+
["code_challenge", challenge],
|
|
5297
|
+
["code_challenge_method", "S256"],
|
|
5298
|
+
["id_token_add_organizations", "true"],
|
|
5299
|
+
["codex_cli_simplified_flow", "true"],
|
|
5300
|
+
["state", state]
|
|
5301
|
+
];
|
|
5302
|
+
const queryString = params.map(([key, value]) => `${key}=${encodeURIComponent(value)}`).join("&");
|
|
5303
|
+
const authUrl = `${AUTH_BASE_URL}/oauth/authorize?${queryString}`;
|
|
5304
|
+
console.log("\u{1F4CB} Opening browser for authentication...");
|
|
5305
|
+
console.log(`If browser doesn't open, visit:
|
|
5306
|
+
${authUrl}
|
|
5307
|
+
`);
|
|
5308
|
+
await openBrowser(authUrl);
|
|
5309
|
+
const tokens = await serverPromise;
|
|
5310
|
+
console.log("\u{1F389} Authentication successful!");
|
|
5311
|
+
return tokens;
|
|
5312
|
+
}
|
|
5313
|
+
|
|
5314
|
+
const CLIENT_ID$1 = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
|
|
5315
|
+
const CLAUDE_AI_AUTHORIZE_URL = "https://claude.ai/oauth/authorize";
|
|
5316
|
+
const TOKEN_URL$1 = "https://console.anthropic.com/v1/oauth/token";
|
|
5317
|
+
const DEFAULT_PORT$1 = 54545;
|
|
5318
|
+
const SCOPE = "user:inference";
|
|
5319
|
+
function generatePKCE$1() {
|
|
5320
|
+
const verifier = crypto.randomBytes(32).toString("base64url").replace(/[^a-zA-Z0-9\-._~]/g, "");
|
|
5321
|
+
const challenge = crypto.createHash("sha256").update(verifier).digest("base64url").replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
|
|
5322
|
+
return { verifier, challenge };
|
|
5323
|
+
}
|
|
5324
|
+
function generateState$1() {
|
|
5325
|
+
return crypto.randomBytes(32).toString("base64url");
|
|
5326
|
+
}
|
|
5327
|
+
async function findAvailablePort$1() {
|
|
5328
|
+
return new Promise((resolve) => {
|
|
5329
|
+
const server = http.createServer();
|
|
5330
|
+
server.listen(0, "127.0.0.1", () => {
|
|
5331
|
+
const port = server.address().port;
|
|
5332
|
+
server.close(() => resolve(port));
|
|
5333
|
+
});
|
|
5334
|
+
});
|
|
5335
|
+
}
|
|
5336
|
+
async function isPortAvailable$1(port) {
|
|
5337
|
+
return new Promise((resolve) => {
|
|
5338
|
+
const testServer = http.createServer();
|
|
5339
|
+
testServer.once("error", () => {
|
|
5340
|
+
testServer.close();
|
|
5341
|
+
resolve(false);
|
|
5342
|
+
});
|
|
5343
|
+
testServer.listen(port, "127.0.0.1", () => {
|
|
5344
|
+
testServer.close(() => resolve(true));
|
|
5345
|
+
});
|
|
5346
|
+
});
|
|
5347
|
+
}
|
|
5348
|
+
async function exchangeCodeForTokens$1(code, verifier, port, state) {
|
|
5349
|
+
const tokenResponse = await fetch(TOKEN_URL$1, {
|
|
5350
|
+
method: "POST",
|
|
5351
|
+
headers: {
|
|
5352
|
+
"Content-Type": "application/json"
|
|
5353
|
+
},
|
|
5354
|
+
body: JSON.stringify({
|
|
5355
|
+
grant_type: "authorization_code",
|
|
5356
|
+
code,
|
|
5357
|
+
redirect_uri: `http://localhost:${port}/callback`,
|
|
5358
|
+
client_id: CLIENT_ID$1,
|
|
5359
|
+
code_verifier: verifier,
|
|
5360
|
+
state
|
|
5361
|
+
})
|
|
5362
|
+
});
|
|
5363
|
+
if (!tokenResponse.ok) {
|
|
5364
|
+
throw new Error(`Token exchange failed: ${tokenResponse.statusText}`);
|
|
5365
|
+
}
|
|
5366
|
+
const tokenData = await tokenResponse.json();
|
|
5367
|
+
return {
|
|
5368
|
+
raw: tokenData,
|
|
5369
|
+
token: tokenData.access_token,
|
|
5370
|
+
expires: Date.now() + tokenData.expires_in * 1e3
|
|
5371
|
+
};
|
|
5372
|
+
}
|
|
5373
|
+
async function startCallbackServer$1(state, verifier, port) {
|
|
5374
|
+
return new Promise((resolve, reject) => {
|
|
5375
|
+
const server = http.createServer(async (req, res) => {
|
|
5376
|
+
const url = new URL(req.url, `http://localhost:${port}`);
|
|
5377
|
+
if (url.pathname === "/callback") {
|
|
5378
|
+
const code = url.searchParams.get("code");
|
|
5379
|
+
const receivedState = url.searchParams.get("state");
|
|
5380
|
+
if (receivedState !== state) {
|
|
5381
|
+
res.writeHead(400);
|
|
5382
|
+
res.end("Invalid state parameter");
|
|
5383
|
+
server.close();
|
|
5384
|
+
reject(new Error("Invalid state parameter"));
|
|
5385
|
+
return;
|
|
5386
|
+
}
|
|
5387
|
+
if (!code) {
|
|
5388
|
+
res.writeHead(400);
|
|
5389
|
+
res.end("No authorization code received");
|
|
5390
|
+
server.close();
|
|
5391
|
+
reject(new Error("No authorization code received"));
|
|
5392
|
+
return;
|
|
5393
|
+
}
|
|
5394
|
+
try {
|
|
5395
|
+
const tokens = await exchangeCodeForTokens$1(code, verifier, port, state);
|
|
5396
|
+
res.writeHead(302, {
|
|
5397
|
+
"Location": "https://console.anthropic.com/oauth/code/success?app=claude-code"
|
|
5398
|
+
});
|
|
5399
|
+
res.end();
|
|
5400
|
+
server.close();
|
|
5401
|
+
resolve(tokens);
|
|
5402
|
+
} catch (error) {
|
|
5403
|
+
res.writeHead(500);
|
|
5404
|
+
res.end("Token exchange failed");
|
|
5405
|
+
server.close();
|
|
5406
|
+
reject(error);
|
|
5407
|
+
}
|
|
5408
|
+
}
|
|
5409
|
+
});
|
|
5410
|
+
server.listen(port, "127.0.0.1", () => {
|
|
5411
|
+
});
|
|
5412
|
+
setTimeout(() => {
|
|
5413
|
+
server.close();
|
|
5414
|
+
reject(new Error("Authentication timeout"));
|
|
5415
|
+
}, 5 * 60 * 1e3);
|
|
5416
|
+
});
|
|
5417
|
+
}
|
|
5418
|
+
async function authenticateClaude() {
|
|
5419
|
+
console.log("\u{1F680} Starting Anthropic Claude authentication...");
|
|
5420
|
+
const { verifier, challenge } = generatePKCE$1();
|
|
5421
|
+
const state = generateState$1();
|
|
5422
|
+
let port = DEFAULT_PORT$1;
|
|
5423
|
+
const portAvailable = await isPortAvailable$1(port);
|
|
5424
|
+
if (!portAvailable) {
|
|
5425
|
+
console.log(`Port ${port} is in use, finding an available port...`);
|
|
5426
|
+
port = await findAvailablePort$1();
|
|
5427
|
+
}
|
|
5428
|
+
console.log(`\u{1F4E1} Using callback port: ${port}`);
|
|
5429
|
+
const serverPromise = startCallbackServer$1(state, verifier, port);
|
|
5430
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
5431
|
+
const redirect_uri = `http://localhost:${port}/callback`;
|
|
5432
|
+
const params = new URLSearchParams({
|
|
5433
|
+
code: "true",
|
|
5434
|
+
// This tells Claude.ai to show the code AND redirect
|
|
5435
|
+
client_id: CLIENT_ID$1,
|
|
5436
|
+
response_type: "code",
|
|
5437
|
+
redirect_uri,
|
|
5438
|
+
scope: SCOPE,
|
|
5439
|
+
code_challenge: challenge,
|
|
5440
|
+
code_challenge_method: "S256",
|
|
5441
|
+
state
|
|
5442
|
+
});
|
|
5443
|
+
const authUrl = `${CLAUDE_AI_AUTHORIZE_URL}?${params}`;
|
|
5444
|
+
console.log("\u{1F4CB} Opening browser for authentication...");
|
|
5445
|
+
console.log("If browser doesn't open, visit this URL:");
|
|
5446
|
+
console.log();
|
|
5447
|
+
console.log(`${authUrl}`);
|
|
5448
|
+
console.log();
|
|
5449
|
+
await openBrowser(authUrl);
|
|
5450
|
+
try {
|
|
5451
|
+
const tokens = await serverPromise;
|
|
5452
|
+
console.log("\u{1F389} Authentication successful!");
|
|
5453
|
+
console.log("\u2705 OAuth tokens received");
|
|
5454
|
+
return tokens;
|
|
5455
|
+
} catch (error) {
|
|
5456
|
+
console.error("\n\u274C Failed to authenticate with Anthropic");
|
|
5457
|
+
throw error;
|
|
5458
|
+
}
|
|
5459
|
+
}
|
|
5460
|
+
|
|
5461
|
+
const execAsync = util.promisify(child_process.exec);
|
|
5462
|
+
const CLIENT_ID = "681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com";
|
|
5463
|
+
const CLIENT_SECRET = "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";
|
|
5464
|
+
const AUTHORIZE_URL = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
5465
|
+
const TOKEN_URL = "https://oauth2.googleapis.com/token";
|
|
5466
|
+
const DEFAULT_PORT = 54545;
|
|
5467
|
+
const SCOPES = [
|
|
5468
|
+
"https://www.googleapis.com/auth/cloud-platform",
|
|
5469
|
+
"https://www.googleapis.com/auth/userinfo.email",
|
|
5470
|
+
"https://www.googleapis.com/auth/userinfo.profile"
|
|
5471
|
+
].join(" ");
|
|
5472
|
+
function generatePKCE() {
|
|
5473
|
+
const verifier = crypto.randomBytes(32).toString("base64url").replace(/[^a-zA-Z0-9\-._~]/g, "");
|
|
5474
|
+
const challenge = crypto.createHash("sha256").update(verifier).digest("base64url").replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
|
|
5475
|
+
return { verifier, challenge };
|
|
5476
|
+
}
|
|
5477
|
+
function generateState() {
|
|
5478
|
+
return crypto.randomBytes(32).toString("hex");
|
|
5479
|
+
}
|
|
5480
|
+
async function findAvailablePort() {
|
|
5481
|
+
return new Promise((resolve) => {
|
|
5482
|
+
const server = http.createServer();
|
|
5483
|
+
server.listen(0, "127.0.0.1", () => {
|
|
5484
|
+
const port = server.address().port;
|
|
5485
|
+
server.close(() => resolve(port));
|
|
5486
|
+
});
|
|
5487
|
+
});
|
|
5488
|
+
}
|
|
5489
|
+
async function isPortAvailable(port) {
|
|
5490
|
+
return new Promise((resolve) => {
|
|
5491
|
+
const testServer = http.createServer();
|
|
5492
|
+
testServer.once("error", () => {
|
|
5493
|
+
testServer.close();
|
|
5494
|
+
resolve(false);
|
|
5495
|
+
});
|
|
5496
|
+
testServer.listen(port, "127.0.0.1", () => {
|
|
5497
|
+
testServer.close(() => resolve(true));
|
|
5498
|
+
});
|
|
5499
|
+
});
|
|
5500
|
+
}
|
|
5501
|
+
async function exchangeCodeForTokens(code, verifier, port) {
|
|
5502
|
+
const response = await fetch(TOKEN_URL, {
|
|
5503
|
+
method: "POST",
|
|
5504
|
+
headers: {
|
|
5505
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
5506
|
+
},
|
|
5507
|
+
body: new URLSearchParams({
|
|
5508
|
+
grant_type: "authorization_code",
|
|
5509
|
+
client_id: CLIENT_ID,
|
|
5510
|
+
client_secret: CLIENT_SECRET,
|
|
5511
|
+
code,
|
|
5512
|
+
code_verifier: verifier,
|
|
5513
|
+
redirect_uri: `http://localhost:${port}/oauth2callback`
|
|
5514
|
+
})
|
|
5515
|
+
});
|
|
5516
|
+
if (!response.ok) {
|
|
5517
|
+
const error = await response.text();
|
|
5518
|
+
throw new Error(`Token exchange failed: ${error}`);
|
|
5519
|
+
}
|
|
5520
|
+
const data = await response.json();
|
|
5521
|
+
return data;
|
|
5522
|
+
}
|
|
5523
|
+
async function startCallbackServer(state, verifier, port) {
|
|
5524
|
+
return new Promise((resolve, reject) => {
|
|
5525
|
+
const server = http.createServer(async (req, res) => {
|
|
5526
|
+
const url = new URL(req.url, `http://localhost:${port}`);
|
|
5527
|
+
if (url.pathname === "/oauth2callback") {
|
|
5528
|
+
const code = url.searchParams.get("code");
|
|
5529
|
+
const receivedState = url.searchParams.get("state");
|
|
5530
|
+
const error = url.searchParams.get("error");
|
|
5531
|
+
if (error) {
|
|
5532
|
+
res.writeHead(302, {
|
|
5533
|
+
"Location": "https://developers.google.com/gemini-code-assist/auth_failure_gemini"
|
|
5534
|
+
});
|
|
5535
|
+
res.end();
|
|
5536
|
+
server.close();
|
|
5537
|
+
reject(new Error(`Authentication error: ${error}`));
|
|
5538
|
+
return;
|
|
5539
|
+
}
|
|
5540
|
+
if (receivedState !== state) {
|
|
5541
|
+
res.writeHead(400);
|
|
5542
|
+
res.end("State mismatch. Possible CSRF attack");
|
|
5543
|
+
server.close();
|
|
5544
|
+
reject(new Error("Invalid state parameter"));
|
|
5545
|
+
return;
|
|
5546
|
+
}
|
|
5547
|
+
if (!code) {
|
|
5548
|
+
res.writeHead(400);
|
|
5549
|
+
res.end("No authorization code received");
|
|
5550
|
+
server.close();
|
|
5551
|
+
reject(new Error("No authorization code received"));
|
|
5552
|
+
return;
|
|
5553
|
+
}
|
|
5554
|
+
try {
|
|
5555
|
+
const tokens = await exchangeCodeForTokens(code, verifier, port);
|
|
5556
|
+
res.writeHead(302, {
|
|
5557
|
+
"Location": "https://developers.google.com/gemini-code-assist/auth_success_gemini"
|
|
5558
|
+
});
|
|
5559
|
+
res.end();
|
|
5560
|
+
server.close();
|
|
5561
|
+
resolve(tokens);
|
|
5562
|
+
} catch (error2) {
|
|
5563
|
+
res.writeHead(500);
|
|
5564
|
+
res.end("Token exchange failed");
|
|
5565
|
+
server.close();
|
|
5566
|
+
reject(error2);
|
|
5567
|
+
}
|
|
5568
|
+
}
|
|
5569
|
+
});
|
|
5570
|
+
server.listen(port, "127.0.0.1", () => {
|
|
5571
|
+
});
|
|
5572
|
+
setTimeout(() => {
|
|
5573
|
+
server.close();
|
|
5574
|
+
reject(new Error("Authentication timeout"));
|
|
5575
|
+
}, 5 * 60 * 1e3);
|
|
5576
|
+
});
|
|
5577
|
+
}
|
|
5578
|
+
async function authenticateGemini() {
|
|
5579
|
+
console.log("\u{1F680} Starting Google Gemini authentication...");
|
|
5580
|
+
const { verifier, challenge } = generatePKCE();
|
|
5581
|
+
const state = generateState();
|
|
5582
|
+
let port = DEFAULT_PORT;
|
|
5583
|
+
const portAvailable = await isPortAvailable(port);
|
|
5584
|
+
if (!portAvailable) {
|
|
5585
|
+
console.log(`Port ${port} is in use, finding an available port...`);
|
|
5586
|
+
port = await findAvailablePort();
|
|
5587
|
+
}
|
|
5588
|
+
console.log(`\u{1F4E1} Using callback port: ${port}`);
|
|
5589
|
+
const serverPromise = startCallbackServer(state, verifier, port);
|
|
5590
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
5591
|
+
const redirect_uri = `http://localhost:${port}/oauth2callback`;
|
|
5592
|
+
const params = new URLSearchParams({
|
|
5593
|
+
client_id: CLIENT_ID,
|
|
5594
|
+
response_type: "code",
|
|
5595
|
+
redirect_uri,
|
|
5596
|
+
scope: SCOPES,
|
|
5597
|
+
access_type: "offline",
|
|
5598
|
+
// To get refresh token
|
|
5599
|
+
code_challenge: challenge,
|
|
5600
|
+
code_challenge_method: "S256",
|
|
5601
|
+
state,
|
|
5602
|
+
prompt: "consent"
|
|
5603
|
+
// Force consent to get refresh token
|
|
5604
|
+
});
|
|
5605
|
+
const authUrl = `${AUTHORIZE_URL}?${params}`;
|
|
5606
|
+
console.log("\n\u{1F4CB} Opening browser for authentication...");
|
|
5607
|
+
console.log("If browser doesn't open, visit this URL:");
|
|
5608
|
+
console.log(`
|
|
5609
|
+
${authUrl}
|
|
5610
|
+
`);
|
|
5611
|
+
const platform = process.platform;
|
|
5612
|
+
const openCommand = platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open";
|
|
5613
|
+
try {
|
|
5614
|
+
await execAsync(`${openCommand} "${authUrl}"`);
|
|
5615
|
+
} catch {
|
|
5616
|
+
console.log("\u26A0\uFE0F Could not open browser automatically");
|
|
5617
|
+
}
|
|
5618
|
+
try {
|
|
5619
|
+
const tokens = await serverPromise;
|
|
5620
|
+
console.log("\n\u{1F389} Authentication successful!");
|
|
5621
|
+
console.log("\u2705 OAuth tokens received");
|
|
5622
|
+
return tokens;
|
|
5623
|
+
} catch (error) {
|
|
5624
|
+
console.error("\n\u274C Failed to authenticate with Google");
|
|
5625
|
+
throw error;
|
|
5626
|
+
}
|
|
5627
|
+
}
|
|
5628
|
+
|
|
5629
|
+
async function handleConnectCommand(args) {
|
|
5630
|
+
const subcommand = args[0];
|
|
5631
|
+
if (!subcommand || subcommand === "help" || subcommand === "--help" || subcommand === "-h") {
|
|
5632
|
+
showConnectHelp();
|
|
5633
|
+
return;
|
|
5634
|
+
}
|
|
5635
|
+
switch (subcommand.toLowerCase()) {
|
|
5636
|
+
case "codex":
|
|
5637
|
+
await handleConnectVendor("codex", "OpenAI");
|
|
5638
|
+
break;
|
|
5639
|
+
case "claude":
|
|
5640
|
+
await handleConnectVendor("claude", "Anthropic");
|
|
5641
|
+
break;
|
|
5642
|
+
case "gemini":
|
|
5643
|
+
await handleConnectVendor("gemini", "Gemini");
|
|
5644
|
+
break;
|
|
5645
|
+
default:
|
|
5646
|
+
console.error(chalk.red(`Unknown connect target: ${subcommand}`));
|
|
5647
|
+
showConnectHelp();
|
|
5648
|
+
process.exit(1);
|
|
5649
|
+
}
|
|
5650
|
+
}
|
|
5651
|
+
function showConnectHelp() {
|
|
5652
|
+
console.log(`
|
|
5653
|
+
${chalk.bold("happy connect")} - Connect AI vendor API keys to Happy cloud
|
|
5654
|
+
|
|
5655
|
+
${chalk.bold("Usage:")}
|
|
5656
|
+
happy connect codex Store your Codex API key in Happy cloud
|
|
5657
|
+
happy connect anthropic Store your Anthropic API key in Happy cloud
|
|
5658
|
+
happy connect gemini Store your Gemini API key in Happy cloud
|
|
5659
|
+
happy connect help Show this help message
|
|
5660
|
+
|
|
5661
|
+
${chalk.bold("Description:")}
|
|
5662
|
+
The connect command allows you to securely store your AI vendor API keys
|
|
5663
|
+
in Happy cloud. This enables you to use these services through Happy
|
|
5664
|
+
without exposing your API keys locally.
|
|
5665
|
+
|
|
5666
|
+
${chalk.bold("Examples:")}
|
|
5667
|
+
happy connect codex
|
|
5668
|
+
happy connect anthropic
|
|
5669
|
+
happy connect gemini
|
|
5670
|
+
|
|
5671
|
+
${chalk.bold("Notes:")}
|
|
5672
|
+
\u2022 You must be authenticated with Happy first (run 'happy auth login')
|
|
5673
|
+
\u2022 API keys are encrypted and stored securely in Happy cloud
|
|
5674
|
+
\u2022 You can manage your stored keys at app.happy.engineering
|
|
5675
|
+
`);
|
|
5676
|
+
}
|
|
5677
|
+
async function handleConnectVendor(vendor, displayName) {
|
|
5678
|
+
console.log(chalk.bold(`
|
|
5679
|
+
\u{1F50C} Connecting ${displayName} to Happy cloud
|
|
5680
|
+
`));
|
|
5681
|
+
const credentials = await types.readCredentials();
|
|
5682
|
+
if (!credentials) {
|
|
5683
|
+
console.log(chalk.yellow("\u26A0\uFE0F Not authenticated with Happy"));
|
|
5684
|
+
console.log(chalk.gray(' Please run "happy auth login" first'));
|
|
5685
|
+
process.exit(1);
|
|
5686
|
+
}
|
|
5687
|
+
const api = new types.ApiClient(credentials.token, credentials.secret);
|
|
5688
|
+
if (vendor === "codex") {
|
|
5689
|
+
console.log("\u{1F680} Registering Codex token with server");
|
|
5690
|
+
const codexAuthTokens = await authenticateCodex();
|
|
5691
|
+
await api.registerVendorToken("openai", { oauth: codexAuthTokens });
|
|
5692
|
+
console.log("\u2705 Codex token registered with server");
|
|
5693
|
+
process.exit(0);
|
|
5694
|
+
} else if (vendor === "claude") {
|
|
5695
|
+
console.log("\u{1F680} Registering Anthropic token with server");
|
|
5696
|
+
const anthropicAuthTokens = await authenticateClaude();
|
|
5697
|
+
await api.registerVendorToken("anthropic", { oauth: anthropicAuthTokens });
|
|
5698
|
+
console.log("\u2705 Anthropic token registered with server");
|
|
5699
|
+
process.exit(0);
|
|
5700
|
+
} else if (vendor === "gemini") {
|
|
5701
|
+
console.log("\u{1F680} Registering Gemini token with server");
|
|
5702
|
+
const geminiAuthTokens = await authenticateGemini();
|
|
5703
|
+
await api.registerVendorToken("gemini", { oauth: geminiAuthTokens });
|
|
5704
|
+
console.log("\u2705 Gemini token registered with server");
|
|
5705
|
+
process.exit(0);
|
|
5706
|
+
} else {
|
|
5707
|
+
throw new Error(`Unsupported vendor: ${vendor}`);
|
|
5708
|
+
}
|
|
5709
|
+
}
|
|
5425
5710
|
|
|
5426
5711
|
(async () => {
|
|
5427
5712
|
const args = process.argv.slice(2);
|
|
@@ -5451,6 +5736,17 @@ const DaemonPrompt = ({ onSelect }) => {
|
|
|
5451
5736
|
process.exit(1);
|
|
5452
5737
|
}
|
|
5453
5738
|
return;
|
|
5739
|
+
} else if (subcommand === "connect") {
|
|
5740
|
+
try {
|
|
5741
|
+
await handleConnectCommand(args.slice(1));
|
|
5742
|
+
} catch (error) {
|
|
5743
|
+
console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
|
|
5744
|
+
if (process.env.DEBUG) {
|
|
5745
|
+
console.error(error);
|
|
5746
|
+
}
|
|
5747
|
+
process.exit(1);
|
|
5748
|
+
}
|
|
5749
|
+
return;
|
|
5454
5750
|
} else if (subcommand === "logout") {
|
|
5455
5751
|
console.log(chalk.yellow('Note: "happy logout" is deprecated. Use "happy auth logout" instead.\n'));
|
|
5456
5752
|
try {
|
|
@@ -5629,9 +5925,8 @@ ${chalk.bold("Happy supports ALL Claude options!")}
|
|
|
5629
5925
|
${chalk.gray("\u2500".repeat(60))}
|
|
5630
5926
|
${chalk.bold.cyan("Claude Code Options (from `claude --help`):")}
|
|
5631
5927
|
`);
|
|
5632
|
-
const { execSync } = await import('child_process');
|
|
5633
5928
|
try {
|
|
5634
|
-
const claudeHelp =
|
|
5929
|
+
const claudeHelp = node_child_process.execFileSync(process.execPath, [claudeCliPath, "--help"], { encoding: "utf8" });
|
|
5635
5930
|
console.log(claudeHelp);
|
|
5636
5931
|
} catch (e) {
|
|
5637
5932
|
console.log(chalk.yellow("Could not retrieve claude help. Make sure claude is installed."));
|
|
@@ -5644,47 +5939,16 @@ ${chalk.bold.cyan("Claude Code Options (from `claude --help`):")}
|
|
|
5644
5939
|
const {
|
|
5645
5940
|
credentials
|
|
5646
5941
|
} = await authAndSetupMachineIfNeeded();
|
|
5647
|
-
|
|
5648
|
-
if (
|
|
5649
|
-
|
|
5650
|
-
|
|
5651
|
-
|
|
5652
|
-
|
|
5653
|
-
|
|
5654
|
-
app.unmount();
|
|
5655
|
-
resolve(autoStart);
|
|
5656
|
-
}
|
|
5657
|
-
};
|
|
5658
|
-
const app = ink.render(React.createElement(DaemonPrompt, { onSelect }), {
|
|
5659
|
-
exitOnCtrlC: false,
|
|
5660
|
-
patchConsole: false
|
|
5661
|
-
});
|
|
5942
|
+
types.logger.debug("Ensuring Happy background service is running & matches our version...");
|
|
5943
|
+
if (!await isDaemonRunningCurrentlyInstalledHappyVersion()) {
|
|
5944
|
+
types.logger.debug("Starting Happy background service...");
|
|
5945
|
+
const daemonProcess = spawnHappyCLI(["daemon", "start-sync"], {
|
|
5946
|
+
detached: true,
|
|
5947
|
+
stdio: "ignore",
|
|
5948
|
+
env: process.env
|
|
5662
5949
|
});
|
|
5663
|
-
|
|
5664
|
-
|
|
5665
|
-
daemonAutoStartWhenRunningHappy: shouldAutoStart
|
|
5666
|
-
}));
|
|
5667
|
-
if (shouldAutoStart) {
|
|
5668
|
-
console.log(chalk.green("\n\u2713 Happy will start the background service automatically"));
|
|
5669
|
-
console.log(chalk.gray(" The service will run whenever you use the happy command"));
|
|
5670
|
-
} else {
|
|
5671
|
-
console.log(chalk.yellow("\n You can enable this later by running: happy daemon install"));
|
|
5672
|
-
}
|
|
5673
|
-
}
|
|
5674
|
-
if (settings && settings.daemonAutoStartWhenRunningHappy) {
|
|
5675
|
-
types.logger.debug("Ensuring Happy background service is running & matches our version...");
|
|
5676
|
-
if (!await isDaemonRunningSameVersion()) {
|
|
5677
|
-
types.logger.debug("Starting Happy background service...");
|
|
5678
|
-
const daemonProcess = spawnHappyCLI(["daemon", "start-sync"], {
|
|
5679
|
-
detached: true,
|
|
5680
|
-
stdio: "ignore",
|
|
5681
|
-
env: process.env
|
|
5682
|
-
});
|
|
5683
|
-
daemonProcess.unref();
|
|
5684
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
5685
|
-
} else {
|
|
5686
|
-
types.logger.debug("Happy background service is running & matches our version");
|
|
5687
|
-
}
|
|
5950
|
+
daemonProcess.unref();
|
|
5951
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
5688
5952
|
}
|
|
5689
5953
|
try {
|
|
5690
5954
|
await start(credentials, options);
|