very-happy-cli 0.2.146 → 0.2.148
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-D4J-b7TB.cjs → AcpBackend-BrM-mcjR.cjs} +2 -2
- package/dist/{AcpBackend--1XmtY13.mjs → AcpBackend-CrcdMnL6.mjs} +2 -2
- package/dist/{AcpSessionManager-BUh-XYb7.cjs → AcpSessionManager-CKhv-rBo.cjs} +1 -1
- package/dist/{AcpSessionManager-BXHAABP5.mjs → AcpSessionManager-DIafxIVU.mjs} +1 -1
- package/dist/{agentGuidance-BL1SZ7I3.mjs → agentGuidance-CR7rOQUF.mjs} +1 -1
- package/dist/{agentGuidance-BI4l2HRo.cjs → agentGuidance-gzl2qRkG.cjs} +1 -1
- package/dist/{agentHome-BdFP00V7.mjs → agentHome-CQrd95jc.mjs} +2 -2
- package/dist/{agentHome-Ck-uqSJ2.cjs → agentHome-Qh6SWyHx.cjs} +2 -2
- package/dist/codex/happyMcpStdioBridge.cjs +2 -2
- package/dist/codex/happyMcpStdioBridge.mjs +2 -2
- package/dist/{config-CpY7JwBj.cjs → config-CFSD65_X.cjs} +4 -4
- package/dist/{config-1MpGxQ9y.mjs → config-DkCS4663.mjs} +4 -4
- package/dist/{index-BRygi44L.cjs → index-C1OeYxxj.cjs} +85 -35
- package/dist/{index-k5Q1BYVe.mjs → index-C4JoaSCO.mjs} +8 -8
- package/dist/{index-BEpwCxqf.cjs → index-C_hZVtR-.cjs} +4 -4
- package/dist/{index-GZ--m9D1.mjs → index-Cobfu9V_.mjs} +81 -31
- package/dist/{index-BnkxOdPZ.mjs → index-eLVXOce3.mjs} +4 -4
- package/dist/{index-BDts8AaI.cjs → index-x5-N7ElS.cjs} +7 -7
- package/dist/index.cjs +4 -4
- package/dist/index.mjs +4 -4
- package/dist/{installTerminalHooks-CcDZxqew.cjs → installTerminalHooks-CuqhiAo9.cjs} +4 -4
- package/dist/{installTerminalHooks-BYMuFagt.mjs → installTerminalHooks-D9pIHXxU.mjs} +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-DNPVbu0o.mjs → limits-BfYfA7tt.mjs} +7 -7
- package/dist/{limits-Cs9ZqLsR.cjs → limits-D364Cpe3.cjs} +7 -7
- package/dist/{mcp-B0SoNWuQ.cjs → mcp-B5ojE8_m.cjs} +4 -4
- package/dist/{mcp-BtuzLwSL.mjs → mcp-DTXsdDwc.mjs} +4 -4
- package/dist/{piRuntime-RzukAlha.mjs → piRuntime-BWEgqBHm.mjs} +2 -2
- package/dist/{piRuntime-C52DKuoS.cjs → piRuntime-CbSaG74b.cjs} +2 -2
- package/dist/{piTerminal-BjATNNi_.cjs → piTerminal-Bzremid-.cjs} +6 -6
- package/dist/{piTerminal-BhHNN6o7.mjs → piTerminal-CnS3TF7M.mjs} +6 -6
- package/dist/{runGemini-H0nzGgAr.mjs → runGemini-C13-aRss.mjs} +6 -6
- package/dist/{runGemini-CSbocY0x.cjs → runGemini-Ci6ULPA7.cjs} +6 -6
- package/dist/{runOpenClaw-DTf27x3y.cjs → runOpenClaw-D2DW9Umc.cjs} +5 -5
- package/dist/{runOpenClaw-DgLSw9ye.mjs → runOpenClaw-D4ABejEy.mjs} +5 -5
- package/dist/{send-D_gFbnUb.cjs → send-1hJXKzeP.cjs} +4 -4
- package/dist/{send-B1xGe7BR.mjs → send-D-9_jOIS.mjs} +4 -4
- package/dist/{sessions-C_5xh0tc.mjs → sessions-C9VwpMKY.mjs} +4 -4
- package/dist/{sessions-DTef64KU.cjs → sessions-DOrymTYB.cjs} +4 -4
- package/dist/{spawn-CaClsWIZ.cjs → spawn-B1IlltVe.cjs} +4 -4
- package/dist/{spawn-jDF92zCE.mjs → spawn-C4uID9JX.mjs} +4 -4
- package/dist/{teams-BL-EFcfu.mjs → teams-CCv46ebr.mjs} +4 -4
- package/dist/{teams-CA6BHcPk.cjs → teams-xIRGgxwJ.cjs} +4 -4
- package/dist/{todo-DxNoHaPw.mjs → todo-5QemH15n.mjs} +3 -3
- package/dist/{todo-B-CAD9Y3.cjs → todo-DrsOEfYU.cjs} +3 -3
- package/dist/{types-DjB5fcAr.mjs → types-CXF6tULJ.mjs} +113 -7
- package/dist/{types-CgOMSh1y.cjs → types-IU5nGuaQ.cjs} +113 -7
- package/package.json +7 -7
|
@@ -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-BfYfA7tt.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
|
/**
|
|
@@ -10479,6 +10492,25 @@ function sanitizeClosedTerminals(raw, max = CLOSED_TERMINALS_MAX) {
|
|
|
10479
10492
|
return out.slice(0, max);
|
|
10480
10493
|
}
|
|
10481
10494
|
|
|
10495
|
+
function directTerminalItems(sessions, tmuxIds) {
|
|
10496
|
+
const out = [];
|
|
10497
|
+
for (const s of sessions) {
|
|
10498
|
+
if (!s.direct || tmuxIds.has(s.id)) continue;
|
|
10499
|
+
const title = (s.direct.title ?? "").trim();
|
|
10500
|
+
out.push({
|
|
10501
|
+
id: s.id,
|
|
10502
|
+
...title ? { title } : {},
|
|
10503
|
+
tags: s.direct.tags ?? [],
|
|
10504
|
+
...s.direct.manual ? { manual: true } : {},
|
|
10505
|
+
cwd: s.direct.cwd,
|
|
10506
|
+
createdAt: s.direct.createdAt,
|
|
10507
|
+
...s.lastOutputAt ? { activityAt: s.lastOutputAt } : {},
|
|
10508
|
+
direct: true
|
|
10509
|
+
});
|
|
10510
|
+
}
|
|
10511
|
+
return out.sort((a, b) => a.createdAt - b.createdAt || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
10512
|
+
}
|
|
10513
|
+
|
|
10482
10514
|
const TOMBSTONE_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
10483
10515
|
function tmuxKillVerified(kill, probe) {
|
|
10484
10516
|
return !kill.error && !probe.error && typeof probe.status === "number" && probe.status !== 0;
|
|
@@ -10615,10 +10647,14 @@ function terminalListSignature(items) {
|
|
|
10615
10647
|
// B-105: a mirror binding appearing/disappearing MUST push the
|
|
10616
10648
|
// list, or the web never learns the toggle became available.
|
|
10617
10649
|
t.mirrorSessionId ?? "",
|
|
10618
|
-
t.attachTmux ?? ""
|
|
10650
|
+
t.attachTmux ?? "",
|
|
10651
|
+
t.direct ? 1 : 0
|
|
10619
10652
|
]);
|
|
10620
10653
|
return JSON.stringify(canon);
|
|
10621
10654
|
}
|
|
10655
|
+
function hostListSignature(items) {
|
|
10656
|
+
return `${isTmuxAvailable() ? "tmux" : "no-tmux"}|${terminalListSignature(items)}`;
|
|
10657
|
+
}
|
|
10622
10658
|
const SHELL_COMMANDS = /* @__PURE__ */ new Set(["zsh", "bash", "fish", "sh", "dash", "ksh", "tcsh", "csh"]);
|
|
10623
10659
|
const TITLE_MAX_CHARS = 60;
|
|
10624
10660
|
const JUNK_TITLES = /* @__PURE__ */ new Set(["tmux", "claude", "node", ...SHELL_COMMANDS]);
|
|
@@ -10767,16 +10803,38 @@ function tmuxSupportsNewSessionEnv(versionOutput) {
|
|
|
10767
10803
|
return maj > 3 || maj === 3 && min >= 2;
|
|
10768
10804
|
}
|
|
10769
10805
|
let tmuxAvailableCache = null;
|
|
10806
|
+
let tmuxMissingSince = 0;
|
|
10807
|
+
const TMUX_MISSING_REPROBE_MS = 3e4;
|
|
10770
10808
|
function isTmuxAvailable() {
|
|
10771
|
-
if (tmuxAvailableCache
|
|
10809
|
+
if (tmuxAvailableCache === true) return true;
|
|
10810
|
+
if (tmuxAvailableCache === false && Date.now() - tmuxMissingSince < TMUX_MISSING_REPROBE_MS) return false;
|
|
10772
10811
|
try {
|
|
10773
10812
|
const r = spawnSync("tmux", tmuxArgs(["-V"]), { stdio: "ignore", env: ptyEnv() });
|
|
10774
10813
|
tmuxAvailableCache = r.status === 0;
|
|
10775
10814
|
} catch {
|
|
10776
10815
|
tmuxAvailableCache = false;
|
|
10777
10816
|
}
|
|
10817
|
+
if (tmuxAvailableCache) {
|
|
10818
|
+
tmuxEnvFlagCache = null;
|
|
10819
|
+
tmuxVersionCache = void 0;
|
|
10820
|
+
} else {
|
|
10821
|
+
tmuxMissingSince = Date.now();
|
|
10822
|
+
}
|
|
10778
10823
|
return tmuxAvailableCache;
|
|
10779
10824
|
}
|
|
10825
|
+
function tmuxRuntimeInfo() {
|
|
10826
|
+
if (!isTmuxAvailable()) return { tmuxAvailable: false };
|
|
10827
|
+
if (tmuxVersionCache === void 0) {
|
|
10828
|
+
try {
|
|
10829
|
+
const r = spawnSync("tmux", tmuxArgs(["-V"]), { encoding: "utf8", timeout: TMUX_PROBE_TIMEOUT_MS, env: ptyEnv() });
|
|
10830
|
+
tmuxVersionCache = (r.stdout || "").trim().slice(0, 64);
|
|
10831
|
+
} catch {
|
|
10832
|
+
tmuxVersionCache = "";
|
|
10833
|
+
}
|
|
10834
|
+
}
|
|
10835
|
+
return tmuxVersionCache ? { tmuxAvailable: true, tmuxVersion: tmuxVersionCache } : { tmuxAvailable: true };
|
|
10836
|
+
}
|
|
10837
|
+
let tmuxVersionCache;
|
|
10780
10838
|
let tmuxEnvFlagCache = null;
|
|
10781
10839
|
function tmuxSupportsEnvFlag() {
|
|
10782
10840
|
if (tmuxEnvFlagCache !== null) return tmuxEnvFlagCache;
|
|
@@ -10861,6 +10919,9 @@ class TerminalSession {
|
|
|
10861
10919
|
id;
|
|
10862
10920
|
tmuxSession;
|
|
10863
10921
|
transport;
|
|
10922
|
+
/** B-486: set only for a direct-pty (no tmux) session — the list row's
|
|
10923
|
+
* source, since tmux knows nothing about it. */
|
|
10924
|
+
direct;
|
|
10864
10925
|
/** B-273: the user tmux session this terminal was created to attach —
|
|
10865
10926
|
* echoed to any later open of the same live session. */
|
|
10866
10927
|
attachedTmux;
|
|
@@ -11324,7 +11385,7 @@ class WebTerminalManager {
|
|
|
11324
11385
|
* other means (the connect-time daemonState write carries the initial
|
|
11325
11386
|
* snapshot), so the first tick doesn't re-push an identical list. */
|
|
11326
11387
|
primeListSignature(list) {
|
|
11327
|
-
this.lastListSignature =
|
|
11388
|
+
this.lastListSignature = hostListSignature(list);
|
|
11328
11389
|
this.noteSeen(list);
|
|
11329
11390
|
}
|
|
11330
11391
|
/** The retained closed-terminal records (newest first) — shipped by
|
|
@@ -11365,6 +11426,7 @@ class WebTerminalManager {
|
|
|
11365
11426
|
*/
|
|
11366
11427
|
buildTerminalList() {
|
|
11367
11428
|
const list = this.listSessions();
|
|
11429
|
+
list.push(...directTerminalItems(this.terminals.values(), new Set(list.map((t) => t.id))));
|
|
11368
11430
|
for (const item of list) {
|
|
11369
11431
|
const live = this.terminals.get(item.id);
|
|
11370
11432
|
if (live?.lastOutputAt) {
|
|
@@ -11402,7 +11464,7 @@ class WebTerminalManager {
|
|
|
11402
11464
|
list.filter((t) => t.activityAt).map((t) => [t.id, t.activityAt])
|
|
11403
11465
|
));
|
|
11404
11466
|
this.mirrorTickObserver?.(list);
|
|
11405
|
-
const sig =
|
|
11467
|
+
const sig = hostListSignature(list);
|
|
11406
11468
|
if (sig === this.lastListSignature) return;
|
|
11407
11469
|
this.lastListSignature = sig;
|
|
11408
11470
|
cb(list);
|
|
@@ -11750,6 +11812,7 @@ class WebTerminalManager {
|
|
|
11750
11812
|
const now = Date.now();
|
|
11751
11813
|
this.snapshots.sweep(now);
|
|
11752
11814
|
for (const [id, session] of [...this.terminals]) {
|
|
11815
|
+
if (session.direct) continue;
|
|
11753
11816
|
if (session.subscribers === 0 && now - session.lastTouch > SESSION_IDLE_MS) {
|
|
11754
11817
|
logger.debug(`[WEB TERMINAL] reaping orphaned idle session ${id} (idle ${Math.round((now - session.lastTouch) / 6e4)}m)`);
|
|
11755
11818
|
this.detach(id);
|
|
@@ -12026,6 +12089,10 @@ class WebTerminalManager {
|
|
|
12026
12089
|
}
|
|
12027
12090
|
}
|
|
12028
12091
|
const created = new TerminalSession(id, { kind: "pty", pty: proc }, void 0, cols, rows);
|
|
12092
|
+
created.direct = { cwd, createdAt: Date.now() };
|
|
12093
|
+
created.onTitleChange((title) => {
|
|
12094
|
+
if (created.direct && !created.direct.manual) created.direct.title = deriveAutoTitle(title, os$1.hostname());
|
|
12095
|
+
});
|
|
12029
12096
|
created.onOutputChunk = (chunk) => {
|
|
12030
12097
|
this.emit("terminal-output", { terminalId: id, data: chunk.data, seq: chunk.seq });
|
|
12031
12098
|
this.noteActivity();
|
|
@@ -12378,6 +12445,26 @@ class WebTerminalManager {
|
|
|
12378
12445
|
* session (so a local `tmux attach` won't find it either). Used when the
|
|
12379
12446
|
* user deletes the terminal from the sidebar. */
|
|
12380
12447
|
killSession(terminalId, opts) {
|
|
12448
|
+
const liveDirect = this.terminals.get(terminalId);
|
|
12449
|
+
if (liveDirect?.direct) {
|
|
12450
|
+
this.recordClosed({
|
|
12451
|
+
id: terminalId,
|
|
12452
|
+
...liveDirect.direct.title?.trim() ? { title: liveDirect.direct.title.trim() } : {},
|
|
12453
|
+
cwd: liveDirect.direct.cwd,
|
|
12454
|
+
tags: liveDirect.direct.tags ?? [],
|
|
12455
|
+
...liveDirect.direct.manual ? { manual: true } : {},
|
|
12456
|
+
reason: "closed",
|
|
12457
|
+
closedAt: Date.now()
|
|
12458
|
+
});
|
|
12459
|
+
this.lastSeenInfo.delete(terminalId);
|
|
12460
|
+
this.detach(terminalId);
|
|
12461
|
+
this.tombstones[terminalId] = Date.now();
|
|
12462
|
+
saveTombstones(this.tombstones);
|
|
12463
|
+
this.notifyTracker.remove(terminalId);
|
|
12464
|
+
logger.debug(`[WEB TERMINAL] killed direct pty ${terminalId}`);
|
|
12465
|
+
this.kickListRefresh();
|
|
12466
|
+
return true;
|
|
12467
|
+
}
|
|
12381
12468
|
const info = this.lastSeenInfo.get(terminalId) ?? (() => {
|
|
12382
12469
|
const t = this.listSessions().find((x) => x.id === terminalId);
|
|
12383
12470
|
return t ? seenInfoOf(t) : void 0;
|
|
@@ -12662,6 +12749,14 @@ class WebTerminalManager {
|
|
|
12662
12749
|
* landed. `ifAbsent` finding an existing title counts as success (there IS
|
|
12663
12750
|
* a title; nothing to retry). */
|
|
12664
12751
|
setTitle(terminalId, title, ifAbsent = false) {
|
|
12752
|
+
const direct = this.terminals.get(terminalId)?.direct;
|
|
12753
|
+
if (direct) {
|
|
12754
|
+
if (ifAbsent && (direct.manual || direct.title?.trim())) return true;
|
|
12755
|
+
direct.title = title;
|
|
12756
|
+
if (!ifAbsent) direct.manual = true;
|
|
12757
|
+
this.kickListRefresh();
|
|
12758
|
+
return true;
|
|
12759
|
+
}
|
|
12665
12760
|
if (!isTmuxAvailable()) return false;
|
|
12666
12761
|
if (!/^[a-zA-Z0-9_-]{1,64}$/.test(terminalId)) return false;
|
|
12667
12762
|
const name = `vh-${terminalId}`;
|
|
@@ -12686,10 +12781,16 @@ class WebTerminalManager {
|
|
|
12686
12781
|
}
|
|
12687
12782
|
/** Persist terminal tags in tmux, parallel to @vh_title. */
|
|
12688
12783
|
setTags(terminalId, tags) {
|
|
12689
|
-
|
|
12784
|
+
const direct = this.terminals.get(terminalId)?.direct;
|
|
12785
|
+
if (!direct && !isTmuxAvailable()) return false;
|
|
12690
12786
|
if (!/^[a-zA-Z0-9_-]{1,64}$/.test(terminalId)) return false;
|
|
12691
12787
|
const valid = validateTerminalTags(tags);
|
|
12692
12788
|
if (!valid) return false;
|
|
12789
|
+
if (direct) {
|
|
12790
|
+
direct.tags = valid;
|
|
12791
|
+
this.kickListRefresh();
|
|
12792
|
+
return true;
|
|
12793
|
+
}
|
|
12693
12794
|
const name = `vh-${terminalId}`;
|
|
12694
12795
|
try {
|
|
12695
12796
|
const r = spawnSync("tmux", tmuxArgs(["set-option", "-t", name, "@vh_tags", JSON.stringify(valid)]), {
|
|
@@ -15777,6 +15878,9 @@ class ApiMachineClient {
|
|
|
15777
15878
|
// even in the (unreachable) case of a null prior state.
|
|
15778
15879
|
status: state?.status ?? "running",
|
|
15779
15880
|
webTerminals: { updatedAt: Date.now(), terminals: list },
|
|
15881
|
+
// B-486: tmux can appear (user installs it) mid-run; the
|
|
15882
|
+
// tracker forces a push on that flip, so restamp here too.
|
|
15883
|
+
terminalHost: { ...tmuxRuntimeInfo(), detectedAt: Date.now() },
|
|
15780
15884
|
// B-084: the closed-terminal records ride every list push
|
|
15781
15885
|
// (a close always changes the list, so they stay in step).
|
|
15782
15886
|
// Read at write time — same freshness rule as the list.
|
|
@@ -15926,6 +16030,8 @@ class ApiMachineClient {
|
|
|
15926
16030
|
terminalRestore: { rpcAvailable: true, detectedAt: now },
|
|
15927
16031
|
// B-273 capability flag (same restamp discipline).
|
|
15928
16032
|
tmuxSessions: { rpcAvailable: true, detectedAt: now, killAttached: true },
|
|
16033
|
+
// B-486: tmux presence (same restamp discipline).
|
|
16034
|
+
terminalHost: { ...tmuxRuntimeInfo(), detectedAt: now },
|
|
15929
16035
|
// B-290 capability flag: `claude-list-history` (same restamp discipline).
|
|
15930
16036
|
claudeHistory: { rpcAvailable: true, detectedAt: now },
|
|
15931
16037
|
// 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-D364Cpe3.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
|
/**
|
|
@@ -10500,6 +10513,25 @@ function sanitizeClosedTerminals(raw, max = CLOSED_TERMINALS_MAX) {
|
|
|
10500
10513
|
return out.slice(0, max);
|
|
10501
10514
|
}
|
|
10502
10515
|
|
|
10516
|
+
function directTerminalItems(sessions, tmuxIds) {
|
|
10517
|
+
const out = [];
|
|
10518
|
+
for (const s of sessions) {
|
|
10519
|
+
if (!s.direct || tmuxIds.has(s.id)) continue;
|
|
10520
|
+
const title = (s.direct.title ?? "").trim();
|
|
10521
|
+
out.push({
|
|
10522
|
+
id: s.id,
|
|
10523
|
+
...title ? { title } : {},
|
|
10524
|
+
tags: s.direct.tags ?? [],
|
|
10525
|
+
...s.direct.manual ? { manual: true } : {},
|
|
10526
|
+
cwd: s.direct.cwd,
|
|
10527
|
+
createdAt: s.direct.createdAt,
|
|
10528
|
+
...s.lastOutputAt ? { activityAt: s.lastOutputAt } : {},
|
|
10529
|
+
direct: true
|
|
10530
|
+
});
|
|
10531
|
+
}
|
|
10532
|
+
return out.sort((a, b) => a.createdAt - b.createdAt || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
10533
|
+
}
|
|
10534
|
+
|
|
10503
10535
|
const TOMBSTONE_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
10504
10536
|
function tmuxKillVerified(kill, probe) {
|
|
10505
10537
|
return !kill.error && !probe.error && typeof probe.status === "number" && probe.status !== 0;
|
|
@@ -10636,10 +10668,14 @@ function terminalListSignature(items) {
|
|
|
10636
10668
|
// B-105: a mirror binding appearing/disappearing MUST push the
|
|
10637
10669
|
// list, or the web never learns the toggle became available.
|
|
10638
10670
|
t.mirrorSessionId ?? "",
|
|
10639
|
-
t.attachTmux ?? ""
|
|
10671
|
+
t.attachTmux ?? "",
|
|
10672
|
+
t.direct ? 1 : 0
|
|
10640
10673
|
]);
|
|
10641
10674
|
return JSON.stringify(canon);
|
|
10642
10675
|
}
|
|
10676
|
+
function hostListSignature(items) {
|
|
10677
|
+
return `${isTmuxAvailable() ? "tmux" : "no-tmux"}|${terminalListSignature(items)}`;
|
|
10678
|
+
}
|
|
10643
10679
|
const SHELL_COMMANDS = /* @__PURE__ */ new Set(["zsh", "bash", "fish", "sh", "dash", "ksh", "tcsh", "csh"]);
|
|
10644
10680
|
const TITLE_MAX_CHARS = 60;
|
|
10645
10681
|
const JUNK_TITLES = /* @__PURE__ */ new Set(["tmux", "claude", "node", ...SHELL_COMMANDS]);
|
|
@@ -10788,16 +10824,38 @@ function tmuxSupportsNewSessionEnv(versionOutput) {
|
|
|
10788
10824
|
return maj > 3 || maj === 3 && min >= 2;
|
|
10789
10825
|
}
|
|
10790
10826
|
let tmuxAvailableCache = null;
|
|
10827
|
+
let tmuxMissingSince = 0;
|
|
10828
|
+
const TMUX_MISSING_REPROBE_MS = 3e4;
|
|
10791
10829
|
function isTmuxAvailable() {
|
|
10792
|
-
if (tmuxAvailableCache
|
|
10830
|
+
if (tmuxAvailableCache === true) return true;
|
|
10831
|
+
if (tmuxAvailableCache === false && Date.now() - tmuxMissingSince < TMUX_MISSING_REPROBE_MS) return false;
|
|
10793
10832
|
try {
|
|
10794
10833
|
const r = node_child_process.spawnSync("tmux", tmuxArgs(["-V"]), { stdio: "ignore", env: ptyEnv() });
|
|
10795
10834
|
tmuxAvailableCache = r.status === 0;
|
|
10796
10835
|
} catch {
|
|
10797
10836
|
tmuxAvailableCache = false;
|
|
10798
10837
|
}
|
|
10838
|
+
if (tmuxAvailableCache) {
|
|
10839
|
+
tmuxEnvFlagCache = null;
|
|
10840
|
+
tmuxVersionCache = void 0;
|
|
10841
|
+
} else {
|
|
10842
|
+
tmuxMissingSince = Date.now();
|
|
10843
|
+
}
|
|
10799
10844
|
return tmuxAvailableCache;
|
|
10800
10845
|
}
|
|
10846
|
+
function tmuxRuntimeInfo() {
|
|
10847
|
+
if (!isTmuxAvailable()) return { tmuxAvailable: false };
|
|
10848
|
+
if (tmuxVersionCache === void 0) {
|
|
10849
|
+
try {
|
|
10850
|
+
const r = node_child_process.spawnSync("tmux", tmuxArgs(["-V"]), { encoding: "utf8", timeout: TMUX_PROBE_TIMEOUT_MS, env: ptyEnv() });
|
|
10851
|
+
tmuxVersionCache = (r.stdout || "").trim().slice(0, 64);
|
|
10852
|
+
} catch {
|
|
10853
|
+
tmuxVersionCache = "";
|
|
10854
|
+
}
|
|
10855
|
+
}
|
|
10856
|
+
return tmuxVersionCache ? { tmuxAvailable: true, tmuxVersion: tmuxVersionCache } : { tmuxAvailable: true };
|
|
10857
|
+
}
|
|
10858
|
+
let tmuxVersionCache;
|
|
10801
10859
|
let tmuxEnvFlagCache = null;
|
|
10802
10860
|
function tmuxSupportsEnvFlag() {
|
|
10803
10861
|
if (tmuxEnvFlagCache !== null) return tmuxEnvFlagCache;
|
|
@@ -10882,6 +10940,9 @@ class TerminalSession {
|
|
|
10882
10940
|
id;
|
|
10883
10941
|
tmuxSession;
|
|
10884
10942
|
transport;
|
|
10943
|
+
/** B-486: set only for a direct-pty (no tmux) session — the list row's
|
|
10944
|
+
* source, since tmux knows nothing about it. */
|
|
10945
|
+
direct;
|
|
10885
10946
|
/** B-273: the user tmux session this terminal was created to attach —
|
|
10886
10947
|
* echoed to any later open of the same live session. */
|
|
10887
10948
|
attachedTmux;
|
|
@@ -11345,7 +11406,7 @@ class WebTerminalManager {
|
|
|
11345
11406
|
* other means (the connect-time daemonState write carries the initial
|
|
11346
11407
|
* snapshot), so the first tick doesn't re-push an identical list. */
|
|
11347
11408
|
primeListSignature(list) {
|
|
11348
|
-
this.lastListSignature =
|
|
11409
|
+
this.lastListSignature = hostListSignature(list);
|
|
11349
11410
|
this.noteSeen(list);
|
|
11350
11411
|
}
|
|
11351
11412
|
/** The retained closed-terminal records (newest first) — shipped by
|
|
@@ -11386,6 +11447,7 @@ class WebTerminalManager {
|
|
|
11386
11447
|
*/
|
|
11387
11448
|
buildTerminalList() {
|
|
11388
11449
|
const list = this.listSessions();
|
|
11450
|
+
list.push(...directTerminalItems(this.terminals.values(), new Set(list.map((t) => t.id))));
|
|
11389
11451
|
for (const item of list) {
|
|
11390
11452
|
const live = this.terminals.get(item.id);
|
|
11391
11453
|
if (live?.lastOutputAt) {
|
|
@@ -11423,7 +11485,7 @@ class WebTerminalManager {
|
|
|
11423
11485
|
list.filter((t) => t.activityAt).map((t) => [t.id, t.activityAt])
|
|
11424
11486
|
));
|
|
11425
11487
|
this.mirrorTickObserver?.(list);
|
|
11426
|
-
const sig =
|
|
11488
|
+
const sig = hostListSignature(list);
|
|
11427
11489
|
if (sig === this.lastListSignature) return;
|
|
11428
11490
|
this.lastListSignature = sig;
|
|
11429
11491
|
cb(list);
|
|
@@ -11771,6 +11833,7 @@ class WebTerminalManager {
|
|
|
11771
11833
|
const now = Date.now();
|
|
11772
11834
|
this.snapshots.sweep(now);
|
|
11773
11835
|
for (const [id, session] of [...this.terminals]) {
|
|
11836
|
+
if (session.direct) continue;
|
|
11774
11837
|
if (session.subscribers === 0 && now - session.lastTouch > SESSION_IDLE_MS) {
|
|
11775
11838
|
persistence.logger.debug(`[WEB TERMINAL] reaping orphaned idle session ${id} (idle ${Math.round((now - session.lastTouch) / 6e4)}m)`);
|
|
11776
11839
|
this.detach(id);
|
|
@@ -12047,6 +12110,10 @@ class WebTerminalManager {
|
|
|
12047
12110
|
}
|
|
12048
12111
|
}
|
|
12049
12112
|
const created = new TerminalSession(id, { kind: "pty", pty: proc }, void 0, cols, rows);
|
|
12113
|
+
created.direct = { cwd, createdAt: Date.now() };
|
|
12114
|
+
created.onTitleChange((title) => {
|
|
12115
|
+
if (created.direct && !created.direct.manual) created.direct.title = deriveAutoTitle(title, os$1.hostname());
|
|
12116
|
+
});
|
|
12050
12117
|
created.onOutputChunk = (chunk) => {
|
|
12051
12118
|
this.emit("terminal-output", { terminalId: id, data: chunk.data, seq: chunk.seq });
|
|
12052
12119
|
this.noteActivity();
|
|
@@ -12399,6 +12466,26 @@ class WebTerminalManager {
|
|
|
12399
12466
|
* session (so a local `tmux attach` won't find it either). Used when the
|
|
12400
12467
|
* user deletes the terminal from the sidebar. */
|
|
12401
12468
|
killSession(terminalId, opts) {
|
|
12469
|
+
const liveDirect = this.terminals.get(terminalId);
|
|
12470
|
+
if (liveDirect?.direct) {
|
|
12471
|
+
this.recordClosed({
|
|
12472
|
+
id: terminalId,
|
|
12473
|
+
...liveDirect.direct.title?.trim() ? { title: liveDirect.direct.title.trim() } : {},
|
|
12474
|
+
cwd: liveDirect.direct.cwd,
|
|
12475
|
+
tags: liveDirect.direct.tags ?? [],
|
|
12476
|
+
...liveDirect.direct.manual ? { manual: true } : {},
|
|
12477
|
+
reason: "closed",
|
|
12478
|
+
closedAt: Date.now()
|
|
12479
|
+
});
|
|
12480
|
+
this.lastSeenInfo.delete(terminalId);
|
|
12481
|
+
this.detach(terminalId);
|
|
12482
|
+
this.tombstones[terminalId] = Date.now();
|
|
12483
|
+
saveTombstones(this.tombstones);
|
|
12484
|
+
this.notifyTracker.remove(terminalId);
|
|
12485
|
+
persistence.logger.debug(`[WEB TERMINAL] killed direct pty ${terminalId}`);
|
|
12486
|
+
this.kickListRefresh();
|
|
12487
|
+
return true;
|
|
12488
|
+
}
|
|
12402
12489
|
const info = this.lastSeenInfo.get(terminalId) ?? (() => {
|
|
12403
12490
|
const t = this.listSessions().find((x) => x.id === terminalId);
|
|
12404
12491
|
return t ? seenInfoOf(t) : void 0;
|
|
@@ -12683,6 +12770,14 @@ class WebTerminalManager {
|
|
|
12683
12770
|
* landed. `ifAbsent` finding an existing title counts as success (there IS
|
|
12684
12771
|
* a title; nothing to retry). */
|
|
12685
12772
|
setTitle(terminalId, title, ifAbsent = false) {
|
|
12773
|
+
const direct = this.terminals.get(terminalId)?.direct;
|
|
12774
|
+
if (direct) {
|
|
12775
|
+
if (ifAbsent && (direct.manual || direct.title?.trim())) return true;
|
|
12776
|
+
direct.title = title;
|
|
12777
|
+
if (!ifAbsent) direct.manual = true;
|
|
12778
|
+
this.kickListRefresh();
|
|
12779
|
+
return true;
|
|
12780
|
+
}
|
|
12686
12781
|
if (!isTmuxAvailable()) return false;
|
|
12687
12782
|
if (!/^[a-zA-Z0-9_-]{1,64}$/.test(terminalId)) return false;
|
|
12688
12783
|
const name = `vh-${terminalId}`;
|
|
@@ -12707,10 +12802,16 @@ class WebTerminalManager {
|
|
|
12707
12802
|
}
|
|
12708
12803
|
/** Persist terminal tags in tmux, parallel to @vh_title. */
|
|
12709
12804
|
setTags(terminalId, tags) {
|
|
12710
|
-
|
|
12805
|
+
const direct = this.terminals.get(terminalId)?.direct;
|
|
12806
|
+
if (!direct && !isTmuxAvailable()) return false;
|
|
12711
12807
|
if (!/^[a-zA-Z0-9_-]{1,64}$/.test(terminalId)) return false;
|
|
12712
12808
|
const valid = validateTerminalTags(tags);
|
|
12713
12809
|
if (!valid) return false;
|
|
12810
|
+
if (direct) {
|
|
12811
|
+
direct.tags = valid;
|
|
12812
|
+
this.kickListRefresh();
|
|
12813
|
+
return true;
|
|
12814
|
+
}
|
|
12714
12815
|
const name = `vh-${terminalId}`;
|
|
12715
12816
|
try {
|
|
12716
12817
|
const r = node_child_process.spawnSync("tmux", tmuxArgs(["set-option", "-t", name, "@vh_tags", JSON.stringify(valid)]), {
|
|
@@ -15798,6 +15899,9 @@ class ApiMachineClient {
|
|
|
15798
15899
|
// even in the (unreachable) case of a null prior state.
|
|
15799
15900
|
status: state?.status ?? "running",
|
|
15800
15901
|
webTerminals: { updatedAt: Date.now(), terminals: list },
|
|
15902
|
+
// B-486: tmux can appear (user installs it) mid-run; the
|
|
15903
|
+
// tracker forces a push on that flip, so restamp here too.
|
|
15904
|
+
terminalHost: { ...tmuxRuntimeInfo(), detectedAt: Date.now() },
|
|
15801
15905
|
// B-084: the closed-terminal records ride every list push
|
|
15802
15906
|
// (a close always changes the list, so they stay in step).
|
|
15803
15907
|
// Read at write time — same freshness rule as the list.
|
|
@@ -15947,6 +16051,8 @@ class ApiMachineClient {
|
|
|
15947
16051
|
terminalRestore: { rpcAvailable: true, detectedAt: now },
|
|
15948
16052
|
// B-273 capability flag (same restamp discipline).
|
|
15949
16053
|
tmuxSessions: { rpcAvailable: true, detectedAt: now, killAttached: true },
|
|
16054
|
+
// B-486: tmux presence (same restamp discipline).
|
|
16055
|
+
terminalHost: { ...tmuxRuntimeInfo(), detectedAt: now },
|
|
15950
16056
|
// B-290 capability flag: `claude-list-history` (same restamp discipline).
|
|
15951
16057
|
claudeHistory: { rpcAvailable: true, detectedAt: now },
|
|
15952
16058
|
// 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.148",
|
|
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": [
|
|
@@ -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.148",
|
|
121
|
+
"very-happy-tools-x64-darwin": "0.2.148",
|
|
122
|
+
"very-happy-tools-arm64-linux": "0.2.148",
|
|
123
|
+
"very-happy-tools-x64-linux": "0.2.148",
|
|
124
|
+
"very-happy-tools-arm64-win32": "0.2.148",
|
|
125
|
+
"very-happy-tools-x64-win32": "0.2.148"
|
|
126
126
|
},
|
|
127
127
|
"devDependencies": {
|
|
128
128
|
"@slopus/happy-wire": "workspace:*",
|