very-happy-cli 0.2.115 → 0.2.117

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 (37) hide show
  1. package/dist/{AcpBackend-DEZTWZZu.mjs → AcpBackend-C4RMHt0I.mjs} +1 -1
  2. package/dist/{AcpBackend-BrZN8KM-.cjs → AcpBackend-CrbXvcgQ.cjs} +1 -1
  3. package/dist/{AcpSessionManager-DjulpKY0.cjs → AcpSessionManager-CjAVO4RX.cjs} +1 -1
  4. package/dist/{AcpSessionManager-BzP3eiOG.mjs → AcpSessionManager-Cnc8Jatl.mjs} +1 -1
  5. package/dist/{config-CMgRBQMS.mjs → config-CnA3JIUn.mjs} +2 -2
  6. package/dist/{config-BNAsx9Wg.cjs → config-O9quMfxg.cjs} +2 -2
  7. package/dist/{index-B2KfhNku.mjs → index-B0o3H6_x.mjs} +2 -2
  8. package/dist/{index-BHNEb4bc.mjs → index-BYZKuQoy.mjs} +590 -41
  9. package/dist/{index-tPXwlc_t.cjs → index-DcESl4cI.cjs} +601 -43
  10. package/dist/{index-CpGR7rIw.mjs → index-DiRN0Rv8.mjs} +78 -8
  11. package/dist/{index-B3UkbonP.cjs → index-DqmVH6R5.cjs} +2 -2
  12. package/dist/{index-ClR0jhjm.cjs → index-W-O4xR_O.cjs} +77 -7
  13. package/dist/index.cjs +2 -2
  14. package/dist/index.mjs +2 -2
  15. package/dist/{installTerminalHooks-DD6QuHfT.mjs → installTerminalHooks-DeYqyjCB.mjs} +2 -2
  16. package/dist/{installTerminalHooks-CWfCF68p.cjs → installTerminalHooks-zfsWSotO.cjs} +2 -2
  17. package/dist/lib.cjs +1 -1
  18. package/dist/lib.d.cts +66 -1
  19. package/dist/lib.d.mts +66 -1
  20. package/dist/lib.mjs +1 -1
  21. package/dist/{mcp-CiNrjYox.mjs → mcp-D_5aOK0I.mjs} +62 -17
  22. package/dist/{mcp-CqjiDRnG.cjs → mcp-nYqC4FYm.cjs} +62 -16
  23. package/dist/{runGemini-DdJqpywr.mjs → runGemini-CEURRmM4.mjs} +4 -4
  24. package/dist/{runGemini-UN5IznJp.cjs → runGemini-DYf3ntib.cjs} +4 -4
  25. package/dist/{runOpenClaw-CM5UrVBy.mjs → runOpenClaw-BozfKWng.mjs} +3 -3
  26. package/dist/{runOpenClaw-9hhfGllZ.cjs → runOpenClaw-CNKjAw-H.cjs} +3 -3
  27. package/dist/{send-CnVtXOj0.cjs → send-BZLhRZb-.cjs} +2 -2
  28. package/dist/{send-C6G2jsQp.mjs → send-CYznZGrp.mjs} +2 -2
  29. package/dist/sessions-DYPB3Peg.mjs +317 -0
  30. package/dist/sessions-YWt9YEN8.cjs +322 -0
  31. package/dist/{spawn-qb344Y5E.mjs → spawn-CsrHZd3y.mjs} +3 -4
  32. package/dist/{spawn-CfYddkS_.cjs → spawn-tSvo3stN.cjs} +6 -7
  33. package/dist/{types-CpnzfHHS.cjs → types-DD3AK6Nb.cjs} +189 -26
  34. package/dist/{types-B3qx8Ga1.mjs → types-jSfU_0o1.mjs} +187 -26
  35. package/package.json +7 -7
  36. package/dist/sessions-CGdcF05T.cjs +0 -204
  37. package/dist/sessions-CmabJzYS.mjs +0 -200
@@ -1,11 +1,11 @@
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, randomBytes, createHash as createHash$1, timingSafeEqual } from 'node:crypto';
4
- import { o as readDaemonState, l as logger, c as configuration, q as clearDaemonState, s as getProjectPath, p as projectPath, t as initializeSandbox, u as wrapCommand, d as contentLogMetadata, I as InvalidateSync, R as RawJSONLinesSchema, g as delay, v as AsyncLock, S as STREAM_DELTA_MAX_CHARS, x as ensurePrivateDirectory, M as MAX_CHAT_ATTACHMENT_SOURCE_BYTES, y as hardenPrivateFile, f as packageJson, j as readSettings, z as readCredentials, B as credentialRelayProblem, C as deriveLocalCliUpdateSummary, k as encodeBase64, D as updateSettings, E as encodeBase64Url, F as decodeBase64, G as writeCredentialsLegacy, H as writeCredentialsDataKey, J as readCredentialsForConfiguredRelay, K as encrypt, r as readPersistedSessions, L as isClaudeSessionId, N as getLocalHappyAgentCredentialPath, O as readLocalHappyAgentCredentials, P as decryptWithDataKey, Q as decryptLegacy, T as extractCompleteLines, U as decideBackfill, V as parseTerminalHookPayload, W as tmuxArgs, X as scrubTmuxClientEnv, Y as decideMirrorBinding, Z as MIRROR_BACKFILL_LINES_DEFAULT, _ as persistSession, $ as mirrorLineKey, a0 as mirrorLocalId, w as writePrivateFileSync, a1 as detectResumeSupport, a2 as detectCLIAvailability, a3 as acquireDaemonLock, a4 as summarizeSpawnSessionForLog, a5 as deletePersistedSessions, a6 as shellescape, a7 as decrypt, a8 as writeDaemonState, A as ApiClient, a9 as writeSettings, aa as fetchCliUpdateState, ab as resolveCliUpdateCheckInterval, ac as DEFAULT_CLI_UPDATE_CHECK_INTERVAL_MS, ad as releaseDaemonLock, i as isValidSessionId, ae as listVhTerminals, af as isValidTerminalId, ag as readVhTerminal, ah as sendToVhTerminal, ai as TITLE_PROMPT_PREFIX, aj as BOARD_ANALYZER_PROMPT_PREFIX, h as connectionState, ak as startOfflineReconnection, al as clearCredentials, am as clearMachineId, m as ensurePrivateDirectorySync, an as SandboxConfigSchema, b as createEnvelope, ao as CodexAppServerClient, ap as getLatestDaemonLog } from './types-B3qx8Ga1.mjs';
4
+ import { o as readDaemonState, l as logger, c as configuration, q as clearDaemonState, s as getProjectPath, p as projectPath, t as initializeSandbox, u as wrapCommand, d as contentLogMetadata, I as InvalidateSync, R as RawJSONLinesSchema, g as delay, v as AsyncLock, S as STREAM_DELTA_MAX_CHARS, x as ensurePrivateDirectory, M as MAX_CHAT_ATTACHMENT_SOURCE_BYTES, y as hardenPrivateFile, f as packageJson, z as commandOnPath, j as readSettings, B as readCredentials, C as credentialRelayProblem, D as deriveLocalCliUpdateSummary, k as encodeBase64, E as updateSettings, F as encodeBase64Url, G as decodeBase64, H as writeCredentialsLegacy, J as writeCredentialsDataKey, K as readCredentialsForConfiguredRelay, L as encrypt, r as readPersistedSessions, N as isClaudeSessionId, w as writePrivateFileSync, O as getLocalHappyAgentCredentialPath, P as readLocalHappyAgentCredentials, Q as decryptWithDataKey, T as decryptLegacy, U as compareExactVersions, V as extractCompleteLines, W as decideBackfill, X as parseTerminalHookPayload, Y as tmuxArgs, Z as scrubTmuxClientEnv, _ as decideMirrorBinding, $ as MIRROR_BACKFILL_LINES_DEFAULT, a0 as persistSession, a1 as mirrorLineKey, a2 as mirrorLocalId, a3 as detectCLIAvailability, a4 as detectResumeSupport, a5 as acquireDaemonLock, a6 as summarizeSpawnSessionForLog, a7 as deletePersistedSessions, a8 as shellescape, a9 as decrypt, aa as writeDaemonState, A as ApiClient, ab as writeSettings, ac as fetchCliUpdateState, ad as resolveCliUpdateCheckInterval, ae as DEFAULT_CLI_UPDATE_CHECK_INTERVAL_MS, af as releaseDaemonLock, i as isValidSessionId, ag as listVhTerminals, ah as isValidTerminalId, ai as readVhTerminal, aj as sendToVhTerminal, ak as TITLE_PROMPT_PREFIX, al as BOARD_ANALYZER_PROMPT_PREFIX, h as connectionState, am as startOfflineReconnection, an as clearCredentials, ao as clearMachineId, m as ensurePrivateDirectorySync, ap as SandboxConfigSchema, b as createEnvelope, aq as CodexAppServerClient, ar as getLatestDaemonLog } from './types-jSfU_0o1.mjs';
5
5
  import spawn$1, { spawn } from 'cross-spawn';
6
6
  import path, { join, resolve, basename, delimiter, isAbsolute, normalize, sep, dirname } from 'node:path';
7
7
  import { createInterface } from 'node:readline';
8
- import { existsSync, readFileSync, readdirSync, statSync, mkdirSync, unlinkSync, writeFileSync, accessSync, constants, renameSync, rmSync as rmSync$1, chmodSync as chmodSync$1 } from 'node:fs';
8
+ import { existsSync, readFileSync, readdirSync, statSync, mkdirSync, unlinkSync, renameSync, writeFileSync, accessSync, constants, rmSync as rmSync$1, chmodSync as chmodSync$1 } from 'node:fs';
9
9
  import { t as trimIdent, A as AGENT_GUIDANCE, P as PREVIEW_TOOL_NAME, R as REPORT_PROGRESS_TOOL_NAME, a as PREVIEW_TOOL_TITLE, b as PREVIEW_TOOL_DESCRIPTION, c as REPORT_PROGRESS_TOOL_TITLE, d as REPORT_PROGRESS_TOOL_DESCRIPTION } from './agentGuidance-C2_pkgRI.mjs';
10
10
  import { readFile, writeFile, rename, unlink, mkdir, stat, open as open$1, appendFile } from 'node:fs/promises';
11
11
  import fs, { watch, access } from 'fs/promises';
@@ -14,7 +14,7 @@ import React, { useState, useRef, useEffect, useCallback } from 'react';
14
14
  import { query as query$1, AbortError } from '@anthropic-ai/claude-agent-sdk';
15
15
  import axios, { AxiosError } from 'axios';
16
16
  import 'node:events';
17
- import 'socket.io-client';
17
+ import { io } from 'socket.io-client';
18
18
  import tweetnacl from 'tweetnacl';
19
19
  import os$1, { homedir as homedir$1 } from 'os';
20
20
  import { spawn as spawn$2, execSync as execSync$1, exec } from 'child_process';
@@ -128,7 +128,9 @@ async function daemonPost(path, body) {
128
128
  signal: AbortSignal.timeout(timeout)
129
129
  });
130
130
  if (!response.ok) {
131
- const errorMessage = `Request failed: ${path}, HTTP ${response.status}`;
131
+ const failure = await response.json().catch(() => null);
132
+ const reason = typeof failure?.error === "string" && failure.error ? failure.error : `HTTP ${response.status}`;
133
+ const errorMessage = `Request failed: ${path}, ${reason}`;
132
134
  logger.debug(`[CONTROL CLIENT] ${errorMessage}`);
133
135
  return {
134
136
  error: errorMessage
@@ -195,6 +197,13 @@ async function pushClipboardViaDaemon(text) {
195
197
  }
196
198
  return result;
197
199
  }
200
+ async function setTerminalTitleViaDaemon(terminalId, title, ifAbsent = false) {
201
+ const result = await daemonPost("/terminal-title", { terminalId, title, ifAbsent });
202
+ if (result?.error) {
203
+ return { ok: false, error: result.error };
204
+ }
205
+ return { ok: result?.status === "ok", error: result?.status === "ok" ? void 0 : "unexpected daemon response" };
206
+ }
198
207
  async function checkIfDaemonRunningAndCleanupStaleState() {
199
208
  const state = await readDaemonState();
200
209
  if (!state) {
@@ -4651,8 +4660,8 @@ function claudeModeHash(mode) {
4651
4660
  }
4652
4661
 
4653
4662
  const SPAWN_ORIGIN_TAG_MAX = 24;
4654
- function spawnOriginTags(env = process.env) {
4655
- if (env.HAPPY_SESSION_VARIANT === "assistant") return void 0;
4663
+ function spawnOriginTags(env = process.env, flavor = void 0) {
4664
+ if (env.HAPPY_SESSION_VARIANT === "assistant" && flavor === "claude") return void 0;
4656
4665
  const origin = (env.HAPPY_SPAWNED_BY ?? "").trim();
4657
4666
  if (!origin) return void 0;
4658
4667
  if (origin.length > SPAWN_ORIGIN_TAG_MAX) return void 0;
@@ -4666,7 +4675,7 @@ function createSessionMetadata(opts) {
4666
4675
  const state = {
4667
4676
  controlledByUser: false
4668
4677
  };
4669
- const originTags = spawnOriginTags();
4678
+ const originTags = spawnOriginTags(process.env, opts.flavor);
4670
4679
  const metadata = {
4671
4680
  path: process.cwd(),
4672
4681
  host: os.hostname(),
@@ -4885,7 +4894,7 @@ function daemonReadiness(authenticated, running, hasState) {
4885
4894
  message: "\u26A0\uFE0F Daemon is not running \u2014 run `very-happy daemon start`"
4886
4895
  };
4887
4896
  }
4888
- function collectRuntimeReadiness(run = spawnSync, currentNodeVersion = process.version) {
4897
+ function collectRuntimeReadiness(run = spawnSync, currentNodeVersion = process.version, onPath = commandOnPath) {
4889
4898
  const tmux = probe("tmux", run);
4890
4899
  return {
4891
4900
  node: {
@@ -4896,7 +4905,8 @@ function collectRuntimeReadiness(run = spawnSync, currentNodeVersion = process.v
4896
4905
  ...tmux,
4897
4906
  supportsSessionEnv: tmux.available && tmuxSupportsSessionEnv(tmux.version)
4898
4907
  },
4899
- agents: ["claude", "codex", "gemini", "opencode", "openclaw"].map((command) => probe(command, run))
4908
+ agents: ["claude", "codex", "gemini", "opencode", "openclaw", "pi"].map((command) => probe(command, run)),
4909
+ piAdapter: { available: onPath("pi-acp") }
4900
4910
  };
4901
4911
  }
4902
4912
 
@@ -5145,6 +5155,11 @@ ${typeLabels[type] || type}:`));
5145
5155
  } else {
5146
5156
  console.log(chalk.green(`\u2713 External agent command${availableAgents.length === 1 ? "" : "s"}: ${availableAgents.map(toolProbeLabel).join(", ")}`));
5147
5157
  }
5158
+ const piProbe = readiness.agents.find((agent) => agent.command === "pi");
5159
+ if (piProbe?.available && !readiness.piAdapter.available) {
5160
+ console.log(chalk.yellow("\u25CB pi found, but the pi-acp adapter is not on PATH \u2014 `very-happy pi` and spawning pi from the Web need it"));
5161
+ console.log(chalk.gray(" Install it for the daemon user: npm install -g pi-acp@0.0.33"));
5162
+ }
5148
5163
  console.log(chalk.bold("\n\u{1F510} Authentication"));
5149
5164
  let authenticated = false;
5150
5165
  try {
@@ -5195,6 +5210,11 @@ ${typeLabels[type] || type}:`));
5195
5210
  console.log(chalk.green("\n\u2705 Doctor diagnosis complete!\n"));
5196
5211
  }
5197
5212
 
5213
+ const SPAWN_AGENTS = ["claude", "codex", "gemini", "openclaw", "pi"];
5214
+ function isSpawnAgent(value) {
5215
+ return typeof value === "string" && SPAWN_AGENTS.includes(value);
5216
+ }
5217
+
5198
5218
  const DEFAULT_LOG_PRUNE_POLICY = {
5199
5219
  maxAgeMs: 14 * 24 * 60 * 6e4,
5200
5220
  maxTotalBytes: 200 * 1024 * 1024,
@@ -5596,6 +5616,10 @@ function spawnHappyCLI(args, options = {}) {
5596
5616
  });
5597
5617
  }
5598
5618
 
5619
+ function assistantSpawnMode(options) {
5620
+ if (options.variant !== "assistant") return "none";
5621
+ return options.agent === void 0 || options.agent === "claude" ? "claude-singleton" : "env-only";
5622
+ }
5599
5623
  function isAssistantTracked(tracked) {
5600
5624
  return tracked.variant === "assistant" || tracked.happySessionMetadataFromLocalWebhook?.variant === "assistant";
5601
5625
  }
@@ -5820,7 +5844,8 @@ function startDaemonControlServer({
5820
5844
  onSessionStateEvent,
5821
5845
  onClaudeAuthFailed,
5822
5846
  pushClipboard,
5823
- onTerminalHook
5847
+ onTerminalHook,
5848
+ setTerminalTitle
5824
5849
  }) {
5825
5850
  return new Promise((resolve) => {
5826
5851
  const app = fastify({
@@ -5940,7 +5965,7 @@ function startDaemonControlServer({
5940
5965
  // ignores it and supplies its own home daemon-side.
5941
5966
  directory: z.string().default(""),
5942
5967
  sessionId: z.string().optional(),
5943
- agent: z.enum(["claude", "codex", "gemini", "openclaw"]).optional(),
5968
+ agent: z.enum(SPAWN_AGENTS).optional(),
5944
5969
  environmentVariables: z.record(z.string(), z.string()).optional(),
5945
5970
  variant: z.enum(["assistant"]).optional(),
5946
5971
  // B-051: assistant only — stop the live assistant, purge its
@@ -6045,6 +6070,30 @@ function startDaemonControlServer({
6045
6070
  logger.debug(`[CONTROL SERVER] Clipboard push request (${text.length} chars)`);
6046
6071
  return pushClipboard(text);
6047
6072
  });
6073
+ typed.post("/terminal-title", {
6074
+ schema: {
6075
+ body: z.object({
6076
+ terminalId: z.string().regex(/^[a-zA-Z0-9_-]{1,64}$/),
6077
+ title: z.string().min(1).max(200),
6078
+ ifAbsent: z.boolean().optional()
6079
+ }),
6080
+ response: {
6081
+ 200: z.object({ status: z.literal("ok") }),
6082
+ 409: z.object({ error: z.string() }),
6083
+ 503: z.object({ error: z.string() })
6084
+ }
6085
+ }
6086
+ }, async (request, reply) => {
6087
+ const { terminalId, title, ifAbsent } = request.body;
6088
+ const landed = setTerminalTitle ? setTerminalTitle(terminalId, title, !!ifAbsent) : "starting";
6089
+ if (landed === "starting") {
6090
+ return reply.code(503).send({ error: "daemon is still starting up" });
6091
+ }
6092
+ if (!landed) {
6093
+ return reply.code(409).send({ error: "Failed to set terminal title (tmux unavailable or terminal gone)" });
6094
+ }
6095
+ return { status: "ok" };
6096
+ });
6048
6097
  typed.post("/stop", {
6049
6098
  schema: {
6050
6099
  response: {
@@ -6850,6 +6899,76 @@ function expandEnvironmentVariables(envVars, sourceEnv = process.env) {
6850
6899
  return expanded;
6851
6900
  }
6852
6901
 
6902
+ const SESSION_MODE_DIR = "session-modes";
6903
+ const ACP_PERMISSION_MODES = ["default", "acceptEdits", "plan", "bypassPermissions"];
6904
+ function normalizeAcpPermissionMode(value) {
6905
+ if (typeof value !== "string") return null;
6906
+ if (value === "yolo") return "bypassPermissions";
6907
+ return ACP_PERMISSION_MODES.includes(value) ? value : null;
6908
+ }
6909
+ function sessionModeFilePath(happySessionId, dir = join(configuration.happyHomeDir, SESSION_MODE_DIR)) {
6910
+ return join(dir, `${happySessionId}.json`);
6911
+ }
6912
+ function sessionModeFilePayload(permissionMode, now = Date.now()) {
6913
+ return { permissionMode, updatedAt: now };
6914
+ }
6915
+ function writeSessionModeFile(happySessionId, permissionMode, opts = {}) {
6916
+ const path = sessionModeFilePath(happySessionId, opts.dir);
6917
+ mkdirSync(join(path, ".."), { recursive: true, mode: 448 });
6918
+ const payload = sessionModeFilePayload(permissionMode, opts.now);
6919
+ const tmp = `${path}.${process.pid}.tmp`;
6920
+ writePrivateFileSync(tmp, JSON.stringify(payload));
6921
+ renameSync(tmp, path);
6922
+ return payload;
6923
+ }
6924
+ function removeSessionModeFile(happySessionId, dir) {
6925
+ const path = sessionModeFilePath(happySessionId, dir);
6926
+ if (existsSync(path)) unlinkSync(path);
6927
+ }
6928
+
6929
+ const IGNORED_FLAGS_WITH_VALUE = /* @__PURE__ */ new Set(["--happy-starting-mode"]);
6930
+ const PI_ADAPTER_INSTALL_HINT = "very-happy pi needs the pi-acp adapter on PATH: npm install -g pi-acp@0.0.33";
6931
+ function piAdapterMissingHint(detail) {
6932
+ return detail && /ENOENT/.test(detail) ? PI_ADAPTER_INSTALL_HINT : null;
6933
+ }
6934
+ function parsePiRunnerArgs(args) {
6935
+ const parsed = { verbose: false, passthrough: [] };
6936
+ for (let i = 0; i < args.length; i++) {
6937
+ const arg = args[i];
6938
+ if (arg === "--") {
6939
+ parsed.passthrough.push(...args.slice(i + 1));
6940
+ break;
6941
+ }
6942
+ if (arg === "--started-by") {
6943
+ const value = args[++i];
6944
+ if (value === "daemon" || value === "terminal") parsed.startedBy = value;
6945
+ continue;
6946
+ }
6947
+ if (arg === "--verbose") {
6948
+ parsed.verbose = true;
6949
+ continue;
6950
+ }
6951
+ if (arg === "--permission-mode") {
6952
+ const mode = normalizeAcpPermissionMode(args[++i]);
6953
+ if (mode) parsed.permissionMode = mode;
6954
+ continue;
6955
+ }
6956
+ if (IGNORED_FLAGS_WITH_VALUE.has(arg)) {
6957
+ i++;
6958
+ continue;
6959
+ }
6960
+ throw new Error(`Unknown option for very-happy pi: ${arg} (use -- to pass args to pi-acp)`);
6961
+ }
6962
+ return parsed;
6963
+ }
6964
+
6965
+ function spawnAgentUnavailableError(agent, availability) {
6966
+ if (agent === "pi" && !availability.pi) {
6967
+ return { type: "error", errorMessage: PI_ADAPTER_INSTALL_HINT };
6968
+ }
6969
+ return null;
6970
+ }
6971
+
6853
6972
  const ResumableMetadataSchema = z.object({
6854
6973
  path: z.string().min(1),
6855
6974
  flavor: z.string().optional(),
@@ -7097,6 +7216,24 @@ function decideHandover(run) {
7097
7216
  return { action: "handover" };
7098
7217
  }
7099
7218
 
7219
+ function decideAutoUpdate(context) {
7220
+ if (!context.enabled) return { action: "skip", reason: "auto-update disabled" };
7221
+ const target = context.recommendedVersion;
7222
+ if (!target) return { action: "skip", reason: "no recommended version published" };
7223
+ const ordering = compareExactVersions(context.currentVersion, target);
7224
+ if (ordering === null) return { action: "skip", reason: "version numbers not comparable" };
7225
+ if (ordering === 0) return { action: "skip", reason: "already current" };
7226
+ if (ordering > 0) return { action: "skip", reason: `already ahead of the recommended ${target}` };
7227
+ if (context.failedVersion === target) {
7228
+ return { action: "skip", reason: `already failed to install ${target} once` };
7229
+ }
7230
+ if (!context.idle) return { action: "skip", reason: "machine is busy" };
7231
+ return { action: "install", version: target };
7232
+ }
7233
+ function autoUpdateInstallArgs(version) {
7234
+ return ["i", "-g", "--allow-scripts=very-happy-cli,node-pty", `very-happy-cli@${version}`];
7235
+ }
7236
+
7100
7237
  const CLAUDE_KEYCHAIN_SERVICE_PREFIX = "Claude Code";
7101
7238
  function keychainIdentity(env = process.env, deps = {}) {
7102
7239
  const home = deps.home ?? homedir();
@@ -8315,6 +8452,7 @@ function sanitizeImportTitle(value) {
8315
8452
  return collapsed.length > 200 ? `${collapsed.slice(0, 199)}\u2026` : collapsed;
8316
8453
  }
8317
8454
  const hostSuffix = process.env.HAPPY_VARIANT === "dev" ? "-dev" : "";
8455
+ const startupCliAvailability = detectCLIAvailability();
8318
8456
  const initialMachineMetadata = {
8319
8457
  host: os$1.hostname() + hostSuffix,
8320
8458
  platform: os$1.platform(),
@@ -8322,7 +8460,7 @@ const initialMachineMetadata = {
8322
8460
  homeDir: os$1.homedir(),
8323
8461
  happyHomeDir: configuration.happyHomeDir,
8324
8462
  happyLibDir: projectPath(),
8325
- cliAvailability: detectCLIAvailability(),
8463
+ cliAvailability: startupCliAvailability,
8326
8464
  resumeSupport: { ...detectResumeSupport(), rpcAvailable: true }
8327
8465
  };
8328
8466
  async function preflightNewBundle(bundlePath) {
@@ -8377,6 +8515,40 @@ function withHandoverHold(state, reason) {
8377
8515
  return reason ? { ...state, handoverHold: { reason, at: Date.now() } } : { ...state, handoverHold: null };
8378
8516
  }
8379
8517
  const HANDOVER_PREFLIGHT_TIMEOUT_MS = 3e4;
8518
+ function runNpmInstall(args) {
8519
+ return new Promise((resolve) => {
8520
+ let settled = false;
8521
+ const done = (code) => {
8522
+ if (!settled) {
8523
+ settled = true;
8524
+ resolve(code);
8525
+ }
8526
+ };
8527
+ let child;
8528
+ try {
8529
+ child = spawn$3("npm", args, { stdio: "ignore", env: process.env });
8530
+ } catch {
8531
+ done(null);
8532
+ return;
8533
+ }
8534
+ const timer = setTimeout(() => {
8535
+ try {
8536
+ child.kill("SIGKILL");
8537
+ } catch {
8538
+ }
8539
+ done(null);
8540
+ }, AUTO_UPDATE_INSTALL_TIMEOUT_MS);
8541
+ child.on("error", () => {
8542
+ clearTimeout(timer);
8543
+ done(null);
8544
+ });
8545
+ child.on("close", (code) => {
8546
+ clearTimeout(timer);
8547
+ done(code);
8548
+ });
8549
+ });
8550
+ }
8551
+ const AUTO_UPDATE_INSTALL_TIMEOUT_MS = 10 * 6e4;
8380
8552
  async function startDaemon() {
8381
8553
  let claudeAuthServiceRef = null;
8382
8554
  let cliUpdateStateRef = null;
@@ -8546,18 +8718,26 @@ async function startDaemon() {
8546
8718
  };
8547
8719
  const assistantSpawnGate = createSpawnGate();
8548
8720
  const spawnSession = (options) => {
8549
- if (options.variant !== "assistant") {
8721
+ if (assistantSpawnMode(options) !== "claude-singleton") {
8550
8722
  return spawnSessionImpl(options);
8551
8723
  }
8552
8724
  return options.forceNew ? assistantSpawnGate.replace(() => spawnSessionImpl(options)) : assistantSpawnGate.join(() => spawnSessionImpl(options));
8553
8725
  };
8726
+ const currentCliAvailability = () => apiMachineRef?.getCLIAvailability() ?? startupCliAvailability;
8554
8727
  const spawnSessionImpl = async (options) => {
8555
8728
  logger.debugLargeJson("[DAEMON RUN] Spawning session", summarizeSpawnSessionForLog(options));
8556
8729
  const spawnPermissionMode = sanitizeSpawnPermissionMode(options.permissionMode);
8557
8730
  if (options.permissionMode !== void 0 && spawnPermissionMode === null) {
8558
8731
  logger.warn("[DAEMON RUN] Ignoring invalid permissionMode in spawn request");
8559
8732
  }
8560
- if (options.variant === "assistant") {
8733
+ const unavailable = spawnAgentUnavailableError(options.agent, currentCliAvailability());
8734
+ if (unavailable) {
8735
+ logger.warn(`[DAEMON RUN] Refusing spawn of agent '${options.agent}': ${unavailable.errorMessage}`);
8736
+ return unavailable;
8737
+ }
8738
+ const assistantMode = assistantSpawnMode(options);
8739
+ const trackedVariant = assistantMode === "claude-singleton" ? options.variant : void 0;
8740
+ if (assistantMode === "claude-singleton") {
8561
8741
  options = { ...options, directory: assistantHome() };
8562
8742
  if (options.forceNew) {
8563
8743
  for (const tracked of [...pidToTrackedSession.values()]) {
@@ -8760,7 +8940,13 @@ async function startDaemon() {
8760
8940
  logger.debug(`[DAEMON RUN] Attempting to spawn session in tmux: ${sessionDesc}`);
8761
8941
  const tmux = getTmuxUtilities(tmuxSessionName);
8762
8942
  const cliPath = join$1(projectPath(), "dist", "index.mjs");
8763
- const agent = options.agent === "gemini" ? "gemini" : options.agent === "codex" ? "codex" : options.agent === "openclaw" ? "openclaw" : "claude";
8943
+ const agent = options.agent ?? "claude";
8944
+ if (!isSpawnAgent(agent)) {
8945
+ return {
8946
+ type: "error",
8947
+ errorMessage: `Unsupported agent type: '${options.agent}'. Please update your CLI to the latest version.`
8948
+ };
8949
+ }
8764
8950
  const resumeId = agent === "claude" ? options.resumeClaudeSessionId : agent === "codex" ? options.resumeCodexThreadId : void 0;
8765
8951
  const resumeFragment = resumeId ? ` --resume ${shellescape(resumeId)}` : "";
8766
8952
  const permissionModeFragment = spawnPermissionMode ? ` --permission-mode ${shellescape(spawnPermissionMode)}` : "";
@@ -8788,7 +8974,7 @@ async function startDaemon() {
8788
8974
  pid: tmuxResult.pid,
8789
8975
  // Real PID from tmux -P flag
8790
8976
  tmuxSessionId: tmuxResult.sessionId,
8791
- variant: options.variant,
8977
+ variant: trackedVariant,
8792
8978
  spawnedBy: options.spawnedBy,
8793
8979
  directoryCreated,
8794
8980
  message: directoryCreated ? `The path '${directory}' did not exist. We created a new folder and spawned a new session in tmux session '${tmuxSessionName}'. Use 'tmux attach -t ${tmuxSessionName}' to view the session.` : `Spawned new session in tmux session '${tmuxSessionName}'. Use 'tmux attach -t ${tmuxSessionName}' to view the session.`
@@ -8835,6 +9021,9 @@ async function startDaemon() {
8835
9021
  case "openclaw":
8836
9022
  agentCommand = "openclaw";
8837
9023
  break;
9024
+ case "pi":
9025
+ agentCommand = "pi";
9026
+ break;
8838
9027
  default:
8839
9028
  return {
8840
9029
  type: "error",
@@ -8866,7 +9055,7 @@ async function startDaemon() {
8866
9055
  },
8867
9056
  directoryCreated,
8868
9057
  message: directoryCreated ? `The path '${directory}' did not exist. We created a new folder and spawned a new session there.` : void 0,
8869
- variant: options.variant,
9058
+ variant: trackedVariant,
8870
9059
  spawnedBy: options.spawnedBy
8871
9060
  });
8872
9061
  }
@@ -9347,6 +9536,10 @@ async function startDaemon() {
9347
9536
  },
9348
9537
  onTerminalHook: (body) => {
9349
9538
  mirrorManagerRef?.handleHookPayload(body);
9539
+ },
9540
+ setTerminalTitle: (terminalId, title, ifAbsent) => {
9541
+ if (!apiMachineRef) return "starting";
9542
+ return apiMachineRef.setTerminalTitle(terminalId, title, ifAbsent);
9350
9543
  }
9351
9544
  });
9352
9545
  const daemonClaudeCredentials = resolveClaudeCredentialReadiness();
@@ -9443,6 +9636,48 @@ async function startDaemon() {
9443
9636
  host: initialMachineMetadata.host
9444
9637
  })).then(() => logger.debug(`[DAEMON RUN] claimed the machine record for host ${initialMachineMetadata.host}`)).catch((error) => logger.debug("[DAEMON RUN] could not refresh the recorded host:", error));
9445
9638
  }
9639
+ let autoUpdateFailedVersion = null;
9640
+ let autoUpdateRunning = false;
9641
+ const maybeAutoUpdate = async (cliUpdate) => {
9642
+ if (autoUpdateRunning) return;
9643
+ const settings = await readSettings();
9644
+ const decision = decideAutoUpdate({
9645
+ enabled: (settings.cliAutoUpdate ?? "idle") !== "off",
9646
+ currentVersion: cliUpdate.currentVersion,
9647
+ recommendedVersion: cliUpdate.recommendedVersion,
9648
+ idle: pidToTrackedSession.size === 0 && !apiMachine.hasLiveTerminals(),
9649
+ failedVersion: autoUpdateFailedVersion
9650
+ });
9651
+ if (decision.action === "skip") {
9652
+ logger.debug(`[DAEMON RUN] auto-update skipped: ${decision.reason}`);
9653
+ return;
9654
+ }
9655
+ autoUpdateRunning = true;
9656
+ const target = decision.version;
9657
+ const report = (state, detail) => {
9658
+ cliUpdateStateRef = { ...cliUpdate, autoUpdate: { state, version: target, detail, at: Date.now() } };
9659
+ void apiMachine.setCliUpdateState(cliUpdateStateRef);
9660
+ };
9661
+ logger.warn(`[DAEMON RUN] auto-updating CLI to ${target} (machine idle)`);
9662
+ report("installing");
9663
+ try {
9664
+ const code = await runNpmInstall(autoUpdateInstallArgs(target));
9665
+ if (code === 0) {
9666
+ logger.warn(`[DAEMON RUN] auto-update installed ${target}; handover follows once it verifies`);
9667
+ report("installed");
9668
+ } else {
9669
+ autoUpdateFailedVersion = target;
9670
+ logger.warn(`[DAEMON RUN] auto-update of ${target} failed with exit ${code}`);
9671
+ report("failed", `npm exited ${code}`);
9672
+ }
9673
+ } catch (error) {
9674
+ autoUpdateFailedVersion = target;
9675
+ logger.warn(`[DAEMON RUN] auto-update of ${target} could not run:`, error);
9676
+ report("failed", String(error).slice(0, 200));
9677
+ } finally {
9678
+ autoUpdateRunning = false;
9679
+ }
9680
+ };
9446
9681
  let cliUpdateCheckRunning = false;
9447
9682
  const refreshCliUpdate = async () => {
9448
9683
  if (cliUpdateCheckRunning) return;
@@ -9455,6 +9690,7 @@ async function startDaemon() {
9455
9690
  cliUpdateStateRef = cliUpdate;
9456
9691
  apiMachine.setCliUpdateState(cliUpdate);
9457
9692
  logger.debug(`[DAEMON RUN] CLI update policy checked: ${cliUpdate.status} (recommended=${cliUpdate.recommendedVersion ?? "none"}, minimum=${cliUpdate.minimumVersion ?? "none"})`);
9693
+ await maybeAutoUpdate(cliUpdate);
9458
9694
  } catch (error) {
9459
9695
  logger.debug("[DAEMON RUN] CLI update policy check failed:", error);
9460
9696
  } finally {
@@ -9667,6 +9903,194 @@ function formatTranscript(bodies) {
9667
9903
  return lines.join("\n");
9668
9904
  }
9669
9905
 
9906
+ const USER_SOCKET_CONNECT_TIMEOUT_MS = 15e3;
9907
+ async function openUserScopedSocket(token, options = {}) {
9908
+ const socket = io(configuration.serverUrl, {
9909
+ auth: {
9910
+ token,
9911
+ clientType: "user-scoped",
9912
+ happyClient: `cli-session-ops/${configuration.currentCliVersion}`
9913
+ },
9914
+ path: "/v1/updates",
9915
+ transports: ["websocket"],
9916
+ reconnection: false,
9917
+ autoConnect: false
9918
+ });
9919
+ await new Promise((resolve, reject) => {
9920
+ const timer = setTimeout(() => {
9921
+ socket.close();
9922
+ reject(new Error(`Timed out connecting to ${configuration.serverUrl}`));
9923
+ }, options.connectTimeoutMs ?? USER_SOCKET_CONNECT_TIMEOUT_MS);
9924
+ socket.once("connect", () => {
9925
+ clearTimeout(timer);
9926
+ resolve();
9927
+ });
9928
+ socket.once("connect_error", (error) => {
9929
+ clearTimeout(timer);
9930
+ socket.close();
9931
+ reject(new Error(`Could not connect to ${configuration.serverUrl}: ${error.message}`));
9932
+ });
9933
+ socket.connect();
9934
+ });
9935
+ return {
9936
+ async rpcCall(payload, timeoutMs) {
9937
+ const ack = await socket.timeout(timeoutMs).emitWithAck("rpc-call", payload);
9938
+ return ack;
9939
+ },
9940
+ close() {
9941
+ socket.close();
9942
+ }
9943
+ };
9944
+ }
9945
+
9946
+ const PERMISSION_OPS_CLIENT = "session-ops";
9947
+ const PERMISSION_RPC_TIMEOUT_MS = 3e4;
9948
+ const SETTLE_TIMEOUT_MS = 5e3;
9949
+ const SETTLE_POLL_MS = 500;
9950
+ function buildPermissionRpcPayload(requestId, verdict) {
9951
+ if (verdict.kind === "approve") {
9952
+ return {
9953
+ id: requestId,
9954
+ approved: true,
9955
+ decision: verdict.forSession ? "approved_for_session" : "approved"
9956
+ };
9957
+ }
9958
+ return {
9959
+ id: requestId,
9960
+ approved: false,
9961
+ decision: "denied",
9962
+ ...verdict.reason ? { reason: verdict.reason } : {}
9963
+ };
9964
+ }
9965
+ function interpretPermissionAck(ack, decryptResult) {
9966
+ if (!ack.ok) {
9967
+ const message = ack.error ?? "RPC call failed";
9968
+ if (message === "RPC method not available") {
9969
+ return { status: "offline", message: "no running wrapper has registered the permission RPC for this session" };
9970
+ }
9971
+ if (message === "RPC target disconnected") {
9972
+ return { status: "offline", message: "the wrapper disconnected while the permission RPC was in flight" };
9973
+ }
9974
+ if (/timed? ?out/i.test(message)) {
9975
+ return { status: "timeout", message };
9976
+ }
9977
+ return { status: "rejected", message };
9978
+ }
9979
+ let body = null;
9980
+ if (typeof ack.result === "string" && ack.result.length > 0) {
9981
+ try {
9982
+ body = decryptResult(ack.result);
9983
+ } catch {
9984
+ body = null;
9985
+ }
9986
+ }
9987
+ if (body && typeof body === "object" && typeof body.error === "string") {
9988
+ return { status: "handler-error", message: body.error };
9989
+ }
9990
+ return { status: "acknowledged" };
9991
+ }
9992
+ function pendingRequestsOf(agentState, now) {
9993
+ const requests = agentState?.requests;
9994
+ if (!requests || typeof requests !== "object") return [];
9995
+ return Object.entries(requests).map(([id, request]) => ({
9996
+ id,
9997
+ tool: typeof request?.tool === "string" ? request.tool : "unknown",
9998
+ ...request?.kind ? { kind: request.kind } : {},
9999
+ ...typeof request?.createdAt === "number" ? { createdAt: request.createdAt, waitingMs: Math.max(0, now - request.createdAt) } : {}
10000
+ })).sort((a, b) => (a.createdAt ?? Number.MAX_SAFE_INTEGER) - (b.createdAt ?? Number.MAX_SAFE_INTEGER));
10001
+ }
10002
+ const RPC_DEADLINE_GRACE_MS = 2e3;
10003
+ function withDeadline(promise, ms, message) {
10004
+ return new Promise((resolve, reject) => {
10005
+ const timer = setTimeout(() => reject(new Error(`${message} (timed out)`)), ms);
10006
+ promise.then(
10007
+ (value) => {
10008
+ clearTimeout(timer);
10009
+ resolve(value);
10010
+ },
10011
+ (error) => {
10012
+ clearTimeout(timer);
10013
+ reject(error);
10014
+ }
10015
+ );
10016
+ });
10017
+ }
10018
+ async function resolvePermissionRequest(sessionId, requestId, verdict, deps = {}) {
10019
+ const now = deps.now ?? Date.now;
10020
+ const persisted = (deps.readPersisted ?? readPersistedSessions)()[sessionId];
10021
+ if (!persisted) {
10022
+ throw new Error(
10023
+ `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.`
10024
+ );
10025
+ }
10026
+ const token = await (deps.bearerToken ?? bearerToken$1)();
10027
+ const fetchState = deps.fetchAgentState ?? fetchDecryptedAgentState;
10028
+ const before = pendingRequestsOf(await fetchState(sessionId, persisted, token), now());
10029
+ if (!before.some((request) => request.id === requestId)) {
10030
+ const hint = before.length > 0 ? `Pending request ids: ${before.map((request) => `${request.id} (${request.tool})`).join(", ")}` : "The session has no pending requests.";
10031
+ throw new Error(`Request ${requestId} is not pending on session ${sessionId}. ${hint}`);
10032
+ }
10033
+ const payload = buildPermissionRpcPayload(requestId, verdict);
10034
+ const key = decodeBase64(persisted.encryptionKey);
10035
+ const params = encodeBase64(encrypt(key, persisted.encryptionVariant, payload));
10036
+ const transport = await (deps.openTransport ?? openUserScopedSocket)(token);
10037
+ const rpcTimeoutMs = deps.rpcTimeoutMs ?? PERMISSION_RPC_TIMEOUT_MS;
10038
+ let ack;
10039
+ try {
10040
+ ack = await withDeadline(
10041
+ transport.rpcCall({ method: `${sessionId}:permission`, params }, rpcTimeoutMs),
10042
+ rpcTimeoutMs + RPC_DEADLINE_GRACE_MS,
10043
+ `permission RPC did not settle within ${rpcTimeoutMs}ms`
10044
+ );
10045
+ } catch (error) {
10046
+ ack = { ok: false, error: error instanceof Error ? error.message : "RPC call failed" };
10047
+ } finally {
10048
+ transport.close();
10049
+ }
10050
+ const outcome = interpretPermissionAck(ack, (encrypted) => decrypt(key, persisted.encryptionVariant, decodeBase64(encrypted)));
10051
+ if (outcome.status !== "acknowledged") {
10052
+ return { sessionId, requestId, payload, outcome };
10053
+ }
10054
+ const sleep = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
10055
+ const deadline = now() + (deps.settleTimeoutMs ?? SETTLE_TIMEOUT_MS);
10056
+ let settled = false;
10057
+ for (; ; ) {
10058
+ const pending = pendingRequestsOf(await fetchState(sessionId, persisted, token), now());
10059
+ if (!pending.some((request) => request.id === requestId)) {
10060
+ settled = true;
10061
+ break;
10062
+ }
10063
+ if (now() >= deadline) break;
10064
+ await sleep(SETTLE_POLL_MS);
10065
+ }
10066
+ return { sessionId, requestId, payload, outcome, settled };
10067
+ }
10068
+ async function bearerToken$1() {
10069
+ const credentials = await readCredentialsForConfiguredRelay();
10070
+ if (!credentials) throw new Error("CLI is not authenticated (no ~/.happy/access.key)");
10071
+ return credentials.token;
10072
+ }
10073
+ async function fetchDecryptedAgentState(sessionId, persisted, token) {
10074
+ const response = await axios.get(
10075
+ `${configuration.serverUrl}/v1/sessions/${encodeURIComponent(sessionId)}`,
10076
+ {
10077
+ headers: {
10078
+ "Authorization": `Bearer ${token}`,
10079
+ "X-Happy-Client": `${PERMISSION_OPS_CLIENT}/${configuration.currentCliVersion}`
10080
+ },
10081
+ timeout: 15e3,
10082
+ // 404 is a real answer ("not yours / gone"), not a transport fault.
10083
+ validateStatus: (status) => status >= 200 && status < 300 || status === 404
10084
+ }
10085
+ );
10086
+ if (response.status === 404) {
10087
+ 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).`);
10088
+ }
10089
+ const raw = response.data?.session?.agentState;
10090
+ if (typeof raw !== "string" || raw.length === 0) return null;
10091
+ return decrypt(decodeBase64(persisted.encryptionKey), persisted.encryptionVariant, decodeBase64(raw));
10092
+ }
10093
+
9670
10094
  const SESSION_OPS_CLIENT = "session-ops";
9671
10095
  const DEFAULT_RECENT_LIMIT = 15;
9672
10096
  const MAX_READ_LIMIT = 100;
@@ -9678,10 +10102,7 @@ function mergeSessionSummaries(live, persisted, options = {}) {
9678
10102
  const id = typeof child.happySessionId === "string" ? child.happySessionId : void 0;
9679
10103
  if (!id || seen.has(id)) continue;
9680
10104
  seen.add(id);
9681
- summaries.push(toSummary(id, persisted[id], {
9682
- live: true,
9683
- pid: typeof child.pid === "number" ? child.pid : void 0
9684
- }));
10105
+ summaries.push(toSummary(id, persisted[id], livenessOf(child)));
9685
10106
  }
9686
10107
  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);
9687
10108
  for (const [id, entry] of rest) {
@@ -9691,6 +10112,13 @@ function mergeSessionSummaries(live, persisted, options = {}) {
9691
10112
  const wanted = options.tag;
9692
10113
  return summaries.filter((summary) => summary.tags?.includes(wanted) === true);
9693
10114
  }
10115
+ function livenessOf(child) {
10116
+ return { live: true, pid: typeof child.pid === "number" ? child.pid : void 0 };
10117
+ }
10118
+ function sessionLiveness(live, sessionId) {
10119
+ const child = live.find((entry) => entry.happySessionId === sessionId);
10120
+ return child ? livenessOf(child) : { live: false };
10121
+ }
9694
10122
  function toSummary(id, persisted, extra) {
9695
10123
  const meta = persisted?.metadata;
9696
10124
  return {
@@ -9710,6 +10138,79 @@ async function listSessions(options = {}) {
9710
10138
  const live = await listDaemonSessions();
9711
10139
  return mergeSessionSummaries(live, readPersistedSessions(), options);
9712
10140
  }
10141
+ function summarizeAccountSession(row, persisted, liveIds, now) {
10142
+ const base = {
10143
+ id: row.id,
10144
+ live: liveIds.has(row.id),
10145
+ url: sessionWebUrl(row.id),
10146
+ decryptable: false,
10147
+ active: row.active === true,
10148
+ archived: typeof row.archivedAt === "number",
10149
+ ...typeof row.activeAt === "number" ? { activeAt: row.activeAt } : {},
10150
+ ...typeof row.updatedAt === "number" ? { updatedAt: row.updatedAt } : {},
10151
+ attention: false
10152
+ };
10153
+ if (!persisted) return base;
10154
+ const key = decodeBase64(persisted.encryptionKey);
10155
+ let metadata = null;
10156
+ let agentState = null;
10157
+ try {
10158
+ metadata = row.metadata ? decrypt(key, persisted.encryptionVariant, decodeBase64(row.metadata)) : null;
10159
+ agentState = row.agentState ? decrypt(key, persisted.encryptionVariant, decodeBase64(row.agentState)) : null;
10160
+ } catch {
10161
+ return base;
10162
+ }
10163
+ if (!metadata) return base;
10164
+ const meta = metadata;
10165
+ const pending = pendingRequestsOf(agentState, now);
10166
+ return {
10167
+ ...base,
10168
+ decryptable: true,
10169
+ ...meta.summary?.text ? { title: meta.summary.text } : {},
10170
+ ...meta.path ? { cwd: meta.path } : {},
10171
+ ...meta.flavor ? { flavor: meta.flavor } : {},
10172
+ ...meta.tags?.length ? { tags: [...meta.tags] } : {},
10173
+ ...meta.variant ? { variant: meta.variant } : {},
10174
+ ...meta.machineId ? { machineId: meta.machineId } : {},
10175
+ ...persisted.savedAt !== void 0 ? { savedAt: persisted.savedAt } : {},
10176
+ pending,
10177
+ attention: pending.length > 0
10178
+ };
10179
+ }
10180
+ function orderAccountSessions(summaries, options = {}) {
10181
+ const oldestWait = (summary) => Math.max(0, ...(summary.pending ?? []).map((request) => request.waitingMs ?? 0));
10182
+ const rank = (summary) => summary.attention ? 0 : summary.live ? 1 : 2;
10183
+ let rows = summaries.filter((summary) => summary.flavor !== "terminal-mirror").slice().sort((a, b) => {
10184
+ const byRank = rank(a) - rank(b);
10185
+ if (byRank !== 0) return byRank;
10186
+ if (a.attention && b.attention) return oldestWait(b) - oldestWait(a);
10187
+ return (b.updatedAt ?? 0) - (a.updatedAt ?? 0);
10188
+ });
10189
+ if (options.tag) {
10190
+ const wanted = options.tag;
10191
+ rows = rows.filter((summary) => summary.tags?.includes(wanted) === true);
10192
+ }
10193
+ const recentLimit = Math.max(0, options.recentLimit ?? DEFAULT_RECENT_LIMIT);
10194
+ let idleKept = 0;
10195
+ return rows.filter((summary) => rank(summary) < 2 || idleKept++ < recentLimit);
10196
+ }
10197
+ async function listAccountSessions(options = {}) {
10198
+ const token = await bearerToken();
10199
+ const response = await axios.get(`${configuration.serverUrl}/v1/sessions`, {
10200
+ headers: {
10201
+ "Authorization": `Bearer ${token}`,
10202
+ "X-Happy-Client": `${SESSION_OPS_CLIENT}/${configuration.currentCliVersion}`
10203
+ },
10204
+ timeout: 15e3
10205
+ });
10206
+ const rows = Array.isArray(response.data?.sessions) ? response.data.sessions : [];
10207
+ const persisted = readPersistedSessions();
10208
+ const live = await listDaemonSessions();
10209
+ const liveIds = new Set(live.map((child) => child.happySessionId).filter((id) => typeof id === "string"));
10210
+ const now = Date.now();
10211
+ const summaries = rows.filter((row) => typeof row?.id === "string").map((row) => summarizeAccountSession(row, persisted[row.id], liveIds, now)).filter((summary) => options.includeArchived || !summary.archived);
10212
+ return orderAccountSessions(summaries, options);
10213
+ }
9713
10214
  async function bearerToken() {
9714
10215
  const credentials = await readCredentialsForConfiguredRelay();
9715
10216
  if (!credentials) throw new Error("CLI is not authenticated (no ~/.happy/access.key)");
@@ -9738,6 +10239,7 @@ async function readSessionTranscript(sessionId, limit) {
9738
10239
  const messages = Array.isArray(response.data?.messages) ? response.data.messages : [];
9739
10240
  messages.reverse();
9740
10241
  const key = decodeBase64(persisted.encryptionKey);
10242
+ const live = await listDaemonSessions();
9741
10243
  const bodies = messages.map((message) => {
9742
10244
  if (message.content?.t !== "encrypted") return null;
9743
10245
  try {
@@ -9747,7 +10249,7 @@ async function readSessionTranscript(sessionId, limit) {
9747
10249
  }
9748
10250
  });
9749
10251
  return {
9750
- summary: toSummary(sessionId, persisted, { live: false }),
10252
+ summary: toSummary(sessionId, persisted, sessionLiveness(live, sessionId)),
9751
10253
  messageCount: messages.length,
9752
10254
  transcript: formatTranscript(bodies)
9753
10255
  };
@@ -9843,13 +10345,16 @@ function normalizeSpawnDirectory(input, homeDir) {
9843
10345
  return { ok: true, directory: normalize(expanded) };
9844
10346
  }
9845
10347
 
9846
- const ASSISTANT_TOOL_NAMES = [
10348
+ const ASSISTANT_SESSION_TOOL_NAMES = [
9847
10349
  "sessions_list",
9848
10350
  "session_read",
9849
10351
  "session_send",
9850
10352
  "session_spawn",
9851
10353
  "session_kill",
9852
- "session_archive",
10354
+ "session_archive"
10355
+ ];
10356
+ const ASSISTANT_TOOL_NAMES = [
10357
+ ...ASSISTANT_SESSION_TOOL_NAMES,
9853
10358
  "terminals_list",
9854
10359
  "terminal_read",
9855
10360
  "terminal_send",
@@ -9886,6 +10391,10 @@ function describeSummary(summary) {
9886
10391
  return parts.join(" ");
9887
10392
  }
9888
10393
  function registerAssistantTools(mcp) {
10394
+ registerAssistantSessionTools(mcp);
10395
+ registerAssistantMachineTools(mcp);
10396
+ }
10397
+ function registerAssistantSessionTools(mcp) {
9889
10398
  mcp.registerTool("sessions_list", {
9890
10399
  description: "List Claude Code sessions on this machine: sessions currently tracked by the local daemon (running) plus recently seen ones. Returns id, title, working directory, agent flavor and web URL for each.",
9891
10400
  title: "List Sessions",
@@ -10002,6 +10511,8 @@ ${body}`);
10002
10511
  return fail(`Failed to archive session: ${error instanceof Error ? error.message : String(error)}`);
10003
10512
  }
10004
10513
  });
10514
+ }
10515
+ function registerAssistantMachineTools(mcp) {
10005
10516
  mcp.registerTool("terminals_list", {
10006
10517
  description: "List the web terminals (tmux sessions) on this machine with their id, title and working directory.",
10007
10518
  title: "List Terminals",
@@ -10671,7 +11182,7 @@ function registerSideQuestionHandler(rpc, deps) {
10671
11182
  const retainMs = deps.retainMs ?? SIDE_QUESTION_RETAIN_MS;
10672
11183
  const maxRunMs = deps.maxRunMs ?? SIDE_QUESTION_MAX_RUN_MS;
10673
11184
  const run = deps.run ?? (async (input) => {
10674
- const { query } = await import('./index-B2KfhNku.mjs');
11185
+ const { query } = await import('./index-B0o3H6_x.mjs');
10675
11186
  return runSideQuestion(query, input);
10676
11187
  });
10677
11188
  const slots = /* @__PURE__ */ new Map();
@@ -11211,7 +11722,7 @@ async function runClaude(credentials, options = {}) {
11211
11722
  const forkedFromMessageId = process.env.HAPPY_FORKED_FROM_MESSAGE_ID;
11212
11723
  const importedFromClaudeSessionId = process.env.HAPPY_IMPORTED_FROM_CLAUDE_SESSION_ID;
11213
11724
  const importTitle = process.env.HAPPY_IMPORT_TITLE;
11214
- const originTags = spawnOriginTags();
11725
+ const originTags = spawnOriginTags(process.env, "claude");
11215
11726
  let metadata = {
11216
11727
  path: workingDirectory,
11217
11728
  host: os.hostname(),
@@ -15601,7 +16112,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
15601
16112
  process.exit(0);
15602
16113
  } else if (subcommand === "install-terminal-hooks") {
15603
16114
  try {
15604
- const { installTerminalHooks, parseTerminalHooksArgs, TERMINAL_HOOKS_HELP } = await import('./installTerminalHooks-DD6QuHfT.mjs');
16115
+ const { installTerminalHooks, parseTerminalHooksArgs, TERMINAL_HOOKS_HELP } = await import('./installTerminalHooks-DeYqyjCB.mjs');
15605
16116
  const command = parseTerminalHooksArgs(args.slice(1));
15606
16117
  if (command.action === "help") {
15607
16118
  console.log(TERMINAL_HOOKS_HELP);
@@ -15618,7 +16129,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
15618
16129
  }
15619
16130
  } else if (subcommand === "spawn") {
15620
16131
  try {
15621
- const { handleSpawnCommand } = await import('./spawn-qb344Y5E.mjs');
16132
+ const { handleSpawnCommand } = await import('./spawn-CsrHZd3y.mjs');
15622
16133
  await handleSpawnCommand(args.slice(1));
15623
16134
  } catch (error) {
15624
16135
  console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
@@ -15630,7 +16141,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
15630
16141
  return;
15631
16142
  } else if (subcommand === "sessions") {
15632
16143
  try {
15633
- const { handleSessionsCommand } = await import('./sessions-CmabJzYS.mjs');
16144
+ const { handleSessionsCommand } = await import('./sessions-DYPB3Peg.mjs');
15634
16145
  await handleSessionsCommand(args.slice(1));
15635
16146
  } catch (error) {
15636
16147
  console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
@@ -15642,7 +16153,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
15642
16153
  return;
15643
16154
  } else if (subcommand === "send") {
15644
16155
  try {
15645
- const { handleSendCommand } = await import('./send-C6G2jsQp.mjs');
16156
+ const { handleSendCommand } = await import('./send-CYznZGrp.mjs');
15646
16157
  await handleSendCommand(args.slice(1));
15647
16158
  } catch (error) {
15648
16159
  console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
@@ -15748,9 +16259,9 @@ Conversation history is preserved on the server, but in-flight tool calls are in
15748
16259
  if (geminiSubcommand === "project" && args[2] === "set" && args[3]) {
15749
16260
  const projectId = args[3];
15750
16261
  try {
15751
- const { saveGoogleCloudProjectToConfig } = await import('./config-CMgRBQMS.mjs');
15752
- const { readCredentialsForConfiguredRelay: readCredentialsForConfiguredRelay2 } = await import('./types-B3qx8Ga1.mjs').then(function (n) { return n.aq; });
15753
- const { ApiClient: ApiClient2 } = await import('./types-B3qx8Ga1.mjs').then(function (n) { return n.ar; });
16262
+ const { saveGoogleCloudProjectToConfig } = await import('./config-CnA3JIUn.mjs');
16263
+ const { readCredentialsForConfiguredRelay: readCredentialsForConfiguredRelay2 } = await import('./types-jSfU_0o1.mjs').then(function (n) { return n.as; });
16264
+ const { ApiClient: ApiClient2 } = await import('./types-jSfU_0o1.mjs').then(function (n) { return n.at; });
15754
16265
  let userEmail = void 0;
15755
16266
  try {
15756
16267
  const credentials = await readCredentialsForConfiguredRelay2();
@@ -15781,7 +16292,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
15781
16292
  }
15782
16293
  if (geminiSubcommand === "project" && args[2] === "get") {
15783
16294
  try {
15784
- const { readGeminiLocalConfig } = await import('./config-CMgRBQMS.mjs');
16295
+ const { readGeminiLocalConfig } = await import('./config-CnA3JIUn.mjs');
15785
16296
  const config = readGeminiLocalConfig();
15786
16297
  if (config.googleCloudProject) {
15787
16298
  console.log(`Current Google Cloud Project: ${config.googleCloudProject}`);
@@ -15821,7 +16332,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
15821
16332
  process.exit(0);
15822
16333
  }
15823
16334
  try {
15824
- const { runGemini } = await import('./runGemini-DdJqpywr.mjs');
16335
+ const { runGemini } = await import('./runGemini-CEURRmM4.mjs');
15825
16336
  let startedBy = void 0;
15826
16337
  for (let i = 1; i < args.length; i++) {
15827
16338
  if (args[i] === "--started-by") {
@@ -15841,9 +16352,44 @@ Conversation history is preserved on the server, but in-flight tool calls are in
15841
16352
  process.exit(1);
15842
16353
  }
15843
16354
  return;
16355
+ } else if (subcommand === "pi") {
16356
+ let hintPrinted = false;
16357
+ const printHint = (detail) => {
16358
+ const hint = piAdapterMissingHint(detail);
16359
+ if (hint && !hintPrinted) {
16360
+ hintPrinted = true;
16361
+ console.error(chalk.gray(hint));
16362
+ }
16363
+ };
16364
+ try {
16365
+ const { runAcp, resolveAcpAgentConfig } = await import('./index-DiRN0Rv8.mjs');
16366
+ const parsed = parsePiRunnerArgs(args.slice(1));
16367
+ const resolved = resolveAcpAgentConfig(["pi", ...parsed.passthrough]);
16368
+ const { credentials } = await authAndSetupMachineIfNeeded();
16369
+ await ensureDaemonRunning();
16370
+ await runAcp({
16371
+ credentials,
16372
+ startedBy: parsed.startedBy,
16373
+ verbose: parsed.verbose,
16374
+ permissionMode: parsed.permissionMode,
16375
+ agentName: resolved.agentName,
16376
+ command: resolved.command,
16377
+ args: resolved.args,
16378
+ onBackendError: printHint
16379
+ });
16380
+ if (hintPrinted) process.exit(1);
16381
+ } catch (error) {
16382
+ console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
16383
+ printHint(error instanceof Error ? error.message : void 0);
16384
+ if (process.env.DEBUG) {
16385
+ console.error(error);
16386
+ }
16387
+ process.exit(1);
16388
+ }
16389
+ return;
15844
16390
  } else if (subcommand === "acp") {
15845
16391
  try {
15846
- const { runAcp, resolveAcpAgentConfig } = await import('./index-CpGR7rIw.mjs');
16392
+ const { runAcp, resolveAcpAgentConfig } = await import('./index-DiRN0Rv8.mjs');
15847
16393
  let startedBy = void 0;
15848
16394
  let verbose = false;
15849
16395
  const acpArgs = [];
@@ -15883,7 +16429,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
15883
16429
  return;
15884
16430
  } else if (subcommand === "openclaw") {
15885
16431
  try {
15886
- const { runOpenClaw } = await import('./runOpenClaw-CM5UrVBy.mjs');
16432
+ const { runOpenClaw } = await import('./runOpenClaw-BozfKWng.mjs');
15887
16433
  let startedBy = void 0;
15888
16434
  let verbose = false;
15889
16435
  let gatewayUrl;
@@ -15934,7 +16480,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
15934
16480
  return;
15935
16481
  } else if (subcommand === "mcp" && args.length === 1) {
15936
16482
  try {
15937
- const { handleMcpCommand } = await import('./mcp-CiNrjYox.mjs');
16483
+ const { handleMcpCommand } = await import('./mcp-D_5aOK0I.mjs');
15938
16484
  await handleMcpCommand();
15939
16485
  } catch (error) {
15940
16486
  process.stderr.write(`[very-happy mcp] Fatal: ${error instanceof Error ? error.message : String(error)}
@@ -16149,10 +16695,12 @@ ${chalk.bold("Usage:")}
16149
16695
  print its web URL (for automation; see spawn --help)
16150
16696
  very-happy send Send a message into an existing session
16151
16697
  (for automation; see send --help)
16152
- very-happy sessions List, read, stop or archive sessions on this
16153
- machine (for automation; see sessions --help)
16698
+ very-happy sessions List, read, stop, archive, approve or deny
16699
+ sessions and their permission requests (for
16700
+ automation; see sessions --help)
16154
16701
  very-happy codex Start Codex mode
16155
16702
  very-happy gemini Start Gemini mode (ACP)
16703
+ very-happy pi Start pi mode (ACP via the pi-acp adapter)
16156
16704
  very-happy acp Start a generic ACP-compatible agent
16157
16705
  very-happy openclaw Connect through a configured OpenClaw gateway
16158
16706
  very-happy install-terminal-hooks
@@ -16178,6 +16726,7 @@ ${chalk.bold("Examples:")}
16178
16726
  very-happy --js-runtime bun Use bun instead of node to spawn Claude Code
16179
16727
  very-happy --claude-env ANTHROPIC_BASE_URL=http://127.0.0.1:3456
16180
16728
  Use a custom API endpoint (e.g., claude-code-router)
16729
+ very-happy pi Start pi (needs pi-acp on PATH)
16181
16730
  very-happy acp gemini Start Gemini via generic ACP runner
16182
16731
  very-happy acp opencode Start OpenCode through its built-in ACP adapter
16183
16732
  very-happy acp -- your-agent --acp
@@ -16292,4 +16841,4 @@ ${chalk.bold("Examples:")}
16292
16841
  }
16293
16842
  }
16294
16843
 
16295
- export { ALLOWED_SPAWN_PERMISSION_MODES as A, BasePermissionHandler as B, MessageBuffer as C, DEFAULT_RECENT_LIMIT as D, CHANGE_TITLE_INSTRUCTION as E, GEMINI_MODEL_ENV as G, MAX_READ_LIMIT as M, sessionWebUrl as a, sanitizeSpawnPermissionMode as b, checkIfDaemonRunningAndCleanupStaleState as c, sendUserMessage as d, stopSession as e, archiveSession as f, DEFAULT_GEMINI_MODEL as g, initialMachineMetadata as h, isValidSpawnOrigin as i, createSessionMetadata as j, setupOfflineReconnection as k, listSessions as l, MessageQueue2 as m, notifyDaemonSessionStarted as n, hashObject as o, pushClipboardViaDaemon as p, query as q, readSessionTranscript as r, spawnDaemonSession as s, tmuxSupportsSessionEnv as t, startHappyServer as u, registerKillSessionHandler as v, waitForSessionKey as w, GEMINI_API_KEY_ENV as x, GOOGLE_API_KEY_ENV as y, BaseReasoningProcessor as z };
16844
+ export { ALLOWED_SPAWN_PERMISSION_MODES as A, registerKillSessionHandler as B, BasePermissionHandler as C, DEFAULT_RECENT_LIMIT as D, writeSessionModeFile 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, MAX_READ_LIMIT as M, CHANGE_TITLE_INSTRUCTION as N, SPAWN_AGENTS as S, TitleGenerator as T, sessionWebUrl as a, sanitizeSpawnPermissionMode as b, checkIfDaemonRunningAndCleanupStaleState as c, sendUserMessage as d, listSessions as e, stopSession as f, resolvePermissionRequest as g, archiveSession as h, isValidSpawnOrigin as i, DEFAULT_GEMINI_MODEL as j, setTerminalTitleViaDaemon as k, listAccountSessions as l, registerAssistantSessionTools as m, initialMachineMetadata as n, createSessionMetadata as o, pushClipboardViaDaemon as p, query as q, readSessionTranscript as r, spawnDaemonSession as s, tmuxSupportsSessionEnv as t, setupOfflineReconnection as u, notifyDaemonSessionStarted as v, waitForSessionKey as w, MessageQueue2 as x, hashObject as y, startHappyServer as z };