diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json new file mode 100644 index 0000000..511bd40 --- /dev/null +++ b/.cursor-plugin/plugin.json @@ -0,0 +1,22 @@ +{ + "name": "pinecone", + "description": "Pinecone vector database integration. Streamline your Pinecone development with powerful tools for managing vector indexes, querying data, and rapid prototyping. Use slash commands like /quickstart to generate AGENTS.md files and initialize Python projects and /query to quickly explore indexes. Access the Pinecone MCP server for creating, describing, upserting and querying indexes with Cursor. Perfect for developers building semantic search, RAG applications, recommendation systems, and other vector-based applications with Pinecone.", + "version": "1.0.0", + "author": { + "name": "Pinecone" + }, + "keywords": [ + "pinecone", + "semantic search", + "retrieval", + "vector search", + "vector database", + "retrieval augmented generation", + "rag", + "agentic rag", + "sparse search", + "assistant", + "pinecone assistant", + "rag chatbot" + ] +} diff --git a/.github/workflows/contextualize-skills.yml b/.github/workflows/contextualize-skills.yml new file mode 100644 index 0000000..4c08337 --- /dev/null +++ b/.github/workflows/contextualize-skills.yml @@ -0,0 +1,137 @@ +name: Contextualize Incoming Skills + +on: + pull_request: + types: [opened, synchronize] + +jobs: + contextualize: + if: startsWith(github.head_ref, 'sync/skills-') + runs-on: ubuntu-latest + + permissions: + contents: write + pull-requests: write + + steps: + - name: Checkout PR branch + uses: actions/checkout@v4 + with: + ref: ${{ github.head_ref }} + fetch-depth: 0 + + - name: Skip if last commit was from bot + id: check_author + run: | + AUTHOR="$(git log -1 --format='%an')" + if [ "$AUTHOR" = "github-actions[bot]" ]; then + echo "skip=true" >> "$GITHUB_OUTPUT" + else + echo "skip=false" >> "$GITHUB_OUTPUT" + fi + + - name: Install Cursor CLI + if: steps.check_author.outputs.skip != 'true' + run: | + curl https://cursor.com/install -fsS | bash + echo "$HOME/.cursor/bin" >> "$GITHUB_PATH" + + - name: Write permissions config + if: steps.check_author.outputs.skip != 'true' + run: | + mkdir -p .cursor + cat > .cursor/cli.json << 'PERMS' + { + "permissions": { + "allow": [ + "Read(**)", + "Write(skills/**/*.md)", + "Write(.contextualize-summary.md)", + "Shell(git)", + "Shell(gh)", + "Shell(mv)" + ], + "deny": [ + "Write(.cursor-plugin/**)", + "Write(.github/**)", + "Write(.mcp.json)", + "Write(skills/**/*.py)", + "Write(.env*)", + "Shell(git:push --force *)", + "Shell(git:reset --hard *)", + "Shell(rm:-rf *)" + ] + } + } + PERMS + + - name: Contextualize skills with Cursor Agent + if: steps.check_author.outputs.skip != 'true' + env: + CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }} + GH_TOKEN: ${{ github.token }} + PR_BRANCH: ${{ github.head_ref }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + agent -p --force "$(cat </SKILL.md\` + - SKILL.md requires YAML frontmatter with \`name\` (lowercase kebab-case, must match folder name) and \`description\` + - Skills can include \`scripts/\`, \`references/\`, and \`assets/\` subdirectories + - Folder names must NOT have a \`pinecone-\` prefix + - Only \`.md\` files should be modified — never touch \`.py\` scripts + + ## Steps + + 1. Find the files changed in this PR by running: git diff --name-only origin/main...HEAD + 2. For each changed skill file, adapt it: + - Ensure SKILL.md files have correct YAML frontmatter with \`name\` and \`description\` fields + - Ensure \`name\` in frontmatter is lowercase kebab-case and matches the parent folder name + - Adapt any Claude Code-specific references to Cursor equivalents: + * "Claude" or "Claude Code" → "Cursor" + * "CLAUDE.md" → "AGENTS.md" + * ".claude/" → ".cursor/" + * ".claude-plugin/" → ".cursor-plugin/" + * Slash command references should note they work via /skill-name in Agent chat + - Update source/attribution tags to reflect that these skills are used within the + Cursor plugin (pinecone-io/pinecone-cursor-plugin), not the Claude Code plugin + or the base skills repo. Any source URLs, repo references, or attribution metadata + should point to this Cursor plugin repo. + - Only modify .md files (SKILL.md, references/*.md, etc.) + - Do NOT modify .py files in scripts/ directories — leave them exactly as they are + - Do not change the meaning or content of the skills + - Only change structure, formatting, field names, or terminology as needed + 3. Rename skill directories to strip the \`pinecone-\` prefix if present. + Use \`mv skills/pinecone-foo skills/foo\` for each directory that has the prefix. + 4. Validate against the Cursor plugin submission checklist: + - .cursor-plugin/plugin.json exists and is valid JSON + - Plugin \`name\` is lowercase kebab-case + - Plugin \`description\` clearly explains purpose + - All skills have proper frontmatter (\`name\` and \`description\` fields) + - All rules (.mdc files) have proper frontmatter (\`description\` field) + - All commands have proper frontmatter (\`name\` and \`description\` fields) + - All paths in manifests are relative (no \`..\`, no absolute paths) + 5. Commit your changes directly to this branch with the message: + "contextualize: adapt synced skills for Cursor plugin" + 6. Post a single PR comment using \`gh pr comment\` summarizing: + - What you changed in each file and why + - What you intentionally left unchanged + - Any submission checklist violations found + - Recommendations for manual changes the reviewer should consider + (e.g. existing rules or skills that may now be redundant given + the incoming skills — flag these for the reviewer but do NOT remove them yourself) + + Do not modify files outside of the skills/ directory. + Do not modify .cursor-plugin/ files. + PROMPT + )" diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml new file mode 100644 index 0000000..61756b5 --- /dev/null +++ b/.github/workflows/validate.yml @@ -0,0 +1,16 @@ +name: Validate Plugin + +on: + push: + branches: [main] + pull_request: + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + - run: node scripts/validate-plugin.mjs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..22abfb4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +# Dependencies +node_modules/ + +# OS files +.DS_Store +Thumbs.db + +# Environment +.env +.env.local + +# Contextualization artifacts +.contextualize-summary.md + +# Local dev docs +REPO_MAINTENANCE.md diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 0000000..034191e --- /dev/null +++ b/.mcp.json @@ -0,0 +1,14 @@ +{ + "mcpServers": { + "pinecone": { + "command": "npx", + "args": [ + "-y", + "@pinecone-database/mcp" + ], + "env": { + "PINECONE_API_KEY": "${PINECONE_API_KEY}" + } + } + } +} diff --git a/README.md b/README.md index b62f28b..e212713 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,25 @@ -# pinecone-cursor-plugin -WIP +# Pinecone Cursor Plugin + +Official Pinecone plugin for Cursor. Provides skills, rules, and a Pinecone MCP server integration for building with Pinecone. + +## Skills + +| Skill | Description | +|-------|-------------| +| `quickstart` | Onboarding — create an index, upload data, run your first search | +| `query` | Natural language search across Pinecone indexes via MCP | +| `cli` | Pinecone CLI (`pc`) for index and vector management | +| `assistant` | Pinecone Assistants for document Q&A with citations | +| `mcp` | Reference docs for all MCP server tools and parameters | +| `docs` | Organized links to official Pinecone documentation | +| `help` | Overview of all skills and getting-started guidance | + +## MCP Server + +The plugin bundles the [Pinecone MCP server](https://github.com/pinecone-io/pinecone-mcp) (`@pinecone-database/mcp`). Requires a `PINECONE_API_KEY` environment variable. + +## Prerequisites + +- [Pinecone account](https://app.pinecone.io) (free) +- Pinecone API key +- Node.js v18+ (for the MCP server) diff --git a/commands/.gitkeep b/commands/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/rules/.gitkeep b/rules/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/scripts/validate-plugin.mjs b/scripts/validate-plugin.mjs new file mode 100644 index 0000000..7fe7a3a --- /dev/null +++ b/scripts/validate-plugin.mjs @@ -0,0 +1,227 @@ +#!/usr/bin/env node + +import { promises as fs } from "node:fs"; +import path from "node:path"; +import process from "node:process"; + +const repoRoot = process.cwd(); +const errors = []; +const warnings = []; + +const pluginNamePattern = /^[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/; + +function addError(msg) { errors.push(msg); } +function addWarning(msg) { warnings.push(msg); } + +async function pathExists(p) { + try { await fs.access(p); return true; } catch { return false; } +} + +async function readJsonFile(filePath, context) { + let raw; + try { raw = await fs.readFile(filePath, "utf8"); } catch { + addError(`${context} is missing: ${filePath}`); + return null; + } + try { return JSON.parse(raw); } catch (e) { + addError(`${context} contains invalid JSON (${filePath}): ${e.message}`); + return null; + } +} + +function parseFrontmatter(content) { + const normalized = content.replace(/\r\n/g, "\n"); + if (!normalized.startsWith("---\n")) return null; + const closingIndex = normalized.indexOf("\n---\n", 4); + if (closingIndex === -1) return null; + + const fields = {}; + for (const line of normalized.slice(4, closingIndex).split("\n")) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith("#")) continue; + const sep = line.indexOf(":"); + if (sep === -1) continue; + fields[line.slice(0, sep).trim()] = line.slice(sep + 1).trim(); + } + return fields; +} + +async function walkFiles(dirPath) { + const files = []; + const stack = [dirPath]; + while (stack.length > 0) { + const current = stack.pop(); + const entries = await fs.readdir(current, { withFileTypes: true }); + for (const entry of entries) { + const p = path.join(current, entry.name); + if (entry.isDirectory()) stack.push(p); + else if (entry.isFile()) files.push(p); + } + } + return files; +} + +function isSafeRelativePath(value) { + if (typeof value !== "string" || value.length === 0) return false; + if (value.startsWith("http://") || value.startsWith("https://")) return true; + if (path.isAbsolute(value)) return false; + const normalized = path.posix.normalize(value.replace(/\\/g, "/")); + return !normalized.startsWith("../") && normalized !== ".."; +} + +function extractPathValues(value) { + if (typeof value === "string") return [value]; + if (Array.isArray(value)) return value.flatMap(extractPathValues); + if (value && typeof value === "object") { + const out = []; + if (typeof value.path === "string") out.push(value.path); + if (typeof value.file === "string") out.push(value.file); + return out; + } + return []; +} + +async function validateFrontmatter(filePath, component, requiredKeys) { + const content = await fs.readFile(filePath, "utf8"); + const parsed = parseFrontmatter(content); + const rel = path.relative(repoRoot, filePath); + + if (!parsed) { + addError(`${component} missing YAML frontmatter: ${rel}`); + return; + } + for (const key of requiredKeys) { + if (!parsed[key] || parsed[key].length === 0) { + addError(`${component} missing "${key}" in frontmatter: ${rel}`); + } + } +} + +async function main() { + // 1. Validate plugin.json + const manifestPath = path.join(repoRoot, ".cursor-plugin", "plugin.json"); + const manifest = await readJsonFile(manifestPath, "Plugin manifest"); + if (!manifest) return summarize(); + + if (typeof manifest.name !== "string" || !pluginNamePattern.test(manifest.name)) { + addError(`plugin.json "name" must be lowercase kebab-case: got "${manifest.name}"`); + } + + if (!manifest.description || manifest.description.length === 0) { + addWarning(`plugin.json missing "description"`); + } + + // 2. Validate manifest path references + const pathFields = ["logo", "rules", "skills", "agents", "commands", "hooks", "mcpServers"]; + for (const field of pathFields) { + for (const value of extractPathValues(manifest[field])) { + if (!isSafeRelativePath(value)) { + addError(`plugin.json "${field}" has unsafe path: "${value}"`); + } else if (!value.startsWith("http")) { + const resolved = path.resolve(repoRoot, value); + if (!(await pathExists(resolved))) { + addError(`plugin.json "${field}" references missing path: "${value}"`); + } + } + } + } + + // 3. Validate skills + const skillsDir = path.join(repoRoot, "skills"); + if (await pathExists(skillsDir)) { + const files = await walkFiles(skillsDir); + const skillMds = files.filter((f) => path.basename(f) === "SKILL.md"); + if (skillMds.length === 0) { + addWarning("skills/ directory exists but contains no SKILL.md files"); + } + for (const file of skillMds) { + await validateFrontmatter(file, "Skill", ["name", "description"]); + + // Check name matches folder + const content = await fs.readFile(file, "utf8"); + const parsed = parseFrontmatter(content); + if (parsed?.name) { + const folderName = path.basename(path.dirname(file)); + if (parsed.name !== folderName) { + addError(`Skill name "${parsed.name}" doesn't match folder "${folderName}": ${path.relative(repoRoot, file)}`); + } + } + } + } + + // 4. Validate rules + const rulesDir = path.join(repoRoot, "rules"); + if (await pathExists(rulesDir)) { + const files = await walkFiles(rulesDir); + for (const file of files) { + const ext = path.extname(file).toLowerCase(); + if ([".md", ".mdc", ".markdown"].includes(ext)) { + await validateFrontmatter(file, "Rule", ["description"]); + } + } + } + + // 5. Validate agents + const agentsDir = path.join(repoRoot, "agents"); + if (await pathExists(agentsDir)) { + const files = await walkFiles(agentsDir); + for (const file of files) { + const ext = path.extname(file).toLowerCase(); + if ([".md", ".mdc", ".markdown"].includes(ext)) { + await validateFrontmatter(file, "Agent", ["name", "description"]); + } + } + } + + // 6. Validate commands + const commandsDir = path.join(repoRoot, "commands"); + if (await pathExists(commandsDir)) { + const files = await walkFiles(commandsDir); + for (const file of files) { + const ext = path.extname(file).toLowerCase(); + if ([".md", ".mdc", ".markdown", ".txt"].includes(ext)) { + await validateFrontmatter(file, "Command", ["name", "description"]); + } + } + } + + // 7. Check MCP config + const mcpPath = path.join(repoRoot, ".mcp.json"); + if (await pathExists(mcpPath)) { + const mcp = await readJsonFile(mcpPath, "MCP config"); + if (mcp && !mcp.mcpServers) { + addError('.mcp.json missing "mcpServers" key'); + } + } else { + addWarning("No .mcp.json found"); + } + + // 8. Check hooks + const hooksPath = path.join(repoRoot, "hooks", "hooks.json"); + if (await pathExists(hooksPath)) { + await readJsonFile(hooksPath, "Hooks config"); + } + + // 9. Check README + if (!(await pathExists(path.join(repoRoot, "README.md")))) { + addWarning("No README.md found"); + } + + summarize(); +} + +function summarize() { + if (warnings.length > 0) { + console.log("Warnings:"); + for (const w of warnings) console.log(` - ${w}`); + console.log(); + } + if (errors.length > 0) { + console.error("Validation failed:"); + for (const e of errors) console.error(` - ${e}`); + process.exit(1); + } + console.log("Validation passed."); +} + +await main(); diff --git a/skills/assistant/.gitkeep b/skills/assistant/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/skills/assistant/references/.gitkeep b/skills/assistant/references/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/skills/assistant/scripts/.gitkeep b/skills/assistant/scripts/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/skills/cli/.gitkeep b/skills/cli/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/skills/cli/references/.gitkeep b/skills/cli/references/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/skills/docs/.gitkeep b/skills/docs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/skills/docs/references/.gitkeep b/skills/docs/references/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/skills/help/.gitkeep b/skills/help/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/skills/mcp/.gitkeep b/skills/mcp/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/skills/query/.gitkeep b/skills/query/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/skills/quickstart/.gitkeep b/skills/quickstart/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/skills/quickstart/scripts/.gitkeep b/skills/quickstart/scripts/.gitkeep new file mode 100644 index 0000000..e69de29