heyi 3.0.0 → 3.1.0

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/README.md CHANGED
@@ -62,6 +62,15 @@ heyi prompt "Analyze top 3 tech companies" --format array --schema "z.object({na
62
62
  heyi prompt "Preset in {{language}}" --var language="German"
63
63
  heyi prompt "Preset in {{input}} and output in {{output}}" --var input="German" --var output="English"
64
64
 
65
+ # Interactive variable prompting (prompts for undefined variables)
66
+ heyi prompt "Translate {{text}} to {{language}}"
67
+ # Will interactively prompt: text: [user enters value]
68
+ # language: [user enters value]
69
+
70
+ # Variable with description (shows custom prompt text)
71
+ heyi prompt "Explain {{topic description='What to explain'}} in simple terms"
72
+ # Will interactively prompt: What to explain (topic): [user enters value]
73
+
65
74
  # Variable replacement with stdin
66
75
  echo "Translate to {{language}}" | heyi prompt --var language="Spanish"
67
76
 
@@ -214,6 +223,56 @@ The tool uses Zod schemas to ensure the AI model returns data in the requested f
214
223
  - Object array: `--format array --schema "z.object({name:z.string(),age:z.number()})"`
215
224
  - Single object: `--format object --schema "z.object({total:z.number(),items:z.array(z.string())})"`
216
225
 
226
+ ## Variables
227
+
228
+ The tool supports variable replacement in prompts using `{{variable}}` syntax. Variables can be provided via the `--var` flag or through interactive prompting.
229
+
230
+ ### Variable Syntax
231
+
232
+ **Basic variable:**
233
+
234
+ ```
235
+ {{variableName}}
236
+ ```
237
+
238
+ **Variable with description (for interactive prompting):**
239
+
240
+ ```
241
+ {{variableName description="Description shown to user"}}
242
+ ```
243
+
244
+ ### Variable Behavior
245
+
246
+ 1. **Provided via --var flag**: Variables are directly replaced with the provided values
247
+ 2. **Not provided**: The tool will interactively prompt the user to enter the value
248
+ 3. **With description**: When prompting, the description is shown to help the user understand what to enter
249
+
250
+ ### Variable Examples
251
+
252
+ ```sh
253
+ # Provide variables via flag
254
+ heyi prompt "Translate {{text}} to {{language}}" --var text="Hello" --var language="Spanish"
255
+
256
+ # Interactive prompting for undefined variables
257
+ heyi prompt "Translate {{text}} to {{language}}"
258
+ # Prompts:
259
+ # text: [user enters value]
260
+ # language: [user enters value]
261
+
262
+ # Mix provided and interactive variables
263
+ heyi prompt "Translate {{text}} to {{language}}" --var language="French"
264
+ # Only prompts for 'text' since 'language' is provided
265
+
266
+ # Use descriptions for better user experience
267
+ heyi prompt "Explain {{topic description='Enter a topic to explain'}} in simple terms"
268
+ # Prompts:
269
+ # Enter a topic to explain (topic): [user enters value]
270
+
271
+ # Variables work in preset files too
272
+ heyi preset translate.json
273
+ # Prompts for any undefined variables in the preset's prompt
274
+ ```
275
+
217
276
  ## Crawlers
218
277
 
219
278
  The tool supports two crawlers for fetching content from URLs:
package/bin/index.js CHANGED
@@ -8,7 +8,7 @@ import { hasFlag } from '../src/utils/argv.js'
8
8
  import { hasStdinData, readStdin } from '../src/utils/input.js'
9
9
  import { loadPreset } from '../src/utils/preset.js'
10
10
  import { buildPrompt } from '../src/utils/prompt.js'
11
- import { replaceVariables } from '../src/utils/variables.js'
11
+ import { findUndefinedVariables, promptForVariable, replaceVariables } from '../src/utils/variables.js'
12
12
 
13
13
  const DEFAULT_MODEL = 'openai/gpt-4o-mini'
14
14
  const DEFAULT_CRAWLER = 'fetch'
@@ -86,6 +86,12 @@ Examples:
86
86
  # Variable replacement
87
87
  $ heyi prompt "Preset in {{language}}" --var language="German"
88
88
 
89
+ # Interactive variable prompting (will prompt for undefined variables)
90
+ $ heyi prompt "Translate {{text}} to {{language}}"
91
+
92
+ # Variable with description (shows during prompt)
93
+ $ heyi prompt "Explain {{topic description='What to explain'}} in simple terms"
94
+
89
95
  # Environment variables
90
96
  $ HEYI_MODEL=perplexity/sonar heyi prompt "Explain AI"
91
97
  $ HEYI_API_KEY=your-key heyi prompt "Hello, AI!"
@@ -111,6 +117,10 @@ Examples:
111
117
  # Variable replacement
112
118
  $ heyi preset file.json --var language=german
113
119
 
120
+ # Interactive variable prompting (will prompt for undefined variables)
121
+ $ heyi preset file.json
122
+ # (prompts for any variables in preset not provided via --var)
123
+
114
124
  # Attach additional context
115
125
  $ heyi preset file.json --file additional.txt
116
126
  $ heyi preset file.json --url https://example.com/additional.html
@@ -175,8 +185,20 @@ const executePromptAction = async (prompt, flags) => {
175
185
  // Build options from flags
176
186
  const options = flagsToOptions(flags)
177
187
 
178
- // Build the prompt and prefer the argument over stdin
179
- const userPrompt = replaceVariables(prompt ?? stdinContent, options.vars)
188
+ // Get the user prompt (prefer argument over stdin)
189
+ const rawPrompt = prompt ?? stdinContent
190
+
191
+ // Find undefined variables in the prompt
192
+ const undefinedVars = findUndefinedVariables(rawPrompt, options.vars)
193
+
194
+ // Prompt user for each undefined variable
195
+ for (const varInfo of undefinedVars) {
196
+ const value = await promptForVariable(varInfo.name, varInfo.description)
197
+ options.vars[varInfo.name] = value
198
+ }
199
+
200
+ // Build the prompt with all variables replaced
201
+ const userPrompt = replaceVariables(rawPrompt, options.vars)
180
202
  const finalPrompt = await buildPrompt(userPrompt, options.files, options.urls, options.crawler)
181
203
 
182
204
  const result = await executePrompt(finalPrompt, {
@@ -207,7 +229,16 @@ const executePresetAction = async (preset, flags) => {
207
229
  // Build options from flags and merge with preset
208
230
  const options = mergeOptionsWithPreset(flagsToOptions(flags), presetContent)
209
231
 
210
- // Build the prompt
232
+ // Find undefined variables in the prompt
233
+ const undefinedVars = findUndefinedVariables(prompt, options.vars)
234
+
235
+ // Prompt user for each undefined variable
236
+ for (const varInfo of undefinedVars) {
237
+ const value = await promptForVariable(varInfo.name, varInfo.description)
238
+ options.vars[varInfo.name] = value
239
+ }
240
+
241
+ // Build the prompt with all variables replaced
211
242
  const userPrompt = replaceVariables(prompt, options.vars)
212
243
  const finalPrompt = await buildPrompt(userPrompt, options.files, options.urls, options.crawler)
213
244
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "heyi",
3
- "version": "3.0.0",
3
+ "version": "3.1.0",
4
4
  "description": "CLI tool to execute AI prompts with flexible output formatting",
5
5
  "keywords": [
6
6
  "ai",
@@ -31,18 +31,18 @@
31
31
  },
32
32
  "dependencies": {
33
33
  "@openrouter/ai-sdk-provider": "^1.5.4",
34
- "ai": "^5.0.121",
35
- "commander": "^14.0.2",
34
+ "ai": "^5.0.129",
35
+ "commander": "^14.0.3",
36
36
  "dotenv": "^16.6.1",
37
- "puppeteer": "^24.35.0",
37
+ "puppeteer": "^24.37.2",
38
38
  "sanitize-html": "^2.17.0",
39
- "zod": "^4.3.5"
39
+ "zod": "^4.3.6"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@electerious/eslint-config": "^5.2.1",
43
43
  "@electerious/prettier-config": "^4.0.0",
44
44
  "eslint": "^9.39.2",
45
- "prettier": "^3.7.4"
45
+ "prettier": "^3.8.1"
46
46
  },
47
47
  "engines": {
48
48
  "node": ">=22"
@@ -109,32 +109,43 @@ const fetchUrlContentWithFetch = async (url) => {
109
109
  const fetchUrlContentWithChrome = async (url) => {
110
110
  validateUrl(url)
111
111
 
112
- const browser = await launch({
113
- headless: true,
114
- // These args are required for running in containerized environments (e.g., Docker, CI/CD)
115
- args: ['--no-sandbox', '--disable-setuid-sandbox'],
116
- })
112
+ // eslint-disable-next-line unicorn/consistent-function-scoping
113
+ const navigateTo = async (page, url) => {
114
+ try {
115
+ await page.goto(url, { waitUntil: 'networkidle2', timeout: 8000 })
116
+ } catch (error) {
117
+ // If it's a timeout error, continue with the content that's already loaded instead of failing
118
+ if (error.message.includes('Navigation timeout')) {
119
+ return
120
+ }
117
121
 
118
- try {
119
- const page = await browser.newPage()
122
+ throw error
123
+ }
124
+ }
120
125
 
121
- // Wait for network to be idle, with a 10-second timeout to prevent indefinite waiting.
122
- // If timeout occurs, continue with whatever content is available.
123
- // Wait for navigation first in case there are redirects.
126
+ // eslint-disable-next-line unicorn/consistent-function-scoping
127
+ const getContent = async (page) => {
124
128
  try {
125
- await Promise.all([
126
- page.waitForNavigation({ timeout: 10000 }),
127
- page.goto(url, { waitUntil: 'networkidle0', timeout: 10000 }),
128
- ])
129
+ return await page.content()
129
130
  } catch (error) {
130
- // If it's a timeout error, continue with the content that's already loaded
131
- // For other errors (e.g., network errors), rethrow
132
- if (!error.message.includes('timeout') && !error.message.includes('Navigation timeout')) {
133
- throw error
131
+ // A client-side navigation might have happened, try to recover by waiting for navigation
132
+ if (error.message.includes('Execution context was destroyed, most likely because of a navigation.')) {
133
+ await page.waitForNavigation({ waitUntil: 'networkidle2', timeout: 8000 })
134
+ return page.content()
134
135
  }
136
+ throw error
135
137
  }
138
+ }
136
139
 
137
- const html = await page.content()
140
+ const browser = await launch({
141
+ // These args are required for running in containerized environments (e.g., Docker, CI/CD)
142
+ args: ['--no-sandbox', '--disable-setuid-sandbox'],
143
+ })
144
+
145
+ try {
146
+ const page = await browser.newPage()
147
+ await navigateTo(page, url)
148
+ const html = await getContent(page)
138
149
 
139
150
  // Sanitize HTML to extract only text content and avoid large data
140
151
  const cleanText = sanitizeHtml(html, {
@@ -1,7 +1,76 @@
1
+ import readline from 'node:readline'
2
+
3
+ /**
4
+ * Extract all variables from a prompt string, including their metadata.
5
+ * Supports both {{variable}} and {{variable description="Description"}} syntax.
6
+ *
7
+ * @param {string} prompt - The prompt with variables
8
+ * @returns {Array<{name: string, description: string|null}>} Array of variable metadata
9
+ */
10
+ export const extractVariables = (prompt) => {
11
+ // Match {{variable}} or {{variable description="..."}} or {{variable description='...'}}
12
+ const pattern = /\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*(?:description\s*=\s*['"]([^'"]*)['"])?\s*\}\}/g
13
+ const variables = []
14
+ const seen = new Set()
15
+
16
+ let match
17
+ while ((match = pattern.exec(prompt)) !== null) {
18
+ const variableName = match[1]
19
+ const description = match[2] || null
20
+
21
+ // Only add each variable once (first occurrence)
22
+ if (!seen.has(variableName)) {
23
+ seen.add(variableName)
24
+ variables.push({
25
+ name: variableName,
26
+ description,
27
+ })
28
+ }
29
+ }
30
+
31
+ return variables
32
+ }
33
+
34
+ /**
35
+ * Find variables that are used in the prompt but not provided in the variables object.
36
+ *
37
+ * @param {string} prompt - The prompt with variables
38
+ * @param {object} variables - Object with variable names as keys
39
+ * @returns {Array<{name: string, description: string|null}>} Array of undefined variable metadata
40
+ */
41
+ export const findUndefinedVariables = (prompt, variables = {}) => {
42
+ const allVariables = extractVariables(prompt)
43
+ return allVariables.filter((v) => !(v.name in variables))
44
+ }
45
+
46
+ /**
47
+ * Prompt user for a variable value interactively.
48
+ *
49
+ * @param {string} variableName - Name of the variable
50
+ * @param {string|null} description - Optional description for the variable
51
+ * @returns {Promise<string>} The value entered by the user
52
+ */
53
+ export const promptForVariable = (variableName, description = null) => {
54
+ const rl = readline.createInterface({
55
+ input: process.stdin,
56
+ output: process.stdout,
57
+ })
58
+
59
+ const prompt = description ? `${description} (${variableName}): ` : `${variableName}: `
60
+
61
+ return new Promise((resolve) => {
62
+ rl.question(prompt, (answer) => {
63
+ rl.close()
64
+ resolve(answer)
65
+ })
66
+ })
67
+ }
68
+
1
69
  /**
2
70
  * Replace variables in a prompt string.
71
+ * Handles both {{variable}} and {{variable description="Description"}} syntax.
3
72
  *
4
- * @param {string} prompt - The prompt with variables in {{variable}} format
73
+ * @param {string} prompt - The prompt with variables
5
74
  * @param {object} variables - Object with variable names as keys and replacement values as values
6
75
  * @returns {string} The prompt with variables replaced
7
76
  */
@@ -9,7 +78,8 @@ export const replaceVariables = (prompt, variables = {}) => {
9
78
  let result = prompt
10
79
 
11
80
  for (const [variable, value] of Object.entries(variables)) {
12
- const pattern = new RegExp(`{{\\s*${variable}\\s*}}`, 'g')
81
+ // Match both {{variable}} and {{variable description="..."}} or {{variable description='...'}}
82
+ const pattern = new RegExp(`\\{\\{\\s*${variable}\\s*(?:description\\s*=\\s*['"][^'"]*['"])?\\s*\\}\\}`, 'g')
13
83
  result = result.replace(pattern, value)
14
84
  }
15
85