diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 05416fc..40118e0 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -6,20 +6,20 @@ }, "metadata": { "description": "Public JoomPulse skills marketplace.", - "version": "0.1.0" + "version": "0.2.0" }, "plugins": [ { - "name": "pulse-skills", + "name": "joompulse-skills", "source": "./", "description": "Public JoomPulse agent skills for product research and marketplace workflows.", - "version": "0.1.0", + "version": "0.2.0", "author": { "name": "Joom", "url": "https://joompulse.com" }, - "homepage": "https://github.com/joomcode/pulse-skills", - "repository": "https://github.com/joomcode/pulse-skills", + "homepage": "https://github.com/joomcode/joompulse-skills", + "repository": "https://github.com/joomcode/joompulse-skills", "license": "MIT", "category": "productivity", "tags": [ diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index fa0a71f..953343f 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,13 +1,13 @@ { - "name": "pulse-skills", + "name": "joompulse-skills", "description": "Public JoomPulse agent skills for product research and marketplace workflows.", - "version": "0.1.0", + "version": "0.2.0", "author": { "name": "Joom", "url": "https://joompulse.com" }, - "homepage": "https://github.com/joomcode/pulse-skills", - "repository": "https://github.com/joomcode/pulse-skills", + "homepage": "https://github.com/joomcode/joompulse-skills", + "repository": "https://github.com/joomcode/joompulse-skills", "license": "MIT", "keywords": [ "joompulse", diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml new file mode 100644 index 0000000..6c26f4e --- /dev/null +++ b/.github/workflows/checks.yml @@ -0,0 +1,23 @@ +name: checks + +on: + push: + branches: [main] + pull_request: + +jobs: + checks: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Public-safety grep + run: bash scripts/check-public-safety.sh + + - name: Manifest and skill consistency + run: bash scripts/check-manifests.sh + + - name: Claude plugin validate + run: | + npm install -g @anthropic-ai/claude-code + claude plugin validate . --strict diff --git a/.gitignore b/.gitignore index d410d06..32443f5 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ node_modules/ .env .env.* *.log +skills.zip diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 0000000..7cb9dbf --- /dev/null +++ b/.mcp.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "joompulse": { + "type": "http", + "url": "https://joompulse.com/mcp" + } + } +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 05cbc95..9298860 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,11 +4,27 @@ All notable changes to this project will be documented in this file. This project follows semantic versioning. -## Unreleased +## 0.2.0 - 2026-08-13 -- Added `ml-product-analysis`. -- Added `product-change-monitor`. -- Added `my-product-vs-catalog`. +- Renamed the plugin from `pulse-skills` to `joompulse-skills` to match the + repository and the JoomPulse brand. Skills are now invoked as + `/joompulse-skills:`. +- Fixed `homepage` and `repository` URLs, which still pointed at the old + `joomcode/pulse-skills` path. +- Bundled `.mcp.json` so installing the plugin configures the JoomPulse MCP + connector; users only need to sign in. +- Added product skills: `ml-product-analysis`, `product-change-monitor`, + `my-product-vs-catalog`. +- Added category skills: `category-monitor`, `category-opportunity-index`, + `growing-leaf-category-tracker`, `uncontested-niche-finder`, + `high-demand-low-quality-finder`, `unbranded-products-in-category`, + `new-growing-products-in-category`, `top-keywords-in-my-category`. +- Added seller and brand skills: `seller-overview-tracker`, + `top-sellers-in-category`, `top-brand-position-tracker`. +- Added international skills: `fast-growing-international-products`, + `popular-international-products`. +- Removed `international-product-matcher`. +- Shortened all `SKILL.md` descriptions to the 1024-character limit. ## 0.1.0 - 2026-06-16 diff --git a/README.md b/README.md index 432c6cd..0970762 100644 --- a/README.md +++ b/README.md @@ -24,16 +24,21 @@ sellers and teams. If you have the Claude terminal installed, run these commands in it one at a time: ```bash -/plugin marketplace add joomcode/pulse-skills -/plugin install pulse-skills@joomcode +/plugin marketplace add joomcode/joompulse-skills +/plugin install joompulse-skills@joomcode ``` After installation, start a new session so the skill metadata is loaded. +Installing the plugin also configures the JoomPulse MCP connector for you — run +`/mcp`, pick **joompulse**, and sign in. You can skip the manual setup below. + ## Connect the JoomPulse MCP (required) To use these skills you first need to connect the **JoomPulse MCP** — it provides the live market data they run on, and without it the skills won't work. +If you installed via the Claude terminal, the connector is already configured; just sign in with `/mcp`. The steps below are for Claude chat and Claude Cowork, where you add it by hand. + > **Get access and details:** https://joompulse.com/mcp-connector **1. Open Claude settings** diff --git a/scripts/check-manifests.sh b/scripts/check-manifests.sh new file mode 100755 index 0000000..8623f73 --- /dev/null +++ b/scripts/check-manifests.sh @@ -0,0 +1,96 @@ +#!/usr/bin/env bash +# Checks the plugin manifests stay consistent with each other, the repository, +# and the changelog. Version drift and stale repository URLs are the two things +# that most often break a plugin submission. +set -euo pipefail + +repo="joomcode/joompulse-skills" +repo_url="https://github.com/${repo}" +fail=0 + +note() { + echo "FAIL: $*" >&2 + fail=1 +} + +for f in .claude-plugin/plugin.json .claude-plugin/marketplace.json; do + python3 -c "import json,sys; json.load(open('$f'))" \ + || note "$f is not valid JSON" +done + +read -r plugin_name plugin_version plugin_home plugin_repo <<<"$(python3 -c " +import json +d = json.load(open('.claude-plugin/plugin.json')) +print(d.get('name'), d.get('version'), d.get('homepage'), d.get('repository')) +")" + +read -r entry_name entry_version entry_home entry_repo <<<"$(python3 -c " +import json +d = json.load(open('.claude-plugin/marketplace.json'))['plugins'][0] +print(d.get('name'), d.get('version'), d.get('homepage'), d.get('repository')) +")" + +[ "$plugin_name" = "$entry_name" ] \ + || note "name differs: plugin.json '$plugin_name' vs marketplace entry '$entry_name'" +[ "$plugin_version" = "$entry_version" ] \ + || note "version differs: plugin.json '$plugin_version' vs marketplace entry '$entry_version'" + +for url in "$plugin_home" "$plugin_repo" "$entry_home" "$entry_repo"; do + [ "$url" = "$repo_url" ] || note "URL '$url' does not match $repo_url" +done + +changelog_version=$(grep -m1 -oE '^## [0-9]+\.[0-9]+\.[0-9]+' CHANGELOG.md | awk '{print $2}') +[ "$plugin_version" = "$changelog_version" ] \ + || note "version differs: plugin.json '$plugin_version' vs newest CHANGELOG entry '$changelog_version'" + +# The plugin name is a permanent identifier: renaming it breaks existing installs. +# CHANGELOG.md is exempt because it records the rename itself. +grep -rIn --exclude-dir=.git --exclude="$(basename "$0")" --exclude=CHANGELOG.md \ + 'joomcode/pulse-skills' . \ + && note "found references to the old joomcode/pulse-skills path" + +python3 - "$plugin_name" <<'PY' || fail=1 +import glob, os, re, sys + +plugin_name = sys.argv[1] +ok = True +for path in sorted(glob.glob("skills/*/SKILL.md")): + directory = os.path.basename(os.path.dirname(path)) + text = open(path).read() + match = re.match(r"^---\n(.*?)\n---", text, re.S) + if not match: + print(f"FAIL: {path} has no YAML frontmatter", file=sys.stderr) + ok = False + continue + front = match.group(1) + + name = re.search(r"^name:\s*(.+)$", front, re.M) + if not name: + print(f"FAIL: {path} has no name", file=sys.stderr) + ok = False + elif name.group(1).strip().strip("\"'") != directory: + print(f"FAIL: {path} name does not match its directory", file=sys.stderr) + ok = False + + desc = re.search(r"^description:\s*(.*?)(?=^\w[\w-]*:|\Z)", front, re.S | re.M) + if not desc: + print(f"FAIL: {path} has no description", file=sys.stderr) + ok = False + else: + collapsed = " ".join(desc.group(1).split()).strip("\"'") + if len(collapsed) > 1024: + print( + f"FAIL: {path} description is {len(collapsed)} chars (limit 1024)", + file=sys.stderr, + ) + ok = False + +sys.exit(0 if ok else 1) +PY + +if [ "$fail" -ne 0 ]; then + echo "Manifest check failed." >&2 + exit 1 +fi + +echo "Manifest check passed (plugin ${plugin_name} ${plugin_version})."