@khorsheed/dsh-local-agent-claude-code 0.2.0-rc.1 → 0.2.0-rc.3

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.
@@ -1,16 +1,45 @@
1
- import type { SubprocessHandle, SubprocessSpawnSpec } from '@deepseek-ai/dsh-subprocess';
2
1
  import type { LocalAgentModelDirectory, LocalAgentModelDirectoryData } from '@khorsheed/dsh-local-agent/types';
2
+ import type { ClaudeEndpointAuth, EndpointCheckFetch } from './endpoint-auth.ts';
3
3
  type JsonObject = Record<string, unknown>;
4
- /** Initialize's native picker vocabulary, not an account entitlement claim. */
4
+ /**
5
+ * Fold the CLI's own `initialize` model vocabulary. Consumed by the LIVE
6
+ * DRIVER's runtime init handling; the catalog no longer reads this — a
7
+ * local list cannot prove anything about the configured endpoint, and
8
+ * showing it made an invalid credential look valid.
9
+ */
5
10
  export declare function claudeDirectory(result: JsonObject): LocalAgentModelDirectoryData;
6
- /** Scoped native control probe. Never syncs credentials, writes settings or submits a user turn. */
11
+ /**
12
+ * The model directory, sourced from the configured ENDPOINT (`GET
13
+ * <baseUrl>/v1/models` with the resolved credential) — never from the local
14
+ * CLI: what the picker lists is exactly what the endpoint serves, so a
15
+ * credential that does not work can never hide behind a locally-known list.
16
+ * The answers are deliberate:
17
+ *
18
+ * - no credential resolved → empty with a "save a key first" reason;
19
+ * - 401/403 → empty with the rejection (definitive — cached as data);
20
+ * - another HTTP status → empty with it (the relay serves no model route);
21
+ * - a transport failure THROWS, so the cache keeps the last good list and
22
+ * retries on schedule instead of blanking a momentarily-down endpoint.
23
+ *
24
+ * Effort levels ride the served entries' `effort` capability when the
25
+ * endpoint advertises them. Entries carry `source: 'native'` — they are the
26
+ * harness's native vocabulary, just read from the endpoint in force.
27
+ */
7
28
  export declare class ClaudeModelCatalog {
8
29
  private readonly deps;
9
30
  private readonly cache;
10
31
  constructor(deps: {
11
- spawn: (spec: SubprocessSpawnSpec) => SubprocessHandle;
12
- baseUrl?: string;
13
- cli?: string;
32
+ /** Per-probe endpoint credential resolver (the scope's file over the settings layer). */
33
+ endpoint?: (homeDir: string) => Promise<ClaudeEndpointAuth>;
34
+ /**
35
+ * The cache key's endpoint component: the settings layer's baseUrl plus a
36
+ * short credential digest, resolved per read so a card write (either
37
+ * field) re-keys the cache. A scope FILE's contribution needs no place
38
+ * here — the file itself is in the key's fingerprinted file list.
39
+ */
40
+ endpointLabel?: () => string | undefined;
41
+ /** The HTTP fetch (injected in tests). */
42
+ fetchImpl?: EndpointCheckFetch;
14
43
  timeoutMs?: number;
15
44
  ttlMs?: number;
16
45
  warn?: (message: string) => void;
@@ -1,8 +1,12 @@
1
- import { dirname, join } from 'node:path';
2
- import { StringDecoder } from 'node:string_decoder';
3
- import { ModelDirectoryCache, delegationEnv, modelDirectoryContextKey } from '@khorsheed/dsh-local-agent';
4
- import { ClaudeControlRequests } from './control-requests.js';
5
- /** Initialize's native picker vocabulary, not an account entitlement claim. */
1
+ import { join } from 'node:path';
2
+ import { ModelDirectoryCache, modelDirectoryContextKey } from '@khorsheed/dsh-local-agent';
3
+ import { fetchEndpointModels, hasEndpointCredential, OFFICIAL_BASE_URL } from './endpoint-auth.js';
4
+ /**
5
+ * Fold the CLI's own `initialize` model vocabulary. Consumed by the LIVE
6
+ * DRIVER's runtime init handling; the catalog no longer reads this — a
7
+ * local list cannot prove anything about the configured endpoint, and
8
+ * showing it made an invalid credential look valid.
9
+ */
6
10
  export function claudeDirectory(result) {
7
11
  const models = result['models'];
8
12
  if (models === undefined)
@@ -28,7 +32,27 @@ export function claudeDirectory(result) {
28
32
  }
29
33
  return { entries: [...entries.values()], complete: true, customInput: true, ...entries.has('default') ? { defaultModel: 'default' } : {} };
30
34
  }
31
- /** Scoped native control probe. Never syncs credentials, writes settings or submits a user turn. */
35
+ /** One directory answer that names why it is empty. */
36
+ function unsupported(reason) {
37
+ return { entries: [], complete: false, customInput: true, unsupported: true, reason };
38
+ }
39
+ /**
40
+ * The model directory, sourced from the configured ENDPOINT (`GET
41
+ * <baseUrl>/v1/models` with the resolved credential) — never from the local
42
+ * CLI: what the picker lists is exactly what the endpoint serves, so a
43
+ * credential that does not work can never hide behind a locally-known list.
44
+ * The answers are deliberate:
45
+ *
46
+ * - no credential resolved → empty with a "save a key first" reason;
47
+ * - 401/403 → empty with the rejection (definitive — cached as data);
48
+ * - another HTTP status → empty with it (the relay serves no model route);
49
+ * - a transport failure THROWS, so the cache keeps the last good list and
50
+ * retries on schedule instead of blanking a momentarily-down endpoint.
51
+ *
52
+ * Effort levels ride the served entries' `effort` capability when the
53
+ * endpoint advertises them. Entries carry `source: 'native'` — they are the
54
+ * harness's native vocabulary, just read from the endpoint in force.
55
+ */
32
56
  export class ClaudeModelCatalog {
33
57
  deps;
34
58
  cache;
@@ -44,75 +68,41 @@ export class ClaudeModelCatalog {
44
68
  });
45
69
  }
46
70
  key(homeDir, cwd = homeDir) {
47
- const files = [join(homeDir, 'settings.json'), join(homeDir, '.credentials.json'), join(homeDir, '.claude.json')];
48
- for (let directory = cwd;; directory = dirname(directory)) {
49
- files.push(join(directory, '.claude', 'settings.json'), join(directory, '.claude', 'settings.local.json'));
50
- if (dirname(directory) === directory)
51
- break;
52
- }
53
- return modelDirectoryContextKey({ provider: 'claude-code', homeDir, cwd, cli: [this.deps.cli ?? 'claude'], files, ...this.deps.baseUrl === undefined ? {} : { endpoint: this.deps.baseUrl } });
71
+ const files = [join(homeDir, 'settings.json'), join(homeDir, 'endpoint-auth.json')];
72
+ const endpointLabel = this.deps.endpointLabel?.();
73
+ return modelDirectoryContextKey({ provider: 'claude-code', homeDir, cwd, cli: [], files, ...endpointLabel === undefined ? {} : { endpoint: endpointLabel } });
54
74
  }
55
75
  read(homeDir, cwd) { return this.cache.read(this.key(homeDir, cwd)); }
56
76
  refresh(homeDir, cwd) { return this.cache.refresh(this.key(homeDir, cwd)); }
57
77
  follow(homeDir, cwd, signal) { return this.cache.follow(() => this.key(homeDir, cwd), signal); }
58
78
  invalidate(homeDir, cwd) { this.cache.invalidate(this.key(homeDir, cwd)); }
59
79
  dispose() { this.cache.dispose(); }
60
- async probe(homeDir, cwd, signal) {
61
- const child = this.deps.spawn({
62
- argv: [this.deps.cli ?? 'claude', '-p', '--verbose', '--input-format', 'stream-json', '--output-format', 'stream-json',
63
- '--no-session-persistence', '--settings', '{"disableAllHooks":true}', '--strict-mcp-config', '--mcp-config', '{"mcpServers":{}}', '--'],
64
- cwd, stdio: { stdin: 'pipe', stdout: 'pipe', stderr: 'pipe' }, graceMs: 2_000,
65
- env: delegationEnv({ CLAUDE_CONFIG_DIR: homeDir, ...this.deps.baseUrl === undefined ? {} : { ANTHROPIC_BASE_URL: this.deps.baseUrl } }),
66
- });
67
- const controls = new ClaudeControlRequests(frame => {
68
- if (child.stdin === undefined)
69
- throw new Error('Claude discovery stdin is unavailable');
70
- child.stdin.write(JSON.stringify(frame) + '\n');
71
- }, this.deps.timeoutMs ?? 5_000);
72
- const decoder = new StringDecoder('utf8');
73
- let buffer = '';
74
- child.stdout?.on('data', (chunk) => {
75
- buffer += decoder.write(chunk);
76
- if (buffer.length > 1_048_576) {
77
- controls.close();
78
- child.terminate();
79
- return;
80
- }
81
- let index;
82
- while ((index = buffer.indexOf('\n')) >= 0) {
83
- const line = buffer.slice(0, index);
84
- buffer = buffer.slice(index + 1);
85
- let event;
86
- try {
87
- event = JSON.parse(line);
88
- }
89
- catch {
90
- continue;
91
- }
92
- if (controls.accept(event))
93
- continue;
94
- if (event['type'] === 'control_request' && typeof event['request_id'] === 'string') {
95
- child.stdin?.write(JSON.stringify({ type: 'control_response', response: { subtype: 'error', request_id: event['request_id'], error: 'Model discovery does not execute tools' } }) + '\n');
96
- }
97
- }
98
- });
99
- child.stderr?.on('data', () => { });
100
- void child.done.then(() => controls.close(), () => controls.close());
101
- let cleanupTimer;
102
- try {
103
- return claudeDirectory(await controls.request({ subtype: 'initialize', hooks: {} }, signal));
80
+ async probe(homeDir, _cwd, signal) {
81
+ const endpoint = await this.deps.endpoint?.(homeDir);
82
+ if (!hasEndpointCredential(endpoint)) {
83
+ return unsupported('No endpoint credential configured — the model list loads from the endpoint once an API Key or Auth Token is saved');
104
84
  }
105
- finally {
106
- controls.close();
107
- child.stdin?.end();
108
- await Promise.race([child.done.catch(() => { }), new Promise(resolve => {
109
- cleanupTimer = setTimeout(resolve, 1_000);
110
- cleanupTimer.unref();
111
- })]);
112
- if (cleanupTimer !== undefined)
113
- clearTimeout(cleanupTimer);
114
- child.terminate();
115
- await child.waitForExit().catch(() => { });
85
+ const result = await fetchEndpointModels({ ...endpoint, baseUrl: endpoint.baseUrl ?? OFFICIAL_BASE_URL }, this.deps.fetchImpl, AbortSignal.any([signal, AbortSignal.timeout(this.deps.timeoutMs ?? 20_000)]));
86
+ if (result.kind === 'unreachable') {
87
+ // Not a verdict: the cache keeps the last good list and retries.
88
+ throw new Error(`endpoint unreachable: ${result.message}`);
89
+ }
90
+ if (result.kind === 'malformed')
91
+ return unsupported('The endpoint answered but the model list did not parse (not /v1/models-shaped)');
92
+ if (result.kind === 'http') {
93
+ if (result.status === 401 || result.status === 403) {
94
+ return unsupported(`The endpoint rejected the credential (HTTP ${String(result.status)}) — the list loads once a valid key is saved`);
95
+ }
96
+ return unsupported(`The endpoint does not serve a model list (HTTP ${String(result.status)})`);
116
97
  }
98
+ const entries = result.models.map(model => ({
99
+ value: model.id,
100
+ label: model.displayName ?? model.id,
101
+ source: 'native',
102
+ ...model.efforts === undefined ? {} : { reasoning: { options: model.efforts.map(effort => ({ value: effort, label: effort })) } },
103
+ }));
104
+ if (entries.length === 0)
105
+ return unsupported('The endpoint serves no models');
106
+ return { entries, complete: true, customInput: true };
117
107
  }
118
108
  }
@@ -1,29 +1,47 @@
1
1
  /**
2
- * Claude Code scoped-home provisioning. Unlike codex (whose credential store
3
- * must be pinned to a file) and kimi (which needs a provider config),
4
- * claude's `CLAUDE_CONFIG_DIR` scoped home needs nothing beyond the
5
- * directory itself: login via `claude auth login` writes `.claude.json`
6
- * (with the `oauthAccount` record) into it, and the real credential lands in
7
- * the macOS keychain under a hashed entry or — on Linux, despite upstream
8
- * bug #47661 — in the default `~/.claude/.credentials.json` while the config
9
- * stays scoped. Sign-out removes the scoped config so `status` reports not
10
- * authenticated; the keychain entry on macOS persists until the CLI removes
11
- * it, which the framework's file-based logout cannot reach.
2
+ * Claude Code scoped-home provisioning. The harness authenticates by
3
+ * endpoint key (see endpoint-auth.ts), so a scoped home needs nothing beyond
4
+ * the directory itself: `claude -p` scaffolds `.claude.json` on its first
5
+ * run, and the endpoint credential arrives per round through the spawn env
6
+ * (resolved from the scope's endpoint-auth.json over the plugin config).
7
+ * When `proxyUrl` is configured, the scoped `settings.json` gains an env
8
+ * block pointing the CLI's own traffic at it — claude reads proxy settings
9
+ * from this file, so the child works even when the host process environment
10
+ * carries no proxy (a supervisor-spawned instance's env is not the user's
11
+ * shell env).
12
12
  * @module @khorsheed/dsh-local-agent-claude-code/provision
13
13
  */
14
14
  /**
15
15
  * Provision the scoped home directory. Claude creates it lazily on first
16
16
  * use, but creating it eagerly keeps the harness's homeDir contract uniform
17
- * with the other harnesses. When `proxyUrl` is configured, the scoped
18
- * `settings.json` gains an env block pointing the CLI's own traffic at it —
19
- * claude reads proxy settings from this file, so the child works even when
20
- * the host process environment carries no proxy (a supervisor-spawned
21
- * instance's env is not the user's shell env). An existing settings file is
22
- * merged, not overwritten; only the two proxy keys are managed.
17
+ * with the other harnesses. Two maintenance duties run here:
18
+ *
19
+ * - Legacy OAuth sweep: the endpoint-key era never reads `.credentials.json`
20
+ * or the `.claude.json` `oauthAccount` marker — both are deleted so a
21
+ * pre-migration scoped home carries no dead grant (measured: claude
22
+ * 2.1.296 prefers the env credential over a stored one anyway; the sweep
23
+ * is hygiene, not priority repair). Other `.claude.json` keys survive.
24
+ * - When `proxyUrl` is configured, the scoped `settings.json` gains an env
25
+ * block pointing the CLI's own traffic at it — claude reads proxy settings
26
+ * from this file, so the child works even when the host process environment
27
+ * carries no proxy (a supervisor-spawned instance's env is not the user's
28
+ * shell env). An existing settings file is merged, not overwritten; only
29
+ * the two proxy keys are managed.
23
30
  * @param homeDir - the `claude-code` harness's scoped home.
24
31
  * @param proxyUrl - optional HTTP proxy URL for the child CLI's traffic.
25
32
  */
26
33
  export declare function provisionClaudeHome(homeDir: string, proxyUrl?: string): Promise<void>;
34
+ /**
35
+ * Remove the OAuth era's artifacts from the scoped home: the
36
+ * `.credentials.json` grant file outright, and the `oauthAccount` marker key
37
+ * from `.claude.json` (the file itself carries live CLI state and stays).
38
+ * Idempotent and best-effort per file — a missing or malformed file is
39
+ * simply not a sweep target. The macOS keychain entry is deliberately left
40
+ * alone: the CLI never consults it once an env credential rides the spawn,
41
+ * and keychain writes are outside the harness's file-based contract.
42
+ * @param homeDir - the `claude-code` harness's scoped home.
43
+ */
44
+ export declare function sweepLegacyOauth(homeDir: string): Promise<void>;
27
45
  /**
28
46
  * Read the scoped settings' configured model: the `model` key of
29
47
  * `<homeDir>/settings.json` — the model a delegation round pins when the
@@ -49,15 +67,4 @@ export declare function readClaudeConfiguredEffort(homeDir: string): Promise<str
49
67
  * @param model - the model to scratch in, or undefined to remove the key.
50
68
  */
51
69
  export declare function writeClaudeScopedModel(homeDir: string, model: string | undefined): Promise<void>;
52
- /**
53
- * Sign out of the scoped account: remove the scoped config file, so
54
- * `/<name> status` reports not authenticated and the next login authorizes
55
- * a fresh account. The macOS keychain entry (a hashed
56
- * `Claude Code-credentials-<sha256(path)[:8]>` record keyed to the scoped
57
- * home path) is NOT removed — the CLI's `claude auth logout` owns that, and
58
- * the harness logout contract is file-based; the stale entry is harmless
59
- * because a fresh login rewrites the same hashed slot.
60
- * @param homeDir - the `claude-code` harness's scoped home.
61
- */
62
- export declare function claudeLogout(homeDir: string): Promise<void>;
63
70
  //# sourceMappingURL=provision.d.ts.map
@@ -1,36 +1,42 @@
1
1
  /**
2
- * Claude Code scoped-home provisioning. Unlike codex (whose credential store
3
- * must be pinned to a file) and kimi (which needs a provider config),
4
- * claude's `CLAUDE_CONFIG_DIR` scoped home needs nothing beyond the
5
- * directory itself: login via `claude auth login` writes `.claude.json`
6
- * (with the `oauthAccount` record) into it, and the real credential lands in
7
- * the macOS keychain under a hashed entry or — on Linux, despite upstream
8
- * bug #47661 — in the default `~/.claude/.credentials.json` while the config
9
- * stays scoped. Sign-out removes the scoped config so `status` reports not
10
- * authenticated; the keychain entry on macOS persists until the CLI removes
11
- * it, which the framework's file-based logout cannot reach.
2
+ * Claude Code scoped-home provisioning. The harness authenticates by
3
+ * endpoint key (see endpoint-auth.ts), so a scoped home needs nothing beyond
4
+ * the directory itself: `claude -p` scaffolds `.claude.json` on its first
5
+ * run, and the endpoint credential arrives per round through the spawn env
6
+ * (resolved from the scope's endpoint-auth.json over the plugin config).
7
+ * When `proxyUrl` is configured, the scoped `settings.json` gains an env
8
+ * block pointing the CLI's own traffic at it — claude reads proxy settings
9
+ * from this file, so the child works even when the host process environment
10
+ * carries no proxy (a supervisor-spawned instance's env is not the user's
11
+ * shell env).
12
12
  * @module @khorsheed/dsh-local-agent-claude-code/provision
13
13
  */
14
14
  import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
15
15
  import { join } from 'node:path';
16
- /** The scoped config file holding the oauthAccount auth marker. */
17
- const SCOPED_CONFIG = '.claude.json';
18
16
  /** The scoped settings file carrying the CLI's env block. */
19
17
  const SCOPED_SETTINGS = 'settings.json';
20
18
  /**
21
19
  * Provision the scoped home directory. Claude creates it lazily on first
22
20
  * use, but creating it eagerly keeps the harness's homeDir contract uniform
23
- * with the other harnesses. When `proxyUrl` is configured, the scoped
24
- * `settings.json` gains an env block pointing the CLI's own traffic at it —
25
- * claude reads proxy settings from this file, so the child works even when
26
- * the host process environment carries no proxy (a supervisor-spawned
27
- * instance's env is not the user's shell env). An existing settings file is
28
- * merged, not overwritten; only the two proxy keys are managed.
21
+ * with the other harnesses. Two maintenance duties run here:
22
+ *
23
+ * - Legacy OAuth sweep: the endpoint-key era never reads `.credentials.json`
24
+ * or the `.claude.json` `oauthAccount` marker — both are deleted so a
25
+ * pre-migration scoped home carries no dead grant (measured: claude
26
+ * 2.1.296 prefers the env credential over a stored one anyway; the sweep
27
+ * is hygiene, not priority repair). Other `.claude.json` keys survive.
28
+ * - When `proxyUrl` is configured, the scoped `settings.json` gains an env
29
+ * block pointing the CLI's own traffic at it — claude reads proxy settings
30
+ * from this file, so the child works even when the host process environment
31
+ * carries no proxy (a supervisor-spawned instance's env is not the user's
32
+ * shell env). An existing settings file is merged, not overwritten; only
33
+ * the two proxy keys are managed.
29
34
  * @param homeDir - the `claude-code` harness's scoped home.
30
35
  * @param proxyUrl - optional HTTP proxy URL for the child CLI's traffic.
31
36
  */
32
37
  export async function provisionClaudeHome(homeDir, proxyUrl) {
33
38
  await mkdir(homeDir, { recursive: true });
39
+ await sweepLegacyOauth(homeDir);
34
40
  if (proxyUrl === undefined)
35
41
  return;
36
42
  const path = join(homeDir, SCOPED_SETTINGS);
@@ -49,6 +55,31 @@ export async function provisionClaudeHome(homeDir, proxyUrl) {
49
55
  settings.env = env;
50
56
  await writeFile(path, JSON.stringify(settings, undefined, 2) + '\n');
51
57
  }
58
+ /**
59
+ * Remove the OAuth era's artifacts from the scoped home: the
60
+ * `.credentials.json` grant file outright, and the `oauthAccount` marker key
61
+ * from `.claude.json` (the file itself carries live CLI state and stays).
62
+ * Idempotent and best-effort per file — a missing or malformed file is
63
+ * simply not a sweep target. The macOS keychain entry is deliberately left
64
+ * alone: the CLI never consults it once an env credential rides the spawn,
65
+ * and keychain writes are outside the harness's file-based contract.
66
+ * @param homeDir - the `claude-code` harness's scoped home.
67
+ */
68
+ export async function sweepLegacyOauth(homeDir) {
69
+ await rm(join(homeDir, '.credentials.json'), { force: true });
70
+ const configPath = join(homeDir, '.claude.json');
71
+ let config;
72
+ try {
73
+ config = JSON.parse(await readFile(configPath, 'utf8'));
74
+ }
75
+ catch {
76
+ return;
77
+ }
78
+ if (typeof config !== 'object' || config === null || !('oauthAccount' in config))
79
+ return;
80
+ delete config['oauthAccount'];
81
+ await writeFile(configPath, JSON.stringify(config, undefined, 2) + '\n');
82
+ }
52
83
  /**
53
84
  * Read the scoped settings' configured model: the `model` key of
54
85
  * `<homeDir>/settings.json` — the model a delegation round pins when the
@@ -117,16 +148,3 @@ export async function writeClaudeScopedModel(homeDir, model) {
117
148
  }
118
149
  await writeFile(path, JSON.stringify(settings, undefined, 2) + '\n');
119
150
  }
120
- /**
121
- * Sign out of the scoped account: remove the scoped config file, so
122
- * `/<name> status` reports not authenticated and the next login authorizes
123
- * a fresh account. The macOS keychain entry (a hashed
124
- * `Claude Code-credentials-<sha256(path)[:8]>` record keyed to the scoped
125
- * home path) is NOT removed — the CLI's `claude auth logout` owns that, and
126
- * the harness logout contract is file-based; the stale entry is harmless
127
- * because a fresh login rewrites the same hashed slot.
128
- * @param homeDir - the `claude-code` harness's scoped home.
129
- */
130
- export async function claudeLogout(homeDir) {
131
- await rm(join(homeDir, SCOPED_CONFIG), { force: true });
132
- }
@@ -11,71 +11,13 @@
11
11
  * timestamp the listing needs; the rest of the file (conversation) is not
12
12
  * parsed. Files are append-only with torn-tail tolerance, and contents are
13
13
  * always untrusted input.
14
+ *
15
+ * Credentials are NOT this module's business: the harness authenticates by
16
+ * endpoint key only (see endpoint-auth.ts), which needs no keychain read,
17
+ * no `.credentials.json` reconcile, and no OAuth marker probe.
14
18
  * @module @khorsheed/dsh-local-agent-claude-code/records
15
19
  */
16
- import { execFile } from 'node:child_process';
17
20
  import type { LocalAgentSessionRecord } from '@khorsheed/dsh-local-agent';
18
- /** Test-swappable process exec (the keychain read goes through `security`). */
19
- export declare const internals: {
20
- exec: typeof execFile.__promisify__;
21
- };
22
- /**
23
- * The scoped `.claude.json`'s modification stamp (epoch ms), undefined when
24
- * absent. A completed login rewrites this file, so the stamp distinguishes a
25
- * fresh login from a leftover marker (a revoked token's record lingers).
26
- * @param homeDir - the `claude-code` harness's scoped home.
27
- * @returns the marker's mtime, or undefined when no scoped config exists.
28
- */
29
- export declare function claudeCredentialStamp(homeDir: string): Promise<number | undefined>;
30
- /**
31
- * Reconcile the keychain credential with `<homeDir>/.credentials.json`,
32
- * NEWER WINS. Claude 2.1.236 on macOS WRITES the scoped login to the hashed
33
- * keychain entry but READS the credentials file at runtime (the same
34
- * write/read split as the Linux #47661 bug) — a login that lands only in the
35
- * keychain still answers "Not logged in", so the file has to be kept current.
36
- *
37
- * The file is not this process's private mirror, though: a containerized
38
- * round bind-mounts this very directory read-write (the unit has no keychain,
39
- * so the CLI inside it reads and writes the file), and the pre-spawn sync runs
40
- * against the HOST path moments before the unit reads it through the mount.
41
- * An unconditional mirror therefore makes this function the one participant
42
- * that can move the credential chain BACKWARD: it hands the next run a refresh
43
- * token the other writer already spent, the endpoint rejects it, and the CLI
44
- * answers by CLEARING the file — which logs the instance out, because that
45
- * file is the instance's credential. So the keychain blob is written only when
46
- * it is not older than the file's. Undefined expiry on either side keeps the
47
- * pre-newer-wins behavior (the keychain writes), and a file that is not USABLE
48
- * never wins.
49
- *
50
- * Only a USABLE blob (non-empty token, see `readKeychainCredential`) is
51
- * mirrored; with none in the keychain, a file holding an empty-token shell —
52
- * debris from an earlier first-match read, or a cleared credential — is
53
- * REMOVED, so the runtime says "Not logged in" instead of the misleading
54
- * "OAuth session expired". Idempotent and content-compare before write.
55
- * @param homeDir - the `claude-code` harness's scoped home.
56
- * @param log - warn channel for the two decisions that used to be silent.
57
- * @returns true when a usable credential is in the file after the call.
58
- */
59
- export declare function syncClaudeCredentialFile(homeDir: string, log?: (message: string) => void): Promise<boolean>;
60
- /**
61
- * Whether the scoped home holds usable Claude Code credentials: the scoped
62
- * `.claude.json` carries an `oauthAccount` record once a device-code login
63
- * has completed, AND the keychain credential is not past its stored expiry
64
- * (when the expiry is readable — a server-side REVOCATION is not visible
65
- * locally; providers report those through `localAgent.reportAuthFailure`).
66
- * The real credential lives in the macOS keychain under a hashed entry
67
- * (`Claude Code-credentials-<sha256(path)[:8]>`, hashed from the config-dir
68
- * path) or in the default `~/.claude/.credentials.json` on Linux (upstream
69
- * bug #47661: `CLAUDE_CONFIG_DIR` does not isolate the credentials file
70
- * there — it writes the scoped dir but READS the default home, so Linux
71
- * credentials may exist even when the scoped home has none). Spawning the
72
- * CLI to probe auth is avoided; the platform differences are documented in
73
- * the README.
74
- * @param homeDir - the `claude-code` harness's scoped home.
75
- * @param log - warn channel handed to the credential reconcile below.
76
- * @returns true when a completed login is recorded and not locally expired.
77
- */
78
- export declare function claudeAuthenticated(homeDir: string, log?: (message: string) => void): Promise<boolean>;
79
21
  /**
80
22
  * Parse the head of a session file for the listing fields. The first `user`
81
23
  * event carries `sessionId`, `cwd`, and `timestamp`; its `cwd` is the only