
Context7
- 97 installs
- 6 repo stars
- Updated July 22, 2026
- julianobarbosa/claude-code-skills
Helps with ai & agent building tasks.
About
context7 is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- context7
- AI & Agent Building
- AI-coding skill
Context7 by the numbers
- 97 all-time installs (skills.sh)
- +1 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #4,520 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/julianobarbosa/claude-code-skills --skill context7Add your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 97 |
|---|---|
| repo stars | ★ 6 |
| Last updated | July 22, 2026 |
| Repository | julianobarbosa/claude-code-skills ↗ |
What it does
Helps with ai & agent building tasks.
Files
Context7
Query up-to-date, version-specific documentation and code examples directly from source libraries via Context7's documentation aggregation platform.
Why Context7
| Benefit | Description |
|---|---|
| Current APIs | No hallucinated or outdated patterns - documentation comes from actual sources |
| Version-Specific | Gets docs for exact library versions you're using |
| Code Examples | Real, working code extracted from actual documentation |
| Broad Coverage | 1000+ libraries including React, Next.js, Vue, Go, Python, Kubernetes, etc. |
Setup
# Install dependencies
cd ~/.claude/skills/context7/Tools
bun install
# Optional: link binaries globally so c7-lookup / c7-resolve / c7-query are on PATH
bun link
# Optional: API key for higher rate limits
export CONTEXT7_API_KEY="ctx7sk_your_key_here" # context7.com/dashboardAvailable CLI Tools
| Tool | Purpose | Short form (after bun link) | Long form |
|---|---|---|---|
lookup | Resolve + query in one shot | c7-lookup <library> <query> | bun src/cli/lookup.ts <library> <query> |
resolve | Find Context7 library ID | c7-resolve <library> [query] | bun src/cli/resolve.ts <library> [query] |
query | Query docs by known ID | c7-query <library_id> <query> | bun src/cli/query.ts <library_id> <query> |
CLI Flags
All three CLIs accept the same flag set.
| Flag | Effect |
|---|---|
--json | Emit JSON on stdout; suppress decorative output. Pipe to jq. |
--quiet, -q | Suppress info/success logs (warn/error still print to stderr). |
--no-cache | Skip the 24h disk cache (~/.cache/context7/resolved.json) for this call. |
--clear-cache | Wipe the disk cache and exit. |
--timeout <ms> | HTTP timeout in milliseconds. Default 30000. |
--max-retries <n> | Retry budget for 429 Rate Limit responses (with Retry-After). Default 1. |
--api-key <key> | Override CONTEXT7_API_KEY env var. |
--version, -V | Print version and exit. |
--help, -h | Print usage and exit. |
Quick Reference
Full Lookup (Recommended)
One command to resolve library and query documentation:
# After `bun link`:
c7-lookup react "useEffect cleanup function"
c7-lookup next.js "app router middleware"
c7-lookup kubernetes "deployment rolling update"
# Or without linking:
cd ~/.claude/skills/context7/Tools && bun src/cli/lookup.ts react "useEffect cleanup function"JSON output (for scripting / piping)
c7-lookup react "useState" --json | jq '.libraryId'
c7-resolve drizzle "many to many" --json | jq '.bestMatch'Step-by-Step (when needed)
c7-resolve react # → /facebook/react
c7-resolve next.js "authentication" # → ranked candidates
c7-query /facebook/react "useEffect cleanup" # → docs for known IDCommon Library IDs
| Library | Context7 ID | CLI Shortcut |
|---|---|---|
| React | /facebook/react | react |
| Next.js | /vercel/next.js | next.js, nextjs |
| Vue | /vuejs/vue | vue |
| Kubernetes | /kubernetes/kubernetes | kubernetes, k8s |
| Go stdlib | /golang/go | go, golang |
| Python | /python/cpython | python |
| Node.js | /nodejs/node | node, nodejs |
| TypeScript | /microsoft/typescript | typescript, ts |
| Prisma | /prisma/prisma | prisma |
| Tailwind | /tailwindlabs/tailwindcss | tailwind, tailwindcss |
Workflow Routing
| Workflow | Trigger | File |
|---|---|---|
| ResolveLibrary | "find library ID", "resolve library" | Workflows/ResolveLibrary.md |
| QueryDocs | "lookup docs", "get documentation", "code examples" | Workflows/QueryDocs.md |
| FullLookup | "help me with [library]", "how do I use [feature]" | Workflows/FullLookup.md |
Gotchas
These are real Context7 API behaviors that bite if you don't know about them. Add to this list whenever a query goes wrong in a way that wasn't already documented.
1. Library ID requires a leading slash. /facebook/react works; facebook/react fails with a cryptic "library not found." The error doesn't tell you the slash is missing. 2. The resolver is fuzzy and ranked, not deterministic. resolve.ts <name> returns multiple candidates ordered by an LLM-powered ranker that takes your optional context query into account. A narrow context can promote a less-canonical fork above the official repo. Always inspect the top match before passing the ID to query.ts. 3. Version pinning matters at major boundaries. Library IDs may include a version segment: /vercel/next.js/14.2.0. The bare ID resolves to whatever Context7 currently treats as "latest stable," which can lag behind real releases — asking about Next.js "app router" without a pin can return Next 13 docs. Pin the version when the framework changed shape across majors. 4. Public rate limits are tight. Without CONTEXT7_API_KEY, you'll hit limits in fewer queries than you expect. The "max 3 calls per question" tip in this skill is a defensive ceiling on Claude's behavior — it is not the real API budget. Set the API key from context7.com/dashboard for any sustained use. 5. Snippet truncation is silent. Long doc pages return excerpts, not full content. If a returned example references a function or symbol the snippet didn't define, do NOT guess what it does — re-query with a tighter, more specific question rather than fabricating the missing piece. 6. The known-IDs cache can go stale. Common libraries (react, next.js, kubernetes, etc.) shortcut to hardcoded IDs in Tools/src/lib/context7.ts and skip the resolver entirely. If an upstream project renames its repo or moves orgs, the cached ID becomes wrong and queries silently miss. To force a fresh resolve, use an alias the cache doesn't know or pass --no-cache.
Common Errors
The CLIs use typed exit codes so callers (Claude included) can distinguish failure categories without parsing strings.
| Exit | Kind | Cause | Recovery |
|---|---|---|---|
3 | auth | 401 — bad or missing CONTEXT7_API_KEY | Set the env var from context7.com/dashboard or pass --api-key <key>. |
4 | not_found | 404 — library or doc path missing; or malformed ID (missing leading /) | Run c7-resolve <name> to get a valid ID; check the /org/project shape. |
5 | rate_limit | 429 — public rate limit hit; auto-retries once if Retry-After is present | Set CONTEXT7_API_KEY for higher limits, or raise --max-retries. |
6 | server | 5xx — transient Context7 server error | Wait briefly and retry. |
7 | timeout | 408 — request aborted by client timeout | Raise --timeout (e.g. --timeout 60000) or check network. |
2 | (arg) | Unknown flag, bad value, missing required flag arg | Run --help. |
1 | (other) | Anything else, including 0-status network errors | Check stderr message; retry. |
Errors always print Error: <message> and a Hint: <remediation> line to stderr. With --json, stdout stays clean and parseable; stderr keeps the human-readable error.
Examples
Example 1: React Hooks Documentation
cd ~/.claude/skills/context7/Tools
bun src/cli/lookup.ts react "useEffect cleanup function"Output includes current React docs with cleanup pattern examples.
Example 2: Kubernetes Deployment Spec
bun src/cli/lookup.ts kubernetes "deployment spec rolling update strategy"Output includes current K8s API reference for Deployment.
Example 3: Next.js App Router
bun src/cli/lookup.ts next.js "middleware authentication app router"Output includes latest Next.js middleware documentation.
Example 4: Using in Claude Code Session
When you need documentation during a coding session:
User: "How do I implement server-side data fetching in Next.js 14?"
Claude runs:
cd ~/.claude/skills/context7/Tools && bun src/cli/lookup.ts next.js "server components data fetching"
Then synthesizes response with current patterns (Server Components, not old getServerSideProps)Environment Variables
| Variable | Description | Default |
|---|---|---|
CONTEXT7_API_KEY | API key for higher rate limits | None (uses public rate limits) |
Get your API key at context7.com/dashboard
Tips
- Be specific in your query for better results
- Max 3 calls per question - if you can't find it after 3 tries, use best available info
- Include version in query if you need specific version docs (e.g., "React 18 concurrent features")
- Combine with local context - use Context7 to verify APIs, then apply to your codebase
- Known IDs skip API - common libraries like
react,next.jsuse cached IDs to skip the resolve step
Project Structure
Tools/
├── package.json # Bun + bin entries (c7-resolve, c7-query, c7-lookup)
├── tsconfig.json # TypeScript strict config
├── src/
│ ├── index.ts # Public exports (client, types, helpers)
│ ├── lib/
│ │ ├── context7.ts # Core API client + retry + typed errors
│ │ ├── cache.ts # Disk cache for resolved library IDs (24h TTL)
│ │ ├── flags.ts # Shared CLI argument parser
│ │ └── errors.ts # Error → exit-code + hint formatter
│ └── cli/
│ ├── lookup.ts # Full lookup command (c7-lookup)
│ ├── resolve.ts # Library ID resolver (c7-resolve)
│ └── query.ts # Documentation query (c7-query)
└── tests/
├── cache.test.ts # Cache I/O + TTL roundtrips
├── flags.test.ts # Argument parsing edge cases
└── errors.test.ts # Exit code + hint mappingAPI Reference
The TypeScript client can also be imported programmatically:
import {
Context7Client,
getKnownLibraryId,
setLogLevel,
formatError,
getCached,
setCached,
} from "./src/index.js";
setLogLevel("warn"); // silence info/success logs
const client = new Context7Client({
apiKey: process.env.CONTEXT7_API_KEY,
timeout: 30_000,
maxRetries: 2,
});
// Full lookup with disk cache
const cached = await getCached("react", 24 * 60 * 60 * 1000);
if (cached) {
const docs = await client.queryDocs(cached, "useEffect cleanup");
console.log(docs.rawContent);
} else {
const result = await client.lookup("react", "useEffect hooks");
if (result.library) await setCached("react", result.library.id);
console.log(result.rawContent);
}node_modules/
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"name": "@pai/context7-tools",
"devDependencies": {
"@types/bun": "latest",
"typescript": "^5.7.0",
},
},
},
"packages": {
"@types/bun": ["@types/bun@1.3.5", "", { "dependencies": { "bun-types": "1.3.5" } }, "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w=="],
"@types/node": ["@types/node@25.0.8", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-powIePYMmC3ibL0UJ2i2s0WIbq6cg6UyVFQxSCpaPxxzAaziRfimGivjdF943sSGV6RADVbk0Nvlm5P/FB44Zg=="],
"bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
}
}
{
"name": "@pai/context7-tools",
"version": "1.2.0",
"description": "TypeScript tools for Context7 documentation lookup",
"type": "module",
"main": "src/index.ts",
"bin": {
"c7-resolve": "src/cli/resolve.ts",
"c7-query": "src/cli/query.ts",
"c7-lookup": "src/cli/lookup.ts"
},
"scripts": {
"resolve": "bun src/cli/resolve.ts",
"query": "bun src/cli/query.ts",
"lookup": "bun src/cli/lookup.ts",
"test": "bun test",
"typecheck": "bunx tsc --noEmit",
"prepublishOnly": "bun test"
},
"devDependencies": {
"@types/bun": "latest",
"typescript": "^5.7.0"
},
"engines": {
"bun": ">=1.0.0"
}
}
#!/usr/bin/env bun
/**
* c7-lookup — Full documentation lookup (resolve + query in one command).
*
* Usage:
* c7-lookup <library_name> <query> [flags]
* bun src/cli/lookup.ts <library_name> <query> [flags]
*
* Examples:
* c7-lookup react "useEffect cleanup function"
* c7-lookup next.js "app router middleware" --json
* c7-lookup kubernetes "deployment spec fields" --no-cache --timeout 60000
*/
import {
Context7Client,
COMMON_LIBRARIES,
getKnownLibraryId,
log,
setLogLevel,
} from "../lib/context7.js";
import { parseArgs, ArgParseError } from "../lib/flags.js";
import { formatError } from "../lib/errors.js";
import { getCached, setCached, clearCache } from "../lib/cache.js";
import pkg from "../../package.json" with { type: "json" };
const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
function printUsage(): void {
console.log(`
Usage: c7-lookup <library_name> <query> [flags]
Full documentation lookup — resolves library and queries docs in one command.
Arguments:
library_name Name of the library (e.g., "react", "next.js", "kubernetes")
query Natural language question about the library
Flags:
--json Emit JSON to stdout; suppress decorative output
--quiet, -q Suppress info/success logs (warn/error still on stderr)
--no-cache Bypass the resolve disk cache for this call
--clear-cache Remove the disk cache and exit
--timeout <ms> HTTP timeout in milliseconds (default 30000)
--max-retries <n> Max retries on 429 Rate Limit (default 1)
--api-key <key> Override CONTEXT7_API_KEY env var
--version, -V Print version and exit
--help, -h This help
Environment:
CONTEXT7_API_KEY Optional API key for higher rate limits (context7.com/dashboard)
Examples:
c7-lookup react "useEffect cleanup function"
c7-lookup next.js "app router middleware" --json | jq .libraryId
c7-lookup kubernetes "rolling update strategy" --timeout 60000
Known shortcuts (skip resolve, no API call):
${Object.keys(COMMON_LIBRARIES).map((name) => ` - ${name}`).join("\n")}
...plus 1000+ more libraries at context7.com
`);
}
async function main(): Promise<void> {
let parsed;
try {
parsed = parseArgs(process.argv.slice(2));
} catch (err) {
const f = formatError(err);
process.stderr.write(`Error: ${f.message}\n`);
if (f.hint) process.stderr.write(`Hint: ${f.hint}\n`);
process.exit(f.exitCode);
}
const { positional, flags } = parsed;
if (flags.help) { printUsage(); process.exit(0); }
if (flags.version) { console.log(pkg.version); process.exit(0); }
if (flags.clearCache) {
await clearCache();
if (!flags.quiet) log("success", "Cache cleared");
if (positional.length < 2) process.exit(0);
}
if (positional.length < 2) {
printUsage();
process.exit(1);
}
if (flags.quiet || flags.json) setLogLevel(flags.json ? "warn" : "silent");
const libraryName = positional[0]!;
const query = positional.slice(1).join(" ");
try {
const client = new Context7Client({
apiKey: flags.apiKey,
timeout: flags.timeoutMs,
maxRetries: flags.maxRetries,
});
let libraryId: string;
let libraryDisplayName = libraryName;
let source: "cache" | "known" | "resolved";
const cached = flags.noCache ? null : await getCached(libraryName, CACHE_TTL_MS);
const knownId = getKnownLibraryId(libraryName);
if (cached) {
libraryId = cached;
source = "cache";
log("info", `Cache hit for '${libraryName}': ${libraryId}`);
} else if (knownId) {
libraryId = knownId;
source = "known";
log("info", `Using known library ID for '${libraryName}': ${knownId}`);
} else {
log("info", `Resolving library: ${libraryName}`);
const searchResult = await client.resolveLibrary(libraryName, query);
if (!searchResult.bestMatch) {
log("error", `Library '${libraryName}' not found`);
if (!flags.json) {
console.log("\nTip: Try one of these known libraries:");
Object.keys(COMMON_LIBRARIES).slice(0, 10).forEach((name) => console.log(` - ${name}`));
}
process.exit(4);
}
libraryId = searchResult.bestMatch.id;
libraryDisplayName = searchResult.bestMatch.name || libraryName;
source = "resolved";
log("success", `Resolved to: ${libraryId}`);
if (!flags.noCache) await setCached(libraryName, libraryId);
}
const result = await client.queryDocs(libraryId, query);
if (flags.json) {
const out = {
library: libraryDisplayName,
libraryId,
query,
source,
rawContent: result.rawContent,
snippets: result.snippets,
};
process.stdout.write(JSON.stringify(out, null, 2) + "\n");
} else {
console.log("\n" + "=".repeat(80));
console.log(`📚 ${libraryDisplayName}`);
console.log(`🔗 ${libraryId}`);
console.log(`❓ ${query}`);
console.log("=".repeat(80) + "\n");
if (result.rawContent) {
console.log(result.rawContent);
} else if (result.snippets.length > 0) {
result.snippets.forEach((snippet, index) => {
console.log(`\n--- Snippet ${index + 1} ---`);
if (snippet.title) console.log(`📄 ${snippet.title}\n`);
console.log(snippet.content);
if (snippet.url) console.log(`\n🔗 ${snippet.url}`);
});
} else {
console.log("No documentation found for this query.");
console.log("\nSuggestions:");
console.log(" 1. Try more specific terms (e.g., 'useEffect cleanup' instead of 'effects')");
console.log(" 2. Try broader terms (e.g., 'hooks' instead of 'useCustomHook')");
console.log(" 3. Include version if relevant (e.g., 'React 18 concurrent features')");
}
console.log("\n" + "=".repeat(80));
log("success", "Documentation lookup complete");
}
} catch (err) {
const f = formatError(err);
process.stderr.write(`Error: ${f.message}\n`);
if (f.hint) process.stderr.write(`Hint: ${f.hint}\n`);
process.exit(f.exitCode);
}
}
main();
#!/usr/bin/env bun
/**
* c7-query — Query documentation for a known library ID.
*
* Usage:
* c7-query <library_id> <query> [flags]
* bun src/cli/query.ts <library_id> <query> [flags]
*
* Examples:
* c7-query /facebook/react "useEffect cleanup function"
* c7-query /vercel/next.js "app router middleware" --json
* c7-query /kubernetes/kubernetes "deployment spec" --quiet
*/
import { Context7Client, COMMON_LIBRARIES, log, setLogLevel } from "../lib/context7.js";
import { parseArgs } from "../lib/flags.js";
import { formatError } from "../lib/errors.js";
import pkg from "../../package.json" with { type: "json" };
function printUsage(): void {
console.log(`
Usage: c7-query <library_id> <query> [flags]
Query up-to-date documentation from Context7 by library ID.
Arguments:
library_id Context7 library ID, must start with '/' (e.g., "/facebook/react")
query Natural language question about the library
Flags:
--json Emit JSON to stdout; suppress decorative output
--quiet, -q Suppress info/success logs
--timeout <ms> HTTP timeout (default 30000)
--max-retries <n> Max retries on 429 (default 1)
--api-key <key> Override CONTEXT7_API_KEY
--version, -V Print version and exit
--help, -h This help
Common library IDs:
${Object.entries(COMMON_LIBRARIES).slice(0, 8).map(([name, id]) => ` ${id.padEnd(25)} (${name})`).join("\n")}
Tip: Use 'c7-resolve <name>' to find unfamiliar library IDs.
`);
}
async function main(): Promise<void> {
let parsed;
try {
parsed = parseArgs(process.argv.slice(2));
} catch (err) {
const f = formatError(err);
process.stderr.write(`Error: ${f.message}\n`);
if (f.hint) process.stderr.write(`Hint: ${f.hint}\n`);
process.exit(f.exitCode);
}
const { positional, flags } = parsed;
if (flags.help) { printUsage(); process.exit(0); }
if (flags.version) { console.log(pkg.version); process.exit(0); }
if (positional.length < 2) {
printUsage();
process.exit(1);
}
if (flags.quiet || flags.json) setLogLevel(flags.json ? "warn" : "silent");
const libraryId = positional[0]!;
const query = positional.slice(1).join(" ");
if (!libraryId.startsWith("/")) {
process.stderr.write(`Error: Invalid library ID format: ${libraryId}\n`);
process.stderr.write(`Hint: Library ID must start with '/' (e.g., '/facebook/react'). Run c7-resolve first.\n`);
process.exit(4);
}
try {
const client = new Context7Client({
apiKey: flags.apiKey,
timeout: flags.timeoutMs,
maxRetries: flags.maxRetries,
});
const result = await client.queryDocs(libraryId, query);
if (flags.json) {
process.stdout.write(
JSON.stringify({ libraryId: result.libraryId, query: result.query, rawContent: result.rawContent }, null, 2) + "\n"
);
} else {
console.log("\n" + "=".repeat(80));
console.log(`Library: ${result.libraryId}`);
console.log(`Query: ${result.query}`);
console.log("=".repeat(80) + "\n");
if (result.rawContent) {
console.log(result.rawContent);
} else if (result.snippets.length > 0) {
result.snippets.forEach((snippet, index) => {
console.log(`--- Snippet ${index + 1} ---`);
if (snippet.title) console.log(`Title: ${snippet.title}`);
console.log(snippet.content);
if (snippet.url) console.log(`Source: ${snippet.url}`);
console.log();
});
} else {
console.log("No documentation found for this query.");
console.log("\nTips:");
console.log(" - Try a more specific query");
console.log(" - Try broader terms");
console.log(" - Check if the library ID is correct (run c7-resolve)");
}
console.log("\n" + "=".repeat(80));
}
} catch (err) {
const f = formatError(err);
process.stderr.write(`Error: ${f.message}\n`);
if (f.hint) process.stderr.write(`Hint: ${f.hint}\n`);
process.exit(f.exitCode);
}
}
main();
#!/usr/bin/env bun
/**
* c7-resolve — Resolve library name to Context7 library ID.
*
* Usage:
* c7-resolve <library_name> [query] [flags]
* bun src/cli/resolve.ts <library_name> [query] [flags]
*
* Examples:
* c7-resolve react
* c7-resolve next.js "app router authentication"
* c7-resolve kubernetes --json
*/
import {
Context7Client,
COMMON_LIBRARIES,
getKnownLibraryId,
log,
setLogLevel,
} from "../lib/context7.js";
import { parseArgs } from "../lib/flags.js";
import { formatError } from "../lib/errors.js";
import { getCached, setCached, clearCache } from "../lib/cache.js";
import pkg from "../../package.json" with { type: "json" };
const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
function printUsage(): void {
console.log(`
Usage: c7-resolve <library_name> [query] [flags]
Resolve a library name to a Context7-compatible library ID.
Arguments:
library_name Name of the library (e.g., "react", "next.js", "kubernetes")
query Optional context query for LLM-powered ranking
Flags:
--json Emit JSON to stdout; suppress decorative output
--quiet, -q Suppress info/success logs
--no-cache Bypass the disk cache for this call
--clear-cache Remove the disk cache and exit
--timeout <ms> HTTP timeout (default 30000)
--max-retries <n> Max retries on 429 (default 1)
--api-key <key> Override CONTEXT7_API_KEY
--version, -V Print version and exit
--help, -h This help
Common shortcuts (skip API call):
${Object.entries(COMMON_LIBRARIES).slice(0, 10).map(([n, id]) => ` ${n.padEnd(15)} -> ${id}`).join("\n")}
...and more
Examples:
c7-resolve react
c7-resolve next.js "server components"
c7-resolve unknown-lib --json
`);
}
async function main(): Promise<void> {
let parsed;
try {
parsed = parseArgs(process.argv.slice(2));
} catch (err) {
const f = formatError(err);
process.stderr.write(`Error: ${f.message}\n`);
if (f.hint) process.stderr.write(`Hint: ${f.hint}\n`);
process.exit(f.exitCode);
}
const { positional, flags } = parsed;
if (flags.help) { printUsage(); process.exit(0); }
if (flags.version) { console.log(pkg.version); process.exit(0); }
if (flags.clearCache) {
await clearCache();
if (!flags.quiet) log("success", "Cache cleared");
if (positional.length === 0) process.exit(0);
}
if (positional.length === 0) {
printUsage();
process.exit(1);
}
if (flags.quiet || flags.json) setLogLevel(flags.json ? "warn" : "silent");
const libraryName = positional[0]!;
const query = positional[1];
try {
// Cache and known-IDs short-circuit only when there's no extra query context.
if (!query) {
const cached = flags.noCache ? null : await getCached(libraryName, CACHE_TTL_MS);
if (cached) {
log("info", `Cache hit for '${libraryName}'`);
if (flags.json) {
process.stdout.write(JSON.stringify({ libraryName, bestMatch: { id: cached }, source: "cache" }, null, 2) + "\n");
} else {
console.log(`\nLibrary ID: ${cached}`);
console.log(`\nTip: c7-query "${cached}" "your query"`);
}
return;
}
const knownId = getKnownLibraryId(libraryName);
if (knownId) {
log("info", `Using known library ID for '${libraryName}'`);
if (flags.json) {
process.stdout.write(JSON.stringify({ libraryName, bestMatch: { id: knownId }, source: "known" }, null, 2) + "\n");
} else {
console.log(`\nLibrary ID: ${knownId}`);
console.log(`\nTip: c7-query "${knownId}" "your query"`);
}
return;
}
}
const client = new Context7Client({
apiKey: flags.apiKey,
timeout: flags.timeoutMs,
maxRetries: flags.maxRetries,
});
const result = await client.resolveLibrary(libraryName, query);
if (!result.bestMatch) {
if (flags.json) {
process.stdout.write(JSON.stringify({ libraryName, libraries: [], bestMatch: null, source: "resolved" }, null, 2) + "\n");
} else {
console.log("\nNo libraries found.");
}
process.exit(4);
}
if (!flags.noCache) await setCached(libraryName, result.bestMatch.id);
if (flags.json) {
process.stdout.write(
JSON.stringify(
{ libraryName, libraries: result.libraries, bestMatch: result.bestMatch, source: "resolved" },
null,
2
) + "\n"
);
} else {
console.log("\n--- Results ---\n");
result.libraries.slice(0, 5).forEach((lib, index) => {
console.log(`[${index + 1}] ${lib.id}`);
console.log(` Name: ${lib.name || "N/A"}`);
if (lib.description) {
console.log(` Description: ${lib.description.slice(0, 100)}${lib.description.length > 100 ? "..." : ""}`);
}
console.log();
});
console.log(`Best match: ${result.bestMatch.id}`);
console.log(`\nTip: c7-query "${result.bestMatch.id}" "your query"`);
}
} catch (err) {
const f = formatError(err);
process.stderr.write(`Error: ${f.message}\n`);
if (f.hint) process.stderr.write(`Hint: ${f.hint}\n`);
process.exit(f.exitCode);
}
}
main();
/**
* Context7 Tools - TypeScript client for documentation lookup
*
* @packageDocumentation
*/
export {
Context7Client,
Context7Error,
type Context7Options,
type Context7ErrorKind,
type LibraryInfo,
type SearchResult,
type DocSnippet,
type QueryResult,
type LogLevel,
getClient,
getKnownLibraryId,
COMMON_LIBRARIES,
log,
setLogLevel,
getLogLevel,
} from "./lib/context7.js";
export {
parseArgs,
ArgParseError,
type ParsedArgs,
type ParsedFlags,
} from "./lib/flags.js";
export {
formatError,
type FormattedError,
} from "./lib/errors.js";
export {
getCachePath,
readCache,
writeCache,
getCached,
setCached,
clearCache,
type CachedResolve,
} from "./lib/cache.js";
/**
* Disk cache for resolved library IDs.
*
* Maps `libraryName` (lowercased) → `{ name, id, ts }`. TTL is checked at read time;
* stale entries return null but remain on disk until overwritten by a fresh resolve
* (cheap; deferred eviction). Corrupt JSON is treated as empty — the cache is
* opportunistic, not authoritative.
*/
import { homedir } from "node:os";
import { join, dirname } from "node:path";
import { existsSync, mkdirSync, unlinkSync } from "node:fs";
export interface CachedResolve {
name: string;
id: string;
ts: number;
}
export function getCachePath(): string {
const xdg = process.env.XDG_CACHE_HOME;
const root = xdg && xdg.trim() ? xdg : join(homedir(), ".cache");
return join(root, "context7", "resolved.json");
}
function ensureCacheDir(path: string): void {
const dir = dirname(path);
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
}
export async function readCache(path: string = getCachePath()): Promise<Record<string, CachedResolve>> {
const file = Bun.file(path);
if (!(await file.exists())) return {};
try {
const data = await file.json();
if (data && typeof data === "object" && !Array.isArray(data)) {
return data as Record<string, CachedResolve>;
}
return {};
} catch {
return {};
}
}
export async function writeCache(
entries: Record<string, CachedResolve>,
path: string = getCachePath()
): Promise<void> {
ensureCacheDir(path);
await Bun.write(path, JSON.stringify(entries, null, 2));
}
export async function getCached(
name: string,
ttlMs: number,
path: string = getCachePath()
): Promise<string | null> {
const cache = await readCache(path);
const key = name.toLowerCase().trim();
const entry = cache[key];
if (!entry) return null;
if (Date.now() - entry.ts > ttlMs) return null;
return entry.id;
}
export async function setCached(
name: string,
id: string,
path: string = getCachePath()
): Promise<void> {
const cache = await readCache(path);
const key = name.toLowerCase().trim();
cache[key] = { name, id, ts: Date.now() };
await writeCache(cache, path);
}
export async function clearCache(path: string = getCachePath()): Promise<void> {
if (existsSync(path)) unlinkSync(path);
}
/**
* Context7 API Client
*
* TypeScript client for Context7 documentation lookup API.
* Provides up-to-date, version-specific documentation for libraries.
*
* @see https://context7.com/docs/api-guide
*/
const BASE_URL = "https://context7.com/api/v2";
export interface LibraryInfo {
id: string;
name: string;
description?: string;
codeSnippets?: number;
reputation?: string;
benchmarkScore?: number;
}
export interface SearchResult {
libraries: LibraryInfo[];
bestMatch: LibraryInfo | null;
}
export interface DocSnippet {
title?: string;
content: string;
source?: string;
url?: string;
}
export interface QueryResult {
libraryId: string;
query: string;
snippets: DocSnippet[];
rawContent: string;
}
export interface Context7Options {
apiKey?: string;
timeout?: number;
/**
* Max number of automatic retries on 429 Rate Limit responses
* that include a `Retry-After` header. Default 1 (one retry, then fail).
* Set to 0 to disable.
*/
maxRetries?: number;
}
export type Context7ErrorKind =
| "auth"
| "not_found"
| "rate_limit"
| "server"
| "timeout"
| "other";
function kindFromStatus(status: number): Context7ErrorKind {
if (status === 401 || status === 403) return "auth";
if (status === 404) return "not_found";
if (status === 429) return "rate_limit";
if (status === 408) return "timeout";
if (status >= 500 && status < 600) return "server";
return "other";
}
function parseRetryAfter(headerValue: string | null): number | undefined {
if (!headerValue) return undefined;
const secs = Number(headerValue);
if (Number.isFinite(secs) && secs >= 0) return secs;
// HTTP-date form: convert to delta-seconds
const ts = Date.parse(headerValue);
if (Number.isFinite(ts)) {
const delta = Math.max(0, Math.ceil((ts - Date.now()) / 1000));
return delta;
}
return undefined;
}
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
* Color codes for terminal output
*/
const Colors = {
reset: "\x1b[0m",
red: "\x1b[31m",
green: "\x1b[32m",
yellow: "\x1b[33m",
blue: "\x1b[34m",
cyan: "\x1b[36m",
dim: "\x1b[2m",
} as const;
export type LogLevel = "silent" | "warn" | "info";
let currentLogLevel: LogLevel = "info";
export function setLogLevel(level: LogLevel): void {
currentLogLevel = level;
}
export function getLogLevel(): LogLevel {
return currentLogLevel;
}
export function log(level: "info" | "success" | "warn" | "error", message: string): void {
// error always passes; warn passes unless silent; info/success only at level=info
if (currentLogLevel === "silent" && level !== "error") return;
if (currentLogLevel === "warn" && (level === "info" || level === "success")) return;
const prefix = {
info: `${Colors.blue}[INFO]${Colors.reset}`,
success: `${Colors.green}[OK]${Colors.reset}`,
warn: `${Colors.yellow}[WARN]${Colors.reset}`,
error: `${Colors.red}[ERROR]${Colors.reset}`,
};
console.error(`${prefix[level]} ${message}`);
}
/**
* Context7 API Client class
*/
export class Context7Client {
private apiKey?: string;
private timeout: number;
private maxRetries: number;
constructor(options: Context7Options = {}) {
this.apiKey = options.apiKey || process.env.CONTEXT7_API_KEY;
this.timeout = options.timeout || 30000;
this.maxRetries = options.maxRetries ?? 1;
}
private buildHeaders(accept: string): Record<string, string> {
const headers: Record<string, string> = { Accept: accept };
if (this.apiKey) headers["Authorization"] = `Bearer ${this.apiKey}`;
return headers;
}
private async doRequest(
url: string,
accept: string
): Promise<Response> {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
try {
const response = await fetch(url, {
method: "GET",
headers: this.buildHeaders(accept),
signal: controller.signal,
});
clearTimeout(timeoutId);
return response;
} catch (error) {
clearTimeout(timeoutId);
if (error instanceof Error && error.name === "AbortError") {
throw new Context7Error("Request timed out", 408, undefined, "timeout");
}
throw new Context7Error(`Network error: ${error}`, 0, undefined, "other");
}
}
/**
* Drive doRequest with rate-limit retry. On 429 with Retry-After, sleeps
* once and retries up to `maxRetries` times. Other non-2xx statuses surface
* immediately as Context7Error with a typed `kind`.
*/
private async doRequestWithRetry(url: string, accept: string): Promise<Response> {
let attempt = 0;
let lastRateLimitError: Context7Error | null = null;
while (true) {
const response = await this.doRequest(url, accept);
if (response.ok) return response;
const errorBody = await response.text().catch(() => "");
const kind = kindFromStatus(response.status);
const retryAfter = parseRetryAfter(response.headers.get("Retry-After"));
const err = new Context7Error(
`API request failed: ${response.status} ${response.statusText}`,
response.status,
errorBody,
kind,
retryAfter
);
if (kind === "rate_limit" && attempt < this.maxRetries && retryAfter !== undefined) {
lastRateLimitError = err;
log("warn", `Rate limited; retrying after ${retryAfter}s (attempt ${attempt + 1}/${this.maxRetries})`);
await sleep(retryAfter * 1000);
attempt++;
continue;
}
// Exhausted retries or non-retryable status — throw the latest error.
throw lastRateLimitError ?? err;
}
}
private async fetch<T>(endpoint: string, params: Record<string, string>): Promise<T> {
const url = new URL(`${BASE_URL}${endpoint}`);
Object.entries(params).forEach(([key, value]) => {
if (value) url.searchParams.set(key, value);
});
const response = await this.doRequestWithRetry(url.toString(), "application/json");
return (await response.json()) as T;
}
/**
* Fetch text/markdown content from API (for docs endpoint)
*/
private async fetchText(endpoint: string, params: Record<string, string>): Promise<string> {
const url = new URL(`${BASE_URL}${endpoint}`);
Object.entries(params).forEach(([key, value]) => {
if (value) url.searchParams.set(key, value);
});
const response = await this.doRequestWithRetry(url.toString(), "text/plain, text/markdown, */*");
return await response.text();
}
/**
* Resolve a library name to Context7 library ID
*/
async resolveLibrary(libraryName: string, query?: string): Promise<SearchResult> {
log("info", `Resolving library: ${libraryName}`);
if (!this.apiKey) {
log("warn", "No CONTEXT7_API_KEY set - using unauthenticated request (lower rate limits)");
}
const params: Record<string, string> = { libraryName };
if (query) {
params.query = query;
log("info", `Using query context: ${query}`);
}
const data = await this.fetch<LibraryInfo[] | { results: LibraryInfo[] }>("/libs/search", params);
// Handle different response formats
const libraries = Array.isArray(data) ? data : data.results || [];
if (libraries.length === 0) {
log("warn", `No libraries found matching '${libraryName}'`);
return { libraries: [], bestMatch: null };
}
log("success", `Found ${libraries.length} matching libraries`);
return {
libraries,
bestMatch: libraries[0] || null,
};
}
/**
* Query documentation for a specific library
* Note: The Context7 API returns plain markdown text, not JSON
*/
async queryDocs(libraryId: string, query: string): Promise<QueryResult> {
log("info", `Querying docs for: ${libraryId}`);
log("info", `Query: ${query}`);
if (!this.apiKey) {
log("warn", "No CONTEXT7_API_KEY set - using unauthenticated request (lower rate limits)");
}
const rawContent = await this.fetchText("/context", { libraryId, query });
if (!rawContent) {
log("warn", "No documentation found for this query");
} else {
log("success", `Retrieved documentation (${rawContent.length} chars)`);
}
return {
libraryId,
query,
snippets: [],
rawContent,
};
}
/**
* Full lookup: resolve library and query docs in one call
*/
async lookup(libraryName: string, query: string): Promise<QueryResult & { library: LibraryInfo | null }> {
const searchResult = await this.resolveLibrary(libraryName, query);
if (!searchResult.bestMatch) {
throw new Context7Error(`Library '${libraryName}' not found`, 404, undefined, "not_found");
}
const libraryId = searchResult.bestMatch.id;
const docsResult = await this.queryDocs(libraryId, query);
return {
...docsResult,
library: searchResult.bestMatch,
};
}
}
/**
* Custom error class for Context7 API errors.
*
* `kind` is the user-facing category (mapped from HTTP status). `retryAfter` is
* populated only on 429 responses that include a parseable `Retry-After` header.
*/
export class Context7Error extends Error {
constructor(
message: string,
public statusCode: number,
public body?: string,
public kind: Context7ErrorKind = "other",
public retryAfter?: number
) {
super(message);
this.name = "Context7Error";
}
}
/**
* Common library ID mappings for quick reference
*/
export const COMMON_LIBRARIES: Record<string, string> = {
react: "/facebook/react",
"next.js": "/vercel/next.js",
nextjs: "/vercel/next.js",
vue: "/vuejs/vue",
kubernetes: "/kubernetes/kubernetes",
k8s: "/kubernetes/kubernetes",
go: "/golang/go",
golang: "/golang/go",
python: "/python/cpython",
node: "/nodejs/node",
nodejs: "/nodejs/node",
typescript: "/microsoft/typescript",
ts: "/microsoft/typescript",
angular: "/angular/angular",
svelte: "/sveltejs/svelte",
express: "/expressjs/express",
fastify: "/fastify/fastify",
nest: "/nestjs/nest",
nestjs: "/nestjs/nest",
prisma: "/prisma/prisma",
drizzle: "/drizzle-team/drizzle-orm",
tailwind: "/tailwindlabs/tailwindcss",
tailwindcss: "/tailwindlabs/tailwindcss",
};
/**
* Try to get a known library ID without API call
*/
export function getKnownLibraryId(libraryName: string): string | null {
const normalized = libraryName.toLowerCase().trim();
return COMMON_LIBRARIES[normalized] || null;
}
// Default client instance
let defaultClient: Context7Client | null = null;
export function getClient(options?: Context7Options): Context7Client {
if (!defaultClient || options) {
defaultClient = new Context7Client(options);
}
return defaultClient;
}
/**
* Translate Context7Error kinds into structured CLI exit info.
*
* Exit code map (3..7 for typed Context7 errors, 1 for generic):
* 3 auth — 401, bad/missing API key
* 4 not_found — 404, library or doc path missing
* 5 rate_limit — 429, with optional retryAfter seconds
* 6 server — 5xx, transient server error
* 7 timeout — 408, request aborted by client timeout
* 1 other — unrecognized
*/
import { Context7Error } from "./context7.js";
import { ArgParseError } from "./flags.js";
export interface FormattedError {
exitCode: number;
message: string;
hint?: string;
retryAfter?: number;
}
export function formatError(err: unknown): FormattedError {
if (err instanceof ArgParseError) {
return { exitCode: err.exitCode, message: err.message };
}
if (err instanceof Context7Error) {
switch (err.kind) {
case "auth":
return {
exitCode: 3,
message: err.message,
hint: "Set CONTEXT7_API_KEY from context7.com/dashboard or pass --api-key",
};
case "not_found":
return {
exitCode: 4,
message: err.message,
hint: "Run resolve first or check the library ID format (/org/project)",
};
case "rate_limit":
return {
exitCode: 5,
message: err.message,
hint: err.retryAfter
? `Rate limited; retry in ${err.retryAfter}s or set CONTEXT7_API_KEY`
: "Rate limited; retry shortly or set CONTEXT7_API_KEY",
retryAfter: err.retryAfter,
};
case "server":
return {
exitCode: 6,
message: err.message,
hint: "Context7 server error — try again in a moment",
};
case "timeout":
return {
exitCode: 7,
message: err.message,
hint: "Request timed out; raise --timeout or check network",
};
default:
return { exitCode: 1, message: err.message };
}
}
if (err instanceof Error) {
return { exitCode: 1, message: err.message };
}
return { exitCode: 1, message: String(err) };
}
/**
* Shared CLI argument parser for c7-lookup, c7-resolve, c7-query.
*
* Throws `ArgParseError` on bad input so callers (and tests) can map to exit codes
* without process.exit() hidden inside the parser.
*/
export interface ParsedFlags {
help: boolean;
version: boolean;
json: boolean;
quiet: boolean;
noCache: boolean;
clearCache: boolean;
timeoutMs?: number;
maxRetries?: number;
apiKey?: string;
}
export interface ParsedArgs {
positional: string[];
flags: ParsedFlags;
}
export class ArgParseError extends Error {
constructor(message: string, public exitCode: number = 2) {
super(message);
this.name = "ArgParseError";
}
}
const KNOWN_FLAGS = new Set<string>([
"--help", "-h",
"--version", "-V",
"--json",
"--quiet", "-q",
"--no-cache",
"--clear-cache",
"--timeout",
"--max-retries",
"--api-key",
]);
function takeValue(argv: string[], i: number, flag: string): string {
const v = argv[i + 1];
if (v === undefined || v.startsWith("-")) {
throw new ArgParseError(`${flag} requires a value`);
}
return v;
}
function parsePositiveNumber(raw: string, flag: string): number {
const n = Number(raw);
if (!Number.isFinite(n) || n <= 0) {
throw new ArgParseError(`${flag} must be a positive number, got: ${raw}`);
}
return n;
}
function parseNonNegativeInt(raw: string, flag: string): number {
const n = Number(raw);
if (!Number.isInteger(n) || n < 0) {
throw new ArgParseError(`${flag} must be a non-negative integer, got: ${raw}`);
}
return n;
}
export function parseArgs(argv: string[]): ParsedArgs {
const positional: string[] = [];
const flags: ParsedFlags = {
help: false,
version: false,
json: false,
quiet: false,
noCache: false,
clearCache: false,
};
let i = 0;
while (i < argv.length) {
const arg = argv[i];
if (arg === undefined) { i++; continue; }
const looksLikeFlag = arg.startsWith("--") || (arg.startsWith("-") && arg.length === 2);
if (looksLikeFlag) {
if (!KNOWN_FLAGS.has(arg)) {
throw new ArgParseError(`Unknown flag: ${arg}`);
}
switch (arg) {
case "--help":
case "-h":
flags.help = true; break;
case "--version":
case "-V":
flags.version = true; break;
case "--json":
flags.json = true; break;
case "--quiet":
case "-q":
flags.quiet = true; break;
case "--no-cache":
flags.noCache = true; break;
case "--clear-cache":
flags.clearCache = true; break;
case "--timeout":
flags.timeoutMs = parsePositiveNumber(takeValue(argv, i, "--timeout"), "--timeout");
i++;
break;
case "--max-retries":
flags.maxRetries = parseNonNegativeInt(takeValue(argv, i, "--max-retries"), "--max-retries");
i++;
break;
case "--api-key":
flags.apiKey = takeValue(argv, i, "--api-key");
i++;
break;
}
} else {
positional.push(arg);
}
i++;
}
return { positional, flags };
}
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
import { existsSync, unlinkSync, mkdirSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import {
readCache,
writeCache,
getCached,
setCached,
clearCache,
getCachePath,
} from "../src/lib/cache.js";
const tmpPath = join(tmpdir(), `context7-cache-test-${process.pid}.json`);
beforeEach(() => {
if (existsSync(tmpPath)) unlinkSync(tmpPath);
});
afterEach(() => {
if (existsSync(tmpPath)) unlinkSync(tmpPath);
});
describe("cache", () => {
test("readCache returns empty object when file missing", async () => {
const entries = await readCache(tmpPath);
expect(entries).toEqual({});
});
test("setCached then getCached roundtrip returns id", async () => {
await setCached("React", "/facebook/react", tmpPath);
const id = await getCached("react", 60_000, tmpPath);
expect(id).toBe("/facebook/react");
});
test("name normalization is case-insensitive and trim-aware", async () => {
await setCached("React", "/facebook/react", tmpPath);
expect(await getCached("REACT", 60_000, tmpPath)).toBe("/facebook/react");
expect(await getCached(" react ", 60_000, tmpPath)).toBe("/facebook/react");
});
test("getCached returns null when TTL exceeded", async () => {
await setCached("foo", "/bar/baz", tmpPath);
const entries = await readCache(tmpPath);
entries["foo"]!.ts = Date.now() - 10_000;
await writeCache(entries, tmpPath);
const fresh = await getCached("foo", 1_000_000, tmpPath);
expect(fresh).toBe("/bar/baz");
const stale = await getCached("foo", 1_000, tmpPath);
expect(stale).toBeNull();
});
test("clearCache removes the file", async () => {
await setCached("x", "/y/z", tmpPath);
expect(existsSync(tmpPath)).toBe(true);
await clearCache(tmpPath);
expect(existsSync(tmpPath)).toBe(false);
});
test("corrupt cache file is treated as empty", async () => {
mkdirSync(join(tmpPath, ".."), { recursive: true });
await Bun.write(tmpPath, "{ this is not json");
const entries = await readCache(tmpPath);
expect(entries).toEqual({});
});
test("getCachePath honors XDG_CACHE_HOME", () => {
const old = process.env.XDG_CACHE_HOME;
process.env.XDG_CACHE_HOME = "/tmp/xdg-test";
try {
expect(getCachePath()).toBe("/tmp/xdg-test/context7/resolved.json");
} finally {
if (old === undefined) delete process.env.XDG_CACHE_HOME;
else process.env.XDG_CACHE_HOME = old;
}
});
});
import { describe, test, expect } from "bun:test";
import { Context7Error } from "../src/lib/context7.js";
import { ArgParseError } from "../src/lib/flags.js";
import { formatError } from "../src/lib/errors.js";
describe("formatError", () => {
test("auth → exit 3 with API key hint", () => {
const err = new Context7Error("Unauthorized", 401, undefined, "auth");
const f = formatError(err);
expect(f.exitCode).toBe(3);
expect(f.hint).toContain("CONTEXT7_API_KEY");
});
test("not_found → exit 4 with resolve hint", () => {
const err = new Context7Error("Not found", 404, undefined, "not_found");
const f = formatError(err);
expect(f.exitCode).toBe(4);
expect(f.hint).toContain("/org/project");
});
test("rate_limit with retryAfter → exit 5, hint mentions seconds", () => {
const err = new Context7Error("429", 429, undefined, "rate_limit", 30);
const f = formatError(err);
expect(f.exitCode).toBe(5);
expect(f.retryAfter).toBe(30);
expect(f.hint).toContain("30s");
});
test("rate_limit without retryAfter → exit 5, generic hint", () => {
const err = new Context7Error("429", 429, undefined, "rate_limit");
const f = formatError(err);
expect(f.exitCode).toBe(5);
expect(f.retryAfter).toBeUndefined();
expect(f.hint).toContain("shortly");
});
test("server → exit 6", () => {
const err = new Context7Error("Internal Server Error", 500, undefined, "server");
const f = formatError(err);
expect(f.exitCode).toBe(6);
expect(f.hint).toContain("server error");
});
test("timeout → exit 7", () => {
const err = new Context7Error("Request timed out", 408, undefined, "timeout");
const f = formatError(err);
expect(f.exitCode).toBe(7);
expect(f.hint).toContain("--timeout");
});
test("other Context7Error → exit 1, no hint", () => {
const err = new Context7Error("Weird error", 418, undefined, "other");
const f = formatError(err);
expect(f.exitCode).toBe(1);
expect(f.hint).toBeUndefined();
});
test("ArgParseError → propagates its own exit code", () => {
const err = new ArgParseError("Unknown flag: --bogus");
const f = formatError(err);
expect(f.exitCode).toBe(2);
expect(f.message).toContain("--bogus");
});
test("plain Error → exit 1, just the message", () => {
const f = formatError(new Error("boom"));
expect(f.exitCode).toBe(1);
expect(f.message).toBe("boom");
});
test("non-Error throwables → exit 1, stringified", () => {
const f = formatError("a string was thrown");
expect(f.exitCode).toBe(1);
expect(f.message).toBe("a string was thrown");
});
});
import { describe, test, expect } from "bun:test";
import { parseArgs, ArgParseError } from "../src/lib/flags.js";
describe("parseArgs", () => {
test("positional only", () => {
const r = parseArgs(["react", "useEffect cleanup"]);
expect(r.positional).toEqual(["react", "useEffect cleanup"]);
expect(r.flags.json).toBe(false);
expect(r.flags.help).toBe(false);
});
test("interleaved flag and positional", () => {
const a = parseArgs(["react", "--json", "useEffect"]);
const b = parseArgs(["--json", "react", "useEffect"]);
const c = parseArgs(["react", "useEffect", "--json"]);
expect(a.flags.json).toBe(true);
expect(b.flags.json).toBe(true);
expect(c.flags.json).toBe(true);
expect(a.positional).toEqual(["react", "useEffect"]);
expect(b.positional).toEqual(["react", "useEffect"]);
expect(c.positional).toEqual(["react", "useEffect"]);
});
test("--timeout parses a positive number", () => {
const r = parseArgs(["--timeout", "10000", "react"]);
expect(r.flags.timeoutMs).toBe(10000);
expect(r.positional).toEqual(["react"]);
});
test("--timeout rejects non-positive", () => {
expect(() => parseArgs(["--timeout", "0"])).toThrow(ArgParseError);
expect(() => parseArgs(["--timeout", "-5"])).toThrow(ArgParseError);
expect(() => parseArgs(["--timeout", "abc"])).toThrow(ArgParseError);
});
test("--max-retries parses non-negative integer", () => {
expect(parseArgs(["--max-retries", "0"]).flags.maxRetries).toBe(0);
expect(parseArgs(["--max-retries", "3"]).flags.maxRetries).toBe(3);
expect(() => parseArgs(["--max-retries", "1.5"])).toThrow(ArgParseError);
expect(() => parseArgs(["--max-retries", "-1"])).toThrow(ArgParseError);
});
test("--api-key takes a value", () => {
const r = parseArgs(["--api-key", "ctx7sk_xxx", "react"]);
expect(r.flags.apiKey).toBe("ctx7sk_xxx");
expect(r.positional).toEqual(["react"]);
});
test("flag value missing throws", () => {
expect(() => parseArgs(["--timeout"])).toThrow(ArgParseError);
expect(() => parseArgs(["--api-key"])).toThrow(ArgParseError);
});
test("unknown flag throws with exit code 2", () => {
try {
parseArgs(["--bogus"]);
throw new Error("should have thrown");
} catch (e) {
expect(e).toBeInstanceOf(ArgParseError);
expect((e as ArgParseError).exitCode).toBe(2);
expect((e as ArgParseError).message).toContain("--bogus");
}
});
test("--help and -h short-circuit (parse to flag, not positional)", () => {
expect(parseArgs(["--help"]).flags.help).toBe(true);
expect(parseArgs(["-h"]).flags.help).toBe(true);
expect(parseArgs(["react", "--help"]).flags.help).toBe(true);
});
test("--version and -V parse", () => {
expect(parseArgs(["--version"]).flags.version).toBe(true);
expect(parseArgs(["-V"]).flags.version).toBe(true);
});
test("--quiet and --no-cache and --clear-cache parse", () => {
const r = parseArgs(["--quiet", "--no-cache", "--clear-cache"]);
expect(r.flags.quiet).toBe(true);
expect(r.flags.noCache).toBe(true);
expect(r.flags.clearCache).toBe(true);
});
});
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"lib": ["ES2022"],
"outDir": "dist",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
FullLookup Workflow
Trigger: "help me with [library]", "how do I use [feature] in [library]", "show me [library] docs"
Purpose
Complete end-to-end documentation lookup: resolve library ID and query documentation in one command.
When to Use
- User asks about a specific library feature
- Need to verify API usage before writing code
- Looking for current best practices or code examples
Command
c7-lookup <library> "<query>"Steps
Step 1: Identify Library and Query
From user request, extract:
- Library name: What library/framework they're asking about
- Specific query: What aspect/feature they need help with
Step 2: Run Full Lookup
c7-lookup <library> "<specific feature/topic>"Step 3: Synthesize Response
Combine Context7 results with:
- User's specific context/codebase
- Any constraints they mentioned
- Best practices for their use case
Complete Example
User: "How do I implement server-side rendering with data fetching in Next.js 14?"
Step 1: Extract
- Library: "next.js"
- Query: "server-side rendering data fetching app router"
Step 2: Run Command
c7-lookup next.js "server components data fetching app router"
Step 3: Synthesize
-> Provide user with current Next.js 14 patterns for SSR data fetching,
including Server Components approach (not old getServerSideProps)Common Library Mappings
| User Says | Library Name |
|---|---|
| "React", "react hooks" | react |
| "Next", "Next.js", "nextjs" | next.js |
| "Vue", "Vue 3" | vue |
| "K8s", "Kubernetes" | kubernetes |
| "Go", "Golang" | go |
| "Node", "Node.js" | node |
| "TS", "TypeScript" | typescript |
| "Python" | python |
| "Tailwind", "TailwindCSS" | tailwind |
| "Prisma" | prisma |
More Examples
React Hooks
c7-lookup react "useCallback useMemo when to use"Kubernetes Resources
c7-lookup kubernetes "ingress nginx annotations"Prisma Relations
c7-lookup prisma "many-to-many relations"Failure Handling
If lookup returns insufficient results: 1. Try broader query terms 2. Try more specific query terms 3. Use step-by-step workflow (resolve then query separately) 4. Inform user and proceed with general knowledge after 3 attempts
Common Errors
| Exit | Meaning | Recovery |
|---|---|---|
3 | Bad/missing CONTEXT7_API_KEY | Set env var or pass --api-key <key> |
4 | Library not found at resolve step | Check spelling; try a different name; consult Common Library Mappings |
5 | Rate limited (429) | Set CONTEXT7_API_KEY or raise --max-retries |
7 | Timed out | Raise --timeout <ms> (e.g. --timeout 60000) |
Without bun link, prefix any command with cd ~/.claude/skills/context7/Tools && bun src/cli/lookup.ts ...
QueryDocs Workflow
Trigger: "lookup docs", "get documentation", "code examples for", "how to use"
Purpose
Query up-to-date documentation and code examples from a specific library using its Context7 library ID.
Prerequisites
- You must have a valid Context7 library ID (from
ResolveLibraryworkflow or known ID) - Library ID format:
/org/projector/org/project/version
Command
c7-query <library_id> "<query>"Parameters:
library_id(required): Context7-compatible library ID (e.g.,/facebook/react)query(required): Natural language question about the library
Steps
Step 1: Run Query Command
c7-query "/org/project" "specific question"Step 2: Process Results
Results include:
- Documentation snippets relevant to your query
- Code examples from actual library documentation
- Version-specific information when available
Step 3: Apply to Task
Use the retrieved documentation to:
- Verify API signatures before writing code
- Get current best practices
- Find working code examples to adapt
Query Tips
| Goal | Query Example |
|---|---|
| API reference | "useState hook signature and return values" |
| Code example | "useEffect with cleanup function example" |
| Configuration | "webpack configuration for typescript" |
| Migration | "migrate from pages router to app router" |
| Troubleshooting | "common errors with async components" |
| Best practices | "when to use useMemo vs useCallback" |
Examples
React Hooks
c7-query /facebook/react "useCallback hook usage and when to use it"Next.js Middleware
c7-query /vercel/next.js "middleware authentication redirect"Kubernetes Deployments
c7-query /kubernetes/kubernetes "deployment strategy rolling update maxSurge"TypeScript Generics
c7-query /microsoft/typescript "generic constraints extends keyof"Prisma Queries
c7-query /prisma/prisma "findMany with include nested relations"Common Library IDs
| Library | ID |
|---|---|
| React | /facebook/react |
| Next.js | /vercel/next.js |
| Vue | /vuejs/vue |
| Kubernetes | /kubernetes/kubernetes |
| Go | /golang/go |
| Python | /python/cpython |
| Node.js | /nodejs/node |
| TypeScript | /microsoft/typescript |
| Prisma | /prisma/prisma |
| Tailwind | /tailwindlabs/tailwindcss |
Important Notes
- Be specific - vague queries return less useful results
- Max 3 calls per question - refine your query if first attempt isn't helpful
- Don't include sensitive info in query parameter
- Library ID must start with
/(e.g.,/facebook/react, notfacebook/react) - Use
c7-lookup <name> <query>if you don't know the library ID - Add
--jsonto emit a parseable{ libraryId, query, rawContent }for scripting - Without
bun link, prefix withcd ~/.claude/skills/context7/Tools && bun src/cli/query.ts ...
Common Errors
| Exit | Meaning | Recovery |
|---|---|---|
4 | Invalid ID format (missing leading /) or library not found | Add the / or run c7-resolve first |
5 | Rate limited (429) | Set CONTEXT7_API_KEY or raise --max-retries |
7 | Timed out | Raise --timeout <ms> |
ResolveLibrary Workflow
Trigger: "find library ID", "resolve library", "what's the context7 ID for"
Purpose
Convert a library/package name to a Context7-compatible library ID that can be used with the query command.
When to Use
- When you need only the library ID (not full documentation)
- When exploring what libraries are available
- When the full lookup command fails and you need to debug
Command
c7-resolve <library_name> [query]Parameters:
library_name(required): The library name to search (e.g., "react", "next.js", "kubernetes")query(optional): Context query for LLM-powered ranking of results
Steps
Step 1: Run Resolve Command
c7-resolve <library_name> "<optional context>"Step 2: Analyze Results
The tool returns matching libraries with:
- Library ID (e.g.,
/facebook/react) - Name and description
- Documentation coverage information
Step 3: Select Best Match
Selection criteria (in order): 1. Name similarity to query (exact matches prioritized) 2. Description relevance to query's intent 3. Higher documentation coverage 4. Official/verified sources
Examples
Basic Resolve
c7-resolve reactOutput:
[INFO] Using known library ID for 'react'
Library ID: /facebook/react
Tip: c7-query "/facebook/react" "your query"Resolve with Context
c7-resolve next.js "authentication middleware"Output includes ranked results based on the context query.
Resolve Unknown Library
c7-resolve drizzle-ormKnown Library IDs (Skip API Call)
These common libraries are cached locally - no API call needed:
| Library | ID |
|---|---|
| react | /facebook/react |
| next.js, nextjs | /vercel/next.js |
| vue | /vuejs/vue |
| kubernetes, k8s | /kubernetes/kubernetes |
| go, golang | /golang/go |
| python | /python/cpython |
| node, nodejs | /nodejs/node |
| typescript, ts | /microsoft/typescript |
| angular | /angular/angular |
| svelte | /sveltejs/svelte |
| express | /expressjs/express |
| fastify | /fastify/fastify |
| nest, nestjs | /nestjs/nest |
| prisma | /prisma/prisma |
| drizzle | /drizzle-team/drizzle-orm |
| tailwind, tailwindcss | /tailwindlabs/tailwindcss |
Important Notes
- Max 3 calls per question - if you can't find a match after 3 attempts, use best available
- Don't include sensitive info in query parameter (no API keys, passwords, personal data)
- The library ID format is always
/org/projector/org/project/version - Known libraries skip the API call entirely for faster response
- Resolved IDs are cached at
~/.cache/context7/resolved.jsonfor 24h; pass--no-cacheto skip or--clear-cacheto wipe - Add
--jsonto emit a parseable structure on stdout (decorative logs go to stderr) - Without
bun link, prefix any command withcd ~/.claude/skills/context7/Tools && bun src/cli/resolve.ts ...
Common Errors
| Exit | Meaning | Recovery |
|---|---|---|
3 | Bad/missing CONTEXT7_API_KEY | Set env var or pass --api-key <key> |
4 | Library not found | Check name spelling; try a broader query; consult Known Library IDs table |
5 | Rate limited (429) | Wait the Retry-After window, raise --max-retries, or set an API key |
7 | Timed out | Raise --timeout <ms> |