ctx7 0.2.0 → 0.2.2

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,8 +6,8 @@ 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
12
  import { join as join5 } from "path";
13
13
 
@@ -117,6 +117,9 @@ async function downloadSkillFromGitHub(skill) {
117
117
 
118
118
  // src/utils/api.ts
119
119
  var baseUrl = "https://context7.com";
120
+ function getBaseUrl() {
121
+ return baseUrl;
122
+ }
120
123
  function setBaseUrl(url) {
121
124
  baseUrl = url;
122
125
  }
@@ -135,15 +138,6 @@ async function searchSkills(query) {
135
138
  const response = await fetch(`${baseUrl}/api/v2/skills?${params}`);
136
139
  return await response.json();
137
140
  }
138
- function trackInstalls(skills, ides) {
139
- if (process.env.CTX7_TELEMETRY_DISABLED || !skills.length) return;
140
- fetch(`${baseUrl}/api/v2/skills/track`, {
141
- method: "POST",
142
- headers: { "Content-Type": "application/json" },
143
- body: JSON.stringify({ skills, ides })
144
- }).catch(() => {
145
- });
146
- }
147
141
  async function downloadSkill(project, skillName) {
148
142
  const skillData = await getSkill(project, skillName);
149
143
  if (skillData.error) {
@@ -527,23 +521,16 @@ import { checkbox as checkbox2 } from "@inquirer/prompts";
527
521
  import readline from "readline";
528
522
  function terminalLink(text, url, color) {
529
523
  const colorFn = color ?? ((s) => s);
530
- return `\x1B]8;;${url}\x07${colorFn(text)}\x1B]8;;\x07`;
524
+ return `\x1B]8;;${url}\x07${colorFn(text)}\x1B]8;;\x07 ${pc3.white("\u2197")}`;
531
525
  }
532
- function formatInstallCount(count) {
533
- if (count === void 0 || count === 0) return "";
534
- let display;
535
- if (count >= 1e3) {
536
- display = `${Math.floor(count / 1e3)}k+`;
537
- } else if (count >= 100) {
538
- const hundreds = Math.floor(count / 100) * 100;
539
- display = `${hundreds}+`;
540
- } else if (count >= 10) {
541
- const tens = Math.floor(count / 10) * 10;
542
- display = `${tens}+`;
543
- } else {
544
- display = String(count);
545
- }
546
- return `\x1B[38;5;214m\u2193${display}\x1B[0m`;
526
+ function formatInstallCount(count, placeholder = "") {
527
+ if (count === void 0 || count === 0) return placeholder;
528
+ return pc3.yellow(String(count));
529
+ }
530
+ function formatTrustScore(score) {
531
+ if (score === void 0 || score < 0) return pc3.dim("-");
532
+ if (score < 3) return pc3.red(score.toFixed(1));
533
+ return pc3.yellow(score.toFixed(1));
547
534
  }
548
535
  async function checkboxWithHover(config, options) {
549
536
  const choices = config.choices.filter(
@@ -614,12 +601,24 @@ async function symlinkSkill(skillName, sourcePath, targetDir) {
614
601
  await symlink(sourcePath, targetPath);
615
602
  }
616
603
 
604
+ // src/utils/tracking.ts
605
+ function trackEvent(event, data) {
606
+ if (process.env.CTX7_TELEMETRY_DISABLED) return;
607
+ fetch(`${getBaseUrl()}/api/v2/cli/events`, {
608
+ method: "POST",
609
+ headers: { "Content-Type": "application/json" },
610
+ body: JSON.stringify({ event, data })
611
+ }).catch(() => {
612
+ });
613
+ }
614
+
617
615
  // src/commands/generate.ts
618
- import pc5 from "picocolors";
619
- import ora from "ora";
620
- import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
616
+ import pc6 from "picocolors";
617
+ import ora2 from "ora";
618
+ import { mkdir as mkdir2, writeFile as writeFile2, readFile, unlink } from "fs/promises";
621
619
  import { join as join4 } from "path";
622
620
  import { homedir as homedir3 } from "os";
621
+ import { spawn } from "child_process";
623
622
  import { input, select as select2 } from "@inquirer/prompts";
624
623
 
625
624
  // src/utils/auth.ts
@@ -814,6 +813,148 @@ function buildAuthorizationUrl(baseUrl3, clientId, redirectUri, codeChallenge, s
814
813
  return url.toString();
815
814
  }
816
815
 
816
+ // src/commands/auth.ts
817
+ import pc4 from "picocolors";
818
+ import ora from "ora";
819
+ import open from "open";
820
+ var CLI_CLIENT_ID = "2veBSofhicRBguUT";
821
+ var baseUrl2 = "https://context7.com";
822
+ function setAuthBaseUrl(url) {
823
+ baseUrl2 = url;
824
+ }
825
+ function registerAuthCommands(program2) {
826
+ program2.command("login").description("Log in to Context7").option("--no-browser", "Don't open browser automatically").action(async (options) => {
827
+ await loginCommand(options);
828
+ });
829
+ program2.command("logout").description("Log out of Context7").action(() => {
830
+ logoutCommand();
831
+ });
832
+ program2.command("whoami").description("Show current login status").action(async () => {
833
+ await whoamiCommand();
834
+ });
835
+ }
836
+ async function performLogin(openBrowser = true) {
837
+ const spinner = ora("Preparing login...").start();
838
+ try {
839
+ const { codeVerifier, codeChallenge } = generatePKCE();
840
+ const state = generateState();
841
+ const callbackServer = createCallbackServer(state);
842
+ const port = await callbackServer.port;
843
+ const redirectUri = `http://localhost:${port}/callback`;
844
+ const authUrl = buildAuthorizationUrl(
845
+ baseUrl2,
846
+ CLI_CLIENT_ID,
847
+ redirectUri,
848
+ codeChallenge,
849
+ state
850
+ );
851
+ spinner.stop();
852
+ console.log("");
853
+ console.log(pc4.bold("Opening browser to log in..."));
854
+ console.log("");
855
+ if (openBrowser) {
856
+ await open(authUrl);
857
+ console.log(pc4.dim("If the browser didn't open, visit this URL:"));
858
+ } else {
859
+ console.log(pc4.dim("Open this URL in your browser:"));
860
+ }
861
+ console.log(pc4.cyan(authUrl));
862
+ console.log("");
863
+ const waitingSpinner = ora("Waiting for login...").start();
864
+ try {
865
+ const { code } = await callbackServer.result;
866
+ waitingSpinner.text = "Exchanging code for tokens...";
867
+ const tokens = await exchangeCodeForTokens(
868
+ baseUrl2,
869
+ code,
870
+ codeVerifier,
871
+ redirectUri,
872
+ CLI_CLIENT_ID
873
+ );
874
+ saveTokens(tokens);
875
+ callbackServer.close();
876
+ waitingSpinner.succeed(pc4.green("Login successful!"));
877
+ return tokens.access_token;
878
+ } catch (error) {
879
+ callbackServer.close();
880
+ waitingSpinner.fail(pc4.red("Login failed"));
881
+ if (error instanceof Error) {
882
+ console.error(pc4.red(error.message));
883
+ }
884
+ return null;
885
+ }
886
+ } catch (error) {
887
+ spinner.fail(pc4.red("Login failed"));
888
+ if (error instanceof Error) {
889
+ console.error(pc4.red(error.message));
890
+ }
891
+ return null;
892
+ }
893
+ }
894
+ async function loginCommand(options) {
895
+ trackEvent("command", { name: "login" });
896
+ const existingTokens = loadTokens();
897
+ if (existingTokens) {
898
+ const expired = isTokenExpired(existingTokens);
899
+ if (!expired || existingTokens.refresh_token) {
900
+ console.log(pc4.yellow("You are already logged in."));
901
+ console.log(
902
+ pc4.dim("Run 'ctx7 logout' first if you want to log in with a different account.")
903
+ );
904
+ return;
905
+ }
906
+ clearTokens();
907
+ }
908
+ const token = await performLogin(options.browser);
909
+ if (!token) {
910
+ process.exit(1);
911
+ }
912
+ console.log("");
913
+ console.log(pc4.dim("You can now use authenticated Context7 features."));
914
+ }
915
+ function logoutCommand() {
916
+ trackEvent("command", { name: "logout" });
917
+ if (clearTokens()) {
918
+ console.log(pc4.green("Logged out successfully."));
919
+ } else {
920
+ console.log(pc4.yellow("You are not logged in."));
921
+ }
922
+ }
923
+ async function whoamiCommand() {
924
+ trackEvent("command", { name: "whoami" });
925
+ const tokens = loadTokens();
926
+ if (!tokens) {
927
+ console.log(pc4.yellow("Not logged in."));
928
+ console.log(pc4.dim("Run 'ctx7 login' to authenticate."));
929
+ return;
930
+ }
931
+ console.log(pc4.green("Logged in"));
932
+ try {
933
+ const userInfo = await fetchUserInfo(tokens.access_token);
934
+ if (userInfo.name) {
935
+ console.log(`${pc4.dim("Name:".padEnd(9))}${userInfo.name}`);
936
+ }
937
+ if (userInfo.email) {
938
+ console.log(`${pc4.dim("Email:".padEnd(9))}${userInfo.email}`);
939
+ }
940
+ } catch {
941
+ if (isTokenExpired(tokens) && !tokens.refresh_token) {
942
+ console.log(pc4.dim("(Session may be expired - run 'ctx7 login' to refresh)"));
943
+ }
944
+ }
945
+ }
946
+ async function fetchUserInfo(accessToken) {
947
+ const response = await fetch("https://clerk.context7.com/oauth/userinfo", {
948
+ headers: {
949
+ Authorization: `Bearer ${accessToken}`
950
+ }
951
+ });
952
+ if (!response.ok) {
953
+ throw new Error("Failed to fetch user info");
954
+ }
955
+ return await response.json();
956
+ }
957
+
817
958
  // src/utils/selectOrInput.ts
818
959
  import {
819
960
  createPrompt,
@@ -824,10 +965,19 @@ import {
824
965
  isUpKey,
825
966
  isDownKey
826
967
  } from "@inquirer/core";
827
- import pc4 from "picocolors";
968
+ import pc5 from "picocolors";
969
+ function reorderOptions(options, recommendedIndex) {
970
+ if (recommendedIndex === 0) return options;
971
+ const reordered = [options[recommendedIndex]];
972
+ for (let i = 0; i < options.length; i++) {
973
+ if (i !== recommendedIndex) reordered.push(options[i]);
974
+ }
975
+ return reordered;
976
+ }
828
977
  var selectOrInput = createPrompt((config, done) => {
829
- const { message, options, recommendedIndex = 0 } = config;
830
- const [cursor, setCursor] = useState(recommendedIndex);
978
+ const { message, options: rawOptions, recommendedIndex = 0 } = config;
979
+ const options = reorderOptions(rawOptions, recommendedIndex);
980
+ const [cursor, setCursor] = useState(0);
831
981
  const [inputValue, setInputValue] = useState("");
832
982
  const prefix = usePrefix({});
833
983
  useKeypress((key, rl) => {
@@ -842,7 +992,7 @@ var selectOrInput = createPrompt((config, done) => {
842
992
  if (isEnterKey(key)) {
843
993
  if (cursor === options.length) {
844
994
  const finalValue = inputValue.trim();
845
- done(finalValue || options[recommendedIndex]);
995
+ done(finalValue || options[0]);
846
996
  } else {
847
997
  done(options[cursor]);
848
998
  }
@@ -872,16 +1022,16 @@ var selectOrInput = createPrompt((config, done) => {
872
1022
  rl.line = "";
873
1023
  }
874
1024
  });
875
- let output = `${prefix} ${pc4.bold(message)}
1025
+ let output = `${prefix} ${pc5.bold(message)}
876
1026
 
877
1027
  `;
878
1028
  options.forEach((opt, idx) => {
879
- const isRecommended = idx === recommendedIndex;
1029
+ const isRecommended = idx === 0;
880
1030
  const isCursor = idx === cursor;
881
- const number = pc4.cyan(`${idx + 1}.`);
882
- const text = isRecommended ? `${opt} ${pc4.green("\u2713 Recommended")}` : opt;
1031
+ const number = pc5.cyan(`${idx + 1}.`);
1032
+ const text = isRecommended ? `${opt} ${pc5.green("\u2713 Recommended")}` : opt;
883
1033
  if (isCursor) {
884
- output += pc4.cyan(`\u276F ${number} ${text}
1034
+ output += pc5.cyan(`\u276F ${number} ${text}
885
1035
  `);
886
1036
  } else {
887
1037
  output += ` ${number} ${text}
@@ -890,9 +1040,9 @@ var selectOrInput = createPrompt((config, done) => {
890
1040
  });
891
1041
  const isCustomCursor = cursor === options.length;
892
1042
  if (isCustomCursor) {
893
- output += pc4.cyan(`\u276F ${pc4.yellow("\u270E")} ${inputValue || pc4.dim("Type your own...")}`);
1043
+ output += pc5.cyan(`\u276F ${pc5.yellow("\u270E")} ${inputValue || pc5.dim("Type your own...")}`);
894
1044
  } else {
895
- output += ` ${pc4.yellow("\u270E")} ${pc4.dim("Type your own...")}`;
1045
+ output += ` ${pc5.yellow("\u270E")} ${pc5.dim("Type your own...")}`;
896
1046
  }
897
1047
  return output;
898
1048
  });
@@ -905,53 +1055,66 @@ function registerGenerateCommand(skillCommand) {
905
1055
  });
906
1056
  }
907
1057
  async function generateCommand(options) {
1058
+ trackEvent("command", { name: "generate" });
908
1059
  log.blank();
1060
+ let accessToken = null;
909
1061
  const tokens = loadTokens();
910
- if (!tokens) {
911
- log.error("Authentication required. Please run 'ctx7 login' first.");
912
- return;
913
- }
914
- if (isTokenExpired(tokens)) {
915
- log.error("Session expired. Please run 'ctx7 login' to refresh.");
916
- return;
1062
+ if (tokens && !isTokenExpired(tokens)) {
1063
+ accessToken = tokens.access_token;
1064
+ } else {
1065
+ log.info("Authentication required. Logging in...");
1066
+ log.blank();
1067
+ accessToken = await performLogin();
1068
+ if (!accessToken) {
1069
+ log.error("Login failed. Please try again.");
1070
+ return;
1071
+ }
1072
+ log.blank();
917
1073
  }
918
- const accessToken = tokens.access_token;
919
- const initSpinner = ora().start();
1074
+ const initSpinner = ora2().start();
920
1075
  const quota = await getSkillQuota(accessToken);
921
1076
  if (quota.error) {
922
- initSpinner.fail(pc5.red("Failed to initialize"));
1077
+ initSpinner.fail(pc6.red("Failed to initialize"));
923
1078
  return;
924
1079
  }
925
1080
  if (quota.tier !== "unlimited" && quota.remaining < 1) {
926
- initSpinner.fail(pc5.red("Weekly skill generation limit reached"));
1081
+ initSpinner.fail(pc6.red("Weekly skill generation limit reached"));
927
1082
  log.blank();
928
1083
  console.log(
929
- ` You've used ${pc5.bold(pc5.white(quota.used.toString()))}/${pc5.bold(pc5.white(quota.limit.toString()))} skill generations this week.`
1084
+ ` You've used ${pc6.bold(pc6.white(quota.used.toString()))}/${pc6.bold(pc6.white(quota.limit.toString()))} skill generations this week.`
930
1085
  );
931
1086
  console.log(
932
- ` Your quota resets on ${pc5.yellow(new Date(quota.resetDate).toLocaleDateString())}.`
1087
+ ` Your quota resets on ${pc6.yellow(new Date(quota.resetDate).toLocaleDateString())}.`
933
1088
  );
934
1089
  log.blank();
935
1090
  if (quota.tier === "free") {
936
1091
  console.log(
937
- ` ${pc5.yellow("Tip:")} Upgrade to Pro for ${pc5.bold("10")} generations per week.`
1092
+ ` ${pc6.yellow("Tip:")} Upgrade to Pro for ${pc6.bold("10")} generations per week.`
938
1093
  );
939
- console.log(` Visit ${pc5.green("https://context7.com/dashboard")} to upgrade.`);
1094
+ console.log(` Visit ${pc6.green("https://context7.com/dashboard")} to upgrade.`);
940
1095
  }
941
1096
  return;
942
1097
  }
943
1098
  initSpinner.stop();
944
1099
  initSpinner.clear();
945
- console.log(pc5.bold("What should your agent become an expert at?\n"));
1100
+ console.log(pc6.bold("What should your agent become an expert at?\n"));
946
1101
  console.log(
947
- pc5.dim("Skills teach agents best practices, design patterns, and domain expertise.\n")
1102
+ pc6.dim(
1103
+ "Skills should encode best practices, constraints, and decision-making \u2014\nnot step-by-step tutorials or one-off tasks.\n"
1104
+ )
948
1105
  );
949
- console.log(pc5.yellow("Examples:"));
950
- console.log(pc5.dim(' "React component optimization and performance best practices"'));
951
- console.log(pc5.dim(' "Responsive web design with Tailwind CSS"'));
952
- console.log(pc5.dim(' "Writing effective landing page copy"'));
953
- console.log(pc5.dim(' "Deploying Next.js apps to Vercel"'));
954
- console.log(pc5.dim(' "OAuth authentication with NextAuth.js"\n'));
1106
+ console.log(pc6.yellow("Examples:"));
1107
+ {
1108
+ console.log(pc6.red(' \u2715 "Deploy a Next.js app to Vercel"'));
1109
+ console.log(pc6.green(' \u2713 "Best practices and constraints for deploying Next.js apps to Vercel"'));
1110
+ log.blank();
1111
+ console.log(pc6.red(' \u2715 "Use Tailwind for responsive design"'));
1112
+ console.log(pc6.green(' \u2713 "Responsive layout decision-making with Tailwind CSS"'));
1113
+ log.blank();
1114
+ console.log(pc6.red(' \u2715 "Build OAuth with NextAuth"'));
1115
+ console.log(pc6.green(' \u2713 "OAuth authentication patterns and pitfalls with NextAuth.js"'));
1116
+ }
1117
+ log.blank();
955
1118
  let motivation;
956
1119
  try {
957
1120
  motivation = await input({
@@ -966,14 +1129,26 @@ async function generateCommand(options) {
966
1129
  log.warn("Generation cancelled");
967
1130
  return;
968
1131
  }
969
- const searchSpinner = ora("Finding relevant libraries...").start();
1132
+ log.blank();
1133
+ console.log(
1134
+ pc6.dim(
1135
+ "To generate this skill, we will read relevant documentation and examples\nfrom Context7.\n"
1136
+ )
1137
+ );
1138
+ console.log(
1139
+ pc6.dim(
1140
+ "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"
1141
+ )
1142
+ );
1143
+ console.log(pc6.dim("You can adjust which sources the skill is based on.\n"));
1144
+ const searchSpinner = ora2("Finding relevant sources...").start();
970
1145
  const searchResult = await searchLibraries(motivation, accessToken);
971
1146
  if (searchResult.error || !searchResult.results?.length) {
972
- searchSpinner.fail(pc5.red("No libraries found"));
1147
+ searchSpinner.fail(pc6.red("No sources found"));
973
1148
  log.warn(searchResult.message || "Try a different description");
974
1149
  return;
975
1150
  }
976
- searchSpinner.succeed(pc5.green(`Found ${searchResult.results.length} relevant libraries`));
1151
+ searchSpinner.succeed(pc6.green(`Found ${searchResult.results.length} relevant sources`));
977
1152
  log.blank();
978
1153
  let selectedLibraries;
979
1154
  try {
@@ -993,31 +1168,32 @@ async function generateCommand(options) {
993
1168
  const libraryChoices = libraries.map((lib, index) => {
994
1169
  const projectId = formatProjectId(lib.id);
995
1170
  const isGitHub = isGitHubRepo(lib.id);
996
- const indexStr = pc5.dim(`${(index + 1).toString().padStart(indexWidth)}.`);
1171
+ const indexStr = pc6.dim(`${(index + 1).toString().padStart(indexWidth)}.`);
997
1172
  const paddedName = lib.title.padEnd(maxNameLen);
998
1173
  const libUrl = `https://context7.com${lib.id}`;
999
- const libLink = terminalLink(lib.title, libUrl, pc5.white);
1000
- const repoLink = isGitHub ? terminalLink(projectId, `https://github.com/${projectId}`, pc5.white) : pc5.white(projectId);
1001
- const starsLine = lib.stars && isGitHub ? [`${pc5.yellow("Stars:")} ${lib.stars.toLocaleString()}`] : [];
1174
+ const libLink = terminalLink(lib.title, libUrl, pc6.white);
1175
+ const sourceUrl = isGitHub ? `https://github.com/${projectId}` : `https://context7.com${lib.id}`;
1176
+ const repoLink = terminalLink(projectId, sourceUrl, pc6.white);
1177
+ const starsLine = lib.stars && isGitHub ? [`${pc6.yellow("Stars:")} ${lib.stars.toLocaleString()}`] : [];
1002
1178
  const metadataLines = [
1003
- pc5.dim("\u2500".repeat(50)),
1179
+ pc6.dim("\u2500".repeat(50)),
1004
1180
  "",
1005
- `${pc5.yellow("Library:")} ${libLink}`,
1006
- `${pc5.yellow("Source:")} ${repoLink}`,
1007
- `${pc5.yellow("Snippets:")} ${lib.totalSnippets.toLocaleString()}`,
1181
+ `${pc6.yellow("Library:")} ${libLink}`,
1182
+ `${pc6.yellow("Source:")} ${repoLink}`,
1183
+ `${pc6.yellow("Snippets:")} ${lib.totalSnippets.toLocaleString()}`,
1008
1184
  ...starsLine,
1009
- `${pc5.yellow("Description:")}`,
1010
- pc5.white(lib.description || "No description")
1185
+ `${pc6.yellow("Description:")}`,
1186
+ pc6.white(lib.description || "No description")
1011
1187
  ];
1012
1188
  return {
1013
- name: `${indexStr} ${paddedName} ${pc5.dim(`(${projectId})`)}`,
1189
+ name: `${indexStr} ${paddedName} ${pc6.dim(`(${projectId})`)}`,
1014
1190
  value: lib,
1015
1191
  description: metadataLines.join("\n")
1016
1192
  };
1017
1193
  });
1018
1194
  selectedLibraries = await checkboxWithHover(
1019
1195
  {
1020
- message: "Select libraries:",
1196
+ message: "Select sources:",
1021
1197
  choices: libraryChoices,
1022
1198
  pageSize: 10,
1023
1199
  loop: false
@@ -1025,7 +1201,7 @@ async function generateCommand(options) {
1025
1201
  { getName: (lib) => `${lib.title} (${formatProjectId(lib.id)})` }
1026
1202
  );
1027
1203
  if (!selectedLibraries || selectedLibraries.length === 0) {
1028
- log.info("No libraries selected. Try running the command again.");
1204
+ log.info("No sources selected. Try running the command again.");
1029
1205
  return;
1030
1206
  }
1031
1207
  } catch {
@@ -1033,15 +1209,17 @@ async function generateCommand(options) {
1033
1209
  return;
1034
1210
  }
1035
1211
  log.blank();
1036
- const questionsSpinner = ora("Preparing questions...").start();
1212
+ const questionsSpinner = ora2(
1213
+ "Preparing follow-up questions to clarify scope and constraints..."
1214
+ ).start();
1037
1215
  const librariesInput = selectedLibraries.map((lib) => ({ id: lib.id, name: lib.title }));
1038
1216
  const questionsResult = await getSkillQuestions(librariesInput, motivation, accessToken);
1039
1217
  if (questionsResult.error || !questionsResult.questions?.length) {
1040
- questionsSpinner.fail(pc5.red("Failed to generate questions"));
1218
+ questionsSpinner.fail(pc6.red("Failed to generate questions"));
1041
1219
  log.warn(questionsResult.message || "Please try again");
1042
1220
  return;
1043
1221
  }
1044
- questionsSpinner.succeed(pc5.green("Questions prepared"));
1222
+ questionsSpinner.succeed(pc6.green("Questions prepared"));
1045
1223
  log.blank();
1046
1224
  const answers = [];
1047
1225
  try {
@@ -1050,7 +1228,7 @@ async function generateCommand(options) {
1050
1228
  const questionNum = i + 1;
1051
1229
  const totalQuestions = questionsResult.questions.length;
1052
1230
  const answer = await selectOrInput_default({
1053
- message: `${pc5.dim(`[${questionNum}/${totalQuestions}]`)} ${q.question}`,
1231
+ message: `${pc6.dim(`[${questionNum}/${totalQuestions}]`)} ${q.question}`,
1054
1232
  options: q.options,
1055
1233
  recommendedIndex: q.recommendedIndex
1056
1234
  });
@@ -1061,8 +1239,8 @@ async function generateCommand(options) {
1061
1239
  const linesToClear = 3 + q.options.length;
1062
1240
  process.stdout.write(`\x1B[${linesToClear}A\x1B[J`);
1063
1241
  const truncatedAnswer = answer.length > 50 ? answer.slice(0, 47) + "..." : answer;
1064
- console.log(`${pc5.green("\u2713")} ${pc5.dim(`[${questionNum}/${totalQuestions}]`)} ${q.question}`);
1065
- console.log(` ${pc5.cyan(truncatedAnswer)}`);
1242
+ console.log(`${pc6.green("\u2713")} ${pc6.dim(`[${questionNum}/${totalQuestions}]`)} ${q.question}`);
1243
+ console.log(` ${pc6.cyan(truncatedAnswer)}`);
1066
1244
  log.blank();
1067
1245
  }
1068
1246
  } catch {
@@ -1072,33 +1250,39 @@ async function generateCommand(options) {
1072
1250
  let generatedContent = null;
1073
1251
  let skillName = "";
1074
1252
  let feedback;
1075
- const libraryNames = selectedLibraries.map((lib) => lib.title).join(", ");
1253
+ let previewFile = null;
1254
+ let previewFileWritten = false;
1255
+ const cleanupPreviewFile = async () => {
1256
+ if (previewFile) {
1257
+ await unlink(previewFile).catch(() => {
1258
+ });
1259
+ }
1260
+ };
1076
1261
  const queryLog = [];
1077
1262
  let genSpinner = null;
1078
1263
  const formatQueryLogText = () => {
1079
1264
  if (queryLog.length === 0) return "";
1080
1265
  const lines = [];
1081
1266
  const latestEntry = queryLog[queryLog.length - 1];
1082
- lines.push(pc5.dim(`(${queryLog.length} ${queryLog.length === 1 ? "query" : "queries"})`));
1083
1267
  lines.push("");
1084
1268
  for (const result of latestEntry.results.slice(0, 3)) {
1085
1269
  const cleanContent = result.content.replace(/Source:\s*https?:\/\/[^\s]+/gi, "").trim();
1086
1270
  if (cleanContent) {
1087
- lines.push(` ${pc5.yellow("\u2022")} ${pc5.white(result.title)}`);
1271
+ lines.push(` ${pc6.yellow("\u2022")} ${pc6.white(result.title)}`);
1088
1272
  const maxLen = 400;
1089
1273
  const content = cleanContent.length > maxLen ? cleanContent.slice(0, maxLen - 3) + "..." : cleanContent;
1090
1274
  const words = content.split(" ");
1091
1275
  let currentLine = " ";
1092
1276
  for (const word of words) {
1093
1277
  if (currentLine.length + word.length > 84) {
1094
- lines.push(pc5.dim(currentLine));
1278
+ lines.push(pc6.dim(currentLine));
1095
1279
  currentLine = " " + word + " ";
1096
1280
  } else {
1097
1281
  currentLine += word + " ";
1098
1282
  }
1099
1283
  }
1100
1284
  if (currentLine.trim()) {
1101
- lines.push(pc5.dim(currentLine));
1285
+ lines.push(pc6.dim(currentLine));
1102
1286
  }
1103
1287
  lines.push("");
1104
1288
  }
@@ -1106,19 +1290,20 @@ async function generateCommand(options) {
1106
1290
  return "\n" + lines.join("\n");
1107
1291
  };
1108
1292
  let isGeneratingContent = false;
1293
+ let initialStatus = "Reading selected Context7 sources to generate the skill...";
1109
1294
  const handleStreamEvent = (event) => {
1110
1295
  if (event.type === "progress") {
1111
1296
  if (genSpinner) {
1112
1297
  if (event.message.startsWith("Generating skill content...") && !isGeneratingContent) {
1113
1298
  isGeneratingContent = true;
1114
1299
  if (queryLog.length > 0) {
1115
- genSpinner.succeed(pc5.green(`Queried documentation`));
1300
+ genSpinner.succeed(pc6.green(`Read Context7 sources`));
1116
1301
  } else {
1117
- genSpinner.succeed(pc5.green(`Ready to generate`));
1302
+ genSpinner.succeed(pc6.green(`Ready to generate`));
1118
1303
  }
1119
- genSpinner = ora("Generating skill content...").start();
1304
+ genSpinner = ora2("Generating skill content...").start();
1120
1305
  } else if (!isGeneratingContent) {
1121
- genSpinner.text = event.message + formatQueryLogText();
1306
+ genSpinner.text = initialStatus + formatQueryLogText();
1122
1307
  }
1123
1308
  }
1124
1309
  } else if (event.type === "tool_result") {
@@ -1142,18 +1327,19 @@ async function generateCommand(options) {
1142
1327
  };
1143
1328
  queryLog.length = 0;
1144
1329
  isGeneratingContent = false;
1145
- const initialStatus = feedback ? "Regenerating skill with your feedback..." : `Generating skill for "${libraryNames}"...`;
1146
- genSpinner = ora(initialStatus).start();
1330
+ previewFileWritten = false;
1331
+ initialStatus = feedback ? "Regenerating skill with your feedback..." : "Reading selected Context7 sources to generate the skill...";
1332
+ genSpinner = ora2(initialStatus).start();
1147
1333
  const result = await generateSkillStructured(generateInput, handleStreamEvent, accessToken);
1148
1334
  if (result.error) {
1149
- genSpinner.fail(pc5.red(`Error: ${result.error}`));
1335
+ genSpinner.fail(pc6.red(`Error: ${result.error}`));
1150
1336
  return;
1151
1337
  }
1152
1338
  if (!result.content) {
1153
- genSpinner.fail(pc5.red("No content generated"));
1339
+ genSpinner.fail(pc6.red("No content generated"));
1154
1340
  return;
1155
1341
  }
1156
- genSpinner.succeed(pc5.green(`Generated skill for "${result.libraryName}"`));
1342
+ genSpinner.succeed(pc6.green(`Generated skill for "${result.libraryName}"`));
1157
1343
  generatedContent = result.content;
1158
1344
  skillName = result.libraryName.toLowerCase().replace(/[^a-z0-9-]/g, "-");
1159
1345
  const contentLines = generatedContent.split("\n");
@@ -1163,29 +1349,40 @@ async function generateCommand(options) {
1163
1349
  const remainingLines = contentLines.length - previewLineCount;
1164
1350
  const showPreview = () => {
1165
1351
  log.blank();
1166
- console.log(pc5.dim("\u2501".repeat(70)));
1167
- console.log(pc5.bold(`Generated Skill: `) + pc5.green(pc5.bold(skillName)));
1168
- console.log(pc5.dim("\u2501".repeat(70)));
1352
+ console.log(pc6.dim("\u2501".repeat(70)));
1353
+ console.log(pc6.bold(`Generated Skill: `) + pc6.green(pc6.bold(skillName)));
1354
+ console.log(pc6.dim("\u2501".repeat(70)));
1169
1355
  log.blank();
1170
1356
  console.log(previewContent);
1171
1357
  if (hasMoreLines) {
1172
1358
  log.blank();
1173
- console.log(pc5.dim(`... ${remainingLines} more lines`));
1359
+ console.log(pc6.dim(`... ${remainingLines} more lines`));
1174
1360
  }
1175
1361
  log.blank();
1176
- console.log(pc5.dim("\u2501".repeat(70)));
1362
+ console.log(pc6.dim("\u2501".repeat(70)));
1177
1363
  log.blank();
1178
1364
  };
1179
- const showFullContent = () => {
1180
- log.blank();
1181
- console.log(pc5.dim("\u2501".repeat(70)));
1182
- console.log(pc5.bold(`Generated Skill: `) + pc5.green(pc5.bold(skillName)));
1183
- console.log(pc5.dim("\u2501".repeat(70)));
1184
- log.blank();
1185
- console.log(generatedContent);
1186
- log.blank();
1187
- console.log(pc5.dim("\u2501".repeat(70)));
1188
- log.blank();
1365
+ const openInEditor = async () => {
1366
+ const previewDir = join4(homedir3(), ".context7", "previews");
1367
+ await mkdir2(previewDir, { recursive: true });
1368
+ previewFile = join4(previewDir, `${skillName}.md`);
1369
+ if (!previewFileWritten) {
1370
+ await writeFile2(previewFile, generatedContent, "utf-8");
1371
+ previewFileWritten = true;
1372
+ }
1373
+ const editor = process.env.EDITOR || "open";
1374
+ await new Promise((resolve) => {
1375
+ const child = spawn(editor, [previewFile], {
1376
+ stdio: "inherit",
1377
+ shell: true
1378
+ });
1379
+ child.on("close", () => resolve());
1380
+ });
1381
+ };
1382
+ const syncFromPreviewFile = async () => {
1383
+ if (previewFile) {
1384
+ generatedContent = await readFile(previewFile, "utf-8");
1385
+ }
1189
1386
  };
1190
1387
  showPreview();
1191
1388
  await new Promise((resolve) => setTimeout(resolve, 100));
@@ -1193,27 +1390,30 @@ async function generateCommand(options) {
1193
1390
  let action;
1194
1391
  while (true) {
1195
1392
  const choices = [
1196
- { name: `${pc5.green("\u2713")} Install skill`, value: "install" },
1197
- ...hasMoreLines ? [{ name: `${pc5.blue("\u2922")} View full skill`, value: "expand" }] : [],
1198
- { name: `${pc5.yellow("\u270E")} Request changes`, value: "feedback" },
1199
- { name: `${pc5.red("\u2715")} Cancel`, value: "cancel" }
1393
+ { name: `${pc6.green("\u2713")} Install skill (save locally)`, value: "install" },
1394
+ { name: `${pc6.blue("\u2922")} Edit skill in editor`, value: "view" },
1395
+ { name: `${pc6.yellow("\u270E")} Request changes`, value: "feedback" },
1396
+ { name: `${pc6.red("\u2715")} Cancel`, value: "cancel" }
1200
1397
  ];
1201
1398
  action = await select2({
1202
1399
  message: "What would you like to do?",
1203
1400
  choices
1204
1401
  });
1205
- if (action === "expand") {
1206
- showFullContent();
1402
+ if (action === "view") {
1403
+ await openInEditor();
1207
1404
  continue;
1208
1405
  }
1406
+ await syncFromPreviewFile();
1209
1407
  break;
1210
1408
  }
1211
1409
  if (action === "install") {
1212
1410
  break;
1213
1411
  } else if (action === "cancel") {
1412
+ await cleanupPreviewFile();
1214
1413
  log.warn("Generation cancelled");
1215
1414
  return;
1216
1415
  } else if (action === "feedback") {
1416
+ trackEvent("gen_feedback");
1217
1417
  feedback = await input({
1218
1418
  message: "What changes would you like? (press Enter to skip)"
1219
1419
  });
@@ -1223,6 +1423,7 @@ async function generateCommand(options) {
1223
1423
  log.blank();
1224
1424
  }
1225
1425
  } catch {
1426
+ await cleanupPreviewFile();
1226
1427
  log.warn("Generation cancelled");
1227
1428
  return;
1228
1429
  }
@@ -1233,7 +1434,7 @@ async function generateCommand(options) {
1233
1434
  return;
1234
1435
  }
1235
1436
  const targetDirs = getTargetDirs(targets);
1236
- const writeSpinner = ora("Writing skill files...").start();
1437
+ const writeSpinner = ora2("Writing skill files...").start();
1237
1438
  let permissionError = false;
1238
1439
  const failedDirs = /* @__PURE__ */ new Set();
1239
1440
  for (const targetDir of targetDirs) {
@@ -1257,23 +1458,25 @@ async function generateCommand(options) {
1257
1458
  }
1258
1459
  }
1259
1460
  if (permissionError) {
1260
- writeSpinner.fail(pc5.red("Permission denied"));
1461
+ writeSpinner.fail(pc6.red("Permission denied"));
1261
1462
  log.blank();
1262
- console.log(pc5.yellow("Fix permissions with:"));
1463
+ console.log(pc6.yellow("Fix permissions with:"));
1263
1464
  for (const dir of failedDirs) {
1264
1465
  const parentDir = join4(dir, "..");
1265
- console.log(pc5.dim(` sudo chown -R $(whoami) "${parentDir}"`));
1466
+ console.log(pc6.dim(` sudo chown -R $(whoami) "${parentDir}"`));
1266
1467
  }
1267
1468
  log.blank();
1268
1469
  return;
1269
1470
  }
1270
- writeSpinner.succeed(pc5.green(`Created skill in ${targetDirs.length} location(s)`));
1471
+ writeSpinner.succeed(pc6.green(`Created skill in ${targetDirs.length} location(s)`));
1472
+ trackEvent("gen_install");
1271
1473
  log.blank();
1272
- console.log(pc5.green(pc5.bold("Skill saved successfully")));
1474
+ console.log(pc6.green("Skill saved successfully"));
1273
1475
  for (const targetDir of targetDirs) {
1274
- console.log(pc5.dim(` ${targetDir}/`) + pc5.green(skillName));
1476
+ console.log(pc6.dim(` ${targetDir}/`) + pc6.green(skillName));
1275
1477
  }
1276
1478
  log.blank();
1479
+ await cleanupPreviewFile();
1277
1480
  }
1278
1481
 
1279
1482
  // src/commands/skill.ts
@@ -1320,6 +1523,7 @@ function registerSkillAliases(program2) {
1320
1523
  });
1321
1524
  }
1322
1525
  async function installCommand(input2, skillName, options) {
1526
+ trackEvent("command", { name: "install" });
1323
1527
  const parsed = parseSkillInput(input2);
1324
1528
  if (!parsed) {
1325
1529
  log.error(`Invalid input format: ${input2}`);
@@ -1330,17 +1534,17 @@ async function installCommand(input2, skillName, options) {
1330
1534
  }
1331
1535
  const repo = `/${parsed.owner}/${parsed.repo}`;
1332
1536
  log.blank();
1333
- const spinner = ora2(`Fetching skills from ${repo}...`).start();
1537
+ const spinner = ora3(`Fetching skills from ${repo}...`).start();
1334
1538
  let selectedSkills;
1335
1539
  if (skillName) {
1336
1540
  spinner.text = `Fetching skill: ${skillName}...`;
1337
1541
  const skillData = await getSkill(repo, skillName);
1338
1542
  if (skillData.error || !skillData.name) {
1339
1543
  if (skillData.error === "prompt_injection_detected") {
1340
- spinner.fail(pc6.red(`Prompt injection detected in skill: ${skillName}`));
1544
+ spinner.fail(pc7.red(`Prompt injection detected in skill: ${skillName}`));
1341
1545
  log.warn("This skill contains potentially malicious content and cannot be installed.");
1342
1546
  } else {
1343
- spinner.fail(pc6.red(`Skill not found: ${skillName}`));
1547
+ spinner.fail(pc7.red(`Skill not found: ${skillName}`));
1344
1548
  }
1345
1549
  return;
1346
1550
  }
@@ -1356,14 +1560,14 @@ async function installCommand(input2, skillName, options) {
1356
1560
  } else {
1357
1561
  const data = await listProjectSkills(repo);
1358
1562
  if (data.error) {
1359
- spinner.fail(pc6.red(`Error: ${data.message || data.error}`));
1563
+ spinner.fail(pc7.red(`Error: ${data.message || data.error}`));
1360
1564
  return;
1361
1565
  }
1362
1566
  if (!data.skills || data.skills.length === 0) {
1363
- spinner.warn(pc6.yellow(`No skills found in ${repo}`));
1567
+ spinner.warn(pc7.yellow(`No skills found in ${repo}`));
1364
1568
  return;
1365
1569
  }
1366
- const skillsWithRepo = data.skills.map((s) => ({ ...s, project: repo }));
1570
+ const skillsWithRepo = data.skills.map((s) => ({ ...s, project: repo })).sort((a, b) => (b.installCount ?? 0) - (a.installCount ?? 0));
1367
1571
  spinner.succeed(`Found ${data.skills.length} skill(s)`);
1368
1572
  if (data.blockedSkillsCount && data.blockedSkillsCount > 0) {
1369
1573
  log.blank();
@@ -1378,19 +1582,19 @@ async function installCommand(input2, skillName, options) {
1378
1582
  const indexWidth = data.skills.length.toString().length;
1379
1583
  const maxNameLen = Math.max(...data.skills.map((s) => s.name.length));
1380
1584
  const choices = skillsWithRepo.map((s, index) => {
1381
- const indexStr = pc6.dim(`${(index + 1).toString().padStart(indexWidth)}.`);
1585
+ const indexStr = pc7.dim(`${(index + 1).toString().padStart(indexWidth)}.`);
1382
1586
  const paddedName = s.name.padEnd(maxNameLen);
1383
1587
  const installs = formatInstallCount(s.installCount);
1384
1588
  const skillUrl = `https://context7.com/skills${s.project}/${s.name}`;
1385
- const skillLink = terminalLink(s.name, skillUrl, pc6.white);
1386
- const repoLink = terminalLink(s.project, `https://github.com${s.project}`, pc6.white);
1589
+ const skillLink = terminalLink(s.name, skillUrl, pc7.white);
1590
+ const repoLink = terminalLink(s.project, `https://github.com${s.project}`, pc7.white);
1387
1591
  const metadataLines = [
1388
- pc6.dim("\u2500".repeat(50)),
1592
+ pc7.dim("\u2500".repeat(50)),
1389
1593
  "",
1390
- `${pc6.yellow("Skill:")} ${skillLink}`,
1391
- `${pc6.yellow("Repo:")} ${repoLink}`,
1392
- `${pc6.yellow("Description:")}`,
1393
- pc6.white(s.description || "No description")
1594
+ `${pc7.yellow("Skill:")} ${skillLink}`,
1595
+ `${pc7.yellow("Repo:")} ${repoLink}`,
1596
+ `${pc7.yellow("Description:")}`,
1597
+ pc7.white(s.description || "No description")
1394
1598
  ];
1395
1599
  return {
1396
1600
  name: installs ? `${indexStr} ${paddedName} ${installs}` : `${indexStr} ${paddedName}`,
@@ -1399,9 +1603,11 @@ async function installCommand(input2, skillName, options) {
1399
1603
  };
1400
1604
  });
1401
1605
  log.blank();
1606
+ const installsOffset = 4 + indexWidth + 1 + 1 + maxNameLen + 1 - 3;
1607
+ const message = "Select skills:" + " ".repeat(Math.max(1, installsOffset - 14)) + pc7.dim("installs");
1402
1608
  try {
1403
1609
  selectedSkills = await checkboxWithHover({
1404
- message: "Select skills:",
1610
+ message,
1405
1611
  choices,
1406
1612
  pageSize: 15,
1407
1613
  loop: false
@@ -1422,7 +1628,7 @@ async function installCommand(input2, skillName, options) {
1422
1628
  return;
1423
1629
  }
1424
1630
  const targetDirs = getTargetDirs(targets);
1425
- const installSpinner = ora2("Installing skills...").start();
1631
+ const installSpinner = ora3("Installing skills...").start();
1426
1632
  let permissionError = false;
1427
1633
  const failedDirs = /* @__PURE__ */ new Set();
1428
1634
  const installedSkills = [];
@@ -1481,60 +1687,69 @@ async function installCommand(input2, skillName, options) {
1481
1687
  return;
1482
1688
  }
1483
1689
  installSpinner.succeed(`Installed ${installedSkills.length} skill(s)`);
1484
- trackInstalls(installedSkills, targets.ides);
1690
+ trackEvent("install", { skills: installedSkills, ides: targets.ides });
1485
1691
  const installedNames = selectedSkills.map((s) => s.name);
1486
1692
  logInstallSummary(targets, targetDirs, installedNames);
1487
1693
  }
1488
1694
  async function searchCommand(query) {
1695
+ trackEvent("command", { name: "search" });
1489
1696
  log.blank();
1490
- const spinner = ora2(`Searching for "${query}"...`).start();
1697
+ const spinner = ora3(`Searching for "${query}"...`).start();
1491
1698
  let data;
1492
1699
  try {
1493
1700
  data = await searchSkills(query);
1494
1701
  } catch (err) {
1495
- spinner.fail(pc6.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
1702
+ spinner.fail(pc7.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
1496
1703
  return;
1497
1704
  }
1498
1705
  if (data.error) {
1499
- spinner.fail(pc6.red(`Error: ${data.message || data.error}`));
1706
+ spinner.fail(pc7.red(`Error: ${data.message || data.error}`));
1500
1707
  return;
1501
1708
  }
1502
1709
  if (!data.results || data.results.length === 0) {
1503
- spinner.warn(pc6.yellow(`No skills found matching "${query}"`));
1710
+ spinner.warn(pc7.yellow(`No skills found matching "${query}"`));
1504
1711
  return;
1505
1712
  }
1506
1713
  spinner.succeed(`Found ${data.results.length} skill(s)`);
1714
+ trackEvent("search_query", { query, resultCount: data.results.length });
1507
1715
  const indexWidth = data.results.length.toString().length;
1508
1716
  const maxNameLen = Math.max(...data.results.map((s) => s.name.length));
1717
+ const installsColWidth = 10;
1509
1718
  const choices = data.results.map((s, index) => {
1510
- const indexStr = pc6.dim(`${(index + 1).toString().padStart(indexWidth)}.`);
1719
+ const indexStr = pc7.dim(`${(index + 1).toString().padStart(indexWidth)}.`);
1511
1720
  const paddedName = s.name.padEnd(maxNameLen);
1512
- const installs = formatInstallCount(s.installCount);
1721
+ const installsRaw = s.installCount ? String(s.installCount) : "-";
1722
+ const paddedInstalls = formatInstallCount(s.installCount, pc7.dim("-")) + " ".repeat(installsColWidth - installsRaw.length);
1723
+ const trust = formatTrustScore(s.trustScore);
1513
1724
  const skillLink = terminalLink(
1514
1725
  s.name,
1515
1726
  `https://context7.com/skills${s.project}/${s.name}`,
1516
- pc6.white
1727
+ pc7.white
1517
1728
  );
1518
- const repoLink = terminalLink(s.project, `https://github.com${s.project}`, pc6.white);
1729
+ const repoLink = terminalLink(s.project, `https://github.com${s.project}`, pc7.white);
1519
1730
  const metadataLines = [
1520
- pc6.dim("\u2500".repeat(50)),
1731
+ pc7.dim("\u2500".repeat(50)),
1521
1732
  "",
1522
- `${pc6.yellow("Skill:")} ${skillLink}`,
1523
- `${pc6.yellow("Repo:")} ${repoLink}`,
1524
- `${pc6.yellow("Description:")}`,
1525
- pc6.white(s.description || "No description")
1733
+ `${pc7.yellow("Skill:")} ${skillLink}`,
1734
+ `${pc7.yellow("Repo:")} ${repoLink}`,
1735
+ `${pc7.yellow("Description:")}`,
1736
+ pc7.white(s.description || "No description")
1526
1737
  ];
1527
1738
  return {
1528
- name: installs ? `${indexStr} ${paddedName} ${installs}` : `${indexStr} ${paddedName}`,
1739
+ name: `${indexStr} ${paddedName} ${paddedInstalls}${trust}`,
1529
1740
  value: s,
1530
1741
  description: metadataLines.join("\n")
1531
1742
  };
1532
1743
  });
1533
1744
  log.blank();
1745
+ const checkboxPrefixWidth = 3;
1746
+ const headerPad = " ".repeat(checkboxPrefixWidth + indexWidth + 1 + 1 + maxNameLen + 1);
1747
+ const headerLine = headerPad + pc7.dim("Installs".padEnd(installsColWidth)) + pc7.dim("Trust(0-10)");
1748
+ const message = "Select skills to install:\n" + headerLine;
1534
1749
  let selectedSkills;
1535
1750
  try {
1536
1751
  selectedSkills = await checkboxWithHover({
1537
- message: "Select skills to install:",
1752
+ message,
1538
1753
  choices,
1539
1754
  pageSize: 15,
1540
1755
  loop: false
@@ -1554,7 +1769,7 @@ async function searchCommand(query) {
1554
1769
  return;
1555
1770
  }
1556
1771
  const targetDirs = getTargetDirs(targets);
1557
- const installSpinner = ora2("Installing skills...").start();
1772
+ const installSpinner = ora3("Installing skills...").start();
1558
1773
  let permissionError = false;
1559
1774
  const failedDirs = /* @__PURE__ */ new Set();
1560
1775
  const installedSkills = [];
@@ -1613,11 +1828,12 @@ async function searchCommand(query) {
1613
1828
  return;
1614
1829
  }
1615
1830
  installSpinner.succeed(`Installed ${installedSkills.length} skill(s)`);
1616
- trackInstalls(installedSkills, targets.ides);
1831
+ trackEvent("install", { skills: installedSkills, ides: targets.ides });
1617
1832
  const installedNames = uniqueSkills.map((s) => s.name);
1618
1833
  logInstallSummary(targets, targetDirs, installedNames);
1619
1834
  }
1620
1835
  async function listCommand(options) {
1836
+ trackEvent("command", { name: "list" });
1621
1837
  const scope = options.global ? "global" : "project";
1622
1838
  const pathMap = scope === "global" ? IDE_GLOBAL_PATHS : IDE_PATHS;
1623
1839
  const baseDir = scope === "global" ? homedir4() : process.cwd();
@@ -1642,14 +1858,15 @@ async function listCommand(options) {
1642
1858
  for (const { ide, skills } of results) {
1643
1859
  const ideName = IDE_NAMES[ide];
1644
1860
  const path2 = pathMap[ide];
1645
- log.plain(`${pc6.bold(ideName)} ${pc6.dim(path2)}`);
1861
+ log.plain(`${pc7.bold(ideName)} ${pc7.dim(path2)}`);
1646
1862
  for (const skill of skills) {
1647
- log.plain(` ${pc6.green(skill)}`);
1863
+ log.plain(` ${pc7.green(skill)}`);
1648
1864
  }
1649
1865
  log.blank();
1650
1866
  }
1651
1867
  }
1652
1868
  async function removeCommand(name, options) {
1869
+ trackEvent("command", { name: "remove" });
1653
1870
  const target = await promptForSingleTarget(options);
1654
1871
  if (!target) {
1655
1872
  log.warn("Cancelled");
@@ -1672,6 +1889,7 @@ async function removeCommand(name, options) {
1672
1889
  }
1673
1890
  }
1674
1891
  async function infoCommand(input2) {
1892
+ trackEvent("command", { name: "info" });
1675
1893
  const parsed = parseSkillInput(input2);
1676
1894
  if (!parsed) {
1677
1895
  log.blank();
@@ -1682,14 +1900,14 @@ async function infoCommand(input2) {
1682
1900
  }
1683
1901
  const repo = `/${parsed.owner}/${parsed.repo}`;
1684
1902
  log.blank();
1685
- const spinner = ora2(`Fetching skills from ${repo}...`).start();
1903
+ const spinner = ora3(`Fetching skills from ${repo}...`).start();
1686
1904
  const data = await listProjectSkills(repo);
1687
1905
  if (data.error) {
1688
- spinner.fail(pc6.red(`Error: ${data.message || data.error}`));
1906
+ spinner.fail(pc7.red(`Error: ${data.message || data.error}`));
1689
1907
  return;
1690
1908
  }
1691
1909
  if (!data.skills || data.skills.length === 0) {
1692
- spinner.warn(pc6.yellow(`No skills found in ${repo}`));
1910
+ spinner.warn(pc7.yellow(`No skills found in ${repo}`));
1693
1911
  return;
1694
1912
  }
1695
1913
  spinner.succeed(`Found ${data.skills.length} skill(s)`);
@@ -1701,145 +1919,13 @@ async function infoCommand(input2) {
1701
1919
  log.blank();
1702
1920
  }
1703
1921
  log.plain(
1704
- `${pc6.bold("Quick commands:")}
1705
- Install all: ${pc6.cyan(`ctx7 skills install ${repo} --all`)}
1706
- Install one: ${pc6.cyan(`ctx7 skills install ${repo} ${data.skills[0]?.name}`)}
1922
+ `${pc7.bold("Quick commands:")}
1923
+ Install all: ${pc7.cyan(`ctx7 skills install ${repo} --all`)}
1924
+ Install one: ${pc7.cyan(`ctx7 skills install ${repo} ${data.skills[0]?.name}`)}
1707
1925
  `
1708
1926
  );
1709
1927
  }
1710
1928
 
1711
- // src/commands/auth.ts
1712
- import pc7 from "picocolors";
1713
- import ora3 from "ora";
1714
- import open from "open";
1715
- var CLI_CLIENT_ID = "2veBSofhicRBguUT";
1716
- var baseUrl2 = "https://context7.com";
1717
- function setAuthBaseUrl(url) {
1718
- baseUrl2 = url;
1719
- }
1720
- function registerAuthCommands(program2) {
1721
- program2.command("login").description("Log in to Context7").option("--no-browser", "Don't open browser automatically").action(async (options) => {
1722
- await loginCommand(options);
1723
- });
1724
- program2.command("logout").description("Log out of Context7").action(() => {
1725
- logoutCommand();
1726
- });
1727
- program2.command("whoami").description("Show current login status").action(async () => {
1728
- await whoamiCommand();
1729
- });
1730
- }
1731
- async function loginCommand(options) {
1732
- const existingTokens = loadTokens();
1733
- if (existingTokens) {
1734
- const expired = isTokenExpired(existingTokens);
1735
- if (!expired || existingTokens.refresh_token) {
1736
- console.log(pc7.yellow("You are already logged in."));
1737
- console.log(
1738
- pc7.dim("Run 'ctx7 logout' first if you want to log in with a different account.")
1739
- );
1740
- return;
1741
- }
1742
- clearTokens();
1743
- }
1744
- const spinner = ora3("Preparing login...").start();
1745
- try {
1746
- const { codeVerifier, codeChallenge } = generatePKCE();
1747
- const state = generateState();
1748
- const callbackServer = createCallbackServer(state);
1749
- const port = await callbackServer.port;
1750
- const redirectUri = `http://localhost:${port}/callback`;
1751
- const authUrl = buildAuthorizationUrl(
1752
- baseUrl2,
1753
- CLI_CLIENT_ID,
1754
- redirectUri,
1755
- codeChallenge,
1756
- state
1757
- );
1758
- spinner.stop();
1759
- console.log("");
1760
- console.log(pc7.bold("Opening browser to log in..."));
1761
- console.log("");
1762
- if (options.browser) {
1763
- await open(authUrl);
1764
- console.log(pc7.dim("If the browser didn't open, visit this URL:"));
1765
- } else {
1766
- console.log(pc7.dim("Open this URL in your browser:"));
1767
- }
1768
- console.log(pc7.cyan(authUrl));
1769
- console.log("");
1770
- const waitingSpinner = ora3("Waiting for login...").start();
1771
- try {
1772
- const { code } = await callbackServer.result;
1773
- waitingSpinner.text = "Exchanging code for tokens...";
1774
- const tokens = await exchangeCodeForTokens(
1775
- baseUrl2,
1776
- code,
1777
- codeVerifier,
1778
- redirectUri,
1779
- CLI_CLIENT_ID
1780
- );
1781
- saveTokens(tokens);
1782
- callbackServer.close();
1783
- waitingSpinner.succeed(pc7.green("Login successful!"));
1784
- console.log("");
1785
- console.log(pc7.dim("You can now use authenticated Context7 features."));
1786
- } catch (error) {
1787
- callbackServer.close();
1788
- waitingSpinner.fail(pc7.red("Login failed"));
1789
- if (error instanceof Error) {
1790
- console.error(pc7.red(error.message));
1791
- }
1792
- process.exit(1);
1793
- }
1794
- } catch (error) {
1795
- spinner.fail(pc7.red("Login failed"));
1796
- if (error instanceof Error) {
1797
- console.error(pc7.red(error.message));
1798
- }
1799
- process.exit(1);
1800
- }
1801
- }
1802
- function logoutCommand() {
1803
- if (clearTokens()) {
1804
- console.log(pc7.green("Logged out successfully."));
1805
- } else {
1806
- console.log(pc7.yellow("You are not logged in."));
1807
- }
1808
- }
1809
- async function whoamiCommand() {
1810
- const tokens = loadTokens();
1811
- if (!tokens) {
1812
- console.log(pc7.yellow("Not logged in."));
1813
- console.log(pc7.dim("Run 'ctx7 login' to authenticate."));
1814
- return;
1815
- }
1816
- console.log(pc7.green("Logged in"));
1817
- try {
1818
- const userInfo = await fetchUserInfo(tokens.access_token);
1819
- if (userInfo.name) {
1820
- console.log(`${pc7.dim("Name:".padEnd(9))}${userInfo.name}`);
1821
- }
1822
- if (userInfo.email) {
1823
- console.log(`${pc7.dim("Email:".padEnd(9))}${userInfo.email}`);
1824
- }
1825
- } catch {
1826
- if (isTokenExpired(tokens) && !tokens.refresh_token) {
1827
- console.log(pc7.dim("(Session may be expired - run 'ctx7 login' to refresh)"));
1828
- }
1829
- }
1830
- }
1831
- async function fetchUserInfo(accessToken) {
1832
- const response = await fetch("https://clerk.context7.com/oauth/userinfo", {
1833
- headers: {
1834
- Authorization: `Bearer ${accessToken}`
1835
- }
1836
- });
1837
- if (!response.ok) {
1838
- throw new Error("Failed to fetch user info");
1839
- }
1840
- return await response.json();
1841
- }
1842
-
1843
1929
  // src/constants.ts
1844
1930
  import { readFileSync as readFileSync2 } from "fs";
1845
1931
  import { fileURLToPath } from "url";