@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,232 @@
1
+ /**
2
+ * The commit boundary for an operator-driven cross-family routing edit.
3
+ *
4
+ * Human dialogs never hold a machine lease. This module owns everything that
5
+ * happens after the operator has answered: a short bounded lease, a fresh
6
+ * under-lease read of both disk and the process-effective config, commit-time
7
+ * drift detection against the persisted prompt snapshot, a merge that preserves
8
+ * every unrelated persisted field, the atomic write, and a routing-only
9
+ * synchronous publication into the current process.
10
+ *
11
+ * Publication is deliberately routing-only. Assigning a whole freshly read
12
+ * config into the live closure would change account, label, usage, and cost
13
+ * behavior without the account rediscovery `/multi-account reload` performs.
14
+ */
15
+
16
+ import { dirname, join } from "node:path";
17
+ import {
18
+ nonRoutingProjectionsEqual,
19
+ readConfig,
20
+ routingProjection,
21
+ routingProjectionsEqual,
22
+ writeConfig,
23
+ type ConfigWriteOutcome,
24
+ type MultiAccountConfig,
25
+ type RoutingProjection,
26
+ } from "./config.js";
27
+ import type { DiagnosticLog } from "./diagnostics.js";
28
+ import {
29
+ MACHINE_LEASE_TTL_MS,
30
+ acquireMachineLease,
31
+ type MachineLeaseHandle,
32
+ type MachineLeaseOptions,
33
+ } from "./machine-lease.js";
34
+
35
+ /** Bounded contention handling: no recurring timer survives the command. */
36
+ export const ROUTING_CONFIG_LEASE_ATTEMPTS = 3;
37
+ export const ROUTING_CONFIG_LEASE_WINDOW_MS = 300;
38
+ const LEASE_RETRY_INTERVAL_MS = Math.ceil(
39
+ ROUTING_CONFIG_LEASE_WINDOW_MS / ROUTING_CONFIG_LEASE_ATTEMPTS,
40
+ );
41
+
42
+ export const ROUTING_CONFIG_LOCK_FILE = "config.lock";
43
+
44
+ export function routingConfigLockPath(configPath: string): string {
45
+ return join(dirname(configPath), ROUTING_CONFIG_LOCK_FILE);
46
+ }
47
+
48
+ export const NON_ROUTING_DRIFT_GUIDANCE =
49
+ "Routing configuration was published, but this process's non-routing configuration differs from the persisted file. Run /multi-account reload or restart Pi before relying on the persisted non-routing values.";
50
+
51
+ export const COMMIT_WARNING_GUIDANCE =
52
+ "Routing configuration was committed and published, but its final file mode or directory durability was not verified. Inspect the machine-global config file, then run /multi-account reload or configure again.";
53
+
54
+ /**
55
+ * A pre-rename write failure. The original error is deliberately discarded:
56
+ * filesystem errors carry raw paths, and this message crosses the operator
57
+ * command surface.
58
+ */
59
+ export class RoutingConfigWriteError extends Error {
60
+ constructor() {
61
+ super(
62
+ "the routing configuration could not be written; the previous configuration is unchanged.",
63
+ );
64
+ this.name = "RoutingConfigWriteError";
65
+ }
66
+ }
67
+
68
+ export type RoutingConfigCommitResult =
69
+ | { readonly status: "applied"; readonly nonRoutingDrift: boolean }
70
+ | { readonly status: "applied-warning"; readonly nonRoutingDrift: boolean }
71
+ | { readonly status: "unchanged" }
72
+ | { readonly status: "busy" }
73
+ | { readonly status: "changed" }
74
+ | { readonly status: "invalid" };
75
+
76
+ export interface RoutingConfigTransactionOptions {
77
+ readonly configPath: string;
78
+ readonly lockPath: string;
79
+ /** Normalized persisted routing policy shown to the operator before dialogs. */
80
+ readonly promptSnapshot: RoutingProjection;
81
+ /** The complete validated routing policy the operator confirmed. */
82
+ readonly candidate: RoutingProjection;
83
+ /** Re-read under the lease; never a pre-dialog snapshot. */
84
+ readonly processEffectiveConfig: () => MultiAccountConfig;
85
+ /** Contracted non-throwing routing-only assignment into the live closure. */
86
+ readonly publishRouting: (projection: RoutingProjection) => void;
87
+ readonly diagnostics?: Pick<DiagnosticLog, "record">;
88
+ readonly signal?: AbortSignal;
89
+ readonly now?: () => number;
90
+ readonly sleep?: (ms: number) => Promise<void>;
91
+ readonly acquireLease?: (
92
+ options: MachineLeaseOptions,
93
+ ) => MachineLeaseHandle | undefined;
94
+ readonly readConfigImpl?: (configPath: string) => MultiAccountConfig;
95
+ readonly writeConfigImpl?: (
96
+ configPath: string,
97
+ config: MultiAccountConfig,
98
+ onCommitted?: () => void,
99
+ ) => ConfigWriteOutcome;
100
+ }
101
+
102
+ const BUSY = Object.freeze({ status: "busy" } as const);
103
+ const CHANGED = Object.freeze({ status: "changed" } as const);
104
+ const INVALID = Object.freeze({ status: "invalid" } as const);
105
+ const UNCHANGED = Object.freeze({ status: "unchanged" } as const);
106
+
107
+ function defaultSleep(ms: number): Promise<void> {
108
+ return new Promise<void>((resolve) => {
109
+ setTimeout(resolve, ms);
110
+ });
111
+ }
112
+
113
+ /**
114
+ * Commits one routing-only configuration change and publishes it to this
115
+ * process.
116
+ *
117
+ * Ordering is load-bearing. The lease is taken only after every dialog has
118
+ * closed; disk and the process-effective config are read only under that lease;
119
+ * the persisted candidate is built from the fresh disk value so unrelated
120
+ * fields survive; and publication runs as the first statement after the atomic
121
+ * rename so disk and this process cannot be split by a later durability
122
+ * failure. Once the rename begins, abort is ignored: there is no conditional
123
+ * rollback, because a stale rollback could destroy a later writer's edit.
124
+ */
125
+ export async function commitRoutingConfig(
126
+ options: RoutingConfigTransactionOptions,
127
+ ): Promise<RoutingConfigCommitResult> {
128
+ const now = options.now ?? Date.now;
129
+ const sleep = options.sleep ?? defaultSleep;
130
+ const acquire = options.acquireLease ?? acquireMachineLease;
131
+ const read = options.readConfigImpl ?? readConfig;
132
+ const write = options.writeConfigImpl ?? writeConfig;
133
+ const signal = options.signal;
134
+
135
+ if (signal?.aborted) return CHANGED;
136
+
137
+ const startedMs = now();
138
+ let handle: MachineLeaseHandle | undefined;
139
+ for (
140
+ let attempt = 1;
141
+ attempt <= ROUTING_CONFIG_LEASE_ATTEMPTS;
142
+ attempt += 1
143
+ ) {
144
+ handle = acquire({
145
+ lockPath: options.lockPath,
146
+ ttlMs: MACHINE_LEASE_TTL_MS,
147
+ now,
148
+ });
149
+ if (handle !== undefined) break;
150
+ if (attempt === ROUTING_CONFIG_LEASE_ATTEMPTS) break;
151
+ const remainingMs = ROUTING_CONFIG_LEASE_WINDOW_MS - (now() - startedMs);
152
+ if (remainingMs <= 0) break;
153
+ await sleep(Math.min(remainingMs, LEASE_RETRY_INTERVAL_MS));
154
+ }
155
+ // A held OR malformed lease is fail-closed, and this caller deliberately
156
+ // does not opt into malformed reclaim. The original reason no longer holds:
157
+ // a malformed record was once indistinguishable from a live writer
158
+ // mid-record, but leases are now published by link(2) from a complete
159
+ // fsynced temporary file, so the lock path is never contentless mid-write.
160
+ // The opt-out stands on its own ground instead. This path is operator
161
+ // initiated and returns BUSY immediately, so a wedged lock is visible and
162
+ // retryable rather than a silent permanent refusal, and
163
+ // .spec/configure-explicit-routing-chains.md requires that actionable busy
164
+ // guidance. Changing it is a spec decision, not a lock-recovery one.
165
+ if (handle === undefined) return BUSY;
166
+
167
+ try {
168
+ if (signal?.aborted) return CHANGED;
169
+
170
+ let fresh: MultiAccountConfig;
171
+ let effective: MultiAccountConfig;
172
+ try {
173
+ fresh = read(options.configPath);
174
+ effective = options.processEffectiveConfig();
175
+ } catch {
176
+ return INVALID;
177
+ }
178
+
179
+ const freshRouting = routingProjection(fresh);
180
+ // Compared against the PERSISTED prompt snapshot, not the process-effective
181
+ // one: a flow started while this process was stale must still be able to
182
+ // converge once disk stops moving.
183
+ if (!routingProjectionsEqual(freshRouting, options.promptSnapshot)) {
184
+ return CHANGED;
185
+ }
186
+
187
+ const candidate = routingProjection(options.candidate);
188
+ const nonRoutingDrift = !nonRoutingProjectionsEqual(fresh, effective);
189
+
190
+ // The only no-op, and it is decided from fresh values on both sides. A
191
+ // pre-dialog comparison could assert "already configured" while this
192
+ // process is still running the old policy.
193
+ if (
194
+ routingProjectionsEqual(freshRouting, candidate) &&
195
+ routingProjectionsEqual(routingProjection(effective), candidate)
196
+ ) {
197
+ return UNCHANGED;
198
+ }
199
+
200
+ const persisted: MultiAccountConfig = { ...fresh, ...candidate };
201
+
202
+ if (signal?.aborted) return CHANGED;
203
+
204
+ let outcome: ConfigWriteOutcome;
205
+ try {
206
+ outcome = write(options.configPath, persisted, () => {
207
+ options.publishRouting(candidate);
208
+ });
209
+ } catch {
210
+ throw new RoutingConfigWriteError();
211
+ }
212
+
213
+ if (nonRoutingDrift) {
214
+ options.diagnostics?.record(
215
+ "warning",
216
+ "config.routing-drift",
217
+ NON_ROUTING_DRIFT_GUIDANCE,
218
+ );
219
+ }
220
+ if (outcome === "committed-warning") {
221
+ options.diagnostics?.record(
222
+ "warning",
223
+ "config.routing-commit",
224
+ COMMIT_WARNING_GUIDANCE,
225
+ );
226
+ return { status: "applied-warning", nonRoutingDrift };
227
+ }
228
+ return { status: "applied", nonRoutingDrift };
229
+ } finally {
230
+ handle.release();
231
+ }
232
+ }