@nmzpy/pi-ember-stack 0.2.1 → 0.2.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.
Files changed (30) hide show
  1. package/README.md +83 -83
  2. package/package.json +63 -59
  3. package/plugins/devin-auth/extensions/index.ts +23 -0
  4. package/plugins/devin-auth/src/cloud-direct/auth.ts +246 -246
  5. package/plugins/devin-auth/src/cloud-direct/catalog.ts +246 -246
  6. package/plugins/devin-auth/src/cloud-direct/chat.ts +1096 -1091
  7. package/plugins/devin-auth/src/cloud-direct/index.ts +41 -41
  8. package/plugins/devin-auth/src/cloud-direct/metadata.ts +78 -78
  9. package/plugins/devin-auth/src/cloud-direct/wire.ts +202 -202
  10. package/plugins/devin-auth/src/oauth/register-user.ts +174 -174
  11. package/plugins/devin-auth/src/oauth/types.ts +71 -71
  12. package/plugins/devin-auth/src/stream.ts +1 -1
  13. package/plugins/pi-compact-tools/index.ts +19 -1
  14. package/plugins/pi-compact-tools/renderer.ts +231 -61
  15. package/plugins/pi-custom-agents/index.ts +310 -102
  16. package/plugins/pi-custom-agents/questionnaire-tool.ts +14 -5
  17. package/plugins/pi-custom-agents/subagent/extensions/agents.ts +18 -1
  18. package/plugins/pi-custom-agents/subagent/extensions/index.ts +116 -224
  19. package/plugins/pi-custom-agents/subagent/extensions/model.ts +96 -96
  20. package/plugins/pi-custom-agents/subagent/extensions/render.ts +205 -1
  21. package/plugins/pi-custom-agents/subagent/extensions/runner.ts +241 -177
  22. package/plugins/pi-custom-agents/subagent/extensions/test/render.test.ts +145 -0
  23. package/plugins/pi-ember-fff/index.ts +275 -17
  24. package/plugins/pi-ember-fff/query.ts +170 -10
  25. package/plugins/pi-ember-fff/test/query.test.ts +157 -1
  26. package/plugins/pi-ember-fff/test/renderer.test.ts +367 -16
  27. package/plugins/pi-ember-tps/index.ts +27 -5
  28. package/plugins/pi-ember-ui/ember.json +3 -0
  29. package/plugins/pi-ember-ui/index.ts +223 -36
  30. package/plugins/pi-ember-ui/mode-colors.ts +36 -8
@@ -1,246 +1,246 @@
1
- /**
2
- * Mint the short-lived `user_jwt` that every chat RPC needs alongside the
3
- * persistent OAuth-issued `api_key`.
4
- *
5
- * POST https://server.codeium.com/exa.auth_pb.AuthService/GetUserJwt
6
- * Content-Type: application/proto ← unary, NOT streaming
7
- * Body: GetUserJwtRequest { metadata: Metadata }
8
- * Response: GetUserJwtResponse { user_jwt: string } (field 1)
9
- *
10
- * The returned JWT has a payload like:
11
- * {
12
- * "api_key": "devin-synthetic-apikey$account-…$user-…",
13
- * "auth_uid": "devin-auth-uid$…",
14
- * "email": "user@example.com",
15
- * "exp": <unix-seconds>, ← ~24 minute TTL
16
- * "pro": true,
17
- * "teams_tier": "TEAMS_TIER_DEVIN_PRO",
18
- * ...
19
- * }
20
- *
21
- * The JWT is signed HS256 by the server — can't be forged client-side. We
22
- * cache it and refresh shortly before `exp`.
23
- */
24
-
25
- import * as crypto from 'crypto';
26
- import { encodeMessage, iterFields } from './wire.js';
27
- import { buildMetadata } from './metadata.js';
28
-
29
- const DEFAULT_HOST = 'https://server.codeium.com';
30
-
31
- /**
32
- * Polyfill for `AbortSignal.any` — composes multiple signals so the result
33
- * aborts when ANY input aborts. Built-in in Node ≥20.3 / Bun ≥1.0. Our
34
- * `engines.node` is `>=18.0.0`, so we ship the fallback ourselves; without
35
- * it the caller's cancel signal silently disappears on older runtimes
36
- * (chat-cancel during a `GetUserJwt` mint would keep the network request
37
- * alive for up to the full 30s timeout).
38
- */
39
- function anySignal(signals: AbortSignal[]): AbortSignal {
40
- const builtin = (AbortSignal as unknown as { any?: (s: AbortSignal[]) => AbortSignal }).any;
41
- if (typeof builtin === 'function') return builtin(signals);
42
- const controller = new AbortController();
43
- const onAbort = (reason: unknown): void => {
44
- if (!controller.signal.aborted) controller.abort(reason);
45
- };
46
- for (const s of signals) {
47
- if (s.aborted) {
48
- onAbort(s.reason);
49
- break;
50
- }
51
- s.addEventListener('abort', () => onAbort(s.reason), { once: true });
52
- }
53
- return controller.signal;
54
- }
55
-
56
- export interface MintedUserJwt {
57
- jwt: string;
58
- /** Unix epoch seconds when the JWT expires. */
59
- expiresAt: number;
60
- }
61
-
62
- export class CloudAuthError extends Error {
63
- constructor(message: string, public readonly status?: number) {
64
- super(message);
65
- this.name = 'CloudAuthError';
66
- }
67
- }
68
-
69
- /**
70
- * Default mint timeout — 30s is generous (the endpoint responds in ~200ms
71
- * in steady state) but enough headroom for slow networks. Callers can pass
72
- * a tighter `signal` to override.
73
- */
74
- const MINT_TIMEOUT_MS = 30_000;
75
-
76
- /**
77
- * Mint a fresh user_jwt by calling exa.auth_pb.AuthService/GetUserJwt.
78
- * `host` defaults to https://server.codeium.com — pass your tenant URL if your
79
- * RegisterUser response gave a different host.
80
- *
81
- * Always applies an internal 30s timeout so a network stall here can't
82
- * deadlock every concurrent chat request. If the caller passes a `signal`,
83
- * we honor whichever fires first via AbortSignal.any.
84
- */
85
- export async function mintUserJwt(
86
- apiKey: string,
87
- host: string = DEFAULT_HOST,
88
- signal?: AbortSignal,
89
- ): Promise<MintedUserJwt> {
90
- const metadata = buildMetadata({
91
- apiKey,
92
- sessionId: crypto.randomUUID(),
93
- requestId: BigInt(Date.now()),
94
- triggerId: crypto.randomUUID(),
95
- });
96
- // GetUserJwtRequest { metadata: Metadata } — Metadata is field 1
97
- const req = encodeMessage(1, metadata);
98
-
99
- // Compose caller signal with our internal timeout via `anySignal` — a
100
- // small polyfill of `AbortSignal.any` for runtimes (Node 18 / older
101
- // Bun) that lack the built-in. The previous fallback silently dropped
102
- // the CALLER's signal on those runtimes, so a chat-cancel during a
103
- // GetUserJwt mint would keep the network request alive for up to the
104
- // full 30s timeout.
105
- const timeoutSignal = AbortSignal.timeout(MINT_TIMEOUT_MS);
106
- const combinedSignal: AbortSignal = signal
107
- ? anySignal([signal, timeoutSignal])
108
- : timeoutSignal;
109
-
110
- const resp = await fetch(`${host.replace(/\/$/, '')}/exa.auth_pb.AuthService/GetUserJwt`, {
111
- method: 'POST',
112
- headers: {
113
- 'Content-Type': 'application/proto',
114
- 'Connect-Protocol-Version': '1',
115
- },
116
- body: req,
117
- signal: combinedSignal,
118
- });
119
- const buf = Buffer.from(await resp.arrayBuffer());
120
-
121
- if (!resp.ok) {
122
- const text = buf.toString('utf8');
123
- throw new CloudAuthError(`GetUserJwt HTTP ${resp.status}: ${text.slice(0, 400)}`, resp.status);
124
- }
125
-
126
- // Response is GetUserJwtResponse { user_jwt: string } where user_jwt is
127
- // field 1, length-delimited. Decode the field properly instead of
128
- // regex-scanning the whole buffer — the previous regex would pick up
129
- // any JWT-shaped substring in the response (trace IDs, signature
130
- // headers, any cached token inadvertently logged) and could even land
131
- // on a non-user_jwt if Cognition ever embeds another JWT in a sibling
132
- // field.
133
- let jwt: string | null = null;
134
- for (const f of iterFields(buf)) {
135
- if (f.num === 1 && f.wire === 2 && Buffer.isBuffer(f.value)) {
136
- const s = (f.value as Buffer).toString('utf8');
137
- // Sanity-check the shape — defensive: if the cloud ever moves user_jwt
138
- // out from field 1 we want a clean error, not silently wrong creds.
139
- // base64url with OPTIONAL `=` padding on each segment. Most modern
140
- // JWTs omit the `=`, but the spec allows it and a future server-side
141
- // change could re-introduce it; either way it's still a valid token.
142
- if (/^eyJ[A-Za-z0-9_-]{10,}={0,2}\.[A-Za-z0-9_-]+={0,2}\.[A-Za-z0-9_-]+={0,2}$/.test(s)) {
143
- jwt = s;
144
- break;
145
- }
146
- }
147
- }
148
- if (!jwt) {
149
- throw new CloudAuthError(
150
- `GetUserJwt 200 but no field-1 JWT found (${buf.length} bytes): ${buf.toString('utf8').slice(0, 200)}`,
151
- );
152
- }
153
-
154
- // Decode the payload to get the expiry.
155
- let expiresAt = Math.floor(Date.now() / 1000) + 600; // fallback: 10 min
156
- try {
157
- const parts = jwt.split('.');
158
- const pad = (s: string) => s + '='.repeat((4 - (s.length % 4)) % 4);
159
- const payload = JSON.parse(
160
- Buffer.from(pad(parts[1]).replace(/-/g, '+').replace(/_/g, '/'), 'base64').toString('utf8'),
161
- );
162
- if (typeof payload.exp === 'number') expiresAt = payload.exp;
163
- } catch { /* fall back to default */ }
164
-
165
- return { jwt, expiresAt };
166
- }
167
-
168
- // ----------------------------------------------------------------------------
169
- // In-memory cache — refresh ~60s before expiry
170
- // ----------------------------------------------------------------------------
171
-
172
- interface CacheEntry {
173
- jwt: string;
174
- expiresAt: number;
175
- apiKey: string;
176
- host: string;
177
- }
178
-
179
- /**
180
- * Cache is keyed by (apiKey, host). A single shared `cache` slot only holds
181
- * the MOST RECENTLY USED entry — common case is one account at a time, so
182
- * a single slot is enough. inFlight is a per-key map so a JWT mint for
183
- * account A doesn't get returned to a concurrent request for account B.
184
- *
185
- * Previously `inFlight` was a singleton — if account A's mint was in flight
186
- * and a request for account B arrived, B got A's JWT. That's the M1
187
- * "concurrent requests after account switch get wrong JWT" bug.
188
- */
189
- let cache: CacheEntry | null = null;
190
- const inFlight = new Map<string, Promise<MintedUserJwt>>();
191
- /**
192
- * Monotonic epoch counter. Incremented on every `clearCachedUserJwt()`
193
- * call so an in-flight mint that started BEFORE the clear can't
194
- * repopulate the cache after-the-fact. Without this, a logout that
195
- * happened concurrently with a mint would silently get its just-
196
- * invalidated JWT cached and served for the next ~24 minutes.
197
- */
198
- let cacheEpoch = 0;
199
-
200
- function flightKey(apiKey: string, host: string): string {
201
- return `${host}\x1f${apiKey}`;
202
- }
203
-
204
- /**
205
- * Get a cached user_jwt or mint a new one. Refreshes when the cached JWT is
206
- * within 60s of expiry. Multiple concurrent callers for the SAME (apiKey, host)
207
- * share the same in-flight mint; concurrent callers for DIFFERENT keys each
208
- * get their own mint.
209
- */
210
- export async function getCachedUserJwt(apiKey: string, host: string = DEFAULT_HOST, signal?: AbortSignal): Promise<string> {
211
- const now = Math.floor(Date.now() / 1000);
212
- if (cache && cache.apiKey === apiKey && cache.host === host && cache.expiresAt > now + 60) {
213
- return cache.jwt;
214
- }
215
- const key = flightKey(apiKey, host);
216
- const existing = inFlight.get(key);
217
- if (existing) return (await existing).jwt;
218
- const promise = mintUserJwt(apiKey, host, signal);
219
- inFlight.set(key, promise);
220
- // Snapshot the epoch BEFORE awaiting the mint. If clearCachedUserJwt()
221
- // fires while we're awaiting (logout-during-mint), the epoch changes
222
- // and we won't repopulate the cache with the just-invalidated JWT.
223
- const epochAtStart = cacheEpoch;
224
- try {
225
- const minted = await promise;
226
- if (cacheEpoch === epochAtStart) {
227
- cache = { jwt: minted.jwt, expiresAt: minted.expiresAt, apiKey, host };
228
- }
229
- return minted.jwt;
230
- } finally {
231
- inFlight.delete(key);
232
- }
233
- }
234
-
235
- /**
236
- * Drop the in-memory JWT cache. Call after credential changes (logout,
237
- * account switch) so long-running opencode processes don't keep using a
238
- * JWT minted from a now-invalid api_key. Also bumps the cache epoch so
239
- * any in-flight mint racing with this clear can't repopulate cache
240
- * with the stale JWT after-the-fact.
241
- */
242
- export function clearCachedUserJwt(): void {
243
- cache = null;
244
- inFlight.clear();
245
- cacheEpoch++;
246
- }
1
+ /**
2
+ * Mint the short-lived `user_jwt` that every chat RPC needs alongside the
3
+ * persistent OAuth-issued `api_key`.
4
+ *
5
+ * POST https://server.codeium.com/exa.auth_pb.AuthService/GetUserJwt
6
+ * Content-Type: application/proto ← unary, NOT streaming
7
+ * Body: GetUserJwtRequest { metadata: Metadata }
8
+ * Response: GetUserJwtResponse { user_jwt: string } (field 1)
9
+ *
10
+ * The returned JWT has a payload like:
11
+ * {
12
+ * "api_key": "devin-synthetic-apikey$account-…$user-…",
13
+ * "auth_uid": "devin-auth-uid$…",
14
+ * "email": "user@example.com",
15
+ * "exp": <unix-seconds>, ← ~24 minute TTL
16
+ * "pro": true,
17
+ * "teams_tier": "TEAMS_TIER_DEVIN_PRO",
18
+ * ...
19
+ * }
20
+ *
21
+ * The JWT is signed HS256 by the server — can't be forged client-side. We
22
+ * cache it and refresh shortly before `exp`.
23
+ */
24
+
25
+ import * as crypto from 'crypto';
26
+ import { encodeMessage, iterFields } from './wire.js';
27
+ import { buildMetadata } from './metadata.js';
28
+
29
+ const DEFAULT_HOST = 'https://server.codeium.com';
30
+
31
+ /**
32
+ * Polyfill for `AbortSignal.any` — composes multiple signals so the result
33
+ * aborts when ANY input aborts. Built-in in Node ≥20.3 / Bun ≥1.0. Our
34
+ * `engines.node` is `>=18.0.0`, so we ship the fallback ourselves; without
35
+ * it the caller's cancel signal silently disappears on older runtimes
36
+ * (chat-cancel during a `GetUserJwt` mint would keep the network request
37
+ * alive for up to the full 30s timeout).
38
+ */
39
+ function anySignal(signals: AbortSignal[]): AbortSignal {
40
+ const builtin = (AbortSignal as unknown as { any?: (s: AbortSignal[]) => AbortSignal }).any;
41
+ if (typeof builtin === 'function') return builtin(signals);
42
+ const controller = new AbortController();
43
+ const onAbort = (reason: unknown): void => {
44
+ if (!controller.signal.aborted) controller.abort(reason);
45
+ };
46
+ for (const s of signals) {
47
+ if (s.aborted) {
48
+ onAbort(s.reason);
49
+ break;
50
+ }
51
+ s.addEventListener('abort', () => onAbort(s.reason), { once: true });
52
+ }
53
+ return controller.signal;
54
+ }
55
+
56
+ export interface MintedUserJwt {
57
+ jwt: string;
58
+ /** Unix epoch seconds when the JWT expires. */
59
+ expiresAt: number;
60
+ }
61
+
62
+ export class CloudAuthError extends Error {
63
+ constructor(message: string, public readonly status?: number) {
64
+ super(message);
65
+ this.name = 'CloudAuthError';
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Default mint timeout — 30s is generous (the endpoint responds in ~200ms
71
+ * in steady state) but enough headroom for slow networks. Callers can pass
72
+ * a tighter `signal` to override.
73
+ */
74
+ const MINT_TIMEOUT_MS = 30_000;
75
+
76
+ /**
77
+ * Mint a fresh user_jwt by calling exa.auth_pb.AuthService/GetUserJwt.
78
+ * `host` defaults to https://server.codeium.com — pass your tenant URL if your
79
+ * RegisterUser response gave a different host.
80
+ *
81
+ * Always applies an internal 30s timeout so a network stall here can't
82
+ * deadlock every concurrent chat request. If the caller passes a `signal`,
83
+ * we honor whichever fires first via AbortSignal.any.
84
+ */
85
+ export async function mintUserJwt(
86
+ apiKey: string,
87
+ host: string = DEFAULT_HOST,
88
+ signal?: AbortSignal,
89
+ ): Promise<MintedUserJwt> {
90
+ const metadata = buildMetadata({
91
+ apiKey,
92
+ sessionId: crypto.randomUUID(),
93
+ requestId: BigInt(Date.now()),
94
+ triggerId: crypto.randomUUID(),
95
+ });
96
+ // GetUserJwtRequest { metadata: Metadata } — Metadata is field 1
97
+ const req = encodeMessage(1, metadata);
98
+
99
+ // Compose caller signal with our internal timeout via `anySignal` — a
100
+ // small polyfill of `AbortSignal.any` for runtimes (Node 18 / older
101
+ // Bun) that lack the built-in. The previous fallback silently dropped
102
+ // the CALLER's signal on those runtimes, so a chat-cancel during a
103
+ // GetUserJwt mint would keep the network request alive for up to the
104
+ // full 30s timeout.
105
+ const timeoutSignal = AbortSignal.timeout(MINT_TIMEOUT_MS);
106
+ const combinedSignal: AbortSignal = signal
107
+ ? anySignal([signal, timeoutSignal])
108
+ : timeoutSignal;
109
+
110
+ const resp = await fetch(`${host.replace(/\/$/, '')}/exa.auth_pb.AuthService/GetUserJwt`, {
111
+ method: 'POST',
112
+ headers: {
113
+ 'Content-Type': 'application/proto',
114
+ 'Connect-Protocol-Version': '1',
115
+ },
116
+ body: req,
117
+ signal: combinedSignal,
118
+ });
119
+ const buf = Buffer.from(await resp.arrayBuffer());
120
+
121
+ if (!resp.ok) {
122
+ const text = buf.toString('utf8');
123
+ throw new CloudAuthError(`GetUserJwt HTTP ${resp.status}: ${text.slice(0, 400)}`, resp.status);
124
+ }
125
+
126
+ // Response is GetUserJwtResponse { user_jwt: string } where user_jwt is
127
+ // field 1, length-delimited. Decode the field properly instead of
128
+ // regex-scanning the whole buffer — the previous regex would pick up
129
+ // any JWT-shaped substring in the response (trace IDs, signature
130
+ // headers, any cached token inadvertently logged) and could even land
131
+ // on a non-user_jwt if Cognition ever embeds another JWT in a sibling
132
+ // field.
133
+ let jwt: string | null = null;
134
+ for (const f of iterFields(buf)) {
135
+ if (f.num === 1 && f.wire === 2 && Buffer.isBuffer(f.value)) {
136
+ const s = (f.value as Buffer).toString('utf8');
137
+ // Sanity-check the shape — defensive: if the cloud ever moves user_jwt
138
+ // out from field 1 we want a clean error, not silently wrong creds.
139
+ // base64url with OPTIONAL `=` padding on each segment. Most modern
140
+ // JWTs omit the `=`, but the spec allows it and a future server-side
141
+ // change could re-introduce it; either way it's still a valid token.
142
+ if (/^eyJ[A-Za-z0-9_-]{10,}={0,2}\.[A-Za-z0-9_-]+={0,2}\.[A-Za-z0-9_-]+={0,2}$/.test(s)) {
143
+ jwt = s;
144
+ break;
145
+ }
146
+ }
147
+ }
148
+ if (!jwt) {
149
+ throw new CloudAuthError(
150
+ `GetUserJwt 200 but no field-1 JWT found (${buf.length} bytes): ${buf.toString('utf8').slice(0, 200)}`,
151
+ );
152
+ }
153
+
154
+ // Decode the payload to get the expiry.
155
+ let expiresAt = Math.floor(Date.now() / 1000) + 600; // fallback: 10 min
156
+ try {
157
+ const parts = jwt.split('.');
158
+ const pad = (s: string) => s + '='.repeat((4 - (s.length % 4)) % 4);
159
+ const payload = JSON.parse(
160
+ Buffer.from(pad(parts[1]).replace(/-/g, '+').replace(/_/g, '/'), 'base64').toString('utf8'),
161
+ );
162
+ if (typeof payload.exp === 'number') expiresAt = payload.exp;
163
+ } catch { /* fall back to default */ }
164
+
165
+ return { jwt, expiresAt };
166
+ }
167
+
168
+ // ----------------------------------------------------------------------------
169
+ // In-memory cache — refresh ~60s before expiry
170
+ // ----------------------------------------------------------------------------
171
+
172
+ interface CacheEntry {
173
+ jwt: string;
174
+ expiresAt: number;
175
+ apiKey: string;
176
+ host: string;
177
+ }
178
+
179
+ /**
180
+ * Cache is keyed by (apiKey, host). A single shared `cache` slot only holds
181
+ * the MOST RECENTLY USED entry — common case is one account at a time, so
182
+ * a single slot is enough. inFlight is a per-key map so a JWT mint for
183
+ * account A doesn't get returned to a concurrent request for account B.
184
+ *
185
+ * Previously `inFlight` was a singleton — if account A's mint was in flight
186
+ * and a request for account B arrived, B got A's JWT. That's the M1
187
+ * "concurrent requests after account switch get wrong JWT" bug.
188
+ */
189
+ let cache: CacheEntry | null = null;
190
+ const inFlight = new Map<string, Promise<MintedUserJwt>>();
191
+ /**
192
+ * Monotonic epoch counter. Incremented on every `clearCachedUserJwt()`
193
+ * call so an in-flight mint that started BEFORE the clear can't
194
+ * repopulate the cache after-the-fact. Without this, a logout that
195
+ * happened concurrently with a mint would silently get its just-
196
+ * invalidated JWT cached and served for the next ~24 minutes.
197
+ */
198
+ let cacheEpoch = 0;
199
+
200
+ function flightKey(apiKey: string, host: string): string {
201
+ return `${host}\x1f${apiKey}`;
202
+ }
203
+
204
+ /**
205
+ * Get a cached user_jwt or mint a new one. Refreshes when the cached JWT is
206
+ * within 60s of expiry. Multiple concurrent callers for the SAME (apiKey, host)
207
+ * share the same in-flight mint; concurrent callers for DIFFERENT keys each
208
+ * get their own mint.
209
+ */
210
+ export async function getCachedUserJwt(apiKey: string, host: string = DEFAULT_HOST, signal?: AbortSignal): Promise<string> {
211
+ const now = Math.floor(Date.now() / 1000);
212
+ if (cache && cache.apiKey === apiKey && cache.host === host && cache.expiresAt > now + 60) {
213
+ return cache.jwt;
214
+ }
215
+ const key = flightKey(apiKey, host);
216
+ const existing = inFlight.get(key);
217
+ if (existing) return (await existing).jwt;
218
+ const promise = mintUserJwt(apiKey, host, signal);
219
+ inFlight.set(key, promise);
220
+ // Snapshot the epoch BEFORE awaiting the mint. If clearCachedUserJwt()
221
+ // fires while we're awaiting (logout-during-mint), the epoch changes
222
+ // and we won't repopulate the cache with the just-invalidated JWT.
223
+ const epochAtStart = cacheEpoch;
224
+ try {
225
+ const minted = await promise;
226
+ if (cacheEpoch === epochAtStart) {
227
+ cache = { jwt: minted.jwt, expiresAt: minted.expiresAt, apiKey, host };
228
+ }
229
+ return minted.jwt;
230
+ } finally {
231
+ inFlight.delete(key);
232
+ }
233
+ }
234
+
235
+ /**
236
+ * Drop the in-memory JWT cache. Call after credential changes (logout,
237
+ * account switch) so long-running opencode processes don't keep using a
238
+ * JWT minted from a now-invalid api_key. Also bumps the cache epoch so
239
+ * any in-flight mint racing with this clear can't repopulate cache
240
+ * with the stale JWT after-the-fact.
241
+ */
242
+ export function clearCachedUserJwt(): void {
243
+ cache = null;
244
+ inFlight.clear();
245
+ cacheEpoch++;
246
+ }