Go 128 lines
package prompts
// Package prompts is eeco's versioned canonical prompt library: one named,
// embedded text/template per AI-using workflow, the single reviewable source
// of truth for the instruction strings eeco sends to a model. The bodies live
// in templates/*.tmpl; the prompt name is the file name without the extension.
//
// At v1.0.0 the library ships two prompts: GetProjectType (Layer 4 init
// detection) and ManifestSummary (per-directory .ai.json body generation).
// Existing scattered prompts (evolve, understand, ...) migrate as additive
// v1.x slices.
import (
"bytes"
"embed"
"fmt"
"io/fs"
"sort"
"strings"
"text/template"
)
//go:embed templates/*.tmpl
var templatesFS embed.FS
// Prompt names. Each MUST match a templates/<name>.tmpl file.
const (
GetProjectType = "get-project-type"
ManifestSummary = "manifest-summary"
)
// funcs are the template helpers available to every prompt template.
var funcs = template.FuncMap{"join": strings.Join}
type entry struct {
raw string
tmpl *template.Template
}
// registry is built once at package load. A malformed shipped template panics
// here on purpose — a prompt template is a build-time artifact, not runtime
// input, so a parse failure must surface immediately, not at first render.
var registry = mustLoad()
func mustLoad() map[string]entry {
files, err := fs.ReadDir(templatesFS, "templates")
if err != nil {
panic("prompts: read templates dir: " + err.Error())
}
reg := make(map[string]entry, len(files))
for _, f := range files {
if f.IsDir() || !strings.HasSuffix(f.Name(), ".tmpl") {
continue
}
body, err := templatesFS.ReadFile("templates/" + f.Name())
if err != nil {
panic("prompts: read template " + f.Name() + ": " + err.Error())
}
name := strings.TrimSuffix(f.Name(), ".tmpl")
t, err := template.New(name).Funcs(funcs).Parse(string(body))
if err != nil {
panic(fmt.Sprintf("prompts: parse %s: %v", name, err))
}
reg[name] = entry{raw: string(body), tmpl: t}
}
return reg
}
// Names returns every available prompt name, sorted.
func Names() []string {
out := make([]string, 0, len(registry))
for n := range registry {
out = append(out, n)
}
sort.Strings(out)
return out
}
// Get returns the raw template body for a prompt — the canonical text an
// operator audits via `eeco show prompt <name>`.
func Get(name string) (string, error) {
e, ok := registry[name]
if !ok {
return "", fmt.Errorf("unknown prompt %q", name)
}
return e.raw, nil
}
// Render executes a prompt template against data and returns the result.
func Render(name string, data any) (string, error) {
e, ok := registry[name]
if !ok {
return "", fmt.Errorf("unknown prompt %q", name)
}
var b bytes.Buffer
if err := e.tmpl.Execute(&b, data); err != nil {
return "", fmt.Errorf("render %s: %w", name, err)
}
return b.String(), nil
}
// GetProjectTypeData is the render input for the GetProjectType prompt.
type GetProjectTypeData struct {
Categories []Category
Tree []string
Description string
}
// Category is one catalog entry rendered into the GetProjectType prompt.
type Category struct {
Category string
Description string
PickWhen string
Dirs []string
}
// ManifestSummaryData is the render input for the ManifestSummary prompt.
type ManifestSummaryData struct {
Dir string
Items []ManifestItem
}
// ManifestItem is one directory entry rendered into the ManifestSummary prompt.
type ManifestItem struct {
Path string
Kind string
}