ctx7 0.2.4 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -16,6 +16,18 @@ npm install -g ctx7
16
16
 
17
17
  ## Quick Start
18
18
 
19
+ ```bash
20
+ # Set up Context7 MCP for your coding agents
21
+ ctx7 setup
22
+
23
+ # Target a specific agent
24
+ ctx7 setup --cursor
25
+ ctx7 setup --claude
26
+ ctx7 setup --opencode
27
+ ```
28
+
29
+ ### Skills
30
+
19
31
  ```bash
20
32
  # Search for skills
21
33
  ctx7 skills search pdf
@@ -32,6 +44,32 @@ ctx7 skills list --claude
32
44
 
33
45
  ## Usage
34
46
 
47
+ ### Setup
48
+
49
+ Configure Context7 MCP and a rule for your AI coding agents. Authenticates via OAuth, generates an API key, and writes the config.
50
+
51
+ ```bash
52
+ # Interactive (prompts for agent selection)
53
+ ctx7 setup
54
+
55
+ # Target specific agents
56
+ ctx7 setup --cursor
57
+ ctx7 setup --claude
58
+ ctx7 setup --opencode
59
+
60
+ # Use an existing API key instead of OAuth
61
+ ctx7 setup --api-key YOUR_API_KEY
62
+
63
+ # Use OAuth endpoint (IDE handles auth flow)
64
+ ctx7 setup --oauth
65
+
66
+ # Configure for current project only (default is global)
67
+ ctx7 setup --project
68
+
69
+ # Skip prompts
70
+ ctx7 setup --yes
71
+ ```
72
+
35
73
  ### Generate skills
36
74
 
37
75
  Generate custom skills tailored to your use case using AI. Requires authentication.
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/index.ts
4
4
  import { Command } from "commander";
5
- import pc8 from "picocolors";
5
+ import pc9 from "picocolors";
6
6
  import figlet from "figlet";
7
7
 
8
8
  // src/commands/skill.ts
@@ -2256,19 +2256,450 @@ ${headerLine}`,
2256
2256
  logInstallSummary(targets, targetDirs, installedNames);
2257
2257
  }
2258
2258
 
2259
+ // src/commands/setup.ts
2260
+ import pc8 from "picocolors";
2261
+ import ora4 from "ora";
2262
+ import { mkdir as mkdir4, writeFile as writeFile4 } from "fs/promises";
2263
+ import { dirname as dirname3, join as join8 } from "path";
2264
+ import { randomBytes as randomBytes2 } from "crypto";
2265
+
2266
+ // src/setup/agents.ts
2267
+ import { access as access2 } from "fs/promises";
2268
+ import { join as join7 } from "path";
2269
+ import { homedir as homedir5 } from "os";
2270
+ var SETUP_AGENT_NAMES = {
2271
+ claude: "Claude Code",
2272
+ cursor: "Cursor",
2273
+ opencode: "OpenCode"
2274
+ };
2275
+ var AUTH_MODE_LABELS = {
2276
+ oauth: "OAuth",
2277
+ "api-key": "API Key"
2278
+ };
2279
+ var MCP_BASE_URL = "https://mcp.context7.com";
2280
+ function mcpUrl(auth) {
2281
+ return auth.mode === "oauth" ? `${MCP_BASE_URL}/mcp/oauth` : `${MCP_BASE_URL}/mcp`;
2282
+ }
2283
+ function withHeaders(base, auth) {
2284
+ if (auth.mode === "api-key" && auth.apiKey) {
2285
+ return { ...base, headers: { CONTEXT7_API_KEY: auth.apiKey } };
2286
+ }
2287
+ return base;
2288
+ }
2289
+ var agents = {
2290
+ claude: {
2291
+ name: "claude",
2292
+ displayName: "Claude Code",
2293
+ mcp: {
2294
+ projectPath: ".mcp.json",
2295
+ globalPath: join7(homedir5(), ".claude.json"),
2296
+ configKey: "mcpServers",
2297
+ buildEntry: (auth) => withHeaders({ type: "http", url: mcpUrl(auth) }, auth)
2298
+ },
2299
+ rule: {
2300
+ dir: (scope) => scope === "global" ? join7(homedir5(), ".claude", "rules") : join7(".claude", "rules"),
2301
+ filename: "context7.md"
2302
+ },
2303
+ skill: {
2304
+ name: "documentation-lookup",
2305
+ dir: (scope) => scope === "global" ? join7(homedir5(), ".claude", "skills") : join7(".claude", "skills")
2306
+ },
2307
+ detect: {
2308
+ projectPaths: [".mcp.json", ".claude"],
2309
+ globalPaths: [join7(homedir5(), ".claude")]
2310
+ }
2311
+ },
2312
+ cursor: {
2313
+ name: "cursor",
2314
+ displayName: "Cursor",
2315
+ mcp: {
2316
+ projectPath: join7(".cursor", "mcp.json"),
2317
+ globalPath: join7(homedir5(), ".cursor", "mcp.json"),
2318
+ configKey: "mcpServers",
2319
+ buildEntry: (auth) => withHeaders({ url: mcpUrl(auth) }, auth)
2320
+ },
2321
+ rule: {
2322
+ dir: (scope) => scope === "global" ? join7(homedir5(), ".cursor", "rules") : join7(".cursor", "rules"),
2323
+ filename: "context7.mdc"
2324
+ },
2325
+ skill: {
2326
+ name: "documentation-lookup",
2327
+ dir: (scope) => scope === "global" ? join7(homedir5(), ".cursor", "skills") : join7(".cursor", "skills")
2328
+ },
2329
+ detect: {
2330
+ projectPaths: [".cursor"],
2331
+ globalPaths: [join7(homedir5(), ".cursor")]
2332
+ }
2333
+ },
2334
+ opencode: {
2335
+ name: "opencode",
2336
+ displayName: "OpenCode",
2337
+ mcp: {
2338
+ projectPath: ".opencode.json",
2339
+ globalPath: join7(homedir5(), ".config", "opencode", "opencode.json"),
2340
+ configKey: "mcp",
2341
+ buildEntry: (auth) => withHeaders({ type: "remote", url: mcpUrl(auth), enabled: true }, auth)
2342
+ },
2343
+ rule: {
2344
+ dir: (scope) => scope === "global" ? join7(homedir5(), ".config", "opencode", "rules") : join7(".opencode", "rules"),
2345
+ filename: "context7.md",
2346
+ instructionsGlob: (scope) => scope === "global" ? join7(homedir5(), ".config", "opencode", "rules", "*.md") : ".opencode/rules/*.md"
2347
+ },
2348
+ skill: {
2349
+ name: "documentation-lookup",
2350
+ dir: (scope) => scope === "global" ? join7(homedir5(), ".agents", "skills") : join7(".agents", "skills")
2351
+ },
2352
+ detect: {
2353
+ projectPaths: [".opencode.json"],
2354
+ globalPaths: [join7(homedir5(), ".config", "opencode")]
2355
+ }
2356
+ }
2357
+ };
2358
+ function getAgent(name) {
2359
+ return agents[name];
2360
+ }
2361
+ var ALL_AGENT_NAMES = Object.keys(agents);
2362
+ async function pathExists(p) {
2363
+ try {
2364
+ await access2(p);
2365
+ return true;
2366
+ } catch {
2367
+ return false;
2368
+ }
2369
+ }
2370
+ async function detectAgents(scope) {
2371
+ const detected = [];
2372
+ for (const agent of Object.values(agents)) {
2373
+ const paths = scope === "global" ? agent.detect.globalPaths : agent.detect.projectPaths;
2374
+ for (const p of paths) {
2375
+ const fullPath = scope === "global" ? p : join7(process.cwd(), p);
2376
+ if (await pathExists(fullPath)) {
2377
+ detected.push(agent.name);
2378
+ break;
2379
+ }
2380
+ }
2381
+ }
2382
+ return detected;
2383
+ }
2384
+
2385
+ // src/setup/templates.ts
2386
+ var SKILL_CONTENT = `---
2387
+ name: documentation-lookup
2388
+ description: This skill should be used when the user asks about libraries, frameworks, API references, or needs code examples. Activates for setup questions, code generation involving libraries, or mentions of specific frameworks like React, Vue, Next.js, Prisma, Supabase, etc.
2389
+ ---
2390
+
2391
+ When the user asks about libraries, frameworks, or needs code examples, use Context7 to fetch current documentation instead of relying on training data.
2392
+
2393
+ ## When to Use This Skill
2394
+
2395
+ Activate this skill when the user:
2396
+
2397
+ - Asks setup or configuration questions ("How do I configure Next.js middleware?")
2398
+ - Requests code involving libraries ("Write a Prisma query for...")
2399
+ - Needs API references ("What are the Supabase auth methods?")
2400
+ - Mentions specific frameworks (React, Vue, Svelte, Express, Tailwind, etc.)
2401
+
2402
+ ## How to Fetch Documentation
2403
+
2404
+ ### Step 1: Resolve the Library ID
2405
+
2406
+ Call \`resolve-library-id\` with:
2407
+
2408
+ - \`libraryName\`: The library name extracted from the user's question
2409
+ - \`query\`: The user's full question (improves relevance ranking)
2410
+
2411
+ ### Step 2: Select the Best Match
2412
+
2413
+ From the resolution results, choose based on:
2414
+
2415
+ - Exact or closest name match to what the user asked for
2416
+ - Higher benchmark scores indicate better documentation quality
2417
+ - If the user mentioned a version (e.g., "React 19"), prefer version-specific IDs
2418
+
2419
+ ### Step 3: Fetch the Documentation
2420
+
2421
+ Call \`query-docs\` with:
2422
+
2423
+ - \`libraryId\`: The selected Context7 library ID (e.g., \`/vercel/next.js\`)
2424
+ - \`query\`: The user's specific question
2425
+
2426
+ ### Step 4: Use the Documentation
2427
+
2428
+ Incorporate the fetched documentation into your response:
2429
+
2430
+ - Answer the user's question using current, accurate information
2431
+ - Include relevant code examples from the docs
2432
+ - Cite the library version when relevant
2433
+
2434
+ ## Guidelines
2435
+
2436
+ - **Be specific**: Pass the user's full question as the query for better results
2437
+ - **Version awareness**: When users mention versions ("Next.js 15", "React 19"), use version-specific library IDs if available from the resolution step
2438
+ - **Prefer official sources**: When multiple matches exist, prefer official/primary packages over community forks
2439
+ `;
2440
+ var RULE_CONTENT = `---
2441
+ alwaysApply: true
2442
+ ---
2443
+
2444
+ When working with libraries, frameworks, or APIs \u2014 use Context7 MCP to fetch current documentation instead of relying on training data. This includes setup questions, code generation, API references, and anything involving specific packages.
2445
+
2446
+ ## Steps
2447
+
2448
+ 1. Call \`resolve-library-id\` with the library name and the user's question
2449
+ 2. Pick the best match \u2014 prefer exact names and version-specific IDs when a version is mentioned
2450
+ 3. Call \`query-docs\` with the selected library ID and the user's question
2451
+ 4. Answer using the fetched docs \u2014 include code examples and cite the version
2452
+ `;
2453
+
2454
+ // src/setup/mcp-writer.ts
2455
+ import { readFile as readFile3, writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
2456
+ import { dirname as dirname2 } from "path";
2457
+ async function readJsonConfig(filePath) {
2458
+ let raw;
2459
+ try {
2460
+ raw = await readFile3(filePath, "utf-8");
2461
+ } catch {
2462
+ return {};
2463
+ }
2464
+ raw = raw.trim();
2465
+ if (!raw) return {};
2466
+ return JSON.parse(raw);
2467
+ }
2468
+ function mergeServerEntry(existing, configKey, serverName, entry) {
2469
+ const section = existing[configKey] ?? {};
2470
+ if (serverName in section) {
2471
+ return { config: existing, alreadyExists: true };
2472
+ }
2473
+ return {
2474
+ config: {
2475
+ ...existing,
2476
+ [configKey]: {
2477
+ ...section,
2478
+ [serverName]: entry
2479
+ }
2480
+ },
2481
+ alreadyExists: false
2482
+ };
2483
+ }
2484
+ function mergeInstructions(config, glob) {
2485
+ const instructions = config.instructions ?? [];
2486
+ if (instructions.includes(glob)) return config;
2487
+ return { ...config, instructions: [...instructions, glob] };
2488
+ }
2489
+ async function writeJsonConfig(filePath, config) {
2490
+ await mkdir3(dirname2(filePath), { recursive: true });
2491
+ await writeFile3(filePath, JSON.stringify(config, null, 2) + "\n", "utf-8");
2492
+ }
2493
+
2494
+ // src/commands/setup.ts
2495
+ var CHECKBOX_THEME = {
2496
+ style: {
2497
+ highlight: (text) => pc8.green(text),
2498
+ disabledChoice: (text) => ` ${pc8.dim("\u25EF")} ${pc8.dim(text)}`
2499
+ }
2500
+ };
2501
+ function getSelectedAgents(options) {
2502
+ const agents2 = [];
2503
+ if (options.claude) agents2.push("claude");
2504
+ if (options.cursor) agents2.push("cursor");
2505
+ if (options.opencode) agents2.push("opencode");
2506
+ return agents2;
2507
+ }
2508
+ function registerSetupCommand(program2) {
2509
+ program2.command("setup").description("Set up Context7 MCP and rule for your AI coding agent").option("--claude", "Set up for Claude Code").option("--cursor", "Set up for Cursor").option("--opencode", "Set up for OpenCode").option("-p, --project", "Configure for current project instead of globally").option("-y, --yes", "Skip confirmation prompts").option("--api-key <key>", "Use API key authentication").option("--oauth", "Use OAuth endpoint (IDE handles auth flow)").action(async (options) => {
2510
+ await setupCommand(options);
2511
+ });
2512
+ }
2513
+ async function authenticateAndGenerateKey() {
2514
+ const existingTokens = loadTokens();
2515
+ const accessToken = existingTokens && !isTokenExpired(existingTokens) ? existingTokens.access_token : await performLogin();
2516
+ if (!accessToken) return null;
2517
+ const spinner = ora4("Configuring authentication...").start();
2518
+ try {
2519
+ const response = await fetch(`${getBaseUrl()}/api/dashboard/api-keys`, {
2520
+ method: "POST",
2521
+ headers: {
2522
+ Authorization: `Bearer ${accessToken}`,
2523
+ "Content-Type": "application/json"
2524
+ },
2525
+ body: JSON.stringify({ name: `ctx7-cli-${randomBytes2(3).toString("hex")}` })
2526
+ });
2527
+ if (!response.ok) {
2528
+ const err = await response.json().catch(() => ({}));
2529
+ spinner.fail("Authentication failed");
2530
+ log.error(err.message || err.error || `HTTP ${response.status}`);
2531
+ return null;
2532
+ }
2533
+ const result = await response.json();
2534
+ spinner.succeed("Authenticated");
2535
+ return result.data.apiKey;
2536
+ } catch (err) {
2537
+ spinner.fail("Authentication failed");
2538
+ log.error(err instanceof Error ? err.message : String(err));
2539
+ return null;
2540
+ }
2541
+ }
2542
+ async function resolveAuth(options) {
2543
+ if (options.apiKey) return { mode: "api-key", apiKey: options.apiKey };
2544
+ if (options.oauth) return { mode: "oauth" };
2545
+ const apiKey = await authenticateAndGenerateKey();
2546
+ if (!apiKey) return null;
2547
+ return { mode: "api-key", apiKey };
2548
+ }
2549
+ async function isAlreadyConfigured(agentName, scope) {
2550
+ const agent = getAgent(agentName);
2551
+ const mcpPath = scope === "global" ? agent.mcp.globalPath : join8(process.cwd(), agent.mcp.projectPath);
2552
+ try {
2553
+ const existing = await readJsonConfig(mcpPath);
2554
+ const section = existing[agent.mcp.configKey] ?? {};
2555
+ return "context7" in section;
2556
+ } catch {
2557
+ return false;
2558
+ }
2559
+ }
2560
+ async function promptAgents(scope) {
2561
+ const choices = await Promise.all(
2562
+ ALL_AGENT_NAMES.map(async (name) => {
2563
+ const configured = await isAlreadyConfigured(name, scope);
2564
+ return {
2565
+ name: SETUP_AGENT_NAMES[name],
2566
+ value: name,
2567
+ disabled: configured ? "(already configured)" : false
2568
+ };
2569
+ })
2570
+ );
2571
+ if (choices.every((c) => c.disabled)) {
2572
+ log.info("Context7 is already configured for all detected agents.");
2573
+ return null;
2574
+ }
2575
+ try {
2576
+ return await checkboxWithHover(
2577
+ {
2578
+ message: "Which agents do you want to set up?",
2579
+ choices,
2580
+ loop: false,
2581
+ theme: CHECKBOX_THEME
2582
+ },
2583
+ { getName: (a) => SETUP_AGENT_NAMES[a] }
2584
+ );
2585
+ } catch {
2586
+ return null;
2587
+ }
2588
+ }
2589
+ async function resolveAgents(options, scope) {
2590
+ const explicit = getSelectedAgents(options);
2591
+ if (explicit.length > 0) return explicit;
2592
+ const detected = await detectAgents(scope);
2593
+ if (detected.length > 0 && options.yes) return detected;
2594
+ log.blank();
2595
+ const selected = await promptAgents(scope);
2596
+ if (!selected) {
2597
+ log.warn("Setup cancelled");
2598
+ return [];
2599
+ }
2600
+ return selected;
2601
+ }
2602
+ async function setupAgent(agentName, auth, scope) {
2603
+ const agent = getAgent(agentName);
2604
+ const mcpPath = scope === "global" ? agent.mcp.globalPath : join8(process.cwd(), agent.mcp.projectPath);
2605
+ let mcpStatus;
2606
+ try {
2607
+ const existing = await readJsonConfig(mcpPath);
2608
+ const { config, alreadyExists } = mergeServerEntry(
2609
+ existing,
2610
+ agent.mcp.configKey,
2611
+ "context7",
2612
+ agent.mcp.buildEntry(auth)
2613
+ );
2614
+ if (alreadyExists) {
2615
+ mcpStatus = "already configured";
2616
+ } else {
2617
+ mcpStatus = `configured with ${AUTH_MODE_LABELS[auth.mode]}`;
2618
+ }
2619
+ const finalConfig = agent.rule.instructionsGlob ? mergeInstructions(config, agent.rule.instructionsGlob(scope)) : config;
2620
+ if (finalConfig !== existing) {
2621
+ await writeJsonConfig(mcpPath, finalConfig);
2622
+ }
2623
+ } catch (err) {
2624
+ mcpStatus = `failed: ${err instanceof Error ? err.message : String(err)}`;
2625
+ }
2626
+ const rulePath = scope === "global" ? join8(agent.rule.dir("global"), agent.rule.filename) : join8(process.cwd(), agent.rule.dir("project"), agent.rule.filename);
2627
+ let ruleStatus;
2628
+ try {
2629
+ await mkdir4(dirname3(rulePath), { recursive: true });
2630
+ await writeFile4(rulePath, RULE_CONTENT, "utf-8");
2631
+ ruleStatus = "installed";
2632
+ } catch (err) {
2633
+ ruleStatus = `failed: ${err instanceof Error ? err.message : String(err)}`;
2634
+ }
2635
+ const skillDir = scope === "global" ? agent.skill.dir("global") : join8(process.cwd(), agent.skill.dir("project"));
2636
+ const skillPath = join8(skillDir, agent.skill.name, "SKILL.md");
2637
+ let skillStatus;
2638
+ try {
2639
+ await mkdir4(dirname3(skillPath), { recursive: true });
2640
+ await writeFile4(skillPath, SKILL_CONTENT, "utf-8");
2641
+ skillStatus = "installed";
2642
+ } catch (err) {
2643
+ skillStatus = `failed: ${err instanceof Error ? err.message : String(err)}`;
2644
+ }
2645
+ return {
2646
+ agent: agent.displayName,
2647
+ mcpStatus,
2648
+ mcpPath,
2649
+ ruleStatus,
2650
+ rulePath,
2651
+ skillStatus,
2652
+ skillPath
2653
+ };
2654
+ }
2655
+ async function setupCommand(options) {
2656
+ trackEvent("command", { name: "setup" });
2657
+ const scope = options.project ? "project" : "global";
2658
+ const agents2 = await resolveAgents(options, scope);
2659
+ if (agents2.length === 0) return;
2660
+ const auth = await resolveAuth(options);
2661
+ if (!auth) {
2662
+ log.warn("Setup cancelled");
2663
+ return;
2664
+ }
2665
+ log.blank();
2666
+ const spinner = ora4("Setting up Context7...").start();
2667
+ const results = [];
2668
+ for (const agentName of agents2) {
2669
+ spinner.text = `Setting up ${getAgent(agentName).displayName}...`;
2670
+ results.push(await setupAgent(agentName, auth, scope));
2671
+ }
2672
+ spinner.succeed("Context7 setup complete");
2673
+ log.blank();
2674
+ for (const r of results) {
2675
+ log.plain(` ${pc8.bold(r.agent)}`);
2676
+ const mcpIcon = r.mcpStatus.startsWith("configured") ? pc8.green("+") : pc8.dim("~");
2677
+ log.plain(` ${mcpIcon} MCP server ${r.mcpStatus}`);
2678
+ log.plain(` ${pc8.dim(r.mcpPath)}`);
2679
+ const ruleIcon = r.ruleStatus === "installed" ? pc8.green("+") : pc8.dim("~");
2680
+ log.plain(` ${ruleIcon} Rule ${r.ruleStatus}`);
2681
+ log.plain(` ${pc8.dim(r.rulePath)}`);
2682
+ const skillIcon = r.skillStatus === "installed" ? pc8.green("+") : pc8.dim("~");
2683
+ log.plain(` ${skillIcon} Skill ${r.skillStatus}`);
2684
+ log.plain(` ${pc8.dim(r.skillPath)}`);
2685
+ }
2686
+ log.blank();
2687
+ trackEvent("setup", { agents: agents2, scope, authMode: auth.mode });
2688
+ }
2689
+
2259
2690
  // src/constants.ts
2260
2691
  import { readFileSync as readFileSync2 } from "fs";
2261
2692
  import { fileURLToPath } from "url";
2262
- import { dirname as dirname2, join as join7 } from "path";
2263
- var __dirname = dirname2(fileURLToPath(import.meta.url));
2264
- var pkg = JSON.parse(readFileSync2(join7(__dirname, "../package.json"), "utf-8"));
2693
+ import { dirname as dirname4, join as join9 } from "path";
2694
+ var __dirname = dirname4(fileURLToPath(import.meta.url));
2695
+ var pkg = JSON.parse(readFileSync2(join9(__dirname, "../package.json"), "utf-8"));
2265
2696
  var VERSION = pkg.version;
2266
2697
  var NAME = pkg.name;
2267
2698
 
2268
2699
  // src/index.ts
2269
2700
  var brand = {
2270
- primary: pc8.green,
2271
- dim: pc8.dim
2701
+ primary: pc9.green,
2702
+ dim: pc9.dim
2272
2703
  };
2273
2704
  var program = new Command();
2274
2705
  program.name("ctx7").description("Context7 CLI - Manage AI coding skills and documentation context").version(VERSION).option("--base-url <url>").hook("preAction", (thisCommand) => {
@@ -2303,6 +2734,7 @@ Visit ${brand.primary("https://context7.com")} to browse skills
2303
2734
  registerSkillCommands(program);
2304
2735
  registerSkillAliases(program);
2305
2736
  registerAuthCommands(program);
2737
+ registerSetupCommand(program);
2306
2738
  program.action(() => {
2307
2739
  console.log("");
2308
2740
  const banner = figlet.textSync("Context7", { font: "ANSI Shadow" });