summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile12
-rw-r--r--add.go11
-rw-r--r--argv.go9
-rw-r--r--argvAutoshell.go2
-rw-r--r--doConnect.go73
-rw-r--r--doConnect.go.notyet28
-rw-r--r--doEditor.go2
-rw-r--r--doGetNextAutoTopic.go5
-rw-r--r--doNewChat.go15
-rw-r--r--doPlayback.go25
-rw-r--r--doStats.go5
-rw-r--r--json.go134
-rw-r--r--main.go35
-rw-r--r--prettyFormat.go58
-rw-r--r--stats.go69
15 files changed, 370 insertions, 113 deletions
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.
+*/