@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.
@@ -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-kB4CXGM6.mjs';
5
- import { spawn, exec as exec$1, execSync, execFileSync } from 'node:child_process';
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, watch, readdirSync, statSync, rmSync } from 'node:fs';
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 as watch$1, access } from 'fs/promises';
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
- * Currently handles: --resume (with or without session ID)
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
- if (this.claudeArgs[i] === "--resume") {
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
- } else {
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 watcher = watch(projectDir);
321
- let resolvedSessionId = null;
322
- const detectedIdsRandomUUID = /* @__PURE__ */ new Set();
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 resolvedSessionId;
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$1(file, { persistent: true, signal: abortController.signal });
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
- return join(__dirname$1, "..", "..", "..", "node_modules", "@anthropic-ai", "claude-code", "cli.js");
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 (this.permissionMode === "bypassPermissions") {
2103
- return { behavior: "allow", updatedInput: input };
2104
- }
2105
- if (this.permissionMode === "acceptEdits" && descriptor.edit) {
2106
- return { behavior: "allow", updatedInput: input };
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: (_sessionInstance) => {
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-Cqxq74Wt.mjs');
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-CGXN1SEJ.mjs');
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-BrXxehR7.mjs');
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");