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