@zhigang1992/happy-cli 0.12.12 → 0.12.14
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-DLt6o6FN.cjs → index-DGfkEaE6.cjs} +559 -64
- package/dist/{index-B0AakJdd.mjs → index-DVKGbEA2.mjs} +566 -71
- package/dist/index.cjs +2 -2
- package/dist/index.mjs +2 -2
- package/dist/lib.cjs +1 -1
- package/dist/lib.d.cts +7 -0
- package/dist/lib.d.mts +7 -0
- package/dist/lib.mjs +1 -1
- package/dist/{list-CGXN1SEJ.mjs → list-Bwy2qrWu.mjs} +1 -1
- package/dist/{list-TnYgXXGw.cjs → list-S3T6MByP.cjs} +1 -1
- package/dist/{prompt-Cf8Tnep4.cjs → prompt-5iJXZPz1.cjs} +1 -1
- package/dist/{prompt-BrXxehR7.mjs → prompt-Bfs9-NO9.mjs} +1 -1
- package/dist/{runCodex-CFU1flPl.cjs → runCodex-DJSRJ2Pg.cjs} +2 -2
- package/dist/{runCodex-Cqxq74Wt.mjs → runCodex-DcdBh2dO.mjs} +2 -2
- package/dist/{types-CGvx6DSD.cjs → types-Cxw1JC-9.cjs} +54 -8
- package/dist/{types-kB4CXGM6.mjs → types-HBw6XZbv.mjs} +53 -7
- 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
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
var chalk = require('chalk');
|
|
4
4
|
var os = require('node:os');
|
|
5
5
|
var node_crypto = require('node:crypto');
|
|
6
|
-
var types = require('./types-
|
|
6
|
+
var types = require('./types-Cxw1JC-9.cjs');
|
|
7
7
|
var node_child_process = require('node:child_process');
|
|
8
8
|
var node_path = require('node:path');
|
|
9
9
|
var node_readline = require('node:readline');
|
|
@@ -74,9 +74,15 @@ class Session {
|
|
|
74
74
|
allowedTools;
|
|
75
75
|
_onModeChange;
|
|
76
76
|
initialPermissionMode;
|
|
77
|
+
/** Path to temporary settings file with SessionStart hook (required for session tracking) */
|
|
78
|
+
hookSettingsPath;
|
|
77
79
|
sessionId;
|
|
78
80
|
mode = "local";
|
|
79
81
|
thinking = false;
|
|
82
|
+
/** Callbacks to be notified when session ID is found/changed */
|
|
83
|
+
sessionFoundCallbacks = [];
|
|
84
|
+
/** Keep alive interval reference for cleanup */
|
|
85
|
+
keepAliveInterval;
|
|
80
86
|
constructor(opts) {
|
|
81
87
|
this.path = opts.path;
|
|
82
88
|
this.api = opts.api;
|
|
@@ -90,11 +96,20 @@ class Session {
|
|
|
90
96
|
this.allowedTools = opts.allowedTools;
|
|
91
97
|
this._onModeChange = opts.onModeChange;
|
|
92
98
|
this.initialPermissionMode = opts.initialPermissionMode ?? "default";
|
|
99
|
+
this.hookSettingsPath = opts.hookSettingsPath;
|
|
93
100
|
this.client.keepAlive(this.thinking, this.mode);
|
|
94
|
-
setInterval(() => {
|
|
101
|
+
this.keepAliveInterval = setInterval(() => {
|
|
95
102
|
this.client.keepAlive(this.thinking, this.mode);
|
|
96
103
|
}, 2e3);
|
|
97
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* Cleanup resources (call when session is no longer needed)
|
|
107
|
+
*/
|
|
108
|
+
cleanup = () => {
|
|
109
|
+
clearInterval(this.keepAliveInterval);
|
|
110
|
+
this.sessionFoundCallbacks = [];
|
|
111
|
+
types.logger.debug("[Session] Cleaned up resources");
|
|
112
|
+
};
|
|
98
113
|
onThinkingChange = (thinking) => {
|
|
99
114
|
this.thinking = thinking;
|
|
100
115
|
this.client.keepAlive(thinking, this.mode);
|
|
@@ -104,6 +119,17 @@ class Session {
|
|
|
104
119
|
this.client.keepAlive(this.thinking, mode);
|
|
105
120
|
this._onModeChange(mode);
|
|
106
121
|
};
|
|
122
|
+
/**
|
|
123
|
+
* Called when Claude session ID is discovered or changed.
|
|
124
|
+
*
|
|
125
|
+
* This is triggered by the SessionStart hook when:
|
|
126
|
+
* - Claude starts a new session (fresh start)
|
|
127
|
+
* - Claude resumes a session (--continue, --resume flags)
|
|
128
|
+
* - Claude forks a session (/compact, double-escape fork)
|
|
129
|
+
*
|
|
130
|
+
* Updates internal state, syncs to API metadata, and notifies
|
|
131
|
+
* all registered callbacks (e.g., SessionScanner) about the change.
|
|
132
|
+
*/
|
|
107
133
|
onSessionFound = (sessionId) => {
|
|
108
134
|
this.sessionId = sessionId;
|
|
109
135
|
this.client.updateMetadata((metadata) => ({
|
|
@@ -111,6 +137,24 @@ class Session {
|
|
|
111
137
|
claudeSessionId: sessionId
|
|
112
138
|
}));
|
|
113
139
|
types.logger.debug(`[Session] Claude Code session ID ${sessionId} added to metadata`);
|
|
140
|
+
for (const callback of this.sessionFoundCallbacks) {
|
|
141
|
+
callback(sessionId);
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
/**
|
|
145
|
+
* Register a callback to be notified when session ID is found/changed
|
|
146
|
+
*/
|
|
147
|
+
addSessionFoundCallback = (callback) => {
|
|
148
|
+
this.sessionFoundCallbacks.push(callback);
|
|
149
|
+
};
|
|
150
|
+
/**
|
|
151
|
+
* Remove a session found callback
|
|
152
|
+
*/
|
|
153
|
+
removeSessionFoundCallback = (callback) => {
|
|
154
|
+
const index = this.sessionFoundCallbacks.indexOf(callback);
|
|
155
|
+
if (index !== -1) {
|
|
156
|
+
this.sessionFoundCallbacks.splice(index, 1);
|
|
157
|
+
}
|
|
114
158
|
};
|
|
115
159
|
/**
|
|
116
160
|
* Clear the current session ID (used by /clear command)
|
|
@@ -121,13 +165,18 @@ class Session {
|
|
|
121
165
|
};
|
|
122
166
|
/**
|
|
123
167
|
* Consume one-time Claude flags from claudeArgs after Claude spawn
|
|
124
|
-
*
|
|
168
|
+
* Handles: --resume (with or without session ID), --continue
|
|
125
169
|
*/
|
|
126
170
|
consumeOneTimeFlags = () => {
|
|
127
171
|
if (!this.claudeArgs) return;
|
|
128
172
|
const filteredArgs = [];
|
|
129
173
|
for (let i = 0; i < this.claudeArgs.length; i++) {
|
|
130
|
-
|
|
174
|
+
const arg = this.claudeArgs[i];
|
|
175
|
+
if (arg === "--continue") {
|
|
176
|
+
types.logger.debug("[Session] Consumed --continue flag");
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (arg === "--resume") {
|
|
131
180
|
if (i + 1 < this.claudeArgs.length) {
|
|
132
181
|
const nextArg = this.claudeArgs[i + 1];
|
|
133
182
|
if (!nextArg.startsWith("-") && nextArg.includes("-")) {
|
|
@@ -139,9 +188,9 @@ class Session {
|
|
|
139
188
|
} else {
|
|
140
189
|
types.logger.debug("[Session] Consumed --resume flag (no session ID)");
|
|
141
190
|
}
|
|
142
|
-
|
|
143
|
-
filteredArgs.push(this.claudeArgs[i]);
|
|
191
|
+
continue;
|
|
144
192
|
}
|
|
193
|
+
filteredArgs.push(arg);
|
|
145
194
|
}
|
|
146
195
|
this.claudeArgs = filteredArgs.length > 0 ? filteredArgs : void 0;
|
|
147
196
|
types.logger.debug(`[Session] Consumed one-time flags, remaining args:`, this.claudeArgs);
|
|
@@ -340,31 +389,20 @@ const claudeCliPath = node_path.resolve(node_path.join(types.projectPath(), "scr
|
|
|
340
389
|
async function claudeLocal(opts) {
|
|
341
390
|
const projectDir = getProjectPath(opts.path);
|
|
342
391
|
fs.mkdirSync(projectDir, { recursive: true });
|
|
343
|
-
const
|
|
344
|
-
|
|
345
|
-
const
|
|
346
|
-
const detectedIdsFileSystem = /* @__PURE__ */ new Set();
|
|
347
|
-
watcher.on("change", (event, filename) => {
|
|
348
|
-
if (typeof filename === "string" && filename.toLowerCase().endsWith(".jsonl")) {
|
|
349
|
-
types.logger.debug("change", event, filename);
|
|
350
|
-
const sessionId = filename.replace(".jsonl", "");
|
|
351
|
-
if (detectedIdsFileSystem.has(sessionId)) {
|
|
352
|
-
return;
|
|
353
|
-
}
|
|
354
|
-
detectedIdsFileSystem.add(sessionId);
|
|
355
|
-
if (resolvedSessionId) {
|
|
356
|
-
return;
|
|
357
|
-
}
|
|
358
|
-
if (detectedIdsRandomUUID.has(sessionId)) {
|
|
359
|
-
resolvedSessionId = sessionId;
|
|
360
|
-
opts.onSessionFound(sessionId);
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
});
|
|
392
|
+
const hasContinueFlag = opts.claudeArgs?.includes("--continue");
|
|
393
|
+
const hasResumeFlag = opts.claudeArgs?.includes("--resume");
|
|
394
|
+
const hasUserSessionControl = hasContinueFlag || hasResumeFlag;
|
|
364
395
|
let startFrom = opts.sessionId;
|
|
365
396
|
if (opts.sessionId && !claudeCheckSession(opts.sessionId, opts.path)) {
|
|
366
397
|
startFrom = null;
|
|
367
398
|
}
|
|
399
|
+
if (startFrom) {
|
|
400
|
+
types.logger.debug(`[ClaudeLocal] Will resume existing session: ${startFrom}`);
|
|
401
|
+
} else if (hasUserSessionControl) {
|
|
402
|
+
types.logger.debug(`[ClaudeLocal] User passed ${hasContinueFlag ? "--continue" : "--resume"} flag, session ID will be determined by hook`);
|
|
403
|
+
} else {
|
|
404
|
+
types.logger.debug(`[ClaudeLocal] Fresh start, session ID will be provided by hook`);
|
|
405
|
+
}
|
|
368
406
|
let thinking = false;
|
|
369
407
|
let stopThinkingTimeout = null;
|
|
370
408
|
const updateThinking = (newThinking) => {
|
|
@@ -381,15 +419,10 @@ async function claudeLocal(opts) {
|
|
|
381
419
|
if (Object.keys(direnvVars).length > 0) {
|
|
382
420
|
types.logger.debug(`[ClaudeLocal] Loaded ${Object.keys(direnvVars).length} direnv environment variables`);
|
|
383
421
|
}
|
|
384
|
-
const env = {
|
|
385
|
-
...process.env,
|
|
386
|
-
...direnvVars,
|
|
387
|
-
...opts.claudeEnvVars
|
|
388
|
-
};
|
|
389
422
|
process.stdin.pause();
|
|
390
423
|
await new Promise((r, reject) => {
|
|
391
424
|
const args = [];
|
|
392
|
-
if (startFrom) {
|
|
425
|
+
if (!hasUserSessionControl && startFrom) {
|
|
393
426
|
args.push("--resume", startFrom);
|
|
394
427
|
}
|
|
395
428
|
args.push("--append-system-prompt", systemPrompt);
|
|
@@ -402,9 +435,18 @@ async function claudeLocal(opts) {
|
|
|
402
435
|
if (opts.claudeArgs) {
|
|
403
436
|
args.push(...opts.claudeArgs);
|
|
404
437
|
}
|
|
438
|
+
args.push("--settings", opts.hookSettingsPath);
|
|
439
|
+
types.logger.debug(`[ClaudeLocal] Using hook settings: ${opts.hookSettingsPath}`);
|
|
405
440
|
if (!claudeCliPath || !fs.existsSync(claudeCliPath)) {
|
|
406
441
|
throw new Error("Claude local launcher not found. Please ensure HAPPY_PROJECT_ROOT is set correctly for development.");
|
|
407
442
|
}
|
|
443
|
+
const env = {
|
|
444
|
+
...process.env,
|
|
445
|
+
...direnvVars,
|
|
446
|
+
...opts.claudeEnvVars
|
|
447
|
+
};
|
|
448
|
+
types.logger.debug(`[ClaudeLocal] Spawning launcher: ${claudeCliPath}`);
|
|
449
|
+
types.logger.debug(`[ClaudeLocal] Args: ${JSON.stringify(args)}`);
|
|
408
450
|
const child = node_child_process.spawn("node", [claudeCliPath, ...args], {
|
|
409
451
|
stdio: ["inherit", "inherit", "inherit", "pipe"],
|
|
410
452
|
signal: opts.abort,
|
|
@@ -421,13 +463,6 @@ async function claudeLocal(opts) {
|
|
|
421
463
|
try {
|
|
422
464
|
const message = JSON.parse(line);
|
|
423
465
|
switch (message.type) {
|
|
424
|
-
case "uuid":
|
|
425
|
-
detectedIdsRandomUUID.add(message.value);
|
|
426
|
-
if (!resolvedSessionId && detectedIdsFileSystem.has(message.value)) {
|
|
427
|
-
resolvedSessionId = message.value;
|
|
428
|
-
opts.onSessionFound(message.value);
|
|
429
|
-
}
|
|
430
|
-
break;
|
|
431
466
|
case "fetch-start":
|
|
432
467
|
activeFetches.set(message.id, {
|
|
433
468
|
hostname: message.hostname,
|
|
@@ -481,7 +516,6 @@ async function claudeLocal(opts) {
|
|
|
481
516
|
});
|
|
482
517
|
});
|
|
483
518
|
} finally {
|
|
484
|
-
watcher.close();
|
|
485
519
|
process.stdin.resume();
|
|
486
520
|
if (stopThinkingTimeout) {
|
|
487
521
|
clearTimeout(stopThinkingTimeout);
|
|
@@ -489,7 +523,7 @@ async function claudeLocal(opts) {
|
|
|
489
523
|
}
|
|
490
524
|
updateThinking(false);
|
|
491
525
|
}
|
|
492
|
-
return
|
|
526
|
+
return startFrom;
|
|
493
527
|
}
|
|
494
528
|
|
|
495
529
|
class Future {
|
|
@@ -743,21 +777,32 @@ async function claudeLocalLauncher(session) {
|
|
|
743
777
|
}
|
|
744
778
|
}
|
|
745
779
|
});
|
|
780
|
+
const scannerSessionCallback = (sessionId) => {
|
|
781
|
+
scanner.onNewSession(sessionId);
|
|
782
|
+
};
|
|
783
|
+
session.addSessionFoundCallback(scannerSessionCallback);
|
|
746
784
|
let exitReason = null;
|
|
747
|
-
|
|
785
|
+
let abortRequested = false;
|
|
786
|
+
let processAbortController = new AbortController();
|
|
748
787
|
let exutFuture = new Future();
|
|
749
788
|
try {
|
|
789
|
+
let getAbortController2 = function() {
|
|
790
|
+
return processAbortController;
|
|
791
|
+
}, getExitFuture2 = function() {
|
|
792
|
+
return exutFuture;
|
|
793
|
+
};
|
|
794
|
+
var getAbortController = getAbortController2, getExitFuture = getExitFuture2;
|
|
750
795
|
async function abort() {
|
|
751
|
-
|
|
752
|
-
|
|
796
|
+
const controller = getAbortController2();
|
|
797
|
+
const exitFuture = getExitFuture2();
|
|
798
|
+
if (!controller.signal.aborted) {
|
|
799
|
+
controller.abort();
|
|
753
800
|
}
|
|
754
|
-
await
|
|
801
|
+
await exitFuture.promise;
|
|
755
802
|
}
|
|
756
803
|
async function doAbort() {
|
|
757
804
|
types.logger.debug("[local]: doAbort");
|
|
758
|
-
|
|
759
|
-
exitReason = "switch";
|
|
760
|
-
}
|
|
805
|
+
abortRequested = true;
|
|
761
806
|
session.queue.reset();
|
|
762
807
|
await abort();
|
|
763
808
|
}
|
|
@@ -795,9 +840,17 @@ async function claudeLocalLauncher(session) {
|
|
|
795
840
|
claudeEnvVars: session.claudeEnvVars,
|
|
796
841
|
claudeArgs: session.claudeArgs,
|
|
797
842
|
mcpServers: session.mcpServers,
|
|
798
|
-
allowedTools: session.allowedTools
|
|
843
|
+
allowedTools: session.allowedTools,
|
|
844
|
+
hookSettingsPath: session.hookSettingsPath
|
|
799
845
|
});
|
|
800
846
|
session.consumeOneTimeFlags();
|
|
847
|
+
if (abortRequested) {
|
|
848
|
+
types.logger.debug("[local]: Aborting current operation, continuing local mode");
|
|
849
|
+
abortRequested = false;
|
|
850
|
+
processAbortController = new AbortController();
|
|
851
|
+
exutFuture = new Future();
|
|
852
|
+
continue;
|
|
853
|
+
}
|
|
801
854
|
if (!exitReason) {
|
|
802
855
|
exitReason = "exit";
|
|
803
856
|
break;
|
|
@@ -805,6 +858,13 @@ async function claudeLocalLauncher(session) {
|
|
|
805
858
|
} catch (e) {
|
|
806
859
|
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
807
860
|
types.logger.debug("[local]: launch error", e);
|
|
861
|
+
if (abortRequested) {
|
|
862
|
+
types.logger.debug("[local]: Aborting after error, continuing local mode");
|
|
863
|
+
abortRequested = false;
|
|
864
|
+
processAbortController = new AbortController();
|
|
865
|
+
exutFuture = new Future();
|
|
866
|
+
continue;
|
|
867
|
+
}
|
|
808
868
|
const reason = exitReason;
|
|
809
869
|
if (reason === "switch") {
|
|
810
870
|
session.client.sendSessionEvent({ type: "message", message: `Error during mode switch: ${errorMessage}` });
|
|
@@ -826,6 +886,7 @@ async function claudeLocalLauncher(session) {
|
|
|
826
886
|
session.client.rpcHandlerManager.registerHandler("switch", async () => {
|
|
827
887
|
});
|
|
828
888
|
session.queue.setOnMessage(null);
|
|
889
|
+
session.removeSessionFoundCallback(scannerSessionCallback);
|
|
829
890
|
await scanner.cleanup();
|
|
830
891
|
}
|
|
831
892
|
return exitReason || "exit";
|
|
@@ -845,6 +906,39 @@ class MessageBuffer {
|
|
|
845
906
|
this.messages.push(message);
|
|
846
907
|
this.notifyListeners();
|
|
847
908
|
}
|
|
909
|
+
/**
|
|
910
|
+
* Update the last message of a specific type by appending content to it
|
|
911
|
+
* Useful for streaming responses where deltas should accumulate in one message
|
|
912
|
+
*/
|
|
913
|
+
updateLastMessage(contentDelta, type = "assistant") {
|
|
914
|
+
for (let i = this.messages.length - 1; i >= 0; i--) {
|
|
915
|
+
if (this.messages[i].type === type) {
|
|
916
|
+
const oldMessage = this.messages[i];
|
|
917
|
+
const updatedMessage = {
|
|
918
|
+
...oldMessage,
|
|
919
|
+
content: oldMessage.content + contentDelta
|
|
920
|
+
};
|
|
921
|
+
this.messages[i] = updatedMessage;
|
|
922
|
+
this.notifyListeners();
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
this.addMessage(contentDelta, type);
|
|
927
|
+
}
|
|
928
|
+
/**
|
|
929
|
+
* Remove the last message of a specific type
|
|
930
|
+
* Useful for removing placeholder messages like "Thinking..." when actual response starts
|
|
931
|
+
*/
|
|
932
|
+
removeLastMessage(type) {
|
|
933
|
+
for (let i = this.messages.length - 1; i >= 0; i--) {
|
|
934
|
+
if (this.messages[i].type === type) {
|
|
935
|
+
this.messages.splice(i, 1);
|
|
936
|
+
this.notifyListeners();
|
|
937
|
+
return true;
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
return false;
|
|
941
|
+
}
|
|
848
942
|
getMessages() {
|
|
849
943
|
return [...this.messages];
|
|
850
944
|
}
|
|
@@ -1090,10 +1184,94 @@ class AbortError extends Error {
|
|
|
1090
1184
|
}
|
|
1091
1185
|
}
|
|
1092
1186
|
|
|
1093
|
-
const __filename$1 = node_url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index-
|
|
1187
|
+
const __filename$1 = node_url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index-DGfkEaE6.cjs', document.baseURI).href)));
|
|
1094
1188
|
const __dirname$1 = node_path.join(__filename$1, "..");
|
|
1189
|
+
function getGlobalClaudeVersion() {
|
|
1190
|
+
try {
|
|
1191
|
+
const cleanEnv = getCleanEnv();
|
|
1192
|
+
const output = node_child_process.execSync("claude --version", {
|
|
1193
|
+
encoding: "utf8",
|
|
1194
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1195
|
+
cwd: os.homedir(),
|
|
1196
|
+
env: cleanEnv
|
|
1197
|
+
}).trim();
|
|
1198
|
+
const match = output.match(/(\d+\.\d+\.\d+)/);
|
|
1199
|
+
types.logger.debug(`[Claude SDK] Global claude --version output: ${output}`);
|
|
1200
|
+
return match ? match[1] : null;
|
|
1201
|
+
} catch {
|
|
1202
|
+
return null;
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
function getCleanEnv() {
|
|
1206
|
+
const env = { ...process.env };
|
|
1207
|
+
const cwd = process.cwd();
|
|
1208
|
+
const pathSep = process.platform === "win32" ? ";" : ":";
|
|
1209
|
+
const pathKey = process.platform === "win32" ? "Path" : "PATH";
|
|
1210
|
+
const actualPathKey = Object.keys(env).find((k) => k.toLowerCase() === "path") || pathKey;
|
|
1211
|
+
if (env[actualPathKey]) {
|
|
1212
|
+
const cleanPath = env[actualPathKey].split(pathSep).filter((p) => {
|
|
1213
|
+
const normalizedP = p.replace(/\\/g, "/").toLowerCase();
|
|
1214
|
+
const normalizedCwd = cwd.replace(/\\/g, "/").toLowerCase();
|
|
1215
|
+
return !normalizedP.startsWith(normalizedCwd);
|
|
1216
|
+
}).join(pathSep);
|
|
1217
|
+
env[actualPathKey] = cleanPath;
|
|
1218
|
+
types.logger.debug(`[Claude SDK] Cleaned PATH, removed local paths from: ${cwd}`);
|
|
1219
|
+
}
|
|
1220
|
+
return env;
|
|
1221
|
+
}
|
|
1222
|
+
function findGlobalClaudePath() {
|
|
1223
|
+
const homeDir = os.homedir();
|
|
1224
|
+
const cleanEnv = getCleanEnv();
|
|
1225
|
+
try {
|
|
1226
|
+
node_child_process.execSync("claude --version", {
|
|
1227
|
+
encoding: "utf8",
|
|
1228
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1229
|
+
cwd: homeDir,
|
|
1230
|
+
env: cleanEnv
|
|
1231
|
+
});
|
|
1232
|
+
types.logger.debug("[Claude SDK] Global claude command available (checked with clean PATH)");
|
|
1233
|
+
return "claude";
|
|
1234
|
+
} catch {
|
|
1235
|
+
}
|
|
1236
|
+
if (process.platform !== "win32") {
|
|
1237
|
+
try {
|
|
1238
|
+
const result = node_child_process.execSync("which claude", {
|
|
1239
|
+
encoding: "utf8",
|
|
1240
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1241
|
+
cwd: homeDir,
|
|
1242
|
+
env: cleanEnv
|
|
1243
|
+
}).trim();
|
|
1244
|
+
if (result && fs.existsSync(result)) {
|
|
1245
|
+
types.logger.debug(`[Claude SDK] Found global claude path via which: ${result}`);
|
|
1246
|
+
return result;
|
|
1247
|
+
}
|
|
1248
|
+
} catch {
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
return null;
|
|
1252
|
+
}
|
|
1095
1253
|
function getDefaultClaudeCodePath() {
|
|
1096
|
-
|
|
1254
|
+
const nodeModulesPath = node_path.join(__dirname$1, "..", "..", "..", "node_modules", "@anthropic-ai", "claude-code", "cli.js");
|
|
1255
|
+
if (process.env.HAPPY_CLAUDE_PATH) {
|
|
1256
|
+
types.logger.debug(`[Claude SDK] Using HAPPY_CLAUDE_PATH: ${process.env.HAPPY_CLAUDE_PATH}`);
|
|
1257
|
+
return process.env.HAPPY_CLAUDE_PATH;
|
|
1258
|
+
}
|
|
1259
|
+
if (process.env.HAPPY_USE_BUNDLED_CLAUDE === "1") {
|
|
1260
|
+
types.logger.debug(`[Claude SDK] Forced bundled version: ${nodeModulesPath}`);
|
|
1261
|
+
return nodeModulesPath;
|
|
1262
|
+
}
|
|
1263
|
+
const globalPath = findGlobalClaudePath();
|
|
1264
|
+
if (!globalPath) {
|
|
1265
|
+
types.logger.debug(`[Claude SDK] No global claude found, using bundled: ${nodeModulesPath}`);
|
|
1266
|
+
return nodeModulesPath;
|
|
1267
|
+
}
|
|
1268
|
+
const globalVersion = getGlobalClaudeVersion();
|
|
1269
|
+
types.logger.debug(`[Claude SDK] Global version: ${globalVersion || "unknown"}`);
|
|
1270
|
+
if (!globalVersion) {
|
|
1271
|
+
types.logger.debug(`[Claude SDK] Cannot compare versions, using global: ${globalPath}`);
|
|
1272
|
+
return globalPath;
|
|
1273
|
+
}
|
|
1274
|
+
return globalPath;
|
|
1097
1275
|
}
|
|
1098
1276
|
function logDebug(message) {
|
|
1099
1277
|
if (process.env.DEBUG) {
|
|
@@ -1322,6 +1500,7 @@ function query(config) {
|
|
|
1322
1500
|
fallbackModel,
|
|
1323
1501
|
strictMcpConfig,
|
|
1324
1502
|
canCallTool,
|
|
1503
|
+
settingsPath,
|
|
1325
1504
|
onStderr
|
|
1326
1505
|
} = {}
|
|
1327
1506
|
} = config;
|
|
@@ -1348,6 +1527,7 @@ function query(config) {
|
|
|
1348
1527
|
}
|
|
1349
1528
|
if (strictMcpConfig) args.push("--strict-mcp-config");
|
|
1350
1529
|
if (permissionMode) args.push("--permission-mode", permissionMode);
|
|
1530
|
+
if (settingsPath) args.push("--settings", settingsPath);
|
|
1351
1531
|
if (fallbackModel) {
|
|
1352
1532
|
if (model && fallbackModel === model) {
|
|
1353
1533
|
throw new Error("Fallback model cannot be the same as the main model. Please specify a different model for fallbackModel option.");
|
|
@@ -1870,6 +2050,7 @@ Echo message: ${echoMessage}` : "");
|
|
|
1870
2050
|
pathToClaudeCodeExecutable: (() => {
|
|
1871
2051
|
return node_path.resolve(node_path.join(types.projectPath(), "scripts", "claude_remote_launcher.cjs"));
|
|
1872
2052
|
})(),
|
|
2053
|
+
settingsPath: opts.hookSettingsPath,
|
|
1873
2054
|
onStderr: opts.onStderr
|
|
1874
2055
|
};
|
|
1875
2056
|
let thinking = false;
|
|
@@ -1905,6 +2086,8 @@ Echo message: ${echoMessage}` : "");
|
|
|
1905
2086
|
const initialContent = await buildMessageContent(initial.message, initial.mode.imageRefs);
|
|
1906
2087
|
messages.push({
|
|
1907
2088
|
type: "user",
|
|
2089
|
+
uuid: node_crypto.randomUUID(),
|
|
2090
|
+
// UUID is required for Claude CLI streaming mode
|
|
1908
2091
|
message: {
|
|
1909
2092
|
role: "user",
|
|
1910
2093
|
content: initialContent
|
|
@@ -1949,7 +2132,12 @@ Echo message: ${echoMessage}` : "");
|
|
|
1949
2132
|
}
|
|
1950
2133
|
mode = next.mode;
|
|
1951
2134
|
const nextContent = await buildMessageContent(next.message, next.mode.imageRefs);
|
|
1952
|
-
messages.push({
|
|
2135
|
+
messages.push({
|
|
2136
|
+
type: "user",
|
|
2137
|
+
uuid: node_crypto.randomUUID(),
|
|
2138
|
+
// UUID is required for Claude CLI streaming mode
|
|
2139
|
+
message: { role: "user", content: nextContent }
|
|
2140
|
+
});
|
|
1953
2141
|
}
|
|
1954
2142
|
if (message.type === "user") {
|
|
1955
2143
|
const msg = message;
|
|
@@ -2097,6 +2285,16 @@ class PermissionHandler {
|
|
|
2097
2285
|
} else {
|
|
2098
2286
|
pending.resolve({ behavior: "deny", message: response.reason || "Plan rejected" });
|
|
2099
2287
|
}
|
|
2288
|
+
} else if (pending.toolName === "AskUserQuestion") {
|
|
2289
|
+
if (response.approved) {
|
|
2290
|
+
const inputWithAnswers = {
|
|
2291
|
+
...pending.input,
|
|
2292
|
+
answers: response.answers || {}
|
|
2293
|
+
};
|
|
2294
|
+
pending.resolve({ behavior: "allow", updatedInput: inputWithAnswers });
|
|
2295
|
+
} else {
|
|
2296
|
+
pending.resolve({ behavior: "deny", message: response.reason || "User declined to answer the questions." });
|
|
2297
|
+
}
|
|
2100
2298
|
} else {
|
|
2101
2299
|
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.` };
|
|
2102
2300
|
pending.resolve(result);
|
|
@@ -2122,11 +2320,13 @@ class PermissionHandler {
|
|
|
2122
2320
|
return { behavior: "allow", updatedInput: input };
|
|
2123
2321
|
}
|
|
2124
2322
|
const descriptor = getToolDescriptor(toolName);
|
|
2125
|
-
if (
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2323
|
+
if (toolName !== "AskUserQuestion") {
|
|
2324
|
+
if (this.permissionMode === "bypassPermissions") {
|
|
2325
|
+
return { behavior: "allow", updatedInput: input };
|
|
2326
|
+
}
|
|
2327
|
+
if (this.permissionMode === "acceptEdits" && descriptor.edit) {
|
|
2328
|
+
return { behavior: "allow", updatedInput: input };
|
|
2329
|
+
}
|
|
2130
2330
|
}
|
|
2131
2331
|
let toolCallId = this.resolveToolCallId(toolName, input);
|
|
2132
2332
|
if (!toolCallId) {
|
|
@@ -2858,6 +3058,7 @@ async function claudeRemoteLauncher(session) {
|
|
|
2858
3058
|
let exitReason = null;
|
|
2859
3059
|
let abortController = null;
|
|
2860
3060
|
let abortFuture = null;
|
|
3061
|
+
let abortRequested = false;
|
|
2861
3062
|
async function abort() {
|
|
2862
3063
|
if (abortController && !abortController.signal.aborted) {
|
|
2863
3064
|
abortController.abort();
|
|
@@ -2866,6 +3067,7 @@ async function claudeRemoteLauncher(session) {
|
|
|
2866
3067
|
}
|
|
2867
3068
|
async function doAbort() {
|
|
2868
3069
|
types.logger.debug("[remote]: doAbort");
|
|
3070
|
+
abortRequested = true;
|
|
2869
3071
|
await abort();
|
|
2870
3072
|
}
|
|
2871
3073
|
async function doSwitch() {
|
|
@@ -3051,6 +3253,7 @@ async function claudeRemoteLauncher(session) {
|
|
|
3051
3253
|
path: session.path,
|
|
3052
3254
|
allowedTools: session.allowedTools ?? [],
|
|
3053
3255
|
mcpServers: session.mcpServers,
|
|
3256
|
+
hookSettingsPath: session.hookSettingsPath,
|
|
3054
3257
|
canCallTool: permissionHandler.handleToolCall,
|
|
3055
3258
|
isAborted: (toolCallId) => {
|
|
3056
3259
|
return permissionHandler.isAborted(toolCallId);
|
|
@@ -3130,16 +3333,30 @@ async function claudeRemoteLauncher(session) {
|
|
|
3130
3333
|
signal: abortController.signal
|
|
3131
3334
|
});
|
|
3132
3335
|
session.consumeOneTimeFlags();
|
|
3336
|
+
if (abortRequested && abortController.signal.aborted) {
|
|
3337
|
+
types.logger.debug("[remote]: Operation aborted by user, continuing remote mode");
|
|
3338
|
+
session.client.sendSessionEvent({ type: "message", message: "Aborted" });
|
|
3339
|
+
abortRequested = false;
|
|
3340
|
+
continue;
|
|
3341
|
+
}
|
|
3133
3342
|
if (!exitReason && abortController.signal.aborted) {
|
|
3134
3343
|
session.client.sendSessionEvent({ type: "message", message: "Aborted by user" });
|
|
3135
3344
|
}
|
|
3136
3345
|
} catch (e) {
|
|
3137
3346
|
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
3138
3347
|
types.logger.debug("[remote]: launch error", e);
|
|
3348
|
+
if (abortRequested) {
|
|
3349
|
+
types.logger.debug("[remote]: Aborting after error, continuing remote mode");
|
|
3350
|
+
session.client.sendSessionEvent({ type: "message", message: "Aborted" });
|
|
3351
|
+
abortRequested = false;
|
|
3352
|
+
continue;
|
|
3353
|
+
}
|
|
3139
3354
|
if (exitReason === "switch") {
|
|
3140
3355
|
session.client.sendSessionEvent({ type: "message", message: `Error during mode switch: ${errorMessage}` });
|
|
3356
|
+
break;
|
|
3141
3357
|
} else if (exitReason === "exit") {
|
|
3142
3358
|
session.client.sendSessionEvent({ type: "message", message: `Error during exit: ${errorMessage}` });
|
|
3359
|
+
break;
|
|
3143
3360
|
} else {
|
|
3144
3361
|
session.client.sendSessionEvent({ type: "message", message: `Process error: ${errorMessage}` });
|
|
3145
3362
|
continue;
|
|
@@ -3208,7 +3425,8 @@ async function loop(opts) {
|
|
|
3208
3425
|
messageQueue: opts.messageQueue,
|
|
3209
3426
|
allowedTools: opts.allowedTools,
|
|
3210
3427
|
onModeChange: opts.onModeChange,
|
|
3211
|
-
initialPermissionMode: opts.permissionMode
|
|
3428
|
+
initialPermissionMode: opts.permissionMode,
|
|
3429
|
+
hookSettingsPath: opts.hookSettingsPath
|
|
3212
3430
|
});
|
|
3213
3431
|
if (opts.onSessionReady) {
|
|
3214
3432
|
opts.onSessionReady(session);
|
|
@@ -3704,6 +3922,139 @@ function extractSDKMetadataAsync(onComplete) {
|
|
|
3704
3922
|
});
|
|
3705
3923
|
}
|
|
3706
3924
|
|
|
3925
|
+
function parseFrontmatter(content) {
|
|
3926
|
+
const trimmed = content.trim();
|
|
3927
|
+
if (!trimmed.startsWith("---")) {
|
|
3928
|
+
return { frontmatter: null, body: content };
|
|
3929
|
+
}
|
|
3930
|
+
const endIndex = trimmed.indexOf("---", 3);
|
|
3931
|
+
if (endIndex === -1) {
|
|
3932
|
+
return { frontmatter: null, body: content };
|
|
3933
|
+
}
|
|
3934
|
+
const frontmatterStr = trimmed.substring(3, endIndex).trim();
|
|
3935
|
+
const body = trimmed.substring(endIndex + 3).trim();
|
|
3936
|
+
const frontmatter = {};
|
|
3937
|
+
const lines = frontmatterStr.split("\n");
|
|
3938
|
+
for (const line of lines) {
|
|
3939
|
+
const colonIndex = line.indexOf(":");
|
|
3940
|
+
if (colonIndex === -1) continue;
|
|
3941
|
+
const key = line.substring(0, colonIndex).trim();
|
|
3942
|
+
let value = line.substring(colonIndex + 1).trim();
|
|
3943
|
+
if (key === "allowed-tools" && value) {
|
|
3944
|
+
frontmatter["allowed-tools"] = value.split(",").map((s) => s.trim());
|
|
3945
|
+
} else if (key === "description") {
|
|
3946
|
+
frontmatter.description = value;
|
|
3947
|
+
} else if (key === "argument-hint") {
|
|
3948
|
+
frontmatter["argument-hint"] = value;
|
|
3949
|
+
} else if (key === "model") {
|
|
3950
|
+
frontmatter.model = value;
|
|
3951
|
+
}
|
|
3952
|
+
}
|
|
3953
|
+
return { frontmatter, body };
|
|
3954
|
+
}
|
|
3955
|
+
function extractDescriptionFromContent(content) {
|
|
3956
|
+
const lines = content.split("\n");
|
|
3957
|
+
for (const line of lines) {
|
|
3958
|
+
const trimmed = line.trim();
|
|
3959
|
+
if (trimmed && !trimmed.startsWith("#") && !trimmed.startsWith("!")) {
|
|
3960
|
+
return trimmed.length > 100 ? trimmed.substring(0, 100) + "..." : trimmed;
|
|
3961
|
+
}
|
|
3962
|
+
}
|
|
3963
|
+
return void 0;
|
|
3964
|
+
}
|
|
3965
|
+
function findMarkdownFiles(dir, baseDir = dir) {
|
|
3966
|
+
const results = [];
|
|
3967
|
+
if (!fs.existsSync(dir)) {
|
|
3968
|
+
return results;
|
|
3969
|
+
}
|
|
3970
|
+
try {
|
|
3971
|
+
const entries = fs.readdirSync(dir);
|
|
3972
|
+
for (const entry of entries) {
|
|
3973
|
+
const fullPath = node_path.join(dir, entry);
|
|
3974
|
+
const stat = fs.statSync(fullPath);
|
|
3975
|
+
if (stat.isDirectory()) {
|
|
3976
|
+
const subResults = findMarkdownFiles(fullPath, baseDir);
|
|
3977
|
+
results.push(...subResults);
|
|
3978
|
+
} else if (stat.isFile() && node_path.extname(entry).toLowerCase() === ".md") {
|
|
3979
|
+
const relativePath = fullPath.substring(baseDir.length + 1);
|
|
3980
|
+
const namespace = relativePath.includes("/") ? relativePath.substring(0, relativePath.lastIndexOf("/")) : void 0;
|
|
3981
|
+
results.push({ filePath: fullPath, namespace });
|
|
3982
|
+
}
|
|
3983
|
+
}
|
|
3984
|
+
} catch (error) {
|
|
3985
|
+
types.logger.debug("[customCommands] Error reading directory:", dir, error);
|
|
3986
|
+
}
|
|
3987
|
+
return results;
|
|
3988
|
+
}
|
|
3989
|
+
function parseCommandFile(filePath, namespace, scope) {
|
|
3990
|
+
try {
|
|
3991
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
3992
|
+
const { frontmatter, body } = parseFrontmatter(content);
|
|
3993
|
+
const name = node_path.basename(filePath, ".md");
|
|
3994
|
+
const description = frontmatter?.description || extractDescriptionFromContent(body);
|
|
3995
|
+
let allowedTools;
|
|
3996
|
+
if (frontmatter?.["allowed-tools"]) {
|
|
3997
|
+
const at = frontmatter["allowed-tools"];
|
|
3998
|
+
allowedTools = Array.isArray(at) ? at : [at];
|
|
3999
|
+
}
|
|
4000
|
+
return {
|
|
4001
|
+
name,
|
|
4002
|
+
description,
|
|
4003
|
+
argumentHint: frontmatter?.["argument-hint"],
|
|
4004
|
+
allowedTools,
|
|
4005
|
+
model: frontmatter?.model,
|
|
4006
|
+
scope,
|
|
4007
|
+
namespace,
|
|
4008
|
+
filePath,
|
|
4009
|
+
content: body
|
|
4010
|
+
};
|
|
4011
|
+
} catch (error) {
|
|
4012
|
+
types.logger.debug("[customCommands] Error parsing command file:", filePath, error);
|
|
4013
|
+
return null;
|
|
4014
|
+
}
|
|
4015
|
+
}
|
|
4016
|
+
function discoverCustomCommands(projectDir) {
|
|
4017
|
+
const commands = [];
|
|
4018
|
+
const projectCommandsDir = node_path.join(projectDir, ".claude", "commands");
|
|
4019
|
+
if (fs.existsSync(projectCommandsDir)) {
|
|
4020
|
+
types.logger.debug("[customCommands] Scanning project commands:", projectCommandsDir);
|
|
4021
|
+
const files = findMarkdownFiles(projectCommandsDir);
|
|
4022
|
+
for (const { filePath, namespace } of files) {
|
|
4023
|
+
const command = parseCommandFile(filePath, namespace, "project");
|
|
4024
|
+
if (command) {
|
|
4025
|
+
commands.push(command);
|
|
4026
|
+
}
|
|
4027
|
+
}
|
|
4028
|
+
}
|
|
4029
|
+
const personalCommandsDir = node_path.join(os.homedir(), ".claude", "commands");
|
|
4030
|
+
if (fs.existsSync(personalCommandsDir)) {
|
|
4031
|
+
types.logger.debug("[customCommands] Scanning personal commands:", personalCommandsDir);
|
|
4032
|
+
const files = findMarkdownFiles(personalCommandsDir);
|
|
4033
|
+
for (const { filePath, namespace } of files) {
|
|
4034
|
+
const command = parseCommandFile(filePath, namespace, "personal");
|
|
4035
|
+
if (command) {
|
|
4036
|
+
const existingIndex = commands.findIndex((c) => c.name === command.name);
|
|
4037
|
+
if (existingIndex === -1) {
|
|
4038
|
+
commands.push(command);
|
|
4039
|
+
} else {
|
|
4040
|
+
types.logger.debug(`[customCommands] Skipping personal command "${command.name}" - project command takes precedence`);
|
|
4041
|
+
}
|
|
4042
|
+
}
|
|
4043
|
+
}
|
|
4044
|
+
}
|
|
4045
|
+
types.logger.debug(`[customCommands] Discovered ${commands.length} custom commands`);
|
|
4046
|
+
return commands;
|
|
4047
|
+
}
|
|
4048
|
+
function commandsToMetadata(commands) {
|
|
4049
|
+
return commands.map((cmd) => ({
|
|
4050
|
+
name: cmd.name,
|
|
4051
|
+
description: cmd.description,
|
|
4052
|
+
argumentHint: cmd.argumentHint,
|
|
4053
|
+
scope: cmd.scope,
|
|
4054
|
+
namespace: cmd.namespace
|
|
4055
|
+
}));
|
|
4056
|
+
}
|
|
4057
|
+
|
|
3707
4058
|
async function daemonPost(path, body) {
|
|
3708
4059
|
const state = await types.readDaemonState();
|
|
3709
4060
|
if (!state?.httpPort) {
|
|
@@ -5108,6 +5459,110 @@ async function startHappyServer(client) {
|
|
|
5108
5459
|
};
|
|
5109
5460
|
}
|
|
5110
5461
|
|
|
5462
|
+
async function startHookServer(options) {
|
|
5463
|
+
const { onSessionHook } = options;
|
|
5464
|
+
return new Promise((resolve, reject) => {
|
|
5465
|
+
const server = node_http.createServer(async (req, res) => {
|
|
5466
|
+
if (req.method === "POST" && req.url === "/hook/session-start") {
|
|
5467
|
+
const timeout = setTimeout(() => {
|
|
5468
|
+
if (!res.headersSent) {
|
|
5469
|
+
types.logger.debug("[hookServer] Request timeout");
|
|
5470
|
+
res.writeHead(408).end("timeout");
|
|
5471
|
+
}
|
|
5472
|
+
}, 5e3);
|
|
5473
|
+
try {
|
|
5474
|
+
const chunks = [];
|
|
5475
|
+
for await (const chunk of req) {
|
|
5476
|
+
chunks.push(chunk);
|
|
5477
|
+
}
|
|
5478
|
+
clearTimeout(timeout);
|
|
5479
|
+
const body = Buffer.concat(chunks).toString("utf-8");
|
|
5480
|
+
types.logger.debug("[hookServer] Received session hook:", body);
|
|
5481
|
+
let data = {};
|
|
5482
|
+
try {
|
|
5483
|
+
data = JSON.parse(body);
|
|
5484
|
+
} catch (parseError) {
|
|
5485
|
+
types.logger.debug("[hookServer] Failed to parse hook data as JSON:", parseError);
|
|
5486
|
+
}
|
|
5487
|
+
const sessionId = data.session_id || data.sessionId;
|
|
5488
|
+
if (sessionId) {
|
|
5489
|
+
types.logger.debug(`[hookServer] Session hook received session ID: ${sessionId}`);
|
|
5490
|
+
onSessionHook(sessionId, data);
|
|
5491
|
+
} else {
|
|
5492
|
+
types.logger.debug("[hookServer] Session hook received but no session_id found in data");
|
|
5493
|
+
}
|
|
5494
|
+
res.writeHead(200, { "Content-Type": "text/plain" }).end("ok");
|
|
5495
|
+
} catch (error) {
|
|
5496
|
+
clearTimeout(timeout);
|
|
5497
|
+
types.logger.debug("[hookServer] Error handling session hook:", error);
|
|
5498
|
+
if (!res.headersSent) {
|
|
5499
|
+
res.writeHead(500).end("error");
|
|
5500
|
+
}
|
|
5501
|
+
}
|
|
5502
|
+
return;
|
|
5503
|
+
}
|
|
5504
|
+
res.writeHead(404).end("not found");
|
|
5505
|
+
});
|
|
5506
|
+
server.listen(0, "127.0.0.1", () => {
|
|
5507
|
+
const address = server.address();
|
|
5508
|
+
if (!address || typeof address === "string") {
|
|
5509
|
+
reject(new Error("Failed to get server address"));
|
|
5510
|
+
return;
|
|
5511
|
+
}
|
|
5512
|
+
const port = address.port;
|
|
5513
|
+
types.logger.debug(`[hookServer] Started on port ${port}`);
|
|
5514
|
+
resolve({
|
|
5515
|
+
port,
|
|
5516
|
+
stop: () => {
|
|
5517
|
+
server.close();
|
|
5518
|
+
types.logger.debug("[hookServer] Stopped");
|
|
5519
|
+
}
|
|
5520
|
+
});
|
|
5521
|
+
});
|
|
5522
|
+
server.on("error", (err) => {
|
|
5523
|
+
types.logger.debug("[hookServer] Server error:", err);
|
|
5524
|
+
reject(err);
|
|
5525
|
+
});
|
|
5526
|
+
});
|
|
5527
|
+
}
|
|
5528
|
+
|
|
5529
|
+
function generateHookSettingsFile(port) {
|
|
5530
|
+
const hooksDir = node_path.join(types.configuration.happyHomeDir, "tmp", "hooks");
|
|
5531
|
+
fs.mkdirSync(hooksDir, { recursive: true });
|
|
5532
|
+
const filename = `session-hook-${process.pid}.json`;
|
|
5533
|
+
const filepath = node_path.join(hooksDir, filename);
|
|
5534
|
+
const forwarderScript = node_path.resolve(types.projectPath(), "scripts", "session_hook_forwarder.cjs");
|
|
5535
|
+
const hookCommand = `node "${forwarderScript}" ${port}`;
|
|
5536
|
+
const settings = {
|
|
5537
|
+
hooks: {
|
|
5538
|
+
SessionStart: [
|
|
5539
|
+
{
|
|
5540
|
+
matcher: "*",
|
|
5541
|
+
hooks: [
|
|
5542
|
+
{
|
|
5543
|
+
type: "command",
|
|
5544
|
+
command: hookCommand
|
|
5545
|
+
}
|
|
5546
|
+
]
|
|
5547
|
+
}
|
|
5548
|
+
]
|
|
5549
|
+
}
|
|
5550
|
+
};
|
|
5551
|
+
fs.writeFileSync(filepath, JSON.stringify(settings, null, 2));
|
|
5552
|
+
types.logger.debug(`[generateHookSettings] Created hook settings file: ${filepath}`);
|
|
5553
|
+
return filepath;
|
|
5554
|
+
}
|
|
5555
|
+
function cleanupHookSettingsFile(filepath) {
|
|
5556
|
+
try {
|
|
5557
|
+
if (fs.existsSync(filepath)) {
|
|
5558
|
+
fs.unlinkSync(filepath);
|
|
5559
|
+
types.logger.debug(`[generateHookSettings] Cleaned up hook settings file: ${filepath}`);
|
|
5560
|
+
}
|
|
5561
|
+
} catch (error) {
|
|
5562
|
+
types.logger.debug(`[generateHookSettings] Failed to cleanup hook settings file: ${error}`);
|
|
5563
|
+
}
|
|
5564
|
+
}
|
|
5565
|
+
|
|
5111
5566
|
function registerKillSessionHandler(rpcHandlerManager, killThisHappy) {
|
|
5112
5567
|
rpcHandlerManager.registerHandler("killSession", async () => {
|
|
5113
5568
|
types.logger.debug("Kill session request received");
|
|
@@ -5195,6 +5650,20 @@ async function runClaude(credentials, options = {}) {
|
|
|
5195
5650
|
} catch (error) {
|
|
5196
5651
|
types.logger.debug("[START] Failed to report to daemon (may not be running):", error);
|
|
5197
5652
|
}
|
|
5653
|
+
const customCommands = discoverCustomCommands(workingDirectory);
|
|
5654
|
+
const customCommandsMetadata = commandsToMetadata(customCommands);
|
|
5655
|
+
types.logger.debug(`[start] Discovered ${customCommands.length} custom commands`);
|
|
5656
|
+
if (customCommandsMetadata.length > 0) {
|
|
5657
|
+
try {
|
|
5658
|
+
api.sessionSyncClient(response).updateMetadata((currentMetadata) => ({
|
|
5659
|
+
...currentMetadata,
|
|
5660
|
+
customCommands: customCommandsMetadata
|
|
5661
|
+
}));
|
|
5662
|
+
types.logger.debug("[start] Session metadata updated with custom commands");
|
|
5663
|
+
} catch (error) {
|
|
5664
|
+
types.logger.debug("[start] Failed to update session metadata with custom commands:", error);
|
|
5665
|
+
}
|
|
5666
|
+
}
|
|
5198
5667
|
extractSDKMetadataAsync(async (sdkMetadata) => {
|
|
5199
5668
|
types.logger.debug("[start] SDK metadata extracted, updating session:", sdkMetadata);
|
|
5200
5669
|
try {
|
|
@@ -5211,6 +5680,22 @@ async function runClaude(credentials, options = {}) {
|
|
|
5211
5680
|
const session = api.sessionSyncClient(response);
|
|
5212
5681
|
const happyServer = await startHappyServer(session);
|
|
5213
5682
|
types.logger.debug(`[START] Happy MCP server started at ${happyServer.url}`);
|
|
5683
|
+
let currentSession = null;
|
|
5684
|
+
const hookServer = await startHookServer({
|
|
5685
|
+
onSessionHook: (sessionId, data) => {
|
|
5686
|
+
types.logger.debug(`[START] Session hook received: ${sessionId}`, data);
|
|
5687
|
+
if (currentSession) {
|
|
5688
|
+
const previousSessionId = currentSession.sessionId;
|
|
5689
|
+
if (previousSessionId !== sessionId) {
|
|
5690
|
+
types.logger.debug(`[START] Claude session ID changed: ${previousSessionId} -> ${sessionId}`);
|
|
5691
|
+
currentSession.onSessionFound(sessionId);
|
|
5692
|
+
}
|
|
5693
|
+
}
|
|
5694
|
+
}
|
|
5695
|
+
});
|
|
5696
|
+
types.logger.debug(`[START] Hook server started on port ${hookServer.port}`);
|
|
5697
|
+
const hookSettingsPath = generateHookSettingsFile(hookServer.port);
|
|
5698
|
+
types.logger.debug(`[START] Generated hook settings file: ${hookSettingsPath}`);
|
|
5214
5699
|
const logPath = types.logger.logFilePath;
|
|
5215
5700
|
types.logger.infoDeveloper(`Session: ${response.id}`);
|
|
5216
5701
|
types.logger.infoDeveloper(`Logs: ${logPath}`);
|
|
@@ -5374,6 +5859,11 @@ async function runClaude(credentials, options = {}) {
|
|
|
5374
5859
|
}
|
|
5375
5860
|
stopCaffeinate();
|
|
5376
5861
|
happyServer.stop();
|
|
5862
|
+
hookServer.stop();
|
|
5863
|
+
cleanupHookSettingsFile(hookSettingsPath);
|
|
5864
|
+
if (currentSession) {
|
|
5865
|
+
currentSession.cleanup();
|
|
5866
|
+
}
|
|
5377
5867
|
types.logger.debug("[START] Cleanup complete, exiting");
|
|
5378
5868
|
process.exit(0);
|
|
5379
5869
|
} catch (error) {
|
|
@@ -5407,7 +5897,8 @@ async function runClaude(credentials, options = {}) {
|
|
|
5407
5897
|
controlledByUser: newMode === "local"
|
|
5408
5898
|
}));
|
|
5409
5899
|
},
|
|
5410
|
-
onSessionReady: (
|
|
5900
|
+
onSessionReady: (sessionInstance) => {
|
|
5901
|
+
currentSession = sessionInstance;
|
|
5411
5902
|
},
|
|
5412
5903
|
mcpServers: {
|
|
5413
5904
|
"happy": {
|
|
@@ -5417,7 +5908,8 @@ async function runClaude(credentials, options = {}) {
|
|
|
5417
5908
|
},
|
|
5418
5909
|
session,
|
|
5419
5910
|
claudeEnvVars: options.claudeEnvVars,
|
|
5420
|
-
claudeArgs: options.claudeArgs
|
|
5911
|
+
claudeArgs: options.claudeArgs,
|
|
5912
|
+
hookSettingsPath
|
|
5421
5913
|
});
|
|
5422
5914
|
session.sendSessionDeath();
|
|
5423
5915
|
types.logger.debug("Waiting for socket to flush...");
|
|
@@ -5428,6 +5920,9 @@ async function runClaude(credentials, options = {}) {
|
|
|
5428
5920
|
types.logger.debug("Stopped sleep prevention");
|
|
5429
5921
|
happyServer.stop();
|
|
5430
5922
|
types.logger.debug("Stopped Happy MCP server");
|
|
5923
|
+
hookServer.stop();
|
|
5924
|
+
cleanupHookSettingsFile(hookSettingsPath);
|
|
5925
|
+
types.logger.debug("Stopped hook server");
|
|
5431
5926
|
process.exit(0);
|
|
5432
5927
|
}
|
|
5433
5928
|
|
|
@@ -6533,7 +7028,7 @@ async function handleConnectVendor(vendor, displayName) {
|
|
|
6533
7028
|
return;
|
|
6534
7029
|
} else if (subcommand === "codex") {
|
|
6535
7030
|
try {
|
|
6536
|
-
const { runCodex } = await Promise.resolve().then(function () { return require('./runCodex-
|
|
7031
|
+
const { runCodex } = await Promise.resolve().then(function () { return require('./runCodex-DJSRJ2Pg.cjs'); });
|
|
6537
7032
|
let startedBy = void 0;
|
|
6538
7033
|
for (let i = 1; i < args.length; i++) {
|
|
6539
7034
|
if (args[i] === "--started-by") {
|
|
@@ -6578,7 +7073,7 @@ async function handleConnectVendor(vendor, displayName) {
|
|
|
6578
7073
|
} else if (subcommand === "list") {
|
|
6579
7074
|
try {
|
|
6580
7075
|
const { credentials } = await authAndSetupMachineIfNeeded();
|
|
6581
|
-
const { listSessions } = await Promise.resolve().then(function () { return require('./list-
|
|
7076
|
+
const { listSessions } = await Promise.resolve().then(function () { return require('./list-S3T6MByP.cjs'); });
|
|
6582
7077
|
let sessionId;
|
|
6583
7078
|
let titleFilter;
|
|
6584
7079
|
let recentMsgs;
|
|
@@ -6680,7 +7175,7 @@ Examples:
|
|
|
6680
7175
|
process.exit(1);
|
|
6681
7176
|
}
|
|
6682
7177
|
const { credentials } = await authAndSetupMachineIfNeeded();
|
|
6683
|
-
const { promptSession } = await Promise.resolve().then(function () { return require('./prompt-
|
|
7178
|
+
const { promptSession } = await Promise.resolve().then(function () { return require('./prompt-5iJXZPz1.cjs'); });
|
|
6684
7179
|
await promptSession(credentials, sessionId, promptText, timeoutMinutes ?? void 0);
|
|
6685
7180
|
} catch (error) {
|
|
6686
7181
|
console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
|