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