diff --git a/Makefile b/Makefile index 37e1c23..b2398ce 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,8 @@ VERSION = $(shell git describe --tags) BUILDTIME = $(shell date +%Y.%m.%d_%H%M) -default: verbose +default: install + regex --json /tmp/regex.55128216-e93b-4339-8854-622ca11af890.gemini-api-request.99.json connect vet: @GO111MODULE=off go vet @@ -15,16 +16,16 @@ build: goimports GO111MODULE=off go build \ -ldflags "-X main.VERSION=${VERSION} -X main.BUILDTIME=${BUILDTIME} -X gui.GUIVERSION=${VERSION}" -install: goimports vet +install: goimports GO111MODULE=off go install \ -ldflags "-X main.VERSION=${VERSION} -X main.BUILDTIME=${BUILDTIME} -X gui.GUIVERSION=${VERSION}" -regex: +regex: goimports GO111MODULE=off go install \ -ldflags "-X main.VERSION=${VERSION} -X main.BUILDTIME=${BUILDTIME} -X gui.GUIVERSION=${VERSION}" dumb-build: - go install \ + go install -v -x \ -ldflags "-X main.VERSION=${VERSION} -X main.BUILDTIME=${BUILDTIME} -X gui.GUIVERSION=${VERSION}" install-raw: goimports vet @@ -38,7 +39,6 @@ gocui: install regex --gui gocui --gui-verbose --gui-file ../../toolkits/gocui/gocui.so >/tmp/regex.log 2>&1 goimports: - reset goimports -w *.go @# // to globally reset paths: @# // gofmt -w -r '"go.wit.com/gui/gadgets" -> "go.wit.com/lib/gadgets"' *.go @@ -55,3 +55,5 @@ playback: regex playback # regex playback --uuid a1b2c3d4-e5f6-4a5b-8c9d-1e2f3a4b5c6d +tmp: + ls -tl /tmp/regex.* |head diff --git a/add.go b/add.go index 88d4ceb..01ff836 100644 --- a/add.go +++ b/add.go @@ -1,7 +1,6 @@ package main import ( - "fmt" "os" "path/filepath" @@ -14,15 +13,15 @@ import ( func addFile(filename string) (*chatpb.Chats, error) { data, err := os.ReadFile(filename) if err != nil { - return nil, fmt.Errorf("failed to read file %s: %w", filename, err) + return nil, log.Errorf("failed to read file %s: %w", filename, err) } logData, err := chatpb.UnmarshalChatsTEXT(data) if err != nil { - return nil, fmt.Errorf("failed to unmarshal log file %s: %w", filename, err) + return nil, log.Errorf("failed to unmarshal log file %s: %w", filename, err) } - log.Infof("Successfully parsed log file: %s", filename) + log.Info("Successfully parsed log file: %s", filename) // Get the directory of the log file to resolve relative content paths. logDir := filepath.Dir(filename) @@ -35,7 +34,7 @@ func addFile(filename string) (*chatpb.Chats, error) { contentPath := filepath.Join(logDir, contentFile) contentBytes, err := os.ReadFile(contentPath) if err != nil { - entry.Content = fmt.Sprintf("--- ERROR: Could not read content file %s: %v ---", contentPath, err) + entry.Content = log.Sprintf("--- ERROR: Could not read content file %s: %v ---", contentPath, err) } else { entry.Content = string(contentBytes) } @@ -48,7 +47,7 @@ func addFile(filename string) (*chatpb.Chats, error) { snippetPath := filepath.Join(logDir, snippetFile) contentBytes, err := os.ReadFile(snippetPath) if err != nil { - snippet.Content = fmt.Sprintf("--- ERROR: Could not read snippet file %s: %v ---", snippetPath, err) + snippet.Content = log.Sprintf("--- ERROR: Could not read snippet file %s: %v ---", snippetPath, err) } else { snippet.Content = string(contentBytes) } diff --git a/argv.go b/argv.go index af66e83..973f616 100644 --- a/argv.go +++ b/argv.go @@ -12,13 +12,17 @@ var argv args type args struct { Add string `arg:"--add" help:"add a new chat"` Format *EmptyCmd `arg:"subcommand:format" help:"add a conversation"` + Connect *EmptyCmd `arg:"subcommand:connect" help:"connect to gemini AI"` Playback *PlaybackCmd `arg:"subcommand:playback" help:"dump your prior conversations to the terminal'"` Output string `arg:"--output" help:"should get a string from regex-cli"` Input string `arg:"--input" help:"should get a string from regex-cli"` - Editor *EmptyCmd `arg:"subcommand:editor" help:"open env EDITOR"` + Editor *EmptyCmd `arg:"subcommand:interact" help:"open env EDITOR"` ImportFile string `arg:"--import" help:"import a file from regex-cli"` + JsonFile string `arg:"--json" help:"import a JSON file from gemini-cli"` + Uuid string `arg:"--uuid" help:"look at this uuid"` + Topic string `arg:"--topic" help:"the topic"` Stats []string `arg:"--stats" help:"add stats to a chat"` - NewChat []string `arg:"--new-chat" help:"create a new chat"` + NewChat *EmptyCmd `arg:"subcommand:newchat" help:"create a new chat"` GetNextAutoTopic bool `arg:"--get-next-auto-topic" help:"get the next auto topic name"` Force bool `arg:"--force" help:"try to strong arm things"` Verbose bool `arg:"--verbose" help:"show more output"` @@ -32,7 +36,6 @@ type EmptyCmd struct { type PlaybackCmd struct { List *EmptyCmd `arg:"subcommand:list" help:"list memories"` Long *EmptyCmd `arg:"subcommand:long" help:"show info on each chat"` - Uuid string `arg:"--uuid" help:"look at this uuid"` } func (args) Version() string { diff --git a/argvAutoshell.go b/argvAutoshell.go index 54549c3..4bdb79d 100644 --- a/argvAutoshell.go +++ b/argvAutoshell.go @@ -30,7 +30,7 @@ func (args) doBashAuto() { default: if argv.BashAuto[0] == ARGNAME { // list the subcommands here - fmt.Println("--add format playback editor") + fmt.Println("--add connect format interact playback") } } os.Exit(0) diff --git a/doConnect.go b/doConnect.go new file mode 100644 index 0000000..2285f0b --- /dev/null +++ b/doConnect.go @@ -0,0 +1,73 @@ +package main + +import ( + "context" + "fmt" + "os" + + "go.wit.com/log" + "google.golang.org/genai" +) + +// doConnect initializes the Gemini client and handles the request flow. +func doConnect() error { + apiKey := os.Getenv("GEMINI_API_KEY") + if apiKey == "" { + return log.Errorf("GEMINI_API_KEY environment variable not set") + } + + ctx := context.Background() + client, err := genai.NewClient(ctx, &genai.ClientConfig{APIKey: apiKey}) + if err != nil { + return log.Errorf("failed to create new genai client: %w", err) + } + + if argv.JsonFile != "" { + req, err := parseJSON(argv.JsonFile) + if err != nil { + return err + } + log.Info("parseJSON() ok. model =", req.Model) + + genaiContent, err := convertToGenai(req) + if err != nil { + return log.Errorf("failed to convert to genai.Content: %w", err) + } + log.Info("Successfully converted JSON to genai.Content") + // Here you would now use the 'genaiContent' to send to the API + _ = genaiContent // Prevent unused variable error for now + + return nil + } + + log.Info("doing sampleHello()") + return sampleHello(client) +} + +// sampleHello sends a hardcoded prompt to the model and prints the response. +func sampleHello(client *genai.Client) error { + log.Info("Sending 'hello, how are you' to the Gemini API...") + ctx := context.Background() + + // Create the parts slice + parts := []*genai.Part{ + {Text: "hello, how are you"}, + } + + content := []*genai.Content{{Parts: parts}} + + resp, err := client.Models.GenerateContent(ctx, "gemini-1.5-flash-latest", content, nil) + if err != nil { + return log.Errorf("error sending message: %v", err) + } + + log.Info("Response from API:") + for _, cand := range resp.Candidates { + if cand.Content != nil { + for _, part := range cand.Content.Parts { + fmt.Println(part) + } + } + } + return nil +} diff --git a/doConnect.go.notyet b/doConnect.go.notyet deleted file mode 100644 index 52fdc7c..0000000 --- a/doConnect.go.notyet +++ /dev/null @@ -1,28 +0,0 @@ -package main - -import ( - "context" - "fmt" - "os" - - "github.com/google/generative-ai-go/genai" - "google.golang.org/api/option" -) - -// doConnect initializes and returns a Gemini client. -// It reads the API key from the GEMINI_API_KEY environment variable. -func doConnect() (*genai.GenerativeModel, error) { - apiKey := os.Getenv("GEMINI_API_KEY") - if apiKey == "" { - return nil, fmt.Errorf("GEMINI_API_KEY environment variable not set") - } - - ctx := context.Background() - client, err := genai.NewClient(ctx, option.WithAPIKey(apiKey)) - if err != nil { - return nil, fmt.Errorf("failed to create new genai client: %w", err) - } - - model := client.GenerativeModel("gemini-pro") - return model, nil -} diff --git a/doEditor.go b/doEditor.go index 25051a3..acaf75c 100644 --- a/doEditor.go +++ b/doEditor.go @@ -32,7 +32,7 @@ func doEditor() error { log.Error(err) } os.Remove("/tmp/regex.ready") - log.Infof("SessionID: %s", string(content)) + log.Info("SessionID: %s", string(content)) logContent, err := ioutil.ReadFile("/tmp/regex.log") if err != nil { diff --git a/doGetNextAutoTopic.go b/doGetNextAutoTopic.go index 47fb524..647d209 100644 --- a/doGetNextAutoTopic.go +++ b/doGetNextAutoTopic.go @@ -1,9 +1,10 @@ package main import ( - "fmt" "strconv" "strings" + + "go.wit.com/log" ) func doGetNextAutoTopic() { @@ -20,5 +21,5 @@ func doGetNextAutoTopic() { } } } - fmt.Printf("Auto %d", max+1) + log.Printf("Auto %d", max+1) } diff --git a/doNewChat.go b/doNewChat.go index bd71f45..f9793d1 100644 --- a/doNewChat.go +++ b/doNewChat.go @@ -1,28 +1,19 @@ package main import ( - "fmt" - "go.wit.com/lib/protobuf/chatpb" "go.wit.com/log" "google.golang.org/protobuf/types/known/timestamppb" ) func doNewChat() { - if len(argv.NewChat) != 2 { - log.Error(fmt.Errorf("expected 2 arguments for --new-chat")) - return - } - uuid := argv.NewChat[0] - topic := argv.NewChat[1] - chat := &chatpb.Chat{ - Uuid: uuid, - ChatName: topic, + Uuid: argv.Uuid, + ChatName: argv.Topic, Ctime: timestamppb.Now(), } me.chats.Chats = append(me.chats.Chats, chat) me.chats.ConfigSave() - log.Info("created new chat for", uuid) + log.Info("created new chat for", argv.Uuid) } diff --git a/doPlayback.go b/doPlayback.go index 5e9ab0f..42dd95d 100644 --- a/doPlayback.go +++ b/doPlayback.go @@ -1,7 +1,6 @@ package main import ( - "fmt" "strings" "go.wit.com/lib/protobuf/chatpb" @@ -9,8 +8,8 @@ import ( ) func doPlayback() { - if argv.Playback.Uuid != "" { - showChat(argv.Playback.Uuid) + if argv.Uuid != "" { + showChat(argv.Uuid) return } @@ -34,8 +33,8 @@ func listChats(chats *chatpb.Chats) { return } - log.Infof("Found %d chat topic(s) in the log.", len(chats.GetChats())) - fmt.Println("-------------------------------------------------") + log.Info("Found %d chat topic(s) in the log.", len(chats.GetChats())) + log.Println("-------------------------------------------------") for _, chat := range chats.GetChats() { entryCount := len(chat.GetEntries()) @@ -47,19 +46,19 @@ func listChats(chats *chatpb.Chats) { formattedTime = "No Timestamp" } - fmt.Printf("Topic: %-25s | Entries: %-4d | Started: %s | UUID: %s\n", + log.Printf("Topic: %-25s | Entries: %-4d | Started: %s | UUID: %s\n", chat.GetChatName(), entryCount, formattedTime, chat.GetUuid(), ) } - fmt.Println("-------------------------------------------------") + log.Println("-------------------------------------------------") } // print out one line for each chat entry func listEntries(chat *chatpb.Chat) { - fmt.Printf("--- Entries for Topic: %s ---\n", chat.GetChatName()) + log.Printf("--- Entries for Topic: %s ---\n", chat.GetChatName()) width := getTerminalWidth() // Determine the maximum length of the author and time string @@ -73,7 +72,7 @@ func listEntries(chat *chatpb.Chat) { } else { formattedTime = "No Time" } - authorAndTime := fmt.Sprintf("[%s] (%s)", author, formattedTime) + authorAndTime := log.Sprintf("[%s] (%s)", author, formattedTime) if len(authorAndTime) > maxAuthorAndTimeLen { maxAuthorAndTimeLen = len(authorAndTime) } @@ -94,7 +93,7 @@ func listEntries(chat *chatpb.Chat) { // Replace newlines with spaces for a clean one-line view contentPreview = strings.ReplaceAll(contentPreview, "\n", " ") - authorAndTime := fmt.Sprintf("[%s] (%s)", author, formattedTime) + authorAndTime := log.Sprintf("[%s] (%s)", author, formattedTime) availableWidth := width - maxAuthorAndTimeLen - 1 // -1 for a space if len(contentPreview) > availableWidth { @@ -112,14 +111,14 @@ func listEntries(chat *chatpb.Chat) { if authorAndTimePadding < 0 { authorAndTimePadding = 0 } - fmt.Printf("%s%s%s%s\n", contentPreview, strings.Repeat(" ", padding), strings.Repeat(" ", authorAndTimePadding), authorAndTime) + log.Printf("%s%s%s%s\n", contentPreview, strings.Repeat(" ", padding), strings.Repeat(" ", authorAndTimePadding), authorAndTime) } else { padding := maxAuthorAndTimeLen - len(authorAndTime) if padding < 0 { padding = 0 } - fmt.Printf("%s%s %s\n", authorAndTime, strings.Repeat(" ", padding), contentPreview) + log.Printf("%s%s %s\n", authorAndTime, strings.Repeat(" ", padding), contentPreview) } } - fmt.Println("-------------------------------------------------") + log.Println("-------------------------------------------------") } diff --git a/doStats.go b/doStats.go index 19ca180..3013c97 100644 --- a/doStats.go +++ b/doStats.go @@ -2,7 +2,6 @@ package main import ( "encoding/json" - "fmt" "go.wit.com/lib/protobuf/chatpb" "go.wit.com/log" @@ -11,7 +10,7 @@ import ( func doStats() { if len(argv.Stats) != 2 { - log.Error(fmt.Errorf("expected 2 arguments for --stats")) + log.Warn("expected 2 arguments for --stats") return } sessionUuid := argv.Stats[0] @@ -36,7 +35,7 @@ func doStats() { var stats chatpb.SessionStats err := json.Unmarshal([]byte(statsString), &stats) if err != nil { - log.Error(fmt.Errorf("error unmarshalling stats: %w", err)) + log.Printf("error unmarshalling stats: %w", err) return } diff --git a/json.go b/json.go new file mode 100644 index 0000000..129046d --- /dev/null +++ b/json.go @@ -0,0 +1,134 @@ +package main + +import ( + "encoding/json" + "os" + + "go.wit.com/log" + "google.golang.org/genai" +) + +// GeminiRequest matches the overall structure of the gemini-cli JSON output. +type GeminiRequest struct { + Model string `json:"model"` + Contents []Content `json:"contents"` + // Config is left as a raw message because its structure is complex and not needed for now. + Config json.RawMessage `json:"config"` +} + +// Content matches the 'contents' array elements. +type Content struct { + Role string `json:"role"` + Parts []Part `json:"parts"` +} + +// Part matches the 'parts' array elements. +// It can contain one of several types of data. +type Part struct { + Text string `json:"text,omitempty"` + ThoughtSignature string `json:"thoughtSignature,omitempty"` + FunctionCall *FunctionCall `json:"functionCall,omitempty"` + FunctionResponse *FunctionResponse `json:"functionResponse,omitempty"` +} + +// FunctionCall matches the 'functionCall' object. +type FunctionCall struct { + Name string `json:"name"` + Args map[string]string `json:"args"` +} + +// FunctionResponse matches the 'functionResponse' object. +type FunctionResponse struct { + ID string `json:"id"` + Name string `json:"name"` + Response map[string]interface{} `json:"response"` +} + +// parseJSON opens the given file, reads it, and unmarshals it into our structs. +func parseJSON(filename string) (*GeminiRequest, error) { + log.Infof("Attempting to parse file: %s\n", filename) + + // Read the entire file + data, err := os.ReadFile(filename) + if err != nil { + return nil, log.Errorf("failed to read file %s: %w", filename, err) + } + + // Unmarshal the JSON data + var req *GeminiRequest + req = new(GeminiRequest) + if err := json.Unmarshal(data, &req); err != nil { + return nil, log.Errorf("failed to unmarshal JSON from %s: %w", filename, err) + } + + dumpSummaryJSON(req) + return req, nil +} + +func dumpSummaryJSON(req *GeminiRequest) { + var totalFC, totalTexts, totalFR int + + // Log the parsed data to confirm it worked + + // Example of accessing deeper data + for _, content := range req.Contents { + // log.Infof("Content[%d] Role: %s", i, content.Role) + for _, part := range content.Parts { + if part.Text != "" { + // log.Infof(" Part[%d] Text: %.60s...", j, part.Text) // Print snippet + totalTexts += 1 + } + if part.FunctionCall != nil { + // log.Infof(" Part[%d] FunctionCall: %s", j, part.FunctionCall.Name) + totalFC += 1 + } + if part.FunctionResponse != nil { + // log.Infof(" Part[%d] FunctionCall: %s", j, part.FunctionCall.Name) + totalFR += 1 + } + } + } + log.Printf("Parsed JSON (Model: %s) (# of content blocks %d) (Text #=%d) (FC=%d) (FR=%d)\n", req.Model, len(req.Contents), totalTexts, totalFC, totalFR) +} + +func dumpFullJSON(req *GeminiRequest) { + // Log the parsed data to confirm it worked + log.Info("Successfully parsed JSON file.") + log.Infof("Model: %s", req.Model) + log.Infof("Number of content blocks: %d", len(req.Contents)) + + // Example of accessing deeper data + for i, content := range req.Contents { + log.Infof("Content[%d] Role: %s", i, content.Role) + for j, part := range content.Parts { + if part.Text != "" { + log.Infof(" Part[%d] Text: %.60s...", j, part.Text) // Print snippet + } + if part.FunctionCall != nil { + log.Infof(" Part[%d] FunctionCall: %s", j, part.FunctionCall.Name) + } + } + } +} + +// convertToGenai transforms the parsed JSON request into the genai.Content format. +func convertToGenai(req *GeminiRequest) ([]*genai.Content, error) { + var contents []*genai.Content + for _, c := range req.Contents { + genaiParts := []*genai.Part{} // Create a slice of the interface type + for _, p := range c.Parts { + if p.Text != "" { + // genai.Text returns a Part interface, which is what we need + var tmp *genai.Part + tmp = new(genai.Part) + tmp.Text = p.Text + genaiParts = append(genaiParts, tmp) + } + } + contents = append(contents, &genai.Content{ + Role: c.Role, + Parts: genaiParts, + }) + } + return contents, nil +} diff --git a/main.go b/main.go index 4afcff4..6f91f27 100644 --- a/main.go +++ b/main.go @@ -32,6 +32,8 @@ var ARGNAME string = "regex" var configSave bool func main() { + // f, _ := os.OpenFile("/tmp/regex.secret.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + // log.CaptureMode(f) me = new(mainType) gui.InitArg() me.pp = arg.MustParse(&argv) @@ -58,6 +60,14 @@ func main() { okExit("") } + if argv.Connect != nil { + err := doConnect() + if err != nil { + badExit(err) + } + okExit("") + } + if argv.Editor != nil { doEditor() okExit("") @@ -105,24 +115,29 @@ func main() { } if argv.Playback != nil { - if argv.Playback.Uuid != "" { - showChat(argv.Playback.Uuid) + if argv.Uuid != "" { + showChat(argv.Uuid) } else { doPlayback() } okExit("") } - // if opening the GUI, always check git for dirty repos - log.Info("look for 'auto' here") - // Find the "auto" chat. - for _, chat := range me.chats.GetChats() { - if chat.GetChatName() == "auto" { - prettyFormatChat(chat) - okExit("") + /* + // if opening the GUI, always check git for dirty repos + log.Info("look for 'auto' here") + // Find the "auto" chat. + for _, chat := range me.chats.GetChats() { + if chat.GetChatName() == "auto" { + prettyFormatChat(chat) + okExit("") + } } - } + */ // doGui() + + // by default, start interacting with gemini-cli + doEditor() okExit("") } diff --git a/prettyFormat.go b/prettyFormat.go index 03602bf..c223214 100644 --- a/prettyFormat.go +++ b/prettyFormat.go @@ -7,20 +7,20 @@ package main import ( - "fmt" "path/filepath" "strings" "go.wit.com/lib/protobuf/chatpb" + "go.wit.com/log" ) const termWidth = 100 // The target width for the formatted output boxes. // prettyFormatChat is the main entry point to print a detailed view of a Chat topic. func prettyFormatChat(chat *chatpb.Chat) { - fmt.Printf("\n========================================================\n") - fmt.Printf("== Chat Topic: %s (UUID: %s)\n", chat.GetChatName(), chat.GetUuid()) - fmt.Printf("========================================================\n\n") + log.Printf("\n========================================================\n") + log.Printf("== Chat Topic: %s (UUID: %s)\n", chat.GetChatName(), chat.GetUuid()) + log.Printf("========================================================\n\n") for _, entry := range chat.GetEntries() { author := entry.GetFrom().String() @@ -50,7 +50,7 @@ func prettyFormatChat(chat *chatpb.Chat) { printCodeSnippet(snippet) } } - fmt.Println() + log.Println() } } @@ -65,8 +65,8 @@ func printContent(author, timestamp, content string) { } func printLeftAligned(author, timestamp, content string) { - prefix := fmt.Sprintf("✦ %s (%s):", author, timestamp) - fmt.Println(prefix) + prefix := log.Sprintf("✦ %s (%s):", author, timestamp) + log.Println(prefix) indent := "\t" contentWidth := termWidth - 8 // 8 spaces for a standard tab @@ -76,7 +76,7 @@ func printLeftAligned(author, timestamp, content string) { for _, paragraph := range paragraphs { words := strings.Fields(paragraph) if len(words) == 0 { - fmt.Println() // Preserve paragraph breaks + log.Println() // Preserve paragraph breaks continue } @@ -85,19 +85,19 @@ func printLeftAligned(author, timestamp, content string) { if len(currentLine)+1+len(word) <= contentWidth { currentLine += " " + word } else { - fmt.Println(currentLine) + log.Println(currentLine) currentLine = indent + word } } - fmt.Println(currentLine) + log.Println(currentLine) } } func printRightAligned(author, timestamp, content string) { - prefix := fmt.Sprintf("(%s) %s ✦", timestamp, author) + prefix := log.Sprintf("(%s) %s ✦", timestamp, author) // Print the prefix first, right-aligned. - fmt.Printf("%*s\n", termWidth, prefix) + log.Printf("%*s\n", termWidth, prefix) // The available width for the text. contentWidth := termWidth - 8 // Leave a tab's worth of margin on the left @@ -107,7 +107,7 @@ func printRightAligned(author, timestamp, content string) { for _, paragraph := range paragraphs { words := strings.Fields(paragraph) if len(words) == 0 { - fmt.Println() // Preserve paragraph breaks + log.Println() // Preserve paragraph breaks continue } @@ -117,12 +117,12 @@ func printRightAligned(author, timestamp, content string) { currentLine += " " + word } else { // Print the completed line, right-aligned. - fmt.Printf("%*s\n", termWidth, currentLine) + log.Printf("%*s\n", termWidth, currentLine) currentLine = word } } // Print the last remaining line of the paragraph, right-aligned. - fmt.Printf("%*s\n", termWidth, currentLine) + log.Printf("%*s\n", termWidth, currentLine) } } @@ -130,11 +130,11 @@ func printTable(table *chatpb.Table) { if table == nil || len(table.GetRows()) == 0 { return } - fmt.Println("┌─[ Table Data ]──────────────────────────────────────────") + log.Println("┌─[ Table Data ]──────────────────────────────────────────") for _, row := range table.GetRows() { - fmt.Printf("│ %s\n", strings.Join(row.GetFields(), " │ ")) + log.Printf("│ %s\n", strings.Join(row.GetFields(), " │ ")) } - fmt.Printf("└─────────────────────────────────────────────────────────\n\n") + log.Printf("└─────────────────────────────────────────────────────────\n\n") } func printCodeSnippet(snippet *chatpb.CodeSnippet) { @@ -142,11 +142,11 @@ func printCodeSnippet(snippet *chatpb.CodeSnippet) { code := snippet.GetContent() language := filepath.Base(snippet.GetFilename()) // Still useful for display - fmt.Println() // Add extra line feed for spacing + log.Println() // Add extra line feed for spacing // --- Top Border --- - topBorder := fmt.Sprintf("┌─[ Code Snippet: %s ]", language) - fmt.Printf("%s%s┐\n", topBorder, strings.Repeat("─", termWidth-len(topBorder)-1)) + topBorder := log.Sprintf("┌─[ Code Snippet: %s ]", language) + log.Printf("%s%s┐\n", topBorder, strings.Repeat("─", termWidth-len(topBorder)-1)) // --- Content Lines --- for _, line := range strings.Split(strings.TrimSpace(code), "\n") { @@ -155,17 +155,17 @@ func printCodeSnippet(snippet *chatpb.CodeSnippet) { if padding < 0 { padding = 0 // Should not happen with wrapping, but as a safeguard } - fmt.Printf("│ %s%s │\n", line, strings.Repeat(" ", padding)) + log.Printf("│ %s%s │\n", line, strings.Repeat(" ", padding)) } // --- Bottom Border --- - fmt.Printf("└%s┘\n\n", strings.Repeat("─", termWidth-2)) + log.Printf("└%s┘\n\n", strings.Repeat("─", termWidth-2)) } func printToolCallBox(tc *chatpb.ToolCall) { boxWidth := termWidth - 2 - fmt.Printf(" ╭%s╮\n", strings.Repeat("─", boxWidth)) - header := fmt.Sprintf(" ✔ %s %s (%s)", tc.GetName(), tc.GetInput(), tc.GetDescription()) + log.Printf(" ╭%s╮\n", strings.Repeat("─", boxWidth)) + header := log.Sprintf(" ✔ %s %s (%s)", tc.GetName(), tc.GetInput(), tc.GetDescription()) printWrappedLine(header, boxWidth) printEmptyLine(boxWidth) if stdout := tc.GetOutputStdout(); stdout != "" { @@ -179,7 +179,7 @@ func printToolCallBox(tc *chatpb.ToolCall) { } } printEmptyLine(boxWidth) - fmt.Printf(" ╰%s╯\n", strings.Repeat("─", boxWidth)) + log.Printf(" ╰%s╯\n", strings.Repeat("─", boxWidth)) } func printWrappedLine(text string, width int) { @@ -188,12 +188,12 @@ func printWrappedLine(text string, width int) { return } for len(text) > width { - fmt.Printf(" │ %-*s │\n", width, text[:width]) + log.Printf(" │ %-*s │\n", width, text[:width]) text = text[width:] } - fmt.Printf(" │ %-*s │\n", width, text) + log.Printf(" │ %-*s │\n", width, text) } func printEmptyLine(width int) { - fmt.Printf(" │ %*s │\n", width, "") + log.Printf(" │ %*s │\n", width, "") } diff --git a/stats.go b/stats.go new file mode 100644 index 0000000..ae0d2c1 --- /dev/null +++ b/stats.go @@ -0,0 +1,69 @@ +package main + +/* + // The following are the Go equivalents of the gemini-cli's TypeScript + // interfaces for session statistics. You can use these to collect and + // store metrics in your application. + + // ToolCallDecision represents the user's decision on a tool call. + type ToolCallDecision string + + const ( + Accept ToolCallDecision = "accept" + Reject ToolCallDecision = "reject" + Modify ToolCallDecision = "modify" + AutoAccept ToolCallDecision = "auto_accept" + ) + + // ToolCallStats holds the statistics for a single tool. + type ToolCallStats struct { + Count int + Success int + Fail int + DurationMs int + Decisions map[ToolCallDecision]int + } + + // ModelMetrics holds the statistics for a single model. + type ModelMetrics struct { + API struct { + TotalRequests int + TotalErrors int + TotalLatencyMs int + } + Tokens struct { + Prompt int + Candidates int + Total int + Cached int + Thoughts int + Tool int + } + } + + // SessionMetrics holds all the statistics for a session. + type SessionMetrics struct { + Models map[string]ModelMetrics + Tools struct { + TotalCalls int + TotalSuccess int + TotalFail int + TotalDurationMs int + TotalDecisions map[ToolCallDecision]int + ByName map[string]ToolCallStats + } + Files struct { + TotalLinesAdded int + TotalLinesRemoved int + } + } + + // You will need to initialize and update this struct as your application + // makes API calls and runs tools. + var sessionMetrics SessionMetrics + + // Example of how you might update the metrics after an API call: + // modelMetrics := sessionMetrics.Models["gemini-pro"] + // modelMetrics.API.TotalRequests++ + // ... and so on. +*/