very-happy-cli 0.2.122 → 0.2.123

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.
Files changed (35) hide show
  1. package/dist/{AcpBackend-YPVwACkW.cjs → AcpBackend-DzIvpTxZ.cjs} +1 -1
  2. package/dist/{AcpBackend-pgDnK76l.mjs → AcpBackend-hM-qHLN3.mjs} +1 -1
  3. package/dist/{AcpSessionManager-Bq5Tiky1.mjs → AcpSessionManager-CRPZPHPa.mjs} +1 -1
  4. package/dist/{AcpSessionManager-6rypahwH.cjs → AcpSessionManager-DyU3Ikgj.cjs} +1 -1
  5. package/dist/{config-rQdv_Yr0.cjs → config-4RaHrVSV.cjs} +2 -2
  6. package/dist/{config-BTEW041n.mjs → config-alG86PDN.mjs} +2 -2
  7. package/dist/{index-BTnlYGaP.mjs → index-36AkXNF2.mjs} +388 -188
  8. package/dist/{index-DKRU6E-x.cjs → index-5b9YhV-1.cjs} +390 -190
  9. package/dist/{index-uPpFZhhr.cjs → index-Beh0UZf8.cjs} +4 -4
  10. package/dist/{index-BeHr5uMY.mjs → index-Cg3Vfc00.mjs} +5 -5
  11. package/dist/{index-BHg0P9cY.cjs → index-DWBKJ5zy.cjs} +2 -2
  12. package/dist/{index-CaIsj1q7.mjs → index-vyYHKKk6.mjs} +2 -2
  13. package/dist/index.cjs +2 -2
  14. package/dist/index.mjs +2 -2
  15. package/dist/{installTerminalHooks-CGqopr1A.cjs → installTerminalHooks-DWCa5j7u.cjs} +2 -2
  16. package/dist/{installTerminalHooks-DFmKhCto.mjs → installTerminalHooks-DqUPkh0l.mjs} +2 -2
  17. package/dist/lib.cjs +1 -1
  18. package/dist/lib.d.cts +7 -0
  19. package/dist/lib.d.mts +7 -0
  20. package/dist/lib.mjs +1 -1
  21. package/dist/{mcp-FlbfMw6_.cjs → mcp-BBya5dsZ.cjs} +2 -2
  22. package/dist/{mcp-DnKBKXYa.mjs → mcp-DZIdcsJp.mjs} +2 -2
  23. package/dist/{runGemini-BsfadDSa.mjs → runGemini-BQa2eF3i.mjs} +4 -4
  24. package/dist/{runGemini-DYawZQTu.cjs → runGemini-DmWxHIwS.cjs} +4 -4
  25. package/dist/{runOpenClaw-xGtbJ7f8.mjs → runOpenClaw-362eg04u.mjs} +3 -3
  26. package/dist/{runOpenClaw-C2XjFP2m.cjs → runOpenClaw-boyJtPBq.cjs} +3 -3
  27. package/dist/{send-B0Wojz18.cjs → send-BINA__Ke.cjs} +2 -2
  28. package/dist/{send-DRZppEr2.mjs → send-CT22-8Tp.mjs} +2 -2
  29. package/dist/{sessions-CTuUV0TW.cjs → sessions-DXYxV6Np.cjs} +2 -2
  30. package/dist/{sessions-BuMgxuxd.mjs → sessions-DemgtCkr.mjs} +2 -2
  31. package/dist/{spawn-7U_4QTHw.cjs → spawn-BtV9dU1i.cjs} +2 -2
  32. package/dist/{spawn-BRdfNDsu.mjs → spawn-D2egvPgO.mjs} +2 -2
  33. package/dist/{types-q7wtjfxb.mjs → types-BqW7nEN_.mjs} +14 -8
  34. package/dist/{types-Jag0GwhO.cjs → types-Cd_tUN3U.cjs} +15 -8
  35. package/package.json +7 -7
@@ -1,13 +1,13 @@
1
1
  import{createRequire as _pkgrollCR}from"node:module";const require=_pkgrollCR(import.meta.url);import chalk from 'chalk';
2
2
  import os, { homedir, userInfo, tmpdir } from 'node:os';
3
3
  import { randomUUID, randomBytes, createHash as createHash$1, timingSafeEqual } from 'node:crypto';
4
- import { o as readDaemonState, l as logger, c as configuration, q as clearDaemonState, t as getProjectPath, p as projectPath, u as initializeSandbox, v as wrapCommand, g as contentLogMetadata, I as InvalidateSync, R as RawJSONLinesSchema, k as delay, x as AsyncLock, S as STREAM_DELTA_MAX_CHARS, y as ensurePrivateDirectory, M as MAX_CHAT_ATTACHMENT_SOURCE_BYTES, z as hardenPrivateFile, j as packageJson, B as commandOnPath, e as readSettings, C as readCredentials, D as credentialRelayProblem, E as deriveLocalCliUpdateSummary, f as encodeBase64, F as readSettingsOutcome, G as updateSettings, H as encodeBase64Url, J as decodeBase64, K as writeCredentialsLegacy, L as writeCredentialsDataKey, N as readCredentialsForConfiguredRelay, O as encrypt, r as readPersistedSessions, P as isClaudeSessionId, w as writePrivateFileSync, Q as getLocalHappyAgentCredentialPath, T as readLocalHappyAgentCredentials, U as decryptWithDataKey, V as decryptLegacy, W as compareExactVersions, X as extractCompleteLines, Y as decideBackfill, Z as parseTerminalHookPayload, _ as tmuxArgs, $ as scrubTmuxClientEnv, a0 as decideMirrorBinding, a1 as MIRROR_BACKFILL_LINES_DEFAULT, a2 as persistSession, a3 as mirrorLineKey, a4 as mirrorLocalId, a5 as detectCLIAvailability, a6 as detectResumeSupport, a7 as acquireDaemonLock, a8 as summarizeSpawnSessionForLog, a9 as deletePersistedSessions, aa as shellescape, ab as decrypt, ac as writeDaemonState, A as ApiClient, ad as writeSettings, ae as fetchCliUpdateState, af as resolveCliUpdateCheckInterval, ag as DEFAULT_CLI_UPDATE_CHECK_INTERVAL_MS, ah as releaseDaemonLock, i as isValidSessionId, ai as listVhTerminals, aj as isValidTerminalId, ak as readVhTerminal, al as sendToVhTerminal, am as TITLE_PROMPT_PREFIX, an as BOARD_ANALYZER_PROMPT_PREFIX, d as connectionState, ao as startOfflineReconnection, ap as clearCredentials, aq as clearMachineId, m as ensurePrivateDirectorySync, ar as SandboxConfigSchema, b as createEnvelope, as as CodexAppServerClient, at as getLatestDaemonLog } from './types-q7wtjfxb.mjs';
4
+ import { o as readDaemonState, l as logger, c as configuration, q as clearDaemonState, t as getProjectPath, p as projectPath, u as initializeSandbox, v as wrapCommand, g as contentLogMetadata, I as InvalidateSync, R as RawJSONLinesSchema, k as delay, x as AsyncLock, S as STREAM_DELTA_MAX_CHARS, y as ensurePrivateDirectory, M as MAX_CHAT_ATTACHMENT_SOURCE_BYTES, z as hardenPrivateFile, j as packageJson, B as commandOnPath, e as readSettings, C as readCredentials, D as credentialRelayProblem, E as deriveLocalCliUpdateSummary, f as encodeBase64, F as readSettingsOutcome, G as updateSettings, H as encodeBase64Url, J as decodeBase64, K as writeCredentialsLegacy, L as writeCredentialsDataKey, N as readCredentialsForConfiguredRelay, O as encrypt, r as readPersistedSessions, P as isClaudeSessionId, w as writePrivateFileSync, Q as getLocalHappyAgentCredentialPath, T as readLocalHappyAgentCredentials, U as decryptWithDataKey, V as decryptLegacy, W as parseExactVersion, X as compareExactVersions, Y as extractCompleteLines, Z as decideBackfill, _ as parseTerminalHookPayload, $ as tmuxArgs, a0 as scrubTmuxClientEnv, a1 as decideMirrorBinding, a2 as MIRROR_BACKFILL_LINES_DEFAULT, a3 as persistSession, a4 as mirrorLineKey, a5 as mirrorLocalId, a6 as detectCLIAvailability, a7 as detectResumeSupport, a8 as acquireDaemonLock, a9 as summarizeSpawnSessionForLog, aa as deletePersistedSessions, ab as shellescape, ac as decrypt, ad as writeDaemonState, A as ApiClient, ae as writeSettings, af as fetchCliUpdateState, ag as resolveCliUpdateCheckInterval, ah as DEFAULT_CLI_UPDATE_CHECK_INTERVAL_MS, ai as releaseDaemonLock, i as isValidSessionId, aj as listVhTerminals, ak as isValidTerminalId, al as readVhTerminal, am as sendToVhTerminal, an as TITLE_PROMPT_PREFIX, ao as BOARD_ANALYZER_PROMPT_PREFIX, d as connectionState, ap as startOfflineReconnection, aq as clearCredentials, ar as clearMachineId, m as ensurePrivateDirectorySync, as as SandboxConfigSchema, b as createEnvelope, at as CodexAppServerClient, au as getLatestDaemonLog } from './types-BqW7nEN_.mjs';
5
5
  import spawn$1, { spawn } from 'cross-spawn';
6
- import path, { join, resolve, basename, delimiter, isAbsolute, normalize, sep, dirname } from 'node:path';
6
+ import path, { join, resolve, basename, isAbsolute, dirname, sep, delimiter, normalize } from 'node:path';
7
7
  import { createInterface } from 'node:readline';
8
8
  import { existsSync, readFileSync, readdirSync, statSync, mkdirSync, unlinkSync, renameSync, writeFileSync, accessSync, constants, rmSync as rmSync$1, chmodSync as chmodSync$1 } from 'node:fs';
9
9
  import { t as trimIdent, A as AGENT_GUIDANCE, P as PREVIEW_TOOL_NAME, R as REPORT_PROGRESS_TOOL_NAME, a as PREVIEW_TOOL_TITLE, b as PREVIEW_TOOL_DESCRIPTION, c as REPORT_PROGRESS_TOOL_TITLE, d as REPORT_PROGRESS_TOOL_DESCRIPTION } from './agentGuidance-C2_pkgRI.mjs';
10
- import { readFile, writeFile, rename, unlink, mkdir, stat, open as open$1, appendFile } from 'node:fs/promises';
10
+ import { readFile, writeFile, rename, unlink, mkdir, symlink, rm, readlink, lstat, stat, open as open$1, appendFile } from 'node:fs/promises';
11
11
  import fs, { watch, access } from 'fs/promises';
12
12
  import { useStdout, useInput, Box, Text, render } from 'ink';
13
13
  import React, { useState, useRef, useEffect, useCallback } from 'react';
@@ -7298,24 +7298,297 @@ function decideHandover(run) {
7298
7298
  return { action: "handover" };
7299
7299
  }
7300
7300
 
7301
- function decideAutoUpdate(context) {
7302
- if (!context.enabled) return { action: "skip", reason: "auto-update disabled" };
7303
- const target = context.autoUpdateVersion;
7304
- if (!target) return { action: "skip", reason: "no version approved for automatic install" };
7305
- const ordering = compareExactVersions(context.currentVersion, target);
7306
- if (ordering === null) return { action: "skip", reason: "version numbers not comparable" };
7307
- if (ordering === 0) return { action: "skip", reason: "already current" };
7308
- if (ordering > 0) return { action: "skip", reason: `already ahead of the approved ${target}` };
7309
- if (context.failedVersion === target) {
7310
- return { action: "skip", reason: `already failed to install ${target} once` };
7311
- }
7312
- if (!context.idle) return { action: "skip", reason: "machine is busy" };
7313
- return { action: "install", version: target };
7301
+ function serialTask(task) {
7302
+ let running = false;
7303
+ return async () => {
7304
+ if (running) return;
7305
+ running = true;
7306
+ try {
7307
+ await task();
7308
+ } finally {
7309
+ running = false;
7310
+ }
7311
+ };
7314
7312
  }
7313
+
7315
7314
  function autoUpdateInstallArgs(version) {
7316
7315
  return ["i", "-g", "--allow-scripts=very-happy-cli,node-pty", `very-happy-cli@${version}`];
7317
7316
  }
7318
7317
 
7318
+ const processDefaults = {
7319
+ spawn: spawn$3,
7320
+ killGroup: (pid) => {
7321
+ process.kill(-pid, "SIGKILL");
7322
+ },
7323
+ platform: process.platform,
7324
+ timeoutMs: 10 * 6e4,
7325
+ terminationGraceMs: 5e3
7326
+ };
7327
+ const runNpmProcess = (args, deps = processDefaults) => new Promise((resolveResult) => {
7328
+ let stdout = "";
7329
+ let stderr = "";
7330
+ let timedOut = false;
7331
+ let settled = false;
7332
+ let grace;
7333
+ let timer;
7334
+ let treeKilled = false;
7335
+ const done = (result) => {
7336
+ if (settled) return;
7337
+ settled = true;
7338
+ clearTimeout(timer);
7339
+ clearTimeout(grace);
7340
+ resolveResult(result);
7341
+ };
7342
+ if (args.some((arg) => !/^[A-Za-z0-9@=.,+/_-]+$/.test(arg))) {
7343
+ done({ code: null, stdout, stderr: "Invalid npm argument" });
7344
+ return;
7345
+ }
7346
+ const child = deps.platform === "win32" ? deps.spawn("cmd.exe", ["/d", "/s", "/c", `npm ${args.join(" ")}`], { stdio: ["ignore", "pipe", "pipe"] }) : deps.spawn("npm", args, { stdio: ["ignore", "pipe", "pipe"], detached: true });
7347
+ timer = setTimeout(() => {
7348
+ timedOut = true;
7349
+ grace = setTimeout(() => done({ code: null, stdout, stderr, terminationConfirmed: false }), deps.terminationGraceMs);
7350
+ if (!child.pid) return;
7351
+ if (deps.platform === "win32") {
7352
+ const killer = deps.spawn("taskkill.exe", ["/PID", String(child.pid), "/T", "/F"], { stdio: "ignore" });
7353
+ killer.on("error", () => void 0);
7354
+ killer.on("close", (code) => {
7355
+ treeKilled = code === 0;
7356
+ });
7357
+ } else {
7358
+ try {
7359
+ deps.killGroup(child.pid);
7360
+ treeKilled = true;
7361
+ } catch {
7362
+ }
7363
+ }
7364
+ }, deps.timeoutMs);
7365
+ child.stdout?.on("data", (data) => {
7366
+ stdout = (stdout + String(data)).slice(-8192);
7367
+ });
7368
+ child.stderr?.on("data", (data) => {
7369
+ stderr = (stderr + String(data)).slice(-8192);
7370
+ });
7371
+ child.on("error", () => done({ code: null, stdout, stderr, terminationConfirmed: !timedOut }));
7372
+ child.on("close", (code) => done({ code: timedOut ? null : code, stdout, stderr, terminationConfirmed: !timedOut || treeKilled }));
7373
+ });
7374
+ const runNpm = (args) => runNpmProcess(args);
7375
+ const defaults = { npm: runNpm, fs: { lstat, readlink, unlink, rm, readFile, symlink }, platform: process.platform };
7376
+ function ownedCliLink(link, target, packageDir) {
7377
+ const resolved = resolve(dirname(link), target);
7378
+ return resolved.startsWith(join(packageDir, "bin") + sep);
7379
+ }
7380
+ async function installCliSafely(version, deps = defaults) {
7381
+ const parsed = parseExactVersion(version);
7382
+ if (!parsed) throw new Error("Invalid exact CLI version");
7383
+ const args = autoUpdateInstallArgs(parsed.exact);
7384
+ if (deps.platform === "win32") {
7385
+ const result2 = await deps.npm(args);
7386
+ return result2.terminationConfirmed === false ? "blocked" : result2.code;
7387
+ }
7388
+ const rootResult = await deps.npm(["root", "-g"]);
7389
+ if (rootResult.terminationConfirmed === false) return "blocked";
7390
+ const prefixResult = await deps.npm(["prefix", "-g"]);
7391
+ if (prefixResult.terminationConfirmed === false) return "blocked";
7392
+ const root = rootResult.stdout.trim();
7393
+ const prefix = prefixResult.stdout.trim();
7394
+ if (rootResult.code !== 0 || prefixResult.code !== 0 || !isAbsolute(root) || !isAbsolute(prefix) || basename(root) !== "node_modules" || resolve(prefix) === "/" || root.includes("\n") || prefix.includes("\n")) {
7395
+ throw new Error("Cannot verify global npm paths; use manual install");
7396
+ }
7397
+ if (resolve(root) !== resolve(prefix, "lib/node_modules")) throw new Error("Unexpected npm root/prefix relationship");
7398
+ const packageDir = join(root, "very-happy-cli");
7399
+ const packageStat = await deps.fs.lstat(packageDir).catch((error) => {
7400
+ if (error.code === "ENOENT") return null;
7401
+ throw error;
7402
+ });
7403
+ if (packageStat && (!packageStat.isDirectory() || packageStat.isSymbolicLink())) {
7404
+ throw new Error("Refusing to modify a linked or non-directory CLI package");
7405
+ }
7406
+ if (packageStat) {
7407
+ const manifest = JSON.parse(await deps.fs.readFile(join(packageDir, "package.json"), "utf8"));
7408
+ if (manifest.name !== "very-happy-cli") throw new Error("Cannot verify installed CLI package identity");
7409
+ }
7410
+ const ownLinks = [];
7411
+ for (const name of ["very-happy", "very-happy-mcp"]) {
7412
+ const link = join(prefix, "bin", name);
7413
+ const stat = await deps.fs.lstat(link).catch((error) => {
7414
+ if (error.code === "ENOENT") return null;
7415
+ throw error;
7416
+ });
7417
+ if (!stat) continue;
7418
+ if (!stat.isSymbolicLink() || !ownedCliLink(link, await deps.fs.readlink(link), packageDir)) {
7419
+ throw new Error("CLI bin is owned by another installation; use manual recovery");
7420
+ }
7421
+ ownLinks.push({ link, target: await deps.fs.readlink(link) });
7422
+ }
7423
+ let result = null;
7424
+ let safeToRestore = true;
7425
+ try {
7426
+ for (const { link } of ownLinks) await deps.fs.unlink(link);
7427
+ const first = await deps.npm(args);
7428
+ if (first.terminationConfirmed === false) {
7429
+ safeToRestore = false;
7430
+ return "blocked";
7431
+ }
7432
+ result = first.code;
7433
+ if (first.code === 0 || first.code === null) return first.code;
7434
+ if (!/\b(?:EEXIST|ENOTEMPTY)\b/.test(first.stderr ?? "")) return first.code;
7435
+ const failedStat = await deps.fs.lstat(packageDir).catch((error) => {
7436
+ if (error.code === "ENOENT") return null;
7437
+ throw error;
7438
+ });
7439
+ if (failedStat && (!packageStat || !failedStat.isDirectory() || failedStat.isSymbolicLink())) {
7440
+ throw new Error("Refusing to remove an unverified CLI package");
7441
+ }
7442
+ if (failedStat) await deps.fs.rm(packageDir, { recursive: true, force: true });
7443
+ const second = await deps.npm(args);
7444
+ result = second.code;
7445
+ if (second.terminationConfirmed === false) {
7446
+ safeToRestore = false;
7447
+ return "blocked";
7448
+ }
7449
+ return result;
7450
+ } finally {
7451
+ if (result !== 0 && safeToRestore) {
7452
+ for (const { link, target } of ownLinks) {
7453
+ const entry = await deps.fs.lstat(resolve(dirname(link), target)).catch(() => null);
7454
+ const current = await deps.fs.lstat(link).catch(() => null);
7455
+ if (entry?.isFile() && !current) await deps.fs.symlink(target, link).catch(() => void 0);
7456
+ }
7457
+ }
7458
+ }
7459
+ }
7460
+
7461
+ function createUpdateController(deps) {
7462
+ let policy = null;
7463
+ let outcome = null;
7464
+ let running = false;
7465
+ let operation = null;
7466
+ let blocked = false;
7467
+ let handoverHold;
7468
+ let lastPublished = "";
7469
+ const now = deps.now ?? Date.now;
7470
+ function publish(state, version, detail) {
7471
+ if (!policy) return;
7472
+ const fingerprint = JSON.stringify([policy.checkedAt, state, version, detail]);
7473
+ if (fingerprint === lastPublished) return;
7474
+ lastPublished = fingerprint;
7475
+ outcome = { state, version, detail, at: now() };
7476
+ deps.publish({ ...policy, retrySupported: !blocked, handoverHold, autoUpdate: outcome });
7477
+ }
7478
+ async function evaluate() {
7479
+ if (!policy || running) return;
7480
+ const target = policy.autoUpdateVersion ?? null;
7481
+ if (outcome?.version === target && ["failed", "installed"].includes(outcome.state)) {
7482
+ const fingerprint = JSON.stringify([policy.checkedAt, outcome.state, outcome.version, outcome.detail]);
7483
+ if (fingerprint !== lastPublished) {
7484
+ lastPublished = fingerprint;
7485
+ deps.publish({ ...policy, retrySupported: !blocked, handoverHold, autoUpdate: outcome });
7486
+ }
7487
+ return;
7488
+ }
7489
+ if (now() - policy.checkedAt > 65 * 6e4) {
7490
+ publish("policy_stale", target);
7491
+ return;
7492
+ }
7493
+ if (!await deps.enabled()) {
7494
+ publish("disabled", target);
7495
+ return;
7496
+ }
7497
+ if (running) return;
7498
+ if (now() - policy.checkedAt > 65 * 6e4) {
7499
+ publish("policy_stale", target);
7500
+ return;
7501
+ }
7502
+ if (!target) {
7503
+ publish("unapproved", null);
7504
+ return;
7505
+ }
7506
+ const order = compareExactVersions(policy.currentVersion, target);
7507
+ if (order !== -1) {
7508
+ publish(order === null ? "policy_stale" : "current", target);
7509
+ return;
7510
+ }
7511
+ if (!deps.idle()) {
7512
+ publish("waiting_idle", target);
7513
+ return;
7514
+ }
7515
+ running = true;
7516
+ publish("installing", target);
7517
+ try {
7518
+ const code = await deps.install(target);
7519
+ blocked = code === "blocked";
7520
+ publish(
7521
+ blocked ? "manual_required" : code === 0 ? "installed" : "failed",
7522
+ target,
7523
+ blocked ? "installer_termination_unconfirmed" : code === 0 ? void 0 : "npm_install_failed"
7524
+ );
7525
+ } catch {
7526
+ publish("failed", target, "npm_install_failed");
7527
+ } finally {
7528
+ running = false;
7529
+ }
7530
+ }
7531
+ async function tick() {
7532
+ if (operation || blocked) return;
7533
+ operation = "tick";
7534
+ try {
7535
+ await evaluate();
7536
+ } finally {
7537
+ operation = null;
7538
+ }
7539
+ }
7540
+ async function refresh() {
7541
+ if (operation || blocked) return;
7542
+ operation = "refresh";
7543
+ try {
7544
+ const next = await deps.policy();
7545
+ if (!next) return;
7546
+ policy = next;
7547
+ await evaluate();
7548
+ } finally {
7549
+ operation = null;
7550
+ }
7551
+ }
7552
+ async function retry(version) {
7553
+ if (operation || blocked) return { error: blocked ? "manual_recovery_required" : "update_in_progress" };
7554
+ if (typeof version !== "string" || outcome?.state !== "failed" || outcome.version !== version) {
7555
+ return { error: "no_matching_failed_update" };
7556
+ }
7557
+ operation = "retry";
7558
+ try {
7559
+ const next = await deps.policy();
7560
+ if (!next || next.autoUpdateVersion !== version || now() - next.checkedAt > 65 * 6e4) {
7561
+ return { error: "update_policy_changed_or_unavailable" };
7562
+ }
7563
+ if (!await deps.enabled()) return { error: "auto_update_disabled" };
7564
+ policy = next;
7565
+ outcome = null;
7566
+ handoverHold = null;
7567
+ publish("waiting_idle", version);
7568
+ setTimeout(() => {
7569
+ void tick().catch(() => publish("policy_stale", version));
7570
+ }, 0);
7571
+ return { accepted: true };
7572
+ } finally {
7573
+ operation = null;
7574
+ }
7575
+ }
7576
+ async function withHandover(task) {
7577
+ if (operation || blocked) return;
7578
+ operation = "handover";
7579
+ try {
7580
+ await task();
7581
+ } finally {
7582
+ operation = null;
7583
+ }
7584
+ }
7585
+ function holdHandover(reason) {
7586
+ handoverHold = { reason, at: now() };
7587
+ publish("failed", policy?.autoUpdateVersion ?? null, "handover_preflight_failed");
7588
+ }
7589
+ return { refresh, retry, tick, withHandover, holdHandover, isRunning: () => running };
7590
+ }
7591
+
7319
7592
  const CLAUDE_KEYCHAIN_SERVICE_PREFIX = "Claude Code";
7320
7593
  function keychainIdentity(env = process.env, deps = {}) {
7321
7594
  const home = deps.home ?? homedir();
@@ -8592,45 +8865,7 @@ async function preflightNewBundle(bundlePath) {
8592
8865
  }
8593
8866
  }
8594
8867
  }
8595
- function withHandoverHold(state, reason) {
8596
- if (!state) return state;
8597
- return reason ? { ...state, handoverHold: { reason, at: Date.now() } } : { ...state, handoverHold: null };
8598
- }
8599
8868
  const HANDOVER_PREFLIGHT_TIMEOUT_MS = 3e4;
8600
- function runNpmInstall(args) {
8601
- return new Promise((resolve) => {
8602
- let settled = false;
8603
- const done = (code) => {
8604
- if (!settled) {
8605
- settled = true;
8606
- resolve(code);
8607
- }
8608
- };
8609
- let child;
8610
- try {
8611
- child = spawn$3("npm", args, { stdio: "ignore", env: process.env });
8612
- } catch {
8613
- done(null);
8614
- return;
8615
- }
8616
- const timer = setTimeout(() => {
8617
- try {
8618
- child.kill("SIGKILL");
8619
- } catch {
8620
- }
8621
- done(null);
8622
- }, AUTO_UPDATE_INSTALL_TIMEOUT_MS);
8623
- child.on("error", () => {
8624
- clearTimeout(timer);
8625
- done(null);
8626
- });
8627
- child.on("close", (code) => {
8628
- clearTimeout(timer);
8629
- done(code);
8630
- });
8631
- });
8632
- }
8633
- const AUTO_UPDATE_INSTALL_TIMEOUT_MS = 10 * 6e4;
8634
8869
  async function startDaemon() {
8635
8870
  let claudeAuthServiceRef = null;
8636
8871
  let cliUpdateStateRef = null;
@@ -9718,67 +9953,30 @@ async function startDaemon() {
9718
9953
  host: initialMachineMetadata.host
9719
9954
  })).then(() => logger.debug(`[DAEMON RUN] claimed the machine record for host ${initialMachineMetadata.host}`)).catch((error) => logger.debug("[DAEMON RUN] could not refresh the recorded host:", error));
9720
9955
  }
9721
- let autoUpdateFailedVersion = null;
9722
- let autoUpdateRunning = false;
9723
- const maybeAutoUpdate = async (cliUpdate) => {
9724
- if (autoUpdateRunning) return;
9725
- const settings = await readSettings();
9726
- const decision = decideAutoUpdate({
9727
- enabled: (settings.cliAutoUpdate ?? "idle") !== "off",
9728
- currentVersion: cliUpdate.currentVersion,
9729
- autoUpdateVersion: cliUpdate.autoUpdateVersion ?? null,
9730
- idle: pidToTrackedSession.size === 0 && !apiMachine.hasLiveTerminals(),
9731
- failedVersion: autoUpdateFailedVersion
9732
- });
9733
- if (decision.action === "skip") {
9734
- logger.debug(`[DAEMON RUN] auto-update skipped: ${decision.reason}`);
9735
- return;
9736
- }
9737
- autoUpdateRunning = true;
9738
- const target = decision.version;
9739
- const report = (state, detail) => {
9740
- cliUpdateStateRef = { ...cliUpdate, autoUpdate: { state, version: target, detail, at: Date.now() } };
9741
- void apiMachine.setCliUpdateState(cliUpdateStateRef);
9742
- };
9743
- logger.warn(`[DAEMON RUN] auto-updating CLI to ${target} (machine idle)`);
9744
- report("installing");
9745
- try {
9746
- const code = await runNpmInstall(autoUpdateInstallArgs(target));
9747
- if (code === 0) {
9748
- logger.warn(`[DAEMON RUN] auto-update installed ${target}; handover follows once it verifies`);
9749
- report("installed");
9956
+ let lastUpdateLog = "";
9957
+ const updateController = createUpdateController({
9958
+ policy: () => fetchCliUpdateState(configuration.serverUrl, packageJson.version),
9959
+ enabled: async () => ((await readSettings()).cliAutoUpdate ?? "idle") !== "off",
9960
+ idle: () => pidToTrackedSession.size === 0 && !apiMachine.hasLiveTerminals(),
9961
+ install: installCliSafely,
9962
+ publish: (state) => {
9963
+ cliUpdateStateRef = state;
9964
+ fileState = { ...fileState, cliUpdate: state };
9965
+ writeDaemonState(fileState);
9966
+ apiMachine.setCliUpdateState(state);
9967
+ const logKey = JSON.stringify([state.autoUpdate?.state, state.autoUpdate?.version, state.autoUpdate?.detail]);
9968
+ if (logKey !== lastUpdateLog) {
9969
+ lastUpdateLog = logKey;
9970
+ logger.warn(`[DAEMON RUN] CLI update: ${state.autoUpdate?.state} target=${state.autoUpdate?.version ?? "none"} detail=${state.autoUpdate?.detail ?? "none"}`);
9750
9971
  } else {
9751
- autoUpdateFailedVersion = target;
9752
- logger.warn(`[DAEMON RUN] auto-update of ${target} failed with exit ${code}`);
9753
- report("failed", `npm exited ${code}`);
9972
+ logger.debug("[DAEMON RUN] CLI update policy refreshed");
9754
9973
  }
9755
- } catch (error) {
9756
- autoUpdateFailedVersion = target;
9757
- logger.warn(`[DAEMON RUN] auto-update of ${target} could not run:`, error);
9758
- report("failed", String(error).slice(0, 200));
9759
- } finally {
9760
- autoUpdateRunning = false;
9761
- }
9762
- };
9763
- let cliUpdateCheckRunning = false;
9764
- const refreshCliUpdate = async () => {
9765
- if (cliUpdateCheckRunning) return;
9766
- cliUpdateCheckRunning = true;
9767
- try {
9768
- const cliUpdate = await fetchCliUpdateState(configuration.serverUrl, packageJson.version);
9769
- if (!cliUpdate) return;
9770
- fileState = { ...fileState, cliUpdate };
9771
- writeDaemonState(fileState);
9772
- cliUpdateStateRef = cliUpdate;
9773
- apiMachine.setCliUpdateState(cliUpdate);
9774
- logger.debug(`[DAEMON RUN] CLI update policy checked: ${cliUpdate.status} (recommended=${cliUpdate.recommendedVersion ?? "none"}, minimum=${cliUpdate.minimumVersion ?? "none"})`);
9775
- await maybeAutoUpdate(cliUpdate);
9776
- } catch (error) {
9777
- logger.debug("[DAEMON RUN] CLI update policy check failed:", error);
9778
- } finally {
9779
- cliUpdateCheckRunning = false;
9780
9974
  }
9781
- };
9975
+ });
9976
+ apiMachine.setCliUpdateRetryHandler(updateController.retry);
9977
+ const refreshCliUpdate = () => updateController.refresh().catch((error) => {
9978
+ logger.debug("[DAEMON RUN] CLI update policy check failed:", error);
9979
+ });
9782
9980
  void refreshCliUpdate();
9783
9981
  const cliUpdateIntervalMs = resolveCliUpdateCheckInterval(process.env.HAPPY_CLI_UPDATE_CHECK_INTERVAL);
9784
9982
  if (process.env.HAPPY_CLI_UPDATE_CHECK_INTERVAL && cliUpdateIntervalMs === DEFAULT_CLI_UPDATE_CHECK_INTERVAL_MS && process.env.HAPPY_CLI_UPDATE_CHECK_INTERVAL.trim() !== String(DEFAULT_CLI_UPDATE_CHECK_INTERVAL_MS)) {
@@ -9786,77 +9984,79 @@ async function startDaemon() {
9786
9984
  }
9787
9985
  const cliUpdateInterval = setInterval(() => void refreshCliUpdate(), cliUpdateIntervalMs);
9788
9986
  const heartbeatIntervalMs = parseInt(process.env.HAPPY_DAEMON_HEARTBEAT_INTERVAL || "60000");
9789
- let heartbeatRunning = false;
9790
- const restartOnStaleVersionAndHeartbeat = setInterval(async () => {
9791
- if (heartbeatRunning) {
9792
- return;
9793
- }
9794
- heartbeatRunning = true;
9795
- if (process.env.DEBUG) {
9796
- logger.debug(`[DAEMON RUN] Health check started at ${(/* @__PURE__ */ new Date()).toLocaleString()}`);
9797
- }
9798
- for (const [pid, _] of pidToTrackedSession.entries()) {
9799
- try {
9800
- process.kill(pid, 0);
9801
- } catch (error) {
9802
- logger.debug(`[DAEMON RUN] Finalizing stale session with PID ${pid} (process no longer exists)`);
9803
- onChildExited(pid);
9987
+ const restartOnStaleVersionAndHeartbeat = setInterval(serialTask(async () => {
9988
+ try {
9989
+ if (process.env.DEBUG) {
9990
+ logger.debug(`[DAEMON RUN] Health check started at ${(/* @__PURE__ */ new Date()).toLocaleString()}`);
9804
9991
  }
9805
- }
9806
- let bundleReplaced = false;
9807
- if (initialBundleMtimeMs > 0) {
9808
- try {
9809
- const currentMtimeMs = statSync$1(bundlePath).mtimeMs;
9810
- bundleReplaced = currentMtimeMs !== initialBundleMtimeMs;
9811
- } catch {
9992
+ for (const [pid, _] of pidToTrackedSession.entries()) {
9993
+ try {
9994
+ process.kill(pid, 0);
9995
+ } catch (error) {
9996
+ logger.debug(`[DAEMON RUN] Finalizing stale session with PID ${pid} (process no longer exists)`);
9997
+ onChildExited(pid);
9998
+ }
9812
9999
  }
9813
- }
9814
- if (bundleReplaced) {
9815
- const preflight = await preflightNewBundle(bundlePath);
9816
- if (preflight.action === "hold") {
9817
- if (lastHandoverHold !== preflight.reason) {
9818
- lastHandoverHold = preflight.reason;
9819
- logger.warn(`[DAEMON RUN] holding handover: ${preflight.reason}`);
9820
- void apiMachine.setCliUpdateState(withHandoverHold(cliUpdateStateRef, preflight.reason));
10000
+ await updateController.tick();
10001
+ await updateController.withHandover(async () => {
10002
+ let bundleReplaced = false;
10003
+ if (initialBundleMtimeMs > 0) {
10004
+ try {
10005
+ const currentMtimeMs = statSync$1(bundlePath).mtimeMs;
10006
+ bundleReplaced = currentMtimeMs !== initialBundleMtimeMs;
10007
+ } catch {
10008
+ }
9821
10009
  }
9822
- return;
10010
+ if (bundleReplaced && !updateController.isRunning() && pidToTrackedSession.size === 0 && !apiMachine.hasLiveTerminals()) {
10011
+ const preflight = await preflightNewBundle(bundlePath);
10012
+ if (preflight.action === "hold") {
10013
+ if (lastHandoverHold !== preflight.reason) {
10014
+ lastHandoverHold = preflight.reason;
10015
+ logger.warn(`[DAEMON RUN] holding handover: ${preflight.reason}`);
10016
+ }
10017
+ updateController.holdHandover(preflight.reason);
10018
+ return;
10019
+ }
10020
+ if (updateController.isRunning() || pidToTrackedSession.size > 0 || apiMachine.hasLiveTerminals()) return;
10021
+ lastHandoverHold = null;
10022
+ logger.debug("[DAEMON RUN] Daemon bundle replaced on disk and verified, handing off to new daemon");
10023
+ clearInterval(restartOnStaleVersionAndHeartbeat);
10024
+ clearInterval(cliUpdateInterval);
10025
+ claudeAuthService.stop();
10026
+ apiMachine.shutdown();
10027
+ await stopControlServer();
10028
+ await cleanupDaemonState();
10029
+ await releaseDaemonLock(daemonLockHandle);
10030
+ await stopCaffeinate();
10031
+ try {
10032
+ spawnHappyCLI(["daemon", "start"], {
10033
+ detached: true,
10034
+ stdio: "ignore"
10035
+ });
10036
+ } catch (error) {
10037
+ logger.debug("[DAEMON RUN] Failed to spawn new daemon, this is quite likely to happen during integration tests as we are cleaning out dist/ directory", error);
10038
+ }
10039
+ process.exit(0);
10040
+ }
10041
+ });
10042
+ const daemonState = await readDaemonState();
10043
+ if (daemonState && daemonState.pid !== process.pid) {
10044
+ logger.debug("[DAEMON RUN] Somehow a different daemon was started without killing us. We should kill ourselves.");
10045
+ requestShutdown("exception", "A different daemon was started without killing us. We should kill ourselves.");
9823
10046
  }
9824
- lastHandoverHold = null;
9825
- logger.debug("[DAEMON RUN] Daemon bundle replaced on disk and verified, handing off to new daemon");
9826
- clearInterval(restartOnStaleVersionAndHeartbeat);
9827
- clearInterval(cliUpdateInterval);
9828
- claudeAuthService.stop();
9829
- apiMachine.shutdown();
9830
- await stopControlServer();
9831
- await cleanupDaemonState();
9832
- await releaseDaemonLock(daemonLockHandle);
9833
- await stopCaffeinate();
9834
10047
  try {
9835
- spawnHappyCLI(["daemon", "start"], {
9836
- detached: true,
9837
- stdio: "ignore"
9838
- });
10048
+ fileState = withDaemonHeartbeat(fileState, (/* @__PURE__ */ new Date()).toLocaleString());
10049
+ writeDaemonState(fileState);
10050
+ if (process.env.DEBUG) {
10051
+ logger.debug(`[DAEMON RUN] Health check completed at ${fileState.lastHeartbeat}`);
10052
+ }
9839
10053
  } catch (error) {
9840
- logger.debug("[DAEMON RUN] Failed to spawn new daemon, this is quite likely to happen during integration tests as we are cleaning out dist/ directory", error);
9841
- }
9842
- process.exit(0);
9843
- }
9844
- const daemonState = await readDaemonState();
9845
- if (daemonState && daemonState.pid !== process.pid) {
9846
- logger.debug("[DAEMON RUN] Somehow a different daemon was started without killing us. We should kill ourselves.");
9847
- requestShutdown("exception", "A different daemon was started without killing us. We should kill ourselves.");
9848
- }
9849
- try {
9850
- fileState = withDaemonHeartbeat(fileState, (/* @__PURE__ */ new Date()).toLocaleString());
9851
- writeDaemonState(fileState);
9852
- if (process.env.DEBUG) {
9853
- logger.debug(`[DAEMON RUN] Health check completed at ${fileState.lastHeartbeat}`);
10054
+ logger.debug("[DAEMON RUN] Failed to write heartbeat", error);
9854
10055
  }
9855
10056
  } catch (error) {
9856
- logger.debug("[DAEMON RUN] Failed to write heartbeat", error);
10057
+ logger.debug("[DAEMON RUN] Heartbeat failed:", error);
9857
10058
  }
9858
- heartbeatRunning = false;
9859
- }, heartbeatIntervalMs);
10059
+ }), heartbeatIntervalMs);
9860
10060
  const cleanupAndShutdown = async (source, errorMessage) => {
9861
10061
  logger.debug(`[DAEMON RUN] Starting proper cleanup (source: ${source}, errorMessage: ${errorMessage})...`);
9862
10062
  if (restartOnStaleVersionAndHeartbeat) {
@@ -11296,7 +11496,7 @@ function registerSideQuestionHandler(rpc, deps) {
11296
11496
  const retainMs = deps.retainMs ?? SIDE_QUESTION_RETAIN_MS;
11297
11497
  const maxRunMs = deps.maxRunMs ?? SIDE_QUESTION_MAX_RUN_MS;
11298
11498
  const run = deps.run ?? (async (input) => {
11299
- const { query } = await import('./index-CaIsj1q7.mjs');
11499
+ const { query } = await import('./index-vyYHKKk6.mjs');
11300
11500
  return runSideQuestion(query, input);
11301
11501
  });
11302
11502
  const slots = /* @__PURE__ */ new Map();
@@ -16247,7 +16447,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
16247
16447
  process.exit(0);
16248
16448
  } else if (subcommand === "install-terminal-hooks") {
16249
16449
  try {
16250
- const { installTerminalHooks, parseTerminalHooksArgs, TERMINAL_HOOKS_HELP } = await import('./installTerminalHooks-DFmKhCto.mjs');
16450
+ const { installTerminalHooks, parseTerminalHooksArgs, TERMINAL_HOOKS_HELP } = await import('./installTerminalHooks-DqUPkh0l.mjs');
16251
16451
  const command = parseTerminalHooksArgs(args.slice(1));
16252
16452
  if (command.action === "help") {
16253
16453
  console.log(TERMINAL_HOOKS_HELP);
@@ -16264,7 +16464,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
16264
16464
  }
16265
16465
  } else if (subcommand === "spawn") {
16266
16466
  try {
16267
- const { handleSpawnCommand } = await import('./spawn-BRdfNDsu.mjs');
16467
+ const { handleSpawnCommand } = await import('./spawn-D2egvPgO.mjs');
16268
16468
  await handleSpawnCommand(args.slice(1));
16269
16469
  } catch (error) {
16270
16470
  console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
@@ -16276,7 +16476,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
16276
16476
  return;
16277
16477
  } else if (subcommand === "sessions") {
16278
16478
  try {
16279
- const { handleSessionsCommand } = await import('./sessions-BuMgxuxd.mjs');
16479
+ const { handleSessionsCommand } = await import('./sessions-DemgtCkr.mjs');
16280
16480
  await handleSessionsCommand(args.slice(1));
16281
16481
  } catch (error) {
16282
16482
  console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
@@ -16288,7 +16488,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
16288
16488
  return;
16289
16489
  } else if (subcommand === "send") {
16290
16490
  try {
16291
- const { handleSendCommand } = await import('./send-DRZppEr2.mjs');
16491
+ const { handleSendCommand } = await import('./send-CT22-8Tp.mjs');
16292
16492
  await handleSendCommand(args.slice(1));
16293
16493
  } catch (error) {
16294
16494
  console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
@@ -16394,9 +16594,9 @@ Conversation history is preserved on the server, but in-flight tool calls are in
16394
16594
  if (geminiSubcommand === "project" && args[2] === "set" && args[3]) {
16395
16595
  const projectId = args[3];
16396
16596
  try {
16397
- const { saveGoogleCloudProjectToConfig } = await import('./config-BTEW041n.mjs');
16398
- const { readCredentialsForConfiguredRelay: readCredentialsForConfiguredRelay2 } = await import('./types-q7wtjfxb.mjs').then(function (n) { return n.au; });
16399
- const { ApiClient: ApiClient2 } = await import('./types-q7wtjfxb.mjs').then(function (n) { return n.av; });
16597
+ const { saveGoogleCloudProjectToConfig } = await import('./config-alG86PDN.mjs');
16598
+ const { readCredentialsForConfiguredRelay: readCredentialsForConfiguredRelay2 } = await import('./types-BqW7nEN_.mjs').then(function (n) { return n.av; });
16599
+ const { ApiClient: ApiClient2 } = await import('./types-BqW7nEN_.mjs').then(function (n) { return n.aw; });
16400
16600
  let userEmail = void 0;
16401
16601
  try {
16402
16602
  const credentials = await readCredentialsForConfiguredRelay2();
@@ -16427,7 +16627,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
16427
16627
  }
16428
16628
  if (geminiSubcommand === "project" && args[2] === "get") {
16429
16629
  try {
16430
- const { readGeminiLocalConfig } = await import('./config-BTEW041n.mjs');
16630
+ const { readGeminiLocalConfig } = await import('./config-alG86PDN.mjs');
16431
16631
  const config = readGeminiLocalConfig();
16432
16632
  if (config.googleCloudProject) {
16433
16633
  console.log(`Current Google Cloud Project: ${config.googleCloudProject}`);
@@ -16467,7 +16667,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
16467
16667
  process.exit(0);
16468
16668
  }
16469
16669
  try {
16470
- const { runGemini } = await import('./runGemini-BsfadDSa.mjs');
16670
+ const { runGemini } = await import('./runGemini-BQa2eF3i.mjs');
16471
16671
  let startedBy = void 0;
16472
16672
  for (let i = 1; i < args.length; i++) {
16473
16673
  if (args[i] === "--started-by") {
@@ -16497,7 +16697,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
16497
16697
  }
16498
16698
  };
16499
16699
  try {
16500
- const { runAcp, resolveAcpAgentConfig } = await import('./index-BeHr5uMY.mjs');
16700
+ const { runAcp, resolveAcpAgentConfig } = await import('./index-Cg3Vfc00.mjs');
16501
16701
  const parsed = parsePiRunnerArgs(args.slice(1));
16502
16702
  const resolved = resolveAcpAgentConfig(["pi", ...parsed.passthrough]);
16503
16703
  const { credentials } = await authAndSetupMachineIfNeeded();
@@ -16524,7 +16724,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
16524
16724
  return;
16525
16725
  } else if (subcommand === "acp") {
16526
16726
  try {
16527
- const { runAcp, resolveAcpAgentConfig } = await import('./index-BeHr5uMY.mjs');
16727
+ const { runAcp, resolveAcpAgentConfig } = await import('./index-Cg3Vfc00.mjs');
16528
16728
  let startedBy = void 0;
16529
16729
  let verbose = false;
16530
16730
  const acpArgs = [];
@@ -16564,7 +16764,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
16564
16764
  return;
16565
16765
  } else if (subcommand === "openclaw") {
16566
16766
  try {
16567
- const { runOpenClaw } = await import('./runOpenClaw-xGtbJ7f8.mjs');
16767
+ const { runOpenClaw } = await import('./runOpenClaw-362eg04u.mjs');
16568
16768
  let startedBy = void 0;
16569
16769
  let verbose = false;
16570
16770
  let gatewayUrl;
@@ -16615,7 +16815,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
16615
16815
  return;
16616
16816
  } else if (subcommand === "mcp" && args.length === 1) {
16617
16817
  try {
16618
- const { handleMcpCommand } = await import('./mcp-DnKBKXYa.mjs');
16818
+ const { handleMcpCommand } = await import('./mcp-DZIdcsJp.mjs');
16619
16819
  await handleMcpCommand();
16620
16820
  } catch (error) {
16621
16821
  process.stderr.write(`[very-happy mcp] Fatal: ${error instanceof Error ? error.message : String(error)}