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 +131 -16
- package/bin/index.js +196 -52
- package/package.json +11 -5
- package/src/utils/argv.js +11 -0
- package/src/utils/input.js +27 -0
- package/src/utils/preset.js +32 -0
- package/src/utils/prompt.js +36 -0
- package/src/utils/variables.js +17 -0
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 {
|
|
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
|
-
|
|
15
|
-
$ heyi "What is
|
|
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
|
|
27
|
-
$
|
|
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
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
162
|
+
throw new Error('A prompt is required either as an argument or via stdin')
|
|
57
163
|
}
|
|
58
164
|
|
|
59
|
-
// Build
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
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
|
-
.
|
|
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(
|
|
89
|
-
.option(
|
|
90
|
-
.option(
|
|
91
|
-
.option(
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
.
|
|
99
|
-
.
|
|
100
|
-
.
|
|
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": "
|
|
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.
|
|
29
|
-
"ai": "^5.0.
|
|
33
|
+
"@openrouter/ai-sdk-provider": "^1.5.4",
|
|
34
|
+
"ai": "^5.0.121",
|
|
30
35
|
"commander": "^14.0.2",
|
|
31
|
-
"dotenv": "^
|
|
32
|
-
"
|
|
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
|
+
}
|
package/src/utils/input.js
CHANGED
|
@@ -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
|
+
}
|