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 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 treeUrl = `${GITHUB_API}/repos/${owner}/${repo}/git/trees/${branch}?recursive=1`;
87
- const treeResponse = await fetch(treeUrl, {
88
- headers: {
89
- Accept: "application/vnd.github.v3+json",
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
- return {
178
- skill: { name: skillName, description: "", url: "", project },
179
- files: [],
180
- error: skillData.message || skillData.error
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
- } else {
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
- return;
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
- const data = await listProjectSkills(repo);
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 mkdir3, readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
2437
- import { dirname as dirname4, join as join9 } from "path";
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 isAlreadyConfigured(agentName, scope) {
2747
- const agent = getAgent(agentName);
2748
- const mcpCandidates = scope === "global" ? agent.mcp.globalPaths : agent.mcp.projectPaths.map((p) => join9(process.cwd(), p));
2749
- const mcpPath = await resolveMcpPath(mcpCandidates);
2750
- try {
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, mode = "mcp") {
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(scope, mode);
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 mkdir3(dirname4(rulePath), { recursive: true });
2813
- await writeFile3(rulePath, content, "utf-8");
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 readFile3(filePath, "utf-8");
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 writeFile3(filePath, updated, "utf-8");
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 mkdir3(dirname4(filePath), { recursive: true });
2833
- await writeFile3(filePath, existing + separator + section + "\n", "utf-8");
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 ? "already configured" : `configured with ${AUTH_MODE_LABELS[auth.mode]}`;
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
- if (alreadyExists) {
2858
- mcpStatus = "already configured";
2859
- } else {
2860
- mcpStatus = `configured with ${AUTH_MODE_LABELS[auth.mode]}`;
2861
- }
2862
- if (config !== existing) {
2863
- await writeJsonConfig(mcpPath, config);
2864
- }
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, "cli");
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, mode);
3248
+ const agents2 = await resolveAgents(options, scope);
2998
3249
  if (agents2.length === 0) return;
2999
3250
  await setupMcp(agents2, options, scope);
3000
3251
  } else {