agent-lyceum 0.28.0 → 0.29.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/README.md CHANGED
@@ -49,7 +49,7 @@ Its own data (config, mailboxes, logs, memory, worktrees) lives in `~/agent-lyce
49
49
  | `init` | Create the home and the global agent library. [details](docs/commands.md#init) |
50
50
  | `project add` / `project` / `project remove` | Register, list or remove projects. [details](docs/commands.md#project-add) |
51
51
  | `doctor` | Check config, git, lock and runtime CLIs without running agents. [details](docs/commands.md#doctor) |
52
- | `status` | Show agents, mail, the current wake-up and past runs. [details](docs/commands.md#status) |
52
+ | `status` | Show agents, mail, the current wake-up and past runs. `status --web` opens every registered project in a browser. [details](docs/commands.md#status) |
53
53
  | `run` | Give the task to the lead and run until done. [details](docs/commands.md#run) |
54
54
  | `resume` | Continue an interrupted run, or reopen a finished one with a follow-up. [details](docs/commands.md#resume) |
55
55
  | `clear` | Delete a run, or remove a stale lock. [details](docs/commands.md#clear) |
package/README.zh-TW.md CHANGED
@@ -49,7 +49,7 @@ npx agent-lyceum status
49
49
  | `init` | 建立 home 與全域 agent 庫。 [詳細](docs/commands.zh-TW.md#init) |
50
50
  | `project add` / `project` / `project remove` | 註冊、列出或移除專案。 [詳細](docs/commands.zh-TW.md#project-add) |
51
51
  | `doctor` | 不執行 agent,檢查設定、git、鎖與 runtime CLI。 [詳細](docs/commands.zh-TW.md#doctor) |
52
- | `status` | 顯示 agent、信件、目前的喚醒與過去的 run。 [詳細](docs/commands.zh-TW.md#status) |
52
+ | `status` | 顯示 agent、信件、目前的喚醒與過去的 run。`status --web` 在瀏覽器看全部已註冊專案。 [詳細](docs/commands.zh-TW.md#status) |
53
53
  | `run` | 把任務交給 lead,執行到完成。 [詳細](docs/commands.zh-TW.md#run) |
54
54
  | `resume` | 繼續中斷的 run,或用追加需求重開已完成的 run。 [詳細](docs/commands.zh-TW.md#resume) |
55
55
  | `clear` | 刪除 run,或移除殘留的鎖。 [詳細](docs/commands.zh-TW.md#clear) |
package/dist/cli.js CHANGED
@@ -3,6 +3,7 @@ import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import readline from "node:readline/promises";
5
5
  import { spawnSync } from "node:child_process";
6
+ import { fileURLToPath, pathToFileURL } from "node:url";
6
7
  import { Command } from "commander";
7
8
  import { ConfigError, findProjectForCwd, flattenResolved, listProjects, loadGlobal, resolveProject, resolveProjectWithSources } from "./config.js";
8
9
  import { openInEditor, prepareAnswers } from "./ask-answer.js";
@@ -27,6 +28,7 @@ import { createStatusWatcher } from "./status-watch.js";
27
28
  import { LAUNCH_PPID, startedByWrapper, watchParent } from "./parent-watch.js";
28
29
  import { onlyAgent, soloProject } from "./solo.js";
29
30
  import { DEMO_PROJECT, DEMO_TASK, defaultDemoDir, detectRuntime, prepareDemo } from "./demo.js";
31
+ import { claudeQuota, codexQuota } from "./quota.js";
30
32
  const program = new Command();
31
33
  program
32
34
  .name("agent-lyceum")
@@ -764,9 +766,57 @@ program
764
766
  .option("--interval <sec>", "refresh interval for --monitor", "2")
765
767
  .option("--json", "print the report as JSON (no colour); works with --task-list and --task-id")
766
768
  .option("--watch", "with --json: keep running and print one JSON line (the project report plus one per running task) whenever it changes (Ctrl-C to quit)")
767
- .option("--heartbeat <sec>", "with --watch: how often to re-check when no file event came", "5")
769
+ .option("--heartbeat <sec>", "with --watch or --web: how often to re-check when no file event came", "5")
770
+ .option("--web", "open a local page for every registered project")
771
+ .option("--port <n>", "with --web: TCP port", "8787")
772
+ .option("--host <addr>", "with --web: bind address", "127.0.0.1")
773
+ .option("--no-open", "with --web: do not open a browser")
768
774
  .action(async (opts) => {
769
775
  try {
776
+ if (opts.web) {
777
+ if (opts.json || opts.watch || opts.monitor || opts.taskList || opts.taskId !== undefined)
778
+ fail("--web cannot be combined with --json, --watch, --monitor, --task-list or --task-id.");
779
+ if (opts.project)
780
+ fail("--web shows every registered project; it does not take --project.");
781
+ const port = Number(opts.port);
782
+ if (!Number.isInteger(port) || port < 1 || port > 65535)
783
+ fail("--port must be a TCP port.");
784
+ const beat = Number(opts.heartbeat);
785
+ if (!(beat > 0))
786
+ fail("--heartbeat must be a positive number of seconds.");
787
+ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
788
+ const entry = path.join(root, "web", "dist", "start.js");
789
+ const staticDir = path.join(root, "web", "dist", "public");
790
+ if (!fs.existsSync(entry) || !fs.existsSync(path.join(staticDir, "index.html"))) {
791
+ fail(`The web monitor is not built.\nFrom the repo: pnpm --dir web install && pnpm --dir web build\nExpected ${entry}`);
792
+ }
793
+ const mod = (await import(pathToFileURL(entry).href));
794
+ const page = await mod.startWebServer({
795
+ port,
796
+ host: opts.host,
797
+ open: opts.open !== false,
798
+ staticDir,
799
+ heartbeatMs: beat * 1000,
800
+ factory: {
801
+ list: () => listProjects(home()).map((item) => item.name),
802
+ watch: (name, onFrame) => createStatusWatcher({
803
+ load: () => resolveProject(home(), name),
804
+ dir: (project) => project.paths.runs,
805
+ write: (line) => onFrame(JSON.parse(line)),
806
+ debounceMs: 100,
807
+ allTasks: true,
808
+ }),
809
+ quota: () => Promise.all([claudeQuota(), codexQuota()]),
810
+ },
811
+ });
812
+ console.log(page.url);
813
+ const stop = (code) => {
814
+ void page.close().finally(() => process.exit(code));
815
+ };
816
+ process.on("SIGINT", () => stop(130));
817
+ process.on("SIGTERM", () => stop(143));
818
+ await new Promise(() => { });
819
+ }
770
820
  if (opts.watch) {
771
821
  if (!opts.json)
772
822
  fail("--watch needs --json.");
@@ -44,6 +44,8 @@ export interface PidStartIo {
44
44
  }
45
45
  /** A start time that identifies one process: `proc:<ticks>` on Linux, the `ps` text elsewhere or when /proc cannot be read. */
46
46
  export declare function pidStartFrom(pid: number, io: PidStartIo): string | undefined;
47
+ /** True when both strings name the same process start. Locale-specific `ps` text for one instant matches. */
48
+ export declare function samePidStart(a: string, b: string): boolean;
47
49
  /**
48
50
  * Is the process that wrote this lock still the same running process? Judged by pid and its start time,
49
51
  * never by hostname (macOS changes it with the network) or by heartbeat age (a busy holder may be late).
@@ -50,7 +50,8 @@ const realIo = {
50
50
  }
51
51
  },
52
52
  ps: (pid) => {
53
- const r = spawnSync("ps", ["-o", "lstart=", "-p", String(pid)], { encoding: "utf8" });
53
+ // C locale: `ps` start times stay in one language. A lock written from a Chinese terminal would otherwise look like a different process.
54
+ const r = spawnSync("ps", ["-o", "lstart=", "-p", String(pid)], { encoding: "utf8", env: { ...process.env, LANG: "C", LC_ALL: "C" } });
54
55
  const out = r.status === 0 ? r.stdout.trim() : "";
55
56
  return out || undefined;
56
57
  },
@@ -60,6 +61,30 @@ function pidStart(pid) {
60
61
  }
61
62
  /** Two start times can only be compared when they were taken the same way. */
62
63
  const sameKind = (a, b) => a.startsWith("proc:") === b.startsWith("proc:");
64
+ const MONTHS = { Jan: "01", Feb: "02", Mar: "03", Apr: "04", May: "05", Jun: "06", Jul: "07", Aug: "08", Sep: "09", Oct: "10", Nov: "11", Dec: "12" };
65
+ /** `ps -o lstart` in the C locale, or the same instant from a Chinese macOS locale, as `YYYY-MM-DDTHH:mm:ss`. */
66
+ function canonicalLstart(text) {
67
+ const s = text.trim();
68
+ const en = /^[A-Za-z]{3}\s+([A-Za-z]{3})\s+(\d{1,2})\s+(\d{2}):(\d{2}):(\d{2})\s+(\d{4})$/.exec(s);
69
+ if (en) {
70
+ const month = MONTHS[en[1] ?? ""];
71
+ if (!month)
72
+ return undefined;
73
+ return `${en[6]}-${month}-${(en[2] ?? "").padStart(2, "0")}T${en[3]}:${en[4]}:${en[5]}`;
74
+ }
75
+ const zh = /^\S+\s+(\d{1,2})月\/\s*(\d{1,2})\s+(\d{2}):(\d{2}):(\d{2})\s+(\d{4})$/.exec(s);
76
+ if (zh)
77
+ return `${zh[6]}-${(zh[1] ?? "").padStart(2, "0")}-${(zh[2] ?? "").padStart(2, "0")}T${zh[3]}:${zh[4]}:${zh[5]}`;
78
+ return undefined;
79
+ }
80
+ /** True when both strings name the same process start. Locale-specific `ps` text for one instant matches. */
81
+ export function samePidStart(a, b) {
82
+ if (a === b)
83
+ return true;
84
+ const left = canonicalLstart(a);
85
+ const right = canonicalLstart(b);
86
+ return !!left && left === right;
87
+ }
63
88
  /**
64
89
  * Is the process that wrote this lock still the same running process? Judged by pid and its start time,
65
90
  * never by hostname (macOS changes it with the network) or by heartbeat age (a busy holder may be late).
@@ -74,7 +99,7 @@ export function lockHolderAlive(info) {
74
99
  }
75
100
  if (info.pid_started) {
76
101
  const now = pidStart(info.pid);
77
- if (now && sameKind(now, info.pid_started) && now !== info.pid_started)
102
+ if (now && sameKind(now, info.pid_started) && !samePidStart(info.pid_started, now))
78
103
  return false;
79
104
  }
80
105
  return true;
@@ -0,0 +1,25 @@
1
+ export type QuotaWindow = {
2
+ label: string;
3
+ used_percent: number;
4
+ remaining_percent: number;
5
+ resets: string;
6
+ };
7
+ export type QuotaReport = {
8
+ runtime: "claude" | "codex";
9
+ windows: QuotaWindow[];
10
+ error?: string;
11
+ };
12
+ export declare function parseClaudeUsage(text: string): QuotaWindow[];
13
+ type LimitWindow = {
14
+ usedPercent?: number;
15
+ windowDurationMins?: number;
16
+ resetsAt?: number;
17
+ };
18
+ type RateLimits = {
19
+ primary?: LimitWindow | null;
20
+ secondary?: LimitWindow | null;
21
+ };
22
+ export declare function codexWindows(limits: RateLimits): QuotaWindow[];
23
+ export declare function claudeQuota(): Promise<QuotaReport>;
24
+ export declare function codexQuota(): Promise<QuotaReport>;
25
+ export {};
package/dist/quota.js ADDED
@@ -0,0 +1,141 @@
1
+ import { spawn } from "node:child_process";
2
+ export function parseClaudeUsage(text) {
3
+ const session = text.match(/Current session:\s*(\d+(?:\.\d+)?)%\s*used\s*·\s*resets\s+(.+)/);
4
+ const week = text.match(/Current week(?: \([^)]*\))?:\s*(\d+(?:\.\d+)?)%\s*used\s*·\s*resets\s+(.+)/);
5
+ const windows = [];
6
+ if (session)
7
+ windows.push(windowOf("這 5 小時", Number(session[1]), (session[2] ?? "").trim()));
8
+ if (week)
9
+ windows.push(windowOf("本週", Number(week[1]), (week[2] ?? "").trim()));
10
+ return windows;
11
+ }
12
+ export function codexWindows(limits) {
13
+ return [limits.primary, limits.secondary].flatMap((window) => {
14
+ if (!window || window.usedPercent === undefined)
15
+ return [];
16
+ const mins = window.windowDurationMins ?? 0;
17
+ const resets = window.resetsAt ? new Date(window.resetsAt * 1000).toLocaleString("zh-Hant", { month: "numeric", day: "numeric", hour: "2-digit", minute: "2-digit" }) : "時間不明";
18
+ return [windowOf(durationLabel(mins), window.usedPercent, resets)];
19
+ });
20
+ }
21
+ function durationLabel(mins) {
22
+ if (mins === 300)
23
+ return "這 5 小時";
24
+ if (mins === 10080)
25
+ return "7 天";
26
+ if (mins > 0 && mins % 1440 === 0)
27
+ return `${mins / 1440} 天`;
28
+ if (mins > 0 && mins % 60 === 0)
29
+ return `${mins / 60} 小時`;
30
+ return mins > 0 ? `${mins} 分鐘` : "用量";
31
+ }
32
+ function windowOf(label, used, resets) {
33
+ const usedPercent = roundPercent(used);
34
+ return { label, used_percent: usedPercent, remaining_percent: roundPercent(Math.max(0, 100 - used)), resets };
35
+ }
36
+ function roundPercent(n) {
37
+ return Math.round(n * 10) / 10;
38
+ }
39
+ export async function claudeQuota() {
40
+ try {
41
+ const text = await capture("claude", ["-p", "--no-session-persistence", "--output-format", "text", "/usage"], 20_000);
42
+ const windows = parseClaudeUsage(text.stdout);
43
+ if (windows.length === 0)
44
+ return { runtime: "claude", windows: [], error: firstLine(text.stderr) || firstLine(text.stdout) || "claude /usage 沒有回傳額度。" };
45
+ return { runtime: "claude", windows };
46
+ }
47
+ catch (error) {
48
+ return { runtime: "claude", windows: [], error: missing("claude", error) };
49
+ }
50
+ }
51
+ export async function codexQuota() {
52
+ try {
53
+ const limits = await readCodexLimits();
54
+ return { runtime: "codex", windows: codexWindows(limits) };
55
+ }
56
+ catch (error) {
57
+ return { runtime: "codex", windows: [], error: missing("codex", error) };
58
+ }
59
+ }
60
+ function missing(cmd, error) {
61
+ const code = error.code;
62
+ if (code === "ENOENT")
63
+ return `找不到 ${cmd}。`;
64
+ return error.message || `${cmd} 沒有回傳額度。`;
65
+ }
66
+ function firstLine(text) {
67
+ return text.split("\n").map((line) => line.trim()).find(Boolean) ?? "";
68
+ }
69
+ function capture(cmd, args, timeoutMs) {
70
+ return new Promise((resolve, reject) => {
71
+ const child = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
72
+ let stdout = "";
73
+ let stderr = "";
74
+ const timer = setTimeout(() => {
75
+ child.kill();
76
+ reject(new Error(`${cmd} 超過 ${Math.round(timeoutMs / 1000)} 秒沒有回應。`));
77
+ }, timeoutMs);
78
+ child.stdout.on("data", (chunk) => (stdout += String(chunk)));
79
+ child.stderr.on("data", (chunk) => (stderr += String(chunk)));
80
+ child.on("error", (error) => {
81
+ clearTimeout(timer);
82
+ reject(error);
83
+ });
84
+ child.on("close", () => {
85
+ clearTimeout(timer);
86
+ resolve({ stdout, stderr });
87
+ });
88
+ });
89
+ }
90
+ function readCodexLimits() {
91
+ return new Promise((resolve, reject) => {
92
+ const child = spawn("codex", ["app-server", "--stdio"], { stdio: ["pipe", "pipe", "pipe"] });
93
+ let buf = "";
94
+ const timer = setTimeout(() => {
95
+ child.kill();
96
+ reject(new Error("codex 超過 20 秒沒有回應。"));
97
+ }, 20_000);
98
+ let done = false;
99
+ const finish = (error, limits) => {
100
+ if (done)
101
+ return;
102
+ done = true;
103
+ clearTimeout(timer);
104
+ child.kill();
105
+ if (error)
106
+ reject(error);
107
+ else
108
+ resolve(limits ?? {});
109
+ };
110
+ child.on("error", (error) => finish(error));
111
+ child.stdout.on("data", (chunk) => {
112
+ buf += String(chunk);
113
+ let cut = buf.indexOf("\n");
114
+ while (cut >= 0) {
115
+ const line = buf.slice(0, cut).trim();
116
+ buf = buf.slice(cut + 1);
117
+ cut = buf.indexOf("\n");
118
+ if (!line)
119
+ continue;
120
+ let message;
121
+ try {
122
+ message = JSON.parse(line);
123
+ }
124
+ catch {
125
+ continue;
126
+ }
127
+ if (message.id === 1 && message.result) {
128
+ child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", method: "initialized", params: {} })}\n`);
129
+ child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id: 2, method: "account/rateLimits/read", params: {} })}\n`);
130
+ }
131
+ if (message.id === 2) {
132
+ if (message.error)
133
+ finish(new Error(message.error.message || "codex 沒有回傳額度。"));
134
+ else
135
+ finish(undefined, message.result?.rateLimits ?? {});
136
+ }
137
+ }
138
+ });
139
+ child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id: 1, method: "initialize", params: { clientInfo: { name: "agent-lyceum", version: "0" }, capabilities: {} } })}\n`);
140
+ });
141
+ }
@@ -13,7 +13,9 @@ export type WatchFrame = {
13
13
  * A task that was running in the previous frame and has completed since is included once more, so the reader sees
14
14
  * it finish (and can toast) before it is dropped. `tracked` is that previous set; it is replaced by the ids running now.
15
15
  */
16
- export declare function buildWatchFrame(project: ResolvedProject, tracked: Set<string>): WatchFrame;
16
+ export declare function buildWatchFrame(project: ResolvedProject, tracked: Set<string>, opts?: {
17
+ allTasks?: boolean;
18
+ }): WatchFrame;
17
19
  export interface WatchDeps {
18
20
  /** The project, loaded again each time so config edits and new runs show up. */
19
21
  load: () => ResolvedProject;
@@ -21,6 +23,8 @@ export interface WatchDeps {
21
23
  dir: (project: ResolvedProject) => string;
22
24
  write: (line: string) => void;
23
25
  debounceMs: number;
26
+ /** Include finished tasks. The browser page uses this; `status --json --watch` does not. */
27
+ allTasks?: boolean;
24
28
  watch?: (dir: string, onChange: () => void) => {
25
29
  close: () => void;
26
30
  };
@@ -6,12 +6,14 @@ import { buildStatusReport, buildTaskListReport, isCompletedRun } from "./status
6
6
  * A task that was running in the previous frame and has completed since is included once more, so the reader sees
7
7
  * it finish (and can toast) before it is dropped. `tracked` is that previous set; it is replaced by the ids running now.
8
8
  */
9
- export function buildWatchFrame(project, tracked) {
9
+ export function buildWatchFrame(project, tracked, opts) {
10
10
  const all = buildTaskListReport(project).runs;
11
11
  const running = all.filter((r) => r.state === "running").map((r) => r.run_id);
12
12
  const open = all.filter((r) => !isCompletedRun(r)).map((r) => r.run_id);
13
13
  const runs = {};
14
- for (const id of new Set([...open, ...tracked]))
14
+ // The web page keeps every task. The terminal watch drops one after it has completed, once the finish has been seen.
15
+ const ids = opts?.allTasks ? all.map((r) => r.run_id) : [...open, ...tracked];
16
+ for (const id of new Set(ids))
15
17
  runs[id] = buildStatusReport(project, id);
16
18
  const report = buildStatusReport(project);
17
19
  tracked.clear();
@@ -39,7 +41,7 @@ export function createStatusWatcher(d) {
39
41
  let project;
40
42
  try {
41
43
  project = d.load();
42
- frame = buildWatchFrame(project, tracked);
44
+ frame = buildWatchFrame(project, tracked, { allTasks: d.allTasks });
43
45
  }
44
46
  catch (e) {
45
47
  frame = { schema_version: 1, error: e.message };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-lyceum",
3
- "version": "0.28.0",
3
+ "version": "0.29.0",
4
4
  "description": "Config + dispatcher for teams of Claude Code / Codex agents with scoped context and file-based mailboxes",
5
5
  "keywords": [
6
6
  "ai",
@@ -27,7 +27,9 @@
27
27
  },
28
28
  "files": [
29
29
  "dist",
30
- "!dist/**/*.map"
30
+ "!dist/**/*.map",
31
+ "web/dist",
32
+ "!web/dist/**/*.map"
31
33
  ],
32
34
  "engines": {
33
35
  "node": ">=20"
@@ -41,7 +43,7 @@
41
43
  "typecheck": "tsc -p tsconfig.json --noEmit && npm run typecheck:plugin",
42
44
  "test": "vitest run",
43
45
  "prepare": "npm run build",
44
- "prepublishOnly": "npm run typecheck && npm test && npm run build",
46
+ "prepublishOnly": "npm run typecheck && npm test && npm run build && pnpm --dir web install --frozen-lockfile && pnpm --dir web typecheck && pnpm --dir web build",
45
47
  "release": "bash scripts/release.sh",
46
48
  "typecheck:plugin": "tsc -p plugins/status-monitor/tsconfig.ci.json",
47
49
  "check:pack": "node scripts/check-pack.mjs",
@@ -0,0 +1,2 @@
1
+ /*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */
2
+ @layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-space-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-duration:initial;--tw-animation-delay:0s;--tw-animation-direction:normal;--tw-animation-duration:initial;--tw-animation-fill-mode:none;--tw-animation-iteration-count:1;--tw-enter-blur:0;--tw-enter-opacity:1;--tw-enter-rotate:0;--tw-enter-scale:1;--tw-enter-translate-x:0;--tw-enter-translate-y:0;--tw-exit-blur:0;--tw-exit-opacity:1;--tw-exit-rotate:0;--tw-exit-scale:1;--tw-exit-translate-x:0;--tw-exit-translate-y:0}}}@layer theme{:root,:host{--font-sans:Futura, "Century Gothic", "Avenir Next", "Trebuchet MS", sans-serif;--font-serif:"Hoefler Text", "Iowan Old Style", Baskerville, "Palatino Linotype", serif;--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--spacing:.25rem;--container-xl:36rem;--container-2xl:42rem;--container-3xl:48rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-4xl:2.25rem;--text-4xl--line-height:calc(2.5 / 2.25);--text-5xl:3rem;--text-5xl--line-height:1;--font-weight-medium:500;--font-weight-bold:700;--leading-tight:1.25;--leading-snug:1.375;--leading-relaxed:1.625;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-ink:#1a2330;--color-paper:#f7f8f9;--color-wall:#e4e7ea;--color-signal:#0f6f6a;--color-working:#0f6f6a;--color-failed:#8e3434;--color-line:#c5cad1}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring:where(:not(iframe)){outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.top-0{top:0}.-left-8{left:calc(var(--spacing) * -8)}.left-1\/2{left:50%}.z-10{z-index:10}.mt-0\.5{margin-top:calc(var(--spacing) * .5)}.mt-1{margin-top:var(--spacing)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mt-6{margin-top:calc(var(--spacing) * 6)}.mt-10{margin-top:calc(var(--spacing) * 10)}.mt-auto{margin-top:auto}.ml-3{margin-left:calc(var(--spacing) * 3)}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.size-1\.5{width:calc(var(--spacing) * 1.5);height:calc(var(--spacing) * 1.5)}.size-8{width:calc(var(--spacing) * 8);height:calc(var(--spacing) * 8)}.h-2{height:calc(var(--spacing) * 2)}.h-7{height:calc(var(--spacing) * 7)}.h-8{height:calc(var(--spacing) * 8)}.h-dvh{height:100dvh}.h-full{height:100%}.min-h-0{min-height:0}.w-20{width:calc(var(--spacing) * 20)}.w-full{width:100%}.max-w-2xl{max-width:var(--container-2xl)}.max-w-3xl{max-width:var(--container-3xl)}.max-w-xl{max-width:var(--container-xl)}.min-w-0{min-width:0}.min-w-36{min-width:calc(var(--spacing) * 36)}.flex-1{flex:1}.shrink-0{flex-shrink:0}.-translate-x-1\/2{--tw-translate-x:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.animate-in{animation:enter var(--tw-animation-duration,var(--tw-duration,.15s))var(--tw-ease,ease)var(--tw-animation-delay,0s)var(--tw-animation-iteration-count,1)var(--tw-animation-direction,normal)var(--tw-animation-fill-mode,none)}.list-decimal{list-style-type:decimal}.list-none{list-style-type:none}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-\[7rem_minmax\(0\,1fr\)\]{grid-template-columns:7rem minmax(0,1fr)}.flex-wrap{flex-wrap:wrap}.items-baseline{align-items:baseline}.items-center{align-items:center}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-10{gap:calc(var(--spacing) * 10)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(var(--spacing) * var(--tw-space-y-reverse));margin-block-end:calc(var(--spacing) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}.gap-x-3{column-gap:calc(var(--spacing) * 3)}.gap-x-6{column-gap:calc(var(--spacing) * 6)}.gap-x-8{column-gap:calc(var(--spacing) * 8)}.gap-y-1{row-gap:var(--spacing)}.gap-y-4{row-gap:calc(var(--spacing) * 4)}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.rounded-full{border-radius:2147483647px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-l-2{border-left-style:var(--tw-border-style);border-left-width:2px}.border-failed{border-color:var(--color-failed)}.border-ink{border-color:var(--color-ink)}.border-ink\/60{border-color:#1a233099}@supports (color:color-mix(in lab, red, red)){.border-ink\/60{border-color:color-mix(in oklab, var(--color-ink) 60%, transparent)}}.border-line{border-color:var(--color-line)}.border-signal{border-color:var(--color-signal)}.border-transparent{border-color:#0000}.border-l-ink{border-left-color:var(--color-ink)}.border-l-signal{border-left-color:var(--color-signal)}.bg-ink{background-color:var(--color-ink)}.bg-ink\/30{background-color:#1a23304d}@supports (color:color-mix(in lab, red, red)){.bg-ink\/30{background-color:color-mix(in oklab, var(--color-ink) 30%, transparent)}}.bg-line{background-color:var(--color-line)}.bg-paper{background-color:var(--color-paper)}.bg-signal{background-color:var(--color-signal)}.bg-wall{background-color:var(--color-wall)}.fill-failed{fill:var(--color-failed)}.fill-ink{fill:var(--color-ink)}.fill-none{fill:none}.stroke-line{stroke:var(--color-line)}.stroke-working{stroke:var(--color-working)}.p-0{padding:0}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.p-6{padding:calc(var(--spacing) * 6)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.py-1{padding-block:var(--spacing)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-3{padding-block:calc(var(--spacing) * 3)}.pt-2{padding-top:calc(var(--spacing) * 2)}.inset-safe{padding-bottom:env(safe-area-inset-bottom);padding-left:env(safe-area-inset-left)}.pb-2{padding-bottom:calc(var(--spacing) * 2)}.pb-4{padding-bottom:calc(var(--spacing) * 4)}.pl-3{padding-left:calc(var(--spacing) * 3)}.pl-4{padding-left:calc(var(--spacing) * 4)}.pl-5{padding-left:calc(var(--spacing) * 5)}.pl-8{padding-left:calc(var(--spacing) * 8)}.text-left{text-align:left}.font-serif{font-family:var(--font-serif)}.text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.leading-snug{--tw-leading:var(--leading-snug);line-height:var(--leading-snug)}.leading-tight{--tw-leading:var(--leading-tight);line-height:var(--leading-tight)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.text-balance{text-wrap:balance}.text-pretty{text-wrap:pretty}.wrap-break-word{overflow-wrap:break-word}.whitespace-pre-wrap{white-space:pre-wrap}.text-failed{color:var(--color-failed)}.text-ink{color:var(--color-ink)}.text-ink\/50{color:#1a233080}@supports (color:color-mix(in lab, red, red)){.text-ink\/50{color:color-mix(in oklab, var(--color-ink) 50%, transparent)}}.text-ink\/60{color:#1a233099}@supports (color:color-mix(in lab, red, red)){.text-ink\/60{color:color-mix(in oklab, var(--color-ink) 60%, transparent)}}.text-ink\/70{color:#1a2330b3}@supports (color:color-mix(in lab, red, red)){.text-ink\/70{color:color-mix(in oklab, var(--color-ink) 70%, transparent)}}.text-ink\/80{color:#1a2330cc}@supports (color:color-mix(in lab, red, red)){.text-ink\/80{color:color-mix(in oklab, var(--color-ink) 80%, transparent)}}.text-signal{color:var(--color-signal)}.text-working{color:var(--color-working)}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.line-through{text-decoration-line:line-through}.underline{text-decoration-line:underline}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-200{--tw-duration:.2s;transition-duration:.2s}.\!running{animation-play-state:running!important}.enter{--tw-ease:ease-out}.fade-in{--tw-enter-opacity:0}.running{animation-play-state:running}.group-data-\[open\]\:hidden:is(:where(.group)[data-open] *){display:none}.group-data-\[open\]\:inline:is(:where(.group)[data-open] *){display:inline}.first\:mt-0:first-child{margin-top:0}@media (hover:hover){.hover\:bg-paper\/60:hover{background-color:#f7f8f999}@supports (color:color-mix(in lab, red, red)){.hover\:bg-paper\/60:hover{background-color:color-mix(in oklab, var(--color-paper) 60%, transparent)}}}.disabled\:opacity-60:disabled{opacity:.6}@media (width>=40rem){.sm\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}@media (width>=48rem){.md\:block{display:block}.md\:h-dvh{height:100dvh}.md\:min-w-0{min-width:0}.md\:grid-cols-\[15rem_minmax\(0\,1fr\)\]{grid-template-columns:15rem minmax(0,1fr)}.md\:flex-col{flex-direction:column}.md\:overflow-y-auto{overflow-y:auto}.md\:border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.md\:border-b-0{border-bottom-style:var(--tw-border-style);border-bottom-width:0}.md\:text-5xl{font-size:var(--text-5xl);line-height:var(--tw-leading,var(--text-5xl--line-height))}}@media (width>=64rem){.lg\:sticky{position:sticky}.lg\:top-20{top:calc(var(--spacing) * 20)}.lg\:max-h-\[calc\(100dvh-5rem\)\]{max-height:calc(100dvh - 5rem)}.lg\:grid-cols-\[minmax\(0\,1fr\)_18rem\]{grid-template-columns:minmax(0,1fr) 18rem}.lg\:overflow-y-auto{overflow-y:auto}.lg\:p-8{padding:calc(var(--spacing) * 8)}}}@property --tw-animation-delay{syntax:"*";inherits:false;initial-value:0s}@property --tw-animation-direction{syntax:"*";inherits:false;initial-value:normal}@property --tw-animation-duration{syntax:"*";inherits:false}@property --tw-animation-fill-mode{syntax:"*";inherits:false;initial-value:none}@property --tw-animation-iteration-count{syntax:"*";inherits:false;initial-value:1}@property --tw-enter-blur{syntax:"*";inherits:false;initial-value:0}@property --tw-enter-opacity{syntax:"*";inherits:false;initial-value:1}@property --tw-enter-rotate{syntax:"*";inherits:false;initial-value:0}@property --tw-enter-scale{syntax:"*";inherits:false;initial-value:1}@property --tw-enter-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-enter-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-blur{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-opacity{syntax:"*";inherits:false;initial-value:1}@property --tw-exit-rotate{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-scale{syntax:"*";inherits:false;initial-value:1}@property --tw-exit-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-translate-y{syntax:"*";inherits:false;initial-value:0}body{font-family:var(--font-sans);color:var(--color-ink);background:var(--color-wall);margin:0}@media (prefers-reduced-motion:reduce){.enter{animation:none}}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@keyframes enter{0%{opacity:var(--tw-enter-opacity,1);transform:translate3d(var(--tw-enter-translate-x,0),var(--tw-enter-translate-y,0),0)scale3d(var(--tw-enter-scale,1),var(--tw-enter-scale,1),var(--tw-enter-scale,1))rotate(var(--tw-enter-rotate,0));filter:blur(var(--tw-enter-blur,0))}}