feat: add content target type for writing literal text to files
All checks were successful
Continuous Integration / Build Package (push) Successful in 43s
Continuous Integration / Lint, Check & Test (push) Successful in 1m1s

Content targets write a string directly to the output file without
invoking any AI provider. They don't require API keys and are not
archived when overwritten.

Example usage in .hokusai.yaml:
  file.txt:
    content: ABC
This commit is contained in:
Konstantin Fickel 2026-02-21 18:14:09 +01:00
parent fda28b5afa
commit bb03975ece
Signed by: kfickel
GPG key ID: A793722F9933C1A5
5 changed files with 163 additions and 10 deletions

View file

@ -12,7 +12,12 @@ from pathlib import Path
import httpx
from hokusai.archive import archive_file
from hokusai.config import DownloadTargetConfig, GenerateTargetConfig, ProjectConfig
from hokusai.config import (
ContentTargetConfig,
DownloadTargetConfig,
GenerateTargetConfig,
ProjectConfig,
)
from hokusai.graph import build_graph, get_build_order, get_subgraph_for_target
from hokusai.prompt import extract_placeholder_files, resolve_prompt
from hokusai.providers import Provider
@ -151,12 +156,17 @@ async def _build_single_target(
# Ensure parent directories exist for targets in subfolders.
output_path.parent.mkdir(parents=True, exist_ok=True)
target_cfg = config.targets[target_name]
# Content targets write literal text — no archiving, no provider needed.
if isinstance(target_cfg, ContentTargetConfig):
_ = output_path.write_text(target_cfg.content)
return
# Archive the existing artifact before overwriting.
if config.archive_folder is not None:
_ = archive_file(output_path, project_dir, config.archive_folder)
target_cfg = config.targets[target_name]
if isinstance(target_cfg, DownloadTargetConfig):
await _download_target(target_name, target_cfg, project_dir)
return
@ -267,6 +277,14 @@ def _is_dirty(
"""Check if a target needs rebuilding."""
target_cfg = config.targets[target_name]
if isinstance(target_cfg, ContentTargetConfig):
return is_target_dirty(
target_name,
content=target_cfg.content,
state=state,
project_dir=project_dir,
)
if isinstance(target_cfg, DownloadTargetConfig):
return is_target_dirty(
target_name,
@ -300,7 +318,7 @@ def _has_provider(
) -> bool:
"""Check that the required provider is available; record failure if not."""
target_cfg = config.targets[target_name]
if isinstance(target_cfg, DownloadTargetConfig):
if isinstance(target_cfg, (DownloadTargetConfig, ContentTargetConfig)):
return True
model_info = resolve_model(target_name, target_cfg, config.defaults)
if model_info.name not in provider_index:
@ -345,7 +363,14 @@ def _process_outcomes(
else:
target_cfg = config.targets[name]
if isinstance(target_cfg, DownloadTargetConfig):
if isinstance(target_cfg, ContentTargetConfig):
record_target_state(
name,
content=target_cfg.content,
state=state,
project_dir=project_dir,
)
elif isinstance(target_cfg, DownloadTargetConfig):
record_target_state(
name,
download=target_cfg.download,