Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ High-level approach.
Available specs:

- `llm-provider.md` - LLM provider abstraction, OpenAI integration, design choices
- `text-input.md` - Direct text input via --text option, alternative to file input

Specification format: Abstract and Requirements sections.

Expand All @@ -135,6 +136,7 @@ Available test cases:
- `image_multimodal.md` - Image input for multimodal prompts
- `image_generate.md` - Image generation and editing command
- `error_handling.md` - Error scenarios and messages
- `text_input.md` - Direct text input via --text option

### Test case template

Expand Down
14 changes: 12 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Idea is simple, imagine you need to generate some docs using LLM as part of CI,

> [!TIP]
> This README was generated with trickery
> trickery generate -i ./prompts/trickery_readme.md > README.md
> trickery generate ./prompts/trickery_readme.md > README.md


## Demo
Expand All @@ -28,7 +28,17 @@ trickery --help

```sh
export OPENAI_API_KEY=s....d
trickery generate -i ./prompts/trickery_readme.md > README.md
trickery generate ./prompts/trickery_readme.md > README.md
```

### Using with OpenAI-compatible gateways

You can use trickery with any OpenAI-compatible API gateway (like LiteLLM, Azure OpenAI, or local models) by setting the `OPENAI_BASE_URL` environment variable:

```sh
export OPENAI_API_KEY=your-key
export OPENAI_BASE_URL=http://localhost:4000/v1
trickery generate ./prompts/my_prompt.md
```

Input file could be any text file, with Jinja2-like template variables, like `{{"{{app_version}}"}}`. To set this variables, please use `-v` flag, like `-v app_version=1.0.0`.
Expand Down
24 changes: 12 additions & 12 deletions docs/image-generation.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ Trickery supports generating and editing images using OpenAI's Responses API wit

## CLI Arguments

### `--input <PATH>` / `-i <PATH>`
### `[INPUT]` (positional) or `-i <INPUT>`

Path to the prompt template file. Supports Jinja2-style `{{ variable }}` substitution.
Input prompt: file path or direct text (auto-detected). Supports Jinja2-style `{{ variable }}` substitution when using a file.

### `--save <PATH>` / `-s <PATH>` (optional)

Expand Down Expand Up @@ -80,13 +80,13 @@ Template variables for prompt substitution.

```bash
# Simple generation with explicit filename
trickery image -i prompts/generate_diagram.md --save docs/images/colorful-architecture.png
trickery image prompts/generate_diagram.md --save docs/images/colorful-architecture.png

# Auto-generated filename (e.g., generate_diagram-a3f5x.png)
trickery image -i prompts/generate_diagram.md
trickery image prompts/generate_diagram.md

# With quality settings
trickery image -i prompts/generate_diagram.md -s architecture.png \
trickery image prompts/generate_diagram.md -s architecture.png \
--size 1536x1024 \
--quality high
```
Expand All @@ -101,12 +101,12 @@ See [prompts/generate_diagram.md](../prompts/generate_diagram.md) for the prompt

```bash
# Make an image look realistic
trickery image -i prompts/make_realistic.md \
trickery image prompts/make_realistic.md \
--image test_data/example_images/image1.png \
--save output.png

# Edit with custom instruction
trickery image -i prompts/edit_image.md \
trickery image prompts/edit_image.md \
--image test_data/example_images/image2.png \
--save modified.png \
-v instruction="make it green on pink"
Expand All @@ -117,7 +117,7 @@ See [prompts/make_realistic.md](../prompts/make_realistic.md) and [prompts/edit_
### Highlight Areas in Image

```bash
trickery image -i prompts/highlight_humans.md \
trickery image prompts/highlight_humans.md \
--image test_data/example_images/image3.jpg \
--save highlighted.png
```
Expand All @@ -127,7 +127,7 @@ See [prompts/highlight_humans.md](../prompts/highlight_humans.md) for the prompt
### With Template Variables

```bash
trickery image -i prompts/generate_icon.md \
trickery image prompts/generate_icon.md \
--save icon.png \
-v subject="rocket" \
-v style="flat design"
Expand All @@ -139,7 +139,7 @@ See [prompts/generate_icon.md](../prompts/generate_icon.md) for the prompt templ

```bash
# Combine elements from multiple images
trickery image -i prompts/edit_image.md \
trickery image prompts/edit_image.md \
--image test_data/example_images/image1.png \
--image test_data/example_images/image2.png \
--save composite.png \
Expand All @@ -149,7 +149,7 @@ trickery image -i prompts/edit_image.md \
### Transparent Background

```bash
trickery image -i prompts/generate_icon.md \
trickery image prompts/generate_icon.md \
--save logo.png \
--background transparent \
--format png \
Expand All @@ -160,7 +160,7 @@ trickery image -i prompts/generate_icon.md \
### JSON Output

```bash
trickery image -i prompts/generate_diagram.md --save result.png -o json
trickery image prompts/generate_diagram.md --save result.png -o json
```

Output:
Expand Down
12 changes: 6 additions & 6 deletions docs/input-images.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ Unknown extensions default to PNG MIME type.
### Single Local Image

```bash
trickery generate -i prompts/describe_image.md --image test_data/example_images/image2.png
trickery generate prompts/describe_image.md --image test_data/example_images/image2.png
```

Where `prompts/describe_image.md` contains:
Expand All @@ -45,13 +45,13 @@ Describe what you see in this image in detail.
### Image from URL

```bash
trickery generate -i prompts/describe_image.md --image https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png
trickery generate prompts/describe_image.md --image https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png
```

### Multiple Images

```bash
trickery generate -i prompts/catalog_images.md \
trickery generate prompts/catalog_images.md \
--image test_data/example_images/image1.png \
--image test_data/example_images/image2.png \
--image test_data/example_images/image3.jpg
Expand All @@ -61,13 +61,13 @@ trickery generate -i prompts/catalog_images.md \

```bash
# Low detail for quick classification
trickery generate -i prompts/describe_image.md --image test_data/example_images/image1.png --image-detail low
trickery generate prompts/describe_image.md --image test_data/example_images/image1.png --image-detail low
```

### Combined with Variables

```bash
trickery generate -i prompts/review_ui.md \
trickery generate prompts/review_ui.md \
--image test_data/example_images/image2.png \
--var focus="accessibility" \
--var format="bullet points"
Expand Down Expand Up @@ -100,7 +100,7 @@ Image support requires a vision-capable model. Recommended models:

Example with explicit model:
```bash
trickery generate -i prompts/describe_image.md --image test_data/example_images/image1.png --model gpt-5.2
trickery generate prompts/describe_image.md --image test_data/example_images/image1.png --model gpt-5.2
```

## Token Considerations
Expand Down
4 changes: 2 additions & 2 deletions prompts/trickery_readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Idea is simple, imagine you need to generate some docs using LLM as part of CI,

> [!TIP]
> This README was generated with trickery
> trickery generate -i ./prompts/trickery_readme.md > README.md
> trickery generate ./prompts/trickery_readme.md > README.md


## Demo
Expand All @@ -39,7 +39,7 @@ trickery --help

```sh
export OPENAI_API_KEY=s....d
trickery generate -i ./prompts/trickery_readme.md > README.md
trickery generate ./prompts/trickery_readme.md > README.md
```

Input file could be any text file, with Jinja2-like template variables, like `{{"{{app_version}}"}}`. To set this variables, please use `-v` flag, like `-v app_version=1.0.0`.
Expand Down
80 changes: 80 additions & 0 deletions specs/text-input.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Text Input

## Abstract

Trickery's input supports both file paths and direct text, with auto-detection. If the provided value exists as a file, it reads from the file; otherwise, it treats the value as direct prompt text. Input is typically provided as a positional argument.

## Requirements

### Input Methods

Input is provided as a positional argument:

```bash
trickery generate "prompt text"
trickery generate prompts/greeting.md
```

The `-i` flag is also supported for backwards compatibility but positional is preferred.

### Input Auto-Detection

Once input is provided (either way), this logic applies:
1. Check if the input value exists as a file on disk
2. If file exists: read content from the file
3. If file doesn't exist: use the input value directly as prompt text

### Behavior

```bash
# File input (file exists, content read from file)
trickery generate prompts/greeting.md

# Text input (not a file, used as direct prompt)
trickery generate "Write a haiku"
```

- Template variables work with both: `--var name=Alice`
- For `image` command, output filename defaults to `image-xxxxx.png` when input is text

### Long Text Support

Positional input supports:

- Multi-line strings (using shell quoting)
- Special characters and Unicode
- Very long prompts (limited only by shell argument length)

### Shell Integration

Examples of passing long text:

```bash
# Multi-line with shell quoting
trickery generate "Line 1
Line 2
Line 3"

# Using heredoc
trickery generate "$(cat <<'EOF'
You are a helpful assistant.

Please analyze the following:
- Point 1
- Point 2
EOF
)"
```

## Design Choices

### Why keep -i as fallback?

1. Backwards compatibility with older scripts
2. Useful when input looks like a flag (edge case)

### Why auto-detect instead of separate options?

1. Simpler API: one input concept
2. Intuitive behavior: file paths look like file paths, text looks like text
3. No ambiguity in practice: prompts rarely look like existing file paths
57 changes: 40 additions & 17 deletions src/commands/generate.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use clap::{Args, ValueHint};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use std::path::Path;
use tokio::fs::read_to_string;

use super::super::trickery::generate::{generate_from_template, GenerateConfig};
Expand Down Expand Up @@ -31,14 +31,22 @@ fn parse_key_val(s: &str) -> Result<(String, Value), String> {
}

#[derive(Args)]
#[command(
args_conflicts_with_subcommands = true,
override_usage = "trickery generate [INPUT] [OPTIONS]"
)]
pub struct GenerateArgs {
/// Path to the input prompt file
#[arg(short, long, value_hint = ValueHint::FilePath)]
input: Option<PathBuf>,
/// Input prompt: file path or direct text (auto-detected)
#[arg(index = 1, value_name = "INPUT", value_hint = ValueHint::FilePath)]
pub input_positional: Option<String>,

/// Input prompt: file path or direct text (auto-detected)
#[arg(short, long = "input", value_name = "INPUT", value_hint = ValueHint::FilePath)]
pub input_option: Option<String>,

/// Variables to be used in prompt
#[arg(short, long="var", value_parser = parse_key_val, number_of_values = 1)]
vars: Vec<(String, Value)>,
pub vars: Vec<(String, Value)>,

/// Model to use (e.g., gpt-5.2, gpt-5-mini, o1, o3-mini)
#[arg(short, long)]
Expand All @@ -65,30 +73,45 @@ fn parse_reasoning_level(s: &str) -> Result<ReasoningLevel, String> {
s.parse()
}

/// Resolve input to template content.
/// If input exists as a file, read from file; otherwise treat as direct text.
async fn resolve_input(input: &str) -> Result<String, Box<dyn std::error::Error>> {
let path = Path::new(input);
if path.exists() {
read_to_string(path)
.await
.map_err(|e| format!("Failed to read input file '{}': {}", path.display(), e).into())
} else {
Ok(input.to_string())
}
}

impl GenerateArgs {
/// Get input from either positional or -i option
pub fn get_input(&self) -> Option<&String> {
self.input_positional
.as_ref()
.or(self.input_option.as_ref())
}
}

impl CommandExec<GenerateResult> for GenerateArgs {
async fn exec(
&self,
context: &impl super::CommandExecutionContext,
) -> Result<Box<dyn CommandResult<GenerateResult>>, Box<dyn std::error::Error>> {
let input_path = match &self.input {
Some(path) => path,
None => return Err("Input file path is required".into()),
};
let input = self
.get_input()
.ok_or("Input required: use positional arg or -i (file path or text)")?;

let template = resolve_input(input).await?;

let input_variables: HashMap<String, Value> = self
.vars
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect();

let template: String = read_to_string(input_path).await.map_err(|e| {
format!(
"Failed to read input file '{}': {}",
input_path.display(),
e
)
})?;

let images: Vec<String> = self.image.clone();

let config = GenerateConfig {
Expand Down
Loading