ctx7 0.2.4 → 0.3.0

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