Ghost v3.4.0 unifies file handling under a single :r command. Text files and images are now routed automatically via MIME type detection.

[*] ghost

A local, general-purpose AI assistant built in Go, powered by Ollama — inspired by cyberpunk worlds like Shadowrun, Cyberpunk 2077, and The Matrix.

One Command, Any File#

The v3.3.0 post teased image support as the last piece for TUI feature parity. This release delivers that and goes further: instead of separate commands for text (:r) and images (:i), there’s now just :r. Ghost figures out what to do with the file.

The routing logic reads the first 512 bytes and uses Go’s http.DetectContentType to identify the MIME type:

type FileType string

const (
	FileTypeText  FileType = "text"
	FileTypeImage FileType = "image"
	FileTypeDir   FileType = "dir"
)

var imageFileTypes = []string{
	"image/png",
	"image/jpeg",
	"image/webp",
}

func DetectFileType(path string) (FileType, error) {
	info, err := os.Stat(path)
	if err != nil {
		return "", fmt.Errorf("%w: %w", ErrFileAccess, err)
	}

	if info.IsDir() {
		return FileTypeDir, nil
	}

	if info.Size() > maxFileSize {
		return "", fmt.Errorf("%w (%d bytes)", ErrFileSize, info.Size())
	}

	file, err := os.Open(path)
	if err != nil {
		return "", fmt.Errorf("%w: %w", ErrReadFile, err)
	}
	defer func() { _ = file.Close() }()

	buffer := make([]byte, 512)
	n, err := file.Read(buffer)
	if err != nil {
		return "", fmt.Errorf("%w: %w", ErrReadFile, err)
	}

	mime := http.DetectContentType(buffer[:n])
	mediaType := strings.SplitN(mime, ";", 2)[0]

	if isImage(mediaType, path) {
		return FileTypeImage, nil
	}

	if isText(mediaType) {
		return FileTypeText, nil
	}

	return "", ErrFileTypeUnsupported
}

Text files get injected into the message history. Images route to the vision model for analysis and inject the description. Unsupported binaries (executables, archives, etc.) get rejected with a helpful error.

SVG files needed special handling since they’re XML but should be analyzed as images:

func isImage(mediaType, path string) bool {
	if slices.Contains(imageFileTypes, mediaType) {
		return true
	}

	if mediaType == "text/xml" && filepath.Ext(path) == ".svg" {
		return true
	}

	return false
}

The MIME detector sees SVG as text/xml, so the function falls back to checking the file extension.

What’s Next#

TUI polish and possibly chat persistence.

Check the release notes for the full changelog.