// REQ-LIMIT-1: human settings; decision 145 migration is a separate print-only read. import { readFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; import { loadSystem } from "./config.mjs"; import { systemVars } from "../../organization/src/index.mjs"; import { CliError } from "./errors.mjs"; import { orgSelector } from "./org-selector.mjs"; import { migrateLaunch } from "../../organization/src/migrate-launch.mjs"; // Row 155 (#1649, Q169): a PATCH may carry `assistants`, the assistant's vars, tracker and definition. // C2-06 M11: `replies --file LIMITS` replaces replyLimits with the file's object, at the current revision. export const LIMITS_USAGE = "mosaic limits [--business ID] [--json] [--tester] | set --file PATCH [--business ID] | " + "reset PATH... [--business ID] | replies --file LIMITS [--business ID] | migrate --file ORGANIZATION --models MAPPING [--roles-dir DIR]; " + "PATCH keys: revision, limits, models, instances, reset, assistants; LIMITS keys: requireCitation, citationsPerReply"; export const DAILY_NOTE = "Daily quotas count only this stack's own use of the account, not its use anywhere else."; // Row 125 (#1619, REQ-LIMIT-3, REQ-LIMIT-4): one line per token limit, its path in the words // `mosaic limits reset` takes. A limit no switched-on check counts reads "not enforced". export function formatTokens(tokens) { if (!Array.isArray(tokens) || tokens.length === 0) return "token limits: none\n"; const lines = tokens.map(t => { const head = `${t.path.join(" ")}: limit ${t.limit} since ${t.since}`; if (t.status === "counted") { const kinds = Object.entries(t.kinds).map(([k, n]) => `${k} ${n}`).join(", "); return `${head}, count ${t.count} (${kinds}), unknown turns ${t.unknownTurns}`; } if (t.status === "not-enforced") return `${head}, not enforced${t.reason ? `: ${t.reason}` : ""}`; return `${head}, check failed: ${t.code}${t.check ? ` (${t.check})` : ""}`; }); // Row 126 (#1620): a daily quota counts only the records this stack wrote. Said once, after the list. const daily = tokens.some(t => t.path?.[1] === "daily") ? `${DAILY_NOTE}\n` : ""; return `token limits:\n${lines.map(l => ` ${l}`).join("\n")}\n${daily}`; } export function parseLimits(args) { const out = { positional: [], flags: new Set(), values: {} }; // --org and --business are one selector in this parser's scope (rename design r2). const selector = orgSelector(() => new CliError(LIMITS_USAGE, 4)); for (let i = 0; i < args.length; i++) { const arg = args[i]; if (selector.owns(arg)) { selector.take(arg, args[i + 1]); i++; } else if (arg === "--json" || arg === "--tester") out.flags.add(arg); else if (["--file", "--models", "--roles-dir"].includes(arg)) { if (!args[i + 1] || Object.hasOwn(out.values, arg)) throw new CliError(LIMITS_USAGE, 4); out.values[arg] = args[++i]; } else if (arg.startsWith("--")) throw new CliError(LIMITS_USAGE, 4); else out.positional.push(arg); } if (selector.given) out.values["--business"] = selector.value; return out; } export async function limitsCommand(args, io, callFor) { const parsed = parseLimits(args); const verb = parsed.positional[0] ?? "view"; const values = parsed.values; if (parsed.positional.length > 1 && verb !== "reset") throw new CliError(LIMITS_USAGE, 4); // Row 175 (#1673, Q173): the tester reads limits; set, reset and migrate stay the human's. if (parsed.flags.has("--tester") && verb !== "view") throw new CliError(`--tester takes only the read\n${LIMITS_USAGE}`, 4); try { if (verb === "migrate") { if (!values["--file"] || !values["--models"] || values["--business"]) { throw new CliError(LIMITS_USAGE, 4); } const file = values["--file"]; const raw = JSON.parse(readFileSync(file, "utf8")); const mapping = JSON.parse(readFileSync(values["--models"], "utf8")); const rolesDir = values["--roles-dir"] ?? fileURLToPath(new URL("../../../roles", import.meta.url)); const inherits = raw.launch?.max && Object.values(raw.roles ?? {}).some(role => role.vars?.model === undefined); const system = inherits ? systemVars(loadSystem({ env: io.env })) : null; const converted = migrateLaunch(raw, mapping, file, { rolesDir, system }); io.stdout.write(JSON.stringify(converted, null, 2) + "\n"); return; } const fileVerb = verb === "set" || verb === "replies"; if (!["view", "set", "reset", "replies"].includes(verb) || values["--models"] || values["--roles-dir"] || (!fileVerb && values["--file"]) || (fileVerb && !values["--file"]) || (verb === "reset" && parsed.positional.length < 2)) { throw new CliError(LIMITS_USAGE, 4); } const call = callFor(parsed); // Row 125 (#1619, REQ-LIMIT-3): `reset tokens providers anthropic` restarts that limit's count // from now. The path is one word per level, so a model name may hold dots or slashes. The // result's `tokens` lists each token limit: counted, not-enforced or check-failed. let result; if (verb === "set") result = await call("limits.set", JSON.parse(readFileSync(values["--file"], "utf8"))); else if (verb === "replies") { const replyLimits = JSON.parse(readFileSync(values["--file"], "utf8")); const { revision } = await call("limits", {}); result = await call("library.limits.set", { revision, replyLimits }); } else if (verb === "reset") { const { revision } = await call("limits", {}); result = await call("limits.set", { revision, reset: [parsed.positional.slice(1)] }); } else result = await call("limits", {}); if (parsed.flags.has("--json")) io.stdout.write(JSON.stringify(result, null, 2) + "\n"); else { const { tokens, ...rest } = result ?? {}; io.stdout.write(JSON.stringify(rest, null, 2) + "\n" + formatTokens(tokens)); } } catch (error) { if (error instanceof CliError) throw error; throw new CliError(error.message, 2); } }