@zgltyq/pi-provider-claude 1.3.1 → 1.3.3

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 (4) hide show
  1. package/README.md +10 -8
  2. package/index.ts +38 -6
  3. package/oauth.ts +10 -16
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -91,12 +91,13 @@ the assistant message (`stopReason: "error"`) via `message_end`, including Claud
91
91
  is hit, the turn errors once, the pool flips (with a UI notification), and you just
92
92
  resend your message to continue on the other account.
93
93
 
94
- **OAuth compatibility and background token refresh (v1.3.1):** The pool now
95
- uses its own Anthropic OAuth login/refresh adapter, keeping it compatible with
96
- pi 0.81.x where the old runtime OAuth export is no longer available. OAuth access
97
- tokens are short-lived. An account that sat idle (e.g. your fallback) would have
98
- a dead access token by the time failover switched to it — producing a `401
99
- authentication_error`. v1.3.1 retains the v1.3.0 keep-warm behavior:
94
+ **OAuth compatibility and background token refresh (v1.3.3):** The pool now
95
+ uses its own Anthropic OAuth login/refresh adapter and the login callback bridge
96
+ matches pi 0.81.x, where the old runtime OAuth export is no longer available.
97
+ OAuth access tokens are short-lived. An account that sat idle (e.g. your fallback)
98
+ would have a dead access token by the time failover switched to it — producing a
99
+ `401 authentication_error`. v1.3.3 also applies `/claude-pool-add` immediately to
100
+ the running session and excludes accounts whose refresh token is expired or revoked:
100
101
 
101
102
  - A `setInterval` keep-warm loop that refreshes **every** pooled account before it
102
103
  expires, even when idle and not the active account (the timer is `unref()`'d so
@@ -124,8 +125,9 @@ so the extension adds a command to snapshot each login into the pool:
124
125
  /claude-pool-status → verify both are present
125
126
  ```
126
127
 
127
- Restart, and failover activates. `/claude-pool-add <label>` only writes
128
- `claude-pool.json` (it never touches the request path).
128
+ Restart after initially creating the pool to activate failover. Once active,
129
+ `/claude-pool-add <label>` updates both `claude-pool.json` and the running session,
130
+ so replacing a revoked credential no longer requires another restart.
129
131
 
130
132
  ### Alternative: harvest via separate profiles
131
133
 
package/index.ts CHANGED
@@ -307,6 +307,7 @@ const POOL_BG_REFRESH_INTERVAL_MS = 4 * 60_000;
307
307
  let poolAccounts: PoolAccount[] = [];
308
308
  let poolActive = 0;
309
309
  let poolCooldownUntil: number[] = [];
310
+ let poolInvalid: boolean[] = [];
310
311
  let poolBgTimer: ReturnType<typeof setInterval> | undefined;
311
312
  let poolRefreshInFlight = false;
312
313
 
@@ -325,6 +326,7 @@ function loadPool(): boolean {
325
326
  if (accts.length < 2) return false;
326
327
  poolAccounts = accts;
327
328
  poolCooldownUntil = accts.map(() => 0);
329
+ poolInvalid = accts.map(() => false);
328
330
  poolActive = 0;
329
331
  return true;
330
332
  } catch {
@@ -354,21 +356,25 @@ function poolSelectActive(): void {
354
356
  // return to it instead of staying on the fallback account.
355
357
  let pick = -1;
356
358
  for (let i = 0; i < poolAccounts.length; i++) {
357
- if (poolCooldownUntil[i] <= now) {
359
+ if (!poolInvalid[i] && poolCooldownUntil[i] <= now) {
358
360
  pick = i;
359
361
  break;
360
362
  }
361
363
  }
362
364
  if (pick === -1) {
363
- // all cooling down pick the one that frees up soonest
365
+ // All usable accounts are cooling down: pick the one that frees up soonest.
364
366
  let min = Number.POSITIVE_INFINITY;
365
367
  for (let i = 0; i < poolCooldownUntil.length; i++) {
366
- if (poolCooldownUntil[i] < min) {
368
+ if (!poolInvalid[i] && poolCooldownUntil[i] < min) {
367
369
  min = poolCooldownUntil[i];
368
370
  pick = i;
369
371
  }
370
372
  }
371
373
  }
374
+ if (pick === -1) {
375
+ poolLog("all pooled accounts have invalid OAuth credentials");
376
+ return;
377
+ }
372
378
  if (pick !== poolActive) {
373
379
  poolActive = pick;
374
380
  poolLog(`switched active → ${poolAccounts[poolActive].label}`);
@@ -386,13 +392,17 @@ async function poolRefreshAccount(i: number, force: boolean): Promise<boolean> {
386
392
  acct.access = next.access;
387
393
  acct.expires = next.expires;
388
394
  if (next.refresh) acct.refresh = next.refresh;
395
+ poolInvalid[i] = false;
389
396
  persistPool();
390
397
  poolLog(`refreshed ${acct.label} (exp ${new Date(acct.expires).toISOString()})`);
391
398
  return true;
392
399
  } catch (e) {
393
- poolLog(
394
- `refresh FAILED ${acct.label}: ${(e as Error)?.message ?? String(e)}`,
395
- );
400
+ const message = (e as Error)?.message ?? String(e);
401
+ poolLog(`refresh FAILED ${acct.label}: ${message}`);
402
+ if (/invalid_grant|refresh token (?:expired|revoked)|authentication_error|\b401\b/i.test(message)) {
403
+ poolInvalid[i] = true;
404
+ poolSelectActive();
405
+ }
396
406
  return false;
397
407
  }
398
408
  }
@@ -517,6 +527,28 @@ function poolUpsert(
517
527
  else cfg.accounts.push(entry);
518
528
  cfg.enabled = cfg.enabled !== false;
519
529
  writeFileSync(POOL_PATH, JSON.stringify(cfg, null, 2), { mode: 0o600 });
530
+
531
+ // Keep a running session in sync with /claude-pool-add. Previously the file
532
+ // changed but getApiKey() kept serving the stale in-memory token until restart.
533
+ const activeLabel = poolAccounts[poolActive]?.label;
534
+ const previousCooldown = new Map(
535
+ poolAccounts.map((account, i) => [account.label, poolCooldownUntil[i] ?? 0]),
536
+ );
537
+ const previousInvalid = new Map(
538
+ poolAccounts.map((account, i) => [account.label, poolInvalid[i] ?? false]),
539
+ );
540
+ poolAccounts = cfg.accounts;
541
+ poolCooldownUntil = poolAccounts.map((account) =>
542
+ account.label === label ? 0 : (previousCooldown.get(account.label) ?? 0),
543
+ );
544
+ poolInvalid = poolAccounts.map((account) =>
545
+ account.label === label ? false : (previousInvalid.get(account.label) ?? false),
546
+ );
547
+ poolActive = Math.max(
548
+ 0,
549
+ poolAccounts.findIndex((account) => account.label === activeLabel),
550
+ );
551
+ poolSelectActive();
520
552
  return cfg.accounts.length;
521
553
  }
522
554
 
package/oauth.ts CHANGED
@@ -8,9 +8,11 @@ export interface AnthropicOAuthCredential {
8
8
  expires: number;
9
9
  }
10
10
 
11
- export interface AnthropicOAuthCallbacks {
12
- notify: (event: Record<string, unknown>) => void;
13
- prompt: (prompt: Record<string, unknown>) => Promise<string>;
11
+ export interface AnthropicOAuthLoginCallbacks {
12
+ onAuth: (info: { url: string; instructions?: string }) => void;
13
+ onManualCodeInput: () => Promise<string>;
14
+ onProgress?: (message: string) => void;
15
+ signal?: AbortSignal;
14
16
  }
15
17
 
16
18
  const CLIENT_ID = Buffer.from("OWQxYzI1MGEtZTYxYi00NGQ5LTg4ZWQtNTk0NGQxOTYyZjVl", "base64").toString("utf8");
@@ -165,12 +167,11 @@ async function exchangeAuthorizationCode(
165
167
  }
166
168
 
167
169
  export async function loginAnthropicOAuth(
168
- interaction: AnthropicOAuthCallbacks,
170
+ callbacks: AnthropicOAuthLoginCallbacks,
169
171
  ): Promise<AnthropicOAuthCredential> {
170
172
  const verifier = randomBytes(32).toString("base64url");
171
173
  const challenge = createHash("sha256").update(verifier).digest("base64url");
172
174
  const server = await startCallbackServer(verifier);
173
- const manualAbort = new AbortController();
174
175
  let manualInput: string | undefined;
175
176
  let manualError: Error | undefined;
176
177
  try {
@@ -184,18 +185,12 @@ export async function loginAnthropicOAuth(
184
185
  code_challenge_method: "S256",
185
186
  state: verifier,
186
187
  });
187
- interaction.notify({
188
- type: "auth_url",
188
+ callbacks.onAuth({
189
189
  url: `${AUTHORIZE_URL}?${authParams.toString()}`,
190
190
  instructions: "Complete login in your browser. If the browser is on another machine, paste the final redirect URL here.",
191
191
  });
192
- const manualPromise = interaction
193
- .prompt({
194
- type: "manual_code",
195
- message: "Complete login in your browser, or paste the authorization code / redirect URL here:",
196
- placeholder: REDIRECT_URI,
197
- signal: manualAbort.signal,
198
- })
192
+ const manualPromise = callbacks
193
+ .onManualCodeInput()
199
194
  .then((input) => {
200
195
  manualInput = input;
201
196
  server.cancelWait();
@@ -216,10 +211,9 @@ export async function loginAnthropicOAuth(
216
211
  parsed.code = fallback.code;
217
212
  }
218
213
  if (!parsed.code) throw new Error("Missing authorization code");
219
- interaction.notify({ type: "progress", message: "Exchanging authorization code for tokens..." });
214
+ callbacks.onProgress?.("Exchanging authorization code for tokens...");
220
215
  return exchangeAuthorizationCode(parsed.code, parsed.state ?? verifier, verifier);
221
216
  } finally {
222
- manualAbort.abort();
223
217
  server.server.close();
224
218
  }
225
219
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zgltyq/pi-provider-claude",
3
- "version": "1.3.1",
3
+ "version": "1.3.3",
4
4
  "description": "Claude (Anthropic OAuth/subscription) compatibility layer for pi-coding-agent — keeps ALL extension tools usable under the Claude subscription by mapping unknown flat tool names to mcp__pi__<name> on the wire instead of dropping them, plus optional multi-account failover. Self-contained fork of the tool half of @benvargas/pi-claude-code-use.",
5
5
  "author": "Leechael",
6
6
  "license": "MIT",