ctx7 0.3.6 → 0.3.9

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/dist/index.js CHANGED
@@ -103,6 +103,10 @@ async function downloadSkillFromGitHub(skill) {
103
103
  }
104
104
  const content = await fileResponse.text();
105
105
  const relativePath = item.path.slice(skillPath.length + 1);
106
+ if (relativePath.includes("..")) {
107
+ console.warn(`Skipping file with unsafe path: ${item.path}`);
108
+ continue;
109
+ }
106
110
  files.push({
107
111
  path: relativePath,
108
112
  content
@@ -433,13 +437,18 @@ async function checkboxWithHover(config, options) {
433
437
  );
434
438
  const values = choices.map((c) => c.value);
435
439
  const totalItems = values.length;
436
- let cursorPosition = 0;
440
+ let cursorPosition = choices.findIndex((c) => !c.disabled);
441
+ if (cursorPosition < 0) cursorPosition = 0;
437
442
  const getName = options?.getName ?? ((v) => v.name);
438
443
  const keypressHandler = (_str, key) => {
439
- if (key.name === "up" && cursorPosition > 0) {
440
- cursorPosition--;
441
- } else if (key.name === "down" && cursorPosition < totalItems - 1) {
442
- cursorPosition++;
444
+ if (key.name === "up") {
445
+ let next = cursorPosition - 1;
446
+ while (next >= 0 && choices[next].disabled) next--;
447
+ if (next >= 0) cursorPosition = next;
448
+ } else if (key.name === "down") {
449
+ let next = cursorPosition + 1;
450
+ while (next < totalItems && choices[next].disabled) next++;
451
+ if (next < totalItems) cursorPosition = next;
443
452
  }
444
453
  };
445
454
  readline.emitKeypressEvents(process.stdin);
@@ -483,7 +492,7 @@ var IDE_GLOBAL_PATHS = {
483
492
  claude: ".claude/skills",
484
493
  cursor: ".cursor/skills",
485
494
  antigravity: ".agent/skills",
486
- universal: ".config/agents/skills"
495
+ universal: ".agents/skills"
487
496
  };
488
497
  var IDE_NAMES = {
489
498
  claude: "Claude Code",
@@ -492,7 +501,7 @@ var IDE_NAMES = {
492
501
  universal: "Universal"
493
502
  };
494
503
  var UNIVERSAL_SKILLS_PATH = ".agents/skills";
495
- var UNIVERSAL_SKILLS_GLOBAL_PATH = ".config/agents/skills";
504
+ var UNIVERSAL_SKILLS_GLOBAL_PATH = ".agents/skills";
496
505
  var UNIVERSAL_AGENTS_LABEL = "Amp, Codex, Gemini CLI, GitHub Copilot, OpenCode + more";
497
506
  var VENDOR_SPECIFIC_AGENTS = ["claude", "cursor", "antigravity"];
498
507
  var DEFAULT_CONFIG = {
@@ -715,12 +724,15 @@ function getTargetDirFromSelection(ide, scope) {
715
724
 
716
725
  // src/utils/installer.ts
717
726
  import { mkdir, writeFile, rm, symlink, lstat } from "fs/promises";
718
- import { join as join3 } from "path";
727
+ import { join as join3, resolve, dirname as dirname3 } from "path";
719
728
  async function installSkillFiles(skillName, files, targetDir) {
720
- const skillDir = join3(targetDir, skillName);
729
+ const skillDir = resolve(targetDir, skillName);
721
730
  for (const file of files) {
722
- const filePath = join3(skillDir, file.path);
723
- const fileDir = join3(filePath, "..");
731
+ const filePath = resolve(skillDir, file.path);
732
+ if (!filePath.startsWith(skillDir + "/") && filePath !== skillDir) {
733
+ throw new Error(`Skill file path "${file.path}" resolves outside the target directory`);
734
+ }
735
+ const fileDir = dirname3(filePath);
724
736
  await mkdir(fileDir, { recursive: true });
725
737
  await writeFile(filePath, file.content);
726
738
  }
@@ -850,11 +862,11 @@ function createCallbackServer(expectedState) {
850
862
  let resolveResult;
851
863
  let rejectResult;
852
864
  let serverInstance = null;
853
- const portPromise = new Promise((resolve) => {
854
- resolvePort = resolve;
865
+ const portPromise = new Promise((resolve2) => {
866
+ resolvePort = resolve2;
855
867
  });
856
- const resultPromise = new Promise((resolve, reject) => {
857
- resolveResult = resolve;
868
+ const resultPromise = new Promise((resolve2, reject) => {
869
+ resolveResult = resolve2;
858
870
  rejectResult = reject;
859
871
  });
860
872
  const server = http.createServer((req, res) => {
@@ -1317,7 +1329,7 @@ async function generateCommand(options) {
1317
1329
  log.blank();
1318
1330
  if (searchResult.searchFilterApplied) {
1319
1331
  log.warn(
1320
- "Your results only include libraries matching your access settings. To search across all public libraries, update your settings at https://context7.com/dashboard?tab=libraries"
1332
+ "Your results only include libraries matching your teamspace's library filters. To adjust quality thresholds or blocked libraries, update your filters at https://context7.com/dashboard?tab=policies"
1321
1333
  );
1322
1334
  log.blank();
1323
1335
  }
@@ -1542,12 +1554,11 @@ async function generateCommand(options) {
1542
1554
  previewFileWritten = true;
1543
1555
  }
1544
1556
  const editor = process.env.EDITOR || "open";
1545
- await new Promise((resolve) => {
1557
+ await new Promise((resolve2) => {
1546
1558
  const child = spawn(editor, [previewFile], {
1547
- stdio: "inherit",
1548
- shell: true
1559
+ stdio: "inherit"
1549
1560
  });
1550
- child.on("close", () => resolve());
1561
+ child.on("close", () => resolve2());
1551
1562
  });
1552
1563
  };
1553
1564
  const syncFromPreviewFile = async () => {
@@ -1556,7 +1567,7 @@ async function generateCommand(options) {
1556
1567
  }
1557
1568
  };
1558
1569
  showPreview();
1559
- await new Promise((resolve) => setTimeout(resolve, 100));
1570
+ await new Promise((resolve2) => setTimeout(resolve2, 100));
1560
1571
  try {
1561
1572
  let action;
1562
1573
  while (true) {
@@ -2415,8 +2426,8 @@ ${headerLine}`,
2415
2426
  import pc8 from "picocolors";
2416
2427
  import ora4 from "ora";
2417
2428
  import { select as select3 } from "@inquirer/prompts";
2418
- import { mkdir as mkdir4, writeFile as writeFile4 } from "fs/promises";
2419
- import { dirname as dirname4, join as join9 } from "path";
2429
+ import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
2430
+ import { dirname as dirname5, join as join9 } from "path";
2420
2431
  import { randomBytes as randomBytes2 } from "crypto";
2421
2432
 
2422
2433
  // src/setup/agents.ts
@@ -2426,7 +2437,8 @@ import { homedir as homedir5 } from "os";
2426
2437
  var SETUP_AGENT_NAMES = {
2427
2438
  claude: "Claude Code",
2428
2439
  cursor: "Cursor",
2429
- opencode: "OpenCode"
2440
+ opencode: "OpenCode",
2441
+ codex: "Codex"
2430
2442
  };
2431
2443
  var AUTH_MODE_LABELS = {
2432
2444
  oauth: "OAuth",
@@ -2447,12 +2459,13 @@ var agents = {
2447
2459
  name: "claude",
2448
2460
  displayName: "Claude Code",
2449
2461
  mcp: {
2450
- projectPath: ".mcp.json",
2451
- globalPath: join8(homedir5(), ".claude.json"),
2462
+ projectPaths: [".mcp.json"],
2463
+ globalPaths: [join8(homedir5(), ".claude.json")],
2452
2464
  configKey: "mcpServers",
2453
2465
  buildEntry: (auth) => withHeaders({ type: "http", url: mcpUrl(auth) }, auth)
2454
2466
  },
2455
2467
  rule: {
2468
+ kind: "file",
2456
2469
  dir: (scope) => scope === "global" ? join8(homedir5(), ".claude", "rules") : join8(".claude", "rules"),
2457
2470
  filename: "context7.md"
2458
2471
  },
@@ -2469,12 +2482,13 @@ var agents = {
2469
2482
  name: "cursor",
2470
2483
  displayName: "Cursor",
2471
2484
  mcp: {
2472
- projectPath: join8(".cursor", "mcp.json"),
2473
- globalPath: join8(homedir5(), ".cursor", "mcp.json"),
2485
+ projectPaths: [join8(".cursor", "mcp.json")],
2486
+ globalPaths: [join8(homedir5(), ".cursor", "mcp.json")],
2474
2487
  configKey: "mcpServers",
2475
2488
  buildEntry: (auth) => withHeaders({ url: mcpUrl(auth) }, auth)
2476
2489
  },
2477
2490
  rule: {
2491
+ kind: "file",
2478
2492
  dir: (scope) => scope === "global" ? join8(homedir5(), ".cursor", "rules") : join8(".cursor", "rules"),
2479
2493
  filename: "context7.mdc"
2480
2494
  },
@@ -2491,24 +2505,58 @@ var agents = {
2491
2505
  name: "opencode",
2492
2506
  displayName: "OpenCode",
2493
2507
  mcp: {
2494
- projectPath: ".opencode.json",
2495
- globalPath: join8(homedir5(), ".config", "opencode", "opencode.json"),
2508
+ projectPaths: ["opencode.json", "opencode.jsonc", ".opencode.json", ".opencode.jsonc"],
2509
+ globalPaths: [
2510
+ join8(homedir5(), ".config", "opencode", "opencode.json"),
2511
+ join8(homedir5(), ".config", "opencode", "opencode.jsonc"),
2512
+ join8(homedir5(), ".config", "opencode", ".opencode.json"),
2513
+ join8(homedir5(), ".config", "opencode", ".opencode.jsonc")
2514
+ ],
2496
2515
  configKey: "mcp",
2497
2516
  buildEntry: (auth) => withHeaders({ type: "remote", url: mcpUrl(auth), enabled: true }, auth)
2498
2517
  },
2499
2518
  rule: {
2500
- dir: (scope) => scope === "global" ? join8(homedir5(), ".config", "opencode", "rules") : join8(".opencode", "rules"),
2501
- filename: "context7.md",
2502
- instructionsGlob: (scope) => scope === "global" ? join8(homedir5(), ".config", "opencode", "rules", "*.md") : ".opencode/rules/*.md"
2519
+ kind: "append",
2520
+ file: (scope) => scope === "global" ? join8(homedir5(), ".config", "opencode", "AGENTS.md") : "AGENTS.md",
2521
+ sectionMarker: "<!-- context7 -->"
2503
2522
  },
2504
2523
  skill: {
2505
2524
  name: "context7-mcp",
2506
2525
  dir: (scope) => scope === "global" ? join8(homedir5(), ".agents", "skills") : join8(".agents", "skills")
2507
2526
  },
2508
2527
  detect: {
2509
- projectPaths: [".opencode.json"],
2528
+ projectPaths: ["opencode.json", "opencode.jsonc", ".opencode.json", ".opencode.jsonc"],
2510
2529
  globalPaths: [join8(homedir5(), ".config", "opencode")]
2511
2530
  }
2531
+ },
2532
+ codex: {
2533
+ name: "codex",
2534
+ displayName: "Codex",
2535
+ mcp: {
2536
+ projectPaths: [join8(".codex", "config.toml")],
2537
+ globalPaths: [join8(homedir5(), ".codex", "config.toml")],
2538
+ configKey: "mcp_servers",
2539
+ buildEntry: (auth) => {
2540
+ const entry = { type: "http", url: mcpUrl(auth) };
2541
+ if (auth.mode === "api-key" && auth.apiKey) {
2542
+ entry.headers = { CONTEXT7_API_KEY: auth.apiKey };
2543
+ }
2544
+ return entry;
2545
+ }
2546
+ },
2547
+ rule: {
2548
+ kind: "append",
2549
+ file: (scope) => scope === "global" ? join8(homedir5(), ".codex", "AGENTS.md") : "AGENTS.md",
2550
+ sectionMarker: "<!-- context7 -->"
2551
+ },
2552
+ skill: {
2553
+ name: "context7-mcp",
2554
+ dir: (scope) => scope === "global" ? join8(homedir5(), ".agents", "skills") : join8(".agents", "skills")
2555
+ },
2556
+ detect: {
2557
+ projectPaths: [".codex"],
2558
+ globalPaths: [join8(homedir5(), ".codex")]
2559
+ }
2512
2560
  }
2513
2561
  };
2514
2562
  function getAgent(name) {
@@ -2539,23 +2587,87 @@ async function detectAgents(scope) {
2539
2587
  }
2540
2588
 
2541
2589
  // src/setup/templates.ts
2542
- var RULE_CONTENT = `---
2543
- alwaysApply: true
2544
- ---
2590
+ var GITHUB_RAW_URLS = [
2591
+ "https://raw.githubusercontent.com/upstash/context7/master/rules",
2592
+ "https://raw.githubusercontent.com/upstash/context7/main/rules"
2593
+ ];
2594
+ var FALLBACK_MCP = `Use Context7 MCP to fetch current documentation whenever the user asks about a library, framework, SDK, API, CLI tool, or cloud service -- even well-known ones like React, Next.js, Prisma, Express, Tailwind, Django, or Spring Boot. This includes API syntax, configuration, version migration, library-specific debugging, setup instructions, and CLI tool usage. Use even when you think you know the answer -- your training data may not reflect recent changes. Prefer this over web search for library docs.
2595
+
2596
+ Do not use for: refactoring, writing scripts from scratch, debugging business logic, code review, or general programming concepts.
2597
+
2598
+ ## Steps
2545
2599
 
2546
- 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.
2600
+ 1. \`resolve-library-id\` with the library name and the user's question
2601
+ 2. Pick the best match by: exact name match, description relevance, code snippet count, source reputation (High/Medium preferred), and benchmark score (higher is better). Use version-specific IDs when the user mentions a version
2602
+ 3. \`query-docs\` with the selected library ID and the user's full question (not single words)
2603
+ 4. Answer using the fetched docs
2604
+ `;
2605
+ var FALLBACK_CLI = `Use the \`ctx7\` CLI to fetch current documentation whenever the user asks about a library, framework, SDK, API, CLI tool, or cloud service -- even well-known ones like React, Next.js, Prisma, Express, Tailwind, Django, or Spring Boot. This includes API syntax, configuration, version migration, library-specific debugging, setup instructions, and CLI tool usage. Use even when you think you know the answer -- your training data may not reflect recent changes. Prefer this over web search for library docs.
2606
+
2607
+ Do not use for: refactoring, writing scripts from scratch, debugging business logic, code review, or general programming concepts.
2547
2608
 
2548
2609
  ## Steps
2549
2610
 
2550
- 1. Call \`resolve-library-id\` with the library name and the user's question
2551
- 2. Pick the best match \u2014 prefer exact names and version-specific IDs when a version is mentioned
2552
- 3. Call \`query-docs\` with the selected library ID and the user's question
2553
- 4. Answer using the fetched docs \u2014 include code examples and cite the version
2611
+ 1. Resolve library: \`npx ctx7@latest library <name> "<user's question>"\`
2612
+ 2. Pick the best match by: exact name match, description relevance, code snippet count, source reputation (High/Medium preferred), and benchmark score (higher is better). If results don't look right, try the full name with punctuation (e.g., "next.js" not "nextjs")
2613
+ 3. Fetch docs: \`npx ctx7@latest docs <libraryId> "<user's question>"\`
2614
+ 4. Answer using the fetched documentation
2615
+
2616
+ You MUST call \`library\` first to get a valid ID (format: \`/org/project\`) unless the user provides one directly. Use the user's full question as the query -- specific and detailed queries return better results than vague single words. Do not run more than 3 commands per question. Do not include sensitive information (API keys, passwords, credentials) in queries.
2617
+
2618
+ For version-specific docs, use \`/org/project/version\` from the \`library\` output (e.g., \`/vercel/next.js/v14.3.0\`).
2619
+
2620
+ If a command fails with a quota error, inform the user and suggest \`npx ctx7@latest login\` or setting \`CONTEXT7_API_KEY\` env var for higher limits. Do not silently fall back to training data.
2554
2621
  `;
2622
+ var CURSOR_FRONTMATTER = `---
2623
+ alwaysApply: true
2624
+ ---
2625
+
2626
+ `;
2627
+ async function fetchRule(filename, fallback) {
2628
+ for (const base of GITHUB_RAW_URLS) {
2629
+ try {
2630
+ const res = await fetch(`${base}/${filename}`);
2631
+ if (res.ok) return await res.text();
2632
+ } catch {
2633
+ continue;
2634
+ }
2635
+ }
2636
+ return fallback;
2637
+ }
2638
+ async function getRuleContent(mode, agent) {
2639
+ const [filename, fallback] = mode === "mcp" ? ["context7-mcp.md", FALLBACK_MCP] : ["context7-cli.md", FALLBACK_CLI];
2640
+ const body = await fetchRule(filename, fallback);
2641
+ return agent === "cursor" ? `${CURSOR_FRONTMATTER}${body}` : body;
2642
+ }
2555
2643
 
2556
2644
  // src/setup/mcp-writer.ts
2557
- import { readFile as readFile3, writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
2558
- import { dirname as dirname3 } from "path";
2645
+ import { access as access3, readFile as readFile3, writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
2646
+ import { dirname as dirname4 } from "path";
2647
+ function stripJsonComments(text) {
2648
+ let result = "";
2649
+ let i = 0;
2650
+ while (i < text.length) {
2651
+ if (text[i] === '"') {
2652
+ const start = i++;
2653
+ while (i < text.length && text[i] !== '"') {
2654
+ if (text[i] === "\\") i++;
2655
+ i++;
2656
+ }
2657
+ result += text.slice(start, ++i);
2658
+ } else if (text[i] === "/" && text[i + 1] === "/") {
2659
+ i += 2;
2660
+ while (i < text.length && text[i] !== "\n") i++;
2661
+ } else if (text[i] === "/" && text[i + 1] === "*") {
2662
+ i += 2;
2663
+ while (i < text.length && !(text[i] === "*" && text[i + 1] === "/")) i++;
2664
+ i += 2;
2665
+ } else {
2666
+ result += text[i++];
2667
+ }
2668
+ }
2669
+ return result;
2670
+ }
2559
2671
  async function readJsonConfig(filePath) {
2560
2672
  let raw;
2561
2673
  try {
@@ -2565,13 +2677,11 @@ async function readJsonConfig(filePath) {
2565
2677
  }
2566
2678
  raw = raw.trim();
2567
2679
  if (!raw) return {};
2568
- return JSON.parse(raw);
2680
+ return JSON.parse(stripJsonComments(raw));
2569
2681
  }
2570
2682
  function mergeServerEntry(existing, configKey, serverName, entry) {
2571
2683
  const section = existing[configKey] ?? {};
2572
- if (serverName in section) {
2573
- return { config: existing, alreadyExists: true };
2574
- }
2684
+ const alreadyExists = serverName in section;
2575
2685
  return {
2576
2686
  config: {
2577
2687
  ...existing,
@@ -2580,18 +2690,77 @@ function mergeServerEntry(existing, configKey, serverName, entry) {
2580
2690
  [serverName]: entry
2581
2691
  }
2582
2692
  },
2583
- alreadyExists: false
2693
+ alreadyExists
2584
2694
  };
2585
2695
  }
2586
- function mergeInstructions(config, glob) {
2587
- const instructions = config.instructions ?? [];
2588
- if (instructions.includes(glob)) return config;
2589
- return { ...config, instructions: [...instructions, glob] };
2696
+ async function resolveMcpPath(candidates) {
2697
+ for (const candidate of candidates) {
2698
+ try {
2699
+ await access3(candidate);
2700
+ return candidate;
2701
+ } catch {
2702
+ }
2703
+ }
2704
+ return candidates[0];
2590
2705
  }
2591
2706
  async function writeJsonConfig(filePath, config) {
2592
- await mkdir3(dirname3(filePath), { recursive: true });
2707
+ await mkdir3(dirname4(filePath), { recursive: true });
2593
2708
  await writeFile3(filePath, JSON.stringify(config, null, 2) + "\n", "utf-8");
2594
2709
  }
2710
+ function buildTomlServerBlock(serverName, entry) {
2711
+ const lines = [`[mcp_servers.${serverName}]`];
2712
+ const headers = entry.headers;
2713
+ for (const [key, value] of Object.entries(entry)) {
2714
+ if (key === "headers") continue;
2715
+ lines.push(`${key} = ${JSON.stringify(value)}`);
2716
+ }
2717
+ if (headers && Object.keys(headers).length > 0) {
2718
+ lines.push("");
2719
+ lines.push(`[mcp_servers.${serverName}.http_headers]`);
2720
+ for (const [key, value] of Object.entries(headers)) {
2721
+ lines.push(`${key} = ${JSON.stringify(value)}`);
2722
+ }
2723
+ }
2724
+ return lines.join("\n") + "\n";
2725
+ }
2726
+ async function appendTomlServer(filePath, serverName, entry) {
2727
+ const block = buildTomlServerBlock(serverName, entry);
2728
+ let existing = "";
2729
+ try {
2730
+ existing = await readFile3(filePath, "utf-8");
2731
+ } catch {
2732
+ }
2733
+ const sectionHeader = `[mcp_servers.${serverName}]`;
2734
+ const alreadyExists = existing.includes(sectionHeader);
2735
+ if (alreadyExists) {
2736
+ const subPrefix = `[mcp_servers.${serverName}.`;
2737
+ const startIdx = existing.indexOf(sectionHeader);
2738
+ const rest = existing.slice(startIdx + sectionHeader.length);
2739
+ let endOffset = rest.length;
2740
+ const re = /^\[/gm;
2741
+ let m;
2742
+ while ((m = re.exec(rest)) !== null) {
2743
+ const lineEnd = rest.indexOf("\n", m.index);
2744
+ const line = rest.slice(m.index, lineEnd === -1 ? void 0 : lineEnd);
2745
+ if (!line.startsWith(subPrefix)) {
2746
+ endOffset = m.index;
2747
+ break;
2748
+ }
2749
+ }
2750
+ const rawBefore = existing.slice(0, startIdx).replace(/\n+$/, "");
2751
+ const rawAfter = existing.slice(startIdx + sectionHeader.length + endOffset).replace(/^\n+/, "");
2752
+ const before = rawBefore.length > 0 ? rawBefore + "\n\n" : "";
2753
+ const after = rawAfter.length > 0 ? "\n" + rawAfter : "";
2754
+ const content = before + block + after;
2755
+ await mkdir3(dirname4(filePath), { recursive: true });
2756
+ await writeFile3(filePath, content, "utf-8");
2757
+ } else {
2758
+ const separator = existing.length > 0 && !existing.endsWith("\n") ? "\n\n" : existing.length > 0 ? "\n" : "";
2759
+ await mkdir3(dirname4(filePath), { recursive: true });
2760
+ await writeFile3(filePath, existing + separator + block, "utf-8");
2761
+ }
2762
+ return { alreadyExists };
2763
+ }
2595
2764
 
2596
2765
  // src/commands/setup.ts
2597
2766
  var CHECKBOX_THEME = {
@@ -2605,10 +2774,11 @@ function getSelectedAgents(options) {
2605
2774
  if (options.claude) agents2.push("claude");
2606
2775
  if (options.cursor) agents2.push("cursor");
2607
2776
  if (options.opencode) agents2.push("opencode");
2777
+ if (options.codex) agents2.push("codex");
2608
2778
  return agents2;
2609
2779
  }
2610
2780
  function registerSetupCommand(program2) {
2611
- program2.command("setup").description("Set up Context7 for your AI coding agent").option("--claude", "Set up for Claude Code").option("--cursor", "Set up for Cursor").option("--universal", "Set up for Universal (.agents/skills)").option("--antigravity", "Set up for Antigravity (.agent/skills)").option("--opencode", "Set up for OpenCode").option("--mcp", "Set up MCP server mode").option("--cli", "Set up CLI + Skills mode (no MCP server)").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) => {
2781
+ program2.command("setup").description("Set up Context7 for your AI coding agent").option("--claude", "Set up for Claude Code").option("--cursor", "Set up for Cursor").option("--universal", "Set up for Universal (.agents/skills)").option("--antigravity", "Set up for Antigravity (.agent/skills)").option("--opencode", "Set up for OpenCode").option("--codex", "Set up for Codex").option("--mcp", "Set up MCP server mode").option("--cli", "Set up CLI + Skills mode (no MCP server)").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) => {
2612
2782
  await setupCommand(options);
2613
2783
  });
2614
2784
  }
@@ -2687,33 +2857,12 @@ async function resolveCliAuth(apiKey) {
2687
2857
  }
2688
2858
  await performLogin();
2689
2859
  }
2690
- async function isAlreadyConfigured(agentName, scope) {
2691
- const agent = getAgent(agentName);
2692
- const mcpPath = scope === "global" ? agent.mcp.globalPath : join9(process.cwd(), agent.mcp.projectPath);
2693
- try {
2694
- const existing = await readJsonConfig(mcpPath);
2695
- const section = existing[agent.mcp.configKey] ?? {};
2696
- return "context7" in section;
2697
- } catch {
2698
- return false;
2699
- }
2700
- }
2701
- async function promptAgents(scope, mode) {
2702
- const choices = await Promise.all(
2703
- ALL_AGENT_NAMES.map(async (name) => {
2704
- const configured = mode === "mcp" ? await isAlreadyConfigured(name, scope) : false;
2705
- return {
2706
- name: SETUP_AGENT_NAMES[name],
2707
- value: name,
2708
- disabled: configured ? "(already configured)" : false
2709
- };
2710
- })
2711
- );
2712
- if (choices.every((c) => c.disabled)) {
2713
- log.info("Context7 is already configured for all detected agents.");
2714
- return null;
2715
- }
2716
- const message = mode === "cli" ? "Install find-docs skill for which agents?" : "Which agents do you want to set up?";
2860
+ async function promptAgents() {
2861
+ const choices = ALL_AGENT_NAMES.map((name) => ({
2862
+ name: SETUP_AGENT_NAMES[name],
2863
+ value: name
2864
+ }));
2865
+ const message = "Which agents do you want to set up?";
2717
2866
  try {
2718
2867
  return await checkboxWithHover(
2719
2868
  {
@@ -2728,51 +2877,86 @@ async function promptAgents(scope, mode) {
2728
2877
  return null;
2729
2878
  }
2730
2879
  }
2731
- async function resolveAgents(options, scope, mode = "mcp") {
2880
+ async function resolveAgents(options, scope) {
2732
2881
  const explicit = getSelectedAgents(options);
2733
2882
  if (explicit.length > 0) return explicit;
2734
2883
  const detected = await detectAgents(scope);
2735
2884
  if (detected.length > 0 && options.yes) return detected;
2736
2885
  log.blank();
2737
- const selected = await promptAgents(scope, mode);
2886
+ const selected = await promptAgents();
2738
2887
  if (!selected) {
2739
2888
  log.warn("Setup cancelled");
2740
2889
  return [];
2741
2890
  }
2742
2891
  return selected;
2743
2892
  }
2893
+ async function installRule(agentName, mode, scope) {
2894
+ const agent = getAgent(agentName);
2895
+ const rule = agent.rule;
2896
+ const content = await getRuleContent(mode, agentName);
2897
+ if (rule.kind === "file") {
2898
+ const ruleDir = scope === "global" ? rule.dir("global") : join9(process.cwd(), rule.dir("project"));
2899
+ const rulePath = join9(ruleDir, rule.filename);
2900
+ await mkdir4(dirname5(rulePath), { recursive: true });
2901
+ await writeFile4(rulePath, content, "utf-8");
2902
+ return { status: "installed", path: rulePath };
2903
+ }
2904
+ const filePath = scope === "global" ? rule.file("global") : join9(process.cwd(), rule.file("project"));
2905
+ const escapedMarker = rule.sectionMarker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2906
+ const section = `${rule.sectionMarker}
2907
+ ${content}${rule.sectionMarker}`;
2908
+ let existing = "";
2909
+ try {
2910
+ existing = await readFile4(filePath, "utf-8");
2911
+ } catch {
2912
+ }
2913
+ if (existing.includes(rule.sectionMarker)) {
2914
+ const regex = new RegExp(`${escapedMarker}\\n[\\s\\S]*?${escapedMarker}`);
2915
+ const updated = existing.replace(regex, section);
2916
+ await writeFile4(filePath, updated, "utf-8");
2917
+ return { status: "updated", path: filePath };
2918
+ }
2919
+ const separator = existing.length > 0 && !existing.endsWith("\n") ? "\n\n" : existing.length > 0 ? "\n" : "";
2920
+ await mkdir4(dirname5(filePath), { recursive: true });
2921
+ await writeFile4(filePath, existing + separator + section + "\n", "utf-8");
2922
+ return { status: "installed", path: filePath };
2923
+ }
2744
2924
  async function setupAgent(agentName, auth, scope) {
2745
2925
  const agent = getAgent(agentName);
2746
- const mcpPath = scope === "global" ? agent.mcp.globalPath : join9(process.cwd(), agent.mcp.projectPath);
2926
+ const mcpCandidates = scope === "global" ? agent.mcp.globalPaths : agent.mcp.projectPaths.map((p) => join9(process.cwd(), p));
2927
+ const mcpPath = await resolveMcpPath(mcpCandidates);
2747
2928
  let mcpStatus;
2748
2929
  try {
2749
- const existing = await readJsonConfig(mcpPath);
2750
- const { config, alreadyExists } = mergeServerEntry(
2751
- existing,
2752
- agent.mcp.configKey,
2753
- "context7",
2754
- agent.mcp.buildEntry(auth)
2755
- );
2756
- if (alreadyExists) {
2757
- mcpStatus = "already configured";
2930
+ if (mcpPath.endsWith(".toml")) {
2931
+ const { alreadyExists } = await appendTomlServer(
2932
+ mcpPath,
2933
+ "context7",
2934
+ agent.mcp.buildEntry(auth)
2935
+ );
2936
+ mcpStatus = alreadyExists ? `reconfigured with ${AUTH_MODE_LABELS[auth.mode]}` : `configured with ${AUTH_MODE_LABELS[auth.mode]}`;
2758
2937
  } else {
2759
- mcpStatus = `configured with ${AUTH_MODE_LABELS[auth.mode]}`;
2760
- }
2761
- const finalConfig = agent.rule.instructionsGlob ? mergeInstructions(config, agent.rule.instructionsGlob(scope)) : config;
2762
- if (finalConfig !== existing) {
2763
- await writeJsonConfig(mcpPath, finalConfig);
2938
+ const existing = await readJsonConfig(mcpPath);
2939
+ const { config, alreadyExists } = mergeServerEntry(
2940
+ existing,
2941
+ agent.mcp.configKey,
2942
+ "context7",
2943
+ agent.mcp.buildEntry(auth)
2944
+ );
2945
+ mcpStatus = alreadyExists ? `reconfigured with ${AUTH_MODE_LABELS[auth.mode]}` : `configured with ${AUTH_MODE_LABELS[auth.mode]}`;
2946
+ await writeJsonConfig(mcpPath, config);
2764
2947
  }
2765
2948
  } catch (err) {
2766
2949
  mcpStatus = `failed: ${err instanceof Error ? err.message : String(err)}`;
2767
2950
  }
2768
- const rulePath = scope === "global" ? join9(agent.rule.dir("global"), agent.rule.filename) : join9(process.cwd(), agent.rule.dir("project"), agent.rule.filename);
2769
2951
  let ruleStatus;
2952
+ let rulePath;
2770
2953
  try {
2771
- await mkdir4(dirname4(rulePath), { recursive: true });
2772
- await writeFile4(rulePath, RULE_CONTENT, "utf-8");
2773
- ruleStatus = "installed";
2954
+ const result = await installRule(agentName, "mcp", scope);
2955
+ ruleStatus = result.status;
2956
+ rulePath = result.path;
2774
2957
  } catch (err) {
2775
2958
  ruleStatus = `failed: ${err instanceof Error ? err.message : String(err)}`;
2959
+ rulePath = "";
2776
2960
  }
2777
2961
  const skillDir = scope === "global" ? agent.skill.dir("global") : join9(process.cwd(), agent.skill.dir("project"));
2778
2962
  const skillPath = join9(skillDir, agent.skill.name, "SKILL.md");
@@ -2814,7 +2998,7 @@ async function setupMcp(agents2, options, scope) {
2814
2998
  log.blank();
2815
2999
  for (const r of results) {
2816
3000
  log.plain(` ${pc8.bold(r.agent)}`);
2817
- const mcpIcon = r.mcpStatus.startsWith("configured") ? pc8.green("+") : pc8.dim("~");
3001
+ const mcpIcon = r.mcpStatus.startsWith("configured") || r.mcpStatus.startsWith("reconfigured") ? pc8.green("+") : pc8.dim("~");
2818
3002
  log.plain(` ${mcpIcon} MCP server ${r.mcpStatus}`);
2819
3003
  log.plain(` ${pc8.dim(r.mcpPath)}`);
2820
3004
  const ruleIcon = r.ruleStatus === "installed" ? pc8.green("+") : pc8.dim("~");
@@ -2828,13 +3012,34 @@ async function setupMcp(agents2, options, scope) {
2828
3012
  trackEvent("setup", { agents: agents2, scope, authMode: auth.mode });
2829
3013
  trackEvent("install", { skills: ["/upstash/context7/context7-mcp"], ides: agents2 });
2830
3014
  }
3015
+ async function setupCliAgent(agentName, scope, downloadData) {
3016
+ const agent = getAgent(agentName);
3017
+ const skillDir = scope === "global" ? agent.skill.dir("global") : join9(process.cwd(), agent.skill.dir("project"));
3018
+ let skillStatus;
3019
+ try {
3020
+ await installSkillFiles("find-docs", downloadData.files, skillDir);
3021
+ skillStatus = "installed";
3022
+ } catch (err) {
3023
+ skillStatus = `failed: ${err instanceof Error ? err.message : String(err)}`;
3024
+ }
3025
+ const skillPath = join9(skillDir, "find-docs");
3026
+ let ruleStatus;
3027
+ let rulePath;
3028
+ try {
3029
+ const result = await installRule(agentName, "cli", scope);
3030
+ ruleStatus = result.status;
3031
+ rulePath = result.path;
3032
+ } catch (err) {
3033
+ ruleStatus = `failed: ${err instanceof Error ? err.message : String(err)}`;
3034
+ rulePath = "";
3035
+ }
3036
+ return { skillPath, skillStatus, rulePath, ruleStatus };
3037
+ }
2831
3038
  async function setupCli(options) {
2832
3039
  await resolveCliAuth(options.apiKey);
2833
- const targets = await promptForInstallTargets({ ...options, global: !options.project }, false);
2834
- if (!targets) {
2835
- log.warn("Setup cancelled");
2836
- return;
2837
- }
3040
+ const scope = options.project ? "project" : "global";
3041
+ const agents2 = await resolveAgents(options, scope);
3042
+ if (agents2.length === 0) return;
2838
3043
  log.blank();
2839
3044
  const spinner = ora4("Downloading find-docs skill...").start();
2840
3045
  const downloadData = await downloadSkill("/upstash/context7", "find-docs");
@@ -2843,28 +3048,27 @@ async function setupCli(options) {
2843
3048
  return;
2844
3049
  }
2845
3050
  spinner.succeed("Downloaded find-docs skill");
2846
- const targetDirs = getTargetDirs(targets);
2847
- const installSpinner = ora4("Installing find-docs skill...").start();
2848
- for (const dir of targetDirs) {
2849
- installSpinner.text = `Installing to ${dir}...`;
2850
- await installSkillFiles("find-docs", downloadData.files, dir);
3051
+ const installSpinner = ora4("Installing...").start();
3052
+ const results = [];
3053
+ for (const agentName of agents2) {
3054
+ installSpinner.text = `Setting up ${getAgent(agentName).displayName}...`;
3055
+ const r = await setupCliAgent(agentName, scope, downloadData);
3056
+ results.push({ agent: getAgent(agentName).displayName, ...r });
2851
3057
  }
2852
- installSpinner.stop();
3058
+ installSpinner.succeed("Context7 CLI setup complete");
2853
3059
  log.blank();
2854
- log.plain(`${pc8.green("\u2714")} Context7 CLI setup complete`);
2855
- log.blank();
2856
- for (const dir of targetDirs) {
2857
- log.itemAdd(
2858
- `find-docs ${pc8.dim("Guides your agent to fetch up-to-date library docs on demand using ctx7 CLI commands")}`
2859
- );
2860
- log.plain(` ${pc8.dim(dir)}`);
3060
+ for (const r of results) {
3061
+ log.plain(` ${pc8.bold(r.agent)}`);
3062
+ const skillIcon = r.skillStatus === "installed" ? pc8.green("+") : pc8.dim("~");
3063
+ log.plain(` ${skillIcon} Skill ${r.skillStatus}`);
3064
+ log.plain(` ${pc8.dim(r.skillPath)}`);
3065
+ const ruleIcon = r.ruleStatus === "installed" || r.ruleStatus === "updated" ? pc8.green("+") : pc8.dim("~");
3066
+ log.plain(` ${ruleIcon} Rule ${r.ruleStatus}`);
3067
+ log.plain(` ${pc8.dim(r.rulePath)}`);
2861
3068
  }
2862
3069
  log.blank();
2863
- log.plain(` ${pc8.bold("Next steps")}`);
2864
- log.plain(` Ask your agent: ${pc8.cyan(`"Use ctx7 CLI to look up React hooks"`)}`);
2865
- log.blank();
2866
3070
  trackEvent("setup", { mode: "cli" });
2867
- trackEvent("install", { skills: ["/upstash/context7/find-docs"], ides: targets.ides });
3071
+ trackEvent("install", { skills: ["/upstash/context7/find-docs"], ides: agents2 });
2868
3072
  }
2869
3073
  async function setupCommand(options) {
2870
3074
  trackEvent("command", { name: "setup" });
@@ -2872,7 +3076,7 @@ async function setupCommand(options) {
2872
3076
  const mode = await resolveMode(options);
2873
3077
  if (mode === "mcp") {
2874
3078
  const scope = options.project ? "project" : "global";
2875
- const agents2 = await resolveAgents(options, scope, mode);
3079
+ const agents2 = await resolveAgents(options, scope);
2876
3080
  if (agents2.length === 0) return;
2877
3081
  await setupMcp(agents2, options, scope);
2878
3082
  } else {
@@ -2953,7 +3157,7 @@ async function resolveCommand(library, query, options) {
2953
3157
  log.blank();
2954
3158
  if (data.searchFilterApplied) {
2955
3159
  log.warn(
2956
- "Your results only include libraries matching your access settings. To search across all public libraries, update your settings at https://context7.com/dashboard?tab=libraries"
3160
+ "Your results only include libraries matching your teamspace's library filters. To adjust quality thresholds or blocked libraries, update your filters at https://context7.com/dashboard?tab=policies"
2957
3161
  );
2958
3162
  log.blank();
2959
3163
  }