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
39 changes: 38 additions & 1 deletion packages/cli/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# ctx7

CLI for the [Context7 Skills Registry](https://context7.com) - install and manage AI coding skills across different AI coding assistants.
CLI for [Context7](https://context7.com) - install and manage AI coding skills, and query up-to-date library documentation.

Skills are reusable prompt instructions that enhance your AI coding assistant with specialized capabilities like working with specific frameworks, libraries, or coding patterns.

Expand Down Expand Up @@ -28,6 +28,10 @@ ctx7 skills generate

# List installed skills
ctx7 skills list --claude

# Get library documentation
ctx7 docs resolve react "how to use hooks"
ctx7 docs get /facebook/react "useEffect examples"
```

## Usage
Expand Down Expand Up @@ -140,6 +144,36 @@ ctx7 skills remove pdf --claude
ctx7 skills remove pdf --global
```

### Library documentation

Query up-to-date documentation for any library indexed by Context7.

#### Resolve a library

Find the Context7 library ID for a library name.

```bash
ctx7 docs resolve react "how to use hooks"
ctx7 docs resolve nextjs "app router setup"
ctx7 docs resolve prisma "database relations"

# Output as JSON
ctx7 docs resolve react "hooks" --json
```

#### Get documentation

Fetch documentation for a specific library using its Context7 ID.

```bash
ctx7 docs get /facebook/react "useEffect cleanup"
ctx7 docs get /vercel/next.js "middleware authentication"
ctx7 docs get /prisma/prisma "one-to-many relations"

# Output as JSON
ctx7 docs get /facebook/react "hooks" --json
```

## Supported Clients

The CLI automatically detects which AI coding assistants you have installed and offers to install skills for them:
Expand All @@ -162,6 +196,9 @@ ctx7 si /anthropics/skills pdf # skills install
ctx7 ss pdf # skills search
ctx7 skills gen # skills generate
ctx7 skills g # skills generate
ctx7 docs r react "hooks" # docs resolve
ctx7 docs g /facebook/react "hooks" # docs get
ctx7 resolve react "hooks" # docs resolve (top-level alias)
```

## Learn More
Expand Down
253 changes: 253 additions & 0 deletions packages/cli/src/commands/docs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
import { Command } from "commander";
import pc from "picocolors";
import ora from "ora";

import { resolveLibrary, getLibraryContext } from "../utils/api.js";
import { log } from "../utils/logger.js";
import { trackEvent } from "../utils/tracking.js";
import { loadTokens } from "../utils/auth.js";
import type { LibrarySearchResult, ContextResponse } from "../types.js";

interface ResolveOptions {
json?: boolean;
}

interface GetOptions {
json?: boolean;
}

function getAccessToken(): string | undefined {
const tokens = loadTokens();
return tokens?.access_token;
}

function formatLibraryResult(lib: LibrarySearchResult, index: number): string {
const lines: string[] = [];
const indexStr = pc.dim(`${index + 1}.`);
lines.push(`${indexStr} ${pc.bold(lib.title)} ${pc.cyan(lib.id)}`);

if (lib.description) {
lines.push(` ${pc.dim(lib.description)}`);
}

const meta: string[] = [];
if (lib.totalSnippets) {
meta.push(`${lib.totalSnippets} snippets`);
}
if (lib.stars && lib.stars > 0) {
meta.push(`${lib.stars.toLocaleString()} stars`);
}
if (lib.trustScore !== undefined) {
meta.push(`trust: ${lib.trustScore}/10`);
}
if (meta.length > 0) {
lines.push(` ${pc.dim(meta.join(" • "))}`);
}

if (lib.versions && lib.versions.length > 0) {
const versionList = lib.versions.slice(0, 3).join(", ");
const more = lib.versions.length > 3 ? ` (+${lib.versions.length - 3} more)` : "";
lines.push(` ${pc.dim(`versions: ${versionList}${more}`)}`);
}

return lines.join("\n");
}

export function registerDocsCommands(program: Command): void {
const docs = program.command("docs").description("Query library documentation");

docs
.command("resolve")
.alias("r")
.argument("<library>", "Library name to search for (e.g., react, nextjs)")
.argument("<query>", "Your question or task (used for relevance ranking)")
.option("--json", "Output as JSON")
.description("Resolve a library name to a Context7 library ID")
.action(async (library: string, query: string, options: ResolveOptions) => {
await resolveCommand(library, query, options);
});

docs
.command("get")
.alias("g")
.argument("<libraryId>", "Context7 library ID (e.g., /facebook/react)")
.argument("<query>", "Your question or task")
.option("--json", "Output as JSON instead of text")
.description("Get documentation context for a library")
.action(async (libraryId: string, query: string, options: GetOptions) => {
await getCommand(libraryId, query, options);
});
}

export function registerDocsAliases(program: Command): void {
program
.command("resolve", { hidden: true })
.argument("<library>", "Library name to search for")
.argument("<query>", "Your question or task")
.option("--json", "Output as JSON")
.description("Resolve library name (alias for: docs resolve)")
.action(async (library: string, query: string, options: ResolveOptions) => {
await resolveCommand(library, query, options);
});
}

async function resolveCommand(
library: string,
query: string,
options: ResolveOptions
): Promise<void> {
trackEvent("command", { name: "docs_resolve" });

const spinner = ora(`Searching for "${library}"...`).start();

const accessToken = getAccessToken();

let data;
try {
data = await resolveLibrary(library, query, accessToken);
} catch (err) {
spinner.fail(pc.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
return;
}

if (data.error) {
spinner.fail(pc.red(`Error: ${data.message || data.error}`));
return;
}

if (!data.results || data.results.length === 0) {
spinner.warn(pc.yellow(`No libraries found matching "${library}"`));
return;
}

spinner.succeed(
`Found ${data.results.length} ${data.results.length === 1 ? "library" : "libraries"}`
);

if (options.json) {
console.log(JSON.stringify(data.results, null, 2));
return;
}

log.blank();

for (let i = 0; i < data.results.length; i++) {
log.plain(formatLibraryResult(data.results[i], i));
log.blank();
}

// Show quick command hint with the best match
if (data.results.length > 0) {
const bestMatch = data.results[0];
log.plain(
`${pc.bold("Quick command:")}\n` +
` Get docs: ${pc.cyan(`ctx7 docs get "${bestMatch.id}" "<your question>"`)}\n`
);
}
}

async function getCommand(libraryId: string, query: string, options: GetOptions): Promise<void> {
trackEvent("command", { name: "docs_get" });

// Validate library ID format
if (!libraryId.startsWith("/")) {
log.error(`Invalid library ID format: ${libraryId}`);
log.info(`Library IDs should start with "/" (e.g., /facebook/react, /vercel/next.js)`);
log.info(`Use "ctx7 docs resolve <library> <query>" to find the correct library ID`);
log.blank();
return;
}

const spinner = ora(`Fetching documentation for "${libraryId}"...`).start();

const accessToken = getAccessToken();
const outputType = options.json ? "json" : "txt";

let result;
try {
result = await getLibraryContext(libraryId, query, { type: outputType }, accessToken);
} catch (err) {
spinner.fail(pc.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
return;
}

// Handle text response
if (typeof result === "string") {
spinner.succeed(`Documentation retrieved`);
log.blank();
console.log(result);
return;
}

// Handle JSON response with potential errors
const contextResponse = result as ContextResponse;

if (contextResponse.error) {
// Handle redirect
if (contextResponse.redirectUrl) {
spinner.warn(pc.yellow(`Library has been redirected`));
log.blank();
log.info(
`The library ${libraryId} has been moved to: ${pc.cyan(contextResponse.redirectUrl)}`
);
log.info(`Use the new library ID to fetch documentation:`);
log.plain(` ${pc.cyan(`ctx7 docs get "${contextResponse.redirectUrl}" "${query}"`)}`);
log.blank();
return;
}

spinner.fail(pc.red(`Error: ${contextResponse.message || contextResponse.error}`));
return;
}

const totalSnippets =
(contextResponse.codeSnippets?.length || 0) + (contextResponse.infoSnippets?.length || 0);

if (totalSnippets === 0) {
spinner.warn(pc.yellow(`No documentation found for query: "${query}"`));
return;
}

spinner.succeed(
`Found ${contextResponse.codeSnippets?.length || 0} code snippet(s) and ${contextResponse.infoSnippets?.length || 0} info snippet(s)`
);

if (options.json) {
console.log(JSON.stringify(contextResponse, null, 2));
return;
}

// Format output for non-JSON mode (this case shouldn't normally happen since we request txt)
log.blank();

if (contextResponse.codeSnippets && contextResponse.codeSnippets.length > 0) {
for (const snippet of contextResponse.codeSnippets) {
log.plain(`${pc.bold(snippet.codeTitle)}`);
if (snippet.codeDescription) {
log.dim(snippet.codeDescription);
}
log.dim(`Source: ${snippet.codeId}`);
log.blank();

for (const code of snippet.codeList) {
log.plain("```" + code.language);
log.plain(code.code);
log.plain("```");
log.blank();
}
}
}

if (contextResponse.infoSnippets && contextResponse.infoSnippets.length > 0) {
for (const snippet of contextResponse.infoSnippets) {
if (snippet.breadcrumb) {
log.plain(pc.bold(snippet.breadcrumb));
}
log.plain(snippet.content);
if (snippet.pageId) {
log.dim(`Source: ${snippet.pageId}`);
}
log.blank();
}
}
}
4 changes: 3 additions & 1 deletion packages/cli/src/commands/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,9 @@ async function generateCommand(options: GenerateOptions): Promise<void> {
: pc.white(projectId);

const starsLine =
lib.stars && isGitHub ? [`${pc.yellow("Stars:")} ${lib.stars.toLocaleString()}`] : [];
lib.stars && lib.stars > 0 && isGitHub
? [`${pc.yellow("Stars:")} ${lib.stars.toLocaleString()}`]
: [];

const metadataLines = [
pc.dim("─".repeat(50)),
Expand Down
7 changes: 7 additions & 0 deletions packages/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import pc from "picocolors";
import figlet from "figlet";
import { registerSkillCommands, registerSkillAliases } from "./commands/skill.js";
import { registerAuthCommands, setAuthBaseUrl } from "./commands/auth.js";
import { registerDocsCommands, registerDocsAliases } from "./commands/docs.js";
import { setBaseUrl } from "./utils/api.js";
import { VERSION } from "./constants.js";

Expand Down Expand Up @@ -45,13 +46,19 @@ Examples:
${brand.primary("npx ctx7 skills list --claude")}
${brand.primary("npx ctx7 skills remove pdf")}

${brand.dim("# Get library documentation")}
${brand.primary('npx ctx7 docs resolve react "how to use hooks"')}
${brand.primary('npx ctx7 docs get /facebook/react "useEffect examples"')}

Visit ${brand.primary("https://context7.com")} to browse skills
`
);

registerSkillCommands(program);
registerSkillAliases(program);
registerAuthCommands(program);
registerDocsCommands(program);
registerDocsAliases(program);

program.action(() => {
console.log("");
Expand Down
Loading