@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,404 @@
1
+ import type { AutocompleteItem } from "@earendil-works/pi-tui";
2
+ import { fuzzyMatch } from "./fuzzy.js";
3
+ import {
4
+ accountSlotIndexes,
5
+ canonicalProviderIdForAccountSlot,
6
+ isAccountLimit,
7
+ isAccountSlotIndex,
8
+ MANAGED_FAMILIES,
9
+ MAX_ACCOUNT_LIMIT,
10
+ type ManagedFamily,
11
+ } from "./config.js";
12
+ import {
13
+ isProviderSlotWithinAccountLimit,
14
+ type ProviderSlot,
15
+ } from "./discovery.js";
16
+ import {
17
+ resolveLogicalModelReference,
18
+ type LogicalModelCandidate,
19
+ type LogicalModelInventory,
20
+ } from "./logical-model-switcher.js";
21
+ import { LOGICAL_PROVIDER_ID } from "./models-declaration.js";
22
+ import {
23
+ MULTI_ACCOUNT_SUBCOMMANDS,
24
+ type MultiAccountSubcommand,
25
+ } from "./commands.js";
26
+ import { PERIOD_TYPES } from "./period-boundaries.js";
27
+
28
+ /**
29
+ * Pure, synchronous, order-preserving, fail-soft completion policy for the two
30
+ * multi-account commands. It parses only the argument prefix after the command's
31
+ * first literal separator, never executes a command, and never reads a
32
+ * credential, model, or human label. Every emitted item is newly copied from
33
+ * named fields.
34
+ */
35
+
36
+ /** The exact source-backed `log` anchors; there is no suggestion for every integer. */
37
+ const LOG_COMPLETION_CHOICES = ["1", "20", "50", "100"] as const;
38
+
39
+ /** Whole-prefix bound shared by both commands. */
40
+ const MAX_ARGUMENT_PREFIX_LENGTH = 1_024;
41
+
42
+ /** Rejects tabs, newlines, and every other ASCII control while allowing a space. */
43
+ const CONTROL_CHARACTER = /[\u0000-\u0019\u001b-\u001f\u007f]/;
44
+
45
+ /**
46
+ * Matches any Unicode whitespace. A logical model ID may contain `/` but no
47
+ * whitespace; a whitespace-bearing candidate would produce a multi-token
48
+ * completion value that Pi could not replace as one argument, so it is excluded.
49
+ */
50
+ const WHITESPACE = /\s/u;
51
+
52
+ export type CompletionAccountIdentity = Readonly<{
53
+ providerId: string;
54
+ family: ManagedFamily;
55
+ }>;
56
+
57
+ export type CompletionSlotChoices = Readonly<
58
+ Record<ManagedFamily, readonly string[]>
59
+ >;
60
+
61
+ export type MultiAccountCompletionSnapshot = Readonly<{
62
+ accounts: readonly CompletionAccountIdentity[];
63
+ addSlots: CompletionSlotChoices;
64
+ groupIds: readonly string[];
65
+ logicalModels: LogicalModelInventory;
66
+ }>;
67
+
68
+ /**
69
+ * Projects the account identities completion may offer as bare direct
70
+ * command arguments: only discovered slots within the current limit that also
71
+ * back a live model, copied to named fields.
72
+ */
73
+ export function projectCompletionAccounts(
74
+ input: Readonly<{
75
+ slots: readonly ProviderSlot[];
76
+ accountLimit: number;
77
+ hasLiveModel: (providerId: string) => boolean;
78
+ }>,
79
+ ): readonly CompletionAccountIdentity[] {
80
+ const accounts: CompletionAccountIdentity[] = [];
81
+ for (const slot of input.slots) {
82
+ if (!isProviderSlotWithinAccountLimit(slot, input.accountLimit)) continue;
83
+ if (!input.hasLiveModel(slot.providerId)) continue;
84
+ accounts.push({ providerId: slot.providerId, family: slot.family });
85
+ }
86
+ return accounts;
87
+ }
88
+
89
+ /**
90
+ * Projects the ascending, current, free, in-range numbered add slots per
91
+ * family from validated config, separately validated occupancy, and Codex
92
+ * availability. It consumes the sole shared iterator and revalidates every
93
+ * yield; it never copies the numeric maximum or writes its own increment loop.
94
+ */
95
+ export function projectCompletionAddSlots(
96
+ input: Readonly<{
97
+ slots: readonly ProviderSlot[];
98
+ spareProviderIds: readonly string[];
99
+ accountLimit: number;
100
+ codexEnabled: boolean;
101
+ }>,
102
+ ): CompletionSlotChoices {
103
+ const effectiveLimit = input.accountLimit;
104
+ if (!isAccountLimit(effectiveLimit)) {
105
+ return {
106
+ anthropic: [],
107
+ "openai-codex": [],
108
+ "google-antigravity": [],
109
+ openai: [],
110
+ };
111
+ }
112
+ const occupied = new Set<string>();
113
+ for (const slot of input.slots) occupied.add(slot.providerId);
114
+ for (const spare of input.spareProviderIds) occupied.add(spare);
115
+ const choices: Record<ManagedFamily, string[]> = {
116
+ anthropic: [],
117
+ "openai-codex": [],
118
+ "google-antigravity": [],
119
+ openai: [],
120
+ };
121
+ for (const family of MANAGED_FAMILIES) {
122
+ if (family === "openai-codex" && !input.codexEnabled) continue;
123
+ for (const index of accountSlotIndexes(effectiveLimit, 2)) {
124
+ if (!isAccountSlotIndex(index, effectiveLimit)) continue;
125
+ if (index < 2) continue;
126
+ const providerId = canonicalProviderIdForAccountSlot(
127
+ family,
128
+ index,
129
+ effectiveLimit,
130
+ );
131
+ if (providerId === null) continue;
132
+ if (occupied.has(providerId)) continue;
133
+ choices[family].push(String(index));
134
+ }
135
+ }
136
+ return choices;
137
+ }
138
+
139
+ type GrammarChoice = Readonly<{
140
+ text: string;
141
+ account?: CompletionAccountIdentity;
142
+ }>;
143
+
144
+ interface GrammarLevel {
145
+ readonly head: string;
146
+ readonly choices: readonly GrammarChoice[];
147
+ isNonterminal(text: string): boolean;
148
+ descend(text: string): GrammarLevel | null;
149
+ }
150
+
151
+ interface RootSpec {
152
+ token2(snapshot: MultiAccountCompletionSnapshot): readonly GrammarChoice[] | null;
153
+ }
154
+
155
+ function accountChoices(
156
+ snapshot: MultiAccountCompletionSnapshot,
157
+ ): GrammarChoice[] {
158
+ return snapshot.accounts.map((account) => ({
159
+ text: account.providerId,
160
+ account,
161
+ }));
162
+ }
163
+
164
+ function familyChoices(): GrammarChoice[] {
165
+ return MANAGED_FAMILIES.map((family) => ({ text: family }));
166
+ }
167
+
168
+ const ROOT_TABLE: Partial<Record<MultiAccountSubcommand, RootSpec>> = {
169
+ status: {
170
+ token2: (snapshot) => [{ text: "--json" }, ...accountChoices(snapshot)],
171
+ },
172
+ limits: { token2: () => null },
173
+ models: {
174
+ token2: (snapshot) => [
175
+ { text: "install" },
176
+ { text: "update" },
177
+ ...accountChoices(snapshot),
178
+ ],
179
+ },
180
+ // The nested logical-model projection is custom because its display label and
181
+ // fuzzy search fields differ from its inserted value. An empty token source
182
+ // marks this root as open; completeMultiAccountArguments delegates below.
183
+ model: { token2: () => [] },
184
+ cost: { token2: () => PERIOD_TYPES.map((period) => ({ text: period })) },
185
+ log: { token2: () => LOG_COMPLETION_CHOICES.map((anchor) => ({ text: anchor })) },
186
+ rediscover: { token2: () => null },
187
+ add: { token2: () => familyChoices() },
188
+ remove: { token2: (snapshot) => accountChoices(snapshot) },
189
+ clear: { token2: (snapshot) => accountChoices(snapshot) },
190
+ next: { token2: () => null },
191
+ switch: { token2: (snapshot) => accountChoices(snapshot) },
192
+ stop: { token2: () => null },
193
+ reset: { token2: () => null },
194
+ reload: { token2: () => null },
195
+ configure: { token2: () => null },
196
+ enable: { token2: () => null },
197
+ disable: { token2: () => familyChoices() },
198
+ group: {
199
+ token2: () => [{ text: "use" }, { text: "reset" }, { text: "status" }],
200
+ },
201
+ };
202
+
203
+ const ROOT_ORDER: readonly MultiAccountSubcommand[] =
204
+ MULTI_ACCOUNT_SUBCOMMANDS.filter((root) => ROOT_TABLE[root] !== undefined);
205
+
206
+ function rootSpec(text: string): RootSpec | undefined {
207
+ return (ROOT_TABLE as Record<string, RootSpec | undefined>)[text];
208
+ }
209
+
210
+ function isManagedFamilyText(text: string): text is ManagedFamily {
211
+ return (MANAGED_FAMILIES as readonly string[]).includes(text);
212
+ }
213
+
214
+ function buildGrammarItem(head: string, choice: GrammarChoice): AutocompleteItem {
215
+ const value = head === "" ? choice.text : `${head} ${choice.text}`;
216
+ if (choice.account !== undefined) {
217
+ return { value, label: value };
218
+ }
219
+ return { value, label: value };
220
+ }
221
+
222
+ function terminalTokenLevel(
223
+ head: string,
224
+ choices: readonly GrammarChoice[],
225
+ ): GrammarLevel {
226
+ return {
227
+ head,
228
+ choices,
229
+ isNonterminal: () => false,
230
+ descend: () => null,
231
+ };
232
+ }
233
+
234
+ function token2Level(
235
+ root: MultiAccountSubcommand,
236
+ source: readonly GrammarChoice[],
237
+ snapshot: MultiAccountCompletionSnapshot,
238
+ ): GrammarLevel {
239
+ return {
240
+ head: root,
241
+ choices: source,
242
+ isNonterminal: (text) =>
243
+ (root === "add" && isManagedFamilyText(text)) ||
244
+ (root === "group" && text === "use"),
245
+ descend: (text) => {
246
+ if (root === "add" && isManagedFamilyText(text)) {
247
+ return terminalTokenLevel(
248
+ `${root} ${text}`,
249
+ snapshot.addSlots[text].map((slot) => ({ text: slot })),
250
+ );
251
+ }
252
+ if (root === "group" && text === "use") {
253
+ return terminalTokenLevel(
254
+ "group use",
255
+ snapshot.groupIds.map((groupId) => ({ text: groupId })),
256
+ );
257
+ }
258
+ return null;
259
+ },
260
+ };
261
+ }
262
+
263
+ function rootLevel(snapshot: MultiAccountCompletionSnapshot): GrammarLevel {
264
+ return {
265
+ head: "",
266
+ choices: ROOT_ORDER.map((root) => ({ text: root })),
267
+ isNonterminal: (text) => {
268
+ const spec = rootSpec(text);
269
+ return spec !== undefined && spec.token2(snapshot) !== null;
270
+ },
271
+ descend: (text) => {
272
+ const spec = rootSpec(text);
273
+ if (spec === undefined) return null;
274
+ const source = spec.token2(snapshot);
275
+ if (source === null) return null;
276
+ return token2Level(text as MultiAccountSubcommand, source, snapshot);
277
+ },
278
+ };
279
+ }
280
+
281
+ function classifyGrammar(
282
+ level: GrammarLevel,
283
+ current: string,
284
+ trailing: boolean,
285
+ ): AutocompleteItem[] | null {
286
+ if (current === "") {
287
+ if (level.choices.length === 0) return null;
288
+ return level.choices.map((choice) => buildGrammarItem(level.head, choice));
289
+ }
290
+ const exact = level.choices.find((choice) => choice.text === current);
291
+ if (exact !== undefined) {
292
+ if (level.isNonterminal(exact.text)) {
293
+ const next = level.descend(exact.text);
294
+ if (next === null) return null;
295
+ return classifyGrammar(next, "", false);
296
+ }
297
+ return null;
298
+ }
299
+ if (!trailing) {
300
+ const matched = level.choices.filter(
301
+ (choice) => fuzzyMatch(current, choice.text).matches,
302
+ );
303
+ if (matched.length === 0) return null;
304
+ return matched.map((choice) => buildGrammarItem(level.head, choice));
305
+ }
306
+ return null;
307
+ }
308
+
309
+ function parseCommandPrefix(
310
+ prefix: string,
311
+ ): { completed: string[]; current: string; trailing: boolean } | null {
312
+ if (prefix.length > MAX_ARGUMENT_PREFIX_LENGTH) return null;
313
+ if (CONTROL_CHARACTER.test(prefix)) return null;
314
+ if (prefix === "") return { completed: [], current: "", trailing: false };
315
+ if (prefix.startsWith(" ")) return null;
316
+ const trailing = prefix.endsWith(" ");
317
+ const core = trailing ? prefix.slice(0, -1) : prefix;
318
+ if (core === "") return null;
319
+ const raw = core.split(" ");
320
+ if (raw.some((token) => token === "")) return null;
321
+ const completed = trailing ? raw : raw.slice(0, -1);
322
+ const current = trailing ? "" : (raw[raw.length - 1] ?? "");
323
+ return { completed, current, trailing };
324
+ }
325
+
326
+ /**
327
+ * Completes `/multi-account` arguments through the finite command grammar.
328
+ * Returns whole-prefix replacement items or `null`; never mutates inputs or
329
+ * process state.
330
+ */
331
+ export function completeMultiAccountArguments(
332
+ argumentPrefix: string,
333
+ snapshot: MultiAccountCompletionSnapshot,
334
+ ): AutocompleteItem[] | null {
335
+ const parsed = parseCommandPrefix(argumentPrefix);
336
+ if (parsed === null) return null;
337
+ if (argumentPrefix === "model" || argumentPrefix.startsWith("model ")) {
338
+ const logicalPrefix =
339
+ argumentPrefix === "model" ? "" : argumentPrefix.slice("model ".length);
340
+ const items = completeLogicalModelArguments(
341
+ logicalPrefix,
342
+ snapshot.logicalModels,
343
+ );
344
+ return items?.map((item) => ({
345
+ value: `model ${item.value}`,
346
+ label: item.label,
347
+ })) ?? null;
348
+ }
349
+ let level = rootLevel(snapshot);
350
+ for (const token of parsed.completed) {
351
+ const next = level.descend(token);
352
+ if (next === null) return null;
353
+ level = next;
354
+ }
355
+ return classifyGrammar(level, parsed.current, parsed.trailing);
356
+ }
357
+
358
+ /**
359
+ * Completes `/multi-account-model` arguments from a fresh logical inventory.
360
+ * Emits bare-ID values with display-only `<id> [unified]` labels, excludes
361
+ * physical, ambiguous, unavailable, and out-of-scope candidates, and closes an
362
+ * exact retained bare ID. Returns `null` on no match or any closed inventory.
363
+ */
364
+ export function completeLogicalModelArguments(
365
+ argumentPrefix: string,
366
+ inventory: LogicalModelInventory,
367
+ ): AutocompleteItem[] | null {
368
+ if (argumentPrefix.length > MAX_ARGUMENT_PREFIX_LENGTH) return null;
369
+ if (CONTROL_CHARACTER.test(argumentPrefix)) return null;
370
+ if (argumentPrefix.startsWith(" ")) return null;
371
+ if (argumentPrefix.includes(" ")) return null;
372
+ if (inventory.status !== "ready") return null;
373
+ const candidates = inventory.candidates;
374
+ const query = argumentPrefix;
375
+ const retained: LogicalModelCandidate[] = [];
376
+ for (const candidate of candidates) {
377
+ if (WHITESPACE.test(candidate.id)) continue;
378
+ if (
379
+ candidate.model.provider !== LOGICAL_PROVIDER_ID ||
380
+ candidate.model.api !== LOGICAL_PROVIDER_ID
381
+ )
382
+ continue;
383
+ const reference = resolveLogicalModelReference(candidate.id, inventory);
384
+ if (reference.status !== "resolved" || reference.candidate.id !== candidate.id)
385
+ continue;
386
+ retained.push(candidate);
387
+ }
388
+ if (query !== "" && retained.some((candidate) => candidate.id === query)) {
389
+ return null;
390
+ }
391
+ const matched = retained.filter(
392
+ (candidate) =>
393
+ fuzzyMatch(
394
+ query,
395
+ `unified pi-multi-account ${LOGICAL_PROVIDER_ID}/${candidate.id} ${candidate.id} ${candidate.name}`,
396
+ ).matches,
397
+ );
398
+ if (matched.length === 0) return null;
399
+ return matched.map((candidate) => {
400
+ const id = candidate.id;
401
+ const label = `${id} [unified]`;
402
+ return { value: id, label };
403
+ });
404
+ }