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.
- package/dist/{AcpBackend-CfKPvoTU.mjs → AcpBackend-Bk9b823B.mjs} +116 -12
- package/dist/{AcpBackend-CauAQpC1.cjs → AcpBackend-p_KNJkHc.cjs} +116 -12
- package/dist/{AcpSessionManager-CA6xeET_.cjs → AcpSessionManager-B9FTbpGQ.cjs} +15 -2
- package/dist/{AcpSessionManager-BQ6pBRW4.mjs → AcpSessionManager-CDvqVHKf.mjs} +15 -2
- package/dist/{config-BpR_CwZu.mjs → config-CIpgmGLR.mjs} +2 -2
- package/dist/{config-BoteFn-v.cjs → config-CkUn1dI4.cjs} +2 -2
- package/dist/{index-C45xN8s0.mjs → index-B6cpyG_G.mjs} +78 -8
- package/dist/{index-D7azxfcs.mjs → index-BUVQf9PI.mjs} +2 -2
- package/dist/{index-4jiUeiUz.cjs → index-BXOXRblP.cjs} +513 -47
- package/dist/{index-DWmhmji9.cjs → index-BzgtwTxC.cjs} +77 -7
- package/dist/{index-lX1UliOs.cjs → index-CxjX8DGz.cjs} +2 -2
- package/dist/{index-Dq-eFp89.mjs → index-DIXjhnV1.mjs} +502 -45
- package/dist/index.cjs +2 -2
- package/dist/index.mjs +2 -2
- package/dist/{installTerminalHooks-k3TrBHD0.mjs → installTerminalHooks-BAa6LaQQ.mjs} +2 -2
- package/dist/{installTerminalHooks-DnaFkrTF.cjs → installTerminalHooks-lXQa6bQU.cjs} +2 -2
- package/dist/lib.cjs +1 -1
- package/dist/lib.d.cts +43 -1
- package/dist/lib.d.mts +43 -1
- package/dist/lib.mjs +1 -1
- package/dist/{mcp-DSpEpM_2.cjs → mcp-DMD7FxBI.cjs} +62 -16
- package/dist/{mcp-DRePTOap.mjs → mcp-Dwc299mU.mjs} +62 -17
- package/dist/{runGemini-_9-a7duU.mjs → runGemini-BaxGXE9w.mjs} +4 -4
- package/dist/{runGemini-IWHyBq0A.cjs → runGemini-Bj4Egq7j.cjs} +4 -4
- package/dist/{runOpenClaw-0myYIz1U.cjs → runOpenClaw-BJhSvXYy.cjs} +3 -3
- package/dist/{runOpenClaw-Boq6LQ9a.mjs → runOpenClaw-BwJhbIKU.mjs} +3 -3
- package/dist/{send-CwWvFfay.cjs → send-BWGUkWhi.cjs} +2 -2
- package/dist/{send-Cyo6mgMl.mjs → send-DX13svrS.mjs} +2 -2
- package/dist/sessions-CGhpBNqn.cjs +322 -0
- package/dist/sessions-L7O3ypda.mjs +317 -0
- package/dist/{spawn-DQ6FTUK9.cjs → spawn-CbY233Eg.cjs} +6 -7
- package/dist/{spawn-CU0DMauA.mjs → spawn-DPDWZdoW.mjs} +3 -4
- package/dist/{types-Ds0SiQ1D.mjs → types-BtuOM9lw.mjs} +52 -19
- package/dist/{types-C5kEpkcZ.cjs → types-ClvvHves.cjs} +54 -19
- package/package.json +7 -7
- package/dist/sessions-CbzqYiAy.cjs +0 -204
- 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,
|
|
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,
|
|
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
|
|
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(
|
|
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.
|
|
7103
|
-
if (!target) return { action: "skip", reason: "no
|
|
7104
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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,
|
|
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
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
15844
|
-
const { readCredentialsForConfiguredRelay: readCredentialsForConfiguredRelay2 } = await import('./types-
|
|
15845
|
-
const { ApiClient: ApiClient2 } = await import('./types-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
|
16245
|
-
|
|
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,
|
|
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 };
|