Detailed changes
@@ -5,6 +5,7 @@ import (
_ "embed"
"encoding/json"
"fmt"
+ "html/template"
"io"
"os"
"path/filepath"
@@ -18,8 +19,25 @@ import (
const CrushLogsToolName = "crush_logs"
-//go:embed crush_logs.md
-var crushLogsDescription string
+//go:embed crush_logs.md.tpl
+var crushLogsDescriptionTmpl []byte
+
+var crushLogsDescriptionTpl = template.Must(
+ template.New("crushLogsDescription").
+ Parse(string(crushLogsDescriptionTmpl)),
+)
+
+type crushLogsDescriptionData struct {
+ DefaultLines int
+ MaxLines int
+}
+
+func crushLogsDescription() string {
+ return renderTemplate(crushLogsDescriptionTpl, crushLogsDescriptionData{
+ DefaultLines: defaultLogLines,
+ MaxLines: maxLogLines,
+ })
+}
// Max line size to prevent memory issues with very long log lines (1 MB).
const maxLogLineSize = 1024 * 1024
@@ -58,7 +76,7 @@ type CrushLogsParams struct {
func NewCrushLogsTool(logFile string) fantasy.AgentTool {
return fantasy.NewAgentTool(
CrushLogsToolName,
- crushLogsDescription,
+ crushLogsDescription(),
func(ctx context.Context, params CrushLogsParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
result := runCrushLogs(logFile, params)
return fantasy.NewTextResponse(result), nil
@@ -1,4 +1,4 @@
-Read Crush's internal application logs (default 50 entries, max 100); useful for diagnosing provider errors, tool failures, LSP/MCP issues.
+Read Crush's internal application logs (default {{ .DefaultLines }} entries, max {{ .MaxLines }}); useful for diagnosing provider errors, tool failures, LSP/MCP issues.
<usage>
- 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
</usage>
<tips>
-- Default returns last 50 entries; use lines parameter for more (max 100)
+- Default returns last {{ .DefaultLines }} entries; use lines parameter for more (max {{ .MaxLines }})
- Look for ERROR and WARN entries first when diagnosing problems
</tips>
@@ -5,6 +5,7 @@ import (
"context"
_ "embed"
"fmt"
+ "html/template"
"io"
"net/http"
"os"
@@ -31,8 +32,23 @@ type DownloadPermissionsParams struct {
const DownloadToolName = "download"
-//go:embed download.md
-var downloadDescription string
+//go:embed download.md.tpl
+var downloadDescriptionTmpl []byte
+
+var downloadDescriptionTpl = template.Must(
+ template.New("downloadDescription").
+ Parse(string(downloadDescriptionTmpl)),
+)
+
+type downloadDescriptionData struct {
+ MaxDownloadTimeout int
+}
+
+func downloadDescription() string {
+ return renderTemplate(downloadDescriptionTpl, downloadDescriptionData{
+ MaxDownloadTimeout: 600,
+ })
+}
func NewDownloadTool(permissions permission.Service, workingDir string, client *http.Client) fantasy.AgentTool {
if client == nil {
@@ -48,7 +64,7 @@ func NewDownloadTool(permissions permission.Service, workingDir string, client *
}
return fantasy.NewParallelAgentTool(
DownloadToolName,
- downloadDescription,
+ downloadDescription(),
func(ctx context.Context, params DownloadParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
if params.URL == "" {
return fantasy.NewTextErrorResponse("URL parameter is required"), nil
@@ -1 +0,0 @@
-Download a URL directly to a local file (binary-safe, streaming, max 100MB); overwrites without warning. For reading content into context use fetch.
@@ -0,0 +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.
@@ -30,6 +30,18 @@ var fetchDescriptionTpl = template.Must(
Parse(string(fetchDescriptionTmpl)),
)
+type fetchDescriptionData struct {
+ GhAvailable bool
+ MaxFetchSizeKB int
+}
+
+func fetchDescription() string {
+ return renderTemplate(fetchDescriptionTpl, fetchDescriptionData{
+ GhAvailable: ghAvailable,
+ MaxFetchSizeKB: MaxFetchSize / 1024,
+ })
+}
+
func NewFetchTool(permissions permission.Service, workingDir string, client *http.Client) fantasy.AgentTool {
if client == nil {
transport := http.DefaultTransport.(*http.Transport).Clone()
@@ -45,7 +57,7 @@ func NewFetchTool(permissions permission.Service, workingDir string, client *htt
return fantasy.NewParallelAgentTool(
FetchToolName,
- renderToolDescription(fetchDescriptionTpl),
+ fetchDescription(),
func(ctx context.Context, params FetchParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
if params.URL == "" {
return fantasy.NewTextErrorResponse("URL parameter is required"), nil
@@ -1,2 +1,2 @@
-Fetch raw content from a URL as text, markdown, or html (max 100KB); no AI processing. For analysis or extraction use agentic_fetch.
+Fetch raw content from a URL as text, markdown, or html (max {{ .MaxFetchSizeKB }}KB); no AI processing. For analysis or extraction use agentic_fetch.
{{- if .GhAvailable }} For GitHub content when an exact repo, issue, or PR link is provided, use `gh` CLI in bash instead.{{- end }}
@@ -6,6 +6,7 @@ import (
"context"
_ "embed"
"fmt"
+ "html/template"
"log/slog"
"os/exec"
"path/filepath"
@@ -19,8 +20,23 @@ import (
const GlobToolName = "glob"
-//go:embed glob.md
-var globDescription string
+//go:embed glob.md.tpl
+var globDescriptionTmpl []byte
+
+var globDescriptionTpl = template.Must(
+ template.New("globDescription").
+ Parse(string(globDescriptionTmpl)),
+)
+
+type globDescriptionData struct {
+ MaxResults int
+}
+
+func globDescription() string {
+ return renderTemplate(globDescriptionTpl, globDescriptionData{
+ MaxResults: 100,
+ })
+}
type GlobParams struct {
Pattern string `json:"pattern" description:"The glob pattern to match files against"`
@@ -35,7 +51,7 @@ type GlobResponseMetadata struct {
func NewGlobTool(workingDir string) fantasy.AgentTool {
return fantasy.NewAgentTool(
GlobToolName,
- globDescription,
+ globDescription(),
func(ctx context.Context, params GlobParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
if params.Pattern == "" {
return fantasy.NewTextErrorResponse("pattern is required"), nil
@@ -1 +0,0 @@
-Find files by name/pattern (glob syntax), sorted by modification time; max 100 results; skips hidden files. Use grep to search file contents.
@@ -0,0 +1 @@
+Find files by name/pattern (glob syntax), sorted by modification time; max {{ .MaxResults }} results; skips hidden files. Use grep to search file contents.
@@ -8,6 +8,7 @@ import (
_ "embed"
"encoding/json"
"fmt"
+ "html/template"
"io"
"net/http"
"os"
@@ -89,8 +90,23 @@ const (
maxGrepContentWidth = 500
)
-//go:embed grep.md
-var grepDescription string
+//go:embed grep.md.tpl
+var grepDescriptionTmpl []byte
+
+var grepDescriptionTpl = template.Must(
+ template.New("grepDescription").
+ Parse(string(grepDescriptionTmpl)),
+)
+
+type grepDescriptionData struct {
+ MaxResults int
+}
+
+func grepDescription() string {
+ return renderTemplate(grepDescriptionTpl, grepDescriptionData{
+ MaxResults: 100,
+ })
+}
// escapeRegexPattern escapes special regex characters so they're treated as literal characters
func escapeRegexPattern(pattern string) string {
@@ -107,7 +123,7 @@ func escapeRegexPattern(pattern string) string {
func NewGrepTool(workingDir string, config config.ToolGrep) fantasy.AgentTool {
return fantasy.NewAgentTool(
GrepToolName,
- grepDescription,
+ grepDescription(),
func(ctx context.Context, params GrepParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
if params.Pattern == "" {
return fantasy.NewTextErrorResponse("pattern is required"), nil
@@ -1 +0,0 @@
-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.
@@ -0,0 +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.
@@ -5,6 +5,7 @@ import (
"context"
_ "embed"
"fmt"
+ "html/template"
"os"
"path/filepath"
"strings"
@@ -52,13 +53,28 @@ const (
maxLSFiles = 1000
)
-//go:embed ls.md
-var lsDescription string
+//go:embed ls.md.tpl
+var lsDescriptionTmpl []byte
+
+var lsDescriptionTpl = template.Must(
+ template.New("lsDescription").
+ Parse(string(lsDescriptionTmpl)),
+)
+
+type lsDescriptionData struct {
+ MaxFiles int
+}
+
+func lsDescription() string {
+ return renderTemplate(lsDescriptionTpl, lsDescriptionData{
+ MaxFiles: maxLSFiles,
+ })
+}
func NewLsTool(permissions permission.Service, workingDir string, lsConfig config.ToolLs) fantasy.AgentTool {
return fantasy.NewAgentTool(
LSToolName,
- lsDescription,
+ lsDescription(),
func(ctx context.Context, params LSParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
searchPath, err := fsext.Expand(cmp.Or(params.Path, workingDir))
if err != nil {
@@ -1 +0,0 @@
-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.
@@ -0,0 +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.
@@ -6,6 +6,7 @@ import (
_ "embed"
"encoding/json"
"fmt"
+ "html/template"
"io"
"net/http"
"strings"
@@ -28,8 +29,23 @@ type SourcegraphResponseMetadata struct {
const SourcegraphToolName = "sourcegraph"
-//go:embed sourcegraph.md
-var sourcegraphDescription string
+//go:embed sourcegraph.md.tpl
+var sourcegraphDescriptionTmpl []byte
+
+var sourcegraphDescriptionTpl = template.Must(
+ template.New("sourcegraphDescription").
+ Parse(string(sourcegraphDescriptionTmpl)),
+)
+
+type sourcegraphDescriptionData struct {
+ MaxResults int
+}
+
+func sourcegraphDescription() string {
+ return renderTemplate(sourcegraphDescriptionTpl, sourcegraphDescriptionData{
+ MaxResults: 20,
+ })
+}
func NewSourcegraphTool(client *http.Client) fantasy.AgentTool {
if client == nil {
@@ -45,7 +61,7 @@ func NewSourcegraphTool(client *http.Client) fantasy.AgentTool {
}
return fantasy.NewParallelAgentTool(
SourcegraphToolName,
- sourcegraphDescription,
+ sourcegraphDescription(),
func(ctx context.Context, params SourcegraphParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
if params.Query == "" {
return fantasy.NewTextErrorResponse("Query parameter is required"), nil
@@ -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.
+Search code across public GitHub repositories via Sourcegraph; supports regex, language/repo/file filters, and symbol search (max {{ .MaxResults }} results). Only searches public repos.
@@ -90,3 +90,12 @@ func renderToolDescription(tmpl *template.Template) string {
}
return out.String()
}
+
+// renderTemplate renders a Go template with the given data.
+func renderTemplate(tmpl *template.Template, data any) string {
+ var out bytes.Buffer
+ if err := tmpl.Execute(&out, data); err != nil {
+ panic("failed to execute tool description template: " + err.Error())
+ }
+ return out.String()
+}
@@ -6,6 +6,7 @@ import (
_ "embed"
"errors"
"fmt"
+ "html/template"
"io"
"io/fs"
"net/http"
@@ -23,8 +24,25 @@ import (
"github.com/charmbracelet/crush/internal/skills"
)
-//go:embed view.md
-var viewDescription string
+//go:embed view.md.tpl
+var viewDescriptionTmpl []byte
+
+var viewDescriptionTpl = template.Must(
+ template.New("viewDescription").
+ Parse(string(viewDescriptionTmpl)),
+)
+
+type viewDescriptionData struct {
+ DefaultReadLimit int
+ MaxViewSizeKB int
+}
+
+func viewDescription() string {
+ return renderTemplate(viewDescriptionTpl, viewDescriptionData{
+ DefaultReadLimit: DefaultReadLimit,
+ MaxViewSizeKB: MaxViewSize / 1024,
+ })
+}
type ViewParams struct {
FilePath string `json:"file_path" description:"The path to the file to read"`
@@ -79,7 +97,7 @@ func NewViewTool(
) fantasy.AgentTool {
return fantasy.NewAgentTool(
ViewToolName,
- viewDescription,
+ viewDescription(),
func(ctx context.Context, params ViewParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
if params.FilePath == "" {
return fantasy.NewTextErrorResponse("file_path is required"), nil
@@ -1 +0,0 @@
-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.
@@ -0,0 +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.