diff --git a/shipkit/__init__.py b/shipkit/__init__.py index 3b3552b..141dd0b 100644 --- a/shipkit/__init__.py +++ b/shipkit/__init__.py @@ -1,3 +1,3 @@ """Shipkit — CLI-agnostic AI dev productivity kit.""" -__version__ = "0.1.12" +__version__ = "0.1.13" diff --git a/shipkit/cli.py b/shipkit/cli.py index 386df47..e3ea05d 100644 --- a/shipkit/cli.py +++ b/shipkit/cli.py @@ -777,10 +777,16 @@ def run(prompt: tuple[str, ...], no_agent: bool): """ import subprocess import shutil + from pathlib import Path from shipkit.sync import sync_project from shipkit.project import ProjectError from shipkit.datadir import DataDirError + # Detect if this is a first-time project (before sync creates the agent file) + repo_path = Path.cwd().resolve() + agent_file = repo_path / ".claude" / "agents" / "shipkit.md" + is_first_run = not agent_file.exists() + # Sync first try: result = sync_project() @@ -803,8 +809,21 @@ def run(prompt: tuple[str, ...], no_agent: bool): cmd = ["claude"] if not no_agent: cmd.extend(["--agent", f"shipkit v{__version__}"]) - if prompt: - cmd.append(" ".join(prompt)) + + # Build prompt — inject init hint for first-time projects + prompt_str = " ".join(prompt) if prompt else "" + if is_first_run and not no_agent: + init_hint = ( + "This project hasn't been configured with shipkit yet. " + "Offer to run /init to set up project-specific MCP servers and preferences." + ) + if prompt_str: + prompt_str = init_hint + " Then: " + prompt_str + else: + prompt_str = init_hint + + if prompt_str: + cmd.append(prompt_str) click.echo("Launching shipkit on Claude Code...") subprocess.run(cmd, check=False) diff --git a/shipkit/compilers/base.py b/shipkit/compilers/base.py index c1a9511..f66f78b 100644 --- a/shipkit/compilers/base.py +++ b/shipkit/compilers/base.py @@ -109,6 +109,11 @@ def team_guidelines(self) -> Path: def team_skills(self) -> Path: return self.repo_path / ".claude" / "skills" + @property + def team_mcp(self) -> Path: + """Project MCP config (git-committed, team-shared).""" + return self.repo_path / ".claude" / "mcp.json" + # --- Subagents --- @property @@ -206,6 +211,9 @@ def mcp_layers(self) -> list[Path]: layers.append(pd / "mcp.json") layers.append(self.user_mcp) + + # Project-level (highest precedence, team-shared) + layers.append(self.team_mcp) return layers @property diff --git a/shipkit/core/skills/init/SKILL.md b/shipkit/core/skills/init/SKILL.md new file mode 100644 index 0000000..1e1e759 --- /dev/null +++ b/shipkit/core/skills/init/SKILL.md @@ -0,0 +1,289 @@ +--- +name: init +description: Initialize shipkit for the current project. Detects tech stack, recommends and configures MCP servers, and runs sync. Use when user says "init", "initialize", "set up this project", or on first use in a new project. +--- + +# Initialize Project + +Set up shipkit for the current project directory. Detects the project's tech stack and walks the user through selecting MCP servers relevant to this project. + +## When To Use + +- First time using shipkit in a project +- When the shipkit agent suggests running `/init` (auto-detected new project) +- When user wants to add or change MCP servers for a project +- When user says "init", "initialize", "set up this project" + +## Workflow + +### Step 1: Verify Prerequisites + +Confirm we're in a git repo and shipkit is installed globally: + +```bash +git rev-parse --show-toplevel +shipkit --version +``` + +If not a git repo, tell the user to run `git init` first. +If shipkit isn't installed, point them to `shipkit install`. + +### Step 2: Check Existing State + +Check what's already configured for this project: + +```bash +# Check if already initialized +test -f .claude/agents/shipkit.md && echo "Agent exists" || echo "No agent" +test -f .claude/mcp.json && echo "MCP config exists" || echo "No MCP config" +test -f CLAUDE.md && echo "CLAUDE.md exists" || echo "No CLAUDE.md" +``` + +If `.claude/mcp.json` already exists, read it and show current MCP servers. +Ask: "Update existing config or start fresh?" + +### Step 3: Detect Tech Stack + +Scan the project root for tech stack indicators: + +**Check for these files (use Glob, not find):** + +| File | Indicates | +|------|-----------| +| `package.json` | Node.js — read it to detect frameworks (React, Vue, Next.js, Express, etc.) | +| `tsconfig.json` | TypeScript | +| `pyproject.toml` | Python — read it to detect frameworks (Flask, Django, FastAPI, etc.) | +| `requirements.txt` | Python | +| `Cargo.toml` | Rust | +| `go.mod` | Go | +| `Gemfile` | Ruby | +| `pom.xml` | Java (Maven) | +| `build.gradle` | Java/Kotlin (Gradle) | +| `*.sln` or `*.csproj` | .NET/C# | +| `docker-compose.yml` | Docker/containers | +| `prisma/schema.prisma` | Database (Prisma ORM) | +| `*.sql` files | Database | + +**Check git remote:** + +```bash +git remote get-url origin 2>/dev/null +``` + +If it's a `github.com` URL, note this for GitHub MCP recommendation. + +**Present findings:** + +``` +Detected tech stack: + - Python (pyproject.toml, FastAPI) + - PostgreSQL (sqlalchemy in dependencies) + - GitHub remote (github.com/team/api-service) + - Docker (docker-compose.yml) +``` + +### Step 4: Recommend MCP Servers + +Based on the detected tech stack, recommend relevant MCP servers. Present as a selection list with recommendations pre-checked. + +**Recommendation rules:** + +| Condition | Recommend | Why | +|-----------|-----------|-----| +| GitHub remote detected | GitHub MCP | Enhances /pr and /review skills | +| Any project | Brave Search | Powers /research skill | +| `package.json` with test deps (jest, vitest, playwright) | Playwright MCP | Browser testing automation | +| Database deps (sqlalchemy, prisma, pg, mysql2, diesel) | Postgres or SQLite MCP | Direct DB queries | +| Any project | Filesystem MCP | Enhanced file operations | + +**Do NOT recommend by default** (only if user asks): +- Slack (privacy concerns) +- Linear (requires paid account) + +**Present selection:** + +``` +Recommended MCP servers for this project: + + [x] GitHub - View/create PRs and issues + Uses your GITHUB_TOKEN + + [x] Brave Search - Web search for /research skill + Free API key: https://brave.com/search/api/ + + [ ] Filesystem - Enhanced file operations + No API key needed + + [ ] Playwright - Browser automation for testing + Requires: Node.js + +Select servers to install, or skip MCP setup: +``` + +Pre-check servers that match the detected stack. Let user toggle. + +### Step 5: Configure Selected MCPs + +For each selected MCP server: + +#### 5.1 Check Prerequisites + +- **Node.js required?** Check `which node`. If missing and needed, warn the user. +- **API key required?** Check if the relevant env var exists. + +#### 5.2 Guide API Key Setup + +For each MCP that needs an API key: + +``` +GitHub MCP needs GITHUB_TOKEN. + +Checking... Found $GITHUB_TOKEN in environment. +Use this token? (y/n) +``` + +If not found: + +``` +GitHub MCP needs GITHUB_TOKEN. + +Options: + 1. Create a new token: https://github.com/settings/tokens + Scopes needed: repo, read:org + 2. Skip GitHub MCP (add later with /init) + +Enter token, or skip: +``` + +**IMPORTANT:** Never store raw API key values in `.claude/mcp.json`. Use environment variable references (`${GITHUB_TOKEN}`) so tokens aren't committed to git. + +#### 5.3 Write Project MCP Config + +Write `.claude/mcp.json` with the selected servers: + +```json +{ + "mcpServers": { + "github": { + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-github"], + "env": { + "GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_TOKEN}" + } + }, + "brave-search": { + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-brave-search"], + "env": { + "BRAVE_API_KEY": "${BRAVE_API_KEY}" + } + } + } +} +``` + +**IMPORTANT:** This file goes in `.claude/mcp.json` (project-level, git-committed). +It is NOT `~/.claude/mcp.json` (global) and NOT `~/.config/shipkit/mcp.json` (user-level). + +Project MCP config is the highest precedence layer — it overrides user-level defaults. + +### Step 6: Sync Project + +Run sync to compile the MCP config into the agent: + +```bash +shipkit sync +``` + +This generates/updates `.claude/agents/shipkit.md` with the MCP servers embedded in the agent frontmatter. + +### Step 7: Summary + +Present what was configured: + +``` +Project initialized! + + Tech stack: Python (FastAPI), PostgreSQL, Docker + MCP servers configured: 2 + + GitHub (using $GITHUB_TOKEN) + + Brave Search (using $BRAVE_API_KEY) + + Files created/updated: + + .claude/mcp.json (project MCP config) + + .claude/agents/shipkit.md (agent with embedded MCPs) + + CLAUDE.md (skill/guideline discovery) + + MCP servers are agent-scoped — they only activate when using + the shipkit agent, not in regular Claude Code sessions. + + To add more MCP servers later, run /init again. +``` + +## MCP Server Reference + +Quick reference for common MCP server configurations: + +### GitHub +```json +{ + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-github"], + "env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_TOKEN}" } +} +``` +Requires: Node.js, GITHUB_TOKEN + +### Brave Search +```json +{ + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-brave-search"], + "env": { "BRAVE_API_KEY": "${BRAVE_API_KEY}" } +} +``` +Requires: Node.js, BRAVE_API_KEY (free at https://brave.com/search/api/) + +### Filesystem +```json +{ + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/allowed/dir"] +} +``` +Requires: Node.js + +### Playwright +```json +{ + "command": "npx", + "args": ["-y", "@playwright/mcp@latest"] +} +``` +Requires: Node.js + +### PostgreSQL +```json +{ + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-postgres", "${DATABASE_URL}"] +} +``` +Requires: Node.js, DATABASE_URL + +### SQLite +```json +{ + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-sqlite", "--db-path", "./db.sqlite"] +} +``` +Requires: Node.js + +## Key Principles + +- **Detect, don't guess** — Scan the actual project before recommending +- **Env vars, not raw tokens** — Never commit secrets to `.claude/mcp.json` +- **Project-level config** — Write to `.claude/mcp.json`, not global config +- **Idempotent** — Running /init again updates existing config safely +- **Respect existing setup** — If MCPs already configured, show and offer to update diff --git a/shipkit/core/skills/install/SKILL.md b/shipkit/core/skills/install/SKILL.md index fad10cb..368ca15 100644 --- a/shipkit/core/skills/install/SKILL.md +++ b/shipkit/core/skills/install/SKILL.md @@ -119,114 +119,13 @@ Select which to enable (space to toggle, enter to confirm): Save preference to memory for config.yaml creation. -#### 3.2 MCP Server Selection +#### 3.2 MCP Servers -Offer to install commonly-used MCP servers: +MCP servers are configured per-project during `/init`, not during global install. +Each project gets its own MCP servers based on its tech stack. -``` -Would you like to install any MCP servers? (Enhance skills like /research, /pr) - -Essential (free, enhance core skills): - [ ] Brave Search - Powers /research skill with web search - Free API key: https://brave.com/search/api/ - - [ ] Filesystem - Enhanced file operations - No API key needed - - [ ] GitHub - View/create PRs and issues (enhances /pr, /review) - Uses your existing GITHUB_TOKEN - -Development Tools: - [ ] Playwright - Browser automation for testing - Requires: Node.js - - [ ] SQLite - Local database queries - No API key needed - -Team/Productivity: - [ ] Slack - Read/send messages - Requires: SLACK_BOT_TOKEN - - [ ] Linear - Issue tracking - Requires: LINEAR_API_KEY - -Or: Skip MCP setup (you can add later) -``` - -For each selected MCP: - -1. **Check prerequisites:** - - Node.js installed? (`which node`) - - API key available? (check environment or prompt) - -2. **Guide API key setup:** - ``` - To use GitHub MCP, you need GITHUB_TOKEN. - - You can: - 1. Use existing token from environment (found: $GITHUB_TOKEN) - 2. Create a new token: https://github.com/settings/tokens - 3. Add to ~/.claude/settings.json: - { - "env": { - "GITHUB_TOKEN": "ghp_your_token" - } - } - - Enter token now, or skip (add later): - ``` - -3. **Test MCP works:** - ```bash - # Test the MCP server responds - echo '{"jsonrpc":"2.0","method":"initialize","params":{},"id":1}' | npx -y @modelcontextprotocol/server-github - ``` - - If successful: "✓ GitHub MCP configured and responding" - If fails: "⚠️ GitHub MCP installed but not responding. Check API key." - -4. **Save MCP preferences to shipkit config:** - Write selected MCPs to `~/.config/shipkit/mcp.json` (NOT `~/.claude/mcp.json`): - ```json - { - "mcpServers": { - "brave-search": { - "command": "npx", - "args": ["-y", "@modelcontextprotocol/server-brave-search"], - "env": { - "BRAVE_API_KEY": "${BRAVE_API_KEY}" - } - }, - "github": { - "command": "npx", - "args": ["-y", "@modelcontextprotocol/server-github"], - "env": { - "GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_TOKEN}" - } - } - } - } - ``` - - **IMPORTANT:** MCP servers are agent-scoped, not global. They are compiled into - `.claude/agents/shipkit.md` during `shipkit sync`, so they only activate when using - the shipkit agent. Do NOT write to `~/.claude/mcp.json` — that would make MCPs - global across all Claude Code sessions. - -**MCP Selection Guidelines:** - -**Always recommend:** -- **Brave Search** if user selected /research skill -- **GitHub** if in a git repo with remote on github.com - -**Recommend based on tech stack detection:** -- **Playwright** if package.json has testing frameworks -- **Postgres/SQLite** if project uses databases -- **Linear** if `.linear` directory exists or mentioned in docs - -**Don't recommend:** -- **Slack** unless user explicitly interested (privacy concerns) -- **Context7** (paid service, requires setup) +When a user first runs `sk` in a project, shipkit will automatically offer to run +`/init` to set up project-specific MCP servers and preferences. ### Phase 4: Install @@ -331,19 +230,20 @@ Show what files were generated (.claude/agents/shipkit.md, CLAUDE.md, etc.) Display success message with guidance: ``` -🎉 Shipkit installed successfully! +Shipkit installed successfully! Installed: - ✅ Layer preferences saved to ~/.config/shipkit/config.yaml - ✅ Hooks merged into ~/.claude/settings.json - ✅ Shipkit agent created at ~/.claude/agents/shipkit.md - ✅ {N} MCP servers configured (agent-scoped, not global) + + Layer preferences saved to ~/.config/shipkit/config.yaml + + Hooks merged into ~/.claude/settings.json + + Shipkit agent created at ~/.claude/agents/shipkit.md Next steps: 1. cd to a git repository - 2. Run: shipkit sync # Generate Claude Code config for this repo - 3. Run: shipkit run # Launch shipkit agent - Or: claude --agent shipkit # Launch manually + 2. Run: sk # Launch shipkit (auto-syncs + offers /init) + Or: shipkit run # Same thing, longer name + + First launch in a project will offer to run /init to set up + project-specific MCP servers based on your tech stack. Quick access: Run: shipkit alias sk --install @@ -353,11 +253,10 @@ Try your new skills: /commit # Smart git commits /pr # Generate pull requests /review # Code reviews - /research "query" # Multi-source research (if Brave MCP installed) + /init # Set up MCP servers for a project Documentation: - Skills: Use /skills in any session to see all available - - MCP docs: ~/.local/lib/.../shipkit/docs/mcp-servers.md - Help: shipkit --help Your existing Claude Code setup has been preserved. All your personal skills, diff --git a/tests/test_compilers.py b/tests/test_compilers.py index 5584a07..bacdd16 100644 --- a/tests/test_compilers.py +++ b/tests/test_compilers.py @@ -59,6 +59,21 @@ def test_plugins_in_layer_order(self, compile_ctx): assert "plugins" in str(layers) + def test_mcp_layers_includes_project(self, compile_ctx): + """Test MCP layers include project-level path as highest precedence.""" + ctx = compile_ctx + layers = ctx.mcp_layers + # Last layer should be the project-level MCP (highest precedence) + assert layers[-1] == ctx.team_mcp + assert str(ctx.repo_path / ".claude" / "mcp.json") == str(layers[-1]) + + def test_team_mcp_property(self, compile_ctx): + """Test team_mcp points to .claude/mcp.json in project.""" + ctx = compile_ctx + expected = ctx.repo_path / ".claude" / "mcp.json" + assert ctx.team_mcp == expected + + class TestGetCompiler: def test_get_claude_compiler(self): diff --git a/tests/test_run.py b/tests/test_run.py index 752cde4..b7c7ce0 100644 --- a/tests/test_run.py +++ b/tests/test_run.py @@ -63,12 +63,59 @@ def test_run_without_agent(self, initialized_home, tmp_repo, monkeypatch): assert "claude" in call_args assert "--agent" not in call_args + def test_run_first_time_injects_init_hint(self, initialized_home, tmp_repo, monkeypatch): + """Test run command injects init hint for uninitialized projects.""" + runner = CliRunner() + + monkeypatch.chdir(tmp_repo) + + # Ensure no agent file exists (first run) + agent_file = tmp_repo / ".claude" / "agents" / "shipkit.md" + assert not agent_file.exists() + + with patch("subprocess.run") as mock_run, \ + patch("shutil.which", return_value="/usr/bin/claude"): + + result = runner.invoke(main, ["run"]) + + assert mock_run.called + call_args = mock_run.call_args[0][0] + # Should include init hint in the prompt + prompt_arg = call_args[-1] + assert "/init" in prompt_arg + + def test_run_existing_project_no_hint(self, initialized_home, tmp_repo, monkeypatch): + """Test run command does NOT inject init hint for already-initialized projects.""" + runner = CliRunner() + + monkeypatch.chdir(tmp_repo) + + # Create the agent file to simulate an initialized project + agent_dir = tmp_repo / ".claude" / "agents" + agent_dir.mkdir(parents=True, exist_ok=True) + (agent_dir / "shipkit.md").write_text("---\nname: shipkit\n---\n") + + with patch("subprocess.run") as mock_run, \ + patch("shutil.which", return_value="/usr/bin/claude"): + + result = runner.invoke(main, ["run"]) + + assert mock_run.called + call_args = mock_run.call_args[0][0] + # Should NOT include any init hint (just claude + --agent) + assert len(call_args) == 3 # claude, --agent, "shipkit v..." + def test_run_with_prompt(self, initialized_home, tmp_repo, monkeypatch): """Test run command with initial prompt.""" runner = CliRunner() monkeypatch.chdir(tmp_repo) + # Create agent file so we test prompt passthrough without init hint + agent_dir = tmp_repo / ".claude" / "agents" + agent_dir.mkdir(parents=True, exist_ok=True) + (agent_dir / "shipkit.md").write_text("---\nname: shipkit\n---\n") + with patch("subprocess.run") as mock_run, \ patch("shutil.which", return_value="/usr/bin/claude"):