@omnicross/daemon 0.1.0 → 0.1.2

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.
package/dist/index.d.ts CHANGED
@@ -1,10 +1,12 @@
1
1
  import { ApiKeyPoolService } from '@omnicross/core/completion/ApiKeyPoolService';
2
2
  import { OutboundKeyDb, OutboundApiServer } from '@omnicross/core/outbound-api';
3
3
  import { ProviderProxy } from '@omnicross/core/provider-proxy';
4
+ import { UsageRecorder, PricingEngine } from '@omnicross/core/usage';
4
5
  import { SubscriptionCredentialStore, FetchLike, SubscriptionProviderRegistry, SubscriptionAccountService } from '@omnicross/subscriptions';
5
- import { OutboundApiServerConfig, ProviderConfigSource, TransformerService, Transformer, ResolvedTransformerChain, ApiServerSettingsStore, Logger, OutboundKeyDb as OutboundKeyDb$1, OutboundKeyDbRow } from '@omnicross/core';
6
+ import { OutboundApiServerConfig, ProviderConfigSource, TransformerService, Transformer, ResolvedTransformerChain, ApiServerSettingsStore, PricingStore, Logger, OutboundKeyDb as OutboundKeyDb$1, OutboundKeyDbRow } from '@omnicross/core';
6
7
  import http from 'node:http';
7
8
  import { LLMProvider, AgentDefaultModels, GlobalModelParameters } from '@omnicross/contracts/llm-config';
9
+ import { PricingEntry, PricingEntryInput, PricingResolution } from '@omnicross/contracts/pricing-types';
8
10
  import { OpenCodeGoTokenConfig, SubscriptionProviderId } from '@omnicross/contracts/subscription-types';
9
11
  import { ClaudeTokenConfig, CodexTokenConfig, GeminiTokenConfig, AccountTokensConfig, SubscriptionAccountSanitized } from '@omnicross/contracts/account-tokens-types';
10
12
 
@@ -391,6 +393,96 @@ declare function loadConfig(path: string): DaemonConfig;
391
393
  * `enc:` untouched) before serializing. */
392
394
  declare function saveConfig(path: string, cfg: DaemonConfig): void;
393
395
 
396
+ /**
397
+ * external-cli-credentials — read-only access to the external CLI native
398
+ * credential stores (external-cli-sync).
399
+ *
400
+ * The daemon never WRITES these files (they belong to the CLIs); it only reads
401
+ * them back to (a) recover from the rotating-refresh-token race — when e.g.
402
+ * Claude Code refreshes `~/.claude/.credentials.json` it rotates OUR stored
403
+ * refresh token out from under us, and the external file then holds the only
404
+ * live credential — and (b) detect divergence for the account-list warning.
405
+ *
406
+ * File shapes (mirrors the shapes the CLIs themselves write):
407
+ * claude `~/.claude/.credentials.json`
408
+ * → `{ claudeAiOauth: { accessToken, refreshToken?, expiresAt(number ms),
409
+ * scopes? } }`
410
+ * codex `~/.codex/auth.json`
411
+ * → `{ tokens: { id_token?, access_token, refresh_token? } }` — no explicit
412
+ * expiry; the access token's JWT `exp` claim is the only expiry signal.
413
+ *
414
+ * Gemini is deliberately excluded: the gemini CLI's oauth store is not a
415
+ * supported import source (parity with the host app's external-sync scope).
416
+ *
417
+ * @module @omnicross/daemon/ports/external-cli-credentials
418
+ */
419
+ /** The two external CLI providers with a readable native store. */
420
+ type ExternalCliProvider = 'claude' | 'codex';
421
+ /**
422
+ * External credentials parsed OUT of a native store file. Field axes match the
423
+ * internal `*TokenConfig` shapes: `expiresAt` is an ISO STRING here (converted
424
+ * from the native ms number / codex JWT `exp`).
425
+ */
426
+ interface ExternalCliCredentials {
427
+ accessToken?: string;
428
+ refreshToken?: string;
429
+ /** ISO string. */
430
+ expiresAt?: string;
431
+ /** codex only. */
432
+ idToken?: string;
433
+ /** claude only. */
434
+ scopes?: string[];
435
+ }
436
+ /** Reader port — injectable so tests never touch the real home directory. */
437
+ type ExternalCliReader = (provider: ExternalCliProvider) => ExternalCliCredentials | null;
438
+
439
+ /**
440
+ * external-cli-store — WRITE side of the external CLI native credential stores
441
+ * (external-cli-sync, import + write-back).
442
+ *
443
+ * Counterpart of `external-cli-credentials` (the read side). The daemon only
444
+ * ever writes a file it MANAGES: an explicit "import existing CLI login" puts
445
+ * an `.omnicross-managed` marker (recording the owning account id) next to the
446
+ * native store, and every subsequent successful refresh of THAT account writes
447
+ * the rotated credential back into the file. Without the write-back, the
448
+ * daemon's refresh would rotate the single-use refresh token and silently log
449
+ * the user's bare CLI out — the write-back keeps both sides on the same live
450
+ * credential.
451
+ *
452
+ * Safety properties:
453
+ * - NEVER writes without a matching marker (an unmanaged / foreign-account
454
+ * file is untouched);
455
+ * - read-then-merge: unrelated top-level keys in the native file (e.g.
456
+ * claude `email`, codex `OPENAI_API_KEY`) are preserved;
457
+ * - one-time `.omnicross-backup` of the original file before the FIRST
458
+ * overwrite (restorable by hand if the user wants the daemon out);
459
+ * - atomic write (temp file → rename) so a crash never leaves a torn file.
460
+ *
461
+ * The envelope shapes mirror what the CLIs themselves write — the round-trip
462
+ * test parses a written file back through `external-cli-credentials` to keep
463
+ * the two sides from drifting.
464
+ *
465
+ * @module @omnicross/daemon/ports/external-cli-store
466
+ */
467
+
468
+ /** Token blocks the write-back accepts (the two external-store providers). */
469
+ type ExternalWritableTokens = ClaudeTokenConfig | CodexTokenConfig;
470
+ /**
471
+ * Injectable port over the external store writes (tests use an in-memory fake;
472
+ * the store wires `realExternalCliStore`).
473
+ */
474
+ interface ExternalCliStorePort {
475
+ /** The owning account id recorded by the marker, or undefined when unmanaged. */
476
+ readMarkerAccountId(provider: ExternalCliProvider): string | undefined;
477
+ /** Record (or move) ownership of the provider's native store to an account. */
478
+ writeMarker(provider: ExternalCliProvider, accountId: string): void;
479
+ /**
480
+ * Write the refreshed tokens back into the native store — ONLY when the
481
+ * marker names `accountId`. Returns true when a write happened.
482
+ */
483
+ writeBack(provider: ExternalCliProvider, accountId: string, tokens: ExternalWritableTokens): boolean;
484
+ }
485
+
394
486
  /**
395
487
  * JsonSubscriptionCredentialStore — the daemon's file-backed
396
488
  * `SubscriptionCredentialStore` port impl (design D1).
@@ -452,6 +544,10 @@ declare class JsonSubscriptionCredentialStore implements SubscriptionCredentialS
452
544
  private readonly tokensPath;
453
545
  private readonly box;
454
546
  private readonly fetchImpl;
547
+ /** Injectable external CLI native-store reader (external-cli-sync). */
548
+ private readonly externalCliReader;
549
+ /** Injectable external CLI native-store WRITER (marker-gated write-back). */
550
+ private readonly externalCliStore;
455
551
  /**
456
552
  * @param tokensPath on-disk `tokens.json` location.
457
553
  * @param box at-rest `SecretBox` (encrypt-on-write / decrypt-on-read).
@@ -460,7 +556,20 @@ declare class JsonSubscriptionCredentialStore implements SubscriptionCredentialS
460
556
  * is unchanged; tests inject a mock fetch. NOT used by any
461
557
  * read/write path — only by `refresh*Token`.
462
558
  */
463
- constructor(tokensPath: string, box: SecretBox, fetchImpl?: FetchLike);
559
+ constructor(tokensPath: string, box: SecretBox, fetchImpl?: FetchLike,
560
+ /** Injectable external CLI native-store reader (external-cli-sync). */
561
+ externalCliReader?: ExternalCliReader,
562
+ /** Injectable external CLI native-store WRITER (marker-gated write-back). */
563
+ externalCliStore?: ExternalCliStorePort);
564
+ /**
565
+ * In-flight refresh coalescing (external-cli-sync). OAuth refresh tokens are
566
+ * SINGLE-USE: two concurrent refreshes of one account each spend the same
567
+ * token and the loser bricks a healthy account. Every refresh entry point
568
+ * (auth-strategy lazy refresh, 401 retry, background scheduler) funnels
569
+ * through `coalesce`, so overlapping callers share ONE upstream round-trip.
570
+ */
571
+ private readonly inFlightRefreshes;
572
+ private coalesce;
464
573
  /** Full parsed account-tokens config (or a minimal `{ updatedAt }` when the
465
574
  * file is absent/corrupt). This is the hot read — the codex / gemini auth
466
575
  * strategies pull `accessToken` / `expiresAt` / `status` from it. */
@@ -478,6 +587,17 @@ declare class JsonSubscriptionCredentialStore implements SubscriptionCredentialS
478
587
  * Used by the admin accounts GET (secret-IN-never-OUT).
479
588
  */
480
589
  listSanitizedAccounts(): Promise<Record<string, SubscriptionAccountSanitized[]>>;
590
+ /**
591
+ * List-time credential-conflict warnings (external-cli-sync). Computed, not
592
+ * persisted: (a) `duplicate-token` when two accounts of one provider share a
593
+ * credential, (b) `external-divergent` when the external CLI native store has
594
+ * rotated PAST the ACTIVE account (claude/codex only). A warning persisted by
595
+ * a failed refresh (`external-not-rotated`) takes precedence — it is the most
596
+ * actionable state.
597
+ */
598
+ private attachSyncWarnings;
599
+ /** Read the external CLI store, never letting an fs/parse error escape. */
600
+ private safeReadExternal;
481
601
  /**
482
602
  * Refresh the Claude OAuth access token (oauth design D4). HONEST `false` when
483
603
  * the block has no refresh_token (setup-token / manual) — no upstream call, the
@@ -501,6 +621,58 @@ declare class JsonSubscriptionCredentialStore implements SubscriptionCredentialS
501
621
  * destroy the ability to refresh again). HONEST `false` when no refresh_token.
502
622
  */
503
623
  refreshGeminiToken(): Promise<boolean>;
624
+ /**
625
+ * Refresh a SPECIFIC account by id (background scheduler sweep,
626
+ * external-cli-sync). Unlike the active-account refreshers it does NOT
627
+ * attempt the external-import fallback — the external CLI file's lineage can
628
+ * only plausibly match the ACTIVE account. Coalesced per `provider:id`; on
629
+ * failure flags ONLY that account `expired`.
630
+ */
631
+ refreshAccountById(provider: 'claude' | 'codex' | 'gemini', id: string): Promise<boolean>;
632
+ /** Dispatch one OAuth refresh round-trip to the provider's shared flow. */
633
+ private refreshUpstream;
634
+ /**
635
+ * External-import fallback for a FAILED active-account refresh
636
+ * (external-cli-sync). Reads the CLI native store; imports when the external
637
+ * lineage ROTATED (different refresh token) or its access token is still
638
+ * valid. When the imported access token is already expired it refreshes once
639
+ * with the rotated refresh token. A `not-rotated` outcome persists the
640
+ * `external-not-rotated` warning on the (about-to-be-expired) account so the
641
+ * UI can tell "genuine revocation" apart from a plain refresh failure.
642
+ */
643
+ private tryExternalImport;
644
+ /**
645
+ * Marker-gated external write-back (external-cli-sync). After a successful
646
+ * refresh of the account that OWNS the provider's native CLI store (imported
647
+ * via `importExternalCliAccount`), push the rotated credential back into the
648
+ * file — otherwise the daemon's refresh invalidates the single-use refresh
649
+ * token and silently logs the bare CLI out. NON-FATAL: the internal store is
650
+ * already persisted; a failed external write only leaves the file stale,
651
+ * which the `external-divergent` warning surfaces.
652
+ */
653
+ private resyncExternal;
654
+ /** Read the marker's owning account id, never letting an fs error escape. */
655
+ private safeReadMarker;
656
+ /**
657
+ * DAEMON-ONLY (admin import button): which providers have a usable external
658
+ * CLI credential on THIS machine. Pure detection — reads the native files,
659
+ * never mutates anything, never returns a token.
660
+ */
661
+ listExternalCliAvailability(): Promise<Record<ExternalCliProvider, boolean>>;
662
+ /**
663
+ * DAEMON-ONLY (admin import button): import the external CLI's current login
664
+ * as a NEW account (+ activate), and take MANAGED ownership of the native
665
+ * store (marker) so subsequent refreshes write back — keeping the bare CLI
666
+ * and the daemon on the same live credential instead of silently killing one
667
+ * side's single-use refresh token.
668
+ */
669
+ importExternalCliAccount(provider: ExternalCliProvider, label?: string): Promise<{
670
+ ok: true;
671
+ id: string;
672
+ } | {
673
+ ok: false;
674
+ reason: 'no-credential';
675
+ }>;
504
676
  /**
505
677
  * Materialize a lazily-synthesized account id to disk (design D3). On a legacy
506
678
  * single-slot file, `readConfig` synthesizes a NON-deterministic account id
@@ -555,6 +727,14 @@ declare class JsonSubscriptionCredentialStore implements SubscriptionCredentialS
555
727
  removeAccount(providerId: SubscriptionProviderId, id: string): Promise<{
556
728
  removed: boolean;
557
729
  }>;
730
+ /**
731
+ * DAEMON-ONLY per-account rename (NOT on the port). Update one account's label;
732
+ * rejects an unknown id. Label-only — no token material is read or written
733
+ * (the secret-free invariant holds).
734
+ */
735
+ renameAccount(providerId: SubscriptionProviderId, id: string, label: string): Promise<{
736
+ ok: boolean;
737
+ }>;
558
738
  /**
559
739
  * DAEMON-ONLY CLEAR (design D1/D3, NOT on the port). Remove a single provider's
560
740
  * block from `tokens.json` and re-persist (the strategies already tolerate an
@@ -614,7 +794,22 @@ interface SubscriptionTokenWriter {
614
794
  removeAccount(providerId: SubscriptionProviderId, id: string): Promise<{
615
795
  removed: boolean;
616
796
  }>;
797
+ /** Rename one account's label (label-only; rejects an unknown id). */
798
+ renameAccount(providerId: SubscriptionProviderId, id: string, label: string): Promise<{
799
+ ok: boolean;
800
+ }>;
617
801
  listSanitizedAccounts(): Promise<Record<string, SubscriptionAccountSanitized[]>>;
802
+ refreshClaudeToken(): Promise<boolean>;
803
+ refreshCodexToken(): Promise<boolean>;
804
+ refreshGeminiToken(): Promise<boolean>;
805
+ listExternalCliAvailability(): Promise<Record<'claude' | 'codex', boolean>>;
806
+ importExternalCliAccount(providerId: 'claude' | 'codex', label?: string): Promise<{
807
+ ok: true;
808
+ id: string;
809
+ } | {
810
+ ok: false;
811
+ reason: 'no-credential';
812
+ }>;
618
813
  }
619
814
 
620
815
  /**
@@ -926,6 +1121,109 @@ declare class JsonApiServerSettingsStore implements ApiServerSettingsStore {
926
1121
  private readFile;
927
1122
  }
928
1123
 
1124
+ /**
1125
+ * JsonPricingStore — the daemon's file-backed `PricingStore` port impl.
1126
+ *
1127
+ * Durable storage for the model pricing table, backed by a pretty-printed json
1128
+ * file (a sibling of `config.json`, `pricing.json` by convention) holding a
1129
+ * `PricingEntry[]`. Reads tolerate a missing/corrupt file (→ empty table);
1130
+ * every mutation rewrites the full array (the table is at most a few thousand
1131
+ * rows — same trade-off as `JsonOutboundKeyDb`). The table starts EMPTY: no
1132
+ * seeding — the first pricing-source refresh (or a manual upsert) populates it.
1133
+ *
1134
+ * Beyond the core port, the store exposes a STORE-LOCAL `delete` (the port is
1135
+ * frozen; the admin DELETE route calls the concrete class and then invalidates
1136
+ * the engine cache).
1137
+ *
1138
+ * @module @omnicross/daemon/ports/JsonPricingStore
1139
+ */
1140
+
1141
+ declare class JsonPricingStore implements PricingStore {
1142
+ private readonly pricingPath;
1143
+ constructor(pricingPath: string);
1144
+ getAll(): Promise<PricingEntry[]>;
1145
+ /**
1146
+ * Insert or update one row keyed (providerId, modelId). `asUserEdit` stamps
1147
+ * user provenance (source 'user', userEdited, editedAt now) so the row is
1148
+ * protected from auto-overwrite during source refreshes; a non-user upsert
1149
+ * stamps source 'litellm' and clears nothing it should not (a plain source
1150
+ * upsert through this method overwrites the row wholesale).
1151
+ */
1152
+ upsert(input: PricingEntryInput, asUserEdit: boolean): Promise<PricingEntry>;
1153
+ /**
1154
+ * Apply a batch fetched from a pricing source. Rows whose local copy is
1155
+ * user-edited are NOT applied — they come back as `{ current, incoming }`
1156
+ * conflicts; everything else is upserted (source 'litellm'). ONE file write
1157
+ * for the whole batch.
1158
+ */
1159
+ bulkApplyFromSource(entries: PricingEntryInput[]): Promise<{
1160
+ applied: PricingEntry[];
1161
+ conflicts: Array<{
1162
+ current: PricingEntry;
1163
+ incoming: PricingEntryInput;
1164
+ }>;
1165
+ }>;
1166
+ /**
1167
+ * Apply per-row conflict decisions: 'overwrite' replaces the local row with
1168
+ * the incoming values (clearing the user-edited mark), 'skip' counts only.
1169
+ */
1170
+ applyResolutions(resolutions: Array<{
1171
+ incoming: PricingEntryInput;
1172
+ action: 'overwrite' | 'skip';
1173
+ }>): Promise<PricingResolution>;
1174
+ /**
1175
+ * STORE-LOCAL (not on the core port): remove one row. Returns whether a row
1176
+ * was actually removed. The admin DELETE handler calls this then invalidates
1177
+ * the engine cache.
1178
+ */
1179
+ delete(providerId: string, modelId: string): Promise<boolean>;
1180
+ /** Upsert into `rows` IN PLACE (no write) and return the resulting entry. */
1181
+ private applyUpsert;
1182
+ /** Read the pricing rows, tolerating a missing/corrupt file (→ empty list). */
1183
+ private readRows;
1184
+ private writeRows;
1185
+ }
1186
+
1187
+ /**
1188
+ * cliLaunch — the admin API's "launch a coding CLI in a terminal, pointed at the
1189
+ * daemon" surface (dashboard parity with the desktop app's Code CLI tab).
1190
+ *
1191
+ * This is the EXTERNAL-terminal analogue of `commands/launch.ts`: it reuses the
1192
+ * same `@omnicross/cli-launcher` builders (which register one route on the
1193
+ * RESIDENT `ProviderProxy` and return the redirect env — `ANTHROPIC_BASE_URL` +
1194
+ * a one-shot ROUTE token, codex's `-c base_url=…` overrides, etc.), then opens a
1195
+ * NEW terminal window running the CLI with that env injected. The route token —
1196
+ * NOT an upstream credential — is the only secret in the env; it is removed when
1197
+ * the session is stopped (`onSessionEnd`).
1198
+ *
1199
+ * SECRET DISCIPLINE: the env carries a route token (proxy-scoped, revocable),
1200
+ * never a provider key. On win32 the token rides the spawned process environment
1201
+ * (inherited by the terminal), never the command line / a file on disk.
1202
+ *
1203
+ * @module @omnicross/daemon/admin/cliLaunch
1204
+ */
1205
+
1206
+ /** Injectable PATH probe (tests stub this; default scans `process.env.PATH`). */
1207
+ type PathProbe = (candidate: string) => string | null;
1208
+ /** Open a NEW terminal window running `command [extraArgs…]` with `env` injected. */
1209
+ type TerminalOpener = (input: {
1210
+ cli: string;
1211
+ command: string;
1212
+ extraArgs: string[];
1213
+ env: Record<string, string>;
1214
+ cwd?: string;
1215
+ platform: NodeJS.Platform;
1216
+ }) => void;
1217
+ /**
1218
+ * Injectable shell runner for `POST /cli/:cli/install` (tests stub this; the
1219
+ * default execs the install command with a bounded timeout). Returns the host's
1220
+ * honest install outcome — `error` carries stderr/the failure reason.
1221
+ */
1222
+ type CommandRunner = (command: string) => Promise<{
1223
+ ok: boolean;
1224
+ error?: string;
1225
+ }>;
1226
+
929
1227
  /**
930
1228
  * migration.ts — the export gather + import apply logic for the passphrase pack
931
1229
  * (app-parity child 6, design D2/D3/D5).
@@ -1072,6 +1370,32 @@ interface AdminApiDeps {
1072
1370
  * Wired from the concrete `credentialStore` in `bootstrap.ts`.
1073
1371
  */
1074
1372
  readonly migrationCredentialStore: MigrationCredentialStore;
1373
+ /**
1374
+ * Usage-stats query facade (usage-pricing child) — delegates to the JSONL
1375
+ * usage-event store. Aggregates only; carries no key material.
1376
+ */
1377
+ readonly usageRecorder: UsageRecorder;
1378
+ /** Pricing engine (upsert / source refresh / conflict resolution). */
1379
+ readonly pricingEngine: PricingEngine;
1380
+ /**
1381
+ * CONCRETE pricing store — ONLY for the store-local row `delete` (the core
1382
+ * `PricingStore` port is frozen; delete is a daemon-local extra).
1383
+ */
1384
+ readonly pricingStore: JsonPricingStore;
1385
+ /**
1386
+ * External-terminal opener for the Code CLI launch route (dashboard parity).
1387
+ * Optional — defaults to the real `defaultTerminalOpener`; tests inject a spy so
1388
+ * no terminal window is actually spawned.
1389
+ */
1390
+ readonly cliTerminalOpener?: TerminalOpener;
1391
+ /** Injectable PATH probe for CLI detection (tests fake "installed"). */
1392
+ readonly cliPathProbe?: PathProbe;
1393
+ /**
1394
+ * Injectable shell runner for the Code CLI install route. Optional — defaults
1395
+ * to the real `exec`-based runner; tests inject a stub so no package manager
1396
+ * actually runs.
1397
+ */
1398
+ readonly cliCommandRunner?: CommandRunner;
1075
1399
  }
1076
1400
  /**
1077
1401
  * Dispatch one `/admin/api/*` request. `path` is the already-extracted pathname
@@ -1098,8 +1422,9 @@ declare function handleAdminApi(req: http.IncomingMessage, res: http.ServerRespo
1098
1422
  * - HARD SAFETY GATE: `networkBinding` (LAN/`0.0.0.0`) without a non-empty
1099
1423
  * `admin.token` → `start` REFUSES to bind (logs + stays down, fail closed).
1100
1424
  *
1101
- * Routing: `GET /` (and `GET /admin`) → `DASHBOARD_HTML`; `* /admin/api/*` → the
1102
- * management API (`handleAdminApi`); everything else`404`.
1425
+ * Routing: `GET /` (and `GET /admin`) → `302 /ui/`; `* /admin/api/*` → the
1426
+ * management API (`handleAdminApi`); `GET /ui[/...]`the Control Panel static
1427
+ * UI (`handleUiStatic`, from `@omnicross/ui`); everything else → `404`.
1103
1428
  *
1104
1429
  * @module @omnicross/daemon/admin/AdminServer
1105
1430
  */
@@ -1122,6 +1447,8 @@ declare class AdminServer {
1122
1447
  private server;
1123
1448
  private boundPort;
1124
1449
  private boundAddr;
1450
+ /** Control Panel dist dir (resolved once at first request; null = no UI). */
1451
+ private uiDist;
1125
1452
  constructor(deps: AdminServerDeps);
1126
1453
  /**
1127
1454
  * Start the admin listener honoring the resolved admin config. Returns the
@@ -1196,17 +1523,69 @@ declare class JsonOutboundKeyDb implements OutboundKeyDb$1 {
1196
1523
  private writeRows;
1197
1524
  }
1198
1525
 
1526
+ /**
1527
+ * TokenRefreshScheduler — proactive background OAuth token refresh
1528
+ * (external-cli-sync).
1529
+ *
1530
+ * The auth strategies already refresh LAZILY (lead-window check before each
1531
+ * request + 401 retry), but a daemon that sits idle past a token's lifetime
1532
+ * pays the refresh latency — or a dead rotated token — on the first request.
1533
+ * This scheduler sweeps every account of every OAuth provider on an interval
1534
+ * and refreshes any token entering the expiry lead window.
1535
+ *
1536
+ * Safety properties:
1537
+ * - the store coalesces in-flight refreshes per account, so a sweep can never
1538
+ * double-spend a single-use refresh token against a concurrent lazy refresh;
1539
+ * - accounts already flagged `expired` are skipped (a dead refresh token is
1540
+ * not retried every tick — recovery is the external-import fallback or a
1541
+ * re-login);
1542
+ * - the ACTIVE account routes through the provider's active refresher (which
1543
+ * carries the external CLI import fallback); non-active accounts refresh
1544
+ * by id;
1545
+ * - one sweep runs at a time (a long sweep never overlaps the next tick).
1546
+ *
1547
+ * Modeled on `ApiKeyPoolService`'s interval lifecycle: `start()` arms an
1548
+ * `unref()`ed timer, `dispose()` clears it.
1549
+ *
1550
+ * @module @omnicross/daemon/TokenRefreshScheduler
1551
+ */
1552
+
1553
+ declare class TokenRefreshScheduler {
1554
+ private readonly store;
1555
+ private readonly logger;
1556
+ private readonly intervalMs;
1557
+ private readonly leadMs;
1558
+ private timer;
1559
+ private sweeping;
1560
+ constructor(store: JsonSubscriptionCredentialStore, logger: Logger, intervalMs?: number, leadMs?: number);
1561
+ /** Arm the sweep interval. Idempotent. The timer never holds the loop open. */
1562
+ start(): void;
1563
+ /** Clear the interval (daemon shutdown / test teardown). Idempotent. */
1564
+ dispose(): void;
1565
+ /** One sweep over every account of every OAuth provider. Exposed for tests. */
1566
+ sweep(now?: number): Promise<void>;
1567
+ /** Expiring within the lead window, refreshable, and not already dead. */
1568
+ private needsRefresh;
1569
+ /** Refresh one account; failures are logged, never thrown (the store has
1570
+ * already flagged the account `expired`). */
1571
+ private refreshOne;
1572
+ private refreshActive;
1573
+ }
1574
+
1199
1575
  /**
1200
1576
  * bootstrap.ts — `buildDaemon` wires `@omnicross/core`'s `ProviderProxy` +
1201
1577
  * `OutboundApiServer` STANDALONE (design D6).
1202
1578
  *
1203
1579
  * A DB-backed embedder wires the same `@omnicross/core` surface differently;
1204
- * this standalone wiring makes three SUBSTITUTIONS (file-backed ports replace
1205
- * DB-backed ones) and three SUBTRACTIONS:
1580
+ * this standalone wiring makes SUBSTITUTIONS (file-backed ports replace
1581
+ * DB-backed ones) and SUBTRACTIONS:
1206
1582
  * - no `CompletionService` (the BYO proxy path doesn't need it),
1207
- * - no `apiKeyPool` / `usageRecorder` (optional `ProviderProxyDeps`),
1208
1583
  * - no `anthropicIngressHandlerFactory` (→ `/v1/messages` returns 502 by core's
1209
1584
  * existing contract — no daemon code needed).
1585
+ * (`apiKeyPool` and `usageRecorder` are NO LONGER subtracted: the pool is wired
1586
+ * for multi-key load balancing, and the usage recorder is wired over the
1587
+ * file-backed pricing/usage stores so every served request is cost-stamped and
1588
+ * persisted to `usage-events.jsonl`.)
1210
1589
  *
1211
1590
  * `getProviderProxy` / `getOutboundApiServer` are module singletons, so the boot
1212
1591
  * smoke test calls `__resetProviderProxyForTests` / `__resetOutboundApiServerForTests`
@@ -1239,6 +1618,21 @@ interface DaemonPaths {
1239
1618
  * hit a real token endpoint. Absent → the global `fetch`.
1240
1619
  */
1241
1620
  oauthExchangeFetch?: FetchLike;
1621
+ /**
1622
+ * TEST SEAM (optional): override the Code CLI external-terminal opener so tests
1623
+ * never spawn a window. Absent → the real `defaultTerminalOpener`.
1624
+ */
1625
+ cliTerminalOpener?: TerminalOpener;
1626
+ /**
1627
+ * TEST SEAM (optional): override the Code CLI PATH probe so tests can fake an
1628
+ * installed CLI. Absent → the real PATH scan.
1629
+ */
1630
+ cliPathProbe?: PathProbe;
1631
+ /**
1632
+ * TEST SEAM (optional): override the Code CLI install command runner so tests
1633
+ * never invoke a real package manager. Absent → the real `exec`-based runner.
1634
+ */
1635
+ cliCommandRunner?: CommandRunner;
1242
1636
  }
1243
1637
  /** The constructed daemon handles the CLI commands operate on. */
1244
1638
  interface Daemon {
@@ -1264,8 +1658,21 @@ interface Daemon {
1264
1658
  /** Subscription account service (token-free `listAll`) — now exposed for the
1265
1659
  * admin dashboard's read-only accounts panel (RT3). */
1266
1660
  readonly subscriptionAccounts: SubscriptionAccountService;
1661
+ /** File-backed pricing table (`pricing.json`; concrete for the admin DELETE). */
1662
+ readonly pricingStore: JsonPricingStore;
1663
+ /** Pricing engine (cost calc + source refresh + conflict resolution). */
1664
+ readonly pricingEngine: PricingEngine;
1665
+ /** Usage recorder over `usage-events.jsonl` — also the admin stats query facade. */
1666
+ readonly usageRecorder: UsageRecorder;
1267
1667
  /** The localhost admin/dashboard HTTP listener (RT3). Started by `start.ts`. */
1268
1668
  readonly adminServer: AdminServer;
1669
+ /**
1670
+ * Proactive background OAuth refresh sweep (external-cli-sync). NOT started
1671
+ * here — `start.ts` arms it for the resident daemon; the short-lived `launch`
1672
+ * boot leaves it off (the lazy strategy refresh covers a single session) but
1673
+ * still disposes it in cleanup.
1674
+ */
1675
+ readonly tokenRefreshScheduler: TokenRefreshScheduler;
1269
1676
  }
1270
1677
  /**
1271
1678
  * Construct the standalone daemon from a loaded config + on-disk paths. Does NOT
@@ -1291,24 +1698,6 @@ declare function buildDaemon(config: DaemonConfig, paths: DaemonPaths): Daemon;
1291
1698
  * via `daemon.adminServer.stop()`. */
1292
1699
  declare function resetDaemonSingletonsForTests(): void;
1293
1700
 
1294
- /**
1295
- * html.ts — the embedded vanilla-JS admin dashboard (RT3, design D7).
1296
- *
1297
- * A SINGLE `text/html` template literal served by `AdminServer` on `GET /`. No
1298
- * framework, no bundler, no new dependency — vanilla `fetch` + DOM. Panels:
1299
- * providers (table + add/edit form), keys (table + create modal showing the
1300
- * one-time plaintext with a copy button + a "shown once" warning, never
1301
- * persisted), server config, read-only accounts status, and a playground
1302
- * (endpoint select + key + request textarea + Send + response area).
1303
- *
1304
- * SECURITY: the create-key plaintext is held only in a local variable inside the
1305
- * modal flow and cleared on dismiss — never written to a field, list, or storage.
1306
- *
1307
- * @module @omnicross/daemon/admin/html
1308
- */
1309
- /** The full dashboard document (style + body + the vanilla client script). */
1310
- declare const DASHBOARD_HTML: string;
1311
-
1312
1701
  /**
1313
1702
  * ccr-import.ts — translate a `claude-code-router` (CCR) `config.json` into an
1314
1703
  * omnicross daemon config (design D9). Pure + testable: `parseCcrConfig(raw)` +
@@ -1372,4 +1761,4 @@ declare function mapCcrToOmnicross(ccr: CcrConfig): {
1372
1761
  notes: string[];
1373
1762
  };
1374
1763
 
1375
- export { type AdminApiDeps, AdminServer, type AdminServerDeps, type AdminServerStatus, type CcrConfig, type CcrProvider, type CcrRouter, ConfigFileProviderConfigSource, ConsoleLogger, DASHBOARD_HTML, DEFAULT_ADMIN_PORT, type Daemon, type DaemonAdminConfig, type DaemonApiFormat, type DaemonConfig, type DaemonPaths, type DaemonProviderConfig, JsonApiServerSettingsStore, JsonOutboundKeyDb, JsonSubscriptionCredentialStore, type ResolvedAdminConfig, buildDaemon, handleAdminApi, inferApiFormat, loadConfig, mapCcrToOmnicross, parseCcrConfig, resetDaemonSingletonsForTests, resolveAdminConfig, saveConfig, validateConfig };
1764
+ export { type AdminApiDeps, AdminServer, type AdminServerDeps, type AdminServerStatus, type CcrConfig, type CcrProvider, type CcrRouter, ConfigFileProviderConfigSource, ConsoleLogger, DEFAULT_ADMIN_PORT, type Daemon, type DaemonAdminConfig, type DaemonApiFormat, type DaemonConfig, type DaemonPaths, type DaemonProviderConfig, JsonApiServerSettingsStore, JsonOutboundKeyDb, JsonSubscriptionCredentialStore, type ResolvedAdminConfig, buildDaemon, handleAdminApi, inferApiFormat, loadConfig, mapCcrToOmnicross, parseCcrConfig, resetDaemonSingletonsForTests, resolveAdminConfig, saveConfig, validateConfig };