switchroom 0.11.0 → 0.11.1

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.
@@ -16105,7 +16105,7 @@ function decodeResponse(line) {
16105
16105
  }
16106
16106
  return ResponseSchema.parse(parsed);
16107
16107
  }
16108
- var MAX_FRAME_BYTES, PROTOCOL_VERSION = 1, ProviderNameSchema, GetCredentialsRequestSchema, ListStateRequestSchema, SetActiveRequestSchema, MarkExhaustedRequestSchema, RefreshAccountRequestSchema, AnthropicCredentialsSchema, GoogleCredentialsSchema, ProviderCredentialsSchema, AddAccountRequestSchema, RmAccountRequestSchema, SetOverrideRequestSchema, ListGoogleAccountsRequestSchema, RequestSchema, GetCredentialsDataSchema, AccountStateSchema, AgentStateSchema, ConsumerStateSchema, ListStateDataSchema, SetActiveDataSchema, MarkExhaustedDataSchema, RefreshAccountDataSchema, AddAccountDataSchema, RmAccountDataSchema, SetOverrideDataSchema, GoogleAccountStateSchema, ListGoogleAccountsDataSchema, ErrorBodySchema, SuccessResponseSchema, ErrorResponseSchema, ResponseSchema;
16108
+ var MAX_FRAME_BYTES, PROTOCOL_VERSION = 1, ProviderNameSchema, GetCredentialsRequestSchema, ListStateRequestSchema, SetActiveRequestSchema, MarkExhaustedRequestSchema, RefreshAccountRequestSchema, AnthropicCredentialsSchema, GoogleCredentialsSchema, ProviderCredentialsSchema, AddAccountRequestSchema, RmAccountRequestSchema, SetOverrideRequestSchema, ListGoogleAccountsRequestSchema, ProbeQuotaRequestSchema, RequestSchema, GetCredentialsDataSchema, AccountStateSchema, AgentStateSchema, ConsumerStateSchema, ListStateDataSchema, SetActiveDataSchema, MarkExhaustedDataSchema, RefreshAccountDataSchema, AddAccountDataSchema, RmAccountDataSchema, SetOverrideDataSchema, GoogleAccountStateSchema, ListGoogleAccountsDataSchema, ErrorBodySchema, SuccessResponseSchema, ErrorResponseSchema, ResponseSchema;
16109
16109
  var init_protocol = __esm(() => {
16110
16110
  init_zod();
16111
16111
  MAX_FRAME_BYTES = 64 * 1024;
@@ -16194,6 +16194,13 @@ var init_protocol = __esm(() => {
16194
16194
  op: exports_external.literal("list-google-accounts"),
16195
16195
  id: exports_external.string().min(1)
16196
16196
  });
16197
+ ProbeQuotaRequestSchema = exports_external.object({
16198
+ v: exports_external.literal(PROTOCOL_VERSION),
16199
+ op: exports_external.literal("probe-quota"),
16200
+ id: exports_external.string().min(1),
16201
+ accounts: exports_external.array(exports_external.string().min(1)).min(1).max(32),
16202
+ timeoutMs: exports_external.number().int().positive().max(60000).optional()
16203
+ });
16197
16204
  RequestSchema = exports_external.discriminatedUnion("op", [
16198
16205
  GetCredentialsRequestSchema,
16199
16206
  ListStateRequestSchema,
@@ -16203,7 +16210,8 @@ var init_protocol = __esm(() => {
16203
16210
  AddAccountRequestSchema,
16204
16211
  RmAccountRequestSchema,
16205
16212
  SetOverrideRequestSchema,
16206
- ListGoogleAccountsRequestSchema
16213
+ ListGoogleAccountsRequestSchema,
16214
+ ProbeQuotaRequestSchema
16207
16215
  ]);
16208
16216
  GetCredentialsDataSchema = exports_external.object({
16209
16217
  account: exports_external.string(),
@@ -16382,6 +16390,16 @@ class AuthBrokerClient {
16382
16390
  });
16383
16391
  return data;
16384
16392
  }
16393
+ async probeQuota(accounts, timeoutMs) {
16394
+ const data = await this.send({
16395
+ v: PROTOCOL_VERSION,
16396
+ id: randomUUID3(),
16397
+ op: "probe-quota",
16398
+ accounts: [...accounts],
16399
+ ...timeoutMs !== undefined ? { timeoutMs } : {}
16400
+ });
16401
+ return data;
16402
+ }
16385
16403
  async setActive(account) {
16386
16404
  const data = await this.send({
16387
16405
  v: PROTOCOL_VERSION,
@@ -23889,7 +23907,7 @@ var init_schema = __esm(() => {
23889
23907
  monthly_budget_usd: exports_external.number().positive().optional().describe("Monthly USD spend budget. If unset, the greeting shows raw usage only.")
23890
23908
  });
23891
23909
  HostControlConfigSchema = exports_external.object({
23892
- enabled: exports_external.boolean().optional().describe("Opt-in to the host-control daemon. Default: false. " + "When true, the compose generator emits per-agent bind mounts " + "at `~/.switchroom/hostd/<name>/sock` for every admin-flagged " + "agent. Install the daemon with `switchroom hostd install` \u2014 " + "it runs as a docker container in its own compose project " + "(`switchroom-hostd`), separate from the agent fleet's compose " + "project so `up -d --remove-orphans` cycles of the fleet " + "can't recreate the daemon mid-RPC. See RFC C \u00a75.1. " + "Since Phase 2 (#1175 PR \u03b3) the gateway's /restart, /new, /reset, " + "and /update apply slash-commands automatically dispatch through " + "hostd when enabled \u2014 replacing the in-container " + "`spawnSwitchroomDetached` shellout that requires docker access. " + "Set enabled: true on docker-mode installs to make those verbs work " + "(they otherwise fail because the agent container has no docker " + "binary/socket).")
23910
+ enabled: exports_external.boolean().default(true).describe("Whether the host-control daemon is in use. Default: true (since " + "RFC C Phase 2 default-flip \u2014 the gateway's /restart, /new, /reset, " + "and /update apply slash-commands all dispatch through hostd, and " + "without it those verbs fail on docker-mode installs because the " + "agent container has no docker binary/socket). " + "When true, the compose generator emits per-agent bind mounts " + "at `~/.switchroom/hostd/<name>/sock` for every admin-flagged " + "agent. Install the daemon with `switchroom hostd install` \u2014 " + "it runs as a docker container in its own compose project " + "(`switchroom-hostd`), separate from the agent fleet's compose " + "project so `up -d --remove-orphans` cycles of the fleet " + "can't recreate the daemon mid-RPC. See RFC C \u00a75.1. " + "Set enabled: false only on legacy systemd-mode installs that " + "still rely on the in-container `spawnSwitchroomDetached` " + "shellout (removal is tracked as RFC C Phase 3).")
23893
23911
  });
23894
23912
  SwitchroomConfigSchema = exports_external.object({
23895
23913
  switchroom: exports_external.object({
@@ -23915,7 +23933,7 @@ var init_schema = __esm(() => {
23915
23933
  drive: GoogleWorkspaceConfigSchema.describe("RFC D legacy key \u2014 use `google_workspace:` instead. Optional Google " + "Workspace onboarding configuration. When set, supplies Google OAuth " + "client credentials, the approver allowlist for `switchroom drive " + "connect`, and the optional tier knob. Env vars " + "(SWITCHROOM_GOOGLE_CLIENT_ID, SWITCHROOM_GOOGLE_CLIENT_SECRET, " + "SWITCHROOM_APPROVER_USER_ID) take precedence over this block when " + "set, preserving back-compat with the env-only flow shipped in #766."),
23916
23934
  google_workspace: GoogleWorkspaceConfigSchema.describe("RFC G canonical key. Top-level Google Workspace configuration \u2014 " + "OAuth client credentials, approver allowlist, and tier knob (`core` " + "| `extended` | `complete`, default `core`). Mutually exclusive with " + "`drive:` at the top level (loader fails fast if both are set)."),
23917
23935
  quota: QuotaConfigSchema.optional().describe("Optional weekly/monthly USD spend budgets rendered in the session " + "greeting. Usage is read from ccusage at runtime; no network calls."),
23918
- host_control: HostControlConfigSchema.optional().describe("Optional host-control daemon configuration. See RFC C " + "(docs/rfcs/host-control-daemon.md) and the field-level help on " + "`enabled` for the Phase 1 scope."),
23936
+ host_control: HostControlConfigSchema.default({}).describe("Host-control daemon configuration. Defaults to enabled=true since " + "RFC C Phase 2 (docs/rfcs/host-control-daemon.md). Omit the block " + "to accept defaults; set `enabled: false` only on legacy systemd-" + "mode installs (removal tracked as RFC C Phase 3)."),
23919
23937
  google_accounts: exports_external.record(exports_external.string().regex(/^[^@\s:]+@[^@\s:]+\.[^@\s:]+$/, {
23920
23938
  message: "Account key must be a Google account email like 'alice@example.com' (colons not allowed)"
23921
23939
  }).transform((v) => v.trim().toLowerCase()), exports_external.object({
@@ -24901,7 +24919,7 @@ function isDockerRuntime() {
24901
24919
  import * as net3 from "node:net";
24902
24920
  import * as fs from "node:fs";
24903
24921
  import { homedir as homedir7 } from "node:os";
24904
- import { join as join17 } from "node:path";
24922
+ import { join as join16 } from "node:path";
24905
24923
  function defaultBrokerSocketPath() {
24906
24924
  if (isDockerRuntime()) {
24907
24925
  if (fs.existsSync(OPERATOR_SOCKET_PATH))
@@ -24911,7 +24929,7 @@ function defaultBrokerSocketPath() {
24911
24929
  return LEGACY_SOCKET_PATH;
24912
24930
  }
24913
24931
  function vaultTokenFilePath(agentSlug) {
24914
- return join17(homedir7(), ".switchroom", "agents", agentSlug, ".vault-token");
24932
+ return join16(homedir7(), ".switchroom", "agents", agentSlug, ".vault-token");
24915
24933
  }
24916
24934
  function readVaultTokenFile(agentSlug) {
24917
24935
  const filePath = vaultTokenFilePath(agentSlug);
@@ -25038,8 +25056,8 @@ var DEFAULT_TIMEOUT_MS3 = 2000, LEGACY_SOCKET_PATH, OPERATOR_SOCKET_PATH;
25038
25056
  var init_client2 = __esm(() => {
25039
25057
  init_protocol2();
25040
25058
  init_peercred();
25041
- LEGACY_SOCKET_PATH = join17(homedir7(), ".switchroom", "vault-broker.sock");
25042
- OPERATOR_SOCKET_PATH = join17(homedir7(), ".switchroom", "broker-operator", "sock");
25059
+ LEGACY_SOCKET_PATH = join16(homedir7(), ".switchroom", "vault-broker.sock");
25060
+ OPERATOR_SOCKET_PATH = join16(homedir7(), ".switchroom", "broker-operator", "sock");
25043
25061
  });
25044
25062
 
25045
25063
  // ../src/drive/deep-links.ts
@@ -38161,7 +38179,8 @@ function createAuthBrokerClient() {
38161
38179
  setActive: (label) => broker.setActive(label),
38162
38180
  rmAccount: (label) => broker.rmAccount(label),
38163
38181
  refreshAccount: (label) => broker.refreshAccount(label),
38164
- setOverride: (agent, account) => broker.setOverride(agent, account)
38182
+ setOverride: (agent, account) => broker.setOverride(agent, account),
38183
+ probeQuota: (accounts, timeoutMs) => broker.probeQuota(accounts, timeoutMs)
38165
38184
  };
38166
38185
  return { client: client3, close: () => broker.close() };
38167
38186
  }
@@ -38272,6 +38291,16 @@ class AuthBrokerClient2 {
38272
38291
  });
38273
38292
  return data;
38274
38293
  }
38294
+ async probeQuota(accounts, timeoutMs) {
38295
+ const data = await this.send({
38296
+ v: PROTOCOL_VERSION,
38297
+ id: randomUUID4(),
38298
+ op: "probe-quota",
38299
+ accounts: [...accounts],
38300
+ ...timeoutMs !== undefined ? { timeoutMs } : {}
38301
+ });
38302
+ return data;
38303
+ }
38275
38304
  async setActive(account) {
38276
38305
  const data = await this.send({
38277
38306
  v: PROTOCOL_VERSION,
@@ -39195,194 +39224,6 @@ function pctSummary(q) {
39195
39224
  return `${Math.round(q.fiveHourUtilizationPct)}% / ${Math.round(q.sevenDayUtilizationPct)}%`;
39196
39225
  }
39197
39226
 
39198
- // quota-check.ts
39199
- import { readFileSync as readFileSync9, existsSync as existsSync13 } from "fs";
39200
- import { join as join13 } from "path";
39201
- var OAUTH_BETA2 = "oauth-2025-04-20";
39202
- var DEFAULT_USER_AGENT2 = "claude-cli/1.0.0 (external, cli)";
39203
- var DEFAULT_PROBE_MODEL2 = "claude-haiku-4-5-20251001";
39204
- function readOauthToken2(claudeConfigDir) {
39205
- const tokenFile = join13(claudeConfigDir, ".oauth-token");
39206
- if (!existsSync13(tokenFile))
39207
- return null;
39208
- try {
39209
- const raw = readFileSync9(tokenFile, "utf-8").trim();
39210
- return raw.length > 0 ? raw : null;
39211
- } catch {
39212
- return null;
39213
- }
39214
- }
39215
- function parseFloatHeader2(headers, name) {
39216
- const v = headers.get(name);
39217
- if (v == null || v.trim().length === 0)
39218
- return null;
39219
- const n = Number(v);
39220
- return Number.isFinite(n) ? n : null;
39221
- }
39222
- function parseEpochHeader2(headers, name) {
39223
- const v = headers.get(name);
39224
- if (v == null)
39225
- return null;
39226
- const n = Number(v);
39227
- if (!Number.isFinite(n) || n <= 0)
39228
- return null;
39229
- return new Date(n * 1000);
39230
- }
39231
- function parseQuotaHeaders2(headers) {
39232
- const fiveHour = parseFloatHeader2(headers, "anthropic-ratelimit-unified-5h-utilization");
39233
- const sevenDay = parseFloatHeader2(headers, "anthropic-ratelimit-unified-7d-utilization");
39234
- if (fiveHour == null && sevenDay == null) {
39235
- return {
39236
- ok: false,
39237
- reason: "no unified rate-limit headers in response (API token, not OAuth?)"
39238
- };
39239
- }
39240
- return {
39241
- ok: true,
39242
- data: {
39243
- fiveHourUtilizationPct: (fiveHour ?? 0) * 100,
39244
- sevenDayUtilizationPct: (sevenDay ?? 0) * 100,
39245
- fiveHourResetAt: parseEpochHeader2(headers, "anthropic-ratelimit-unified-5h-reset"),
39246
- sevenDayResetAt: parseEpochHeader2(headers, "anthropic-ratelimit-unified-7d-reset"),
39247
- representativeClaim: headers.get("anthropic-ratelimit-unified-representative-claim"),
39248
- overageStatus: headers.get("anthropic-ratelimit-unified-overage-status"),
39249
- overageDisabledReason: headers.get("anthropic-ratelimit-unified-overage-disabled-reason")
39250
- }
39251
- };
39252
- }
39253
- async function fetchQuota2(opts) {
39254
- let token;
39255
- if (opts.accessToken && opts.claudeConfigDir) {
39256
- return {
39257
- ok: false,
39258
- reason: "pass only one of `accessToken` or `claudeConfigDir`, not both"
39259
- };
39260
- }
39261
- if (opts.accessToken) {
39262
- token = opts.accessToken.trim().length > 0 ? opts.accessToken : null;
39263
- } else if (opts.claudeConfigDir) {
39264
- token = readOauthToken2(opts.claudeConfigDir);
39265
- } else {
39266
- return {
39267
- ok: false,
39268
- reason: "fetchQuota requires `accessToken` or `claudeConfigDir`"
39269
- };
39270
- }
39271
- if (!token) {
39272
- return { ok: false, reason: "no OAuth token at .oauth-token" };
39273
- }
39274
- const controller = new AbortController;
39275
- const timeout = setTimeout(() => controller.abort(), opts.timeoutMs ?? 1e4);
39276
- const fetchFn = opts.fetchImpl ?? fetch;
39277
- let resp;
39278
- try {
39279
- resp = await fetchFn("https://api.anthropic.com/v1/messages", {
39280
- method: "POST",
39281
- headers: {
39282
- "anthropic-version": "2023-06-01",
39283
- "anthropic-beta": OAUTH_BETA2,
39284
- authorization: `Bearer ${token}`,
39285
- "x-app": "cli",
39286
- "user-agent": DEFAULT_USER_AGENT2,
39287
- "content-type": "application/json"
39288
- },
39289
- body: JSON.stringify({
39290
- model: opts.model ?? DEFAULT_PROBE_MODEL2,
39291
- max_tokens: 1,
39292
- messages: [{ role: "user", content: "hi" }]
39293
- }),
39294
- signal: controller.signal
39295
- });
39296
- } catch (err) {
39297
- const msg = err?.message ?? String(err);
39298
- return { ok: false, reason: `request failed: ${msg}` };
39299
- } finally {
39300
- clearTimeout(timeout);
39301
- }
39302
- if (resp.status === 401 || resp.status === 403) {
39303
- return { ok: false, reason: `auth rejected (HTTP ${resp.status})` };
39304
- }
39305
- const parsed = parseQuotaHeaders2(resp.headers);
39306
- if (!parsed.ok && resp.status >= 400) {
39307
- return { ok: false, reason: `HTTP ${resp.status}, ${parsed.reason}` };
39308
- }
39309
- return parsed;
39310
- }
39311
- function formatResetRelative2(target, now = new Date) {
39312
- if (!target)
39313
- return "\u2014";
39314
- const deltaMs = target.getTime() - now.getTime();
39315
- if (deltaMs <= 0)
39316
- return "resets now";
39317
- const totalMin = Math.round(deltaMs / 60000);
39318
- if (totalMin < 60)
39319
- return `resets in ${totalMin}m`;
39320
- const hours = Math.floor(totalMin / 60);
39321
- const mins = totalMin % 60;
39322
- if (hours < 24)
39323
- return mins > 0 ? `resets in ${hours}h ${mins}m` : `resets in ${hours}h`;
39324
- const days = Math.floor(hours / 24);
39325
- const remH = hours % 24;
39326
- return remH > 0 ? `resets in ${days}d ${remH}h` : `resets in ${days}d`;
39327
- }
39328
- function formatQuotaBlock(q, now = new Date) {
39329
- const lines = [];
39330
- lines.push("<b>Claude plan quota</b>");
39331
- lines.push("");
39332
- lines.push(`<b>5h window</b> ${Math.round(q.fiveHourUtilizationPct)}% \u00b7 ${formatResetRelative2(q.fiveHourResetAt, now)}`);
39333
- lines.push(`<b>7d window</b> ${Math.round(q.sevenDayUtilizationPct)}% \u00b7 ${formatResetRelative2(q.sevenDayResetAt, now)}`);
39334
- if (q.representativeClaim) {
39335
- lines.push("");
39336
- lines.push(`<i>Binding window: ${q.representativeClaim.replace(/_/g, " ")}</i>`);
39337
- }
39338
- if (q.overageStatus && q.overageStatus !== "allowed") {
39339
- const reason = q.overageDisabledReason ? ` (${q.overageDisabledReason})` : "";
39340
- lines.push(`<i>Overage: ${q.overageStatus}${reason}</i>`);
39341
- }
39342
- return lines.join(`
39343
- `);
39344
- }
39345
- function readAccountAccessToken(label, home2 = process.env.HOME ?? "/root") {
39346
- const credPath = join13(home2, ".switchroom", "accounts", label, "credentials.json");
39347
- if (!existsSync13(credPath))
39348
- return null;
39349
- try {
39350
- const raw = readFileSync9(credPath, "utf-8");
39351
- const parsed = JSON.parse(raw);
39352
- const token = parsed.claudeAiOauth?.accessToken?.trim();
39353
- return token && token.length > 0 ? token : null;
39354
- } catch {
39355
- return null;
39356
- }
39357
- }
39358
- var ACCOUNT_QUOTA_CACHE_TTL_MS2 = 5 * 60000;
39359
- var accountQuotaCache2 = new Map;
39360
- async function fetchAccountQuota(label, opts = {}) {
39361
- const now = opts.now?.() ?? Date.now();
39362
- if (!opts.force) {
39363
- const cached = accountQuotaCache2.get(label);
39364
- if (cached && now - cached.fetchedAt < ACCOUNT_QUOTA_CACHE_TTL_MS2) {
39365
- return cached.result;
39366
- }
39367
- }
39368
- const token = readAccountAccessToken(label, opts.home);
39369
- if (!token) {
39370
- const result2 = {
39371
- ok: false,
39372
- reason: "no credentials.json or accessToken for account"
39373
- };
39374
- accountQuotaCache2.set(label, { fetchedAt: now, result: result2 });
39375
- return result2;
39376
- }
39377
- const result = await fetchQuota2({
39378
- accessToken: token,
39379
- fetchImpl: opts.fetchImpl,
39380
- timeoutMs: opts.timeoutMs
39381
- });
39382
- accountQuotaCache2.set(label, { fetchedAt: now, result });
39383
- return result;
39384
- }
39385
-
39386
39227
  // gateway/restart-watchdog.ts
39387
39228
  function parseSystemdShowOutput(raw) {
39388
39229
  const lines = raw.split(/\r?\n/);
@@ -40278,8 +40119,8 @@ function isTurnFlushSafetyEnabled(env = process.env) {
40278
40119
  }
40279
40120
 
40280
40121
  // handoff-continuity.ts
40281
- import { readFileSync as readFileSync10, unlinkSync as unlinkSync2, existsSync as existsSync14, writeFileSync as writeFileSync6, renameSync as renameSync2 } from "node:fs";
40282
- import { dirname as dirname7, join as join14 } from "node:path";
40122
+ import { readFileSync as readFileSync9, unlinkSync as unlinkSync2, existsSync as existsSync13, writeFileSync as writeFileSync6, renameSync as renameSync2 } from "node:fs";
40123
+ import { dirname as dirname7, join as join13 } from "node:path";
40283
40124
  var TOPIC_DISPLAY_MAX = 117;
40284
40125
  var HANDOFF_TOPIC_FILENAME = ".handoff-topic";
40285
40126
  var LAST_TURN_SUMMARY_FILENAME = ".last-turn-summary";
@@ -40290,12 +40131,12 @@ function resolveAgentDirFromEnv() {
40290
40131
  return dirname7(state3);
40291
40132
  }
40292
40133
  function readHandoffTopic(agentDir) {
40293
- const p = join14(agentDir, HANDOFF_TOPIC_FILENAME);
40294
- if (!existsSync14(p))
40134
+ const p = join13(agentDir, HANDOFF_TOPIC_FILENAME);
40135
+ if (!existsSync13(p))
40295
40136
  return null;
40296
40137
  let raw;
40297
40138
  try {
40298
- raw = readFileSync10(p, "utf-8");
40139
+ raw = readFileSync9(p, "utf-8");
40299
40140
  } catch {
40300
40141
  return null;
40301
40142
  }
@@ -40309,12 +40150,12 @@ function readHandoffTopic(agentDir) {
40309
40150
  return topic;
40310
40151
  }
40311
40152
  function readLastTurnSummary(agentDir) {
40312
- const p = join14(agentDir, LAST_TURN_SUMMARY_FILENAME);
40313
- if (!existsSync14(p))
40153
+ const p = join13(agentDir, LAST_TURN_SUMMARY_FILENAME);
40154
+ if (!existsSync13(p))
40314
40155
  return null;
40315
40156
  let raw;
40316
40157
  try {
40317
- raw = readFileSync10(p, "utf-8");
40158
+ raw = readFileSync9(p, "utf-8");
40318
40159
  } catch {
40319
40160
  return null;
40320
40161
  }
@@ -40329,8 +40170,8 @@ function readLastTurnSummary(agentDir) {
40329
40170
  }
40330
40171
  function consumeHandoffTopic(agentDir) {
40331
40172
  const primary = readHandoffTopic(agentDir);
40332
- const primaryPath = join14(agentDir, HANDOFF_TOPIC_FILENAME);
40333
- const fallbackPath = join14(agentDir, LAST_TURN_SUMMARY_FILENAME);
40173
+ const primaryPath = join13(agentDir, HANDOFF_TOPIC_FILENAME);
40174
+ const fallbackPath = join13(agentDir, LAST_TURN_SUMMARY_FILENAME);
40334
40175
  const removeFallback = () => {
40335
40176
  try {
40336
40177
  unlinkSync2(fallbackPath);
@@ -40383,19 +40224,19 @@ function escapeMarkdownV2(s) {
40383
40224
  }
40384
40225
 
40385
40226
  // active-reactions.ts
40386
- import { readFileSync as readFileSync11, writeFileSync as writeFileSync7, renameSync as renameSync3, existsSync as existsSync15, unlinkSync as unlinkSync3 } from "node:fs";
40387
- import { join as join15 } from "node:path";
40227
+ import { readFileSync as readFileSync10, writeFileSync as writeFileSync7, renameSync as renameSync3, existsSync as existsSync14, unlinkSync as unlinkSync3 } from "node:fs";
40228
+ import { join as join14 } from "node:path";
40388
40229
  var ACTIVE_REACTIONS_FILENAME = ".active-reactions.json";
40389
40230
  function reactionsPath(agentDir) {
40390
- return join15(agentDir, ACTIVE_REACTIONS_FILENAME);
40231
+ return join14(agentDir, ACTIVE_REACTIONS_FILENAME);
40391
40232
  }
40392
40233
  function readActiveReactions(agentDir) {
40393
40234
  const p = reactionsPath(agentDir);
40394
- if (!existsSync15(p))
40235
+ if (!existsSync14(p))
40395
40236
  return [];
40396
40237
  let raw;
40397
40238
  try {
40398
- raw = readFileSync11(p, "utf-8");
40239
+ raw = readFileSync10(p, "utf-8");
40399
40240
  } catch {
40400
40241
  return [];
40401
40242
  }
@@ -40451,19 +40292,19 @@ function clearActiveReactions(agentDir) {
40451
40292
  }
40452
40293
 
40453
40294
  // active-reactions.ts
40454
- import { readFileSync as readFileSync12, writeFileSync as writeFileSync8, renameSync as renameSync4, existsSync as existsSync16, unlinkSync as unlinkSync4 } from "node:fs";
40455
- import { join as join16 } from "node:path";
40295
+ import { readFileSync as readFileSync11, writeFileSync as writeFileSync8, renameSync as renameSync4, existsSync as existsSync15, unlinkSync as unlinkSync4 } from "node:fs";
40296
+ import { join as join15 } from "node:path";
40456
40297
  var ACTIVE_REACTIONS_FILENAME2 = ".active-reactions.json";
40457
40298
  function reactionsPath2(agentDir) {
40458
- return join16(agentDir, ACTIVE_REACTIONS_FILENAME2);
40299
+ return join15(agentDir, ACTIVE_REACTIONS_FILENAME2);
40459
40300
  }
40460
40301
  function readActiveReactions2(agentDir) {
40461
40302
  const p = reactionsPath2(agentDir);
40462
- if (!existsSync16(p))
40303
+ if (!existsSync15(p))
40463
40304
  return [];
40464
40305
  let raw;
40465
40306
  try {
40466
- raw = readFileSync12(p, "utf-8");
40307
+ raw = readFileSync11(p, "utf-8");
40467
40308
  } catch {
40468
40309
  return [];
40469
40310
  }
@@ -41321,6 +41162,156 @@ async function approvalRecord(args, opts) {
41321
41162
  return r.resp.decision_id;
41322
41163
  }
41323
41164
 
41165
+ // quota-check.ts
41166
+ import { readFileSync as readFileSync13, existsSync as existsSync17 } from "fs";
41167
+ import { join as join17 } from "path";
41168
+ var OAUTH_BETA2 = "oauth-2025-04-20";
41169
+ var DEFAULT_USER_AGENT2 = "claude-cli/1.0.0 (external, cli)";
41170
+ var DEFAULT_PROBE_MODEL2 = "claude-haiku-4-5-20251001";
41171
+ function readOauthToken2(claudeConfigDir) {
41172
+ const tokenFile = join17(claudeConfigDir, ".oauth-token");
41173
+ if (!existsSync17(tokenFile))
41174
+ return null;
41175
+ try {
41176
+ const raw = readFileSync13(tokenFile, "utf-8").trim();
41177
+ return raw.length > 0 ? raw : null;
41178
+ } catch {
41179
+ return null;
41180
+ }
41181
+ }
41182
+ function parseFloatHeader2(headers, name) {
41183
+ const v = headers.get(name);
41184
+ if (v == null || v.trim().length === 0)
41185
+ return null;
41186
+ const n = Number(v);
41187
+ return Number.isFinite(n) ? n : null;
41188
+ }
41189
+ function parseEpochHeader2(headers, name) {
41190
+ const v = headers.get(name);
41191
+ if (v == null)
41192
+ return null;
41193
+ const n = Number(v);
41194
+ if (!Number.isFinite(n) || n <= 0)
41195
+ return null;
41196
+ return new Date(n * 1000);
41197
+ }
41198
+ function parseQuotaHeaders2(headers) {
41199
+ const fiveHour = parseFloatHeader2(headers, "anthropic-ratelimit-unified-5h-utilization");
41200
+ const sevenDay = parseFloatHeader2(headers, "anthropic-ratelimit-unified-7d-utilization");
41201
+ if (fiveHour == null && sevenDay == null) {
41202
+ return {
41203
+ ok: false,
41204
+ reason: "no unified rate-limit headers in response (API token, not OAuth?)"
41205
+ };
41206
+ }
41207
+ return {
41208
+ ok: true,
41209
+ data: {
41210
+ fiveHourUtilizationPct: (fiveHour ?? 0) * 100,
41211
+ sevenDayUtilizationPct: (sevenDay ?? 0) * 100,
41212
+ fiveHourResetAt: parseEpochHeader2(headers, "anthropic-ratelimit-unified-5h-reset"),
41213
+ sevenDayResetAt: parseEpochHeader2(headers, "anthropic-ratelimit-unified-7d-reset"),
41214
+ representativeClaim: headers.get("anthropic-ratelimit-unified-representative-claim"),
41215
+ overageStatus: headers.get("anthropic-ratelimit-unified-overage-status"),
41216
+ overageDisabledReason: headers.get("anthropic-ratelimit-unified-overage-disabled-reason")
41217
+ }
41218
+ };
41219
+ }
41220
+ async function fetchQuota2(opts) {
41221
+ let token;
41222
+ if (opts.accessToken && opts.claudeConfigDir) {
41223
+ return {
41224
+ ok: false,
41225
+ reason: "pass only one of `accessToken` or `claudeConfigDir`, not both"
41226
+ };
41227
+ }
41228
+ if (opts.accessToken) {
41229
+ token = opts.accessToken.trim().length > 0 ? opts.accessToken : null;
41230
+ } else if (opts.claudeConfigDir) {
41231
+ token = readOauthToken2(opts.claudeConfigDir);
41232
+ } else {
41233
+ return {
41234
+ ok: false,
41235
+ reason: "fetchQuota requires `accessToken` or `claudeConfigDir`"
41236
+ };
41237
+ }
41238
+ if (!token) {
41239
+ return { ok: false, reason: "no OAuth token at .oauth-token" };
41240
+ }
41241
+ const controller = new AbortController;
41242
+ const timeout = setTimeout(() => controller.abort(), opts.timeoutMs ?? 1e4);
41243
+ const fetchFn = opts.fetchImpl ?? fetch;
41244
+ let resp;
41245
+ try {
41246
+ resp = await fetchFn("https://api.anthropic.com/v1/messages", {
41247
+ method: "POST",
41248
+ headers: {
41249
+ "anthropic-version": "2023-06-01",
41250
+ "anthropic-beta": OAUTH_BETA2,
41251
+ authorization: `Bearer ${token}`,
41252
+ "x-app": "cli",
41253
+ "user-agent": DEFAULT_USER_AGENT2,
41254
+ "content-type": "application/json"
41255
+ },
41256
+ body: JSON.stringify({
41257
+ model: opts.model ?? DEFAULT_PROBE_MODEL2,
41258
+ max_tokens: 1,
41259
+ messages: [{ role: "user", content: "hi" }]
41260
+ }),
41261
+ signal: controller.signal
41262
+ });
41263
+ } catch (err) {
41264
+ const msg = err?.message ?? String(err);
41265
+ return { ok: false, reason: `request failed: ${msg}` };
41266
+ } finally {
41267
+ clearTimeout(timeout);
41268
+ }
41269
+ if (resp.status === 401 || resp.status === 403) {
41270
+ return { ok: false, reason: `auth rejected (HTTP ${resp.status})` };
41271
+ }
41272
+ const parsed = parseQuotaHeaders2(resp.headers);
41273
+ if (!parsed.ok && resp.status >= 400) {
41274
+ return { ok: false, reason: `HTTP ${resp.status}, ${parsed.reason}` };
41275
+ }
41276
+ return parsed;
41277
+ }
41278
+ function formatResetRelative2(target, now = new Date) {
41279
+ if (!target)
41280
+ return "\u2014";
41281
+ const deltaMs = target.getTime() - now.getTime();
41282
+ if (deltaMs <= 0)
41283
+ return "resets now";
41284
+ const totalMin = Math.round(deltaMs / 60000);
41285
+ if (totalMin < 60)
41286
+ return `resets in ${totalMin}m`;
41287
+ const hours = Math.floor(totalMin / 60);
41288
+ const mins = totalMin % 60;
41289
+ if (hours < 24)
41290
+ return mins > 0 ? `resets in ${hours}h ${mins}m` : `resets in ${hours}h`;
41291
+ const days = Math.floor(hours / 24);
41292
+ const remH = hours % 24;
41293
+ return remH > 0 ? `resets in ${days}d ${remH}h` : `resets in ${days}d`;
41294
+ }
41295
+ function formatQuotaBlock(q, now = new Date) {
41296
+ const lines = [];
41297
+ lines.push("<b>Claude plan quota</b>");
41298
+ lines.push("");
41299
+ lines.push(`<b>5h window</b> ${Math.round(q.fiveHourUtilizationPct)}% \u00b7 ${formatResetRelative2(q.fiveHourResetAt, now)}`);
41300
+ lines.push(`<b>7d window</b> ${Math.round(q.sevenDayUtilizationPct)}% \u00b7 ${formatResetRelative2(q.sevenDayResetAt, now)}`);
41301
+ if (q.representativeClaim) {
41302
+ lines.push("");
41303
+ lines.push(`<i>Binding window: ${q.representativeClaim.replace(/_/g, " ")}</i>`);
41304
+ }
41305
+ if (q.overageStatus && q.overageStatus !== "allowed") {
41306
+ const reason = q.overageDisabledReason ? ` (${q.overageDisabledReason})` : "";
41307
+ lines.push(`<i>Overage: ${q.overageStatus}${reason}</i>`);
41308
+ }
41309
+ return lines.join(`
41310
+ `);
41311
+ }
41312
+ var ACCOUNT_QUOTA_CACHE_TTL_MS2 = 5 * 60000;
41313
+ var accountQuotaCache2 = new Map;
41314
+
41324
41315
  // auto-fallback.ts
41325
41316
  var DEFAULT_FALLBACK_COOLDOWN_MS = 2 * 60000;
41326
41317
  var LOCKOUT_FILE = "auto-fallback-lockout.json";
@@ -42419,7 +42410,7 @@ function isHostdEnabled() {
42419
42410
  return _hostdEnabled;
42420
42411
  try {
42421
42412
  const cfg = loadConfig();
42422
- _hostdEnabled = cfg.host_control?.enabled === true;
42413
+ _hostdEnabled = cfg.host_control?.enabled !== false;
42423
42414
  } catch {
42424
42415
  _hostdEnabled = false;
42425
42416
  }
@@ -46241,10 +46232,10 @@ function sweepStaleTurnActiveMarker(stateDir, opts) {
46241
46232
  }
46242
46233
 
46243
46234
  // ../src/build-info.ts
46244
- var VERSION = "0.11.0";
46245
- var COMMIT_SHA = "abff20c7";
46246
- var COMMIT_DATE = "2026-05-15T16:50:03+10:00";
46247
- var LATEST_PR = 1335;
46235
+ var VERSION = "0.11.1";
46236
+ var COMMIT_SHA = "f5d84dfb";
46237
+ var COMMIT_DATE = "2026-05-15T19:01:48+10:00";
46238
+ var LATEST_PR = 1342;
46248
46239
  var COMMITS_AHEAD_OF_TAG = 0;
46249
46240
 
46250
46241
  // gateway/boot-version.ts
@@ -51962,7 +51953,11 @@ async function doFireFleetAutoFallback(triggerAgent) {
51962
51953
  return false;
51963
51954
  }
51964
51955
  const state3 = await client3.listState();
51965
- const quotas = await Promise.all(state3.accounts.map((a) => fetchAccountQuota(a.label, { force: true })));
51956
+ const probeResp = state3.accounts.length > 0 ? await client3.probeQuota(state3.accounts.map((a) => a.label)).catch(() => ({ results: [] })) : { results: [] };
51957
+ const quotas = state3.accounts.map((a) => {
51958
+ const hit = probeResp.results.find((r) => r.label === a.label);
51959
+ return hit?.result ?? { ok: false, reason: "broker returned no result for account" };
51960
+ });
51966
51961
  const tz = process.env.SWITCHROOM_TIMEZONE ?? process.env.TZ ?? "UTC";
51967
51962
  const outcome = await runFleetAutoFallback({
51968
51963
  state: state3,
@@ -52087,7 +52082,20 @@ Send <code>/auth cancel</code> to abort.`, { html: true });
52087
52082
  isAdmin: isAdmin2,
52088
52083
  client: client3,
52089
52084
  chatId,
52090
- liveQuotas: async (accounts) => Promise.all(accounts.map((a) => fetchAccountQuota(a.label, { force: true }))),
52085
+ liveQuotas: async (accounts) => {
52086
+ try {
52087
+ const { results } = await client3.probeQuota(accounts.map((a) => a.label));
52088
+ return accounts.map((a) => {
52089
+ const hit = results.find((r) => r.label === a.label);
52090
+ if (!hit)
52091
+ return { ok: false, reason: "broker returned no result for account" };
52092
+ return hit.result;
52093
+ });
52094
+ } catch (err) {
52095
+ const reason = `broker probe-quota failed: ${err?.message ?? String(err)}`;
52096
+ return accounts.map(() => ({ ok: false, reason }));
52097
+ }
52098
+ },
52091
52099
  tz: process.env.SWITCHROOM_TIMEZONE ?? process.env.TZ
52092
52100
  });
52093
52101
  if (reply.keyboard && reply.keyboard.length > 0) {
@@ -53095,7 +53103,11 @@ async function handleAuthDashboardCallback(ctx) {
53095
53103
  return;
53096
53104
  }
53097
53105
  const state3 = await client3.listState();
53098
- const quotas = await Promise.all(state3.accounts.map((a) => fetchAccountQuota(a.label, { force: true })));
53106
+ const probeResp = state3.accounts.length > 0 ? await client3.probeQuota(state3.accounts.map((a) => a.label)).catch(() => ({ results: [] })) : { results: [] };
53107
+ const quotas = state3.accounts.map((a) => {
53108
+ const hit = probeResp.results.find((r) => r.label === a.label);
53109
+ return hit?.result ?? { ok: false, reason: "broker returned no result for account" };
53110
+ });
53099
53111
  const tz = process.env.SWITCHROOM_TIMEZONE ?? process.env.TZ ?? "UTC";
53100
53112
  const { renderAuthSnapshotFormat2: renderAuthSnapshotFormat23, buildSnapshotsFromState: buildSnapshotsFromState3, buildSnapshotKeyboard: buildSnapshotKeyboard3 } = await Promise.resolve().then(() => (init_auth_snapshot_format(), exports_auth_snapshot_format));
53101
53113
  const snapshots = buildSnapshotsFromState3(state3, quotas);
@@ -53488,7 +53500,11 @@ bot.command("usage", async (ctx) => {
53488
53500
  if (client3) {
53489
53501
  const state3 = await client3.listState();
53490
53502
  if (state3.accounts.length > 0) {
53491
- const quotas = await Promise.all(state3.accounts.map((a) => fetchAccountQuota(a.label, { force: true })));
53503
+ const probeResp = await client3.probeQuota(state3.accounts.map((a) => a.label)).catch(() => ({ results: [] }));
53504
+ const quotas = state3.accounts.map((a) => {
53505
+ const hit = probeResp.results.find((r) => r.label === a.label);
53506
+ return hit?.result ?? { ok: false, reason: "broker returned no result for account" };
53507
+ });
53492
53508
  const { renderAuthSnapshotFormat2: renderAuthSnapshotFormat23, buildSnapshotsFromState: buildSnapshotsFromState3 } = await Promise.resolve().then(() => (init_auth_snapshot_format(), exports_auth_snapshot_format));
53493
53509
  const tz = process.env.SWITCHROOM_TIMEZONE ?? process.env.TZ ?? "UTC";
53494
53510
  const snapshots = buildSnapshotsFromState3(state3, quotas);
@@ -30,6 +30,8 @@ export function createAuthBrokerClient(): {
30
30
  refreshAccount: (label: string) => broker.refreshAccount(label),
31
31
  setOverride: (agent: string, account: string | null) =>
32
32
  broker.setOverride(agent, account),
33
+ probeQuota: (accounts: readonly string[], timeoutMs?: number) =>
34
+ broker.probeQuota(accounts, timeoutMs),
33
35
  }
34
36
  return { client, close: () => broker.close() }
35
37
  }