@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,340 @@
1
+ /**
2
+ * The preview-confirm-atomic-write workflow behind `multi-account account
3
+ * set-plan`.
4
+ *
5
+ * This module holds the domain transaction only: resolving and validating a
6
+ * candidate assignment against the current catalog and canonical account set,
7
+ * building the immutable {@link AccountRateRecord} it selects, and committing
8
+ * it through exactly one atomic {@link writeConfig} call. It never guesses
9
+ * `effectiveFrom`, a history start, or a next-renewal boundary, and it never
10
+ * suggests a nearby preset for an unknown id -- both are rejected outright.
11
+ *
12
+ * Ordering mirrors `routing-config-transaction.ts`'s documented invariant:
13
+ * human dialogs never hold a machine lease. The preview is built from an
14
+ * unlocked read; the machine lease guarding `config.json` is acquired only
15
+ * after the operator has confirmed, and every value written to disk is
16
+ * re-resolved from a fresh, locked read immediately beforehand, so a stale
17
+ * preview -- or a catalog/account edit racing the confirmation dialog -- can
18
+ * never reach the single atomic write.
19
+ */
20
+
21
+ import {
22
+ isCanonicalSubscriptionAccountId,
23
+ readConfig,
24
+ validateConfig,
25
+ writeConfig,
26
+ type ConfigWriteOutcome,
27
+ type MultiAccountConfig,
28
+ } from "./config.js";
29
+ import {
30
+ findSubscriptionPlanPreset,
31
+ loadEffectiveSubscriptionPlanCatalog,
32
+ type CatalogAccountType,
33
+ type CatalogProvider,
34
+ type SubscriptionPlanCatalog,
35
+ type SubscriptionPlanPreset,
36
+ } from "./subscription-plan-catalog.js";
37
+ import {
38
+ assertValidEffectiveFrom,
39
+ createAccountRateRecord,
40
+ normalizeAccountRateHistory,
41
+ type AccountRateRecord,
42
+ } from "./account-rate-history.js";
43
+ import {
44
+ MACHINE_LEASE_TTL_MS,
45
+ acquireMachineLease,
46
+ type MachineLeaseHandle,
47
+ type MachineLeaseOptions,
48
+ } from "./machine-lease.js";
49
+ import { routingConfigLockPath } from "./routing-config-transaction.js";
50
+
51
+ export class AccountPlanAssignmentError extends Error {
52
+ constructor(message: string) {
53
+ super(`[account-plan-assignment] ${message}`);
54
+ this.name = "AccountPlanAssignmentError";
55
+ }
56
+ }
57
+
58
+ /**
59
+ * A fixed, non-caller-controlled provenance string for an explicit
60
+ * `--monthly-usd` override. `createAccountRateRecord` requires override
61
+ * provenance whenever an override amount is supplied; this command never
62
+ * accepts free-text provenance from the operator, so every override this
63
+ * command produces carries exactly this string.
64
+ */
65
+ export const OPERATOR_OVERRIDE_PROVENANCE =
66
+ "operator-provided override via account set-plan";
67
+
68
+ export interface AccountPlanAssignmentInput {
69
+ readonly accountId: string;
70
+ readonly presetId: string;
71
+ /** RFC 3339 instant with an explicit "Z" or numeric offset. */
72
+ readonly effectiveFrom: string;
73
+ readonly monthlyUsdOverride?: number;
74
+ }
75
+
76
+ export interface AccountPlanAssignmentPreview {
77
+ readonly accountId: string;
78
+ readonly provider: CatalogProvider;
79
+ readonly accountType: CatalogAccountType;
80
+ readonly presetId: string;
81
+ readonly presetLabel: string;
82
+ readonly monthlyUsd: number;
83
+ readonly isOverride: boolean;
84
+ readonly effectiveFrom: string;
85
+ readonly catalogVersion: number;
86
+ }
87
+
88
+ /**
89
+ * Resolves and validates one candidate assignment against a supplied config
90
+ * and resolved catalog snapshot. Pure: performs no I/O and never guesses a
91
+ * missing or malformed input. Throws {@link AccountPlanAssignmentError} naming
92
+ * the rejected account id or preset id, or an `AccountRateHistoryError` (via
93
+ * {@link assertValidEffectiveFrom}) for a malformed `effectiveFrom`.
94
+ */
95
+ export function resolveAccountPlanAssignmentPreview(
96
+ input: AccountPlanAssignmentInput,
97
+ config: Pick<MultiAccountConfig, "accountLimit">,
98
+ catalog: SubscriptionPlanCatalog,
99
+ ): AccountPlanAssignmentPreview {
100
+ if (!isCanonicalSubscriptionAccountId(input.accountId, config.accountLimit)) {
101
+ throw new AccountPlanAssignmentError(
102
+ `Unknown account "${input.accountId}". It is not a configured canonical account.`,
103
+ );
104
+ }
105
+ const preset = findSubscriptionPlanPreset(catalog, input.presetId);
106
+ if (preset === undefined) {
107
+ throw new AccountPlanAssignmentError(
108
+ `Unknown preset "${input.presetId}". No catalog preset matches that id.`,
109
+ );
110
+ }
111
+ assertValidEffectiveFrom(input.effectiveFrom, "--effective-from");
112
+ if (
113
+ input.monthlyUsdOverride !== undefined &&
114
+ (!Number.isFinite(input.monthlyUsdOverride) || input.monthlyUsdOverride < 0)
115
+ ) {
116
+ throw new AccountPlanAssignmentError(
117
+ "--monthly-usd must be a finite, non-negative USD amount.",
118
+ );
119
+ }
120
+ const isOverride = input.monthlyUsdOverride !== undefined;
121
+ return {
122
+ accountId: input.accountId,
123
+ provider: preset.provider,
124
+ accountType: preset.accountType,
125
+ presetId: preset.id,
126
+ presetLabel: preset.label,
127
+ monthlyUsd: isOverride ? (input.monthlyUsdOverride as number) : preset.monthlyUsd,
128
+ isOverride,
129
+ effectiveFrom: input.effectiveFrom,
130
+ catalogVersion: catalog.version,
131
+ };
132
+ }
133
+
134
+ /**
135
+ * Builds the immutable candidate record for one resolved preset, copying only
136
+ * the allowlisted fields `createAccountRateRecord` accepts. An explicit
137
+ * override always carries the fixed {@link OPERATOR_OVERRIDE_PROVENANCE};
138
+ * this function never accepts caller-supplied provenance text.
139
+ */
140
+ export function buildAccountPlanAssignmentRecord(
141
+ input: AccountPlanAssignmentInput,
142
+ preset: SubscriptionPlanPreset,
143
+ catalogVersion: number,
144
+ ): AccountRateRecord {
145
+ return createAccountRateRecord({
146
+ accountId: input.accountId,
147
+ preset,
148
+ catalogVersion,
149
+ effectiveFrom: input.effectiveFrom,
150
+ ...(input.monthlyUsdOverride !== undefined
151
+ ? {
152
+ monthlyUsdOverride: input.monthlyUsdOverride,
153
+ overrideProvenance: OPERATOR_OVERRIDE_PROVENANCE,
154
+ }
155
+ : {}),
156
+ });
157
+ }
158
+
159
+ /**
160
+ * Returns true when every term `preview` displayed to the operator for
161
+ * confirmation is still present, byte-for-byte, in `freshPreview` -- the
162
+ * preview re-resolved from the locked, current read immediately before the
163
+ * atomic write. Compares every operator-confirmed term (account, provider,
164
+ * account type, preset id and label, the copied or overridden monthly rate,
165
+ * the effective instant, and catalog version), never merely `catalogVersion`
166
+ * or object identity: {@link resolveSubscriptionPlanCatalog} keeps the
167
+ * shipped catalog's `version` unchanged across a machine-global override
168
+ * edit, so a `catalogVersion`-only check would miss exactly the price or
169
+ * definition drift this function exists to catch, and a fresh preview is
170
+ * always a newly built object, so reference identity never applies.
171
+ */
172
+ function confirmedPreviewTermsStillMatch(
173
+ preview: AccountPlanAssignmentPreview,
174
+ freshPreview: AccountPlanAssignmentPreview,
175
+ ): boolean {
176
+ return (
177
+ preview.accountId === freshPreview.accountId &&
178
+ preview.provider === freshPreview.provider &&
179
+ preview.accountType === freshPreview.accountType &&
180
+ preview.presetId === freshPreview.presetId &&
181
+ preview.presetLabel === freshPreview.presetLabel &&
182
+ preview.monthlyUsd === freshPreview.monthlyUsd &&
183
+ preview.isOverride === freshPreview.isOverride &&
184
+ preview.effectiveFrom === freshPreview.effectiveFrom &&
185
+ preview.catalogVersion === freshPreview.catalogVersion
186
+ );
187
+ }
188
+
189
+ export type AccountPlanAssignmentCommitResult =
190
+ | { readonly status: "applied"; readonly record: AccountRateRecord; readonly outcome: ConfigWriteOutcome }
191
+ | { readonly status: "declined" }
192
+ | { readonly status: "busy" };
193
+
194
+ export interface AccountPlanAssignmentTransactionOptions {
195
+ readonly configPath: string;
196
+ readonly lockPath?: string;
197
+ readonly input: AccountPlanAssignmentInput;
198
+ /** Called once with the resolved preview before {@link confirm} runs. */
199
+ readonly onPreview: (preview: AccountPlanAssignmentPreview) => void;
200
+ /** Never called while a machine lease is held. */
201
+ readonly confirm: () => boolean | Promise<boolean>;
202
+ readonly now?: () => number;
203
+ readonly acquireLease?: (
204
+ options: MachineLeaseOptions,
205
+ ) => MachineLeaseHandle | undefined;
206
+ readonly readConfigImpl?: (configPath: string) => MultiAccountConfig;
207
+ readonly writeConfigImpl?: (
208
+ configPath: string,
209
+ config: MultiAccountConfig,
210
+ onCommitted?: () => void,
211
+ ) => ConfigWriteOutcome;
212
+ }
213
+
214
+ /**
215
+ * Commits one account plan assignment.
216
+ *
217
+ * 1. Reads config unlocked, resolves and validates the preview, and hands it
218
+ * to `onPreview` -- no lease is held yet.
219
+ * 2. Awaits `confirm()`. A decline returns `{status:"declined"}` and performs
220
+ * no further work: retained bytes are untouched.
221
+ * 3. Acquires the machine lease guarding `config.json`. A held lease returns
222
+ * `{status:"busy"}` immediately; no write is attempted.
223
+ * 4. Re-reads config under the lease and re-resolves the preview against that
224
+ * fresh snapshot, then compares every operator-confirmed term (account,
225
+ * provider, account type, preset id and label, the copied or overridden
226
+ * monthly rate, the effective instant, and catalog version) against that
227
+ * fresh preview. A stale preset id, removed account slot, or any catalog
228
+ * edit racing the dialog -- including a price or definition change the
229
+ * operator never saw -- is rejected here, before anything is written. No
230
+ * automatic re-prompt: the caller must rerun the command to see and
231
+ * confirm the fresh terms.
232
+ * 5. Builds the candidate record, appends it to that account's existing
233
+ * history, and normalizes the result -- a duplicate or overlapping
234
+ * `effectiveFrom` throws `AccountRateHistoryError` and nothing is written.
235
+ * 6. Verifies the fully-built candidate config parses back through
236
+ * `validateConfig` -- a structural self-check performed before the
237
+ * candidate ever reaches disk.
238
+ * 7. Performs exactly one atomic write via `writeConfig`.
239
+ *
240
+ * Every step before (7) can throw or return without ever calling the write
241
+ * implementation, so a failure at validation, confirmation, the lease, or this
242
+ * pre-write verification always leaves the persisted bytes exactly as they
243
+ * were.
244
+ */
245
+ export async function commitAccountPlanAssignment(
246
+ options: AccountPlanAssignmentTransactionOptions,
247
+ ): Promise<AccountPlanAssignmentCommitResult> {
248
+ const read = options.readConfigImpl ?? readConfig;
249
+ const write = options.writeConfigImpl ?? writeConfig;
250
+ const acquire = options.acquireLease ?? acquireMachineLease;
251
+ const now = options.now ?? Date.now;
252
+ const lockPath = options.lockPath ?? routingConfigLockPath(options.configPath);
253
+
254
+ const promptConfig = read(options.configPath);
255
+ const promptCatalog = loadEffectiveSubscriptionPlanCatalog(
256
+ promptConfig.subscriptionPlanCatalogOverrides ?? {},
257
+ );
258
+ const preview = resolveAccountPlanAssignmentPreview(
259
+ options.input,
260
+ promptConfig,
261
+ promptCatalog,
262
+ );
263
+ options.onPreview(preview);
264
+
265
+ const confirmed = await options.confirm();
266
+ if (!confirmed) return { status: "declined" };
267
+
268
+ const handle = acquire({ lockPath, ttlMs: MACHINE_LEASE_TTL_MS, now });
269
+ if (handle === undefined) return { status: "busy" };
270
+
271
+ try {
272
+ // Fresh, locked re-read and re-validation. This is the pre-write
273
+ // verification step: the candidate is re-derived and re-checked against
274
+ // the CURRENT catalog, account limit, and history immediately before the
275
+ // single atomic write, so a stale preview -- or a concurrent edit that
276
+ // landed between the preview and this confirmation -- can never reach
277
+ // disk.
278
+ const fresh = read(options.configPath);
279
+ const freshCatalog = loadEffectiveSubscriptionPlanCatalog(
280
+ fresh.subscriptionPlanCatalogOverrides ?? {},
281
+ );
282
+ const freshPreview = resolveAccountPlanAssignmentPreview(
283
+ options.input,
284
+ fresh,
285
+ freshCatalog,
286
+ );
287
+ if (!confirmedPreviewTermsStillMatch(preview, freshPreview)) {
288
+ // Fail closed: a term the operator confirmed (account, provider,
289
+ // account type, preset, its label, the copied or overridden monthly
290
+ // rate, the effective instant, or catalog version) no longer matches
291
+ // the fresh, locked snapshot -- most commonly a machine-global
292
+ // catalog override edit racing the confirmation dialog. Reject
293
+ // outright with no write and no automatic re-prompt; the operator
294
+ // reruns the command to see and confirm the current terms. This
295
+ // never touches `fresh`, so any unrelated concurrent config edit it
296
+ // already observed is preserved, not rolled back.
297
+ throw new AccountPlanAssignmentError(
298
+ `Confirmed terms for "${options.input.accountId}" changed before the write ` +
299
+ `could be committed (e.g. a concurrent catalog edit). Nothing was ` +
300
+ `written; rerun account set-plan to review and confirm the current ` +
301
+ `terms.`,
302
+ );
303
+ }
304
+ const preset = findSubscriptionPlanPreset(freshCatalog, freshPreview.presetId);
305
+ if (preset === undefined) {
306
+ // Unreachable except under a racing catalog edit between the two
307
+ // lookups immediately above: resolveAccountPlanAssignmentPreview
308
+ // already proved this preset id resolves in freshCatalog.
309
+ throw new AccountPlanAssignmentError(
310
+ `Preset "${options.input.presetId}" is no longer available.`,
311
+ );
312
+ }
313
+ const record = buildAccountPlanAssignmentRecord(
314
+ options.input,
315
+ preset,
316
+ freshCatalog.version,
317
+ );
318
+ const existingCandidates = (
319
+ fresh.accountRateHistory?.[options.input.accountId] ?? []
320
+ ).map((existingRecord) => ({ record: existingRecord }));
321
+ const normalized = normalizeAccountRateHistory([
322
+ ...existingCandidates,
323
+ { record },
324
+ ]);
325
+ const candidate: MultiAccountConfig = {
326
+ ...fresh,
327
+ accountRateHistory: {
328
+ ...fresh.accountRateHistory,
329
+ [options.input.accountId]: normalized,
330
+ },
331
+ };
332
+ // Structural self-check only -- no disk I/O -- performed before the
333
+ // single atomic write is ever attempted.
334
+ validateConfig(candidate);
335
+ const outcome = write(options.configPath, candidate);
336
+ return { status: "applied", record, outcome };
337
+ } finally {
338
+ handle.release();
339
+ }
340
+ }