very-happy-cli 0.2.114 → 0.2.116
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-BYv1pHW6.cjs → AcpBackend-CauAQpC1.cjs} +1 -1
- package/dist/{AcpBackend-4DC6CDpP.mjs → AcpBackend-CfKPvoTU.mjs} +1 -1
- package/dist/{AcpSessionManager-CvDQWnwm.mjs → AcpSessionManager-BQ6pBRW4.mjs} +1 -1
- package/dist/{AcpSessionManager-BBCEuz5G.cjs → AcpSessionManager-CA6xeET_.cjs} +1 -1
- package/dist/{config-B1W5KTvn.cjs → config-BoteFn-v.cjs} +2 -2
- package/dist/{config-BYEj8cnm.mjs → config-BpR_CwZu.mjs} +2 -2
- package/dist/{index-CQ9f70v9.cjs → index-4jiUeiUz.cjs} +187 -20
- package/dist/{index-B-PX710g.mjs → index-C45xN8s0.mjs} +5 -5
- package/dist/{index-DXJ4FLp5.mjs → index-D7azxfcs.mjs} +2 -2
- package/dist/{index-BtFBYvj8.cjs → index-DWmhmji9.cjs} +4 -4
- package/dist/{index-DJcR5PQb.mjs → index-Dq-eFp89.mjs} +188 -21
- package/dist/{index-CiNMkjGe.cjs → index-lX1UliOs.cjs} +2 -2
- package/dist/index.cjs +2 -2
- package/dist/index.mjs +2 -2
- package/dist/{installTerminalHooks-CvWutFrA.cjs → installTerminalHooks-DnaFkrTF.cjs} +2 -2
- package/dist/{installTerminalHooks-BKqlOoi4.mjs → installTerminalHooks-k3TrBHD0.mjs} +2 -2
- package/dist/lib.cjs +1 -1
- package/dist/lib.d.cts +33 -0
- package/dist/lib.d.mts +33 -0
- package/dist/lib.mjs +1 -1
- package/dist/{mcp-kyB44gVF.mjs → mcp-DRePTOap.mjs} +2 -2
- package/dist/{mcp-KhfiJ6OC.cjs → mcp-DSpEpM_2.cjs} +2 -2
- package/dist/{runGemini-DYN1hVth.cjs → runGemini-IWHyBq0A.cjs} +4 -4
- package/dist/{runGemini-Jn7L2hqR.mjs → runGemini-_9-a7duU.mjs} +4 -4
- package/dist/{runOpenClaw-DHqzKgke.cjs → runOpenClaw-0myYIz1U.cjs} +3 -3
- package/dist/{runOpenClaw-BZL1CSvh.mjs → runOpenClaw-Boq6LQ9a.mjs} +3 -3
- package/dist/{send-m-ZqzS6w.cjs → send-CwWvFfay.cjs} +2 -2
- package/dist/{send-DlNrAHnH.mjs → send-Cyo6mgMl.mjs} +2 -2
- package/dist/{sessions-Qbk_H0Av.cjs → sessions-CbzqYiAy.cjs} +2 -2
- package/dist/{sessions-CpM_qzf7.mjs → sessions-lvfTH_ql.mjs} +2 -2
- package/dist/{spawn-Do8fUG8J.mjs → spawn-CU0DMauA.mjs} +2 -2
- package/dist/{spawn-D09KFk0I.cjs → spawn-DQ6FTUK9.cjs} +2 -2
- package/dist/{types-D14vU6_q.cjs → types-C5kEpkcZ.cjs} +156 -15
- package/dist/{types-C6vCFbPv.mjs → types-Ds0SiQ1D.mjs} +155 -14
- package/package.json +7 -7
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { readFileSync } from 'node:fs';
|
|
3
3
|
import { resolve } from 'node:path';
|
|
4
|
-
import { r as readPersistedSessions, c as configuration } from './types-
|
|
5
|
-
import { d as sendUserMessage, a as sessionWebUrl } from './index-
|
|
4
|
+
import { r as readPersistedSessions, c as configuration } from './types-Ds0SiQ1D.mjs';
|
|
5
|
+
import { d as sendUserMessage, a as sessionWebUrl } from './index-Dq-eFp89.mjs';
|
|
6
6
|
import 'axios';
|
|
7
7
|
import 'node:util';
|
|
8
8
|
import 'node:os';
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var chalk = require('chalk');
|
|
4
|
-
var index = require('./index-
|
|
5
|
-
var persistence = require('./types-
|
|
4
|
+
var index = require('./index-4jiUeiUz.cjs');
|
|
5
|
+
var persistence = require('./types-C5kEpkcZ.cjs');
|
|
6
6
|
require('node:os');
|
|
7
7
|
require('node:crypto');
|
|
8
8
|
require('cross-spawn');
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import { D as DEFAULT_RECENT_LIMIT, M as MAX_READ_LIMIT, l as listSessions, r as readSessionTranscript, e as stopSession, f as archiveSession } from './index-
|
|
3
|
-
import { i as isValidSessionId } from './types-
|
|
2
|
+
import { D as DEFAULT_RECENT_LIMIT, M as MAX_READ_LIMIT, l as listSessions, r as readSessionTranscript, e as stopSession, f as archiveSession } from './index-Dq-eFp89.mjs';
|
|
3
|
+
import { i as isValidSessionId } from './types-Ds0SiQ1D.mjs';
|
|
4
4
|
import 'node:os';
|
|
5
5
|
import 'node:crypto';
|
|
6
6
|
import 'cross-spawn';
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { readFileSync, statSync } from 'node:fs';
|
|
3
3
|
import { resolve } from 'node:path';
|
|
4
|
-
import { c as checkIfDaemonRunningAndCleanupStaleState, s as spawnDaemonSession, a as sessionWebUrl, i as isValidSpawnOrigin, b as sanitizeSpawnPermissionMode, A as ALLOWED_SPAWN_PERMISSION_MODES, w as waitForSessionKey, d as sendUserMessage } from './index-
|
|
5
|
-
import { l as logger } from './types-
|
|
4
|
+
import { c as checkIfDaemonRunningAndCleanupStaleState, s as spawnDaemonSession, a as sessionWebUrl, i as isValidSpawnOrigin, b as sanitizeSpawnPermissionMode, A as ALLOWED_SPAWN_PERMISSION_MODES, w as waitForSessionKey, d as sendUserMessage } from './index-Dq-eFp89.mjs';
|
|
5
|
+
import { l as logger } from './types-Ds0SiQ1D.mjs';
|
|
6
6
|
import 'node:os';
|
|
7
7
|
import 'node:crypto';
|
|
8
8
|
import 'cross-spawn';
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
var chalk = require('chalk');
|
|
4
4
|
var node_fs = require('node:fs');
|
|
5
5
|
var path = require('node:path');
|
|
6
|
-
var index = require('./index-
|
|
7
|
-
var persistence = require('./types-
|
|
6
|
+
var index = require('./index-4jiUeiUz.cjs');
|
|
7
|
+
var persistence = require('./types-C5kEpkcZ.cjs');
|
|
8
8
|
require('node:os');
|
|
9
9
|
require('node:crypto');
|
|
10
10
|
require('cross-spawn');
|
|
@@ -51,7 +51,7 @@ var z__namespace = /*#__PURE__*/_interopNamespaceDefault(z);
|
|
|
51
51
|
var pty__namespace = /*#__PURE__*/_interopNamespaceDefault(pty);
|
|
52
52
|
|
|
53
53
|
var name = "very-happy-cli";
|
|
54
|
-
var version = "0.2.
|
|
54
|
+
var version = "0.2.116";
|
|
55
55
|
var description = "Remote multi-agent workspace CLI with deep Claude integration, native terminal streaming, and self-hosting.";
|
|
56
56
|
var author = "Kirill Dubovitskiy";
|
|
57
57
|
var contributors = [
|
|
@@ -164,12 +164,12 @@ var dependencies = {
|
|
|
164
164
|
zod: "^4.0.0"
|
|
165
165
|
};
|
|
166
166
|
var optionalDependencies = {
|
|
167
|
-
"very-happy-tools-arm64-darwin": "0.2.
|
|
168
|
-
"very-happy-tools-x64-darwin": "0.2.
|
|
169
|
-
"very-happy-tools-arm64-linux": "0.2.
|
|
170
|
-
"very-happy-tools-x64-linux": "0.2.
|
|
171
|
-
"very-happy-tools-arm64-win32": "0.2.
|
|
172
|
-
"very-happy-tools-x64-win32": "0.2.
|
|
167
|
+
"very-happy-tools-arm64-darwin": "0.2.116",
|
|
168
|
+
"very-happy-tools-x64-darwin": "0.2.116",
|
|
169
|
+
"very-happy-tools-arm64-linux": "0.2.116",
|
|
170
|
+
"very-happy-tools-x64-linux": "0.2.116",
|
|
171
|
+
"very-happy-tools-arm64-win32": "0.2.116",
|
|
172
|
+
"very-happy-tools-x64-win32": "0.2.116"
|
|
173
173
|
};
|
|
174
174
|
var devDependencies = {
|
|
175
175
|
"@slopus/happy-wire": "workspace:*",
|
|
@@ -1070,7 +1070,20 @@ const CliUpdateStateSchema = z.z.object({
|
|
|
1070
1070
|
recommendedVersion: z.z.string().nullable(),
|
|
1071
1071
|
minimumVersion: z.z.string().nullable(),
|
|
1072
1072
|
status: z.z.enum(["current", "available", "required"]),
|
|
1073
|
-
checkedAt: z.z.number()
|
|
1073
|
+
checkedAt: z.z.number(),
|
|
1074
|
+
/**
|
|
1075
|
+
* B-321: a new bundle is on disk but refused to run, so the daemon is still
|
|
1076
|
+
* serving the old code. Present only while that is true. Optional and additive:
|
|
1077
|
+
* old web builds ignore it, old daemons never send it (iron rule 4).
|
|
1078
|
+
*/
|
|
1079
|
+
handoverHold: z.z.object({ reason: z.z.string(), at: z.z.number() }).nullable().optional(),
|
|
1080
|
+
/** B-327: the last thing the machine's auto-update did, or why it did nothing. */
|
|
1081
|
+
autoUpdate: z.z.object({
|
|
1082
|
+
state: z.z.union([z.z.enum(["installing", "installed", "failed", "skipped"]), z.z.string()]),
|
|
1083
|
+
version: z.z.string().nullable(),
|
|
1084
|
+
detail: z.z.string().optional(),
|
|
1085
|
+
at: z.z.number()
|
|
1086
|
+
}).nullable().optional()
|
|
1074
1087
|
});
|
|
1075
1088
|
const WebTerminalListItemSchema = z.z.object({
|
|
1076
1089
|
id: z.z.string(),
|
|
@@ -1667,7 +1680,7 @@ class RpcHandlerManager {
|
|
|
1667
1680
|
}
|
|
1668
1681
|
}
|
|
1669
1682
|
|
|
1670
|
-
const __dirname$1 = path$1.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('types-
|
|
1683
|
+
const __dirname$1 = path$1.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('types-C5kEpkcZ.cjs', document.baseURI).href))));
|
|
1671
1684
|
function projectPath() {
|
|
1672
1685
|
const path = path$1.resolve(__dirname$1, "..");
|
|
1673
1686
|
return path;
|
|
@@ -9850,6 +9863,49 @@ class SnapshotStore {
|
|
|
9850
9863
|
}
|
|
9851
9864
|
}
|
|
9852
9865
|
|
|
9866
|
+
const OUTPUT_COALESCE_MS = 16;
|
|
9867
|
+
const OUTPUT_COALESCE_MAX_BYTES = 64 * 1024;
|
|
9868
|
+
function createOutputCoalescer({ maxDelayMs, maxBytes } = {
|
|
9869
|
+
maxDelayMs: OUTPUT_COALESCE_MS,
|
|
9870
|
+
maxBytes: OUTPUT_COALESCE_MAX_BYTES
|
|
9871
|
+
}) {
|
|
9872
|
+
let buffered = [];
|
|
9873
|
+
let bufferedBytes = 0;
|
|
9874
|
+
let firstBufferedAt = 0;
|
|
9875
|
+
let lastEmitAt = Number.NEGATIVE_INFINITY;
|
|
9876
|
+
const drain = (now) => {
|
|
9877
|
+
if (bufferedBytes === 0) return null;
|
|
9878
|
+
const out = buffered.length === 1 ? buffered[0] : Buffer.concat(buffered, bufferedBytes);
|
|
9879
|
+
buffered = [];
|
|
9880
|
+
bufferedBytes = 0;
|
|
9881
|
+
firstBufferedAt = 0;
|
|
9882
|
+
lastEmitAt = now;
|
|
9883
|
+
return out;
|
|
9884
|
+
};
|
|
9885
|
+
return {
|
|
9886
|
+
push(data, now) {
|
|
9887
|
+
if (data.length === 0) return null;
|
|
9888
|
+
if (bufferedBytes === 0 && now - lastEmitAt >= maxDelayMs) {
|
|
9889
|
+
lastEmitAt = now;
|
|
9890
|
+
return data;
|
|
9891
|
+
}
|
|
9892
|
+
if (bufferedBytes === 0) firstBufferedAt = now;
|
|
9893
|
+
buffered.push(data);
|
|
9894
|
+
bufferedBytes += data.length;
|
|
9895
|
+
return bufferedBytes >= maxBytes ? drain(now) : null;
|
|
9896
|
+
},
|
|
9897
|
+
flush(now) {
|
|
9898
|
+
return drain(now);
|
|
9899
|
+
},
|
|
9900
|
+
dueAt() {
|
|
9901
|
+
return bufferedBytes === 0 ? null : firstBufferedAt + maxDelayMs;
|
|
9902
|
+
},
|
|
9903
|
+
pendingBytes() {
|
|
9904
|
+
return bufferedBytes;
|
|
9905
|
+
}
|
|
9906
|
+
};
|
|
9907
|
+
}
|
|
9908
|
+
|
|
9853
9909
|
const MAX_SEND_KEYS_PAYLOAD_BYTES = 1e3;
|
|
9854
9910
|
function tmuxSingleQuote(text) {
|
|
9855
9911
|
return `'${text.split("'").join("'\\''")}'`;
|
|
@@ -10760,6 +10816,18 @@ class TerminalSession {
|
|
|
10760
10816
|
lastOutputAt;
|
|
10761
10817
|
cols;
|
|
10762
10818
|
rows;
|
|
10819
|
+
/**
|
|
10820
|
+
* B-334: `%output` arrives as one message per producer write — pi's startup
|
|
10821
|
+
* frame measured 1029 chunks of a median 9 bytes. Merging them before they
|
|
10822
|
+
* are given a seq is what keeps the wire (and the web's write chain) from
|
|
10823
|
+
* carrying a thousand frames for 12 KB. See outputCoalescer.ts.
|
|
10824
|
+
*/
|
|
10825
|
+
coalescer = createOutputCoalescer();
|
|
10826
|
+
flushTimer = null;
|
|
10827
|
+
/** Set by the manager right after construction: how a merged chunk reaches
|
|
10828
|
+
* the subscribers. Left undefined ⇒ the session buffers and drops nothing,
|
|
10829
|
+
* it just has nobody to tell. */
|
|
10830
|
+
onOutputChunk;
|
|
10763
10831
|
constructor(id, transport, tmuxSession, cols, rows) {
|
|
10764
10832
|
this.id = id;
|
|
10765
10833
|
this.transport = transport;
|
|
@@ -10806,6 +10874,61 @@ class TerminalSession {
|
|
|
10806
10874
|
}
|
|
10807
10875
|
return chunk;
|
|
10808
10876
|
}
|
|
10877
|
+
/**
|
|
10878
|
+
* The live-output entry point (B-334). Every `%output` / pty chunk goes
|
|
10879
|
+
* through here instead of straight to `ingest`: the coalescer decides
|
|
10880
|
+
* whether this byte run is emitted now (the common interactive case — an
|
|
10881
|
+
* echo after an idle window pays zero extra latency) or merged into the
|
|
10882
|
+
* frame currently forming. Merging is byte-exact concatenation, so one seq
|
|
10883
|
+
* covering the merged run is indistinguishable from tmux having handed us
|
|
10884
|
+
* one bigger read — the ring/replay/gap protocol is untouched.
|
|
10885
|
+
*/
|
|
10886
|
+
pushOutput(data) {
|
|
10887
|
+
const ready = this.coalescer.push(data, Date.now());
|
|
10888
|
+
if (ready) {
|
|
10889
|
+
this.clearFlushTimer();
|
|
10890
|
+
this.emitOutput(ready);
|
|
10891
|
+
}
|
|
10892
|
+
this.armFlushTimer();
|
|
10893
|
+
}
|
|
10894
|
+
/**
|
|
10895
|
+
* Emit whatever is buffered right now. MUST be called before anything that
|
|
10896
|
+
* reads or changes the screen's identity, because buffered bytes have not
|
|
10897
|
+
* been ingested yet and therefore own no seq:
|
|
10898
|
+
* - the capture anchor (`runCaptureBatch`) — pre-anchor output is already
|
|
10899
|
+
* inside the capture, so it has to own a seq <= `seqAtAnchor` or the
|
|
10900
|
+
* client applies it a second time on top of the restore;
|
|
10901
|
+
* - `resizeHeadless` — those bytes were produced at the OLD size;
|
|
10902
|
+
* - transport exit — otherwise the last frame before a shell dies is lost.
|
|
10903
|
+
*/
|
|
10904
|
+
flushOutput() {
|
|
10905
|
+
this.clearFlushTimer();
|
|
10906
|
+
const ready = this.coalescer.flush(Date.now());
|
|
10907
|
+
if (ready) this.emitOutput(ready);
|
|
10908
|
+
}
|
|
10909
|
+
emitOutput(data) {
|
|
10910
|
+
const chunk = this.ingest(data);
|
|
10911
|
+
this.onOutputChunk?.(chunk);
|
|
10912
|
+
}
|
|
10913
|
+
armFlushTimer() {
|
|
10914
|
+
const due = this.coalescer.dueAt();
|
|
10915
|
+
if (due === null) {
|
|
10916
|
+
this.clearFlushTimer();
|
|
10917
|
+
return;
|
|
10918
|
+
}
|
|
10919
|
+
if (this.flushTimer) return;
|
|
10920
|
+
const timer = setTimeout(() => {
|
|
10921
|
+
this.flushTimer = null;
|
|
10922
|
+
this.flushOutput();
|
|
10923
|
+
}, Math.max(0, due - Date.now()));
|
|
10924
|
+
timer.unref?.();
|
|
10925
|
+
this.flushTimer = timer;
|
|
10926
|
+
}
|
|
10927
|
+
clearFlushTimer() {
|
|
10928
|
+
if (!this.flushTimer) return;
|
|
10929
|
+
clearTimeout(this.flushTimer);
|
|
10930
|
+
this.flushTimer = null;
|
|
10931
|
+
}
|
|
10809
10932
|
/**
|
|
10810
10933
|
* Write a RESTORE payload (the open capture) into the authoritative screen
|
|
10811
10934
|
* WITHOUT giving it a seq: it is not a live chunk, so it must not enter the
|
|
@@ -10825,6 +10948,7 @@ class TerminalSession {
|
|
|
10825
10948
|
/** Keep the authoritative screen's dimensions in lockstep with the terminal
|
|
10826
10949
|
* so a later snapshot serialize() reflects the real geometry. */
|
|
10827
10950
|
resizeHeadless(cols, rows) {
|
|
10951
|
+
this.flushOutput();
|
|
10828
10952
|
this.cols = cols;
|
|
10829
10953
|
this.rows = rows;
|
|
10830
10954
|
try {
|
|
@@ -10904,6 +11028,7 @@ class TerminalSession {
|
|
|
10904
11028
|
} catch {
|
|
10905
11029
|
}
|
|
10906
11030
|
}
|
|
11031
|
+
this.clearFlushTimer();
|
|
10907
11032
|
try {
|
|
10908
11033
|
this.headless.dispose();
|
|
10909
11034
|
} catch {
|
|
@@ -11755,14 +11880,16 @@ class WebTerminalManager {
|
|
|
11755
11880
|
client: void 0
|
|
11756
11881
|
// replaced below
|
|
11757
11882
|
}, tmuxSession, cols, rows);
|
|
11883
|
+
created.onOutputChunk = (chunk) => {
|
|
11884
|
+
this.emit("terminal-output", { terminalId: id, data: chunk.data, seq: chunk.seq });
|
|
11885
|
+
this.noteActivity();
|
|
11886
|
+
};
|
|
11758
11887
|
const client = new ControlClient(tmuxSession, env, {
|
|
11759
11888
|
onOutput: (pane, data) => {
|
|
11760
11889
|
if (this.terminals.get(id) !== created) return;
|
|
11761
11890
|
if (!created.ingesting) return;
|
|
11762
11891
|
if (created.paneId && pane !== created.paneId) return;
|
|
11763
|
-
|
|
11764
|
-
this.emit("terminal-output", { terminalId: id, data: chunk.data, seq: chunk.seq });
|
|
11765
|
-
this.noteActivity();
|
|
11892
|
+
created.pushOutput(data);
|
|
11766
11893
|
},
|
|
11767
11894
|
onNotification: (name, args2) => {
|
|
11768
11895
|
if (this.terminals.get(id) !== created) return;
|
|
@@ -11784,6 +11911,7 @@ class WebTerminalManager {
|
|
|
11784
11911
|
},
|
|
11785
11912
|
onExit: (code) => {
|
|
11786
11913
|
if (this.terminals.get(id) !== created) return;
|
|
11914
|
+
created.flushOutput();
|
|
11787
11915
|
this.terminals.delete(id);
|
|
11788
11916
|
this.snapshots.drop(id);
|
|
11789
11917
|
created.dispose();
|
|
@@ -11804,14 +11932,17 @@ class WebTerminalManager {
|
|
|
11804
11932
|
}
|
|
11805
11933
|
}
|
|
11806
11934
|
const created = new TerminalSession(id, { kind: "pty", pty: proc }, void 0, cols, rows);
|
|
11807
|
-
|
|
11808
|
-
if (this.terminals.get(id) !== created) return;
|
|
11809
|
-
const chunk = created.ingest(Buffer.from(data, "utf8"));
|
|
11935
|
+
created.onOutputChunk = (chunk) => {
|
|
11810
11936
|
this.emit("terminal-output", { terminalId: id, data: chunk.data, seq: chunk.seq });
|
|
11811
11937
|
this.noteActivity();
|
|
11938
|
+
};
|
|
11939
|
+
proc.onData((data) => {
|
|
11940
|
+
if (this.terminals.get(id) !== created) return;
|
|
11941
|
+
created.pushOutput(Buffer.from(data, "utf8"));
|
|
11812
11942
|
});
|
|
11813
11943
|
proc.onExit(({ exitCode }) => {
|
|
11814
11944
|
if (this.terminals.get(id) !== created) return;
|
|
11945
|
+
created.flushOutput();
|
|
11815
11946
|
this.terminals.delete(id);
|
|
11816
11947
|
this.snapshots.drop(id);
|
|
11817
11948
|
created.dispose();
|
|
@@ -11955,6 +12086,7 @@ class WebTerminalManager {
|
|
|
11955
12086
|
command: c.command,
|
|
11956
12087
|
label: c.key,
|
|
11957
12088
|
onBlock: c.key === "anchor" ? () => {
|
|
12089
|
+
session.flushOutput();
|
|
11958
12090
|
seqAtAnchor = session.seq;
|
|
11959
12091
|
if (fresh) session.ingesting = true;
|
|
11960
12092
|
} : void 0
|
|
@@ -14425,6 +14557,15 @@ class ApiMachineClient {
|
|
|
14425
14557
|
// recover account material or impersonate a Web client. (Declared before
|
|
14426
14558
|
// webTerminal so the emit closure can read it.)
|
|
14427
14559
|
encTerminals = /* @__PURE__ */ new Set();
|
|
14560
|
+
/** B-327: an auto-update swaps the process that owns these terminals, so it
|
|
14561
|
+
* waits until there are none. */
|
|
14562
|
+
hasLiveTerminals() {
|
|
14563
|
+
try {
|
|
14564
|
+
return this.webTerminal.buildTerminalList().length > 0;
|
|
14565
|
+
} catch {
|
|
14566
|
+
return true;
|
|
14567
|
+
}
|
|
14568
|
+
}
|
|
14428
14569
|
webTerminal = new WebTerminalManager((event, payload) => {
|
|
14429
14570
|
let out = payload;
|
|
14430
14571
|
if (event === "terminal-output" && this.encTerminals.has(payload.terminalId)) {
|
|
@@ -29,7 +29,7 @@ import { SandboxManager } from '@anthropic-ai/sandbox-runtime';
|
|
|
29
29
|
import { Expo } from 'expo-server-sdk';
|
|
30
30
|
|
|
31
31
|
var name = "very-happy-cli";
|
|
32
|
-
var version = "0.2.
|
|
32
|
+
var version = "0.2.116";
|
|
33
33
|
var description = "Remote multi-agent workspace CLI with deep Claude integration, native terminal streaming, and self-hosting.";
|
|
34
34
|
var author = "Kirill Dubovitskiy";
|
|
35
35
|
var contributors = [
|
|
@@ -142,12 +142,12 @@ var dependencies = {
|
|
|
142
142
|
zod: "^4.0.0"
|
|
143
143
|
};
|
|
144
144
|
var optionalDependencies = {
|
|
145
|
-
"very-happy-tools-arm64-darwin": "0.2.
|
|
146
|
-
"very-happy-tools-x64-darwin": "0.2.
|
|
147
|
-
"very-happy-tools-arm64-linux": "0.2.
|
|
148
|
-
"very-happy-tools-x64-linux": "0.2.
|
|
149
|
-
"very-happy-tools-arm64-win32": "0.2.
|
|
150
|
-
"very-happy-tools-x64-win32": "0.2.
|
|
145
|
+
"very-happy-tools-arm64-darwin": "0.2.116",
|
|
146
|
+
"very-happy-tools-x64-darwin": "0.2.116",
|
|
147
|
+
"very-happy-tools-arm64-linux": "0.2.116",
|
|
148
|
+
"very-happy-tools-x64-linux": "0.2.116",
|
|
149
|
+
"very-happy-tools-arm64-win32": "0.2.116",
|
|
150
|
+
"very-happy-tools-x64-win32": "0.2.116"
|
|
151
151
|
};
|
|
152
152
|
var devDependencies = {
|
|
153
153
|
"@slopus/happy-wire": "workspace:*",
|
|
@@ -1048,7 +1048,20 @@ const CliUpdateStateSchema = z$1.object({
|
|
|
1048
1048
|
recommendedVersion: z$1.string().nullable(),
|
|
1049
1049
|
minimumVersion: z$1.string().nullable(),
|
|
1050
1050
|
status: z$1.enum(["current", "available", "required"]),
|
|
1051
|
-
checkedAt: z$1.number()
|
|
1051
|
+
checkedAt: z$1.number(),
|
|
1052
|
+
/**
|
|
1053
|
+
* B-321: a new bundle is on disk but refused to run, so the daemon is still
|
|
1054
|
+
* serving the old code. Present only while that is true. Optional and additive:
|
|
1055
|
+
* old web builds ignore it, old daemons never send it (iron rule 4).
|
|
1056
|
+
*/
|
|
1057
|
+
handoverHold: z$1.object({ reason: z$1.string(), at: z$1.number() }).nullable().optional(),
|
|
1058
|
+
/** B-327: the last thing the machine's auto-update did, or why it did nothing. */
|
|
1059
|
+
autoUpdate: z$1.object({
|
|
1060
|
+
state: z$1.union([z$1.enum(["installing", "installed", "failed", "skipped"]), z$1.string()]),
|
|
1061
|
+
version: z$1.string().nullable(),
|
|
1062
|
+
detail: z$1.string().optional(),
|
|
1063
|
+
at: z$1.number()
|
|
1064
|
+
}).nullable().optional()
|
|
1052
1065
|
});
|
|
1053
1066
|
const WebTerminalListItemSchema = z$1.object({
|
|
1054
1067
|
id: z$1.string(),
|
|
@@ -9828,6 +9841,49 @@ class SnapshotStore {
|
|
|
9828
9841
|
}
|
|
9829
9842
|
}
|
|
9830
9843
|
|
|
9844
|
+
const OUTPUT_COALESCE_MS = 16;
|
|
9845
|
+
const OUTPUT_COALESCE_MAX_BYTES = 64 * 1024;
|
|
9846
|
+
function createOutputCoalescer({ maxDelayMs, maxBytes } = {
|
|
9847
|
+
maxDelayMs: OUTPUT_COALESCE_MS,
|
|
9848
|
+
maxBytes: OUTPUT_COALESCE_MAX_BYTES
|
|
9849
|
+
}) {
|
|
9850
|
+
let buffered = [];
|
|
9851
|
+
let bufferedBytes = 0;
|
|
9852
|
+
let firstBufferedAt = 0;
|
|
9853
|
+
let lastEmitAt = Number.NEGATIVE_INFINITY;
|
|
9854
|
+
const drain = (now) => {
|
|
9855
|
+
if (bufferedBytes === 0) return null;
|
|
9856
|
+
const out = buffered.length === 1 ? buffered[0] : Buffer.concat(buffered, bufferedBytes);
|
|
9857
|
+
buffered = [];
|
|
9858
|
+
bufferedBytes = 0;
|
|
9859
|
+
firstBufferedAt = 0;
|
|
9860
|
+
lastEmitAt = now;
|
|
9861
|
+
return out;
|
|
9862
|
+
};
|
|
9863
|
+
return {
|
|
9864
|
+
push(data, now) {
|
|
9865
|
+
if (data.length === 0) return null;
|
|
9866
|
+
if (bufferedBytes === 0 && now - lastEmitAt >= maxDelayMs) {
|
|
9867
|
+
lastEmitAt = now;
|
|
9868
|
+
return data;
|
|
9869
|
+
}
|
|
9870
|
+
if (bufferedBytes === 0) firstBufferedAt = now;
|
|
9871
|
+
buffered.push(data);
|
|
9872
|
+
bufferedBytes += data.length;
|
|
9873
|
+
return bufferedBytes >= maxBytes ? drain(now) : null;
|
|
9874
|
+
},
|
|
9875
|
+
flush(now) {
|
|
9876
|
+
return drain(now);
|
|
9877
|
+
},
|
|
9878
|
+
dueAt() {
|
|
9879
|
+
return bufferedBytes === 0 ? null : firstBufferedAt + maxDelayMs;
|
|
9880
|
+
},
|
|
9881
|
+
pendingBytes() {
|
|
9882
|
+
return bufferedBytes;
|
|
9883
|
+
}
|
|
9884
|
+
};
|
|
9885
|
+
}
|
|
9886
|
+
|
|
9831
9887
|
const MAX_SEND_KEYS_PAYLOAD_BYTES = 1e3;
|
|
9832
9888
|
function tmuxSingleQuote(text) {
|
|
9833
9889
|
return `'${text.split("'").join("'\\''")}'`;
|
|
@@ -10738,6 +10794,18 @@ class TerminalSession {
|
|
|
10738
10794
|
lastOutputAt;
|
|
10739
10795
|
cols;
|
|
10740
10796
|
rows;
|
|
10797
|
+
/**
|
|
10798
|
+
* B-334: `%output` arrives as one message per producer write — pi's startup
|
|
10799
|
+
* frame measured 1029 chunks of a median 9 bytes. Merging them before they
|
|
10800
|
+
* are given a seq is what keeps the wire (and the web's write chain) from
|
|
10801
|
+
* carrying a thousand frames for 12 KB. See outputCoalescer.ts.
|
|
10802
|
+
*/
|
|
10803
|
+
coalescer = createOutputCoalescer();
|
|
10804
|
+
flushTimer = null;
|
|
10805
|
+
/** Set by the manager right after construction: how a merged chunk reaches
|
|
10806
|
+
* the subscribers. Left undefined ⇒ the session buffers and drops nothing,
|
|
10807
|
+
* it just has nobody to tell. */
|
|
10808
|
+
onOutputChunk;
|
|
10741
10809
|
constructor(id, transport, tmuxSession, cols, rows) {
|
|
10742
10810
|
this.id = id;
|
|
10743
10811
|
this.transport = transport;
|
|
@@ -10784,6 +10852,61 @@ class TerminalSession {
|
|
|
10784
10852
|
}
|
|
10785
10853
|
return chunk;
|
|
10786
10854
|
}
|
|
10855
|
+
/**
|
|
10856
|
+
* The live-output entry point (B-334). Every `%output` / pty chunk goes
|
|
10857
|
+
* through here instead of straight to `ingest`: the coalescer decides
|
|
10858
|
+
* whether this byte run is emitted now (the common interactive case — an
|
|
10859
|
+
* echo after an idle window pays zero extra latency) or merged into the
|
|
10860
|
+
* frame currently forming. Merging is byte-exact concatenation, so one seq
|
|
10861
|
+
* covering the merged run is indistinguishable from tmux having handed us
|
|
10862
|
+
* one bigger read — the ring/replay/gap protocol is untouched.
|
|
10863
|
+
*/
|
|
10864
|
+
pushOutput(data) {
|
|
10865
|
+
const ready = this.coalescer.push(data, Date.now());
|
|
10866
|
+
if (ready) {
|
|
10867
|
+
this.clearFlushTimer();
|
|
10868
|
+
this.emitOutput(ready);
|
|
10869
|
+
}
|
|
10870
|
+
this.armFlushTimer();
|
|
10871
|
+
}
|
|
10872
|
+
/**
|
|
10873
|
+
* Emit whatever is buffered right now. MUST be called before anything that
|
|
10874
|
+
* reads or changes the screen's identity, because buffered bytes have not
|
|
10875
|
+
* been ingested yet and therefore own no seq:
|
|
10876
|
+
* - the capture anchor (`runCaptureBatch`) — pre-anchor output is already
|
|
10877
|
+
* inside the capture, so it has to own a seq <= `seqAtAnchor` or the
|
|
10878
|
+
* client applies it a second time on top of the restore;
|
|
10879
|
+
* - `resizeHeadless` — those bytes were produced at the OLD size;
|
|
10880
|
+
* - transport exit — otherwise the last frame before a shell dies is lost.
|
|
10881
|
+
*/
|
|
10882
|
+
flushOutput() {
|
|
10883
|
+
this.clearFlushTimer();
|
|
10884
|
+
const ready = this.coalescer.flush(Date.now());
|
|
10885
|
+
if (ready) this.emitOutput(ready);
|
|
10886
|
+
}
|
|
10887
|
+
emitOutput(data) {
|
|
10888
|
+
const chunk = this.ingest(data);
|
|
10889
|
+
this.onOutputChunk?.(chunk);
|
|
10890
|
+
}
|
|
10891
|
+
armFlushTimer() {
|
|
10892
|
+
const due = this.coalescer.dueAt();
|
|
10893
|
+
if (due === null) {
|
|
10894
|
+
this.clearFlushTimer();
|
|
10895
|
+
return;
|
|
10896
|
+
}
|
|
10897
|
+
if (this.flushTimer) return;
|
|
10898
|
+
const timer = setTimeout(() => {
|
|
10899
|
+
this.flushTimer = null;
|
|
10900
|
+
this.flushOutput();
|
|
10901
|
+
}, Math.max(0, due - Date.now()));
|
|
10902
|
+
timer.unref?.();
|
|
10903
|
+
this.flushTimer = timer;
|
|
10904
|
+
}
|
|
10905
|
+
clearFlushTimer() {
|
|
10906
|
+
if (!this.flushTimer) return;
|
|
10907
|
+
clearTimeout(this.flushTimer);
|
|
10908
|
+
this.flushTimer = null;
|
|
10909
|
+
}
|
|
10787
10910
|
/**
|
|
10788
10911
|
* Write a RESTORE payload (the open capture) into the authoritative screen
|
|
10789
10912
|
* WITHOUT giving it a seq: it is not a live chunk, so it must not enter the
|
|
@@ -10803,6 +10926,7 @@ class TerminalSession {
|
|
|
10803
10926
|
/** Keep the authoritative screen's dimensions in lockstep with the terminal
|
|
10804
10927
|
* so a later snapshot serialize() reflects the real geometry. */
|
|
10805
10928
|
resizeHeadless(cols, rows) {
|
|
10929
|
+
this.flushOutput();
|
|
10806
10930
|
this.cols = cols;
|
|
10807
10931
|
this.rows = rows;
|
|
10808
10932
|
try {
|
|
@@ -10882,6 +11006,7 @@ class TerminalSession {
|
|
|
10882
11006
|
} catch {
|
|
10883
11007
|
}
|
|
10884
11008
|
}
|
|
11009
|
+
this.clearFlushTimer();
|
|
10885
11010
|
try {
|
|
10886
11011
|
this.headless.dispose();
|
|
10887
11012
|
} catch {
|
|
@@ -11733,14 +11858,16 @@ class WebTerminalManager {
|
|
|
11733
11858
|
client: void 0
|
|
11734
11859
|
// replaced below
|
|
11735
11860
|
}, tmuxSession, cols, rows);
|
|
11861
|
+
created.onOutputChunk = (chunk) => {
|
|
11862
|
+
this.emit("terminal-output", { terminalId: id, data: chunk.data, seq: chunk.seq });
|
|
11863
|
+
this.noteActivity();
|
|
11864
|
+
};
|
|
11736
11865
|
const client = new ControlClient(tmuxSession, env, {
|
|
11737
11866
|
onOutput: (pane, data) => {
|
|
11738
11867
|
if (this.terminals.get(id) !== created) return;
|
|
11739
11868
|
if (!created.ingesting) return;
|
|
11740
11869
|
if (created.paneId && pane !== created.paneId) return;
|
|
11741
|
-
|
|
11742
|
-
this.emit("terminal-output", { terminalId: id, data: chunk.data, seq: chunk.seq });
|
|
11743
|
-
this.noteActivity();
|
|
11870
|
+
created.pushOutput(data);
|
|
11744
11871
|
},
|
|
11745
11872
|
onNotification: (name, args2) => {
|
|
11746
11873
|
if (this.terminals.get(id) !== created) return;
|
|
@@ -11762,6 +11889,7 @@ class WebTerminalManager {
|
|
|
11762
11889
|
},
|
|
11763
11890
|
onExit: (code) => {
|
|
11764
11891
|
if (this.terminals.get(id) !== created) return;
|
|
11892
|
+
created.flushOutput();
|
|
11765
11893
|
this.terminals.delete(id);
|
|
11766
11894
|
this.snapshots.drop(id);
|
|
11767
11895
|
created.dispose();
|
|
@@ -11782,14 +11910,17 @@ class WebTerminalManager {
|
|
|
11782
11910
|
}
|
|
11783
11911
|
}
|
|
11784
11912
|
const created = new TerminalSession(id, { kind: "pty", pty: proc }, void 0, cols, rows);
|
|
11785
|
-
|
|
11786
|
-
if (this.terminals.get(id) !== created) return;
|
|
11787
|
-
const chunk = created.ingest(Buffer.from(data, "utf8"));
|
|
11913
|
+
created.onOutputChunk = (chunk) => {
|
|
11788
11914
|
this.emit("terminal-output", { terminalId: id, data: chunk.data, seq: chunk.seq });
|
|
11789
11915
|
this.noteActivity();
|
|
11916
|
+
};
|
|
11917
|
+
proc.onData((data) => {
|
|
11918
|
+
if (this.terminals.get(id) !== created) return;
|
|
11919
|
+
created.pushOutput(Buffer.from(data, "utf8"));
|
|
11790
11920
|
});
|
|
11791
11921
|
proc.onExit(({ exitCode }) => {
|
|
11792
11922
|
if (this.terminals.get(id) !== created) return;
|
|
11923
|
+
created.flushOutput();
|
|
11793
11924
|
this.terminals.delete(id);
|
|
11794
11925
|
this.snapshots.drop(id);
|
|
11795
11926
|
created.dispose();
|
|
@@ -11933,6 +12064,7 @@ class WebTerminalManager {
|
|
|
11933
12064
|
command: c.command,
|
|
11934
12065
|
label: c.key,
|
|
11935
12066
|
onBlock: c.key === "anchor" ? () => {
|
|
12067
|
+
session.flushOutput();
|
|
11936
12068
|
seqAtAnchor = session.seq;
|
|
11937
12069
|
if (fresh) session.ingesting = true;
|
|
11938
12070
|
} : void 0
|
|
@@ -14403,6 +14535,15 @@ class ApiMachineClient {
|
|
|
14403
14535
|
// recover account material or impersonate a Web client. (Declared before
|
|
14404
14536
|
// webTerminal so the emit closure can read it.)
|
|
14405
14537
|
encTerminals = /* @__PURE__ */ new Set();
|
|
14538
|
+
/** B-327: an auto-update swaps the process that owns these terminals, so it
|
|
14539
|
+
* waits until there are none. */
|
|
14540
|
+
hasLiveTerminals() {
|
|
14541
|
+
try {
|
|
14542
|
+
return this.webTerminal.buildTerminalList().length > 0;
|
|
14543
|
+
} catch {
|
|
14544
|
+
return true;
|
|
14545
|
+
}
|
|
14546
|
+
}
|
|
14406
14547
|
webTerminal = new WebTerminalManager((event, payload) => {
|
|
14407
14548
|
let out = payload;
|
|
14408
14549
|
if (event === "terminal-output" && this.encTerminals.has(payload.terminalId)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "very-happy-cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.116",
|
|
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": [
|
|
@@ -113,12 +113,12 @@
|
|
|
113
113
|
"zod": "^4.0.0"
|
|
114
114
|
},
|
|
115
115
|
"optionalDependencies": {
|
|
116
|
-
"very-happy-tools-arm64-darwin": "0.2.
|
|
117
|
-
"very-happy-tools-x64-darwin": "0.2.
|
|
118
|
-
"very-happy-tools-arm64-linux": "0.2.
|
|
119
|
-
"very-happy-tools-x64-linux": "0.2.
|
|
120
|
-
"very-happy-tools-arm64-win32": "0.2.
|
|
121
|
-
"very-happy-tools-x64-win32": "0.2.
|
|
116
|
+
"very-happy-tools-arm64-darwin": "0.2.116",
|
|
117
|
+
"very-happy-tools-x64-darwin": "0.2.116",
|
|
118
|
+
"very-happy-tools-arm64-linux": "0.2.116",
|
|
119
|
+
"very-happy-tools-x64-linux": "0.2.116",
|
|
120
|
+
"very-happy-tools-arm64-win32": "0.2.116",
|
|
121
|
+
"very-happy-tools-x64-win32": "0.2.116"
|
|
122
122
|
},
|
|
123
123
|
"devDependencies": {
|
|
124
124
|
"@slopus/happy-wire": "workspace:*",
|