very-happy-cli 0.2.96 → 0.2.98
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-BXiGiFC7.mjs} +1 -1
- package/dist/{AcpBackend-BaCt4_zS.cjs → AcpBackend-DhR8ml8L.cjs} +1 -1
- package/dist/{AcpSessionManager-grr_aJEI.mjs → AcpSessionManager-CDnrxAfQ.mjs} +1 -1
- package/dist/{AcpSessionManager-CvgmPlXB.cjs → AcpSessionManager-Ckc0NFmf.cjs} +1 -1
- package/dist/{config-B69TZlJj.mjs → config-B3pA9YsN.mjs} +3 -3
- package/dist/{config-BjsUeQPA.cjs → config-GJQc_kpD.cjs} +3 -3
- package/dist/{index-DID1FV_n.cjs → index-BBm2b34F.cjs} +5 -5
- package/dist/{index-C-uJXSm3.mjs → index-Bh5Rcy0s.mjs} +6 -6
- package/dist/{index-uc-dtu-3.cjs → index-CO1gNtuP.cjs} +528 -25
- package/dist/{index-BMmbxCTq.mjs → index-CuUVSZnK.mjs} +527 -24
- package/dist/index.cjs +3 -3
- package/dist/index.mjs +3 -3
- package/dist/{installTerminalHooks-CepXxAM_.cjs → installTerminalHooks-Cp9JVxQ-.cjs} +3 -3
- package/dist/{installTerminalHooks-CBHjEQ-a.mjs → installTerminalHooks-Ru0dn_zF.mjs} +3 -3
- package/dist/lib.cjs +1 -1
- package/dist/lib.d.cts +77 -0
- package/dist/lib.d.mts +77 -0
- package/dist/lib.mjs +1 -1
- package/dist/{mcp-CAtbtTXF.cjs → mcp-B5HMF24K.cjs} +3 -3
- package/dist/{mcp-BdSHqi2d.mjs → mcp-CQoVN0u2.mjs} +3 -3
- package/dist/{runGemini-qStPY5fH.cjs → runGemini-BBzPVzuc.cjs} +5 -5
- package/dist/{runGemini-cMZ3_CRf.mjs → runGemini-CwZXQsgu.mjs} +5 -5
- package/dist/{runOpenClaw-B8HR6GnX.cjs → runOpenClaw-B4nC6XGL.cjs} +4 -4
- package/dist/{runOpenClaw-BJ36wEbk.mjs → runOpenClaw-C-jz4rWr.mjs} +4 -4
- package/dist/{send-6eY55o3C.cjs → send-CWgaINHC.cjs} +3 -3
- package/dist/{send-TX13v8Xe.mjs → send-DN-nVBEI.mjs} +3 -3
- package/dist/{spawn-vOZA0kOi.cjs → spawn-C0NSP84Y.cjs} +3 -3
- package/dist/{spawn-CfCE6JAv.mjs → spawn-CqcfbOnk.mjs} +3 -3
- package/dist/{types-okYCRlgl.cjs → types-BP-4phS_.cjs} +358 -72
- package/dist/{types-CLRl0ET3.mjs → types-Cl4vWuDY.mjs} +356 -72
- 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
|
|
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 shellescape, a5 as decrypt, a6 as writeDaemonState, A as ApiClient, a7 as writeSettings, a8 as fetchCliUpdateState, a9 as resolveCliUpdateCheckInterval, aa as DEFAULT_CLI_UPDATE_CHECK_INTERVAL_MS, ab as releaseDaemonLock, ac as isValidSessionId, ad as listVhTerminals, ae as isValidTerminalId, af as readVhTerminal, ag as sendToVhTerminal, h as connectionState, ah as startOfflineReconnection, ai as clearCredentials, aj as clearMachineId, k as ensurePrivateDirectorySync, ak as SandboxConfigSchema, b as createEnvelope, al as CodexAppServerClient, am as getLatestDaemonLog } from './types-Cl4vWuDY.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 = [];
|
|
@@ -7350,9 +7828,6 @@ async function claimSessionOrExit(happySessionId, options) {
|
|
|
7350
7828
|
process.on("exit", () => releaseSessionLock(happySessionId));
|
|
7351
7829
|
}
|
|
7352
7830
|
|
|
7353
|
-
function shellescape(s) {
|
|
7354
|
-
return "'" + s.replace(/'/g, "'\\''") + "'";
|
|
7355
|
-
}
|
|
7356
7831
|
const hostSuffix = process.env.HAPPY_VARIANT === "dev" ? "-dev" : "";
|
|
7357
7832
|
const initialMachineMetadata = {
|
|
7358
7833
|
host: os$1.hostname() + hostSuffix,
|
|
@@ -7365,6 +7840,8 @@ const initialMachineMetadata = {
|
|
|
7365
7840
|
resumeSupport: { ...detectResumeSupport(), rpcAvailable: true }
|
|
7366
7841
|
};
|
|
7367
7842
|
async function startDaemon() {
|
|
7843
|
+
let claudeAuthServiceRef = null;
|
|
7844
|
+
let claudeCredentialStoreSetting = (await readSettings()).claudeCredentialStore === "file" ? "file" : "auto";
|
|
7368
7845
|
let requestShutdown;
|
|
7369
7846
|
let resolvesWhenShutdownRequested = new Promise((resolve) => {
|
|
7370
7847
|
requestShutdown = (source, errorMessage) => {
|
|
@@ -7671,7 +8148,11 @@ async function startDaemon() {
|
|
|
7671
8148
|
}
|
|
7672
8149
|
let extraEnv = {
|
|
7673
8150
|
...authEnv,
|
|
7674
|
-
...options.environmentVariables ?? {}
|
|
8151
|
+
...options.environmentVariables ?? {},
|
|
8152
|
+
// B-276 D8: credentialStore=file pins the spawned Claude Code (SDK
|
|
8153
|
+
// Query, title/board one-shots — all inside this wrapper) to
|
|
8154
|
+
// ~/.claude/.credentials.json via the keychain-off `security` shim.
|
|
8155
|
+
...claudeAuthServiceRef?.claudeProcessEnvOverrides() ?? {}
|
|
7675
8156
|
};
|
|
7676
8157
|
if (options.parentSessionId) {
|
|
7677
8158
|
extraEnv.HAPPY_FORKED_FROM_SESSION_ID = options.parentSessionId;
|
|
@@ -8297,6 +8778,7 @@ async function startDaemon() {
|
|
|
8297
8778
|
requestShutdown: () => requestShutdown("happy-cli"),
|
|
8298
8779
|
onHappySessionWebhook,
|
|
8299
8780
|
onSessionStateEvent,
|
|
8781
|
+
onClaudeAuthFailed: (sessionId) => claudeAuthServiceRef?.signalAuthFailed(sessionId),
|
|
8300
8782
|
pushClipboard: (text) => {
|
|
8301
8783
|
if (!apiMachineRef) {
|
|
8302
8784
|
return { delivered: false, truncated: false, totalBytes: 0, error: "daemon is still starting up" };
|
|
@@ -8367,6 +8849,25 @@ async function startDaemon() {
|
|
|
8367
8849
|
listTrackedSessionIds: () => [...pidToTrackedSession.values()].map((session) => session.happySessionId).filter((sessionId) => typeof sessionId === "string"),
|
|
8368
8850
|
requestShutdown: () => requestShutdown("happy-app")
|
|
8369
8851
|
});
|
|
8852
|
+
const claudeAuthService = new ClaudeAuthService({
|
|
8853
|
+
happyHomeDir: configuration.happyHomeDir,
|
|
8854
|
+
happyLibDir: projectPath(),
|
|
8855
|
+
credentialSource: daemonClaudeCredentials.source,
|
|
8856
|
+
getCredentialStore: () => claudeCredentialStoreSetting,
|
|
8857
|
+
setCredentialStore: async (store) => {
|
|
8858
|
+
const settings = await readSettings();
|
|
8859
|
+
await writeSettings({ ...settings, claudeCredentialStore: store });
|
|
8860
|
+
claudeCredentialStoreSetting = store;
|
|
8861
|
+
},
|
|
8862
|
+
publish: (state) => apiMachine.setClaudeAuthState(state)
|
|
8863
|
+
});
|
|
8864
|
+
claudeAuthServiceRef = claudeAuthService;
|
|
8865
|
+
apiMachine.setClaudeAuthHandlers({
|
|
8866
|
+
probe: () => claudeAuthService.probe("rpc", true),
|
|
8867
|
+
repair: (action) => claudeAuthService.repair(action),
|
|
8868
|
+
setStore: (store) => claudeAuthService.setStore(store)
|
|
8869
|
+
});
|
|
8870
|
+
claudeAuthService.start();
|
|
8370
8871
|
apiMachine.connect();
|
|
8371
8872
|
let cliUpdateCheckRunning = false;
|
|
8372
8873
|
const refreshCliUpdate = async () => {
|
|
@@ -8421,6 +8922,8 @@ async function startDaemon() {
|
|
|
8421
8922
|
logger.debug("[DAEMON RUN] Daemon bundle replaced on disk, handing off to new daemon");
|
|
8422
8923
|
clearInterval(restartOnStaleVersionAndHeartbeat);
|
|
8423
8924
|
clearInterval(cliUpdateInterval);
|
|
8925
|
+
claudeAuthService.stop();
|
|
8926
|
+
claudeAuthService.stop();
|
|
8424
8927
|
apiMachine.shutdown();
|
|
8425
8928
|
await stopControlServer();
|
|
8426
8929
|
await cleanupDaemonState();
|
|
@@ -14235,7 +14738,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
14235
14738
|
process.exit(0);
|
|
14236
14739
|
} else if (subcommand === "install-terminal-hooks") {
|
|
14237
14740
|
try {
|
|
14238
|
-
const { installTerminalHooks, parseTerminalHooksArgs, TERMINAL_HOOKS_HELP } = await import('./installTerminalHooks-
|
|
14741
|
+
const { installTerminalHooks, parseTerminalHooksArgs, TERMINAL_HOOKS_HELP } = await import('./installTerminalHooks-Ru0dn_zF.mjs');
|
|
14239
14742
|
const command = parseTerminalHooksArgs(args.slice(1));
|
|
14240
14743
|
if (command.action === "help") {
|
|
14241
14744
|
console.log(TERMINAL_HOOKS_HELP);
|
|
@@ -14252,7 +14755,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
14252
14755
|
}
|
|
14253
14756
|
} else if (subcommand === "spawn") {
|
|
14254
14757
|
try {
|
|
14255
|
-
const { handleSpawnCommand } = await import('./spawn-
|
|
14758
|
+
const { handleSpawnCommand } = await import('./spawn-CqcfbOnk.mjs');
|
|
14256
14759
|
await handleSpawnCommand(args.slice(1));
|
|
14257
14760
|
} catch (error) {
|
|
14258
14761
|
console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
|
|
@@ -14264,7 +14767,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
14264
14767
|
return;
|
|
14265
14768
|
} else if (subcommand === "send") {
|
|
14266
14769
|
try {
|
|
14267
|
-
const { handleSendCommand } = await import('./send-
|
|
14770
|
+
const { handleSendCommand } = await import('./send-DN-nVBEI.mjs');
|
|
14268
14771
|
await handleSendCommand(args.slice(1));
|
|
14269
14772
|
} catch (error) {
|
|
14270
14773
|
console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
|
|
@@ -14370,9 +14873,9 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
14370
14873
|
if (geminiSubcommand === "project" && args[2] === "set" && args[3]) {
|
|
14371
14874
|
const projectId = args[3];
|
|
14372
14875
|
try {
|
|
14373
|
-
const { saveGoogleCloudProjectToConfig } = await import('./config-
|
|
14374
|
-
const { readCredentialsForConfiguredRelay: readCredentialsForConfiguredRelay2 } = await import('./types-
|
|
14375
|
-
const { ApiClient: ApiClient2 } = await import('./types-
|
|
14876
|
+
const { saveGoogleCloudProjectToConfig } = await import('./config-B3pA9YsN.mjs');
|
|
14877
|
+
const { readCredentialsForConfiguredRelay: readCredentialsForConfiguredRelay2 } = await import('./types-Cl4vWuDY.mjs').then(function (n) { return n.an; });
|
|
14878
|
+
const { ApiClient: ApiClient2 } = await import('./types-Cl4vWuDY.mjs').then(function (n) { return n.ao; });
|
|
14376
14879
|
let userEmail = void 0;
|
|
14377
14880
|
try {
|
|
14378
14881
|
const credentials = await readCredentialsForConfiguredRelay2();
|
|
@@ -14403,7 +14906,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
14403
14906
|
}
|
|
14404
14907
|
if (geminiSubcommand === "project" && args[2] === "get") {
|
|
14405
14908
|
try {
|
|
14406
|
-
const { readGeminiLocalConfig } = await import('./config-
|
|
14909
|
+
const { readGeminiLocalConfig } = await import('./config-B3pA9YsN.mjs');
|
|
14407
14910
|
const config = readGeminiLocalConfig();
|
|
14408
14911
|
if (config.googleCloudProject) {
|
|
14409
14912
|
console.log(`Current Google Cloud Project: ${config.googleCloudProject}`);
|
|
@@ -14443,7 +14946,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
14443
14946
|
process.exit(0);
|
|
14444
14947
|
}
|
|
14445
14948
|
try {
|
|
14446
|
-
const { runGemini } = await import('./runGemini-
|
|
14949
|
+
const { runGemini } = await import('./runGemini-CwZXQsgu.mjs');
|
|
14447
14950
|
let startedBy = void 0;
|
|
14448
14951
|
for (let i = 1; i < args.length; i++) {
|
|
14449
14952
|
if (args[i] === "--started-by") {
|
|
@@ -14465,7 +14968,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
14465
14968
|
return;
|
|
14466
14969
|
} else if (subcommand === "acp") {
|
|
14467
14970
|
try {
|
|
14468
|
-
const { runAcp, resolveAcpAgentConfig } = await import('./index-
|
|
14971
|
+
const { runAcp, resolveAcpAgentConfig } = await import('./index-Bh5Rcy0s.mjs');
|
|
14469
14972
|
let startedBy = void 0;
|
|
14470
14973
|
let verbose = false;
|
|
14471
14974
|
const acpArgs = [];
|
|
@@ -14505,7 +15008,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
14505
15008
|
return;
|
|
14506
15009
|
} else if (subcommand === "openclaw") {
|
|
14507
15010
|
try {
|
|
14508
|
-
const { runOpenClaw } = await import('./runOpenClaw-
|
|
15011
|
+
const { runOpenClaw } = await import('./runOpenClaw-C-jz4rWr.mjs');
|
|
14509
15012
|
let startedBy = void 0;
|
|
14510
15013
|
let verbose = false;
|
|
14511
15014
|
let gatewayUrl;
|
|
@@ -14556,7 +15059,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
14556
15059
|
return;
|
|
14557
15060
|
} else if (subcommand === "mcp" && args.length === 1) {
|
|
14558
15061
|
try {
|
|
14559
|
-
const { handleMcpCommand } = await import('./mcp-
|
|
15062
|
+
const { handleMcpCommand } = await import('./mcp-CQoVN0u2.mjs');
|
|
14560
15063
|
await handleMcpCommand();
|
|
14561
15064
|
} catch (error) {
|
|
14562
15065
|
process.stderr.write(`[very-happy mcp] Fatal: ${error instanceof Error ? error.message : String(error)}
|