+35
-4
@@ -11,3 +11,3 @@ #!/usr/bin/env node | ||
| import { buildPrompt } from '../src/utils/prompt.js' | ||
| import { replaceVariables } from '../src/utils/variables.js' | ||
| import { findUndefinedVariables, promptForVariable, replaceVariables } from '../src/utils/variables.js' | ||
@@ -90,2 +90,8 @@ const DEFAULT_MODEL = 'openai/gpt-4o-mini' | ||
| # Interactive variable prompting (will prompt for undefined variables) | ||
| $ heyi prompt "Translate {{text}} to {{language}}" | ||
| # Variable with description (shows during prompt) | ||
| $ heyi prompt "Explain {{topic description='What to explain'}} in simple terms" | ||
| # Environment variables | ||
@@ -116,2 +122,6 @@ $ HEYI_MODEL=perplexity/sonar heyi prompt "Explain AI" | ||
| # Interactive variable prompting (will prompt for undefined variables) | ||
| $ heyi preset file.json | ||
| # (prompts for any variables in preset not provided via --var) | ||
| # Attach additional context | ||
@@ -181,4 +191,16 @@ $ heyi preset file.json --file additional.txt | ||
| // Build the prompt and prefer the argument over stdin | ||
| const userPrompt = replaceVariables(prompt ?? stdinContent, options.vars) | ||
| // Get the user prompt (prefer argument over stdin) | ||
| const rawPrompt = prompt ?? stdinContent | ||
| // Find undefined variables in the prompt | ||
| const undefinedVars = findUndefinedVariables(rawPrompt, options.vars) | ||
| // Prompt user for each undefined variable | ||
| for (const varInfo of undefinedVars) { | ||
| const value = await promptForVariable(varInfo.name, varInfo.description) | ||
| options.vars[varInfo.name] = value | ||
| } | ||
| // Build the prompt with all variables replaced | ||
| const userPrompt = replaceVariables(rawPrompt, options.vars) | ||
| const finalPrompt = await buildPrompt(userPrompt, options.files, options.urls, options.crawler) | ||
@@ -214,3 +236,12 @@ | ||
| // Build the prompt | ||
| // Find undefined variables in the prompt | ||
| const undefinedVars = findUndefinedVariables(prompt, options.vars) | ||
| // Prompt user for each undefined variable | ||
| for (const varInfo of undefinedVars) { | ||
| const value = await promptForVariable(varInfo.name, varInfo.description) | ||
| options.vars[varInfo.name] = value | ||
| } | ||
| // Build the prompt with all variables replaced | ||
| const userPrompt = replaceVariables(prompt, options.vars) | ||
@@ -217,0 +248,0 @@ const finalPrompt = await buildPrompt(userPrompt, options.files, options.urls, options.crawler) |
+6
-6
| { | ||
| "name": "heyi", | ||
| "version": "3.0.0", | ||
| "version": "3.1.0", | ||
| "description": "CLI tool to execute AI prompts with flexible output formatting", | ||
@@ -34,8 +34,8 @@ "keywords": [ | ||
| "@openrouter/ai-sdk-provider": "^1.5.4", | ||
| "ai": "^5.0.121", | ||
| "commander": "^14.0.2", | ||
| "ai": "^5.0.129", | ||
| "commander": "^14.0.3", | ||
| "dotenv": "^16.6.1", | ||
| "puppeteer": "^24.35.0", | ||
| "puppeteer": "^24.37.2", | ||
| "sanitize-html": "^2.17.0", | ||
| "zod": "^4.3.5" | ||
| "zod": "^4.3.6" | ||
| }, | ||
@@ -46,3 +46,3 @@ "devDependencies": { | ||
| "eslint": "^9.39.2", | ||
| "prettier": "^3.7.4" | ||
| "prettier": "^3.8.1" | ||
| }, | ||
@@ -49,0 +49,0 @@ "engines": { |
+59
-0
@@ -65,2 +65,11 @@ # heyi | ||
| # Interactive variable prompting (prompts for undefined variables) | ||
| heyi prompt "Translate {{text}} to {{language}}" | ||
| # Will interactively prompt: text: [user enters value] | ||
| # language: [user enters value] | ||
| # Variable with description (shows custom prompt text) | ||
| heyi prompt "Explain {{topic description='What to explain'}} in simple terms" | ||
| # Will interactively prompt: What to explain (topic): [user enters value] | ||
| # Variable replacement with stdin | ||
@@ -218,2 +227,52 @@ echo "Translate to {{language}}" | heyi prompt --var language="Spanish" | ||
| ## Variables | ||
| The tool supports variable replacement in prompts using `{{variable}}` syntax. Variables can be provided via the `--var` flag or through interactive prompting. | ||
| ### Variable Syntax | ||
| **Basic variable:** | ||
| ``` | ||
| {{variableName}} | ||
| ``` | ||
| **Variable with description (for interactive prompting):** | ||
| ``` | ||
| {{variableName description="Description shown to user"}} | ||
| ``` | ||
| ### Variable Behavior | ||
| 1. **Provided via --var flag**: Variables are directly replaced with the provided values | ||
| 2. **Not provided**: The tool will interactively prompt the user to enter the value | ||
| 3. **With description**: When prompting, the description is shown to help the user understand what to enter | ||
| ### Variable Examples | ||
| ```sh | ||
| # Provide variables via flag | ||
| heyi prompt "Translate {{text}} to {{language}}" --var text="Hello" --var language="Spanish" | ||
| # Interactive prompting for undefined variables | ||
| heyi prompt "Translate {{text}} to {{language}}" | ||
| # Prompts: | ||
| # text: [user enters value] | ||
| # language: [user enters value] | ||
| # Mix provided and interactive variables | ||
| heyi prompt "Translate {{text}} to {{language}}" --var language="French" | ||
| # Only prompts for 'text' since 'language' is provided | ||
| # Use descriptions for better user experience | ||
| heyi prompt "Explain {{topic description='Enter a topic to explain'}} in simple terms" | ||
| # Prompts: | ||
| # Enter a topic to explain (topic): [user enters value] | ||
| # Variables work in preset files too | ||
| heyi preset translate.json | ||
| # Prompts for any undefined variables in the preset's prompt | ||
| ``` | ||
| ## Crawlers | ||
@@ -220,0 +279,0 @@ |
+30
-19
@@ -112,4 +112,31 @@ import { readFile } from 'node:fs/promises' | ||
| // eslint-disable-next-line unicorn/consistent-function-scoping | ||
| const navigateTo = async (page, url) => { | ||
| try { | ||
| await page.goto(url, { waitUntil: 'networkidle2', timeout: 8000 }) | ||
| } catch (error) { | ||
| // If it's a timeout error, continue with the content that's already loaded instead of failing | ||
| if (error.message.includes('Navigation timeout')) { | ||
| return | ||
| } | ||
| throw error | ||
| } | ||
| } | ||
| // eslint-disable-next-line unicorn/consistent-function-scoping | ||
| const getContent = async (page) => { | ||
| try { | ||
| return await page.content() | ||
| } catch (error) { | ||
| // A client-side navigation might have happened, try to recover by waiting for navigation | ||
| if (error.message.includes('Execution context was destroyed, most likely because of a navigation.')) { | ||
| await page.waitForNavigation({ waitUntil: 'networkidle2', timeout: 8000 }) | ||
| return page.content() | ||
| } | ||
| throw error | ||
| } | ||
| } | ||
| const browser = await launch({ | ||
| headless: true, | ||
| // These args are required for running in containerized environments (e.g., Docker, CI/CD) | ||
@@ -121,21 +148,5 @@ args: ['--no-sandbox', '--disable-setuid-sandbox'], | ||
| const page = await browser.newPage() | ||
| await navigateTo(page, url) | ||
| const html = await getContent(page) | ||
| // Wait for network to be idle, with a 10-second timeout to prevent indefinite waiting. | ||
| // If timeout occurs, continue with whatever content is available. | ||
| // Wait for navigation first in case there are redirects. | ||
| try { | ||
| await Promise.all([ | ||
| page.waitForNavigation({ timeout: 10000 }), | ||
| page.goto(url, { waitUntil: 'networkidle0', timeout: 10000 }), | ||
| ]) | ||
| } catch (error) { | ||
| // If it's a timeout error, continue with the content that's already loaded | ||
| // For other errors (e.g., network errors), rethrow | ||
| if (!error.message.includes('timeout') && !error.message.includes('Navigation timeout')) { | ||
| throw error | ||
| } | ||
| } | ||
| const html = await page.content() | ||
| // Sanitize HTML to extract only text content and avoid large data | ||
@@ -142,0 +153,0 @@ const cleanText = sanitizeHtml(html, { |
@@ -0,5 +1,74 @@ | ||
| import readline from 'node:readline' | ||
| /** | ||
| * Extract all variables from a prompt string, including their metadata. | ||
| * Supports both {{variable}} and {{variable description="Description"}} syntax. | ||
| * | ||
| * @param {string} prompt - The prompt with variables | ||
| * @returns {Array<{name: string, description: string|null}>} Array of variable metadata | ||
| */ | ||
| export const extractVariables = (prompt) => { | ||
| // Match {{variable}} or {{variable description="..."}} or {{variable description='...'}} | ||
| const pattern = /\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*(?:description\s*=\s*['"]([^'"]*)['"])?\s*\}\}/g | ||
| const variables = [] | ||
| const seen = new Set() | ||
| let match | ||
| while ((match = pattern.exec(prompt)) !== null) { | ||
| const variableName = match[1] | ||
| const description = match[2] || null | ||
| // Only add each variable once (first occurrence) | ||
| if (!seen.has(variableName)) { | ||
| seen.add(variableName) | ||
| variables.push({ | ||
| name: variableName, | ||
| description, | ||
| }) | ||
| } | ||
| } | ||
| return variables | ||
| } | ||
| /** | ||
| * Find variables that are used in the prompt but not provided in the variables object. | ||
| * | ||
| * @param {string} prompt - The prompt with variables | ||
| * @param {object} variables - Object with variable names as keys | ||
| * @returns {Array<{name: string, description: string|null}>} Array of undefined variable metadata | ||
| */ | ||
| export const findUndefinedVariables = (prompt, variables = {}) => { | ||
| const allVariables = extractVariables(prompt) | ||
| return allVariables.filter((v) => !(v.name in variables)) | ||
| } | ||
| /** | ||
| * Prompt user for a variable value interactively. | ||
| * | ||
| * @param {string} variableName - Name of the variable | ||
| * @param {string|null} description - Optional description for the variable | ||
| * @returns {Promise<string>} The value entered by the user | ||
| */ | ||
| export const promptForVariable = (variableName, description = null) => { | ||
| const rl = readline.createInterface({ | ||
| input: process.stdin, | ||
| output: process.stdout, | ||
| }) | ||
| const prompt = description ? `${description} (${variableName}): ` : `${variableName}: ` | ||
| return new Promise((resolve) => { | ||
| rl.question(prompt, (answer) => { | ||
| rl.close() | ||
| resolve(answer) | ||
| }) | ||
| }) | ||
| } | ||
| /** | ||
| * Replace variables in a prompt string. | ||
| * Handles both {{variable}} and {{variable description="Description"}} syntax. | ||
| * | ||
| * @param {string} prompt - The prompt with variables in {{variable}} format | ||
| * @param {string} prompt - The prompt with variables | ||
| * @param {object} variables - Object with variable names as keys and replacement values as values | ||
@@ -12,3 +81,4 @@ * @returns {string} The prompt with variables replaced | ||
| for (const [variable, value] of Object.entries(variables)) { | ||
| const pattern = new RegExp(`{{\\s*${variable}\\s*}}`, 'g') | ||
| // Match both {{variable}} and {{variable description="..."}} or {{variable description='...'}} | ||
| const pattern = new RegExp(`\\{\\{\\s*${variable}\\s*(?:description\\s*=\\s*['"][^'"]*['"])?\\s*\\}\\}`, 'g') | ||
| result = result.replace(pattern, value) | ||
@@ -15,0 +85,0 @@ } |
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Uses eval
Supply chain riskPackage uses dynamic code execution (e.g., eval()), which is a dangerous practice. This can prevent the code from running in certain environments and increases the risk that the code may contain exploits or malicious behavior.
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 3 instances in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Uses eval
Supply chain riskPackage uses dynamic code execution (e.g., eval()), which is a dangerous practice. This can prevent the code from running in certain environments and increases the risk that the code may contain exploits or malicious behavior.
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 3 instances in 1 package
36143
19.23%632
17.47%328
21.93%7
75%+ Added
+ Added
- Removed
- Removed
Updated
Updated
Updated
Updated