@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/cli.cjs +1713 -625
- package/dist/cli.js +1707 -615
- package/dist/index.cjs +1647 -593
- package/dist/index.d.cts +415 -26
- package/dist/index.d.ts +415 -26
- package/dist/index.js +1637 -578
- package/package.json +3 -2
package/dist/index.d.cts
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`) → `
|
|
1102
|
-
* management API (`handleAdminApi`);
|
|
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
|
|
1205
|
-
* DB-backed ones) and
|
|
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,
|
|
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 };
|