@prestyj/cli 5.3.1 → 5.4.0

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.
@@ -29,7 +29,7 @@ import { driveAutopilotCycle, frameAutopilotInjection } from "./core/autopilot-c
29
29
  import { validateNolanModelPref, effectiveNolanModel, } from "./core/nolan-model.js";
30
30
  import { normalizeAutopilotMarkersForHistory, normalizeAppMarkersForHistory, normalizeNolanTurnsForHistory, restoreUserRow, restoreAssistantTexts, autopilotMarkerCopySeed, } from "./core/session-history.js";
31
31
  import { AuthStorage } from "./core/auth-storage.js";
32
- import { MOONSHOT_OAUTH_KEY, XIAOMI_CREDITS_KEY } from "@prestyj/core";
32
+ import { fetchSubscriptionUsage, MOONSHOT_OAUTH_KEY, SubscriptionUsageError, XIAOMI_CREDITS_KEY, } from "@prestyj/core";
33
33
  import { loginAnthropic } from "./core/oauth/anthropic.js";
34
34
  import { loginOpenAI } from "./core/oauth/openai.js";
35
35
  import { loginGemini } from "./core/oauth/gemini.js";
@@ -509,6 +509,70 @@ async function main() {
509
509
  for (const ctx of sessions.values())
510
510
  ctx.broadcast("progress", { ...snapshot, origin: ctx.id === originId });
511
511
  });
512
+ const usageCache = new Map();
513
+ const usageRequests = new Map();
514
+ async function fetchUsageProvider(provider) {
515
+ const displayName = provider === "anthropic" ? "Anthropic" : "Codex";
516
+ if (!(await auth.hasProviderAuth(provider))) {
517
+ return { provider, displayName, connected: false, windows: [], fetchedAt: Date.now() };
518
+ }
519
+ try {
520
+ let credentials = await auth.resolveCredentials(provider);
521
+ try {
522
+ return { ...(await fetchSubscriptionUsage(provider, credentials)), connected: true };
523
+ }
524
+ catch (error) {
525
+ // A provider can revoke an access token before its stored expiry. Refresh
526
+ // once on 401, matching inference auth recovery, then retry the usage call.
527
+ if (error instanceof SubscriptionUsageError && error.status === 401) {
528
+ credentials = await auth.resolveCredentials(provider, { forceRefresh: true });
529
+ return { ...(await fetchSubscriptionUsage(provider, credentials)), connected: true };
530
+ }
531
+ throw error;
532
+ }
533
+ }
534
+ catch (error) {
535
+ const message = error instanceof Error ? error.message : String(error);
536
+ log("WARN", "app-sidecar", "subscription usage fetch failed", { provider, message });
537
+ const connected = await auth.hasProviderAuth(provider);
538
+ return {
539
+ provider,
540
+ displayName,
541
+ connected,
542
+ windows: [],
543
+ fetchedAt: Date.now(),
544
+ error: connected ? "Usage is temporarily unavailable." : undefined,
545
+ };
546
+ }
547
+ }
548
+ async function subscriptionUsage(provider) {
549
+ const cached = usageCache.get(provider);
550
+ if (cached && cached.expiresAt > Date.now())
551
+ return cached.result;
552
+ const inFlight = usageRequests.get(provider);
553
+ if (inFlight)
554
+ return inFlight;
555
+ const request = fetchUsageProvider(provider);
556
+ usageRequests.set(provider, request);
557
+ try {
558
+ const result = await request;
559
+ // Anthropic can return utilization before it assigns reset timestamps
560
+ // (notably before the account's first active request). Retry that partial
561
+ // snapshot quickly; complete snapshots keep the normal one-minute cache.
562
+ const missingReset = result.connected &&
563
+ result.windows.length > 0 &&
564
+ result.windows.some((window) => window.resetsAt === undefined);
565
+ usageCache.set(provider, {
566
+ result,
567
+ expiresAt: Date.now() + (missingReset ? 10_000 : 60_000),
568
+ });
569
+ return result;
570
+ }
571
+ finally {
572
+ if (usageRequests.get(provider) === request)
573
+ usageRequests.delete(provider);
574
+ }
575
+ }
512
576
  /** Resolve the target session id: the `x-gg-session` header, else a
513
577
  * `?session=` query param (used by the SSE /events connection). */
514
578
  function sessionIdFromReq(req, url) {
@@ -583,6 +647,25 @@ async function main() {
583
647
  daemonJson(res, 200, progress.snapshot());
584
648
  return;
585
649
  }
650
+ // Subscription quota is account-wide, not project/session-specific. OAuth
651
+ // tokens stay in this daemon; only the active provider's normalized snapshot
652
+ // reaches the webview.
653
+ if (method === "GET" && (url === "/usage" || url.startsWith("/usage?"))) {
654
+ void (async () => {
655
+ const provider = new URL(url, `http://${host}`).searchParams.get("provider");
656
+ if (provider !== "anthropic" && provider !== "openai") {
657
+ daemonJson(res, 400, { error: "unsupported usage provider" });
658
+ return;
659
+ }
660
+ daemonJson(res, 200, await subscriptionUsage(provider));
661
+ })().catch((error) => {
662
+ log("ERROR", "app-sidecar", "subscription usage request failed", {
663
+ message: error instanceof Error ? error.message : String(error),
664
+ });
665
+ daemonJson(res, 500, { error: "Usage is temporarily unavailable." });
666
+ });
667
+ return;
668
+ }
586
669
  // ── Per-session delegation ───────────────────────────────────────────
587
670
  const id = sessionIdFromReq(req, url);
588
671
  const ctx = id ? sessions.get(id) : undefined;
@@ -788,6 +871,32 @@ async function createSession(deps, opts) {
788
871
  for (const c of clients)
789
872
  c.res.write(frame);
790
873
  }
874
+ // Replace CLI-specific guidance (slash commands, CLI tool names) with
875
+ // desktop-app equivalents so the webview never shows "run ezcoder login".
876
+ // Applied to BOTH the message and guidance fields — the auth "Not logged in…
877
+ // Run "ezcoder login"" string lives in `message`, not `guidance`.
878
+ function desktopGuidance(guidance) {
879
+ return (guidance
880
+ // Auth: `Run "ezcoder login"` / `Run 'ezcoder login'` / `Run `ezcoder login``
881
+ // (any quote style, or none) → button. Do this first so the whole phrase
882
+ // is rewritten cleanly instead of leaving a dangling `Run "…"`.
883
+ .replaceAll(/Run ["'`]?ezcoder login["'`]?/gi, "Use the Login to AI Providers button")
884
+ // Any remaining bare mention.
885
+ .replaceAll(/ezcoder login/gi, "the Login to AI Providers button")
886
+ // /compact: the app has NO manual compact command or button — it only
887
+ // auto-compacts (and now auto-recovers on overflow). If this guidance is
888
+ // reached, auto-compaction already couldn't reduce enough, so the only
889
+ // real affordance is a fresh session. Don't tell the user to run a
890
+ // command that doesn't exist in the app.
891
+ .replaceAll(/Run \/compact to shrink history, or start a new session\./gi, "Start a new session to reset the context.")
892
+ // /model: handle each phrasing pattern
893
+ .replaceAll(/switch to claude-fable-5 with \/model/gi, "switch to claude-fable-5 using the model selector")
894
+ .replaceAll(/Switch with \/model\./gi, "Switch using the model selector.")
895
+ .replaceAll(/try a different model with \/model\./gi, "try a different model using the model selector.")
896
+ .replaceAll(/Use \/model to switch/gi, "Use the model selector to switch")
897
+ // /help
898
+ .replaceAll(/see \/help/gi, "check the help menu"));
899
+ }
791
900
  // Turn any thrown value into the same clear headline/message/guidance shape
792
901
  // the TUI shows (see gg-ai's formatError) instead of a bare `err.message`, log
793
902
  // the full detail, and broadcast it under `type` ("error" or "nolan_error").
@@ -796,18 +905,20 @@ async function createSession(deps, opts) {
796
905
  // context that the CLI has always given.
797
906
  function broadcastError(type, logLabel, err) {
798
907
  const f = formatError(err);
908
+ const message = f.message ? desktopGuidance(f.message) : undefined;
909
+ const guidance = desktopGuidance(f.guidance);
799
910
  log("ERROR", "app-sidecar", logLabel, {
800
911
  headline: f.headline,
801
912
  source: f.source,
802
- ...(f.message ? { message: f.message } : {}),
913
+ ...(message ? { message } : {}),
803
914
  ...(f.provider ? { provider: f.provider } : {}),
804
915
  ...(f.statusCode != null ? { statusCode: String(f.statusCode) } : {}),
805
916
  ...(f.requestId ? { requestId: f.requestId } : {}),
806
917
  });
807
918
  broadcast(type, {
808
919
  headline: f.headline,
809
- ...(f.message ? { message: f.message } : {}),
810
- guidance: f.guidance,
920
+ ...(message ? { message } : {}),
921
+ guidance,
811
922
  ...(f.provider ? { provider: f.provider } : {}),
812
923
  ...(f.statusCode != null ? { statusCode: f.statusCode } : {}),
813
924
  ...(f.resetsAt != null ? { resetsAt: f.resetsAt } : {}),
@@ -818,8 +929,8 @@ async function createSession(deps, opts) {
818
929
  .persistAppMarker("error", {
819
930
  scope: type,
820
931
  headline: f.headline,
821
- ...(f.message ? { message: f.message } : {}),
822
- guidance: f.guidance,
932
+ ...(message ? { message } : {}),
933
+ guidance,
823
934
  })
824
935
  .catch(() => { });
825
936
  }