heyi 1.1.0 → 2.0.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
@@ -15,7 +15,8 @@ npm install heyi -g
15
15
  ### CLI
16
16
 
17
17
  ```sh
18
- heyi [prompt] [options]
18
+ heyi prompt [prompt] [options]
19
+ heyi preset [file] [options]
19
20
  ```
20
21
 
21
22
  #### Options
@@ -24,6 +25,8 @@ heyi [prompt] [options]
24
25
  - `-f, --format <format>` - Output format: `string`, `number`, `object`, `array` (default: `string`)
25
26
  - `-s, --schema <schema>` - Zod schema for object/array format (required when format is `object` or `array`)
26
27
  - `--file <path>` - Read content from file and include as context (can be used multiple times)
28
+ - `--url <url>` - Fetch content from URL and include as context (can be used multiple times)
29
+ - `--var <key=value>` - Define variables for replacement in prompt using `{{key}}` syntax (can be used multiple times)
27
30
  - `-h, --help` - Display help information
28
31
  - `-V, --version` - Display version number
29
32
 
@@ -36,39 +39,151 @@ heyi [prompt] [options]
36
39
 
37
40
  ```sh
38
41
  # Simple text prompt
39
- heyi "What is the capital of France?"
42
+ heyi prompt "What is the capital of France?"
40
43
 
41
44
  # Use a different model
42
- heyi "Explain quantum computing" --model google/gemini-2.0-flash-exp
45
+ heyi prompt "Explain quantum computing" --model google/gemini-2.0-flash-exp
43
46
 
44
47
  # Get structured output as array of strings
45
- heyi "List 5 programming languages" --format array --schema "z.string()"
48
+ heyi prompt "List 5 programming languages" --format array --schema "z.string()"
46
49
 
47
50
  # Get structured output as array of objects
48
- heyi "List 3 countries with their capitals" --format array --schema "z.object({name:z.string(),capital:z.string()})"
51
+ heyi prompt "List 3 countries with their capitals" --format array --schema "z.object({name:z.string(),capital:z.string()})"
49
52
 
50
53
  # Get structured output as single object
51
- heyi "Analyze: revenue 100k, costs 60k" --format object --schema "z.object({revenue:z.number(),costs:z.number()})"
54
+ heyi prompt "Analyze: revenue 100k, costs 60k" --format object --schema "z.object({revenue:z.number(),costs:z.number()})"
52
55
 
53
56
  # Complex nested schema
54
- heyi "Analyze top 3 tech companies" --format array --schema "z.object({name:z.string(),founded:z.number(),products:z.array(z.string())})"
57
+ heyi prompt "Analyze top 3 tech companies" --format array --schema "z.object({name:z.string(),founded:z.number(),products:z.array(z.string())})"
58
+
59
+ # Variable replacement in prompts
60
+ heyi prompt "Preset in {{language}}" --var language="German"
61
+ heyi prompt "Preset in {{input}} and output in {{output}}" --var input="German" --var output="English"
62
+
63
+ # Variable replacement with stdin
64
+ echo "Translate to {{language}}" | heyi prompt --var language="Spanish"
55
65
 
56
66
  # Set default model via environment variable
57
- MODEL=perplexity/sonar heyi "Explain AI"
67
+ MODEL=perplexity/sonar heyi prompt "Explain AI"
58
68
 
59
69
  # Set API key via environment variable
60
- API_KEY=your-key heyi "Hello, AI!"
70
+ API_KEY=your-key heyi prompt "Hello, AI!"
61
71
 
62
72
  # Input from file as context
63
- heyi "Summarize this content" --file input.txt
73
+ heyi prompt "Summarize this content" --file input.txt
64
74
 
65
75
  # Input from multiple files as context
66
- heyi "Compare these files" --file file1.txt --file file2.txt
67
- heyi "Analyze all these documents" --file doc1.md --file doc2.md --file doc3.md
76
+ heyi prompt "Compare these files" --file file1.txt --file file2.txt
77
+ heyi prompt "Analyze all these documents" --file doc1.md --file doc2.md --file doc3.md
78
+
79
+ # Input from URL as context
80
+ heyi prompt "Summarize this article" --url https://example.com/article.html
81
+
82
+ # Input from multiple URLs as context
83
+ heyi prompt "Compare these articles" --url https://example.com/article1.html --url https://example.com/article2.html
84
+
85
+ # Mix files and URLs as context
86
+ heyi prompt "Compare local and remote content" --file local.txt --url https://example.com/remote.txt
68
87
 
69
88
  # Input from stdin
70
- cat article.md | heyi "Extract all URLs mentioned"
71
- echo "Analyze this text" | heyi
89
+ cat article.md | heyi prompt "Extract all URLs mentioned"
90
+ echo "Analyze this text" | heyi prompt
91
+
92
+ # Preset files
93
+ heyi preset file.json
94
+ heyi preset file.json --var language=german
95
+ heyi preset file.json --model openai/gpt-4o
96
+ heyi preset file.json --file additional.txt --url https://example.com
97
+ ```
98
+
99
+ ## Preset Files
100
+
101
+ Preset files allow you to define reusable configurations with prompts, models, files, and URLs. Create a JSON file with the following structure:
102
+
103
+ ```json
104
+ {
105
+ "prompt": "Your prompt with {{variables}}",
106
+ "model": "openai/gpt-4o-mini",
107
+ "format": "array",
108
+ "schema": "z.string()",
109
+ "files": ["path/to/file1.txt", "path/to/file2.txt"],
110
+ "urls": ["https://example.com/page.html"]
111
+ }
112
+ ```
113
+
114
+ ### Preset Configuration
115
+
116
+ - **prompt**: The AI prompt to execute. Supports variable replacement using `{{variable}}` syntax.
117
+ - **model** (optional): AI model to use (e.g., `openai/gpt-4o-mini`, `google/gemini-2.0-flash-exp`).
118
+ - **format** (optional): Output format: `string`, `number`, `object`, `array` (default: `string`).
119
+ - **schema** (optional): Zod schema for object/array format (required when format is `object` or `array`).
120
+ - **files** (optional): Array of file paths to include as context.
121
+ - **urls** (optional): Array of URLs to fetch and include as context.
122
+
123
+ ### Preset Examples
124
+
125
+ **Basic preset with variables:**
126
+
127
+ ```json
128
+ {
129
+ "prompt": "Explain {{topic}} in {{language}}"
130
+ }
131
+ ```
132
+
133
+ ```sh
134
+ heyi preset explain.json --var topic="quantum computing" --var language="simple terms"
135
+ ```
136
+
137
+ **Preset with files and URLs:**
138
+
139
+ ```json
140
+ {
141
+ "prompt": "Analyze and compare the following documents",
142
+ "model": "google/gemini-2.0-flash-exp",
143
+ "files": ["report1.txt", "report2.txt"],
144
+ "urls": ["https://example.com/data.html"]
145
+ }
146
+ ```
147
+
148
+ ```sh
149
+ heyi preset analyze.json
150
+ ```
151
+
152
+ **Preset with structured output:**
153
+
154
+ ```json
155
+ {
156
+ "prompt": "List programming languages mentioned in these files",
157
+ "format": "array",
158
+ "schema": "z.string()",
159
+ "files": ["code1.js", "code2.py"]
160
+ }
161
+ ```
162
+
163
+ ```sh
164
+ heyi preset languages.json
165
+ ```
166
+
167
+ ### CLI Override Behavior
168
+
169
+ - **Model override**: Using `--model` flag overrides the model specified in the preset file.
170
+ - **Format override**: Using `--format` flag overrides the format specified in the preset file.
171
+ - **Schema override**: Using `--schema` flag overrides the schema specified in the preset file.
172
+ - **Files and URLs append**: Using `--file` or `--url` flags adds additional context to the preset's files and URLs.
173
+ - **Variables**: Use `--var` to replace variables in the preset's prompt.
174
+
175
+ ```sh
176
+ # Override model from preset
177
+ heyi preset file.json --model openai/gpt-4o
178
+
179
+ # Override format from preset
180
+ heyi preset file.json --format object --schema "z.object({name:z.string()})"
181
+
182
+ # Add additional files to preset's files
183
+ heyi preset file.json --file extra.txt
184
+
185
+ # Replace variables in preset prompt
186
+ heyi preset file.json --var name="Alice" --var role="developer"
72
187
  ```
73
188
 
74
189
  ## Output Formats
@@ -100,10 +215,10 @@ npm test
100
215
  npm run format
101
216
 
102
217
  # Run the CLI in development
103
- npm start -- "Your prompt here"
218
+ npm start -- prompt "Your prompt here"
104
219
 
105
220
  # Or run directly
106
- ./bin/index.js "Your prompt here"
221
+ ./bin/index.js prompt "Your prompt here"
107
222
  ```
108
223
 
109
224
  ## Related
package/bin/index.js CHANGED
@@ -1,50 +1,156 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { Command } from 'commander'
4
+ import { z } from 'zod'
4
5
  import pkg from '../package.json' with { type: 'json' }
5
6
  import { executePrompt } from '../src/index.js'
6
- import { hasStdinData, readFileContent, readStdin } from '../src/utils/input.js'
7
+ import { hasFlag } from '../src/utils/argv.js'
8
+ import { hasStdinData, readStdin } from '../src/utils/input.js'
9
+ import { loadPreset } from '../src/utils/preset.js'
10
+ import { buildPrompt } from '../src/utils/prompt.js'
11
+ import { replaceVariables } from '../src/utils/variables.js'
7
12
 
8
13
  const DEFAULT_MODEL = 'openai/gpt-4o-mini'
9
14
 
15
+ const modelFlag = ['-m, --model <model>', 'AI model to use', process.env.MODEL ?? DEFAULT_MODEL]
16
+ const formatFlag = ['-f, --format <format>', 'Output format: string, number, object, array', 'string']
17
+ const schemaFlag = [
18
+ '-s, --schema <schema>',
19
+ 'Zod schema for object/array format (required when format is object or array)',
20
+ ]
21
+ const fileFlag = [
22
+ '--file <path>',
23
+ 'Read content from file and include as context (can be used multiple times)',
24
+ (value, previous) => {
25
+ return previous ? [...previous, value] : [value]
26
+ },
27
+ ]
28
+ const urlFlag = [
29
+ '--url <url>',
30
+ 'Fetch content from URL and include as context (can be used multiple times)',
31
+ (value, previous) => {
32
+ return previous ? [...previous, value] : [value]
33
+ },
34
+ ]
35
+ const varFlag = [
36
+ '--var <variable=value>',
37
+ 'Define variables for replacement in prompt using {{variable}} syntax (can be used multiple times)',
38
+ (value, previous) => {
39
+ const [variable, ...variableValueParts] = value.split('=')
40
+ const variableValue = variableValueParts.join('=') // Handle values with = in them
41
+
42
+ if (!variable) {
43
+ throw new Error(`Invalid --var format: '${value}'. Expected format: variable=value`)
44
+ }
45
+
46
+ return { ...previous, [variable]: variableValue }
47
+ },
48
+ ]
49
+
50
+ const hasModelFlag = hasFlag(['--model', '-m'])
51
+ const hasFormatFlag = hasFlag(['--format', '-f'])
52
+ const hasSchemaFlag = hasFlag(['--schema', '-s'])
53
+
10
54
  const program = new Command()
11
55
 
12
56
  const helpText = `
13
57
  Examples:
14
- $ heyi "What is the capital of France?"
15
- $ heyi "What is quantum computing?" --model google/gemini-2.5-pro
58
+ # Prompts
59
+ $ heyi prompt "What is the capital of France?"
60
+ $ heyi prompt "What is quantum computing?" --model google/gemini-2.5-pro
61
+ $ heyi help prompt
62
+
63
+ # Presets
64
+ $ heyi preset file.json
65
+ $ heyi preset file.json --model google/gemini-2.5-pro
66
+ $ heyi help preset
67
+ `
68
+
69
+ const promptHelpText = `
70
+ Examples:
71
+ $ heyi prompt "What is the capital of France?"
72
+ $ heyi prompt "What is quantum computing?" --model google/gemini-2.5-pro
16
73
 
17
74
  # Different output formats
18
- $ heyi "List 5 programming languages" --format array --schema "z.string()"
19
- $ heyi "Analyze this data" --format object --schema "z.object({revenue:z.number(),costs:z.number()})"
20
- $ heyi "List 3 countries" --format array --schema "z.object({name:z.string(),capital:z.string()})"
75
+ $ heyi prompt "List 5 programming languages" --format array --schema "z.string()"
76
+ $ heyi prompt "Analyze this data" --format object --schema "z.object({revenue:z.number(),costs:z.number()})"
77
+ $ heyi prompt "List 3 countries" --format array --schema "z.object({name:z.string(),capital:z.string()})"
78
+
79
+ # Variable replacement
80
+ $ heyi prompt "Preset in {{language}}" --var language="German"
21
81
 
22
82
  # Environment variables
23
- $ MODEL=perplexity/sonar heyi "Explain AI"
24
- $ API_KEY=your-key heyi "Hello, AI!"
83
+ $ MODEL=perplexity/sonar heyi prompt "Explain AI"
84
+ $ API_KEY=your-key heyi prompt "Hello, AI!"
85
+
86
+ # Attach context
87
+ $ heyi prompt "Summarize this content" --file input.txt
88
+ $ heyi prompt "Compare these files" --file a.txt --file b.txt
89
+ $ heyi prompt "Summarize this article" --url https://example.com/article.html
25
90
 
26
- # Input from stdin or file
27
- $ heyi "Summarize this content" --file input.txt
28
- $ heyi "Compare these files" --file a.txt --file b.txt
29
- $ cat prompt.txt | heyi
91
+ # Input from stdin
92
+ $ cat prompt.txt | heyi prompt
30
93
  `
31
94
 
32
- const action = async (prompt, options) => {
33
- try {
34
- // Validate that schema is provided for object/array formats
35
- if ((options.format === 'object' || options.format === 'array') && !options.schema) {
36
- throw new Error(`--schema or -s is required when format is '${options.format}'`)
37
- }
95
+ const presetHelpText = `
96
+ Examples:
97
+ $ heyi preset file.json
98
+ $ heyi preset file.json --model google/gemini-2.5-pro
38
99
 
39
- // Handle file content as context
40
- const fileContents = []
41
- if (options.file) {
42
- for (const filePath of options.file) {
43
- const content = await readFileContent(filePath)
44
- fileContents.push({ path: filePath, content })
45
- }
46
- }
100
+ # Overwrite options from preset
101
+ $ heyi preset file.json --model openai/gpt-4
102
+ $ heyi preset file.json --format array --schema "z.string()"
103
+
104
+ # Variable replacement
105
+ $ heyi preset file.json --var language=german
47
106
 
107
+ # Attach additional context
108
+ $ heyi preset file.json --file additional.txt
109
+ $ heyi preset file.json --url https://example.com/additional.html
110
+ `
111
+
112
+ const optionsSchema = z
113
+ .object({
114
+ model: z.string(),
115
+ format: z.enum(['string', 'number', 'object', 'array']),
116
+ schema: z.string().optional(),
117
+ files: z.array(z.string()).default([]),
118
+ urls: z.array(z.string()).default([]),
119
+ vars: z.record(z.string(), z.string()).default({}),
120
+ })
121
+ .refine((data) => !['object', 'array'].includes(data.format) || data.schema, {
122
+ message: '--schema or -s is required when format is object or array',
123
+ path: ['schema'],
124
+ })
125
+
126
+ const flagsToOptions = (flags) => {
127
+ return optionsSchema.parse({
128
+ model: flags.model,
129
+ format: flags.format,
130
+ schema: flags.schema,
131
+ files: flags.file,
132
+ urls: flags.url,
133
+ vars: flags.var,
134
+ })
135
+ }
136
+
137
+ const mergeOptionsWithPreset = (options, presetContent) => {
138
+ return optionsSchema.parse({
139
+ // Overwrite model, format, schema only if not provided via flags
140
+ model: hasModelFlag ? options.model : (presetContent.model ?? options.model),
141
+ format: hasFormatFlag ? options.format : (presetContent.format ?? options.format),
142
+ schema: hasSchemaFlag ? options.schema : (presetContent.schema ?? options.schema),
143
+ // Merge files
144
+ files: [...presetContent.files, ...options.files],
145
+ // Merge URLs
146
+ urls: [...presetContent.urls, ...options.urls],
147
+ // Keep vars as is
148
+ vars: options.vars,
149
+ })
150
+ }
151
+
152
+ const executePromptAction = async (prompt, flags) => {
153
+ try {
48
154
  // Handle stdin input
49
155
  let stdinContent = null
50
156
  if (hasStdinData()) {
@@ -53,17 +159,48 @@ const action = async (prompt, options) => {
53
159
 
54
160
  // Validate that we have a prompt
55
161
  if (!prompt && !stdinContent) {
56
- throw new Error('A prompt is required. Provide it as an argument or via stdin.')
162
+ throw new Error('A prompt is required either as an argument or via stdin')
57
163
  }
58
164
 
59
- // Build the final prompt
60
- let finalPrompt = prompt ?? stdinContent
61
- if (fileContents.length > 0) {
62
- const fileContexts = fileContents.map(({ path, content }) => `File: ${path}\n${content}`).join('\n\n---\n\n')
63
- const contextLabel = fileContents.length === 1 ? 'Context from file:' : 'Context from files:'
64
- finalPrompt = `${finalPrompt}\n\n${contextLabel}\n${fileContexts}`
165
+ // Build options from flags
166
+ const options = flagsToOptions(flags)
167
+
168
+ // Build the prompt and prefer the argument over stdin
169
+ const userPrompt = replaceVariables(prompt ?? stdinContent, options.vars)
170
+ const finalPrompt = await buildPrompt(userPrompt, options.files, options.urls)
171
+
172
+ const result = await executePrompt(finalPrompt, {
173
+ model: options.model,
174
+ format: options.format,
175
+ schema: options.schema,
176
+ })
177
+
178
+ console.log(result)
179
+ } catch (error) {
180
+ console.error(error)
181
+
182
+ process.exit(1)
183
+ }
184
+ }
185
+
186
+ const executePresetAction = async (preset, flags) => {
187
+ try {
188
+ // Validate that preset file is provided
189
+ if (!preset) {
190
+ throw new Error('Preset file path is required when using "preset" command')
65
191
  }
66
192
 
193
+ // Load preset and use prompt from it
194
+ const presetContent = await loadPreset(preset)
195
+ const prompt = presetContent.prompt
196
+
197
+ // Build options from flags and merge with preset
198
+ const options = mergeOptionsWithPreset(flagsToOptions(flags), presetContent)
199
+
200
+ // Build the prompt
201
+ const userPrompt = replaceVariables(prompt, options.vars)
202
+ const finalPrompt = await buildPrompt(userPrompt, options.files, options.urls)
203
+
67
204
  const result = await executePrompt(finalPrompt, {
68
205
  model: options.model,
69
206
  format: options.format,
@@ -72,29 +209,36 @@ const action = async (prompt, options) => {
72
209
 
73
210
  console.log(result)
74
211
  } catch (error) {
75
- const relevantFields = Object.keys(error).filter((key) => ['stack', 'isRetryable', 'data'].includes(key) === false)
76
- const relevantError = Object.fromEntries(relevantFields.map((key) => [key, error[key]]))
77
- console.error(relevantError)
212
+ console.error(error)
78
213
 
79
214
  process.exit(1)
80
215
  }
81
216
  }
82
217
 
218
+ program.name(pkg.name).description(pkg.description).version(pkg.version).addHelpText('after', helpText)
219
+
83
220
  program
84
- .name(pkg.name)
85
- .description(pkg.description)
86
- .version(pkg.version)
221
+ .command('prompt')
87
222
  .argument('[prompt]', 'The AI prompt to execute (optional when using stdin)')
88
- .option('-m, --model <model>', 'AI model to use', process.env.MODEL ?? DEFAULT_MODEL)
89
- .option('-f, --format <format>', 'Output format: string, number, object, array', 'string')
90
- .option('-s, --schema <schema>', 'Zod schema for object/array format (required when format is object or array)')
91
- .option(
92
- '--file <path>',
93
- 'Read content from file and include as context (can be used multiple times)',
94
- (value, previous) => {
95
- return previous ? [...previous, value] : [value]
96
- },
97
- )
98
- .addHelpText('after', helpText)
99
- .action(action)
100
- .parse()
223
+ .option(...modelFlag)
224
+ .option(...formatFlag)
225
+ .option(...schemaFlag)
226
+ .option(...fileFlag)
227
+ .option(...urlFlag)
228
+ .option(...varFlag)
229
+ .addHelpText('after', promptHelpText)
230
+ .action(executePromptAction)
231
+
232
+ program
233
+ .command('preset')
234
+ .argument('[file]', 'Path to preset JSON file')
235
+ .option(...modelFlag)
236
+ .option(...formatFlag)
237
+ .option(...schemaFlag)
238
+ .option(...fileFlag)
239
+ .option(...urlFlag)
240
+ .option(...varFlag)
241
+ .addHelpText('after', presetHelpText)
242
+ .action(executePresetAction)
243
+
244
+ program.parse()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "heyi",
3
- "version": "1.1.0",
3
+ "version": "2.0.0",
4
4
  "description": "CLI tool to execute AI prompts with flexible output formatting",
5
5
  "keywords": [
6
6
  "ai",
@@ -9,6 +9,11 @@
9
9
  "llm",
10
10
  "prompt"
11
11
  ],
12
+ "homepage": "https://github.com/electerious/heyi",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "https://github.com/electerious/heyi.git"
16
+ },
12
17
  "license": "MIT",
13
18
  "type": "module",
14
19
  "bin": "./bin/index.js",
@@ -25,11 +30,12 @@
25
30
  "test": "npm run lint"
26
31
  },
27
32
  "dependencies": {
28
- "@openrouter/ai-sdk-provider": "^1.5.3",
29
- "ai": "^5.0.113",
33
+ "@openrouter/ai-sdk-provider": "^1.5.4",
34
+ "ai": "^5.0.121",
30
35
  "commander": "^14.0.2",
31
- "dotenv": "^14.3.2",
32
- "zod": "^4.2.0"
36
+ "dotenv": "^16.6.1",
37
+ "sanitize-html": "^2.17.0",
38
+ "zod": "^4.3.5"
33
39
  },
34
40
  "devDependencies": {
35
41
  "@electerious/eslint-config": "^5.2.1",
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Check if any of the specified flag names exist in process.argv.
3
+ *
4
+ * @param {string[]} flagNames - Array of flag names to check for (e.g., ['--model', '-m'])
5
+ * @returns {boolean} True if any flag is found in process.argv
6
+ */
7
+ export const hasFlag = (flagNames) => {
8
+ return process.argv.some((arg) => {
9
+ return flagNames.includes(arg) || flagNames.some((name) => arg.startsWith(`${name}=`))
10
+ })
11
+ }
@@ -1,5 +1,6 @@
1
1
  import { readFile } from 'node:fs/promises'
2
2
  import { createInterface } from 'node:readline'
3
+ import sanitizeHtml from 'sanitize-html'
3
4
 
4
5
  /**
5
6
  * Read content from a file.
@@ -54,3 +55,29 @@ export const readStdin = () => {
54
55
  export const hasStdinData = () => {
55
56
  return !process.stdin.isTTY
56
57
  }
58
+
59
+ /**
60
+ * Fetch content from a URL.
61
+ *
62
+ * @param {string} url - URL to fetch content from
63
+ * @returns {Promise<string>} The URL content
64
+ */
65
+ export const fetchUrlContent = async (url) => {
66
+ try {
67
+ const response = await fetch(url)
68
+ if (!response.ok) {
69
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`)
70
+ }
71
+ const html = await response.text()
72
+ // Sanitize HTML to extract only text content and avoid large data
73
+ const cleanText = sanitizeHtml(html, {
74
+ allowedTags: [],
75
+ allowedAttributes: {},
76
+ allowedSchemes: [],
77
+ allowedSchemesAppliedToAttributes: [],
78
+ })
79
+ return cleanText.trim()
80
+ } catch (error) {
81
+ throw new Error(`Failed to fetch URL '${url}'`, { cause: error })
82
+ }
83
+ }
@@ -0,0 +1,32 @@
1
+ import { readFile } from 'node:fs/promises'
2
+ import { z } from 'zod'
3
+
4
+ const presetSchema = z.object({
5
+ prompt: z.string(),
6
+ model: z.string().optional(),
7
+ format: z.enum(['string', 'number', 'object', 'array']).optional(),
8
+ schema: z.string().optional(),
9
+ files: z.array(z.string()).default([]),
10
+ urls: z.array(z.string()).default([]),
11
+ })
12
+
13
+ /**
14
+ * Load and parse a preset JSON file.
15
+ *
16
+ * @param {string} filePath - Path to the preset JSON file
17
+ * @returns {Promise<object>} The parsed preset configuration
18
+ */
19
+ export const loadPreset = async (filePath) => {
20
+ try {
21
+ const content = await readFile(filePath, 'utf8')
22
+ const preset = JSON.parse(content)
23
+
24
+ return presetSchema.parse(preset)
25
+ } catch (error) {
26
+ if (error.code === 'ENOENT') {
27
+ throw new Error(`Preset file '${filePath}' not found`, { cause: error })
28
+ }
29
+
30
+ throw new Error(`Error while parsing preset file '${filePath}'`, { cause: error })
31
+ }
32
+ }
@@ -0,0 +1,36 @@
1
+ import { fetchUrlContent, readFileContent } from './input.js'
2
+
3
+ /**
4
+ * Build a prompt with context by combining prompt with file and URL contexts.
5
+ *
6
+ * @param {string} prompt - The prompt
7
+ * @param {string[]} filePaths - Array of file paths to include as context
8
+ * @param {string[]} urls - Array of URLs to include as context
9
+ * @returns {Promise<string>} The final prompt with all contexts combined
10
+ */
11
+ export const buildPrompt = async (prompt, filePaths = [], urls = []) => {
12
+ // Handle file content as context
13
+ const fileContents = []
14
+ for (const filePath of filePaths) {
15
+ const content = await readFileContent(filePath)
16
+ fileContents.push({ path: filePath, content })
17
+ }
18
+
19
+ // Handle URL content as context
20
+ const urlContents = []
21
+ for (const url of urls) {
22
+ const content = await fetchUrlContent(url)
23
+ urlContents.push({ path: url, content })
24
+ }
25
+
26
+ // Combine file and URL contexts
27
+ const allContexts = [...fileContents, ...urlContents]
28
+ if (allContexts.length > 0) {
29
+ const contextItems = allContexts.map(({ path, content }) => `Source: ${path}\n${content}`).join('\n\n---\n\n')
30
+ const contextLabel = allContexts.length === 1 ? 'Context from source:' : 'Context from sources:'
31
+
32
+ return `${prompt}\n\n${contextLabel}\n${contextItems}`
33
+ }
34
+
35
+ return prompt
36
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Replace variables in a prompt string.
3
+ *
4
+ * @param {string} prompt - The prompt with variables in {{variable}} format
5
+ * @param {object} variables - Object with variable names as keys and replacement values as values
6
+ * @returns {string} The prompt with variables replaced
7
+ */
8
+ export const replaceVariables = (prompt, variables = {}) => {
9
+ let result = prompt
10
+
11
+ for (const [variable, value] of Object.entries(variables)) {
12
+ const pattern = new RegExp(`{{\\s*${variable}\\s*}}`, 'g')
13
+ result = result.replace(pattern, value)
14
+ }
15
+
16
+ return result
17
+ }