From 8468ffe4ca60603d5d1da74c80446b2b347f0fdc Mon Sep 17 00:00:00 2001 From: Nisarg Patel Date: Fri, 15 May 2026 23:01:18 -0700 Subject: [PATCH 1/3] fix: live url plan --- .ultraplans/remote-logging/prompt.md | 368 +++++++++++++++++++++++++++ 1 file changed, 368 insertions(+) create mode 100644 .ultraplans/remote-logging/prompt.md diff --git a/.ultraplans/remote-logging/prompt.md b/.ultraplans/remote-logging/prompt.md new file mode 100644 index 0000000..825e7c4 --- /dev/null +++ b/.ultraplans/remote-logging/prompt.md @@ -0,0 +1,368 @@ +# Remote Log Relay for debug-agent + +Build a hosted log relay using Cloudflare Durable Objects so production apps can push NDJSON logs to a public URL and a local Claude Code agent can read them via HTTP GET or SSE stream. This solves the problem that localhost URLs don't work for production debugging. + +## What to build + +Two changes: + +1. **New package `packages/debug-agent-remote`** — A Cloudflare Worker with a SQLite-backed Durable Object that acts as a log relay. Each session auto-expires after 1 hour. +2. **New `remote` command in `packages/debug-agent`** — A CLI subcommand that creates remote sessions, streams logs, and integrates with the existing skill workflow. +3. **Update `packages/debug-agent/skill/SKILL.md`** — Add remote mode instructions so the agent can infer when to use remote vs local. + +--- + +## Part 1: `packages/debug-agent-remote` (Cloudflare Worker) + +### Package setup + +- npm name: `@debug-agent/remote` +- Add to `pnpm-workspace.yaml` (already covered by `packages/*` glob) +- Use `wrangler` for builds and deployment (not vite-plus — this is a Worker, not a Node library) +- `package.json` with `wrangler` as a dev dependency, `@cloudflare/workers-types` for types +- Compatibility date: `2026-02-24` or later (needed for `deleteAll()` to also delete alarms) +- `wrangler.jsonc` config with a single Durable Object binding + +### Durable Object: `LogSession` + +SQLite-backed Durable Object class. One instance per debug session. + +**On creation (first request):** +- Record `createdAt` timestamp in storage +- Set an alarm for 1 hour from now via `this.ctx.storage.setAlarm(Date.now() + 3_600_000)` +- Create a SQLite table for log entries: + ```sql + CREATE TABLE IF NOT EXISTS logs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + entry_id TEXT, + data TEXT NOT NULL, + created_at INTEGER NOT NULL DEFAULT (unixepoch('subsec') * 1000) + ) + ``` + (`entry_id` is the optional dedup `id` field from the log payload; `data` is the full JSON string) + +**Storage limits (enforce on POST):** +- Max 10,000 log entries per session +- Max 10KB (10,240 bytes) per individual log entry (the JSON string) +- Max 100MB (104,857,600 bytes) total storage per session (sum of all `data` column lengths) +- Return `413 Payload Too Large` with a JSON error body when any limit is exceeded + +**Alarm handler (`alarm()`):** +- Call `this.ctx.storage.deleteAll()` — this clears all SQLite data AND the alarm itself (compatibility date 2026-02-24+) +- Close all active SSE connections with an `expired` event before closing +- The Durable Object ceases to exist once storage is empty and it shuts down + +**In-memory state:** +- `Set` for active SSE connections (to broadcast new logs) +- `initialized: boolean` flag to track if the SQLite table has been created + +### Worker entry point + +The Worker routes requests to the appropriate Durable Object. + +**Routes:** + +| Method | Path | Handler | +|--------|------|---------| +| `POST` | `/sessions` | Create a new session: generate nanoid(21), get DO stub by name, call DO to initialize, return session info | +| `POST` | `/s/:id` | Forward to DO — ingest a log entry | +| `GET` | `/s/:id` | Forward to DO — return all buffered logs as NDJSON | +| `GET` | `/s/:id/stream` | Forward to DO — SSE stream | +| `DELETE` | `/s/:id` | Forward to DO — clear logs (but keep session alive) | +| `OPTIONS` | `*` | CORS preflight | + +**CORS headers on ALL responses:** +``` +Access-Control-Allow-Origin: * +Access-Control-Allow-Methods: GET, POST, DELETE, OPTIONS +Access-Control-Allow-Headers: Content-Type +``` + +**Session creation (`POST /sessions`):** +- Generate a nanoid (21 chars, URL-safe alphabet, use `crypto.getRandomValues` — no npm dependency needed) +- Derive the Durable Object ID from the nanoid using `env.LOG_SESSION.idFromName(sessionId)` +- Call the DO to initialize it (POST with empty body or a special init action) +- Return JSON: + ```json + { + "sessionId": "V1StGXR8_Z5jdHi6B-myT", + "endpoint": "https:///s/V1StGXR8_Z5jdHi6B-myT", + "streamUrl": "https:///s/V1StGXR8_Z5jdHi6B-myT/stream", + "expiresAt": 1733460389000 + } + ``` + +**For all `/s/:id` routes:** +- Parse the session ID from the URL +- Get the DO stub via `env.LOG_SESSION.idFromName(sessionId)` +- Forward the request to the DO +- If the DO's storage is empty (session expired/never existed), return `410 Gone` + +### Durable Object request handling + +**`POST /s/:id` (ingest):** +- Parse JSON body +- If body has an `id` field, check for duplicates: `SELECT 1 FROM logs WHERE entry_id = ? LIMIT 1` + - If duplicate, return `{ ok: true, duplicate: true }` +- Add `sessionId` and `timestamp` fields if missing (same as local server) +- Check size limits before inserting +- Insert into SQLite: `INSERT INTO logs (entry_id, data) VALUES (?, ?)` +- Broadcast to all active SSE connections as a `log` event +- Return `{ ok: true }` + +**`GET /s/:id` (read all):** +- `SELECT data FROM logs ORDER BY id ASC` +- Concatenate all `data` values with `\n` separator +- Return with `Content-Type: application/x-ndjson` + +**`GET /s/:id/stream` (SSE):** +- Return a streaming `Response` with: + ``` + Content-Type: text/event-stream + Cache-Control: no-cache + Connection: keep-alive + ``` +- Immediately send a `connected` event: + ``` + event: connected + data: {"sessionId":"...","expiresAt":1733460389000,"bufferedLogs":42} + ``` +- Replay all existing logs as `log` events: + ``` + event: log + data: {"sessionId":"a1b2c3","location":"test.js:42",...} + ``` +- Send a `replay-complete` marker: + ``` + event: replay-complete + data: {} + ``` +- Keep the connection open. When new logs arrive (via POST), broadcast as `log` events. +- When the alarm fires (session expires), send an `expired` event and close: + ``` + event: expired + data: {"reason":"Session expired after 1 hour"} + ``` +- Register the writer in the DO's in-memory SSE connection set. Remove on disconnect. + +**`DELETE /s/:id` (clear):** +- `DELETE FROM logs` +- Reset any in-memory dedup tracking +- Return `{ ok: true, cleared: true }` + +### nanoid implementation + +Implement nanoid inline in the Worker (no npm dependency). Use the standard URL-safe alphabet: + +```typescript +const ALPHABET = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict"; +const NANOID_LENGTH = 21; + +const generateSessionId = (): string => { + const bytes = crypto.getRandomValues(new Uint8Array(NANOID_LENGTH)); + let id = ""; + for (let i = 0; i < NANOID_LENGTH; i++) { + id += ALPHABET[bytes[i] & 63]; + } + return id; +}; +``` + +### File structure + +``` +packages/debug-agent-remote/ +├── package.json +├── tsconfig.json +├── wrangler.jsonc +└── src/ + ├── index.ts # Worker entry point (routing, CORS, nanoid) + ├── log-session.ts # LogSession Durable Object class + └── constants.ts # Limits, TTL, alphabet constants +``` + +### Constants (`src/constants.ts`) + +```typescript +export const SESSION_TTL_MS = 3_600_000; +export const MAX_LOG_ENTRIES = 10_000; +export const MAX_ENTRY_SIZE_BYTES = 10_240; +export const MAX_TOTAL_STORAGE_BYTES = 104_857_600; +export const NANOID_LENGTH = 21; +export const NANOID_ALPHABET = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict"; +``` + +--- + +## Part 2: `remote` command in `packages/debug-agent` + +### New command: `src/commands/remote.ts` + +Add a `remote` subcommand to the existing CLI in `src/cli.ts`. Follow the exact same patterns as `src/commands/serve.ts`. + +**Options:** +- `--url ` — Override the Worker URL (default: hardcoded constant) +- `--daemon` — Create session, print JSON info, exit +- `--json` — Create session, start SSE streaming, output NDJSON lines to stdout +- (no flags) — Interactive mode with spinner + pretty output + +**Daemon mode (`--daemon`):** +1. POST to `{workerUrl}/sessions` to create a session +2. Print the response JSON to stdout (single line): + ```json + {"sessionId":"...","endpoint":"https://...","streamUrl":"https://...","expiresAt":1733460389000} + ``` +3. Exit immediately (nothing to background — the Worker is already running) + +**JSON mode (`--json`):** +1. Create session (same as daemon) +2. Print session info JSON line +3. Connect to SSE stream at `streamUrl` +4. Pipe each `log` event's data to stdout as a line (NDJSON output) +5. On `expired` event, print `{"event":"expired"}` and exit + +**Interactive mode:** +1. Create session with spinner +2. Display session info: + ``` + ✔ Remote session created (expires in 60 min) + Endpoint: https://xxx.workers.dev/s/V1StGXR8_Z5jdHi6B-myT + Stream: https://xxx.workers.dev/s/V1StGXR8_Z5jdHi6B-myT/stream + ``` +3. Connect to SSE stream +4. Pretty-print each log event as it arrives (timestamp, location, message) +5. Handle Ctrl+C gracefully + +**SSE client implementation:** +Use native `fetch()` with streaming response body (no EventSource dependency needed in Node — parse SSE manually from the ReadableStream). Parse `event:` and `data:` lines per the SSE spec. + +### Constants update + +Add to `packages/debug-agent/src/constants.ts`: +```typescript +export const DEFAULT_REMOTE_URL = "https://debug-agent-remote..workers.dev"; +``` + +The actual workers.dev subdomain will be determined after first `wrangler deploy`. Use a placeholder that the deployer updates. + +### CLI registration + +In `src/cli.ts`, add: +```typescript +import { remoteCommand } from "./commands/remote.js"; +// ... +.addCommand(remoteCommand) +``` + +--- + +## Part 3: Update SKILL.md + +Update `packages/debug-agent/skill/SKILL.md` to support both local and remote modes. The key changes: + +### Add a mode selection section after "STEP 0" + +Before Step 0, add a decision point: + +```markdown +### Choosing local vs remote mode + +- **Local mode** (default): Use when the buggy code runs on the same machine as this agent (localhost, local dev server, local scripts). Logs stay on disk. +- **Remote mode**: Use when the buggy code runs on a remote server, cloud environment, or production — anywhere that cannot reach `localhost`. Logs are relayed through a hosted service. + +If the bug is in code that runs remotely or in production, use remote mode. Otherwise, use local mode. +``` + +### Update STEP 0 to show both modes + +Add a remote variant of Step 0: + +```markdown +**Remote mode** — run this instead: + +\`\`\`bash +npx debug-agent remote --daemon +\`\`\` + +The command prints a single JSON line to stdout and exits: + +\`\`\`json +{ + "sessionId": "V1StGXR8_Z5jdHi6B-myT", + "endpoint": "https://xxx.workers.dev/s/V1StGXR8_Z5jdHi6B-myT", + "streamUrl": "https://xxx.workers.dev/s/V1StGXR8_Z5jdHi6B-myT/stream", + "expiresAt": 1733460389000 +} +\`\`\` + +Capture the **endpoint** value. There is no local log file in remote mode. + +**Important:** Remote sessions expire after 1 hour. If the session expires mid-debug, create a new one. +``` + +### Update STEP 2 (instrumentation) + +In remote mode, ALL languages (not just JS/TS) must use HTTP POST to the endpoint, since there is no local log file. Add a note: + +```markdown +- In **remote mode**, ALL languages must use HTTP POST to the **endpoint** (there is no local log file). Use `fetch`, `curl`, `requests.post`, `http.Post`, or equivalent for your language. +``` + +### Update STEP 4 (reading logs) + +In remote mode, instead of reading the file at logPath, the agent fetches logs via HTTP: + +```markdown +- In **remote mode**, fetch logs via HTTP instead of reading a file: + \`\`\`bash + curl -s ENDPOINT + \`\`\` + This returns the same NDJSON format as the local log file. +``` + +### Update Server API reference table + +Add the remote API: + +```markdown +### Remote API (remote mode) + +| Method | Path | Effect | +|--------|------|--------| +| `POST /s/:id` | Append JSON body as NDJSON log entry | +| `GET /s/:id` | Read all buffered log entries as NDJSON | +| `GET /s/:id/stream` | SSE stream (replays buffered logs then live) | +| `DELETE /s/:id` | Clear all log entries (session stays alive) | +``` + +--- + +## Technical constraints + +- Follow all rules from `CLAUDE.md`: arrow functions, interfaces over types, kebab-case files, SCREAMING_SNAKE_CASE constants with unit suffixes, descriptive variable names, no comments unless the "why" is non-obvious, one utility per file in `utils/`, `Boolean()` over `!!` +- The Worker package uses `wrangler` for builds, NOT vite-plus +- The Worker targets Cloudflare Workers runtime (not Node) — no `node:*` imports in the Worker code +- Use the `@cloudflare/workers-types` package for Worker/DO type definitions +- The `remote` CLI command uses native Node `fetch()` for HTTP calls (available in Node 18+) +- Parse SSE manually from `fetch()` streaming response — do not add an EventSource library +- No new dependencies in `packages/debug-agent` beyond what's already there (commander, ora, picocolors, prompts) +- Run `pnpm check` before committing + +## Acceptance criteria + +1. `wrangler dev` starts the Worker locally and all endpoints work: + - `POST /sessions` returns session info with a nanoid session ID + - `POST /s/{id}` ingests a JSON log entry + - `GET /s/{id}` returns all logs as NDJSON + - `GET /s/{id}/stream` returns an SSE stream that replays buffered logs, then streams live logs + - `DELETE /s/{id}` clears logs + - Expired sessions return `410 Gone` +2. `npx debug-agent remote --daemon` creates a remote session and prints JSON info +3. `npx debug-agent remote --json` creates a session and streams logs as NDJSON to stdout +4. `npx debug-agent remote` shows an interactive session with pretty-printed logs +5. SKILL.md correctly instructs the agent on when and how to use remote mode +6. Size limits are enforced (10K entries, 10KB/entry, 100MB total) +7. Durable Object self-destructs after 1 hour via alarm +8. Dedup works via the optional `id` field on log entries +9. CORS headers allow cross-origin POST from any origin +10. `pnpm check` passes From c4b2a6bbc0cd8a52d463540fc5dda10f3acd2cbb Mon Sep 17 00:00:00 2001 From: Nisarg Patel Date: Sat, 16 May 2026 00:02:51 -0700 Subject: [PATCH 2/3] style: apply formatter to debug-agent-browser Incidental whitespace/formatting cleanup surfaced by `pnpm check --fix` while working on remote logging. No behavioral changes. Co-authored-by: Cursor --- packages/debug-agent-browser/package.json | 4 +- packages/debug-agent-browser/src/browser.ts | 38 ++++++++++++------- .../debug-agent-browser/src/cdp-discovery.ts | 6 +-- .../src/chrome-launcher.ts | 5 ++- .../src/cookies/browser-detector.ts | 3 +- .../src/cookies/cdp-client.ts | 3 +- .../src/cookies/chromium-sqlite.ts | 11 ++---- .../src/cookies/chromium.ts | 13 ++----- .../src/cookies/cookies.ts | 15 ++++---- .../debug-agent-browser/src/cookies/index.ts | 7 +++- .../debug-agent-browser/src/cookies/layers.ts | 7 +++- .../src/cookies/sqlite-client.ts | 5 ++- .../src/cookies/utils/chromium-normalize.ts | 7 +--- packages/debug-agent-browser/src/index.ts | 7 +--- .../tests/cdp-discovery.test.ts | 32 +++++++--------- .../tests/cookies/crypto.test.ts | 14 +------ 16 files changed, 82 insertions(+), 95 deletions(-) diff --git a/packages/debug-agent-browser/package.json b/packages/debug-agent-browser/package.json index 8d17391..fff3d71 100644 --- a/packages/debug-agent-browser/package.json +++ b/packages/debug-agent-browser/package.json @@ -5,10 +5,10 @@ "keywords": [ "browser", "cdp", - "playwright", "chrome", "cookies", - "debug" + "debug", + "playwright" ], "homepage": "https://github.com/millionco/debug-agent", "bugs": { diff --git a/packages/debug-agent-browser/src/browser.ts b/packages/debug-agent-browser/src/browser.ts index 1ee6d10..a9d85ff 100644 --- a/packages/debug-agent-browser/src/browser.ts +++ b/packages/debug-agent-browser/src/browser.ts @@ -1,5 +1,20 @@ -import { chromium, webkit, firefox, type Browser as PlaywrightBrowser, type BrowserContext, type Locator, type Page } from "playwright"; -import { Browsers, Cookies, browserKeyOf, Cookie, createBrowsers, type Browser as BrowserProfile } from "./cookies"; +import { + chromium, + webkit, + firefox, + type Browser as PlaywrightBrowser, + type BrowserContext, + type Locator, + type Page, +} from "playwright"; +import { + Browsers, + Cookies, + browserKeyOf, + Cookie, + createBrowsers, + type Browser as BrowserProfile, +} from "./cookies"; import { CONTENT_ROLES, HEADLESS_CHROMIUM_ARGS, @@ -56,7 +71,11 @@ const dedupCookies = (cookies: readonly Cookie[]): Cookie[] => { return result; }; -const withTimeout = async (promise: Promise, timeoutMs: number, message: string): Promise => { +const withTimeout = async ( + promise: Promise, + timeoutMs: number, + message: string, +): Promise => { return new Promise((resolve, reject) => { const timer = setTimeout(() => reject(new Error(message)), timeoutMs); promise @@ -119,9 +138,7 @@ export class Browser { return dedupCookies(cookies); } - private async extractCookiesForBrowserKeys( - browserKeys: readonly string[], - ): Promise { + private async extractCookiesForBrowserKeys(browserKeys: readonly string[]): Promise { const browsers = await this.getBrowsers(); let allProfiles: BrowserProfile[]; try { @@ -205,9 +222,7 @@ export class Browser { existingContexts.length > 0 ? existingContexts[0]! : await playwrightBrowser.newContext(contextOptions).catch((cause) => { - throw new BrowserLaunchError( - cause instanceof Error ? cause.message : String(cause), - ); + throw new BrowserLaunchError(cause instanceof Error ? cause.message : String(cause)); }); if (options.cookies && !isCdpConnected) { @@ -232,10 +247,7 @@ export class Browser { try { await page.goto(url, { waitUntil: options.waitUntil ?? "load" }); } catch (cause) { - throw new NavigationError( - url, - cause instanceof Error ? cause.message : String(cause), - ); + throw new NavigationError(url, cause instanceof Error ? cause.message : String(cause)); } } diff --git a/packages/debug-agent-browser/src/cdp-discovery.ts b/packages/debug-agent-browser/src/cdp-discovery.ts index c7618ca..416e43a 100644 --- a/packages/debug-agent-browser/src/cdp-discovery.ts +++ b/packages/debug-agent-browser/src/cdp-discovery.ts @@ -2,11 +2,7 @@ import * as fs from "node:fs/promises"; import net from "node:net"; import * as os from "node:os"; import * as path from "node:path"; -import { - CDP_COMMON_PORTS, - CDP_DISCOVERY_TIMEOUT_MS, - CDP_PORT_PROBE_TIMEOUT_MS, -} from "./constants"; +import { CDP_COMMON_PORTS, CDP_DISCOVERY_TIMEOUT_MS, CDP_PORT_PROBE_TIMEOUT_MS } from "./constants"; import { CdpDiscoveryError } from "./errors"; import { parseDevToolsActivePort } from "./utils/parse-devtools-active-port"; import { defaultLogger, type Logger } from "./logger"; diff --git a/packages/debug-agent-browser/src/chrome-launcher.ts b/packages/debug-agent-browser/src/chrome-launcher.ts index bfa400c..9ce3d49 100644 --- a/packages/debug-agent-browser/src/chrome-launcher.ts +++ b/packages/debug-agent-browser/src/chrome-launcher.ts @@ -236,7 +236,10 @@ export const launchSystemChrome = async ( const onSpawnError = (error: Error) => { settle(() => reject( - new ChromeLaunchTimeoutError(CDP_LAUNCH_TIMEOUT_MS, `Chrome process error: ${error.message}`), + new ChromeLaunchTimeoutError( + CDP_LAUNCH_TIMEOUT_MS, + `Chrome process error: ${error.message}`, + ), ), ); }; diff --git a/packages/debug-agent-browser/src/cookies/browser-detector.ts b/packages/debug-agent-browser/src/cookies/browser-detector.ts index 51a75eb..46849d6 100644 --- a/packages/debug-agent-browser/src/cookies/browser-detector.ts +++ b/packages/debug-agent-browser/src/cookies/browser-detector.ts @@ -21,8 +21,7 @@ export class Browsers { return results .flat() .filter( - (browser) => - browser._tag !== "ChromiumBrowser" || browser.profileName !== "System Profile", + (browser) => browser._tag !== "ChromiumBrowser" || browser.profileName !== "System Profile", ); } diff --git a/packages/debug-agent-browser/src/cookies/cdp-client.ts b/packages/debug-agent-browser/src/cookies/cdp-client.ts index 8cfe48d..8adb485 100644 --- a/packages/debug-agent-browser/src/cookies/cdp-client.ts +++ b/packages/debug-agent-browser/src/cookies/cdp-client.ts @@ -76,7 +76,8 @@ const findDebuggerUrl = async (port: number): Promise => { } catch (cause) { lastError = cause; } - const backoff = CDP_RETRY_BASE_DELAY_MS * 2 ** Math.min(attempt, CDP_RETRY_BACKOFF_CAP_EXPONENT); + const backoff = + CDP_RETRY_BASE_DELAY_MS * 2 ** Math.min(attempt, CDP_RETRY_BACKOFF_CAP_EXPONENT); await sleep(backoff); } throw new ExtractionError( diff --git a/packages/debug-agent-browser/src/cookies/chromium-sqlite.ts b/packages/debug-agent-browser/src/cookies/chromium-sqlite.ts index 62411f0..3705144 100644 --- a/packages/debug-agent-browser/src/cookies/chromium-sqlite.ts +++ b/packages/debug-agent-browser/src/cookies/chromium-sqlite.ts @@ -39,10 +39,7 @@ export interface DecryptFn { } export interface ChromiumKeyProvider { - buildDecryptor( - browserKey: ChromiumBrowserKey, - stripHashPrefix: boolean, - ): Promise; + buildDecryptor(browserKey: ChromiumBrowserKey, stripHashPrefix: boolean): Promise; } export const chromiumKeyProviderDarwin: ChromiumKeyProvider = { @@ -97,8 +94,7 @@ export const chromiumKeyProviderLinux: ChromiumKeyProvider = { const candidateKeys = Array.from(candidatePasswords).map((candidateKey) => deriveKey(candidateKey, PBKDF2_ITERATIONS_LINUX), ); - return (encrypted: Uint8Array) => - decryptAes128Cbc(encrypted, candidateKeys, stripHashPrefix); + return (encrypted: Uint8Array) => decryptAes128Cbc(encrypted, candidateKeys, stripHashPrefix); }, }; @@ -242,7 +238,8 @@ export class ChromiumSqliteFallback { let cookieValue = typeof row.value === "string" ? row.value : undefined; if (!cookieValue || cookieValue.length === 0) { - const encrypted = row.encrypted_value instanceof Uint8Array ? row.encrypted_value : undefined; + const encrypted = + row.encrypted_value instanceof Uint8Array ? row.encrypted_value : undefined; if (!encrypted) continue; const decrypted = decryptValue(encrypted); if (decrypted === undefined) continue; diff --git a/packages/debug-agent-browser/src/cookies/chromium.ts b/packages/debug-agent-browser/src/cookies/chromium.ts index ae5fc6b..714a2d1 100644 --- a/packages/debug-agent-browser/src/cookies/chromium.ts +++ b/packages/debug-agent-browser/src/cookies/chromium.ts @@ -74,8 +74,7 @@ export const createChromiumPlatformWin32 = async (): Promise = const programFiles = process.env["ProgramFiles"] ?? "C:\\Program Files"; const programFilesX86 = process.env["ProgramFiles(x86)"] ?? "C:\\Program Files (x86)"; - const localAppData = - process.env["LOCALAPPDATA"] ?? path.join(os.homedir(), "AppData", "Local"); + const localAppData = process.env["LOCALAPPDATA"] ?? path.join(os.homedir(), "AppData", "Local"); return { executableCandidates: (config) => { @@ -104,10 +103,7 @@ const exists = async (filePath: string): Promise => { } }; -const readJsonSafe = async ( - filePath: string, - schema: z.ZodType, -): Promise => { +const readJsonSafe = async (filePath: string, schema: z.ZodType): Promise => { try { const content = await fs.readFile(filePath, "utf-8"); return schema.parse(JSON.parse(content)); @@ -122,10 +118,7 @@ const getLastUsedProfile = async (userDataDir: string): Promise => { - const preferences = await readJsonSafe( - path.join(profilePath, "Preferences"), - preferencesSchema, - ); + const preferences = await readJsonSafe(path.join(profilePath, "Preferences"), preferencesSchema); if (!preferences) return undefined; const languages = preferences.intl?.selected_languages ?? preferences.intl?.accept_languages; if (!languages) return undefined; diff --git a/packages/debug-agent-browser/src/cookies/cookies.ts b/packages/debug-agent-browser/src/cookies/cookies.ts index dda1614..d73b7c6 100644 --- a/packages/debug-agent-browser/src/cookies/cookies.ts +++ b/packages/debug-agent-browser/src/cookies/cookies.ts @@ -8,14 +8,11 @@ import { ExtractionError, RequiresFullDiskAccess, UnknownError } from "./errors" import { parseBinaryCookies } from "./utils/binary-cookies"; import { Cookie, type Browser, type SameSitePolicy } from "./types"; import { defaultLogger, type Logger } from "./logger"; -import { - MS_PER_SECOND, - SAME_SITE_LAX, - SAME_SITE_NONE, - SAME_SITE_STRICT, -} from "./constants"; +import { MS_PER_SECOND, SAME_SITE_LAX, SAME_SITE_NONE, SAME_SITE_STRICT } from "./constants"; -const sqliteBoolSchema = z.union([z.number(), z.bigint()]).transform((value) => Number(value) !== 0); +const sqliteBoolSchema = z + .union([z.number(), z.bigint()]) + .transform((value) => Number(value) !== 0); const firefoxExpirySchema = z .union([z.number(), z.bigint(), z.string()]) @@ -139,6 +136,8 @@ export class Cookies { throw new ExtractionError(new UnknownError(cause)); } - return parseBinaryCookies(data).filter((cookie) => Boolean(cookie.name) && Boolean(cookie.domain)); + return parseBinaryCookies(data).filter( + (cookie) => Boolean(cookie.name) && Boolean(cookie.domain), + ); } } diff --git a/packages/debug-agent-browser/src/cookies/index.ts b/packages/debug-agent-browser/src/cookies/index.ts index 556be5b..31e4033 100644 --- a/packages/debug-agent-browser/src/cookies/index.ts +++ b/packages/debug-agent-browser/src/cookies/index.ts @@ -51,7 +51,12 @@ export { type ExtractionReason, } from "./errors"; -export { BROWSER_CONFIGS, configByKey, configByBundleId, configByDesktopFile } from "./browser-config"; +export { + BROWSER_CONFIGS, + configByKey, + configByBundleId, + configByDesktopFile, +} from "./browser-config"; export type { BrowserConfig, ChromiumConfig, FirefoxConfig, SafariConfig } from "./browser-config"; export { diff --git a/packages/debug-agent-browser/src/cookies/layers.ts b/packages/debug-agent-browser/src/cookies/layers.ts index 294f590..9176676 100644 --- a/packages/debug-agent-browser/src/cookies/layers.ts +++ b/packages/debug-agent-browser/src/cookies/layers.ts @@ -6,7 +6,12 @@ import { chromiumPlatformLinux, createChromiumPlatformWin32, } from "./chromium"; -import { registerFirefoxSource, firefoxPlatformDarwin, firefoxPlatformLinux, firefoxPlatformWin32 } from "./firefox"; +import { + registerFirefoxSource, + firefoxPlatformDarwin, + firefoxPlatformLinux, + firefoxPlatformWin32, +} from "./firefox"; import { registerSafariSource, safariPlatformDarwin } from "./safari"; import { defaultLogger, type Logger } from "./logger"; diff --git a/packages/debug-agent-browser/src/cookies/sqlite-client.ts b/packages/debug-agent-browser/src/cookies/sqlite-client.ts index 2b71911..8092afd 100644 --- a/packages/debug-agent-browser/src/cookies/sqlite-client.ts +++ b/packages/debug-agent-browser/src/cookies/sqlite-client.ts @@ -14,7 +14,10 @@ interface SqliteDatabase { export type SqliteEngine = "bun" | "node" | "libsql"; -const openDatabase = async (engine: SqliteEngine, databasePath: string): Promise => { +const openDatabase = async ( + engine: SqliteEngine, + databasePath: string, +): Promise => { if (engine === "bun") { const { Database } = await import(BUN_SQLITE_MODULE); return new Database(databasePath, { readonly: true }) as SqliteDatabase; diff --git a/packages/debug-agent-browser/src/cookies/utils/chromium-normalize.ts b/packages/debug-agent-browser/src/cookies/utils/chromium-normalize.ts index 9e9e3f4..1fb8d67 100644 --- a/packages/debug-agent-browser/src/cookies/utils/chromium-normalize.ts +++ b/packages/debug-agent-browser/src/cookies/utils/chromium-normalize.ts @@ -1,9 +1,4 @@ -import { - MS_PER_SECOND, - SAME_SITE_LAX, - SAME_SITE_NONE, - SAME_SITE_STRICT, -} from "../constants"; +import { MS_PER_SECOND, SAME_SITE_LAX, SAME_SITE_NONE, SAME_SITE_STRICT } from "../constants"; const MAX_UNIX_EPOCH_SECONDS = 253_402_300_799; const CHROME_EPOCH_THRESHOLD = 10_000_000_000_000; diff --git a/packages/debug-agent-browser/src/index.ts b/packages/debug-agent-browser/src/index.ts index 2d464c5..80a8725 100644 --- a/packages/debug-agent-browser/src/index.ts +++ b/packages/debug-agent-browser/src/index.ts @@ -11,12 +11,7 @@ export { export { consoleLogger, silentLogger, defaultLogger, type Logger } from "./logger"; -export type { - Browser as BrowserProfile, - BrowserKey, - Cookie, - ExtractOptions, -} from "./cookies"; +export type { Browser as BrowserProfile, BrowserKey, Cookie, ExtractOptions } from "./cookies"; export { BrowserError, diff --git a/packages/debug-agent-browser/tests/cdp-discovery.test.ts b/packages/debug-agent-browser/tests/cdp-discovery.test.ts index 790b5c3..ca0b0e3 100644 --- a/packages/debug-agent-browser/tests/cdp-discovery.test.ts +++ b/packages/debug-agent-browser/tests/cdp-discovery.test.ts @@ -43,28 +43,24 @@ describe("discoverCdpUrl", () => { }); it("falls back to /json/list when /json/version has no debugger URL", async () => { - fetchMock - .mockResolvedValueOnce(jsonResponse({})) - .mockResolvedValueOnce( - jsonResponse([ - { type: "page", webSocketDebuggerUrl: "ws://127.0.0.1:9222/devtools/page/xyz" }, - { type: "browser", webSocketDebuggerUrl: "ws://127.0.0.1:9222/devtools/browser/abc" }, - ]), - ); + fetchMock.mockResolvedValueOnce(jsonResponse({})).mockResolvedValueOnce( + jsonResponse([ + { type: "page", webSocketDebuggerUrl: "ws://127.0.0.1:9222/devtools/page/xyz" }, + { type: "browser", webSocketDebuggerUrl: "ws://127.0.0.1:9222/devtools/browser/abc" }, + ]), + ); const result = await discoverCdpUrl("127.0.0.1", 9222); expect(result).toBe("ws://127.0.0.1:9222/devtools/browser/abc"); }); it("prefers the 'browser' target in /json/list", async () => { - fetchMock - .mockResolvedValueOnce(jsonResponse({})) - .mockResolvedValueOnce( - jsonResponse([ - { type: "page", webSocketDebuggerUrl: "ws://127.0.0.1:9222/devtools/page/xyz" }, - { type: "browser", webSocketDebuggerUrl: "ws://127.0.0.1:9222/devtools/browser/abc" }, - ]), - ); + fetchMock.mockResolvedValueOnce(jsonResponse({})).mockResolvedValueOnce( + jsonResponse([ + { type: "page", webSocketDebuggerUrl: "ws://127.0.0.1:9222/devtools/page/xyz" }, + { type: "browser", webSocketDebuggerUrl: "ws://127.0.0.1:9222/devtools/browser/abc" }, + ]), + ); const result = await discoverCdpUrl("127.0.0.1", 9222); expect(result).toContain("/devtools/browser/abc"); @@ -90,9 +86,7 @@ describe("discoverCdpUrl", () => { }); it("throws CdpDiscoveryError when /json/list returns empty array", async () => { - fetchMock - .mockResolvedValueOnce(jsonResponse({})) - .mockResolvedValueOnce(jsonResponse([])); + fetchMock.mockResolvedValueOnce(jsonResponse({})).mockResolvedValueOnce(jsonResponse([])); await expect(discoverCdpUrl("127.0.0.1", 9222)).rejects.toBeInstanceOf(CdpDiscoveryError); }); diff --git a/packages/debug-agent-browser/tests/cookies/crypto.test.ts b/packages/debug-agent-browser/tests/cookies/crypto.test.ts index 186f4ee..44ed5a9 100644 --- a/packages/debug-agent-browser/tests/cookies/crypto.test.ts +++ b/packages/debug-agent-browser/tests/cookies/crypto.test.ts @@ -1,10 +1,6 @@ import * as crypto from "node:crypto"; import { describe, expect, it } from "vite-plus/test"; -import { - decryptAes128Cbc, - decryptAes256Gcm, - deriveKey, -} from "../../src/cookies/utils/crypto"; +import { decryptAes128Cbc, decryptAes256Gcm, deriveKey } from "../../src/cookies/utils/crypto"; const PBKDF2_SALT = "saltysalt"; const PBKDF2_KEY_LENGTH_BYTES = 16; @@ -32,13 +28,7 @@ describe("deriveKey", () => { const key = deriveKey("peanuts", 1); expect(key.length).toBe(PBKDF2_KEY_LENGTH_BYTES); - const expected = crypto.pbkdf2Sync( - "peanuts", - PBKDF2_SALT, - 1, - PBKDF2_KEY_LENGTH_BYTES, - "sha1", - ); + const expected = crypto.pbkdf2Sync("peanuts", PBKDF2_SALT, 1, PBKDF2_KEY_LENGTH_BYTES, "sha1"); expect(key.equals(expected)).toBe(true); }); From 2bc948a7686c3398ea2e700c977bd303a8377dbb Mon Sep 17 00:00:00 2001 From: Nisarg Patel Date: Sat, 16 May 2026 00:03:02 -0700 Subject: [PATCH 3/3] feat: add hosted remote log relay for production debugging MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces a hosted log relay so production apps can push NDJSON logs to a public URL and a local debug-agent session can read them back. Solves the problem that `localhost` endpoints don't work for production bugs. - New package `@debug-agent/remote`: Cloudflare Worker with a SQLite-backed Durable Object (`LogSession`). Each session is created by `POST /sessions` (nanoid id), auto-expires after 1 hour via a DO alarm that calls `deleteAll()`, and enforces 10K entries / 10KB per entry / 100MB total limits with `413 Payload Too Large`. Routes: `POST/GET/DELETE /s/:id` and SSE at `GET /s/:id/stream` (replays buffered logs then streams live). Returns `410 Gone` for expired or unknown sessions. CORS open on every response. - New `remote` subcommand in `debug-agent` CLI with `--daemon`, `--json`, and interactive modes. SSE is parsed manually from `fetch()` — no EventSource dep. Default Worker URL is configurable via `--url`. - Updated SKILL.md with a local-vs-remote decision section and remote variants of STEP 0, STEP 2, STEP 4, plus a remote API reference table. Deployed at https://debug-agent-remote.aidenbai.workers.dev and end-to-end verified via the debug-agent skill workflow. Co-authored-by: Cursor --- .gitignore | 3 + .ultraplans/remote-logging/prompt.md | 58 +- packages/debug-agent-remote/package.json | 32 ++ packages/debug-agent-remote/src/constants.ts | 5 + packages/debug-agent-remote/src/env.d.ts | 3 + packages/debug-agent-remote/src/index.ts | 113 ++++ .../debug-agent-remote/src/log-session.ts | 317 +++++++++++ packages/debug-agent-remote/tsconfig.json | 17 + packages/debug-agent-remote/wrangler.jsonc | 25 + packages/debug-agent/skill/SKILL.md | 50 +- packages/debug-agent/src/cli.ts | 4 +- packages/debug-agent/src/commands/remote.ts | 221 +++++++ packages/debug-agent/src/constants.ts | 1 + .../debug-agent/src/utils/parse-sse-stream.ts | 57 ++ pnpm-lock.yaml | 538 +++++++++++++++++- 15 files changed, 1421 insertions(+), 23 deletions(-) create mode 100644 packages/debug-agent-remote/package.json create mode 100644 packages/debug-agent-remote/src/constants.ts create mode 100644 packages/debug-agent-remote/src/env.d.ts create mode 100644 packages/debug-agent-remote/src/index.ts create mode 100644 packages/debug-agent-remote/src/log-session.ts create mode 100644 packages/debug-agent-remote/tsconfig.json create mode 100644 packages/debug-agent-remote/wrangler.jsonc create mode 100644 packages/debug-agent/src/commands/remote.ts create mode 100644 packages/debug-agent/src/utils/parse-sse-stream.ts diff --git a/.gitignore b/.gitignore index c970128..7bccaa3 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,6 @@ dist *.log .DS_Store .next +.wrangler +.dev.vars +.dev.vars.* diff --git a/.ultraplans/remote-logging/prompt.md b/.ultraplans/remote-logging/prompt.md index 825e7c4..5df8216 100644 --- a/.ultraplans/remote-logging/prompt.md +++ b/.ultraplans/remote-logging/prompt.md @@ -28,6 +28,7 @@ Two changes: SQLite-backed Durable Object class. One instance per debug session. **On creation (first request):** + - Record `createdAt` timestamp in storage - Set an alarm for 1 hour from now via `this.ctx.storage.setAlarm(Date.now() + 3_600_000)` - Create a SQLite table for log entries: @@ -42,17 +43,20 @@ SQLite-backed Durable Object class. One instance per debug session. (`entry_id` is the optional dedup `id` field from the log payload; `data` is the full JSON string) **Storage limits (enforce on POST):** + - Max 10,000 log entries per session - Max 10KB (10,240 bytes) per individual log entry (the JSON string) - Max 100MB (104,857,600 bytes) total storage per session (sum of all `data` column lengths) - Return `413 Payload Too Large` with a JSON error body when any limit is exceeded **Alarm handler (`alarm()`):** + - Call `this.ctx.storage.deleteAll()` — this clears all SQLite data AND the alarm itself (compatibility date 2026-02-24+) - Close all active SSE connections with an `expired` event before closing - The Durable Object ceases to exist once storage is empty and it shuts down **In-memory state:** + - `Set` for active SSE connections (to broadcast new logs) - `initialized: boolean` flag to track if the SQLite table has been created @@ -62,16 +66,17 @@ The Worker routes requests to the appropriate Durable Object. **Routes:** -| Method | Path | Handler | -|--------|------|---------| -| `POST` | `/sessions` | Create a new session: generate nanoid(21), get DO stub by name, call DO to initialize, return session info | -| `POST` | `/s/:id` | Forward to DO — ingest a log entry | -| `GET` | `/s/:id` | Forward to DO — return all buffered logs as NDJSON | -| `GET` | `/s/:id/stream` | Forward to DO — SSE stream | -| `DELETE` | `/s/:id` | Forward to DO — clear logs (but keep session alive) | -| `OPTIONS` | `*` | CORS preflight | +| Method | Path | Handler | +| --------- | --------------- | ---------------------------------------------------------------------------------------------------------- | +| `POST` | `/sessions` | Create a new session: generate nanoid(21), get DO stub by name, call DO to initialize, return session info | +| `POST` | `/s/:id` | Forward to DO — ingest a log entry | +| `GET` | `/s/:id` | Forward to DO — return all buffered logs as NDJSON | +| `GET` | `/s/:id/stream` | Forward to DO — SSE stream | +| `DELETE` | `/s/:id` | Forward to DO — clear logs (but keep session alive) | +| `OPTIONS` | `*` | CORS preflight | **CORS headers on ALL responses:** + ``` Access-Control-Allow-Origin: * Access-Control-Allow-Methods: GET, POST, DELETE, OPTIONS @@ -79,6 +84,7 @@ Access-Control-Allow-Headers: Content-Type ``` **Session creation (`POST /sessions`):** + - Generate a nanoid (21 chars, URL-safe alphabet, use `crypto.getRandomValues` — no npm dependency needed) - Derive the Durable Object ID from the nanoid using `env.LOG_SESSION.idFromName(sessionId)` - Call the DO to initialize it (POST with empty body or a special init action) @@ -93,6 +99,7 @@ Access-Control-Allow-Headers: Content-Type ``` **For all `/s/:id` routes:** + - Parse the session ID from the URL - Get the DO stub via `env.LOG_SESSION.idFromName(sessionId)` - Forward the request to the DO @@ -101,6 +108,7 @@ Access-Control-Allow-Headers: Content-Type ### Durable Object request handling **`POST /s/:id` (ingest):** + - Parse JSON body - If body has an `id` field, check for duplicates: `SELECT 1 FROM logs WHERE entry_id = ? LIMIT 1` - If duplicate, return `{ ok: true, duplicate: true }` @@ -111,11 +119,13 @@ Access-Control-Allow-Headers: Content-Type - Return `{ ok: true }` **`GET /s/:id` (read all):** + - `SELECT data FROM logs ORDER BY id ASC` - Concatenate all `data` values with `\n` separator - Return with `Content-Type: application/x-ndjson` **`GET /s/:id/stream` (SSE):** + - Return a streaming `Response` with: ``` Content-Type: text/event-stream @@ -146,6 +156,7 @@ Access-Control-Allow-Headers: Content-Type - Register the writer in the DO's in-memory SSE connection set. Remove on disconnect. **`DELETE /s/:id` (clear):** + - `DELETE FROM logs` - Reset any in-memory dedup tracking - Return `{ ok: true, cleared: true }` @@ -201,20 +212,28 @@ export const NANOID_ALPHABET = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZ Add a `remote` subcommand to the existing CLI in `src/cli.ts`. Follow the exact same patterns as `src/commands/serve.ts`. **Options:** + - `--url ` — Override the Worker URL (default: hardcoded constant) - `--daemon` — Create session, print JSON info, exit - `--json` — Create session, start SSE streaming, output NDJSON lines to stdout - (no flags) — Interactive mode with spinner + pretty output **Daemon mode (`--daemon`):** + 1. POST to `{workerUrl}/sessions` to create a session 2. Print the response JSON to stdout (single line): ```json - {"sessionId":"...","endpoint":"https://...","streamUrl":"https://...","expiresAt":1733460389000} + { + "sessionId": "...", + "endpoint": "https://...", + "streamUrl": "https://...", + "expiresAt": 1733460389000 + } ``` 3. Exit immediately (nothing to background — the Worker is already running) **JSON mode (`--json`):** + 1. Create session (same as daemon) 2. Print session info JSON line 3. Connect to SSE stream at `streamUrl` @@ -222,6 +241,7 @@ Add a `remote` subcommand to the existing CLI in `src/cli.ts`. Follow the exact 5. On `expired` event, print `{"event":"expired"}` and exit **Interactive mode:** + 1. Create session with spinner 2. Display session info: ``` @@ -239,6 +259,7 @@ Use native `fetch()` with streaming response body (no EventSource dependency nee ### Constants update Add to `packages/debug-agent/src/constants.ts`: + ```typescript export const DEFAULT_REMOTE_URL = "https://debug-agent-remote..workers.dev"; ``` @@ -248,6 +269,7 @@ The actual workers.dev subdomain will be determined after first `wrangler deploy ### CLI registration In `src/cli.ts`, add: + ```typescript import { remoteCommand } from "./commands/remote.js"; // ... @@ -288,10 +310,10 @@ The command prints a single JSON line to stdout and exits: \`\`\`json { - "sessionId": "V1StGXR8_Z5jdHi6B-myT", - "endpoint": "https://xxx.workers.dev/s/V1StGXR8_Z5jdHi6B-myT", - "streamUrl": "https://xxx.workers.dev/s/V1StGXR8_Z5jdHi6B-myT/stream", - "expiresAt": 1733460389000 +"sessionId": "V1StGXR8_Z5jdHi6B-myT", +"endpoint": "https://xxx.workers.dev/s/V1StGXR8_Z5jdHi6B-myT", +"streamUrl": "https://xxx.workers.dev/s/V1StGXR8_Z5jdHi6B-myT/stream", +"expiresAt": 1733460389000 } \`\`\` @@ -327,12 +349,12 @@ Add the remote API: ```markdown ### Remote API (remote mode) -| Method | Path | Effect | -|--------|------|--------| -| `POST /s/:id` | Append JSON body as NDJSON log entry | -| `GET /s/:id` | Read all buffered log entries as NDJSON | +| Method | Path | Effect | +| ------------------- | -------------------------------------------- | ------ | +| `POST /s/:id` | Append JSON body as NDJSON log entry | +| `GET /s/:id` | Read all buffered log entries as NDJSON | | `GET /s/:id/stream` | SSE stream (replays buffered logs then live) | -| `DELETE /s/:id` | Clear all log entries (session stays alive) | +| `DELETE /s/:id` | Clear all log entries (session stays alive) | ``` --- diff --git a/packages/debug-agent-remote/package.json b/packages/debug-agent-remote/package.json new file mode 100644 index 0000000..d48f153 --- /dev/null +++ b/packages/debug-agent-remote/package.json @@ -0,0 +1,32 @@ +{ + "name": "@debug-agent/remote", + "version": "0.0.1", + "private": true, + "description": "Hosted log relay for debug-agent (Cloudflare Worker + Durable Object)", + "license": "MIT", + "author": { + "name": "Aiden Bai", + "email": "aiden@million.dev" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/millionco/debug-agent.git" + }, + "type": "module", + "scripts": { + "dev": "wrangler dev", + "deploy": "wrangler deploy", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "nanoid": "^5.1.11" + }, + "devDependencies": { + "@cloudflare/workers-types": "^4.20260516.1", + "typescript": "^6.0.2", + "wrangler": "^4.92.0" + }, + "engines": { + "node": ">=22" + } +} diff --git a/packages/debug-agent-remote/src/constants.ts b/packages/debug-agent-remote/src/constants.ts new file mode 100644 index 0000000..6924034 --- /dev/null +++ b/packages/debug-agent-remote/src/constants.ts @@ -0,0 +1,5 @@ +export const SESSION_TTL_MS = 3_600_000; +export const MAX_LOG_ENTRIES = 10_000; +export const MAX_ENTRY_SIZE_BYTES = 10_240; +export const MAX_TOTAL_STORAGE_BYTES = 104_857_600; +export const NANOID_LENGTH = 21; diff --git a/packages/debug-agent-remote/src/env.d.ts b/packages/debug-agent-remote/src/env.d.ts new file mode 100644 index 0000000..d7ce42d --- /dev/null +++ b/packages/debug-agent-remote/src/env.d.ts @@ -0,0 +1,3 @@ +interface Env { + LOG_SESSION: DurableObjectNamespace; +} diff --git a/packages/debug-agent-remote/src/index.ts b/packages/debug-agent-remote/src/index.ts new file mode 100644 index 0000000..336f7c5 --- /dev/null +++ b/packages/debug-agent-remote/src/index.ts @@ -0,0 +1,113 @@ +import { nanoid } from "nanoid"; +import { NANOID_LENGTH } from "./constants.js"; +import { LogSession } from "./log-session.js"; + +export { LogSession }; + +const CORS_HEADERS: Record = { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type", +}; + +const SESSION_ID_PATTERN = /^[A-Za-z0-9_-]{8,64}$/; + +const generateSessionId = (): string => nanoid(NANOID_LENGTH); + +const jsonResponse = (body: unknown, init: ResponseInit = {}): Response => + new Response(JSON.stringify(body), { + ...init, + headers: { + "Content-Type": "application/json", + ...CORS_HEADERS, + ...init.headers, + }, + }); + +const withCors = (response: Response): Response => { + const headers = new Headers(response.headers); + for (const [name, value] of Object.entries(CORS_HEADERS)) { + headers.set(name, value); + } + return new Response(response.body, { + status: response.status, + statusText: response.statusText, + headers, + }); +}; + +const forwardToSession = async ( + env: Env, + sessionId: string, + internalPath: string, + init: RequestInit, +): Promise => { + const doId = env.LOG_SESSION.idFromName(sessionId); + const stub = env.LOG_SESSION.get(doId); + const internalUrl = `https://do.internal${internalPath}?sessionId=${encodeURIComponent(sessionId)}`; + const response = await stub.fetch(internalUrl, init); + return withCors(response); +}; + +export default { + async fetch(request: Request, env: Env): Promise { + const url = new URL(request.url); + + if (request.method === "OPTIONS") { + return new Response(null, { status: 204, headers: CORS_HEADERS }); + } + + if (url.pathname === "/" && request.method === "GET") { + return jsonResponse({ ok: true, service: "debug-agent-remote" }); + } + + if (url.pathname === "/sessions" && request.method === "POST") { + const sessionId = generateSessionId(); + const workerOrigin = `${url.protocol}//${url.host}`; + const doId = env.LOG_SESSION.idFromName(sessionId); + const stub = env.LOG_SESSION.get(doId); + const initUrl = + `https://do.internal/init?sessionId=${encodeURIComponent(sessionId)}` + + `&origin=${encodeURIComponent(workerOrigin)}`; + const initResponse = await stub.fetch(initUrl, { method: "POST" }); + return withCors(initResponse); + } + + const sessionMatch = url.pathname.match(/^\/s\/([^/]+)(\/stream)?\/?$/); + if (sessionMatch) { + const sessionId = sessionMatch[1]; + if (!SESSION_ID_PATTERN.test(sessionId)) { + return jsonResponse({ error: "Invalid session id" }, { status: 400 }); + } + const isStream = Boolean(sessionMatch[2]); + + if (isStream) { + if (request.method !== "GET") { + return jsonResponse({ error: "Method not allowed" }, { status: 405 }); + } + return forwardToSession(env, sessionId, "/stream", { method: "GET" }); + } + + if (request.method === "POST") { + const bodyText = await request.text(); + return forwardToSession(env, sessionId, "/log", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: bodyText, + }); + } + + if (request.method === "GET") { + return forwardToSession(env, sessionId, "/log", { method: "GET" }); + } + + if (request.method === "DELETE") { + return forwardToSession(env, sessionId, "/log", { method: "DELETE" }); + } + + return jsonResponse({ error: "Method not allowed" }, { status: 405 }); + } + + return jsonResponse({ error: "Not found" }, { status: 404 }); + }, +} satisfies ExportedHandler; diff --git a/packages/debug-agent-remote/src/log-session.ts b/packages/debug-agent-remote/src/log-session.ts new file mode 100644 index 0000000..82ae588 --- /dev/null +++ b/packages/debug-agent-remote/src/log-session.ts @@ -0,0 +1,317 @@ +import { DurableObject } from "cloudflare:workers"; +import { + MAX_ENTRY_SIZE_BYTES, + MAX_LOG_ENTRIES, + MAX_TOTAL_STORAGE_BYTES, + SESSION_TTL_MS, +} from "./constants.js"; + +interface LogPayload { + id?: unknown; + sessionId?: unknown; + timestamp?: unknown; + [key: string]: unknown; +} + +interface CountRow extends Record { + count: number; +} + +interface TotalRow extends Record { + total: number | null; +} + +interface DataRow extends Record { + data: string; +} + +interface ExistsRow extends Record { + one: number; +} + +const CORS_HEADERS: Record = { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type", +}; + +const jsonResponse = (body: unknown, init: ResponseInit = {}): Response => + new Response(JSON.stringify(body), { + ...init, + headers: { + "Content-Type": "application/json", + ...CORS_HEADERS, + ...init.headers, + }, + }); + +const sseEvent = (event: string, data: unknown): string => + `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`; + +export class LogSession extends DurableObject { + private initialized = false; + private createdAt = 0; + private sseWriters = new Set>(); + private textEncoder = new TextEncoder(); + + override async fetch(request: Request): Promise { + const url = new URL(request.url); + const action = url.pathname.split("/").pop() || ""; + + if (request.method === "OPTIONS") { + return new Response(null, { status: 204, headers: CORS_HEADERS }); + } + + if (action === "init" && request.method === "POST") { + const sessionId = url.searchParams.get("sessionId") || ""; + const workerOrigin = url.searchParams.get("origin") || ""; + await this.ensureInitialized(); + return jsonResponse({ + sessionId, + endpoint: `${workerOrigin}/s/${sessionId}`, + streamUrl: `${workerOrigin}/s/${sessionId}/stream`, + expiresAt: this.createdAt + SESSION_TTL_MS, + }); + } + + if (!(await this.sessionExists())) { + return jsonResponse({ error: "Session expired or not found" }, { status: 410 }); + } + + if (action === "log" && request.method === "POST") { + return this.handleIngest(request, url); + } + + if (action === "log" && request.method === "GET") { + return this.handleReadAll(); + } + + if (action === "stream" && request.method === "GET") { + return this.handleStream(url); + } + + if (action === "log" && request.method === "DELETE") { + return this.handleClear(); + } + + return jsonResponse({ error: "Not found" }, { status: 404 }); + } + + override async alarm(): Promise { + for (const writer of this.sseWriters) { + try { + await writer.write( + this.textEncoder.encode(sseEvent("expired", { reason: "Session expired after 1 hour" })), + ); + await writer.close(); + } catch {} + } + this.sseWriters.clear(); + await this.ctx.storage.deleteAll(); + this.initialized = false; + this.createdAt = 0; + } + + private async sessionExists(): Promise { + if (this.initialized) return true; + const createdAt = await this.ctx.storage.get("createdAt"); + if (typeof createdAt !== "number") return false; + this.createdAt = createdAt; + this.ensureSchema(); + this.initialized = true; + return true; + } + + private async ensureInitialized(): Promise { + if (this.initialized) return; + const existingCreatedAt = await this.ctx.storage.get("createdAt"); + if (typeof existingCreatedAt === "number") { + this.createdAt = existingCreatedAt; + } else { + this.createdAt = Date.now(); + await this.ctx.storage.put("createdAt", this.createdAt); + await this.ctx.storage.setAlarm(this.createdAt + SESSION_TTL_MS); + } + this.ensureSchema(); + this.initialized = true; + } + + private ensureSchema(): void { + this.ctx.storage.sql.exec( + `CREATE TABLE IF NOT EXISTS logs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + entry_id TEXT, + data TEXT NOT NULL, + created_at INTEGER NOT NULL DEFAULT (unixepoch('subsec') * 1000) + )`, + ); + this.ctx.storage.sql.exec(`CREATE INDEX IF NOT EXISTS logs_entry_id_idx ON logs(entry_id)`); + } + + private async handleIngest(request: Request, url: URL): Promise { + const sessionId = url.searchParams.get("sessionId") || ""; + let payload: LogPayload; + try { + payload = (await request.json()) as LogPayload; + } catch { + return jsonResponse({ error: "Invalid JSON" }, { status: 400 }); + } + + const entryId = typeof payload.id === "string" ? payload.id : null; + if (entryId) { + const duplicateRow = this.ctx.storage.sql + .exec("SELECT 1 AS one FROM logs WHERE entry_id = ? LIMIT 1", entryId) + .toArray(); + if (duplicateRow.length > 0) { + return jsonResponse({ ok: true, duplicate: true }); + } + } + + if (!payload.sessionId) payload.sessionId = sessionId; + if (!payload.timestamp) payload.timestamp = Date.now(); + + const serialized = JSON.stringify(payload); + const entryByteLength = this.textEncoder.encode(serialized).byteLength; + + if (entryByteLength > MAX_ENTRY_SIZE_BYTES) { + return jsonResponse( + { + error: "Log entry too large", + maxBytes: MAX_ENTRY_SIZE_BYTES, + actualBytes: entryByteLength, + }, + { status: 413 }, + ); + } + + const countRow = this.ctx.storage.sql + .exec("SELECT COUNT(*) AS count FROM logs") + .one(); + if (countRow.count >= MAX_LOG_ENTRIES) { + return jsonResponse( + { error: "Maximum log entries reached", maxEntries: MAX_LOG_ENTRIES }, + { status: 413 }, + ); + } + + const totalRow = this.ctx.storage.sql + .exec("SELECT COALESCE(SUM(LENGTH(data)), 0) AS total FROM logs") + .one(); + const currentTotalBytes = totalRow.total ?? 0; + if (currentTotalBytes + entryByteLength > MAX_TOTAL_STORAGE_BYTES) { + return jsonResponse( + { + error: "Maximum storage size reached", + maxBytes: MAX_TOTAL_STORAGE_BYTES, + currentBytes: currentTotalBytes, + }, + { status: 413 }, + ); + } + + this.ctx.storage.sql.exec( + "INSERT INTO logs (entry_id, data) VALUES (?, ?)", + entryId, + serialized, + ); + + await this.broadcast(sseEvent("log", payload)); + + return jsonResponse({ ok: true }); + } + + private handleReadAll(): Response { + const rows = this.ctx.storage.sql + .exec("SELECT data FROM logs ORDER BY id ASC") + .toArray(); + const body = rows.map((innerRow) => innerRow.data).join("\n"); + return new Response(body, { + status: 200, + headers: { + "Content-Type": "application/x-ndjson", + ...CORS_HEADERS, + }, + }); + } + + private async handleStream(url: URL): Promise { + const sessionId = url.searchParams.get("sessionId") || ""; + const stream = new TransformStream(); + const writer = stream.writable.getWriter(); + this.sseWriters.add(writer); + + const rows = this.ctx.storage.sql + .exec("SELECT data FROM logs ORDER BY id ASC") + .toArray(); + + const connectedPayload = { + sessionId, + expiresAt: this.createdAt + SESSION_TTL_MS, + bufferedLogs: rows.length, + }; + + const removeWriter = () => { + this.sseWriters.delete(writer); + try { + writer.close(); + } catch {} + }; + + this.ctx.waitUntil( + (async () => { + try { + await writer.write(this.textEncoder.encode(sseEvent("connected", connectedPayload))); + for (const innerRow of rows) { + let parsed: unknown; + try { + parsed = JSON.parse(innerRow.data); + } catch { + parsed = innerRow.data; + } + await writer.write(this.textEncoder.encode(sseEvent("log", parsed))); + } + await writer.write(this.textEncoder.encode(sseEvent("replay-complete", {}))); + } catch { + removeWriter(); + } + })(), + ); + + return new Response(stream.readable, { + status: 200, + headers: { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + Connection: "keep-alive", + "X-Accel-Buffering": "no", + ...CORS_HEADERS, + }, + }); + } + + private handleClear(): Response { + this.ctx.storage.sql.exec("DELETE FROM logs"); + return jsonResponse({ ok: true, cleared: true }); + } + + private async broadcast(payload: string): Promise { + if (this.sseWriters.size === 0) return; + const chunk = this.textEncoder.encode(payload); + const failed: WritableStreamDefaultWriter[] = []; + await Promise.all( + Array.from(this.sseWriters).map(async (innerWriter) => { + try { + await innerWriter.write(chunk); + } catch { + failed.push(innerWriter); + } + }), + ); + for (const innerWriter of failed) { + this.sseWriters.delete(innerWriter); + try { + innerWriter.close(); + } catch {} + } + } +} diff --git a/packages/debug-agent-remote/tsconfig.json b/packages/debug-agent-remote/tsconfig.json new file mode 100644 index 0000000..e846f91 --- /dev/null +++ b/packages/debug-agent-remote/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "esnext", + "module": "esnext", + "moduleResolution": "bundler", + "lib": ["esnext"], + "types": ["@cloudflare/workers-types"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "isolatedModules": true, + "noEmit": true, + "resolveJsonModule": true + }, + "include": ["src"], + "exclude": ["node_modules"] +} diff --git a/packages/debug-agent-remote/wrangler.jsonc b/packages/debug-agent-remote/wrangler.jsonc new file mode 100644 index 0000000..b3c3972 --- /dev/null +++ b/packages/debug-agent-remote/wrangler.jsonc @@ -0,0 +1,25 @@ +{ + "$schema": "node_modules/wrangler/config-schema.json", + "name": "debug-agent-remote", + "account_id": "5640a6fb804378931e7c364873200e9c", + "main": "src/index.ts", + "compatibility_date": "2026-02-24", + "compatibility_flags": ["nodejs_compat"], + "observability": { + "enabled": true, + }, + "durable_objects": { + "bindings": [ + { + "name": "LOG_SESSION", + "class_name": "LogSession", + }, + ], + }, + "migrations": [ + { + "tag": "v1", + "new_sqlite_classes": ["LogSession"], + }, + ], +} diff --git a/packages/debug-agent/skill/SKILL.md b/packages/debug-agent/skill/SKILL.md index 95cad3a..36d7d10 100644 --- a/packages/debug-agent/skill/SKILL.md +++ b/packages/debug-agent/skill/SKILL.md @@ -40,9 +40,16 @@ They guess based on code alone. You **cannot** and **must NOT** fix bugs this wa ## Logging +### Choosing local vs remote mode + +- **Local mode** (default): Use when the buggy code runs on the same machine as this agent (localhost, local dev server, local scripts). Logs are written to a file on disk. +- **Remote mode**: Use when the buggy code runs on a remote server, cloud environment, or production — anywhere that cannot reach `localhost`. Logs are relayed through a hosted service and read over HTTP. + +If the bug is in code that runs remotely or in production, use remote mode. Otherwise, use local mode. + ### STEP 0: Start the logging server (MANDATORY BEFORE ANY INSTRUMENTATION) -Run the debug server in **daemon mode** before any instrumentation. The `--daemon` flag starts the server in the background and exits immediately with the server info — no backgrounding or `&` required. +**Local mode** — run the debug server in **daemon mode** before any instrumentation. The `--daemon` flag starts the server in the background and exits immediately with the server info — no backgrounding or `&` required. ```bash npx debug-agent --daemon @@ -71,6 +78,27 @@ If the server fails to start, STOP IMMEDIATELY and inform the user. - The server is idempotent — if one is already running, it returns the existing server's info instead of starting a duplicate. - You do not need to pre-create the log file; it will be created automatically when your instrumentation first writes to it. +**Remote mode** — run this instead: + +```bash +npx debug-agent remote --daemon +``` + +The command prints a single JSON line to stdout and exits: + +```json +{ + "sessionId": "V1StGXR8_Z5jdHi6B-myT", + "endpoint": "https://debug-agent-remote.aidenbai.workers.dev/s/V1StGXR8_Z5jdHi6B-myT", + "streamUrl": "https://debug-agent-remote.aidenbai.workers.dev/s/V1StGXR8_Z5jdHi6B-myT/stream", + "expiresAt": 1733460389000 +} +``` + +Capture the **endpoint** value. There is no local log file in remote mode. + +**Important:** Remote sessions expire after 1 hour. If the session expires mid-debug, create a new one. + ### STEP 1: Understand the log format - Logs are written in **NDJSON format** (one JSON object per line) to the file specified by the **log path**. @@ -102,6 +130,8 @@ fetch('ENDPOINT',{method:'POST',headers:{'Content-Type':'application/json'},body - In **non-JavaScript languages** (Python, Go, Rust, Java, C, C++, Ruby), instrument by opening the **log path** in append mode using standard library file I/O, writing a single NDJSON line with your payload, and then closing the file. Keep these snippets as tiny and compact as possible (ideally one line, or just a few). +- In **remote mode**, ALL languages must use HTTP POST to the **endpoint** (there is no local log file). Use `fetch`, `curl`, `requests.post`, `http.Post`, or equivalent for your language. + - Decide how many instrumentation logs to insert based on the complexity of the code under investigation and the hypotheses you are testing. A single well-placed log may be enough when the issue is highly localized; complex multi-step flows may need more. Aim for the minimum number that can confirm or reject ALL your hypotheses. Guidelines: - At least 1 log is required; never skip instrumentation entirely - Do not exceed 10 logs — if you think you need more, narrow your hypotheses first @@ -136,6 +166,13 @@ fetch('ENDPOINT',{method:'POST',headers:{'Content-Type':'application/json'},body - The log file will contain NDJSON entries (one JSON object per line) from your instrumentation. - Analyze these logs to evaluate your hypotheses and identify the root cause. - If log file is empty or missing: tell user the reproduction may have failed and ask them to try again. +- In **remote mode**, fetch logs via HTTP instead of reading a file: + + ```bash + curl -s ENDPOINT + ``` + + This returns the same NDJSON format as the local log file. ### STEP 5: Keep logs during fixes @@ -177,8 +214,19 @@ This is why wrapping every debug log in `#region debug log` / `#endregion` is ma ## Server API reference +### Local API (local mode) + | Method | Effect | | --------------------------- | ------------------------------------------- | | `POST /ingest/:sessionId` | Append JSON body as NDJSON line to log file | | `GET /ingest/:sessionId` | Read full log file contents | | `DELETE /ingest/:sessionId` | Clear the log file | + +### Remote API (remote mode) + +| Method | Effect | +| ------------------- | -------------------------------------------- | +| `POST /s/:id` | Append JSON body as NDJSON log entry | +| `GET /s/:id` | Read all buffered log entries as NDJSON | +| `GET /s/:id/stream` | SSE stream (replays buffered logs then live) | +| `DELETE /s/:id` | Clear all log entries (session stays alive) | diff --git a/packages/debug-agent/src/cli.ts b/packages/debug-agent/src/cli.ts index 174b4a2..8f22824 100644 --- a/packages/debug-agent/src/cli.ts +++ b/packages/debug-agent/src/cli.ts @@ -3,6 +3,7 @@ import { Command } from "commander"; import { serveCommand } from "./commands/serve.js"; import { initCommand } from "./commands/init.js"; +import { remoteCommand } from "./commands/remote.js"; import { VERSION_API_URL } from "./constants.js"; const VERSION = process.env.VERSION ?? "0.0.0"; @@ -16,6 +17,7 @@ const program = new Command() .description("Debugging skills for AI agents") .version(VERSION, "-v, --version", "display the version number") .addCommand(serveCommand, { isDefault: true }) - .addCommand(initCommand); + .addCommand(initCommand) + .addCommand(remoteCommand); program.parse(); diff --git a/packages/debug-agent/src/commands/remote.ts b/packages/debug-agent/src/commands/remote.ts new file mode 100644 index 0000000..2c37d9b --- /dev/null +++ b/packages/debug-agent/src/commands/remote.ts @@ -0,0 +1,221 @@ +import { Command } from "commander"; +import { DEFAULT_REMOTE_URL } from "../constants.js"; +import { logger } from "../utils/logger.js"; +import { highlighter } from "../utils/highlighter.js"; +import { spinner } from "../utils/spinner.js"; +import { parseSseStream } from "../utils/parse-sse-stream.js"; +import { getErrorMessage } from "../utils/get-error-message.js"; + +interface RemoteSessionInfo { + sessionId: string; + endpoint: string; + streamUrl: string; + expiresAt: number; +} + +interface RemoteOptions { + url?: string; + daemon?: boolean; + json?: boolean; +} + +const stripTrailingSlash = (input: string): string => + input.endsWith("/") ? input.slice(0, -1) : input; + +const createRemoteSession = async (workerUrl: string): Promise => { + const response = await fetch(`${stripTrailingSlash(workerUrl)}/sessions`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: "{}", + }); + if (!response.ok) { + throw new Error(`Failed to create remote session (HTTP ${response.status})`); + } + return (await response.json()) as RemoteSessionInfo; +}; + +const openStream = async ( + streamUrl: string, + signal: AbortSignal, +): Promise> => { + const response = await fetch(streamUrl, { + method: "GET", + headers: { Accept: "text/event-stream" }, + signal, + }); + if (!response.ok || !response.body) { + throw new Error(`Failed to open SSE stream (HTTP ${response.status})`); + } + return response.body; +}; + +const formatRemainingMinutes = (expiresAt: number): number => + Math.max(0, Math.round((expiresAt - Date.now()) / 60_000)); + +const runDaemon = async (options: RemoteOptions): Promise => { + const workerUrl = options.url || DEFAULT_REMOTE_URL; + const sessionInfo = await createRemoteSession(workerUrl); + console.log(JSON.stringify(sessionInfo)); +}; + +const runJson = async (options: RemoteOptions): Promise => { + const workerUrl = options.url || DEFAULT_REMOTE_URL; + const sessionInfo = await createRemoteSession(workerUrl); + console.log(JSON.stringify(sessionInfo)); + + const abortController = new AbortController(); + const shutdown = () => { + abortController.abort(); + process.exit(0); + }; + process.on("SIGINT", shutdown); + process.on("SIGTERM", shutdown); + + const stream = await openStream(sessionInfo.streamUrl, abortController.signal); + + for await (const sseEvent of parseSseStream(stream)) { + if (sseEvent.event === "log") { + console.log(sseEvent.data); + continue; + } + if (sseEvent.event === "expired") { + console.log(JSON.stringify({ event: "expired" })); + process.exit(0); + } + } +}; + +const runInteractive = async (options: RemoteOptions): Promise => { + const workerUrl = options.url || DEFAULT_REMOTE_URL; + + const createSpinner = spinner("Creating remote session...").start(); + let sessionInfo: RemoteSessionInfo; + try { + sessionInfo = await createRemoteSession(workerUrl); + } catch (error: unknown) { + createSpinner.fail(`Failed to create remote session: ${getErrorMessage(error)}`); + process.exit(1); + } + + const expirationMinutes = formatRemainingMinutes(sessionInfo.expiresAt); + createSpinner.succeed(`Remote session created (expires in ${expirationMinutes} min)`); + logger.dim(` Endpoint: ${sessionInfo.endpoint}`); + logger.dim(` Stream: ${sessionInfo.streamUrl}`); + logger.break(); + + const abortController = new AbortController(); + const shutdown = () => { + abortController.abort(); + logger.dim("Disconnected."); + process.exit(0); + }; + process.on("SIGINT", shutdown); + process.on("SIGTERM", shutdown); + + const stream = await openStream(sessionInfo.streamUrl, abortController.signal); + + for await (const sseEvent of parseSseStream(stream)) { + if (sseEvent.event === "connected") { + try { + const parsedPayload = JSON.parse(sseEvent.data) as { bufferedLogs?: number }; + const bufferedCount = parsedPayload.bufferedLogs ?? 0; + logger.dim( + bufferedCount === 0 + ? "Connected. Waiting for logs..." + : `Connected. Replaying ${bufferedCount} buffered ${bufferedCount === 1 ? "log" : "logs"}...`, + ); + } catch { + logger.dim("Connected."); + } + continue; + } + + if (sseEvent.event === "replay-complete") { + logger.dim("Replay complete. Streaming live logs."); + logger.break(); + continue; + } + + if (sseEvent.event === "log") { + printPrettyLog(sseEvent.data); + continue; + } + + if (sseEvent.event === "expired") { + logger.warn("Session expired after 1 hour. Create a new session to continue."); + process.exit(0); + } + } +}; + +interface PrettyLogPayload { + timestamp?: number; + location?: string; + message?: string; + hypothesisId?: string; + runId?: string; + data?: unknown; +} + +const printPrettyLog = (rawData: string): void => { + let payload: PrettyLogPayload; + try { + payload = JSON.parse(rawData) as PrettyLogPayload; + } catch { + logger.log(rawData); + return; + } + + const timeLabel = + typeof payload.timestamp === "number" + ? new Date(payload.timestamp).toISOString().slice(11, 23) + : new Date().toISOString().slice(11, 23); + + const locationLabel = payload.location + ? highlighter.bold(payload.location) + : highlighter.dim("(no location)"); + const messageLabel = payload.message ?? ""; + + const tags: string[] = []; + if (payload.hypothesisId) tags.push(highlighter.info(`H:${payload.hypothesisId}`)); + if (payload.runId) tags.push(highlighter.dim(`run:${payload.runId}`)); + const tagSuffix = tags.length > 0 ? ` ${tags.join(" ")}` : ""; + + logger.log(`${highlighter.dim(timeLabel)} ${locationLabel} ${messageLabel}${tagSuffix}`); + + if (payload.data !== undefined) { + try { + const dataString = JSON.stringify(payload.data, null, 2); + logger.dim( + dataString + .split("\n") + .map((innerLine) => ` ${innerLine}`) + .join("\n"), + ); + } catch { + logger.dim(` ${String(payload.data)}`); + } + } +}; + +export const remoteCommand = new Command("remote") + .description("Create a hosted remote logging session for production debugging") + .option("--url ", "override the worker URL", DEFAULT_REMOTE_URL) + .option("-d, --daemon", "create session, print JSON info, exit") + .option("--json", "create session and stream logs as NDJSON to stdout") + .action(async (options: RemoteOptions) => { + try { + if (options.daemon) { + await runDaemon(options); + return; + } + if (options.json) { + await runJson(options); + return; + } + await runInteractive(options); + } catch (error: unknown) { + logger.error(getErrorMessage(error)); + process.exit(1); + } + }); diff --git a/packages/debug-agent/src/constants.ts b/packages/debug-agent/src/constants.ts index de456d4..51d79b9 100644 --- a/packages/debug-agent/src/constants.ts +++ b/packages/debug-agent/src/constants.ts @@ -3,3 +3,4 @@ export const LOCK_PING_TIMEOUT_MS = 1000; export const LOG_DIRECTORY_NAME = "debug-agent"; export const MAX_DEDUP_ENTRIES = 10_000; export const VERSION_API_URL = "https://www.debug-agent.com/api/version"; +export const DEFAULT_REMOTE_URL = "https://debug-agent-remote.aidenbai.workers.dev"; diff --git a/packages/debug-agent/src/utils/parse-sse-stream.ts b/packages/debug-agent/src/utils/parse-sse-stream.ts new file mode 100644 index 0000000..908eda1 --- /dev/null +++ b/packages/debug-agent/src/utils/parse-sse-stream.ts @@ -0,0 +1,57 @@ +export interface ParsedSseEvent { + event: string; + data: string; +} + +export const parseSseStream = async function* ( + stream: ReadableStream, +): AsyncGenerator { + const reader = stream.getReader(); + const textDecoder = new TextDecoder(); + let buffer = ""; + let currentEvent = "message"; + let currentDataLines: string[] = []; + + const flushEvent = (): ParsedSseEvent | null => { + if (currentDataLines.length === 0) { + currentEvent = "message"; + return null; + } + const data = currentDataLines.join("\n"); + const result: ParsedSseEvent = { event: currentEvent, data }; + currentEvent = "message"; + currentDataLines = []; + return result; + }; + + try { + while (true) { + const { value: chunk, done } = await reader.read(); + if (done) break; + buffer += textDecoder.decode(chunk, { stream: true }); + + let newlineIndex = buffer.indexOf("\n"); + while (newlineIndex !== -1) { + let rawLine = buffer.slice(0, newlineIndex); + buffer = buffer.slice(newlineIndex + 1); + if (rawLine.endsWith("\r")) rawLine = rawLine.slice(0, -1); + + if (rawLine === "") { + const flushed = flushEvent(); + if (flushed) yield flushed; + } else if (rawLine.startsWith(":")) { + // ignore SSE comment lines + } else if (rawLine.startsWith("event:")) { + currentEvent = rawLine.slice(6).trim(); + } else if (rawLine.startsWith("data:")) { + const dataPiece = rawLine.startsWith("data: ") ? rawLine.slice(6) : rawLine.slice(5); + currentDataLines.push(dataPiece); + } + + newlineIndex = buffer.indexOf("\n"); + } + } + } finally { + reader.releaseLock(); + } +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5131181..8c4bf13 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -187,6 +187,22 @@ importers: specifier: ^5.7.0 version: 5.9.3 + packages/debug-agent-remote: + dependencies: + nanoid: + specifier: ^5.1.11 + version: 5.1.11 + devDependencies: + '@cloudflare/workers-types': + specifier: ^4.20260516.1 + version: 4.20260516.1 + typescript: + specifier: ^6.0.2 + version: 6.0.2 + wrangler: + specifier: ^4.92.0 + version: 4.92.0(@cloudflare/workers-types@4.20260516.1) + packages: '@alloc/quick-lru@5.2.0': @@ -420,6 +436,56 @@ packages: '@changesets/write@0.4.0': resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==} + '@cloudflare/kv-asset-handler@0.5.0': + resolution: {integrity: sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==} + engines: {node: '>=22.0.0'} + + '@cloudflare/unenv-preset@2.16.1': + resolution: {integrity: sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw==} + peerDependencies: + unenv: 2.0.0-rc.24 + workerd: '>1.20260305.0 <2.0.0-0' + peerDependenciesMeta: + workerd: + optional: true + + '@cloudflare/workerd-darwin-64@1.20260515.1': + resolution: {integrity: sha512-Wtw44el2pNbzixvTkWdfeBDTrQwQbJRz7/JUvPKV27I0pQWXbhNJPpM8cstq/pbrU5AGcA/HjFH6yPMRTIRKig==} + engines: {node: '>=16'} + cpu: [x64] + os: [darwin] + + '@cloudflare/workerd-darwin-arm64@1.20260515.1': + resolution: {integrity: sha512-X8EqkZej6FfmhF9AVAQ3FhyQRr9acS4RcDunMU2YiuxKHF1IU8zzH3vY30/POaG+rUu9vGDp/VgUl49VPenHJQ==} + engines: {node: '>=16'} + cpu: [arm64] + os: [darwin] + + '@cloudflare/workerd-linux-64@1.20260515.1': + resolution: {integrity: sha512-CDC89QxQ7Y7t7RG1Jd9vj/qolE1sQRkI2OSEuV5BMJi0vW/gV4OVG6xjpdK3b1OYnSWDzF7NpvlR5Yg86q7k4g==} + engines: {node: '>=16'} + cpu: [x64] + os: [linux] + + '@cloudflare/workerd-linux-arm64@1.20260515.1': + resolution: {integrity: sha512-WxbW/PToYES4fvHXzsr/5qOiETQs/Z9iZ0mjSZAiEwq5cMLZemzGN0COx+uFb9OvQwzh6Pg159qPFnw3+i9FuA==} + engines: {node: '>=16'} + cpu: [arm64] + os: [linux] + + '@cloudflare/workerd-windows-64@1.20260515.1': + resolution: {integrity: sha512-WmV/iv+MHjYsvkcMVzpM2B5/mf06UUkdpVhZrtMfV9graWjBGPYFvE/eab8748RPVGKh1Xe1vXofLzDSwc08lA==} + engines: {node: '>=16'} + cpu: [x64] + os: [win32] + + '@cloudflare/workers-types@4.20260516.1': + resolution: {integrity: sha512-aPckyZSPQXhOo+nSIvGLtXVl9M2qmf2GwFZYmvut9z47F1XTjHYcaYpI9/BvxrI+Q5l99UtXZU4bmQtX10JG+w==} + + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + '@dotenvx/dotenvx@1.60.1': resolution: {integrity: sha512-pPKqhE/HiaPDfbSf6doJnxeqzLszWP4eLICB89wRDZGaBaLzGpa3RgahVYIauBonaEWT8oxqAyacWKHtD+n3hQ==} hasBin: true @@ -445,156 +511,312 @@ packages: cpu: [ppc64] os: [aix] + '@esbuild/aix-ppc64@0.27.3': + resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/android-arm64@0.25.12': resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} engines: {node: '>=18'} cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.27.3': + resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm@0.25.12': resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} engines: {node: '>=18'} cpu: [arm] os: [android] + '@esbuild/android-arm@0.27.3': + resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-x64@0.25.12': resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} engines: {node: '>=18'} cpu: [x64] os: [android] + '@esbuild/android-x64@0.27.3': + resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/darwin-arm64@0.25.12': resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.27.3': + resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-x64@0.25.12': resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} engines: {node: '>=18'} cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.27.3': + resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/freebsd-arm64@0.25.12': resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.27.3': + resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-x64@0.25.12': resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.27.3': + resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/linux-arm64@0.25.12': resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} engines: {node: '>=18'} cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.27.3': + resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm@0.25.12': resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} engines: {node: '>=18'} cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.27.3': + resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-ia32@0.25.12': resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} engines: {node: '>=18'} cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.27.3': + resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-loong64@0.25.12': resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} engines: {node: '>=18'} cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.27.3': + resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-mips64el@0.25.12': resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.27.3': + resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-ppc64@0.25.12': resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.27.3': + resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-riscv64@0.25.12': resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.27.3': + resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-s390x@0.25.12': resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} engines: {node: '>=18'} cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.27.3': + resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-x64@0.25.12': resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} engines: {node: '>=18'} cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.27.3': + resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + '@esbuild/netbsd-arm64@0.25.12': resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] + '@esbuild/netbsd-arm64@0.27.3': + resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + '@esbuild/netbsd-x64@0.25.12': resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.27.3': + resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + '@esbuild/openbsd-arm64@0.25.12': resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-arm64@0.27.3': + resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-x64@0.25.12': resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.27.3': + resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + '@esbuild/openharmony-arm64@0.25.12': resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] + '@esbuild/openharmony-arm64@0.27.3': + resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + '@esbuild/sunos-x64@0.25.12': resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} engines: {node: '>=18'} cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.27.3': + resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/win32-arm64@0.25.12': resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} engines: {node: '>=18'} cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.27.3': + resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-ia32@0.25.12': resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} engines: {node: '>=18'} cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.27.3': + resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-x64@0.25.12': resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} engines: {node: '>=18'} cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.27.3': + resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@floating-ui/core@1.7.5': resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} @@ -832,6 +1054,9 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@libsql/darwin-arm64@0.5.29': resolution: {integrity: sha512-K+2RIB1OGFPYQbfay48GakLhqf3ArcbHqPFu7EZiaUcRgFcdw8RoltsMyvbj5ix2fY0HV3Q3Ioa/ByvQdaSM0A==} cpu: [arm64] @@ -1281,6 +1506,15 @@ packages: '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + '@poppinss/colors@4.1.6': + resolution: {integrity: sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==} + + '@poppinss/dumper@0.6.5': + resolution: {integrity: sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==} + + '@poppinss/exception@1.2.3': + resolution: {integrity: sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==} + '@rolldown/binding-android-arm64@1.0.0-rc.12': resolution: {integrity: sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1523,10 +1757,17 @@ packages: '@sec-ant/readable-stream@0.4.1': resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} + '@sindresorhus/is@7.2.0': + resolution: {integrity: sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==} + engines: {node: '>=18'} + '@sindresorhus/merge-streams@4.0.0': resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} engines: {node: '>=18'} + '@speed-highlight/core@1.2.15': + resolution: {integrity: sha512-BMq1K3DsElxDWawkX6eLg9+CKJrTVGCBAWVuHXVUV2u0s2711qiChLSId6ikYPfxhdYocLNt3wWwSvDiTvFabw==} + '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} @@ -1900,6 +2141,9 @@ packages: resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} engines: {node: '>=4'} + blake3-wasm@2.1.5: + resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} + body-parser@2.2.2: resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} engines: {node: '>=18'} @@ -2154,6 +2398,9 @@ packages: error-ex@1.3.4: resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + error-stack-parser-es@1.0.5: + resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} + es-define-property@1.0.1: resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} engines: {node: '>= 0.4'} @@ -2174,6 +2421,11 @@ packages: engines: {node: '>=18'} hasBin: true + esbuild@0.27.3: + resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -2727,6 +2979,11 @@ packages: resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} engines: {node: '>=18'} + miniflare@4.20260515.0: + resolution: {integrity: sha512-2j0oQWizk1Eu4Cm8tDX7Z+Nsjd0nebIj1TQcQ+Oy1QKeo0Ay9+bdn8wfLAtOj9znDCybDCUlnS1+nYvKXEdfNg==} + engines: {node: '>=22.0.0'} + hasBin: true + minimatch@10.2.5: resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} engines: {node: 18 || 20 || >=22} @@ -2784,6 +3041,11 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + nanoid@5.1.11: + resolution: {integrity: sha512-v+KEsUv2ps74PaSKv0gHTxTCgMXOIfBEbaqa6w6ISIGC7ZsvHN4N9oJ8d4cmf0n5oTzQz2SLmThbQWhjd/8eKg==} + engines: {node: ^18 || >=20} + hasBin: true + negotiator@1.0.0: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} @@ -2989,6 +3251,9 @@ packages: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -3306,6 +3571,10 @@ packages: babel-plugin-macros: optional: true + supports-color@10.2.2: + resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} + engines: {node: '>=18'} + tabbable@6.4.0: resolution: {integrity: sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==} @@ -3425,6 +3694,13 @@ packages: undici-types@7.18.2: resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + undici@7.24.8: + resolution: {integrity: sha512-6KQ/+QxK49Z/p3HO6E5ZCZWNnCasyZLa5ExaVYyvPxUwKtbCPMKELJOqh7EqOle0t9cH/7d2TaaTRRa6Nhs4YQ==} + engines: {node: '>=20.18.1'} + + unenv@2.0.0-rc.24: + resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==} + unicorn-magic@0.3.0: resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} engines: {node: '>=18'} @@ -3596,6 +3872,21 @@ packages: engines: {node: ^20.17.0 || >=22.9.0} hasBin: true + workerd@1.20260515.1: + resolution: {integrity: sha512-MjKOJLcvU45xXedQowvuiHtJTxu4WTHYQeIlF7YmjuqhiI6dImTFxWCEoRQHiskztxuVSNEmdO7/0UfDu6OMnQ==} + engines: {node: '>=16'} + hasBin: true + + wrangler@4.92.0: + resolution: {integrity: sha512-/DKpQHPxkuZbQsO9dFW2700VTD/4DSZMHjy92fO/frNoDRi/zQsFCAd2ONCV6TGqcUoXcP3D8Bo2gj/L4M0qQQ==} + engines: {node: '>=22.0.0'} + hasBin: true + peerDependencies: + '@cloudflare/workers-types': ^4.20260515.1 + peerDependenciesMeta: + '@cloudflare/workers-types': + optional: true + wrap-ansi@6.2.0: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} engines: {node: '>=8'} @@ -3607,6 +3898,18 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + ws@8.18.0: + resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + ws@8.20.0: resolution: {integrity: sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==} engines: {node: '>=10.0.0'} @@ -3655,6 +3958,12 @@ packages: resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} engines: {node: '>=18'} + youch-core@0.3.3: + resolution: {integrity: sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==} + + youch@4.1.0-beta.10: + resolution: {integrity: sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==} + zod-to-json-schema@3.25.2: resolution: {integrity: sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==} peerDependencies: @@ -4068,6 +4377,35 @@ snapshots: human-id: 4.1.3 prettier: 2.8.8 + '@cloudflare/kv-asset-handler@0.5.0': {} + + '@cloudflare/unenv-preset@2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260515.1)': + dependencies: + unenv: 2.0.0-rc.24 + optionalDependencies: + workerd: 1.20260515.1 + + '@cloudflare/workerd-darwin-64@1.20260515.1': + optional: true + + '@cloudflare/workerd-darwin-arm64@1.20260515.1': + optional: true + + '@cloudflare/workerd-linux-64@1.20260515.1': + optional: true + + '@cloudflare/workerd-linux-arm64@1.20260515.1': + optional: true + + '@cloudflare/workerd-windows-64@1.20260515.1': + optional: true + + '@cloudflare/workers-types@4.20260516.1': {} + + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + '@dotenvx/dotenvx@1.60.1': dependencies: commander: 11.1.0 @@ -4104,81 +4442,159 @@ snapshots: '@esbuild/aix-ppc64@0.25.12': optional: true + '@esbuild/aix-ppc64@0.27.3': + optional: true + '@esbuild/android-arm64@0.25.12': optional: true + '@esbuild/android-arm64@0.27.3': + optional: true + '@esbuild/android-arm@0.25.12': optional: true + '@esbuild/android-arm@0.27.3': + optional: true + '@esbuild/android-x64@0.25.12': optional: true + '@esbuild/android-x64@0.27.3': + optional: true + '@esbuild/darwin-arm64@0.25.12': optional: true + '@esbuild/darwin-arm64@0.27.3': + optional: true + '@esbuild/darwin-x64@0.25.12': optional: true + '@esbuild/darwin-x64@0.27.3': + optional: true + '@esbuild/freebsd-arm64@0.25.12': optional: true + '@esbuild/freebsd-arm64@0.27.3': + optional: true + '@esbuild/freebsd-x64@0.25.12': optional: true + '@esbuild/freebsd-x64@0.27.3': + optional: true + '@esbuild/linux-arm64@0.25.12': optional: true + '@esbuild/linux-arm64@0.27.3': + optional: true + '@esbuild/linux-arm@0.25.12': optional: true + '@esbuild/linux-arm@0.27.3': + optional: true + '@esbuild/linux-ia32@0.25.12': optional: true + '@esbuild/linux-ia32@0.27.3': + optional: true + '@esbuild/linux-loong64@0.25.12': optional: true + '@esbuild/linux-loong64@0.27.3': + optional: true + '@esbuild/linux-mips64el@0.25.12': optional: true + '@esbuild/linux-mips64el@0.27.3': + optional: true + '@esbuild/linux-ppc64@0.25.12': optional: true + '@esbuild/linux-ppc64@0.27.3': + optional: true + '@esbuild/linux-riscv64@0.25.12': optional: true + '@esbuild/linux-riscv64@0.27.3': + optional: true + '@esbuild/linux-s390x@0.25.12': optional: true + '@esbuild/linux-s390x@0.27.3': + optional: true + '@esbuild/linux-x64@0.25.12': optional: true + '@esbuild/linux-x64@0.27.3': + optional: true + '@esbuild/netbsd-arm64@0.25.12': optional: true + '@esbuild/netbsd-arm64@0.27.3': + optional: true + '@esbuild/netbsd-x64@0.25.12': optional: true + '@esbuild/netbsd-x64@0.27.3': + optional: true + '@esbuild/openbsd-arm64@0.25.12': optional: true + '@esbuild/openbsd-arm64@0.27.3': + optional: true + '@esbuild/openbsd-x64@0.25.12': optional: true + '@esbuild/openbsd-x64@0.27.3': + optional: true + '@esbuild/openharmony-arm64@0.25.12': optional: true + '@esbuild/openharmony-arm64@0.27.3': + optional: true + '@esbuild/sunos-x64@0.25.12': optional: true + '@esbuild/sunos-x64@0.27.3': + optional: true + '@esbuild/win32-arm64@0.25.12': optional: true + '@esbuild/win32-arm64@0.27.3': + optional: true + '@esbuild/win32-ia32@0.25.12': optional: true + '@esbuild/win32-ia32@0.27.3': + optional: true + '@esbuild/win32-x64@0.25.12': optional: true + '@esbuild/win32-x64@0.27.3': + optional: true + '@floating-ui/core@1.7.5': dependencies: '@floating-ui/utils': 0.2.11 @@ -4202,8 +4618,7 @@ snapshots: '@iarna/toml@2.2.5': {} - '@img/colour@1.1.0': - optional: true + '@img/colour@1.1.0': {} '@img/sharp-darwin-arm64@0.34.5': optionalDependencies: @@ -4353,6 +4768,11 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + '@libsql/darwin-arm64@0.5.29': optional: true @@ -4631,6 +5051,18 @@ snapshots: '@polka/url@1.0.0-next.29': {} + '@poppinss/colors@4.1.6': + dependencies: + kleur: 4.1.5 + + '@poppinss/dumper@0.6.5': + dependencies: + '@poppinss/colors': 4.1.6 + '@sindresorhus/is': 7.2.0 + supports-color: 10.2.2 + + '@poppinss/exception@1.2.3': {} + '@rolldown/binding-android-arm64@1.0.0-rc.12': optional: true @@ -4762,8 +5194,12 @@ snapshots: '@sec-ant/readable-stream@0.4.1': {} + '@sindresorhus/is@7.2.0': {} + '@sindresorhus/merge-streams@4.0.0': {} + '@speed-highlight/core@1.2.15': {} + '@standard-schema/spec@1.1.0': {} '@swc/helpers@0.5.15': @@ -5062,6 +5498,8 @@ snapshots: dependencies: is-windows: 1.0.2 + blake3-wasm@2.1.5: {} + body-parser@2.2.2: dependencies: bytes: 3.1.2 @@ -5266,6 +5704,8 @@ snapshots: dependencies: is-arrayish: 0.2.1 + error-stack-parser-es@1.0.5: {} + es-define-property@1.0.1: {} es-errors@1.3.0: {} @@ -5305,6 +5745,35 @@ snapshots: '@esbuild/win32-ia32': 0.25.12 '@esbuild/win32-x64': 0.25.12 + esbuild@0.27.3: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.3 + '@esbuild/android-arm': 0.27.3 + '@esbuild/android-arm64': 0.27.3 + '@esbuild/android-x64': 0.27.3 + '@esbuild/darwin-arm64': 0.27.3 + '@esbuild/darwin-x64': 0.27.3 + '@esbuild/freebsd-arm64': 0.27.3 + '@esbuild/freebsd-x64': 0.27.3 + '@esbuild/linux-arm': 0.27.3 + '@esbuild/linux-arm64': 0.27.3 + '@esbuild/linux-ia32': 0.27.3 + '@esbuild/linux-loong64': 0.27.3 + '@esbuild/linux-mips64el': 0.27.3 + '@esbuild/linux-ppc64': 0.27.3 + '@esbuild/linux-riscv64': 0.27.3 + '@esbuild/linux-s390x': 0.27.3 + '@esbuild/linux-x64': 0.27.3 + '@esbuild/netbsd-arm64': 0.27.3 + '@esbuild/netbsd-x64': 0.27.3 + '@esbuild/openbsd-arm64': 0.27.3 + '@esbuild/openbsd-x64': 0.27.3 + '@esbuild/openharmony-arm64': 0.27.3 + '@esbuild/sunos-x64': 0.27.3 + '@esbuild/win32-arm64': 0.27.3 + '@esbuild/win32-ia32': 0.27.3 + '@esbuild/win32-x64': 0.27.3 + escalade@3.2.0: {} escape-html@1.0.3: {} @@ -5800,6 +6269,18 @@ snapshots: mimic-function@5.0.1: {} + miniflare@4.20260515.0: + dependencies: + '@cspotcode/source-map-support': 0.8.1 + sharp: 0.34.5 + undici: 7.24.8 + workerd: 1.20260515.1 + ws: 8.18.0 + youch: 4.1.0-beta.10 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + minimatch@10.2.5: dependencies: brace-expansion: 5.0.5 @@ -5855,6 +6336,8 @@ snapshots: nanoid@3.3.11: {} + nanoid@5.1.11: {} + negotiator@1.0.0: {} next-themes@0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4): @@ -6079,6 +6562,8 @@ snapshots: path-type@4.0.0: {} + pathe@2.0.3: {} + picocolors@1.1.1: {} picomatch@2.3.2: {} @@ -6377,7 +6862,6 @@ snapshots: '@img/sharp-win32-arm64': 0.34.5 '@img/sharp-win32-ia32': 0.34.5 '@img/sharp-win32-x64': 0.34.5 - optional: true shebang-command@2.0.0: dependencies: @@ -6492,6 +6976,8 @@ snapshots: optionalDependencies: '@babel/core': 7.29.0 + supports-color@10.2.2: {} + tabbable@6.4.0: {} tagged-tag@1.0.0: {} @@ -6575,6 +7061,12 @@ snapshots: undici-types@7.18.2: {} + undici@7.24.8: {} + + unenv@2.0.0-rc.24: + dependencies: + pathe: 2.0.3 + unicorn-magic@0.3.0: {} universalify@0.1.2: {} @@ -6703,6 +7195,31 @@ snapshots: dependencies: isexe: 4.0.0 + workerd@1.20260515.1: + optionalDependencies: + '@cloudflare/workerd-darwin-64': 1.20260515.1 + '@cloudflare/workerd-darwin-arm64': 1.20260515.1 + '@cloudflare/workerd-linux-64': 1.20260515.1 + '@cloudflare/workerd-linux-arm64': 1.20260515.1 + '@cloudflare/workerd-windows-64': 1.20260515.1 + + wrangler@4.92.0(@cloudflare/workers-types@4.20260516.1): + dependencies: + '@cloudflare/kv-asset-handler': 0.5.0 + '@cloudflare/unenv-preset': 2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260515.1) + blake3-wasm: 2.1.5 + esbuild: 0.27.3 + miniflare: 4.20260515.0 + path-to-regexp: 6.3.0 + unenv: 2.0.0-rc.24 + workerd: 1.20260515.1 + optionalDependencies: + '@cloudflare/workers-types': 4.20260516.1 + fsevents: 2.3.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + wrap-ansi@6.2.0: dependencies: ansi-styles: 4.3.0 @@ -6717,6 +7234,8 @@ snapshots: wrappy@1.0.2: {} + ws@8.18.0: {} + ws@8.20.0: {} wsl-utils@0.3.1: @@ -6750,6 +7269,19 @@ snapshots: yoctocolors@2.1.2: {} + youch-core@0.3.3: + dependencies: + '@poppinss/exception': 1.2.3 + error-stack-parser-es: 1.0.5 + + youch@4.1.0-beta.10: + dependencies: + '@poppinss/colors': 4.1.6 + '@poppinss/dumper': 0.6.5 + '@speed-highlight/core': 1.2.15 + cookie: 1.1.1 + youch-core: 0.3.3 + zod-to-json-schema@3.25.2(zod@3.25.76): dependencies: zod: 3.25.76