124 lines
6.9 KiB
Text
124 lines
6.9 KiB
Text
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||
|
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||
|
|
|
||
|
|
export module Crafter.Review:Seats;
|
||
|
|
import std;
|
||
|
|
import :Json;
|
||
|
|
|
||
|
|
// How much usage each Claude seat has left: the dotenv loader, the one-word
|
||
|
|
// probe, the window maths, the ranking and the report. The seat pool
|
||
|
|
// (:SeatPool) places on these; `crafter-review seats` prints the report (its
|
||
|
|
// glue, which needs the ledger and the board, is in :Driver).
|
||
|
|
//
|
||
|
|
// Seats are CLAUDE_CODE_OAUTH_TOKEN1..6 by fixed name (the legacy
|
||
|
|
// CLAUDE_CODE_OAUTH_TOKEN is seat 1 when TOKEN1 is empty), API-credit seats
|
||
|
|
// ANTHROPIC_API_KEY1..6, OpenRouter seats OPENROUTER_KEY[1..6]; never printed:
|
||
|
|
// the table and the JSON carry indices and percentages only. A probe result
|
||
|
|
// and a seat are JSON: the dump and --json show them.
|
||
|
|
export namespace Crafter::Review::Seats {
|
||
|
|
constexpr std::int32_t MaxSeats = 6;
|
||
|
|
constexpr std::string_view ProbeModel = "haiku";
|
||
|
|
// The Fable weekly limit is a window of its own, reported ONLY in the
|
||
|
|
// rate_limit_event of a Fable request (verified 2026-09-08). The CLI has
|
||
|
|
// named it seven_day_overage_included and seven_day_fable; either lands
|
||
|
|
// under the canonical key.
|
||
|
|
constexpr std::string_view FableProbeModel = "claude-fable-5-1";
|
||
|
|
constexpr std::string_view FableWindow = "seven_day_overage_included";
|
||
|
|
constexpr double ProbeTimeoutS = 30;
|
||
|
|
// Fixed rules, not knobs: utilisation is rounded to RankQuantum before
|
||
|
|
// ranking, and a seat whose 7-day window is at or above LongWindowCutoff is
|
||
|
|
// out regardless — its reset is days away. The Fable window is exempt here.
|
||
|
|
constexpr std::int32_t RankQuantum = 5;
|
||
|
|
constexpr std::int32_t LongWindowCutoff = 95;
|
||
|
|
|
||
|
|
// JavaScript's Math.round: halves go up, never to even.
|
||
|
|
double JsRound(double x);
|
||
|
|
// A number the way a JS template prints it: 5 not 5.0, 62.5 as is.
|
||
|
|
std::string Fmt(const Json& value);
|
||
|
|
|
||
|
|
// {maxUtil, lensCostPct, waitMaxMin, policy, handoverPct}: the placement
|
||
|
|
// knobs; garbage leaves the default.
|
||
|
|
Json DefaultConfig();
|
||
|
|
Json ConfigFromEnv(const std::map<std::string, std::string>& env);
|
||
|
|
std::vector<std::pair<std::int32_t, std::string>> SeatTokensFromEnv(const std::map<std::string, std::string>& env);
|
||
|
|
std::vector<std::pair<std::int32_t, std::string>> ApiKeysFromEnv(const std::map<std::string, std::string>& env);
|
||
|
|
std::vector<std::pair<std::int32_t, std::string>> OpenRouterKeysFromEnv(const std::map<std::string, std::string>& env);
|
||
|
|
// Only the seat lines of a dotenv file; `export` and quotes stripped,
|
||
|
|
// everything else ignored.
|
||
|
|
std::map<std::string, std::string> SeatTokensFromEnvFile(std::string_view text);
|
||
|
|
|
||
|
|
// The CLI's fraction as a percent, past 100 for a seat over its limit.
|
||
|
|
Json Pct(const Json& utilization);
|
||
|
|
// {status, rateLimitType, resetsAt, windows, overageReason}, or null.
|
||
|
|
Json ParseRateLimitInfo(const Json& raw);
|
||
|
|
// The last result line, the last rate_limit_event and the session id.
|
||
|
|
Json ParseStream(std::string_view text);
|
||
|
|
|
||
|
|
std::vector<std::string> ProbeArgs(std::string_view model = ProbeModel);
|
||
|
|
// The probe child's whole environment: one token under the name the CLI
|
||
|
|
// reads, an isolated config dir, thinking off, no nonessential traffic, no
|
||
|
|
// updater, and none of the other pool tokens.
|
||
|
|
std::map<std::string, std::string> ProbeEnv(std::string_view token, const std::string& configDir, const std::map<std::string, std::string>& base);
|
||
|
|
// The CLI's own login: no token or API key from the environment.
|
||
|
|
std::map<std::string, std::string> LocalEnv(const std::map<std::string, std::string>& base);
|
||
|
|
// The key's approval in the config dir's .claude.json, once.
|
||
|
|
std::filesystem::path WriteApiKeyApproval(const std::filesystem::path& configDir, std::string_view key);
|
||
|
|
std::map<std::string, std::string> ApiEnv(std::string_view key, const std::string& configDir, const std::map<std::string, std::string>& base);
|
||
|
|
bool LooksLikeCreditExhaustion(const Json& result, std::string_view text);
|
||
|
|
bool LooksLikeAuthFailure(const Json& result, std::string_view text);
|
||
|
|
// One line, at most 160 characters, the seat's own secrets and anything
|
||
|
|
// token-shaped redacted.
|
||
|
|
std::string ClipReason(std::string_view text, std::span<const std::string> secrets = {});
|
||
|
|
|
||
|
|
struct ProbeOptions {
|
||
|
|
std::string model = std::string(ProbeModel);
|
||
|
|
double timeoutS = ProbeTimeoutS;
|
||
|
|
std::string claudeBin;
|
||
|
|
bool local = false;
|
||
|
|
bool api = false;
|
||
|
|
};
|
||
|
|
// One probe of one seat: {seat, probedWith, windows, state: ok | limited |
|
||
|
|
// unknown | invalid | out, util, binding, resetsAt, error, costUsd}.
|
||
|
|
Json ProbeSeat(std::int32_t seat, const std::string& token, const ProbeOptions& options);
|
||
|
|
|
||
|
|
double Quantize(double util);
|
||
|
|
// The most-used window and its utilisation, raw.
|
||
|
|
std::pair<Json, std::string> BindingWindow(const Json& windows);
|
||
|
|
// Quantised for ranking.
|
||
|
|
std::pair<Json, std::string> SeatUtil(const Json& windows);
|
||
|
|
std::string ModelFamily(std::string_view model);
|
||
|
|
bool IsFableWindow(std::string_view key);
|
||
|
|
const Json* FableWindowOf(const Json& seat);
|
||
|
|
bool LongWindowsOk(const Json& seat);
|
||
|
|
bool IsEligible(const Json& seat, const Json& config);
|
||
|
|
bool FableOpen(const Json& seat, const Json& config);
|
||
|
|
Json PickNext(const std::vector<Json>& seats, const Json& config);
|
||
|
|
bool NeedsHaiku(const Json& seat);
|
||
|
|
std::string FableState(const Json& fable);
|
||
|
|
Json MergeFableProbe(Json base, const Json& fable);
|
||
|
|
|
||
|
|
// Where the CLI is: CLAUDE_BIN / REVIEW_GATES_CLAUDE_BIN, then PATH, then
|
||
|
|
// the editor extensions' bundled binary (newest first), then the
|
||
|
|
// standalone installs. The bare name when nothing is found.
|
||
|
|
std::string FindClaudeBin(const std::map<std::string, std::string>& env, const std::string& home);
|
||
|
|
std::string FindClaudeBin();
|
||
|
|
|
||
|
|
// The report's zone: $TZ, else /etc/localtime. A zone by name, as
|
||
|
|
// zoneinfo reads it (this libc++ has no tzdb, so the C library converts,
|
||
|
|
// with TZ set to the name); an empty name is the process's own zone.
|
||
|
|
struct Zone {
|
||
|
|
std::string name;
|
||
|
|
std::string label;
|
||
|
|
};
|
||
|
|
Zone LocalZone();
|
||
|
|
Zone ZoneNamed(std::string_view name);
|
||
|
|
// "02:40 (in 1h 50m)"; more than a day out with its date: "Wed 16 Sept,
|
||
|
|
// 11:00 (in 6d 10h)".
|
||
|
|
std::string FormatWhen(double epoch, double now, const Zone& zone);
|
||
|
|
std::string RenderTable(const std::vector<std::vector<std::string>>& rows);
|
||
|
|
std::string RenderStatusReport(const std::vector<Json>& seats, const Json& config, double now, const Zone& zone, const Json& board = nullptr,
|
||
|
|
const std::map<std::int32_t, std::string>& keys = {});
|
||
|
|
std::string RenderApiReport(const std::vector<Json>& apiSeats, const Json& apiConfig, const Json& board = nullptr,
|
||
|
|
const std::map<std::int32_t, std::string>& keys = {});
|
||
|
|
std::string RenderDrivers(const Json& board, double now, const Zone& zone);
|
||
|
|
}
|