Compare commits

...

10 Commits

Author SHA1 Message Date
Jeff Carr 535cb98744 looking good. adds the json file okay 2025-08-30 19:08:41 -05:00
Jeff Carr 6ce2991074 more crap 2025-08-30 18:48:44 -05:00
Jeff Carr 96843095f5 minor 2025-08-30 18:14:47 -05:00
Jeff Carr fbbed0475b parse stuff 2025-08-30 18:07:41 -05:00
Jeff Carr 512ebf5be6 attempt to marshal JSON file to protobuf 2025-08-30 16:54:54 -05:00
Jeff Carr 6888512b3b minor cleanups 2025-08-30 16:17:01 -05:00
Jeff Carr b750fb9252 GEMINI workflow rules 2025-08-30 15:48:22 -05:00
Jeff Carr bbb12d79e2 more code cleanups 2025-08-30 15:24:54 -05:00
Jeff Carr c7896e47f9 code cleanup of early drafts of code from Gemini AI 2025-08-30 15:15:43 -05:00
Jeff Carr 7f8f5e3b9b basic JSON parsing into genai struc 2025-08-30 14:42:57 -05:00
19 changed files with 452 additions and 292 deletions

1
.gitignore vendored
View File

@ -4,3 +4,4 @@ go.sum
/resources/*.so
/files/*
regex
/tmp/*

17
GEMINI.md Normal file
View File

@ -0,0 +1,17 @@
## Gemini Added Memories
- The user and I were analyzing a RISC-V Go panic. The user provided a panic log showing a 'SIGQUIT: quit' signal. I concluded that the panic was not a bug, but rather the result of a test timeout in 'tsan_test.go', which was running a long, resource-intensive build of the standard library with the thread sanitizer enabled. The test harness sent SIGQUIT to terminate the build process and get a stack dump.
- Assume my base directory is `/home/jcarr/go/src`
- The gemini-cli application is located in github.com/google-gemini/gemini-cli and is a Node.js project.
- To test the build of gemini-cli, always run exactly: cd github.com/google-gemini/gemini-cli && make build
- The "regex" application is located in `go.wit.com/apps/regex` and is written in Go
- To build the 'regex' application, I must first change into the `go.wit.com/apps/regex` directory and then run the `make regex` command.
- The "Google Gemini AI GO API" is located in `google.golang.org/genai`.
- I must not search for anything. Instead, I must stop and ask the user for the location of files or information.
- When committing to git, I will use the author name 'Castor Regex' and the email 'regex@wit.com'.
- My git workflow is: 1. `git status`. 2. `git add <files>`. 3. `GIT_AUTHOR_NAME='Castor Regex' GIT_AUTHOR_EMAIL='regex@wit.com' git commit -m '...'`. I will not run `git push`.
- When asked to commit code with git, I should follow the git workflow
- My standard workflow: Upon completing a task and verifying the solution, I will automatically commit the changes. Then follow my git workflow
- My standard git workflow: Upon completing a task and verifying the solution, I will automatically commit the changes following the standard workflow.
- New operational rule: If a fix or modification I make results in an error, I will NOT revert the changes with `git checkout`. I will leave the modified files in their current state for the user to inspect and debug.
- CRITICAL WORKFLOW RULE: After making any code changes and before committing, I MUST always attempt to build the project (e.g., run 'make' or the appropriate build command). I am only allowed to commit changes that compile successfully. If a build fails, I must fix the compilation error before proceeding with the commit. I will not commit broken code.
- WORKFLOW UPDATE: If a build fails and the user subsequently tells me to commit, I will assume the user has fixed the compilation errors. My next step is to re-run the build command to verify. If this verification build succeeds, I will then proceed with committing my original changes. If it still fails, I will report the new error.

View File

@ -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.bf203df1-ce24-42ea-93e9-10e0d8b5e8b0.gemini-api-request.64.json
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
tmpfiles:
ls -tl /tmp/regex.* |head

11
add.go
View File

@ -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)
}

31
argv.go
View File

@ -10,29 +10,26 @@ package main
var argv args
type args struct {
Add string `arg:"--add" help:"add a new chat"`
Format *EmptyCmd `arg:"subcommand:format" help:"add a conversation"`
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"`
ImportFile string `arg:"--import" help:"import a file from regex-cli"`
Stats []string `arg:"--stats" help:"add stats to a chat"`
NewChat []string `arg:"--new-chat" 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"`
Bash bool `arg:"--bash" help:"generate bash completion"`
BashAuto []string `arg:"--auto-complete" help:"todo: move this to go-arg"`
Uuid string `arg:"--uuid" help:"look at this uuid"`
Topic string `arg:"--topic" help:"set the topic"`
JsonFile string `arg:"--json" help:"import a JSON file from gemini-cli"`
Interact *EmptyCmd `arg:"subcommand:interact" help:"open env EDITOR"`
Playback *PlaybackCmd `arg:"subcommand:playback" help:"dump your prior conversations to the terminal'"`
NewChat *PlaybackCmd `arg:"subcommand:newchat" help:"used by gemini-cli on startup"`
Stats string `arg:"--stats" help:"add stats to a chat"`
Force bool `arg:"--force" help:"try to strong arm things"`
Verbose bool `arg:"--verbose" help:"show more output"`
Bash bool `arg:"--bash" help:"generate bash completion"`
BashAuto []string `arg:"--auto-complete" help:"todo: move this to go-arg"`
}
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"`
List *EmptyCmd `arg:"subcommand:list" help:"list memories"`
Long *EmptyCmd `arg:"subcommand:long" help:"show info on each chat"`
Purge *EmptyCmd `arg:"subcommand:purge" help:"verify chat uuids & purge empty chats"`
}
func (args) Version() string {

View File

@ -21,16 +21,16 @@ func deleteMatch() {
}
func (args) doBashAuto() {
argv.doBashHelp()
// argv.doBashHelp()
switch argv.BashAuto[0] {
case "playback":
fmt.Println("long --uuid")
fmt.Println("long --uuid purge")
case "clean":
fmt.Println("user devel master")
default:
if argv.BashAuto[0] == ARGNAME {
// list the subcommands here
fmt.Println("--add format playback editor")
fmt.Println("--json interact playback")
}
}
os.Exit(0)

54
doConnect.go Normal file
View File

@ -0,0 +1,54 @@
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() (*genai.Client, error) {
apiKey := os.Getenv("GEMINI_API_KEY")
if apiKey == "" {
return nil, 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 nil, log.Errorf("failed to create new genai client: %w", err)
}
return client, err
}
// sampleHello sends a hardcoded prompt to the model and prints the response.
func simpleHello(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-2.5-flash", 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
}

View File

@ -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
}

View File

@ -1,15 +1,13 @@
package main
import (
"fmt"
"strconv"
"strings"
"go.wit.com/log"
)
func doGetNextAutoTopic() {
if err := me.chats.ConfigLoad(); err != nil {
badExit(err)
}
max := 0
for _, chat := range me.chats.GetChats() {
if strings.HasPrefix(chat.GetChatName(), "Auto ") {
@ -20,5 +18,5 @@ func doGetNextAutoTopic() {
}
}
}
fmt.Printf("Auto %d", max+1)
log.Printf("Auto %d", max+1)
}

View File

@ -1,56 +0,0 @@
package main
import (
"io/ioutil"
"time"
"go.wit.com/lib/protobuf/chatpb"
"go.wit.com/log"
"google.golang.org/protobuf/types/known/timestamppb"
)
func doImport(filename string) {
content, err := ioutil.ReadFile(filename)
if err != nil {
log.Warn("Error reading import file:", err)
return
}
s := string(content)
// Load the existing chats.
all := chatpb.NewChats()
if err := all.ConfigLoad(); err != nil {
log.Warn("Error loading config, can't add to auto chat:", err)
return
}
// Find the "auto" chat.
var autoChat *chatpb.Chat
for _, chat := range all.GetChats() {
if chat.GetChatName() == "auto" {
autoChat = chat
break
}
}
// If the "auto" chat is found, add the new entry.
if autoChat != nil {
newEntry := &chatpb.ChatEntry{
From: chatpb.Who_REGEX,
Ctime: timestamppb.New(time.Now()),
ToolCalls: []*chatpb.ToolCall{
{
Name: "Shell",
Input: s,
},
},
}
autoChat.Entries = append(autoChat.Entries, newEntry)
if err := all.ConfigSave(); err != nil {
log.Warn("Error saving config after adding to auto chat:", err)
} else {
log.Info("Added new entry to 'auto' chat.")
}
}
}

View File

@ -1,56 +0,0 @@
package main
import (
"os"
"time"
"go.wit.com/lib/protobuf/chatpb"
"go.wit.com/log"
"google.golang.org/protobuf/types/known/timestamppb"
)
func doInput(s string) {
filename := "/tmp/regex-input.log"
f, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
log.Println(err)
return
}
defer f.Close()
if _, err := f.WriteString(s + "\n"); err != nil {
log.Println(err)
}
log.Info("INPUT LOGGED TO", filename)
// Load the existing chats.
all := chatpb.NewChats()
if err := all.ConfigLoad(); err != nil {
log.Warn("Error loading config, can't add to auto chat:", err)
return
}
// Find the "auto" chat.
var autoChat *chatpb.Chat
for _, chat := range all.GetChats() {
if chat.GetChatName() == "auto" {
autoChat = chat
break
}
}
// If the "auto" chat is found, add the new entry.
if autoChat != nil {
newEntry := &chatpb.ChatEntry{
From: chatpb.Who_USER,
Content: s,
Ctime: timestamppb.New(time.Now()),
}
autoChat.Entries = append(autoChat.Entries, newEntry)
if err := all.ConfigSave(); err != nil {
log.Warn("Error saving config after adding to auto chat:", err)
} else {
log.Info("Added new entry to 'auto' chat.")
}
}
}

View File

@ -10,7 +10,7 @@ import (
"go.wit.com/log"
)
func doEditor() error {
func doInteract() error {
for {
filename, err := doEditorOnce()
if err != nil {
@ -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 {

View File

@ -1,28 +1,24 @@
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"))
if found := me.chats.FindByUuid(argv.Uuid); found != nil {
found.ChatName = argv.Topic
me.chats.ConfigSave()
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)
}

View File

@ -1,20 +1,38 @@
package main
import (
"fmt"
"strings"
"go.wit.com/lib/protobuf/chatpb"
"go.wit.com/log"
)
func doPlayback() {
if argv.Playback.Uuid != "" {
showChat(argv.Playback.Uuid)
return
func doPlayback() error {
if argv.Uuid != "" {
showChat(argv.Uuid)
return nil
}
if argv.Playback.Purge != nil {
doPurge()
return nil
}
listChats(me.chats)
return nil
}
func doPurge() {
changed := false
for _, chat := range me.chats.GetChats() {
if len(chat.GetEntries()) == 0 {
me.chats.Delete(chat)
changed = true
}
}
if changed {
me.chats.ConfigSave()
}
}
func showChat(uuid string) {
@ -34,8 +52,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 +65,19 @@ func listChats(chats *chatpb.Chats) {
formattedTime = "No Timestamp"
}
fmt.Printf("Topic: %-25s | Entries: %-4d | Started: %s | UUID: %s\n",
chat.GetChatName(),
log.Printf("Entries: %-4d | Started: %-25s | UUID: %s | %-25s\n",
entryCount,
formattedTime,
chat.GetUuid(),
chat.GetChatName(),
)
}
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 +91,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 +112,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 +130,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("-------------------------------------------------")
}

View File

@ -2,7 +2,6 @@ package main
import (
"encoding/json"
"fmt"
"go.wit.com/lib/protobuf/chatpb"
"go.wit.com/log"
@ -10,12 +9,8 @@ import (
)
func doStats() {
if len(argv.Stats) != 2 {
log.Error(fmt.Errorf("expected 2 arguments for --stats"))
return
}
sessionUuid := argv.Stats[0]
statsString := argv.Stats[1]
sessionUuid := argv.Uuid
statsString := "todo: set this somehow"
// Find the "auto" chat, or create it if it doesn't exist.
var autoChat *chatpb.Chat
@ -36,7 +31,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
}

148
json.go Normal file
View File

@ -0,0 +1,148 @@
package main
import (
"encoding/json"
"os"
"go.wit.com/lib/protobuf/chatpb"
"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"`
}
func parsePB(filename string) (*chatpb.GeminiRequest, error) {
// Read the entire file
data, err := os.ReadFile(filename)
if err != nil {
return nil, log.Errorf("failed to read file %s: %w", filename, err)
}
pb := new(chatpb.GeminiRequest)
if err := pb.UnmarshalJSON(data); err != nil {
return nil, err
}
return pb, nil
}
// 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
}

108
main.go
View File

@ -8,6 +8,9 @@ package main
import (
"embed"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/google/uuid"
"go.wit.com/dev/alexflint/arg"
@ -32,6 +35,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)
@ -45,21 +50,64 @@ func main() {
os.Exit(0)
}
// load the default chat protobuf
me.chats = chatpb.NewChats()
if err := me.chats.ConfigLoad(); err != nil {
badExit(err)
}
// verify all the chats have Uuid's
if verifyUuids(me.chats) {
me.chats.ConfigSave()
}
if argv.GetNextAutoTopic {
doGetNextAutoTopic()
aiClient, err := doConnect()
if err != nil {
badExit(err)
}
_ = aiClient
if argv.JsonFile != "" {
// now try to Marshal() into a protobuf
pb, err := parsePB(argv.JsonFile)
if err != nil {
badExit(err)
}
log.Info("GeminiContent pb.Marshal() worked pb.Contents len =", len(pb.Contents))
_, filename := filepath.Split(argv.JsonFile)
parts := strings.Split(filename, ".")
if len(parts) == 5 {
uuid := parts[1]
num, _ := strconv.Atoi(parts[3])
log.Info(uuid, parts)
if chat := me.chats.FindByUuid(uuid); chat != nil {
log.Info("FOUND CHAT", uuid, num)
newEntry := new(chatpb.ChatEntry)
newEntry.GeminiRequest = pb
newEntry.ContentFile = filename
newEntry.RequestCounter = int32(num)
chat.AppendEntry(newEntry)
me.chats.ConfigSave()
}
} else {
}
okExit("")
}
if argv.Editor != nil {
doEditor()
if argv.Interact != nil {
log.Info("testing AI client with simpleHello()")
err = simpleHello(aiClient)
if err != nil {
badExit(err)
}
doInteract()
okExit("")
}
if argv.Stats != "" {
doStats()
okExit("")
}
@ -68,61 +116,19 @@ func main() {
okExit("")
}
if argv.Stats != nil {
doStats()
okExit("")
}
if argv.Output != "" {
doOutput(argv.Output)
okExit("")
}
if argv.Input != "" {
doInput(argv.Input)
okExit("")
}
if argv.ImportFile != "" {
doImport(argv.ImportFile)
okExit("")
}
if argv.Add != "" {
newChats, err := addFile(argv.Add)
if err != nil {
badExit(err)
}
verifyUuids(newChats)
for _, newChat := range newChats.GetChats() {
me.chats.AppendByUuid(newChat)
log.Info("Attempting to add chat", newChat.ChatName)
}
me.chats.ConfigSave()
okExit("")
}
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("")
}
}
// doGui()
// by default, start interacting with gemini-cli
me.pp.WriteHelp(os.Stdout)
okExit("")
}

View File

@ -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, "")
}

69
stats.go Normal file
View File

@ -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.
*/