very-happy-cli 0.2.147 → 0.2.149

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 (51) hide show
  1. package/dist/{AcpBackend-CtoDlI4b.mjs → AcpBackend-Baqb8dNm.mjs} +2 -2
  2. package/dist/{AcpBackend-DYAbnvv0.cjs → AcpBackend-CnZpEfGd.cjs} +2 -2
  3. package/dist/{AcpSessionManager-BHnqkhBf.mjs → AcpSessionManager-CC6mdFXs.mjs} +1 -1
  4. package/dist/{AcpSessionManager-Cn2214Af.cjs → AcpSessionManager-e2vxmIbj.cjs} +1 -1
  5. package/dist/{agentGuidance-FDBKJgGC.cjs → agentGuidance--QIi_rZ2.cjs} +1 -1
  6. package/dist/{agentGuidance-CK792QWi.mjs → agentGuidance-oMVJvUl7.mjs} +1 -1
  7. package/dist/{agentHome-z_h7cVnq.cjs → agentHome-3kRCtzOL.cjs} +2 -2
  8. package/dist/{agentHome-Cj4LYQV-.mjs → agentHome-CKSPsS8d.mjs} +2 -2
  9. package/dist/codex/happyMcpStdioBridge.cjs +2 -2
  10. package/dist/codex/happyMcpStdioBridge.mjs +2 -2
  11. package/dist/{config-CVvEoSGv.mjs → config-C8jpk6IA.mjs} +4 -4
  12. package/dist/{config-C1ysAgX3.cjs → config-CzJZcvTO.cjs} +4 -4
  13. package/dist/{index-DUw34UHh.mjs → index-BPCXUrEe.mjs} +4 -4
  14. package/dist/{index-CXc3BQNG.cjs → index-CyfXsCXl.cjs} +76 -53
  15. package/dist/{index-C1QWpPoB.cjs → index-DBN2Zpqc.cjs} +4 -4
  16. package/dist/{index-B7UauKjp.mjs → index-Dfkyar7S.mjs} +72 -49
  17. package/dist/{index-DhcgPLDK.cjs → index-DgK4KX-Z.cjs} +7 -7
  18. package/dist/{index-EH6Pjsic.mjs → index-DrVq0CqK.mjs} +8 -8
  19. package/dist/index.cjs +4 -4
  20. package/dist/index.mjs +4 -4
  21. package/dist/{installTerminalHooks-AUERBJd4.mjs → installTerminalHooks-COqkLz6f.mjs} +4 -4
  22. package/dist/{installTerminalHooks-BMpt_X5L.cjs → installTerminalHooks-kdAoTfNT.cjs} +4 -4
  23. package/dist/lib.cjs +2 -2
  24. package/dist/lib.d.cts +10 -0
  25. package/dist/lib.d.mts +10 -0
  26. package/dist/lib.mjs +2 -2
  27. package/dist/{limits-Dh0u29C4.mjs → limits-BXI1Gzvd.mjs} +8 -8
  28. package/dist/{limits-WZLbGgEg.cjs → limits-FZWhlsBu.cjs} +8 -8
  29. package/dist/{mcp-3lcIERfY.mjs → mcp-Dfnoirpi.mjs} +4 -4
  30. package/dist/{mcp-D-Crar0K.cjs → mcp-DoLZbbZp.cjs} +4 -4
  31. package/dist/{piRuntime-dQ6fN-W9.mjs → piRuntime-BK6YZm2X.mjs} +2 -2
  32. package/dist/{piRuntime-Dkk0SGGN.cjs → piRuntime-DmE4ysfG.cjs} +2 -2
  33. package/dist/{piTerminal-YoY7Z7sP.mjs → piTerminal-Bczubq2n.mjs} +6 -6
  34. package/dist/{piTerminal-C1gXyJA1.cjs → piTerminal-qQAzQwIU.cjs} +6 -6
  35. package/dist/{runGemini-D2Hr4-Xz.mjs → runGemini-Cce_pjTs.mjs} +6 -6
  36. package/dist/{runGemini-Cj284Qhk.cjs → runGemini-ComTu0EB.cjs} +6 -6
  37. package/dist/{runOpenClaw-0tleKtRj.mjs → runOpenClaw-DXP5_chb.mjs} +5 -5
  38. package/dist/{runOpenClaw-USGbHPm-.cjs → runOpenClaw-M3YlDt8z.cjs} +5 -5
  39. package/dist/{send-Bt9iWhWx.cjs → send-0LrSwGgr.cjs} +4 -4
  40. package/dist/{send-vmjOSMYO.mjs → send-DDD57Cr_.mjs} +4 -4
  41. package/dist/{sessions-lzuQguqk.cjs → sessions-2Bkau9T4.cjs} +4 -4
  42. package/dist/{sessions-BeqWzIdm.mjs → sessions-ClBPEtz2.mjs} +4 -4
  43. package/dist/{spawn-13v957Zv.mjs → spawn-7lLymdT1.mjs} +4 -4
  44. package/dist/{spawn-B9kfeZZL.cjs → spawn-Cm3Da8Ge.cjs} +4 -4
  45. package/dist/{teams-DQ1pAIhZ.cjs → teams-DKKeIG-u.cjs} +4 -4
  46. package/dist/{teams-BkFamplo.mjs → teams-DyBjdXI3.mjs} +4 -4
  47. package/dist/{todo-Ba0Wpu2G.cjs → todo-8RgCTDxH.cjs} +3 -3
  48. package/dist/{todo-CxQEUY9J.mjs → todo-jXHGGH89.mjs} +3 -3
  49. package/dist/{types-DFYgWw3-.mjs → types-ipzMqciq.mjs} +116 -9
  50. package/dist/{types-CGACQD3M.cjs → types-t1gxALVE.cjs} +116 -9
  51. package/package.json +8 -8
@@ -1,5 +1,5 @@
1
1
  import axios from 'axios';
2
- import { l as logger, g as encodeBase64, j as encrypt, k as decrypt, m as decodeBase64, c as configuration, o as readDaemonState, q as clearDaemonState, s as decryptBlob, t as prepareClipboardText, v as prepareClipboardHistoryText, r as readSettings, d as readPersistedSessions, x as credentialRelayProblem, p as packageJson, y as readTrackedClaudeSessionIds, z as readTrackedCodexThreadIds, A as libsodiumEncryptForPublicKey, B as libsodiumPublicKeyFromSecretKey, D as getRandomBytes, E as redactLogValue } from './limits-Dh0u29C4.mjs';
2
+ import { l as logger, g as encodeBase64, j as encrypt, k as decrypt, m as decodeBase64, c as configuration, o as readDaemonState, q as clearDaemonState, s as decryptBlob, t as prepareClipboardText, v as prepareClipboardHistoryText, r as readSettings, d as readPersistedSessions, x as credentialRelayProblem, p as packageJson, y as readTrackedClaudeSessionIds, z as readTrackedCodexThreadIds, A as libsodiumEncryptForPublicKey, B as libsodiumPublicKeyFromSecretKey, D as getRandomBytes, E as redactLogValue } from './limits-BXI1Gzvd.mjs';
3
3
  import { EventEmitter } from 'node:events';
4
4
  import { io } from 'socket.io-client';
5
5
  import * as z from 'zod';
@@ -887,7 +887,9 @@ const WebTerminalListItemSchema = z$1.object({
887
887
  /** B-265: @vh_title_manual is set (user renamed it). */
888
888
  manual: z$1.boolean().optional(),
889
889
  /** B-273: opened attached to the user's tmux session of this name. */
890
- attachTmux: z$1.string().optional()
890
+ attachTmux: z$1.string().optional(),
891
+ /** B-486: direct-pty shell (no tmux) — not restorable after a daemon restart. */
892
+ direct: z$1.boolean().optional()
891
893
  });
892
894
  const ClosedTerminalRecordSchema = z$1.object({
893
895
  id: z$1.string(),
@@ -991,6 +993,17 @@ z$1.object({
991
993
  rpcAvailable: z$1.boolean(),
992
994
  detectedAt: z$1.number()
993
995
  }).optional(),
996
+ /**
997
+ * B-486: can web terminals on this machine be durable? `tmuxAvailable:
998
+ * false` ⇒ every new terminal is a direct shell that dies with the daemon,
999
+ * so the web tells the user to install tmux. Same trust rule
1000
+ * (`detectedAt >= startedAt`); old webs ignore it.
1001
+ */
1002
+ terminalHost: z$1.object({
1003
+ tmuxAvailable: z$1.boolean(),
1004
+ tmuxVersion: z$1.string().optional(),
1005
+ detectedAt: z$1.number()
1006
+ }).optional(),
994
1007
  /** Relay-owned CLI compatibility/update policy last checked by this daemon. */
995
1008
  cliUpdate: CliUpdateStateSchema.optional(),
996
1009
  /**
@@ -1921,6 +1934,7 @@ const PRICING = {
1921
1934
  "claude-mythos-5-1": { input: 10, output: 50, cache_write: 12.5, cache_read: 0.25 },
1922
1935
  "claude-mythos-5": { input: 10, output: 50, cache_write: 12.5, cache_read: 1 },
1923
1936
  // --- Claude 5 ---
1937
+ "claude-opus-5-5": { input: 4, output: 20, cache_write: 5, cache_read: 0.2 },
1924
1938
  "claude-opus-5": { input: 5, output: 25, cache_write: 6.25, cache_read: 0.5 },
1925
1939
  "claude-sonnet-5": { input: 2, output: 10, cache_write: 2.5, cache_read: 0.2 },
1926
1940
  // --- Claude 4.x ---
@@ -1943,12 +1957,12 @@ const PRICING = {
1943
1957
  "claude-3-haiku-20240307": { input: 0.25, output: 1.25, cache_write: 0.3125, cache_read: 0.025 },
1944
1958
  "claude-3-5-haiku-20241022": { input: 0.8, output: 4, cache_write: 1, cache_read: 0.08 }
1945
1959
  };
1946
- const DEFAULT_MODEL = "claude-opus-5";
1960
+ const DEFAULT_MODEL = "claude-opus-5-5";
1947
1961
  const FAMILY_DEFAULTS = {
1948
1962
  fable: "claude-fable-5-1",
1949
1963
  // Project Glasswing counterpart of Fable: same tier, same published rates.
1950
1964
  mythos: "claude-mythos-5-1",
1951
- opus: "claude-opus-5",
1965
+ opus: "claude-opus-5-5",
1952
1966
  sonnet: "claude-sonnet-5",
1953
1967
  haiku: "claude-haiku-4-5"
1954
1968
  };
@@ -10479,6 +10493,25 @@ function sanitizeClosedTerminals(raw, max = CLOSED_TERMINALS_MAX) {
10479
10493
  return out.slice(0, max);
10480
10494
  }
10481
10495
 
10496
+ function directTerminalItems(sessions, tmuxIds) {
10497
+ const out = [];
10498
+ for (const s of sessions) {
10499
+ if (!s.direct || tmuxIds.has(s.id)) continue;
10500
+ const title = (s.direct.title ?? "").trim();
10501
+ out.push({
10502
+ id: s.id,
10503
+ ...title ? { title } : {},
10504
+ tags: s.direct.tags ?? [],
10505
+ ...s.direct.manual ? { manual: true } : {},
10506
+ cwd: s.direct.cwd,
10507
+ createdAt: s.direct.createdAt,
10508
+ ...s.lastOutputAt ? { activityAt: s.lastOutputAt } : {},
10509
+ direct: true
10510
+ });
10511
+ }
10512
+ return out.sort((a, b) => a.createdAt - b.createdAt || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
10513
+ }
10514
+
10482
10515
  const TOMBSTONE_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
10483
10516
  function tmuxKillVerified(kill, probe) {
10484
10517
  return !kill.error && !probe.error && typeof probe.status === "number" && probe.status !== 0;
@@ -10615,10 +10648,14 @@ function terminalListSignature(items) {
10615
10648
  // B-105: a mirror binding appearing/disappearing MUST push the
10616
10649
  // list, or the web never learns the toggle became available.
10617
10650
  t.mirrorSessionId ?? "",
10618
- t.attachTmux ?? ""
10651
+ t.attachTmux ?? "",
10652
+ t.direct ? 1 : 0
10619
10653
  ]);
10620
10654
  return JSON.stringify(canon);
10621
10655
  }
10656
+ function hostListSignature(items) {
10657
+ return `${isTmuxAvailable() ? "tmux" : "no-tmux"}|${terminalListSignature(items)}`;
10658
+ }
10622
10659
  const SHELL_COMMANDS = /* @__PURE__ */ new Set(["zsh", "bash", "fish", "sh", "dash", "ksh", "tcsh", "csh"]);
10623
10660
  const TITLE_MAX_CHARS = 60;
10624
10661
  const JUNK_TITLES = /* @__PURE__ */ new Set(["tmux", "claude", "node", ...SHELL_COMMANDS]);
@@ -10767,16 +10804,38 @@ function tmuxSupportsNewSessionEnv(versionOutput) {
10767
10804
  return maj > 3 || maj === 3 && min >= 2;
10768
10805
  }
10769
10806
  let tmuxAvailableCache = null;
10807
+ let tmuxMissingSince = 0;
10808
+ const TMUX_MISSING_REPROBE_MS = 3e4;
10770
10809
  function isTmuxAvailable() {
10771
- if (tmuxAvailableCache !== null) return tmuxAvailableCache;
10810
+ if (tmuxAvailableCache === true) return true;
10811
+ if (tmuxAvailableCache === false && Date.now() - tmuxMissingSince < TMUX_MISSING_REPROBE_MS) return false;
10772
10812
  try {
10773
10813
  const r = spawnSync("tmux", tmuxArgs(["-V"]), { stdio: "ignore", env: ptyEnv() });
10774
10814
  tmuxAvailableCache = r.status === 0;
10775
10815
  } catch {
10776
10816
  tmuxAvailableCache = false;
10777
10817
  }
10818
+ if (tmuxAvailableCache) {
10819
+ tmuxEnvFlagCache = null;
10820
+ tmuxVersionCache = void 0;
10821
+ } else {
10822
+ tmuxMissingSince = Date.now();
10823
+ }
10778
10824
  return tmuxAvailableCache;
10779
10825
  }
10826
+ function tmuxRuntimeInfo() {
10827
+ if (!isTmuxAvailable()) return { tmuxAvailable: false };
10828
+ if (tmuxVersionCache === void 0) {
10829
+ try {
10830
+ const r = spawnSync("tmux", tmuxArgs(["-V"]), { encoding: "utf8", timeout: TMUX_PROBE_TIMEOUT_MS, env: ptyEnv() });
10831
+ tmuxVersionCache = (r.stdout || "").trim().slice(0, 64);
10832
+ } catch {
10833
+ tmuxVersionCache = "";
10834
+ }
10835
+ }
10836
+ return tmuxVersionCache ? { tmuxAvailable: true, tmuxVersion: tmuxVersionCache } : { tmuxAvailable: true };
10837
+ }
10838
+ let tmuxVersionCache;
10780
10839
  let tmuxEnvFlagCache = null;
10781
10840
  function tmuxSupportsEnvFlag() {
10782
10841
  if (tmuxEnvFlagCache !== null) return tmuxEnvFlagCache;
@@ -10861,6 +10920,9 @@ class TerminalSession {
10861
10920
  id;
10862
10921
  tmuxSession;
10863
10922
  transport;
10923
+ /** B-486: set only for a direct-pty (no tmux) session — the list row's
10924
+ * source, since tmux knows nothing about it. */
10925
+ direct;
10864
10926
  /** B-273: the user tmux session this terminal was created to attach —
10865
10927
  * echoed to any later open of the same live session. */
10866
10928
  attachedTmux;
@@ -11324,7 +11386,7 @@ class WebTerminalManager {
11324
11386
  * other means (the connect-time daemonState write carries the initial
11325
11387
  * snapshot), so the first tick doesn't re-push an identical list. */
11326
11388
  primeListSignature(list) {
11327
- this.lastListSignature = terminalListSignature(list);
11389
+ this.lastListSignature = hostListSignature(list);
11328
11390
  this.noteSeen(list);
11329
11391
  }
11330
11392
  /** The retained closed-terminal records (newest first) — shipped by
@@ -11365,6 +11427,7 @@ class WebTerminalManager {
11365
11427
  */
11366
11428
  buildTerminalList() {
11367
11429
  const list = this.listSessions();
11430
+ list.push(...directTerminalItems(this.terminals.values(), new Set(list.map((t) => t.id))));
11368
11431
  for (const item of list) {
11369
11432
  const live = this.terminals.get(item.id);
11370
11433
  if (live?.lastOutputAt) {
@@ -11402,7 +11465,7 @@ class WebTerminalManager {
11402
11465
  list.filter((t) => t.activityAt).map((t) => [t.id, t.activityAt])
11403
11466
  ));
11404
11467
  this.mirrorTickObserver?.(list);
11405
- const sig = terminalListSignature(list);
11468
+ const sig = hostListSignature(list);
11406
11469
  if (sig === this.lastListSignature) return;
11407
11470
  this.lastListSignature = sig;
11408
11471
  cb(list);
@@ -11750,6 +11813,7 @@ class WebTerminalManager {
11750
11813
  const now = Date.now();
11751
11814
  this.snapshots.sweep(now);
11752
11815
  for (const [id, session] of [...this.terminals]) {
11816
+ if (session.direct) continue;
11753
11817
  if (session.subscribers === 0 && now - session.lastTouch > SESSION_IDLE_MS) {
11754
11818
  logger.debug(`[WEB TERMINAL] reaping orphaned idle session ${id} (idle ${Math.round((now - session.lastTouch) / 6e4)}m)`);
11755
11819
  this.detach(id);
@@ -12026,6 +12090,10 @@ class WebTerminalManager {
12026
12090
  }
12027
12091
  }
12028
12092
  const created = new TerminalSession(id, { kind: "pty", pty: proc }, void 0, cols, rows);
12093
+ created.direct = { cwd, createdAt: Date.now() };
12094
+ created.onTitleChange((title) => {
12095
+ if (created.direct && !created.direct.manual) created.direct.title = deriveAutoTitle(title, os$1.hostname());
12096
+ });
12029
12097
  created.onOutputChunk = (chunk) => {
12030
12098
  this.emit("terminal-output", { terminalId: id, data: chunk.data, seq: chunk.seq });
12031
12099
  this.noteActivity();
@@ -12378,6 +12446,26 @@ class WebTerminalManager {
12378
12446
  * session (so a local `tmux attach` won't find it either). Used when the
12379
12447
  * user deletes the terminal from the sidebar. */
12380
12448
  killSession(terminalId, opts) {
12449
+ const liveDirect = this.terminals.get(terminalId);
12450
+ if (liveDirect?.direct) {
12451
+ this.recordClosed({
12452
+ id: terminalId,
12453
+ ...liveDirect.direct.title?.trim() ? { title: liveDirect.direct.title.trim() } : {},
12454
+ cwd: liveDirect.direct.cwd,
12455
+ tags: liveDirect.direct.tags ?? [],
12456
+ ...liveDirect.direct.manual ? { manual: true } : {},
12457
+ reason: "closed",
12458
+ closedAt: Date.now()
12459
+ });
12460
+ this.lastSeenInfo.delete(terminalId);
12461
+ this.detach(terminalId);
12462
+ this.tombstones[terminalId] = Date.now();
12463
+ saveTombstones(this.tombstones);
12464
+ this.notifyTracker.remove(terminalId);
12465
+ logger.debug(`[WEB TERMINAL] killed direct pty ${terminalId}`);
12466
+ this.kickListRefresh();
12467
+ return true;
12468
+ }
12381
12469
  const info = this.lastSeenInfo.get(terminalId) ?? (() => {
12382
12470
  const t = this.listSessions().find((x) => x.id === terminalId);
12383
12471
  return t ? seenInfoOf(t) : void 0;
@@ -12662,6 +12750,14 @@ class WebTerminalManager {
12662
12750
  * landed. `ifAbsent` finding an existing title counts as success (there IS
12663
12751
  * a title; nothing to retry). */
12664
12752
  setTitle(terminalId, title, ifAbsent = false) {
12753
+ const direct = this.terminals.get(terminalId)?.direct;
12754
+ if (direct) {
12755
+ if (ifAbsent && (direct.manual || direct.title?.trim())) return true;
12756
+ direct.title = title;
12757
+ if (!ifAbsent) direct.manual = true;
12758
+ this.kickListRefresh();
12759
+ return true;
12760
+ }
12665
12761
  if (!isTmuxAvailable()) return false;
12666
12762
  if (!/^[a-zA-Z0-9_-]{1,64}$/.test(terminalId)) return false;
12667
12763
  const name = `vh-${terminalId}`;
@@ -12686,10 +12782,16 @@ class WebTerminalManager {
12686
12782
  }
12687
12783
  /** Persist terminal tags in tmux, parallel to @vh_title. */
12688
12784
  setTags(terminalId, tags) {
12689
- if (!isTmuxAvailable()) return false;
12785
+ const direct = this.terminals.get(terminalId)?.direct;
12786
+ if (!direct && !isTmuxAvailable()) return false;
12690
12787
  if (!/^[a-zA-Z0-9_-]{1,64}$/.test(terminalId)) return false;
12691
12788
  const valid = validateTerminalTags(tags);
12692
12789
  if (!valid) return false;
12790
+ if (direct) {
12791
+ direct.tags = valid;
12792
+ this.kickListRefresh();
12793
+ return true;
12794
+ }
12693
12795
  const name = `vh-${terminalId}`;
12694
12796
  try {
12695
12797
  const r = spawnSync("tmux", tmuxArgs(["set-option", "-t", name, "@vh_tags", JSON.stringify(valid)]), {
@@ -15777,6 +15879,9 @@ class ApiMachineClient {
15777
15879
  // even in the (unreachable) case of a null prior state.
15778
15880
  status: state?.status ?? "running",
15779
15881
  webTerminals: { updatedAt: Date.now(), terminals: list },
15882
+ // B-486: tmux can appear (user installs it) mid-run; the
15883
+ // tracker forces a push on that flip, so restamp here too.
15884
+ terminalHost: { ...tmuxRuntimeInfo(), detectedAt: Date.now() },
15780
15885
  // B-084: the closed-terminal records ride every list push
15781
15886
  // (a close always changes the list, so they stay in step).
15782
15887
  // Read at write time — same freshness rule as the list.
@@ -15926,6 +16031,8 @@ class ApiMachineClient {
15926
16031
  terminalRestore: { rpcAvailable: true, detectedAt: now },
15927
16032
  // B-273 capability flag (same restamp discipline).
15928
16033
  tmuxSessions: { rpcAvailable: true, detectedAt: now, killAttached: true },
16034
+ // B-486: tmux presence (same restamp discipline).
16035
+ terminalHost: { ...tmuxRuntimeInfo(), detectedAt: now },
15929
16036
  // B-290 capability flag: `claude-list-history` (same restamp discipline).
15930
16037
  claudeHistory: { rpcAvailable: true, detectedAt: now },
15931
16038
  // B-464 capability flag: `codex-list-history` / `codex-import-session`.
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  var axios = require('axios');
4
- var persistence = require('./limits-WZLbGgEg.cjs');
4
+ var persistence = require('./limits-FZWhlsBu.cjs');
5
5
  var node_events = require('node:events');
6
6
  var socket_ioClient = require('socket.io-client');
7
7
  var z = require('zod');
@@ -908,7 +908,9 @@ const WebTerminalListItemSchema = z.z.object({
908
908
  /** B-265: @vh_title_manual is set (user renamed it). */
909
909
  manual: z.z.boolean().optional(),
910
910
  /** B-273: opened attached to the user's tmux session of this name. */
911
- attachTmux: z.z.string().optional()
911
+ attachTmux: z.z.string().optional(),
912
+ /** B-486: direct-pty shell (no tmux) — not restorable after a daemon restart. */
913
+ direct: z.z.boolean().optional()
912
914
  });
913
915
  const ClosedTerminalRecordSchema = z.z.object({
914
916
  id: z.z.string(),
@@ -1012,6 +1014,17 @@ z.z.object({
1012
1014
  rpcAvailable: z.z.boolean(),
1013
1015
  detectedAt: z.z.number()
1014
1016
  }).optional(),
1017
+ /**
1018
+ * B-486: can web terminals on this machine be durable? `tmuxAvailable:
1019
+ * false` ⇒ every new terminal is a direct shell that dies with the daemon,
1020
+ * so the web tells the user to install tmux. Same trust rule
1021
+ * (`detectedAt >= startedAt`); old webs ignore it.
1022
+ */
1023
+ terminalHost: z.z.object({
1024
+ tmuxAvailable: z.z.boolean(),
1025
+ tmuxVersion: z.z.string().optional(),
1026
+ detectedAt: z.z.number()
1027
+ }).optional(),
1015
1028
  /** Relay-owned CLI compatibility/update policy last checked by this daemon. */
1016
1029
  cliUpdate: CliUpdateStateSchema.optional(),
1017
1030
  /**
@@ -1942,6 +1955,7 @@ const PRICING = {
1942
1955
  "claude-mythos-5-1": { input: 10, output: 50, cache_write: 12.5, cache_read: 0.25 },
1943
1956
  "claude-mythos-5": { input: 10, output: 50, cache_write: 12.5, cache_read: 1 },
1944
1957
  // --- Claude 5 ---
1958
+ "claude-opus-5-5": { input: 4, output: 20, cache_write: 5, cache_read: 0.2 },
1945
1959
  "claude-opus-5": { input: 5, output: 25, cache_write: 6.25, cache_read: 0.5 },
1946
1960
  "claude-sonnet-5": { input: 2, output: 10, cache_write: 2.5, cache_read: 0.2 },
1947
1961
  // --- Claude 4.x ---
@@ -1964,12 +1978,12 @@ const PRICING = {
1964
1978
  "claude-3-haiku-20240307": { input: 0.25, output: 1.25, cache_write: 0.3125, cache_read: 0.025 },
1965
1979
  "claude-3-5-haiku-20241022": { input: 0.8, output: 4, cache_write: 1, cache_read: 0.08 }
1966
1980
  };
1967
- const DEFAULT_MODEL = "claude-opus-5";
1981
+ const DEFAULT_MODEL = "claude-opus-5-5";
1968
1982
  const FAMILY_DEFAULTS = {
1969
1983
  fable: "claude-fable-5-1",
1970
1984
  // Project Glasswing counterpart of Fable: same tier, same published rates.
1971
1985
  mythos: "claude-mythos-5-1",
1972
- opus: "claude-opus-5",
1986
+ opus: "claude-opus-5-5",
1973
1987
  sonnet: "claude-sonnet-5",
1974
1988
  haiku: "claude-haiku-4-5"
1975
1989
  };
@@ -10500,6 +10514,25 @@ function sanitizeClosedTerminals(raw, max = CLOSED_TERMINALS_MAX) {
10500
10514
  return out.slice(0, max);
10501
10515
  }
10502
10516
 
10517
+ function directTerminalItems(sessions, tmuxIds) {
10518
+ const out = [];
10519
+ for (const s of sessions) {
10520
+ if (!s.direct || tmuxIds.has(s.id)) continue;
10521
+ const title = (s.direct.title ?? "").trim();
10522
+ out.push({
10523
+ id: s.id,
10524
+ ...title ? { title } : {},
10525
+ tags: s.direct.tags ?? [],
10526
+ ...s.direct.manual ? { manual: true } : {},
10527
+ cwd: s.direct.cwd,
10528
+ createdAt: s.direct.createdAt,
10529
+ ...s.lastOutputAt ? { activityAt: s.lastOutputAt } : {},
10530
+ direct: true
10531
+ });
10532
+ }
10533
+ return out.sort((a, b) => a.createdAt - b.createdAt || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
10534
+ }
10535
+
10503
10536
  const TOMBSTONE_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
10504
10537
  function tmuxKillVerified(kill, probe) {
10505
10538
  return !kill.error && !probe.error && typeof probe.status === "number" && probe.status !== 0;
@@ -10636,10 +10669,14 @@ function terminalListSignature(items) {
10636
10669
  // B-105: a mirror binding appearing/disappearing MUST push the
10637
10670
  // list, or the web never learns the toggle became available.
10638
10671
  t.mirrorSessionId ?? "",
10639
- t.attachTmux ?? ""
10672
+ t.attachTmux ?? "",
10673
+ t.direct ? 1 : 0
10640
10674
  ]);
10641
10675
  return JSON.stringify(canon);
10642
10676
  }
10677
+ function hostListSignature(items) {
10678
+ return `${isTmuxAvailable() ? "tmux" : "no-tmux"}|${terminalListSignature(items)}`;
10679
+ }
10643
10680
  const SHELL_COMMANDS = /* @__PURE__ */ new Set(["zsh", "bash", "fish", "sh", "dash", "ksh", "tcsh", "csh"]);
10644
10681
  const TITLE_MAX_CHARS = 60;
10645
10682
  const JUNK_TITLES = /* @__PURE__ */ new Set(["tmux", "claude", "node", ...SHELL_COMMANDS]);
@@ -10788,16 +10825,38 @@ function tmuxSupportsNewSessionEnv(versionOutput) {
10788
10825
  return maj > 3 || maj === 3 && min >= 2;
10789
10826
  }
10790
10827
  let tmuxAvailableCache = null;
10828
+ let tmuxMissingSince = 0;
10829
+ const TMUX_MISSING_REPROBE_MS = 3e4;
10791
10830
  function isTmuxAvailable() {
10792
- if (tmuxAvailableCache !== null) return tmuxAvailableCache;
10831
+ if (tmuxAvailableCache === true) return true;
10832
+ if (tmuxAvailableCache === false && Date.now() - tmuxMissingSince < TMUX_MISSING_REPROBE_MS) return false;
10793
10833
  try {
10794
10834
  const r = node_child_process.spawnSync("tmux", tmuxArgs(["-V"]), { stdio: "ignore", env: ptyEnv() });
10795
10835
  tmuxAvailableCache = r.status === 0;
10796
10836
  } catch {
10797
10837
  tmuxAvailableCache = false;
10798
10838
  }
10839
+ if (tmuxAvailableCache) {
10840
+ tmuxEnvFlagCache = null;
10841
+ tmuxVersionCache = void 0;
10842
+ } else {
10843
+ tmuxMissingSince = Date.now();
10844
+ }
10799
10845
  return tmuxAvailableCache;
10800
10846
  }
10847
+ function tmuxRuntimeInfo() {
10848
+ if (!isTmuxAvailable()) return { tmuxAvailable: false };
10849
+ if (tmuxVersionCache === void 0) {
10850
+ try {
10851
+ const r = node_child_process.spawnSync("tmux", tmuxArgs(["-V"]), { encoding: "utf8", timeout: TMUX_PROBE_TIMEOUT_MS, env: ptyEnv() });
10852
+ tmuxVersionCache = (r.stdout || "").trim().slice(0, 64);
10853
+ } catch {
10854
+ tmuxVersionCache = "";
10855
+ }
10856
+ }
10857
+ return tmuxVersionCache ? { tmuxAvailable: true, tmuxVersion: tmuxVersionCache } : { tmuxAvailable: true };
10858
+ }
10859
+ let tmuxVersionCache;
10801
10860
  let tmuxEnvFlagCache = null;
10802
10861
  function tmuxSupportsEnvFlag() {
10803
10862
  if (tmuxEnvFlagCache !== null) return tmuxEnvFlagCache;
@@ -10882,6 +10941,9 @@ class TerminalSession {
10882
10941
  id;
10883
10942
  tmuxSession;
10884
10943
  transport;
10944
+ /** B-486: set only for a direct-pty (no tmux) session — the list row's
10945
+ * source, since tmux knows nothing about it. */
10946
+ direct;
10885
10947
  /** B-273: the user tmux session this terminal was created to attach —
10886
10948
  * echoed to any later open of the same live session. */
10887
10949
  attachedTmux;
@@ -11345,7 +11407,7 @@ class WebTerminalManager {
11345
11407
  * other means (the connect-time daemonState write carries the initial
11346
11408
  * snapshot), so the first tick doesn't re-push an identical list. */
11347
11409
  primeListSignature(list) {
11348
- this.lastListSignature = terminalListSignature(list);
11410
+ this.lastListSignature = hostListSignature(list);
11349
11411
  this.noteSeen(list);
11350
11412
  }
11351
11413
  /** The retained closed-terminal records (newest first) — shipped by
@@ -11386,6 +11448,7 @@ class WebTerminalManager {
11386
11448
  */
11387
11449
  buildTerminalList() {
11388
11450
  const list = this.listSessions();
11451
+ list.push(...directTerminalItems(this.terminals.values(), new Set(list.map((t) => t.id))));
11389
11452
  for (const item of list) {
11390
11453
  const live = this.terminals.get(item.id);
11391
11454
  if (live?.lastOutputAt) {
@@ -11423,7 +11486,7 @@ class WebTerminalManager {
11423
11486
  list.filter((t) => t.activityAt).map((t) => [t.id, t.activityAt])
11424
11487
  ));
11425
11488
  this.mirrorTickObserver?.(list);
11426
- const sig = terminalListSignature(list);
11489
+ const sig = hostListSignature(list);
11427
11490
  if (sig === this.lastListSignature) return;
11428
11491
  this.lastListSignature = sig;
11429
11492
  cb(list);
@@ -11771,6 +11834,7 @@ class WebTerminalManager {
11771
11834
  const now = Date.now();
11772
11835
  this.snapshots.sweep(now);
11773
11836
  for (const [id, session] of [...this.terminals]) {
11837
+ if (session.direct) continue;
11774
11838
  if (session.subscribers === 0 && now - session.lastTouch > SESSION_IDLE_MS) {
11775
11839
  persistence.logger.debug(`[WEB TERMINAL] reaping orphaned idle session ${id} (idle ${Math.round((now - session.lastTouch) / 6e4)}m)`);
11776
11840
  this.detach(id);
@@ -12047,6 +12111,10 @@ class WebTerminalManager {
12047
12111
  }
12048
12112
  }
12049
12113
  const created = new TerminalSession(id, { kind: "pty", pty: proc }, void 0, cols, rows);
12114
+ created.direct = { cwd, createdAt: Date.now() };
12115
+ created.onTitleChange((title) => {
12116
+ if (created.direct && !created.direct.manual) created.direct.title = deriveAutoTitle(title, os$1.hostname());
12117
+ });
12050
12118
  created.onOutputChunk = (chunk) => {
12051
12119
  this.emit("terminal-output", { terminalId: id, data: chunk.data, seq: chunk.seq });
12052
12120
  this.noteActivity();
@@ -12399,6 +12467,26 @@ class WebTerminalManager {
12399
12467
  * session (so a local `tmux attach` won't find it either). Used when the
12400
12468
  * user deletes the terminal from the sidebar. */
12401
12469
  killSession(terminalId, opts) {
12470
+ const liveDirect = this.terminals.get(terminalId);
12471
+ if (liveDirect?.direct) {
12472
+ this.recordClosed({
12473
+ id: terminalId,
12474
+ ...liveDirect.direct.title?.trim() ? { title: liveDirect.direct.title.trim() } : {},
12475
+ cwd: liveDirect.direct.cwd,
12476
+ tags: liveDirect.direct.tags ?? [],
12477
+ ...liveDirect.direct.manual ? { manual: true } : {},
12478
+ reason: "closed",
12479
+ closedAt: Date.now()
12480
+ });
12481
+ this.lastSeenInfo.delete(terminalId);
12482
+ this.detach(terminalId);
12483
+ this.tombstones[terminalId] = Date.now();
12484
+ saveTombstones(this.tombstones);
12485
+ this.notifyTracker.remove(terminalId);
12486
+ persistence.logger.debug(`[WEB TERMINAL] killed direct pty ${terminalId}`);
12487
+ this.kickListRefresh();
12488
+ return true;
12489
+ }
12402
12490
  const info = this.lastSeenInfo.get(terminalId) ?? (() => {
12403
12491
  const t = this.listSessions().find((x) => x.id === terminalId);
12404
12492
  return t ? seenInfoOf(t) : void 0;
@@ -12683,6 +12771,14 @@ class WebTerminalManager {
12683
12771
  * landed. `ifAbsent` finding an existing title counts as success (there IS
12684
12772
  * a title; nothing to retry). */
12685
12773
  setTitle(terminalId, title, ifAbsent = false) {
12774
+ const direct = this.terminals.get(terminalId)?.direct;
12775
+ if (direct) {
12776
+ if (ifAbsent && (direct.manual || direct.title?.trim())) return true;
12777
+ direct.title = title;
12778
+ if (!ifAbsent) direct.manual = true;
12779
+ this.kickListRefresh();
12780
+ return true;
12781
+ }
12686
12782
  if (!isTmuxAvailable()) return false;
12687
12783
  if (!/^[a-zA-Z0-9_-]{1,64}$/.test(terminalId)) return false;
12688
12784
  const name = `vh-${terminalId}`;
@@ -12707,10 +12803,16 @@ class WebTerminalManager {
12707
12803
  }
12708
12804
  /** Persist terminal tags in tmux, parallel to @vh_title. */
12709
12805
  setTags(terminalId, tags) {
12710
- if (!isTmuxAvailable()) return false;
12806
+ const direct = this.terminals.get(terminalId)?.direct;
12807
+ if (!direct && !isTmuxAvailable()) return false;
12711
12808
  if (!/^[a-zA-Z0-9_-]{1,64}$/.test(terminalId)) return false;
12712
12809
  const valid = validateTerminalTags(tags);
12713
12810
  if (!valid) return false;
12811
+ if (direct) {
12812
+ direct.tags = valid;
12813
+ this.kickListRefresh();
12814
+ return true;
12815
+ }
12714
12816
  const name = `vh-${terminalId}`;
12715
12817
  try {
12716
12818
  const r = node_child_process.spawnSync("tmux", tmuxArgs(["set-option", "-t", name, "@vh_tags", JSON.stringify(valid)]), {
@@ -15798,6 +15900,9 @@ class ApiMachineClient {
15798
15900
  // even in the (unreachable) case of a null prior state.
15799
15901
  status: state?.status ?? "running",
15800
15902
  webTerminals: { updatedAt: Date.now(), terminals: list },
15903
+ // B-486: tmux can appear (user installs it) mid-run; the
15904
+ // tracker forces a push on that flip, so restamp here too.
15905
+ terminalHost: { ...tmuxRuntimeInfo(), detectedAt: Date.now() },
15801
15906
  // B-084: the closed-terminal records ride every list push
15802
15907
  // (a close always changes the list, so they stay in step).
15803
15908
  // Read at write time — same freshness rule as the list.
@@ -15947,6 +16052,8 @@ class ApiMachineClient {
15947
16052
  terminalRestore: { rpcAvailable: true, detectedAt: now },
15948
16053
  // B-273 capability flag (same restamp discipline).
15949
16054
  tmuxSessions: { rpcAvailable: true, detectedAt: now, killAttached: true },
16055
+ // B-486: tmux presence (same restamp discipline).
16056
+ terminalHost: { ...tmuxRuntimeInfo(), detectedAt: now },
15950
16057
  // B-290 capability flag: `claude-list-history` (same restamp discipline).
15951
16058
  claudeHistory: { rpcAvailable: true, detectedAt: now },
15952
16059
  // B-464 capability flag: `codex-list-history` / `codex-import-session`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "very-happy-cli",
3
- "version": "0.2.147",
3
+ "version": "0.2.149",
4
4
  "description": "Remote multi-agent workspace CLI with deep Claude integration, native terminal streaming, and self-hosting.",
5
5
  "author": "Kirill Dubovitskiy",
6
6
  "contributors": [
@@ -83,7 +83,7 @@
83
83
  },
84
84
  "dependencies": {
85
85
  "@agentclientprotocol/sdk": "^0.14.1",
86
- "@anthropic-ai/claude-agent-sdk": "0.3.267",
86
+ "@anthropic-ai/claude-agent-sdk": "0.3.281",
87
87
  "@anthropic-ai/sandbox-runtime": "^0.0.37",
88
88
  "@modelcontextprotocol/sdk": "1.30.0",
89
89
  "@noble/ed25519": "^3.0.0",
@@ -117,12 +117,12 @@
117
117
  "zod": "^4.0.0"
118
118
  },
119
119
  "optionalDependencies": {
120
- "very-happy-tools-arm64-darwin": "0.2.147",
121
- "very-happy-tools-x64-darwin": "0.2.147",
122
- "very-happy-tools-arm64-linux": "0.2.147",
123
- "very-happy-tools-x64-linux": "0.2.147",
124
- "very-happy-tools-arm64-win32": "0.2.147",
125
- "very-happy-tools-x64-win32": "0.2.147"
120
+ "very-happy-tools-arm64-darwin": "0.2.149",
121
+ "very-happy-tools-x64-darwin": "0.2.149",
122
+ "very-happy-tools-arm64-linux": "0.2.149",
123
+ "very-happy-tools-x64-linux": "0.2.149",
124
+ "very-happy-tools-arm64-win32": "0.2.149",
125
+ "very-happy-tools-x64-win32": "0.2.149"
126
126
  },
127
127
  "devDependencies": {
128
128
  "@slopus/happy-wire": "workspace:*",