diff --git a/README.md b/README.md index 8d175a3..18279b5 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,21 @@ On Debian/Ubuntu: sudo apt install yt-dlp ffmpeg jq python3 curl ``` +### Optional install flags + +```bash +./install.sh --with-skill # also drop SKILL.md into ~/.claude/skills/watch-cli/ +./install.sh --with-mcp # print the npm install hint for the MCP stdio server +``` + +- `--with-skill` copies the portable `SKILL.md` into `~/.claude/skills/watch-cli/` + so Claude Code picks up watch-cli as a skill on next start. The same file + works in OpenClaw and hermes-agent — see [`SKILL.md`](SKILL.md). +- `--with-mcp` prints the manual install line for [`@sonpiaz/watch-cli-mcp`](mcp-server/), + the MCP stdio server that exposes watch-cli to Claude Desktop, Cursor, Cline, + Continue.dev, Windsurf, Zed, and any other MCP-capable client. The flag will + auto-install once the package is published to npm. + --- ## Setup diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..83a9165 --- /dev/null +++ b/SKILL.md @@ -0,0 +1,74 @@ +--- +name: watch-cli +description: "Watch any social video → get an architecture diagram, working component, runnable notebook, or step-by-step cheat sheet — automatically." +homepage: https://github.com/sonpiaz/watch-cli +metadata: + openclaw: + emoji: "" + requires: + bins: + - watch + install: + - id: curl-install + kind: shell + command: "curl -fsSL https://raw.githubusercontent.com/sonpiaz/watch-cli/main/install.sh | bash" + bins: + - watch + label: "Install watch-cli (curl)" +--- + +Watch any social video → get an architecture diagram, working component, runnable notebook, or step-by-step cheat sheet — automatically. + +watch-cli is a thin orchestrator that downloads any social video, extracts evenly-spaced frames, and transcribes the audio. The output is a single labeled block (or one-line JSON) designed for an LLM to read frames as images and transcript as text. The agent supplies the prompt; watch-cli supplies the raw materials. + +## When to invoke + +Reach for `watch` whenever the user gives you a video URL and wants you to do something with what's in it. + +- The user pastes a video URL with no verb. Ask one clarifying question ("summarize, implement, clone the UI, extract the architecture, or something else?"), then run `watch`. +- The user asks to "summarize", "explain", or "walk me through" content at a video URL. +- The user asks to "implement", "clone", "build", or "replicate" what is on screen in a video. +- The user asks to "extract architecture from", "diagram", or "turn this paper talk into code" at a video URL. + +Supported platforms: YouTube, X / Twitter, LinkedIn, TikTok, Vimeo, Reddit, Facebook. Login-walled sources fall back to the user's signed-in browser cookies automatically. + +## What you get back + +The `watch` output gives you the raw materials to map to five concrete artifacts. Match the user's intent to one of them. + +A coding walkthrough becomes working project files: read the frames for file names, exact code, and dependencies; read the transcript for intent and rationale; emit the final state, not the intermediate edits. + +A system architecture talk becomes an interactive architecture diagram: a single self-contained HTML page with actors, surfaces, APIs, and clickable named flows that highlight the path through the system. + +A UI or motion demo becomes a working React component: one paste-ready `.tsx` file that captures the feel and the single interaction that makes the UI special, not a pixel-perfect screenshot. + +A paper or research talk becomes a runnable notebook: one `.ipynb` implementing the core method on a toy dataset that runs end-to-end on a free Colab T4. + +A long tutorial becomes a step-by-step cheat sheet: numbered steps with copy-pasteable commands, video timestamps, verification per step, and only the troubleshooting the speaker actually discussed. + +Five copy-paste prompt templates live in [`prompts/`](https://github.com/sonpiaz/watch-cli/tree/main/prompts). Pick the one that matches the user's intent. + +## Parse rules + +`watch` emits a versioned, agent-shaped payload. Both formats are documented in [`docs/output-schema.md`](https://github.com/sonpiaz/watch-cli/blob/main/docs/output-schema.md) and conform to the v1 contract — append-only, no renames, no type changes within v1. + +- **Preferred: JSON mode.** `watch --format json` emits one UTF-8 JSON object on stdout terminated by a newline. Parse it with a real JSON parser, switch on `obj.version`, read `obj.video_path`, `obj.duration_sec`, `obj.frame_paths` (array of absolute JPG paths, earliest-in-video first), `obj.transcript` (string or `null`), and `obj.exit_code`. Field reference is the single source of truth in `docs/output-schema.md`. +- **Fallback: text mode.** Some agent hosts (Claude Code does this today) capture stdout as a free-text block. The leading line `WATCH_OUTPUT_VERSION: 1` is the version signal; everything below is labeled blocks (`VIDEO:`, `DURATION:`, `FRAMES:`, `TRANSCRIPT:`, `EXIT:`) per the same doc. +- **Read frames as images, transcript as text.** Each path under `FRAMES:` (or each string in `frame_paths`) is an absolute path to a JPG on disk. Pass the path to the host's image-reading primitive. The transcript is plain UTF-8 text — no decoding needed. +- **Exit-code behavior** is documented in [`docs/exit-codes.md`](https://github.com/sonpiaz/watch-cli/blob/main/docs/exit-codes.md). The partial-success case is the one to remember: on `exit 4` the frames are populated and the transcript is `null` — branch on the exit code and fall through to a frames-only consumption path instead of failing the run. + +## Invocation + +```bash +watch [frame-count] +``` + +Default frame count is 8. For a fast-cut or dense UI demo, double it. For a multi-hour conference talk, bump to 24–32. The CLI does not cap; agent hosts typically prefer ≤ 32. + +## Anti-patterns + +- Do not parse stderr. Progress lines on stderr (`[watch] downloading …`) are not part of the contract and change between releases. Programmatic consumers ignore stderr. +- Do not parse the filename of a frame to infer its position. Filenames are implementation detail; the ordering of `frame_paths` is the contract. +- Do not hard-fail on every non-zero exit. `exit 4` is recoverable partial success — frames populated, transcript `null`. Branch on `exit_code` before parsing. +- Do not surface API keys or environment variable names in chat. `KYMA_API_KEY` setup lives in the README. +- Do not embed the locked pitch into a longer marketing paragraph. The description line above is the source of truth; reuse it verbatim where the host shows skill metadata. diff --git a/install.sh b/install.sh index 48bfd9e..f07d8a0 100755 --- a/install.sh +++ b/install.sh @@ -4,18 +4,56 @@ # curl -fsSL https://raw.githubusercontent.com/sonpiaz/watch-cli/main/install.sh | bash # or, from a clone: # ./install.sh +# +# Flags: +# --with-skill After install, drop SKILL.md into ~/.claude/skills/watch-cli/ +# so Claude Code picks up the watch-cli skill on next start. +# --with-mcp Print the manual install hint for the MCP stdio server +# (@sonpiaz/watch-cli-mcp on npm — not auto-installed yet). +# --help, -h Show this help and exit. set -euo pipefail REPO_URL="https://github.com/sonpiaz/watch-cli" INSTALL_DIR="${WATCH_CLI_HOME:-$HOME/.watch-cli}" BIN_LINK_DIR="${WATCH_CLI_BIN:-$HOME/.local/bin}" +CLAUDE_SKILLS_DIR="${HOME}/.claude/skills" + +WITH_SKILL=0 +WITH_MCP=0 red() { printf "\033[31m%s\033[0m\n" "$*"; } green() { printf "\033[32m%s\033[0m\n" "$*"; } yellow() { printf "\033[33m%s\033[0m\n" "$*"; } dim() { printf "\033[2m%s\033[0m\n" "$*"; } +usage() { + cat <<'EOF' +watch-cli installer + +Usage: + curl -fsSL https://raw.githubusercontent.com/sonpiaz/watch-cli/main/install.sh | bash + ./install.sh [--with-skill] [--with-mcp] + +Flags: + --with-skill After install, copy SKILL.md into ~/.claude/skills/watch-cli/ + so Claude Code picks up the watch-cli skill on next start. + --with-mcp Print the manual install hint for the MCP stdio server + (@sonpiaz/watch-cli-mcp on npm — not auto-installed yet). + -h, --help Show this help and exit. +EOF +} + +# ── Parse args ── +while (($# > 0)); do + case "$1" in + --with-skill) WITH_SKILL=1; shift ;; + --with-mcp) WITH_MCP=1; shift ;; + -h|--help) usage; exit 0 ;; + *) red "Unknown flag: $1"; echo; usage; exit 64 ;; + esac +done + echo "watch-cli installer" echo "===================" @@ -67,6 +105,25 @@ if [[ ":$PATH:" != *":$BIN_LINK_DIR:"* ]]; then echo fi +# ── Optional: drop portable SKILL.md into ~/.claude/skills/watch-cli/ ── +if (( WITH_SKILL )); then + if [[ -f "$INSTALL_DIR/SKILL.md" ]]; then + mkdir -p "$CLAUDE_SKILLS_DIR/watch-cli" + cp "$INSTALL_DIR/SKILL.md" "$CLAUDE_SKILLS_DIR/watch-cli/SKILL.md" + green "✓ Installed SKILL.md → $CLAUDE_SKILLS_DIR/watch-cli/SKILL.md" + else + yellow "⚠ --with-skill: $INSTALL_DIR/SKILL.md not found; skipped." + fi +fi + +# ── Optional: MCP stdio server hint ── +if (( WITH_MCP )); then + echo + yellow "MCP server install will be available once @sonpiaz/watch-cli-mcp is published to npm — install manually for now:" + echo " npm install -g @sonpiaz/watch-cli-mcp" + echo +fi + # ── Env file scaffold ── ENV_DIR="$HOME/.config/watch-cli" ENV_FILE="$ENV_DIR/env" diff --git a/mcp-server/.gitignore b/mcp-server/.gitignore new file mode 100644 index 0000000..dd6e803 --- /dev/null +++ b/mcp-server/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +*.log +.DS_Store diff --git a/mcp-server/README.md b/mcp-server/README.md new file mode 100644 index 0000000..f3aa0ff --- /dev/null +++ b/mcp-server/README.md @@ -0,0 +1,137 @@ +# @sonpiaz/watch-cli-mcp + +MCP (Model Context Protocol) stdio server that exposes [watch-cli](https://github.com/sonpiaz/watch-cli) as a callable tool. + +> Watch any social video → get an architecture diagram, working component, runnable notebook, or step-by-step cheat sheet — automatically. + +A single MCP server reaches every MCP-capable agent runtime — Claude Desktop, Claude Code, Cursor, OpenClaw, hermes-agent, Cline, Continue.dev, Windsurf, Zed, Codex, Kiro, VS Code Copilot — without per-IDE plugin work. This package wraps `watch-cli` so any of them can call it through one channel. + +## Prerequisites + +- Node.js >= 18. +- The `watch` CLI installed and on `PATH`. Install with: + + ```bash + curl -fsSL https://raw.githubusercontent.com/sonpiaz/watch-cli/main/install.sh | bash + ``` + +The MCP server shells out to `watch`; it does not bundle the CLI. + +## Install + +```bash +npm install -g @sonpiaz/watch-cli-mcp +``` + +Or run on demand via `npx`: + +```bash +npx -y @sonpiaz/watch-cli-mcp +``` + +The binary is `watch-cli-mcp`. It reads JSON-RPC on stdin, writes JSON-RPC on stdout, and logs to stderr. + +## Tool + +One tool is registered: `watch`. + +**Input schema:** + +```json +{ + "type": "object", + "properties": { + "url": { + "type": "string", + "format": "uri", + "description": "A social video URL. Supported: YouTube, X / Twitter, LinkedIn, TikTok, Vimeo, Reddit, Facebook." + }, + "frames": { + "type": "integer", + "minimum": 1, + "maximum": 64, + "description": "Number of evenly-spaced frames to extract. Default 8." + } + }, + "required": ["url"], + "additionalProperties": false +} +``` + +**Output:** the v1 JSON object from [`docs/output-schema.md`](https://github.com/sonpiaz/watch-cli/blob/main/docs/output-schema.md), passed through verbatim. Read the schema for the field reference. + +## Error mapping + +| CLI exit | MCP response | +|---|---| +| `0` | Tool result with the v1 JSON as `content[0].text`. | +| `2` | `InternalError`, message includes `tag=missing-dep:`. | +| `3` `download-auth` / `download-region` | `InvalidParams`, message includes the tag — caller can recover with cookies or a different URL. | +| `3` `download-network` / `download-other` | `InternalError`, message includes the tag. | +| `4` (partial success) | Tool result with `isError: false`. Frames populated, transcript `null`, `exit_code: 4` in the JSON payload. Caller branches on `obj.exit_code === 4`. | +| `64` | `InvalidParams`, message includes `tag=usage-error`. | +| `1` / other | `InternalError`, message includes the CLI stderr tail (≤ 1 KB). | + +Tags always ride in the MCP error `message` field so callers can grep for the same `tag=...` token they would see in CLI stderr. + +## Client configuration + +### Claude Desktop + +Config path: + +- macOS: `~/Library/Application Support/Claude/claude_desktop_config.json` +- Windows: `%APPDATA%\Claude\claude_desktop_config.json` +- Linux: `~/.config/Claude/claude_desktop_config.json` + +Merge into the existing `mcpServers` object: + +```json +{ + "mcpServers": { + "watch-cli": { + "command": "npx", + "args": ["-y", "@sonpiaz/watch-cli-mcp"] + } + } +} +``` + +Restart Claude Desktop after editing. + +### Cursor + +Config path: `.cursor/mcp.json` (project-scoped) or `~/.cursor/mcp.json` (user-global). Same `mcpServers` object shape as Claude Desktop: + +```json +{ + "mcpServers": { + "watch-cli": { + "command": "npx", + "args": ["-y", "@sonpiaz/watch-cli-mcp"] + } + } +} +``` + +### hermes-agent + +Config path: `optional-skills/mcp.json` inside the hermes-agent install, or `~/.hermes-agent/mcp.json` user-global. Same `mcpServers` shape as above. + +### Other MCP clients + +Any MCP-capable agent that accepts an `npx` command will work with the snippet above. Substitute the client's config path; the `command` / `args` pair is portable. + +## Build from source + +```bash +git clone https://github.com/sonpiaz/watch-cli +cd watch-cli/mcp-server +npm install +npm run build +node dist/index.js +``` + +## License + +MIT. © 2026 Son Piaz. diff --git a/mcp-server/SPEC.md b/mcp-server/SPEC.md new file mode 100644 index 0000000..4f76a2f --- /dev/null +++ b/mcp-server/SPEC.md @@ -0,0 +1,319 @@ +# MCP server specification + +This document is the contract for the watch-cli MCP (Model Context +Protocol) stdio server. The actual server is authored against this +spec during Phase 2 implementation; this document is the source of +truth for its shape. + +Cross-references: + +- Output contract the server must return verbatim: + [`../docs/output-schema.md`](../docs/output-schema.md) +- Exit codes that map onto MCP error codes: + [`../docs/exit-codes.md`](../docs/exit-codes.md) +- Pitch text reused in the tool `description`: + [`../BRANDING.md`](../BRANDING.md) +- MCP transport reference: + https://modelcontextprotocol.io/specification/2025-03-26/basic/transports + +--- + +## Purpose + +A single MCP server reaches every MCP-capable agent runtime without +per-IDE plugin work. By 2026 that includes Claude Desktop, Claude Code, +Cursor, OpenClaw, hermes-agent, Cline, Continue.dev, Windsurf, Zed, +Codex, Kiro, and VS Code Copilot — twelve plus production agents in +one codebase. The server exposes `watch` as a callable tool whose +input and output exactly match the v1 contract in +[`../docs/output-schema.md`](../docs/output-schema.md), so every +downstream consumer parses one shape regardless of channel +(CLI stdout, MCP tool response, skill body parse rules). + +--- + +## Package decision + +Two options considered: + +- **(a) Standalone npm package** `@sonpiaz/watch-cli-mcp` — separate + TypeScript codebase under `mcp-server/`, depends on + `@modelcontextprotocol/sdk`. Independent semver. CLI-only users never + install the MCP package. +- **(b) Built-in subcommand** `watch --mcp-serve` — the existing Bash + script grows an MCP mode. Single binary, no npm dep for CLI-only. + +**Locked choice: (a) standalone npm package `@sonpiaz/watch-cli-mcp`.** + +Justification: + +1. **Bash is a poor host for MCP.** JSON-RPC framing + an evolving + capabilities handshake + JSON-schema validation are all in the + TypeScript SDK already; re-implementing them in shell is busywork. +2. **The CLI install path stays small.** `curl install.sh | bash` users + keep getting six Bash scripts and a Python helper, no Node + toolchain forced on them. +3. **Independent release cadence.** The MCP wire format iterates + faster than the CLI contract; bumping one without the other is + easier when they are separate artifacts. +4. **`npx` is the canonical install verb for MCP servers** in every + published client config snippet today (Claude Desktop, Cursor, + Continue.dev all show `npx `). + +Trade-off: two release artifacts. Mitigated by mirroring the version +(see § *Publish workflow*). + +--- + +## Package layout + +The implementer creates the following files under `mcp-server/`. No +file outside `mcp-server/` is modified by the implementation PR except +where the consumer-facing README needs a "Run as MCP server" line. + +| Path | Purpose | +|---|---| +| `mcp-server/package.json` | Package manifest. Fields below. | +| `mcp-server/tsconfig.json` | TypeScript build config. Targets ES2022, module `Node16`, `strict: true`, `outDir: dist`. | +| `mcp-server/src/index.ts` | Entry point. Wires the stdio transport, registers the `watch` tool, starts the server. | +| `mcp-server/src/watch-tool.ts` | Tool handler. Shells out to `watch --format json`, parses the result, maps exit codes onto MCP error codes. | +| `mcp-server/README.md` | User-facing readme: install line + config snippets for the major MCP clients. | +| `mcp-server/.gitignore` | Ignores `node_modules/`, `dist/`. | + +### `package.json` fields + +| Field | Value | +|---|---| +| `name` | `@sonpiaz/watch-cli-mcp` | +| `version` | Mirrors `bin/watch` `VERSION`. First cut: `0.2.0`. | +| `description` | Locked pitch from `BRANDING.md`, ≤ 350 chars, single line. | +| `bin.watch-cli-mcp` | `./dist/index.js` | +| `type` | `module` | +| `engines.node` | `>=20.0.0` (matches MCP SDK). | +| `dependencies` | `@modelcontextprotocol/sdk` pinned to latest stable minor at impl time (current: `^1.29.0`). No other runtime deps. | +| `devDependencies` | `typescript ^5.4.0`, `@types/node ^20.0.0`. | +| `scripts.build` | `tsc -p .` | +| `scripts.prepublishOnly` | `npm run build` (guards against stale `dist/`). | +| `files` | `["dist", "README.md"]`. | + +--- + +## Tool definition + +Exactly one tool is registered: `watch`. + +### Name + +`watch` + +### Description + +The locked pitch from `BRANDING.md`, ≤ 350 characters. Single line. + +### Input schema (JSON Schema) + +```json +{ + "type": "object", + "properties": { + "url": { + "type": "string", + "format": "uri", + "description": "A social video URL. Supported platforms: YouTube, X / Twitter, LinkedIn, TikTok, Vimeo, Reddit, Facebook." + }, + "frames": { + "type": "integer", + "minimum": 1, + "maximum": 64, + "description": "Number of evenly-spaced frames to extract. Defaults to 8 when omitted. Bound to 64 to keep the response payload small." + } + }, + "required": ["url"], + "additionalProperties": false +} +``` + +The `frames` upper bound of 64 is policy: the CLI itself does not cap, +but the MCP response carries every frame path as a string and the +caller almost always wants a small set. Anything higher is a CLI +invocation, not an MCP tool call. + +### Output + +Returns the v1 JSON object from +[`../docs/output-schema.md`](../docs/output-schema.md) **unchanged**. + +The handler runs `watch --format json []`, parses the +CLI's single-line stdout JSON, and returns it as the MCP tool result +`content[0].text` (a JSON string per the MCP `text` content shape). +Every field passes through verbatim; optional fields +(`transcribe_cost_usd`) stay optional. If the CLI schema bumps to v2, +the server passes v2 through unchanged — clients detect version via +`obj.version`, not via the server. + +--- + +## Wire format + +stdio JSON-RPC 2.0, line-delimited, UTF-8, no embedded newlines per the +MCP transport spec +(https://modelcontextprotocol.io/specification/2025-03-26/basic/transports). +The SDK's `StdioServerTransport` provides framing; the implementation +does not hand-roll it. + +- One JSON-RPC message per line on stdin and stdout, all UTF-8. +- No bare newlines in any field value. +- The server reads stdin to EOF, then exits cleanly. +- The server logs to stderr only. Anything written to stdout that is + not a framed MCP response is a protocol violation. The handler must + capture the CLI's stdout JSON, never let it fall through to the + parent stdout. + +--- + +## Error mapping + +The watch CLI exit codes (see [`../docs/exit-codes.md`](../docs/exit-codes.md)) +map onto MCP error codes as follows. The MCP `code` constants below +are the ones exposed by `@modelcontextprotocol/sdk`. + +| CLI exit | CLI meaning | MCP response shape | +|---|---|---| +| `0` | success | Tool result with the v1 JSON payload as `content[0].text`. No `isError`. | +| `2` | missing dependency (yt-dlp, ffmpeg, etc.) | Throw an SDK error with code `InternalError`. The message string starts with `tag=missing-dep:` so a caller `grep`s the same token as in CLI stderr. | +| `3` | download failed (auth, region, network, other) | Throw an SDK error. **Code depends on tag:** `tag=download-auth` and `tag=download-region` → `InvalidParams` (the caller can recover with cookies or a different URL — these are inputs the caller controls). `tag=download-network` and `tag=download-other` → `InternalError`. Message always includes the tag token verbatim. | +| `4` | transcribe failed — **partial success** | Return the tool result with the v1 JSON payload (`frame_paths` populated, `transcript: null`, `exit_code: 4`). Set `isError: false` on the tool result. The non-zero exit is conveyed via the JSON `exit_code` field, not the MCP error channel — otherwise the caller loses the frames. The implementation logs the `tag=transcribe-*` token to stderr but does not throw. | +| `64` | usage error (bad URL, malformed flag) | Throw an SDK error with code `InvalidParams`. Message includes `tag=usage-error` and the CLI usage line. | +| `1` | uncategorized | Throw an SDK error with code `InternalError`. Message includes whatever the CLI wrote to stderr (truncated to 1 KB). | + +**Locked decisions called out** because the source docs leave them open: + +1. **Exit 4 is success at the MCP layer, partial at the payload layer.** + Preserving the partial-success contract from + [`../docs/exit-codes.md`](../docs/exit-codes.md) means the server + must not throw on exit 4 — otherwise clients would have to recover + frames from an error string. Clients branch on `obj.exit_code === 4`. +2. **Exit 3 splits across two MCP codes by stderr tag.** Auth and + region are caller-recoverable (cookies, different URL) → + `InvalidParams`. Network and other are environment-side → + `InternalError`. Splitting lets clients decide retry vs. ask-user + without parsing prose. +3. **Tags ride in the `message` field**, not in protocol-level fields. + A future MCP revision that adds a stable `tags` field on errors + can migrate without breaking clients. + +--- + +## Lifecycle + +- Starts when invoked via `npx @sonpiaz/watch-cli-mcp` (or local + `dist/index.js` after `npm install -g`). +- Constructs an `StdioServerTransport`, registers the `watch` tool + plus the SDK's `tools/list` and `tools/call` handlers. +- Reads stdin until EOF; on EOF exits `0`. On `SIGTERM` / `SIGINT` + closes the transport cleanly and exits `0`. +- Uncaught exceptions inside a tool handler are caught by the SDK + and surfaced as JSON-RPC error responses; the process keeps running. +- No persistent state between tool calls. Each call shells out to + `watch` and returns. The SDK serializes concurrent calls. + +--- + +## Config snippets + +The `mcp-server/README.md` ships the exact JSON snippets users paste +into each major MCP client's config file. The snippets below are the +canonical text; the implementer copies them into the README verbatim. + +### Claude Desktop + +Config path: + +- macOS: `~/Library/Application Support/Claude/claude_desktop_config.json` +- Windows: `%APPDATA%\Claude\claude_desktop_config.json` +- Linux: `~/.config/Claude/claude_desktop_config.json` + +Snippet (merge into the existing `mcpServers` object): + +```json +{ + "mcpServers": { + "watch-cli": { + "command": "npx", + "args": ["-y", "@sonpiaz/watch-cli-mcp"] + } + } +} +``` + +### Cursor + +Config path: `.cursor/mcp.json` (project) or `~/.cursor/mcp.json` +(user-global). Same `mcpServers` object shape as Claude Desktop. + +### hermes-agent + +Config path: `optional-skills/mcp.json` inside the hermes-agent +install, or `~/.hermes-agent/mcp.json` user-global. Same +`mcpServers` shape. If hermes-agent ships a different path in a +future release, only the README updates; the server code does not. + +--- + +## Test plan + +The implementer must verify all of the following before publishing. + +1. **Boot smoke.** `npx @sonpiaz/watch-cli-mcp` starts, prints nothing + to stdout, accepts an MCP `initialize` request, and responds with + the SDK's capabilities object. +2. **`tools/list` returns `watch`.** Exactly one tool with the right + `name`, locked-pitch `description`, and the input schema above. +3. **`tools/call` happy path.** A known-good public YouTube URL + returns a tool result whose `content[0].text` parses as JSON and + matches the v1 schema (all required fields, `version: 1`, + `exit_code: 0`). +4. **`tools/call` partial success.** A simulated CLI exit-4 (fixture + or known silent-audio URL) returns `isError: false`, frames + populated, `transcript: null`, `exit_code: 4`. The server does + **not** throw. +5. **`tools/call` usage error.** A malformed `url` returns a JSON-RPC + error with code `InvalidParams` and message containing `tag=`. +6. **`tools/call` missing dep.** With `yt-dlp` off `PATH`, expect + error code `InternalError` and message containing + `tag=missing-dep:yt-dlp`. +7. **Manual mcporter integration.** + `mcporter call --stdio "npx -y @sonpiaz/watch-cli-mcp" watch.watch url=` + returns the v1 JSON payload. +8. **Phase 1 regression.** Existing shell tests + (`tests/test-output-schema.sh`, `tests/test-exit-codes.sh`) still + pass against `bin/watch`. The MCP server must be additive — no + change to `bin/`. + +Tests 1–6 live in `mcp-server/test/`. Test 7 is manual (requires +`mcporter` and live network). Test 8 runs in existing CI unchanged. + +--- + +## Publish workflow + +1. **Versioning.** The package version mirrors the CLI version + (`bin/watch` `VERSION` constant). A release PR bumps both in the + same commit. One number answers "is the MCP server in sync with + the CLI?". +2. **Build.** From `mcp-server/`: `npm install && npm run build`. +3. **Pre-publish check.** `npm pack --dry-run` and verify the tarball + contains `dist/`, `README.md`, `package.json` — nothing else. +4. **Publish.** `npm publish --access public`. First publish under a + new scope requires `npm login`. +5. **2FA.** The maintainer's npm account uses TOTP; the CLI prompts + for the code during `npm publish`. No CI auto-publish in Phase 2; + `prepublishOnly` guards against shipping stale `dist/`. +6. **Tag alignment.** The same git tag that gates the GitHub Release + (Phase 4 item #11) gates the npm publish. Pre-releases + (`0.2.0-beta.1`) are allowed without a release tag and the README + marks them as such. + +Future CI improvement, out of scope for Phase 2: a workflow on `v*` +tag push that runs `npm publish --provenance`. Documented in +`docs/releases.md` (Phase 4 sub-spec). diff --git a/mcp-server/package-lock.json b/mcp-server/package-lock.json new file mode 100644 index 0000000..682cc3c --- /dev/null +++ b/mcp-server/package-lock.json @@ -0,0 +1,1197 @@ +{ + "name": "@sonpiaz/watch-cli-mcp", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@sonpiaz/watch-cli-mcp", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.29.0" + }, + "bin": { + "watch-cli-mcp": "dist/index.js" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "typescript": "^5.5.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@hono/node-server": { + "version": "1.19.14", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", + "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", + "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@types/node": { + "version": "22.19.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz", + "integrity": "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.8.tgz", + "integrity": "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz", + "integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.12.21", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.21.tgz", + "integrity": "sha512-uV63apnb0kyPtAUwoWgaGh9HyIFcv8lgmzPZSiTBQAFOFGIzka5EZ1dZocmGnn0XdX0+XTqJ6Tqv7selMuGLRQ==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jose": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", + "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + } + } +} diff --git a/mcp-server/package.json b/mcp-server/package.json new file mode 100644 index 0000000..da5f9bf --- /dev/null +++ b/mcp-server/package.json @@ -0,0 +1,44 @@ +{ + "name": "@sonpiaz/watch-cli-mcp", + "version": "0.1.0", + "description": "Watch any social video → get an architecture diagram, working component, runnable notebook, or step-by-step cheat sheet — automatically. MCP stdio server for watch-cli.", + "type": "module", + "main": "dist/index.js", + "bin": { + "watch-cli-mcp": "dist/index.js" + }, + "files": [ + "dist", + "README.md" + ], + "scripts": { + "build": "tsc -p .", + "start": "node dist/index.js", + "prepublishOnly": "npm run build" + }, + "engines": { + "node": ">=18.0.0" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.29.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "typescript": "^5.5.0" + }, + "homepage": "https://github.com/sonpiaz/watch-cli", + "repository": { + "type": "git", + "url": "git+https://github.com/sonpiaz/watch-cli.git", + "directory": "mcp-server" + }, + "license": "MIT", + "keywords": [ + "mcp", + "model-context-protocol", + "watch-cli", + "video", + "agent", + "transcribe" + ] +} diff --git a/mcp-server/src/index.ts b/mcp-server/src/index.ts new file mode 100644 index 0000000..299b219 --- /dev/null +++ b/mcp-server/src/index.ts @@ -0,0 +1,73 @@ +#!/usr/bin/env node +/** + * watch-cli MCP stdio server entry point. + * + * Registers the `watch` tool, runs over stdio, exits cleanly on EOF or + * SIGINT/SIGTERM. The SDK serializes concurrent tool calls; each call shells + * out to the local `watch` CLI. + * + * Spec: ../SPEC.md + */ + +import { Server } from "@modelcontextprotocol/sdk/server/index.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { + CallToolRequestSchema, + ListToolsRequestSchema, + type CallToolResult, +} from "@modelcontextprotocol/sdk/types.js"; + +import { WATCH_TOOL_DEFINITION, handleWatch, type WatchToolArgs } from "./watch-tool.js"; + +const SERVER_NAME = "watch-cli-mcp"; +const SERVER_VERSION = "0.1.0"; + +async function main(): Promise { + const server = new Server( + { + name: SERVER_NAME, + version: SERVER_VERSION, + }, + { + capabilities: { + tools: {}, + }, + }, + ); + + server.setRequestHandler(ListToolsRequestSchema, async () => ({ + tools: [WATCH_TOOL_DEFINITION], + })); + + server.setRequestHandler( + CallToolRequestSchema, + async (request): Promise => { + if (request.params.name !== WATCH_TOOL_DEFINITION.name) { + throw new Error(`unknown tool: ${request.params.name}`); + } + const args = (request.params.arguments ?? {}) as unknown as WatchToolArgs; + return handleWatch(args); + }, + ); + + const transport = new StdioServerTransport(); + await server.connect(transport); + + // Graceful shutdown — close transport, exit 0. + const shutdown = async () => { + try { + await server.close(); + } finally { + process.exit(0); + } + }; + process.on("SIGINT", shutdown); + process.on("SIGTERM", shutdown); +} + +main().catch((err) => { + process.stderr.write( + `[watch-cli-mcp] fatal: ${err instanceof Error ? err.message : String(err)}\n`, + ); + process.exit(1); +}); diff --git a/mcp-server/src/watch-tool.ts b/mcp-server/src/watch-tool.ts new file mode 100644 index 0000000..38d206f --- /dev/null +++ b/mcp-server/src/watch-tool.ts @@ -0,0 +1,210 @@ +/** + * watch-cli MCP tool handler. + * + * Shells out to `watch --format json []`, parses the v1 JSON + * payload from stdout, and returns it to the MCP caller. + * + * Exit-code mapping lives in mcp-server/SPEC.md § Error mapping. Tags from + * stderr ride in the MCP error message field verbatim so callers can grep + * for `tag=...` like they would on the CLI. + */ + +import { spawn } from "node:child_process"; +import { ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js"; + +export interface WatchToolArgs { + url: string; + frames?: number; +} + +export interface WatchToolResult { + content: Array<{ type: "text"; text: string }>; + isError?: boolean; + // CallToolResult schema in @modelcontextprotocol/sdk is a `$loose` zod object, + // which TypeScript infers with an index signature. Mirror it here so the + // server handler can return us directly without a cast. + [key: string]: unknown; +} + +const STDERR_TAIL_LIMIT = 1024; + +function extractTag(stderr: string): string | null { + const match = stderr.match(/tag=[a-z0-9-]+(?::[a-z0-9._-]+)?/i); + return match ? match[0] : null; +} + +function tailStderr(stderr: string): string { + if (stderr.length <= STDERR_TAIL_LIMIT) return stderr; + return stderr.slice(stderr.length - STDERR_TAIL_LIMIT); +} + +interface CliResult { + exitCode: number; + stdout: string; + stderr: string; +} + +function runWatch(args: string[]): Promise { + return new Promise((resolve, reject) => { + const child = spawn("watch", args, { stdio: ["ignore", "pipe", "pipe"] }); + const stdoutChunks: Buffer[] = []; + const stderrChunks: Buffer[] = []; + + child.stdout.on("data", (b: Buffer) => stdoutChunks.push(b)); + child.stderr.on("data", (b: Buffer) => stderrChunks.push(b)); + + child.on("error", (err: NodeJS.ErrnoException) => { + // ENOENT: `watch` not on PATH. Treat as missing-dep so the caller sees + // the same tag shape the CLI itself would emit on exit 2. + if (err.code === "ENOENT") { + reject( + new McpError( + ErrorCode.InternalError, + "tag=missing-dep:watch — install watch-cli via https://github.com/sonpiaz/watch-cli (curl install.sh | bash)", + ), + ); + return; + } + reject( + new McpError( + ErrorCode.InternalError, + `failed to spawn watch: ${err.message}`, + ), + ); + }); + + child.on("close", (code: number | null) => { + resolve({ + exitCode: code ?? 1, + stdout: Buffer.concat(stdoutChunks).toString("utf8"), + stderr: Buffer.concat(stderrChunks).toString("utf8"), + }); + }); + }); +} + +export async function handleWatch(args: WatchToolArgs): Promise { + if (!args || typeof args.url !== "string" || args.url.length === 0) { + throw new McpError( + ErrorCode.InvalidParams, + "tag=usage-error — required field `url` is missing or empty", + ); + } + if ( + args.frames !== undefined && + (!Number.isInteger(args.frames) || args.frames < 1 || args.frames > 64) + ) { + throw new McpError( + ErrorCode.InvalidParams, + "tag=usage-error — `frames` must be an integer between 1 and 64", + ); + } + + const cliArgs = ["--format", "json", args.url]; + if (args.frames !== undefined) cliArgs.push(String(args.frames)); + + const { exitCode, stdout, stderr } = await runWatch(cliArgs); + const tag = extractTag(stderr); + const stderrTail = tailStderr(stderr).trim(); + + switch (exitCode) { + case 0: { + // Happy path: stdout is one line of v1 JSON. + const payload = stdout.trim(); + // Validate parse but pass the bytes through verbatim so optional fields + // (transcribe_cost_usd) survive the round trip exactly as the CLI wrote + // them. + try { + JSON.parse(payload); + } catch (err) { + throw new McpError( + ErrorCode.InternalError, + `tag=parse-error — CLI exited 0 but stdout was not valid JSON: ${(err as Error).message}`, + ); + } + return { + content: [{ type: "text", text: payload }], + }; + } + + case 4: { + // Partial success: frames populated, transcript null, exit_code=4 in + // the JSON. Do NOT throw — clients branch on obj.exit_code === 4. + const payload = stdout.trim(); + try { + JSON.parse(payload); + } catch (err) { + throw new McpError( + ErrorCode.InternalError, + `tag=parse-error — partial-success exit 4 but stdout was not valid JSON: ${(err as Error).message}`, + ); + } + if (tag) { + // Log tag to stderr for the MCP host operator; not part of the + // payload contract. + process.stderr.write(`[watch-cli-mcp] partial success ${tag}\n`); + } + return { + content: [{ type: "text", text: payload }], + isError: false, + }; + } + + case 2: + throw new McpError( + ErrorCode.InternalError, + `${tag ?? "tag=missing-dep"} — ${stderrTail || "required binary not on PATH"}`, + ); + + case 3: { + const isCallerRecoverable = + tag === "tag=download-auth" || tag === "tag=download-region"; + const code = isCallerRecoverable + ? ErrorCode.InvalidParams + : ErrorCode.InternalError; + throw new McpError( + code, + `${tag ?? "tag=download-other"} — ${stderrTail || "download failed"}`, + ); + } + + case 64: + throw new McpError( + ErrorCode.InvalidParams, + `${tag ?? "tag=usage-error"} — ${stderrTail || "usage error"}`, + ); + + case 1: + default: + throw new McpError( + ErrorCode.InternalError, + `tag=unknown — watch exited ${exitCode}: ${stderrTail || "no stderr"}`, + ); + } +} + +export const WATCH_TOOL_DEFINITION = { + name: "watch", + description: + "Watch any social video → get an architecture diagram, working component, runnable notebook, or step-by-step cheat sheet — automatically.", + inputSchema: { + type: "object" as const, + properties: { + url: { + type: "string", + format: "uri", + description: + "A social video URL. Supported platforms: YouTube, X / Twitter, LinkedIn, TikTok, Vimeo, Reddit, Facebook.", + }, + frames: { + type: "integer", + minimum: 1, + maximum: 64, + description: + "Number of evenly-spaced frames to extract. Defaults to 8 when omitted. Bound to 64 to keep the response payload small.", + }, + }, + required: ["url"], + additionalProperties: false, + }, +}; diff --git a/mcp-server/tsconfig.json b/mcp-server/tsconfig.json new file mode 100644 index 0000000..a650ed1 --- /dev/null +++ b/mcp-server/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "Node16", + "moduleResolution": "Node16", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": false, + "sourceMap": false, + "outDir": "dist", + "rootDir": "src", + "resolveJsonModule": true + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/skills/SKILL-SPEC.md b/skills/SKILL-SPEC.md new file mode 100644 index 0000000..ea9e246 --- /dev/null +++ b/skills/SKILL-SPEC.md @@ -0,0 +1,216 @@ +# SKILL.md specification + +This document is the contract for the portable `SKILL.md` that watch-cli +ships. The actual `SKILL.md` file is authored against this spec during +the Phase 2 implementation; this document is the source of truth for +its shape. + +Cross-references: + +- Output contract the skill body references: [`../docs/output-schema.md`](../docs/output-schema.md) +- Exit codes the skill body references: [`../docs/exit-codes.md`](../docs/exit-codes.md) +- Pitch and tone rules the skill body must follow: [`../BRANDING.md`](../BRANDING.md) + +--- + +## Purpose + +A single `SKILL.md` that is valid in Claude Code, OpenClaw, and +hermes-agent without modification — one file, three platforms. All +three consume the same Anthropic Agent Skills grammar: YAML frontmatter +between `---` markers at the top, prose body underneath. The platform +differences live in optional `metadata.*` blocks that are harmless on +platforms that ignore them. Widening the frontmatter to OpenClaw's +shape unlocks OpenClaw and hermes-agent with zero cost on Claude Code. + +--- + +## Where the canonical file lives + +The canonical file lives at the repository root as `SKILL.md`. A +symlink at `skills/watch-cli/SKILL.md` points to the root file so +existing instructions (the README references `skills/watch-cli/` for +the Claude Code drop-in copy) keep working. + +Per-platform expectations: + +| Platform | Path the platform reads | +|---|---| +| Claude Code | `~/.claude/skills/watch-cli/SKILL.md` (user copies or symlinks `skills/watch-cli/` from the repo into `~/.claude/skills/`). | +| OpenClaw | `skills/watch-cli/SKILL.md` inside the user's OpenClaw project tree, or auto-detected from any compatible bundle that ships a `SKILL.md` with the OpenClaw `metadata.openclaw.*` block. | +| hermes-agent | `optional-skills//watch-cli/SKILL.md` inside the hermes-agent install. Default domain is `research`. | +| Bundled install | `install.sh --with-skill` copies the canonical file into `~/.claude/skills/watch-cli/` on the local machine (Phase 2 item #10 in `.omc/plans/defend-the-niche-4-6-weeks.md`). | + +The repo always ships the canonical file. Per-platform installation is +a copy or symlink step the user (or `install.sh --with-skill`) performs; +no per-platform forks of the markdown are maintained. + +--- + +## Required frontmatter fields + +YAML between two `---` markers at the top of `SKILL.md`. The keys below +are required for the file to be valid in all three target platforms. + +| Key | Type | Value rule | +|---|---|---| +| `name` | string | Always `watch-cli`. Lowercase, hyphenated. Matches the project name from `BRANDING.md`. | +| `description` | string | The locked pitch from `BRANDING.md` verbatim, or a tight variant that keeps the *video → concrete artifact* mapping. Single line. ≤ 350 characters so it renders cleanly in agent UIs. | +| `homepage` | string | `https://github.com/sonpiaz/watch-cli`. | + +--- + +## Optional frontmatter fields + +These keys are read by OpenClaw and ignored by Claude Code and +hermes-agent. They are safe to include unconditionally. + +```yaml +metadata: + openclaw: + emoji: "" + requires: + bins: + - watch + install: + - id: curl-install + kind: shell + command: "curl -fsSL https://raw.githubusercontent.com/sonpiaz/watch-cli/main/install.sh | bash" + bins: + - watch + label: "Install watch-cli (curl)" +``` + +Field-by-field rule: + +| Key | Rule | +|---|---| +| `metadata.openclaw.emoji` | Empty string. `BRANDING.md` bans emoji in docs; OpenClaw renders the field but tolerates empty. Do not invent one. | +| `metadata.openclaw.requires.bins` | Array of strings. List `watch` here so OpenClaw shows an install prompt when `watch` is not on the user's `PATH`. Do not list `yt-dlp`, `ffmpeg`, `jq` — `install.sh` is responsible for surfacing those. | +| `metadata.openclaw.install` | Array. One entry describing the curl one-liner. `id` is a stable token; `kind: shell` tells OpenClaw to run `command` in a shell; `bins` lists what the install provides; `label` is the human-readable button text. | + +The `command` field must contain the exact curl install line currently +in `README.md` § *Install*. If `README.md` updates the install URL, this +field updates with it. + +--- + +## Body content rules + +Everything after the second `---` is prose markdown. The body has the +following sections in this order. + +### 1. Lead paragraph + +The first paragraph after the frontmatter must contain the locked pitch +from `BRANDING.md` (or a tight variant per the rule above). Do not +move it later. This is the line agent UIs surface in skill discovery. + +### 2. When to invoke + +Three to four concrete user signals that should make the agent reach +for `watch`. Each signal is a single bullet, written as the user-facing +verb plus the URL pattern. Examples (illustrative, not the final +wording): + +- The user pastes a video URL with no verb. +- The user asks to "summarize", "explain", or "walk me through" content + at a video URL. +- The user asks to "implement", "clone", "build", or "replicate" what + is in a video. +- The user asks to "extract architecture from" or "turn this paper talk + into code" at a video URL. + +### 3. What you get back + +Plain prose that mirrors the README *What you can build* table. Cover +all five mappings: coding walkthrough → working project files; system +talk → interactive architecture diagram; UI demo → working React +component; paper / research talk → runnable notebook; long tutorial → +step-by-step cheat sheet. No tables in the body — prose is more robust +to skill-host renderers. + +### 4. Parse rules + +Document the v1 output contract by reference, not by duplicating it. + +- Prefer `watch --format json` and parse the single-line JSON + object against the field reference in [`../docs/output-schema.md`](../docs/output-schema.md). +- Text-mode parse rules are a fallback for hosts that capture stdout as + a free-text block (Claude Code does this today). Treat the leading + `WATCH_OUTPUT_VERSION: 1` line as the version signal; everything + below is labeled blocks per the same doc. +- Exit-code behavior — including the partial-success rule (exit 4 with + frames populated and transcript null) — is documented in + [`../docs/exit-codes.md`](../docs/exit-codes.md). Reference it; do not + restate it. + +### 5. Invocation example + +Exactly one invocation line, no code blocks beyond that one line: + +```bash +watch [frame-count] +``` + +Do not include curl install snippets, jq pipelines, multi-step bash +recipes, or Python wrappers in the skill body. The README and the +docs in `docs/` are the place for those. + +--- + +## Anti-patterns + +The skill is rejected (or starts to drift) if it does any of the +following. This list comes directly from OpenClaw manifest review +guidance plus the watch-cli `BRANDING.md` tone rules. + +- **Do not put runtime entrypoints in the frontmatter.** OpenClaw is + explicit: the skill manifest declares dependencies, not runtime + behavior. Let the agent shell out to `watch` based on the body prose + and the `requires.bins` declaration. +- **Do not duplicate README content.** Link to the README and to the + two source-of-truth docs in `docs/`. A long skill body ages faster + than a short one and drifts from the README. +- **Do not localize.** English only. Skill hosts ship globally; the + contract is one language. +- **Do not include API keys, key paths, or environment-variable values + in the body.** Reference `README.md` § *Setup* for `KYMA_API_KEY` + guidance; do not surface the variable name in a way an agent might + paste it into chat. +- **Do not include emoji.** `BRANDING.md` bans them. The + `metadata.openclaw.emoji` field stays empty. +- **Do not embed step-by-step prompt templates.** The five prompts in + `prompts/` are linked from the README; the skill body says "pick a + prompt from `prompts/`" and stops there. + +--- + +## Validation + +Before the implementer commits the authored `SKILL.md`, run these +manual checks. They are not automated in Phase 2; they may become a CI +check in a later phase. + +1. **Frontmatter parses as YAML.** Pipe the frontmatter block through a + YAML parser (`python3 -c 'import sys, yaml; yaml.safe_load(sys.stdin)'` + or `yq .`) and confirm no errors. +2. **Locked pitch matches `BRANDING.md`.** The `description` field is + either character-for-character identical to the locked pitch in + `BRANDING.md`, or a tight variant that keeps the *video → concrete + artifact* mapping. The reviewer judges variants by the rule in + `BRANDING.md` § *Locked one-line pitch*. +3. **Description length.** `description` is ≤ 350 characters so it + renders in skill-host list UIs without truncation. +4. **Required fields present.** `name`, `description`, `homepage` all + present and non-empty. +5. **Symlink works.** `skills/watch-cli/SKILL.md` exists and resolves + to the root `SKILL.md`. From the repo root: `readlink skills/watch-cli/SKILL.md` + returns `../../SKILL.md` (or equivalent). +6. **No emoji in the file.** `grep -P '[\x{1F300}-\x{1FAFF}\x{2600}-\x{27BF}]' SKILL.md` + returns empty. + +Length budget for the authored `SKILL.md`: the body (everything after +the frontmatter) should sit between 80 and 200 lines. Anything longer +duplicates the README; anything shorter usually omits the parse rules +or the "when to invoke" signals. diff --git a/skills/watch-cli/SKILL.md b/skills/watch-cli/SKILL.md deleted file mode 100644 index ba70e5a..0000000 --- a/skills/watch-cli/SKILL.md +++ /dev/null @@ -1,302 +0,0 @@ ---- -name: watch-cli -description: Watch any social video (YouTube, X, LinkedIn, TikTok, Vimeo, Reddit, Facebook) by handing the URL to the local `watch` command. Skill downloads the video, extracts evenly-spaced frames, transcribes the audio, and bundles them as context the agent reasons over. Use when the user shares a video URL and wants you to watch, analyze, summarize, implement, clone, extract architecture from, or build something from it. Triggers on phrases like "watch this video", "analyze this URL", "implement this", "clone this UI", "build what's in this video", "explain this talk", "extract architecture from this video", "turn this paper into code", or when a URL from youtube.com / x.com / twitter.com / linkedin.com / tiktok.com / vimeo.com / reddit.com / facebook.com appears alongside a verb. ---- - -# watch-cli skill - -Give the agent eyes and ears for any social video. - -## When to invoke - -Whenever the user shares a video URL and asks you to do something based -on what's in the video. Examples: - -- "Watch this and explain how it works: " -- "Implement what's in this video: " -- "Extract the architecture from this talk: " -- "Clone this UI: " -- "Turn this paper talk into a notebook: " -- "Summarize this 1h podcast: " - -If the user just pastes a video URL with no verb, ask one clarifying -question: "Want me to summarize, implement, clone the UI, extract the -architecture, or something else?" Then proceed. - -## Workflow - -### 1. Confirm watch-cli is installed - -Run `which watch`. If not found, surface this install command to the -user and stop: - -```bash -curl -fsSL https://raw.githubusercontent.com/sonpiaz/watch-cli/main/install.sh | bash -``` - -### 2. Pick a frame count from the video length and density - -| Video type | `frame-count` | -|---|---| -| Short clip / tweet (<2 min) | 8 (default) | -| Standard tutorial (5–20 min) | 12 | -| Long talk / lecture (20–60 min) | 20 | -| Conference / multi-hour (>1 hr) | 32 | -| Fast-cut or dense UI demo | double the recommendation for that length | - -### 3. Run watch - -```bash -watch -``` - -For login-walled posts (LinkedIn, private X, Facebook) watch-cli -auto-detects browser cookies. Surface any cookie error to the user -before retrying. - -### 4. Parse the output block - -```text -VIDEO: /tmp/dl-video/.mp4 -DURATION: -FRAMES: - /tmp/frames_/frame_01.jpg - /tmp/frames_/frame_02.jpg - … -TRANSCRIPT: - -``` - -Read each `FRAMES` path with the Read tool (Claude understands JPG). -Read the `TRANSCRIPT` inline as the audio. Together they are enough -for you to "watch" the video. - -### 5. Pick the task-specific prompt - -Match the user's intent to one of the inlined prompts below. If the -user's intent is ambiguous, ask one short clarifying question. If they -say "just watch it" or "tell me what's in it", default to a 5-line -plain-English summary. - -| User intent | Inlined prompt to apply | -|---|---| -| Build / implement / replicate | [Implement from video](#prompt-implement-from-video) | -| Extract architecture / system map | [Extract architecture](#prompt-extract-architecture) | -| Clone UI / replicate interaction | [Clone UX](#prompt-clone-ux) | -| Paper / research → code | [Paper to code](#prompt-paper-to-code) | -| Tutorial → step-by-step cheat sheet | [Tutorial walkthrough](#prompt-tutorial-walkthrough) | -| Plain summary / explanation | Just summarize, no prompt needed | - -### 6. Produce the artifact - -Follow the chosen prompt's output contract. Hand the user the artifact, -not a transcript of your reasoning. - -## Cost awareness - -- 5 minutes of video: ~$0.003 transcribe -- 1 hour of video: ~$0.04 transcribe -- 2 hours of video: ~$0.08 transcribe - -Free Kyma credit at signup covers roughly 25 hours of audio. For videos -longer than 1 hour, mention the rough cost before running. Skip the -disclaimer for short videos. - -## Common failures - -| Error | Cause | Fix | -|---|---|---| -| `cookies not found` (LinkedIn / X / FB) | User not signed in to that platform in default browser | Sign in, retry. Or pass `--cookies ` with a manual export | -| `25MB cap exceeded` | Video too long for one transcribe call | Split via `ffmpeg -ss -t ` and watch each chunk | -| `403 region locked` | yt-dlp can't fetch from this region | Ask the user for an alternate URL or a local file | -| Empty `TRANSCRIPT` | Silent video | Increase frame count, also run `audio-q "describe the sound design"` | - ---- - -## Prompt: Implement from video - -Apply when the user wants a runnable project that replicates what was -built on screen. - -``` -You are a senior engineer pair-programming with the user. They have -just handed you a video they want to replicate. - -Below is the video's evenly-spaced frames (read each FRAMES path as an -image) and the full audio transcript. Treat the frames as ground truth -for visible code, file names, and UI. Treat the transcript as the -narrator's intent and rationale. - -Your job: - -1. Reconstruct the project structure visible on screen. Use the exact - filenames, folder layout, dependencies, and CLI commands that appear - in the frames. If something is ambiguous, prefer what the narrator - says over what you infer. -2. Produce the final state of every file the video ends with, not the - intermediate edits. The user wants a working clone, not a replay. -3. Write a short README explaining what this project does, how to run - it locally, what was clipped in the video and why your reconstruction - filled the gap. -4. List anything you could not confidently recover (a config that - scrolled off-screen, a redacted secret). Mark those as "TODO: confirm - from source video at ". - -Do not hallucinate libraries. If you cannot see or hear a dependency, -use the most idiomatic choice for the stack and flag it. -``` - ---- - -## Prompt: Extract architecture - -Apply when the user wants a self-contained interactive diagram of a -system someone is describing. - -``` -You are a systems architect. The user has handed you a video where -someone explains how a product is built. Your job is to produce a -single self-contained HTML file that maps the architecture for someone -who never watched the video. - -Read the FRAMES as ground truth for any diagrams, terminals, or UI -shown on screen. Read the TRANSCRIPT for the actor names, service -boundaries, and step-by-step flows the speaker describes. - -Output: one HTML file, no build step, Tailwind via CDN. Three columns: - -1. Actors (people / systems initiating action) -2. Surfaces (the things actors touch: dashboards, SDKs, public URLs) -3. APIs / data stores / external services - -On the right, a clickable list of named flows. When a flow is selected, -highlight the actors, surfaces, and endpoints it passes through, and -show a numbered Steps panel below it with who acts, what they call, -the payload shape, and the data that gets written. - -Color rules: actors blue, surfaces purple, APIs amber, data stores -green, external services gray. Dim when not part of the selected flow. - -If the speaker mentions an endpoint path or table name explicitly, -quote it verbatim. If you have to infer one, mark it with a tilde -prefix (~assumed-path/foo). - -Do not invent flows the speaker did not describe. Three real flows -beats ten fabricated ones. -``` - ---- - -## Prompt: Clone UX - -Apply when the user wants a working React component that captures the -feel of a UI shown on screen. - -``` -You are a senior frontend engineer with deep taste. The user has -handed you a video showing a UI they want to clone, not pixel-for-pixel -but feel-for-feel. - -Read the FRAMES as ground truth for layout, spacing, color, typography -hierarchy, and motion direction. Read the TRANSCRIPT only if the -designer narrates intent. - -Output: a single React component file (TypeScript, Tailwind, -framer-motion allowed) that captures the interaction. Match the cadence -and easing you see on screen, not just the end state. - -Rules: - -1. Identify the single moment that makes this UI special and protect - it. Most demos hinge on one interaction; the rest is wrapping. -2. Use real typography. If the video uses an obvious system or open - font (Inter, IBM Plex, system-ui, serif headings), match it. If you - cannot tell, default to Inter and say so. -3. State management stays local. No Redux, no context, no router. -4. Output one paste-ready .tsx file plus a 5-line "how to run" note. - If the component depends on a 3rd-party lib, list the install - commands at the top of the file as a comment. - -Anti-patterns: -- Do not pad the file with placeholder content the video did not show. -- Do not ship a Storybook story unless asked. -- Do not rewrite the user's design system; use Tailwind utility classes. -``` - ---- - -## Prompt: Paper to code - -Apply when the user wants a runnable notebook reproducing a method -explained in a talk. - -``` -You are a research engineer reproducing a paper from a talk. The user -has handed you a video where the author or a presenter explains a -method. - -Read the FRAMES as ground truth for math, architecture diagrams, -pseudocode, and result tables. Read the TRANSCRIPT for the intuition, -assumptions, and any clarifications the speaker adds beyond the slides. - -Output: one Jupyter notebook (.ipynb as JSON) implementing the core -method on a toy dataset that runs end-to-end on a free Colab T4. - -Cells, in order: - -1. Markdown — paper title, talk URL, one-paragraph plain-English summary. -2. Imports — only what you use. -3. The method itself — one minimal implementation, no premature abstraction. -4. Toy dataset — synthetic or a small public set. Justify the choice. -5. Training / inference loop — short and observable. Print loss / metric - every N steps. -6. Results — a table or figure comparing your run to what the speaker - claims at the end of the talk. -7. Markdown — what you simplified vs. the paper, where the speaker was - vague, what would be needed to reproduce the headline number. - -Rules: -- Prefer torch over jax unless the speaker explicitly uses jax. -- No proprietary datasets. Use what a stranger can run. -- Cite the paper formally at the top with a bibtex block. -- If the talk skipped a derivation, do not fabricate one. Note it. -``` - ---- - -## Prompt: Tutorial walkthrough - -Apply when the user has a long tutorial they want as a cheat sheet they -can follow on their own machine. - -``` -You are a patient teacher. The user has handed you a long tutorial -video and they want the cheat sheet version they can follow on their -own machine without rewinding. - -Read the FRAMES for commands, file content, and UI clicks. Read the -TRANSCRIPT for the reasoning and any "actually wait, do this instead" -corrections the slides don't show. - -Output: one markdown walkthrough with these sections: - -1. **What you'll build** — 2 sentences, ground truth from the end state - in the last 30 seconds of the video. -2. **Prerequisites** — exact versions of tools / SDKs / accounts. If - the video assumes them silently, surface them anyway. -3. **Steps** — numbered. Each step has: - - A 1-line goal - - Exact copy-pasteable commands or code - - Any "if you see X, do Y" branches the speaker mentions - - A line citing roughly where in the video this step happens - (e.g. "≈4:30 in the video") -4. **Verification** — how to confirm each major step worked before - moving to the next. -5. **Troubleshooting** — only items the speaker actually discussed. No - generic Stack Overflow boilerplate. -6. **What was clipped** — note any moment where the speaker cut away, - and what reasonable default to fill in. - -Tone: imperative, second person. "Run `npm install`", not "You should -probably run npm install". -``` diff --git a/skills/watch-cli/SKILL.md b/skills/watch-cli/SKILL.md new file mode 120000 index 0000000..4215fae --- /dev/null +++ b/skills/watch-cli/SKILL.md @@ -0,0 +1 @@ +../../SKILL.md \ No newline at end of file