happy-coder 0.11.2 → 0.12.0

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.
@@ -31,7 +31,7 @@ async function main() {
31
31
  if (httpClient) return httpClient;
32
32
  const client = new index_js.Client(
33
33
  { name: "happy-stdio-bridge", version: "1.0.0" },
34
- { capabilities: { tools: {} } }
34
+ { capabilities: {} }
35
35
  );
36
36
  const transport = new streamableHttp_js.StreamableHTTPClientTransport(new URL(baseUrl));
37
37
  await client.connect(transport);
@@ -40,8 +40,7 @@ async function main() {
40
40
  }
41
41
  const server = new mcp_js.McpServer({
42
42
  name: "Happy MCP Bridge",
43
- version: "1.0.0",
44
- description: "STDIO bridge forwarding to Happy HTTP MCP"
43
+ version: "1.0.0"
45
44
  });
46
45
  server.registerTool(
47
46
  "change_title",
@@ -29,7 +29,7 @@ async function main() {
29
29
  if (httpClient) return httpClient;
30
30
  const client = new Client(
31
31
  { name: "happy-stdio-bridge", version: "1.0.0" },
32
- { capabilities: { tools: {} } }
32
+ { capabilities: {} }
33
33
  );
34
34
  const transport = new StreamableHTTPClientTransport(new URL(baseUrl));
35
35
  await client.connect(transport);
@@ -38,8 +38,7 @@ async function main() {
38
38
  }
39
39
  const server = new McpServer({
40
40
  name: "Happy MCP Bridge",
41
- version: "1.0.0",
42
- description: "STDIO bridge forwarding to Happy HTTP MCP"
41
+ version: "1.0.0"
43
42
  });
44
43
  server.registerTool(
45
44
  "change_title",
@@ -1,13 +1,13 @@
1
1
  import chalk from 'chalk';
2
2
  import os$1, { homedir } from 'node:os';
3
3
  import { randomUUID, randomBytes } from 'node:crypto';
4
- import { l as logger, p as projectPath, d as backoff, e as delay, R as RawJSONLinesSchema, f as AsyncLock, c as configuration, g as readDaemonState, h as clearDaemonState, b as packageJson, r as readSettings, i as readCredentials, j as encodeBase64, u as updateSettings, k as encodeBase64Url, m as decodeBase64, w as writeCredentialsLegacy, n as writeCredentialsDataKey, o as acquireDaemonLock, q as writeDaemonState, A as ApiClient, s as releaseDaemonLock, t as clearCredentials, v as clearMachineId, x as getLatestDaemonLog } from './types-CjceR-4_.mjs';
4
+ import { l as logger, p as projectPath, d as backoff, e as delay, R as RawJSONLinesSchema, f as AsyncLock, c as configuration, g as readDaemonState, h as clearDaemonState, b as packageJson, r as readSettings, i as readCredentials, j as encodeBase64, u as updateSettings, k as encodeBase64Url, m as decodeBase64, w as writeCredentialsLegacy, n as writeCredentialsDataKey, o as acquireDaemonLock, q as writeDaemonState, A as ApiClient, s as releaseDaemonLock, t as clearCredentials, v as clearMachineId, x as getLatestDaemonLog } from './types-7HcYY6Ao.mjs';
5
5
  import { spawn, execSync, 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, rmSync } from 'node:fs';
9
9
  import { readFile } from 'node:fs/promises';
10
- import fs, { watch as watch$1, access } from 'fs/promises';
10
+ import fs, { watch, access } from 'fs/promises';
11
11
  import { useStdout, useInput, Box, Text, render } from 'ink';
12
12
  import React, { useState, useRef, useEffect, useCallback } from 'react';
13
13
  import { fileURLToPath } from 'node:url';
@@ -220,31 +220,19 @@ const claudeCliPath = resolve(join(projectPath(), "scripts", "claude_local_launc
220
220
  async function claudeLocal(opts) {
221
221
  const projectDir = getProjectPath(opts.path);
222
222
  mkdirSync(projectDir, { recursive: true });
223
- const watcher = watch(projectDir);
224
- let resolvedSessionId = null;
225
- const detectedIdsRandomUUID = /* @__PURE__ */ new Set();
226
- const detectedIdsFileSystem = /* @__PURE__ */ new Set();
227
- watcher.on("change", (event, filename) => {
228
- if (typeof filename === "string" && filename.toLowerCase().endsWith(".jsonl")) {
229
- logger.debug("change", event, filename);
230
- const sessionId = filename.replace(".jsonl", "");
231
- if (detectedIdsFileSystem.has(sessionId)) {
232
- return;
233
- }
234
- detectedIdsFileSystem.add(sessionId);
235
- if (resolvedSessionId) {
236
- return;
237
- }
238
- if (detectedIdsRandomUUID.has(sessionId)) {
239
- resolvedSessionId = sessionId;
240
- opts.onSessionFound(sessionId);
241
- }
242
- }
243
- });
244
223
  let startFrom = opts.sessionId;
245
224
  if (opts.sessionId && !claudeCheckSession(opts.sessionId, opts.path)) {
246
225
  startFrom = null;
247
226
  }
227
+ const newSessionId = startFrom ? null : randomUUID();
228
+ const effectiveSessionId = startFrom || newSessionId;
229
+ if (newSessionId) {
230
+ logger.debug(`[ClaudeLocal] Generated new session ID: ${newSessionId}`);
231
+ opts.onSessionFound(newSessionId);
232
+ } else {
233
+ logger.debug(`[ClaudeLocal] Resuming session: ${startFrom}`);
234
+ opts.onSessionFound(startFrom);
235
+ }
248
236
  let thinking = false;
249
237
  let stopThinkingTimeout = null;
250
238
  const updateThinking = (newThinking) => {
@@ -262,6 +250,8 @@ async function claudeLocal(opts) {
262
250
  const args = [];
263
251
  if (startFrom) {
264
252
  args.push("--resume", startFrom);
253
+ } else {
254
+ args.push("--session-id", newSessionId);
265
255
  }
266
256
  args.push("--append-system-prompt", systemPrompt);
267
257
  if (opts.mcpServers && Object.keys(opts.mcpServers).length > 0) {
@@ -280,6 +270,8 @@ async function claudeLocal(opts) {
280
270
  ...process.env,
281
271
  ...opts.claudeEnvVars
282
272
  };
273
+ logger.debug(`[ClaudeLocal] Spawning launcher: ${claudeCliPath}`);
274
+ logger.debug(`[ClaudeLocal] Args: ${JSON.stringify(args)}`);
283
275
  const child = spawn("node", [claudeCliPath, ...args], {
284
276
  stdio: ["inherit", "inherit", "inherit", "pipe"],
285
277
  signal: opts.abort,
@@ -296,13 +288,6 @@ async function claudeLocal(opts) {
296
288
  try {
297
289
  const message = JSON.parse(line);
298
290
  switch (message.type) {
299
- case "uuid":
300
- detectedIdsRandomUUID.add(message.value);
301
- if (!resolvedSessionId && detectedIdsFileSystem.has(message.value)) {
302
- resolvedSessionId = message.value;
303
- opts.onSessionFound(message.value);
304
- }
305
- break;
306
291
  case "fetch-start":
307
292
  activeFetches.set(message.id, {
308
293
  hostname: message.hostname,
@@ -356,7 +341,6 @@ async function claudeLocal(opts) {
356
341
  });
357
342
  });
358
343
  } finally {
359
- watcher.close();
360
344
  process.stdin.resume();
361
345
  if (stopThinkingTimeout) {
362
346
  clearTimeout(stopThinkingTimeout);
@@ -364,7 +348,7 @@ async function claudeLocal(opts) {
364
348
  }
365
349
  updateThinking(false);
366
350
  }
367
- return resolvedSessionId;
351
+ return effectiveSessionId;
368
352
  }
369
353
 
370
354
  class Future {
@@ -460,7 +444,7 @@ function startFileWatcher(file, onFileChange) {
460
444
  while (true) {
461
445
  try {
462
446
  logger.debug(`[FILE_WATCHER] Starting watcher for ${file}`);
463
- const watcher = watch$1(file, { persistent: true, signal: abortController.signal });
447
+ const watcher = watch(file, { persistent: true, signal: abortController.signal });
464
448
  for await (const event of watcher) {
465
449
  if (abortController.signal.aborted) {
466
450
  return;
@@ -482,6 +466,11 @@ function startFileWatcher(file, onFileChange) {
482
466
  };
483
467
  }
484
468
 
469
+ const INTERNAL_CLAUDE_EVENT_TYPES = /* @__PURE__ */ new Set([
470
+ "file-history-snapshot",
471
+ "change",
472
+ "queue-operation"
473
+ ]);
485
474
  async function createSessionScanner(opts) {
486
475
  const projectDir = getProjectPath(opts.workingDirectory);
487
476
  let finishedSessions = /* @__PURE__ */ new Set();
@@ -491,26 +480,42 @@ async function createSessionScanner(opts) {
491
480
  let processedMessageKeys = /* @__PURE__ */ new Set();
492
481
  if (opts.sessionId) {
493
482
  let messages = await readSessionLog(projectDir, opts.sessionId);
483
+ logger.debug(`[SESSION_SCANNER] Marking ${messages.length} existing messages as processed from session ${opts.sessionId}`);
494
484
  for (let m of messages) {
495
485
  processedMessageKeys.add(messageKey(m));
496
486
  }
487
+ currentSessionId = opts.sessionId;
497
488
  }
498
489
  const sync = new InvalidateSync(async () => {
499
490
  let sessions = [];
500
491
  for (let p of pendingSessions) {
501
492
  sessions.push(p);
502
493
  }
503
- if (currentSessionId) {
494
+ if (currentSessionId && !pendingSessions.has(currentSessionId)) {
504
495
  sessions.push(currentSessionId);
505
496
  }
497
+ for (let [sessionId] of watchers) {
498
+ if (!sessions.includes(sessionId)) {
499
+ sessions.push(sessionId);
500
+ }
501
+ }
506
502
  for (let session of sessions) {
507
- for (let file of await readSessionLog(projectDir, session)) {
503
+ const sessionMessages = await readSessionLog(projectDir, session);
504
+ let skipped = 0;
505
+ let sent = 0;
506
+ for (let file of sessionMessages) {
508
507
  let key = messageKey(file);
509
508
  if (processedMessageKeys.has(key)) {
509
+ skipped++;
510
510
  continue;
511
511
  }
512
512
  processedMessageKeys.add(key);
513
+ logger.debug(`[SESSION_SCANNER] Sending new message: type=${file.type}, uuid=${file.type === "summary" ? file.leafUuid : file.uuid}`);
513
514
  opts.onMessage(file);
515
+ sent++;
516
+ }
517
+ if (sessionMessages.length > 0) {
518
+ logger.debug(`[SESSION_SCANNER] Session ${session}: found=${sessionMessages.length}, skipped=${skipped}, sent=${sent}`);
514
519
  }
515
520
  }
516
521
  for (let p of sessions) {
@@ -521,6 +526,7 @@ async function createSessionScanner(opts) {
521
526
  }
522
527
  for (let p of sessions) {
523
528
  if (!watchers.has(p)) {
529
+ logger.debug(`[SESSION_SCANNER] Starting watcher for session: ${p}`);
524
530
  watchers.set(p, startFileWatcher(join(projectDir, `${p}.jsonl`), () => {
525
531
  sync.invalidate();
526
532
  }));
@@ -594,9 +600,11 @@ async function readSessionLog(projectDir, sessionId) {
594
600
  continue;
595
601
  }
596
602
  let message = JSON.parse(l);
603
+ if (message.type && INTERNAL_CLAUDE_EVENT_TYPES.has(message.type)) {
604
+ continue;
605
+ }
597
606
  let parsed = RawJSONLinesSchema.safeParse(message);
598
607
  if (!parsed.success) {
599
- logger.debugLargeJson(`[SESSION_SCANNER] Failed to parse message`, message);
600
608
  continue;
601
609
  }
602
610
  messages.push(parsed.data);
@@ -961,8 +969,92 @@ class AbortError extends Error {
961
969
 
962
970
  const __filename = fileURLToPath(import.meta.url);
963
971
  const __dirname = join(__filename, "..");
972
+ function getGlobalClaudeVersion() {
973
+ try {
974
+ const cleanEnv = getCleanEnv();
975
+ const output = execSync("claude --version", {
976
+ encoding: "utf8",
977
+ stdio: ["pipe", "pipe", "pipe"],
978
+ cwd: homedir(),
979
+ env: cleanEnv
980
+ }).trim();
981
+ const match = output.match(/(\d+\.\d+\.\d+)/);
982
+ logger.debug(`[Claude SDK] Global claude --version output: ${output}`);
983
+ return match ? match[1] : null;
984
+ } catch {
985
+ return null;
986
+ }
987
+ }
988
+ function getCleanEnv() {
989
+ const env = { ...process.env };
990
+ const cwd = process.cwd();
991
+ const pathSep = process.platform === "win32" ? ";" : ":";
992
+ const pathKey = process.platform === "win32" ? "Path" : "PATH";
993
+ const actualPathKey = Object.keys(env).find((k) => k.toLowerCase() === "path") || pathKey;
994
+ if (env[actualPathKey]) {
995
+ const cleanPath = env[actualPathKey].split(pathSep).filter((p) => {
996
+ const normalizedP = p.replace(/\\/g, "/").toLowerCase();
997
+ const normalizedCwd = cwd.replace(/\\/g, "/").toLowerCase();
998
+ return !normalizedP.startsWith(normalizedCwd);
999
+ }).join(pathSep);
1000
+ env[actualPathKey] = cleanPath;
1001
+ logger.debug(`[Claude SDK] Cleaned PATH, removed local paths from: ${cwd}`);
1002
+ }
1003
+ return env;
1004
+ }
1005
+ function findGlobalClaudePath() {
1006
+ const homeDir = homedir();
1007
+ const cleanEnv = getCleanEnv();
1008
+ try {
1009
+ execSync("claude --version", {
1010
+ encoding: "utf8",
1011
+ stdio: ["pipe", "pipe", "pipe"],
1012
+ cwd: homeDir,
1013
+ env: cleanEnv
1014
+ });
1015
+ logger.debug("[Claude SDK] Global claude command available (checked with clean PATH)");
1016
+ return "claude";
1017
+ } catch {
1018
+ }
1019
+ if (process.platform !== "win32") {
1020
+ try {
1021
+ const result = execSync("which claude", {
1022
+ encoding: "utf8",
1023
+ stdio: ["pipe", "pipe", "pipe"],
1024
+ cwd: homeDir,
1025
+ env: cleanEnv
1026
+ }).trim();
1027
+ if (result && existsSync(result)) {
1028
+ logger.debug(`[Claude SDK] Found global claude path via which: ${result}`);
1029
+ return result;
1030
+ }
1031
+ } catch {
1032
+ }
1033
+ }
1034
+ return null;
1035
+ }
964
1036
  function getDefaultClaudeCodePath() {
965
- return join(__dirname, "..", "..", "..", "node_modules", "@anthropic-ai", "claude-code", "cli.js");
1037
+ const nodeModulesPath = join(__dirname, "..", "..", "..", "node_modules", "@anthropic-ai", "claude-code", "cli.js");
1038
+ if (process.env.HAPPY_CLAUDE_PATH) {
1039
+ logger.debug(`[Claude SDK] Using HAPPY_CLAUDE_PATH: ${process.env.HAPPY_CLAUDE_PATH}`);
1040
+ return process.env.HAPPY_CLAUDE_PATH;
1041
+ }
1042
+ if (process.env.HAPPY_USE_BUNDLED_CLAUDE === "1") {
1043
+ logger.debug(`[Claude SDK] Forced bundled version: ${nodeModulesPath}`);
1044
+ return nodeModulesPath;
1045
+ }
1046
+ const globalPath = findGlobalClaudePath();
1047
+ if (!globalPath) {
1048
+ logger.debug(`[Claude SDK] No global claude found, using bundled: ${nodeModulesPath}`);
1049
+ return nodeModulesPath;
1050
+ }
1051
+ const globalVersion = getGlobalClaudeVersion();
1052
+ logger.debug(`[Claude SDK] Global version: ${globalVersion || "unknown"}`);
1053
+ if (!globalVersion) {
1054
+ logger.debug(`[Claude SDK] Cannot compare versions, using global: ${globalPath}`);
1055
+ return globalPath;
1056
+ }
1057
+ return globalPath;
966
1058
  }
967
1059
  function logDebug(message) {
968
1060
  if (process.env.DEBUG) {
@@ -1227,17 +1319,22 @@ function query(config) {
1227
1319
  } else {
1228
1320
  args.push("--input-format", "stream-json");
1229
1321
  }
1230
- if (!existsSync(pathToClaudeCodeExecutable)) {
1322
+ const isJsFile = pathToClaudeCodeExecutable.endsWith(".js") || pathToClaudeCodeExecutable.endsWith(".cjs");
1323
+ const isCommandOnly = pathToClaudeCodeExecutable === "claude";
1324
+ if (!isCommandOnly && !existsSync(pathToClaudeCodeExecutable)) {
1231
1325
  throw new ReferenceError(`Claude Code executable not found at ${pathToClaudeCodeExecutable}. Is options.pathToClaudeCodeExecutable set?`);
1232
1326
  }
1233
- logDebug(`Spawning Claude Code process: ${executable} ${[...executableArgs, pathToClaudeCodeExecutable, ...args].join(" ")}`);
1234
- const child = spawn(executable, [...executableArgs, pathToClaudeCodeExecutable, ...args], {
1327
+ const spawnCommand = isJsFile ? executable : pathToClaudeCodeExecutable;
1328
+ const spawnArgs = isJsFile ? [...executableArgs, pathToClaudeCodeExecutable, ...args] : args;
1329
+ const spawnEnv = isCommandOnly ? getCleanEnv() : process.env;
1330
+ logDebug(`Spawning Claude Code process: ${spawnCommand} ${spawnArgs.join(" ")} (using ${isCommandOnly ? "clean" : "normal"} env)`);
1331
+ const child = spawn(spawnCommand, spawnArgs, {
1235
1332
  cwd,
1236
1333
  stdio: ["pipe", "pipe", "pipe"],
1237
1334
  signal: config.options?.abort,
1238
- env: {
1239
- ...process.env
1240
- }
1335
+ env: spawnEnv,
1336
+ // Use shell on Windows for global binaries and command-only mode
1337
+ shell: !isJsFile && process.platform === "win32"
1241
1338
  });
1242
1339
  let childStdin = null;
1243
1340
  if (typeof prompt === "string") {
@@ -4583,8 +4680,7 @@ async function startHappyServer(client) {
4583
4680
  };
4584
4681
  const mcp = new McpServer({
4585
4682
  name: "Happy MCP",
4586
- version: "1.0.0",
4587
- description: "Happy CLI MCP server with chat session management tools"
4683
+ version: "1.0.0"
4588
4684
  });
4589
4685
  mcp.registerTool("change_title", {
4590
4686
  description: "Change the title of the current chat session",
@@ -4675,7 +4771,7 @@ async function runClaude(credentials, options = {}) {
4675
4771
  const settings = await readSettings();
4676
4772
  let machineId = settings?.machineId;
4677
4773
  if (!machineId) {
4678
- console.error(`[START] No machine ID found in settings, which is unexepcted since authAndSetupMachineIfNeeded should have created it. Please report this issue on https://github.com/slopus/happy-cli/issues`);
4774
+ console.error(`[START] No machine ID found in settings, which is unexpected since authAndSetupMachineIfNeeded should have created it. Please report this issue on https://github.com/slopus/happy-cli/issues`);
4679
4775
  process.exit(1);
4680
4776
  }
4681
4777
  logger.debug(`Using machineId: ${machineId}`);
@@ -5794,7 +5890,7 @@ async function handleConnectVendor(vendor, displayName) {
5794
5890
  return;
5795
5891
  } else if (subcommand === "codex") {
5796
5892
  try {
5797
- const { runCodex } = await import('./runCodex-BnjA1TX6.mjs');
5893
+ const { runCodex } = await import('./runCodex-BgCwl6pc.mjs');
5798
5894
  let startedBy = void 0;
5799
5895
  for (let i = 1; i < args.length; i++) {
5800
5896
  if (args[i] === "--started-by") {