Ghost v3.4.0: Unified File Handling
Ghost v3.4.0 unifies file handling under a single :r command. Text files and images are now routed automatically via MIME type detection.
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.