@zhigang1992/happy-cli 0.12.12 → 0.12.13
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-B0AakJdd.mjs → index-Bzl-ixRP.mjs} +362 -62
- package/dist/{index-DLt6o6FN.cjs → index-VlluCzOw.cjs} +356 -56
- package/dist/index.cjs +2 -2
- package/dist/index.mjs +2 -2
- package/dist/lib.cjs +1 -1
- package/dist/lib.mjs +1 -1
- package/dist/{list-CGXN1SEJ.mjs → list-C0F4TAPa.mjs} +1 -1
- package/dist/{list-TnYgXXGw.cjs → list-DXxyuwT9.cjs} +1 -1
- package/dist/{prompt-BrXxehR7.mjs → prompt-Br-GiaVj.mjs} +1 -1
- package/dist/{prompt-Cf8Tnep4.cjs → prompt-LEVV0Hst.cjs} +1 -1
- package/dist/{runCodex-CFU1flPl.cjs → runCodex-3jsfzjVM.cjs} +2 -2
- package/dist/{runCodex-Cqxq74Wt.mjs → runCodex-DO1tAsMY.mjs} +2 -2
- package/dist/{types-kB4CXGM6.mjs → types-C1SMg54t.mjs} +53 -7
- package/dist/{types-CGvx6DSD.cjs → types-DwjUGi0J.cjs} +54 -8
- package/package.json +1 -1
- package/scripts/claude_local_launcher.cjs +10 -35
- package/scripts/claude_remote_launcher.cjs +4 -1
- package/scripts/claude_version_utils.cjs +377 -0
- package/scripts/session_hook_forwarder.cjs +48 -0
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import os$1, { homedir } from 'node:os';
|
|
3
3
|
import { randomUUID, randomBytes, createHmac } from 'node:crypto';
|
|
4
|
-
import { l as logger, p as projectPath, j as backoff, k as delay, R as RawJSONLinesSchema, m as AsyncLock, c as configuration, n as readDaemonState, o as clearDaemonState, i as packageJson, r as readSettings, q as readCredentials, g as encodeBase64, u as updateSettings, s as encodeBase64Url, d as decodeBase64, w as writeCredentialsLegacy, t as writeCredentialsDataKey, v as acquireDaemonLock, x as writeDaemonState, A as ApiClient, y as releaseDaemonLock, z as authChallenge, B as clearCredentials, C as clearMachineId, D as getLatestDaemonLog } from './types-
|
|
5
|
-
import { spawn, exec as exec$1,
|
|
4
|
+
import { l as logger, p as projectPath, j as backoff, k as delay, R as RawJSONLinesSchema, m as AsyncLock, c as configuration, n as readDaemonState, o as clearDaemonState, i as packageJson, r as readSettings, q as readCredentials, g as encodeBase64, u as updateSettings, s as encodeBase64Url, d as decodeBase64, w as writeCredentialsLegacy, t as writeCredentialsDataKey, v as acquireDaemonLock, x as writeDaemonState, A as ApiClient, y as releaseDaemonLock, z as authChallenge, B as clearCredentials, C as clearMachineId, D as getLatestDaemonLog } from './types-C1SMg54t.mjs';
|
|
5
|
+
import { spawn, execSync, exec as exec$1, execFileSync } from 'node:child_process';
|
|
6
6
|
import { resolve, join } from 'node:path';
|
|
7
7
|
import { createInterface } from 'node:readline';
|
|
8
|
-
import { existsSync, readFileSync, mkdirSync,
|
|
8
|
+
import { existsSync, readFileSync, mkdirSync, readdirSync, statSync, writeFileSync, unlinkSync, rmSync } from 'node:fs';
|
|
9
9
|
import { exec, spawn as spawn$1, execSync as execSync$1 } from 'child_process';
|
|
10
10
|
import { promisify } from 'util';
|
|
11
|
-
import { existsSync as existsSync$1, readFileSync as readFileSync$1, writeFileSync, chmodSync, unlinkSync } from 'fs';
|
|
11
|
+
import { existsSync as existsSync$1, readFileSync as readFileSync$1, writeFileSync as writeFileSync$1, chmodSync, unlinkSync as unlinkSync$1 } from 'fs';
|
|
12
12
|
import { join as join$1, dirname, basename } from 'path';
|
|
13
13
|
import { readFile } from 'node:fs/promises';
|
|
14
|
-
import fs, { watch
|
|
14
|
+
import fs, { watch, access } from 'fs/promises';
|
|
15
15
|
import { useStdout, useInput, Box, Text, render } from 'ink';
|
|
16
16
|
import React, { useState, useRef, useEffect, useCallback } from 'react';
|
|
17
17
|
import { fileURLToPath } from 'node:url';
|
|
@@ -51,9 +51,15 @@ class Session {
|
|
|
51
51
|
allowedTools;
|
|
52
52
|
_onModeChange;
|
|
53
53
|
initialPermissionMode;
|
|
54
|
+
/** Path to temporary settings file with SessionStart hook (required for session tracking) */
|
|
55
|
+
hookSettingsPath;
|
|
54
56
|
sessionId;
|
|
55
57
|
mode = "local";
|
|
56
58
|
thinking = false;
|
|
59
|
+
/** Callbacks to be notified when session ID is found/changed */
|
|
60
|
+
sessionFoundCallbacks = [];
|
|
61
|
+
/** Keep alive interval reference for cleanup */
|
|
62
|
+
keepAliveInterval;
|
|
57
63
|
constructor(opts) {
|
|
58
64
|
this.path = opts.path;
|
|
59
65
|
this.api = opts.api;
|
|
@@ -67,11 +73,20 @@ class Session {
|
|
|
67
73
|
this.allowedTools = opts.allowedTools;
|
|
68
74
|
this._onModeChange = opts.onModeChange;
|
|
69
75
|
this.initialPermissionMode = opts.initialPermissionMode ?? "default";
|
|
76
|
+
this.hookSettingsPath = opts.hookSettingsPath;
|
|
70
77
|
this.client.keepAlive(this.thinking, this.mode);
|
|
71
|
-
setInterval(() => {
|
|
78
|
+
this.keepAliveInterval = setInterval(() => {
|
|
72
79
|
this.client.keepAlive(this.thinking, this.mode);
|
|
73
80
|
}, 2e3);
|
|
74
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* Cleanup resources (call when session is no longer needed)
|
|
84
|
+
*/
|
|
85
|
+
cleanup = () => {
|
|
86
|
+
clearInterval(this.keepAliveInterval);
|
|
87
|
+
this.sessionFoundCallbacks = [];
|
|
88
|
+
logger.debug("[Session] Cleaned up resources");
|
|
89
|
+
};
|
|
75
90
|
onThinkingChange = (thinking) => {
|
|
76
91
|
this.thinking = thinking;
|
|
77
92
|
this.client.keepAlive(thinking, this.mode);
|
|
@@ -81,6 +96,17 @@ class Session {
|
|
|
81
96
|
this.client.keepAlive(this.thinking, mode);
|
|
82
97
|
this._onModeChange(mode);
|
|
83
98
|
};
|
|
99
|
+
/**
|
|
100
|
+
* Called when Claude session ID is discovered or changed.
|
|
101
|
+
*
|
|
102
|
+
* This is triggered by the SessionStart hook when:
|
|
103
|
+
* - Claude starts a new session (fresh start)
|
|
104
|
+
* - Claude resumes a session (--continue, --resume flags)
|
|
105
|
+
* - Claude forks a session (/compact, double-escape fork)
|
|
106
|
+
*
|
|
107
|
+
* Updates internal state, syncs to API metadata, and notifies
|
|
108
|
+
* all registered callbacks (e.g., SessionScanner) about the change.
|
|
109
|
+
*/
|
|
84
110
|
onSessionFound = (sessionId) => {
|
|
85
111
|
this.sessionId = sessionId;
|
|
86
112
|
this.client.updateMetadata((metadata) => ({
|
|
@@ -88,6 +114,24 @@ class Session {
|
|
|
88
114
|
claudeSessionId: sessionId
|
|
89
115
|
}));
|
|
90
116
|
logger.debug(`[Session] Claude Code session ID ${sessionId} added to metadata`);
|
|
117
|
+
for (const callback of this.sessionFoundCallbacks) {
|
|
118
|
+
callback(sessionId);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
/**
|
|
122
|
+
* Register a callback to be notified when session ID is found/changed
|
|
123
|
+
*/
|
|
124
|
+
addSessionFoundCallback = (callback) => {
|
|
125
|
+
this.sessionFoundCallbacks.push(callback);
|
|
126
|
+
};
|
|
127
|
+
/**
|
|
128
|
+
* Remove a session found callback
|
|
129
|
+
*/
|
|
130
|
+
removeSessionFoundCallback = (callback) => {
|
|
131
|
+
const index = this.sessionFoundCallbacks.indexOf(callback);
|
|
132
|
+
if (index !== -1) {
|
|
133
|
+
this.sessionFoundCallbacks.splice(index, 1);
|
|
134
|
+
}
|
|
91
135
|
};
|
|
92
136
|
/**
|
|
93
137
|
* Clear the current session ID (used by /clear command)
|
|
@@ -98,13 +142,18 @@ class Session {
|
|
|
98
142
|
};
|
|
99
143
|
/**
|
|
100
144
|
* Consume one-time Claude flags from claudeArgs after Claude spawn
|
|
101
|
-
*
|
|
145
|
+
* Handles: --resume (with or without session ID), --continue
|
|
102
146
|
*/
|
|
103
147
|
consumeOneTimeFlags = () => {
|
|
104
148
|
if (!this.claudeArgs) return;
|
|
105
149
|
const filteredArgs = [];
|
|
106
150
|
for (let i = 0; i < this.claudeArgs.length; i++) {
|
|
107
|
-
|
|
151
|
+
const arg = this.claudeArgs[i];
|
|
152
|
+
if (arg === "--continue") {
|
|
153
|
+
logger.debug("[Session] Consumed --continue flag");
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (arg === "--resume") {
|
|
108
157
|
if (i + 1 < this.claudeArgs.length) {
|
|
109
158
|
const nextArg = this.claudeArgs[i + 1];
|
|
110
159
|
if (!nextArg.startsWith("-") && nextArg.includes("-")) {
|
|
@@ -116,9 +165,9 @@ class Session {
|
|
|
116
165
|
} else {
|
|
117
166
|
logger.debug("[Session] Consumed --resume flag (no session ID)");
|
|
118
167
|
}
|
|
119
|
-
|
|
120
|
-
filteredArgs.push(this.claudeArgs[i]);
|
|
168
|
+
continue;
|
|
121
169
|
}
|
|
170
|
+
filteredArgs.push(arg);
|
|
122
171
|
}
|
|
123
172
|
this.claudeArgs = filteredArgs.length > 0 ? filteredArgs : void 0;
|
|
124
173
|
logger.debug(`[Session] Consumed one-time flags, remaining args:`, this.claudeArgs);
|
|
@@ -317,31 +366,20 @@ const claudeCliPath = resolve(join(projectPath(), "scripts", "claude_local_launc
|
|
|
317
366
|
async function claudeLocal(opts) {
|
|
318
367
|
const projectDir = getProjectPath(opts.path);
|
|
319
368
|
mkdirSync(projectDir, { recursive: true });
|
|
320
|
-
const
|
|
321
|
-
|
|
322
|
-
const
|
|
323
|
-
const detectedIdsFileSystem = /* @__PURE__ */ new Set();
|
|
324
|
-
watcher.on("change", (event, filename) => {
|
|
325
|
-
if (typeof filename === "string" && filename.toLowerCase().endsWith(".jsonl")) {
|
|
326
|
-
logger.debug("change", event, filename);
|
|
327
|
-
const sessionId = filename.replace(".jsonl", "");
|
|
328
|
-
if (detectedIdsFileSystem.has(sessionId)) {
|
|
329
|
-
return;
|
|
330
|
-
}
|
|
331
|
-
detectedIdsFileSystem.add(sessionId);
|
|
332
|
-
if (resolvedSessionId) {
|
|
333
|
-
return;
|
|
334
|
-
}
|
|
335
|
-
if (detectedIdsRandomUUID.has(sessionId)) {
|
|
336
|
-
resolvedSessionId = sessionId;
|
|
337
|
-
opts.onSessionFound(sessionId);
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
});
|
|
369
|
+
const hasContinueFlag = opts.claudeArgs?.includes("--continue");
|
|
370
|
+
const hasResumeFlag = opts.claudeArgs?.includes("--resume");
|
|
371
|
+
const hasUserSessionControl = hasContinueFlag || hasResumeFlag;
|
|
341
372
|
let startFrom = opts.sessionId;
|
|
342
373
|
if (opts.sessionId && !claudeCheckSession(opts.sessionId, opts.path)) {
|
|
343
374
|
startFrom = null;
|
|
344
375
|
}
|
|
376
|
+
if (startFrom) {
|
|
377
|
+
logger.debug(`[ClaudeLocal] Will resume existing session: ${startFrom}`);
|
|
378
|
+
} else if (hasUserSessionControl) {
|
|
379
|
+
logger.debug(`[ClaudeLocal] User passed ${hasContinueFlag ? "--continue" : "--resume"} flag, session ID will be determined by hook`);
|
|
380
|
+
} else {
|
|
381
|
+
logger.debug(`[ClaudeLocal] Fresh start, session ID will be provided by hook`);
|
|
382
|
+
}
|
|
345
383
|
let thinking = false;
|
|
346
384
|
let stopThinkingTimeout = null;
|
|
347
385
|
const updateThinking = (newThinking) => {
|
|
@@ -358,15 +396,10 @@ async function claudeLocal(opts) {
|
|
|
358
396
|
if (Object.keys(direnvVars).length > 0) {
|
|
359
397
|
logger.debug(`[ClaudeLocal] Loaded ${Object.keys(direnvVars).length} direnv environment variables`);
|
|
360
398
|
}
|
|
361
|
-
const env = {
|
|
362
|
-
...process.env,
|
|
363
|
-
...direnvVars,
|
|
364
|
-
...opts.claudeEnvVars
|
|
365
|
-
};
|
|
366
399
|
process.stdin.pause();
|
|
367
400
|
await new Promise((r, reject) => {
|
|
368
401
|
const args = [];
|
|
369
|
-
if (startFrom) {
|
|
402
|
+
if (!hasUserSessionControl && startFrom) {
|
|
370
403
|
args.push("--resume", startFrom);
|
|
371
404
|
}
|
|
372
405
|
args.push("--append-system-prompt", systemPrompt);
|
|
@@ -379,9 +412,18 @@ async function claudeLocal(opts) {
|
|
|
379
412
|
if (opts.claudeArgs) {
|
|
380
413
|
args.push(...opts.claudeArgs);
|
|
381
414
|
}
|
|
415
|
+
args.push("--settings", opts.hookSettingsPath);
|
|
416
|
+
logger.debug(`[ClaudeLocal] Using hook settings: ${opts.hookSettingsPath}`);
|
|
382
417
|
if (!claudeCliPath || !existsSync(claudeCliPath)) {
|
|
383
418
|
throw new Error("Claude local launcher not found. Please ensure HAPPY_PROJECT_ROOT is set correctly for development.");
|
|
384
419
|
}
|
|
420
|
+
const env = {
|
|
421
|
+
...process.env,
|
|
422
|
+
...direnvVars,
|
|
423
|
+
...opts.claudeEnvVars
|
|
424
|
+
};
|
|
425
|
+
logger.debug(`[ClaudeLocal] Spawning launcher: ${claudeCliPath}`);
|
|
426
|
+
logger.debug(`[ClaudeLocal] Args: ${JSON.stringify(args)}`);
|
|
385
427
|
const child = spawn("node", [claudeCliPath, ...args], {
|
|
386
428
|
stdio: ["inherit", "inherit", "inherit", "pipe"],
|
|
387
429
|
signal: opts.abort,
|
|
@@ -398,13 +440,6 @@ async function claudeLocal(opts) {
|
|
|
398
440
|
try {
|
|
399
441
|
const message = JSON.parse(line);
|
|
400
442
|
switch (message.type) {
|
|
401
|
-
case "uuid":
|
|
402
|
-
detectedIdsRandomUUID.add(message.value);
|
|
403
|
-
if (!resolvedSessionId && detectedIdsFileSystem.has(message.value)) {
|
|
404
|
-
resolvedSessionId = message.value;
|
|
405
|
-
opts.onSessionFound(message.value);
|
|
406
|
-
}
|
|
407
|
-
break;
|
|
408
443
|
case "fetch-start":
|
|
409
444
|
activeFetches.set(message.id, {
|
|
410
445
|
hostname: message.hostname,
|
|
@@ -458,7 +493,6 @@ async function claudeLocal(opts) {
|
|
|
458
493
|
});
|
|
459
494
|
});
|
|
460
495
|
} finally {
|
|
461
|
-
watcher.close();
|
|
462
496
|
process.stdin.resume();
|
|
463
497
|
if (stopThinkingTimeout) {
|
|
464
498
|
clearTimeout(stopThinkingTimeout);
|
|
@@ -466,7 +500,7 @@ async function claudeLocal(opts) {
|
|
|
466
500
|
}
|
|
467
501
|
updateThinking(false);
|
|
468
502
|
}
|
|
469
|
-
return
|
|
503
|
+
return startFrom;
|
|
470
504
|
}
|
|
471
505
|
|
|
472
506
|
class Future {
|
|
@@ -562,7 +596,7 @@ function startFileWatcher(file, onFileChange) {
|
|
|
562
596
|
while (true) {
|
|
563
597
|
try {
|
|
564
598
|
logger.debug(`[FILE_WATCHER] Starting watcher for ${file}`);
|
|
565
|
-
const watcher = watch
|
|
599
|
+
const watcher = watch(file, { persistent: true, signal: abortController.signal });
|
|
566
600
|
for await (const event of watcher) {
|
|
567
601
|
if (abortController.signal.aborted) {
|
|
568
602
|
return;
|
|
@@ -720,6 +754,10 @@ async function claudeLocalLauncher(session) {
|
|
|
720
754
|
}
|
|
721
755
|
}
|
|
722
756
|
});
|
|
757
|
+
const scannerSessionCallback = (sessionId) => {
|
|
758
|
+
scanner.onNewSession(sessionId);
|
|
759
|
+
};
|
|
760
|
+
session.addSessionFoundCallback(scannerSessionCallback);
|
|
723
761
|
let exitReason = null;
|
|
724
762
|
const processAbortController = new AbortController();
|
|
725
763
|
let exutFuture = new Future();
|
|
@@ -772,7 +810,8 @@ async function claudeLocalLauncher(session) {
|
|
|
772
810
|
claudeEnvVars: session.claudeEnvVars,
|
|
773
811
|
claudeArgs: session.claudeArgs,
|
|
774
812
|
mcpServers: session.mcpServers,
|
|
775
|
-
allowedTools: session.allowedTools
|
|
813
|
+
allowedTools: session.allowedTools,
|
|
814
|
+
hookSettingsPath: session.hookSettingsPath
|
|
776
815
|
});
|
|
777
816
|
session.consumeOneTimeFlags();
|
|
778
817
|
if (!exitReason) {
|
|
@@ -803,6 +842,7 @@ async function claudeLocalLauncher(session) {
|
|
|
803
842
|
session.client.rpcHandlerManager.registerHandler("switch", async () => {
|
|
804
843
|
});
|
|
805
844
|
session.queue.setOnMessage(null);
|
|
845
|
+
session.removeSessionFoundCallback(scannerSessionCallback);
|
|
806
846
|
await scanner.cleanup();
|
|
807
847
|
}
|
|
808
848
|
return exitReason || "exit";
|
|
@@ -822,6 +862,39 @@ class MessageBuffer {
|
|
|
822
862
|
this.messages.push(message);
|
|
823
863
|
this.notifyListeners();
|
|
824
864
|
}
|
|
865
|
+
/**
|
|
866
|
+
* Update the last message of a specific type by appending content to it
|
|
867
|
+
* Useful for streaming responses where deltas should accumulate in one message
|
|
868
|
+
*/
|
|
869
|
+
updateLastMessage(contentDelta, type = "assistant") {
|
|
870
|
+
for (let i = this.messages.length - 1; i >= 0; i--) {
|
|
871
|
+
if (this.messages[i].type === type) {
|
|
872
|
+
const oldMessage = this.messages[i];
|
|
873
|
+
const updatedMessage = {
|
|
874
|
+
...oldMessage,
|
|
875
|
+
content: oldMessage.content + contentDelta
|
|
876
|
+
};
|
|
877
|
+
this.messages[i] = updatedMessage;
|
|
878
|
+
this.notifyListeners();
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
this.addMessage(contentDelta, type);
|
|
883
|
+
}
|
|
884
|
+
/**
|
|
885
|
+
* Remove the last message of a specific type
|
|
886
|
+
* Useful for removing placeholder messages like "Thinking..." when actual response starts
|
|
887
|
+
*/
|
|
888
|
+
removeLastMessage(type) {
|
|
889
|
+
for (let i = this.messages.length - 1; i >= 0; i--) {
|
|
890
|
+
if (this.messages[i].type === type) {
|
|
891
|
+
this.messages.splice(i, 1);
|
|
892
|
+
this.notifyListeners();
|
|
893
|
+
return true;
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
return false;
|
|
897
|
+
}
|
|
825
898
|
getMessages() {
|
|
826
899
|
return [...this.messages];
|
|
827
900
|
}
|
|
@@ -1069,8 +1142,92 @@ class AbortError extends Error {
|
|
|
1069
1142
|
|
|
1070
1143
|
const __filename$1 = fileURLToPath(import.meta.url);
|
|
1071
1144
|
const __dirname$1 = join(__filename$1, "..");
|
|
1145
|
+
function getGlobalClaudeVersion() {
|
|
1146
|
+
try {
|
|
1147
|
+
const cleanEnv = getCleanEnv();
|
|
1148
|
+
const output = execSync("claude --version", {
|
|
1149
|
+
encoding: "utf8",
|
|
1150
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1151
|
+
cwd: homedir(),
|
|
1152
|
+
env: cleanEnv
|
|
1153
|
+
}).trim();
|
|
1154
|
+
const match = output.match(/(\d+\.\d+\.\d+)/);
|
|
1155
|
+
logger.debug(`[Claude SDK] Global claude --version output: ${output}`);
|
|
1156
|
+
return match ? match[1] : null;
|
|
1157
|
+
} catch {
|
|
1158
|
+
return null;
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
function getCleanEnv() {
|
|
1162
|
+
const env = { ...process.env };
|
|
1163
|
+
const cwd = process.cwd();
|
|
1164
|
+
const pathSep = process.platform === "win32" ? ";" : ":";
|
|
1165
|
+
const pathKey = process.platform === "win32" ? "Path" : "PATH";
|
|
1166
|
+
const actualPathKey = Object.keys(env).find((k) => k.toLowerCase() === "path") || pathKey;
|
|
1167
|
+
if (env[actualPathKey]) {
|
|
1168
|
+
const cleanPath = env[actualPathKey].split(pathSep).filter((p) => {
|
|
1169
|
+
const normalizedP = p.replace(/\\/g, "/").toLowerCase();
|
|
1170
|
+
const normalizedCwd = cwd.replace(/\\/g, "/").toLowerCase();
|
|
1171
|
+
return !normalizedP.startsWith(normalizedCwd);
|
|
1172
|
+
}).join(pathSep);
|
|
1173
|
+
env[actualPathKey] = cleanPath;
|
|
1174
|
+
logger.debug(`[Claude SDK] Cleaned PATH, removed local paths from: ${cwd}`);
|
|
1175
|
+
}
|
|
1176
|
+
return env;
|
|
1177
|
+
}
|
|
1178
|
+
function findGlobalClaudePath() {
|
|
1179
|
+
const homeDir = homedir();
|
|
1180
|
+
const cleanEnv = getCleanEnv();
|
|
1181
|
+
try {
|
|
1182
|
+
execSync("claude --version", {
|
|
1183
|
+
encoding: "utf8",
|
|
1184
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1185
|
+
cwd: homeDir,
|
|
1186
|
+
env: cleanEnv
|
|
1187
|
+
});
|
|
1188
|
+
logger.debug("[Claude SDK] Global claude command available (checked with clean PATH)");
|
|
1189
|
+
return "claude";
|
|
1190
|
+
} catch {
|
|
1191
|
+
}
|
|
1192
|
+
if (process.platform !== "win32") {
|
|
1193
|
+
try {
|
|
1194
|
+
const result = execSync("which claude", {
|
|
1195
|
+
encoding: "utf8",
|
|
1196
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1197
|
+
cwd: homeDir,
|
|
1198
|
+
env: cleanEnv
|
|
1199
|
+
}).trim();
|
|
1200
|
+
if (result && existsSync(result)) {
|
|
1201
|
+
logger.debug(`[Claude SDK] Found global claude path via which: ${result}`);
|
|
1202
|
+
return result;
|
|
1203
|
+
}
|
|
1204
|
+
} catch {
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
return null;
|
|
1208
|
+
}
|
|
1072
1209
|
function getDefaultClaudeCodePath() {
|
|
1073
|
-
|
|
1210
|
+
const nodeModulesPath = join(__dirname$1, "..", "..", "..", "node_modules", "@anthropic-ai", "claude-code", "cli.js");
|
|
1211
|
+
if (process.env.HAPPY_CLAUDE_PATH) {
|
|
1212
|
+
logger.debug(`[Claude SDK] Using HAPPY_CLAUDE_PATH: ${process.env.HAPPY_CLAUDE_PATH}`);
|
|
1213
|
+
return process.env.HAPPY_CLAUDE_PATH;
|
|
1214
|
+
}
|
|
1215
|
+
if (process.env.HAPPY_USE_BUNDLED_CLAUDE === "1") {
|
|
1216
|
+
logger.debug(`[Claude SDK] Forced bundled version: ${nodeModulesPath}`);
|
|
1217
|
+
return nodeModulesPath;
|
|
1218
|
+
}
|
|
1219
|
+
const globalPath = findGlobalClaudePath();
|
|
1220
|
+
if (!globalPath) {
|
|
1221
|
+
logger.debug(`[Claude SDK] No global claude found, using bundled: ${nodeModulesPath}`);
|
|
1222
|
+
return nodeModulesPath;
|
|
1223
|
+
}
|
|
1224
|
+
const globalVersion = getGlobalClaudeVersion();
|
|
1225
|
+
logger.debug(`[Claude SDK] Global version: ${globalVersion || "unknown"}`);
|
|
1226
|
+
if (!globalVersion) {
|
|
1227
|
+
logger.debug(`[Claude SDK] Cannot compare versions, using global: ${globalPath}`);
|
|
1228
|
+
return globalPath;
|
|
1229
|
+
}
|
|
1230
|
+
return globalPath;
|
|
1074
1231
|
}
|
|
1075
1232
|
function logDebug(message) {
|
|
1076
1233
|
if (process.env.DEBUG) {
|
|
@@ -2074,6 +2231,16 @@ class PermissionHandler {
|
|
|
2074
2231
|
} else {
|
|
2075
2232
|
pending.resolve({ behavior: "deny", message: response.reason || "Plan rejected" });
|
|
2076
2233
|
}
|
|
2234
|
+
} else if (pending.toolName === "AskUserQuestion") {
|
|
2235
|
+
if (response.approved) {
|
|
2236
|
+
const inputWithAnswers = {
|
|
2237
|
+
...pending.input,
|
|
2238
|
+
answers: response.answers || {}
|
|
2239
|
+
};
|
|
2240
|
+
pending.resolve({ behavior: "allow", updatedInput: inputWithAnswers });
|
|
2241
|
+
} else {
|
|
2242
|
+
pending.resolve({ behavior: "deny", message: response.reason || "User declined to answer the questions." });
|
|
2243
|
+
}
|
|
2077
2244
|
} else {
|
|
2078
2245
|
const result = response.approved ? { behavior: "allow", updatedInput: pending.input || {} } : { behavior: "deny", message: response.reason || `The user doesn't want to proceed with this tool use. The tool use was rejected (eg. if it was a file edit, the new_string was NOT written to the file). STOP what you are doing and wait for the user to tell you how to proceed.` };
|
|
2079
2246
|
pending.resolve(result);
|
|
@@ -2099,11 +2266,13 @@ class PermissionHandler {
|
|
|
2099
2266
|
return { behavior: "allow", updatedInput: input };
|
|
2100
2267
|
}
|
|
2101
2268
|
const descriptor = getToolDescriptor(toolName);
|
|
2102
|
-
if (
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2269
|
+
if (toolName !== "AskUserQuestion") {
|
|
2270
|
+
if (this.permissionMode === "bypassPermissions") {
|
|
2271
|
+
return { behavior: "allow", updatedInput: input };
|
|
2272
|
+
}
|
|
2273
|
+
if (this.permissionMode === "acceptEdits" && descriptor.edit) {
|
|
2274
|
+
return { behavior: "allow", updatedInput: input };
|
|
2275
|
+
}
|
|
2107
2276
|
}
|
|
2108
2277
|
let toolCallId = this.resolveToolCallId(toolName, input);
|
|
2109
2278
|
if (!toolCallId) {
|
|
@@ -3185,7 +3354,8 @@ async function loop(opts) {
|
|
|
3185
3354
|
messageQueue: opts.messageQueue,
|
|
3186
3355
|
allowedTools: opts.allowedTools,
|
|
3187
3356
|
onModeChange: opts.onModeChange,
|
|
3188
|
-
initialPermissionMode: opts.permissionMode
|
|
3357
|
+
initialPermissionMode: opts.permissionMode,
|
|
3358
|
+
hookSettingsPath: opts.hookSettingsPath
|
|
3189
3359
|
});
|
|
3190
3360
|
if (opts.onSessionReady) {
|
|
3191
3361
|
opts.onSessionReady(session);
|
|
@@ -5085,6 +5255,110 @@ async function startHappyServer(client) {
|
|
|
5085
5255
|
};
|
|
5086
5256
|
}
|
|
5087
5257
|
|
|
5258
|
+
async function startHookServer(options) {
|
|
5259
|
+
const { onSessionHook } = options;
|
|
5260
|
+
return new Promise((resolve, reject) => {
|
|
5261
|
+
const server = createServer(async (req, res) => {
|
|
5262
|
+
if (req.method === "POST" && req.url === "/hook/session-start") {
|
|
5263
|
+
const timeout = setTimeout(() => {
|
|
5264
|
+
if (!res.headersSent) {
|
|
5265
|
+
logger.debug("[hookServer] Request timeout");
|
|
5266
|
+
res.writeHead(408).end("timeout");
|
|
5267
|
+
}
|
|
5268
|
+
}, 5e3);
|
|
5269
|
+
try {
|
|
5270
|
+
const chunks = [];
|
|
5271
|
+
for await (const chunk of req) {
|
|
5272
|
+
chunks.push(chunk);
|
|
5273
|
+
}
|
|
5274
|
+
clearTimeout(timeout);
|
|
5275
|
+
const body = Buffer.concat(chunks).toString("utf-8");
|
|
5276
|
+
logger.debug("[hookServer] Received session hook:", body);
|
|
5277
|
+
let data = {};
|
|
5278
|
+
try {
|
|
5279
|
+
data = JSON.parse(body);
|
|
5280
|
+
} catch (parseError) {
|
|
5281
|
+
logger.debug("[hookServer] Failed to parse hook data as JSON:", parseError);
|
|
5282
|
+
}
|
|
5283
|
+
const sessionId = data.session_id || data.sessionId;
|
|
5284
|
+
if (sessionId) {
|
|
5285
|
+
logger.debug(`[hookServer] Session hook received session ID: ${sessionId}`);
|
|
5286
|
+
onSessionHook(sessionId, data);
|
|
5287
|
+
} else {
|
|
5288
|
+
logger.debug("[hookServer] Session hook received but no session_id found in data");
|
|
5289
|
+
}
|
|
5290
|
+
res.writeHead(200, { "Content-Type": "text/plain" }).end("ok");
|
|
5291
|
+
} catch (error) {
|
|
5292
|
+
clearTimeout(timeout);
|
|
5293
|
+
logger.debug("[hookServer] Error handling session hook:", error);
|
|
5294
|
+
if (!res.headersSent) {
|
|
5295
|
+
res.writeHead(500).end("error");
|
|
5296
|
+
}
|
|
5297
|
+
}
|
|
5298
|
+
return;
|
|
5299
|
+
}
|
|
5300
|
+
res.writeHead(404).end("not found");
|
|
5301
|
+
});
|
|
5302
|
+
server.listen(0, "127.0.0.1", () => {
|
|
5303
|
+
const address = server.address();
|
|
5304
|
+
if (!address || typeof address === "string") {
|
|
5305
|
+
reject(new Error("Failed to get server address"));
|
|
5306
|
+
return;
|
|
5307
|
+
}
|
|
5308
|
+
const port = address.port;
|
|
5309
|
+
logger.debug(`[hookServer] Started on port ${port}`);
|
|
5310
|
+
resolve({
|
|
5311
|
+
port,
|
|
5312
|
+
stop: () => {
|
|
5313
|
+
server.close();
|
|
5314
|
+
logger.debug("[hookServer] Stopped");
|
|
5315
|
+
}
|
|
5316
|
+
});
|
|
5317
|
+
});
|
|
5318
|
+
server.on("error", (err) => {
|
|
5319
|
+
logger.debug("[hookServer] Server error:", err);
|
|
5320
|
+
reject(err);
|
|
5321
|
+
});
|
|
5322
|
+
});
|
|
5323
|
+
}
|
|
5324
|
+
|
|
5325
|
+
function generateHookSettingsFile(port) {
|
|
5326
|
+
const hooksDir = join(configuration.happyHomeDir, "tmp", "hooks");
|
|
5327
|
+
mkdirSync(hooksDir, { recursive: true });
|
|
5328
|
+
const filename = `session-hook-${process.pid}.json`;
|
|
5329
|
+
const filepath = join(hooksDir, filename);
|
|
5330
|
+
const forwarderScript = resolve(projectPath(), "scripts", "session_hook_forwarder.cjs");
|
|
5331
|
+
const hookCommand = `node "${forwarderScript}" ${port}`;
|
|
5332
|
+
const settings = {
|
|
5333
|
+
hooks: {
|
|
5334
|
+
SessionStart: [
|
|
5335
|
+
{
|
|
5336
|
+
matcher: "*",
|
|
5337
|
+
hooks: [
|
|
5338
|
+
{
|
|
5339
|
+
type: "command",
|
|
5340
|
+
command: hookCommand
|
|
5341
|
+
}
|
|
5342
|
+
]
|
|
5343
|
+
}
|
|
5344
|
+
]
|
|
5345
|
+
}
|
|
5346
|
+
};
|
|
5347
|
+
writeFileSync(filepath, JSON.stringify(settings, null, 2));
|
|
5348
|
+
logger.debug(`[generateHookSettings] Created hook settings file: ${filepath}`);
|
|
5349
|
+
return filepath;
|
|
5350
|
+
}
|
|
5351
|
+
function cleanupHookSettingsFile(filepath) {
|
|
5352
|
+
try {
|
|
5353
|
+
if (existsSync(filepath)) {
|
|
5354
|
+
unlinkSync(filepath);
|
|
5355
|
+
logger.debug(`[generateHookSettings] Cleaned up hook settings file: ${filepath}`);
|
|
5356
|
+
}
|
|
5357
|
+
} catch (error) {
|
|
5358
|
+
logger.debug(`[generateHookSettings] Failed to cleanup hook settings file: ${error}`);
|
|
5359
|
+
}
|
|
5360
|
+
}
|
|
5361
|
+
|
|
5088
5362
|
function registerKillSessionHandler(rpcHandlerManager, killThisHappy) {
|
|
5089
5363
|
rpcHandlerManager.registerHandler("killSession", async () => {
|
|
5090
5364
|
logger.debug("Kill session request received");
|
|
@@ -5188,6 +5462,22 @@ async function runClaude(credentials, options = {}) {
|
|
|
5188
5462
|
const session = api.sessionSyncClient(response);
|
|
5189
5463
|
const happyServer = await startHappyServer(session);
|
|
5190
5464
|
logger.debug(`[START] Happy MCP server started at ${happyServer.url}`);
|
|
5465
|
+
let currentSession = null;
|
|
5466
|
+
const hookServer = await startHookServer({
|
|
5467
|
+
onSessionHook: (sessionId, data) => {
|
|
5468
|
+
logger.debug(`[START] Session hook received: ${sessionId}`, data);
|
|
5469
|
+
if (currentSession) {
|
|
5470
|
+
const previousSessionId = currentSession.sessionId;
|
|
5471
|
+
if (previousSessionId !== sessionId) {
|
|
5472
|
+
logger.debug(`[START] Claude session ID changed: ${previousSessionId} -> ${sessionId}`);
|
|
5473
|
+
currentSession.onSessionFound(sessionId);
|
|
5474
|
+
}
|
|
5475
|
+
}
|
|
5476
|
+
}
|
|
5477
|
+
});
|
|
5478
|
+
logger.debug(`[START] Hook server started on port ${hookServer.port}`);
|
|
5479
|
+
const hookSettingsPath = generateHookSettingsFile(hookServer.port);
|
|
5480
|
+
logger.debug(`[START] Generated hook settings file: ${hookSettingsPath}`);
|
|
5191
5481
|
const logPath = logger.logFilePath;
|
|
5192
5482
|
logger.infoDeveloper(`Session: ${response.id}`);
|
|
5193
5483
|
logger.infoDeveloper(`Logs: ${logPath}`);
|
|
@@ -5351,6 +5641,11 @@ async function runClaude(credentials, options = {}) {
|
|
|
5351
5641
|
}
|
|
5352
5642
|
stopCaffeinate();
|
|
5353
5643
|
happyServer.stop();
|
|
5644
|
+
hookServer.stop();
|
|
5645
|
+
cleanupHookSettingsFile(hookSettingsPath);
|
|
5646
|
+
if (currentSession) {
|
|
5647
|
+
currentSession.cleanup();
|
|
5648
|
+
}
|
|
5354
5649
|
logger.debug("[START] Cleanup complete, exiting");
|
|
5355
5650
|
process.exit(0);
|
|
5356
5651
|
} catch (error) {
|
|
@@ -5384,7 +5679,8 @@ async function runClaude(credentials, options = {}) {
|
|
|
5384
5679
|
controlledByUser: newMode === "local"
|
|
5385
5680
|
}));
|
|
5386
5681
|
},
|
|
5387
|
-
onSessionReady: (
|
|
5682
|
+
onSessionReady: (sessionInstance) => {
|
|
5683
|
+
currentSession = sessionInstance;
|
|
5388
5684
|
},
|
|
5389
5685
|
mcpServers: {
|
|
5390
5686
|
"happy": {
|
|
@@ -5394,7 +5690,8 @@ async function runClaude(credentials, options = {}) {
|
|
|
5394
5690
|
},
|
|
5395
5691
|
session,
|
|
5396
5692
|
claudeEnvVars: options.claudeEnvVars,
|
|
5397
|
-
claudeArgs: options.claudeArgs
|
|
5693
|
+
claudeArgs: options.claudeArgs,
|
|
5694
|
+
hookSettingsPath
|
|
5398
5695
|
});
|
|
5399
5696
|
session.sendSessionDeath();
|
|
5400
5697
|
logger.debug("Waiting for socket to flush...");
|
|
@@ -5405,6 +5702,9 @@ async function runClaude(credentials, options = {}) {
|
|
|
5405
5702
|
logger.debug("Stopped sleep prevention");
|
|
5406
5703
|
happyServer.stop();
|
|
5407
5704
|
logger.debug("Stopped Happy MCP server");
|
|
5705
|
+
hookServer.stop();
|
|
5706
|
+
cleanupHookSettingsFile(hookSettingsPath);
|
|
5707
|
+
logger.debug("Stopped hook server");
|
|
5408
5708
|
process.exit(0);
|
|
5409
5709
|
}
|
|
5410
5710
|
|
|
@@ -5456,7 +5756,7 @@ async function install$1() {
|
|
|
5456
5756
|
</dict>
|
|
5457
5757
|
</plist>
|
|
5458
5758
|
`);
|
|
5459
|
-
writeFileSync(PLIST_FILE$1, plistContent);
|
|
5759
|
+
writeFileSync$1(PLIST_FILE$1, plistContent);
|
|
5460
5760
|
chmodSync(PLIST_FILE$1, 420);
|
|
5461
5761
|
logger.info(`Created daemon plist at ${PLIST_FILE$1}`);
|
|
5462
5762
|
execSync$1(`launchctl load ${PLIST_FILE$1}`, { stdio: "inherit" });
|
|
@@ -5493,7 +5793,7 @@ async function uninstall$1() {
|
|
|
5493
5793
|
} catch (error) {
|
|
5494
5794
|
logger.info("Failed to unload daemon (it might not be running)");
|
|
5495
5795
|
}
|
|
5496
|
-
unlinkSync(PLIST_FILE);
|
|
5796
|
+
unlinkSync$1(PLIST_FILE);
|
|
5497
5797
|
logger.info(`Removed daemon plist from ${PLIST_FILE}`);
|
|
5498
5798
|
logger.info("Daemon uninstalled successfully");
|
|
5499
5799
|
} catch (error) {
|
|
@@ -6510,7 +6810,7 @@ async function handleConnectVendor(vendor, displayName) {
|
|
|
6510
6810
|
return;
|
|
6511
6811
|
} else if (subcommand === "codex") {
|
|
6512
6812
|
try {
|
|
6513
|
-
const { runCodex } = await import('./runCodex-
|
|
6813
|
+
const { runCodex } = await import('./runCodex-DO1tAsMY.mjs');
|
|
6514
6814
|
let startedBy = void 0;
|
|
6515
6815
|
for (let i = 1; i < args.length; i++) {
|
|
6516
6816
|
if (args[i] === "--started-by") {
|
|
@@ -6555,7 +6855,7 @@ async function handleConnectVendor(vendor, displayName) {
|
|
|
6555
6855
|
} else if (subcommand === "list") {
|
|
6556
6856
|
try {
|
|
6557
6857
|
const { credentials } = await authAndSetupMachineIfNeeded();
|
|
6558
|
-
const { listSessions } = await import('./list-
|
|
6858
|
+
const { listSessions } = await import('./list-C0F4TAPa.mjs');
|
|
6559
6859
|
let sessionId;
|
|
6560
6860
|
let titleFilter;
|
|
6561
6861
|
let recentMsgs;
|
|
@@ -6657,7 +6957,7 @@ Examples:
|
|
|
6657
6957
|
process.exit(1);
|
|
6658
6958
|
}
|
|
6659
6959
|
const { credentials } = await authAndSetupMachineIfNeeded();
|
|
6660
|
-
const { promptSession } = await import('./prompt-
|
|
6960
|
+
const { promptSession } = await import('./prompt-Br-GiaVj.mjs');
|
|
6661
6961
|
await promptSession(credentials, sessionId, promptText, timeoutMinutes ?? void 0);
|
|
6662
6962
|
} catch (error) {
|
|
6663
6963
|
console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
|