@cli4ai/lib
Advanced tools
+2
-9
| { | ||
| "name": "@cli4ai/lib", | ||
| "version": "1.0.8", | ||
| "version": "1.0.9", | ||
| "description": "Shared CLI framework for cli4ai tools", | ||
@@ -16,8 +16,3 @@ "author": "cliforai", | ||
| }, | ||
| "./cli.ts": { | ||
| "types": "./dist/cli.d.ts", | ||
| "import": "./dist/cli.js", | ||
| "default": "./dist/cli.js" | ||
| }, | ||
| "./browser.ts": { | ||
| "./browser": { | ||
| "types": "./dist/browser.d.ts", | ||
@@ -56,4 +51,2 @@ "import": "./dist/browser.js", | ||
| "files": [ | ||
| "cli.ts", | ||
| "browser.ts", | ||
| "dist" | ||
@@ -60,0 +53,0 @@ ], |
-147
| /** | ||
| * Shared browser utilities for packages that depend on @cli4ai/chrome | ||
| * Auto-launches managed browser if not running | ||
| */ | ||
| import puppeteer, { Browser, Page } from 'puppeteer'; | ||
| import { readFileSync, writeFileSync, existsSync, unlinkSync, mkdirSync } from 'fs'; | ||
| import { join } from 'path'; | ||
| import { homedir } from 'os'; | ||
| // Persistent storage paths (same as chrome package) | ||
| const CLI4AI_DIR = join(homedir(), '.cli4ai'); | ||
| const CHROME_DIR = join(CLI4AI_DIR, 'chrome'); | ||
| const PROFILE_DIR = join(CHROME_DIR, 'profile'); | ||
| const WS_FILE = join(CHROME_DIR, 'ws-endpoint'); | ||
| const PID_FILE = join(CHROME_DIR, 'pid'); | ||
| function ensureDirs() { | ||
| if (!existsSync(CLI4AI_DIR)) mkdirSync(CLI4AI_DIR, { recursive: true }); | ||
| if (!existsSync(CHROME_DIR)) mkdirSync(CHROME_DIR, { recursive: true }); | ||
| } | ||
| function isBrowserRunning(): boolean { | ||
| if (!existsSync(PID_FILE)) return false; | ||
| const pid = parseInt(readFileSync(PID_FILE, 'utf-8').trim(), 10); | ||
| try { | ||
| process.kill(pid, 0); | ||
| return true; | ||
| } catch { | ||
| cleanup(); | ||
| return false; | ||
| } | ||
| } | ||
| function cleanup() { | ||
| if (existsSync(WS_FILE)) unlinkSync(WS_FILE); | ||
| if (existsSync(PID_FILE)) unlinkSync(PID_FILE); | ||
| } | ||
| async function launchBrowser(headless: boolean): Promise<Browser> { | ||
| ensureDirs(); | ||
| const browser = await puppeteer.launch({ | ||
| headless: headless ? 'shell' : false, | ||
| userDataDir: PROFILE_DIR, | ||
| args: [ | ||
| '--no-first-run', | ||
| '--no-default-browser-check', | ||
| '--disable-infobars', | ||
| '--disable-blink-features=AutomationControlled', | ||
| '--disable-features=IsolateOrigins,site-per-process', | ||
| ], | ||
| defaultViewport: null, | ||
| ignoreDefaultArgs: ['--enable-automation'], | ||
| }); | ||
| // Remove automation detection signals | ||
| const userAgent = process.platform === 'win32' | ||
| ? 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36' | ||
| : 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36'; | ||
| const pages = await browser.pages(); | ||
| for (const page of pages) { | ||
| await page.setUserAgent(userAgent); | ||
| await page.evaluateOnNewDocument(() => { | ||
| Object.defineProperty(navigator, 'webdriver', { get: () => undefined }); | ||
| }); | ||
| } | ||
| const wsEndpoint = browser.wsEndpoint(); | ||
| writeFileSync(WS_FILE, wsEndpoint); | ||
| const browserProcess = browser.process(); | ||
| if (browserProcess?.pid) { | ||
| writeFileSync(PID_FILE, String(browserProcess.pid)); | ||
| } | ||
| return browser; | ||
| } | ||
| /** | ||
| * Get or create browser instance | ||
| * Auto-launches if not running | ||
| */ | ||
| export async function getBrowser(headless = false): Promise<Browser> { | ||
| if (isBrowserRunning() && existsSync(WS_FILE)) { | ||
| try { | ||
| const ws = readFileSync(WS_FILE, 'utf-8').trim(); | ||
| return await puppeteer.connect({ browserWSEndpoint: ws }); | ||
| } catch { | ||
| cleanup(); | ||
| } | ||
| } | ||
| return launchBrowser(headless); | ||
| } | ||
| /** | ||
| * Get a page from the browser | ||
| */ | ||
| export async function getPage(browser: Browser, newTab = false): Promise<Page> { | ||
| if (newTab) { | ||
| return browser.newPage(); | ||
| } | ||
| const pages = await browser.pages(); | ||
| return pages[pages.length - 1] || browser.newPage(); | ||
| } | ||
| /** | ||
| * Execute a function with a page, handling browser connection | ||
| */ | ||
| export async function withPage<T>( | ||
| fn: (page: Page) => Promise<T>, | ||
| options: { headless?: boolean; newTab?: boolean } = {} | ||
| ): Promise<T> { | ||
| const browser = await getBrowser(options.headless ?? false); | ||
| const page = await getPage(browser, options.newTab ?? false); | ||
| try { | ||
| return await fn(page); | ||
| } finally { | ||
| browser.disconnect(); | ||
| } | ||
| } | ||
| /** | ||
| * Execute a function with a new tab | ||
| */ | ||
| export async function withNewPage<T>( | ||
| fn: (page: Page) => Promise<T>, | ||
| headless = false | ||
| ): Promise<T> { | ||
| const browser = await getBrowser(headless); | ||
| const page = await browser.newPage(); | ||
| try { | ||
| return await fn(page); | ||
| } finally { | ||
| await page.close(); | ||
| browser.disconnect(); | ||
| } | ||
| } | ||
| /** | ||
| * Check if browser is currently running | ||
| */ | ||
| export function isRunning(): boolean { | ||
| return isBrowserRunning(); | ||
| } | ||
| export { Browser, Page }; |
-299
| /** | ||
| * cli4ai - cli4ai.com | ||
| * Standardized CLI framework for AI agent tools | ||
| */ | ||
| import { Command } from 'commander'; | ||
| import { resolve } from 'path'; | ||
| import { existsSync, readFileSync } from 'fs'; | ||
| const BRAND = 'cli4ai - cli4ai.com'; | ||
| // ═══════════════════════════════════════════════════════════════════════════ | ||
| // TYPES | ||
| // ═══════════════════════════════════════════════════════════════════════════ | ||
| export interface CLIError { | ||
| error: string; | ||
| message: string; | ||
| [key: string]: unknown; | ||
| } | ||
| export type CommandFn = (...args: string[]) => Promise<void> | void; | ||
| // ═══════════════════════════════════════════════════════════════════════════ | ||
| // ENV LOADING | ||
| // ═══════════════════════════════════════════════════════════════════════════ | ||
| let envLoaded = false; | ||
| export interface LoadEnvOptions { | ||
| cwd?: string; | ||
| searchParents?: boolean; | ||
| maxDepth?: number; | ||
| } | ||
| /** | ||
| * Load .env file from disk (explicit opt-in). | ||
| * | ||
| * SECURITY NOTE: Prefer `cli4ai secrets set <key>` over loading .env files. | ||
| */ | ||
| export function loadEnv(options: LoadEnvOptions = {}): void { | ||
| if (envLoaded) return; | ||
| envLoaded = true; | ||
| const cwd = options.cwd ?? process.cwd(); | ||
| const searchParents = options.searchParents ?? false; | ||
| const maxDepth = options.maxDepth ?? (searchParents ? 5 : 0); | ||
| // Start from current working directory (optionally walk parents) | ||
| let dir = cwd; | ||
| for (let i = 0; i <= maxDepth; i++) { | ||
| const envPath = resolve(dir, '.env'); | ||
| if (existsSync(envPath)) { | ||
| try { | ||
| const content = readFileSync(envPath, 'utf-8'); | ||
| for (const line of content.split('\n')) { | ||
| const trimmed = line.trim(); | ||
| if (trimmed && !trimmed.startsWith('#')) { | ||
| const eqIndex = trimmed.indexOf('='); | ||
| if (eqIndex > 0) { | ||
| const key = trimmed.slice(0, eqIndex).trim(); | ||
| const value = trimmed.slice(eqIndex + 1).trim(); | ||
| if (!process.env[key]) { | ||
| process.env[key] = value; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| return; | ||
| } catch {} | ||
| } | ||
| if (!searchParents) return; | ||
| dir = resolve(dir, '..'); | ||
| } | ||
| } | ||
| // ═══════════════════════════════════════════════════════════════════════════ | ||
| // ENV VALIDATION | ||
| // ═══════════════════════════════════════════════════════════════════════════ | ||
| // SECURITY NOTE: We intentionally do NOT auto-load .env files from the filesystem. | ||
| // This prevents supply chain attacks where malicious .env files in parent directories | ||
| // could inject credentials or override security settings. | ||
| // | ||
| // Use `cli4ai secrets set <key>` for secure credential storage, or set environment | ||
| // variables explicitly in your shell/CI environment. | ||
| /** | ||
| * Require environment variables to be set. Exits with parseable error if missing. | ||
| */ | ||
| export function requireEnv(...variables: string[]): void { | ||
| const missing = variables.filter(v => !process.env[v]); | ||
| if (missing.length > 0) { | ||
| outputError('ENV_MISSING', `Missing required environment variables: ${missing.join(', ')}`, { | ||
| variables: missing, | ||
| hint: 'Use "cli4ai secrets set <key>" to store securely, or set in your shell environment' | ||
| }); | ||
| } | ||
| } | ||
| /** | ||
| * Get env var or exit with error | ||
| */ | ||
| export function env(name: string): string { | ||
| const value = process.env[name]; | ||
| if (!value) { | ||
| outputError('ENV_MISSING', `Missing required environment variable: ${name}`, { | ||
| variables: [name], | ||
| hint: 'Use "cli4ai secrets set ' + name + '" to store securely, or set in your shell environment' | ||
| }); | ||
| } | ||
| return value; | ||
| } | ||
| /** | ||
| * Get env var or return default value | ||
| */ | ||
| export function envOr(name: string, defaultValue: string): string { | ||
| return process.env[name] || defaultValue; | ||
| } | ||
| // ═══════════════════════════════════════════════════════════════════════════ | ||
| // OUTPUT | ||
| // ═══════════════════════════════════════════════════════════════════════════ | ||
| const EXIT_CODES: Record<string, number> = { | ||
| NOT_FOUND: 2, | ||
| INVALID_INPUT: 3, | ||
| ENV_MISSING: 4, | ||
| MANIFEST_ERROR: 4, | ||
| INSTALL_ERROR: 4, | ||
| AUTH_FAILED: 6, | ||
| NETWORK_ERROR: 7, | ||
| RATE_LIMITED: 8, | ||
| TIMEOUT: 9, | ||
| PARSE_ERROR: 10, | ||
| NPM_ERROR: 11, | ||
| }; | ||
| function getExitCode(code: string): number { | ||
| return EXIT_CODES[code] ?? 1; | ||
| } | ||
| /** | ||
| * Output JSON data to stdout | ||
| */ | ||
| export function output(data: unknown): void { | ||
| console.log(JSON.stringify(data, null, 2)); | ||
| } | ||
| /** | ||
| * Output error and exit. Format is parseable JSON. | ||
| */ | ||
| export function outputError( | ||
| code: string, | ||
| message: string, | ||
| details?: Record<string, unknown> | ||
| ): never { | ||
| console.error(JSON.stringify({ | ||
| error: code, | ||
| message, | ||
| ...details | ||
| })); | ||
| process.exit(getExitCode(code)); | ||
| } | ||
| /** | ||
| * Log to stderr (for progress messages) | ||
| */ | ||
| export function log(message: string): void { | ||
| console.error(message); | ||
| } | ||
| // ═══════════════════════════════════════════════════════════════════════════ | ||
| // CLI CREATION | ||
| // ═══════════════════════════════════════════════════════════════════════════ | ||
| /** | ||
| * Create a branded CLI program | ||
| */ | ||
| export function cli(name: string, version: string, description: string): Command { | ||
| const program = new Command() | ||
| .name(name) | ||
| .version(version, '-v, --version', 'Show version') | ||
| .description(description) | ||
| .addHelpText('beforeAll', `\n${BRAND}\n`) | ||
| .configureOutput({ | ||
| writeErr: (str) => { | ||
| // Don't double-output errors | ||
| if (!str.includes('"error"')) { | ||
| process.stderr.write(str); | ||
| } | ||
| } | ||
| }) | ||
| .exitOverride((err) => { | ||
| if (err.code === 'commander.helpDisplayed' || err.code === 'commander.version') { | ||
| process.exit(0); | ||
| } | ||
| if (err.code === 'commander.missingArgument') { | ||
| outputError('INVALID_INPUT', err.message, { code: err.code }); | ||
| } | ||
| if (err.code === 'commander.unknownCommand') { | ||
| outputError('INVALID_INPUT', err.message, { code: err.code }); | ||
| } | ||
| // Don't exit for help - commander already handles it | ||
| if (err.code !== 'commander.help') { | ||
| process.exit(1); | ||
| } | ||
| }); | ||
| // Add help command explicitly | ||
| program.addHelpCommand('help [command]', 'Show help for command'); | ||
| return program; | ||
| } | ||
| // ═══════════════════════════════════════════════════════════════════════════ | ||
| // ERROR CODES (for consistent error handling) | ||
| // ═══════════════════════════════════════════════════════════════════════════ | ||
| export const ErrorCodes = { | ||
| ENV_MISSING: 'ENV_MISSING', | ||
| INVALID_INPUT: 'INVALID_INPUT', | ||
| NOT_FOUND: 'NOT_FOUND', | ||
| AUTH_FAILED: 'AUTH_FAILED', | ||
| API_ERROR: 'API_ERROR', | ||
| NETWORK_ERROR: 'NETWORK_ERROR', | ||
| RATE_LIMITED: 'RATE_LIMITED', | ||
| TIMEOUT: 'TIMEOUT', | ||
| PARSE_ERROR: 'PARSE_ERROR', | ||
| MANIFEST_ERROR: 'MANIFEST_ERROR', | ||
| INSTALL_ERROR: 'INSTALL_ERROR', | ||
| NPM_ERROR: 'NPM_ERROR', | ||
| } as const; | ||
| // ═══════════════════════════════════════════════════════════════════════════ | ||
| // UTILITY HELPERS | ||
| // ═══════════════════════════════════════════════════════════════════════════ | ||
| /** | ||
| * Parse a string as JSON, or return error | ||
| */ | ||
| export function parseJson<T>(str: string, context?: string): T { | ||
| try { | ||
| return JSON.parse(str); | ||
| } catch { | ||
| outputError('PARSE_ERROR', `Invalid JSON${context ? ` in ${context}` : ''}`, { | ||
| input: str.slice(0, 100) | ||
| }); | ||
| } | ||
| } | ||
| /** | ||
| * Sleep for ms milliseconds | ||
| */ | ||
| export const sleep = (ms: number): Promise<void> => | ||
| new Promise(r => setTimeout(r, ms)); | ||
| /** | ||
| * Wrap an async action with error handling | ||
| */ | ||
| export function withErrorHandling<T extends unknown[]>( | ||
| fn: (...args: T) => Promise<void> | ||
| ): (...args: T) => Promise<void> { | ||
| return async (...args: T) => { | ||
| try { | ||
| await fn(...args); | ||
| } catch (err) { | ||
| const message = err instanceof Error ? err.message : String(err); | ||
| outputError('API_ERROR', message); | ||
| } | ||
| }; | ||
| } | ||
| /** | ||
| * Format bytes to human readable | ||
| */ | ||
| export function formatBytes(bytes: number): string { | ||
| if (bytes < 1024) return `${bytes} B`; | ||
| if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; | ||
| if (bytes < 1024 * 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MB`; | ||
| return `${(bytes / 1024 / 1024 / 1024).toFixed(1)} GB`; | ||
| } | ||
| /** | ||
| * Format date to ISO string (date only) | ||
| */ | ||
| export function formatDate(date: Date | number | string): string { | ||
| const d = new Date(date); | ||
| return d.toISOString().slice(0, 10); | ||
| } | ||
| /** | ||
| * Format date to ISO string (datetime) | ||
| */ | ||
| export function formatDateTime(date: Date | number | string): string { | ||
| const d = new Date(date); | ||
| return d.toISOString().slice(0, 19).replace('T', ' '); | ||
| } |
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
8
-46.67%22018
-40.15%8
-20%502
-43.97%