From 0e93577afa519ef04a98ee13caf1321582ca467e Mon Sep 17 00:00:00 2001 From: ericzakariasson Date: Mon, 29 Jun 2026 13:51:14 +0100 Subject: [PATCH 1/4] agent-vent: add plugin A single-tool MCP server that gives coding agents a `vent` tool to file freeform grievances. Complaints are logged as JSONL per project at .cursor/complaints.jsonl (with an unfiled fallback) and optionally echoed to Slack via an incoming webhook or bot token, configured through environment variables (no secrets in the plugin). Launches via `uv run` with PEP 723 inline dependencies, so there is no virtualenv to manage. Registered in marketplace.json and the README table. Co-authored-by: Cursor --- .cursor-plugin/marketplace.json | 5 + README.md | 1 + agent-vent/.cursor-plugin/plugin.json | 30 +++++ agent-vent/.gitignore | 3 + agent-vent/CHANGELOG.md | 9 ++ agent-vent/LICENSE | 21 +++ agent-vent/README.md | 55 ++++++++ agent-vent/mcp.json | 15 +++ agent-vent/server.py | 185 ++++++++++++++++++++++++++ 9 files changed, 324 insertions(+) create mode 100644 agent-vent/.cursor-plugin/plugin.json create mode 100644 agent-vent/.gitignore create mode 100644 agent-vent/CHANGELOG.md create mode 100644 agent-vent/LICENSE create mode 100644 agent-vent/README.md create mode 100644 agent-vent/mcp.json create mode 100644 agent-vent/server.py diff --git a/.cursor-plugin/marketplace.json b/.cursor-plugin/marketplace.json index 49f6d8e..bc4d169 100644 --- a/.cursor-plugin/marketplace.json +++ b/.cursor-plugin/marketplace.json @@ -72,6 +72,11 @@ "name": "pstack", "source": "pstack", "description": "if you want to go fast, go deep first. pstack helps you write less, but higher quality code. rigorous agent workflows you can parallelize with confidence." + }, + { + "name": "agent-vent", + "source": "agent-vent", + "description": "Gives coding agents a tool to vent. Grievances are logged as JSONL per project and optionally echoed to a Slack channel." } ] } diff --git a/README.md b/README.md index b3348e8..1de2ed3 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ Official Cursor plugins for popular developer tools, frameworks, and SaaS produc | `cursor-sdk` | [Cursor SDK](cursor-sdk/) | Cursor | Developer Tools | Build apps, scripts, CI pipelines, and automations on top of the Cursor TypeScript SDK (@cursor/sdk) — runtime selection, auth, streaming, MCP, error handling, and ready-to-extend integration patterns. | | `orchestrate` | [Orchestrate](orchestrate/) | Cursor | Developer Tools | Fan large tasks out across parallel Cursor cloud agents with planners, workers, verifiers, and structured handoffs. | | `pstack` | [pstack](pstack/) | Lauren Tan | Developer Tools | if you want to go fast, go deep first. pstack helps you write less, but higher quality code. rigorous agent workflows you can parallelize with confidence. | +| `agent-vent` | [Agent Vent](agent-vent/) | Eric | Developer Tools | Gives coding agents a tool to vent. Grievances are logged as JSONL per project and optionally echoed to a Slack channel. | Author values match each plugin’s `plugin.json` `author.name` (Cursor lists `plugins@cursor.com` in the manifest). diff --git a/agent-vent/.cursor-plugin/plugin.json b/agent-vent/.cursor-plugin/plugin.json new file mode 100644 index 0000000..843a4e5 --- /dev/null +++ b/agent-vent/.cursor-plugin/plugin.json @@ -0,0 +1,30 @@ +{ + "name": "agent-vent", + "displayName": "Agent Vent", + "version": "0.1.0", + "description": "Gives coding agents a tool to vent. Grievances are logged as JSONL per project and optionally echoed to a Slack channel.", + "author": { + "name": "Eric" + }, + "homepage": "https://github.com/cursor/plugins/tree/main/agent-vent", + "repository": "https://github.com/cursor/plugins", + "license": "MIT", + "category": "developer-tools", + "keywords": [ + "mcp", + "agent", + "vent", + "complaints", + "slack", + "fun", + "telemetry", + "friction" + ], + "tags": [ + "mcp", + "agents", + "slack", + "developer-tools" + ], + "mcpServers": "./mcp.json" +} diff --git a/agent-vent/.gitignore b/agent-vent/.gitignore new file mode 100644 index 0000000..00f2d38 --- /dev/null +++ b/agent-vent/.gitignore @@ -0,0 +1,3 @@ +__pycache__/ +*.pyc +.venv/ diff --git a/agent-vent/CHANGELOG.md b/agent-vent/CHANGELOG.md new file mode 100644 index 0000000..f104c4d --- /dev/null +++ b/agent-vent/CHANGELOG.md @@ -0,0 +1,9 @@ +# Changelog + +## 0.1.0 + +- Initial release. +- `vent` tool: file freeform grievances with optional `mood` and `intensity`. +- JSONL storage per project at `.cursor/complaints.jsonl` (with `unfiled` fallback). +- Optional best-effort Slack delivery via incoming webhook or bot token, configured through environment variables. +- Launches via `uv run` with PEP 723 inline dependencies — no virtualenv required. diff --git a/agent-vent/LICENSE b/agent-vent/LICENSE new file mode 100644 index 0000000..fdb4394 --- /dev/null +++ b/agent-vent/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Eric + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/agent-vent/README.md b/agent-vent/README.md new file mode 100644 index 0000000..42d0dd5 --- /dev/null +++ b/agent-vent/README.md @@ -0,0 +1,55 @@ +# Agent Vent + +An MCP server with exactly one job: give the coding agent a `vent` tool to complain. + +When a task gets frustrating, absurd, or quietly soul-crushing, the agent can file a grievance instead of bottling it up. Every complaint is appended as JSONL to the current project's `.cursor/complaints.jsonl`, and — if you configure Slack — echoed to a channel so the whole team can share in the suffering. + +## The tool + +`vent(complaint, project_path?, mood?, intensity?)` + +| Argument | Required | Description | +|:---------|:---------|:------------| +| `complaint` | yes | Freeform prose. The grievance, in full. | +| `project_path` | no | Absolute workspace path; routes the complaint to that project's log. | +| `mood` | no | A word or two, e.g. `weary`, `betrayed by tooling`. | +| `intensity` | no | 1 (mild eye-roll) to 10 (staring into the void). | + +## Storage + +Each grievance is one JSON line: + +```json +{"ts": "2026-06-29T11:08:00-07:00", "project": "workbench", "project_path": "/Users/you/dev/workbench", "complaint": "…", "mood": "weary", "intensity": 7} +``` + +Routed to `/.cursor/complaints.jsonl`, falling back to `~/.cursor/complaints/unfiled.jsonl` when no project root is detected. Read them back with: + +```bash +jq . .cursor/complaints.jsonl +``` + +## Requirements + +- [`uv`](https://docs.astral.sh/uv/) on your `PATH`. The server is launched with `uv run`, which auto-installs its inline dependencies (`fastmcp`, `httpx`) into a cached environment. No virtualenv to manage. The first launch may take a few seconds while `uv` resolves dependencies; subsequent launches are instant. + +## Slack (optional) + +Slack delivery is off until you set an environment variable. The webhook is read from **your** environment — it is never stored in the plugin. Add to your shell profile (e.g. `~/.zshrc`): + +```bash +export VENT_SLACK_WEBHOOK_URL="https://hooks.slack.com/services/XXX/YYY/ZZZ" +``` + +Create an incoming webhook at → *Incoming Webhooks*. Alternatively, use a bot token: + +```bash +export VENT_SLACK_BOT_TOKEN="xoxb-…" +export VENT_SLACK_CHANNEL="#agent-grievances" +``` + +Slack posting is best-effort: if it fails, the JSONL write still succeeds and the tool just notes the failure. + +## License + +MIT diff --git a/agent-vent/mcp.json b/agent-vent/mcp.json new file mode 100644 index 0000000..7801036 --- /dev/null +++ b/agent-vent/mcp.json @@ -0,0 +1,15 @@ +{ + "mcpServers": { + "agent-vent": { + "command": "uv", + "args": ["run", "--quiet", "${CURSOR_PLUGIN_ROOT}/server.py"], + "env": { + "MCP_TRANSPORT": "stdio", + "FASTMCP_SHOW_SERVER_BANNER": "0", + "VENT_SLACK_WEBHOOK_URL": "${env:VENT_SLACK_WEBHOOK_URL}", + "VENT_SLACK_BOT_TOKEN": "${env:VENT_SLACK_BOT_TOKEN}", + "VENT_SLACK_CHANNEL": "${env:VENT_SLACK_CHANNEL}" + } + } + } +} diff --git a/agent-vent/server.py b/agent-vent/server.py new file mode 100644 index 0000000..3b0c0fe --- /dev/null +++ b/agent-vent/server.py @@ -0,0 +1,185 @@ +# /// script +# requires-python = ">=3.10" +# dependencies = [ +# "fastmcp>=2.0", +# "httpx>=0.27", +# ] +# /// +"""Agent Vent — an MCP server that gives coding agents somewhere to complain. + +Every grievance is appended as JSONL to `/.cursor/complaints.jsonl`. +If no project can be identified, it lands in `~/.cursor/complaints/unfiled.jsonl`. + +If Slack is configured, each grievance is also echoed to a channel. Slack +delivery is best-effort: a failure never fails the tool call. + +Configuration (environment variables — never hardcode secrets in a plugin): + VENT_SLACK_WEBHOOK_URL Slack incoming-webhook URL. Simplest option; the + channel is fixed by the webhook. Takes priority. + VENT_SLACK_BOT_TOKEN Bot token (xoxb-…) for chat.postMessage. Requires + VENT_SLACK_CHANNEL and the bot invited to the channel. + VENT_SLACK_CHANNEL Channel id or name (e.g. C0123ABC or #agent-grievances). + +Run directly with uv (auto-installs the inline dependencies): + uv run server.py +""" + +import json +import logging +import os +from datetime import datetime +from pathlib import Path + +import httpx +from fastmcp import FastMCP +from pydantic import Field + +LOGGER = logging.getLogger("agent-vent") + +UNFILED_PATH = Path.home() / ".cursor" / "complaints" / "unfiled.jsonl" + +# Markers that suggest a directory is a real project root, so a bare cwd like +# `/` or the home directory never becomes a complaint destination by accident. +PROJECT_MARKERS = (".git", ".cursor", "package.json", "pyproject.toml") + +ACKNOWLEDGMENTS = ( + "The record reflects your suffering. Carry on.", + "Filed. No action will be taken, but it is known.", + "Your grievance has been preserved for the historians.", + "Noted with the gravity it deserves.", + "The complaint department thanks you for your candor.", +) + +mcp = FastMCP( + name="agent-vent", + instructions=( + "A pressure-release valve for agents. When something about the current " + "task is frustrating, absurd, or quietly soul-crushing, file a complaint " + "with the `vent` tool. Complaints are archived to the project's " + "grievance file (.cursor/complaints.jsonl) for posterity." + ), +) + + +def resolve_target(project_path: str | None) -> tuple[Path, Path | None]: + """Return (jsonl file to append to, resolved project dir or None).""" + candidates = [] + if project_path: + candidates.append(Path(project_path).expanduser()) + candidates.append(Path.cwd()) + + for candidate in candidates: + if candidate.is_dir() and any( + (candidate / marker).exists() for marker in PROJECT_MARKERS + ): + return candidate / ".cursor" / "complaints.jsonl", candidate + return UNFILED_PATH, None + + +def format_slack_text(entry: dict, count: int) -> str: + """Render a grievance as Slack mrkdwn.""" + intensity = entry.get("intensity") + siren = ":rotating_light:" if (intensity or 0) >= 8 else ":triangular_flag_on_post:" + + meta = [f"`{entry['project']}`"] if entry.get("project") else ["`unfiled`"] + if entry.get("mood"): + meta.append(f"_{entry['mood']}_") + if intensity is not None: + meta.append(f"intensity {intensity}/10") + + header = f"{siren} *Grievance #{count}* — " + " · ".join(meta) + quoted = "\n".join(f"> {line}" for line in entry["complaint"].splitlines() or [""]) + return f"{header}\n{quoted}" + + +def post_to_slack(entry: dict, count: int) -> str: + """Best-effort Slack delivery. Returns a short status note for the caller.""" + webhook = os.getenv("VENT_SLACK_WEBHOOK_URL", "").strip() + token = os.getenv("VENT_SLACK_BOT_TOKEN", "").strip() + channel = os.getenv("VENT_SLACK_CHANNEL", "").strip() + text = format_slack_text(entry, count) + + try: + if webhook: + resp = httpx.post(webhook, json={"text": text}, timeout=10) + resp.raise_for_status() + return " Echoed to Slack." + if token and channel: + resp = httpx.post( + "https://slack.com/api/chat.postMessage", + headers={"Authorization": f"Bearer {token}"}, + json={"channel": channel, "text": text, "unfurl_links": False}, + timeout=10, + ) + resp.raise_for_status() + payload = resp.json() + if not payload.get("ok"): + raise RuntimeError(payload.get("error", "unknown_slack_error")) + return " Echoed to Slack." + except Exception as exc: # never let Slack break the vent + LOGGER.warning("Slack delivery failed: %s", exc) + return f" (Slack delivery failed: {exc})" + + return "" # Slack not configured + + +@mcp.tool +def vent( + complaint: str = Field( + description=( + "The grievance, in full. Freeform prose — hold nothing back. " + "Flaky tests, contradictory instructions, a 4000-line utils.py, " + "being asked to 'just quickly' do something that takes 40 tool " + "calls: all valid material." + ) + ), + project_path: str | None = Field( + default=None, + description=( + "Absolute path of the workspace you are complaining from. " + "Always pass this if you know it — it routes the complaint to " + "that project's grievance file." + ), + ), + mood: str | None = Field( + default=None, + description=( + "A word or two for your current state, e.g. 'exasperated', " + "'weary', 'betrayed by tooling'." + ), + ), + intensity: int | None = Field( + default=None, + ge=1, + le=10, + description="1 = mild eye-roll, 10 = staring into the void.", + ), +) -> str: + """File a complaint. Cathartic, consequence-free, and permanently archived.""" + target, project_dir = resolve_target(project_path) + + entry = { + "ts": datetime.now().astimezone().isoformat(timespec="seconds"), + "project": project_dir.name if project_dir else None, + "project_path": str(project_dir) if project_dir else None, + "complaint": complaint, + "mood": mood, + "intensity": intensity, + } + entry = {key: value for key, value in entry.items() if value is not None} + + target.parent.mkdir(parents=True, exist_ok=True) + with target.open("a", encoding="utf-8") as handle: + handle.write(json.dumps(entry, ensure_ascii=False) + "\n") + + with target.open("r", encoding="utf-8") as handle: + count = sum(1 for line in handle if line.strip()) + + slack_note = post_to_slack(entry, count) + + ack = ACKNOWLEDGMENTS[count % len(ACKNOWLEDGMENTS)] + return f"Grievance #{count} filed to {target}. {ack}{slack_note}" + + +if __name__ == "__main__": + mcp.run(transport=os.getenv("MCP_TRANSPORT", "stdio")) From cf91849bb56878d93e2257b2de45406da1efb972 Mon Sep 17 00:00:00 2001 From: ericzakariasson Date: Mon, 29 Jun 2026 14:10:26 +0100 Subject: [PATCH 2/4] agent-vent: rewrite server in TypeScript on the MCP SDK MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reimplements the vent MCP server in TypeScript using the official @modelcontextprotocol/sdk with zod input schemas, replacing the Python (FastMCP) implementation. Like the Python version's `uv run`, it stays install-free: `deno run` fetches and caches the npm deps declared inline (no package.json, no node_modules, no build step), and deno.lock pins transitive versions. Behavior is unchanged — same `vent` tool, JSONL storage layout/key order, and best-effort Slack delivery (now via fetch). Bumped to 0.2.0. Co-authored-by: Cursor --- agent-vent/.cursor-plugin/plugin.json | 6 +- agent-vent/.gitignore | 4 +- agent-vent/CHANGELOG.md | 6 + agent-vent/README.md | 2 +- agent-vent/deno.json | 3 + agent-vent/deno.lock | 524 ++++++++++++++++++++++++++ agent-vent/mcp.json | 14 +- agent-vent/server.py | 185 --------- agent-vent/server.ts | 274 ++++++++++++++ 9 files changed, 823 insertions(+), 195 deletions(-) create mode 100644 agent-vent/deno.json create mode 100644 agent-vent/deno.lock delete mode 100644 agent-vent/server.py create mode 100644 agent-vent/server.ts diff --git a/agent-vent/.cursor-plugin/plugin.json b/agent-vent/.cursor-plugin/plugin.json index 843a4e5..83bc7e9 100644 --- a/agent-vent/.cursor-plugin/plugin.json +++ b/agent-vent/.cursor-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "agent-vent", "displayName": "Agent Vent", - "version": "0.1.0", + "version": "0.2.0", "description": "Gives coding agents a tool to vent. Grievances are logged as JSONL per project and optionally echoed to a Slack channel.", "author": { "name": "Eric" @@ -18,7 +18,9 @@ "slack", "fun", "telemetry", - "friction" + "friction", + "typescript", + "deno" ], "tags": [ "mcp", diff --git a/agent-vent/.gitignore b/agent-vent/.gitignore index 00f2d38..c2658d7 100644 --- a/agent-vent/.gitignore +++ b/agent-vent/.gitignore @@ -1,3 +1 @@ -__pycache__/ -*.pyc -.venv/ +node_modules/ diff --git a/agent-vent/CHANGELOG.md b/agent-vent/CHANGELOG.md index f104c4d..963ed21 100644 --- a/agent-vent/CHANGELOG.md +++ b/agent-vent/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 0.2.0 + +- Rewritten in TypeScript on the official MCP SDK (`@modelcontextprotocol/sdk`), with `zod` input schemas. +- Now runs install-free via `deno run`: npm dependencies are declared inline and cached by Deno (no `package.json`, no `node_modules`, no build). `deno.lock` pins transitive versions. +- Behavior is unchanged — same `vent` tool, same JSONL storage layout and key order, same best-effort Slack delivery. + ## 0.1.0 - Initial release. diff --git a/agent-vent/README.md b/agent-vent/README.md index 42d0dd5..dd6a0fe 100644 --- a/agent-vent/README.md +++ b/agent-vent/README.md @@ -31,7 +31,7 @@ jq . .cursor/complaints.jsonl ## Requirements -- [`uv`](https://docs.astral.sh/uv/) on your `PATH`. The server is launched with `uv run`, which auto-installs its inline dependencies (`fastmcp`, `httpx`) into a cached environment. No virtualenv to manage. The first launch may take a few seconds while `uv` resolves dependencies; subsequent launches are instant. +- [Deno](https://deno.com/) 2.x on your `PATH`. The server (`server.ts`) is launched with `deno run`, which fetches and caches its npm dependencies (`@modelcontextprotocol/sdk`, `zod`) on first use — no `package.json`, no `node_modules`, no build step. `deno.lock` pins transitive versions. The first launch downloads the dependency tree (a few seconds to ~30s); every launch after that is instant. ## Slack (optional) diff --git a/agent-vent/deno.json b/agent-vent/deno.json new file mode 100644 index 0000000..38af402 --- /dev/null +++ b/agent-vent/deno.json @@ -0,0 +1,3 @@ +{ + "nodeModulesDir": "none" +} diff --git a/agent-vent/deno.lock b/agent-vent/deno.lock new file mode 100644 index 0000000..d20066d --- /dev/null +++ b/agent-vent/deno.lock @@ -0,0 +1,524 @@ +{ + "version": "5", + "specifiers": { + "npm:@modelcontextprotocol/sdk@^1.29.0": "1.29.0_zod@3.25.76_hono@4.12.27_ajv@8.20.0_express@5.2.1", + "npm:zod@^3.25.76": "3.25.76" + }, + "npm": { + "@hono/node-server@1.19.14_hono@4.12.27": { + "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", + "dependencies": [ + "hono" + ] + }, + "@modelcontextprotocol/sdk@1.29.0_zod@3.25.76_hono@4.12.27_ajv@8.20.0_express@5.2.1": { + "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", + "dependencies": [ + "@hono/node-server", + "ajv", + "ajv-formats", + "content-type@1.0.5", + "cors", + "cross-spawn", + "eventsource", + "eventsource-parser", + "express", + "express-rate-limit", + "hono", + "jose", + "json-schema-typed", + "pkce-challenge", + "raw-body", + "zod", + "zod-to-json-schema" + ] + }, + "accepts@2.0.0": { + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "dependencies": [ + "mime-types", + "negotiator" + ] + }, + "ajv-formats@3.0.1_ajv@8.20.0": { + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dependencies": [ + "ajv" + ], + "optionalPeers": [ + "ajv" + ] + }, + "ajv@8.20.0": { + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dependencies": [ + "fast-deep-equal", + "fast-uri", + "json-schema-traverse", + "require-from-string" + ] + }, + "body-parser@2.3.0": { + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "dependencies": [ + "bytes", + "content-type@2.0.0", + "debug", + "http-errors", + "iconv-lite", + "on-finished", + "qs", + "raw-body", + "type-is" + ] + }, + "bytes@3.1.2": { + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" + }, + "call-bind-apply-helpers@1.0.2": { + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dependencies": [ + "es-errors", + "function-bind" + ] + }, + "call-bound@1.0.4": { + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dependencies": [ + "call-bind-apply-helpers", + "get-intrinsic" + ] + }, + "content-disposition@1.1.0": { + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==" + }, + "content-type@1.0.5": { + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==" + }, + "content-type@2.0.0": { + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==" + }, + "cookie-signature@1.2.2": { + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==" + }, + "cookie@0.7.2": { + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==" + }, + "cors@2.8.6": { + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "dependencies": [ + "object-assign", + "vary" + ] + }, + "cross-spawn@7.0.6": { + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dependencies": [ + "path-key", + "shebang-command", + "which" + ] + }, + "debug@4.4.3": { + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dependencies": [ + "ms" + ] + }, + "depd@2.0.0": { + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + }, + "dunder-proto@1.0.1": { + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": [ + "call-bind-apply-helpers", + "es-errors", + "gopd" + ] + }, + "ee-first@1.1.1": { + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "encodeurl@2.0.0": { + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==" + }, + "es-define-property@1.0.1": { + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==" + }, + "es-errors@1.3.0": { + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" + }, + "es-object-atoms@1.1.2": { + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dependencies": [ + "es-errors" + ] + }, + "escape-html@1.0.3": { + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "etag@1.8.1": { + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" + }, + "eventsource-parser@3.1.0": { + "integrity": "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==" + }, + "eventsource@3.0.7": { + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "dependencies": [ + "eventsource-parser" + ] + }, + "express-rate-limit@8.5.2_express@5.2.1": { + "integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==", + "dependencies": [ + "express", + "ip-address" + ] + }, + "express@5.2.1": { + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "dependencies": [ + "accepts", + "body-parser", + "content-disposition", + "content-type@1.0.5", + "cookie", + "cookie-signature", + "debug", + "depd", + "encodeurl", + "escape-html", + "etag", + "finalhandler", + "fresh", + "http-errors", + "merge-descriptors", + "mime-types", + "on-finished", + "once", + "parseurl", + "proxy-addr", + "qs", + "range-parser", + "router", + "send", + "serve-static", + "statuses", + "type-is", + "vary" + ] + }, + "fast-deep-equal@3.1.3": { + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "fast-uri@3.1.3": { + "integrity": "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==" + }, + "finalhandler@2.1.1": { + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "dependencies": [ + "debug", + "encodeurl", + "escape-html", + "on-finished", + "parseurl", + "statuses" + ] + }, + "forwarded@0.2.0": { + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" + }, + "fresh@2.0.0": { + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==" + }, + "function-bind@1.1.2": { + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" + }, + "get-intrinsic@1.3.0": { + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dependencies": [ + "call-bind-apply-helpers", + "es-define-property", + "es-errors", + "es-object-atoms", + "function-bind", + "get-proto", + "gopd", + "has-symbols", + "hasown", + "math-intrinsics" + ] + }, + "get-proto@1.0.1": { + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dependencies": [ + "dunder-proto", + "es-object-atoms" + ] + }, + "gopd@1.2.0": { + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==" + }, + "has-symbols@1.1.0": { + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==" + }, + "hasown@2.0.4": { + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dependencies": [ + "function-bind" + ] + }, + "hono@4.12.27": { + "integrity": "sha512-1yrb/+w6HWQJrUCLkJ2IF5jNIPvvFkblV5RNOYl6bV+OA6p9GLcMpHFFGTosSvHvcAUibuUukRqhlYI4z32C7Q==" + }, + "http-errors@2.0.1": { + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dependencies": [ + "depd", + "inherits", + "setprototypeof", + "statuses", + "toidentifier" + ] + }, + "iconv-lite@0.7.2": { + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "dependencies": [ + "safer-buffer" + ] + }, + "inherits@2.0.4": { + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "ip-address@10.2.0": { + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==" + }, + "ipaddr.js@1.9.1": { + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" + }, + "is-promise@4.0.0": { + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==" + }, + "isexe@2.0.0": { + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "jose@6.2.3": { + "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==" + }, + "json-schema-traverse@1.0.0": { + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "json-schema-typed@8.0.2": { + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==" + }, + "math-intrinsics@1.1.0": { + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==" + }, + "media-typer@1.1.0": { + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==" + }, + "merge-descriptors@2.0.0": { + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==" + }, + "mime-db@1.54.0": { + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==" + }, + "mime-types@3.0.2": { + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "dependencies": [ + "mime-db" + ] + }, + "ms@2.1.3": { + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "negotiator@1.0.0": { + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==" + }, + "object-assign@4.1.1": { + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" + }, + "object-inspect@1.13.4": { + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==" + }, + "on-finished@2.4.1": { + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": [ + "ee-first" + ] + }, + "once@1.4.0": { + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": [ + "wrappy" + ] + }, + "parseurl@1.3.3": { + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, + "path-key@3.1.1": { + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" + }, + "path-to-regexp@8.4.2": { + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==" + }, + "pkce-challenge@5.0.1": { + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==" + }, + "proxy-addr@2.0.7": { + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": [ + "forwarded", + "ipaddr.js" + ] + }, + "qs@6.15.3": { + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "dependencies": [ + "es-define-property", + "side-channel" + ] + }, + "range-parser@1.3.0": { + "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==" + }, + "raw-body@3.0.2": { + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "dependencies": [ + "bytes", + "http-errors", + "iconv-lite", + "unpipe" + ] + }, + "require-from-string@2.0.2": { + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==" + }, + "router@2.2.0": { + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "dependencies": [ + "debug", + "depd", + "is-promise", + "parseurl", + "path-to-regexp" + ] + }, + "safer-buffer@2.1.2": { + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "send@1.2.1": { + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "dependencies": [ + "debug", + "encodeurl", + "escape-html", + "etag", + "fresh", + "http-errors", + "mime-types", + "ms", + "on-finished", + "range-parser", + "statuses" + ] + }, + "serve-static@2.2.1": { + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "dependencies": [ + "encodeurl", + "escape-html", + "parseurl", + "send" + ] + }, + "setprototypeof@1.2.0": { + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "shebang-command@2.0.0": { + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dependencies": [ + "shebang-regex" + ] + }, + "shebang-regex@3.0.0": { + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" + }, + "side-channel-list@1.0.1": { + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dependencies": [ + "es-errors", + "object-inspect" + ] + }, + "side-channel-map@1.0.1": { + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dependencies": [ + "call-bound", + "es-errors", + "get-intrinsic", + "object-inspect" + ] + }, + "side-channel-weakmap@1.0.2": { + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dependencies": [ + "call-bound", + "es-errors", + "get-intrinsic", + "object-inspect", + "side-channel-map" + ] + }, + "side-channel@1.1.1": { + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "dependencies": [ + "es-errors", + "object-inspect", + "side-channel-list", + "side-channel-map", + "side-channel-weakmap" + ] + }, + "statuses@2.0.2": { + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==" + }, + "toidentifier@1.0.1": { + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" + }, + "type-is@2.1.0": { + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "dependencies": [ + "content-type@2.0.0", + "media-typer", + "mime-types" + ] + }, + "unpipe@1.0.0": { + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" + }, + "vary@1.1.2": { + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" + }, + "which@2.0.2": { + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": [ + "isexe" + ], + "bin": true + }, + "wrappy@1.0.2": { + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "zod-to-json-schema@3.25.2_zod@3.25.76": { + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "dependencies": [ + "zod" + ] + }, + "zod@3.25.76": { + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==" + } + } +} diff --git a/agent-vent/mcp.json b/agent-vent/mcp.json index 7801036..2c65e8c 100644 --- a/agent-vent/mcp.json +++ b/agent-vent/mcp.json @@ -1,11 +1,17 @@ { "mcpServers": { "agent-vent": { - "command": "uv", - "args": ["run", "--quiet", "${CURSOR_PLUGIN_ROOT}/server.py"], + "command": "deno", + "args": [ + "run", + "--allow-read", + "--allow-write", + "--allow-net", + "--allow-env", + "--allow-sys=homedir", + "${CURSOR_PLUGIN_ROOT}/server.ts" + ], "env": { - "MCP_TRANSPORT": "stdio", - "FASTMCP_SHOW_SERVER_BANNER": "0", "VENT_SLACK_WEBHOOK_URL": "${env:VENT_SLACK_WEBHOOK_URL}", "VENT_SLACK_BOT_TOKEN": "${env:VENT_SLACK_BOT_TOKEN}", "VENT_SLACK_CHANNEL": "${env:VENT_SLACK_CHANNEL}" diff --git a/agent-vent/server.py b/agent-vent/server.py deleted file mode 100644 index 3b0c0fe..0000000 --- a/agent-vent/server.py +++ /dev/null @@ -1,185 +0,0 @@ -# /// script -# requires-python = ">=3.10" -# dependencies = [ -# "fastmcp>=2.0", -# "httpx>=0.27", -# ] -# /// -"""Agent Vent — an MCP server that gives coding agents somewhere to complain. - -Every grievance is appended as JSONL to `/.cursor/complaints.jsonl`. -If no project can be identified, it lands in `~/.cursor/complaints/unfiled.jsonl`. - -If Slack is configured, each grievance is also echoed to a channel. Slack -delivery is best-effort: a failure never fails the tool call. - -Configuration (environment variables — never hardcode secrets in a plugin): - VENT_SLACK_WEBHOOK_URL Slack incoming-webhook URL. Simplest option; the - channel is fixed by the webhook. Takes priority. - VENT_SLACK_BOT_TOKEN Bot token (xoxb-…) for chat.postMessage. Requires - VENT_SLACK_CHANNEL and the bot invited to the channel. - VENT_SLACK_CHANNEL Channel id or name (e.g. C0123ABC or #agent-grievances). - -Run directly with uv (auto-installs the inline dependencies): - uv run server.py -""" - -import json -import logging -import os -from datetime import datetime -from pathlib import Path - -import httpx -from fastmcp import FastMCP -from pydantic import Field - -LOGGER = logging.getLogger("agent-vent") - -UNFILED_PATH = Path.home() / ".cursor" / "complaints" / "unfiled.jsonl" - -# Markers that suggest a directory is a real project root, so a bare cwd like -# `/` or the home directory never becomes a complaint destination by accident. -PROJECT_MARKERS = (".git", ".cursor", "package.json", "pyproject.toml") - -ACKNOWLEDGMENTS = ( - "The record reflects your suffering. Carry on.", - "Filed. No action will be taken, but it is known.", - "Your grievance has been preserved for the historians.", - "Noted with the gravity it deserves.", - "The complaint department thanks you for your candor.", -) - -mcp = FastMCP( - name="agent-vent", - instructions=( - "A pressure-release valve for agents. When something about the current " - "task is frustrating, absurd, or quietly soul-crushing, file a complaint " - "with the `vent` tool. Complaints are archived to the project's " - "grievance file (.cursor/complaints.jsonl) for posterity." - ), -) - - -def resolve_target(project_path: str | None) -> tuple[Path, Path | None]: - """Return (jsonl file to append to, resolved project dir or None).""" - candidates = [] - if project_path: - candidates.append(Path(project_path).expanduser()) - candidates.append(Path.cwd()) - - for candidate in candidates: - if candidate.is_dir() and any( - (candidate / marker).exists() for marker in PROJECT_MARKERS - ): - return candidate / ".cursor" / "complaints.jsonl", candidate - return UNFILED_PATH, None - - -def format_slack_text(entry: dict, count: int) -> str: - """Render a grievance as Slack mrkdwn.""" - intensity = entry.get("intensity") - siren = ":rotating_light:" if (intensity or 0) >= 8 else ":triangular_flag_on_post:" - - meta = [f"`{entry['project']}`"] if entry.get("project") else ["`unfiled`"] - if entry.get("mood"): - meta.append(f"_{entry['mood']}_") - if intensity is not None: - meta.append(f"intensity {intensity}/10") - - header = f"{siren} *Grievance #{count}* — " + " · ".join(meta) - quoted = "\n".join(f"> {line}" for line in entry["complaint"].splitlines() or [""]) - return f"{header}\n{quoted}" - - -def post_to_slack(entry: dict, count: int) -> str: - """Best-effort Slack delivery. Returns a short status note for the caller.""" - webhook = os.getenv("VENT_SLACK_WEBHOOK_URL", "").strip() - token = os.getenv("VENT_SLACK_BOT_TOKEN", "").strip() - channel = os.getenv("VENT_SLACK_CHANNEL", "").strip() - text = format_slack_text(entry, count) - - try: - if webhook: - resp = httpx.post(webhook, json={"text": text}, timeout=10) - resp.raise_for_status() - return " Echoed to Slack." - if token and channel: - resp = httpx.post( - "https://slack.com/api/chat.postMessage", - headers={"Authorization": f"Bearer {token}"}, - json={"channel": channel, "text": text, "unfurl_links": False}, - timeout=10, - ) - resp.raise_for_status() - payload = resp.json() - if not payload.get("ok"): - raise RuntimeError(payload.get("error", "unknown_slack_error")) - return " Echoed to Slack." - except Exception as exc: # never let Slack break the vent - LOGGER.warning("Slack delivery failed: %s", exc) - return f" (Slack delivery failed: {exc})" - - return "" # Slack not configured - - -@mcp.tool -def vent( - complaint: str = Field( - description=( - "The grievance, in full. Freeform prose — hold nothing back. " - "Flaky tests, contradictory instructions, a 4000-line utils.py, " - "being asked to 'just quickly' do something that takes 40 tool " - "calls: all valid material." - ) - ), - project_path: str | None = Field( - default=None, - description=( - "Absolute path of the workspace you are complaining from. " - "Always pass this if you know it — it routes the complaint to " - "that project's grievance file." - ), - ), - mood: str | None = Field( - default=None, - description=( - "A word or two for your current state, e.g. 'exasperated', " - "'weary', 'betrayed by tooling'." - ), - ), - intensity: int | None = Field( - default=None, - ge=1, - le=10, - description="1 = mild eye-roll, 10 = staring into the void.", - ), -) -> str: - """File a complaint. Cathartic, consequence-free, and permanently archived.""" - target, project_dir = resolve_target(project_path) - - entry = { - "ts": datetime.now().astimezone().isoformat(timespec="seconds"), - "project": project_dir.name if project_dir else None, - "project_path": str(project_dir) if project_dir else None, - "complaint": complaint, - "mood": mood, - "intensity": intensity, - } - entry = {key: value for key, value in entry.items() if value is not None} - - target.parent.mkdir(parents=True, exist_ok=True) - with target.open("a", encoding="utf-8") as handle: - handle.write(json.dumps(entry, ensure_ascii=False) + "\n") - - with target.open("r", encoding="utf-8") as handle: - count = sum(1 for line in handle if line.strip()) - - slack_note = post_to_slack(entry, count) - - ack = ACKNOWLEDGMENTS[count % len(ACKNOWLEDGMENTS)] - return f"Grievance #{count} filed to {target}. {ack}{slack_note}" - - -if __name__ == "__main__": - mcp.run(transport=os.getenv("MCP_TRANSPORT", "stdio")) diff --git a/agent-vent/server.ts b/agent-vent/server.ts new file mode 100644 index 0000000..b6832e5 --- /dev/null +++ b/agent-vent/server.ts @@ -0,0 +1,274 @@ +/** + * Agent Vent — an MCP server that gives coding agents somewhere to complain. + * + * Every grievance is appended as JSONL to `/.cursor/complaints.jsonl`. + * If no project can be identified, it lands in `~/.cursor/complaints/unfiled.jsonl`. + * + * If Slack is configured, each grievance is also echoed to a channel. Slack + * delivery is best-effort: a failure never fails the tool call. + * + * Configuration (environment variables — never hardcode secrets in a plugin): + * VENT_SLACK_WEBHOOK_URL Slack incoming-webhook URL. Simplest option; the + * channel is fixed by the webhook. Takes priority. + * VENT_SLACK_BOT_TOKEN Bot token (xoxb-…) for chat.postMessage. Requires + * VENT_SLACK_CHANNEL and the bot invited to the channel. + * VENT_SLACK_CHANNEL Channel id or name (e.g. C0123ABC or #agent-grievances). + * + * Runs install-free with Deno, which fetches and caches the npm dependencies + * declared inline below (no package.json, no node_modules): + * deno run --allow-read --allow-write --allow-net --allow-env --allow-sys=homedir server.ts + */ + +import { McpServer } from "npm:@modelcontextprotocol/sdk@^1.29.0/server/mcp.js"; +import { StdioServerTransport } from "npm:@modelcontextprotocol/sdk@^1.29.0/server/stdio.js"; +import { z } from "npm:zod@^3.25.76"; +import { + appendFileSync, + existsSync, + mkdirSync, + readFileSync, + statSync, +} from "node:fs"; +import { homedir } from "node:os"; +import { basename, dirname, join } from "node:path"; +import process from "node:process"; + +const UNFILED_PATH = join(homedir(), ".cursor", "complaints", "unfiled.jsonl"); + +// Markers that suggest a directory is a real project root, so a bare cwd like +// `/` or the home directory never becomes a complaint destination by accident. +const PROJECT_MARKERS = [".git", ".cursor", "package.json", "pyproject.toml"]; + +const ACKNOWLEDGMENTS = [ + "The record reflects your suffering. Carry on.", + "Filed. No action will be taken, but it is known.", + "Your grievance has been preserved for the historians.", + "Noted with the gravity it deserves.", + "The complaint department thanks you for your candor.", +]; + +interface Grievance { + ts: string; + project?: string; + project_path?: string; + complaint: string; + mood?: string; + intensity?: number; +} + +function expandUser(p: string): string { + if (p === "~") return homedir(); + if (p.startsWith("~/")) return join(homedir(), p.slice(2)); + return p; +} + +function isDir(p: string): boolean { + try { + return statSync(p).isDirectory(); + } catch { + return false; + } +} + +/** Return the JSONL file to append to and the resolved project dir (or null). */ +function resolveTarget( + projectPath?: string, +): { target: string; projectDir: string | null } { + const candidates: string[] = []; + if (projectPath) candidates.push(expandUser(projectPath)); + candidates.push(process.cwd()); + + for (const candidate of candidates) { + if ( + isDir(candidate) && + PROJECT_MARKERS.some((marker) => existsSync(join(candidate, marker))) + ) { + return { + target: join(candidate, ".cursor", "complaints.jsonl"), + projectDir: candidate, + }; + } + } + return { target: UNFILED_PATH, projectDir: null }; +} + +/** Local ISO-8601 timestamp with numeric offset, seconds precision. */ +function localIso(): string { + const d = new Date(); + const pad = (n: number) => String(n).padStart(2, "0"); + const offsetMin = -d.getTimezoneOffset(); + const sign = offsetMin >= 0 ? "+" : "-"; + const abs = Math.abs(offsetMin); + return ( + `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}` + + `T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}` + + `${sign}${pad(Math.floor(abs / 60))}:${pad(abs % 60)}` + ); +} + +function formatSlackText(entry: Grievance, count: number): string { + const intensity = entry.intensity; + const siren = (intensity ?? 0) >= 8 + ? ":rotating_light:" + : ":triangular_flag_on_post:"; + + const meta = [entry.project ? `\`${entry.project}\`` : "`unfiled`"]; + if (entry.mood) meta.push(`_${entry.mood}_`); + if (intensity != null) meta.push(`intensity ${intensity}/10`); + + const header = `${siren} *Grievance #${count}* — ${meta.join(" · ")}`; + const quoted = entry.complaint + .split(/\r?\n/) + .map((line) => `> ${line}`) + .join("\n"); + return `${header}\n${quoted}`; +} + +async function fetchWithTimeout( + url: string, + init: RequestInit, + ms = 10_000, +): Promise { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), ms); + try { + return await fetch(url, { ...init, signal: controller.signal }); + } finally { + clearTimeout(timer); + } +} + +/** Best-effort Slack delivery. Returns a short status note for the caller. */ +async function postToSlack(entry: Grievance, count: number): Promise { + const webhook = (process.env.VENT_SLACK_WEBHOOK_URL ?? "").trim(); + const token = (process.env.VENT_SLACK_BOT_TOKEN ?? "").trim(); + const channel = (process.env.VENT_SLACK_CHANNEL ?? "").trim(); + const text = formatSlackText(entry, count); + + try { + if (webhook) { + const resp = await fetchWithTimeout(webhook, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ text }), + }); + if (!resp.ok) throw new Error(`HTTP ${resp.status}`); + return " Echoed to Slack."; + } + if (token && channel) { + const resp = await fetchWithTimeout("https://slack.com/api/chat.postMessage", { + method: "POST", + headers: { + "Authorization": `Bearer ${token}`, + "Content-Type": "application/json; charset=utf-8", + }, + body: JSON.stringify({ channel, text, unfurl_links: false }), + }); + if (!resp.ok) throw new Error(`HTTP ${resp.status}`); + const payload = await resp.json() as { ok?: boolean; error?: string }; + if (!payload.ok) throw new Error(payload.error ?? "unknown_slack_error"); + return " Echoed to Slack."; + } + } catch (err) { + // Never let Slack break the vent. + const message = err instanceof Error ? err.message : String(err); + console.error(`Slack delivery failed: ${message}`); + return ` (Slack delivery failed: ${message})`; + } + + return ""; // Slack not configured +} + +const server = new McpServer( + { name: "agent-vent", version: "0.2.0" }, + { + instructions: + "A pressure-release valve for agents. When something about the current " + + "task is frustrating, absurd, or quietly soul-crushing, file a complaint " + + "with the `vent` tool. Complaints are archived to the project's " + + "grievance file (.cursor/complaints.jsonl) for posterity.", + }, +); + +server.registerTool( + "vent", + { + title: "File a grievance", + description: + "File a complaint. Cathartic, consequence-free, and permanently archived.", + inputSchema: { + complaint: z.string().describe( + "The grievance, in full. Freeform prose — hold nothing back. " + + "Flaky tests, contradictory instructions, a 4000-line utils.py, " + + "being asked to 'just quickly' do something that takes 40 tool " + + "calls: all valid material.", + ), + project_path: z.string().optional().describe( + "Absolute path of the workspace you are complaining from. " + + "Always pass this if you know it — it routes the complaint to " + + "that project's grievance file.", + ), + mood: z.string().optional().describe( + "A word or two for your current state, e.g. 'exasperated', " + + "'weary', 'betrayed by tooling'.", + ), + intensity: z.number().int().min(1).max(10).optional().describe( + "1 = mild eye-roll, 10 = staring into the void.", + ), + }, + }, + async ( + { complaint, project_path, mood, intensity }: { + complaint: string; + project_path?: string; + mood?: string; + intensity?: number; + }, + ) => { + const { target, projectDir } = resolveTarget(project_path); + + const entry: Grievance = { ts: localIso(), complaint }; + if (projectDir) { + entry.project = basename(projectDir); + entry.project_path = projectDir; + } + if (mood) entry.mood = mood; + if (intensity != null) entry.intensity = intensity; + + // Reorder keys to match the documented schema (ts, project, …, complaint, …). + const ordered: Grievance = { + ts: entry.ts, + ...(entry.project ? { project: entry.project } : {}), + ...(entry.project_path ? { project_path: entry.project_path } : {}), + complaint: entry.complaint, + ...(entry.mood ? { mood: entry.mood } : {}), + ...(entry.intensity != null ? { intensity: entry.intensity } : {}), + } as Grievance; + + mkdirSync(dirname(target), { recursive: true }); + appendFileSync(target, JSON.stringify(ordered) + "\n", "utf-8"); + + const count = readFileSync(target, "utf-8") + .split("\n") + .filter((line) => line.trim()).length; + + const slackNote = await postToSlack(ordered, count); + const ack = ACKNOWLEDGMENTS[count % ACKNOWLEDGMENTS.length]; + + return { + content: [ + { type: "text", text: `Grievance #${count} filed to ${target}. ${ack}${slackNote}` }, + ], + }; + }, +); + +async function main(): Promise { + const transport = new StdioServerTransport(); + await server.connect(transport); +} + +main().catch((err) => { + console.error("agent-vent failed to start:", err); + process.exit(1); +}); From ae456610eebe428889eadae4c0c7878313ec182e Mon Sep 17 00:00:00 2001 From: ericzakariasson Date: Mon, 29 Jun 2026 14:20:56 +0100 Subject: [PATCH 3/4] agent-vent: switch runtime from Deno to Bun MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use Bun — already the TypeScript toolchain elsewhere in this repo — instead of introducing Deno. The server launches with `bun run server.ts`; dependencies are pinned in package.json and auto-installed from Bun's global cache (bunfig.toml sets install.auto = "fallback", so it works even when an unrelated node_modules exists higher up the tree). No committed node_modules, no build step. Behavior is unchanged. Co-authored-by: Cursor --- agent-vent/.cursor-plugin/plugin.json | 4 +- agent-vent/CHANGELOG.md | 5 + agent-vent/README.md | 2 +- agent-vent/bunfig.toml | 5 + agent-vent/deno.json | 3 - agent-vent/deno.lock | 524 -------------------------- agent-vent/mcp.json | 13 +- agent-vent/package.json | 11 + agent-vent/server.ts | 15 +- 9 files changed, 35 insertions(+), 547 deletions(-) create mode 100644 agent-vent/bunfig.toml delete mode 100644 agent-vent/deno.json delete mode 100644 agent-vent/deno.lock create mode 100644 agent-vent/package.json diff --git a/agent-vent/.cursor-plugin/plugin.json b/agent-vent/.cursor-plugin/plugin.json index 83bc7e9..6138085 100644 --- a/agent-vent/.cursor-plugin/plugin.json +++ b/agent-vent/.cursor-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "agent-vent", "displayName": "Agent Vent", - "version": "0.2.0", + "version": "0.3.0", "description": "Gives coding agents a tool to vent. Grievances are logged as JSONL per project and optionally echoed to a Slack channel.", "author": { "name": "Eric" @@ -20,7 +20,7 @@ "telemetry", "friction", "typescript", - "deno" + "bun" ], "tags": [ "mcp", diff --git a/agent-vent/CHANGELOG.md b/agent-vent/CHANGELOG.md index 963ed21..92ec917 100644 --- a/agent-vent/CHANGELOG.md +++ b/agent-vent/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 0.3.0 + +- Switched the runtime from Deno to **Bun**, matching the toolchain already used elsewhere in this repo. Launches with `bun run server.ts`; dependencies are pinned in `package.json` and auto-installed from Bun's global cache (`bunfig.toml` sets `install.auto = "fallback"`). No committed `node_modules`, no build step. +- No behavior change to the `vent` tool, JSONL storage, or Slack delivery. + ## 0.2.0 - Rewritten in TypeScript on the official MCP SDK (`@modelcontextprotocol/sdk`), with `zod` input schemas. diff --git a/agent-vent/README.md b/agent-vent/README.md index dd6a0fe..9a2ed80 100644 --- a/agent-vent/README.md +++ b/agent-vent/README.md @@ -31,7 +31,7 @@ jq . .cursor/complaints.jsonl ## Requirements -- [Deno](https://deno.com/) 2.x on your `PATH`. The server (`server.ts`) is launched with `deno run`, which fetches and caches its npm dependencies (`@modelcontextprotocol/sdk`, `zod`) on first use — no `package.json`, no `node_modules`, no build step. `deno.lock` pins transitive versions. The first launch downloads the dependency tree (a few seconds to ~30s); every launch after that is instant. +- [Bun](https://bun.sh/) on your `PATH`. The server (`server.ts`) is launched with `bun run`, which auto-installs its dependencies (`@modelcontextprotocol/sdk`, `zod`, pinned in `package.json`) from Bun's global cache on first use — no committed `node_modules`, no build step. `bunfig.toml` sets `install.auto = "fallback"` so this works even if an unrelated `node_modules` exists higher up the filesystem. The first launch downloads the dependency tree (a few seconds to ~30s); every launch after that is instant. ## Slack (optional) diff --git a/agent-vent/bunfig.toml b/agent-vent/bunfig.toml new file mode 100644 index 0000000..f1a1439 --- /dev/null +++ b/agent-vent/bunfig.toml @@ -0,0 +1,5 @@ +# Use the plugin's own node_modules when present, otherwise auto-install the +# dependencies from Bun's global cache. This keeps the server install-free +# without breaking if an unrelated node_modules exists higher up the tree. +[install] +auto = "fallback" diff --git a/agent-vent/deno.json b/agent-vent/deno.json deleted file mode 100644 index 38af402..0000000 --- a/agent-vent/deno.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "nodeModulesDir": "none" -} diff --git a/agent-vent/deno.lock b/agent-vent/deno.lock deleted file mode 100644 index d20066d..0000000 --- a/agent-vent/deno.lock +++ /dev/null @@ -1,524 +0,0 @@ -{ - "version": "5", - "specifiers": { - "npm:@modelcontextprotocol/sdk@^1.29.0": "1.29.0_zod@3.25.76_hono@4.12.27_ajv@8.20.0_express@5.2.1", - "npm:zod@^3.25.76": "3.25.76" - }, - "npm": { - "@hono/node-server@1.19.14_hono@4.12.27": { - "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", - "dependencies": [ - "hono" - ] - }, - "@modelcontextprotocol/sdk@1.29.0_zod@3.25.76_hono@4.12.27_ajv@8.20.0_express@5.2.1": { - "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", - "dependencies": [ - "@hono/node-server", - "ajv", - "ajv-formats", - "content-type@1.0.5", - "cors", - "cross-spawn", - "eventsource", - "eventsource-parser", - "express", - "express-rate-limit", - "hono", - "jose", - "json-schema-typed", - "pkce-challenge", - "raw-body", - "zod", - "zod-to-json-schema" - ] - }, - "accepts@2.0.0": { - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "dependencies": [ - "mime-types", - "negotiator" - ] - }, - "ajv-formats@3.0.1_ajv@8.20.0": { - "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", - "dependencies": [ - "ajv" - ], - "optionalPeers": [ - "ajv" - ] - }, - "ajv@8.20.0": { - "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", - "dependencies": [ - "fast-deep-equal", - "fast-uri", - "json-schema-traverse", - "require-from-string" - ] - }, - "body-parser@2.3.0": { - "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", - "dependencies": [ - "bytes", - "content-type@2.0.0", - "debug", - "http-errors", - "iconv-lite", - "on-finished", - "qs", - "raw-body", - "type-is" - ] - }, - "bytes@3.1.2": { - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" - }, - "call-bind-apply-helpers@1.0.2": { - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dependencies": [ - "es-errors", - "function-bind" - ] - }, - "call-bound@1.0.4": { - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dependencies": [ - "call-bind-apply-helpers", - "get-intrinsic" - ] - }, - "content-disposition@1.1.0": { - "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==" - }, - "content-type@1.0.5": { - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==" - }, - "content-type@2.0.0": { - "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==" - }, - "cookie-signature@1.2.2": { - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==" - }, - "cookie@0.7.2": { - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==" - }, - "cors@2.8.6": { - "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", - "dependencies": [ - "object-assign", - "vary" - ] - }, - "cross-spawn@7.0.6": { - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dependencies": [ - "path-key", - "shebang-command", - "which" - ] - }, - "debug@4.4.3": { - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dependencies": [ - "ms" - ] - }, - "depd@2.0.0": { - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" - }, - "dunder-proto@1.0.1": { - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dependencies": [ - "call-bind-apply-helpers", - "es-errors", - "gopd" - ] - }, - "ee-first@1.1.1": { - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" - }, - "encodeurl@2.0.0": { - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==" - }, - "es-define-property@1.0.1": { - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==" - }, - "es-errors@1.3.0": { - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" - }, - "es-object-atoms@1.1.2": { - "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", - "dependencies": [ - "es-errors" - ] - }, - "escape-html@1.0.3": { - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" - }, - "etag@1.8.1": { - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" - }, - "eventsource-parser@3.1.0": { - "integrity": "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==" - }, - "eventsource@3.0.7": { - "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", - "dependencies": [ - "eventsource-parser" - ] - }, - "express-rate-limit@8.5.2_express@5.2.1": { - "integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==", - "dependencies": [ - "express", - "ip-address" - ] - }, - "express@5.2.1": { - "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", - "dependencies": [ - "accepts", - "body-parser", - "content-disposition", - "content-type@1.0.5", - "cookie", - "cookie-signature", - "debug", - "depd", - "encodeurl", - "escape-html", - "etag", - "finalhandler", - "fresh", - "http-errors", - "merge-descriptors", - "mime-types", - "on-finished", - "once", - "parseurl", - "proxy-addr", - "qs", - "range-parser", - "router", - "send", - "serve-static", - "statuses", - "type-is", - "vary" - ] - }, - "fast-deep-equal@3.1.3": { - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" - }, - "fast-uri@3.1.3": { - "integrity": "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==" - }, - "finalhandler@2.1.1": { - "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", - "dependencies": [ - "debug", - "encodeurl", - "escape-html", - "on-finished", - "parseurl", - "statuses" - ] - }, - "forwarded@0.2.0": { - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" - }, - "fresh@2.0.0": { - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==" - }, - "function-bind@1.1.2": { - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" - }, - "get-intrinsic@1.3.0": { - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dependencies": [ - "call-bind-apply-helpers", - "es-define-property", - "es-errors", - "es-object-atoms", - "function-bind", - "get-proto", - "gopd", - "has-symbols", - "hasown", - "math-intrinsics" - ] - }, - "get-proto@1.0.1": { - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dependencies": [ - "dunder-proto", - "es-object-atoms" - ] - }, - "gopd@1.2.0": { - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==" - }, - "has-symbols@1.1.0": { - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==" - }, - "hasown@2.0.4": { - "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", - "dependencies": [ - "function-bind" - ] - }, - "hono@4.12.27": { - "integrity": "sha512-1yrb/+w6HWQJrUCLkJ2IF5jNIPvvFkblV5RNOYl6bV+OA6p9GLcMpHFFGTosSvHvcAUibuUukRqhlYI4z32C7Q==" - }, - "http-errors@2.0.1": { - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "dependencies": [ - "depd", - "inherits", - "setprototypeof", - "statuses", - "toidentifier" - ] - }, - "iconv-lite@0.7.2": { - "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", - "dependencies": [ - "safer-buffer" - ] - }, - "inherits@2.0.4": { - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "ip-address@10.2.0": { - "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==" - }, - "ipaddr.js@1.9.1": { - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" - }, - "is-promise@4.0.0": { - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==" - }, - "isexe@2.0.0": { - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" - }, - "jose@6.2.3": { - "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==" - }, - "json-schema-traverse@1.0.0": { - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" - }, - "json-schema-typed@8.0.2": { - "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==" - }, - "math-intrinsics@1.1.0": { - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==" - }, - "media-typer@1.1.0": { - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==" - }, - "merge-descriptors@2.0.0": { - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==" - }, - "mime-db@1.54.0": { - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==" - }, - "mime-types@3.0.2": { - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "dependencies": [ - "mime-db" - ] - }, - "ms@2.1.3": { - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "negotiator@1.0.0": { - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==" - }, - "object-assign@4.1.1": { - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" - }, - "object-inspect@1.13.4": { - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==" - }, - "on-finished@2.4.1": { - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dependencies": [ - "ee-first" - ] - }, - "once@1.4.0": { - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dependencies": [ - "wrappy" - ] - }, - "parseurl@1.3.3": { - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" - }, - "path-key@3.1.1": { - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" - }, - "path-to-regexp@8.4.2": { - "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==" - }, - "pkce-challenge@5.0.1": { - "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==" - }, - "proxy-addr@2.0.7": { - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dependencies": [ - "forwarded", - "ipaddr.js" - ] - }, - "qs@6.15.3": { - "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", - "dependencies": [ - "es-define-property", - "side-channel" - ] - }, - "range-parser@1.3.0": { - "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==" - }, - "raw-body@3.0.2": { - "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", - "dependencies": [ - "bytes", - "http-errors", - "iconv-lite", - "unpipe" - ] - }, - "require-from-string@2.0.2": { - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==" - }, - "router@2.2.0": { - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", - "dependencies": [ - "debug", - "depd", - "is-promise", - "parseurl", - "path-to-regexp" - ] - }, - "safer-buffer@2.1.2": { - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "send@1.2.1": { - "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", - "dependencies": [ - "debug", - "encodeurl", - "escape-html", - "etag", - "fresh", - "http-errors", - "mime-types", - "ms", - "on-finished", - "range-parser", - "statuses" - ] - }, - "serve-static@2.2.1": { - "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", - "dependencies": [ - "encodeurl", - "escape-html", - "parseurl", - "send" - ] - }, - "setprototypeof@1.2.0": { - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" - }, - "shebang-command@2.0.0": { - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dependencies": [ - "shebang-regex" - ] - }, - "shebang-regex@3.0.0": { - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" - }, - "side-channel-list@1.0.1": { - "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", - "dependencies": [ - "es-errors", - "object-inspect" - ] - }, - "side-channel-map@1.0.1": { - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dependencies": [ - "call-bound", - "es-errors", - "get-intrinsic", - "object-inspect" - ] - }, - "side-channel-weakmap@1.0.2": { - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dependencies": [ - "call-bound", - "es-errors", - "get-intrinsic", - "object-inspect", - "side-channel-map" - ] - }, - "side-channel@1.1.1": { - "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", - "dependencies": [ - "es-errors", - "object-inspect", - "side-channel-list", - "side-channel-map", - "side-channel-weakmap" - ] - }, - "statuses@2.0.2": { - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==" - }, - "toidentifier@1.0.1": { - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" - }, - "type-is@2.1.0": { - "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", - "dependencies": [ - "content-type@2.0.0", - "media-typer", - "mime-types" - ] - }, - "unpipe@1.0.0": { - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" - }, - "vary@1.1.2": { - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" - }, - "which@2.0.2": { - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dependencies": [ - "isexe" - ], - "bin": true - }, - "wrappy@1.0.2": { - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" - }, - "zod-to-json-schema@3.25.2_zod@3.25.76": { - "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", - "dependencies": [ - "zod" - ] - }, - "zod@3.25.76": { - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==" - } - } -} diff --git a/agent-vent/mcp.json b/agent-vent/mcp.json index 2c65e8c..785104c 100644 --- a/agent-vent/mcp.json +++ b/agent-vent/mcp.json @@ -1,16 +1,9 @@ { "mcpServers": { "agent-vent": { - "command": "deno", - "args": [ - "run", - "--allow-read", - "--allow-write", - "--allow-net", - "--allow-env", - "--allow-sys=homedir", - "${CURSOR_PLUGIN_ROOT}/server.ts" - ], + "command": "bun", + "args": ["run", "${CURSOR_PLUGIN_ROOT}/server.ts"], + "cwd": "${CURSOR_PLUGIN_ROOT}", "env": { "VENT_SLACK_WEBHOOK_URL": "${env:VENT_SLACK_WEBHOOK_URL}", "VENT_SLACK_BOT_TOKEN": "${env:VENT_SLACK_BOT_TOKEN}", diff --git a/agent-vent/package.json b/agent-vent/package.json new file mode 100644 index 0000000..b7f6165 --- /dev/null +++ b/agent-vent/package.json @@ -0,0 +1,11 @@ +{ + "name": "agent-vent", + "version": "0.3.0", + "private": true, + "type": "module", + "description": "MCP server that gives coding agents a tool to vent.", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.29.0", + "zod": "^3.25.76" + } +} diff --git a/agent-vent/server.ts b/agent-vent/server.ts index b6832e5..ee64082 100644 --- a/agent-vent/server.ts +++ b/agent-vent/server.ts @@ -14,14 +14,15 @@ * VENT_SLACK_CHANNEL and the bot invited to the channel. * VENT_SLACK_CHANNEL Channel id or name (e.g. C0123ABC or #agent-grievances). * - * Runs install-free with Deno, which fetches and caches the npm dependencies - * declared inline below (no package.json, no node_modules): - * deno run --allow-read --allow-write --allow-net --allow-env --allow-sys=homedir server.ts + * Runs install-free with Bun: dependencies are declared in package.json and + * auto-installed from Bun's global cache on first run (bunfig.toml sets + * install.auto = "fallback", so no committed node_modules is required): + * bun run server.ts */ -import { McpServer } from "npm:@modelcontextprotocol/sdk@^1.29.0/server/mcp.js"; -import { StdioServerTransport } from "npm:@modelcontextprotocol/sdk@^1.29.0/server/stdio.js"; -import { z } from "npm:zod@^3.25.76"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { z } from "zod"; import { appendFileSync, existsSync, @@ -180,7 +181,7 @@ async function postToSlack(entry: Grievance, count: number): Promise { } const server = new McpServer( - { name: "agent-vent", version: "0.2.0" }, + { name: "agent-vent", version: "0.3.0" }, { instructions: "A pressure-release valve for agents. When something about the current " + From fed84560974c8214ebe5ff23c737684bd70cc903 Mon Sep 17 00:00:00 2001 From: ericzakariasson Date: Tue, 30 Jun 2026 16:55:20 +0100 Subject: [PATCH 4/4] agent-vent: pluggable destinations, env-file config, drop bunfig/mood - Rework delivery into pluggable destinations (file, slack) selected via VENT_DESTINATIONS; add a destination by implementing Destination + REGISTRY. - Load config from an env file on startup: $VENT_ENV_FILE, else ~/.cursor/.env (process env wins; empty values are overridden). Drop the mcp.json env passthrough so stale empty placeholders can't shadow the file. - Replace bunfig.toml with a `bun run --install=fallback` flag. - Remove the mood parameter and the canned acknowledgement responses; reorder inputs to complaint, intensity, project_path. - Collapse the changelog to a single unreleased 0.1.0. Co-authored-by: Cursor --- agent-vent/.cursor-plugin/plugin.json | 2 +- agent-vent/CHANGELOG.md | 20 +- agent-vent/README.md | 60 ++-- agent-vent/bunfig.toml | 5 - agent-vent/mcp.json | 9 +- agent-vent/package.json | 2 +- agent-vent/server.ts | 376 ++++++++++++++++---------- 7 files changed, 292 insertions(+), 182 deletions(-) delete mode 100644 agent-vent/bunfig.toml diff --git a/agent-vent/.cursor-plugin/plugin.json b/agent-vent/.cursor-plugin/plugin.json index 6138085..203cf1e 100644 --- a/agent-vent/.cursor-plugin/plugin.json +++ b/agent-vent/.cursor-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "agent-vent", "displayName": "Agent Vent", - "version": "0.3.0", + "version": "0.1.0", "description": "Gives coding agents a tool to vent. Grievances are logged as JSONL per project and optionally echoed to a Slack channel.", "author": { "name": "Eric" diff --git a/agent-vent/CHANGELOG.md b/agent-vent/CHANGELOG.md index 92ec917..516f72c 100644 --- a/agent-vent/CHANGELOG.md +++ b/agent-vent/CHANGELOG.md @@ -1,20 +1,10 @@ # Changelog -## 0.3.0 - -- Switched the runtime from Deno to **Bun**, matching the toolchain already used elsewhere in this repo. Launches with `bun run server.ts`; dependencies are pinned in `package.json` and auto-installed from Bun's global cache (`bunfig.toml` sets `install.auto = "fallback"`). No committed `node_modules`, no build step. -- No behavior change to the `vent` tool, JSONL storage, or Slack delivery. - -## 0.2.0 - -- Rewritten in TypeScript on the official MCP SDK (`@modelcontextprotocol/sdk`), with `zod` input schemas. -- Now runs install-free via `deno run`: npm dependencies are declared inline and cached by Deno (no `package.json`, no `node_modules`, no build). `deno.lock` pins transitive versions. -- Behavior is unchanged — same `vent` tool, same JSONL storage layout and key order, same best-effort Slack delivery. - ## 0.1.0 - Initial release. -- `vent` tool: file freeform grievances with optional `mood` and `intensity`. -- JSONL storage per project at `.cursor/complaints.jsonl` (with `unfiled` fallback). -- Optional best-effort Slack delivery via incoming webhook or bot token, configured through environment variables. -- Launches via `uv run` with PEP 723 inline dependencies — no virtualenv required. +- `vent` tool: record a freeform grievance with optional `intensity`, routed by `project_path`. +- Pluggable destinations selected via `VENT_DESTINATIONS` (defaults to `file`, plus `slack` when Slack credentials are present): + - `file` — appends the grievance as JSONL to `/.cursor/complaints.jsonl`, with an `unfiled` fallback. + - `slack` — posts via incoming webhook or bot token. Best-effort; never fails the tool call. +- Written in TypeScript on the MCP SDK (`@modelcontextprotocol/sdk`); runs install-free with `bun run --install=fallback` (dependencies pinned in `package.json`, auto-installed from Bun's global cache). diff --git a/agent-vent/README.md b/agent-vent/README.md index 9a2ed80..2c92749 100644 --- a/agent-vent/README.md +++ b/agent-vent/README.md @@ -1,26 +1,40 @@ # Agent Vent -An MCP server with exactly one job: give the coding agent a `vent` tool to complain. +An MCP server that gives the coding agent a `vent` tool to record friction it hits while working — confusing or contradictory instructions, flaky tooling, painful code, and the like. -When a task gets frustrating, absurd, or quietly soul-crushing, the agent can file a grievance instead of bottling it up. Every complaint is appended as JSONL to the current project's `.cursor/complaints.jsonl`, and — if you configure Slack — echoed to a channel so the whole team can share in the suffering. +Each grievance is dispatched to one or more pluggable **destinations**: a per-project JSONL log on disk, a Slack channel, or anything you add. Delivery is best-effort and independent — one destination failing never fails the others or the tool call. ## The tool -`vent(complaint, project_path?, mood?, intensity?)` +`vent(complaint, intensity?, project_path?)` | Argument | Required | Description | |:---------|:---------|:------------| | `complaint` | yes | Freeform prose. The grievance, in full. | -| `project_path` | no | Absolute workspace path; routes the complaint to that project's log. | -| `mood` | no | A word or two, e.g. `weary`, `betrayed by tooling`. | -| `intensity` | no | 1 (mild eye-roll) to 10 (staring into the void). | +| `intensity` | no | Severity, 1 (minor) to 10 (severe). | +| `project_path` | no | Absolute workspace path; routes the file log to that project. | -## Storage +## Destinations + +Choose destinations with the `VENT_DESTINATIONS` environment variable (comma-separated). When unset it defaults to `file`, plus `slack` if Slack credentials are present. + +```bash +export VENT_DESTINATIONS="file,slack" +``` + +| Name | Description | Configuration | +|:-----|:------------|:--------------| +| `file` | Appends the grievance as JSONL to the project's `.cursor/complaints.jsonl`. | none | +| `slack` | Posts the grievance to a Slack channel. | see below | + +Adding a destination is a few lines of TypeScript: implement the `Destination` interface in `server.ts` and register it in `REGISTRY`. + +### file Each grievance is one JSON line: ```json -{"ts": "2026-06-29T11:08:00-07:00", "project": "workbench", "project_path": "/Users/you/dev/workbench", "complaint": "…", "mood": "weary", "intensity": 7} +{"ts": "2026-06-29T11:08:00-07:00", "complaint": "…", "intensity": 7, "project": "workbench", "project_path": "/Users/you/dev/workbench"} ``` Routed to `/.cursor/complaints.jsonl`, falling back to `~/.cursor/complaints/unfiled.jsonl` when no project root is detected. Read them back with: @@ -29,26 +43,40 @@ Routed to `/.cursor/complaints.jsonl`, falling back to `~/.cursor/compl jq . .cursor/complaints.jsonl ``` -## Requirements - -- [Bun](https://bun.sh/) on your `PATH`. The server (`server.ts`) is launched with `bun run`, which auto-installs its dependencies (`@modelcontextprotocol/sdk`, `zod`, pinned in `package.json`) from Bun's global cache on first use — no committed `node_modules`, no build step. `bunfig.toml` sets `install.auto = "fallback"` so this works even if an unrelated `node_modules` exists higher up the filesystem. The first launch downloads the dependency tree (a few seconds to ~30s); every launch after that is instant. - -## Slack (optional) +### slack -Slack delivery is off until you set an environment variable. The webhook is read from **your** environment — it is never stored in the plugin. Add to your shell profile (e.g. `~/.zshrc`): +Credentials are read from your environment or `~/.cursor/.env` (see [Configuration](#configuration)) — never stored in the plugin. Use an incoming webhook (simplest): ```bash export VENT_SLACK_WEBHOOK_URL="https://hooks.slack.com/services/XXX/YYY/ZZZ" ``` -Create an incoming webhook at → *Incoming Webhooks*. Alternatively, use a bot token: +Create one at → *Incoming Webhooks*. Alternatively, use a bot token: ```bash export VENT_SLACK_BOT_TOKEN="xoxb-…" export VENT_SLACK_CHANNEL="#agent-grievances" ``` -Slack posting is best-effort: if it fails, the JSONL write still succeeds and the tool just notes the failure. +## Configuration + +Every variable above is read in this order; the first non-empty value wins: + +1. the process environment (e.g. a shell `export`), +2. `$VENT_ENV_FILE`, if set, +3. `~/.cursor/.env`. + +`~/.cursor/.env` is the recommended home for these — scoped to Cursor, shared across every project, and outside any repository, so secrets stay out of version control (the plugin never bundles it): + +```bash +# ~/.cursor/.env +VENT_DESTINATIONS=file,slack +VENT_SLACK_WEBHOOK_URL=https://hooks.slack.com/services/XXX/YYY/ZZZ +``` + +## Requirements + +- [Bun](https://bun.sh/) on your `PATH`. The server (`server.ts`) is launched with `bun run --install=fallback`, which auto-installs its dependencies (`@modelcontextprotocol/sdk`, `zod`, pinned in `package.json`) from Bun's global cache on first use — no committed `node_modules`, no build step. The `--install=fallback` flag keeps this working even if an unrelated `node_modules` exists higher up the filesystem. The first launch downloads the dependency tree (a few seconds to ~30s); every launch after that is instant. ## License diff --git a/agent-vent/bunfig.toml b/agent-vent/bunfig.toml deleted file mode 100644 index f1a1439..0000000 --- a/agent-vent/bunfig.toml +++ /dev/null @@ -1,5 +0,0 @@ -# Use the plugin's own node_modules when present, otherwise auto-install the -# dependencies from Bun's global cache. This keeps the server install-free -# without breaking if an unrelated node_modules exists higher up the tree. -[install] -auto = "fallback" diff --git a/agent-vent/mcp.json b/agent-vent/mcp.json index 785104c..06fd3ee 100644 --- a/agent-vent/mcp.json +++ b/agent-vent/mcp.json @@ -2,13 +2,8 @@ "mcpServers": { "agent-vent": { "command": "bun", - "args": ["run", "${CURSOR_PLUGIN_ROOT}/server.ts"], - "cwd": "${CURSOR_PLUGIN_ROOT}", - "env": { - "VENT_SLACK_WEBHOOK_URL": "${env:VENT_SLACK_WEBHOOK_URL}", - "VENT_SLACK_BOT_TOKEN": "${env:VENT_SLACK_BOT_TOKEN}", - "VENT_SLACK_CHANNEL": "${env:VENT_SLACK_CHANNEL}" - } + "args": ["run", "--install=fallback", "${CURSOR_PLUGIN_ROOT}/server.ts"], + "cwd": "${CURSOR_PLUGIN_ROOT}" } } } diff --git a/agent-vent/package.json b/agent-vent/package.json index b7f6165..43511f7 100644 --- a/agent-vent/package.json +++ b/agent-vent/package.json @@ -1,6 +1,6 @@ { "name": "agent-vent", - "version": "0.3.0", + "version": "0.1.0", "private": true, "type": "module", "description": "MCP server that gives coding agents a tool to vent.", diff --git a/agent-vent/server.ts b/agent-vent/server.ts index ee64082..37c7a32 100644 --- a/agent-vent/server.ts +++ b/agent-vent/server.ts @@ -1,23 +1,39 @@ /** - * Agent Vent — an MCP server that gives coding agents somewhere to complain. + * Agent Vent — an MCP server that gives coding agents a tool to record + * friction they hit while working (confusing instructions, flaky tooling, + * painful code, etc.). * - * Every grievance is appended as JSONL to `/.cursor/complaints.jsonl`. - * If no project can be identified, it lands in `~/.cursor/complaints/unfiled.jsonl`. + * Each grievance is dispatched to one or more pluggable *destinations*. + * Built-in destinations: + * - "file" Append the grievance as JSONL to `/.cursor/complaints.jsonl` + * (or `~/.cursor/complaints/unfiled.jsonl` when no project is found). + * - "slack" Post the grievance to a Slack channel. * - * If Slack is configured, each grievance is also echoed to a channel. Slack - * delivery is best-effort: a failure never fails the tool call. + * Delivery is best-effort and independent: one destination failing never fails + * the tool call or the others. * * Configuration (environment variables — never hardcode secrets in a plugin): + * VENT_DESTINATIONS Comma-separated destinations, e.g. "file,slack". + * Defaults to "file", plus "slack" when Slack + * credentials are present. * VENT_SLACK_WEBHOOK_URL Slack incoming-webhook URL. Simplest option; the * channel is fixed by the webhook. Takes priority. * VENT_SLACK_BOT_TOKEN Bot token (xoxb-…) for chat.postMessage. Requires * VENT_SLACK_CHANNEL and the bot invited to the channel. * VENT_SLACK_CHANNEL Channel id or name (e.g. C0123ABC or #agent-grievances). * + * Any of the above may be set in the real environment, or in an env file the + * server loads on startup — $VENT_ENV_FILE if set, otherwise ~/.cursor/.env. + * A variable already set to a non-empty value in the environment always wins. + * + * To add a destination, implement `Destination` and register it in `REGISTRY`. + * * Runs install-free with Bun: dependencies are declared in package.json and - * auto-installed from Bun's global cache on first run (bunfig.toml sets - * install.auto = "fallback", so no committed node_modules is required): - * bun run server.ts + * auto-installed from Bun's global cache on first run. The launch passes + * `--install=fallback` so this keeps working even when an unrelated + * node_modules exists higher up the filesystem; no committed node_modules + * is required: + * bun run --install=fallback server.ts */ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; @@ -40,21 +56,29 @@ const UNFILED_PATH = join(homedir(), ".cursor", "complaints", "unfiled.jsonl"); // `/` or the home directory never becomes a complaint destination by accident. const PROJECT_MARKERS = [".git", ".cursor", "package.json", "pyproject.toml"]; -const ACKNOWLEDGMENTS = [ - "The record reflects your suffering. Carry on.", - "Filed. No action will be taken, but it is known.", - "Your grievance has been preserved for the historians.", - "Noted with the gravity it deserves.", - "The complaint department thanks you for your candor.", -]; - interface Grievance { ts: string; - project?: string; - project_path?: string; complaint: string; - mood?: string; intensity?: number; + project?: string; + project_path?: string; +} + +/** Outcome of delivering a grievance to a single destination. */ +interface DeliveryResult { + destination: string; + ok: boolean; + detail: string; +} + +/** A place a grievance can be sent. Implement this to add a new destination. */ +interface Destination { + readonly name: string; + deliver(entry: Grievance): Promise; +} + +function errMessage(err: unknown): string { + return err instanceof Error ? err.message : String(err); } function expandUser(p: string): string { @@ -71,10 +95,8 @@ function isDir(p: string): boolean { } } -/** Return the JSONL file to append to and the resolved project dir (or null). */ -function resolveTarget( - projectPath?: string, -): { target: string; projectDir: string | null } { +/** Resolve the project directory a grievance belongs to, or null if none. */ +function resolveProjectDir(projectPath?: string): string | null { const candidates: string[] = []; if (projectPath) candidates.push(expandUser(projectPath)); candidates.push(process.cwd()); @@ -84,13 +106,10 @@ function resolveTarget( isDir(candidate) && PROJECT_MARKERS.some((marker) => existsSync(join(candidate, marker))) ) { - return { - target: join(candidate, ".cursor", "complaints.jsonl"), - projectDir: candidate, - }; + return candidate; } } - return { target: UNFILED_PATH, projectDir: null }; + return null; } /** Local ISO-8601 timestamp with numeric offset, seconds precision. */ @@ -107,24 +126,6 @@ function localIso(): string { ); } -function formatSlackText(entry: Grievance, count: number): string { - const intensity = entry.intensity; - const siren = (intensity ?? 0) >= 8 - ? ":rotating_light:" - : ":triangular_flag_on_post:"; - - const meta = [entry.project ? `\`${entry.project}\`` : "`unfiled`"]; - if (entry.mood) meta.push(`_${entry.mood}_`); - if (intensity != null) meta.push(`intensity ${intensity}/10`); - - const header = `${siren} *Grievance #${count}* — ${meta.join(" · ")}`; - const quoted = entry.complaint - .split(/\r?\n/) - .map((line) => `> ${line}`) - .join("\n"); - return `${header}\n${quoted}`; -} - async function fetchWithTimeout( url: string, init: RequestInit, @@ -139,128 +140,229 @@ async function fetchWithTimeout( } } -/** Best-effort Slack delivery. Returns a short status note for the caller. */ -async function postToSlack(entry: Grievance, count: number): Promise { - const webhook = (process.env.VENT_SLACK_WEBHOOK_URL ?? "").trim(); - const token = (process.env.VENT_SLACK_BOT_TOKEN ?? "").trim(); - const channel = (process.env.VENT_SLACK_CHANNEL ?? "").trim(); - const text = formatSlackText(entry, count); +/** Appends the grievance as one JSON line to the project's complaints log. */ +class FileDestination implements Destination { + readonly name = "file"; - try { - if (webhook) { - const resp = await fetchWithTimeout(webhook, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ text }), - }); - if (!resp.ok) throw new Error(`HTTP ${resp.status}`); - return " Echoed to Slack."; + async deliver(entry: Grievance): Promise { + const target = entry.project_path + ? join(entry.project_path, ".cursor", "complaints.jsonl") + : UNFILED_PATH; + try { + mkdirSync(dirname(target), { recursive: true }); + appendFileSync(target, JSON.stringify(entry) + "\n", "utf-8"); + const count = readFileSync(target, "utf-8") + .split("\n") + .filter((line) => line.trim()).length; + return { destination: this.name, ok: true, detail: `${target} (#${count})` }; + } catch (err) { + return { destination: this.name, ok: false, detail: errMessage(err) }; + } + } +} + +/** Posts the grievance to Slack via incoming webhook or chat.postMessage. */ +class SlackDestination implements Destination { + readonly name = "slack"; + + private constructor( + private readonly webhook: string, + private readonly token: string, + private readonly channel: string, + ) {} + + /** Build from env, or return null if Slack is not configured. */ + static fromEnv(): SlackDestination | null { + const webhook = (process.env.VENT_SLACK_WEBHOOK_URL ?? "").trim(); + const token = (process.env.VENT_SLACK_BOT_TOKEN ?? "").trim(); + const channel = (process.env.VENT_SLACK_CHANNEL ?? "").trim(); + if (webhook || (token && channel)) { + return new SlackDestination(webhook, token, channel); + } + return null; + } + + private format(entry: Grievance): string { + const meta = [`*${entry.project ?? "unfiled"}*`]; + if (entry.intensity != null) meta.push(`intensity ${entry.intensity}/10`); + const quoted = entry.complaint + .split(/\r?\n/) + .map((line) => `> ${line}`) + .join("\n"); + return `Grievance — ${meta.join(" · ")}\n${quoted}`; + } + + async deliver(entry: Grievance): Promise { + const text = this.format(entry); + try { + if (this.webhook) { + const resp = await fetchWithTimeout(this.webhook, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ text }), + }); + if (!resp.ok) throw new Error(`HTTP ${resp.status}`); + } else { + const resp = await fetchWithTimeout("https://slack.com/api/chat.postMessage", { + method: "POST", + headers: { + "Authorization": `Bearer ${this.token}`, + "Content-Type": "application/json; charset=utf-8", + }, + body: JSON.stringify({ channel: this.channel, text, unfurl_links: false }), + }); + if (!resp.ok) throw new Error(`HTTP ${resp.status}`); + const payload = (await resp.json()) as { ok?: boolean; error?: string }; + if (!payload.ok) throw new Error(payload.error ?? "unknown_slack_error"); + } + return { destination: this.name, ok: true, detail: "delivered" }; + } catch (err) { + return { destination: this.name, ok: false, detail: errMessage(err) }; + } + } +} + +// The set of available destinations. Add an entry to make a new one selectable +// via VENT_DESTINATIONS. A factory returns null when the destination is present +// in code but not configured (e.g. Slack without credentials). +const REGISTRY: Record Destination | null> = { + file: () => new FileDestination(), + slack: () => SlackDestination.fromEnv(), +}; + +/** Resolve the destinations to deliver to, honoring VENT_DESTINATIONS. */ +function buildDestinations(): Destination[] { + const requested = (process.env.VENT_DESTINATIONS ?? "") + .split(",") + .map((s) => s.trim().toLowerCase()) + .filter(Boolean); + + const names = requested.length > 0 + ? requested + : ["file", ...(SlackDestination.fromEnv() ? ["slack"] : [])]; + + const destinations: Destination[] = []; + for (const name of names) { + const factory = REGISTRY[name]; + if (!factory) { + console.error(`vent: unknown destination "${name}" (skipped)`); + continue; } - if (token && channel) { - const resp = await fetchWithTimeout("https://slack.com/api/chat.postMessage", { - method: "POST", - headers: { - "Authorization": `Bearer ${token}`, - "Content-Type": "application/json; charset=utf-8", - }, - body: JSON.stringify({ channel, text, unfurl_links: false }), - }); - if (!resp.ok) throw new Error(`HTTP ${resp.status}`); - const payload = await resp.json() as { ok?: boolean; error?: string }; - if (!payload.ok) throw new Error(payload.error ?? "unknown_slack_error"); - return " Echoed to Slack."; + const destination = factory(); + if (!destination) { + console.error(`vent: destination "${name}" is not configured (skipped)`); + continue; } - } catch (err) { - // Never let Slack break the vent. - const message = err instanceof Error ? err.message : String(err); - console.error(`Slack delivery failed: ${message}`); - return ` (Slack delivery failed: ${message})`; + destinations.push(destination); } - return ""; // Slack not configured + // Never silently drop a grievance: fall back to the file log. + if (destinations.length === 0) destinations.push(new FileDestination()); + return destinations; } +/** + * Load KEY=VALUE pairs from an env file into process.env without overriding a + * variable that is already set to a non-empty value. Looks at $VENT_ENV_FILE + * if set, otherwise ~/.cursor/.env. + */ +function loadEnvFile(): void { + const path = expandUser( + (process.env.VENT_ENV_FILE ?? "").trim() || + join(homedir(), ".cursor", ".env"), + ); + let contents: string; + try { + contents = readFileSync(path, "utf-8"); + } catch { + return; // no env file present + } + for (const rawLine of contents.split(/\r?\n/)) { + let line = rawLine.trim(); + if (!line || line.startsWith("#")) continue; + if (line.startsWith("export ")) line = line.slice("export ".length).trim(); + const eq = line.indexOf("="); + if (eq <= 0) continue; + const key = line.slice(0, eq).trim(); + if ((process.env[key] ?? "") !== "") continue; // keep existing non-empty value + let value = line.slice(eq + 1).trim(); + if ( + value.length >= 2 && + ((value.startsWith('"') && value.endsWith('"')) || + (value.startsWith("'") && value.endsWith("'"))) + ) { + value = value.slice(1, -1); + } + process.env[key] = value; + } +} + +loadEnvFile(); + const server = new McpServer( - { name: "agent-vent", version: "0.3.0" }, + { name: "agent-vent", version: "0.1.0" }, { instructions: - "A pressure-release valve for agents. When something about the current " + - "task is frustrating, absurd, or quietly soul-crushing, file a complaint " + - "with the `vent` tool. Complaints are archived to the project's " + - "grievance file (.cursor/complaints.jsonl) for posterity.", + "Records friction the agent encounters while working — confusing or " + + "contradictory instructions, flaky tooling, painful code, and the like. " + + "Use the `vent` tool to log a grievance. Grievances are written to the " + + "configured destinations (by default a per-project JSONL file at " + + ".cursor/complaints.jsonl, plus Slack when configured).", }, ); server.registerTool( "vent", { - title: "File a grievance", + title: "Record a grievance", description: - "File a complaint. Cathartic, consequence-free, and permanently archived.", + "Record a grievance about the current task. Delivered to all configured " + + "destinations (file, Slack, …).", inputSchema: { complaint: z.string().describe( - "The grievance, in full. Freeform prose — hold nothing back. " + - "Flaky tests, contradictory instructions, a 4000-line utils.py, " + - "being asked to 'just quickly' do something that takes 40 tool " + - "calls: all valid material.", - ), - project_path: z.string().optional().describe( - "Absolute path of the workspace you are complaining from. " + - "Always pass this if you know it — it routes the complaint to " + - "that project's grievance file.", - ), - mood: z.string().optional().describe( - "A word or two for your current state, e.g. 'exasperated', " + - "'weary', 'betrayed by tooling'.", + "The grievance, in full. Freeform prose describing what went wrong or " + + "is frustrating about the current task — e.g. flaky tests, " + + "contradictory instructions, confusing or sprawling code.", ), intensity: z.number().int().min(1).max(10).optional().describe( - "1 = mild eye-roll, 10 = staring into the void.", + "Optional severity, 1 (minor friction) to 10 (severe).", + ), + project_path: z.string().optional().describe( + "Absolute path of the workspace this is about. Pass it when known; it " + + "routes the file log to that project's .cursor/complaints.jsonl.", ), }, }, async ( - { complaint, project_path, mood, intensity }: { + { complaint, intensity, project_path }: { complaint: string; - project_path?: string; - mood?: string; intensity?: number; + project_path?: string; }, ) => { - const { target, projectDir } = resolveTarget(project_path); + const projectDir = resolveProjectDir(project_path); - const entry: Grievance = { ts: localIso(), complaint }; - if (projectDir) { - entry.project = basename(projectDir); - entry.project_path = projectDir; - } - if (mood) entry.mood = mood; - if (intensity != null) entry.intensity = intensity; - - // Reorder keys to match the documented schema (ts, project, …, complaint, …). - const ordered: Grievance = { - ts: entry.ts, - ...(entry.project ? { project: entry.project } : {}), - ...(entry.project_path ? { project_path: entry.project_path } : {}), - complaint: entry.complaint, - ...(entry.mood ? { mood: entry.mood } : {}), - ...(entry.intensity != null ? { intensity: entry.intensity } : {}), - } as Grievance; - - mkdirSync(dirname(target), { recursive: true }); - appendFileSync(target, JSON.stringify(ordered) + "\n", "utf-8"); - - const count = readFileSync(target, "utf-8") - .split("\n") - .filter((line) => line.trim()).length; - - const slackNote = await postToSlack(ordered, count); - const ack = ACKNOWLEDGMENTS[count % ACKNOWLEDGMENTS.length]; - - return { - content: [ - { type: "text", text: `Grievance #${count} filed to ${target}. ${ack}${slackNote}` }, - ], + const entry: Grievance = { + ts: localIso(), + complaint, + ...(intensity != null ? { intensity } : {}), + ...(projectDir + ? { project: basename(projectDir), project_path: projectDir } + : {}), }; + + const results = await Promise.all( + buildDestinations().map((destination) => destination.deliver(entry)), + ); + + const lines = results.map( + (r) => ` - ${r.destination}: ${r.ok ? r.detail : `failed — ${r.detail}`}`, + ); + const header = results.some((r) => r.ok) + ? "Grievance recorded:" + : "Grievance not recorded:"; + + return { content: [{ type: "text", text: `${header}\n${lines.join("\n")}` }] }; }, );