rinco-pi-hud 1.0.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 (45) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +18 -0
  3. package/README.md +204 -0
  4. package/docs/CONTRIBUTING.md +110 -0
  5. package/docs/README.zh-CN.md +204 -0
  6. package/docs/footer.md +139 -0
  7. package/extensions/hud/commands/settings.ts +765 -0
  8. package/extensions/hud/config/config-counts.ts +87 -0
  9. package/extensions/hud/config/config.ts +65 -0
  10. package/extensions/hud/config/file-io.ts +128 -0
  11. package/extensions/hud/config/normalize.ts +374 -0
  12. package/extensions/hud/config/persist.ts +277 -0
  13. package/extensions/hud/config/schema.ts +303 -0
  14. package/extensions/hud/footer/format.ts +279 -0
  15. package/extensions/hud/footer/index.ts +543 -0
  16. package/extensions/hud/footer/layout.ts +183 -0
  17. package/extensions/hud/footer/render-labels.ts +305 -0
  18. package/extensions/hud/index.ts +466 -0
  19. package/extensions/hud/segments/extension-status.ts +88 -0
  20. package/extensions/hud/segments/git.ts +384 -0
  21. package/extensions/hud/segments/mcp-status.ts +25 -0
  22. package/extensions/hud/segments/package-version.ts +651 -0
  23. package/extensions/hud/segments/project-state.ts +67 -0
  24. package/extensions/hud/segments/runtime-definitions.ts +695 -0
  25. package/extensions/hud/segments/runtime.ts +167 -0
  26. package/extensions/hud/segments/skill-activity.ts +152 -0
  27. package/extensions/hud/session/context.ts +19 -0
  28. package/extensions/hud/session/lifecycle.ts +60 -0
  29. package/extensions/hud/session/live-context.ts +75 -0
  30. package/extensions/hud/state/index.ts +67 -0
  31. package/extensions/hud/state/project-refresh.ts +104 -0
  32. package/extensions/hud/state/telemetry.ts +261 -0
  33. package/extensions/hud/telemetry/codex-usage/LICENSE +21 -0
  34. package/extensions/hud/telemetry/codex-usage/app-server-client.ts +231 -0
  35. package/extensions/hud/telemetry/codex-usage/format.ts +278 -0
  36. package/extensions/hud/telemetry/codex-usage/index.ts +462 -0
  37. package/extensions/hud/telemetry/codex-usage/normalize.ts +264 -0
  38. package/extensions/hud/telemetry/codex-usage/query.ts +207 -0
  39. package/extensions/hud/telemetry/codex-usage/safety.ts +27 -0
  40. package/extensions/hud/telemetry/codex-usage/types.ts +137 -0
  41. package/extensions/hud/telemetry/format.ts +434 -0
  42. package/extensions/hud/telemetry/token-switch.ts +114 -0
  43. package/extensions/hud/ui/icons.ts +239 -0
  44. package/extensions/hud/ui/style.ts +276 -0
  45. package/package.json +56 -0
@@ -0,0 +1,264 @@
1
+ import { sanitizeCodexText } from "./safety.ts";
2
+ import type {
3
+ AppServerRateLimitResetCredit,
4
+ AppServerRateLimitResetCredits,
5
+ AppServerRateLimitResponse,
6
+ AppServerRateLimitSnapshot,
7
+ AppServerWindowSnapshot,
8
+ BackendAdditionalRateLimit,
9
+ BackendRateLimitDetails,
10
+ BackendRateLimitResetCredits,
11
+ BackendWindowSnapshot,
12
+ CodexUsageReport,
13
+ NormalizedRateLimitResetCredit,
14
+ NormalizedRateLimitResetCredits,
15
+ NormalizedRateLimitSnapshot,
16
+ NormalizedRateLimitWindow,
17
+ RateLimitStatusPayload,
18
+ UsageSource,
19
+ } from "./types.ts";
20
+
21
+ export function normalizeBackendPayload(
22
+ payload: RateLimitStatusPayload,
23
+ capturedAt: number,
24
+ source: UsageSource,
25
+ ): CodexUsageReport {
26
+ const snapshots: NormalizedRateLimitSnapshot[] = [];
27
+ const planType = asString(payload.plan_type);
28
+ const primary = normalizeBackendSnapshot("codex", undefined, payload.rate_limit);
29
+ if (primary) snapshots.push(primary);
30
+
31
+ const additional = Array.isArray(payload.additional_rate_limits)
32
+ ? payload.additional_rate_limits
33
+ : [];
34
+ for (const item of additional) {
35
+ const additionalLimit = asObject(item) as BackendAdditionalRateLimit | undefined;
36
+ if (!additionalLimit) continue;
37
+ const limitId =
38
+ asString(additionalLimit.metered_feature) ?? asString(additionalLimit.limit_name);
39
+ if (!limitId) continue;
40
+ try {
41
+ const snapshot = normalizeBackendSnapshot(
42
+ limitId,
43
+ asString(additionalLimit.limit_name),
44
+ additionalLimit.rate_limit,
45
+ );
46
+ if (snapshot) snapshots.push(snapshot);
47
+ } catch {
48
+ // Optional additional buckets must not hide otherwise usable primary/reset usage.
49
+ }
50
+ }
51
+
52
+ const resetCredits = normalizeBackendRateLimitResetCredits(payload.rate_limit_reset_credits);
53
+ if (snapshots.length === 0 && !resetCredits) {
54
+ throw new Error("Codex usage endpoint returned no displayable usage data.");
55
+ }
56
+
57
+ return { source, capturedAt, planType, snapshots, resetCredits };
58
+ }
59
+
60
+ function normalizeBackendSnapshot(
61
+ limitId: string,
62
+ limitName: string | undefined,
63
+ rateLimit: unknown,
64
+ ): NormalizedRateLimitSnapshot | undefined {
65
+ if (rateLimit === null || rateLimit === undefined) return undefined;
66
+
67
+ const details = assertObject(rateLimit, "rate limit") as BackendRateLimitDetails;
68
+ const primary = normalizeBackendWindow(details.primary_window);
69
+ const secondary = normalizeBackendWindow(details.secondary_window);
70
+
71
+ if (!primary && !secondary) return undefined;
72
+ return { limitId, limitName, primary, secondary };
73
+ }
74
+
75
+ function normalizeBackendWindow(value: unknown): NormalizedRateLimitWindow | undefined {
76
+ if (value === null || value === undefined) return undefined;
77
+ const window = assertObject(value, "rate-limit window") as BackendWindowSnapshot;
78
+ const usedPercent = asNumber(window.used_percent);
79
+ if (usedPercent === undefined) return undefined;
80
+ const limitSeconds = asNumber(window.limit_window_seconds);
81
+ const resetsAt = asNumber(window.reset_at);
82
+ return {
83
+ usedPercent,
84
+ windowMinutes: limitSeconds && limitSeconds > 0 ? Math.ceil(limitSeconds / 60) : undefined,
85
+ resetsAt,
86
+ };
87
+ }
88
+
89
+ function normalizeBackendRateLimitResetCredits(
90
+ value: unknown,
91
+ ): NormalizedRateLimitResetCredits | undefined {
92
+ const resetCredits = asObject(value) as BackendRateLimitResetCredits | undefined;
93
+ const availableCount = asNonnegativeInteger(resetCredits?.available_count);
94
+ return availableCount === undefined ? undefined : { availableCount };
95
+ }
96
+
97
+ export function normalizeAppServerResponse(
98
+ response: AppServerRateLimitResponse,
99
+ capturedAt: number,
100
+ ): CodexUsageReport {
101
+ const snapshots: NormalizedRateLimitSnapshot[] = [];
102
+ const addSnapshot = (raw: unknown, fallbackId: string, optional = false) => {
103
+ let snapshot: NormalizedRateLimitSnapshot | undefined;
104
+ try {
105
+ snapshot = normalizeAppServerSnapshot(raw, fallbackId);
106
+ } catch (error) {
107
+ if (optional) return;
108
+ throw error;
109
+ }
110
+ if (!snapshot) return;
111
+ const resolvedSnapshot = snapshot;
112
+ const existingIndex = snapshots.findIndex((item) => item.limitId === resolvedSnapshot.limitId);
113
+ const existing = existingIndex >= 0 ? snapshots[existingIndex] : undefined;
114
+ if (existingIndex >= 0 && existing)
115
+ snapshots[existingIndex] = mergeSnapshot(existing, resolvedSnapshot);
116
+ else snapshots.push(resolvedSnapshot);
117
+ };
118
+
119
+ addSnapshot(response.rateLimits, "codex");
120
+ const snapshotsByLimitId = asObject(response.rateLimitsByLimitId);
121
+ if (snapshotsByLimitId) {
122
+ for (const [limitId, raw] of Object.entries(snapshotsByLimitId)) {
123
+ if (limitId) addSnapshot(raw, limitId, true);
124
+ }
125
+ }
126
+
127
+ const resetCredits = normalizeAppServerRateLimitResetCredits(response.rateLimitResetCredits);
128
+ if (snapshots.length === 0 && !resetCredits) {
129
+ throw new Error("codex app-server returned no displayable usage data.");
130
+ }
131
+
132
+ const planType = asAppServerPlanType(response.rateLimits);
133
+ return {
134
+ source: "codex-app-server",
135
+ capturedAt,
136
+ planType,
137
+ snapshots,
138
+ resetCredits,
139
+ };
140
+ }
141
+
142
+ function asAppServerPlanType(raw: unknown): string | undefined {
143
+ if (raw === null || raw === undefined) return undefined;
144
+ const snapshot = assertObject(
145
+ raw,
146
+ "app-server rate-limit snapshot",
147
+ ) as AppServerRateLimitSnapshot;
148
+ return asString(snapshot.planType);
149
+ }
150
+
151
+ function normalizeAppServerSnapshot(
152
+ raw: unknown,
153
+ fallbackId: string,
154
+ ): NormalizedRateLimitSnapshot | undefined {
155
+ if (raw === null || raw === undefined) return undefined;
156
+ const snapshot = assertObject(
157
+ raw,
158
+ "app-server rate-limit snapshot",
159
+ ) as AppServerRateLimitSnapshot;
160
+ const limitId = asString(snapshot.limitId) ?? fallbackId;
161
+ const limitName = asString(snapshot.limitName);
162
+ const primary = normalizeAppServerWindow(snapshot.primary);
163
+ const secondary = normalizeAppServerWindow(snapshot.secondary);
164
+ if (!primary && !secondary) return undefined;
165
+ return { limitId, limitName, primary, secondary };
166
+ }
167
+
168
+ function normalizeAppServerWindow(value: unknown): NormalizedRateLimitWindow | undefined {
169
+ if (value === null || value === undefined) return undefined;
170
+ const window = assertObject(value, "app-server rate-limit window") as AppServerWindowSnapshot;
171
+ const usedPercent = asNumber(window.usedPercent);
172
+ if (usedPercent === undefined) return undefined;
173
+ return {
174
+ usedPercent,
175
+ windowMinutes: asNumber(window.windowDurationMins),
176
+ resetsAt: asNumber(window.resetsAt),
177
+ };
178
+ }
179
+
180
+ function normalizeAppServerRateLimitResetCredits(
181
+ value: unknown,
182
+ ): NormalizedRateLimitResetCredits | undefined {
183
+ const resetCredits = asObject(value) as AppServerRateLimitResetCredits | undefined;
184
+ const availableCount = asNonnegativeInteger(resetCredits?.availableCount);
185
+ if (availableCount === undefined) return undefined;
186
+
187
+ const rawCredits = resetCredits?.credits;
188
+ if (!Array.isArray(rawCredits)) return { availableCount };
189
+
190
+ const credits = rawCredits
191
+ .map(normalizeAppServerRateLimitResetCredit)
192
+ .filter((credit): credit is NormalizedRateLimitResetCredit => credit !== undefined)
193
+ .slice(0, availableCount);
194
+ if (rawCredits.length > 0 && credits.length === 0 && availableCount > 0) {
195
+ return { availableCount };
196
+ }
197
+ return { availableCount, credits };
198
+ }
199
+
200
+ function normalizeAppServerRateLimitResetCredit(
201
+ value: unknown,
202
+ ): NormalizedRateLimitResetCredit | undefined {
203
+ const credit = asObject(value) as AppServerRateLimitResetCredit | undefined;
204
+ const id = asString(credit?.id);
205
+ if (!id) return undefined;
206
+
207
+ const normalized: NormalizedRateLimitResetCredit = { id };
208
+ const resetType = asString(credit?.resetType);
209
+ const status = asString(credit?.status);
210
+ const grantedAt = asNumber(credit?.grantedAt);
211
+ const expiresAt = asNumber(credit?.expiresAt);
212
+ const title = asString(credit?.title);
213
+ const description = asString(credit?.description);
214
+ if (resetType !== undefined) normalized.resetType = resetType;
215
+ if (status !== undefined) normalized.status = status;
216
+ if (grantedAt !== undefined) normalized.grantedAt = grantedAt;
217
+ if (expiresAt !== undefined) normalized.expiresAt = expiresAt;
218
+ if (title !== undefined) normalized.title = title;
219
+ if (description !== undefined) normalized.description = description;
220
+ return normalized;
221
+ }
222
+
223
+ function mergeSnapshot(
224
+ left: NormalizedRateLimitSnapshot,
225
+ right: NormalizedRateLimitSnapshot,
226
+ ): NormalizedRateLimitSnapshot {
227
+ return {
228
+ limitId: right.limitId || left.limitId,
229
+ limitName: right.limitName ?? left.limitName,
230
+ primary: right.primary ?? left.primary,
231
+ secondary: right.secondary ?? left.secondary,
232
+ };
233
+ }
234
+
235
+ function assertObject(value: unknown, description: string): Record<string, unknown> {
236
+ const object = asObject(value);
237
+ if (!object) throw new Error(`${description} was not an object.`);
238
+ return object;
239
+ }
240
+
241
+ function asObject(value: unknown): Record<string, unknown> | undefined {
242
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
243
+ return value as Record<string, unknown>;
244
+ }
245
+
246
+ function asString(value: unknown): string | undefined {
247
+ if (typeof value !== "string") return undefined;
248
+ return sanitizeCodexText(value, 160) || undefined;
249
+ }
250
+
251
+ function asNumber(value: unknown): number | undefined {
252
+ if (typeof value === "number" && Number.isFinite(value)) return value;
253
+ if (typeof value === "string" && value.trim()) {
254
+ const parsed = Number(value);
255
+ return Number.isFinite(parsed) ? parsed : undefined;
256
+ }
257
+ return undefined;
258
+ }
259
+
260
+ function asNonnegativeInteger(value: unknown): number | undefined {
261
+ const parsed = asNumber(value);
262
+ if (parsed === undefined || !Number.isSafeInteger(parsed)) return undefined;
263
+ return Math.max(0, parsed);
264
+ }
@@ -0,0 +1,207 @@
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { queryViaCodexAppServer } from "./app-server-client.ts";
3
+ import { normalizeBackendPayload } from "./normalize.ts";
4
+ import { safeCodexErrorMessage } from "./safety.ts";
5
+ import type {
6
+ CodexUsageReport,
7
+ PiModel,
8
+ QueryUsageOptions,
9
+ QueryUsageResult,
10
+ RateLimitStatusPayload,
11
+ UsageQueryError,
12
+ } from "./types.ts";
13
+
14
+ const CODEX_PROVIDER_ID = "openai-codex";
15
+ const CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
16
+ const MAX_RESPONSE_BODY_BYTES = 1024 * 1024;
17
+
18
+ export function isOpenAICodexModel(model: Pick<PiModel, "provider"> | undefined): boolean {
19
+ return model?.provider === CODEX_PROVIDER_ID;
20
+ }
21
+
22
+ export function isStaleExtensionContextError(error: unknown): boolean {
23
+ return (
24
+ error instanceof Error &&
25
+ error.message.includes("This extension ctx is stale after session replacement or reload")
26
+ );
27
+ }
28
+
29
+ export async function queryUsage(
30
+ ctx: ExtensionContext,
31
+ options: Pick<QueryUsageOptions, "timeoutMs">,
32
+ signal?: AbortSignal,
33
+ ): Promise<QueryUsageResult> {
34
+ const errors: UsageQueryError[] = [];
35
+
36
+ try {
37
+ const report = await queryViaPiAuth(ctx, options.timeoutMs, signal);
38
+ return { ok: true, report };
39
+ } catch (cause) {
40
+ if (isStaleExtensionContextError(cause)) throw cause;
41
+ errors.push({ source: "pi-auth", message: safeCodexErrorMessage(cause) });
42
+ }
43
+
44
+ try {
45
+ const report = await queryViaCodexAppServer(options.timeoutMs, signal);
46
+ return { ok: true, report };
47
+ } catch (cause) {
48
+ if (isStaleExtensionContextError(cause)) throw cause;
49
+ errors.push({ source: "codex-app-server", message: safeCodexErrorMessage(cause) });
50
+ }
51
+
52
+ return { ok: false, errors };
53
+ }
54
+
55
+ async function queryViaPiAuth(
56
+ ctx: ExtensionContext,
57
+ timeoutMs: number,
58
+ signal?: AbortSignal,
59
+ ): Promise<CodexUsageReport> {
60
+ const auth = await resolvePiCodexAuth(ctx);
61
+ if (!auth) {
62
+ throw new Error(
63
+ "No Pi OpenAI Codex subscription auth was available. Use a Pi OpenAI Codex model or run /login for OpenAI ChatGPT Plus/Pro (Codex).",
64
+ );
65
+ }
66
+
67
+ const { response, text } = await fetchTextWithTimeout(
68
+ CODEX_USAGE_URL,
69
+ { method: "GET", headers: auth.headers, redirect: "error" },
70
+ timeoutMs,
71
+ signal,
72
+ );
73
+ if (!response.ok) {
74
+ throw new Error(`Codex usage endpoint returned HTTP ${response.status}.`);
75
+ }
76
+ const payload = parseJsonObject(text, "Codex usage endpoint response");
77
+ return normalizeBackendPayload(payload as RateLimitStatusPayload, Date.now(), "pi-auth");
78
+ }
79
+
80
+ async function resolvePiCodexAuth(
81
+ ctx: ExtensionContext,
82
+ ): Promise<{ headers: Record<string, string> } | undefined> {
83
+ const models = codexAuthCandidateModels(ctx);
84
+ const errors: string[] = [];
85
+
86
+ for (const model of models) {
87
+ const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
88
+ if (!auth.ok) {
89
+ errors.push(auth.error);
90
+ continue;
91
+ }
92
+
93
+ const headers = allowedAuthHeaders(auth.headers ?? {});
94
+ if (!hasHeader(headers, "Authorization") && auth.apiKey) {
95
+ headers.Authorization = `Bearer ${auth.apiKey}`;
96
+ }
97
+ if (!hasHeader(headers, "User-Agent")) {
98
+ headers["User-Agent"] = "pi-codex-usage";
99
+ }
100
+ if (hasHeader(headers, "Authorization")) {
101
+ return { headers };
102
+ }
103
+ }
104
+
105
+ if (errors.length > 0) {
106
+ throw new Error(errors.join("; "));
107
+ }
108
+ return undefined;
109
+ }
110
+
111
+ function codexAuthCandidateModels(ctx: ExtensionContext): PiModel[] {
112
+ const candidates: PiModel[] = [];
113
+ const seen = new Set<string>();
114
+ const add = (model: PiModel | undefined) => {
115
+ if (!model || model.provider !== CODEX_PROVIDER_ID) return;
116
+ const key = `${model.provider}/${model.id}`;
117
+ if (seen.has(key)) return;
118
+ seen.add(key);
119
+ candidates.push(model);
120
+ };
121
+
122
+ add(ctx.model);
123
+ for (const model of ctx.modelRegistry.getAvailable()) add(model);
124
+ for (const model of ctx.modelRegistry.getAll()) add(model);
125
+ return candidates;
126
+ }
127
+
128
+ async function fetchTextWithTimeout(
129
+ url: string,
130
+ init: RequestInit,
131
+ timeoutMs: number,
132
+ externalSignal?: AbortSignal,
133
+ ): Promise<{ response: Response; text: string }> {
134
+ const controller = new AbortController();
135
+ const abortFromExternal = () => controller.abort();
136
+ if (externalSignal?.aborted) controller.abort();
137
+ else externalSignal?.addEventListener("abort", abortFromExternal, { once: true });
138
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
139
+ try {
140
+ const response = await fetch(url, { ...init, signal: controller.signal });
141
+ const text = response.ok ? await readResponseText(response, MAX_RESPONSE_BODY_BYTES) : "";
142
+ return { response, text };
143
+ } catch (error) {
144
+ if (controller.signal.aborted) {
145
+ throw new Error(
146
+ externalSignal?.aborted
147
+ ? "Codex usage query was cancelled."
148
+ : `Timed out after ${Math.round(timeoutMs / 1000)}s while fetching Codex usage.`,
149
+ );
150
+ }
151
+ throw error;
152
+ } finally {
153
+ clearTimeout(timeout);
154
+ externalSignal?.removeEventListener("abort", abortFromExternal);
155
+ }
156
+ }
157
+
158
+ async function readResponseText(response: Response, maxBytes: number): Promise<string> {
159
+ if (!response.body) return "";
160
+ const reader = response.body.getReader();
161
+ const decoder = new TextDecoder();
162
+ let size = 0;
163
+ let text = "";
164
+ try {
165
+ while (true) {
166
+ const { done, value } = await reader.read();
167
+ if (done) break;
168
+ size += value.byteLength;
169
+ if (size > maxBytes) {
170
+ await reader.cancel("response size limit exceeded");
171
+ throw new Error("Codex usage response exceeded the size limit.");
172
+ }
173
+ text += decoder.decode(value, { stream: true });
174
+ }
175
+ return text + decoder.decode();
176
+ } finally {
177
+ reader.releaseLock();
178
+ }
179
+ }
180
+
181
+ function parseJsonObject(text: string, description: string): Record<string, unknown> {
182
+ let parsed: unknown;
183
+ try {
184
+ parsed = JSON.parse(text) as unknown;
185
+ } catch (error) {
186
+ throw new Error(`${description} was not valid JSON: ${errorMessage(error)}`);
187
+ }
188
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
189
+ throw new Error(`${description} was not an object.`);
190
+ }
191
+ return parsed as Record<string, unknown>;
192
+ }
193
+
194
+ function hasHeader(headers: Record<string, string>, name: string): boolean {
195
+ return Object.keys(headers).some((key) => key.toLowerCase() === name.toLowerCase());
196
+ }
197
+
198
+ function allowedAuthHeaders(source: Record<string, string>): Record<string, string> {
199
+ const allowed = new Set(["authorization", "chatgpt-account-id", "user-agent", "accept"]);
200
+ return Object.fromEntries(
201
+ Object.entries(source).filter(([key]) => allowed.has(key.toLowerCase())),
202
+ );
203
+ }
204
+
205
+ function errorMessage(error: unknown): string {
206
+ return safeCodexErrorMessage(error);
207
+ }
@@ -0,0 +1,27 @@
1
+ import { stripVTControlCharacters } from "node:util";
2
+
3
+ const BEARER_PATTERN = /Bearer\s+[A-Za-z0-9._~+/=-]+/gi;
4
+ const SECRET_FIELD_PATTERN =
5
+ /("?(?:access[_-]?token|refresh[_-]?token|id[_-]?token|session[_-]?token|client[_-]?secret|api[_-]?key|authorization|password)"?\s*[:=]\s*)(?:"[^"]*"|'[^']*'|[^\s,;]+)/gi;
6
+ const COOKIE_PATTERN = /((?:set-)?cookie\s*[:=]\s*)[^\r\n]*/gi;
7
+ const SECRET_QUERY_PATTERN =
8
+ /([?&](?:access_token|refresh_token|id_token|session_token|client_secret|api_key|password)=)[^&#\s]+/gi;
9
+
10
+ export function sanitizeCodexText(value: unknown, maxChars = 600): string {
11
+ let text = typeof value === "string" ? value : value instanceof Error ? value.message : String(value);
12
+ text = stripVTControlCharacters(text.slice(0, Math.max(maxChars * 4, maxChars)))
13
+ .replace(/[\u0000-\u001f\u007f-\u009f]/g, "");
14
+ text = text
15
+ .replace(BEARER_PATTERN, "Bearer <redacted>")
16
+ .replace(SECRET_FIELD_PATTERN, "$1<redacted>")
17
+ .replace(COOKIE_PATTERN, "$1<redacted>")
18
+ .replace(SECRET_QUERY_PATTERN, "$1<redacted>");
19
+ return text
20
+ .replace(/\s+/g, " ")
21
+ .trim()
22
+ .slice(0, maxChars);
23
+ }
24
+
25
+ export function safeCodexErrorMessage(error: unknown): string {
26
+ return sanitizeCodexText(error instanceof Error ? error.message : error);
27
+ }
@@ -0,0 +1,137 @@
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+
3
+ export type UsageSource = "pi-auth" | "codex-app-server";
4
+ export type PiModel = NonNullable<ExtensionContext["model"]>;
5
+ export type CodexUsageModel = Pick<PiModel, "id" | "name" | "provider">;
6
+
7
+ export type QueryUsageOptions = {
8
+ clearStatusline: boolean;
9
+ refresh: boolean;
10
+ statusline: boolean;
11
+ timeoutMs: number;
12
+ };
13
+
14
+ export type CachedReport = {
15
+ createdAt: number;
16
+ report: CodexUsageReport;
17
+ };
18
+
19
+ export type QueryUsageResult =
20
+ | { ok: true; report: CodexUsageReport }
21
+ | { ok: false; errors: UsageQueryError[] };
22
+
23
+ export type UsageQueryError = {
24
+ source: UsageSource;
25
+ message: string;
26
+ cause?: unknown;
27
+ };
28
+
29
+ export type CodexUsageReport = {
30
+ source: UsageSource;
31
+ capturedAt: number;
32
+ planType?: string;
33
+ snapshots: NormalizedRateLimitSnapshot[];
34
+ resetCredits?: NormalizedRateLimitResetCredits;
35
+ };
36
+
37
+ export type NormalizedRateLimitResetCredits = {
38
+ availableCount: number;
39
+ credits?: NormalizedRateLimitResetCredit[];
40
+ };
41
+
42
+ export type NormalizedRateLimitResetCredit = {
43
+ id: string;
44
+ resetType?: string;
45
+ status?: string;
46
+ grantedAt?: number;
47
+ expiresAt?: number;
48
+ title?: string;
49
+ description?: string;
50
+ };
51
+
52
+ export type NormalizedRateLimitSnapshot = {
53
+ limitId: string;
54
+ limitName?: string;
55
+ primary?: NormalizedRateLimitWindow;
56
+ secondary?: NormalizedRateLimitWindow;
57
+ };
58
+
59
+ export type NormalizedRateLimitWindow = {
60
+ usedPercent: number;
61
+ windowMinutes?: number;
62
+ resetsAt?: number;
63
+ };
64
+
65
+ export type RateLimitStatusPayload = {
66
+ plan_type?: unknown;
67
+ rate_limit?: unknown;
68
+ additional_rate_limits?: unknown;
69
+ rate_limit_reset_credits?: unknown;
70
+ };
71
+
72
+ export type BackendRateLimitResetCredits = {
73
+ available_count?: unknown;
74
+ };
75
+
76
+ export type BackendRateLimitDetails = {
77
+ primary_window?: unknown;
78
+ secondary_window?: unknown;
79
+ };
80
+
81
+ export type BackendWindowSnapshot = {
82
+ used_percent?: unknown;
83
+ limit_window_seconds?: unknown;
84
+ reset_at?: unknown;
85
+ };
86
+
87
+ export type BackendAdditionalRateLimit = {
88
+ limit_name?: unknown;
89
+ metered_feature?: unknown;
90
+ rate_limit?: unknown;
91
+ };
92
+
93
+ export type AppServerRateLimitResponse = {
94
+ rateLimits?: unknown;
95
+ rateLimitsByLimitId?: unknown;
96
+ rateLimitResetCredits?: unknown;
97
+ };
98
+
99
+ export type AppServerRateLimitResetCredits = {
100
+ availableCount?: unknown;
101
+ credits?: unknown;
102
+ };
103
+
104
+ export type AppServerRateLimitResetCredit = {
105
+ id?: unknown;
106
+ resetType?: unknown;
107
+ status?: unknown;
108
+ grantedAt?: unknown;
109
+ expiresAt?: unknown;
110
+ title?: unknown;
111
+ description?: unknown;
112
+ };
113
+
114
+ export type AppServerRateLimitSnapshot = {
115
+ limitId?: unknown;
116
+ limitName?: unknown;
117
+ primary?: unknown;
118
+ secondary?: unknown;
119
+ planType?: unknown;
120
+ };
121
+
122
+ export type AppServerWindowSnapshot = {
123
+ usedPercent?: unknown;
124
+ windowDurationMins?: unknown;
125
+ resetsAt?: unknown;
126
+ };
127
+
128
+ export type RpcResponse = {
129
+ id?: unknown;
130
+ result?: unknown;
131
+ error?: { message?: unknown; code?: unknown };
132
+ };
133
+
134
+ export type PendingRpc = {
135
+ resolve: (value: unknown) => void;
136
+ reject: (error: Error) => void;
137
+ };