very-happy-cli 0.2.116 → 0.2.118

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-CfKPvoTU.mjs → AcpBackend-Bk9b823B.mjs} +116 -12
  2. package/dist/{AcpBackend-CauAQpC1.cjs → AcpBackend-p_KNJkHc.cjs} +116 -12
  3. package/dist/{AcpSessionManager-CA6xeET_.cjs → AcpSessionManager-B9FTbpGQ.cjs} +15 -2
  4. package/dist/{AcpSessionManager-BQ6pBRW4.mjs → AcpSessionManager-CDvqVHKf.mjs} +15 -2
  5. package/dist/{config-BpR_CwZu.mjs → config-CIpgmGLR.mjs} +2 -2
  6. package/dist/{config-BoteFn-v.cjs → config-CkUn1dI4.cjs} +2 -2
  7. package/dist/{index-C45xN8s0.mjs → index-B6cpyG_G.mjs} +78 -8
  8. package/dist/{index-D7azxfcs.mjs → index-BUVQf9PI.mjs} +2 -2
  9. package/dist/{index-4jiUeiUz.cjs → index-BXOXRblP.cjs} +513 -47
  10. package/dist/{index-DWmhmji9.cjs → index-BzgtwTxC.cjs} +77 -7
  11. package/dist/{index-lX1UliOs.cjs → index-CxjX8DGz.cjs} +2 -2
  12. package/dist/{index-Dq-eFp89.mjs → index-DIXjhnV1.mjs} +502 -45
  13. package/dist/index.cjs +2 -2
  14. package/dist/index.mjs +2 -2
  15. package/dist/{installTerminalHooks-k3TrBHD0.mjs → installTerminalHooks-BAa6LaQQ.mjs} +2 -2
  16. package/dist/{installTerminalHooks-DnaFkrTF.cjs → installTerminalHooks-lXQa6bQU.cjs} +2 -2
  17. package/dist/lib.cjs +1 -1
  18. package/dist/lib.d.cts +43 -1
  19. package/dist/lib.d.mts +43 -1
  20. package/dist/lib.mjs +1 -1
  21. package/dist/{mcp-DSpEpM_2.cjs → mcp-DMD7FxBI.cjs} +62 -16
  22. package/dist/{mcp-DRePTOap.mjs → mcp-Dwc299mU.mjs} +62 -17
  23. package/dist/{runGemini-_9-a7duU.mjs → runGemini-BaxGXE9w.mjs} +4 -4
  24. package/dist/{runGemini-IWHyBq0A.cjs → runGemini-Bj4Egq7j.cjs} +4 -4
  25. package/dist/{runOpenClaw-0myYIz1U.cjs → runOpenClaw-BJhSvXYy.cjs} +3 -3
  26. package/dist/{runOpenClaw-Boq6LQ9a.mjs → runOpenClaw-BwJhbIKU.mjs} +3 -3
  27. package/dist/{send-CwWvFfay.cjs → send-BWGUkWhi.cjs} +2 -2
  28. package/dist/{send-Cyo6mgMl.mjs → send-DX13svrS.mjs} +2 -2
  29. package/dist/sessions-CGhpBNqn.cjs +322 -0
  30. package/dist/sessions-L7O3ypda.mjs +317 -0
  31. package/dist/{spawn-DQ6FTUK9.cjs → spawn-CbY233Eg.cjs} +6 -7
  32. package/dist/{spawn-CU0DMauA.mjs → spawn-DPDWZdoW.mjs} +3 -4
  33. package/dist/{types-Ds0SiQ1D.mjs → types-BtuOM9lw.mjs} +52 -19
  34. package/dist/{types-C5kEpkcZ.cjs → types-ClvvHves.cjs} +54 -19
  35. package/package.json +7 -7
  36. package/dist/sessions-CbzqYiAy.cjs +0 -204
  37. package/dist/sessions-lvfTH_ql.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-Ds0SiQ1D.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, g as contentLogMetadata, I as InvalidateSync, R as RawJSONLinesSchema, k as delay, v as AsyncLock, S as STREAM_DELTA_MAX_CHARS, x as ensurePrivateDirectory, M as MAX_CHAT_ATTACHMENT_SOURCE_BYTES, y as hardenPrivateFile, j as packageJson, z as commandOnPath, e as readSettings, B as readCredentials, C as credentialRelayProblem, D as deriveLocalCliUpdateSummary, f 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, d 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-BtuOM9lw.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(),
@@ -7099,9 +7218,12 @@ function decideHandover(run) {
7099
7218
 
7100
7219
  function decideAutoUpdate(context) {
7101
7220
  if (!context.enabled) return { action: "skip", reason: "auto-update disabled" };
7102
- const target = context.recommendedVersion;
7103
- if (!target) return { action: "skip", reason: "no recommended version published" };
7104
- if (target === context.currentVersion) return { action: "skip", reason: "already current" };
7221
+ const target = context.autoUpdateVersion;
7222
+ if (!target) return { action: "skip", reason: "no version approved for automatic install" };
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 approved ${target}` };
7105
7227
  if (context.failedVersion === target) {
7106
7228
  return { action: "skip", reason: `already failed to install ${target} once` };
7107
7229
  }
@@ -8330,6 +8452,7 @@ function sanitizeImportTitle(value) {
8330
8452
  return collapsed.length > 200 ? `${collapsed.slice(0, 199)}\u2026` : collapsed;
8331
8453
  }
8332
8454
  const hostSuffix = process.env.HAPPY_VARIANT === "dev" ? "-dev" : "";
8455
+ const startupCliAvailability = detectCLIAvailability();
8333
8456
  const initialMachineMetadata = {
8334
8457
  host: os$1.hostname() + hostSuffix,
8335
8458
  platform: os$1.platform(),
@@ -8337,7 +8460,7 @@ const initialMachineMetadata = {
8337
8460
  homeDir: os$1.homedir(),
8338
8461
  happyHomeDir: configuration.happyHomeDir,
8339
8462
  happyLibDir: projectPath(),
8340
- cliAvailability: detectCLIAvailability(),
8463
+ cliAvailability: startupCliAvailability,
8341
8464
  resumeSupport: { ...detectResumeSupport(), rpcAvailable: true }
8342
8465
  };
8343
8466
  async function preflightNewBundle(bundlePath) {
@@ -8595,18 +8718,26 @@ async function startDaemon() {
8595
8718
  };
8596
8719
  const assistantSpawnGate = createSpawnGate();
8597
8720
  const spawnSession = (options) => {
8598
- if (options.variant !== "assistant") {
8721
+ if (assistantSpawnMode(options) !== "claude-singleton") {
8599
8722
  return spawnSessionImpl(options);
8600
8723
  }
8601
8724
  return options.forceNew ? assistantSpawnGate.replace(() => spawnSessionImpl(options)) : assistantSpawnGate.join(() => spawnSessionImpl(options));
8602
8725
  };
8726
+ const currentCliAvailability = () => apiMachineRef?.getCLIAvailability() ?? startupCliAvailability;
8603
8727
  const spawnSessionImpl = async (options) => {
8604
8728
  logger.debugLargeJson("[DAEMON RUN] Spawning session", summarizeSpawnSessionForLog(options));
8605
8729
  const spawnPermissionMode = sanitizeSpawnPermissionMode(options.permissionMode);
8606
8730
  if (options.permissionMode !== void 0 && spawnPermissionMode === null) {
8607
8731
  logger.warn("[DAEMON RUN] Ignoring invalid permissionMode in spawn request");
8608
8732
  }
8609
- 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") {
8610
8741
  options = { ...options, directory: assistantHome() };
8611
8742
  if (options.forceNew) {
8612
8743
  for (const tracked of [...pidToTrackedSession.values()]) {
@@ -8809,7 +8940,13 @@ async function startDaemon() {
8809
8940
  logger.debug(`[DAEMON RUN] Attempting to spawn session in tmux: ${sessionDesc}`);
8810
8941
  const tmux = getTmuxUtilities(tmuxSessionName);
8811
8942
  const cliPath = join$1(projectPath(), "dist", "index.mjs");
8812
- 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
+ }
8813
8950
  const resumeId = agent === "claude" ? options.resumeClaudeSessionId : agent === "codex" ? options.resumeCodexThreadId : void 0;
8814
8951
  const resumeFragment = resumeId ? ` --resume ${shellescape(resumeId)}` : "";
8815
8952
  const permissionModeFragment = spawnPermissionMode ? ` --permission-mode ${shellescape(spawnPermissionMode)}` : "";
@@ -8837,7 +8974,7 @@ async function startDaemon() {
8837
8974
  pid: tmuxResult.pid,
8838
8975
  // Real PID from tmux -P flag
8839
8976
  tmuxSessionId: tmuxResult.sessionId,
8840
- variant: options.variant,
8977
+ variant: trackedVariant,
8841
8978
  spawnedBy: options.spawnedBy,
8842
8979
  directoryCreated,
8843
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.`
@@ -8884,6 +9021,9 @@ async function startDaemon() {
8884
9021
  case "openclaw":
8885
9022
  agentCommand = "openclaw";
8886
9023
  break;
9024
+ case "pi":
9025
+ agentCommand = "pi";
9026
+ break;
8887
9027
  default:
8888
9028
  return {
8889
9029
  type: "error",
@@ -8915,7 +9055,7 @@ async function startDaemon() {
8915
9055
  },
8916
9056
  directoryCreated,
8917
9057
  message: directoryCreated ? `The path '${directory}' did not exist. We created a new folder and spawned a new session there.` : void 0,
8918
- variant: options.variant,
9058
+ variant: trackedVariant,
8919
9059
  spawnedBy: options.spawnedBy
8920
9060
  });
8921
9061
  }
@@ -9396,6 +9536,10 @@ async function startDaemon() {
9396
9536
  },
9397
9537
  onTerminalHook: (body) => {
9398
9538
  mirrorManagerRef?.handleHookPayload(body);
9539
+ },
9540
+ setTerminalTitle: (terminalId, title, ifAbsent) => {
9541
+ if (!apiMachineRef) return "starting";
9542
+ return apiMachineRef.setTerminalTitle(terminalId, title, ifAbsent);
9399
9543
  }
9400
9544
  });
9401
9545
  const daemonClaudeCredentials = resolveClaudeCredentialReadiness();
@@ -9500,7 +9644,7 @@ async function startDaemon() {
9500
9644
  const decision = decideAutoUpdate({
9501
9645
  enabled: (settings.cliAutoUpdate ?? "idle") !== "off",
9502
9646
  currentVersion: cliUpdate.currentVersion,
9503
- recommendedVersion: cliUpdate.recommendedVersion,
9647
+ autoUpdateVersion: cliUpdate.autoUpdateVersion ?? null,
9504
9648
  idle: pidToTrackedSession.size === 0 && !apiMachine.hasLiveTerminals(),
9505
9649
  failedVersion: autoUpdateFailedVersion
9506
9650
  });
@@ -9759,6 +9903,194 @@ function formatTranscript(bodies) {
9759
9903
  return lines.join("\n");
9760
9904
  }
9761
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
+
9762
10094
  const SESSION_OPS_CLIENT = "session-ops";
9763
10095
  const DEFAULT_RECENT_LIMIT = 15;
9764
10096
  const MAX_READ_LIMIT = 100;
@@ -9770,10 +10102,7 @@ function mergeSessionSummaries(live, persisted, options = {}) {
9770
10102
  const id = typeof child.happySessionId === "string" ? child.happySessionId : void 0;
9771
10103
  if (!id || seen.has(id)) continue;
9772
10104
  seen.add(id);
9773
- summaries.push(toSummary(id, persisted[id], {
9774
- live: true,
9775
- pid: typeof child.pid === "number" ? child.pid : void 0
9776
- }));
10105
+ summaries.push(toSummary(id, persisted[id], livenessOf(child)));
9777
10106
  }
9778
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);
9779
10108
  for (const [id, entry] of rest) {
@@ -9783,6 +10112,13 @@ function mergeSessionSummaries(live, persisted, options = {}) {
9783
10112
  const wanted = options.tag;
9784
10113
  return summaries.filter((summary) => summary.tags?.includes(wanted) === true);
9785
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
+ }
9786
10122
  function toSummary(id, persisted, extra) {
9787
10123
  const meta = persisted?.metadata;
9788
10124
  return {
@@ -9802,6 +10138,79 @@ async function listSessions(options = {}) {
9802
10138
  const live = await listDaemonSessions();
9803
10139
  return mergeSessionSummaries(live, readPersistedSessions(), options);
9804
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
+ }
9805
10214
  async function bearerToken() {
9806
10215
  const credentials = await readCredentialsForConfiguredRelay();
9807
10216
  if (!credentials) throw new Error("CLI is not authenticated (no ~/.happy/access.key)");
@@ -9830,6 +10239,7 @@ async function readSessionTranscript(sessionId, limit) {
9830
10239
  const messages = Array.isArray(response.data?.messages) ? response.data.messages : [];
9831
10240
  messages.reverse();
9832
10241
  const key = decodeBase64(persisted.encryptionKey);
10242
+ const live = await listDaemonSessions();
9833
10243
  const bodies = messages.map((message) => {
9834
10244
  if (message.content?.t !== "encrypted") return null;
9835
10245
  try {
@@ -9839,7 +10249,7 @@ async function readSessionTranscript(sessionId, limit) {
9839
10249
  }
9840
10250
  });
9841
10251
  return {
9842
- summary: toSummary(sessionId, persisted, { live: false }),
10252
+ summary: toSummary(sessionId, persisted, sessionLiveness(live, sessionId)),
9843
10253
  messageCount: messages.length,
9844
10254
  transcript: formatTranscript(bodies)
9845
10255
  };
@@ -9935,13 +10345,16 @@ function normalizeSpawnDirectory(input, homeDir) {
9935
10345
  return { ok: true, directory: normalize(expanded) };
9936
10346
  }
9937
10347
 
9938
- const ASSISTANT_TOOL_NAMES = [
10348
+ const ASSISTANT_SESSION_TOOL_NAMES = [
9939
10349
  "sessions_list",
9940
10350
  "session_read",
9941
10351
  "session_send",
9942
10352
  "session_spawn",
9943
10353
  "session_kill",
9944
- "session_archive",
10354
+ "session_archive"
10355
+ ];
10356
+ const ASSISTANT_TOOL_NAMES = [
10357
+ ...ASSISTANT_SESSION_TOOL_NAMES,
9945
10358
  "terminals_list",
9946
10359
  "terminal_read",
9947
10360
  "terminal_send",
@@ -9978,6 +10391,10 @@ function describeSummary(summary) {
9978
10391
  return parts.join(" ");
9979
10392
  }
9980
10393
  function registerAssistantTools(mcp) {
10394
+ registerAssistantSessionTools(mcp);
10395
+ registerAssistantMachineTools(mcp);
10396
+ }
10397
+ function registerAssistantSessionTools(mcp) {
9981
10398
  mcp.registerTool("sessions_list", {
9982
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.",
9983
10400
  title: "List Sessions",
@@ -10094,6 +10511,8 @@ ${body}`);
10094
10511
  return fail(`Failed to archive session: ${error instanceof Error ? error.message : String(error)}`);
10095
10512
  }
10096
10513
  });
10514
+ }
10515
+ function registerAssistantMachineTools(mcp) {
10097
10516
  mcp.registerTool("terminals_list", {
10098
10517
  description: "List the web terminals (tmux sessions) on this machine with their id, title and working directory.",
10099
10518
  title: "List Terminals",
@@ -10763,7 +11182,7 @@ function registerSideQuestionHandler(rpc, deps) {
10763
11182
  const retainMs = deps.retainMs ?? SIDE_QUESTION_RETAIN_MS;
10764
11183
  const maxRunMs = deps.maxRunMs ?? SIDE_QUESTION_MAX_RUN_MS;
10765
11184
  const run = deps.run ?? (async (input) => {
10766
- const { query } = await import('./index-D7azxfcs.mjs');
11185
+ const { query } = await import('./index-BUVQf9PI.mjs');
10767
11186
  return runSideQuestion(query, input);
10768
11187
  });
10769
11188
  const slots = /* @__PURE__ */ new Map();
@@ -11303,7 +11722,7 @@ async function runClaude(credentials, options = {}) {
11303
11722
  const forkedFromMessageId = process.env.HAPPY_FORKED_FROM_MESSAGE_ID;
11304
11723
  const importedFromClaudeSessionId = process.env.HAPPY_IMPORTED_FROM_CLAUDE_SESSION_ID;
11305
11724
  const importTitle = process.env.HAPPY_IMPORT_TITLE;
11306
- const originTags = spawnOriginTags();
11725
+ const originTags = spawnOriginTags(process.env, "claude");
11307
11726
  let metadata = {
11308
11727
  path: workingDirectory,
11309
11728
  host: os.hostname(),
@@ -15693,7 +16112,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
15693
16112
  process.exit(0);
15694
16113
  } else if (subcommand === "install-terminal-hooks") {
15695
16114
  try {
15696
- const { installTerminalHooks, parseTerminalHooksArgs, TERMINAL_HOOKS_HELP } = await import('./installTerminalHooks-k3TrBHD0.mjs');
16115
+ const { installTerminalHooks, parseTerminalHooksArgs, TERMINAL_HOOKS_HELP } = await import('./installTerminalHooks-BAa6LaQQ.mjs');
15697
16116
  const command = parseTerminalHooksArgs(args.slice(1));
15698
16117
  if (command.action === "help") {
15699
16118
  console.log(TERMINAL_HOOKS_HELP);
@@ -15710,7 +16129,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
15710
16129
  }
15711
16130
  } else if (subcommand === "spawn") {
15712
16131
  try {
15713
- const { handleSpawnCommand } = await import('./spawn-CU0DMauA.mjs');
16132
+ const { handleSpawnCommand } = await import('./spawn-DPDWZdoW.mjs');
15714
16133
  await handleSpawnCommand(args.slice(1));
15715
16134
  } catch (error) {
15716
16135
  console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
@@ -15722,7 +16141,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
15722
16141
  return;
15723
16142
  } else if (subcommand === "sessions") {
15724
16143
  try {
15725
- const { handleSessionsCommand } = await import('./sessions-lvfTH_ql.mjs');
16144
+ const { handleSessionsCommand } = await import('./sessions-L7O3ypda.mjs');
15726
16145
  await handleSessionsCommand(args.slice(1));
15727
16146
  } catch (error) {
15728
16147
  console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
@@ -15734,7 +16153,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
15734
16153
  return;
15735
16154
  } else if (subcommand === "send") {
15736
16155
  try {
15737
- const { handleSendCommand } = await import('./send-Cyo6mgMl.mjs');
16156
+ const { handleSendCommand } = await import('./send-DX13svrS.mjs');
15738
16157
  await handleSendCommand(args.slice(1));
15739
16158
  } catch (error) {
15740
16159
  console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
@@ -15840,9 +16259,9 @@ Conversation history is preserved on the server, but in-flight tool calls are in
15840
16259
  if (geminiSubcommand === "project" && args[2] === "set" && args[3]) {
15841
16260
  const projectId = args[3];
15842
16261
  try {
15843
- const { saveGoogleCloudProjectToConfig } = await import('./config-BpR_CwZu.mjs');
15844
- const { readCredentialsForConfiguredRelay: readCredentialsForConfiguredRelay2 } = await import('./types-Ds0SiQ1D.mjs').then(function (n) { return n.aq; });
15845
- const { ApiClient: ApiClient2 } = await import('./types-Ds0SiQ1D.mjs').then(function (n) { return n.ar; });
16262
+ const { saveGoogleCloudProjectToConfig } = await import('./config-CIpgmGLR.mjs');
16263
+ const { readCredentialsForConfiguredRelay: readCredentialsForConfiguredRelay2 } = await import('./types-BtuOM9lw.mjs').then(function (n) { return n.as; });
16264
+ const { ApiClient: ApiClient2 } = await import('./types-BtuOM9lw.mjs').then(function (n) { return n.at; });
15846
16265
  let userEmail = void 0;
15847
16266
  try {
15848
16267
  const credentials = await readCredentialsForConfiguredRelay2();
@@ -15873,7 +16292,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
15873
16292
  }
15874
16293
  if (geminiSubcommand === "project" && args[2] === "get") {
15875
16294
  try {
15876
- const { readGeminiLocalConfig } = await import('./config-BpR_CwZu.mjs');
16295
+ const { readGeminiLocalConfig } = await import('./config-CIpgmGLR.mjs');
15877
16296
  const config = readGeminiLocalConfig();
15878
16297
  if (config.googleCloudProject) {
15879
16298
  console.log(`Current Google Cloud Project: ${config.googleCloudProject}`);
@@ -15913,7 +16332,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
15913
16332
  process.exit(0);
15914
16333
  }
15915
16334
  try {
15916
- const { runGemini } = await import('./runGemini-_9-a7duU.mjs');
16335
+ const { runGemini } = await import('./runGemini-BaxGXE9w.mjs');
15917
16336
  let startedBy = void 0;
15918
16337
  for (let i = 1; i < args.length; i++) {
15919
16338
  if (args[i] === "--started-by") {
@@ -15933,9 +16352,44 @@ Conversation history is preserved on the server, but in-flight tool calls are in
15933
16352
  process.exit(1);
15934
16353
  }
15935
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-B6cpyG_G.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;
15936
16390
  } else if (subcommand === "acp") {
15937
16391
  try {
15938
- const { runAcp, resolveAcpAgentConfig } = await import('./index-C45xN8s0.mjs');
16392
+ const { runAcp, resolveAcpAgentConfig } = await import('./index-B6cpyG_G.mjs');
15939
16393
  let startedBy = void 0;
15940
16394
  let verbose = false;
15941
16395
  const acpArgs = [];
@@ -15975,7 +16429,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
15975
16429
  return;
15976
16430
  } else if (subcommand === "openclaw") {
15977
16431
  try {
15978
- const { runOpenClaw } = await import('./runOpenClaw-Boq6LQ9a.mjs');
16432
+ const { runOpenClaw } = await import('./runOpenClaw-BwJhbIKU.mjs');
15979
16433
  let startedBy = void 0;
15980
16434
  let verbose = false;
15981
16435
  let gatewayUrl;
@@ -16026,7 +16480,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
16026
16480
  return;
16027
16481
  } else if (subcommand === "mcp" && args.length === 1) {
16028
16482
  try {
16029
- const { handleMcpCommand } = await import('./mcp-DRePTOap.mjs');
16483
+ const { handleMcpCommand } = await import('./mcp-Dwc299mU.mjs');
16030
16484
  await handleMcpCommand();
16031
16485
  } catch (error) {
16032
16486
  process.stderr.write(`[very-happy mcp] Fatal: ${error instanceof Error ? error.message : String(error)}
@@ -16241,10 +16695,12 @@ ${chalk.bold("Usage:")}
16241
16695
  print its web URL (for automation; see spawn --help)
16242
16696
  very-happy send Send a message into an existing session
16243
16697
  (for automation; see send --help)
16244
- very-happy sessions List, read, stop or archive sessions on this
16245
- 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)
16246
16701
  very-happy codex Start Codex mode
16247
16702
  very-happy gemini Start Gemini mode (ACP)
16703
+ very-happy pi Start pi mode (ACP via the pi-acp adapter)
16248
16704
  very-happy acp Start a generic ACP-compatible agent
16249
16705
  very-happy openclaw Connect through a configured OpenClaw gateway
16250
16706
  very-happy install-terminal-hooks
@@ -16270,6 +16726,7 @@ ${chalk.bold("Examples:")}
16270
16726
  very-happy --js-runtime bun Use bun instead of node to spawn Claude Code
16271
16727
  very-happy --claude-env ANTHROPIC_BASE_URL=http://127.0.0.1:3456
16272
16728
  Use a custom API endpoint (e.g., claude-code-router)
16729
+ very-happy pi Start pi (needs pi-acp on PATH)
16273
16730
  very-happy acp gemini Start Gemini via generic ACP runner
16274
16731
  very-happy acp opencode Start OpenCode through its built-in ACP adapter
16275
16732
  very-happy acp -- your-agent --acp
@@ -16384,4 +16841,4 @@ ${chalk.bold("Examples:")}
16384
16841
  }
16385
16842
  }
16386
16843
 
16387
- 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 };