← back to Cli Printing Press
feat(llmpolish): add LLM Vision Synthesis for domain-aware customization
611f4b67c4fc1c5ae94106c3d99918ab8caeea32 · 2026-03-25 21:35:14 -0700 · Matt Van Horn
SynthesizeVision() takes profiler output + spec and asks Claude/Codex
to reason about which fields to index in FTS5, what to name workflow
commands, sync priorities, and realistic examples. Returns
VisionCustomization struct. Graceful nil,nil when no LLM available.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Files touched
A internal/llmpolish/vision.goA internal/llmpolish/vision_test.go
Diff
commit 611f4b67c4fc1c5ae94106c3d99918ab8caeea32
Author: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Date: Wed Mar 25 21:35:14 2026 -0700
feat(llmpolish): add LLM Vision Synthesis for domain-aware customization
SynthesizeVision() takes profiler output + spec and asks Claude/Codex
to reason about which fields to index in FTS5, what to name workflow
commands, sync priorities, and realistic examples. Returns
VisionCustomization struct. Graceful nil,nil when no LLM available.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---
internal/llmpolish/vision.go | 183 ++++++++++++++++++++++++++++++++++++++
internal/llmpolish/vision_test.go | 72 +++++++++++++++
2 files changed, 255 insertions(+)
diff --git a/internal/llmpolish/vision.go b/internal/llmpolish/vision.go
new file mode 100644
index 00000000..cd428d92
--- /dev/null
+++ b/internal/llmpolish/vision.go
@@ -0,0 +1,183 @@
+package llmpolish
+
+import (
+ "encoding/json"
+ "fmt"
+ "sort"
+ "strings"
+
+ "github.com/mvanhorn/cli-printing-press/internal/llm"
+ "github.com/mvanhorn/cli-printing-press/internal/profiler"
+ "github.com/mvanhorn/cli-printing-press/internal/spec"
+)
+
+// VisionCustomization holds LLM-generated customizations for vision templates.
+type VisionCustomization struct {
+ // ResourcePriority is the suggested sync order (most important first)
+ ResourcePriority []string `json:"resource_priority"`
+ // FTSFields maps resource name to fields that should be indexed in FTS5
+ FTSFields map[string][]string `json:"fts_fields"`
+ // WorkflowNames maps generic workflow names to domain-specific names
+ WorkflowNames map[string]string `json:"workflow_names"`
+ // ExampleOverrides maps command names to better domain-specific examples
+ ExampleOverrides map[string]string `json:"example_overrides"`
+ // DescOverrides maps command names to better domain-specific descriptions
+ DescOverrides map[string]string `json:"desc_overrides"`
+ // SyncHints maps resource name to sync strategy hints
+ SyncHints map[string]SyncHint `json:"sync_hints"`
+}
+
+// SyncHint describes how to sync a specific resource.
+type SyncHint struct {
+ Direction string `json:"direction"` // "newest_first" or "oldest_first"
+ BatchSize int `json:"batch_size"`
+ Priority int `json:"priority"` // 1 = highest
+}
+
+func SynthesizeVision(profile *profiler.APIProfile, apiSpec *spec.APISpec) (*VisionCustomization, error) {
+ if !llm.Available() {
+ return nil, nil
+ }
+
+ prompt := buildVisionPrompt(profile, apiSpec)
+ response, err := llm.Run(prompt)
+ if err != nil {
+ return nil, nil
+ }
+
+ var customization VisionCustomization
+ if err := json.Unmarshal([]byte(strings.TrimSpace(response)), &customization); err != nil {
+ return nil, nil
+ }
+
+ return &customization, nil
+}
+
+func buildVisionPrompt(profile *profiler.APIProfile, apiSpec *spec.APISpec) string {
+ if profile == nil {
+ profile = &profiler.APIProfile{}
+ }
+ if apiSpec == nil {
+ apiSpec = &spec.APISpec{}
+ }
+
+ var builder strings.Builder
+ fmt.Fprintf(&builder, "You are analyzing the %s API to customize a CLI's power-user features.\n\n", defaultString(apiSpec.Name, "unknown"))
+ builder.WriteString("The API profiler detected these signals:\n")
+ fmt.Fprintf(&builder, "- High volume: %t\n", profile.HighVolume)
+ fmt.Fprintf(&builder, "- Needs local search: %t\n", profile.NeedsSearch)
+ fmt.Fprintf(&builder, "- Has real-time events: %t\n", profile.HasRealtime)
+ fmt.Fprintf(&builder, "- Has chronological data: %t\n", profile.HasChronological)
+ fmt.Fprintf(&builder, "- Syncable resources: %s\n", formatStringSlice(profile.SyncableResources))
+ fmt.Fprintf(&builder, "- Searchable fields per resource: %s\n", formatSearchableFields(profile.SearchableFields))
+ if apiSpec.Description != "" {
+ fmt.Fprintf(&builder, "\nAPI description: %s\n", apiSpec.Description)
+ }
+
+ fmt.Fprintf(&builder, "\nProfiler recommended features: %s\n", formatStringSlice(profile.RecommendedFeatures()))
+ builder.WriteString("\nResources and their endpoints:\n")
+ builder.WriteString(formatResourceSummaries(apiSpec.Resources, ""))
+ builder.WriteString(`
+Based on this API's domain, answer these questions as a JSON object:
+
+1. resource_priority: What order should resources be synced? Put the most valuable/frequently-accessed resources first.
+
+2. fts_fields: For each syncable resource, which string fields contain actual searchable content (not IDs, not timestamps, not enum values)? Be selective - index content users would search for.
+
+3. workflow_names: Map these generic workflow names to domain-specific names that make sense for this API:
+ - "archive" -> what would a power user call this? (e.g., "archive" for Discord, "backup" for a CMS)
+ - "audit" -> what would they call this? (e.g., "audit" for Discord, "activity" for GitHub)
+
+4. example_overrides: For the sync, search, export, and tail commands, write realistic example command lines using actual resource names and plausible IDs.
+
+5. desc_overrides: For the sync, search, export, and tail commands, write developer-friendly one-line descriptions specific to this API's domain.
+
+6. sync_hints: For the top 3 syncable resources, specify:
+ - direction: "newest_first" or "oldest_first" (which makes more sense for this data?)
+ - batch_size: optimal page size (usually 100, but some APIs differ)
+ - priority: 1-3 ranking
+
+Return ONLY valid JSON, no markdown fences, no explanation.
+`)
+
+ return builder.String()
+}
+
+func defaultString(value, fallback string) string {
+ if strings.TrimSpace(value) == "" {
+ return fallback
+ }
+ return value
+}
+
+func formatStringSlice(values []string) string {
+ if len(values) == 0 {
+ return "[]"
+ }
+
+ sorted := append([]string(nil), values...)
+ sort.Strings(sorted)
+
+ return "[" + strings.Join(sorted, ", ") + "]"
+}
+
+func formatSearchableFields(fields map[string][]string) string {
+ if len(fields) == 0 {
+ return "{}"
+ }
+
+ resources := make([]string, 0, len(fields))
+ for resource := range fields {
+ resources = append(resources, resource)
+ }
+ sort.Strings(resources)
+
+ parts := make([]string, 0, len(resources))
+ for _, resource := range resources {
+ parts = append(parts, fmt.Sprintf("%s: %s", resource, formatStringSlice(fields[resource])))
+ }
+
+ return "{" + strings.Join(parts, "; ") + "}"
+}
+
+func formatResourceSummaries(resources map[string]spec.Resource, prefix string) string {
+ if len(resources) == 0 {
+ return "- none\n"
+ }
+
+ names := make([]string, 0, len(resources))
+ for name := range resources {
+ names = append(names, name)
+ }
+ sort.Strings(names)
+
+ var builder strings.Builder
+ for _, name := range names {
+ resource := resources[name]
+ fullName := name
+ if prefix != "" {
+ fullName = prefix + "." + name
+ }
+
+ endpoints := make([]string, 0, len(resource.Endpoints))
+ for endpointName, endpoint := range resource.Endpoints {
+ endpoints = append(endpoints, fmt.Sprintf("%s (%s)", endpointName, strings.ToUpper(endpoint.Method)))
+ }
+ sort.Strings(endpoints)
+
+ line := fmt.Sprintf("- %s", fullName)
+ if resource.Description != "" {
+ line += ": " + resource.Description
+ }
+ if len(endpoints) > 0 {
+ line += " | endpoints: " + strings.Join(endpoints, ", ")
+ }
+ builder.WriteString(line + "\n")
+
+ if len(resource.SubResources) > 0 {
+ builder.WriteString(formatResourceSummaries(resource.SubResources, fullName))
+ }
+ }
+
+ return builder.String()
+}
diff --git a/internal/llmpolish/vision_test.go b/internal/llmpolish/vision_test.go
new file mode 100644
index 00000000..90940dee
--- /dev/null
+++ b/internal/llmpolish/vision_test.go
@@ -0,0 +1,72 @@
+package llmpolish
+
+import (
+ "encoding/json"
+ "os"
+ "testing"
+
+ "github.com/mvanhorn/cli-printing-press/internal/profiler"
+ "github.com/mvanhorn/cli-printing-press/internal/spec"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestSynthesizeVisionReturnsNilWhenLLMUnavailable(t *testing.T) {
+ originalPath := os.Getenv("PATH")
+ t.Setenv("PATH", t.TempDir())
+ defer os.Setenv("PATH", originalPath)
+
+ customization, err := SynthesizeVision(nil, nil)
+
+ require.NoError(t, err)
+ assert.Nil(t, customization)
+}
+
+func TestParseVisionCustomization(t *testing.T) {
+ raw := `{"resource_priority":["messages","members","channels"],"fts_fields":{"messages":["content"],"members":["username","bio"]},"workflow_names":{"archive":"archive","audit":"audit-log"},"example_overrides":{"sync":"discord-cli sync --guild 1234567890"},"desc_overrides":{"sync":"Sync guild messages to local SQLite for offline search"},"sync_hints":{"messages":{"direction":"newest_first","batch_size":100,"priority":1}}}`
+
+ var vc VisionCustomization
+ err := json.Unmarshal([]byte(raw), &vc)
+
+ require.NoError(t, err)
+ assert.Len(t, vc.ResourcePriority, 3)
+ assert.Equal(t, []string{"content"}, vc.FTSFields["messages"])
+ assert.Equal(t, "archive", vc.WorkflowNames["archive"])
+}
+
+func TestBuildVisionPrompt(t *testing.T) {
+ profile := &profiler.APIProfile{
+ HighVolume: true,
+ NeedsSearch: true,
+ SyncableResources: []string{"messages", "channels"},
+ SearchableFields: map[string][]string{
+ "messages": {"content", "author"},
+ },
+ }
+ apiSpec := &spec.APISpec{
+ Name: "discord",
+ Description: "Discord API",
+ Resources: map[string]spec.Resource{
+ "channels": {
+ Endpoints: map[string]spec.Endpoint{
+ "list": {Method: "GET", Path: "/channels"},
+ },
+ },
+ },
+ }
+
+ prompt := buildVisionPrompt(profile, apiSpec)
+
+ assert.Contains(t, prompt, "discord")
+ assert.Contains(t, prompt, "messages")
+ assert.Contains(t, prompt, "High volume: true")
+}
+
+func TestBuildVisionPromptNilSafe(t *testing.T) {
+ assert.NotPanics(t, func() {
+ prompt := buildVisionPrompt(nil, nil)
+ assert.Contains(t, prompt, "unknown")
+ assert.Contains(t, prompt, "High volume: false")
+ assert.Contains(t, prompt, "Resources and their endpoints:")
+ })
+}
← a783ac12 fix(profiler): improve HighVolume and NeedsSearch heuristics
·
back to Cli Printing Press
·
feat(store): generate per-resource SQLite tables from profil bbd0a47c →