cortxt
Version: 
AI-friendly CLI to share project context or file code easily. The fastest way to provide project context to AI intelligence like ChatGPT, Claude, and other AI assistants.
343 lines (289 loc) โข 9.69 kB
JavaScript
import fs from "fs";
import path from "path";
import ora from "ora";
import { readProject } from "../utils/read.js";
import { copyToClipboard } from "../utils/clipboard.js";
import { formatBytes, getProjectName } from "../utils/helpers.js";
import { colors, ui } from "../utils/colors.js";
import chalk from "chalk";
export async function runContext(options = {}) {
  const spinner = ora({
    text: 'Scanning project...',
    color: 'cyan'
  });
  try {
    spinner.start();
    const projectName = getProjectName();
    const projectType = detectProjectType();
    if (projectName) {
      const displayName = projectType
        ? `${projectName} (${projectType})`
        : projectName;
      spinner.text = `Scanning ${displayName}...`;
    }
    const startTime = Date.now();
    const result = await readProject(process.cwd(), options);
    let files = Object.entries(result.data);
    const originalFileCount = files.length;
    const originalTotalSize = files.reduce(
      (sum, [, content]) => sum + content.length,
      0
    );
    spinner.stop();
    if (originalFileCount === 0) {
      console.log("โ No files found to process");
      console.log(
        "๐ก Not in a project folder? Navigate to your code directory first"
      );
      return;
    }
    // Smart handling for large projects
    if (originalTotalSize > 500000 && !options.force) {
      console.log("๐ง  Smart processing for large project...");
      files = handleLargeProject(files, options);
    }
    const processingSpinner = ora({
      text: 'Processing files...',
      color: 'green'
    }).start();
    const finalFileCount = files.length;
    const finalTotalSize = files.reduce(
      (sum, [, content]) => sum + content.length,
      0
    );
    const formatted = files
      .map(([file, code]) => `\n\n### ${file}\n\`\`\`\n${code}\n\`\`\``)
      .join("\n");
    processingSpinner.stop();
    // Show stats
    const processTime = ((Date.now() - startTime) / 1000).toFixed(1);
    if (finalFileCount !== originalFileCount) {
      console.log(
        `โ
 Processed ${finalFileCount} priority files (${formatBytes(
          finalTotalSize
        )}) in ${processTime}s`
      );
      console.log(
        `๐ Original: ${originalFileCount} files (${formatBytes(
          originalTotalSize
        )})`
      );
    } else {
      console.log(
        `โ
 Processed ${finalFileCount} files (${formatBytes(
          finalTotalSize
        )}) in ${processTime}s`
      );
    }
    if (result.skipped > 0) {
      console.log(`Skipped ${result.skipped} binary/large files`);
    }
    if (options.stats) {
      showDetailedStats(files, finalTotalSize);
    }
    // Copy to clipboard - friendly message
    const clipboardSuccess = copyToClipboard(formatted);
    if (clipboardSuccess) {
      console.log(
        `๐ ${colors.success("Project content copied to clipboard!")} ${colors.brand(
          "Paste to any AI & watch magic โจ"
        )}`
      );
    }
    // Smart suggestions based on project
    showSmartSuggestions(
      finalTotalSize,
      finalFileCount,
      files,
      originalTotalSize !== finalTotalSize
    );
    // New feature notification
    console.log(
      `\n๐ ${colors.info(
        "New Feature:" 
      )} ${colors.brand("You can now select multiple files using `npx cortxt file --multiple`")}`
    );
    if (options.verbose) {
      console.log(
        `\nStats: ${finalFileCount} files, ${formatBytes(
          finalTotalSize
        )}, ~${Math.round(finalTotalSize / 4)} tokens`
      );
    }
  } catch (error) {
    if (spinner.isSpinning) spinner.fail('Failed to scan project');
    
    console.error(`โ Error: ${error.message}`);
    if (error.code === "ENOENT") {
      console.log(
        "๐ก Not in a project folder? Navigate to your code directory first"
      );
    } else {
      console.log("๐ก Make sure you're in a valid project directory");
    }
    process.exit(1);
  }
}
function handleLargeProject(files, options) {
  // Priority order for files
  const filePriority = {
    "package.json": 100,
    "README.md": 90,
    "index.js": 80,
    "main.js": 80,
    "app.js": 80,
    "server.js": 75,
  };
  // Get file priority score
  function getFilePriority(filename) {
    // Exact match
    if (filePriority[filename]) return filePriority[filename];
    // Extension-based priority
    const ext = path.extname(filename).toLowerCase();
    const extensionPriority = {
      ".js": 70,
      ".ts": 70,
      ".jsx": 65,
      ".tsx": 65,
      ".vue": 60,
      ".py": 60,
      ".go": 55,
      ".rs": 55,
      ".md": 50,
      ".json": 30,
      ".yaml": 25,
      ".yml": 25,
    };
    return extensionPriority[ext] || 20;
  }
  // Truncate very large files
  function truncateIfNeeded(content, filename, maxLength = 5000) {
    if (content.length <= maxLength) return content;
    const truncated = content.substring(0, maxLength);
    const lastNewline = truncated.lastIndexOf("\n");
    const safeContent =
      lastNewline > 0 ? truncated.substring(0, lastNewline) : truncated;
    return `${safeContent}\n\n// ... (file truncated - showing first ${safeContent.length} of ${content.length} characters)\n// Use 'cortxt file ${filename}' for complete content`;
  }
  // Sort files by priority
  const prioritizedFiles = files
    .map(([file, content]) => ({
      file,
      content,
      priority: getFilePriority(path.basename(file)),
    }))
    .sort((a, b) => b.priority - a.priority);
  // Get size limit
  const maxSizeBytes = parseInt(options.maxSize) * 1024;
  let currentSize = 0;
  const selectedFiles = [];
  for (const { file, content } of prioritizedFiles) {
    const truncatedContent = truncateIfNeeded(content, file);
    const contentSize = truncatedContent.length;
    if (currentSize + contentSize <= maxSizeBytes) {
      selectedFiles.push([file, truncatedContent]);
      currentSize += contentSize;
    } else if (selectedFiles.length === 0) {
      // Always include at least one file
      selectedFiles.push([file, truncatedContent]);
      break;
    } else {
      break;
    }
  }
  return selectedFiles;
}
function showDetailedStats(files, totalSize) {
  console.log(`\n${colors.brand.bold("๐ Detailed Statistics:")}`);
  
  // File extensions breakdown
  const extensions = {};
  files.forEach(([file]) => {
    const ext = path.extname(file) || "no extension";
    extensions[ext] = (extensions[ext] || 0) + 1;
  });
  console.log(`${colors.info("File types:")}`);
  Object.entries(extensions)
    .sort(([, a], [, b]) => b - a)
    .slice(0, 5)
    .forEach(([ext, count]) => {
      console.log(`  ${colors.filename(ext.padEnd(12))} ${colors.number(count)} files`);
    });
  // Largest files
  console.log(`\n${colors.info("Largest files:")}`);
  files
    .map(([file, content]) => [file, content.length])
    .sort(([, a], [, b]) => b - a)
    .slice(0, 3)
    .forEach(([file, size]) => {
      console.log(`  ${colors.filename(file.padEnd(25))} ${colors.size(formatBytes(size))}`);
    });
}
function showSmartSuggestions(
  finalTotalSize,
  finalFileCount,
  files,
  wasFiltered
) {
  console.log(`\n${colors.info("๐ก Smart Suggestions: --help")}`);
  if (finalTotalSize > 200000) {
    console.log(
      `${colors.warning("โ ๏ธ  Large context size detected")} - Consider using ${colors.brand("cortxt file")} for specific files`
    );
  }
  if (wasFiltered) {
    console.log(
      `${colors.info("๐ฏ Smart filtering applied")} - Use ${colors.brand("--force")} to include all files`
    );
  }
  // Detect common files that might be useful
  const hasTests = files.some(([file]) =>
    file.includes("test") || file.includes("spec")
  );
  const hasConfig = files.some(([file]) =>
    file.includes("config") || file.includes(".config")
  );
  if (!hasTests && hasConfig) {
    console.log(
      `${colors.info("๐งช No test files found")} - Consider adding test files to your project context`
    );
  }
  if (finalFileCount < 5) {
    console.log(
      `${colors.info("๐ Small project detected")} - Perfect size for AI analysis!`
    );
  }
}
function detectProjectType() {
  try {
    const cwd = process.cwd();
    
    if (fs.existsSync(path.join(cwd, "package.json"))) {
      const pkg = JSON.parse(fs.readFileSync(path.join(cwd, "package.json"), "utf-8"));
      
      if (pkg.dependencies || pkg.devDependencies) {
        const deps = { ...pkg.dependencies, ...pkg.devDependencies };
        
        if (deps.next) return "Next.js";
        if (deps.react) return "React";
        if (deps.vue) return "Vue.js";
        if (deps.express) return "Express";
        if (deps.nuxt) return "Nuxt.js";
        if (deps.gatsby) return "Gatsby";
        if (deps.angular || deps["@angular/core"]) return "Angular";
      }
      
      return "Node.js";
    }
    
    if (fs.existsSync(path.join(cwd, "requirements.txt")) || 
        fs.existsSync(path.join(cwd, "pyproject.toml"))) {
      return "Python";
    }
    
    if (fs.existsSync(path.join(cwd, "Cargo.toml"))) {
      return "Rust";
    }
    
    if (fs.existsSync(path.join(cwd, "go.mod"))) {
      return "Go";
    }
    
    if (fs.existsSync(path.join(cwd, "composer.json"))) {
      return "PHP";
    }
    
    return null;
  } catch {
    return null;
  }
}