@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,720 @@
1
+ /**
2
+ * The standalone `multi-account` shell command entry point.
3
+ *
4
+ * `scripts/multi-account.mjs` is the package's published `bin` launcher; it
5
+ * installs a Node module-customization hook so this package's NodeNext-style
6
+ * ".js"-suffixed relative specifiers resolve to their ".ts" siblings under a
7
+ * bare `node` invocation, then calls {@link runStandaloneCli} with the raw
8
+ * argv. This module owns argument parsing, the preview/confirmation dialog,
9
+ * and stable process exit codes; the actual assignment transaction lives in
10
+ * `account-plan-assignment.ts`, and the read-only cost report projection
11
+ * lives in `cost-report-reader.ts`/`cost-report.ts`.
12
+ *
13
+ * This module implements two commands: `account set-plan` and `cost`
14
+ * (including its explicit `refresh-pricing`/`close-periods` actions). Every
15
+ * command shares one stable exit table: `0` success (including a declined
16
+ * confirmation or a truthful empty report -- the operator's explicit choice
17
+ * or legitimately empty history, not a failure); `1` an unexpected internal
18
+ * failure (a held lease, an I/O error, an unreadable configuration file);
19
+ * `2` a syntax, argument, or domain validation failure (an unknown account or
20
+ * preset id, a malformed or missing `--effective-from`, mutually exclusive
21
+ * report selectors, or an unsupported flag); `3` when retained data cannot
22
+ * satisfy the requested report precision; `4` for corrupt bounded retained
23
+ * cost history; and `5` for an explicit `refresh-pricing`/`close-periods`
24
+ * action failure. `cost` (without `refresh-pricing`/`close-periods`) is a
25
+ * strict read-only, offline probe: it never contacts a provider, refreshes
26
+ * pricing, closes a period, migrates configuration, edits an account rate, or
27
+ * creates a catalog, including on an empty first run.
28
+ */
29
+
30
+ import { readFileSync } from "node:fs";
31
+ import { homedir } from "node:os";
32
+ import { dirname, join } from "node:path";
33
+ import { createInterface } from "node:readline";
34
+ import { fileURLToPath } from "node:url";
35
+ import { ANTHROPIC_MODELS } from "@earendil-works/pi-ai/providers/anthropic.models";
36
+ import { OPENAI_CODEX_MODELS } from "@earendil-works/pi-ai/providers/openai-codex.models";
37
+ import {
38
+ AccountPlanAssignmentError,
39
+ commitAccountPlanAssignment,
40
+ type AccountPlanAssignmentInput,
41
+ type AccountPlanAssignmentPreview,
42
+ } from "./account-plan-assignment.js";
43
+ import { AccountRateHistoryError } from "./account-rate-history.js";
44
+ import { parsePiCatalogSnapshot, type PiCatalogSnapshot } from "./api-pricing.js";
45
+ import { ConfigValidationError, readConfig } from "./config.js";
46
+ import {
47
+ CostReportRangeRequestError,
48
+ createDefaultCostReportReader,
49
+ type CostReportRangeRequest,
50
+ type CostReportReader,
51
+ } from "./cost-report-reader.js";
52
+ import { renderCostReportJson } from "./cost-report-json.js";
53
+ import {
54
+ createDefaultCostPeriodCloser,
55
+ type CostPeriodCloseResult,
56
+ } from "./cost-period-closer.js";
57
+ import { renderCostReport } from "./cost-report-render.js";
58
+ import { PERIOD_TYPES, type PeriodType } from "./period-boundaries.js";
59
+ import {
60
+ defaultPricingCachePaths,
61
+ OpenRouterPricingCache,
62
+ type PricingCacheResult,
63
+ } from "./pricing-cache.js";
64
+
65
+ export const EXIT_SUCCESS = 0;
66
+ export const EXIT_UNEXPECTED_FAILURE = 1;
67
+ export const EXIT_VALIDATION_FAILURE = 2;
68
+ export const EXIT_UNSATISFIABLE_PRECISION = 3;
69
+ export const EXIT_CORRUPT_RETAINED_INPUT = 4;
70
+ export const EXIT_ACTION_FAILURE = 5;
71
+
72
+ export interface StandaloneCliDependencies {
73
+ readonly configPath?: string;
74
+ readonly lockPath?: string;
75
+ readonly stdin?: NodeJS.ReadableStream;
76
+ readonly stdout?: Pick<NodeJS.WritableStream, "write">;
77
+ readonly stderr?: Pick<NodeJS.WritableStream, "write">;
78
+ /** Overrides the report clock (for testing only). Defaults to `Date.now`. */
79
+ readonly now?: () => number;
80
+ }
81
+
82
+ function defaultConfigPath(): string {
83
+ const baseDirectory = process.env.PI_CODING_AGENT_DIR ?? join(homedir(), ".pi", "agent");
84
+ return join(baseDirectory, "pi-multi-account", "config.json");
85
+ }
86
+
87
+ /**
88
+ * Resolves the actually-installed `@earendil-works/pi-ai` package version by
89
+ * walking up from its resolved `providers/anthropic.models` module file to
90
+ * the nearest `package.json` whose `name` matches. Reading a real installed
91
+ * `package.json` from disk is a local, offline, bounded lookup -- never a
92
+ * network request -- and gives an honest provenance label for whichever
93
+ * pinned catalog this process actually loaded, rather than a hard-coded
94
+ * version string that could drift from what is really installed.
95
+ */
96
+ function resolveInstalledPiAiVersion(): string {
97
+ try {
98
+ const moduleUrl = import.meta.resolve(
99
+ "@earendil-works/pi-ai/providers/anthropic.models",
100
+ );
101
+ let directory = dirname(fileURLToPath(moduleUrl));
102
+ for (let depth = 0; depth < 6; depth += 1) {
103
+ try {
104
+ const candidate = JSON.parse(
105
+ readFileSync(join(directory, "package.json"), "utf-8"),
106
+ ) as { readonly name?: unknown; readonly version?: unknown };
107
+ if (
108
+ candidate.name === "@earendil-works/pi-ai" &&
109
+ typeof candidate.version === "string" &&
110
+ candidate.version.length > 0
111
+ ) {
112
+ return `pi-ai@${candidate.version}`;
113
+ }
114
+ } catch {
115
+ // Keep walking toward the installed package root.
116
+ }
117
+ const parent = dirname(directory);
118
+ if (parent === directory) break;
119
+ directory = parent;
120
+ }
121
+ } catch {
122
+ // Resolution failure falls through to the bounded fallback below.
123
+ }
124
+ return "pi-ai@unknown";
125
+ }
126
+
127
+ interface OfflineCatalogModelSource {
128
+ readonly vendor: "anthropic" | "openai";
129
+ readonly id: string;
130
+ readonly cost: {
131
+ readonly input: number;
132
+ readonly output: number;
133
+ readonly cacheRead: number;
134
+ readonly cacheWrite: number;
135
+ readonly tiers?: ReadonlyArray<{
136
+ readonly input: number;
137
+ readonly output: number;
138
+ readonly cacheRead: number;
139
+ readonly cacheWrite: number;
140
+ readonly inputTokensAbove: number;
141
+ }>;
142
+ };
143
+ }
144
+
145
+ /**
146
+ * Every managed-vendor model the pinned `@earendil-works/pi-ai` package
147
+ * ships, tagged with the vendor prefix `lookupPiCatalogCost` resolves
148
+ * requests against (see `api-pricing.ts`'s `accountAuthor`): the `anthropic`
149
+ * family keeps its own `anthropic` provider id, while the `openai-codex`
150
+ * family's models are authored by, and therefore keyed under, `openai`. The
151
+ * separate owning-vendor-api `openai` platform family is deliberately
152
+ * excluded -- it never resolves through `accountAuthor` and stays outside
153
+ * this subscription-value report's scope.
154
+ */
155
+ function offlineManagedModels(): readonly OfflineCatalogModelSource[] {
156
+ const models: OfflineCatalogModelSource[] = [];
157
+ for (const model of Object.values(ANTHROPIC_MODELS)) {
158
+ models.push({ vendor: "anthropic", id: model.id, cost: model.cost });
159
+ }
160
+ for (const model of Object.values(OPENAI_CODEX_MODELS)) {
161
+ models.push({ vendor: "openai", id: model.id, cost: model.cost });
162
+ }
163
+ return models;
164
+ }
165
+
166
+ /**
167
+ * The standalone CLI's read-only, offline Pi-catalog source. The pinned
168
+ * `@earendil-works/pi-ai` dependency ships the exact same per-response tier
169
+ * metadata (`model.cost`/`model.cost.tiers`, matched by `inputTokensAbove`)
170
+ * that a live `ExtensionContext.modelRegistry` exposes to the extension's
171
+ * slash/tool surfaces (see `src/index.ts`'s own live-registry adapter).
172
+ * Reading it here is a local package import already resolved by Node's own
173
+ * module loader, never a network call or a write, so the report stays
174
+ * offline even on an empty first run. Returns `undefined` (never throws) so
175
+ * a shape this parser rejects degrades to the pre-existing
176
+ * OpenRouter-snapshot-only pricing instead of failing the whole report.
177
+ */
178
+ function buildOfflinePiCatalogSnapshot(nowMs: number): PiCatalogSnapshot | undefined {
179
+ const costsBySourceModelId: Record<string, unknown> = {};
180
+ for (const model of offlineManagedModels()) {
181
+ costsBySourceModelId[`${model.vendor}/${model.id}`] = {
182
+ input: model.cost.input,
183
+ output: model.cost.output,
184
+ cacheRead: model.cost.cacheRead,
185
+ cacheWrite: model.cost.cacheWrite,
186
+ ...(model.cost.tiers === undefined ? {} : { tiers: model.cost.tiers }),
187
+ };
188
+ }
189
+ try {
190
+ return parsePiCatalogSnapshot({
191
+ schemaVersion: 1,
192
+ source: "pi-installed-catalog",
193
+ catalogVersion: resolveInstalledPiAiVersion(),
194
+ capturedAtMs: nowMs,
195
+ costsBySourceModelId,
196
+ });
197
+ } catch {
198
+ return undefined;
199
+ }
200
+ }
201
+
202
+ /** Binds the pure cost-report projection to `configPath` and the offline catalog above. Never itself contacts a provider or writes anything. */
203
+ function buildCostReportReader(
204
+ configPath: string,
205
+ now: () => number,
206
+ ): CostReportReader {
207
+ return createDefaultCostReportReader({
208
+ config: () => readConfig(configPath),
209
+ now,
210
+ piCatalog: () => buildOfflinePiCatalogSnapshot(now()),
211
+ });
212
+ }
213
+
214
+ class CostCliError extends Error {
215
+ constructor(message: string) {
216
+ super(message);
217
+ this.name = "CostCliError";
218
+ }
219
+ }
220
+
221
+ const VALID_FORMATS = new Set(["text", "json"]);
222
+
223
+ function validTimeZone(timeZone: string): boolean {
224
+ try {
225
+ new Intl.DateTimeFormat("en-US", { timeZone });
226
+ return true;
227
+ } catch {
228
+ return false;
229
+ }
230
+ }
231
+
232
+ type CostCliRangeSelector =
233
+ | { readonly kind: "period"; readonly periodType: PeriodType; readonly timeZone: string }
234
+ | {
235
+ readonly kind: "custom";
236
+ readonly fromRaw: string;
237
+ readonly toRaw: string;
238
+ readonly timeZone: string;
239
+ }
240
+ | { readonly kind: "all-history" };
241
+
242
+ interface CostReportCliRequest {
243
+ readonly selector: CostCliRangeSelector;
244
+ readonly format: "text" | "json";
245
+ }
246
+
247
+ /**
248
+ * Parses `cost`'s report flags only; `refresh-pricing`/`close-periods` are
249
+ * dispatched before this ever runs. `--period`, paired `--from`/`--to`, and
250
+ * `--all-history` are mutually exclusive; `--format` defaults to `text` and
251
+ * `--timezone` defaults to `UTC`. Never infers a partial `--from`/`--to` pair
252
+ * or silently drops an unsupported flag.
253
+ */
254
+ function parseCostReportArgs(args: readonly string[]): CostReportCliRequest {
255
+ let period: string | undefined;
256
+ let sawPeriodFlag = false;
257
+ let fromRaw: string | undefined;
258
+ let toRaw: string | undefined;
259
+ let sawAllHistory = false;
260
+ let format: string | undefined;
261
+ let timeZone: string | undefined;
262
+
263
+ for (let index = 0; index < args.length; index += 1) {
264
+ const token = args[index];
265
+ if (token === "--period") {
266
+ sawPeriodFlag = true;
267
+ period = args[index + 1];
268
+ index += 1;
269
+ } else if (token === "--from") {
270
+ fromRaw = args[index + 1];
271
+ index += 1;
272
+ } else if (token === "--to") {
273
+ toRaw = args[index + 1];
274
+ index += 1;
275
+ } else if (token === "--all-history") {
276
+ sawAllHistory = true;
277
+ } else if (token === "--format") {
278
+ format = args[index + 1];
279
+ index += 1;
280
+ } else if (token === "--timezone") {
281
+ timeZone = args[index + 1];
282
+ index += 1;
283
+ } else {
284
+ throw new CostCliError(`Unsupported argument "${token}".`);
285
+ }
286
+ }
287
+
288
+ const selectorCount =
289
+ (sawPeriodFlag ? 1 : 0) +
290
+ (fromRaw !== undefined || toRaw !== undefined ? 1 : 0) +
291
+ (sawAllHistory ? 1 : 0);
292
+ if (selectorCount > 1) {
293
+ throw new CostCliError(
294
+ "--period, --from/--to, and --all-history are mutually exclusive.",
295
+ );
296
+ }
297
+ if ((fromRaw === undefined) !== (toRaw === undefined)) {
298
+ throw new CostCliError("--from and --to must both be supplied together.");
299
+ }
300
+
301
+ const resolvedFormat = format ?? "text";
302
+ if (!VALID_FORMATS.has(resolvedFormat)) {
303
+ throw new CostCliError(
304
+ `--format must be "text" or "json", not "${resolvedFormat}".`,
305
+ );
306
+ }
307
+
308
+ const resolvedTimeZone = timeZone ?? "UTC";
309
+ if (!validTimeZone(resolvedTimeZone)) {
310
+ throw new CostCliError(
311
+ `"${resolvedTimeZone}" is not a supported IANA timezone.`,
312
+ );
313
+ }
314
+
315
+ let selector: CostCliRangeSelector;
316
+ if (sawAllHistory) {
317
+ selector = { kind: "all-history" };
318
+ } else if (fromRaw !== undefined && toRaw !== undefined) {
319
+ selector = { kind: "custom", fromRaw, toRaw, timeZone: resolvedTimeZone };
320
+ } else {
321
+ const resolvedPeriod = period ?? "month";
322
+ if (!PERIOD_TYPES.includes(resolvedPeriod as PeriodType)) {
323
+ throw new CostCliError(
324
+ `--period must be one of ${PERIOD_TYPES.join(", ")}, not "${resolvedPeriod}".`,
325
+ );
326
+ }
327
+ selector = {
328
+ kind: "period",
329
+ periodType: resolvedPeriod as PeriodType,
330
+ timeZone: resolvedTimeZone,
331
+ };
332
+ }
333
+
334
+ return { selector, format: resolvedFormat as "text" | "json" };
335
+ }
336
+
337
+ /**
338
+ * A bare `--period` request carries no timezone slot in
339
+ * {@link CostReportRangeRequest} (see `cost-report-reader.ts`): only its
340
+ * `"period"` and `"custom"` variants do. For the default `UTC` timezone this
341
+ * passes the bare {@link PeriodType} straight through, byte-identical to the
342
+ * existing slash/tool compatibility path. A non-UTC `--timezone` resolves
343
+ * through the reader's `"period"` request kind instead, which dispatches
344
+ * directly to `buildCostReport`'s own `periodType`/`timeZone` bounds path --
345
+ * never through `"custom"` -- so the selected calendar period keeps its
346
+ * correct zoned bounds and "current unfinished period" semantics rather than
347
+ * collapsing to a single exact-bounds custom window.
348
+ */
349
+ function resolveRangeRequest(selector: CostCliRangeSelector): CostReportRangeRequest {
350
+ if (selector.kind === "all-history") return { kind: "all-history" };
351
+ if (selector.kind === "custom") {
352
+ return {
353
+ kind: "custom",
354
+ fromRaw: selector.fromRaw,
355
+ toRaw: selector.toRaw,
356
+ timeZone: selector.timeZone,
357
+ };
358
+ }
359
+ if (selector.timeZone === "UTC") return selector.periodType;
360
+ return {
361
+ kind: "period",
362
+ periodType: selector.periodType,
363
+ timeZone: selector.timeZone,
364
+ };
365
+ }
366
+
367
+ async function runCostReport(
368
+ args: readonly string[],
369
+ deps: {
370
+ readonly stdout: Pick<NodeJS.WritableStream, "write">;
371
+ readonly stderr: Pick<NodeJS.WritableStream, "write">;
372
+ readonly configPath: string;
373
+ readonly now: () => number;
374
+ },
375
+ ): Promise<number> {
376
+ let parsed: CostReportCliRequest;
377
+ let range: CostReportRangeRequest;
378
+ try {
379
+ parsed = parseCostReportArgs(args);
380
+ range = resolveRangeRequest(parsed.selector);
381
+ } catch (error) {
382
+ deps.stderr.write(`multi-account: ${(error as Error).message}\n${COST_USAGE}`);
383
+ return EXIT_VALIDATION_FAILURE;
384
+ }
385
+
386
+ const reader = buildCostReportReader(deps.configPath, deps.now);
387
+ try {
388
+ const report = reader(range);
389
+ const rendered =
390
+ parsed.format === "json" ? renderCostReportJson(report) : renderCostReport(report);
391
+ deps.stdout.write(`${rendered}\n`);
392
+ return EXIT_SUCCESS;
393
+ } catch (error) {
394
+ if (error instanceof CostReportRangeRequestError) {
395
+ deps.stderr.write(`multi-account: ${error.message}\n`);
396
+ return error.exitCode;
397
+ }
398
+ if (error instanceof ConfigValidationError) {
399
+ deps.stderr.write(`multi-account: ${error.message}\n`);
400
+ return EXIT_UNEXPECTED_FAILURE;
401
+ }
402
+ deps.stderr.write(
403
+ `multi-account: retained cost history could not be read: ${(error as Error).message}\n`,
404
+ );
405
+ return EXIT_CORRUPT_RETAINED_INPUT;
406
+ }
407
+ }
408
+
409
+ async function runRefreshPricing(deps: {
410
+ readonly stdout: Pick<NodeJS.WritableStream, "write">;
411
+ readonly stderr: Pick<NodeJS.WritableStream, "write">;
412
+ }): Promise<number> {
413
+ const cache = new OpenRouterPricingCache(defaultPricingCachePaths());
414
+ let result: PricingCacheResult;
415
+ try {
416
+ result = await cache.refreshIfNeeded();
417
+ } catch (error) {
418
+ deps.stderr.write(
419
+ `multi-account: pricing refresh failed: ${(error as Error).message}\n`,
420
+ );
421
+ return EXIT_ACTION_FAILURE;
422
+ }
423
+ if (result.state === "fresh") {
424
+ deps.stdout.write(
425
+ `Pricing cache refreshed (${Object.keys(result.snapshot.rates).length} rate(s), fetched ${new Date(result.snapshot.fetchedAtMs).toISOString()}).\n`,
426
+ );
427
+ return EXIT_SUCCESS;
428
+ }
429
+ deps.stderr.write(`multi-account: pricing refresh failed (${result.reason}).\n`);
430
+ return EXIT_ACTION_FAILURE;
431
+ }
432
+
433
+ async function runClosePeriods(deps: {
434
+ readonly stdout: Pick<NodeJS.WritableStream, "write">;
435
+ readonly stderr: Pick<NodeJS.WritableStream, "write">;
436
+ }): Promise<number> {
437
+ const closer = createDefaultCostPeriodCloser();
438
+ let result: CostPeriodCloseResult;
439
+ try {
440
+ result = await closer.closeCompletedPeriods();
441
+ } catch (error) {
442
+ deps.stderr.write(
443
+ `multi-account: period closure failed: ${(error as Error).message}\n`,
444
+ );
445
+ return EXIT_ACTION_FAILURE;
446
+ }
447
+ if (result.status === "closed") {
448
+ deps.stdout.write(`Closed ${result.appendedRows} cost period row(s).\n`);
449
+ return EXIT_SUCCESS;
450
+ }
451
+ if (result.status === "no-op") {
452
+ deps.stdout.write("No completed cost periods were ready to close.\n");
453
+ return EXIT_SUCCESS;
454
+ }
455
+ const reason =
456
+ result.status === "lease-held"
457
+ ? "another writer holds the digest lease"
458
+ : result.status === "skipped"
459
+ ? "already attempted for the current day"
460
+ : "the digest write did not complete";
461
+ deps.stderr.write(`multi-account: period closure failed (${reason}).\n`);
462
+ return EXIT_ACTION_FAILURE;
463
+ }
464
+
465
+ const USAGE =
466
+ 'Usage: multi-account account set-plan <account-id> --type <preset-id> --effective-from <timestamp> [--monthly-usd <amount>]\n' +
467
+ ' <timestamp> must be an RFC 3339 instant with an explicit "Z" or numeric offset.\n';
468
+
469
+ const COST_USAGE =
470
+ "Usage: multi-account cost [--period day|week|month|quarter|half-year|year]\n" +
471
+ " [--from <bound> --to <bound>] [--all-history]\n" +
472
+ " [--format text|json] [--timezone <iana-zone>]\n" +
473
+ " multi-account cost refresh-pricing\n" +
474
+ " multi-account cost close-periods\n";
475
+
476
+ const SET_PLAN_HELP =
477
+ "multi-account account set-plan - assign an editable-catalog preset as an account's effective rate record.\n\n" +
478
+ "multi-account account set-plan <account-id> --type <preset-id> --effective-from <timestamp> [--monthly-usd <amount>]\n\n" +
479
+ " <account-id> Canonical account id (for example anthropic, anthropic-account-2).\n" +
480
+ " --type <preset-id> Shipped or operator-added catalog preset id.\n" +
481
+ ' --effective-from <timestamp> Required RFC 3339 instant with an explicit "Z" or numeric offset; never inferred, never a history start or renewal boundary.\n' +
482
+ " --monthly-usd <amount> Optional override; an explicit 0 is a valid rate and differs from having no rate at all.\n\n" +
483
+ "Resolves the preset, prints a preview naming the account, provider, account type, preset, monthly rate, effective instant, and catalog version, then asks for a literal \"y\"/\"yes\" confirmation before writing anything.\n" +
484
+ "The preset's terms, label, catalog version, and provenance are copied into the new immutable rate record at write time; a later catalog edit never rewrites an existing record or a past report result.\n" +
485
+ "Performs no configuration migration: nothing here rewrites the legacy monthlySubscriptionUsd value or invents earlier history.\n\n" +
486
+ "Exit codes: 0 success (including a declined confirmation), 1 unexpected internal failure, 2 syntax/argument/domain validation failure.\n";
487
+
488
+ const COST_HELP =
489
+ "multi-account cost - read-only, offline subscription-value report.\n\n" +
490
+ COST_USAGE +
491
+ "\n" +
492
+ " --period <day|week|month|quarter|half-year|year> Calendar period (default: month).\n" +
493
+ " --from <bound> --to <bound> Explicit custom range, start-inclusive and end-exclusive.\n" +
494
+ " --all-history Every retained period, without double-counting overlapping rollups.\n" +
495
+ " --format <text|json> Output shape (default: text). json is one versioned document.\n" +
496
+ " --timezone <iana-zone> IANA zone for calendar boundaries and date-only bounds (default: UTC). Account-cost allocation always splits at UTC month boundaries regardless of this zone.\n\n" +
497
+ "--period, --from/--to, and --all-history are mutually exclusive.\n" +
498
+ 'A custom bound is an ISO date (YYYY-MM-DD) or an RFC 3339 timestamp with an explicit "Z" or numeric offset; an offsetless date-time is rejected.\n' +
499
+ "This command is read-only and offline, including on an empty first run: it never contacts a provider, refreshes pricing, closes a period, migrates configuration, edits an account rate, or creates a catalog.\n\n" +
500
+ "multi-account cost refresh-pricing Explicit action: refresh the cached OpenRouter API-equivalent rate snapshot (network, write).\n" +
501
+ "multi-account cost close-periods Explicit action: close completed cost periods into the retained digest (network, write; governed by the existing machine lease).\n\n" +
502
+ "Exit codes: 0 success (including a truthful empty report), 1 unexpected internal failure, 2 syntax/argument error, 3 requested precision unavailable, 4 corrupt retained cost history, 5 explicit action failure.\n";
503
+
504
+ const TOP_LEVEL_USAGE =
505
+ "multi-account - standalone subscription-value reporting and account plan assignment.\n\n" +
506
+ "Commands:\n" +
507
+ " cost [selector] [--format text|json] [--timezone <iana-zone>] Read-only, offline subscription-value report.\n" +
508
+ " cost refresh-pricing Explicit pricing-cache refresh action.\n" +
509
+ " cost close-periods Explicit period-closure action.\n" +
510
+ " account set-plan <account-id> --type <preset-id> --effective-from <timestamp> [--monthly-usd <amount>]\n" +
511
+ " Assign a catalog preset as an account's effective rate.\n\n" +
512
+ "Run `multi-account cost --help` or `multi-account account set-plan --help` for command-specific detail.\n";
513
+
514
+ /**
515
+ * Parses `account set-plan` arguments. Never infers or defaults
516
+ * `effectiveFrom`: a missing or empty `--effective-from` value is rejected
517
+ * here, identically whether or not the target account already has history.
518
+ */
519
+ function parseSetPlanArgs(args: readonly string[]): AccountPlanAssignmentInput {
520
+ const positionals: string[] = [];
521
+ let presetId: string | undefined;
522
+ let effectiveFrom: string | undefined;
523
+ let sawEffectiveFromFlag = false;
524
+ let monthlyUsdOverride: number | undefined;
525
+
526
+ for (let index = 0; index < args.length; index += 1) {
527
+ const token = args[index];
528
+ if (token === "--type") {
529
+ presetId = args[index + 1];
530
+ index += 1;
531
+ } else if (token === "--effective-from") {
532
+ sawEffectiveFromFlag = true;
533
+ effectiveFrom = args[index + 1];
534
+ index += 1;
535
+ } else if (token === "--monthly-usd") {
536
+ const raw = args[index + 1];
537
+ index += 1;
538
+ if (raw === undefined) {
539
+ throw new AccountPlanAssignmentError("--monthly-usd requires a value.");
540
+ }
541
+ const parsedAmount = Number(raw);
542
+ if (!Number.isFinite(parsedAmount)) {
543
+ throw new AccountPlanAssignmentError(
544
+ `--monthly-usd "${raw}" is not a finite number.`,
545
+ );
546
+ }
547
+ monthlyUsdOverride = parsedAmount;
548
+ } else if (token !== undefined && token.startsWith("--")) {
549
+ throw new AccountPlanAssignmentError(`Unsupported flag "${token}".`);
550
+ } else if (token !== undefined) {
551
+ positionals.push(token);
552
+ }
553
+ }
554
+
555
+ if (positionals.length !== 1) {
556
+ throw new AccountPlanAssignmentError(
557
+ "account set-plan requires exactly one <account-id> argument.",
558
+ );
559
+ }
560
+ if (presetId === undefined || presetId.length === 0) {
561
+ throw new AccountPlanAssignmentError(
562
+ "account set-plan requires --type <preset-id>.",
563
+ );
564
+ }
565
+ // A missing flag, a present-but-empty value, and (by construction, since
566
+ // this function never derives one) any reliance on a renewal or history
567
+ // default are rejected identically here.
568
+ if (!sawEffectiveFromFlag || effectiveFrom === undefined || effectiveFrom.length === 0) {
569
+ throw new AccountPlanAssignmentError(
570
+ 'account set-plan requires --effective-from <timestamp> as an RFC 3339 instant with an explicit "Z" or numeric offset.',
571
+ );
572
+ }
573
+
574
+ return {
575
+ accountId: positionals[0] as string,
576
+ presetId,
577
+ effectiveFrom,
578
+ ...(monthlyUsdOverride !== undefined ? { monthlyUsdOverride } : {}),
579
+ };
580
+ }
581
+
582
+ function formatPreview(preview: AccountPlanAssignmentPreview): string {
583
+ const rateLine = preview.isOverride
584
+ ? `$${preview.monthlyUsd.toFixed(2)}/mo (explicit override)`
585
+ : `$${preview.monthlyUsd.toFixed(2)}/mo (catalog default)`;
586
+ return [
587
+ "Account plan assignment preview:",
588
+ ` account: ${preview.accountId}`,
589
+ ` provider: ${preview.provider}`,
590
+ ` account type: ${preview.accountType}`,
591
+ ` preset: ${preview.presetId} (${preview.presetLabel})`,
592
+ ` monthly rate: ${rateLine}`,
593
+ ` effective from: ${preview.effectiveFrom}`,
594
+ ` catalog version: ${preview.catalogVersion}`,
595
+ "",
596
+ ].join("\n");
597
+ }
598
+
599
+ /**
600
+ * Reads exactly one line from `input` and accepts only an exact (trimmed,
601
+ * case-insensitive) "y" or "yes" as confirmation. EOF without a line, or any
602
+ * other input, declines.
603
+ */
604
+ async function readConfirmation(input: NodeJS.ReadableStream): Promise<boolean> {
605
+ const rl = createInterface({ input, terminal: false });
606
+ try {
607
+ for await (const line of rl) {
608
+ const normalized = line.trim().toLowerCase();
609
+ return normalized === "y" || normalized === "yes";
610
+ }
611
+ return false;
612
+ } finally {
613
+ rl.close();
614
+ }
615
+ }
616
+
617
+ async function runSetPlan(
618
+ args: readonly string[],
619
+ deps: Required<Pick<StandaloneCliDependencies, "stdout" | "stderr" | "stdin">> &
620
+ Pick<StandaloneCliDependencies, "configPath" | "lockPath">,
621
+ ): Promise<number> {
622
+ let input: AccountPlanAssignmentInput;
623
+ try {
624
+ input = parseSetPlanArgs(args);
625
+ } catch (error) {
626
+ deps.stderr.write(`multi-account: ${(error as Error).message}\n${USAGE}`);
627
+ return EXIT_VALIDATION_FAILURE;
628
+ }
629
+
630
+ const configPath = deps.configPath ?? defaultConfigPath();
631
+
632
+ try {
633
+ const result = await commitAccountPlanAssignment({
634
+ configPath,
635
+ ...(deps.lockPath !== undefined ? { lockPath: deps.lockPath } : {}),
636
+ input,
637
+ onPreview: (preview) => {
638
+ deps.stdout.write(formatPreview(preview));
639
+ },
640
+ confirm: async () => {
641
+ deps.stdout.write("Apply this account plan assignment? [y/N] ");
642
+ return readConfirmation(deps.stdin);
643
+ },
644
+ });
645
+ if (result.status === "declined") {
646
+ deps.stdout.write("No change was written.\n");
647
+ return EXIT_SUCCESS;
648
+ }
649
+ if (result.status === "busy") {
650
+ deps.stderr.write(
651
+ "multi-account: configuration is locked by another writer; try again.\n",
652
+ );
653
+ return EXIT_UNEXPECTED_FAILURE;
654
+ }
655
+ deps.stdout.write(
656
+ `Recorded ${result.record.presetLabel} for ${result.record.accountId}, effective ${result.record.effectiveFrom}.\n`,
657
+ );
658
+ return EXIT_SUCCESS;
659
+ } catch (error) {
660
+ if (
661
+ error instanceof AccountPlanAssignmentError ||
662
+ error instanceof AccountRateHistoryError ||
663
+ error instanceof ConfigValidationError
664
+ ) {
665
+ deps.stderr.write(`multi-account: ${error.message}\n`);
666
+ return EXIT_VALIDATION_FAILURE;
667
+ }
668
+ deps.stderr.write(`multi-account: ${(error as Error).message}\n`);
669
+ return EXIT_UNEXPECTED_FAILURE;
670
+ }
671
+ }
672
+
673
+ export async function runStandaloneCli(
674
+ argv: readonly string[],
675
+ deps: StandaloneCliDependencies = {},
676
+ ): Promise<number> {
677
+ const stdout = deps.stdout ?? process.stdout;
678
+ const stderr = deps.stderr ?? process.stderr;
679
+ const stdin = deps.stdin ?? process.stdin;
680
+ const now = deps.now ?? Date.now;
681
+
682
+ if (argv[0] === "--help" || argv[0] === "help") {
683
+ stdout.write(TOP_LEVEL_USAGE);
684
+ return EXIT_SUCCESS;
685
+ }
686
+
687
+ if (argv[0] === "account" && argv[1] === "set-plan") {
688
+ const rest = argv.slice(2);
689
+ if (rest.includes("--help")) {
690
+ stdout.write(SET_PLAN_HELP);
691
+ return EXIT_SUCCESS;
692
+ }
693
+ return runSetPlan(rest, {
694
+ stdout,
695
+ stderr,
696
+ stdin,
697
+ ...(deps.configPath !== undefined ? { configPath: deps.configPath } : {}),
698
+ ...(deps.lockPath !== undefined ? { lockPath: deps.lockPath } : {}),
699
+ });
700
+ }
701
+
702
+ if (argv[0] === "cost") {
703
+ const rest = argv.slice(1);
704
+ if (rest.includes("--help")) {
705
+ stdout.write(COST_HELP);
706
+ return EXIT_SUCCESS;
707
+ }
708
+ if (rest.length === 1 && rest[0] === "refresh-pricing") {
709
+ return runRefreshPricing({ stdout, stderr });
710
+ }
711
+ if (rest.length === 1 && rest[0] === "close-periods") {
712
+ return runClosePeriods({ stdout, stderr });
713
+ }
714
+ const configPath = deps.configPath ?? defaultConfigPath();
715
+ return runCostReport(rest, { stdout, stderr, configPath, now });
716
+ }
717
+
718
+ stderr.write(`multi-account: unsupported command "${argv.join(" ")}".\n${TOP_LEVEL_USAGE}`);
719
+ return EXIT_VALIDATION_FAILURE;
720
+ }