@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.
@@ -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-CGvx6DSD.cjs');
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
- * Currently handles: --resume (with or without session ID)
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
- if (this.claudeArgs[i] === "--resume") {
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
- } else {
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 watcher = fs.watch(projectDir);
344
- let resolvedSessionId = null;
345
- const detectedIdsRandomUUID = /* @__PURE__ */ new Set();
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 resolvedSessionId;
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
- const processAbortController = new AbortController();
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
- if (!processAbortController.signal.aborted) {
752
- processAbortController.abort();
796
+ const controller = getAbortController2();
797
+ const exitFuture = getExitFuture2();
798
+ if (!controller.signal.aborted) {
799
+ controller.abort();
753
800
  }
754
- await exutFuture.promise;
801
+ await exitFuture.promise;
755
802
  }
756
803
  async function doAbort() {
757
804
  types.logger.debug("[local]: doAbort");
758
- if (!exitReason) {
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-DLt6o6FN.cjs', document.baseURI).href)));
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
- return node_path.join(__dirname$1, "..", "..", "..", "node_modules", "@anthropic-ai", "claude-code", "cli.js");
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({ type: "user", message: { role: "user", content: nextContent } });
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 (this.permissionMode === "bypassPermissions") {
2126
- return { behavior: "allow", updatedInput: input };
2127
- }
2128
- if (this.permissionMode === "acceptEdits" && descriptor.edit) {
2129
- return { behavior: "allow", updatedInput: input };
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: (_sessionInstance) => {
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-CFU1flPl.cjs'); });
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-TnYgXXGw.cjs'); });
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-Cf8Tnep4.cjs'); });
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");