@prohost/cli 0.5.0 → 0.7.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,257 @@
1
+ /**
2
+ * Runtime, model and remaining capacity — what `agent run` tells ProhostAI about itself.
3
+ *
4
+ * ProhostAI shows every paired AI employee with its runtime ("Claude Code"),
5
+ * model ("Opus 5.5"), machine and how much of the operator's Claude or Codex
6
+ * subscription is left. Only this machine can see those things, so the harness
7
+ * reports them in a `runtime_status` frame on the socket it already holds.
8
+ *
9
+ * **Credentials never leave this machine, and this module never reads them.**
10
+ * Every number comes from the agent CLI answering with its own login:
11
+ *
12
+ * - Claude Code: the `rate_limit_event` and `system/init` lines of a run's own
13
+ * `stream-json` output, and — when idle — the `get_usage` control request to
14
+ * an unmodified `claude -p --input-format stream-json` (no model turn).
15
+ * - Codex: the rollout file Codex writes for each run under
16
+ * `$CODEX_HOME/sessions`, and — when idle — `account/rateLimits/read` on
17
+ * `codex app-server`.
18
+ *
19
+ * Nothing here opens the Claude Code keychain item or `~/.codex/auth.json`, and
20
+ * nothing forwards an account id, email or org. Only labels, percentages and
21
+ * reset times go on the wire. Unknown or missing data is omitted, never sent as
22
+ * zero.
23
+ *
24
+ * Every parser here fails soft, in the same spirit as `readClaudeStream`: the
25
+ * shapes belong to other tools and grow without asking us.
26
+ */
27
+ import { spawn as nodeSpawn } from 'node:child_process';
28
+ import type { AccountBlock } from './accounts.js';
29
+ /** Feature name the server lists in `auth_ok.features` when it accepts the frame. */
30
+ export declare const RUNTIME_STATUS_FEATURE = "runtime_status";
31
+ /** Routine report cadence while connected. */
32
+ export declare const STATUS_INTERVAL_MS: number;
33
+ /**
34
+ * Minimum spacing between frames. The server accepts one per 30 s per socket
35
+ * and drops the rest; a second of margin keeps clock jitter from costing us
36
+ * the frame that carried a change.
37
+ */
38
+ export declare const MIN_FRAME_SPACING_MS = 31000;
39
+ /** Ask the agent CLI for fresh numbers when the last ones are this old. */
40
+ export declare const QUOTA_REFRESH_AFTER_MS: number;
41
+ /** Ceiling on one idle quota probe. */
42
+ export declare const PROVIDER_TIMEOUT_MS = 10000;
43
+ export type QuotaKind = 'session' | 'weekly' | 'weekly_model';
44
+ export interface QuotaWindow {
45
+ kind: QuotaKind;
46
+ /** 0–100. */
47
+ used_percent: number;
48
+ /**
49
+ * ISO-8601, or `null` for a window whose usage is known but which has not
50
+ * started — Claude's 5-hour window after five idle hours. Reported as such
51
+ * rather than dropped: "0%, starts on next use" is an answer, a missing
52
+ * window is not.
53
+ */
54
+ resets_at: string | null;
55
+ /** The model a `weekly_model` window is scoped to, e.g. `Fable`. */
56
+ model_label?: string;
57
+ }
58
+ /** What one source managed to learn. Every field is optional; absent means "didn't learn it". */
59
+ export interface RuntimeObservation {
60
+ runtime_version?: string;
61
+ model?: string;
62
+ plan_label?: string;
63
+ quota_windows?: QuotaWindow[];
64
+ }
65
+ /** A percentage on a 0–``scale`` scale, as 0–100 rounded to one decimal. */
66
+ export declare function percent(value: unknown, scale: 1 | 100): number | undefined;
67
+ /** Epoch seconds or an ISO string, as ISO-8601; anything else is `undefined`. */
68
+ export declare function isoTime(value: unknown): string | undefined;
69
+ /** First `X.Y.Z…` in a `--version` line, e.g. `2.1.282 (Claude Code)` or `codex-cli 0.146.0`. */
70
+ export declare function parseVersion(stdout: string): string | undefined;
71
+ /**
72
+ * Windows from a stream `rate_limit_event.rate_limit_info`.
73
+ *
74
+ * `unifiedWindows.*.utilization` is a FRACTION (0–1, and it can exceed 1) with
75
+ * epoch-second resets — unlike `get_usage`, which is 0–100 with ISO strings.
76
+ * The field is marked internal upstream, so an unexpected shape yields nothing.
77
+ */
78
+ export declare function fromClaudeRateLimitInfo(info: unknown): QuotaWindow[];
79
+ /**
80
+ * Windows from a `get_usage` control response (0–100, ISO strings).
81
+ *
82
+ * A window with a usage but a `null` reset is kept, with `resets_at: null` —
83
+ * that is how an idle account's 5-hour window reads, and dropping it is what
84
+ * left paired agents showing a weekly window and nothing else (B3).
85
+ *
86
+ * Per-model weekly windows come from `rate_limits.limits[]` rows of kind
87
+ * `weekly_scoped` (classified on `kind`, as the schema asks, never on a
88
+ * label), falling back to `model_scoped[]`.
89
+ */
90
+ export declare function fromClaudeGetUsage(usage: unknown): QuotaWindow[];
91
+ /** `get_usage.subscription_type` → display label; `undefined` for anything else. */
92
+ export declare function claudePlanLabel(subscriptionType: unknown): string | undefined;
93
+ /**
94
+ * Windows from Codex `rate_limits` (rollout, snake_case) or `rateLimits`
95
+ * (app-server, camelCase).
96
+ *
97
+ * Classified by window LENGTH, never by slot: on some plans `primary` is the
98
+ * weekly window and there is no 5-hour one at all.
99
+ */
100
+ export declare function fromCodexRateLimits(rateLimits: unknown, lineTimestamp?: string): QuotaWindow[];
101
+ /** `plan_type` / `planType` → `ChatGPT Pro`; `undefined` when there is no plan. */
102
+ export declare function codexPlanLabel(planType: unknown): string | undefined;
103
+ /** Model named in the exec string itself (`-m gpt-5` / `--model gpt-5` / `--model=gpt-5`). */
104
+ export declare function modelFromExec(exec: string): string | undefined;
105
+ /** `$CODEX_HOME`, defaulting to `~/.codex` exactly as Codex does. */
106
+ export declare function codexHome(env?: NodeJS.ProcessEnv): string;
107
+ /**
108
+ * The top-level `model = "…"` in `$CODEX_HOME/config.toml` — Codex's default.
109
+ *
110
+ * Only the top level, before the first `[table]`: a `model` inside a profile
111
+ * table is not the default. A first guess only; the rollout's `turn_context`
112
+ * replaces it after the first run.
113
+ */
114
+ export declare function codexConfiguredModel(env?: NodeJS.ProcessEnv): string | undefined;
115
+ /**
116
+ * Locate the rollout for one thread: `sessions/YYYY/MM/DD/rollout-*-<thread_id>.jsonl`.
117
+ *
118
+ * The date directory is local time and a resumed thread lives under the day it
119
+ * started, so the date is never guessed — days are scanned newest first.
120
+ */
121
+ export declare function findCodexRollout(threadId: string, env?: NodeJS.ProcessEnv): string | undefined;
122
+ /**
123
+ * What a Codex rollout says: the model the turn really used (after `-m`,
124
+ * profiles and `-c`), the CLI version, and the last rate-limit snapshot.
125
+ */
126
+ export declare function parseCodexRollout(content: string): RuntimeObservation;
127
+ /** Read the rollout for a finished Codex thread, if it can be found. */
128
+ export declare function readCodexObservation(threadId: string | undefined, env?: NodeJS.ProcessEnv): RuntimeObservation | undefined;
129
+ /**
130
+ * Where fresh numbers come from when no run has produced any lately.
131
+ *
132
+ * Implementations ask the unmodified agent CLI through its own protocol, so
133
+ * the CLI answers with its own login and this process never holds a
134
+ * credential. Resolve `undefined` on any failure — the reporter keeps what it
135
+ * had and omits what it doesn't know.
136
+ */
137
+ export interface QuotaProvider {
138
+ refresh(): Promise<RuntimeObservation | undefined>;
139
+ }
140
+ type SpawnFn = typeof nodeSpawn;
141
+ /**
142
+ * Claude Code's `get_usage` control request. No model turn is made, so no
143
+ * quota is spent. The `initialize` response carries account identity; it is
144
+ * never read.
145
+ */
146
+ export declare function claudeQuotaProvider(options: {
147
+ binary: string;
148
+ spawnImpl?: SpawnFn;
149
+ timeoutMs?: number;
150
+ /** Account environment, read at each probe so a switch applies to the next one. */
151
+ env?: () => NodeJS.ProcessEnv;
152
+ }): QuotaProvider;
153
+ /** Codex's `account/rateLimits/read` over `codex app-server` stdio JSON-RPC. */
154
+ export declare function codexQuotaProvider(options: {
155
+ binary: string;
156
+ clientVersion: string;
157
+ spawnImpl?: SpawnFn;
158
+ timeoutMs?: number;
159
+ /** Account environment, read at each probe so a switch applies to the next one. */
160
+ env?: () => NodeJS.ProcessEnv;
161
+ }): QuotaProvider;
162
+ /**
163
+ * Fold new windows into the ones already known, one KIND at a time.
164
+ *
165
+ * Sources see different slices: a run's `rate_limit_event` carries only the
166
+ * 5-hour and weekly windows (and omits one whose reset has passed), while the
167
+ * idle `get_usage` probe also carries the per-model weekly ones. Replacing the
168
+ * whole list with whatever arrived last is how a stream event used to erase
169
+ * the windows it simply didn't mention. A kind the observation names replaces
170
+ * every window of that kind; a kind it doesn't name is kept.
171
+ */
172
+ export declare function mergeWindows(known: QuotaWindow[] | undefined, observed: QuotaWindow[]): QuotaWindow[];
173
+ /** What the harness knows about accounts on this machine, for the frame. */
174
+ export interface AccountsSnapshot {
175
+ /** The account this agent runs on. */
176
+ current?: AccountBlock;
177
+ /**
178
+ * Every account on the machine, identity and sign-in state only. Omitted
179
+ * when the list would be partial: the server treats it as authoritative
180
+ * and would forget whatever it doesn't name.
181
+ */
182
+ all?: AccountBlock[];
183
+ }
184
+ export interface RuntimeStatusReporterOptions {
185
+ /** `claude_code` | `codex` | `goose` | `custom`. */
186
+ runtime: string;
187
+ /** Idle quota source; omit for a runtime that has none. */
188
+ provider?: QuotaProvider;
189
+ /**
190
+ * Accounts on this machine and which one this agent uses. Re-read on the
191
+ * same cadence as the idle quota probe (it asks each login whether it is
192
+ * signed in), and straight away after {@link RuntimeStatusReporter.accountsChanged}.
193
+ */
194
+ accounts?: () => Promise<AccountsSnapshot | undefined>;
195
+ /** A run is executing — it will bring fresh numbers, so don't probe beside it. */
196
+ isBusy?: () => boolean;
197
+ now?: () => number;
198
+ /** Timer seam for tests. Returns a cancel function. */
199
+ schedule?: (fn: () => void, ms: number) => () => void;
200
+ intervalMs?: number;
201
+ log?: (line: string) => void;
202
+ }
203
+ /**
204
+ * Holds the latest runtime snapshot and decides when to send it.
205
+ *
206
+ * Sends only while attached — i.e. only on a socket whose server advertised
207
+ * {@link RUNTIME_STATUS_FEATURE}. Cadence: once on attach, every
208
+ * {@link STATUS_INTERVAL_MS}, and promptly when the model, plan or runtime
209
+ * version changes or capacity crosses a band — but never closer together than
210
+ * {@link MIN_FRAME_SPACING_MS}.
211
+ */
212
+ export declare class RuntimeStatusReporter {
213
+ private readonly options;
214
+ private readonly now;
215
+ private readonly schedule;
216
+ private state;
217
+ private quotaObservedAt;
218
+ private send;
219
+ private lastSentAt;
220
+ private cancelPending;
221
+ private cancelTick;
222
+ private refreshing;
223
+ private accountGeneration;
224
+ private accounts;
225
+ private accountsObservedAt;
226
+ constructor(options: RuntimeStatusReporterOptions);
227
+ /** Merge what a source learned. Absent fields keep their previous value. */
228
+ observe(observation: RuntimeObservation | undefined): void;
229
+ /**
230
+ * Record the accounts snapshot. Moving to another account drops what was
231
+ * known about the previous one's capacity and plan — they describe a
232
+ * different subscription — and asks for fresh numbers straight away.
233
+ */
234
+ setAccounts(snapshot: AccountsSnapshot | undefined, options?: {
235
+ quiet?: boolean;
236
+ }): void;
237
+ /**
238
+ * The account choice changed (an operator picked one in ProhostAI): re-read
239
+ * accounts and capacity on the next tick rather than in fifteen minutes.
240
+ */
241
+ accountsChanged(): void;
242
+ /** The model to report on a run's completion, when nothing more specific is known. */
243
+ model(): string | undefined;
244
+ /** Start reporting on a socket that advertised the feature. */
245
+ attach(send: (frame: Record<string, unknown>) => void): void;
246
+ /** Stop reporting — the socket is gone. */
247
+ detach(): void;
248
+ /** The frame as it would be sent now. */
249
+ frame(): Record<string, unknown>;
250
+ /** Routine tick: refresh idle quota if it is stale, send, and re-arm. */
251
+ private tick;
252
+ private refresh;
253
+ /** Send now if spacing allows, else once it does. Coalesces. */
254
+ private sendSoon;
255
+ private flush;
256
+ }
257
+ export {};