very-happy-cli 0.2.96 → 0.2.97
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-B5Eh8G3W.mjs → AcpBackend-CTUM4dvG.mjs} +1 -1
- package/dist/{AcpBackend-BaCt4_zS.cjs → AcpBackend-DrwdVSeJ.cjs} +1 -1
- package/dist/{AcpSessionManager-CvgmPlXB.cjs → AcpSessionManager-BieE187D.cjs} +1 -1
- package/dist/{AcpSessionManager-grr_aJEI.mjs → AcpSessionManager-BoBuHee3.mjs} +1 -1
- package/dist/{config-BjsUeQPA.cjs → config-COq6X7U7.cjs} +3 -3
- package/dist/{config-B69TZlJj.mjs → config-DCH33LOM.mjs} +3 -3
- package/dist/{index-uc-dtu-3.cjs → index-BD2fHJ8o.cjs} +526 -20
- package/dist/{index-BMmbxCTq.mjs → index-Bcgquogx.mjs} +527 -21
- package/dist/{index-C-uJXSm3.mjs → index-DkhMvofH.mjs} +6 -6
- package/dist/{index-DID1FV_n.cjs → index-Dn0iOBFd.cjs} +5 -5
- package/dist/index.cjs +3 -3
- package/dist/index.mjs +3 -3
- package/dist/{installTerminalHooks-CBHjEQ-a.mjs → installTerminalHooks-DqNtW6a2.mjs} +3 -3
- package/dist/{installTerminalHooks-CepXxAM_.cjs → installTerminalHooks-H3kpFEZS.cjs} +3 -3
- package/dist/lib.cjs +1 -1
- package/dist/lib.d.cts +67 -0
- package/dist/lib.d.mts +67 -0
- package/dist/lib.mjs +1 -1
- package/dist/{mcp-BdSHqi2d.mjs → mcp-BKAuzaLX.mjs} +3 -3
- package/dist/{mcp-CAtbtTXF.cjs → mcp-BwNc5eRs.cjs} +3 -3
- package/dist/{runGemini-cMZ3_CRf.mjs → runGemini-Brp0UmLx.mjs} +5 -5
- package/dist/{runGemini-qStPY5fH.cjs → runGemini-w35qiFBB.cjs} +5 -5
- package/dist/{runOpenClaw-BJ36wEbk.mjs → runOpenClaw-BvPRFx-L.mjs} +4 -4
- package/dist/{runOpenClaw-B8HR6GnX.cjs → runOpenClaw-CkHtTCpI.cjs} +4 -4
- package/dist/{send-TX13v8Xe.mjs → send-c-wEmh5U.mjs} +3 -3
- package/dist/{send-6eY55o3C.cjs → send-tyKG13xt.cjs} +3 -3
- package/dist/{spawn-vOZA0kOi.cjs → spawn-xdCsG2RT.cjs} +3 -3
- package/dist/{spawn-CfCE6JAv.mjs → spawn-yS6yY8Xi.mjs} +3 -3
- package/dist/{types-okYCRlgl.cjs → types-DoGaKVfK.cjs} +77 -10
- package/dist/{types-CLRl0ET3.mjs → types-xZOYW0_G.mjs} +76 -10
- package/package.json +7 -7
- package/scripts/shims/keychain-off/security +51 -0
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import{createRequire as _pkgrollCR}from"node:module";const require=_pkgrollCR(import.meta.url);import chalk from 'chalk';
|
|
2
|
-
import os, { homedir } from 'node:os';
|
|
2
|
+
import os, { homedir, userInfo } from 'node:os';
|
|
3
3
|
import { randomUUID, randomBytes, createHash as createHash$1, timingSafeEqual } from 'node:crypto';
|
|
4
|
-
import { n as readDaemonState, l as logger, c as configuration, o as clearDaemonState, q as getProjectPath, p as projectPath, s as initializeSandbox, t as wrapCommand, d as contentLogMetadata, I as InvalidateSync, R as RawJSONLinesSchema, g as delay, u as AsyncLock, v as ensurePrivateDirectory, M as MAX_CHAT_ATTACHMENT_SOURCE_BYTES, x as hardenPrivateFile, f as packageJson, i as readSettings, y as readCredentials, z as credentialRelayProblem, B as deriveLocalCliUpdateSummary, j as encodeBase64, C as updateSettings, D as encodeBase64Url, E as decodeBase64, F as writeCredentialsLegacy, G as writeCredentialsDataKey, H as readCredentialsForConfiguredRelay, J as encrypt, r as readPersistedSessions, K as isClaudeSessionId, L as getLocalHappyAgentCredentialPath, N as readLocalHappyAgentCredentials, O as decryptWithDataKey, P as decryptLegacy, Q as extractCompleteLines, S as decideBackfill, T as parseTerminalHookPayload, U as tmuxArgs, V as scrubTmuxClientEnv, W as decideMirrorBinding, X as MIRROR_BACKFILL_LINES_DEFAULT, Y as persistSession, Z as mirrorLineKey, _ as mirrorLocalId, w as writePrivateFileSync, $ as detectResumeSupport, a0 as detectCLIAvailability, a1 as acquireDaemonLock, a2 as summarizeSpawnSessionForLog, a3 as deletePersistedSessions, a4 as decrypt, a5 as writeDaemonState, A as ApiClient, a6 as
|
|
4
|
+
import { n as readDaemonState, l as logger, c as configuration, o as clearDaemonState, q as getProjectPath, p as projectPath, s as initializeSandbox, t as wrapCommand, d as contentLogMetadata, I as InvalidateSync, R as RawJSONLinesSchema, g as delay, u as AsyncLock, v as ensurePrivateDirectory, M as MAX_CHAT_ATTACHMENT_SOURCE_BYTES, x as hardenPrivateFile, f as packageJson, i as readSettings, y as readCredentials, z as credentialRelayProblem, B as deriveLocalCliUpdateSummary, j as encodeBase64, C as updateSettings, D as encodeBase64Url, E as decodeBase64, F as writeCredentialsLegacy, G as writeCredentialsDataKey, H as readCredentialsForConfiguredRelay, J as encrypt, r as readPersistedSessions, K as isClaudeSessionId, L as getLocalHappyAgentCredentialPath, N as readLocalHappyAgentCredentials, O as decryptWithDataKey, P as decryptLegacy, Q as extractCompleteLines, S as decideBackfill, T as parseTerminalHookPayload, U as tmuxArgs, V as scrubTmuxClientEnv, W as decideMirrorBinding, X as MIRROR_BACKFILL_LINES_DEFAULT, Y as persistSession, Z as mirrorLineKey, _ as mirrorLocalId, w as writePrivateFileSync, $ as detectResumeSupport, a0 as detectCLIAvailability, a1 as acquireDaemonLock, a2 as summarizeSpawnSessionForLog, a3 as deletePersistedSessions, a4 as decrypt, a5 as writeDaemonState, A as ApiClient, a6 as writeSettings, a7 as fetchCliUpdateState, a8 as resolveCliUpdateCheckInterval, a9 as DEFAULT_CLI_UPDATE_CHECK_INTERVAL_MS, aa as releaseDaemonLock, ab as isValidSessionId, ac as listVhTerminals, ad as isValidTerminalId, ae as readVhTerminal, af as sendToVhTerminal, h as connectionState, ag as startOfflineReconnection, ah as clearCredentials, ai as clearMachineId, k as ensurePrivateDirectorySync, aj as SandboxConfigSchema, b as createEnvelope, ak as CodexAppServerClient, al as getLatestDaemonLog } from './types-xZOYW0_G.mjs';
|
|
5
5
|
import spawn$1, { spawn } from 'cross-spawn';
|
|
6
|
-
import path, { join, resolve, basename, isAbsolute, normalize, sep, dirname } from 'node:path';
|
|
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,
|
|
8
|
+
import { existsSync, readFileSync, readdirSync, statSync, mkdirSync, unlinkSync, writeFileSync, accessSync, constants, renameSync, rmSync, 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';
|
|
@@ -19,7 +19,7 @@ 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';
|
|
21
21
|
import { createId } from '@paralleldrive/cuid2';
|
|
22
|
-
import { execSync, spawnSync, spawn as spawn$3, execFileSync } from 'node:child_process';
|
|
22
|
+
import { execSync, spawnSync, spawn as spawn$3, execFile, execFileSync } from 'node:child_process';
|
|
23
23
|
import { existsSync as existsSync$1, statSync as statSync$1, writeFileSync as writeFileSync$1, chmodSync, unlinkSync as unlinkSync$1 } from 'fs';
|
|
24
24
|
import { join as join$1 } from 'path';
|
|
25
25
|
import '@anthropic-ai/sandbox-runtime';
|
|
@@ -31,11 +31,11 @@ import open from 'open';
|
|
|
31
31
|
import fastify from 'fastify';
|
|
32
32
|
import { z } from 'zod';
|
|
33
33
|
import { validatorCompiler, serializerCompiler } from 'fastify-type-provider-zod';
|
|
34
|
+
import { createRequire } from 'node:module';
|
|
34
35
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
35
36
|
import { createServer } from 'node:http';
|
|
36
37
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
37
38
|
import { C as CLIPBOARD_TOOL_NAME, a as CLIPBOARD_TOOL_TITLE, b as CLIPBOARD_TOOL_DESCRIPTION, c as CLIPBOARD_MAX_BYTES } from './limits-CwFb5S2b.mjs';
|
|
38
|
-
import { createRequire } from 'node:module';
|
|
39
39
|
import { createServer as createServer$1 } from 'http';
|
|
40
40
|
import { promisify } from 'util';
|
|
41
41
|
import inquirer from 'inquirer';
|
|
@@ -175,6 +175,9 @@ async function spawnDaemonSession(directory, sessionId, opts) {
|
|
|
175
175
|
async function notifyDaemonSessionEvent(sessionId, event, spawnedBy) {
|
|
176
176
|
return daemonPost("/session-event", { sessionId, event, spawnedBy });
|
|
177
177
|
}
|
|
178
|
+
async function notifyDaemonClaudeAuthFailed(sessionId) {
|
|
179
|
+
return daemonPost("/session-event", { sessionId, event: "auth_failed" });
|
|
180
|
+
}
|
|
178
181
|
async function stopDaemonHttp() {
|
|
179
182
|
await daemonPost("/stop");
|
|
180
183
|
}
|
|
@@ -1744,6 +1747,20 @@ function parseSpecialCommand(message) {
|
|
|
1744
1747
|
};
|
|
1745
1748
|
}
|
|
1746
1749
|
|
|
1750
|
+
const AUTH_FAILURE_RESULT_TEXT = /Failed to authenticate|OAuth session expired/i;
|
|
1751
|
+
const QUERY_RECYCLE_NOTICE = {
|
|
1752
|
+
authentication_failed: "\u26A0\uFE0F Claude Code could not refresh its OAuth session. This agent process was ended so your next message starts a fresh one that re-reads the credentials on this machine; if it fails again, run `claude` on the machine to log in again."
|
|
1753
|
+
};
|
|
1754
|
+
function queryRecycleReason(result, lastAssistantError) {
|
|
1755
|
+
if (lastAssistantError === "authentication_failed") {
|
|
1756
|
+
return "authentication_failed";
|
|
1757
|
+
}
|
|
1758
|
+
if (result.is_error && typeof result.result === "string" && AUTH_FAILURE_RESULT_TEXT.test(result.result)) {
|
|
1759
|
+
return "authentication_failed";
|
|
1760
|
+
}
|
|
1761
|
+
return null;
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1747
1764
|
class PushableAsyncIterable {
|
|
1748
1765
|
queue = [];
|
|
1749
1766
|
waiters = [];
|
|
@@ -2004,11 +2021,15 @@ async function claudeRemote(opts) {
|
|
|
2004
2021
|
}
|
|
2005
2022
|
});
|
|
2006
2023
|
}
|
|
2024
|
+
let lastAssistantError;
|
|
2007
2025
|
updateThinking(true);
|
|
2008
2026
|
try {
|
|
2009
2027
|
logger.debug(`[claudeRemote] Starting to iterate over response`);
|
|
2010
2028
|
for await (const message of response) {
|
|
2011
2029
|
logger.debug(`[claudeRemote] Message ${message.type}`, contentLogMetadata(message));
|
|
2030
|
+
if (message.type === "assistant" && message.error) {
|
|
2031
|
+
lastAssistantError = message.error;
|
|
2032
|
+
}
|
|
2012
2033
|
const outboundMessage = isCompactCommand && message.type === "assistant" ? { ...message, isCompactSummary: true } : message;
|
|
2013
2034
|
opts.onMessage(outboundMessage);
|
|
2014
2035
|
if (message.type === "system" && message.subtype === "init") {
|
|
@@ -2057,6 +2078,15 @@ async function claudeRemote(opts) {
|
|
|
2057
2078
|
isCompactCommand = false;
|
|
2058
2079
|
}
|
|
2059
2080
|
opts.onReady(message);
|
|
2081
|
+
const recycleReason = queryRecycleReason(message, lastAssistantError);
|
|
2082
|
+
lastAssistantError = void 0;
|
|
2083
|
+
if (recycleReason) {
|
|
2084
|
+
logger.warn(`[claudeRemote] Ending SDK query after ${recycleReason}; the next message starts a fresh Claude Code process`);
|
|
2085
|
+
opts.onCompletionEvent?.(QUERY_RECYCLE_NOTICE[recycleReason]);
|
|
2086
|
+
opts.onAuthFailure?.(recycleReason);
|
|
2087
|
+
messages.end();
|
|
2088
|
+
continue;
|
|
2089
|
+
}
|
|
2060
2090
|
opts.nextMessage().then((next) => {
|
|
2061
2091
|
if (!next) {
|
|
2062
2092
|
messages.end();
|
|
@@ -3260,8 +3290,9 @@ async function cleanupStdinAfterInk(opts) {
|
|
|
3260
3290
|
}
|
|
3261
3291
|
|
|
3262
3292
|
function applyClaudeResultLifecycle(result, callbacks) {
|
|
3263
|
-
if (result && result.subtype !== "success") {
|
|
3264
|
-
const
|
|
3293
|
+
if (result && (result.subtype !== "success" || result.is_error === true)) {
|
|
3294
|
+
const frame = result;
|
|
3295
|
+
const error = frame.errors?.filter(Boolean).join("\n").trim() || (typeof frame.result === "string" ? frame.result.trim() : "") || frame.subtype;
|
|
3265
3296
|
callbacks.closeFailed(error);
|
|
3266
3297
|
callbacks.onFailed(error);
|
|
3267
3298
|
return;
|
|
@@ -3733,6 +3764,10 @@ async function claudeRemoteLauncher(session, onPermissionModeChange, onEffective
|
|
|
3733
3764
|
logger.debug("[remote]: Completion event received:", contentLogMetadata(message));
|
|
3734
3765
|
session.client.sendSessionEvent({ type: "message", message });
|
|
3735
3766
|
},
|
|
3767
|
+
onAuthFailure: (reason) => {
|
|
3768
|
+
session.client.sendSessionEvent({ type: "message", message: `Claude Code auth: ${reason}`, kind: "claude-auth-failed" });
|
|
3769
|
+
void notifyDaemonClaudeAuthFailed(session.client.sessionId).catch((error) => logger.debug("[remote]: auth_failed report to daemon failed:", error));
|
|
3770
|
+
},
|
|
3736
3771
|
onSessionReset: () => {
|
|
3737
3772
|
logger.debug("[remote]: Session reset");
|
|
3738
3773
|
session.clearSessionId();
|
|
@@ -5406,6 +5441,7 @@ function startDaemonControlServer({
|
|
|
5406
5441
|
requestShutdown,
|
|
5407
5442
|
onHappySessionWebhook,
|
|
5408
5443
|
onSessionStateEvent,
|
|
5444
|
+
onClaudeAuthFailed,
|
|
5409
5445
|
pushClipboard,
|
|
5410
5446
|
onTerminalHook
|
|
5411
5447
|
}) {
|
|
@@ -5461,7 +5497,7 @@ function startDaemonControlServer({
|
|
|
5461
5497
|
schema: {
|
|
5462
5498
|
body: z.object({
|
|
5463
5499
|
sessionId: z.string(),
|
|
5464
|
-
event: z.enum(["completed", "needs_input"]),
|
|
5500
|
+
event: z.enum(["completed", "needs_input", "auth_failed"]),
|
|
5465
5501
|
spawnedBy: z.string().optional()
|
|
5466
5502
|
}),
|
|
5467
5503
|
response: {
|
|
@@ -5473,6 +5509,10 @@ function startDaemonControlServer({
|
|
|
5473
5509
|
}, async (request) => {
|
|
5474
5510
|
const { sessionId, event, spawnedBy } = request.body;
|
|
5475
5511
|
logger.debug(`[CONTROL SERVER] Session event: ${sessionId} ${event} (spawnedBy=${spawnedBy ?? "unset"})`);
|
|
5512
|
+
if (event === "auth_failed") {
|
|
5513
|
+
onClaudeAuthFailed?.(sessionId);
|
|
5514
|
+
return { status: "ok" };
|
|
5515
|
+
}
|
|
5476
5516
|
onSessionStateEvent?.(sessionId, event, spawnedBy);
|
|
5477
5517
|
return { status: "ok" };
|
|
5478
5518
|
});
|
|
@@ -6646,6 +6686,444 @@ async function handleResumeCommand(args) {
|
|
|
6646
6686
|
}
|
|
6647
6687
|
}
|
|
6648
6688
|
|
|
6689
|
+
const CLAUDE_KEYCHAIN_SERVICE_PREFIX = "Claude Code";
|
|
6690
|
+
function keychainIdentity(env = process.env, deps = {}) {
|
|
6691
|
+
const home = deps.home ?? homedir();
|
|
6692
|
+
const secureDir = env.CLAUDE_SECURESTORAGE_CONFIG_DIR?.trim();
|
|
6693
|
+
const configDirEnv = env.CLAUDE_CONFIG_DIR?.trim();
|
|
6694
|
+
const configDir = secureDir || configDirEnv || join(home, ".claude");
|
|
6695
|
+
const hasCustomDir = Boolean(secureDir || configDirEnv);
|
|
6696
|
+
const dirSuffix = hasCustomDir ? "-" + createHash$1("sha256").update(configDir.normalize("NFC")).digest("hex").slice(0, 8) : "";
|
|
6697
|
+
const oauthSuffix = env.CLAUDE_CODE_OAUTH_CLIENT_ID?.trim() ? "-custom-oauth" : "";
|
|
6698
|
+
const rawAccount = env.USER || safeUsername(deps.username);
|
|
6699
|
+
const account = /^[a-zA-Z0-9._-]+$/.test(rawAccount) ? rawAccount : "claude-code-user";
|
|
6700
|
+
return {
|
|
6701
|
+
service: `${CLAUDE_KEYCHAIN_SERVICE_PREFIX}${oauthSuffix}-credentials${dirSuffix}`,
|
|
6702
|
+
account,
|
|
6703
|
+
configDir,
|
|
6704
|
+
credentialsPath: join(configDir, ".credentials.json")
|
|
6705
|
+
};
|
|
6706
|
+
}
|
|
6707
|
+
function safeUsername(provider) {
|
|
6708
|
+
try {
|
|
6709
|
+
return provider ? provider() : userInfo().username;
|
|
6710
|
+
} catch {
|
|
6711
|
+
return "";
|
|
6712
|
+
}
|
|
6713
|
+
}
|
|
6714
|
+
|
|
6715
|
+
const CLAUDE_AUTH_PROBE_VERSION = 1;
|
|
6716
|
+
const LOCAL_CREDENTIAL_SOURCE = "Claude local credentials";
|
|
6717
|
+
function classifyAuthStatus(run, credentialSource) {
|
|
6718
|
+
const localSource = credentialSource === void 0 || credentialSource === LOCAL_CREDENTIAL_SOURCE;
|
|
6719
|
+
if (run.spawnError) {
|
|
6720
|
+
return { status: "error", diagnosis: "probe-crash", detail: `could not run claude auth status: ${run.spawnError}` };
|
|
6721
|
+
}
|
|
6722
|
+
if (run.timedOut) {
|
|
6723
|
+
return { status: "error", diagnosis: "probe-timeout", detail: "claude auth status did not answer within the timeout" };
|
|
6724
|
+
}
|
|
6725
|
+
const parsed = parseJsonObject(run.stdout);
|
|
6726
|
+
if (!parsed) {
|
|
6727
|
+
if (!localSource) return { status: "unknown", authMethod: credentialSource, detail: `auth status unparseable under ${credentialSource}` };
|
|
6728
|
+
return { status: "error", diagnosis: "probe-crash", detail: `claude auth status returned no JSON (exit ${run.exitCode ?? "null"})` };
|
|
6729
|
+
}
|
|
6730
|
+
const authMethod = typeof parsed.authMethod === "string" ? parsed.authMethod : void 0;
|
|
6731
|
+
const subscriptionType = typeof parsed.subscriptionType === "string" ? parsed.subscriptionType : void 0;
|
|
6732
|
+
if (parsed.loggedIn === true) {
|
|
6733
|
+
return { status: "ok", authMethod: localSource ? authMethod : credentialSource, subscriptionType };
|
|
6734
|
+
}
|
|
6735
|
+
if (!localSource) {
|
|
6736
|
+
return { status: "unknown", authMethod: credentialSource, detail: `Claude Code reports loggedIn=false under ${credentialSource}; not a local-credential failure` };
|
|
6737
|
+
}
|
|
6738
|
+
return { status: "not-logged-in", authMethod, subscriptionType, detail: "Claude Code in the daemon context reports it is not logged in" };
|
|
6739
|
+
}
|
|
6740
|
+
function parseJsonObject(text) {
|
|
6741
|
+
const trimmed = text.trim();
|
|
6742
|
+
if (!trimmed) return null;
|
|
6743
|
+
const start = trimmed.indexOf("{");
|
|
6744
|
+
if (start < 0) return null;
|
|
6745
|
+
try {
|
|
6746
|
+
const value = JSON.parse(trimmed.slice(start));
|
|
6747
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
6748
|
+
} catch {
|
|
6749
|
+
return null;
|
|
6750
|
+
}
|
|
6751
|
+
}
|
|
6752
|
+
function resolveSdkClaudeBinary(platform = process.platform, arch = process.arch, resolver = (s) => createRequire(import.meta.url).resolve(s)) {
|
|
6753
|
+
const platformArch = `${platform}-${arch}`;
|
|
6754
|
+
const candidates = [
|
|
6755
|
+
`@anthropic-ai/claude-agent-sdk-${platformArch}/claude`,
|
|
6756
|
+
`@anthropic-ai/claude-agent-sdk-${platformArch}/claude.exe`
|
|
6757
|
+
];
|
|
6758
|
+
for (const candidate of candidates) {
|
|
6759
|
+
try {
|
|
6760
|
+
return resolver(candidate);
|
|
6761
|
+
} catch {
|
|
6762
|
+
}
|
|
6763
|
+
}
|
|
6764
|
+
return null;
|
|
6765
|
+
}
|
|
6766
|
+
function interpretSecurityRead(result) {
|
|
6767
|
+
if (result.error === "ENOENT") return { kind: "unsupported" };
|
|
6768
|
+
if (result.error === "timeout") return { kind: "error", detail: "security timed out" };
|
|
6769
|
+
if (result.exitCode === 44) return { kind: "absent" };
|
|
6770
|
+
if (result.exitCode === 36) return { kind: "unreadable" };
|
|
6771
|
+
if (result.exitCode !== 0) return { kind: "error", detail: `security exited ${result.exitCode ?? "null"}` };
|
|
6772
|
+
const raw = result.stdout.trim();
|
|
6773
|
+
if (!raw) return { kind: "absent" };
|
|
6774
|
+
const parsed = parseJsonObject(raw);
|
|
6775
|
+
const oauth = parsed && typeof parsed.claudeAiOauth === "object" && parsed.claudeAiOauth ? parsed.claudeAiOauth : null;
|
|
6776
|
+
if (!oauth) return { kind: "present", accessToken: "", refreshToken: "", raw };
|
|
6777
|
+
return {
|
|
6778
|
+
kind: "present",
|
|
6779
|
+
accessToken: typeof oauth.accessToken === "string" ? oauth.accessToken : "",
|
|
6780
|
+
refreshToken: typeof oauth.refreshToken === "string" ? oauth.refreshToken : "",
|
|
6781
|
+
raw
|
|
6782
|
+
};
|
|
6783
|
+
}
|
|
6784
|
+
function interpretCredentialsFile(text) {
|
|
6785
|
+
if (text === null) return { exists: false, hasTokens: false, refreshToken: "" };
|
|
6786
|
+
const parsed = parseJsonObject(text);
|
|
6787
|
+
const oauth = parsed && typeof parsed.claudeAiOauth === "object" && parsed.claudeAiOauth ? parsed.claudeAiOauth : null;
|
|
6788
|
+
const access = oauth && typeof oauth.accessToken === "string" ? oauth.accessToken : "";
|
|
6789
|
+
const refresh = oauth && typeof oauth.refreshToken === "string" ? oauth.refreshToken : "";
|
|
6790
|
+
return { exists: true, hasTokens: access.length > 0 && refresh.length > 0, refreshToken: refresh };
|
|
6791
|
+
}
|
|
6792
|
+
function tokenTail(token) {
|
|
6793
|
+
return token ? createHash$1("sha256").update(token).digest("hex").slice(-6) : "";
|
|
6794
|
+
}
|
|
6795
|
+
function diagnoseStores(input) {
|
|
6796
|
+
const { status, keychain, file } = input;
|
|
6797
|
+
if (keychain.kind === "present") {
|
|
6798
|
+
const empty = keychain.accessToken.length === 0 && keychain.refreshToken.length === 0;
|
|
6799
|
+
if (empty && file.hasTokens && status === "not-logged-in") {
|
|
6800
|
+
return {
|
|
6801
|
+
diagnosis: "keychain-empty-item",
|
|
6802
|
+
repairable: "delete-empty-keychain-item",
|
|
6803
|
+
keychainRaw: keychain.raw,
|
|
6804
|
+
detail: "The login keychain holds a Claude Code credentials item with empty tokens; Claude Code prefers it over the valid ~/.claude/.credentials.json and reports not logged in."
|
|
6805
|
+
};
|
|
6806
|
+
}
|
|
6807
|
+
if (!empty && file.hasTokens && tokenTail(keychain.refreshToken) !== tokenTail(file.refreshToken)) {
|
|
6808
|
+
return {
|
|
6809
|
+
diagnosis: "store-divergence",
|
|
6810
|
+
detail: "Keychain and ~/.claude/.credentials.json hold different refresh tokens; one of them will stop working at its next refresh. Treat the daemon-context login as canonical, or pin credentialStore=file."
|
|
6811
|
+
};
|
|
6812
|
+
}
|
|
6813
|
+
return {};
|
|
6814
|
+
}
|
|
6815
|
+
if ((keychain.kind === "absent" || keychain.kind === "unreadable" || keychain.kind === "unsupported") && !file.hasTokens) {
|
|
6816
|
+
return { diagnosis: "no-credentials", detail: "No Claude Code credentials found for this daemon context; log in with `claude` on this machine." };
|
|
6817
|
+
}
|
|
6818
|
+
if (keychain.kind === "unreadable" && file.hasTokens && status === "not-logged-in") {
|
|
6819
|
+
return { detail: "The daemon context cannot read the keychain and the file credentials were not accepted; compare `claude auth status` on the machine." };
|
|
6820
|
+
}
|
|
6821
|
+
if (keychain.kind === "error") {
|
|
6822
|
+
return { detail: `keychain check failed: ${keychain.detail}` };
|
|
6823
|
+
}
|
|
6824
|
+
return {};
|
|
6825
|
+
}
|
|
6826
|
+
function securityReadArgs(identity) {
|
|
6827
|
+
return ["find-generic-password", "-s", identity.service, "-a", identity.account, "-w"];
|
|
6828
|
+
}
|
|
6829
|
+
function securityDeleteArgs(identity) {
|
|
6830
|
+
return ["delete-generic-password", "-s", identity.service, "-a", identity.account];
|
|
6831
|
+
}
|
|
6832
|
+
const HAPPY_DAEMON_LAUNCHD_LABEL = "com.mereith.happy-daemon";
|
|
6833
|
+
function classifyLineage(input) {
|
|
6834
|
+
if (input.platform !== "darwin") return "other";
|
|
6835
|
+
const label = input.label ?? HAPPY_DAEMON_LAUNCHD_LABEL;
|
|
6836
|
+
if (input.env.XPC_SERVICE_NAME !== label) return "other";
|
|
6837
|
+
if (input.launchdJobPid !== null && input.ancestorPids.includes(input.launchdJobPid)) return "launchd";
|
|
6838
|
+
return "inherited-env";
|
|
6839
|
+
}
|
|
6840
|
+
function parseLaunchctlPid(printOutput) {
|
|
6841
|
+
const match = /^\s*pid\s*=\s*(\d+)/m.exec(printOutput);
|
|
6842
|
+
return match ? Number(match[1]) : null;
|
|
6843
|
+
}
|
|
6844
|
+
function keychainOffShimDir(happyLibDir) {
|
|
6845
|
+
return join(happyLibDir, "scripts", "shims", "keychain-off");
|
|
6846
|
+
}
|
|
6847
|
+
function withKeychainOffPath(path, shimDir) {
|
|
6848
|
+
const parts = (path ?? "").split(delimiter).filter((p) => p && p !== shimDir);
|
|
6849
|
+
return [shimDir, ...parts].join(delimiter);
|
|
6850
|
+
}
|
|
6851
|
+
function buildClaudeAuthState(input) {
|
|
6852
|
+
const { classification, diagnosis } = input;
|
|
6853
|
+
const detail = (diagnosis?.detail ?? classification.detail)?.slice(0, 200);
|
|
6854
|
+
const state = {
|
|
6855
|
+
probeVersion: CLAUDE_AUTH_PROBE_VERSION,
|
|
6856
|
+
daemonPid: input.daemonPid,
|
|
6857
|
+
status: classification.status,
|
|
6858
|
+
context: { platform: input.platform, lineage: input.lineage, credentialStore: input.credentialStore },
|
|
6859
|
+
checkedAt: input.now ?? Date.now()
|
|
6860
|
+
};
|
|
6861
|
+
if (classification.authMethod) state.authMethod = classification.authMethod;
|
|
6862
|
+
if (classification.subscriptionType) state.subscriptionType = classification.subscriptionType;
|
|
6863
|
+
const diag = diagnosis?.diagnosis ?? classification.diagnosis;
|
|
6864
|
+
if (diag) state.diagnosis = diag;
|
|
6865
|
+
if (detail) state.detail = detail;
|
|
6866
|
+
if (diagnosis?.repairable) state.repairable = diagnosis.repairable;
|
|
6867
|
+
return state;
|
|
6868
|
+
}
|
|
6869
|
+
function claudeAuthStateChanged(prev, next) {
|
|
6870
|
+
if (!prev) return true;
|
|
6871
|
+
const strip = ({ checkedAt: _c, ...rest }) => JSON.stringify(rest);
|
|
6872
|
+
return strip(prev) !== strip(next);
|
|
6873
|
+
}
|
|
6874
|
+
function keychainIdentityFor(env) {
|
|
6875
|
+
return keychainIdentity(env);
|
|
6876
|
+
}
|
|
6877
|
+
|
|
6878
|
+
const CLAUDE_AUTH_PROBE_INTERVAL_MS = 10 * 60 * 1e3;
|
|
6879
|
+
const CLAUDE_AUTH_REFRESH_INTERVAL_MS = 30 * 60 * 1e3;
|
|
6880
|
+
const PROBE_TIMEOUT_MS = 8e3;
|
|
6881
|
+
const SECURITY_TIMEOUT_MS = 3e3;
|
|
6882
|
+
const SIGNAL_DEBOUNCE_MS = 1e3;
|
|
6883
|
+
class ClaudeAuthService {
|
|
6884
|
+
constructor(opts) {
|
|
6885
|
+
this.opts = opts;
|
|
6886
|
+
this.env = opts.env ?? process.env;
|
|
6887
|
+
this.platform = opts.platform ?? process.platform;
|
|
6888
|
+
}
|
|
6889
|
+
timer = null;
|
|
6890
|
+
signalTimer = null;
|
|
6891
|
+
inFlight = null;
|
|
6892
|
+
last = null;
|
|
6893
|
+
dirty = false;
|
|
6894
|
+
lastPublishedAt = 0;
|
|
6895
|
+
lineage = null;
|
|
6896
|
+
lastDiagnosis;
|
|
6897
|
+
env;
|
|
6898
|
+
platform;
|
|
6899
|
+
start() {
|
|
6900
|
+
setTimeout(() => void this.probe("startup"), 2e3).unref();
|
|
6901
|
+
this.timer = setInterval(() => void this.probe("interval"), CLAUDE_AUTH_PROBE_INTERVAL_MS);
|
|
6902
|
+
this.timer.unref();
|
|
6903
|
+
}
|
|
6904
|
+
stop() {
|
|
6905
|
+
if (this.timer) clearInterval(this.timer);
|
|
6906
|
+
if (this.signalTimer) clearTimeout(this.signalTimer);
|
|
6907
|
+
this.timer = null;
|
|
6908
|
+
this.signalTimer = null;
|
|
6909
|
+
}
|
|
6910
|
+
current() {
|
|
6911
|
+
return this.last;
|
|
6912
|
+
}
|
|
6913
|
+
/** D3: a session reported `authentication_failed`; re-probe after a short debounce. */
|
|
6914
|
+
signalAuthFailed(sessionId) {
|
|
6915
|
+
logger.debug(`[CLAUDE AUTH] auth_failed signal from session ${sessionId}`);
|
|
6916
|
+
if (this.signalTimer) clearTimeout(this.signalTimer);
|
|
6917
|
+
this.signalTimer = setTimeout(() => {
|
|
6918
|
+
this.signalTimer = null;
|
|
6919
|
+
void this.probe("signal", true);
|
|
6920
|
+
}, SIGNAL_DEBOUNCE_MS);
|
|
6921
|
+
this.signalTimer.unref();
|
|
6922
|
+
}
|
|
6923
|
+
/** Env the SDK/one-shot Claude Code processes should get for the current store setting. */
|
|
6924
|
+
claudeProcessEnvOverrides() {
|
|
6925
|
+
if (this.opts.getCredentialStore() !== "file") return {};
|
|
6926
|
+
const shim = this.resolveShimDir();
|
|
6927
|
+
if (!shim) return {};
|
|
6928
|
+
return { PATH: withKeychainOffPath(this.env.PATH, shim) };
|
|
6929
|
+
}
|
|
6930
|
+
effectiveCredentialStore() {
|
|
6931
|
+
if (this.opts.getCredentialStore() !== "file") return "auto";
|
|
6932
|
+
return this.resolveShimDir() ? "file" : "auto";
|
|
6933
|
+
}
|
|
6934
|
+
async probe(reason, withDiagnosis = false) {
|
|
6935
|
+
if (this.inFlight) return this.inFlight;
|
|
6936
|
+
this.inFlight = this.runProbe(reason, withDiagnosis).finally(() => {
|
|
6937
|
+
this.inFlight = null;
|
|
6938
|
+
});
|
|
6939
|
+
return this.inFlight;
|
|
6940
|
+
}
|
|
6941
|
+
async setStore(store) {
|
|
6942
|
+
await this.opts.setCredentialStore(store);
|
|
6943
|
+
return this.probe("set-store", true);
|
|
6944
|
+
}
|
|
6945
|
+
/** D4: delete the empty-token keychain item after re-validating every precondition. */
|
|
6946
|
+
async repair(action) {
|
|
6947
|
+
if (action !== "delete-empty-keychain-item") return { error: "unknown-action", claudeAuth: this.last };
|
|
6948
|
+
if (this.platform !== "darwin" || this.effectiveCredentialStore() === "file") return { error: "precondition-failed", claudeAuth: this.last };
|
|
6949
|
+
const state = await this.probe("repair-precheck", true);
|
|
6950
|
+
const seen = this.lastDiagnosis;
|
|
6951
|
+
if (state.diagnosis !== "keychain-empty-item" || !seen?.keychainRaw) return { error: "precondition-failed", claudeAuth: state };
|
|
6952
|
+
const identity = keychainIdentityFor(this.env);
|
|
6953
|
+
const again = interpretSecurityRead(await this.security(securityReadArgs(identity)));
|
|
6954
|
+
if (again.kind !== "present" || again.raw !== seen.keychainRaw || again.accessToken || again.refreshToken) {
|
|
6955
|
+
return { error: "precondition-failed", claudeAuth: state };
|
|
6956
|
+
}
|
|
6957
|
+
try {
|
|
6958
|
+
const dir = join(this.opts.happyHomeDir, "backups");
|
|
6959
|
+
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
6960
|
+
writeFileSync(join(dir, `claude-keychain-${Date.now()}.json`), again.raw + "\n", { mode: 384 });
|
|
6961
|
+
} catch (error) {
|
|
6962
|
+
return { error: `backup-failed: ${String(error)}`, claudeAuth: state };
|
|
6963
|
+
}
|
|
6964
|
+
const del = await this.security(securityDeleteArgs(identity));
|
|
6965
|
+
if (del.exitCode !== 0) return { error: `delete-failed: security exited ${del.exitCode ?? "null"}`, claudeAuth: state };
|
|
6966
|
+
logger.debug("[CLAUDE AUTH] deleted empty-token keychain item after backup");
|
|
6967
|
+
const after = await this.probe("repair-done", true);
|
|
6968
|
+
return { ok: true, claudeAuth: after };
|
|
6969
|
+
}
|
|
6970
|
+
// ── internals ────────────────────────────────────────────────────────
|
|
6971
|
+
resolveShimDir() {
|
|
6972
|
+
const dir = keychainOffShimDir(this.opts.happyLibDir);
|
|
6973
|
+
const bin = join(dir, "security");
|
|
6974
|
+
try {
|
|
6975
|
+
if (!statSync(bin).isFile()) return null;
|
|
6976
|
+
accessSync(bin, constants.X_OK);
|
|
6977
|
+
return dir;
|
|
6978
|
+
} catch {
|
|
6979
|
+
return null;
|
|
6980
|
+
}
|
|
6981
|
+
}
|
|
6982
|
+
async runProbe(reason, withDiagnosis) {
|
|
6983
|
+
const store = this.effectiveCredentialStore();
|
|
6984
|
+
const storeRequested = this.opts.getCredentialStore();
|
|
6985
|
+
const classification = await this.runAuthStatus();
|
|
6986
|
+
let diagnosis;
|
|
6987
|
+
this.lastDiagnosis = void 0;
|
|
6988
|
+
if (this.platform === "darwin" && store !== "file" && (classification.status === "not-logged-in" || withDiagnosis && classification.status !== "unknown")) {
|
|
6989
|
+
diagnosis = await this.diagnose(classification.status);
|
|
6990
|
+
this.lastDiagnosis = diagnosis;
|
|
6991
|
+
} else if (classification.status === "not-logged-in" && store === "file") {
|
|
6992
|
+
const file = interpretCredentialsFile(this.readCredentialsFile());
|
|
6993
|
+
if (!file.hasTokens) diagnosis = { diagnosis: "no-credentials", detail: "credentialStore=file and ~/.claude/.credentials.json has no tokens; log in with `claude` from a terminal on this machine." };
|
|
6994
|
+
}
|
|
6995
|
+
if (storeRequested === "file" && store === "auto") {
|
|
6996
|
+
diagnosis = { ...diagnosis ?? {}, detail: `credentialStore=file requested but the keychain-off shim is missing under ${keychainOffShimDir(this.opts.happyLibDir)}; running in auto mode.` };
|
|
6997
|
+
}
|
|
6998
|
+
const state = buildClaudeAuthState({
|
|
6999
|
+
daemonPid: process.pid,
|
|
7000
|
+
platform: this.platform,
|
|
7001
|
+
lineage: await this.resolveLineage(),
|
|
7002
|
+
credentialStore: store,
|
|
7003
|
+
classification,
|
|
7004
|
+
diagnosis,
|
|
7005
|
+
now: this.opts.now?.() ?? Date.now()
|
|
7006
|
+
});
|
|
7007
|
+
const changed = claudeAuthStateChanged(this.last, state);
|
|
7008
|
+
const stale = state.checkedAt - this.lastPublishedAt >= CLAUDE_AUTH_REFRESH_INTERVAL_MS;
|
|
7009
|
+
if (changed) {
|
|
7010
|
+
logger.debug(`[CLAUDE AUTH] ${reason}: status=${state.status} diagnosis=${state.diagnosis ?? "-"} lineage=${state.context.lineage} store=${state.context.credentialStore}`);
|
|
7011
|
+
}
|
|
7012
|
+
this.last = state;
|
|
7013
|
+
if (changed || stale || this.dirty) {
|
|
7014
|
+
const ok = await this.opts.publish(state).catch((error) => {
|
|
7015
|
+
logger.debug("[CLAUDE AUTH] publish failed:", error);
|
|
7016
|
+
return false;
|
|
7017
|
+
});
|
|
7018
|
+
this.dirty = !ok;
|
|
7019
|
+
if (ok) this.lastPublishedAt = state.checkedAt;
|
|
7020
|
+
else logger.warn("[CLAUDE AUTH] daemonState publish did not land; will force resend on the next probe");
|
|
7021
|
+
}
|
|
7022
|
+
return state;
|
|
7023
|
+
}
|
|
7024
|
+
async runAuthStatus() {
|
|
7025
|
+
const binary = resolveSdkClaudeBinary(this.platform, process.arch);
|
|
7026
|
+
if (!binary) {
|
|
7027
|
+
return { status: "claude-missing", diagnosis: "sdk-binary-missing", detail: "The Claude Code binary bundled with the Agent SDK is missing; remote sessions cannot start." };
|
|
7028
|
+
}
|
|
7029
|
+
const cwd = join(this.opts.happyHomeDir, "tmp", "auth-probe");
|
|
7030
|
+
try {
|
|
7031
|
+
mkdirSync(cwd, { recursive: true });
|
|
7032
|
+
} catch {
|
|
7033
|
+
}
|
|
7034
|
+
const env = { ...this.env, ...this.claudeProcessEnvOverrides() };
|
|
7035
|
+
const run = await new Promise((resolve) => {
|
|
7036
|
+
let stdout = "";
|
|
7037
|
+
let settled = false;
|
|
7038
|
+
const done = (r) => {
|
|
7039
|
+
if (!settled) {
|
|
7040
|
+
settled = true;
|
|
7041
|
+
resolve(r);
|
|
7042
|
+
}
|
|
7043
|
+
};
|
|
7044
|
+
let child;
|
|
7045
|
+
try {
|
|
7046
|
+
child = spawn$3(binary, ["auth", "status"], { cwd, env, stdio: ["ignore", "pipe", "ignore"] });
|
|
7047
|
+
} catch (error) {
|
|
7048
|
+
done({ stdout: "", exitCode: null, timedOut: false, spawnError: String(error) });
|
|
7049
|
+
return;
|
|
7050
|
+
}
|
|
7051
|
+
const timer = setTimeout(() => {
|
|
7052
|
+
try {
|
|
7053
|
+
child.kill("SIGKILL");
|
|
7054
|
+
} catch {
|
|
7055
|
+
}
|
|
7056
|
+
done({ stdout, exitCode: null, timedOut: true });
|
|
7057
|
+
}, PROBE_TIMEOUT_MS);
|
|
7058
|
+
child.stdout?.on("data", (chunk) => {
|
|
7059
|
+
if (stdout.length < 65536) stdout += chunk.toString("utf8");
|
|
7060
|
+
});
|
|
7061
|
+
child.on("error", (error) => {
|
|
7062
|
+
clearTimeout(timer);
|
|
7063
|
+
done({ stdout, exitCode: null, timedOut: false, spawnError: String(error) });
|
|
7064
|
+
});
|
|
7065
|
+
child.on("close", (code) => {
|
|
7066
|
+
clearTimeout(timer);
|
|
7067
|
+
done({ stdout, exitCode: code, timedOut: false });
|
|
7068
|
+
});
|
|
7069
|
+
});
|
|
7070
|
+
return classifyAuthStatus(run, this.opts.credentialSource);
|
|
7071
|
+
}
|
|
7072
|
+
async diagnose(status) {
|
|
7073
|
+
const identity = keychainIdentityFor(this.env);
|
|
7074
|
+
const keychain = interpretSecurityRead(await this.security(securityReadArgs(identity)));
|
|
7075
|
+
const file = interpretCredentialsFile(this.readCredentialsFile(identity.credentialsPath));
|
|
7076
|
+
return diagnoseStores({ status, keychain, file });
|
|
7077
|
+
}
|
|
7078
|
+
readCredentialsFile(path) {
|
|
7079
|
+
const target = path ?? keychainIdentityFor(this.env).credentialsPath;
|
|
7080
|
+
try {
|
|
7081
|
+
return existsSync(target) ? readFileSync(target, "utf8") : null;
|
|
7082
|
+
} catch {
|
|
7083
|
+
return null;
|
|
7084
|
+
}
|
|
7085
|
+
}
|
|
7086
|
+
security(args) {
|
|
7087
|
+
return new Promise((resolve) => {
|
|
7088
|
+
execFile("security", args, { env: this.env, timeout: SECURITY_TIMEOUT_MS, maxBuffer: 65536 }, (error, stdout) => {
|
|
7089
|
+
const err = error;
|
|
7090
|
+
if (err && err.code === "ENOENT") return resolve({ exitCode: null, stdout: "", error: "ENOENT" });
|
|
7091
|
+
if (err && err.killed) return resolve({ exitCode: null, stdout: "", error: "timeout" });
|
|
7092
|
+
const exitCode = err ? typeof err.code === "number" ? err.code : 1 : 0;
|
|
7093
|
+
resolve({ exitCode, stdout: String(stdout ?? "") });
|
|
7094
|
+
});
|
|
7095
|
+
});
|
|
7096
|
+
}
|
|
7097
|
+
async resolveLineage() {
|
|
7098
|
+
if (this.lineage) return this.lineage;
|
|
7099
|
+
if (this.platform !== "darwin") {
|
|
7100
|
+
this.lineage = "other";
|
|
7101
|
+
return this.lineage;
|
|
7102
|
+
}
|
|
7103
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : null;
|
|
7104
|
+
const launchdJobPid = uid === null ? null : parseLaunchctlPid(await this.exec("launchctl", ["print", `gui/${uid}/${HAPPY_DAEMON_LAUNCHD_LABEL}`]));
|
|
7105
|
+
const ancestors = await this.ancestorPids();
|
|
7106
|
+
this.lineage = classifyLineage({ platform: this.platform, env: this.env, launchdJobPid, ancestorPids: ancestors });
|
|
7107
|
+
return this.lineage;
|
|
7108
|
+
}
|
|
7109
|
+
async ancestorPids() {
|
|
7110
|
+
const out = [];
|
|
7111
|
+
let pid = process.ppid;
|
|
7112
|
+
for (let i = 0; i < 10 && pid > 1; i++) {
|
|
7113
|
+
out.push(pid);
|
|
7114
|
+
const ppid = Number((await this.exec("ps", ["-o", "ppid=", "-p", String(pid)])).trim());
|
|
7115
|
+
if (!Number.isFinite(ppid) || ppid === pid) break;
|
|
7116
|
+
pid = ppid;
|
|
7117
|
+
}
|
|
7118
|
+
return out;
|
|
7119
|
+
}
|
|
7120
|
+
exec(cmd, args) {
|
|
7121
|
+
return new Promise((resolve) => {
|
|
7122
|
+
execFile(cmd, args, { env: this.env, timeout: SECURITY_TIMEOUT_MS, maxBuffer: 65536 }, (_error, stdout) => resolve(String(stdout ?? "")));
|
|
7123
|
+
});
|
|
7124
|
+
}
|
|
7125
|
+
}
|
|
7126
|
+
|
|
6649
7127
|
const INTERNAL_CLAUDE_EVENT_TYPES = /* @__PURE__ */ new Set(["file-history-snapshot", "change", "queue-operation"]);
|
|
6650
7128
|
function parseLines(lines) {
|
|
6651
7129
|
const out = [];
|
|
@@ -7365,6 +7843,8 @@ const initialMachineMetadata = {
|
|
|
7365
7843
|
resumeSupport: { ...detectResumeSupport(), rpcAvailable: true }
|
|
7366
7844
|
};
|
|
7367
7845
|
async function startDaemon() {
|
|
7846
|
+
let claudeAuthServiceRef = null;
|
|
7847
|
+
let claudeCredentialStoreSetting = (await readSettings()).claudeCredentialStore === "file" ? "file" : "auto";
|
|
7368
7848
|
let requestShutdown;
|
|
7369
7849
|
let resolvesWhenShutdownRequested = new Promise((resolve) => {
|
|
7370
7850
|
requestShutdown = (source, errorMessage) => {
|
|
@@ -7671,7 +8151,11 @@ async function startDaemon() {
|
|
|
7671
8151
|
}
|
|
7672
8152
|
let extraEnv = {
|
|
7673
8153
|
...authEnv,
|
|
7674
|
-
...options.environmentVariables ?? {}
|
|
8154
|
+
...options.environmentVariables ?? {},
|
|
8155
|
+
// B-276 D8: credentialStore=file pins the spawned Claude Code (SDK
|
|
8156
|
+
// Query, title/board one-shots — all inside this wrapper) to
|
|
8157
|
+
// ~/.claude/.credentials.json via the keychain-off `security` shim.
|
|
8158
|
+
...claudeAuthServiceRef?.claudeProcessEnvOverrides() ?? {}
|
|
7675
8159
|
};
|
|
7676
8160
|
if (options.parentSessionId) {
|
|
7677
8161
|
extraEnv.HAPPY_FORKED_FROM_SESSION_ID = options.parentSessionId;
|
|
@@ -8297,6 +8781,7 @@ async function startDaemon() {
|
|
|
8297
8781
|
requestShutdown: () => requestShutdown("happy-cli"),
|
|
8298
8782
|
onHappySessionWebhook,
|
|
8299
8783
|
onSessionStateEvent,
|
|
8784
|
+
onClaudeAuthFailed: (sessionId) => claudeAuthServiceRef?.signalAuthFailed(sessionId),
|
|
8300
8785
|
pushClipboard: (text) => {
|
|
8301
8786
|
if (!apiMachineRef) {
|
|
8302
8787
|
return { delivered: false, truncated: false, totalBytes: 0, error: "daemon is still starting up" };
|
|
@@ -8367,6 +8852,25 @@ async function startDaemon() {
|
|
|
8367
8852
|
listTrackedSessionIds: () => [...pidToTrackedSession.values()].map((session) => session.happySessionId).filter((sessionId) => typeof sessionId === "string"),
|
|
8368
8853
|
requestShutdown: () => requestShutdown("happy-app")
|
|
8369
8854
|
});
|
|
8855
|
+
const claudeAuthService = new ClaudeAuthService({
|
|
8856
|
+
happyHomeDir: configuration.happyHomeDir,
|
|
8857
|
+
happyLibDir: projectPath(),
|
|
8858
|
+
credentialSource: daemonClaudeCredentials.source,
|
|
8859
|
+
getCredentialStore: () => claudeCredentialStoreSetting,
|
|
8860
|
+
setCredentialStore: async (store) => {
|
|
8861
|
+
const settings = await readSettings();
|
|
8862
|
+
await writeSettings({ ...settings, claudeCredentialStore: store });
|
|
8863
|
+
claudeCredentialStoreSetting = store;
|
|
8864
|
+
},
|
|
8865
|
+
publish: (state) => apiMachine.setClaudeAuthState(state)
|
|
8866
|
+
});
|
|
8867
|
+
claudeAuthServiceRef = claudeAuthService;
|
|
8868
|
+
apiMachine.setClaudeAuthHandlers({
|
|
8869
|
+
probe: () => claudeAuthService.probe("rpc", true),
|
|
8870
|
+
repair: (action) => claudeAuthService.repair(action),
|
|
8871
|
+
setStore: (store) => claudeAuthService.setStore(store)
|
|
8872
|
+
});
|
|
8873
|
+
claudeAuthService.start();
|
|
8370
8874
|
apiMachine.connect();
|
|
8371
8875
|
let cliUpdateCheckRunning = false;
|
|
8372
8876
|
const refreshCliUpdate = async () => {
|
|
@@ -8421,6 +8925,8 @@ async function startDaemon() {
|
|
|
8421
8925
|
logger.debug("[DAEMON RUN] Daemon bundle replaced on disk, handing off to new daemon");
|
|
8422
8926
|
clearInterval(restartOnStaleVersionAndHeartbeat);
|
|
8423
8927
|
clearInterval(cliUpdateInterval);
|
|
8928
|
+
claudeAuthService.stop();
|
|
8929
|
+
claudeAuthService.stop();
|
|
8424
8930
|
apiMachine.shutdown();
|
|
8425
8931
|
await stopControlServer();
|
|
8426
8932
|
await cleanupDaemonState();
|
|
@@ -14235,7 +14741,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
14235
14741
|
process.exit(0);
|
|
14236
14742
|
} else if (subcommand === "install-terminal-hooks") {
|
|
14237
14743
|
try {
|
|
14238
|
-
const { installTerminalHooks, parseTerminalHooksArgs, TERMINAL_HOOKS_HELP } = await import('./installTerminalHooks-
|
|
14744
|
+
const { installTerminalHooks, parseTerminalHooksArgs, TERMINAL_HOOKS_HELP } = await import('./installTerminalHooks-DqNtW6a2.mjs');
|
|
14239
14745
|
const command = parseTerminalHooksArgs(args.slice(1));
|
|
14240
14746
|
if (command.action === "help") {
|
|
14241
14747
|
console.log(TERMINAL_HOOKS_HELP);
|
|
@@ -14252,7 +14758,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
14252
14758
|
}
|
|
14253
14759
|
} else if (subcommand === "spawn") {
|
|
14254
14760
|
try {
|
|
14255
|
-
const { handleSpawnCommand } = await import('./spawn-
|
|
14761
|
+
const { handleSpawnCommand } = await import('./spawn-yS6yY8Xi.mjs');
|
|
14256
14762
|
await handleSpawnCommand(args.slice(1));
|
|
14257
14763
|
} catch (error) {
|
|
14258
14764
|
console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
|
|
@@ -14264,7 +14770,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
14264
14770
|
return;
|
|
14265
14771
|
} else if (subcommand === "send") {
|
|
14266
14772
|
try {
|
|
14267
|
-
const { handleSendCommand } = await import('./send-
|
|
14773
|
+
const { handleSendCommand } = await import('./send-c-wEmh5U.mjs');
|
|
14268
14774
|
await handleSendCommand(args.slice(1));
|
|
14269
14775
|
} catch (error) {
|
|
14270
14776
|
console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
|
|
@@ -14370,9 +14876,9 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
14370
14876
|
if (geminiSubcommand === "project" && args[2] === "set" && args[3]) {
|
|
14371
14877
|
const projectId = args[3];
|
|
14372
14878
|
try {
|
|
14373
|
-
const { saveGoogleCloudProjectToConfig } = await import('./config-
|
|
14374
|
-
const { readCredentialsForConfiguredRelay: readCredentialsForConfiguredRelay2 } = await import('./types-
|
|
14375
|
-
const { ApiClient: ApiClient2 } = await import('./types-
|
|
14879
|
+
const { saveGoogleCloudProjectToConfig } = await import('./config-DCH33LOM.mjs');
|
|
14880
|
+
const { readCredentialsForConfiguredRelay: readCredentialsForConfiguredRelay2 } = await import('./types-xZOYW0_G.mjs').then(function (n) { return n.am; });
|
|
14881
|
+
const { ApiClient: ApiClient2 } = await import('./types-xZOYW0_G.mjs').then(function (n) { return n.an; });
|
|
14376
14882
|
let userEmail = void 0;
|
|
14377
14883
|
try {
|
|
14378
14884
|
const credentials = await readCredentialsForConfiguredRelay2();
|
|
@@ -14403,7 +14909,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
14403
14909
|
}
|
|
14404
14910
|
if (geminiSubcommand === "project" && args[2] === "get") {
|
|
14405
14911
|
try {
|
|
14406
|
-
const { readGeminiLocalConfig } = await import('./config-
|
|
14912
|
+
const { readGeminiLocalConfig } = await import('./config-DCH33LOM.mjs');
|
|
14407
14913
|
const config = readGeminiLocalConfig();
|
|
14408
14914
|
if (config.googleCloudProject) {
|
|
14409
14915
|
console.log(`Current Google Cloud Project: ${config.googleCloudProject}`);
|
|
@@ -14443,7 +14949,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
14443
14949
|
process.exit(0);
|
|
14444
14950
|
}
|
|
14445
14951
|
try {
|
|
14446
|
-
const { runGemini } = await import('./runGemini-
|
|
14952
|
+
const { runGemini } = await import('./runGemini-Brp0UmLx.mjs');
|
|
14447
14953
|
let startedBy = void 0;
|
|
14448
14954
|
for (let i = 1; i < args.length; i++) {
|
|
14449
14955
|
if (args[i] === "--started-by") {
|
|
@@ -14465,7 +14971,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
14465
14971
|
return;
|
|
14466
14972
|
} else if (subcommand === "acp") {
|
|
14467
14973
|
try {
|
|
14468
|
-
const { runAcp, resolveAcpAgentConfig } = await import('./index-
|
|
14974
|
+
const { runAcp, resolveAcpAgentConfig } = await import('./index-DkhMvofH.mjs');
|
|
14469
14975
|
let startedBy = void 0;
|
|
14470
14976
|
let verbose = false;
|
|
14471
14977
|
const acpArgs = [];
|
|
@@ -14505,7 +15011,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
14505
15011
|
return;
|
|
14506
15012
|
} else if (subcommand === "openclaw") {
|
|
14507
15013
|
try {
|
|
14508
|
-
const { runOpenClaw } = await import('./runOpenClaw-
|
|
15014
|
+
const { runOpenClaw } = await import('./runOpenClaw-BvPRFx-L.mjs');
|
|
14509
15015
|
let startedBy = void 0;
|
|
14510
15016
|
let verbose = false;
|
|
14511
15017
|
let gatewayUrl;
|
|
@@ -14556,7 +15062,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
14556
15062
|
return;
|
|
14557
15063
|
} else if (subcommand === "mcp" && args.length === 1) {
|
|
14558
15064
|
try {
|
|
14559
|
-
const { handleMcpCommand } = await import('./mcp-
|
|
15065
|
+
const { handleMcpCommand } = await import('./mcp-BKAuzaLX.mjs');
|
|
14560
15066
|
await handleMcpCommand();
|
|
14561
15067
|
} catch (error) {
|
|
14562
15068
|
process.stderr.write(`[very-happy mcp] Fatal: ${error instanceof Error ? error.message : String(error)}
|