@puberty-labs/clits
Version:
CLiTS (Chrome Logging and Inspection Tool Suite) is a powerful Node.js library for automated Chrome browser testing, logging, and inspection. It provides a comprehensive suite of tools for monitoring network requests, console logs, DOM mutations, and more
832 lines (831 loc) • 46.9 kB
JavaScript
// BSD: Entry point for the CLiTS-INSPECTOR CLI tool. Handles command-line arguments and orchestrates log/data extraction.
import { Command } from 'commander';
import { readFileSync } from 'fs';
import { PathResolver } from './utils/path-resolver.js';
import { ChromeExtractor } from './chrome-extractor.js';
import { ChromeAutomation } from './chrome-automation.js';
import { AIErrorHandler } from './utils/ai-error-handler.js';
import inquirer from 'inquirer';
// import tabtab from 'tabtab'; // Disabled to prevent automatic shell completion prompts
const pathResolver = PathResolver.getInstance();
const packageJson = JSON.parse(readFileSync(pathResolver.resolvePath('package.json'), 'utf8'));
const program = new Command();
async function main() {
program
.name('clits')
.description('CLI tool for extracting and sharing debugging data for AI and web projects (CLITS)')
.version(packageJson.version)
.addHelpText('after', `
Examples:
$ clits extract --chrome --interactive
$ clits extract --source ./logs --patterns "*.log" --output-file ./output.json
$ clits navigate --url "http://localhost:5173/displays" --wait-for ".displays-manager"
$ clits navigate --link-text "Display Manager" --wait-for ".displays-manager"
$ clits navigate --url-contains "display" --screenshot "navigation.png"
$ clits interact --click "[data-testid='edit-btn']" --wait-for ".edit-dialog" --capture-network
$ clits automate --script automation.json --monitor --save-results results.json
$ clits discover-links --chrome-port 9222
$ clits vision --screenshot --selector ".error-message" --output "error.png" --meta "error.json"
$ clits vision --screenshot --selectors ".error,.warning" --output-dir "./screenshots"
$ clits vision --screenshot --fullpage --output "page.png" --base64
`);
program
.command('extract')
.description('Extract debugging data from specified sources like local files or a running Chrome instance.')
.option('-s, --source <path>', 'Source directory or file path to extract logs from')
.option('-p, --patterns <patterns...>', 'File patterns to match (e.g., "*.log")')
.option('-m, --max-size <size>', 'Maximum file size in MB to process', '10')
.option('-f, --max-files <count>', 'Maximum number of files to process', '100')
.option('--chrome', 'Extract logs and other data from a running Chrome instance.')
.option('--chrome-host <host>', 'Specify the host for the Chrome DevTools protocol.', 'localhost')
.option('--chrome-port <port>', 'Specify the port for the Chrome DevTools protocol.', '9222')
.option('--no-network', 'Disable network log extraction from Chrome DevTools.')
.option('--no-console', 'Disable console log extraction from Chrome DevTools.')
.option('--log-levels <levels>', 'Comma-separated list of log levels to include (e.g., "error,warning").', 'error,warning,info,debug,log')
.option('--sources <sources>', 'Comma-separated list of log sources to include (e.g., "network,console").', 'network,console,devtools')
.option('--domains <domains>', 'Comma-separated list of domain patterns to filter network requests.')
.option('--keywords <keywords>', 'Comma-separated list of keywords to filter logs.')
.option('--exclude <patterns>', 'Comma-separated list of regex patterns to exclude logs.')
.option('--group-by-source', 'Group extracted logs by their source (e.g., network, console).')
.option('--group-by-level', 'Group extracted logs by their log level (e.g., error, warning).')
.option('--no-timestamps', 'Omit timestamps from the log output.')
.option('--no-stack-traces', 'Omit stack traces from error logs.')
.option('--output-file <path>', 'Path to save the extracted logs to a file.')
.option('--error-summary', 'Include a summary of error frequencies in the output.')
.option('--live-mode [duration]', 'Run in live mode, continuously extracting logs for a specified duration in seconds.', '60')
.option('--interactive-login', 'Prompt for manual login within the browser before extraction.')
.option('--target-id <id>', 'Specify a Chrome tab/page Target ID to connect to, skipping interactive selection.')
.option('-i, --interactive', 'Run in interactive mode to select monitoring options.')
.option('--json-errors', 'Output structured JSON error responses for AI processing')
.action(async (options) => {
try {
const errorHandler = AIErrorHandler.getInstance();
if (options.jsonErrors) {
errorHandler.enableJsonErrors();
}
const extractionOptions = { ...options };
if (options.interactive) {
const answers = await inquirer.prompt([
{
type: 'checkbox',
name: 'monitoring',
message: 'Select monitoring features to enable:',
choices: [
{ name: 'React Hook Monitoring', value: 'enableReactHookMonitoring' },
{ name: 'WebSocket Monitoring', value: 'includeWebSockets' },
{ name: 'JWT Monitoring', value: 'includeJwtMonitoring' },
{ name: 'GraphQL Monitoring', value: 'includeGraphqlMonitoring' },
{ name: 'Redux State Monitoring', value: 'includeReduxMonitoring' },
{ name: 'Performance Monitoring', value: 'includePerformanceMonitoring' },
{ name: 'Event Loop Monitoring', value: 'includeEventLoopMonitoring' },
{ name: 'User Interaction Recording', value: 'includeUserInteractionRecording' },
{ name: 'DOM Mutation Monitoring', value: 'includeDomMutationMonitoring' },
{ name: 'CSS Change Monitoring', value: 'includeCssChangeMonitoring' },
{ name: 'Headless Mode', value: 'headless' },
]
}
]);
answers.monitoring.forEach((feature) => {
extractionOptions[feature] = true;
});
}
// Chrome DevTools extraction
if (extractionOptions.chrome) {
const chromeExtractor = new ChromeExtractor({
port: parseInt(extractionOptions.chromePort),
host: extractionOptions.chromeHost,
includeNetwork: extractionOptions.network !== false,
includeConsole: extractionOptions.console !== false,
enableReactHookMonitoring: extractionOptions.enableReactHookMonitoring,
includeWebSockets: extractionOptions.includeWebSockets,
includeJwtMonitoring: extractionOptions.includeJwtMonitoring,
includeGraphqlMonitoring: extractionOptions.includeGraphqlMonitoring,
includeReduxMonitoring: extractionOptions.includeReduxMonitoring,
includePerformanceMonitoring: extractionOptions.includePerformanceMonitoring,
includeEventLoopMonitoring: extractionOptions.includeEventLoopMonitoring,
includeUserInteractionRecording: extractionOptions.includeUserInteractionRecording,
includeDomMutationMonitoring: extractionOptions.includeDomMutationMonitoring,
includeCssChangeMonitoring: extractionOptions.includeCssChangeMonitoring,
headless: extractionOptions.headless,
filters: {
logLevels: extractionOptions.logLevels?.split(','),
sources: extractionOptions.sources?.split(','),
domains: extractionOptions.domains?.split(','),
keywords: extractionOptions.keywords?.split(','),
excludePatterns: extractionOptions.exclude?.split(','),
},
format: {
groupBySource: extractionOptions.groupBySource,
groupByLevel: extractionOptions.groupByLevel,
includeTimestamp: extractionOptions.noTimestamps !== true,
includeStackTrace: extractionOptions.noStackTraces !== true,
},
reconnect: {
enabled: true, // Always enable reconnection for CLI usage
maxAttempts: 5,
delayBetweenAttemptsMs: 2000,
},
});
let targetId = extractionOptions.targetId;
if (!targetId) {
const targets = await chromeExtractor.getDebuggablePageTargets();
if (targets.length === 0) {
errorHandler.handleError('No debuggable Chrome tabs found', {
command: 'extract',
chromePort: extractionOptions.chromePort,
chromeHost: extractionOptions.chromeHost
}, 'Error: No debuggable Chrome tabs found. Please open a new tab in Chrome running with --remote-debugging-port=9222');
process.exit(1);
}
else if (targets.length === 1) {
targetId = targets[0].id;
console.log(`Automatically selected target: ${targets[0].title || targets[0].url}`);
}
else {
const choices = targets.map((t, index) => ({
name: `[${index + 1}] ${t.title || t.url} (ID: ${t.id})`,
value: t.id,
}));
const answer = await inquirer.prompt([
{
type: 'list',
name: 'selectedTargetId',
message: 'Multiple debuggable Chrome tabs found. Please select one:',
choices: choices,
},
]);
targetId = answer.selectedTargetId;
}
}
if (!targetId) {
errorHandler.handleError('No Chrome target selected', {
command: 'extract',
chromePort: extractionOptions.chromePort,
chromeHost: extractionOptions.chromeHost
}, 'Error: No Chrome target selected.');
process.exit(1);
}
console.log('[CLiTS-INSPECTOR] Starting Chrome DevTools extraction...');
const extractedLogs = await chromeExtractor.extract(targetId);
if (options.jsonErrors) {
errorHandler.handleSuccess(extractedLogs);
}
else {
console.log(JSON.stringify(extractedLogs, null, 2));
}
return;
}
// File system extraction
if (extractionOptions.source) {
const { exists, error } = pathResolver.validatePath(extractionOptions.source);
if (!exists) {
console.error(`[CLiTS-INSPECTOR] Error: Invalid source path. ${error}. Please ensure the path is correct and accessible.`);
process.exit(1);
}
console.log(`Extracting logs from ${extractionOptions.source}...`);
// TODO: Implement file system extraction
return;
}
console.error('[CLiTS-INSPECTOR] Error: No extraction source specified. Use --source for files or --chrome for Chrome DevTools.');
process.exit(1);
}
catch (error) {
const errorHandler = AIErrorHandler.getInstance();
if (options.jsonErrors) {
errorHandler.enableJsonErrors();
}
if (error instanceof Error) {
errorHandler.handleError(error, {
command: 'extract',
chromePort: options.chromePort,
chromeHost: options.chromeHost
}, `[CLiTS-INSPECTOR] An error occurred during extraction: ${error.message}`);
// Add more specific error handling here if needed
if (!options.jsonErrors && (error.message.includes('Chrome target with ID') || error.message.includes('No debuggable Chrome tabs found'))) {
console.error('[CLiTS-INSPECTOR] Please ensure Chrome is running with remote debugging enabled (--remote-debugging-port=9222) and a debuggable tab is open.');
}
}
else {
errorHandler.handleError(String(error), {
command: 'extract',
chromePort: options.chromePort,
chromeHost: options.chromeHost
}, `[CLiTS-INSPECTOR] An unexpected error occurred: ${String(error)}`);
}
process.exit(1);
}
});
// Navigate command
program
.command('navigate')
.description('Navigate to URLs and wait for elements')
.option('--url <url>', 'Navigate to specific URL')
.option('--link-text <text>', 'Navigate by finding link with matching text (fuzzy matching)')
.option('--url-contains <pattern>', 'Navigate by finding link with URL containing pattern')
.option('--tab <index>', 'Navigate in specific tab by index (0-based)')
.option('--wait-for <selector>', 'Wait for CSS selector to appear')
.option('--timeout <ms>', 'Timeout in milliseconds', '30000')
.option('--screenshot <path>', 'Take screenshot after navigation')
.option('--chrome-host <host>', 'Specify the host for the Chrome DevTools protocol', 'localhost')
.option('--chrome-port <port>', 'Specify the port for the Chrome DevTools protocol', '9222')
.option('--json-errors', 'Output structured JSON error responses for AI processing')
.action(async (options) => {
try {
// Validate that at least one navigation method is provided
if (!options.url && !options.linkText && !options.urlContains) {
console.error('[CLiTS-NAVIGATOR] Error: Must specify one of --url, --link-text, or --url-contains');
process.exit(1);
}
// If using link-text or url-contains, use clits-inspect to find and navigate
if (options.linkText || options.urlContains) {
const { spawn } = await import('child_process');
const { fileURLToPath } = await import('url');
const { dirname, resolve } = await import('path');
// Build the clits-inspect command
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const inspectPath = resolve(__dirname, 'cli-inspect.js');
const args = [
inspectPath,
'--auto',
'--json',
'--port', options.chromePort,
'--host', options.chromeHost
];
if (options.linkText) {
args.push('--action', 'navigate-by-text', '--link-text', options.linkText);
}
else if (options.urlContains) {
args.push('--action', 'navigate-by-url', '--url-contains', options.urlContains);
}
const inspectProcess = spawn('node', args, { stdio: 'pipe' });
let output = '';
let error = '';
inspectProcess.stdout.on('data', (data) => {
output += data.toString();
});
inspectProcess.stderr.on('data', (data) => {
error += data.toString();
});
inspectProcess.on('close', async (code) => {
if (code === 0) {
try {
const result = JSON.parse(output);
if (result.success && result.navigated) {
console.log(`[CLiTS-NAVIGATOR] Successfully navigated via ${result.navigated.method}: ${result.navigated.text} -> ${result.navigated.url}`);
// If wait-for or screenshot options are provided, handle them with ChromeAutomation
if (options.waitFor || options.screenshot) {
const automation = new ChromeAutomation(parseInt(options.chromePort), options.chromeHost);
// Use current URL since we already navigated
await automation.navigate({
url: result.navigated.url,
waitForSelector: options.waitFor,
timeout: parseInt(options.timeout),
screenshotPath: options.screenshot,
chromePort: parseInt(options.chromePort),
chromeHost: options.chromeHost
});
if (options.waitFor) {
console.log(`[CLiTS-NAVIGATOR] Element found: ${options.waitFor}`);
}
if (options.screenshot) {
console.log(`[CLiTS-NAVIGATOR] Screenshot saved: ${options.screenshot}`);
}
}
}
else {
console.error(`[CLiTS-NAVIGATOR] Navigation failed: ${result.error || 'Unknown error'}`);
process.exit(1);
}
}
catch (parseError) {
console.error('[CLiTS-NAVIGATOR] Failed to parse navigation result:', output);
process.exit(1);
}
}
else {
console.error('[CLiTS-NAVIGATOR] Navigation failed:', error);
process.exit(1);
}
});
return;
}
// Traditional URL-based navigation
const automation = new ChromeAutomation(parseInt(options.chromePort), options.chromeHost);
// Handle tab switching before navigation if specified
if (options.tab !== undefined) {
const tabIndex = parseInt(options.tab);
await automation.switchToTab(tabIndex);
console.log(`[CLiTS-NAVIGATOR] Switched to tab ${tabIndex} before navigation`);
}
const navigationResult = await automation.navigate({
url: options.url,
waitForSelector: options.waitFor,
timeout: parseInt(options.timeout),
screenshotPath: options.screenshot,
chromePort: parseInt(options.chromePort),
chromeHost: options.chromeHost
});
console.log(`[CLiTS-NAVIGATOR] Successfully navigated to: ${navigationResult.actualUrl}`);
}
catch (error) {
console.error(`[CLiTS-NAVIGATOR] Navigation failed: ${error instanceof Error ? error.message : String(error)}`);
process.exit(1);
}
});
// Interact command
program
.command('interact')
.description('Interact with page elements and capture visual state')
.option('--click <selector>', 'Click on element matching CSS selector')
.option('--click-text <text>', 'Click element containing specific text')
.option('--by-text <text>', 'Click element containing specific text (alias for --click-text)')
.option('--click-color <color>', 'Click element with specific color (hex, rgb, or name)')
.option('--click-region <region>', 'Click by screen region (top-left, top-right, bottom-left, bottom-right, center)')
.option('--click-description <description>', 'Click by visual description (experimental)')
.option('--type <selector> <text>', 'Type text into input field')
.option('--toggle <selector>', 'Toggle switch/checkbox elements')
.option('--wait-for <selector>', 'Wait for element after interaction')
.option('--timeout <ms>', 'Timeout in milliseconds', '30000')
.option('--capture-network', 'Capture network requests during interaction')
.option('--screenshot [path]', 'Take screenshot after interaction (optional file path)')
.option('--base64', 'Output screenshot as base64 to stdout')
.option('--stdout', 'Output results to stdout (JSON format)')
.option('--with-metadata', 'Include element positions and text in screenshot data')
.option('--annotated', 'Add visual annotations (boxes around clickable elements)')
.option('--selector-map', 'Output map of clickable elements with coordinates')
.option('--fullpage', 'Take full-page screenshot instead of viewport')
// NEW: Tab Management Commands
.option('--switch-tab <index>', 'Switch to tab by index (0-based)')
.option('--tab-next', 'Switch to next tab')
.option('--tab-prev', 'Switch to previous tab')
// NEW: Keyboard Commands
.option('--key <key>', 'Send keyboard key or shortcut (e.g., "F5", "cmd+r", "escape")')
.option('--keys <keys>', 'Send multiple keyboard keys or shortcuts (alias for --key)')
.option('--chrome-host <host>', 'Specify the host for the Chrome DevTools protocol', 'localhost')
.option('--chrome-port <port>', 'Specify the port for the Chrome DevTools protocol', '9222')
.option('--json-errors', 'Output structured JSON error responses for AI processing')
.action(async (options) => {
try {
const errorHandler = AIErrorHandler.getInstance();
if (options.jsonErrors) {
errorHandler.enableJsonErrors();
}
const automation = new ChromeAutomation(parseInt(options.chromePort), options.chromeHost);
// Parse type command if provided
let typeSelector;
let typeText;
if (options.type) {
const typeArgs = options.type.split(' ');
if (typeArgs.length >= 2) {
typeSelector = typeArgs[0];
typeText = typeArgs.slice(1).join(' ');
}
}
// Handle visual element selection methods
let clickSelector = options.click;
let useJavaScriptExpression = false;
let jsExpression = '';
// Support both --click-text and --by-text (alias)
const textToClick = options.clickText || options.byText;
if (textToClick) {
// Use JavaScript evaluation for text-based selection
jsExpression = await automation.findElementByText(textToClick);
useJavaScriptExpression = true;
clickSelector = '__JS_EXPRESSION__'; // Special marker for JS evaluation
}
else if (options.clickColor) {
// Use JavaScript evaluation for color-based selection
jsExpression = await automation.findElementByColor(options.clickColor);
useJavaScriptExpression = true;
clickSelector = '__JS_EXPRESSION__'; // Special marker for JS evaluation
}
else if (options.clickRegion) {
// Use JavaScript evaluation for region-based selection
jsExpression = await automation.findElementByRegion(options.clickRegion);
useJavaScriptExpression = true;
clickSelector = '__JS_EXPRESSION__'; // Special marker for JS evaluation
}
else if (options.clickDescription) {
// Use JavaScript evaluation for description-based selection
jsExpression = await automation.findElementByDescription(options.clickDescription);
useJavaScriptExpression = true;
clickSelector = '__JS_EXPRESSION__'; // Special marker for JS evaluation
}
// Enhanced interaction options
const interactionResult = await automation.interact({
clickSelector,
typeSelector,
typeText,
toggleSelector: options.toggle,
waitForSelector: options.waitFor,
timeout: parseInt(options.timeout),
captureNetwork: options.captureNetwork,
screenshotPath: typeof options.screenshot === 'string' ? options.screenshot : undefined,
takeScreenshot: options.screenshot !== undefined,
base64Output: options.base64,
fullPageScreenshot: options.fullpage,
withMetadata: options.withMetadata,
annotated: options.annotated,
selectorMap: options.selectorMap,
chromePort: parseInt(options.chromePort),
chromeHost: options.chromeHost,
// Pass JavaScript expression for advanced element selection
useJavaScriptExpression: useJavaScriptExpression,
jsExpression: jsExpression,
// NEW: Tab Management
switchTabIndex: options.switchTab !== undefined ? parseInt(options.switchTab) : undefined,
tabNext: options.tabNext,
tabPrev: options.tabPrev,
// NEW: Keyboard Commands
keyCommand: options.key || options.keys
});
// Handle output format
if (options.stdout || options.base64) {
console.log(JSON.stringify(interactionResult, null, 2));
}
else {
console.log('[CLiTS-INTERACTOR] Interaction completed successfully');
if (interactionResult.screenshotPath) {
console.log(`[CLiTS-INTERACTOR] Screenshot saved: ${interactionResult.screenshotPath}`);
}
if (interactionResult.selectorMap) {
console.log(`[CLiTS-INTERACTOR] Found ${interactionResult.selectorMap.length} clickable elements`);
}
}
}
catch (error) {
const errorHandler = AIErrorHandler.getInstance();
if (options.jsonErrors) {
errorHandler.enableJsonErrors();
}
errorHandler.handleError(error instanceof Error ? error : String(error), {
command: 'interact',
chromePort: options.chromePort,
chromeHost: options.chromeHost,
selector: options.click || options.clickText || options.byText || options.toggle
}, `[CLiTS-INTERACTOR] Interaction failed: ${error instanceof Error ? error.message : String(error)}`);
process.exit(1);
}
});
// Automate command
program
.command('automate')
.alias('automation')
.description('Run automation scripts')
.option('--script <path>', 'JSON file with automation steps')
.option('--stdin', 'Read automation script from stdin')
.option('--monitor', 'Enable monitoring during automation')
.option('--save-results <path>', 'Save results to file')
.option('--chrome-host <host>', 'Specify the host for the Chrome DevTools protocol', 'localhost')
.option('--chrome-port <port>', 'Specify the port for the Chrome DevTools protocol', '9222')
.action(async (options) => {
try {
const automation = new ChromeAutomation(parseInt(options.chromePort), options.chromeHost);
let scriptContent;
if (options.stdin) {
// Read from stdin
const chunks = [];
for await (const chunk of process.stdin) {
chunks.push(chunk);
}
scriptContent = Buffer.concat(chunks).toString();
}
else if (options.script) {
// Read from file
const fs = await import('fs');
scriptContent = fs.readFileSync(options.script, 'utf8');
}
else {
throw new Error('Either --script <path> or --stdin must be provided');
}
// Validate the script
JSON.parse(scriptContent); // Just validate, don't store
// Create a temporary file if reading from stdin
let tempScriptPath = options.script;
if (options.stdin) {
const os = await import('os');
const path = await import('path');
const fs = await import('fs');
tempScriptPath = path.join(os.tmpdir(), `clits_automation_${Date.now()}.json`);
fs.writeFileSync(tempScriptPath, scriptContent);
}
const result = await automation.runAutomation({
scriptPath: tempScriptPath,
monitor: options.monitor,
saveResultsPath: options.saveResults,
chromePort: parseInt(options.chromePort),
chromeHost: options.chromeHost
});
// Clean up temp file if created
if (options.stdin && tempScriptPath !== options.script) {
const fs = await import('fs');
try {
fs.unlinkSync(tempScriptPath);
}
catch (e) {
// Ignore cleanup errors
}
}
if (result.success) {
console.log(`[CLiTS-AUTOMATOR] Automation completed successfully: ${result.completedSteps}/${result.totalSteps} steps`);
// Output results to console for testing
console.log(JSON.stringify(result, null, 2));
}
else {
console.error(`[CLiTS-AUTOMATOR] Automation failed: ${result.error}`);
console.error(`[CLiTS-AUTOMATOR] Completed ${result.completedSteps}/${result.totalSteps} steps`);
process.exit(1);
}
}
catch (error) {
console.error(`[CLiTS-AUTOMATOR] Automation failed: ${error instanceof Error ? error.message : String(error)}`);
process.exit(1);
}
});
// Inspect command - Interactive website inspector with Chrome Remote Control
program
.command('inspect')
.description('Interactive website inspector with Chrome Remote Control and selector discovery')
.option('--find-selectors', 'List all available CSS selectors on the page')
.option('--find-clickable', 'List all clickable elements with coordinates')
.option('--element-map', 'Generate visual map of page elements')
.option('--tabs', 'List all open browser tabs with URLs and titles')
.option('--chrome-host <host>', 'Specify the host for the Chrome DevTools protocol', 'localhost')
.option('--chrome-port <port>', 'Specify the port for the Chrome DevTools protocol', '9222')
.option('--output-format <format>', 'Output format: json, table, or interactive', 'interactive')
.action(async (options) => {
try {
// Handle discovery tools
if (options.findSelectors || options.findClickable || options.elementMap || options.tabs) {
const automation = new ChromeAutomation(parseInt(options.chromePort), options.chromeHost);
if (options.findSelectors) {
const selectors = await automation.discoverAllSelectors();
if (options.outputFormat === 'json') {
console.log(JSON.stringify({ selectors }, null, 2));
}
else {
console.log('[CLiTS-INSPECTOR] Available CSS Selectors:');
selectors.forEach((selector, index) => {
console.log(` ${index + 1}. ${selector}`);
});
}
}
if (options.findClickable) {
const interactionResult = await automation.interact({
selectorMap: true,
chromePort: parseInt(options.chromePort),
chromeHost: options.chromeHost
});
if (options.outputFormat === 'json') {
console.log(JSON.stringify({ clickableElements: interactionResult.selectorMap }, null, 2));
}
else {
console.log('[CLiTS-INSPECTOR] Clickable Elements:');
interactionResult.selectorMap?.forEach((element, index) => {
console.log(` ${index + 1}. ${element.selector}`);
console.log(` Text: "${element.text}"`);
console.log(` Coordinates: (${element.coordinates.x}, ${element.coordinates.y})`);
console.log(` Bounding Box: ${element.boundingBox.width}x${element.boundingBox.height}`);
console.log('');
});
}
}
if (options.elementMap) {
const elementMap = await automation.generateElementMap();
if (options.outputFormat === 'json') {
console.log(JSON.stringify({ elementMap }, null, 2));
}
else {
console.log('[CLiTS-INSPECTOR] Element Map:');
elementMap.forEach((element, index) => {
console.log(` ${index + 1}. ${element.tag} - ${element.selector}`);
if (element.text)
console.log(` Text: "${element.text}"`);
console.log(` Position: (${element.x}, ${element.y})`);
console.log('');
});
}
}
if (options.tabs) {
const tabs = await automation.listBrowserTabs();
if (options.outputFormat === 'json') {
console.log(JSON.stringify({ tabs }, null, 2));
}
else {
console.log('[CLiTS-INSPECTOR] Browser Tabs:');
tabs.forEach((tab, index) => {
console.log(` ${index}. ${tab.title || 'Untitled'}`);
console.log(` URL: ${tab.url}`);
console.log(` ID: ${tab.id}`);
if (tab.type !== 'page')
console.log(` Type: ${tab.type}`);
console.log('');
});
}
}
return;
}
// Original interactive inspector
const { main: runInspect } = await import('./cli-inspect.js');
await runInspect();
}
catch (error) {
console.error(`[CLiTS-INSPECTOR] Inspect failed: ${error instanceof Error ? error.message : String(error)}`);
process.exit(1);
}
});
// Discover Links command - convenience command for discovering navigation links
program
.command('discover-links')
.description('Discover all navigation links on the current page')
.option('--chrome-host <host>', 'Specify the host for the Chrome DevTools protocol', 'localhost')
.option('--chrome-port <port>', 'Specify the port for the Chrome DevTools protocol', '9222')
.option('--verbose', 'Enable verbose output')
.action(async (options) => {
try {
const { spawn } = await import('child_process');
const { fileURLToPath } = await import('url');
const { dirname, resolve } = await import('path');
// Build the clits-inspect command
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const inspectPath = resolve(__dirname, 'cli-inspect.js');
const args = [
inspectPath,
'--auto',
'--json',
'--action', 'discover-links',
'--port', options.chromePort,
'--host', options.chromeHost
];
if (options.verbose) {
args.push('--verbose');
}
const inspectProcess = spawn('node', args, { stdio: 'pipe' });
let output = '';
let error = '';
inspectProcess.stdout.on('data', (data) => {
output += data.toString();
});
inspectProcess.stderr.on('data', (data) => {
error += data.toString();
});
inspectProcess.on('close', (code) => {
if (code === 0) {
try {
const result = JSON.parse(output);
console.log(JSON.stringify(result, null, 2));
}
catch (parseError) {
console.log(output);
}
}
else {
console.error('[CLiTS-DISCOVER-LINKS] Discovery failed:', error);
process.exit(1);
}
});
}
catch (error) {
console.error(`[CLiTS-DISCOVER-LINKS] Discovery failed: ${error instanceof Error ? error.message : String(error)}`);
process.exit(1);
}
});
// Discover Tabs command - convenience command for discovering tabs in dialogs
program
.command('discover-tabs')
.description('Discover all tab labels in dialogs or tabbed interfaces')
.option('--chrome-host <host>', 'Specify the host for the Chrome DevTools protocol', 'localhost')
.option('--chrome-port <port>', 'Specify the port for the Chrome DevTools protocol', '9222')
.option('--tab-label <label>', 'Select tab by label after discovery')
.option('--tab-label-regex <pattern>', 'Select tab by regex pattern')
.option('--custom-save-patterns <patterns>', 'Custom save button text patterns (comma-separated)')
.option('--find-save-button', 'Also discover the best save button in the current dialog')
.option('--verbose', 'Enable verbose output')
.action(async (options) => {
try {
const automation = new ChromeAutomation(parseInt(options.chromePort), options.chromeHost);
// Use the existing interact method for tab discovery and selection
const result = await automation.interact({
discoverTabs: true,
findSaveButton: options.findSaveButton,
customSavePatterns: options.customSavePatterns ?
options.customSavePatterns.split(',').map((p) => p.trim()) :
undefined,
clickSelector: options.tabLabel || options.tabLabelRegex,
tabLabelPattern: options.tabLabelRegex,
chromePort: parseInt(options.chromePort),
chromeHost: options.chromeHost
});
console.log(JSON.stringify(result, null, 2));
}
catch (error) {
console.error(`[CLiTS-DISCOVER-TABS] Discovery failed: ${error instanceof Error ? error.message : String(error)}`);
process.exit(1);
}
});
// Direct Chrome Remote Control command - bypasses Playwright
program
.command('chrome-control')
.description('Direct Chrome Remote Control (no Playwright) - works with existing Chrome debugging session')
.option('--host <host>', 'Specify the host for the Chrome DevTools protocol', 'localhost')
.option('--port <port>', 'Specify the port for the Chrome DevTools protocol', '9222')
.option('--chrome-host <host>', 'Specify the host for the Chrome DevTools protocol (alias)', 'localhost')
.option('--chrome-port <port>', 'Specify the port for the Chrome DevTools protocol (alias)', '9222')
.option('--timeout <ms>', 'Exit after specified timeout (for testing)', '0')
.action(async (options) => {
try {
// Support both --port/--host and --chrome-port/--chrome-host for compatibility
const port = parseInt(options.port || options.chromePort || '9222');
const host = options.host || options.chromeHost || 'localhost';
const timeout = parseInt(options.timeout);
// If timeout is specified, exit after that duration
if (timeout > 0) {
setTimeout(() => {
console.log(`[CLiTS-CHROME-CONTROL] Exiting after ${timeout}ms timeout`);
process.exit(0);
}, timeout);
}
// Import and run the direct Chrome control
const { directChromeControl } = await import('./cli-inspect.js');
await directChromeControl(port, host);
}
catch (error) {
console.error(`[CLiTS-CHROME-CONTROL] Chrome control failed: ${error instanceof Error ? error.message : String(error)}`);
process.exit(1);
}
});
// VisionCLITS - Advanced visual state capture and screenshot automation
program
.command('vision')
.description('Visual state capture and screenshot automation')
.option('--screenshot', 'Take screenshot(s)')
.option('--selector <selector>', 'CSS selector for element-specific screenshot')
.option('--selectors <selectors>', 'Multiple CSS selectors (comma-separated)')
.option('--output <path>', 'Output file path for screenshot')
.option('--output-dir <dir>', 'Output directory for multiple screenshots')
.option('--meta <path>', 'Output JSON metadata file path')
.option('--fullpage', 'Take full-page screenshot')
.option('--base64', 'Output screenshot as base64 to stdout')
.option('--stdout', 'Output results to stdout (JSON format)')
.option('--include-text', 'Include text content in metadata')
.option('--include-styles', 'Include computed styles in metadata')
.option('--include-bbox', 'Include bounding box information')
.option('--include-visibility', 'Include visibility state information')
.option('--diff', 'Enable visual diff mode for regression testing')
.option('--baseline <path>', 'Baseline screenshot or directory for comparison')
.option('--compare-with <path>', 'Compare current screenshot with this image')
.option('--diff-threshold <number>', 'Diff sensitivity threshold (0-1, default: 0.1)', '0.1')
.option('--diff-output <path>', 'Output path for diff result image')
.option('--diff-report <path>', 'Output path for diff analysis JSON report')
.option('--save-baseline', 'Save current screenshot as new baseline')
.option('--batch-diff', 'Enable batch processing for multiple screenshot comparisons')
.option('--video', 'Enable video recording for interaction workflows')
.option('--video-output <path>', 'Output path for recorded video (default: clits-recording.webm)')
.option('--video-duration <seconds>', 'Recording duration in seconds (default: 30)', '30')
.option('--video-fps <fps>', 'Video frame rate (default: 10)', '10')
.option('--highlight', 'Add visual annotations to screenshots')
.option('--highlight-color <color>', 'Color for element highlighting (hex, default: #ff0000)', '#ff0000')
.option('--highlight-thickness <pixels>', 'Border thickness for highlighting (default: 3)', '3')
.option('--highlight-all-clickable', 'Highlight all clickable elements on the page')
.option('--annotate-text', 'Add text labels to highlighted elements')
.option('--chrome-host <host>', 'Specify the host for the Chrome DevTools protocol', 'localhost')
.option('--chrome-port <port>', 'Specify the port for the Chrome DevTools protocol', '9222')
.option('--timeout <ms>', 'Timeout in milliseconds', '30000')
.action(async (options) => {
try {
// Dynamic import to handle module resolution issues
let VisionHandler;
try {
const visionModule = await import('./vision-handler.js');
VisionHandler = visionModule.VisionHandler;
}
catch (importError) {
console.error('[CLiTS-VISION] Vision handler not available. Please ensure the module is built properly.');
process.exit(1);
}
const visionHandler = new VisionHandler(parseInt(options.chromePort), options.chromeHost);
await visionHandler.execute(options);
}
catch (error) {
console.error(`[CLiTS-VISION] Vision command failed: ${error instanceof Error ? error.message : String(error)}`);
process.exit(1);
}
});
// Add command completion (optional - only if explicitly requested)
// Temporarily disabled to debug option parsing issues
// program
// .command('completion')
// .description('Generate completion script for your shell.')
// .action(async () => {
// console.log('Shell completion is currently disabled to prevent automatic prompts.');
// console.log('To enable completion, uncomment the tabtab code in src/cli.ts');
// });
await program.parseAsync();
}
main().catch((error) => {
console.error('[CLiTS-INSPECTOR] A fatal error occurred outside the command handler:', error);
process.exit(1);
});