@event4u/agent-switch 2.3.1 → 2.3.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/daemon.js +92 -24
- package/dist/index.js +7 -8
- package/dist/providers.js +23 -12
- package/dist/telemetry.js +4 -0
- package/dist/token-freshen.js +17 -6
- package/package.json +1 -1
package/dist/daemon.js
CHANGED
|
@@ -24,11 +24,12 @@
|
|
|
24
24
|
* freshness, suggestion decision in `usage.ts`) are exported for unit testing;
|
|
25
25
|
* `runDaemon` is the loop.
|
|
26
26
|
*/
|
|
27
|
+
import * as crypto from "node:crypto";
|
|
27
28
|
import * as fs from "node:fs";
|
|
28
29
|
import * as path from "node:path";
|
|
29
30
|
import { ROOT, activeFor, configDir, isDisabled, labelFor, listProfiles, readAutoSwitch, readBinaryPath, sharesDefaultLogin } from "./profiles.js";
|
|
30
31
|
import { profileDir } from "./profiles.js";
|
|
31
|
-
import { fetchUsage, liveSessionPids } from "./api.js";
|
|
32
|
+
import { fetchUsage, liveSessionPids, readProfileCredential } from "./api.js";
|
|
32
33
|
import { defaultFreshenDeps, usageToken } from "./token-freshen.js";
|
|
33
34
|
import { parseUsage, detectCrossings, pickSwitchTarget, maxUtilization } from "./usage.js";
|
|
34
35
|
import { readCodexUsage } from "./codex-usage.js";
|
|
@@ -89,14 +90,6 @@ export function writeDaemonState(state, file = STATE_FILE) {
|
|
|
89
90
|
fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
90
91
|
fs.writeFileSync(file, JSON.stringify(state, null, 2) + "\n", { mode: 0o600 });
|
|
91
92
|
}
|
|
92
|
-
/** True when the state's lastPoll is within maxAgeMs of `now` — the CLI/GUI use
|
|
93
|
-
* this to decide whether to trust the cache instead of hitting the API. */
|
|
94
|
-
export function isFresh(state, maxAgeMs, now = Date.now()) {
|
|
95
|
-
if (!state?.lastPoll)
|
|
96
|
-
return false;
|
|
97
|
-
const t = Date.parse(state.lastPoll);
|
|
98
|
-
return !isNaN(t) && now - t <= maxAgeMs;
|
|
99
|
-
}
|
|
100
93
|
// ---------- single instance (pidfile) ----------
|
|
101
94
|
export function processAlive(pid) {
|
|
102
95
|
try {
|
|
@@ -182,6 +175,54 @@ export function nextIntervalMs(baseMs, consecutiveFailures, capMs = MAX_BACKOFF_
|
|
|
182
175
|
return base;
|
|
183
176
|
return Math.min(base * 2 ** consecutiveFailures, capMs);
|
|
184
177
|
}
|
|
178
|
+
/** The profile's backoff after a read — undefined once a read succeeds. */
|
|
179
|
+
export function nextProfileBackoff(prev, ok, baseMs, now, stamp) {
|
|
180
|
+
if (ok)
|
|
181
|
+
return undefined;
|
|
182
|
+
const failures = (prev?.failures ?? 0) + 1;
|
|
183
|
+
return { failures, nextAt: now + nextIntervalMs(baseMs, failures), ...(stamp !== undefined ? { stamp } : {}) };
|
|
184
|
+
}
|
|
185
|
+
export function backoffBlocks(b, now, stamp) {
|
|
186
|
+
if (b === undefined)
|
|
187
|
+
return false;
|
|
188
|
+
if (b.stamp !== undefined && stamp !== b.stamp)
|
|
189
|
+
return false;
|
|
190
|
+
return now < b.nextAt;
|
|
191
|
+
}
|
|
192
|
+
/** A fingerprint of a stored login, so a change can be noticed without the
|
|
193
|
+
* service holding on to the credential. */
|
|
194
|
+
export function loginStamp(raw) {
|
|
195
|
+
return raw === null ? null : crypto.createHash("sha256").update(raw, "utf8").digest("hex").slice(0, 16);
|
|
196
|
+
}
|
|
197
|
+
/** The targets a cycle reads: those not held back by their own backoff. The
|
|
198
|
+
* login is read only for a profile that has a backoff entry. */
|
|
199
|
+
export function dueTargets(provider, targets, backoff, now, stampOf) {
|
|
200
|
+
return targets.filter((n) => {
|
|
201
|
+
const b = backoff.get(`${provider}/${n}`);
|
|
202
|
+
return !b || !backoffBlocks(b, now, b.stamp !== undefined ? stampOf(n) : undefined);
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
/** The loop-wide failure count after a cycle: it grows only when every read
|
|
206
|
+
* failed (offline, API down) — single failures are handled by
|
|
207
|
+
* {@link nextProfileBackoff}. A cycle that read nothing leaves it unchanged. */
|
|
208
|
+
export function nextConsecutiveFailures(prev, failures, attempted) {
|
|
209
|
+
if (attempted === 0)
|
|
210
|
+
return prev;
|
|
211
|
+
return failures >= attempted ? prev + 1 : 0;
|
|
212
|
+
}
|
|
213
|
+
/** How old a cached snapshot may be and still be served instead of a live read:
|
|
214
|
+
* the daemon's slowest cadence (an idle account, or its current interval when
|
|
215
|
+
* that is longer) plus one loop wake. A healthy daemon answers from cache; a
|
|
216
|
+
* profile whose reads keep failing ages out and is read live. */
|
|
217
|
+
export function cacheMaxAgeMs(pollIntervalMs) {
|
|
218
|
+
return Math.max(POOL_POLL_INTERVAL_MS, pollIntervalMs ?? 0) + MIN_INTERVAL_MS;
|
|
219
|
+
}
|
|
220
|
+
export function snapshotFresh(snap, now = Date.now(), maxAgeMs = cacheMaxAgeMs(undefined)) {
|
|
221
|
+
if (!snap)
|
|
222
|
+
return false;
|
|
223
|
+
const t = Date.parse(snap.capturedAt);
|
|
224
|
+
return !isNaN(t) && now - t <= maxAgeMs;
|
|
225
|
+
}
|
|
185
226
|
/**
|
|
186
227
|
* Compliance-locked threshold decision (Council follow-up). Given the active
|
|
187
228
|
* profile and the same-provider candidates polled this cycle, decide whether to
|
|
@@ -307,20 +348,38 @@ export function monitorContext(provider, name, state, log) {
|
|
|
307
348
|
* the number of failures this cycle (for backoff). */
|
|
308
349
|
/** One profile's own usage snapshot: Claude via its OAuth endpoint, Codex via
|
|
309
350
|
* the live `wham/usage` read. null when unreadable (counts as a poll failure). */
|
|
310
|
-
async function snapshotFor(provider, name) {
|
|
351
|
+
async function snapshotFor(provider, name, log) {
|
|
311
352
|
if (provider === "codex")
|
|
312
353
|
return readCodexUsage(configDir("codex", name));
|
|
313
354
|
// An expired access token is the daemon's most common "usage stopped moving"
|
|
314
355
|
// cause — a profile nobody is running never refreshes itself. Let Claude Code
|
|
315
356
|
// refresh it first (cooldown-guarded, no-op while the token is still good) —
|
|
316
357
|
// never for a profile sharing the default install's login.
|
|
317
|
-
const token = usageToken(configDir("claude", name), defaultFreshenDeps(readBinaryPath("claude")), sharesDefaultLogin("claude", name))
|
|
358
|
+
const token = usageToken(configDir("claude", name), defaultFreshenDeps(readBinaryPath("claude")), sharesDefaultLogin("claude", name), (outcome) => {
|
|
359
|
+
if (outcome === "unavailable")
|
|
360
|
+
log(`freshen: claude/${name} — could not start \`claude doctor\` (not found on this service's PATH; link it in the app)`);
|
|
361
|
+
else if (outcome === "still-expired")
|
|
362
|
+
log(`freshen: claude/${name} — Claude Code could not refresh the login (sign in again)`);
|
|
363
|
+
});
|
|
318
364
|
if (!token)
|
|
319
365
|
return null;
|
|
320
366
|
const raw = await fetchUsage(token);
|
|
321
367
|
return raw ? parseUsage(raw) : null;
|
|
322
368
|
}
|
|
323
|
-
|
|
369
|
+
/** The stored login behind a profile, fingerprinted: Claude's credential
|
|
370
|
+
* store, Codex's auth.json. Read after a failure, so a refresh the failed
|
|
371
|
+
* read already triggered counts as the baseline, not as a change. */
|
|
372
|
+
function loginStampFor(provider, name) {
|
|
373
|
+
if (provider === "claude")
|
|
374
|
+
return loginStamp(readProfileCredential(configDir("claude", name)));
|
|
375
|
+
try {
|
|
376
|
+
return loginStamp(fs.readFileSync(path.join(configDir(provider, name), "auth.json"), "utf8"));
|
|
377
|
+
}
|
|
378
|
+
catch {
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
async function pollProvider(provider, state, thresholds, notifiedReset, backoff, baseMs, log) {
|
|
324
383
|
const names = listProfiles(provider).filter((n) => !isDisabled(provider, n));
|
|
325
384
|
const active = activeFor(provider);
|
|
326
385
|
const autoSwitch = readAutoSwitch(provider);
|
|
@@ -332,14 +391,19 @@ async function pollProvider(provider, state, thresholds, notifiedReset, log) {
|
|
|
332
391
|
const t = Date.parse(state.profiles[`${provider}/${n}`]?.capturedAt ?? "");
|
|
333
392
|
return isNaN(t) ? null : t;
|
|
334
393
|
};
|
|
335
|
-
const targets = selectPollTargets(names, active, isLive, autoSwitch.enabled, pooled, lastCaptured);
|
|
394
|
+
const targets = dueTargets(provider, selectPollTargets(names, active, isLive, autoSwitch.enabled, pooled, lastCaptured), backoff, Date.now(), (n) => loginStampFor(provider, n));
|
|
336
395
|
let failures = 0;
|
|
337
396
|
const polled = [];
|
|
338
397
|
state.notifiedFetchFail = state.notifiedFetchFail ?? {};
|
|
339
398
|
state.fetchFailingSince = state.fetchFailingSince ?? {};
|
|
340
399
|
for (const name of targets) {
|
|
341
400
|
const key = `${provider}/${name}`;
|
|
342
|
-
const snapshot = await snapshotFor(provider, name);
|
|
401
|
+
const snapshot = await snapshotFor(provider, name, log);
|
|
402
|
+
const nextBackoff = snapshot ? undefined : nextProfileBackoff(backoff.get(key), false, baseMs, Date.now(), loginStampFor(provider, name));
|
|
403
|
+
if (nextBackoff)
|
|
404
|
+
backoff.set(key, nextBackoff);
|
|
405
|
+
else
|
|
406
|
+
backoff.delete(key);
|
|
343
407
|
if (!snapshot) {
|
|
344
408
|
failures++;
|
|
345
409
|
// Fail-safe: the last-known snapshot in state.profiles[key] is left intact
|
|
@@ -388,12 +452,13 @@ async function pollProvider(provider, state, thresholds, notifiedReset, log) {
|
|
|
388
452
|
// profile even when the usage API poll failed (expired token, offline).
|
|
389
453
|
if (active)
|
|
390
454
|
monitorContext(provider, active, state, log);
|
|
455
|
+
const result = { failures, attempted: targets.length };
|
|
391
456
|
if (!autoSwitch.enabled || !active)
|
|
392
|
-
return
|
|
457
|
+
return result;
|
|
393
458
|
const activeSnap = polled.find((p) => p.name === active)?.snapshot;
|
|
394
459
|
const activeMax = activeSnap ? maxUtilization(activeSnap) : null;
|
|
395
460
|
if (activeMax === null || activeMax < autoSwitch.threshold)
|
|
396
|
-
return
|
|
461
|
+
return result; // still has headroom
|
|
397
462
|
// Codex only (banked resets are a Codex feature): a reset credit is scarce and
|
|
398
463
|
// redeeming it is the user's call (D3) — the daemon only says one is there,
|
|
399
464
|
// once per reset cycle, and never calls the redeem endpoint itself.
|
|
@@ -429,16 +494,18 @@ async function pollProvider(provider, state, thresholds, notifiedReset, log) {
|
|
|
429
494
|
message: `${provider}/${active} hit ≥${autoSwitch.threshold}% — suggested profile: ${provider}/${target}. ${how}`,
|
|
430
495
|
});
|
|
431
496
|
}
|
|
432
|
-
return
|
|
497
|
+
return result;
|
|
433
498
|
}
|
|
434
|
-
async function pollOnce(state, thresholds, notifiedReset, log) {
|
|
435
|
-
|
|
499
|
+
async function pollOnce(state, thresholds, notifiedReset, backoff, baseMs, log) {
|
|
500
|
+
const total = { failures: 0, attempted: 0 };
|
|
436
501
|
for (const provider of AUTO_PROVIDERS) {
|
|
437
|
-
|
|
502
|
+
const r = await pollProvider(provider, state, thresholds, notifiedReset, backoff, baseMs, log);
|
|
503
|
+
total.failures += r.failures;
|
|
504
|
+
total.attempted += r.attempted;
|
|
438
505
|
}
|
|
439
506
|
state.lastPoll = new Date().toISOString();
|
|
440
|
-
state.lastError = failures > 0 ? `${failures} profile(s) failed to poll` : null;
|
|
441
|
-
return
|
|
507
|
+
state.lastError = total.failures > 0 ? `${total.failures} profile(s) failed to poll` : null;
|
|
508
|
+
return total;
|
|
442
509
|
}
|
|
443
510
|
/** Run the daemon loop. Single-instance; SIGTERM-clean. */
|
|
444
511
|
export async function runDaemon(opts = {}) {
|
|
@@ -469,13 +536,14 @@ export async function runDaemon(opts = {}) {
|
|
|
469
536
|
// `<provider>/<name>` → the window resets_at we last announced a banked reset
|
|
470
537
|
// for, so the "reset available" notice fires once per cycle, not every poll.
|
|
471
538
|
const notifiedReset = new Map();
|
|
539
|
+
const backoff = new Map();
|
|
472
540
|
let consecutiveFailures = 0;
|
|
473
541
|
let cycles = 0;
|
|
474
542
|
log(`started (pid ${process.pid}, base interval ${baseInterval}ms).`);
|
|
475
543
|
while (running) {
|
|
476
544
|
try {
|
|
477
|
-
const failures = await pollOnce(state, thresholds, notifiedReset, log);
|
|
478
|
-
consecutiveFailures =
|
|
545
|
+
const { failures, attempted } = await pollOnce(state, thresholds, notifiedReset, backoff, baseInterval, log);
|
|
546
|
+
consecutiveFailures = nextConsecutiveFailures(consecutiveFailures, failures, attempted);
|
|
479
547
|
}
|
|
480
548
|
catch (err) {
|
|
481
549
|
consecutiveFailures++;
|
package/dist/index.js
CHANGED
|
@@ -47,7 +47,7 @@ import { formatSnapshot, parseUsage } from "./usage.js";
|
|
|
47
47
|
import { readCodexUsage } from "./codex-usage.js";
|
|
48
48
|
import { redeemResetCredit } from "./codex-reset.js";
|
|
49
49
|
import { assertValidSessionId, cleanupForkVehicle, codexSessionCommand, deleteSession, detectRunningClaudeProfile, listSessions, listCodexSessions, locateSession, locateCodexSession, markLive, openQuestion, restoreSession, sharedHistory, sweepTrash, transferSession, transferCodexSession, trashedSessionExists, } from "./sessions.js";
|
|
50
|
-
import {
|
|
50
|
+
import { cacheMaxAgeMs, snapshotFresh, readDaemonState, readPid, PIDFILE, processAlive } from "./daemon.js";
|
|
51
51
|
import { cmdService, serviceAfterUpdate, serviceInstall, serviceInstallFromDisk, serviceTarget, serviceUninstall } from "./service.js";
|
|
52
52
|
import { fileURLToPath } from "node:url";
|
|
53
53
|
import { readContext, turnInFlight } from "./telemetry.js";
|
|
@@ -456,14 +456,13 @@ function cmdWhoami(providerId, name) {
|
|
|
456
456
|
* credential is unreadable or the API shape is unknown. Codex/Antigravity have no
|
|
457
457
|
* usage readout (verified), so this is Claude-only. */
|
|
458
458
|
async function claudeSnapshot(name) {
|
|
459
|
-
// Prefer the daemon's cache
|
|
460
|
-
// daemon is a cache, and the CLI works with or without it.
|
|
459
|
+
// Prefer the daemon's cache while THIS profile's snapshot is fresh — the
|
|
460
|
+
// daemon is a cache, and the CLI works with or without it. The age of the
|
|
461
|
+
// last poll says nothing about a profile whose reads have been failing.
|
|
461
462
|
const state = readDaemonState();
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
return cached;
|
|
466
|
-
}
|
|
463
|
+
const cached = state?.profiles?.[`claude/${name}`];
|
|
464
|
+
if (snapshotFresh(cached, Date.now(), cacheMaxAgeMs(state?.pollIntervalMs)))
|
|
465
|
+
return cached;
|
|
467
466
|
const token = usageToken(configDir("claude", name), defaultFreshenDeps(readBinaryPath("claude")), sharesDefaultLogin("claude", name));
|
|
468
467
|
if (!token)
|
|
469
468
|
return null;
|
package/dist/providers.js
CHANGED
|
@@ -139,18 +139,30 @@ export function provider(id) {
|
|
|
139
139
|
export function allProviders() {
|
|
140
140
|
return PROVIDER_IDS.map((id) => PROVIDERS[id]);
|
|
141
141
|
}
|
|
142
|
-
/**
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
|
|
146
|
-
|
|
142
|
+
/**
|
|
143
|
+
* Where an installer may drop a binary off the minimal PATH a GUI app (and the
|
|
144
|
+
* service it starts) runs with on macOS — `/usr/bin:/bin:/usr/sbin:/sbin`.
|
|
145
|
+
* ~/.local/bin (e.g. agy via `curl … | bash`) first, then Homebrew on Apple
|
|
146
|
+
* silicon and Intel. The first existing path, or null.
|
|
147
|
+
*/
|
|
148
|
+
export function offPathBinary(binary, home = os.homedir(), exists = fs.existsSync, platform = process.platform) {
|
|
149
|
+
const p = platform === "win32" ? path.win32 : path.posix;
|
|
150
|
+
const dirs = [p.join(home, ".local", "bin")];
|
|
151
|
+
if (platform !== "win32")
|
|
152
|
+
dirs.push("/opt/homebrew/bin", "/usr/local/bin");
|
|
153
|
+
for (const dir of dirs) {
|
|
154
|
+
const file = p.join(dir, binary);
|
|
155
|
+
if (exists(file))
|
|
156
|
+
return file;
|
|
157
|
+
}
|
|
158
|
+
return null;
|
|
147
159
|
}
|
|
148
160
|
/**
|
|
149
161
|
* Resolve a CLI binary to an invocable command: a user-linked path when set and
|
|
150
162
|
* present (the CLI is not on PATH but the user pointed us at it), otherwise its
|
|
151
|
-
* bare name when it is on PATH, otherwise its
|
|
152
|
-
*
|
|
153
|
-
*
|
|
163
|
+
* bare name when it is on PATH, otherwise its path in one of the
|
|
164
|
+
* {@link offPathBinary} dirs, otherwise the bare name so the caller surfaces a
|
|
165
|
+
* clean "not found" error.
|
|
154
166
|
*/
|
|
155
167
|
export function resolveBinary(binary, linked) {
|
|
156
168
|
if (linked && fs.existsSync(linked))
|
|
@@ -158,14 +170,13 @@ export function resolveBinary(binary, linked) {
|
|
|
158
170
|
const probe = spawnSync(binary, ["--version"], { stdio: "ignore", timeout: 5000 });
|
|
159
171
|
if (probe.error?.code !== "ENOENT")
|
|
160
172
|
return binary;
|
|
161
|
-
|
|
162
|
-
return fs.existsSync(local) ? local : binary;
|
|
173
|
+
return offPathBinary(binary) ?? binary;
|
|
163
174
|
}
|
|
164
175
|
/**
|
|
165
176
|
* Best-effort: is this provider's CLI binary usable? True when a user-linked
|
|
166
177
|
* path is set and present, else when `<binary> --version` resolves on PATH
|
|
167
178
|
* (only ENOENT counts as not-installed — a non-zero exit or a timeout still
|
|
168
|
-
* means the binary exists), else when it lives in
|
|
179
|
+
* means the binary exists), else when it lives in an {@link offPathBinary} dir. Used to gate
|
|
169
180
|
* enabling a provider the user hasn't installed (or hasn't linked).
|
|
170
181
|
*/
|
|
171
182
|
export function isProviderInstalled(id, linked) {
|
|
@@ -175,5 +186,5 @@ export function isProviderInstalled(id, linked) {
|
|
|
175
186
|
const probe = spawnSync(bin, ["--version"], { stdio: "ignore", timeout: 5000 });
|
|
176
187
|
if (probe.error?.code !== "ENOENT")
|
|
177
188
|
return true;
|
|
178
|
-
return
|
|
189
|
+
return offPathBinary(bin) !== null;
|
|
179
190
|
}
|
package/dist/telemetry.js
CHANGED
|
@@ -43,6 +43,10 @@ export const SUPPORTED_CLAUDE = ["2.1"];
|
|
|
43
43
|
* tokens, never guess a percentage). User overrides via state.json merge on
|
|
44
44
|
* top. Codex needs no table — its rollout carries model_context_window. */
|
|
45
45
|
export const CONTEXT_WINDOWS = {
|
|
46
|
+
"claude-fable-5-1": 1_000_000,
|
|
47
|
+
"claude-opus-5-5": 1_000_000,
|
|
48
|
+
"claude-sonnet-5-5": 1_000_000,
|
|
49
|
+
"claude-haiku-5-5": 200_000,
|
|
46
50
|
"claude-fable-5": 1_000_000,
|
|
47
51
|
"claude-opus-4-8": 1_000_000,
|
|
48
52
|
"claude-opus-4-7": 1_000_000,
|
package/dist/token-freshen.js
CHANGED
|
@@ -96,14 +96,24 @@ function recordAttemptFile(configDir, at) {
|
|
|
96
96
|
/* best-effort: a missing stamp only costs an extra attempt */
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
|
+
/**
|
|
100
|
+
* The PATH `claude doctor` runs with: the resolved binary's dir and this node's
|
|
101
|
+
* dir ahead of the inherited PATH. An npm-installed `claude` is a
|
|
102
|
+
* `#!/usr/bin/env node` script, and the service's minimal PATH has no node.
|
|
103
|
+
*/
|
|
104
|
+
export function doctorPath(bin, execPath, inherited) {
|
|
105
|
+
const dirs = [path.isAbsolute(bin) ? path.dirname(bin) : null, path.dirname(execPath), ...(inherited ?? "").split(path.delimiter)];
|
|
106
|
+
return [...new Set(dirs.filter((d) => !!d))].join(path.delimiter);
|
|
107
|
+
}
|
|
99
108
|
/**
|
|
100
109
|
* `claude doctor` against one profile's config dir. stdout/stderr are dropped —
|
|
101
110
|
* this is a side-effect call, not a readout. Any completed run returns true;
|
|
102
111
|
* whether the token actually got refreshed is decided by re-reading the store.
|
|
103
112
|
*/
|
|
104
113
|
function runClaudeDoctor(configDir, linkedBinary) {
|
|
105
|
-
const
|
|
106
|
-
|
|
114
|
+
const bin = resolveBinary("claude", linkedBinary);
|
|
115
|
+
const res = spawnSync(bin, ["doctor"], {
|
|
116
|
+
env: { ...process.env, CLAUDE_CONFIG_DIR: configDir, PATH: doctorPath(bin, process.execPath, process.env.PATH) },
|
|
107
117
|
// Claude Code reads settings from the CWD; run it somewhere neutral so a
|
|
108
118
|
// project's config can never influence a background health check.
|
|
109
119
|
cwd: os.tmpdir(),
|
|
@@ -147,8 +157,9 @@ export function freshenToken(configDir, deps = defaultFreshenDeps()) {
|
|
|
147
157
|
* no readable credential at all); a freshen that could not heal the login just
|
|
148
158
|
* means the caller's read fails as it did before.
|
|
149
159
|
*/
|
|
150
|
-
export function freshAccessToken(configDir, deps = defaultFreshenDeps()) {
|
|
151
|
-
freshenToken(configDir, deps);
|
|
160
|
+
export function freshAccessToken(configDir, deps = defaultFreshenDeps(), onOutcome) {
|
|
161
|
+
const outcome = freshenToken(configDir, deps);
|
|
162
|
+
onOutcome?.(outcome);
|
|
152
163
|
const creds = deps.readCredential(configDir);
|
|
153
164
|
return creds ? accessTokenOf(creds) : null;
|
|
154
165
|
}
|
|
@@ -157,9 +168,9 @@ export function freshAccessToken(configDir, deps = defaultFreshenDeps()) {
|
|
|
157
168
|
* install (seeded by `import`) is never refreshed: whichever side refreshes
|
|
158
169
|
* first invalidates the other, so its stored token is used only while valid.
|
|
159
170
|
*/
|
|
160
|
-
export function usageToken(configDir, deps, sharesDefaultLogin) {
|
|
171
|
+
export function usageToken(configDir, deps, sharesDefaultLogin, onOutcome) {
|
|
161
172
|
if (!sharesDefaultLogin)
|
|
162
|
-
return freshAccessToken(configDir, deps);
|
|
173
|
+
return freshAccessToken(configDir, deps, onOutcome);
|
|
163
174
|
const creds = deps.readCredential(configDir);
|
|
164
175
|
return creds && !needsFreshen(creds, deps.now(), deps.bufferMs) ? accessTokenOf(creds) : null;
|
|
165
176
|
}
|
package/package.json
CHANGED