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.
- package/dist/{AcpBackend-CtoDlI4b.mjs → AcpBackend-Baqb8dNm.mjs} +2 -2
- package/dist/{AcpBackend-DYAbnvv0.cjs → AcpBackend-CnZpEfGd.cjs} +2 -2
- package/dist/{AcpSessionManager-BHnqkhBf.mjs → AcpSessionManager-CC6mdFXs.mjs} +1 -1
- package/dist/{AcpSessionManager-Cn2214Af.cjs → AcpSessionManager-e2vxmIbj.cjs} +1 -1
- package/dist/{agentGuidance-FDBKJgGC.cjs → agentGuidance--QIi_rZ2.cjs} +1 -1
- package/dist/{agentGuidance-CK792QWi.mjs → agentGuidance-oMVJvUl7.mjs} +1 -1
- package/dist/{agentHome-z_h7cVnq.cjs → agentHome-3kRCtzOL.cjs} +2 -2
- package/dist/{agentHome-Cj4LYQV-.mjs → agentHome-CKSPsS8d.mjs} +2 -2
- package/dist/codex/happyMcpStdioBridge.cjs +2 -2
- package/dist/codex/happyMcpStdioBridge.mjs +2 -2
- package/dist/{config-CVvEoSGv.mjs → config-C8jpk6IA.mjs} +4 -4
- package/dist/{config-C1ysAgX3.cjs → config-CzJZcvTO.cjs} +4 -4
- package/dist/{index-DUw34UHh.mjs → index-BPCXUrEe.mjs} +4 -4
- package/dist/{index-CXc3BQNG.cjs → index-CyfXsCXl.cjs} +76 -53
- package/dist/{index-C1QWpPoB.cjs → index-DBN2Zpqc.cjs} +4 -4
- package/dist/{index-B7UauKjp.mjs → index-Dfkyar7S.mjs} +72 -49
- package/dist/{index-DhcgPLDK.cjs → index-DgK4KX-Z.cjs} +7 -7
- package/dist/{index-EH6Pjsic.mjs → index-DrVq0CqK.mjs} +8 -8
- package/dist/index.cjs +4 -4
- package/dist/index.mjs +4 -4
- package/dist/{installTerminalHooks-AUERBJd4.mjs → installTerminalHooks-COqkLz6f.mjs} +4 -4
- package/dist/{installTerminalHooks-BMpt_X5L.cjs → installTerminalHooks-kdAoTfNT.cjs} +4 -4
- package/dist/lib.cjs +2 -2
- package/dist/lib.d.cts +10 -0
- package/dist/lib.d.mts +10 -0
- package/dist/lib.mjs +2 -2
- package/dist/{limits-Dh0u29C4.mjs → limits-BXI1Gzvd.mjs} +8 -8
- package/dist/{limits-WZLbGgEg.cjs → limits-FZWhlsBu.cjs} +8 -8
- package/dist/{mcp-3lcIERfY.mjs → mcp-Dfnoirpi.mjs} +4 -4
- package/dist/{mcp-D-Crar0K.cjs → mcp-DoLZbbZp.cjs} +4 -4
- package/dist/{piRuntime-dQ6fN-W9.mjs → piRuntime-BK6YZm2X.mjs} +2 -2
- package/dist/{piRuntime-Dkk0SGGN.cjs → piRuntime-DmE4ysfG.cjs} +2 -2
- package/dist/{piTerminal-YoY7Z7sP.mjs → piTerminal-Bczubq2n.mjs} +6 -6
- package/dist/{piTerminal-C1gXyJA1.cjs → piTerminal-qQAzQwIU.cjs} +6 -6
- package/dist/{runGemini-D2Hr4-Xz.mjs → runGemini-Cce_pjTs.mjs} +6 -6
- package/dist/{runGemini-Cj284Qhk.cjs → runGemini-ComTu0EB.cjs} +6 -6
- package/dist/{runOpenClaw-0tleKtRj.mjs → runOpenClaw-DXP5_chb.mjs} +5 -5
- package/dist/{runOpenClaw-USGbHPm-.cjs → runOpenClaw-M3YlDt8z.cjs} +5 -5
- package/dist/{send-Bt9iWhWx.cjs → send-0LrSwGgr.cjs} +4 -4
- package/dist/{send-vmjOSMYO.mjs → send-DDD57Cr_.mjs} +4 -4
- package/dist/{sessions-lzuQguqk.cjs → sessions-2Bkau9T4.cjs} +4 -4
- package/dist/{sessions-BeqWzIdm.mjs → sessions-ClBPEtz2.mjs} +4 -4
- package/dist/{spawn-13v957Zv.mjs → spawn-7lLymdT1.mjs} +4 -4
- package/dist/{spawn-B9kfeZZL.cjs → spawn-Cm3Da8Ge.cjs} +4 -4
- package/dist/{teams-DQ1pAIhZ.cjs → teams-DKKeIG-u.cjs} +4 -4
- package/dist/{teams-BkFamplo.mjs → teams-DyBjdXI3.mjs} +4 -4
- package/dist/{todo-Ba0Wpu2G.cjs → todo-8RgCTDxH.cjs} +3 -3
- package/dist/{todo-CxQEUY9J.mjs → todo-jXHGGH89.mjs} +3 -3
- package/dist/{types-DFYgWw3-.mjs → types-ipzMqciq.mjs} +116 -9
- package/dist/{types-CGACQD3M.cjs → types-t1gxALVE.cjs} +116 -9
- 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-
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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-
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
121
|
-
"very-happy-tools-x64-darwin": "0.2.
|
|
122
|
-
"very-happy-tools-arm64-linux": "0.2.
|
|
123
|
-
"very-happy-tools-x64-linux": "0.2.
|
|
124
|
-
"very-happy-tools-arm64-win32": "0.2.
|
|
125
|
-
"very-happy-tools-x64-win32": "0.2.
|
|
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:*",
|