@prohost/cli 0.8.3 → 0.10.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.
Files changed (41) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +119 -2
  3. package/dist/agent/account_runtime.d.ts +19 -3
  4. package/dist/agent/account_runtime.js +28 -6
  5. package/dist/agent/accounts.d.ts +17 -1
  6. package/dist/agent/accounts.js +43 -2
  7. package/dist/agent/agent_commands.js +6 -3
  8. package/dist/agent/api.d.ts +6 -0
  9. package/dist/agent/api.js +10 -5
  10. package/dist/agent/claude.js +9 -3
  11. package/dist/agent/command.d.ts +9 -0
  12. package/dist/agent/command.js +64 -2
  13. package/dist/agent/daemon.d.ts +4 -0
  14. package/dist/agent/daemon.js +4 -0
  15. package/dist/agent/prompt.d.ts +27 -0
  16. package/dist/agent/prompt.js +21 -1
  17. package/dist/agent/run.d.ts +28 -3
  18. package/dist/agent/run.js +90 -18
  19. package/dist/agent/scheduler.d.ts +44 -0
  20. package/dist/agent/scheduler.js +93 -0
  21. package/dist/agent/worktrees.d.ts +136 -0
  22. package/dist/agent/worktrees.js +317 -0
  23. package/dist/index.d.ts +1 -0
  24. package/dist/index.js +29 -2
  25. package/dist/usage/backoff.d.ts +42 -0
  26. package/dist/usage/backoff.js +85 -0
  27. package/dist/usage/command.d.ts +31 -0
  28. package/dist/usage/command.js +269 -0
  29. package/dist/usage/config.d.ts +38 -0
  30. package/dist/usage/config.js +75 -0
  31. package/dist/usage/discovery.d.ts +63 -0
  32. package/dist/usage/discovery.js +187 -0
  33. package/dist/usage/quota.d.ts +96 -0
  34. package/dist/usage/quota.js +145 -0
  35. package/dist/usage/scanner.d.ts +117 -0
  36. package/dist/usage/scanner.js +438 -0
  37. package/dist/usage/service.d.ts +122 -0
  38. package/dist/usage/service.js +299 -0
  39. package/dist/version.d.ts +2 -2
  40. package/dist/version.js +1 -1
  41. package/package.json +1 -1
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Remaining capacity for every login on this machine, in `POST /v1/usage/reports` shape.
3
+ *
4
+ * The probes are the ones `agent run` already uses when idle (see
5
+ * `runtime_status.ts`): Claude Code's `get_usage` control request and Codex's
6
+ * `account/rateLimits/read`, each asked of the unmodified CLI with the
7
+ * account's own config directory, so no model turn is spent and no credential
8
+ * is read here. Sign-in state comes from `checkAuth` in `accounts.ts`.
9
+ *
10
+ * Each account is probed at most once per {@link QUOTA_REFRESH_AFTER_MS}, a
11
+ * few at a time, and what was learned is kept between probes (and restarts)
12
+ * so every report carries the latest known numbers with the time they were
13
+ * observed.
14
+ */
15
+ import type { AccountBilling, AccountRecord, AccountRuntime, AuthCheck, AuthState, CommandRunner } from '../agent/accounts.js';
16
+ import type { QuotaWindow, RuntimeObservation } from '../agent/runtime_status.js';
17
+ /** Accounts probed at once. Each probe is a short-lived agent CLI process. */
18
+ export declare const PROBE_CONCURRENCY = 3;
19
+ /** What is known about one account between probes. Persisted in the status file. */
20
+ export interface AccountObservation {
21
+ /** Epoch ms of the last probe attempt, successful or not. */
22
+ probedAt?: number;
23
+ /** ISO time the windows were last learned. */
24
+ observedAt?: string;
25
+ authState: AuthState;
26
+ billing?: AccountBilling;
27
+ planLabel?: string;
28
+ windows: QuotaWindow[];
29
+ }
30
+ export type ObservationCache = Record<string, AccountObservation>;
31
+ export interface ProbeResult {
32
+ auth: AuthCheck;
33
+ observation?: RuntimeObservation;
34
+ }
35
+ export type Prober = (record: AccountRecord) => Promise<ProbeResult>;
36
+ /** Wire window (`RawWindow` in the server contract). */
37
+ export interface RawWindow {
38
+ kind: string;
39
+ usedPercent: number;
40
+ resetsAt: string | null;
41
+ modelLabel?: string;
42
+ }
43
+ export interface ReportedAccount {
44
+ accountKey: string;
45
+ label: string;
46
+ runtime: AccountRuntime;
47
+ billing: AccountBilling;
48
+ planLabel?: string;
49
+ authState: AuthState;
50
+ quotaWindows: RawWindow[];
51
+ observedAt?: string;
52
+ }
53
+ export interface UsageReportFrame {
54
+ source: 'cli';
55
+ machineId: string;
56
+ machine: string;
57
+ accounts: ReportedAccount[];
58
+ }
59
+ /**
60
+ * Environment that points the agent CLI at this account. The default login is
61
+ * the CLI's own directory, so an inherited `CLAUDE_CONFIG_DIR` / `CODEX_HOME`
62
+ * (a foreground shell that exports one) is cleared rather than followed —
63
+ * otherwise "default" would probe some other login. Node drops `undefined`
64
+ * entries from a child's environment.
65
+ */
66
+ export declare function probeEnv(record: AccountRecord): NodeJS.ProcessEnv;
67
+ /** The probes `agent run` uses, pointed at one account at a time. */
68
+ export declare function defaultProber(options?: {
69
+ run?: CommandRunner;
70
+ timeoutMs?: number;
71
+ }): Prober;
72
+ /** Run ``fn`` over ``items`` with at most ``limit`` in flight. Never rejects for one item. */
73
+ export declare function mapLimit<T>(items: T[], limit: number, fn: (item: T) => Promise<void>): Promise<void>;
74
+ /** Accounts due a probe: never probed, or probed {@link QUOTA_REFRESH_AFTER_MS} ago or more. */
75
+ export declare function dueForProbe(records: AccountRecord[], cache: ObservationCache, nowMs: number, refreshAfterMs?: number): AccountRecord[];
76
+ /** Probe every account that is due, folding the answers into ``cache``. */
77
+ export declare function probeAccounts(records: AccountRecord[], cache: ObservationCache, options: {
78
+ prober: Prober;
79
+ now?: () => number;
80
+ concurrency?: number;
81
+ refreshAfterMs?: number;
82
+ }): Promise<AccountRecord[]>;
83
+ export declare function toRawWindow(window: QuotaWindow): RawWindow;
84
+ /**
85
+ * The report body. At most {@link MAX_REPORTED_ACCOUNTS} accounts — the
86
+ * defaults first, then in registry order; `omitted` says how many did not fit.
87
+ */
88
+ export declare function buildReportFrame(options: {
89
+ machineId: string;
90
+ machine: string;
91
+ records: AccountRecord[];
92
+ cache: ObservationCache;
93
+ }): {
94
+ frame: UsageReportFrame;
95
+ omitted: number;
96
+ };
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Remaining capacity for every login on this machine, in `POST /v1/usage/reports` shape.
3
+ *
4
+ * The probes are the ones `agent run` already uses when idle (see
5
+ * `runtime_status.ts`): Claude Code's `get_usage` control request and Codex's
6
+ * `account/rateLimits/read`, each asked of the unmodified CLI with the
7
+ * account's own config directory, so no model turn is spent and no credential
8
+ * is read here. Sign-in state comes from `checkAuth` in `accounts.ts`.
9
+ *
10
+ * Each account is probed at most once per {@link QUOTA_REFRESH_AFTER_MS}, a
11
+ * few at a time, and what was learned is kept between probes (and restarts)
12
+ * so every report carries the latest known numbers with the time they were
13
+ * observed.
14
+ */
15
+ import { DEFAULT_ACCOUNT_LABEL, MAX_REPORTED_ACCOUNTS, checkAuth, defaultCommandRunner } from '../agent/accounts.js';
16
+ import { PROVIDER_TIMEOUT_MS, QUOTA_REFRESH_AFTER_MS, claudeQuotaProvider, codexQuotaProvider, mergeWindows, } from '../agent/runtime_status.js';
17
+ import { CLI_VERSION } from '../version.js';
18
+ /** Accounts probed at once. Each probe is a short-lived agent CLI process. */
19
+ export const PROBE_CONCURRENCY = 3;
20
+ /** Most windows per account the server accepts. */
21
+ const MAX_WINDOWS = 8;
22
+ const MAX_LABEL_CHARS = 60;
23
+ const MAX_MACHINE_CHARS = 120;
24
+ /**
25
+ * Environment that points the agent CLI at this account. The default login is
26
+ * the CLI's own directory, so an inherited `CLAUDE_CONFIG_DIR` / `CODEX_HOME`
27
+ * (a foreground shell that exports one) is cleared rather than followed —
28
+ * otherwise "default" would probe some other login. Node drops `undefined`
29
+ * entries from a child's environment.
30
+ */
31
+ export function probeEnv(record) {
32
+ const variable = record.runtime === 'claude_code' ? 'CLAUDE_CONFIG_DIR' : 'CODEX_HOME';
33
+ return { [variable]: record.dir ?? undefined };
34
+ }
35
+ /** The probes `agent run` uses, pointed at one account at a time. */
36
+ export function defaultProber(options = {}) {
37
+ const baseRun = options.run ?? defaultCommandRunner;
38
+ return async (record) => {
39
+ const env = probeEnv(record);
40
+ // `checkAuth` passes only the account's own variable; the override clears
41
+ // an inherited one for the default login.
42
+ const auth = await checkAuth(record, { run: (command, args, extra) => baseRun(command, args, { ...extra, ...env }) });
43
+ // A signed-out login has nothing to say about capacity.
44
+ if (auth.state === 'expired')
45
+ return { auth };
46
+ const provider = record.runtime === 'claude_code'
47
+ ? claudeQuotaProvider({ binary: 'claude', timeoutMs: options.timeoutMs ?? PROVIDER_TIMEOUT_MS, env: () => env })
48
+ : codexQuotaProvider({
49
+ binary: 'codex',
50
+ clientVersion: CLI_VERSION,
51
+ timeoutMs: options.timeoutMs ?? PROVIDER_TIMEOUT_MS,
52
+ env: () => env,
53
+ });
54
+ let observation;
55
+ try {
56
+ observation = await provider.refresh();
57
+ }
58
+ catch {
59
+ observation = undefined;
60
+ }
61
+ return { auth, observation };
62
+ };
63
+ }
64
+ /** Run ``fn`` over ``items`` with at most ``limit`` in flight. Never rejects for one item. */
65
+ export async function mapLimit(items, limit, fn) {
66
+ let next = 0;
67
+ const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
68
+ while (next < items.length) {
69
+ const item = items[next];
70
+ next += 1;
71
+ try {
72
+ await fn(item);
73
+ }
74
+ catch {
75
+ /* one account's failure never stops the others */
76
+ }
77
+ }
78
+ });
79
+ await Promise.all(workers);
80
+ }
81
+ /** Accounts due a probe: never probed, or probed {@link QUOTA_REFRESH_AFTER_MS} ago or more. */
82
+ export function dueForProbe(records, cache, nowMs, refreshAfterMs = QUOTA_REFRESH_AFTER_MS) {
83
+ return records.filter((r) => {
84
+ const probedAt = cache[r.key]?.probedAt;
85
+ return probedAt === undefined || nowMs - probedAt >= refreshAfterMs;
86
+ });
87
+ }
88
+ /** Probe every account that is due, folding the answers into ``cache``. */
89
+ export async function probeAccounts(records, cache, options) {
90
+ const now = options.now ?? Date.now;
91
+ const due = dueForProbe(records, cache, now(), options.refreshAfterMs);
92
+ await mapLimit(due, options.concurrency ?? PROBE_CONCURRENCY, async (record) => {
93
+ const startedAt = now();
94
+ const previous = cache[record.key] ?? { authState: 'unknown', windows: [] };
95
+ // Stamped before the probe so a probe that throws still waits its turn.
96
+ cache[record.key] = { ...previous, probedAt: startedAt };
97
+ const { auth, observation } = await options.prober(record);
98
+ const next = { ...previous, probedAt: startedAt, authState: auth.state };
99
+ if (auth.billing)
100
+ next.billing = auth.billing;
101
+ if (observation?.plan_label)
102
+ next.planLabel = observation.plan_label;
103
+ if (observation?.quota_windows && observation.quota_windows.length > 0) {
104
+ next.windows = mergeWindows(previous.windows, observation.quota_windows);
105
+ next.observedAt = new Date(now()).toISOString();
106
+ }
107
+ cache[record.key] = next;
108
+ });
109
+ return due;
110
+ }
111
+ export function toRawWindow(window) {
112
+ return {
113
+ kind: window.kind,
114
+ usedPercent: window.used_percent,
115
+ resetsAt: window.resets_at,
116
+ ...(window.model_label ? { modelLabel: window.model_label } : {}),
117
+ };
118
+ }
119
+ /**
120
+ * The report body. At most {@link MAX_REPORTED_ACCOUNTS} accounts — the
121
+ * defaults first, then in registry order; `omitted` says how many did not fit.
122
+ */
123
+ export function buildReportFrame(options) {
124
+ const ordered = [
125
+ ...options.records.filter((r) => r.label === DEFAULT_ACCOUNT_LABEL),
126
+ ...options.records.filter((r) => r.label !== DEFAULT_ACCOUNT_LABEL),
127
+ ];
128
+ const accounts = ordered.slice(0, MAX_REPORTED_ACCOUNTS).map((record) => {
129
+ const seen = options.cache[record.key];
130
+ return {
131
+ accountKey: record.key,
132
+ label: record.label.slice(0, MAX_LABEL_CHARS),
133
+ runtime: record.runtime,
134
+ billing: seen?.billing ?? record.billing,
135
+ ...(seen?.planLabel ? { planLabel: seen.planLabel } : {}),
136
+ authState: seen?.authState ?? 'unknown',
137
+ quotaWindows: (seen?.windows ?? []).slice(0, MAX_WINDOWS).map(toRawWindow),
138
+ ...(seen?.observedAt ? { observedAt: seen.observedAt } : {}),
139
+ };
140
+ });
141
+ return {
142
+ frame: { source: 'cli', machineId: options.machineId, machine: options.machine.slice(0, MAX_MACHINE_CHARS), accounts },
143
+ omitted: Math.max(0, ordered.length - MAX_REPORTED_ACCOUNTS),
144
+ };
145
+ }
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Token totals from the session logs Claude Code and Codex already write.
3
+ *
4
+ * Aggregates only: tokens per (local day, account, runtime, model, project),
5
+ * where the project is the basename of the working directory the log names.
6
+ * No prompt, reply, tool call or file path ever leaves this module — lines are
7
+ * parsed for their usage counters and nothing else is kept.
8
+ *
9
+ * - **Claude Code** writes `<config dir>/projects/<project>/<session>.jsonl`.
10
+ * Each assistant line carries the API response's `message.usage`; one
11
+ * response is split over several lines (one per content block) that repeat
12
+ * the same usage, and a resumed session can copy earlier lines into a new
13
+ * file. So usage is keyed on `message.id` across every file: the first
14
+ * sighting counts, a later one only adds what grew.
15
+ * - **Codex** writes `$CODEX_HOME/sessions/YYYY/MM/DD/rollout-*.jsonl` (moved
16
+ * to `archived_sessions/` when archived). Its `token_count` events carry the
17
+ * session's running `total_token_usage`; usage is the growth of that total
18
+ * between events, credited to the model of the latest `turn_context`. Its
19
+ * `input_tokens` include the cached ones, so they are split apart here.
20
+ *
21
+ * Incremental: a cursor per file (byte offset, size, mtime) lives in the scan
22
+ * state, so each pass reads only what was appended. The first pass looks back
23
+ * {@link RETENTION_DAYS} days. Every (day, account) a pass changes is marked dirty and is
24
+ * re-sent whole — the server replaces a day's rows, so a resend is harmless.
25
+ */
26
+ import type { AccountRuntime } from '../agent/accounts.js';
27
+ /** How far back the first scan looks, and how long totals are kept locally. */
28
+ export declare const RETENTION_DAYS = 30;
29
+ /** Most rows in one `POST /v1/usage/token-totals`. */
30
+ export declare const MAX_ROWS_PER_BATCH = 2000;
31
+ /** `[input, cachedInput, cacheWrite, output]`. Claude's input excludes cache; Codex's is split to match. */
32
+ export type Totals = [number, number, number, number];
33
+ export interface ScanAccount {
34
+ key: string;
35
+ runtime: AccountRuntime;
36
+ /** The login's config directory. */
37
+ dir: string;
38
+ }
39
+ interface ClaudeCursor {
40
+ kind: 'claude';
41
+ offset: number;
42
+ size: number;
43
+ mtimeMs: number;
44
+ }
45
+ interface CodexCursor {
46
+ kind: 'codex';
47
+ offset: number;
48
+ size: number;
49
+ mtimeMs: number;
50
+ /** The session's running `total_token_usage` as last seen, raw (input includes cached). */
51
+ totals: Totals;
52
+ model?: string;
53
+ project?: string | null;
54
+ }
55
+ type Cursor = ClaudeCursor | CodexCursor;
56
+ export interface ScanState {
57
+ version: 1;
58
+ files: Record<string, Cursor>;
59
+ /** Bucket key ({@link bucketKey}) → totals. */
60
+ buckets: Record<string, Totals>;
61
+ /** Claude response id → the bucket it was credited to and what was credited. */
62
+ seen: Record<string, [string, number, number, number, number]>;
63
+ /** `day|accountKey` pairs ({@link pendingKey}) changed since they were last uploaded. */
64
+ dirty: string[];
65
+ /** Pending pair → consecutive uploads the server only partly accepted. */
66
+ strikes: Record<string, number>;
67
+ }
68
+ /** A dirty entry: one account's rows for one day. */
69
+ export declare function pendingKey(day: string, accountKey: string): string;
70
+ /** The day of a dirty entry. */
71
+ export declare function pendingDay(entry: string): string;
72
+ /** Days with anything pending, sorted. */
73
+ export declare function pendingDays(state: ScanState): string[];
74
+ export interface TokenRow {
75
+ day: string;
76
+ accountKey: string;
77
+ runtime: AccountRuntime;
78
+ model: string;
79
+ project: string | null;
80
+ inputTokens: number;
81
+ cachedInputTokens: number;
82
+ cacheWriteTokens: number;
83
+ outputTokens: number;
84
+ }
85
+ export interface ScanOptions {
86
+ now?: number;
87
+ /** IANA zone the days are bucketed in. Defaults to this machine's. */
88
+ timeZone?: string;
89
+ }
90
+ export interface ScanSummary {
91
+ filesRead: number;
92
+ /** Days whose totals changed in this pass. */
93
+ touchedDays: string[];
94
+ }
95
+ export declare function emptyScanState(): ScanState;
96
+ export declare function loadScanState(file: string): ScanState;
97
+ export declare function saveScanState(file: string, state: ScanState): void;
98
+ /** `YYYY-MM-DD` of an instant in a time zone (the machine's by default). */
99
+ export declare function dayKey(ms: number, timeZone?: string): string;
100
+ /** Bucket identity. JSON so it round-trips; the day is first so it can be read cheaply. */
101
+ export declare function bucketKey(day: string, accountKey: string, runtime: AccountRuntime, model: string, project: string | null): string;
102
+ /** Basename of a working directory — the repo or folder name, never the path. */
103
+ export declare function projectName(cwd: unknown): string | null;
104
+ /**
105
+ * One incremental pass over every account's logs. Mutates and returns
106
+ * ``state``; the caller persists it.
107
+ */
108
+ export declare function scanUsage(accounts: ScanAccount[], state: ScanState, options?: ScanOptions): ScanSummary;
109
+ /** The rows for the given days, in upload shape. Zero-token buckets are left out. */
110
+ export declare function rowsForDays(state: ScanState, days: Iterable<string>): TokenRow[];
111
+ /** The rows for the given `day|accountKey` pairs, in upload shape. */
112
+ export declare function rowsForPending(state: ScanState, pairs: Iterable<string>): TokenRow[];
113
+ /** Split rows into upload batches. */
114
+ export declare function batches<T>(rows: T[], size?: number): T[][];
115
+ /** Local totals per account since ``fromDay`` (inclusive), for `usage status`. */
116
+ export declare function localTotals(state: ScanState, fromDay: string): Map<string, Totals>;
117
+ export {};