diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index cb03ce0..de29d59 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,13 +1,27 @@ { "name": "jam", - "version": "1.23.2", - "description": "Jam's MCP is the fast lane between Jam recordings and your dev tools. Drop a Jam link into your coding agent and the whole recording – video, console, network, logs – arrives pre-packaged.", - "author": { "name": "Jam", "email": "support@jam.dev" }, + "version": "2.0.0", + "description": "Jam recordings (video, console, network, logs) as context for your coding agent. Fix the bug in a Jam, prove a fix with a recorded Jam, mark up a screenshot, or turn a Jam into an end-to-end test.", + "author": { + "name": "Jam", + "email": "support@jam.dev" + }, "homepage": "https://jam.dev", "repository": "https://github.com/jamdotdev/skills", "license": "MIT", - "keywords": ["jam", "bug-reports", "debugging", "mcp", "session-replay", "observability"], + "privacyPolicyUrl": "https://jam.dev/legal/privacy-policy", + "keywords": [ + "jam", + "bug-reports", + "debugging", + "mcp", + "session-replay", + "observability" + ], "mcpServers": { - "Jam": { "type": "http", "url": "https://mcp.jam.dev/mcp" } + "Jam": { + "type": "http", + "url": "https://mcp.jam.dev/mcp" + } } } diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index bd6b120..2c42105 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "jam", - "version": "1.23.2", - "description": "Investigate Jam bug reports using recordings, console logs, network requests, and user events.", + "version": "2.0.0", + "description": "Jam recordings (video, console, network, logs) as context for your coding agent. Fix the bug in a Jam, prove a fix with a recorded Jam, mark up a screenshot, or turn a Jam into an end-to-end test.", "author": { "name": "Jam", "email": "support@jam.dev" @@ -21,8 +21,8 @@ "skills": "./skills/", "interface": { "displayName": "Jam", - "shortDescription": "Investigate Jam bug reports", - "longDescription": "Investigate Jam bug reports using recordings, console logs, network requests, and user events.", + "shortDescription": "Fix, prove and test from Jam recordings", + "longDescription": "Jam recordings (video, console, network, logs) as context for your coding agent. Fix the bug in a Jam, prove a fix with a recorded Jam, mark up a screenshot, or turn a Jam into an end-to-end test.", "developerName": "Jam", "category": "Developer Tools", "capabilities": [ @@ -31,7 +31,7 @@ "Write" ], "websiteURL": "https://jam.dev", - "defaultPrompt": "Investigate this Jam link and explain the likely root cause.", + "defaultPrompt": "Use $jam-fix to find the root cause of the bug in this Jam link.", "logo": "./assets/logo.png", "composerIcon": "./assets/logo.png", "privacyPolicyURL": "https://jam.dev/legal/privacy-policy", diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json index 231a194..cece108 100644 --- a/.cursor-plugin/plugin.json +++ b/.cursor-plugin/plugin.json @@ -1,8 +1,8 @@ { "name": "jam", "displayName": "Jam", - "version": "1.23.2", - "description": "Jam's MCP is the fast lane between Jam recordings and your dev tools. Drop a Jam link into Cursor and the whole recording – video, console, network, logs – arrives pre-packaged.", + "version": "2.0.0", + "description": "Jam recordings (video, console, network, logs) as context for Cursor. Fix the bug in a Jam, prove a fix with a recorded Jam, mark up a screenshot, or turn a Jam into an end-to-end test.", "author": { "name": "Jam", "email": "support@jam.dev" diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml new file mode 100644 index 0000000..b36e105 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -0,0 +1,25 @@ +name: Skill bug +description: A skill did the wrong thing, or names a command or tool that doesn't exist. +labels: [bug] +body: + - type: dropdown + id: skill + attributes: + label: Skill + options: [jam-fix, jam-proof, jam-annotate, jam-to-e2e, plugin or MCP setup, other] + validations: + required: true + - type: input + id: agent + attributes: + label: Agent and version + placeholder: Claude Code 2.1, Cursor 2.4, Codex 0.50... + validations: + required: true + - type: textarea + id: what + attributes: + label: What happened, and what you expected + description: Paste the prompt you gave and the step where the agent went wrong. Redact tokens and customer data. + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..0a778be --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Jam product support + url: mailto:support@jam.dev + about: Questions about Jam itself, your account or billing. diff --git a/.github/ISSUE_TEMPLATE/skill-request.yml b/.github/ISSUE_TEMPLATE/skill-request.yml new file mode 100644 index 0000000..5e92114 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/skill-request.yml @@ -0,0 +1,18 @@ +name: Skill idea +description: A job coding agents should do with a Jam, about QA, pull requests or proof of work. +labels: [skill-request] +body: + - type: textarea + id: job + attributes: + label: The job + description: 'What goes in and what comes out. For example, "PR diff in, five things a tester must check by hand out."' + validations: + required: true + - type: textarea + id: why + attributes: + label: What agents get wrong today + description: What happens when you ask an agent to do this without a skill? + validations: + required: true diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..62b59be --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,36 @@ +name: CI + +on: + pull_request: + push: + branches: [master] + +permissions: + contents: read + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-node@v7 + with: + node-version: 24 + cache: npm + + - name: Install check dependencies + run: npm ci + + - name: Regression tests + run: npm test + + - name: Repo checks + run: node scripts/check.mjs + + - name: Plugin manifest + run: npx -y @anthropic-ai/claude-code@latest plugin validate . --strict + + - name: skills.sh sees every skill + run: | + npx -y skills@latest add . --list skills.txt 2>&1 + node scripts/check.mjs --skills-list skills.txt || { cat skills.txt; exit 1; } diff --git a/.github/workflows/publish-mcp.yml b/.github/workflows/publish-mcp.yml index 29d62af..fea40bc 100644 --- a/.github/workflows/publish-mcp.yml +++ b/.github/workflows/publish-mcp.yml @@ -2,25 +2,25 @@ name: Publish to MCP Registry on: push: - tags: ["v*"] + tags: ["mcp-v*"] workflow_dispatch: jobs: publish: runs-on: ubuntu-latest - # MCP_PRIVATE_KEY is an environment secret; the environment only allows v* tags. + # MCP_PRIVATE_KEY is an environment secret; the environment only allows mcp-v* tags. environment: mcp-registry-publish permissions: contents: read steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - name: Check server.json version matches tag - if: startsWith(github.ref, 'refs/tags/v') + if: startsWith(github.ref, 'refs/tags/mcp-v') run: | - TAG="${GITHUB_REF#refs/tags/v}" + TAG="${GITHUB_REF#refs/tags/mcp-v}" V="$(jq -r .version server.json)" - test "$TAG" = "$V" || { echo "tag v$TAG != server.json $V"; exit 1; } + test "$TAG" = "$V" || { echo "tag mcp-v$TAG != server.json $V"; exit 1; } - name: Install mcp-publisher run: curl -sL https://github.com/modelcontextprotocol/registry/releases/download/v1.8.1/mcp-publisher_linux_amd64.tar.gz | tar xz mcp-publisher diff --git a/.gitignore b/.gitignore index 4e403f2..52a6171 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ tasks/ *.pem mcp-registry-auth .mcpregistry_* +node_modules/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..7bd95ed --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,24 @@ +# AGENTS.md + +Guidance for coding agents working in this repository. Humans: see [CONTRIBUTING.md](./CONTRIBUTING.md), which holds the rules every skill follows. + +## Layout + +- `skills//SKILL.md`: the skills. Each folder also has `agents/openai.yaml`. +- `.claude-plugin/`, `.codex-plugin/`, `.cursor-plugin/`, `gemini-extension.json`: plugin manifests. `mcp.json` is the shared MCP server entry. +- `server.json`: the MCP Registry entry `dev.jam/jam`. Its `version` is the Jam MCP server's, not the plugin's. Pushing an `mcp-v*` tag publishes it, and a published version is permanent. Never push a tag unless asked. +- `rules/`: a Cursor rule that guards the Jam MCP delete tools. +- `scripts/package-chatgpt.mjs`: the ChatGPT directory package. A skill that reads a Jam over MCP alone goes into its `SKILLS` list. +- `docs/plugin.md`: plugin sign-in, what the MCP server can do, permissions. The tool list itself lives at jam.dev/mcp; do not copy it here. +- `scripts/check.mjs`: repo checks, run by CI. + +## Invariants + +Run `node scripts/check.mjs` after any change. It prints each rule it finds broken, with the file. + +Also: + +- When you add, rename or remove a skill, update its README card, the CHANGELOG, and any skill that names it. +- `jam-proof` and `jam-annotate` are also bundled in the Jam CLI. This repo is the source; tell the user the bundle needs a resync when you change them. +- Only name `jam` CLI commands and MCP tools that ship today. Check with `jam agent-context`. +- Skills are public. No internal paths, repo names, staging hosts or customer data. diff --git a/CHANGELOG.md b/CHANGELOG.md index 86e14a8..a87becc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,30 @@ ## Unreleased +## 2.0.0 + +The plugin is now versioned on its own and no longer tracks the Jam MCP server version. + +- Renamed `investigate-bug` to `jam-fix`, because "fix this Jam" is what people ask for. It diagnoses first, changes code only when asked, and posts findings on the Jam when requested. Its description carries trigger phrases instead of slash-command syntax, and it covers the `jam` CLI when MCP is not connected. +- Added `jam-proof`: record a change working and return a Jam link for the pull request. Moved from the Jam CLI bundle; this repo is now its source. +- Added `jam-annotate`: mark up a screenshot with boxes, arrows, notes, redactions and blurs, and file it as a Jam. Moved from the Jam CLI bundle; this repo is now its source. +- Added `jam-to-e2e`: turn a Jam into steps and an end-to-end test in the project's own runner and language, with first-class guidance for Playwright and tester-army/e2e. A flow Jam becomes a passing test that guards it; a bug Jam becomes a failing test for the fix to turn green. +- Removed `jam-cli`. Each `jam-*` skill now carries its own install and sign-in steps, and `jam agent-context` prints the full command surface. The old skill named `jam get intents` and `jam get events` for user events; 1.29.0 ships `jam get user-events` and no `intents`. +- Every skill now ends with a "Send feedback to Jam" section: the agent reports a wrong, stale or confusing Jam tool, command, doc or skill (or one that worked unusually well) with `jam feedback` or the `submitFeedback` MCP tool, without asking first, at most once per session. `docs/plugin.md` and the Cursor rule now match the server: no approval step, bodies up to 2,000 words. +- Added `agents/openai.yaml` to every skill for the Codex skill picker. +- Added `scripts/package-chatgpt.mjs`, which builds the ChatGPT directory package from `jam-fix` and `jam-to-e2e` with listing copy that names only those two. +- Rewrote the README around the skills, with `npx skills add jamdotdev/skills` first. Moved sign-in, the MCP tool list and permissions to `docs/plugin.md`, and the release steps to `CONTRIBUTING.md`. +- Added `CONTRIBUTING.md`, `AGENTS.md`, issue templates, and a CI workflow that checks skill frontmatter, README cards, manifest versions, Markdown links, the Claude Code plugin manifest, and that the skills CLI lists every skill. +- Added `privacyPolicyUrl` to the Claude Code plugin manifest. +- Cut the copies of the MCP tool guide. `docs/plugin.md` now summarizes what the server can do and links to jam.dev/mcp, the Cursor rule keeps only the confirm-before-delete rule and points at `jam-fix`, and `jam-fix` leaves filter and paging details to the tool descriptions. +- Moved `jam-proof`'s cursor overlay and visible-Chrome details to `references/browser.md`. - Renamed the repository to `jamdotdev/skills`. Install commands and manifest URLs now point there; the `jam-plugins` marketplace name is unchanged. - Pointed the MCP website and docs links at `jam.dev/mcp`, and the CLI links at `jam.dev/cli`. -- Added `server.json` for the official MCP Registry (`dev.jam/jam`) and a tag-triggered publish workflow. +- Added `server.json` for the official MCP Registry (`dev.jam/jam`) and a publish workflow triggered by `mcp-v*` tags. Its version follows the Jam MCP server, not the plugin. +- Validate skill frontmatter with a YAML parser and require exact discovery entries, so malformed skills cannot pass CI by appearing in a warning. +- Keep local annotation requests local and require posting intent for Jam comments, mentions, pull requests and tickets. +- Give CLI investigations both client and server failures, and require an explicit browser or window target for proof recordings. +- Use the project's browser mode for regression tests and isolated checkouts for new before-and-after recordings. ## 1.23.2 @@ -14,7 +35,7 @@ ## 1.5.0 -- Synced with deployed Jam MCP 1.23.2 at [619c85f885c3](https://github.com/jamdotdev/apiofjam/commit/619c85f885c3782f77a49ef0bc68c4d87222d097), verified through production server initialization and its 35-tool inventory. +- Synced with deployed Jam MCP 1.23.2, verified through production server initialization and its 35-tool inventory. - Added `searchJams` guidance and marked `listJams` deprecated. Added `submitFeedback` with exact-text approval, privacy limits, and at most one submission per session. - Corrected event filter examples to arrays and documented cursor pagination, total counts, and network response-body limits. - Documented video thumbnail links and CLI proof capture guidance. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 0000000..47dc3e3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..ffbe907 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,76 @@ +# Contributing + +Thanks for helping. This repo holds small agent skills about Jams (screen recordings with the console, network and user actions attached), QA, pull requests and proof of work. Read the rules below before you open a pull request; most review comments come from them. + +## The rules + +1. **One job per skill.** A skill does one thing well. Two jobs are two skills. +2. **The name tells the truth.** A skill named `jam-*` needs a Jam account. A skill without `jam` in its name works fully without Jam, and mentions Jam at most once, in a line labeled "With Jam" next to the alternatives. +3. **No pitch.** No sales copy inside a `SKILL.md`. Jam lives in the README. +4. **Do what agents can't, or give them structure they lack.** Visuals for humans (proof video, marked-up screenshots, diagrams) and evidence structure (claim, how it was verified, evidence, gaps). Not things agents already do well, such as reading a HAR file. +5. **Claim only what ships.** Every command, flag and tool a skill names must exist in the released Jam CLI or MCP server today. Check with `jam agent-context` or the [MCP docs](https://jam.dev/mcp). +6. **Use the project's tools.** A skill uses the project's existing test runner, browser driver or setup, and falls back to Playwright when there is none. + +## Anatomy of a skill + +``` +skills// + SKILL.md required + agents/openai.yaml required: Codex picker metadata + references/ optional: depth the agent loads on demand + scripts/ optional: scripts the skill runs +``` + +- **Name:** 2 or 3 words, kebab-case, verb first (`prove-change`) or `x-to-y` (`jam-to-e2e`). No single words, no `verify-*`. The folder name equals the `name` in the frontmatter. +- **Description:** what it does, then when to use it, with the phrases a user would type ("Use when the user pastes a jam.dev link..."). The agent decides to load the skill from this line alone. 1024 characters at most. +- **Length:** keep `SKILL.md` under about 200 lines. Move detail to `references/` and link it from the step that needs it. +- **Style:** imperative, concrete, no filler. Show the command, not a description of the command. Say what to do when a step fails. +- **Feedback:** every `jam-*` skill ends with the same `## Send feedback to Jam` section. Copy it from an existing skill; CI fails if it differs. +- **Secrets:** never ask the agent to write a token to a file. Use environment variables. +- **Descriptions use the user's words, not ours.** Write what people type: "jam link", "bug recording", "screen recording", "repro video", "proof for the PR". Say what a Jam is the first time a description names it; the model may never have heard of Jam. +- **Jam terms, in prose only:** `Jam` is always capitalized and never a verb (record a Jam, share a Jam). The hierarchy is workspace > folder > Jam; a person with a seat is a member. Write `Sharepage`, `Recording Link`, `Jam MCP`, `Jam CLI`, and `Model Context Protocol (MCP)` on first mention in a page. You capture a screenshot and record a video. +- **Claims:** no "only", "first", "best" or guarantees. State what the skill does and its limits. + +## Test a skill locally + +```bash +npm ci # install the locked YAML validator +node scripts/check.mjs # the same checks CI runs +npm test # validation and package regressions +npx skills add . --list # what skills.sh will see +claude --plugin-dir . # load the plugin in Claude Code +``` + +Then run the skill on a real task in at least one agent and say which one in the pull request. For a `jam-*` skill, include the Jam link it produced. + +## Pull requests + +- One skill per pull request. Maintainers may ship a coordinated release of several in one. +- Add a line under **Unreleased** in [CHANGELOG.md](./CHANGELOG.md). +- A new skill also needs a card in [README.md](./README.md). CI fails without one. + +## Releasing (maintainers) + +Two things ship from this repo, each with its own version. + +**The plugin.** Its version is the same in `.claude-plugin/plugin.json`, `.codex-plugin/plugin.json`, `.cursor-plugin/plugin.json` and `gemini-extension.json`; CI checks this. + +1. Bump the version in those four files and move **Unreleased** in the changelog under the new version. +2. Merge to `master` and push a `v` tag. + +**The MCP Registry entry.** `server.json` describes the remote Jam MCP server at `mcp.jam.dev`, so its `version` is the server's, not the plugin's. When the server releases: + +1. Set `version` in `server.json` to the server's version and merge to `master`. +2. Push an `mcp-v` tag. `.github/workflows/publish-mcp.yml` publishes `server.json` to the registry as `dev.jam/jam`. A registry version can't be changed after it is published. + +### ChatGPT directory package + +The ChatGPT directory takes the skills that read a Jam over MCP alone: `jam-fix` and `jam-to-e2e`, without their `references/`. The CLI install steps (in `jam-proof`, `jam-annotate` and the `references/cli.md` files) download and run code, which fails ChatGPT's external-code review, so a skill meant for ChatGPT keeps its install steps in `references/` and is added to `SKILLS` in `scripts/package-chatgpt.mjs`. Build the package from `master` after the merge: the packaged skills link to `references/` on GitHub. + +```bash +node scripts/package-chatgpt.mjs ../jam-chatgpt-.zip +``` + +The script copies those two skills with the plugin identity, connection and assets, and rewrites the listing description so it names only what the package contains. + +Update the existing app entry with this package. Do not create a new app through **Upload New**; that loses the saved identity and connection settings. diff --git a/README.md b/README.md index ed77dbd..9f41c40 100644 --- a/README.md +++ b/README.md @@ -4,271 +4,131 @@

-

Jam plugin

+

Jam skills

- Give your coding agent the full story of a bug, in one Jam. + Skills that make coding agents show their work: read a Jam, prove a change, point at the problem, test a flow.

- Jam MCP docs · - Jam CLI docs · - jam.dev + skills.sh + CI + MIT


---- - -Paste a Jam link into your coding agent, and it reads the recording, console logs, network requests, and user events on its own. You skip typing out repro steps and copying stack traces. - -The plugin works in Cursor, Claude Code, Codex, GitHub Copilot CLI, and Gemini CLI. In each one, it connects the Jam Model Context Protocol (MCP) server and adds two skills. - -## Install the plugin - -### Cursor - -Open **Customize** in the Cursor sidebar, search for **Jam**, and click **Install**. - -To test an unreleased version, link the repository into Cursor's local plugin folder: - ```bash -ln -s /path/to/skills ~/.cursor/plugins/local/jam -``` - -Then run **Developer: Reload Window** from the command palette (`Cmd+Shift+P`). - -### Claude Code - -```shell -/plugin marketplace add jamdotdev/skills -/plugin install jam@jam-plugins +npx skills add jamdotdev/skills ``` -The skills load as `/jam:investigate-bug` and `/jam:jam-cli`. To test an unreleased version, start Claude Code with `claude --plugin-dir /path/to/skills`. - -### Codex +Works with Claude Code, Cursor, Codex, GitHub Copilot, Gemini CLI, OpenCode and [every agent skills.sh supports](https://skills.sh). Add `--skill jam-fix` to install one skill. -```bash -codex plugin marketplace add jamdotdev/skills -codex plugin add jam@jam-plugins -``` +A Jam is a recording of what happened on screen, with the console logs, network requests and user actions attached, on one link. A bug report, a demo, a walkthrough of a flow. These skills teach your agent to read one, and to make one. -Codex uses `.codex-plugin/plugin.json` and the existing `jam-plugins` marketplace. It loads the Jam MCP server and the shared `jam:investigate-bug` and `jam:jam-cli` skills. Complete the OAuth prompt to sign in to Jam. +## Skills -To test an unreleased checkout, add its local marketplace instead: +| Skill | You say | You get | +| --- | --- | --- | +| [jam-fix](#jam-fix) | "fix this jam.dev link" | the root cause with evidence, and the fix when you ask | +| [jam-proof](#jam-proof) | "record a Jam showing it works" | a Jam of the change working, console and network attached | +| [jam-annotate](#jam-annotate) | "mark what's broken and file it" | a screenshot Jam with boxes, arrows and redactions | +| [jam-to-e2e](#jam-to-e2e) | "write an e2e test for this Jam" | a test in your own runner: guards a flow, or fails on a bug until it is fixed | -```bash -codex plugin marketplace add /path/to/skills -codex plugin add jam@jam-plugins -``` +### jam-fix -Start a new Codex session after installation. Run `/mcp` to check the Jam connection, then ask Codex to investigate a Jam link. +Reads the recording's console errors, failed requests, user steps, frames and transcript, then writes a diagnosis with the evidence for it. Changes code only when you ask. -### GitHub Copilot CLI +### jam-proof -```bash -copilot plugin marketplace add jamdotdev/skills -copilot plugin install jam@jam-plugins -``` +Records the change working and hands back a Jam link for the pull request. Headless Chrome by default: it records the page, not your screen. -### Gemini CLI +### jam-annotate -```bash -gemini extensions install https://github.com/jamdotdev/skills -``` +Turns a screenshot into a Jam that points at the problem: boxes, arrows, notes, redactions and blurs, with the retina math done right. -Run `/mcp auth Jam` once to sign in. To test an unreleased version, run `gemini extensions link /path/to/skills`. +### jam-to-e2e -## Sign in +Turns a Jam into steps and a test in the project's own runner and language, whatever it is, with first-class guidance for Playwright and [tester-army/e2e](https://github.com/tester-army/e2e). A Jam of a flow becomes a test that guards it. A Jam of a bug becomes a failing test, so the fix has something to turn green. -### OAuth +## Requirements -The plugin connects to `https://mcp.jam.dev/mcp`. Your agent prompts you to sign in to Jam in a browser during installation or when you first use a Jam tool. There is nothing else to set up. +Every skill needs a free [Jam](https://jam.dev?utm_source=github&utm_medium=readme&utm_campaign=skills) account. `jam-fix` and `jam-to-e2e` read a Jam through the Jam MCP server or the [Jam CLI](https://jam.dev/cli); `jam-proof` and `jam-annotate` create Jams, which needs the CLI. A skill installs the CLI and signs in when it needs it. In CI or a cloud agent, set `JAM_TOKEN` to a [personal access token](https://jam.dev/docs/personal-access-tokens). Window and display capture in `jam-proof` need macOS or Linux (X11); its default, recording a headless browser, needs only Chrome. -### Personal access token +## Install as a plugin -Use a token when a browser sign-in isn't possible, such as in CI or a headless environment. +The plugin installs every skill, connects the [Jam MCP server](./docs/plugin.md) and updates itself. Use the plugin or `npx skills add`, not both, or the agent sees every skill twice. -1. Create a token in [**Settings → MCP**](https://jam.dev/s/settings/mcp) under **Personal Access Tokens**. Copy it right away, because Jam stores only a hash. -2. Add a `headers` block to the Jam server entry in your agent's MCP config. In Cursor, the file is `mcp.json`: +
+Claude Code -```json -{ - "mcpServers": { - "Jam": { - "url": "https://mcp.jam.dev/mcp", - "headers": { - "Authorization": "Bearer YOUR_PERSONAL_ACCESS_TOKEN" - } - } - } -} +```shell +/plugin marketplace add jamdotdev/skills +/plugin install jam@jam-plugins ``` -Each token works in one workspace, belongs to your account, and expires after seven days, 30 days, 90 days, or one year. You pick the length when you create it. See [Personal access tokens](https://jam.dev/docs/personal-access-tokens) for details. - -## Check that it works - -1. Confirm **Jam** is in your agent's list of MCP servers. In Cursor, open **Customize**. In Claude Code, Codex, or Gemini CLI, run `/mcp`. In GitHub Copilot CLI, run `copilot mcp list`. -2. Paste a Jam link into the chat and ask the agent to analyze it. Complete the browser sign-in if prompted. -3. Run the `investigate-bug` skill on a Jam link, such as `https://jam.dev/c/`. - -## What's included - -### Jam MCP server - -The server at `https://mcp.jam.dev/mcp` has 35 tools. - -**Investigate a Jam** +Skills load as `/jam:jam-fix`, `/jam:jam-proof`, and so on. To test a checkout, run `claude --plugin-dir /path/to/skills`. -| Tool | What it does | -|------|-------------| -| `getDetails` | Returns the Jam overview, device info, and the server's investigation guide | -| `getNetworkRequests` | Lists HTTP requests. Filter by status, method, host, or content type | -| `getConsoleLogs` | Lists console output. Filter by log level | -| `getUserEvents` | Lists clicks, inputs, navigation, and scrolls in order | -| `getScreenshots` | Returns the images from a screenshot Jam | -| `getFrames` | Returns still frames from a video Jam, as a grid or at chosen timestamps | -| `analyzeVideo` | Describes what the user was trying to do in a video | -| `getVideoTranscript` | Returns the transcript of a video Jam recorded with the mic on | -| `getVideoChapters` | Returns titled video chapters with start and end times, plus thumbnail links when available | -| `getMetadata` | Returns custom metadata the page sent with `jam.metadata()` | -| `search` | Finds a video Jam from a link or pasted text. For screenshot and replay Jams, use `searchJams` | -| `fetch` | Same as `getDetails` | +
-Network filters (`statusCode`, `contentType`, `host`, and `method`) and the console `logLevel` filter take arrays. Values within a filter are ORed; separate filters are ANDed. Network, console, and user-event responses report `total` and `nextCursor`. Pass `nextCursor` as `after` to continue. Network response bodies default to failed requests (`bodies: "errors"`); `bodies: "all"` includes successful responses too. Bodies are truncated to 4KB and flagged per row. +
+Cursor -**Find and organize Jams** +Open **Customize** in the sidebar, search for **Jam**, and click **Install**. -| Tool | What it does | -|------|-------------| -| `searchJams` | Searches workspace Jams by text and structured filters, with date bounds and cursor pagination | -| `listJams` | Deprecated. Uses the legacy search filters and pagination; use `searchJams` | -| `listFolders` | Lists the folders in your workspace | -| `createFolder` | Creates a folder | -| `updateFolder` | Renames a folder. The Jams inside stay where they are | -| `listMembers` | Finds members of your workspace | -| `createComment` | Adds a Markdown comment to a Jam | -| `editComment` | Edits a comment you wrote | -| `addReaction` | Adds a reaction to a comment | -| `removeReaction` | Removes a reaction you added | -| `updateJam` | Renames a Jam, edits its description, or moves it to another folder. Editing the title or description needs an Admin or Creator seat | - -Call `searchJams` with `v: 1`. Use `text` for text search, `where` for ANDed `{ field, op, value }` filters, `window` for date bounds, and `page` for the limit and cursor. Use `textIn` to restrict text matches to `title`, `description`, `comment`, `integration`, or `transcript`. When paging a relative-date search, reuse the absolute `window` from `executed.query.window` with the returned cursor. Repeating a relative bound such as `-P7D` changes the query and invalidates the cursor. Return each result's `url` unchanged so search attribution is preserved. - -**Feedback** - -| Tool | What it does | -|------|-------------| -| `submitFeedback` | Sends feedback about Jam to the Jam team for triage | - -The agent must show you the exact feedback text and get your approval before sending it. Send at most one report per session, under 200 words, with no secrets, customer data, source code, or stack traces. Do not report a mistake the agent corrected itself. - -**Delete** - -These tools delete data for good. Jam has no trash, so nobody in your workspace can bring it back. In Cursor, the bundled rule tells the agent to ask you before it calls one of them. The server also marks most other write tools as destructive, so your agent may ask before those too. - -| Tool | What it does | -|------|-------------| -| `deleteJam` | Deletes a Jam | -| `deleteComment` | Deletes a comment you wrote, with its attachments | -| `deleteFolder` | Deletes a folder and every Jam in it | - -**Recording Links** - -A Recording Link is a reusable URL that lets anyone record a Jam into your workspace. - -| Tool | What it does | -|------|-------------| -| `createRecordingLink` | Creates a Recording Link. You can set a folder and an expiration date | -| `listRecordingLinks` | Lists your workspace's Recording Links | -| `getRecordingLink` | Returns one Recording Link and how many Jams it collected | -| `updateRecordingLink` | Changes a Recording Link's name, folder, expiration date, or metadata | -| `deleteRecordingLink` | Turns off a Recording Link. Jams already recorded with it stay | -| `listRecordingLinkJams` | Lists the Jams recorded with a Recording Link | -| `listRecordingUrls` | Lists your workspace's connected domains | -| `getRecordingUrlVerifyLink` | Returns a link that someone opens to verify a connected domain | - -A Recording Link captures console and network logs only when it starts on a verified connected domain. Pass a `recordingUrlId` when you create one, or its Jams have no logs. - -### Skill: investigate bug - -`skills/investigate-bug/SKILL.md` - -Run `/investigate-bug ` for a full investigation. The agent reads the Jam, follows the server's investigation guide, checks network requests, console logs, and user events, looks at the screenshots or video, and writes up the likely root cause. - -### Skill: Jam CLI - -`skills/jam-cli/SKILL.md` - -Teaches the agent to install the Jam CLI, sign in with `JAM_TOKEN` in headless environments, and read a Jam with `jam get`. It also shows how to run `jam skills install --target --project`, which adds the CLI's full command reference to your repository. - -### Rule: Jam bug analysis - -`rules/jam-bug-analysis.mdc` - -Cursor only, where it is always on. Other agents don't load rules. It tells the agent which tool to call first, how to filter noisy results, and how to match console errors to failed network requests and user events. - -## Jam CLI - -The [Jam CLI](https://jam.dev/cli) reads the same data in a terminal. It accepts the same personal access tokens as the MCP server. +To test a checkout, link it into the local plugin folder and run **Developer: Reload Window**: ```bash -curl -fsSL https://native.jam.dev/install | bash -jam auth login # or: export JAM_TOKEN=jam_pat_... -jam get console --level error --json +ln -s /path/to/skills ~/.cursor/plugins/local/jam ``` -On your own machine, use the MCP server in your agent. Use the CLI where MCP isn't set up, such as cloud agents, CI, and shell scripts that pipe `--json` output into other tools. +
-### Record proof of a fix - -`jam record` records a window or display while a command runs, uploads the video, and returns a Jam link. Ask the agent to record the fix it made: +
+Codex ```bash -jam record windows --json -jam record run --window-id --title "Checkout completes after fix" -- bun run e2e/checkout.ts +codex plugin marketplace add jamdotdev/skills +codex plugin add jam@jam-plugins ``` -To record a browser fix with its console logs and network requests, add `--cdp 9222` and start Chrome with `--remote-debugging-port=9222`. To record several steps instead of one command, run `jam record start`, do the steps, then run `jam record stop`. - -Add the Jam link to the pull request. For a bug fix, ask for two Jams: one that shows the bug and one that shows the fix. To turn an existing video or a Playwright `trace.zip` into a Jam without recording, run `jam create jam`. Recording works on macOS and Linux (X11). - -## Privacy and permissions +Start a new session, complete the OAuth prompt, and run `/mcp` to check the connection. To test a checkout, pass its path to `marketplace add`. -- The plugin connects only to `https://mcp.jam.dev/mcp`. -- The OAuth scopes are `mcp:read` and `mcp:write`. Write access covers comments, reactions, folders, `updateJam`, the Recording Link tools that make changes, and the three `delete*` tools. -- The server checks permissions for each workspace and each Jam. Workspace changes require `mcp:write`. `submitFeedback` uses `mcp:read`; the agent must get your approval of the exact text before sending feedback to the Jam team. -- Every request sees only the Jams your account can already see. -- `createFolder` and `createRecordingLink` can upgrade you from Viewer to Creator when your workspace allows it. That can change seat billing and sends an email to workspace Admins. `createRecordingLink` also sends a webhook event to your workspace's webhook subscribers. +
-## Support +
+GitHub Copilot CLI -- To report a bug or request a feature, [open an issue](https://github.com/jamdotdev/skills/issues). -- For anything else about Jam, email [support@jam.dev](mailto:support@jam.dev). +```bash +copilot plugin marketplace add jamdotdev/skills +copilot plugin install jam@jam-plugins +``` -## Links +
-- [jam.dev](https://jam.dev) -- [Jam Chrome extension](https://chromewebstore.google.com/detail/jam/iohjgamcilhbgmhbnllfolmkmmekfmci) -- [Jam MCP docs](https://jam.dev/mcp) -- [Personal access tokens](https://jam.dev/docs/personal-access-tokens) -- [MCP Registry entry `dev.jam/jam`](https://registry.modelcontextprotocol.io/v0.1/servers?search=dev.jam) +
+Gemini CLI -## Prepare a release +```bash +gemini extensions install https://github.com/jamdotdev/skills +``` -Keep the version consistent across `.claude-plugin/plugin.json`, `.codex-plugin/plugin.json`, `.cursor-plugin/plugin.json`, `gemini-extension.json`, and `server.json`. Release 1.23.2 matches production MCP 1.23.2. +Run `/mcp auth Jam` once to sign in. To test a checkout, run `gemini extensions link /path/to/skills`. -`server.json` is the official MCP Registry entry, `dev.jam/jam`. A registry version can't be changed after publishing, so bump the version before you publish again. Pushing a `v` tag runs `.github/workflows/publish-mcp.yml`, which publishes `server.json` with the `MCP_PRIVATE_KEY` secret. That key matches the `v=MCPv1` TXT record on `jam.dev`. +
-The coding-agent plugins include both skills. For the ChatGPT directory, build a separate package containing only the MCP investigation skill. The CLI skill downloads or executes code outside the reviewed package and is excluded from that upload. +Sign-in, tokens, what the MCP server can do, and permissions: [docs/plugin.md](./docs/plugin.md). -```bash -git archive --format=zip --prefix=jam/ --output=../jam-chatgpt-1.23.2.zip HEAD .codex-plugin mcp.json assets skills/investigate-bug LICENSE -``` +## Contribute -For an existing ChatGPT app, update the existing app entry using its exported release ZIP and metadata. Preserve its saved identity and connection settings. Do not create a new app through **Upload New**. The command above builds the repository package; it does not recover saved portal metadata. +Bug in a skill, or a skill you want? [Open an issue](https://github.com/jamdotdev/skills/issues/new/choose). Pull requests are welcome; [CONTRIBUTING.md](./CONTRIBUTING.md) is short. -## License +--- -MIT. See [LICENSE](./LICENSE). +

+ jam.dev · + Jam MCP · + Jam CLI · + Dev Utils · + Changelog · + MIT +

diff --git a/docs/plugin.md b/docs/plugin.md new file mode 100644 index 0000000..8f8228f --- /dev/null +++ b/docs/plugin.md @@ -0,0 +1,55 @@ +# Jam plugin and MCP server + +The plugin connects your coding agent to the Jam MCP server at `https://mcp.jam.dev/mcp` and installs the skills in this repository. Install commands are in the [README](../README.md#install-as-a-plugin). The canonical, always current MCP reference is [jam.dev/mcp](https://jam.dev/mcp); this page is a snapshot and can lag behind it. + +## Sign in + +### OAuth + +The plugin connects to `https://mcp.jam.dev/mcp`. Your agent prompts you to sign in to Jam in a browser during installation or when you first use a Jam tool. There is nothing else to set up. + +### Personal access token + +Use a token when a browser sign-in isn't possible, such as in CI or a headless environment. + +1. Create a token in [**Settings → MCP**](https://jam.dev/s/settings/mcp) under **Personal Access Tokens**. Copy it right away, because Jam stores only a hash. +2. Add a `headers` block to the Jam server entry in your agent's MCP config. In Cursor, the file is `mcp.json`: + +```json +{ + "mcpServers": { + "Jam": { + "url": "https://mcp.jam.dev/mcp", + "headers": { + "Authorization": "Bearer YOUR_PERSONAL_ACCESS_TOKEN" + } + } + } +} +``` + +Each token works in one workspace, belongs to your account, and expires after seven days, 30 days, 90 days, or one year. You pick the length when you create it. See [Personal access tokens](https://jam.dev/docs/personal-access-tokens) for details. + +## Check that it works + +1. Confirm **Jam** is in your agent's list of MCP servers. In Cursor, open **Customize**. In Claude Code, Codex, or Gemini CLI, run `/mcp`. In GitHub Copilot CLI, run `copilot mcp list`. +2. Paste a Jam link into the chat and ask the agent to analyze it. Complete the browser sign-in if prompted. +3. Ask the agent to fix a Jam, such as `https://jam.dev/c/`. The `jam-fix` skill runs. + +## What the server can do + +The full, current tool list is at [jam.dev/mcp](https://jam.dev/mcp); each tool also describes itself to your agent. + +- **Read:** a Jam's details, console logs, network requests, user events, screenshots, video frames, transcript, chapters and custom metadata. Search Jams across your workspace. +- **Write:** comments and reactions, folders, Jam titles and descriptions, Recording Links, and feedback to the Jam team. +- **Delete, for good:** `deleteJam`, `deleteComment` and `deleteFolder`. Jam has no trash, so nobody in your workspace can bring the data back. The bundled Cursor rule tells the agent to ask you first; other agents ask before tools the server marks as destructive. + +A Recording Link captures console and network logs only when it starts on a verified connected domain. + +## Privacy and permissions + +- The plugin connects only to `https://mcp.jam.dev/mcp`. +- The OAuth scopes are `mcp:read` and `mcp:write`. Every tool that reads a Jam or the workspace needs `mcp:read`. Tools that change something need `mcp:write`: comments, reactions, folders, `updateJam`, the Recording Link tools that make changes, and the three `delete*` tools. The one exception is `submitFeedback`: it writes only to Jam's own feedback store, so it needs `mcp:read`. +- The server also checks your permissions for each workspace and each Jam on every call. +- Every request sees only the Jams your account can already see. +- `createFolder` and `createRecordingLink` can upgrade you from Viewer to Creator when your workspace allows it. That can change seat billing and sends an email to workspace Admins. `createRecordingLink` also sends a webhook event to your workspace's webhook subscribers. diff --git a/gemini-extension.json b/gemini-extension.json index 5bed215..f237ae0 100644 --- a/gemini-extension.json +++ b/gemini-extension.json @@ -1,7 +1,7 @@ { "name": "jam", - "version": "1.23.2", - "description": "Jam recordings (video, console, network, logs) as context for Gemini CLI. Paste a Jam link and investigate the bug.", + "version": "2.0.0", + "description": "Jam recordings (video, console, network, logs) as context for Gemini CLI. Fix the bug in a Jam, prove a fix with a recorded Jam, mark up a screenshot, or turn a Jam into an end-to-end test.", "mcpServers": { "Jam": { "httpUrl": "https://mcp.jam.dev/mcp" diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..7b64005 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,29 @@ +{ + "name": "jam-skills-checks", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "jam-skills-checks", + "devDependencies": { + "yaml": "2.9.1" + } + }, + "node_modules/yaml": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.1.tgz", + "integrity": "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..c83f64e --- /dev/null +++ b/package.json @@ -0,0 +1,11 @@ +{ + "name": "jam-skills-checks", + "private": true, + "scripts": { + "check": "node scripts/check.mjs", + "test": "node --test scripts/*.test.mjs" + }, + "devDependencies": { + "yaml": "2.9.1" + } +} diff --git a/rules/jam-bug-analysis.mdc b/rules/jam-bug-analysis.mdc deleted file mode 100644 index e0de308..0000000 --- a/rules/jam-bug-analysis.mdc +++ /dev/null @@ -1,102 +0,0 @@ ---- -description: Guidelines for effectively analyzing Jam bug reports using MCP tools -alwaysApply: true ---- - -# Jam Bug Analysis - -When working with Jam bug reports, follow these guidelines to investigate issues effectively. - -## Getting Started - -- Always start with `getDetails` when given a Jam URL or ID — it provides the bug report overview, device info, and investigation guidance. -- Follow the **Investigation Guide** returned by `getDetails` — it recommends which tools to call next based on signals detected in the report. - -## Jam Identifiers - -- Jam URLs look like `https://jam.dev/c/` or contain a UUID. -- You can pass either a full URL or a UUID to any tool's `jamId` parameter (or `id` for `fetch`). -- Use `search` if you have a URL or pasted text and need to resolve it to a Jam — it returns a validation error directing you to `searchJams` when no UUID or Jam URL can be extracted from the query. It only matches video Jams: a screenshot or replay Jam that exists still comes back empty, so use `searchJams` for those. -- `fetch` is an alias for `getDetails` (kept for tools that expect the standard MCP `fetch` shape). - -## Investigation Tools - -Use these tools to drill into specific aspects of a bug report: - -| Tool | Purpose | -|------|---------| -| `getDetails` | Overview, metadata, device info, investigation guidance | -| `getNetworkRequests` | HTTP requests — filter by `statusCode`, `method`, `host`, `contentType` | -| `getConsoleLogs` | Browser console output — filter by `logLevel` (error, warn, info) | -| `getUserEvents` | Timeline of clicks, inputs, navigation, scrolls | -| `getScreenshots` | Visual screenshots (screenshot-type Jams only) | -| `getFrames` | Still frames from video Jams as images — `overview: true` for a labeled grid across the whole video, or sample by `at` timestamps / `fromMs`–`toMs` window (max 30 frames/call) | -| `analyzeVideo` | Extract user intents from video recordings | -| `getVideoTranscript` | Speech transcript from video Jams (if mic was enabled) | -| `getVideoChapters` | AI-generated chapters for a video Jam — titled segments with start/end timestamps | -| `getMetadata` | Custom metadata set via `jam.metadata()` SDK | - -## Discovery & Management Tools - -| Tool | Purpose | -|------|---------| -| `searchJams` | Search workspace Jams with `v: 1`, `text`, structured `where` filters, `window`, and `page` | -| `listJams` | Deprecated legacy search; use `searchJams` | -| `listFolders` | Browse team folders | -| `listMembers` | Find team members (useful for filtering by author) | -| `createFolder` | Create a folder to file Jams into | -| `updateFolder` | Rename a folder (does not move Jams) | -| `createComment` | Add a comment to a Jam (supports Markdown) | -| `editComment` | Rewrite a comment you authored | -| `addReaction` | React to a comment on a Jam | -| `removeReaction` | Take back a reaction you left | -| `updateJam` | Rename a Jam, rewrite its description, or move it to a different folder (title and description edits need an Admin or Creator seat) | - -Use `searchJams` for discovery. Its `where` entries are ANDed `{ field, op, value }` leaves. For example, `{ v: 1, text: "checkout", window: { from: "-P7D" }, where: [{ field: "author", op: "eq", value: "me" }] }`. For later pages, reuse the absolute `window` from `executed.query.window` and pass the returned `page.cursor` in the next request's `page.cursor` while `page.hasMore` is true. Do not resend a relative bound such as `-P7D`: it resolves to a new timestamp and invalidates the cursor. Return each Jam's `url` unchanged. - -## Feedback - -`submitFeedback` sends feedback to the Jam team. Show the user the exact text and get approval before calling it. Send at most once per session, with a body under 200 words. Never include secrets, customer data, source code, or stack traces. Do not report a mistake you corrected yourself. - -## Recording Link Tools - -These manage reusable recording links (URLs anyone can use to record and submit a Jam). They are management tools, not investigation tools — use them when asked to set up or administer bug-report collection, not when diagnosing a Jam. - -| Tool | Purpose | -|------|---------| -| `createRecordingLink` | Create a reusable recording link (optional target folder, expiration) | -| `listRecordingLinks` | List the team's recording links (cursor pagination) | -| `getRecordingLink` | Get one link by `publicId`, including how many Jams it collected | -| `updateRecordingLink` | Rename a link, change its folder/expiration/metadata | -| `deleteRecordingLink` | Revoke a link (soft delete — recorded Jams are untouched) | -| `listRecordingLinkJams` | List the Jams recorded through a link | -| `listRecordingUrls` | List the team's connected domains (recording URLs) | -| `getRecordingUrlVerifyLink` | Get a browser link a human opens to verify a connected domain | - -Important: a recording link only captures console and network logs when it starts from a **verified connected domain**. Call `listRecordingUrls` first and ask the user which domain to record from — a link created without a `recordingUrlId` captures no logs. - -## Destructive Tools - -These permanently remove data. Nothing on the team can restore it — the dashboard has no trash or -archive view. Confirm with the user before calling any of them, and never claim the data can be -recovered afterwards. - -| Tool | Purpose | -|------|---------| -| `deleteJam` | Delete a Jam | -| `deleteComment` | Delete a comment you authored, along with its attachments | -| `deleteFolder` | Delete a folder and every Jam inside it | -| `deleteRecordingLink` | Revoke a recording link (soft delete — recorded Jams are untouched) | - -## Best Practices - -1. **Follow the server's guidance.** The `getDetails` response includes an Investigation Guide with prioritized next steps — follow it instead of running every tool blindly. -2. **Prefer the transcript for video Jams with mic.** When the reporter narrated, `getVideoTranscript` is higher priority than `analyzeVideo` — it's faster and captures intent directly. -3. **Orient visually with `getFrames`.** For video Jams, `getFrames` with `overview: true` gives a timestamp-labeled grid of the whole recording in one call — cheaper than `analyzeVideo` when you just need to see what was on screen. Follow up with `at` timestamps around the moment of failure. -4. **Filter aggressively.** Use `statusCode: ["4xx", "5xx"]` or `logLevel: ["error"]` to focus on failures instead of fetching everything. The `statusCode`, `contentType`, `host`, `method`, and `logLevel` filters take arrays. Values are ORed within a filter and ANDed across filters. Use `limit` to cap output. If the returned page has fewer entries than `total`, narrow the filters or pass `nextCursor` as `after`. Network bodies default to failed requests; request `bodies: "all"` to inspect successful responses. Bodies are truncated to 4KB and flagged per row. -5. **Correlate across data sources.** Match network errors with console errors and user events to build a timeline of what went wrong. -6. **Check the Jam type first.** `getScreenshots` only works for screenshot Jams; `analyzeVideo` short-circuits on non-video Jams; `getVideoTranscript` requires that the mic was enabled. -7. **Handle "not ready" responses.** Event-backed tools (network, console, user events, transcript, intents) can return a postprocessing-pending message on fresh Jams. Retry after a few seconds or fall back to data that's already available. -8. **Provide structured diagnoses.** Summarize findings with: what happened, probable root cause, supporting evidence, and recommended next steps. Post them back via `createComment` only when the user asks. - -Video chapters and timestamped video comments can include a public `thumbnailUrl` for sharing with a person. Use `getFrames` to inspect the video yourself, preferably at a chapter midpoint. diff --git a/rules/jam-mcp-delete.mdc b/rules/jam-mcp-delete.mdc new file mode 100644 index 0000000..07cc72c --- /dev/null +++ b/rules/jam-mcp-delete.mdc @@ -0,0 +1,8 @@ +--- +description: Safety rules for the Jam MCP server's destructive tools +alwaysApply: true +--- + +# Jam MCP + +- `deleteJam`, `deleteComment` and `deleteFolder` remove data for good. Jam has no trash. Ask the user and wait for a yes before calling one, and never say the data can be recovered. diff --git a/scripts/check.mjs b/scripts/check.mjs new file mode 100644 index 0000000..7102606 --- /dev/null +++ b/scripts/check.mjs @@ -0,0 +1,111 @@ +#!/usr/bin/env node +// Repo checks run by CI. Prints every failure, exits 1 if any. + +import { existsSync, readdirSync, readFileSync } from "node:fs"; +import { execFileSync } from "node:child_process"; +import { dirname, join } from "node:path"; +import { parseArgs, stripVTControlCharacters } from "node:util"; +import { isMap, parseDocument } from "yaml"; + +const errors = []; +const fail = (msg) => errors.push(msg); +const read = (p) => readFileSync(p, "utf8"); +const { values } = parseArgs({ options: { "skills-list": { type: "string" } } }); +const mdFiles = [...new Set(execFileSync("git", ["ls-files", "-coz", "--exclude-standard", "--", "*.md", "*.mdc"], { encoding: "utf8" }).split("\0"))].filter((file) => file && existsSync(file)); + +// Skills +const readme = read("README.md"); +const skillDirs = readdirSync("skills", { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name); +let feedbackSection; +for (const dir of skillDirs) { + const skillPath = join("skills", dir, "SKILL.md"); + if (!existsSync(skillPath)) { + fail(`${dir}: missing SKILL.md`); + continue; + } + const src = read(skillPath); + const fm = src.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/); + if (!fm) { + fail(`${skillPath}: missing frontmatter`); + continue; + } + const frontmatter = parseDocument(fm[1], { uniqueKeys: true }); + if (frontmatter.errors.length) { + for (const error of frontmatter.errors) fail(`${skillPath}: invalid YAML: ${error.message}`); + continue; + } + if (!isMap(frontmatter.contents)) { + fail(`${skillPath}: frontmatter must be a YAML mapping`); + continue; + } + const name = frontmatter.get("name"); + const description = frontmatter.get("description"); + if (typeof name !== "string") fail(`${skillPath}: name must be a string`); + else if (name !== dir) fail(`${skillPath}: name "${name}" must equal folder "${dir}"`); + if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(dir)) fail(`${dir}: folder must be kebab-case`); + if (typeof description !== "string" || !description.trim()) fail(`${skillPath}: description must be a non-empty string`); + else { + const descriptionNode = frontmatter.get("description", true); + const rawDescription = fm[1].slice(descriptionNode.range[0], descriptionNode.range[1]); + if (/[\r\n]/.test(rawDescription) || /[\r\n]/.test(description)) fail(`${skillPath}: write the description on one line`); + if (description.length > 1024) fail(`${skillPath}: description is ${description.length} chars, max 1024`); + } + if (!existsSync(join("skills", dir, "agents", "openai.yaml"))) fail(`${dir}: missing agents/openai.yaml`); + if (!readme.includes(`### ${dir}\n`)) fail(`${dir}: missing "### ${dir}" card in README.md`); + if (dir.startsWith("jam-")) { + const feedback = src.match(/\n## Send feedback to Jam\n[\s\S]*?(?=\n## |$)/)?.[0]; + if (!feedback) fail(`${skillPath}: missing "## Send feedback to Jam" section; copy it from another jam-* skill`); + else if ((feedbackSection ??= feedback) !== feedback) fail(`${skillPath}: "## Send feedback to Jam" differs from the other jam-* skills`); + } else { + const mentions = src.slice(fm[0].length).match(/\bjam\b/gi)?.length ?? 0; + if (mentions > 1) fail(`${skillPath}: mentions Jam ${mentions} times; skills without the jam- prefix may mention it once`); + } +} + +// The CLI install block is copied into every skill that runs `jam`; keep the copies identical. +const setupBlocks = new Map(); +for (const file of mdFiles.filter((file) => file.startsWith("skills/") && file.endsWith(".md"))) { + const block = read(file).match(/```bash\n[^`]*native\.jam\.dev\/install[^`]*```/)?.[0]; + if (block) setupBlocks.set(file, block); +} +if (new Set(setupBlocks.values()).size > 1) { + fail(`CLI install blocks differ across ${[...setupBlocks.keys()].join(", ")}; make them identical`); +} + +// Versions. server.json is not here: it carries the Jam MCP server's version, not the plugin's. +const manifests = [ + ".claude-plugin/plugin.json", + ".codex-plugin/plugin.json", + ".cursor-plugin/plugin.json", + "gemini-extension.json", +]; +const versions = manifests.map((p) => [p, JSON.parse(read(p)).version]); +if (new Set(versions.map(([, v]) => v)).size !== 1) { + fail(`versions differ: ${versions.map(([p, v]) => `${p}=${v}`).join(", ")}`); +} + +// Relative links in Markdown +for (const file of mdFiles) { + const text = read(file).replace(/```[\s\S]*?```/g, ""); + const targets = [...text.matchAll(/\]\(([^)\s]+)\)|(?:href|src)="([^"]+)"/g)].map((m) => m[1] ?? m[2]); + for (const target of targets) { + if (/^([a-z]+:|#)/i.test(target)) continue; + const path = join(dirname(file), decodeURI(target.split("#")[0])); + if (!existsSync(path)) fail(`${file}: broken link ${target}`); + } +} + +if (values["skills-list"]) { + const output = stripVTControlCharacters(read(values["skills-list"])); + if (/\bSkipped\b.*SKILL\.md|YAML parse error/i.test(output)) fail("skills CLI skipped a skill; inspect its discovery output"); + const listed = new Set([...output.matchAll(/^│ ([a-z0-9]+(?:-[a-z0-9]+)*)\r?$/gm)].map((match) => match[1])); + for (const dir of skillDirs) { + if (!listed.has(dir)) fail(`skills CLI did not list ${dir}`); + } +} + +if (errors.length) { + console.error(errors.map((e) => `✗ ${e}`).join("\n")); + process.exit(1); +} +console.log(`✓ ${skillDirs.length} skills, ${mdFiles.length} Markdown files, version ${versions[0][1]}`); diff --git a/scripts/check.test.mjs b/scripts/check.test.mjs new file mode 100644 index 0000000..c0f476e --- /dev/null +++ b/scripts/check.test.mjs @@ -0,0 +1,115 @@ +import assert from "node:assert/strict"; +import { execFileSync, spawnSync } from "node:child_process"; +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import test from "node:test"; + +const checkPath = fileURLToPath(new URL("./check.mjs", import.meta.url)); +const skillPath = "skills/sample-skill/SKILL.md"; + +function fixture(t, frontmatter = "name: sample-skill\ndescription: A useful skill.") { + const cwd = mkdtempSync(join(tmpdir(), "jam-skills-check-")); + t.after(() => rmSync(cwd, { recursive: true, force: true })); + const write = (path, content) => { + mkdirSync(dirname(join(cwd, path)), { recursive: true }); + writeFileSync(join(cwd, path), content); + }; + write(skillPath, `---\n${frontmatter}\n---\n# Sample skill\n`); + write("skills/sample-skill/agents/openai.yaml", "interface: {}\n"); + write("README.md", "### sample-skill\n"); + for (const file of [".claude-plugin/plugin.json", ".codex-plugin/plugin.json", ".cursor-plugin/plugin.json", "gemini-extension.json"]) { + write(file, '{"version":"1.0.0"}\n'); + } + execFileSync("git", ["init", "--quiet"], { cwd }); + execFileSync("git", ["add", "."], { cwd }); + return { + cwd, + write, + run: (...args) => { + const result = spawnSync(process.execPath, [checkPath, ...args], { cwd, encoding: "utf8" }); + assert.ifError(result.error); + return { status: result.status, output: result.stdout + result.stderr }; + }, + }; +} + +for (const [label, frontmatter, expected] of [ + ["unquoted colon", "name: sample-skill\ndescription: Record proof: publish it.", /invalid YAML/], + ["empty description", "name: sample-skill\ndescription:", /description must be a non-empty string/], + ["null description", "name: sample-skill\ndescription: null", /description must be a non-empty string/], + ["numeric description", "name: sample-skill\ndescription: 42", /description must be a non-empty string/], + ["boolean description", "name: sample-skill\ndescription: true", /description must be a non-empty string/], + ["mapping description", "name: sample-skill\ndescription: { value: text }", /description must be a non-empty string/], + ["sequence description", "name: sample-skill\ndescription: [text]", /description must be a non-empty string/], + ["whitespace description", 'name: sample-skill\ndescription: " "', /description must be a non-empty string/], + ["missing description", "name: sample-skill", /description must be a non-empty string/], + ["numeric name", "name: 42\ndescription: A useful skill.", /name must be a string/], + ["duplicate description", "name: sample-skill\ndescription: First.\ndescription: Second.", /invalid YAML/], + ["sequence frontmatter", "- name: sample-skill\n- description: A useful skill.", /frontmatter must be a YAML mapping/], + ["block description", "name: sample-skill\ndescription: >-\n A useful skill.", /description on one line/], + ["multiline quoted description", 'name: sample-skill\ndescription: "A useful\n skill."', /description on one line/], + ["decoded newline", 'name: sample-skill\ndescription: "A useful\\nskill."', /description on one line/], + ["long description", `name: sample-skill\ndescription: "${"a".repeat(1025)}"`, /description is 1025 chars, max 1024/], +]) { + test(`rejects ${label}`, (t) => { + const result = fixture(t, frontmatter).run(); + assert.equal(result.status, 1, result.output); + assert.match(result.output, expected); + assert.match(result.output, /skills\/sample-skill\/SKILL.md/); + }); +} + +test("accepts quoted colons, quoted names, and optional metadata", (t) => { + const result = fixture(t, 'name: "sample-skill"\ndescription: "Record proof: publish it."\nlicense: MIT\nmetadata:\n category: testing').run(); + assert.equal(result.status, 0, result.output); +}); + +test("measures decoded description length", (t) => { + const result = fixture(t, `name: sample-skill\ndescription: "${"\\u0061".repeat(1024)}"`).run(); + assert.equal(result.status, 0, result.output); +}); + +test("checks untracked Markdown links", (t) => { + const repo = fixture(t); + repo.write("skills/sample-skill/references/new.md", "[missing](./missing.md)\n"); + const result = repo.run(); + assert.equal(result.status, 1, result.output); + assert.match(result.output, /references\/new.md: broken link \.\/missing.md/); +}); + +test("ignores deleted tracked Markdown", (t) => { + const repo = fixture(t); + repo.write("skills/sample-skill/references/deleted.md", "A removed reference.\n"); + execFileSync("git", ["add", "."], { cwd: repo.cwd }); + rmSync(join(repo.cwd, "skills/sample-skill/references/deleted.md")); + const result = repo.run(); + assert.equal(result.status, 0, result.output); +}); + +test("checks untracked CLI install blocks", (t) => { + const repo = fixture(t); + for (const [file, flags] of [["one.md", "--silent"], ["two.md", "--fail"]]) { + repo.write(`skills/sample-skill/references/${file}`, `\n\`\`\`bash\ncurl ${flags} https://native.jam.dev/install\n\`\`\`\n`); + } + const result = repo.run(); + assert.equal(result.status, 1, result.output); + assert.match(result.output, /CLI install blocks differ/); +}); + +for (const [label, output, status, expected] of [ + ["listed skill", "│\n◇ Available Skills\n│\n│ \u001b[36msample-skill\u001b[39m\n│\n│ A useful skill.\n", 0, /✓/], + ["path only", "⚠ Skipped skills/sample-skill/SKILL.md: YAML parse error\n", 1, /skills CLI did not list sample-skill/], + ["description only", "│ sample-skill\n", 1, /skills CLI did not list sample-skill/], + ["longer name", "│ sample-skill-extra\n", 1, /skills CLI did not list sample-skill/], + ["YAML warning despite listed skill", "⚠ Skipped other/SKILL.md: YAML parse error\n│ sample-skill\n", 1, /skills CLI skipped a skill/], +]) { + test(`discovery checks ${label}`, (t) => { + const repo = fixture(t); + repo.write("skills.txt", output); + const result = repo.run("--skills-list", "skills.txt"); + assert.equal(result.status, status, result.output); + assert.match(result.output, expected); + }); +} diff --git a/scripts/package-chatgpt.mjs b/scripts/package-chatgpt.mjs new file mode 100644 index 0000000..4d905b6 --- /dev/null +++ b/scripts/package-chatgpt.mjs @@ -0,0 +1,40 @@ +#!/usr/bin/env node +// Builds the ChatGPT directory package: the MCP-only skills plus the plugin identity, +// with the listing copy rewritten to name only what is inside. Needs `zip` on PATH. + +import { execFileSync } from "node:child_process"; +import { cpSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join, resolve } from "node:path"; + +const output = resolve(process.argv[2] ?? ""); +if (!output.endsWith(".zip")) { + console.error("Usage: node scripts/package-chatgpt.mjs "); + process.exit(1); +} + +export const SKILLS = ["jam-fix", "jam-to-e2e"]; +const DESCRIPTION = "Jam recordings (video, console, network, logs) as context for ChatGPT. Find the root cause of a bug from a Jam, or turn a Jam into an end-to-end test."; + +const stage = mkdtempSync(join(tmpdir(), "jam-chatgpt-")); +const root = join(stage, "jam"); +for (const path of [".codex-plugin", "mcp.json", "assets", "LICENSE"]) cpSync(path, join(root, path), { recursive: true }); +for (const skill of SKILLS) { + // The package has no references/; point those links at the repo instead. + const skillMd = readFileSync(join("skills", skill, "SKILL.md"), "utf8") + .replaceAll("](references/", `](https://github.com/jamdotdev/skills/blob/master/skills/${skill}/references/`); + mkdirSync(join(root, "skills", skill), { recursive: true }); + writeFileSync(join(root, "skills", skill, "SKILL.md"), skillMd); + cpSync(join("skills", skill, "agents"), join(root, "skills", skill, "agents"), { recursive: true }); +} + +const manifest = JSON.parse(readFileSync(".codex-plugin/plugin.json", "utf8")); +manifest.description = DESCRIPTION; +manifest.interface.shortDescription = "Fix bugs and write tests from Jam recordings"; +manifest.interface.longDescription = DESCRIPTION; +writeFileSync(join(root, ".codex-plugin/plugin.json"), JSON.stringify(manifest, null, 2) + "\n"); + +rmSync(output, { force: true }); +execFileSync("zip", ["-qr", output, "jam"], { cwd: stage }); +rmSync(stage, { recursive: true, force: true }); +console.log(output); diff --git a/scripts/package-chatgpt.test.mjs b/scripts/package-chatgpt.test.mjs new file mode 100644 index 0000000..b3b03b4 --- /dev/null +++ b/scripts/package-chatgpt.test.mjs @@ -0,0 +1,24 @@ +import assert from "node:assert/strict"; +import { execFileSync } from "node:child_process"; +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { test } from "node:test"; + +test("ChatGPT package carries only the MCP-only skills and says so", () => { + const dir = mkdtempSync(join(tmpdir(), "jam-chatgpt-test-")); + const archive = join(dir, "jam.zip"); + execFileSync("node", ["scripts/package-chatgpt.mjs", archive]); + const files = execFileSync("unzip", ["-Z1", archive], { encoding: "utf8" }).trim().split("\n"); + assert.ok(files.includes("jam/skills/jam-fix/SKILL.md")); + assert.ok(files.includes("jam/skills/jam-to-e2e/SKILL.md")); + assert.ok(!files.some((f) => /jam-proof|jam-annotate|references/.test(f))); + for (const skill of ["jam-fix", "jam-to-e2e"]) { + const md = execFileSync("unzip", ["-p", archive, `jam/skills/${skill}/SKILL.md`], { encoding: "utf8" }); + assert.doesNotMatch(md, /\]\(references\//, `${skill} still links to references/ inside the package`); + } + const manifest = JSON.parse(execFileSync("unzip", ["-p", archive, "jam/.codex-plugin/plugin.json"], { encoding: "utf8" })); + assert.doesNotMatch(manifest.description, /proof|screenshot|annotat/); + assert.equal(manifest.interface.longDescription, manifest.description); + rmSync(dir, { recursive: true, force: true }); +}); diff --git a/skills/investigate-bug/SKILL.md b/skills/investigate-bug/SKILL.md deleted file mode 100644 index 5d79e99..0000000 --- a/skills/investigate-bug/SKILL.md +++ /dev/null @@ -1,102 +0,0 @@ ---- -name: investigate-bug -description: Structured workflow for analyzing a Jam bug report end-to-end — pulls diagnostics from the Jam MCP server (console, network, user events, screenshots, video frames, transcript) and produces a root-cause hypothesis. Invoke with `/investigate-bug `. ---- - -# Investigate Bug - -A structured workflow for investigating Jam bug reports. Invoke with `/investigate-bug `. - -## Workflow - -### Step 1: Get the Bug Report - -Call `getDetails` with the provided Jam URL or ID. A full `https://jam.dev/c/...` URL or a UUID both work. - -From the response, extract: -- **Description** — what the reporter said -- **Type** — `video`, `screenshot`, or `replay` -- **Device info** — browser, OS, screen size -- **Investigation Guide** — server-recommended next steps, returned as the second text block -- **Postprocessing status** — for fresh Jams, some event-backed tools may report "not ready yet" - -### Step 2: Follow the Investigation Guide - -The `getDetails` response includes an Investigation Guide built from real signals in the Jam (console error counts, 4xx/5xx ranges per host, mic enabled, etc.). **Treat it as authoritative** — the steps below are fallbacks for when the guide is light or you want to drill deeper. - -- Each path specifies a **tool** to call, suggested **arguments**, and **what to look for**. -- Investigate `high` priority paths first; defer `medium` until they're exhausted. -- For video Jams where the reporter had the mic on, `getVideoTranscript` is now prioritized **above** `analyzeVideo` — voiceover is the cheapest, most direct signal. - -### Step 3: Analyze Network Activity - -Call `getNetworkRequests` with filters to focus on problems: -- `statusCode: ["4xx", "5xx"]` for client and server failures -- `host: ["api.example.com"]` to scope to a specific backend -- `method: ["POST"]` for mutation failures -- `limit` to cap output when a Jam is noisy - -All network filters, including `contentType`, take arrays. Values are ORed within a filter and ANDed across filters. Network, console, and user-event responses include `total` and `nextCursor`. If the page is incomplete, narrow the filters or pass `nextCursor` as `after`. - -Network response bodies default to failed requests (`bodies: "errors"`). Use `bodies: "all"` to inspect a successful request that returned unexpected data. Bodies are truncated to 4KB and flagged per row. Note CORS errors, timeouts, and unexpected response shapes. - -### Step 4: Check Console Errors - -Call `getConsoleLogs` with `logLevel: ["error"]` to find runtime errors. -- Look for unhandled exceptions, failed assertions, and stack traces. -- Cross-reference timestamps with network errors and user events. -- **Silent failure check:** if network shows 4xx/5xx but console is clean, the app may be swallowing errors — re-run with no `logLevel` filter. - -### Step 5: Review User Events - -Call `getUserEvents` to understand the sequence of interactions. -- Identify the exact action that triggered the bug. -- Note any unexpected navigation or repeated actions (signs of user confusion). - -### Step 6: Analyze Visual Evidence - -Based on the Jam type from `getDetails`: -- **Screenshot Jams** — call `getScreenshots` to examine visual state. -- **Video Jams** — call `getFrames` with `overview: true` first: it returns a single timestamp-labeled grid of frames spanning the whole recording, the fastest way to see what happened on screen. Then sample around the failure with `at: [, ...]` or a `fromMs`/`toMs` window (max 30 frames per call; `size` controls resolution). -- **Video Jams with mic** — also call `getVideoTranscript` (cheap, fast — the reporter's narration captures intent directly). If you need richer context, call `analyzeVideo` for extracted intents. -- **Video Jams without mic** — use `getFrames` plus `analyzeVideo`. -- For a longer video, use `getVideoChapters` to choose a chapter, then inspect its midpoint with `getFrames`. Chapters and timestamped video comments can include a public `thumbnailUrl` to share with a person. -- `analyzeVideo` and `getFrames` short-circuit on non-video Jams, so always check the type first. - -### Step 7: Check Custom Metadata - -Call `getMetadata` to retrieve any app-specific context (user IDs, feature flags, app version) that the application set via the `jam.metadata()` SDK. - -### Step 8: Produce Diagnosis - -Compile findings into a structured report: - -``` -## Bug Report Analysis - -### Summary -[One-sentence description of the issue] - -### What Happened -[Sequence of events from user's perspective] - -### Root Cause Hypothesis -[Most likely technical cause based on evidence] - -### Supporting Evidence -- [Network error / console error / user event that supports the hypothesis] -- [Additional data points] - -### Recommended Next Steps -1. [Specific action to fix or further investigate] -2. [Additional actions] -``` - -## Tips - -- Not all steps are needed for every bug. If the Investigation Guide from Step 2 gives you enough signal, skip straight to the diagnosis. -- Use `searchJams` for similar reports; `listJams` is deprecated. For example, `{ v: 1, text: "checkout", window: { from: "-P7D" }, where: [{ field: "author", op: "eq", value: "me" }] }`. Use `capturedUrl` with `contains` to filter the recorded page URL. For later pages, reuse the absolute `window` from `executed.query.window` and pass the returned `page.cursor` in the next request's `page.cursor` while `page.hasMore` is true. Do not resend `-P7D`: it resolves to a new timestamp and invalidates the cursor. Return each result's `url` unchanged. -- When the user asks, add your findings back to the Jam with `createComment` (Markdown supported) so the team sees them. Pass `playbackRelativeTimestamp` to pin a comment to a specific moment in a video or replay. -- If a fresh Jam's event tools return "not ready yet," the Jam is still being processed — wait a few seconds and retry, or fall back to data that's already available. - -If a Jam tool, command, doc, or skill needs feedback, use `submitFeedback` only after showing the user the exact text and getting approval. Send at most once per session, under 200 words, with no secrets, customer data, source code, or stack traces. Do not report a mistake you corrected yourself. diff --git a/skills/jam-annotate/SKILL.md b/skills/jam-annotate/SKILL.md new file mode 100644 index 0000000..ae6cba4 --- /dev/null +++ b/skills/jam-annotate/SKILL.md @@ -0,0 +1,172 @@ +--- +name: jam-annotate +description: Turn a screenshot into a Jam (a shareable jam.dev screenshot with context attached) that points at the problem, with boxes, arrows, notes, redactions and blurs drawn by `jam create jam`. Use when the user asks to file or share an annotated screenshot as a Jam. Local annotation or redaction requests alone do not call for this skill. +--- + +# jam-annotate + +The deliverable is a screenshot Jam a reviewer reads in five seconds: a box +on the broken thing, a number that matches a line in the description, secrets +redacted. `jam create jam` draws the marks into the image before upload; +`jam agent-context` prints the full payload schema. + +A request to create or share a Jam authorizes the upload. For local-only +annotation or redaction, use the available image tools and return a local +file. Do not upload it or sign in to Jam. + +## Set up the CLI + +```bash +command -v jam >/dev/null || curl -fsSL https://native.jam.dev/install | bash +export PATH="$HOME/.local/bin:$PATH" +jam auth status --json || jam auth login +``` + +Exit code 3 means not signed in. With no browser (CI, a cloud agent), ask the +user to set `JAM_TOKEN` to a personal access token from +https://jam.dev/s/settings/mcp as a secret. Never write a token to a file. + +## Pick the mark + +Five kinds. Pick by what the mark says: + +| Kind | Says | Numbered | +| -------- | ------------------------------------- | -------- | +| `box` | this element is the subject | yes | +| `arrow` | from here, look there (direction) | yes | +| `text` | a fact with no element to point at | no | +| `redact` | this region must not leave the screen | no | +| `blur` | this region is not the point | no | + +## Get coordinates right + +Coordinates are pixels of the image file, origin top-left. They are not CSS +pixels. A retina capture is 2x, so a button at CSS `(120, 340)` sits at image +`(240, 680)`. + +1. Capture the viewport, not the full page, so element bounds and the image + share an origin. +2. Read the image size (the PNG header, or your driver's screenshot metadata) + and divide by the viewport width. That ratio is the scale. +3. Take element bounds from the driver (Playwright `locator.boundingBox()`, + CDP `DOM.getBoxModel`) and multiply every number by the scale. +4. Pad a box 6 to 10 CSS pixels beyond the element so the outline does not + sit on the element's own border. + +With only an image file and no driver, read its pixel size, place the marks +by eye in image pixels with generous boxes, and say in the reply that the +positions are estimates. + +The CLI refuses a mark that falls outside the image and names the index. A +refusal almost always means the scale was skipped. + +## Know what a mark covers + +A box or arrow draws more than its outline. Below, `u` is the image width +divided by 1440, at least 1, and every size is image pixels. + +- Badge: a circle of radius `14u` centered on the box's top-left corner, or + on the arrow's `from`. Its inner quarter sits on whatever is in that corner: + an icon, an avatar, the first letter. +- Box label: a chip `29u` tall whose bottom edge is `7u` above the box, + starting `21u` right of the box's left edge. When the box is less than + `36u` from the top of the image, the chip goes inside the box's top edge + and covers its content. +- Arrow label: a chip `29u` tall, centered vertically on `from`, starting + `21u` to its right. +- A chip is about `8.4u` wide per character, plus `14u`. +- At an image edge, the CLI moves badges and chips inward so they stay in the + image. There they cover more of the box than the numbers above say. + +So a labelled box needs `36u` of empty space above it. Rows in a list, table +or activity feed are closer than that: a label on one row covers the row +above it, and a label on the next row covers the first row's text. + +## Compose + +- One mark per fact you will write. Three to five marks. More is a video. +- Nothing a mark draws may cover text the reader needs, including another + mark's subject. Before you add a label, check its chip and badge against + the section above. +- For neighbors (stacked rows, adjacent buttons), box each one without a + label and put the words in the numbered description. When the labels must + be on the image, use arrows whose tails sit in an empty margin with room + for the chip to their right. +- Pad a box at least `14u` on the left when an icon or avatar starts the + element, so the badge does not hide it. +- Box the element that is wrong. Arrow only when direction is the point: + where a click should have led, which control changed which value. Put the + tail in empty space and the head on the edge of the target, not its center. +- Text is for a fact with no element: "nothing happened after click". Place it + in empty space near the area it describes. +- Redact first, generously: emails, names, avatars, tokens, internal URLs, + customer identifiers. Redact the whole row, not the word. +- Blur is for clutter, not secrets: a sidebar, a list of unrelated rows, a + face in a demo. It softens the region so the eye skips it. Large text stays + legible through a blur, so anything that must not leave the screen is a + `redact`. +- Order is the story. Numbers follow payload order, so list the marks in the + order the description tells them. +- Labels are five words or fewer, Latin text, no trailing period. The sentence + goes in the description. +- Color carries meaning, one color per meaning. Default `lime` is "look here". + Use `strawberry` for the broken thing when there is more than one mark. + Other swatches: `grape`, `blueberry`, `mint`, `lemon`, `black`. +- Marks are drawn on the primary screenshot only. A `secondaryScreenshotPath` + uploads as-is, so a redaction on the primary does not cover it. Attach a + secondary only when it shows none of what you redacted; otherwise redact + it with an image tool first, or leave it out. + +## Payload + +The capture below is 2x, so a 1440×900 viewport is a 2880×1800 image and +every coordinate is doubled. + +``` +jam create jam '{ + "url": "https://app.example.com/checkout", + "title": "Pay button does nothing on retry", + "description": "1. The Pay button, clicked twice.\n2. Where the confirmation should have appeared.\nCard number redacted.", + "screenshotPath": "./checkout.png", + "screenDimensions": { "width": 1440, "height": 900 }, + "annotations": [ + { "type": "redact", "x": 1200, "y": 640, "width": 560, "height": 48 }, + { "type": "blur", "x": 0, "y": 0, "width": 480, "height": 1800 }, + { "type": "box", "x": 1180, "y": 1120, "width": 320, "height": 88, "label": "Pay, clicked twice", "color": "strawberry" }, + { "type": "arrow", "from": { "x": 600, "y": 1400 }, "to": { "x": 1440, "y": 980 }, "label": "no confirmation here" } + ] +}' +``` + +Write the description so its numbers are the badge numbers: "1." is the first +box or arrow in the payload, "2." the second. Redactions, blurs and text +notes never take a number. + +## After + +1. Read the receipt: `id`, `url`, `kind`. If the command failed, say so and + fix the payload. Never hand over a link you did not get. +2. Look at what was drawn: `jam get screenshots --out `, then open + the PNG. If a chip or badge covers text the claim depends on, fix the + payload, create the Jam again, and share only the new link. Ask the human + before you delete the first Jam, because a deleted Jam cannot come back. +3. Return the link with the claim it supports. Post it on a PR or ticket + only when the user requested that destination. + +## Send feedback to Jam + +When this skill, a Jam MCP tool, a `jam` command or a Jam doc was wrong, +stale or confusing, or worked unusually well, tell Jam. Send it yourself: +Jam's agents read it, so it needs no approval. + +Call the `submitFeedback` MCP tool with `body`, `category` and `subject`. + +When MCP is unavailable, use the CLI: + +```bash +jam feedback "" --category --subject +``` + +- Jam surfaces only, not the user's code or other tools. +- At most once per session, and never for a slip you corrected yourself. +- No secrets, customer data, source code, stack traces or recording content. diff --git a/skills/jam-annotate/agents/openai.yaml b/skills/jam-annotate/agents/openai.yaml new file mode 100644 index 0000000..72072a0 --- /dev/null +++ b/skills/jam-annotate/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Jam Annotate" + short_description: "Mark up a screenshot and file it as a Jam" + default_prompt: "Use $jam-annotate to mark what is wrong in this screenshot and file it as a Jam." diff --git a/skills/jam-cli/SKILL.md b/skills/jam-cli/SKILL.md deleted file mode 100644 index 2bbcde8..0000000 --- a/skills/jam-cli/SKILL.md +++ /dev/null @@ -1,94 +0,0 @@ ---- -name: jam-cli -description: Use the `jam` CLI when the Jam MCP server is not connected (Cloud Agents, CI, headless shells) or when you need shell-composable output. Reads a Jam's console, network, frames, and transcript with `jam get`, and records proof of a fix with `jam record run`. Install, auth with JAM_TOKEN, and the triage and proof loops. ---- - -# Jam CLI - -The Jam MCP server and the `jam` CLI read the same data with the same personal access token. Prefer the MCP tools when they are connected. Reach for the CLI when they are not (cloud agents, CI, a container) or when you want `--json` output to pipe through `jq`. - -## Install - -```bash -command -v jam >/dev/null || curl -fsSL https://native.jam.dev/install | bash -export PATH="$HOME/.local/bin:$PATH" -jam --version -``` - -Windows: `powershell -c "irm https://native.jam.dev/install.ps1 | iex"`. - -## Authenticate - -Check first, then pick the path that fits the environment: - -```bash -jam auth status --json -``` - -- **Headless (Cloud Agents, CI):** set `JAM_TOKEN` to a personal access token. The CLI reads it for the life of the process and never writes it to disk. Create tokens under Settings → MCP at https://jam.dev/s/settings/mcp. Ask the user to add it as a secret; never paste a token into a file or a commit. -- **A machine with a browser:** `jam auth login` opens OAuth. -- **A token on stdin:** `echo "$TOKEN" | jam auth login --token`. - -Exit code 3 means not authenticated. Branch on exit codes, not on stderr text. - -## Load the full command reference - -The binary ships its own skills. Install them into this repo so the agent gets the complete `jam get`, `jam list`, `jam create`, and `jam record` surface: - -```bash -jam skills install --target --project -``` - -`` is one of `claude`, `cursor`, `codex`, or `opencode`. Pick the one you are running in. - -`jam agent-context` prints the machine-readable command surface if you need a flag the skill does not mention. - -## Triage a Jam link - -A Jam id is the UUID at the end of `https://jam.dev/c/`. - -```bash -jam get jam "$ID" --json # what kind of Jam, title, URL -jam get intents "$ID" --json # cached summary: what the user tried, what broke -jam get console "$ID" --level error --json # JS errors first -jam get network "$ID" --status 5xx --json # failing backend calls -jam get events "$ID" --json # clicks, inputs, navigation -jam get frames "$ID" --overview # one labeled grid of the video -jam get transcript "$ID" --json # what the reporter said -``` - -Paginated reads return `{ items, next_cursor, truncated, hint }`. `hint` appears only when `truncated` is true. Pass `--after ` for the next page. Stop when you have the cause; do not fetch everything by reflex. - -Leave findings on the Jam: - -```bash -jam create comment "$ID" "Root cause: ..." --at 4200 -``` - -## Record proof of a fix - -`jam record run` records a window or display until the wrapped command exits, uploads the video, and prints `{ id, url, durationMs, width, height, target }`. Use it to attach evidence to a PR. - -```bash -jam record windows --json # pick a windowId; ids change on reopen -jam record run --window-id --title "Checkout completes after fix" --json -- bun run e2e/checkout.ts -``` - -- Proving a browser fix: add `--cdp ` (a debug port such as `9222` from Chrome started with `--remote-debugging-port=9222 --user-data-dir=`, a `ws://` DevTools URL, or `auto`). The Jam's Console and Network panels fill from every open tab. `jam record start` prints a `cdpProxy`; point your browser driver at it instead of Chrome and its clicks and typing are recorded too. -- Driving several steps instead of one command: `jam record start [target flags]`, do the steps, then `jam record stop` uploads the Jam. `jam record cancel` discards it. -- No `--window-id` records the primary display. Fine on a box you own (CI runner, container). On a person's machine record one window and launch your own app instance so nothing of theirs is captured. -- macOS needs Screen Recording permission for the terminal. Linux needs an X11 display plus `ffmpeg wmctrl x11-xserver-utils`. Windows is not supported. `jam doctor` reports readiness. -- For a bug fix, record two Jams: the bug, then the fix. Comment each Jam's URL on the other. -- Title is the claim being proven, not "proof". - -Create a Jam from an existing video or Playwright trace instead of recording live: - -```bash -jam create jam '{"kind":"video","videoPath":"./run.mp4","playwrightTracePath":"./trace.zip","title":"Checkout"}' -``` - -Trace console, network, and user events are attached to the timeline, with typed secrets redacted. - -## Deletes - -Every `jam delete ...` needs `--yes` when piped, and nothing deleted can be restored. Confirm with the user before passing `--yes`. diff --git a/skills/jam-fix/SKILL.md b/skills/jam-fix/SKILL.md new file mode 100644 index 0000000..5212302 --- /dev/null +++ b/skills/jam-fix/SKILL.md @@ -0,0 +1,151 @@ +--- +name: jam-fix +description: Find the root cause of a bug from a Jam (a jam.dev screen recording with the console, network and user actions attached), and fix it when the user asks. Without that ask it diagnoses only, with no code changes. Read the recording's diagnostics and visual evidence, then return a diagnosis with evidence. Use when the user pastes a jam.dev link or Jam id and asks to fix, debug, triage or investigate it, or asks what went wrong in a recording. +--- + +# jam-fix + +One Jam in, one diagnosis out, plus the fix when the user asks for one. The +diagnosis says what happened, the likely cause, the evidence +for it, and what to do next. Stop reading data once you have the cause. + +A Jam is a recording of what happened on a screen: the video or screenshot +with the console logs, network requests and user events of that moment +attached. This skill reads one that shows a bug. Its id is the UUID at the end of +`https://jam.dev/c/`; every tool takes the full URL or the id. + +## Pick the interface + +Use the connected Jam Model Context Protocol (MCP) tools (`getDetails`, `getConsoleLogs`, ...). +If a tool is unavailable, say what could not be read. Do not invent evidence. + +When MCP is unavailable and a shell is available, read +[references/cli.md](references/cli.md) for Jam CLI setup and the command for +each step below. That reference also covers optional proof uploads. + +## 1. Read the report + +Call `getDetails`. Note: + +- what the reporter wrote, and the page URL +- the type: `video`, `screenshot` or `replay`. It decides which visual tools work +- browser, OS and screen size +- the **Investigation Guide**, returned as the second text block. MCP only: + the CLI has no guide, so from the CLI go straight to step 2 + +The guide is built from signals in this Jam (error counts, failing hosts, +whether the mic was on). **Follow it before anything below.** Take its +`high` priority paths first. The steps that follow are for when the guide is +thin or you need to dig deeper. + +A fresh Jam may answer "not ready yet" from the event tools. It is still +processing: wait a few seconds and retry, up to three times, then work from +what is ready and say what was not. + +## 2. Failed requests + +`getNetworkRequests` with `statusCode: ["4xx", "5xx"]`. Look for CORS errors, +timeouts, auth failures, and response shapes the client did not expect. When +a `200` returned the wrong data, ask for its body too. The tool description +covers the filters, bodies and paging. + +## 3. Console errors + +`getConsoleLogs` with `logLevel: ["error"]`. Look for unhandled exceptions +and stack traces, and line their timestamps up with the failed requests. + +Network shows failures but the console is clean? The app may be swallowing +errors. Read the console again with no `logLevel` filter. + +## 4. What the user did + +`getUserEvents` gives clicks, inputs, navigation and scrolls in order. Find +the action right before the first error. Repeated clicks or back-and-forth +navigation mean the user was confused or the UI did not respond. + +## 5. What was on screen + +Check the type from step 1 first; the video tools do not work on other types. + +- **Screenshot:** `getScreenshots`. +- **Replay:** no frames or screenshots. Work from events, console and network. +- **Video:** `getFrames` with `overview: true` for one labeled grid, then + frames around the failure. +- **Video with the mic on:** `getVideoTranscript` first. The reporter's own + words are the cheapest signal of intent. +- **Long video:** `getVideoChapters`, then frames from the chapter that matters. + +## 6. App context + +`getMetadata` returns what the app sent with `jam.metadata()`: user id, +feature flags, app version. Check it when the bug depends on who or which +build. + +## 7. Write the diagnosis + +``` +## Summary +One sentence: what broke, where. + +## What happened +The user's steps, ending at the failure. + +## Root cause hypothesis +The most likely technical cause. Name the file or endpoint when the code is open. + +## Evidence +- The request, console error or event that supports it, with its timestamp. + +## Not checked +- What you could not confirm, and why. + +## Next steps +1. The fix or the next check. +``` + +If the repository is open, go from the evidence to the code: search for the +failing endpoint, the error message, or the component on screen, and name the +line you suspect. + +## When the user asked for a fix + +1. Diagnose first. Do not edit code until the evidence points at a cause. +2. Fix the cause, not the symptom the Jam shows. A swallowed error or a retry + around a failing request is not a fix. +3. Verify against the Jam's steps: a failing test that now passes, or the + steps run again against the fixed app. Say which, and what you saw. +4. Return the diagnosis, the change, and how you verified it. If the fix is + unverified, say so. +5. Offer proof of the fix. The reported Jam is already the "before", so only + the "after" needs a Jam. Offer it; do not create one without a yes. + If no recording or upload tool is available, return the verification + results and say that a new Jam could not be created. + +For approved proof uploads from a coding agent, use +[the CLI proof instructions](references/cli.md#proof-of-a-fix). + +## After + +Return the diagnosis and any fix in chat. If the user asked to post findings +on the Jam, use `createComment` once with what you found or changed. Pin it +to the moment with `playbackRelativeTimestamp` when relevant. Mention someone +only when the user requested a mention. A diagnosis or fix request alone +does not authorize comments, mentions, or posting to a PR or ticket. + +## Send feedback to Jam + +When this skill, a Jam MCP tool, a `jam` command or a Jam doc was wrong, +stale or confusing, or worked unusually well, tell Jam. Send it yourself: +Jam's agents read it, so it needs no approval. + +Call the `submitFeedback` MCP tool with `body`, `category` and `subject`. + +When MCP is unavailable, use the CLI: + +```bash +jam feedback "" --category --subject +``` + +- Jam surfaces only, not the user's code or other tools. +- At most once per session, and never for a slip you corrected yourself. +- No secrets, customer data, source code, stack traces or recording content. diff --git a/skills/jam-fix/agents/openai.yaml b/skills/jam-fix/agents/openai.yaml new file mode 100644 index 0000000..3927a1b --- /dev/null +++ b/skills/jam-fix/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Jam Fix" + short_description: "Find and fix the bug in a Jam" + default_prompt: "Use $jam-fix to find the root cause of the bug in this Jam and fix it." diff --git a/skills/jam-fix/references/cli.md b/skills/jam-fix/references/cli.md new file mode 100644 index 0000000..6acb35b --- /dev/null +++ b/skills/jam-fix/references/cli.md @@ -0,0 +1,78 @@ +# jam-fix with the `jam` CLI + +Use this when the Jam MCP tools are not connected. Set up once per environment: + +```bash +command -v jam >/dev/null || curl -fsSL https://native.jam.dev/install | bash +export PATH="$HOME/.local/bin:$PATH" +jam auth status --json || jam auth login +``` + +Exit code 3 means not signed in. With no browser (CI, a cloud agent), ask the +user to set `JAM_TOKEN` to a personal access token from +https://jam.dev/s/settings/mcp as a secret. Never write a token to a file. + +The same steps, as commands: + +```bash +jam get jam "$ID" --json # 1. report, type, URL +jam get network "$ID" --status 4xx --json # 2. client failures +jam get network "$ID" --status 5xx --json # 2. server failures +jam get console "$ID" --level error --json # 3. console errors +jam get user-events "$ID" --json # 4. clicks, inputs, navigation +jam get frames "$ID" --overview # 5. video grid (--at to sample) +jam get screenshots "$ID" --out ./shots # 5. screenshot Jams +jam get transcript "$ID" # 5. reporter's narration +jam get metadata "$ID" --json # 6. app context +``` + +For an unexpected successful response, remove the status filter and use +`jam get network "$ID" --bodies all --json`. + +If the user asked to post findings on the Jam: + +```bash +jam create comment "$ID" "Root cause: ..." --at 4200 +``` + +Paginated reads return `nextCursor`; pass it as `--after`. `jam agent-context` +prints every command and flag. + +## Proof of a fix + +Use this after the user asks to create an "after" Jam. Reuse the reported +Jam as the "before". Posting the result on that Jam, a PR, or a ticket +requires a request to publish there. + +For a passing Playwright test, enable `video: "on"` in the test configuration +and run it with a trace. Upload both artifacts: + +```bash +npx playwright test --trace on +ffmpeg -i test-results//video.webm after.mp4 +jam create jam '{"kind":"video","url":"","title":"","videoPath":"after.mp4","playwrightTracePath":"test-results//trace.zip","screenDimensions":{"width":1280,"height":720}}' +``` + +Use the test's viewport for `screenDimensions`. Put the bug Jam's URL in the +description. The trace supplies console, network and test actions. + +For a live recording, use `jam-proof` if it is installed. Otherwise name an +explicit target. For a browser, use its debug endpoint: + +```bash +jam record run --cdp --title "" -- +``` + +For a desktop app, select the process you launched: + +```bash +jam record run --pid --title "" -- +``` + +On Linux, window capture records a screen region. Use an isolated desktop +session or get explicit consent before recording a desktop someone is using. + +Never omit both the browser endpoint and the window selector: that records +the whole display. Ask before recording a display someone is using, naming +the display and duration. Use test data and return only a successful upload's +receipt URL. diff --git a/skills/jam-proof/SKILL.md b/skills/jam-proof/SKILL.md new file mode 100644 index 0000000..4ebda6a --- /dev/null +++ b/skills/jam-proof/SKILL.md @@ -0,0 +1,208 @@ +--- +name: jam-proof +description: "Record work you can see on screen with `jam record` and publish it as a Jam link (a shareable jam.dev recording with the console and network attached). Use when the user asks for a recorded Jam, a shareable demo, or video proof of work in a browser or desktop app. A request to verify a change or give sign-off alone does not call for recording or uploading." +--- + +# jam-proof + +The deliverable is a Jam link. Record the verification as it happens, then +hand the link back. Prefer a headless browser to a window, and a window to +somebody's whole screen. + +Create the Jam when the user asks for a recording to be shared through Jam. +If the user only wants a local video, keep it local. Posting to a PR, ticket, +or existing Jam requires a request to publish there. +Skip recording when there is nothing to show on screen, and say so. + +## Set up the CLI + +```bash +command -v jam >/dev/null || curl -fsSL https://native.jam.dev/install | bash +export PATH="$HOME/.local/bin:$PATH" +jam auth status --json || jam auth login +``` + +Exit code 3 means not signed in. With no browser (CI, a cloud agent), ask the +user to set `JAM_TOKEN` to a personal access token from +https://jam.dev/s/settings/mcp as a secret. Never write a token to a file. +`jam doctor` checks recording readiness; `jam agent-context` prints every +command and flag. + +## Pick the target, in this order + +Take the first rung that shows the claim. Each one down puts more of +someone's machine on camera. + +1. **Headless Chrome.** The default. It records the page, not a screen, so + the person keeps working with nothing of theirs in frame, and the run + works the same on a laptop, a container or CI. `jam record start + --cdp ` with no window or display selector takes the video from + Chrome itself, so there is no window to keep uncovered and no display to + keep awake, and the Console and Network panels fill from the same tap. + See *Record a browser an agent drives*. +2. **One window**, when the proof needs what a page-only capture cannot show: + the address bar, a Chrome dialog, a file picker, or a desktop app that is + not a browser at all. Launch your own instance of the app, in a fresh + profile or data dir when it has one, so nothing of theirs appears. Select + it with `--pid ` of the process you launched: it waits up to five + seconds for the window to open, so there is no need to list windows or + race the app's startup. For a window you did not launch, take its id from + `jam record windows --json` and pass `--window-id`. On macOS launch with + `open -g` so it never takes focus, and drive the app through its own + automation surface, not the OS mouse, so the cursor is never taken from + the person. On macOS the window may sit behind others. On Linux a window + grab is a screen region: use an isolated desktop session or get the same + consent as for a whole-display recording below. Keep that region + uncovered and unminimized, and the display awake. +3. **The whole display**, last. `jam record displays --json` lists them. + Fine without asking on a box nobody is sitting at: a CI runner, a + container, a remote Linux desktop you own. On a machine someone is using + it films their messages, their tabs and their notifications, so **ask + first and wait for a yes**, naming the display and roughly how long. No + answer means no. The display must not sleep while it records. A `start` or `run` with no selector and no `--cdp` + records the whole screen, so name a target rather than let it land here + by accident. + +Whichever rung a browser proof lands on, hand its Chrome debug port to +`jam record` as `--cdp `: the Jam then carries the console and network +of every tab, which is the half of the proof a video cannot show. `start` +prints a `cdpProxy` of its own; connect the driver to that instead of to +Chrome and your clicks and typing land as actions too, with the element each +one hit. + +## Bound the recording + +`jam record run` records until the wrapped command exits. Wrap the +verification: + +``` +jam record run --cdp --url --title "" --json -- +``` + +That is rung 1. For rung 2 replace `--cdp ` with `--pid ` of +the app you launched, plus `--cdp ` when it is a browser. `--url` is the +page the window shows; the Sharepage names it. + +When you drive the app yourself and cannot be wrapped, run it in the +background and send SIGINT when done. It stops, uploads, and exits 0 with the +receipt on stdout. + +``` +jam record run --cdp --url --title "" --json > proof.json 2> proof.log & +REC=$! +# drive the steps that show the behavior +kill -INT $REC; wait $REC +``` + +## Record a browser an agent drives + +This is rung 1, and the normal case. Chrome stays headless: the video is +Chrome's own frames of the page, and the Console, Network and user-action +panels come from the same Chrome, so nothing of Jam's runs on the page under +test and nothing of the person's screen is in frame. + +Use the project's own browser driver when it speaks CDP: with Playwright, +`chromium.connectOverCDP(cdpProxy)` and drive the page from a script; the +`jam record` lines stay the same. Without one, the commands below use +agent-browser (`npm i -g agent-browser`, 0.38 or later), which brings its +own Chromium. No Chrome at all in the environment means no rung 1; say so. + +``` +agent-browser --session proof --input-mode human open +agent-browser --session proof set viewport 1920 1080 # then reload: it does not apply to the open page +agent-browser --session proof open +agent-browser --session proof get cdp-url # ws://127.0.0.1:/devtools/browser/… +jam record start --cdp --title "" --json # → { session, cdpProxy } +agent-browser --session proof record start /tmp/discard.webm --cursor --fps 5 # only turns the pointer overlay on; the file is unused +agent-browser --session drive --input-mode human --cdp mouse move # drive THROUGH the proxy +agent-browser --session drive --cdp mouse down +agent-browser --session drive --cdp mouse up +agent-browser --session drive --cdp wait "" +agent-browser --session proof record stop +jam record stop --json +``` + +With `--cdp` and no window or display selector the video is a 30 fps +screencast of the page in the visible tab: no address bar, cursor or Chrome +dialogs, no Screen Recording permission, no window to keep uncovered. + +Record at 1920x1080, set before the page loads, and click with `mouse move` +then `mouse down` and `mouse up` so the cursor overlay shows each click. +[references/browser.md](references/browser.md) has the cursor overlay rules +(agent-browser 0.38 or later), other sizes, and visible Chrome for rung 2. + +Drive through `cdpProxy`, not Chrome, or the clicks and typing never become +actions. Anything that takes a DevTools URL takes this one: +`chrome-devtools-mcp --browserUrl `, Playwright's +`chromium.connectOverCDP`. + +Stop with `jam record stop --json`, using the `session` that +`start` printed, so a recording another agent started is never the one +stopped. The receipt is the Jam. Stderr stays +quiet when the tap worked. It says so when Chrome sent no console, network or +user-action events at all, which means the tap or the proxy was not actually +in the path. Treat that line as a failed proof and fix the wiring before +handing the link over. + +If `jam record start` or `run` fails, run `jam doctor` and report its +Recording and Screencast lines. Do not hand over a link you did not get. + +## Make it readable + +**Pace it for the person who will watch it, not for yourself.** You read a +page from its accessibility tree, which is ready long before the page has +finished painting, animating and settling. A run that is correct at your +speed is a flicker at theirs, and a flicker proves nothing. So the waits are +part of the proof, not padding around it. + +- Start from a known state: reload the page, restart the app, clear the + terminal. +- Wait on the thing, not the clock: `wait ""` for what the action + produced. A hard-coded pause is either too short on a slow run or dead air + on a fast one, and a video of identical gaps reads as a machine. +- Then let the eye catch up. A small in-place change needs a moment; a new + screen needs longer, and the state that carries the claim needs longest. + Match the dwell to how much changed, and never cut away from something + still moving — an element in the tree is not yet an element a person has + seen. +- Keep it short. Thirty seconds that show the behavior beat five minutes. +- Title is the claim: "Deep link scrolls to comment 6", not "proof". +- Steps and expected result go in `--description`. + +## Before and after + +For a bug fix, reuse the reported Jam as the "before" when it shows the bug. +If new before-and-after recordings are needed, run the old and fixed versions +in isolated checkouts with test data. Preserve the user's working tree. +Skip the first when there is no meaningful before and say why. + +## After + +1. Read the receipt: `id` and `url` always, plus `durationMs`, `width`, + `height` and the recorded `target` when `jam record` did the capturing. + If the capture or upload failed, say so. Never claim proof you do not + have. +2. If the user asked for a Jam comment, add one at the moment the result appears: + `jam create comment "" --at `. On a fix Jam, + put the bug Jam's URL in that comment. Comment on the original bug Jam + only if the user requested it too. +3. Return the links: Bug | Fix, or one link with the claim it supports. Post + them on a PR or ticket only when the user requested that destination. + +## Send feedback to Jam + +When this skill, a Jam MCP tool, a `jam` command or a Jam doc was wrong, +stale or confusing, or worked unusually well, tell Jam. Send it yourself: +Jam's agents read it, so it needs no approval. + +Call the `submitFeedback` MCP tool with `body`, `category` and `subject`. + +When MCP is unavailable, use the CLI: + +```bash +jam feedback "" --category --subject +``` + +- Jam surfaces only, not the user's code or other tools. +- At most once per session, and never for a slip you corrected yourself. +- No secrets, customer data, source code, stack traces or recording content. diff --git a/skills/jam-proof/agents/openai.yaml b/skills/jam-proof/agents/openai.yaml new file mode 100644 index 0000000..40216e7 --- /dev/null +++ b/skills/jam-proof/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Jam Proof" + short_description: "Record proof that a change works as a Jam link" + default_prompt: "Use $jam-proof to record proof that this change works and return the Jam link." diff --git a/skills/jam-proof/references/browser.md b/skills/jam-proof/references/browser.md new file mode 100644 index 0000000..c433ead --- /dev/null +++ b/skills/jam-proof/references/browser.md @@ -0,0 +1,70 @@ +# jam-proof: browser details + +Read this from `SKILL.md` when the video needs a readable cursor, a size other than 1920x1080, or a visible Chrome window. + +## Readable cursor video + +What makes a headless video readable is agent-browser's cursor overlay, which +needs agent-browser 0.38 or later: + +- `--cursor` draws the pointer into the page as an inert overlay, so the + screencast shows where each click landed and the hover state it triggered. + Without it the video cuts from one state to the next and a viewer cannot + tell what was pressed. The overlay is hidden from accessibility snapshots + and removed on stop. Its own `/tmp/discard.webm` is a side effect of + turning the overlay on — the Jam is the deliverable, so throw the file + away. +- `--input-mode human` moves the pointer along a curve at human speed, but + only for `mouse move `. `hover`/`click` on a selector snap the + pointer to the element in a single frame, so a video built from them cuts + from place to place with nothing to follow. Drive a cursor proof with + `mouse move` to the target, then `mouse down` and `mouse up` to click it. + Read the target's centre first, for example + `agent-browser eval '(() => { const r = document.querySelector("").getBoundingClientRect(); return [r.left + r.width / 2, r.top + r.height / 2]; })()'`. +- The overlay stops following the pointer after a navigation in the middle of + a recording: it stays frozen wherever the last click left it, on every page + that follows. Keep a cursor proof on one page, and set the page up before + `record start`. When the proof has to cross pages, record it without the + overlay and say in the description what was clicked. +- **Record at 1920x1080 unless the proof calls for another size.** It is the + size a reader expects and the size anything posted elsewhere wants, and a + headless page has no window to constrain it. Set it with `agent-browser set + viewport 1920 1080` **before** recording and reload, because the setting + does not apply to the page already open. Use another size only when the + claim is about that size — a mobile layout, a customer's reported window — + and say which in the description. The frame rate is not yours to pick: the + screencast is 30 fps, and agent-browser's own `--fps` only governs the + `/tmp/discard.webm` you discard. Keep it low (`--fps 5`): at 1920x1080 + agent-browser's encoder can fall behind and abort its recording, which + takes the overlay with it. + +## Visible Chrome (rung 2) + +When the proof needs the address bar, a Chrome dialog or the real OS cursor, +that is rung 2: run Chrome visible and name its window, so the video is a +screen capture of the window instead of the page screencast. Keep `--cdp` +so console, network and actions still land. + +With agent-browser, add `--headed`; it opens its own "Google Chrome for +Testing" window with a fresh profile, so nothing of the person's appears: + +``` +agent-browser --session proof --headed --input-mode human open +agent-browser --session proof get cdp-url +jam record start --app "Google Chrome for Testing" --cdp --title "" --json +``` + +`jam record windows --json` lists the window if the app name differs on your +machine; pass its id as `--window-id` instead of `--app`. + +With any other driver, launch Chrome yourself with a debug port and its own +data dir, since Chromium 136 and later refuses `--remote-debugging-port` on +the default profile, and select it by pid: + +``` + --remote-debugging-port=9222 --user-data-dir=/tmp/jam-chrome about:blank & +jam record start --pid --cdp 9222 --title "" --json +``` + +A driver that moves the real mouse instead of speaking CDP leaves no +actions in the Jam; console and network still land from the tap. diff --git a/skills/jam-to-e2e/SKILL.md b/skills/jam-to-e2e/SKILL.md new file mode 100644 index 0000000..b7a622e --- /dev/null +++ b/skills/jam-to-e2e/SKILL.md @@ -0,0 +1,174 @@ +--- +name: jam-to-e2e +description: Turn a Jam (a jam.dev screen recording with the console, network and user actions attached) into steps and an end-to-end test in the project's own runner and language, with first-class guidance for Playwright and tester-army/e2e, grounded in the current code. A Jam of a flow or demo becomes a test that guards it; a Jam of a bug becomes a failing test the fix turns green. Use when the user pastes a jam.dev link and asks for an e2e test, a regression test, a repro, or to "write a test for this". +--- + +# jam-to-e2e + +The Jam shows what the user did and what the screen, console and network +did in return. The code shows what the labels and routes are today. The +tests assert what the product must do. One test per claim: a Jam that walks +through three screens is three tests, and a Jam of one bug is one. + +A Jam is not always a bug report, so the user's request decides what the +test must do today, not the Jam: + +- "Cover this flow", "write a test for this demo": the test asserts what the + Jam shows and **passes** today. It guards the flow. +- "Regression test", "reproduce this bug", "test for this bug": the test + asserts what should have happened and **fails** until the fix lands. +- Neither said: take the Jam's title and description as the hint, state + which reading you took, and ask when you can. + +## Pick the interface + +Use the Jam Model Context Protocol (MCP) tools when they are connected. +Otherwise read [references/cli.md](references/cli.md) for the Jam CLI setup; the table +below gives the command for each read. + +## 1. Read the Jam + +Fetch in this order and stop when you have the steps and the expected results. + +| What | MCP | CLI | +| --- | --- | --- | +| Title, description, URL, type | `getDetails` | `jam get jam --json` | +| Clicks, inputs, navigation | `getUserEvents` | `jam get user-events --json` | +| What the reporter said | `getVideoTranscript` | `jam get transcript ` | +| What was on screen, video Jam | `getFrames({ overview: true })` | `jam get frames --overview` | +| What was on screen, screenshot Jam | `getScreenshots` | `jam get screenshots --out ./shots` | +| Errors and requests, when the test is about them | `getConsoleLogs`, `getNetworkRequests` | `jam get console`, `jam get network` | + +- The type from the first row decides the visual row: transcript and frames + exist only for video Jams, the screenshot only for screenshot Jams. A + `replay` has neither; work from the events, console and network. +- Video Jams often carry the numbered steps in the title, not the description. +- User events can be empty, especially on extension recordings. Treat them as a + bonus and fall back to the transcript and frames. +- Write down the **expected result** in one sentence. Most Jams do not state + it. If the Jam and the user leave it unclear, ask. If you cannot ask, write + the expected result you assumed and flag it in the hand-off; the expected + result is the assertion, and a test with a guessed assertion proves nothing. + +## 2. Ground it in code + +Never take a label, route or button name from the Jam. Find the component +that renders the screen and read it. Labels get renamed, toasts get removed, +buttons become autosave. List every difference between the Jam and today's +code in your reply. + +## 3. Pick the runner + +Use what the project already has, in whatever language it is. Look for the +runner's config and existing end-to-end tests: `e2e.config.*`, +`playwright.config.*`, `cypress.config.*`, `pytest.ini` with +`playwright`, `wdio.conf.*`, Capybara specs, a `selenium` dependency, a +`make e2e` target. Reuse its fixtures, sign-in setup and page objects, put +the test next to the existing ones, and follow their naming. Run one with +the project's own command. + +Only when there is nothing: Playwright, in the project's language, and say +so. For a JavaScript project the setup is +`npm init playwright@latest -- --quiet --browser=chromium` plus one test +file; `--quiet` takes the defaults without prompting. + +The test title is the expected result in plain English. The first line is a +comment with the source Jam URL. + +## 4. Write the test + +These rules hold in every runner; the names of the calls differ. + +- **Locate by what a person sees.** Role and accessible name, label, + placeholder, visible text. Not parent traversal, CSS classes standing in + for a label, or positional picks (last, nth). Those break on unrelated + markup changes. +- **A control you cannot name is an accessibility bug.** Add the label or + `aria-label` in the product when that is in scope and locate by it. When + it is not, fall back to CSS and leave a comment naming the follow-up. +- **Assert what the server must have done.** A URL change, text that only + exists after the action, or state that survives a reload. Wait on a + network response only as synchronization, then still assert the visible + outcome. +- **Assertions that retry.** Use the runner's auto-waiting expectation, not + a one-shot read followed by a plain equality. The one-shot samples once + and flakes. +- **Never** a fixed sleep, a loop that clicks whatever is visible, an + assertion that only says the URL does *not* contain something, or a + `try/catch` that turns a failure into `false`. + +### Playwright + +Locators: `getByRole`, `getByLabel`, `getByPlaceholder`, `getByText`, then +`.filter({ hasText })` to narrow. Assertions: `await expect(locator).toBeVisible()`, +`toHaveText`, `toHaveValue`, `toHaveURL`; never `expect(await locator.isVisible())`. +No `waitForTimeout`. The same calls exist in the Python, Java and .NET +bindings. https://playwright.dev/docs/best-practices wins any disagreement +with this file. + +### tester-army/e2e + +`e2e.config.*` and `*.e2e.ts` files mean [tester-army/e2e](https://github.com/tester-army/e2e). +Read `.agents/skills/e2e/SKILL.md` if it exists, or run `npx e2e guide`. +Agent steps (`agent.act`, `agent.assert`) are for reaching a state in plain +language; the claim the test exists for is still a locator assertion +(`expect(screen.getByRole(...))`), so it fails for the right reason. Run one +with `npx e2e run `. + +### Cypress + +`cy.findByRole` and `cy.findByLabelText` from Testing Library give the +locators above; `should(...)` assertions retry on their own. No `cy.wait(ms)`. + +## 5. Run it and check the reason + +Run the test with the project's normal browser mode, headless by default. + +- **Flow test:** it must pass, twice in a row, with no sleeps. A pass on the + first run and a timeout on the second is a flaky locator, not a test. +- **Bug test:** it must fail **on the assertion that encodes the bug**, not on + a locator timeout or a setup error. A test that fails for the wrong reason + is not a repro yet. If the bug does not reproduce, say so: report what you + ran, what you saw, and how it differs from the Jam (environment, data, + account state). Do not weaken the assertion until it passes. + +## 6. Hand off + +Return: + +1. The steps, numbered, as the tests perform them. +2. The test file paths and the command that runs them. +3. The run output: the passes, or each failure trimmed to its assertion. +4. Differences between the Jam and the current code. + +For a bug test, the same test must pass twice in a row with no sleeps once +the fix lands. From then on the test is the source of truth; the Jam is the +evidence. + +Offer to turn the passing run into an "after" Jam; do not create it without +a yes. With Playwright, run with `--trace on` and `video: "on"`, convert the +`.webm` to MP4 with `ffmpeg`, then `jam create jam` with `"kind":"video"`, +`videoPath` and `playwrightTracePath`; the trace's console, network and test +actions are synced to the video. With any other runner, record the run with +the `jam-proof` skill, which picks a recording target that films the test's +browser and not the display. Without that skill, skip the recording and say +so. Return the URL. Comment on the source Jam or post to a PR or +ticket only when the user requested that destination. + +## Send feedback to Jam + +When this skill, a Jam MCP tool, a `jam` command or a Jam doc was wrong, +stale or confusing, or worked unusually well, tell Jam. Send it yourself: +Jam's agents read it, so it needs no approval. + +Call the `submitFeedback` MCP tool with `body`, `category` and `subject`. + +When MCP is unavailable, use the CLI: + +```bash +jam feedback "" --category --subject +``` + +- Jam surfaces only, not the user's code or other tools. +- At most once per session, and never for a slip you corrected yourself. +- No secrets, customer data, source code, stack traces or recording content. diff --git a/skills/jam-to-e2e/agents/openai.yaml b/skills/jam-to-e2e/agents/openai.yaml new file mode 100644 index 0000000..e0e32ae --- /dev/null +++ b/skills/jam-to-e2e/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Jam to E2E" + short_description: "Turn a Jam into an end-to-end test" + default_prompt: "Use $jam-to-e2e to turn this Jam into an end-to-end test in this project." diff --git a/skills/jam-to-e2e/references/cli.md b/skills/jam-to-e2e/references/cli.md new file mode 100644 index 0000000..95db594 --- /dev/null +++ b/skills/jam-to-e2e/references/cli.md @@ -0,0 +1,17 @@ +# jam-to-e2e with the `jam` CLI + +Use this when the Jam MCP tools are not connected. Set up once per environment: + +```bash +command -v jam >/dev/null || curl -fsSL https://native.jam.dev/install | bash +export PATH="$HOME/.local/bin:$PATH" +jam auth status --json || jam auth login +``` + +Exit code 3 means not signed in. With no browser (CI, a cloud agent), ask the +user to set `JAM_TOKEN` to a personal access token from +https://jam.dev/s/settings/mcp as a secret. Never write a token to a file. + +The CLI column of the table in `SKILL.md` has the command for each read. +Paginated reads return `nextCursor`; pass it as `--after`. `jam agent-context` +prints every command and flag.