ctx7 0.3.5 → 0.3.8

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
@@ -1,4 +1,11 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ appendTomlServer,
4
+ mergeServerEntry,
5
+ readJsonConfig,
6
+ resolveMcpPath,
7
+ writeJsonConfig
8
+ } from "./chunk-WKOIWR6Y.js";
2
9
 
3
10
  // src/index.ts
4
11
  import { Command } from "commander";
@@ -103,6 +110,10 @@ async function downloadSkillFromGitHub(skill) {
103
110
  }
104
111
  const content = await fileResponse.text();
105
112
  const relativePath = item.path.slice(skillPath.length + 1);
113
+ if (relativePath.includes("..")) {
114
+ console.warn(`Skipping file with unsafe path: ${item.path}`);
115
+ continue;
116
+ }
106
117
  files.push({
107
118
  path: relativePath,
108
119
  content
@@ -123,6 +134,7 @@ var __dirname = dirname(fileURLToPath(import.meta.url));
123
134
  var pkg = JSON.parse(readFileSync(join(__dirname, "../package.json"), "utf-8"));
124
135
  var VERSION = pkg.version;
125
136
  var NAME = pkg.name;
137
+ var CLI_CLIENT_ID = "2veBSofhicRBguUT";
126
138
 
127
139
  // src/utils/api.ts
128
140
  var baseUrl = "https://context7.com";
@@ -432,13 +444,18 @@ async function checkboxWithHover(config, options) {
432
444
  );
433
445
  const values = choices.map((c) => c.value);
434
446
  const totalItems = values.length;
435
- let cursorPosition = 0;
447
+ let cursorPosition = choices.findIndex((c) => !c.disabled);
448
+ if (cursorPosition < 0) cursorPosition = 0;
436
449
  const getName = options?.getName ?? ((v) => v.name);
437
450
  const keypressHandler = (_str, key) => {
438
- if (key.name === "up" && cursorPosition > 0) {
439
- cursorPosition--;
440
- } else if (key.name === "down" && cursorPosition < totalItems - 1) {
441
- cursorPosition++;
451
+ if (key.name === "up") {
452
+ let next = cursorPosition - 1;
453
+ while (next >= 0 && choices[next].disabled) next--;
454
+ if (next >= 0) cursorPosition = next;
455
+ } else if (key.name === "down") {
456
+ let next = cursorPosition + 1;
457
+ while (next < totalItems && choices[next].disabled) next++;
458
+ if (next < totalItems) cursorPosition = next;
442
459
  }
443
460
  };
444
461
  readline.emitKeypressEvents(process.stdin);
@@ -482,7 +499,7 @@ var IDE_GLOBAL_PATHS = {
482
499
  claude: ".claude/skills",
483
500
  cursor: ".cursor/skills",
484
501
  antigravity: ".agent/skills",
485
- universal: ".config/agents/skills"
502
+ universal: ".agents/skills"
486
503
  };
487
504
  var IDE_NAMES = {
488
505
  claude: "Claude Code",
@@ -491,7 +508,7 @@ var IDE_NAMES = {
491
508
  universal: "Universal"
492
509
  };
493
510
  var UNIVERSAL_SKILLS_PATH = ".agents/skills";
494
- var UNIVERSAL_SKILLS_GLOBAL_PATH = ".config/agents/skills";
511
+ var UNIVERSAL_SKILLS_GLOBAL_PATH = ".agents/skills";
495
512
  var UNIVERSAL_AGENTS_LABEL = "Amp, Codex, Gemini CLI, GitHub Copilot, OpenCode + more";
496
513
  var VENDOR_SPECIFIC_AGENTS = ["claude", "cursor", "antigravity"];
497
514
  var DEFAULT_CONFIG = {
@@ -714,12 +731,15 @@ function getTargetDirFromSelection(ide, scope) {
714
731
 
715
732
  // src/utils/installer.ts
716
733
  import { mkdir, writeFile, rm, symlink, lstat } from "fs/promises";
717
- import { join as join3 } from "path";
734
+ import { join as join3, resolve, dirname as dirname3 } from "path";
718
735
  async function installSkillFiles(skillName, files, targetDir) {
719
- const skillDir = join3(targetDir, skillName);
736
+ const skillDir = resolve(targetDir, skillName);
720
737
  for (const file of files) {
721
- const filePath = join3(skillDir, file.path);
722
- const fileDir = join3(filePath, "..");
738
+ const filePath = resolve(skillDir, file.path);
739
+ if (!filePath.startsWith(skillDir + "/") && filePath !== skillDir) {
740
+ throw new Error(`Skill file path "${file.path}" resolves outside the target directory`);
741
+ }
742
+ const fileDir = dirname3(filePath);
723
743
  await mkdir(fileDir, { recursive: true });
724
744
  await writeFile(filePath, file.content);
725
745
  }
@@ -810,17 +830,50 @@ function isTokenExpired(tokens) {
810
830
  }
811
831
  return Date.now() > tokens.expires_at - 6e4;
812
832
  }
833
+ async function refreshAccessToken(refreshToken) {
834
+ const response = await fetch(`${getBaseUrl()}/api/oauth/token`, {
835
+ method: "POST",
836
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
837
+ body: new URLSearchParams({
838
+ grant_type: "refresh_token",
839
+ client_id: CLI_CLIENT_ID,
840
+ refresh_token: refreshToken
841
+ }).toString()
842
+ });
843
+ if (!response.ok) {
844
+ const err = await response.json().catch(() => ({}));
845
+ throw new Error(err.error_description || err.error || "Failed to refresh token");
846
+ }
847
+ return await response.json();
848
+ }
849
+ async function getValidAccessToken() {
850
+ const tokens = loadTokens();
851
+ if (!tokens) return null;
852
+ if (!isTokenExpired(tokens)) {
853
+ return tokens.access_token;
854
+ }
855
+ if (!tokens.refresh_token) {
856
+ return null;
857
+ }
858
+ try {
859
+ const newTokens = await refreshAccessToken(tokens.refresh_token);
860
+ saveTokens(newTokens);
861
+ return newTokens.access_token;
862
+ } catch {
863
+ return null;
864
+ }
865
+ }
813
866
  var CALLBACK_PORT = 52417;
814
867
  function createCallbackServer(expectedState) {
815
868
  let resolvePort;
816
869
  let resolveResult;
817
870
  let rejectResult;
818
871
  let serverInstance = null;
819
- const portPromise = new Promise((resolve) => {
820
- resolvePort = resolve;
872
+ const portPromise = new Promise((resolve2) => {
873
+ resolvePort = resolve2;
821
874
  });
822
- const resultPromise = new Promise((resolve, reject) => {
823
- resolveResult = resolve;
875
+ const resultPromise = new Promise((resolve2, reject) => {
876
+ resolveResult = resolve2;
824
877
  rejectResult = reject;
825
878
  });
826
879
  const server = http.createServer((req, res) => {
@@ -953,7 +1006,6 @@ function buildAuthorizationUrl(baseUrl3, clientId, redirectUri, codeChallenge, s
953
1006
  import pc4 from "picocolors";
954
1007
  import ora from "ora";
955
1008
  import open from "open";
956
- var CLI_CLIENT_ID = "2veBSofhicRBguUT";
957
1009
  var baseUrl2 = "https://context7.com";
958
1010
  function setAuthBaseUrl(url) {
959
1011
  baseUrl2 = url;
@@ -1029,18 +1081,13 @@ async function performLogin(openBrowser = true) {
1029
1081
  }
1030
1082
  async function loginCommand(options) {
1031
1083
  trackEvent("command", { name: "login" });
1032
- const existingTokens = loadTokens();
1033
- if (existingTokens) {
1034
- const expired = isTokenExpired(existingTokens);
1035
- if (!expired || existingTokens.refresh_token) {
1036
- console.log(pc4.yellow("You are already logged in."));
1037
- console.log(
1038
- pc4.dim("Run 'ctx7 logout' first if you want to log in with a different account.")
1039
- );
1040
- return;
1041
- }
1042
- clearTokens();
1084
+ const existingToken = await getValidAccessToken();
1085
+ if (existingToken) {
1086
+ console.log(pc4.yellow("You are already logged in."));
1087
+ console.log(pc4.dim("Run 'ctx7 logout' first if you want to log in with a different account."));
1088
+ return;
1043
1089
  }
1090
+ clearTokens();
1044
1091
  const token = await performLogin(options.browser);
1045
1092
  if (!token) {
1046
1093
  process.exit(1);
@@ -1058,29 +1105,30 @@ function logoutCommand() {
1058
1105
  }
1059
1106
  async function whoamiCommand() {
1060
1107
  trackEvent("command", { name: "whoami" });
1061
- const tokens = loadTokens();
1062
- if (!tokens) {
1108
+ const accessToken = await getValidAccessToken();
1109
+ if (!accessToken) {
1063
1110
  console.log(pc4.yellow("Not logged in."));
1064
1111
  console.log(pc4.dim("Run 'ctx7 login' to authenticate."));
1065
1112
  return;
1066
1113
  }
1067
1114
  console.log(pc4.green("Logged in"));
1068
1115
  try {
1069
- const userInfo = await fetchUserInfo(tokens.access_token);
1070
- if (userInfo.name) {
1071
- console.log(`${pc4.dim("Name:".padEnd(9))}${userInfo.name}`);
1116
+ const whoami = await fetchWhoami(accessToken);
1117
+ if (whoami.name) {
1118
+ console.log(`${pc4.dim("Name:".padEnd(13))}${whoami.name}`);
1072
1119
  }
1073
- if (userInfo.email) {
1074
- console.log(`${pc4.dim("Email:".padEnd(9))}${userInfo.email}`);
1120
+ if (whoami.email) {
1121
+ console.log(`${pc4.dim("Email:".padEnd(13))}${whoami.email}`);
1075
1122
  }
1076
- } catch {
1077
- if (isTokenExpired(tokens) && !tokens.refresh_token) {
1078
- console.log(pc4.dim("(Session may be expired - run 'ctx7 login' to refresh)"));
1123
+ if (whoami.teamspace) {
1124
+ console.log(`${pc4.dim("Teamspace:".padEnd(13))}${whoami.teamspace.name}`);
1079
1125
  }
1126
+ } catch {
1127
+ console.log(pc4.dim("(Session may be expired - run 'ctx7 login' to refresh)"));
1080
1128
  }
1081
1129
  }
1082
- async function fetchUserInfo(accessToken) {
1083
- const response = await fetch("https://clerk.context7.com/oauth/userinfo", {
1130
+ async function fetchWhoami(accessToken) {
1131
+ const response = await fetch(`${getBaseUrl()}/api/dashboard/whoami`, {
1084
1132
  headers: {
1085
1133
  Authorization: `Bearer ${accessToken}`
1086
1134
  }
@@ -1288,7 +1336,7 @@ async function generateCommand(options) {
1288
1336
  log.blank();
1289
1337
  if (searchResult.searchFilterApplied) {
1290
1338
  log.warn(
1291
- "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"
1339
+ "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"
1292
1340
  );
1293
1341
  log.blank();
1294
1342
  }
@@ -1513,12 +1561,11 @@ async function generateCommand(options) {
1513
1561
  previewFileWritten = true;
1514
1562
  }
1515
1563
  const editor = process.env.EDITOR || "open";
1516
- await new Promise((resolve) => {
1564
+ await new Promise((resolve2) => {
1517
1565
  const child = spawn(editor, [previewFile], {
1518
- stdio: "inherit",
1519
- shell: true
1566
+ stdio: "inherit"
1520
1567
  });
1521
- child.on("close", () => resolve());
1568
+ child.on("close", () => resolve2());
1522
1569
  });
1523
1570
  };
1524
1571
  const syncFromPreviewFile = async () => {
@@ -1527,7 +1574,7 @@ async function generateCommand(options) {
1527
1574
  }
1528
1575
  };
1529
1576
  showPreview();
1530
- await new Promise((resolve) => setTimeout(resolve, 100));
1577
+ await new Promise((resolve2) => setTimeout(resolve2, 100));
1531
1578
  try {
1532
1579
  let action;
1533
1580
  while (true) {
@@ -2386,7 +2433,7 @@ ${headerLine}`,
2386
2433
  import pc8 from "picocolors";
2387
2434
  import ora4 from "ora";
2388
2435
  import { select as select3 } from "@inquirer/prompts";
2389
- import { mkdir as mkdir4, writeFile as writeFile4 } from "fs/promises";
2436
+ import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
2390
2437
  import { dirname as dirname4, join as join9 } from "path";
2391
2438
  import { randomBytes as randomBytes2 } from "crypto";
2392
2439
 
@@ -2397,7 +2444,8 @@ import { homedir as homedir5 } from "os";
2397
2444
  var SETUP_AGENT_NAMES = {
2398
2445
  claude: "Claude Code",
2399
2446
  cursor: "Cursor",
2400
- opencode: "OpenCode"
2447
+ opencode: "OpenCode",
2448
+ codex: "Codex"
2401
2449
  };
2402
2450
  var AUTH_MODE_LABELS = {
2403
2451
  oauth: "OAuth",
@@ -2418,12 +2466,13 @@ var agents = {
2418
2466
  name: "claude",
2419
2467
  displayName: "Claude Code",
2420
2468
  mcp: {
2421
- projectPath: ".mcp.json",
2422
- globalPath: join8(homedir5(), ".claude.json"),
2469
+ projectPaths: [".mcp.json"],
2470
+ globalPaths: [join8(homedir5(), ".claude.json")],
2423
2471
  configKey: "mcpServers",
2424
2472
  buildEntry: (auth) => withHeaders({ type: "http", url: mcpUrl(auth) }, auth)
2425
2473
  },
2426
2474
  rule: {
2475
+ kind: "file",
2427
2476
  dir: (scope) => scope === "global" ? join8(homedir5(), ".claude", "rules") : join8(".claude", "rules"),
2428
2477
  filename: "context7.md"
2429
2478
  },
@@ -2440,12 +2489,13 @@ var agents = {
2440
2489
  name: "cursor",
2441
2490
  displayName: "Cursor",
2442
2491
  mcp: {
2443
- projectPath: join8(".cursor", "mcp.json"),
2444
- globalPath: join8(homedir5(), ".cursor", "mcp.json"),
2492
+ projectPaths: [join8(".cursor", "mcp.json")],
2493
+ globalPaths: [join8(homedir5(), ".cursor", "mcp.json")],
2445
2494
  configKey: "mcpServers",
2446
2495
  buildEntry: (auth) => withHeaders({ url: mcpUrl(auth) }, auth)
2447
2496
  },
2448
2497
  rule: {
2498
+ kind: "file",
2449
2499
  dir: (scope) => scope === "global" ? join8(homedir5(), ".cursor", "rules") : join8(".cursor", "rules"),
2450
2500
  filename: "context7.mdc"
2451
2501
  },
@@ -2462,24 +2512,58 @@ var agents = {
2462
2512
  name: "opencode",
2463
2513
  displayName: "OpenCode",
2464
2514
  mcp: {
2465
- projectPath: ".opencode.json",
2466
- globalPath: join8(homedir5(), ".config", "opencode", "opencode.json"),
2515
+ projectPaths: ["opencode.json", "opencode.jsonc", ".opencode.json", ".opencode.jsonc"],
2516
+ globalPaths: [
2517
+ join8(homedir5(), ".config", "opencode", "opencode.json"),
2518
+ join8(homedir5(), ".config", "opencode", "opencode.jsonc"),
2519
+ join8(homedir5(), ".config", "opencode", ".opencode.json"),
2520
+ join8(homedir5(), ".config", "opencode", ".opencode.jsonc")
2521
+ ],
2467
2522
  configKey: "mcp",
2468
2523
  buildEntry: (auth) => withHeaders({ type: "remote", url: mcpUrl(auth), enabled: true }, auth)
2469
2524
  },
2470
2525
  rule: {
2471
- dir: (scope) => scope === "global" ? join8(homedir5(), ".config", "opencode", "rules") : join8(".opencode", "rules"),
2472
- filename: "context7.md",
2473
- instructionsGlob: (scope) => scope === "global" ? join8(homedir5(), ".config", "opencode", "rules", "*.md") : ".opencode/rules/*.md"
2526
+ kind: "append",
2527
+ file: (scope) => scope === "global" ? join8(homedir5(), ".config", "opencode", "AGENTS.md") : "AGENTS.md",
2528
+ sectionMarker: "<!-- context7 -->"
2474
2529
  },
2475
2530
  skill: {
2476
2531
  name: "context7-mcp",
2477
2532
  dir: (scope) => scope === "global" ? join8(homedir5(), ".agents", "skills") : join8(".agents", "skills")
2478
2533
  },
2479
2534
  detect: {
2480
- projectPaths: [".opencode.json"],
2535
+ projectPaths: ["opencode.json", "opencode.jsonc", ".opencode.json", ".opencode.jsonc"],
2481
2536
  globalPaths: [join8(homedir5(), ".config", "opencode")]
2482
2537
  }
2538
+ },
2539
+ codex: {
2540
+ name: "codex",
2541
+ displayName: "Codex",
2542
+ mcp: {
2543
+ projectPaths: [join8(".codex", "config.toml")],
2544
+ globalPaths: [join8(homedir5(), ".codex", "config.toml")],
2545
+ configKey: "mcp_servers",
2546
+ buildEntry: (auth) => {
2547
+ const entry = { type: "http", url: mcpUrl(auth) };
2548
+ if (auth.mode === "api-key" && auth.apiKey) {
2549
+ entry.headers = { CONTEXT7_API_KEY: auth.apiKey };
2550
+ }
2551
+ return entry;
2552
+ }
2553
+ },
2554
+ rule: {
2555
+ kind: "append",
2556
+ file: (scope) => scope === "global" ? join8(homedir5(), ".codex", "AGENTS.md") : "AGENTS.md",
2557
+ sectionMarker: "<!-- context7 -->"
2558
+ },
2559
+ skill: {
2560
+ name: "context7-mcp",
2561
+ dir: (scope) => scope === "global" ? join8(homedir5(), ".agents", "skills") : join8(".agents", "skills")
2562
+ },
2563
+ detect: {
2564
+ projectPaths: [".codex"],
2565
+ globalPaths: [join8(homedir5(), ".codex")]
2566
+ }
2483
2567
  }
2484
2568
  };
2485
2569
  function getAgent(name) {
@@ -2510,58 +2594,58 @@ async function detectAgents(scope) {
2510
2594
  }
2511
2595
 
2512
2596
  // src/setup/templates.ts
2513
- var RULE_CONTENT = `---
2514
- alwaysApply: true
2515
- ---
2597
+ var GITHUB_RAW_URLS = [
2598
+ "https://raw.githubusercontent.com/upstash/context7/master/rules",
2599
+ "https://raw.githubusercontent.com/upstash/context7/main/rules"
2600
+ ];
2601
+ 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.
2516
2602
 
2517
- 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.
2603
+ Do not use for: refactoring, writing scripts from scratch, debugging business logic, code review, or general programming concepts.
2518
2604
 
2519
2605
  ## Steps
2520
2606
 
2521
- 1. Call \`resolve-library-id\` with the library name and the user's question
2522
- 2. Pick the best match \u2014 prefer exact names and version-specific IDs when a version is mentioned
2523
- 3. Call \`query-docs\` with the selected library ID and the user's question
2524
- 4. Answer using the fetched docs \u2014 include code examples and cite the version
2607
+ 1. \`resolve-library-id\` with the library name and the user's question
2608
+ 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
2609
+ 3. \`query-docs\` with the selected library ID and the user's full question (not single words)
2610
+ 4. Answer using the fetched docs
2525
2611
  `;
2612
+ 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.
2526
2613
 
2527
- // src/setup/mcp-writer.ts
2528
- import { readFile as readFile3, writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
2529
- import { dirname as dirname3 } from "path";
2530
- async function readJsonConfig(filePath) {
2531
- let raw;
2532
- try {
2533
- raw = await readFile3(filePath, "utf-8");
2534
- } catch {
2535
- return {};
2536
- }
2537
- raw = raw.trim();
2538
- if (!raw) return {};
2539
- return JSON.parse(raw);
2540
- }
2541
- function mergeServerEntry(existing, configKey, serverName, entry) {
2542
- const section = existing[configKey] ?? {};
2543
- if (serverName in section) {
2544
- return { config: existing, alreadyExists: true };
2614
+ Do not use for: refactoring, writing scripts from scratch, debugging business logic, code review, or general programming concepts.
2615
+
2616
+ ## Steps
2617
+
2618
+ 1. Resolve library: \`npx ctx7@latest library <name> "<user's question>"\`
2619
+ 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")
2620
+ 3. Fetch docs: \`npx ctx7@latest docs <libraryId> "<user's question>"\`
2621
+ 4. Answer using the fetched documentation
2622
+
2623
+ 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.
2624
+
2625
+ For version-specific docs, use \`/org/project/version\` from the \`library\` output (e.g., \`/vercel/next.js/v14.3.0\`).
2626
+
2627
+ 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.
2628
+ `;
2629
+ var CURSOR_FRONTMATTER = `---
2630
+ alwaysApply: true
2631
+ ---
2632
+
2633
+ `;
2634
+ async function fetchRule(filename, fallback) {
2635
+ for (const base of GITHUB_RAW_URLS) {
2636
+ try {
2637
+ const res = await fetch(`${base}/${filename}`);
2638
+ if (res.ok) return await res.text();
2639
+ } catch {
2640
+ continue;
2641
+ }
2545
2642
  }
2546
- return {
2547
- config: {
2548
- ...existing,
2549
- [configKey]: {
2550
- ...section,
2551
- [serverName]: entry
2552
- }
2553
- },
2554
- alreadyExists: false
2555
- };
2556
- }
2557
- function mergeInstructions(config, glob) {
2558
- const instructions = config.instructions ?? [];
2559
- if (instructions.includes(glob)) return config;
2560
- return { ...config, instructions: [...instructions, glob] };
2643
+ return fallback;
2561
2644
  }
2562
- async function writeJsonConfig(filePath, config) {
2563
- await mkdir3(dirname3(filePath), { recursive: true });
2564
- await writeFile3(filePath, JSON.stringify(config, null, 2) + "\n", "utf-8");
2645
+ async function getRuleContent(mode, agent) {
2646
+ const [filename, fallback] = mode === "mcp" ? ["context7-mcp.md", FALLBACK_MCP] : ["context7-cli.md", FALLBACK_CLI];
2647
+ const body = await fetchRule(filename, fallback);
2648
+ return agent === "cursor" ? `${CURSOR_FRONTMATTER}${body}` : body;
2565
2649
  }
2566
2650
 
2567
2651
  // src/commands/setup.ts
@@ -2576,16 +2660,16 @@ function getSelectedAgents(options) {
2576
2660
  if (options.claude) agents2.push("claude");
2577
2661
  if (options.cursor) agents2.push("cursor");
2578
2662
  if (options.opencode) agents2.push("opencode");
2663
+ if (options.codex) agents2.push("codex");
2579
2664
  return agents2;
2580
2665
  }
2581
2666
  function registerSetupCommand(program2) {
2582
- 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) => {
2667
+ 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) => {
2583
2668
  await setupCommand(options);
2584
2669
  });
2585
2670
  }
2586
2671
  async function authenticateAndGenerateKey() {
2587
- const existingTokens = loadTokens();
2588
- const accessToken = existingTokens && !isTokenExpired(existingTokens) ? existingTokens.access_token : await performLogin();
2672
+ const accessToken = await getValidAccessToken() ?? await performLogin();
2589
2673
  if (!accessToken) return null;
2590
2674
  const spinner = ora4("Configuring authentication...").start();
2591
2675
  try {
@@ -2625,15 +2709,15 @@ async function resolveMode(options) {
2625
2709
  return select3({
2626
2710
  message: "How should your agent access Context7?",
2627
2711
  choices: [
2628
- {
2629
- name: `CLI + Skills
2630
- ${pc8.dim("Installs a find-docs skill that guides your agent to fetch up-to-date library docs using ")}${pc8.dim(pc8.bold("ctx7"))}${pc8.dim(" CLI commands")}`,
2631
- value: "cli"
2632
- },
2633
2712
  {
2634
2713
  name: `MCP server
2635
2714
  ${pc8.dim("Agent calls Context7 tools via MCP protocol to retrieve up-to-date library docs")}`,
2636
2715
  value: "mcp"
2716
+ },
2717
+ {
2718
+ name: `CLI + Skills
2719
+ ${pc8.dim("Installs a find-docs skill that guides your agent to fetch up-to-date library docs using ")}${pc8.dim(pc8.bold("ctx7"))}${pc8.dim(" CLI commands")}`,
2720
+ value: "cli"
2637
2721
  }
2638
2722
  ],
2639
2723
  theme: {
@@ -2651,8 +2735,8 @@ async function resolveCliAuth(apiKey) {
2651
2735
  log.plain(`${pc8.green("\u2714")} Authenticated`);
2652
2736
  return;
2653
2737
  }
2654
- const existingTokens = loadTokens();
2655
- if (existingTokens && !isTokenExpired(existingTokens)) {
2738
+ const validToken = await getValidAccessToken();
2739
+ if (validToken) {
2656
2740
  log.blank();
2657
2741
  log.plain(`${pc8.green("\u2714")} Authenticated`);
2658
2742
  return;
@@ -2661,8 +2745,13 @@ async function resolveCliAuth(apiKey) {
2661
2745
  }
2662
2746
  async function isAlreadyConfigured(agentName, scope) {
2663
2747
  const agent = getAgent(agentName);
2664
- const mcpPath = scope === "global" ? agent.mcp.globalPath : join9(process.cwd(), agent.mcp.projectPath);
2748
+ const mcpCandidates = scope === "global" ? agent.mcp.globalPaths : agent.mcp.projectPaths.map((p) => join9(process.cwd(), p));
2749
+ const mcpPath = await resolveMcpPath(mcpCandidates);
2665
2750
  try {
2751
+ if (mcpPath.endsWith(".toml")) {
2752
+ const { readTomlServerExists } = await import("./mcp-writer-IYBCUACD.js");
2753
+ return readTomlServerExists(mcpPath, "context7");
2754
+ }
2666
2755
  const existing = await readJsonConfig(mcpPath);
2667
2756
  const section = existing[agent.mcp.configKey] ?? {};
2668
2757
  return "context7" in section;
@@ -2685,7 +2774,7 @@ async function promptAgents(scope, mode) {
2685
2774
  log.info("Context7 is already configured for all detected agents.");
2686
2775
  return null;
2687
2776
  }
2688
- const message = mode === "cli" ? "Install find-docs skill for which agents?" : "Which agents do you want to set up?";
2777
+ const message = "Which agents do you want to set up?";
2689
2778
  try {
2690
2779
  return await checkboxWithHover(
2691
2780
  {
@@ -2713,38 +2802,79 @@ async function resolveAgents(options, scope, mode = "mcp") {
2713
2802
  }
2714
2803
  return selected;
2715
2804
  }
2805
+ async function installRule(agentName, mode, scope) {
2806
+ const agent = getAgent(agentName);
2807
+ const rule = agent.rule;
2808
+ const content = await getRuleContent(mode, agentName);
2809
+ if (rule.kind === "file") {
2810
+ const ruleDir = scope === "global" ? rule.dir("global") : join9(process.cwd(), rule.dir("project"));
2811
+ const rulePath = join9(ruleDir, rule.filename);
2812
+ await mkdir3(dirname4(rulePath), { recursive: true });
2813
+ await writeFile3(rulePath, content, "utf-8");
2814
+ return { status: "installed", path: rulePath };
2815
+ }
2816
+ const filePath = scope === "global" ? rule.file("global") : join9(process.cwd(), rule.file("project"));
2817
+ const escapedMarker = rule.sectionMarker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2818
+ const section = `${rule.sectionMarker}
2819
+ ${content}${rule.sectionMarker}`;
2820
+ let existing = "";
2821
+ try {
2822
+ existing = await readFile3(filePath, "utf-8");
2823
+ } catch {
2824
+ }
2825
+ if (existing.includes(rule.sectionMarker)) {
2826
+ const regex = new RegExp(`${escapedMarker}\\n[\\s\\S]*?${escapedMarker}`);
2827
+ const updated = existing.replace(regex, section);
2828
+ await writeFile3(filePath, updated, "utf-8");
2829
+ return { status: "updated", path: filePath };
2830
+ }
2831
+ const separator = existing.length > 0 && !existing.endsWith("\n") ? "\n\n" : existing.length > 0 ? "\n" : "";
2832
+ await mkdir3(dirname4(filePath), { recursive: true });
2833
+ await writeFile3(filePath, existing + separator + section + "\n", "utf-8");
2834
+ return { status: "installed", path: filePath };
2835
+ }
2716
2836
  async function setupAgent(agentName, auth, scope) {
2717
2837
  const agent = getAgent(agentName);
2718
- const mcpPath = scope === "global" ? agent.mcp.globalPath : join9(process.cwd(), agent.mcp.projectPath);
2838
+ const mcpCandidates = scope === "global" ? agent.mcp.globalPaths : agent.mcp.projectPaths.map((p) => join9(process.cwd(), p));
2839
+ const mcpPath = await resolveMcpPath(mcpCandidates);
2719
2840
  let mcpStatus;
2720
2841
  try {
2721
- const existing = await readJsonConfig(mcpPath);
2722
- const { config, alreadyExists } = mergeServerEntry(
2723
- existing,
2724
- agent.mcp.configKey,
2725
- "context7",
2726
- agent.mcp.buildEntry(auth)
2727
- );
2728
- if (alreadyExists) {
2729
- mcpStatus = "already configured";
2842
+ if (mcpPath.endsWith(".toml")) {
2843
+ const { alreadyExists } = await appendTomlServer(
2844
+ mcpPath,
2845
+ "context7",
2846
+ agent.mcp.buildEntry(auth)
2847
+ );
2848
+ mcpStatus = alreadyExists ? "already configured" : `configured with ${AUTH_MODE_LABELS[auth.mode]}`;
2730
2849
  } else {
2731
- mcpStatus = `configured with ${AUTH_MODE_LABELS[auth.mode]}`;
2732
- }
2733
- const finalConfig = agent.rule.instructionsGlob ? mergeInstructions(config, agent.rule.instructionsGlob(scope)) : config;
2734
- if (finalConfig !== existing) {
2735
- await writeJsonConfig(mcpPath, finalConfig);
2850
+ const existing = await readJsonConfig(mcpPath);
2851
+ const { config, alreadyExists } = mergeServerEntry(
2852
+ existing,
2853
+ agent.mcp.configKey,
2854
+ "context7",
2855
+ agent.mcp.buildEntry(auth)
2856
+ );
2857
+ if (alreadyExists) {
2858
+ mcpStatus = "already configured";
2859
+ } else {
2860
+ mcpStatus = `configured with ${AUTH_MODE_LABELS[auth.mode]}`;
2861
+ }
2862
+ if (config !== existing) {
2863
+ await writeJsonConfig(mcpPath, config);
2864
+ }
2736
2865
  }
2737
2866
  } catch (err) {
2738
2867
  mcpStatus = `failed: ${err instanceof Error ? err.message : String(err)}`;
2739
2868
  }
2740
- const rulePath = scope === "global" ? join9(agent.rule.dir("global"), agent.rule.filename) : join9(process.cwd(), agent.rule.dir("project"), agent.rule.filename);
2741
2869
  let ruleStatus;
2870
+ let rulePath;
2742
2871
  try {
2743
- await mkdir4(dirname4(rulePath), { recursive: true });
2744
- await writeFile4(rulePath, RULE_CONTENT, "utf-8");
2745
- ruleStatus = "installed";
2872
+ const result = await installRule(agentName, "mcp", scope);
2873
+ ruleStatus = result.status;
2874
+ rulePath = result.path;
2746
2875
  } catch (err) {
2747
2876
  ruleStatus = `failed: ${err instanceof Error ? err.message : String(err)}`;
2877
+ rulePath = "";
2748
2878
  }
2749
2879
  const skillDir = scope === "global" ? agent.skill.dir("global") : join9(process.cwd(), agent.skill.dir("project"));
2750
2880
  const skillPath = join9(skillDir, agent.skill.name, "SKILL.md");
@@ -2800,13 +2930,34 @@ async function setupMcp(agents2, options, scope) {
2800
2930
  trackEvent("setup", { agents: agents2, scope, authMode: auth.mode });
2801
2931
  trackEvent("install", { skills: ["/upstash/context7/context7-mcp"], ides: agents2 });
2802
2932
  }
2933
+ async function setupCliAgent(agentName, scope, downloadData) {
2934
+ const agent = getAgent(agentName);
2935
+ const skillDir = scope === "global" ? agent.skill.dir("global") : join9(process.cwd(), agent.skill.dir("project"));
2936
+ let skillStatus;
2937
+ try {
2938
+ await installSkillFiles("find-docs", downloadData.files, skillDir);
2939
+ skillStatus = "installed";
2940
+ } catch (err) {
2941
+ skillStatus = `failed: ${err instanceof Error ? err.message : String(err)}`;
2942
+ }
2943
+ const skillPath = join9(skillDir, "find-docs");
2944
+ let ruleStatus;
2945
+ let rulePath;
2946
+ try {
2947
+ const result = await installRule(agentName, "cli", scope);
2948
+ ruleStatus = result.status;
2949
+ rulePath = result.path;
2950
+ } catch (err) {
2951
+ ruleStatus = `failed: ${err instanceof Error ? err.message : String(err)}`;
2952
+ rulePath = "";
2953
+ }
2954
+ return { skillPath, skillStatus, rulePath, ruleStatus };
2955
+ }
2803
2956
  async function setupCli(options) {
2804
2957
  await resolveCliAuth(options.apiKey);
2805
- const targets = await promptForInstallTargets({ ...options, global: !options.project }, false);
2806
- if (!targets) {
2807
- log.warn("Setup cancelled");
2808
- return;
2809
- }
2958
+ const scope = options.project ? "project" : "global";
2959
+ const agents2 = await resolveAgents(options, scope, "cli");
2960
+ if (agents2.length === 0) return;
2810
2961
  log.blank();
2811
2962
  const spinner = ora4("Downloading find-docs skill...").start();
2812
2963
  const downloadData = await downloadSkill("/upstash/context7", "find-docs");
@@ -2815,28 +2966,27 @@ async function setupCli(options) {
2815
2966
  return;
2816
2967
  }
2817
2968
  spinner.succeed("Downloaded find-docs skill");
2818
- const targetDirs = getTargetDirs(targets);
2819
- const installSpinner = ora4("Installing find-docs skill...").start();
2820
- for (const dir of targetDirs) {
2821
- installSpinner.text = `Installing to ${dir}...`;
2822
- await installSkillFiles("find-docs", downloadData.files, dir);
2969
+ const installSpinner = ora4("Installing...").start();
2970
+ const results = [];
2971
+ for (const agentName of agents2) {
2972
+ installSpinner.text = `Setting up ${getAgent(agentName).displayName}...`;
2973
+ const r = await setupCliAgent(agentName, scope, downloadData);
2974
+ results.push({ agent: getAgent(agentName).displayName, ...r });
2823
2975
  }
2824
- installSpinner.stop();
2976
+ installSpinner.succeed("Context7 CLI setup complete");
2825
2977
  log.blank();
2826
- log.plain(`${pc8.green("\u2714")} Context7 CLI setup complete`);
2827
- log.blank();
2828
- for (const dir of targetDirs) {
2829
- log.itemAdd(
2830
- `find-docs ${pc8.dim("Guides your agent to fetch up-to-date library docs on demand using ctx7 CLI commands")}`
2831
- );
2832
- log.plain(` ${pc8.dim(dir)}`);
2978
+ for (const r of results) {
2979
+ log.plain(` ${pc8.bold(r.agent)}`);
2980
+ const skillIcon = r.skillStatus === "installed" ? pc8.green("+") : pc8.dim("~");
2981
+ log.plain(` ${skillIcon} Skill ${r.skillStatus}`);
2982
+ log.plain(` ${pc8.dim(r.skillPath)}`);
2983
+ const ruleIcon = r.ruleStatus === "installed" || r.ruleStatus === "updated" ? pc8.green("+") : pc8.dim("~");
2984
+ log.plain(` ${ruleIcon} Rule ${r.ruleStatus}`);
2985
+ log.plain(` ${pc8.dim(r.rulePath)}`);
2833
2986
  }
2834
2987
  log.blank();
2835
- log.plain(` ${pc8.bold("Next steps")}`);
2836
- log.plain(` Ask your agent: ${pc8.cyan(`"Use ctx7 CLI to look up React hooks"`)}`);
2837
- log.blank();
2838
2988
  trackEvent("setup", { mode: "cli" });
2839
- trackEvent("install", { skills: ["/upstash/context7/find-docs"], ides: targets.ides });
2989
+ trackEvent("install", { skills: ["/upstash/context7/find-docs"], ides: agents2 });
2840
2990
  }
2841
2991
  async function setupCommand(options) {
2842
2992
  trackEvent("command", { name: "setup" });
@@ -2925,7 +3075,7 @@ async function resolveCommand(library, query, options) {
2925
3075
  log.blank();
2926
3076
  if (data.searchFilterApplied) {
2927
3077
  log.warn(
2928
- "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"
3078
+ "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"
2929
3079
  );
2930
3080
  log.blank();
2931
3081
  }