@bridge4dev/runner 0.65.1 → 0.67.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.
@@ -0,0 +1,433 @@
1
+ import { type ClaudeIdentity } from './agent-auth.js';
2
+ import { type UsageReading, type UsageRow, type UsageSignature } from './adapters/claude-usage.js';
3
+ import { AccountError, isAccountId } from './login-marks.js';
4
+ /**
5
+ * Several Claude logins on one machine – one HOME per account (#422, D1).
6
+ *
7
+ * An account is a small directory of its own: its login (`.credentials.json`)
8
+ * and the CLI's working files (`.claude.json` – where the last measured limits
9
+ * live – `sessions/`, `backups/`). Everything else is a link into the machine's
10
+ * `~/.claude`: the transcripts (without them no conversation resumes across
11
+ * accounts), settings, plugins, agents, commands, hooks, skills. Switching is
12
+ * choosing which home the NEXT session starts with – `CLAUDE_CONFIG_DIR` in the
13
+ * session's own environment, never in the daemon's (risk К1 of the plan).
14
+ *
15
+ * The login of this machine is the default home, `~/.claude`. It is the first
16
+ * row, it cannot be forgotten, and while it is active nothing here sets any
17
+ * variable – the behaviour is exactly the pre-#422 one. No code in this module
18
+ * writes, moves or deletes the machine's own login file: the one legitimate way
19
+ * to touch it stays the person's «Log in again» on that row, which is the old
20
+ * sign-in path in `auth-relay.ts`, not this file.
21
+ *
22
+ * NOT ONE COPY of a login file, by construction rather than by agreement. The
23
+ * CLI's token-refresh lock is taken by the PATH of the file, so a copy in a
24
+ * second home is a second holder of a one-time refresh token – the copy that
25
+ * signed out 47 agents and four sessions on 08.09.2026 (gotcha 460). Every
26
+ * login here exists in exactly one place; a login is only ever moved into its
27
+ * home by `rename`, and a `rename` that would have to cross filesystems is
28
+ * refused instead of falling back to a copy.
29
+ *
30
+ * Proven live on this machine before a line of it was written (S1 item 1,
31
+ * 17.09.2026, CLI 2.1.273, SDK 0.3.226): a session started through the Agent
32
+ * SDK with `CLAUDE_CONFIG_DIR` in `env` ran under that home's subscription, its
33
+ * transcript landed in the machine's `projects/` through the link, and the
34
+ * same session resumed under the machine login – a different subscription –
35
+ * and back again, with its context.
36
+ */
37
+ /** Where the saved homes live – next to the runner's other private state. */
38
+ export declare function claudeHomesDir(): string;
39
+ /**
40
+ * The machine's own Claude home. `os.homedir()` rather than anything cleverer:
41
+ * it is what the CLI itself uses when no `CLAUDE_CONFIG_DIR` is set, which is
42
+ * exactly the definition of «the machine login».
43
+ */
44
+ export declare function machineHome(homedir?: string): string;
45
+ /** The id format and the refusal live beside the machine row's id – shared by both agents. */
46
+ export { AccountError, isAccountId };
47
+ /** The home of a saved account. Throws on anything that is not an account id. */
48
+ export declare function accountHome(id: string): string;
49
+ /**
50
+ * What a saved home reaches through a link rather than owning (R20).
51
+ *
52
+ * `projects` is the one that must never be missing: it is where transcripts
53
+ * live, and a session resumes only from the home it can see them in. The rest
54
+ * is parity with the terminal – the machine's settings (hooks, the search
55
+ * guard, enabled plugins), its plugins, agents, commands, hooks and skills.
56
+ * Deliberately NOT here: `.credentials.json` and `.claude.json` (they are the
57
+ * account), and the CLI's per-process scratch (`sessions`, `session-env`,
58
+ * `shell-snapshots`, `backups`, …), which it recreates on its own.
59
+ */
60
+ export declare const SHARED_ENTRIES: readonly ["projects", "settings.json", "settings.local.json", "CLAUDE.md", "plugins", "agents", "commands", "hooks", "skills"];
61
+ export interface SharedLinkChange {
62
+ name: string;
63
+ action: 'linked' | 'relinked' | 'displaced' | 'dropped';
64
+ /** Where a file that stood in place of the link was moved to. */
65
+ displacedTo?: string;
66
+ }
67
+ /**
68
+ * Create and repair the links of a saved home into the machine's `~/.claude`.
69
+ *
70
+ * A link is set only where its target exists; `projects` is the exception and
71
+ * gets its target created, because a home without it would quietly start a
72
+ * transcript directory of its own and every later resume across accounts would
73
+ * fail to find the conversation.
74
+ *
75
+ * A link the CLI replaced with a real file or directory (an atomic write of
76
+ * `settings.json` does exactly that) is NOT silently re-linked: the thing in
77
+ * its place may be the only copy of what was written through it. It is moved
78
+ * aside under a dated name and logged, and the link is put back.
79
+ *
80
+ * Never follows a link to remove anything, never writes into the machine home
81
+ * beyond creating its empty `projects/`.
82
+ */
83
+ export declare function ensureSharedLinks(home: string, machine?: string): SharedLinkChange[];
84
+ /**
85
+ * Seed a home's `.claude.json` with the machine's configuration (R20).
86
+ *
87
+ * A home without the file gets one. A home WITH one (a login moved in from
88
+ * elsewhere) gets only the keys it does not have yet, project by project – its
89
+ * own identity and anything it configured for itself stay as they are.
90
+ *
91
+ * Only for a home no CLI is running in: the CLI rewrites this file on its own
92
+ * schedule, and a write racing it would lose one side. That is why it runs when
93
+ * a home is created or moved in, and not at every session start.
94
+ */
95
+ export declare function ensureHomeConfig(home: string, machineConfig?: string): 'created' | 'merged' | 'unchanged';
96
+ /** What `claude auth status` says about a home – without a verdict on the login. */
97
+ export interface AccountIdentity {
98
+ email?: string;
99
+ orgId?: string;
100
+ orgName?: string;
101
+ plan?: string;
102
+ }
103
+ /** Runs `claude auth status` in a home. Replaced only by tests. */
104
+ export type IdentityRunner = (home: string | null) => Promise<AccountIdentity | null>;
105
+ /**
106
+ * The environment `claude auth status` runs with (R14) – and, since S2, every
107
+ * CLI run that works in a home as that home's own account: the sign-in through
108
+ * `agent_account_login_start` uses it too (§8), for the reason below.
109
+ *
110
+ * `CLAUDE_CONFIG_DIR` of the home, or none for the machine, and never the
111
+ * operator's `CLAUDE_CODE_OAUTH_TOKEN`: the CLI puts that variable above every
112
+ * login file, so on a machine that has one every home would answer with the
113
+ * token's blind identity – no e-mail, no plan, the same for all of them.
114
+ */
115
+ export declare function identityProbeEnv(home: string | null, base?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
116
+ /** `claude auth status --json` → identity. `loggedIn: false` is no identity at all. */
117
+ export declare function parseAuthStatus(output: string): AccountIdentity | null;
118
+ export declare function setIdentityRunner(run: IdentityRunner | null): void;
119
+ /** Ask a home who it is. Only the four callers R14 names reach this. */
120
+ export declare function readAccountIdentity(home: string | null): Promise<AccountIdentity | null>;
121
+ /** Forget remembered misses – tests. */
122
+ export declare function resetIdentityMisses(): void;
123
+ /**
124
+ * Who a row is, from the cache or – when that is unknown, old, or contradicted by
125
+ * the home – from `claude auth status` in its home, persisted (R14).
126
+ *
127
+ * A probe that answers nothing keeps what was known: a CLI that timed out once
128
+ * must not turn a named account into «card not filled». And it is remembered for
129
+ * a while, so a home with no identity to give is not asked at every session start.
130
+ */
131
+ export declare function refreshAccountIdentity(id: string, options?: {
132
+ force?: boolean;
133
+ }): Promise<ClaudeIdentity | null>;
134
+ /** What one credentials file says, before any fallback is consulted. */
135
+ export interface CredentialVerdict {
136
+ status: 'ok' | 'expired' | 'missing' | 'unknown';
137
+ expiresAt?: string;
138
+ detail?: string;
139
+ /**
140
+ * May a token this runner captured answer instead? Only where the pre-#422
141
+ * verdict consulted it: no file, nothing recognisable in it, or a dead login.
142
+ * Never over a file we could not parse or could not read.
143
+ */
144
+ fallbackEligible: boolean;
145
+ }
146
+ /**
147
+ * Judge a Claude credentials file – moved here unchanged from `claudeAuthStatus`
148
+ * so a saved home and the machine are judged by the same lines (#422 R14).
149
+ *
150
+ * `expiresAt` is NOT the login. It is the expiry of a short-lived access token
151
+ * (~8 hours on a live file), and next to it sits `refreshToken` with
152
+ * `refreshTokenExpiresAt` ~26 days out, which the CLI spends silently on its
153
+ * next run. Judging the login by `expiresAt` alone is why every server nobody
154
+ * had touched since the morning reported "login expired — re-login needed"
155
+ * over a login that was good for another three weeks (#121). Codex has carried
156
+ * exactly this guard since day one (`readCodexCredential`); Claude did not.
157
+ *
158
+ * Deliberately NOT asking the CLI. `claude auth status --json` looks like an
159
+ * arbiter and is not one: measured live (SDK binary 2.1.218), it answers
160
+ * `loggedIn: true` for a credential whose access token has expired AND which
161
+ * carries no refresh token at all — i.e. for a genuinely dead login. It never
162
+ * leaves the machine, so it cannot see a server-side revocation either. It
163
+ * would have cost a ~800 ms / ~300 MB subprocess per poll under `MemoryMax=2G`
164
+ * (gotcha #100) and echoed the account's e-mail and org name to every member of
165
+ * the organization, in exchange for no truth at all. A revoked login is caught
166
+ * instead by the refusal marks — from a real refusal, not a guess.
167
+ */
168
+ export declare function judgeCredentialFile(file: string): CredentialVerdict;
169
+ /** The verdict on one row, as the panel and the list both read it. */
170
+ export interface ClaudeLoginStatus {
171
+ status: 'ok' | 'expired' | 'missing' | 'unknown';
172
+ expiresAt?: string;
173
+ detail?: string;
174
+ }
175
+ /**
176
+ * The machine row: the pre-#422 verdict, word for word.
177
+ *
178
+ * The operator's variable first, then the credentials file, then a token this
179
+ * runner captured – the LAST word, never the first. Read from disk rather than
180
+ * from the environment, so `doctor` (a different process, which never applied
181
+ * it) gives the same verdict as the daemon.
182
+ *
183
+ * One change, from S1 item 3: a refusal no longer DISCARDS the captured token
184
+ * (the old discard erased the whole file, and with it every saved account). The
185
+ * refusal is kept as a mark instead, and a token captured after it outranks it.
186
+ */
187
+ export declare function machineLoginStatus(homedir?: string): ClaudeLoginStatus;
188
+ /**
189
+ * Do the machine's Claude settings – which every saved home reads through its
190
+ * link – carry a credential that outranks a home's own login?
191
+ *
192
+ * `withAccountHome` takes the operator's token out of the PROCESS environment,
193
+ * but a settings file with `env: { CLAUDE_CODE_OAUTH_TOKEN }`, an API key or an
194
+ * `apiKeyHelper` puts one back inside the CLI, and a switch of account would
195
+ * then be a silent no-op under a green verdict – #121 once more (К14, found by
196
+ * the independent check of S1).
197
+ */
198
+ export declare function machineSettingsOverrideLogin(machine?: string): boolean;
199
+ /**
200
+ * A saved row: its own file and nothing else (R14).
201
+ *
202
+ * No operator variable and no captured token – a session under a saved account
203
+ * has both taken out of its environment, so neither can make this row green.
204
+ * Anything short of a live login in the file is `expired`: the row stays in the
205
+ * list with that mark (D2), and «missing» would read as «never added».
206
+ */
207
+ export declare function savedLoginStatus(id: string): ClaudeLoginStatus;
208
+ /**
209
+ * A session under this account was refused (D2): mark the row, remove nothing.
210
+ *
211
+ * By the id of the account the SESSION ran under, not «whatever is active now»
212
+ * (R14): a session started under A and refused after the machine was switched to
213
+ * B says nothing about B.
214
+ */
215
+ export declare function markLoginExpired(id: string): void;
216
+ /** The account worked again. Writes only when there was a mark to clear. */
217
+ export declare function clearLoginExpired(id: string): void;
218
+ /**
219
+ * The account the next session starts under. Absent pointer: the machine login,
220
+ * which is how a pre-#422 file reads.
221
+ *
222
+ * A pointer to a row or a home that is gone reads as the machine too, and says
223
+ * so once – sessions and the verdict then agree on the same login instead of
224
+ * one of them running under a home the other cannot see.
225
+ */
226
+ export declare function activeAccountId(): string;
227
+ /** Make a row active. `activate` leaves the usage cache alone: keys keep accounts apart (R16). */
228
+ export declare function setActiveAccount(id: string): string;
229
+ /** The account a session runs under – captured at its start and kept to its end (R14, R16). */
230
+ export interface SessionAccount {
231
+ id: string;
232
+ kind: 'machine' | 'saved';
233
+ /** `CLAUDE_CONFIG_DIR`; null for the machine login. */
234
+ home: string | null;
235
+ email?: string;
236
+ orgId?: string;
237
+ orgName?: string;
238
+ plan?: string;
239
+ }
240
+ /** Who a row is, for the verdict's `activeAccount` (§8) – from what is known, no CLI. */
241
+ export declare function readActiveAccountSummary(id: string): {
242
+ id: string;
243
+ email?: string;
244
+ orgId?: string;
245
+ };
246
+ /**
247
+ * Which home a one-shot run of the agent should use – the active account, read
248
+ * only: no link repair, no identity. For the commit-message run, which is
249
+ * Claude working as the machine's active account too (found by the independent
250
+ * check of S1: it kept running as the machine login after a switch).
251
+ */
252
+ export declare function activeAccountHome(): Pick<SessionAccount, 'kind' | 'home'>;
253
+ /**
254
+ * The active account, ready for a session to start under.
255
+ *
256
+ * A saved home has its links re-asserted first: a link the CLI replaced with a
257
+ * file since the last session is repaired before this session can write through
258
+ * the gap (it would otherwise resume nothing and share nothing).
259
+ */
260
+ export declare function resolveSessionAccount(): SessionAccount;
261
+ /**
262
+ * The environment of a process that works AS this account.
263
+ *
264
+ * For a saved account: its home, and nothing that outranks the home. The CLI
265
+ * puts `CLAUDE_CODE_OAUTH_TOKEN` above every login file, so with the operator's
266
+ * token left in, switching would be a silent no-op under a green verdict – #121
267
+ * again (К14). The API keys go for the same reason.
268
+ *
269
+ * For the machine login: the input as it was, minus a `CLAUDE_CONFIG_DIR` that
270
+ * would point it elsewhere – the pre-#422 environment.
271
+ */
272
+ export declare function withAccountHome<T extends Record<string, string | undefined>>(env: T, account: Pick<SessionAccount, 'kind' | 'home'>): T;
273
+ /**
274
+ * Who a home's `/usage` reading belongs to, read right after the probe (S1 item 5).
275
+ *
276
+ * `accountUuid` is where the CLI itself signs its figures; `organizationUuid` is
277
+ * the same key `claude auth status` calls `orgId` (checked on both homes of this
278
+ * machine, 17.09.2026). An internal file, so best-effort: nothing read, nothing
279
+ * signed.
280
+ */
281
+ export declare function usageSignature(account: Pick<SessionAccount, 'home'>): UsageSignature;
282
+ /**
283
+ * The subscription a home's own file names at this moment (S2, found by the
284
+ * independent check): a live session signs its limits with the subscription it
285
+ * STARTED under, and has to notice when somebody signed the home in to another
286
+ * one – the CLI moves the running process onto the new login by path (gotcha
287
+ * 531). Cheap enough for every frame: a `stat`, and a parse only after a write.
288
+ */
289
+ export declare function currentHomeOrgId(home: string | null): string | undefined;
290
+ /**
291
+ * The limits the CLI last measured in a home, from its own `.claude.json` (R19).
292
+ *
293
+ * Only for the line «when measured»: an internal file, rewritten by any update
294
+ * of the CLI (§11). Accepted only when it is signed by the account the same file
295
+ * says it belongs to – half-way through a re-login the two can disagree, and a
296
+ * number under the wrong name is worse than none.
297
+ */
298
+ export declare function readLastUsage(home: string | null): UsageReading | null;
299
+ /** A session's process is starting under this account. */
300
+ export declare function noteSessionAccount(sessionId: string, accountId: string): void;
301
+ /** That process is gone. */
302
+ export declare function releaseSessionAccount(sessionId: string): void;
303
+ /** Accounts some live session process is running under right now. */
304
+ export declare function liveAccountIds(): Set<string>;
305
+ /**
306
+ * Finish a swap of homes the previous daemon did not finish (R15).
307
+ *
308
+ * A replacement moves the old home to `<id>.retired-<ts>` for the length of one
309
+ * call: long enough to put it back if the new login cannot take its place. A
310
+ * daemon that died inside that call leaves the name behind. If `<id>` is there,
311
+ * the swap went through and the retired home is the replaced login – removed. If
312
+ * `<id>` is missing, the swap did not happen – the retired home is put back,
313
+ * because it is the row's only login. Run at daemon start, when nothing runs.
314
+ */
315
+ export declare function recoverInterruptedSwaps(): string[];
316
+ /** One row of the list, without a secret or a path to one (§8 `Card`). */
317
+ export interface AccountCard {
318
+ id: string;
319
+ kind: 'machine' | 'saved';
320
+ email?: string;
321
+ orgId?: string;
322
+ orgName?: string;
323
+ plan?: string;
324
+ /** Best-effort, from the home's `.claude.json` – e.g. `default_claude_max_20x`. */
325
+ tier?: string;
326
+ /** Always on a saved row; the machine login was never «added». */
327
+ addedAt?: string;
328
+ active: boolean;
329
+ login: 'ok' | 'expired' | 'unknown';
330
+ loginUntil?: string;
331
+ usage?: UsageRow[];
332
+ usageMeasuredAt?: string;
333
+ /** A saved row of the same subscription as the machine login (§2, scenarios). */
334
+ sameAsMachine?: true;
335
+ }
336
+ export interface AccountList {
337
+ accounts: AccountCard[];
338
+ active: string;
339
+ }
340
+ /**
341
+ * The list, from what is already known – no subprocess (§8 `agent_accounts`
342
+ * reads this after `listAccounts` refreshed the identities).
343
+ */
344
+ export declare function buildAccountList(homedir?: string): AccountList;
345
+ /**
346
+ * The list of Claude accounts on this machine (§8 `agent_accounts`).
347
+ *
348
+ * `probeIdentity` is the one place in the product where `claude auth status`
349
+ * runs on request (R14 a): home by home, one at a time, and only for a row whose
350
+ * known identity is older than its login file or than a few hours. Never from
351
+ * the minute poll – that reads the panel verdict, which asks no CLI.
352
+ */
353
+ export declare function listAccounts(options?: {
354
+ probeIdentity?: boolean;
355
+ }): Promise<AccountList>;
356
+ /**
357
+ * A fresh home for a sign-in that has not happened yet (§8 `target: 'saved'`).
358
+ *
359
+ * Links and config first, so the CLI that signs in here writes its identity
360
+ * into a file that already has the machine's configuration. Hidden by its name:
361
+ * the list never shows a staging home, and an abandoned one older than an hour
362
+ * is removed the next time somebody starts a sign-in.
363
+ */
364
+ export declare function prepareStagingHome(): string;
365
+ /**
366
+ * Remove every staging home, whatever its age – at daemon start, and only there.
367
+ *
368
+ * A sign-in whose code was accepted leaves the relay before its home is adopted
369
+ * (the identity is asked first, and waits in the machine-wide queue). A daemon
370
+ * that stops in that moment – «Update runner», a restart, an OOM – leaves a live
371
+ * login in a hidden home nobody lists, forgets or adopts; the hourly sweep in
372
+ * `prepareStagingHome` reaches it only when somebody next signs in (found by the
373
+ * independent check of S2). At start nothing can be adopting yet, so all of them go.
374
+ */
375
+ export declare function discardAbandonedStagingHomes(): string[];
376
+ /** Throw a sign-in away, whatever state it reached – it may hold a login nobody adopted. */
377
+ export declare function discardStagingHome(dir: string): void;
378
+ export interface AdoptResult {
379
+ account: AccountCard;
380
+ active: string;
381
+ /** The saved row whose login this sign-in replaced – same subscription (D19). */
382
+ replaced?: string;
383
+ }
384
+ /**
385
+ * A sign-in in a staging home succeeded: make it a saved account (§8
386
+ * `agent_account_login_code`).
387
+ *
388
+ * One subscription – one row (D19, R1). Identity is asked once, in the staging
389
+ * home, BEFORE anything moves (R14 b). A known `orgId` that a saved row already
390
+ * has REPLACES that row: the new login takes the row's id and `addedAt` (marks
391
+ * and a live session's reading are keyed by the id, R15) and the old login is
392
+ * deleted. An unknown `orgId` merges with nothing («card not filled»). The
393
+ * machine row takes no part: it is a different file, and a saved row of the same
394
+ * subscription is marked `sameAsMachine` instead of merged.
395
+ *
396
+ * A session running under the replaced row moves onto the new login at its next
397
+ * token refresh – the CLI finds its home by PATH, and the path now holds the new
398
+ * login. The plan (R15) asked to keep the old home renamed while such a session
399
+ * lives, «because the CLI writes its refreshed token into it»; the independent
400
+ * check of S1 read the CLI and showed it does not – a renamed home is never
401
+ * touched again, and keeping it would only keep an unused grant on disk. Both
402
+ * logins are of ONE subscription (that is what made them the same row), so the
403
+ * session keeps its account.
404
+ *
405
+ * Every move is a `rename`; the login is never in two places. A swap that cannot
406
+ * finish is undone – the old login goes back where it was. The row is recorded
407
+ * BEFORE a new home is moved into place, so a failed write never leaves a login
408
+ * nobody can list or forget. The signed-in row becomes active (R15).
409
+ */
410
+ export declare function adoptLoginResult(stagingDir: string): Promise<AdoptResult>;
411
+ /**
412
+ * Bring a home that already holds a login into the store – by rename (D17, R7).
413
+ *
414
+ * For the one login this plan names: the probe's `/root/.probe-422/login/home`.
415
+ * Refused, rather than done by copy, when the rename would cross filesystems.
416
+ * Refused for the machine home and for anything already in the store, whatever
417
+ * the spelling of the path. Refused when a saved row of the same subscription
418
+ * exists (one subscription – one row, and nothing here is allowed to delete a
419
+ * login to make room). Recorded before it moves; never activates.
420
+ */
421
+ export declare function importLoginHome(source: string): Promise<AccountCard>;
422
+ /**
423
+ * Forget a saved account: its home goes, its record goes (§8 `agent_account_forget`).
424
+ *
425
+ * The machine row cannot be forgotten. A row a live session runs under is
426
+ * refused – its process writes into that home until it ends. `rmSync` removes
427
+ * the links of the home as links: nothing in `~/.claude` is followed or touched.
428
+ * Forgetting the active row makes the machine login active.
429
+ */
430
+ export declare function forgetAccount(id: string): {
431
+ active: string;
432
+ };
433
+ //# sourceMappingURL=claude-homes.d.ts.map