on main help sign in
feat(prompts): extend templating system to more prompts
0c1014eKieran Klukas
internal/agent/tools/crush_logs.go+21−3
@@ -5,6 +5,7 @@ import (
5 _ "embed"
5 _ "embed"
6 "encoding/json"
6 "encoding/json"
7 "fmt"
7 "fmt"
8+ "html/template"
8 "io"
9 "io"
9 "os"
10 "os"
10 "path/filepath"
11 "path/filepath"
@@ -18,8 +19,25 @@ import (
18
19
19const CrushLogsToolName = "crush_logs"
20const CrushLogsToolName = "crush_logs"
20
21
21−//go:embed crush_logs.md
22+//go:embed crush_logs.md.tpl
23+var crushLogsDescriptionTmpl []byte
24+
25+var crushLogsDescriptionTpl = template.Must(
26+ template.New("crushLogsDescription").
27+ Parse(string(crushLogsDescriptionTmpl)),
28+)
29+
30+type crushLogsDescriptionData struct {
31+ DefaultLines int
32+ MaxLines int
33+}
34+
22−var crushLogsDescription string
35+func crushLogsDescription() string {
36+ return renderTemplate(crushLogsDescriptionTpl, crushLogsDescriptionData{
37+ DefaultLines: defaultLogLines,
38+ MaxLines: maxLogLines,
39+ })
40+}
23
41
24// Max line size to prevent memory issues with very long log lines (1 MB).
42// Max line size to prevent memory issues with very long log lines (1 MB).
25const maxLogLineSize = 1024 * 1024
43const maxLogLineSize = 1024 * 1024
@@ -58,7 +76,7 @@ type CrushLogsParams struct {
58func NewCrushLogsTool(logFile string) fantasy.AgentTool {
76func NewCrushLogsTool(logFile string) fantasy.AgentTool {
59 return fantasy.NewAgentTool(
77 return fantasy.NewAgentTool(
60 CrushLogsToolName,
78 CrushLogsToolName,
61− crushLogsDescription,
79+ crushLogsDescription(),
62 func(ctx context.Context, params CrushLogsParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
80 func(ctx context.Context, params CrushLogsParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
63 result := runCrushLogs(logFile, params)
81 result := runCrushLogs(logFile, params)
64 return fantasy.NewTextResponse(result), nil
82 return fantasy.NewTextResponse(result), nil
internal/agent/tools/crush_logs.md → internal/agent/tools/crush_logs.md.tplrenamed+2−2
@@ -1,4 +1,4 @@
1−Read Crush's internal application logs (default 50 entries, max 100); useful for diagnosing provider errors, tool failures, LSP/MCP issues.
1+Read Crush's internal application logs (default {{ .DefaultLines }} entries, max {{ .MaxLines }}); useful for diagnosing provider errors, tool failures, LSP/MCP issues.
2
2
3<usage>
3<usage>
4- Returns recent log entries from Crush's internal log file
4- Returns recent log entries from Crush's internal log file
@@ -8,6 +8,6 @@ Read Crush's internal application logs (default 50 entries, max 100); useful for
8</usage>
8</usage>
9
9
10<tips>
10<tips>
11−- Default returns last 50 entries; use lines parameter for more (max 100)
11+- Default returns last {{ .DefaultLines }} entries; use lines parameter for more (max {{ .MaxLines }})
12- Look for ERROR and WARN entries first when diagnosing problems
12- Look for ERROR and WARN entries first when diagnosing problems
13</tips>
13</tips>
internal/agent/tools/download.go+19−3
@@ -5,6 +5,7 @@ import (
5 "context"
5 "context"
6 _ "embed"
6 _ "embed"
7 "fmt"
7 "fmt"
8+ "html/template"
8 "io"
9 "io"
9 "net/http"
10 "net/http"
10 "os"
11 "os"
@@ -31,8 +32,23 @@ type DownloadPermissionsParams struct {
31
32
32const DownloadToolName = "download"
33const DownloadToolName = "download"
33
34
34−//go:embed download.md
35+//go:embed download.md.tpl
36+var downloadDescriptionTmpl []byte
37+
38+var downloadDescriptionTpl = template.Must(
39+ template.New("downloadDescription").
40+ Parse(string(downloadDescriptionTmpl)),
41+)
42+
43+type downloadDescriptionData struct {
44+ MaxDownloadTimeout int
45+}
46+
35−var downloadDescription string
47+func downloadDescription() string {
48+ return renderTemplate(downloadDescriptionTpl, downloadDescriptionData{
49+ MaxDownloadTimeout: 600,
50+ })
51+}
36
52
37func NewDownloadTool(permissions permission.Service, workingDir string, client *http.Client) fantasy.AgentTool {
53func NewDownloadTool(permissions permission.Service, workingDir string, client *http.Client) fantasy.AgentTool {
38 if client == nil {
54 if client == nil {
@@ -48,7 +64,7 @@ func NewDownloadTool(permissions permission.Service, workingDir string, client *
48 }
64 }
49 return fantasy.NewParallelAgentTool(
65 return fantasy.NewParallelAgentTool(
50 DownloadToolName,
66 DownloadToolName,
51− downloadDescription,
67+ downloadDescription(),
52 func(ctx context.Context, params DownloadParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
68 func(ctx context.Context, params DownloadParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
53 if params.URL == "" {
69 if params.URL == "" {
54 return fantasy.NewTextErrorResponse("URL parameter is required"), nil
70 return fantasy.NewTextErrorResponse("URL parameter is required"), nil
internal/agent/tools/download.mddeleted−1
@@ -1 +0,0 @@
1−Download a URL directly to a local file (binary-safe, streaming, max 100MB); overwrites without warning. For reading content into context use fetch.
internal/agent/tools/download.md.tplnew file+1
@@ -0,0 +1 @@
1+Download a URL directly to a local file (binary-safe, streaming, max {{ .MaxDownloadTimeout }}s timeout); overwrites without warning. For reading content into context use fetch.
internal/agent/tools/fetch.go+13−1
@@ -30,6 +30,18 @@ var fetchDescriptionTpl = template.Must(
30 Parse(string(fetchDescriptionTmpl)),
30 Parse(string(fetchDescriptionTmpl)),
31)
31)
32
32
33+type fetchDescriptionData struct {
34+ GhAvailable bool
35+ MaxFetchSizeKB int
36+}
37+
38+func fetchDescription() string {
39+ return renderTemplate(fetchDescriptionTpl, fetchDescriptionData{
40+ GhAvailable: ghAvailable,
41+ MaxFetchSizeKB: MaxFetchSize / 1024,
42+ })
43+}
44+
33func NewFetchTool(permissions permission.Service, workingDir string, client *http.Client) fantasy.AgentTool {
45func NewFetchTool(permissions permission.Service, workingDir string, client *http.Client) fantasy.AgentTool {
34 if client == nil {
46 if client == nil {
35 transport := http.DefaultTransport.(*http.Transport).Clone()
47 transport := http.DefaultTransport.(*http.Transport).Clone()
@@ -45,7 +57,7 @@ func NewFetchTool(permissions permission.Service, workingDir string, client *htt
45
57
46 return fantasy.NewParallelAgentTool(
58 return fantasy.NewParallelAgentTool(
47 FetchToolName,
59 FetchToolName,
48− renderToolDescription(fetchDescriptionTpl),
60+ fetchDescription(),
49 func(ctx context.Context, params FetchParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
61 func(ctx context.Context, params FetchParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
50 if params.URL == "" {
62 if params.URL == "" {
51 return fantasy.NewTextErrorResponse("URL parameter is required"), nil
63 return fantasy.NewTextErrorResponse("URL parameter is required"), nil
internal/agent/tools/fetch.md.tpl+1−1
@@ -1,2 +1,2 @@
1−Fetch raw content from a URL as text, markdown, or html (max 100KB); no AI processing. For analysis or extraction use agentic_fetch.
1+Fetch raw content from a URL as text, markdown, or html (max {{ .MaxFetchSizeKB }}KB); no AI processing. For analysis or extraction use agentic_fetch.
2{{- if .GhAvailable }} For GitHub content when an exact repo, issue, or PR link is provided, use `gh` CLI in bash instead.{{- end }}
2{{- if .GhAvailable }} For GitHub content when an exact repo, issue, or PR link is provided, use `gh` CLI in bash instead.{{- end }}
internal/agent/tools/glob.go+19−3
@@ -6,6 +6,7 @@ import (
6 "context"
6 "context"
7 _ "embed"
7 _ "embed"
8 "fmt"
8 "fmt"
9+ "html/template"
9 "log/slog"
10 "log/slog"
10 "os/exec"
11 "os/exec"
11 "path/filepath"
12 "path/filepath"
@@ -19,8 +20,23 @@ import (
19
20
20const GlobToolName = "glob"
21const GlobToolName = "glob"
21
22
22−//go:embed glob.md
23+//go:embed glob.md.tpl
24+var globDescriptionTmpl []byte
25+
26+var globDescriptionTpl = template.Must(
27+ template.New("globDescription").
28+ Parse(string(globDescriptionTmpl)),
29+)
30+
31+type globDescriptionData struct {
32+ MaxResults int
33+}
34+
23−var globDescription string
35+func globDescription() string {
36+ return renderTemplate(globDescriptionTpl, globDescriptionData{
37+ MaxResults: 100,
38+ })
39+}
24
40
25type GlobParams struct {
41type GlobParams struct {
26 Pattern string `json:"pattern" description:"The glob pattern to match files against"`
42 Pattern string `json:"pattern" description:"The glob pattern to match files against"`
@@ -35,7 +51,7 @@ type GlobResponseMetadata struct {
35func NewGlobTool(workingDir string) fantasy.AgentTool {
51func NewGlobTool(workingDir string) fantasy.AgentTool {
36 return fantasy.NewAgentTool(
52 return fantasy.NewAgentTool(
37 GlobToolName,
53 GlobToolName,
38− globDescription,
54+ globDescription(),
39 func(ctx context.Context, params GlobParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
55 func(ctx context.Context, params GlobParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
40 if params.Pattern == "" {
56 if params.Pattern == "" {
41 return fantasy.NewTextErrorResponse("pattern is required"), nil
57 return fantasy.NewTextErrorResponse("pattern is required"), nil
internal/agent/tools/glob.mddeleted−1
@@ -1 +0,0 @@
1−Find files by name/pattern (glob syntax), sorted by modification time; max 100 results; skips hidden files. Use grep to search file contents.
internal/agent/tools/glob.md.tplnew file+1
@@ -0,0 +1 @@
1+Find files by name/pattern (glob syntax), sorted by modification time; max {{ .MaxResults }} results; skips hidden files. Use grep to search file contents.
internal/agent/tools/grep.go+19−3
@@ -8,6 +8,7 @@ import (
8 _ "embed"
8 _ "embed"
9 "encoding/json"
9 "encoding/json"
10 "fmt"
10 "fmt"
11+ "html/template"
11 "io"
12 "io"
12 "net/http"
13 "net/http"
13 "os"
14 "os"
@@ -89,8 +90,23 @@ const (
89 maxGrepContentWidth = 500
90 maxGrepContentWidth = 500
90)
91)
91
92
92−//go:embed grep.md
93+//go:embed grep.md.tpl
94+var grepDescriptionTmpl []byte
95+
96+var grepDescriptionTpl = template.Must(
97+ template.New("grepDescription").
98+ Parse(string(grepDescriptionTmpl)),
99+)
100+
101+type grepDescriptionData struct {
102+ MaxResults int
103+}
104+
93−var grepDescription string
105+func grepDescription() string {
106+ return renderTemplate(grepDescriptionTpl, grepDescriptionData{
107+ MaxResults: 100,
108+ })
109+}
94
110
95// escapeRegexPattern escapes special regex characters so they're treated as literal characters
111// escapeRegexPattern escapes special regex characters so they're treated as literal characters
96func escapeRegexPattern(pattern string) string {
112func escapeRegexPattern(pattern string) string {
@@ -107,7 +123,7 @@ func escapeRegexPattern(pattern string) string {
107func NewGrepTool(workingDir string, config config.ToolGrep) fantasy.AgentTool {
123func NewGrepTool(workingDir string, config config.ToolGrep) fantasy.AgentTool {
108 return fantasy.NewAgentTool(
124 return fantasy.NewAgentTool(
109 GrepToolName,
125 GrepToolName,
110− grepDescription,
126+ grepDescription(),
111 func(ctx context.Context, params GrepParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
127 func(ctx context.Context, params GrepParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
112 if params.Pattern == "" {
128 if params.Pattern == "" {
113 return fantasy.NewTextErrorResponse("pattern is required"), nil
129 return fantasy.NewTextErrorResponse("pattern is required"), nil
internal/agent/tools/grep.mddeleted−1
@@ -1 +0,0 @@
1−Search file contents by regex or literal text; returns matching file paths sorted by modification time (max 100); respects .gitignore. Use glob to filter by filename, not contents.
internal/agent/tools/grep.md.tplnew file+1
@@ -0,0 +1 @@
1+Search file contents by regex or literal text; returns matching file paths sorted by modification time (max {{ .MaxResults }}); respects .gitignore. Use glob to filter by filename, not contents.
internal/agent/tools/ls.go+19−3
@@ -5,6 +5,7 @@ import (
5 "context"
5 "context"
6 _ "embed"
6 _ "embed"
7 "fmt"
7 "fmt"
8+ "html/template"
8 "os"
9 "os"
9 "path/filepath"
10 "path/filepath"
10 "strings"
11 "strings"
@@ -52,13 +53,28 @@ const (
52 maxLSFiles = 1000
53 maxLSFiles = 1000
53)
54)
54
55
55−//go:embed ls.md
56+//go:embed ls.md.tpl
57+var lsDescriptionTmpl []byte
58+
59+var lsDescriptionTpl = template.Must(
60+ template.New("lsDescription").
61+ Parse(string(lsDescriptionTmpl)),
62+)
63+
64+type lsDescriptionData struct {
65+ MaxFiles int
66+}
67+
56−var lsDescription string
68+func lsDescription() string {
69+ return renderTemplate(lsDescriptionTpl, lsDescriptionData{
70+ MaxFiles: maxLSFiles,
71+ })
72+}
57
73
58func NewLsTool(permissions permission.Service, workingDir string, lsConfig config.ToolLs) fantasy.AgentTool {
74func NewLsTool(permissions permission.Service, workingDir string, lsConfig config.ToolLs) fantasy.AgentTool {
59 return fantasy.NewAgentTool(
75 return fantasy.NewAgentTool(
60 LSToolName,
76 LSToolName,
61− lsDescription,
77+ lsDescription(),
62 func(ctx context.Context, params LSParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
78 func(ctx context.Context, params LSParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
63 searchPath, err := fsext.Expand(cmp.Or(params.Path, workingDir))
79 searchPath, err := fsext.Expand(cmp.Or(params.Path, workingDir))
64 if err != nil {
80 if err != nil {
internal/agent/tools/ls.mddeleted−1
@@ -1 +0,0 @@
1−List files and directories as a tree; skips hidden files and common system dirs; max 1000 files. Use glob to find files by pattern, grep to search contents.
internal/agent/tools/ls.md.tplnew file+1
@@ -0,0 +1 @@
1+List files and directories as a tree; skips hidden files and common system dirs; max {{ .MaxFiles }} files. Use glob to find files by pattern, grep to search contents.
internal/agent/tools/sourcegraph.go+19−3
@@ -6,6 +6,7 @@ import (
6 _ "embed"
6 _ "embed"
7 "encoding/json"
7 "encoding/json"
8 "fmt"
8 "fmt"
9+ "html/template"
9 "io"
10 "io"
10 "net/http"
11 "net/http"
11 "strings"
12 "strings"
@@ -28,8 +29,23 @@ type SourcegraphResponseMetadata struct {
28
29
29const SourcegraphToolName = "sourcegraph"
30const SourcegraphToolName = "sourcegraph"
30
31
31−//go:embed sourcegraph.md
32+//go:embed sourcegraph.md.tpl
33+var sourcegraphDescriptionTmpl []byte
34+
35+var sourcegraphDescriptionTpl = template.Must(
36+ template.New("sourcegraphDescription").
37+ Parse(string(sourcegraphDescriptionTmpl)),
38+)
39+
40+type sourcegraphDescriptionData struct {
41+ MaxResults int
42+}
43+
32−var sourcegraphDescription string
44+func sourcegraphDescription() string {
45+ return renderTemplate(sourcegraphDescriptionTpl, sourcegraphDescriptionData{
46+ MaxResults: 20,
47+ })
48+}
33
49
34func NewSourcegraphTool(client *http.Client) fantasy.AgentTool {
50func NewSourcegraphTool(client *http.Client) fantasy.AgentTool {
35 if client == nil {
51 if client == nil {
@@ -45,7 +61,7 @@ func NewSourcegraphTool(client *http.Client) fantasy.AgentTool {
45 }
61 }
46 return fantasy.NewParallelAgentTool(
62 return fantasy.NewParallelAgentTool(
47 SourcegraphToolName,
63 SourcegraphToolName,
48− sourcegraphDescription,
64+ sourcegraphDescription(),
49 func(ctx context.Context, params SourcegraphParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
65 func(ctx context.Context, params SourcegraphParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
50 if params.Query == "" {
66 if params.Query == "" {
51 return fantasy.NewTextErrorResponse("Query parameter is required"), nil
67 return fantasy.NewTextErrorResponse("Query parameter is required"), nil
internal/agent/tools/sourcegraph.md → internal/agent/tools/sourcegraph.md.tplrenamed+1−1
@@ -1 +1 @@
1−Search code across public GitHub repositories via Sourcegraph; supports regex, language/repo/file filters, and symbol search (max 20 results). Only searches public repos.
1+Search code across public GitHub repositories via Sourcegraph; supports regex, language/repo/file filters, and symbol search (max {{ .MaxResults }} results). Only searches public repos.
internal/agent/tools/tools.go+9
@@ -90,3 +90,12 @@ func renderToolDescription(tmpl *template.Template) string {
90 }
90 }
91 return out.String()
91 return out.String()
92}
92}
93+
94+// renderTemplate renders a Go template with the given data.
95+func renderTemplate(tmpl *template.Template, data any) string {
96+ var out bytes.Buffer
97+ if err := tmpl.Execute(&out, data); err != nil {
98+ panic("failed to execute tool description template: " + err.Error())
99+ }
100+ return out.String()
101+}
internal/agent/tools/view.go+21−3
@@ -6,6 +6,7 @@ import (
6 _ "embed"
6 _ "embed"
7 "errors"
7 "errors"
8 "fmt"
8 "fmt"
9+ "html/template"
9 "io"
10 "io"
10 "io/fs"
11 "io/fs"
11 "net/http"
12 "net/http"
@@ -23,8 +24,25 @@ import (
23 "github.com/charmbracelet/crush/internal/skills"
24 "github.com/charmbracelet/crush/internal/skills"
24)
25)
25
26
26−//go:embed view.md
27+//go:embed view.md.tpl
28+var viewDescriptionTmpl []byte
29+
30+var viewDescriptionTpl = template.Must(
31+ template.New("viewDescription").
32+ Parse(string(viewDescriptionTmpl)),
33+)
34+
35+type viewDescriptionData struct {
36+ DefaultReadLimit int
37+ MaxViewSizeKB int
38+}
39+
27−var viewDescription string
40+func viewDescription() string {
41+ return renderTemplate(viewDescriptionTpl, viewDescriptionData{
42+ DefaultReadLimit: DefaultReadLimit,
43+ MaxViewSizeKB: MaxViewSize / 1024,
44+ })
45+}
28
46
29type ViewParams struct {
47type ViewParams struct {
30 FilePath string `json:"file_path" description:"The path to the file to read"`
48 FilePath string `json:"file_path" description:"The path to the file to read"`
@@ -79,7 +97,7 @@ func NewViewTool(
79) fantasy.AgentTool {
97) fantasy.AgentTool {
80 return fantasy.NewAgentTool(
98 return fantasy.NewAgentTool(
81 ViewToolName,
99 ViewToolName,
82− viewDescription,
100+ viewDescription(),
83 func(ctx context.Context, params ViewParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
101 func(ctx context.Context, params ViewParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
84 if params.FilePath == "" {
102 if params.FilePath == "" {
85 return fantasy.NewTextErrorResponse("file_path is required"), nil
103 return fantasy.NewTextErrorResponse("file_path is required"), nil
internal/agent/tools/view.mddeleted−1
@@ -1 +0,0 @@
1−Read a file by path with line numbers; supports offset and line limit (default 2000, max 200KB); renders images (PNG, JPEG, GIF, BMP, SVG, WebP); use ls for directories.
internal/agent/tools/view.md.tplnew file+1
@@ -0,0 +1 @@
1+Read a file by path with line numbers; supports offset and line limit (default {{ .DefaultReadLimit }}, max {{ .MaxViewSizeKB }}KB returned file content section); renders images (PNG, JPEG, GIF, WebP); use ls for directories.