ctx7 0.2.1 → 0.2.3

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
@@ -6,10 +6,10 @@ import pc8 from "picocolors";
6
6
  import figlet from "figlet";
7
7
 
8
8
  // src/commands/skill.ts
9
- import pc6 from "picocolors";
10
- import ora2 from "ora";
9
+ import pc7 from "picocolors";
10
+ import ora3 from "ora";
11
11
  import { readdir, rm as rm2 } from "fs/promises";
12
- import { join as join5 } from "path";
12
+ import { join as join6 } from "path";
13
13
 
14
14
  // src/utils/parse-input.ts
15
15
  function parseSkillInput(input2) {
@@ -138,6 +138,18 @@ async function searchSkills(query) {
138
138
  const response = await fetch(`${baseUrl}/api/v2/skills?${params}`);
139
139
  return await response.json();
140
140
  }
141
+ async function suggestSkills(dependencies, accessToken) {
142
+ const headers = { "Content-Type": "application/json" };
143
+ if (accessToken) {
144
+ headers["Authorization"] = `Bearer ${accessToken}`;
145
+ }
146
+ const response = await fetch(`${baseUrl}/api/v2/skills/suggest`, {
147
+ method: "POST",
148
+ headers,
149
+ body: JSON.stringify({ dependencies })
150
+ });
151
+ return await response.json();
152
+ }
141
153
  async function downloadSkill(project, skillName) {
142
154
  const skillData = await getSkill(project, skillName);
143
155
  if (skillData.error) {
@@ -521,12 +533,17 @@ import { checkbox as checkbox2 } from "@inquirer/prompts";
521
533
  import readline from "readline";
522
534
  function terminalLink(text, url, color) {
523
535
  const colorFn = color ?? ((s) => s);
524
- return `\x1B]8;;${url}\x07${colorFn(text)}\x1B]8;;\x07`;
536
+ return `\x1B]8;;${url}\x07${colorFn(text)}\x1B]8;;\x07 ${pc3.white("\u2197")}`;
525
537
  }
526
- function formatInstallCount(count) {
527
- if (count === void 0 || count === 0) return "";
538
+ function formatInstallCount(count, placeholder = "") {
539
+ if (count === void 0 || count === 0) return placeholder;
528
540
  return pc3.yellow(String(count));
529
541
  }
542
+ function formatTrustScore(score) {
543
+ if (score === void 0 || score < 0) return pc3.dim("-");
544
+ if (score < 3) return pc3.red(score.toFixed(1));
545
+ return pc3.yellow(score.toFixed(1));
546
+ }
530
547
  async function checkboxWithHover(config, options) {
531
548
  const choices = config.choices.filter(
532
549
  (c) => typeof c === "object" && c !== null && !("type" in c && c.type === "separator")
@@ -608,11 +625,12 @@ function trackEvent(event, data) {
608
625
  }
609
626
 
610
627
  // src/commands/generate.ts
611
- import pc5 from "picocolors";
612
- import ora from "ora";
613
- import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
628
+ import pc6 from "picocolors";
629
+ import ora2 from "ora";
630
+ import { mkdir as mkdir2, writeFile as writeFile2, readFile, unlink } from "fs/promises";
614
631
  import { join as join4 } from "path";
615
632
  import { homedir as homedir3 } from "os";
633
+ import { spawn } from "child_process";
616
634
  import { input, select as select2 } from "@inquirer/prompts";
617
635
 
618
636
  // src/utils/auth.ts
@@ -807,6 +825,148 @@ function buildAuthorizationUrl(baseUrl3, clientId, redirectUri, codeChallenge, s
807
825
  return url.toString();
808
826
  }
809
827
 
828
+ // src/commands/auth.ts
829
+ import pc4 from "picocolors";
830
+ import ora from "ora";
831
+ import open from "open";
832
+ var CLI_CLIENT_ID = "2veBSofhicRBguUT";
833
+ var baseUrl2 = "https://context7.com";
834
+ function setAuthBaseUrl(url) {
835
+ baseUrl2 = url;
836
+ }
837
+ function registerAuthCommands(program2) {
838
+ program2.command("login").description("Log in to Context7").option("--no-browser", "Don't open browser automatically").action(async (options) => {
839
+ await loginCommand(options);
840
+ });
841
+ program2.command("logout").description("Log out of Context7").action(() => {
842
+ logoutCommand();
843
+ });
844
+ program2.command("whoami").description("Show current login status").action(async () => {
845
+ await whoamiCommand();
846
+ });
847
+ }
848
+ async function performLogin(openBrowser = true) {
849
+ const spinner = ora("Preparing login...").start();
850
+ try {
851
+ const { codeVerifier, codeChallenge } = generatePKCE();
852
+ const state = generateState();
853
+ const callbackServer = createCallbackServer(state);
854
+ const port = await callbackServer.port;
855
+ const redirectUri = `http://localhost:${port}/callback`;
856
+ const authUrl = buildAuthorizationUrl(
857
+ baseUrl2,
858
+ CLI_CLIENT_ID,
859
+ redirectUri,
860
+ codeChallenge,
861
+ state
862
+ );
863
+ spinner.stop();
864
+ console.log("");
865
+ console.log(pc4.bold("Opening browser to log in..."));
866
+ console.log("");
867
+ if (openBrowser) {
868
+ await open(authUrl);
869
+ console.log(pc4.dim("If the browser didn't open, visit this URL:"));
870
+ } else {
871
+ console.log(pc4.dim("Open this URL in your browser:"));
872
+ }
873
+ console.log(pc4.cyan(authUrl));
874
+ console.log("");
875
+ const waitingSpinner = ora("Waiting for login...").start();
876
+ try {
877
+ const { code } = await callbackServer.result;
878
+ waitingSpinner.text = "Exchanging code for tokens...";
879
+ const tokens = await exchangeCodeForTokens(
880
+ baseUrl2,
881
+ code,
882
+ codeVerifier,
883
+ redirectUri,
884
+ CLI_CLIENT_ID
885
+ );
886
+ saveTokens(tokens);
887
+ callbackServer.close();
888
+ waitingSpinner.succeed(pc4.green("Login successful!"));
889
+ return tokens.access_token;
890
+ } catch (error) {
891
+ callbackServer.close();
892
+ waitingSpinner.fail(pc4.red("Login failed"));
893
+ if (error instanceof Error) {
894
+ console.error(pc4.red(error.message));
895
+ }
896
+ return null;
897
+ }
898
+ } catch (error) {
899
+ spinner.fail(pc4.red("Login failed"));
900
+ if (error instanceof Error) {
901
+ console.error(pc4.red(error.message));
902
+ }
903
+ return null;
904
+ }
905
+ }
906
+ async function loginCommand(options) {
907
+ trackEvent("command", { name: "login" });
908
+ const existingTokens = loadTokens();
909
+ if (existingTokens) {
910
+ const expired = isTokenExpired(existingTokens);
911
+ if (!expired || existingTokens.refresh_token) {
912
+ console.log(pc4.yellow("You are already logged in."));
913
+ console.log(
914
+ pc4.dim("Run 'ctx7 logout' first if you want to log in with a different account.")
915
+ );
916
+ return;
917
+ }
918
+ clearTokens();
919
+ }
920
+ const token = await performLogin(options.browser);
921
+ if (!token) {
922
+ process.exit(1);
923
+ }
924
+ console.log("");
925
+ console.log(pc4.dim("You can now use authenticated Context7 features."));
926
+ }
927
+ function logoutCommand() {
928
+ trackEvent("command", { name: "logout" });
929
+ if (clearTokens()) {
930
+ console.log(pc4.green("Logged out successfully."));
931
+ } else {
932
+ console.log(pc4.yellow("You are not logged in."));
933
+ }
934
+ }
935
+ async function whoamiCommand() {
936
+ trackEvent("command", { name: "whoami" });
937
+ const tokens = loadTokens();
938
+ if (!tokens) {
939
+ console.log(pc4.yellow("Not logged in."));
940
+ console.log(pc4.dim("Run 'ctx7 login' to authenticate."));
941
+ return;
942
+ }
943
+ console.log(pc4.green("Logged in"));
944
+ try {
945
+ const userInfo = await fetchUserInfo(tokens.access_token);
946
+ if (userInfo.name) {
947
+ console.log(`${pc4.dim("Name:".padEnd(9))}${userInfo.name}`);
948
+ }
949
+ if (userInfo.email) {
950
+ console.log(`${pc4.dim("Email:".padEnd(9))}${userInfo.email}`);
951
+ }
952
+ } catch {
953
+ if (isTokenExpired(tokens) && !tokens.refresh_token) {
954
+ console.log(pc4.dim("(Session may be expired - run 'ctx7 login' to refresh)"));
955
+ }
956
+ }
957
+ }
958
+ async function fetchUserInfo(accessToken) {
959
+ const response = await fetch("https://clerk.context7.com/oauth/userinfo", {
960
+ headers: {
961
+ Authorization: `Bearer ${accessToken}`
962
+ }
963
+ });
964
+ if (!response.ok) {
965
+ throw new Error("Failed to fetch user info");
966
+ }
967
+ return await response.json();
968
+ }
969
+
810
970
  // src/utils/selectOrInput.ts
811
971
  import {
812
972
  createPrompt,
@@ -817,10 +977,19 @@ import {
817
977
  isUpKey,
818
978
  isDownKey
819
979
  } from "@inquirer/core";
820
- import pc4 from "picocolors";
980
+ import pc5 from "picocolors";
981
+ function reorderOptions(options, recommendedIndex) {
982
+ if (recommendedIndex === 0) return options;
983
+ const reordered = [options[recommendedIndex]];
984
+ for (let i = 0; i < options.length; i++) {
985
+ if (i !== recommendedIndex) reordered.push(options[i]);
986
+ }
987
+ return reordered;
988
+ }
821
989
  var selectOrInput = createPrompt((config, done) => {
822
- const { message, options, recommendedIndex = 0 } = config;
823
- const [cursor, setCursor] = useState(recommendedIndex);
990
+ const { message, options: rawOptions, recommendedIndex = 0 } = config;
991
+ const options = reorderOptions(rawOptions, recommendedIndex);
992
+ const [cursor, setCursor] = useState(0);
824
993
  const [inputValue, setInputValue] = useState("");
825
994
  const prefix = usePrefix({});
826
995
  useKeypress((key, rl) => {
@@ -835,7 +1004,7 @@ var selectOrInput = createPrompt((config, done) => {
835
1004
  if (isEnterKey(key)) {
836
1005
  if (cursor === options.length) {
837
1006
  const finalValue = inputValue.trim();
838
- done(finalValue || options[recommendedIndex]);
1007
+ done(finalValue || options[0]);
839
1008
  } else {
840
1009
  done(options[cursor]);
841
1010
  }
@@ -865,16 +1034,16 @@ var selectOrInput = createPrompt((config, done) => {
865
1034
  rl.line = "";
866
1035
  }
867
1036
  });
868
- let output = `${prefix} ${pc4.bold(message)}
1037
+ let output = `${prefix} ${pc5.bold(message)}
869
1038
 
870
1039
  `;
871
1040
  options.forEach((opt, idx) => {
872
- const isRecommended = idx === recommendedIndex;
1041
+ const isRecommended = idx === 0;
873
1042
  const isCursor = idx === cursor;
874
- const number = pc4.cyan(`${idx + 1}.`);
875
- const text = isRecommended ? `${opt} ${pc4.green("\u2713 Recommended")}` : opt;
1043
+ const number = pc5.cyan(`${idx + 1}.`);
1044
+ const text = isRecommended ? `${opt} ${pc5.green("\u2713 Recommended")}` : opt;
876
1045
  if (isCursor) {
877
- output += pc4.cyan(`\u276F ${number} ${text}
1046
+ output += pc5.cyan(`\u276F ${number} ${text}
878
1047
  `);
879
1048
  } else {
880
1049
  output += ` ${number} ${text}
@@ -883,9 +1052,9 @@ var selectOrInput = createPrompt((config, done) => {
883
1052
  });
884
1053
  const isCustomCursor = cursor === options.length;
885
1054
  if (isCustomCursor) {
886
- output += pc4.cyan(`\u276F ${pc4.yellow("\u270E")} ${inputValue || pc4.dim("Type your own...")}`);
1055
+ output += pc5.cyan(`\u276F ${pc5.yellow("\u270E")} ${inputValue || pc5.dim("Type your own...")}`);
887
1056
  } else {
888
- output += ` ${pc4.yellow("\u270E")} ${pc4.dim("Type your own...")}`;
1057
+ output += ` ${pc5.yellow("\u270E")} ${pc5.dim("Type your own...")}`;
889
1058
  }
890
1059
  return output;
891
1060
  });
@@ -900,52 +1069,64 @@ function registerGenerateCommand(skillCommand) {
900
1069
  async function generateCommand(options) {
901
1070
  trackEvent("command", { name: "generate" });
902
1071
  log.blank();
1072
+ let accessToken = null;
903
1073
  const tokens = loadTokens();
904
- if (!tokens) {
905
- log.error("Authentication required. Please run 'ctx7 login' first.");
906
- return;
907
- }
908
- if (isTokenExpired(tokens)) {
909
- log.error("Session expired. Please run 'ctx7 login' to refresh.");
910
- return;
1074
+ if (tokens && !isTokenExpired(tokens)) {
1075
+ accessToken = tokens.access_token;
1076
+ } else {
1077
+ log.info("Authentication required. Logging in...");
1078
+ log.blank();
1079
+ accessToken = await performLogin();
1080
+ if (!accessToken) {
1081
+ log.error("Login failed. Please try again.");
1082
+ return;
1083
+ }
1084
+ log.blank();
911
1085
  }
912
- const accessToken = tokens.access_token;
913
- const initSpinner = ora().start();
1086
+ const initSpinner = ora2().start();
914
1087
  const quota = await getSkillQuota(accessToken);
915
1088
  if (quota.error) {
916
- initSpinner.fail(pc5.red("Failed to initialize"));
1089
+ initSpinner.fail(pc6.red("Failed to initialize"));
917
1090
  return;
918
1091
  }
919
1092
  if (quota.tier !== "unlimited" && quota.remaining < 1) {
920
- initSpinner.fail(pc5.red("Weekly skill generation limit reached"));
1093
+ initSpinner.fail(pc6.red("Weekly skill generation limit reached"));
921
1094
  log.blank();
922
1095
  console.log(
923
- ` You've used ${pc5.bold(pc5.white(quota.used.toString()))}/${pc5.bold(pc5.white(quota.limit.toString()))} skill generations this week.`
1096
+ ` You've used ${pc6.bold(pc6.white(quota.used.toString()))}/${pc6.bold(pc6.white(quota.limit.toString()))} skill generations this week.`
924
1097
  );
925
1098
  console.log(
926
- ` Your quota resets on ${pc5.yellow(new Date(quota.resetDate).toLocaleDateString())}.`
1099
+ ` Your quota resets on ${pc6.yellow(new Date(quota.resetDate).toLocaleDateString())}.`
927
1100
  );
928
1101
  log.blank();
929
1102
  if (quota.tier === "free") {
930
1103
  console.log(
931
- ` ${pc5.yellow("Tip:")} Upgrade to Pro for ${pc5.bold("10")} generations per week.`
1104
+ ` ${pc6.yellow("Tip:")} Upgrade to Pro for ${pc6.bold("10")} generations per week.`
932
1105
  );
933
- console.log(` Visit ${pc5.green("https://context7.com/dashboard")} to upgrade.`);
1106
+ console.log(` Visit ${pc6.green("https://context7.com/dashboard")} to upgrade.`);
934
1107
  }
935
1108
  return;
936
1109
  }
937
1110
  initSpinner.stop();
938
1111
  initSpinner.clear();
939
- console.log(pc5.bold("What should your agent become an expert at?\n"));
1112
+ console.log(pc6.bold("What should your agent become an expert at?\n"));
940
1113
  console.log(
941
- pc5.dim("Skills teach agents best practices, design patterns, and domain expertise.\n")
1114
+ pc6.dim(
1115
+ "Skills should encode best practices, constraints, and decision-making \u2014\nnot step-by-step tutorials or one-off tasks.\n"
1116
+ )
942
1117
  );
943
- console.log(pc5.yellow("Examples:"));
944
- console.log(pc5.dim(' "React component optimization and performance best practices"'));
945
- console.log(pc5.dim(' "Responsive web design with Tailwind CSS"'));
946
- console.log(pc5.dim(' "Writing effective landing page copy"'));
947
- console.log(pc5.dim(' "Deploying Next.js apps to Vercel"'));
948
- console.log(pc5.dim(' "OAuth authentication with NextAuth.js"\n'));
1118
+ console.log(pc6.yellow("Examples:"));
1119
+ {
1120
+ console.log(pc6.red(' \u2715 "Deploy a Next.js app to Vercel"'));
1121
+ console.log(pc6.green(' \u2713 "Best practices and constraints for deploying Next.js apps to Vercel"'));
1122
+ log.blank();
1123
+ console.log(pc6.red(' \u2715 "Use Tailwind for responsive design"'));
1124
+ console.log(pc6.green(' \u2713 "Responsive layout decision-making with Tailwind CSS"'));
1125
+ log.blank();
1126
+ console.log(pc6.red(' \u2715 "Build OAuth with NextAuth"'));
1127
+ console.log(pc6.green(' \u2713 "OAuth authentication patterns and pitfalls with NextAuth.js"'));
1128
+ }
1129
+ log.blank();
949
1130
  let motivation;
950
1131
  try {
951
1132
  motivation = await input({
@@ -960,14 +1141,26 @@ async function generateCommand(options) {
960
1141
  log.warn("Generation cancelled");
961
1142
  return;
962
1143
  }
963
- const searchSpinner = ora("Finding relevant libraries...").start();
1144
+ log.blank();
1145
+ console.log(
1146
+ pc6.dim(
1147
+ "To generate this skill, we will read relevant documentation and examples\nfrom Context7.\n"
1148
+ )
1149
+ );
1150
+ console.log(
1151
+ pc6.dim(
1152
+ "These sources are used to:\n\u2022 extract best practices and constraints\n\u2022 compare patterns across official docs and examples\n\u2022 avoid outdated or incorrect guidance\n"
1153
+ )
1154
+ );
1155
+ console.log(pc6.dim("You can adjust which sources the skill is based on.\n"));
1156
+ const searchSpinner = ora2("Finding relevant sources...").start();
964
1157
  const searchResult = await searchLibraries(motivation, accessToken);
965
1158
  if (searchResult.error || !searchResult.results?.length) {
966
- searchSpinner.fail(pc5.red("No libraries found"));
1159
+ searchSpinner.fail(pc6.red("No sources found"));
967
1160
  log.warn(searchResult.message || "Try a different description");
968
1161
  return;
969
1162
  }
970
- searchSpinner.succeed(pc5.green(`Found ${searchResult.results.length} relevant libraries`));
1163
+ searchSpinner.succeed(pc6.green(`Found ${searchResult.results.length} relevant sources`));
971
1164
  log.blank();
972
1165
  let selectedLibraries;
973
1166
  try {
@@ -987,31 +1180,32 @@ async function generateCommand(options) {
987
1180
  const libraryChoices = libraries.map((lib, index) => {
988
1181
  const projectId = formatProjectId(lib.id);
989
1182
  const isGitHub = isGitHubRepo(lib.id);
990
- const indexStr = pc5.dim(`${(index + 1).toString().padStart(indexWidth)}.`);
1183
+ const indexStr = pc6.dim(`${(index + 1).toString().padStart(indexWidth)}.`);
991
1184
  const paddedName = lib.title.padEnd(maxNameLen);
992
1185
  const libUrl = `https://context7.com${lib.id}`;
993
- const libLink = terminalLink(lib.title, libUrl, pc5.white);
994
- const repoLink = isGitHub ? terminalLink(projectId, `https://github.com/${projectId}`, pc5.white) : pc5.white(projectId);
995
- const starsLine = lib.stars && isGitHub ? [`${pc5.yellow("Stars:")} ${lib.stars.toLocaleString()}`] : [];
1186
+ const libLink = terminalLink(lib.title, libUrl, pc6.white);
1187
+ const sourceUrl = isGitHub ? `https://github.com/${projectId}` : `https://context7.com${lib.id}`;
1188
+ const repoLink = terminalLink(projectId, sourceUrl, pc6.white);
1189
+ const starsLine = lib.stars && isGitHub ? [`${pc6.yellow("Stars:")} ${lib.stars.toLocaleString()}`] : [];
996
1190
  const metadataLines = [
997
- pc5.dim("\u2500".repeat(50)),
1191
+ pc6.dim("\u2500".repeat(50)),
998
1192
  "",
999
- `${pc5.yellow("Library:")} ${libLink}`,
1000
- `${pc5.yellow("Source:")} ${repoLink}`,
1001
- `${pc5.yellow("Snippets:")} ${lib.totalSnippets.toLocaleString()}`,
1193
+ `${pc6.yellow("Library:")} ${libLink}`,
1194
+ `${pc6.yellow("Source:")} ${repoLink}`,
1195
+ `${pc6.yellow("Snippets:")} ${lib.totalSnippets.toLocaleString()}`,
1002
1196
  ...starsLine,
1003
- `${pc5.yellow("Description:")}`,
1004
- pc5.white(lib.description || "No description")
1197
+ `${pc6.yellow("Description:")}`,
1198
+ pc6.white(lib.description || "No description")
1005
1199
  ];
1006
1200
  return {
1007
- name: `${indexStr} ${paddedName} ${pc5.dim(`(${projectId})`)}`,
1201
+ name: `${indexStr} ${paddedName} ${pc6.dim(`(${projectId})`)}`,
1008
1202
  value: lib,
1009
1203
  description: metadataLines.join("\n")
1010
1204
  };
1011
1205
  });
1012
1206
  selectedLibraries = await checkboxWithHover(
1013
1207
  {
1014
- message: "Select libraries:",
1208
+ message: "Select sources:",
1015
1209
  choices: libraryChoices,
1016
1210
  pageSize: 10,
1017
1211
  loop: false
@@ -1019,7 +1213,7 @@ async function generateCommand(options) {
1019
1213
  { getName: (lib) => `${lib.title} (${formatProjectId(lib.id)})` }
1020
1214
  );
1021
1215
  if (!selectedLibraries || selectedLibraries.length === 0) {
1022
- log.info("No libraries selected. Try running the command again.");
1216
+ log.info("No sources selected. Try running the command again.");
1023
1217
  return;
1024
1218
  }
1025
1219
  } catch {
@@ -1027,15 +1221,17 @@ async function generateCommand(options) {
1027
1221
  return;
1028
1222
  }
1029
1223
  log.blank();
1030
- const questionsSpinner = ora("Preparing questions...").start();
1224
+ const questionsSpinner = ora2(
1225
+ "Preparing follow-up questions to clarify scope and constraints..."
1226
+ ).start();
1031
1227
  const librariesInput = selectedLibraries.map((lib) => ({ id: lib.id, name: lib.title }));
1032
1228
  const questionsResult = await getSkillQuestions(librariesInput, motivation, accessToken);
1033
1229
  if (questionsResult.error || !questionsResult.questions?.length) {
1034
- questionsSpinner.fail(pc5.red("Failed to generate questions"));
1230
+ questionsSpinner.fail(pc6.red("Failed to generate questions"));
1035
1231
  log.warn(questionsResult.message || "Please try again");
1036
1232
  return;
1037
1233
  }
1038
- questionsSpinner.succeed(pc5.green("Questions prepared"));
1234
+ questionsSpinner.succeed(pc6.green("Questions prepared"));
1039
1235
  log.blank();
1040
1236
  const answers = [];
1041
1237
  try {
@@ -1044,7 +1240,7 @@ async function generateCommand(options) {
1044
1240
  const questionNum = i + 1;
1045
1241
  const totalQuestions = questionsResult.questions.length;
1046
1242
  const answer = await selectOrInput_default({
1047
- message: `${pc5.dim(`[${questionNum}/${totalQuestions}]`)} ${q.question}`,
1243
+ message: `${pc6.dim(`[${questionNum}/${totalQuestions}]`)} ${q.question}`,
1048
1244
  options: q.options,
1049
1245
  recommendedIndex: q.recommendedIndex
1050
1246
  });
@@ -1055,8 +1251,8 @@ async function generateCommand(options) {
1055
1251
  const linesToClear = 3 + q.options.length;
1056
1252
  process.stdout.write(`\x1B[${linesToClear}A\x1B[J`);
1057
1253
  const truncatedAnswer = answer.length > 50 ? answer.slice(0, 47) + "..." : answer;
1058
- console.log(`${pc5.green("\u2713")} ${pc5.dim(`[${questionNum}/${totalQuestions}]`)} ${q.question}`);
1059
- console.log(` ${pc5.cyan(truncatedAnswer)}`);
1254
+ console.log(`${pc6.green("\u2713")} ${pc6.dim(`[${questionNum}/${totalQuestions}]`)} ${q.question}`);
1255
+ console.log(` ${pc6.cyan(truncatedAnswer)}`);
1060
1256
  log.blank();
1061
1257
  }
1062
1258
  } catch {
@@ -1066,33 +1262,39 @@ async function generateCommand(options) {
1066
1262
  let generatedContent = null;
1067
1263
  let skillName = "";
1068
1264
  let feedback;
1069
- const libraryNames = selectedLibraries.map((lib) => lib.title).join(", ");
1265
+ let previewFile = null;
1266
+ let previewFileWritten = false;
1267
+ const cleanupPreviewFile = async () => {
1268
+ if (previewFile) {
1269
+ await unlink(previewFile).catch(() => {
1270
+ });
1271
+ }
1272
+ };
1070
1273
  const queryLog = [];
1071
1274
  let genSpinner = null;
1072
1275
  const formatQueryLogText = () => {
1073
1276
  if (queryLog.length === 0) return "";
1074
1277
  const lines = [];
1075
1278
  const latestEntry = queryLog[queryLog.length - 1];
1076
- lines.push(pc5.dim(`(${queryLog.length} ${queryLog.length === 1 ? "query" : "queries"})`));
1077
1279
  lines.push("");
1078
1280
  for (const result of latestEntry.results.slice(0, 3)) {
1079
1281
  const cleanContent = result.content.replace(/Source:\s*https?:\/\/[^\s]+/gi, "").trim();
1080
1282
  if (cleanContent) {
1081
- lines.push(` ${pc5.yellow("\u2022")} ${pc5.white(result.title)}`);
1283
+ lines.push(` ${pc6.yellow("\u2022")} ${pc6.white(result.title)}`);
1082
1284
  const maxLen = 400;
1083
1285
  const content = cleanContent.length > maxLen ? cleanContent.slice(0, maxLen - 3) + "..." : cleanContent;
1084
1286
  const words = content.split(" ");
1085
1287
  let currentLine = " ";
1086
1288
  for (const word of words) {
1087
1289
  if (currentLine.length + word.length > 84) {
1088
- lines.push(pc5.dim(currentLine));
1290
+ lines.push(pc6.dim(currentLine));
1089
1291
  currentLine = " " + word + " ";
1090
1292
  } else {
1091
1293
  currentLine += word + " ";
1092
1294
  }
1093
1295
  }
1094
1296
  if (currentLine.trim()) {
1095
- lines.push(pc5.dim(currentLine));
1297
+ lines.push(pc6.dim(currentLine));
1096
1298
  }
1097
1299
  lines.push("");
1098
1300
  }
@@ -1100,19 +1302,20 @@ async function generateCommand(options) {
1100
1302
  return "\n" + lines.join("\n");
1101
1303
  };
1102
1304
  let isGeneratingContent = false;
1305
+ let initialStatus = "Reading selected Context7 sources to generate the skill...";
1103
1306
  const handleStreamEvent = (event) => {
1104
1307
  if (event.type === "progress") {
1105
1308
  if (genSpinner) {
1106
1309
  if (event.message.startsWith("Generating skill content...") && !isGeneratingContent) {
1107
1310
  isGeneratingContent = true;
1108
1311
  if (queryLog.length > 0) {
1109
- genSpinner.succeed(pc5.green(`Queried documentation`));
1312
+ genSpinner.succeed(pc6.green(`Read Context7 sources`));
1110
1313
  } else {
1111
- genSpinner.succeed(pc5.green(`Ready to generate`));
1314
+ genSpinner.succeed(pc6.green(`Ready to generate`));
1112
1315
  }
1113
- genSpinner = ora("Generating skill content...").start();
1316
+ genSpinner = ora2("Generating skill content...").start();
1114
1317
  } else if (!isGeneratingContent) {
1115
- genSpinner.text = event.message + formatQueryLogText();
1318
+ genSpinner.text = initialStatus + formatQueryLogText();
1116
1319
  }
1117
1320
  }
1118
1321
  } else if (event.type === "tool_result") {
@@ -1136,18 +1339,19 @@ async function generateCommand(options) {
1136
1339
  };
1137
1340
  queryLog.length = 0;
1138
1341
  isGeneratingContent = false;
1139
- const initialStatus = feedback ? "Regenerating skill with your feedback..." : `Generating skill for "${libraryNames}"...`;
1140
- genSpinner = ora(initialStatus).start();
1342
+ previewFileWritten = false;
1343
+ initialStatus = feedback ? "Regenerating skill with your feedback..." : "Reading selected Context7 sources to generate the skill...";
1344
+ genSpinner = ora2(initialStatus).start();
1141
1345
  const result = await generateSkillStructured(generateInput, handleStreamEvent, accessToken);
1142
1346
  if (result.error) {
1143
- genSpinner.fail(pc5.red(`Error: ${result.error}`));
1347
+ genSpinner.fail(pc6.red(`Error: ${result.error}`));
1144
1348
  return;
1145
1349
  }
1146
1350
  if (!result.content) {
1147
- genSpinner.fail(pc5.red("No content generated"));
1351
+ genSpinner.fail(pc6.red("No content generated"));
1148
1352
  return;
1149
1353
  }
1150
- genSpinner.succeed(pc5.green(`Generated skill for "${result.libraryName}"`));
1354
+ genSpinner.succeed(pc6.green(`Generated skill for "${result.libraryName}"`));
1151
1355
  generatedContent = result.content;
1152
1356
  skillName = result.libraryName.toLowerCase().replace(/[^a-z0-9-]/g, "-");
1153
1357
  const contentLines = generatedContent.split("\n");
@@ -1157,29 +1361,40 @@ async function generateCommand(options) {
1157
1361
  const remainingLines = contentLines.length - previewLineCount;
1158
1362
  const showPreview = () => {
1159
1363
  log.blank();
1160
- console.log(pc5.dim("\u2501".repeat(70)));
1161
- console.log(pc5.bold(`Generated Skill: `) + pc5.green(pc5.bold(skillName)));
1162
- console.log(pc5.dim("\u2501".repeat(70)));
1364
+ console.log(pc6.dim("\u2501".repeat(70)));
1365
+ console.log(pc6.bold(`Generated Skill: `) + pc6.green(pc6.bold(skillName)));
1366
+ console.log(pc6.dim("\u2501".repeat(70)));
1163
1367
  log.blank();
1164
1368
  console.log(previewContent);
1165
1369
  if (hasMoreLines) {
1166
1370
  log.blank();
1167
- console.log(pc5.dim(`... ${remainingLines} more lines`));
1371
+ console.log(pc6.dim(`... ${remainingLines} more lines`));
1168
1372
  }
1169
1373
  log.blank();
1170
- console.log(pc5.dim("\u2501".repeat(70)));
1374
+ console.log(pc6.dim("\u2501".repeat(70)));
1171
1375
  log.blank();
1172
1376
  };
1173
- const showFullContent = () => {
1174
- log.blank();
1175
- console.log(pc5.dim("\u2501".repeat(70)));
1176
- console.log(pc5.bold(`Generated Skill: `) + pc5.green(pc5.bold(skillName)));
1177
- console.log(pc5.dim("\u2501".repeat(70)));
1178
- log.blank();
1179
- console.log(generatedContent);
1180
- log.blank();
1181
- console.log(pc5.dim("\u2501".repeat(70)));
1182
- log.blank();
1377
+ const openInEditor = async () => {
1378
+ const previewDir = join4(homedir3(), ".context7", "previews");
1379
+ await mkdir2(previewDir, { recursive: true });
1380
+ previewFile = join4(previewDir, `${skillName}.md`);
1381
+ if (!previewFileWritten) {
1382
+ await writeFile2(previewFile, generatedContent, "utf-8");
1383
+ previewFileWritten = true;
1384
+ }
1385
+ const editor = process.env.EDITOR || "open";
1386
+ await new Promise((resolve) => {
1387
+ const child = spawn(editor, [previewFile], {
1388
+ stdio: "inherit",
1389
+ shell: true
1390
+ });
1391
+ child.on("close", () => resolve());
1392
+ });
1393
+ };
1394
+ const syncFromPreviewFile = async () => {
1395
+ if (previewFile) {
1396
+ generatedContent = await readFile(previewFile, "utf-8");
1397
+ }
1183
1398
  };
1184
1399
  showPreview();
1185
1400
  await new Promise((resolve) => setTimeout(resolve, 100));
@@ -1187,24 +1402,26 @@ async function generateCommand(options) {
1187
1402
  let action;
1188
1403
  while (true) {
1189
1404
  const choices = [
1190
- { name: `${pc5.green("\u2713")} Install skill`, value: "install" },
1191
- ...hasMoreLines ? [{ name: `${pc5.blue("\u2922")} View full skill`, value: "expand" }] : [],
1192
- { name: `${pc5.yellow("\u270E")} Request changes`, value: "feedback" },
1193
- { name: `${pc5.red("\u2715")} Cancel`, value: "cancel" }
1405
+ { name: `${pc6.green("\u2713")} Install skill (save locally)`, value: "install" },
1406
+ { name: `${pc6.blue("\u2922")} Edit skill in editor`, value: "view" },
1407
+ { name: `${pc6.yellow("\u270E")} Request changes`, value: "feedback" },
1408
+ { name: `${pc6.red("\u2715")} Cancel`, value: "cancel" }
1194
1409
  ];
1195
1410
  action = await select2({
1196
1411
  message: "What would you like to do?",
1197
1412
  choices
1198
1413
  });
1199
- if (action === "expand") {
1200
- showFullContent();
1414
+ if (action === "view") {
1415
+ await openInEditor();
1201
1416
  continue;
1202
1417
  }
1418
+ await syncFromPreviewFile();
1203
1419
  break;
1204
1420
  }
1205
1421
  if (action === "install") {
1206
1422
  break;
1207
1423
  } else if (action === "cancel") {
1424
+ await cleanupPreviewFile();
1208
1425
  log.warn("Generation cancelled");
1209
1426
  return;
1210
1427
  } else if (action === "feedback") {
@@ -1218,6 +1435,7 @@ async function generateCommand(options) {
1218
1435
  log.blank();
1219
1436
  }
1220
1437
  } catch {
1438
+ await cleanupPreviewFile();
1221
1439
  log.warn("Generation cancelled");
1222
1440
  return;
1223
1441
  }
@@ -1228,7 +1446,7 @@ async function generateCommand(options) {
1228
1446
  return;
1229
1447
  }
1230
1448
  const targetDirs = getTargetDirs(targets);
1231
- const writeSpinner = ora("Writing skill files...").start();
1449
+ const writeSpinner = ora2("Writing skill files...").start();
1232
1450
  let permissionError = false;
1233
1451
  const failedDirs = /* @__PURE__ */ new Set();
1234
1452
  for (const targetDir of targetDirs) {
@@ -1252,28 +1470,116 @@ async function generateCommand(options) {
1252
1470
  }
1253
1471
  }
1254
1472
  if (permissionError) {
1255
- writeSpinner.fail(pc5.red("Permission denied"));
1473
+ writeSpinner.fail(pc6.red("Permission denied"));
1256
1474
  log.blank();
1257
- console.log(pc5.yellow("Fix permissions with:"));
1475
+ console.log(pc6.yellow("Fix permissions with:"));
1258
1476
  for (const dir of failedDirs) {
1259
1477
  const parentDir = join4(dir, "..");
1260
- console.log(pc5.dim(` sudo chown -R $(whoami) "${parentDir}"`));
1478
+ console.log(pc6.dim(` sudo chown -R $(whoami) "${parentDir}"`));
1261
1479
  }
1262
1480
  log.blank();
1263
1481
  return;
1264
1482
  }
1265
- writeSpinner.succeed(pc5.green(`Created skill in ${targetDirs.length} location(s)`));
1483
+ writeSpinner.succeed(pc6.green(`Created skill in ${targetDirs.length} location(s)`));
1266
1484
  trackEvent("gen_install");
1267
1485
  log.blank();
1268
- console.log(pc5.green(pc5.bold("Skill saved successfully")));
1486
+ console.log(pc6.green("Skill saved successfully"));
1269
1487
  for (const targetDir of targetDirs) {
1270
- console.log(pc5.dim(` ${targetDir}/`) + pc5.green(skillName));
1488
+ console.log(pc6.dim(` ${targetDir}/`) + pc6.green(skillName));
1271
1489
  }
1272
1490
  log.blank();
1491
+ await cleanupPreviewFile();
1273
1492
  }
1274
1493
 
1275
1494
  // src/commands/skill.ts
1276
1495
  import { homedir as homedir4 } from "os";
1496
+
1497
+ // src/utils/deps.ts
1498
+ import { readFile as readFile2 } from "fs/promises";
1499
+ import { join as join5 } from "path";
1500
+ async function readFileOrNull(path2) {
1501
+ try {
1502
+ return await readFile2(path2, "utf-8");
1503
+ } catch {
1504
+ return null;
1505
+ }
1506
+ }
1507
+ function isSkippedLocally(name) {
1508
+ return name.startsWith("@types/");
1509
+ }
1510
+ async function parsePackageJson(cwd) {
1511
+ const content = await readFileOrNull(join5(cwd, "package.json"));
1512
+ if (!content) return [];
1513
+ try {
1514
+ const pkg2 = JSON.parse(content);
1515
+ const names = /* @__PURE__ */ new Set();
1516
+ for (const key of Object.keys(pkg2.dependencies || {})) {
1517
+ if (!isSkippedLocally(key)) names.add(key);
1518
+ }
1519
+ for (const key of Object.keys(pkg2.devDependencies || {})) {
1520
+ if (!isSkippedLocally(key)) names.add(key);
1521
+ }
1522
+ return [...names];
1523
+ } catch {
1524
+ return [];
1525
+ }
1526
+ }
1527
+ async function parseRequirementsTxt(cwd) {
1528
+ const content = await readFileOrNull(join5(cwd, "requirements.txt"));
1529
+ if (!content) return [];
1530
+ const deps = [];
1531
+ for (const line of content.split("\n")) {
1532
+ const trimmed = line.trim();
1533
+ if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("-")) continue;
1534
+ const name = trimmed.split(/[=<>!~;@\s\[]/)[0].trim();
1535
+ if (name && !isSkippedLocally(name)) {
1536
+ deps.push(name);
1537
+ }
1538
+ }
1539
+ return deps;
1540
+ }
1541
+ async function parsePyprojectToml(cwd) {
1542
+ const content = await readFileOrNull(join5(cwd, "pyproject.toml"));
1543
+ if (!content) return [];
1544
+ const deps = [];
1545
+ const seen = /* @__PURE__ */ new Set();
1546
+ const projectDepsMatch = content.match(/\[project\]\s[\s\S]*?dependencies\s*=\s*\[([\s\S]*?)\]/);
1547
+ if (projectDepsMatch) {
1548
+ const entries = projectDepsMatch[1].match(/"([^"]+)"/g) || [];
1549
+ for (const entry of entries) {
1550
+ const name = entry.replace(/"/g, "").split(/[=<>!~;@\s\[]/)[0].trim();
1551
+ if (name && !isSkippedLocally(name) && !seen.has(name)) {
1552
+ seen.add(name);
1553
+ deps.push(name);
1554
+ }
1555
+ }
1556
+ }
1557
+ const poetryMatch = content.match(/\[tool\.poetry\.dependencies\]([\s\S]*?)(?:\n\[|$)/);
1558
+ if (poetryMatch) {
1559
+ const lines = poetryMatch[1].split("\n");
1560
+ for (const line of lines) {
1561
+ const match = line.match(/^(\S+)\s*=/);
1562
+ if (match) {
1563
+ const name = match[1].trim();
1564
+ if (name && !isSkippedLocally(name) && name !== "python" && !seen.has(name)) {
1565
+ seen.add(name);
1566
+ deps.push(name);
1567
+ }
1568
+ }
1569
+ }
1570
+ }
1571
+ return deps;
1572
+ }
1573
+ async function detectProjectDependencies(cwd) {
1574
+ const results = await Promise.all([
1575
+ parsePackageJson(cwd),
1576
+ parseRequirementsTxt(cwd),
1577
+ parsePyprojectToml(cwd)
1578
+ ]);
1579
+ return [...new Set(results.flat())];
1580
+ }
1581
+
1582
+ // src/commands/skill.ts
1277
1583
  function logInstallSummary(targets, targetDirs, skillNames) {
1278
1584
  log.blank();
1279
1585
  let dirIndex = 0;
@@ -1306,6 +1612,9 @@ function registerSkillCommands(program2) {
1306
1612
  skill.command("info").argument("<repository>", "GitHub repository (/owner/repo)").description("Show skills in a repository").action(async (project) => {
1307
1613
  await infoCommand(project);
1308
1614
  });
1615
+ skill.command("suggest").option("--global", "Install globally instead of current directory").option("--claude", "Claude Code (.claude/skills/)").option("--cursor", "Cursor (.cursor/skills/)").option("--codex", "Codex (.codex/skills/)").option("--opencode", "OpenCode (.opencode/skills/)").option("--amp", "Amp (.agents/skills/)").option("--antigravity", "Antigravity (.agent/skills/)").description("Suggest skills based on your project dependencies").action(async (options) => {
1616
+ await suggestCommand(options);
1617
+ });
1309
1618
  }
1310
1619
  function registerSkillAliases(program2) {
1311
1620
  program2.command("si", { hidden: true }).argument("<repository>", "GitHub repository (/owner/repo)").argument("[skill]", "Specific skill name to install").option("--all", "Install all skills without prompting").option("--global", "Install globally instead of current directory").option("--claude", "Claude Code (.claude/skills/)").option("--cursor", "Cursor (.cursor/skills/)").option("--codex", "Codex (.codex/skills/)").option("--opencode", "OpenCode (.opencode/skills/)").option("--amp", "Amp (.agents/skills/)").option("--antigravity", "Antigravity (.agent/skills/)").description("Install skills (alias for: skills install)").action(async (project, skillName, options) => {
@@ -1314,6 +1623,9 @@ function registerSkillAliases(program2) {
1314
1623
  program2.command("ss", { hidden: true }).argument("<keywords...>", "Search keywords").description("Search for skills (alias for: skills search)").action(async (keywords) => {
1315
1624
  await searchCommand(keywords.join(" "));
1316
1625
  });
1626
+ program2.command("ssg", { hidden: true }).option("--global", "Install globally instead of current directory").option("--claude", "Claude Code (.claude/skills/)").option("--cursor", "Cursor (.cursor/skills/)").option("--codex", "Codex (.codex/skills/)").option("--opencode", "OpenCode (.opencode/skills/)").option("--amp", "Amp (.agents/skills/)").option("--antigravity", "Antigravity (.agent/skills/)").description("Suggest skills (alias for: skills suggest)").action(async (options) => {
1627
+ await suggestCommand(options);
1628
+ });
1317
1629
  }
1318
1630
  async function installCommand(input2, skillName, options) {
1319
1631
  trackEvent("command", { name: "install" });
@@ -1327,17 +1639,17 @@ async function installCommand(input2, skillName, options) {
1327
1639
  }
1328
1640
  const repo = `/${parsed.owner}/${parsed.repo}`;
1329
1641
  log.blank();
1330
- const spinner = ora2(`Fetching skills from ${repo}...`).start();
1642
+ const spinner = ora3(`Fetching skills from ${repo}...`).start();
1331
1643
  let selectedSkills;
1332
1644
  if (skillName) {
1333
1645
  spinner.text = `Fetching skill: ${skillName}...`;
1334
1646
  const skillData = await getSkill(repo, skillName);
1335
1647
  if (skillData.error || !skillData.name) {
1336
1648
  if (skillData.error === "prompt_injection_detected") {
1337
- spinner.fail(pc6.red(`Prompt injection detected in skill: ${skillName}`));
1649
+ spinner.fail(pc7.red(`Prompt injection detected in skill: ${skillName}`));
1338
1650
  log.warn("This skill contains potentially malicious content and cannot be installed.");
1339
1651
  } else {
1340
- spinner.fail(pc6.red(`Skill not found: ${skillName}`));
1652
+ spinner.fail(pc7.red(`Skill not found: ${skillName}`));
1341
1653
  }
1342
1654
  return;
1343
1655
  }
@@ -1353,11 +1665,11 @@ async function installCommand(input2, skillName, options) {
1353
1665
  } else {
1354
1666
  const data = await listProjectSkills(repo);
1355
1667
  if (data.error) {
1356
- spinner.fail(pc6.red(`Error: ${data.message || data.error}`));
1668
+ spinner.fail(pc7.red(`Error: ${data.message || data.error}`));
1357
1669
  return;
1358
1670
  }
1359
1671
  if (!data.skills || data.skills.length === 0) {
1360
- spinner.warn(pc6.yellow(`No skills found in ${repo}`));
1672
+ spinner.warn(pc7.yellow(`No skills found in ${repo}`));
1361
1673
  return;
1362
1674
  }
1363
1675
  const skillsWithRepo = data.skills.map((s) => ({ ...s, project: repo })).sort((a, b) => (b.installCount ?? 0) - (a.installCount ?? 0));
@@ -1375,19 +1687,19 @@ async function installCommand(input2, skillName, options) {
1375
1687
  const indexWidth = data.skills.length.toString().length;
1376
1688
  const maxNameLen = Math.max(...data.skills.map((s) => s.name.length));
1377
1689
  const choices = skillsWithRepo.map((s, index) => {
1378
- const indexStr = pc6.dim(`${(index + 1).toString().padStart(indexWidth)}.`);
1690
+ const indexStr = pc7.dim(`${(index + 1).toString().padStart(indexWidth)}.`);
1379
1691
  const paddedName = s.name.padEnd(maxNameLen);
1380
1692
  const installs = formatInstallCount(s.installCount);
1381
1693
  const skillUrl = `https://context7.com/skills${s.project}/${s.name}`;
1382
- const skillLink = terminalLink(s.name, skillUrl, pc6.white);
1383
- const repoLink = terminalLink(s.project, `https://github.com${s.project}`, pc6.white);
1694
+ const skillLink = terminalLink(s.name, skillUrl, pc7.white);
1695
+ const repoLink = terminalLink(s.project, `https://github.com${s.project}`, pc7.white);
1384
1696
  const metadataLines = [
1385
- pc6.dim("\u2500".repeat(50)),
1697
+ pc7.dim("\u2500".repeat(50)),
1386
1698
  "",
1387
- `${pc6.yellow("Skill:")} ${skillLink}`,
1388
- `${pc6.yellow("Repo:")} ${repoLink}`,
1389
- `${pc6.yellow("Description:")}`,
1390
- pc6.white(s.description || "No description")
1699
+ `${pc7.yellow("Skill:")} ${skillLink}`,
1700
+ `${pc7.yellow("Repo:")} ${repoLink}`,
1701
+ `${pc7.yellow("Description:")}`,
1702
+ pc7.white(s.description || "No description")
1391
1703
  ];
1392
1704
  return {
1393
1705
  name: installs ? `${indexStr} ${paddedName} ${installs}` : `${indexStr} ${paddedName}`,
@@ -1397,7 +1709,7 @@ async function installCommand(input2, skillName, options) {
1397
1709
  });
1398
1710
  log.blank();
1399
1711
  const installsOffset = 4 + indexWidth + 1 + 1 + maxNameLen + 1 - 3;
1400
- const message = "Select skills:" + " ".repeat(Math.max(1, installsOffset - 14)) + pc6.dim("installs");
1712
+ const message = "Select skills:" + " ".repeat(Math.max(1, installsOffset - 14)) + pc7.dim("installs");
1401
1713
  try {
1402
1714
  selectedSkills = await checkboxWithHover({
1403
1715
  message,
@@ -1421,7 +1733,7 @@ async function installCommand(input2, skillName, options) {
1421
1733
  return;
1422
1734
  }
1423
1735
  const targetDirs = getTargetDirs(targets);
1424
- const installSpinner = ora2("Installing skills...").start();
1736
+ const installSpinner = ora3("Installing skills...").start();
1425
1737
  let permissionError = false;
1426
1738
  const failedDirs = /* @__PURE__ */ new Set();
1427
1739
  const installedSkills = [];
@@ -1445,7 +1757,7 @@ async function installCommand(input2, skillName, options) {
1445
1757
  }
1446
1758
  throw dirErr;
1447
1759
  }
1448
- const primarySkillDir = join5(primaryDir, skill.name);
1760
+ const primarySkillDir = join6(primaryDir, skill.name);
1449
1761
  for (const targetDir of symlinkDirs) {
1450
1762
  try {
1451
1763
  await symlinkSkill(skill.name, primarySkillDir, targetDir);
@@ -1473,7 +1785,7 @@ async function installCommand(input2, skillName, options) {
1473
1785
  log.blank();
1474
1786
  log.warn("Fix permissions with:");
1475
1787
  for (const dir of failedDirs) {
1476
- const parentDir = join5(dir, "..");
1788
+ const parentDir = join6(dir, "..");
1477
1789
  log.dim(` sudo chown -R $(whoami) "${parentDir}"`);
1478
1790
  }
1479
1791
  log.blank();
@@ -1487,53 +1799,58 @@ async function installCommand(input2, skillName, options) {
1487
1799
  async function searchCommand(query) {
1488
1800
  trackEvent("command", { name: "search" });
1489
1801
  log.blank();
1490
- const spinner = ora2(`Searching for "${query}"...`).start();
1802
+ const spinner = ora3(`Searching for "${query}"...`).start();
1491
1803
  let data;
1492
1804
  try {
1493
1805
  data = await searchSkills(query);
1494
1806
  } catch (err) {
1495
- spinner.fail(pc6.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
1807
+ spinner.fail(pc7.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
1496
1808
  return;
1497
1809
  }
1498
1810
  if (data.error) {
1499
- spinner.fail(pc6.red(`Error: ${data.message || data.error}`));
1811
+ spinner.fail(pc7.red(`Error: ${data.message || data.error}`));
1500
1812
  return;
1501
1813
  }
1502
1814
  if (!data.results || data.results.length === 0) {
1503
- spinner.warn(pc6.yellow(`No skills found matching "${query}"`));
1815
+ spinner.warn(pc7.yellow(`No skills found matching "${query}"`));
1504
1816
  return;
1505
1817
  }
1506
1818
  spinner.succeed(`Found ${data.results.length} skill(s)`);
1507
1819
  trackEvent("search_query", { query, resultCount: data.results.length });
1508
1820
  const indexWidth = data.results.length.toString().length;
1509
1821
  const maxNameLen = Math.max(...data.results.map((s) => s.name.length));
1822
+ const installsColWidth = 10;
1510
1823
  const choices = data.results.map((s, index) => {
1511
- const indexStr = pc6.dim(`${(index + 1).toString().padStart(indexWidth)}.`);
1824
+ const indexStr = pc7.dim(`${(index + 1).toString().padStart(indexWidth)}.`);
1512
1825
  const paddedName = s.name.padEnd(maxNameLen);
1513
- const installs = formatInstallCount(s.installCount);
1826
+ const installsRaw = s.installCount ? String(s.installCount) : "-";
1827
+ const paddedInstalls = formatInstallCount(s.installCount, pc7.dim("-")) + " ".repeat(installsColWidth - installsRaw.length);
1828
+ const trust = formatTrustScore(s.trustScore);
1514
1829
  const skillLink = terminalLink(
1515
1830
  s.name,
1516
1831
  `https://context7.com/skills${s.project}/${s.name}`,
1517
- pc6.white
1832
+ pc7.white
1518
1833
  );
1519
- const repoLink = terminalLink(s.project, `https://github.com${s.project}`, pc6.white);
1834
+ const repoLink = terminalLink(s.project, `https://github.com${s.project}`, pc7.white);
1520
1835
  const metadataLines = [
1521
- pc6.dim("\u2500".repeat(50)),
1836
+ pc7.dim("\u2500".repeat(50)),
1522
1837
  "",
1523
- `${pc6.yellow("Skill:")} ${skillLink}`,
1524
- `${pc6.yellow("Repo:")} ${repoLink}`,
1525
- `${pc6.yellow("Description:")}`,
1526
- pc6.white(s.description || "No description")
1838
+ `${pc7.yellow("Skill:")} ${skillLink}`,
1839
+ `${pc7.yellow("Repo:")} ${repoLink}`,
1840
+ `${pc7.yellow("Description:")}`,
1841
+ pc7.white(s.description || "No description")
1527
1842
  ];
1528
1843
  return {
1529
- name: installs ? `${indexStr} ${paddedName} ${installs}` : `${indexStr} ${paddedName}`,
1844
+ name: `${indexStr} ${paddedName} ${paddedInstalls}${trust}`,
1530
1845
  value: s,
1531
1846
  description: metadataLines.join("\n")
1532
1847
  };
1533
1848
  });
1534
1849
  log.blank();
1535
- const installsOffset = 4 + indexWidth + 1 + 1 + maxNameLen + 1 - 3;
1536
- const message = "Select skills to install:" + " ".repeat(Math.max(1, installsOffset - 25)) + pc6.dim("installs");
1850
+ const checkboxPrefixWidth = 3;
1851
+ const headerPad = " ".repeat(checkboxPrefixWidth + indexWidth + 1 + 1 + maxNameLen + 1);
1852
+ const headerLine = headerPad + pc7.dim("Installs".padEnd(installsColWidth)) + pc7.dim("Trust(0-10)");
1853
+ const message = "Select skills to install:\n" + headerLine;
1537
1854
  let selectedSkills;
1538
1855
  try {
1539
1856
  selectedSkills = await checkboxWithHover({
@@ -1557,7 +1874,7 @@ async function searchCommand(query) {
1557
1874
  return;
1558
1875
  }
1559
1876
  const targetDirs = getTargetDirs(targets);
1560
- const installSpinner = ora2("Installing skills...").start();
1877
+ const installSpinner = ora3("Installing skills...").start();
1561
1878
  let permissionError = false;
1562
1879
  const failedDirs = /* @__PURE__ */ new Set();
1563
1880
  const installedSkills = [];
@@ -1581,7 +1898,7 @@ async function searchCommand(query) {
1581
1898
  }
1582
1899
  throw dirErr;
1583
1900
  }
1584
- const primarySkillDir = join5(primaryDir, skill.name);
1901
+ const primarySkillDir = join6(primaryDir, skill.name);
1585
1902
  for (const targetDir of symlinkDirs) {
1586
1903
  try {
1587
1904
  await symlinkSkill(skill.name, primarySkillDir, targetDir);
@@ -1609,7 +1926,7 @@ async function searchCommand(query) {
1609
1926
  log.blank();
1610
1927
  log.warn("Fix permissions with:");
1611
1928
  for (const dir of failedDirs) {
1612
- const parentDir = join5(dir, "..");
1929
+ const parentDir = join6(dir, "..");
1613
1930
  log.dim(` sudo chown -R $(whoami) "${parentDir}"`);
1614
1931
  }
1615
1932
  log.blank();
@@ -1628,7 +1945,7 @@ async function listCommand(options) {
1628
1945
  const idesToCheck = hasExplicitIdeOption(options) ? getSelectedIdes(options) : Object.keys(IDE_NAMES);
1629
1946
  const results = [];
1630
1947
  for (const ide of idesToCheck) {
1631
- const skillsDir = join5(baseDir, pathMap[ide]);
1948
+ const skillsDir = join6(baseDir, pathMap[ide]);
1632
1949
  try {
1633
1950
  const entries = await readdir(skillsDir, { withFileTypes: true });
1634
1951
  const skillFolders = entries.filter((e) => e.isDirectory() || e.isSymbolicLink()).map((e) => e.name);
@@ -1646,9 +1963,9 @@ async function listCommand(options) {
1646
1963
  for (const { ide, skills } of results) {
1647
1964
  const ideName = IDE_NAMES[ide];
1648
1965
  const path2 = pathMap[ide];
1649
- log.plain(`${pc6.bold(ideName)} ${pc6.dim(path2)}`);
1966
+ log.plain(`${pc7.bold(ideName)} ${pc7.dim(path2)}`);
1650
1967
  for (const skill of skills) {
1651
- log.plain(` ${pc6.green(skill)}`);
1968
+ log.plain(` ${pc7.green(skill)}`);
1652
1969
  }
1653
1970
  log.blank();
1654
1971
  }
@@ -1661,7 +1978,7 @@ async function removeCommand(name, options) {
1661
1978
  return;
1662
1979
  }
1663
1980
  const skillsDir = getTargetDirFromSelection(target.ide, target.scope);
1664
- const skillPath = join5(skillsDir, name);
1981
+ const skillPath = join6(skillsDir, name);
1665
1982
  try {
1666
1983
  await rm2(skillPath, { recursive: true });
1667
1984
  log.success(`Removed skill: ${name}`);
@@ -1688,14 +2005,14 @@ async function infoCommand(input2) {
1688
2005
  }
1689
2006
  const repo = `/${parsed.owner}/${parsed.repo}`;
1690
2007
  log.blank();
1691
- const spinner = ora2(`Fetching skills from ${repo}...`).start();
2008
+ const spinner = ora3(`Fetching skills from ${repo}...`).start();
1692
2009
  const data = await listProjectSkills(repo);
1693
2010
  if (data.error) {
1694
- spinner.fail(pc6.red(`Error: ${data.message || data.error}`));
2011
+ spinner.fail(pc7.red(`Error: ${data.message || data.error}`));
1695
2012
  return;
1696
2013
  }
1697
2014
  if (!data.skills || data.skills.length === 0) {
1698
- spinner.warn(pc6.yellow(`No skills found in ${repo}`));
2015
+ spinner.warn(pc7.yellow(`No skills found in ${repo}`));
1699
2016
  return;
1700
2017
  }
1701
2018
  spinner.succeed(`Found ${data.skills.length} skill(s)`);
@@ -1707,154 +2024,175 @@ async function infoCommand(input2) {
1707
2024
  log.blank();
1708
2025
  }
1709
2026
  log.plain(
1710
- `${pc6.bold("Quick commands:")}
1711
- Install all: ${pc6.cyan(`ctx7 skills install ${repo} --all`)}
1712
- Install one: ${pc6.cyan(`ctx7 skills install ${repo} ${data.skills[0]?.name}`)}
2027
+ `${pc7.bold("Quick commands:")}
2028
+ Install all: ${pc7.cyan(`ctx7 skills install ${repo} --all`)}
2029
+ Install one: ${pc7.cyan(`ctx7 skills install ${repo} ${data.skills[0]?.name}`)}
1713
2030
  `
1714
2031
  );
1715
2032
  }
1716
-
1717
- // src/commands/auth.ts
1718
- import pc7 from "picocolors";
1719
- import ora3 from "ora";
1720
- import open from "open";
1721
- var CLI_CLIENT_ID = "2veBSofhicRBguUT";
1722
- var baseUrl2 = "https://context7.com";
1723
- function setAuthBaseUrl(url) {
1724
- baseUrl2 = url;
1725
- }
1726
- function registerAuthCommands(program2) {
1727
- program2.command("login").description("Log in to Context7").option("--no-browser", "Don't open browser automatically").action(async (options) => {
1728
- await loginCommand(options);
1729
- });
1730
- program2.command("logout").description("Log out of Context7").action(() => {
1731
- logoutCommand();
1732
- });
1733
- program2.command("whoami").description("Show current login status").action(async () => {
1734
- await whoamiCommand();
1735
- });
1736
- }
1737
- async function loginCommand(options) {
1738
- trackEvent("command", { name: "login" });
1739
- const existingTokens = loadTokens();
1740
- if (existingTokens) {
1741
- const expired = isTokenExpired(existingTokens);
1742
- if (!expired || existingTokens.refresh_token) {
1743
- console.log(pc7.yellow("You are already logged in."));
1744
- console.log(
1745
- pc7.dim("Run 'ctx7 logout' first if you want to log in with a different account.")
1746
- );
1747
- return;
1748
- }
1749
- clearTokens();
2033
+ async function suggestCommand(options) {
2034
+ trackEvent("command", { name: "suggest" });
2035
+ log.blank();
2036
+ const scanSpinner = ora3("Scanning project dependencies...").start();
2037
+ const deps = await detectProjectDependencies(process.cwd());
2038
+ if (deps.length === 0) {
2039
+ scanSpinner.warn(pc7.yellow("No dependencies detected"));
2040
+ log.info(`Try ${pc7.cyan("ctx7 skills search <keyword>")} to search manually`);
2041
+ return;
1750
2042
  }
1751
- const spinner = ora3("Preparing login...").start();
2043
+ scanSpinner.succeed(`Found ${deps.length} dependencies`);
2044
+ const searchSpinner = ora3("Finding matching skills...").start();
2045
+ const tokens = loadTokens();
2046
+ const accessToken = tokens && !isTokenExpired(tokens) ? tokens.access_token : void 0;
2047
+ let data;
1752
2048
  try {
1753
- const { codeVerifier, codeChallenge } = generatePKCE();
1754
- const state = generateState();
1755
- const callbackServer = createCallbackServer(state);
1756
- const port = await callbackServer.port;
1757
- const redirectUri = `http://localhost:${port}/callback`;
1758
- const authUrl = buildAuthorizationUrl(
1759
- baseUrl2,
1760
- CLI_CLIENT_ID,
1761
- redirectUri,
1762
- codeChallenge,
1763
- state
1764
- );
1765
- spinner.stop();
1766
- console.log("");
1767
- console.log(pc7.bold("Opening browser to log in..."));
1768
- console.log("");
1769
- if (options.browser) {
1770
- await open(authUrl);
1771
- console.log(pc7.dim("If the browser didn't open, visit this URL:"));
1772
- } else {
1773
- console.log(pc7.dim("Open this URL in your browser:"));
1774
- }
1775
- console.log(pc7.cyan(authUrl));
1776
- console.log("");
1777
- const waitingSpinner = ora3("Waiting for login...").start();
1778
- try {
1779
- const { code } = await callbackServer.result;
1780
- waitingSpinner.text = "Exchanging code for tokens...";
1781
- const tokens = await exchangeCodeForTokens(
1782
- baseUrl2,
1783
- code,
1784
- codeVerifier,
1785
- redirectUri,
1786
- CLI_CLIENT_ID
1787
- );
1788
- saveTokens(tokens);
1789
- callbackServer.close();
1790
- waitingSpinner.succeed(pc7.green("Login successful!"));
1791
- console.log("");
1792
- console.log(pc7.dim("You can now use authenticated Context7 features."));
1793
- } catch (error) {
1794
- callbackServer.close();
1795
- waitingSpinner.fail(pc7.red("Login failed"));
1796
- if (error instanceof Error) {
1797
- console.error(pc7.red(error.message));
1798
- }
1799
- process.exit(1);
1800
- }
1801
- } catch (error) {
1802
- spinner.fail(pc7.red("Login failed"));
1803
- if (error instanceof Error) {
1804
- console.error(pc7.red(error.message));
1805
- }
1806
- process.exit(1);
2049
+ data = await suggestSkills(deps, accessToken);
2050
+ } catch {
2051
+ searchSpinner.fail(pc7.red("Failed to connect to Context7"));
2052
+ return;
1807
2053
  }
1808
- }
1809
- function logoutCommand() {
1810
- trackEvent("command", { name: "logout" });
1811
- if (clearTokens()) {
1812
- console.log(pc7.green("Logged out successfully."));
1813
- } else {
1814
- console.log(pc7.yellow("You are not logged in."));
2054
+ if (data.error) {
2055
+ searchSpinner.fail(pc7.red(`Error: ${data.message || data.error}`));
2056
+ return;
1815
2057
  }
1816
- }
1817
- async function whoamiCommand() {
1818
- trackEvent("command", { name: "whoami" });
1819
- const tokens = loadTokens();
1820
- if (!tokens) {
1821
- console.log(pc7.yellow("Not logged in."));
1822
- console.log(pc7.dim("Run 'ctx7 login' to authenticate."));
2058
+ const skills = data.skills;
2059
+ if (skills.length === 0) {
2060
+ searchSpinner.warn(pc7.yellow("No matching skills found for your dependencies"));
1823
2061
  return;
1824
2062
  }
1825
- console.log(pc7.green("Logged in"));
2063
+ searchSpinner.succeed(`Found ${skills.length} relevant skill(s)`);
2064
+ trackEvent("suggest_results", { depCount: deps.length, skillCount: skills.length });
2065
+ log.blank();
2066
+ const maxNameLen = Math.max(...skills.map((s) => s.name.length));
2067
+ const installsColWidth = 10;
2068
+ const trustColWidth = 12;
2069
+ const maxMatchedLen = Math.max(...skills.map((s) => s.matchedDep.length));
2070
+ const indexWidth = skills.length.toString().length;
2071
+ const choices = skills.map((s, index) => {
2072
+ const indexStr = pc7.dim(`${(index + 1).toString().padStart(indexWidth)}.`);
2073
+ const paddedName = s.name.padEnd(maxNameLen);
2074
+ const installsRaw = s.installCount ? String(s.installCount) : "-";
2075
+ const paddedInstalls = formatInstallCount(s.installCount, pc7.dim("-")) + " ".repeat(installsColWidth - installsRaw.length);
2076
+ const trustRaw = s.trustScore !== void 0 && s.trustScore >= 0 ? s.trustScore.toFixed(1) : "-";
2077
+ const trust = formatTrustScore(s.trustScore) + " ".repeat(trustColWidth - trustRaw.length);
2078
+ const matched = pc7.yellow(s.matchedDep.padEnd(maxMatchedLen));
2079
+ const skillLink = terminalLink(
2080
+ s.name,
2081
+ `https://context7.com/skills${s.project}/${s.name}`,
2082
+ pc7.white
2083
+ );
2084
+ const repoLink = terminalLink(s.project, `https://github.com${s.project}`, pc7.white);
2085
+ const metadataLines = [
2086
+ pc7.dim("\u2500".repeat(50)),
2087
+ "",
2088
+ `${pc7.yellow("Skill:")} ${skillLink}`,
2089
+ `${pc7.yellow("Repo:")} ${repoLink}`,
2090
+ `${pc7.yellow("Relevant:")} ${pc7.white(s.matchedDep)}`,
2091
+ `${pc7.yellow("Description:")}`,
2092
+ pc7.white(s.description || "No description")
2093
+ ];
2094
+ return {
2095
+ name: `${indexStr} ${paddedName} ${paddedInstalls}${trust}${matched}`,
2096
+ value: s,
2097
+ description: metadataLines.join("\n")
2098
+ };
2099
+ });
2100
+ const checkboxPrefixWidth = 3;
2101
+ const headerPad = " ".repeat(checkboxPrefixWidth + indexWidth + 1 + 1 + maxNameLen + 1);
2102
+ const headerLine = headerPad + pc7.dim("Installs".padEnd(installsColWidth)) + pc7.dim("Trust(0-10)".padEnd(trustColWidth)) + pc7.dim("Relevant");
2103
+ const message = "Select skills to install:\n" + headerLine;
2104
+ let selectedSkills;
1826
2105
  try {
1827
- const userInfo = await fetchUserInfo(tokens.access_token);
1828
- if (userInfo.name) {
1829
- console.log(`${pc7.dim("Name:".padEnd(9))}${userInfo.name}`);
1830
- }
1831
- if (userInfo.email) {
1832
- console.log(`${pc7.dim("Email:".padEnd(9))}${userInfo.email}`);
1833
- }
2106
+ selectedSkills = await checkboxWithHover({
2107
+ message,
2108
+ choices,
2109
+ pageSize: 15,
2110
+ loop: false
2111
+ });
1834
2112
  } catch {
1835
- if (isTokenExpired(tokens) && !tokens.refresh_token) {
1836
- console.log(pc7.dim("(Session may be expired - run 'ctx7 login' to refresh)"));
2113
+ log.warn("Installation cancelled");
2114
+ return;
2115
+ }
2116
+ if (selectedSkills.length === 0) {
2117
+ log.warn("No skills selected");
2118
+ return;
2119
+ }
2120
+ const targets = await promptForInstallTargets(options);
2121
+ if (!targets) {
2122
+ log.warn("Installation cancelled");
2123
+ return;
2124
+ }
2125
+ const targetDirs = getTargetDirs(targets);
2126
+ const installSpinner = ora3("Installing skills...").start();
2127
+ let permissionError = false;
2128
+ const failedDirs = /* @__PURE__ */ new Set();
2129
+ const installedSkills = [];
2130
+ for (const skill of selectedSkills) {
2131
+ try {
2132
+ installSpinner.text = `Downloading ${skill.name}...`;
2133
+ const downloadData = await downloadSkill(skill.project, skill.name);
2134
+ if (downloadData.error) {
2135
+ log.warn(`Failed to download ${skill.name}: ${downloadData.error}`);
2136
+ continue;
2137
+ }
2138
+ installSpinner.text = `Installing ${skill.name}...`;
2139
+ const [primaryDir, ...symlinkDirs] = targetDirs;
2140
+ try {
2141
+ await installSkillFiles(skill.name, downloadData.files, primaryDir);
2142
+ } catch (dirErr) {
2143
+ const error = dirErr;
2144
+ if (error.code === "EACCES" || error.code === "EPERM") {
2145
+ permissionError = true;
2146
+ failedDirs.add(primaryDir);
2147
+ }
2148
+ throw dirErr;
2149
+ }
2150
+ const primarySkillDir = join6(primaryDir, skill.name);
2151
+ for (const targetDir of symlinkDirs) {
2152
+ try {
2153
+ await symlinkSkill(skill.name, primarySkillDir, targetDir);
2154
+ } catch (dirErr) {
2155
+ const error = dirErr;
2156
+ if (error.code === "EACCES" || error.code === "EPERM") {
2157
+ permissionError = true;
2158
+ failedDirs.add(targetDir);
2159
+ }
2160
+ throw dirErr;
2161
+ }
2162
+ }
2163
+ installedSkills.push(`${skill.project}/${skill.name}`);
2164
+ } catch (err) {
2165
+ const error = err;
2166
+ if (error.code === "EACCES" || error.code === "EPERM") {
2167
+ continue;
2168
+ }
2169
+ const errMsg = err instanceof Error ? err.message : String(err);
2170
+ log.warn(`Failed to install ${skill.name}: ${errMsg}`);
1837
2171
  }
1838
2172
  }
1839
- }
1840
- async function fetchUserInfo(accessToken) {
1841
- const response = await fetch("https://clerk.context7.com/oauth/userinfo", {
1842
- headers: {
1843
- Authorization: `Bearer ${accessToken}`
2173
+ if (permissionError) {
2174
+ installSpinner.fail("Permission denied");
2175
+ log.blank();
2176
+ log.warn("Fix permissions with:");
2177
+ for (const dir of failedDirs) {
2178
+ const parentDir = join6(dir, "..");
2179
+ log.dim(` sudo chown -R $(whoami) "${parentDir}"`);
1844
2180
  }
1845
- });
1846
- if (!response.ok) {
1847
- throw new Error("Failed to fetch user info");
2181
+ log.blank();
2182
+ return;
1848
2183
  }
1849
- return await response.json();
2184
+ installSpinner.succeed(`Installed ${installedSkills.length} skill(s)`);
2185
+ trackEvent("suggest_install", { skills: installedSkills, ides: targets.ides });
2186
+ const installedNames = selectedSkills.map((s) => s.name);
2187
+ logInstallSummary(targets, targetDirs, installedNames);
1850
2188
  }
1851
2189
 
1852
2190
  // src/constants.ts
1853
2191
  import { readFileSync as readFileSync2 } from "fs";
1854
2192
  import { fileURLToPath } from "url";
1855
- import { dirname as dirname2, join as join6 } from "path";
2193
+ import { dirname as dirname2, join as join7 } from "path";
1856
2194
  var __dirname = dirname2(fileURLToPath(import.meta.url));
1857
- var pkg = JSON.parse(readFileSync2(join6(__dirname, "../package.json"), "utf-8"));
2195
+ var pkg = JSON.parse(readFileSync2(join7(__dirname, "../package.json"), "utf-8"));
1858
2196
  var VERSION = pkg.version;
1859
2197
  var NAME = pkg.name;
1860
2198