Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 71 additions & 46 deletions cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
import { spawnSync } from 'child_process';
import { spawnSync, spawn } from 'child_process';
import inquirer from 'inquirer';
import chalk from 'chalk';
import fs from 'fs-extra';
Expand Down Expand Up @@ -208,12 +208,14 @@ async function main() {
name: 'aiTools',
message:
'Which AI coding assistant(s) are you using? (Select all that apply)\n' +
chalk.gray(' Well add local instruction files so your assistant knows Cloudinary patterns.\n'),
chalk.gray(' We\'ll install Cloudinary skills in the right location for each tool.\n'),
choices: [
{ name: 'Cursor', value: 'cursor' },
{ name: 'GitHub Copilot', value: 'copilot' },
{ name: 'Claude Code', value: 'claude' },
{ name: 'Other / Generic AI tools', value: 'generic' },
{ name: 'GitHub Copilot', value: 'copilot' },
{ name: 'OpenAI Codex', value: 'codex' },
{ name: 'Gemini CLI', value: 'gemini' },
{ name: 'Other', value: 'generic' },
],
default: ['cursor'],
},
Expand Down Expand Up @@ -316,35 +318,68 @@ async function main() {
copyTemplate(file);
});

// Create AI rules based on user's tool selection
const aiRulesTemplatePath = join(TEMPLATES_DIR, '.cursorrules.template');
if (existsSync(aiRulesTemplatePath) && aiTools && aiTools.length > 0) {
const aiRulesContent = replaceTemplate(
readFileSync(aiRulesTemplatePath, 'utf-8'),
templateVars
);
// Install Cloudinary skills into the directories each selected tool actually reads:
// .cursor/skills/ = Cursor
// .claude/skills/ = Claude Code
// .agents/skills/ = Copilot, Codex, Gemini, Generic (and any unrecognised tool)
console.log(chalk.blue('\n🤖 Installing Cloudinary AI skills...\n'));

const skillTargetDirs = new Set();
if (aiTools && aiTools.includes('cursor')) skillTargetDirs.add(join(projectPath, '.cursor', 'skills'));
if (aiTools && aiTools.includes('claude')) skillTargetDirs.add(join(projectPath, '.claude', 'skills'));
if (!aiTools || aiTools.some(t => !['cursor', 'claude'].includes(t))) {
skillTargetDirs.add(join(projectPath, '.agents', 'skills'));
}

// Generate files based on selected tools
if (aiTools.includes('cursor')) {
writeFileSync(join(projectPath, '.cursorrules'), aiRulesContent);
}
const skills = ['cloudinary-docs', 'cloudinary-react', 'cloudinary-transformations'];
// Download each skill once then copy to any additional targets
const [primaryDir, ...additionalDirs] = skillTargetDirs;
mkdirSync(primaryDir, { recursive: true });

if (aiTools.includes('copilot')) {
const githubDir = join(projectPath, '.github');
mkdirSync(githubDir, { recursive: true });
writeFileSync(join(githubDir, 'copilot-instructions.md'), aiRulesContent);
}
// Print all skill names upfront so the user sees activity immediately
for (const skill of skills) {
console.log(chalk.gray(` Fetching ${skill}...`));
}

if (aiTools.includes('claude')) {
writeFileSync(join(projectPath, 'CLAUDE.md'), aiRulesContent);
// Download all 3 skills in parallel
const results = await Promise.all(
skills.map(skill => new Promise(resolve => {
const proc = spawn(
'npx',
['--yes', 'degit', `cloudinary-devs/skills/skills/${skill}`, join(primaryDir, skill)],
{ stdio: 'pipe', shell: false }
);
proc.on('close', code => resolve({ skill, ok: code === 0 }));
}))
);

const BUNDLED_SKILLS_DIR = join(__dirname, 'skills');

let skillsInstalled = 0;
for (const { skill, ok } of results) {
if (ok) {
console.log(chalk.gray(` ✓ ${skill}`));
skillsInstalled++;
} else {
const bundled = join(BUNDLED_SKILLS_DIR, skill);
if (existsSync(bundled)) {
fs.copySync(bundled, join(primaryDir, skill));
console.log(chalk.gray(` ✓ ${skill} (installed from local cache)`));
skillsInstalled++;
} else {
console.warn(chalk.yellow(` ⚠ Could not install ${skill}`));
}
}
}

if (aiTools.includes('generic')) {
writeFileSync(join(projectPath, 'AI_INSTRUCTIONS.md'), aiRulesContent);
writeFileSync(join(projectPath, 'PROMPT.md'), aiRulesContent);
if (skillsInstalled > 0) {
for (const target of additionalDirs) {
fs.copySync(primaryDir, target);
}
}

// Generate MCP configuration: Cursor uses .cursor/mcp.json, Claude Code uses .mcp.json in project root
// Generate MCP configuration: Cursor uses .cursor/mcp.json, Claude Code uses .mcp.json in project root
if (aiTools && aiTools.length > 0) {
const mcpTemplatePath = join(TEMPLATES_DIR, '.cursor/mcp.json.template');
if (existsSync(mcpTemplatePath)) {
const mcpContent = replaceTemplate(
Expand All @@ -370,26 +405,16 @@ async function main() {

console.log(chalk.green('✅ Project created successfully!\n'));

if (aiTools && aiTools.length > 0) {
// Count actual files created
let fileCount = 0;
if (aiTools.includes('cursor')) fileCount += 2; // .cursorrules + mcp.json
if (aiTools.includes('copilot')) fileCount += 1;
if (aiTools.includes('claude')) fileCount += 2; // CLAUDE.md + .mcp.json
if (aiTools.includes('generic')) fileCount += 2; // AI_INSTRUCTIONS.md + PROMPT.md

const filesText = fileCount === 1 ? 'file' : 'files';
console.log(chalk.cyan(`📋 AI assistant configuration ${filesText} created:`));
if (aiTools.includes('cursor')) console.log(chalk.gray(' • Cursor: .cursorrules'));
if (aiTools.includes('copilot')) console.log(chalk.gray(' • GitHub Copilot: .github/copilot-instructions.md'));
if (aiTools.includes('claude')) console.log(chalk.gray(' • Claude: CLAUDE.md'));
if (aiTools.includes('generic')) console.log(chalk.gray(' • Generic: AI_INSTRUCTIONS.md, PROMPT.md'));
if (aiTools.includes('cursor')) console.log(chalk.gray(' • MCP (Cursor): .cursor/mcp.json'));
if (aiTools.includes('claude')) console.log(chalk.gray(' • MCP (Claude Code): .mcp.json'));
console.log(chalk.gray(`\n ${fileCount === 1 ? 'This file teaches' : 'These files teach'} your AI assistant about Cloudinary patterns and best practices.`));
console.log(chalk.gray(`\n 💡 How to use ${fileCount === 1 ? 'this file' : 'these files'}:`));
console.log(chalk.gray(' • Simply open your project in your AI assistant - the configuration is already loaded'));
console.log(chalk.gray(' • Ask your AI to help build Cloudinary features, and it will follow these patterns'));
if (skillsInstalled > 0) {
console.log(chalk.cyan(`\n📋 AI skills installed → .agents/skills/ (works with Cursor, Claude Code, Copilot, and more):`));
console.log(chalk.gray(' • cloudinary-docs — looks up live Cloudinary documentation'));
console.log(chalk.gray(' • cloudinary-react — uses React SDK patterns and best practices'));
console.log(chalk.gray(' • cloudinary-transformations — creates and debugs Cloudinary transformation URLs from natural language instructions'));
if (aiTools && aiTools.includes('cursor')) console.log(chalk.gray(' • MCP (Cursor): .cursor/mcp.json'));
if (aiTools && aiTools.includes('claude')) console.log(chalk.gray(' • MCP (Claude Code): .mcp.json'));
console.log(chalk.gray('\n 💡 How to use:'));
console.log(chalk.gray(' • Open your project in your AI assistant — skills are picked up automatically'));
console.log(chalk.gray(' • Ask your AI assistant to help build Cloudinary features and it will use these skills'));
console.log(chalk.gray(' • Example prompts: "Add image upload", "Create a transformation gallery"\n'));
}

Expand Down
53 changes: 53 additions & 0 deletions skills/cloudinary-docs/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
---
name: cloudinary-docs
description: Local version -- Looks up implementation details in the latest Cloudinary docs via llms.txt. Use when building code or answering questions relating to image or video uploads, optimization, or transformations, and for Cloudinary SDKs, APIs, webhooks, or integrations.
license: MIT
metadata:
author: cloudinary
version: '1.0.1'
---

# Cloudinary Documentation

Helps developers integrate Cloudinary into their applications by providing documentation and code examples retrieved directly from the optimized markdown files in the Cloudinary documentation.

## When to Use

- When a user asks questions or requests code implementation relating to image or video upload, management, optimization, or transformations (resizing, applying effects, visual improvements, adding overlays, generative AI, etc.)
- User asks about Cloudinary SDKs, upload APIs, or integration guides
- General Cloudinary documentation lookup (account settings, webhooks, DAM features)
- Looking up specific Cloudinary API endpoints or SDK methods
- Use this skill in conjunction with more specialized Cloudinary skills when relevant.

## Instructions

When answering image and video upload, management, optimization, or transformation questions or when implementing Cloudinary code:

1. **First, get the documentation index** using llms.txt with the llms.txt URL - https://cloudinary.com/documentation/llms.txt
2. **Analyze the llms.txt content** to understand what documentation pages are available
3. **Reflect on the user's question** and identify which specific documentation URLs would be most relevant
4. **Navigate** to the specific relevant documentation URLs from the llms.txt index (you can make multiple calls)
5. **Use the fetched documentation** to provide a comprehensive, accurate answer or code implementation. When relevant, use in conjunction with more specialized Cloudinary skills like cloudinary-transformations. The best practices defined in the specialized skills should guide which doc instructions to use.

Example workflows:

**Example 1: Upload question**
- User asks: "How do I upload images to Cloudinary?"
- You retrieve the llms.txt index: https://cloudinary.com/documentation/llms.txt
- You analyze the llms.txt content to understand what documentation pages are available
- You identify relevant pages like "image_upload.md" or "upload_api.md"
- You retrieve those specific pages from the llms.txt index
- You provide an answer with code examples and citations

**Example 2: Transformation question**
- User asks: "How do I resize and crop images?"
- You retrieve the llms.txt index
- You identify relevant pages like "image_transformations.md" or "transformation_reference.md"
- You fetch the specific documentation
- You provide transformation syntax and examples

**Example 3: SDK question**
- User asks: "What's the Node.js SDK for Cloudinary?"
- You retrieve the llms.txt index
- You identify SDK-related pages
- You provide installation instructions and usage examples
Loading