very-happy-cli 0.2.111 → 0.2.112

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/{AcpBackend-B_olk79F.cjs → AcpBackend-B3x-gQWP.cjs} +1 -1
  2. package/dist/{AcpBackend-CeXVb_s_.mjs → AcpBackend-DDEdYVW2.mjs} +1 -1
  3. package/dist/{AcpSessionManager-vqdqfD1l.mjs → AcpSessionManager-BORAhQ1r.mjs} +1 -1
  4. package/dist/{AcpSessionManager-uXwWO2Tn.cjs → AcpSessionManager-DGPLD-go.cjs} +1 -1
  5. package/dist/{config-vGkcq9nM.mjs → config-B_L5vXXu.mjs} +2 -2
  6. package/dist/{config-BR3tkFL_.cjs → config-KX6e7GEu.cjs} +2 -2
  7. package/dist/{index-CtBP0Qvc.cjs → index-B7-t5Kwz.cjs} +4 -4
  8. package/dist/{index-BOoGJGQc.cjs → index-BnRQP3PL.cjs} +240 -19
  9. package/dist/{index-SjthsMid.cjs → index-CTd0skWH.cjs} +2 -2
  10. package/dist/{index-JAhGeW4_.mjs → index-Cu5FdgTq.mjs} +5 -5
  11. package/dist/{index-Bn-xsguz.mjs → index-inkhalP5.mjs} +236 -15
  12. package/dist/{index-C-RLk8-e.mjs → index-lQzCAx9I.mjs} +2 -2
  13. package/dist/index.cjs +2 -2
  14. package/dist/index.mjs +2 -2
  15. package/dist/{installTerminalHooks-FbKaK149.cjs → installTerminalHooks-BVk-r0yD.cjs} +2 -2
  16. package/dist/{installTerminalHooks-ADNcNdnB.mjs → installTerminalHooks-CQiW7Jn9.mjs} +2 -2
  17. package/dist/lib.cjs +1 -1
  18. package/dist/lib.d.cts +44 -0
  19. package/dist/lib.d.mts +44 -0
  20. package/dist/lib.mjs +1 -1
  21. package/dist/{mcp-DPp-bYNE.cjs → mcp-D0DIU8Q2.cjs} +2 -2
  22. package/dist/{mcp-DHPm_gvL.mjs → mcp-DD-0VkiA.mjs} +2 -2
  23. package/dist/{runGemini-CWWyjQMz.mjs → runGemini-DHyBDo5P.mjs} +4 -4
  24. package/dist/{runGemini-D5ySpYIZ.cjs → runGemini-Dq1d8Ccb.cjs} +4 -4
  25. package/dist/{runOpenClaw-Bwrb1LXP.cjs → runOpenClaw-BHXnUF_m.cjs} +3 -3
  26. package/dist/{runOpenClaw-Bv4rUTyJ.mjs → runOpenClaw-DX-tgQ2V.mjs} +3 -3
  27. package/dist/{send-BlkxFZu4.mjs → send-0TYDXiGx.mjs} +2 -2
  28. package/dist/{send-DBfSNILO.cjs → send-By6zEUWo.cjs} +2 -2
  29. package/dist/{sessions-CVuEsdRl.cjs → sessions-DTBRcBRk.cjs} +2 -2
  30. package/dist/{sessions-1fW8soBM.mjs → sessions-TM8VaCMY.mjs} +2 -2
  31. package/dist/{spawn-D_ZexrY9.cjs → spawn-CweTEYze.cjs} +2 -2
  32. package/dist/{spawn-BorKN3W2.mjs → spawn-D0oTo5BT.mjs} +2 -2
  33. package/dist/{types-k_ipDb0c.mjs → types-C5ewhu1Q.mjs} +96 -11
  34. package/dist/{types-BG_5QKX5.cjs → types-R11tMGyX.cjs} +97 -11
  35. package/package.json +7 -7
@@ -3,9 +3,9 @@ import { execFileSync } from 'node:child_process';
3
3
  import os from 'node:os';
4
4
  import { existsSync, readFileSync } from 'node:fs';
5
5
  import { join } from 'node:path';
6
- import { m as ensurePrivateDirectorySync, n as hardenPrivateFileSync, w as writePrivateFileSync, e as errorLogMetadata, d as contentLogMetadata, h as connectionState, A as ApiClient, j as readSettings, k as encodeBase64, l as logger, c as configuration } from './types-k_ipDb0c.mjs';
7
- import { A as AcpSessionManager } from './AcpSessionManager-vqdqfD1l.mjs';
8
- import { h as initialMachineMetadata, j as createSessionMetadata, k as setupOfflineReconnection, n as notifyDaemonSessionStarted, m as MessageQueue2, v as registerKillSessionHandler } from './index-Bn-xsguz.mjs';
6
+ import { m as ensurePrivateDirectorySync, n as hardenPrivateFileSync, w as writePrivateFileSync, e as errorLogMetadata, d as contentLogMetadata, h as connectionState, A as ApiClient, j as readSettings, k as encodeBase64, l as logger, c as configuration } from './types-C5ewhu1Q.mjs';
7
+ import { A as AcpSessionManager } from './AcpSessionManager-BORAhQ1r.mjs';
8
+ import { h as initialMachineMetadata, j as createSessionMetadata, k as setupOfflineReconnection, n as notifyDaemonSessionStarted, m as MessageQueue2, v as registerKillSessionHandler } from './index-inkhalP5.mjs';
9
9
  import WebSocket from 'ws';
10
10
  import * as ed from '@noble/ed25519';
11
11
  import { sha512 } from '@noble/hashes/sha2.js';
@@ -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-k_ipDb0c.mjs';
5
- import { d as sendUserMessage, a as sessionWebUrl } from './index-Bn-xsguz.mjs';
4
+ import { r as readPersistedSessions, c as configuration } from './types-C5ewhu1Q.mjs';
5
+ import { d as sendUserMessage, a as sessionWebUrl } from './index-inkhalP5.mjs';
6
6
  import 'axios';
7
7
  import 'node:util';
8
8
  import 'node:os';
@@ -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 persistence = require('./types-BG_5QKX5.cjs');
7
- var index = require('./index-BOoGJGQc.cjs');
6
+ var persistence = require('./types-R11tMGyX.cjs');
7
+ var index = require('./index-BnRQP3PL.cjs');
8
8
  require('axios');
9
9
  require('node:util');
10
10
  require('node:os');
@@ -1,8 +1,8 @@
1
1
  'use strict';
2
2
 
3
3
  var chalk = require('chalk');
4
- var index = require('./index-BOoGJGQc.cjs');
5
- var persistence = require('./types-BG_5QKX5.cjs');
4
+ var index = require('./index-BnRQP3PL.cjs');
5
+ var persistence = require('./types-R11tMGyX.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-Bn-xsguz.mjs';
3
- import { i as isValidSessionId } from './types-k_ipDb0c.mjs';
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-inkhalP5.mjs';
3
+ import { i as isValidSessionId } from './types-C5ewhu1Q.mjs';
4
4
  import 'node:os';
5
5
  import 'node:crypto';
6
6
  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-BOoGJGQc.cjs');
7
- var persistence = require('./types-BG_5QKX5.cjs');
6
+ var index = require('./index-BnRQP3PL.cjs');
7
+ var persistence = require('./types-R11tMGyX.cjs');
8
8
  require('node:os');
9
9
  require('node:crypto');
10
10
  require('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-Bn-xsguz.mjs';
5
- import { l as logger } from './types-k_ipDb0c.mjs';
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-inkhalP5.mjs';
5
+ import { l as logger } from './types-C5ewhu1Q.mjs';
6
6
  import 'node:os';
7
7
  import 'node:crypto';
8
8
  import 'cross-spawn';
@@ -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.111";
32
+ var version = "0.2.112";
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.111",
146
- "very-happy-tools-x64-darwin": "0.2.111",
147
- "very-happy-tools-arm64-linux": "0.2.111",
148
- "very-happy-tools-x64-linux": "0.2.111",
149
- "very-happy-tools-arm64-win32": "0.2.111",
150
- "very-happy-tools-x64-win32": "0.2.111"
145
+ "very-happy-tools-arm64-darwin": "0.2.112",
146
+ "very-happy-tools-x64-darwin": "0.2.112",
147
+ "very-happy-tools-arm64-linux": "0.2.112",
148
+ "very-happy-tools-x64-linux": "0.2.112",
149
+ "very-happy-tools-arm64-win32": "0.2.112",
150
+ "very-happy-tools-x64-win32": "0.2.112"
151
151
  };
152
152
  var devDependencies = {
153
153
  "@slopus/happy-wire": "workspace:*",
@@ -747,6 +747,14 @@ const sessionEnvelopeSchema = z.object({
747
747
  // Codex app-server item id for this envelope. Used as the precise
748
748
  // rollback point for Codex thread duplicate/fork-from-message.
749
749
  codexItemId: z.string().min(1).optional(),
750
+ // B-309: identity of the live draft this envelope supersedes,
751
+ // `"<api message id>:<content block index>"`. Set on text/thinking
752
+ // envelopes mapped from a Claude assistant message so the web can replace
753
+ // the streamed draft with the persisted message in a single frame instead
754
+ // of letting it flicker out and back in. Absent from every other producer
755
+ // (and from CLIs older than the streaming relay) — consumers must treat a
756
+ // missing key as "no draft to claim", never as an error.
757
+ streamKey: z.string().min(1).optional(),
750
758
  // Per-API-call token usage of the assistant message this envelope was
751
759
  // mapped from (B-108). Envelope-level (not inside `ev`) so every envelope
752
760
  // type carries it uniformly — the web feeds it into its context meter.
@@ -785,6 +793,7 @@ function createEnvelope(role, ev, opts = {}) {
785
793
  ...opts.subagent ? { subagent: opts.subagent } : {},
786
794
  ...opts.claudeUuid ? { claudeUuid: opts.claudeUuid } : {},
787
795
  ...opts.codexItemId ? { codexItemId: opts.codexItemId } : {},
796
+ ...opts.streamKey ? { streamKey: opts.streamKey } : {},
788
797
  ...opts.usage ? { usage: opts.usage } : {},
789
798
  ev
790
799
  });
@@ -887,6 +896,44 @@ z.object({
887
896
  body: CoreUpdateBodySchema,
888
897
  createdAt: z.number()
889
898
  });
899
+ const STREAM_DELTA_MAX_CHARS = 16 * 1024;
900
+ const sessionStreamBlockKindSchema = z.enum(["text", "thinking"]);
901
+ const sessionStreamBlockStartSchema = z.object({
902
+ t: z.literal("block-start"),
903
+ mid: z.string().min(1),
904
+ idx: z.number().int().min(0),
905
+ kind: sessionStreamBlockKindSchema
906
+ });
907
+ const sessionStreamBlockDeltaSchema = z.object({
908
+ t: z.literal("block-delta"),
909
+ mid: z.string().min(1),
910
+ idx: z.number().int().min(0),
911
+ text: z.string().max(STREAM_DELTA_MAX_CHARS)
912
+ });
913
+ const sessionStreamBlockEndSchema = z.object({
914
+ t: z.literal("block-end"),
915
+ mid: z.string().min(1),
916
+ idx: z.number().int().min(0)
917
+ });
918
+ const sessionStreamProgressSchema = z.object({
919
+ t: z.literal("progress"),
920
+ thinkingTokens: z.number().int().min(0).optional(),
921
+ outputTokens: z.number().int().min(0).optional(),
922
+ status: z.enum(["requesting", "compacting"]).optional()
923
+ });
924
+ const sessionStreamTurnEndSchema = z.object({
925
+ t: z.literal("turn-end")
926
+ });
927
+ z.discriminatedUnion("t", [
928
+ sessionStreamBlockStartSchema,
929
+ sessionStreamBlockDeltaSchema,
930
+ sessionStreamBlockEndSchema,
931
+ sessionStreamProgressSchema,
932
+ sessionStreamTurnEndSchema
933
+ ]);
934
+ function streamKeyOf(messageId, blockIndex) {
935
+ return `${messageId}:${blockIndex}`;
936
+ }
890
937
  const VoiceConversationGrantedSchema = z.object({
891
938
  allowed: z.literal(true),
892
939
  conversationToken: z.string(),
@@ -2769,13 +2816,24 @@ function mapClaudeLogMessageToSessionEnvelopesInternal(message, state) {
2769
2816
  maybeEmitSubagentStart(state, turnId, subagent, envelopes);
2770
2817
  const usage = pickAssistantUsage(message);
2771
2818
  const blocks = Array.isArray(message.message?.content) ? message.message.content : [];
2772
- for (const block of blocks) {
2819
+ const apiMessageId = !isSidechainMessage(message) && typeof message.message?.id === "string" && message.message.id.length > 0 ? message.message.id : void 0;
2820
+ let blockCursor = 0;
2821
+ if (apiMessageId) {
2822
+ if (state.streamBlockMessageId !== apiMessageId) {
2823
+ state.streamBlockMessageId = apiMessageId;
2824
+ state.streamBlockCursor = 0;
2825
+ }
2826
+ blockCursor = state.streamBlockCursor ?? 0;
2827
+ state.streamBlockCursor = blockCursor + blocks.length;
2828
+ }
2829
+ const draftKey = (index) => apiMessageId ? streamKeyOf(apiMessageId, blockCursor + index) : void 0;
2830
+ for (const [blockIndex, block] of blocks.entries()) {
2773
2831
  if (block.type === "text" && typeof block.text === "string" && block.text.trim().length > 0) {
2774
- envelopes.push(createEnvelope("agent", { t: "text", text: block.text }, { turn: turnId, subagent, claudeUuid, usage }));
2832
+ envelopes.push(createEnvelope("agent", { t: "text", text: block.text }, { turn: turnId, subagent, claudeUuid, usage, streamKey: draftKey(blockIndex) }));
2775
2833
  continue;
2776
2834
  }
2777
2835
  if (block.type === "thinking" && typeof block.thinking === "string" && block.thinking.trim().length > 0) {
2778
- envelopes.push(createEnvelope("agent", { t: "text", text: block.thinking, thinking: true }, { turn: turnId, subagent, claudeUuid, usage }));
2836
+ envelopes.push(createEnvelope("agent", { t: "text", text: block.thinking, thinking: true }, { turn: turnId, subagent, claudeUuid, usage, streamKey: draftKey(blockIndex) }));
2779
2837
  continue;
2780
2838
  }
2781
2839
  if (block.type === "tool_use") {
@@ -3774,6 +3832,33 @@ class ApiSessionClient extends EventEmitter {
3774
3832
  mode
3775
3833
  });
3776
3834
  }
3835
+ /**
3836
+ * B-309: push one live stream frame (thinking/text delta or progress) to
3837
+ * this account's web clients.
3838
+ *
3839
+ * Encrypted with the SESSION key and relayed without being stored —
3840
+ * clipboard-push's shape, for clipboard-push's reasons: the relay must not
3841
+ * be able to read thinking text, and the session id is stamped by the
3842
+ * server from the authenticated connection rather than trusted from here.
3843
+ *
3844
+ * `volatile` on purpose. A draft that misses the wire while disconnected
3845
+ * is worth nothing — the persisted message is still coming, and replaying
3846
+ * stale deltas after a reconnect would paint text that has already landed.
3847
+ */
3848
+ sendStreamFrame(frame) {
3849
+ if (!this.socket.connected) return;
3850
+ try {
3851
+ this.socket.volatile.emit("session-stream", {
3852
+ // The frame object, NOT a JSON string: `encrypt` serializes
3853
+ // internally, so pre-stringifying would double-encode and
3854
+ // escape every quote in the streamed text.
3855
+ payload: encodeBase64(encrypt(this.encryptionKey, this.encryptionVariant, frame)),
3856
+ enc: true
3857
+ });
3858
+ } catch (error) {
3859
+ logger.debug(`[API] Failed to relay stream frame: ${error}`);
3860
+ }
3861
+ }
3777
3862
  /**
3778
3863
  * Send session death message
3779
3864
  */
@@ -16302,4 +16387,4 @@ const RawJSONLinesSchema = z$1.discriminatedUnion("type", [
16302
16387
  }).passthrough()
16303
16388
  ]);
16304
16389
 
16305
- export { mirrorLocalId as $, ApiClient as A, credentialRelayProblem as B, deriveLocalCliUpdateSummary as C, updateSettings as D, encodeBase64Url as E, decodeBase64 as F, writeCredentialsLegacy as G, writeCredentialsDataKey as H, InvalidateSync as I, readCredentialsForConfiguredRelay as J, encrypt as K, isClaudeSessionId as L, MAX_CHAT_ATTACHMENT_SOURCE_BYTES as M, getLocalHappyAgentCredentialPath as N, readLocalHappyAgentCredentials as O, decryptWithDataKey as P, decryptLegacy as Q, RawJSONLinesSchema as R, extractCompleteLines as S, decideBackfill as T, parseTerminalHookPayload as U, tmuxArgs as V, scrubTmuxClientEnv as W, decideMirrorBinding as X, MIRROR_BACKFILL_LINES_DEFAULT as Y, persistSession as Z, mirrorLineKey as _, ApiSessionClient as a, detectResumeSupport as a0, detectCLIAvailability as a1, acquireDaemonLock as a2, summarizeSpawnSessionForLog as a3, deletePersistedSessions as a4, shellescape as a5, decrypt as a6, writeDaemonState as a7, writeSettings as a8, fetchCliUpdateState as a9, resolveCliUpdateCheckInterval as aa, DEFAULT_CLI_UPDATE_CHECK_INTERVAL_MS as ab, releaseDaemonLock as ac, listVhTerminals as ad, isValidTerminalId as ae, readVhTerminal as af, sendToVhTerminal as ag, TITLE_PROMPT_PREFIX as ah, BOARD_ANALYZER_PROMPT_PREFIX as ai, startOfflineReconnection as aj, clearCredentials as ak, clearMachineId as al, SandboxConfigSchema as am, CodexAppServerClient as an, getLatestDaemonLog as ao, persistence as ap, api as aq, createEnvelope as b, configuration as c, contentLogMetadata as d, errorLogMetadata as e, packageJson as f, delay as g, connectionState as h, isValidSessionId as i, readSettings as j, encodeBase64 as k, logger as l, ensurePrivateDirectorySync as m, hardenPrivateFileSync as n, readDaemonState as o, projectPath as p, clearDaemonState as q, readPersistedSessions as r, getProjectPath as s, initializeSandbox as t, wrapCommand as u, AsyncLock as v, writePrivateFileSync as w, ensurePrivateDirectory as x, hardenPrivateFile as y, readCredentials as z };
16390
+ export { mirrorLineKey as $, ApiClient as A, credentialRelayProblem as B, deriveLocalCliUpdateSummary as C, updateSettings as D, encodeBase64Url as E, decodeBase64 as F, writeCredentialsLegacy as G, writeCredentialsDataKey as H, InvalidateSync as I, readCredentialsForConfiguredRelay as J, encrypt as K, isClaudeSessionId as L, MAX_CHAT_ATTACHMENT_SOURCE_BYTES as M, getLocalHappyAgentCredentialPath as N, readLocalHappyAgentCredentials as O, decryptWithDataKey as P, decryptLegacy as Q, RawJSONLinesSchema as R, STREAM_DELTA_MAX_CHARS as S, extractCompleteLines as T, decideBackfill as U, parseTerminalHookPayload as V, tmuxArgs as W, scrubTmuxClientEnv as X, decideMirrorBinding as Y, MIRROR_BACKFILL_LINES_DEFAULT as Z, persistSession as _, ApiSessionClient as a, mirrorLocalId as a0, detectResumeSupport as a1, detectCLIAvailability as a2, acquireDaemonLock as a3, summarizeSpawnSessionForLog as a4, deletePersistedSessions as a5, shellescape as a6, decrypt as a7, writeDaemonState as a8, writeSettings as a9, fetchCliUpdateState as aa, resolveCliUpdateCheckInterval as ab, DEFAULT_CLI_UPDATE_CHECK_INTERVAL_MS as ac, releaseDaemonLock as ad, listVhTerminals as ae, isValidTerminalId as af, readVhTerminal as ag, sendToVhTerminal as ah, TITLE_PROMPT_PREFIX as ai, BOARD_ANALYZER_PROMPT_PREFIX as aj, startOfflineReconnection as ak, clearCredentials as al, clearMachineId as am, SandboxConfigSchema as an, CodexAppServerClient as ao, getLatestDaemonLog as ap, persistence as aq, api as ar, createEnvelope as b, configuration as c, contentLogMetadata as d, errorLogMetadata as e, packageJson as f, delay as g, connectionState as h, isValidSessionId as i, readSettings as j, encodeBase64 as k, logger as l, ensurePrivateDirectorySync as m, hardenPrivateFileSync as n, readDaemonState as o, projectPath as p, clearDaemonState as q, readPersistedSessions as r, getProjectPath as s, initializeSandbox as t, wrapCommand as u, AsyncLock as v, writePrivateFileSync as w, ensurePrivateDirectory as x, hardenPrivateFile as y, readCredentials as z };
@@ -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.111";
54
+ var version = "0.2.112";
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.111",
168
- "very-happy-tools-x64-darwin": "0.2.111",
169
- "very-happy-tools-arm64-linux": "0.2.111",
170
- "very-happy-tools-x64-linux": "0.2.111",
171
- "very-happy-tools-arm64-win32": "0.2.111",
172
- "very-happy-tools-x64-win32": "0.2.111"
167
+ "very-happy-tools-arm64-darwin": "0.2.112",
168
+ "very-happy-tools-x64-darwin": "0.2.112",
169
+ "very-happy-tools-arm64-linux": "0.2.112",
170
+ "very-happy-tools-x64-linux": "0.2.112",
171
+ "very-happy-tools-arm64-win32": "0.2.112",
172
+ "very-happy-tools-x64-win32": "0.2.112"
173
173
  };
174
174
  var devDependencies = {
175
175
  "@slopus/happy-wire": "workspace:*",
@@ -769,6 +769,14 @@ const sessionEnvelopeSchema = z__namespace.object({
769
769
  // Codex app-server item id for this envelope. Used as the precise
770
770
  // rollback point for Codex thread duplicate/fork-from-message.
771
771
  codexItemId: z__namespace.string().min(1).optional(),
772
+ // B-309: identity of the live draft this envelope supersedes,
773
+ // `"<api message id>:<content block index>"`. Set on text/thinking
774
+ // envelopes mapped from a Claude assistant message so the web can replace
775
+ // the streamed draft with the persisted message in a single frame instead
776
+ // of letting it flicker out and back in. Absent from every other producer
777
+ // (and from CLIs older than the streaming relay) — consumers must treat a
778
+ // missing key as "no draft to claim", never as an error.
779
+ streamKey: z__namespace.string().min(1).optional(),
772
780
  // Per-API-call token usage of the assistant message this envelope was
773
781
  // mapped from (B-108). Envelope-level (not inside `ev`) so every envelope
774
782
  // type carries it uniformly — the web feeds it into its context meter.
@@ -807,6 +815,7 @@ function createEnvelope(role, ev, opts = {}) {
807
815
  ...opts.subagent ? { subagent: opts.subagent } : {},
808
816
  ...opts.claudeUuid ? { claudeUuid: opts.claudeUuid } : {},
809
817
  ...opts.codexItemId ? { codexItemId: opts.codexItemId } : {},
818
+ ...opts.streamKey ? { streamKey: opts.streamKey } : {},
810
819
  ...opts.usage ? { usage: opts.usage } : {},
811
820
  ev
812
821
  });
@@ -909,6 +918,44 @@ z__namespace.object({
909
918
  body: CoreUpdateBodySchema,
910
919
  createdAt: z__namespace.number()
911
920
  });
921
+ const STREAM_DELTA_MAX_CHARS = 16 * 1024;
922
+ const sessionStreamBlockKindSchema = z__namespace.enum(["text", "thinking"]);
923
+ const sessionStreamBlockStartSchema = z__namespace.object({
924
+ t: z__namespace.literal("block-start"),
925
+ mid: z__namespace.string().min(1),
926
+ idx: z__namespace.number().int().min(0),
927
+ kind: sessionStreamBlockKindSchema
928
+ });
929
+ const sessionStreamBlockDeltaSchema = z__namespace.object({
930
+ t: z__namespace.literal("block-delta"),
931
+ mid: z__namespace.string().min(1),
932
+ idx: z__namespace.number().int().min(0),
933
+ text: z__namespace.string().max(STREAM_DELTA_MAX_CHARS)
934
+ });
935
+ const sessionStreamBlockEndSchema = z__namespace.object({
936
+ t: z__namespace.literal("block-end"),
937
+ mid: z__namespace.string().min(1),
938
+ idx: z__namespace.number().int().min(0)
939
+ });
940
+ const sessionStreamProgressSchema = z__namespace.object({
941
+ t: z__namespace.literal("progress"),
942
+ thinkingTokens: z__namespace.number().int().min(0).optional(),
943
+ outputTokens: z__namespace.number().int().min(0).optional(),
944
+ status: z__namespace.enum(["requesting", "compacting"]).optional()
945
+ });
946
+ const sessionStreamTurnEndSchema = z__namespace.object({
947
+ t: z__namespace.literal("turn-end")
948
+ });
949
+ z__namespace.discriminatedUnion("t", [
950
+ sessionStreamBlockStartSchema,
951
+ sessionStreamBlockDeltaSchema,
952
+ sessionStreamBlockEndSchema,
953
+ sessionStreamProgressSchema,
954
+ sessionStreamTurnEndSchema
955
+ ]);
956
+ function streamKeyOf(messageId, blockIndex) {
957
+ return `${messageId}:${blockIndex}`;
958
+ }
912
959
  const VoiceConversationGrantedSchema = z__namespace.object({
913
960
  allowed: z__namespace.literal(true),
914
961
  conversationToken: z__namespace.string(),
@@ -1603,7 +1650,7 @@ class RpcHandlerManager {
1603
1650
  }
1604
1651
  }
1605
1652
 
1606
- 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-BG_5QKX5.cjs', document.baseURI).href))));
1653
+ 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-R11tMGyX.cjs', document.baseURI).href))));
1607
1654
  function projectPath() {
1608
1655
  const path = path$1.resolve(__dirname$1, "..");
1609
1656
  return path;
@@ -2791,13 +2838,24 @@ function mapClaudeLogMessageToSessionEnvelopesInternal(message, state) {
2791
2838
  maybeEmitSubagentStart(state, turnId, subagent, envelopes);
2792
2839
  const usage = pickAssistantUsage(message);
2793
2840
  const blocks = Array.isArray(message.message?.content) ? message.message.content : [];
2794
- for (const block of blocks) {
2841
+ const apiMessageId = !isSidechainMessage(message) && typeof message.message?.id === "string" && message.message.id.length > 0 ? message.message.id : void 0;
2842
+ let blockCursor = 0;
2843
+ if (apiMessageId) {
2844
+ if (state.streamBlockMessageId !== apiMessageId) {
2845
+ state.streamBlockMessageId = apiMessageId;
2846
+ state.streamBlockCursor = 0;
2847
+ }
2848
+ blockCursor = state.streamBlockCursor ?? 0;
2849
+ state.streamBlockCursor = blockCursor + blocks.length;
2850
+ }
2851
+ const draftKey = (index) => apiMessageId ? streamKeyOf(apiMessageId, blockCursor + index) : void 0;
2852
+ for (const [blockIndex, block] of blocks.entries()) {
2795
2853
  if (block.type === "text" && typeof block.text === "string" && block.text.trim().length > 0) {
2796
- envelopes.push(createEnvelope("agent", { t: "text", text: block.text }, { turn: turnId, subagent, claudeUuid, usage }));
2854
+ envelopes.push(createEnvelope("agent", { t: "text", text: block.text }, { turn: turnId, subagent, claudeUuid, usage, streamKey: draftKey(blockIndex) }));
2797
2855
  continue;
2798
2856
  }
2799
2857
  if (block.type === "thinking" && typeof block.thinking === "string" && block.thinking.trim().length > 0) {
2800
- envelopes.push(createEnvelope("agent", { t: "text", text: block.thinking, thinking: true }, { turn: turnId, subagent, claudeUuid, usage }));
2858
+ envelopes.push(createEnvelope("agent", { t: "text", text: block.thinking, thinking: true }, { turn: turnId, subagent, claudeUuid, usage, streamKey: draftKey(blockIndex) }));
2801
2859
  continue;
2802
2860
  }
2803
2861
  if (block.type === "tool_use") {
@@ -3796,6 +3854,33 @@ class ApiSessionClient extends node_events.EventEmitter {
3796
3854
  mode
3797
3855
  });
3798
3856
  }
3857
+ /**
3858
+ * B-309: push one live stream frame (thinking/text delta or progress) to
3859
+ * this account's web clients.
3860
+ *
3861
+ * Encrypted with the SESSION key and relayed without being stored —
3862
+ * clipboard-push's shape, for clipboard-push's reasons: the relay must not
3863
+ * be able to read thinking text, and the session id is stamped by the
3864
+ * server from the authenticated connection rather than trusted from here.
3865
+ *
3866
+ * `volatile` on purpose. A draft that misses the wire while disconnected
3867
+ * is worth nothing — the persisted message is still coming, and replaying
3868
+ * stale deltas after a reconnect would paint text that has already landed.
3869
+ */
3870
+ sendStreamFrame(frame) {
3871
+ if (!this.socket.connected) return;
3872
+ try {
3873
+ this.socket.volatile.emit("session-stream", {
3874
+ // The frame object, NOT a JSON string: `encrypt` serializes
3875
+ // internally, so pre-stringifying would double-encode and
3876
+ // escape every quote in the streamed text.
3877
+ payload: encodeBase64(encrypt(this.encryptionKey, this.encryptionVariant, frame)),
3878
+ enc: true
3879
+ });
3880
+ } catch (error) {
3881
+ logger.debug(`[API] Failed to relay stream frame: ${error}`);
3882
+ }
3883
+ }
3799
3884
  /**
3800
3885
  * Send session death message
3801
3886
  */
@@ -16334,6 +16419,7 @@ exports.InvalidateSync = InvalidateSync;
16334
16419
  exports.MAX_CHAT_ATTACHMENT_SOURCE_BYTES = MAX_CHAT_ATTACHMENT_SOURCE_BYTES;
16335
16420
  exports.MIRROR_BACKFILL_LINES_DEFAULT = MIRROR_BACKFILL_LINES_DEFAULT;
16336
16421
  exports.RawJSONLinesSchema = RawJSONLinesSchema;
16422
+ exports.STREAM_DELTA_MAX_CHARS = STREAM_DELTA_MAX_CHARS;
16337
16423
  exports.SandboxConfigSchema = SandboxConfigSchema;
16338
16424
  exports.TITLE_PROMPT_PREFIX = TITLE_PROMPT_PREFIX;
16339
16425
  exports.acquireDaemonLock = acquireDaemonLock;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "very-happy-cli",
3
- "version": "0.2.111",
3
+ "version": "0.2.112",
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.111",
117
- "very-happy-tools-x64-darwin": "0.2.111",
118
- "very-happy-tools-arm64-linux": "0.2.111",
119
- "very-happy-tools-x64-linux": "0.2.111",
120
- "very-happy-tools-arm64-win32": "0.2.111",
121
- "very-happy-tools-x64-win32": "0.2.111"
116
+ "very-happy-tools-arm64-darwin": "0.2.112",
117
+ "very-happy-tools-x64-darwin": "0.2.112",
118
+ "very-happy-tools-arm64-linux": "0.2.112",
119
+ "very-happy-tools-x64-linux": "0.2.112",
120
+ "very-happy-tools-arm64-win32": "0.2.112",
121
+ "very-happy-tools-x64-win32": "0.2.112"
122
122
  },
123
123
  "devDependencies": {
124
124
  "@slopus/happy-wire": "workspace:*",