@nettee/beacon 0.3.2 → 0.3.5

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
@@ -111,16 +111,16 @@ per-Run directories with mode `0700` when Pi starts.
111
111
 
112
112
  Configuration is strict: unknown YAML/JSON fields, YAML aliases or warnings, missing paths, duplicate Schedule IDs, invalid timezones/cron expressions, a missing complete `persona.md` + `task.md` pair, escaped Profile markdown, permissive secret or runtime-environment permissions, missing Profile credentials, and unknown Feishu channel names all fail startup. Beacon validates all Profiles before opening a Feishu connection.
113
113
 
114
- Feishu delivery destinations are registered globally in `~/.beacon/channels.yaml`
115
- (beside `config.yaml`), then referenced by semantic `name` from each Profile:
114
+ Group notify destinations are registered globally in `~/.beacon/channels.yaml`
115
+ (beside `config.yaml`), then referenced by semantic `name` from each Profile's
116
+ `notify`. Admin DMs are **not** channel entries: each Profile keeps a raw
117
+ `admin.chat_id` (one bot×person private chat; putting those in the registry
118
+ would explode as bots and people grow).
116
119
 
117
120
  ```yaml
118
- # ~/.beacon/channels.yaml
121
+ # ~/.beacon/channels.yaml — group chats only
119
122
  version: 1
120
123
  channels:
121
- - name: admin-dm
122
- description: Example admin direct chat
123
- chat_id: REPLACE_WITH_ADMIN_DIRECT_CHAT_ID
124
124
  - name: weekday-brief-group
125
125
  description: Example weekday brief notify group
126
126
  chat_id: REPLACE_WITH_GROUP_CHAT_ID
@@ -129,7 +129,7 @@ channels:
129
129
  ```yaml
130
130
  # profile.yaml destinations
131
131
  admin:
132
- name: admin-dm
132
+ chat_id: REPLACE_WITH_ADMIN_DIRECT_CHAT_ID
133
133
  schedules:
134
134
  - id: weekday-brief
135
135
  cron: "0 9 * * 1-5"
@@ -140,11 +140,12 @@ schedules:
140
140
  ```
141
141
 
142
142
  Each channel has `name` (stable slug), `description` (human-readable Chinese or
143
- English label), and `chat_id` (the real Feishu chat id). Profile destinations
144
- bind `name` and may optionally override `description` for local readability;
143
+ English label), and `chat_id` (the real Feishu group chat id). Profile `notify`
144
+ binds `name` and may optionally override `description` for local readability;
145
145
  Beacon resolves `name` → `chat_id` at load time. Unknown names fail startup.
146
- Legacy `chat_id` directly under `admin` / `notify` is still accepted so existing
147
- hosts can migrate one Profile at a time; new Profiles should use `name`.
146
+ `admin` must use bare `chat_id` (not a channel `name`). Legacy bare `chat_id`
147
+ under `notify` still loads so hosts can migrate one Profile at a time; new
148
+ `notify` entries should use `name`.
148
149
 
149
150
  Each Schedule uses a five-field cron expression and an IANA timezone. Profile
150
151
  `admin` is the private chat used for schedule `reply_text` /
@@ -216,7 +217,7 @@ Subscribe in the desired `profile.yaml`:
216
217
 
217
218
  ```yaml
218
219
  admin:
219
- name: admin-dm
220
+ chat_id: REPLACE_WITH_ADMIN_DIRECT_CHAT_ID
220
221
  listener:
221
222
  sources:
222
223
  - https://deploy.example.com/production
@@ -10,6 +10,11 @@ const profileIdPattern = /^[a-z0-9](?:[a-z0-9_-]{0,62})$/;
10
10
  export const profilePersonaFile = "persona.md";
11
11
  export const profileTaskFile = "task.md";
12
12
  export const workspaceProfileDirectoryName = ".beacon-profile";
13
+ /** Admin DM: raw Feishu chat_id only (bot×person DMs are not channel registry entries). */
14
+ const adminDestinationSchema = z
15
+ .object({ chat_id: z.string().trim().min(1) })
16
+ .strict();
17
+ /** Group notify: prefer semantic name from ~/.beacon/channels.yaml; bare chat_id still loads. */
13
18
  const namedDestinationSchema = z
14
19
  .object({
15
20
  name: z
@@ -21,7 +26,7 @@ const namedDestinationSchema = z
21
26
  const legacyChatDestinationSchema = z
22
27
  .object({ chat_id: z.string().trim().min(1) })
23
28
  .strict();
24
- const destinationSchema = z.union([
29
+ const notifyDestinationSchema = z.union([
25
30
  namedDestinationSchema,
26
31
  legacyChatDestinationSchema,
27
32
  ]);
@@ -47,7 +52,7 @@ const scheduleSchema = z
47
52
  }
48
53
  }, "Schedule timezone must be a valid IANA timezone"),
49
54
  input: z.string().trim().min(1),
50
- notify: destinationSchema.optional(),
55
+ notify: notifyDestinationSchema.optional(),
51
56
  })
52
57
  .strict();
53
58
  const profileDocumentSchema = z
@@ -60,13 +65,13 @@ const profileDocumentSchema = z
60
65
  id: z.string().min(1),
61
66
  })
62
67
  .strict(),
63
- admin: destinationSchema.optional(),
68
+ admin: adminDestinationSchema.optional(),
64
69
  schedules: z.array(scheduleSchema).default([]),
65
70
  listener: z
66
71
  .object({
67
72
  sources: z.array(eventSourceSchema).min(1),
68
73
  types: z.array(z.string().min(1)).min(1),
69
- notify: destinationSchema.optional(),
74
+ notify: notifyDestinationSchema.optional(),
70
75
  })
71
76
  .strict()
72
77
  .optional(),
@@ -163,7 +168,7 @@ async function loadPersonaAndTask(profileId, workspace, profileDirectory) {
163
168
  ];
164
169
  throw new Error(`Profile ${profileId} has no complete ${profilePersonaFile} + ${profileTaskFile} pair. Missing: ${missing.join(", ")}`);
165
170
  }
166
- function resolveDestination(destination, channels, channelsPath) {
171
+ function resolveNotifyDestination(destination, channels, channelsPath) {
167
172
  if ("chat_id" in destination) {
168
173
  return { chatId: destination.chat_id };
169
174
  }
@@ -202,9 +207,7 @@ export async function loadProfile(profileId, profilesDirectory = join(homedir(),
202
207
  if (config.listener && !config.admin) {
203
208
  throw new Error(`Profile ${profileId} with listener must declare admin`);
204
209
  }
205
- const admin = config.admin
206
- ? resolveDestination(config.admin, channels, channelsPath)
207
- : undefined;
210
+ const admin = config.admin ? { chatId: config.admin.chat_id } : undefined;
208
211
  const canonicalProfileDirectory = await realpath(profileDirectory);
209
212
  const workspace = isAbsolute(config.workspace)
210
213
  ? config.workspace
@@ -238,7 +241,7 @@ export async function loadProfile(profileId, profilesDirectory = join(homedir(),
238
241
  types: config.listener.types,
239
242
  ...(config.listener.notify
240
243
  ? {
241
- notify: resolveDestination(config.listener.notify, channels, channelsPath),
244
+ notify: resolveNotifyDestination(config.listener.notify, channels, channelsPath),
242
245
  }
243
246
  : {}),
244
247
  },
@@ -251,7 +254,7 @@ export async function loadProfile(profileId, profilesDirectory = join(homedir(),
251
254
  input: schedule.input,
252
255
  ...(schedule.notify
253
256
  ? {
254
- notify: resolveDestination(schedule.notify, channels, channelsPath),
257
+ notify: resolveNotifyDestination(schedule.notify, channels, channelsPath),
255
258
  }
256
259
  : {}),
257
260
  })),
@@ -0,0 +1,173 @@
1
+ import { readdir, stat } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { z } from "zod";
4
+ import { channelNamePattern } from "../config/channels.js";
5
+ import { parseStrictYaml } from "../config/yaml.js";
6
+ const profileIdPattern = /^[a-z0-9](?:[a-z0-9_-]{0,62})$/;
7
+ const adminDestinationSchema = z
8
+ .object({ chat_id: z.string().trim().min(1) })
9
+ .strict();
10
+ const namedDestinationSchema = z
11
+ .object({
12
+ name: z
13
+ .string()
14
+ .regex(channelNamePattern, "Channel name must be a lowercase slug (a-z, 0-9, _, -)"),
15
+ description: z.string().trim().min(1).optional(),
16
+ })
17
+ .strict();
18
+ const legacyChatDestinationSchema = z
19
+ .object({ chat_id: z.string().trim().min(1) })
20
+ .strict();
21
+ const notifyDestinationSchema = z.union([
22
+ namedDestinationSchema,
23
+ legacyChatDestinationSchema,
24
+ ]);
25
+ const scheduleSchema = z
26
+ .object({
27
+ id: z.string().regex(profileIdPattern),
28
+ cron: z.string().trim().min(1),
29
+ timezone: z.string().min(1),
30
+ input: z.string().trim().min(1),
31
+ notify: notifyDestinationSchema.optional(),
32
+ })
33
+ .strict();
34
+ const profileDocumentSchema = z
35
+ .object({
36
+ workspace: z.string().min(1),
37
+ runtime: z.literal("pi"),
38
+ model: z
39
+ .object({
40
+ provider: z.string().min(1),
41
+ id: z.string().min(1),
42
+ })
43
+ .strict(),
44
+ admin: adminDestinationSchema.optional(),
45
+ schedules: z.array(scheduleSchema).default([]),
46
+ listener: z
47
+ .object({
48
+ sources: z.array(z.string().min(1)).min(1),
49
+ types: z.array(z.string().min(1)).min(1),
50
+ notify: notifyDestinationSchema.optional(),
51
+ })
52
+ .strict()
53
+ .optional(),
54
+ })
55
+ .strict();
56
+ function toNotifyView(notify) {
57
+ if (!notify)
58
+ return null;
59
+ if ("name" in notify) {
60
+ return {
61
+ kind: "channel",
62
+ name: notify.name,
63
+ ...(notify.description ? { description: notify.description } : {}),
64
+ };
65
+ }
66
+ return { kind: "chat_id", chatId: notify.chat_id };
67
+ }
68
+ function toDetailView(profileId, document) {
69
+ return {
70
+ id: profileId,
71
+ workspace: document.workspace,
72
+ runtime: document.runtime,
73
+ model: document.model,
74
+ admin: document.admin ? { chatId: document.admin.chat_id } : null,
75
+ schedules: document.schedules.map((schedule) => ({
76
+ id: schedule.id,
77
+ cron: schedule.cron,
78
+ timezone: schedule.timezone,
79
+ input: schedule.input,
80
+ notify: toNotifyView(schedule.notify),
81
+ })),
82
+ listener: document.listener
83
+ ? {
84
+ sources: document.listener.sources,
85
+ types: document.listener.types,
86
+ notify: toNotifyView(document.listener.notify),
87
+ }
88
+ : null,
89
+ secrets: {
90
+ present: false,
91
+ note: "Feishu app credentials are stored in secrets.json and are not exposed by the dashboard.",
92
+ },
93
+ };
94
+ }
95
+ export function isProfileId(value) {
96
+ return profileIdPattern.test(value);
97
+ }
98
+ async function readProfileYaml(profilesDirectory, profileId) {
99
+ const configPath = join(profilesDirectory, profileId, "profile.yaml");
100
+ return profileDocumentSchema.parse(await parseStrictYaml(configPath));
101
+ }
102
+ export async function listProfileViews(profilesDirectory) {
103
+ let names;
104
+ try {
105
+ names = await readdir(profilesDirectory);
106
+ }
107
+ catch (error) {
108
+ throw new Error(`Cannot read Beacon profiles directory ${profilesDirectory}`, { cause: error });
109
+ }
110
+ const ids = [];
111
+ for (const name of names) {
112
+ if (!isProfileId(name))
113
+ continue;
114
+ try {
115
+ const metadata = await stat(join(profilesDirectory, name));
116
+ if (metadata.isDirectory())
117
+ ids.push(name);
118
+ }
119
+ catch {
120
+ // Skip unreadable entries.
121
+ }
122
+ }
123
+ ids.sort((left, right) => left.localeCompare(right));
124
+ const items = [];
125
+ for (const id of ids) {
126
+ try {
127
+ const document = await readProfileYaml(profilesDirectory, id);
128
+ items.push({
129
+ id,
130
+ workspace: document.workspace,
131
+ runtime: document.runtime,
132
+ model: document.model,
133
+ scheduleCount: document.schedules.length,
134
+ hasAdmin: Boolean(document.admin),
135
+ hasListener: Boolean(document.listener),
136
+ error: null,
137
+ });
138
+ }
139
+ catch (error) {
140
+ items.push({
141
+ id,
142
+ workspace: "",
143
+ runtime: "pi",
144
+ model: { provider: "", id: "" },
145
+ scheduleCount: 0,
146
+ hasAdmin: false,
147
+ hasListener: false,
148
+ error: error instanceof Error ? error.message : String(error),
149
+ });
150
+ }
151
+ }
152
+ return items;
153
+ }
154
+ export async function getProfileDetailView(profilesDirectory, profileId) {
155
+ if (!isProfileId(profileId))
156
+ return null;
157
+ const directory = join(profilesDirectory, profileId);
158
+ try {
159
+ const metadata = await stat(directory);
160
+ if (!metadata.isDirectory())
161
+ return null;
162
+ }
163
+ catch {
164
+ return null;
165
+ }
166
+ try {
167
+ const document = await readProfileYaml(profilesDirectory, profileId);
168
+ return toDetailView(profileId, document);
169
+ }
170
+ catch (error) {
171
+ throw new Error(`Cannot load Profile ${profileId}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
172
+ }
173
+ }
@@ -5,6 +5,7 @@ import { defaultChannelsPath, loadChannelRegistry, } from "../config/channels.js
5
5
  import { loadProfile } from "../config/profile.js";
6
6
  import { buildAgentSystemPrompt, } from "../runtime/system-prompt.js";
7
7
  import { exportSessionHtml } from "./export.js";
8
+ import { getProfileDetailView, isProfileId, listProfileViews, } from "./profiles.js";
8
9
  import { findRunSummary, listRunSummaries, runIdPattern, toRunListItem, } from "./records.js";
9
10
  import { sendStaticFile } from "./static.js";
10
11
  function send(response, status, contentType, body) {
@@ -68,6 +69,32 @@ export async function startDashboard(options) {
68
69
  send(response, 200, "application/json; charset=utf-8", `${JSON.stringify({ runs: rows.map(toRunListItem) })}\n`);
69
70
  return;
70
71
  }
72
+ if (url.pathname === "/api/profiles") {
73
+ const profiles = await listProfileViews(options.profilesDirectory);
74
+ send(response, 200, "application/json; charset=utf-8", `${JSON.stringify({ profiles })}\n`);
75
+ return;
76
+ }
77
+ const profileMatch = /^\/api\/profiles\/([^/]+)$/.exec(url.pathname);
78
+ if (profileMatch) {
79
+ const profileId = decodeURIComponent(profileMatch[1] ?? "");
80
+ if (!isProfileId(profileId)) {
81
+ sendText(response, 404, "Unknown profile");
82
+ return;
83
+ }
84
+ try {
85
+ const profile = await getProfileDetailView(options.profilesDirectory, profileId);
86
+ if (!profile) {
87
+ sendText(response, 404, `Unknown profile ${profileId}`);
88
+ return;
89
+ }
90
+ send(response, 200, "application/json; charset=utf-8", `${JSON.stringify({ profile })}\n`);
91
+ }
92
+ catch (error) {
93
+ const message = error instanceof Error ? error.message : String(error);
94
+ sendText(response, 500, message);
95
+ }
96
+ return;
97
+ }
71
98
  const match = /^\/runs\/([^/]+)$/.exec(url.pathname);
72
99
  if (match) {
73
100
  const runId = decodeURIComponent(match[1] ?? "");
@@ -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-space-y-reverse:0;--tw-border-style:solid;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial}}}@layer theme{:root,:host{--font-sans:-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-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-100:oklch(93.6% .032 17.717);--color-red-600:oklch(57.7% .245 27.325);--color-red-800:oklch(44.4% .177 26.899);--color-amber-100:oklch(96.2% .059 95.617);--color-amber-600:oklch(66.6% .179 58.318);--color-amber-800:oklch(47.3% .137 46.201);--color-emerald-700:oklch(50.8% .118 165.612);--color-sky-50:oklch(97.7% .013 236.62);--color-sky-100:oklch(95.1% .026 236.824);--color-sky-200:oklch(90.1% .058 230.902);--color-sky-300:oklch(82.8% .111 230.318);--color-sky-500:oklch(68.5% .169 237.323);--color-sky-600:oklch(58.8% .158 241.966);--color-sky-700:oklch(50% .134 242.749);--color-sky-800:oklch(44.3% .11 240.79);--color-sky-900:oklch(39.1% .09 240.876);--color-zinc-50:oklch(98.5% 0 none);--color-zinc-100:oklch(96.7% .001 286.375);--color-zinc-200:oklch(92% .004 286.32);--color-zinc-300:oklch(87.1% .006 286.286);--color-zinc-400:oklch(70.5% .015 286.067);--color-zinc-500:oklch(55.2% .016 285.938);--color-zinc-600:oklch(44.2% .017 285.786);--color-zinc-700:oklch(37% .013 285.805);--color-zinc-800:oklch(27.4% .006 286.033);--color-zinc-900:oklch(21% .006 285.885);--color-white:#fff;--spacing:.25rem;--container-3xl:48rem;--container-4xl:56rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--font-weight-medium:500;--font-weight-semibold:600;--tracking-tight:-.025em;--tracking-normal:0em;--tracking-wide:.025em;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--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)}}@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}.top-full{top:100%}.left-0{left:0}.z-10{z-index:10}.z-30{z-index:30}.mx-auto{margin-inline:auto}.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)}.ml-1{margin-left:var(--spacing)}.ml-2{margin-left:calc(var(--spacing) * 2)}.block{display:block}.flex{display:flex}.grid{display:grid}.inline-flex{display:inline-flex}.h-3{height:calc(var(--spacing) * 3)}.max-h-72{max-height:calc(var(--spacing) * 72)}.min-h-screen{min-height:100vh}.w-3{width:calc(var(--spacing) * 3)}.w-64{width:calc(var(--spacing) * 64)}.w-full{width:100%}.max-w-3xl{max-width:var(--container-3xl)}.max-w-4xl{max-width:var(--container-4xl)}.min-w-0{min-width:0}.min-w-52{min-width:calc(var(--spacing) * 52)}.min-w-\[720px\]{min-width:720px}.min-w-\[960px\]{min-width:960px}.flex-1{flex:1}.shrink-0{flex-shrink:0}.grow{flex-grow:1}.border-collapse{border-collapse:collapse}.cursor-pointer{cursor:pointer}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-end{align-items:flex-end}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.gap-1{gap:var(--spacing)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}: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-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)))}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.overflow-x-auto{overflow-x:auto}.rounded{border-radius:.25rem}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-xl{border-radius:var(--radius-xl)}.border{border-style:var(--tw-border-style);border-width:1px}.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-dashed{--tw-border-style:dashed;border-style:dashed}.border-zinc-100{border-color:var(--color-zinc-100)}.border-zinc-200{border-color:var(--color-zinc-200)}.border-zinc-300{border-color:var(--color-zinc-300)}.bg-amber-100{background-color:var(--color-amber-100)}.bg-red-100{background-color:var(--color-red-100)}.bg-sky-100{background-color:var(--color-sky-100)}.bg-sky-200{background-color:var(--color-sky-200)}.bg-white{background-color:var(--color-white)}.bg-white\/90{background-color:#ffffffe6}@supports (color:color-mix(in lab, red, red)){.bg-white\/90{background-color:color-mix(in oklab, var(--color-white) 90%, transparent)}}.bg-zinc-50{background-color:var(--color-zinc-50)}.bg-zinc-50\/80{background-color:#fafafacc}@supports (color:color-mix(in lab, red, red)){.bg-zinc-50\/80{background-color:color-mix(in oklab, var(--color-zinc-50) 80%, transparent)}}.px-1{padding-inline:var(--spacing)}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-5{padding-inline:calc(var(--spacing) * 5)}.px-6{padding-inline:calc(var(--spacing) * 6)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:var(--spacing)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-4{padding-block:calc(var(--spacing) * 4)}.py-6{padding-block:calc(var(--spacing) * 6)}.py-8{padding-block:calc(var(--spacing) * 8)}.py-10{padding-block:calc(var(--spacing) * 10)}.pt-0{padding-top:0}.pt-4{padding-top:calc(var(--spacing) * 4)}.pb-3{padding-bottom:calc(var(--spacing) * 3)}.text-center{text-align:center}.text-left{text-align:left}.font-mono{font-family:var(--font-mono)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--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-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-\[0\.2em\]{--tw-tracking:.2em;letter-spacing:.2em}.tracking-normal{--tw-tracking:var(--tracking-normal);letter-spacing:var(--tracking-normal)}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.break-all{word-break:break-all}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.text-amber-600{color:var(--color-amber-600)}.text-amber-800{color:var(--color-amber-800)}.text-emerald-700{color:var(--color-emerald-700)}.text-red-600{color:var(--color-red-600)}.text-red-800{color:var(--color-red-800)}.text-sky-600{color:var(--color-sky-600)}.text-sky-700{color:var(--color-sky-700)}.text-sky-800{color:var(--color-sky-800)}.text-sky-900{color:var(--color-sky-900)}.text-zinc-400{color:var(--color-zinc-400)}.text-zinc-500{color:var(--color-zinc-500)}.text-zinc-600{color:var(--color-zinc-600)}.text-zinc-700{color:var(--color-zinc-700)}.text-zinc-800{color:var(--color-zinc-800)}.text-zinc-900{color:var(--color-zinc-900)}.normal-case{text-transform:none}.uppercase{text-transform:uppercase}.opacity-80{opacity:.8}.shadow-xl{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a), 0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring-sky-500\/40{--tw-ring-color:#00a5ef66}@supports (color:color-mix(in lab, red, red)){.ring-sky-500\/40{--tw-ring-color:color-mix(in oklab, var(--color-sky-500) 40%, transparent)}}.backdrop-blur{--tw-backdrop-blur:blur(8px);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.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))}.outline-none{--tw-outline-style:none;outline-style:none}.placeholder\:text-zinc-400::placeholder{color:var(--color-zinc-400)}.first\:border-t-0:first-child{border-top-style:var(--tw-border-style);border-top-width:0}.first\:pt-0:first-child{padding-top:0}@media (hover:hover){.hover\:border-sky-300:hover{border-color:var(--color-sky-300)}.hover\:bg-sky-50\/40:hover{background-color:#f0f9ff66}@supports (color:color-mix(in lab, red, red)){.hover\:bg-sky-50\/40:hover{background-color:color-mix(in oklab, var(--color-sky-50) 40%, transparent)}}.hover\:bg-white:hover{background-color:var(--color-white)}.hover\:bg-zinc-50:hover{background-color:var(--color-zinc-50)}.hover\:bg-zinc-100:hover{background-color:var(--color-zinc-100)}.hover\:text-sky-900:hover{color:var(--color-sky-900)}.hover\:text-zinc-800:hover{color:var(--color-zinc-800)}.hover\:text-zinc-900:hover{color:var(--color-zinc-900)}}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}@media (width>=40rem){.sm\:col-span-2{grid-column:span 2/span 2}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (width>=48rem){.md\:sticky{position:sticky}.md\:top-0{top:0}.md\:flex{display:flex}.md\:h-screen{height:100vh}.md\:w-56{width:calc(var(--spacing) * 56)}.md\:flex-1{flex:1}.md\:flex-col{flex-direction:column}.md\:gap-0\.5{gap:calc(var(--spacing) * .5)}.md\:overflow-visible{overflow:visible}.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\:px-3{padding-inline:calc(var(--spacing) * 3)}.md\:px-5{padding-inline:calc(var(--spacing) * 5)}.md\:pt-5{padding-top:calc(var(--spacing) * 5)}.md\:pb-3{padding-bottom:calc(var(--spacing) * 3)}.md\:pb-4{padding-bottom:calc(var(--spacing) * 4)}}}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}