diff --git a/cmd/api/cmd.go b/cmd/api/cmd.go index 89db993..2ef52c9 100644 --- a/cmd/api/cmd.go +++ b/cmd/api/cmd.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + // Packages "github.com/djthorpe/go-tablewriter" "github.com/mutablelogic/go-client" ) diff --git a/cmd/api/homeassistant.go b/cmd/api/homeassistant.go index f6afab9..239a73f 100644 --- a/cmd/api/homeassistant.go +++ b/cmd/api/homeassistant.go @@ -2,14 +2,15 @@ package main import ( "context" + "maps" "slices" "strings" "time" + // Packages "github.com/djthorpe/go-tablewriter" "github.com/mutablelogic/go-client" "github.com/mutablelogic/go-client/pkg/homeassistant" - "golang.org/x/exp/maps" ) /////////////////////////////////////////////////////////////////////////////// @@ -129,7 +130,7 @@ func haDomains(_ context.Context, w *tablewriter.Writer, args []string) error { var services []string if domain, exists := map_domains[c]; exists { if v := domain.Services; v != nil { - services = maps.Keys(v) + services = slices.Collect(maps.Keys(v)) } } result = append(result, haDomain{ diff --git a/cmd/api/install.go b/cmd/api/install.go index cbea66b..bd30990 100644 --- a/cmd/api/install.go +++ b/cmd/api/install.go @@ -44,5 +44,8 @@ func install(flags *Flags) error { } func statEquals(a, b os.FileInfo) bool { - return a.Size() == b.Size() && a.ModTime() == b.ModTime() + if a == nil || b == nil { + return false + } + return a.Size() == b.Size() && a.ModTime().Equal(b.ModTime()) } diff --git a/cmd/api/newsapi.go b/cmd/api/newsapi.go index 2b0c9e8..e31de45 100644 --- a/cmd/api/newsapi.go +++ b/cmd/api/newsapi.go @@ -6,6 +6,7 @@ import ( "strconv" "strings" + // Packages "github.com/djthorpe/go-tablewriter" "github.com/mutablelogic/go-client" "github.com/mutablelogic/go-client/pkg/newsapi" diff --git a/cmd/api/weatherapi.go b/cmd/api/weatherapi.go index efcb86a..3fff7b1 100644 --- a/cmd/api/weatherapi.go +++ b/cmd/api/weatherapi.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + // Packages "github.com/djthorpe/go-tablewriter" "github.com/mutablelogic/go-client" "github.com/mutablelogic/go-client/pkg/weatherapi" diff --git a/etc/_cli/command.go b/etc/_cli/command.go deleted file mode 100644 index 51db7cc..0000000 --- a/etc/_cli/command.go +++ /dev/null @@ -1,100 +0,0 @@ -package main - -import ( - "flag" - "fmt" -) - -/////////////////////////////////////////////////////////////////////////////// -// TYPES - -type Client struct { - ns string - description string - cmd []Command -} - -type Command struct { - Name string - Description string - Syntax string - MinArgs int - MaxArgs int - Fn CommandFn -} - -type CommandFn func() error - -/////////////////////////////////////////////////////////////////////////////// -// RUN COMMAND - -func Run(clients []Client, flags *Flags) error { - var ns = map[string][]Command{} - for _, client := range clients { - ns[client.ns] = client.cmd - } - - if flags.NArg() == 0 { - return flag.ErrHelp - } - cmd, exists := ns[flags.Arg(0)] - if !exists { - return flag.ErrHelp - } - - var fn CommandFn - for _, cmd := range cmd { - // Match on minimum number of arguments - if flags.NArg() < cmd.MinArgs { - continue - } - // Match on maximum number of arguments - if cmd.MaxArgs >= cmd.MinArgs && flags.NArg() > cmd.MaxArgs { - continue - } - // Match on name - if cmd.Name != "" && cmd.Name != flags.Arg(1) { - continue - } - - // Here we have matched, so break loop - fn = cmd.Fn - break - } - - // Run function - if fn == nil { - return fmt.Errorf("no command matched for: %q", flags.Args()) - } else { - return fn() - } -} - -func PrintCommands(flags *Flags, clients []Client) { - for i, client := range clients { - if len(client.cmd) == 0 { - continue - } - if i > 0 { - fmt.Fprintln(flags.Output()) - } - fmt.Fprintf(flags.Output(), " %s:\n", client.ns) - if client.description != "" { - fmt.Fprintf(flags.Output(), " %s\n", client.description) - } - for _, cmd := range client.cmd { - if cmd.MinArgs == 0 && cmd.MaxArgs == 0 { - fmt.Fprintf(flags.Output(), " %s %s", flags.Name(), client.ns) - } else { - fmt.Fprintf(flags.Output(), " %s %s %s", flags.Name(), client.ns, cmd.Name) - } - if cmd.Syntax != "" { - fmt.Fprintf(flags.Output(), " %s", cmd.Syntax) - } - if cmd.Description != "" { - fmt.Fprintf(flags.Output(), "\n %s", cmd.Description) - } - fmt.Fprintln(flags.Output()) - } - } -} diff --git a/etc/_cli/elevenlabs.go b/etc/_cli/elevenlabs.go deleted file mode 100644 index b5edb56..0000000 --- a/etc/_cli/elevenlabs.go +++ /dev/null @@ -1,125 +0,0 @@ -package main - -import ( - "fmt" - "os" - "regexp" - "strings" - - // Packages - "github.com/mutablelogic/go-client" - "github.com/mutablelogic/go-client/pkg/elevenlabs" -) - -///////////////////////////////////////////////////////////////////// -// TYPES - -type result struct { - Path string `json:"path"` - Bytes int64 `json:"bytes_written"` - Mime string `json:"mime_type,omitempty"` -} - -///////////////////////////////////////////////////////////////////// -// GLOBALS - -var ( - reVoiceId = regexp.MustCompile("^[a-zA-Z0-9]{20}$") -) - -///////////////////////////////////////////////////////////////////// -// REGISTER FUNCTIONS - -func ElevenlabsFlags(flags *Flags) { - flags.String("elevenlabs-api-key", "${ELEVENLABS_API_KEY}", "ElevenLabs API key") -} - -func ElevenlabsRegister(cmd []Client, opts []client.ClientOpt, flags *Flags) ([]Client, error) { - elevenlabs, err := elevenlabs.New(flags.GetString("elevenlabs-api-key"), opts...) - if err != nil { - return nil, err - } - - // Register commands - cmd = append(cmd, Client{ - ns: "elevenlabs", - cmd: []Command{ - {Name: "voices", Description: "Return registered voices", MinArgs: 2, MaxArgs: 2, Fn: elVoices(elevenlabs, flags)}, - {Name: "voice", Description: "Return voice information", Syntax: "", MinArgs: 3, MaxArgs: 3, Fn: elVoice(elevenlabs, flags)}, - {Name: "speak", Description: "Create speech from a prompt", Syntax: " ", MinArgs: 4, MaxArgs: 4, Fn: elSpeak(elevenlabs, flags)}, - }, - }) - - // Return success - return cmd, nil -} - -///////////////////////////////////////////////////////////////////// -// API CALL FUNCTIONS - -func elVoices(client *elevenlabs.Client, flags *Flags) CommandFn { - return func() error { - if voices, err := client.Voices(); err != nil { - return err - } else { - return flags.Write(voices) - } - } -} - -func elVoice(client *elevenlabs.Client, flags *Flags) CommandFn { - return func() error { - if voice, err := client.Voice(flags.Arg(2)); err != nil { - return err - } else { - return flags.Write(voice) - } - } -} - -func elSpeak(client *elevenlabs.Client, flags *Flags) CommandFn { - return func() error { - voice, err := elGetVoiceId(client, flags.Arg(2)) - if err != nil { - return err - } - - // Set options - opts := []elevenlabs.Opt{} - - // Create the audio - out := flags.GetOutFilename("speech.mp3", 0) - file, err := os.Create(out) - if err != nil { - return err - } - defer file.Close() - if n, err := client.TextToSpeech(file, voice, flags.Arg(3), opts...); err != nil { - return err - } else if err := flags.Write(result{Path: out, Bytes: n}); err != nil { - return err - } - - // Return success - return nil - } -} - -///////////////////////////////////////////////////////////////////// -// PRIVATE METHODS - -// return a voice-id given a parameter, which can be a voice-id or name -func elGetVoiceId(client *elevenlabs.Client, voice string) (string, error) { - if reVoiceId.MatchString(voice) { - return voice, nil - } else if voices, err := client.Voices(); err != nil { - return "", err - } else { - for _, v := range voices { - if strings.EqualFold(v.Name, voice) || v.Id == voice { - return v.Id, nil - } - } - } - return "", fmt.Errorf("voice not found: %q", voice) -} diff --git a/etc/_cli/flags.go b/etc/_cli/flags.go deleted file mode 100644 index 08d3853..0000000 --- a/etc/_cli/flags.go +++ /dev/null @@ -1,175 +0,0 @@ -package main - -import ( - "flag" - "fmt" - "os" - "path/filepath" - "strconv" - "strings" - "time" - - // Packages - tablewriter "github.com/djthorpe/go-tablewriter" - - // Namespace imports - . "github.com/djthorpe/go-errors" -) - -/////////////////////////////////////////////////////////////////////////////// -// TYPES - -type Flags struct { - *flag.FlagSet - writer *tablewriter.TableWriter -} - -type FlagsRegister func(*Flags) - -/////////////////////////////////////////////////////////////////////////////// -// LIFECYCLE - -func NewFlags(name string, args []string, register ...FlagsRegister) (*Flags, error) { - flags := new(Flags) - flags.FlagSet = flag.NewFlagSet(name, flag.ContinueOnError) - - // Register flags - flags.Bool("debug", false, "Enable debug logging") - flags.Duration("timeout", 0, "Timeout") - flags.String("out", "", "Output format or file name") - flags.String("cols", "", "Comma-separated list of columns to output") - for _, fn := range register { - fn(flags) - } - - // Parse command line - if err := flags.Parse(args); err != nil { - return nil, err - } - - // Create a writer - flags.writer = tablewriter.New(os.Stdout, tablewriter.OptOutputText()) - - // Return success - return flags, nil -} - -/////////////////////////////////////////////////////////////////////////////// -// PUBLIC METHODS - -func (flags *Flags) IsDebug() bool { - return flags.Lookup("debug").Value.(flag.Getter).Get().(bool) -} - -func (flags *Flags) Timeout() time.Duration { - return flags.Lookup("timeout").Value.(flag.Getter).Get().(time.Duration) -} - -func (flags *Flags) GetOut() string { - return flags.GetString("out") -} - -func (flags *Flags) GetOutExt() string { - out := flags.GetOut() - if out == "" { - return "" - } - if ext := filepath.Ext(out); ext == "" { - return out - } else { - return strings.TrimPrefix(ext, ".") - } -} - -// Return a filename for output, returns an empty string if the output -// argument is not a filename (it requires an extension) -func (flags *Flags) GetOutFilename(def string, n int) string { - filename := flags.GetOut() - if filename == "" { - filename = filepath.Base(def) - } - if filename == "" { - return "" - } - ext := filepath.Ext(filename) - if ext == "" { - return "" - } - if n > 0 { - filename = filename[:len(filename)-len(ext)] + "-" + fmt.Sprint(n) + ext - } else { - filename = filename[:len(filename)-len(ext)] + ext - } - return filepath.Clean(filename) -} - -func (flags *Flags) GetString(key string) string { - if flag := flags.Lookup(key); flag == nil { - return "" - } else { - return os.ExpandEnv(flag.Value.String()) - } -} - -func (flags *Flags) GetUint(key string) (uint, error) { - if flag := flags.Lookup(key); flag == nil { - return 0, ErrNotFound.With(key) - } else if v, err := strconv.ParseUint(os.ExpandEnv(flag.Value.String()), 10, 64); err != nil { - return 0, ErrBadParameter.With(key) - } else { - return uint(v), nil - } -} - -func (flags *Flags) GetInt(key string) (int, error) { - if flag := flags.Lookup(key); flag == nil { - return 0, ErrNotFound.With(key) - } else if v, err := strconv.ParseInt(os.ExpandEnv(flag.Value.String()), 10, 64); err != nil { - return 0, ErrBadParameter.With(key) - } else { - return int(v), nil - } -} - -func (flags *Flags) GetBool(key string) bool { - if flag := flags.Lookup(key); flag == nil { - return false - } else if v, err := strconv.ParseBool(os.ExpandEnv(flag.Value.String())); err != nil { - return false - } else { - return v - } -} - -func (flags *Flags) GetFloat64(key string) *float64 { - if flag := flags.Lookup(key); flag == nil { - return nil - } else if v, err := strconv.ParseFloat(os.ExpandEnv(flag.Value.String()), 64); err != nil { - return nil - } else { - return &v - } -} - -func (flags *Flags) Write(v any) error { - opts := []tablewriter.TableOpt{} - - // Set header - opts = append(opts, tablewriter.OptHeader()) - - // Set terminal options - //opts = append(opts, TerminalOpts(flags.Output())...) - - // Set output options - /* - switch flags.GetOut() { - case "text", "txt", "ascii": - opts = append(opts, writer.OptText('|', true, 0)) - case "csv": - opts = append(opts, writer.OptCSV(',', true)) - case "tsv": - opts = append(opts, writer.OptCSV('\t', true)) - } - */ - return flags.writer.Write(v, opts...) -} diff --git a/etc/_cli/homeassistant.go b/etc/_cli/homeassistant.go deleted file mode 100644 index 086530c..0000000 --- a/etc/_cli/homeassistant.go +++ /dev/null @@ -1,72 +0,0 @@ -package main - -import ( - // Packages - "github.com/mutablelogic/go-client" - "github.com/mutablelogic/go-client/pkg/homeassistant" -) - -///////////////////////////////////////////////////////////////////// -// REGISTER FUNCTIONS - -func HomeAssistantFlags(flags *Flags) { - flags.String("ha-token", "${HA_TOKEN}", "Home Assistant API key") - flags.String("ha-endpoint", "${HA_ENDPOINT}", "Home Assistant Endpoint") -} - -func HomeAssistantRegister(cmd []Client, opts []client.ClientOpt, flags *Flags) ([]Client, error) { - // Create home assistant client - ha, err := homeassistant.New(flags.GetString("ha-endpoint"), flags.GetString("ha-token"), opts...) - if err != nil { - return nil, err - } - - // Register commands for router - cmd = append(cmd, Client{ - ns: "ha", - cmd: []Command{ - {Name: "status", Description: "Return status string", MinArgs: 2, MaxArgs: 2, Fn: haStatus(ha, flags)}, - {Name: "events", Description: "Enumerate event objects. Each event object contains event name and listener count.", MinArgs: 2, MaxArgs: 2, Fn: haEvents(ha, flags)}, - {Name: "states", Description: "Enumerate entity states", MinArgs: 2, MaxArgs: 2, Fn: haStates(ha, flags)}, - }, - }) - - // Return success - return cmd, nil -} - -///////////////////////////////////////////////////////////////////// -// API CALLS - -func haStatus(client *homeassistant.Client, flags *Flags) CommandFn { - return func() error { - if message, err := client.Health(); err != nil { - return err - } else if err := flags.Write(struct{ Status string }{message}); err != nil { - return err - } - return nil - } -} - -func haEvents(client *homeassistant.Client, flags *Flags) CommandFn { - return func() error { - if events, err := client.Events(); err != nil { - return err - } else if err := flags.Write(events); err != nil { - return err - } - return nil - } -} - -func haStates(client *homeassistant.Client, flags *Flags) CommandFn { - return func() error { - if states, err := client.States(); err != nil { - return err - } else if err := flags.Write(states); err != nil { - return err - } - return nil - } -} diff --git a/etc/_cli/ipify.go b/etc/_cli/ipify.go deleted file mode 100644 index 7655c30..0000000 --- a/etc/_cli/ipify.go +++ /dev/null @@ -1,42 +0,0 @@ -package main - -import ( - // Packages - "github.com/mutablelogic/go-client" - "github.com/mutablelogic/go-client/pkg/ipify" -) - -///////////////////////////////////////////////////////////////////// -// REGISTER FUNCTIONS - -func IpifyRegister(cmd []Client, opts []client.ClientOpt, flags *Flags) ([]Client, error) { - // Create ipify client - ipify, err := ipify.New(opts...) - if err != nil { - return nil, err - } - - // Register commands for router - cmd = append(cmd, Client{ - ns: "ipify", - cmd: []Command{ - {MinArgs: 1, MaxArgs: 1, Description: "Get external IP address", Fn: IpifyGet(ipify, flags)}, - }, - }) - - // Return success - return cmd, nil -} - -///////////////////////////////////////////////////////////////////// -// API CALLS - -func IpifyGet(ipify *ipify.Client, flags *Flags) CommandFn { - return func() error { - addr, err := ipify.Get() - if err != nil { - return err - } - return flags.Write(addr) - } -} diff --git a/etc/_cli/main.go b/etc/_cli/main.go deleted file mode 100644 index 0471b2e..0000000 --- a/etc/_cli/main.go +++ /dev/null @@ -1,89 +0,0 @@ -package main - -import ( - "flag" - "fmt" - "os" - "path" - - // Packages - "github.com/mutablelogic/go-client" - "github.com/pkg/errors" -) - -func main() { - name := path.Base(os.Args[0]) - flags, err := NewFlags(name, os.Args[1:], OpenAIFlags, MistralFlags, ElevenlabsFlags, HomeAssistantFlags, NewsAPIFlags, OllamaFlags) - if err != nil { - if err != flag.ErrHelp { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } else { - os.Exit(0) - } - } - - // Add client options - opts := []client.ClientOpt{} - if flags.IsDebug() { - opts = append(opts, client.OptTrace(os.Stderr, true)) - } - if timeout := flags.Timeout(); timeout > 0 { - opts = append(opts, client.OptTimeout(timeout)) - } - - // Register commands - var cmd []Client - - cmd, err = IpifyRegister(cmd, opts, flags) - if err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } - - cmd, err = ElevenlabsRegister(cmd, opts, flags) - if err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } - - cmd, err = OpenAIRegister(cmd, opts, flags) - if err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } - - cmd, err = MistralRegister(cmd, opts, flags) - if err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } - - cmd, err = HomeAssistantRegister(cmd, opts, flags) - if err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } - - cmd, err = NewsAPIRegister(cmd, opts, flags) - if err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } - - cmd, err = OllamaRegister(cmd, opts, flags) - if err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } - - // Run command - if err := Run(cmd, flags); err != nil { - if errors.Is(err, flag.ErrHelp) { - PrintCommands(flags, cmd) - } else { - fmt.Fprintln(os.Stderr, err) - } - os.Exit(1) - } -} diff --git a/etc/_cli/mistral.go b/etc/_cli/mistral.go deleted file mode 100644 index 6a32028..0000000 --- a/etc/_cli/mistral.go +++ /dev/null @@ -1,60 +0,0 @@ -package main - -import ( - // Package imports - "github.com/mutablelogic/go-client" - "github.com/mutablelogic/go-client/pkg/mistral" - "github.com/mutablelogic/go-client/pkg/openai/schema" -) - -///////////////////////////////////////////////////////////////////// -// REGISTER FUNCTIONS - -func MistralFlags(flags *Flags) { - flags.String("mistral-api-key", "${MISTRAL_API_KEY}", "Mistral API key") -} - -func MistralRegister(cmd []Client, opts []client.ClientOpt, flags *Flags) ([]Client, error) { - mistral, err := mistral.New(flags.GetString("mistral-api-key"), opts...) - if err != nil { - return nil, err - } - - // Register commands - cmd = append(cmd, Client{ - ns: "mistral", - cmd: []Command{ - {Name: "models", Description: "Return registered models", MinArgs: 2, MaxArgs: 2, Fn: mistralModels(mistral, flags)}, - {Name: "chat", Description: "Chat", Syntax: "", MinArgs: 3, MaxArgs: 3, Fn: mistralChat(mistral, flags)}, - }, - }) - - // Return success - return cmd, nil -} - -///////////////////////////////////////////////////////////////////// -// API CALL FUNCTIONS - -func mistralModels(client *mistral.Client, flags *Flags) CommandFn { - return func() error { - if models, err := client.ListModels(); err != nil { - return err - } else { - return flags.Write(models) - } - } -} - -func mistralChat(client *mistral.Client, flags *Flags) CommandFn { - return func() error { - if message, err := client.Chat([]schema.Message{ - {Role: "user", Content: flags.Arg(2)}, - }); err != nil { - return err - } else if err := flags.Write(message); err != nil { - return err - } - return nil - } -} diff --git a/etc/_cli/newsapi.go b/etc/_cli/newsapi.go deleted file mode 100644 index c847d00..0000000 --- a/etc/_cli/newsapi.go +++ /dev/null @@ -1,76 +0,0 @@ -package main - -import ( - // Package imports - "github.com/mutablelogic/go-client" - "github.com/mutablelogic/go-client/pkg/newsapi" -) - -///////////////////////////////////////////////////////////////////// -// REGISTER FUNCTIONS - -func NewsAPIFlags(flags *Flags) { - flags.String("news-api-key", "${NEWSAPI_KEY}", "NewsAPI key") - flags.String("q", "", "Search query") -} - -func NewsAPIRegister(cmd []Client, opts []client.ClientOpt, flags *Flags) ([]Client, error) { - newsapi, err := newsapi.New(flags.GetString("news-api-key"), opts...) - if err != nil { - return nil, err - } - - // Register commands - cmd = append(cmd, Client{ - ns: "newsapi", - cmd: []Command{ - {Name: "sources", Description: "Return news sources", MinArgs: 2, MaxArgs: 2, Fn: newsAPISources(newsapi, flags)}, - {Name: "headlines", Description: "Return news headlines", MinArgs: 2, MaxArgs: 2, Fn: newsAPIHeadlines(newsapi, flags)}, - {Name: "articles", Description: "Return news articles", MinArgs: 2, MaxArgs: 2, Fn: newsAPIArticles(newsapi, flags)}, - }, - }) - - // Return success - return cmd, nil -} - -///////////////////////////////////////////////////////////////////// -// API CALL FUNCTIONS - -func newsAPISources(client *newsapi.Client, flags *Flags) CommandFn { - return func() error { - if sources, err := client.Sources(); err != nil { - return err - } else { - return flags.Write(sources) - } - } -} - -func newsAPIHeadlines(client *newsapi.Client, flags *Flags) CommandFn { - return func() error { - opts := []newsapi.Opt{} - if q := flags.GetString("q"); q != "" { - opts = append(opts, newsapi.OptQuery(q)) - } - if articles, err := client.Headlines(opts...); err != nil { - return err - } else { - return flags.Write(articles) - } - } -} - -func newsAPIArticles(client *newsapi.Client, flags *Flags) CommandFn { - return func() error { - opts := []newsapi.Opt{} - if q := flags.GetString("q"); q != "" { - opts = append(opts, newsapi.OptQuery(q)) - } - if articles, err := client.Articles(opts...); err != nil { - return err - } else { - return flags.Write(articles) - } - } -} diff --git a/etc/_cli/ollama.go b/etc/_cli/ollama.go deleted file mode 100644 index bb1f51e..0000000 --- a/etc/_cli/ollama.go +++ /dev/null @@ -1,45 +0,0 @@ -package main - -import ( - // Package imports - "github.com/mutablelogic/go-client" - "github.com/mutablelogic/go-client/pkg/ollama" -) - -///////////////////////////////////////////////////////////////////// -// REGISTER FUNCTIONS - -func OllamaFlags(flags *Flags) { - flags.String("ollama-endpoint", "${OLLAMA_ENDPOINT}", "Ollama endpoint url") -} - -func OllamaRegister(cmd []Client, opts []client.ClientOpt, flags *Flags) ([]Client, error) { - ollama, err := ollama.New(flags.GetString("ollama-endpoint"), opts...) - if err != nil { - return nil, err - } - - // Register commands - cmd = append(cmd, Client{ - ns: "ollama", - cmd: []Command{ - {Name: "models", Description: "List local models", MinArgs: 2, MaxArgs: 2, Fn: ollamaListModels(ollama, flags)}, - }, - }) - - // Return success - return cmd, nil -} - -///////////////////////////////////////////////////////////////////// -// API CALL FUNCTIONS - -func ollamaListModels(client *ollama.Client, flags *Flags) CommandFn { - return func() error { - if models, err := client.ListModels(); err != nil { - return err - } else { - return flags.Write(models) - } - } -} diff --git a/etc/_cli/open.go b/etc/_cli/open.go deleted file mode 100644 index cfd8f71..0000000 --- a/etc/_cli/open.go +++ /dev/null @@ -1,24 +0,0 @@ -package main - -import ( - "os/exec" - "runtime" -) - -// open opens the specified files with the operating system -func open(url ...string) error { - var cmd string - var args []string - - switch runtime.GOOS { - case "windows": - cmd = "cmd" - args = []string{"/c", "start"} - case "darwin": - cmd = "open" - default: // "linux", "freebsd", "openbsd", "netbsd" - cmd = "xdg-open" - } - args = append(args, url...) - return exec.Command(cmd, args...).Start() -} diff --git a/etc/_cli/openai.go b/etc/_cli/openai.go deleted file mode 100644 index af0c01a..0000000 --- a/etc/_cli/openai.go +++ /dev/null @@ -1,326 +0,0 @@ -package main - -import ( - "errors" - "net/url" - "os" - "path/filepath" - "regexp" - "strconv" - - "github.com/mutablelogic/go-client" - "github.com/mutablelogic/go-client/pkg/openai" -) - -///////////////////////////////////////////////////////////////////// -// TYPES - -type openaiImageResponse struct { - Url string `json:"-"` - Path string `json:"path"` - Bytes uint `json:"bytes_written"` -} - -///////////////////////////////////////////////////////////////////// -// GLOBALS - -var ( - reOpenAISize = regexp.MustCompile(`^(\d+)x(\d+)$`) - defaultVoice = "alloy" -) - -///////////////////////////////////////////////////////////////////// -// REGISTER FUNCTIONS - -func OpenAIFlags(flags *Flags) { - flags.String("openai-api-key", "${OPENAI_API_KEY}", "OpenAI API key") - flags.String("model", "", "Model to use for generation") - flags.Uint("count", 0, "Number of results to return") - flags.Bool("natural", false, "Create more natural images") - flags.Bool("hd", false, "Create images with finer details and greater consistency across the image") - flags.String("size", "", "Size of output image (256x256, 512x512, 1024x1024, 1792x1024 or 1024x1792)") - flags.Bool("open", false, "Open images in default viewer") - flags.String("language", "", "Audio language") - flags.String("prompt", "", "Text to guide the transcription style or continue a previous audio segment") - flags.Float64("temperature", 0, "Sampling temperature for generation") -} - -func OpenAIRegister(cmd []Client, opts []client.ClientOpt, flags *Flags) ([]Client, error) { - // Create client - openai, err := openai.New(flags.GetString("openai-api-key"), opts...) - if err != nil { - return nil, err - } - - // Register commands - cmd = append(cmd, Client{ - ns: "openai", - cmd: []Command{ - {Name: "models", Description: "Return registered models", MinArgs: 2, MaxArgs: 2, Fn: openaiModels(openai, flags)}, - {Name: "model", Description: "Return model information", Syntax: "", MinArgs: 3, MaxArgs: 3, Fn: openaiModel(openai, flags)}, - {Name: "image", Description: "Create images from a prompt", Syntax: "", MinArgs: 3, MaxArgs: 3, Fn: openaiImages(openai, flags)}, - {Name: "speak", Description: "Create speech from a prompt", Syntax: "() ", MinArgs: 3, MaxArgs: 4, Fn: openaiSpeak(openai, flags)}, - {Name: "transcribe", Description: "Transcribe audio to text", Syntax: "", MinArgs: 3, MaxArgs: 3, Fn: openaiTranscribe(openai, flags)}, - {Name: "translate", Description: "Translate audio to English", Syntax: "", MinArgs: 3, MaxArgs: 3, Fn: openaiTranslate(openai, flags)}, - {Name: "caption", Description: "Provide a caption for an image", Syntax: "", MinArgs: 3, MaxArgs: 3, Fn: openaiCaption(openai, flags)}, - }, - }) - - // Return success - return cmd, nil -} - -///////////////////////////////////////////////////////////////////// -// API CALLS - -func openaiModels(client *openai.Client, flags *Flags) CommandFn { - return func() error { - if models, err := client.ListModels(); err != nil { - return err - } else if err := flags.Write(models); err != nil { - return err - } - return nil - } -} - -func openaiModel(client *openai.Client, flags *Flags) CommandFn { - return func() error { - if model, err := client.GetModel(flags.Arg(2)); err != nil { - return err - } else if err := flags.Write(model); err != nil { - return err - } - return nil - } -} - -func openaiTranscribe(client *openai.Client, flags *Flags) CommandFn { - return func() error { - // Set options - opts := []openai.Opt{} - if model := flags.GetString("model"); model != "" { - opts = append(opts, openai.OptModel(model)) - } - if prompt := flags.GetString("prompt"); prompt != "" { - opts = append(opts, openai.OptPrompt(prompt)) - } - if language := flags.GetString("language"); language != "" { - opts = append(opts, openai.OptLanguage(language)) - } - if temp := flags.GetFloat64("temperature"); temp != nil && *temp > 0 { - opts = append(opts, openai.OptTemperature(*temp)) - } - if format := flags.GetOutExt(); format != "" { - opts = append(opts, openai.OptResponseFormat(format)) - } - - // Open audio file for reading - r, err := os.Open(flags.Arg(2)) - if err != nil { - return err - } - defer r.Close() - - // Perform transcription - if transcription, err := client.Transcribe(r, opts...); err != nil { - return err - } else if err := flags.Write(transcription); err != nil { - return err - } - - // Return success - return nil - } -} - -func openaiTranslate(client *openai.Client, flags *Flags) CommandFn { - return func() error { - // Set options - opts := []openai.Opt{} - if model := flags.GetString("model"); model != "" { - opts = append(opts, openai.OptModel(model)) - } - if prompt := flags.GetString("prompt"); prompt != "" { - opts = append(opts, openai.OptPrompt(prompt)) - } - if temp := flags.GetFloat64("temperature"); temp != nil && *temp > 0 { - opts = append(opts, openai.OptTemperature(*temp)) - } - if format := flags.GetOutExt(); format != "" { - opts = append(opts, openai.OptResponseFormat(format)) - } - - // Open audio file for reading - r, err := os.Open(flags.Arg(2)) - if err != nil { - return err - } - defer r.Close() - - // Perform transcription - if transcription, err := client.Transcribe(r, opts...); err != nil { - return err - } else if err := flags.Write(transcription); err != nil { - return err - } - - // Return success - return nil - } -} - -func openaiSpeak(client *openai.Client, flags *Flags) CommandFn { - return func() error { - // Set options - opts := []openai.Opt{} - if model := flags.GetString("model"); model != "" { - opts = append(opts, openai.OptModel(model)) - } - if format := flags.GetOutExt(); format != "" { - opts = append(opts, openai.OptResponseFormat(format)) - } - var voice, prompt string - if flags.NArg() == 4 { - voice = flags.Arg(2) - prompt = flags.Arg(3) - } else { - voice = defaultVoice - prompt = flags.Arg(2) - } - - // Determine the filename // TODO - w, err := os.Create("output.mp3") - if err != nil { - return err - } - defer w.Close() - - // Create the audio - if _, err := client.Speech(w, voice, prompt, opts...); err != nil { - return err - } - - // Open the audio - if flags.GetBool("open") { - if err := open("output.mp3"); err != nil { - return err - } - } - - // Return any errors - return nil - } -} - -func openaiImages(client *openai.Client, flags *Flags) CommandFn { - return func() error { - // Set options - opts := []openai.Opt{} - if model := flags.GetString("model"); model != "" { - opts = append(opts, openai.OptModel(model)) - } - if count, err := flags.GetInt("count"); err != nil { - return err - } else if count > 0 { - opts = append(opts, openai.OptCount(count)) - } - if flags.GetBool("hd") { - opts = append(opts, openai.OptQuality("hd"), openai.OptModel("dall-e-3")) - } - if flags.GetBool("natural") { - opts = append(opts, openai.OptStyle("natural")) - } - if size := flags.GetString("size"); size != "" { - if width, height, err := openaiSize(size); err != nil { - return err - } else { - opts = append(opts, openai.OptSize(width, height)) - } - } - if format := flags.GetOutExt(); format != "" { - opts = append(opts, openai.OptResponseFormat(format)) - } - - // Create images - response, err := client.CreateImages(flags.Arg(2), opts...) - if err != nil { - return err - } - - // Write out images - var result error - var written []openaiImageResponse - for _, image := range response { - if url, err := url.Parse(image.Url); err != nil { - result = errors.Join(result, err) - } else if w, err := os.Create(filepath.Base(url.Path)); err != nil { - result = errors.Join(result, err) - } else { - defer w.Close() - if n, err := client.WriteImage(w, image); err != nil { - result = errors.Join(result, err) - } else { - written = append(written, openaiImageResponse{Url: image.Url, Bytes: uint(n), Path: w.Name()}) - } - } - } - - // Open images - if flags.GetBool("open") { - var paths []string - for _, image := range written { - paths = append(paths, image.Path) - } - if err := open(paths...); err != nil { - result = errors.Join(result, err) - } - } else if err := flags.Write(written); err != nil { - result = errors.Join(result, err) - } - - // Return any errors - return result - } -} - -func openaiCaption(client *openai.Client, flags *Flags) CommandFn { - return func() error { - url, err := url.Parse(flags.Arg(2)) - if err != nil { - return err - } - message := openai.NewMessage("user", "Provide a short caption for this image") - if url.Scheme == "" || url.Scheme == "file" { - // TODO: Image needs to be uploaded first - message.AppendImageFile(url.Path) - } else { - message.AppendImageUrl(url.String()) - } - if response, err := client.Chat([]*openai.Message{message}, openai.OptModel("gpt-4-vision-preview")); err != nil { - return err - } else if len(response.Choices) == 0 { - return errors.New("no response from OpenAI") - } else if err := flags.Write(response.Choices[0].Message); err != nil { - return err - } - - // Return success - return nil - } -} - -///////////////////////////////////////////////////////////////////// -// PRIVATE METHODS - -func openaiSize(size string) (uint, uint, error) { - if n := reOpenAISize.FindStringSubmatch(size); n == nil || len(n) != 3 { - return 0, 0, errors.New("invalid size, should be x") - } else if w, err := strconv.ParseUint(n[1], 10, 64); err != nil { - return 0, 0, err - } else if h, err := strconv.ParseUint(n[2], 10, 64); err != nil { - return 0, 0, err - } else { - return uint(w), uint(h), nil - } -} diff --git a/etc/test/IMG_20130413_095348.JPG b/etc/test/IMG_20130413_095348.JPG deleted file mode 100644 index 7e16517..0000000 Binary files a/etc/test/IMG_20130413_095348.JPG and /dev/null differ diff --git a/etc/test/Jean_de_La_Fontaine.mp3 b/etc/test/Jean_de_La_Fontaine.mp3 deleted file mode 100644 index d5ff2bb..0000000 Binary files a/etc/test/Jean_de_La_Fontaine.mp3 and /dev/null differ diff --git a/etc/test/david.mp3 b/etc/test/david.mp3 deleted file mode 100644 index 4f8fef5..0000000 Binary files a/etc/test/david.mp3 and /dev/null differ diff --git a/etc/test/harvard.wav b/etc/test/harvard.wav deleted file mode 100644 index b05ec79..0000000 Binary files a/etc/test/harvard.wav and /dev/null differ diff --git a/etc/test/mu.png b/etc/test/mu.png deleted file mode 100644 index 2c7e067..0000000 Binary files a/etc/test/mu.png and /dev/null differ diff --git a/go.mod b/go.mod index 6ab21a9..90bddb0 100644 --- a/go.mod +++ b/go.mod @@ -9,16 +9,9 @@ require ( github.com/stretchr/testify v1.11.1 github.com/xdg-go/pbkdf2 v1.0.0 golang.org/x/crypto v0.41.0 - golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b golang.org/x/term v0.34.0 ) -require ( - github.com/kr/pretty v0.3.1 // indirect - github.com/rogpeppe/go-internal v1.14.1 // indirect - gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect -) - require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect diff --git a/go.sum b/go.sum index b1f9e64..d715839 100644 --- a/go.sum +++ b/go.sum @@ -1,44 +1,29 @@ github.com/andreburgaud/crypt2go v1.8.0 h1:J73vGTb1P6XL69SSuumbKs0DWn3ulbl9L92ZXBjw6pc= github.com/andreburgaud/crypt2go v1.8.0/go.mod h1:L5nfShQ91W78hOWhUH2tlGRPO+POAPJAF5fKOLB9SXg= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/djthorpe/go-errors v1.0.3 h1:GZeMPkC1mx2vteXLI/gvxZS0Ee9zxzwD1mcYyKU5jD0= github.com/djthorpe/go-errors v1.0.3/go.mod h1:HtfrZnMd6HsX75Mtbv9Qcnn0BqOrrFArvCaj3RMnZhY= github.com/djthorpe/go-tablewriter v0.0.11 h1:CimrEsiAG/KN2C8bTDC85RsZTsP2s5a7m7dqhaGFTv0= github.com/djthorpe/go-tablewriter v0.0.11/go.mod h1:ednj4tB4GHpenQL6NtDrbQW9VXyDdbIVTSH2693B+lI= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= -github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= -github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= -golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b h1:DXr+pvt3nC887026GRP39Ej11UATqWDmWuS99x26cD0= -golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4= golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/homeassistant/README.md b/pkg/homeassistant/README.md index 85681a3..e9b35ae 100644 --- a/pkg/homeassistant/README.md +++ b/pkg/homeassistant/README.md @@ -4,5 +4,5 @@ This package provides a client for the Home Assistant API, which is used to inte References: -- API https://developers.home-assistant.io/docs/api/rest/ -- Package https://pkg.go.dev/github.com/mutablelogic/go-client/pkg/homeassistant +- API +- Package diff --git a/pkg/homeassistant/events.go b/pkg/homeassistant/events.go index 0fbc98b..076f8de 100644 --- a/pkg/homeassistant/events.go +++ b/pkg/homeassistant/events.go @@ -1,6 +1,9 @@ package homeassistant -import "github.com/mutablelogic/go-client" +import ( + // Packages + "github.com/mutablelogic/go-client" +) /////////////////////////////////////////////////////////////////////////////// // TYPES diff --git a/pkg/homeassistant/services.go b/pkg/homeassistant/services.go index eb22cc4..61f73f5 100644 --- a/pkg/homeassistant/services.go +++ b/pkg/homeassistant/services.go @@ -2,11 +2,12 @@ package homeassistant import ( "encoding/json" + "maps" + "slices" "strings" // Packages "github.com/mutablelogic/go-client" - "golang.org/x/exp/maps" // Namespace imports . "github.com/djthorpe/go-errors" @@ -76,7 +77,7 @@ func (c *Client) Services(domain string) ([]*Service, error) { for k, v := range v.Services { v.Call = k } - return maps.Values(v.Services), nil + return slices.Collect(maps.Values(v.Services)), nil } // Return not found return nil, ErrNotFound.Withf("domain not found: %q", domain)