ctx7 0.5.0 → 0.5.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
@@ -925,58 +925,153 @@ function trackEvent(event, data) {
925
925
  // src/commands/generate.ts
926
926
  import pc6 from "picocolors";
927
927
  import ora2 from "ora";
928
- import { mkdir as mkdir2, writeFile as writeFile2, readFile, unlink } from "fs/promises";
928
+ import { mkdir as mkdir3, writeFile as writeFile2, readFile, unlink } from "fs/promises";
929
929
  import { join as join4 } from "path";
930
930
  import { homedir as homedir3 } from "os";
931
931
  import { spawn } from "child_process";
932
932
  import { input, select as select2 } from "@inquirer/prompts";
933
933
 
934
934
  // src/utils/auth.ts
935
- import * as crypto from "crypto";
936
- import * as http from "http";
935
+ import * as fs2 from "fs";
936
+ import * as os2 from "os";
937
+
938
+ // src/utils/storage-paths.ts
937
939
  import * as fs from "fs";
938
- import * as path from "path";
940
+ import { access as access2, chmod, mkdir as mkdir2, rename } from "fs/promises";
939
941
  import * as os from "os";
940
- var CONFIG_DIR = path.join(os.homedir(), ".context7");
941
- var CREDENTIALS_FILE = path.join(CONFIG_DIR, "credentials.json");
942
- function generatePKCE() {
943
- const codeVerifier = crypto.randomBytes(32).toString("base64url");
944
- const codeChallenge = crypto.createHash("sha256").update(codeVerifier).digest("base64url");
945
- return { codeVerifier, codeChallenge };
942
+ import * as path from "path";
943
+ var APP_DIR = "context7";
944
+ var LEGACY_DIR = ".context7";
945
+ var CREDENTIALS_FILE_NAME = "credentials.json";
946
+ var UPDATE_STATE_FILE_NAME = "cli-state.json";
947
+ var PREVIEWS_DIR_NAME = "previews";
948
+ function xdgBase(envVar, ...defaultSegments) {
949
+ const value = process.env[envVar];
950
+ const base = value && path.isAbsolute(value) ? value : path.join(os.homedir(), ...defaultSegments);
951
+ return path.join(base, APP_DIR);
952
+ }
953
+ function getConfigDir() {
954
+ return xdgBase("XDG_CONFIG_HOME", ".config");
955
+ }
956
+ function getStateDir() {
957
+ return xdgBase("XDG_STATE_HOME", ".local", "state");
958
+ }
959
+ function getCacheDir() {
960
+ return xdgBase("XDG_CACHE_HOME", ".cache");
961
+ }
962
+ function getCredentialsFilePath() {
963
+ return path.join(getConfigDir(), CREDENTIALS_FILE_NAME);
964
+ }
965
+ function getUpdateStateFilePath() {
966
+ return path.join(getStateDir(), UPDATE_STATE_FILE_NAME);
967
+ }
968
+ function getPreviewsDir() {
969
+ return path.join(getCacheDir(), PREVIEWS_DIR_NAME);
970
+ }
971
+ function getLegacyFilePath(fileName) {
972
+ return path.join(os.homedir(), LEGACY_DIR, fileName);
973
+ }
974
+ function migrateLegacyFileSync(fileName, targetPath, mode) {
975
+ const legacyPath = getLegacyFilePath(fileName);
976
+ if (legacyPath === targetPath || fs.existsSync(targetPath) || !fs.existsSync(legacyPath)) {
977
+ return;
978
+ }
979
+ try {
980
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true, mode: 448 });
981
+ fs.renameSync(legacyPath, targetPath);
982
+ if (mode !== void 0) {
983
+ fs.chmodSync(targetPath, mode);
984
+ }
985
+ } catch {
986
+ }
987
+ }
988
+ async function migrateLegacyFile(fileName, targetPath, mode) {
989
+ const legacyPath = getLegacyFilePath(fileName);
990
+ if (legacyPath === targetPath || await exists(targetPath) || !await exists(legacyPath)) {
991
+ return;
992
+ }
993
+ try {
994
+ await mkdir2(path.dirname(targetPath), { recursive: true, mode: 448 });
995
+ await rename(legacyPath, targetPath);
996
+ if (mode !== void 0) {
997
+ await chmod(targetPath, mode);
998
+ }
999
+ } catch {
1000
+ }
946
1001
  }
947
- function generateState() {
948
- return crypto.randomBytes(16).toString("base64url");
1002
+ function resolveReadPathSync(fileName, targetPath, mode) {
1003
+ migrateLegacyFileSync(fileName, targetPath, mode);
1004
+ if (fs.existsSync(targetPath)) {
1005
+ return targetPath;
1006
+ }
1007
+ const legacyPath = getLegacyFilePath(fileName);
1008
+ return fs.existsSync(legacyPath) ? legacyPath : targetPath;
949
1009
  }
1010
+ async function resolveReadPath(fileName, targetPath, mode) {
1011
+ await migrateLegacyFile(fileName, targetPath, mode);
1012
+ if (await exists(targetPath)) {
1013
+ return targetPath;
1014
+ }
1015
+ const legacyPath = getLegacyFilePath(fileName);
1016
+ return await exists(legacyPath) ? legacyPath : targetPath;
1017
+ }
1018
+ async function exists(filePath) {
1019
+ try {
1020
+ await access2(filePath);
1021
+ return true;
1022
+ } catch {
1023
+ return false;
1024
+ }
1025
+ }
1026
+
1027
+ // src/utils/auth.ts
950
1028
  function ensureConfigDir() {
951
- if (!fs.existsSync(CONFIG_DIR)) {
952
- fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
1029
+ const configDir = getConfigDir();
1030
+ if (!fs2.existsSync(configDir)) {
1031
+ fs2.mkdirSync(configDir, { recursive: true, mode: 448 });
953
1032
  }
954
1033
  }
1034
+ var CREDENTIALS_MODE = 384;
955
1035
  function saveTokens(tokens) {
1036
+ const credentialsFile = getCredentialsFilePath();
1037
+ migrateLegacyFileSync(CREDENTIALS_FILE_NAME, credentialsFile, CREDENTIALS_MODE);
956
1038
  ensureConfigDir();
957
1039
  const data = {
958
1040
  ...tokens,
959
1041
  expires_at: tokens.expires_at ?? (tokens.expires_in ? Date.now() + tokens.expires_in * 1e3 : void 0)
960
1042
  };
961
- fs.writeFileSync(CREDENTIALS_FILE, JSON.stringify(data, null, 2), { mode: 384 });
1043
+ fs2.writeFileSync(credentialsFile, JSON.stringify(data, null, 2), { mode: CREDENTIALS_MODE });
1044
+ fs2.chmodSync(credentialsFile, CREDENTIALS_MODE);
962
1045
  }
963
1046
  function loadTokens() {
964
- if (!fs.existsSync(CREDENTIALS_FILE)) {
1047
+ const credentialsFile = resolveReadPathSync(
1048
+ CREDENTIALS_FILE_NAME,
1049
+ getCredentialsFilePath(),
1050
+ CREDENTIALS_MODE
1051
+ );
1052
+ if (!fs2.existsSync(credentialsFile)) {
965
1053
  return null;
966
1054
  }
967
1055
  try {
968
- const data = JSON.parse(fs.readFileSync(CREDENTIALS_FILE, "utf-8"));
1056
+ const data = JSON.parse(fs2.readFileSync(credentialsFile, "utf-8"));
969
1057
  return data;
970
1058
  } catch {
971
1059
  return null;
972
1060
  }
973
1061
  }
974
1062
  function clearTokens() {
975
- if (fs.existsSync(CREDENTIALS_FILE)) {
976
- fs.unlinkSync(CREDENTIALS_FILE);
977
- return true;
1063
+ const credentialsFile = getCredentialsFilePath();
1064
+ let removed = false;
1065
+ if (fs2.existsSync(credentialsFile)) {
1066
+ fs2.unlinkSync(credentialsFile);
1067
+ removed = true;
978
1068
  }
979
- return false;
1069
+ const legacyCredentialsFile = getLegacyFilePath(CREDENTIALS_FILE_NAME);
1070
+ if (fs2.existsSync(legacyCredentialsFile)) {
1071
+ fs2.unlinkSync(legacyCredentialsFile);
1072
+ removed = true;
1073
+ }
1074
+ return removed;
980
1075
  }
981
1076
  function isTokenExpired(tokens) {
982
1077
  if (!tokens.expires_at) {
@@ -1017,157 +1112,12 @@ async function getValidAccessToken() {
1017
1112
  return null;
1018
1113
  }
1019
1114
  }
1020
- var CALLBACK_PORT = 52417;
1021
- function createCallbackServer(expectedState) {
1022
- let resolvePort;
1023
- let resolveResult;
1024
- let rejectResult;
1025
- let serverInstance = null;
1026
- const portPromise = new Promise((resolve3) => {
1027
- resolvePort = resolve3;
1028
- });
1029
- const resultPromise = new Promise((resolve3, reject) => {
1030
- resolveResult = resolve3;
1031
- rejectResult = reject;
1032
- });
1033
- const server = http.createServer((req, res) => {
1034
- const url = new URL(req.url || "/", `http://localhost`);
1035
- if (url.pathname === "/callback") {
1036
- const code = url.searchParams.get("code");
1037
- const state = url.searchParams.get("state");
1038
- const error = url.searchParams.get("error");
1039
- const errorDescription = url.searchParams.get("error_description");
1040
- res.writeHead(200, { "Content-Type": "text/html" });
1041
- if (error) {
1042
- res.end(errorPage(errorDescription || error));
1043
- serverInstance?.close();
1044
- rejectResult(new Error(errorDescription || error));
1045
- return;
1046
- }
1047
- if (!code || !state) {
1048
- res.end(errorPage("Missing authorization code or state"));
1049
- serverInstance?.close();
1050
- rejectResult(new Error("Missing authorization code or state"));
1051
- return;
1052
- }
1053
- if (state !== expectedState) {
1054
- res.end(errorPage("State mismatch - possible CSRF attack"));
1055
- serverInstance?.close();
1056
- rejectResult(new Error("State mismatch"));
1057
- return;
1058
- }
1059
- res.end(successPage());
1060
- serverInstance?.close();
1061
- resolveResult({ code, state });
1062
- } else {
1063
- res.writeHead(404);
1064
- res.end("Not found");
1065
- }
1066
- });
1067
- serverInstance = server;
1068
- server.on("error", (err) => {
1069
- rejectResult(err);
1070
- });
1071
- server.listen(CALLBACK_PORT, "127.0.0.1", () => {
1072
- resolvePort(CALLBACK_PORT);
1073
- });
1074
- const timeout = setTimeout(
1075
- () => {
1076
- server.close();
1077
- rejectResult(new Error("Login timed out after 5 minutes"));
1078
- },
1079
- 5 * 60 * 1e3
1080
- );
1081
- return {
1082
- port: portPromise,
1083
- result: resultPromise,
1084
- close: () => {
1085
- clearTimeout(timeout);
1086
- server.close();
1087
- }
1088
- };
1089
- }
1090
- function successPage() {
1091
- return `<!DOCTYPE html>
1092
- <html>
1093
- <head><title>Login Successful</title></head>
1094
- <body style="font-family: system-ui; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #f9fafb;">
1095
- <div style="text-align: center; padding: 2rem;">
1096
- <div style="width: 64px; height: 64px; background: #16a34a; border-radius: 50%; display: flex; align-items: center; justify-content: center; margin: 0 auto 1rem;">
1097
- <svg width="32" height="32" fill="none" stroke="white" stroke-width="3" viewBox="0 0 24 24">
1098
- <path d="M5 13l4 4L19 7" stroke-linecap="round" stroke-linejoin="round"/>
1099
- </svg>
1100
- </div>
1101
- <h1 style="color: #16a34a; margin: 0 0 0.5rem;">Login Successful!</h1>
1102
- <p style="color: #6b7280; margin: 0;">You can close this window and return to the terminal.</p>
1103
- </div>
1104
- </body>
1105
- </html>`;
1106
- }
1107
- function escapeHtml(text) {
1108
- return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
1109
- }
1110
- function errorPage(message) {
1111
- const safeMessage = escapeHtml(message);
1112
- return `<!DOCTYPE html>
1113
- <html>
1114
- <head><title>Login Failed</title></head>
1115
- <body style="font-family: system-ui; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #f9fafb;">
1116
- <div style="text-align: center; padding: 2rem;">
1117
- <div style="width: 64px; height: 64px; background: #dc2626; border-radius: 50%; display: flex; align-items: center; justify-content: center; margin: 0 auto 1rem;">
1118
- <svg width="32" height="32" fill="none" stroke="white" stroke-width="3" viewBox="0 0 24 24">
1119
- <path d="M6 18L18 6M6 6l12 12" stroke-linecap="round" stroke-linejoin="round"/>
1120
- </svg>
1121
- </div>
1122
- <h1 style="color: #dc2626; margin: 0 0 0.5rem;">Login Failed</h1>
1123
- <p style="color: #6b7280; margin: 0;">${safeMessage}</p>
1124
- <p style="color: #9ca3af; margin: 1rem 0 0; font-size: 0.875rem;">You can close this window.</p>
1125
- </div>
1126
- </body>
1127
- </html>`;
1128
- }
1129
- async function exchangeCodeForTokens(baseUrl3, code, codeVerifier, redirectUri, clientId) {
1130
- const response = await fetch(`${baseUrl3}/api/oauth/token`, {
1131
- method: "POST",
1132
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
1133
- body: new URLSearchParams({
1134
- grant_type: "authorization_code",
1135
- client_id: clientId,
1136
- code,
1137
- code_verifier: codeVerifier,
1138
- redirect_uri: redirectUri
1139
- }).toString()
1140
- });
1141
- if (!response.ok) {
1142
- const err = await response.json().catch(() => ({}));
1143
- throw new Error(err.error_description || err.error || "Failed to exchange code for tokens");
1144
- }
1145
- return await response.json();
1146
- }
1147
- function buildAuthorizationUrl(baseUrl3, clientId, redirectUri, codeChallenge, state) {
1148
- const url = new URL(`${baseUrl3}/api/oauth/authorize`);
1149
- url.searchParams.set("client_id", clientId);
1150
- url.searchParams.set("redirect_uri", redirectUri);
1151
- url.searchParams.set("code_challenge", codeChallenge);
1152
- url.searchParams.set("code_challenge_method", "S256");
1153
- url.searchParams.set("state", state);
1154
- url.searchParams.set("scope", "profile email");
1155
- url.searchParams.set("response_type", "code");
1156
- return url.toString();
1157
- }
1158
1115
  var DEVICE_CODE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
1159
1116
  var DEFAULT_DEVICE_POLL_INTERVAL_SECONDS = 5;
1160
- function shouldUseDeviceFlow() {
1161
- if (process.env.SSH_CONNECTION || process.env.SSH_CLIENT || process.env.SSH_TTY) return true;
1162
- if (process.platform === "linux" && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) {
1163
- return true;
1164
- }
1165
- return false;
1166
- }
1167
1117
  async function startDeviceAuthorization(baseUrl3, clientId) {
1168
1118
  const params = new URLSearchParams({ client_id: clientId });
1169
1119
  try {
1170
- const hostname2 = os.hostname();
1120
+ const hostname2 = os2.hostname();
1171
1121
  if (hostname2) params.set("hostname", hostname2);
1172
1122
  } catch {
1173
1123
  }
@@ -1236,7 +1186,7 @@ function setAuthBaseUrl(url) {
1236
1186
  baseUrl2 = url;
1237
1187
  }
1238
1188
  function registerAuthCommands(program2) {
1239
- program2.command("login").description("Log in to Context7").option("--no-browser", "Don't open browser automatically").option("--device", "Force device-code flow (use on SSH / headless hosts)").action(async (options) => {
1189
+ program2.command("login").description("Log in to Context7").option("--no-browser", "Don't open browser automatically").action(async (options) => {
1240
1190
  await loginCommand(options);
1241
1191
  });
1242
1192
  program2.command("logout").description("Log out of Context7").action(() => {
@@ -1299,7 +1249,7 @@ async function announceIdentity(accessToken) {
1299
1249
  return "Login successful!";
1300
1250
  }
1301
1251
  }
1302
- async function performDeviceLogin(openBrowser = true) {
1252
+ async function performLogin(openBrowser = true) {
1303
1253
  const spinner = ora("Preparing login...").start();
1304
1254
  let authorization;
1305
1255
  try {
@@ -1367,67 +1317,6 @@ async function performDeviceLogin(openBrowser = true) {
1367
1317
  waitingSpinner.fail(pc4.red("Code expired without approval."));
1368
1318
  return null;
1369
1319
  }
1370
- async function performLogin(openBrowser = true, forceDevice = false) {
1371
- if (forceDevice || shouldUseDeviceFlow()) {
1372
- return performDeviceLogin(openBrowser);
1373
- }
1374
- const spinner = ora("Preparing login...").start();
1375
- try {
1376
- const { codeVerifier, codeChallenge } = generatePKCE();
1377
- const state = generateState();
1378
- const callbackServer = createCallbackServer(state);
1379
- const port = await callbackServer.port;
1380
- const redirectUri = `http://localhost:${port}/callback`;
1381
- const authUrl = buildAuthorizationUrl(
1382
- baseUrl2,
1383
- CLI_CLIENT_ID,
1384
- redirectUri,
1385
- codeChallenge,
1386
- state
1387
- );
1388
- spinner.stop();
1389
- console.log("");
1390
- console.log(pc4.bold("Opening browser to log in..."));
1391
- console.log("");
1392
- if (openBrowser) {
1393
- await open(authUrl);
1394
- console.log(pc4.dim("If the browser didn't open, visit this URL:"));
1395
- } else {
1396
- console.log(pc4.dim("Open this URL in your browser:"));
1397
- }
1398
- console.log(pc4.cyan(authUrl));
1399
- console.log("");
1400
- const waitingSpinner = ora("Waiting for login...").start();
1401
- try {
1402
- const { code } = await callbackServer.result;
1403
- waitingSpinner.text = "Exchanging code for tokens...";
1404
- const tokens = await exchangeCodeForTokens(
1405
- baseUrl2,
1406
- code,
1407
- codeVerifier,
1408
- redirectUri,
1409
- CLI_CLIENT_ID
1410
- );
1411
- saveTokens(tokens);
1412
- callbackServer.close();
1413
- waitingSpinner.succeed(pc4.green("Login successful!"));
1414
- return tokens.access_token;
1415
- } catch (error) {
1416
- callbackServer.close();
1417
- waitingSpinner.fail(pc4.red("Login failed"));
1418
- if (error instanceof Error) {
1419
- console.error(pc4.red(error.message));
1420
- }
1421
- return null;
1422
- }
1423
- } catch (error) {
1424
- spinner.fail(pc4.red("Login failed"));
1425
- if (error instanceof Error) {
1426
- console.error(pc4.red(error.message));
1427
- }
1428
- return null;
1429
- }
1430
- }
1431
1320
  async function loginCommand(options) {
1432
1321
  trackEvent("command", { name: "login" });
1433
1322
  const existingToken = await getValidAccessToken();
@@ -1437,7 +1326,7 @@ async function loginCommand(options) {
1437
1326
  return;
1438
1327
  }
1439
1328
  clearTokens();
1440
- const token = await performLogin(options.browser, options.device ?? false);
1329
+ const token = await performLogin(options.browser);
1441
1330
  if (!token) {
1442
1331
  process.exit(1);
1443
1332
  }
@@ -1902,8 +1791,8 @@ async function generateCommand(options) {
1902
1791
  log.blank();
1903
1792
  };
1904
1793
  const openInEditor = async () => {
1905
- const previewDir = join4(homedir3(), ".context7", "previews");
1906
- await mkdir2(previewDir, { recursive: true });
1794
+ const previewDir = getPreviewsDir();
1795
+ await mkdir3(previewDir, { recursive: true });
1907
1796
  previewFile = join4(previewDir, `${skillName}.md`);
1908
1797
  if (!previewFileWritten) {
1909
1798
  await writeFile2(previewFile, generatedContent, "utf-8");
@@ -1983,7 +1872,7 @@ async function generateCommand(options) {
1983
1872
  const skillDir = join4(finalDir, skillName);
1984
1873
  const skillPath = join4(skillDir, "SKILL.md");
1985
1874
  try {
1986
- await mkdir2(skillDir, { recursive: true });
1875
+ await mkdir3(skillDir, { recursive: true });
1987
1876
  await writeFile2(skillPath, generatedContent, "utf-8");
1988
1877
  } catch (err) {
1989
1878
  const error = err;
@@ -2835,12 +2724,12 @@ ${headerLine}`,
2835
2724
  import pc8 from "picocolors";
2836
2725
  import ora4 from "ora";
2837
2726
  import { select as select3 } from "@inquirer/prompts";
2838
- import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
2839
- import { dirname as dirname6, join as join8 } from "path";
2840
- import { randomBytes as randomBytes2 } from "crypto";
2727
+ import { mkdir as mkdir5, readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
2728
+ import { dirname as dirname7, join as join8 } from "path";
2729
+ import { randomBytes } from "crypto";
2841
2730
 
2842
2731
  // src/setup/agents.ts
2843
- import { access as access2 } from "fs/promises";
2732
+ import { access as access3 } from "fs/promises";
2844
2733
  import { join as join7 } from "path";
2845
2734
  import { homedir as homedir5 } from "os";
2846
2735
  var SETUP_AGENT_NAMES = {
@@ -3044,7 +2933,7 @@ function getAgent(name) {
3044
2933
  var ALL_AGENT_NAMES = Object.keys(agents);
3045
2934
  async function pathExists(p) {
3046
2935
  try {
3047
- await access2(p);
2936
+ await access3(p);
3048
2937
  return true;
3049
2938
  } catch {
3050
2939
  return false;
@@ -3153,8 +3042,8 @@ function customizeSkillFilesForAgent(agent, skillName, files) {
3153
3042
  }
3154
3043
 
3155
3044
  // src/setup/mcp-writer.ts
3156
- import { access as access3, readFile as readFile3, writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
3157
- import { dirname as dirname5 } from "path";
3045
+ import { access as access4, readFile as readFile3, writeFile as writeFile3, mkdir as mkdir4 } from "fs/promises";
3046
+ import { dirname as dirname6 } from "path";
3158
3047
  function stripJsonComments(text) {
3159
3048
  let result = "";
3160
3049
  let i = 0;
@@ -3225,7 +3114,7 @@ function removeServerEntry(existing, configKey, serverName) {
3225
3114
  async function resolveMcpPath(candidates) {
3226
3115
  for (const candidate of candidates) {
3227
3116
  try {
3228
- await access3(candidate);
3117
+ await access4(candidate);
3229
3118
  return candidate;
3230
3119
  } catch {
3231
3120
  }
@@ -3233,7 +3122,7 @@ async function resolveMcpPath(candidates) {
3233
3122
  return candidates[0];
3234
3123
  }
3235
3124
  async function writeJsonConfig(filePath, config) {
3236
- await mkdir3(dirname5(filePath), { recursive: true });
3125
+ await mkdir4(dirname6(filePath), { recursive: true });
3237
3126
  await writeFile3(filePath, JSON.stringify(config, null, 2) + "\n", "utf-8");
3238
3127
  }
3239
3128
  async function readTomlServerExists(filePath, serverName) {
@@ -3353,11 +3242,11 @@ async function appendTomlServer(filePath, serverName, entry) {
3353
3242
  const before = rawBefore.length > 0 ? rawBefore + "\n\n" : "";
3354
3243
  const after = rawAfter.length > 0 ? "\n" + rawAfter : "";
3355
3244
  const content = before + block + after;
3356
- await mkdir3(dirname5(filePath), { recursive: true });
3245
+ await mkdir4(dirname6(filePath), { recursive: true });
3357
3246
  await writeFile3(filePath, content, "utf-8");
3358
3247
  } else {
3359
3248
  const separator = existing.length > 0 && !existing.endsWith("\n") ? "\n\n" : existing.length > 0 ? "\n" : "";
3360
- await mkdir3(dirname5(filePath), { recursive: true });
3249
+ await mkdir4(dirname6(filePath), { recursive: true });
3361
3250
  await writeFile3(filePath, existing + separator + block, "utf-8");
3362
3251
  }
3363
3252
  return { alreadyExists };
@@ -3390,7 +3279,7 @@ async function removeTomlServer(filePath, serverName) {
3390
3279
  const rawBefore = existing.slice(0, startIdx).replace(/\n+$/, "");
3391
3280
  const rawAfter = existing.slice(startIdx + sectionHeader.length + endOffset).replace(/^\n+/, "");
3392
3281
  const content = [rawBefore, rawAfter].filter(Boolean).join("\n\n");
3393
- await mkdir3(dirname5(filePath), { recursive: true });
3282
+ await mkdir4(dirname6(filePath), { recursive: true });
3394
3283
  await writeFile3(filePath, content.length > 0 ? `${content}
3395
3284
  ` : "", "utf-8");
3396
3285
  return { removed: true };
@@ -3432,7 +3321,7 @@ async function authenticateAndGenerateKey() {
3432
3321
  Authorization: `Bearer ${accessToken}`,
3433
3322
  "Content-Type": "application/json"
3434
3323
  },
3435
- body: JSON.stringify({ name: `ctx7-cli-${randomBytes2(3).toString("hex")}` })
3324
+ body: JSON.stringify({ name: `ctx7-cli-${randomBytes(3).toString("hex")}` })
3436
3325
  });
3437
3326
  if (!response.ok) {
3438
3327
  const err = await response.json().catch(() => ({}));
@@ -3536,7 +3425,7 @@ async function installRule(agentName, mode, scope) {
3536
3425
  if (rule.kind === "file") {
3537
3426
  const ruleDir = scope === "global" ? rule.dir("global") : join8(process.cwd(), rule.dir("project"));
3538
3427
  const rulePath = join8(ruleDir, rule.filename);
3539
- await mkdir4(dirname6(rulePath), { recursive: true });
3428
+ await mkdir5(dirname7(rulePath), { recursive: true });
3540
3429
  await writeFile4(rulePath, content, "utf-8");
3541
3430
  return { status: "installed", path: rulePath };
3542
3431
  }
@@ -3556,7 +3445,7 @@ ${content}${rule.sectionMarker}`;
3556
3445
  return { status: "updated", path: filePath };
3557
3446
  }
3558
3447
  const separator = existing.length > 0 && !existing.endsWith("\n") ? "\n\n" : existing.length > 0 ? "\n" : "";
3559
- await mkdir4(dirname6(filePath), { recursive: true });
3448
+ await mkdir5(dirname7(filePath), { recursive: true });
3560
3449
  await writeFile4(filePath, existing + separator + section + "\n", "utf-8");
3561
3450
  return { status: "installed", path: filePath };
3562
3451
  }
@@ -3660,7 +3549,7 @@ async function setupMcp(agents2, options, scope) {
3660
3549
  log.plain(` ${pc8.dim(r.skillPath)}`);
3661
3550
  if (r.skillStatus.includes("EACCES")) {
3662
3551
  log.plain(
3663
- ` ${pc8.yellow("tip:")} fix permissions with: ${pc8.cyan(`sudo chown -R $(whoami) ${dirname6(dirname6(r.skillPath))}`)}`
3552
+ ` ${pc8.yellow("tip:")} fix permissions with: ${pc8.cyan(`sudo chown -R $(whoami) ${dirname7(dirname7(r.skillPath))}`)}`
3664
3553
  );
3665
3554
  }
3666
3555
  }
@@ -3721,7 +3610,7 @@ async function setupCli(options) {
3721
3610
  log.plain(` ${pc8.dim(r.skillPath)}`);
3722
3611
  if (r.skillStatus.includes("EACCES")) {
3723
3612
  log.plain(
3724
- ` ${pc8.yellow("tip:")} fix permissions with: ${pc8.cyan(`sudo chown -R $(whoami) ${dirname6(dirname6(r.skillPath))}`)}`
3613
+ ` ${pc8.yellow("tip:")} fix permissions with: ${pc8.cyan(`sudo chown -R $(whoami) ${dirname7(dirname7(r.skillPath))}`)}`
3725
3614
  );
3726
3615
  }
3727
3616
  const ruleIcon = r.ruleStatus === "installed" || r.ruleStatus === "updated" ? pc8.green("+") : pc8.dim("~");
@@ -3754,7 +3643,7 @@ async function setupCommand(options) {
3754
3643
  import pc9 from "picocolors";
3755
3644
  import ora5 from "ora";
3756
3645
  import { join as join9 } from "path";
3757
- import { access as access4, readFile as readFile5, rm as rm3, writeFile as writeFile5 } from "fs/promises";
3646
+ import { access as access5, readFile as readFile5, rm as rm3, writeFile as writeFile5 } from "fs/promises";
3758
3647
  var CHECKBOX_THEME2 = {
3759
3648
  style: {
3760
3649
  highlight: (text) => pc9.green(text),
@@ -3854,7 +3743,7 @@ function resolveFlagModes(options) {
3854
3743
  }
3855
3744
  async function pathExists2(path2) {
3856
3745
  try {
3857
- await access4(path2);
3746
+ await access5(path2);
3858
3747
  return true;
3859
3748
  } catch {
3860
3749
  return false;
@@ -4101,6 +3990,18 @@ async function removeCommand2(options) {
4101
3990
  // src/commands/docs.ts
4102
3991
  import pc10 from "picocolors";
4103
3992
  import ora6 from "ora";
3993
+
3994
+ // src/utils/library-id.ts
3995
+ function recoverLibraryId(input2) {
3996
+ if (input2.startsWith("//")) return input2.replace(/^\/+/, "/");
3997
+ if (input2.startsWith("/")) return input2;
3998
+ if (!/^[A-Za-z]:[\\/]/.test(input2)) return input2;
3999
+ const normalized = input2.replace(/\\/g, "/");
4000
+ const match = normalized.match(/^[A-Za-z]:\/.*?\/(?:Git|PortableGit|git-bash)\/(.+)$/i);
4001
+ return match ? `/${match[1]}` : input2;
4002
+ }
4003
+
4004
+ // src/commands/docs.ts
4104
4005
  var isTTY = process.stdout.isTTY;
4105
4006
  function getReputationLabel(score) {
4106
4007
  if (score === void 0 || score < 0) return "Unknown";
@@ -4186,10 +4087,16 @@ async function resolveCommand(library, query, options) {
4186
4087
  }
4187
4088
  async function queryCommand(libraryId, query, options) {
4188
4089
  trackEvent("command", { name: "docs" });
4090
+ libraryId = recoverLibraryId(libraryId);
4189
4091
  if (!libraryId.startsWith("/") || !/^\/[^/]+\/[^/]/.test(libraryId)) {
4190
4092
  log.error(`Invalid library ID: "${libraryId}"`);
4191
4093
  log.info(`Expected format: /owner/repo or /owner/repo/version (e.g., /facebook/react)`);
4192
4094
  log.info(`Run "ctx7 library <name>" to find the correct ID`);
4095
+ if (process.platform === "win32") {
4096
+ log.info(
4097
+ `On Git Bash, prefix the ID with an extra slash to avoid path conversion: ctx7 docs "//facebook/react" "<your question>"`
4098
+ );
4099
+ }
4193
4100
  process.exitCode = 1;
4194
4101
  return;
4195
4102
  }
@@ -4273,25 +4180,36 @@ import { spawn as spawn2 } from "child_process";
4273
4180
  import pc11 from "picocolors";
4274
4181
 
4275
4182
  // src/utils/update-check.ts
4276
- import { homedir as homedir6 } from "os";
4277
- import { dirname as dirname7, join as join10 } from "path";
4278
- import { mkdir as mkdir5, readFile as readFile6, writeFile as writeFile6 } from "fs/promises";
4183
+ import { dirname as dirname8 } from "path";
4184
+ import { mkdir as mkdir6, readFile as readFile6, writeFile as writeFile6 } from "fs/promises";
4279
4185
  var DEFAULT_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
4280
- var UPDATE_STATE_FILE = join10(homedir6(), ".context7", "cli-state.json");
4281
4186
  function getStateFilePath(stateFile) {
4282
- return stateFile ?? UPDATE_STATE_FILE;
4187
+ return stateFile ?? getUpdateStateFilePath();
4188
+ }
4189
+ async function readStateFilePath(stateFile) {
4190
+ if (stateFile) {
4191
+ return stateFile;
4192
+ }
4193
+ return resolveReadPath(UPDATE_STATE_FILE_NAME, getUpdateStateFilePath());
4194
+ }
4195
+ async function writeStateFilePath(stateFile) {
4196
+ const path2 = getStateFilePath(stateFile);
4197
+ if (!stateFile) {
4198
+ await migrateLegacyFile(UPDATE_STATE_FILE_NAME, path2);
4199
+ }
4200
+ return path2;
4283
4201
  }
4284
4202
  async function readUpdateState(stateFile) {
4285
4203
  try {
4286
- const raw = await readFile6(getStateFilePath(stateFile), "utf-8");
4204
+ const raw = await readFile6(await readStateFilePath(stateFile), "utf-8");
4287
4205
  return JSON.parse(raw);
4288
4206
  } catch {
4289
4207
  return {};
4290
4208
  }
4291
4209
  }
4292
4210
  async function writeUpdateState(state, stateFile) {
4293
- const path2 = getStateFilePath(stateFile);
4294
- await mkdir5(dirname7(path2), { recursive: true });
4211
+ const path2 = await writeStateFilePath(stateFile);
4212
+ await mkdir6(dirname8(path2), { recursive: true });
4295
4213
  await writeFile6(path2, JSON.stringify(state, null, 2) + "\n", "utf-8");
4296
4214
  }
4297
4215
  function compareVersions(a, b) {
@@ -4595,7 +4513,7 @@ var brand = {
4595
4513
  dim: pc12.dim
4596
4514
  };
4597
4515
  var program = new Command();
4598
- program.name("ctx7").description("Context7 CLI - Fetch documentation context and configure Context7").version(VERSION).option("--base-url <url>").hook("preAction", (thisCommand) => {
4516
+ program.name("ctx7").description("Context7 CLI - Fetch documentation context and configure Context7").version(VERSION, "-v, --version").option("--base-url <url>").hook("preAction", (thisCommand) => {
4599
4517
  const opts = thisCommand.opts();
4600
4518
  if (opts.baseUrl) {
4601
4519
  setBaseUrl(opts.baseUrl);