@albinocrabs/o-switcher 0.1.1 → 0.3.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Multi-profile auth rotation for OpenAI accounts
8
+ - Watch all `auth-work*.json` files, not just `auth.json`
9
+ - Add `switchProfile` / `switchToNextProfile` for credential rotation
10
+ - Auto-switch to next healthy profile when health drops below 30%
11
+
12
+ ## 0.2.0
13
+
14
+ ### Minor Changes
15
+
16
+ - 57f1eb9: Add TUI sidebar panel and status dashboard for OpenCode
17
+ - Sidebar footer shows active profile, health %, and target count
18
+ - `/switcher` slash command opens full status dashboard with all targets
19
+ - File-based state bridge between server plugin and TUI (atomic writes, debounced)
20
+
3
21
  All notable changes to this project will be documented in this file.
4
22
 
5
23
  The format is based on [Keep a Changelog](https://keepachangelog.com/),
@@ -6,10 +6,10 @@ var crypto = require('crypto');
6
6
  var eventemitter3 = require('eventemitter3');
7
7
  var cockatiel = require('cockatiel');
8
8
  var PQueue = require('p-queue');
9
+ var tool = require('@opencode-ai/plugin/tool');
9
10
  var promises = require('fs/promises');
10
11
  var os = require('os');
11
12
  var path = require('path');
12
- var tool = require('@opencode-ai/plugin/tool');
13
13
 
14
14
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
15
15
 
@@ -1363,6 +1363,78 @@ var reloadConfig = (deps, rawConfig) => {
1363
1363
  throw err;
1364
1364
  }
1365
1365
  };
1366
+ var { schema: z3 } = tool.tool;
1367
+ var createOperatorTools = (deps) => ({
1368
+ listTargets: tool.tool({
1369
+ description: "List all routing targets with health scores, states, and circuit breaker status.",
1370
+ args: {},
1371
+ async execute() {
1372
+ deps.logger.info({ op: "listTargets" }, "operator: listTargets");
1373
+ const result = listTargets(deps);
1374
+ return JSON.stringify(result, null, 2);
1375
+ }
1376
+ }),
1377
+ pauseTarget: tool.tool({
1378
+ description: "Pause a target, preventing new requests from being routed to it.",
1379
+ args: { target_id: z3.string().min(1) },
1380
+ async execute(args) {
1381
+ deps.logger.info({ op: "pauseTarget", target_id: args.target_id }, "operator: pauseTarget");
1382
+ const result = pauseTarget(deps, args.target_id);
1383
+ return JSON.stringify(result, null, 2);
1384
+ }
1385
+ }),
1386
+ resumeTarget: tool.tool({
1387
+ description: "Resume a previously paused or disabled target, allowing new requests.",
1388
+ args: { target_id: z3.string().min(1) },
1389
+ async execute(args) {
1390
+ deps.logger.info({ op: "resumeTarget", target_id: args.target_id }, "operator: resumeTarget");
1391
+ const result = resumeTarget(deps, args.target_id);
1392
+ return JSON.stringify(result, null, 2);
1393
+ }
1394
+ }),
1395
+ drainTarget: tool.tool({
1396
+ description: "Drain a target, allowing in-flight requests to complete but preventing new ones.",
1397
+ args: { target_id: z3.string().min(1) },
1398
+ async execute(args) {
1399
+ deps.logger.info({ op: "drainTarget", target_id: args.target_id }, "operator: drainTarget");
1400
+ const result = drainTarget(deps, args.target_id);
1401
+ return JSON.stringify(result, null, 2);
1402
+ }
1403
+ }),
1404
+ disableTarget: tool.tool({
1405
+ description: "Disable a target entirely, removing it from routing.",
1406
+ args: { target_id: z3.string().min(1) },
1407
+ async execute(args) {
1408
+ deps.logger.info(
1409
+ { op: "disableTarget", target_id: args.target_id },
1410
+ "operator: disableTarget"
1411
+ );
1412
+ const result = disableTarget(deps, args.target_id);
1413
+ return JSON.stringify(result, null, 2);
1414
+ }
1415
+ }),
1416
+ inspectRequest: tool.tool({
1417
+ description: "Inspect a request trace by ID, showing attempts, segments, and outcome.",
1418
+ args: { request_id: z3.string().min(1) },
1419
+ async execute(args) {
1420
+ deps.logger.info(
1421
+ { op: "inspectRequest", request_id: args.request_id },
1422
+ "operator: inspectRequest"
1423
+ );
1424
+ const result = inspectRequest(deps, args.request_id);
1425
+ return JSON.stringify(result, null, 2);
1426
+ }
1427
+ }),
1428
+ reloadConfig: tool.tool({
1429
+ description: "Reload routing configuration with diff-apply. Validates new config before applying.",
1430
+ args: { config: z3.record(z3.string(), z3.unknown()) },
1431
+ async execute(args) {
1432
+ deps.logger.info({ op: "reloadConfig" }, "operator: reloadConfig");
1433
+ const result = reloadConfig(deps, args.config);
1434
+ return JSON.stringify(result, null, 2);
1435
+ }
1436
+ })
1437
+ });
1366
1438
  var PROFILES_DIR = path.join(os.homedir(), ".local", "share", "o-switcher");
1367
1439
  var PROFILES_PATH = path.join(PROFILES_DIR, "profiles.json");
1368
1440
  var loadProfiles = async (path = PROFILES_PATH) => {
@@ -1430,6 +1502,8 @@ var nextProfileId = (store, provider) => {
1430
1502
  return `${provider}-${maxN + 1}`;
1431
1503
  };
1432
1504
  var AUTH_JSON_PATH = path.join(os.homedir(), ".local", "share", "opencode", "auth.json");
1505
+ var AUTH_DIR = path.join(os.homedir(), ".local", "share", "opencode");
1506
+ var isAuthFile = (filename) => filename === "auth.json" || /^auth-work\d+\.json$/.test(filename);
1433
1507
  var DEBOUNCE_MS = 100;
1434
1508
  var readAuthJson = async (path) => {
1435
1509
  try {
@@ -1439,6 +1513,22 @@ var readAuthJson = async (path) => {
1439
1513
  return {};
1440
1514
  }
1441
1515
  };
1516
+ var readAllAuthFiles = async (dir) => {
1517
+ const { readdir } = await import('fs/promises');
1518
+ const merged = {};
1519
+ try {
1520
+ const files = await readdir(dir);
1521
+ for (const file of files.filter(isAuthFile)) {
1522
+ const data = await readAuthJson(path.join(dir, file));
1523
+ for (const [provider, entry] of Object.entries(data)) {
1524
+ const key = `${provider}:${file}`;
1525
+ merged[key] = entry;
1526
+ }
1527
+ }
1528
+ } catch {
1529
+ }
1530
+ return merged;
1531
+ };
1442
1532
  var toCredential = (entry) => {
1443
1533
  if (entry.type === "oauth" || entry.refresh !== void 0 && entry.access !== void 0) {
1444
1534
  return {
@@ -1460,22 +1550,23 @@ var entriesEqual = (a, b) => {
1460
1550
  return JSON.stringify(a) === JSON.stringify(b);
1461
1551
  };
1462
1552
  var createAuthWatcher = (options) => {
1463
- const authPath = options?.authJsonPath ?? AUTH_JSON_PATH;
1553
+ const authDir = options?.authJsonPath ? path.dirname(options.authJsonPath) : AUTH_DIR;
1464
1554
  const profPath = options?.profilesPath ?? PROFILES_PATH;
1465
1555
  const log = options?.logger?.child({ component: "profile-watcher" });
1466
1556
  let lastKnownAuth = {};
1467
1557
  let abortController = null;
1468
1558
  let debounceTimer = null;
1469
1559
  const processChange = async () => {
1470
- const newAuth = await readAuthJson(authPath);
1560
+ const newAuth = await readAllAuthFiles(authDir);
1471
1561
  let store = await loadProfiles(profPath);
1472
1562
  let changed = false;
1473
- for (const [provider, entry] of Object.entries(newAuth)) {
1563
+ for (const [compositeKey, entry] of Object.entries(newAuth)) {
1474
1564
  if (!entry) continue;
1475
- const previousEntry = lastKnownAuth[provider];
1565
+ const provider = compositeKey.split(":")[0] ?? compositeKey;
1566
+ const previousEntry = lastKnownAuth[compositeKey];
1476
1567
  if (previousEntry !== void 0 && !entriesEqual(previousEntry, entry)) {
1477
1568
  log?.info(
1478
- { provider, action: "credential_changed" },
1569
+ { provider, source: compositeKey, action: "credential_changed" },
1479
1570
  "Credential change detected \u2014 saving previous credential"
1480
1571
  );
1481
1572
  const prevCredential = toCredential(previousEntry);
@@ -1485,7 +1576,7 @@ var createAuthWatcher = (options) => {
1485
1576
  changed = true;
1486
1577
  }
1487
1578
  } else if (previousEntry === void 0) {
1488
- log?.info({ provider, action: "new_provider" }, "New provider detected");
1579
+ log?.info({ provider, source: compositeKey, action: "new_provider" }, "New auth file detected");
1489
1580
  const credential = toCredential(entry);
1490
1581
  const newStore = addProfile(store, provider, credential);
1491
1582
  if (newStore !== store) {
@@ -1512,32 +1603,32 @@ var createAuthWatcher = (options) => {
1512
1603
  }, DEBOUNCE_MS);
1513
1604
  };
1514
1605
  const start = async () => {
1515
- const currentAuth = await readAuthJson(authPath);
1606
+ const currentAuth = await readAllAuthFiles(authDir);
1516
1607
  const currentStore = await loadProfiles(profPath);
1517
- log?.info({ providers: Object.keys(currentAuth) }, "Auth watcher started");
1608
+ log?.info({ auth_keys: Object.keys(currentAuth).length }, "Auth watcher started");
1518
1609
  if (Object.keys(currentStore).length === 0 && Object.keys(currentAuth).length > 0) {
1519
1610
  let store = {};
1520
- for (const [provider, entry] of Object.entries(currentAuth)) {
1611
+ for (const [compositeKey, entry] of Object.entries(currentAuth)) {
1521
1612
  if (!entry) continue;
1613
+ const provider = compositeKey.split(":")[0] ?? compositeKey;
1522
1614
  const credential = toCredential(entry);
1523
1615
  store = addProfile(store, provider, credential);
1524
1616
  }
1525
1617
  await saveProfiles(store, profPath);
1526
1618
  log?.info(
1527
1619
  { profiles_initialized: Object.keys(store).length },
1528
- "Initialized profiles from auth.json"
1620
+ "Initialized profiles from auth files"
1529
1621
  );
1530
1622
  }
1531
1623
  lastKnownAuth = currentAuth;
1532
1624
  abortController = new AbortController();
1533
- const parentDir = path.dirname(authPath);
1534
1625
  (async () => {
1535
1626
  try {
1536
- const watcher = promises.watch(parentDir, {
1627
+ const watcher = promises.watch(authDir, {
1537
1628
  signal: abortController.signal
1538
1629
  });
1539
1630
  for await (const event of watcher) {
1540
- if (event.filename === "auth.json" || event.filename === null) {
1631
+ if (event.filename !== null && isAuthFile(event.filename)) {
1541
1632
  onFileChange();
1542
1633
  }
1543
1634
  }
@@ -1559,7 +1650,89 @@ var createAuthWatcher = (options) => {
1559
1650
  };
1560
1651
  return { start, stop };
1561
1652
  };
1562
- var { schema: z3 } = tool.tool;
1653
+ var credentialToAuthEntry = (cred) => {
1654
+ if (cred.type === "oauth") {
1655
+ return {
1656
+ type: "oauth",
1657
+ refresh: cred.refresh,
1658
+ access: cred.access,
1659
+ expires: cred.expires,
1660
+ accountId: cred.accountId
1661
+ };
1662
+ }
1663
+ return {
1664
+ type: "api-key",
1665
+ key: cred.key
1666
+ };
1667
+ };
1668
+ var readCurrentAuth = async (authPath) => {
1669
+ try {
1670
+ const content = await promises.readFile(authPath, "utf-8");
1671
+ return JSON.parse(content);
1672
+ } catch {
1673
+ return void 0;
1674
+ }
1675
+ };
1676
+ var switchProfile = async (options) => {
1677
+ const profPath = options.profilesPath ?? PROFILES_PATH;
1678
+ const authPath = options.authJsonPath ?? AUTH_JSON_PATH;
1679
+ const log = options.logger?.child({ component: "profile-switcher" });
1680
+ const store = await loadProfiles(profPath);
1681
+ const target = store[options.targetProfileId];
1682
+ if (target === void 0) {
1683
+ log?.warn({ profileId: options.targetProfileId }, "Profile not found");
1684
+ return { success: false, to: options.targetProfileId, reason: "profile_not_found" };
1685
+ }
1686
+ const currentAuth = await readCurrentAuth(authPath);
1687
+ const currentAccountId = currentAuth?.[target.provider] ? currentAuth[target.provider].accountId : void 0;
1688
+ const newAuth = {};
1689
+ newAuth[target.provider] = credentialToAuthEntry(target.credentials);
1690
+ const dir = path.dirname(authPath);
1691
+ await promises.mkdir(dir, { recursive: true });
1692
+ const tmpPath = `${authPath}.tmp`;
1693
+ await promises.writeFile(tmpPath, JSON.stringify(newAuth, null, 2), "utf-8");
1694
+ await promises.rename(tmpPath, authPath);
1695
+ log?.info(
1696
+ {
1697
+ from: currentAccountId,
1698
+ to: target.credentials.type === "oauth" ? target.credentials.accountId : "(api-key)",
1699
+ profileId: target.id,
1700
+ provider: target.provider
1701
+ },
1702
+ "Switched active profile"
1703
+ );
1704
+ return {
1705
+ success: true,
1706
+ from: currentAccountId ?? void 0,
1707
+ to: target.id
1708
+ };
1709
+ };
1710
+ var switchToNextProfile = async (options) => {
1711
+ const profPath = options.profilesPath ?? PROFILES_PATH;
1712
+ const log = options.logger?.child({ component: "profile-switcher" });
1713
+ const store = await loadProfiles(profPath);
1714
+ const excludeSet = new Set(options.excludeProfileIds ?? []);
1715
+ if (options.currentProfileId) {
1716
+ excludeSet.add(options.currentProfileId);
1717
+ }
1718
+ const candidates = Object.values(store).filter((p) => p.provider === options.provider && !excludeSet.has(p.id)).sort((a, b) => new Date(a.created).getTime() - new Date(b.created).getTime());
1719
+ if (candidates.length === 0) {
1720
+ log?.warn({ provider: options.provider, excluded: [...excludeSet] }, "No available profiles to switch to");
1721
+ return {
1722
+ success: false,
1723
+ to: options.currentProfileId ?? "none",
1724
+ reason: "no_candidates"
1725
+ };
1726
+ }
1727
+ const next = candidates[0];
1728
+ return switchProfile({
1729
+ targetProfileId: next.id,
1730
+ profilesPath: profPath,
1731
+ authJsonPath: options.authJsonPath,
1732
+ logger: options.logger
1733
+ });
1734
+ };
1735
+ var { schema: z4 } = tool.tool;
1563
1736
  var createProfileTools = (options) => ({
1564
1737
  profilesList: tool.tool({
1565
1738
  description: "List all saved auth profiles with provider, type, and creation date.",
@@ -1578,7 +1751,7 @@ var createProfileTools = (options) => ({
1578
1751
  }),
1579
1752
  profilesRemove: tool.tool({
1580
1753
  description: "Remove a saved auth profile by ID.",
1581
- args: { id: z3.string().min(1) },
1754
+ args: { id: z4.string().min(1) },
1582
1755
  async execute(args) {
1583
1756
  const store = await loadProfiles(options?.profilesPath);
1584
1757
  const { store: newStore, removed } = removeProfile(store, args.id);
@@ -1632,6 +1805,7 @@ exports.createConcurrencyTracker = createConcurrencyTracker;
1632
1805
  exports.createCooldownManager = createCooldownManager;
1633
1806
  exports.createFailoverOrchestrator = createFailoverOrchestrator;
1634
1807
  exports.createLogSubscriber = createLogSubscriber;
1808
+ exports.createOperatorTools = createOperatorTools;
1635
1809
  exports.createProfileTools = createProfileTools;
1636
1810
  exports.createRegistry = createRegistry;
1637
1811
  exports.createRequestLogger = createRequestLogger;
@@ -1658,6 +1832,8 @@ exports.removeProfile = removeProfile;
1658
1832
  exports.resumeTarget = resumeTarget;
1659
1833
  exports.saveProfiles = saveProfiles;
1660
1834
  exports.selectTarget = selectTarget;
1835
+ exports.switchProfile = switchProfile;
1836
+ exports.switchToNextProfile = switchToNextProfile;
1661
1837
  exports.updateHealthScore = updateHealthScore;
1662
1838
  exports.updateLatencyEma = updateLatencyEma;
1663
1839
  exports.validateConfig = validateConfig;
@@ -4,10 +4,10 @@ import crypto from 'crypto';
4
4
  import { EventEmitter } from 'eventemitter3';
5
5
  import { CircuitState, ConsecutiveBreaker, CountBreaker, BrokenCircuitError, IsolatedCircuitError, circuitBreaker, handleAll } from 'cockatiel';
6
6
  import PQueue from 'p-queue';
7
+ import { tool } from '@opencode-ai/plugin/tool';
7
8
  import { readFile, mkdir, writeFile, rename, watch } from 'fs/promises';
8
9
  import { homedir } from 'os';
9
10
  import { join, dirname } from 'path';
10
- import { tool } from '@opencode-ai/plugin/tool';
11
11
 
12
12
  // src/config/defaults.ts
13
13
  var DEFAULT_RETRY_BUDGET = 3;
@@ -1355,6 +1355,78 @@ var reloadConfig = (deps, rawConfig) => {
1355
1355
  throw err;
1356
1356
  }
1357
1357
  };
1358
+ var { schema: z3 } = tool;
1359
+ var createOperatorTools = (deps) => ({
1360
+ listTargets: tool({
1361
+ description: "List all routing targets with health scores, states, and circuit breaker status.",
1362
+ args: {},
1363
+ async execute() {
1364
+ deps.logger.info({ op: "listTargets" }, "operator: listTargets");
1365
+ const result = listTargets(deps);
1366
+ return JSON.stringify(result, null, 2);
1367
+ }
1368
+ }),
1369
+ pauseTarget: tool({
1370
+ description: "Pause a target, preventing new requests from being routed to it.",
1371
+ args: { target_id: z3.string().min(1) },
1372
+ async execute(args) {
1373
+ deps.logger.info({ op: "pauseTarget", target_id: args.target_id }, "operator: pauseTarget");
1374
+ const result = pauseTarget(deps, args.target_id);
1375
+ return JSON.stringify(result, null, 2);
1376
+ }
1377
+ }),
1378
+ resumeTarget: tool({
1379
+ description: "Resume a previously paused or disabled target, allowing new requests.",
1380
+ args: { target_id: z3.string().min(1) },
1381
+ async execute(args) {
1382
+ deps.logger.info({ op: "resumeTarget", target_id: args.target_id }, "operator: resumeTarget");
1383
+ const result = resumeTarget(deps, args.target_id);
1384
+ return JSON.stringify(result, null, 2);
1385
+ }
1386
+ }),
1387
+ drainTarget: tool({
1388
+ description: "Drain a target, allowing in-flight requests to complete but preventing new ones.",
1389
+ args: { target_id: z3.string().min(1) },
1390
+ async execute(args) {
1391
+ deps.logger.info({ op: "drainTarget", target_id: args.target_id }, "operator: drainTarget");
1392
+ const result = drainTarget(deps, args.target_id);
1393
+ return JSON.stringify(result, null, 2);
1394
+ }
1395
+ }),
1396
+ disableTarget: tool({
1397
+ description: "Disable a target entirely, removing it from routing.",
1398
+ args: { target_id: z3.string().min(1) },
1399
+ async execute(args) {
1400
+ deps.logger.info(
1401
+ { op: "disableTarget", target_id: args.target_id },
1402
+ "operator: disableTarget"
1403
+ );
1404
+ const result = disableTarget(deps, args.target_id);
1405
+ return JSON.stringify(result, null, 2);
1406
+ }
1407
+ }),
1408
+ inspectRequest: tool({
1409
+ description: "Inspect a request trace by ID, showing attempts, segments, and outcome.",
1410
+ args: { request_id: z3.string().min(1) },
1411
+ async execute(args) {
1412
+ deps.logger.info(
1413
+ { op: "inspectRequest", request_id: args.request_id },
1414
+ "operator: inspectRequest"
1415
+ );
1416
+ const result = inspectRequest(deps, args.request_id);
1417
+ return JSON.stringify(result, null, 2);
1418
+ }
1419
+ }),
1420
+ reloadConfig: tool({
1421
+ description: "Reload routing configuration with diff-apply. Validates new config before applying.",
1422
+ args: { config: z3.record(z3.string(), z3.unknown()) },
1423
+ async execute(args) {
1424
+ deps.logger.info({ op: "reloadConfig" }, "operator: reloadConfig");
1425
+ const result = reloadConfig(deps, args.config);
1426
+ return JSON.stringify(result, null, 2);
1427
+ }
1428
+ })
1429
+ });
1358
1430
  var PROFILES_DIR = join(homedir(), ".local", "share", "o-switcher");
1359
1431
  var PROFILES_PATH = join(PROFILES_DIR, "profiles.json");
1360
1432
  var loadProfiles = async (path = PROFILES_PATH) => {
@@ -1422,6 +1494,8 @@ var nextProfileId = (store, provider) => {
1422
1494
  return `${provider}-${maxN + 1}`;
1423
1495
  };
1424
1496
  var AUTH_JSON_PATH = join(homedir(), ".local", "share", "opencode", "auth.json");
1497
+ var AUTH_DIR = join(homedir(), ".local", "share", "opencode");
1498
+ var isAuthFile = (filename) => filename === "auth.json" || /^auth-work\d+\.json$/.test(filename);
1425
1499
  var DEBOUNCE_MS = 100;
1426
1500
  var readAuthJson = async (path) => {
1427
1501
  try {
@@ -1431,6 +1505,22 @@ var readAuthJson = async (path) => {
1431
1505
  return {};
1432
1506
  }
1433
1507
  };
1508
+ var readAllAuthFiles = async (dir) => {
1509
+ const { readdir } = await import('fs/promises');
1510
+ const merged = {};
1511
+ try {
1512
+ const files = await readdir(dir);
1513
+ for (const file of files.filter(isAuthFile)) {
1514
+ const data = await readAuthJson(join(dir, file));
1515
+ for (const [provider, entry] of Object.entries(data)) {
1516
+ const key = `${provider}:${file}`;
1517
+ merged[key] = entry;
1518
+ }
1519
+ }
1520
+ } catch {
1521
+ }
1522
+ return merged;
1523
+ };
1434
1524
  var toCredential = (entry) => {
1435
1525
  if (entry.type === "oauth" || entry.refresh !== void 0 && entry.access !== void 0) {
1436
1526
  return {
@@ -1452,22 +1542,23 @@ var entriesEqual = (a, b) => {
1452
1542
  return JSON.stringify(a) === JSON.stringify(b);
1453
1543
  };
1454
1544
  var createAuthWatcher = (options) => {
1455
- const authPath = options?.authJsonPath ?? AUTH_JSON_PATH;
1545
+ const authDir = options?.authJsonPath ? dirname(options.authJsonPath) : AUTH_DIR;
1456
1546
  const profPath = options?.profilesPath ?? PROFILES_PATH;
1457
1547
  const log = options?.logger?.child({ component: "profile-watcher" });
1458
1548
  let lastKnownAuth = {};
1459
1549
  let abortController = null;
1460
1550
  let debounceTimer = null;
1461
1551
  const processChange = async () => {
1462
- const newAuth = await readAuthJson(authPath);
1552
+ const newAuth = await readAllAuthFiles(authDir);
1463
1553
  let store = await loadProfiles(profPath);
1464
1554
  let changed = false;
1465
- for (const [provider, entry] of Object.entries(newAuth)) {
1555
+ for (const [compositeKey, entry] of Object.entries(newAuth)) {
1466
1556
  if (!entry) continue;
1467
- const previousEntry = lastKnownAuth[provider];
1557
+ const provider = compositeKey.split(":")[0] ?? compositeKey;
1558
+ const previousEntry = lastKnownAuth[compositeKey];
1468
1559
  if (previousEntry !== void 0 && !entriesEqual(previousEntry, entry)) {
1469
1560
  log?.info(
1470
- { provider, action: "credential_changed" },
1561
+ { provider, source: compositeKey, action: "credential_changed" },
1471
1562
  "Credential change detected \u2014 saving previous credential"
1472
1563
  );
1473
1564
  const prevCredential = toCredential(previousEntry);
@@ -1477,7 +1568,7 @@ var createAuthWatcher = (options) => {
1477
1568
  changed = true;
1478
1569
  }
1479
1570
  } else if (previousEntry === void 0) {
1480
- log?.info({ provider, action: "new_provider" }, "New provider detected");
1571
+ log?.info({ provider, source: compositeKey, action: "new_provider" }, "New auth file detected");
1481
1572
  const credential = toCredential(entry);
1482
1573
  const newStore = addProfile(store, provider, credential);
1483
1574
  if (newStore !== store) {
@@ -1504,32 +1595,32 @@ var createAuthWatcher = (options) => {
1504
1595
  }, DEBOUNCE_MS);
1505
1596
  };
1506
1597
  const start = async () => {
1507
- const currentAuth = await readAuthJson(authPath);
1598
+ const currentAuth = await readAllAuthFiles(authDir);
1508
1599
  const currentStore = await loadProfiles(profPath);
1509
- log?.info({ providers: Object.keys(currentAuth) }, "Auth watcher started");
1600
+ log?.info({ auth_keys: Object.keys(currentAuth).length }, "Auth watcher started");
1510
1601
  if (Object.keys(currentStore).length === 0 && Object.keys(currentAuth).length > 0) {
1511
1602
  let store = {};
1512
- for (const [provider, entry] of Object.entries(currentAuth)) {
1603
+ for (const [compositeKey, entry] of Object.entries(currentAuth)) {
1513
1604
  if (!entry) continue;
1605
+ const provider = compositeKey.split(":")[0] ?? compositeKey;
1514
1606
  const credential = toCredential(entry);
1515
1607
  store = addProfile(store, provider, credential);
1516
1608
  }
1517
1609
  await saveProfiles(store, profPath);
1518
1610
  log?.info(
1519
1611
  { profiles_initialized: Object.keys(store).length },
1520
- "Initialized profiles from auth.json"
1612
+ "Initialized profiles from auth files"
1521
1613
  );
1522
1614
  }
1523
1615
  lastKnownAuth = currentAuth;
1524
1616
  abortController = new AbortController();
1525
- const parentDir = dirname(authPath);
1526
1617
  (async () => {
1527
1618
  try {
1528
- const watcher = watch(parentDir, {
1619
+ const watcher = watch(authDir, {
1529
1620
  signal: abortController.signal
1530
1621
  });
1531
1622
  for await (const event of watcher) {
1532
- if (event.filename === "auth.json" || event.filename === null) {
1623
+ if (event.filename !== null && isAuthFile(event.filename)) {
1533
1624
  onFileChange();
1534
1625
  }
1535
1626
  }
@@ -1551,7 +1642,89 @@ var createAuthWatcher = (options) => {
1551
1642
  };
1552
1643
  return { start, stop };
1553
1644
  };
1554
- var { schema: z3 } = tool;
1645
+ var credentialToAuthEntry = (cred) => {
1646
+ if (cred.type === "oauth") {
1647
+ return {
1648
+ type: "oauth",
1649
+ refresh: cred.refresh,
1650
+ access: cred.access,
1651
+ expires: cred.expires,
1652
+ accountId: cred.accountId
1653
+ };
1654
+ }
1655
+ return {
1656
+ type: "api-key",
1657
+ key: cred.key
1658
+ };
1659
+ };
1660
+ var readCurrentAuth = async (authPath) => {
1661
+ try {
1662
+ const content = await readFile(authPath, "utf-8");
1663
+ return JSON.parse(content);
1664
+ } catch {
1665
+ return void 0;
1666
+ }
1667
+ };
1668
+ var switchProfile = async (options) => {
1669
+ const profPath = options.profilesPath ?? PROFILES_PATH;
1670
+ const authPath = options.authJsonPath ?? AUTH_JSON_PATH;
1671
+ const log = options.logger?.child({ component: "profile-switcher" });
1672
+ const store = await loadProfiles(profPath);
1673
+ const target = store[options.targetProfileId];
1674
+ if (target === void 0) {
1675
+ log?.warn({ profileId: options.targetProfileId }, "Profile not found");
1676
+ return { success: false, to: options.targetProfileId, reason: "profile_not_found" };
1677
+ }
1678
+ const currentAuth = await readCurrentAuth(authPath);
1679
+ const currentAccountId = currentAuth?.[target.provider] ? currentAuth[target.provider].accountId : void 0;
1680
+ const newAuth = {};
1681
+ newAuth[target.provider] = credentialToAuthEntry(target.credentials);
1682
+ const dir = dirname(authPath);
1683
+ await mkdir(dir, { recursive: true });
1684
+ const tmpPath = `${authPath}.tmp`;
1685
+ await writeFile(tmpPath, JSON.stringify(newAuth, null, 2), "utf-8");
1686
+ await rename(tmpPath, authPath);
1687
+ log?.info(
1688
+ {
1689
+ from: currentAccountId,
1690
+ to: target.credentials.type === "oauth" ? target.credentials.accountId : "(api-key)",
1691
+ profileId: target.id,
1692
+ provider: target.provider
1693
+ },
1694
+ "Switched active profile"
1695
+ );
1696
+ return {
1697
+ success: true,
1698
+ from: currentAccountId ?? void 0,
1699
+ to: target.id
1700
+ };
1701
+ };
1702
+ var switchToNextProfile = async (options) => {
1703
+ const profPath = options.profilesPath ?? PROFILES_PATH;
1704
+ const log = options.logger?.child({ component: "profile-switcher" });
1705
+ const store = await loadProfiles(profPath);
1706
+ const excludeSet = new Set(options.excludeProfileIds ?? []);
1707
+ if (options.currentProfileId) {
1708
+ excludeSet.add(options.currentProfileId);
1709
+ }
1710
+ const candidates = Object.values(store).filter((p) => p.provider === options.provider && !excludeSet.has(p.id)).sort((a, b) => new Date(a.created).getTime() - new Date(b.created).getTime());
1711
+ if (candidates.length === 0) {
1712
+ log?.warn({ provider: options.provider, excluded: [...excludeSet] }, "No available profiles to switch to");
1713
+ return {
1714
+ success: false,
1715
+ to: options.currentProfileId ?? "none",
1716
+ reason: "no_candidates"
1717
+ };
1718
+ }
1719
+ const next = candidates[0];
1720
+ return switchProfile({
1721
+ targetProfileId: next.id,
1722
+ profilesPath: profPath,
1723
+ authJsonPath: options.authJsonPath,
1724
+ logger: options.logger
1725
+ });
1726
+ };
1727
+ var { schema: z4 } = tool;
1555
1728
  var createProfileTools = (options) => ({
1556
1729
  profilesList: tool({
1557
1730
  description: "List all saved auth profiles with provider, type, and creation date.",
@@ -1570,7 +1743,7 @@ var createProfileTools = (options) => ({
1570
1743
  }),
1571
1744
  profilesRemove: tool({
1572
1745
  description: "Remove a saved auth profile by ID.",
1573
- args: { id: z3.string().min(1) },
1746
+ args: { id: z4.string().min(1) },
1574
1747
  async execute(args) {
1575
1748
  const store = await loadProfiles(options?.profilesPath);
1576
1749
  const { store: newStore, removed } = removeProfile(store, args.id);
@@ -1587,4 +1760,4 @@ var createProfileTools = (options) => ({
1587
1760
  })
1588
1761
  });
1589
1762
 
1590
- export { ADMISSION_RESULTS, BackoffConfigSchema, ConfigValidationError, DEFAULT_ALPHA, DEFAULT_BACKOFF_BASE_MS, DEFAULT_BACKOFF_JITTER, DEFAULT_BACKOFF_MAX_MS, DEFAULT_BACKOFF_MULTIPLIER, DEFAULT_BACKOFF_PARAMS, DEFAULT_FAILOVER_BUDGET, DEFAULT_RETRY, DEFAULT_RETRY_BUDGET, DEFAULT_TIMEOUT_MS, DualBreaker, EXCLUSION_REASONS, ErrorClassSchema, INITIAL_HEALTH_SCORE, REDACT_PATHS, SwitcherConfigSchema, TARGET_STATES, TargetConfigSchema, TargetRegistry, addProfile, applyConfigDiff, checkHardRejects, computeBackoffMs, computeConfigDiff, computeCooldownMs, computeScore, createAdmissionController, createAuditLogger, createAuthWatcher, createCircuitBreaker, createConcurrencyTracker, createCooldownManager, createFailoverOrchestrator, createLogSubscriber, createProfileTools, createRegistry, createRequestLogger, createRequestTraceBuffer, createRetryPolicy, createRoutingEventBus, disableTarget, discoverTargets, discoverTargetsFromProfiles, drainTarget, generateCorrelationId, getExclusionReason, getTargetStateTransition, inspectRequest, isRetryable, listProfiles, listTargets, loadProfiles, nextProfileId, normalizeLatency, pauseTarget, reloadConfig, removeProfile, resumeTarget, saveProfiles, selectTarget, updateHealthScore, updateLatencyEma, validateConfig };
1763
+ export { ADMISSION_RESULTS, BackoffConfigSchema, ConfigValidationError, DEFAULT_ALPHA, DEFAULT_BACKOFF_BASE_MS, DEFAULT_BACKOFF_JITTER, DEFAULT_BACKOFF_MAX_MS, DEFAULT_BACKOFF_MULTIPLIER, DEFAULT_BACKOFF_PARAMS, DEFAULT_FAILOVER_BUDGET, DEFAULT_RETRY, DEFAULT_RETRY_BUDGET, DEFAULT_TIMEOUT_MS, DualBreaker, EXCLUSION_REASONS, ErrorClassSchema, INITIAL_HEALTH_SCORE, REDACT_PATHS, SwitcherConfigSchema, TARGET_STATES, TargetConfigSchema, TargetRegistry, addProfile, applyConfigDiff, checkHardRejects, computeBackoffMs, computeConfigDiff, computeCooldownMs, computeScore, createAdmissionController, createAuditLogger, createAuthWatcher, createCircuitBreaker, createConcurrencyTracker, createCooldownManager, createFailoverOrchestrator, createLogSubscriber, createOperatorTools, createProfileTools, createRegistry, createRequestLogger, createRequestTraceBuffer, createRetryPolicy, createRoutingEventBus, disableTarget, discoverTargets, discoverTargetsFromProfiles, drainTarget, generateCorrelationId, getExclusionReason, getTargetStateTransition, inspectRequest, isRetryable, listProfiles, listTargets, loadProfiles, nextProfileId, normalizeLatency, pauseTarget, reloadConfig, removeProfile, resumeTarget, saveProfiles, selectTarget, switchProfile, switchToNextProfile, updateHealthScore, updateLatencyEma, validateConfig };