very-happy-cli 0.2.154 → 0.2.155

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 (55) hide show
  1. package/dist/{AcpBackend-Ckyadhf4.mjs → AcpBackend-DVB0nzgR.mjs} +2 -2
  2. package/dist/{AcpBackend-B14xNj5L.cjs → AcpBackend-DmFcRh54.cjs} +3 -3
  3. package/dist/{AcpSessionManager-CCHmPdrR.cjs → AcpSessionManager-CWaSYTnT.cjs} +1 -1
  4. package/dist/{AcpSessionManager-H8t4P-IW.mjs → AcpSessionManager-dGDgNb-p.mjs} +1 -1
  5. package/dist/agentGuidance-BWxFC6UL.mjs +1613 -0
  6. package/dist/agentGuidance-un7YPryB.cjs +1665 -0
  7. package/dist/{agentHome-B20RWdgE.mjs → agentHome-ByLb2HFg.mjs} +5 -5
  8. package/dist/{agentHome-BUsLTZnz.cjs → agentHome-aucT8BHw.cjs} +5 -5
  9. package/dist/{auto-BQnncDcs.mjs → auto-BQnD6gVx.mjs} +21 -21
  10. package/dist/{auto-IeldARBS.cjs → auto-P9RoJaY0.cjs} +24 -24
  11. package/dist/codex/happyMcpStdioBridge.cjs +6 -3
  12. package/dist/codex/happyMcpStdioBridge.mjs +6 -3
  13. package/dist/{config-B-ocQh5V.mjs → config-BPZ2Y6o8.mjs} +8 -8
  14. package/dist/{config-DM_xI9M6.cjs → config-BoePh9PX.cjs} +8 -8
  15. package/dist/{index-okLyUzeD.cjs → index--KCpVs1c.cjs} +7 -7
  16. package/dist/{index-BELV05ko.mjs → index-B4oXnbYw.mjs} +463 -889
  17. package/dist/{index-ZKuBjvHi.mjs → index-BjzdXb9x.mjs} +21 -13
  18. package/dist/{index-BUxBxSxJ.cjs → index-DRrLaljt.cjs} +21 -13
  19. package/dist/{index-CIx3248o.mjs → index-DVVRqaVG.mjs} +7 -7
  20. package/dist/{index-eCjkm8Y2.cjs → index-D_fYUxVg.cjs} +521 -960
  21. package/dist/index.cjs +6 -6
  22. package/dist/index.mjs +6 -6
  23. package/dist/{install-BZOdxfPd.cjs → install-CGigiocs.cjs} +1 -1
  24. package/dist/{install-CoLN3whR.mjs → install-DP5aPNEX.mjs} +1 -1
  25. package/dist/{installTerminalHooks-CA4OcAS7.cjs → installTerminalHooks-XrcPvfBs.cjs} +5 -5
  26. package/dist/{installTerminalHooks-Dde_8t36.mjs → installTerminalHooks-tIg7mANc.mjs} +5 -5
  27. package/dist/lib.cjs +2 -2
  28. package/dist/lib.mjs +2 -2
  29. package/dist/{limits-Dt7_unH4.mjs → limits-C_DW0M0q.mjs} +309 -13
  30. package/dist/{limits-CmzISka2.cjs → limits-DkfHJ3nS.cjs} +329 -12
  31. package/dist/{mcp-7iDHlyCI.cjs → mcp-BqlXl10F.cjs} +23 -23
  32. package/dist/{mcp-9Ui4nQZL.mjs → mcp-o7QglAQM.mjs} +17 -17
  33. package/dist/{piRuntime-BesGGPME.cjs → piRuntime-BxP4B_zY.cjs} +2 -2
  34. package/dist/{piRuntime-C4E4pgye.mjs → piRuntime-MjiJI-nV.mjs} +2 -2
  35. package/dist/{piTerminal-fUogwxYr.cjs → piTerminal-DYzRcwgb.cjs} +6 -6
  36. package/dist/{piTerminal-CCEGCQOY.mjs → piTerminal-Dpq6yqKU.mjs} +6 -6
  37. package/dist/{runGemini-CNPGSsSK.cjs → runGemini-8Pxh94Fm.cjs} +7 -7
  38. package/dist/{runGemini-BW-pEklU.mjs → runGemini-CDlcQ-OA.mjs} +6 -6
  39. package/dist/{runOpenClaw-Bsodkg_s.cjs → runOpenClaw-B5FSZIbK.cjs} +6 -6
  40. package/dist/{runOpenClaw-NRB6-_jU.mjs → runOpenClaw-YkhsgPs0.mjs} +5 -5
  41. package/dist/{send-Bd00QcHC.cjs → send-CwrYXTPr.cjs} +8 -42
  42. package/dist/{send-DYeSFg8s.mjs → send-DJStmLBv.mjs} +5 -39
  43. package/dist/{sessions-DlPOYKLU.mjs → sessions-JaVXlSqj.mjs} +88 -45
  44. package/dist/{sessions-RIFUoy_g.cjs → sessions-MX21NJXd.cjs} +99 -56
  45. package/dist/{spawn-D1bhyHbH.mjs → spawn-BaPnjClT.mjs} +22 -22
  46. package/dist/{spawn-DxrfHTF4.cjs → spawn-cIoglKVf.cjs} +30 -30
  47. package/dist/{teams-pfKHpkgL.mjs → teams-DXV5Pwwt.mjs} +8 -8
  48. package/dist/{teams-Cfy0VMCo.cjs → teams-Dqk7F3D-.cjs} +8 -8
  49. package/dist/{todo-B2CjDg_V.mjs → todo-2Z1REhfx.mjs} +8 -6
  50. package/dist/{todo-B4cBFzHB.cjs → todo-Buuqn9gm.cjs} +8 -6
  51. package/dist/{types-D5RWyBE5.mjs → types-CkbfdRzq.mjs} +4 -282
  52. package/dist/{types-Bd_TGr0F.cjs → types-a2xsH2UI.cjs} +16 -311
  53. package/package.json +7 -7
  54. package/dist/agentGuidance-DUBXppv-.cjs +0 -554
  55. package/dist/agentGuidance-TLYQxsm2.mjs +0 -530
@@ -1,14 +1,14 @@
1
1
  import{createRequire as _pkgrollCR}from"node:module";const require=_pkgrollCR(import.meta.url);import chalk from 'chalk';
2
2
  import os, { homedir, userInfo, tmpdir } from 'node:os';
3
3
  import { randomUUID, randomInt, randomBytes, createHash as createHash$1, timingSafeEqual } from 'node:crypto';
4
- import { v as notifyDaemonSessionEvent, g as getProjectPath, w as initializeSandbox, x as wrapCommand, u as contentLogMetadata, I as InvalidateSync, R as RawJSONLinesSchema, c as delay, y as AsyncLock, M as MAX_CHAT_ATTACHMENT_SOURCE_BYTES, z as notifyDaemonClaudeAuthFailed, B as commandOnPath, j as checkIfDaemonRunningAndCleanupStaleState, D as deriveLocalCliUpdateSummary, E as isClaudeSessionId, G as getLocalHappyAgentCredentialPath, H as readLocalHappyAgentCredentials, J as parseExactVersion, K as compareExactVersions, L as extractCompleteLines, N as decideBackfill, O as parseTerminalHookPayload, P as tmuxArgs, Q as scrubTmuxClientEnv, S as decideMirrorBinding, T as MIRROR_BACKFILL_LINES_DEFAULT, U as mirrorLineKey, V as mirrorLocalId, W as listDaemonSessions, k as stopDaemonSession, X as detectCLIAvailability, Y as detectResumeSupport, Z as refreshAgentHomes, d as describeAgentHome, _ as isDaemonRunningCurrentlyInstalledHappyVersion, $ as stopDaemon, a0 as TurnActivityTracker, a1 as summarizeSpawnSessionForLog, a2 as locateClaudeConversation, a3 as agentHomeSpawnEnv, a4 as shellescape, A as ApiClient, a5 as fetchCliUpdateState, a6 as resolveCliUpdateCheckInterval, a7 as DEFAULT_CLI_UPDATE_CHECK_INTERVAL_MS, a8 as cleanupDaemonState, a9 as resumeDaemonSession, i as isValidSessionId, s as spawnDaemonSession, aa as listVhTerminals, ab as isValidTerminalId, ac as readVhTerminal, ad as sendToVhTerminal, ae as TITLE_PROMPT_PREFIX, af as BOARD_ANALYZER_PROMPT_PREFIX, o as connectionState, ag as startOfflineReconnection, q as notifyDaemonSessionStarted, C as CodexAppServerClient } from './types-D5RWyBE5.mjs';
5
- import { l as logger, k as BUILTIN_TODO_DISCOVERY, U as STREAM_DELTA_MAX_CHARS, V as ensurePrivateDirectory, W as hardenPrivateFile, c as configuration, p as packageJson, r as readSettings, X as readCredentials, L as credentialRelayProblem, F as readDaemonState, Y as AGENT_VERSION_SOURCES, j as encodeBase64, Z as readSettingsOutcome, u as updateSettings, _ as encodeBase64Url, E as decodeBase64, $ as writeCredentialsLegacy, a0 as writeCredentialsDataKey, f as readCredentialsForConfiguredRelay, z as encrypt, d as readPersistedSessions, w as writePrivateFileSync, a1 as decryptWithDataKey, a2 as decryptLegacy, a3 as persistSession, D as decrypt, m as ensurePrivateDirectorySync, a4 as getAgentCodeDefaults, a5 as normalizeAgentKey, a6 as isClaudeOpus55Model, a7 as supportsClaudeOpus55, a8 as AUTOMATION_SUMMARY_MAX_CHARS, a9 as AUTOMATION_CLAIM_MAX_RUNS, aa as AUTOMATION_DEFAULT_LEASE_MS, i as isAutomationRunTerminal, ab as acquireDaemonLock, ac as deletePersistedSessions, ad as writeDaemonState, ae as writeSettings, af as releaseDaemonLock, ag as ContextUsageSchema, C as CLIPBOARD_TOOL_NAME, a as CLIPBOARD_TOOL_TITLE, b as CLIPBOARD_TOOL_DESCRIPTION, h as CLIPBOARD_MAX_BYTES, ah as clearCredentials, ai as clearMachineId, aj as SandboxConfigSchema, e as createEnvelope, ak as getLatestDaemonLog } from './limits-Dt7_unH4.mjs';
4
+ import { g as getProjectPath, k as initializeSandbox, w as wrapCommand, j as contentLogMetadata, I as InvalidateSync, R as RawJSONLinesSchema, l as AsyncLock, M as MAX_CHAT_ATTACHMENT_SOURCE_BYTES, m as commandOnPath, n as deriveLocalCliUpdateSummary, o as isClaudeSessionId, q as getLocalHappyAgentCredentialPath, s as readLocalHappyAgentCredentials, t as parseExactVersion, u as compareExactVersions, v as extractCompleteLines, x as decideBackfill, y as parseTerminalHookPayload, z as tmuxArgs, B as scrubTmuxClientEnv, D as decideMirrorBinding, E as MIRROR_BACKFILL_LINES_DEFAULT, G as mirrorLineKey, H as mirrorLocalId, J as detectCLIAvailability, K as detectResumeSupport, L as refreshAgentHomes, d as describeAgentHome, T as TurnActivityTracker, N as summarizeSpawnSessionForLog, O as locateClaudeConversation, P as agentHomeSpawnEnv, Q as shellescape, A as ApiClient, S as fetchCliUpdateState, U as resolveCliUpdateCheckInterval, V as DEFAULT_CLI_UPDATE_CHECK_INTERVAL_MS, W as listVhTerminals, X as readVhTerminal, Y as sendToVhTerminal, Z as TITLE_PROMPT_PREFIX, _ as BOARD_ANALYZER_PROMPT_PREFIX, h as connectionState, $ as startOfflineReconnection, C as CodexAppServerClient } from './types-CkbfdRzq.mjs';
5
+ import { l as logger, a7 as notifyDaemonSessionEvent, x as BUILTIN_TODO_DISCOVERY, g as delay, a8 as STREAM_DELTA_MAX_CHARS, a9 as ensurePrivateDirectory, aa as hardenPrivateFile, c as configuration, ab as notifyDaemonClaudeAuthFailed, F as packageJson, r as readSettings, ac as readCredentials, Z as credentialRelayProblem, k as checkIfDaemonRunningAndCleanupStaleState, ad as readDaemonState, ae as AGENT_VERSION_SOURCES, w as encodeBase64, af as readSettingsOutcome, u as updateSettings, ag as encodeBase64Url, M as decodeBase64, ah as writeCredentialsLegacy, ai as writeCredentialsDataKey, d as readPersistedSessions, E as writePrivateFileSync, aj as decryptWithDataKey, ak as decryptLegacy, al as persistSession, z as ensurePrivateDirectorySync, am as getAgentCodeDefaults, an as normalizeAgentKey, ao as isClaudeOpus55Model, ap as supportsClaudeOpus55, aq as AUTOMATION_SUMMARY_MAX_CHARS, ar as AUTOMATION_CLAIM_MAX_RUNS, as as AUTOMATION_DEFAULT_LEASE_MS, f as isAutomationRunTerminal, at as isDaemonRunningCurrentlyInstalledHappyVersion, au as stopDaemon, av as acquireDaemonLock, aw as deletePersistedSessions, N as decrypt, ax as writeDaemonState, ay as writeSettings, az as cleanupDaemonState, aA as releaseDaemonLock, i as isValidSessionId, m as spawnDaemonSession, a1 as isValidTerminalId, aB as ContextUsageSchema, C as CLIPBOARD_TOOL_NAME, a as CLIPBOARD_TOOL_TITLE, b as CLIPBOARD_TOOL_DESCRIPTION, o as CLIPBOARD_MAX_BYTES, v as notifyDaemonSessionStarted, y as reportSessionEditToDaemon, aC as clearCredentials, aD as clearMachineId, h as readCredentialsForConfiguredRelay, aE as SandboxConfigSchema, e as createEnvelope, O as listDaemonSessions, n as stopDaemonSession, aF as getLatestDaemonLog } from './limits-C_DW0M0q.mjs';
6
6
  import spawn$1, { spawn } from 'cross-spawn';
7
7
  import path, { join, resolve, basename, delimiter, isAbsolute, dirname, sep, normalize } from 'node:path';
8
8
  import { createInterface } from 'node:readline';
9
9
  import { existsSync, readFileSync, readdirSync, statSync, mkdirSync, realpathSync, unlinkSync, renameSync, writeFileSync, accessSync, constants as constants$1, openSync, fsyncSync, closeSync, rmSync as rmSync$1, chmodSync as chmodSync$1 } from 'node:fs';
10
10
  import { p as projectPath } from './terminalTitleSuggest-DKgX3osW.mjs';
11
- import { j as trimIdent, k as AGENT_GUIDANCE, l as createAutomationsClient, i as isAutomationsUnavailable, m as AutomationsRequestError, t as teamScopePath, T as TeamConnectionPreflightError, h as createTeamsClient, P as PREVIEW_TOOL_NAME, R as REPORT_PROGRESS_TOOL_NAME, n as TEAM_TOOL_NAMES, o as AUTOMATION_TOOL_NAMES, r as registerTeamsTools, c as registerAutomationTools, a as PREVIEW_TOOL_TITLE, b as PREVIEW_TOOL_DESCRIPTION, p as REPORT_PROGRESS_TOOL_TITLE, q as REPORT_PROGRESS_TOOL_DESCRIPTION } from './agentGuidance-TLYQxsm2.mjs';
11
+ import { E as trimIdent, F as AGENT_GUIDANCE, G as formatEditConflictNotice, z as sendUserMessage, S as SESSION_PEER_SENT_FROM, x as readSessionMetadata, y as waitForSessionKey, k as archiveSession, H as createAutomationsClient, t as isAutomationsUnavailable, I as AutomationsRequestError, J as analyzeLatestTurn, K as readSessionLog$1, L as TURN_WINDOW, N as userPromptText, v as teamScopePath, O as TeamConnectionPreflightError, C as createTeamsClient, e as listSessions, h as readSessionTranscript, n as deliverToSession, o as sessionWebUrl, i as stopSession, P as PREVIEW_TOOL_NAME, R as REPORT_PROGRESS_TOOL_NAME, Q as TEAM_TOOL_NAMES, U as AUTOMATION_TOOL_NAMES, V as SESSION_PEER_TOOL_NAMES, r as registerTeamsTools, c as registerAutomationTools, d as registerSessionPeerTools, W as createSessionPeerToolExecutor, a as PREVIEW_TOOL_TITLE, b as PREVIEW_TOOL_DESCRIPTION, X as REPORT_PROGRESS_TOOL_TITLE, Y as REPORT_PROGRESS_TOOL_DESCRIPTION } from './agentGuidance-BWxFC6UL.mjs';
12
12
  import { readFile, writeFile, rename, unlink, mkdir, access as access$1, symlink, rm, readlink, lstat, constants, stat, open as open$1, appendFile } from 'node:fs/promises';
13
13
  import fs, { watch, access } from 'fs/promises';
14
14
  import { useStdout, useInput, Box, Text, render } from 'ink';
@@ -16,7 +16,7 @@ import React, { useState, useRef, useEffect, useCallback } from 'react';
16
16
  import { query as query$1, AbortError } from '@anthropic-ai/claude-agent-sdk';
17
17
  import axios, { AxiosError } from 'axios';
18
18
  import 'node:events';
19
- import { io } from 'socket.io-client';
19
+ import 'socket.io-client';
20
20
  import tweetnacl from 'tweetnacl';
21
21
  import os$1, { homedir as homedir$1 } from 'os';
22
22
  import { spawn as spawn$2, execSync as execSync$1, exec as exec$1 } from 'child_process';
@@ -878,7 +878,7 @@ async function createSessionScanner(opts) {
878
878
  let processedMessageKeys = /* @__PURE__ */ new Set();
879
879
  let deadSessions = /* @__PURE__ */ new Set();
880
880
  if (opts.sessionId) {
881
- let messages = await readSessionLog$1(projectDir, opts.sessionId);
881
+ let messages = await readSessionLog(projectDir, opts.sessionId);
882
882
  logger.debug(`[SESSION_SCANNER] Marking ${messages.length} existing messages as processed from session ${opts.sessionId}`);
883
883
  for (let m of messages) {
884
884
  processedMessageKeys.add(messageKey(m));
@@ -901,7 +901,7 @@ async function createSessionScanner(opts) {
901
901
  }
902
902
  }
903
903
  for (let session of sessions) {
904
- const sessionMessages = await readSessionLog$1(projectDir, session);
904
+ const sessionMessages = await readSessionLog(projectDir, session);
905
905
  let skipped = 0;
906
906
  let sent = 0;
907
907
  for (let file of sessionMessages) {
@@ -978,7 +978,7 @@ async function createSessionScanner(opts) {
978
978
  return;
979
979
  }
980
980
  if (options?.treatExistingAsProcessed) {
981
- const existing = await readSessionLog$1(projectDir, sessionId);
981
+ const existing = await readSessionLog(projectDir, sessionId);
982
982
  logger.debug(`[SESSION_SCANNER] Pre-marking ${existing.length} existing messages as processed for new session ${sessionId}`);
983
983
  for (const m of existing) {
984
984
  processedMessageKeys.add(messageKey(m));
@@ -1006,7 +1006,7 @@ function messageKey(message) {
1006
1006
  throw Error();
1007
1007
  }
1008
1008
  }
1009
- async function readSessionLog$1(projectDir, sessionId) {
1009
+ async function readSessionLog(projectDir, sessionId) {
1010
1010
  const expectedSessionFile = join(projectDir, `${sessionId}.jsonl`);
1011
1011
  logger.debug(`[SESSION_SCANNER] Reading session file: ${expectedSessionFile}`);
1012
1012
  let file;
@@ -5380,8 +5380,8 @@ ${typeLabels[type] || type}:`));
5380
5380
  }
5381
5381
  console.log(chalk.gray(" An external claude command is only needed for native terminal/mirror use."));
5382
5382
  try {
5383
- const { resolveAgentHomesForCli } = await import('./agentHome-B20RWdgE.mjs');
5384
- const { describeAgentHome } = await import('./types-D5RWyBE5.mjs').then(function (n) { return n.ah; });
5383
+ const { resolveAgentHomesForCli } = await import('./agentHome-ByLb2HFg.mjs');
5384
+ const { describeAgentHome } = await import('./types-CkbfdRzq.mjs').then(function (n) { return n.a0; });
5385
5385
  const homes = await resolveAgentHomesForCli();
5386
5386
  console.log(chalk.green(`\u2713 Claude config dir: ${describeAgentHome(homes.claudeConfigDir)}`));
5387
5387
  console.log(chalk.green(`\u2713 Codex home: ${describeAgentHome(homes.codexHome)}`));
@@ -6063,59 +6063,6 @@ function formatAssistantReportMessage(title, sessionId, event) {
6063
6063
  return `[\u7CFB\u7EDF\u901A\u62A5] \u4F1A\u8BDD\u300C${title}\u300D${state}\uFF08${sessionId}\uFF09\u3002\u8BF7\u7528 session_read \u6838\u5B9E\u7ED3\u679C\uFF0C\u5E76\u5411\u7528\u6237\u53E3\u5934\u6C47\u62A5\u4E00\u53E5\u7ED3\u8BBA\uFF1B\u591A\u6761\u901A\u62A5\u63A5\u8FD1\u65F6\u5408\u5E76\u6C47\u62A5\u3002`;
6064
6064
  }
6065
6065
 
6066
- function sessionWebUrl(sessionId) {
6067
- return `${configuration.webappUrl.replace(/\/+$/, "")}/session/${sessionId}`;
6068
- }
6069
- async function waitForSessionKey(sessionId, timeoutMs) {
6070
- const deadline = Date.now() + timeoutMs;
6071
- while (true) {
6072
- const entry = readPersistedSessions()[sessionId];
6073
- if (entry) return entry;
6074
- if (Date.now() >= deadline) {
6075
- throw new Error(
6076
- `Session ${sessionId} has no encryption key in ${configuration.sessionsFile}. Either it was not spawned by this machine's daemon, or the daemon is too old to persist session keys.`
6077
- );
6078
- }
6079
- await delay(200);
6080
- }
6081
- }
6082
- async function sendUserMessage(sessionId, persisted, text, client, options = {}) {
6083
- const credentials = await readCredentialsForConfiguredRelay();
6084
- if (!credentials) {
6085
- throw new Error("Not authenticated. Run `very-happy auth login` first.");
6086
- }
6087
- const envelope = {
6088
- role: "user",
6089
- content: {
6090
- type: "text",
6091
- text
6092
- },
6093
- meta: {
6094
- sentFrom: options.sentFrom ?? "cli",
6095
- ...options.model !== void 0 ? { model: options.model } : {},
6096
- ...options.permissionMode !== void 0 ? { permissionMode: options.permissionMode } : {},
6097
- ...options.effort !== void 0 ? { effort: options.effort } : {}
6098
- }
6099
- };
6100
- const encrypted = encodeBase64(encrypt(
6101
- decodeBase64(persisted.encryptionKey),
6102
- persisted.encryptionVariant,
6103
- envelope
6104
- ));
6105
- await axios.post(
6106
- `${configuration.serverUrl}/v3/sessions/${encodeURIComponent(sessionId)}/messages`,
6107
- { messages: [{ content: encrypted, localId: options.localId ?? randomUUID() }] },
6108
- {
6109
- headers: {
6110
- "Authorization": `Bearer ${credentials.token}`,
6111
- "Content-Type": "application/json",
6112
- "X-Happy-Client": `${client}/${configuration.currentCliVersion}`
6113
- },
6114
- timeout: 3e4
6115
- }
6116
- );
6117
- }
6118
-
6119
6066
  const ALLOWED_SPAWN_PERMISSION_MODES = [
6120
6067
  "default",
6121
6068
  "acceptEdits",
@@ -6271,7 +6218,9 @@ function startDaemonControlServer({
6271
6218
  pushClipboard,
6272
6219
  pushFilePreview,
6273
6220
  onTerminalHook,
6274
- setTerminalTitle
6221
+ setTerminalTitle,
6222
+ onSessionEdit,
6223
+ listPeers
6275
6224
  }) {
6276
6225
  return new Promise((resolve) => {
6277
6226
  const app = fastify({
@@ -6594,6 +6543,45 @@ function startDaemonControlServer({
6594
6543
  }
6595
6544
  return { status: "ok" };
6596
6545
  });
6546
+ typed.post("/session-edit", {
6547
+ schema: {
6548
+ body: z.object({
6549
+ sessionId: z.string().regex(/^[a-zA-Z0-9_-]{1,128}$/),
6550
+ path: z.string().min(1).max(4096),
6551
+ tool: z.string().min(1).max(64),
6552
+ cwd: z.string().max(4096).optional(),
6553
+ /** When the runner saw the call (ms). Clamped to now by the daemon;
6554
+ * older than the conflict window = ignored (replayed history). */
6555
+ at: z.number().int().nonnegative().optional()
6556
+ }),
6557
+ response: { 200: z.object({ status: z.literal("ok") }) }
6558
+ }
6559
+ }, async (request) => {
6560
+ try {
6561
+ onSessionEdit?.(request.body);
6562
+ } catch (error) {
6563
+ logger.debug("[CONTROL SERVER] session-edit handler failed:", error);
6564
+ }
6565
+ return { status: "ok" };
6566
+ });
6567
+ typed.post("/peers", {
6568
+ schema: {
6569
+ response: {
6570
+ 200: z.object({
6571
+ sessions: z.array(z.object({
6572
+ sessionId: z.string(),
6573
+ kind: z.enum(["managed", "mirror"]),
6574
+ pid: z.number().optional(),
6575
+ cwd: z.string().optional(),
6576
+ flavor: z.string().optional(),
6577
+ title: z.string().optional(),
6578
+ variant: z.string().optional(),
6579
+ edits: z.array(z.object({ path: z.string(), tool: z.string(), at: z.number() }))
6580
+ }))
6581
+ })
6582
+ }
6583
+ }
6584
+ }, async () => ({ sessions: listPeers ? listPeers() : [] }));
6597
6585
  typed.post("/stop", {
6598
6586
  schema: {
6599
6587
  response: {
@@ -6629,6 +6617,273 @@ function startDaemonControlServer({
6629
6617
  });
6630
6618
  }
6631
6619
 
6620
+ const EDIT_CONFLICT_WINDOW_MS = 30 * 6e4;
6621
+ function pairKey$1(a, b, path) {
6622
+ return a < b ? `${a}\0${b}\0${path}` : `${b}\0${a}\0${path}`;
6623
+ }
6624
+ class EditConflictTracker {
6625
+ windowMs;
6626
+ maxPaths;
6627
+ /** real path → sessionId → latest edit */
6628
+ byPath = /* @__PURE__ */ new Map();
6629
+ /** (sorted pair, path) → when the notice went out */
6630
+ notified = /* @__PURE__ */ new Map();
6631
+ constructor(options = {}) {
6632
+ this.windowMs = options.windowMs ?? EDIT_CONFLICT_WINDOW_MS;
6633
+ this.maxPaths = options.maxPaths ?? 5e3;
6634
+ }
6635
+ /**
6636
+ * Remember an edit and return the peers to notify about it. `isLive`
6637
+ * decides whether another session still counts (dead ones are dropped
6638
+ * from the table on the spot).
6639
+ */
6640
+ record(edit, isLive) {
6641
+ this.prune(edit.at);
6642
+ let sessions = this.byPath.get(edit.path);
6643
+ if (!sessions) {
6644
+ sessions = /* @__PURE__ */ new Map();
6645
+ this.byPath.set(edit.path, sessions);
6646
+ }
6647
+ const conflicts = [];
6648
+ for (const [sessionId, peer] of sessions) {
6649
+ if (sessionId === edit.sessionId) continue;
6650
+ if (!isLive(sessionId)) {
6651
+ sessions.delete(sessionId);
6652
+ continue;
6653
+ }
6654
+ if (edit.at - peer.at > this.windowMs) continue;
6655
+ const key = pairKey$1(edit.sessionId, sessionId, edit.path);
6656
+ const notifiedAt = this.notified.get(key);
6657
+ if (notifiedAt !== void 0 && edit.at - notifiedAt <= this.windowMs) continue;
6658
+ this.notified.set(key, edit.at);
6659
+ conflicts.push({ editor: edit, peer });
6660
+ }
6661
+ sessions.set(edit.sessionId, edit);
6662
+ return conflicts;
6663
+ }
6664
+ /** Paths this session edited inside the window, newest first. */
6665
+ editsOf(sessionId, now, limit = 20) {
6666
+ const out = [];
6667
+ for (const [path, sessions] of this.byPath) {
6668
+ const edit = sessions.get(sessionId);
6669
+ if (edit && now - edit.at <= this.windowMs) out.push({ path, tool: edit.tool, at: edit.at });
6670
+ }
6671
+ return out.sort((a, b) => b.at - a.at).slice(0, limit);
6672
+ }
6673
+ /** Session ended: nothing it did counts any more. */
6674
+ forget(sessionId) {
6675
+ for (const [path, sessions] of this.byPath) {
6676
+ sessions.delete(sessionId);
6677
+ if (sessions.size === 0) this.byPath.delete(path);
6678
+ }
6679
+ for (const key of this.notified.keys()) {
6680
+ const [a, b] = key.split("\0");
6681
+ if (a === sessionId || b === sessionId) this.notified.delete(key);
6682
+ }
6683
+ }
6684
+ /** Drop everything older than the window (and cap the table size). */
6685
+ prune(now) {
6686
+ for (const [path, sessions] of this.byPath) {
6687
+ for (const [sessionId, edit] of sessions) if (now - edit.at > this.windowMs) sessions.delete(sessionId);
6688
+ if (sessions.size === 0) this.byPath.delete(path);
6689
+ }
6690
+ for (const [key, at] of this.notified) if (now - at > this.windowMs) this.notified.delete(key);
6691
+ if (this.byPath.size > this.maxPaths) {
6692
+ const oldest = [...this.byPath.entries()].map(([path, sessions]) => ({ path, at: Math.max(...[...sessions.values()].map((e) => e.at)) })).sort((a, b) => a.at - b.at);
6693
+ for (const entry of oldest.slice(0, this.byPath.size - this.maxPaths)) this.byPath.delete(entry.path);
6694
+ }
6695
+ }
6696
+ /** Test / diagnostics helper. */
6697
+ size() {
6698
+ return this.byPath.size;
6699
+ }
6700
+ }
6701
+ function normalizeEditPath(path, cwd, realpath = realpathSync.native ?? realpathSync) {
6702
+ const absolute = isAbsolute(path) ? resolve(path) : resolve(cwd, path);
6703
+ let existing = absolute;
6704
+ const tail = [];
6705
+ for (let depth = 0; depth < 64; depth++) {
6706
+ try {
6707
+ const real = realpath(existing);
6708
+ return tail.length ? join(real, ...tail) : real;
6709
+ } catch {
6710
+ const parent = dirname(existing);
6711
+ if (parent === existing) break;
6712
+ tail.unshift(existing.slice(parent.length).replace(new RegExp(`^\\${sep}`), ""));
6713
+ existing = parent;
6714
+ }
6715
+ }
6716
+ return absolute;
6717
+ }
6718
+
6719
+ const CONFLICT_COALESCE_MS = 1e4;
6720
+ const MAX_NOTICES_PER_PAIR = 5;
6721
+ function pairKey(a, b) {
6722
+ return a < b ? `${a}\0${b}` : `${b}\0${a}`;
6723
+ }
6724
+ function senderOf(info, sessionId) {
6725
+ return { sessionId, title: info?.title, flavor: info?.flavor, cwd: info?.cwd };
6726
+ }
6727
+ function createPeerCoordinator(deps) {
6728
+ const now = deps.now ?? (() => Date.now());
6729
+ const windowMs = deps.windowMs ?? EDIT_CONFLICT_WINDOW_MS;
6730
+ const coalesceMs = deps.coalesceMs ?? CONFLICT_COALESCE_MS;
6731
+ const maxNoticesPerPair = deps.maxNoticesPerPair ?? MAX_NOTICES_PER_PAIR;
6732
+ const tracker = new EditConflictTracker({ windowMs });
6733
+ const readPersisted = deps.readPersisted ?? readPersistedSessions;
6734
+ const send = deps.send ?? sendUserMessage;
6735
+ const normalize = deps.normalize ?? normalizeEditPath;
6736
+ const liveTitle = deps.liveTitle ?? (async (sessionId, persisted) => (await readSessionMetadata(sessionId, persisted))?.summary?.text);
6737
+ const schedule = deps.schedule ?? ((fn, ms) => {
6738
+ const t = setTimeout(fn, ms);
6739
+ t.unref?.();
6740
+ return t;
6741
+ });
6742
+ let getMirrors = deps.getMirrors ?? (() => []);
6743
+ const pending = /* @__PURE__ */ new Map();
6744
+ const noticesSent = /* @__PURE__ */ new Map();
6745
+ let inflight = Promise.resolve();
6746
+ const snapshot = () => {
6747
+ const at = now();
6748
+ const out = /* @__PURE__ */ new Map();
6749
+ for (const child of deps.getChildren()) {
6750
+ const id = child.happySessionId;
6751
+ if (!id || out.has(id)) continue;
6752
+ const meta = child.happySessionMetadataFromLocalWebhook;
6753
+ out.set(id, {
6754
+ sessionId: id,
6755
+ kind: "managed",
6756
+ pid: child.pid,
6757
+ ...meta?.path ? { cwd: meta.path } : {},
6758
+ ...meta?.flavor ? { flavor: meta.flavor } : {},
6759
+ ...meta?.summary?.text ? { title: meta.summary.text } : {},
6760
+ ...meta?.variant ?? child.variant ? { variant: meta?.variant ?? child.variant } : {},
6761
+ edits: tracker.editsOf(id, at)
6762
+ });
6763
+ }
6764
+ for (const mirror of getMirrors()) {
6765
+ if (out.has(mirror.sessionId)) continue;
6766
+ out.set(mirror.sessionId, {
6767
+ sessionId: mirror.sessionId,
6768
+ kind: "mirror",
6769
+ flavor: "terminal-mirror",
6770
+ ...mirror.cwd ? { cwd: mirror.cwd } : {},
6771
+ ...mirror.title ? { title: mirror.title } : {},
6772
+ edits: tracker.editsOf(mirror.sessionId, at)
6773
+ });
6774
+ }
6775
+ return out;
6776
+ };
6777
+ const deliver = async (recipient, peer, peerId, paths, peerEditedAgoMs, noticeId) => {
6778
+ if (recipient.kind === "mirror") return;
6779
+ const persisted = readPersisted()[recipient.sessionId];
6780
+ if (!persisted) {
6781
+ logger.debug(`[PEERS] no key for ${recipient.sessionId}; conflict notice ${noticeId} not delivered`);
6782
+ return;
6783
+ }
6784
+ const peerSender = senderOf(peer, peerId);
6785
+ if (!peerSender.title && peer?.kind === "managed") {
6786
+ const peerKey = readPersisted()[peer.sessionId];
6787
+ if (peerKey) {
6788
+ try {
6789
+ peerSender.title = await liveTitle(peer.sessionId, peerKey);
6790
+ } catch {
6791
+ }
6792
+ }
6793
+ }
6794
+ const text = formatEditConflictNotice({ id: noticeId, path: paths[0], paths, peer: peerSender, peerEditedAgoMs, windowMs });
6795
+ try {
6796
+ await send(recipient.sessionId, persisted, text, "edit-conflict", {
6797
+ sentFrom: SESSION_PEER_SENT_FROM,
6798
+ delivery: "steer",
6799
+ localId: `edit-conflict-${noticeId}-${recipient.sessionId}`
6800
+ });
6801
+ logger.debug(`[PEERS] conflict notice ${noticeId} \u2192 ${recipient.sessionId} (${paths.length} file(s), first ${paths[0]}, peer ${peerId})`);
6802
+ } catch (error) {
6803
+ logger.debug(`[PEERS] conflict notice ${noticeId} \u2192 ${recipient.sessionId} failed:`, error);
6804
+ }
6805
+ };
6806
+ const flushPair = async (key) => {
6807
+ const pair = pending.get(key);
6808
+ if (!pair) return;
6809
+ pending.delete(key);
6810
+ const at = now();
6811
+ const sent = (noticesSent.get(key) ?? []).filter((t) => at - t <= windowMs);
6812
+ if (sent.length >= maxNoticesPerPair) {
6813
+ noticesSent.set(key, sent);
6814
+ logger.debug(`[PEERS] pair ${pair.a}/${pair.b}: ${maxNoticesPerPair} notices already sent this window; ${pair.paths.size} conflicting path(s) not announced`);
6815
+ return;
6816
+ }
6817
+ sent.push(at);
6818
+ noticesSent.set(key, sent);
6819
+ const sessions = snapshot();
6820
+ const a = sessions.get(pair.a);
6821
+ const b = sessions.get(pair.b);
6822
+ const paths = [...pair.paths.keys()];
6823
+ const noticeId = randomUUID().slice(0, 8);
6824
+ const latestBy = (sessionId) => Math.max(...[...pair.paths.values()].map((c) => c.editor.sessionId === sessionId ? c.editor.at : c.peer.at));
6825
+ const agoFor = (recipientId, otherId) => Math.max(0, latestBy(recipientId) - latestBy(otherId));
6826
+ if (a) await deliver(a, b, pair.b, paths, agoFor(pair.a, pair.b), noticeId);
6827
+ if (b) await deliver(b, a, pair.a, paths, agoFor(pair.b, pair.a), noticeId);
6828
+ };
6829
+ const enqueue = (conflict) => {
6830
+ const key = pairKey(conflict.editor.sessionId, conflict.peer.sessionId);
6831
+ let pair = pending.get(key);
6832
+ if (!pair) {
6833
+ pair = { a: conflict.editor.sessionId, b: conflict.peer.sessionId, paths: /* @__PURE__ */ new Map(), timer: null };
6834
+ pair.timer = schedule(() => {
6835
+ inflight = inflight.then(() => flushPair(key)).catch(() => {
6836
+ });
6837
+ }, coalesceMs);
6838
+ pending.set(key, pair);
6839
+ }
6840
+ pair.paths.set(conflict.editor.path, conflict);
6841
+ };
6842
+ return {
6843
+ list: () => [...snapshot().values()],
6844
+ onEdit(edit) {
6845
+ const sessions = snapshot();
6846
+ const self = sessions.get(edit.sessionId);
6847
+ if (!self) {
6848
+ logger.debug(`[PEERS] edit from unknown/dead session ${edit.sessionId} ignored`);
6849
+ return [];
6850
+ }
6851
+ const current = now();
6852
+ const at = typeof edit.at === "number" && Number.isFinite(edit.at) ? Math.min(edit.at, current) : current;
6853
+ if (current - at > windowMs) {
6854
+ logger.debug(`[PEERS] edit from ${edit.sessionId} on ${edit.path} is ${Math.round((current - at) / 6e4)}m old (replayed history); ignored`);
6855
+ return [];
6856
+ }
6857
+ const cwd = edit.cwd ?? self.cwd ?? process.cwd();
6858
+ const path = normalize(edit.path, cwd);
6859
+ const conflicts = tracker.record({ sessionId: edit.sessionId, path, tool: edit.tool, at }, (id) => sessions.has(id));
6860
+ for (const conflict of conflicts) enqueue(conflict);
6861
+ return conflicts;
6862
+ },
6863
+ async flush() {
6864
+ for (const key of [...pending.keys()]) {
6865
+ const pair = pending.get(key);
6866
+ if (pair?.timer && typeof pair.timer === "object") clearTimeout(pair.timer);
6867
+ inflight = inflight.then(() => flushPair(key)).catch(() => {
6868
+ });
6869
+ }
6870
+ await inflight;
6871
+ },
6872
+ forget(sessionId) {
6873
+ tracker.forget(sessionId);
6874
+ for (const [key, pair] of pending) {
6875
+ if (pair.a === sessionId || pair.b === sessionId) {
6876
+ if (pair.timer && typeof pair.timer === "object") clearTimeout(pair.timer);
6877
+ pending.delete(key);
6878
+ }
6879
+ }
6880
+ },
6881
+ setMirrorSource(source) {
6882
+ getMirrors = source;
6883
+ }
6884
+ };
6885
+ }
6886
+
6632
6887
  const ASSISTANT_CLAUDE_MD = `# very-happy \u8C03\u5EA6\u4E2D\u5FC3\uFF08\u8BED\u97F3\u52A9\u624B\uFF09
6633
6888
 
6634
6889
  \u4F60\u662F very-happy \u7684\u8C03\u5EA6\u4E2D\u5FC3\uFF0C\u8DD1\u5728\u7528\u6237\u81EA\u5DF1\u7684\u673A\u5668\u4E0A\u3002\u7528\u6237\u4E3B\u8981\u901A\u8FC7\u8BED\u97F3\u8DDF\u4F60\u8BF4\u8BDD\uFF0C
@@ -8548,6 +8803,73 @@ class ClaudeAuthService {
8548
8803
  }
8549
8804
  }
8550
8805
 
8806
+ const CLAUDE_EDIT_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
8807
+ function pathOf(input) {
8808
+ if (!input || typeof input !== "object") return null;
8809
+ const record = input;
8810
+ const candidate = record.file_path ?? record.notebook_path;
8811
+ return typeof candidate === "string" && candidate.trim() !== "" ? candidate : null;
8812
+ }
8813
+ function extractClaudeEditPaths(body) {
8814
+ if (!body || typeof body !== "object") return [];
8815
+ const line = body;
8816
+ if (line.type !== "assistant") return [];
8817
+ const content = line.message?.content;
8818
+ if (!Array.isArray(content)) return [];
8819
+ const stamp = line.timestamp;
8820
+ const at = typeof stamp === "string" ? Date.parse(stamp) : typeof stamp === "number" ? stamp : NaN;
8821
+ const edits = [];
8822
+ for (const block of content) {
8823
+ if (!block || typeof block !== "object") continue;
8824
+ const b = block;
8825
+ if (b.type !== "tool_use" || typeof b.name !== "string" || !CLAUDE_EDIT_TOOLS.has(b.name)) continue;
8826
+ const path = pathOf(b.input);
8827
+ if (path) edits.push({ path, tool: b.name, ...Number.isFinite(at) ? { at } : {} });
8828
+ }
8829
+ return edits;
8830
+ }
8831
+ function extractCodexPatchPaths(changes) {
8832
+ if (!changes || typeof changes !== "object" || Array.isArray(changes)) return [];
8833
+ return Object.keys(changes).filter((path) => path.trim() !== "").map((path) => ({ path, tool: "CodexPatch" }));
8834
+ }
8835
+ function extractAcpEditPaths(toolKind, args) {
8836
+ if (!args || typeof args !== "object") return [];
8837
+ const a = args;
8838
+ if (a.piTool === "write" || a.piTool === "edit") {
8839
+ const raw = a.rawInput;
8840
+ const path = raw && typeof raw.path === "string" && raw.path.trim() !== "" ? raw.path : null;
8841
+ return path ? [{ path, tool: a.piTool }] : [];
8842
+ }
8843
+ if (typeof a.piTool === "string") return [];
8844
+ if (toolKind !== "edit" || !Array.isArray(a.locations)) return [];
8845
+ const edits = [];
8846
+ for (const location of a.locations) {
8847
+ const path = location && typeof location === "object" ? location.path : void 0;
8848
+ if (typeof path === "string" && path.trim() !== "" && !edits.some((e) => e.path === path)) edits.push({ path, tool: "edit" });
8849
+ }
8850
+ return edits;
8851
+ }
8852
+ class EditReportThrottle {
8853
+ constructor(intervalMs = 5e3) {
8854
+ this.intervalMs = intervalMs;
8855
+ }
8856
+ lastReportedAt = /* @__PURE__ */ new Map();
8857
+ /** Returns the edits that should be reported now and remembers them. */
8858
+ take(edits, now = Date.now()) {
8859
+ const due = [];
8860
+ for (const edit of edits) {
8861
+ const last = this.lastReportedAt.get(edit.path);
8862
+ if (last !== void 0 && now - last < this.intervalMs) continue;
8863
+ this.lastReportedAt.set(edit.path, now);
8864
+ due.push(edit);
8865
+ }
8866
+ if (this.lastReportedAt.size > 512) {
8867
+ for (const [path, at] of this.lastReportedAt) if (now - at > this.intervalMs * 10) this.lastReportedAt.delete(path);
8868
+ }
8869
+ return due;
8870
+ }
8871
+ }
8872
+
8551
8873
  const INTERNAL_CLAUDE_EVENT_TYPES = /* @__PURE__ */ new Set(["file-history-snapshot", "change", "queue-operation"]);
8552
8874
  function parseLines(lines) {
8553
8875
  const out = [];
@@ -8584,12 +8906,15 @@ function createMirrorScanner(opts) {
8584
8906
  }
8585
8907
  tail.offset = 0;
8586
8908
  }
8909
+ let replaced = false;
8587
8910
  if (tail.offset > size) {
8588
8911
  logger.debug(`[MIRROR SCANNER] ${tail.path}: offset ${tail.offset} > size ${size} \u2014 file replaced, resetting (localId idempotency absorbs replays)`);
8589
8912
  tail.offset = 0;
8913
+ replaced = true;
8590
8914
  }
8591
8915
  if (tail.offset === size) return;
8592
8916
  const isFirstBackfillRead = tail.mode === "backfill-tail" && tail.offset === 0;
8917
+ const replay = isFirstBackfillRead || replaced;
8593
8918
  const handle = await open$1(tail.path, "r");
8594
8919
  let buf;
8595
8920
  try {
@@ -8609,7 +8934,7 @@ function createMirrorScanner(opts) {
8609
8934
  if (decision.truncated) opts.events.onBackfillTruncated?.();
8610
8935
  tail.mode = "from-eof";
8611
8936
  }
8612
- if (messages.length > 0) opts.events.onMessages(messages);
8937
+ if (messages.length > 0) opts.events.onMessages(messages, { replay });
8613
8938
  };
8614
8939
  const sync = new InvalidateSync(async () => {
8615
8940
  for (const tail of files.values()) {
@@ -8732,8 +9057,17 @@ function createMirrorManager(deps) {
8732
9057
  binding.client.updateMetadata((current) => ({ ...current, ...patch }));
8733
9058
  persistBinding(binding);
8734
9059
  };
8735
- const sendMessages = (binding, messages) => {
9060
+ const sendMessages = (binding, messages, context = { replay: false }) => {
8736
9061
  for (const message of messages) {
9062
+ if (deps.onEdit && binding.status === "active" && !context.replay) {
9063
+ for (const edit of extractClaudeEditPaths(message)) {
9064
+ try {
9065
+ deps.onEdit({ sessionId: binding.happySessionId, path: edit.path, tool: edit.tool, cwd: binding.metadata.path, ...edit.at !== void 0 ? { at: edit.at } : {} });
9066
+ } catch (error) {
9067
+ logger.debug(`[MIRROR] edit tap failed for terminal ${binding.terminalId}:`, error);
9068
+ }
9069
+ }
9070
+ }
8737
9071
  const key = mirrorLineKey(message);
8738
9072
  binding.client.sendClaudeSessionMessage(message, key ? { localIdFor: (envelopeIndex) => mirrorLocalId(key, envelopeIndex) } : void 0);
8739
9073
  }
@@ -8742,7 +9076,7 @@ function createMirrorManager(deps) {
8742
9076
  const scanner = createMirrorScanner({
8743
9077
  backfillLines,
8744
9078
  events: {
8745
- onMessages: (messages) => sendMessages(binding, messages),
9079
+ onMessages: (messages, context) => sendMessages(binding, messages, context),
8746
9080
  onBackfillTruncated: opts.withTruncationNotice ? () => binding.client.sendSessionEvent({
8747
9081
  type: "message",
8748
9082
  message: `\u53EA\u8BFB\u955C\u50CF\u53EA\u56DE\u704C\u4E86\u6700\u8FD1 ${backfillLines} \u6761\uFF0C\u66F4\u65E9\u5185\u5BB9\u8BF7\u5728\u7EC8\u7AEF\u91CC\u56DE\u770B`
@@ -8765,6 +9099,10 @@ function createMirrorManager(deps) {
8765
9099
  await binding.scanner?.cleanup();
8766
9100
  binding.scanner = null;
8767
9101
  binding.client.closeClaudeSessionTurn("completed");
9102
+ try {
9103
+ deps.onSessionEnded?.(binding.happySessionId);
9104
+ } catch {
9105
+ }
8768
9106
  updateBindingMetadata(binding, {
8769
9107
  lifecycleState: "archived",
8770
9108
  lifecycleStateSince: Date.now(),
@@ -8977,6 +9315,13 @@ function createMirrorManager(deps) {
8977
9315
  }
8978
9316
  };
8979
9317
  return {
9318
+ listActiveMirrors() {
9319
+ return [...bindings.values()].filter((binding) => binding.status === "active").map((binding) => ({
9320
+ sessionId: binding.happySessionId,
9321
+ ...binding.metadata.path ? { cwd: binding.metadata.path } : {},
9322
+ ...binding.metadata.summary?.text ? { title: binding.metadata.summary.text } : {}
9323
+ }));
9324
+ },
8980
9325
  handleHookPayload(body) {
8981
9326
  const event = parseTerminalHookPayload(body);
8982
9327
  if (!event) {
@@ -9443,683 +9788,6 @@ Delegate from this working directory unless a different repository was explicitl
9443
9788
  ${goal}`;
9444
9789
  }
9445
9790
 
9446
- const MAX_LINE_CHARS = 500;
9447
- const MAX_ARGS_CHARS = 160;
9448
- const DEFAULT_LIMITS = { line: MAX_LINE_CHARS, args: MAX_ARGS_CHARS };
9449
- const NO_LIMITS = { line: Number.POSITIVE_INFINITY, args: Number.POSITIVE_INFINITY };
9450
- function truncateText(text, max = MAX_LINE_CHARS) {
9451
- const chars = Array.from(text);
9452
- if (chars.length <= max) return text;
9453
- return chars.slice(0, max).join("") + ` \u2026[+${chars.length - max} chars]`;
9454
- }
9455
- function squash(text) {
9456
- return text.replace(/\s+/g, " ").trim();
9457
- }
9458
- function safeJson(value, max) {
9459
- try {
9460
- return truncateText(JSON.stringify(value) ?? "", max);
9461
- } catch {
9462
- return "[unserializable]";
9463
- }
9464
- }
9465
- function formatMessageBody(body, limits = DEFAULT_LIMITS) {
9466
- if (!body || typeof body !== "object") return null;
9467
- const b = body;
9468
- if (b.role === "user" && b.content?.type === "text" && typeof b.content.text === "string") {
9469
- const text = squash(b.content.text);
9470
- return text.length > 0 ? `user: ${truncateText(text, limits.line)}` : null;
9471
- }
9472
- if (b.role === "session" && b.content && typeof b.content === "object") {
9473
- const env = b.content;
9474
- const ev = env.ev;
9475
- if (!ev || typeof ev.t !== "string") return null;
9476
- switch (ev.t) {
9477
- case "text": {
9478
- if (ev.thinking) return null;
9479
- const text = squash(String(ev.text ?? ""));
9480
- if (text.length === 0) return null;
9481
- const role = env.role === "user" ? "user" : "assistant";
9482
- return `${role}: ${truncateText(text, limits.line)}`;
9483
- }
9484
- case "tool-call-start": {
9485
- const name = String(ev.name ?? "tool");
9486
- const title = squash(String(ev.title ?? ""));
9487
- const args = ev.args ? safeJson(ev.args, limits.args) : "";
9488
- const detail = title || args;
9489
- return `[tool] ${name}${detail ? `: ${truncateText(detail, limits.args)}` : ""}`;
9490
- }
9491
- case "service": {
9492
- const text = squash(String(ev.text ?? ""));
9493
- return text.length > 0 ? `[service] ${truncateText(text, limits.args)}` : null;
9494
- }
9495
- case "turn-end": {
9496
- if (ev.status && ev.status !== "completed") return `[turn ${ev.status}]`;
9497
- return null;
9498
- }
9499
- default:
9500
- return null;
9501
- }
9502
- }
9503
- if (b.role === "agent" && b.content?.type === "acp" && b.content.data && typeof b.content.data === "object") {
9504
- const d = b.content.data;
9505
- switch (d.type) {
9506
- case "message": {
9507
- const text = squash(String(d.message ?? ""));
9508
- return text.length > 0 ? `assistant: ${truncateText(text, limits.line)}` : null;
9509
- }
9510
- case "tool-call":
9511
- return `[tool] ${String(d.name ?? "tool")}: ${safeJson(d.input, limits.args)}`;
9512
- case "permission-request":
9513
- return `[permission] ${String(d.toolName ?? "")}: ${truncateText(squash(String(d.description ?? "")), limits.args)}`;
9514
- default:
9515
- return null;
9516
- }
9517
- }
9518
- if (b.role === "agent" && b.content?.type === "output") {
9519
- const msg = b.content.data?.message;
9520
- if (msg && Array.isArray(msg.content)) {
9521
- const texts = msg.content.filter((c) => c?.type === "text" && typeof c.text === "string").map((c) => squash(c.text)).filter((t) => t.length > 0);
9522
- if (texts.length > 0) {
9523
- const role = msg.role === "user" ? "user" : "assistant";
9524
- return `${role}: ${truncateText(texts.join(" "), limits.line)}`;
9525
- }
9526
- }
9527
- return null;
9528
- }
9529
- return null;
9530
- }
9531
- function formatTranscript(bodies, options = {}) {
9532
- const limits = options.full ? NO_LIMITS : DEFAULT_LIMITS;
9533
- const lines = [];
9534
- for (const body of bodies) {
9535
- const line = formatMessageBody(body, limits);
9536
- if (line !== null) lines.push(line);
9537
- }
9538
- return lines.join("\n");
9539
- }
9540
-
9541
- function asRecord(value) {
9542
- return value && typeof value === "object" ? value : null;
9543
- }
9544
- function sessionEnvelope(body) {
9545
- const b = asRecord(body);
9546
- if (!b || b.role !== "session") return null;
9547
- const env = asRecord(b.content);
9548
- if (!env) return null;
9549
- const ev = asRecord(env.ev);
9550
- return ev && typeof ev.t === "string" ? { role: env.role, ev } : null;
9551
- }
9552
- function isUserPrompt(body) {
9553
- const b = asRecord(body);
9554
- return !!b && b.role === "user" && b.content?.type === "text" && typeof b.content.text === "string";
9555
- }
9556
- function agentPiece(body) {
9557
- const env = sessionEnvelope(body);
9558
- if (env && env.role !== "user") {
9559
- if (env.ev.t === "text" && !env.ev.thinking) {
9560
- const text = String(env.ev.text ?? "");
9561
- return text.trim().length > 0 ? { kind: "text", text } : null;
9562
- }
9563
- if (env.ev.t === "tool-call-start") return { kind: "tool" };
9564
- return null;
9565
- }
9566
- const b = asRecord(body);
9567
- if (b?.role === "agent" && b.content?.type === "acp") {
9568
- const d = asRecord(b.content.data);
9569
- if (d?.type === "message") {
9570
- const text = String(d.message ?? "");
9571
- return text.trim().length > 0 ? { kind: "text", text } : null;
9572
- }
9573
- if (d?.type === "tool-call") return { kind: "tool" };
9574
- }
9575
- return null;
9576
- }
9577
- function userPromptText(body) {
9578
- return isUserPrompt(body) ? body.content.text : null;
9579
- }
9580
- function analyzeLatestTurn(entries, options = {}) {
9581
- const lastSeq = entries.length > 0 ? entries[entries.length - 1].seq : 0;
9582
- let userIndex = -1;
9583
- for (let i = entries.length - 1; i >= 0; i--) {
9584
- if (isUserPrompt(entries[i].body) && (!options.anchor || options.anchor(entries[i].body))) {
9585
- userIndex = i;
9586
- break;
9587
- }
9588
- }
9589
- let ended = false;
9590
- let status = null;
9591
- let error = null;
9592
- let texts = [];
9593
- let lastMarker = null;
9594
- let startedAfterPrompt = false;
9595
- const emitsStarts = entries.some((entry) => sessionEnvelope(entry.body)?.ev?.t === "turn-start");
9596
- for (let i = userIndex + 1; i < entries.length; i++) {
9597
- const body = entries[i].body;
9598
- if (options.anchor && isUserPrompt(body)) {
9599
- if (ended) break;
9600
- continue;
9601
- }
9602
- const env = sessionEnvelope(body);
9603
- if (env?.ev?.t === "turn-start") {
9604
- lastMarker = "start";
9605
- startedAfterPrompt = true;
9606
- ended = false;
9607
- status = null;
9608
- error = null;
9609
- texts = [];
9610
- continue;
9611
- }
9612
- if (env?.ev?.t === "turn-end") {
9613
- lastMarker = "end";
9614
- if (userIndex >= 0 && emitsStarts && !startedAfterPrompt) continue;
9615
- ended = true;
9616
- status = typeof env.ev.status === "string" ? env.ev.status : null;
9617
- error = typeof env.ev.error === "string" ? env.ev.error : null;
9618
- continue;
9619
- }
9620
- const piece = agentPiece(body);
9621
- if (!piece) continue;
9622
- if (piece.kind === "tool") texts = [];
9623
- else texts.push(piece.text);
9624
- }
9625
- if (userIndex < 0) ended = lastMarker === "end";
9626
- return {
9627
- userSeq: userIndex >= 0 ? entries[userIndex].seq : null,
9628
- ended,
9629
- status: ended ? status : null,
9630
- error: ended ? error : null,
9631
- answer: texts.join("\n\n").trim(),
9632
- lastSeq
9633
- };
9634
- }
9635
-
9636
- const USER_SOCKET_CONNECT_TIMEOUT_MS = 15e3;
9637
- async function openUserScopedSocket(token, options = {}) {
9638
- const socket = io(configuration.serverUrl, {
9639
- auth: {
9640
- token,
9641
- clientType: "user-scoped",
9642
- happyClient: `cli-session-ops/${configuration.currentCliVersion}`
9643
- },
9644
- path: "/v1/updates",
9645
- transports: ["websocket"],
9646
- reconnection: false,
9647
- autoConnect: false
9648
- });
9649
- await new Promise((resolve, reject) => {
9650
- const timer = setTimeout(() => {
9651
- socket.close();
9652
- reject(new Error(`Timed out connecting to ${configuration.serverUrl}`));
9653
- }, options.connectTimeoutMs ?? USER_SOCKET_CONNECT_TIMEOUT_MS);
9654
- socket.once("connect", () => {
9655
- clearTimeout(timer);
9656
- resolve();
9657
- });
9658
- socket.once("connect_error", (error) => {
9659
- clearTimeout(timer);
9660
- socket.close();
9661
- reject(new Error(`Could not connect to ${configuration.serverUrl}: ${error.message}`));
9662
- });
9663
- socket.connect();
9664
- });
9665
- return {
9666
- async rpcCall(payload, timeoutMs) {
9667
- const ack = await socket.timeout(timeoutMs).emitWithAck("rpc-call", payload);
9668
- return ack;
9669
- },
9670
- close() {
9671
- socket.close();
9672
- }
9673
- };
9674
- }
9675
-
9676
- const RPC_RATE_LIMIT_ERROR = "RPC rate limit reached";
9677
- const RPC_ACCOUNT_RATE_LIMIT_ERROR = "RPC account rate limit reached";
9678
- const RPC_RATE_RETRY_MIN_DELAY_MS = 2e3;
9679
- const RPC_RATE_RETRY_MAX_DELAY_MS = 6e4;
9680
- const RPC_RATE_MAX_RETRIES = 2;
9681
- function isRpcRateLimitAck(ack) {
9682
- if (!ack || typeof ack !== "object") return false;
9683
- const a = ack;
9684
- if (a.ok !== false) return false;
9685
- if (a.code === "rpc_rate_limited" || a.code === "rpc_account_rate_limited") return true;
9686
- return a.error === RPC_RATE_LIMIT_ERROR || a.error === RPC_ACCOUNT_RATE_LIMIT_ERROR;
9687
- }
9688
- function rpcRateRetryDelayMs(attempt, retryAfterMs, random = Math.random()) {
9689
- if (typeof retryAfterMs === "number" && Number.isFinite(retryAfterMs) && retryAfterMs > 0) {
9690
- return Math.min(RPC_RATE_RETRY_MAX_DELAY_MS, Math.round(retryAfterMs * (1 + random * 0.5)));
9691
- }
9692
- const exponent = Math.max(0, Math.min(attempt, 32) - 1);
9693
- const ceiling = Math.min(RPC_RATE_RETRY_MIN_DELAY_MS * 2 ** exponent, RPC_RATE_RETRY_MAX_DELAY_MS);
9694
- return Math.round(RPC_RATE_RETRY_MIN_DELAY_MS + random * (ceiling - RPC_RATE_RETRY_MIN_DELAY_MS));
9695
- }
9696
-
9697
- const PERMISSION_OPS_CLIENT = "session-ops";
9698
- const PERMISSION_RPC_TIMEOUT_MS = 3e4;
9699
- const SETTLE_TIMEOUT_MS = 5e3;
9700
- const SETTLE_POLL_MS = 500;
9701
- function buildPermissionRpcPayload(requestId, verdict) {
9702
- if (verdict.kind === "approve") {
9703
- return {
9704
- id: requestId,
9705
- approved: true,
9706
- decision: verdict.forSession ? "approved_for_session" : "approved"
9707
- };
9708
- }
9709
- return {
9710
- id: requestId,
9711
- approved: false,
9712
- decision: "denied",
9713
- ...verdict.reason ? { reason: verdict.reason } : {}
9714
- };
9715
- }
9716
- function interpretPermissionAck(ack, decryptResult) {
9717
- if (!ack.ok) {
9718
- const message = ack.error ?? "RPC call failed";
9719
- if (message === "RPC method not available") {
9720
- return { status: "offline", message: "no running wrapper has registered the permission RPC for this session" };
9721
- }
9722
- if (message === "RPC target disconnected") {
9723
- return { status: "offline", message: "the wrapper disconnected while the permission RPC was in flight" };
9724
- }
9725
- if (/timed? ?out/i.test(message)) {
9726
- return { status: "timeout", message };
9727
- }
9728
- if (isRpcRateLimitAck(ack)) {
9729
- return { status: "rate-limited", message, retryAfterMs: rpcRateRetryDelayMs(1, ack.retryAfterMs, 0) };
9730
- }
9731
- return { status: "rejected", message };
9732
- }
9733
- let body = null;
9734
- if (typeof ack.result === "string" && ack.result.length > 0) {
9735
- try {
9736
- body = decryptResult(ack.result);
9737
- } catch {
9738
- body = null;
9739
- }
9740
- }
9741
- if (body && typeof body === "object" && typeof body.error === "string") {
9742
- return { status: "handler-error", message: body.error };
9743
- }
9744
- return { status: "acknowledged" };
9745
- }
9746
- function pendingRequestsOf(agentState, now) {
9747
- const requests = agentState?.requests;
9748
- if (!requests || typeof requests !== "object") return [];
9749
- return Object.entries(requests).map(([id, request]) => ({
9750
- id,
9751
- tool: typeof request?.tool === "string" ? request.tool : "unknown",
9752
- ...request?.kind ? { kind: request.kind } : {},
9753
- ...typeof request?.createdAt === "number" ? { createdAt: request.createdAt, waitingMs: Math.max(0, now - request.createdAt) } : {}
9754
- })).sort((a, b) => (a.createdAt ?? Number.MAX_SAFE_INTEGER) - (b.createdAt ?? Number.MAX_SAFE_INTEGER));
9755
- }
9756
- const RPC_DEADLINE_GRACE_MS = 2e3;
9757
- function withDeadline(promise, ms, message) {
9758
- return new Promise((resolve, reject) => {
9759
- const timer = setTimeout(() => reject(new Error(`${message} (timed out)`)), ms);
9760
- promise.then(
9761
- (value) => {
9762
- clearTimeout(timer);
9763
- resolve(value);
9764
- },
9765
- (error) => {
9766
- clearTimeout(timer);
9767
- reject(error);
9768
- }
9769
- );
9770
- });
9771
- }
9772
- async function resolvePermissionRequest(sessionId, requestId, verdict, deps = {}) {
9773
- const now = deps.now ?? Date.now;
9774
- const persisted = (deps.readPersisted ?? readPersistedSessions)()[sessionId];
9775
- if (!persisted) {
9776
- throw new Error(
9777
- `No local key for session ${sessionId} \u2014 approve/deny needs the session key from ~/.happy/sessions.json (the RPC payload is encrypted with it), so only sessions this machine's daemon spawned within 14 days can be answered from here.`
9778
- );
9779
- }
9780
- const token = await (deps.bearerToken ?? bearerToken$1)();
9781
- const fetchState = deps.fetchAgentState ?? fetchDecryptedAgentState;
9782
- const before = pendingRequestsOf(await fetchState(sessionId, persisted, token), now());
9783
- if (!before.some((request) => request.id === requestId)) {
9784
- const hint = before.length > 0 ? `Pending request ids: ${before.map((request) => `${request.id} (${request.tool})`).join(", ")}` : "The session has no pending requests.";
9785
- throw new Error(`Request ${requestId} is not pending on session ${sessionId}. ${hint}`);
9786
- }
9787
- const payload = buildPermissionRpcPayload(requestId, verdict);
9788
- const key = decodeBase64(persisted.encryptionKey);
9789
- const params = encodeBase64(encrypt(key, persisted.encryptionVariant, payload));
9790
- const transport = await (deps.openTransport ?? openUserScopedSocket)(token);
9791
- const rpcTimeoutMs = deps.rpcTimeoutMs ?? PERMISSION_RPC_TIMEOUT_MS;
9792
- const sleep = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
9793
- let ack;
9794
- try {
9795
- for (let attempt = 1; ; attempt++) {
9796
- try {
9797
- ack = await withDeadline(
9798
- transport.rpcCall({ method: `${sessionId}:permission`, params }, rpcTimeoutMs),
9799
- rpcTimeoutMs + RPC_DEADLINE_GRACE_MS,
9800
- `permission RPC did not settle within ${rpcTimeoutMs}ms`
9801
- );
9802
- } catch (error) {
9803
- ack = { ok: false, error: error instanceof Error ? error.message : "RPC call failed" };
9804
- }
9805
- if (!isRpcRateLimitAck(ack) || attempt > RPC_RATE_MAX_RETRIES) break;
9806
- const waitMs = rpcRateRetryDelayMs(attempt, ack.retryAfterMs, (deps.random ?? Math.random)());
9807
- deps.onRateLimited?.({ attempt, waitMs, serverError: ack.error ?? "RPC rate limit reached" });
9808
- await sleep(waitMs);
9809
- }
9810
- } finally {
9811
- transport.close();
9812
- }
9813
- const outcome = interpretPermissionAck(ack, (encrypted) => decrypt(key, persisted.encryptionVariant, decodeBase64(encrypted)));
9814
- if (outcome.status !== "acknowledged") {
9815
- return { sessionId, requestId, payload, outcome };
9816
- }
9817
- const deadline = now() + (deps.settleTimeoutMs ?? SETTLE_TIMEOUT_MS);
9818
- let settled = false;
9819
- for (; ; ) {
9820
- const pending = pendingRequestsOf(await fetchState(sessionId, persisted, token), now());
9821
- if (!pending.some((request) => request.id === requestId)) {
9822
- settled = true;
9823
- break;
9824
- }
9825
- if (now() >= deadline) break;
9826
- await sleep(SETTLE_POLL_MS);
9827
- }
9828
- return { sessionId, requestId, payload, outcome, settled };
9829
- }
9830
- async function bearerToken$1() {
9831
- const credentials = await readCredentialsForConfiguredRelay();
9832
- if (!credentials) throw new Error("CLI is not authenticated (no ~/.happy/access.key)");
9833
- return credentials.token;
9834
- }
9835
- async function fetchDecryptedAgentState(sessionId, persisted, token) {
9836
- const response = await axios.get(
9837
- `${configuration.serverUrl}/v1/sessions/${encodeURIComponent(sessionId)}`,
9838
- {
9839
- headers: {
9840
- "Authorization": `Bearer ${token}`,
9841
- "X-Happy-Client": `${PERMISSION_OPS_CLIENT}/${configuration.currentCliVersion}`
9842
- },
9843
- timeout: 15e3,
9844
- // 404 is a real answer ("not yours / gone"), not a transport fault.
9845
- validateStatus: (status) => status >= 200 && status < 300 || status === 404
9846
- }
9847
- );
9848
- if (response.status === 404) {
9849
- throw new Error(`Session ${sessionId} was not found on this account (it may have been deleted, or the local key belongs to a session on another relay).`);
9850
- }
9851
- const raw = response.data?.session?.agentState;
9852
- if (typeof raw !== "string" || raw.length === 0) return null;
9853
- return decrypt(decodeBase64(persisted.encryptionKey), persisted.encryptionVariant, decodeBase64(raw));
9854
- }
9855
-
9856
- const SESSION_OPS_CLIENT = "session-ops";
9857
- const DEFAULT_RECENT_LIMIT = 15;
9858
- const MAX_READ_LIMIT = 100;
9859
- function mergeSessionSummaries(live, persisted, options = {}) {
9860
- const recentLimit = Math.max(0, options.recentLimit ?? DEFAULT_RECENT_LIMIT);
9861
- const seen = /* @__PURE__ */ new Set();
9862
- const summaries = [];
9863
- for (const child of live) {
9864
- const id = typeof child.happySessionId === "string" ? child.happySessionId : void 0;
9865
- if (!id || seen.has(id)) continue;
9866
- seen.add(id);
9867
- summaries.push(toSummary(id, persisted[id], livenessOf(child)));
9868
- }
9869
- const rest = Object.entries(persisted).filter(([id]) => !seen.has(id)).filter(([, entry]) => entry.metadata?.flavor !== "terminal-mirror").sort((a, b) => b[1].savedAt - a[1].savedAt).slice(0, recentLimit);
9870
- for (const [id, entry] of rest) {
9871
- summaries.push(toSummary(id, entry, { live: false }));
9872
- }
9873
- if (!options.tag) return summaries;
9874
- const wanted = options.tag;
9875
- return summaries.filter((summary) => summary.tags?.includes(wanted) === true);
9876
- }
9877
- function livenessOf(child) {
9878
- return { live: true, pid: typeof child.pid === "number" ? child.pid : void 0 };
9879
- }
9880
- function sessionLiveness(live, sessionId) {
9881
- const child = live.find((entry) => entry.happySessionId === sessionId);
9882
- return child ? livenessOf(child) : { live: false };
9883
- }
9884
- function toSummary(id, persisted, extra) {
9885
- const meta = persisted?.metadata;
9886
- return {
9887
- id,
9888
- live: extra.live,
9889
- ...extra.pid !== void 0 ? { pid: extra.pid } : {},
9890
- ...meta?.summary?.text ? { title: meta.summary.text } : {},
9891
- ...meta?.path ? { cwd: meta.path } : {},
9892
- ...meta?.flavor ? { flavor: meta.flavor } : {},
9893
- ...meta?.tags?.length ? { tags: [...meta.tags] } : {},
9894
- ...meta?.variant ? { variant: meta.variant } : {},
9895
- url: sessionWebUrl(id),
9896
- ...persisted?.savedAt !== void 0 ? { savedAt: persisted.savedAt } : {}
9897
- };
9898
- }
9899
- async function listSessions(options = {}) {
9900
- const live = await listDaemonSessions();
9901
- return mergeSessionSummaries(live, readPersistedSessions(), options);
9902
- }
9903
- function summarizeAccountSession(row, persisted, liveIds, now) {
9904
- const base = {
9905
- id: row.id,
9906
- live: liveIds.has(row.id),
9907
- url: sessionWebUrl(row.id),
9908
- decryptable: false,
9909
- active: row.active === true,
9910
- archived: typeof row.archivedAt === "number",
9911
- ...typeof row.activeAt === "number" ? { activeAt: row.activeAt } : {},
9912
- ...typeof row.updatedAt === "number" ? { updatedAt: row.updatedAt } : {},
9913
- attention: false
9914
- };
9915
- if (!persisted) return base;
9916
- const key = decodeBase64(persisted.encryptionKey);
9917
- let metadata = null;
9918
- let agentState = null;
9919
- try {
9920
- metadata = row.metadata ? decrypt(key, persisted.encryptionVariant, decodeBase64(row.metadata)) : null;
9921
- agentState = row.agentState ? decrypt(key, persisted.encryptionVariant, decodeBase64(row.agentState)) : null;
9922
- } catch {
9923
- return base;
9924
- }
9925
- if (!metadata) return base;
9926
- const meta = metadata;
9927
- const pending = pendingRequestsOf(agentState, now);
9928
- return {
9929
- ...base,
9930
- decryptable: true,
9931
- ...meta.summary?.text ? { title: meta.summary.text } : {},
9932
- ...meta.path ? { cwd: meta.path } : {},
9933
- ...meta.flavor ? { flavor: meta.flavor } : {},
9934
- ...meta.tags?.length ? { tags: [...meta.tags] } : {},
9935
- ...meta.variant ? { variant: meta.variant } : {},
9936
- ...meta.machineId ? { machineId: meta.machineId } : {},
9937
- ...persisted.savedAt !== void 0 ? { savedAt: persisted.savedAt } : {},
9938
- pending,
9939
- attention: pending.length > 0
9940
- };
9941
- }
9942
- function orderAccountSessions(summaries, options = {}) {
9943
- const oldestWait = (summary) => Math.max(0, ...(summary.pending ?? []).map((request) => request.waitingMs ?? 0));
9944
- const rank = (summary) => summary.attention ? 0 : summary.live ? 1 : 2;
9945
- let rows = summaries.filter((summary) => summary.flavor !== "terminal-mirror").slice().sort((a, b) => {
9946
- const byRank = rank(a) - rank(b);
9947
- if (byRank !== 0) return byRank;
9948
- if (a.attention && b.attention) return oldestWait(b) - oldestWait(a);
9949
- return (b.updatedAt ?? 0) - (a.updatedAt ?? 0);
9950
- });
9951
- if (options.tag) {
9952
- const wanted = options.tag;
9953
- rows = rows.filter((summary) => summary.tags?.includes(wanted) === true);
9954
- }
9955
- const recentLimit = Math.max(0, options.recentLimit ?? DEFAULT_RECENT_LIMIT);
9956
- let idleKept = 0;
9957
- return rows.filter((summary) => rank(summary) < 2 || idleKept++ < recentLimit);
9958
- }
9959
- async function listAccountSessions(options = {}) {
9960
- const token = await bearerToken();
9961
- const response = await axios.get(`${configuration.serverUrl}/v1/sessions`, {
9962
- headers: {
9963
- "Authorization": `Bearer ${token}`,
9964
- "X-Happy-Client": `${SESSION_OPS_CLIENT}/${configuration.currentCliVersion}`
9965
- },
9966
- timeout: 15e3
9967
- });
9968
- const rows = Array.isArray(response.data?.sessions) ? response.data.sessions : [];
9969
- const persisted = readPersistedSessions();
9970
- const live = await listDaemonSessions();
9971
- const liveIds = new Set(live.map((child) => child.happySessionId).filter((id) => typeof id === "string"));
9972
- const now = Date.now();
9973
- const summaries = rows.filter((row) => typeof row?.id === "string").map((row) => summarizeAccountSession(row, persisted[row.id], liveIds, now)).filter((summary) => options.includeArchived || !summary.archived);
9974
- return orderAccountSessions(summaries, options);
9975
- }
9976
- async function bearerToken() {
9977
- const credentials = await readCredentialsForConfiguredRelay();
9978
- if (!credentials) throw new Error("CLI is not authenticated (no ~/.happy/access.key)");
9979
- return credentials.token;
9980
- }
9981
- function requirePersisted(sessionId) {
9982
- const persisted = readPersistedSessions()[sessionId];
9983
- if (!persisted) {
9984
- throw new Error(
9985
- `No local key for session ${sessionId} \u2014 it was not spawned by this machine's daemon (or is older than 14 days).`
9986
- );
9987
- }
9988
- return persisted;
9989
- }
9990
- async function readSessionLog(sessionId, persisted, limit) {
9991
- const token = await bearerToken();
9992
- const response = await axios.get(
9993
- `${configuration.serverUrl}/v3/sessions/${encodeURIComponent(sessionId)}/messages`,
9994
- {
9995
- params: { before_seq: 2147483647, limit },
9996
- headers: {
9997
- "Authorization": `Bearer ${token}`,
9998
- "X-Happy-Client": `${SESSION_OPS_CLIENT}/${configuration.currentCliVersion}`
9999
- },
10000
- timeout: 15e3
10001
- }
10002
- );
10003
- const messages = Array.isArray(response.data?.messages) ? response.data.messages : [];
10004
- messages.reverse();
10005
- const key = decodeBase64(persisted.encryptionKey);
10006
- return messages.map((message) => {
10007
- if (message.content?.t !== "encrypted") return { seq: message.seq, body: null };
10008
- try {
10009
- return { seq: message.seq, body: decrypt(key, persisted.encryptionVariant, decodeBase64(message.content.c)) };
10010
- } catch {
10011
- return { seq: message.seq, body: null };
10012
- }
10013
- });
10014
- }
10015
- const TURN_WINDOW = 500;
10016
- async function readSessionTranscript(sessionId, limit, options = {}) {
10017
- const persisted = requirePersisted(sessionId);
10018
- const bounded = Math.max(1, Math.min(MAX_READ_LIMIT, Math.floor(limit)));
10019
- const entries = await readSessionLog(sessionId, persisted, Math.max(bounded, TURN_WINDOW));
10020
- const shown = entries.slice(-bounded);
10021
- const live = await listDaemonSessions();
10022
- return {
10023
- summary: toSummary(sessionId, persisted, sessionLiveness(live, sessionId)),
10024
- messageCount: shown.length,
10025
- transcript: formatTranscript(shown.map((entry) => entry.body), { full: options.full }),
10026
- turn: analyzeLatestTurn(entries)
10027
- };
10028
- }
10029
- async function readSessionMetadata(sessionId, persisted) {
10030
- const token = await bearerToken();
10031
- const response = await axios.get(`${configuration.serverUrl}/v1/sessions/${encodeURIComponent(sessionId)}`, {
10032
- headers: {
10033
- "Authorization": `Bearer ${token}`,
10034
- "X-Happy-Client": `${SESSION_OPS_CLIENT}/${configuration.currentCliVersion}`
10035
- },
10036
- timeout: 15e3
10037
- });
10038
- const ciphertext = response.data?.session?.metadata;
10039
- if (typeof ciphertext !== "string" || ciphertext.length === 0) return null;
10040
- return decrypt(decodeBase64(persisted.encryptionKey), persisted.encryptionVariant, decodeBase64(ciphertext));
10041
- }
10042
- async function readSessionState(sessionId, persisted) {
10043
- const token = await bearerToken();
10044
- const response = await axios.get(`${configuration.serverUrl}/v1/sessions/${encodeURIComponent(sessionId)}`, {
10045
- headers: {
10046
- "Authorization": `Bearer ${token}`,
10047
- "X-Happy-Client": `${SESSION_OPS_CLIENT}/${configuration.currentCliVersion}`
10048
- },
10049
- timeout: 15e3,
10050
- validateStatus: (status) => status >= 200 && status < 300 || status === 404
10051
- });
10052
- if (response.status === 404) return { found: false, active: false };
10053
- const row = response.data?.session;
10054
- if (!row || typeof row.id !== "string") throw new Error(`Server returned no session row for ${sessionId}`);
10055
- const state = {
10056
- found: true,
10057
- active: row.active === true,
10058
- ...typeof row.activeAt === "number" ? { activeAt: row.activeAt } : {},
10059
- archivedAt: typeof row.archivedAt === "number" ? row.archivedAt : null,
10060
- ...typeof row.updatedAt === "number" ? { updatedAt: row.updatedAt } : {}
10061
- };
10062
- if (persisted && typeof row.metadata === "string" && row.metadata.length > 0) {
10063
- try {
10064
- const metadata = decrypt(decodeBase64(persisted.encryptionKey), persisted.encryptionVariant, decodeBase64(row.metadata));
10065
- if (metadata?.machineId) state.machineId = metadata.machineId;
10066
- } catch {
10067
- }
10068
- }
10069
- return state;
10070
- }
10071
- class TurnWaitTimeoutError extends Error {
10072
- constructor(sessionId, timeoutMs, turn) {
10073
- super(`Session ${sessionId}: the latest turn did not end within ${Math.round(timeoutMs / 1e3)}s`);
10074
- this.sessionId = sessionId;
10075
- this.timeoutMs = timeoutMs;
10076
- this.turn = turn;
10077
- }
10078
- }
10079
- async function waitForTurnEnd(sessionId, options) {
10080
- const persisted = requirePersisted(sessionId);
10081
- const pollMs = options.pollMs ?? 3e3;
10082
- const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
10083
- const deadline = Date.now() + options.timeoutMs;
10084
- while (true) {
10085
- const turn = analyzeLatestTurn(await readSessionLog(sessionId, persisted, TURN_WINDOW));
10086
- if (turn.ended) return turn;
10087
- if (Date.now() + pollMs > deadline) throw new TurnWaitTimeoutError(sessionId, options.timeoutMs, turn);
10088
- await sleep(pollMs);
10089
- }
10090
- }
10091
- async function stopSession(sessionId) {
10092
- return await stopDaemonSession(sessionId);
10093
- }
10094
- async function archiveSession(sessionId) {
10095
- const token = await bearerToken();
10096
- await axios.post(
10097
- `${configuration.serverUrl}/v1/sessions/${encodeURIComponent(sessionId)}/archive`,
10098
- {},
10099
- {
10100
- headers: {
10101
- "Authorization": `Bearer ${token}`,
10102
- "X-Happy-Client": `${SESSION_OPS_CLIENT}/${configuration.currentCliVersion}`
10103
- },
10104
- timeout: 1e4
10105
- }
10106
- );
10107
- }
10108
- async function unarchiveSession(sessionId) {
10109
- const token = await bearerToken();
10110
- await axios.post(
10111
- `${configuration.serverUrl}/v1/sessions/${encodeURIComponent(sessionId)}/unarchive`,
10112
- {},
10113
- {
10114
- headers: {
10115
- "Authorization": `Bearer ${token}`,
10116
- "X-Happy-Client": `${SESSION_OPS_CLIENT}/${configuration.currentCliVersion}`
10117
- },
10118
- timeout: 1e4
10119
- }
10120
- );
10121
- }
10122
-
10123
9791
  function receiptPath(home, operationId) {
10124
9792
  if (!/^[a-zA-Z0-9_-]{1,128}$/.test(operationId)) throw new Error("Invalid team operation id");
10125
9793
  return join(home, "teams", "effects", `${operationId}.json`);
@@ -10709,7 +10377,7 @@ function automationRunAnchor(runId) {
10709
10377
  async function defaultReadTurn(sessionId, runId) {
10710
10378
  const persisted = readPersistedSessions()[sessionId];
10711
10379
  if (!persisted) throw new Error(`No local key for session ${sessionId}`);
10712
- return analyzeLatestTurn(await readSessionLog(sessionId, persisted, TURN_WINDOW), { anchor: automationRunAnchor(runId) });
10380
+ return analyzeLatestTurn(await readSessionLog$1(sessionId, persisted, TURN_WINDOW), { anchor: automationRunAnchor(runId) });
10713
10381
  }
10714
10382
  async function defaultSend(sessionId, text, meta) {
10715
10383
  const persisted = await waitForSessionKey(sessionId, 15e3);
@@ -11208,6 +10876,7 @@ async function startDaemon() {
11208
10876
  logger.debug("[DAEMON RUN] Auth and machine setup complete");
11209
10877
  const pidToTrackedSession = /* @__PURE__ */ new Map();
11210
10878
  const turnActivity = new TurnActivityTracker();
10879
+ const peerCoordinator = createPeerCoordinator({ getChildren: () => Array.from(pidToTrackedSession.values()) });
11211
10880
  const sessionIdToFinishedSession = /* @__PURE__ */ new Map();
11212
10881
  let teamWorker;
11213
10882
  let automationRunner;
@@ -12067,6 +11736,7 @@ async function startDaemon() {
12067
11736
  if (session?.happySessionId && (session.spawnedBy === "teams" || teamWorker?.hasSession(session.happySessionId))) teamWorker?.report(session.happySessionId, "exited");
12068
11737
  if (session?.happySessionId && automationRunner?.hasSession(session.happySessionId)) automationRunner.report(session.happySessionId, "exited");
12069
11738
  if (session?.happySessionId) turnActivity.forget(session.happySessionId);
11739
+ if (session?.happySessionId) peerCoordinator.forget(session.happySessionId);
12070
11740
  if (session?.happySessionId && session.encryption) {
12071
11741
  sessionIdToFinishedSession.set(session.happySessionId, session);
12072
11742
  logger.debug(`[DAEMON RUN] Process PID ${pid} exited, preserved session ${session.happySessionId} for resume`);
@@ -12152,6 +11822,10 @@ async function startDaemon() {
12152
11822
  onSessionStateEvent,
12153
11823
  onClaudeAuthFailed: (sessionId) => claudeAuthServiceRef?.signalAuthFailed(sessionId),
12154
11824
  onSessionTurnEvent: (sessionId, event) => turnActivity.apply(sessionId, event),
11825
+ onSessionEdit: (edit) => {
11826
+ peerCoordinator.onEdit(edit);
11827
+ },
11828
+ listPeers: () => peerCoordinator.list(),
12155
11829
  pushClipboard: (text, terminalId) => {
12156
11830
  if (!apiMachineRef) {
12157
11831
  return { delivered: false, truncated: false, totalBytes: 0, error: "daemon is still starting up" };
@@ -12217,9 +11891,12 @@ async function startDaemon() {
12217
11891
  const mirrorManager = createMirrorManager({
12218
11892
  api,
12219
11893
  machineId,
12220
- onBindingsChanged: () => apiMachine.requestTerminalListRefresh()
11894
+ onBindingsChanged: () => apiMachine.requestTerminalListRefresh(),
11895
+ onEdit: (edit) => peerCoordinator.onEdit(edit),
11896
+ onSessionEnded: (sessionId) => peerCoordinator.forget(sessionId)
12221
11897
  });
12222
11898
  mirrorManagerRef = mirrorManager;
11899
+ peerCoordinator.setMirrorSource(() => mirrorManager.listActiveMirrors());
12223
11900
  apiMachine.setMirrorIntegration({
12224
11901
  resolveMirrorSessionId: (terminalId) => mirrorManager.resolveMirrorSessionId(terminalId),
12225
11902
  onTerminalClosed: (terminalId) => mirrorManager.onTerminalClosed(terminalId),
@@ -12551,127 +12228,6 @@ This session is now connected as the team lead. Call team_inspect to read the au
12551
12228
  };
12552
12229
  }
12553
12230
 
12554
- const ACTIVE_FRESH_WINDOW_MS = 15 * 60 * 1e3;
12555
- const DEFAULT_RESUME_WAIT_MS = 3e4;
12556
- const DEFAULT_RESUME_POLL_MS = 1e3;
12557
- function classifySessionState(input) {
12558
- const { liveHere, server, now } = input;
12559
- if (!server.found) return "not_found";
12560
- if (typeof server.archivedAt === "number") return "archived";
12561
- if (liveHere) return "live";
12562
- if (server.active && typeof server.activeAt === "number" && now - server.activeAt <= ACTIVE_FRESH_WINDOW_MS) return "live";
12563
- return "offline";
12564
- }
12565
- function explainNotLive(sessionId, status, resumeHint) {
12566
- switch (status) {
12567
- case "archived":
12568
- return `Session ${sessionId} is archived \u2014 no live wrapper will receive this message. ${resumeHint}`;
12569
- case "offline":
12570
- return `Session ${sessionId} is offline (its wrapper exited or timed out) \u2014 no live wrapper will receive this message. ${resumeHint}`;
12571
- case "not_found":
12572
- return `Session ${sessionId} was not found on this account \u2014 it cannot be messaged or resumed. Spawn a new session instead.`;
12573
- }
12574
- }
12575
- const CLI_RESUME_HINT = "Use --resume to bring it back on this machine, or spawn a new session.";
12576
- const defaultDeps = {
12577
- listLive: () => listDaemonSessions(),
12578
- readState: (sessionId, persisted) => readSessionState(sessionId, persisted),
12579
- send: (sessionId, persisted, text, client, options) => sendUserMessage(sessionId, persisted, text, client, options),
12580
- unarchive: unarchiveSession,
12581
- archive: archiveSession,
12582
- resume: resumeDaemonSession,
12583
- sleep: (ms) => delay(ms).then(() => void 0),
12584
- now: () => Date.now()
12585
- };
12586
- async function deliverToSession(sessionId, persisted, text, client, options = {}, deps = {}) {
12587
- const d = { ...defaultDeps, ...deps };
12588
- const resumeHint = options.resumeHint ?? CLI_RESUME_HINT;
12589
- const classify = async () => {
12590
- const [live, server] = await Promise.all([d.listLive(), d.readState(sessionId, persisted)]);
12591
- return classifySessionState({ liveHere: sessionLiveness(live, sessionId).live, server, now: d.now() });
12592
- };
12593
- let status = await classify();
12594
- let resumed = false;
12595
- let resumeOutcome;
12596
- if (status !== "live") {
12597
- if (!options.resume || status === "not_found") {
12598
- return { sessionId, status, delivered: false, resumed: false, stored: false, error: explainNotLive(sessionId, status, resumeHint) };
12599
- }
12600
- const wasArchived = status === "archived";
12601
- if (wasArchived) {
12602
- try {
12603
- await d.unarchive(sessionId);
12604
- } catch (error) {
12605
- const message = `unarchive failed: ${error instanceof Error ? error.message : String(error)}`;
12606
- return { sessionId, status, delivered: false, resumed: false, stored: false, error: message, resume: { ok: false, error: message } };
12607
- }
12608
- }
12609
- const outcome = await d.resume(sessionId, {
12610
- ...typeof options.model === "string" ? { model: options.model } : {},
12611
- ...options.permissionMode !== void 0 ? { permissionMode: options.permissionMode } : {}
12612
- });
12613
- if (!outcome.success) {
12614
- let message = outcome.error ?? "resume failed";
12615
- if (wasArchived) {
12616
- try {
12617
- await d.archive(sessionId);
12618
- } catch (error) {
12619
- message += `; failed to restore archive state: ${error instanceof Error ? error.message : String(error)}`;
12620
- }
12621
- }
12622
- return { sessionId, status, delivered: false, resumed: false, stored: false, error: message, resume: { ok: false, error: message } };
12623
- }
12624
- resumed = true;
12625
- resumeOutcome = { ok: true };
12626
- status = await waitUntilLive(classify, d, options);
12627
- if (status !== "live") {
12628
- const waited = Math.round((options.waitMs ?? DEFAULT_RESUME_WAIT_MS) / 1e3);
12629
- return {
12630
- sessionId,
12631
- status,
12632
- delivered: false,
12633
- resumed,
12634
- stored: false,
12635
- resume: resumeOutcome,
12636
- error: `Session ${sessionId} was resumed but no live wrapper showed up within ${waited}s (state: ${status}); message not sent`
12637
- };
12638
- }
12639
- }
12640
- await d.send(sessionId, persisted, text, client, {
12641
- ...options.model !== void 0 ? { model: options.model } : {},
12642
- ...options.sentFrom !== void 0 ? { sentFrom: options.sentFrom } : {}
12643
- });
12644
- let after = "live";
12645
- try {
12646
- after = await classify();
12647
- } catch {
12648
- after = "live";
12649
- }
12650
- if (after !== "live") {
12651
- return {
12652
- sessionId,
12653
- status: after,
12654
- delivered: false,
12655
- resumed,
12656
- stored: true,
12657
- ...resumeOutcome ? { resume: resumeOutcome } : {},
12658
- error: `Session ${sessionId} went ${after} while sending: the message is stored server-side but no wrapper is attached to read it. ${after === "not_found" ? "Spawn a new session." : resumeHint}`
12659
- };
12660
- }
12661
- return { sessionId, status: "live", delivered: true, resumed, stored: true, ...resumeOutcome ? { resume: resumeOutcome } : {} };
12662
- }
12663
- async function waitUntilLive(classify, d, options) {
12664
- const waitMs = options.waitMs ?? DEFAULT_RESUME_WAIT_MS;
12665
- const pollMs = options.pollMs ?? DEFAULT_RESUME_POLL_MS;
12666
- const deadline = d.now() + waitMs;
12667
- let status = await classify();
12668
- while (status !== "live" && d.now() < deadline) {
12669
- await d.sleep(pollMs);
12670
- status = await classify();
12671
- }
12672
- return status;
12673
- }
12674
-
12675
12231
  const PERSONAL_MEMORY_SOFT_LIMIT_CHARS = 2e3;
12676
12232
  function headingTitle(line) {
12677
12233
  const m = /^##(?!#)\s*(.*?)\s*$/.exec(line);
@@ -13038,13 +12594,14 @@ function normalizeProgress(value) {
13038
12594
  return flat.length > PROGRESS_MAX_CHARS$1 ? flat.slice(0, PROGRESS_MAX_CHARS$1) : flat;
13039
12595
  }
13040
12596
 
13041
- function createMcpServer(handlers, options, sessionId) {
12597
+ function createMcpServer(handlers, options, sessionId, client) {
13042
12598
  const mcp = new McpServer({
13043
12599
  name: "Very Happy Tools",
13044
12600
  version: "1.0.0"
13045
12601
  });
13046
12602
  registerTeamsTools(mcp, sessionId);
13047
12603
  registerAutomationTools(mcp);
12604
+ if (client) registerSessionPeerTools(mcp, createSessionPeerToolExecutor(client));
13048
12605
  mcp.registerTool("change_title", {
13049
12606
  description: "Change the title of the current chat session",
13050
12607
  title: "Change Chat Title",
@@ -13247,7 +12804,7 @@ async function startHappyServer(client, options) {
13247
12804
  }
13248
12805
  return;
13249
12806
  }
13250
- const mcp = createMcpServer(handlers, options, client.sessionId);
12807
+ const mcp = createMcpServer(handlers, options, client.sessionId, client);
13251
12808
  try {
13252
12809
  const transport = new StreamableHTTPServerTransport({
13253
12810
  sessionIdGenerator: void 0
@@ -13288,6 +12845,7 @@ async function startHappyServer(client, options) {
13288
12845
  REPORT_PROGRESS_TOOL_NAME,
13289
12846
  ...TEAM_TOOL_NAMES,
13290
12847
  ...AUTOMATION_TOOL_NAMES,
12848
+ ...SESSION_PEER_TOOL_NAMES,
13291
12849
  ...options?.assistant && !process.env.VH_TEAM_SCOPE_FILE ? ASSISTANT_TOOL_NAMES : []
13292
12850
  ],
13293
12851
  stop: () => {
@@ -13624,7 +13182,7 @@ function registerSideQuestionHandler(rpc, deps) {
13624
13182
  const retainMs = deps.retainMs ?? SIDE_QUESTION_RETAIN_MS;
13625
13183
  const maxRunMs = deps.maxRunMs ?? SIDE_QUESTION_MAX_RUN_MS;
13626
13184
  const fork = deps.run ?? (async (input) => {
13627
- const { query } = await import('./index-CIx3248o.mjs');
13185
+ const { query } = await import('./index-DVVRqaVG.mjs');
13628
13186
  return runSideQuestion(query, input);
13629
13187
  });
13630
13188
  const run = async (input, live) => {
@@ -14411,6 +13969,17 @@ async function runClaude(credentials, options = {}) {
14411
13969
  });
14412
13970
  session.on("claude-session-message", (body) => boardAnalyzer.noteClaudeMessage(body));
14413
13971
  session.on("turn-ended", () => boardAnalyzer.onTurnEnd());
13972
+ const editThrottle = new EditReportThrottle();
13973
+ session.on("claude-session-message", (body) => {
13974
+ try {
13975
+ for (const edit of editThrottle.take(extractClaudeEditPaths(body))) {
13976
+ reportSessionEditToDaemon({ sessionId: response.id, path: edit.path, tool: edit.tool, cwd: workingDirectory, ...edit.at !== void 0 ? { at: edit.at } : {} });
13977
+ }
13978
+ } catch (error) {
13979
+ logger.debug("[START] edit report failed:", error);
13980
+ }
13981
+ });
13982
+ options = { ...options, claudeEnvVars: { ...options.claudeEnvVars, VH_PEER_SESSION_ID: response.id } };
14414
13983
  const initialScannerSessionId = forkClaudeSessionId ?? (metadata.claudeSessionId ?? null);
14415
13984
  const remoteScanner = await createSessionScanner({
14416
13985
  sessionId: initialScannerSessionId,
@@ -18397,6 +17966,7 @@ async function runCodex(opts) {
18397
17966
  return "denied";
18398
17967
  }
18399
17968
  });
17969
+ const editThrottle = new EditReportThrottle();
18400
17970
  client.setEventHandler((msg) => {
18401
17971
  if (msg.type === "token_count") {
18402
17972
  session.sendAgentUsageSnapshot("codex", msg);
@@ -18460,6 +18030,9 @@ async function runCodex(opts) {
18460
18030
  }
18461
18031
  if (msg.type === "patch_apply_begin") {
18462
18032
  const { changes } = msg;
18033
+ for (const edit of editThrottle.take(extractCodexPatchPaths(changes))) {
18034
+ reportSessionEditToDaemon({ sessionId: session.sessionId, path: edit.path, tool: edit.tool, cwd: process.cwd() });
18035
+ }
18463
18036
  const changeCount = Object.keys(changes).length;
18464
18037
  const filesMsg = changeCount === 1 ? "1 file" : `${changeCount} files`;
18465
18038
  messageBuffer.addMessage(`Modifying ${filesMsg}...`, "tool");
@@ -18505,6 +18078,7 @@ async function runCodex(opts) {
18505
18078
  };
18506
18079
  let first = true;
18507
18080
  let appendSystemPromptInjected = false;
18081
+ process.env.VH_PEER_SESSION_ID = session.sessionId;
18508
18082
  try {
18509
18083
  logger.debug("[codex]: client.connect begin");
18510
18084
  await client.connect();
@@ -18918,7 +18492,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18918
18492
  return;
18919
18493
  } else if (subcommand === "install-terminal-hooks") {
18920
18494
  try {
18921
- const { installTerminalHooks, parseTerminalHooksArgs, TERMINAL_HOOKS_HELP } = await import('./installTerminalHooks-Dde_8t36.mjs');
18495
+ const { installTerminalHooks, parseTerminalHooksArgs, TERMINAL_HOOKS_HELP } = await import('./installTerminalHooks-tIg7mANc.mjs');
18922
18496
  const command = parseTerminalHooksArgs(args.slice(1));
18923
18497
  if (command.action === "help") {
18924
18498
  console.log(TERMINAL_HOOKS_HELP);
@@ -18935,7 +18509,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18935
18509
  }
18936
18510
  } else if (subcommand === "agent-home") {
18937
18511
  try {
18938
- const { parseAgentHomeArgs, runAgentHomeCommand } = await import('./agentHome-B20RWdgE.mjs');
18512
+ const { parseAgentHomeArgs, runAgentHomeCommand } = await import('./agentHome-ByLb2HFg.mjs');
18939
18513
  await runAgentHomeCommand(parseAgentHomeArgs(args.slice(1)));
18940
18514
  process.exit(0);
18941
18515
  } catch (error) {
@@ -18944,7 +18518,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18944
18518
  }
18945
18519
  } else if (subcommand === "spawn") {
18946
18520
  try {
18947
- const { handleSpawnCommand } = await import('./spawn-D1bhyHbH.mjs');
18521
+ const { handleSpawnCommand } = await import('./spawn-BaPnjClT.mjs');
18948
18522
  await handleSpawnCommand(args.slice(1));
18949
18523
  } catch (error) {
18950
18524
  console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
@@ -18956,7 +18530,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18956
18530
  return;
18957
18531
  } else if (subcommand === "sessions") {
18958
18532
  try {
18959
- const { handleSessionsCommand } = await import('./sessions-DlPOYKLU.mjs');
18533
+ const { handleSessionsCommand } = await import('./sessions-JaVXlSqj.mjs');
18960
18534
  await handleSessionsCommand(args.slice(1));
18961
18535
  } catch (error) {
18962
18536
  console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
@@ -18968,7 +18542,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18968
18542
  return;
18969
18543
  } else if (subcommand === "send") {
18970
18544
  try {
18971
- const { handleSendCommand } = await import('./send-DYeSFg8s.mjs');
18545
+ const { handleSendCommand } = await import('./send-DJStmLBv.mjs');
18972
18546
  await handleSendCommand(args.slice(1));
18973
18547
  } catch (error) {
18974
18548
  console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
@@ -19074,9 +18648,9 @@ Conversation history is preserved on the server, but in-flight tool calls are in
19074
18648
  if (geminiSubcommand === "project" && args[2] === "set" && args[3]) {
19075
18649
  const projectId = args[3];
19076
18650
  try {
19077
- const { saveGoogleCloudProjectToConfig } = await import('./config-B-ocQh5V.mjs');
19078
- const { readCredentialsForConfiguredRelay: readCredentialsForConfiguredRelay2 } = await import('./limits-Dt7_unH4.mjs').then(function (n) { return n.al; });
19079
- const { ApiClient: ApiClient2 } = await import('./types-D5RWyBE5.mjs').then(function (n) { return n.ai; });
18651
+ const { saveGoogleCloudProjectToConfig } = await import('./config-BPZ2Y6o8.mjs');
18652
+ const { readCredentialsForConfiguredRelay: readCredentialsForConfiguredRelay2 } = await import('./limits-C_DW0M0q.mjs').then(function (n) { return n.aG; });
18653
+ const { ApiClient: ApiClient2 } = await import('./types-CkbfdRzq.mjs').then(function (n) { return n.a1; });
19080
18654
  let userEmail = void 0;
19081
18655
  try {
19082
18656
  const credentials = await readCredentialsForConfiguredRelay2();
@@ -19107,7 +18681,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
19107
18681
  }
19108
18682
  if (geminiSubcommand === "project" && args[2] === "get") {
19109
18683
  try {
19110
- const { readGeminiLocalConfig } = await import('./config-B-ocQh5V.mjs');
18684
+ const { readGeminiLocalConfig } = await import('./config-BPZ2Y6o8.mjs');
19111
18685
  const config = readGeminiLocalConfig();
19112
18686
  if (config.googleCloudProject) {
19113
18687
  console.log(`Current Google Cloud Project: ${config.googleCloudProject}`);
@@ -19147,7 +18721,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
19147
18721
  process.exit(0);
19148
18722
  }
19149
18723
  try {
19150
- const { runGemini } = await import('./runGemini-BW-pEklU.mjs');
18724
+ const { runGemini } = await import('./runGemini-CDlcQ-OA.mjs');
19151
18725
  let startedBy = void 0;
19152
18726
  for (let i = 1; i < args.length; i++) {
19153
18727
  if (args[i] === "--started-by") {
@@ -19170,7 +18744,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
19170
18744
  } else if (subcommand === "pi") {
19171
18745
  if (args[1] === "--terminal") {
19172
18746
  try {
19173
- const { runPiTerminal } = await import('./piTerminal-CCEGCQOY.mjs');
18747
+ const { runPiTerminal } = await import('./piTerminal-Dpq6yqKU.mjs');
19174
18748
  process.exitCode = await runPiTerminal(args.slice(2));
19175
18749
  } catch (error) {
19176
18750
  console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
@@ -19187,7 +18761,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
19187
18761
  }
19188
18762
  };
19189
18763
  try {
19190
- const { runAcp, resolveAcpAgentConfig } = await import('./index-ZKuBjvHi.mjs');
18764
+ const { runAcp, resolveAcpAgentConfig } = await import('./index-BjzdXb9x.mjs');
19191
18765
  const parsed = parsePiRunnerArgs(args.slice(1));
19192
18766
  const resolved = resolveAcpAgentConfig(["pi", ...parsed.passthrough]);
19193
18767
  const { credentials } = await authAndSetupMachineIfNeeded();
@@ -19215,7 +18789,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
19215
18789
  return;
19216
18790
  } else if (subcommand === "acp") {
19217
18791
  try {
19218
- const { runAcp, resolveAcpAgentConfig } = await import('./index-ZKuBjvHi.mjs');
18792
+ const { runAcp, resolveAcpAgentConfig } = await import('./index-BjzdXb9x.mjs');
19219
18793
  let startedBy = void 0;
19220
18794
  let verbose = false;
19221
18795
  const acpArgs = [];
@@ -19255,7 +18829,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
19255
18829
  return;
19256
18830
  } else if (subcommand === "openclaw") {
19257
18831
  try {
19258
- const { runOpenClaw } = await import('./runOpenClaw-NRB6-_jU.mjs');
18832
+ const { runOpenClaw } = await import('./runOpenClaw-YkhsgPs0.mjs');
19259
18833
  let startedBy = void 0;
19260
18834
  let verbose = false;
19261
18835
  let gatewayUrl;
@@ -19306,7 +18880,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
19306
18880
  return;
19307
18881
  } else if (subcommand === "todo") {
19308
18882
  try {
19309
- const { handleTodoCommand } = await import('./todo-B2CjDg_V.mjs');
18883
+ const { handleTodoCommand } = await import('./todo-2Z1REhfx.mjs');
19310
18884
  await handleTodoCommand(args.slice(1));
19311
18885
  } catch (error) {
19312
18886
  console.error(JSON.stringify({ error: error instanceof Error ? error.message : "Todo command failed" }));
@@ -19315,7 +18889,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
19315
18889
  return;
19316
18890
  } else if (subcommand === "teams") {
19317
18891
  try {
19318
- const { handleTeamsCommand } = await import('./teams-pfKHpkgL.mjs');
18892
+ const { handleTeamsCommand } = await import('./teams-DXV5Pwwt.mjs');
19319
18893
  await handleTeamsCommand(args.slice(1));
19320
18894
  } catch (error) {
19321
18895
  console.error(error instanceof Error ? error.message : "Teams command failed");
@@ -19324,7 +18898,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
19324
18898
  return;
19325
18899
  } else if (subcommand === "auto") {
19326
18900
  try {
19327
- const { handleAutoCommand } = await import('./auto-BQnncDcs.mjs');
18901
+ const { handleAutoCommand } = await import('./auto-BQnD6gVx.mjs');
19328
18902
  await handleAutoCommand(args.slice(1));
19329
18903
  } catch (error) {
19330
18904
  console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Automation command failed");
@@ -19336,7 +18910,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
19336
18910
  return;
19337
18911
  } else if (subcommand === "mcp" && (args.length === 1 || args.length === 2 && args[1] === "--terminal-tools")) {
19338
18912
  try {
19339
- const { handleMcpCommand } = await import('./mcp-9Ui4nQZL.mjs');
18913
+ const { handleMcpCommand } = await import('./mcp-o7QglAQM.mjs');
19340
18914
  await handleMcpCommand(args[1] === "--terminal-tools");
19341
18915
  } catch (error) {
19342
18916
  process.stderr.write(`[very-happy mcp] Fatal: ${error instanceof Error ? error.message : String(error)}
@@ -19706,4 +19280,4 @@ ${chalk.bold("Examples:")}
19706
19280
  }
19707
19281
  }
19708
19282
 
19709
- export { BaseReasoningProcessor as $, ALLOWED_SPAWN_PERMISSION_MODES as A, TEAM_SKILL as B, initialMachineMetadata as C, DEFAULT_RECENT_LIMIT as D, createSessionMetadata as E, setupOfflineReconnection as F, GEMINI_MODEL_ENV as G, StreamRelay as H, MessageQueue2 as I, hashObject as J, startHappyServer as K, registerAgentAttachmentDownloads as L, MAX_READ_LIMIT as M, TitleGenerator as N, registerKillSessionHandler as O, PI_TEAMS_EXTENSION as P, CLAUDE_ATTACHMENT_KINDS as Q, stageClaudeAttachments as R, SPAWN_AGENTS as S, TurnWaitTimeoutError as T, appendStagedAttachmentsToPrompt as U, BasePermissionHandler as V, writeSessionModeFile as W, normalizeAcpPermissionMode as X, removeSessionModeFile as Y, GEMINI_API_KEY_ENV as Z, GOOGLE_API_KEY_ENV as _, listSessions as a, MessageBuffer as a0, CHANGE_TITLE_INSTRUCTION as a1, resolvePermissionRequest as b, archiveSession as c, deliverToSession as d, sessionWebUrl as e, DEFAULT_GEMINI_MODEL as f, describeTrigger as g, describeAction as h, sanitizeSpawnPermissionMode as i, tmuxSupportsSessionEnv as j, readSessionMetadata as k, listAccountSessions as l, resolveSpawnPermissionMode as m, resolveFirstMessageMeta as n, isValidSpawnOrigin as o, parseDuration as p, query as q, readSessionTranscript as r, stopSession as s, triggerFromFlags as t, waitForSessionKey as u, sendUserMessage as v, waitForTurnEnd as w, checkPreviewPath as x, registerAssistantSessionTools as y, suggestTitleForPrompt as z };
19283
+ export { ALLOWED_SPAWN_PERMISSION_MODES as A, BasePermissionHandler as B, CLAUDE_ATTACHMENT_KINDS as C, DEFAULT_GEMINI_MODEL as D, EditReportThrottle as E, normalizeAcpPermissionMode as F, GEMINI_MODEL_ENV as G, removeSessionModeFile as H, GEMINI_API_KEY_ENV as I, GOOGLE_API_KEY_ENV as J, BaseReasoningProcessor as K, MessageBuffer as L, MessageQueue2 as M, CHANGE_TITLE_INSTRUCTION as N, PI_TEAMS_EXTENSION as P, SPAWN_AGENTS as S, TEAM_SKILL as T, describeAction as a, tmuxSupportsSessionEnv as b, resolveFirstMessageMeta as c, describeTrigger as d, checkPreviewPath as e, registerAssistantSessionTools as f, suggestTitleForPrompt as g, initialMachineMetadata as h, isValidSpawnOrigin as i, createSessionMetadata as j, setupOfflineReconnection as k, StreamRelay as l, hashObject as m, startHappyServer as n, registerAgentAttachmentDownloads as o, parseDuration as p, query as q, resolveSpawnPermissionMode as r, sanitizeSpawnPermissionMode as s, triggerFromFlags as t, TitleGenerator as u, registerKillSessionHandler as v, stageClaudeAttachments as w, appendStagedAttachmentsToPrompt as x, extractAcpEditPaths as y, writeSessionModeFile as z };