diff --git a/AGENTS.md b/AGENTS.md index 76bd33cf..7c9bd598 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -71,3 +71,15 @@ A task is only "done" when **all** of these are true: task "seems small". - Hand-roll HTTP clients for SaaS APIs that already have a composite tool / MCP server available. + +## Project-specific deep dives + +When working inside one of these subdirectories, read the linked +handoff *before* writing code β€” it captures the architecture, prior +decisions, things Jaden has explicitly liked/disliked, and the known +open work for that project. + +- **`replicas-matrix-bridge/` + `replicas-telegram-bridge/`** β€” + Cloudflare Workers that bridge Beeper/Matrix and Telegram to the + Replicas agent runtime. Full handoff: [`docs/agent-handoff.md`](docs/agent-handoff.md). + Each subfolder also has its own `AGENTS.md` pointing here. diff --git a/docs/acp-telegram-status-research.md b/docs/acp-telegram-status-research.md new file mode 100644 index 00000000..b50f4321 --- /dev/null +++ b/docs/acp-telegram-status-research.md @@ -0,0 +1,266 @@ +# Research: ACP + Telegram Bot Status Surface Patterns + +**Date:** 2026-05-29 +**Why:** Jaden asked for a deep-research pass on how *open ACP / AI Telegram bots* surface live status. The bridges already work; this is to find any pattern we should borrow vs. confirm we're aligned with the field. + +## Update (2026-05-29, follow-up) + +Jaden pointed me at **[Open-ACP/OpenACP](https://github.com/Open-ACP/OpenACP)** β€” a self-hosted bridge that connects Claude Code / Codex / Gemini / Cursor / 28+ AI coding agents to Telegram, Discord, and Slack via ACP. Direct fellow-traveler in our exact space, built in TypeScript, 8MB repo. Their Telegram status design is fundamentally different from ours and worth understanding. + +**The big design split:** + +| Concern | Our bridges | OpenACP | +|---|---|---| +| Status surface | **One message, edited in place** through phases | **Many separate messages** β€” ThinkingIndicator, then a ToolCard per tool, then final text | +| Tool calls | Pushed as lines into the single status frame's `lines[]` | Each tool gets its own Telegram message ("ToolCard") that updates as the tool progresses | +| "Thinking" preview | Italic line under the phase header in the status frame | Separate "πŸ’­ Thinking..." message; dismissed when first tool runs | +| Text streaming cadence | `EDIT_MIN_INTERVAL_MS = 2000` (Matrix) / inheriting from Matrix on TG | `FLUSH_INTERVAL = 5000ms` debounced; only the FINAL text buffer is sent | +| Thinking ticker | `TICKER_REFRESH_MS = 4000` | `THINKING_REFRESH_MS = 15000` with a `THINKING_MAX_MS = 3 * 60 * 1000` auto-stop | +| Markdown | `markdownToTelegramHtml` only on the final reply; inline `` for tool args | Always HTML; the comment in their `renderer.ts:12-14` explicitly says HTML handles diffs + code fences "more reliably" than plain text | +| Cancellation / sealing | Phase flip to DONE/FAILED; rolling log preserved | `sealToolCardIfNeeded()` "preserves tool results as historical record" when text starts streaming, then opens a fresh card | +| 429 handling | Per-room `rateLimitedUntil`, terminal renders retry inline up to 4Γ— | Not visible in `streaming.ts` β€” they delegate to a `sendQueue` abstraction that "presumably manages rate-limiting transparently" | +| Topic isolation | None β€” DM or single room per replica | **Each session gets its own Telegram forum topic** (in groups with topics enabled) | + +**Concrete numbers worth borrowing:** + +- **5s debounce on text streaming** (vs. our ~2s edit floor). They're MUCH more conservative than us, which makes sense because they're sending separate cards not edits. +- **15s thinking-ticker** (vs. our 4s). For an agent that thinks for 30s+, refreshing every 4s is probably 4x too aggressive. +- **3-minute thinking-indicator hard cap** β€” auto-dismiss to avoid stale "thinking..." messages. We don't have this; our long-running turns just keep showing the same phase header. Worth adding a "Still working..." re-render at a longer cadence. + +**Concrete patterns worth adopting:** + +1. **The `sealToolCard` concept.** When text streaming starts after a tool, OpenACP "seals" the tool card β€” finalizes it, then opens a fresh state for whatever comes next. For us this maps to "don't keep mutating the tool line in `lines[]` once we've moved on; lock its rendered output." Subtle but real for any tool whose `tool_result` arrives late. +2. **Hard-cap on thinking duration.** Our agent can sit in `PLANNING Β· step 0 Β· 1:23` forever if the model is genuinely thinking; UX-wise this looks frozen. OpenACP auto-dismisses after 3 minutes. Ours could re-render the header to add `Still planning…` or similar at the 1-min mark. +3. **HTML-only by default** β€” they don't even try plain text. We agree on this; our `formatToolUseLine` already wraps everything in `` HTML tags. +4. **Forum topics per session** β€” Telegram-specific. Each session = its own thread inside the group. We don't do this. Worth considering for the Telegram bridge if users want multiple concurrent agents in one group. + +**Where we beat them:** + +- **Phase emoji on the header** (πŸ€”/πŸ“‹/✏️/πŸ”§/πŸ§ͺ/🚒/πŸŽ‰/❌) β€” they don't have this; their phases are implicit in the message-type split. +- **Reaction emojis on the user prompt** (πŸ‘€ β†’ πŸŽ‰/😭). Not visible in their code. Direct ack-on-receipt + ack-on-completion is a UX win they're missing. +- **`!model` per-room command.** Their model switching looks coarser (session-level config, not per-room). + +**Where they beat us:** + +- **Tool cards as scrollable history.** When a turn has 10 tools, our rolling log truncates the older ones to a `
`. They keep each as a permanent Telegram message β€” the user can scroll back to see "what exactly did Read on file X return?" days later. Lossless history. +- **Permission buttons inline.** They surface `πŸ” permission` requests with allow/deny buttons; the agent blocks until the user clicks. We can't do this because Replicas doesn't expose permission requests in `/history`. +- **Topics-per-session isolation.** + +**The 40-line PR I proposed in the prior section is still right** β€” bumping group cadence, adding a char-delta gate, adding self-tuning backoff are still valid wins regardless of whether we adopt OpenACP's many-messages model. The structural split (one edited message vs. many separate cards) is a deeper architectural choice that's mostly a matter of taste + which UX you optimize for. + +**Quick links:** +- Repo: [Open-ACP/OpenACP](https://github.com/Open-ACP/OpenACP) +- Telegram plugin: [`src/plugins/telegram/`](https://github.com/Open-ACP/OpenACP/tree/main/src/plugins/telegram) +- The three files that matter: `streaming.ts` (5s debounce + finalize), `renderer.ts` (HTML composition per message type), `activity.ts` (ThinkingIndicator + ToolCard + ActivityTracker β€” 545 lines, the core of their UX) + +--- + +## TL;DR + +- **ACP (Agent Client Protocol)** β€” Zed's open standard for editor↔agent streaming β€” uses a typed `SessionUpdate` discriminated union pushed via `session/update` notifications. Five status-relevant variants: `content`, `tool_call`, `plan`, `available_commands`, `current_mode`. Tool calls carry a four-state status (`pending|in_progress|completed|failed`). Plans replace-not-delta. Permission requests are blocking RPCs. Cancellation must still drain pending updates. +- **Telegram's per-chat rate cap is ~1 msg/sec** (community-derived; Telegram doesn't publish official numbers). **Per-group cap is 20/min.** Global ~30/sec. 429s come back with a `parameters.retry_after` field (seconds). The standard production reaction is to halt all sends for `retry_after + 0.1s`. +- **OSS streaming bots** (n3d1117/chatgpt-telegram-bot, AIORateLimiter in python-telegram-bot) use a **delta-or-completion edit trigger**: edit when character delta crosses a cutoff OR stream completes. They add **5-second exponential backoff per RetryAfter** and fall back to buffering on persistent errors. Markdown is only enabled on the FINAL edit β€” partial-markdown breakage on intermediate edits is a known foot-gun. +- **Our bridges are already aligned with the field** in shape; the gaps are: we don't yet expose a typed `tool_call.status` lifecycle, and our group cadence (`EDIT_MIN_INTERVAL_MS = 2000`) may still be aggressive vs. the 20-edit/min group cap (which is ~3s/edit, not 2s). + +--- + +## 1. ACP β€” Agent Client Protocol (Zed) + +[https://agentclientprotocol.com](https://agentclientprotocol.com) Β· [zed.dev/acp](https://zed.dev/acp) + +Open spec for streaming agent state from agents β†’ clients (editors / chat surfaces). Drives Zed's Agent Panel. + +### Streaming protocol + +- Client sends `session/prompt` request +- Agent streams **zero or more `SessionNotification` envelopes** via `session/update` +- Each envelope carries one `SessionUpdate` discriminated-union variant +- Agent can issue **blocking** `session/request_permission` calls in the middle +- Agent ends with a `PromptResponse` carrying a `stopReason` (`completed`, `cancelled`, etc.) +- **Cancellation isn't immediate** β€” clients MUST keep accepting `tool_call` updates after `session/cancel` because the agent may flush pending state before responding with the cancelled stop reason + +### The SessionUpdate variants + +| Variant | Purpose | Notes | +|---|---|---| +| `content` | Streamed text/image/audio chunk | Delivered **incrementally**; client accumulates | +| `tool_call` | Tool invocation lifecycle | Status: `pending β†’ in_progress β†’ completed/failed`. Carries `toolCallId`, `toolName`, `toolInput` | +| `plan` | Multi-task execution plan | **Replace, not delta** β€” agent sends the entire entry list every update; client replaces. Entries: `content`, `status (pending/in_progress/completed)`, `priority` | +| `available_commands` | What the agent can run | Used to show/hide slash commands | +| `current_mode` / `config_option` | Session config / mode flip | E.g. switching from "ask" to "edit" mode | + +### Key UX guarantees + +- **Stateful, not deltaed** for plans and tool calls β€” the latest envelope is the truth +- **Incremental** for content chunks β€” accumulate +- **Permission-gated for risky ops** β€” agent blocks until client approves +- **Non-blocking notifications** β€” agent doesn't wait for client ack on updates + +### What our bridges already mirror + +- We project a `lines[]` rolling log analogous to a stream of `content` + `tool_call` updates +- We have a `plan` PlanState parsed from agent text and rendered with `β–Ί/βœ“/β—¦` markers β€” same "replace, not delta" semantics +- We have phase transitions (`STARTING β†’ PLANNING β†’ ... β†’ DONE`) that map to the lifecycle ACP encodes per-tool + +### What we DON'T do that ACP does + +- **No typed tool_call status lifecycle.** We render every `tool_use` as a final-looking line; we don't show "pending" vs "in_progress" vs "completed" per call. Replicas' history doesn't give us this granularity either β€” by the time we see a `tool_use` in `/history`, the tool has effectively executed. +- **No permission-request surfacing.** Agents that need approval just emit a result line; no blocking UI moment. +- **No `current_mode` exposure.** We have `!model` but not `!mode`. + +**Verdict:** ACP's model is richer than ours by ~one notch on tool-call state. To match it we'd need Replicas to emit tool-call lifecycle events (start/end), which it doesn't currently. Not actionable until upstream changes. + +--- + +## 2. Telegram Bot API β€” Rate Limits + 429 Handling + +[core.telegram.org/bots/faq](https://core.telegram.org/bots/faq) Β· [gramio.dev/rate-limits](https://gramio.dev/rate-limits) + +Telegram **does not officially publish exact numbers.** Community-derived caps: + +| Scope | Cap | Source | +|---|---|---| +| Same chat (private) | ~1 msg/sec | community / GramIO | +| Same group | **20 msg/min** | Telegram FAQ β€” explicit | +| Broadcasting / global | ~30 msg/sec | Telegram FAQ β€” explicit | +| Paid broadcast (allow_paid_broadcast) | up to 1000/sec, 0.1 Stars each | Bot API 7.1+ | + +### 429 response shape + +```json +{ + "error_code": 429, + "description": "Too Many Requests: retry after 35", + "parameters": { "retry_after": 35 } +} +``` + +**Standard production response:** halt ALL sends for `retry_after + 0.1s` (per `python-telegram-bot`'s AIORateLimiter). Some libs (gramio's `withRetries`) auto-retry transparently. + +### Bot lib patterns we should know + +**python-telegram-bot's AIORateLimiter** β€” two-tier limiter: +- `group_limiter(group_id)` (20/min by default) wraps +- `overall_limiter` (30/sec by default) +- On `RetryAfter` it halts **everything**, not just the offending chat, for `retry_after + 0.1s` +- `max_retries` defaults to 0 β€” devs opt in + +**Our Matrix bridge already does the equivalent** β€” `rateLimitedUntil` in DO storage, non-terminal renders bail until it passes. We do NOT halt globally though; the rate-limited flag is per-replica. For matrix.org's per-room cap that's the right granularity. + +### Important nuance for our TG bridge + +Telegram caps groups at **20 edits/min = one edit every 3 seconds**. Our current TG bridge constants (mirrored from Matrix) are `EDIT_MIN_INTERVAL_MS = 2000ms` and `TICKER_REFRESH_MS = 4000ms`. Edit cadence under load: 1 every 2s, ticker forces 1 every 4s. That's **30/min** during active phases β€” over the 20/min group cap. + +**Recommended:** for Telegram bridge in groups, bump `EDIT_MIN_INTERVAL_MS` to ~3500ms when room > 2 members. We don't currently differentiate, and we probably should. + +--- + +## 3. OSS Streaming Telegram AI Bots β€” Patterns + +### `n3d1117/chatgpt-telegram-bot` (3.4k stars, the canonical Python ChatGPT-on-Telegram clone) + +[GitHub](https://github.com/n3d1117/chatgpt-telegram-bot) β€” `bot/telegram_bot.py` streaming loop: + +```python +backoff = 0 +async for content, tokens in stream_response: + # Throttle: edit only on big-enough delta OR stream done + cutoff = get_stream_cutoff_values(update, content) + backoff + if abs(len(content) - len(prev)) > cutoff or tokens != 'not_finished': + try: + use_markdown = tokens != 'not_finished' # only on FINAL edit + await edit_message_with_retry(..., use_markdown) + prev = content + except RetryAfter as e: + backoff += 5 + await asyncio.sleep(e.retry_after) + continue + except TimedOut: + backoff += 5 + await asyncio.sleep(0.5) + continue + await asyncio.sleep(0.01) # ~10ms base loop pacing +``` + +**Key takeaways:** + +1. **Delta-based, not time-based** edit trigger β€” `cutoff` is a character-count threshold per chat type. They don't edit every N ms; they edit every N chars. +2. **Dynamic backoff: +5s per error.** Each 429 / TimedOut bumps the cutoff so edits get rarer the more pressured the channel is. Self-tuning. +3. **Markdown only on the final edit.** Partial-markdown during streaming breaks Telegram parser (mid-`**bold` etc.). They send plaintext during the stream, switch to Markdown V2 only at completion. +4. **No global halt on 429** β€” only the affected chat backs off. (Different from python-telegram-bot's AIORateLimiter.) + +### Markdown safety during stream + +[community.latenode.com discussion](https://community.latenode.com/t/how-to-handle-streaming-responses-from-google-ai-in-telegram-bot-without-markdown-parsing-errors/21646) β€” building with aiogram + Gemini. The community consensus: never send `parse_mode=MarkdownV2` while streaming. Either: +- Strip markdown on intermediate edits, render on final, OR +- Use Telegram's safer `HTML` parse mode but balance tags carefully on each edit + +**Our Telegram bridge** uses `parse_mode=HTML` with `markdownToTelegramHtml` ([`replicas-telegram-bridge/src/markdown.ts`](/replicas-telegram-bridge/src/markdown.ts)) and renders only the final reply. The status frame itself uses bot-controlled HTML (the phase header etc.), which is balanced by construction. **We're already on the safer path.** + +--- + +## 4. Zed Agent Panel β€” UI design takeaways + +[zed.dev/docs/ai/agent-panel](https://zed.dev/docs/ai/agent-panel) + +What Zed's panel shows during a live agent turn: + +- **Tool indicators** alongside the streaming message β€” "which tools the model is using" +- **An accordion bar above the editor** with "which files, how many, and how many lines have been edited" (expand for the file list). This is the closest analogue to our focus-window-that-got-reverted, but Zed implements it as a *summary widget above* the message, not as a collapsing of the rolling log itself +- **Confirmation menus** appear inline when the agent needs permission β€” "allow once / always / deny" +- **Linear chronological thread** for older content β€” no "compress old steps into a count" UX +- **Restore Checkpoint** buttons after file-edit batches β€” lets the user roll back the agent's changes + +What Zed **doesn't** do that I once thought it did: +- No checklist/spine-of-the-plan UI rendered as the message body +- No "current step highlighted" focus window inside the conversation + +This actually matches the lesson learned on our side: **the tool-call stream IS the body. Don't replace it with a summary**. The summary belongs in a sidebar widget *next to* the stream, not *instead of* it. + +--- + +## 5. Synthesis β€” what's worth changing in our bridges + +### Already aligned with the field βœ“ + +- Edit-in-place single status message (vs. spamming new messages) +- Phase header + emoji + elapsed time +- Plan checklist with current-item highlight (`β–Ί` matches ACP's `in_progress`) +- Rate-limit-aware backoff with `retry_after` honored +- Markdown rendered only on the final reply, not during stream +- Reaction emoji on the user's prompt as an at-a-glance ack signal (πŸ‘€ β†’ πŸŽ‰/😭) + +### Worth tuning + +1. **TG group cadence is over the 20/min cap.** Bump `EDIT_MIN_INTERVAL_MS` to ~3500ms when room is a group. Low risk, ~10 line change. +2. **Adopt the n3d1117 delta-trigger pattern** as an additional edit gate. Right now we edit on every diff (every ~180ms alarm tick that has new events). If the diff is just a single character of a thinking preview, that wastes an edit. Edit only when the rendered string changes by >N chars, OR phase advances, OR turn ends. Saves edits β†’ fewer 429s. +3. **`+5s per 429` self-tuning backoff** β€” when we hit a 429 we currently respect `retry_after_ms` but don't make subsequent edits slower. n3d1117's pattern of progressively raising the cutoff means a bot in a noisy group naturally settles into a sustainable rate. Worth borrowing. + +### Out of scope / not actionable + +1. **Typed tool_call status lifecycle** β€” requires Replicas to emit start/end events per tool_use, which it doesn't. +2. **Permission-request blocking UX** β€” would need the Replicas agent to surface "I'd like to run X, OK?" prompts. Not in `/history` today. +3. **`current_mode` / `available_commands`** notifications β€” Replicas-side feature, not bridge. + +### Recommended single PR + +Wrap the three "worth tuning" items into one PR for the Telegram bridge specifically: + +- `EDIT_MIN_INTERVAL_MS_GROUP = 3500` constant; use it instead of the 2000 when chat is a group +- Add a `lastRenderedLen` storage key; skip edit when `|new_len βˆ’ last_len| < CHAR_DELTA_CUTOFF` (default 60 chars) AND no phase change AND not terminal +- On 429, `backoff += 1500ms` (added to the next edit's gate) until the alarm chain idles for >30s + +Estimated: ~40-line change, all in `replicas-telegram-bridge/src/poller.ts`. Tests: extend `index.test.ts` with 429 backoff + group-cadence cases. + +## Sources + +- [Agent Client Protocol β€” Introduction](https://agentclientprotocol.com/get-started/introduction) +- [Agent Client Protocol β€” Schema](https://agentclientprotocol.com/protocol/schema) +- [Zed ACP page](https://zed.dev/acp) +- [Zed Agent Panel docs](https://zed.dev/docs/ai/agent-panel) +- [Telegram Bots FAQ](https://core.telegram.org/bots/faq) +- [Telegram Bot API reference](https://core.telegram.org/bots/api) +- [GramIO rate-limit guide](https://gramio.dev/rate-limits) +- [python-telegram-bot AIORateLimiter](https://docs.python-telegram-bot.org/en/v22.0/telegram.ext.aioratelimiter.html) +- [n3d1117/chatgpt-telegram-bot](https://github.com/n3d1117/chatgpt-telegram-bot) +- [community.latenode.com β€” streaming + markdown errors](https://community.latenode.com/t/how-to-handle-streaming-responses-from-google-ai-in-telegram-bot-without-markdown-parsing-errors/21646) diff --git a/docs/agent-handoff.md b/docs/agent-handoff.md new file mode 100644 index 00000000..52a4c88e --- /dev/null +++ b/docs/agent-handoff.md @@ -0,0 +1,279 @@ +# Replicas Bridges β€” Agent Handoff (2026-05-29) + +> You're the next agent on this work. Read this top-to-bottom before touching code. +> Jaden has limited patience for re-explanations; this doc exists so you don't waste it. + +--- + +## 1. What this is + +Two Cloudflare Workers that bridge between chat platforms and the **Replicas** agent runtime so a user can `@Jada` (the bot persona) in Beeper / Matrix or Telegram and have a Replicas workspace spawn, run their prompt, and surface tool calls + final reply back into the chat in real time. + +| Bridge | Repo path | Worker name | Latest deploy | +|---|---|---|---| +| **Matrix** (Beeper, matrix.org) | `replicas-matrix-bridge/` | `replicas-matrix-bridge` | `941d8c95` (commit `e5f0ae3`) | +| **Telegram** | `replicas-telegram-bridge/` | `replicas-telegram-bridge` | `e050ce54` (commit `8728c39`) | + +Both live in repo **`itsablabla/garza-os-github`**, branch **`feat/replicas-telegram-bridge`** (yes, the matrix work happens on the telegram-named branch β€” historical). + +All commits through `e5f0ae3` are pushed. + +## 2. Who you're working for + +**Jaden Garza.** Owner of Garza OS / Replicas. He is: + +- Fast-moving. Will tell you "you did a bad job" if a change makes the chat UX worse. +- Specific about likes: **real-time animation of tool calls, line by line**. The streaming feel IS the product. He explicitly called out the πŸ‘€ β†’ πŸŽ‰ prompt-reaction emojis as core to the experience. +- Allergic to: invisible work, missing outcomes (Done frame without a body), multi-message fragmentation, fancy UI rewrites that hide tool calls behind expandables. + +**What I got wrong in this session** (so you don't repeat): + +1. **Focus-window UI rewrite (commit `8728c39`)** β€” I built a three-tier `CURRENT / RECENT / OLDER` window that collapsed tool calls into compressed lines and an expandable. Jaden hated it: "Most of the changes you made were pretty terrible." Reverted in `dd74c60`. The tool-call stream IS the showpiece β€” do not hide it. +2. **Took too long to notice the reaction emojis were silently 429ing** under rate limit. They are user-facing status signals β€” treat them as load-bearing. +3. **Telegram bridge still has the focus-window code** as of commit `8728c39`. Matrix is reverted; TG isn't, because Jaden was actively testing Matrix only. **Open item:** revert TG's `render.ts` to match Matrix when you get to it. + +## 3. End-to-end flow (Matrix bridge) + +``` +User sends "@Jada do X" in a Beeper room + ↓ +MatrixListener DO (global, alarmed every 1s) + β†’ polls /sync + β†’ decrypts m.room.encrypted via OlmVault (Megolm v1) + β†’ for each m.room.message in joined rooms (filtered by self-tx + shouldHandleMessage) + β†’ calls dispatch.handleMatrixMessage(roomId, eventId, body) + ↓ +dispatch.handleMatrixMessage (in-process, src/dispatch.ts) + β†’ KV dedupe by eventId (60s TTL) + β†’ fires πŸ‘€ reaction on user's prompt (now with retry-on-429) + β†’ fires "πŸ€” Starting Β· 0s" initial frame in parallel with Replicas spawn + β†’ if existing replica for room β†’ sendFollowUp + startWatcher (parallel) + β†’ else β†’ createReplica + startWatcher + β†’ /ack endpoint forwards ackReactionId + initialStatusEventId to the watcher + ↓ +ReplicaPoller DO (one per replicaId, alarmed every 180ms while active) + β†’ fetch /v1/replica/{id}/history + β†’ diff against lastSeenCount, project new events: + claude-assistant.thinking β†’ currentAction (italic preview) + claude-assistant.tool_use β†’ push "πŸ”§ cmd" to lines, bump phase + claude-assistant.text β†’ parsePlan OR push "πŸ’¬ narration" + claude-user.tool_result β†’ push "↳ output" or "βœ— error" + claude-system β†’ systemInfo (model + MCP count + tool count) + context-usage β†’ contextUsage (ctx %) + claude-result β†’ setTerminal(Done, resultHtml=markdownToHtml(result)) + β†’ renderAndSend on each tick: edits the SAME Matrix event (statusEventId) + β†’ on 429: terminal renders retry inline, non-terminal renders defer + β†’ after Done: pendingCleanup=true, 30s alarm wipes state +``` + +## 4. Critical files (Matrix bridge) + +All paths relative to `replicas-matrix-bridge/src/`. + +### Hot path β€” touch these most + +- **`poller.ts` (764 lines)** β€” ReplicaPoller DO. The state machine. + - L74-85: timing constants. **Don't tighten these** β€” they were loosened (1000β†’2000ms edit interval, 3000β†’4000ms ticker) because matrix.org rate-limits at ~30 events/min/room. + - L96-132: `fetch()` β€” routes `/watch`, `/cancel`, `/ack`, `/debug`. + - L134-228: `handleWatch` β€” steering path (existing turn, mid-flight follow-up) vs fresh-spawn path. Steering gate now includes `priorPhase !== DONE && priorPhase !== FAILED` (commit `3dc3f33`) to close the race window where a follow-up landed between `setTerminal` and `pendingCleanup`. + - L260-518: `alarmInner` β€” the diff loop. Reads /history, projects events into `lines[]`, `plan`, `systemInfo`, `resultMeta`. Handles `sawResult` β†’ `setTerminal` β†’ mark cleanup. **Note:** the trailing `πŸ’¬ narration` line is popped on `sawResult` (commit `9394264`) so the embedded `resultHtml` in Done doesn't duplicate the body. + - L570-655: `renderAndSend` β€” the edit-or-send code. **Terminal renders retry on 429 inline (4 attempts honoring `retry_after_ms`)** because the alternative is the 30s cleanup wiping state before the Done frame lands. Non-terminal renders defer via `rateLimitedUntil` in DO storage. + - L657-676: `setTerminal` β€” accepts `resultHtml`. When set, it gets embedded inline in the Done frame. + - L678-722: `swapReaction` β€” redacts prior reaction + places new one. Both halves retry up to 4 times on 429 (commit `dd74c60`). + +- **`render.ts` (444 lines)** β€” pure rendering. Platform-independent (Matrix HTML). + - `renderActive` / `renderTerminal` β€” the simple `recent[-4]` + older-blockquote-expandable model. **Do not rebuild the focus window here.** + - `renderPlan` β€” first not-done item gets `β–Ί` marker. + - `formatCost` β€” `$0.08` not `$0.0781`; sub-cent β†’ `<$0.01`; drops decimals past $10. + - `StatusState.terminal.resultHtml` β€” when set, `renderTerminal` embeds it below the op log. Single message per turn (avoids the Beeper-drops-second-message bug). + +- **`matrix.ts` (334 lines)** β€” Client-Server API helpers. + - `MatrixError` carries `retryAfterMs` parsed from 429 bodies (commit `d79c798`). + - `sendMessage` accepts an optional `txnId` for idempotent retries (commit `82d3256`). + - `editMessage`, `react`, `redact`, `pin`, `unpin`, `sync`, `joinRoom`, `typing`. + +- **`dispatch.ts` (234 lines)** β€” in-process turn-spawn or follow-up. + - `reactWithRetry` helper (commit `dd74c60`) for the initial πŸ‘€ ack so it survives 429. + - `prefixWithRoutingHeader` β€” wraps user text with `[matrix:room=...:event=...]` + hint comment so the Replicas agent knows how to format its reply. + - Reads per-room model override from KV (`!model` command β€” commit `2837f9b`). + +- **`listener.ts` (347 lines)** β€” MatrixListener DO. Single global instance. + - 1s alarm calls `/sync` with a 28s long-poll. + - Filters out the bot's own sends via `ev.unsigned.transaction_id` (works even when the bot account is the same Matrix user β€” "self-bot mode", see memory). + - Decrypts incoming `m.room.encrypted` via Megolm (calls `tryDecrypt`), looking up session keys from static import + OlmVault. + - `shouldHandleMessage` β€” 2-person room β†’ always dispatch; larger room β†’ require `@mention`. + - `!cancel` and `!model` commands handled inline (no replica spawn). + +### Encryption surface (only touch if you must) + +- **`megolm.ts`** β€” pure-JS Megolm v1 decryption (AES-CBC + HMAC). No WASM needed. +- **`olm.wasm` + `olm-init.ts`** β€” @matrix-org/olm WASM bootstrap for CF Workers via `OLM_OPTIONS.instantiateWasm` shim. +- **`olm-vault.ts` (455 lines)** β€” OlmVault DO. Holds the bot's Olm Account pickle, identity keys, OTK pool, and the live keystore of Megolm sessions captured via `/sendToDevice` `m.room_key` events. +- **`ssss.ts`** β€” Recovery key decode + SSSS (Secret Storage v1) decryption for cross-signing keys. +- **`megolm-keys.ts`** β€” Parses Element key export JSON. + +**Memory note**: the bot has a dedicated device on matrix.org (`device_id = Ww3fWv0z7s`). Don't reuse Jaden's Element session token β€” `/keys/upload` overwrites. (See `~/.claude/projects/-home-user-workspaces/memory/feedback_matrix_dedicated_device.md` if it exists.) + +### Telegram bridge + +`replicas-telegram-bridge/src/` mirrors matrix structure minus the encryption: +- `index.ts` β€” webhook handler + `/start`/`/cancel`/`/model` commands +- `poller.ts` β€” same ReplicaPoller pattern, slightly different message-cap handling (TG's 4096-char `editMessageText` limit) +- `render.ts` β€” same render model. **Has stale focus-window code** as of commit `8728c39`. Revert when convenient (mirror Matrix's `render.ts`). +- `markdown.ts` β€” markdown β†’ Telegram HTML + +The Telegram bridge **keeps the separate final-reply send** (no embed-in-Done-frame) because TG's per-message char cap is too tight. + +## 5. The state model (Durable Object storage keys) + +Per-replica `ReplicaPoller` instance stores: + +| Key | Type | Purpose | +|---|---|---| +| `watch` | WatchSpec | replicaId, roomId, startEventId, userText, ackReactionId, initialStatusEventId | +| `lastSeenCount` | number | events.length watermark for diff | +| `lines` | string[] | the rolling tool-call/narration log; this is what renders | +| `phase` | Phase | STARTING/PLANNING/EDITING/RUNNING/TESTING/SHIPPING/DONE/FAILED | +| `stepCount` | number | bumped per tool_use | +| `currentAction` | string | latest thinking/narration preview (italic) | +| `plan` | PlanState | parsed from "Plan (N/M)" headed text blocks | +| `systemInfo` | SystemInfo | model + mcpCount + mcpActive + toolCount | +| `contextUsage` | ContextUsage | totalTokens, maxTokens, pct | +| `resultMeta` | ResultMeta | costUsd, inputTokens, outputTokens | +| `statusEventId` | string | the Matrix event we keep editing | +| `reactionEventId` | string | for swapping πŸ‘€ β†’ πŸŽ‰ | +| `lastRendered` | string | dedup check (skip re-edit if text unchanged) | +| `lastEditAt` | number | enforces EDIT_MIN_INTERVAL_MS | +| `lastTypingAt` | number | enforces TYPING_INTERVAL_MS | +| `pinned` | boolean | pinned the status frame on first send | +| `pendingCleanup` | boolean | terminal reached; alarm in 30s will wipe everything | +| `rateLimitedUntil` | number | timestamp; non-terminal renders skip until this passes | +| `startedAt` | number | turn start time for elapsed display | + +## 6. Recent commits β€” what + why + +Newest first. Use these when triaging "why does this exist". + +| Hash | Title | Why | +|---|---|---| +| `e5f0ae3` | terminal renders retry on 429 | Done frame was silently dying when the edit 429'd because the 30s cleanup wiped state | +| `dd74c60` | revert focus window; harden reactions | Jaden hated the UI rewrite; reactions were silently 429ing | +| `d79c798` | stop fragmenting status frames under rate limit | matrix.org 429s + fall-through to `sendMessage` created N bubbles per turn | +| `0a552a9` | embed result body in Done frame | Beeper was dropping the separate final-reply message; embedded body = single message per turn | +| `8728c39` | **(REVERTED on Matrix, stale on TG)** focus-window UI | over-engineered; Jaden wants the line-by-line stream | +| `3dc3f33` | close setTerminalβ†’pendingCleanup race | `πŸŽ‰` was landing prematurely on the next prompt during a ~1s window | +| `9394264` | kill Done-frame edit loop + cross-tick narration dup | trailing `πŸ’¬` survived past terminal; drain-pending fell through to ticker | +| `82d3256` | kill duplicated final-reply renders | retry loop without stable txn id was double-sending | +| `fcf6da1` | Telegram parity with Matrix tier 1+2 perf | TTFB cut on TG side | +| `e9722e7` | surface every Replicas event + restyle | claude-system, claude-user tool_result, claude-result, context-usage all projected | + +## 7. Production endpoints + +| URL | What | +|---|---| +| `https://replicas-matrix-bridge.jadengarza.workers.dev/health` | quick liveness ping | +| `https://replicas-matrix-bridge.jadengarza.workers.dev/debug/listener` | inspect MatrixListener since-token + alarm | +| `https://replicas-matrix-bridge.jadengarza.workers.dev/debug/watcher/{replicaId}` | inspect a ReplicaPoller DO's full state + next alarm | +| `https://replicas-matrix-bridge.jadengarza.workers.dev/debug/vault/identity` | OlmVault curve25519 + ed25519 public keys | +| `https://replicas-matrix-bridge.jadengarza.workers.dev/debug/vault/keystore` | live captured Megolm session keys | +| `https://replicas-telegram-bridge.jadengarza.workers.dev/` | TG webhook root | + +Admin endpoints (POST): `/admin/listener/reset`, `/admin/vault/{reset,cross-sign,bootstrap,upload-device,upload-otks}`. + +## 8. Replicas API surface used + +Read `~/.replicas/REPLICAS_API_CONTROL_PLANE.md` first. Key endpoints: + +- `POST /v1/replica` β€” spawn a new replica. We use `environment_id`, `coding_agent="claude"`, `model` (per-room override), `lifecycle_policy="delete_after_inactivity"`, `auto_stop_minutes=60`, `metadata.matrix_room_id`/`matrix_event_id`. +- `POST /v1/replica/{id}/messages` β€” send a follow-up. 404/410 means the replica expired; we cancel-and-respawn. +- `GET /v1/replica/{id}/history?include=content&verbose=1` β€” full event stream, polled every 180ms. **The API does NOT support `since`/`offset`/`limit` cursors** β€” confirmed via probe (Tier 4 perf was a no-op). +- `DELETE /v1/replica/{id}` β€” used by `/cancel` handler. + +Auth: `Authorization: Bearer ${REPLICAS_API_KEY}` + `Replicas-Org-Id: 778b1aa3-4327-45a4-9874-c8a3a72df610`. + +## 9. Operational essentials + +### Deploy + +```bash +# Matrix +cd replicas-matrix-bridge && CLOUDFLARE_ACCOUNT_ID=... CLOUDFLARE_API_TOKEN=... npx wrangler deploy + +# Telegram +cd replicas-telegram-bridge && CLOUDFLARE_ACCOUNT_ID=... CLOUDFLARE_API_TOKEN=... npx wrangler deploy +``` + +CF creds live in the Garza OS Global env (`CLOUDFLARE_ACCOUNT_ID`, `CLOUDFLARE_API_TOKEN`) β€” pull from the Replicas env vars endpoint if not in shell. + +### Tail logs + +```bash +CLOUDFLARE_ACCOUNT_ID=... CLOUDFLARE_API_TOKEN=... \ + npx wrangler tail replicas-matrix-bridge --format pretty +``` + +Filter for the symptom you're hunting β€” `editMessage`, `react`, `429`, `editMessage 429`, `pendingFinalReply` (legacy), etc. + +### Inspect a stuck watcher + +`GET /debug/watcher/{replicaId}` returns `{state: {...}, alarmAt}`. Empty `state` = the DO has been wiped (cleanup ran or never started). `alarmAt = null` post-cleanup is normal. + +### Typecheck + tests + +```bash +cd replicas-matrix-bridge && bun run typecheck && bun test src/render.test.ts +cd replicas-telegram-bridge && bun run typecheck && bun test src/render.test.ts +``` + +`bun test` will fail on some old `matrix.test.ts` tests because of a `vi.stubGlobal is not a function` mismatch β€” those are pre-existing vitest-under-bun issues, **not your changes**. Ignore unless they're in `render.test.ts`. + +## 10. Known issues / open work + +1. **Telegram bridge still has focus-window code** β€” commit `8728c39` ships with `parseOps`/`renderOpsFocused`/`compressToolLine` in `replicas-telegram-bridge/src/render.ts`. Matrix was reverted in `dd74c60` but Telegram wasn't because Jaden was testing Matrix. To revert: copy `replicas-matrix-bridge/src/render.ts` and `render.test.ts` onto the TG side (they were already file-identical before the focus-window divergence, EXCEPT for the `MAX_RENDER_CHARS` cap β€” TG should stay at 3800 because of the 4096-char `editMessageText` limit). Telegram poller doesn't use `resultHtml` embedding either β€” keep TG's separate-message flow. + +2. **The DeepSeek V3.1 empty-turn bug on OpenClaw CT 232** β€” unrelated to bridges; Jaden's note. ~1 in 3 turns return `stopReason=stop payloads=0` on the Jada persona with longer history. Options offered: cap `dmHistoryLimit` on main agent (currently only dev2 has it at 8), or switch to Hermes 4 405B. Not acted on. See memory file `project_openclaw_ct232.md` if it exists. + +3. **Beeper-side rendering of Matrix edits** β€” Beeper sometimes shows each edit as a separate visible bubble in its UI (visible in screenshots). My read: this is a Beeper rendering decision, not a Matrix wire issue (the latest edit IS the canonical state). The fix on our side was minimizing the number of edits (slower cadence + reaction retries). If Jaden is still seeing fragmentation post-`e5f0ae3`, the next move is probably to verify edits are landing as real edits (not new messages from the 429 fall-through, which the latest code prevents). + +4. **Rate-limit budget is per-room** β€” multiple concurrent group rooms eat the budget. If Jaden's bot gets popular, may need exponential backoff sharing across rooms via a global counter in OlmVault DO or a dedicated rate-limit DO. + +5. **No fallback if Done edit ultimately fails after 4 retries** β€” currently it logs and gives up. If matrix.org is rate-limited for 30+ seconds, the cleanup wipe loses the embedded answer body. Possible fix: send a separate fresh message with just the result body as last resort, accepting fragmentation only at the worst case. + +## 11. The Slack thread + +Jaden runs this work from Slack DM `D0B6JL0UTT8`, thread `1780030352.364909` β€” that's where status updates land. Slack creds in shell env (`SLACK_BOT_TOKEN`, `SLACK_CHANNEL_ID`, `SLACK_THREAD_TS`). When you ship anything user-visible, post a short summary there. + +## 12. Things Jaden specifically likes (don't break) + +- **Live edits** β€” the bot's frame updates every couple seconds during a turn. Slowing the cadence is fine; turning it into "send a new message per phase change" is not. +- **Phase emoji on the user's prompt** β€” πŸ‘€ on receive, πŸŽ‰ / 😭 at completion. These are the AT-A-GLANCE status signals. They have retry-on-429 now. **Audit them if anything related changes.** +- **Sonnet 4.6 default + per-room `!model` command** β€” switching to sonnet/opus/haiku alias or full id (commit `2837f9b`). +- **Auto-respond in 2-person rooms** without requiring mentions (commit `2837f9b`). +- **Markdown rendering of the agent's final reply** β€” bold, inline code, lists, headers. Done by `markdownToHtml` and now embedded in the Done frame for Matrix. + +## 13. Things Jaden has explicitly disliked (don't reintroduce) + +- The focus-window collapse of tool calls. Tool calls are the showpiece. +- Stacked reaction emojis (the πŸ‘€ and πŸŽ‰ both staying β€” must redact prior before placing next). +- Two messages per turn (Done frame + separate reply) in Matrix when one would do. +- Empty Done frames where the answer never lands. +- Big rewrites without checking in first. **Talk before you ship anything beyond a clear bug fix.** + +## 14. Where to find more + +- `~/.replicas/REPLICAS_API_CONTROL_PLANE.md` β€” Replicas REST surface +- `~/.replicas/plans/matrix-bridge-plan.md` β€” original architecture sketch +- `~/.replicas/plans/telegram-status-ux-research.md` β€” UX research for status frames +- `~/.replicas/plans/factory-droid-status-research.md` β€” research for design patterns +- `~/.replicas/plans/2026-05-29_bridge-ui-cleanup-proposal.md` β€” the focus-window proposal (now superseded; kept as historical reference for what NOT to do) +- `~/.claude/projects/-home-user-workspaces/memory/MEMORY.md` β€” agent memory index, contains feedback notes and project state + +## 15. First moves I'd recommend + +1. **Read the latest commits** (`git log --oneline -20`) and skim `replicas-matrix-bridge/src/poller.ts` end-to-end (it's 764 lines but well-structured). +2. **Verify the deployed version** matches HEAD: `npx wrangler deployments list | head` should show `941d8c95-5b5f-4dea-aed5-4acc8b44d80d` as current for matrix. +3. **Send a test prompt** in Beeper to confirm end-to-end works (look for πŸ‘€ β†’ in-place "Starting β†’ Planning β†’ Running β†’ Done" updates β†’ πŸŽ‰ with embedded body). +4. **If Jaden flags more wonkiness in group chats**, tail logs first (`wrangler tail replicas-matrix-bridge`) and look for `429` / `editMessage 429` patterns before assuming a code bug. +5. **Then go back to the Telegram revert** (open issue #1 above) β€” should be quick: copy `render.ts` from matrix, adjust `MAX_RENDER_CHARS`, leave poller untouched. + +Good luck. Don't rebuild the focus window. diff --git a/replicas-matrix-bridge/.gitignore b/replicas-matrix-bridge/.gitignore new file mode 100644 index 00000000..cdfbe7a4 --- /dev/null +++ b/replicas-matrix-bridge/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +.wrangler/ +.dev.vars +dist/ +*.log diff --git a/replicas-matrix-bridge/AGENTS.md b/replicas-matrix-bridge/AGENTS.md new file mode 100644 index 00000000..1347b2b4 --- /dev/null +++ b/replicas-matrix-bridge/AGENTS.md @@ -0,0 +1,22 @@ +# AGENTS.md β€” replicas-matrix-bridge + +Read **[`../docs/agent-handoff.md`](../docs/agent-handoff.md)** before touching +code in this directory. It covers: + +- Architecture + end-to-end flow (listener β†’ dispatch β†’ poller) +- File-by-file index with line ranges for the hot path +- The full Durable Object storage-key model +- Recent commits with reasoning (why each fix exists) +- Production endpoints, deploy commands, log-tail filters +- Known open issues +- **Things Jaden likes vs. has explicitly disliked** β€” do not rebuild + the focus-window UI; do not break the prompt-reaction emojis + +Then follow the universal principles in the root [`../AGENTS.md`](../AGENTS.md). + +Quick orientation: + +- Worker name: `replicas-matrix-bridge` +- Latest deploy at the time of handoff: matrix version `941d8c95` (commit `e5f0ae3`) +- Hot file: `src/poller.ts` (the ReplicaPoller DO state machine) +- Render: `src/render.ts` (simple line-stream rolling log β€” do not collapse tool calls) diff --git a/replicas-matrix-bridge/README.md b/replicas-matrix-bridge/README.md new file mode 100644 index 00000000..49826df5 --- /dev/null +++ b/replicas-matrix-bridge/README.md @@ -0,0 +1,110 @@ +# replicas-matrix-bridge + +Cloudflare Worker that bridges a Matrix (Beeper) bot account to Replicas workspaces, mirroring the Telegram bridge UX. Sibling package to `replicas-telegram-bridge/`. + +## Architecture (Phase 1 β€” unencrypted rooms) + +``` +You (Beeper/Element) β†’ Matrix Client-Server API β†’ MatrixListener DO (alarm-driven /sync) + ↓ + /v1/replica spawn or follow-up + ↓ + ReplicaPoller DO (per replica) + ↓ + m.replace edit on the status message +``` + +Same `render.ts` / `markdown.ts` / state-machine semantics as the Telegram bridge β€” only the outbound calls differ. Tool-call lines, plan blocks (`Plan (X/N)` + `~item~`), narration messages, expandable old-log, phase emoji, time ticker, reactions on the user's prompt: all identical. + +## Setup + +### 1. Create a Beeper bot account + +- Sign up `replicas-bot@…` on `beeper.com` as a second account. +- In Beeper Desktop β†’ Settings β†’ Help & About β†’ **Show access token**. Format starts with `syt_…`. +- Note the bot's Matrix user ID (e.g. `@replicas-bot:beeper.com`) β€” visible at the top of the same page. + +### 2. Wire Cloudflare + +```bash +cd replicas-matrix-bridge +bun install + +# Create the KV namespace (paste the returned id into wrangler.toml). +wrangler kv:namespace create MAP + +# Set secrets. +wrangler secret put MATRIX_ACCESS_TOKEN # syt_… +wrangler secret put MATRIX_USER_ID # @replicas-bot:beeper.com +wrangler secret put REPLICAS_API_KEY # same key the Telegram bridge uses + +# Deploy. +wrangler deploy +``` + +### 3. Kick the listener once + +```bash +curl -X POST https://replicas-matrix-bridge..workers.dev/start-listener +``` + +The cron trigger (`*/1 * * * *`) will keep it alive after that. + +### 4. Invite the bot to a room + +From your own Beeper account, invite `@replicas-bot:beeper.com` to a new DM. The listener auto-accepts within ~1s. Send a message β€” should produce πŸ‘€ + a status frame, edited in place as the agent works. + +## Status frame example + +``` +πŸ€” Starting Β· 0s +``` + +becomes, mid-task: + +``` +πŸ”§ Running Β· step 3 Β· 14s +Reading the codebase to choose strategy… + +πŸ“‹ Plan (1/3) +βœ“ scaffold +β—¦ wire it up +β—¦ test + +πŸ”§ ls -la src/ +πŸ“– src/index.ts +✏️ src/server.ts +``` + +and finally: + +``` +πŸŽ‰ Done Β· 14s +``` + +β€” with the actual answer landing as a separate Markdown-rendered message right under it. The bot also sets reactions on the user's prompt (`πŸ‘€` β†’ phase β†’ `πŸŽ‰` or `😭`), pins the active status message, and keeps a typing indicator alive throughout. + +## Cancelling a run + +- Reply to the status message with `!cancel`. The listener picks it up and tells the DO to `DELETE /v1/replica/{id}`. + +## Limitations + +- **Unencrypted rooms only**. Beeper enforces E2EE by default; Matrix's Olm/Megolm key handling doesn't fit on Cloudflare Workers. Either use a manually-unencrypted DM, or wait for v2 (hosted listener with `matrix-bot-sdk`). +- **Self-bot mode supported**. If `MATRIX_USER_ID` is your own account (no dedicated bot user), the listener still works: it skips events whose `unsigned.transaction_id` is set (those echo the bot worker's own sends) instead of filtering by `sender`. Messages you type in Element/Beeper come back without `transaction_id` set and trigger normally. +- **No inline keyboards**. Matrix has no equivalent; cancel is via `!cancel` reply. +- **No `
`**. Older log entries go into a plain `
` (the user scrolls), not the expandable caret we use in Telegram. + +## Files + +| File | Purpose | +|------|---------| +| `src/index.ts` | Worker entrypoint + HTTP routes | +| `src/listener.ts` | `MatrixListener` DO β€” holds the /sync long-poll | +| `src/poller.ts` | `ReplicaPoller` DO β€” Matrix-flavored ReplicaPoller | +| `src/dispatch.ts` | `handleMatrixMessage` β€” KV lookup β†’ spawn/follow-up | +| `src/matrix.ts` | Matrix Client-Server API client (sendMessage, editMessage via m.replace, react via m.annotation, redact, pin, unpin, typing, joinRoom, sync, whoami) | +| `src/render.ts` | Pure render functions β€” copied verbatim from telegram bridge | +| `src/markdown.ts` | Markdown β†’ HTML β€” copied verbatim from telegram bridge | +| `src/*.test.ts` | Vitest suite β€” 58 tests | +| `wrangler.toml` | KV/DO bindings, env vars, cron trigger | diff --git a/replicas-matrix-bridge/bun.lock b/replicas-matrix-bridge/bun.lock new file mode 100644 index 00000000..2003c166 --- /dev/null +++ b/replicas-matrix-bridge/bun.lock @@ -0,0 +1,508 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "replicas-matrix-bridge", + "dependencies": { + "@matrix-org/olm": "^3.2.15", + }, + "devDependencies": { + "@cloudflare/vitest-pool-workers": "^0.5.0", + "@cloudflare/workers-types": "^4.20240925.0", + "typescript": "^5.5.0", + "vitest": "^2.0.5", + "wrangler": "^3.78.0", + }, + }, + }, + "packages": { + "@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.3.4", "", { "dependencies": { "mime": "^3.0.0" } }, "sha512-YLPHc8yASwjNkmcDMQMY35yiWjoKAKnhUbPRszBRS0YgH+IXtsMp61j+yTcnCE3oO2DgP0U3iejLC8FTtKDC8Q=="], + + "@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.0.2", "", { "peerDependencies": { "unenv": "2.0.0-rc.14", "workerd": "^1.20250124.0" }, "optionalPeers": ["workerd"] }, "sha512-nyzYnlZjjV5xT3LizahG1Iu6mnrCaxglJ04rZLpDwlDVDZ7v46lNsfxhV3A/xtfgQuSHmLnc6SVI+KwBpc3Lwg=="], + + "@cloudflare/vitest-pool-workers": ["@cloudflare/vitest-pool-workers@0.5.41", "", { "dependencies": { "birpc": "0.2.14", "cjs-module-lexer": "^1.2.3", "devalue": "^4.3.0", "esbuild": "0.17.19", "miniflare": "3.20241230.0", "semver": "^7.5.1", "wrangler": "3.100.0", "zod": "^3.22.3" }, "peerDependencies": { "@vitest/runner": "2.0.x - 2.1.x", "@vitest/snapshot": "2.0.x - 2.1.x", "vitest": "2.0.x - 2.1.x" } }, "sha512-J0uYmOKJgyo/az5nV8QHlR6xQ+HHB6S65tOEutkvUPbuPDbFlBPRT+XHJhSTNNvZGeM1t2qZIzxp0WGmXLtNlQ=="], + + "@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20250718.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-FHf4t7zbVN8yyXgQ/r/GqLPaYZSGUVzeR7RnL28Mwj2djyw2ZergvytVc7fdGcczl6PQh+VKGfZCfUqpJlbi9g=="], + + "@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20250718.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-fUiyUJYyqqp4NqJ0YgGtp4WJh/II/YZsUnEb6vVy5Oeas8lUOxnN+ZOJ8N/6/5LQCVAtYCChRiIrBbfhTn5Z8Q=="], + + "@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20250718.0", "", { "os": "linux", "cpu": "x64" }, "sha512-5+eb3rtJMiEwp08Kryqzzu8d1rUcK+gdE442auo5eniMpT170Dz0QxBrqkg2Z48SFUPYbj+6uknuA5tzdRSUSg=="], + + "@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20250718.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-Aa2M/DVBEBQDdATMbn217zCSFKE+ud/teS+fFS+OQqKABLn0azO2qq6ANAHYOIE6Q3Sq4CxDIQr8lGdaJHwUog=="], + + "@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20250718.0", "", { "os": "win32", "cpu": "x64" }, "sha512-dY16RXKffmugnc67LTbyjdDHZn5NoTF1yHEf2fN4+OaOnoGSp3N1x77QubTDwqZ9zECWxgQfDLjddcH8dWeFhg=="], + + "@cloudflare/workers-types": ["@cloudflare/workers-types@4.20260529.1", "", {}, "sha512-33n3nsaWELSgn4DLKj1X9dwZc3kVDnO+jF/hLH9fdaXG9mQzKDeUkQaVRWLJXvrPXPa9RaIuSAFO4Zh9YOqOog=="], + + "@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="], + + "@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], + + "@esbuild-plugins/node-globals-polyfill": ["@esbuild-plugins/node-globals-polyfill@0.2.3", "", { "peerDependencies": { "esbuild": "*" } }, "sha512-r3MIryXDeXDOZh7ih1l/yE9ZLORCd5e8vWg02azWRGj5SPTuoh69A2AIyn0Z31V/kHBfZ4HgWJ+OK3GTTwLmnw=="], + + "@esbuild-plugins/node-modules-polyfill": ["@esbuild-plugins/node-modules-polyfill@0.2.2", "", { "dependencies": { "escape-string-regexp": "^4.0.0", "rollup-plugin-node-polyfills": "^0.2.1" }, "peerDependencies": { "esbuild": "*" } }, "sha512-LXV7QsWJxRuMYvKbiznh+U1ilIop3g2TeKRzUxOG5X3YITc8JyyTa90BmLwqqv0YnX4v32CSlG+vsziZp9dMvA=="], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.17.19", "", { "os": "android", "cpu": "arm" }, "sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.17.19", "", { "os": "android", "cpu": "arm64" }, "sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.17.19", "", { "os": "android", "cpu": "x64" }, "sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.17.19", "", { "os": "darwin", "cpu": "arm64" }, "sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.17.19", "", { "os": "darwin", "cpu": "x64" }, "sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.17.19", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.17.19", "", { "os": "freebsd", "cpu": "x64" }, "sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.17.19", "", { "os": "linux", "cpu": "arm" }, "sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.17.19", "", { "os": "linux", "cpu": "arm64" }, "sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.17.19", "", { "os": "linux", "cpu": "ia32" }, "sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.17.19", "", { "os": "linux", "cpu": "none" }, "sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.17.19", "", { "os": "linux", "cpu": "none" }, "sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.17.19", "", { "os": "linux", "cpu": "ppc64" }, "sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.17.19", "", { "os": "linux", "cpu": "none" }, "sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.17.19", "", { "os": "linux", "cpu": "s390x" }, "sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.17.19", "", { "os": "linux", "cpu": "x64" }, "sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.17.19", "", { "os": "none", "cpu": "x64" }, "sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.17.19", "", { "os": "openbsd", "cpu": "x64" }, "sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.17.19", "", { "os": "sunos", "cpu": "x64" }, "sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.17.19", "", { "os": "win32", "cpu": "arm64" }, "sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.17.19", "", { "os": "win32", "cpu": "ia32" }, "sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.17.19", "", { "os": "win32", "cpu": "x64" }, "sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA=="], + + "@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="], + + "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.0.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ=="], + + "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.0.4" }, "os": "darwin", "cpu": "x64" }, "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q=="], + + "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.0.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg=="], + + "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.0.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ=="], + + "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.0.5", "", { "os": "linux", "cpu": "arm" }, "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g=="], + + "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA=="], + + "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.0.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA=="], + + "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw=="], + + "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA=="], + + "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw=="], + + "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.0.5" }, "os": "linux", "cpu": "arm" }, "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ=="], + + "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.0.4" }, "os": "linux", "cpu": "arm64" }, "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA=="], + + "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.0.4" }, "os": "linux", "cpu": "s390x" }, "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q=="], + + "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.0.4" }, "os": "linux", "cpu": "x64" }, "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA=="], + + "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" }, "os": "linux", "cpu": "arm64" }, "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g=="], + + "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.0.4" }, "os": "linux", "cpu": "x64" }, "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw=="], + + "@img/sharp-wasm32": ["@img/sharp-wasm32@0.33.5", "", { "dependencies": { "@emnapi/runtime": "^1.2.0" }, "cpu": "none" }, "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg=="], + + "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.33.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ=="], + + "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.33.5", "", { "os": "win32", "cpu": "x64" }, "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="], + + "@matrix-org/olm": ["@matrix-org/olm@3.2.15", "", {}, "sha512-S7lOrndAK9/8qOtaTq/WhttJC/o4GAzdfK0MUPpo8ApzsJEC0QjtwrkC3KBXdFP1cD1MXi/mlKR7aaoVMKgs6Q=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.4", "", { "os": "android", "cpu": "arm" }, "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.60.4", "", { "os": "android", "cpu": "arm64" }, "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.60.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.60.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.60.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.60.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.60.4", "", { "os": "linux", "cpu": "arm" }, "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.60.4", "", { "os": "linux", "cpu": "arm" }, "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.60.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.60.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.60.4", "", { "os": "linux", "cpu": "none" }, "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ=="], + + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.60.4", "", { "os": "linux", "cpu": "none" }, "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.60.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg=="], + + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.60.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.60.4", "", { "os": "linux", "cpu": "none" }, "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.60.4", "", { "os": "linux", "cpu": "none" }, "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.60.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.60.4", "", { "os": "linux", "cpu": "x64" }, "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.60.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg=="], + + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.60.4", "", { "os": "openbsd", "cpu": "x64" }, "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.60.4", "", { "os": "none", "cpu": "arm64" }, "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.60.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.60.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.60.4", "", { "os": "win32", "cpu": "x64" }, "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.60.4", "", { "os": "win32", "cpu": "x64" }, "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw=="], + + "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], + + "@types/node": ["@types/node@25.9.1", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg=="], + + "@types/node-forge": ["@types/node-forge@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw=="], + + "@vitest/expect": ["@vitest/expect@2.1.9", "", { "dependencies": { "@vitest/spy": "2.1.9", "@vitest/utils": "2.1.9", "chai": "^5.1.2", "tinyrainbow": "^1.2.0" } }, "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw=="], + + "@vitest/mocker": ["@vitest/mocker@2.1.9", "", { "dependencies": { "@vitest/spy": "2.1.9", "estree-walker": "^3.0.3", "magic-string": "^0.30.12" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^5.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg=="], + + "@vitest/pretty-format": ["@vitest/pretty-format@2.1.9", "", { "dependencies": { "tinyrainbow": "^1.2.0" } }, "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ=="], + + "@vitest/runner": ["@vitest/runner@2.1.9", "", { "dependencies": { "@vitest/utils": "2.1.9", "pathe": "^1.1.2" } }, "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g=="], + + "@vitest/snapshot": ["@vitest/snapshot@2.1.9", "", { "dependencies": { "@vitest/pretty-format": "2.1.9", "magic-string": "^0.30.12", "pathe": "^1.1.2" } }, "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ=="], + + "@vitest/spy": ["@vitest/spy@2.1.9", "", { "dependencies": { "tinyspy": "^3.0.2" } }, "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ=="], + + "@vitest/utils": ["@vitest/utils@2.1.9", "", { "dependencies": { "@vitest/pretty-format": "2.1.9", "loupe": "^3.1.2", "tinyrainbow": "^1.2.0" } }, "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ=="], + + "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], + + "acorn-walk": ["acorn-walk@8.3.5", "", { "dependencies": { "acorn": "^8.11.0" } }, "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw=="], + + "as-table": ["as-table@1.0.55", "", { "dependencies": { "printable-characters": "^1.0.42" } }, "sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ=="], + + "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], + + "birpc": ["birpc@0.2.14", "", {}, "sha512-37FHE8rqsYM5JEKCnXFyHpBCzvgHEExwVVTq+nUmloInU7l8ezD1TpOhKpS8oe1DTYFqEK27rFZVKG43oTqXRA=="], + + "blake3-wasm": ["blake3-wasm@2.1.5", "", {}, "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g=="], + + "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], + + "capnp-ts": ["capnp-ts@0.7.0", "", { "dependencies": { "debug": "^4.3.1", "tslib": "^2.2.0" } }, "sha512-XKxXAC3HVPv7r674zP0VC3RTXz+/JKhfyw94ljvF80yynK6VkTnqE3jMuN8b3dUVmmc43TjyxjW4KTsmB3c86g=="], + + "chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="], + + "check-error": ["check-error@2.1.3", "", {}, "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA=="], + + "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], + + "cjs-module-lexer": ["cjs-module-lexer@1.4.3", "", {}, "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q=="], + + "color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="], + + "confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], + + "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], + + "data-uri-to-buffer": ["data-uri-to-buffer@2.0.2", "", {}, "sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA=="], + + "date-fns": ["date-fns@4.3.0", "", {}, "sha512-OYcL+3N/jyWbYdFGqoMAhytDgxP9pbYPUUiRCOgn4Fewaadk9l/Wam4Avciiyp2BgkpfQyBV9B+ehnVJych+eQ=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="], + + "defu": ["defu@6.1.7", "", {}, "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ=="], + + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + + "devalue": ["devalue@4.3.3", "", {}, "sha512-UH8EL6H2ifcY8TbD2QsxwCC/pr5xSwPvv85LrLXVihmHVC3T3YqTCIwnR5ak0yO1KYqlxrPVOA/JVZJYPy2ATg=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], + + "esbuild": ["esbuild@0.17.19", "", { "optionalDependencies": { "@esbuild/android-arm": "0.17.19", "@esbuild/android-arm64": "0.17.19", "@esbuild/android-x64": "0.17.19", "@esbuild/darwin-arm64": "0.17.19", "@esbuild/darwin-x64": "0.17.19", "@esbuild/freebsd-arm64": "0.17.19", "@esbuild/freebsd-x64": "0.17.19", "@esbuild/linux-arm": "0.17.19", "@esbuild/linux-arm64": "0.17.19", "@esbuild/linux-ia32": "0.17.19", "@esbuild/linux-loong64": "0.17.19", "@esbuild/linux-mips64el": "0.17.19", "@esbuild/linux-ppc64": "0.17.19", "@esbuild/linux-riscv64": "0.17.19", "@esbuild/linux-s390x": "0.17.19", "@esbuild/linux-x64": "0.17.19", "@esbuild/netbsd-x64": "0.17.19", "@esbuild/openbsd-x64": "0.17.19", "@esbuild/sunos-x64": "0.17.19", "@esbuild/win32-arm64": "0.17.19", "@esbuild/win32-ia32": "0.17.19", "@esbuild/win32-x64": "0.17.19" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw=="], + + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + + "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + + "exit-hook": ["exit-hook@2.2.1", "", {}, "sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw=="], + + "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="], + + "exsolve": ["exsolve@1.0.8", "", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "get-source": ["get-source@2.0.12", "", { "dependencies": { "data-uri-to-buffer": "^2.0.0", "source-map": "^0.6.1" } }, "sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w=="], + + "glob-to-regexp": ["glob-to-regexp@0.4.1", "", {}, "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="], + + "hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="], + + "is-arrayish": ["is-arrayish@0.3.4", "", {}, "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA=="], + + "is-core-module": ["is-core-module@2.16.2", "", { "dependencies": { "hasown": "^2.0.3" } }, "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA=="], + + "itty-time": ["itty-time@1.0.6", "", {}, "sha512-+P8IZaLLBtFv8hCkIjcymZOp4UJ+xW6bSlQsXGqrkmJh7vSiMFSlNne0mCYagEE0N7HDNR5jJBRxwN0oYv61Rw=="], + + "loupe": ["loupe@3.2.1", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="], + + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "mime": ["mime@3.0.0", "", { "bin": { "mime": "cli.js" } }, "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="], + + "miniflare": ["miniflare@3.20241230.0", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "acorn": "^8.8.0", "acorn-walk": "^8.2.0", "capnp-ts": "^0.7.0", "exit-hook": "^2.2.1", "glob-to-regexp": "^0.4.1", "stoppable": "^1.1.0", "undici": "^5.28.4", "workerd": "1.20241230.0", "ws": "^8.18.0", "youch": "^3.2.2", "zod": "^3.22.3" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-ZtWNoNAIj5Q0Vb3B4SPEKr7DDmVG8a0Stsp/AuRkYXoJniA5hsbKjFNIGhTXGMIHVP5bvDrKJWt/POIDGfpiKg=="], + + "mlly": ["mlly@1.8.2", "", { "dependencies": { "acorn": "^8.16.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.3" } }, "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "mustache": ["mustache@4.2.0", "", { "bin": { "mustache": "bin/mustache" } }, "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ=="], + + "nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="], + + "node-forge": ["node-forge@1.4.0", "", {}, "sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ=="], + + "ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="], + + "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], + + "path-to-regexp": ["path-to-regexp@6.3.0", "", {}, "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="], + + "pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="], + + "pathval": ["pathval@2.0.1", "", {}, "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="], + + "postcss": ["postcss@8.5.15", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A=="], + + "printable-characters": ["printable-characters@1.0.42", "", {}, "sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ=="], + + "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], + + "resolve": ["resolve@1.22.12", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA=="], + + "rollup": ["rollup@4.60.4", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.4", "@rollup/rollup-android-arm64": "4.60.4", "@rollup/rollup-darwin-arm64": "4.60.4", "@rollup/rollup-darwin-x64": "4.60.4", "@rollup/rollup-freebsd-arm64": "4.60.4", "@rollup/rollup-freebsd-x64": "4.60.4", "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", "@rollup/rollup-linux-arm-musleabihf": "4.60.4", "@rollup/rollup-linux-arm64-gnu": "4.60.4", "@rollup/rollup-linux-arm64-musl": "4.60.4", "@rollup/rollup-linux-loong64-gnu": "4.60.4", "@rollup/rollup-linux-loong64-musl": "4.60.4", "@rollup/rollup-linux-ppc64-gnu": "4.60.4", "@rollup/rollup-linux-ppc64-musl": "4.60.4", "@rollup/rollup-linux-riscv64-gnu": "4.60.4", "@rollup/rollup-linux-riscv64-musl": "4.60.4", "@rollup/rollup-linux-s390x-gnu": "4.60.4", "@rollup/rollup-linux-x64-gnu": "4.60.4", "@rollup/rollup-linux-x64-musl": "4.60.4", "@rollup/rollup-openbsd-x64": "4.60.4", "@rollup/rollup-openharmony-arm64": "4.60.4", "@rollup/rollup-win32-arm64-msvc": "4.60.4", "@rollup/rollup-win32-ia32-msvc": "4.60.4", "@rollup/rollup-win32-x64-gnu": "4.60.4", "@rollup/rollup-win32-x64-msvc": "4.60.4", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g=="], + + "rollup-plugin-inject": ["rollup-plugin-inject@3.0.2", "", { "dependencies": { "estree-walker": "^0.6.1", "magic-string": "^0.25.3", "rollup-pluginutils": "^2.8.1" } }, "sha512-ptg9PQwzs3orn4jkgXJ74bfs5vYz1NCZlSQMBUA0wKcGp5i5pA1AO3fOUEte8enhGUC+iapTCzEWw2jEFFUO/w=="], + + "rollup-plugin-node-polyfills": ["rollup-plugin-node-polyfills@0.2.1", "", { "dependencies": { "rollup-plugin-inject": "^3.0.0" } }, "sha512-4kCrKPTJ6sK4/gLL/U5QzVT8cxJcofO0OU74tnB19F40cmuAKSzH5/siithxlofFEjwvw1YAhPmbvGNA6jEroA=="], + + "rollup-pluginutils": ["rollup-pluginutils@2.8.2", "", { "dependencies": { "estree-walker": "^0.6.1" } }, "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ=="], + + "selfsigned": ["selfsigned@2.4.1", "", { "dependencies": { "@types/node-forge": "^1.3.0", "node-forge": "^1" } }, "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q=="], + + "semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="], + + "sharp": ["sharp@0.33.5", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", "semver": "^7.6.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.33.5", "@img/sharp-darwin-x64": "0.33.5", "@img/sharp-libvips-darwin-arm64": "1.0.4", "@img/sharp-libvips-darwin-x64": "1.0.4", "@img/sharp-libvips-linux-arm": "1.0.5", "@img/sharp-libvips-linux-arm64": "1.0.4", "@img/sharp-libvips-linux-s390x": "1.0.4", "@img/sharp-libvips-linux-x64": "1.0.4", "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", "@img/sharp-libvips-linuxmusl-x64": "1.0.4", "@img/sharp-linux-arm": "0.33.5", "@img/sharp-linux-arm64": "0.33.5", "@img/sharp-linux-s390x": "0.33.5", "@img/sharp-linux-x64": "0.33.5", "@img/sharp-linuxmusl-arm64": "0.33.5", "@img/sharp-linuxmusl-x64": "0.33.5", "@img/sharp-wasm32": "0.33.5", "@img/sharp-win32-ia32": "0.33.5", "@img/sharp-win32-x64": "0.33.5" } }, "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw=="], + + "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], + + "simple-swizzle": ["simple-swizzle@0.2.4", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw=="], + + "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "sourcemap-codec": ["sourcemap-codec@1.4.8", "", {}, "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA=="], + + "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], + + "stacktracey": ["stacktracey@2.2.0", "", { "dependencies": { "as-table": "^1.0.36", "get-source": "^2.0.12" } }, "sha512-ETyQEz+CzXiLjEbyJqpbp+/T79RQD/6wqFucRBIlVNZfYq2Ay7wbretD4cxpbymZlaPWx58aIhPEY1Cr8DlVvg=="], + + "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], + + "stoppable": ["stoppable@1.1.0", "", {}, "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw=="], + + "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], + + "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], + + "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], + + "tinypool": ["tinypool@1.1.1", "", {}, "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg=="], + + "tinyrainbow": ["tinyrainbow@1.2.0", "", {}, "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ=="], + + "tinyspy": ["tinyspy@3.0.2", "", {}, "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "ufo": ["ufo@1.6.4", "", {}, "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA=="], + + "undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="], + + "undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="], + + "unenv": ["unenv@2.0.0-rc.14", "", { "dependencies": { "defu": "^6.1.4", "exsolve": "^1.0.1", "ohash": "^2.0.10", "pathe": "^2.0.3", "ufo": "^1.5.4" } }, "sha512-od496pShMen7nOy5VmVJCnq8rptd45vh6Nx/r2iPbrba6pa6p+tS2ywuIHRZ/OBvSbQZB0kWvpO9XBNVFXHD3Q=="], + + "vite": ["vite@5.4.21", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw=="], + + "vite-node": ["vite-node@2.1.9", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.3.7", "es-module-lexer": "^1.5.4", "pathe": "^1.1.2", "vite": "^5.0.0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA=="], + + "vitest": ["vitest@2.1.9", "", { "dependencies": { "@vitest/expect": "2.1.9", "@vitest/mocker": "2.1.9", "@vitest/pretty-format": "^2.1.9", "@vitest/runner": "2.1.9", "@vitest/snapshot": "2.1.9", "@vitest/spy": "2.1.9", "@vitest/utils": "2.1.9", "chai": "^5.1.2", "debug": "^4.3.7", "expect-type": "^1.1.0", "magic-string": "^0.30.12", "pathe": "^1.1.2", "std-env": "^3.8.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.1", "tinypool": "^1.0.1", "tinyrainbow": "^1.2.0", "vite": "^5.0.0", "vite-node": "2.1.9", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/node": "^18.0.0 || >=20.0.0", "@vitest/browser": "2.1.9", "@vitest/ui": "2.1.9", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q=="], + + "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], + + "workerd": ["workerd@1.20250718.0", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20250718.0", "@cloudflare/workerd-darwin-arm64": "1.20250718.0", "@cloudflare/workerd-linux-64": "1.20250718.0", "@cloudflare/workerd-linux-arm64": "1.20250718.0", "@cloudflare/workerd-windows-64": "1.20250718.0" }, "bin": { "workerd": "bin/workerd" } }, "sha512-kqkIJP/eOfDlUyBzU7joBg+tl8aB25gEAGqDap+nFWb+WHhnooxjGHgxPBy3ipw2hnShPFNOQt5lFRxbwALirg=="], + + "wrangler": ["wrangler@3.114.17", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.3.4", "@cloudflare/unenv-preset": "2.0.2", "@esbuild-plugins/node-globals-polyfill": "0.2.3", "@esbuild-plugins/node-modules-polyfill": "0.2.2", "blake3-wasm": "2.1.5", "esbuild": "0.17.19", "miniflare": "3.20250718.3", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.14", "workerd": "1.20250718.0" }, "optionalDependencies": { "fsevents": "~2.3.2", "sharp": "^0.33.5" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20250408.0" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-tAvf7ly+tB+zwwrmjsCyJ2pJnnc7SZhbnNwXbH+OIdVas3zTSmjcZOjmLKcGGptssAA3RyTKhcF9BvKZzMUycA=="], + + "ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="], + + "xxhash-wasm": ["xxhash-wasm@1.1.0", "", {}, "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA=="], + + "youch": ["youch@3.3.4", "", { "dependencies": { "cookie": "^0.7.1", "mustache": "^4.2.0", "stacktracey": "^2.1.8" } }, "sha512-UeVBXie8cA35DS6+nBkls68xaBBXCye0CNznrhszZjTbRVnJKQuNsyLKBTTL4ln1o1rh2PKtv35twV7irj5SEg=="], + + "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "@cloudflare/vitest-pool-workers/wrangler": ["wrangler@3.100.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.3.4", "@esbuild-plugins/node-globals-polyfill": "^0.2.3", "@esbuild-plugins/node-modules-polyfill": "^0.2.2", "blake3-wasm": "^2.1.5", "chokidar": "^4.0.1", "date-fns": "^4.1.0", "esbuild": "0.17.19", "itty-time": "^1.0.6", "miniflare": "3.20241230.0", "nanoid": "^3.3.3", "path-to-regexp": "^6.3.0", "resolve": "^1.22.8", "selfsigned": "^2.0.1", "source-map": "^0.6.1", "unenv": "npm:unenv-nightly@2.0.0-20241218-183400-5d6aec3", "workerd": "1.20241230.0", "xxhash-wasm": "^1.0.1" }, "optionalDependencies": { "fsevents": "~2.3.2" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20241230.0" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-+nsZK374Xnp2BEQQuB/18pnObgsOey0AHVlg75pAdwNaKAmB2aa0/E5rFb7i89DiiwFYoZMz3cARY1UKcm/WQQ=="], + + "miniflare/workerd": ["workerd@1.20241230.0", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20241230.0", "@cloudflare/workerd-darwin-arm64": "1.20241230.0", "@cloudflare/workerd-linux-64": "1.20241230.0", "@cloudflare/workerd-linux-arm64": "1.20241230.0", "@cloudflare/workerd-windows-64": "1.20241230.0" }, "bin": { "workerd": "bin/workerd" } }, "sha512-EgixXP0JGXGq6J9lz17TKIZtfNDUvJNG+cl9paPMfZuYWT920fFpBx+K04YmnbQRLnglsivF1GT9pxh1yrlWhg=="], + + "mlly/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "pkg-types/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "rollup/@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + + "rollup-plugin-inject/estree-walker": ["estree-walker@0.6.1", "", {}, "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w=="], + + "rollup-plugin-inject/magic-string": ["magic-string@0.25.9", "", { "dependencies": { "sourcemap-codec": "^1.4.8" } }, "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ=="], + + "rollup-pluginutils/estree-walker": ["estree-walker@0.6.1", "", {}, "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w=="], + + "unenv/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "vite/esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="], + + "wrangler/miniflare": ["miniflare@3.20250718.3", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "acorn": "8.14.0", "acorn-walk": "8.3.2", "exit-hook": "2.2.1", "glob-to-regexp": "0.4.1", "stoppable": "1.1.0", "undici": "^5.28.5", "workerd": "1.20250718.0", "ws": "8.18.0", "youch": "3.3.4", "zod": "3.22.3" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-JuPrDJhwLrNLEJiNLWO7ZzJrv/Vv9kZuwMYCfv0LskQDM6Eonw4OvywO3CH/wCGjgHzha/qyjUh8JQ068TjDgQ=="], + + "@cloudflare/vitest-pool-workers/wrangler/unenv": ["unenv-nightly@2.0.0-20241218-183400-5d6aec3", "", { "dependencies": { "defu": "^6.1.4", "mlly": "^1.7.3", "ohash": "^1.1.4", "pathe": "^1.1.2", "ufo": "^1.5.4" } }, "sha512-7Xpi29CJRbOV1/IrC03DawMJ0hloklDLq/cigSe+J2jkcC+iDres2Cy0r4ltj5f0x7DqsaGaB4/dLuCPPFZnZA=="], + + "@cloudflare/vitest-pool-workers/wrangler/workerd": ["workerd@1.20241230.0", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20241230.0", "@cloudflare/workerd-darwin-arm64": "1.20241230.0", "@cloudflare/workerd-linux-64": "1.20241230.0", "@cloudflare/workerd-linux-arm64": "1.20241230.0", "@cloudflare/workerd-windows-64": "1.20241230.0" }, "bin": { "workerd": "bin/workerd" } }, "sha512-EgixXP0JGXGq6J9lz17TKIZtfNDUvJNG+cl9paPMfZuYWT920fFpBx+K04YmnbQRLnglsivF1GT9pxh1yrlWhg=="], + + "miniflare/workerd/@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20241230.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-BZHLg4bbhNQoaY1Uan81O3FV/zcmWueC55juhnaI7NAobiQth9RppadPNpxNAmS9fK2mR5z8xrwMQSQrHmztyQ=="], + + "miniflare/workerd/@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20241230.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-lllxycj7EzYoJ0VOJh8M3palUgoonVrILnzGrgsworgWlIpgjfXGS7b41tEGCw6AxSxL9prmTIGtfSPUvn/rjg=="], + + "miniflare/workerd/@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20241230.0", "", { "os": "linux", "cpu": "x64" }, "sha512-Y3mHcW0KghOmWdNZyHYpEOG4Ba/ga8tht5vj1a+WXfagEjMO8Y98XhZUlCaYa9yB7Wh5jVcK5LM2jlO/BLgqpA=="], + + "miniflare/workerd/@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20241230.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-IAjhsWPlHzhhkJ6I49sDG6XfMnhPvv0szKGXxTWQK/IWMrbGdHm4RSfNKBSoLQm67jGMIzbmcrX9UIkms27Y1g=="], + + "miniflare/workerd/@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20241230.0", "", { "os": "win32", "cpu": "x64" }, "sha512-y5SPIk9iOb2gz+yWtHxoeMnjPnkYQswiCJ480oHC6zexnJLlKTpcmBCjDH1nWCT4pQi8F25gaH8thgElf4NvXQ=="], + + "vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="], + + "vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.21.5", "", { "os": "android", "cpu": "arm64" }, "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A=="], + + "vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.21.5", "", { "os": "android", "cpu": "x64" }, "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA=="], + + "vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.21.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ=="], + + "vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.21.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw=="], + + "vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.21.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g=="], + + "vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.21.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ=="], + + "vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.21.5", "", { "os": "linux", "cpu": "arm" }, "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA=="], + + "vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.21.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q=="], + + "vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.21.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg=="], + + "vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg=="], + + "vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg=="], + + "vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.21.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w=="], + + "vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA=="], + + "vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.21.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A=="], + + "vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.21.5", "", { "os": "linux", "cpu": "x64" }, "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ=="], + + "vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.21.5", "", { "os": "none", "cpu": "x64" }, "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg=="], + + "vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.21.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow=="], + + "vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.21.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg=="], + + "vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.21.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A=="], + + "vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.21.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA=="], + + "vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="], + + "wrangler/miniflare/acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="], + + "wrangler/miniflare/acorn-walk": ["acorn-walk@8.3.2", "", {}, "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A=="], + + "wrangler/miniflare/ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="], + + "wrangler/miniflare/zod": ["zod@3.22.3", "", {}, "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug=="], + + "@cloudflare/vitest-pool-workers/wrangler/unenv/ohash": ["ohash@1.1.6", "", {}, "sha512-TBu7PtV8YkAZn0tSxobKY2n2aAQva936lhRrj6957aDaCf9IEtqsKbgMzXE/F/sjqYOwmrukeORHNLe5glk7Cg=="], + + "@cloudflare/vitest-pool-workers/wrangler/workerd/@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20241230.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-BZHLg4bbhNQoaY1Uan81O3FV/zcmWueC55juhnaI7NAobiQth9RppadPNpxNAmS9fK2mR5z8xrwMQSQrHmztyQ=="], + + "@cloudflare/vitest-pool-workers/wrangler/workerd/@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20241230.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-lllxycj7EzYoJ0VOJh8M3palUgoonVrILnzGrgsworgWlIpgjfXGS7b41tEGCw6AxSxL9prmTIGtfSPUvn/rjg=="], + + "@cloudflare/vitest-pool-workers/wrangler/workerd/@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20241230.0", "", { "os": "linux", "cpu": "x64" }, "sha512-Y3mHcW0KghOmWdNZyHYpEOG4Ba/ga8tht5vj1a+WXfagEjMO8Y98XhZUlCaYa9yB7Wh5jVcK5LM2jlO/BLgqpA=="], + + "@cloudflare/vitest-pool-workers/wrangler/workerd/@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20241230.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-IAjhsWPlHzhhkJ6I49sDG6XfMnhPvv0szKGXxTWQK/IWMrbGdHm4RSfNKBSoLQm67jGMIzbmcrX9UIkms27Y1g=="], + + "@cloudflare/vitest-pool-workers/wrangler/workerd/@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20241230.0", "", { "os": "win32", "cpu": "x64" }, "sha512-y5SPIk9iOb2gz+yWtHxoeMnjPnkYQswiCJ480oHC6zexnJLlKTpcmBCjDH1nWCT4pQi8F25gaH8thgElf4NvXQ=="], + } +} diff --git a/replicas-matrix-bridge/package.json b/replicas-matrix-bridge/package.json new file mode 100644 index 00000000..470b33a0 --- /dev/null +++ b/replicas-matrix-bridge/package.json @@ -0,0 +1,24 @@ +{ + "name": "replicas-matrix-bridge", + "version": "0.1.0", + "private": true, + "description": "Cloudflare Worker that bridges a Matrix bot to Replicas workspaces (Phase 1 β€” unencrypted rooms, sibling to replicas-telegram-bridge)", + "type": "module", + "scripts": { + "dev": "wrangler dev", + "deploy": "wrangler deploy", + "test": "vitest run", + "test:watch": "vitest", + "typecheck": "tsc --noEmit" + }, + "devDependencies": { + "@cloudflare/vitest-pool-workers": "^0.5.0", + "@cloudflare/workers-types": "^4.20240925.0", + "typescript": "^5.5.0", + "vitest": "^2.0.5", + "wrangler": "^3.78.0" + }, + "dependencies": { + "@matrix-org/olm": "^3.2.15" + } +} diff --git a/replicas-matrix-bridge/src/dispatch.ts b/replicas-matrix-bridge/src/dispatch.ts new file mode 100644 index 00000000..43ec9cd0 --- /dev/null +++ b/replicas-matrix-bridge/src/dispatch.ts @@ -0,0 +1,234 @@ +import type { Env } from "./index"; +import { MatrixError, react, sendMessage } from "./matrix"; + +function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +// The πŸ‘€ ack is the FIRST signal the user gets that the bot heard them +// (lands before the "Starting" frame even sends). It must survive 429s +// from matrix.org β€” that's exactly the moment a busy room hits the +// limit. Retry up to 3x honoring retry_after_ms. +async function reactWithRetry( + env: { MATRIX_HOMESERVER: string; MATRIX_ACCESS_TOKEN: string; MATRIX_USER_ID: string }, + roomId: string, + eventId: string, + emoji: string, +): Promise { + for (let attempt = 0; attempt < 3; attempt++) { + try { + return await react(env, roomId, eventId, emoji); + } catch (e) { + if (e instanceof MatrixError && e.status === 429) { + const waitMs = Math.min(8_000, Math.max(500, e.retryAfterMs ?? 1_500)); + await sleep(waitMs); + continue; + } + return ""; + } + } + return ""; +} + +interface ReplicaCreateResponse { + replica?: { id: string }; + id?: string; +} + +export async function handleMatrixMessage( + env: Env, + roomId: string, + eventId: string, + text: string, +): Promise { + const key = `room:${roomId}`; + const seenKey = `seen:${roomId}:${eventId}`; + const seen = await env.MAP.get(seenKey); + if (seen) { + console.log(`[dispatch] DEDUPE skip room=${roomId} ev=${eventId}`); + return; + } + await env.MAP.put(seenKey, "1", { expirationTtl: 600 }); + + // Capture the πŸ‘€ ack reaction id so the watcher can redact it later when + // it swaps in the terminal emoji β€” otherwise both stack on the prompt. + // Goes through reactWithRetry so a 429 on a busy room doesn't silently + // eat the user's first acknowledgment signal. + const ackP = reactWithRetry(matrixEnvShape(env), roomId, eventId, "πŸ‘€"); + + const existing = await env.MAP.get(key); + console.log(`[dispatch] proceed room=${roomId} ev=${eventId} existing=${existing ?? "none"} text=${JSON.stringify(text.slice(0, 50))}`); + let replicaId: string | null = null; + let spawnedFresh = false; + + // Optimistic path: kick off the initial "Starting Β· 0s" status frame and + // the Replicas spawn/follow-up in parallel β€” and for follow-ups, start + // the watcher in parallel with sendFollowUp. The user sees activity in + // ~150ms instead of waiting on the 1.5s POST /replica roundtrip. + const initialFrameP = sendMessage( + matrixEnvShape(env), + roomId, + `πŸ€” Starting Β· 0s`, + { replyTo: eventId }, + ).catch(() => ""); + + if (existing) { + // Watcher fires in parallel with sendFollowUp. If the follow-up turns + // out to be `gone` (replica expired), we'll cancel and respawn. + const followUpP = sendFollowUp(existing, text, env, roomId, eventId); + const watcherP = startWatcher(env, existing, roomId, eventId, text, undefined, undefined); + const followUp = await followUpP; + await watcherP; + if (followUp.ok) { + replicaId = existing; + } else if (followUp.gone) { + env.WATCHER.get(env.WATCHER.idFromName(existing)) + .fetch("https://watcher/cancel", { method: "POST" }) + .catch(() => {}); + await env.MAP.delete(key); + replicaId = await createReplica(env, roomId, eventId, text); + spawnedFresh = true; + } + } else { + replicaId = await createReplica(env, roomId, eventId, text); + spawnedFresh = true; + } + + const initialFrameId = await initialFrameP; + + if (replicaId) { + const settledReplicaId = replicaId; + // For fresh spawns the watcher hasn't started yet. For follow-ups we + // already started it above and just need to forward the initial frame + // + ack ids β€” same /ack endpoint already handles that. + if (spawnedFresh) { + await startWatcher(env, settledReplicaId, roomId, eventId, text, undefined, initialFrameId || undefined); + } else if (initialFrameId) { + env.WATCHER.get(env.WATCHER.idFromName(settledReplicaId)) + .fetch("https://watcher/ack", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ initialStatusEventId: initialFrameId }), + }) + .catch(() => {}); + } + // Background: forward the ack reaction id once the react call lands. + ackP + .then((id) => { + if (!id) return; + const stub = env.WATCHER.get(env.WATCHER.idFromName(settledReplicaId)); + return stub.fetch("https://watcher/ack", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ ackReactionId: id }), + }); + }) + .catch(() => {}); + } + + if (!replicaId) { + console.log(`[dispatch] no replica for room=${roomId} ev=${eventId}`); + return; + } + + if (spawnedFresh) { + const ttl = Math.max(60, parseInt(env.REPLICA_TTL_SECONDS, 10) || 604800); + await env.MAP.put(key, replicaId, { expirationTtl: ttl }); + } +} + +async function sendFollowUp( + replicaId: string, + text: string, + env: Env, + roomId: string, + eventId: string, +): Promise<{ ok: boolean; gone: boolean }> { + const r = await fetch(`${env.REPLICAS_API_BASE}/replica/${replicaId}/messages`, { + method: "POST", + headers: replicasHeaders(env), + body: JSON.stringify({ message: prefixWithRoutingHeader(roomId, eventId, text) }), + }); + return { ok: r.ok, gone: r.status === 404 || r.status === 410 }; +} + +async function createReplica( + env: Env, + roomId: string, + eventId: string, + text: string, +): Promise { + // Per-room model override set via `!model ` chat command; falls + // back to the worker-wide default if not set for this room. + const roomModel = await env.MAP.get(`model:${roomId}`); + const body = { + name: `mx-${roomId.replace(/[^a-z0-9]/gi, "").slice(0, 16)}-${Date.now()}`, + message: prefixWithRoutingHeader(roomId, eventId, text), + environment_id: env.REPLICAS_ENV_ID, + source: "matrix", + coding_agent: env.REPLICAS_AGENT_OVERRIDE || "claude", + model: roomModel || env.REPLICAS_MODEL_OVERRIDE || "claude-sonnet-4-6", + thinking_level: env.REPLICAS_THINKING_OVERRIDE || "medium", + lifecycle_policy: "delete_after_inactivity", + auto_stop_minutes: 60, + metadata: { matrix_room_id: roomId, matrix_event_id: eventId }, + }; + const r = await fetch(`${env.REPLICAS_API_BASE}/replica`, { + method: "POST", + headers: replicasHeaders(env), + body: JSON.stringify(body), + }); + if (!r.ok) return null; + const json = (await r.json()) as ReplicaCreateResponse; + const replicaId = json.replica?.id ?? json.id ?? null; + // Caller does the startWatcher with the awaited ack reaction id. + return replicaId; +} + +async function startWatcher( + env: Env, + replicaId: string, + roomId: string, + eventId: string, + text: string, + ackReactionId?: string, + initialStatusEventId?: string, +): Promise { + const stub = env.WATCHER.get(env.WATCHER.idFromName(replicaId)); + await stub + .fetch("https://watcher/watch", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + replicaId, + roomId, + startEventId: eventId, + userText: text, + ackReactionId: ackReactionId || undefined, + initialStatusEventId: initialStatusEventId || undefined, + }), + }) + .catch(() => {}); +} + +export function prefixWithRoutingHeader(roomId: string, eventId: string, text: string): string { + const header = `[matrix:room=${roomId}:event=${eventId}]`; + const hint = + "# Spawned from Matrix. Your tool calls and final reply are auto-surfaced via an external poller. Emit Markdown freely; it'll be rendered to Matrix HTML."; + return `${header}\n${hint}\n\n${text}`; +} + +function replicasHeaders(env: Env): HeadersInit { + return { + Authorization: `Bearer ${env.REPLICAS_API_KEY}`, + "Replicas-Org-Id": env.REPLICAS_ORG_ID, + }; +} + +function matrixEnvShape(env: Env) { + return { + MATRIX_HOMESERVER: env.MATRIX_HOMESERVER, + MATRIX_ACCESS_TOKEN: env.MATRIX_ACCESS_TOKEN, + MATRIX_USER_ID: env.MATRIX_USER_ID, + }; +} diff --git a/replicas-matrix-bridge/src/index.ts b/replicas-matrix-bridge/src/index.ts new file mode 100644 index 00000000..764a1f9c --- /dev/null +++ b/replicas-matrix-bridge/src/index.ts @@ -0,0 +1,132 @@ +import { handleMatrixMessage } from "./dispatch"; + +export interface Env { + MAP: KVNamespace; + WATCHER: DurableObjectNamespace; + LISTENER: DurableObjectNamespace; + OLM_VAULT: DurableObjectNamespace; + MATRIX_HOMESERVER: string; + MATRIX_ACCESS_TOKEN: string; + MATRIX_USER_ID: string; + MATRIX_DEVICE_ID?: string; + // Base58-encoded SSSS recovery key (e.g. `EsTu zRAC eDpv …`) used to + // decrypt the cross-signing private keys stashed in the user's account + // data. Optional β€” only required by /admin/vault/cross-sign. + MATRIX_RECOVERY_KEY?: string; + REPLICAS_API_KEY: string; + REPLICAS_ORG_ID: string; + REPLICAS_ENV_ID: string; + REPLICAS_API_BASE: string; + REPLICA_TTL_SECONDS: string; + REPLICAS_AGENT_OVERRIDE?: string; + REPLICAS_MODEL_OVERRIDE?: string; + REPLICAS_THINKING_OVERRIDE?: string; + // JSON array of Megolm session keys (Element key-export format, decrypted + // out of band and stashed here). Used by the listener to decrypt + // m.room.encrypted events whose session_id we hold. + MATRIX_MEGOLM_KEYS_JSON?: string; +} + +export { ReplicaPoller } from "./poller"; +export { MatrixListener } from "./listener"; +export { OlmVault } from "./olm-vault"; + +interface DispatchBody { + roomId: string; + eventId: string; + body: string; +} + +export default { + async fetch(req: Request, env: Env, ctx: ExecutionContext): Promise { + const url = new URL(req.url); + + if (req.method === "GET" && url.pathname === "/health") { + return new Response("ok"); + } + + if (req.method === "POST" && url.pathname === "/start-listener") { + const stub = env.LISTENER.get(env.LISTENER.idFromName("global")); + await stub.fetch("https://listener/start", { method: "POST" }); + return new Response("ok"); + } + + if (req.method === "GET" && url.pathname === "/debug/listener") { + const stub = env.LISTENER.get(env.LISTENER.idFromName("global")); + return stub.fetch("https://listener/debug"); + } + + if (req.method === "GET" && url.pathname === "/debug/vault/identity") { + const stub = env.OLM_VAULT.get(env.OLM_VAULT.idFromName("global")); + return stub.fetch("https://vault/identity"); + } + if (req.method === "POST" && url.pathname === "/admin/listener/reset") { + const stub = env.LISTENER.get(env.LISTENER.idFromName("global")); + return stub.fetch("https://listener/reset", { method: "POST" }); + } + if (req.method === "POST" && url.pathname === "/admin/vault/reset") { + const stub = env.OLM_VAULT.get(env.OLM_VAULT.idFromName("global")); + return stub.fetch("https://vault/reset", { method: "POST" }); + } + if (req.method === "POST" && url.pathname === "/admin/vault/cross-sign") { + const stub = env.OLM_VAULT.get(env.OLM_VAULT.idFromName("global")); + return stub.fetch("https://vault/cross-sign", { method: "POST" }); + } + if (req.method === "POST" && url.pathname === "/admin/vault/bootstrap") { + const stub = env.OLM_VAULT.get(env.OLM_VAULT.idFromName("global")); + return stub.fetch("https://vault/bootstrap", { method: "POST" }); + } + if (req.method === "POST" && url.pathname === "/admin/vault/upload-device") { + const stub = env.OLM_VAULT.get(env.OLM_VAULT.idFromName("global")); + return stub.fetch("https://vault/upload-device", { method: "POST" }); + } + if (req.method === "POST" && url.pathname === "/admin/vault/upload-otks") { + const stub = env.OLM_VAULT.get(env.OLM_VAULT.idFromName("global")); + return stub.fetch("https://vault/upload-otks", { method: "POST", body: '{"count":50}' }); + } + if (req.method === "GET" && url.pathname === "/debug/vault/keystore") { + const stub = env.OLM_VAULT.get(env.OLM_VAULT.idFromName("global")); + return stub.fetch("https://vault/keystore"); + } + + if (req.method === "GET" && url.pathname === "/debug/olm") { + try { + const { getOlm } = await import("./olm-init"); + const Olm = await getOlm(); + const acc = new Olm.Account(); + acc.create(); + const ids = acc.identity_keys(); + acc.free(); + return new Response( + JSON.stringify({ ok: true, library_version: Olm.get_library_version(), identity_keys: JSON.parse(ids) }), + { headers: { "Content-Type": "application/json" } }, + ); + } catch (e) { + return new Response( + JSON.stringify({ ok: false, error: e instanceof Error ? `${e.name}: ${e.message}` : String(e), stack: e instanceof Error ? e.stack : undefined }), + { status: 500, headers: { "Content-Type": "application/json" } }, + ); + } + } + + if (req.method === "GET" && url.pathname.startsWith("/debug/watcher/")) { + const replicaId = url.pathname.slice("/debug/watcher/".length); + if (!replicaId) return new Response("missing replica id", { status: 400 }); + const stub = env.WATCHER.get(env.WATCHER.idFromName(replicaId)); + return stub.fetch("https://watcher/debug", { method: "GET" }); + } + + if (req.method === "POST" && url.pathname === "/dispatch") { + const body = (await req.json()) as DispatchBody; + ctx.waitUntil(handleMatrixMessage(env, body.roomId, body.eventId, body.body)); + return new Response("ok"); + } + + return new Response("not found", { status: 404 }); + }, + + async scheduled(_event: ScheduledController, env: Env, _ctx: ExecutionContext): Promise { + const stub = env.LISTENER.get(env.LISTENER.idFromName("global")); + await stub.fetch("https://listener/start", { method: "POST" }).catch(() => {}); + }, +}; diff --git a/replicas-matrix-bridge/src/listener.ts b/replicas-matrix-bridge/src/listener.ts new file mode 100644 index 00000000..9674faed --- /dev/null +++ b/replicas-matrix-bridge/src/listener.ts @@ -0,0 +1,347 @@ +import { handleMatrixMessage } from "./dispatch"; +import type { Env } from "./index"; +import { joinRoom, sendMessage, sync, type SyncResponse } from "./matrix"; +import { decryptMegolm, findSessionKey } from "./megolm"; +import { parseKeyExport, type MegolmSessionKey } from "./megolm-keys"; +import { escapeHtml } from "./render"; + +// Aliases to make the chat command friendlier than typing the full id. +function resolveModelAlias(input: string): string { + const n = input.trim().toLowerCase(); + if (n === "sonnet") return "claude-sonnet-4-6"; + if (n === "opus") return "claude-opus-4-7"; + if (n === "haiku") return "claude-haiku-4-5"; + return input.trim(); +} + +/** + * MatrixListener β€” single global Durable Object that holds the bot's + * /sync long-poll. Re-runs on a ~1s alarm so we keep catching up + * regardless of bot inactivity. + * + * For every new m.room.message event in a joined room from a non-self + * sender, it triggers the Worker's incoming-message route to spawn or + * follow-up the appropriate Replicas workspace. + * + * For every m.room.member invite, auto-accepts so users can add the + * bot just by inviting it. + */ + +const ALARM_INTERVAL_MS = 1000; +const SYNC_TIMEOUT_MS = 28_000; // under CF Workers' 30s fetch limit + +export class MatrixListener { + private state: DurableObjectState; + private env: Env; + + constructor(state: DurableObjectState, env: Env) { + this.state = state; + this.env = env; + } + + async fetch(req: Request): Promise { + const url = new URL(req.url); + if (req.method === "POST" && url.pathname === "/start") { + await this.state.storage.setAlarm(Date.now() + 100); + return new Response("ok"); + } + if (req.method === "POST" && url.pathname === "/reset") { + // Drop the since token so the next /sync starts cleanly. Used when + // the access token / device changes β€” the to_device queue on the + // new device starts fresh. + await this.state.storage.delete("since"); + await this.state.storage.setAlarm(Date.now() + 100); + return new Response("ok"); + } + if (req.method === "POST" && url.pathname === "/stop") { + await this.state.storage.deleteAlarm(); + return new Response("ok"); + } + if (req.method === "GET" && url.pathname === "/debug") { + const since = await this.state.storage.get("since"); + const alarmAt = await this.state.storage.getAlarm(); + return new Response( + JSON.stringify({ since: since ?? null, alarmAt }, null, 2), + { headers: { "Content-Type": "application/json" } }, + ); + } + return new Response("not found", { status: 404 }); + } + + async alarm(): Promise { + try { + await this.alarmInner(); + } catch (e) { + console.error("[listener] alarm threw", e instanceof Error ? e.message : String(e)); + } + await this.state.storage.setAlarm(Date.now() + ALARM_INTERVAL_MS); + } + + private async alarmInner(): Promise { + const since = await this.state.storage.get("since"); + let resp: SyncResponse; + try { + resp = await sync(matrixEnv(this.env), since, SYNC_TIMEOUT_MS); + } catch (e) { + console.log(`[listener] /sync failed: ${e instanceof Error ? e.message : String(e)}`); + return; + } + + if (resp.next_batch) await this.state.storage.put("since", resp.next_batch); + + // On the very first sync (no prior token), skip historical events to + // avoid re-processing everything ever sent in joined rooms. + if (!since) return; + + // Drain to_device events before timeline so any m.room_key shares + // land in the live keystore before we try to decrypt this batch's + // encrypted room messages. + const toDevice = resp.to_device?.events ?? []; + for (const ev of toDevice) { + if (ev.type !== "m.room.encrypted") continue; + if (ev.content?.algorithm !== "m.olm.v1.curve25519-aes-sha2") continue; + const senderKey = (ev.content as { sender_key?: string }).sender_key; + if (!senderKey) continue; + const ourId = await this.ourCurve25519(); + const entry = ev.content.ciphertext?.[ourId]; + if (!entry?.body || entry.type === undefined) continue; + try { + const stub = this.env.OLM_VAULT.get(this.env.OLM_VAULT.idFromName("global")); + const r = await stub.fetch("https://vault/decrypt-todevice", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ senderCurve25519: senderKey, ciphertext: { type: entry.type, body: entry.body } }), + }); + const j = (await r.json()) as { ok?: boolean; captured?: boolean }; + console.log( + `[listener] to_device sender=${senderKey.slice(0, 12)}… type=${entry.type} ok=${j.ok} captured=${j.captured}`, + ); + } catch (e) { + console.log(`[listener] to_device decrypt failed: ${e instanceof Error ? e.message : e}`); + } + } + + const invited = resp.rooms?.invite ?? {}; + for (const roomId of Object.keys(invited)) { + console.log(`[listener] auto-join invite ${roomId}`); + try { + await joinRoom(matrixEnv(this.env), roomId); + } catch (e) { + console.log(`[listener] join failed for ${roomId}: ${e instanceof Error ? e.message : e}`); + } + } + + const joined = resp.rooms?.join ?? {}; + for (const [roomId, room] of Object.entries(joined)) { + const events = room.timeline?.events ?? []; + const megolmKeys = this.megolmKeys(); + for (const ev of events) { + if (!ev.event_id || !ev.sender) continue; + // Skip our own sends. We can't filter by `sender` because the + // bot account may be the same Matrix user the human types + // from (self-bot mode). Instead, /sync echoes back the + // transaction_id only to the access token that PUT the event, + // so its presence means "this came from us" regardless of who + // the sender field says it is. + if (ev.unsigned?.transaction_id) continue; + + let msgtype: string | undefined; + let body: string | undefined; + + if (ev.type === "m.room.message") { + const content = ev.content ?? {}; + msgtype = content.msgtype as string | undefined; + body = content.body as string | undefined; + } else if (ev.type === "m.room.encrypted") { + // E2EE event β€” try to decrypt using imported Megolm keys. + const decrypted = await this.tryDecrypt(roomId, ev, megolmKeys); + if (!decrypted) continue; + msgtype = decrypted.msgtype; + body = decrypted.body; + } else { + continue; + } + if (msgtype !== "m.text" || !body) continue; + + // Commands always run regardless of room size or mention. + const trimmed = body.trim(); + if (trimmed.startsWith("!cancel")) { + await this.handleCancel(roomId); + continue; + } + if (trimmed.startsWith("!model")) { + await this.handleModelCommand(roomId, ev.event_id, trimmed); + continue; + } + + // Gate auto-dispatch on room size + mention. In a 2-person + // room (you + me), every message is for me. In a larger room + // the bot should stay quiet unless it's been mentioned. + const shouldDispatch = await this.shouldHandleMessage(roomId, ev); + if (!shouldDispatch) continue; + + await this.dispatchMessage(roomId, ev.event_id, body); + } + } + } + + private cachedKeys: MegolmSessionKey[] | undefined; + private cachedCurve25519: string | undefined; + + private megolmKeys(): MegolmSessionKey[] { + if (this.cachedKeys === undefined) { + this.cachedKeys = parseKeyExport(this.env.MATRIX_MEGOLM_KEYS_JSON); + console.log(`[listener] loaded ${this.cachedKeys.length} Megolm session keys`); + } + return this.cachedKeys; + } + + private async ourCurve25519(): Promise { + if (this.cachedCurve25519) return this.cachedCurve25519; + const stub = this.env.OLM_VAULT.get(this.env.OLM_VAULT.idFromName("global")); + const r = await stub.fetch("https://vault/identity"); + const j = (await r.json()) as { identity_keys?: { curve25519?: string } }; + this.cachedCurve25519 = j.identity_keys?.curve25519 ?? ""; + return this.cachedCurve25519; + } + + // Live keystore lookup: ask the OlmVault for a Megolm session_key captured + // via /sendToDevice. Falls back to the static import. + private async findKey(roomId: string, sessionId: string, keys: MegolmSessionKey[]): Promise { + const fromImport = findSessionKey(keys, roomId, sessionId); + if (fromImport) return fromImport; + try { + const stub = this.env.OLM_VAULT.get(this.env.OLM_VAULT.idFromName("global")); + const r = await stub.fetch( + `https://vault/lookup?room=${encodeURIComponent(roomId)}&session=${encodeURIComponent(sessionId)}`, + ); + const j = (await r.json()) as { found?: boolean; session_key?: string | null }; + return j.found && j.session_key ? j.session_key : undefined; + } catch { + return undefined; + } + } + + private async tryDecrypt( + roomId: string, + ev: { content?: Record; event_id?: string }, + keys: MegolmSessionKey[], + ): Promise<{ msgtype: string; body: string } | undefined> { + const content = ev.content ?? {}; + if (content.algorithm !== "m.megolm.v1.aes-sha2") return undefined; + const sessionId = content.session_id as string | undefined; + const ciphertext = content.ciphertext as string | undefined; + if (!sessionId || !ciphertext) return undefined; + const sessionKey = await this.findKey(roomId, sessionId, keys); + if (!sessionKey) { + console.log(`[listener] no key for room=${roomId} session=${sessionId.slice(0, 16)}…`); + return undefined; + } + try { + const { plaintext } = await decryptMegolm(sessionKey, ciphertext); + const inner = JSON.parse(plaintext) as { + type?: string; + content?: { msgtype?: string; body?: string }; + }; + if (inner.type !== "m.room.message") return undefined; + return { + msgtype: inner.content?.msgtype ?? "", + body: inner.content?.body ?? "", + }; + } catch (e) { + console.log(`[listener] decrypt fail ev=${ev.event_id}: ${e instanceof Error ? e.message : e}`); + return undefined; + } + } + + // Returns true if the bot should respond. In a 2-person room every + // message is for the bot. In a larger room we require an explicit + // mention via `m.mentions.user_ids` (preferred per Matrix spec) or + // a matrix.to link in the formatted_body. This prevents the bot from + // chiming in on every random message once you add it to group chats. + private async shouldHandleMessage( + roomId: string, + ev: { content?: Record }, + ): Promise { + const memberCount = await this.cachedRoomMemberCount(roomId); + if (memberCount <= 2) return true; + const content = ev.content ?? {}; + const mentions = (content["m.mentions"] as { user_ids?: string[] } | undefined)?.user_ids; + if (Array.isArray(mentions) && mentions.includes(this.env.MATRIX_USER_ID)) return true; + const fmt = content.formatted_body as string | undefined; + if (fmt && (fmt.includes(this.env.MATRIX_USER_ID) || fmt.includes(`/${this.env.MATRIX_USER_ID}`))) return true; + return false; + } + + private async cachedRoomMemberCount(roomId: string): Promise { + const cacheKey = `members:${roomId}`; + const cached = await this.env.MAP.get(cacheKey); + if (cached) { + const n = parseInt(cached, 10); + if (Number.isFinite(n)) return n; + } + try { + const url = `${this.env.MATRIX_HOMESERVER}/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/joined_members`; + const r = await fetch(url, { + headers: { Authorization: `Bearer ${this.env.MATRIX_ACCESS_TOKEN}` }, + }); + if (!r.ok) return 2; // fail-open to "treat as DM" so we don't go silent + const j = (await r.json()) as { joined?: Record }; + const n = Object.keys(j.joined ?? {}).length; + await this.env.MAP.put(cacheKey, String(n), { expirationTtl: 3600 }); + return n; + } catch { + return 2; + } + } + + // `!model` chat command. `!model` alone prints current + options; + // `!model ` sets a per-room override stored in KV. Dispatch reads + // this on spawn, so the next turn uses the new model. + private async handleModelCommand(roomId: string, eventId: string, body: string): Promise { + const rest = body.slice("!model".length).trim(); + const cacheKey = `model:${roomId}`; + if (!rest) { + const current = (await this.env.MAP.get(cacheKey)) ?? this.env.REPLICAS_MODEL_OVERRIDE ?? "claude-sonnet-4-6"; + const html = [ + `Current model: ${escapeHtml(current)}`, + `Set with !model sonnet, !model opus, !model haiku, or a full id like !model claude-opus-4-7.`, + ].join("

"); + await sendMessage(matrixEnv(this.env), roomId, html, { replyTo: eventId }); + return; + } + const resolved = resolveModelAlias(rest); + await this.env.MAP.put(cacheKey, resolved, { expirationTtl: 60 * 60 * 24 * 365 }); + const html = `Model for this room set to ${escapeHtml(resolved)}
Next turn will use it.`; + await sendMessage(matrixEnv(this.env), roomId, html, { replyTo: eventId }); + } + + private async dispatchMessage(roomId: string, eventId: string, body: string): Promise { + // In-process call so we don't pay an extra Worker round-trip. + try { + await handleMatrixMessage(this.env, roomId, eventId, body); + } catch (e) { + console.log(`[listener] dispatch failed: ${e instanceof Error ? e.message : e}`); + } + } + + private async handleCancel(roomId: string): Promise { + const key = `room:${roomId}`; + const replicaId = await this.env.MAP.get(key); + if (!replicaId) return; + const stub = this.env.WATCHER.get(this.env.WATCHER.idFromName(replicaId)); + await stub.fetch("https://watcher/cancel", { method: "POST" }).catch(() => {}); + } +} + +function matrixEnv(env: Env): { + MATRIX_HOMESERVER: string; + MATRIX_ACCESS_TOKEN: string; + MATRIX_USER_ID: string; +} { + return { + MATRIX_HOMESERVER: env.MATRIX_HOMESERVER, + MATRIX_ACCESS_TOKEN: env.MATRIX_ACCESS_TOKEN, + MATRIX_USER_ID: env.MATRIX_USER_ID, + }; +} diff --git a/replicas-matrix-bridge/src/markdown.test.ts b/replicas-matrix-bridge/src/markdown.test.ts new file mode 100644 index 00000000..6ca527cb --- /dev/null +++ b/replicas-matrix-bridge/src/markdown.test.ts @@ -0,0 +1,117 @@ +import { describe, expect, it } from "vitest"; +import { markdownToTelegramHtml } from "./markdown"; + +describe("markdownToTelegramHtml", () => { + it("converts double-asterisk bold", () => { + expect(markdownToTelegramHtml("**Core (always loaded)**")).toBe( + "Core (always loaded)", + ); + }); + + it("converts single-asterisk italic without eating bold", () => { + expect(markdownToTelegramHtml("a *one* and **two** there")).toBe( + "a one and two there", + ); + }); + + it("converts inline code and escapes HTML inside", () => { + expect(markdownToTelegramHtml("call `` now")).toBe( + "call <foo & bar> now", + ); + }); + + it("converts fenced code blocks with language", () => { + expect(markdownToTelegramHtml("```ts\nconst x = 1;\n```")).toBe( + '
const x = 1;
', + ); + }); + + it("converts plain fenced code blocks without language", () => { + expect(markdownToTelegramHtml("```\nhello\n```")).toBe("
hello
"); + }); + + it("converts markdown links", () => { + expect(markdownToTelegramHtml("[github](https://github.com/owner/repo)")).toBe( + 'github', + ); + }); + + it("escapes ampersands and angle brackets in prose", () => { + expect(markdownToTelegramHtml("if x < 5 && y > 0")).toBe("if x < 5 && y > 0"); + }); + + it("preserves code-block contents from markdown processing", () => { + expect(markdownToTelegramHtml("Use ```\n**not bold**\n```")).toBe( + "Use
**not bold**
", + ); + }); + + it("converts single-tilde and double-tilde strikethrough", () => { + expect(markdownToTelegramHtml("~~gone~~")).toBe("gone"); + expect(markdownToTelegramHtml("~item~")).toBe("item"); + expect(markdownToTelegramHtml("a ~mid~ b")).toBe("a mid b"); + }); + + it("does not strike words that include a tilde elsewhere", () => { + expect(markdownToTelegramHtml("foo~bar~baz")).toBe("foo~bar~baz"); + }); + + it("handles a realistic agent reply", () => { + const md = [ + "Here's a high-level overview:", + "", + "**Core (always loaded)**", + "- File ops: Read, Write, Edit, Glob, Grep", + "- Shell: Bash", + "", + "**Deferred** β€” load via `ToolSearch`:", + "- *italic* and ~~strike~~ work too", + ].join("\n"); + + const html = markdownToTelegramHtml(md); + expect(html).toContain("Core (always loaded)"); + expect(html).toContain("Deferred"); + expect(html).toContain("ToolSearch"); + expect(html).toContain("italic"); + expect(html).toContain("strike"); + }); + + it("renders ATX headers as

-

", () => { + expect(markdownToTelegramHtml("# Big")).toContain("

Big

"); + expect(markdownToTelegramHtml("### Smaller")).toContain("

Smaller

"); + expect(markdownToTelegramHtml("###### Smallest")).toContain("
Smallest
"); + }); + + it("renders bullet lists as