very-happy-cli 0.2.75 → 0.2.77
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-5CPQMP3H.mjs → AcpBackend-BFFC77vV.mjs} +5 -2
- package/dist/{AcpBackend-B0qCm-bY.cjs → AcpBackend-CsLnh1So.cjs} +5 -2
- package/dist/{AcpSessionManager-B4M0y5fI.cjs → AcpSessionManager--CixPN-p.cjs} +1 -1
- package/dist/{AcpSessionManager-BTmJyKDO.mjs → AcpSessionManager-CU5vsxgF.mjs} +1 -1
- package/dist/{config-DbY2R-H7.mjs → config-CRFTYuTN.mjs} +2 -2
- package/dist/{config-DBIfKaNJ.cjs → config-pv17N6Kg.cjs} +2 -2
- package/dist/{index-CJaqyq4z.mjs → index-BkvUwyIU.mjs} +8 -5
- package/dist/{index-CqxywXyf.cjs → index-C1OnwMx0.cjs} +7 -4
- package/dist/{index-Cq30c63l.mjs → index-DOb57EGb.mjs} +16 -12
- package/dist/{index-edEH95Mf.cjs → index-Dcv1RKe8.cjs} +19 -15
- package/dist/index.cjs +2 -2
- package/dist/index.mjs +2 -2
- package/dist/{installTerminalHooks-8TUdSFCY.mjs → installTerminalHooks-IXvZMGJq.mjs} +2 -2
- package/dist/{installTerminalHooks-BvQbDFcE.cjs → installTerminalHooks-YyT3fv6w.cjs} +2 -2
- package/dist/lib.cjs +1 -1
- package/dist/lib.d.cts +23 -1
- package/dist/lib.d.mts +23 -1
- package/dist/lib.mjs +1 -1
- package/dist/{mcp-B4UxtBKQ.mjs → mcp-B-GdhlKD.mjs} +2 -2
- package/dist/{mcp-D12X7XKG.cjs → mcp-CsKnmeE5.cjs} +2 -2
- package/dist/{runGemini-BD8OGMQr.mjs → runGemini-D0lddF43.mjs} +5 -4
- package/dist/{runGemini-LIck5ard.cjs → runGemini-DuUdmlVZ.cjs} +5 -4
- package/dist/{runOpenClaw-ENAgQy1E.mjs → runOpenClaw-BiwwYnYf.mjs} +15 -3
- package/dist/{runOpenClaw-B5XNV1zZ.cjs → runOpenClaw-DEMCYSoD.cjs} +15 -3
- package/dist/{send-BVbiwtrQ.mjs → send-DM8oqJFp.mjs} +2 -2
- package/dist/{send-o4xljyoV.cjs → send-DijSXAMp.cjs} +2 -2
- package/dist/{spawn-DTA6f9TE.cjs → spawn-De3b5Hsu.cjs} +2 -2
- package/dist/{spawn-BmqqHUYI.mjs → spawn-nISXbZD5.mjs} +2 -2
- package/dist/{types-Dj9JEGDh.mjs → types-D2qSKjsO.mjs} +400 -79
- package/dist/{types-DEre2zxO.cjs → types-NwtcBgfS.cjs} +401 -80
- package/package.json +7 -7
|
@@ -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.77";
|
|
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.77",
|
|
168
|
+
"very-happy-tools-x64-darwin": "0.2.77",
|
|
169
|
+
"very-happy-tools-arm64-linux": "0.2.77",
|
|
170
|
+
"very-happy-tools-x64-linux": "0.2.77",
|
|
171
|
+
"very-happy-tools-arm64-win32": "0.2.77",
|
|
172
|
+
"very-happy-tools-x64-win32": "0.2.77"
|
|
173
173
|
};
|
|
174
174
|
var devDependencies = {
|
|
175
175
|
"@slopus/happy-wire": "workspace:*",
|
|
@@ -918,6 +918,15 @@ z.z.object({
|
|
|
918
918
|
region: z.z.string().min(1).max(64),
|
|
919
919
|
version: z.z.string().min(1).max(64)
|
|
920
920
|
});
|
|
921
|
+
const ReleaseSlotSchema = z.z.enum(["blue", "green"]);
|
|
922
|
+
const ReleaseDrainNoticeSchema = z.z.object({
|
|
923
|
+
epoch: z.z.string().regex(/^[A-Za-z0-9._-]{8,128}$/),
|
|
924
|
+
fromRelease: z.z.string().regex(/^[0-9a-f]{40}$/),
|
|
925
|
+
toRelease: z.z.string().regex(/^[0-9a-f]{40}$/),
|
|
926
|
+
candidateSlot: ReleaseSlotSchema,
|
|
927
|
+
deadline: z.z.number().int().positive(),
|
|
928
|
+
mode: z.z.literal("make-before-break")
|
|
929
|
+
});
|
|
921
930
|
|
|
922
931
|
z.z.object({
|
|
923
932
|
host: z.z.string(),
|
|
@@ -1381,6 +1390,56 @@ class RpcHandlerManager {
|
|
|
1381
1390
|
socket.emit("rpc-register", { method: prefixedMethod });
|
|
1382
1391
|
}
|
|
1383
1392
|
}
|
|
1393
|
+
/**
|
|
1394
|
+
* Attach a candidate transport and wait until the server confirms every
|
|
1395
|
+
* method before a release handover closes the old transport.
|
|
1396
|
+
*/
|
|
1397
|
+
async onSocketConnectAndWait(socket, timeoutMs = 1e4) {
|
|
1398
|
+
this.sockets.add(socket);
|
|
1399
|
+
const pending = new Set(this.handlers.keys());
|
|
1400
|
+
const acknowledged = /* @__PURE__ */ new Set();
|
|
1401
|
+
await new Promise((resolve, reject) => {
|
|
1402
|
+
let settleTimer = null;
|
|
1403
|
+
const timer = setTimeout(() => {
|
|
1404
|
+
cleanup();
|
|
1405
|
+
reject(new Error(`RPC registration timed out (${pending.size} pending)`));
|
|
1406
|
+
}, timeoutMs);
|
|
1407
|
+
timer.unref?.();
|
|
1408
|
+
const maybeSettle = () => {
|
|
1409
|
+
if (pending.size > 0 || settleTimer) return;
|
|
1410
|
+
settleTimer = setTimeout(() => {
|
|
1411
|
+
settleTimer = null;
|
|
1412
|
+
for (const prefixedMethod of this.handlers.keys()) {
|
|
1413
|
+
if (acknowledged.has(prefixedMethod) || pending.has(prefixedMethod)) continue;
|
|
1414
|
+
pending.add(prefixedMethod);
|
|
1415
|
+
socket.emit("rpc-register", { method: prefixedMethod });
|
|
1416
|
+
}
|
|
1417
|
+
if (pending.size === 0) {
|
|
1418
|
+
cleanup();
|
|
1419
|
+
resolve();
|
|
1420
|
+
}
|
|
1421
|
+
}, 0);
|
|
1422
|
+
settleTimer.unref?.();
|
|
1423
|
+
};
|
|
1424
|
+
const onRegistered = (data) => {
|
|
1425
|
+
if (typeof data?.method !== "string") return;
|
|
1426
|
+
acknowledged.add(data.method);
|
|
1427
|
+
pending.delete(data.method);
|
|
1428
|
+
maybeSettle();
|
|
1429
|
+
};
|
|
1430
|
+
const cleanup = () => {
|
|
1431
|
+
clearTimeout(timer);
|
|
1432
|
+
if (settleTimer) clearTimeout(settleTimer);
|
|
1433
|
+
socket.off("rpc-registered", onRegistered);
|
|
1434
|
+
};
|
|
1435
|
+
socket.on("rpc-registered", onRegistered);
|
|
1436
|
+
for (const prefixedMethod of pending) socket.emit("rpc-register", { method: prefixedMethod });
|
|
1437
|
+
maybeSettle();
|
|
1438
|
+
}).catch((error) => {
|
|
1439
|
+
this.sockets.delete(socket);
|
|
1440
|
+
throw error;
|
|
1441
|
+
});
|
|
1442
|
+
}
|
|
1384
1443
|
onSocketDisconnect(socket) {
|
|
1385
1444
|
if (socket) this.sockets.delete(socket);
|
|
1386
1445
|
else this.sockets.clear();
|
|
@@ -1415,7 +1474,7 @@ class RpcHandlerManager {
|
|
|
1415
1474
|
}
|
|
1416
1475
|
}
|
|
1417
1476
|
|
|
1418
|
-
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-
|
|
1477
|
+
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-NwtcBgfS.cjs', document.baseURI).href))));
|
|
1419
1478
|
function projectPath() {
|
|
1420
1479
|
const path = path$1.resolve(__dirname$1, "..");
|
|
1421
1480
|
return path;
|
|
@@ -2686,6 +2745,45 @@ class InvalidateSync {
|
|
|
2686
2745
|
};
|
|
2687
2746
|
}
|
|
2688
2747
|
|
|
2748
|
+
function record(value) {
|
|
2749
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
2750
|
+
}
|
|
2751
|
+
function finiteNonNegative(value) {
|
|
2752
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : void 0;
|
|
2753
|
+
}
|
|
2754
|
+
function firstNumber(source, keys) {
|
|
2755
|
+
for (const key of keys) {
|
|
2756
|
+
const value = finiteNonNegative(source[key]);
|
|
2757
|
+
if (value !== void 0) return value;
|
|
2758
|
+
}
|
|
2759
|
+
return void 0;
|
|
2760
|
+
}
|
|
2761
|
+
function normalizeAgentUsage(raw) {
|
|
2762
|
+
const outer = record(raw);
|
|
2763
|
+
if (!outer) return null;
|
|
2764
|
+
const wrapped = record(outer.totalTokenUsage) ?? record(outer.total_token_usage) ?? record(outer.total) ?? record(outer.usage) ?? outer;
|
|
2765
|
+
const input = firstNumber(wrapped, ["inputTokens", "input_tokens", "input"]);
|
|
2766
|
+
const output = firstNumber(wrapped, ["outputTokens", "output_tokens", "output"]);
|
|
2767
|
+
const cacheRead = firstNumber(wrapped, ["cachedInputTokens", "cached_input_tokens", "cachedReadTokens", "cacheReadInputTokens", "cache_read_input_tokens", "cache_read"]);
|
|
2768
|
+
const cacheCreation = firstNumber(wrapped, ["cacheCreationInputTokens", "cache_creation_input_tokens", "cachedWriteTokens", "cacheWriteInputTokens", "cache_write_input_tokens", "cache_creation"]);
|
|
2769
|
+
const reasoning = firstNumber(wrapped, ["reasoningOutputTokens", "reasoning_output_tokens", "thoughtTokens", "reasoning"]);
|
|
2770
|
+
const explicitTotal = firstNumber(wrapped, ["totalTokens", "total_tokens", "total"]);
|
|
2771
|
+
const fallbackTotal = input !== void 0 || output !== void 0 ? (input ?? 0) + (output ?? 0) : void 0;
|
|
2772
|
+
const total = explicitTotal ?? fallbackTotal;
|
|
2773
|
+
if (total === void 0) return null;
|
|
2774
|
+
return {
|
|
2775
|
+
total,
|
|
2776
|
+
...input !== void 0 ? { input } : {},
|
|
2777
|
+
...output !== void 0 ? { output } : {},
|
|
2778
|
+
...cacheCreation !== void 0 ? { cache_creation: cacheCreation } : {},
|
|
2779
|
+
...cacheRead !== void 0 ? { cache_read: cacheRead } : {},
|
|
2780
|
+
...reasoning !== void 0 ? { reasoning } : {}
|
|
2781
|
+
};
|
|
2782
|
+
}
|
|
2783
|
+
function usageAgentKey(agent) {
|
|
2784
|
+
return agent.toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "") || "unknown";
|
|
2785
|
+
}
|
|
2786
|
+
|
|
2689
2787
|
class ApiSessionClient extends node_events.EventEmitter {
|
|
2690
2788
|
token;
|
|
2691
2789
|
sessionId;
|
|
@@ -2726,12 +2824,24 @@ class ApiSessionClient extends node_events.EventEmitter {
|
|
|
2726
2824
|
startedSubagents: /* @__PURE__ */ new Set(),
|
|
2727
2825
|
activeSubagents: /* @__PURE__ */ new Set()
|
|
2728
2826
|
};
|
|
2827
|
+
seenClaudeUsageEvents = /* @__PURE__ */ new Set();
|
|
2828
|
+
claudeUsageTotals = {
|
|
2829
|
+
total: 0,
|
|
2830
|
+
input: 0,
|
|
2831
|
+
output: 0,
|
|
2832
|
+
cache_creation: 0,
|
|
2833
|
+
cache_read: 0,
|
|
2834
|
+
costTotal: 0,
|
|
2835
|
+
costInput: 0,
|
|
2836
|
+
costOutput: 0
|
|
2837
|
+
};
|
|
2729
2838
|
lastSeq = 0;
|
|
2730
2839
|
pendingOutbox = [];
|
|
2731
2840
|
sendSync;
|
|
2732
2841
|
receiveSync;
|
|
2733
2842
|
directInboundLocalIds = /* @__PURE__ */ new Set();
|
|
2734
2843
|
routedInboundLocalIds = /* @__PURE__ */ new Set();
|
|
2844
|
+
handoverInFlight = null;
|
|
2735
2845
|
constructor(token, session) {
|
|
2736
2846
|
super();
|
|
2737
2847
|
this.token = token;
|
|
@@ -2751,26 +2861,15 @@ class ApiSessionClient extends node_events.EventEmitter {
|
|
|
2751
2861
|
logger: (msg, data) => logger.debug(msg, data)
|
|
2752
2862
|
});
|
|
2753
2863
|
registerCommonHandlers(this.rpcHandlerManager, this.metadata.path);
|
|
2754
|
-
this.socket =
|
|
2755
|
-
|
|
2756
|
-
token: this.token,
|
|
2757
|
-
clientType: "session-scoped",
|
|
2758
|
-
sessionId: this.sessionId,
|
|
2759
|
-
happyClient: `cli-coding-session/${configuration.currentCliVersion}`
|
|
2760
|
-
},
|
|
2761
|
-
path: "/v1/updates",
|
|
2762
|
-
reconnection: false,
|
|
2763
|
-
transports: ["websocket"],
|
|
2764
|
-
withCredentials: true,
|
|
2765
|
-
autoConnect: false
|
|
2766
|
-
});
|
|
2864
|
+
this.socket = this.createControlSocket();
|
|
2865
|
+
const controlSocket = this.socket;
|
|
2767
2866
|
this.socket.on("connect", () => {
|
|
2768
2867
|
logger.debug("Socket connected successfully");
|
|
2769
2868
|
if (this.reconnectInterval) {
|
|
2770
2869
|
clearInterval(this.reconnectInterval);
|
|
2771
2870
|
this.reconnectInterval = null;
|
|
2772
2871
|
}
|
|
2773
|
-
this.rpcHandlerManager.onSocketConnect(
|
|
2872
|
+
this.rpcHandlerManager.onSocketConnect(controlSocket);
|
|
2774
2873
|
this.receiveSync.invalidate();
|
|
2775
2874
|
void this.connectSessionRelay();
|
|
2776
2875
|
});
|
|
@@ -2779,12 +2878,12 @@ class ApiSessionClient extends node_events.EventEmitter {
|
|
|
2779
2878
|
});
|
|
2780
2879
|
this.socket.on("disconnect", (reason) => {
|
|
2781
2880
|
logger.debug(`[API] Socket disconnected: ${reason}`);
|
|
2782
|
-
this.rpcHandlerManager.onSocketDisconnect();
|
|
2881
|
+
this.rpcHandlerManager.onSocketDisconnect(controlSocket);
|
|
2783
2882
|
this.startSmartReconnect();
|
|
2784
2883
|
});
|
|
2785
2884
|
this.socket.on("connect_error", (error) => {
|
|
2786
2885
|
logger.debug("[API] Socket connection error:", error);
|
|
2787
|
-
this.rpcHandlerManager.onSocketDisconnect();
|
|
2886
|
+
this.rpcHandlerManager.onSocketDisconnect(controlSocket);
|
|
2788
2887
|
if (error.message === "Session archived") {
|
|
2789
2888
|
logger.debug("[API] Archived session rejected during reconnect; exiting");
|
|
2790
2889
|
this.emit("archived");
|
|
@@ -2855,8 +2954,103 @@ class ApiSessionClient extends node_events.EventEmitter {
|
|
|
2855
2954
|
this.socket.on("error", (error) => {
|
|
2856
2955
|
logger.debug("[API] Socket error:", error);
|
|
2857
2956
|
});
|
|
2957
|
+
this.socket.on("server-draining", (data) => {
|
|
2958
|
+
const parsed = ReleaseDrainNoticeSchema.safeParse(data);
|
|
2959
|
+
if (!parsed.success || parsed.data.deadline <= Date.now()) return;
|
|
2960
|
+
void this.startReleaseHandover(parsed.data);
|
|
2961
|
+
});
|
|
2858
2962
|
this.socket.connect();
|
|
2859
2963
|
}
|
|
2964
|
+
createControlSocket(handover) {
|
|
2965
|
+
return socket_ioClient.io(configuration.serverUrl, {
|
|
2966
|
+
auth: {
|
|
2967
|
+
token: this.token,
|
|
2968
|
+
clientType: "session-scoped",
|
|
2969
|
+
sessionId: this.sessionId,
|
|
2970
|
+
happyClient: `cli-coding-session/${configuration.currentCliVersion}`,
|
|
2971
|
+
...handover ? { handoverEpoch: handover.epoch } : {}
|
|
2972
|
+
},
|
|
2973
|
+
...handover ? { query: { vh_slot: handover.candidateSlot }, forceNew: true } : {},
|
|
2974
|
+
path: "/v1/updates",
|
|
2975
|
+
reconnection: false,
|
|
2976
|
+
transports: ["websocket"],
|
|
2977
|
+
withCredentials: true,
|
|
2978
|
+
autoConnect: false
|
|
2979
|
+
});
|
|
2980
|
+
}
|
|
2981
|
+
startReleaseHandover(notice) {
|
|
2982
|
+
if (this.handoverInFlight) return this.handoverInFlight;
|
|
2983
|
+
this.handoverInFlight = this.releaseHandover(notice).finally(() => {
|
|
2984
|
+
this.handoverInFlight = null;
|
|
2985
|
+
});
|
|
2986
|
+
return this.handoverInFlight;
|
|
2987
|
+
}
|
|
2988
|
+
async releaseHandover(notice) {
|
|
2989
|
+
const startedAt = Date.now();
|
|
2990
|
+
const previous = this.socket;
|
|
2991
|
+
const candidate = this.createControlSocket(notice);
|
|
2992
|
+
const transferredListeners = /* @__PURE__ */ new Map();
|
|
2993
|
+
for (const listener of previous.listeners("rpc-request")) candidate.on("rpc-request", listener);
|
|
2994
|
+
const timeoutMs = Math.max(1, Math.min(1e4, notice.deadline - Date.now()));
|
|
2995
|
+
try {
|
|
2996
|
+
await new Promise((resolve, reject) => {
|
|
2997
|
+
const timer = setTimeout(() => reject(new Error("release handover timeout")), timeoutMs);
|
|
2998
|
+
candidate.once("connect", () => {
|
|
2999
|
+
clearTimeout(timer);
|
|
3000
|
+
resolve();
|
|
3001
|
+
});
|
|
3002
|
+
candidate.once("connect_error", (error) => {
|
|
3003
|
+
clearTimeout(timer);
|
|
3004
|
+
reject(error);
|
|
3005
|
+
});
|
|
3006
|
+
candidate.connect();
|
|
3007
|
+
});
|
|
3008
|
+
await this.rpcHandlerManager.onSocketConnectAndWait(candidate, timeoutMs);
|
|
3009
|
+
if (this.socket !== previous) {
|
|
3010
|
+
this.rpcHandlerManager.onSocketDisconnect(candidate);
|
|
3011
|
+
candidate.close();
|
|
3012
|
+
return;
|
|
3013
|
+
}
|
|
3014
|
+
for (const event of ["session-archive", "update", "error", "server-draining"]) {
|
|
3015
|
+
const listeners = previous.listeners(event);
|
|
3016
|
+
previous.removeAllListeners(event);
|
|
3017
|
+
for (const listener of listeners) candidate.on(event, listener);
|
|
3018
|
+
transferredListeners.set(event, listeners);
|
|
3019
|
+
}
|
|
3020
|
+
this.socket = candidate;
|
|
3021
|
+
candidate.on("connect", () => {
|
|
3022
|
+
if (this.reconnectInterval) {
|
|
3023
|
+
clearInterval(this.reconnectInterval);
|
|
3024
|
+
this.reconnectInterval = null;
|
|
3025
|
+
}
|
|
3026
|
+
this.rpcHandlerManager.onSocketConnect(candidate);
|
|
3027
|
+
this.receiveSync.invalidate();
|
|
3028
|
+
});
|
|
3029
|
+
candidate.on("disconnect", () => {
|
|
3030
|
+
this.rpcHandlerManager.onSocketDisconnect(candidate);
|
|
3031
|
+
this.startSmartReconnect();
|
|
3032
|
+
});
|
|
3033
|
+
candidate.on("connect_error", () => {
|
|
3034
|
+
this.rpcHandlerManager.onSocketDisconnect(candidate);
|
|
3035
|
+
this.startSmartReconnect();
|
|
3036
|
+
});
|
|
3037
|
+
this.receiveSync.invalidate();
|
|
3038
|
+
candidate.emit("release-handover-result", { result: "success", durationMs: Date.now() - startedAt });
|
|
3039
|
+
this.rpcHandlerManager.onSocketDisconnect(previous);
|
|
3040
|
+
previous.removeAllListeners("disconnect");
|
|
3041
|
+
previous.close();
|
|
3042
|
+
} catch (error) {
|
|
3043
|
+
if (this.socket === candidate) this.socket = previous;
|
|
3044
|
+
for (const [event, listeners] of transferredListeners) {
|
|
3045
|
+
candidate.removeAllListeners(event);
|
|
3046
|
+
for (const listener of listeners) previous.on(event, listener);
|
|
3047
|
+
}
|
|
3048
|
+
this.rpcHandlerManager.onSocketDisconnect(candidate);
|
|
3049
|
+
candidate.close();
|
|
3050
|
+
if (previous.connected) previous.emit("release-handover-result", { result: "failed", durationMs: Date.now() - startedAt });
|
|
3051
|
+
logger.debug(`[API] Release handover failed: ${error instanceof Error ? error.message : error}`);
|
|
3052
|
+
}
|
|
3053
|
+
}
|
|
2860
3054
|
onUserMessage(callback) {
|
|
2861
3055
|
this.pendingMessageCallback = callback;
|
|
2862
3056
|
while (this.pendingMessages.length > 0) {
|
|
@@ -3188,7 +3382,8 @@ class ApiSessionClient extends node_events.EventEmitter {
|
|
|
3188
3382
|
}
|
|
3189
3383
|
if (body.type === "assistant" && body.message?.usage) {
|
|
3190
3384
|
try {
|
|
3191
|
-
|
|
3385
|
+
const eventId = typeof body.uuid === "string" ? body.uuid : typeof body.message.id === "string" ? body.message.id : void 0;
|
|
3386
|
+
this.sendUsageData(body.message.usage, body.message.model, eventId);
|
|
3192
3387
|
} catch (error) {
|
|
3193
3388
|
logger.debug("[SOCKET] Failed to send usage data:", error);
|
|
3194
3389
|
}
|
|
@@ -3339,28 +3534,56 @@ class ApiSessionClient extends node_events.EventEmitter {
|
|
|
3339
3534
|
/**
|
|
3340
3535
|
* Send usage data to the server
|
|
3341
3536
|
*/
|
|
3342
|
-
sendUsageData(usage, model) {
|
|
3537
|
+
sendUsageData(usage, model, eventId) {
|
|
3538
|
+
if (eventId && this.seenClaudeUsageEvents.has(eventId)) return;
|
|
3539
|
+
if (eventId) this.seenClaudeUsageEvents.add(eventId);
|
|
3343
3540
|
const totalTokens = usage.input_tokens + usage.output_tokens + (usage.cache_creation_input_tokens || 0) + (usage.cache_read_input_tokens || 0);
|
|
3344
3541
|
const costs = calculateCost(usage, model);
|
|
3542
|
+
this.claudeUsageTotals.total += totalTokens;
|
|
3543
|
+
this.claudeUsageTotals.input += usage.input_tokens;
|
|
3544
|
+
this.claudeUsageTotals.output += usage.output_tokens;
|
|
3545
|
+
this.claudeUsageTotals.cache_creation += usage.cache_creation_input_tokens || 0;
|
|
3546
|
+
this.claudeUsageTotals.cache_read += usage.cache_read_input_tokens || 0;
|
|
3547
|
+
this.claudeUsageTotals.costTotal += costs.total;
|
|
3548
|
+
this.claudeUsageTotals.costInput += costs.input;
|
|
3549
|
+
this.claudeUsageTotals.costOutput += costs.output;
|
|
3345
3550
|
const usageReport = {
|
|
3551
|
+
// Keep the legacy key so upgrading replaces the old last-call row
|
|
3552
|
+
// instead of making old + new clients double-count one session.
|
|
3346
3553
|
key: "claude-session",
|
|
3347
3554
|
sessionId: this.sessionId,
|
|
3348
3555
|
tokens: {
|
|
3349
|
-
total:
|
|
3350
|
-
input:
|
|
3351
|
-
output:
|
|
3352
|
-
cache_creation:
|
|
3353
|
-
cache_read:
|
|
3556
|
+
total: this.claudeUsageTotals.total,
|
|
3557
|
+
input: this.claudeUsageTotals.input,
|
|
3558
|
+
output: this.claudeUsageTotals.output,
|
|
3559
|
+
cache_creation: this.claudeUsageTotals.cache_creation,
|
|
3560
|
+
cache_read: this.claudeUsageTotals.cache_read
|
|
3354
3561
|
},
|
|
3355
3562
|
cost: {
|
|
3356
|
-
total:
|
|
3357
|
-
input:
|
|
3358
|
-
output:
|
|
3563
|
+
total: this.claudeUsageTotals.costTotal,
|
|
3564
|
+
input: this.claudeUsageTotals.costInput,
|
|
3565
|
+
output: this.claudeUsageTotals.costOutput
|
|
3359
3566
|
}
|
|
3360
3567
|
};
|
|
3361
3568
|
logger.debugLargeJson("[SOCKET] Sending usage data:", usageReport);
|
|
3362
3569
|
this.socket.emit("usage-report", usageReport);
|
|
3363
3570
|
}
|
|
3571
|
+
/** Save a provider's cumulative session snapshot under one upsert key. */
|
|
3572
|
+
sendAgentUsageSnapshot(agent, rawUsage) {
|
|
3573
|
+
const tokens = normalizeAgentUsage(rawUsage);
|
|
3574
|
+
if (!tokens) return false;
|
|
3575
|
+
const usageReport = {
|
|
3576
|
+
key: `usage:${usageAgentKey(agent)}:session`,
|
|
3577
|
+
sessionId: this.sessionId,
|
|
3578
|
+
tokens,
|
|
3579
|
+
// Provider token-count events do not expose reliable billed cost.
|
|
3580
|
+
// The Web treats this sentinel as unknown, not as real $0.00.
|
|
3581
|
+
cost: { total: 0 }
|
|
3582
|
+
};
|
|
3583
|
+
logger.debugLargeJson("[SOCKET] Sending agent usage snapshot:", usageReport);
|
|
3584
|
+
this.socket.emit("usage-report", usageReport);
|
|
3585
|
+
return true;
|
|
3586
|
+
}
|
|
3364
3587
|
/**
|
|
3365
3588
|
* Returns the latest session metadata known to the client.
|
|
3366
3589
|
*/
|
|
@@ -13028,6 +13251,7 @@ class ApiMachineClient {
|
|
|
13028
13251
|
stopSessionHandler = null;
|
|
13029
13252
|
listTrackedSessionIds = null;
|
|
13030
13253
|
reconnectInterval = null;
|
|
13254
|
+
handoverInFlight = null;
|
|
13031
13255
|
// Terminals negotiated with `encStream` protect the live byte payload with
|
|
13032
13256
|
// the per-machine key. This keeps it opaque to passive storage/forwarding
|
|
13033
13257
|
// paths, but is not an operator trust boundary: the trusted relay can
|
|
@@ -13488,57 +13712,72 @@ class ApiMachineClient {
|
|
|
13488
13712
|
connect() {
|
|
13489
13713
|
const serverUrl = configuration.serverUrl.replace(/^http/, "ws");
|
|
13490
13714
|
logger.debug(`[API MACHINE] Connecting to ${serverUrl}`);
|
|
13491
|
-
this.socket =
|
|
13715
|
+
this.socket = this.createControlSocket();
|
|
13716
|
+
const controlSocket = this.socket;
|
|
13717
|
+
this.bindRealtimeHandlers(controlSocket, "control");
|
|
13718
|
+
controlSocket.on("connect", () => {
|
|
13719
|
+
if (this.socket !== controlSocket) return;
|
|
13720
|
+
this.activateControlSocket(controlSocket);
|
|
13721
|
+
});
|
|
13722
|
+
controlSocket.on("disconnect", (reason) => {
|
|
13723
|
+
logger.debug(`[API MACHINE] Disconnected from server \u2014 reason: ${reason}`);
|
|
13724
|
+
this.rpcHandlerManager.onSocketDisconnect(controlSocket);
|
|
13725
|
+
if (!this.relaySocket?.connected) this.webTerminal.resetSubscribers();
|
|
13726
|
+
this.stopKeepAlive();
|
|
13727
|
+
if (this.socket === controlSocket) this.startSmartReconnect();
|
|
13728
|
+
});
|
|
13729
|
+
this.bindControlDataHandlers(controlSocket);
|
|
13730
|
+
}
|
|
13731
|
+
createControlSocket(handover) {
|
|
13732
|
+
const serverUrl = configuration.serverUrl.replace(/^http/, "ws");
|
|
13733
|
+
return socket_ioClient.io(serverUrl, {
|
|
13492
13734
|
transports: ["websocket"],
|
|
13493
13735
|
auth: {
|
|
13494
13736
|
token: this.token,
|
|
13495
13737
|
clientType: "machine-scoped",
|
|
13496
13738
|
machineId: this.machine.id,
|
|
13497
|
-
happyClient: `cli-daemon/${configuration.currentCliVersion}
|
|
13739
|
+
happyClient: `cli-daemon/${configuration.currentCliVersion}`,
|
|
13740
|
+
...handover ? { handoverEpoch: handover.epoch } : {}
|
|
13498
13741
|
},
|
|
13742
|
+
...handover ? { query: { vh_slot: handover.candidateSlot }, forceNew: true } : {},
|
|
13499
13743
|
path: "/v1/updates",
|
|
13500
|
-
reconnection: false
|
|
13501
|
-
|
|
13502
|
-
this.bindRealtimeHandlers(this.socket, "control");
|
|
13503
|
-
this.socket.on("connect", () => {
|
|
13504
|
-
logger.debug("[API MACHINE] Connected to server");
|
|
13505
|
-
if (this.reconnectInterval) {
|
|
13506
|
-
clearInterval(this.reconnectInterval);
|
|
13507
|
-
this.reconnectInterval = null;
|
|
13508
|
-
}
|
|
13509
|
-
const initialTerminals = this.webTerminal.buildTerminalList();
|
|
13510
|
-
this.webTerminal.primeListSignature(initialTerminals);
|
|
13511
|
-
this.updateDaemonState((state) => {
|
|
13512
|
-
const now = Date.now();
|
|
13513
|
-
const currentState = withCurrentCliUpdateState(state, this.cliUpdateState);
|
|
13514
|
-
return {
|
|
13515
|
-
...currentState,
|
|
13516
|
-
status: "running",
|
|
13517
|
-
pid: process.pid,
|
|
13518
|
-
httpPort: this.machine.daemonState?.httpPort,
|
|
13519
|
-
startedAt: now,
|
|
13520
|
-
webTerminals: { updatedAt: now, terminals: initialTerminals },
|
|
13521
|
-
// B-084: closed records survive daemon restarts (persisted
|
|
13522
|
-
// in closed-terminals.json), so the connect snapshot ships
|
|
13523
|
-
// them too — not just the incremental pushes.
|
|
13524
|
-
closedTerminals: this.webTerminal.getClosedTerminals()
|
|
13525
|
-
};
|
|
13526
|
-
});
|
|
13527
|
-
this.webTerminal.startListTracking((terminals) => this.pushTerminalList(terminals));
|
|
13528
|
-
this.rpcHandlerManager.onSocketConnect(this.socket);
|
|
13529
|
-
this.syncResumeSessionRpcRegistration();
|
|
13530
|
-
this.startKeepAlive();
|
|
13531
|
-
void this.reconcileArchivedSessions();
|
|
13532
|
-
void this.refreshRelayConnection();
|
|
13744
|
+
reconnection: false,
|
|
13745
|
+
...handover ? { autoConnect: false } : {}
|
|
13533
13746
|
});
|
|
13534
|
-
|
|
13535
|
-
|
|
13536
|
-
|
|
13537
|
-
|
|
13538
|
-
this.
|
|
13539
|
-
this.
|
|
13747
|
+
}
|
|
13748
|
+
activateControlSocket(socket, rpcAlreadyRegistered = false) {
|
|
13749
|
+
logger.debug("[API MACHINE] Connected to server");
|
|
13750
|
+
if (this.reconnectInterval) {
|
|
13751
|
+
clearInterval(this.reconnectInterval);
|
|
13752
|
+
this.reconnectInterval = null;
|
|
13753
|
+
}
|
|
13754
|
+
const initialTerminals = this.webTerminal.buildTerminalList();
|
|
13755
|
+
this.webTerminal.primeListSignature(initialTerminals);
|
|
13756
|
+
this.updateDaemonState((state) => {
|
|
13757
|
+
const now = Date.now();
|
|
13758
|
+
const currentState = withCurrentCliUpdateState(state, this.cliUpdateState);
|
|
13759
|
+
return {
|
|
13760
|
+
...currentState,
|
|
13761
|
+
status: "running",
|
|
13762
|
+
pid: process.pid,
|
|
13763
|
+
httpPort: this.machine.daemonState?.httpPort,
|
|
13764
|
+
startedAt: now,
|
|
13765
|
+
webTerminals: { updatedAt: now, terminals: initialTerminals },
|
|
13766
|
+
// B-084: closed records survive daemon restarts (persisted
|
|
13767
|
+
// in closed-terminals.json), so the connect snapshot ships
|
|
13768
|
+
// them too — not just the incremental pushes.
|
|
13769
|
+
closedTerminals: this.webTerminal.getClosedTerminals()
|
|
13770
|
+
};
|
|
13540
13771
|
});
|
|
13541
|
-
this.
|
|
13772
|
+
this.webTerminal.startListTracking((terminals) => this.pushTerminalList(terminals));
|
|
13773
|
+
if (!rpcAlreadyRegistered) this.rpcHandlerManager.onSocketConnect(socket);
|
|
13774
|
+
this.syncResumeSessionRpcRegistration();
|
|
13775
|
+
this.startKeepAlive();
|
|
13776
|
+
void this.reconcileArchivedSessions();
|
|
13777
|
+
void this.refreshRelayConnection();
|
|
13778
|
+
}
|
|
13779
|
+
bindControlDataHandlers(socket) {
|
|
13780
|
+
socket.on("update", (data) => {
|
|
13542
13781
|
if (data.body.t === "update-machine" && data.body.machineId === this.machine.id) {
|
|
13543
13782
|
const update = data.body;
|
|
13544
13783
|
if (update.metadata) {
|
|
@@ -13555,13 +13794,83 @@ class ApiMachineClient {
|
|
|
13555
13794
|
logger.debug(`[API MACHINE] Received unknown update type: ${data.body.t}`);
|
|
13556
13795
|
}
|
|
13557
13796
|
});
|
|
13558
|
-
|
|
13797
|
+
socket.on("connect_error", (error) => {
|
|
13559
13798
|
logger.debug(`[API MACHINE] Connection error: ${error.message}`);
|
|
13560
|
-
this.startSmartReconnect();
|
|
13799
|
+
if (this.socket === socket) this.startSmartReconnect();
|
|
13561
13800
|
});
|
|
13562
|
-
|
|
13801
|
+
socket.io.on("error", (error) => {
|
|
13563
13802
|
logger.debug("[API MACHINE] Socket error:", error);
|
|
13564
13803
|
});
|
|
13804
|
+
socket.on("server-draining", (data) => {
|
|
13805
|
+
if (this.socket !== socket) return;
|
|
13806
|
+
const parsed = ReleaseDrainNoticeSchema.safeParse(data);
|
|
13807
|
+
if (!parsed.success || parsed.data.deadline <= Date.now()) return;
|
|
13808
|
+
void this.startReleaseHandover(parsed.data);
|
|
13809
|
+
});
|
|
13810
|
+
}
|
|
13811
|
+
startReleaseHandover(notice) {
|
|
13812
|
+
if (this.handoverInFlight) return this.handoverInFlight;
|
|
13813
|
+
this.handoverInFlight = this.releaseHandover(notice).finally(() => {
|
|
13814
|
+
this.handoverInFlight = null;
|
|
13815
|
+
});
|
|
13816
|
+
return this.handoverInFlight;
|
|
13817
|
+
}
|
|
13818
|
+
async releaseHandover(notice) {
|
|
13819
|
+
const startedAt = Date.now();
|
|
13820
|
+
const previous = this.socket;
|
|
13821
|
+
const candidate = this.createControlSocket(notice);
|
|
13822
|
+
let commandsTransferred = false;
|
|
13823
|
+
this.bindRpcRequestHandler(candidate, "control");
|
|
13824
|
+
const timeoutMs = Math.max(1, Math.min(1e4, notice.deadline - Date.now()));
|
|
13825
|
+
try {
|
|
13826
|
+
await new Promise((resolve, reject) => {
|
|
13827
|
+
const timer = setTimeout(() => reject(new Error("release handover timeout")), timeoutMs);
|
|
13828
|
+
candidate.once("connect", () => {
|
|
13829
|
+
clearTimeout(timer);
|
|
13830
|
+
resolve();
|
|
13831
|
+
});
|
|
13832
|
+
candidate.once("connect_error", (error) => {
|
|
13833
|
+
clearTimeout(timer);
|
|
13834
|
+
reject(error);
|
|
13835
|
+
});
|
|
13836
|
+
candidate.connect();
|
|
13837
|
+
});
|
|
13838
|
+
await this.rpcHandlerManager.onSocketConnectAndWait(candidate, timeoutMs);
|
|
13839
|
+
if (this.socket !== previous) {
|
|
13840
|
+
this.rpcHandlerManager.onSocketDisconnect(candidate);
|
|
13841
|
+
candidate.close();
|
|
13842
|
+
return;
|
|
13843
|
+
}
|
|
13844
|
+
this.unbindMachineCommandHandlers(previous);
|
|
13845
|
+
this.bindMachineCommandHandlers(candidate);
|
|
13846
|
+
commandsTransferred = true;
|
|
13847
|
+
this.bindControlDataHandlers(candidate);
|
|
13848
|
+
this.socket = candidate;
|
|
13849
|
+
candidate.on("connect", () => {
|
|
13850
|
+
if (this.socket === candidate) this.activateControlSocket(candidate);
|
|
13851
|
+
});
|
|
13852
|
+
candidate.on("disconnect", () => {
|
|
13853
|
+
this.rpcHandlerManager.onSocketDisconnect(candidate);
|
|
13854
|
+
if (!this.relaySocket?.connected) this.webTerminal.resetSubscribers();
|
|
13855
|
+
this.stopKeepAlive();
|
|
13856
|
+
if (this.socket === candidate) this.startSmartReconnect();
|
|
13857
|
+
});
|
|
13858
|
+
this.activateControlSocket(candidate, true);
|
|
13859
|
+
candidate.emit("release-handover-result", { result: "success", durationMs: Date.now() - startedAt });
|
|
13860
|
+
this.rpcHandlerManager.onSocketDisconnect(previous);
|
|
13861
|
+
previous.removeAllListeners("disconnect");
|
|
13862
|
+
previous.close();
|
|
13863
|
+
} catch (error) {
|
|
13864
|
+
if (this.socket === candidate) this.socket = previous;
|
|
13865
|
+
if (commandsTransferred) {
|
|
13866
|
+
this.unbindMachineCommandHandlers(candidate);
|
|
13867
|
+
this.bindMachineCommandHandlers(previous);
|
|
13868
|
+
}
|
|
13869
|
+
this.rpcHandlerManager.onSocketDisconnect(candidate);
|
|
13870
|
+
candidate.close();
|
|
13871
|
+
if (previous.connected) previous.emit("release-handover-result", { result: "failed", durationMs: Date.now() - startedAt });
|
|
13872
|
+
logger.debug(`[API MACHINE] Release handover failed: ${error instanceof Error ? error.message : error}`);
|
|
13873
|
+
}
|
|
13565
13874
|
}
|
|
13566
13875
|
startKeepAlive() {
|
|
13567
13876
|
this.stopKeepAlive();
|
|
@@ -13595,11 +13904,13 @@ class ApiMachineClient {
|
|
|
13595
13904
|
}, 2e4);
|
|
13596
13905
|
logger.debug("[API MACHINE] Keep-alive started (20s interval)");
|
|
13597
13906
|
}
|
|
13598
|
-
|
|
13907
|
+
bindRpcRequestHandler(socket, transport) {
|
|
13599
13908
|
socket.on("rpc-request", async (data, callback) => {
|
|
13600
13909
|
logger.debugLargeJson(`[API MACHINE] Received RPC request via ${transport}:`, data);
|
|
13601
13910
|
callback(await this.rpcHandlerManager.handleRequest(data));
|
|
13602
13911
|
});
|
|
13912
|
+
}
|
|
13913
|
+
bindMachineCommandHandlers(socket) {
|
|
13603
13914
|
socket.on("terminal-input", (data) => {
|
|
13604
13915
|
let payload = data.data;
|
|
13605
13916
|
if (data.enc) {
|
|
@@ -13617,6 +13928,16 @@ class ApiMachineClient {
|
|
|
13617
13928
|
logger.debug(`[API MACHINE] Server archived session ${data.sessionId}; local process stopped=${stopped}`);
|
|
13618
13929
|
});
|
|
13619
13930
|
}
|
|
13931
|
+
unbindMachineCommandHandlers(socket) {
|
|
13932
|
+
socket.removeAllListeners("terminal-input");
|
|
13933
|
+
socket.removeAllListeners("terminal-resize");
|
|
13934
|
+
socket.removeAllListeners("terminal-close");
|
|
13935
|
+
socket.removeAllListeners("session-archive");
|
|
13936
|
+
}
|
|
13937
|
+
bindRealtimeHandlers(socket, transport) {
|
|
13938
|
+
this.bindRpcRequestHandler(socket, transport);
|
|
13939
|
+
this.bindMachineCommandHandlers(socket);
|
|
13940
|
+
}
|
|
13620
13941
|
/** Reconcile commands missed while the daemon was offline. The server
|
|
13621
13942
|
* returns only ids owned by this account and durably archived in the DB. */
|
|
13622
13943
|
async reconcileArchivedSessions() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "very-happy-cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.77",
|
|
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.77",
|
|
117
|
+
"very-happy-tools-x64-darwin": "0.2.77",
|
|
118
|
+
"very-happy-tools-arm64-linux": "0.2.77",
|
|
119
|
+
"very-happy-tools-x64-linux": "0.2.77",
|
|
120
|
+
"very-happy-tools-arm64-win32": "0.2.77",
|
|
121
|
+
"very-happy-tools-x64-win32": "0.2.77"
|
|
122
122
|
},
|
|
123
123
|
"devDependencies": {
|
|
124
124
|
"@slopus/happy-wire": "workspace:*",
|