mirror of
https://github.com/danielmiessler/Fabric.git
synced 2026-02-13 07:25:10 -05:00
### CHANGES - Simplify error wrapping by removing redundant Sprintf calls in CLI - Pass HTTP client to FetchModelsDirectly to enable connection reuse - Store persistent HTTP client instance inside the OpenAI provider struct - Update compatible AI providers to match the new function signature - Add error handling for pattern loading from absolute file paths
276 lines
7.8 KiB
Go
276 lines
7.8 KiB
Go
package fsdb
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/danielmiessler/fabric/internal/i18n"
|
|
"github.com/danielmiessler/fabric/internal/plugins/template"
|
|
"github.com/danielmiessler/fabric/internal/util"
|
|
)
|
|
|
|
type PatternsEntity struct {
|
|
*StorageEntity
|
|
SystemPatternFile string
|
|
UniquePatternsFilePath string
|
|
CustomPatternsDir string
|
|
}
|
|
|
|
// Pattern represents a single pattern with its metadata
|
|
type Pattern struct {
|
|
Name string
|
|
Description string
|
|
Pattern string
|
|
}
|
|
|
|
// GetApplyVariables main entry point for getting patterns from any source
|
|
func (o *PatternsEntity) GetApplyVariables(
|
|
source string, variables map[string]string, input string) (pattern *Pattern, err error) {
|
|
|
|
if pattern, err = o.loadPattern(source); err != nil {
|
|
return
|
|
}
|
|
|
|
err = o.applyVariables(pattern, variables, input)
|
|
return
|
|
}
|
|
|
|
// GetWithoutVariables returns a pattern with only the {{input}} placeholder processed
|
|
// and skips template variable replacement
|
|
func (o *PatternsEntity) GetWithoutVariables(source, input string) (pattern *Pattern, err error) {
|
|
|
|
if pattern, err = o.loadPattern(source); err != nil {
|
|
return
|
|
}
|
|
|
|
o.applyInput(pattern, input)
|
|
return
|
|
}
|
|
|
|
func (o *PatternsEntity) loadPattern(source string) (pattern *Pattern, err error) {
|
|
// Determine if this is a file path
|
|
isFilePath := strings.HasPrefix(source, "\\") ||
|
|
strings.HasPrefix(source, "/") ||
|
|
strings.HasPrefix(source, "~") ||
|
|
strings.HasPrefix(source, ".")
|
|
|
|
if isFilePath {
|
|
// Resolve the file path using GetAbsolutePath
|
|
var absPath string
|
|
if absPath, err = util.GetAbsolutePath(source); err != nil {
|
|
return nil, fmt.Errorf("could not resolve file path: %v", err)
|
|
}
|
|
|
|
// Use the resolved absolute path to get the pattern
|
|
if pattern, err = o.getFromFile(absPath); err != nil {
|
|
return nil, fmt.Errorf("could not load pattern from file %s: %w", absPath, err)
|
|
}
|
|
} else {
|
|
// Otherwise, get the pattern from the database
|
|
pattern, err = o.getFromDB(source)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func (o *PatternsEntity) ensureInput(pattern *Pattern) {
|
|
if !strings.Contains(pattern.Pattern, "{{input}}") {
|
|
if !strings.HasSuffix(pattern.Pattern, "\n") {
|
|
pattern.Pattern += "\n"
|
|
}
|
|
pattern.Pattern += "{{input}}"
|
|
}
|
|
}
|
|
|
|
func (o *PatternsEntity) applyInput(pattern *Pattern, input string) {
|
|
o.ensureInput(pattern)
|
|
pattern.Pattern = strings.ReplaceAll(pattern.Pattern, "{{input}}", input)
|
|
}
|
|
|
|
func (o *PatternsEntity) applyVariables(
|
|
pattern *Pattern, variables map[string]string, input string) (err error) {
|
|
|
|
o.ensureInput(pattern)
|
|
|
|
// Temporarily replace {{input}} with a sentinel token to protect it
|
|
// from recursive variable resolution
|
|
withSentinel := strings.ReplaceAll(pattern.Pattern, "{{input}}", template.InputSentinel)
|
|
|
|
// Process all other template variables in the pattern
|
|
// Pass the actual input so extension calls can use {{input}} within their value parameter
|
|
var processed string
|
|
if processed, err = template.ApplyTemplate(withSentinel, variables, input); err != nil {
|
|
return
|
|
}
|
|
|
|
// Finally, replace our sentinel with the actual user input
|
|
// The input has already been processed for variables if InputHasVars was true
|
|
pattern.Pattern = strings.ReplaceAll(processed, template.InputSentinel, input)
|
|
return
|
|
}
|
|
|
|
// retrieves a pattern from the database by name
|
|
func (o *PatternsEntity) getFromDB(name string) (ret *Pattern, err error) {
|
|
// First check custom patterns directory if it exists
|
|
if o.CustomPatternsDir != "" {
|
|
customPatternPath := filepath.Join(o.CustomPatternsDir, name, o.SystemPatternFile)
|
|
if pattern, customErr := os.ReadFile(customPatternPath); customErr == nil {
|
|
ret = &Pattern{
|
|
Name: name,
|
|
Pattern: string(pattern),
|
|
}
|
|
return ret, nil
|
|
}
|
|
}
|
|
|
|
// Fallback to main patterns directory
|
|
patternPath := filepath.Join(o.Dir, name, o.SystemPatternFile)
|
|
|
|
var pattern []byte
|
|
if pattern, err = os.ReadFile(patternPath); err != nil {
|
|
// Check if the patterns directory is empty to provide helpful error message
|
|
if os.IsNotExist(err) {
|
|
var entries []os.DirEntry
|
|
entries, _ = os.ReadDir(o.Dir)
|
|
if len(entries) == 0 || (len(entries) == 1 && entries[0].Name() == "loaded") {
|
|
// Patterns directory is empty or only has 'loaded' file
|
|
return nil, fmt.Errorf(i18n.T("pattern_not_found_no_patterns"), name)
|
|
}
|
|
}
|
|
return nil, fmt.Errorf(i18n.T("pattern_not_found_list_available"), name)
|
|
}
|
|
|
|
patternStr := string(pattern)
|
|
ret = &Pattern{
|
|
Name: name,
|
|
Pattern: patternStr,
|
|
}
|
|
return
|
|
}
|
|
|
|
func (o *PatternsEntity) PrintLatestPatterns(latestNumber int) (err error) {
|
|
var contents []byte
|
|
if contents, err = os.ReadFile(o.UniquePatternsFilePath); err != nil {
|
|
err = fmt.Errorf("could not read unique patterns file. Please run --updatepatterns (%s)", err)
|
|
return
|
|
}
|
|
uniquePatterns := strings.Split(string(contents), "\n")
|
|
if latestNumber > len(uniquePatterns) {
|
|
latestNumber = len(uniquePatterns)
|
|
}
|
|
|
|
for i := len(uniquePatterns) - 1; i > len(uniquePatterns)-latestNumber-1; i-- {
|
|
fmt.Println(uniquePatterns[i])
|
|
}
|
|
return
|
|
}
|
|
|
|
// reads a pattern from a file path and returns it
|
|
func (o *PatternsEntity) getFromFile(pathStr string) (pattern *Pattern, err error) {
|
|
// Handle home directory expansion
|
|
if strings.HasPrefix(pathStr, "~/") {
|
|
var homedir string
|
|
if homedir, err = os.UserHomeDir(); err != nil {
|
|
err = fmt.Errorf("could not get home directory: %v", err)
|
|
return
|
|
}
|
|
pathStr = filepath.Join(homedir, pathStr[2:])
|
|
}
|
|
|
|
var content []byte
|
|
if content, err = os.ReadFile(pathStr); err != nil {
|
|
err = fmt.Errorf("could not read pattern file %s: %v", pathStr, err)
|
|
return
|
|
}
|
|
pattern = &Pattern{
|
|
Name: pathStr,
|
|
Pattern: string(content),
|
|
}
|
|
return
|
|
}
|
|
|
|
// GetNames overrides StorageEntity.GetNames to include custom patterns directory
|
|
func (o *PatternsEntity) GetNames() (ret []string, err error) {
|
|
// Get names from main patterns directory
|
|
mainNames, err := o.StorageEntity.GetNames()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Create a map to track unique pattern names (custom patterns override main ones)
|
|
nameMap := make(map[string]bool)
|
|
for _, name := range mainNames {
|
|
nameMap[name] = true
|
|
}
|
|
|
|
// Get names from custom patterns directory if it exists
|
|
if o.CustomPatternsDir != "" {
|
|
// Create a temporary StorageEntity for the custom directory
|
|
customStorage := &StorageEntity{
|
|
Dir: o.CustomPatternsDir,
|
|
ItemIsDir: o.StorageEntity.ItemIsDir,
|
|
FileExtension: o.StorageEntity.FileExtension,
|
|
}
|
|
|
|
customNames, customErr := customStorage.GetNames()
|
|
if customErr == nil {
|
|
// Add custom patterns, they will override main patterns with same name
|
|
for _, name := range customNames {
|
|
nameMap[name] = true
|
|
}
|
|
}
|
|
// Ignore errors from custom directory (it might not exist)
|
|
}
|
|
|
|
// Convert map keys back to slice
|
|
ret = make([]string, 0, len(nameMap))
|
|
for name := range nameMap {
|
|
ret = append(ret, name)
|
|
}
|
|
|
|
// Sort the patterns alphabetically
|
|
sort.Strings(ret)
|
|
|
|
return ret, nil
|
|
}
|
|
|
|
// ListNames overrides StorageEntity.ListNames to use PatternsEntity.GetNames
|
|
func (o *PatternsEntity) ListNames(shellCompleteList bool) (err error) {
|
|
var names []string
|
|
if names, err = o.GetNames(); err != nil {
|
|
return
|
|
}
|
|
|
|
if len(names) == 0 {
|
|
if !shellCompleteList {
|
|
fmt.Printf("\nNo %v\n", o.StorageEntity.Label)
|
|
}
|
|
return
|
|
}
|
|
|
|
for _, item := range names {
|
|
fmt.Printf("%s\n", item)
|
|
}
|
|
return
|
|
}
|
|
|
|
// Get required for Storage interface
|
|
func (o *PatternsEntity) Get(name string) (*Pattern, error) {
|
|
// Use GetPattern with no variables
|
|
return o.GetApplyVariables(name, nil, "")
|
|
}
|
|
func (o *PatternsEntity) Save(name string, content []byte) (err error) {
|
|
patternDir := filepath.Join(o.Dir, name)
|
|
if err = os.MkdirAll(patternDir, os.ModePerm); err != nil {
|
|
return fmt.Errorf("could not create pattern directory: %v", err)
|
|
}
|
|
patternPath := filepath.Join(patternDir, o.SystemPatternFile)
|
|
if err = os.WriteFile(patternPath, content, 0644); err != nil {
|
|
return fmt.Errorf("could not save pattern: %v", err)
|
|
}
|
|
return nil
|
|
}
|