1import type { ClaudeSettings } from "@t3tools/contracts";
2import * as Effect from "effect/Effect";
3import * as FileSystem from "effect/FileSystem";
4import * as Path from "effect/Path";
5import * as Schema from "effect/Schema";
6
7import { expandHomePath } from "@t3tools/provider-core/server/pathExpansion";
8import * as HostProcess from "@t3tools/shared/HostProcess";
9
10const quotePath = Schema.encodeSync(Schema.fromJsonString(Schema.String));
11
12/**
13 * Resolve the Claude config directory the CLI would use: the instance's
14 * `homePath` (exported as `CLAUDE_CONFIG_DIR`), then an inherited
15 * `CLAUDE_CONFIG_DIR`, then Claude's default `~/.claude`. Empty must not
16 * fall back to bare `$HOME` — that leftover from the old HOME override
17 * produced a different continuation group than an explicit `~/.claude`.
18 */
19export const resolveClaudeHomePath = Effect.fn("resolveClaudeHomePath")(function* (
20 config: Pick<ClaudeSettings, "homePath">,
21 environment?: NodeJS.ProcessEnv,
22): Effect.fn.Return<string, never, Path.Path> {
23 const path = yield* Path.Path;
24 const home = yield* HostProcess.HomeDirectory;
25 const homePath = config.homePath.trim();
26 if (homePath.length > 0) {
27 return path.resolve(expandHomePath(homePath, home));
28 }
29 // Inherited env vars are not shell-expanded, so a literal `~` stays literal.
30 const inherited = environment?.CLAUDE_CONFIG_DIR?.trim() ?? "";
31 if (inherited.length > 0) {
32 return path.resolve(inherited);
33 }
34 return path.resolve(path.join(home, ".claude"));
35});
36
37export const makeClaudeEnvironment = Effect.fn("makeClaudeEnvironment")(function* (
38 config: Pick<ClaudeSettings, "homePath">,
39 baseEnv?: NodeJS.ProcessEnv,
40): Effect.fn.Return<NodeJS.ProcessEnv, never, Path.Path> {
41 const resolvedBaseEnv = baseEnv ?? process.env;
42 const homePath = config.homePath.trim();
43 if (homePath.length === 0) return resolvedBaseEnv;
44 const resolvedHomePath = yield* resolveClaudeHomePath(config);
45 return {
46 ...resolvedBaseEnv,
47 // Isolate this instance's config via CLAUDE_CONFIG_DIR rather than HOME.
48 // Overriding HOME also relocates the macOS login keychain lookup
49 // ($HOME/Library/Keychains), so the spawned CLI can't find its stored
50 // OAuth credentials and reports "Not logged in". CLAUDE_CONFIG_DIR points
51 // Claude Code at its config dir directly while leaving HOME (and the
52 // keychain) intact.
53 CLAUDE_CONFIG_DIR: resolvedHomePath,
54 };
55});
56
57/**
58 * Whether an administrator installed Claude Code's `managed-mcp.json`. While it
59 * exists Claude refuses `--strict-mcp-config` and exits before initializing,
60 * but it already ignores user, project and `--mcp-config` servers on its own,
61 * so callers that pass the flag for isolation can drop it. Paths match the
62 * ones Claude Code checks.
63 */
64export const hasClaudeManagedMcpConfig = Effect.fn("hasClaudeManagedMcpConfig")(function* () {
65 const fileSystem = yield* FileSystem.FileSystem;
66 const platform = yield* HostProcess.Platform;
67 const managedMcpConfigPath =
68 platform === "darwin"
69 ? "/Library/Application Support/ClaudeCode/managed-mcp.json"
70 : platform === "win32"
71 ? "C:\\Program Files\\ClaudeCode\\managed-mcp.json"
72 : "/etc/claude-code/managed-mcp.json";
73 return yield* fileSystem.exists(managedMcpConfigPath).pipe(Effect.orElseSucceed(() => false));
74});
75
76export const makeClaudeContinuationGroupKey = Effect.fn("makeClaudeContinuationGroupKey")(
77 function* (
78 config: Pick<ClaudeSettings, "homePath">,
79 environment?: NodeJS.ProcessEnv,
80 ): Effect.fn.Return<string, never, Path.Path> {
81 const resolvedHomePath = yield* resolveClaudeHomePath(config, environment);
82 return `claude:home:${resolvedHomePath}`;
83 },
84);
85
86export const makeClaudeCapabilitiesCacheKey = Effect.fn("makeClaudeCapabilitiesCacheKey")(
87 function* (
88 config: Pick<ClaudeSettings, "binaryPath" | "homePath">,
89 cwd?: string,
90 environment?: NodeJS.ProcessEnv,
91 ): Effect.fn.Return<string, never, Path.Path> {
92 const resolvedHomePath = yield* resolveClaudeHomePath(config, environment);
93 return `${config.binaryPath}\0${resolvedHomePath}\0${cwd ?? ""}`;
94 },
95);
96
97/**
98 * Describe the spawned CLI's environment separately from the login command so
99 * paths remain literal on every shell, including relative inherited values.
100 */
101export const claudeSignedOutMessage = (input: {
102 readonly configDir: string | undefined;
103 readonly cwd: string;
104}): string => {
105 const configuration =
106 input.configDir !== undefined
107 ? ` from ${quotePath(input.cwd)}, with CLAUDE_CONFIG_DIR set to ${quotePath(input.configDir)}`
108 : "";
109 return `Claude could not authenticate. For subscription login, run \`claude auth login\` on this environment's machine${configuration}, then start a new thread. For API-key authentication, check this instance's configured credentials.`;
110};