ctx7 0.3.8 → 0.3.10
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 +343 -92
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-WKOIWR6Y.js +0 -120
- package/dist/chunk-WKOIWR6Y.js.map +0 -1
- package/dist/mcp-writer-IYBCUACD.js +0 -20
- package/dist/mcp-writer-IYBCUACD.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,11 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
appendTomlServer,
|
|
4
|
-
mergeServerEntry,
|
|
5
|
-
readJsonConfig,
|
|
6
|
-
resolveMcpPath,
|
|
7
|
-
writeJsonConfig
|
|
8
|
-
} from "./chunk-WKOIWR6Y.js";
|
|
9
2
|
|
|
10
3
|
// src/index.ts
|
|
11
4
|
import { Command } from "commander";
|
|
@@ -36,6 +29,7 @@ function parseSkillInput(input2) {
|
|
|
36
29
|
}
|
|
37
30
|
|
|
38
31
|
// src/utils/github.ts
|
|
32
|
+
import { execSync } from "child_process";
|
|
39
33
|
var GITHUB_API = "https://api.github.com";
|
|
40
34
|
var GITHUB_RAW = "https://raw.githubusercontent.com";
|
|
41
35
|
function parseGitHubUrl(url) {
|
|
@@ -76,6 +70,115 @@ function parseGitHubUrl(url) {
|
|
|
76
70
|
return null;
|
|
77
71
|
}
|
|
78
72
|
}
|
|
73
|
+
function getGitHubToken() {
|
|
74
|
+
const envToken = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
|
|
75
|
+
if (envToken) return envToken;
|
|
76
|
+
try {
|
|
77
|
+
return execSync("gh auth token", { stdio: ["pipe", "pipe", "ignore"] }).toString().trim();
|
|
78
|
+
} catch {
|
|
79
|
+
return void 0;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function parseSkillFrontmatter(content) {
|
|
83
|
+
const frontmatterMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
84
|
+
if (!frontmatterMatch) return null;
|
|
85
|
+
const frontmatter = frontmatterMatch[1];
|
|
86
|
+
const nameMatch = frontmatter.match(/^name:\s*(.+)$/m);
|
|
87
|
+
if (!nameMatch) return null;
|
|
88
|
+
const name = nameMatch[1].trim().replace(/^["']|["']$/g, "").toLowerCase();
|
|
89
|
+
let description = "";
|
|
90
|
+
const multiLineMatch = frontmatter.match(/^description:\s*([|>])-?\s*$/m);
|
|
91
|
+
if (multiLineMatch) {
|
|
92
|
+
const descLineIndex = frontmatter.indexOf("description:");
|
|
93
|
+
const lines = frontmatter.slice(descLineIndex).split("\n").slice(1);
|
|
94
|
+
const indentedLines = [];
|
|
95
|
+
for (const line of lines) {
|
|
96
|
+
if (line.trim() === "") {
|
|
97
|
+
indentedLines.push("");
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (/^\s+/.test(line)) {
|
|
101
|
+
indentedLines.push(line);
|
|
102
|
+
} else {
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const firstNonEmpty = indentedLines.find((l) => l.trim().length > 0);
|
|
107
|
+
const indent = firstNonEmpty?.match(/^(\s+)/)?.[1].length ?? 0;
|
|
108
|
+
description = indentedLines.map((line) => line.slice(indent)).join(" ").replace(/\s+/g, " ").trim();
|
|
109
|
+
} else {
|
|
110
|
+
const singleMatch = frontmatter.match(/^description:\s*(.+)$/m);
|
|
111
|
+
if (singleMatch) {
|
|
112
|
+
const value = singleMatch[1].trim();
|
|
113
|
+
if (!["|", ">", "|-", ">-"].includes(value)) {
|
|
114
|
+
description = value.replace(/^["']|["']$/g, "");
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (!description) return null;
|
|
119
|
+
return { name, description };
|
|
120
|
+
}
|
|
121
|
+
function getGitHubHeaders() {
|
|
122
|
+
const ghToken = getGitHubToken();
|
|
123
|
+
return {
|
|
124
|
+
Accept: "application/vnd.github.v3+json",
|
|
125
|
+
"User-Agent": "context7-cli",
|
|
126
|
+
...ghToken && { Authorization: `token ${ghToken}` }
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
async function fetchRepoTree(owner, repo, branch, headers) {
|
|
130
|
+
const treeUrl = `${GITHUB_API}/repos/${owner}/${repo}/git/trees/${branch}?recursive=1`;
|
|
131
|
+
const response = await fetch(treeUrl, { headers });
|
|
132
|
+
if (!response.ok) return null;
|
|
133
|
+
return await response.json();
|
|
134
|
+
}
|
|
135
|
+
async function fetchDefaultBranch(owner, repo, headers) {
|
|
136
|
+
const response = await fetch(`${GITHUB_API}/repos/${owner}/${repo}`, { headers });
|
|
137
|
+
if (!response.ok) return { status: response.status };
|
|
138
|
+
const data = await response.json();
|
|
139
|
+
return { branch: data.default_branch };
|
|
140
|
+
}
|
|
141
|
+
async function listSkillsFromGitHub(project) {
|
|
142
|
+
try {
|
|
143
|
+
const parts = project.split("/").filter(Boolean);
|
|
144
|
+
if (parts.length < 2) return { status: "error", error: "Invalid project format" };
|
|
145
|
+
const [owner, repo] = parts;
|
|
146
|
+
const headers = getGitHubHeaders();
|
|
147
|
+
const branchResult = await fetchDefaultBranch(owner, repo, headers);
|
|
148
|
+
if ("status" in branchResult) return { status: "repo_not_found" };
|
|
149
|
+
const treeData = await fetchRepoTree(owner, repo, branchResult.branch, headers);
|
|
150
|
+
if (!treeData) return { status: "error", error: "Could not fetch repository tree" };
|
|
151
|
+
const skillMdFiles = treeData.tree.filter(
|
|
152
|
+
(item) => item.type === "blob" && item.path.toLowerCase().endsWith("skill.md")
|
|
153
|
+
);
|
|
154
|
+
const skills = [];
|
|
155
|
+
for (const item of skillMdFiles) {
|
|
156
|
+
const rawUrl = `${GITHUB_RAW}/${owner}/${repo}/${branchResult.branch}/${item.path}`;
|
|
157
|
+
const response = await fetch(rawUrl, { headers });
|
|
158
|
+
if (!response.ok) continue;
|
|
159
|
+
const content = await response.text();
|
|
160
|
+
const meta = parseSkillFrontmatter(content);
|
|
161
|
+
if (!meta) continue;
|
|
162
|
+
const skillDir = item.path.split("/").slice(0, -1).join("/");
|
|
163
|
+
skills.push({
|
|
164
|
+
name: meta.name,
|
|
165
|
+
description: meta.description,
|
|
166
|
+
url: `https://github.com/${owner}/${repo}/tree/${branchResult.branch}/${skillDir}`,
|
|
167
|
+
project
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
return { status: "ok", skills };
|
|
171
|
+
} catch (err) {
|
|
172
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
173
|
+
return { status: "error", error: message };
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
async function getSkillFromGitHub(project, skillName) {
|
|
177
|
+
const result = await listSkillsFromGitHub(project);
|
|
178
|
+
if (result.status !== "ok") return result;
|
|
179
|
+
const skill = result.skills.find((s) => s.name === skillName.toLowerCase());
|
|
180
|
+
return { ...result, skill };
|
|
181
|
+
}
|
|
79
182
|
async function downloadSkillFromGitHub(skill) {
|
|
80
183
|
try {
|
|
81
184
|
const parsed = parseGitHubUrl(skill.url);
|
|
@@ -83,17 +186,11 @@ async function downloadSkillFromGitHub(skill) {
|
|
|
83
186
|
return { files: [], error: `Invalid GitHub URL: ${skill.url}` };
|
|
84
187
|
}
|
|
85
188
|
const { owner, repo, branch, path: skillPath } = parsed;
|
|
86
|
-
const
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
"User-Agent": "context7-cli"
|
|
91
|
-
}
|
|
92
|
-
});
|
|
93
|
-
if (!treeResponse.ok) {
|
|
94
|
-
return { files: [], error: `GitHub API error: ${treeResponse.status}` };
|
|
189
|
+
const ghHeaders = getGitHubHeaders();
|
|
190
|
+
const treeData = await fetchRepoTree(owner, repo, branch, ghHeaders);
|
|
191
|
+
if (!treeData) {
|
|
192
|
+
return { files: [], error: `GitHub API error` };
|
|
95
193
|
}
|
|
96
|
-
const treeData = await treeResponse.json();
|
|
97
194
|
const skillFiles = treeData.tree.filter(
|
|
98
195
|
(item) => item.type === "blob" && item.path.startsWith(skillPath + "/")
|
|
99
196
|
);
|
|
@@ -103,7 +200,7 @@ async function downloadSkillFromGitHub(skill) {
|
|
|
103
200
|
const files = [];
|
|
104
201
|
for (const item of skillFiles) {
|
|
105
202
|
const rawUrl = `${GITHUB_RAW}/${owner}/${repo}/${branch}/${item.path}`;
|
|
106
|
-
const fileResponse = await fetch(rawUrl);
|
|
203
|
+
const fileResponse = await fetch(rawUrl, { headers: ghHeaders });
|
|
107
204
|
if (!fileResponse.ok) {
|
|
108
205
|
console.warn(`Failed to fetch ${item.path}: ${fileResponse.status}`);
|
|
109
206
|
continue;
|
|
@@ -174,11 +271,19 @@ async function suggestSkills(dependencies, accessToken) {
|
|
|
174
271
|
async function downloadSkill(project, skillName) {
|
|
175
272
|
const skillData = await getSkill(project, skillName);
|
|
176
273
|
if (skillData.error) {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
274
|
+
const ghResult = await getSkillFromGitHub(project, skillName);
|
|
275
|
+
if (ghResult.status !== "ok" || !ghResult.skill) {
|
|
276
|
+
return {
|
|
277
|
+
skill: { name: skillName, description: "", url: "", project },
|
|
278
|
+
files: [],
|
|
279
|
+
error: skillData.message || skillData.error
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
const { files: files2, error: error2 } = await downloadSkillFromGitHub(ghResult.skill);
|
|
283
|
+
if (error2) {
|
|
284
|
+
return { skill: ghResult.skill, files: [], error: error2 };
|
|
285
|
+
}
|
|
286
|
+
return { skill: ghResult.skill, files: files2 };
|
|
182
287
|
}
|
|
183
288
|
const skill = {
|
|
184
289
|
name: skillData.name,
|
|
@@ -1833,23 +1938,45 @@ async function installCommand(input2, skillName, options) {
|
|
|
1833
1938
|
if (skillData.error === "prompt_injection_detected") {
|
|
1834
1939
|
spinner.fail(pc7.red(`Prompt injection detected in skill: ${skillName}`));
|
|
1835
1940
|
log.warn("This skill contains potentially malicious content and cannot be installed.");
|
|
1836
|
-
|
|
1941
|
+
return;
|
|
1942
|
+
}
|
|
1943
|
+
spinner.text = `Fetching skill from GitHub: ${skillName}...`;
|
|
1944
|
+
const ghResult = await getSkillFromGitHub(repo, skillName);
|
|
1945
|
+
if (ghResult.status === "repo_not_found") {
|
|
1946
|
+
spinner.fail(pc7.red(`Repository not found: ${repo}`));
|
|
1947
|
+
return;
|
|
1948
|
+
}
|
|
1949
|
+
if (ghResult.status !== "ok" || !ghResult.skill) {
|
|
1837
1950
|
spinner.fail(pc7.red(`Skill not found: ${skillName}`));
|
|
1951
|
+
return;
|
|
1838
1952
|
}
|
|
1839
|
-
|
|
1953
|
+
spinner.succeed(`Found skill: ${skillName}`);
|
|
1954
|
+
selectedSkills = [ghResult.skill];
|
|
1955
|
+
} else {
|
|
1956
|
+
spinner.succeed(`Found skill: ${skillName}`);
|
|
1957
|
+
selectedSkills = [
|
|
1958
|
+
{
|
|
1959
|
+
name: skillData.name,
|
|
1960
|
+
description: skillData.description,
|
|
1961
|
+
url: skillData.url,
|
|
1962
|
+
project: repo
|
|
1963
|
+
}
|
|
1964
|
+
];
|
|
1840
1965
|
}
|
|
1841
|
-
spinner.succeed(`Found skill: ${skillName}`);
|
|
1842
|
-
selectedSkills = [
|
|
1843
|
-
{
|
|
1844
|
-
name: skillData.name,
|
|
1845
|
-
description: skillData.description,
|
|
1846
|
-
url: skillData.url,
|
|
1847
|
-
project: repo
|
|
1848
|
-
}
|
|
1849
|
-
];
|
|
1850
1966
|
} else {
|
|
1851
|
-
|
|
1852
|
-
if (data.error) {
|
|
1967
|
+
let data = await listProjectSkills(repo);
|
|
1968
|
+
if ((data.error || !data.skills || data.skills.length === 0) && !data.blockedSkillsCount) {
|
|
1969
|
+
spinner.text = `Fetching skills from GitHub...`;
|
|
1970
|
+
const ghResult = await listSkillsFromGitHub(repo);
|
|
1971
|
+
if (ghResult.status === "repo_not_found") {
|
|
1972
|
+
spinner.fail(pc7.red(`Repository not found: ${repo}`));
|
|
1973
|
+
return;
|
|
1974
|
+
}
|
|
1975
|
+
if (ghResult.status === "ok" && ghResult.skills.length > 0) {
|
|
1976
|
+
data = { project: repo, skills: ghResult.skills };
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
if (data.error && (!data.skills || data.skills.length === 0)) {
|
|
1853
1980
|
spinner.fail(pc7.red(`Error: ${data.message || data.error}`));
|
|
1854
1981
|
return;
|
|
1855
1982
|
}
|
|
@@ -2433,8 +2560,8 @@ ${headerLine}`,
|
|
|
2433
2560
|
import pc8 from "picocolors";
|
|
2434
2561
|
import ora4 from "ora";
|
|
2435
2562
|
import { select as select3 } from "@inquirer/prompts";
|
|
2436
|
-
import { mkdir as
|
|
2437
|
-
import { dirname as
|
|
2563
|
+
import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
|
|
2564
|
+
import { dirname as dirname5, join as join9 } from "path";
|
|
2438
2565
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
2439
2566
|
|
|
2440
2567
|
// src/setup/agents.ts
|
|
@@ -2445,7 +2572,8 @@ var SETUP_AGENT_NAMES = {
|
|
|
2445
2572
|
claude: "Claude Code",
|
|
2446
2573
|
cursor: "Cursor",
|
|
2447
2574
|
opencode: "OpenCode",
|
|
2448
|
-
codex: "Codex"
|
|
2575
|
+
codex: "Codex",
|
|
2576
|
+
gemini: "Gemini CLI"
|
|
2449
2577
|
};
|
|
2450
2578
|
var AUTH_MODE_LABELS = {
|
|
2451
2579
|
oauth: "OAuth",
|
|
@@ -2564,6 +2692,29 @@ var agents = {
|
|
|
2564
2692
|
projectPaths: [".codex"],
|
|
2565
2693
|
globalPaths: [join8(homedir5(), ".codex")]
|
|
2566
2694
|
}
|
|
2695
|
+
},
|
|
2696
|
+
gemini: {
|
|
2697
|
+
name: "gemini",
|
|
2698
|
+
displayName: "Gemini CLI",
|
|
2699
|
+
mcp: {
|
|
2700
|
+
projectPaths: [join8(".gemini", "settings.json")],
|
|
2701
|
+
globalPaths: [join8(homedir5(), ".gemini", "settings.json")],
|
|
2702
|
+
configKey: "mcpServers",
|
|
2703
|
+
buildEntry: (auth) => withHeaders({ httpUrl: mcpUrl(auth) }, auth)
|
|
2704
|
+
},
|
|
2705
|
+
rule: {
|
|
2706
|
+
kind: "append",
|
|
2707
|
+
file: (scope) => scope === "global" ? join8(homedir5(), ".gemini", "GEMINI.md") : "GEMINI.md",
|
|
2708
|
+
sectionMarker: "<!-- context7 -->"
|
|
2709
|
+
},
|
|
2710
|
+
skill: {
|
|
2711
|
+
name: "context7-mcp",
|
|
2712
|
+
dir: (scope) => scope === "global" ? join8(homedir5(), ".gemini", "skills") : join8(".gemini", "skills")
|
|
2713
|
+
},
|
|
2714
|
+
detect: {
|
|
2715
|
+
projectPaths: [".gemini"],
|
|
2716
|
+
globalPaths: [join8(homedir5(), ".gemini")]
|
|
2717
|
+
}
|
|
2567
2718
|
}
|
|
2568
2719
|
};
|
|
2569
2720
|
function getAgent(name) {
|
|
@@ -2604,7 +2755,7 @@ Do not use for: refactoring, writing scripts from scratch, debugging business lo
|
|
|
2604
2755
|
|
|
2605
2756
|
## Steps
|
|
2606
2757
|
|
|
2607
|
-
1. \`resolve-library-id\` with the library name and the user's question
|
|
2758
|
+
1. \`resolve-library-id\` with the library name and the user's question. Use the official library name with proper punctuation (e.g., "Next.js" not "nextjs", "Customer.io" not "customerio", "Three.js" not "threejs")
|
|
2608
2759
|
2. Pick the best match by: exact name match, description relevance, code snippet count, source reputation (High/Medium preferred), and benchmark score (higher is better). Use version-specific IDs when the user mentions a version
|
|
2609
2760
|
3. \`query-docs\` with the selected library ID and the user's full question (not single words)
|
|
2610
2761
|
4. Answer using the fetched docs
|
|
@@ -2648,6 +2799,127 @@ async function getRuleContent(mode, agent) {
|
|
|
2648
2799
|
return agent === "cursor" ? `${CURSOR_FRONTMATTER}${body}` : body;
|
|
2649
2800
|
}
|
|
2650
2801
|
|
|
2802
|
+
// src/setup/mcp-writer.ts
|
|
2803
|
+
import { access as access3, readFile as readFile3, writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
|
|
2804
|
+
import { dirname as dirname4 } from "path";
|
|
2805
|
+
function stripJsonComments(text) {
|
|
2806
|
+
let result = "";
|
|
2807
|
+
let i = 0;
|
|
2808
|
+
while (i < text.length) {
|
|
2809
|
+
if (text[i] === '"') {
|
|
2810
|
+
const start = i++;
|
|
2811
|
+
while (i < text.length && text[i] !== '"') {
|
|
2812
|
+
if (text[i] === "\\") i++;
|
|
2813
|
+
i++;
|
|
2814
|
+
}
|
|
2815
|
+
result += text.slice(start, ++i);
|
|
2816
|
+
} else if (text[i] === "/" && text[i + 1] === "/") {
|
|
2817
|
+
i += 2;
|
|
2818
|
+
while (i < text.length && text[i] !== "\n") i++;
|
|
2819
|
+
} else if (text[i] === "/" && text[i + 1] === "*") {
|
|
2820
|
+
i += 2;
|
|
2821
|
+
while (i < text.length && !(text[i] === "*" && text[i + 1] === "/")) i++;
|
|
2822
|
+
i += 2;
|
|
2823
|
+
} else {
|
|
2824
|
+
result += text[i++];
|
|
2825
|
+
}
|
|
2826
|
+
}
|
|
2827
|
+
return result;
|
|
2828
|
+
}
|
|
2829
|
+
async function readJsonConfig(filePath) {
|
|
2830
|
+
let raw;
|
|
2831
|
+
try {
|
|
2832
|
+
raw = await readFile3(filePath, "utf-8");
|
|
2833
|
+
} catch {
|
|
2834
|
+
return {};
|
|
2835
|
+
}
|
|
2836
|
+
raw = raw.trim();
|
|
2837
|
+
if (!raw) return {};
|
|
2838
|
+
return JSON.parse(stripJsonComments(raw));
|
|
2839
|
+
}
|
|
2840
|
+
function mergeServerEntry(existing, configKey, serverName, entry) {
|
|
2841
|
+
const section = existing[configKey] ?? {};
|
|
2842
|
+
const alreadyExists = serverName in section;
|
|
2843
|
+
return {
|
|
2844
|
+
config: {
|
|
2845
|
+
...existing,
|
|
2846
|
+
[configKey]: {
|
|
2847
|
+
...section,
|
|
2848
|
+
[serverName]: entry
|
|
2849
|
+
}
|
|
2850
|
+
},
|
|
2851
|
+
alreadyExists
|
|
2852
|
+
};
|
|
2853
|
+
}
|
|
2854
|
+
async function resolveMcpPath(candidates) {
|
|
2855
|
+
for (const candidate of candidates) {
|
|
2856
|
+
try {
|
|
2857
|
+
await access3(candidate);
|
|
2858
|
+
return candidate;
|
|
2859
|
+
} catch {
|
|
2860
|
+
}
|
|
2861
|
+
}
|
|
2862
|
+
return candidates[0];
|
|
2863
|
+
}
|
|
2864
|
+
async function writeJsonConfig(filePath, config) {
|
|
2865
|
+
await mkdir3(dirname4(filePath), { recursive: true });
|
|
2866
|
+
await writeFile3(filePath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
2867
|
+
}
|
|
2868
|
+
function buildTomlServerBlock(serverName, entry) {
|
|
2869
|
+
const lines = [`[mcp_servers.${serverName}]`];
|
|
2870
|
+
const headers = entry.headers;
|
|
2871
|
+
for (const [key, value] of Object.entries(entry)) {
|
|
2872
|
+
if (key === "headers") continue;
|
|
2873
|
+
lines.push(`${key} = ${JSON.stringify(value)}`);
|
|
2874
|
+
}
|
|
2875
|
+
if (headers && Object.keys(headers).length > 0) {
|
|
2876
|
+
lines.push("");
|
|
2877
|
+
lines.push(`[mcp_servers.${serverName}.http_headers]`);
|
|
2878
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
2879
|
+
lines.push(`${key} = ${JSON.stringify(value)}`);
|
|
2880
|
+
}
|
|
2881
|
+
}
|
|
2882
|
+
return lines.join("\n") + "\n";
|
|
2883
|
+
}
|
|
2884
|
+
async function appendTomlServer(filePath, serverName, entry) {
|
|
2885
|
+
const block = buildTomlServerBlock(serverName, entry);
|
|
2886
|
+
let existing = "";
|
|
2887
|
+
try {
|
|
2888
|
+
existing = await readFile3(filePath, "utf-8");
|
|
2889
|
+
} catch {
|
|
2890
|
+
}
|
|
2891
|
+
const sectionHeader = `[mcp_servers.${serverName}]`;
|
|
2892
|
+
const alreadyExists = existing.includes(sectionHeader);
|
|
2893
|
+
if (alreadyExists) {
|
|
2894
|
+
const subPrefix = `[mcp_servers.${serverName}.`;
|
|
2895
|
+
const startIdx = existing.indexOf(sectionHeader);
|
|
2896
|
+
const rest = existing.slice(startIdx + sectionHeader.length);
|
|
2897
|
+
let endOffset = rest.length;
|
|
2898
|
+
const re = /^\[/gm;
|
|
2899
|
+
let m;
|
|
2900
|
+
while ((m = re.exec(rest)) !== null) {
|
|
2901
|
+
const lineEnd = rest.indexOf("\n", m.index);
|
|
2902
|
+
const line = rest.slice(m.index, lineEnd === -1 ? void 0 : lineEnd);
|
|
2903
|
+
if (!line.startsWith(subPrefix)) {
|
|
2904
|
+
endOffset = m.index;
|
|
2905
|
+
break;
|
|
2906
|
+
}
|
|
2907
|
+
}
|
|
2908
|
+
const rawBefore = existing.slice(0, startIdx).replace(/\n+$/, "");
|
|
2909
|
+
const rawAfter = existing.slice(startIdx + sectionHeader.length + endOffset).replace(/^\n+/, "");
|
|
2910
|
+
const before = rawBefore.length > 0 ? rawBefore + "\n\n" : "";
|
|
2911
|
+
const after = rawAfter.length > 0 ? "\n" + rawAfter : "";
|
|
2912
|
+
const content = before + block + after;
|
|
2913
|
+
await mkdir3(dirname4(filePath), { recursive: true });
|
|
2914
|
+
await writeFile3(filePath, content, "utf-8");
|
|
2915
|
+
} else {
|
|
2916
|
+
const separator = existing.length > 0 && !existing.endsWith("\n") ? "\n\n" : existing.length > 0 ? "\n" : "";
|
|
2917
|
+
await mkdir3(dirname4(filePath), { recursive: true });
|
|
2918
|
+
await writeFile3(filePath, existing + separator + block, "utf-8");
|
|
2919
|
+
}
|
|
2920
|
+
return { alreadyExists };
|
|
2921
|
+
}
|
|
2922
|
+
|
|
2651
2923
|
// src/commands/setup.ts
|
|
2652
2924
|
var CHECKBOX_THEME = {
|
|
2653
2925
|
style: {
|
|
@@ -2661,10 +2933,11 @@ function getSelectedAgents(options) {
|
|
|
2661
2933
|
if (options.cursor) agents2.push("cursor");
|
|
2662
2934
|
if (options.opencode) agents2.push("opencode");
|
|
2663
2935
|
if (options.codex) agents2.push("codex");
|
|
2936
|
+
if (options.gemini) agents2.push("gemini");
|
|
2664
2937
|
return agents2;
|
|
2665
2938
|
}
|
|
2666
2939
|
function registerSetupCommand(program2) {
|
|
2667
|
-
program2.command("setup").description("Set up Context7 for your AI coding agent").option("--claude", "Set up for Claude Code").option("--cursor", "Set up for Cursor").option("--universal", "Set up for Universal (.agents/skills)").option("--antigravity", "Set up for Antigravity (.agent/skills)").option("--opencode", "Set up for OpenCode").option("--codex", "Set up for Codex").option("--mcp", "Set up MCP server mode").option("--cli", "Set up CLI + Skills mode (no MCP server)").option("-p, --project", "Configure for current project instead of globally").option("-y, --yes", "Skip confirmation prompts").option("--api-key <key>", "Use API key authentication").option("--oauth", "Use OAuth endpoint (IDE handles auth flow)").action(async (options) => {
|
|
2940
|
+
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("--gemini", "Set up for Gemini CLI").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) => {
|
|
2668
2941
|
await setupCommand(options);
|
|
2669
2942
|
});
|
|
2670
2943
|
}
|
|
@@ -2743,37 +3016,11 @@ async function resolveCliAuth(apiKey) {
|
|
|
2743
3016
|
}
|
|
2744
3017
|
await performLogin();
|
|
2745
3018
|
}
|
|
2746
|
-
async function
|
|
2747
|
-
const
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
if (mcpPath.endsWith(".toml")) {
|
|
2752
|
-
const { readTomlServerExists } = await import("./mcp-writer-IYBCUACD.js");
|
|
2753
|
-
return readTomlServerExists(mcpPath, "context7");
|
|
2754
|
-
}
|
|
2755
|
-
const existing = await readJsonConfig(mcpPath);
|
|
2756
|
-
const section = existing[agent.mcp.configKey] ?? {};
|
|
2757
|
-
return "context7" in section;
|
|
2758
|
-
} catch {
|
|
2759
|
-
return false;
|
|
2760
|
-
}
|
|
2761
|
-
}
|
|
2762
|
-
async function promptAgents(scope, mode) {
|
|
2763
|
-
const choices = await Promise.all(
|
|
2764
|
-
ALL_AGENT_NAMES.map(async (name) => {
|
|
2765
|
-
const configured = mode === "mcp" ? await isAlreadyConfigured(name, scope) : false;
|
|
2766
|
-
return {
|
|
2767
|
-
name: SETUP_AGENT_NAMES[name],
|
|
2768
|
-
value: name,
|
|
2769
|
-
disabled: configured ? "(already configured)" : false
|
|
2770
|
-
};
|
|
2771
|
-
})
|
|
2772
|
-
);
|
|
2773
|
-
if (choices.every((c) => c.disabled)) {
|
|
2774
|
-
log.info("Context7 is already configured for all detected agents.");
|
|
2775
|
-
return null;
|
|
2776
|
-
}
|
|
3019
|
+
async function promptAgents() {
|
|
3020
|
+
const choices = ALL_AGENT_NAMES.map((name) => ({
|
|
3021
|
+
name: SETUP_AGENT_NAMES[name],
|
|
3022
|
+
value: name
|
|
3023
|
+
}));
|
|
2777
3024
|
const message = "Which agents do you want to set up?";
|
|
2778
3025
|
try {
|
|
2779
3026
|
return await checkboxWithHover(
|
|
@@ -2789,13 +3036,13 @@ async function promptAgents(scope, mode) {
|
|
|
2789
3036
|
return null;
|
|
2790
3037
|
}
|
|
2791
3038
|
}
|
|
2792
|
-
async function resolveAgents(options, scope
|
|
3039
|
+
async function resolveAgents(options, scope) {
|
|
2793
3040
|
const explicit = getSelectedAgents(options);
|
|
2794
3041
|
if (explicit.length > 0) return explicit;
|
|
2795
3042
|
const detected = await detectAgents(scope);
|
|
2796
3043
|
if (detected.length > 0 && options.yes) return detected;
|
|
2797
3044
|
log.blank();
|
|
2798
|
-
const selected = await promptAgents(
|
|
3045
|
+
const selected = await promptAgents();
|
|
2799
3046
|
if (!selected) {
|
|
2800
3047
|
log.warn("Setup cancelled");
|
|
2801
3048
|
return [];
|
|
@@ -2809,8 +3056,8 @@ async function installRule(agentName, mode, scope) {
|
|
|
2809
3056
|
if (rule.kind === "file") {
|
|
2810
3057
|
const ruleDir = scope === "global" ? rule.dir("global") : join9(process.cwd(), rule.dir("project"));
|
|
2811
3058
|
const rulePath = join9(ruleDir, rule.filename);
|
|
2812
|
-
await
|
|
2813
|
-
await
|
|
3059
|
+
await mkdir4(dirname5(rulePath), { recursive: true });
|
|
3060
|
+
await writeFile4(rulePath, content, "utf-8");
|
|
2814
3061
|
return { status: "installed", path: rulePath };
|
|
2815
3062
|
}
|
|
2816
3063
|
const filePath = scope === "global" ? rule.file("global") : join9(process.cwd(), rule.file("project"));
|
|
@@ -2819,18 +3066,18 @@ async function installRule(agentName, mode, scope) {
|
|
|
2819
3066
|
${content}${rule.sectionMarker}`;
|
|
2820
3067
|
let existing = "";
|
|
2821
3068
|
try {
|
|
2822
|
-
existing = await
|
|
3069
|
+
existing = await readFile4(filePath, "utf-8");
|
|
2823
3070
|
} catch {
|
|
2824
3071
|
}
|
|
2825
3072
|
if (existing.includes(rule.sectionMarker)) {
|
|
2826
3073
|
const regex = new RegExp(`${escapedMarker}\\n[\\s\\S]*?${escapedMarker}`);
|
|
2827
3074
|
const updated = existing.replace(regex, section);
|
|
2828
|
-
await
|
|
3075
|
+
await writeFile4(filePath, updated, "utf-8");
|
|
2829
3076
|
return { status: "updated", path: filePath };
|
|
2830
3077
|
}
|
|
2831
3078
|
const separator = existing.length > 0 && !existing.endsWith("\n") ? "\n\n" : existing.length > 0 ? "\n" : "";
|
|
2832
|
-
await
|
|
2833
|
-
await
|
|
3079
|
+
await mkdir4(dirname5(filePath), { recursive: true });
|
|
3080
|
+
await writeFile4(filePath, existing + separator + section + "\n", "utf-8");
|
|
2834
3081
|
return { status: "installed", path: filePath };
|
|
2835
3082
|
}
|
|
2836
3083
|
async function setupAgent(agentName, auth, scope) {
|
|
@@ -2845,7 +3092,7 @@ async function setupAgent(agentName, auth, scope) {
|
|
|
2845
3092
|
"context7",
|
|
2846
3093
|
agent.mcp.buildEntry(auth)
|
|
2847
3094
|
);
|
|
2848
|
-
mcpStatus = alreadyExists ?
|
|
3095
|
+
mcpStatus = alreadyExists ? `reconfigured with ${AUTH_MODE_LABELS[auth.mode]}` : `configured with ${AUTH_MODE_LABELS[auth.mode]}`;
|
|
2849
3096
|
} else {
|
|
2850
3097
|
const existing = await readJsonConfig(mcpPath);
|
|
2851
3098
|
const { config, alreadyExists } = mergeServerEntry(
|
|
@@ -2854,14 +3101,8 @@ async function setupAgent(agentName, auth, scope) {
|
|
|
2854
3101
|
"context7",
|
|
2855
3102
|
agent.mcp.buildEntry(auth)
|
|
2856
3103
|
);
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
} else {
|
|
2860
|
-
mcpStatus = `configured with ${AUTH_MODE_LABELS[auth.mode]}`;
|
|
2861
|
-
}
|
|
2862
|
-
if (config !== existing) {
|
|
2863
|
-
await writeJsonConfig(mcpPath, config);
|
|
2864
|
-
}
|
|
3104
|
+
mcpStatus = alreadyExists ? `reconfigured with ${AUTH_MODE_LABELS[auth.mode]}` : `configured with ${AUTH_MODE_LABELS[auth.mode]}`;
|
|
3105
|
+
await writeJsonConfig(mcpPath, config);
|
|
2865
3106
|
}
|
|
2866
3107
|
} catch (err) {
|
|
2867
3108
|
mcpStatus = `failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
@@ -2916,7 +3157,7 @@ async function setupMcp(agents2, options, scope) {
|
|
|
2916
3157
|
log.blank();
|
|
2917
3158
|
for (const r of results) {
|
|
2918
3159
|
log.plain(` ${pc8.bold(r.agent)}`);
|
|
2919
|
-
const mcpIcon = r.mcpStatus.startsWith("configured") ? pc8.green("+") : pc8.dim("~");
|
|
3160
|
+
const mcpIcon = r.mcpStatus.startsWith("configured") || r.mcpStatus.startsWith("reconfigured") ? pc8.green("+") : pc8.dim("~");
|
|
2920
3161
|
log.plain(` ${mcpIcon} MCP server ${r.mcpStatus}`);
|
|
2921
3162
|
log.plain(` ${pc8.dim(r.mcpPath)}`);
|
|
2922
3163
|
const ruleIcon = r.ruleStatus === "installed" ? pc8.green("+") : pc8.dim("~");
|
|
@@ -2925,6 +3166,11 @@ async function setupMcp(agents2, options, scope) {
|
|
|
2925
3166
|
const skillIcon = r.skillStatus === "installed" ? pc8.green("+") : pc8.dim("~");
|
|
2926
3167
|
log.plain(` ${skillIcon} Skill ${r.skillStatus}`);
|
|
2927
3168
|
log.plain(` ${pc8.dim(r.skillPath)}`);
|
|
3169
|
+
if (r.skillStatus.includes("EACCES")) {
|
|
3170
|
+
log.plain(
|
|
3171
|
+
` ${pc8.yellow("tip:")} fix permissions with: ${pc8.cyan(`sudo chown -R $(whoami) ${dirname5(dirname5(r.skillPath))}`)}`
|
|
3172
|
+
);
|
|
3173
|
+
}
|
|
2928
3174
|
}
|
|
2929
3175
|
log.blank();
|
|
2930
3176
|
trackEvent("setup", { agents: agents2, scope, authMode: auth.mode });
|
|
@@ -2956,7 +3202,7 @@ async function setupCliAgent(agentName, scope, downloadData) {
|
|
|
2956
3202
|
async function setupCli(options) {
|
|
2957
3203
|
await resolveCliAuth(options.apiKey);
|
|
2958
3204
|
const scope = options.project ? "project" : "global";
|
|
2959
|
-
const agents2 = await resolveAgents(options, scope
|
|
3205
|
+
const agents2 = await resolveAgents(options, scope);
|
|
2960
3206
|
if (agents2.length === 0) return;
|
|
2961
3207
|
log.blank();
|
|
2962
3208
|
const spinner = ora4("Downloading find-docs skill...").start();
|
|
@@ -2980,6 +3226,11 @@ async function setupCli(options) {
|
|
|
2980
3226
|
const skillIcon = r.skillStatus === "installed" ? pc8.green("+") : pc8.dim("~");
|
|
2981
3227
|
log.plain(` ${skillIcon} Skill ${r.skillStatus}`);
|
|
2982
3228
|
log.plain(` ${pc8.dim(r.skillPath)}`);
|
|
3229
|
+
if (r.skillStatus.includes("EACCES")) {
|
|
3230
|
+
log.plain(
|
|
3231
|
+
` ${pc8.yellow("tip:")} fix permissions with: ${pc8.cyan(`sudo chown -R $(whoami) ${dirname5(dirname5(r.skillPath))}`)}`
|
|
3232
|
+
);
|
|
3233
|
+
}
|
|
2983
3234
|
const ruleIcon = r.ruleStatus === "installed" || r.ruleStatus === "updated" ? pc8.green("+") : pc8.dim("~");
|
|
2984
3235
|
log.plain(` ${ruleIcon} Rule ${r.ruleStatus}`);
|
|
2985
3236
|
log.plain(` ${pc8.dim(r.rulePath)}`);
|
|
@@ -2994,7 +3245,7 @@ async function setupCommand(options) {
|
|
|
2994
3245
|
const mode = await resolveMode(options);
|
|
2995
3246
|
if (mode === "mcp") {
|
|
2996
3247
|
const scope = options.project ? "project" : "global";
|
|
2997
|
-
const agents2 = await resolveAgents(options, scope
|
|
3248
|
+
const agents2 = await resolveAgents(options, scope);
|
|
2998
3249
|
if (agents2.length === 0) return;
|
|
2999
3250
|
await setupMcp(agents2, options, scope);
|
|
3000
3251
|
} else {
|