cli: add `/export` command to export current session

This commit is contained in:
Ariful Alam 2025-08-20 12:57:10 +06:00
parent f804e8a460
commit 8a62dbb814
2 changed files with 116 additions and 0 deletions

View File

@ -11,6 +11,7 @@ import (
"regexp" "regexp"
"slices" "slices"
"strings" "strings"
"time"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -36,6 +37,7 @@ func generateInteractive(cmd *cobra.Command, opts runOptions) error {
fmt.Fprintln(os.Stderr, " /show Show model information") fmt.Fprintln(os.Stderr, " /show Show model information")
fmt.Fprintln(os.Stderr, " /load <model> Load a session or model") fmt.Fprintln(os.Stderr, " /load <model> Load a session or model")
fmt.Fprintln(os.Stderr, " /save <model> Save your current session") fmt.Fprintln(os.Stderr, " /save <model> Save your current session")
fmt.Fprintln(os.Stderr, " /export [path] Export current session")
fmt.Fprintln(os.Stderr, " /clear Clear session context") fmt.Fprintln(os.Stderr, " /clear Clear session context")
fmt.Fprintln(os.Stderr, " /bye Exit") fmt.Fprintln(os.Stderr, " /bye Exit")
fmt.Fprintln(os.Stderr, " /?, /help Help for a command") fmt.Fprintln(os.Stderr, " /?, /help Help for a command")
@ -239,6 +241,82 @@ func generateInteractive(cmd *cobra.Command, opts runOptions) error {
} }
fmt.Printf("Created new model '%s'\n", args[1]) fmt.Printf("Created new model '%s'\n", args[1])
continue continue
case strings.HasPrefix(line, "/export"):
args := strings.Fields(line)
if len(opts.Messages) == 0 {
fmt.Println("No conversation to export.")
continue
}
var outPath string
switch len(args) {
case 1:
home, err := os.UserHomeDir()
if err != nil {
fmt.Printf("error: couldn't resolve home directory: %v\n", err)
continue
}
exportDir := filepath.Join(home, ".ollama", "exports")
if err := os.MkdirAll(exportDir, 0o755); err != nil {
fmt.Printf("error: couldn't create export directory: %v\n", err)
continue
}
filename := fmt.Sprintf("chat-%s.txt", time.Now().Format("20060102-150405"))
outPath = filepath.Join(exportDir, filename)
case 2:
p := args[1]
if strings.HasPrefix(p, "~/") || p == "~" {
if home, err := os.UserHomeDir(); err == nil {
p = filepath.Join(home, strings.TrimPrefix(p, "~/"))
}
}
if filepath.IsAbs(p) {
outPath = p
} else {
cwd, err := os.Getwd()
if err != nil {
fmt.Printf("error: couldn't determine working directory: %v\n", err)
continue
}
outPath = filepath.Join(cwd, p)
}
// If no extension specified, default to .txt
if filepath.Ext(outPath) == "" {
outPath += ".txt"
}
if err := os.MkdirAll(filepath.Dir(outPath), 0o755); err != nil {
fmt.Printf("error: couldn't create parent directory: %v\n", err)
continue
}
default:
fmt.Println("Usage:\n /export [path]")
continue
}
if _, err := os.Stat(outPath); err == nil {
fmt.Printf("File exists: %s\nOverwrite? [y/N]: ", outPath)
confirm, err := scanner.Readline()
if err != nil {
fmt.Println()
fmt.Printf("error: %v\n", err)
continue
}
c := strings.ToLower(strings.TrimSpace(confirm))
if c != "y" && c != "yes" {
fmt.Println("Export canceled.")
continue
}
}
transcript := buildTranscript(opts.Model, opts.Messages)
if err := os.WriteFile(outPath, []byte(transcript), 0o644); err != nil {
fmt.Printf("error: couldn't write file: %v\n", err)
continue
}
fmt.Printf("Exported to %s\n", outPath)
continue
case strings.HasPrefix(line, "/clear"): case strings.HasPrefix(line, "/clear"):
opts.Messages = []api.Message{} opts.Messages = []api.Message{}
if opts.System != "" { if opts.System != "" {
@ -631,3 +709,29 @@ func getImageData(filePath string) ([]byte, error) {
return buf, nil return buf, nil
} }
// buildTranscript converts a slice of api.Message to a minimal, readable text transcript.
func buildTranscript(model string, messages []api.Message) string {
var sb strings.Builder
if model != "" {
fmt.Fprintf(&sb, "%s\n\n", model)
}
for i, m := range messages {
role := m.Role
switch role {
case "system":
role = "System"
case "user":
role = "User"
case "assistant":
role = "Assistant"
default:
role = strings.Title(role)
}
fmt.Fprintf(&sb, "%s: %s\n", role, m.Content)
if i < len(messages)-1 {
fmt.Fprintln(&sb)
}
}
return sb.String()
}

View File

@ -5,6 +5,7 @@ import (
"path/filepath" "path/filepath"
"testing" "testing"
"github.com/ollama/ollama/api"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -84,3 +85,14 @@ func TestExtractFileDataRemovesQuotedFilepath(t *testing.T) {
assert.Len(t, imgs, 1) assert.Len(t, imgs, 1)
assert.Equal(t, cleaned, "before after") assert.Equal(t, cleaned, "before after")
} }
func TestBuildTranscript(t *testing.T) {
msgs := []api.Message{
{Role: "system", Content: "You are helpful."},
{Role: "user", Content: "Hello"},
{Role: "assistant", Content: "Hi there!"},
}
got := buildTranscript("llama3", msgs)
want := "llama3\n\nSystem: You are helpful.\n\nUser: Hello\n\nAssistant: Hi there!\n"
assert.Equal(t, want, got)
}