UNPKG

ctx7

Version:

Context7 CLI - Fetch documentation context and configure Context7

2,803 lines • 107 kB
#!/usr/bin/env node
import {
  CODE_LIMITS,
  CODE_OPERATIONS,
  DEFAULT_CONTEXT7_BASE_URL,
  NAME,
  UPDATE_STATE_FILE_NAME,
  VERSION,
  getBaseUrl,
  getLibraryCode,
  getLibraryContext,
  getUpdateStateFilePath,
  getValidAccessToken,
  hasSourceCode,
  isContext7ApiKey,
  migrateLegacyFile,
  performLogin,
  registerAuthCommands,
  resolveLibrary,
  resolveReadPath,
  saveTokens,
  searchDocumentation,
  setBaseUrl,
  trackEvent
} from "./chunk-F53NPYYA.js";

// src/index.ts
import { Command } from "commander";
import pc7 from "picocolors";
import figlet from "figlet";

// src/commands/setup.ts
import pc3 from "picocolors";
import ora2 from "ora";
import { password, select } from "@inquirer/prompts";
import { mkdir as mkdir5, readFile as readFile5, writeFile as writeFile5 } from "fs/promises";
import { dirname as dirname7, join as join2 } from "path";

// src/utils/logger.ts
import pc from "picocolors";
var ANSI_PATTERN = /\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g;
function visibleLength(text) {
  return text.replace(ANSI_PATTERN, "").length;
}
function padVisible(text, width) {
  const padding = Math.max(0, width - visibleLength(text));
  return text + " ".repeat(padding);
}
function box(lines, color = pc.green) {
  const contentWidth = Math.max(...lines.map((line) => visibleLength(line)), 0);
  const top = color(`\u250C${"\u2500".repeat(contentWidth + 2)}\u2510`);
  const bottom = color(`\u2514${"\u2500".repeat(contentWidth + 2)}\u2518`);
  console.log(top);
  for (const line of lines) {
    console.log(color("\u2502 ") + padVisible(line, contentWidth) + color(" \u2502"));
  }
  console.log(bottom);
}
var log = {
  info: (message) => console.log(pc.cyan(message)),
  success: (message) => console.log(pc.green(`\u2714 ${message}`)),
  warn: (message) => console.log(pc.yellow(`\u26A0 ${message}`)),
  error: (message) => console.log(pc.red(`\u2716 ${message}`)),
  dim: (message) => console.log(pc.dim(message)),
  item: (message) => console.log(pc.green(`  ${message}`)),
  itemAdd: (message) => console.log(`  ${pc.green("+")} ${message}`),
  plain: (message) => console.log(message),
  blank: () => console.log(""),
  box
};

// src/utils/prompts.ts
import pc2 from "picocolors";
import { checkbox } from "@inquirer/prompts";
import readline from "readline";
async function checkboxWithHover(config, options2) {
  const choices = config.choices.filter(
    (c) => typeof c === "object" && c !== null && !("type" in c && c.type === "separator")
  );
  const values = choices.map((c) => c.value);
  const totalItems = values.length;
  let cursorPosition = choices.findIndex((c) => !c.disabled);
  if (cursorPosition < 0) cursorPosition = 0;
  const getName = options2?.getName ?? ((v) => v.name);
  const keypressHandler = (_str, key) => {
    if (key.name === "up") {
      let next = cursorPosition - 1;
      while (next >= 0 && choices[next].disabled) next--;
      if (next >= 0) cursorPosition = next;
    } else if (key.name === "down") {
      let next = cursorPosition + 1;
      while (next < totalItems && choices[next].disabled) next++;
      if (next < totalItems) cursorPosition = next;
    }
  };
  readline.emitKeypressEvents(process.stdin);
  process.stdin.on("keypress", keypressHandler);
  const customConfig = {
    ...config,
    theme: {
      ...config.theme,
      style: {
        answer: (text) => pc2.green(text),
        ...config.theme?.style,
        highlight: (text) => pc2.green(text),
        renderSelectedChoices: (selected, _allChoices) => {
          if (selected.length === 0) {
            return pc2.dim(getName(values[cursorPosition]));
          }
          return selected.map((c) => getName(c.value)).join(", ");
        }
      }
    }
  };
  try {
    const selected = await checkbox(customConfig);
    if (selected.length === 0) {
      return [values[cursorPosition]];
    }
    return selected;
  } finally {
    process.stdin.removeListener("keypress", keypressHandler);
  }
}

// src/utils/installer.ts
import { mkdir, writeFile } from "fs/promises";
import { resolve as resolve2, dirname as dirname2 } from "path";

// src/utils/skill-name.ts
import { resolve, dirname, basename } from "path";
var SAFE_NAME = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
function isSafeSkillName(name) {
  if (typeof name !== "string") return false;
  if (name.length === 0 || name.length > 128) return false;
  if (name === "." || name === "..") return false;
  if (name.includes("\0")) return false;
  if (!SAFE_NAME.test(name)) return false;
  return true;
}
function assertSkillNameInRoot(skillsRoot, skillName) {
  if (!isSafeSkillName(skillName)) {
    throw new Error(`Unsafe skill name: ${JSON.stringify(skillName)}`);
  }
  const root = resolve(skillsRoot);
  const target = resolve(root, skillName);
  if (dirname(target) !== root || basename(target) !== skillName) {
    throw new Error(`Skill name "${skillName}" escapes the skills root`);
  }
  return target;
}

// src/utils/installer.ts
async function installSkillFiles(skillName, files, skillsRoot) {
  const skillDir = assertSkillNameInRoot(skillsRoot, skillName);
  for (const file of files) {
    const filePath = resolve2(skillDir, file.path);
    if (!filePath.startsWith(skillDir + "/") && !filePath.startsWith(skillDir + "\\") && filePath !== skillDir) {
      throw new Error(`Skill file path "${file.path}" resolves outside the target directory`);
    }
    const fileDir = dirname2(filePath);
    await mkdir(fileDir, { recursive: true });
    await writeFile(filePath, file.content);
  }
}

// src/setup/auth.ts
import { randomBytes } from "crypto";
import ora from "ora";
async function resolveSetupApiKey() {
  const accessToken = await getValidAccessToken({ recoverExpiredSession: false }) ?? await performLogin();
  if (!accessToken) return null;
  if (isContext7ApiKey(accessToken)) return accessToken;
  const spinner = ora("Configuring authentication...").start();
  try {
    const response = await fetch(`${getBaseUrl()}/api/dashboard/api-keys`, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${accessToken}`,
        "Content-Type": "application/json"
      },
      body: JSON.stringify({ name: `ctx7-cli-${randomBytes(3).toString("hex")}` })
    });
    if (!response.ok) {
      const err = await response.json().catch(() => ({}));
      spinner.fail("Authentication failed");
      log.error(err.message || err.error || `HTTP ${response.status}`);
      return null;
    }
    const result = await response.json();
    spinner.succeed("Authenticated");
    return result.data.apiKey;
  } catch (err) {
    spinner.fail("Authentication failed");
    log.error(err instanceof Error ? err.message : String(err));
    return null;
  }
}

// src/setup/agents.ts
import { access, readFile, stat } from "fs/promises";
import { dirname as dirname3, join } from "path";
import { homedir } from "os";
var AUTH_MODE_LABELS = {
  oauth: "OAuth",
  "api-key": "API Key",
  none: "no authentication"
};
var STDIO_PACKAGE = "@upstash/context7-mcp";
function stdioArgs(auth) {
  const args = ["-y", STDIO_PACKAGE];
  if (auth.mode === "api-key") {
    args.push("--api-key", auth.apiKey);
  }
  return args;
}
function stdioEntry(auth) {
  return { command: "npx", args: stdioArgs(auth) };
}
function claudeConfigDir() {
  return process.env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude");
}
function claudeGlobalMcpPath() {
  if (process.env.CLAUDE_CONFIG_DIR) {
    return join(claudeConfigDir(), ".claude.json");
  }
  return join(homedir(), ".claude.json");
}
function resolveVscodeUserDir(platform = process.platform, home = homedir(), env = {
  APPDATA: process.env.APPDATA,
  XDG_CONFIG_HOME: process.env.XDG_CONFIG_HOME
}) {
  if (platform === "win32") {
    return join(env.APPDATA || join(home, "AppData", "Roaming"), "Code", "User");
  }
  if (platform === "darwin") {
    return join(home, "Library", "Application Support", "Code", "User");
  }
  return join(env.XDG_CONFIG_HOME || join(home, ".config"), "Code", "User");
}
function resolveDevinConfigDir(platform = process.platform, home = homedir(), env = { APPDATA: process.env.APPDATA }) {
  if (platform === "win32") {
    return join(env.APPDATA || join(home, "AppData", "Roaming"), "devin");
  }
  return join(home, ".config", "devin");
}
function resolvePiAgentDir(platform = process.platform, home = homedir(), env = { PI_CODING_AGENT_DIR: process.env.PI_CODING_AGENT_DIR }) {
  const override = env.PI_CODING_AGENT_DIR;
  if (!override) return join(home, ".pi", "agent");
  if (override === "~") return home;
  if (override.startsWith("~/") || platform === "win32" && override.startsWith("~\\")) {
    return join(home, override.slice(2));
  }
  return override;
}
function defaultAppendRuleFile(rule, scope) {
  return scope === "global" ? rule.file("global") : join(process.cwd(), rule.file("project"));
}
function appendRuleCandidates(rule, scope) {
  const defaultFile = defaultAppendRuleFile(rule, scope);
  if (!rule.contextFileOrder) return [defaultFile];
  return rule.contextFileOrder.map((name) => join(dirname3(defaultFile), name));
}
async function resolveAppendRuleFile(rule, scope) {
  for (const candidate of appendRuleCandidates(rule, scope)) {
    try {
      if ((await stat(candidate)).isFile()) return candidate;
    } catch {
    }
  }
  return defaultAppendRuleFile(rule, scope);
}
async function findAppendRuleFile(rule, scope) {
  for (const candidate of appendRuleCandidates(rule, scope)) {
    try {
      if ((await readFile(candidate, "utf-8")).includes(rule.sectionMarker)) return candidate;
    } catch {
    }
  }
  return void 0;
}
function withHeaders(base, auth) {
  if (auth.mode === "api-key") {
    return { ...base, headers: { Authorization: `Bearer ${auth.apiKey}` } };
  }
  return base;
}
var agents = {
  claude: {
    displayName: "Claude Code",
    mcp: {
      projectPaths: [".mcp.json"],
      get globalPaths() {
        return [claudeGlobalMcpPath()];
      },
      configKey: "mcpServers",
      buildEntry: (auth, transport, mcpUrl) => transport === "stdio" ? stdioEntry(auth) : withHeaders({ type: "http", url: mcpUrl }, auth)
    },
    rule: {
      kind: "file",
      dir: (scope) => scope === "global" ? join(claudeConfigDir(), "rules") : join(".claude", "rules"),
      filename: "context7.md"
    },
    skill: {
      name: "context7-mcp",
      dir: (scope) => scope === "global" ? join(claudeConfigDir(), "skills") : join(".claude", "skills")
    },
    detect: {
      projectPaths: [".mcp.json", ".claude"],
      get globalPaths() {
        return [claudeConfigDir()];
      }
    }
  },
  cursor: {
    displayName: "Cursor",
    mcp: {
      projectPaths: [join(".cursor", "mcp.json")],
      globalPaths: [join(homedir(), ".cursor", "mcp.json")],
      configKey: "mcpServers",
      buildEntry: (auth, transport, mcpUrl) => transport === "stdio" ? stdioEntry(auth) : withHeaders({ url: mcpUrl }, auth)
    },
    rule: {
      kind: "file",
      dir: (scope) => scope === "global" ? join(homedir(), ".cursor", "rules") : join(".cursor", "rules"),
      filename: "context7.mdc",
      contentPrefix: `---
alwaysApply: true
---

`
    },
    skill: {
      name: "context7-mcp",
      dir: (scope) => scope === "global" ? join(homedir(), ".cursor", "skills") : join(".cursor", "skills")
    },
    detect: {
      projectPaths: [".cursor"],
      globalPaths: [join(homedir(), ".cursor")]
    }
  },
  vscode: {
    displayName: "VS Code",
    mcp: {
      projectPaths: [join(".vscode", "mcp.json")],
      get globalPaths() {
        return [join(resolveVscodeUserDir(), "mcp.json")];
      },
      configKey: "servers",
      buildEntry: (auth, transport, mcpUrl) => transport === "stdio" ? { type: "stdio", ...stdioEntry(auth) } : withHeaders({ type: "http", url: mcpUrl }, auth)
    },
    rule: {
      kind: "file",
      dir: (scope) => scope === "global" ? join(resolveVscodeUserDir(), "prompts") : join(".github", "instructions"),
      filename: "context7.instructions.md",
      contentPrefix: `---
applyTo: "**"
---

`
    },
    skill: {
      name: "context7-mcp",
      dir: (scope) => scope === "global" ? join(homedir(), ".agents", "skills") : join(".agents", "skills")
    },
    detect: {
      projectPaths: [".vscode"],
      get globalPaths() {
        return [resolveVscodeUserDir()];
      }
    }
  },
  devin: {
    displayName: "Devin",
    mcp: {
      projectPaths: [join(".devin", "mcp_config.json")],
      get globalPaths() {
        return [join(resolveDevinConfigDir(), "mcp_config.json")];
      },
      configKey: "mcpServers",
      buildEntry: (auth, transport, mcpUrl) => transport === "stdio" ? stdioEntry(auth) : withHeaders({ transport: "http", url: mcpUrl }, auth)
    },
    rule: {
      kind: "append",
      file: (scope) => scope === "global" ? join(resolveDevinConfigDir(), "AGENTS.md") : "AGENTS.md",
      sectionMarker: "<!-- context7 -->"
    },
    skill: {
      name: "context7-mcp",
      dir: (scope) => scope === "global" ? join(resolveDevinConfigDir(), "skills") : join(".devin", "skills")
    },
    detect: {
      projectPaths: [".devin"],
      get globalPaths() {
        return [resolveDevinConfigDir()];
      }
    }
  },
  copilot: {
    displayName: "GitHub Copilot CLI",
    mcp: {
      projectPaths: [".mcp.json"],
      globalPaths: [join(homedir(), ".copilot", "mcp-config.json")],
      configKey: "mcpServers",
      buildEntry: (auth, transport, mcpUrl) => transport === "stdio" ? { type: "stdio", ...stdioEntry(auth), tools: ["*"] } : withHeaders({ type: "http", url: mcpUrl, tools: ["*"] }, auth)
    },
    rule: {
      kind: "append",
      file: (scope) => scope === "global" ? join(homedir(), ".copilot", "AGENTS.md") : "AGENTS.md",
      sectionMarker: "<!-- context7 -->"
    },
    skill: {
      name: "context7-mcp",
      dir: (scope) => scope === "global" ? join(homedir(), ".agents", "skills") : join(".agents", "skills")
    },
    detect: {
      // Copilot shares .mcp.json with Claude Code, so project ownership cannot
      // be inferred safely. Project setup remains available via --copilot.
      projectPaths: [],
      globalPaths: [join(homedir(), ".copilot")]
    }
  },
  opencode: {
    displayName: "OpenCode",
    mcp: {
      projectPaths: ["opencode.json", "opencode.jsonc", ".opencode.json", ".opencode.jsonc"],
      globalPaths: [
        join(homedir(), ".config", "opencode", "opencode.json"),
        join(homedir(), ".config", "opencode", "opencode.jsonc"),
        join(homedir(), ".config", "opencode", ".opencode.json"),
        join(homedir(), ".config", "opencode", ".opencode.jsonc")
      ],
      configKey: "mcp",
      buildEntry: (auth, transport, mcpUrl) => transport === "stdio" ? { type: "local", command: ["npx", ...stdioArgs(auth)], enabled: true } : withHeaders({ type: "remote", url: mcpUrl, enabled: true }, auth)
    },
    rule: {
      kind: "append",
      file: (scope) => scope === "global" ? join(homedir(), ".config", "opencode", "AGENTS.md") : "AGENTS.md",
      sectionMarker: "<!-- context7 -->"
    },
    skill: {
      name: "context7-mcp",
      dir: (scope) => scope === "global" ? join(homedir(), ".agents", "skills") : join(".agents", "skills")
    },
    detect: {
      projectPaths: ["opencode.json", "opencode.jsonc", ".opencode.json", ".opencode.jsonc"],
      globalPaths: [join(homedir(), ".config", "opencode")]
    }
  },
  codex: {
    displayName: "Codex",
    mcp: {
      projectPaths: [join(".codex", "config.toml")],
      globalPaths: [join(homedir(), ".codex", "config.toml")],
      configKey: "mcp_servers",
      buildEntry: (auth, transport, mcpUrl) => transport === "stdio" ? stdioEntry(auth) : withHeaders({ type: "http", url: mcpUrl }, auth)
    },
    rule: {
      kind: "append",
      file: (scope) => scope === "global" ? join(homedir(), ".codex", "AGENTS.md") : "AGENTS.md",
      sectionMarker: "<!-- context7 -->"
    },
    skill: {
      name: "context7-mcp",
      dir: (scope) => scope === "global" ? join(homedir(), ".agents", "skills") : join(".agents", "skills")
    },
    detect: {
      projectPaths: [".codex"],
      globalPaths: [join(homedir(), ".codex")]
    }
  },
  // Antigravity is built on Gemini infrastructure and shares ~/.gemini/. Per
  // the official Codelabs guide, Antigravity 2.0/IDE/CLI read MCP servers from
  // ~/.gemini/config/mcp_config.json globally; there is no project-level MCP
  // config, so projectPaths is empty and setupAgent falls back to global.
  antigravity: {
    displayName: "Antigravity",
    setupDescription: "Set up for Antigravity (.agent/skills)",
    mcp: {
      projectPaths: [],
      globalPaths: [join(homedir(), ".gemini", "config", "mcp_config.json")],
      configKey: "mcpServers",
      buildEntry: (auth, transport, mcpUrl) => transport === "stdio" ? stdioEntry(auth) : withHeaders({ serverUrl: mcpUrl }, auth)
    },
    rule: {
      kind: "append",
      file: (scope) => scope === "global" ? join(homedir(), ".gemini", "GEMINI.md") : "GEMINI.md",
      sectionMarker: "<!-- context7 -->"
    },
    skill: {
      name: "context7-mcp",
      dir: (scope) => scope === "global" ? join(homedir(), ".agent", "skills") : join(".agent", "skills")
    },
    detect: {
      projectPaths: [".agent"],
      globalPaths: [join(homedir(), ".gemini", "antigravity"), join(homedir(), ".agent")]
    }
  },
  gemini: {
    displayName: "Gemini CLI",
    mcp: {
      projectPaths: [join(".gemini", "settings.json")],
      globalPaths: [join(homedir(), ".gemini", "settings.json")],
      configKey: "mcpServers",
      buildEntry: (auth, transport, mcpUrl) => transport === "stdio" ? stdioEntry(auth) : withHeaders({ httpUrl: mcpUrl }, auth)
    },
    rule: {
      kind: "append",
      file: (scope) => scope === "global" ? join(homedir(), ".gemini", "GEMINI.md") : "GEMINI.md",
      sectionMarker: "<!-- context7 -->"
    },
    skill: {
      name: "context7-mcp",
      dir: (scope) => scope === "global" ? join(homedir(), ".gemini", "skills") : join(".gemini", "skills")
    },
    detect: {
      projectPaths: [".gemini"],
      globalPaths: [join(homedir(), ".gemini")]
    }
  },
  // Pi has built-in MCP support from v0.99.0. Without an explicit exposure it
  // hides MCP tools behind codemode scripts, so the rule's direct calls to
  // resolve-library-id and query-docs would not find them. Pi's docs recommend
  // `direct` for small, frequently used servers like Context7.
  pi: {
    displayName: "Pi",
    mcp: {
      projectPaths: [join(".pi", "mcp.json")],
      get globalPaths() {
        return [join(resolvePiAgentDir(), "mcp.json")];
      },
      configKey: "mcpServers",
      buildEntry: (auth, transport, mcpUrl) => ({
        ...transport === "stdio" ? stdioEntry(auth) : withHeaders({ url: mcpUrl }, auth),
        exposure: "direct"
      }),
      oauthSignInCommand: "pi mcp login context7"
    },
    rule: {
      kind: "append",
      file: (scope) => scope === "global" ? join(resolvePiAgentDir(), "AGENTS.md") : "AGENTS.md",
      sectionMarker: "<!-- context7 -->",
      // Pi reads one context file per folder, so a new AGENTS.md would hide an existing CLAUDE.md.
      contextFileOrder: ["AGENTS.override.md", "AGENTS.md", "AGENTS.MD", "CLAUDE.md", "CLAUDE.MD"]
    },
    skill: {
      name: "context7-mcp",
      dir: (scope) => scope === "global" ? join(homedir(), ".agents", "skills") : join(".agents", "skills")
    },
    detect: {
      projectPaths: [".pi"],
      get globalPaths() {
        return [resolvePiAgentDir()];
      }
    }
  }
};
function getAgent(name) {
  return agents[name];
}
var ALL_AGENT_NAMES = Object.keys(agents);
async function pathExists(p) {
  try {
    await access(p);
    return true;
  } catch {
    return false;
  }
}
async function detectAgents(scope) {
  const detected = [];
  for (const name of ALL_AGENT_NAMES) {
    const agent = agents[name];
    const paths = scope === "global" ? agent.detect.globalPaths : agent.detect.projectPaths;
    for (const p of paths) {
      const fullPath = scope === "global" ? p : join(process.cwd(), p);
      if (await pathExists(fullPath)) {
        detected.push(name);
        break;
      }
    }
  }
  return detected;
}

// src/setup/templates.ts
var GITHUB_RAW_URLS = [
  "https://raw.githubusercontent.com/upstash/context7/master",
  "https://raw.githubusercontent.com/upstash/context7/main"
];
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 \u2014 even well-known ones like React, Next.js, Prisma, Express, Tailwind, Django, or Spring Boot. This includes API syntax, configuration, version migration, porting code between libraries, platforms, or SQL dialects, library-specific debugging, setup instructions, and CLI tool usage. Use even when you think you know the answer \u2014 your training data may not reflect recent changes. Prefer this over web search for library docs.

Do not use for: refactoring, debugging, or reviewing code that does not depend on a library or platform API, or general programming concepts.

## Steps

1. \`resolve-library-id\` with the library name and what to look up in the library's documentation. Use the official library name with proper punctuation (e.g., "Next.js" not "nextjs", "Customer.io" not "customerio", "Three.js" not "threejs")
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
3. \`query-docs\` with the selected library ID and what to look up in the library's documentation (not single words), scoped to a single concept. If the question spans multiple distinct concepts (e.g. routing and auth and caching), make a separate \`query-docs\` call per concept with the same library ID, unless the question is about how the concepts interact \u2014 combined queries dilute ranking and return shallow results for each topic
4. Answer using the fetched docs

## Library source code

If the \`query-code\` tool is available, use it when the docs do not answer an exact question: a signature or type, a default value, an option name, where an error message comes from, or how something works inside.

- Use a library ID that \`resolve-library-id\` marks with "Source code", usually the GitHub repository (e.g. \`/colinhacks/zod\`). \`/websites/...\` and \`/llmstxt/...\` IDs have no source
- Start with \`grep\` (set \`fixed_strings\` for error messages) or \`definition\` for a symbol, then \`read\` the file range in the result's "Next:" line
- For a version, pass its git tag in \`version\` (e.g. \`v3.24.2\`)
- Most questions need 1 to 4 \`query-code\` calls; stop at 6
- If it answers "being indexed", retry at most twice, then answer from \`query-docs\`
- Cite the file and line of the source you used
`;
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 \u2014 even well-known ones like React, Next.js, Prisma, Express, Tailwind, Django, or Spring Boot. This includes API syntax, configuration, version migration, porting code between libraries, platforms, or SQL dialects, library-specific debugging, setup instructions, and CLI tool usage. Use even when you think you know the answer \u2014 your training data may not reflect recent changes. Prefer this over web search for library docs.

Do not use for: refactoring, debugging, or reviewing code that does not depend on a library or platform API, or general programming concepts.

## Steps

1. Resolve library: \`npx ctx7@latest library <name> "<what to look up>"\`
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")
3. Fetch docs: \`npx ctx7@latest docs <libraryId> "<what to look up>"\` \u2014 run a separate \`docs\` command per distinct concept if the question spans multiple topics, unless it's about how they interact
4. Answer using the fetched documentation

## Library source code

When the docs do not answer an exact question (a signature or type, a default value, an option name, where an error message comes from, or how something works inside), search the library's source at the docs version.

- Grep: \`npx ctx7@latest code <libraryId> "<regex>"\`. Add \`-F\` for plain text such as error messages
- Definition: \`npx ctx7@latest code <libraryId> <symbol> --op definition\`. Read code: \`npx ctx7@latest code <libraryId> --op read --path <file>#L10-L40\`. \`--op references\`, \`--op outline\` and \`--op list\` also exist
- Use an ID that \`library\` marks with "Source code", usually the GitHub repository. \`/websites/...\` and \`/llmstxt/...\` IDs have no source
- For a version, add the git tag to the library ID (e.g. \`/colinhacks/zod/v3.24.2\`). A tag that is not a docs version is indexed first (exit code 75). If indexing fails, the output lists the docs versions you can use
- Follow the \`Next:\` line of the output. Its fields map to flags: \`offset\` is \`--offset\`, \`output_mode "count"\` is \`-c\`, \`fixed_strings\` is \`-F\`, \`ignore_case\` is \`-i\`, \`read: <file>#L..\` is \`--op read --path <file>#L..\`, and the other fields use the flag of the same name (\`head_limit\` is \`--head-limit\`, \`version\` is \`--tag\`)
- Exit code 75 means the source is being indexed: retry at most twice, then answer from \`docs\`
- Cite the file and line of the source you used

You MUST call \`library\` first to get a valid ID (format: \`/org/project\`) unless the user provides one directly. Be specific about what to look up in the library's documentation \u2014 specific and detailed queries return better results than vague single words, but keep each query to a single concept unless the question is about how concepts interact; combined multi-topic queries dilute ranking and return shallow results for each topic. Do not run more than 3 \`library\` and \`docs\` commands per question, and not more than 6 \`code\` commands. Do not include sensitive information (API keys, passwords, credentials) in queries.

For version-specific docs, use \`/org/project/version\` from the \`library\` output (e.g., \`/vercel/next.js/v16.2.9\`).

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.
`;
var CODEX_CLI_SANDBOX_GUIDANCE = `Run Context7 CLI requests outside Codex's default sandbox. If a Context7 CLI command fails with DNS or network errors such as ENOTFOUND, host resolution failures, or fetch failed, rerun it outside the sandbox instead of retrying inside the sandbox.`;
function customizeRuleContent(mode, agent, body) {
  if (mode === "cli" && agent === "codex" && !body.includes(CODEX_CLI_SANDBOX_GUIDANCE)) {
    body = `${body.trimEnd()}
${CODEX_CLI_SANDBOX_GUIDANCE}
`;
  }
  return body;
}
async function fetchRepoFile(path) {
  for (const base of GITHUB_RAW_URLS) {
    try {
      const res = await fetch(`${base}/${path}`);
      if (res.ok) return await res.text();
    } catch {
      continue;
    }
  }
  return null;
}
async function getRuleContent(mode, agent) {
  const [filename, fallback] = mode === "mcp" ? ["context7-mcp.md", FALLBACK_MCP] : ["context7-cli.md", FALLBACK_CLI];
  const body = await fetchRepoFile(`rules/${filename}`) ?? fallback;
  return customizeRuleContent(mode, agent, body);
}
async function fetchSetupSkillFiles(name) {
  const content = await fetchRepoFile(`skills/${name}/SKILL.md`);
  return content ? [{ path: "SKILL.md", content }] : null;
}
function getBundledRuleContent(mode, agent) {
  return customizeRuleContent(mode, agent, mode === "mcp" ? FALLBACK_MCP : FALLBACK_CLI);
}
function getBundledMcpSkillFiles() {
  return [
    {
      path: "SKILL.md",
      content: `---
name: context7-mcp
description: Fetch current library documentation with Context7 MCP tools.
---

# Context7 MCP

${FALLBACK_MCP}`
    }
  ];
}
function getBundledFindDocsSkillFiles() {
  return [
    {
      path: "SKILL.md",
      content: `---
name: find-docs
description: Fetch current library documentation with the ctx7 CLI.
---

# Find Docs

${FALLBACK_CLI}`
    }
  ];
}
function customizeSkillFilesForAgent(agent, skillName, files) {
  if (agent !== "codex" || skillName !== "find-docs") {
    return files;
  }
  return files.map((file) => {
    if (file.path !== "SKILL.md" || file.content.includes(CODEX_CLI_SANDBOX_GUIDANCE)) {
      return file;
    }
    const marker = "## Step 1: Resolve a Library";
    const guidance = `${CODEX_CLI_SANDBOX_GUIDANCE}

`;
    if (file.content.includes(marker)) {
      return {
        ...file,
        content: file.content.replace(marker, `${guidance}${marker}`)
      };
    }
    const separator = file.content.endsWith("\n") ? "\n" : "\n\n";
    return {
      ...file,
      content: `${file.content}${separator}${CODEX_CLI_SANDBOX_GUIDANCE}
`
    };
  });
}

// src/setup/deployment.ts
var HOSTED_MCP_BASE_URL = "https://mcp.context7.com";
function normalizeDeploymentBaseUrl(input) {
  const raw = input?.trim() || DEFAULT_CONTEXT7_BASE_URL;
  let url;
  try {
    url = new URL(raw);
  } catch {
    throw new Error(`Invalid Context7 base URL: ${raw}`);
  }
  if (url.protocol !== "http:" && url.protocol !== "https:") {
    throw new Error("Context7 base URL must use http:// or https://");
  }
  if (url.username || url.password) {
    throw new Error("Context7 base URL must not contain credentials");
  }
  if (url.search || url.hash) {
    throw new Error("Context7 base URL must not contain a query string or fragment");
  }
  url.pathname = url.pathname.replace(/\/+$/, "") || "/";
  const normalized = url.toString().replace(/\/$/, "");
  if (url.pathname.endsWith("/mcp") || url.pathname.endsWith("/api")) {
    throw new Error("Pass the Context7 deployment root, without /mcp or /api");
  }
  return normalized;
}
function resolveSetupDeployment(input) {
  const baseUrl = normalizeDeploymentBaseUrl(input);
  return baseUrl === DEFAULT_CONTEXT7_BASE_URL ? { kind: "hosted", baseUrl: DEFAULT_CONTEXT7_BASE_URL } : { kind: "custom", baseUrl };
}
function getMcpUrl(deployment, auth) {
  if (deployment.kind === "hosted") {
    return auth.mode === "oauth" ? `${HOSTED_MCP_BASE_URL}/mcp/oauth` : `${HOSTED_MCP_BASE_URL}/mcp`;
  }
  return `${deployment.baseUrl}/mcp`;
}
async function getOnPremMcpAuthStatus(deployment) {
  const response = await fetch(`${deployment.baseUrl}/api/auth/mcp`, {
    headers: { Accept: "application/json" },
    redirect: "manual",
    signal: AbortSignal.timeout(1e4)
  });
  if (response.status >= 300 && response.status < 400) {
    throw new Error(
      `Authentication discovery was redirected. Pass the final deployment URL instead of ${deployment.baseUrl}.`
    );
  }
  if (!response.ok) {
    throw new Error(`HTTP ${response.status} from ${deployment.baseUrl}/api/auth/mcp`);
  }
  const body = await response.json();
  if (typeof body.enabled !== "boolean") {
    throw new Error(`Invalid response from ${deployment.baseUrl}/api/auth/mcp`);
  }
  return body.enabled;
}

// src/setup/mcp-writer.ts
import { access as access2, readFile as readFile3, writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
import { dirname as dirname5 } from "path";
import { applyEdits, modify, parse, printParseErrorCode } from "jsonc-parser";

// src/setup/toml-editor.ts
import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
import { dirname as dirname4 } from "path";
var TOML_BASIC_ESCAPES = {
  b: "\b",
  t: "	",
  n: "\n",
  f: "\f",
  r: "\r",
  '"': '"',
  "\\": "\\"
};
function skipTomlArrayTrivia(source, start) {
  let index = start;
  while (index < source.length) {
    if (/\s/.test(source[index])) {
      index++;
      continue;
    }
    if (source[index] !== "#") break;
    while (index < source.length && source[index] !== "\n") index++;
  }
  return index;
}
function parseTomlBasicString(source, start) {
  let value = "";
  let index = start + 1;
  while (index < source.length) {
    const char = source[index++];
    if (char === '"') return { value, start, end: index };
    if (char === "\n" || char === "\r") {
      throw new Error("Multiline strings are not supported in MCP args");
    }
    if (char !== "\\") {
      value += char;
      continue;
    }
    const escape = source[index++];
    if (Object.hasOwn(TOML_BASIC_ESCAPES, escape)) {
      value += TOML_BASIC_ESCAPES[escape];
      continue;
    }
    if (escape !== "u" && escape !== "U") {
      throw new Error(`Unsupported TOML escape \\${escape}`);
    }
    const width = escape === "u" ? 4 : 8;
    const hex = source.slice(index, index + width);
    if (!new RegExp(`^[0-9A-Fa-f]{${width}}$`).test(hex)) {
      throw new Error(`Invalid TOML Unicode escape \\${escape}${hex}`);
    }
    const codePoint = Number.parseInt(hex, 16);
    if (codePoint > 1114111 || codePoint >= 55296 && codePoint <= 57343) {
      throw new Error(`Invalid TOML Unicode code point U+${hex}`);
    }
    value += String.fromCodePoint(codePoint);
    index += width;
  }
  throw new Error("Unterminated TOML basic string in MCP args");
}
function parseTomlLiteralString(source, start) {
  const endQuote = source.indexOf("'", start + 1);
  if (endQuote === -1) throw new Error("Unterminated TOML literal string in MCP args");
  const value = source.slice(start + 1, endQuote);
  if (value.includes("\n") || value.includes("\r")) {
    throw new Error("Multiline strings are not supported in MCP args");
  }
  return { value, start, end: endQuote + 1 };
}
function parseTomlStringArray(source, start) {
  if (source[start] !== "[") throw new Error("Expected a TOML array for MCP args");
  const tokens = [];
  let index = start + 1;
  while (index < source.length) {
    index = skipTomlArrayTrivia(source, index);
    if (source[index] === "]") return { tokens, end: index + 1 };
    const quote = source[index];
    if (quote !== '"' && quote !== "'") {
      throw new Error("MCP args must be a TOML array containing only strings");
    }
    if (source.slice(index, index + 3) === quote.repeat(3)) {
      throw new Error("Multiline strings are not supported in MCP args");
    }
    const token = quote === '"' ? parseTomlBasicString(source, index) : parseTomlLiteralString(source, index);
    tokens.push(token);
    index = skipTomlArrayTrivia(source, token.end);
    if (source[index] === ",") {
      index++;
      continue;
    }
    if (source[index] !== "]") {
      throw new Error("Expected a comma or closing bracket in MCP args");
    }
  }
  throw new Error("Unterminated TOML array in MCP args");
}
function findTomlServerArgs(raw, serverName) {
  const escapedName = serverName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
  const tableKey = `(?:mcp_servers|"mcp_servers"|'mcp_servers')`;
  const serverKey = `(?:${escapedName}|"${escapedName}"|'${escapedName}')`;
  const headerRe = new RegExp(
    `^[\\uFEFF\\t ]*\\[[\\t ]*${tableKey}[\\t ]*\\.[\\t ]*${serverKey}[\\t ]*\\][\\t ]*(?:#.*)?\\r?$`,
    "m"
  );
  const header = headerRe.exec(raw);
  if (!header) return null;
  const bodyStart = raw.indexOf("\n", header.index + header[0].length) + 1;
  const effectiveBodyStart = bodyStart === 0 ? raw.length : bodyStart;
  const nextHeaderRe = /^[\t ]*\[[^\r\n]+\][\t ]*(?:#.*)?\r?$/gm;
  nextHeaderRe.lastIndex = effectiveBodyStart;
  const nextHeader = nextHeaderRe.exec(raw);
  const bodyEnd = nextHeader?.index ?? raw.length;
  const body = raw.slice(effectiveBodyStart, bodyEnd);
  const argsRe = /^[\t ]*(?:args|"args"|'args')[\t ]*=[\t ]*/gm;
  const args = argsRe.exec(body);
  if (!args) {
    throw new Error("Existing MCP server has no safely editable args array");
  }
  const start = effectiveBodyStart + args.index + args[0].length;
  const parsed = parseTomlStringArray(raw, start);
  return { start, end: parsed.end, tokens: parsed.tokens };
}
function withoutApiKey(args) {
  const result = [];
  for (let index = 0; index < args.length; index++) {
    if (args[index] === "--api-key") {
      index++;
      continue;
    }
    if (args[index].startsWith("--api-key=")) continue;
    result.push(args[index]);
  }
  return result;
}
function isContext7Package(arg) {
  return arg === STDIO_PACKAGE || arg.startsWith(`${STDIO_PACKAGE}@`);
}
async function patchTomlStdioApiKey(filePath, serverName, apiKey) {
  let raw;
  try {
    raw = await readFile2(filePath, "utf-8");
  } catch {
    return false;
  }
  if (raw.includes('"""') || raw.includes("'''")) {
    throw new Error("TOML files containing multiline strings are not safely editable");
  }
  const range = findTomlServerArgs(raw, serverName);
  if (!range) return false;
  const args = range.tokens.map((token) => token.value);
  if (!args.some(isContext7Package)) {
    throw new Error(`Existing MCP args do not invoke ${STDIO_PACKAGE}`);
  }
  const apiKeyIndexes = args.flatMap((arg, index) => arg === "--api-key" ? [index] : []);
  let content;
  const hasInlineApiKey = args.some((arg) => arg.startsWith("--api-key="));
  if (apiKey && !hasInlineApiKey && apiKeyIndexes.length === 1 && apiKeyIndexes[0] + 1 < range.tokens.length) {
    const valueToken = range.tokens[apiKeyIndexes[0] + 1];
    content = raw.slice(0, valueToken.start) + JSON.stringify(apiKey) + raw.slice(valueToken.end);
  } else {
    const patched = withoutApiKey(args);
    if (apiKey) patched.push("--api-key", apiKey);
    content = raw.slice(0, range.start) + JSON.stringify(patched) + raw.slice(range.end);
  }
  if (content !== raw) {
    await mkdir2(dirname4(filePath), { recursive: true });
    await writeFile2(filePath, content, "utf-8");
  }
  return true;
}

// src/setup/mcp-writer.ts
async function readJsonConfig(filePath) {
  let raw;
  try {
    raw = await readFile3(filePath, "utf-8");
  } catch (error) {
    if (error.code === "ENOENT") return {};
    throw error;
  }
  return parseJsonConfig(raw);
}
function parseJsonConfig(raw) {
  raw = raw.trim();
  if (!raw) return {};
  const errors = [];
  const config = parse(raw, errors, { allowTrailingComma: true });
  if (errors.length > 0) {
    const firstError = errors[0];
    throw new SyntaxError(
      `Invalid JSON config at offset ${firstError.offset}: ${printParseErrorCode(firstError.error)}`
    );
  }
  return config;
}
function mergeServerEntry(existing, configKey, serverName, entry) {
  const section = existing[configKey] ?? {};
  const alreadyExists = serverName in section;
  return {
    config: {
      ...existing,
      [configKey]: {
        ...section,
        [serverName]: entry
      }
    },
    alreadyExists
  };
}
function removeServerEntry(existing, configKey, serverName) {
  const section = existing[configKey];
  if (!section || typeof section !== "object" || Array.isArray(section)) {
    return { config: existing, removed: false };
  }
  const current = section;
  if (!(serverName in current)) {
    return { config: existing, removed: false };
  }
  const rest = Object.fromEntries(Object.entries(current).filter(([key]) => key !== serverName));
  const next = { ...existing };
  if (Object.keys(rest).length === 0) {
    delete next[configKey];
  } else {
    next[configKey] = rest;
  }
  return { config: next, removed: true };
}
async function resolveMcpPath(candidates) {
  for (const candidate of candidates) {
    try {
      await access2(candidate);
      return candidate;
    } catch {
    }
  }
  return candidates[0];
}
async function writeJsonConfig(filePath, config) {
  await mkdir3(dirname5(filePath), { recursive: true });
  await writeFile3(filePath, JSON.stringify(config, null, 2) + "\n", "utf-8");
}
async function patchJsonConfig(filePath, path, value) {
  let raw;
  try {
    raw = await readFile3(filePath, "utf-8");
  } catch {
    return false;
  }
  if (!raw.trim()) return false;
  const errors = [];
  parse(raw, errors, { allowTrailingComma: true });
  if (errors.length > 0) return false;
  const edits = modify(raw, path, value, {
    formattingOptions: {
      insertSpaces: true,
      tabSize: 2,
      // Keep one line-ending style in the file instead of mixing CRLF and LF.
      eol: raw.includes("\r\n") ? "\r\n" : "\n"
    }
  });
  const updated = applyEdits(raw, edits);
  const verifyErrors = [];
  parse(updated, verifyErrors, { allowTrailingComma: true });
  if (verifyErrors.length > 0) return false;
  if (updated !== raw) {
    await writeFile3(filePath, updated, "utf-8");
  }
  return true;
}
async function readTomlServerExists(filePath, serverName) {
  try {
    const raw = await readFile3(filePath, "utf-8");
    return raw.includes(`[mcp_servers.${serverName}]`);
  } catch {
    return false;
  }
}
function isStdioContext7Entry(entry) {
  if (!entry || typeof entry !== "object") return false;
  const e = entry;
  const refs = (s) => typeof s === "string" && s.includes(STDIO_PACKAGE);
  if (Array.isArray(e.command)) {
    return e.command.some(refs);
  }
  if (typeof e.command === "string" && Array.isArray(e.args)) {
    return e.args.some(refs);
  }
  return false;
}
function getJsonServerEntry(config, configKey, serverName) {
  const section = config[configKey];
  if (!section || typeof section !== "object") return void 0;
  const entry = section[serverName];
  return entry && typeof entry === "object" ? entry : void 0;
}
function stripApiKeyPair(args) {
  const result = [];
  for (let i = 0; i < args.length; i++) {
    if (args[i] === "--api-key") {
      i++;
      continue;
    }
    if (args[i].startsWith("--api-key=")) continue;
    result.push(args[i]);
  }
  return result;
}
function patchStdioApiKey(entry, apiKey) {
  if (Array.isArray(entry.command)) {
    const cmd = stripApiKeyPair(entry.command);
    if (apiKey) cmd.push("--api-key", apiKey);
    return { ...entry, command: cmd };
  }
  const args = Array.isArray(entry.args) ? stripApiKeyPair(entry.args) : [];
  if (apiKey) args.push("--api-key", apiKey);
  return { ...entry, args };
}
function buildTomlServerBlock(serverName, entry) {
  const lines = [`[mcp_servers.${serverName}]`];
  const headers = entry.headers;
  for (const [key, value] of Object.entries(entry)) {
    if (key === "headers") continue;
    lines.push(`${key} = ${JSON.stringify(value)}`);
  }
  if (headers && Object.keys(headers).length > 0) {
    lines.push("");
    lines.push(`[mcp_servers.${serverName}.http_headers]`);
    for (const [key, value] of Object.entries(headers)) {
      lines.push(`${key} = ${JSON.stringify(value)}`);
    }
  }
  return lines.join("\n") + "\n";
}
async function appendTomlServer(filePath, serverName, entry) {
  const block = buildTomlServerBlock(serverName, entry);
  let existing = "";
  try {
    existing = await readFile3(filePath, "utf-8");
  } catch {
  }
  const sectionHeader = `[mcp_servers.${serverName}]`;
  const alreadyExists = existing.includes(sectionHeader);
  if (alreadyExists) {
    const subPrefix = `[mcp_servers.${serverName}.`;
    const startIdx = existing.indexOf(sectionHeader);
    const rest = existing.slice(startIdx + sectionHeader.length);
    let endOffset = rest.length;
    const re = /^\[/gm;
    let m;
    while ((m = re.exec(rest)) !== null) {
      const lineEnd = rest.indexOf("\n", m.index);
      const line = rest.slice(m.index, lineEnd === -1 ? void 0 : lineEnd);
      if (!line.startsWith(subPrefix)) {
        endOffset = m.index;
        break;
      }
    }
    const rawBefore = existing.slice(0, startIdx).replace(/\n+$/, "");
    const rawAfter = existing.slice(startIdx + sectionHeader.length + endOffset).replace(/^\n+/, "");
    const before = rawBefore.length > 0 ? rawBefore + "\n\n" : "";
    const after = rawAfter.length > 0 ? "\n" + rawAfter : "";
    const content = before + block + after;
    await mkdir3(dirname5(filePath), { recursive: true });
    await writeFile3(filePath, content, "utf-8");
  } else {
    const separator = existing.length > 0 && !existing.endsWith("\n") ? "\n\n" : existing.length > 0 ? "\n" : "";
    await mkdir3(dirname5(filePath), { recursive: true });
    await writeFile3(filePath, existing + separator + block, "utf-8");
  }
  return { alreadyExists };
}
async function removeTomlServer(filePath, serverName) {
  let existing = "";
  try {
    existing = await readFile3(filePath, "utf-8");
  } catch {
    return { removed: false };
  }
  const sectionHeader = `[mcp_servers.${serverName}]`;
  const startIdx = existing.indexOf(sectionHeader);
  if (startIdx === -1) {
    return { removed: false };
  }
  const subPrefix = `[mcp_servers.${serverName}.`;
  const rest = existing.slice(startIdx + sectionHeader.length);
  let endOffset = rest.length;
  const re = /^\[/gm;
  let match;
  while ((match = re.exec(rest)) !== null) {
    const lineEnd = rest.indexOf("\n", match.index);
    const line = rest.slice(match.index, lineEnd === -1 ? void 0 : lineEnd);
    if (!line.startsWith(subPrefix)) {
      endOffset = match.index;
      break;
    }
  }
  const rawBefore = existing.slice(0, startIdx).replace(/\n+$/, "");
  const rawAfter = existing.slice(startIdx + sectionHeader.length + endOffset).replace(/^\n+/, "");
  const content = [rawBefore, rawAfter].filter(Boolean).join("\n\n");
  await mkdir3(dirname5(filePath), { recursive: true });
  await writeFile3(filePath, content.length > 0 ? `${content}
` : "", "utf-8");
  return { removed: true };
}

// src/setup/opencode-editor.ts
import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
import { dirname as dirname6 } from "path";
import { applyEdits as applyEdits2, modify as modify2 } from "jsonc-parser";
var options = { formattingOptions: { tabSize: 2, insertSpaces: true } };
var SERVER_NAME = "context7";
var SERVER_PARENT = {
  v1: ["mcp"],
  v2: ["mcp", "servers"]
};
function isObject(value) {
  return !!value && typeof value === "object" && !Array.isArray(value);
}
function getAt(config, path) {
  return path.reduce(
    (node, key) => isObject(node) ? node[key] : void 0,
    config
  );
}
function getLayout(config) {
  const servers = getAt(config, SERVER_PARENT.v2);
  return isObject(servers) && typeof servers.type !== "string" ? "v2" : "v1";
}
function toLayout(entry, layout) {
  if (layout === "v1" || !("enabled" in entry)) return entry;
  const { enabled, ...rest } = entry;
  return enabled === false ? { ...rest, disabled: true } : rest;
}
function findServer(config) {
  for (const parent of [SERVER_PARENT.v2, SERVER_PARENT.v1]) {
    const entry = getAt(config, [...parent, SERVER_NAME]);
    if (isObject(entry)) return entry;
  }
  return void 0;
}
function hasOpenCodeServer(config) {
  return [SERVER_PARENT.v2, SERVER_PARENT.v1].some((parent) => {
    const section = getAt(config, parent);
    return isObject(section) && SERVER_NAME in section;
  });
}
function removeServer(text, config, layout) {
  const parent = SERVER_PARENT[layout];
  const section = getAt(config, parent);
  if (!isObject(section) || !(SERVER_NAME in section)) return text;
  const edits = Object.keys(section).join() === SERVER_NAME ? modify2(text, parent, {}, options) : modify2(text, [...parent, SERVER_NAME], void 0, options);
  return applyEdits2(text, edits);
}
async function readConfigFile(filePath) {
  let raw = "";
  try {
    raw = await readFile4(filePath, "utf-8");
  } catch (error) {
    if (error.code !== "ENOENT") throw error;
  }
  const config = parseJsonConfig(raw);
  return { text: raw.trim() ? raw : "{}\n", config: isObject(config) ? config : {} };
}
async function writeConfigFile(filePath, text) {
  await mkdir4(dirname6(filePath), { recursive: true });
  await writeFile4(filePath, text, "utf-8");
}
async function writeOpenCodeServer(filePath, buildEntry) {
  const { text, config } = await readConfigFile(filePath);
  const layout = getLayout(config);
  const entry = toLayout(buildEntry(findServer(config)), layout);
  const cleaned = layout === "v2" ? removeServer(text, config, "v1") : text;
  const path = [...SERVER_PARENT[layout], SERVER_NAME];
  await writeConfigFile(filePath, applyEdits2(cleaned, modify2(cleaned, path, entry, options)));
  return hasOpenCodeServer(config);
}
async function removeOpenCodeServer(filePath) {
  const { text, config } = await readConfigFile(filePath);
  if (!hasOpenCodeServer(config)) return false;
  const v2Removed = removeServer(text, config, "v2");
  await writeConfigFile(filePath, removeServer(v2Removed, config, "v1"));
  return true;
}

// src/commands/setup.ts
function resolveTransport(options2) {
  return options2.stdio ? "stdio" : "http";
}
var CHECKBOX_THEME = {
  style: {
    highlight: (text) => pc3.green(text),
    disabledChoice: (text) => ` ${pc3.dim("\u25EF")} ${pc3.dim(text)}`
  }
};
function getSelectedAgents(options2) {
  return ALL_AGENT_NAMES.filter(
    (name) => options2[name] || options2.t3 && (name === "claude" || name === "codex")
  );
}
function registerSetupCommand(program2) {
  const command = program2.command("setup").description("Set up Context7 for your AI coding agent");
  for (const name of ALL_AGENT_NAMES) {
    const agent = getAgent(name);
    command.option(`--${name}`, agent.setupDescription ?? `Set up for ${agent.displayName}`);
  }
  command.option("--t3", "Set up for T3 Code (Claude Code and Codex providers)").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)").option("--stdio", "Configure the MCP server as a local stdio process (default: HTTP)").option("--base-url <url>", "Use a custom Context7 deployment (for example, on-premise)").action(async (_options, command2) => {
    await setupCommand(command2.optsWithGlobals());
  });
}
async function promptForOnPremApiKey(deployment) {
  try {
    return await password({
      message: `Personal API key (create one at ${deployment.baseUrl}/account)`,
      mask: true,
      validate: (value) => value.trim().length > 0 || "API key is required"
    }).then((value) => value.trim());
  } catch {
    log.error("Setup cancelled before a personal API key was entered.");
    return null;
  }
}
async function resolveAuth(options2, deployment) {
  const explicitApiKey = options2.apiKey?.trim();
  if (deployment.kind === "custom") {
    try {
      if (!await getOnPremMcpAuthStatus(deployment)) return { mode: "none" };
    } catch (err) {
      log.error(
        `Could not check MCP authentication at ${deployment.baseUrl}: ${err instanceof Error ? err.message : String(err)}`
      );
      process.exitCode = 1;
      return null;
    }
    const apiKey = explicitApiKey || process.env.CONTEXT7_API_KEY?.trim();
    if (apiKey) return { mode: "api-key", apiKey };
    if (!options2.yes) {
      const promptedApiKey = await promptForOnPremApiKey(deployment);
      return promptedApiKey ? { mode: "api-key", apiKey: promptedApiKey } : null;
    }
    log.error(
      `MCP authentication is enabled at ${deployment.baseUrl}. Pass --api-key, set CONTEXT7_API_KEY, or rerun without --yes to enter a personal API key securely.`
    );
    process.exitCode = 1;
    return null;
  }
  if (explicitApiKey) return { mode: "api-key", apiKey: explicitApiKey };
  if (options2.oauth) return { mode: "oauth" };
  const resolvedApiKey = await resolveSetupApiKey();
  if (!resolvedApiKey) return null;
  return { mode: "api-key", apiKey: resolvedApiKey };
}
function getSetupValidationError(mode, options2, deployment) {
  if (deployment.kind === "custom") {
    if (mode === "cli") {
      return "--base-url currently supports MCP setup only. Use --mcp with an on-premise deployment.";
    }
    if (options2.stdio) {
      return "--stdio is not supported with --base-url. On-premise setup uses the HTTP /mcp endpoint.";
    }
    if (options2.oauth) {
      return "--oauth is only supported by hosted Context7. Use a personal on-premise API key.";
    }
  }
  if (mode === "mcp" && options2.stdio && options2.oauth) {
    return "--stdio is incompatible with --oauth (OAuth uses the hosted HTTP endpoint).";
  }
  return null;
}
async function resolveMode(options2) {
  if (options2.cli) return "cli";
  if (options2.baseUrl || options2.mcp || options2.yes || options2.oauth || options2.stdio) return "mcp";
  return select({
    message: "How should your agent access Context7?",
    choices: [
      {
        name: `MCP server
    ${pc3.dim("Agent calls Context7 tools via MCP protocol to retrieve up-to-date library docs")}`,
        value: "mcp"
      },
      {
        name: `CLI + Skills
    ${pc3.dim("Installs a find-docs skill that guides your agent to fetch up-to-date library docs using ")}${pc3.dim(pc3.bold("ctx7"))}${pc3.dim(" CLI commands")}`,
        value: "cli"
      }
    ],
    theme: {
      style: {
        highlight: (text) => pc3.green(text),
        answer: (text) => pc3.green(text.split("\n")[0].trim())
      }
    }
  });
}
async function resolveCliAuth(apiKey) {
  if (apiKey) {
    saveTokens({ access_token: apiKey, token_type: "bearer" });
    log.blank();
    log.plain(`${pc3.green("\u2714")} Authenticated`);
    return;
  }
  const validToken = await getValidAccessToken({ recoverExpiredSession: false });
  if (validToken) {
    log.blank();
    log.plain(`${pc3.green("\u2714")} Authenticated`);
    return;
  }
  await performLogin();
}
async function promptAgents() {
  const choices = ALL_AGENT_NAMES.map((name) => ({
    name: getAgent(name).displayName,
    value: name
  }));
  choices.push({ name: "T3 Code (Claude Code and Codex providers)", value: "t3" });
  const message = "Which agents do you want to set up?";
  try {
    const selected = await checkboxWithHover(
      {
        message,
        choices,
        loop: false,
        theme: CHECKBOX_THEME
      },
      { getName: (a) => a === "t3" ? "T3 Code" : getAgent(a).displayName }
    );
    return [
      ...new Set(
        selected.flatMap((agent) => agent === "t3" ? ["claude", "codex"] : [agent])
      )
    ];
  } catch {
    return null;
  }
}
async function resolveAgents(options2, scope) {
  const explicit = getSelectedAgents(options2);
  if (explicit.length > 0) return explicit;
  const detected = await detectAgents(scope);
  if (detected.length > 0 && options2.yes) return detected;
  log.blank();
  const selected = await promptAgents();
  if (!selected) {
    log.warn("Setup cancelled");
    return [];
  }
  return selected;
}
async function installRule(agentName, scope, content) {
  const agent = getAgent(agentName);
  const rule = agent.rule;
  if (rule.kind === "file") {
    const ruleContent = `${rule.contentPrefix ?? ""}${content}`;
    const ruleDir = scope === "global" ? rule.dir("global") : join2(process.cwd(), rule.dir("project"));
    const rulePath = join2(ruleDir, rule.filename);
    await mkdir5(dirname7(rulePath), { recursive: true });
    await writeFile5(rulePath, ruleContent, "utf-8");
    return { status: "installed", path: rulePath };
  }
  const filePath = await resolveAppendRuleFile(rule, scope);
  const escapedMarker = rule.sectionMarker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
  const section = `${rule.sectionMarker}
${content}${rule.sectionMarker}`;
  let existing = "";
  try {
    existing = await readFile5(filePath, "utf-8");
  } catch {
  }
  if (existing.includes(rule.sectionMarker)) {
    const regex = new RegExp(`${escapedMarker}(\\r?\\n)[\\s\\S]*?${escapedMarker}`);
    const updated = existing.replace(
      regex,
      (_match, eol) => section.replace(/\r?\n/g, eol)
    );
    await writeFile5(filePath, updated, "utf-8");
    return { status: "updated", path: filePath };
  }
  const separator = existing.length > 0 && !existing.endsWith("\n") ? "\n\n" : existing.length > 0 ? "\n" : "";
  await mkdir5(dirname7(filePath), { recursive: true });
  await writeFile5(filePath, existing + separator + section + "\n", "utf-8");
  return { status: "installed", path: filePath };
}
async function loadMcpSkillPayload(deployment) {
  const bundled = getBundledMcpSkillFiles();
  if (deployment.kind === "custom") {
    return { files: bundled, status: "installed (bundled)" };
  }
  const files = await fetchSetupSkillFiles("context7-mcp");
  return files ? { files, status: "installed" } : { files: bundled, status: "installed (bundled fallback)" };
}
function resolveEntryToWrite(agent, auth, transport, existingEntry, mcpUrl) {
  if (transport === "stdio" && existingEntry && isStdioContext7Entry(existingEntry)) {
    const apiKey = auth.mode === "api-key" ? auth.apiKey : void 0;
    return patchStdioApiKey(existingEntry, apiKey);
  }
  return agent.mcp.buildEntry(auth, transport, mcpUrl);
}
async function setupAgent(agentName, auth, transport, scope, deployment, mcpUrl, skillPayload) {
  const agent = getAgent(agentName);
  const mcpCandidates = scope === "global" || agent.mcp.projectPaths.length === 0 ? agent.mcp.globalPaths : agent.mcp.projectPaths.map((p) => join2(process.cwd(), p));
  const mcpPath = await resolveMcpPath(mcpCandidates);
  let mcpStatus;
  let signInCommand;
  try {
    let alreadyExists;
    if (mcpPath.endsWith(".toml")) {
      const apiKey = auth.mode === "api-key" ? auth.apiKey : void 0;
      const patched = transport === "stdio" ? await patchTomlStdioApiKey(mcpPath, "context7", apiKey) : false;
      alreadyExists = patched || (await appendTomlServer(
        mcpPath,
        "context7",
        resolveEntryToWrite(agent, auth, transport, void 0, mcpUrl)
      )).alreadyExists;
    } else if (agentName === "opencode") {
      alreadyExists = await writeOpenCodeServer(
        mcpPath,
        (existing) => resolveEntryToWrite(agent, auth, transport, existing, mcpUrl)
      );
    } else {
      const existing = await readJsonConfig(mcpPath);
      const existingJsonEntry = transport === "stdio" ? getJsonServerEntry(existing, agent.mcp.configKey, "context7") : void 0;
      const entry = resolveEntryToWrite(agent, auth, transport, existingJsonEntry, mcpUrl);
      const merged = mergeServerEntry(existing, agent.mcp.configKey, "context7", entry);
      if (!await patchJsonConfig(mcpPath, [agent.mcp.configKey, "context7"], entry)) {
        await writeJsonConfig(mcpPath, merged.config);
      }
      alreadyExists = merged.alreadyExists;
    }
    mcpStatus = `${alreadyExists ? "reconfigured" : "configured"} with ${AUTH_MODE_LABELS[auth.mode]}`;
    if (auth.mode === "oauth") signInCommand = agent.mcp.oauthSignInCommand;
  } catch (err) {
    mcpStatus = `failed: ${err instanceof Error ? err.message : String(err)}`;
  }
  let ruleStatus;
  let rulePath;
  try {
    const ruleContent = deployment.kind === "custom" ? getBundledRuleContent("mcp", agentName) : await getRuleContent("mcp", agentName);
    const result = await installRule(agentName, scope, ruleContent);
    ruleStatus = result.status;
    rulePath = result.path;
  } catch (err) {
    ruleStatus = `failed: ${err instanceof Error ? err.message : String(err)}`;
    rulePath = "";
  }
  const skillDir = scope === "global" ? agent.skill.dir("global") : join2(process.cwd(), agent.skill.dir("project"));
  const skillPath = join2(skillDir, agent.skill.name, "SKILL.md");
  let skillStatus;
  try {
    await installSkillFiles(agent.skill.name, skillPayload.files, skillDir);
    skillStatus = skillPayload.status;
  } catch (err) {
    skillStatus = `failed: ${err instanceof Error ? err.message : String(err)}`;
  }
  return {
    agent: agent.displayName,
    mcpStatus,
    mcpPath,
    ruleStatus,
    rulePath,
    skillStatus,
    skillPath,
    signInCommand
  };
}
function logSkillStatus(skillStatus, skillPath) {
  const skillFailed = skillStatus.startsWith("failed:");
  const skillIcon = skillStatus.startsWith("installed") ? pc3.green("+") : skillFailed ? pc3.red("\u2716") : pc3.dim("~");
  log.plain(`    ${skillIcon} Skill ${skillFailed ? "failed" : skillStatus}`);
  log.plain(`      ${pc3.dim(skillPath)}`);
  if (skillFailed) {
    log.plain(`      ${pc3.red(skillStatus.slice("failed: ".length))}`);
    if (skillStatus.includes("EACCES")) {
      log.plain(
        `      ${pc3.yellow("tip:")} fix permissions with: ${pc3.cyan(`sudo chown -R $(whoami) ${dirname7(dirname7(skillPath))}`)}`
      );
    }
  }
}
async function setupMcp(agents2, options2, scope, deployment) {
  const transport = resolveTransport(options2);
  const auth = await resolveAuth(options2, deployment);
  if (!auth) {
    log.warn("Setup cancelled");
    return;
  }
  log.blank();
  const spinner = ora2("Setting up Context7...").start();
  const mcpUrl = getMcpUrl(deployment, auth);
  const skillPayload = await loadMcpSkillPayload(deployment);
  const results = [];
  for (const agentName of agents2) {
    spinner.text = `Setting up ${getAgent(agentName).displayName}...`;
    results.push(
      await setupAgent(agentName, auth, transport, scope, deployment, mcpUrl, skillPayload)
    );
  }
  spinner.succeed("Context7 setup complete");
  log.blank();
  for (const r of results) {
    log.plain(`  ${pc3.bold(r.agent)}`);
    const mcpIcon = r.mcpStatus.startsWith("configured") || r.mcpStatus.startsWith("reconfigured") ? pc3.green("+") : pc3.dim("~");
    log.plain(`    ${mcpIcon} MCP server ${r.mcpStatus}`);
    log.plain(`      ${pc3.dim(r.mcpPath)}`);
    if (r.signInCommand) {
      log.plain(`      ${pc3.yellow("next:")} sign in with ${pc3.cyan(r.signInCommand)}`);
    }
    const ruleIcon = r.ruleStatus === "installed" ? pc3.green("+") : pc3.dim("~");
    log.plain(`    ${ruleIcon} Rule ${r.ruleStatus}`);
    log.plain(`      ${pc3.dim(r.rulePath)}`);
    logSkillStatus(r.skillStatus, r.skillPath);
  }
  log.blank();
  trackEvent("setup", { agents: agents2, scope, authMode: auth.mode });
}
async function setupCliAgent(agentName, scope, skillFiles) {
  const agent = getAgent(agentName);
  const skillDir = scope === "global" ? agent.skill.dir("global") : join2(process.cwd(), agent.skill.dir("project"));
  let skillStatus;
  try {
    const files = customizeSkillFilesForAgent(agentName, "find-docs", skillFiles);
    await installSkillFiles("find-docs", files, skillDir);
    skillStatus = "installed";
  } catch (err) {
    skillStatus = `failed: ${err instanceof Error ? err.message : String(err)}`;
  }
  const skillPath = join2(skillDir, "find-docs");
  let ruleStatus;
  let rulePath;
  try {
    const result = await installRule(agentName, scope, await getRuleContent("cli", agentName));
    ruleStatus = result.status;
    rulePath = result.path;
  } catch (err) {
    ruleStatus = `failed: ${err instanceof Error ? err.message : String(err)}`;
    rulePath = "";
  }
  return { skillPath, skillStatus, rulePath, ruleStatus };
}
async function setupCli(options2) {
  await resolveCliAuth(options2.apiKey);
  const scope = options2.project ? "project" : "global";
  const agents2 = await resolveAgents(options2, scope);
  if (agents2.length === 0) return;
  log.blank();
  const spinner = ora2("Downloading find-docs skill...").start();
  const downloaded = await fetchSetupSkillFiles("find-docs");
  const files = downloaded ?? getBundledFindDocsSkillFiles();
  if (downloaded) {
    spinner.succeed("Downloaded find-docs skill");
  } else {
    spinner.warn("Could not download find-docs skill, using the bundled copy");
  }
  const installSpinner = ora2("Installing...").start();
  const results = [];
  for (const agentName of agents2) {
    const agentDef = getAgent(agentName);
    installSpinner.text = `Setting up ${agentDef.displayName}...`;
    const r = await setupCliAgent(agentName, scope, files);
    results.push({ agent: agentDef.displayName, ...r });
  }
  installSpinner.succeed("Context7 CLI setup complete");
  log.blank();
  for (const r of results) {
    log.plain(`  ${pc3.bold(r.agent)}`);
    logSkillStatus(r.skillStatus, r.skillPath);
    const ruleIcon = r.ruleStatus === "installed" || r.ruleStatus === "updated" ? pc3.green("+") : pc3.dim("~");
    log.plain(`    ${ruleIcon} Rule ${r.ruleStatus}`);
    log.plain(`      ${pc3.dim(r.rulePath)}`);
  }
  log.blank();
  trackEvent("setup", { mode: "cli" });
}
async function setupCommand(options2) {
  try {
    let deployment;
    try {
      deployment = resolveSetupDeployment(options2.baseUrl);
    } catch (err) {
      log.error(err instanceof Error ? err.message : String(err));
      process.exitCode = 1;
      return;
    }
    const mode = await resolveMode(options2);
    const validationError = getSetupValidationError(mode, options2, deployment);
    if (validationError) {
      log.error(validationError);
      process.exitCode = 1;
      return;
    }
    trackEvent("command", { name: "setup" });
    if (mode === "mcp") {
      const scope = options2.project ? "project" : "global";
      const agents2 = await resolveAgents(options2, scope);
      if (agents2.length === 0) return;
      await setupMcp(agents2, options2, scope, deployment);
    } else {
      await setupCli(options2);
    }
  } catch (err) {
    if (err instanceof Error && err.name === "ExitPromptError") process.exit(0);
    throw err;
  }
}

// src/commands/remove.ts
import pc4 from "picocolors";
import ora3 from "ora";
import { join as join3 } from "path";
import { access as access3, readFile as readFile6, rm, writeFile as writeFile6 } from "fs/promises";
var CHECKBOX_THEME2 = {
  style: {
    highlight: (text) => pc4.green(text),
    disabledChoice: (text) => ` ${pc4.dim("\u25EF")} ${pc4.dim(text)}`
  }
};
var CONTEXT7_SECTION_MARKER = "<!-- context7 -->";
var MODE_SKILLS = {
  mcp: ["context7-mcp"],
  cli: ["find-docs"]
};
var MODE_LABELS = {
  mcp: "MCP",
  cli: "CLI + Skills"
};
function registerRemoveCommand(program2) {
  const command = program2.command("remove").alias("uninstall").description("Remove Context7 setup from your AI coding agent");
  for (const name of ALL_AGENT_NAMES) {
    const agent = getAgent(name);
    command.option(`--${name}`, `Remove from ${agent.displayName}`);
  }
  command.option("--t3", "Remove from T3 Code (Claude Code and Codex providers)").option("--all", "Remove both MCP setup and CLI + Skills setup").option("--mcp", "Remove MCP setup").option("--cli", "Remove CLI + Skills setup").option("-p, --project", "Remove from the current project instead of global config").option("-y, --yes", "Skip confirmation prompts").action(async (options2) => {
    await removeCommand(options2);
  });
}
function getSelectedAgents2(options2) {
  return ALL_AGENT_NAMES.filter(
    (name) => options2[name] || options2.t3 && (name === "claude" || name === "codex")
  );
}
async function promptAgents2(detected) {
  const choices = detected.map((name) => ({
    name: getAgent(name).displayName,
    value: name
  }));
  if (detected.length > 0) {
    log.dim(`Detected: ${detected.map((agent) => getAgent(agent).displayName).join(", ")}`);
  }
  try {
    return await checkboxWithHover(
      {
        message: "Which agents do you want to remove Context7 setup from?",
        choices,
        loop: false,
        theme: CHECKBOX_THEME2
      },
      { getName: (agent) => getAgent(agent).displayName }
    );
  } catch {
    return null;
  }
}
async function promptModes(modes) {
  const choices = modes.map((mode) => ({
    name: MODE_LABELS[mode],
    value: mode
  }));
  try {
    return await checkboxWithHover(
      {
        message: "Which Context7 setup modes do you want to remove?",
        choices,
        loop: false,
        theme: CHECKBOX_THEME2
      },
      { getName: (mode) => MODE_LABELS[mode] }
    );
  } catch {
    return null;
  }
}
async function resolveAgents2(options2, scope) {
  const explicit = getSelectedAgents2(options2);
  if (explicit.length > 0) return explicit;
  const detected = await detectConfiguredAgents(scope);
  if (detected.length > 0 && options2.yes) return detected;
  if (detected.length === 0) {
    log.warn(
      `No Context7 setup detected. Pass one of: ${ALL_AGENT_NAMES.map((name) => `--${name}`).join(", ")}.`
    );
    return [];
  }
  log.blank();
  const selected = await promptAgents2(detected);
  if (!selected) {
    log.warn("Remove cancelled");
    return [];
  }
  return selected;
}
function resolveFlagModes(options2) {
  if (options2.all) return ["mcp", "cli"];
  const selected = [];
  if (options2.mcp) selected.push("mcp");
  if (options2.cli) selected.push("cli");
  return selected.length > 0 ? selected : ["mcp", "cli"];
}
async function pathExists2(path) {
  try {
    await access3(path);
    return true;
  } catch {
    return false;
  }
}
async function hasMcpConfig(agentName, scope) {
  const agent = getAgent(agentName);
  if (scope === "project" && agent.mcp.projectPaths.length === 0) return false;
  const candidates = scope === "global" ? agent.mcp.globalPaths : agent.mcp.projectPaths.map((path) => join3(process.cwd(), path));
  const mcpPath = await resolveMcpPath(candidates);
  if (mcpPath.endsWith(".toml")) {
    return readTomlServerExists(mcpPath, "context7");
  }
  let existing;
  try {
    existing = await readJsonConfig(mcpPath);
  } catch (err) {
    log.warn(
      `Skipped ${mcpPath}: could not parse (${err instanceof Error ? err.message : String(err)})`
    );
    return false;
  }
  if (agentName === "opencode") return hasOpenCodeServer(existing);
  const section = existing[agent.mcp.configKey];
  return !!section && typeof section === "object" && !Array.isArray(section) && "context7" in section;
}
async function hasRule(agentName, scope) {
  const agent = getAgent(agentName);
  const rule = agent.rule;
  if (rule.kind === "file") {
    const ruleDir = scope === "global" ? rule.dir("global") : join3(process.cwd(), rule.dir("project"));
    return pathExists2(join3(ruleDir, rule.filename));
  }
  return await findAppendRuleFile(rule, scope) !== void 0;
}
async function hasSkill(agentName, scope, skillName) {
  const agent = getAgent(agentName);
  const skillsDir = scope === "global" ? agent.skill.dir("global") : join3(process.cwd(), agent.skill.dir("project"));
  return pathExists2(join3(skillsDir, skillName));
}
async function detectAvailableModes(agents2, scope) {
  let hasMcpArtifacts = false;
  let hasCliArtifacts = false;
  let hasRuleArtifacts = false;
  for (const agent of agents2) {
    hasMcpArtifacts = hasMcpArtifacts || await hasMcpConfig(agent, scope) || await hasSkill(agent, scope, MODE_SKILLS.mcp[0]);
    hasCliArtifacts = hasCliArtifacts || await hasSkill(agent, scope, MODE_SKILLS.cli[0]);
    hasRuleArtifacts = hasRuleArtifacts || await hasRule(agent, scope);
  }
  const modes = [];
  if (hasMcpArtifacts) modes.push("mcp");
  if (hasCliArtifacts) modes.push("cli");
  if (modes.length === 0 && hasRuleArtifacts) {
    return ["mcp", "cli"];
  }
  return modes;
}
async function hasAnyContext7Artifacts(agent, scope) {
  return await hasMcpConfig(agent, scope) || await hasRule(agent, scope) || await hasSkill(agent, scope, MODE_SKILLS.mcp[0]) || await hasSkill(agent, scope, MODE_SKILLS.cli[0]);
}
async function detectConfiguredAgents(scope) {
  const detected = [];
  for (const agent of ALL_AGENT_NAMES) {
    if (await hasAnyContext7Artifacts(agent, scope)) {
      detected.push(agent);
    }
  }
  return detected;
}
async function resolveModes(options2, agents2, scope) {
  if (options2.all || options2.mcp || options2.cli) {
    return resolveFlagModes(options2);
  }
  const detectedModes = await detectAvailableModes(agents2, scope);
  if (detectedModes.length <= 1) {
    return detectedModes.length === 1 ? detectedModes : ["mcp", "cli"];
  }
  if (options2.yes) {
    return detectedModes;
  }
  log.blank();
  const selected = await promptModes(detectedModes);
  if (!selected) {
    log.warn("Remove cancelled");
    return [];
  }
  return selected;
}
async function uninstallMcp(agentName, scope) {
  const agent = getAgent(agentName);
  if (scope === "project" && agent.mcp.projectPaths.length === 0) {
    return { status: "not found", path: "" };
  }
  const mcpCandidates = scope === "global" ? agent.mcp.globalPaths : agent.mcp.projectPaths.map((path) => join3(process.cwd(), path));
  const mcpPath = await resolveMcpPath(mcpCandidates);
  try {
    if (mcpPath.endsWith(".toml")) {
      const { removed: removed2 } = await removeTomlServer(mcpPath, "context7");
      return { status: removed2 ? "removed" : "not found", path: mcpPath };
    }
    if (agentName === "opencode") {
      const removed2 = await removeOpenCodeServer(mcpPath);
      return { status: removed2 ? "removed" : "not found", path: mcpPath };
    }
    const existing = await readJsonConfig(mcpPath);
    const { config, removed } = removeServerEntry(existing, agent.mcp.configKey, "context7");
    if (removed) {
      const sectionEmptied = !(agent.mcp.configKey in config);
      const path = sectionEmptied ? [agent.mcp.configKey] : [agent.mcp.configKey, "context7"];
      if (!await patchJsonConfig(mcpPath, path, void 0)) {
        await writeJsonConfig(mcpPath, config);
      }
    }
    return { status: removed ? "removed" : "not found", path: mcpPath };
  } catch (err) {
    return { status: `failed: ${err instanceof Error ? err.message : String(err)}`, path: mcpPath };
  }
}
async function uninstallRule(agentName, scope) {
  const agent = getAgent(agentName);
  const rule = agent.rule;
  if (rule.kind === "file") {
    const rulePath = scope === "global" ? rule.dir("global") : join3(process.cwd(), rule.dir("project"));
    const targetPath = join3(rulePath, rule.filename);
    try {
      await rm(targetPath);
      return { status: "removed", path: targetPath };
    } catch (err) {
      const error = err;
      if (error.code === "ENOENT") return { status: "not found", path: targetPath };
      return { status: `failed: ${error.message}`, path: targetPath };
    }
  }
  const filePath = await findAppendRuleFile(rule, scope) ?? await resolveAppendRuleFile(rule, scope);
  try {
    const existing = await readFile6(filePath, "utf-8");
    if (!existing.includes(CONTEXT7_SECTION_MARKER)) {
      return { status: "not found", path: filePath };
    }
    const escapedMarker = CONTEXT7_SECTION_MARKER.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
    const section = new RegExp(`${escapedMarker}(\\r?\\n)[\\s\\S]*?${escapedMarker}`).exec(
      existing
    );
    if (!section) {
      return { status: "not found", path: filePath };
    }
    const eol = section[1];
    const before = existing.slice(0, section.index).replace(/(?:\r?\n)+$/, "");
    const after = existing.slice(section.index + section[0].length).replace(/^(?:\r?\n)+/, "");
    const updated = (before && after ? `${before}${eol}${eol}${after}` : before || after).replace(
      /(?:\r?\n)*$/,
      ""
    );
    if (updated.length === 0) {
      await rm(filePath);
    } else {
      await writeFile6(filePath, `${updated}${eol}`, "utf-8");
    }
    return { status: "removed", path: filePath };
  } catch (err) {
    const error = err;
    if (error.code === "ENOENT") return { status: "not found", path: filePath };
    return { status: `failed: ${error.message}`, path: filePath };
  }
}
async function uninstallSkills(agentName, scope, skillNames) {
  const agent = getAgent(agentName);
  const skillsDir = scope === "global" ? agent.skill.dir("global") : join3(process.cwd(), agent.skill.dir("project"));
  const results = [];
  for (const skillName of skillNames) {
    const skillPath = join3(skillsDir, skillName);
    try {
      await rm(skillPath, { recursive: true });
      results.push({ name: skillName, status: "removed", path: skillPath });
    } catch (err) {
      const error = err;
      if (error.code === "ENOENT") {
        results.push({ name: skillName, status: "not found", path: skillPath });
      } else {
        results.push({ name: skillName, status: `failed: ${error.message}`, path: skillPath });
      }
    }
  }
  return results;
}
async function uninstallAgent(agentName, scope, modes) {
  const result = { agent: getAgent(agentName).displayName };
  const skillNames = Array.from(new Set(modes.flatMap((mode) => [...MODE_SKILLS[mode]])));
  const shouldRemoveRule = modes.includes("mcp") || modes.includes("cli");
  if (modes.includes("mcp")) {
    result.mcp = await uninstallMcp(agentName, scope);
  }
  if (shouldRemoveRule) {
    result.rule = await uninstallRule(agentName, scope);
  }
  if (skillNames.length > 0) {
    result.skills = await uninstallSkills(agentName, scope, skillNames);
  }
  return result;
}
function iconForStatus(status) {
  if (status === "removed") return pc4.green("-");
  if (status === "not found") return pc4.dim("~");
  return pc4.red("!");
}
function printResults(results, modes) {
  log.blank();
  const shouldPrintRule = modes.includes("mcp") || modes.includes("cli");
  let hasVisibleResults = false;
  for (const result of results) {
    const visibleSkills = result.skills?.filter((skill) => skill.status !== "not found") ?? [];
    const showMcp = modes.includes("mcp") && result.mcp && result.mcp.status !== "not found";
    const showRule = shouldPrintRule && result.rule && result.rule.status !== "not found";
    if (!showMcp && !showRule && visibleSkills.length === 0) {
      continue;
    }
    hasVisibleResults = true;
    log.plain(`  ${pc4.bold(result.agent)}`);
    if (showMcp && result.mcp) {
      log.plain(`    ${iconForStatus(result.mcp.status)} MCP config ${result.mcp.status}`);
      log.plain(`      ${pc4.dim(result.mcp.path)}`);
    }
    if (showRule && result.rule) {
      log.plain(`    ${iconForStatus(result.rule.status)} Rule ${result.rule.status}`);
      log.plain(`      ${pc4.dim(result.rule.path)}`);
    }
    for (const skill of visibleSkills) {
      log.plain(`    ${iconForStatus(skill.status)} Skill ${skill.name} ${skill.status}`);
      log.plain(`      ${pc4.dim(skill.path)}`);
    }
  }
  if (hasVisibleResults) {
    log.blank();
  } else {
    log.plain(`  ${pc4.dim("No matching Context7 setup was found to remove.")}`);
    log.blank();
  }
}
async function removeCommand(options2) {
  trackEvent("command", { name: "remove" });
  const scope = options2.project ? "project" : "global";
  const agents2 = await resolveAgents2(options2, scope);
  if (agents2.length === 0) return;
  const modes = await resolveModes(options2, agents2, scope);
  if (modes.length === 0) return;
  log.blank();
  const spinner = ora3("Removing Context7 setup...").start();
  const results = [];
  for (const agentName of agents2) {
    spinner.text = `Cleaning up ${getAgent(agentName).displayName}...`;
    results.push(await uninstallAgent(agentName, scope, modes));
  }
  spinner.succeed("Context7 cleanup complete");
  printResults(results, modes);
  trackEvent("remove", { agents: agents2, scope, modes });
}

// src/commands/docs.ts
import pc5 from "picocolors";
import ora4 from "ora";

// src/utils/library-id.ts
function recoverLibraryId(input) {
  if (input.startsWith("//")) return input.replace(/^\/+/, "/");
  if (input.startsWith("/")) return input;
  if (!/^[A-Za-z]:[\\/]/.test(input)) return input;
  const normalized = input.replace(/\\/g, "/");
  const scoopMatch = normalized.match(
    /^[A-Za-z]:\/.*\/apps\/git\/(?:v?\d+(?:\.\d+)+[^/]*|current)\/(.+)$/i
  );
  if (scoopMatch) return `/${scoopMatch[1]}`;
  const match = normalized.match(/^[A-Za-z]:\/.*?\/(?:Git|PortableGit|git-bash)\/(.+)$/i);
  return match ? `/${match[1]}` : input;
}

// src/commands/docs.ts
var isTTY = process.stdout.isTTY;
function getReputationLabel(score) {
  if (score === void 0 || score < 0) return "Unknown";
  if (score >= 7) return "High";
  if (score >= 4) return "Medium";
  return "Low";
}
function formatLibraryResult(lib, index) {
  const lines = [];
  lines.push(`${pc5.dim(`${index + 1}.`)} ${pc5.bold(`Title: ${lib.title}`)}`);
  lines.push(`   ${pc5.cyan(`Context7-compatible library ID: ${lib.id}`)}`);
  if (lib.description) {
    lines.push(`   ${pc5.dim(`Description: ${lib.description}`)}`);
  }
  if (lib.totalSnippets) {
    lines.push(`   ${pc5.dim(`Code Snippets: ${lib.totalSnippets}`)}`);
  }
  if (lib.trustScore !== void 0) {
    lines.push(`   ${pc5.dim(`Source Reputation: ${getReputationLabel(lib.trustScore)}`)}`);
  }
  if (lib.benchmarkScore !== void 0 && lib.benchmarkScore > 0) {
    lines.push(`   ${pc5.dim(`Benchmark Score: ${lib.benchmarkScore}`)}`);
  }
  if (lib.versions && lib.versions.length > 0) {
    lines.push(`   ${pc5.dim(`Versions: ${lib.versions.join(", ")}`)}`);
  }
  if (hasSourceCode(lib)) {
    lines.push(`   ${pc5.dim("Source code: searchable with ctx7 code")}`);
  }
  return lines.join("\n");
}
async function resolveCommand(library, query, options2) {
  trackEvent("command", { name: "library" });
  const accessToken = await getValidAccessToken();
  const spinner = isTTY ? ora4(`Searching for "${library}"...`).start() : null;
  let data;
  try {
    data = await resolveLibrary(library, query, accessToken);
  } catch (err) {
    spinner?.fail(`Error: ${err instanceof Error ? err.message : String(err)}`);
    if (!spinner) log.error(err instanceof Error ? err.message : String(err));
    process.exitCode = 1;
    return;
  }
  if (data.error) {
    spinner?.fail(data.message || data.error);
    if (!spinner) log.error(data.message || data.error);
    process.exitCode = 1;
    return;
  }
  if (!data.results || data.results.length === 0) {
    spinner?.warn(`No libraries found matching "${library}"`);
    if (!spinner) log.warn(`No libraries found matching "${library}"`);
    return;
  }
  const results = data.results;
  spinner?.stop();
  if (options2.json) {
    console.log(JSON.stringify(results, null, 2));
    return;
  }
  log.blank();
  if (data.searchFilterApplied) {
    log.warn(
      "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"
    );
    log.blank();
  }
  for (let i = 0; i < results.length; i++) {
    log.plain(formatLibraryResult(results[i], i));
    log.blank();
  }
  if (isTTY && results.length > 0) {
    const best = results[0];
    log.plain(
      `${pc5.bold("Quick command:")}
  ${pc5.cyan(`ctx7 docs "${best.id}" "<your question>"`)}`
    );
    log.blank();
  }
}
async function queryCommand(libraryId, query, options2) {
  trackEvent("command", { name: "docs" });
  libraryId = recoverLibraryId(libraryId);
  if (!libraryId.startsWith("/") || !/^\/[^/]+\/[^/]/.test(libraryId)) {
    log.error(`Invalid library ID: "${libraryId}"`);
    log.info(`Expected format: /owner/repo or /owner/repo/version (e.g., /reactjs/react.dev)`);
    log.info(`Run "ctx7 library <name>" to find the correct ID`);
    if (process.platform === "win32") {
      log.info(
        `On Git Bash, prefix the ID with an extra slash to avoid path conversion: ctx7 docs "//reactjs/react.dev" "<your question>"`
      );
    }
    process.exitCode = 1;
    return;
  }
  const accessToken = await getValidAccessToken();
  const spinner = isTTY ? ora4(`Fetching docs for "${libraryId}"...`).start() : null;
  const outputType = options2.json ? "json" : "txt";
  let result;
  try {
    result = await getLibraryContext(
      libraryId,
      query,
      { type: outputType, language: options2.language },
      accessToken
    );
  } catch (err) {
    spinner?.fail(`Error: ${err instanceof Error ? err.message : String(err)}`);
    if (!spinner) log.error(err instanceof Error ? err.message : String(err));
    process.exitCode = 1;
    return;
  }
  if (typeof result === "string") {
    spinner?.stop();
    console.log(result);
    return;
  }
  const ctx = result;
  if (ctx.error) {
    if (ctx.redirectUrl) {
      spinner?.warn("Library has been redirected");
      if (!spinner) log.warn("Library has been redirected");
      log.info(`New ID: ${pc5.cyan(ctx.redirectUrl)}`);
      log.info(`Run: ${pc5.cyan(`ctx7 docs "${ctx.redirectUrl}" "${query}"`)}`);
      process.exitCode = 1;
      return;
    }
    spinner?.fail(ctx.message || ctx.error);
    if (!spinner) log.error(ctx.message || ctx.error);
    process.exitCode = 1;
    return;
  }
  const total = (ctx.codeSnippets?.length || 0) + (ctx.infoSnippets?.length || 0);
  if (total === 0) {
    spinner?.warn(`No documentation found for: "${query}"`);
    if (!spinner) log.warn(`No documentation found for: "${query}"`);
    return;
  }
  spinner?.stop();
  if (options2.json) {
    console.log(JSON.stringify(ctx, null, 2));
    return;
  }
  log.blank();
  if (ctx.codeSnippets) {
    for (const snippet of ctx.codeSnippets) {
      log.plain(pc5.bold(snippet.codeTitle));
      if (snippet.codeDescription) log.dim(snippet.codeDescription);
      log.blank();
      for (const code of snippet.codeList) {
        log.plain("```" + code.language);
        log.plain(code.code);
        log.plain("```");
        log.blank();
      }
    }
  }
  if (ctx.infoSnippets) {
    for (const snippet of ctx.infoSnippets) {
      if (snippet.breadcrumb) log.plain(pc5.bold(snippet.breadcrumb));
      log.plain(snippet.content);
      log.blank();
    }
  }
}
function wholeNumber(value, name, limits) {
  if (value === void 0) return void 0;
  const n = value.trim() === "" ? NaN : Number(value);
  if (!Number.isFinite(n)) {
    log.error(`${name} must be a number.`);
    process.exitCode = 1;
    return null;
  }
  return Math.min(limits.max, Math.max(limits.min, Math.trunc(n)));
}
async function codeCommand(libraryId, pattern, options2) {
  trackEvent("command", { name: "code" });
  libraryId = recoverLibraryId(libraryId);
  if (!/^\/[^/]+\/[^/]/.test(libraryId)) {
    log.error(`Invalid library ID: "${libraryId}"`);
    log.info(
      `Expected format: /owner/repo or /owner/repo/version. Run "ctx7 library <name>" to find it.`
    );
    process.exitCode = 1;
    return;
  }
  const op = options2.op;
  if (op && !CODE_OPERATIONS.includes(op)) {
    log.error(`Invalid --op "${op}". Use one of: ${CODE_OPERATIONS.join(", ")}.`);
    process.exitCode = 1;
    return;
  }
  const context = wholeNumber(options2.context, "--context", CODE_LIMITS.context);
  const headLimit = wholeNumber(options2.headLimit, "--head-limit", CODE_LIMITS.headLimit);
  const offset = wholeNumber(options2.offset, "--offset", CODE_LIMITS.offset);
  if (context === null || headLimit === null || offset === null) return;
  const accessToken = await getValidAccessToken();
  const target = options2.tag ? `${libraryId} ${options2.tag}` : libraryId;
  const spinner = isTTY ? ora4(`Searching the source of "${target}"...`).start() : null;
  let result;
  try {
    result = await getLibraryCode(
      libraryId,
      pattern,
      {
        version: options2.tag,
        operation: op,
        fixedStrings: options2.fixedStrings,
        ignoreCase: options2.ignoreCase,
        glob: options2.glob,
        path: options2.path,
        context,
        outputMode: options2.count ? "count" : options2.filesWithMatches ? "files_with_matches" : void 0,
        headLimit: headLimit || void 0,
        offset: offset || void 0,
        json: options2.json
      },
      accessToken
    );
  } catch (err) {
    const message = err instanceof Error ? err.message : String(err);
    spinner?.fail(`Error: ${message}`);
    if (!spinner) log.error(message);
    process.exitCode = 1;
    return;
  }
  spinner?.stop();
  if (result.kind === "indexing") {
    if (options2.json) {
      console.log(
        JSON.stringify({
          error: "indexing",
          message: result.message,
          retryAfter: result.retryAfter
        })
      );
    } else {
      log.warn(result.message);
      log.info(
        `Run the same command again in about ${result.retryAfter} seconds, at most twice. Meanwhile, use "ctx7 docs".`
      );
    }
    process.exitCode = 75;
    return;
  }
  if (result.kind === "error") {
    if (options2.json)
      console.log(JSON.stringify({ error: result.error, message: result.message || result.error }));
    else log.error(result.message || result.error);
    process.exitCode = 1;
    return;
  }
  console.log(result.body);
}
function registerDocsCommands(program2) {
  program2.command("library").argument("<name>", "Library name to search for").argument("[query]", "What to look up in the library's documentation").option("--json", "Output as JSON").description("Resolve a library name to a Context7 library ID").action(async (name, query, options2) => {
    await resolveCommand(name, query, options2);
  });
  program2.command("docs").argument("<libraryId>", "Context7 library ID (e.g., /reactjs/react.dev)").argument(
    "<query>",
    "Single-topic question to get docs for (run a separate query per distinct concept, unless asking how they interact)"
  ).option("--json", "Output as JSON").option(
    "--language <name>",
    "Programming language you write in (e.g., Python, Go); ranks code examples in that language first"
  ).description("Query documentation for a library").action(
    async (libraryId, query, options2) => {
      await queryCommand(libraryId, query, options2);
    }
  );
  program2.command("code").argument(
    "<libraryId>",
    "Context7 library ID of a Git repository, with an optional tag (e.g., /vercel/next.js or /colinhacks/zod/v3.24.2)"
  ).argument("[pattern]", "A regex to grep for, or a symbol for --op definition/references/read").option("--op <operation>", `grep (default), definition, references, outline, read or list`).option("-F, --fixed-strings", "Treat the pattern as plain text").option("-i, --ignore-case", "Case-insensitive search").option(
    "-g, --glob <glob>",
    "Only files matching a glob; start with ! to exclude. Repeat for more",
    (value, previous = []) => [...previous, value]
  ).option("-C, --context <n>", "Lines of context around each match (0 to 10)").option("-l, --files-with-matches", "Print only the paths of matching files").option("-c, --count", "Print the number of matches per file").option(
    "--path <path>",
    "A folder or file of the library; for --op read, a file with #L10-L40; for --op definition, a library file that uses the symbol"
  ).option(
    "--head-limit <n>",
    "Maximum results, 1 to 500 (default: 20; definition 10, outline 50, list 100)"
  ).option("--offset <n>", "Skip this many results (0 to 10000)").option("--json", "Output as JSON").option(
    "--tag <tag>",
    "A git tag or commit of the library, e.g. v3.24.2 (default: the docs commit)"
  ).description("Search and read a library's source code at a version, like ripgrep").action(async (libraryId, pattern, options2) => {
    await codeCommand(libraryId, pattern, options2);
  });
}

// src/commands/search.ts
import ora5 from "ora";
var isTTY2 = process.stdout.isTTY;
async function searchCommand(query, options2) {
  trackEvent("command", { name: "search" });
  const accessToken = await getValidAccessToken();
  const spinner = isTTY2 ? ora5("Searching documentation...").start() : null;
  let result;
  try {
    result = await searchDocumentation(
      query,
      {
        type: options2.json ? "json" : "txt",
        // Git Bash on Windows rewrites "/owner/repo" into a Windows path; recover it.
        libraries: options2.library?.map(recoverLibraryId),
        version: options2.libraryVersion,
        language: options2.language
      },
      accessToken
    );
  } catch (err) {
    spinner?.fail(err instanceof Error ? err.message : String(err));
    if (!spinner) log.error(err instanceof Error ? err.message : String(err));
    process.exitCode = 1;
    return;
  }
  spinner?.stop();
  console.log(typeof result === "string" ? result : JSON.stringify(result, null, 2));
}
function registerSearchCommand(program2) {
  program2.command("search").argument("<query>", "Question or task to search documentation for").option(
    "--library <name>",
    "Library name or Context7 library ID to search (e.g., Next.js, /vercel/next.js); repeat for up to four libraries",
    (name, names = []) => [...names, name]
  ).option("--library-version <version>", "Library version to search (e.g., 15.4.0)").option(
    "--language <name>",
    "Programming language you write in (e.g., Python, Go); prefers results in that language"
  ).option("--json", "Output as JSON").description("Search documentation across all libraries in one step").action(async (query, options2) => {
    await searchCommand(query, options2);
  });
}

// src/commands/upgrade.ts
import { confirm } from "@inquirer/prompts";
import { spawn } from "child_process";
import pc6 from "picocolors";

// src/utils/update-check.ts
import { dirname as dirname8 } from "path";
import { mkdir as mkdir6, readFile as readFile7, writeFile as writeFile7 } from "fs/promises";
var DEFAULT_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
function getStateFilePath(stateFile) {
  return stateFile ?? getUpdateStateFilePath();
}
async function readStateFilePath(stateFile) {
  if (stateFile) {
    return stateFile;
  }
  return resolveReadPath(UPDATE_STATE_FILE_NAME, getUpdateStateFilePath());
}
async function writeStateFilePath(stateFile) {
  const path = getStateFilePath(stateFile);
  if (!stateFile) {
    await migrateLegacyFile(UPDATE_STATE_FILE_NAME, path);
  }
  return path;
}
async function readUpdateState(stateFile) {
  try {
    const raw = await readFile7(await readStateFilePath(stateFile), "utf-8");
    return JSON.parse(raw);
  } catch {
    return {};
  }
}
async function writeUpdateState(state, stateFile) {
  const path = await writeStateFilePath(stateFile);
  await mkdir6(dirname8(path), { recursive: true });
  await writeFile7(path, JSON.stringify(state, null, 2) + "\n", "utf-8");
}
function compareVersions(a, b) {
  const normalize = (version) => version.split("-", 1)[0].split(".").map((part) => Number.parseInt(part, 10) || 0);
  const left = normalize(a);
  const right = normalize(b);
  const max = Math.max(left.length, right.length);
  for (let i = 0; i < max; i++) {
    const diff = (left[i] ?? 0) - (right[i] ?? 0);
    if (diff !== 0) return diff;
  }
  return Number(b.includes("-")) - Number(a.includes("-"));
}
function detectInstallMethod(env = process.env) {
  const execPath = env.npm_execpath?.toLowerCase() ?? "";
  const npmCommand = env.npm_command?.toLowerCase() ?? "";
  const userAgent = env.npm_config_user_agent?.toLowerCase() ?? "";
  if (execPath.includes("pnpm") && npmCommand === "dlx") return "pnpm-dlx";
  if (execPath.includes("pnpm")) return "pnpm-global";
  if (execPath.includes("bun") && npmCommand === "x") return "bunx";
  if (execPath.includes("bun")) return "bun-global";
  if (execPath.includes("npm") && npmCommand === "exec") return "npx";
  if (execPath.includes("npm")) return "npm-global";
  if (userAgent.startsWith("pnpm/")) return "pnpm-global";
  if (userAgent.startsWith("bun/")) return "bun-global";
  if (userAgent.startsWith("npm/")) return "npm-global";
  return "unknown";
}
function getUpgradePlan(installMethod = detectInstallMethod(), packageName = NAME) {
  switch (installMethod) {
    case "pnpm-global":
      return {
        installMethod,
        command: "pnpm",
        args: ["add", "-g", `${packageName}@latest`],
        displayCommand: `pnpm add -g ${packageName}@latest`,
        canRun: true,
        needsExplicitVersion: false
      };
    case "bun-global":
      return {
        installMethod,
        command: "bun",
        args: ["add", "-g", `${packageName}@latest`],
        displayCommand: `bun add -g ${packageName}@latest`,
        canRun: true,
        needsExplicitVersion: false
      };
    case "npx":
      return {
        installMethod,
        command: "npx",
        args: [`${packageName}@latest`],
        displayCommand: `npx ${packageName}@latest <command>`,
        canRun: false,
        needsExplicitVersion: true
      };
    case "pnpm-dlx":
      return {
        installMethod,
        command: "pnpm",
        args: ["dlx", `${packageName}@latest`],
        displayCommand: `pnpm dlx ${packageName}@latest <command>`,
        canRun: false,
        needsExplicitVersion: true
      };
    case "bunx":
      return {
        installMethod,
        command: "bunx",
        args: [`${packageName}@latest`],
        displayCommand: `bunx ${packageName}@latest <command>`,
        canRun: false,
        needsExplicitVersion: true
      };
    case "unknown":
      return {
        installMethod,
        command: "npm",
        args: ["install", "-g", `${packageName}@latest`],
        displayCommand: `npm install -g ${packageName}@latest`,
        canRun: false,
        needsExplicitVersion: false
      };
    case "npm-global":
    default:
      return {
        installMethod: "npm-global",
        command: "npm",
        args: ["install", "-g", `${packageName}@latest`],
        displayCommand: `npm install -g ${packageName}@latest`,
        canRun: true,
        needsExplicitVersion: false
      };
  }
}
async function fetchLatestVersion(packageName = NAME) {
  try {
    const response = await fetch(
      `https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest`,
      {
        headers: { Accept: "application/json" },
        signal: AbortSignal.timeout(1500)
      }
    );
    if (!response.ok) return null;
    const data = await response.json();
    return typeof data.version === "string" ? data.version : null;
  } catch {
    return null;
  }
}
async function checkForUpdates(options2 = {}) {
  const now = options2.now ?? Date.now();
  const cacheTtlMs = options2.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS;
  const stateFile = options2.stateFile;
  const state = await readUpdateState(stateFile);
  const isStale = options2.force || !state.lastCheckedAt || now - state.lastCheckedAt >= cacheTtlMs || !state.latestVersion;
  let latestVersion = state.latestVersion ?? null;
  if (isStale) {
    const fetchedVersion = await fetchLatestVersion();
    if (fetchedVersion) {
      latestVersion = fetchedVersion;
      await writeUpdateState(
        {
          ...state,
          latestVersion: fetchedVersion,
          lastCheckedAt: now
        },
        stateFile
      );
    }
  }
  if (!latestVersion) return null;
  const installMethod = detectInstallMethod();
  return {
    currentVersion: VERSION,
    latestVersion,
    updateAvailable: compareVersions(latestVersion, VERSION) > 0,
    installMethod,
    upgradePlan: getUpgradePlan(installMethod)
  };
}
async function shouldShowUpdateNotification(info, options2 = {}) {
  if (!info.updateAvailable) return false;
  const now = options2.now ?? Date.now();
  const cooldownMs = options2.cooldownMs ?? DEFAULT_CACHE_TTL_MS;
  const state = await readUpdateState(options2.stateFile);
  if (state.notifiedVersion === info.latestVersion && state.lastNotifiedAt && now - state.lastNotifiedAt < cooldownMs) {
    return false;
  }
  return true;
}
async function markUpdateNotificationShown(latestVersion, options2 = {}) {
  const now = options2.now ?? Date.now();
  const state = await readUpdateState(options2.stateFile);
  await writeUpdateState(
    {
      ...state,
      notifiedVersion: latestVersion,
      lastNotifiedAt: now
    },
    options2.stateFile
  );
}
function shouldSkipUpdateNotifier(argv = process.argv) {
  return argv.some(
    (arg) => arg === "--json" || arg === "-v" || arg === "--version" || arg === "--base-url" || arg.startsWith("--base-url=")
  );
}

// src/commands/upgrade.ts
function registerUpgradeCommand(program2) {
  program2.command("upgrade").description("Check for a newer ctx7 version and upgrade when possible").option("-y, --yes", "Run the suggested upgrade command without prompting").option("--check", "Only check for updates without running the upgrade command").action(async (options2) => {
    await upgradeCommand(options2);
  });
}
function runCommand(command, args) {
  return new Promise((resolve3, reject) => {
    const child = spawn(command, args, {
      stdio: "inherit",
      shell: process.platform === "win32"
    });
    child.on("error", reject);
    child.on("close", (code) => resolve3(code));
  });
}
async function runUpgradePlan(plan) {
  return runCommand(plan.command, plan.args);
}
function showUpgradeFailureHelp(plan) {
  log.info(`Try rerunning: ${pc6.cyan(plan.displayCommand)}`);
  const isGlobalNpmInstall = (plan.installMethod === "npm-global" || plan.installMethod === "unknown") && plan.command === "npm" && plan.args.includes("-g");
  const isGlobalAltInstall = (plan.installMethod === "pnpm-global" || plan.installMethod === "bun-global") && plan.args.includes("-g");
  if (isGlobalNpmInstall) {
    log.dim(
      "If this failed due to permissions, your global npm directory may require elevated privileges on this machine."
    );
  } else if (isGlobalAltInstall) {
    log.dim(
      "If this failed due to permissions, your global package manager install location may require additional privileges on this machine."
    );
  }
}
async function maybeShowUpgradeNotice(options2 = {}) {
  const actionName = options2.actionName ?? "";
  const argv = options2.argv ?? process.argv;
  const isInteractive = options2.isInteractive ?? Boolean(process.stdout.isTTY && process.stdin.isTTY);
  if (!isInteractive || shouldSkipUpdateNotifier(argv) || actionName === "upgrade") {
    return;
  }
  const info = await checkForUpdates();
  if (!info || !info.updateAvailable || !await shouldShowUpdateNotification(info)) {
    return;
  }
  log.blank();
  if (info.upgradePlan.needsExplicitVersion) {
    log.box([
      `${pc6.white(pc6.bold("Update available:"))} ${pc6.green(pc6.bold(`v${info.currentVersion}`))} ${pc6.dim("->")} ${pc6.green(pc6.bold(`v${info.latestVersion}`))}`,
      `${pc6.white("Use")} ${pc6.yellow(pc6.bold(info.upgradePlan.displayCommand))} ${pc6.white("to run the latest version")}`
    ]);
    await markUpdateNotificationShown(info.latestVersion);
    log.blank();
    return;
  }
  if (!info.upgradePlan.canRun) {
    log.box([
      `${pc6.white(pc6.bold("Update available:"))} ${pc6.green(pc6.bold(`v${info.currentVersion}`))} ${pc6.dim("->")} ${pc6.green(pc6.bold(`v${info.latestVersion}`))}`,
      `${pc6.white("Run")} ${pc6.yellow(pc6.bold("ctx7 upgrade"))} ${pc6.white("for update steps")}`,
      `${pc6.white("Or run")} ${pc6.yellow(info.upgradePlan.displayCommand)}`
    ]);
    await markUpdateNotificationShown(info.latestVersion);
    log.blank();
    return;
  }
  log.box([
    `${pc6.white(pc6.bold("Update available:"))} ${pc6.green(pc6.bold(`v${info.currentVersion}`))} ${pc6.dim("->")} ${pc6.green(pc6.bold(`v${info.latestVersion}`))}`,
    `${pc6.white("Run")} ${pc6.yellow(pc6.bold("ctx7 upgrade"))} ${pc6.white("to update now")}`,
    `${pc6.white("Or run")} ${pc6.yellow(info.upgradePlan.displayCommand)}`
  ]);
  await markUpdateNotificationShown(info.latestVersion);
  log.blank();
}
async function upgradeCommand(options2) {
  trackEvent("command", { name: "upgrade" });
  const info = await checkForUpdates({ force: true });
  const plan = info?.upgradePlan ?? getUpgradePlan();
  if (!info) {
    log.warn("Couldn't check for updates right now.");
    log.info(`Try again later or run ${pc6.cyan(plan.displayCommand)} manually.`);
    return;
  }
  if (!info.updateAvailable) {
    log.success(`ctx7 is up to date (${pc6.bold(`v${VERSION}`)})`);
    return;
  }
  log.blank();
  log.info(
    `Update available: ${pc6.bold(`v${info.currentVersion}`)} ${pc6.dim("->")} ${pc6.bold(`v${info.latestVersion}`)}`
  );
  if (plan.needsExplicitVersion) {
    log.info(`You're using an ephemeral runner (${plan.installMethod}).`);
    log.info(`Use ${pc6.cyan(plan.displayCommand)} to run the latest version immediately.`);
    log.info(`Or install globally with ${pc6.cyan("npm install -g ctx7@latest")}.`);
    return;
  }
  if (!plan.canRun) {
    log.info(`Run ${pc6.cyan(plan.displayCommand)} to update your installed version.`);
    return;
  }
  log.info(`Upgrade command: ${pc6.cyan(plan.displayCommand)}`);
  if (options2.check) {
    return;
  }
  let shouldRun = options2.yes ?? false;
  if (!shouldRun && process.stdout.isTTY) {
    shouldRun = await confirm({
      message: `Run ${plan.displayCommand} now?`,
      default: true
    });
  }
  if (!shouldRun) {
    log.dim("Upgrade skipped.");
    return;
  }
  log.blank();
  const exitCode = await runUpgradePlan(plan);
  if (exitCode === 0) {
    log.blank();
    log.success("Upgrade complete.");
    log.info(`Run ${pc6.cyan("ctx7 --version")} to verify the installed version.`);
    return;
  }
  log.blank();
  log.error(`Upgrade command exited with code ${exitCode ?? "unknown"}.`);
  showUpgradeFailureHelp(plan);
  process.exitCode = 1;
}

// src/index.ts
var brand = {
  primary: pc7.green,
  dim: pc7.dim
};
var program = new Command();
program.name("ctx7").description("Context7 CLI - Fetch documentation context and configure Context7").version(VERSION, "-v, --version").option("--base-url <url>", "Use a custom Context7 deployment URL").hook("preAction", (thisCommand) => {
  const opts = thisCommand.opts();
  if (opts.baseUrl) {
    setBaseUrl(opts.baseUrl);
  }
}).hook("preAction", async (_thisCommand, actionCommand) => {
  await maybeShowUpgradeNotice({
    actionName: actionCommand.name(),
    argv: process.argv
  });
}).addHelpText(
  "after",
  `
Examples:
  ${brand.dim("# Configure Context7 for your coding agent")}
  ${brand.primary("npx ctx7 setup")}
  ${brand.primary("npx ctx7 setup --mcp")}
  ${brand.primary("npx ctx7 setup --cli")}
  ${brand.primary("npx ctx7 setup --mcp --base-url https://context7.internal.example --codex")}

  ${brand.dim("# Remove Context7 setup")}
  ${brand.primary("npx ctx7 remove --cursor")}
  ${brand.primary("npx ctx7 remove --cursor --all")}
  ${brand.primary("npx ctx7 remove --cursor --cli")}
  ${brand.primary("npx ctx7 remove --claude --mcp")}

  ${brand.dim("# Query library documentation")}
  ${brand.primary('npx ctx7 search "how to stream an OpenAI response from a Next.js route"')}
  ${brand.primary('npx ctx7 library react "how to use hooks"')}
  ${brand.primary('npx ctx7 docs /reactjs/react.dev "useEffect examples"')}
`
);
registerAuthCommands(program);
registerSetupCommand(program);
registerRemoveCommand(program);
registerDocsCommands(program);
registerSearchCommand(program);
registerUpgradeCommand(program);
program.action(() => {
  console.log("");
  const banner = figlet.textSync("Context7", { font: "ANSI Shadow" });
  console.log(brand.primary(banner));
  console.log(brand.dim("  Documentation context for AI coding agents"));
  console.log("");
  console.log("  Quick start:");
  console.log(`    ${brand.primary("npx ctx7 setup")}`);
  console.log(`    ${brand.primary('npx ctx7 search "how to validate a request body"')}`);
  console.log(`    ${brand.primary('npx ctx7 docs /reactjs/react.dev "useEffect examples"')}`);
  console.log("");
  console.log(`  Run ${brand.primary("npx ctx7 --help")} for all commands and options`);
  console.log("");
});
await program.parseAsync();
//# sourceMappingURL=index.js.map