claude-use 4.3.0 → 4.5.0
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/README.md +3 -0
- package/dist/cli.cjs +1416 -1128
- package/dist/index.cjs +2748 -0
- package/dist/index.mjs +2682 -0
- package/dist/types/cliError.d.ts +27 -0
- package/dist/types/config/classify.d.ts +49 -0
- package/dist/types/config/load.d.ts +42 -0
- package/dist/types/config/schema.d.ts +689 -0
- package/dist/types/config/store.d.ts +46 -0
- package/dist/types/frontdoor/state.d.ts +19 -0
- package/dist/types/headroom/ensure.d.ts +32 -0
- package/dist/types/headroom/mitm.d.ts +138 -0
- package/dist/types/headroom/state.d.ts +73 -0
- package/dist/types/headroom/supervisor.d.ts +107 -0
- package/dist/types/index.d.ts +16 -0
- package/dist/types/launcher/farm.d.ts +165 -0
- package/dist/types/launcher/guard.d.ts +57 -0
- package/dist/types/launcher/lock.d.ts +41 -0
- package/dist/types/launcher/ports.d.ts +121 -0
- package/dist/types/paths.d.ts +67 -0
- package/dist/types/resolve/conditions.d.ts +37 -0
- package/dist/types/resolve/decide.d.ts +45 -0
- package/dist/types/resolve/extends.d.ts +41 -0
- package/dist/types/resolve/flatten.d.ts +13 -0
- package/dist/types/resolve/match.d.ts +58 -0
- package/dist/types/resolve/pipeline.d.ts +30 -0
- package/dist/types/resolve/plan.d.ts +57 -0
- package/dist/types/resolve/projects.d.ts +61 -0
- package/dist/types/resolve/reconcile.d.ts +80 -0
- package/dist/types/resolve/types.d.ts +159 -0
- package/dist/types/resolve/walk.d.ts +85 -0
- package/package.json +16 -2
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Filesystem primitives `store.ts` needs, injected so tests can simulate a crash between the temp-file write and the rename without touching real process semantics (e.g. actually killing the process mid-write). The default implementation (`nodeStoreFs`) is real `node:fs`, used by every CLI adapter in normal operation and by this module's own tests when exercising the real atomic-rename behaviour end to end.
|
|
4
|
+
*/
|
|
5
|
+
export interface StoreFs {
|
|
6
|
+
/** Reads a file's contents as UTF-8 text, or returns undefined when it does not exist. */
|
|
7
|
+
readonly readFileUtf8: (filePath: string) => string | undefined;
|
|
8
|
+
/** Writes a file's full contents as UTF-8 text, creating or truncating it. */
|
|
9
|
+
readonly writeFileUtf8: (filePath: string, contents: string) => void;
|
|
10
|
+
/** Atomically renames `from` to `to` (same filesystem — both are always in the same directory in this module's own usage). */
|
|
11
|
+
readonly renameSync: (from: string, to: string) => void;
|
|
12
|
+
/** Creates a directory (and any missing parents), succeeding silently if it already exists. */
|
|
13
|
+
readonly mkdirSync: (dirPath: string) => void;
|
|
14
|
+
/** Removes a file, succeeding silently if it does not exist — used to clean up a stray temp file after a failed rename. */
|
|
15
|
+
readonly unlinkSync: (filePath: string) => void;
|
|
16
|
+
}
|
|
17
|
+
/** The real `node:fs`-backed implementation of `StoreFs`, used by every CLI adapter in normal operation. */
|
|
18
|
+
export declare const nodeStoreFs: StoreFs;
|
|
19
|
+
/**
|
|
20
|
+
* Reads and validates one JSON config file against `schema`.
|
|
21
|
+
*
|
|
22
|
+
* Returns undefined when the file does not exist. Throws `ConfigValidationError` (the same error type `src/config/load.ts` throws for every other config file in this project) when it exists but fails validation.
|
|
23
|
+
*/
|
|
24
|
+
export declare function readJson<S extends z.ZodType>(filePath: string, schema: S, storeFs?: StoreFs): z.infer<S> | undefined;
|
|
25
|
+
/**
|
|
26
|
+
* Writes `contents` as UTF-8 text to `filePath` atomically: writes to a temp file in the *same directory* first, then renames it into place. A crash (or thrown error) between the write and the rename leaves the original file at `filePath` completely untouched — the failure mode this exists to prevent is a config file left half-written after an interrupted process.
|
|
27
|
+
*
|
|
28
|
+
* The temp file's name includes the destination's own basename, the process id, and a random UUID, so two concurrent writers (even to the same file, even from two processes) never collide on the same temp path.
|
|
29
|
+
*/
|
|
30
|
+
export declare function writeTextAtomic(filePath: string, contents: string, storeFs?: StoreFs): void;
|
|
31
|
+
/** Writes `data` as pretty-printed JSON to `filePath`, via `writeTextAtomic`. */
|
|
32
|
+
export declare function writeJsonAtomic(filePath: string, data: unknown, storeFs?: StoreFs): void;
|
|
33
|
+
/** Inputs to `applyPatch`. */
|
|
34
|
+
export interface ApplyPatchOptions<T> {
|
|
35
|
+
/** Used as the base to merge `patch` into when `filePath` does not exist yet. Required for a file that may not exist yet (e.g. a config profile's own first `set` before it was ever written by `create`); omit only when the file is guaranteed to already exist. */
|
|
36
|
+
readonly defaults?: T;
|
|
37
|
+
readonly storeFs?: StoreFs;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Reads `filePath` (or falls back to `options.defaults` when it does not exist), shallow-merges `patch` over the top-level keys of the result, re-validates the merged object against `schema`, and writes it back atomically.
|
|
41
|
+
*
|
|
42
|
+
* The merge is deliberately shallow — only top-level keys of the parsed config are replaced wholesale by the corresponding key in `patch`. A caller that wants to add one key to a nested object (e.g. one more category inside `categories`) must read the existing nested object itself and pass the already-merged nested object as part of `patch`; `applyPatch` does not recurse into nested objects on its own.
|
|
43
|
+
*
|
|
44
|
+
* Throws when the file does not exist and no `defaults` were given, and re-throws `ConfigValidationError` (via the underlying `schema.parse`) when the merged result does not validate.
|
|
45
|
+
*/
|
|
46
|
+
export declare function applyPatch<S extends z.ZodType>(filePath: string, schema: S, patch: Readonly<Partial<z.infer<S>>>, options?: ApplyPatchOptions<z.infer<S>>): z.infer<S>;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { HeadroomFs } from "../headroom/state";
|
|
3
|
+
/**
|
|
4
|
+
* The front-door supervisor's state.json under `<home>/frontdoor/`. Every field is optional because the file exists in stages, exactly like headroom's and the old codex daemon's: a fresh supervisor writes its own pid before the listener is up, and a shut-down front door leaves only the sticky `lastPort` and any `lastError` worth surfacing.
|
|
5
|
+
*/
|
|
6
|
+
declare const FrontDoorStateSchema: z.ZodObject<{
|
|
7
|
+
supervisorPid: z.ZodOptional<z.ZodNumber>;
|
|
8
|
+
port: z.ZodOptional<z.ZodNumber>;
|
|
9
|
+
lastPort: z.ZodOptional<z.ZodNumber>;
|
|
10
|
+
lastError: z.ZodOptional<z.ZodString>;
|
|
11
|
+
}, z.core.$strict>;
|
|
12
|
+
export type FrontDoorState = z.infer<typeof FrontDoorStateSchema>;
|
|
13
|
+
/** Reads state.json, or undefined when absent or malformed: a malformed file is overwritten by the next supervisor rather than failing every launch over one bad write. */
|
|
14
|
+
export declare function readFrontDoorState(fs: HeadroomFs, stateFile: string): FrontDoorState | undefined;
|
|
15
|
+
/** Writes state.json. A plain write, for the reason headroom's is: the file is advisory coordination read tolerantly, and the exclusive-create start lock is the actual mutual exclusion. */
|
|
16
|
+
export declare function writeFrontDoorState(fs: HeadroomFs, stateFile: string, state: Readonly<FrontDoorState>): void;
|
|
17
|
+
/** The origin a routed session's base URL sits under, or undefined before the front door has ever served: what the headroom allowlist must admit for a codex provider routed through headroom, since headroom forwards codex traffic back to this address. */
|
|
18
|
+
export declare function frontDoorOrigin(state: FrontDoorState | undefined): string | undefined;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { CliError } from "../cliError";
|
|
2
|
+
import type { LayoutPaths } from "../paths";
|
|
3
|
+
import { type HeadroomFs } from "./state";
|
|
4
|
+
/** Raised when the daemon cannot be brought up in time, or reports a fatal `lastError`: a launch that asked for headroom must never silently fall back to a direct connection. */
|
|
5
|
+
export declare class HeadroomStartError extends CliError {
|
|
6
|
+
constructor(message: string);
|
|
7
|
+
}
|
|
8
|
+
/** Every effect the ensure step performs, injected so it runs against fakes in tests. */
|
|
9
|
+
export interface EnsureHeadroomPorts {
|
|
10
|
+
readonly fs: HeadroomFs;
|
|
11
|
+
/** Zombie-aware liveness: a supervisor or daemon that exited without being reaped still answers signal 0 as alive, but will never serve a request or write state, so it must read as dead here (see `realIsProcessRunning`). */
|
|
12
|
+
readonly isRunning: (pid: number) => boolean;
|
|
13
|
+
readonly now: () => number;
|
|
14
|
+
readonly sleep: (ms: number) => void;
|
|
15
|
+
/** Spawns the detached supervisor process that owns the headroom daemon, returning its pid. */
|
|
16
|
+
readonly spawnSupervisor: (paths: LayoutPaths) => number;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Brings the headroom daemon up for this launch, or finds it already running, and registers this launcher pid in its session registry (the fact that keeps the daemon alive and defers drift restarts until this launch is done).
|
|
20
|
+
*
|
|
21
|
+
* The exclusive-create start lock decides which of several concurrent launches spawns the one supervisor: its holder spawns and keeps waiting like everyone else, everyone else waits on state.json, and a lock whose holder has died is removed and retried. Readiness is "state names a live supervisor, a live headroom pid, a port, and a MITM port", which the supervisor only writes after its own `/readyz` probe has passed and its proxy is bound, so polling state alone never mistakes a bound-but-not-ready port for a usable daemon.
|
|
22
|
+
*
|
|
23
|
+
* Throws `HeadroomStartError` (naming the daemon log path and any recorded `lastError`) when the daemon is not up within `HEADROOM_START_TIMEOUT_MS`, or when a live supervisor has recorded a fatal error.
|
|
24
|
+
*/
|
|
25
|
+
export declare function ensureHeadroom(params: {
|
|
26
|
+
readonly paths: LayoutPaths;
|
|
27
|
+
readonly launcherPid: number;
|
|
28
|
+
readonly ports: EnsureHeadroomPorts;
|
|
29
|
+
}): {
|
|
30
|
+
readonly port: number;
|
|
31
|
+
readonly mitmPort: number;
|
|
32
|
+
};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import type { IncomingHttpHeaders, IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
import * as net from "node:net";
|
|
3
|
+
/**
|
|
4
|
+
* A CONNECT proxy the headroom supervisor runs as a second server in its own process, so an OAuth session can have headroom's compression AND Claude Code's Remote Control at once: Remote Control refuses any `ANTHROPIC_BASE_URL` other than the real API, but happily honours `HTTPS_PROXY`, so OAuth launches route at the proxy layer instead.
|
|
5
|
+
*
|
|
6
|
+
* Every CONNECT target is blind-tunnelled byte for byte EXCEPT the intercept host (Claude Code's own API), whose TLS this proxy terminates with a leaf certificate signed by a locally generated CA. On the terminated session, paths headroom serves are forwarded to headroom's existing loopback port over plain HTTP (compression, per-session stats, and upstream auth unchanged; the client's Authorization header passes through untouched), and every other path is piped by this proxy to the real upstream over TLS, so Remote Control's streaming, OAuth refreshes, and unknown endpoints bypass headroom entirely.
|
|
7
|
+
*/
|
|
8
|
+
/** The one CONNECT host whose TLS gets terminated: Claude Code's own API, the upstream headroom compresses. */
|
|
9
|
+
export declare const MITM_INTERCEPT_HOST = "api.anthropic.com";
|
|
10
|
+
/** Paths under this prefix are what headroom serves; everything else on the terminated session goes to the real upstream. */
|
|
11
|
+
export declare const HEADROOM_SERVED_PATH_PREFIX = "/v1/";
|
|
12
|
+
/** The port HTTPS is served on, both the default when a CONNECT authority names none and the port non-headroom paths are piped to on the real upstream. */
|
|
13
|
+
export declare const HTTPS_PORT = 443;
|
|
14
|
+
/** One parsed CONNECT authority, the host lowercased and the port defaulted. */
|
|
15
|
+
export interface ConnectTarget {
|
|
16
|
+
readonly host: string;
|
|
17
|
+
readonly port: number;
|
|
18
|
+
}
|
|
19
|
+
/** Parses a CONNECT authority (`host:port`, or a bare `host` meaning 443), or undefined when it names no host or a non-numeric port. */
|
|
20
|
+
export declare function parseConnectTarget(authority: string): ConnectTarget | undefined;
|
|
21
|
+
/** Whether a CONNECT target's host (port already stripped) is the one whose TLS this proxy terminates. */
|
|
22
|
+
export declare function isInterceptedHost(host: string, interceptHost: string): boolean;
|
|
23
|
+
/** Whether a request path on the terminated session is one headroom serves. The query string stays part of the path: `/v1/messages?beta=true` is still headroom's. */
|
|
24
|
+
export declare function servedByHeadroom(path: string | undefined): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Headers a forwarded HTTP message keeps: everything except the hop-by-hop set, which belongs to whichever connection carried it and must not leak onto the next one. The `Connection` header's own value can name further hop-by-hop headers (RFC 9110 section 7.6.1), so those go too. Returns a fresh object; the input is the live parser's map and is never mutated.
|
|
27
|
+
*/
|
|
28
|
+
export declare function forwardableHeaders(headers: Readonly<IncomingHttpHeaders>): IncomingHttpHeaders;
|
|
29
|
+
/** A CA certificate and its private key, both as PEM. */
|
|
30
|
+
export interface CaMaterial {
|
|
31
|
+
readonly certPem: string;
|
|
32
|
+
readonly keyPem: string;
|
|
33
|
+
}
|
|
34
|
+
/** A TLS leaf certificate and its private key, both as PEM. */
|
|
35
|
+
export interface LeafCert {
|
|
36
|
+
readonly certPem: string;
|
|
37
|
+
readonly keyPem: string;
|
|
38
|
+
}
|
|
39
|
+
/** Generates the local CA: one RSA keypair, self-signed, CA-only by basicConstraints and keyUsage, with a random serial. Pure node-forge, no openssl, so the same code works on every platform claude-use ships to. */
|
|
40
|
+
export declare function generateCa(now: Readonly<Date>): CaMaterial;
|
|
41
|
+
/** Mints the TLS leaf for one host, signed by the CA: its own RSA keypair, the host in the SAN, server-auth usage only. */
|
|
42
|
+
export declare function mintLeaf(ca: CaMaterial, host: string, now: Readonly<Date>): LeafCert;
|
|
43
|
+
/** Mints each host's leaf once from the CA and reuses it, because a 2048-bit keypair in pure JS costs seconds and nothing about the certificate changes between sessions. */
|
|
44
|
+
export declare function createLeafCache(ca: CaMaterial, now: () => Date): (host: string) => LeafCert;
|
|
45
|
+
/**
|
|
46
|
+
* Persistence for the CA: generated once on the machine's first headroom start and reused forever after, because regenerating it would invalidate every child's `NODE_EXTRA_CA_CERTS` pointing at the old file. The real implementation writes the key with mode 0600; injected fakes keep it in memory.
|
|
47
|
+
*/
|
|
48
|
+
export interface MitmCertStore {
|
|
49
|
+
/** The persisted CA, or undefined when none exists yet (or only half of it does: a cert without its key can sign nothing, so it counts as absent). */
|
|
50
|
+
readonly loadCa: () => CaMaterial | undefined;
|
|
51
|
+
/** Persists both halves of the CA, the key unreadable to other users. */
|
|
52
|
+
readonly writeCa: (ca: CaMaterial) => void;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Loads the persisted CA, generating and persisting one when no usable CA exists. A stored pair that node-forge cannot parse is treated as absent and replaced: a truncated write is the one failure mode a regeneration can actually repair, and every child that trusted the old file needs a new `NODE_EXTRA_CA_CERTS` anyway once its CA stops parsing.
|
|
56
|
+
*/
|
|
57
|
+
export declare function ensureCa(store: MitmCertStore, generate: () => CaMaterial): CaMaterial;
|
|
58
|
+
/** Where one forwarded request goes, and how. Headroom's loopback port is plain HTTP; the real upstream is TLS. */
|
|
59
|
+
interface MitmForwardTarget {
|
|
60
|
+
readonly host: string;
|
|
61
|
+
readonly port: number;
|
|
62
|
+
readonly tls: boolean;
|
|
63
|
+
/** The SNI name to present; defaults to `host`. Separated so a test can connect to a local fake while presenting the production hostname. */
|
|
64
|
+
readonly servername?: string;
|
|
65
|
+
/** Extra trust anchors for the target's certificate, when it is not system-trusted (a test's local fake upstream). */
|
|
66
|
+
readonly ca?: readonly string[];
|
|
67
|
+
/** Overrides certificate verification; production leaves it at the Node default (verify against system trust or `ca`). */
|
|
68
|
+
readonly rejectUnauthorized?: boolean;
|
|
69
|
+
}
|
|
70
|
+
/** Handles one parsed HTTP request on a terminated TLS session. */
|
|
71
|
+
type MitmRequestHandler = (request: IncomingMessage, response: ServerResponse) => void;
|
|
72
|
+
/** A bound loopback listener and how to stop it. */
|
|
73
|
+
interface MitmListenerHandle {
|
|
74
|
+
readonly port: number;
|
|
75
|
+
/** Stops accepting, ends every live connection, and resolves once the port is released. */
|
|
76
|
+
readonly close: () => Promise<void>;
|
|
77
|
+
}
|
|
78
|
+
/** Presents one leaf's server-side TLS on CONNECTed sockets. */
|
|
79
|
+
interface TlsAcceptor {
|
|
80
|
+
/** Starts the TLS handshake on a socket that has just received its CONNECT response. */
|
|
81
|
+
readonly accept: (socket: net.Socket) => void;
|
|
82
|
+
/** Drops every session this acceptor is terminating. */
|
|
83
|
+
readonly close: () => void;
|
|
84
|
+
}
|
|
85
|
+
/** Parses HTTP over terminated sockets and serves each request through the handler, keep-alive included. */
|
|
86
|
+
interface HttpParserSession {
|
|
87
|
+
readonly serve: (socket: net.Socket) => void;
|
|
88
|
+
readonly close: () => void;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Every effect the MITM proxy performs, injected so the routing decisions and lifecycle run against fakes in unit tests (the SupervisorPorts pattern): TCP listening, TLS termination, HTTP parsing, plain TCP tunnels, and request forwarding. The real implementation is `realMitmEffects`; the TLS round-trip test uses it with redirected targets.
|
|
92
|
+
*/
|
|
93
|
+
export interface MitmEffects {
|
|
94
|
+
/** Binds a loopback TCP listener; `preferredPort` is tried first and any free port used when it is taken (bind, do not probe: check-then-bind races). Resolves with the bound port and a close handle. */
|
|
95
|
+
readonly listenLoopback: (preferredPort: number | undefined, onSocket: (socket: net.Socket) => void) => Promise<MitmListenerHandle>;
|
|
96
|
+
/** Builds the TLS terminator for one leaf, handing each successfully handshaked session to `onSecure`. */
|
|
97
|
+
readonly createTlsAcceptor: (leaf: LeafCert, onSecure: (secure: net.Socket) => void) => TlsAcceptor;
|
|
98
|
+
/** Builds the HTTP parser bound to one request handler. */
|
|
99
|
+
readonly createHttpSession: (handler: MitmRequestHandler) => HttpParserSession;
|
|
100
|
+
/** Opens a raw TCP connection for a blind tunnel. */
|
|
101
|
+
readonly connectTcp: (host: string, port: number) => Promise<net.Socket>;
|
|
102
|
+
/** Streams one request to `target` and the response back, never buffering a body. */
|
|
103
|
+
readonly forwardHttp: (target: MitmForwardTarget, request: IncomingMessage, response: ServerResponse) => void;
|
|
104
|
+
}
|
|
105
|
+
/** Everything the proxy needs to route, resolved before it starts. */
|
|
106
|
+
export interface MitmServerConfig {
|
|
107
|
+
/** The CONNECT host whose TLS gets terminated. */
|
|
108
|
+
readonly interceptHost: string;
|
|
109
|
+
/**
|
|
110
|
+
* Headroom's loopback port, read per request rather than captured at start-up: the daemon can crash and restart on a different port while this proxy keeps listening, and headroom-served paths must follow it. While it reads undefined (the restart window), headroom-served paths fail fast with 502 rather than silently bypassing headroom to the real upstream.
|
|
111
|
+
*/
|
|
112
|
+
readonly headroomPort: () => number | undefined;
|
|
113
|
+
/** The leaf to terminate `interceptHost` with, minted and cached per host. */
|
|
114
|
+
readonly leafFor: (host: string) => LeafCert;
|
|
115
|
+
/** Where non-headroom paths on the terminated session are piped: the real upstream over TLS. */
|
|
116
|
+
readonly upstream: MitmForwardTarget;
|
|
117
|
+
}
|
|
118
|
+
/** A running MITM proxy. */
|
|
119
|
+
export interface MitmServerHandle {
|
|
120
|
+
readonly port: number;
|
|
121
|
+
/** Stops the listener, drops every live tunnel and terminated session, and resolves once the port is released. */
|
|
122
|
+
readonly close: () => Promise<void>;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Starts the MITM proxy: binds the listener, mints the intercept host's leaf, and wires the per-connection routing. Resolves once the listener is bound.
|
|
126
|
+
*
|
|
127
|
+
* The connection flow: read the CONNECT head (never more of the stream than that, so an early ClientHello glued to the head is pushed back with `unshift` and still seen by whatever consumes the socket next), then either blind-tunnel the target or terminate its TLS and parse HTTP on the session. Every effect flows through `effects`; nothing here touches the network or filesystem itself.
|
|
128
|
+
*/
|
|
129
|
+
export declare function startMitmServer(config: MitmServerConfig, effects: MitmEffects, preferredPort: number | undefined): Promise<MitmServerHandle>;
|
|
130
|
+
/** The real `MitmEffects` over node's own net, tls, and http. `connectTcp` opens real connections to the CONNECTed host, so tests that want a blind tunnel redirect it. */
|
|
131
|
+
export declare function realMitmEffects(): MitmEffects;
|
|
132
|
+
/** The real `MitmCertStore` on the filesystem: `ca.pem` world-readable (it is public material), `ca.key` mode 0600 exactly, chmod'ed after the write because a create's mode bits pass through the process umask. */
|
|
133
|
+
export declare function realMitmCertStore(paths: {
|
|
134
|
+
readonly headroomCaDir: string;
|
|
135
|
+
readonly headroomCaCertFile: string;
|
|
136
|
+
readonly headroomCaKeyFile: string;
|
|
137
|
+
}): MitmCertStore;
|
|
138
|
+
export {};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { type Provider } from "../config/schema";
|
|
3
|
+
import type { FarmFs } from "../launcher/ports";
|
|
4
|
+
/** The upstream headroom serves when no provider's base URL claims the request: Claude Code's own API. */
|
|
5
|
+
export declare const HEADROOM_ANTHROPIC_UPSTREAM = "https://api.anthropic.com";
|
|
6
|
+
/**
|
|
7
|
+
* The headroom supervisor's state.json under `<home>/headroom/`. Every field is optional because the file exists in stages: a fresh supervisor writes only `supervisorPid` before the daemon is up, and a shut-down daemon leaves the file with everything cleared except any `lastError` worth surfacing.
|
|
8
|
+
*/
|
|
9
|
+
export declare const HeadroomStateSchema: z.ZodObject<{
|
|
10
|
+
supervisorPid: z.ZodOptional<z.ZodNumber>;
|
|
11
|
+
headroomPid: z.ZodOptional<z.ZodNumber>;
|
|
12
|
+
port: z.ZodOptional<z.ZodNumber>;
|
|
13
|
+
mitmPort: z.ZodOptional<z.ZodNumber>;
|
|
14
|
+
lastMitmPort: z.ZodOptional<z.ZodNumber>;
|
|
15
|
+
lastPort: z.ZodOptional<z.ZodNumber>;
|
|
16
|
+
version: z.ZodOptional<z.ZodString>;
|
|
17
|
+
allowlistHash: z.ZodOptional<z.ZodString>;
|
|
18
|
+
installedSource: z.ZodOptional<z.ZodString>;
|
|
19
|
+
lastError: z.ZodOptional<z.ZodString>;
|
|
20
|
+
}, z.core.$strict>;
|
|
21
|
+
export type HeadroomState = z.infer<typeof HeadroomStateSchema>;
|
|
22
|
+
/** The filesystem operations headroom coordination needs: the same shape as `FarmFs`, injected so every decision over state, sessions, and the start lock runs against an in-memory fake in tests. */
|
|
23
|
+
export type HeadroomFs = Pick<FarmFs, "mkdirp" | "readFileUtf8" | "writeFileUtf8" | "writeFileExclusive" | "readdir" | "removeRecursive">;
|
|
24
|
+
/** The record held in one session-registry file. */
|
|
25
|
+
declare const HeadroomSessionSchema: z.ZodObject<{
|
|
26
|
+
pid: z.ZodNumber;
|
|
27
|
+
startedAt: z.ZodNumber;
|
|
28
|
+
}, z.core.$strict>;
|
|
29
|
+
export type HeadroomSession = z.infer<typeof HeadroomSessionSchema>;
|
|
30
|
+
/** The exclusive-create start lock's recorded holder. */
|
|
31
|
+
declare const HeadroomLockSchema: z.ZodObject<{
|
|
32
|
+
pid: z.ZodNumber;
|
|
33
|
+
at: z.ZodNumber;
|
|
34
|
+
}, z.core.$strict>;
|
|
35
|
+
export type HeadroomLock = z.infer<typeof HeadroomLockSchema>;
|
|
36
|
+
/** Reads state.json, or undefined when absent. A malformed file is treated as absent: the lock-and-spawn flow below overwrites it with a fresh supervisor's state rather than crashing every future launch over one bad write. */
|
|
37
|
+
export declare function readHeadroomState(fs: HeadroomFs, stateFile: string): HeadroomState | undefined;
|
|
38
|
+
/**
|
|
39
|
+
* Writes state.json. A plain write rather than the write-then-rename most config files get: state.json is advisory coordination, read tolerantly (`readHeadroomState` treats a malformed file as absent) and rewritten constantly by the supervisor, and the exclusive-create start lock below, not this file, is the actual mutual exclusion.
|
|
40
|
+
*/
|
|
41
|
+
export declare function writeHeadroomState(fs: HeadroomFs, stateFile: string, state: Readonly<HeadroomState>): void;
|
|
42
|
+
/** The allowlist the daemon is started with: every provider's base URL plus Claude Code's own API, so a session with no provider selected reaches the real Anthropic upstream through the same proxy. */
|
|
43
|
+
export declare function headroomAllowlist(providers: readonly {
|
|
44
|
+
readonly baseUrl: string;
|
|
45
|
+
}[]): readonly string[];
|
|
46
|
+
/**
|
|
47
|
+
* The upstreams a set of providers routes to through headroom: every `http` provider's base URL, plus the front door's origin when there is a codex provider and the front door has ever served. A codex session routed through headroom is forwarded back to the front door's own listener (the codex translation is one of its routes), so headroom must admit that address like any provider's; the front door's port is sticky, so its origin stays stable across restarts. Before the front door has ever served there is no address to admit, and the first codex launch starts it before bringing headroom up, so a freshly started headroom daemon already sees it.
|
|
48
|
+
*/
|
|
49
|
+
export declare function headroomUpstreams(providers: readonly Provider[], frontDoorOrigin: string | undefined): readonly {
|
|
50
|
+
readonly baseUrl: string;
|
|
51
|
+
}[];
|
|
52
|
+
/** A stable hash of an allowlist, so drift detection compares one short string instead of recomputing set equality against a running process it cannot interrogate. */
|
|
53
|
+
export declare function hashAllowlist(allowlist: readonly string[]): string;
|
|
54
|
+
/** Every provider definition under `providersDir`, best-effort: unreadable files are skipped, because the allowlist's job is to open every upstream a launch might use, not to fail a launch over one broken file. */
|
|
55
|
+
export declare function readAllProviders(fs: HeadroomFs, providersDir: string): readonly {
|
|
56
|
+
name: string;
|
|
57
|
+
provider: Provider;
|
|
58
|
+
}[];
|
|
59
|
+
/** One live session-registry entry, or undefined when the launcher pid has no file. */
|
|
60
|
+
export declare function readSession(fs: HeadroomFs, sessionsDir: string, pid: number): HeadroomSession | undefined;
|
|
61
|
+
/** Registers this launch in the session registry: the fact the daemon must keep serving, and the fact whose disappearance starts the idle shutdown clock. */
|
|
62
|
+
export declare function writeSession(fs: HeadroomFs, sessionsDir: string, session: Readonly<HeadroomSession>): void;
|
|
63
|
+
/** Removes a launcher's registry entry. Idempotent: an absent entry is not an error, because the supervisor's own pruning may have removed it first. */
|
|
64
|
+
export declare function removeSession(fs: HeadroomFs, sessionsDir: string, pid: number): void;
|
|
65
|
+
/** Every registry entry on disk, unreadable files skipped, sorted by pid: what `headroom status` prints and what the supervisor's idle and drift decisions read. */
|
|
66
|
+
export declare function listSessions(fs: HeadroomFs, sessionsDir: string): readonly HeadroomSession[];
|
|
67
|
+
/**
|
|
68
|
+
* Removes every registry entry whose pid is no longer running: a launcher that died without releasing its entry must not keep the daemon awake or block a drift restart forever. The predicate must be zombie-aware (see `realIsProcessRunning`): a launcher that exited but was never reaped still answers signal 0 as alive, which would keep its session registered indefinitely.
|
|
69
|
+
*/
|
|
70
|
+
export declare function pruneDeadSessions(fs: HeadroomFs, sessionsDir: string, isRunning: (pid: number) => boolean): readonly number[];
|
|
71
|
+
/** Reads the start lock, or undefined when absent or unreadable. */
|
|
72
|
+
export declare function readStartLock(fs: HeadroomFs, lockFile: string): HeadroomLock | undefined;
|
|
73
|
+
export {};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { LayoutPaths } from "../paths";
|
|
2
|
+
import type { MitmServerHandle } from "./mitm";
|
|
3
|
+
import { type HeadroomFs } from "./state";
|
|
4
|
+
/** Everything the supervisor needs to run the daemon, resolved from the global config and the filesystem before it starts. */
|
|
5
|
+
export interface HeadroomSupervisorConfig {
|
|
6
|
+
/** The install spec as configured (`headroom.source`), already defaulted. */
|
|
7
|
+
readonly source: string;
|
|
8
|
+
/** `headroom.idleShutdownMinutes`, already defaulted. */
|
|
9
|
+
readonly idleShutdownMinutes: number;
|
|
10
|
+
}
|
|
11
|
+
/** How the supervisor resolves the configured source against its defaults. */
|
|
12
|
+
export declare function resolveSupervisorConfig(configured: {
|
|
13
|
+
readonly source?: string;
|
|
14
|
+
readonly idleShutdownMinutes?: number;
|
|
15
|
+
}): HeadroomSupervisorConfig;
|
|
16
|
+
/**
|
|
17
|
+
* Every effect the supervisor performs, injected so the whole lifecycle is testable against fakes: no real process, port, clock, or HTTP call ever happens in a unit test.
|
|
18
|
+
*/
|
|
19
|
+
export interface SupervisorPorts {
|
|
20
|
+
readonly fs: HeadroomFs;
|
|
21
|
+
readonly paths: LayoutPaths;
|
|
22
|
+
readonly ownPid: number;
|
|
23
|
+
readonly now: () => number;
|
|
24
|
+
/**
|
|
25
|
+
* Waits `ms`, yielding to the event loop while it does. Deliberately NOT a synchronous Atomics.wait-style sleep: the real supervisor learns of its child's death through the ChildProcess exit event, and a blocking sleep would stop the event loop from ever delivering it (the exact failure that left an unreaped zombie answering liveness checks as alive).
|
|
26
|
+
*/
|
|
27
|
+
readonly sleep: (ms: number) => Promise<void>;
|
|
28
|
+
/**
|
|
29
|
+
* Zombie-aware liveness, and for a daemon this supervisor spawned, exit-event-backed: the implementation keeps the spawned ChildProcess handle, consumes its exit event (which is also what reaps it), and reports the pid as not running from that moment. A bare signal-0 check is not enough, because a defunct process still answers it.
|
|
30
|
+
*/
|
|
31
|
+
readonly isRunning: (pid: number) => boolean;
|
|
32
|
+
/** Returns a free loopback port. Async because the only reliable way to reserve one is to bind and release a socket. */
|
|
33
|
+
readonly freePort: () => Promise<number>;
|
|
34
|
+
/** Whether nothing is currently listening on one specific loopback port: the check that decides whether the sticky `lastPort` can be reused. The same bind-and-release technique `freePort` uses, applied to a number the caller already cares about. */
|
|
35
|
+
readonly isPortFree: (port: number) => Promise<boolean>;
|
|
36
|
+
/**
|
|
37
|
+
* Starts `headroom proxy` bound to `port` with the given allowlist, returning its pid. Output goes to the daemon log. The implementation must keep the ChildProcess handle and attach an exit listener (detaching the process is fine; unref'ing a child you still need events from is not, since without the listener nothing reaps it and it lingers as a zombie).
|
|
38
|
+
*/
|
|
39
|
+
readonly spawnHeadroom: (port: number, allowlist: readonly string[]) => number;
|
|
40
|
+
/** Stops a process the supervisor owns, escalating SIGTERM to SIGKILL on a bounded timeout (see `stopSupervisedProcess`). */
|
|
41
|
+
readonly stopProcess: (pid: number) => void;
|
|
42
|
+
/** True once `GET /readyz` on the port succeeds. */
|
|
43
|
+
readonly ready: (port: number) => Promise<boolean>;
|
|
44
|
+
/** Runs `uv tool install <spec>`, logging output to the daemon log. Returns ok=false with the error when `uv` is absent or the install fails. */
|
|
45
|
+
readonly install: (spec: string) => {
|
|
46
|
+
readonly ok: boolean;
|
|
47
|
+
readonly error?: string;
|
|
48
|
+
};
|
|
49
|
+
/** The installed `headroom --version` output, or undefined when the binary is not on PATH. */
|
|
50
|
+
readonly headroomVersion: () => string | undefined;
|
|
51
|
+
/**
|
|
52
|
+
* Starts this supervisor's second server: the in-process MITM CONNECT proxy on a loopback port, binding `preferredPort` when it is free and any free port otherwise. The proxy forwards the intercept host's headroom-served paths to `headroomPort()` (read live, because the daemon can crash and restart on a different port while the proxy keeps listening) and tunnels everything else untouched. Resolves once listening; rejects when no port can be bound.
|
|
53
|
+
*/
|
|
54
|
+
readonly startMitm: (preferredPort: number | undefined, headroomPort: () => number | undefined) => Promise<MitmServerHandle>;
|
|
55
|
+
readonly log: (line: string) => void;
|
|
56
|
+
}
|
|
57
|
+
/** First retry delay after a failed start. Half a second keeps a transient blip invisible to the next launch. */
|
|
58
|
+
export declare const HEADROOM_BACKOFF_BASE_MS = 500;
|
|
59
|
+
/** Ceiling for the retry delay: with no live sessions there is no user waiting, so half a minute between attempts is the most the daemon log should grow. */
|
|
60
|
+
export declare const HEADROOM_BACKOFF_CAP_MS = 30000;
|
|
61
|
+
/** Consecutive failed starts before the supervisor gives up, records `lastError`, and exits: a proxy that cannot come up five times in a row is broken, not unlucky. */
|
|
62
|
+
export declare const HEADROOM_START_RETRY_BUDGET = 5;
|
|
63
|
+
/** The supervisor's own tick rate for crash detection, drift checks, and idle shutdown. */
|
|
64
|
+
export declare const HEADROOM_POLL_MS = 1000;
|
|
65
|
+
/** The process-control primitives the escalating stop needs, injected so the escalation itself is unit-testable against a process that ignores SIGTERM. */
|
|
66
|
+
export interface StopProcessPrimitives {
|
|
67
|
+
/** Delivers a signal; throws when the target is already gone. */
|
|
68
|
+
readonly signal: (pid: number, signal: NodeJS.Signals) => void;
|
|
69
|
+
/** Zombie-aware liveness: a defunct process is not running. */
|
|
70
|
+
readonly isRunning: (pid: number) => boolean;
|
|
71
|
+
/** Synchronous wait between liveness polls; blocking is correct here, since nothing productive can happen until the target is gone or the grace expires. */
|
|
72
|
+
readonly waitMs: (ms: number) => void;
|
|
73
|
+
readonly now: () => number;
|
|
74
|
+
}
|
|
75
|
+
/** How `stopSupervisedProcess` ended. `still-running` means even SIGKILL did not clear the pid within its grace (an uninterruptible process); callers log it loudly. */
|
|
76
|
+
export type StopOutcome = "exited-on-term" | "killed" | "still-running";
|
|
77
|
+
/** Grace after SIGTERM before escalating to SIGKILL: enough for a proxy to drain an in-flight request, short enough that a restart is not visibly stalled. This proxy's server has been observed to ignore SIGTERM outright, so the escalation is not optional. */
|
|
78
|
+
export declare const TERM_GRACE_MS = 1500;
|
|
79
|
+
/** Grace after SIGKILL before declaring the process unkillable. SIGKILL is immediate at the kernel level; this only bounds the wait for the scheduler to reap it. */
|
|
80
|
+
export declare const KILL_GRACE_MS = 1500;
|
|
81
|
+
/**
|
|
82
|
+
* Stops a process the supervisor owns: SIGTERM, wait up to `TERM_GRACE_MS`; SIGKILL, wait up to `KILL_GRACE_MS`; then report. Liveness during the waits must be zombie-aware: an unreaped target still answers signal 0, which would read as "survived SIGKILL".
|
|
83
|
+
*/
|
|
84
|
+
export declare function stopSupervisedProcess(pid: number, primitives: StopProcessPrimitives): StopOutcome;
|
|
85
|
+
/** The backoff for attempt `attempt` (1-based): base doubled per failure, capped. */
|
|
86
|
+
export declare function backoffForAttempt(attempt: number): number;
|
|
87
|
+
/**
|
|
88
|
+
* Whether an installed `headroom --version` string satisfies the configured source. A source carrying a PEP 440 specifier (`headroom>=0.39`, `headroom==0.39.1`) is checked against the version; anything else (a bare git URL, the default) carries no version to check and is satisfied by any install, with drift handled by comparing `installedSource` instead.
|
|
89
|
+
*/
|
|
90
|
+
export declare function versionSatisfies(version: string | undefined, source: string): boolean;
|
|
91
|
+
/** Options for `runSupervisor`. */
|
|
92
|
+
export interface RunSupervisorOptions {
|
|
93
|
+
/**
|
|
94
|
+
* Bounds the supervisor loop's iterations. The real supervisor runs until it exits on its own (idle shutdown or fatal error); a bounded run is how tests observe steady-state behaviour (drift deferral, restart-on-crash, pruning) without an infinite loop, and such a run returns `HEADROOM_SUPERVISOR_STILL_RUNNING` instead of an exit code.
|
|
95
|
+
*/
|
|
96
|
+
readonly tickLimit?: number;
|
|
97
|
+
}
|
|
98
|
+
/** Returned by a tick-bounded `runSupervisor` that reached its limit while still managing the daemon: not an exit code, and never produced without `tickLimit`. */
|
|
99
|
+
export declare const HEADROOM_SUPERVISOR_STILL_RUNNING = -1;
|
|
100
|
+
/**
|
|
101
|
+
* Runs the headroom supervisor loop: install, bind this process's MITM proxy, start the daemon, keep both alive (the proxy lives in this process, so only the daemon needs restarting, for crash or drift), and shut both down after the configured idle period with an empty session registry. Returns the process exit code (0 for an idle shutdown, 1 for a fatal setup failure).
|
|
102
|
+
*
|
|
103
|
+
* Every effect flows through `ports`, so the whole loop is unit-testable with a fake clock, filesystem, and processes. The loop awaits its sleeps, which in the real implementation run on a timer: between ticks the event loop turns, the spawned child's exit event is delivered (and the child thereby reaped), and the next tick's liveness check reads the truth.
|
|
104
|
+
*/
|
|
105
|
+
export declare function runSupervisor(config: HeadroomSupervisorConfig, ports: SupervisorPorts, options?: RunSupervisorOptions): Promise<number>;
|
|
106
|
+
/** The allowlist as it stands right now: every provider's upstream (the front door's origin for a codex provider, which headroom forwards codex traffic back to) plus Claude Code's own API. */
|
|
107
|
+
export declare function currentHeadroomAllowlist(fs: HeadroomFs, paths: LayoutPaths): readonly string[];
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The library surface of claude-use: the pure, port-injected parts other tools can call in-process instead of shelling out to the CLI. Three groups, none of which imports interactive prompting or argument parsing:
|
|
3
|
+
*
|
|
4
|
+
* - identity and configuration-profile resolution and farm sync (`resolveDecisions`, `resyncFarm`, `buildEntryFacts`, ...), pure over facts and ports passed in;
|
|
5
|
+
* - headroom routing: the Remote-Control-preserving MITM CONNECT proxy and its certificate authority, and the supervisor and ensure lifecycle, pure over `SupervisorPorts`;
|
|
6
|
+
* - the ambient-credential guard.
|
|
7
|
+
*
|
|
8
|
+
* Everything the CLI alone needs (commander wiring, prompts, `doctor`, `check`) is deliberately not exported here.
|
|
9
|
+
*/
|
|
10
|
+
export { resolveDecisions, topLevelNames, type ResolveDecisionsInput, type ResolvedState } from "./resolve/pipeline";
|
|
11
|
+
export type { CompiledRule, Decision, Diagnostic, EliminatedRule, EntryFact, EntryFacts, FlattenedCascade, Layer, LayerId } from "./resolve/types";
|
|
12
|
+
export { buildEntryFacts, carryOver, FARM_MANIFEST_FILENAME, readFarmManifest, recoverFarm, recoveryDiagnostics, resyncFarm, type BuildEntryFactsParams, type CarryOverParams, type CarryOverResult, type RecoverFarmParams, type RecoveryResult, type ResyncFarmParams, type ResyncFarmResult, } from "./launcher/farm";
|
|
13
|
+
export { createLeafCache, ensureCa, forwardableHeaders, generateCa, HEADROOM_SERVED_PATH_PREFIX, isInterceptedHost, MITM_INTERCEPT_HOST, mintLeaf, parseConnectTarget, realMitmCertStore, realMitmEffects, servedByHeadroom, startMitmServer, type CaMaterial, type ConnectTarget, type LeafCert, type MitmCertStore, type MitmEffects, type MitmServerConfig, type MitmServerHandle, } from "./headroom/mitm";
|
|
14
|
+
export { ensureHeadroom, HeadroomStartError, type EnsureHeadroomPorts } from "./headroom/ensure";
|
|
15
|
+
export { resolveSupervisorConfig, runSupervisor, type HeadroomSupervisorConfig, type RunSupervisorOptions, type SupervisorPorts } from "./headroom/supervisor";
|
|
16
|
+
export { AMBIENT_CREDENTIAL_VARS, detectAmbientCredential, evaluateAmbientCredentialGuard, formatAmbientCredentialGuardMessage, type AmbientCredentialDetection, type AmbientCredentialGuardResult, type AmbientCredentialVar, type EvaluateAmbientCredentialGuardParams, type InjectedCredential, } from "./launcher/guard";
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import type { CategoryClassification, CategoryClassificationOverlay } from "../config/schema";
|
|
2
|
+
import { type FarmManifest } from "../resolve/reconcile";
|
|
3
|
+
import { type ResolvedState } from "../resolve/pipeline";
|
|
4
|
+
import type { CascadeInput } from "../resolve/walk";
|
|
5
|
+
import type { Diagnostic, EntryFacts } from "../resolve/types";
|
|
6
|
+
import type { FarmFs } from "./ports";
|
|
7
|
+
/** The manifest file each farm carries at its own root, recording what that resync built. Named distinctly from anything Claude Code itself writes into a config directory. */
|
|
8
|
+
export declare const FARM_MANIFEST_FILENAME = ".claude-use-farm.json";
|
|
9
|
+
/** Reads and validates the manifest at a farm root, or undefined when it is missing, unparseable, or does not validate. */
|
|
10
|
+
export declare function readFarmManifest(fs: FarmFs, farmRoot: string): FarmManifest | undefined;
|
|
11
|
+
/** Everything `buildEntryFacts` needs. Everything except the tree itself is injected, so the fact manifest is reproducible from a fake filesystem, a fake clock, and a fake branch. */
|
|
12
|
+
export interface BuildEntryFactsParams {
|
|
13
|
+
readonly fs: FarmFs;
|
|
14
|
+
readonly claudeHome: string;
|
|
15
|
+
readonly home: string;
|
|
16
|
+
readonly cwd: string;
|
|
17
|
+
readonly nowMs: number;
|
|
18
|
+
readonly env: Readonly<Record<string, string | undefined>>;
|
|
19
|
+
readonly branch?: string;
|
|
20
|
+
readonly branchDetached?: boolean;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Walks the canonical `~/.claude` and builds the `EntryFacts` manifest the pure resolver consumes.
|
|
24
|
+
*
|
|
25
|
+
* Two properties matter more than the walk itself. Symlinks are never followed: a directory symlink escaping the tree is one entry, not a subtree, which keeps the walk finite and matches how the farm links such an entry back. And every directory's `latestMtimeMs`/`totalSizeBytes` aggregate over its whole subtree rather than reporting its own inode, because a directory-scoped `newerThan` or `maxSizeBytes` condition asks about the data inside the directory — a directory's own mtime does not move when a file three levels down is rewritten.
|
|
26
|
+
*/
|
|
27
|
+
export declare function buildEntryFacts(params: BuildEntryFactsParams): EntryFacts;
|
|
28
|
+
/** Inputs to `carryOver`. */
|
|
29
|
+
export interface CarryOverParams {
|
|
30
|
+
readonly fs: FarmFs;
|
|
31
|
+
/** The superseded farm, already renamed aside. */
|
|
32
|
+
readonly previousRoot: string;
|
|
33
|
+
/** The new farm, already swapped into place. */
|
|
34
|
+
readonly farmRoot: string;
|
|
35
|
+
/**
|
|
36
|
+
* When given, a colliding name classified as `runtime` is resolved automatically rather than reported — see `CarryOverResult.autoResolved` for why that is always safe. Optional because a caller with no classification loaded (there is none, today) should get the old, fully-manual behaviour rather than a crash; every real call site passes it.
|
|
37
|
+
*/
|
|
38
|
+
readonly classification?: {
|
|
39
|
+
readonly defaults: CategoryClassification;
|
|
40
|
+
readonly overlay?: CategoryClassificationOverlay;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/** What `carryOver` moved, resolved on its own, and could not. */
|
|
44
|
+
export interface CarryOverResult {
|
|
45
|
+
/** Top-level names moved from the superseded farm into the new one. */
|
|
46
|
+
readonly carried: readonly string[];
|
|
47
|
+
/** Top-level names that collided but were resolved automatically because their category is `runtime` — see the doc comment on `carryOver` for why discarding the old copy is always safe here. */
|
|
48
|
+
readonly autoResolved: readonly string[];
|
|
49
|
+
/** Top-level names left behind because the new farm has its own entry of that name, and resolving them needs a human — everything that collided but was not classified `runtime`. */
|
|
50
|
+
readonly collided: readonly string[];
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Moves an identity's own locally-written state out of a superseded farm and into the new one.
|
|
54
|
+
*
|
|
55
|
+
* This is what makes a whole-directory swap safe at all. The farm root *is* `CLAUDE_CONFIG_DIR`, so it holds more than symlinks: the identity's own `identity.json`, the credentials file Claude Code writes on Linux and Windows, its `.claude.json`, its daemon and runtime state. None of that is shared, none of it is rebuildable, and a swap that simply replaced the directory would destroy all of it.
|
|
56
|
+
*
|
|
57
|
+
* Anything the previous resync built itself is skipped rather than carried: a symlink is a view of the canonical tree with no data of its own, and a directory the manifest records as materialised had its real children adopted into `~/.claude` before the swap ever started. Everything else is real local data and moves across by rename, so a credential file is relocated rather than duplicated — never briefly existing as two copies on disk.
|
|
58
|
+
*
|
|
59
|
+
* A name that exists in both is genuinely ambiguous in general — overwriting the new farm's own entry would discard whatever the resync just decided; overwriting the old one would discard data — so by default it is reported rather than resolved here, and the caller keeps the superseded farm on disk in that case.
|
|
60
|
+
*
|
|
61
|
+
* One category is not ambiguous, though: `runtime`'s own definition (see `config/categories.default.json`'s category table in the README) is specifically "live per-process or per-machine artifacts" — daemon locks, an MCP auth-needed cache, an update-check result — that make no sense being preserved across a swap at all, let alone fought over. When `classification` is given, a colliding name whose category resolves to `runtime` is discarded from the superseded copy and left exactly as the new farm already has it, with no data ever moved: `keep-new` is not a judgement call for this category, it is what the category already means. This needs only the name's *static* classification, never the resolved shared/not-shared decision for the current directory — a `runtime` entry is disposable whether or not this identity currently chooses to share it, so no cascade resolution is needed to make the call.
|
|
62
|
+
*
|
|
63
|
+
* Exported so `launcher/farmResolve.ts` can reuse this exact collision detection (and the same `runtime` auto-resolution) for `claude-use identity resolve-conflicts`'s interactive pass, rather than a second implementation that could drift from this one.
|
|
64
|
+
*/
|
|
65
|
+
export declare function carryOver(params: CarryOverParams): CarryOverResult;
|
|
66
|
+
/** What recovery found and did. */
|
|
67
|
+
export interface RecoveryResult {
|
|
68
|
+
/** Abandoned scratch trees removed. */
|
|
69
|
+
readonly removedScratch: readonly string[];
|
|
70
|
+
/** Set when the farm root was missing and a superseded farm was restored into its place. */
|
|
71
|
+
readonly restoredFrom?: string;
|
|
72
|
+
/** Superseded farms whose carry-over was completed and which were then discarded. */
|
|
73
|
+
readonly completed: readonly string[];
|
|
74
|
+
/** Top-level names, across every superseded farm processed, resolved automatically because their category is `runtime` — see `carryOver`'s own doc comment for why that needs no human decision. */
|
|
75
|
+
readonly autoResolved: readonly string[];
|
|
76
|
+
/** Superseded farms left on disk because they still held colliding data a human still needs to decide. */
|
|
77
|
+
readonly retained: readonly string[];
|
|
78
|
+
/** True when recovery changed anything, in which case the farm cannot be assumed to match its own manifest. */
|
|
79
|
+
readonly recovered: boolean;
|
|
80
|
+
}
|
|
81
|
+
/** Inputs to `recoverFarm`: the lock-taking wrapper around `recoverInterruptedSwap`. */
|
|
82
|
+
export interface RecoverFarmParams {
|
|
83
|
+
readonly fs: FarmFs;
|
|
84
|
+
readonly identitiesDir: string;
|
|
85
|
+
readonly identity: string;
|
|
86
|
+
readonly now: () => number;
|
|
87
|
+
readonly lock: ResyncFarmParams["lock"];
|
|
88
|
+
readonly classification?: {
|
|
89
|
+
readonly defaults: CategoryClassification;
|
|
90
|
+
readonly overlay?: CategoryClassificationOverlay;
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Runs `recoverInterruptedSwap` under the identity's own lock.
|
|
95
|
+
*
|
|
96
|
+
* This exists as its own entry point because recovery has to happen *before* the identity's `identity.json` is read, not merely before the farm is rebuilt: that file lives inside the farm root, so a crash between the swap's two renames leaves it sitting in a superseded directory where nothing will find it. Reading the identity first would silently launch with its `defaultConfigProfile` and `allowAmbientCredential` unset — the first of which decides what the farm is about to share.
|
|
97
|
+
*
|
|
98
|
+
* `resyncFarm` calls `recoverInterruptedSwap` directly rather than calling this, since it already holds the lock; taking it a second time from the same process would block against itself.
|
|
99
|
+
*/
|
|
100
|
+
export declare function recoverFarm(params: RecoverFarmParams): RecoveryResult;
|
|
101
|
+
/** Inputs to `resyncFarm`. */
|
|
102
|
+
export interface ResyncFarmParams {
|
|
103
|
+
readonly fs: FarmFs;
|
|
104
|
+
/** `~/.claude-use/identities` — the farm root's parent, and where the lock, scratch trees, and superseded farms all live. */
|
|
105
|
+
readonly identitiesDir: string;
|
|
106
|
+
readonly identity: string;
|
|
107
|
+
/** The configuration profile this launch resolved to, recorded in the manifest for `claude-use check`. */
|
|
108
|
+
readonly configProfile?: string;
|
|
109
|
+
/** The canonical `~/.claude` every farm symlink points back into. */
|
|
110
|
+
readonly claudeHome: string;
|
|
111
|
+
readonly home: string;
|
|
112
|
+
readonly cwd: string;
|
|
113
|
+
readonly env: Readonly<Record<string, string | undefined>>;
|
|
114
|
+
readonly branch?: string;
|
|
115
|
+
readonly branchDetached?: boolean;
|
|
116
|
+
readonly cascade: CascadeInput;
|
|
117
|
+
readonly classification: {
|
|
118
|
+
readonly defaults: CategoryClassification;
|
|
119
|
+
readonly overlay?: CategoryClassificationOverlay;
|
|
120
|
+
};
|
|
121
|
+
/** Reads the current time. Called repeatedly while waiting on the lock, then once more to stamp the build. */
|
|
122
|
+
readonly now: () => number;
|
|
123
|
+
/** Distinguishes this resync's scratch and superseded directory names from any other process's. `${pid}.${uuid}` in real use; a fixed string in tests. */
|
|
124
|
+
readonly uniqueSuffix: string;
|
|
125
|
+
readonly lock: {
|
|
126
|
+
readonly pid: number;
|
|
127
|
+
readonly isRunning: (pid: number) => boolean;
|
|
128
|
+
readonly sleep: (ms: number) => void;
|
|
129
|
+
readonly staleAfterMs?: number;
|
|
130
|
+
readonly retryDelayMs?: number;
|
|
131
|
+
readonly maxAttempts?: number;
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
/** The outcome of one resync. */
|
|
135
|
+
export interface ResyncFarmResult {
|
|
136
|
+
readonly farmRoot: string;
|
|
137
|
+
/** True when the resolved decision already matched what the farm's own manifest recorded, so nothing was written. */
|
|
138
|
+
readonly noOp: boolean;
|
|
139
|
+
/** The manifest the farm now carries — the newly written one, or the existing one on a no-op. */
|
|
140
|
+
readonly manifest: FarmManifest;
|
|
141
|
+
/** The fully resolved state, including the flattened cascade's launch flags, which the launcher goes on to use. */
|
|
142
|
+
readonly resolved: ResolvedState;
|
|
143
|
+
readonly adopted: readonly string[];
|
|
144
|
+
readonly recovery: RecoveryResult;
|
|
145
|
+
readonly diagnostics: readonly Diagnostic[];
|
|
146
|
+
readonly retainedPrevious?: string;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Brings one identity's farm into line with the cascade resolved for the current directory, and hands back everything the launcher needs to go on and spawn the real binary.
|
|
150
|
+
*
|
|
151
|
+
* The order below is fixed and each step depends on the one before it:
|
|
152
|
+
*
|
|
153
|
+
* 1. Take the per-identity lock, so two sessions under one identity in two directories cannot rewrite the same farm toward two different states at once.
|
|
154
|
+
* 2. Finish or undo any swap a previous launch was killed in the middle of, so everything after this point sees a coherent farm.
|
|
155
|
+
* 3. Read the previous manifest and, guided by it, list the parts of the still-live farm reconciliation is allowed to look at. This step only reads the farm; nothing mutates it.
|
|
156
|
+
* 4. Adopt anything Claude Code wrote into a materialised directory back into the canonical `~/.claude`.
|
|
157
|
+
* 5. **Then** build the fact manifest, from a canonical tree that now includes everything step 4 adopted. Building facts before adoption would make newly adopted data invisible to this resync and only visible on the next launch — the single most important ordering constraint in this module.
|
|
158
|
+
* 6. Resolve the cascade and plan the farm, both of them pure functions over those facts.
|
|
159
|
+
* 7. If the plan matches what the farm's manifest already records and step 4 adopted nothing, stop: no filesystem write happens at all. Launching repeatedly from the same directory with no configuration change is the overwhelmingly common case and it must stay cheap.
|
|
160
|
+
* 8. Otherwise build a scratch tree, write its manifest, and swap it into place.
|
|
161
|
+
* 9. Release the lock — before the real binary is spawned, never held across it.
|
|
162
|
+
*/
|
|
163
|
+
export declare function resyncFarm(params: ResyncFarmParams): ResyncFarmResult;
|
|
164
|
+
/** Turns a recovery result into the one diagnostic worth reporting about it, or nothing when there was nothing to recover. */
|
|
165
|
+
export declare function recoveryDiagnostics(recovery: RecoveryResult, identity: string): Diagnostic[];
|