diff --git a/apps/web/content/docs/(unrag)/ai-assisted-coding.mdx b/apps/web/content/docs/(unrag)/ai-assisted-coding.mdx index a3ae363..0755b7d 100644 --- a/apps/web/content/docs/(unrag)/ai-assisted-coding.mdx +++ b/apps/web/content/docs/(unrag)/ai-assisted-coding.mdx @@ -8,13 +8,25 @@ AI coding assistants often hallucinate method signatures and configuration optio ## Installing the Unrag skill -Installing the skill is straightforward. You use the `add-skill` CLI, a tool from Vercel Labs that installs Agent Skills into your project: +There are two ways to install the Unrag skill: + +### Option 1: Using the Unrag CLI (recommended) + +```bash +bunx unrag add skills +``` + +This launches an interactive prompt where you can select which AI coding assistants to install the skill for. You can install for multiple agents at once—Claude Code, Cursor, Windsurf, Antigravity, or Codex. + +### Option 2: Using add-skill + +You can also use the `add-skill` CLI from Vercel Labs: ```bash bunx add-skill betterstacks/unrag ``` -This command downloads the skill and places it in your project's `.claude/skills/` directory (or the equivalent location for your AI assistant). From that point forward, your AI assistant has access to the complete Unrag reference when working in your codebase. +This installs the skill specifically for Claude Code into your project's `.claude/skills/` directory. The skill is designed with Claude Code in mind, but the format works with other AI assistants that support similar knowledge structures. The underlying content is just markdown files with structured information—any assistant that can read and reason over project files can benefit. @@ -119,7 +131,7 @@ The Unrag skill is versioned to track Unrag releases: | Component | Version | |-----------|---------| -| Skill Version | 1.0.0 | +| Skill Version | 0.3.2 | | Unrag CLI Version | 0.3.2 | | Config Version | 2 | diff --git a/apps/web/content/docs/(unrag)/meta.json b/apps/web/content/docs/(unrag)/meta.json index 80ffe72..d47cc5f 100644 --- a/apps/web/content/docs/(unrag)/meta.json +++ b/apps/web/content/docs/(unrag)/meta.json @@ -22,4 +22,4 @@ "examples", "reference" ] -} \ No newline at end of file +} diff --git a/bun.lock b/bun.lock index ef1b443..b13e7d4 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "unrag", diff --git a/packages/unrag/cli/commands/add.ts b/packages/unrag/cli/commands/add.ts index fafd02b..051b4ad 100644 --- a/packages/unrag/cli/commands/add.ts +++ b/packages/unrag/cli/commands/add.ts @@ -481,7 +481,7 @@ const addPackageJsonScripts = async (args: { } type ParsedAddArgs = { - kind?: 'connector' | 'extractor' | 'battery' + kind?: 'connector' | 'extractor' | 'battery' | 'skills' name?: string yes?: boolean noInstall?: boolean @@ -515,6 +515,10 @@ const parseAddArgs = (args: string[]): ParsedAddArgs => { out.kind = 'battery' continue } + if (a === 'skills') { + out.kind = 'skills' + continue + } out.kind = 'connector' out.name = a continue @@ -534,6 +538,14 @@ const parseAddArgs = (args: string[]): ParsedAddArgs => { } export async function addCommand(args: string[]) { + // Handle skills subcommand early (doesn't require project root) + const parsed = parseAddArgs(args) + if (parsed.kind === 'skills') { + const {skillsCommand} = await import('./skills') + await skillsCommand(args) + return + } + const root = await tryFindProjectRoot(process.cwd()) if (!root) { throw new Error( @@ -541,7 +553,6 @@ export async function addCommand(args: string[]) { ) } - const parsed = parseAddArgs(args) const kind = parsed.kind ?? 'connector' const name = parsed.name const noInstall = diff --git a/packages/unrag/cli/commands/skills.ts b/packages/unrag/cli/commands/skills.ts new file mode 100644 index 0000000..35a284c --- /dev/null +++ b/packages/unrag/cli/commands/skills.ts @@ -0,0 +1,213 @@ +import {copyFile, mkdir, readdir} from 'node:fs/promises' +import {homedir} from 'node:os' +import path from 'node:path' +import {fileURLToPath} from 'node:url' +import {cancel, isCancel, log, multiselect, outro} from '@clack/prompts' +import {exists, findUp} from '../lib/fs' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) + +// ANSI color codes +const GRAY = '\x1b[37m' +const DARK = '\x1b[90m' +const RESET = '\x1b[0m' +const B = GRAY +const D = DARK +const R = RESET + +const BANNER = ` +${B} ██${D}╗ ${B}██${D}╗${B}███${D}╗ ${B}██${D}╗${B}██████${D}╗ ${B}█████${D}╗ ${B}██████${D}╗${R} +${B} ██${D}║ ${B}██${D}║${B}████${D}╗ ${B}██${D}║${B}██${D}╔══${B}██${D}╗${B}██${D}╔══${B}██${D}╗${B}██${D}╔════╝${R} +${B} ██${D}║ ${B}██${D}║${B}██${D}╔${B}██${D}╗ ${B}██${D}║${B}██████${D}╔╝${B}███████${D}║${B}██${D}║ ${B}███${D}╗${R} +${B} ██${D}║ ${B}██${D}║${B}██${D}║╚${B}██${D}╗${B}██${D}║${B}██${D}╔══${B}██${D}╗${B}██${D}╔══${B}██${D}║${B}██${D}║ ${B}██${D}║${R} +${D} ╚${B}██████${D}╔╝${B}██${D}║ ╚${B}████${D}║${B}██${D}║ ${B}██${D}║${B}██${D}║ ${B}██${D}║╚${B}██████${D}╔╝${R} +${D} ╚═════╝ ╚═╝ ╚═══╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝${R} +${D} skills${R} +` + +type AgentId = 'claude-code' | 'cursor' | 'windsurf' | 'antigravity' | 'codex' + +interface AgentConfig { + id: AgentId + name: string + description: string + getInstallPath: (projectRoot: string) => string + isGlobal: boolean +} + +const AGENTS: AgentConfig[] = [ + { + id: 'claude-code', + name: 'Claude Code', + description: 'Anthropic Claude Code (.claude/skills/)', + getInstallPath: (projectRoot) => + path.join(projectRoot, '.claude', 'skills', 'unrag'), + isGlobal: false + }, + { + id: 'cursor', + name: 'Cursor', + description: 'Cursor IDE (.cursor/rules/)', + getInstallPath: (projectRoot) => + path.join(projectRoot, '.cursor', 'rules', 'unrag'), + isGlobal: false + }, + { + id: 'windsurf', + name: 'Windsurf', + description: 'Windsurf AI (.windsurf/rules/)', + getInstallPath: (projectRoot) => + path.join(projectRoot, '.windsurf', 'rules', 'unrag'), + isGlobal: false + }, + { + id: 'antigravity', + name: 'Antigravity', + description: 'Google Antigravity (.gemini/skills/)', + getInstallPath: (projectRoot) => + path.join(projectRoot, '.gemini', 'skills', 'unrag'), + isGlobal: false + }, + { + id: 'codex', + name: 'Codex', + description: 'OpenAI Codex CLI (~/.codex/prompts/)', + getInstallPath: () => + path.join(homedir(), '.codex', 'prompts', 'unrag'), + isGlobal: true + } +] + +async function copyDirectory(src: string, dest: string): Promise { + const copiedFiles: string[] = [] + + await mkdir(dest, {recursive: true}) + + const entries = await readdir(src, {withFileTypes: true}) + + for (const entry of entries) { + const srcPath = path.join(src, entry.name) + const destPath = path.join(dest, entry.name) + + if (entry.isDirectory()) { + const nested = await copyDirectory(srcPath, destPath) + copiedFiles.push(...nested) + } else { + await copyFile(srcPath, destPath) + copiedFiles.push(destPath) + } + } + + return copiedFiles +} + +export async function skillsCommand(args: string[]) { + const nonInteractive = args.includes('--yes') || args.includes('-y') + + // Print the banner + console.log(BANNER) + + // Find CLI package root to locate bundled skills + const cliPackageRoot = await findUp(__dirname, 'package.json') + if (!cliPackageRoot) { + outro('Could not locate CLI package root.') + process.exitCode = 1 + return + } + + // Skills are bundled in the package + const skillsSource = path.join(cliPackageRoot, 'skills', 'unrag') + if (!(await exists(skillsSource))) { + outro('Skills bundle not found in CLI package.') + process.exitCode = 1 + return + } + + // Find project root for project-local installations + const projectRoot = process.cwd() + + let selectedAgents: AgentConfig[] = [] + + if (nonInteractive) { + // Default to Claude Code in non-interactive mode + const claudeCode = AGENTS.find((a) => a.id === 'claude-code') + if (claudeCode) { + selectedAgents = [claudeCode] + } + } else { + const choices = await multiselect({ + message: + 'Which IDE/agents would you like to install the Unrag skill for?', + options: AGENTS.map((agent) => ({ + value: agent.id, + label: agent.name, + hint: agent.description + })), + required: true + }) + + if (isCancel(choices)) { + cancel('Cancelled.') + return + } + + selectedAgents = (choices as AgentId[]) + .map((id) => AGENTS.find((a) => a.id === id)) + .filter((a): a is AgentConfig => a !== undefined) + } + + if (selectedAgents.length === 0) { + outro('No agents selected.') + return + } + + const installed: string[] = [] + const skipped: string[] = [] + + for (const agent of selectedAgents) { + const installPath = agent.getInstallPath(projectRoot) + + // Check if already installed + if (await exists(installPath)) { + skipped.push(agent.name) + continue + } + + // Copy the skills + log.step(`Installing Unrag skills for ${agent.name}...`) + + try { + await copyDirectory(skillsSource, installPath) + installed.push(agent.name) + + if (agent.isGlobal) { + log.success(`Installed to ${installPath}`) + } else { + log.success( + `Installed to ${path.relative(projectRoot, installPath)}/` + ) + } + } catch (err) { + log.error( + `Failed to install for ${agent.name}: ${err instanceof Error ? err.message : String(err)}` + ) + } + } + + // Summary + const summary: string[] = [] + + if (installed.length > 0) { + summary.push(`✓ Installed for: ${installed.join(', ')}`) + } + if (skipped.length > 0) { + summary.push(`⊘ Already installed: ${skipped.join(', ')}`) + } + + if (summary.length > 0) { + outro( + `${summary.join('\n')}\n\nYour AI assistants now have access to Unrag documentation and patterns.` + ) + } +} diff --git a/packages/unrag/cli/run.ts b/packages/unrag/cli/run.ts index 37333a7..8dc861f 100644 --- a/packages/unrag/cli/run.ts +++ b/packages/unrag/cli/run.ts @@ -34,6 +34,7 @@ function renderHelp() { ' add Install a connector (notion, google-drive)', ' add extractor Install an extractor (pdf-llm, image-ocr, etc.)', ' add battery Install a battery module (reranker, eval, debug)', + ' add skills Install Unrag agent skills for your IDE/agent', ' upgrade Upgrade vendored sources (git-style merge)', ' doctor Validate installation and configuration', ' doctor setup Generate project-specific doctor config and scripts', diff --git a/packages/unrag/package.json b/packages/unrag/package.json index 882fe60..cba37a9 100644 --- a/packages/unrag/package.json +++ b/packages/unrag/package.json @@ -47,7 +47,7 @@ "unrag:doctor:db": "unrag doctor --config .unrag/doctor.json --db", "unrag:doctor:ci": "unrag doctor --config .unrag/doctor.json --db --strict --json" }, - "files": ["dist/**", "registry/**", "README.md"], + "files": ["dist/**", "registry/**", "skills/**", "README.md"], "publishConfig": { "access": "public" } diff --git a/skills/unrag/SKILL.md b/packages/unrag/skills/unrag/SKILL.md similarity index 99% rename from skills/unrag/SKILL.md rename to packages/unrag/skills/unrag/SKILL.md index e41e65e..63e3925 100644 --- a/skills/unrag/SKILL.md +++ b/packages/unrag/skills/unrag/SKILL.md @@ -1,7 +1,7 @@ --- name: unrag description: Covers RAG installation, ContextEngine API, embedding providers, store adapters, extractors, connectors, batteries, and CLI commands for the unrag TypeScript library. -version: 1.0.0 +version: 0.3.2 --- # Unrag Agent Skill diff --git a/skills/unrag/references/api-reference.md b/packages/unrag/skills/unrag/references/api-reference.md similarity index 100% rename from skills/unrag/references/api-reference.md rename to packages/unrag/skills/unrag/references/api-reference.md diff --git a/skills/unrag/references/batteries.md b/packages/unrag/skills/unrag/references/batteries.md similarity index 100% rename from skills/unrag/references/batteries.md rename to packages/unrag/skills/unrag/references/batteries.md diff --git a/skills/unrag/references/cli-commands.md b/packages/unrag/skills/unrag/references/cli-commands.md similarity index 100% rename from skills/unrag/references/cli-commands.md rename to packages/unrag/skills/unrag/references/cli-commands.md diff --git a/skills/unrag/references/connectors.md b/packages/unrag/skills/unrag/references/connectors.md similarity index 100% rename from skills/unrag/references/connectors.md rename to packages/unrag/skills/unrag/references/connectors.md diff --git a/skills/unrag/references/embedding-providers.md b/packages/unrag/skills/unrag/references/embedding-providers.md similarity index 100% rename from skills/unrag/references/embedding-providers.md rename to packages/unrag/skills/unrag/references/embedding-providers.md diff --git a/skills/unrag/references/extractors.md b/packages/unrag/skills/unrag/references/extractors.md similarity index 100% rename from skills/unrag/references/extractors.md rename to packages/unrag/skills/unrag/references/extractors.md diff --git a/skills/unrag/references/patterns.md b/packages/unrag/skills/unrag/references/patterns.md similarity index 100% rename from skills/unrag/references/patterns.md rename to packages/unrag/skills/unrag/references/patterns.md diff --git a/skills/unrag/references/store-adapters.md b/packages/unrag/skills/unrag/references/store-adapters.md similarity index 100% rename from skills/unrag/references/store-adapters.md rename to packages/unrag/skills/unrag/references/store-adapters.md diff --git a/skills/unrag/references/troubleshooting.md b/packages/unrag/skills/unrag/references/troubleshooting.md similarity index 100% rename from skills/unrag/references/troubleshooting.md rename to packages/unrag/skills/unrag/references/troubleshooting.md