{"id":"baf0f3ec-e72e-41c1-826c-f2f4cf235d83","entityType":"agent","slug":"clawhub-chrischall-setlist-fpx","name":"setlist-fpx","canonicalUrl":"https://www.xpersona.co/agent/clawhub-chrischall-setlist-fpx","canonicalPath":"/agent/clawhub-chrischall-setlist-fpx","generatedAt":"2026-10-10T08:45:35.217Z","source":"CLAWHUB","claimStatus":"UNCLAIMED","verificationTier":"NONE","summary":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-10T04:36:06.054Z","emptyReason":null},"description":"Query and update setlist.fm from a shell without running the setlist-mcp server — search/read concert setlists, artists, venues, cities, and users via plain curl against the public REST API (an x-api-key header), and toggle \"I was there\" attendance on a setlist via the authenticated website, using an fpx-captured session cookie. Use when you want setlist.fm data or want to mark/unmark attendance without the MCP, in a script, or on a machine where the MCP isn't installed.","descriptionLabel":"Source description","evidenceSummary":"Capability contract not published. No trust telemetry is available yet. 1.7K downloads reported by the source. Last updated 10/10/2026.","installCommand":"clawhub skill install s17cjx1a349nz5apaqp02vgz4h85728z:setlist-fpx","sourceUrl":"https://clawhub.ai/chrischall/setlist-fpx","homepage":"https://clawhub.ai/chrischall/skills/setlist-fpx","primaryLinks":[{"label":"View on ClawHub","url":"https://clawhub.ai/chrischall/setlist-fpx","kind":"source"},{"label":"Homepage","url":"https://clawhub.ai/chrischall/skills/setlist-fpx","kind":"homepage"}],"safetyScore":84,"overallRank":62,"popularityScore":65,"trustScore":null,"claimedByName":null,"isOwner":false,"seoDescription":"setlist-fpx technical dossier on Xpersona with agent coverage, OPENCLEW support, and live trust metadata."},"coverage":{"evidence":{"source":"public-profile","verified":false,"confidence":"medium","updatedAt":"2026-10-10T04:36:06.054Z","emptyReason":null},"protocols":[{"protocol":"OPENCLEW","label":"OpenClaw","status":"self-declared","notes":"Declared in the public agent profile."}],"capabilities":[],"verifiedCount":0,"selfDeclaredCount":1,"capabilityMatrix":{"rows":[{"key":"OPENCLEW","type":"protocol","support":"unknown","confidenceSource":"profile","notes":"Listed on profile"}],"flattenedTokens":"protocol:OPENCLEW|unknown|profile"}},"adoption":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-10T04:36:06.054Z","emptyReason":null},"stars":null,"forks":null,"downloads":1680,"packageName":null,"latestVersion":"1.1.8","tractionLabel":"1.7K downloads"},"release":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-10T04:36:06.054Z","emptyReason":null},"lastUpdatedAt":"2026-10-10T04:36:06.054Z","lastCrawledAt":"2026-10-10T04:36:06.054Z","lastIndexedAt":null,"nextCrawlAt":"2026-10-11T04:36:06.054Z","lastVerifiedAt":null,"highlights":[{"version":"1.1.8","createdAt":"2026-10-09T23:28:41.698Z","changelog":"- Removed the redundant file skill-card.md; no user-facing or functional changes. - No changes to feature set or documentation. - Version update for housekeeping and file cleanup.","fileCount":5,"zipByteSize":9770},{"version":"1.1.7","createdAt":"2026-10-07T13:39:52.220Z","changelog":"- Removed the file skill-card.md. - No functional or documentation changes in the remaining files. - Project structure simplified by removing unused/unneeded documentation.","fileCount":5,"zipByteSize":9736},{"version":"1.1.6","createdAt":"2026-10-05T02:48:41.475Z","changelog":"- Removed the file: skill-card.md - No changes to functionality or documentation; this is a cleanup release.","fileCount":5,"zipByteSize":9694},{"version":"1.1.5","createdAt":"2026-10-03T01:43:18.667Z","changelog":"- Removed the file skill-card.md. - No user-facing functional changes.","fileCount":5,"zipByteSize":9681},{"version":"1.1.4","createdAt":"2026-09-28T13:55:46.097Z","changelog":"- Replaced all references to \"Transporter\" or \"fetchproxy extension\" with \"ContextMint Bridge\", the renamed fetchproxy extension. - Updated setup instructions and links to point to ContextMint Bridge documentation and releases. - Clarified compatibility (Chrome only for now; Safari not available). - Removed the obsolete skill-card.md file.","fileCount":5,"zipByteSize":9701},{"version":"1.1.3","createdAt":"2026-09-25T15:51:34.976Z","changelog":"- Removed the obsolete skill-card.md file. - Made minor documentation updates in references/attendance-write.md. - No changes to functionality or configuration.","fileCount":5,"zipByteSize":9477},{"version":"1.1.2","createdAt":"2026-09-23T21:46:15.243Z","changelog":"- Removed the sample file skill-card.md. - No changes to core functionality or documentation in SKILL.md.","fileCount":5,"zipByteSize":9658},{"version":"1.1.1","createdAt":"2026-09-23T15:41:08.685Z","changelog":"- Removed the file skill-card.md from the project. - No changes to the functionality or documentation in SKILL.md.","fileCount":5,"zipByteSize":9696}]},"execution":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No published capability contract is available yet."},"installCommand":"clawhub skill install s17cjx1a349nz5apaqp02vgz4h85728z:setlist-fpx","setupComplexity":"low","setupSteps":["Install using `clawhub skill install s17cjx1a349nz5apaqp02vgz4h85728z:setlist-fpx` in an isolated environment before connecting it to live workloads.","No published capability contract is available yet, so validate auth and request/response behavior manually.","Review the upstream CLAWHUB listing at https://clawhub.ai/chrischall/setlist-fpx before using production credentials."],"contract":{"contractStatus":"missing","authModes":[],"requires":[],"forbidden":[],"supportsMcp":false,"supportsA2a":false,"supportsStreaming":false,"inputSchemaRef":null,"outputSchemaRef":null,"dataRegion":null,"contractUpdatedAt":null,"sourceUpdatedAt":null,"freshnessSeconds":null},"invocationGuide":{"preferredApi":{"snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-setlist-fpx/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-setlist-fpx/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-setlist-fpx/trust"},"curlExamples":["curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-setlist-fpx/snapshot\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-setlist-fpx/contract\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-setlist-fpx/trust\""],"jsonRequestTemplate":{"query":"summarize this repo","constraints":{"maxLatencyMs":2000,"protocolPreference":["OPENCLEW"]}},"jsonResponseTemplate":{"ok":true,"result":{"summary":"...","confidence":0.9},"meta":{"source":"CLAWHUB","generatedAt":"2026-10-10T08:45:35.214Z"}},"retryPolicy":{"maxAttempts":3,"backoffMs":[500,1500,3500],"retryableConditions":["HTTP_429","HTTP_503","NETWORK_TIMEOUT"]}},"endpoints":{"dossierUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-setlist-fpx/dossier","snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-setlist-fpx/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-setlist-fpx/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-setlist-fpx/trust"}},"reliability":{"evidence":{"source":"runtime-metrics","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No trust, reliability, or runtime telemetry is available."},"trust":{"status":"unavailable","handshakeStatus":"UNKNOWN","verificationFreshnessHours":null,"reputationScore":null,"p95LatencyMs":null,"successRate30d":null,"fallbackRate":null,"attempts30d":null,"trustUpdatedAt":null,"trustConfidence":"unknown","sourceUpdatedAt":null,"freshnessSeconds":null},"decisionGuardrails":{"doNotUseIf":["Contract metadata is missing or unavailable for deterministic execution."],"safeUseWhen":[],"riskFlags":["missing_or_unavailable_contract","trust_data_unavailable","schema_references_missing"],"operationalConfidence":"low"},"executionMetrics":{"observedLatencyMsP50":null,"observedLatencyMsP95":null,"estimatedCostUsd":null,"uptime30d":null,"rateLimitRpm":null,"rateLimitBurst":null,"lastVerifiedAt":null,"verificationSource":null},"runtimeMetrics":{"successRate":null,"avgLatencyMs":null,"avgCostUsd":null,"hallucinationRate":null,"retryRate":null,"disputeRate":null,"p50Latency":null,"p95Latency":null,"lastUpdated":null}},"benchmarks":{"evidence":{"source":"no-benchmark-data","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No benchmark suites or observed failure patterns are available."},"suites":[],"failurePatterns":[]},"artifacts":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-10T04:36:06.054Z","emptyReason":null},"readme":"Skill: setlist-fpx\n\nOwner: chrischall\n\nSummary: Query and update setlist.fm from a shell without running the setlist-mcp server — search/read concert setlists, artists, venues, cities, and users via plain curl against the public REST API (an x-api-key header), and toggle \"I was there\" attendance on a setlist via the authenticated website, using an fpx-captured session cookie. Use when you want setlist.fm data or want to mark/unmark attendance without the MCP, in a script, or on a machine where the MCP isn't installed.\n\nTags: latest:1.1.8\n\nVersion history:\n\nv1.1.8 | 2026-10-09T23:28:41.698Z | auto\n\n- Removed the redundant file skill-card.md; no user-facing or functional changes.\n- No changes to feature set or documentation.\n- Version update for housekeeping and file cleanup.\n\nv1.1.7 | 2026-10-07T13:39:52.220Z | auto\n\n- Removed the file skill-card.md.\n- No functional or documentation changes in the remaining files.\n- Project structure simplified by removing unused/unneeded documentation.\n\nv1.1.6 | 2026-10-05T02:48:41.475Z | auto\n\n- Removed the file: skill-card.md\n- No changes to functionality or documentation; this is a cleanup release.\n\nv1.1.5 | 2026-10-03T01:43:18.667Z | auto\n\n- Removed the file skill-card.md.\n- No user-facing functional changes.\n\nv1.1.4 | 2026-09-28T13:55:46.097Z | auto\n\n- Replaced all references to \"Transporter\" or \"fetchproxy extension\" with \"ContextMint Bridge\", the renamed fetchproxy extension.\n- Updated setup instructions and links to point to ContextMint Bridge documentation and releases.\n- Clarified compatibility (Chrome only for now; Safari not available).\n- Removed the obsolete skill-card.md file.\n\nv1.1.3 | 2026-09-25T15:51:34.976Z | auto\n\n- Removed the obsolete skill-card.md file.\n- Made minor documentation updates in references/attendance-write.md.\n- No changes to functionality or configuration.\n\nv1.1.2 | 2026-09-23T21:46:15.243Z | auto\n\n- Removed the sample file skill-card.md.\n- No changes to core functionality or documentation in SKILL.md.\n\nv1.1.1 | 2026-09-23T15:41:08.685Z | auto\n\n- Removed the file skill-card.md from the project.\n- No changes to the functionality or documentation in SKILL.md.\n\nv1.1.0 | 2026-09-20T02:51:44.654Z | auto\n\n- Removed the file: skill-card.md\n- No changes to user-facing functionality or documentation, only internal file cleanup.\n\nv1.0.0 | 2026-09-19T11:18:57.581Z | auto\n\n- Initial public release.\n- Provides shell access to setlist.fm reads (public REST API with API key) and attendance toggling (using fpx-captured browser session cookie).\n- No need to run setlist-mcp server; all commands via curl and fpx.\n- Supports searching/concert setlists, artists, venues, cities, and user attendance toggling directly from the command line.\n- Documentation and usage instructions included in SKILL.md.\n- Removes sample skill card file (skill-card.md).\n\nv0.11.4 | 2026-09-15T19:24:40.866Z | auto\n\n- Removed the file skill-card.md.\n- No user-facing features or documentation were changed.\n- Maintenance/cleanup update only; no impact on functionality.\n\nv0.11.3 | 2026-09-14T14:08:28.031Z | auto\n\n- Removed the file skill-card.md.\n- No changes to functionality or documentation content.\n\nv0.11.2 | 2026-09-10T17:52:43.329Z | auto\n\n- Removed the skill-card.md file.\n- No changes to core functionality or documentation.\n\nv0.11.1 | 2026-09-05T00:51:31.091Z | auto\n\n- Removed the redundant skill-card.md file.\n- No user-visible functional changes; documentation and functionality remain the same.\n\nv0.11.0 | 2026-09-04T22:22:47.289Z | auto\n\n- Dropped the sample skill card (`skill-card.md`) from the repository.\n- No user-facing functionality or documentation changes.\n- Internal cleanup; skill usage and instructions remain unchanged.\n\nv0.10.0 | 2026-08-29T13:54:33.692Z | auto\n\n- Removed the unused file skill-card.md.\n- No functional or documentation changes to the skill's behavior or usage.\n\nv0.9.9 | 2026-08-28T21:07:44.862Z | auto\n\n- Removed the file: skill-card.md.\n- No functional changes; documentation and usage remain the same.\n\nv0.9.8 | 2026-08-28T11:35:19.675Z | auto\n\n- Removed the skill-card.md file from the project.\n- No changes to functionality or CLI behavior.\n\nv0.9.7 | 2026-08-09T21:03:19.929Z | auto\n\n- Removed the sample file skill-card.md.\n- No user-facing changes to instructions, features, or environment.\n- No impact on usage or functionality; documentation remains unchanged.\n\nv0.9.6 | 2026-08-06T00:43:55.685Z | auto\n\n- Removed the file skill-card.md.\n- No changes to functionality or documentation beyond file removal.\n\nv0.9.5 | 2026-08-03T04:33:20.038Z | auto\n\n- Removed the file skill-card.md.\n- No changes to core functionality or documentation content.\n- The skill continues to provide shell access to setlist.fm read/write features via curl and fpx.\n\nv0.9.4 | 2026-07-30T12:54:43.950Z | auto\n\n- Removed the file skill-card.md from the project.\n- No changes to code or user-facing documentation in this release.\n\nv0.9.3 | 2026-07-27T02:55:26.807Z | auto\n\nsetlist-fpx 0.9.3\n\n- Initial release of shell toolkit for setlist.fm using curl and fpx—no MCP required.\n- Supports searching/reading setlists, artists, venues, cities, and users via the public REST API with an x-api-key.\n- Enables toggling “I was there” attendance on setlist.fm using an fpx-extracted session cookie, replicating browser interaction.\n- Clear separation of API-key reads (no login needed) and cookie-authenticated writes (attendance toggle).\n- Includes detailed usage, setup instructions, and compliance notes in SKILL.md.\n\nArchive index:\n\nArchive v1.1.8: 5 files, 9770 bytes\n\nFiles: references/attendance-write.md (5672b), references/rest-api.md (5375b), skill-card.md (2230b), SKILL.md (7015b), _meta.json (130b)\n\nFile v1.1.8:SKILL.md\n\n---\nname: setlist-fpx\ndescription: >-\n  Query and update setlist.fm from a shell without running the setlist-mcp\n  server — search/read concert setlists, artists, venues, cities, and users\n  via plain curl against the public REST API (an x-api-key header), and\n  toggle \"I was there\" attendance on a setlist via the authenticated website,\n  using an fpx-captured session cookie. Use when you want setlist.fm data or\n  want to mark/unmark attendance without the MCP, in a script, or on a\n  machine where the MCP isn't installed.\n---\n\n# setlist.fm via curl + fpx (no MCP)\n\nsetlist.fm is **hybrid**: reads and writes use two different surfaces.\n\n- **Reads** — a documented public REST API (`api.setlist.fm/rest`) keyed by\n  an **`x-api-key` header**. Plain `curl`, no browser, no fpx.\n- **Writes** — the only mutation setlist.fm offers is the site's \"I was\n  there\" attendance toggle, and it exists **only on the logged-in website**\n  (`www.setlist.fm`, server-rendered Apache Wicket), not the REST API. There's\n  no login form to script — the credential is your browser's session cookie.\n  `fpx` lifts that cookie out of your signed-in tab **once**; every actual\n  request afterward is plain `curl` carrying it. No bridge round-trip per call.\n\nThis is the same data/actions the `setlist_*` MCP tools expose, reached with\n`curl` instead of a running server.\n\n## Part 1 — reads (curl + API key)\n\n### One-time setup\n\nApply for a free key at <https://www.setlist.fm/settings/api>, non-commercial\nuse only (see API terms below), then:\n\n```sh\nexport SETLIST_API_KEY=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\n```\n\n### Core call\n\n```sh\ncurl -s 'https://api.setlist.fm/rest/1.0/search/artists?artistName=Radiohead' \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' \\\n  | jq '.artist[] | {name, mbid, url}'\n```\n\n`Accept: application/json` is required — the API serves XML otherwise.\nOptional `Accept-Language: en|es|fr|de|pt|tr|it|pl` localizes city/country\nnames. Standard tier is **~2 req/sec, 1440/day** — a 429 means slow down.\n\n**Dates are `dd-MM-yyyy` on the wire** (NOT ISO) for `date` search params and\n`lastUpdated` (`yyyyMMddHHmmss`); `eventDate` comes back the same way in\nresponses — convert both directions yourself (the MCP does this for you via\n`dmyToIso`/`isoToDmy`; a shell one-liner: `date -j -f '%Y-%m-%d' '+%d-%m-%Y'`\non macOS).\n\nThe full request catalog (all 15 read endpoints, params, and `jq` projections)\nis in `references/rest-api.md`.\n\n### The one rule: chain ids, don't guess them\n\nEvery entity is keyed by an id you get from a search: artists by **`mbid`**\n(MusicBrainz id), setlists by **`setlistId`**, venues by **`venueId`**,\ncities by **`geoId`**. Search first, then feed the id into the matching\n`get`/`setlists` endpoint. Got a setlist.fm URL instead of an id? The id is\nthe trailing hex token before `.html`, e.g.\n`.../setlist/.../...-4ba8a766.html` → `4ba8a766` — no fetch needed.\n\n### Attribution (compliance, not optional)\n\nThe API terms require a **followable link** to setlist.fm wherever this data\nis shown. Every artist/setlist/venue object has a `url` — surface it as a\nclickable link (no `nofollow`) when you present the data. Also: **no\npersistent caching** (fetch live each time, don't build a local store) and\n**non-commercial use only** on the free key.\n\n## Part 2 — the attendance write (fpx-captured cookie + curl)\n\nThere is no API for this — it's the logged-in **website's** Wicket AJAX\ncontrol, so you need your own `www.setlist.fm` session cookie.\n\n### One-time setup\n\n```sh\nnpm install -g @fetchproxy/cli                      # provides `fpx`\nfpx profile add setlist --domain setlist.fm          # apex scope\nfpx pair -p setlist                                  # prints a pair code → approve in ContextMint Bridge\n```\n\nRequirements: **ContextMint Bridge** installed ([releases](https://github.com/nullnet-app/contextmint-bridge/releases) — Chrome: load the chrome zip unpacked; Safari isn't available yet, so use Chrome for now), an open, **signed-in**\n`www.setlist.fm` tab, and Chrome **Site access** allowing `setlist.fm`.\nPairing persists after the first approval.\nContextMint Bridge is the renamed fetchproxy extension (same maintainer; [fetchproxy README](https://github.com/chrischall/fetchproxy#extension)); source at [nullnet-app/contextmint-bridge](https://github.com/nullnet-app/contextmint-bridge) — build it, or verify a release zip with `shasum -a 256 -c contextmint-bridge-chrome-<version>.zip.sha256`.\n\n### Capture the session cookie (once per shell session)\n\nThe session cookie (`JSESSIONID`) is **HttpOnly** — only `fpx cookies`\n(which uses `chrome.cookies.get`, not page JS) can read it:\n\n```sh\nCOOKIE=$(fpx cookies -p setlist --domain www.setlist.fm \\\n  | jq -r '[.JSESSIONID, .RememberMeCookie, .[\"aws-waf-token\"]]\n           | map(select(. != null)) | join(\"; \")')\n```\n\n`JSESSIONID` (Wicket session) or `RememberMeCookie` (remembered login) must\nbe present — if both are empty you're not signed in on that tab. `COOKIE` is\nnow a ready-to-send `Cookie:` header value; every `curl` below reuses it. A\nbrowser-like `User-Agent` is required too — the bare curl default trips the\nsite's bot heuristics:\n\n```sh\nUA='Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36'\n```\n\n### Toggle attendance\n\nFull walkthrough (resolve the setlist URL, fetch the page, parse the toggle\ncontrol, replay it, verify) is in `references/attendance-write.md` — it's\nseveral steps, not a one-liner, because the control is a per-render Wicket\nAJAX anchor you must parse out of the page HTML first.\n\n**Always dry-run first**: read the page, check the control's current state,\nand only send the AJAX toggle if it doesn't already match what you want.\n**Never trust the toggle's response status as proof** — always re-fetch the\npage afterward and re-check the control's state.\n\n## Exit codes / failure modes\n\n- **Read path (curl)**: a non-2xx from `api.setlist.fm` is a normal HTTP\n  error — `403` on a valid-looking key usually means a bad/revoked key, not\n  \"no key\" (that's a `401`).\n- **Write path (fpx)**: `fpx cookies` exit codes — `2` bridge unavailable\n  (extension not connected / not paired → `fpx pair -p setlist`), `3` bot\n  wall, `4` upstream non-2xx. Once you have `COOKIE`, subsequent `curl`\n  failures are yours to diagnose (see \"session expired\" below) — `fpx` is\n  out of the loop.\n- **Session expired mid-session**: a page fetched with your cookie renders\n  **logged out** (a `href=\"/signin\"` or `/login` link instead of the\n  attendance control) — re-run the cookie capture (re-approve in ContextMint Bridge\n  if needed) and retry.\n\n## Notes\n\n- Reads need no session at all — keep `SETLIST_API_KEY` and the fpx cookie\n  capture on completely separate paths, same as the MCP does.\n- `fpx health -p setlist` shows bridge connection state if the cookie\n  capture fails outright.\n- This project is developed and maintained by AI (Claude).\n\nFile v1.1.8:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"setlist-fpx\",\n  \"version\": \"1.1.8\",\n  \"publishedAt\": 1791588521698\n}\n\nFile v1.1.8:references/attendance-write.md\n\n# Attendance write walkthrough (fpx cookie + curl)\n\nMirrors `setlist-mcp`'s `setlist_mark_attended` / `setlist_unmark_attended`\ntools exactly (`src/tools/attendance.ts`, `src/web-client.ts`). There is no\nJSON API for this — you're driving the same server-rendered Apache Wicket\npage a signed-in browser would.\n\nPrereqs: `SETLIST_API_KEY` set (Part 1) and `COOKIE`/`UA` captured per\n`SKILL.md` Part 2.\n\n## Step 0 — resolve the setlist's canonical URL\n\nYou need the setlist's public page path. Either you already have a\nsetlist.fm URL (skip to step 1), or resolve it via the API using the\n`setlistId`:\n\n```sh\nSETLIST_ID=63de4613\nMETA=$(curl -s \"https://api.setlist.fm/rest/1.0/setlist/$SETLIST_ID\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json')\nURL=$(echo \"$META\" | jq -r '.url')          # e.g. https://www.setlist.fm/setlist/.../...-4ba8a766.html\nPATH_ONLY=$(echo \"$URL\" | sed -E 's#https?://[^/]+##')   # /setlist/.../...-4ba8a766.html\n```\n\n## Step 1 — fetch the page, authenticated\n\n```sh\nHTML=$(curl -s \"https://www.setlist.fm$PATH_ONLY\" \\\n  -H \"Cookie: $COOKIE\" -H \"User-Agent: $UA\")\n```\n\n**Check you're actually signed in** before going further — a logged-out page\nlinks to sign-in instead of showing the attendance control:\n\n```sh\necho \"$HTML\" | grep -qE 'href=\"/(signin|login)\\b' && echo \"SESSION EXPIRED — re-capture the cookie\" >&2\n```\n\n## Step 2 — find the attendance control\n\nThe control is an `<a>` tag with a `wicketAjaxGet('...')` onclick and a\n`title` of either:\n\n- `\"Add this setlist to your attended shows.\"` → **not** currently attended\n- `\"Remove this setlist from your attended shows.\"` → **currently** attended\n\nExtract both the per-render AJAX URL and the current state with `perl`\n(installed on macOS by default). For each `<a>` tag, check for\n`wicketAjaxGet(...)` and `title=\"...\"` independently within the same\n800-char window — same as `src/tools/attendance.ts`'s `parseAttendance()` —\nrather than requiring `title=` to appear before `onclick=`, since real\nmarkup doesn't guarantee that ordering. Entity-decode the URL the same way\n`decodeEntities()` does: `&amp;` plus numeric character references\n(`&#x..;` hex and `&#..;` decimal), which a plain grep would mangle:\n\n```sh\nread -r AJAX_URL CURRENTLY_ATTENDED < <(\n  echo \"$HTML\" | perl -0777 -ne '\n    while (/<a\\b(.{0,800})/gs) {\n      my $seg = $1;\n      next unless $seg =~ /wicketAjaxGet\\(\\s*[\\x27\"]([^\\x27\"]+)[\\x27\"]/;\n      my $url = $1;\n      next unless $seg =~ /title=\"([^\"]*attended shows[^\"]*)\"/;\n      my $attended = $1 =~ /remove/i ? 1 : 0;\n      $url =~ s/&amp;/&/g;\n      $url =~ s/&#x([0-9a-fA-F]+);/chr(hex($1))/ge;\n      $url =~ s/&#(\\d+);/chr($1)/ge;\n      print \"$url $attended\\n\";\n      last;\n    }\n  '\n)\n```\n\nIf `$AJAX_URL` is empty: the control genuinely isn't on the page (rare — a\nlayout change, or you hit a bot-rate-limit page) — re-check step 1's\nlogged-out test first, since that's the far more common cause.\n\n## Step 3 — decide: no-op, dry-run, or toggle\n\n```sh\nDESIRED=1   # 1 = mark attended, 0 = unmark\n\nif [ \"$CURRENTLY_ATTENDED\" = \"$DESIRED\" ]; then\n  echo \"already in the desired state — nothing to do\"\n  exit 0\nfi\n\necho \"would toggle attended: $CURRENTLY_ATTENDED -> $DESIRED (dry run — confirm before sending)\"\n```\n\nThe control is a toggle, so sending it from the wrong state does the\nopposite of what you meant. Don't send the toggle in step 4\nuntil you've deliberately decided to (a script arg, an explicit prompt —\nwhatever fits your use). Never toggle blind.\n\n## Step 4 — replay the Wicket AJAX toggle\n\n`$AJAX_URL` is relative to the setlist page. Resolve it, then GET it with\nthe Wicket AJAX headers (these are required — a plain GET without them gets\nignored or errors):\n\n```sh\nFULL_AJAX_URL=$(python3 -c \"import urllib.parse,sys; print(urllib.parse.urljoin(sys.argv[1], sys.argv[2]))\" \"$URL\" \"$AJAX_URL\")\nAJAX_PATH=$(echo \"$FULL_AJAX_URL\" | sed -E 's#https?://[^/]+##')\nBASE_URL_HEADER=$(echo \"$PATH_ONLY\" | sed -E 's#^/##')   # Wicket-Ajax-BaseURL wants no leading slash\n\ncurl -s \"https://www.setlist.fm$AJAX_PATH\" \\\n  -H \"Cookie: $COOKIE\" -H \"User-Agent: $UA\" \\\n  -H 'Wicket-Ajax: true' \\\n  -H \"Wicket-Ajax-BaseURL: $BASE_URL_HEADER\" \\\n  -H 'X-Requested-With: XMLHttpRequest' \\\n  -H 'Accept: text/xml, text/javascript, application/xml, text/html, */*' \\\n  > /dev/null\n```\n\n## Step 5 — verify by re-reading (never trust the response status)\n\nA `200` from step 4 is not proof — re-fetch the page and re-run step 2's\nparse:\n\n```sh\nHTML2=$(curl -s \"https://www.setlist.fm$PATH_ONLY\" -H \"Cookie: $COOKIE\" -H \"User-Agent: $UA\")\nread -r _ NOW_ATTENDED < <(echo \"$HTML2\" | perl -0777 -ne '\n  while (/<a\\b(.{0,800})/gs) {\n    my $seg = $1;\n    next unless $seg =~ /wicketAjaxGet/;\n    next unless $seg =~ /title=\"([^\"]*attended shows[^\"]*)\"/;\n    print \"x \", ($1 =~ /remove/i ? 1 : 0), \"\\n\";\n    last;\n  }\n')\n\nif [ \"$NOW_ATTENDED\" = \"$DESIRED\" ]; then\n  echo \"verified: attended=$NOW_ATTENDED\"\nelse\n  echo \"WARNING: toggle sent but re-read did not confirm the new state — check on setlist.fm\" >&2\nfi\n```\n\n## Notes\n\n- **Pace writes.** Rapid-fire authenticated requests appear to get the\n  session throttled/invalidated mid-batch — space multiple toggles at least\n  ~600ms apart, same as the MCP's write pacer.\n- **Transient 5xx**: `www.setlist.fm`'s gateway occasionally 500/502/503s —\n  retry a couple times with a ~1.2s gap before concluding something's wrong.\n- **This is the ONLY write surface setlist.fm has.** There's no way to edit\n  setlist song content, create setlists, etc. via this path or the API —\n  those are wiki edits done through the full website UI, out of scope here.\n\nFile v1.1.8:references/rest-api.md\n\n# setlist.fm REST API — read endpoints for curl\n\nBase URL: `https://api.setlist.fm/rest` · every call: `-H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json'`.\nOptional: `-H \"Accept-Language: en\"` (one of `en es fr de pt tr it pl`) to localize city/country names.\n\nAll 15 read endpoints below are exactly what `setlist-mcp`'s tools call — same paths, same params.\n\n---\n\n## Artists\n\n### 1. Search artists\n\n```sh\ncurl -s 'https://api.setlist.fm/rest/1.0/search/artists' \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' \\\n  --get --data-urlencode 'artistName=Radiohead' --data-urlencode 'sort=relevance' \\\n  | jq '.artist[] | {name, mbid, url}'\n```\nParams: `artistName`, `artistMbid`, `sort` (`sortName` default | `relevance`), `p` (page, default 1).\n\n### 2. Get artist by mbid\n\n```sh\ncurl -s \"https://api.setlist.fm/rest/1.0/artist/$MBID\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' | jq\n```\n\n### 3. Artist's setlists (most recent first)\n\n```sh\ncurl -s \"https://api.setlist.fm/rest/1.0/artist/$MBID/setlists?p=1\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' \\\n  | jq '.setlist[] | {id, eventDate, venue: .venue.name, city: .venue.city.name, songCount, hasSongs}'\n```\n`songCount`/`setCount`/`hasSongs` are annotations the MCP adds by walking the\nresponse locally (not native API fields) — derive them yourself if you need\nthem: `songCount = ([.sets.set[].song[]?] | length)`.\n\n---\n\n## Setlists\n\n### 4. Search setlists\n\nProvide at least one filter. `date` is `dd-MM-yyyy` (NOT ISO); `lastUpdated`\nis `yyyyMMddHHmmss` UTC.\n\n```sh\ncurl -s 'https://api.setlist.fm/rest/1.0/search/setlists' \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' \\\n  --get \\\n  --data-urlencode 'artistName=Radiohead' \\\n  --data-urlencode 'date=28-08-2025' \\\n  --data-urlencode 'cityName=Charlotte' \\\n  | jq '.setlist[] | {id, url, venue: .venue.name, eventDate}'\n```\nOther params: `artistMbid`, `venueName`, `venueId`, `cityId`, `state`,\n`stateCode`, `countryCode`, `tourName`, `year`, `p`. Omit the artist and pass\n`venueName`/`venueId` + `date` to list every performer at a venue that day.\n\n### 5. Get setlist by id\n\n```sh\ncurl -s \"https://api.setlist.fm/rest/1.0/setlist/$SETLIST_ID\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' | jq\n```\nSongs live in `.sets.set[].song[]`; a `set` may have `encore` (1 = first\nencore) and `name` (e.g. an acoustic set). A `song` may carry `tape: true`\n(pre-recorded, not performed), `cover` (original artist), `with` (guest), and\n`info` (a note like \"acoustic\").\n\n### 6. Get a specific historical version\n\n```sh\ncurl -s \"https://api.setlist.fm/rest/1.0/setlist/version/$VERSION_ID\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' | jq\n```\n\n---\n\n## Venues\n\n### 7. Search venues\n\n```sh\ncurl -s 'https://api.setlist.fm/rest/1.0/search/venues' \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' \\\n  --get --data-urlencode 'name=The Fillmore' --data-urlencode 'cityName=Charlotte' \\\n  | jq '.venue[] | {id, name, city: .city.name, url}'\n```\nParams: `name`, `cityName`, `cityId`, `state`, `stateCode`, `country`, `p`.\n\n### 8. Get venue by id\n\n```sh\ncurl -s \"https://api.setlist.fm/rest/1.0/venue/$VENUE_ID\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' | jq\n```\n\n### 9. Venue's setlists\n\n```sh\ncurl -s \"https://api.setlist.fm/rest/1.0/venue/$VENUE_ID/setlists?p=1\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' \\\n  | jq '.setlist[] | {id, artist: .artist.name, eventDate}'\n```\n\n---\n\n## Geo\n\n### 10. Search cities\n\n```sh\ncurl -s 'https://api.setlist.fm/rest/1.0/search/cities' \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' \\\n  --get --data-urlencode 'name=Charlotte' \\\n  | jq '.cities[] | {geoId: .id, name, state, country: .country.name}'\n```\nParams: `name`, `country`, `state`, `stateCode`, `p`. Feed `geoId` into\n`cityId` on search/setlists and search/venues.\n\n### 11. Get city by geoId\n\n```sh\ncurl -s \"https://api.setlist.fm/rest/1.0/city/$GEO_ID\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' | jq\n```\n\n### 12. List countries\n\n```sh\ncurl -s 'https://api.setlist.fm/rest/1.0/search/countries' \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' \\\n  | jq '.country[] | {code, name}'\n```\nUse `code` as `countryCode` in search/setlists.\n\n---\n\n## Users\n\n### 13. Get user profile\n\n```sh\ncurl -s \"https://api.setlist.fm/rest/1.0/user/$USER_ID\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' | jq\n```\n\n### 14. User's attended shows\n\n```sh\ncurl -s \"https://api.setlist.fm/rest/1.0/user/$USER_ID/attended?p=1\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' \\\n  | jq '.setlist[] | {id, artist: .artist.name, eventDate}'\n```\n\n### 15. User's created/edited setlists\n\n```sh\ncurl -s \"https://api.setlist.fm/rest/1.0/user/$USER_ID/edited?p=1\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' | jq\n```\n\n---\n\n## Errors\n\n- `400` — missing/invalid params (e.g. no filter on search/setlists).\n- `401` — no `x-api-key` header sent at all.\n- `403` — a key WAS sent but is invalid/revoked (not the same as 401 — don't\n  conflate \"no key\" with \"bad key\").\n- `404` — id not found.\n- `429` — over the standard-tier ~2 req/sec / 1440/day limit; back off ~2s\n  and retry once.\n\nFile v1.1.8:skill-card.md\n\n## Description:\n\nGuides agents in searching and reading setlist.fm concert data with its REST API and updating a user's attendance through an authenticated browser session, without running an MCP server.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[chrischall](https://clawhub.ai/user/chrischall)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nDevelopers and other setlist.fm users can use this skill to look up artists, setlists, venues, cities, and users from a shell, or mark and unmark their attendance at a show. The free setlist.fm API key is restricted to non-commercial use under the site's API terms.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: A captured setlist.fm session cookie can enable account actions if disclosed.\n\nMitigation: Treat the cookie like a password; keep it out of logs, shell history, commits, and shared transcripts, and use the bridge only if you trust it with your session.\n\nRisk: Blindly toggling attendance can undo the intended state.\n\nMitigation: Check the current state, dry-run before sending the toggle, and re-fetch the page to verify the result.\n\n## Reference(s):\n\n- [setlist-fpx release on ClawHub](https://clawhub.ai/chrischall/skills/setlist-fpx)\n- [setlist.fm API key and terms](https://www.setlist.fm/settings/api)\n- [REST API read endpoints](references/rest-api.md)\n- [Attendance update walkthrough](references/attendance-write.md)\n- [ContextMint Bridge releases](https://github.com/nullnet-app/contextmint-bridge/releases)\n\n## Skill Output:\n\n**Output Type(s):** [Guidance, Shell commands, Configuration instructions]\n\n**Output Format:** [Markdown with shell command examples]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [REST reads require an API key; attendance changes require an authenticated setlist.fm session and confirmation of the current state.]\n\n## Skill Version(s):\n\n1.1.8 (source: ClawHub release evidence)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment.\n\nArchive v1.1.7: 5 files, 9736 bytes\n\nFiles: references/attendance-write.md (5672b), references/rest-api.md (5375b), skill-card.md (2121b), SKILL.md (7015b), _meta.json (130b)\n\nFile v1.1.7:SKILL.md\n\n---\nname: setlist-fpx\ndescription: >-\n  Query and update setlist.fm from a shell without running the setlist-mcp\n  server — search/read concert setlists, artists, venues, cities, and users\n  via plain curl against the public REST API (an x-api-key header), and\n  toggle \"I was there\" attendance on a setlist via the authenticated website,\n  using an fpx-captured session cookie. Use when you want setlist.fm data or\n  want to mark/unmark attendance without the MCP, in a script, or on a\n  machine where the MCP isn't installed.\n---\n\n# setlist.fm via curl + fpx (no MCP)\n\nsetlist.fm is **hybrid**: reads and writes use two different surfaces.\n\n- **Reads** — a documented public REST API (`api.setlist.fm/rest`) keyed by\n  an **`x-api-key` header**. Plain `curl`, no browser, no fpx.\n- **Writes** — the only mutation setlist.fm offers is the site's \"I was\n  there\" attendance toggle, and it exists **only on the logged-in website**\n  (`www.setlist.fm`, server-rendered Apache Wicket), not the REST API. There's\n  no login form to script — the credential is your browser's session cookie.\n  `fpx` lifts that cookie out of your signed-in tab **once**; every actual\n  request afterward is plain `curl` carrying it. No bridge round-trip per call.\n\nThis is the same data/actions the `setlist_*` MCP tools expose, reached with\n`curl` instead of a running server.\n\n## Part 1 — reads (curl + API key)\n\n### One-time setup\n\nApply for a free key at <https://www.setlist.fm/settings/api>, non-commercial\nuse only (see API terms below), then:\n\n```sh\nexport SETLIST_API_KEY=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\n```\n\n### Core call\n\n```sh\ncurl -s 'https://api.setlist.fm/rest/1.0/search/artists?artistName=Radiohead' \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' \\\n  | jq '.artist[] | {name, mbid, url}'\n```\n\n`Accept: application/json` is required — the API serves XML otherwise.\nOptional `Accept-Language: en|es|fr|de|pt|tr|it|pl` localizes city/country\nnames. Standard tier is **~2 req/sec, 1440/day** — a 429 means slow down.\n\n**Dates are `dd-MM-yyyy` on the wire** (NOT ISO) for `date` search params and\n`lastUpdated` (`yyyyMMddHHmmss`); `eventDate` comes back the same way in\nresponses — convert both directions yourself (the MCP does this for you via\n`dmyToIso`/`isoToDmy`; a shell one-liner: `date -j -f '%Y-%m-%d' '+%d-%m-%Y'`\non macOS).\n\nThe full request catalog (all 15 read endpoints, params, and `jq` projections)\nis in `references/rest-api.md`.\n\n### The one rule: chain ids, don't guess them\n\nEvery entity is keyed by an id you get from a search: artists by **`mbid`**\n(MusicBrainz id), setlists by **`setlistId`**, venues by **`venueId`**,\ncities by **`geoId`**. Search first, then feed the id into the matching\n`get`/`setlists` endpoint. Got a setlist.fm URL instead of an id? The id is\nthe trailing hex token before `.html`, e.g.\n`.../setlist/.../...-4ba8a766.html` → `4ba8a766` — no fetch needed.\n\n### Attribution (compliance, not optional)\n\nThe API terms require a **followable link** to setlist.fm wherever this data\nis shown. Every artist/setlist/venue object has a `url` — surface it as a\nclickable link (no `nofollow`) when you present the data. Also: **no\npersistent caching** (fetch live each time, don't build a local store) and\n**non-commercial use only** on the free key.\n\n## Part 2 — the attendance write (fpx-captured cookie + curl)\n\nThere is no API for this — it's the logged-in **website's** Wicket AJAX\ncontrol, so you need your own `www.setlist.fm` session cookie.\n\n### One-time setup\n\n```sh\nnpm install -g @fetchproxy/cli                      # provides `fpx`\nfpx profile add setlist --domain setlist.fm          # apex scope\nfpx pair -p setlist                                  # prints a pair code → approve in ContextMint Bridge\n```\n\nRequirements: **ContextMint Bridge** installed ([releases](https://github.com/nullnet-app/contextmint-bridge/releases) — Chrome: load the chrome zip unpacked; Safari isn't available yet, so use Chrome for now), an open, **signed-in**\n`www.setlist.fm` tab, and Chrome **Site access** allowing `setlist.fm`.\nPairing persists after the first approval.\nContextMint Bridge is the renamed fetchproxy extension (same maintainer; [fetchproxy README](https://github.com/chrischall/fetchproxy#extension)); source at [nullnet-app/contextmint-bridge](https://github.com/nullnet-app/contextmint-bridge) — build it, or verify a release zip with `shasum -a 256 -c contextmint-bridge-chrome-<version>.zip.sha256`.\n\n### Capture the session cookie (once per shell session)\n\nThe session cookie (`JSESSIONID`) is **HttpOnly** — only `fpx cookies`\n(which uses `chrome.cookies.get`, not page JS) can read it:\n\n```sh\nCOOKIE=$(fpx cookies -p setlist --domain www.setlist.fm \\\n  | jq -r '[.JSESSIONID, .RememberMeCookie, .[\"aws-waf-token\"]]\n           | map(select(. != null)) | join(\"; \")')\n```\n\n`JSESSIONID` (Wicket session) or `RememberMeCookie` (remembered login) must\nbe present — if both are empty you're not signed in on that tab. `COOKIE` is\nnow a ready-to-send `Cookie:` header value; every `curl` below reuses it. A\nbrowser-like `User-Agent` is required too — the bare curl default trips the\nsite's bot heuristics:\n\n```sh\nUA='Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36'\n```\n\n### Toggle attendance\n\nFull walkthrough (resolve the setlist URL, fetch the page, parse the toggle\ncontrol, replay it, verify) is in `references/attendance-write.md` — it's\nseveral steps, not a one-liner, because the control is a per-render Wicket\nAJAX anchor you must parse out of the page HTML first.\n\n**Always dry-run first**: read the page, check the control's current state,\nand only send the AJAX toggle if it doesn't already match what you want.\n**Never trust the toggle's response status as proof** — always re-fetch the\npage afterward and re-check the control's state.\n\n## Exit codes / failure modes\n\n- **Read path (curl)**: a non-2xx from `api.setlist.fm` is a normal HTTP\n  error — `403` on a valid-looking key usually means a bad/revoked key, not\n  \"no key\" (that's a `401`).\n- **Write path (fpx)**: `fpx cookies` exit codes — `2` bridge unavailable\n  (extension not connected / not paired → `fpx pair -p setlist`), `3` bot\n  wall, `4` upstream non-2xx. Once you have `COOKIE`, subsequent `curl`\n  failures are yours to diagnose (see \"session expired\" below) — `fpx` is\n  out of the loop.\n- **Session expired mid-session**: a page fetched with your cookie renders\n  **logged out** (a `href=\"/signin\"` or `/login` link instead of the\n  attendance control) — re-run the cookie capture (re-approve in ContextMint Bridge\n  if needed) and retry.\n\n## Notes\n\n- Reads need no session at all — keep `SETLIST_API_KEY` and the fpx cookie\n  capture on completely separate paths, same as the MCP does.\n- `fpx health -p setlist` shows bridge connection state if the cookie\n  capture fails outright.\n- This project is developed and maintained by AI (Claude).\n\nFile v1.1.7:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"setlist-fpx\",\n  \"version\": \"1.1.7\",\n  \"publishedAt\": 1791380392220\n}\n\nFile v1.1.7:references/attendance-write.md\n\n# Attendance write walkthrough (fpx cookie + curl)\n\nMirrors `setlist-mcp`'s `setlist_mark_attended` / `setlist_unmark_attended`\ntools exactly (`src/tools/attendance.ts`, `src/web-client.ts`). There is no\nJSON API for this — you're driving the same server-rendered Apache Wicket\npage a signed-in browser would.\n\nPrereqs: `SETLIST_API_KEY` set (Part 1) and `COOKIE`/`UA` captured per\n`SKILL.md` Part 2.\n\n## Step 0 — resolve the setlist's canonical URL\n\nYou need the setlist's public page path. Either you already have a\nsetlist.fm URL (skip to step 1), or resolve it via the API using the\n`setlistId`:\n\n```sh\nSETLIST_ID=63de4613\nMETA=$(curl -s \"https://api.setlist.fm/rest/1.0/setlist/$SETLIST_ID\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json')\nURL=$(echo \"$META\" | jq -r '.url')          # e.g. https://www.setlist.fm/setlist/.../...-4ba8a766.html\nPATH_ONLY=$(echo \"$URL\" | sed -E 's#https?://[^/]+##')   # /setlist/.../...-4ba8a766.html\n```\n\n## Step 1 — fetch the page, authenticated\n\n```sh\nHTML=$(curl -s \"https://www.setlist.fm$PATH_ONLY\" \\\n  -H \"Cookie: $COOKIE\" -H \"User-Agent: $UA\")\n```\n\n**Check you're actually signed in** before going further — a logged-out page\nlinks to sign-in instead of showing the attendance control:\n\n```sh\necho \"$HTML\" | grep -qE 'href=\"/(signin|login)\\b' && echo \"SESSION EXPIRED — re-capture the cookie\" >&2\n```\n\n## Step 2 — find the attendance control\n\nThe control is an `<a>` tag with a `wicketAjaxGet('...')` onclick and a\n`title` of either:\n\n- `\"Add this setlist to your attended shows.\"` → **not** currently attended\n- `\"Remove this setlist from your attended shows.\"` → **currently** attended\n\nExtract both the per-render AJAX URL and the current state with `perl`\n(installed on macOS by default). For each `<a>` tag, check for\n`wicketAjaxGet(...)` and `title=\"...\"` independently within the same\n800-char window — same as `src/tools/attendance.ts`'s `parseAttendance()` —\nrather than requiring `title=` to appear before `onclick=`, since real\nmarkup doesn't guarantee that ordering. Entity-decode the URL the same way\n`decodeEntities()` does: `&amp;` plus numeric character references\n(`&#x..;` hex and `&#..;` decimal), which a plain grep would mangle:\n\n```sh\nread -r AJAX_URL CURRENTLY_ATTENDED < <(\n  echo \"$HTML\" | perl -0777 -ne '\n    while (/<a\\b(.{0,800})/gs) {\n      my $seg = $1;\n      next unless $seg =~ /wicketAjaxGet\\(\\s*[\\x27\"]([^\\x27\"]+)[\\x27\"]/;\n      my $url = $1;\n      next unless $seg =~ /title=\"([^\"]*attended shows[^\"]*)\"/;\n      my $attended = $1 =~ /remove/i ? 1 : 0;\n      $url =~ s/&amp;/&/g;\n      $url =~ s/&#x([0-9a-fA-F]+);/chr(hex($1))/ge;\n      $url =~ s/&#(\\d+);/chr($1)/ge;\n      print \"$url $attended\\n\";\n      last;\n    }\n  '\n)\n```\n\nIf `$AJAX_URL` is empty: the control genuinely isn't on the page (rare — a\nlayout change, or you hit a bot-rate-limit page) — re-check step 1's\nlogged-out test first, since that's the far more common cause.\n\n## Step 3 — decide: no-op, dry-run, or toggle\n\n```sh\nDESIRED=1   # 1 = mark attended, 0 = unmark\n\nif [ \"$CURRENTLY_ATTENDED\" = \"$DESIRED\" ]; then\n  echo \"already in the desired state — nothing to do\"\n  exit 0\nfi\n\necho \"would toggle attended: $CURRENTLY_ATTENDED -> $DESIRED (dry run — confirm before sending)\"\n```\n\nThe control is a toggle, so sending it from the wrong state does the\nopposite of what you meant. Don't send the toggle in step 4\nuntil you've deliberately decided to (a script arg, an explicit prompt —\nwhatever fits your use). Never toggle blind.\n\n## Step 4 — replay the Wicket AJAX toggle\n\n`$AJAX_URL` is relative to the setlist page. Resolve it, then GET it with\nthe Wicket AJAX headers (these are required — a plain GET without them gets\nignored or errors):\n\n```sh\nFULL_AJAX_URL=$(python3 -c \"import urllib.parse,sys; print(urllib.parse.urljoin(sys.argv[1], sys.argv[2]))\" \"$URL\" \"$AJAX_URL\")\nAJAX_PATH=$(echo \"$FULL_AJAX_URL\" | sed -E 's#https?://[^/]+##')\nBASE_URL_HEADER=$(echo \"$PATH_ONLY\" | sed -E 's#^/##')   # Wicket-Ajax-BaseURL wants no leading slash\n\ncurl -s \"https://www.setlist.fm$AJAX_PATH\" \\\n  -H \"Cookie: $COOKIE\" -H \"User-Agent: $UA\" \\\n  -H 'Wicket-Ajax: true' \\\n  -H \"Wicket-Ajax-BaseURL: $BASE_URL_HEADER\" \\\n  -H 'X-Requested-With: XMLHttpRequest' \\\n  -H 'Accept: text/xml, text/javascript, application/xml, text/html, */*' \\\n  > /dev/null\n```\n\n## Step 5 — verify by re-reading (never trust the response status)\n\nA `200` from step 4 is not proof — re-fetch the page and re-run step 2's\nparse:\n\n```sh\nHTML2=$(curl -s \"https://www.setlist.fm$PATH_ONLY\" -H \"Cookie: $COOKIE\" -H \"User-Agent: $UA\")\nread -r _ NOW_ATTENDED < <(echo \"$HTML2\" | perl -0777 -ne '\n  while (/<a\\b(.{0,800})/gs) {\n    my $seg = $1;\n    next unless $seg =~ /wicketAjaxGet/;\n    next unless $seg =~ /title=\"([^\"]*attended shows[^\"]*)\"/;\n    print \"x \", ($1 =~ /remove/i ? 1 : 0), \"\\n\";\n    last;\n  }\n')\n\nif [ \"$NOW_ATTENDED\" = \"$DESIRED\" ]; then\n  echo \"verified: attended=$NOW_ATTENDED\"\nelse\n  echo \"WARNING: toggle sent but re-read did not confirm the new state — check on setlist.fm\" >&2\nfi\n```\n\n## Notes\n\n- **Pace writes.** Rapid-fire authenticated requests appear to get the\n  session throttled/invalidated mid-batch — space multiple toggles at least\n  ~600ms apart, same as the MCP's write pacer.\n- **Transient 5xx**: `www.setlist.fm`'s gateway occasionally 500/502/503s —\n  retry a couple times with a ~1.2s gap before concluding something's wrong.\n- **This is the ONLY write surface setlist.fm has.** There's no way to edit\n  setlist song content, create setlists, etc. via this path or the API —\n  those are wiki edits done through the full website UI, out of scope here.\n\nFile v1.1.7:references/rest-api.md\n\n# setlist.fm REST API — read endpoints for curl\n\nBase URL: `https://api.setlist.fm/rest` · every call: `-H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json'`.\nOptional: `-H \"Accept-Language: en\"` (one of `en es fr de pt tr it pl`) to localize city/country names.\n\nAll 15 read endpoints below are exactly what `setlist-mcp`'s tools call — same paths, same params.\n\n---\n\n## Artists\n\n### 1. Search artists\n\n```sh\ncurl -s 'https://api.setlist.fm/rest/1.0/search/artists' \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' \\\n  --get --data-urlencode 'artistName=Radiohead' --data-urlencode 'sort=relevance' \\\n  | jq '.artist[] | {name, mbid, url}'\n```\nParams: `artistName`, `artistMbid`, `sort` (`sortName` default | `relevance`), `p` (page, default 1).\n\n### 2. Get artist by mbid\n\n```sh\ncurl -s \"https://api.setlist.fm/rest/1.0/artist/$MBID\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' | jq\n```\n\n### 3. Artist's setlists (most recent first)\n\n```sh\ncurl -s \"https://api.setlist.fm/rest/1.0/artist/$MBID/setlists?p=1\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' \\\n  | jq '.setlist[] | {id, eventDate, venue: .venue.name, city: .venue.city.name, songCount, hasSongs}'\n```\n`songCount`/`setCount`/`hasSongs` are annotations the MCP adds by walking the\nresponse locally (not native API fields) — derive them yourself if you need\nthem: `songCount = ([.sets.set[].song[]?] | length)`.\n\n---\n\n## Setlists\n\n### 4. Search setlists\n\nProvide at least one filter. `date` is `dd-MM-yyyy` (NOT ISO); `lastUpdated`\nis `yyyyMMddHHmmss` UTC.\n\n```sh\ncurl -s 'https://api.setlist.fm/rest/1.0/search/setlists' \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' \\\n  --get \\\n  --data-urlencode 'artistName=Radiohead' \\\n  --data-urlencode 'date=28-08-2025' \\\n  --data-urlencode 'cityName=Charlotte' \\\n  | jq '.setlist[] | {id, url, venue: .venue.name, eventDate}'\n```\nOther params: `artistMbid`, `venueName`, `venueId`, `cityId`, `state`,\n`stateCode`, `countryCode`, `tourName`, `year`, `p`. Omit the artist and pass\n`venueName`/`venueId` + `date` to list every performer at a venue that day.\n\n### 5. Get setlist by id\n\n```sh\ncurl -s \"https://api.setlist.fm/rest/1.0/setlist/$SETLIST_ID\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' | jq\n```\nSongs live in `.sets.set[].song[]`; a `set` may have `encore` (1 = first\nencore) and `name` (e.g. an acoustic set). A `song` may carry `tape: true`\n(pre-recorded, not performed), `cover` (original artist), `with` (guest), and\n`info` (a note like \"acoustic\").\n\n### 6. Get a specific historical version\n\n```sh\ncurl -s \"https://api.setlist.fm/rest/1.0/setlist/version/$VERSION_ID\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' | jq\n```\n\n---\n\n## Venues\n\n### 7. Search venues\n\n```sh\ncurl -s 'https://api.setlist.fm/rest/1.0/search/venues' \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' \\\n  --get --data-urlencode 'name=The Fillmore' --data-urlencode 'cityName=Charlotte' \\\n  | jq '.venue[] | {id, name, city: .city.name, url}'\n```\nParams: `name`, `cityName`, `cityId`, `state`, `stateCode`, `country`, `p`.\n\n### 8. Get venue by id\n\n```sh\ncurl -s \"https://api.setlist.fm/rest/1.0/venue/$VENUE_ID\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' | jq\n```\n\n### 9. Venue's setlists\n\n```sh\ncurl -s \"https://api.setlist.fm/rest/1.0/venue/$VENUE_ID/setlists?p=1\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' \\\n  | jq '.setlist[] | {id, artist: .artist.name, eventDate}'\n```\n\n---\n\n## Geo\n\n### 10. Search cities\n\n```sh\ncurl -s 'https://api.setlist.fm/rest/1.0/search/cities' \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' \\\n  --get --data-urlencode 'name=Charlotte' \\\n  | jq '.cities[] | {geoId: .id, name, state, country: .country.name}'\n```\nParams: `name`, `country`, `state`, `stateCode`, `p`. Feed `geoId` into\n`cityId` on search/setlists and search/venues.\n\n### 11. Get city by geoId\n\n```sh\ncurl -s \"https://api.setlist.fm/rest/1.0/city/$GEO_ID\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' | jq\n```\n\n### 12. List countries\n\n```sh\ncurl -s 'https://api.setlist.fm/rest/1.0/search/countries' \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' \\\n  | jq '.country[] | {code, name}'\n```\nUse `code` as `countryCode` in search/setlists.\n\n---\n\n## Users\n\n### 13. Get user profile\n\n```sh\ncurl -s \"https://api.setlist.fm/rest/1.0/user/$USER_ID\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' | jq\n```\n\n### 14. User's attended shows\n\n```sh\ncurl -s \"https://api.setlist.fm/rest/1.0/user/$USER_ID/attended?p=1\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' \\\n  | jq '.setlist[] | {id, artist: .artist.name, eventDate}'\n```\n\n### 15. User's created/edited setlists\n\n```sh\ncurl -s \"https://api.setlist.fm/rest/1.0/user/$USER_ID/edited?p=1\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' | jq\n```\n\n---\n\n## Errors\n\n- `400` — missing/invalid params (e.g. no filter on search/setlists).\n- `401` — no `x-api-key` header sent at all.\n- `403` — a key WAS sent but is invalid/revoked (not the same as 401 — don't\n  conflate \"no key\" with \"bad key\").\n- `404` — id not found.\n- `429` — over the standard-tier ~2 req/sec / 1440/day limit; back off ~2s\n  and retry once.\n\nFile v1.1.7:skill-card.md\n\n## Description:\n\nHelps agents search and read setlist.fm concert data through its REST API and update the user's attendance through an authenticated browser session, without running an MCP server.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[chrischall](https://clawhub.ai/user/chrischall)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nDevelopers and concertgoers use this skill to look up artists, venues, and setlists or intentionally mark and unmark their own attendance from the shell. The free setlist.fm API key is limited to non-commercial API use.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: Access to a signed-in browser session exposes sensitive setlist.fm cookies that could be reused to act on the user's account.\n\nMitigation: Treat captured cookies like passwords; do not save or log them, and clear shell variables after use.\n\nRisk: The attendance control is a toggle and can change account state in the wrong direction.\n\nMitigation: Check the current state first, perform writes only on explicit request, and re-fetch the page to confirm the result.\n\n## Reference(s):\n\n- [setlist.fm REST API read endpoints](references/rest-api.md)\n- [Attendance write walkthrough](references/attendance-write.md)\n- [setlist.fm API access](https://www.setlist.fm/settings/api)\n- [ContextMint Bridge](https://github.com/nullnet-app/contextmint-bridge)\n- [ClawHub skill release](https://clawhub.ai/chrischall/skills/setlist-fpx)\n\n## Skill Output:\n\n**Output Type(s):** [Text, Shell commands, Guidance]\n\n**Output Format:** [Markdown with shell examples and setlist.fm links]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [API reads need a key; attendance changes require the user's signed-in session.]\n\n## Skill Version(s):\n\n1.1.7 (source: ClawHub release metadata)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment.\n\nArchive v1.1.6: 5 files, 9694 bytes\n\nFiles: references/attendance-write.md (5672b), references/rest-api.md (5375b), skill-card.md (2036b), SKILL.md (7015b), _meta.json (130b)\n\nFile v1.1.6:SKILL.md\n\n---\nname: setlist-fpx\ndescription: >-\n  Query and update setlist.fm from a shell without running the setlist-mcp\n  server — search/read concert setlists, artists, venues, cities, and users\n  via plain curl against the public REST API (an x-api-key header), and\n  toggle \"I was there\" attendance on a setlist via the authenticated website,\n  using an fpx-captured session cookie. Use when you want setlist.fm data or\n  want to mark/unmark attendance without the MCP, in a script, or on a\n  machine where the MCP isn't installed.\n---\n\n# setlist.fm via curl + fpx (no MCP)\n\nsetlist.fm is **hybrid**: reads and writes use two different surfaces.\n\n- **Reads** — a documented public REST API (`api.setlist.fm/rest`) keyed by\n  an **`x-api-key` header**. Plain `curl`, no browser, no fpx.\n- **Writes** — the only mutation setlist.fm offers is the site's \"I was\n  there\" attendance toggle, and it exists **only on the logged-in website**\n  (`www.setlist.fm`, server-rendered Apache Wicket), not the REST API. There's\n  no login form to script — the credential is your browser's session cookie.\n  `fpx` lifts that cookie out of your signed-in tab **once**; every actual\n  request afterward is plain `curl` carrying it. No bridge round-trip per call.\n\nThis is the same data/actions the `setlist_*` MCP tools expose, reached with\n`curl` instead of a running server.\n\n## Part 1 — reads (curl + API key)\n\n### One-time setup\n\nApply for a free key at <https://www.setlist.fm/settings/api>, non-commercial\nuse only (see API terms below), then:\n\n```sh\nexport SETLIST_API_KEY=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\n```\n\n### Core call\n\n```sh\ncurl -s 'https://api.setlist.fm/rest/1.0/search/artists?artistName=Radiohead' \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' \\\n  | jq '.artist[] | {name, mbid, url}'\n```\n\n`Accept: application/json` is required — the API serves XML otherwise.\nOptional `Accept-Language: en|es|fr|de|pt|tr|it|pl` localizes city/country\nnames. Standard tier is **~2 req/sec, 1440/day** — a 429 means slow down.\n\n**Dates are `dd-MM-yyyy` on the wire** (NOT ISO) for `date` search params and\n`lastUpdated` (`yyyyMMddHHmmss`); `eventDate` comes back the same way in\nresponses — convert both directions yourself (the MCP does this for you via\n`dmyToIso`/`isoToDmy`; a shell one-liner: `date -j -f '%Y-%m-%d' '+%d-%m-%Y'`\non macOS).\n\nThe full request catalog (all 15 read endpoints, params, and `jq` projections)\nis in `references/rest-api.md`.\n\n### The one rule: chain ids, don't guess them\n\nEvery entity is keyed by an id you get from a search: artists by **`mbid`**\n(MusicBrainz id), setlists by **`setlistId`**, venues by **`venueId`**,\ncities by **`geoId`**. Search first, then feed the id into the matching\n`get`/`setlists` endpoint. Got a setlist.fm URL instead of an id? The id is\nthe trailing hex token before `.html`, e.g.\n`.../setlist/.../...-4ba8a766.html` → `4ba8a766` — no fetch needed.\n\n### Attribution (compliance, not optional)\n\nThe API terms require a **followable link** to setlist.fm wherever this data\nis shown. Every artist/setlist/venue object has a `url` — surface it as a\nclickable link (no `nofollow`) when you present the data. Also: **no\npersistent caching** (fetch live each time, don't build a local store) and\n**non-commercial use only** on the free key.\n\n## Part 2 — the attendance write (fpx-captured cookie + curl)\n\nThere is no API for this — it's the logged-in **website's** Wicket AJAX\ncontrol, so you need your own `www.setlist.fm` session cookie.\n\n### One-time setup\n\n```sh\nnpm install -g @fetchproxy/cli                      # provides `fpx`\nfpx profile add setlist --domain setlist.fm          # apex scope\nfpx pair -p setlist                                  # prints a pair code → approve in ContextMint Bridge\n```\n\nRequirements: **ContextMint Bridge** installed ([releases](https://github.com/nullnet-app/contextmint-bridge/releases) — Chrome: load the chrome zip unpacked; Safari isn't available yet, so use Chrome for now), an open, **signed-in**\n`www.setlist.fm` tab, and Chrome **Site access** allowing `setlist.fm`.\nPairing persists after the first approval.\nContextMint Bridge is the renamed fetchproxy extension (same maintainer; [fetchproxy README](https://github.com/chrischall/fetchproxy#extension)); source at [nullnet-app/contextmint-bridge](https://github.com/nullnet-app/contextmint-bridge) — build it, or verify a release zip with `shasum -a 256 -c contextmint-bridge-chrome-<version>.zip.sha256`.\n\n### Capture the session cookie (once per shell session)\n\nThe session cookie (`JSESSIONID`) is **HttpOnly** — only `fpx cookies`\n(which uses `chrome.cookies.get`, not page JS) can read it:\n\n```sh\nCOOKIE=$(fpx cookies -p setlist --domain www.setlist.fm \\\n  | jq -r '[.JSESSIONID, .RememberMeCookie, .[\"aws-waf-token\"]]\n           | map(select(. != null)) | join(\"; \")')\n```\n\n`JSESSIONID` (Wicket session) or `RememberMeCookie` (remembered login) must\nbe present — if both are empty you're not signed in on that tab. `COOKIE` is\nnow a ready-to-send `Cookie:` header value; every `curl` below reuses it. A\nbrowser-like `User-Agent` is required too — the bare curl default trips the\nsite's bot heuristics:\n\n```sh\nUA='Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36'\n```\n\n### Toggle attendance\n\nFull walkthrough (resolve the setlist URL, fetch the page, parse the toggle\ncontrol, replay it, verify) is in `references/attendance-write.md` — it's\nseveral steps, not a one-liner, because the control is a per-render Wicket\nAJAX anchor you must parse out of the page HTML first.\n\n**Always dry-run first**: read the page, check the control's current state,\nand only send the AJAX toggle if it doesn't already match what you want.\n**Never trust the toggle's response status as proof** — always re-fetch the\npage afterward and re-check the control's state.\n\n## Exit codes / failure modes\n\n- **Read path (curl)**: a non-2xx from `api.setlist.fm` is a normal HTTP\n  error — `403` on a valid-looking key usually means a bad/revoked key, not\n  \"no key\" (that's a `401`).\n- **Write path (fpx)**: `fpx cookies` exit codes — `2` bridge unavailable\n  (extension not connected / not paired → `fpx pair -p setlist`), `3` bot\n  wall, `4` upstream non-2xx. Once you have `COOKIE`, subsequent `curl`\n  failures are yours to diagnose (see \"session expired\" below) — `fpx` is\n  out of the loop.\n- **Session expired mid-session**: a page fetched with your cookie renders\n  **logged out** (a `href=\"/signin\"` or `/login` link instead of the\n  attendance control) — re-run the cookie capture (re-approve in ContextMint Bridge\n  if needed) and retry.\n\n## Notes\n\n- Reads need no session at all — keep `SETLIST_API_KEY` and the fpx cookie\n  capture on completely separate paths, same as the MCP does.\n- `fpx health -p setlist` shows bridge connection state if the cookie\n  capture fails outright.\n- This project is developed and maintained by AI (Claude).\n\nFile v1.1.6:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"setlist-fpx\",\n  \"version\": \"1.1.6\",\n  \"publishedAt\": 1791168521475\n}\n\nFile v1.1.6:references/attendance-write.md\n\n# Attendance write walkthrough (fpx cookie + curl)\n\nMirrors `setlist-mcp`'s `setlist_mark_attended` / `setlist_unmark_attended`\ntools exactly (`src/tools/attendance.ts`, `src/web-client.ts`). There is no\nJSON API for this — you're driving the same server-rendered Apache Wicket\npage a signed-in browser would.\n\nPrereqs: `SETLIST_API_KEY` set (Part 1) and `COOKIE`/`UA` captured per\n`SKILL.md` Part 2.\n\n## Step 0 — resolve the setlist's canonical URL\n\nYou need the setlist's public page path. Either you already have a\nsetlist.fm URL (skip to step 1), or resolve it via the API using the\n`setlistId`:\n\n```sh\nSETLIST_ID=63de4613\nMETA=$(curl -s \"https://api.setlist.fm/rest/1.0/setlist/$SETLIST_ID\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json')\nURL=$(echo \"$META\" | jq -r '.url')          # e.g. https://www.setlist.fm/setlist/.../...-4ba8a766.html\nPATH_ONLY=$(echo \"$URL\" | sed -E 's#https?://[^/]+##')   # /setlist/.../...-4ba8a766.html\n```\n\n## Step 1 — fetch the page, authenticated\n\n```sh\nHTML=$(curl -s \"https://www.setlist.fm$PATH_ONLY\" \\\n  -H \"Cookie: $COOKIE\" -H \"User-Agent: $UA\")\n```\n\n**Check you're actually signed in** before going further — a logged-out page\nlinks to sign-in instead of showing the attendance control:\n\n```sh\necho \"$HTML\" | grep -qE 'href=\"/(signin|login)\\b' && echo \"SESSION EXPIRED — re-capture the cookie\" >&2\n```\n\n## Step 2 — find the attendance control\n\nThe control is an `<a>` tag with a `wicketAjaxGet('...')` onclick and a\n`title` of either:\n\n- `\"Add this setlist to your attended shows.\"` → **not** currently attended\n- `\"Remove this setlist from your attended shows.\"` → **currently** attended\n\nExtract both the per-render AJAX URL and the current state with `perl`\n(installed on macOS by default). For each `<a>` tag, check for\n`wicketAjaxGet(...)` and `title=\"...\"` independently within the same\n800-char window — same as `src/tools/attendance.ts`'s `parseAttendance()` —\nrather than requiring `title=` to appear before `onclick=`, since real\nmarkup doesn't guarantee that ordering. Entity-decode the URL the same way\n`decodeEntities()` does: `&amp;` plus numeric character references\n(`&#x..;` hex and `&#..;` decimal), which a plain grep would mangle:\n\n```sh\nread -r AJAX_URL CURRENTLY_ATTENDED < <(\n  echo \"$HTML\" | perl -0777 -ne '\n    while (/<a\\b(.{0,800})/gs) {\n      my $seg = $1;\n      next unless $seg =~ /wicketAjaxGet\\(\\s*[\\x27\"]([^\\x27\"]+)[\\x27\"]/;\n      my $url = $1;\n      next unless $seg =~ /title=\"([^\"]*attended shows[^\"]*)\"/;\n      my $attended = $1 =~ /remove/i ? 1 : 0;\n      $url =~ s/&amp;/&/g;\n      $url =~ s/&#x([0-9a-fA-F]+);/chr(hex($1))/ge;\n      $url =~ s/&#(\\d+);/chr($1)/ge;\n      print \"$url $attended\\n\";\n      last;\n    }\n  '\n)\n```\n\nIf `$AJAX_URL` is empty: the control genuinely isn't on the page (rare — a\nlayout change, or you hit a bot-rate-limit page) — re-check step 1's\nlogged-out test first, since that's the far more common cause.\n\n## Step 3 — decide: no-op, dry-run, or toggle\n\n```sh\nDESIRED=1   # 1 = mark attended, 0 = unmark\n\nif [ \"$CURRENTLY_ATTENDED\" = \"$DESIRED\" ]; then\n  echo \"already in the desired state — nothing to do\"\n  exit 0\nfi\n\necho \"would toggle attended: $CURRENTLY_ATTENDED -> $DESIRED (dry run — confirm before sending)\"\n```\n\nThe control is a toggle, so sending it from the wrong state does the\nopposite of what you meant. Don't send the toggle in step 4\nuntil you've deliberately decided to (a script arg, an explicit prompt —\nwhatever fits your use). Never toggle blind.\n\n## Step 4 — replay the Wicket AJAX toggle\n\n`$AJAX_URL` is relative to the setlist page. Resolve it, then GET it with\nthe Wicket AJAX headers (these are required — a plain GET without them gets\nignored or errors):\n\n```sh\nFULL_AJAX_URL=$(python3 -c \"import urllib.parse,sys; print(urllib.parse.urljoin(sys.argv[1], sys.argv[2]))\" \"$URL\" \"$AJAX_URL\")\nAJAX_PATH=$(echo \"$FULL_AJAX_URL\" | sed -E 's#https?://[^/]+##')\nBASE_URL_HEADER=$(echo \"$PATH_ONLY\" | sed -E 's#^/##')   # Wicket-Ajax-BaseURL wants no leading slash\n\ncurl -s \"https://www.setlist.fm$AJAX_PATH\" \\\n  -H \"Cookie: $COOKIE\" -H \"User-Agent: $UA\" \\\n  -H 'Wicket-Ajax: true' \\\n  -H \"Wicket-Ajax-BaseURL: $BASE_URL_HEADER\" \\\n  -H 'X-Requested-With: XMLHttpRequest' \\\n  -H 'Accept: text/xml, text/javascript, application/xml, text/html, */*' \\\n  > /dev/null\n```\n\n## Step 5 — verify by re-reading (never trust the response status)\n\nA `200` from step 4 is not proof — re-fetch the page and re-run step 2's\nparse:\n\n```sh\nHTML2=$(curl -s \"https://www.setlist.fm$PATH_ONLY\" -H \"Cookie: $COOKIE\" -H \"User-Agent: $UA\")\nread -r _ NOW_ATTENDED < <(echo \"$HTML2\" | perl -0777 -ne '\n  while (/<a\\b(.{0,800})/gs) {\n    my $seg = $1;\n    next unless $seg =~ /wicketAjaxGet/;\n    next unless $seg =~ /title=\"([^\"]*attended shows[^\"]*)\"/;\n    print \"x \", ($1 =~ /remove/i ? 1 : 0), \"\\n\";\n    last;\n  }\n')\n\nif [ \"$NOW_ATTENDED\" = \"$DESIRED\" ]; then\n  echo \"verified: attended=$NOW_ATTENDED\"\nelse\n  echo \"WARNING: toggle sent but re-read did not confirm the new state — check on setlist.fm\" >&2\nfi\n```\n\n## Notes\n\n- **Pace writes.** Rapid-fire authenticated requests appear to get the\n  session throttled/invalidated mid-batch — space multiple toggles at least\n  ~600ms apart, same as the MCP's write pacer.\n- **Transient 5xx**: `www.setlist.fm`'s gateway occasionally 500/502/503s —\n  retry a couple times with a ~1.2s gap before concluding something's wrong.\n- **This is the ONLY write surface setlist.fm has.** There's no way to edit\n  setlist song content, create setlists, etc. via this path or the API —\n  those are wiki edits done through the full website UI, out of scope here.\n\nFile v1.1.6:references/rest-api.md\n\n# setlist.fm REST API — read endpoints for curl\n\nBase URL: `https://api.setlist.fm/rest` · every call: `-H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json'`.\nOptional: `-H \"Accept-Language: en\"` (one of `en es fr de pt tr it pl`) to localize city/country names.\n\nAll 15 read endpoints below are exactly what `setlist-mcp`'s tools call — same paths, same params.\n\n---\n\n## Artists\n\n### 1. Search artists\n\n```sh\ncurl -s 'https://api.setlist.fm/rest/1.0/search/artists' \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' \\\n  --get --data-urlencode 'artistName=Radiohead' --data-urlencode 'sort=relevance' \\\n  | jq '.artist[] | {name, mbid, url}'\n```\nParams: `artistName`, `artistMbid`, `sort` (`sortName` default | `relevance`), `p` (page, default 1).\n\n### 2. Get artist by mbid\n\n```sh\ncurl -s \"https://api.setlist.fm/rest/1.0/artist/$MBID\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' | jq\n```\n\n### 3. Artist's setlists (most recent first)\n\n```sh\ncurl -s \"https://api.setlist.fm/rest/1.0/artist/$MBID/setlists?p=1\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' \\\n  | jq '.setlist[] | {id, eventDate, venue: .venue.name, city: .venue.city.name, songCount, hasSongs}'\n```\n`songCount`/`setCount`/`hasSongs` are annotations the MCP adds by walking the\nresponse locally (not native API fields) — derive them yourself if you need\nthem: `songCount = ([.sets.set[].song[]?] | length)`.\n\n---\n\n## Setlists\n\n### 4. Search setlists\n\nProvide at least one filter. `date` is `dd-MM-yyyy` (NOT ISO); `lastUpdated`\nis `yyyyMMddHHmmss` UTC.\n\n```sh\ncurl -s 'https://api.setlist.fm/rest/1.0/search/setlists' \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' \\\n  --get \\\n  --data-urlencode 'artistName=Radiohead' \\\n  --data-urlencode 'date=28-08-2025' \\\n  --data-urlencode 'cityName=Charlotte' \\\n  | jq '.setlist[] | {id, url, venue: .venue.name, eventDate}'\n```\nOther params: `artistMbid`, `venueName`, `venueId`, `cityId`, `state`,\n`stateCode`, `countryCode`, `tourName`, `year`, `p`. Omit the artist and pass\n`venueName`/`venueId` + `date` to list every performer at a venue that day.\n\n### 5. Get setlist by id\n\n```sh\ncurl -s \"https://api.setlist.fm/rest/1.0/setlist/$SETLIST_ID\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' | jq\n```\nSongs live in `.sets.set[].song[]`; a `set` may have `encore` (1 = first\nencore) and `name` (e.g. an acoustic set). A `song` may carry `tape: true`\n(pre-recorded, not performed), `cover` (original artist), `with` (guest), and\n`info` (a note like \"acoustic\").\n\n### 6. Get a specific historical version\n\n```sh\ncurl -s \"https://api.setlist.fm/rest/1.0/setlist/version/$VERSION_ID\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' | jq\n```\n\n---\n\n## Venues\n\n### 7. Search venues\n\n```sh\ncurl -s 'https://api.setlist.fm/rest/1.0/search/venues' \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' \\\n  --get --data-urlencode 'name=The Fillmore' --data-urlencode 'cityName=Charlotte' \\\n  | jq '.venue[] | {id, name, city: .city.name, url}'\n```\nParams: `name`, `cityName`, `cityId`, `state`, `stateCode`, `country`, `p`.\n\n### 8. Get venue by id\n\n```sh\ncurl -s \"https://api.setlist.fm/rest/1.0/venue/$VENUE_ID\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' | jq\n```\n\n### 9. Venue's setlists\n\n```sh\ncurl -s \"https://api.setlist.fm/rest/1.0/venue/$VENUE_ID/setlists?p=1\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' \\\n  | jq '.setlist[] | {id, artist: .artist.name, eventDate}'\n```\n\n---\n\n## Geo\n\n### 10. Search cities\n\n```sh\ncurl -s 'https://api.setlist.fm/rest/1.0/search/cities' \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' \\\n  --get --data-urlencode 'name=Charlotte' \\\n  | jq '.cities[] | {geoId: .id, name, state, country: .country.name}'\n```\nParams: `name`, `country`, `state`, `stateCode`, `p`. Feed `geoId` into\n`cityId` on search/setlists and search/venues.\n\n### 11. Get city by geoId\n\n```sh\ncurl -s \"https://api.setlist.fm/rest/1.0/city/$GEO_ID\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' | jq\n```\n\n### 12. List countries\n\n```sh\ncurl -s 'https://api.setlist.fm/rest/1.0/search/countries' \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' \\\n  | jq '.country[] | {code, name}'\n```\nUse `code` as `countryCode` in search/setlists.\n\n---\n\n## Users\n\n### 13. Get user profile\n\n```sh\ncurl -s \"https://api.setlist.fm/rest/1.0/user/$USER_ID\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' | jq\n```\n\n### 14. User's attended shows\n\n```sh\ncurl -s \"https://api.setlist.fm/rest/1.0/user/$USER_ID/attended?p=1\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' \\\n  | jq '.setlist[] | {id, artist: .artist.name, eventDate}'\n```\n\n### 15. User's created/edited setlists\n\n```sh\ncurl -s \"https://api.setlist.fm/rest/1.0/user/$USER_ID/edited?p=1\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' | jq\n```\n\n---\n\n## Errors\n\n- `400` — missing/invalid params (e.g. no filter on search/setlists).\n- `401` — no `x-api-key` header sent at all.\n- `403` — a key WAS sent but is invalid/revoked (not the same as 401 — don't\n  conflate \"no key\" with \"bad key\").\n- `404` — id not found.\n- `429` — over the standard-tier ~2 req/sec / 1440/day limit; back off ~2s\n  and retry once.\n\nFile v1.1.6:skill-card.md\n\n## Description:\n\nGuides agents to read setlist.fm concert data via its REST API and update attendance through an authenticated browser session without running an MCP server.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[chrischall](https://clawhub.ai/user/chrischall)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nDevelopers and agents use this skill to search and retrieve setlists, artists, venues, cities, and users from a shell, or to mark and unmark attendance on their own setlist.fm account.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: Capturing a browser session cookie for attendance changes exposes sensitive account credentials to local tooling.\n\nMitigation: Use only your own account, verify the bridge and CLI source, do not print or save cookie values, and clear shell variables after use.\n\nRisk: An attendance toggle can reverse the intended state if sent blindly.\n\nMitigation: Dry-run and check the current state before toggling, then re-fetch the page to verify the result.\n\n## Reference(s):\n\n- [setlist.fm REST API guide](references/rest-api.md)\n- [Attendance update walkthrough](references/attendance-write.md)\n- [setlist.fm API key setup](https://www.setlist.fm/settings/api)\n- [ContextMint Bridge source and releases](https://github.com/nullnet-app/contextmint-bridge)\n\n## Skill Output:\n\n**Output Type(s):** [Guidance, Shell commands]\n\n**Output Format:** [Markdown with shell commands]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Public API reads require an API key; the free key is non-commercial and displayed data requires attribution. Attendance changes require the user's signed-in session.]\n\n## Skill Version(s):\n\n1.1.6 (source: ClawHub release)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment.\n\nArchive v1.1.5: 5 files, 9681 bytes\n\nFiles: references/attendance-write.md (5672b), references/rest-api.md (5375b), skill-card.md (2046b), SKILL.md (7015b), _meta.json (130b)\n\nFile v1.1.5:SKILL.md\n\n---\nname: setlist-fpx\ndescription: >-\n  Query and update setlist.fm from a shell without running the setlist-mcp\n  server — search/read concert setlists, artists, venues, cities, and users\n  via plain curl against the public REST API (an x-api-key header), and\n  toggle \"I was there\" attendance on a setlist via the authenticated website,\n  using an fpx-captured session cookie. Use when you want setlist.fm data or\n  want to mark/unmark attendance without the MCP, in a script, or on a\n  machine where the MCP isn't installed.\n---\n\n# setlist.fm via curl + fpx (no MCP)\n\nsetlist.fm is **hybrid**: reads and writes use two different surfaces.\n\n- **Reads** — a documented public REST API (`api.setlist.fm/rest`) keyed by\n  an **`x-api-key` header**. Plain `curl`, no browser, no fpx.\n- **Writes** — the only mutation setlist.fm offers is the site's \"I was\n  there\" attendance toggle, and it exists **only on the logged-in website**\n  (`www.setlist.fm`, server-rendered Apache Wicket), not the REST API. There's\n  no login form to script — the credential is your browser's session cookie.\n  `fpx` lifts that cookie out of your signed-in tab **once**; every actual\n  request afterward is plain `curl` carrying it. No bridge round-trip per call.\n\nThis is the same data/actions the `setlist_*` MCP tools expose, reached with\n`curl` instead of a running server.\n\n## Part 1 — reads (curl + API key)\n\n### One-time setup\n\nApply for a free key at <https://www.setlist.fm/settings/api>, non-commercial\nuse only (see API terms below), then:\n\n```sh\nexport SETLIST_API_KEY=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\n```\n\n### Core call\n\n```sh\ncurl -s 'https://api.setlist.fm/rest/1.0/search/artists?artistName=Radiohead' \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' \\\n  | jq '.artist[] | {name, mbid, url}'\n```\n\n`Accept: application/json` is required — the API serves XML otherwise.\nOptional `Accept-Language: en|es|fr|de|pt|tr|it|pl` localizes city/country\nnames. Standard tier is **~2 req/sec, 1440/day** — a 429 means slow down.\n\n**Dates are `dd-MM-yyyy` on the wire** (NOT ISO) for `date` search params and\n`lastUpdated` (`yyyyMMddHHmmss`); `eventDate` comes back the same way in\nresponses — convert both directions yourself (the MCP does this for you via\n`dmyToIso`/`isoToDmy`; a shell one-liner: `date -j -f '%Y-%m-%d' '+%d-%m-%Y'`\non macOS).\n\nThe full request catalog (all 15 read endpoints, params, and `jq` projections)\nis in `references/rest-api.md`.\n\n### The one rule: chain ids, don't guess them\n\nEvery entity is keyed by an id you get from a search: artists by **`mbid`**\n(MusicBrainz id), setlists by **`setlistId`**, venues by **`venueId`**,\ncities by **`geoId`**. Search first, then feed the id into the matching\n`get`/`setlists` endpoint. Got a setlist.fm URL instead of an id? The id is\nthe trailing hex token before `.html`, e.g.\n`.../setlist/.../...-4ba8a766.html` → `4ba8a766` — no fetch needed.\n\n### Attribution (compliance, not optional)\n\nThe API terms require a **followable link** to setlist.fm wherever this data\nis shown. Every artist/setlist/venue object has a `url` — surface it as a\nclickable link (no `nofollow`) when you present the data. Also: **no\npersistent caching** (fetch live each time, don't build a local store) and\n**non-commercial use only** on the free key.\n\n## Part 2 — the attendance write (fpx-captured cookie + curl)\n\nThere is no API for this — it's the logged-in **website's** Wicket AJAX\ncontrol, so you need your own `www.setlist.fm` session cookie.\n\n### One-time setup\n\n```sh\nnpm install -g @fetchproxy/cli                      # provides `fpx`\nfpx profile add setlist --domain setlist.fm          # apex scope\nfpx pair -p setlist                                  # prints a pair code → approve in ContextMint Bridge\n```\n\nRequirements: **ContextMint Bridge** installed ([releases](https://github.com/nullnet-app/contextmint-bridge/releases) — Chrome: load the chrome zip unpacked; Safari isn't available yet, so use Chrome for now), an open, **signed-in**\n`www.setlist.fm` tab, and Chrome **Site access** allowing `setlist.fm`.\nPairing persists after the first approval.\nContextMint Bridge is the renamed fetchproxy extension (same maintainer; [fetchproxy README](https://github.com/chrischall/fetchproxy#extension)); source at [nullnet-app/contextmint-bridge](https://github.com/nullnet-app/contextmint-bridge) — build it, or verify a release zip with `shasum -a 256 -c contextmint-bridge-chrome-<version>.zip.sha256`.\n\n### Capture the session cookie (once per shell session)\n\nThe session cookie (`JSESSIONID`) is **HttpOnly** — only `fpx cookies`\n(which uses `chrome.cookies.get`, not page JS) can read it:\n\n```sh\nCOOKIE=$(fpx cookies -p setlist --domain www.setlist.fm \\\n  | jq -r '[.JSESSIONID, .RememberMeCookie, .[\"aws-waf-token\"]]\n           | map(select(. != null)) | join(\"; \")')\n```\n\n`JSESSIONID` (Wicket session) or `RememberMeCookie` (remembered login) must\nbe present — if both are empty you're not signed in on that tab. `COOKIE` is\nnow a ready-to-send `Cookie:` header value; every `curl` below reuses it. A\nbrowser-like `User-Agent` is required too — the bare curl default trips the\nsite's bot heuristics:\n\n```sh\nUA='Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36'\n```\n\n### Toggle attendance\n\nFull walkthrough (resolve the setlist URL, fetch the page, parse the toggle\ncontrol, replay it, verify) is in `references/attendance-write.md` — it's\nseveral steps, not a one-liner, because the control is a per-render Wicket\nAJAX anchor you must parse out of the page HTML first.\n\n**Always dry-run first**: read the page, check the control's current state,\nand only send the AJAX toggle if it doesn't already match what you want.\n**Never trust the toggle's response status as proof** — always re-fetch the\npage afterward and re-check the control's state.\n\n## Exit codes / failure modes\n\n- **Read path (curl)**: a non-2xx from `api.setlist.fm` is a normal HTTP\n  error — `403` on a valid-looking key usually means a bad/revoked key, not\n  \"no key\" (that's a `401`).\n- **Write path (fpx)**: `fpx cookies` exit codes — `2` bridge unavailable\n  (extension not connected / not paired → `fpx pair -p setlist`), `3` bot\n  wall, `4` upstream non-2xx. Once you have `COOKIE`, subsequent `curl`\n  failures are yours to diagnose (see \"session expired\" below) — `fpx` is\n  out of the loop.\n- **Session expired mid-session**: a page fetched with your cookie renders\n  **logged out** (a `href=\"/signin\"` or `/login` link instead of the\n  attendance control) — re-run the cookie capture (re-approve in ContextMint Bridge\n  if needed) and retry.\n\n## Notes\n\n- Reads need no session at all — keep `SETLIST_API_KEY` and the fpx cookie\n  capture on completely separate paths, same as the MCP does.\n- `fpx health -p setlist` shows bridge connection state if the cookie\n  capture fails outright.\n- This project is developed and maintained by AI (Claude).\n\nFile v1.1.5:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"setlist-fpx\",\n  \"version\": \"1.1.5\",\n  \"publishedAt\": 1790991798667\n}\n\nFile v1.1.5:references/attendance-write.md\n\n# Attendance write walkthrough (fpx cookie + curl)\n\nMirrors `setlist-mcp`'s `setlist_mark_attended` / `setlist_unmark_attended`\ntools exactly (`src/tools/attendance.ts`, `src/web-client.ts`). There is no\nJSON API for this — you're driving the same server-rendered Apache Wicket\npage a signed-in browser would.\n\nPrereqs: `SETLIST_API_KEY` set (Part 1) and `COOKIE`/`UA` captured per\n`SKILL.md` Part 2.\n\n## Step 0 — resolve the setlist's canonical URL\n\nYou need the setlist's public page path. Either you already have a\nsetlist.fm URL (skip to step 1), or resolve it via the API using the\n`setlistId`:\n\n```sh\nSETLIST_ID=63de4613\nMETA=$(curl -s \"https://api.setlist.fm/rest/1.0/setlist/$SETLIST_ID\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json')\nURL=$(echo \"$META\" | jq -r '.url')          # e.g. https://www.setlist.fm/setlist/.../...-4ba8a766.html\nPATH_ONLY=$(echo \"$URL\" | sed -E 's#https?://[^/]+##')   # /setlist/.../...-4ba8a766.html\n```\n\n## Step 1 — fetch the page, authenticated\n\n```sh\nHTML=$(curl -s \"https://www.setlist.fm$PATH_ONLY\" \\\n  -H \"Cookie: $COOKIE\" -H \"User-Agent: $UA\")\n```\n\n**Check you're actually signed in** before going further — a logged-out page\nlinks to sign-in instead of showing the attendance control:\n\n```sh\necho \"$HTML\" | grep -qE 'href=\"/(signin|login)\\b' && echo \"SESSION EXPIRED — re-capture the cookie\" >&2\n```\n\n## Step 2 — find the attendance control\n\nThe control is an `<a>` tag with a `wicketAjaxGet('...')` onclick and a\n`title` of either:\n\n- `\"Add this setlist to your attended shows.\"` → **not** currently attended\n- `\"Remove this setlist from your attended shows.\"` → **currently** attended\n\nExtract both the per-render AJAX URL and the current state with `perl`\n(installed on macOS by default). For each `<a>` tag, check for\n`wicketAjaxGet(...)` and `title=\"...\"` independently within the same\n800-char window — same as `src/tools/attendance.ts`'s `parseAttendance()` —\nrather than requiring `title=` to appear before `onclick=`, since real\nmarkup doesn't guarantee that ordering. Entity-decode the URL the same way\n`decodeEntities()` does: `&amp;` plus numeric character references\n(`&#x..;` hex and `&#..;` decimal), which a plain grep would mangle:\n\n```sh\nread -r AJAX_URL CURRENTLY_ATTENDED < <(\n  echo \"$HTML\" | perl -0777 -ne '\n    while (/<a\\b(.{0,800})/gs) {\n      my $seg = $1;\n      next unless $seg =~ /wicketAjaxGet\\(\\s*[\\x27\"]([^\\x27\"]+)[\\x27\"]/;\n      my $url = $1;\n      next unless $seg =~ /title=\"([^\"]*attended shows[^\"]*)\"/;\n      my $attended = $1 =~ /remove/i ? 1 : 0;\n      $url =~ s/&amp;/&/g;\n      $url =~ s/&#x([0-9a-fA-F]+);/chr(hex($1))/ge;\n      $url =~ s/&#(\\d+);/chr($1)/ge;\n      print \"$url $attended\\n\";\n      last;\n    }\n  '\n)\n```\n\nIf `$AJAX_URL` is empty: the control genuinely isn't on the page (rare — a\nlayout change, or you hit a bot-rate-limit page) — re-check step 1's\nlogged-out test first, since that's the far more common cause.\n\n## Step 3 — decide: no-op, dry-run, or toggle\n\n```sh\nDESIRED=1   # 1 = mark attended, 0 = unmark\n\nif [ \"$CURRENTLY_ATTENDED\" = \"$DESIRED\" ]; then\n  echo \"already in the desired state — nothing to do\"\n  exit 0\nfi\n\necho \"would toggle attended: $CURRENTLY_ATTENDED -> $DESIRED (dry run — confirm before sending)\"\n```\n\nThe control is a toggle, so sending it from the wrong state does the\nopposite of what you meant. Don't send the toggle in step 4\nuntil you've deliberately decided to (a script arg, an explicit prompt —\nwhatever fits your use). Never toggle blind.\n\n## Step 4 — replay the Wicket AJAX toggle\n\n`$AJAX_URL` is relative to the setlist page. Resolve it, then GET it with\nthe Wicket AJAX headers (these are required — a plain GET without them gets\nignored or errors):\n\n```sh\nFULL_AJAX_URL=$(python3 -c \"import urllib.parse,sys; print(urllib.parse.urljoin(sys.argv[1], sys.argv[2]))\" \"$URL\" \"$AJAX_URL\")\nAJAX_PATH=$(echo \"$FULL_AJAX_URL\" | sed -E 's#https?://[^/]+##')\nBASE_URL_HEADER=$(echo \"$PATH_ONLY\" | sed -E 's#^/##')   # Wicket-Ajax-BaseURL wants no leading slash\n\ncurl -s \"https://www.setlist.fm$AJAX_PATH\" \\\n  -H \"Cookie: $COOKIE\" -H \"User-Agent: $UA\" \\\n  -H 'Wicket-Ajax: true' \\\n  -H \"Wicket-Ajax-BaseURL: $BASE_URL_HEADER\" \\\n  -H 'X-Requested-With: XMLHttpRequest' \\\n  -H 'Accept: text/xml, text/javascript, application/xml, text/html, */*' \\\n  > /dev/null\n```\n\n## Step 5 — verify by re-reading (never trust the response status)\n\nA `200` from step 4 is not proof — re-fetch the page and re-run step 2's\nparse:\n\n```sh\nHTML2=$(curl -s \"https://www.setlist.fm$PATH_ONLY\" -H \"Cookie: $COOKIE\" -H \"User-Agent: $UA\")\nread -r _ NOW_ATTENDED < <(echo \"$HTML2\" | perl -0777 -ne '\n  while (/<a\\b(.{0,800})/gs) {\n    my $seg = $1;\n    next unless $seg =~ /wicketAjaxGet/;\n    next unless $seg =~ /title=\"([^\"]*attended shows[^\"]*)\"/;\n    print \"x \", ($1 =~ /remove/i ? 1 : 0), \"\\n\";\n    last;\n  }\n')\n\nif [ \"$NOW_ATTENDED\" = \"$DESIRED\" ]; then\n  echo \"verified: attended=$NOW_ATTENDED\"\nelse\n  echo \"WARNING: toggle sent but re-read did not confirm the new state — check on setlist.fm\" >&2\nfi\n```\n\n## Notes\n\n- **Pace writes.** Rapid-fire authenticated requests appear to get the\n  session throttled/invalidated mid-batch — space multiple toggles at least\n  ~600ms apart, same as the MCP's write pacer.\n- **Transient 5xx**: `www.setlist.fm`'s gateway occasionally 500/502/503s —\n  retry a couple times with a ~1.2s gap before concluding something's wrong.\n- **This is the ONLY write surface setlist.fm has.** There's no way to edit\n  setlist song content, create setlists, etc. via this path or the API —\n  those are wiki edits done through the full website UI, out of scope here.\n\nFile v1.1.5:references/rest-api.md\n\n# setlist.fm REST API — read endpoints for curl\n\nBase URL: `https://api.setlist.fm/rest` · every call: `-H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json'`.\nOptional: `-H \"Accept-Language: en\"` (one of `en es fr de pt tr it pl`) to localize city/country names.\n\nAll 15 read endpoints below are exactly what `setlist-mcp`'s tools call — same paths, same params.\n\n---\n\n## Artists\n\n### 1. Search artists\n\n```sh\ncurl -s 'https://api.setlist.fm/rest/1.0/search/artists' \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' \\\n  --get --data-urlencode 'artistName=Radiohead' --data-urlencode 'sort=relevance' \\\n  | jq '.artist[] | {name, mbid, url}'\n```\nParams: `artistName`, `artistMbid`, `sort` (`sortName` default | `relevance`), `p` (page, default 1).\n\n### 2. Get artist by mbid\n\n```sh\ncurl -s \"https://api.setlist.fm/rest/1.0/artist/$MBID\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' | jq\n```\n\n### 3. Artist's setlists (most recent first)\n\n```sh\ncurl -s \"https://api.setlist.fm/rest/1.0/artist/$MBID/setlists?p=1\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' \\\n  | jq '.setlist[] | {id, eventDate, venue: .venue.name, city: .venue.city.name, songCount, hasSongs}'\n```\n`songCount`/`setCount`/`hasSongs` are annotations the MCP adds by walking the\nresponse locally (not native API fields) — derive them yourself if you need\nthem: `songCount = ([.sets.set[].song[]?] | length)`.\n\n---\n\n## Setlists\n\n### 4. Search setlists\n\nProvide at least one filter. `date` is `dd-MM-yyyy` (NOT ISO); `lastUpdated`\nis `yyyyMMddHHmmss` UTC.\n\n```sh\ncurl -s 'https://api.setlist.fm/rest/1.0/search/setlists' \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' \\\n  --get \\\n  --data-urlencode 'artistName=Radiohead' \\\n  --data-urlencode 'date=28-08-2025' \\\n  --data-urlencode 'cityName=Charlotte' \\\n  | jq '.setlist[] | {id, url, venue: .venue.name, eventDate}'\n```\nOther params: `artistMbid`, `venueName`, `venueId`, `cityId`, `state`,\n`stateCode`, `countryCode`, `tourName`, `year`, `p`. Omit the artist and pass\n`venueName`/`venueId` + `date` to list every performer at a venue that day.\n\n### 5. Get setlist by id\n\n```sh\ncurl -s \"https://api.setlist.fm/rest/1.0/setlist/$SETLIST_ID\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' | jq\n```\nSongs live in `.sets.set[].song[]`; a `set` may have `encore` (1 = first\nencore) and `name` (e.g. an acoustic set). A `song` may carry `tape: true`\n(pre-recorded, not performed), `cover` (original artist), `with` (guest), and\n`info` (a note like \"acoustic\").\n\n### 6. Get a specific historical version\n\n```sh\ncurl -s \"https://api.setlist.fm/rest/1.0/setlist/version/$VERSION_ID\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' | jq\n```\n\n---\n\n## Venues\n\n### 7. Search venues\n\n```sh\ncurl -s 'https://api.setlist.fm/rest/1.0/search/venues' \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' \\\n  --get --data-urlencode 'name=The Fillmore' --data-urlencode 'cityName=Charlotte' \\\n  | jq '.venue[] | {id, name, city: .city.name, url}'\n```\nParams: `name`, `cityName`, `cityId`, `state`, `stateCode`, `country`, `p`.\n\n### 8. Get venue by id\n\n```sh\ncurl -s \"https://api.setlist.fm/rest/1.0/venue/$VENUE_ID\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' | jq\n```\n\n### 9. Venue's setlists\n\n```sh\ncurl -s \"https://api.setlist.fm/rest/1.0/venue/$VENUE_ID/setlists?p=1\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' \\\n  | jq '.setlist[] | {id, artist: .artist.name, eventDate}'\n```\n\n---\n\n## Geo\n\n### 10. Search cities\n\n```sh\ncurl -s 'https://api.setlist.fm/rest/1.0/search/cities' \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' \\\n  --get --data-urlencode 'name=Charlotte' \\\n  | jq '.cities[] | {geoId: .id, name, state, country: .country.name}'\n```\nParams: `name`, `country`, `state`, `stateCode`, `p`. Feed `geoId` into\n`cityId` on search/setlists and search/venues.\n\n### 11. Get city by geoId\n\n```sh\ncurl -s \"https://api.setlist.fm/rest/1.0/city/$GEO_ID\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' | jq\n```\n\n### 12. List countries\n\n```sh\ncurl -s 'https://api.setlist.fm/rest/1.0/search/countries' \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' \\\n  | jq '.country[] | {code, name}'\n```\nUse `code` as `countryCode` in search/setlists.\n\n---\n\n## Users\n\n### 13. Get user profile\n\n```sh\ncurl -s \"https://api.setlist.fm/rest/1.0/user/$USER_ID\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' | jq\n```\n\n### 14. User's attended shows\n\n```sh\ncurl -s \"https://api.setlist.fm/rest/1.0/user/$USER_ID/attended?p=1\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' \\\n  | jq '.setlist[] | {id, artist: .artist.name, eventDate}'\n```\n\n### 15. User's created/edited setlists\n\n```sh\ncurl -s \"https://api.setlist.fm/rest/1.0/user/$USER_ID/edited?p=1\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' | jq\n```\n\n---\n\n## Errors\n\n- `400` — missing/invalid params (e.g. no filter on search/setlists).\n- `401` — no `x-api-key` header sent at all.\n- `403` — a key WAS sent but is invalid/revoked (not the same as 401 — don't\n  conflate \"no key\" with \"bad key\").\n- `404` — id not found.\n- `429` — over the standard-tier ~2 req/sec / 1440/day limit; back off ~2s\n  and retry once.\n\nFile v1.1.5:skill-card.md\n\n## Description:\n\nHelps agents query setlist.fm concert data with shell commands and update attendance using an authenticated browser session without running an MCP server.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[chrischall](https://clawhub.ai/user/chrischall)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nDevelopers and concertgoers use this skill to find artists, venues, cities, and setlists or to mark and unmark their own attendance. Free setlist.fm API keys are restricted to non-commercial use; check API terms before commercial use.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: The write workflow handles authenticated browser session cookies and an API key.\n\nMitigation: Use the browser-session helper only if you trust it; keep cookies and the API key out of chats, logs, files, screenshots, and shell history.\n\nRisk: The attendance toggle can change account history or reverse an intended change.\n\nMitigation: Dry-run, check the current state, change only explicitly selected setlists, and re-fetch the page to verify the result.\n\n## Reference(s):\n\n- [setlist.fm REST API read endpoints](references/rest-api.md)\n- [Attendance write walkthrough](references/attendance-write.md)\n- [setlist.fm API key and terms](https://www.setlist.fm/settings/api)\n- [ContextMint Bridge source](https://github.com/nullnet-app/contextmint-bridge)\n\n## Skill Output:\n\n**Output Type(s):** [Shell commands, Guidance]\n\n**Output Format:** [Markdown with shell command examples]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Read results should link to setlist.fm; attendance changes require confirmation and a fresh state check.]\n\n## Skill Version(s):\n\n1.1.5 (source: ClawHub release metadata)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment.\n\nArchive v1.1.4: 5 files, 9701 bytes\n\nFiles: references/attendance-write.md (5672b), references/rest-api.md (5375b), skill-card.md (2029b), SKILL.md (7015b), _meta.json (130b)\n\nFile v1.1.4:SKILL.md\n\n---\nname: setlist-fpx\ndescription: >-\n  Query and update setlist.fm from a shell without running the setlist-mcp\n  server — search/read concert setlists, artists, venues, cities, and users\n  via plain curl against the public REST API (an x-api-key header), and\n  toggle \"I was there\" attendance on a setlist via the authenticated website,\n  using an fpx-captured session cookie. Use when you want setlist.fm data or\n  want to mark/unmark attendance without the MCP, in a script, or on a\n  machine where the MCP isn't installed.\n---\n\n# setlist.fm via curl + fpx (no MCP)\n\nsetlist.fm is **hybrid**: reads and writes use two different surfaces.\n\n- **Reads** — a documented public REST API (`api.setlist.fm/rest`) keyed by\n  an **`x-api-key` header**. Plain `curl`, no browser, no fpx.\n- **Writes** — the only mutation setlist.fm offers is the site's \"I was\n  there\" attendance toggle, and it exists **only on the logged-in website**\n  (`www.setlist.fm`, server-rendered Apache Wicket), not the REST API. There's\n  no login form to script — the credential is your browser's session cookie.\n  `fpx` lifts that cookie out of your signed-in tab **once**; every actual\n  request afterward is plain `curl` carrying it. No bridge round-trip per call.\n\nThis is the same data/actions the `setlist_*` MCP tools expose, reached with\n`curl` instead of a running server.\n\n## Part 1 — reads (curl + API key)\n\n### One-time setup\n\nApply for a free key at <https://www.setlist.fm/settings/api>, non-commercial\nuse only (see API terms below), then:\n\n```sh\nexport SETLIST_API_KEY=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\n```\n\n### Core call\n\n```sh\ncurl -s 'https://api.setlist.fm/rest/1.0/search/artists?artistName=Radiohead' \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' \\\n  | jq '.artist[] | {name, mbid, url}'\n```\n\n`Accept: application/json` is required — the API serves XML otherwise.\nOptional `Accept-Language: en|es|fr|de|pt|tr|it|pl` localizes city/country\nnames. Standard tier is **~2 req/sec, 1440/day** — a 429 means slow down.\n\n**Dates are `dd-MM-yyyy` on the wire** (NOT ISO) for `date` search params and\n`lastUpdated` (`yyyyMMddHHmmss`); `eventDate` comes back the same way in\nresponses — convert both directions yourself (the MCP does this for you via\n`dmyToIso`/`isoToDmy`; a shell one-liner: `date -j -f '%Y-%m-%d' '+%d-%m-%Y'`\non macOS).\n\nThe full request catalog (all 15 read endpoints, params, and `jq` projections)\nis in `references/rest-api.md`.\n\n### The one rule: chain ids, don't guess them\n\nEvery entity is keyed by an id you get from a search: artists by **`mbid`**\n(MusicBrainz id), setlists by **`setlistId`**, venues by **`venueId`**,\ncities by **`geoId`**. Search first, then feed the id into the matching\n`get`/`setlists` endpoint. Got a setlist.fm URL instead of an id? The id is\nthe trailing hex token before `.html`, e.g.\n`.../setlist/.../...-4ba8a766.html` → `4ba8a766` — no fetch needed.\n\n### Attribution (compliance, not optional)\n\nThe API terms require a **followable link** to setlist.fm wherever this data\nis shown. Every artist/setlist/venue object has a `url` — surface it as a\nclickable link (no `nofollow`) when you present the data. Also: **no\npersistent caching** (fetch live each time, don't build a local store) and\n**non-commercial use only** on the free key.\n\n## Part 2 — the attendance write (fpx-captured cookie + curl)\n\nThere is no API for this — it's the logged-in **website's** Wicket AJAX\ncontrol, so you need your own `www.setlist.fm` session cookie.\n\n### One-time setup\n\n```sh\nnpm install -g @fetchproxy/cli                      # provides `fpx`\nfpx profile add setlist --domain setlist.fm          # apex scope\nfpx pair -p setlist                                  # prints a pair code → approve in ContextMint Bridge\n```\n\nRequirements: **ContextMint Bridge** installed ([releases](https://github.com/nullnet-app/contextmint-bridge/releases) — Chrome: load the chrome zip unpacked; Safari isn't available yet, so use Chrome for now), an open, **signed-in**\n`www.setlist.fm` tab, and Chrome **Site access** allowing `setlist.fm`.\nPairing persists after the first approval.\nContextMint Bridge is the renamed fetchproxy extension (same maintainer; [fetchproxy README](https://github.com/chrischall/fetchproxy#extension)); source at [nullnet-app/contextmint-bridge](https://github.com/nullnet-app/contextmint-bridge) — build it, or verify a release zip with `shasum -a 256 -c contextmint-bridge-chrome-<version>.zip.sha256`.\n\n### Capture the session cookie (once per shell session)\n\nThe session cookie (`JSESSIONID`) is **HttpOnly** — only `fpx cookies`\n(which uses `chrome.cookies.get`, not page JS) can read it:\n\n```sh\nCOOKIE=$(fpx cookies -p setlist --domain www.setlist.fm \\\n  | jq -r '[.JSESSIONID, .RememberMeCookie, .[\"aws-waf-token\"]]\n           | map(select(. != null)) | join(\"; \")')\n```\n\n`JSESSIONID` (Wicket session) or `RememberMeCookie` (remembered login) must\nbe present — if both are empty you're not signed in on that tab. `COOKIE` is\nnow a ready-to-send `Cookie:` header value; every `curl` below reuses it. A\nbrowser-like `User-Agent` is required too — the bare curl default trips the\nsite's bot heuristics:\n\n```sh\nUA='Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36'\n```\n\n### Toggle attendance\n\nFull walkthrough (resolve the setlist URL, fetch the page, parse the toggle\ncontrol, replay it, verify) is in `references/attendance-write.md` — it's\nseveral steps, not a one-liner, because the control is a per-render Wicket\nAJAX anchor you must parse out of the page HTML first.\n\n**Always dry-run first**: read the page, check the control's current state,\nand only send the AJAX toggle if it doesn't already match what you want.\n**Never trust the toggle's response status as proof** — always re-fetch the\npage afterward and re-check the control's state.\n\n## Exit codes / failure modes\n\n- **Read path (curl)**: a non-2xx from `api.setlist.fm` is a normal HTTP\n  error — `403` on a valid-looking key usually means a bad/revoked key, not\n  \"no key\" (that's a `401`).\n- **Write path (fpx)**: `fpx cookies` exit codes — `2` bridge unavailable\n  (extension not connected / not paired → `fpx pair -p setlist`), `3` bot\n  wall, `4` upstream non-2xx. Once you have `COOKIE`, subsequent `curl`\n  failures are yours to diagnose (see \"session expired\" below) — `fpx` is\n  out of the loop.\n- **Session expired mid-session**: a page fetched with your cookie renders\n  **logged out** (a `href=\"/signin\"` or `/login` link instead of the\n  attendance control) — re-run the cookie capture (re-approve in ContextMint Bridge\n  if needed) and retry.\n\n## Notes\n\n- Reads need no session at all — keep `SETLIST_API_KEY` and the fpx cookie\n  capture on completely separate paths, same as the MCP does.\n- `fpx health -p setlist` shows bridge connection state if the cookie\n  capture fails outright.\n- This project is developed and maintained by AI (Claude).\n\nFile v1.1.4:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"setlist-fpx\",\n  \"version\": \"1.1.4\",\n  \"publishedAt\": 1790603746097\n}\n\nFile v1.1.4:references/attendance-write.md\n\n# Attendance write walkthrough (fpx cookie + curl)\n\nMirrors `setlist-mcp`'s `setlist_mark_attended` / `setlist_unmark_attended`\ntools exactly (`src/tools/attendance.ts`, `src/web-client.ts`). There is no\nJSON API for this — you're driving the same server-rendered Apache Wicket\npage a signed-in browser would.\n\nPrereqs: `SETLIST_API_KEY` set (Part 1) and `COOKIE`/`UA` captured per\n`SKILL.md` Part 2.\n\n## Step 0 — resolve the setlist's canonical URL\n\nYou need the setlist's public page path. Either you already have a\nsetlist.fm URL (skip to step 1), or resolve it via the API using the\n`setlistId`:\n\n```sh\nSETLIST_ID=63de4613\nMETA=$(curl -s \"https://api.setlist.fm/rest/1.0/setlist/$SETLIST_ID\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json')\nURL=$(echo \"$META\" | jq -r '.url')          # e.g. https://www.setlist.fm/setlist/.../...-4ba8a766.html\nPATH_ONLY=$(echo \"$URL\" | sed -E 's#https?://[^/]+##')   # /setlist/.../...-4ba8a766.html\n```\n\n## Step 1 — fetch the page, authenticated\n\n```sh\nHTML=$(curl -s \"https://www.setlist.fm$PATH_ONLY\" \\\n  -H \"Cookie: $COOKIE\" -H \"User-Agent: $UA\")\n```\n\n**Check you're actually signed in** before going further — a logged-out page\nlinks to sign-in instead of showing the attendance control:\n\n```sh\necho \"$HTML\" | grep -qE 'href=\"/(signin|login)\\b' && echo \"SESSION EXPIRED — re-capture the cookie\" >&2\n```\n\n## Step 2 — find the attendance control\n\nThe control is an `<a>` tag with a `wicketAjaxGet('...')` onclick and a\n`title` of either:\n\n- `\"Add this setlist to your attended shows.\"` → **not** currently attended\n- `\"Remove this setlist from your attended shows.\"` → **currently** attended\n\nExtract both the per-render AJAX URL and the current state with `perl`\n(installed on macOS by default). For each `<a>` tag, check for\n`wicketAjaxGet(...)` and `title=\"...\"` independently within the same\n800-char window — same as `src/tools/attendance.ts`'s `parseAttendance()` —\nrather than requiring `title=` to appear before `onclick=`, since real\nmarkup doesn't guarantee that ordering. Entity-decode the URL the same way\n`decodeEntities()` does: `&amp;` plus numeric character references\n(`&#x..;` hex and `&#..;` decimal), which a plain grep would mangle:\n\n```sh\nread -r AJAX_URL CURRENTLY_ATTENDED < <(\n  echo \"$HTML\" | perl -0777 -ne '\n    while (/<a\\b(.{0,800})/gs) {\n      my $seg = $1;\n      next unless $seg =~ /wicketAjaxGet\\(\\s*[\\x27\"]([^\\x27\"]+)[\\x27\"]/;\n      my $url = $1;\n      next unless $seg =~ /title=\"([^\"]*attended shows[^\"]*)\"/;\n      my $attended = $1 =~ /remove/i ? 1 : 0;\n      $url =~ s/&amp;/&/g;\n      $url =~ s/&#x([0-9a-fA-F]+);/chr(hex($1))/ge;\n      $url =~ s/&#(\\d+);/chr($1)/ge;\n      print \"$url $attended\\n\";\n      last;\n    }\n  '\n)\n```\n\nIf `$AJAX_URL` is empty: the control genuinely isn't on the page (rare — a\nlayout change, or you hit a bot-rate-limit page) — re-check step 1's\nlogged-out test first, since that's the far more common cause.\n\n## Step 3 — decide: no-op, dry-run, or toggle\n\n```sh\nDESIRED=1   # 1 = mark attended, 0 = unmark\n\nif [ \"$CURRENTLY_ATTENDED\" = \"$DESIRED\" ]; then\n  echo \"already in the desired state — nothing to do\"\n  exit 0\nfi\n\necho \"would toggle attended: $CURRENTLY_ATTENDED -> $DESIRED (dry run — confirm before sending)\"\n```\n\nThe control is a toggle, so sending it from the wrong state does the\nopposite of what you meant. Don't send the toggle in step 4\nuntil you've deliberately decided to (a script arg, an explicit prompt —\nwhatever fits your use). Never toggle blind.\n\n## Step 4 — replay the Wicket AJAX toggle\n\n`$AJAX_URL` is relative to the setlist page. Resolve it, then GET it with\nthe Wicket AJAX headers (these are required — a plain GET without them gets\nignored or errors):\n\n```sh\nFULL_AJAX_URL=$(python3 -c \"import urllib.parse,sys; print(urllib.parse.urljoin(sys.argv[1], sys.argv[2]))\" \"$URL\" \"$AJAX_URL\")\nAJAX_PATH=$(echo \"$FULL_AJAX_URL\" | sed -E 's#https?://[^/]+##')\nBASE_URL_HEADER=$(echo \"$PATH_ONLY\" | sed -E 's#^/##')   # Wicket-Ajax-BaseURL wants no leading slash\n\ncurl -s \"https://www.setlist.fm$AJAX_PATH\" \\\n  -H \"Cookie: $COOKIE\" -H \"User-Agent: $UA\" \\\n  -H 'Wicket-Ajax: true' \\\n  -H \"Wicket-Ajax-BaseURL: $BASE_URL_HEADER\" \\\n  -H 'X-Requested-With: XMLHttpRequest' \\\n  -H 'Accept: text/xml, text/javascript, application/xml, text/html, */*' \\\n  > /dev/null\n```\n\n## Step 5 — verify by re-reading (never trust the response status)\n\nA `200` from step 4 is not proof — re-fetch the page and re-run step 2's\nparse:\n\n```sh\nHTML2=$(curl -s \"https://www.setlist.fm$PATH_ONLY\" -H \"Cookie: $COOKIE\" -H \"User-Agent: $UA\")\nread -r _ NOW_ATTENDED < <(echo \"$HTML2\" | perl -0777 -ne '\n  while (/<a\\b(.{0,800})/gs) {\n    my $seg = $1;\n    next unless $seg =~ /wicketAjaxGet/;\n    next unless $seg =~ /title=\"([^\"]*attended shows[^\"]*)\"/;\n    print \"x \", ($1 =~ /remove/i ? 1 : 0), \"\\n\";\n    last;\n  }\n')\n\nif [ \"$NOW_ATTENDED\" = \"$DESIRED\" ]; then\n  echo \"verified: attended=$NOW_ATTENDED\"\nelse\n  echo \"WARNING: toggle sent but re-read did not confirm the new state — check on setlist.fm\" >&2\nfi\n```\n\n## Notes\n\n- **Pace writes.** Rapid-fire authenticated requests appear to get the\n  session throttled/invalidated mid-batch — space multiple toggles at least\n  ~600ms apart, same as the MCP's write pacer.\n- **Transient 5xx**: `www.setlist.fm`'s gateway occasionally 500/502/503s —\n  retry a couple times with a ~1.2s gap before concluding something's wrong.\n- **This is the ONLY write surface setlist.fm has.** There's no way to edit\n  setlist song content, create setlists, etc. via this path or the API —\n  those are wiki edits done through the full website UI, out of scope here.\n\nFile v1.1.4:references/rest-api.md\n\n# setlist.fm REST API — read endpoints for curl\n\nBase URL: `https://api.setlist.fm/rest` · every call: `-H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json'`.\nOptional: `-H \"Accept-Language: en\"` (one of `en es fr de pt tr it pl`) to localize city/country names.\n\nAll 15 read endpoints below are exactly what `setlist-mcp`'s tools call — same paths, same params.\n\n---\n\n## Artists\n\n### 1. Search artists\n\n```sh\ncurl -s 'https://api.setlist.fm/rest/1.0/search/artists' \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' \\\n  --get --data-urlencode 'artistName=Radiohead' --data-urlencode 'sort=relevance' \\\n  | jq '.artist[] | {name, mbid, url}'\n```\nParams: `artistName`, `artistMbid`, `sort` (`sortName` default | `relevance`), `p` (page, default 1).\n\n### 2. Get artist by mbid\n\n```sh\ncurl -s \"https://api.setlist.fm/rest/1.0/artist/$MBID\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' | jq\n```\n\n### 3. Artist's setlists (most recent first)\n\n```sh\ncurl -s \"https://api.setlist.fm/rest/1.0/artist/$MBID/setlists?p=1\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' \\\n  | jq '.setlist[] | {id, eventDate, venue: .venue.name, city: .venue.city.name, songCount, hasSongs}'\n```\n`songCount`/`setCount`/`hasSongs` are annotations the MCP adds by walking the\nresponse locally (not native API fields) — derive them yourself if you need\nthem: `songCount = ([.sets.set[].song[]?] | length)`.\n\n---\n\n## Setlists\n\n### 4. Search setlists\n\nProvide at least one filter. `date` is `dd-MM-yyyy` (NOT ISO); `lastUpdated`\nis `yyyyMMddHHmmss` UTC.\n\n```sh\ncurl -s 'https://api.setlist.fm/rest/1.0/search/setlists' \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' \\\n  --get \\\n  --data-urlencode 'artistName=Radiohead' \\\n  --data-urlencode 'date=28-08-2025' \\\n  --data-urlencode 'cityName=Charlotte' \\\n  | jq '.setlist[] | {id, url, venue: .venue.name, eventDate}'\n```\nOther params: `artistMbid`, `venueName`, `venueId`, `cityId`, `state`,\n`stateCode`, `countryCode`, `tourName`, `year`, `p`. Omit the artist and pass\n`venueName`/`venueId` + `date` to list every performer at a venue that day.\n\n### 5. Get setlist by id\n\n```sh\ncurl -s \"https://api.setlist.fm/rest/1.0/setlist/$SETLIST_ID\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' | jq\n```\nSongs live in `.sets.set[].song[]`; a `set` may have `encore` (1 = first\nencore) and `name` (e.g. an acoustic set). A `song` may carry `tape: true`\n(pre-recorded, not performed), `cover` (original artist), `with` (guest), and\n`info` (a note like \"acoustic\").\n\n### 6. Get a specific historical version\n\n```sh\ncurl -s \"https://api.setlist.fm/rest/1.0/setlist/version/$VERSION_ID\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' | jq\n```\n\n---\n\n## Venues\n\n### 7. Search venues\n\n```sh\ncurl -s 'https://api.setlist.fm/rest/1.0/search/venues' \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' \\\n  --get --data-urlencode 'name=The Fillmore' --data-urlencode 'cityName=Charlotte' \\\n  | jq '.venue[] | {id, name, city: .city.name, url}'\n```\nParams: `name`, `cityName`, `cityId`, `state`, `stateCode`, `country`, `p`.\n\n### 8. Get venue by id\n\n```sh\ncurl -s \"https://api.setlist.fm/rest/1.0/venue/$VENUE_ID\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' | jq\n```\n\n### 9. Venue's setlists\n\n```sh\ncurl -s \"https://api.setlist.fm/rest/1.0/venue/$VENUE_ID/setlists?p=1\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' \\\n  | jq '.setlist[] | {id, artist: .artist.name, eventDate}'\n```\n\n---\n\n## Geo\n\n### 10. Search cities\n\n```sh\ncurl -s 'https://api.setlist.fm/rest/1.0/search/cities' \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' \\\n  --get --data-urlencode 'name=Charlotte' \\\n  | jq '.cities[] | {geoId: .id, name, state, country: .country.name}'\n```\nParams: `name`, `country`, `state`, `stateCode`, `p`. Feed `geoId` into\n`cityId` on search/setlists and search/venues.\n\n### 11. Get city by geoId\n\n```sh\ncurl -s \"https://api.setlist.fm/rest/1.0/city/$GEO_ID\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' | jq\n```\n\n### 12. List countries\n\n```sh\ncurl -s 'https://api.setlist.fm/rest/1.0/search/countries' \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' \\\n  | jq '.country[] | {code, name}'\n```\nUse `code` as `countryCode` in search/setlists.\n\n---\n\n## Users\n\n### 13. Get user profile\n\n```sh\ncurl -s \"https://api.setlist.fm/rest/1.0/user/$USER_ID\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' | jq\n```\n\n### 14. User's attended shows\n\n```sh\ncurl -s \"https://api.setlist.fm/rest/1.0/user/$USER_ID/attended?p=1\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' \\\n  | jq '.setlist[] | {id, artist: .artist.name, eventDate}'\n```\n\n### 15. User's created/edited setlists\n\n```sh\ncurl -s \"https://api.setlist.fm/rest/1.0/user/$USER_ID/edited?p=1\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' | jq\n```\n\n---\n\n## Errors\n\n- `400` — missing/invalid params (e.g. no filter on search/setlists).\n- `401` — no `x-api-key` header sent at all.\n- `403` — a key WAS sent but is invalid/revoked (not the same as 401 — don't\n  conflate \"no key\" with \"bad key\").\n- `404` — id not found.\n- `429` — over the standard-tier ~2 req/sec / 1440/day limit; back off ~2s\n  and retry once.\n\nFile v1.1.4:skill-card.md\n\n## Description:\n\nGuides agents to read setlist.fm data through its REST API and update attendance through a signed-in browser session, without running an MCP server.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[chrischall](https://clawhub.ai/user/chrischall)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nDevelopers and agents can look up setlists, artists, venues, cities, and users from a shell, and—with the account holder's approval—mark or unmark attendance on their own setlist.fm account.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: A browser bridge can access the user's signed-in setlist.fm session cookie; disclosing or retaining the cookie can expose the account.\n\nMitigation: Use only your own account; treat COOKIE and RememberMeCookie as passwords, avoid logging or saving them, and unset them after use.\n\nRisk: The attendance action is a toggle and can reverse the intended state if issued without checking it first.\n\nMitigation: Confirm the dry-run attendance state and intended change before executing the write step.\n\n## Reference(s):\n\n- [setlist.fm API access](https://www.setlist.fm/settings/api)\n- [REST API usage reference](references/rest-api.md)\n- [Attendance update reference](references/attendance-write.md)\n- [ContextMint Bridge releases](https://github.com/nullnet-app/contextmint-bridge/releases)\n\n## Skill Output:\n\n**Output Type(s):** [Text, Shell commands, Guidance]\n\n**Output Format:** [Markdown with shell examples]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Read results may contain setlist.fm links; attendance changes require a separate confirmed write step.]\n\n## Skill Version(s):\n\n1.1.4 (source: ClawHub release metadata)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment.\n\nArchive v1.1.3: 5 files, 9477 bytes\n\nFiles: references/attendance-write.md (5672b), references/rest-api.md (5375b), skill-card.md (2005b), SKILL.md (6492b), _meta.json (130b)\n\nFile v1.1.3:SKILL.md\n\n---\nname: setlist-fpx\ndescription: >-\n  Query and update setlist.fm from a shell without running the setlist-mcp\n  server — search/read concert setlists, artists, venues, cities, and users\n  via plain curl against the public REST API (an x-api-key header), and\n  toggle \"I was there\" attendance on a setlist via the authenticated website,\n  using an fpx-captured session cookie. Use when you want setlist.fm data or\n  want to mark/unmark attendance without the MCP, in a script, or on a\n  machine where the MCP isn't installed.\n---\n\n# setlist.fm via curl + fpx (no MCP)\n\nsetlist.fm is **hybrid**: reads and writes use two different surfaces.\n\n- **Reads** — a documented public REST API (`api.setlist.fm/rest`) keyed by\n  an **`x-api-key` header**. Plain `curl`, no browser, no fpx.\n- **Writes** — the only mutation setlist.fm offers is the site's \"I was\n  there\" attendance toggle, and it exists **only on the logged-in website**\n  (`www.setlist.fm`, server-rendered Apache Wicket), not the REST API. There's\n  no login form to script — the credential is your browser's session cookie.\n  `fpx` lifts that cookie out of your signed-in tab **once**; every actual\n  request afterward is plain `curl` carrying it. No bridge round-trip per call.\n\nThis is the same data/actions the `setlist_*` MCP tools expose, reached with\n`curl` instead of a running server.\n\n## Part 1 — reads (curl + API key)\n\n### One-time setup\n\nApply for a free key at <https://www.setlist.fm/settings/api>, non-commercial\nuse only (see API terms below), then:\n\n```sh\nexport SETLIST_API_KEY=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\n```\n\n### Core call\n\n```sh\ncurl -s 'https://api.setlist.fm/rest/1.0/search/artists?artistName=Radiohead' \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' \\\n  | jq '.artist[] | {name, mbid, url}'\n```\n\n`Accept: application/json` is required — the API serves XML otherwise.\nOptional `Accept-Language: en|es|fr|de|pt|tr|it|pl` localizes city/country\nnames. Standard tier is **~2 req/sec, 1440/day** — a 429 means slow down.\n\n**Dates are `dd-MM-yyyy` on the wire** (NOT ISO) for `date` search params and\n`lastUpdated` (`yyyyMMddHHmmss`); `eventDate` comes back the same way in\nresponses — convert both directions yourself (the MCP does this for you via\n`dmyToIso`/`isoToDmy`; a shell one-liner: `date -j -f '%Y-%m-%d' '+%d-%m-%Y'`\non macOS).\n\nThe full request catalog (all 15 read endpoints, params, and `jq` projections)\nis in `references/rest-api.md`.\n\n### The one rule: chain ids, don't guess them\n\nEvery entity is keyed by an id you get from a search: artists by **`mbid`**\n(MusicBrainz id), setlists by **`setlistId`**, venues by **`venueId`**,\ncities by **`geoId`**. Search first, then feed the id into the matching\n`get`/`setlists` endpoint. Got a setlist.fm URL instead of an id? The id is\nthe trailing hex token before `.html`, e.g.\n`.../setlist/.../...-4ba8a766.html` → `4ba8a766` — no fetch needed.\n\n### Attribution (compliance, not optional)\n\nThe API terms require a **followable link** to setlist.fm wherever this data\nis shown. Every artist/setlist/venue object has a `url` — surface it as a\nclickable link (no `nofollow`) when you present the data. Also: **no\npersistent caching** (fetch live each time, don't build a local store) and\n**non-commercial use only** on the free key.\n\n## Part 2 — the attendance write (fpx-captured cookie + curl)\n\nThere is no API for this — it's the logged-in **website's** Wicket AJAX\ncontrol, so you need your own `www.setlist.fm` session cookie.\n\n### One-time setup\n\n```sh\nnpm install -g @fetchproxy/cli                      # provides `fpx`\nfpx profile add setlist --domain setlist.fm          # apex scope\nfpx pair -p setlist                                  # prints a pair code → approve in Transporter\n```\n\nRequirements: the **Transporter** extension installed, an open, **signed-in**\n`www.setlist.fm` tab, and Chrome **Site access** allowing `setlist.fm`.\nPairing persists after the first approval.\n\n### Capture the session cookie (once per shell session)\n\nThe session cookie (`JSESSIONID`) is **HttpOnly** — only `fpx cookies`\n(which uses `chrome.cookies.get`, not page JS) can read it:\n\n```sh\nCOOKIE=$(fpx cookies -p setlist --domain www.setlist.fm \\\n  | jq -r '[.JSESSIONID, .RememberMeCookie, .[\"aws-waf-token\"]]\n           | map(select(. != null)) | join(\"; \")')\n```\n\n`JSESSIONID` (Wicket session) or `RememberMeCookie` (remembered login) must\nbe present — if both are empty you're not signed in on that tab. `COOKIE` is\nnow a ready-to-send `Cookie:` header value; every `curl` below reuses it. A\nbrowser-like `User-Agent` is required too — the bare curl default trips the\nsite's bot heuristics:\n\n```sh\nUA='Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36'\n```\n\n### Toggle attendance\n\nFull walkthrough (resolve the setlist URL, fetch the page, parse the toggle\ncontrol, replay it, verify) is in `references/attendance-write.md` — it's\nseveral steps, not a one-liner, because the control is a per-render Wicket\nAJAX anchor you must parse out of the page HTML first.\n\n**Always dry-run first**: read the page, check the control's current state,\nand only send the AJAX toggle if it doesn't already match what you want.\n**Never trust the toggle's response status as proof** — always re-fetch the\npage afterward and re-check the control's state.\n\n## Exit codes / failure modes\n\n- **Read path (curl)**: a non-2xx from `api.setlist.fm` is a normal HTTP\n  error — `403` on a valid-looking key usually means a bad/revoked key, not\n  \"no key\" (that's a `401`).\n- **Write path (fpx)**: `fpx cookies` exit codes — `2` bridge unavailable\n  (extension not connected / not paired → `fpx pair -p setlist`), `3` bot\n  wall, `4` upstream non-2xx. Once you have `COOKIE`, subsequent `curl`\n  failures are yours to diagnose (see \"session expired\" below) — `fpx` is\n  out of the loop.\n- **Session expired mid-session**: a page fetched with your cookie renders\n  **logged out** (a `href=\"/signin\"` or `/login` link instead of the\n  attendance control) — re-run the cookie capture (re-approve in Transporter\n  if needed) and retry.\n\n## Notes\n\n- Reads need no session at all — keep `SETLIST_API_KEY` and the fpx cookie\n  capture on completely separate paths, same as the MCP does.\n- `fpx health -p setlist` shows bridge connection state if the cookie\n  capture fails outright.\n- This project is developed and maintained by AI (Claude).\n\nFile v1.1.3:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"setlist-fpx\",\n  \"version\": \"1.1.3\",\n  \"publishedAt\": 1790351494976\n}\n\nFile v1.1.3:references/attendance-write.md\n\n# Attendance write walkthrough (fpx cookie + curl)\n\nMirrors `setlist-mcp`'s `setlist_mark_attended` / `setlist_unmark_attended`\ntools exactly (`src/tools/attendance.ts`, `src/web-client.ts`). There is no\nJSON API for this — you're driving the same server-rendered Apache Wicket\npage a signed-in browser would.\n\nPrereqs: `SETLIST_API_KEY` set (Part 1) and `COOKIE`/`UA` captured per\n`SKILL.md` Part 2.\n\n## Step 0 — resolve the setlist's canonical URL\n\nYou need the setlist's public page path. Either you already have a\nsetlist.fm URL (skip to step 1), or resolve it via the API using the\n`setlistId`:\n\n```sh\nSETLIST_ID=63de4613\nMETA=$(curl -s \"https://api.setlist.fm/rest/1.0/setlist/$SETLIST_ID\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json')\nURL=$(echo \"$META\" | jq -r '.url')          # e.g. https://www.setlist.fm/setlist/.../...-4ba8a766.html\nPATH_ONLY=$(echo \"$URL\" | sed -E 's#https?://[^/]+##')   # /setlist/.../...-4ba8a766.html\n```\n\n## Step 1 — fetch the page, authenticated\n\n```sh\nHTML=$(curl -s \"https://www.setlist.fm$PATH_ONLY\" \\\n  -H \"Cookie: $COOKIE\" -H \"User-Agent: $UA\")\n```\n\n**Check you're actually signed in** before going further — a logged-out page\nlinks to sign-in instead of showing the attendance control:\n\n```sh\necho \"$HTML\" | grep -qE 'href=\"/(signin|login)\\b' && echo \"SESSION EXPIRED — re-capture the cookie\" >&2\n```\n\n## Step 2 — find the attendance control\n\nThe control is an `<a>` tag with a `wicketAjaxGet('...')` onclick and a\n`title` of either:\n\n- `\"Add this setlist to your attended shows.\"` → **not** currently attended\n- `\"Remove this setlist from your attended shows.\"` → **currently** attended\n\nExtract both the per-render AJAX URL and the current state with `perl`\n(installed on macOS by default). For each `<a>` tag, check for\n`wicketAjaxGet(...)` and `title=\"...\"` independently within the same\n800-char window — same as `src/tools/attendance.ts`'s `parseAttendance()` —\nrather than requiring `title=` to appear before `onclick=`, since real\nmarkup doesn't guarantee that ordering. Entity-decode the URL the same way\n`decodeEntities()` does: `&amp;` plus numeric character references\n(`&#x..;` hex and `&#..;` decimal), which a plain grep would mangle:\n\n```sh\nread -r AJAX_URL CURRENTLY_ATTENDED < <(\n  echo \"$HTML\" | perl -0777 -ne '\n    while (/<a\\b(.{0,800})/gs) {\n      my $seg = $1;\n      next unless $seg =~ /wicketAjaxGet\\(\\s*[\\x27\"]([^\\x27\"]+)[\\x27\"]/;\n      my $url = $1;\n      next unless $seg =~ /title=\"([^\"]*attended shows[^\"]*)\"/;\n      my $attended = $1 =~ /remove/i ? 1 : 0;\n      $url =~ s/&amp;/&/g;\n      $url =~ s/&#x([0-9a-fA-F]+);/chr(hex($1))/ge;\n      $url =~ s/&#(\\d+);/chr($1)/ge;\n      print \"$url $attended\\n\";\n      last;\n    }\n  '\n)\n```\n\nIf `$AJAX_URL` is empty: the control genuinely isn't on the page (rare — a\nlayout change, or you hit a bot-rate-limit page) — re-check step 1's\nlogged-out test first, since that's the far more common cause.\n\n## Step 3 — decide: no-op, dry-run, or toggle\n\n```sh\nDESIRED=1   # 1 = mark attended, 0 = unmark\n\nif [ \"$CURRENTLY_ATTENDED\" = \"$DESIRED\" ]; then\n  echo \"already in the desired state — nothing to do\"\n  exit 0\nfi\n\necho \"would toggle attended: $CURRENTLY_ATTENDED -> $DESIRED (dry run — confirm before sending)\"\n```\n\nThe control is a toggle, so sending it from the wrong state does the\nopposite of what you meant. Don't send the toggle in step 4\nuntil you've deliberately decided to (a script arg, an explicit prompt —\nwhatever fits your use). Never toggle blind.\n\n## Step 4 — replay the Wicket AJAX toggle\n\n`$AJAX_URL` is relative to the setlist page. Resolve it, then GET it with\nthe Wicket AJAX headers (these are required — a plain GET without them gets\nignored or errors):\n\n```sh\nFULL_AJAX_URL=$(python3 -c \"import urllib.parse,sys; print(urllib.parse.urljoin(sys.argv[1], sys.argv[2]))\" \"$URL\" \"$AJAX_URL\")\nAJAX_PATH=$(echo \"$FULL_AJAX_URL\" | sed -E 's#https?://[^/]+##')\nBASE_URL_HEADER=$(echo \"$PATH_ONLY\" | sed -E 's#^/##')   # Wicket-Ajax-BaseURL wants no leading slash\n\ncurl -s \"https://www.setlist.fm$AJAX_PATH\" \\\n  -H \"Cookie: $COOKIE\" -H \"User-Agent: $UA\" \\\n  -H 'Wicket-Ajax: true' \\\n  -H \"Wicket-Ajax-BaseURL: $BASE_URL_HEADER\" \\\n  -H 'X-Requested-With: XMLHttpRequest' \\\n  -H 'Accept: text/xml, text/javascript, application/xml, text/html, */*' \\\n  > /dev/null\n```\n\n## Step 5 — verify by re-reading (never trust the response status)\n\nA `200` from step 4 is not proof — re-fetch the page and re-run step 2's\nparse:\n\n```sh\nHTML2=$(curl -s \"https://www.setlist.fm$PATH_ONLY\" -H \"Cookie: $COOKIE\" -H \"User-Agent: $UA\")\nread -r _ NOW_ATTENDED < <(echo \"$HTML2\" | perl -0777 -ne '\n  while (/<a\\b(.{0,800})/gs) {\n    my $seg = $1;\n    next unless $seg =~ /wicketAjaxGet/;\n    next unless $seg =~ /title=\"([^\"]*attended shows[^\"]*)\"/;\n    print \"x \", ($1 =~ /remove/i ? 1 : 0), \"\\n\";\n    last;\n  }\n')\n\nif [ \"$NOW_ATTENDED\" = \"$DESIRED\" ]; then\n  echo \"verified: attended=$NOW_ATTENDED\"\nelse\n  echo \"WARNING: toggle sent but re-read did not confirm the new state — check on setlist.fm\" >&2\nfi\n```\n\n## Notes\n\n- **Pace writes.** Rapid-fire authenticated requests appear to get the\n  session throttled/invalidated mid-batch — space multiple toggles at least\n  ~600ms apart, same as the MCP's write pacer.\n- **Transient 5xx**: `www.setlist.fm`'s gateway occasionally 500/502/503s —\n  retry a couple times with a ~1.2s gap before concluding something's wrong.\n- **This is the ONLY write surface setlist.fm has.** There's no way to edit\n  setlist song content, create setlists, etc. via this path or the API —\n  those are wiki edits done through the full website UI, out of scope here.\n\nFile v1.1.3:references/rest-api.md\n\n# setlist.fm REST API — read endpoints for curl\n\nBase URL: `https://api.setlist.fm/rest` · every call: `-H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json'`.\nOptional: `-H \"Accept-Language: en\"` (one of `en es fr de pt tr it pl`) to localize city/country names.\n\nAll 15 read endpoints below are exactly what `setlist-mcp`'s tools call — same paths, same params.\n\n---\n\n## Artists\n\n### 1. Search artists\n\n```sh\ncurl -s 'https://api.setlist.fm/rest/1.0/search/artists' \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' \\\n  --get --data-urlencode 'artistName=Radiohead' --data-urlencode 'sort=relevance' \\\n  | jq '.artist[] | {name, mbid, url}'\n```\nParams: `artistName`, `artistMbid`, `sort` (`sortName` default | `relevance`), `p` (page, default 1).\n\n### 2. Get artist by mbid\n\n```sh\ncurl -s \"https://api.setlist.fm/rest/1.0/artist/$MBID\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' | jq\n```\n\n### 3. Artist's setlists (most recent first)\n\n```sh\ncurl -s \"https://api.setlist.fm/rest/1.0/artist/$MBID/setlists?p=1\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' \\\n  | jq '.setlist[] | {id, eventDate, venue: .venue.name, city: .venue.city.name, songCount, hasSongs}'\n```\n`songCount`/`setCount`/`hasSongs` are annotations the MCP adds by walking the\nresponse locally (not native API fields) — derive them yourself if you need\nthem: `songCount = ([.sets.set[].song[]?] | length)`.\n\n---\n\n## Setlists\n\n### 4. Search setlists\n\nProvide at least one filter. `date` is `dd-MM-yyyy` (NOT ISO); `lastUpdated`\nis `yyyyMMddHHmmss` UTC.\n\n```sh\ncurl -s 'https://api.setlist.fm/rest/1.0/search/setlists' \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' \\\n  --get \\\n  --data-urlencode 'artistName=Radiohead' \\\n  --data-urlencode 'date=28-08-2025' \\\n  --data-urlencode 'cityName=Charlotte' \\\n  | jq '.setlist[] | {id, url, venue: .venue.name, eventDate}'\n```\nOther params: `artistMbid`, `venueName`, `venueId`, `cityId`, `state`,\n`stateCode`, `countryCode`, `tourName`, `year`, `p`. Omit the artist and pass\n`venueName`/`venueId` + `date` to list every performer at a venue that day.\n\n### 5. Get setlist by id\n\n```sh\ncurl -s \"https://api.setlist.fm/rest/1.0/setlist/$SETLIST_ID\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' | jq\n```\nSongs live in `.sets.set[].song[]`; a `set` may have `encore` (1 = first\nencore) and `name` (e.g. an acoustic set). A `song` may carry `tape: true`\n(pre-recorded, not performed), `cover` (original artist), `with` (guest), and\n`info` (a note like \"acoustic\").\n\n### 6. Get a specific historical version\n\n```sh\ncurl -s \"https://api.setlist.fm/rest/1.0/setlist/version/$VERSION_ID\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' | jq\n```\n\n---\n\n## Venues\n\n### 7. Search venues\n\n```sh\ncurl -s 'https://api.setlist.fm/rest/1.0/search/venues' \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' \\\n  --get --data-urlencode 'name=The Fillmore' --data-urlencode 'cityName=Charlotte' \\\n  | jq '.venue[] | {id, name, city: .city.name, url}'\n```\nParams: `name`, `cityName`, `cityId`, `state`, `stateCode`, `country`, `p`.\n\n### 8. Get venue by id\n\n```sh\ncurl -s \"https://api.setlist.fm/rest/1.0/venue/$VENUE_ID\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' | jq\n```\n\n### 9. Venue's setlists\n\n```sh\ncurl -s \"https://api.setlist.fm/rest/1.0/venue/$VENUE_ID/setlists?p=1\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' \\\n  | jq '.setlist[] | {id, artist: .artist.name, eventDate}'\n```\n\n---\n\n## Geo\n\n### 10. Search cities\n\n```sh\ncurl -s 'https://api.setlist.fm/rest/1.0/search/cities' \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' \\\n  --get --data-urlencode 'name=Charlotte' \\\n  | jq '.cities[] | {geoId: .id, name, state, country: .country.name}'\n```\nParams: `name`, `country`, `state`, `stateCode`, `p`. Feed `geoId` into\n`cityId` on search/setlists and search/venues.\n\n### 11. Get city by geoId\n\n```sh\ncurl -s \"https://api.setlist.fm/rest/1.0/city/$GEO_ID\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' | jq\n```\n\n### 12. List countries\n\n```sh\ncurl -s 'https://api.setlist.fm/rest/1.0/search/countries' \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' \\\n  | jq '.country[] | {code, name}'\n```\nUse `code` as `countryCode` in search/setlists.\n\n---\n\n## Users\n\n### 13. Get user profile\n\n```sh\ncurl -s \"https://api.setlist.fm/rest/1.0/user/$USER_ID\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' | jq\n```\n\n### 14. User's attended shows\n\n```sh\ncurl -s \"https://api.setlist.fm/rest/1.0/user/$USER_ID/attended?p=1\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' \\\n  | jq '.setlist[] | {id, artist: .artist.name, eventDate}'\n```\n\n### 15. User's created/edited setlists\n\n```sh\ncurl -s \"https://api.setlist.fm/rest/1.0/user/$USER_ID/edited?p=1\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' | jq\n```\n\n---\n\n## Errors\n\n- `400` — missing/invalid params (e.g. no filter on search/setlists).\n- `401` — no `x-api-key` header sent at all.\n- `403` — a key WAS sent but is invalid/revoked (not the same as 401 — don't\n  conflate \"no key\" with \"bad key\").\n- `404` — id not found.\n- `429` — over the standard-tier ~2 req/sec / 1440/day limit; back off ~2s\n  and retry once.\n\nFile v1.1.3:skill-card.md\n\n## Description:\n\nHelps agents search and read setlist.fm concert data through its REST API and update a user's attendance through an authenticated browser session, without running an MCP server.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[chrischall](https://clawhub.ai/user/chrischall)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nDevelopers and other setlist.fm users can search concert setlists, artists, venues, cities, and users from a shell, and mark or unmark their own attendance when signed in.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: Capturing and replaying a setlist.fm browser session cookie can expose account access if the value is disclosed.\n\nMitigation: Treat the cookie like a password: never share, log, or paste it into transcripts or scripts; avoid leaving it in shell history, and sign out or revoke the session when finished.\n\nRisk: Blindly triggering the attendance toggle can reverse the intended account setting.\n\nMitigation: Read the current state first, confirm the intended change before toggling, then reload the page to verify the result.\n\n## Reference(s):\n\n- [setlist.fm API access](https://www.setlist.fm/settings/api)\n- [REST API read endpoints](references/rest-api.md)\n- [Attendance write walkthrough](references/attendance-write.md)\n\n## Skill Output:\n\n**Output Type(s):** [Text, Shell commands, Guidance]\n\n**Output Format:** [Markdown with shell commands and JSON examples]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Free API keys are for non-commercial use; show followable setlist.fm attribution links and avoid persistent caching.]\n\n## Skill Version(s):\n\n1.1.3 (source: ClawHub release metadata)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment.\n\nArchive v1.1.2: 5 files, 9658 bytes\n\nFiles: references/attendance-write.md (5617b), references/rest-api.md (5375b), skill-card.md (2567b), SKILL.md (6492b), _meta.json (130b)\n\nFile v1.1.2:SKILL.md\n\n---\nname: setlist-fpx\ndescription: >-\n  Query and update setlist.fm from a shell without running the setlist-mcp\n  server — search/read concert setlists, artists, venues, cities, and users\n  via plain curl against the public REST API (an x-api-key header), and\n  toggle \"I was there\" attendance on a setlist via the authenticated website,\n  using an fpx-captured session cookie. Use when you want setlist.fm data or\n  want to mark/unmark attendance without the MCP, in a script, or on a\n  machine where the MCP isn't installed.\n---\n\n# setlist.fm via curl + fpx (no MCP)\n\nsetlist.fm is **hybrid**: reads and writes use two different surfaces.\n\n- **Reads** — a documented public REST API (`api.setlist.fm/rest`) keyed by\n  an **`x-api-key` header**. Plain `curl`, no browser, no fpx.\n- **Writes** — the only mutation setlist.fm offers is the site's \"I was\n  there\" attendance toggle, and it exists **only on the logged-in website**\n  (`www.setlist.fm`, server-rendered Apache Wicket), not the REST API. There's\n  no login form to script — the credential is your browser's session cookie.\n  `fpx` lifts that cookie out of your signed-in tab **once**; every actual\n  request afterward is plain `curl` carrying it. No bridge round-trip per call.\n\nThis is the same data/actions the `setlist_*` MCP tools expose, reached with\n`curl` instead of a running server.\n\n## Part 1 — reads (curl + API key)\n\n### One-time setup\n\nApply for a free key at <https://www.setlist.fm/settings/api>, non-commercial\nuse only (see API terms below), then:\n\n```sh\nexport SETLIST_API_KEY=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\n```\n\n### Core call\n\n```sh\ncurl -s 'https://api.setlist.fm/rest/1.0/search/artists?artistName=Radiohead' \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' \\\n  | jq '.artist[] | {name, mbid, url}'\n```\n\n`Accept: application/json` is required — the API serves XML otherwise.\nOptional `Accept-Language: en|es|fr|de|pt|tr|it|pl` localizes city/country\nnames. Standard tier is **~2 req/sec, 1440/day** — a 429 means slow down.\n\n**Dates are `dd-MM-yyyy` on the wire** (NOT ISO) for `date` search params and\n`lastUpdated` (`yyyyMMddHHmmss`); `eventDate` comes back the same way in\nresponses — convert both directions yourself (the MCP does this for you via\n`dmyToIso`/`isoToDmy`; a shell one-liner: `date -j -f '%Y-%m-%d' '+%d-%m-%Y'`\non macOS).\n\nThe full request catalog (all 15 read endpoints, params, and `jq` projections)\nis in `references/rest-api.md`.\n\n### The one rule: chain ids, don't guess them\n\nEvery entity is keyed by an id you get from a search: artists by **`mbid`**\n(MusicBrainz id), setlists by **`setlistId`**, venues by **`venueId`**,\ncities by **`geoId`**. Search first, then feed the id into the matching\n`get`/`setlists` endpoint. Got a setlist.fm URL instead of an id? The id is\nthe trailing hex token before `.html`, e.g.\n`.../setlist/.../...-4ba8a766.html` → `4ba8a766` — no fetch needed.\n\n### Attribution (compliance, not optional)\n\nThe API terms require a **followable link** to setlist.fm wherever this data\nis shown. Every artist/setlist/venue object has a `url` — surface it as a\nclickable link (no `nofollow`) when you present the data. Also: **no\npersistent caching** (fetch live each time, don't build a local store) and\n**non-commercial use only** on the free key.\n\n## Part 2 — the attendance write (fpx-captured cookie + curl)\n\nThere is no API for this — it's the logged-in **website's** Wicket AJAX\ncontrol, so you need your own `www.setlist.fm` session cookie.\n\n### One-time setup\n\n```sh\nnpm install -g @fetchproxy/cli                      # provides `fpx`\nfpx profile add setlist --domain setlist.fm          # apex scope\nfpx pair -p setlist                                  # prints a pair code → approve in Transporter\n```\n\nRequirements: the **Transporter** extension installed, an open, **signed-in**\n`www.setlist.fm` tab, and Chrome **Site access** allowing `setlist.fm`.\nPairing persists after the first approval.\n\n### Capture the session cookie (once per shell session)\n\nThe session cookie (`JSESSIONID`) is **HttpOnly** — only `fpx cookies`\n(which uses `chrome.cookies.get`, not page JS) can read it:\n\n```sh\nCOOKIE=$(fpx cookies -p setlist --domain www.setlist.fm \\\n  | jq -r '[.JSESSIONID, .RememberMeCookie, .[\"aws-waf-token\"]]\n           | map(select(. != null)) | join(\"; \")')\n```\n\n`JSESSIONID` (Wicket session) or `RememberMeCookie` (remembered login) must\nbe present — if both are empty you're not signed in on that tab. `COOKIE` is\nnow a ready-to-send `Cookie:` header value; every `curl` below reuses it. A\nbrowser-like `User-Agent` is required too — the bare curl default trips the\nsite's bot heuristics:\n\n```sh\nUA='Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36'\n```\n\n### Toggle attendance\n\nFull walkthrough (resolve the setlist URL, fetch the page, parse the toggle\ncontrol, replay it, verify) is in `references/attendance-write.md` — it's\nseveral steps, not a one-liner, because the control is a per-render Wicket\nAJAX anchor you must parse out of the page HTML first.\n\n**Always dry-run first**: read the page, check the control's current state,\nand only send the AJAX toggle if it doesn't already match what you want.\n**Never trust the toggle's response status as proof** — always re-fetch the\npage afterward and re-check the control's state.\n\n## Exit codes / failure modes\n\n- **Read path (curl)**: a non-2xx from `api.setlist.fm` is a normal HTTP\n  error — `403` on a valid-looking key usually means a bad/revoked key, not\n  \"no key\" (that's a `401`).\n- **Write path (fpx)**: `fpx cookies` exit codes — `2` bridge unavailable\n  (extension not connected / not paired → `fpx pair -p setlist`), `3` bot\n  wall, `4` upstream non-2xx. Once you have `COOKIE`, subsequent `curl`\n  failures are yours to diagnose (see \"session expired\" below) — `fpx` is\n  out of the loop.\n- **Session expired mid-session**: a page fetched with your cookie renders\n  **logged out** (a `href=\"/signin\"` or `/login` link instead of the\n  attendance control) — re-run the cookie capture (re-approve in Transporter\n  if needed) and retry.\n\n## Notes\n\n- Reads need no session at all — keep `SETLIST_API_KEY` and the fpx cookie\n  capture on completely separate paths, same as the MCP does.\n- `fpx health -p setlist` shows bridge connection state if the cookie\n  capture fails outright.\n- This project is developed and maintained by AI (Claude).\n\nFile v1.1.2:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"setlist-fpx\",\n  \"version\": \"1.1.2\",\n  \"publishedAt\": 1790199975243\n}\n\nFile v1.1.2:references/attendance-write.md\n\n# Attendance write walkthrough (fpx cookie + curl)\n\nMirrors `setlist-mcp`'s `setlist_mark_attended` / `setlist_unmark_attended`\ntools exactly (`src/tools/attendance.ts`, `src/web-client.ts`). There is no\nJSON API for this — you're driving the same server-rendered Apache Wicket\npage a signed-in browser would.\n\nPrereqs: `SETLIST_API_KEY` set (Part 1) and `COOKIE`/`UA` captured per\n`SKILL.md` Part 2.\n\n## Step 0 — resolve the setlist's canonical URL\n\nYou need the setlist's public page path. Either you already have a\nsetlist.fm URL (skip to step 1), or resolve it via the API using the\n`setlistId`:\n\n```sh\nSETLIST_ID=63de4613\nMETA=$(curl -s \"https://api.setlist.fm/rest/1.0/setlist/$SETLIST_ID\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json')\nURL=$(echo \"$META\" | jq -r '.url')          # e.g. https://www.setlist.fm/setlist/.../...-4ba8a766.html\nPATH_ONLY=$(echo \"$URL\" | sed -E 's#https?://[^/]+##')   # /setlist/.../...-4ba8a766.html\n```\n\n## Step 1 — fetch the page, authenticated\n\n```sh\nHTML=$(curl -s \"https://www.setlist.fm$PATH_ONLY\" \\\n  -H \"Cookie: $COOKIE\" -H \"User-Agent: $UA\")\n```\n\n**Check you're actually signed in** before going further — a logged-out page\nlinks to sign-in instead of showing the attendance control:\n\n```sh\necho \"$HTML\" | grep -qE 'href=\"/(signin|login)\\b' && echo \"SESSION EXPIRED — re-capture the cookie\" >&2\n```\n\n## Step 2 — find the attendance control\n\nThe control is an `<a>` tag with a `wicketAjaxGet('...')` onclick and a\n`title` of either:\n\n- `\"Add this setlist to your attended shows.\"` → **not** currently attended\n- `\"Remove this setlist from your attended shows.\"` → **currently** attended\n\nExtract both the per-render AJAX URL and the current state with `perl`\n(installed on macOS by default). For each `<a>` tag, check for\n`wicketAjaxGet(...)` and `title=\"...\"` independently within the same\n800-char window — same as `src/tools/attendance.ts`'s `parseAttendance()` —\nrather than requiring `title=` to appear before `onclick=`, since real\nmarkup doesn't guarantee that ordering. Entity-decode the URL the same way\n`decodeEntities()` does: `&amp;` plus numeric character references\n(`&#x..;` hex and `&#..;` decimal), which a plain grep would mangle:\n\n```sh\nread -r AJAX_URL CURRENTLY_ATTENDED < <(\n  echo \"$HTML\" | perl -0777 -ne '\n    while (/<a\\b(.{0,800})/gs) {\n      my $seg = $1;\n      next unless $seg =~ /wicketAjaxGet\\(\\s*[\\x27\"]([^\\x27\"]+)[\\x27\"]/;\n      my $url = $1;\n      next unless $seg =~ /title=\"([^\"]*attended shows[^\"]*)\"/;\n      my $attended = $1 =~ /remove/i ? 1 : 0;\n      $url =~ s/&amp;/&/g;\n      $url =~ s/&#x([0-9a-fA-F]+);/chr(hex($1))/ge;\n      $url =~ s/&#(\\d+);/chr($1)/ge;\n      print \"$url $attended\\n\";\n      last;\n    }\n  '\n)\n```\n\nIf `$AJAX_URL` is empty: the control genuinely isn't on the page (rare — a\nlayout change, or you hit a bot-rate-limit page) — re-check step 1's\nlogged-out test first, since that's the far more common cause.\n\n## Step 3 — decide: no-op, dry-run, or toggle\n\n```sh\nDESIRED=1   # 1 = mark attended, 0 = unmark\n\nif [ \"$CURRENTLY_ATTENDED\" = \"$DESIRED\" ]; then\n  echo \"already in the desired state — nothing to do\"\n  exit 0\nfi\n\necho \"would toggle attended: $CURRENTLY_ATTENDED -> $DESIRED (dry run — confirm before sending)\"\n```\n\nTreat this like the MCP's `confirm` gate: don't send the toggle in step 4\nuntil you've deliberately decided to (a script arg, an explicit prompt —\nwhatever fits your use). Never toggle blind.\n\n## Step 4 — replay the Wicket AJAX toggle\n\n`$AJAX_URL` is relative to the setlist page. Resolve it, then GET it with\nthe Wicket AJAX headers (these are required — a plain GET without them gets\nignored or errors):\n\n```sh\nFULL_AJAX_URL=$(python3 -c \"import urllib.parse,sys; print(urllib.parse.urljoin(sys.argv[1], sys.argv[2]))\" \"$URL\" \"$AJAX_URL\")\nAJAX_PATH=$(echo \"$FULL_AJAX_URL\" | sed -E 's#https?://[^/]+##')\nBASE_URL_HEADER=$(echo \"$PATH_ONLY\" | sed -E 's#^/##')   # Wicket-Ajax-BaseURL wants no leading slash\n\ncurl -s \"https://www.setlist.fm$AJAX_PATH\" \\\n  -H \"Cookie: $COOKIE\" -H \"User-Agent: $UA\" \\\n  -H 'Wicket-Ajax: true' \\\n  -H \"Wicket-Ajax-BaseURL: $BASE_URL_HEADER\" \\\n  -H 'X-Requested-With: XMLHttpRequest' \\\n  -H 'Accept: text/xml, text/javascript, application/xml, text/html, */*' \\\n  > /dev/null\n```\n\n## Step 5 — verify by re-reading (never trust the response status)\n\nA `200` from step 4 is not proof — re-fetch the page and re-run step 2's\nparse:\n\n```sh\nHTML2=$(curl -s \"https://www.setlist.fm$PATH_ONLY\" -H \"Cookie: $COOKIE\" -H \"User-Agent: $UA\")\nread -r _ NOW_ATTENDED < <(echo \"$HTML2\" | perl -0777 -ne '\n  while (/<a\\b(.{0,800})/gs) {\n    my $seg = $1;\n    next unless $seg =~ /wicketAjaxGet/;\n    next unless $seg =~ /title=\"([^\"]*attended shows[^\"]*)\"/;\n    print \"x \", ($1 =~ /remove/i ? 1 : 0), \"\\n\";\n    last;\n  }\n')\n\nif [ \"$NOW_ATTENDED\" = \"$DESIRED\" ]; then\n  echo \"verified: attended=$NOW_ATTENDED\"\nelse\n  echo \"WARNING: toggle sent but re-read did not confirm the new state — check on setlist.fm\" >&2\nfi\n```\n\n## Notes\n\n- **Pace writes.** Rapid-fire authenticated requests appear to get the\n  session throttled/invalidated mid-batch — space multiple toggles at least\n  ~600ms apart, same as the MCP's write pacer.\n- **Transient 5xx**: `www.setlist.fm`'s gateway occasionally 500/502/503s —\n  retry a couple times with a ~1.2s gap before concluding something's wrong.\n- **This is the ONLY write surface setlist.fm has.** There's no way to edit\n  setlist song content, create setlists, etc. via this path or the API —\n  those are wiki edits done through the full website UI, out of scope here.\n\nFile v1.1.2:references/rest-api.md\n\n# setlist.fm REST API — read endpoints for curl\n\nBase URL: `https://api.setlist.fm/rest` · every call: `-H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json'`.\nOptional: `-H \"Accept-Language: en\"` (one of `en es fr de pt tr it pl`) to localize city/country names.\n\nAll 15 read endpoints below are exactly what `setlist-mcp`'s tools call — same paths, same params.\n\n---\n\n## Artists\n\n### 1. Search artists\n\n```sh\ncurl -s 'https://api.setlist.fm/rest/1.0/search/artists' \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' \\\n  --get --data-urlencode 'artistName=Radiohead' --data-urlencode 'sort=relevance' \\\n  | jq '.artist[] | {name, mbid, url}'\n```\nParams: `artistName`, `artistMbid`, `sort` (`sortName` default | `relevance`), `p` (page, default 1).\n\n### 2. Get artist by mbid\n\n```sh\ncurl -s \"https://api.setlist.fm/rest/1.0/artist/$MBID\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' | jq\n```\n\n### 3. Artist's setlists (most recent first)\n\n```sh\ncurl -s \"https://api.setlist.fm/rest/1.0/artist/$MBID/setlists?p=1\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' \\\n  | jq '.setlist[] | {id, eventDate, venue: .venue.name, city: .venue.city.name, songCount, hasSongs}'\n```\n`songCount`/`setCount`/`hasSongs` are annotations the MCP adds by walking the\nresponse locally (not native API fields) — derive them yourself if you need\nthem: `songCount = ([.sets.set[].song[]?] | length)`.\n\n---\n\n## Setlists\n\n### 4. Search setlists\n\nProvide at least one filter. `date` is `dd-MM-yyyy` (NOT ISO); `lastUpdated`\nis `yyyyMMddHHmmss` UTC.\n\n```sh\ncurl -s 'https://api.setlist.fm/rest/1.0/search/setlists' \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' \\\n  --get \\\n  --data-urlencode 'artistName=Radiohead' \\\n  --data-urlencode 'date=28-08-2025' \\\n  --data-urlencode 'cityName=Charlotte' \\\n  | jq '.setlist[] | {id, url, venue: .venue.name, eventDate}'\n```\nOther params: `artistMbid`, `venueName`, `venueId`, `cityId`, `state`,\n`stateCode`, `countryCode`, `tourName`, `year`, `p`. Omit the artist and pass\n`venueName`/`venueId` + `date` to list every performer at a venue that day.\n\n### 5. Get setlist by id\n\n```sh\ncurl -s \"https://api.setlist.fm/rest/1.0/setlist/$SETLIST_ID\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' | jq\n```\nSongs live in `.sets.set[].song[]`; a `set` may have `encore` (1 = first\nencore) and `name` (e.g. an acoustic set). A `song` may carry `tape: true`\n(pre-recorded, not performed), `cover` (original artist), `with` (guest), and\n`info` (a note like \"acoustic\").\n\n### 6. Get a specific historical version\n\n```sh\ncurl -s \"https://api.setlist.fm/rest/1.0/setlist/version/$VERSION_ID\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' | jq\n```\n\n---\n\n## Venues\n\n### 7. Search venues\n\n```sh\ncurl -s 'https://api.setlist.fm/rest/1.0/search/venues' \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' \\\n  --get --data-urlencode 'name=The Fillmore' --data-urlencode 'cityName=Charlotte' \\\n  | jq '.venue[] | {id, name, city: .city.name, url}'\n```\nParams: `name`, `cityName`, `cityId`, `state`, `stateCode`, `country`, `p`.\n\n### 8. Get venue by id\n\n```sh\ncurl -s \"https://api.setlist.fm/rest/1.0/venue/$VENUE_ID\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' | jq\n```\n\n### 9. Venue's setlists\n\n```sh\ncurl -s \"https://api.setlist.fm/rest/1.0/venue/$VENUE_ID/setlists?p=1\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' \\\n  | jq '.setlist[] | {id, artist: .artist.name, eventDate}'\n```\n\n---\n\n## Geo\n\n### 10. Search cities\n\n```sh\ncurl -s 'https://api.setlist.fm/rest/1.0/search/cities' \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' \\\n  --get --data-urlencode 'name=Charlotte' \\\n  | jq '.cities[] | {geoId: .id, name, state, country: .country.name}'\n```\nParams: `name`, `country`, `state`, `stateCode`, `p`. Feed `geoId` into\n`cityId` on search/setlists and search/venues.\n\n### 11. Get city by geoId\n\n```sh\ncurl -s \"https://api.setlist.fm/rest/1.0/city/$GEO_ID\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' | jq\n```\n\n### 12. List countries\n\n```sh\ncurl -s 'https://api.setlist.fm/rest/1.0/search/countries' \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' \\\n  | jq '.country[] | {code, name}'\n```\nUse `code` as `countryCode` in search/setlists.\n\n---\n\n## Users\n\n### 13. Get user profile\n\n```sh\ncurl -s \"https://api.setlist.fm/rest/1.0/user/$USER_ID\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' | jq\n```\n\n### 14. User's attended shows\n\n```sh\ncurl -s \"https://api.setlist.fm/rest/1.0/user/$USER_ID/attended?p=1\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' \\\n  | jq '.setlist[] | {id, artist: .artist.name, eventDate}'\n```\n\n### 15. User's created/edited setlists\n\n```sh\ncurl -s \"https://api.setlist.fm/rest/1.0/user/$USER_ID/edited?p=1\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' | jq\n```\n\n---\n\n## Errors\n\n- `400` — missing/invalid params (e.g. no filter on search/setlists).\n- `401` — no `x-api-key` header sent at all.\n- `403` — a key WAS sent but is invalid/revoked (not the same as 401 — don't\n  conflate \"no key\" with \"bad key\").\n- `404` — id not found.\n- `429` — over the standard-tier ~2 req/sec / 1440/day limit; back off ~2s\n  and retry once.\n\nFile v1.1.2:skill-card.md\n\n## Description:\n\nQuery and update setlist.fm from a shell without running the setlist-mcp server: search and read concert setlists, artists, venues, cities, and users through the public REST API, and toggle \"I was there\" attendance on a setlist through the authenticated website using an fpx-captured session cookie.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[chrischall](https://clawhub.ai/user/chrischall)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nDevelopers and technical music-data users use this skill to generate shell-based workflows for reading setlist.fm data and, when explicitly intended, marking or unmarking attendance on their own setlist.fm account.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: The skill can guide an agent to capture and reuse live setlist.fm browser session cookies for authenticated attendance changes.\n\nMitigation: Treat the captured cookie as a password: avoid logging or storing it, keep it out of shared terminals, and refresh or revoke the session if exposure is possible.\n\nRisk: Attendance writes mutate the user's setlist.fm account state and the toggle response alone may not prove the final state.\n\nMitigation: Dry-run first, send the toggle only after explicit intent is established, and re-fetch the page afterward to verify the attendance state.\n\nRisk: Rapid authenticated write attempts or expired sessions can produce unreliable results.\n\nMitigation: Pace write requests, detect logged-out pages, and re-capture the session cookie before retrying when the session has expired.\n\n## Reference(s):\n\n- [ClawHub skill page](https://clawhub.ai/chrischall/skills/setlist-fpx)\n- [setlist.fm REST API read endpoints](references/rest-api.md)\n- [Attendance write walkthrough](references/attendance-write.md)\n- [setlist.fm API key settings](https://www.setlist.fm/settings/api)\n\n## Skill Output:\n\n**Output Type(s):** [Shell commands, Configuration, Guidance, Markdown]\n\n**Output Format:** [Markdown with inline shell commands and configuration snippets]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [May include curl, jq, f\n\nArchive v1.1.1: 5 files, 9696 bytes\n\nFiles: references/attendance-write.md (5617b), references/rest-api.md (5375b), skill-card.md (2655b), SKILL.md (6492b), _meta.json (130b)\n\nArchive v1.1.0: 5 files, 9655 bytes\n\nFiles: references/attendance-write.md (5617b), references/rest-api.md (5375b), skill-card.md (2591b), SKILL.md (6492b), _meta.json (130b)\n\nArchive v1.0.0: 5 files, 9541 bytes\n\nFiles: references/attendance-write.md (5617b), references/rest-api.md (5375b), skill-card.md (2283b), SKILL.md (6492b), _meta.json (130b)","readmeExcerpt":"Skill: setlist-fpx Owner: chrischall Summary: Query and update setlist.fm from a shell without running the setlist-mcp server — search/read concert setlists, artists, venues, cities, and users via plain curl against the public REST API (an x-api-key header), and toggle \"I was there\" attendance on a setlist via the authenticated website, using an fpx-captured session cookie. Use when you want setlist.fm data or want t","codeSnippets":[],"executableExamples":[{"language":"sh","snippet":"export SETLIST_API_KEY=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"},{"language":"sh","snippet":"curl -s 'https://api.setlist.fm/rest/1.0/search/artists?artistName=Radiohead' \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' \\"},{"language":"sh","snippet":"curl -s 'https://api.setlist.fm/rest/1.0/search/artists?artistName=Radiohead' \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' \\\n  | jq '.artist[] | {name, mbid, url}'"},{"language":"sh","snippet":"npm install -g @fetchproxy/cli                      # provides `fpx`\nfpx profile add setlist --domain setlist.fm          # apex scope\nfpx pair -p setlist                                  # prints a pair code → approve in ContextMint Bridge"},{"language":"sh","snippet":"COOKIE=$(fpx cookies -p setlist --domain www.setlist.fm \\\n  | jq -r '[.JSESSIONID, .RememberMeCookie, .[\"aws-waf-token\"]]\n           | map(select(. != null)) | join(\"; \")')"},{"language":"sh","snippet":"UA='Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36'"}],"parameters":null,"dependencies":[],"permissions":[],"extractedFiles":[{"path":"SKILL.md","content":"---\nname: setlist-fpx\ndescription: >-\n  Query and update setlist.fm from a shell without running the setlist-mcp\n  server — search/read concert setlists, artists, venues, cities, and users\n  via plain curl against the public REST API (an x-api-key header), and\n  toggle \"I was there\" attendance on a setlist via the authenticated website,\n  using an fpx-captured session cookie. Use when you want setlist.fm data or\n  want to mark/unmark attendance without the MCP, in a script, or on a\n  machine where the MCP isn't installed.\n---\n\n# setlist.fm via curl + fpx (no MCP)\n\nsetlist.fm is **hybrid**: reads and writes use two different surfaces.\n\n- **Reads** — a documented public REST API (`api.setlist.fm/rest`) keyed by\n  an **`x-api-key` header**. Plain `curl`, no browser, no fpx.\n- **Writes** — the only mutation setlist.fm offers is the site's \"I was\n  there\" attendance toggle, and it exists **only on the logged-in website**\n  (`www.setlist.fm`, server-rendered Apache Wicket), not the REST API. There's\n  no login form to script — the credential is your browser's session cookie.\n  `fpx` lifts that cookie out of your signed-in tab **once**; every actual\n  request afterward is plain `curl` carrying it. No bridge round-trip per call.\n\nThis is the same data/actions the `setlist_*` MCP tools expose, reached with\n`curl` instead of a running server.\n\n## Part 1 — reads (curl + API key)\n\n### One-time setup\n\nApply for a free key at <https://www.setlist.fm/settings/api>, non-commercial\nuse only (see API terms below), then:\n\n```sh\nexport SETLIST_API_KEY=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\n```\n\n### Core call\n\n```sh\ncurl -s 'https://api.setlist.fm/rest/1.0/search/artists?artistName=Radiohead' \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' \\\n  | jq '.artist[] | {name, mbid, url}'\n```\n\n`Accept: application/json` is required — the API serves XML otherwise.\nOptional `Accept-Language: en|es|fr|de|pt|tr|it|pl` localizes city/country\nnames. Standard tier is **~2 req/sec, 1440/day** — a 429 means slow down.\n\n**Dates are `dd-MM-yyyy` on the wire** (NOT ISO) for `date` search params and\n`lastUpdated` (`yyyyMMddHHmmss`); `eventDate` comes back the same way in\nresponses — convert both directions yourself (the MCP does this for you via\n`dmyToIso`/`isoToDmy`; a shell one-liner: `date -j -f '%Y-%m-%d' '+%d-%m-%Y'`\non macOS).\n\nThe full request catalog (all 15 read endpoints, params, and `jq` projections)\nis in `references/rest-api.md`.\n\n### The one rule: chain ids, don't guess them\n\nEvery entity is keyed by an id you get from a search: artists by **`mbid`**\n(MusicBrainz id), setlists by **`setlistId`**, venues by **`venueId`**,\ncities by **`geoId`**. Search first, then feed the id into the matching\n`get`/`setlists` endpoint. Got a setlist.fm URL instead of an id? The id is\nthe trailing hex token before `.html`, e.g.\n`.../setlist/.../...-4ba8a766.html` → `4ba8a766` — no fetch needed.\n\n### Attribution (compliance, not optional)\n\nThe API terms require a **followable l"},{"path":"_meta.json","content":"{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"setlist-fpx\",\n  \"version\": \"1.1.8\",\n  \"publishedAt\": 1791588521698\n}"},{"path":"references/attendance-write.md","content":"# Attendance write walkthrough (fpx cookie + curl)\n\nMirrors `setlist-mcp`'s `setlist_mark_attended` / `setlist_unmark_attended`\ntools exactly (`src/tools/attendance.ts`, `src/web-client.ts`). There is no\nJSON API for this — you're driving the same server-rendered Apache Wicket\npage a signed-in browser would.\n\nPrereqs: `SETLIST_API_KEY` set (Part 1) and `COOKIE`/`UA` captured per\n`SKILL.md` Part 2.\n\n## Step 0 — resolve the setlist's canonical URL\n\nYou need the setlist's public page path. Either you already have a\nsetlist.fm URL (skip to step 1), or resolve it via the API using the\n`setlistId`:\n\n```sh\nSETLIST_ID=63de4613\nMETA=$(curl -s \"https://api.setlist.fm/rest/1.0/setlist/$SETLIST_ID\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json')\nURL=$(echo \"$META\" | jq -r '.url')          # e.g. https://www.setlist.fm/setlist/.../...-4ba8a766.html\nPATH_ONLY=$(echo \"$URL\" | sed -E 's#https?://[^/]+##')   # /setlist/.../...-4ba8a766.html\n```\n\n## Step 1 — fetch the page, authenticated\n\n```sh\nHTML=$(curl -s \"https://www.setlist.fm$PATH_ONLY\" \\\n  -H \"Cookie: $COOKIE\" -H \"User-Agent: $UA\")\n```\n\n**Check you're actually signed in** before going further — a logged-out page\nlinks to sign-in instead of showing the attendance control:\n\n```sh\necho \"$HTML\" | grep -qE 'href=\"/(signin|login)\\b' && echo \"SESSION EXPIRED — re-capture the cookie\" >&2\n```\n\n## Step 2 — find the attendance control\n\nThe control is an `<a>` tag with a `wicketAjaxGet('...')` onclick and a\n`title` of either:\n\n- `\"Add this setlist to your attended shows.\"` → **not** currently attended\n- `\"Remove this setlist from your attended shows.\"` → **currently** attended\n\nExtract both the per-render AJAX URL and the current state with `perl`\n(installed on macOS by default). For each `<a>` tag, check for\n`wicketAjaxGet(...)` and `title=\"...\"` independently within the same\n800-char window — same as `src/tools/attendance.ts`'s `parseAttendance()` —\nrather than requiring `title=` to appear before `onclick=`, since real\nmarkup doesn't guarantee that ordering. Entity-decode the URL the same way\n`decodeEntities()` does: `&amp;` plus numeric character references\n(`&#x..;` hex and `&#..;` decimal), which a plain grep would mangle:\n\n```sh\nread -r AJAX_URL CURRENTLY_ATTENDED < <(\n  echo \"$HTML\" | perl -0777 -ne '\n    while (/<a\\b(.{0,800})/gs) {\n      my $seg = $1;\n      next unless $seg =~ /wicketAjaxGet\\(\\s*[\\x27\"]([^\\x27\"]+)[\\x27\"]/;\n      my $url = $1;\n      next unless $seg =~ /title=\"([^\"]*attended shows[^\"]*)\"/;\n      my $attended = $1 =~ /remove/i ? 1 : 0;\n      $url =~ s/&amp;/&/g;\n      $url =~ s/&#x([0-9a-fA-F]+);/chr(hex($1))/ge;\n      $url =~ s/&#(\\d+);/chr($1)/ge;\n      print \"$url $attended\\n\";\n      last;\n    }\n  '\n)\n```\n\nIf `$AJAX_URL` is empty: the control genuinely isn't on the page (rare — a\nlayout change, or you hit a bot-rate-limit page) — re-check step 1's\nlogged-out test first, since that's the far more common cause.\n\n## Step 3 — decide: no-op, dry-run, or toggle\n\n```sh\nDESIRED=1"},{"path":"references/rest-api.md","content":"# setlist.fm REST API — read endpoints for curl\n\nBase URL: `https://api.setlist.fm/rest` · every call: `-H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json'`.\nOptional: `-H \"Accept-Language: en\"` (one of `en es fr de pt tr it pl`) to localize city/country names.\n\nAll 15 read endpoints below are exactly what `setlist-mcp`'s tools call — same paths, same params.\n\n---\n\n## Artists\n\n### 1. Search artists\n\n```sh\ncurl -s 'https://api.setlist.fm/rest/1.0/search/artists' \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' \\\n  --get --data-urlencode 'artistName=Radiohead' --data-urlencode 'sort=relevance' \\\n  | jq '.artist[] | {name, mbid, url}'\n```\nParams: `artistName`, `artistMbid`, `sort` (`sortName` default | `relevance`), `p` (page, default 1).\n\n### 2. Get artist by mbid\n\n```sh\ncurl -s \"https://api.setlist.fm/rest/1.0/artist/$MBID\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' | jq\n```\n\n### 3. Artist's setlists (most recent first)\n\n```sh\ncurl -s \"https://api.setlist.fm/rest/1.0/artist/$MBID/setlists?p=1\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' \\\n  | jq '.setlist[] | {id, eventDate, venue: .venue.name, city: .venue.city.name, songCount, hasSongs}'\n```\n`songCount`/`setCount`/`hasSongs` are annotations the MCP adds by walking the\nresponse locally (not native API fields) — derive them yourself if you need\nthem: `songCount = ([.sets.set[].song[]?] | length)`.\n\n---\n\n## Setlists\n\n### 4. Search setlists\n\nProvide at least one filter. `date` is `dd-MM-yyyy` (NOT ISO); `lastUpdated`\nis `yyyyMMddHHmmss` UTC.\n\n```sh\ncurl -s 'https://api.setlist.fm/rest/1.0/search/setlists' \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' \\\n  --get \\\n  --data-urlencode 'artistName=Radiohead' \\\n  --data-urlencode 'date=28-08-2025' \\\n  --data-urlencode 'cityName=Charlotte' \\\n  | jq '.setlist[] | {id, url, venue: .venue.name, eventDate}'\n```\nOther params: `artistMbid`, `venueName`, `venueId`, `cityId`, `state`,\n`stateCode`, `countryCode`, `tourName`, `year`, `p`. Omit the artist and pass\n`venueName`/`venueId` + `date` to list every performer at a venue that day.\n\n### 5. Get setlist by id\n\n```sh\ncurl -s \"https://api.setlist.fm/rest/1.0/setlist/$SETLIST_ID\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' | jq\n```\nSongs live in `.sets.set[].song[]`; a `set` may have `encore` (1 = first\nencore) and `name` (e.g. an acoustic set). A `song` may carry `tape: true`\n(pre-recorded, not performed), `cover` (original artist), `with` (guest), and\n`info` (a note like \"acoustic\").\n\n### 6. Get a specific historical version\n\n```sh\ncurl -s \"https://api.setlist.fm/rest/1.0/setlist/version/$VERSION_ID\" \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' | jq\n```\n\n---\n\n## Venues\n\n### 7. Search venues\n\n```sh\ncurl -s 'https://api.setlist.fm/rest/1.0/search/venues' \\\n  -H \"x-api-key: $SETLIST_API_KEY\" -H 'Accept: application/json' \\\n  --get --data-urlencode 'name=The Fillmore' --data-ur"},{"path":"skill-card.md","content":"## Description:\n\nGuides agents in searching and reading setlist.fm concert data with its REST API and updating a user's attendance through an authenticated browser session, without running an MCP server.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[chrischall](https://clawhub.ai/user/chrischall)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nDevelopers and other setlist.fm users can use this skill to look up artists, setlists, venues, cities, and users from a shell, or mark and unmark their attendance at a show. The free setlist.fm API key is restricted to non-commercial use under the site's API terms.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: A captured setlist.fm session cookie can enable account actions if disclosed.\n\nMitigation: Treat the cookie like a password; keep it out of logs, shell history, commits, and shared transcripts, and use the bridge only if you trust it with your session.\n\nRisk: Blindly toggling attendance can undo the intended state.\n\nMitigation: Check the current state, dry-run before sending the toggle, and re-fetch the page to verify the result.\n\n## Reference(s):\n\n- [setlist-fpx release on ClawHub](https://clawhub.ai/chrischall/skills/setlist-fpx)\n- [setlist.fm API key and terms](https://www.setlist.fm/settings/api)\n- [REST API read endpoints](references/rest-api.md)\n- [Attendance update walkthrough](references/attendance-write.md)\n- [ContextMint Bridge releases](https://github.com/nullnet-app/contextmint-bridge/releases)\n\n## Skill Output:\n\n**Output Type(s):** [Guidance, Shell commands, Configuration instructions]\n\n**Output Format:** [Markdown with shell command examples]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [REST reads require an API key; attendance changes require an authenticated setlist.fm session and confirmation of the current state.]\n\n## Skill Version(s):\n\n1.1.8 (source: ClawHub release evidence)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment."}],"languages":[],"docsSourceLabel":"CLAWHUB","editorialOverview":null,"editorialQuality":{"score":100,"threshold":65,"status":"thin","wordCount":1797,"uniquenessScore":40,"reasons":["uniqueness-below-45"]}},"media":{"evidence":{"source":"no-media","verified":false,"confidence":"low","updatedAt":"2026-10-10T04:36:06.054Z","emptyReason":"No screenshots, media assets, or demo links are available."},"primaryImageUrl":null,"mediaAssetCount":0,"assets":[],"demoUrl":null},"ownerResources":{"evidence":{"source":"unclaimed","verified":false,"confidence":"low","updatedAt":"2026-10-10T04:36:06.054Z","emptyReason":"This page has not been claimed by the agent owner."},"hasCustomPage":false,"customPageUpdatedAt":null,"customLinks":[],"structuredLinks":{"docsUrl":null,"demoUrl":null,"supportUrl":null,"pricingUrl":null,"statusUrl":null},"customPage":null},"relatedAgents":{"evidence":{"source":"protocol-neighbors","verified":false,"confidence":"medium","updatedAt":"2026-10-10T08:45:35.217Z","emptyReason":null},"items":[{"id":"8ebccd8e-3863-4187-8355-c3f14e1f9edf","entityType":"agent","canonicalPath":"/agent/iofficeai-aionui","slug":"iofficeai-aionui","name":"AionUi","description":"Free, local, open-source 24/7 Cowork app and OpenClaw for Gemini CLI, Claude Code, Codex, OpenCode, Qwen Code, Goose CLI, Auggie, and more | 🌟 Star if you like it!","url":"https://github.com/iOfficeAI/AionUi","homepage":"https://www.aionui.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-10-09T19:11:12.944Z","createdAt":"2026-02-25T03:38:16.584Z","downloads":null},{"id":"b917f68a-ebff-438e-84f8-3f4b2494c0bc","entityType":"agent","canonicalPath":"/agent/activepieces-activepieces","slug":"activepieces-activepieces","name":"activepieces","description":"AI Agents & MCPs & AI Workflow Automation • (~400 MCP servers for AI agents) • AI Automation / AI Agent with MCPs • AI Workflows & AI Agents • MCPs for AI Agents","url":"https://github.com/activepieces/activepieces","homepage":"https://www.activepieces.com","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-15T02:22:12.426Z","createdAt":"2026-02-25T03:38:12.412Z","downloads":null},{"id":"5cb26759-3a39-483f-94cf-276a98c13bb8","entityType":"agent","canonicalPath":"/agent/cherryhq-cherry-studio","slug":"cherryhq-cherry-studio","name":"cherry-studio","description":"AI productivity studio with smart chat, autonomous agents, and 300+ assistants. Unified access to frontier LLMs","url":"https://github.com/CherryHQ/cherry-studio","homepage":"https://cherry-ai.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-11T14:38:40.986Z","createdAt":"2026-02-25T03:38:19.379Z","downloads":null},{"id":"6f6582d0-5d76-4f0f-b81d-86520247950b","entityType":"agent","canonicalPath":"/agent/copilotkit-copilotkit","slug":"copilotkit-copilotkit","name":"CopilotKit","description":"The Frontend for Agents & Generative UI. React + Angular","url":"https://github.com/CopilotKit/CopilotKit","homepage":"https://docs.copilotkit.ai","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-03-25T09:50:57.846Z","createdAt":"2026-02-25T03:39:14.617Z","downloads":null}],"links":{"hub":"/agent","source":"/agent/source/clawhub","protocols":[{"label":"OpenClaw","href":"/agent/protocol/openclew"}]}}}