@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,235 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import {
3
+ chmodSync,
4
+ closeSync,
5
+ fsyncSync,
6
+ mkdirSync,
7
+ openSync,
8
+ readFileSync,
9
+ renameSync,
10
+ statSync,
11
+ unlinkSync,
12
+ writeFileSync,
13
+ } from "node:fs";
14
+ import { homedir } from "node:os";
15
+ import { basename, dirname, join } from "node:path";
16
+ import {
17
+ parseOpenRouterRateSnapshot,
18
+ parseStoredApiRateSnapshot,
19
+ type ApiRateSnapshot,
20
+ } from "./api-pricing.js";
21
+ import { acquireMachineLease } from "./machine-lease.js";
22
+
23
+ export const OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models";
24
+ export const PRICING_REFRESH_INTERVAL_MS = 6 * 60 * 60 * 1_000;
25
+ const PRICING_FETCH_TIMEOUT_MS = 10_000;
26
+ const MAX_RESPONSE_BYTES = 2_000_000;
27
+ const MAX_CACHE_BYTES = 1_000_000;
28
+
29
+ interface PricingFetchResponse {
30
+ readonly ok: boolean;
31
+ readonly status: number;
32
+ text(): Promise<string>;
33
+ }
34
+
35
+ type PricingFetch = (
36
+ url: string,
37
+ init?: RequestInit,
38
+ ) => Promise<PricingFetchResponse>;
39
+
40
+ export type PricingCacheResult =
41
+ | { readonly state: "fresh"; readonly snapshot: ApiRateSnapshot }
42
+ | {
43
+ readonly state: "stale";
44
+ readonly snapshot: ApiRateSnapshot;
45
+ readonly reason: "expired" | "refresh-failed" | "lease-held";
46
+ }
47
+ | {
48
+ readonly state: "unavailable";
49
+ readonly reason: "cache-miss" | "refresh-failed" | "lease-held";
50
+ };
51
+
52
+ export function defaultPricingCachePaths(): {
53
+ readonly cachePath: string;
54
+ readonly lockPath: string;
55
+ } {
56
+ const agentDirectory =
57
+ process.env.PI_CODING_AGENT_DIR ?? join(homedir(), ".pi", "agent");
58
+ // Package storage identity stays decoupled from the logical provider ID.
59
+ const directory = join(agentDirectory, "pi-multi-account");
60
+ return {
61
+ cachePath: join(directory, "api-pricing.json"),
62
+ lockPath: join(directory, "api-pricing.lock"),
63
+ };
64
+ }
65
+
66
+ function writeSnapshot(cachePath: string, snapshot: ApiRateSnapshot): void {
67
+ const raw = `${JSON.stringify(snapshot)}\n`;
68
+ if (Buffer.byteLength(raw, "utf8") > MAX_CACHE_BYTES) {
69
+ throw new RangeError("API rate snapshot exceeds the cache size limit.");
70
+ }
71
+ const directory = dirname(cachePath);
72
+ mkdirSync(directory, { recursive: true, mode: 0o700 });
73
+ chmodSync(directory, 0o700);
74
+ const temporaryPath = join(
75
+ directory,
76
+ `.${basename(cachePath)}.${process.pid}.${randomUUID()}.tmp`,
77
+ );
78
+ let descriptor: number | undefined;
79
+ try {
80
+ descriptor = openSync(temporaryPath, "wx", 0o600);
81
+ writeFileSync(descriptor, raw, { encoding: "utf8" });
82
+ fsyncSync(descriptor);
83
+ closeSync(descriptor);
84
+ descriptor = undefined;
85
+ renameSync(temporaryPath, cachePath);
86
+ chmodSync(cachePath, 0o600);
87
+ } catch (error) {
88
+ if (descriptor !== undefined) {
89
+ try {
90
+ closeSync(descriptor);
91
+ } catch {
92
+ // Preserve the original write failure.
93
+ }
94
+ }
95
+ try {
96
+ unlinkSync(temporaryPath);
97
+ } catch {
98
+ // The rename may already have consumed the temporary path.
99
+ }
100
+ throw error;
101
+ }
102
+ }
103
+
104
+ function readSnapshot(cachePath: string): ApiRateSnapshot | undefined {
105
+ try {
106
+ const stats = statSync(cachePath);
107
+ if (!stats.isFile() || stats.size < 1 || stats.size > MAX_CACHE_BYTES) {
108
+ return undefined;
109
+ }
110
+ return parseStoredApiRateSnapshot(
111
+ JSON.parse(readFileSync(cachePath, "utf8")) as unknown,
112
+ );
113
+ } catch {
114
+ return undefined;
115
+ }
116
+ }
117
+
118
+ function withReason(
119
+ current: PricingCacheResult,
120
+ reason: "refresh-failed" | "lease-held",
121
+ ): PricingCacheResult {
122
+ return current.state === "fresh"
123
+ ? current
124
+ : current.state === "stale"
125
+ ? { state: "stale", snapshot: current.snapshot, reason }
126
+ : { state: "unavailable", reason };
127
+ }
128
+
129
+ export class OpenRouterPricingCache {
130
+ readonly #cachePath: string;
131
+ readonly #lockPath: string;
132
+ readonly #fetchImpl: PricingFetch;
133
+ readonly #now: () => number;
134
+ readonly #refreshIntervalMs: number;
135
+ readonly #timeoutMs: number;
136
+
137
+ constructor(options: {
138
+ readonly cachePath: string;
139
+ readonly lockPath: string;
140
+ readonly fetchImpl?: PricingFetch;
141
+ readonly now?: () => number;
142
+ readonly refreshIntervalMs?: number;
143
+ readonly timeoutMs?: number;
144
+ }) {
145
+ this.#cachePath = options.cachePath;
146
+ this.#lockPath = options.lockPath;
147
+ this.#fetchImpl =
148
+ options.fetchImpl ??
149
+ ((url, init) => {
150
+ if (process.env.VITEST === "true") {
151
+ return Promise.reject(new Error("network disabled in tests"));
152
+ }
153
+ if (url !== OPENROUTER_MODELS_URL) {
154
+ return Promise.reject(new Error("pricing URL is not allowlisted"));
155
+ }
156
+ return globalThis.fetch(url, init);
157
+ });
158
+ this.#now = options.now ?? Date.now;
159
+ this.#refreshIntervalMs =
160
+ options.refreshIntervalMs ?? PRICING_REFRESH_INTERVAL_MS;
161
+ this.#timeoutMs = options.timeoutMs ?? PRICING_FETCH_TIMEOUT_MS;
162
+ if (
163
+ !Number.isFinite(this.#refreshIntervalMs) ||
164
+ this.#refreshIntervalMs <= 0 ||
165
+ !Number.isFinite(this.#timeoutMs) ||
166
+ this.#timeoutMs <= 0
167
+ ) {
168
+ throw new RangeError("Pricing cache intervals must be finite and positive.");
169
+ }
170
+ }
171
+
172
+ readStatus(): PricingCacheResult {
173
+ const snapshot = readSnapshot(this.#cachePath);
174
+ if (snapshot === undefined) {
175
+ return { state: "unavailable", reason: "cache-miss" };
176
+ }
177
+ const ageMs = this.#now() - snapshot.fetchedAtMs;
178
+ return ageMs >= 0 && ageMs <= this.#refreshIntervalMs
179
+ ? { state: "fresh", snapshot }
180
+ : { state: "stale", snapshot, reason: "expired" };
181
+ }
182
+
183
+ async #fetchSnapshot(): Promise<ApiRateSnapshot> {
184
+ const controller = new AbortController();
185
+ const timeout = setTimeout(() => controller.abort(), this.#timeoutMs);
186
+ try {
187
+ const response = await this.#fetchImpl(OPENROUTER_MODELS_URL, {
188
+ method: "GET",
189
+ headers: { accept: "application/json" },
190
+ signal: controller.signal,
191
+ });
192
+ if (!response.ok) throw new Error("pricing endpoint failed");
193
+ const raw = await response.text();
194
+ if (Buffer.byteLength(raw, "utf8") > MAX_RESPONSE_BYTES) {
195
+ throw new RangeError("pricing response exceeds the size limit");
196
+ }
197
+ const snapshot = parseOpenRouterRateSnapshot(
198
+ JSON.parse(raw) as unknown,
199
+ this.#now(),
200
+ );
201
+ if (Object.keys(snapshot.rates).length === 0) {
202
+ throw new TypeError("pricing response has no managed model rates");
203
+ }
204
+ return snapshot;
205
+ } finally {
206
+ clearTimeout(timeout);
207
+ }
208
+ }
209
+
210
+ async refreshIfNeeded(): Promise<PricingCacheResult> {
211
+ const initial = this.readStatus();
212
+ if (initial.state === "fresh") return initial;
213
+ const lease = acquireMachineLease({
214
+ lockPath: this.#lockPath,
215
+ ttlMs: Math.max(30_000, this.#timeoutMs * 2),
216
+ now: this.#now,
217
+ // A wedged lock would otherwise freeze pricing at its last snapshot.
218
+ reclaimMalformed: true,
219
+ });
220
+ if (lease === undefined) return withReason(initial, "lease-held");
221
+ try {
222
+ const afterLease = this.readStatus();
223
+ if (afterLease.state === "fresh") return afterLease;
224
+ try {
225
+ const snapshot = await this.#fetchSnapshot();
226
+ writeSnapshot(this.#cachePath, snapshot);
227
+ return { state: "fresh", snapshot };
228
+ } catch {
229
+ return withReason(afterLease, "refresh-failed");
230
+ }
231
+ } finally {
232
+ lease.release();
233
+ }
234
+ }
235
+ }
@@ -0,0 +1,100 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { createHash } from "node:crypto";
3
+ import { realpathSync } from "node:fs";
4
+ import { basename, dirname, resolve } from "node:path";
5
+
6
+ /** Bounded, non-identifying key allowed in the machine-global history store. */
7
+ export const PROJECT_KEY_PATTERN = /^project-[0-9a-f]{24}$/;
8
+
9
+ export type GitRootResolver = (absoluteCwd: string) => string | undefined;
10
+
11
+ export interface ProjectKeyResolverOptions {
12
+ readonly gitRoot?: GitRootResolver;
13
+ readonly memoize?: boolean;
14
+ }
15
+
16
+ export function isProjectKey(value: string): boolean {
17
+ return PROJECT_KEY_PATTERN.test(value);
18
+ }
19
+
20
+ function canonicalAbsolute(path: string): string {
21
+ const absolute = resolve(path);
22
+ try {
23
+ return realpathSync.native(absolute);
24
+ } catch {
25
+ return absolute;
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Convert an already-resolved local project root into the only representation
31
+ * that may cross into retained machine-global records.
32
+ */
33
+ export function deriveProjectKey(identityRoot: string): string {
34
+ const absoluteRoot = canonicalAbsolute(identityRoot);
35
+ const digest = createHash("sha256").update(absoluteRoot).digest("hex");
36
+ return `project-${digest.slice(0, 24)}`;
37
+ }
38
+
39
+ function discoverGitRoot(absoluteCwd: string): string | undefined {
40
+ try {
41
+ const output = execFileSync(
42
+ "git",
43
+ [
44
+ "-C",
45
+ absoluteCwd,
46
+ "rev-parse",
47
+ "--path-format=absolute",
48
+ "--git-common-dir",
49
+ ],
50
+ {
51
+ encoding: "utf8",
52
+ stdio: ["ignore", "pipe", "ignore"],
53
+ timeout: 1_000,
54
+ },
55
+ ).trim();
56
+ if (output.length === 0) return undefined;
57
+ const commonDirectory = canonicalAbsolute(output);
58
+ return basename(commonDirectory) === ".git"
59
+ ? dirname(commonDirectory)
60
+ : commonDirectory;
61
+ } catch {
62
+ return undefined;
63
+ }
64
+ }
65
+
66
+ /** Resolve linked worktrees to their shared repository root, or use cwd. */
67
+ export function resolveProjectRoot(
68
+ cwd: string,
69
+ gitRoot: GitRootResolver = discoverGitRoot,
70
+ ): string {
71
+ const absoluteCwd = canonicalAbsolute(cwd);
72
+ try {
73
+ const root = gitRoot(absoluteCwd);
74
+ return root && root.trim().length > 0
75
+ ? canonicalAbsolute(root)
76
+ : absoluteCwd;
77
+ } catch {
78
+ return absoluteCwd;
79
+ }
80
+ }
81
+
82
+ /** Build a resolver whose optional cache never stores a path outside process memory. */
83
+ export function createProjectKeyResolver(
84
+ options: ProjectKeyResolverOptions = {},
85
+ ): (cwd?: string) => string {
86
+ const gitRoot = options.gitRoot ?? discoverGitRoot;
87
+ const cache = new Map<string, string>();
88
+ return (cwd = process.cwd()) => {
89
+ const absoluteCwd = canonicalAbsolute(cwd);
90
+ if (options.memoize === true) {
91
+ const cached = cache.get(absoluteCwd);
92
+ if (cached !== undefined) return cached;
93
+ }
94
+ const key = deriveProjectKey(resolveProjectRoot(absoluteCwd, gitRoot));
95
+ if (options.memoize === true) cache.set(absoluteCwd, key);
96
+ return key;
97
+ };
98
+ }
99
+
100
+ export const projectKeyForCwd = createProjectKeyResolver({ memoize: true });