@prohost/cli 0.9.0 → 0.10.1

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,344 @@
1
+ /**
2
+ * `prohost usage …` — observer mode: report this machine's Claude Code and
3
+ * Codex capacity and token totals to ProhostAI without pairing an agent.
4
+ *
5
+ * Flag plumbing only; the work lives in `discovery.ts`, `quota.ts`,
6
+ * `scanner.ts` and `service.ts`.
7
+ */
8
+ import os from 'node:os';
9
+ import path from 'node:path';
10
+ import process from 'node:process';
11
+ import { existsSync } from 'node:fs';
12
+ import { fileURLToPath } from 'node:url';
13
+ import { DEFAULT_ACCOUNT_LABEL, loadAccounts, runtimeName } from '../agent/accounts.js';
14
+ import { baseUrlProblem } from '../agent/base-url.js';
15
+ import { daemonEnvironment } from '../agent/command.js';
16
+ import { maskSecret, prohostHome } from '../agent/credentials.js';
17
+ import { buildSystemdUnit, installLaunchdDaemon, launchdLabel, launchdPlistPath, uninstallLaunchdDaemon, } from '../agent/daemon.js';
18
+ import { acquireRunLock } from '../agent/lock.js';
19
+ import { ensurePrivateDir, resolveUsageKey, saveUsageCredentials, usageStateDir } from './config.js';
20
+ import { accountConfigDir, discoverAccounts } from './discovery.js';
21
+ import { PipedKeyTooLongError, promptHiddenKey, readPipedKey } from './key-input.js';
22
+ import { RETENTION_DAYS, dayKey, loadScanState, localTotals, pendingDays } from './scanner.js';
23
+ import { UsageService, loadStatus, runUsageLoop, scanStatePath } from './service.js';
24
+ const defaultIO = {
25
+ out: (line) => process.stdout.write(`${line}\n`),
26
+ err: (line) => process.stderr.write(`${line}\n`),
27
+ };
28
+ /** launchd label prefix for the usage reporter — distinct from the agent daemon's. */
29
+ export const USAGE_LAUNCHD_PREFIX = 'ai.prohost.usage';
30
+ /** Label for the usage daemon serving one `$PROHOST_HOME`. Same digest as the agent's, other prefix. */
31
+ export function usageLaunchdLabel(home) {
32
+ const digest = launchdLabel(home).split('.').pop();
33
+ return `${USAGE_LAUNCHD_PREFIX}.${digest}`;
34
+ }
35
+ export function usageLogPath(env = process.env) {
36
+ return path.join(prohostHome(env), 'usage.log');
37
+ }
38
+ /** `prohost usage start` argv for a supervisor. */
39
+ export function usageDaemonArguments(options) {
40
+ return [options.execPath, options.scriptPath, 'usage', 'start', ...(options.machine ? ['--machine', options.machine] : [])];
41
+ }
42
+ function flagString(flags, name) {
43
+ const value = flags[name];
44
+ return typeof value === 'string' ? value : undefined;
45
+ }
46
+ function tildify(dir, env) {
47
+ const home = env.HOME || os.homedir();
48
+ return dir === home || dir.startsWith(home + path.sep) ? `~${dir.slice(home.length)}` : dir;
49
+ }
50
+ /** Dispatch a `usage` subcommand. Returns the exit code. */
51
+ export async function runUsage(subcommand, flags, printHelp, io = defaultIO) {
52
+ if (subcommand === 'connect')
53
+ return connectCommand(flags, io);
54
+ if (subcommand === 'discover')
55
+ return discoverCommand(flags, io);
56
+ if (subcommand === 'start')
57
+ return startCommand(flags, io);
58
+ if (subcommand === 'status')
59
+ return statusCommand(io);
60
+ if (subcommand === 'install-daemon')
61
+ return installCommand(flags, io);
62
+ if (subcommand === 'uninstall-daemon')
63
+ return uninstallCommand(io);
64
+ io.err(subcommand
65
+ ? `Unknown usage subcommand: ${subcommand}\n`
66
+ : 'Missing usage subcommand — expected `connect`, `discover`, `start`, `status`, `install-daemon` or `uninstall-daemon`.\n');
67
+ printHelp();
68
+ return 1;
69
+ }
70
+ export function renderDiscovery(result, env, dryRun) {
71
+ const lines = [];
72
+ const total = result.added.length + result.known.length + result.skipped.length;
73
+ if (total === 0) {
74
+ return [
75
+ 'No Claude Code or Codex logins found (looked for ~/.claude*, ~/.codex*, $CLAUDE_CONFIG_DIR and $CODEX_HOME).',
76
+ ];
77
+ }
78
+ lines.push(`Found ${total} login${total === 1 ? '' : 's'}:`);
79
+ const row = (mark, label, runtime, dir, note) => ` ${mark} ${label.padEnd(16)} ${runtimeName(runtime).padEnd(12)} ${tildify(dir, env)}${note ? ` (${note})` : ''}`;
80
+ for (const a of result.added)
81
+ lines.push(row('+', a.label, a.runtime, a.dir, dryRun ? 'would add' : 'added'));
82
+ for (const k of result.known)
83
+ lines.push(row('=', k.label, k.runtime, k.dir, 'already registered'));
84
+ for (const s of result.skipped)
85
+ lines.push(row('!', s.label, s.runtime, s.dir, `skipped: ${s.reason}`));
86
+ return lines;
87
+ }
88
+ /**
89
+ * The key `connect` should store, in order: `--key -` (stdin, or a hidden
90
+ * prompt at a terminal), a literal `--key <value>` (warned about), then
91
+ * `$PROHOST_API_KEY`, then piped stdin, then — at a terminal, with no
92
+ * credential to fall back on — a hidden prompt. `undefined` means "none given";
93
+ * `null` means a usage error that has already been printed.
94
+ */
95
+ async function connectKey(flags, io, env) {
96
+ const stdin = io.stdin ?? process.stdin;
97
+ const prompt = () => promptHiddenKey(stdin, (text) => process.stderr.write(text));
98
+ const literal = flagString(flags, 'key');
99
+ if (literal === '-') {
100
+ const key = stdin.isTTY ? await prompt() : await readPipedKey(stdin);
101
+ if (!key)
102
+ io.err('✗ --key - got no key. Pipe one in (pbpaste | prohost usage connect --key -) or type it at the prompt.');
103
+ return key ?? null;
104
+ }
105
+ if (literal) {
106
+ io.err('! --key <value> leaves the key in your shell history and the process list. ' +
107
+ 'Next time pipe it instead: pbpaste | prohost usage connect --key -');
108
+ return literal.trim();
109
+ }
110
+ if (env.PROHOST_API_KEY)
111
+ return env.PROHOST_API_KEY.trim();
112
+ if (!stdin.isTTY)
113
+ return readPipedKey(stdin);
114
+ // At a terminal: prompt only when there is nothing to fall back on, so a
115
+ // connected machine (or one with a paired agent) is not asked again.
116
+ if (resolveUsageKey(env))
117
+ return undefined;
118
+ return prompt();
119
+ }
120
+ async function connectCommand(flags, io) {
121
+ const env = io.env ?? process.env;
122
+ if (flags.key === true) {
123
+ io.err('--key needs a value. Use --key - to read it from stdin (pbpaste | prohost usage connect --key -), ' +
124
+ 'or leave --key off to be prompted. Create a key with the usage:write scope in ProhostAI → Settings → API keys.');
125
+ return 1;
126
+ }
127
+ if (flags['base-url'] === true) {
128
+ io.err('--base-url needs a value, e.g. --base-url https://connect.prohost.ai');
129
+ return 1;
130
+ }
131
+ // Checked before any key is read or stored: the key goes to this URL.
132
+ const baseUrl = flagString(flags, 'base-url');
133
+ const problem = baseUrl ? baseUrlProblem(baseUrl) : undefined;
134
+ if (problem) {
135
+ io.err(`✗ --base-url rejected: ${problem}`);
136
+ return 1;
137
+ }
138
+ let key;
139
+ try {
140
+ key = await connectKey(flags, io, env);
141
+ }
142
+ catch (err) {
143
+ if (!(err instanceof PipedKeyTooLongError))
144
+ throw err;
145
+ io.err('✗ The piped input is not an API key (no line ended within 4096 characters). Nothing was saved.');
146
+ return 1;
147
+ }
148
+ if (key === null)
149
+ return 1;
150
+ if (key) {
151
+ const file = saveUsageCredentials({ apiKey: key, baseUrl }, env);
152
+ io.out(`✓ Saved key ${maskSecret(key)} to ${file} (owner-only).`);
153
+ }
154
+ else {
155
+ const existing = resolveUsageKey(env);
156
+ if (!existing) {
157
+ io.err('✗ No API key. Run prohost usage connect at a terminal to be prompted, pipe one in with --key -, ' +
158
+ 'or set $PROHOST_API_KEY (scope usage:write). ' +
159
+ 'A machine with a paired agent can skip this — its credential is used.');
160
+ return 1;
161
+ }
162
+ io.out(existing.source === 'agent'
163
+ ? `✓ Using this machine's paired agent credential (${maskSecret(existing.apiKey)}).`
164
+ : `✓ Already connected (${maskSecret(existing.apiKey)}).`);
165
+ }
166
+ io.out('');
167
+ for (const line of renderDiscovery(discoverAccounts({}, env), env, false))
168
+ io.out(line);
169
+ io.out('');
170
+ io.out('Report now and every 5 minutes: prohost usage start (or keep it running: prohost usage install-daemon)');
171
+ return 0;
172
+ }
173
+ /**
174
+ * The stored key, when it can be sent: present, and its base URL is https://
175
+ * (or loopback http://). Prints why not and returns `null` otherwise.
176
+ */
177
+ function usableKey(env, io) {
178
+ const key = resolveUsageKey(env);
179
+ if (!key) {
180
+ io.err('✗ Not connected. Run: prohost usage connect');
181
+ return null;
182
+ }
183
+ const problem = baseUrlProblem(key.baseUrl);
184
+ if (problem) {
185
+ io.err(`✗ Refusing to send the API key: the stored API base URL is unusable. ${problem} ` +
186
+ (key.source === 'agent'
187
+ ? 'Re-pair the agent with an https:// --base-url.'
188
+ : 'Reconnect: prohost usage connect --key - --base-url https://…'));
189
+ return null;
190
+ }
191
+ return key;
192
+ }
193
+ function discoverCommand(flags, io) {
194
+ const env = io.env ?? process.env;
195
+ const dryRun = flags['dry-run'] === true;
196
+ for (const line of renderDiscovery(discoverAccounts({ dryRun }, env), env, dryRun))
197
+ io.out(line);
198
+ return 0;
199
+ }
200
+ async function startCommand(flags, io) {
201
+ const env = io.env ?? process.env;
202
+ if (!usableKey(env, io))
203
+ return 1;
204
+ // Its own lock, under $PROHOST_HOME/usage — `agent run` holds the home's.
205
+ const stateDir = ensurePrivateDir(usageStateDir(env));
206
+ const lock = acquireRunLock({ ...env, PROHOST_HOME: stateDir });
207
+ if (!lock.ok) {
208
+ io.err(`✗ Another \`prohost usage start\` (pid ${lock.holder.pid}) is already reporting for ${prohostHome(env)}. ` +
209
+ 'Stop it first — or, if it is the daemon, use `prohost usage uninstall-daemon`.');
210
+ return 1;
211
+ }
212
+ const controller = new AbortController();
213
+ const onSig = () => controller.abort();
214
+ process.on('SIGINT', onSig);
215
+ process.on('SIGTERM', onSig);
216
+ try {
217
+ // New logins since the last start are picked up without a separate step.
218
+ const found = discoverAccounts({}, env);
219
+ for (const a of found.added)
220
+ io.out(`+ Registered ${runtimeName(a.runtime)} login "${a.label}" (${tildify(a.dir, env)})`);
221
+ const service = new UsageService({ env, machine: flagString(flags, 'machine') });
222
+ if (flags.once === true) {
223
+ const result = await service.tick();
224
+ io.out(`Report ${result.reported ? 'sent' : 'not sent'}; ${result.uploadedRows} token row(s) uploaded.`);
225
+ return 0;
226
+ }
227
+ io.out(`Reporting ${loadAccounts(env).length} login(s) every 5 minutes. Ctrl-C to stop.`);
228
+ await runUsageLoop(service, { signal: controller.signal });
229
+ return 0;
230
+ }
231
+ finally {
232
+ process.off('SIGINT', onSig);
233
+ process.off('SIGTERM', onSig);
234
+ lock.release();
235
+ }
236
+ }
237
+ function cliEntrypoint() {
238
+ return path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'index.js');
239
+ }
240
+ function installCommand(flags, io) {
241
+ const env = io.env ?? process.env;
242
+ if (!usableKey(env, io))
243
+ return 1;
244
+ const workdir = ensurePrivateDir(usageStateDir(env));
245
+ const spec = {
246
+ label: usageLaunchdLabel(prohostHome(env)),
247
+ programArguments: usageDaemonArguments({
248
+ execPath: process.execPath,
249
+ scriptPath: cliEntrypoint(),
250
+ machine: flagString(flags, 'machine'),
251
+ }),
252
+ workdir,
253
+ logPath: usageLogPath(env),
254
+ env: daemonEnvironment(),
255
+ };
256
+ if (os.platform() !== 'darwin') {
257
+ io.out(`Automatic install is macOS-only. On Linux, save this as ~/.config/systemd/user/${spec.label}.service and run:\n` +
258
+ ` systemctl --user daemon-reload && systemctl --user enable --now ${spec.label}\n` +
259
+ ' loginctl enable-linger "$USER" # so it survives logout\n\n' +
260
+ buildSystemdUnit(spec).replace('ProhostAI paired agent', 'ProhostAI usage reporter'));
261
+ return 0;
262
+ }
263
+ const result = installLaunchdDaemon(spec);
264
+ if (!result.ok) {
265
+ io.err(result.message);
266
+ return 1;
267
+ }
268
+ io.out(`✓ Installed ${spec.label}`);
269
+ io.out(` plist: ${result.plistPath}`);
270
+ io.out(` logs: ${spec.logPath}`);
271
+ io.out(' Usage is reported every 5 minutes from login on, and the reporter restarts if it exits.');
272
+ io.out(' Stop it with: prohost usage uninstall-daemon');
273
+ return 0;
274
+ }
275
+ function uninstallCommand(io) {
276
+ const env = io.env ?? process.env;
277
+ const label = usageLaunchdLabel(prohostHome(env));
278
+ if (os.platform() !== 'darwin') {
279
+ io.out(`Automatic removal is macOS-only. On Linux:\n systemctl --user disable --now ${label}`);
280
+ return 0;
281
+ }
282
+ const existed = existsSync(launchdPlistPath(label));
283
+ uninstallLaunchdDaemon(label);
284
+ io.out(existed ? `✓ Removed ${label}` : `No usage reporter was installed for ${prohostHome(env)}. Nothing to remove.`);
285
+ return 0;
286
+ }
287
+ /** `1234567` → `1.2M`. */
288
+ export function compactTokens(n) {
289
+ if (n >= 1e9)
290
+ return `${(n / 1e9).toFixed(1)}B`;
291
+ if (n >= 1e6)
292
+ return `${(n / 1e6).toFixed(1)}M`;
293
+ if (n >= 1e3)
294
+ return `${(n / 1e3).toFixed(1)}k`;
295
+ return String(n);
296
+ }
297
+ function sum(t) {
298
+ return t ? t[0] + t[1] + t[2] + t[3] : 0;
299
+ }
300
+ function describeCall(call) {
301
+ if (!call)
302
+ return 'never';
303
+ const outcome = call.ok ? '✓' : `✗ ${call.status === 0 ? 'no response' : `HTTP ${call.status}`}`;
304
+ return `${call.at} ${outcome}${call.count !== undefined ? ` (${call.count})` : ''}`;
305
+ }
306
+ function statusCommand(io) {
307
+ const env = io.env ?? process.env;
308
+ const key = resolveUsageKey(env);
309
+ io.out(key
310
+ ? `Connected ${maskSecret(key.apiKey)} → ${key.baseUrl}${key.source === 'agent' ? ' (paired agent credential)' : ''}` +
311
+ `${baseUrlProblem(key.baseUrl) ? ' (not https:// — nothing is sent)' : ''}`
312
+ : 'Connected no — run: prohost usage connect');
313
+ const plist = launchdPlistPath(usageLaunchdLabel(prohostHome(env)));
314
+ io.out(`Daemon ${os.platform() === 'darwin' ? (existsSync(plist) ? `installed (${tildify(plist, env)})` : 'not installed') : 'see systemd'}`);
315
+ const status = loadStatus(env);
316
+ io.out(`Last tick ${status.lastTickAt ?? 'never'}`);
317
+ io.out(`Report ${describeCall(status.lastReport)}`);
318
+ io.out(`Totals ${describeCall(status.lastTotals)}`);
319
+ const scan = loadScanState(scanStatePath(env));
320
+ if (scan.dirty.length > 0)
321
+ io.out(`Pending ${pendingDays(scan).length} day(s) not yet uploaded`);
322
+ const accounts = loadAccounts(env);
323
+ io.out('');
324
+ if (accounts.length === 0) {
325
+ io.out('No logins registered. Run: prohost usage discover');
326
+ return 0;
327
+ }
328
+ const now = Date.now();
329
+ const week = localTotals(scan, dayKey(now - 6 * 86_400_000));
330
+ const month = localTotals(scan, dayKey(now - (RETENTION_DAYS - 1) * 86_400_000));
331
+ io.out('Logins (tokens: last 7 days / 30 days, from local logs)');
332
+ for (const record of accounts) {
333
+ const seen = status.accounts[record.key];
334
+ const auth = seen?.authState === 'ok' ? 'signed in' : seen?.authState === 'expired' ? 'signed out' : 'unknown';
335
+ const windows = (seen?.windows ?? [])
336
+ .map((w) => `${w.kind === 'weekly_model' ? `weekly ${w.model_label ?? ''}`.trim() : w.kind} ${w.used_percent}%`)
337
+ .join(', ');
338
+ io.out(` ${record.label.padEnd(16)} ${runtimeName(record.runtime).padEnd(12)} ${auth.padEnd(10)} ` +
339
+ `${compactTokens(sum(week.get(record.key)))} / ${compactTokens(sum(month.get(record.key)))}` +
340
+ `${windows ? ` · ${windows}` : ''}` +
341
+ `${record.label === DEFAULT_ACCOUNT_LABEL ? '' : ` · ${tildify(accountConfigDir(record, env), env)}`}`);
342
+ }
343
+ return 0;
344
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Where `prohost usage` keeps its own state, and which API key it reports with.
3
+ *
4
+ * Observer mode needs no paired agent: `prohost usage connect --key …` stores
5
+ * a key (scope `usage:write`) in `$PROHOST_HOME/usage.json`, written `0600`
6
+ * inside a `0700` directory exactly like `agent.json`. A machine that already
7
+ * paired an agent needs nothing new — its `agent_runs:write` key is accepted
8
+ * too, so without a usage key the agent's credential is used.
9
+ *
10
+ * The key is never printed; {@link maskSecret} is the only form that reaches
11
+ * the terminal.
12
+ */
13
+ export interface UsageCredentials {
14
+ version: 1;
15
+ base_url: string;
16
+ api_key: string;
17
+ connected_at: string;
18
+ }
19
+ export interface ResolvedKey {
20
+ baseUrl: string;
21
+ apiKey: string;
22
+ /** `usage` — `usage.json`; `agent` — the paired agent's `agent.json`. */
23
+ source: 'usage' | 'agent';
24
+ }
25
+ export declare function usageCredentialsPath(env?: NodeJS.ProcessEnv): string;
26
+ /** Directory for the scanner cursor and reporter status. */
27
+ export declare function usageStateDir(env?: NodeJS.ProcessEnv): string;
28
+ /** Create a `0700` directory (tightening one that already exists). */
29
+ export declare function ensurePrivateDir(dir: string): string;
30
+ /** Write a file `0600` via write-then-rename, so a crash never leaves half a file. */
31
+ export declare function writePrivateFile(file: string, content: string): void;
32
+ export declare function saveUsageCredentials(options: {
33
+ apiKey: string;
34
+ baseUrl?: string;
35
+ }, env?: NodeJS.ProcessEnv): string;
36
+ export declare function loadUsageCredentials(env?: NodeJS.ProcessEnv): UsageCredentials | null;
37
+ /** The key reports go out with: `usage.json` first, then a paired agent's. */
38
+ export declare function resolveUsageKey(env?: NodeJS.ProcessEnv): ResolvedKey | null;
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Where `prohost usage` keeps its own state, and which API key it reports with.
3
+ *
4
+ * Observer mode needs no paired agent: `prohost usage connect --key …` stores
5
+ * a key (scope `usage:write`) in `$PROHOST_HOME/usage.json`, written `0600`
6
+ * inside a `0700` directory exactly like `agent.json`. A machine that already
7
+ * paired an agent needs nothing new — its `agent_runs:write` key is accepted
8
+ * too, so without a usage key the agent's credential is used.
9
+ *
10
+ * The key is never printed; {@link maskSecret} is the only form that reaches
11
+ * the terminal.
12
+ */
13
+ import { chmodSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
14
+ import path from 'node:path';
15
+ import { DEFAULT_API_BASE_URL } from '../agent/contract.js';
16
+ import { prohostHome, tryLoadCredentials } from '../agent/credentials.js';
17
+ export function usageCredentialsPath(env = process.env) {
18
+ return path.join(prohostHome(env), 'usage.json');
19
+ }
20
+ /** Directory for the scanner cursor and reporter status. */
21
+ export function usageStateDir(env = process.env) {
22
+ return path.join(prohostHome(env), 'usage');
23
+ }
24
+ /** Create a `0700` directory (tightening one that already exists). */
25
+ export function ensurePrivateDir(dir) {
26
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
27
+ chmodSync(dir, 0o700);
28
+ return dir;
29
+ }
30
+ /** Write a file `0600` via write-then-rename, so a crash never leaves half a file. */
31
+ export function writePrivateFile(file, content) {
32
+ ensurePrivateDir(path.dirname(file));
33
+ const temp = `${file}.${process.pid}.tmp`;
34
+ writeFileSync(temp, content, { mode: 0o600 });
35
+ chmodSync(temp, 0o600);
36
+ renameSync(temp, file);
37
+ }
38
+ export function saveUsageCredentials(options, env = process.env) {
39
+ ensurePrivateDir(prohostHome(env));
40
+ const file = usageCredentialsPath(env);
41
+ const record = {
42
+ version: 1,
43
+ base_url: options.baseUrl || DEFAULT_API_BASE_URL,
44
+ api_key: options.apiKey,
45
+ connected_at: new Date().toISOString(),
46
+ };
47
+ writePrivateFile(file, `${JSON.stringify(record, null, 2)}\n`);
48
+ return file;
49
+ }
50
+ export function loadUsageCredentials(env = process.env) {
51
+ try {
52
+ const parsed = JSON.parse(readFileSync(usageCredentialsPath(env), 'utf8'));
53
+ if (typeof parsed?.api_key !== 'string' || !parsed.api_key)
54
+ return null;
55
+ return {
56
+ version: 1,
57
+ base_url: typeof parsed.base_url === 'string' && parsed.base_url ? parsed.base_url : DEFAULT_API_BASE_URL,
58
+ api_key: parsed.api_key,
59
+ connected_at: typeof parsed.connected_at === 'string' ? parsed.connected_at : '',
60
+ };
61
+ }
62
+ catch {
63
+ return null;
64
+ }
65
+ }
66
+ /** The key reports go out with: `usage.json` first, then a paired agent's. */
67
+ export function resolveUsageKey(env = process.env) {
68
+ const usage = loadUsageCredentials(env);
69
+ if (usage)
70
+ return { baseUrl: usage.base_url, apiKey: usage.api_key, source: 'usage' };
71
+ const agent = tryLoadCredentials(env);
72
+ if (agent)
73
+ return { baseUrl: agent.base_url || DEFAULT_API_BASE_URL, apiKey: agent.api_key, source: 'agent' };
74
+ return null;
75
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Find the Claude Code and Codex logins already on this machine.
3
+ *
4
+ * People who juggle several subscriptions keep each one in its own config
5
+ * directory — `~/.claude-work`, `~/.codex-personal`, or whatever
6
+ * `$CLAUDE_CONFIG_DIR` / `$CODEX_HOME` points at. Observer mode reports all of
7
+ * them, so instead of asking for each one to be re-created under
8
+ * `~/.prohost-accounts`, it registers the directories that are already there.
9
+ *
10
+ * Only directory names and the presence of a few non-secret markers
11
+ * (`projects/`, `settings.json`, `sessions/`, `config.toml`…) are looked at —
12
+ * never a file's contents, and never anything that holds a credential.
13
+ * Whether a login is signed in is asked of the unmodified agent CLI later,
14
+ * exactly as `accounts.ts` already does.
15
+ */
16
+ import type { AccountRecord, AccountRuntime } from '../agent/accounts.js';
17
+ export interface DiscoveredDir {
18
+ runtime: AccountRuntime;
19
+ /** Absolute, symlinks resolved. */
20
+ dir: string;
21
+ /** The label it would be registered under. */
22
+ label: string;
23
+ /** `~/.claude` / `~/.codex` — the agent CLI's own default login. */
24
+ isDefault: boolean;
25
+ /** Why it was found: a home-directory entry or an environment variable. */
26
+ via: 'home' | 'CLAUDE_CONFIG_DIR' | 'CODEX_HOME';
27
+ }
28
+ export interface DiscoveryResult {
29
+ /** Newly registered (or, on a dry run, that would be). */
30
+ added: Array<DiscoveredDir & {
31
+ record?: AccountRecord;
32
+ }>;
33
+ /** Found but already in the registry. */
34
+ known: Array<DiscoveredDir & {
35
+ record: AccountRecord;
36
+ }>;
37
+ /** Found but not registered, and why. */
38
+ skipped: Array<DiscoveredDir & {
39
+ reason: string;
40
+ }>;
41
+ }
42
+ /** The directory an account's logs live in: its own, or the CLI's default under `$HOME`. */
43
+ export declare function accountConfigDir(record: AccountRecord, env?: NodeJS.ProcessEnv): string;
44
+ /**
45
+ * A registry label from a directory name: `~/.claude-baxy` → `baxy`,
46
+ * `~/.codex_work` → `work`, `~/.claude` → `default`. Anything a label can't
47
+ * hold becomes `-`; an empty result falls back to the runtime's name.
48
+ */
49
+ export declare function deriveLabel(dirName: string, runtime: AccountRuntime): string;
50
+ /**
51
+ * Every config directory on this machine, before any registry comparison:
52
+ * `~/.claude*` / `~/.codex*` that look like config directories, plus
53
+ * `$CLAUDE_CONFIG_DIR` / `$CODEX_HOME`. Deduplicated by real path.
54
+ */
55
+ export declare function findConfigDirs(env?: NodeJS.ProcessEnv): DiscoveredDir[];
56
+ /**
57
+ * Register every login found by {@link findConfigDirs} that the machine-wide
58
+ * registry doesn't already know. `dryRun` reports the same answer and writes
59
+ * nothing.
60
+ */
61
+ export declare function discoverAccounts(options?: {
62
+ dryRun?: boolean;
63
+ }, env?: NodeJS.ProcessEnv): DiscoveryResult;