@hogsend/cli 0.2.2 → 0.6.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.
@@ -4,26 +4,51 @@ import { color } from "../lib/output.js";
4
4
  import type { Command, CommandContext } from "./types.js";
5
5
 
6
6
  const usage = `hogsend events <userId> [options]
7
+ hogsend events send <name> [options]
7
8
 
8
- Stream the event history for a single user, newest first. Wraps
9
- GET /v1/admin/events?userId=<userId>.
9
+ Read a single user's event history (admin API), or send an event into the data
10
+ plane to drive journeys/buckets.
10
11
 
11
- Arguments:
12
- <userId> The user (distinct) id to fetch events for. Required.
12
+ Read mode — hogsend events <userId>:
13
+ Stream the event history for a single user, newest first. Wraps
14
+ GET /v1/admin/events?userId=<userId>.
13
15
 
14
- Options:
15
- --event <name> Filter to a single event name.
16
- --from <iso> Only events at/after this ISO-8601 timestamp.
17
- --to <iso> Only events at/before this ISO-8601 timestamp.
18
- --limit <n> Max events to return (1-100, default 50).
19
- --offset <n> Pagination offset (default 0).
20
- --json Emit machine-readable JSON only.
21
- -h, --help Show this help.
16
+ Arguments:
17
+ <userId> The user (distinct) id to fetch events for. Required.
18
+
19
+ Options:
20
+ --event <name> Filter to a single event name.
21
+ --from <iso> Only events at/after this ISO-8601 timestamp.
22
+ --to <iso> Only events at/before this ISO-8601 timestamp.
23
+ --limit <n> Max events to return (1-100, default 50).
24
+ --offset <n> Pagination offset (default 0).
25
+
26
+ Send mode — hogsend events send <name>:
27
+ Push an event into POST /v1/events (data plane, ingest key). At least one of
28
+ --email / --user-id is required.
29
+
30
+ Options:
31
+ --email <addr> Recipient/identity email.
32
+ --user-id <id> External (distinct) id.
33
+ --prop <key=value> Event property; repeatable. Value parsed as JSON,
34
+ falling back to a string.
35
+ --props <json> Event properties as one JSON object.
36
+ --contact-prop <k=v> Contact property to merge onto the contact; repeatable.
37
+ --contact-props <json> Contact properties as one JSON object.
38
+ --list <id> Subscribe to a list; repeatable.
39
+ --unlist <id> Unsubscribe from a list; repeatable.
40
+ --idempotency-key <k> Dedup key (sent as the Idempotency-Key header).
41
+ --timestamp <iso> Override the event timestamp.
42
+
43
+ Global options (handled by the router): --url, --admin-key, --data-key, --json,
44
+ -h/--help.
22
45
 
23
46
  Examples:
24
47
  hogsend events user_123
25
48
  hogsend events user_123 --event signup --limit 10
26
- hogsend events user_123 --from 2026-01-01T00:00:00Z --json`;
49
+ hogsend events user_123 --from 2026-01-01T00:00:00Z --json
50
+ hogsend events send signup --user-id user_123 --prop plan=pro
51
+ hogsend events send purchase --email a@b.com --props '{"amount":49}' --json`;
27
52
 
28
53
  interface UserEvent {
29
54
  id: string;
@@ -40,9 +65,31 @@ interface EventsResponse {
40
65
  offset: number;
41
66
  }
42
67
 
68
+ /** Shape returned by POST /v1/events. */
69
+ interface ExitResult {
70
+ journeyId: string;
71
+ stateId: string;
72
+ exited: boolean;
73
+ }
74
+
75
+ interface SendResponse {
76
+ stored: boolean;
77
+ exits: ExitResult[];
78
+ }
79
+
43
80
  async function run(ctx: CommandContext): Promise<void> {
81
+ // `events send <name>` is the write path; everything else is the read path
82
+ // (bare `events <userId>`). Dispatch on the first positional WITHOUT a global
83
+ // --help short-circuit here, so `events send --help` still shows usage.
84
+ if (ctx.argv[0] === "send") {
85
+ return runSend(ctx, ctx.argv.slice(1));
86
+ }
87
+ return runRead(ctx, ctx.argv);
88
+ }
89
+
90
+ async function runRead(ctx: CommandContext, argv: string[]): Promise<void> {
44
91
  const { values, positionals } = parseArgs({
45
- args: ctx.argv,
92
+ args: argv,
46
93
  allowPositionals: true,
47
94
  options: {
48
95
  event: { type: "string" },
@@ -120,6 +167,197 @@ async function run(ctx: CommandContext): Promise<void> {
120
167
  );
121
168
  }
122
169
 
170
+ async function runSend(ctx: CommandContext, argv: string[]): Promise<void> {
171
+ const { values, positionals } = parseArgs({
172
+ args: argv,
173
+ allowPositionals: true,
174
+ options: {
175
+ email: { type: "string" },
176
+ "user-id": { type: "string" },
177
+ prop: { type: "string", multiple: true },
178
+ props: { type: "string" },
179
+ "contact-prop": { type: "string", multiple: true },
180
+ "contact-props": { type: "string" },
181
+ list: { type: "string", multiple: true },
182
+ unlist: { type: "string", multiple: true },
183
+ "idempotency-key": { type: "string" },
184
+ timestamp: { type: "string" },
185
+ help: { type: "boolean", short: "h", default: false },
186
+ },
187
+ });
188
+
189
+ if (values.help) {
190
+ ctx.out.log(usage);
191
+ return;
192
+ }
193
+
194
+ // positionals[0] is the event name (the "send" token was already stripped).
195
+ const name = positionals[0];
196
+ if (!name) {
197
+ ctx.out.fail(
198
+ "events send requires an event name, e.g. hogsend events send signup --user-id user_123",
199
+ );
200
+ }
201
+
202
+ const email = values.email;
203
+ const userId = values["user-id"];
204
+ if (!email && !userId) {
205
+ ctx.out.fail("events send requires at least one of --email or --user-id");
206
+ }
207
+
208
+ const eventProperties = parseProps(ctx, values.props, values.prop, "prop");
209
+ const contactProperties = parseProps(
210
+ ctx,
211
+ values["contact-props"],
212
+ values["contact-prop"],
213
+ "contact-prop",
214
+ );
215
+ const lists = parseLists(values.list, values.unlist);
216
+
217
+ const body: {
218
+ name: string;
219
+ email?: string;
220
+ userId?: string;
221
+ eventProperties?: Record<string, unknown>;
222
+ contactProperties?: Record<string, unknown>;
223
+ lists?: Record<string, boolean>;
224
+ idempotencyKey?: string;
225
+ timestamp?: string;
226
+ } = { name };
227
+ if (email) body.email = email;
228
+ if (userId) body.userId = userId;
229
+ if (eventProperties) body.eventProperties = eventProperties;
230
+ if (contactProperties) body.contactProperties = contactProperties;
231
+ if (lists) body.lists = lists;
232
+ if (values["idempotency-key"]) {
233
+ body.idempotencyKey = values["idempotency-key"];
234
+ }
235
+ if (values.timestamp) body.timestamp = values.timestamp;
236
+
237
+ let res: SendResponse;
238
+ try {
239
+ res = await ctx.out.step(`Sending event ${name}`, () =>
240
+ ctx.dataHttp.post<SendResponse>("/v1/events", body),
241
+ );
242
+ } catch (error) {
243
+ if (isHttpError(error)) {
244
+ ctx.out.fail(error.message);
245
+ }
246
+ throw error;
247
+ }
248
+
249
+ if (ctx.json) {
250
+ ctx.out.json(res);
251
+ return;
252
+ }
253
+
254
+ ctx.out.intro(`${color.bgMagenta(color.black(" hogsend "))} events send`);
255
+
256
+ const exited = res.exits.filter((e) => e.exited);
257
+ ctx.out.kv(
258
+ {
259
+ event: name,
260
+ stored: res.stored,
261
+ identity: email ?? userId ?? "",
262
+ exits: res.exits.length,
263
+ "journeys exited": exited.length,
264
+ },
265
+ "Event sent",
266
+ );
267
+
268
+ if (exited.length > 0) {
269
+ ctx.out.table(
270
+ exited.map((e) => ({ journeyId: e.journeyId, stateId: e.stateId })),
271
+ ["journeyId", "stateId"],
272
+ );
273
+ }
274
+
275
+ ctx.out.outro(
276
+ res.stored
277
+ ? `${color.green("Stored")} ${name}.`
278
+ : color.dim(`${name} was deduped (not stored).`),
279
+ );
280
+ }
281
+
282
+ /**
283
+ * Parse `--<flag> key=value` (repeatable) + an optional `--<flag>s <json>`
284
+ * object into a single properties record. Each value is JSON-parsed when valid
285
+ * JSON, else kept as a string. The JSON object is applied first so later
286
+ * key=value flags win. `flagName` is used only for error messages.
287
+ */
288
+ function parseProps(
289
+ ctx: CommandContext,
290
+ json: string | undefined,
291
+ pairs: string[] | undefined,
292
+ flagName: string,
293
+ ): Record<string, unknown> | undefined {
294
+ const out: Record<string, unknown> = {};
295
+ let any = false;
296
+
297
+ if (json !== undefined) {
298
+ let parsed: unknown;
299
+ try {
300
+ parsed = JSON.parse(json);
301
+ } catch {
302
+ ctx.out.fail(`--${flagName}s must be valid JSON, got: ${json}`);
303
+ }
304
+ if (
305
+ parsed === null ||
306
+ typeof parsed !== "object" ||
307
+ Array.isArray(parsed)
308
+ ) {
309
+ ctx.out.fail(`--${flagName}s must be a JSON object`);
310
+ }
311
+ Object.assign(out, parsed as Record<string, unknown>);
312
+ any = true;
313
+ }
314
+
315
+ for (const pair of pairs ?? []) {
316
+ const eq = pair.indexOf("=");
317
+ if (eq === -1) {
318
+ ctx.out.fail(`--${flagName} must be key=value, got: ${pair}`);
319
+ }
320
+ const key = pair.slice(0, eq).trim();
321
+ if (key === "") {
322
+ ctx.out.fail(`--${flagName} key cannot be empty, got: ${pair}`);
323
+ }
324
+ out[key] = coerceValue(pair.slice(eq + 1));
325
+ any = true;
326
+ }
327
+
328
+ return any ? out : undefined;
329
+ }
330
+
331
+ /** JSON-parse a flag value, falling back to the raw string. */
332
+ function coerceValue(raw: string): unknown {
333
+ try {
334
+ return JSON.parse(raw);
335
+ } catch {
336
+ return raw;
337
+ }
338
+ }
339
+
340
+ /**
341
+ * Build a `lists` map from repeatable `--list <id>` (true) / `--unlist <id>`
342
+ * (false) flags. Returns undefined when neither was passed.
343
+ */
344
+ function parseLists(
345
+ subscribe: string[] | undefined,
346
+ unsubscribe: string[] | undefined,
347
+ ): Record<string, boolean> | undefined {
348
+ const out: Record<string, boolean> = {};
349
+ let any = false;
350
+ for (const id of subscribe ?? []) {
351
+ out[id] = true;
352
+ any = true;
353
+ }
354
+ for (const id of unsubscribe ?? []) {
355
+ out[id] = false;
356
+ any = true;
357
+ }
358
+ return any ? out : undefined;
359
+ }
360
+
123
361
  /**
124
362
  * Parse an optional numeric flag. Returns undefined when absent (lets the
125
363
  * server apply its default); fails on a non-numeric value.
@@ -148,7 +386,7 @@ function summarizeProps(props: Record<string, unknown> | null): string {
148
386
 
149
387
  export const eventsCommand: Command = {
150
388
  name: "events",
151
- summary: "Stream a single user's event history",
389
+ summary: "Stream a user's event history, or send an event",
152
390
  usage,
153
391
  run,
154
392
  };
@@ -1,6 +1,8 @@
1
+ import { campaignsCommand } from "./campaigns.js";
1
2
  import { contactsCommand } from "./contacts.js";
2
3
  import { doctorCommand } from "./doctor.js";
3
4
  import { ejectCommand } from "./eject.js";
5
+ import { emailsCommand } from "./emails.js";
4
6
  import { eventsCommand } from "./events.js";
5
7
  import { journeysCommand } from "./journeys.js";
6
8
  import { patchCommand } from "./patch.js";
@@ -25,6 +27,8 @@ export const commands: Command[] = [
25
27
  contactsCommand,
26
28
  statsCommand,
27
29
  eventsCommand,
30
+ emailsCommand,
31
+ campaignsCommand,
28
32
  studioCommand,
29
33
  setupCommand,
30
34
  skillsCommand,
@@ -1,5 +1,5 @@
1
1
  import type { ResolvedConfig } from "../lib/config.js";
2
- import type { AdminClient } from "../lib/http.js";
2
+ import type { AdminClient, DataPlaneClient } from "../lib/http.js";
3
3
  import type { Output } from "../lib/output.js";
4
4
 
5
5
  /**
@@ -17,8 +17,14 @@ export interface CommandContext {
17
17
  argv: string[];
18
18
  /** Base URL + admin key, already resolved via flags > env > .env. */
19
19
  cfg: ResolvedConfig;
20
- /** Pre-built admin HTTP client, bound to `cfg`. */
20
+ /** Pre-built admin HTTP client, bound to `cfg` (admin key). */
21
21
  http: AdminClient;
22
+ /**
23
+ * Pre-built data-plane HTTP client, bound to `cfg.dataKey` (ingest key). Used
24
+ * by the write commands (`contacts upsert`, `events send`, `emails send`)
25
+ * that hit the authed `/v1/contacts`, `/v1/events`, `/v1/emails` routes.
26
+ */
27
+ dataHttp: DataPlaneClient;
22
28
  /** Output sink — human (TTY clack) vs json, already mode-selected. */
23
29
  out: Output;
24
30
  /** True when the global `--json` flag was passed. Mirrors `out.isJson`. */
package/src/lib/config.ts CHANGED
@@ -8,12 +8,19 @@ export interface ResolvedConfig {
8
8
  baseUrl: string;
9
9
  /** Admin bearer token, if resolvable. `doctor`/health works without it. */
10
10
  adminKey: string | undefined;
11
+ /**
12
+ * Data-plane bearer token (an `ingest`-scoped key), if resolvable. Used by the
13
+ * write commands (`contacts upsert`, `events send`, `emails send`). Falls back
14
+ * to the admin key since `full-admin` implies `ingest`.
15
+ */
16
+ dataKey: string | undefined;
11
17
  }
12
18
 
13
19
  /** Global flags parsed off the front of any command's argv. */
14
20
  export interface GlobalFlags {
15
21
  url?: string;
16
22
  adminKey?: string;
23
+ dataKey?: string;
17
24
  json: boolean;
18
25
  help: boolean;
19
26
  /** The remaining args after global flags are stripped. */
@@ -39,6 +46,7 @@ export function parseGlobalFlags(argv: string[]): GlobalFlags {
39
46
  options: {
40
47
  url: { type: "string" },
41
48
  "admin-key": { type: "string" },
49
+ "data-key": { type: "string" },
42
50
  json: { type: "boolean", default: false },
43
51
  help: { type: "boolean", short: "h", default: false },
44
52
  },
@@ -47,7 +55,7 @@ export function parseGlobalFlags(argv: string[]): GlobalFlags {
47
55
  // Rebuild `rest` from the token stream, dropping only the global flags we
48
56
  // own (and their values). Everything else — positionals and unknown option
49
57
  // tokens — is preserved verbatim for the command's own parser.
50
- const owned = new Set(["url", "admin-key", "json", "help", "h"]);
58
+ const owned = new Set(["url", "admin-key", "data-key", "json", "help", "h"]);
51
59
  const rest: string[] = [];
52
60
  for (const token of tokens) {
53
61
  if (token.kind === "positional") {
@@ -68,6 +76,8 @@ export function parseGlobalFlags(argv: string[]): GlobalFlags {
68
76
  url: typeof values.url === "string" ? values.url : undefined,
69
77
  adminKey:
70
78
  typeof values["admin-key"] === "string" ? values["admin-key"] : undefined,
79
+ dataKey:
80
+ typeof values["data-key"] === "string" ? values["data-key"] : undefined,
71
81
  json: values.json === true,
72
82
  help: values.help === true,
73
83
  rest,
@@ -120,6 +130,7 @@ export function loadDotEnv(
120
130
  *
121
131
  * baseUrl: --url > HOGSEND_API_URL (env) > HOGSEND_API_URL (.env) > localhost:3002
122
132
  * adminKey: --admin-key > HOGSEND_ADMIN_KEY|ADMIN_API_KEY (env) > (.env equiv)
133
+ * dataKey: --data-key > HOGSEND_DATA_KEY > HOGSEND_API_KEY (env then .env)
123
134
  */
124
135
  export function resolveConfig(
125
136
  flags: GlobalFlags,
@@ -140,8 +151,19 @@ export function resolveConfig(
140
151
  dotenv.HOGSEND_ADMIN_KEY ??
141
152
  dotenv.ADMIN_API_KEY;
142
153
 
154
+ // Data-plane (ingest-scoped) key. Precedence is independent of adminKey:
155
+ // explicit data key first, then a dedicated env/.env var, then the generic
156
+ // HOGSEND_API_KEY (which a fresh scaffold mints as an ingest key).
157
+ const dataKey =
158
+ flags.dataKey ??
159
+ process.env.HOGSEND_DATA_KEY ??
160
+ process.env.HOGSEND_API_KEY ??
161
+ dotenv.HOGSEND_DATA_KEY ??
162
+ dotenv.HOGSEND_API_KEY;
163
+
143
164
  return {
144
165
  baseUrl: baseUrlRaw.replace(/\/+$/, ""),
145
166
  adminKey: adminKey && adminKey.length > 0 ? adminKey : undefined,
167
+ dataKey: dataKey && dataKey.length > 0 ? dataKey : undefined,
146
168
  };
147
169
  }
package/src/lib/http.ts CHANGED
@@ -77,68 +77,141 @@ function bodyMessage(status: number, body: unknown): string {
77
77
  return `request failed with status ${status}`;
78
78
  }
79
79
 
80
- /** Build an {@link AdminClient} bound to the given resolved config. */
81
- export function createAdminClient(cfg: ResolvedConfig): AdminClient {
82
- async function request<T>(
83
- method: string,
84
- path: string,
85
- opts: { query?: Query; body?: unknown; auth: boolean },
86
- ): Promise<T> {
87
- if (opts.auth && !cfg.adminKey) {
88
- throw makeHttpError(
89
- "no admin key configured — pass --admin-key, or set HOGSEND_ADMIN_KEY / ADMIN_API_KEY",
90
- 0,
91
- undefined,
92
- );
93
- }
94
-
95
- const headers: Record<string, string> = { Accept: "application/json" };
96
- if (opts.auth && cfg.adminKey) {
97
- headers.Authorization = `Bearer ${cfg.adminKey}`;
98
- }
99
- if (opts.body !== undefined) {
100
- headers["Content-Type"] = "application/json";
101
- }
80
+ /** Internal options accepted by the shared {@link request} core. */
81
+ interface RequestOpts {
82
+ query?: Query;
83
+ body?: unknown;
84
+ /** When false, no Authorization header is sent (e.g. /v1/health). */
85
+ auth: boolean;
86
+ }
102
87
 
103
- const url = buildUrl(cfg.baseUrl, path, opts.query);
88
+ /**
89
+ * The single HTTP core, shared by the admin client and the data-plane client.
90
+ * Bound to a `baseUrl` + a bearer `key`; the only difference between the two
91
+ * clients is which key (and which "missing key" message) they carry. Sends
92
+ * `Authorization: Bearer <key>` when `auth` is set, parses JSON, and throws an
93
+ * {@link HttpError} on any non-2xx response (or transport failure).
94
+ */
95
+ async function request<T>(
96
+ baseUrl: string,
97
+ key: string | undefined,
98
+ missingKeyMessage: string,
99
+ method: string,
100
+ path: string,
101
+ opts: RequestOpts,
102
+ ): Promise<T> {
103
+ if (opts.auth && !key) {
104
+ throw makeHttpError(missingKeyMessage, 0, undefined);
105
+ }
104
106
 
105
- let res: Response;
106
- try {
107
- res = await fetch(url, {
108
- method,
109
- headers,
110
- body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined,
111
- });
112
- } catch (cause) {
113
- const msg = cause instanceof Error ? cause.message : String(cause);
114
- throw makeHttpError(`cannot reach ${cfg.baseUrl} (${msg})`, 0, undefined);
115
- }
107
+ const headers: Record<string, string> = { Accept: "application/json" };
108
+ if (opts.auth && key) {
109
+ headers.Authorization = `Bearer ${key}`;
110
+ }
111
+ if (opts.body !== undefined) {
112
+ headers["Content-Type"] = "application/json";
113
+ }
116
114
 
117
- const text = await res.text();
118
- let parsed: unknown;
119
- if (text.length > 0) {
120
- try {
121
- parsed = JSON.parse(text);
122
- } catch {
123
- parsed = text;
124
- }
125
- }
115
+ const url = buildUrl(baseUrl, path, opts.query);
116
+
117
+ let res: Response;
118
+ try {
119
+ res = await fetch(url, {
120
+ method,
121
+ headers,
122
+ body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined,
123
+ });
124
+ } catch (cause) {
125
+ const msg = cause instanceof Error ? cause.message : String(cause);
126
+ throw makeHttpError(`cannot reach ${baseUrl} (${msg})`, 0, undefined);
127
+ }
126
128
 
127
- if (!res.ok) {
128
- throw makeHttpError(bodyMessage(res.status, parsed), res.status, parsed);
129
+ const text = await res.text();
130
+ let parsed: unknown;
131
+ if (text.length > 0) {
132
+ try {
133
+ parsed = JSON.parse(text);
134
+ } catch {
135
+ parsed = text;
129
136
  }
137
+ }
130
138
 
131
- return parsed as T;
139
+ if (!res.ok) {
140
+ throw makeHttpError(bodyMessage(res.status, parsed), res.status, parsed);
132
141
  }
133
142
 
143
+ return parsed as T;
144
+ }
145
+
146
+ /** Build an {@link AdminClient} bound to the given resolved config. */
147
+ export function createAdminClient(cfg: ResolvedConfig): AdminClient {
148
+ const missing =
149
+ "no admin key configured — pass --admin-key, or set HOGSEND_ADMIN_KEY / ADMIN_API_KEY";
134
150
  return {
135
151
  cfg,
136
152
  get: <T>(path: string, query?: Query, extras?: RequestExtras) =>
137
- request<T>("GET", path, { query, auth: extras?.auth ?? true }),
153
+ request<T>(cfg.baseUrl, cfg.adminKey, missing, "GET", path, {
154
+ query,
155
+ auth: extras?.auth ?? true,
156
+ }),
138
157
  patch: <T>(path: string, body: unknown) =>
139
- request<T>("PATCH", path, { body, auth: true }),
158
+ request<T>(cfg.baseUrl, cfg.adminKey, missing, "PATCH", path, {
159
+ body,
160
+ auth: true,
161
+ }),
162
+ post: <T>(path: string, body: unknown) =>
163
+ request<T>(cfg.baseUrl, cfg.adminKey, missing, "POST", path, {
164
+ body,
165
+ auth: true,
166
+ }),
167
+ };
168
+ }
169
+
170
+ /**
171
+ * Thin data-plane HTTP client over the same core as {@link createAdminClient},
172
+ * but bound to `cfg.dataKey` (an `ingest`-scoped key). Used by the write
173
+ * commands (`contacts upsert`, `events send`, `emails send`) which hit the
174
+ * authed `/v1/contacts`, `/v1/events`, and `/v1/emails` data-plane routes.
175
+ *
176
+ * Exposes the full read/write verb set the data plane needs: `get`/`post`/
177
+ * `put`/`del`. Every call is authenticated (there is no unauthenticated
178
+ * data-plane route), so there is no `{ auth: false }` escape hatch here.
179
+ */
180
+ export interface DataPlaneClient {
181
+ get<T = unknown>(path: string, query?: Query): Promise<T>;
182
+ post<T = unknown>(path: string, body: unknown): Promise<T>;
183
+ put<T = unknown>(path: string, body: unknown): Promise<T>;
184
+ del<T = unknown>(path: string, body?: unknown): Promise<T>;
185
+ /** The resolved config this client is bound to (for messages/JSON output). */
186
+ readonly cfg: ResolvedConfig;
187
+ }
188
+
189
+ /** Build a {@link DataPlaneClient} bound to `cfg.dataKey`. */
190
+ export function createDataPlaneClient(cfg: ResolvedConfig): DataPlaneClient {
191
+ const missing =
192
+ "no data key configured — pass --data-key, or set HOGSEND_DATA_KEY / HOGSEND_API_KEY";
193
+ return {
194
+ cfg,
195
+ get: <T>(path: string, query?: Query) =>
196
+ request<T>(cfg.baseUrl, cfg.dataKey, missing, "GET", path, {
197
+ query,
198
+ auth: true,
199
+ }),
140
200
  post: <T>(path: string, body: unknown) =>
141
- request<T>("POST", path, { body, auth: true }),
201
+ request<T>(cfg.baseUrl, cfg.dataKey, missing, "POST", path, {
202
+ body,
203
+ auth: true,
204
+ }),
205
+ put: <T>(path: string, body: unknown) =>
206
+ request<T>(cfg.baseUrl, cfg.dataKey, missing, "PUT", path, {
207
+ body,
208
+ auth: true,
209
+ }),
210
+ del: <T>(path: string, body?: unknown) =>
211
+ request<T>(cfg.baseUrl, cfg.dataKey, missing, "DELETE", path, {
212
+ body,
213
+ auth: true,
214
+ }),
142
215
  };
143
216
  }
144
217
 
@@ -0,0 +1 @@
1
+ *,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}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;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}:root{--background: 0 0% 100%;--foreground: 240 10% 3.9%;--card: 0 0% 100%;--card-foreground: 240 10% 3.9%;--primary: 240 5.9% 10%;--primary-foreground: 0 0% 98%;--secondary: 240 4.8% 95.9%;--secondary-foreground: 240 5.9% 10%;--muted: 240 4.8% 95.9%;--muted-foreground: 240 3.8% 46.1%;--accent: 240 4.8% 95.9%;--accent-foreground: 240 5.9% 10%;--destructive: 0 84.2% 60.2%;--destructive-foreground: 0 0% 98%;--border: 240 5.9% 90%;--input: 240 5.9% 90%;--ring: 240 5.9% 10%;--radius: .5rem}*{border-color:hsl(var(--border))}body{background-color:hsl(var(--background));color:hsl(var(--foreground))}.pointer-events-none{pointer-events:none}.pointer-events-auto{pointer-events:auto}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{top:0;right:0;bottom:0;left:0}.inset-y-0{top:0;bottom:0}.bottom-4{bottom:1rem}.left-3{left:.75rem}.right-0{right:0}.right-2{right:.5rem}.right-4{right:1rem}.top-1\/2{top:50%}.z-10{z-index:10}.z-50{z-index:50}.z-\[60\]{z-index:60}.-mr-2{margin-right:-.5rem}.-mt-2{margin-top:-.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.ml-1{margin-left:.25rem}.ml-1\.5{margin-left:.375rem}.ml-auto{margin-left:auto}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.block{display:block}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.h-10{height:2.5rem}.h-12{height:3rem}.h-14{height:3.5rem}.h-2{height:.5rem}.h-20{height:5rem}.h-24{height:6rem}.h-28{height:7rem}.h-3{height:.75rem}.h-3\.5{height:.875rem}.h-4{height:1rem}.h-40{height:10rem}.h-48{height:12rem}.h-5{height:1.25rem}.h-7{height:1.75rem}.h-8{height:2rem}.h-9{height:2.25rem}.h-96{height:24rem}.h-\[600px\]{height:600px}.h-full{height:100%}.max-h-48{max-height:12rem}.min-h-\[140px\]{min-height:140px}.w-3{width:.75rem}.w-3\.5{width:.875rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-60{width:15rem}.w-7{width:1.75rem}.w-8{width:2rem}.w-9{width:2.25rem}.w-full{width:100%}.w-px{width:1px}.min-w-0{min-width:0px}.min-w-\[2px\]{min-width:2px}.max-w-2xl{max-width:42rem}.max-w-lg{max-width:32rem}.max-w-sm{max-width:24rem}.max-w-xs{max-width:20rem}.flex-1{flex:1 1 0%}.shrink-0{flex-shrink:0}.caption-bottom{caption-side:bottom}.-translate-y-1\/2{--tw-translate-y: -50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}.cursor-pointer{cursor:pointer}.appearance-none{-webkit-appearance:none;-moz-appearance:none;appearance:none}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-row{flex-direction:row}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.space-y-0>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(0px * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(0px * var(--tw-space-y-reverse))}.space-y-0\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.125rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.125rem * var(--tw-space-y-reverse))}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-reverse))}.space-y-1\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.375rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.375rem * var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem * var(--tw-space-y-reverse))}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.whitespace-nowrap{white-space:nowrap}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:var(--radius)}.rounded-md{border-radius:calc(var(--radius) - 2px)}.rounded-t{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-l{border-left-width:1px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-dashed{border-style:dashed}.border-destructive\/40{border-color:hsl(var(--destructive) / .4)}.border-emerald-500\/40{border-color:#10b98166}.border-input{border-color:hsl(var(--input))}.border-primary\/30{border-color:hsl(var(--primary) / .3)}.border-sky-500\/40{border-color:#0ea5e966}.border-transparent{border-color:transparent}.border-violet-500\/40{border-color:#8b5cf666}.bg-accent{background-color:hsl(var(--accent))}.bg-background{background-color:hsl(var(--background))}.bg-black\/50{background-color:#00000080}.bg-border{background-color:hsl(var(--border))}.bg-card{background-color:hsl(var(--card))}.bg-destructive{background-color:hsl(var(--destructive))}.bg-destructive\/5{background-color:hsl(var(--destructive) / .05)}.bg-muted{background-color:hsl(var(--muted))}.bg-muted\/20{background-color:hsl(var(--muted) / .2)}.bg-muted\/30{background-color:hsl(var(--muted) / .3)}.bg-primary{background-color:hsl(var(--primary))}.bg-primary\/5{background-color:hsl(var(--primary) / .05)}.bg-primary\/70{background-color:hsl(var(--primary) / .7)}.bg-secondary{background-color:hsl(var(--secondary))}.bg-transparent{background-color:transparent}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.p-12{padding:3rem}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.px-8{padding-left:2rem;padding-right:2rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.pb-2{padding-bottom:.5rem}.pb-3{padding-bottom:.75rem}.pb-4{padding-bottom:1rem}.pl-9{padding-left:2.25rem}.pr-8{padding-right:2rem}.pt-0{padding-top:0}.pt-3{padding-top:.75rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.align-middle{vertical-align:middle}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-\[10px\]{font-size:10px}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.capitalize{text-transform:capitalize}.leading-none{line-height:1}.tracking-tight{letter-spacing:-.025em}.tracking-wide{letter-spacing:.025em}.text-accent-foreground{color:hsl(var(--accent-foreground))}.text-card-foreground{color:hsl(var(--card-foreground))}.text-destructive{color:hsl(var(--destructive))}.text-destructive-foreground{color:hsl(var(--destructive-foreground))}.text-emerald-500{--tw-text-opacity: 1;color:rgb(16 185 129 / var(--tw-text-opacity, 1))}.text-emerald-600{--tw-text-opacity: 1;color:rgb(5 150 105 / var(--tw-text-opacity, 1))}.text-foreground{color:hsl(var(--foreground))}.text-green-500{--tw-text-opacity: 1;color:rgb(34 197 94 / var(--tw-text-opacity, 1))}.text-muted-foreground{color:hsl(var(--muted-foreground))}.text-primary{color:hsl(var(--primary))}.text-primary-foreground{color:hsl(var(--primary-foreground))}.text-secondary-foreground{color:hsl(var(--secondary-foreground))}.text-sky-600{--tw-text-opacity: 1;color:rgb(2 132 199 / var(--tw-text-opacity, 1))}.text-violet-600{--tw-text-opacity: 1;color:rgb(124 58 237 / var(--tw-text-opacity, 1))}.underline{text-decoration-line:underline}.underline-offset-4{text-underline-offset:4px}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.shadow-lg{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-xl{--tw-shadow: 0 20px 25px -5px rgb(0 0 0 / .1), 0 8px 10px -6px rgb(0 0 0 / .1);--tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.outline{outline-style:solid}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.file\:border-0::file-selector-button{border-width:0px}.file\:bg-transparent::file-selector-button{background-color:transparent}.file\:text-sm::file-selector-button{font-size:.875rem;line-height:1.25rem}.file\:font-medium::file-selector-button{font-weight:500}.placeholder\:text-muted-foreground::-moz-placeholder{color:hsl(var(--muted-foreground))}.placeholder\:text-muted-foreground::placeholder{color:hsl(var(--muted-foreground))}.hover\:bg-accent:hover{background-color:hsl(var(--accent))}.hover\:bg-destructive\/90:hover{background-color:hsl(var(--destructive) / .9)}.hover\:bg-muted\/50:hover{background-color:hsl(var(--muted) / .5)}.hover\:bg-primary\/90:hover{background-color:hsl(var(--primary) / .9)}.hover\:bg-secondary\/80:hover{background-color:hsl(var(--secondary) / .8)}.hover\:text-accent-foreground:hover{color:hsl(var(--accent-foreground))}.hover\:text-foreground:hover{color:hsl(var(--foreground))}.hover\:underline:hover{text-decoration-line:underline}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-2:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-ring:focus{--tw-ring-color: hsl(var(--ring))}.focus-visible\:outline-none:focus-visible{outline:2px solid transparent;outline-offset:2px}.focus-visible\:ring-1:focus-visible{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus-visible\:ring-2:focus-visible{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus-visible\:ring-ring:focus-visible{--tw-ring-color: hsl(var(--ring))}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}.group:hover .group-hover\:bg-primary{background-color:hsl(var(--primary))}.peer:disabled~.peer-disabled\:cursor-not-allowed{cursor:not-allowed}.peer:disabled~.peer-disabled\:opacity-70{opacity:.7}.data-\[state\=selected\]\:bg-muted[data-state=selected]{background-color:hsl(var(--muted))}.dark\:text-emerald-400:is(.dark *){--tw-text-opacity: 1;color:rgb(52 211 153 / var(--tw-text-opacity, 1))}.dark\:text-sky-400:is(.dark *){--tw-text-opacity: 1;color:rgb(56 189 248 / var(--tw-text-opacity, 1))}.dark\:text-violet-400:is(.dark *){--tw-text-opacity: 1;color:rgb(167 139 250 / var(--tw-text-opacity, 1))}@media(min-width:640px){.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}@media(min-width:768px){.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}@media(min-width:1024px){.lg\:col-span-2{grid-column:span 2 / span 2}.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.lg\:grid-cols-\[260px_1fr\]{grid-template-columns:260px 1fr}}.\[\&\:has\(\[role\=checkbox\]\)\]\:pr-0:has([role=checkbox]){padding-right:0}.\[\&\>li\:last-child\>div\:first-child\>span\:last-child\]\:hidden>li:last-child>div:first-child>span:last-child{display:none}.\[\&_tr\:last-child\]\:border-0 tr:last-child{border-width:0px}.\[\&_tr\]\:border-b tr{border-bottom-width:1px}