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
@@ -3,7 +3,7 @@
3
3
  var chalk = require('chalk');
4
4
  var os = require('node:os');
5
5
  var node_crypto = require('node:crypto');
6
- var persistence = require('./types-CpnzfHHS.cjs');
6
+ var persistence = require('./types-DD3AK6Nb.cjs');
7
7
  var spawn = require('cross-spawn');
8
8
  var path = require('node:path');
9
9
  var node_readline = require('node:readline');
@@ -16,7 +16,7 @@ var React = require('react');
16
16
  var claudeAgentSdk = require('@anthropic-ai/claude-agent-sdk');
17
17
  var axios = require('axios');
18
18
  require('node:events');
19
- require('socket.io-client');
19
+ var socket_ioClient = require('socket.io-client');
20
20
  var tweetnacl = require('tweetnacl');
21
21
  var os$1 = require('os');
22
22
  var child_process = require('child_process');
@@ -150,7 +150,9 @@ async function daemonPost(path, body) {
150
150
  signal: AbortSignal.timeout(timeout)
151
151
  });
152
152
  if (!response.ok) {
153
- const errorMessage = `Request failed: ${path}, HTTP ${response.status}`;
153
+ const failure = await response.json().catch(() => null);
154
+ const reason = typeof failure?.error === "string" && failure.error ? failure.error : `HTTP ${response.status}`;
155
+ const errorMessage = `Request failed: ${path}, ${reason}`;
154
156
  persistence.logger.debug(`[CONTROL CLIENT] ${errorMessage}`);
155
157
  return {
156
158
  error: errorMessage
@@ -217,6 +219,13 @@ async function pushClipboardViaDaemon(text) {
217
219
  }
218
220
  return result;
219
221
  }
222
+ async function setTerminalTitleViaDaemon(terminalId, title, ifAbsent = false) {
223
+ const result = await daemonPost("/terminal-title", { terminalId, title, ifAbsent });
224
+ if (result?.error) {
225
+ return { ok: false, error: result.error };
226
+ }
227
+ return { ok: result?.status === "ok", error: result?.status === "ok" ? void 0 : "unexpected daemon response" };
228
+ }
220
229
  async function checkIfDaemonRunningAndCleanupStaleState() {
221
230
  const state = await persistence.readDaemonState();
222
231
  if (!state) {
@@ -4673,8 +4682,8 @@ function claudeModeHash(mode) {
4673
4682
  }
4674
4683
 
4675
4684
  const SPAWN_ORIGIN_TAG_MAX = 24;
4676
- function spawnOriginTags(env = process.env) {
4677
- if (env.HAPPY_SESSION_VARIANT === "assistant") return void 0;
4685
+ function spawnOriginTags(env = process.env, flavor = void 0) {
4686
+ if (env.HAPPY_SESSION_VARIANT === "assistant" && flavor === "claude") return void 0;
4678
4687
  const origin = (env.HAPPY_SPAWNED_BY ?? "").trim();
4679
4688
  if (!origin) return void 0;
4680
4689
  if (origin.length > SPAWN_ORIGIN_TAG_MAX) return void 0;
@@ -4688,7 +4697,7 @@ function createSessionMetadata(opts) {
4688
4697
  const state = {
4689
4698
  controlledByUser: false
4690
4699
  };
4691
- const originTags = spawnOriginTags();
4700
+ const originTags = spawnOriginTags(process.env, opts.flavor);
4692
4701
  const metadata = {
4693
4702
  path: process.cwd(),
4694
4703
  host: os.hostname(),
@@ -4907,7 +4916,7 @@ function daemonReadiness(authenticated, running, hasState) {
4907
4916
  message: "\u26A0\uFE0F Daemon is not running \u2014 run `very-happy daemon start`"
4908
4917
  };
4909
4918
  }
4910
- function collectRuntimeReadiness(run = node_child_process.spawnSync, currentNodeVersion = process.version) {
4919
+ function collectRuntimeReadiness(run = node_child_process.spawnSync, currentNodeVersion = process.version, onPath = persistence.commandOnPath) {
4911
4920
  const tmux = probe("tmux", run);
4912
4921
  return {
4913
4922
  node: {
@@ -4918,7 +4927,8 @@ function collectRuntimeReadiness(run = node_child_process.spawnSync, currentNode
4918
4927
  ...tmux,
4919
4928
  supportsSessionEnv: tmux.available && tmuxSupportsSessionEnv(tmux.version)
4920
4929
  },
4921
- agents: ["claude", "codex", "gemini", "opencode", "openclaw"].map((command) => probe(command, run))
4930
+ agents: ["claude", "codex", "gemini", "opencode", "openclaw", "pi"].map((command) => probe(command, run)),
4931
+ piAdapter: { available: onPath("pi-acp") }
4922
4932
  };
4923
4933
  }
4924
4934
 
@@ -5167,6 +5177,11 @@ ${typeLabels[type] || type}:`));
5167
5177
  } else {
5168
5178
  console.log(chalk.green(`\u2713 External agent command${availableAgents.length === 1 ? "" : "s"}: ${availableAgents.map(toolProbeLabel).join(", ")}`));
5169
5179
  }
5180
+ const piProbe = readiness.agents.find((agent) => agent.command === "pi");
5181
+ if (piProbe?.available && !readiness.piAdapter.available) {
5182
+ 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"));
5183
+ console.log(chalk.gray(" Install it for the daemon user: npm install -g pi-acp@0.0.33"));
5184
+ }
5170
5185
  console.log(chalk.bold("\n\u{1F510} Authentication"));
5171
5186
  let authenticated = false;
5172
5187
  try {
@@ -5217,6 +5232,11 @@ ${typeLabels[type] || type}:`));
5217
5232
  console.log(chalk.green("\n\u2705 Doctor diagnosis complete!\n"));
5218
5233
  }
5219
5234
 
5235
+ const SPAWN_AGENTS = ["claude", "codex", "gemini", "openclaw", "pi"];
5236
+ function isSpawnAgent(value) {
5237
+ return typeof value === "string" && SPAWN_AGENTS.includes(value);
5238
+ }
5239
+
5220
5240
  const DEFAULT_LOG_PRUNE_POLICY = {
5221
5241
  maxAgeMs: 14 * 24 * 60 * 6e4,
5222
5242
  maxTotalBytes: 200 * 1024 * 1024,
@@ -5618,6 +5638,10 @@ function spawnHappyCLI(args, options = {}) {
5618
5638
  });
5619
5639
  }
5620
5640
 
5641
+ function assistantSpawnMode(options) {
5642
+ if (options.variant !== "assistant") return "none";
5643
+ return options.agent === void 0 || options.agent === "claude" ? "claude-singleton" : "env-only";
5644
+ }
5621
5645
  function isAssistantTracked(tracked) {
5622
5646
  return tracked.variant === "assistant" || tracked.happySessionMetadataFromLocalWebhook?.variant === "assistant";
5623
5647
  }
@@ -5842,7 +5866,8 @@ function startDaemonControlServer({
5842
5866
  onSessionStateEvent,
5843
5867
  onClaudeAuthFailed,
5844
5868
  pushClipboard,
5845
- onTerminalHook
5869
+ onTerminalHook,
5870
+ setTerminalTitle
5846
5871
  }) {
5847
5872
  return new Promise((resolve) => {
5848
5873
  const app = fastify({
@@ -5962,7 +5987,7 @@ function startDaemonControlServer({
5962
5987
  // ignores it and supplies its own home daemon-side.
5963
5988
  directory: z.z.string().default(""),
5964
5989
  sessionId: z.z.string().optional(),
5965
- agent: z.z.enum(["claude", "codex", "gemini", "openclaw"]).optional(),
5990
+ agent: z.z.enum(SPAWN_AGENTS).optional(),
5966
5991
  environmentVariables: z.z.record(z.z.string(), z.z.string()).optional(),
5967
5992
  variant: z.z.enum(["assistant"]).optional(),
5968
5993
  // B-051: assistant only — stop the live assistant, purge its
@@ -6067,6 +6092,30 @@ function startDaemonControlServer({
6067
6092
  persistence.logger.debug(`[CONTROL SERVER] Clipboard push request (${text.length} chars)`);
6068
6093
  return pushClipboard(text);
6069
6094
  });
6095
+ typed.post("/terminal-title", {
6096
+ schema: {
6097
+ body: z.z.object({
6098
+ terminalId: z.z.string().regex(/^[a-zA-Z0-9_-]{1,64}$/),
6099
+ title: z.z.string().min(1).max(200),
6100
+ ifAbsent: z.z.boolean().optional()
6101
+ }),
6102
+ response: {
6103
+ 200: z.z.object({ status: z.z.literal("ok") }),
6104
+ 409: z.z.object({ error: z.z.string() }),
6105
+ 503: z.z.object({ error: z.z.string() })
6106
+ }
6107
+ }
6108
+ }, async (request, reply) => {
6109
+ const { terminalId, title, ifAbsent } = request.body;
6110
+ const landed = setTerminalTitle ? setTerminalTitle(terminalId, title, !!ifAbsent) : "starting";
6111
+ if (landed === "starting") {
6112
+ return reply.code(503).send({ error: "daemon is still starting up" });
6113
+ }
6114
+ if (!landed) {
6115
+ return reply.code(409).send({ error: "Failed to set terminal title (tmux unavailable or terminal gone)" });
6116
+ }
6117
+ return { status: "ok" };
6118
+ });
6070
6119
  typed.post("/stop", {
6071
6120
  schema: {
6072
6121
  response: {
@@ -6872,6 +6921,76 @@ function expandEnvironmentVariables(envVars, sourceEnv = process.env) {
6872
6921
  return expanded;
6873
6922
  }
6874
6923
 
6924
+ const SESSION_MODE_DIR = "session-modes";
6925
+ const ACP_PERMISSION_MODES = ["default", "acceptEdits", "plan", "bypassPermissions"];
6926
+ function normalizeAcpPermissionMode(value) {
6927
+ if (typeof value !== "string") return null;
6928
+ if (value === "yolo") return "bypassPermissions";
6929
+ return ACP_PERMISSION_MODES.includes(value) ? value : null;
6930
+ }
6931
+ function sessionModeFilePath(happySessionId, dir = path.join(persistence.configuration.happyHomeDir, SESSION_MODE_DIR)) {
6932
+ return path.join(dir, `${happySessionId}.json`);
6933
+ }
6934
+ function sessionModeFilePayload(permissionMode, now = Date.now()) {
6935
+ return { permissionMode, updatedAt: now };
6936
+ }
6937
+ function writeSessionModeFile(happySessionId, permissionMode, opts = {}) {
6938
+ const path$1 = sessionModeFilePath(happySessionId, opts.dir);
6939
+ node_fs.mkdirSync(path.join(path$1, ".."), { recursive: true, mode: 448 });
6940
+ const payload = sessionModeFilePayload(permissionMode, opts.now);
6941
+ const tmp = `${path$1}.${process.pid}.tmp`;
6942
+ persistence.writePrivateFileSync(tmp, JSON.stringify(payload));
6943
+ node_fs.renameSync(tmp, path$1);
6944
+ return payload;
6945
+ }
6946
+ function removeSessionModeFile(happySessionId, dir) {
6947
+ const path = sessionModeFilePath(happySessionId, dir);
6948
+ if (node_fs.existsSync(path)) node_fs.unlinkSync(path);
6949
+ }
6950
+
6951
+ const IGNORED_FLAGS_WITH_VALUE = /* @__PURE__ */ new Set(["--happy-starting-mode"]);
6952
+ const PI_ADAPTER_INSTALL_HINT = "very-happy pi needs the pi-acp adapter on PATH: npm install -g pi-acp@0.0.33";
6953
+ function piAdapterMissingHint(detail) {
6954
+ return detail && /ENOENT/.test(detail) ? PI_ADAPTER_INSTALL_HINT : null;
6955
+ }
6956
+ function parsePiRunnerArgs(args) {
6957
+ const parsed = { verbose: false, passthrough: [] };
6958
+ for (let i = 0; i < args.length; i++) {
6959
+ const arg = args[i];
6960
+ if (arg === "--") {
6961
+ parsed.passthrough.push(...args.slice(i + 1));
6962
+ break;
6963
+ }
6964
+ if (arg === "--started-by") {
6965
+ const value = args[++i];
6966
+ if (value === "daemon" || value === "terminal") parsed.startedBy = value;
6967
+ continue;
6968
+ }
6969
+ if (arg === "--verbose") {
6970
+ parsed.verbose = true;
6971
+ continue;
6972
+ }
6973
+ if (arg === "--permission-mode") {
6974
+ const mode = normalizeAcpPermissionMode(args[++i]);
6975
+ if (mode) parsed.permissionMode = mode;
6976
+ continue;
6977
+ }
6978
+ if (IGNORED_FLAGS_WITH_VALUE.has(arg)) {
6979
+ i++;
6980
+ continue;
6981
+ }
6982
+ throw new Error(`Unknown option for very-happy pi: ${arg} (use -- to pass args to pi-acp)`);
6983
+ }
6984
+ return parsed;
6985
+ }
6986
+
6987
+ function spawnAgentUnavailableError(agent, availability) {
6988
+ if (agent === "pi" && !availability.pi) {
6989
+ return { type: "error", errorMessage: PI_ADAPTER_INSTALL_HINT };
6990
+ }
6991
+ return null;
6992
+ }
6993
+
6875
6994
  const ResumableMetadataSchema = z.z.object({
6876
6995
  path: z.z.string().min(1),
6877
6996
  flavor: z.z.string().optional(),
@@ -7119,6 +7238,24 @@ function decideHandover(run) {
7119
7238
  return { action: "handover" };
7120
7239
  }
7121
7240
 
7241
+ function decideAutoUpdate(context) {
7242
+ if (!context.enabled) return { action: "skip", reason: "auto-update disabled" };
7243
+ const target = context.recommendedVersion;
7244
+ if (!target) return { action: "skip", reason: "no recommended version published" };
7245
+ const ordering = persistence.compareExactVersions(context.currentVersion, target);
7246
+ if (ordering === null) return { action: "skip", reason: "version numbers not comparable" };
7247
+ if (ordering === 0) return { action: "skip", reason: "already current" };
7248
+ if (ordering > 0) return { action: "skip", reason: `already ahead of the recommended ${target}` };
7249
+ if (context.failedVersion === target) {
7250
+ return { action: "skip", reason: `already failed to install ${target} once` };
7251
+ }
7252
+ if (!context.idle) return { action: "skip", reason: "machine is busy" };
7253
+ return { action: "install", version: target };
7254
+ }
7255
+ function autoUpdateInstallArgs(version) {
7256
+ return ["i", "-g", "--allow-scripts=very-happy-cli,node-pty", `very-happy-cli@${version}`];
7257
+ }
7258
+
7122
7259
  const CLAUDE_KEYCHAIN_SERVICE_PREFIX = "Claude Code";
7123
7260
  function keychainIdentity(env = process.env, deps = {}) {
7124
7261
  const home = deps.home ?? os.homedir();
@@ -7182,7 +7319,7 @@ function parseJsonObject(text) {
7182
7319
  return null;
7183
7320
  }
7184
7321
  }
7185
- function resolveSdkClaudeBinary(platform = process.platform, arch = process.arch, resolver = (s) => node_module.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index-tPXwlc_t.cjs', document.baseURI).href))).resolve(s)) {
7322
+ function resolveSdkClaudeBinary(platform = process.platform, arch = process.arch, resolver = (s) => node_module.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index-DcESl4cI.cjs', document.baseURI).href))).resolve(s)) {
7186
7323
  const platformArch = `${platform}-${arch}`;
7187
7324
  const candidates = [
7188
7325
  `@anthropic-ai/claude-agent-sdk-${platformArch}/claude`,
@@ -8337,6 +8474,7 @@ function sanitizeImportTitle(value) {
8337
8474
  return collapsed.length > 200 ? `${collapsed.slice(0, 199)}\u2026` : collapsed;
8338
8475
  }
8339
8476
  const hostSuffix = process.env.HAPPY_VARIANT === "dev" ? "-dev" : "";
8477
+ const startupCliAvailability = persistence.detectCLIAvailability();
8340
8478
  const initialMachineMetadata = {
8341
8479
  host: os$1.hostname() + hostSuffix,
8342
8480
  platform: os$1.platform(),
@@ -8344,7 +8482,7 @@ const initialMachineMetadata = {
8344
8482
  homeDir: os$1.homedir(),
8345
8483
  happyHomeDir: persistence.configuration.happyHomeDir,
8346
8484
  happyLibDir: persistence.projectPath(),
8347
- cliAvailability: persistence.detectCLIAvailability(),
8485
+ cliAvailability: startupCliAvailability,
8348
8486
  resumeSupport: { ...persistence.detectResumeSupport(), rpcAvailable: true }
8349
8487
  };
8350
8488
  async function preflightNewBundle(bundlePath) {
@@ -8399,6 +8537,40 @@ function withHandoverHold(state, reason) {
8399
8537
  return reason ? { ...state, handoverHold: { reason, at: Date.now() } } : { ...state, handoverHold: null };
8400
8538
  }
8401
8539
  const HANDOVER_PREFLIGHT_TIMEOUT_MS = 3e4;
8540
+ function runNpmInstall(args) {
8541
+ return new Promise((resolve) => {
8542
+ let settled = false;
8543
+ const done = (code) => {
8544
+ if (!settled) {
8545
+ settled = true;
8546
+ resolve(code);
8547
+ }
8548
+ };
8549
+ let child;
8550
+ try {
8551
+ child = node_child_process.spawn("npm", args, { stdio: "ignore", env: process.env });
8552
+ } catch {
8553
+ done(null);
8554
+ return;
8555
+ }
8556
+ const timer = setTimeout(() => {
8557
+ try {
8558
+ child.kill("SIGKILL");
8559
+ } catch {
8560
+ }
8561
+ done(null);
8562
+ }, AUTO_UPDATE_INSTALL_TIMEOUT_MS);
8563
+ child.on("error", () => {
8564
+ clearTimeout(timer);
8565
+ done(null);
8566
+ });
8567
+ child.on("close", (code) => {
8568
+ clearTimeout(timer);
8569
+ done(code);
8570
+ });
8571
+ });
8572
+ }
8573
+ const AUTO_UPDATE_INSTALL_TIMEOUT_MS = 10 * 6e4;
8402
8574
  async function startDaemon() {
8403
8575
  let claudeAuthServiceRef = null;
8404
8576
  let cliUpdateStateRef = null;
@@ -8568,18 +8740,26 @@ async function startDaemon() {
8568
8740
  };
8569
8741
  const assistantSpawnGate = createSpawnGate();
8570
8742
  const spawnSession = (options) => {
8571
- if (options.variant !== "assistant") {
8743
+ if (assistantSpawnMode(options) !== "claude-singleton") {
8572
8744
  return spawnSessionImpl(options);
8573
8745
  }
8574
8746
  return options.forceNew ? assistantSpawnGate.replace(() => spawnSessionImpl(options)) : assistantSpawnGate.join(() => spawnSessionImpl(options));
8575
8747
  };
8748
+ const currentCliAvailability = () => apiMachineRef?.getCLIAvailability() ?? startupCliAvailability;
8576
8749
  const spawnSessionImpl = async (options) => {
8577
8750
  persistence.logger.debugLargeJson("[DAEMON RUN] Spawning session", persistence.summarizeSpawnSessionForLog(options));
8578
8751
  const spawnPermissionMode = sanitizeSpawnPermissionMode(options.permissionMode);
8579
8752
  if (options.permissionMode !== void 0 && spawnPermissionMode === null) {
8580
8753
  persistence.logger.warn("[DAEMON RUN] Ignoring invalid permissionMode in spawn request");
8581
8754
  }
8582
- if (options.variant === "assistant") {
8755
+ const unavailable = spawnAgentUnavailableError(options.agent, currentCliAvailability());
8756
+ if (unavailable) {
8757
+ persistence.logger.warn(`[DAEMON RUN] Refusing spawn of agent '${options.agent}': ${unavailable.errorMessage}`);
8758
+ return unavailable;
8759
+ }
8760
+ const assistantMode = assistantSpawnMode(options);
8761
+ const trackedVariant = assistantMode === "claude-singleton" ? options.variant : void 0;
8762
+ if (assistantMode === "claude-singleton") {
8583
8763
  options = { ...options, directory: assistantHome() };
8584
8764
  if (options.forceNew) {
8585
8765
  for (const tracked of [...pidToTrackedSession.values()]) {
@@ -8782,7 +8962,13 @@ async function startDaemon() {
8782
8962
  persistence.logger.debug(`[DAEMON RUN] Attempting to spawn session in tmux: ${sessionDesc}`);
8783
8963
  const tmux = getTmuxUtilities(tmuxSessionName);
8784
8964
  const cliPath = path$1.join(persistence.projectPath(), "dist", "index.mjs");
8785
- const agent = options.agent === "gemini" ? "gemini" : options.agent === "codex" ? "codex" : options.agent === "openclaw" ? "openclaw" : "claude";
8965
+ const agent = options.agent ?? "claude";
8966
+ if (!isSpawnAgent(agent)) {
8967
+ return {
8968
+ type: "error",
8969
+ errorMessage: `Unsupported agent type: '${options.agent}'. Please update your CLI to the latest version.`
8970
+ };
8971
+ }
8786
8972
  const resumeId = agent === "claude" ? options.resumeClaudeSessionId : agent === "codex" ? options.resumeCodexThreadId : void 0;
8787
8973
  const resumeFragment = resumeId ? ` --resume ${persistence.shellescape(resumeId)}` : "";
8788
8974
  const permissionModeFragment = spawnPermissionMode ? ` --permission-mode ${persistence.shellescape(spawnPermissionMode)}` : "";
@@ -8810,7 +8996,7 @@ async function startDaemon() {
8810
8996
  pid: tmuxResult.pid,
8811
8997
  // Real PID from tmux -P flag
8812
8998
  tmuxSessionId: tmuxResult.sessionId,
8813
- variant: options.variant,
8999
+ variant: trackedVariant,
8814
9000
  spawnedBy: options.spawnedBy,
8815
9001
  directoryCreated,
8816
9002
  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.`
@@ -8857,6 +9043,9 @@ async function startDaemon() {
8857
9043
  case "openclaw":
8858
9044
  agentCommand = "openclaw";
8859
9045
  break;
9046
+ case "pi":
9047
+ agentCommand = "pi";
9048
+ break;
8860
9049
  default:
8861
9050
  return {
8862
9051
  type: "error",
@@ -8888,7 +9077,7 @@ async function startDaemon() {
8888
9077
  },
8889
9078
  directoryCreated,
8890
9079
  message: directoryCreated ? `The path '${directory}' did not exist. We created a new folder and spawned a new session there.` : void 0,
8891
- variant: options.variant,
9080
+ variant: trackedVariant,
8892
9081
  spawnedBy: options.spawnedBy
8893
9082
  });
8894
9083
  }
@@ -9369,6 +9558,10 @@ async function startDaemon() {
9369
9558
  },
9370
9559
  onTerminalHook: (body) => {
9371
9560
  mirrorManagerRef?.handleHookPayload(body);
9561
+ },
9562
+ setTerminalTitle: (terminalId, title, ifAbsent) => {
9563
+ if (!apiMachineRef) return "starting";
9564
+ return apiMachineRef.setTerminalTitle(terminalId, title, ifAbsent);
9372
9565
  }
9373
9566
  });
9374
9567
  const daemonClaudeCredentials = resolveClaudeCredentialReadiness();
@@ -9465,6 +9658,48 @@ async function startDaemon() {
9465
9658
  host: initialMachineMetadata.host
9466
9659
  })).then(() => persistence.logger.debug(`[DAEMON RUN] claimed the machine record for host ${initialMachineMetadata.host}`)).catch((error) => persistence.logger.debug("[DAEMON RUN] could not refresh the recorded host:", error));
9467
9660
  }
9661
+ let autoUpdateFailedVersion = null;
9662
+ let autoUpdateRunning = false;
9663
+ const maybeAutoUpdate = async (cliUpdate) => {
9664
+ if (autoUpdateRunning) return;
9665
+ const settings = await persistence.readSettings();
9666
+ const decision = decideAutoUpdate({
9667
+ enabled: (settings.cliAutoUpdate ?? "idle") !== "off",
9668
+ currentVersion: cliUpdate.currentVersion,
9669
+ recommendedVersion: cliUpdate.recommendedVersion,
9670
+ idle: pidToTrackedSession.size === 0 && !apiMachine.hasLiveTerminals(),
9671
+ failedVersion: autoUpdateFailedVersion
9672
+ });
9673
+ if (decision.action === "skip") {
9674
+ persistence.logger.debug(`[DAEMON RUN] auto-update skipped: ${decision.reason}`);
9675
+ return;
9676
+ }
9677
+ autoUpdateRunning = true;
9678
+ const target = decision.version;
9679
+ const report = (state, detail) => {
9680
+ cliUpdateStateRef = { ...cliUpdate, autoUpdate: { state, version: target, detail, at: Date.now() } };
9681
+ void apiMachine.setCliUpdateState(cliUpdateStateRef);
9682
+ };
9683
+ persistence.logger.warn(`[DAEMON RUN] auto-updating CLI to ${target} (machine idle)`);
9684
+ report("installing");
9685
+ try {
9686
+ const code = await runNpmInstall(autoUpdateInstallArgs(target));
9687
+ if (code === 0) {
9688
+ persistence.logger.warn(`[DAEMON RUN] auto-update installed ${target}; handover follows once it verifies`);
9689
+ report("installed");
9690
+ } else {
9691
+ autoUpdateFailedVersion = target;
9692
+ persistence.logger.warn(`[DAEMON RUN] auto-update of ${target} failed with exit ${code}`);
9693
+ report("failed", `npm exited ${code}`);
9694
+ }
9695
+ } catch (error) {
9696
+ autoUpdateFailedVersion = target;
9697
+ persistence.logger.warn(`[DAEMON RUN] auto-update of ${target} could not run:`, error);
9698
+ report("failed", String(error).slice(0, 200));
9699
+ } finally {
9700
+ autoUpdateRunning = false;
9701
+ }
9702
+ };
9468
9703
  let cliUpdateCheckRunning = false;
9469
9704
  const refreshCliUpdate = async () => {
9470
9705
  if (cliUpdateCheckRunning) return;
@@ -9477,6 +9712,7 @@ async function startDaemon() {
9477
9712
  cliUpdateStateRef = cliUpdate;
9478
9713
  apiMachine.setCliUpdateState(cliUpdate);
9479
9714
  persistence.logger.debug(`[DAEMON RUN] CLI update policy checked: ${cliUpdate.status} (recommended=${cliUpdate.recommendedVersion ?? "none"}, minimum=${cliUpdate.minimumVersion ?? "none"})`);
9715
+ await maybeAutoUpdate(cliUpdate);
9480
9716
  } catch (error) {
9481
9717
  persistence.logger.debug("[DAEMON RUN] CLI update policy check failed:", error);
9482
9718
  } finally {
@@ -9689,6 +9925,194 @@ function formatTranscript(bodies) {
9689
9925
  return lines.join("\n");
9690
9926
  }
9691
9927
 
9928
+ const USER_SOCKET_CONNECT_TIMEOUT_MS = 15e3;
9929
+ async function openUserScopedSocket(token, options = {}) {
9930
+ const socket = socket_ioClient.io(persistence.configuration.serverUrl, {
9931
+ auth: {
9932
+ token,
9933
+ clientType: "user-scoped",
9934
+ happyClient: `cli-session-ops/${persistence.configuration.currentCliVersion}`
9935
+ },
9936
+ path: "/v1/updates",
9937
+ transports: ["websocket"],
9938
+ reconnection: false,
9939
+ autoConnect: false
9940
+ });
9941
+ await new Promise((resolve, reject) => {
9942
+ const timer = setTimeout(() => {
9943
+ socket.close();
9944
+ reject(new Error(`Timed out connecting to ${persistence.configuration.serverUrl}`));
9945
+ }, options.connectTimeoutMs ?? USER_SOCKET_CONNECT_TIMEOUT_MS);
9946
+ socket.once("connect", () => {
9947
+ clearTimeout(timer);
9948
+ resolve();
9949
+ });
9950
+ socket.once("connect_error", (error) => {
9951
+ clearTimeout(timer);
9952
+ socket.close();
9953
+ reject(new Error(`Could not connect to ${persistence.configuration.serverUrl}: ${error.message}`));
9954
+ });
9955
+ socket.connect();
9956
+ });
9957
+ return {
9958
+ async rpcCall(payload, timeoutMs) {
9959
+ const ack = await socket.timeout(timeoutMs).emitWithAck("rpc-call", payload);
9960
+ return ack;
9961
+ },
9962
+ close() {
9963
+ socket.close();
9964
+ }
9965
+ };
9966
+ }
9967
+
9968
+ const PERMISSION_OPS_CLIENT = "session-ops";
9969
+ const PERMISSION_RPC_TIMEOUT_MS = 3e4;
9970
+ const SETTLE_TIMEOUT_MS = 5e3;
9971
+ const SETTLE_POLL_MS = 500;
9972
+ function buildPermissionRpcPayload(requestId, verdict) {
9973
+ if (verdict.kind === "approve") {
9974
+ return {
9975
+ id: requestId,
9976
+ approved: true,
9977
+ decision: verdict.forSession ? "approved_for_session" : "approved"
9978
+ };
9979
+ }
9980
+ return {
9981
+ id: requestId,
9982
+ approved: false,
9983
+ decision: "denied",
9984
+ ...verdict.reason ? { reason: verdict.reason } : {}
9985
+ };
9986
+ }
9987
+ function interpretPermissionAck(ack, decryptResult) {
9988
+ if (!ack.ok) {
9989
+ const message = ack.error ?? "RPC call failed";
9990
+ if (message === "RPC method not available") {
9991
+ return { status: "offline", message: "no running wrapper has registered the permission RPC for this session" };
9992
+ }
9993
+ if (message === "RPC target disconnected") {
9994
+ return { status: "offline", message: "the wrapper disconnected while the permission RPC was in flight" };
9995
+ }
9996
+ if (/timed? ?out/i.test(message)) {
9997
+ return { status: "timeout", message };
9998
+ }
9999
+ return { status: "rejected", message };
10000
+ }
10001
+ let body = null;
10002
+ if (typeof ack.result === "string" && ack.result.length > 0) {
10003
+ try {
10004
+ body = decryptResult(ack.result);
10005
+ } catch {
10006
+ body = null;
10007
+ }
10008
+ }
10009
+ if (body && typeof body === "object" && typeof body.error === "string") {
10010
+ return { status: "handler-error", message: body.error };
10011
+ }
10012
+ return { status: "acknowledged" };
10013
+ }
10014
+ function pendingRequestsOf(agentState, now) {
10015
+ const requests = agentState?.requests;
10016
+ if (!requests || typeof requests !== "object") return [];
10017
+ return Object.entries(requests).map(([id, request]) => ({
10018
+ id,
10019
+ tool: typeof request?.tool === "string" ? request.tool : "unknown",
10020
+ ...request?.kind ? { kind: request.kind } : {},
10021
+ ...typeof request?.createdAt === "number" ? { createdAt: request.createdAt, waitingMs: Math.max(0, now - request.createdAt) } : {}
10022
+ })).sort((a, b) => (a.createdAt ?? Number.MAX_SAFE_INTEGER) - (b.createdAt ?? Number.MAX_SAFE_INTEGER));
10023
+ }
10024
+ const RPC_DEADLINE_GRACE_MS = 2e3;
10025
+ function withDeadline(promise, ms, message) {
10026
+ return new Promise((resolve, reject) => {
10027
+ const timer = setTimeout(() => reject(new Error(`${message} (timed out)`)), ms);
10028
+ promise.then(
10029
+ (value) => {
10030
+ clearTimeout(timer);
10031
+ resolve(value);
10032
+ },
10033
+ (error) => {
10034
+ clearTimeout(timer);
10035
+ reject(error);
10036
+ }
10037
+ );
10038
+ });
10039
+ }
10040
+ async function resolvePermissionRequest(sessionId, requestId, verdict, deps = {}) {
10041
+ const now = deps.now ?? Date.now;
10042
+ const persisted = (deps.readPersisted ?? persistence.readPersistedSessions)()[sessionId];
10043
+ if (!persisted) {
10044
+ throw new Error(
10045
+ `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.`
10046
+ );
10047
+ }
10048
+ const token = await (deps.bearerToken ?? bearerToken$1)();
10049
+ const fetchState = deps.fetchAgentState ?? fetchDecryptedAgentState;
10050
+ const before = pendingRequestsOf(await fetchState(sessionId, persisted, token), now());
10051
+ if (!before.some((request) => request.id === requestId)) {
10052
+ const hint = before.length > 0 ? `Pending request ids: ${before.map((request) => `${request.id} (${request.tool})`).join(", ")}` : "The session has no pending requests.";
10053
+ throw new Error(`Request ${requestId} is not pending on session ${sessionId}. ${hint}`);
10054
+ }
10055
+ const payload = buildPermissionRpcPayload(requestId, verdict);
10056
+ const key = persistence.decodeBase64(persisted.encryptionKey);
10057
+ const params = persistence.encodeBase64(persistence.encrypt(key, persisted.encryptionVariant, payload));
10058
+ const transport = await (deps.openTransport ?? openUserScopedSocket)(token);
10059
+ const rpcTimeoutMs = deps.rpcTimeoutMs ?? PERMISSION_RPC_TIMEOUT_MS;
10060
+ let ack;
10061
+ try {
10062
+ ack = await withDeadline(
10063
+ transport.rpcCall({ method: `${sessionId}:permission`, params }, rpcTimeoutMs),
10064
+ rpcTimeoutMs + RPC_DEADLINE_GRACE_MS,
10065
+ `permission RPC did not settle within ${rpcTimeoutMs}ms`
10066
+ );
10067
+ } catch (error) {
10068
+ ack = { ok: false, error: error instanceof Error ? error.message : "RPC call failed" };
10069
+ } finally {
10070
+ transport.close();
10071
+ }
10072
+ const outcome = interpretPermissionAck(ack, (encrypted) => persistence.decrypt(key, persisted.encryptionVariant, persistence.decodeBase64(encrypted)));
10073
+ if (outcome.status !== "acknowledged") {
10074
+ return { sessionId, requestId, payload, outcome };
10075
+ }
10076
+ const sleep = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
10077
+ const deadline = now() + (deps.settleTimeoutMs ?? SETTLE_TIMEOUT_MS);
10078
+ let settled = false;
10079
+ for (; ; ) {
10080
+ const pending = pendingRequestsOf(await fetchState(sessionId, persisted, token), now());
10081
+ if (!pending.some((request) => request.id === requestId)) {
10082
+ settled = true;
10083
+ break;
10084
+ }
10085
+ if (now() >= deadline) break;
10086
+ await sleep(SETTLE_POLL_MS);
10087
+ }
10088
+ return { sessionId, requestId, payload, outcome, settled };
10089
+ }
10090
+ async function bearerToken$1() {
10091
+ const credentials = await persistence.readCredentialsForConfiguredRelay();
10092
+ if (!credentials) throw new Error("CLI is not authenticated (no ~/.happy/access.key)");
10093
+ return credentials.token;
10094
+ }
10095
+ async function fetchDecryptedAgentState(sessionId, persisted, token) {
10096
+ const response = await axios.get(
10097
+ `${persistence.configuration.serverUrl}/v1/sessions/${encodeURIComponent(sessionId)}`,
10098
+ {
10099
+ headers: {
10100
+ "Authorization": `Bearer ${token}`,
10101
+ "X-Happy-Client": `${PERMISSION_OPS_CLIENT}/${persistence.configuration.currentCliVersion}`
10102
+ },
10103
+ timeout: 15e3,
10104
+ // 404 is a real answer ("not yours / gone"), not a transport fault.
10105
+ validateStatus: (status) => status >= 200 && status < 300 || status === 404
10106
+ }
10107
+ );
10108
+ if (response.status === 404) {
10109
+ 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).`);
10110
+ }
10111
+ const raw = response.data?.session?.agentState;
10112
+ if (typeof raw !== "string" || raw.length === 0) return null;
10113
+ return persistence.decrypt(persistence.decodeBase64(persisted.encryptionKey), persisted.encryptionVariant, persistence.decodeBase64(raw));
10114
+ }
10115
+
9692
10116
  const SESSION_OPS_CLIENT = "session-ops";
9693
10117
  const DEFAULT_RECENT_LIMIT = 15;
9694
10118
  const MAX_READ_LIMIT = 100;
@@ -9700,10 +10124,7 @@ function mergeSessionSummaries(live, persisted, options = {}) {
9700
10124
  const id = typeof child.happySessionId === "string" ? child.happySessionId : void 0;
9701
10125
  if (!id || seen.has(id)) continue;
9702
10126
  seen.add(id);
9703
- summaries.push(toSummary(id, persisted[id], {
9704
- live: true,
9705
- pid: typeof child.pid === "number" ? child.pid : void 0
9706
- }));
10127
+ summaries.push(toSummary(id, persisted[id], livenessOf(child)));
9707
10128
  }
9708
10129
  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);
9709
10130
  for (const [id, entry] of rest) {
@@ -9713,6 +10134,13 @@ function mergeSessionSummaries(live, persisted, options = {}) {
9713
10134
  const wanted = options.tag;
9714
10135
  return summaries.filter((summary) => summary.tags?.includes(wanted) === true);
9715
10136
  }
10137
+ function livenessOf(child) {
10138
+ return { live: true, pid: typeof child.pid === "number" ? child.pid : void 0 };
10139
+ }
10140
+ function sessionLiveness(live, sessionId) {
10141
+ const child = live.find((entry) => entry.happySessionId === sessionId);
10142
+ return child ? livenessOf(child) : { live: false };
10143
+ }
9716
10144
  function toSummary(id, persisted, extra) {
9717
10145
  const meta = persisted?.metadata;
9718
10146
  return {
@@ -9732,6 +10160,79 @@ async function listSessions(options = {}) {
9732
10160
  const live = await listDaemonSessions();
9733
10161
  return mergeSessionSummaries(live, persistence.readPersistedSessions(), options);
9734
10162
  }
10163
+ function summarizeAccountSession(row, persisted, liveIds, now) {
10164
+ const base = {
10165
+ id: row.id,
10166
+ live: liveIds.has(row.id),
10167
+ url: sessionWebUrl(row.id),
10168
+ decryptable: false,
10169
+ active: row.active === true,
10170
+ archived: typeof row.archivedAt === "number",
10171
+ ...typeof row.activeAt === "number" ? { activeAt: row.activeAt } : {},
10172
+ ...typeof row.updatedAt === "number" ? { updatedAt: row.updatedAt } : {},
10173
+ attention: false
10174
+ };
10175
+ if (!persisted) return base;
10176
+ const key = persistence.decodeBase64(persisted.encryptionKey);
10177
+ let metadata = null;
10178
+ let agentState = null;
10179
+ try {
10180
+ metadata = row.metadata ? persistence.decrypt(key, persisted.encryptionVariant, persistence.decodeBase64(row.metadata)) : null;
10181
+ agentState = row.agentState ? persistence.decrypt(key, persisted.encryptionVariant, persistence.decodeBase64(row.agentState)) : null;
10182
+ } catch {
10183
+ return base;
10184
+ }
10185
+ if (!metadata) return base;
10186
+ const meta = metadata;
10187
+ const pending = pendingRequestsOf(agentState, now);
10188
+ return {
10189
+ ...base,
10190
+ decryptable: true,
10191
+ ...meta.summary?.text ? { title: meta.summary.text } : {},
10192
+ ...meta.path ? { cwd: meta.path } : {},
10193
+ ...meta.flavor ? { flavor: meta.flavor } : {},
10194
+ ...meta.tags?.length ? { tags: [...meta.tags] } : {},
10195
+ ...meta.variant ? { variant: meta.variant } : {},
10196
+ ...meta.machineId ? { machineId: meta.machineId } : {},
10197
+ ...persisted.savedAt !== void 0 ? { savedAt: persisted.savedAt } : {},
10198
+ pending,
10199
+ attention: pending.length > 0
10200
+ };
10201
+ }
10202
+ function orderAccountSessions(summaries, options = {}) {
10203
+ const oldestWait = (summary) => Math.max(0, ...(summary.pending ?? []).map((request) => request.waitingMs ?? 0));
10204
+ const rank = (summary) => summary.attention ? 0 : summary.live ? 1 : 2;
10205
+ let rows = summaries.filter((summary) => summary.flavor !== "terminal-mirror").slice().sort((a, b) => {
10206
+ const byRank = rank(a) - rank(b);
10207
+ if (byRank !== 0) return byRank;
10208
+ if (a.attention && b.attention) return oldestWait(b) - oldestWait(a);
10209
+ return (b.updatedAt ?? 0) - (a.updatedAt ?? 0);
10210
+ });
10211
+ if (options.tag) {
10212
+ const wanted = options.tag;
10213
+ rows = rows.filter((summary) => summary.tags?.includes(wanted) === true);
10214
+ }
10215
+ const recentLimit = Math.max(0, options.recentLimit ?? DEFAULT_RECENT_LIMIT);
10216
+ let idleKept = 0;
10217
+ return rows.filter((summary) => rank(summary) < 2 || idleKept++ < recentLimit);
10218
+ }
10219
+ async function listAccountSessions(options = {}) {
10220
+ const token = await bearerToken();
10221
+ const response = await axios.get(`${persistence.configuration.serverUrl}/v1/sessions`, {
10222
+ headers: {
10223
+ "Authorization": `Bearer ${token}`,
10224
+ "X-Happy-Client": `${SESSION_OPS_CLIENT}/${persistence.configuration.currentCliVersion}`
10225
+ },
10226
+ timeout: 15e3
10227
+ });
10228
+ const rows = Array.isArray(response.data?.sessions) ? response.data.sessions : [];
10229
+ const persisted = persistence.readPersistedSessions();
10230
+ const live = await listDaemonSessions();
10231
+ const liveIds = new Set(live.map((child) => child.happySessionId).filter((id) => typeof id === "string"));
10232
+ const now = Date.now();
10233
+ const summaries = rows.filter((row) => typeof row?.id === "string").map((row) => summarizeAccountSession(row, persisted[row.id], liveIds, now)).filter((summary) => options.includeArchived || !summary.archived);
10234
+ return orderAccountSessions(summaries, options);
10235
+ }
9735
10236
  async function bearerToken() {
9736
10237
  const credentials = await persistence.readCredentialsForConfiguredRelay();
9737
10238
  if (!credentials) throw new Error("CLI is not authenticated (no ~/.happy/access.key)");
@@ -9760,6 +10261,7 @@ async function readSessionTranscript(sessionId, limit) {
9760
10261
  const messages = Array.isArray(response.data?.messages) ? response.data.messages : [];
9761
10262
  messages.reverse();
9762
10263
  const key = persistence.decodeBase64(persisted.encryptionKey);
10264
+ const live = await listDaemonSessions();
9763
10265
  const bodies = messages.map((message) => {
9764
10266
  if (message.content?.t !== "encrypted") return null;
9765
10267
  try {
@@ -9769,7 +10271,7 @@ async function readSessionTranscript(sessionId, limit) {
9769
10271
  }
9770
10272
  });
9771
10273
  return {
9772
- summary: toSummary(sessionId, persisted, { live: false }),
10274
+ summary: toSummary(sessionId, persisted, sessionLiveness(live, sessionId)),
9773
10275
  messageCount: messages.length,
9774
10276
  transcript: formatTranscript(bodies)
9775
10277
  };
@@ -9865,13 +10367,16 @@ function normalizeSpawnDirectory(input, homeDir) {
9865
10367
  return { ok: true, directory: path.normalize(expanded) };
9866
10368
  }
9867
10369
 
9868
- const ASSISTANT_TOOL_NAMES = [
10370
+ const ASSISTANT_SESSION_TOOL_NAMES = [
9869
10371
  "sessions_list",
9870
10372
  "session_read",
9871
10373
  "session_send",
9872
10374
  "session_spawn",
9873
10375
  "session_kill",
9874
- "session_archive",
10376
+ "session_archive"
10377
+ ];
10378
+ const ASSISTANT_TOOL_NAMES = [
10379
+ ...ASSISTANT_SESSION_TOOL_NAMES,
9875
10380
  "terminals_list",
9876
10381
  "terminal_read",
9877
10382
  "terminal_send",
@@ -9908,6 +10413,10 @@ function describeSummary(summary) {
9908
10413
  return parts.join(" ");
9909
10414
  }
9910
10415
  function registerAssistantTools(mcp) {
10416
+ registerAssistantSessionTools(mcp);
10417
+ registerAssistantMachineTools(mcp);
10418
+ }
10419
+ function registerAssistantSessionTools(mcp) {
9911
10420
  mcp.registerTool("sessions_list", {
9912
10421
  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.",
9913
10422
  title: "List Sessions",
@@ -10024,6 +10533,8 @@ ${body}`);
10024
10533
  return fail(`Failed to archive session: ${error instanceof Error ? error.message : String(error)}`);
10025
10534
  }
10026
10535
  });
10536
+ }
10537
+ function registerAssistantMachineTools(mcp) {
10027
10538
  mcp.registerTool("terminals_list", {
10028
10539
  description: "List the web terminals (tmux sessions) on this machine with their id, title and working directory.",
10029
10540
  title: "List Terminals",
@@ -10693,7 +11204,7 @@ function registerSideQuestionHandler(rpc, deps) {
10693
11204
  const retainMs = deps.retainMs ?? SIDE_QUESTION_RETAIN_MS;
10694
11205
  const maxRunMs = deps.maxRunMs ?? SIDE_QUESTION_MAX_RUN_MS;
10695
11206
  const run = deps.run ?? (async (input) => {
10696
- const { query } = await Promise.resolve().then(function () { return require('./index-B3UkbonP.cjs'); });
11207
+ const { query } = await Promise.resolve().then(function () { return require('./index-DqmVH6R5.cjs'); });
10697
11208
  return runSideQuestion(query, input);
10698
11209
  });
10699
11210
  const slots = /* @__PURE__ */ new Map();
@@ -10795,7 +11306,7 @@ const MAX_TITLE_CHARS = 60;
10795
11306
  const GENERATION_TIMEOUT_MS = 3e4;
10796
11307
  function resolveClaudeBinary() {
10797
11308
  try {
10798
- const require$1 = node_module.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index-tPXwlc_t.cjs', document.baseURI).href)));
11309
+ const require$1 = node_module.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index-DcESl4cI.cjs', document.baseURI).href)));
10799
11310
  const utilsPath = path.resolve(path.join(persistence.projectPath(), "scripts", "claude_version_utils.cjs"));
10800
11311
  const { getClaudeCliPath } = require$1(utilsPath);
10801
11312
  const path$1 = getClaudeCliPath();
@@ -11233,7 +11744,7 @@ async function runClaude(credentials, options = {}) {
11233
11744
  const forkedFromMessageId = process.env.HAPPY_FORKED_FROM_MESSAGE_ID;
11234
11745
  const importedFromClaudeSessionId = process.env.HAPPY_IMPORTED_FROM_CLAUDE_SESSION_ID;
11235
11746
  const importTitle = process.env.HAPPY_IMPORT_TITLE;
11236
- const originTags = spawnOriginTags();
11747
+ const originTags = spawnOriginTags(process.env, "claude");
11237
11748
  let metadata = {
11238
11749
  path: workingDirectory,
11239
11750
  host: os.hostname(),
@@ -12997,9 +13508,9 @@ function resolveLocalSignupBootstrap(configuredMode, configuredInviteCodes, gene
12997
13508
  };
12998
13509
  }
12999
13510
 
13000
- const __filename$1 = node_url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index-tPXwlc_t.cjs', document.baseURI).href)));
13511
+ const __filename$1 = node_url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index-DcESl4cI.cjs', document.baseURI).href)));
13001
13512
  const __dirname$1 = path.dirname(__filename$1);
13002
- const require_ = node_module.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index-tPXwlc_t.cjs', document.baseURI).href)));
13513
+ const require_ = node_module.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index-DcESl4cI.cjs', document.baseURI).href)));
13003
13514
  const PRISMA_QUERY_ENGINE_FILES = {
13004
13515
  "arm64-darwin": "libquery_engine-darwin-arm64.dylib.node",
13005
13516
  "x64-darwin": "libquery_engine-darwin.dylib.node",
@@ -15623,7 +16134,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
15623
16134
  process.exit(0);
15624
16135
  } else if (subcommand === "install-terminal-hooks") {
15625
16136
  try {
15626
- const { installTerminalHooks, parseTerminalHooksArgs, TERMINAL_HOOKS_HELP } = await Promise.resolve().then(function () { return require('./installTerminalHooks-CWfCF68p.cjs'); });
16137
+ const { installTerminalHooks, parseTerminalHooksArgs, TERMINAL_HOOKS_HELP } = await Promise.resolve().then(function () { return require('./installTerminalHooks-zfsWSotO.cjs'); });
15627
16138
  const command = parseTerminalHooksArgs(args.slice(1));
15628
16139
  if (command.action === "help") {
15629
16140
  console.log(TERMINAL_HOOKS_HELP);
@@ -15640,7 +16151,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
15640
16151
  }
15641
16152
  } else if (subcommand === "spawn") {
15642
16153
  try {
15643
- const { handleSpawnCommand } = await Promise.resolve().then(function () { return require('./spawn-CfYddkS_.cjs'); });
16154
+ const { handleSpawnCommand } = await Promise.resolve().then(function () { return require('./spawn-tSvo3stN.cjs'); });
15644
16155
  await handleSpawnCommand(args.slice(1));
15645
16156
  } catch (error) {
15646
16157
  console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
@@ -15652,7 +16163,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
15652
16163
  return;
15653
16164
  } else if (subcommand === "sessions") {
15654
16165
  try {
15655
- const { handleSessionsCommand } = await Promise.resolve().then(function () { return require('./sessions-CGdcF05T.cjs'); });
16166
+ const { handleSessionsCommand } = await Promise.resolve().then(function () { return require('./sessions-YWt9YEN8.cjs'); });
15656
16167
  await handleSessionsCommand(args.slice(1));
15657
16168
  } catch (error) {
15658
16169
  console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
@@ -15664,7 +16175,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
15664
16175
  return;
15665
16176
  } else if (subcommand === "send") {
15666
16177
  try {
15667
- const { handleSendCommand } = await Promise.resolve().then(function () { return require('./send-CnVtXOj0.cjs'); });
16178
+ const { handleSendCommand } = await Promise.resolve().then(function () { return require('./send-BZLhRZb-.cjs'); });
15668
16179
  await handleSendCommand(args.slice(1));
15669
16180
  } catch (error) {
15670
16181
  console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
@@ -15770,9 +16281,9 @@ Conversation history is preserved on the server, but in-flight tool calls are in
15770
16281
  if (geminiSubcommand === "project" && args[2] === "set" && args[3]) {
15771
16282
  const projectId = args[3];
15772
16283
  try {
15773
- const { saveGoogleCloudProjectToConfig } = await Promise.resolve().then(function () { return require('./config-BNAsx9Wg.cjs'); });
15774
- const { readCredentialsForConfiguredRelay: readCredentialsForConfiguredRelay2 } = await Promise.resolve().then(function () { return require('./types-CpnzfHHS.cjs'); }).then(function (n) { return n.persistence; });
15775
- const { ApiClient: ApiClient2 } = await Promise.resolve().then(function () { return require('./types-CpnzfHHS.cjs'); }).then(function (n) { return n.api; });
16284
+ const { saveGoogleCloudProjectToConfig } = await Promise.resolve().then(function () { return require('./config-O9quMfxg.cjs'); });
16285
+ const { readCredentialsForConfiguredRelay: readCredentialsForConfiguredRelay2 } = await Promise.resolve().then(function () { return require('./types-DD3AK6Nb.cjs'); }).then(function (n) { return n.persistence; });
16286
+ const { ApiClient: ApiClient2 } = await Promise.resolve().then(function () { return require('./types-DD3AK6Nb.cjs'); }).then(function (n) { return n.api; });
15776
16287
  let userEmail = void 0;
15777
16288
  try {
15778
16289
  const credentials = await readCredentialsForConfiguredRelay2();
@@ -15803,7 +16314,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
15803
16314
  }
15804
16315
  if (geminiSubcommand === "project" && args[2] === "get") {
15805
16316
  try {
15806
- const { readGeminiLocalConfig } = await Promise.resolve().then(function () { return require('./config-BNAsx9Wg.cjs'); });
16317
+ const { readGeminiLocalConfig } = await Promise.resolve().then(function () { return require('./config-O9quMfxg.cjs'); });
15807
16318
  const config = readGeminiLocalConfig();
15808
16319
  if (config.googleCloudProject) {
15809
16320
  console.log(`Current Google Cloud Project: ${config.googleCloudProject}`);
@@ -15843,7 +16354,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
15843
16354
  process.exit(0);
15844
16355
  }
15845
16356
  try {
15846
- const { runGemini } = await Promise.resolve().then(function () { return require('./runGemini-UN5IznJp.cjs'); });
16357
+ const { runGemini } = await Promise.resolve().then(function () { return require('./runGemini-DYf3ntib.cjs'); });
15847
16358
  let startedBy = void 0;
15848
16359
  for (let i = 1; i < args.length; i++) {
15849
16360
  if (args[i] === "--started-by") {
@@ -15863,9 +16374,44 @@ Conversation history is preserved on the server, but in-flight tool calls are in
15863
16374
  process.exit(1);
15864
16375
  }
15865
16376
  return;
16377
+ } else if (subcommand === "pi") {
16378
+ let hintPrinted = false;
16379
+ const printHint = (detail) => {
16380
+ const hint = piAdapterMissingHint(detail);
16381
+ if (hint && !hintPrinted) {
16382
+ hintPrinted = true;
16383
+ console.error(chalk.gray(hint));
16384
+ }
16385
+ };
16386
+ try {
16387
+ const { runAcp, resolveAcpAgentConfig } = await Promise.resolve().then(function () { return require('./index-W-O4xR_O.cjs'); });
16388
+ const parsed = parsePiRunnerArgs(args.slice(1));
16389
+ const resolved = resolveAcpAgentConfig(["pi", ...parsed.passthrough]);
16390
+ const { credentials } = await authAndSetupMachineIfNeeded();
16391
+ await ensureDaemonRunning();
16392
+ await runAcp({
16393
+ credentials,
16394
+ startedBy: parsed.startedBy,
16395
+ verbose: parsed.verbose,
16396
+ permissionMode: parsed.permissionMode,
16397
+ agentName: resolved.agentName,
16398
+ command: resolved.command,
16399
+ args: resolved.args,
16400
+ onBackendError: printHint
16401
+ });
16402
+ if (hintPrinted) process.exit(1);
16403
+ } catch (error) {
16404
+ console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
16405
+ printHint(error instanceof Error ? error.message : void 0);
16406
+ if (process.env.DEBUG) {
16407
+ console.error(error);
16408
+ }
16409
+ process.exit(1);
16410
+ }
16411
+ return;
15866
16412
  } else if (subcommand === "acp") {
15867
16413
  try {
15868
- const { runAcp, resolveAcpAgentConfig } = await Promise.resolve().then(function () { return require('./index-ClR0jhjm.cjs'); });
16414
+ const { runAcp, resolveAcpAgentConfig } = await Promise.resolve().then(function () { return require('./index-W-O4xR_O.cjs'); });
15869
16415
  let startedBy = void 0;
15870
16416
  let verbose = false;
15871
16417
  const acpArgs = [];
@@ -15905,7 +16451,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
15905
16451
  return;
15906
16452
  } else if (subcommand === "openclaw") {
15907
16453
  try {
15908
- const { runOpenClaw } = await Promise.resolve().then(function () { return require('./runOpenClaw-9hhfGllZ.cjs'); });
16454
+ const { runOpenClaw } = await Promise.resolve().then(function () { return require('./runOpenClaw-CNKjAw-H.cjs'); });
15909
16455
  let startedBy = void 0;
15910
16456
  let verbose = false;
15911
16457
  let gatewayUrl;
@@ -15956,7 +16502,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
15956
16502
  return;
15957
16503
  } else if (subcommand === "mcp" && args.length === 1) {
15958
16504
  try {
15959
- const { handleMcpCommand } = await Promise.resolve().then(function () { return require('./mcp-CqjiDRnG.cjs'); });
16505
+ const { handleMcpCommand } = await Promise.resolve().then(function () { return require('./mcp-nYqC4FYm.cjs'); });
15960
16506
  await handleMcpCommand();
15961
16507
  } catch (error) {
15962
16508
  process.stderr.write(`[very-happy mcp] Fatal: ${error instanceof Error ? error.message : String(error)}
@@ -16171,10 +16717,12 @@ ${chalk.bold("Usage:")}
16171
16717
  print its web URL (for automation; see spawn --help)
16172
16718
  very-happy send Send a message into an existing session
16173
16719
  (for automation; see send --help)
16174
- very-happy sessions List, read, stop or archive sessions on this
16175
- machine (for automation; see sessions --help)
16720
+ very-happy sessions List, read, stop, archive, approve or deny
16721
+ sessions and their permission requests (for
16722
+ automation; see sessions --help)
16176
16723
  very-happy codex Start Codex mode
16177
16724
  very-happy gemini Start Gemini mode (ACP)
16725
+ very-happy pi Start pi mode (ACP via the pi-acp adapter)
16178
16726
  very-happy acp Start a generic ACP-compatible agent
16179
16727
  very-happy openclaw Connect through a configured OpenClaw gateway
16180
16728
  very-happy install-terminal-hooks
@@ -16200,6 +16748,7 @@ ${chalk.bold("Examples:")}
16200
16748
  very-happy --js-runtime bun Use bun instead of node to spawn Claude Code
16201
16749
  very-happy --claude-env ANTHROPIC_BASE_URL=http://127.0.0.1:3456
16202
16750
  Use a custom API endpoint (e.g., claude-code-router)
16751
+ very-happy pi Start pi (needs pi-acp on PATH)
16203
16752
  very-happy acp gemini Start Gemini via generic ACP runner
16204
16753
  very-happy acp opencode Start OpenCode through its built-in ACP adapter
16205
16754
  very-happy acp -- your-agent --acp
@@ -16326,24 +16875,33 @@ exports.GOOGLE_API_KEY_ENV = GOOGLE_API_KEY_ENV;
16326
16875
  exports.MAX_READ_LIMIT = MAX_READ_LIMIT;
16327
16876
  exports.MessageBuffer = MessageBuffer;
16328
16877
  exports.MessageQueue2 = MessageQueue2;
16878
+ exports.SPAWN_AGENTS = SPAWN_AGENTS;
16879
+ exports.TitleGenerator = TitleGenerator;
16329
16880
  exports.archiveSession = archiveSession;
16330
16881
  exports.checkIfDaemonRunningAndCleanupStaleState = checkIfDaemonRunningAndCleanupStaleState;
16331
16882
  exports.createSessionMetadata = createSessionMetadata;
16332
16883
  exports.hashObject = hashObject;
16333
16884
  exports.initialMachineMetadata = initialMachineMetadata;
16334
16885
  exports.isValidSpawnOrigin = isValidSpawnOrigin;
16886
+ exports.listAccountSessions = listAccountSessions;
16335
16887
  exports.listSessions = listSessions;
16888
+ exports.normalizeAcpPermissionMode = normalizeAcpPermissionMode;
16336
16889
  exports.notifyDaemonSessionStarted = notifyDaemonSessionStarted;
16337
16890
  exports.pushClipboardViaDaemon = pushClipboardViaDaemon;
16338
16891
  exports.query = query;
16339
16892
  exports.readSessionTranscript = readSessionTranscript;
16893
+ exports.registerAssistantSessionTools = registerAssistantSessionTools;
16340
16894
  exports.registerKillSessionHandler = registerKillSessionHandler;
16895
+ exports.removeSessionModeFile = removeSessionModeFile;
16896
+ exports.resolvePermissionRequest = resolvePermissionRequest;
16341
16897
  exports.sanitizeSpawnPermissionMode = sanitizeSpawnPermissionMode;
16342
16898
  exports.sendUserMessage = sendUserMessage;
16343
16899
  exports.sessionWebUrl = sessionWebUrl;
16900
+ exports.setTerminalTitleViaDaemon = setTerminalTitleViaDaemon;
16344
16901
  exports.setupOfflineReconnection = setupOfflineReconnection;
16345
16902
  exports.spawnDaemonSession = spawnDaemonSession;
16346
16903
  exports.startHappyServer = startHappyServer;
16347
16904
  exports.stopSession = stopSession;
16348
16905
  exports.tmuxSupportsSessionEnv = tmuxSupportsSessionEnv;
16349
16906
  exports.waitForSessionKey = waitForSessionKey;
16907
+ exports.writeSessionModeFile = writeSessionModeFile;