@centerforagenticai/pi-multi-account 0.1.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.
Files changed (124) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +29 -0
  3. package/README.md +999 -0
  4. package/config/models/pi-multi-account.v1.json +32 -0
  5. package/config/subscription-plans.v1.json +122 -0
  6. package/package.json +76 -0
  7. package/packages/pi-anthropic-oauth/LICENSE +21 -0
  8. package/packages/pi-anthropic-oauth/package.json +54 -0
  9. package/packages/pi-anthropic-oauth/src/auth.ts +396 -0
  10. package/packages/pi-anthropic-oauth/src/context.ts +116 -0
  11. package/packages/pi-anthropic-oauth/src/convert.ts +303 -0
  12. package/packages/pi-anthropic-oauth/src/index.ts +37 -0
  13. package/packages/pi-anthropic-oauth/src/prompt.ts +137 -0
  14. package/packages/pi-anthropic-oauth/src/stream.ts +476 -0
  15. package/packages/pi-antigravity/LICENSE +21 -0
  16. package/packages/pi-antigravity/package.json +77 -0
  17. package/packages/pi-antigravity/src/auth/index.ts +14 -0
  18. package/packages/pi-antigravity/src/auth/oauth.ts +442 -0
  19. package/packages/pi-antigravity/src/client/client.ts +561 -0
  20. package/packages/pi-antigravity/src/client/index.ts +1 -0
  21. package/packages/pi-antigravity/src/context.ts +110 -0
  22. package/packages/pi-antigravity/src/diagnostics/diagnostics.ts +96 -0
  23. package/packages/pi-antigravity/src/diagnostics/index.ts +1 -0
  24. package/packages/pi-antigravity/src/image/image.ts +336 -0
  25. package/packages/pi-antigravity/src/image/index.ts +1 -0
  26. package/packages/pi-antigravity/src/index.ts +280 -0
  27. package/packages/pi-antigravity/src/models/discovery.ts +154 -0
  28. package/packages/pi-antigravity/src/models/grouping.ts +424 -0
  29. package/packages/pi-antigravity/src/models/index.ts +3 -0
  30. package/packages/pi-antigravity/src/models/models.ts +500 -0
  31. package/packages/pi-antigravity/src/stream/index.ts +1 -0
  32. package/packages/pi-antigravity/src/stream/stream.ts +1478 -0
  33. package/packages/pi-antigravity/src/types/enums.ts +42 -0
  34. package/packages/pi-antigravity/src/types/index.ts +2 -0
  35. package/packages/pi-antigravity/src/types/types.ts +292 -0
  36. package/packages/pi-antigravity/src/usage/index.ts +1 -0
  37. package/packages/pi-antigravity/src/usage/usage.ts +416 -0
  38. package/packages/pi-antigravity/src/utils/http.ts +91 -0
  39. package/packages/pi-antigravity/src/utils/index.ts +3 -0
  40. package/packages/pi-antigravity/src/utils/security.ts +73 -0
  41. package/packages/pi-antigravity/src/utils/util.ts +132 -0
  42. package/scripts/multi-account.mjs +44 -0
  43. package/src/account-labels.ts +223 -0
  44. package/src/account-plan-assignment.ts +340 -0
  45. package/src/account-rate-history.ts +372 -0
  46. package/src/anthropic-adaptive-stream.ts +531 -0
  47. package/src/anthropic-alias-stream.ts +140 -0
  48. package/src/anthropic-context-compat.ts +80 -0
  49. package/src/api-pricing.ts +579 -0
  50. package/src/bounded-file-lines.ts +97 -0
  51. package/src/catalog-rebinding.ts +177 -0
  52. package/src/catalog-registration-probe.ts +111 -0
  53. package/src/codex-adapter.ts +345 -0
  54. package/src/codex-model-defaults.ts +785 -0
  55. package/src/command-completions.ts +404 -0
  56. package/src/commands.ts +2000 -0
  57. package/src/compaction.ts +14 -0
  58. package/src/config.ts +1317 -0
  59. package/src/continuation.ts +569 -0
  60. package/src/cooldowns.ts +110 -0
  61. package/src/cost-digest-store.ts +332 -0
  62. package/src/cost-digest.ts +1044 -0
  63. package/src/cost-history.ts +251 -0
  64. package/src/cost-period-closer.ts +160 -0
  65. package/src/cost-report-json.ts +318 -0
  66. package/src/cost-report-reader.ts +368 -0
  67. package/src/cost-report-render.ts +207 -0
  68. package/src/cost-report.ts +1104 -0
  69. package/src/coverage-attestation.ts +397 -0
  70. package/src/credential-lifecycle.ts +169 -0
  71. package/src/credential-refresh.ts +248 -0
  72. package/src/declaration-notice-marker.ts +238 -0
  73. package/src/diagnostic-store.ts +276 -0
  74. package/src/diagnostics.ts +309 -0
  75. package/src/discovery.ts +471 -0
  76. package/src/duration.ts +13 -0
  77. package/src/error-classification.ts +256 -0
  78. package/src/fuzzy.ts +15 -0
  79. package/src/group-policy.ts +81 -0
  80. package/src/history-store.ts +897 -0
  81. package/src/index.ts +5572 -0
  82. package/src/lifecycle.ts +378 -0
  83. package/src/logical-dispatch.ts +279 -0
  84. package/src/logical-model-selector.ts +254 -0
  85. package/src/logical-model-switcher.ts +430 -0
  86. package/src/logical-provider-attribution.ts +544 -0
  87. package/src/logical-provider.ts +1237 -0
  88. package/src/logical-route-indicator.ts +215 -0
  89. package/src/machine-lease.ts +445 -0
  90. package/src/model-support.ts +66 -0
  91. package/src/models-declaration.ts +1091 -0
  92. package/src/openai-adapter.ts +117 -0
  93. package/src/openrouter-budget.ts +304 -0
  94. package/src/openrouter-fallback.ts +146 -0
  95. package/src/period-boundaries.ts +376 -0
  96. package/src/pi-anthropic-oauth.d.ts +6 -0
  97. package/src/preflight.ts +253 -0
  98. package/src/pricing-cache.ts +235 -0
  99. package/src/project-identity.ts +100 -0
  100. package/src/provider-registration.ts +942 -0
  101. package/src/rate-formula.ts +163 -0
  102. package/src/recovery-engine.ts +853 -0
  103. package/src/recovery-output.ts +837 -0
  104. package/src/recovery-plan.ts +239 -0
  105. package/src/report-range.ts +203 -0
  106. package/src/route-resolver.ts +789 -0
  107. package/src/routing-config-transaction.ts +232 -0
  108. package/src/routing.ts +1163 -0
  109. package/src/runtime-state.ts +630 -0
  110. package/src/session-account-groups.ts +284 -0
  111. package/src/session-restore.ts +287 -0
  112. package/src/shared-usage.ts +1392 -0
  113. package/src/standalone-cli.ts +720 -0
  114. package/src/status-view.ts +578 -0
  115. package/src/subscription-plan-catalog.ts +346 -0
  116. package/src/tier-model-resolver.ts +46 -0
  117. package/src/upstream-anthropic.ts +315 -0
  118. package/src/upstream-antigravity.ts +327 -0
  119. package/src/usage-fetch.ts +1634 -0
  120. package/src/usage.ts +1026 -0
  121. package/src/vendor.ts +87 -0
  122. package/src/warmer.ts +231 -0
  123. package/src/watchdog.ts +219 -0
  124. package/src/window-history.ts +270 -0
@@ -0,0 +1,276 @@
1
+ import {
2
+ appendFileSync,
3
+ chmodSync,
4
+ mkdirSync,
5
+ readFileSync,
6
+ renameSync,
7
+ statSync,
8
+ unlinkSync,
9
+ writeFileSync,
10
+ } from "node:fs";
11
+ import { randomUUID } from "node:crypto";
12
+ import { dirname, join } from "node:path";
13
+ import {
14
+ sanitizeDiagnosticText,
15
+ sanitizeForJson,
16
+ type DiagnosticEvent,
17
+ type DiagnosticLevel,
18
+ type DiagnosticPersistence,
19
+ } from "./diagnostics.js";
20
+ import { acquireMachineLease } from "./machine-lease.js";
21
+
22
+ export const DEFAULT_DIAGNOSTIC_MAX_BYTES = 1024 * 1024;
23
+ const MAX_DIAGNOSTIC_RECORD_BYTES = 64 * 1024;
24
+ const MAX_PERSISTED_FIELDS = 64;
25
+ const DIAGNOSTIC_LEASE_TTL_MS = 5_000;
26
+ const LEVELS = new Set<DiagnosticLevel>(["info", "warning", "error"]);
27
+
28
+ export interface DiagnosticStorePaths {
29
+ readonly path: string;
30
+ readonly lockPath: string;
31
+ }
32
+
33
+ export function defaultDiagnosticStorePaths(): DiagnosticStorePaths {
34
+ const agentDir =
35
+ process.env.PI_CODING_AGENT_DIR ??
36
+ join(process.env.HOME ?? ".", ".pi", "agent");
37
+ // Package storage identity stays decoupled from the logical provider ID.
38
+ const directory = join(agentDir, "pi-multi-account");
39
+ return {
40
+ path: join(directory, "diagnostics.ndjson"),
41
+ lockPath: join(directory, "diagnostics.ndjson.lock"),
42
+ };
43
+ }
44
+
45
+ function completePrefixLength(raw: string): number {
46
+ return raw.endsWith("\n") ? raw.length : raw.lastIndexOf("\n") + 1;
47
+ }
48
+
49
+ function validTimestamp(value: unknown): value is number {
50
+ return (
51
+ typeof value === "number" &&
52
+ Number.isSafeInteger(value) &&
53
+ value >= 0
54
+ );
55
+ }
56
+
57
+ function projectFields(value: unknown): Readonly<Record<string, string>> | undefined {
58
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
59
+ return undefined;
60
+ }
61
+ const sanitized = sanitizeForJson(value);
62
+ if (typeof sanitized !== "object" || sanitized === null || Array.isArray(sanitized)) {
63
+ return undefined;
64
+ }
65
+ const fields: Record<string, string> = {};
66
+ for (const [key, fieldValue] of Object.entries(sanitized).slice(
67
+ 0,
68
+ MAX_PERSISTED_FIELDS,
69
+ )) {
70
+ if (typeof fieldValue !== "string") continue;
71
+ fields[sanitizeDiagnosticText(key)] = sanitizeDiagnosticText(fieldValue);
72
+ }
73
+ return fields;
74
+ }
75
+
76
+ function projectEvent(value: unknown): DiagnosticEvent | undefined {
77
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
78
+ return undefined;
79
+ }
80
+ const candidate = value as Record<string, unknown>;
81
+ const allowed = new Set(["timestampMs", "level", "category", "message", "fields"]);
82
+ if (Object.keys(candidate).some((key) => !allowed.has(key))) return undefined;
83
+ if (!validTimestamp(candidate.timestampMs)) return undefined;
84
+ if (typeof candidate.level !== "string" || !LEVELS.has(candidate.level as DiagnosticLevel)) {
85
+ return undefined;
86
+ }
87
+ const fields = projectFields(candidate.fields);
88
+ if (fields === undefined) return undefined;
89
+ return {
90
+ timestampMs: candidate.timestampMs,
91
+ level: candidate.level as DiagnosticLevel,
92
+ category: sanitizeDiagnosticText(candidate.category),
93
+ message: sanitizeDiagnosticText(candidate.message),
94
+ fields,
95
+ };
96
+ }
97
+
98
+ function parseCompleteEvents(raw: string): readonly DiagnosticEvent[] {
99
+ const prefix = raw.slice(0, completePrefixLength(raw));
100
+ const events: DiagnosticEvent[] = [];
101
+ for (const line of prefix.split("\n")) {
102
+ if (!line.trim()) continue;
103
+ try {
104
+ const event = projectEvent(JSON.parse(line));
105
+ if (event !== undefined) events.push(event);
106
+ } catch {
107
+ // A malformed or torn record is isolated from later complete records.
108
+ }
109
+ }
110
+ return events;
111
+ }
112
+
113
+ function newestLinesWithin(
114
+ events: readonly DiagnosticEvent[],
115
+ budgetBytes: number,
116
+ ): string {
117
+ const retained: string[] = [];
118
+ let used = 0;
119
+ for (let index = events.length - 1; index >= 0; index -= 1) {
120
+ const line = `${JSON.stringify(events[index])}\n`;
121
+ const bytes = Buffer.byteLength(line, "utf8");
122
+ if (bytes > budgetBytes - used) break;
123
+ retained.push(line);
124
+ used += bytes;
125
+ }
126
+ return retained.reverse().join("");
127
+ }
128
+
129
+ /** Machine-global, credential-free, bounded diagnostic event history. */
130
+ export class DiagnosticStore implements DiagnosticPersistence {
131
+ readonly #path: string;
132
+ readonly #lockPath: string;
133
+ readonly #maxBytes: number;
134
+ readonly #beforeCompactionRename: (() => void) | undefined;
135
+
136
+ constructor(options: {
137
+ readonly path?: string;
138
+ readonly lockPath?: string;
139
+ readonly maxBytes?: number;
140
+ /** Test seam for a peer append immediately before the stable-tail sample. */
141
+ readonly beforeCompactionRename?: () => void;
142
+ } = {}) {
143
+ const defaults = defaultDiagnosticStorePaths();
144
+ this.#path = options.path ?? defaults.path;
145
+ this.#lockPath = options.lockPath ?? defaults.lockPath;
146
+ this.#maxBytes = options.maxBytes ?? DEFAULT_DIAGNOSTIC_MAX_BYTES;
147
+ this.#beforeCompactionRename = options.beforeCompactionRename;
148
+ if (
149
+ !Number.isSafeInteger(this.#maxBytes) ||
150
+ this.#maxBytes < 1_024 ||
151
+ this.#maxBytes > 64 * 1024 * 1024
152
+ ) {
153
+ throw new RangeError("maxBytes must be an integer from 1024 through 67108864.");
154
+ }
155
+ }
156
+
157
+ path(): string {
158
+ return this.#path;
159
+ }
160
+
161
+ readRecent(limit: number): readonly DiagnosticEvent[] {
162
+ const safeLimit = Number.isSafeInteger(limit)
163
+ ? Math.max(0, Math.min(limit, 1_000))
164
+ : 100;
165
+ if (safeLimit === 0) return [];
166
+ try {
167
+ return parseCompleteEvents(readFileSync(this.#path, "utf8")).slice(-safeLimit);
168
+ } catch {
169
+ return [];
170
+ }
171
+ }
172
+
173
+ append(event: DiagnosticEvent): boolean {
174
+ try {
175
+ const projected = projectEvent(event);
176
+ if (projected === undefined) return false;
177
+ const line = `${JSON.stringify(projected)}\n`;
178
+ if (Buffer.byteLength(line, "utf8") > MAX_DIAGNOSTIC_RECORD_BYTES) {
179
+ return false;
180
+ }
181
+ const directory = dirname(this.#path);
182
+ mkdirSync(directory, { recursive: true, mode: 0o700 });
183
+ chmodSync(directory, 0o700);
184
+ let separator = "";
185
+ try {
186
+ const current = readFileSync(this.#path, "utf8");
187
+ if (current.length > 0 && !current.endsWith("\n")) separator = "\n";
188
+ } catch {
189
+ // appendFileSync creates a missing file below.
190
+ }
191
+ appendFileSync(this.#path, `${separator}${line}`, {
192
+ encoding: "utf8",
193
+ mode: 0o600,
194
+ });
195
+ chmodSync(this.#path, 0o600);
196
+ this.#compactIfNeeded();
197
+ return true;
198
+ } catch {
199
+ return false;
200
+ }
201
+ }
202
+
203
+ #compactIfNeeded(): boolean {
204
+ try {
205
+ if (statSync(this.#path).size <= this.#maxBytes) return false;
206
+ } catch {
207
+ return false;
208
+ }
209
+ let lease: ReturnType<typeof acquireMachineLease>;
210
+ try {
211
+ lease = acquireMachineLease({
212
+ lockPath: this.#lockPath,
213
+ ttlMs: DIAGNOSTIC_LEASE_TTL_MS,
214
+ // A wedged lock would otherwise stop compaction and let the
215
+ // diagnostics log grow past its bound forever.
216
+ reclaimMalformed: true,
217
+ });
218
+ } catch {
219
+ return false;
220
+ }
221
+ if (lease === undefined) return false;
222
+ let temporaryPath: string | undefined;
223
+ try {
224
+ if (!lease.renew()) return false;
225
+ const initialRaw = readFileSync(this.#path, "utf8");
226
+ const prefixLength = completePrefixLength(initialRaw);
227
+ const completePrefix = initialRaw.slice(0, prefixLength);
228
+ const events = parseCompleteEvents(completePrefix);
229
+ this.#beforeCompactionRename?.();
230
+
231
+ let tail: string | undefined;
232
+ for (let snapshot = 0; snapshot < 3; snapshot += 1) {
233
+ const before = statSync(this.#path);
234
+ const latestRaw = readFileSync(this.#path, "utf8");
235
+ const after = statSync(this.#path);
236
+ if (
237
+ before.dev !== after.dev ||
238
+ before.ino !== after.ino ||
239
+ before.size !== after.size
240
+ ) {
241
+ continue;
242
+ }
243
+ if (!latestRaw.startsWith(completePrefix)) return false;
244
+ tail = latestRaw.slice(prefixLength);
245
+ break;
246
+ }
247
+ if (tail === undefined) return false;
248
+ const tailBytes = Buffer.byteLength(tail, "utf8");
249
+ if (tailBytes > this.#maxBytes) return false;
250
+ const targetBytes = Math.min(
251
+ Math.floor(this.#maxBytes / 2),
252
+ this.#maxBytes - tailBytes,
253
+ );
254
+ const compacted = newestLinesWithin(events, targetBytes);
255
+ const encoded = compacted + tail;
256
+ if (Buffer.byteLength(encoded, "utf8") > this.#maxBytes) return false;
257
+ temporaryPath = `${this.#path}.${process.pid}.${randomUUID()}.tmp`;
258
+ writeFileSync(temporaryPath, encoded, { encoding: "utf8", mode: 0o600 });
259
+ renameSync(temporaryPath, this.#path);
260
+ temporaryPath = undefined;
261
+ chmodSync(this.#path, 0o600);
262
+ return true;
263
+ } catch {
264
+ return false;
265
+ } finally {
266
+ if (temporaryPath !== undefined) {
267
+ try {
268
+ unlinkSync(temporaryPath);
269
+ } catch {
270
+ // Best-effort cleanup of an uncommitted compacted snapshot.
271
+ }
272
+ }
273
+ lease.release();
274
+ }
275
+ }
276
+ }
@@ -0,0 +1,309 @@
1
+ const MAX_DIAGNOSTIC_LENGTH = 512;
2
+ const REDACTED = "[REDACTED]";
3
+ const SENSITIVE_NAME_FRAGMENT = /(?:authorization|authentication|auth|token|credential|secret|cookie|apikey)/;
4
+ const PRIVATE_REASONING_KEY = /^(?:thinking|reasoning)/;
5
+ const TOKEN_QUOTA_REMAINING_HEADERS = new Set([
6
+ "x-ratelimit-remaining-tokens",
7
+ "x-ratelimit-tokens-remaining",
8
+ "anthropic-ratelimit-tokens-remaining",
9
+ ]);
10
+ const TOKEN_QUOTA_DURATION_RESET_HEADERS = new Set([
11
+ "x-ratelimit-reset-tokens",
12
+ ]);
13
+ const TOKEN_QUOTA_ABSOLUTE_RESET_HEADERS = new Set([
14
+ "anthropic-ratelimit-tokens-reset",
15
+ ]);
16
+
17
+ export type DiagnosticLevel = "info" | "warning" | "error";
18
+
19
+ export interface DiagnosticEvent {
20
+ readonly timestampMs: number;
21
+ readonly level: DiagnosticLevel;
22
+ readonly category: string;
23
+ readonly message: string;
24
+ readonly fields: Readonly<Record<string, string>>;
25
+ }
26
+
27
+ /** Credential-free persistence seam; implementations must fail soft. */
28
+ export interface DiagnosticPersistence {
29
+ readRecent(limit: number): readonly DiagnosticEvent[];
30
+ append(event: DiagnosticEvent): boolean;
31
+ }
32
+
33
+ function bound(value: string): string {
34
+ return value.slice(0, MAX_DIAGNOSTIC_LENGTH);
35
+ }
36
+
37
+ function safeString(value: unknown): string {
38
+ if (typeof value === "string") return value;
39
+ try {
40
+ return String(value);
41
+ } catch {
42
+ return "[UNAVAILABLE]";
43
+ }
44
+ }
45
+
46
+ function redactDiagnosticText(value: unknown): string {
47
+ return safeString(value)
48
+ .replace(
49
+ /\b(?:proxy[-_\s]*)?authorization\b["']?\s*[:=]\s*(?:["'][^"']*["']|(?:bearer|basic)\s+[^\s,;}&]+|[^\s,;}&]+)/gi,
50
+ REDACTED,
51
+ )
52
+ .replace(/\b(?:bearer|basic)\s+[^\s,;]+/gi, REDACTED)
53
+ .replace(
54
+ /\b(?:(?:x[-_\s]+)?api[-_\s]*key|access[-_\s]*token|refresh[-_\s]*token|id[-_\s]*token)\b["']?\s*[:=]\s*(?:["'][^"']*["']|[^\s,;}&]+)/gi,
55
+ REDACTED,
56
+ )
57
+ .replace(/\b(?:sk|rk|pk)-(?:ant-)?[A-Za-z0-9_-]{8,}\b/g, REDACTED)
58
+ .replace(/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g, REDACTED)
59
+ .replace(/\b(?:(?:test|synthetic)[-_]?)?(?:sk|token|canary)(?:[-_:][A-Za-z0-9._-]+)+\b/gi, REDACTED)
60
+ .replace(/<(?:thinking|reasoning)>[\s\S]*?<\/(?:thinking|reasoning)>/gi, REDACTED)
61
+ .replace(
62
+ /\b(?:thinking|reasoning)(?:[-_\s]*(?:content|text|detail|data|signature))?\b["']?\s*[:=]\s*(?:["'][^"']*["']|[^\n,;}&]+)/gi,
63
+ REDACTED,
64
+ )
65
+ .replace(/\b[A-Za-z0-9_-]{80,}\b/g, REDACTED);
66
+ }
67
+
68
+ function normalizedName(value: string): string {
69
+ return value.toLowerCase().replace(/[^a-z0-9]/g, "");
70
+ }
71
+
72
+ function isSensitiveName(name: string): boolean {
73
+ const normalized = normalizedName(name);
74
+ return SENSITIVE_NAME_FRAGMENT.test(normalized) || PRIVATE_REASONING_KEY.test(normalized);
75
+ }
76
+
77
+ function validTokenCount(value: string): boolean {
78
+ if (value.length > 32 || !/^\d+$/.test(value)) return false;
79
+ return Number.isSafeInteger(Number(value));
80
+ }
81
+
82
+ function validDuration(value: string): boolean {
83
+ if (value.length > 64) return false;
84
+ const matches = [...value.matchAll(/(\d+(?:\.\d+)?)(ms|s|m|h)/g)];
85
+ if (matches.length === 0 || matches.map((match) => match[0]).join("") !== value) {
86
+ return false;
87
+ }
88
+ let milliseconds = 0;
89
+ for (const match of matches) {
90
+ let multiplier = 1;
91
+ switch (match[2]) {
92
+ case "h": multiplier = 3_600_000; break;
93
+ case "m": multiplier = 60_000; break;
94
+ case "s": multiplier = 1_000; break;
95
+ }
96
+ milliseconds += Number(match[1]) * multiplier;
97
+ if (!Number.isSafeInteger(Math.ceil(milliseconds)) || milliseconds < 0) return false;
98
+ }
99
+ return true;
100
+ }
101
+
102
+ function validAbsoluteReset(value: string): boolean {
103
+ if (value.length > 128) return false;
104
+
105
+ const epoch = /^(?<seconds>[1-9]\d{9})(?:\.(?<fraction>\d{1,3}))?$/.exec(value);
106
+ if (epoch?.groups) {
107
+ const epochMs = Number(epoch.groups["seconds"]) * 1_000
108
+ + Number((epoch.groups["fraction"] ?? "").padEnd(3, "0"));
109
+ if (!Number.isSafeInteger(epochMs)) return false;
110
+ const fraction = epochMs % 1_000;
111
+ const canonical = `${Math.floor(epochMs / 1_000)}${
112
+ fraction === 0 ? "" : `.${String(fraction).padStart(3, "0").replace(/0+$/, "")}`
113
+ }`;
114
+ return canonical === value;
115
+ }
116
+
117
+ const iso = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/.exec(value);
118
+ if (iso) {
119
+ const epochMs = Date.parse(value);
120
+ if (!Number.isFinite(epochMs) || epochMs < 0) return false;
121
+ const canonical = new Date(epochMs).toISOString();
122
+ return canonical === (value.includes(".") ? value : value.replace(/Z$/, ".000Z"));
123
+ }
124
+
125
+ const rfc1123 = /^(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun), (?:0[1-9]|[12]\d|3[01]) (?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) \d{4} (?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d GMT$/.exec(value);
126
+ if (!rfc1123) return false;
127
+ const epochMs = Date.parse(value);
128
+ return Number.isFinite(epochMs) && epochMs >= 0 && new Date(epochMs).toUTCString() === value;
129
+ }
130
+
131
+ function validatedTokenQuotaValue(name: string, value: unknown): string | undefined {
132
+ const normalized = name.toLowerCase();
133
+ const isTokenQuotaHeader = TOKEN_QUOTA_REMAINING_HEADERS.has(normalized)
134
+ || TOKEN_QUOTA_DURATION_RESET_HEADERS.has(normalized)
135
+ || TOKEN_QUOTA_ABSOLUTE_RESET_HEADERS.has(normalized);
136
+ if (!isTokenQuotaHeader || typeof value !== "string") return undefined;
137
+
138
+ const trimmed = value.trim();
139
+ if (value !== trimmed) return undefined;
140
+ if (TOKEN_QUOTA_REMAINING_HEADERS.has(normalized)) {
141
+ return validTokenCount(trimmed) ? trimmed : undefined;
142
+ }
143
+ if (TOKEN_QUOTA_DURATION_RESET_HEADERS.has(normalized)) {
144
+ return validDuration(trimmed) || validAbsoluteReset(trimmed) ? trimmed : undefined;
145
+ }
146
+ return validAbsoluteReset(trimmed) ? trimmed : undefined;
147
+ }
148
+
149
+ /** Sanitizes and bounds one upstream-derived diagnostic value before storage. */
150
+ export function sanitizeDiagnosticText(value: unknown): string {
151
+ return bound(redactDiagnosticText(value));
152
+ }
153
+
154
+ /** Sensitive headers are replaced wholesale except for strictly validated token-quota observations. */
155
+ export function sanitizeHeaderValue(name: string, value: unknown): string {
156
+ const tokenQuotaValue = validatedTokenQuotaValue(name, value);
157
+ if (tokenQuotaValue !== undefined) return tokenQuotaValue;
158
+ return isSensitiveName(name) ? REDACTED : sanitizeDiagnosticText(value);
159
+ }
160
+
161
+ function sanitizeFields(value: unknown, depth = 0): unknown {
162
+ if (depth > 5) return "[TRUNCATED]";
163
+ // Preserve JSON's native absence semantics: `undefined` is omitted from
164
+ // objects and rendered as null in arrays by JSON.stringify. Returning it
165
+ // verbatim (rather than falling through to sanitizeDiagnosticText, which
166
+ // would coerce it to the literal string "undefined") keeps optional status
167
+ // fields like coolingUntilMs / usage absent instead of a bogus string.
168
+ if (value === undefined) return undefined;
169
+ if (value === null || typeof value === "boolean" || typeof value === "number") return value;
170
+ if (typeof value === "string" || typeof value === "bigint" || typeof value === "symbol") {
171
+ return sanitizeDiagnosticText(value);
172
+ }
173
+ if (value instanceof Error) {
174
+ try {
175
+ return sanitizeDiagnosticText(value.message);
176
+ } catch {
177
+ return "[UNAVAILABLE]";
178
+ }
179
+ }
180
+ if (Array.isArray(value)) return value.slice(0, 50).map((entry) => sanitizeFields(entry, depth + 1));
181
+ if (typeof value !== "object") return sanitizeDiagnosticText(value);
182
+
183
+ const projected: Record<string, unknown> = {};
184
+ for (const [key, entry] of Object.entries(value).slice(0, 50)) {
185
+ const sanitized = isSensitiveName(key)
186
+ ? REDACTED
187
+ : sanitizeFields(entry, depth + 1);
188
+ // Omit undefined-valued keys entirely, matching JSON.stringify's native
189
+ // object semantics (an absent field, not a key mapped to "undefined").
190
+ if (sanitized === undefined) continue;
191
+ projected[sanitizeDiagnosticText(key)] = sanitized;
192
+ }
193
+ return projected;
194
+ }
195
+
196
+ /** Produces a detached, sanitized JSON value; the input object is never retained. */
197
+ export function sanitizeForJson(value: unknown): unknown {
198
+ return sanitizeFields(value);
199
+ }
200
+
201
+ export function sanitizedJson(value: unknown): string {
202
+ return JSON.stringify(sanitizeForJson(value));
203
+ }
204
+
205
+ export class DiagnosticLog {
206
+ readonly #events: DiagnosticEvent[] = [];
207
+ readonly #maxEvents: number;
208
+ readonly #now: () => number;
209
+ readonly #persistence: DiagnosticPersistence | undefined;
210
+
211
+ constructor(options: {
212
+ maxEvents?: number;
213
+ now?: () => number;
214
+ persistence?: DiagnosticPersistence;
215
+ } = {}) {
216
+ this.#maxEvents = options.maxEvents ?? 100;
217
+ this.#now = options.now ?? Date.now;
218
+ this.#persistence = options.persistence;
219
+ if (!Number.isSafeInteger(this.#maxEvents) || this.#maxEvents < 1 || this.#maxEvents > 1_000) {
220
+ throw new RangeError("maxEvents must be an integer from 1 through 1000.");
221
+ }
222
+ try {
223
+ const persisted = this.#persistence?.readRecent(this.#maxEvents) ?? [];
224
+ for (const event of persisted.slice(-this.#maxEvents)) {
225
+ this.#events.push(Object.freeze({
226
+ ...event,
227
+ fields: Object.freeze({ ...event.fields }),
228
+ }));
229
+ }
230
+ } catch {
231
+ // Diagnostics persistence is best-effort and must never block startup.
232
+ }
233
+ }
234
+
235
+ record(
236
+ level: DiagnosticLevel,
237
+ category: string,
238
+ message: unknown,
239
+ fields: Readonly<Record<string, unknown>> = {},
240
+ ): DiagnosticEvent {
241
+ const safeFields: Record<string, string> = {};
242
+ for (const [key, value] of Object.entries(fields)) {
243
+ safeFields[sanitizeDiagnosticText(key)] = sanitizeDiagnosticText(value);
244
+ }
245
+ const event = Object.freeze({
246
+ timestampMs: this.#now(),
247
+ level,
248
+ category: sanitizeDiagnosticText(category),
249
+ message: sanitizeDiagnosticText(message),
250
+ fields: Object.freeze(safeFields),
251
+ });
252
+ this.#events.push(event);
253
+ if (this.#events.length > this.#maxEvents) this.#events.splice(0, this.#events.length - this.#maxEvents);
254
+ try {
255
+ this.#persistence?.append(event);
256
+ } catch {
257
+ // Recording remains available in memory even when persistence fails.
258
+ }
259
+ return event;
260
+ }
261
+
262
+ recordError(category: string, _error: unknown): DiagnosticEvent {
263
+ // Generic errors can originate at provider, OAuth, model-selection, fetch,
264
+ // request, or extension boundaries. Their messages may therefore contain
265
+ // endpoint bodies, credential material, request payloads, or conversation
266
+ // content. Persist the bounded failure fact and category only; callers that
267
+ // possess safe structured facts record those separately.
268
+ return this.record(
269
+ "error",
270
+ category,
271
+ "An error occurred; raw details were omitted.",
272
+ );
273
+ }
274
+
275
+ recordHeaders(category: string, headers: Readonly<Record<string, unknown>>): DiagnosticEvent {
276
+ const safeHeaders: Record<string, string> = {};
277
+ for (const [name, value] of Object.entries(headers)) {
278
+ safeHeaders[sanitizeDiagnosticText(name)] = sanitizeHeaderValue(name, value);
279
+ }
280
+ return this.record("info", category, "Provider response headers observed.", safeHeaders);
281
+ }
282
+
283
+ recent(limit = 20): readonly DiagnosticEvent[] {
284
+ const safeLimit = Number.isSafeInteger(limit) ? Math.max(0, Math.min(limit, 100)) : 20;
285
+ return this.#events.slice(-safeLimit).map((event) => ({ ...event, fields: { ...event.fields } }));
286
+ }
287
+
288
+ formatRecent(limit = 20): string {
289
+ return this.recent(limit)
290
+ .map((event) => `${event.level} ${event.category}: ${event.message} ${sanitizedJson(event.fields)}`)
291
+ .join("\n");
292
+ }
293
+
294
+ serialize(): string {
295
+ return sanitizedJson(this.#events);
296
+ }
297
+
298
+ sanitizeOutput(value: unknown): string {
299
+ // Aggregate command/status output may contain many independently bounded
300
+ // values. Redact it again at the final sink without truncating the aggregate.
301
+ return redactDiagnosticText(value);
302
+ }
303
+
304
+ clear(): void {
305
+ this.#events.length = 0;
306
+ }
307
+ }
308
+
309
+ export { MAX_DIAGNOSTIC_LENGTH, REDACTED };