diff --git a/.changeset/restructure-plugin-into-subdirectory.md b/.changeset/restructure-plugin-into-subdirectory.md new file mode 100644 index 0000000..fdc03c6 --- /dev/null +++ b/.changeset/restructure-plugin-into-subdirectory.md @@ -0,0 +1,5 @@ +--- +"slack": patch +--- + +Restructure the repository so the plugin lives under `plugins/slack/` and add a top-level `.claude-plugin/marketplace.json`, enabling a Slack-hosted marketplace and future multi-plugin layout. No change to the plugin's skills, commands, or MCP configuration. diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json new file mode 100644 index 0000000..c2f9e64 --- /dev/null +++ b/.claude-plugin/marketplace.json @@ -0,0 +1,18 @@ +{ + "name": "slack-skills", + "owner": { + "name": "Slack" + }, + "metadata": { + "description": "Official Slack MCP Server and developer skills for AI coding agents" + }, + "plugins": [ + { + "name": "slack", + "description": "Slack integration for searching messages, sending communications, managing canvases, and more", + "source": "./plugins/slack", + "category": "development", + "tags": ["slack", "mcp", "bolt"] + } + ] +} diff --git a/.github/maintainers_guide.md b/.github/maintainers_guide.md index c5a8c76..cb61682 100644 --- a/.github/maintainers_guide.md +++ b/.github/maintainers_guide.md @@ -87,10 +87,10 @@ make typecheck # Mypy static type checks ``` Markdown linting is powered by [rumdl](https://github.com/rvben/rumdl), a -markdownlint-compatible Rust linter. It validates `skills/`, `commands/`, -`README.md`, and `AGENTS.md`. Rules and disabled checks are configured under -`[tool.rumdl]` in `pyproject.toml` — tune that section when a new skill trips a -rule that isn't worth enforcing. +markdownlint-compatible Rust linter. It validates `plugins/slack/skills/`, +`plugins/slack/commands/`, `README.md`, and `AGENTS.md`. Rules and disabled +checks are configured under `[tool.rumdl]` in `pyproject.toml` — tune that +section when a new skill trips a rule that isn't worth enforcing. ### Testing in Claude Code @@ -98,20 +98,23 @@ Load your local changes into Claude Code for a single session with the `--plugin-dir` flag: ```sh -claude --plugin-dir ./ +claude --plugin-dir ./plugins/slack ``` This loads the `slack` plugin from your checkout — its skills and commands, and -the HTTP MCP server from `.mcp.json`. If you already have the published +the HTTP MCP server from `plugins/slack/.mcp.json`. If you already have the published `slack` plugin installed, the local copy takes precedence **for that session only**: nothing is written to your settings, and the installed version is untouched when you exit. After editing a skill or command, run `/reload-plugins` inside the session to pick up the change without restarting. -Check the plugin's structure without launching a session: +Check the structure without launching a session. Run from the repo root to +validate the marketplace manifest (`.claude-plugin/marketplace.json`), which +descends into the plugin, or point at the plugin directory to validate it alone: ```sh -claude plugin validate +claude plugin validate . # marketplace + the plugin it lists +claude plugin validate ./plugins/slack # just the slack plugin ``` ### Testing in Cursor diff --git a/AGENTS.md b/AGENTS.md index db981a7..bf273a9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -40,7 +40,7 @@ When one `SKILL.md` references another skill (e.g., to delegate a step instead o - Add a sentence of prose explaining what the referenced section does and why you're delegating to it. - Don't use markdown anchor links (`[text](#step-1)`), `@`-include syntax (`@path/to/SKILL.md`), or bare file paths — none are idiomatic in installed skills, and `@`-includes force-load context. -See `skills/create-slack-app/SKILL.md` Step 1a for an example. +See `plugins/slack/skills/create-slack-app/SKILL.md` Step 1a for an example. ## Testing diff --git a/README.md b/README.md index 4dfba40..ead5395 100644 --- a/README.md +++ b/README.md @@ -40,12 +40,12 @@ The plugin connects your AI tool to Slack's hosted [MCP server][slack-mcp-docs]: Six skills load on demand to handle messaging tasks and developer workflows: -- [`slack:slack-messaging`](skills/slack-messaging/SKILL.md) - composing well-formatted, effective Slack messages -- [`slack:slack-search`](skills/slack-search/SKILL.md) - finding messages, files, channels, and people -- [`slack:slack-api`](skills/slack-api/SKILL.md) - discovering and calling Slack Web API methods -- [`slack:slack-cli`](skills/slack-cli/SKILL.md) - using the [Slack CLI][slack-cli] to create, run, and manage apps -- [`slack:create-slack-app`](skills/create-slack-app/SKILL.md) - building a Slack app or agent with the CLI and [Bolt][bolt] -- [`slack:block-kit`](skills/block-kit/SKILL.md) - building and validating [Block Kit][block-kit] layouts +- [`slack:slack-messaging`](plugins/slack/skills/slack-messaging/SKILL.md) - composing well-formatted, effective Slack messages +- [`slack:slack-search`](plugins/slack/skills/slack-search/SKILL.md) - finding messages, files, channels, and people +- [`slack:slack-api`](plugins/slack/skills/slack-api/SKILL.md) - discovering and calling Slack Web API methods +- [`slack:slack-cli`](plugins/slack/skills/slack-cli/SKILL.md) - using the [Slack CLI][slack-cli] to create, run, and manage apps +- [`slack:create-slack-app`](plugins/slack/skills/create-slack-app/SKILL.md) - building a Slack app or agent with the CLI and [Bolt][bolt] +- [`slack:block-kit`](plugins/slack/skills/block-kit/SKILL.md) - building and validating [Block Kit][block-kit] layouts ### Commands diff --git a/.claude-plugin/plugin.json b/plugins/slack/.claude-plugin/plugin.json similarity index 78% rename from .claude-plugin/plugin.json rename to plugins/slack/.claude-plugin/plugin.json index d235c58..7897e0c 100644 --- a/.claude-plugin/plugin.json +++ b/plugins/slack/.claude-plugin/plugin.json @@ -6,6 +6,6 @@ "name": "Slack", "url": "https://slack.com" }, - "homepage": "https://github.com/slackapi/slack-mcp-cursor-plugin", + "homepage": "https://github.com/slackapi/slack-mcp-plugin", "license": "MIT" } diff --git a/.cursor-mcp.json b/plugins/slack/.cursor-mcp.json similarity index 100% rename from .cursor-mcp.json rename to plugins/slack/.cursor-mcp.json diff --git a/.cursor-plugin/plugin.json b/plugins/slack/.cursor-plugin/plugin.json similarity index 100% rename from .cursor-plugin/plugin.json rename to plugins/slack/.cursor-plugin/plugin.json diff --git a/.mcp.json b/plugins/slack/.mcp.json similarity index 100% rename from .mcp.json rename to plugins/slack/.mcp.json diff --git a/commands/channel-digest.md b/plugins/slack/commands/channel-digest.md similarity index 100% rename from commands/channel-digest.md rename to plugins/slack/commands/channel-digest.md diff --git a/commands/draft-announcement.md b/plugins/slack/commands/draft-announcement.md similarity index 100% rename from commands/draft-announcement.md rename to plugins/slack/commands/draft-announcement.md diff --git a/commands/find-discussions.md b/plugins/slack/commands/find-discussions.md similarity index 100% rename from commands/find-discussions.md rename to plugins/slack/commands/find-discussions.md diff --git a/commands/standup.md b/plugins/slack/commands/standup.md similarity index 100% rename from commands/standup.md rename to plugins/slack/commands/standup.md diff --git a/commands/summarize-channel.md b/plugins/slack/commands/summarize-channel.md similarity index 100% rename from commands/summarize-channel.md rename to plugins/slack/commands/summarize-channel.md diff --git a/skills/block-kit/SKILL.md b/plugins/slack/skills/block-kit/SKILL.md similarity index 100% rename from skills/block-kit/SKILL.md rename to plugins/slack/skills/block-kit/SKILL.md diff --git a/skills/block-kit/references/common-patterns.md b/plugins/slack/skills/block-kit/references/common-patterns.md similarity index 100% rename from skills/block-kit/references/common-patterns.md rename to plugins/slack/skills/block-kit/references/common-patterns.md diff --git a/skills/create-slack-app/SKILL.md b/plugins/slack/skills/create-slack-app/SKILL.md similarity index 100% rename from skills/create-slack-app/SKILL.md rename to plugins/slack/skills/create-slack-app/SKILL.md diff --git a/skills/slack-api/SKILL.md b/plugins/slack/skills/slack-api/SKILL.md similarity index 100% rename from skills/slack-api/SKILL.md rename to plugins/slack/skills/slack-api/SKILL.md diff --git a/skills/slack-cli/SKILL.md b/plugins/slack/skills/slack-cli/SKILL.md similarity index 100% rename from skills/slack-cli/SKILL.md rename to plugins/slack/skills/slack-cli/SKILL.md diff --git a/skills/slack-messaging/SKILL.md b/plugins/slack/skills/slack-messaging/SKILL.md similarity index 100% rename from skills/slack-messaging/SKILL.md rename to plugins/slack/skills/slack-messaging/SKILL.md diff --git a/skills/slack-search/SKILL.md b/plugins/slack/skills/slack-search/SKILL.md similarity index 100% rename from skills/slack-search/SKILL.md rename to plugins/slack/skills/slack-search/SKILL.md diff --git a/pyproject.toml b/pyproject.toml index 822cb60..44861bb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,7 @@ tools = [ # This project is installed only to run the test suite; the importable package # is `tests`. Scope discovery to it so setuptools' flat-layout autodiscovery -# doesn't trip over the sibling `skills/` and `commands/` directories. +# doesn't trip over the sibling `plugins/` and `scripts/` directories. [tool.setuptools.packages.find] include = ["tests*"] @@ -49,7 +49,7 @@ indent-style = "space" # rumdl: markdownlint-compatible linter for the plugin's authored markdown. [tool.rumdl] -include = ["skills/**/*.md", "commands/*.md", "README.md", "AGENTS.md"] +include = ["plugins/slack/skills/**/*.md", "plugins/slack/commands/*.md", "README.md", "AGENTS.md"] exclude = [".venv", "node_modules", ".changeset"] respect-gitignore = true # Disable rules that conflict with the skill-authoring style: diff --git a/scripts/cursor.py b/scripts/cursor.py index f617b38..fa74bdd 100644 --- a/scripts/cursor.py +++ b/scripts/cursor.py @@ -21,6 +21,9 @@ } REPO_ROOT = Path(__file__).resolve().parent.parent +# The plugin lives under plugins/slack/; glob and re-base against it so the installed +# layout is flattened (no plugins/slack/ prefix), matching what Cursor expects at its root. +PLUGIN_ROOT = REPO_ROOT / "plugins" / "slack" CLAUDE_HOME_DIR = Path.home() / ".claude" CLAUDE_INSTALLED_PLUGINS_PATH = CLAUDE_HOME_DIR / "plugins" / "installed_plugins.json" @@ -42,7 +45,7 @@ def get_target_path(plugin_key: str) -> Path: def plugin_name() -> str: """Read the plugin name from the Cursor plugin file so renames are picked up.""" cursor_plugin = json.loads( - (REPO_ROOT / ".cursor-plugin" / "plugin.json").read_text() + (PLUGIN_ROOT / ".cursor-plugin" / "plugin.json").read_text() ) name: str = cursor_plugin["name"] return name @@ -52,10 +55,10 @@ def plugin_files() -> set[Path]: included = { path for pattern in INCLUDE - for path in REPO_ROOT.glob(pattern) + for path in PLUGIN_ROOT.glob(pattern) if path.is_file() } - excluded = {path for pattern in EXCLUDE for path in REPO_ROOT.glob(pattern)} + excluded = {path for pattern in EXCLUDE for path in PLUGIN_ROOT.glob(pattern)} return included - excluded @@ -78,12 +81,12 @@ def install() -> None: files = plugin_files() if not files: - logger.warning(f"No plugin files found under {REPO_ROOT}; nothing to install") + logger.warning(f"No plugin files found under {PLUGIN_ROOT}; nothing to install") return shutil.rmtree(target, ignore_errors=True) for source in files: - dest = target / source.relative_to(REPO_ROOT) + dest = target / source.relative_to(PLUGIN_ROOT) dest.parent.mkdir(parents=True, exist_ok=True) shutil.copy2(source, dest) logger.info(f"Copied {len(files)} plugin files to {target}") diff --git a/scripts/sync_versions.py b/scripts/sync_versions.py index 05494b8..be34774 100644 --- a/scripts/sync_versions.py +++ b/scripts/sync_versions.py @@ -5,12 +5,14 @@ logger = logging.getLogger(Path(__file__).stem) REPO_ROOT = Path(__file__).resolve().parent.parent +PLUGIN_ROOT = REPO_ROOT / "plugins" / "slack" # package.json (committed at the repo root) is the version source of truth; changesets -# bumps it, then this script synchronizes the version out to everywhere else its defined. +# bumps it, then this script synchronizes the version out to the plugin manifests under +# plugins/slack/. PACKAGE_JSON_PATH = REPO_ROOT / "package.json" -CLAUDE_PLUGIN_PATH = REPO_ROOT / ".claude-plugin" / "plugin.json" -CURSOR_PLUGIN_PATH = REPO_ROOT / ".cursor-plugin" / "plugin.json" +CLAUDE_PLUGIN_PATH = PLUGIN_ROOT / ".claude-plugin" / "plugin.json" +CURSOR_PLUGIN_PATH = PLUGIN_ROOT / ".cursor-plugin" / "plugin.json" def read_version(package_path: Path) -> str: diff --git a/tests/config.py b/tests/config.py index f7b5ba2..1aa28a2 100644 --- a/tests/config.py +++ b/tests/config.py @@ -5,8 +5,12 @@ from dotenv import load_dotenv +# Repo root holds the tooling; the plugin itself lives under plugins/slack/. +REPO_ROOT = Path(__file__).parent.parent +PLUGIN_ROOT = REPO_ROOT / "plugins" / "slack" + # Load .env from the repo root (if present) before reading the environment -load_dotenv(Path(__file__).parent.parent / ".env") +load_dotenv(REPO_ROOT / ".env") def get_gemini_api_key_pool(environ: Mapping[str, str]) -> list[str]: @@ -14,10 +18,10 @@ def get_gemini_api_key_pool(environ: Mapping[str, str]) -> list[str]: # Filesystem -SKILLS_ROOT = Path(__file__).parent.parent / "skills" +SKILLS_ROOT = PLUGIN_ROOT / "skills" # Plugin namespace (single source of truth: the plugin manifest) -PLUGIN_MANIFEST = Path(__file__).parent.parent / ".claude-plugin" / "plugin.json" +PLUGIN_MANIFEST = PLUGIN_ROOT / ".claude-plugin" / "plugin.json" PLUGIN_NAME = json.loads(PLUGIN_MANIFEST.read_text())["name"] # Skill inventory (single source of truth)