@irythill/orq 0.0.0-stage → 0.1.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.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +240 -3
  3. package/README.pt-BR.md +241 -0
  4. package/dist/accounts.js +18 -0
  5. package/dist/agents.js +202 -0
  6. package/dist/banner.js +87 -0
  7. package/dist/cli.js +90 -0
  8. package/dist/commands/agent.js +334 -0
  9. package/dist/commands/baseline.js +101 -0
  10. package/dist/commands/block.js +74 -0
  11. package/dist/commands/board.js +435 -0
  12. package/dist/commands/claim.js +88 -0
  13. package/dist/commands/completion.js +88 -0
  14. package/dist/commands/config.js +35 -0
  15. package/dist/commands/deps.js +82 -0
  16. package/dist/commands/instructions.js +66 -0
  17. package/dist/commands/lang.js +35 -0
  18. package/dist/commands/lock.js +69 -0
  19. package/dist/commands/pick.js +91 -0
  20. package/dist/commands/quota.js +10 -0
  21. package/dist/commands/run.js +46 -0
  22. package/dist/commands/set.js +27 -0
  23. package/dist/commands/setup.js +187 -0
  24. package/dist/commands/stats.js +41 -0
  25. package/dist/commands/verify.js +123 -0
  26. package/dist/commands/wait.js +81 -0
  27. package/dist/config.js +93 -0
  28. package/dist/fsutil.js +9 -0
  29. package/dist/i18n.js +21 -0
  30. package/dist/install.js +137 -0
  31. package/dist/keyring.js +51 -0
  32. package/dist/lock.js +81 -0
  33. package/dist/paths.js +34 -0
  34. package/dist/pick.js +115 -0
  35. package/dist/process.js +147 -0
  36. package/dist/quota.js +276 -0
  37. package/dist/roles.js +28 -0
  38. package/dist/stats.js +159 -0
  39. package/dist/suggest.js +121 -0
  40. package/dist/terminal.js +83 -0
  41. package/dist/testrun.js +203 -0
  42. package/dist/toml-set.js +46 -0
  43. package/package.json +27 -6
  44. package/skills/issue/SKILL.md +197 -0
  45. package/skills/orq-setup/SKILL.md +114 -0
  46. package/skills/orq-status/SKILL.md +37 -0
  47. package/templates/workspace.CLAUDE.md +49 -0
package/dist/pick.js ADDED
@@ -0,0 +1,115 @@
1
+ import { m } from "./i18n.js";
2
+ import { legacyRole, resolveRole, vendorOf } from "./roles.js";
3
+ /** "<name> 5h=NN% week=NN% [note]" lines → QuotaMap (same reading as the old orq-pick). */
4
+ export function parseQuota(text) {
5
+ const out = {};
6
+ for (const line of text.split(/\r?\n/)) {
7
+ const p = line.split(/\s+/);
8
+ if (p.length < 3)
9
+ continue;
10
+ const vals = p.slice(1, 3).map((x) => x.replace(/.*=/, ""));
11
+ if (vals.some((v) => v.startsWith("?"))) {
12
+ out[p[0]] = null;
13
+ continue;
14
+ }
15
+ const nums = vals.map((v) => v.replace(/%$/, "")).filter((v) => /^\d+$/.test(v)).map(Number);
16
+ out[p[0]] = nums.length ? Math.max(...nums) : -1;
17
+ }
18
+ return out;
19
+ }
20
+ export function modelFor(a, role, level) {
21
+ if (a.models === undefined)
22
+ return ["", undefined]; // no models configured: the CLI's default
23
+ const models = a.models;
24
+ const lr = legacyRole(role);
25
+ const ms = models[lr] ?? (lr === "fix" ? models.impl : undefined) ?? {};
26
+ const spec = ms[level] ?? ms.default ?? ms.medium ?? ms.simple;
27
+ if (!spec)
28
+ return undefined;
29
+ const [model, effort] = spec.split("@");
30
+ return [model, effort || undefined];
31
+ }
32
+ const WEIGHTS = { quality: 0.4, tokens: 0.45, speed: 0.15, rotation: 0.2, reserve: 0.3, explore: 0.1, min_samples: 8, window: 6 };
33
+ export const weights = (cfg) => ({ ...WEIGHTS, ...((cfg.policy ?? {}).weights ?? {}) });
34
+ export function pick(inp) {
35
+ const { cfg, role, level } = inp;
36
+ const agents = (cfg.agents ?? {});
37
+ const pol = (cfg.policy ?? {});
38
+ const limit = Number(pol.quota_limit ?? 90);
39
+ const w = weights(cfg);
40
+ const res = resolveRole(cfg, role);
41
+ const names = res.mode === "fixed" ? res.order : res.pool;
42
+ const notes = [];
43
+ const out = (n, why) => { notes.push(`${n}: ${why}`); };
44
+ const pass = [];
45
+ for (const name of names) {
46
+ const a = agents[name];
47
+ if (!a) {
48
+ out(name, m("not declared in [agents]", "não declarado em [agents]"));
49
+ continue;
50
+ }
51
+ if (inp.exclude.has(name)) {
52
+ out(name, m("excluded (--exclude-agent)", "excluído (--exclude-agent)"));
53
+ continue;
54
+ }
55
+ if (inp.blocks.has(name)) {
56
+ out(name, m("blocked (orq block -l)", "bloqueado (orq block -l)"));
57
+ continue;
58
+ }
59
+ if ((role === "review" || role === "opinion") && res.rule === "other-vendor" && inp.implVendor
60
+ && vendorOf(name, a) === inp.implVendor) {
61
+ out(name, m("same vendor as the implementer", "mesmo fornecedor do implementador"));
62
+ continue;
63
+ }
64
+ const mf = modelFor(a, role, level);
65
+ if (!mf) {
66
+ out(name, m(`no model for ${role}/${level}`, `sem modelo para ${role}/${level}`));
67
+ continue;
68
+ }
69
+ // Quota: an agent with several accounts (claude) uses the least used one under the limit.
70
+ const qn = a.quota ?? [];
71
+ const accs = qn.filter((n) => !inp.blocks.has(n)).map((n) => [n, n in inp.quota ? inp.quota[n] : -1]);
72
+ if (qn.length && !accs.length) {
73
+ out(name, m("all accounts blocked", "todas as contas bloqueadas"));
74
+ continue;
75
+ }
76
+ const usable = accs.filter(([, v]) => v !== null && v < limit);
77
+ if (accs.length && !usable.length) {
78
+ out(name, `${m("quota", "cota")} >= ${limit}% ${m("or no data", "ou sem dados")} (${accs.map(([n, v]) => `${n}=${v}`).join(", ")})`);
79
+ continue;
80
+ }
81
+ const acct = accs.length > 1 ? [...usable].sort((x, y) => x[1] - y[1])[0][0] : undefined;
82
+ const statRole = role === "fix" || role === "worker" ? "impl" : role;
83
+ const st = inp.stats.find((r) => r.role === statRole && r.agent === name && r.model === (mf[0].split("/").pop() || "default"));
84
+ pass.push({ agent: name, model: mf[0], effort: mf[1], acct, a, st });
85
+ if (res.mode === "fixed")
86
+ break; // the first that passes wins
87
+ }
88
+ if (res.mode === "fixed") {
89
+ const c = pass[0];
90
+ const best = c && { agent: c.agent, model: c.model, effort: c.effort, acct: c.acct, score: 0,
91
+ why: m(`fixed by [roles.${res.section}]`, `fixo em [roles.${res.section}]`) };
92
+ return { mode: "fixed", best, cands: best ? [best] : [], notes };
93
+ }
94
+ const few = (st) => (st?.n ?? 0) < w.min_samples;
95
+ const toks = pass.filter((c) => c.st?.new && !few(c.st)).map((c) => c.st.new);
96
+ const mins = pass.filter((c) => c.st?.min && !few(c.st)).map((c) => c.st.min);
97
+ const cands = pass.map((c) => {
98
+ const prior = Number(c.a.prior ?? 0.6), n = c.st?.n ?? 0, reserve = Boolean(c.a.reserve);
99
+ const deliv = role === "impl" && c.st?.approved != null && c.st.n_appr >= 3 ? c.st.approved / 100
100
+ : c.st ? c.st.ok / 100 : undefined;
101
+ const d = few(c.st) || deliv === undefined ? prior : deliv;
102
+ const t = few(c.st) || !c.st?.new || !toks.length ? prior : Math.min(...toks) / c.st.new;
103
+ const s = few(c.st) || !c.st?.min || !mins.length ? prior : Math.min(...mins) / Math.max(c.st.min, 0.1);
104
+ const used = inp.recent.filter((p) => p.agent === c.agent).length;
105
+ const score = w.quality * d + w.tokens * t + w.speed * s - w.rotation * used
106
+ - (reserve ? w.reserve : 0) + (few(c.st) ? w.explore : 0);
107
+ const why = `${m("delivery", "entrega")}=${d.toFixed(2)} ${m("economy", "economia")}=${t.toFixed(2)} `
108
+ + `${m("speed", "rapidez")}=${s.toFixed(2)} ${m("rotation", "rodízio")}=-${used} `
109
+ + (reserve ? m("reserve ", "reserva ") : "") + (few(c.st) ? `${m("few samples", "poucos dados")}(n=${n})` : `n=${n}`);
110
+ return { agent: c.agent, model: c.model, effort: c.effort, acct: c.acct, score, why };
111
+ }).sort((x, y) => y.score - x.score);
112
+ return { mode: "auto", best: cands[0], cands, notes };
113
+ }
114
+ /** The arguments for the agent runner, e.g. "-a codex -m gpt-6-sol -e low -c work". */
115
+ export const asArgs = (c) => `-a ${c.agent}` + (c.model ? ` -m ${c.model}` : "") + (c.effort ? ` -e ${c.effort}` : "") + (c.acct ? ` -c ${c.acct}` : "");
@@ -0,0 +1,147 @@
1
+ // ProcessHost — run a command under a memory cap, on any OS.
2
+ // kernel Linux with a systemd user session: `systemd-run --user --scope -p MemoryMax=` (the kernel kills only
3
+ // that command, as orq always did)
4
+ // watchdog everywhere else (macOS, Windows, Linux without systemd): orq samples the command's process tree every
5
+ // few seconds and kills the whole tree above the cap
6
+ // off no cap
7
+ // mode "auto" = kernel when available, else watchdog. (Windows Job Objects need a native addon: later, if the
8
+ // watchdog proves too coarse.)
9
+ import { execFile, spawn, spawnSync } from "node:child_process";
10
+ import { m } from "./i18n.js";
11
+ /** "4G", "512M", "1.5G", "off" → bytes (undefined = no cap). */
12
+ export function parseSize(s) {
13
+ if (s === undefined || s === "" || s === "off" || s === false)
14
+ return undefined;
15
+ if (typeof s === "number")
16
+ return s;
17
+ const r = /^(\d+(?:\.\d+)?)\s*([KMGT]?)i?B?$/i.exec(s.trim());
18
+ if (!r)
19
+ throw new Error(m(`invalid size: ${s} (e.g. 4G, 512M, off)`, `tamanho inválido: ${s} (ex.: 4G, 512M, off)`));
20
+ return Math.round(Number(r[1]) * 1024 ** "BKMGT".indexOf((r[2] || "B").toUpperCase()));
21
+ }
22
+ let kernelOk;
23
+ export function hasKernelCap() {
24
+ if (process.platform !== "linux")
25
+ return false;
26
+ kernelOk ??= spawnSync("systemd-run", ["--user", "--scope", "-q", "--", "true"], { stdio: "ignore", timeout: 10_000 }).status === 0;
27
+ return kernelOk;
28
+ }
29
+ // systemd-run expands ${...} in its arguments (the prompt!) unless told not to; the flag exists in systemd >= 254.
30
+ let noExpand;
31
+ const noExp = () => noExpand ??= /--expand-environment/.test(spawnSync("systemd-run", ["--help"], { encoding: "utf8" }).stdout ?? "")
32
+ ? ["--expand-environment=no"] : [];
33
+ export function resolveMode(mode, cap) {
34
+ if (cap === undefined || mode === "off")
35
+ return "off";
36
+ if (mode === "kernel" || (mode === "auto" && hasKernelCap()))
37
+ return "kernel";
38
+ return "watchdog";
39
+ }
40
+ const LIST = process.platform === "win32"
41
+ ? ["powershell", ["-NoProfile", "-NonInteractive", "-Command",
42
+ "Get-CimInstance Win32_Process -Property ProcessId,ParentProcessId,WorkingSetSize | ForEach-Object { \"$($_.ProcessId) $($_.ParentProcessId) $($_.WorkingSetSize)\" }"]]
43
+ : ["ps", ["-A", "-o", "pid=,ppid=,rss="]];
44
+ /** pid → [ppid, bytes] for every process (ps on Linux/macOS, CIM on Windows). */
45
+ export function processTable() {
46
+ return parseTable(spawnSync(LIST[0], LIST[1], { encoding: "utf8", timeout: 15_000 }).stdout ?? "");
47
+ }
48
+ /** Same, without blocking: PowerShell takes seconds on Windows and the watchdog must keep running meanwhile. */
49
+ export function processTableAsync() {
50
+ return new Promise((res) => execFile(LIST[0], LIST[1], { encoding: "utf8", timeout: 15_000, windowsHide: true }, (_e, out) => res(parseTable(out ?? ""))));
51
+ }
52
+ function parseTable(text) {
53
+ const t = new Map();
54
+ for (const line of text.split(/\r?\n/)) {
55
+ const [pid, ppid, mem] = line.trim().split(/\s+/).map(Number);
56
+ if (pid)
57
+ t.set(pid, [ppid, process.platform === "win32" ? mem : mem * 1024]);
58
+ }
59
+ return t;
60
+ }
61
+ /** Total memory of `root` and all its descendants. */
62
+ export function treeMem(root, table = processTable()) {
63
+ const kids = new Map();
64
+ for (const [pid, [ppid]] of table)
65
+ kids.set(ppid, [...(kids.get(ppid) ?? []), pid]);
66
+ let total = 0;
67
+ const seen = new Set(), todo = [root];
68
+ while (todo.length) {
69
+ const p = todo.pop();
70
+ if (seen.has(p))
71
+ continue;
72
+ seen.add(p);
73
+ total += table.get(p)?.[1] ?? 0;
74
+ todo.push(...(kids.get(p) ?? []));
75
+ }
76
+ return total;
77
+ }
78
+ export function killTree(pid) {
79
+ if (process.platform === "win32")
80
+ spawnSync("taskkill", ["/T", "/F", "/PID", String(pid)], { stdio: "ignore" });
81
+ else
82
+ try {
83
+ process.kill(-pid, "SIGKILL");
84
+ }
85
+ catch {
86
+ try {
87
+ process.kill(pid, "SIGKILL");
88
+ }
89
+ catch { /* gone */ }
90
+ }
91
+ }
92
+ /** cmd.exe quoting for one argument (only used when Windows needs a shell). */
93
+ export const winQuote = (a) => /[\s"&|<>^%()]/.test(a) || a === "" ? `"${a.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\+)$/, "$1$1")}"` : a;
94
+ const gb = (b) => `${(b / 2 ** 30).toFixed(1)} GB`;
95
+ /** Run `cmd args` with stdio inherited, under the cap; resolves to the exit code (137 = killed for memory). */
96
+ export function runCapped(cmd, args, o = {}) {
97
+ const mode = resolveMode(o.mode ?? "auto", o.cap);
98
+ if (mode === "kernel") {
99
+ const sd = ["--user", "--scope", "-q", ...noExp(), ...(o.unit ? [`--unit=${o.unit}`] : []), "-p", `MemoryMax=${o.cap}`,
100
+ ...(o.swap !== undefined ? ["-p", `MemorySwapMax=${o.swap}`] : []), "--", cmd, ...args];
101
+ const c = spawn("systemd-run", sd, { stdio: o.stdio ?? "inherit", cwd: o.cwd, env: o.env });
102
+ o.onSpawn?.(c);
103
+ return wait(c);
104
+ }
105
+ const win = process.platform === "win32";
106
+ // Own process group on Linux/macOS, so the whole tree can be killed at once.
107
+ // Windows: a shell only for .cmd/.bat shims (npx, claude…); an .exe runs directly (paths with spaces stay intact).
108
+ const shell = win && !/\.exe$/i.test(cmd), q = shell && o.quote;
109
+ const child = spawn(q ? winQuote(cmd) : cmd, q ? args.map(winQuote) : args, { stdio: o.stdio ?? "inherit", cwd: o.cwd, env: o.env, detached: !win, shell, windowsHide: true });
110
+ o.onSpawn?.(child);
111
+ if (mode === "off" || !child.pid)
112
+ return wait(child);
113
+ const fwd = (sig) => () => { try {
114
+ process.kill(-child.pid, sig);
115
+ }
116
+ catch {
117
+ child.kill(sig);
118
+ } };
119
+ const onInt = fwd("SIGINT"), onTerm = fwd("SIGTERM");
120
+ if (!win) {
121
+ process.on("SIGINT", onInt);
122
+ process.on("SIGTERM", onTerm);
123
+ }
124
+ let killed = false, sampling = false;
125
+ const timer = setInterval(async () => {
126
+ if (sampling || killed)
127
+ return; // a slow sample (Windows) never overlaps the next
128
+ sampling = true;
129
+ const used = treeMem(child.pid, await processTableAsync());
130
+ sampling = false;
131
+ if (used > o.cap && !killed) {
132
+ killed = true;
133
+ console.error(m(`orq: memory ${gb(used)} over the cap (${gb(o.cap)}): killing the process tree`, `orq: memória ${gb(used)} acima do limite (${gb(o.cap)}): encerrando a árvore de processos`));
134
+ killTree(child.pid);
135
+ }
136
+ }, o.intervalMs ?? 1000);
137
+ return wait(child).then((code) => {
138
+ clearInterval(timer);
139
+ process.off("SIGINT", onInt);
140
+ process.off("SIGTERM", onTerm);
141
+ return killed ? 137 : code;
142
+ });
143
+ }
144
+ const wait = (c) => new Promise((res) => {
145
+ c.on("error", (e) => { console.error(`orq: ${e.message}`); res(127); });
146
+ c.on("exit", (code, sig) => res(code ?? (sig ? 128 + 9 : 1)));
147
+ });
package/dist/quota.js ADDED
@@ -0,0 +1,276 @@
1
+ // Quota per agent/account, from the sources in the [quota] config section:
2
+ // "builtin" claude: the account's usage (Anthropic OAuth usage endpoint, same login Claude Code uses)
3
+ // codex: the rate_limits Codex writes into its latest session file
4
+ // antigravity: per-model quota from Cloud Code (fetchAvailableModels), with the login agy keeps
5
+ // in the OS keyring (src/keyring.ts: Secret Service, Keychain, Credential Manager)
6
+ // copilot: premium requests left this month (`gh api /copilot_internal/user`; a monthly
7
+ // allowance, shown in the week slot)
8
+ // false left out of the list entirely
9
+ // "none" no collector: never skipped for quota; use `orq block` when it runs out
10
+ // { command = "..." } any command printing lines "<name> 5h=NN% week=NN% [note]"
11
+ // Output line (read by orq-pick and the board): "<name> 5h=NN% week=NN% [note]".
12
+ // - = the plan has no such window (not limiting); ? = no data (expired login, collector failed).
13
+ import { spawnSync } from "node:child_process";
14
+ import { readdirSync, readFileSync, statSync } from "node:fs";
15
+ import { homedir, userInfo } from "node:os";
16
+ import { join } from "node:path";
17
+ import { claudeAccounts } from "./accounts.js";
18
+ import { writeAtomic } from "./fsutil.js";
19
+ import { readSecret } from "./keyring.js";
20
+ import { stateDir } from "./paths.js";
21
+ import { m } from "./i18n.js";
22
+ const show = (p) => (typeof p === "number" ? `${Math.floor(p)}%` : p);
23
+ export const format = (q) => `${q.name} 5h=${show(q.five)} week=${show(q.week)}${q.note ? ` [${q.note}]` : ""}`;
24
+ const noData = (name, note) => ({ name, five: "?", week: "?", note });
25
+ const KEEP_MS = 5 * 3600_000;
26
+ const cacheFile = (name) => join(stateDir(), "quota", `${name}.json`);
27
+ function remember(name, five, week) {
28
+ try {
29
+ writeAtomic(cacheFile(name), JSON.stringify({ five, week, at: Date.now() }));
30
+ }
31
+ catch { /* best effort */ }
32
+ return { name, five: five.pct, week: week.pct };
33
+ }
34
+ /** The cached reading (its age in the note), window by window; nothing usable → the "no data" line. */
35
+ export function fromCache(name, why, now = Date.now()) {
36
+ try {
37
+ const c = JSON.parse(readFileSync(cacheFile(name), "utf8"));
38
+ const still = (w) => {
39
+ if (w.resetsAt) {
40
+ const t = Date.parse(w.resetsAt);
41
+ if (!Number.isNaN(t))
42
+ return t > now ? w.pct : 0;
43
+ }
44
+ return now - c.at < KEEP_MS ? w.pct : undefined;
45
+ };
46
+ const five = still(c.five), week = still(c.week);
47
+ if (five !== undefined && week !== undefined) {
48
+ const min = Math.round((now - c.at) / 60_000);
49
+ return { name, five, week, note: `${why}; ` + m(`last reading, ${min} min ago`, `última leitura, há ${min} min`) };
50
+ }
51
+ }
52
+ catch { /* no cache */ }
53
+ return noData(name, why);
54
+ }
55
+ function claudeToken(dir) {
56
+ try {
57
+ return JSON.parse(readFileSync(join(dir, ".credentials.json"), "utf8")).claudeAiOauth;
58
+ }
59
+ catch { /* not in a file: macOS keeps it in the Keychain */ }
60
+ if (process.platform !== "darwin")
61
+ return undefined;
62
+ try {
63
+ return JSON.parse(readSecret("Claude Code-credentials", userInfo().username) ?? "").claudeAiOauth;
64
+ }
65
+ catch {
66
+ return undefined;
67
+ }
68
+ }
69
+ export async function claudeQuota(name, dir) {
70
+ const t = claudeToken(dir);
71
+ if (!t?.accessToken)
72
+ return noData(name, m("not logged in", "sem login"));
73
+ // Never refresh the token here (that would rotate Claude Code's login): an expired one just means "open claude".
74
+ if (t.expiresAt && t.expiresAt < Date.now())
75
+ return noData(name, m("login expired: open claude once", "login expirado: abra o claude uma vez"));
76
+ try {
77
+ const r = await fetch("https://api.anthropic.com/api/oauth/usage", {
78
+ headers: { Authorization: `Bearer ${t.accessToken}`, "anthropic-beta": "oauth-2025-04-20" },
79
+ signal: AbortSignal.timeout(15_000),
80
+ });
81
+ if (r.status === 401)
82
+ return noData(name, m("login expired: open claude once", "login expirado: abra o claude uma vez"));
83
+ if (!r.ok)
84
+ return fromCache(name, `HTTP ${r.status}`);
85
+ const j = await r.json();
86
+ const win = (w) => ({ pct: typeof w?.utilization === "number" ? w.utilization : "-",
87
+ ...(typeof w?.resets_at === "string" ? { resetsAt: w.resets_at } : {}) });
88
+ return remember(name, win(j.five_hour), win(j.seven_day));
89
+ }
90
+ catch (e) {
91
+ return fromCache(name, m("collector failed", "coletor falhou") + `: ${e.message}`);
92
+ }
93
+ }
94
+ // --- codex ----------------------------------------------------------------------------------------------------------
95
+ function latestCodexSession() {
96
+ const root = join(process.env.CODEX_HOME ?? join(homedir(), ".codex"), "sessions");
97
+ let best;
98
+ const walk = (d, depth) => {
99
+ let entries;
100
+ try {
101
+ entries = readdirSync(d, { withFileTypes: true });
102
+ }
103
+ catch {
104
+ return;
105
+ }
106
+ for (const e of entries) {
107
+ const p = join(d, e.name);
108
+ if (e.isDirectory() && depth < 3)
109
+ walk(p, depth + 1);
110
+ else if (e.isFile() && e.name.endsWith(".jsonl")) {
111
+ const t = statSync(p).mtimeMs;
112
+ if (!best || t > best.t)
113
+ best = { f: p, t };
114
+ }
115
+ }
116
+ };
117
+ walk(root, 0);
118
+ return best?.f;
119
+ }
120
+ /** The last rate_limits in a Codex session (.jsonl): 300-min window = 5h, 10080 = week; a window already reset → 0. */
121
+ export function codexFromSession(text, nowSec) {
122
+ let last;
123
+ for (const line of text.split("\n")) {
124
+ if (!line.includes('"rate_limits"'))
125
+ continue;
126
+ try {
127
+ const find = (o) => {
128
+ if (o && typeof o === "object") {
129
+ const r = o.rate_limits;
130
+ if (r && typeof r === "object")
131
+ last = r;
132
+ Object.values(o).forEach(find);
133
+ }
134
+ };
135
+ find(JSON.parse(line));
136
+ }
137
+ catch { /* partial line */ }
138
+ }
139
+ if (!last)
140
+ return undefined;
141
+ const ws = [last.primary, last.secondary].filter((w) => !!w);
142
+ const win = (mins) => {
143
+ const w = ws.find((x) => x.window_minutes === mins);
144
+ if (!w)
145
+ return "-";
146
+ return (w.resets_at ?? 0) < nowSec ? 0 : Math.floor(w.used_percent ?? 0);
147
+ };
148
+ return { five: win(300), week: win(10080) };
149
+ }
150
+ export function codexQuota() {
151
+ const f = latestCodexSession();
152
+ if (!f)
153
+ return noData("codex", m("no Codex session yet", "nenhuma sessão do Codex ainda"));
154
+ const q = codexFromSession(readFileSync(f, "utf8"), Date.now() / 1000);
155
+ if (!q)
156
+ return noData("codex", m("no limit data", "sem dados de limite"));
157
+ const age = Math.round((Date.now() - statSync(f).mtimeMs) / 60_000);
158
+ return { name: "codex", ...q, note: m(`from the last session, ${age} min ago`, `da última sessão, há ${age} min`) };
159
+ }
160
+ /** Worst usage per window across models: a reset within 6 h belongs to the 5h window, a later one to the week. */
161
+ export function antigravityFromModels(models, now = Date.now()) {
162
+ const five = { pct: "-" }, week = { pct: "-" };
163
+ for (const { quotaInfo: q } of Object.values(models ?? {})) {
164
+ if (typeof q?.remainingFraction !== "number" || !q.resetTime)
165
+ continue;
166
+ const at = Date.parse(q.resetTime);
167
+ if (Number.isNaN(at))
168
+ continue;
169
+ const w = at - now <= 6 * 3600_000 ? five : week;
170
+ const used = Math.round((1 - q.remainingFraction) * 100);
171
+ if (w.pct === "-" || used > w.pct) {
172
+ w.pct = used;
173
+ w.resetsAt = q.resetTime;
174
+ }
175
+ }
176
+ return { five, week };
177
+ }
178
+ function agyToken() {
179
+ // agy (go-keyring) keeps {"token": {access_token, expiry, ...}} under service "gemini", user "antigravity"
180
+ // (on Windows go-keyring names the credential "gemini:antigravity").
181
+ const raw = readSecret("gemini", "antigravity", "gemini:antigravity");
182
+ if (!raw)
183
+ return undefined;
184
+ try {
185
+ const t = JSON.parse(raw).token;
186
+ return { access: t?.access_token, expiry: t?.expiry };
187
+ }
188
+ catch {
189
+ return undefined;
190
+ }
191
+ }
192
+ export async function antigravityQuota() {
193
+ const name = "antigravity";
194
+ const t = agyToken();
195
+ if (!t?.access)
196
+ return noData(name, m("no agy login in the keyring", "sem login do agy no chaveiro"));
197
+ // Never refresh it here (that is agy's login): an expired one just means "run agy once" (`agy models` is enough).
198
+ const expired = m("login expired: run `agy models`", "login expirado: rode `agy models`");
199
+ if (t.expiry && Date.parse(t.expiry) < Date.now())
200
+ return fromCache(name, expired);
201
+ try {
202
+ const r = await fetch("https://cloudcode-pa.googleapis.com/v1internal:fetchAvailableModels", {
203
+ method: "POST", body: "{}",
204
+ // The endpoint answers 403 without a client User-Agent.
205
+ headers: { Authorization: `Bearer ${t.access}`, "Content-Type": "application/json", "User-Agent": "antigravity" },
206
+ signal: AbortSignal.timeout(15_000),
207
+ });
208
+ if (r.status === 401)
209
+ return fromCache(name, expired);
210
+ if (!r.ok)
211
+ return fromCache(name, `HTTP ${r.status}`);
212
+ const j = await r.json();
213
+ const { five, week } = antigravityFromModels(j.models ?? {});
214
+ return remember(name, five, week);
215
+ }
216
+ catch (e) {
217
+ return fromCache(name, m("collector failed", "coletor falhou") + `: ${e.message}`);
218
+ }
219
+ }
220
+ // --- copilot --------------------------------------------------------------------------------------------------------
221
+ /** Premium requests used this month, from the snapshot `gh api /copilot_internal/user` returns. No 5h window. */
222
+ export function copilotFromUser(j) {
223
+ const u = j;
224
+ const p = u?.quota_snapshots?.premium_interactions;
225
+ if (!p)
226
+ return noData("copilot", m("no premium request data", "sem dados de requisições premium"));
227
+ if (p.unlimited)
228
+ return { name: "copilot", five: "-", week: "-", note: m("unlimited", "ilimitado") };
229
+ if (typeof p.percent_remaining !== "number")
230
+ return noData("copilot", m("no premium request data", "sem dados de requisições premium"));
231
+ const reset = u.quota_reset_date ? m(`, resets ${u.quota_reset_date}`, `, renova em ${u.quota_reset_date}`) : "";
232
+ return { name: "copilot", five: "-", week: Math.max(0, 100 - p.percent_remaining),
233
+ note: m("monthly premium requests", "requisições premium do mês") + reset };
234
+ }
235
+ export function copilotQuota() {
236
+ const r = spawnSync("gh", ["api", "/copilot_internal/user"], { encoding: "utf8", timeout: 15_000 });
237
+ if (r.error || r.status !== 0)
238
+ return noData("copilot", m("gh api failed (gh auth login?)", "gh api falhou (gh auth login?)"));
239
+ try {
240
+ return copilotFromUser(JSON.parse(r.stdout));
241
+ }
242
+ catch {
243
+ return noData("copilot", m("collector failed", "coletor falhou"));
244
+ }
245
+ }
246
+ // --- command ----------------------------------------------------------------------------------------------------------
247
+ function commandQuota(name, cmd) {
248
+ const r = spawnSync(cmd, { shell: true, encoding: "utf8", timeout: 30_000 });
249
+ const lines = (r.stdout ?? "").split(/\r?\n/).filter((l) => /\S+ 5h=\S+ week=\S+/.test(l));
250
+ return lines.length ? lines : [format(noData(name, m("collector failed", "coletor falhou")))];
251
+ }
252
+ // --- all --------------------------------------------------------------------------------------------------------------
253
+ /** One or more output lines per configured source, in config order. `want` filters by line name. */
254
+ export async function collect(sources, want = []) {
255
+ const wanted = (n) => !want.length || want.includes(n);
256
+ const jobs = Object.entries(sources).map(async ([name, src]) => {
257
+ if (src === false)
258
+ return [];
259
+ if (src === "none")
260
+ return wanted(name) ? [format({ name, five: "-", week: "-", note: m("no quota collector", "sem coletor de cota") })] : [];
261
+ if (typeof src === "object" && src && "command" in src)
262
+ return commandQuota(name, src.command).filter((l) => wanted(l.split(" ")[0]));
263
+ if (name === "claude") {
264
+ const accs = claudeAccounts().filter((a) => wanted(a.name) || (want.includes("claude")));
265
+ return (await Promise.all(accs.map((a) => claudeQuota(a.name, a.dir)))).map(format);
266
+ }
267
+ if (name === "codex")
268
+ return wanted(name) ? [format(codexQuota())] : [];
269
+ if (name === "antigravity")
270
+ return wanted(name) ? [format(await antigravityQuota())] : [];
271
+ if (name === "copilot")
272
+ return wanted(name) ? [format(copilotQuota())] : [];
273
+ return wanted(name) ? [format(noData(name, m("no builtin collector: use \"none\" or a command", "sem coletor embutido: use \"none\" ou um comando")))] : [];
274
+ });
275
+ return (await Promise.all(jobs)).flat();
276
+ }
package/dist/roles.js ADDED
@@ -0,0 +1,28 @@
1
+ /** What the conductor asks for (orq pick <role>) → the [roles] section that answers it. */
2
+ export const PICK_ROLES = ["impl", "fix", "worker", "review", "opinion"];
3
+ const SECTION = {
4
+ impl: ["primary"], fix: ["primary"], worker: ["workers", "primary"], review: ["reviewer"], opinion: ["opinion"],
5
+ };
6
+ /** The role name used in the old agents.<name>.roles lists. */
7
+ export const legacyRole = (r) => (r === "worker" ? "impl" : r);
8
+ /** Vendors of the built-in agents, so a config may leave `vendor` out. */
9
+ export const BUILTIN_VENDORS = {
10
+ claude: "anthropic", codex: "openai", antigravity: "google", copilot: "github",
11
+ };
12
+ export const vendorOf = (name, a) => a?.vendor ?? BUILTIN_VENDORS[name];
13
+ export function resolveRole(cfg, role) {
14
+ const agents = (cfg.agents ?? {});
15
+ const roles = (cfg.roles ?? {});
16
+ const policyRule = String((cfg.policy ?? {}).review ?? "other-vendor");
17
+ const section = SECTION[role].find((s) => roles[s]);
18
+ const r = section ? roles[section] : undefined;
19
+ const rule = String(r?.rule ?? policyRule);
20
+ if (r && typeof r.agent === "string")
21
+ return { mode: "fixed", order: [r.agent, ...(r.fallback ?? [])], rule, section: section };
22
+ if (r && Array.isArray(r.pool))
23
+ return { mode: "auto", pool: r.pool, rule, section };
24
+ const legacy = legacyRole(role);
25
+ const pool = Object.entries(agents).filter(([, a]) => (a.roles ?? []).includes(legacy))
26
+ .map(([n]) => n);
27
+ return { mode: "auto", pool, rule, section };
28
+ }